diff --git a/.env b/.env index c5d6c9f2..2846e7d8 100755 --- a/.env +++ b/.env @@ -2,6 +2,9 @@ ENV = 'development' VITE_BASE=/ VUE_APP_PREVIEW=false -VITE_BASE_API=https://xxx.xxx.com -VITE_SOCKET_API=wss://xxx.xxx.com -VUE_APP_WEBSITE_NAME="Lumen IM" \ No newline at end of file +# VITE_BASE_API=https://chat.vlist.cc +VITE_BASE_API=http://127.0.0.1:9503 +# VITE_SOCKET_API=wss://broker.emqx.io:8084 +# VITE_SOCKET_API=mqtt://127.0.0.1:11883 +VITE_SOCKET_API=ws://127.0.0.1:18888 +VUE_APP_WEBSITE_NAME="ChatFlow" \ No newline at end of file diff --git a/.env.electron b/.env.electron index be76da73..fbffedec 100644 --- a/.env.electron +++ b/.env.electron @@ -2,5 +2,5 @@ ENV = 'production' VITE_BASE=./ VITE_ROUTER_MODE=hash -VITE_BASE_API=https://xxx.xxx.com -VITE_SOCKET_API=wss://xxx.xxx.com +VITE_BASE_API=https://chat.vlist.cc +VITE_SOCKET_API=wss://broker.emqx.io:8084 diff --git a/.env.production b/.env.production index 1a87ceac..159bb650 100644 --- a/.env.production +++ b/.env.production @@ -3,5 +3,5 @@ ENV = 'production' VITE_BASE=/ VITE_ROUTER_MODE=history -VITE_BASE_API=https://xxxx.xxx.com -VITE_SOCKET_API=wss://xxxx.xxxx.com \ No newline at end of file +VITE_BASE_API=https://chat.vlist.cc +VITE_SOCKET_API=wss://broker.emqx.io:8084 \ No newline at end of file diff --git a/.github/workflows/Build-and-Deploy-to-GitHub-Pages.yml b/.github/workflows/Build-and-Deploy-to-GitHub-Pages.yml new file mode 100644 index 00000000..81fb3f7f --- /dev/null +++ b/.github/workflows/Build-and-Deploy-to-GitHub-Pages.yml @@ -0,0 +1,61 @@ +name: Build and Deploy to GitHub Pages + +on: + push: + branches: + - main # 触发构建的分支 + workflow_dispatch: + +permissions: # 添加权限配置 + contents: read + id-token: write + pages: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: Install Dependencies + run: npm install + + - name: Build + run: npm run build + + # 推送构建内容到 gh-pages 分支 + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GIT_TOKEN }} + publish_dir: ./dist + + # 在 gh-pages 分支上触发 GitHub Pages 部署 + deploy-to-pages: + runs-on: ubuntu-latest + needs: build-and-deploy # 确保在构建完成后执行 + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v3 + with: + ref: 'gh-pages' + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: '.' + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/Build-and-Deploy-to-Remote-Server.yml b/.github/workflows/Build-and-Deploy-to-Remote-Server.yml new file mode 100644 index 00000000..7b7d0310 --- /dev/null +++ b/.github/workflows/Build-and-Deploy-to-Remote-Server.yml @@ -0,0 +1,38 @@ +name: Build and Deploy to Remote Server + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: Install Dependencies + run: npm install + + - name: Build + run: npm run build + + # 使用用户名和密码连接 SSH + - name: Deploy dist to Remote Server + env: + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_USERNAME: ${{ secrets.SSH_USERNAME }} + SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }} + run: | + sudo apt-get update + sudo apt-get install -y sshpass + sshpass -p $SSH_PASSWORD scp -o StrictHostKeyChecking=no -r ./dist/* $SSH_USERNAME@$SSH_HOST:/etc/nginx/html/ diff --git a/.github/workflows/Build-and-Deploy-to-S3.yml b/.github/workflows/Build-and-Deploy-to-S3.yml new file mode 100644 index 00000000..872c4801 --- /dev/null +++ b/.github/workflows/Build-and-Deploy-to-S3.yml @@ -0,0 +1,38 @@ +# 编译的静态文件部署到S3存储 +name: Node.js Build and Deploy to S3 + +on: + create: + tags: + - '*' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + + - name: Install Dependencies + run: npm install + + - name: Build + run: npm run build + + - name: Deploy to S3 + uses: jakejarvis/s3-sync-action@v0.5.1 + with: + args: --acl public-read --follow-symlinks --delete --endpoint-url ${{ secrets.CUSTOM_S3_ENDPOINT }} + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} # Or your AWS region + SOURCE_DIR: 'dist' diff --git a/.github/workflows/Electron-Build-and-Release.yml b/.github/workflows/Electron-Build-and-Release.yml new file mode 100644 index 00000000..96c5a174 --- /dev/null +++ b/.github/workflows/Electron-Build-and-Release.yml @@ -0,0 +1,58 @@ +name: Electron Build and Release + +# 定义触发事件 +on: + create: + tags: + - 'release-v*' + +# 定义任务 +jobs: + build-and-release: + # 运行环境 + runs-on: ubuntu-latest + # 步骤 + steps: + - uses: actions/checkout@v3 + - name: Set up Node.js + # 设置 Node.js 环境 + uses: actions/setup-node@v3 + with: + node-version: '16.x' + - name: Install Dependencies + # 安装依赖 + run: npm install + - name: Build Electron App + # 构建 Electron 应用,生成 .dmg 和 .exe 文件 + run: npm run electron:build + - name: Create Release + # 创建 GitHub 发布 + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + - name: Upload DMG Asset + # 上传 DMG 文件作为发布的一部分 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./dist_electron/ChatFlow-0.2.0-arm64.dmg + asset_name: ChatFlow-0.2.0-arm64.dmg + asset_content_type: application/octet-stream + - name: Upload EXE Asset + # 上传 EXE 文件作为发布的一部分 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./dist_electron/ChatFlow Setup 0.2.1.exe + asset_name: ChatFlow Setup 0.2.1.exe + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore index da4fb3af..77d5545b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ makefile *.njsproj *.sln *.sw? -makefile +yarn.lock +package-lock.json +package-lock.json +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b7252c6d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# 第一阶段:使用 Node.js 构建项目 +FROM node:latest as builder + +# 设置工作目录 +WORKDIR /app + +# 复制 package.json 和 package-lock.json(如果存在) +COPY package*.json ./ + +# 安装项目依赖 +RUN npm install + +# 复制项目文件 +COPY . . + +# 构建项目 +RUN npm run build + +# 第二阶段:设置 Nginx +FROM nginx:alpine + +# 从 builder 阶段复制构建出的 dist 目录 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 创建一个自定义的 Nginx 配置文件 +RUN echo 'server {\ + listen 80;\ + server_name localhost;\ +\ + location / {\ + root /usr/share/nginx/html;\ + index index.html index.htm;\ + try_files $uri $uri/ /index.html;\ + }\ +\ + location /api/v1 {\ + proxy_pass http://127.0.0.1:9503;\ + }\ +}' > /etc/nginx/conf.d/default.conf + +# 暴露 80 端口 +EXPOSE 80 + +# 启动 Nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index fb5a401f..1b0bfb87 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,61 @@ -# Lumen IM 即时聊天 - +# Chat Studio 对话工作台 -### 项目介绍 -Lumen IM 是一个网页版在线聊天项目,前端使用 Naive UI + Vue3,后端采用 GO 开发。 +[](https://atorber.github.io/chat-studio/) + +## 项目介绍 + +Chat Studio是一个网页版即时聊天系统,界面简约美观。基于[Lumen IM](https://github.com/gzydong/LumenIM)二次开发,后端对接ChatFlow项目。 + +配套后端项目 ChatFlowAdmin [https://github.com/atorber/chatflow-admin](https://github.com/atorber/chatflow-admin) + +访问[项目语雀文档](https://www.yuque.com/atorber/chatflow)了解更多信息 + +## 功能模块 -### 功能模块 - 支持私聊及群聊 - 支持多种聊天消息类型 例如:文本消息、代码块、群投票、图片及其它类型文件,并支持文件下载 - 支持聊天消息撤回、删除(批量删除)、转发消息(逐条转发、合并转发) - 支持编写笔记 -### 项目预览 -- 地址: [http://im.gzydong.com](http://im.gzydong.com) +## 项目预览 + +- 地址: [http://im2.vlist.cc](http://im2.vlist.cc) +- 账号: 维格表空间名称 +- 密码: 维格表token + +## 项目安装(部署) -### 项目安装(部署) +### 下载安装 -###### 下载安装 ```bash ## 克隆项目源码包 -git clone https://gitee.com/gzydong/LumenIM.git -或 -git clone https://github.com/gzydong/LumenIM.git +git clone https://github.com/atorber/chat-studio.git ## 安装项目依赖扩展组件 -yarn install +npm install # 启动本地开发环境 -yarn dev +npm run dev # 启动本地开发环境桌面客户端 -yarn electron:dev +npm run electron:dev ## 生产环境构建项目 -yarn build +npm run build ## 生产环境桌面客户端打包 -yarn electron:build +npm run electron:build ``` -###### 修改 .env 配置信息 +## 效果展示 -```env -VITE_BASE_API=http://127.0.0.1:9503 -VITE_SOCKET_API=ws://127.0.0.1:9504 -``` - -###### 关于 Nginx 的一些配置 -```nginx -server { - listen 80; - server_name www.yourdomain.com; - - root /project-path/dist; - index index.html; + - location / { - try_files $uri $uri/ /index.html; - } - - location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|ico)$ { - expires 7d; - } - - location ~ .*\.(js|css)?$ { - expires 7d; - } -} -``` +## 联系方式 -### 项目源码 -|代码仓库|前端源码|后端源码| -|-|-|-| -|Github|https://github.com/gzydong/LumenIM|https://github.com/gzydong/go-chat| -|码云|https://gitee.com/gzydong/LumenIM|https://gitee.com/gzydong/go-chat| +QQ群 : 583830241 +微信 : ledongmao -#### 联系方式 -QQ作者 : 837215079 +## 如果你觉得还不错,请 Star , Fork 给作者鼓励一下。 -### 如果你觉得还不错,请 Star , Fork 给作者鼓励一下。 \ No newline at end of file +[](https://star-history.com/#atorber/chat-studio&Date) diff --git a/build/getConfigFileName.ts b/build/getConfigFileName.ts new file mode 100644 index 00000000..b0169dc6 --- /dev/null +++ b/build/getConfigFileName.ts @@ -0,0 +1,10 @@ +/** + * Get the configuration file variable name + * @param env + */ +export const getConfigFileName = (env: Record) => { + return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__` + .toUpperCase() + .replace(/\s/g, ''); + }; + \ No newline at end of file diff --git a/build/icons/lumen-im-mac.png b/build/icons/lumen-im-mac.png index 5ec94e90..13f50909 100644 Binary files a/build/icons/lumen-im-mac.png and b/build/icons/lumen-im-mac.png differ diff --git a/build/icons/lumen-im-win.ico b/build/icons/lumen-im-win.ico index 7f8f5dbe..cda4b2a9 100644 Binary files a/build/icons/lumen-im-win.ico and b/build/icons/lumen-im-win.ico differ diff --git a/build/icons/lumenim.icns b/build/icons/lumenim.icns index 2b2bec91..df527f50 100644 Binary files a/build/icons/lumenim.icns and b/build/icons/lumenim.icns differ diff --git a/build/icons/lumenim.ico b/build/icons/lumenim.ico index 7f8f5dbe..cda4b2a9 100644 Binary files a/build/icons/lumenim.ico and b/build/icons/lumenim.ico differ diff --git a/build/icons/lumenim.png b/build/icons/lumenim.png index 5ec94e90..13f50909 100644 Binary files a/build/icons/lumenim.png and b/build/icons/lumenim.png differ diff --git a/components.d.ts b/components.d.ts new file mode 100644 index 00000000..6484a67e --- /dev/null +++ b/components.d.ts @@ -0,0 +1,71 @@ +// generated by unplugin-vue-components +// We suggest you to commit this file into source control +// Read more: https://github.com/vuejs/core/pull/3399 +import '@vue/runtime-core' + +export {} + +declare module '@vue/runtime-core' { + export interface GlobalComponents { + Application: typeof import('./src/components/Application/Application.vue')['default'] + BasicForm: typeof import('./src/components/Form/src/BasicForm.vue')['default'] + BasicModal: typeof import('./src/components/Modal/src/basicModal.vue')['default'] + BasicUpload: typeof import('./src/components/Upload/src/BasicUpload.vue')['default'] + ColumnSetting: typeof import('./src/components/Table/src/components/settings/ColumnSetting.vue')['default'] + CountTo: typeof import('./src/components/CountTo/CountTo.vue')['default'] + EditableCell: typeof import('./src/components/Table/src/components/editable/EditableCell.vue')['default'] + Lockscreen: typeof import('./src/components/Lockscreen/Lockscreen.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] + NAvatar: typeof import('naive-ui')['NAvatar'] + NBackTop: typeof import('naive-ui')['NBackTop'] + NBadge: typeof import('naive-ui')['NBadge'] + NBreadcrumb: typeof import('naive-ui')['NBreadcrumb'] + NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem'] + NButton: typeof import('naive-ui')['NButton'] + NCard: typeof import('naive-ui')['NCard'] + NCheckbox: typeof import('naive-ui')['NCheckbox'] + NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup'] + NCol: typeof import('naive-ui')['NCol'] + NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDataTable: typeof import('naive-ui')['NDataTable'] + NDatePicker: typeof import('naive-ui')['NDatePicker'] + NDialogProvider: typeof import('naive-ui')['NDialogProvider'] + NDivider: typeof import('naive-ui')['NDivider'] + NDrawer: typeof import('naive-ui')['NDrawer'] + NDrawerContent: typeof import('naive-ui')['NDrawerContent'] + NDropdown: typeof import('naive-ui')['NDropdown'] + NForm: typeof import('naive-ui')['NForm'] + NFormItem: typeof import('naive-ui')['NFormItem'] + NGi: typeof import('naive-ui')['NGi'] + NGrid: typeof import('naive-ui')['NGrid'] + NGridItem: typeof import('naive-ui')['NGridItem'] + NIcon: typeof import('naive-ui')['NIcon'] + NInput: typeof import('naive-ui')['NInput'] + NLayout: typeof import('naive-ui')['NLayout'] + NLayoutContent: typeof import('naive-ui')['NLayoutContent'] + NLayoutHeader: typeof import('naive-ui')['NLayoutHeader'] + NLayoutSider: typeof import('naive-ui')['NLayoutSider'] + NMenu: typeof import('naive-ui')['NMenu'] + NMessageProvider: typeof import('naive-ui')['NMessageProvider'] + NModal: typeof import('naive-ui')['NModal'] + NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] + NPopover: typeof import('naive-ui')['NPopover'] + NProgress: typeof import('naive-ui')['NProgress'] + NRadio: typeof import('naive-ui')['NRadio'] + NRadioGroup: typeof import('naive-ui')['NRadioGroup'] + NRow: typeof import('naive-ui')['NRow'] + NSelect: typeof import('naive-ui')['NSelect'] + NSkeleton: typeof import('naive-ui')['NSkeleton'] + NSpace: typeof import('naive-ui')['NSpace'] + NSwitch: typeof import('naive-ui')['NSwitch'] + NTabPane: typeof import('naive-ui')['NTabPane'] + NTabs: typeof import('naive-ui')['NTabs'] + NTag: typeof import('naive-ui')['NTag'] + NTooltip: typeof import('naive-ui')['NTooltip'] + Recharge: typeof import('./src/components/Lockscreen/Recharge.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Table: typeof import('./src/components/Table/src/Table.vue')['default'] + TableAction: typeof import('./src/components/Table/src/components/TableAction.vue')['default'] + } +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 00000000..53e02cb8 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,8 @@ +/// +declare module '*.vue' { + import { ComponentOptions } from 'vue' + const componentOptions: ComponentOptions + export default componentOptions + } + + declare module 'quill-image-uploader' \ No newline at end of file diff --git a/index.html b/index.html index 157560b0..1fe3551e 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Lumen IM + ChatFlow - + + .first-loading-wrap > h1 { + font-size: 128px + } + + .first-loading-wrap .loading-wrap { + padding: 98px; + display: flex; + justify-content: center; + align-items: center + } + + .dot { + animation: antRotate 1.2s infinite linear; + transform: rotate(45deg); + position: relative; + display: inline-block; + font-size: 32px; + width: 32px; + height: 32px; + box-sizing: border-box + } + + .dot i { + width: 14px; + height: 14px; + position: absolute; + display: block; + background-color: #1890ff; + border-radius: 100%; + transform: scale(.75); + transform-origin: 50% 50%; + opacity: .3; + animation: antSpinMove 1s infinite linear alternate + } + + .dot i:nth-child(1) { + top: 0; + left: 0 + } + + .dot i:nth-child(2) { + top: 0; + right: 0; + -webkit-animation-delay: .4s; + animation-delay: .4s + } + + .dot i:nth-child(3) { + right: 0; + bottom: 0; + -webkit-animation-delay: .8s; + animation-delay: .8s + } + + .dot i:nth-child(4) { + bottom: 0; + left: 0; + -webkit-animation-delay: 1.2s; + animation-delay: 1.2s + } + + @keyframes antRotate { + to { + -webkit-transform: rotate(405deg); + transform: rotate(405deg) + } + } + + @-webkit-keyframes antRotate { + to { + -webkit-transform: rotate(405deg); + transform: rotate(405deg) + } + } + + @keyframes antSpinMove { + to { + opacity: 1 + } + } + + @-webkit-keyframes antSpinMove { + to { + opacity: 1 + } + } @@ -71,6 +156,7 @@ + diff --git a/package.json b/package.json index 2e1b1978..b4d93d36 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,45 @@ { - "name": "LumenIM", + "name": "ChatStudio", "private": true, - "version": "0.0.0", + "version": "0.6.8", "main": "electron/main.js", "scripts": { "dev": "vite --mode development", "build": "vite build", "preview": "vite preview", - "electron": "wait-on tcp:5174 && cross-env NODE_ENV=development PROT=5174 electron .", + "electron": "wait-on tcp:5173 --timeout 30000 && cross-env NODE_ENV=development PROT=5173 electron .", "electron:dev": "concurrently -k \"npm run dev\" \"npm run electron\"", - "electron:build": "vite build --mode electron && electron-builder --mac && electron-builder --win --x64", + "electron:build-win": "vite build --mode electron && electron-builder --win --x64", "electron:build-mac": "vite build --mode electron && electron-builder --mac", "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", - "format": "prettier --write src/" + "format": "prettier --write src/", + "dev:host": "npx vite --mode development --host" }, "dependencies": { "@highlightjs/vue-plugin": "^2.1.0", "@kangc/v-md-editor": "^2.3.18", + "@vicons/antd": "^0.12.0", + "@vicons/ionicons5": "^0.12.0", "@vueup/vue-quill": "^1.2.0", + "@vueuse/core": "^10.6.1", + "ant-design-vue": "^3.2.0", "axios": "^1.6.2", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.7", "highlight.js": "^11.5.0", "js-audio-recorder": "^1.0.7", + "paho-mqtt": "^1.1.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.0", "quill": "^1.3.7", "quill-image-uploader": "^1.3.0", "quill-mention": "^3.2.0", - "vue": "^3.3.8", + "showdown": "^2.1.0", + "vue": "^3.3.11", "vue-cropper": "^1.1.1", "vue-router": "^4.2.5", + "vue-types": "^5.1.1", "vue-virtual-scroll-list": "^2.3.5", "vue-virtual-scroller": "^2.0.0-beta.8", "vuedraggable": "^4.1.0", @@ -46,9 +56,10 @@ "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.4.0", + "autoprefixer": "^10.4.16", "concurrently": "^7.3.0", "cross-env": "^7.0.3", - "electron": "^19.1.9", + "electron": "^28.3.3", "electron-builder": "^23.6.0", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", @@ -58,17 +69,19 @@ "less-loader": "^10.2.0", "naive-ui": "^2.35.0", "npm-run-all2": "^6.1.1", + "postcss": "^8.4.32", "prettier": "^3.1.0", + "tailwindcss": "^3.3.6", "typescript": "~5.2.0", - "vite": "^4.5.0", + "vite": "^4.5.1", "vite-plugin-compression": "^0.5.1", - "vue-tsc": "^1.8.19", + "vue-tsc": "^1.8.25", "wait-on": "^6.0.1" }, "build": { "appId": "com.gzydong.lumenim", - "productName": "LumenIM", - "copyright": "Copyright © 2023 LumenIM", + "productName": "ChatFlow", + "copyright": "Copyright © 2023 ChatFlow", "mac": { "category": "public.app-category.utilities", "icon": "build/icons/lumen-im-mac.png" diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/favicon.ico b/public/favicon.ico index 7f8f5dbe..cda4b2a9 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue index ccef8d49..234fee40 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,4 +1,4 @@ - diff --git a/src/components/Application/index.ts b/src/components/Application/index.ts new file mode 100644 index 00000000..5cea04a6 --- /dev/null +++ b/src/components/Application/index.ts @@ -0,0 +1,3 @@ +import AppProvider from './Application.vue'; + +export { AppProvider }; diff --git a/src/components/Chat/ChatCard.vue b/src/components/Chat/ChatCard.vue new file mode 100644 index 00000000..6e29bf9f --- /dev/null +++ b/src/components/Chat/ChatCard.vue @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 选择内容 + + {{ selectText }} + + + + + + + + + + + + + + + + + {{ item }} + + + + + + diff --git a/src/components/Chat/ChatInput.vue b/src/components/Chat/ChatInput.vue new file mode 100644 index 00000000..2d34edd6 --- /dev/null +++ b/src/components/Chat/ChatInput.vue @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Chat/Knowledge.vue b/src/components/Chat/Knowledge.vue new file mode 100644 index 00000000..1ed52fa4 --- /dev/null +++ b/src/components/Chat/Knowledge.vue @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + {{ model ? model.label : '' }} + 请选择 + + + + + +新增模型 + + + {{ item.label }} + + + + + + + + + + + + + diff --git a/src/components/Chat/More.vue b/src/components/Chat/More.vue new file mode 100644 index 00000000..73e7837e --- /dev/null +++ b/src/components/Chat/More.vue @@ -0,0 +1,41 @@ + + + + + + + + + 点击加载更多 + + + diff --git a/src/components/Chat/chat.vue b/src/components/Chat/chat.vue new file mode 100644 index 00000000..c4d6299b --- /dev/null +++ b/src/components/Chat/chat.vue @@ -0,0 +1,861 @@ + + + + + + + + + + + + + + + + + + 新会话 + + + + 会话记录 + + + + + + + + + + + + 会话历史记录 + + + + + + + + + + + + {{ item.title || '新会话' }} + + + + + + {{ dayjs(item.gmt_update).toNow() }} + + + {{ item.last_message }} + + + + + + + + + + + + + + diff --git a/src/components/Chat/index.vue b/src/components/Chat/index.vue new file mode 100644 index 00000000..74ba1181 --- /dev/null +++ b/src/components/Chat/index.vue @@ -0,0 +1,310 @@ + + + + + + + Chat Copilot + + 有版本更新,点击下载 + + + + + + + + + + + + + + + + + + + 聊天 + + + 知识库 + + + 摘要 + + + 翻译 + + + + + + + + + + + diff --git a/src/components/Chat/summary.vue b/src/components/Chat/summary.vue new file mode 100644 index 00000000..ecebb9a7 --- /dev/null +++ b/src/components/Chat/summary.vue @@ -0,0 +1,751 @@ + + + + + + + + + + + + + + + + + + 新会话 + + + + 会话记录 + + + + + + + + + + + + 会话历史记录 + + + + + + + + + + + + {{ item.title || "新会话" }} + + + + + + {{ + dayjs(item.gmt_update).toNow() + }} + + + {{ + item.last_message + }} + + + + + + + + + + + + + + diff --git a/src/components/Chat/summaryCard.vue b/src/components/Chat/summaryCard.vue new file mode 100644 index 00000000..4e9c7c95 --- /dev/null +++ b/src/components/Chat/summaryCard.vue @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 选择内容 + + {{ selectText }} + + + + + + + + + + + + + + + + + {{ item }} + + + + + + diff --git a/src/components/CountTo/CountTo.vue b/src/components/CountTo/CountTo.vue new file mode 100644 index 00000000..9576a82a --- /dev/null +++ b/src/components/CountTo/CountTo.vue @@ -0,0 +1,110 @@ + + + {{ value }} + + + diff --git a/src/components/CountTo/index.ts b/src/components/CountTo/index.ts new file mode 100644 index 00000000..946e5252 --- /dev/null +++ b/src/components/CountTo/index.ts @@ -0,0 +1,4 @@ +import { withInstall } from '@/utils'; +import countTo from './CountTo.vue'; + +export const CountTo = withInstall(countTo); diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts new file mode 100644 index 00000000..0f37df5d --- /dev/null +++ b/src/components/Form/index.ts @@ -0,0 +1,4 @@ +export { default as BasicForm } from './src/BasicForm.vue'; +export { useForm } from './src/hooks/useForm'; +export * from './src/types/form'; +export * from './src/types/index'; diff --git a/src/components/Form/src/BasicForm.vue b/src/components/Form/src/BasicForm.vue new file mode 100644 index 00000000..f108e8f4 --- /dev/null +++ b/src/components/Form/src/BasicForm.vue @@ -0,0 +1,325 @@ + + + + + + + + {{ schema.label }} + + + + + + + {{ schema.labelMessage }} + + + + + + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + + + + + + + + + + + + + {{ getProps.submitButtonText }} + {{ getProps.resetButtonText }} + + + + + + + + + + {{ overflow ? '展开' : '收起' }} + + + + + + + + + + diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts new file mode 100644 index 00000000..5db0e32e --- /dev/null +++ b/src/components/Form/src/helper.ts @@ -0,0 +1,42 @@ +import { ComponentType } from './types/index'; + +/** + * @description: 生成placeholder + */ +export function createPlaceholderMessage(component: ComponentType) { + if (component === 'NInput') return '请输入'; + if ( + ['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes( + component + ) + ) + return '请选择'; + return ''; +} + +const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker']; + +function genType() { + return [...DATE_TYPE, 'RangePicker']; +} + +/** + * 时间字段 + */ +export const dateItemType = genType(); + +export function defaultType(component) { + if (component === 'NInput') return ''; + if (component === 'NInputNumber') return null; + return [ + 'NPicker', + 'NSelect', + 'NCheckbox', + 'NRadio', + 'NSwitch', + 'NDatePicker', + 'NTimePicker', + ].includes(component) + ? '' + : undefined; +} diff --git a/src/components/Form/src/hooks/useForm.ts b/src/components/Form/src/hooks/useForm.ts new file mode 100644 index 00000000..da279fd9 --- /dev/null +++ b/src/components/Form/src/hooks/useForm.ts @@ -0,0 +1,95 @@ +import type { FormProps, FormActionType, UseFormReturnType } from '../types/form'; +import type { DynamicProps } from '/#/utils'; + +import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; +import { isProdMode } from '@/utils/env'; +import { getDynamicProps } from '@/utils'; + +type Props = Partial>; + +export function useForm(props?: Props): UseFormReturnType { + const formRef = ref>(null); + const loadedRef = ref>(false); + + async function getForm() { + const form = unref(formRef); + if (!form) { + console.error( + 'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!' + ); + } + await nextTick(); + return form as FormActionType; + } + + function register(instance: FormActionType) { + isProdMode() && + onUnmounted(() => { + formRef.value = null; + loadedRef.value = null; + }); + if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return; + + formRef.value = instance; + loadedRef.value = true; + + watch( + () => props, + () => { + props && instance.setProps(getDynamicProps(props)); + }, + { + immediate: true, + deep: true, + } + ); + } + + const methods: FormActionType = { + setProps: async (formProps: Partial) => { + const form = await getForm(); + await form.setProps(formProps); + }, + + resetFields: async () => { + getForm().then(async (form) => { + await form.resetFields(); + }); + }, + + clearValidate: async (name?: string | string[]) => { + const form = await getForm(); + await form.clearValidate(name); + }, + + getFieldsValue: () => { + return unref(formRef)?.getFieldsValue() as T; + }, + + setFieldsValue: async (values: T) => { + const form = await getForm(); + await form.setFieldsValue(values); + }, + + submit: async (): Promise => { + const form = await getForm(); + return form.submit(); + }, + + validate: async (nameList?: any[]): Promise => { + const form = await getForm(); + return form.validate(nameList); + }, + + setLoading: (value: boolean) => { + loadedRef.value = value; + }, + + setSchema: async (values) => { + const form = await getForm(); + form.setSchema(values); + }, + }; + + return [register, methods]; +} diff --git a/src/components/Form/src/hooks/useFormContext.ts b/src/components/Form/src/hooks/useFormContext.ts new file mode 100644 index 00000000..2d2a212b --- /dev/null +++ b/src/components/Form/src/hooks/useFormContext.ts @@ -0,0 +1,11 @@ +import { provide, inject } from 'vue'; + +const key = Symbol('formElRef'); + +export function createFormContext(instance) { + provide(key, instance); +} + +export function useFormContext() { + return inject(key); +} diff --git a/src/components/Form/src/hooks/useFormEvents.ts b/src/components/Form/src/hooks/useFormEvents.ts new file mode 100644 index 00000000..61e44a65 --- /dev/null +++ b/src/components/Form/src/hooks/useFormEvents.ts @@ -0,0 +1,116 @@ +import type { ComputedRef, Ref } from 'vue'; +import type { FormProps, FormSchema, FormActionType } from '../types/form'; +import { unref, toRaw } from 'vue'; +import { isFunction } from '@/utils/is'; + +declare type EmitType = (event: string, ...args: any[]) => void; + +interface UseFormActionContext { + emit: EmitType; + getProps: ComputedRef; + getSchema: ComputedRef; + formModel: Recordable; + formElRef: Ref; + defaultFormModel: Recordable; + loadingSub: Ref; + handleFormValues: Function; +} + +export function useFormEvents({ + emit, + getProps, + formModel, + getSchema, + formElRef, + defaultFormModel, + loadingSub, + handleFormValues, +}: UseFormActionContext) { + // 验证 + async function validate() { + return unref(formElRef)?.validate(); + } + + // 提交 + async function handleSubmit(e?: Event): Promise { + e && e.preventDefault(); + loadingSub.value = true; + const { submitFunc } = unref(getProps); + if (submitFunc && isFunction(submitFunc)) { + await submitFunc(); + loadingSub.value = false; + return false; + } + const formEl = unref(formElRef); + if (!formEl) return false; + try { + await validate(); + const values = getFieldsValue(); + loadingSub.value = false; + emit('submit', values); + return values; + } catch (error: any) { + emit('submit', false); + loadingSub.value = false; + console.error(error); + return false; + } + } + + //清空校验 + async function clearValidate() { + // @ts-ignore + await unref(formElRef)?.restoreValidation(); + } + + //重置 + async function resetFields(): Promise { + const { resetFunc, submitOnReset } = unref(getProps); + resetFunc && isFunction(resetFunc) && (await resetFunc()); + + const formEl = unref(formElRef); + if (!formEl) return; + Object.keys(formModel).forEach((key) => { + formModel[key] = unref(defaultFormModel)[key] || null; + }); + await clearValidate(); + const fromValues = handleFormValues(toRaw(unref(formModel))); + emit('reset', fromValues); + submitOnReset && (await handleSubmit()); + } + + //获取表单值 + function getFieldsValue(): Recordable { + const formEl = unref(formElRef); + if (!formEl) return {}; + return handleFormValues(toRaw(unref(formModel))); + } + + //设置表单字段值 + async function setFieldsValue(values: Recordable): Promise { + const fields = unref(getSchema) + .map((item) => item.field) + .filter(Boolean); + + Object.keys(values).forEach((key) => { + const value = values[key]; + if (fields.includes(key)) { + formModel[key] = value; + } + }); + } + + function setLoading(value: boolean): void { + loadingSub.value = value; + } + + return { + handleSubmit, + validate, + resetFields, + getFieldsValue, + clearValidate, + setFieldsValue, + setLoading, + }; +} diff --git a/src/components/Form/src/hooks/useFormValues.ts b/src/components/Form/src/hooks/useFormValues.ts new file mode 100644 index 00000000..2d45bbe9 --- /dev/null +++ b/src/components/Form/src/hooks/useFormValues.ts @@ -0,0 +1,54 @@ +import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is'; +import { unref } from 'vue'; +import type { Ref, ComputedRef } from 'vue'; +import type { FormSchema } from '../types/form'; +import { set } from 'lodash-es'; + +interface UseFormValuesContext { + defaultFormModel: Ref; + getSchema: ComputedRef; + formModel: Recordable; +} +export function useFormValues({ defaultFormModel, getSchema, formModel }: UseFormValuesContext) { + // 加工 form values + function handleFormValues(values: Recordable) { + if (!isObject(values)) { + return {}; + } + const res: Recordable = {}; + for (const item of Object.entries(values)) { + let [, value] = item; + const [key] = item; + if ( + !key || + (isArray(value) && value.length === 0) || + isFunction(value) || + isNullOrUnDef(value) + ) { + continue; + } + // 删除空格 + if (isString(value)) { + value = value.trim(); + } + set(res, key, value); + } + return res; + } + + //初始化默认值 + function initDefault() { + const schemas = unref(getSchema); + const obj: Recordable = {}; + schemas.forEach((item) => { + const { defaultValue } = item; + if (!isNullOrUnDef(defaultValue)) { + obj[item.field] = defaultValue; + formModel[item.field] = defaultValue; + } + }); + defaultFormModel.value = obj; + } + + return { handleFormValues, initDefault }; +} diff --git a/src/components/Form/src/props.ts b/src/components/Form/src/props.ts new file mode 100644 index 00000000..46582e7d --- /dev/null +++ b/src/components/Form/src/props.ts @@ -0,0 +1,82 @@ +import type { CSSProperties, PropType } from 'vue'; +import { FormSchema } from './types/form'; +import type { GridProps, GridItemProps } from 'naive-ui/lib/grid'; +import type { ButtonProps } from 'naive-ui/lib/button'; +import { propTypes } from '@/utils/propTypes'; +export const basicProps = { + // 标签宽度 固定宽度 + labelWidth: { + type: [Number, String] as PropType, + default: 80, + }, + // 表单配置规则 + schemas: { + type: [Array] as PropType, + default: () => [], + }, + //布局方式 + layout: { + type: String, + default: 'inline', + }, + //是否展示为行内表单 + inline: { + type: Boolean, + default: false, + }, + //大小 + size: { + type: String, + default: 'medium', + }, + //标签位置 + labelPlacement: { + type: String, + default: 'left', + }, + //组件是否width 100% + isFull: { + type: Boolean, + default: true, + }, + //是否显示操作按钮(查询/重置) + showActionButtonGroup: propTypes.bool.def(true), + // 显示重置按钮 + showResetButton: propTypes.bool.def(true), + //重置按钮配置 + resetButtonOptions: Object as PropType>, + // 显示确认按钮 + showSubmitButton: propTypes.bool.def(true), + // 确认按钮配置 + submitButtonOptions: Object as PropType>, + //展开收起按钮 + showAdvancedButton: propTypes.bool.def(true), + // 确认按钮文字 + submitButtonText: { + type: String, + default: '查询', + }, + //重置按钮文字 + resetButtonText: { + type: String, + default: '重置', + }, + //grid 配置 + gridProps: Object as PropType, + //gi配置 + giProps: Object as PropType, + //grid 样式 + baseGridStyle: { + type: Object as PropType, + }, + //是否折叠 + collapsed: { + type: Boolean, + default: false, + }, + //默认展示的行数 + collapsedRows: { + type: Number, + default: 1, + }, +}; diff --git a/src/components/Form/src/types/form.ts b/src/components/Form/src/types/form.ts new file mode 100644 index 00000000..d4c70cca --- /dev/null +++ b/src/components/Form/src/types/form.ts @@ -0,0 +1,61 @@ +import { ComponentType } from './index'; +import type { CSSProperties } from 'vue'; +import type { GridProps, GridItemProps } from 'naive-ui/lib/grid'; +import type { ButtonProps } from 'naive-ui/lib/button'; + +export interface FormSchema { + field: string; + label: string; + labelMessage?: string; + labelMessageStyle?: object | string; + defaultValue?: any; + component?: ComponentType; + componentProps?: object; + slot?: string; + rules?: object | object[]; + giProps?: GridItemProps; + isFull?: boolean; + suffix?: string; +} + +export interface FormProps { + model?: Recordable; + labelWidth?: number | string; + schemas?: FormSchema[]; + inline: boolean; + layout?: string; + size: string; + labelPlacement: string; + isFull: boolean; + showActionButtonGroup?: boolean; + showResetButton?: boolean; + resetButtonOptions?: Partial; + showSubmitButton?: boolean; + showAdvancedButton?: boolean; + submitButtonOptions?: Partial; + submitButtonText?: string; + resetButtonText?: string; + gridProps?: GridProps; + giProps?: GridItemProps; + resetFunc?: () => Promise; + submitFunc?: () => Promise; + submitOnReset?: boolean; + baseGridStyle?: CSSProperties; + collapsedRows?: number; +} + +export interface FormActionType { + submit: () => Promise; + setProps: (formProps: Partial) => Promise; + setSchema: (schemaProps: Partial) => Promise; + setFieldsValue: (values: Recordable) => void; + clearValidate: (name?: string | string[]) => Promise; + getFieldsValue: () => Recordable; + resetFields: () => Promise; + validate: (nameList?: any[]) => Promise; + setLoading: (status: boolean) => void; +} + +export type RegisterFn = (formInstance: FormActionType) => void; + +export type UseFormReturnType = [RegisterFn, FormActionType]; diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts new file mode 100644 index 00000000..5cb0baa0 --- /dev/null +++ b/src/components/Form/src/types/index.ts @@ -0,0 +1,28 @@ +export type ComponentType = + | 'NInput' + | 'NInputGroup' + | 'NInputPassword' + | 'NInputSearch' + | 'NInputTextArea' + | 'NInputNumber' + | 'NInputCountDown' + | 'NSelect' + | 'NTreeSelect' + | 'NRadioButtonGroup' + | 'NRadioGroup' + | 'NCheckbox' + | 'NCheckboxGroup' + | 'NAutoComplete' + | 'NCascader' + | 'NDatePicker' + | 'NMonthPicker' + | 'NRangePicker' + | 'NWeekPicker' + | 'NTimePicker' + | 'NSwitch' + | 'NStrengthMeter' + | 'NUpload' + | 'NIconPicker' + | 'NRender' + | 'NSlider' + | 'NRate'; diff --git a/src/components/Lockscreen/Lockscreen.vue b/src/components/Lockscreen/Lockscreen.vue new file mode 100644 index 00000000..bf3dde53 --- /dev/null +++ b/src/components/Lockscreen/Lockscreen.vue @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + {{ hour }}:{{ minute }} + {{ month }}月{{ day }}号,星期{{ week }} + + + + + + + + + + + + + + + + + + {{ loginParams.username }} + + + + + + + + + + + {{ errorMsg }} + + + + 返回 + 重新登录 + 进入系统 + + + + + + + + + diff --git a/src/components/Lockscreen/Recharge.vue b/src/components/Lockscreen/Recharge.vue new file mode 100644 index 00000000..4eef4438 --- /dev/null +++ b/src/components/Lockscreen/Recharge.vue @@ -0,0 +1,164 @@ + + + {{ battery.level }}% + + + + + + + + {{ batteryStatus }} + + 剩余可使用时间:{{ calcDischargingTime }} + + + 距离电池充满需要:{{ calcChargingTime }} + + + + + + + + diff --git a/src/components/Lockscreen/index.ts b/src/components/Lockscreen/index.ts new file mode 100644 index 00000000..7e086dcc --- /dev/null +++ b/src/components/Lockscreen/index.ts @@ -0,0 +1,3 @@ +import LockScreen from './Lockscreen.vue'; + +export { LockScreen }; diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts new file mode 100644 index 00000000..586a945e --- /dev/null +++ b/src/components/Modal/index.ts @@ -0,0 +1,3 @@ +export { default as basicModal } from './src/basicModal.vue'; +export { useModal } from './src/hooks/useModal'; +export * from './src/type'; diff --git a/src/components/Modal/src/basicModal.vue b/src/components/Modal/src/basicModal.vue new file mode 100644 index 00000000..7ee81a60 --- /dev/null +++ b/src/components/Modal/src/basicModal.vue @@ -0,0 +1,107 @@ + + + + {{ getBindValue.title }} + + + + + + + 取消 + {{ + subBtuText + }} + + + + + + + + + + + diff --git a/src/components/Modal/src/hooks/useModal.ts b/src/components/Modal/src/hooks/useModal.ts new file mode 100644 index 00000000..0b737d61 --- /dev/null +++ b/src/components/Modal/src/hooks/useModal.ts @@ -0,0 +1,54 @@ +import { ref, unref, getCurrentInstance, watch } from 'vue'; +import { isProdMode } from '@/utils/env'; +import { ModalMethods, UseModalReturnType } from '../type'; +import { getDynamicProps } from '@/utils'; +import { tryOnUnmounted } from '@vueuse/core'; +export function useModal(props): UseModalReturnType { + const modalRef = ref>(null); + const currentInstance = getCurrentInstance(); + + const getInstance = () => { + const instance = unref(modalRef.value); + if (!instance) { + console.error('useModal instance is undefined!'); + } + return instance; + }; + + const register = (modalInstance: ModalMethods) => { + isProdMode() && + tryOnUnmounted(() => { + modalRef.value = null; + }); + modalRef.value = modalInstance; + currentInstance?.emit('register', modalInstance); + + watch( + () => props, + () => { + props && modalInstance.setProps(getDynamicProps(props)); + }, + { + immediate: true, + deep: true, + } + ); + }; + + const methods: ModalMethods = { + setProps: (props): void => { + getInstance()?.setProps(props); + }, + openModal: () => { + getInstance()?.openModal(); + }, + closeModal: () => { + getInstance()?.closeModal(); + }, + setSubLoading: (status) => { + getInstance()?.setSubLoading(status); + }, + }; + + return [register, methods]; +} diff --git a/src/components/Modal/src/props.ts b/src/components/Modal/src/props.ts new file mode 100644 index 00000000..9fafa12c --- /dev/null +++ b/src/components/Modal/src/props.ts @@ -0,0 +1,30 @@ +import { NModal } from 'naive-ui'; + +export const basicProps = { + ...NModal.props, + // 确认按钮文字 + subBtuText: { + type: String, + default: '确认', + }, + showIcon: { + type: Boolean, + default: false, + }, + width: { + type: Number, + default: 446, + }, + title: { + type: String, + default: '', + }, + maskClosable: { + type: Boolean, + default: false, + }, + preset: { + type: String, + default: 'dialog', + }, +}; diff --git a/src/components/Modal/src/type/index.ts b/src/components/Modal/src/type/index.ts new file mode 100644 index 00000000..804e7417 --- /dev/null +++ b/src/components/Modal/src/type/index.ts @@ -0,0 +1,19 @@ +import type { DialogOptions } from 'naive-ui/lib/dialog'; +/** + * @description: 弹窗对外暴露的方法 + */ +export interface ModalMethods { + setProps: (props) => void; + openModal: () => void; + closeModal: () => void; + setSubLoading: (status) => void; +} + +/** + * 支持修改,DialogOptions 參數 + */ +export type ModalProps = DialogOptions; + +export type RegisterFn = (ModalInstance: ModalMethods) => void; + +export type UseModalReturnType = [RegisterFn, ModalMethods]; diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts new file mode 100644 index 00000000..38725adb --- /dev/null +++ b/src/components/Table/index.ts @@ -0,0 +1,4 @@ +export { default as BasicTable } from './src/Table.vue'; +export { default as TableAction } from './src/components/TableAction.vue'; +export * from './src/types/table'; +export * from './src/types/tableAction'; diff --git a/src/components/Table/src/Table.vue b/src/components/Table/src/Table.vue new file mode 100644 index 00000000..75e1293a --- /dev/null +++ b/src/components/Table/src/Table.vue @@ -0,0 +1,353 @@ + + + + + + + {{ title }} + + + + + + + {{ titleTooltip }} + + + + + + + + + + + + + + + + + + 表格斑马纹 + + + + + + + + + + + + + 刷新 + + + + + + + + + + + + + + 密度 + + + + + + + + + + + + + + + + + diff --git a/src/components/Table/src/componentMap.ts b/src/components/Table/src/componentMap.ts new file mode 100644 index 00000000..9fbb3fd6 --- /dev/null +++ b/src/components/Table/src/componentMap.ts @@ -0,0 +1,41 @@ +import type { Component } from 'vue'; +import { + NInput, + NSelect, + NCheckbox, + NInputNumber, + NSwitch, + NDatePicker, + NTimePicker, +} from 'naive-ui'; +import type { ComponentType } from './types/componentType'; + +export enum EventEnum { + NInput = 'on-input', + NInputNumber = 'on-input', + NSelect = 'on-update:value', + NSwitch = 'on-update:value', + NCheckbox = 'on-update:value', + NDatePicker = 'on-update:value', + NTimePicker = 'on-update:value', +} + +const componentMap = new Map(); + +componentMap.set('NInput', NInput); +componentMap.set('NInputNumber', NInputNumber); +componentMap.set('NSelect', NSelect); +componentMap.set('NSwitch', NSwitch); +componentMap.set('NCheckbox', NCheckbox); +componentMap.set('NDatePicker', NDatePicker); +componentMap.set('NTimePicker', NTimePicker); + +export function add(compName: ComponentType, component: Component) { + componentMap.set(compName, component); +} + +export function del(compName: ComponentType) { + componentMap.delete(compName); +} + +export { componentMap }; diff --git a/src/components/Table/src/components/TableAction.vue b/src/components/Table/src/components/TableAction.vue new file mode 100644 index 00000000..cf0bc523 --- /dev/null +++ b/src/components/Table/src/components/TableAction.vue @@ -0,0 +1,139 @@ + + + + + + {{ action.label }} + + + + + + + + + + 更多 + + + + + + + + + + + + + + diff --git a/src/components/Table/src/components/editable/CellComponent.ts b/src/components/Table/src/components/editable/CellComponent.ts new file mode 100644 index 00000000..8085e0cc --- /dev/null +++ b/src/components/Table/src/components/editable/CellComponent.ts @@ -0,0 +1,47 @@ +import type { FunctionalComponent, defineComponent } from 'vue'; +import type { ComponentType } from '../../types/componentType'; +import { componentMap } from '@/components/Table/src/componentMap'; + +import { h } from 'vue'; + +import { NPopover } from 'naive-ui'; + +export interface ComponentProps { + component: ComponentType; + rule: boolean; + popoverVisible: boolean; + ruleMessage: string; +} + +export const CellComponent: FunctionalComponent = ( + { component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps, + { attrs } +) => { + const Comp = componentMap.get(component) as typeof defineComponent; + + const DefaultComp = h(Comp, attrs); + if (!rule) { + return DefaultComp; + } + return h( + NPopover, + { 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' }, + { + trigger: () => DefaultComp, + default: () => + h( + 'span', + { + style: { + color: 'red', + width: '90px', + display: 'inline-block', + }, + }, + { + default: () => ruleMessage, + } + ), + } + ); +}; diff --git a/src/components/Table/src/components/editable/EditableCell.vue b/src/components/Table/src/components/editable/EditableCell.vue new file mode 100644 index 00000000..715f940f --- /dev/null +++ b/src/components/Table/src/components/editable/EditableCell.vue @@ -0,0 +1,418 @@ + + + + {{ getValues }} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Table/src/components/editable/helper.ts b/src/components/Table/src/components/editable/helper.ts new file mode 100644 index 00000000..48e7682b --- /dev/null +++ b/src/components/Table/src/components/editable/helper.ts @@ -0,0 +1,15 @@ +import { ComponentType } from '../../types/componentType'; + +/** + * @description: 生成placeholder + */ +export function createPlaceholderMessage(component: ComponentType) { + if (component === 'NInput') return '请输入'; + if ( + ['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes( + component + ) + ) + return '请选择'; + return ''; +} diff --git a/src/components/Table/src/components/editable/index.ts b/src/components/Table/src/components/editable/index.ts new file mode 100644 index 00000000..064763dd --- /dev/null +++ b/src/components/Table/src/components/editable/index.ts @@ -0,0 +1,49 @@ +import type { BasicColumn } from '@/components/Table/src/types/table'; +import { h, Ref } from 'vue'; + +import EditableCell from './EditableCell.vue'; + +export function renderEditCell(column: BasicColumn) { + return (record, index) => { + const _key = column.key; + const value = record[_key]; + record.onEdit = async (edit: boolean, submit = false) => { + if (!submit) { + record.editable = edit; + } + + if (!edit && submit) { + const res = await record.onSubmitEdit?.(); + if (res) { + record.editable = false; + return true; + } + return false; + } + // cancel + if (!edit && !submit) { + record.onCancelEdit?.(); + } + return true; + }; + return h(EditableCell, { + value, + record, + column, + index, + }); + }; +} + +export type EditRecordRow = Partial< + { + onEdit: (editable: boolean, submit?: boolean) => Promise; + editable: boolean; + onCancel: Fn; + onSubmit: Fn; + submitCbs: Fn[]; + cancelCbs: Fn[]; + validCbs: Fn[]; + editValueRefs: Recordable; + } & T +>; diff --git a/src/components/Table/src/components/settings/ColumnSetting.vue b/src/components/Table/src/components/settings/ColumnSetting.vue new file mode 100644 index 00000000..8963ac23 --- /dev/null +++ b/src/components/Table/src/components/settings/ColumnSetting.vue @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + 列展示 + 勾选列 + 重置 + + + + + + + + + + + + + + + + + + + + + + 固定到左侧 + + + + + + + + + 固定到右侧 + + + + + + + + + + + 列设置 + + + + + + diff --git a/src/components/Table/src/const.ts b/src/components/Table/src/const.ts new file mode 100644 index 00000000..82ebca69 --- /dev/null +++ b/src/components/Table/src/const.ts @@ -0,0 +1,11 @@ +import componentSetting from '@/settings/componentSetting'; + +const { table } = componentSetting; + +const { apiSetting, defaultPageSize, pageSizes } = table; + +export const DEFAULTPAGESIZE = defaultPageSize; + +export const APISETTING = apiSetting; + +export const PAGESIZES = pageSizes; diff --git a/src/components/Table/src/hooks/useColumns.ts b/src/components/Table/src/hooks/useColumns.ts new file mode 100644 index 00000000..13ffaf7c --- /dev/null +++ b/src/components/Table/src/hooks/useColumns.ts @@ -0,0 +1,162 @@ +import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue'; +import type { BasicColumn, BasicTableProps } from '../types/table'; +import { isEqual, cloneDeep } from 'lodash-es'; +import { isArray, isString, isBoolean, isFunction } from '@/utils/is'; +import { ActionItem } from '@/components/Table'; +import { renderEditCell } from '../components/editable'; +import { NTooltip, NIcon } from 'naive-ui'; +import { FormOutlined } from '@vicons/antd'; + +export function useColumns(propsRef: ComputedRef) { + const columnsRef = ref(unref(propsRef).columns) as unknown as Ref; + let cacheColumns = unref(propsRef).columns; + + const getColumnsRef = computed(() => { + const columns = cloneDeep(unref(columnsRef)); + + handleActionColumn(propsRef, columns); + if (!columns) return []; + return columns; + }); + + function isIfShow(action: ActionItem): boolean { + const ifShow = action.ifShow; + + let isIfShow = true; + + if (isBoolean(ifShow)) { + isIfShow = ifShow; + } + if (isFunction(ifShow)) { + isIfShow = ifShow(action); + } + return isIfShow; + } + + const renderTooltip = (trigger, content) => { + return h(NTooltip, null, { + trigger: () => trigger, + default: () => content, + }); + }; + + const getPageColumns = computed(() => { + const pageColumns = unref(getColumnsRef); + const columns = cloneDeep(pageColumns); + return columns + .filter((column) => { + // return hasPermission(column.auth as string[]) && isIfShow(column); + return true + }) + .map((column) => { + //默认 ellipsis 为true + column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false; + const { edit } = column; + if (edit) { + column.render = renderEditCell(column); + if (edit) { + const title: any = column.title; + column.title = () => { + return renderTooltip( + h('span', {}, [ + h('span', { style: { 'margin-right': '5px' } }, title), + h( + NIcon, + { + size: 14, + }, + { + default: () => h(FormOutlined), + } + ), + ]), + '该列可编辑' + ); + }; + } + } + return column; + }); + }); + + watch( + () => unref(propsRef).columns, + (columns) => { + columnsRef.value = columns; + cacheColumns = columns; + } + ); + + function handleActionColumn(propsRef: ComputedRef, columns: BasicColumn[]) { + const { actionColumn } = unref(propsRef); + if (!actionColumn) return; + !columns.find((col) => col.key === 'action') && + columns.push({ + ...(actionColumn as any), + }); + } + + //设置 + function setColumns(columnList: string[]) { + const columns: any[] = cloneDeep(columnList); + if (!isArray(columns)) return; + + if (!columns.length) { + columnsRef.value = []; + return; + } + const cacheKeys = cacheColumns.map((item) => item.key); + //针对拖拽排序 + if (!isString(columns[0])) { + columnsRef.value = columns; + } else { + const newColumns: any[] = []; + cacheColumns.forEach((item) => { + if (columnList.includes(item.key)) { + newColumns.push({ ...item }); + } + }); + if (!isEqual(cacheKeys, columns)) { + newColumns.sort((prev, next) => { + return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key); + }); + } + columnsRef.value = newColumns; + } + } + + //获取 + function getColumns(): BasicColumn[] { + const columns = toRaw(unref(getColumnsRef)); + return columns.map((item) => { + return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }; + }); + } + + //获取原始 + function getCacheColumns(isKey?: boolean): any[] { + return isKey ? cacheColumns.map((item) => item.key) : cacheColumns; + } + + //更新原始数据单个字段 + function setCacheColumnsField(key: string | undefined, value: Partial) { + if (!key || !value) { + return; + } + cacheColumns.forEach((item) => { + if (item.key === key) { + Object.assign(item, value); + return; + } + }); + } + + return { + getColumnsRef, + getCacheColumns, + setCacheColumnsField, + setColumns, + getColumns, + getPageColumns, + }; +} diff --git a/src/components/Table/src/hooks/useDataSource.ts b/src/components/Table/src/hooks/useDataSource.ts new file mode 100644 index 00000000..9bf628e4 --- /dev/null +++ b/src/components/Table/src/hooks/useDataSource.ts @@ -0,0 +1,150 @@ +import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue'; +import type { BasicTableProps } from '../types/table'; +import type { PaginationProps } from '../types/pagination'; +import { isBoolean, isFunction, isArray } from '@/utils/is'; +import { APISETTING } from '../const'; + +export function useDataSource( + propsRef: ComputedRef, + { getPaginationInfo, setPagination, setLoading, tableData }, + emit +) { + const dataSourceRef = ref([]); + + watchEffect(() => { + tableData.value = unref(dataSourceRef); + }); + + watch( + () => unref(propsRef).dataSource, + () => { + const { dataSource }: any = unref(propsRef); + dataSource && (dataSourceRef.value = dataSource); + }, + { + immediate: true, + } + ); + + const getRowKey = computed(() => { + const { rowKey }: any = unref(propsRef); + return rowKey + ? rowKey + : () => { + return 'key'; + }; + }); + + const getDataSourceRef = computed(() => { + const dataSource = unref(dataSourceRef); + if (!dataSource || dataSource.length === 0) { + return unref(dataSourceRef); + } + return unref(dataSourceRef); + }); + + async function fetch(opt?) { + try { + setLoading(true); + const { request, pagination, beforeRequest, afterRequest }: any = unref(propsRef); + if (!request) return; + //组装分页信息 + const pageField = APISETTING.pageField; + const sizeField = APISETTING.sizeField; + const totalField = APISETTING.totalField; + const listField = APISETTING.listField; + const itemCount = APISETTING.countField; + let pageParams = {}; + const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps; + + if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) { + pageParams = {}; + } else { + pageParams[pageField] = (opt && opt[pageField]) || page; + pageParams[sizeField] = pageSize; + } + + let params = { + ...pageParams, + ...opt, + }; + if (beforeRequest && isFunction(beforeRequest)) { + // The params parameter can be modified by outsiders + params = (await beforeRequest(params)) || params; + } + const res = await request(params); + const resultTotal = res[totalField]; + const currentPage = res[pageField]; + const total = res[itemCount]; + const results = res[listField] ? res[listField] : []; + + // 如果数据异常,需获取正确的页码再次执行 + if (resultTotal) { + const currentTotalPage = Math.ceil(total / pageSize); + if (page > currentTotalPage) { + setPagination({ + page: currentTotalPage, + itemCount: total, + }); + return await fetch(opt); + } + } + let resultInfo = res[listField] ? res[listField] : []; + if (afterRequest && isFunction(afterRequest)) { + // can modify the data returned by the interface for processing + resultInfo = (await afterRequest(resultInfo)) || resultInfo; + } + dataSourceRef.value = resultInfo; + setPagination({ + page: currentPage, + pageCount: resultTotal, + itemCount: total, + }); + if (opt && opt[pageField]) { + setPagination({ + page: opt[pageField] || 1, + }); + } + emit('fetch-success', { + items: unref(resultInfo), + resultTotal, + }); + } catch (error) { + console.error(error); + emit('fetch-error', error); + dataSourceRef.value = []; + setPagination({ + pageCount: 0, + }); + } finally { + setLoading(false); + } + } + + onMounted(() => { + setTimeout(() => { + fetch(); + }, 16); + }); + + function setTableData(values) { + dataSourceRef.value = values; + } + + function getDataSource(): any[] { + return getDataSourceRef.value; + } + + async function reload(opt?) { + await fetch(opt); + } + + return { + fetch, + getRowKey, + getDataSourceRef, + getDataSource, + setTableData, + reload, + }; +} diff --git a/src/components/Table/src/hooks/useLoading.ts b/src/components/Table/src/hooks/useLoading.ts new file mode 100644 index 00000000..0a670b00 --- /dev/null +++ b/src/components/Table/src/hooks/useLoading.ts @@ -0,0 +1,21 @@ +import { ref, ComputedRef, unref, computed, watch } from 'vue'; +import type { BasicTableProps } from '../types/table'; + +export function useLoading(props: ComputedRef) { + const loadingRef = ref(unref(props).loading); + + watch( + () => unref(props).loading, + (loading) => { + loadingRef.value = loading; + } + ); + + const getLoading = computed(() => unref(loadingRef)); + + function setLoading(loading: boolean) { + loadingRef.value = loading; + } + + return { getLoading, setLoading }; +} diff --git a/src/components/Table/src/hooks/usePagination.ts b/src/components/Table/src/hooks/usePagination.ts new file mode 100644 index 00000000..8ae55b3c --- /dev/null +++ b/src/components/Table/src/hooks/usePagination.ts @@ -0,0 +1,62 @@ +import type { PaginationProps } from '../types/pagination'; +import type { BasicTableProps } from '../types/table'; +import { computed, unref, ref, ComputedRef, watch } from 'vue'; + +import { isBoolean } from '@/utils/is'; +import { DEFAULTPAGESIZE, PAGESIZES } from '../const'; + +export function usePagination(refProps: ComputedRef) { + const configRef = ref({}); + const show = ref(true); + + watch( + () => unref(refProps).pagination, + (pagination) => { + if (!isBoolean(pagination) && pagination) { + configRef.value = { + ...unref(configRef), + ...(pagination ?? {}), + }; + } + } + ); + + const getPaginationInfo = computed((): PaginationProps | boolean => { + const { pagination } = unref(refProps); + if (!unref(show) || (isBoolean(pagination) && !pagination)) { + return false; + } + return { + page: 1, //当前页 + pageSize: DEFAULTPAGESIZE, //分页大小 + pageSizes: PAGESIZES, // 每页条数 + showSizePicker: true, + showQuickJumper: true, + prefix: (pagingInfo) => `共 ${pagingInfo.itemCount} 条`, // 不需要可以通过 pagination 重置或者删除 + ...(isBoolean(pagination) ? {} : pagination), + ...unref(configRef), + }; + }); + + function setPagination(info: Partial) { + const paginationInfo = unref(getPaginationInfo); + configRef.value = { + ...(!isBoolean(paginationInfo) ? paginationInfo : {}), + ...info, + }; + } + + function getPagination() { + return unref(getPaginationInfo); + } + + function getShowPagination() { + return unref(show); + } + + async function setShowPagination(flag: boolean) { + show.value = flag; + } + + return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination }; +} diff --git a/src/components/Table/src/hooks/useTableContext.ts b/src/components/Table/src/hooks/useTableContext.ts new file mode 100644 index 00000000..e975ac3f --- /dev/null +++ b/src/components/Table/src/hooks/useTableContext.ts @@ -0,0 +1,22 @@ +import type { Ref } from 'vue'; +import type { BasicTableProps, TableActionType } from '../types/table'; +import { provide, inject, ComputedRef } from 'vue'; + +const key = Symbol('s-table'); + +type Instance = TableActionType & { + wrapRef: Ref>; + getBindValues: ComputedRef; +}; + +type RetInstance = Omit & { + getBindValues: ComputedRef; +}; + +export function createTableContext(instance: Instance) { + provide(key, instance); +} + +export function useTableContext(): RetInstance { + return inject(key) as RetInstance; +} diff --git a/src/components/Table/src/props.ts b/src/components/Table/src/props.ts new file mode 100644 index 00000000..34b9111c --- /dev/null +++ b/src/components/Table/src/props.ts @@ -0,0 +1,59 @@ +import type { PropType } from 'vue'; +import { propTypes } from '@/utils/propTypes'; +import { BasicColumn } from './types/table'; +import { NDataTable } from 'naive-ui'; +export const basicProps = { + ...NDataTable.props, // 这里继承原 UI 组件的 props + title: { + type: String, + default: null, + }, + titleTooltip: { + type: String, + default: null, + }, + size: { + type: String, + default: 'medium', + }, + dataSource: { + type: [Object], + default: () => [], + }, + columns: { + type: [Array] as PropType, + default: () => [], + required: true, + }, + beforeRequest: { + type: Function as PropType<(...arg: any[]) => void | Promise>, + default: null, + }, + request: { + type: Function as PropType<(...arg: any[]) => Promise>, + default: null, + }, + afterRequest: { + type: Function as PropType<(...arg: any[]) => void | Promise>, + default: null, + }, + rowKey: { + type: [String, Function] as PropType string)>, + default: undefined, + }, + pagination: { + type: [Object, Boolean], + default: () => {}, + }, + //废弃 + showPagination: { + type: [String, Boolean], + default: 'auto', + }, + actionColumn: { + type: Object as PropType, + default: null, + }, + canResize: propTypes.bool.def(true), + resizeHeightOffset: propTypes.number.def(0), +}; diff --git a/src/components/Table/src/types/componentType.ts b/src/components/Table/src/types/componentType.ts new file mode 100644 index 00000000..92b2408e --- /dev/null +++ b/src/components/Table/src/types/componentType.ts @@ -0,0 +1,9 @@ +export type ComponentType = + | 'NInput' + | 'NInputNumber' + | 'NSelect' + | 'NCheckbox' + | 'NSwitch' + | 'NDatePicker' + | 'NTimePicker' + | 'NCascader'; diff --git a/src/components/Table/src/types/pagination.ts b/src/components/Table/src/types/pagination.ts new file mode 100644 index 00000000..29ffe0a7 --- /dev/null +++ b/src/components/Table/src/types/pagination.ts @@ -0,0 +1,10 @@ +export interface PaginationProps { + page?: number; //ܿģʽµĵǰҳ + itemCount?: number; // + pageCount?: number; //ҳ + pageSize?: number; //ܿģʽµķҳС + pageSizes?: number[]; //ÿҳ Զ + showSizePicker?: boolean; //Ƿʾÿҳѡ + showQuickJumper?: boolean; //Ƿʾת + prefix?: any; //ҳǰ +} diff --git a/src/components/Table/src/types/table.ts b/src/components/Table/src/types/table.ts new file mode 100644 index 00000000..38cebf93 --- /dev/null +++ b/src/components/Table/src/types/table.ts @@ -0,0 +1,38 @@ +import type { InternalRowData, TableBaseColumn } from 'naive-ui/lib/data-table/src/interface'; +import { ComponentType } from './componentType'; +export interface BasicColumn extends TableBaseColumn { + //编辑表格 + edit?: boolean; + editRow?: boolean; + editable?: boolean; + editComponent?: ComponentType; + editComponentProps?: Recordable; + editRule?: boolean | ((text: string, record: Recordable) => Promise); + editValueMap?: (value: any) => string; + onEditRow?: () => void; + // 权限编码控制是否显示 + auth?: string[]; + // 业务控制是否显示 + ifShow?: boolean | ((column: BasicColumn) => boolean); + // 控制是否支持拖拽,默认支持 + draggable?: boolean; +} + +export interface TableActionType { + reload: (opt) => Promise; + emit?: any; + getColumns: (opt?) => BasicColumn[]; + setColumns: (columns: BasicColumn[] | string[]) => void; +} + +export interface BasicTableProps { + title?: string; + dataSource: Function; + columns: any[]; + pagination: object; + showPagination: boolean; + actionColumn: any[]; + canResize: boolean; + resizeHeightOffset: number; + loading: boolean; +} diff --git a/src/components/Table/src/types/tableAction.ts b/src/components/Table/src/types/tableAction.ts new file mode 100644 index 00000000..27d483f7 --- /dev/null +++ b/src/components/Table/src/types/tableAction.ts @@ -0,0 +1,27 @@ +import { NButton } from 'naive-ui'; +import type { Component } from 'vue'; +import { PermissionsEnum } from '@/enums/permissionsEnum'; +export interface ActionItem extends Partial> { + onClick?: Fn; + label?: string; + type?: 'success' | 'error' | 'warning' | 'info' | 'primary' | 'default'; + // 设定 color 后会覆盖 type 的样式 + color?: string; + icon?: Component; + popConfirm?: PopConfirm; + disabled?: boolean; + divider?: boolean; + // 权限编码控制是否显示 + auth?: PermissionsEnum | PermissionsEnum[] | string | string[]; + // 业务控制是否显示 + ifShow?: boolean | ((action: ActionItem) => boolean); +} + +export interface PopConfirm { + title: string; + okText?: string; + cancelText?: string; + confirm: Fn; + cancel?: Fn; + icon?: Component; +} diff --git a/src/components/Upload/index.ts b/src/components/Upload/index.ts new file mode 100644 index 00000000..10a2c43d --- /dev/null +++ b/src/components/Upload/index.ts @@ -0,0 +1 @@ +export { default as BasicUpload } from './src/BasicUpload.vue'; diff --git a/src/components/Upload/src/BasicUpload.vue b/src/components/Upload/src/BasicUpload.vue new file mode 100644 index 00000000..0269b121 --- /dev/null +++ b/src/components/Upload/src/BasicUpload.vue @@ -0,0 +1,311 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 上传图片 + + + + + + + + + + {{ helpText }} + + + + + + + + + + + + + diff --git a/src/components/Upload/src/props.ts b/src/components/Upload/src/props.ts new file mode 100644 index 00000000..461aea1f --- /dev/null +++ b/src/components/Upload/src/props.ts @@ -0,0 +1,34 @@ +import type { PropType } from 'vue'; +import { NUpload } from 'naive-ui'; + +export const basicProps = { + ...NUpload.props, + accept: { + type: String, + default: '.jpg,.png,.jpeg,.svg,.gif', + }, + helpText: { + type: String as PropType, + default: '', + }, + maxSize: { + type: Number as PropType, + default: 2, + }, + maxNumber: { + type: Number as PropType, + default: Infinity, + }, + value: { + type: Array as PropType, + default: () => [], + }, + width: { + type: Number as PropType, + default: 104, + }, + height: { + type: Number as PropType, + default: 104, //建议不小于这个尺寸 太小页面可能显示有异常 + }, +}; diff --git a/src/components/Upload/src/type/index.ts b/src/components/Upload/src/type/index.ts new file mode 100644 index 00000000..b2ea97f4 --- /dev/null +++ b/src/components/Upload/src/type/index.ts @@ -0,0 +1,7 @@ +export interface BasicProps { + title?: string; + dataSource: Function; + columns: any[]; + pagination: object; + showPagination: boolean; +} diff --git a/src/components/common/DialogApi.vue b/src/components/common/DialogApi.vue index 561a48fe..6a33f014 100644 --- a/src/components/common/DialogApi.vue +++ b/src/components/common/DialogApi.vue @@ -1,4 +1,4 @@ - + + + + + + + diff --git a/src/components/icons/arrowUp.vue b/src/components/icons/arrowUp.vue new file mode 100644 index 00000000..6fa32ffd --- /dev/null +++ b/src/components/icons/arrowUp.vue @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/src/components/icons/article.vue b/src/components/icons/article.vue new file mode 100644 index 00000000..1e29af69 --- /dev/null +++ b/src/components/icons/article.vue @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/src/components/icons/book.vue b/src/components/icons/book.vue new file mode 100644 index 00000000..cfbaabc3 --- /dev/null +++ b/src/components/icons/book.vue @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/src/components/icons/checkOutlined.vue b/src/components/icons/checkOutlined.vue new file mode 100644 index 00000000..072ca424 --- /dev/null +++ b/src/components/icons/checkOutlined.vue @@ -0,0 +1,30 @@ + + + + + + diff --git a/src/components/icons/close.vue b/src/components/icons/close.vue new file mode 100644 index 00000000..a5c9d4c7 --- /dev/null +++ b/src/components/icons/close.vue @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/src/components/icons/closeCircle.vue b/src/components/icons/closeCircle.vue new file mode 100644 index 00000000..0216ee92 --- /dev/null +++ b/src/components/icons/closeCircle.vue @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/src/components/icons/code.vue b/src/components/icons/code.vue new file mode 100644 index 00000000..746d0b8e --- /dev/null +++ b/src/components/icons/code.vue @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/src/components/icons/copy.vue b/src/components/icons/copy.vue new file mode 100644 index 00000000..5268423b --- /dev/null +++ b/src/components/icons/copy.vue @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/components/icons/edit.vue b/src/components/icons/edit.vue new file mode 100644 index 00000000..e8b5b3c5 --- /dev/null +++ b/src/components/icons/edit.vue @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/src/components/icons/exit.vue b/src/components/icons/exit.vue new file mode 100644 index 00000000..5475cee2 --- /dev/null +++ b/src/components/icons/exit.vue @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/src/components/icons/expand.vue b/src/components/icons/expand.vue new file mode 100644 index 00000000..4d7e2dbb --- /dev/null +++ b/src/components/icons/expand.vue @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/src/components/icons/github.vue b/src/components/icons/github.vue new file mode 100644 index 00000000..1fba4e11 --- /dev/null +++ b/src/components/icons/github.vue @@ -0,0 +1,49 @@ + + + + + icon_GitHub + Created with sketchtool. + + + + + + + + + + + diff --git a/src/components/icons/history.vue b/src/components/icons/history.vue new file mode 100644 index 00000000..472c18a3 --- /dev/null +++ b/src/components/icons/history.vue @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/src/components/icons/index.js b/src/components/icons/index.js new file mode 100644 index 00000000..f45bb945 --- /dev/null +++ b/src/components/icons/index.js @@ -0,0 +1,59 @@ +import IArticle from "./article.vue"; +import IBook from "./book.vue"; +import ICode from "./code.vue"; +import IEdit from "./edit.vue"; +import IQuestion from "./question.vue"; +import ISyntax from "./syntax.vue"; +import ITransform from "./transform.vue"; +import IUserFilled from "./userFilled.vue"; +import IMenu from "./menu.vue"; +import ITag from "./tag.vue"; +import IExpand from "./expand.vue"; +import IClose from "./close.vue"; +import IArrowDown from "./arrowDown.vue"; +import IArrowUp from "./arrowUp.vue"; +import ICopy from "./copy.vue"; +import IRefreshRight from "./refreshRight.vue"; +import IUpdate from "./update.vue"; +import IGithub from "./github.vue"; +import IPlus from "./plus.vue"; +import IHistory from "./history.vue"; +import ISend from "./send.vue"; +import ITagFilled from "./tagFilled.vue"; +import ISet from "./set.vue"; +import IExit from "./exit.vue"; +import ICheckOutlined from "./checkOutlined.vue"; +import IMoreOutlined from "./moreOutlined.vue"; +import IOct from "./oct.vue"; +import ICloseCircle from "./closeCircle.vue"; + +export { + IArticle, + IBook, + ICode, + IEdit, + IQuestion, + ISyntax, + ITransform, + IUserFilled, + IMenu, + ITag, + IExpand, + IClose, + IArrowDown, + IArrowUp, + ICopy, + IRefreshRight, + IUpdate, + IGithub, + IPlus, + IHistory, + ISend, + ITagFilled, + ISet, + IExit, + ICheckOutlined, + IMoreOutlined, + IOct, + ICloseCircle, +}; diff --git a/src/components/icons/map.js b/src/components/icons/map.js new file mode 100644 index 00000000..f54d943f --- /dev/null +++ b/src/components/icons/map.js @@ -0,0 +1,60 @@ +import { + IArticle, + IBook, + ICode, + IEdit, + IQuestion, + ISyntax, + ITransform, + IUserFilled, + IMenu, + ITag, + IExpand, + IClose, + IArrowDown, + IArrowUp, + ICopy, + IRefreshRight, + IUpdate, + IGithub, + IPlus, + IHistory, + ISend, + ITagFilled, + ISet, + IExit, + ICheckOutlined, + IOct, + ICloseCircle, +} from "./index"; + +export default { + article: IArticle, + book: IBook, + code: ICode, + edit: IEdit, + question: IQuestion, + syntax: ISyntax, + transform: ITransform, + userfilled: IUserFilled, + menu: IMenu, + tag: ITag, + expand: IExpand, + close: IClose, + arrowdown: IArrowDown, + arrowup: IArrowUp, + copy: ICopy, + refreshRight: IRefreshRight, + update: IUpdate, + github: IGithub, + plus: IPlus, + history: IHistory, + send: ISend, + tagFilled: ITagFilled, + set: ISet, + exit: IExit, + checkOutlined: ICheckOutlined, + IMoreOutlined: ICheckOutlined, + IOct: IOct, + ICloseCircle: ICloseCircle, +}; diff --git a/src/components/icons/menu.vue b/src/components/icons/menu.vue new file mode 100644 index 00000000..49c226c4 --- /dev/null +++ b/src/components/icons/menu.vue @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/src/components/icons/moreOutlined.vue b/src/components/icons/moreOutlined.vue new file mode 100644 index 00000000..57015d36 --- /dev/null +++ b/src/components/icons/moreOutlined.vue @@ -0,0 +1,30 @@ + + + + + + diff --git a/src/components/icons/oct.vue b/src/components/icons/oct.vue new file mode 100644 index 00000000..34d3862d --- /dev/null +++ b/src/components/icons/oct.vue @@ -0,0 +1,36 @@ + + + + + + diff --git a/src/components/icons/plus.vue b/src/components/icons/plus.vue new file mode 100644 index 00000000..f1df43e2 --- /dev/null +++ b/src/components/icons/plus.vue @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/src/components/icons/question.vue b/src/components/icons/question.vue new file mode 100644 index 00000000..47721234 --- /dev/null +++ b/src/components/icons/question.vue @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/src/components/icons/refreshRight.vue b/src/components/icons/refreshRight.vue new file mode 100644 index 00000000..2b8eb6fe --- /dev/null +++ b/src/components/icons/refreshRight.vue @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/components/icons/send.vue b/src/components/icons/send.vue new file mode 100644 index 00000000..b91851e9 --- /dev/null +++ b/src/components/icons/send.vue @@ -0,0 +1,36 @@ + + + + + + diff --git a/src/components/icons/set.vue b/src/components/icons/set.vue new file mode 100644 index 00000000..eb787140 --- /dev/null +++ b/src/components/icons/set.vue @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/src/components/icons/syntax.vue b/src/components/icons/syntax.vue new file mode 100644 index 00000000..1d611d7d --- /dev/null +++ b/src/components/icons/syntax.vue @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/src/components/icons/tag.vue b/src/components/icons/tag.vue new file mode 100644 index 00000000..c4440f4e --- /dev/null +++ b/src/components/icons/tag.vue @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/src/components/icons/tagFilled.vue b/src/components/icons/tagFilled.vue new file mode 100644 index 00000000..d83e725b --- /dev/null +++ b/src/components/icons/tagFilled.vue @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/src/components/icons/transform.vue b/src/components/icons/transform.vue new file mode 100644 index 00000000..0d9bb583 --- /dev/null +++ b/src/components/icons/transform.vue @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/components/icons/update.vue b/src/components/icons/update.vue new file mode 100644 index 00000000..60cecff5 --- /dev/null +++ b/src/components/icons/update.vue @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/src/components/icons/userFilled.vue b/src/components/icons/userFilled.vue new file mode 100644 index 00000000..13d61d98 --- /dev/null +++ b/src/components/icons/userFilled.vue @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/src/components/talk/ForwardRecord.vue b/src/components/talk/ForwardRecord.vue index 68b7ed4b..6c76a092 100644 --- a/src/components/talk/ForwardRecord.vue +++ b/src/components/talk/ForwardRecord.vue @@ -1,4 +1,4 @@ - diff --git a/src/components/talk/message/ForwardMessage.vue b/src/components/talk/message/ForwardMessage.vue index c2082483..07c619ce 100644 --- a/src/components/talk/message/ForwardMessage.vue +++ b/src/components/talk/message/ForwardMessage.vue @@ -1,13 +1,12 @@ - @@ -41,12 +33,7 @@ if (pids == '' || pids == undefined) { 转发:聊天会话记录 ({{ extra.msg_ids.length }}条) - + diff --git a/src/components/talk/message/GroupNoticeMessage.vue b/src/components/talk/message/GroupNoticeMessage.vue index 839f70da..520c2f2d 100644 --- a/src/components/talk/message/GroupNoticeMessage.vue +++ b/src/components/talk/message/GroupNoticeMessage.vue @@ -1,11 +1,11 @@ - diff --git a/src/components/talk/message/LoginMessage.vue b/src/components/talk/message/LoginMessage.vue index b82dfab4..7729e832 100644 --- a/src/components/talk/message/LoginMessage.vue +++ b/src/components/talk/message/LoginMessage.vue @@ -1,8 +1,11 @@ - @@ -24,7 +29,8 @@ textContent = textReplaceEmoji(textContent) :class="{ left: float == 'left', right: float == 'right', - maxwidth: maxWidth + maxwidth: maxWidth, + 'radius-reset': source != 'panel' }" > @@ -50,6 +56,10 @@ textContent = textReplaceEmoji(textContent) max-width: 70%; } + &.radius-reset { + border-radius: 0; + } + pre { white-space: pre-wrap; overflow: hidden; diff --git a/src/components/talk/message/VideoMessage.vue b/src/components/talk/message/VideoMessage.vue index e6a96b00..39676784 100644 --- a/src/components/talk/message/VideoMessage.vue +++ b/src/components/talk/message/VideoMessage.vue @@ -1,18 +1,16 @@ - + + + + + + + + + + + + + + + + 取消 + + 保存修改 + + + + + + + diff --git a/src/connect.ts b/src/connect.ts new file mode 100644 index 00000000..56b911eb --- /dev/null +++ b/src/connect.ts @@ -0,0 +1,592 @@ +import { h } from 'vue' +import { NAvatar } from 'naive-ui' +import { useTalkStore, useUserStore, useDialogueStore } from '@/store' +import { notifyIcon } from '@/constant/default' + +import { isLoggedIn } from './utils/auth' + +import EventTalk from './event/socket/talk' +import EventKeyboard from './event/socket/keyboard' +import EventLogin from './event/socket/login' +import EventRevoke from './event/socket/revoke' + +import { Client } from 'paho-mqtt' // 从'mqtt-paho'库导入Client +import { v4 } from 'uuid' +import { storage } from '@/utils/storage' + +import { getKeyByBasicString, encrypt, decrypt } from '@/utils/crypto-use-crypto-js.mjs' + +/** + * 延时函数 + * @param {*} ms 毫秒 + */ +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * MQTT 连接实例 + * + * 注释: 所有 mqtt 消息接收处理在此实例中处理 + */ +class Socket { + /** + * mqtt 实例 + */ + client + + endpoint + + port + + clientID + + options + + apis + + botid + + key + + /** + * Socket 初始化实例 + */ + constructor() { + console.debug('Socket init...') + const VITE_SOCKET_API = import.meta.env.VITE_SOCKET_API + // 分割字符串VITE_SOCKET_API为host和端口 + const hostAndPort = VITE_SOCKET_API.split('://')[1] + this.endpoint = hostAndPort.split(':')[0] + this.port = hostAndPort.split(':')[1] + console.log('mqtt endpoint:', this.endpoint) + console.log('mqtt port:', this.port) + this.clientID = v4() + console.log('client id:', this.clientID) + this.client = new Client(this.endpoint, Number(this.port), this.clientID) // 创建新的mqtt-paho客户端实例 + + this.options = { + useSSL: false, // 使用 SSL/TLS 进行安全连接 + timeout: 40, + userName: '', + password: '', + cleanSession: true, + onSuccess: async () => { + console.debug('MQTT连接成功~') + this.subscribeToTopics() + // 更新 MQTT 连接状态 + useUserStore().updateSocketStatus(true) + // 载入聊天列表 + useTalkStore().loadTalkList() + }, + onFailure: (evt) => { + console.debug('MQTT连接失败~', evt) + } + } + + this.client.onConnectionLost = (responseObject) => { + console.debug('MQTT连接丢失~', responseObject) + // 更新 MQTT 连接状态 + useUserStore().updateSocketStatus(false) + + // 连接丢失时的回调函数 + if (responseObject.errorCode !== 0 && this.client.isConnected()) { + console.log('MQTT 连接丢失:', responseObject.errorMessage) + this.connect('断线重连...') // 连接到mqtt服务器 + } + } + + this.client.onMessageArrived = (message) => { + // 收到消息时的回调函数 + // console.log('接收到消息:', JSON.stringify(message)); + this.onMessage(message.destinationName, message.payloadString) + } + // console.debug("MQTT开始连接~"); + // this.connect(); + } + + formatMsgToWechaty(data) { + // {"type":"text","content":"ok","quote_id":"","mention":{"all":0,"uids":[]},"receiver":{"receiver_id":"wxid_pnza7m7kf9tq12","talk_type":1}} + // {"type":"image","width":1024,"height":1024,"url":"https://im.gzydong.com/public/media/image/common/20231030/2143db60700049fd68ab44263cd8b2cc_1024x1024.png","size":10000,"receiver":{"receiver_id":"20889085065@chatroom","talk_type":2}} + const msg_type = data.type + let messageType: any = 'Text' + let messagePayload = '' + + switch (msg_type) { + case 'text': + messageType = 'Text' + messagePayload = data.content + break + case 'image': + messagePayload = data.url + messageType = 'Image' + break + case 'Emoticon': + messageType = 'Text' + break + case 'ChatHistory': + messageType = 'Text' + break + case 'Audio': + messageType = 4 + break + case 'Attachment': + messageType = 6 + break + case 'Video': + messageType = 5 + break + case 'MiniProgram': + messageType = 1 + break + case 'Url': + messageType = 1 + break + case 'Recalled': + messageType = 1 + break + case 'RedEnvelope': + messageType = 1 + break + case 'Contact': + messageType = 1 + break + case 'Location': + messageType = 1 + break + default: + messageType = 'Text' + break + } + const msg = { + reqId: v4(), + method: 'thing.command.invoke', + version: '1.0', + timestamp: new Date().getTime(), + name: 'send', + params: { + toContacts: [ + data.receiver.receiver_id + // "5550027590@chatroom", + ], + messageType: messageType, + messagePayload: messagePayload + } + } + const msgStr = JSON.stringify(msg) + const msgStrEncrypted = encrypt(msgStr, this.key) + console.debug('msgStrEncrypted...', msgStrEncrypted) + const msgStrDecrypted = decrypt(msgStrEncrypted, this.key) + console.debug('msgStrDecrypted...', msgStrDecrypted) + return msgStrEncrypted + } + + formatMsgToCommand(data) { + const msg = { + reqId: v4(), + method: 'thing.command.invoke', + version: '1.0', + timestamp: new Date().getTime(), + name: 'updateConfig', + params: data + } + const msgStr = JSON.stringify(msg) + const msgStrEncrypted = encrypt(msgStr, this.key) + console.debug('msgStrEncrypted...', msgStrEncrypted) + const msgStrDecrypted = decrypt(msgStrEncrypted, this.key) + console.debug('msgStrDecrypted...', msgStrDecrypted) + return msgStrEncrypted + } + + // 格式化消息数据 + formatMsg(data) { + const msgStr = JSON.stringify({ + reqId: v4(), + method: 'thing.event.post', + version: '1.0', + timestamp: new Date().getTime(), + events: data + }) + const msgStrEncrypted = encrypt(msgStr, this.key) + console.debug('msgStrEncrypted...', msgStrEncrypted) + const msgStrDecrypted = decrypt(msgStrEncrypted, this.key) + console.debug('msgStrDecrypted...', msgStrDecrypted) + return msgStrEncrypted +} + + subscribeToTopics() { + console.debug('订阅消息主题:', JSON.stringify(this.apis)) + this.client.subscribe(this.apis.eventApi) + this.client.subscribe(this.apis.commandApi) + } + + // 新增方法处理读取消息事件 + handleMessageRead(data) { + console.debug('客户端:', this.clientID) + const dialogueStore = useDialogueStore() + if (dialogueStore.index_name === `1_${data.sender_id}`) { + for (const msgid of data.ids) { + dialogueStore.updateDialogueRecord({ id: msgid, is_read: 1 }) + } + } + } + + // 连接 mqtt 服务 + connect(from) { + console.debug('connect()请求来自:', from) + delay(3000) + + const user = storage.get('user_info') + console.debug('从缓存中获取用户信息:', user) + + const { hash } = user.user_info + this.botid = hash + console.debug('从用户信息中获取hash', hash) + this.key = getKeyByBasicString(hash) + console.debug('从用户信息中获取key', this.key) + this.apis = { + eventApi: `thing/chatbot/${this.botid}/event/post`, + commandApi: `thing/chatbot/${this.botid}/command/invoke` + } + + console.debug('MQTT连接状态:', this.client.isConnected()) + + if (!this.client.isConnected() && isLoggedIn()) { + try { + console.debug('MQTT建立连接...') + this.client.connect(this.options) + console.debug('MQTT建立连接成功~') + } catch (e) { + console.error('MQTT建立连接失败:', e) + } + } else { + console.debug('MQTT连接存在,不需要连接...') + } + console.debug('connect() success') + } + + // 连接 mqtt 服务 + disconnect() { + console.debug('disconnect()...') + this.client.disconnect() + } + + isConnect() { + console.debug('isConnect()..., MQTT连接状态:', this.client.isConnected()) + if (!this.client.isConnected()) { + return false + } + return true + } + + /** + * 注册回调消息处理事件 + */ + onMessage(topic, message) { + console.debug('topic:', topic) + console.debug('payload:', message.toString()) + + try{ + message = decrypt(message.toString(), this.key) + console.debug('decrypt message:', message) + + const messageObj = JSON.parse(message) + + if (topic === this.apis.eventApi) { + if (messageObj.events['onMessage']) { + const rawMsg = messageObj.events.onMessage + // if(rawMsg.room) { + // rawMsg.room.id = 1029; + // } + // rawMsg.talker.id = 2055; + const talk_type = rawMsg.room ? 2 : 1 + const user_id = rawMsg.talker.id + const receiver_id = rawMsg.room ? rawMsg.room.id : rawMsg.listener.id + const messageType = rawMsg.type + let msg_type = 1 + let text:any = {} + const messagePayload = rawMsg.data.payload.text + try{ + text = JSON.parse(rawMsg.text) + }catch(e){ + console.error('JSON.parse(rawMsg.text) error:', e) + } + console.debug('text', text) + let extra: any = { + content: messagePayload, + }; + switch (messageType) { + case 'Text': + msg_type = 1 + break + case 'Image': { + msg_type = 3 + if (text.url) { + extra = { + width: 54, + height: 54, + url: text.url, + size: text.size + } + } + break + } + case 'Emoticon': + msg_type = 1 + break + case 'ChatHistory': + msg_type = 9 + break + case 'Audio': + msg_type = 4 + if (text.url) { + const filename = text.name + const suffix = filename.split('.').pop() + extra = { + "suffix": suffix, + "name": text.name, + "path": text.url, + "size": 0, + "drive": 1 + } + } + break + case 'Attachment': { + msg_type = 6 + if (text.url) { + const filename = text.name + const suffix = filename.split('.').pop() + extra = { + "suffix": suffix, + "name": text.name, + "path": text.url, + "size": 0, + "drive": 1 + } + } + break + } + case 'Video': { + msg_type = 5 + if (text.url) { + const filename = text.name + const suffix = filename.split('.').pop() + extra = { + "suffix": suffix, + "name": text.name, + "path": text.url, + "size": 0, + "drive": 1 + } + } + break + } + case 'MiniProgram': + msg_type = 1 + break + case 'Url': + msg_type = 1 + break + case 'Recalled': + msg_type = 1 + break + case 'RedEnvelope': + msg_type = 1 + break + case 'Contact': + msg_type = 1 + break + case 'Location': + msg_type = 1 + break + case 'GroupNote': + msg_type = 1 + break + case 'Transfer': + msg_type = 1 + break + case 'Post': + msg_type = 1 + break + case 'qrcode': + msg_type = 3 + break + case 'Unknown': + msg_type = 1 + break + default: + break + } + + if (['Image', 'Attachment', 'Video', 'Audio'].includes(messageType)) { + extra = { + height: 1000, + name: '无效文件', + size: 0, + suffix: '', + url: '', + width: 563, + }; + try { + const textObj = JSON.parse(messagePayload); + extra.name = textObj.name; + extra.url = textObj.url; + } catch (e) { + console.debug('解析消息内容失败', e); + } + } + + const newMsg = { + receiver_id, // 接收者ID + sender_id: user_id, // 发送者ID + talk_type, // 对话类型 + data: { + id: rawMsg.data.payload.timestamp || rawMsg.data.id, // 消息ID + sequence: rawMsg.data.payload.timestamp, // 消息序列号 + msg_id: rawMsg.data.id, // 消息ID + talk_type, // 对话类型 + msg_type, // 消息类型 + user_id, // 发送者ID + receiver_id, // 接收者ID + nickname: rawMsg.talker.payload.name, // 发送者昵称 + avatar: + rawMsg.talker.payload.avatar || + 'https://im.gzydong.com/public/media/image/avatar/20230530/f76a14ce98ca684752df742974f5473a_200x200.png', // 发送者头像 + is_revoke: 0, // 是否撤回 + is_mark: 0, // 是否标记 + is_read: 0, // 是否已读 + created_at: rawMsg.time, // 创建时间 + extra, + } + } + this.emit('im.message', newMsg) + // this.client.publish(this.apis.eventApi, formatMsg({ 'im.message': newMsg })) + } + + if (messageObj.events['ping']) { + this.emit('pong', '') + } + + if (messageObj.events['pong']) { + console.debug('pong') + } + + // 对话消息事件 + if (messageObj.events['im.message']) { + const data = messageObj.events['im.message'] + new EventTalk(data) + } + + if (messageObj.events['im.message.read']) { + const data = messageObj.events['im.message.read'] + console.debug('im.message.read', data) + + const dialogueStore = useDialogueStore() + + if (dialogueStore.index_name == `1_${data.sender_id}`) { + for (const msgid of data.ids) { + dialogueStore.updateDialogueRecord({ id: msgid, is_read: 1 }) + } + } + } + + // 好友在线状态事件 + if (messageObj.events['im.contact.status']) { + const data = messageObj.events['im.contact.status'] + new EventLogin(data) + } + + // 好友键盘输入事件 + if (messageObj.events['im.message.keyboard']) { + const data = messageObj.events['im.message.keyboard'] + new EventKeyboard(data) + } + + // 消息撤回事件 + if (messageObj.events['im.message.revoke']) { + const data = messageObj.events['im.message.revoke'] + new EventRevoke(data) + } + + // 好友申请事件 + if (messageObj.events['im.contact.apply']) { + const data = messageObj.events['im.contact.apply'] + window['$notification'].create({ + title: '好友申请通知', + content: data.remark, + description: `申请人: ${data.friend.nickname}`, + meta: data.friend.created_at, + avatar: () => + h(NAvatar, { + size: 'small', + round: true, + src: notifyIcon, + style: 'background-color:#fff;' + }), + duration: 3000 + }) + useUserStore().isContactApply = true + } + + // 群组申请事件 + if (messageObj.events['im.group.apply']) { + const data = messageObj.events['im.group.apply'] + console.debug('im.group.apply', data) + window['$notification'].create({ + title: '入群申请通知', + content: '有新的入群申请,请注意查收', + avatar: () => + h(NAvatar, { + size: 'small', + round: true, + src: notifyIcon, + style: 'background-color:#fff;' + }), + duration: 30000 + }) + + useUserStore().isGroupApply = true + } + + // 报错事件 + if (messageObj.events.event_error) { + const data = messageObj.events['event_error'] + window['$message'].error(JSON.stringify(data)) + } + } + }catch(e){ + console.error('onMessage() error:', e) + } + } + + /** + * 聊天发送数据 + * + * @param {Object} mesage + */ + send(message) { + if (this.isConnect()) { + // this.client.publish(this.apis.eventApi, formatMsg({ 'im.message': message })) + this.client.publish(this.apis.commandApi, this.formatMsgToWechaty(message)) + } else { + this.client.end() + } + } + + /** + * 推送消息 + * + * @param {String} event 事件名 + * @param {Object} data 数据 + */ + emit(event, data) { + console.debug('emit() event:', event) + console.debug('emit() data:', data) + const rawMsg = {} + rawMsg[event] = data + const payload = this.formatMsg(rawMsg) + this.client.publish(this.apis.eventApi, payload) + } +} + +export default new Socket() diff --git a/src/constant/theme.js b/src/constant/theme.js index 4b3745bb..756703c5 100644 --- a/src/constant/theme.js +++ b/src/constant/theme.js @@ -2,14 +2,13 @@ export const overrides = { common: { primaryColor: '#1890ff', - // primaryColor: '#4a72ef', primaryColorHover: '#1890ff', primaryColorPressed: '#1890ff', primaryColorSuppl: '#1890ff', bodyColor: '#ffffff' - } + }, - // Dialog: { - // borderRadius: '10px', - // }, + Dialog: { + borderRadius: '10px' + } } diff --git a/src/directive/dropsize.js b/src/directive/dropsize.js index 06d05dfb..cac2e666 100644 --- a/src/directive/dropsize.js +++ b/src/directive/dropsize.js @@ -7,23 +7,26 @@ function getCacheKey(key, direction) { } export default { - // binding.value = {min:10,max:100,direction:"top",key:""} mounted: function (el, binding) { let { min, max, direction = 'right', key = '' } = binding.value - const cacheKey = getCacheKey(key, direction) - el.style.position = 'relative' el.touch = { status: false, pageX: 0, pageY: 0, width: 0, height: 0 } - let linedom = document.createElement('div') + const cacheKey = getCacheKey(key, direction) + const cursor = ['left', 'right'].includes(direction) ? 'col-resize' : 'row-resize' + + const linedom = document.createElement('div') linedom.className = `dropsize-line dropsize-line-${direction}` el.linedomMouseup = function () { if (!el.touch.status) return + el.touch.status = false - document.querySelector('body').style.cursor = '' + linedom.classList.remove('dropsize-resizing') + + document.querySelector('body').classList.remove(`dropsize-${cursor}`) } el.linedomMousemove = function (e) { @@ -74,16 +77,16 @@ export default { height: el.offsetHeight } - let cursor = ['left', 'right'].includes(direction) ? 'col-resize' : 'row-resize' + this.classList.add('dropsize-resizing') - document.querySelector('body').style.cursor = cursor + document.querySelector('body').classList.add(`dropsize-${cursor}`) document.addEventListener('mouseup', el.linedomMouseup) document.addEventListener('mousemove', el.linedomMousemove) }) if (cacheKey) { - let value = storage.get(cacheKey) + const value = storage.get(cacheKey) if (direction == 'left' || direction == 'right') { el.style.width = `${value}px` diff --git a/src/directive/index.ts b/src/directive/index.ts new file mode 100644 index 00000000..1e68a63e --- /dev/null +++ b/src/directive/index.ts @@ -0,0 +1,19 @@ +import dropsize from './dropsize' +import focus from './focus' +// import paste from './paste' +import loading from './loading' +// import copy from './copy' + +const directives = { + dropsize, + focus, + // paste, + loading + // copy +} + +export function setupDirective(app: any) { + for (const key in directives) { + app.directive(key, directives[key]) + } +} diff --git a/src/directive/inner/loading.vue b/src/directive/inner/loading.vue new file mode 100644 index 00000000..25ef8bd4 --- /dev/null +++ b/src/directive/inner/loading.vue @@ -0,0 +1,178 @@ + + + + + + + + + + 加载中... + + + + + + \ No newline at end of file diff --git a/src/directive/loading.js b/src/directive/loading.js index 180828e7..a405c2c2 100644 --- a/src/directive/loading.js +++ b/src/directive/loading.js @@ -1,5 +1,5 @@ import { createApp } from 'vue' -import Loading from './loading.vue' +import Loading from './inner/loading.vue' export default { mounted(el, binding) { diff --git a/src/directives/clickOutside.ts b/src/directives/clickOutside.ts new file mode 100644 index 00000000..2758a9da --- /dev/null +++ b/src/directives/clickOutside.ts @@ -0,0 +1,86 @@ +import { on } from '@/utils/domUtils'; +import { isServer } from '@/utils/is'; +import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue'; + +type DocumentHandler = (mouseup: T, mousedown: T) => void; + +type FlushList = Map< + HTMLElement, + { + documentHandler: DocumentHandler; + bindingFn: (...args: unknown[]) => unknown; + } +>; + +const nodeList: FlushList = new Map(); + +let startClick: MouseEvent; + +if (!isServer) { + on(document, 'mousedown', (e: MouseEvent) => (startClick = e)); + on(document, 'mouseup', (e: MouseEvent) => { + for (const { documentHandler } of nodeList.values()) { + documentHandler(e, startClick); + } + }); +} + +function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler { + let excludes: HTMLElement[] = []; + if (Array.isArray(binding.arg)) { + excludes = binding.arg; + } else { + // due to current implementation on binding type is wrong the type casting is necessary here + excludes.push(binding.arg as unknown as HTMLElement); + } + return function (mouseup, mousedown) { + const popperRef = ( + binding.instance as ComponentPublicInstance<{ + popperRef: Nullable; + }> + ).popperRef; + const mouseUpTarget = mouseup.target as Node; + const mouseDownTarget = mousedown.target as Node; + const isBound = !binding || !binding.instance; + const isTargetExists = !mouseUpTarget || !mouseDownTarget; + const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget); + const isSelf = el === mouseUpTarget; + + const isTargetExcluded = + (excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) || + (excludes.length && excludes.includes(mouseDownTarget as HTMLElement)); + const isContainedByPopper = + popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget)); + if ( + isBound || + isTargetExists || + isContainedByEl || + isSelf || + isTargetExcluded || + isContainedByPopper + ) { + return; + } + binding.value(); + }; +} + +const ClickOutside: ObjectDirective = { + beforeMount(el, binding) { + nodeList.set(el, { + documentHandler: createDocumentHandler(el, binding), + bindingFn: binding.value, + }); + }, + updated(el, binding) { + nodeList.set(el, { + documentHandler: createDocumentHandler(el, binding), + bindingFn: binding.value, + }); + }, + unmounted(el) { + nodeList.delete(el); + }, +}; + +export default ClickOutside; diff --git a/src/enums/breakpointEnum.ts b/src/enums/breakpointEnum.ts new file mode 100644 index 00000000..93acc1a3 --- /dev/null +++ b/src/enums/breakpointEnum.ts @@ -0,0 +1,28 @@ +export enum sizeEnum { + XS = 'XS', + SM = 'SM', + MD = 'MD', + LG = 'LG', + XL = 'XL', + XXL = 'XXL', +} + +export enum screenEnum { + XS = 480, + SM = 576, + MD = 768, + LG = 992, + XL = 1200, + XXL = 1600, +} + +const screenMap = new Map(); + +screenMap.set(sizeEnum.XS, screenEnum.XS); +screenMap.set(sizeEnum.SM, screenEnum.SM); +screenMap.set(sizeEnum.MD, screenEnum.MD); +screenMap.set(sizeEnum.LG, screenEnum.LG); +screenMap.set(sizeEnum.XL, screenEnum.XL); +screenMap.set(sizeEnum.XXL, screenEnum.XXL); + +export { screenMap }; diff --git a/src/enums/cacheEnum.ts b/src/enums/cacheEnum.ts new file mode 100644 index 00000000..77cfdbdd --- /dev/null +++ b/src/enums/cacheEnum.ts @@ -0,0 +1,20 @@ +// token key +export const TOKEN_KEY = 'TOKEN'; + +// user info key +export const USER_INFO_KEY = 'USER__INFO__'; + +// role info key +export const ROLES_KEY = 'ROLES__KEY__'; + +// project config key +export const PROJ_CFG_KEY = 'PROJ__CFG__KEY__'; + +// lock info +export const LOCK_INFO_KEY = 'LOCK__INFO__KEY__'; + +// base global local key +export const BASE_LOCAL_CACHE_KEY = 'LOCAL__CACHE__KEY__'; + +// base global session key +export const BASE_SESSION_CACHE_KEY = 'SESSION__CACHE__KEY__'; diff --git a/src/enums/httpEnum.ts b/src/enums/httpEnum.ts new file mode 100644 index 00000000..9aa83e0e --- /dev/null +++ b/src/enums/httpEnum.ts @@ -0,0 +1,34 @@ +/** + * @description: 请求结果集 + */ +export enum ResultEnum { + SUCCESS = 200, + ERROR = -1, + TIMEOUT = 10042, + TYPE = 'success', +} + +/** + * @description: 请求方法 + */ +export enum RequestEnum { + GET = 'GET', + POST = 'POST', + PATCH = 'PATCH', + PUT = 'PUT', + DELETE = 'DELETE', +} + +/** + * @description: 常用的contentTyp类型 + */ +export enum ContentTypeEnum { + // json + JSON = 'application/json;charset=UTF-8', + // json + TEXT = 'text/plain;charset=UTF-8', + // form-data 一般配合qs + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + // form-data 上传 + FORM_DATA = 'multipart/form-data;charset=UTF-8', +} diff --git a/src/enums/pageEnum.ts b/src/enums/pageEnum.ts new file mode 100644 index 00000000..d08ada9f --- /dev/null +++ b/src/enums/pageEnum.ts @@ -0,0 +1,14 @@ +export enum PageEnum { + // 登录 + BASE_LOGIN = '/login', + BASE_LOGIN_NAME = 'Login', + //重定向 + REDIRECT = '/redirect', + REDIRECT_NAME = 'Redirect', + // 首页 + BASE_HOME = '/dashboard', + //首页跳转默认路由 + BASE_HOME_REDIRECT = '/dashboard/console', + // 错误 + ERROR_PAGE_NAME = 'ErrorPage', +} diff --git a/src/enums/permissionsEnum.ts b/src/enums/permissionsEnum.ts new file mode 100644 index 00000000..bf3f53a2 --- /dev/null +++ b/src/enums/permissionsEnum.ts @@ -0,0 +1,4 @@ +export interface PermissionsEnum { + value: string; + label: string; +} diff --git a/src/enums/roleEnum.ts b/src/enums/roleEnum.ts new file mode 100644 index 00000000..e8f01056 --- /dev/null +++ b/src/enums/roleEnum.ts @@ -0,0 +1,7 @@ +export enum RoleEnum { + // 管理员 + ADMIN = 'admin', + + // 普通用户 + NORMAL = 'normal', +} diff --git a/src/event/socket/keyboard.js b/src/event/socket/keyboard.js index bfc66eec..2c1aff80 100644 --- a/src/event/socket/keyboard.js +++ b/src/event/socket/keyboard.js @@ -28,7 +28,7 @@ class Keyboard extends Base { let params = this.getTalkParams() // 判断当前是否正在对话 - if (params.index_name === null) { + if (!params.index_name) { return false } diff --git a/src/event/socket/talk.js b/src/event/socket/talk.js index a62d4d6e..400b5dfd 100644 --- a/src/event/socket/talk.js +++ b/src/event/socket/talk.js @@ -1,10 +1,11 @@ import Base from './base' import { nextTick } from 'vue' -import socket from '@/socket' +import ws from '@/connect' import { parseTime } from '@/utils/datetime' import { WebNotify } from '@/utils/notification' import * as message from '@/constant/message' import { formatTalkItem, palyMusic, formatTalkRecord } from '@/utils/talk' +import { isElectronMode } from '@/utils/common' import { ServeClearTalkUnreadNum, ServeCreateTalkList } from '@/api/chat' import { useTalkStore, useDialogueStore, useSettingsStore } from '@/store' @@ -75,10 +76,16 @@ class Talk extends Base { * 获取聊天列表左侧的对话信息 */ getTalkText() { - let text = this.resource.content.replace(//g, '') - - if (this.resource.msg_type != message.ChatMsgTypeText) { - text = message.ChatMsgTypeMapping[this.resource.msg_type] + let text = '' + try{ + if (this.resource.msg_type != message.ChatMsgTypeText) { + text = message.ChatMsgTypeMapping[this.resource.msg_type] + } else { + text = this.resource.extra.content.replace(//g, '') + } + // console.debug('获取聊天列表左侧的对话信息成功', text) + } catch (e) { + // console.debug('获取聊天列表左侧的对话信息失败', e) } return text @@ -87,19 +94,15 @@ class Talk extends Base { // 播放提示音 play() { // 客户端有消息提示 - if (window.electron) { - return - } + if (isElectronMode()) return useSettingsStore().isPromptTone && palyMusic() } handle() { - // TODO 需要做消息去重处理 - + // 不是自己发送的消息则需要播放提示音 if (!this.isCurrSender()) { - // 判断消息是否来自于我自己,否则会提示消息通知 - // this.showMessageNocice() + this.play() } // 判断会话列表是否存在,不存在则创建 @@ -111,7 +114,6 @@ class Talk extends Base { if (this.isTalk(this.talk_type, this.receiver_id, this.sender_id)) { this.insertTalkRecord() } else { - this.play() this.updateTalkItem() } } @@ -156,8 +158,9 @@ class Talk extends Base { receiver_id }).then(({ code, data }) => { if (code == 200) { - useTalkStore().addItem(formatTalkItem(data)) - this.play() + let item = formatTalkItem(data) + item.unread_num = 1 + useTalkStore().addItem(item) } }) } @@ -168,6 +171,7 @@ class Talk extends Base { insertTalkRecord() { let record = this.resource + // 群成员变化的消息,需要更新群成员列表 if ([1102, 1103, 1104].includes(record.msg_type)) { useDialogueStore().updateGroupMembers() } @@ -177,7 +181,7 @@ class Talk extends Base { if (!this.isCurrSender()) { // 推送已读消息 setTimeout(() => { - socket.emit('im.message.read', { + ws.emit('im.message.read', { receiver_id: this.sender_id, msg_id: [this.resource.id] }) @@ -185,20 +189,18 @@ class Talk extends Base { } // 获取聊天面板元素节点 - let el = document.getElementById('imChatPanel') - if (!el) { - return - } + const el = document.getElementById('imChatPanel') + if (!el) return // 判断的滚动条是否在底部 - let isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight + const isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight if (isBottom || record.user_id == this.getAccountId()) { nextTick(() => { el.scrollTop = el.scrollHeight + 1000 }) } else { - useDialogueStore().setUnreadBubble(1) + useDialogueStore().setUnreadBubble() } useTalkStore().updateItem({ diff --git a/src/hooks/event/useBreakpoint.ts b/src/hooks/event/useBreakpoint.ts new file mode 100644 index 00000000..4b2d29c7 --- /dev/null +++ b/src/hooks/event/useBreakpoint.ts @@ -0,0 +1,89 @@ +import { ref, computed, ComputedRef, unref } from 'vue'; +import { useEventListener } from '@/hooks/event/useEventListener'; +import { screenMap, sizeEnum, screenEnum } from '@/enums/breakpointEnum'; + +let globalScreenRef: ComputedRef; +let globalWidthRef: ComputedRef; +let globalRealWidthRef: ComputedRef; + +export interface CreateCallbackParams { + screen: ComputedRef; + width: ComputedRef; + realWidth: ComputedRef; + screenEnum: typeof screenEnum; + screenMap: Map; + sizeEnum: typeof sizeEnum; +} + +export function useBreakpoint() { + return { + screenRef: computed(() => unref(globalScreenRef)), + widthRef: globalWidthRef, + screenEnum, + realWidthRef: globalRealWidthRef, + }; +} + +// Just call it once +export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void) { + const screenRef = ref(sizeEnum.XL); + const realWidthRef = ref(window.innerWidth); + + function getWindowWidth() { + const width = document.body.clientWidth; + const xs = screenMap.get(sizeEnum.XS)!; + const sm = screenMap.get(sizeEnum.SM)!; + const md = screenMap.get(sizeEnum.MD)!; + const lg = screenMap.get(sizeEnum.LG)!; + const xl = screenMap.get(sizeEnum.XL)!; + if (width < xs) { + screenRef.value = sizeEnum.XS; + } else if (width < sm) { + screenRef.value = sizeEnum.SM; + } else if (width < md) { + screenRef.value = sizeEnum.MD; + } else if (width < lg) { + screenRef.value = sizeEnum.LG; + } else if (width < xl) { + screenRef.value = sizeEnum.XL; + } else { + screenRef.value = sizeEnum.XXL; + } + realWidthRef.value = width; + } + + useEventListener({ + el: window, + name: 'resize', + + listener: () => { + getWindowWidth(); + resizeFn(); + }, + // wait: 100, + }); + + getWindowWidth(); + globalScreenRef = computed(() => unref(screenRef)); + globalWidthRef = computed((): number => screenMap.get(unref(screenRef)!)!); + globalRealWidthRef = computed((): number => unref(realWidthRef)); + + function resizeFn() { + fn?.({ + screen: globalScreenRef, + width: globalWidthRef, + realWidth: globalRealWidthRef, + screenEnum, + screenMap, + sizeEnum, + }); + } + + resizeFn(); + return { + screenRef: globalScreenRef, + screenEnum, + widthRef: globalWidthRef, + realWidthRef: globalRealWidthRef, + }; +} diff --git a/src/hooks/event/useEventListener.ts b/src/hooks/event/useEventListener.ts new file mode 100644 index 00000000..600afabd --- /dev/null +++ b/src/hooks/event/useEventListener.ts @@ -0,0 +1,62 @@ +import type { Ref } from 'vue'; + +import { ref, watch, unref } from 'vue'; +import { useThrottleFn, useDebounceFn } from '@vueuse/core'; + +export type RemoveEventFn = () => void; + +export interface UseEventParams { + el?: Element | Ref | Window | any; + name: string; + listener: EventListener; + options?: boolean | AddEventListenerOptions; + autoRemove?: boolean; + isDebounce?: boolean; + wait?: number; +} + +export function useEventListener({ + el = window, + name, + listener, + options, + autoRemove = true, + isDebounce = true, + wait = 80, +}: UseEventParams): { removeEvent: RemoveEventFn } { + /* eslint-disable-next-line */ + let remove: RemoveEventFn = () => { + }; + const isAddRef = ref(false); + + if (el) { + const element: Ref = ref(el as Element); + + const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait); + const realHandler = wait ? handler : listener; + const removeEventListener = (e: Element) => { + isAddRef.value = true; + e.removeEventListener(name, realHandler, options); + }; + const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options); + + const removeWatch = watch( + element, + (v, _ov, cleanUp) => { + if (v) { + !unref(isAddRef) && addEventListener(v); + cleanUp(() => { + autoRemove && removeEventListener(v); + }); + } + }, + { immediate: true } + ); + + remove = () => { + removeEventListener(element.value); + removeWatch(); + }; + } + return { removeEvent: remove }; +} diff --git a/src/hooks/event/useWindowSizeFn copy.ts b/src/hooks/event/useWindowSizeFn copy.ts new file mode 100644 index 00000000..7b18ca0e --- /dev/null +++ b/src/hooks/event/useWindowSizeFn copy.ts @@ -0,0 +1,36 @@ +import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'; +import { useDebounceFn } from '@vueuse/core'; + +interface WindowSizeOptions { + once?: boolean; + immediate?: boolean; + listenerOptions?: AddEventListenerOptions | boolean; +} + +export function useWindowSizeFn(fn: Fn, wait = 150, options?: WindowSizeOptions) { + let handler = () => { + fn(); + }; + const handleSize = useDebounceFn(handler, wait); + handler = handleSize; + + const start = () => { + if (options && options.immediate) { + handler(); + } + window.addEventListener('resize', handler); + }; + + const stop = () => { + window.removeEventListener('resize', handler); + }; + + tryOnMounted(() => { + start(); + }); + + tryOnUnmounted(() => { + stop(); + }); + return [start, stop]; +} diff --git a/src/hooks/event/useWindowSizeFn.ts b/src/hooks/event/useWindowSizeFn.ts new file mode 100644 index 00000000..01cdc752 --- /dev/null +++ b/src/hooks/event/useWindowSizeFn.ts @@ -0,0 +1,35 @@ +import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core'; + +interface WindowSizeOptions { + once?: boolean; + immediate?: boolean; + listenerOptions?: AddEventListenerOptions | boolean; +} + +export function useWindowSizeFn(fn: Fn, wait = 150, options?: WindowSizeOptions) { + let handler = () => { + fn(); + }; + const handleSize = useDebounceFn(handler, wait); + handler = handleSize; + + const start = () => { + if (options && options.immediate) { + handler(); + } + window.addEventListener('resize', handler); + }; + + const stop = () => { + window.removeEventListener('resize', handler); + }; + + tryOnMounted(() => { + start(); + }); + + tryOnUnmounted(() => { + stop(); + }); + return [start, stop]; +} diff --git a/src/hooks/setting/useDesignSetting.ts b/src/hooks/setting/useDesignSetting.ts new file mode 100644 index 00000000..0a873b5d --- /dev/null +++ b/src/hooks/setting/useDesignSetting.ts @@ -0,0 +1,18 @@ +import { computed } from 'vue'; +import { useDesignSettingStore } from '@/store/modules/designSetting'; + +export function useDesignSetting() { + const designStore = useDesignSettingStore(); + + const getDarkTheme = computed(() => designStore.darkTheme); + + const getAppTheme = computed(() => designStore.appTheme); + + const getAppThemeList = computed(() => designStore.appThemeList); + + return { + getDarkTheme, + getAppTheme, + getAppThemeList, + }; +} diff --git a/src/hooks/useConnectStatus.ts b/src/hooks/useConnectStatus.ts index 6fce1871..3fc2b2a9 100644 --- a/src/hooks/useConnectStatus.ts +++ b/src/hooks/useConnectStatus.ts @@ -2,12 +2,13 @@ import { watchEffect } from 'vue' import { useRouter } from 'vue-router' import { useSettingsStore } from '@/store' import { isLoggedIn } from '@/utils/auth' -import socket from '@/socket' +import ws from '@/connect' +import { useUserStore } from '@/store' export const useConnectStatus = () => { const settingsStore = useSettingsStore() const router = useRouter() - + const userStore = useUserStore() watchEffect(() => { if (settingsStore.isLeaveWeb) { return @@ -18,7 +19,8 @@ export const useConnectStatus = () => { const paths = ['/auth/login', '/auth/register', '/auth/forget'] if (!paths.includes(pathname) && isLoggedIn()) { - !socket.isConnect() && socket.connect() + userStore.loadSetting() + !ws.isConnect() && ws.connect('useConnectStatus...') } }) diff --git a/src/hooks/useIconProvider.js b/src/hooks/useIconProvider.js deleted file mode 100644 index 2ed24b3b..00000000 --- a/src/hooks/useIconProvider.js +++ /dev/null @@ -1,15 +0,0 @@ -import { IconProvider, DEFAULT_ICON_CONFIGS } from '@icon-park/vue-next' - -export function useIconProvider() { - function iconProvider() { - IconProvider({ - ...DEFAULT_ICON_CONFIGS, - theme: 'outline', - size: 24, - strokeWidth: 3, - strokeLinejoin: 'bevel' - }) - } - - return { iconProvider } -} diff --git a/src/hooks/useSessionMenu.ts b/src/hooks/useSessionMenu.ts new file mode 100644 index 00000000..3d2d1a56 --- /dev/null +++ b/src/hooks/useSessionMenu.ts @@ -0,0 +1,277 @@ +import { reactive, nextTick, computed, h, inject } from 'vue' +import { ISession } from '@/types/chat' +import { renderIcon } from '@/utils/util' +import { + ArrowUp, + ArrowDown, + Logout, + Delete, + Clear, + Remind, + CloseRemind, + EditTwo, + IdCard +} from '@icon-park/vue-next' +import { ServeTopTalkList, ServeDeleteTalkList, ServeSetNotDisturb } from '@/api/chat' +import { useDialogueStore, useTalkStore } from '@/store' +import { ServeSecedeGroup } from '@/api/group' +import { ServeDeleteContact, ServeEditContactRemark } from '@/api/contact' +import { NInput } from 'naive-ui' + +interface IDropdown { + options: any[] + show: boolean + x: number + y: number + item: any +} + +export function useSessionMenu() { + const dropdown: IDropdown = reactive({ + options: [], + show: false, + x: 0, + y: 0, + item: {} + }) + + const dialogueStore = useDialogueStore() + const talkStore = useTalkStore() + + const user: any = inject('$user') + + // 当前会话索引 + const indexName = computed(() => dialogueStore.index_name) + + const onContextMenu = (e: any, item: ISession) => { + dropdown.show = false + dropdown.item = Object.assign({}, item) + dropdown.options = [] + + const options: any[] = [] + + if (item.talk_type == 1) { + options.push({ + icon: renderIcon(IdCard), + label: '好友信息', + key: 'info' + }) + + options.push({ + icon: renderIcon(EditTwo), + label: '修改备注', + key: 'remark' + }) + } + + options.push({ + icon: renderIcon(item.is_top ? ArrowDown : ArrowUp), + label: item.is_top ? '取消置顶' : '会话置顶', + key: 'top' + }) + + options.push({ + icon: renderIcon(item.is_disturb ? Remind : CloseRemind), + label: item.is_disturb ? '关闭免打扰' : '开启免打扰', + key: 'disturb' + }) + + options.push({ + icon: renderIcon(Clear), + label: '移除会话', + key: 'remove' + }) + + if (item.talk_type == 1) { + options.push({ + icon: renderIcon(Delete), + label: '删除好友', + key: 'delete_contact' + }) + } else { + options.push({ + icon: renderIcon(Logout), + label: '退出群聊', + key: 'signout_group' + }) + } + + dropdown.options = [...options] + + nextTick(() => { + dropdown.show = true + dropdown.x = e.clientX + dropdown.y = e.clientY + }) + + e.preventDefault() + } + + const onCloseContextMenu = () => { + dropdown.show = false + dropdown.item = {} + } + + const onDeleteTalk = (index_name = '') => { + talkStore.delItem(index_name) + + index_name === indexName.value && dialogueStore.$reset() + } + + const onUserInfo = (item: ISession) => { + user(item.receiver_id) + } + + // 移除会话 + const onRemoveTalk = (item: ISession) => { + ServeDeleteTalkList({ + list_id: item.id + }).then(({ code }) => { + if (code == 200) { + onDeleteTalk(item.index_name) + } + }) + } + + // 设置消息免打扰 + const onSetDisturb = (item: ISession) => { + ServeSetNotDisturb({ + talk_type: item.talk_type, + receiver_id: item.receiver_id, + is_disturb: item.is_disturb == 0 ? 1 : 0 + }).then(({ code, message }) => { + if (code == 200) { + window['$message'].success('设置成功!') + talkStore.updateItem({ + index_name: item.index_name, + is_disturb: item.is_disturb == 0 ? 1 : 0 + }) + } else { + window['$message'].error(message) + } + }) + } + + // 置顶会话 + const onToTopTalk = (item: ISession) => { + if (item.is_top == 0 && talkStore.topItems.length >= 18) { + return window['$message'].info('置顶最多不能超过18个会话') + } + + ServeTopTalkList({ + list_id: item.id, + type: item.is_top == 0 ? 1 : 2 + }).then(({ code, message }) => { + if (code == 200) { + talkStore.updateItem({ + index_name: item.index_name, + is_top: item.is_top == 0 ? 1 : 0 + }) + } else { + window['$message'].error(message) + } + }) + } + + // 移除联系人 + const onDeleteContact = (item: ISession) => { + const name = item.remark || item.name + + window['$dialog'].create({ + showIcon: false, + title: `删除 [${name}] 联系人?`, + content: '删除后不再接收对方任何消息。', + positiveText: '确定', + negativeText: '取消', + onPositiveClick: () => { + ServeDeleteContact({ + friend_id: item.receiver_id + }).then(({ code, message }) => { + if (code == 200) { + window['$message'].success('删除联系人成功') + onDeleteTalk(item.index_name) + } else { + window['$message'].error(message) + } + }) + } + }) + } + + // 退出群聊 + const onSignOutGroup = (item: ISession) => { + window['$dialog'].create({ + showIcon: false, + title: `退出 [${item.name}] 群聊?`, + content: '退出后不再接收此群的任何消息。', + positiveText: '确定', + negativeText: '取消', + onPositiveClick: () => { + ServeSecedeGroup({ + group_id: item.receiver_id + }).then(({ code, message }) => { + if (code == 200) { + window['$message'].success('已退出群聊') + onDeleteTalk(item.index_name) + } else { + window['$message'].error(message) + } + }) + } + }) + } + + const onChangeRemark = (item: ISession) => { + let remark = '' + window['$dialog'].create({ + showIcon: false, + title: '修改备注', + content: () => { + return h(NInput, { + defaultValue: item.remark, + placeholder: '请输入备注信息', + style: { marginTop: '20px' }, + onInput: (value) => (remark = value), + autofocus: true + }) + }, + negativeText: '取消', + positiveText: '修改备注', + onPositiveClick: () => { + ServeEditContactRemark({ + friend_id: item.receiver_id, + remark: remark + }).then(({ code, message }) => { + if (code == 200) { + window['$message'].success('备注成功') + talkStore.updateItem({ + index_name: item.index_name, + remark: remark + }) + } else { + window['$message'].error(message) + } + }) + } + }) + } + + // 会话列表右键菜单回调事件 + const onContextMenuTalkHandle = (key: string) => { + // 注册回调事件 + const evnets = { + info: onUserInfo, + top: onToTopTalk, + remove: onRemoveTalk, + disturb: onSetDisturb, + signout_group: onSignOutGroup, + delete_contact: onDeleteContact, + remark: onChangeRemark + } + + dropdown.show = false + evnets[key] && evnets[key](dropdown.item) + } + + return { dropdown, onContextMenu, onCloseContextMenu, onContextMenuTalkHandle, onToTopTalk } +} diff --git a/src/hooks/useTalkRecord.ts b/src/hooks/useTalkRecord.ts index 2c017d89..aee02727 100644 --- a/src/hooks/useTalkRecord.ts +++ b/src/hooks/useTalkRecord.ts @@ -1,7 +1,7 @@ import { reactive, computed, nextTick } from 'vue' import { ServeTalkRecords } from '@/api/chat' import { useDialogueStore } from '@/store' -import { IMessageRecord } from '@/types/chat' +import { ITalkRecord } from '@/types/chat' import { formatTalkRecord } from '@/utils/talk' import { addClass, removeClass } from '@/utils/dom' @@ -14,7 +14,7 @@ interface Params { export const useTalkRecord = (uid: number) => { const dialogueStore = useDialogueStore() - const records = computed((): IMessageRecord[] => dialogueStore.records) + const records = computed((): ITalkRecord[] => dialogueStore.records) const location = reactive({ msgid: '', @@ -25,7 +25,7 @@ export const useTalkRecord = (uid: number) => { receiver_id: 0, talk_type: 0, status: 0, - minRecord: 0 + cursor: 0 }) const onJumpMessage = (msgid: string) => { @@ -47,11 +47,10 @@ export const useTalkRecord = (uid: number) => { const el = document.getElementById('imChatPanel') - el?.scrollTo({ + return el?.scrollTo({ top: 0, behavior: 'smooth' }) - return } location.msgid = '' @@ -71,8 +70,9 @@ export const useTalkRecord = (uid: number) => { // 加载数据列表 const load = async (params: Params) => { const request = { - ...params, - record_id: loadConfig.minRecord, + talk_type: params.talk_type, + receiver_id: params.receiver_id, + cursor: loadConfig.cursor, limit: 30 } @@ -97,9 +97,9 @@ export const useTalkRecord = (uid: number) => { return (location.msgid = '') } - const items = (data.items || []).map((item: IMessageRecord) => formatTalkRecord(uid, item)) + const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item)) - if (request.record_id == 0) { + if (request.cursor == 0) { // 判断是否是初次加载 dialogueStore.clearDialogueRecord() } @@ -108,19 +108,21 @@ export const useTalkRecord = (uid: number) => { loadConfig.status = items.length >= request.limit ? 1 : 2 - loadConfig.minRecord = data.record_id + loadConfig.cursor = data.cursor nextTick(() => { - if (!el) return + const el = document.getElementById('imChatPanel') - if (request.record_id == 0) { - el.scrollTop = el.scrollHeight + if (el) { + if (request.cursor == 0) { + el.scrollTop = el.scrollHeight - setTimeout(() => { - el && (el.scrollTop = el?.scrollHeight) - }, 100) - } else { - el.scrollTop = el.scrollHeight - scrollHeight + setTimeout(() => { + el.scrollTop = el.scrollHeight + 1000 + }, 50) + } else { + el.scrollTop = el.scrollHeight - scrollHeight + } } if (location.msgid) { @@ -140,7 +142,7 @@ export const useTalkRecord = (uid: number) => { } const onLoad = (params: Params) => { - loadConfig.minRecord = 0 + loadConfig.cursor = 0 loadConfig.receiver_id = params.receiver_id loadConfig.talk_type = params.talk_type diff --git a/src/hooks/useThemeMode.ts b/src/hooks/useThemeMode.ts index eda9bfd6..2243cbb5 100644 --- a/src/hooks/useThemeMode.ts +++ b/src/hooks/useThemeMode.ts @@ -10,7 +10,6 @@ export function useThemeMode() { const theme = settingsStore.darkTheme ? 'dark' : 'light' document.getElementsByTagName('html')[0].dataset.theme = theme - document.getElementsByTagName('html')[0].style = '' return settingsStore.darkTheme ? darkTheme : undefined }) diff --git a/src/hooks/useUnreadMessage.ts b/src/hooks/useUnreadMessage.ts index 966ae53f..e10a8251 100644 --- a/src/hooks/useUnreadMessage.ts +++ b/src/hooks/useUnreadMessage.ts @@ -13,7 +13,8 @@ export const useUnreadMessage = () => { } else { setInterval(() => { if (useTalk.talkUnreadNum > 0) { - el.innerText = el.innerText == title ? '您有新的消息未读' : title + el.innerText = + el.innerText == title ? `您有新的消息未读(${useTalk.talkUnreadNum})` : title } else { el.innerText = title } diff --git a/src/hooks/web/usePermission.ts b/src/hooks/web/usePermission.ts new file mode 100644 index 00000000..66f8e205 --- /dev/null +++ b/src/hooks/web/usePermission.ts @@ -0,0 +1,52 @@ +import { useUserStore } from '@/store/modules/user'; + +export function usePermission() { + const userStore = useUserStore(); + + /** + * 检查权限 + * @param accesses + */ + function _somePermissions(accesses: string[]) { + return userStore.getPermissions.some((item) => { + const { value }: any = item; + return accesses.includes(value); + }); + } + + /** + * 判断是否存在权限 + * 可用于 v-if 显示逻辑 + * */ + function hasPermission(accesses: string[]): boolean { + if (!accesses || !accesses.length) return true; + return _somePermissions(accesses); + } + + /** + * 是否包含指定的所有权限 + * @param accesses + */ + function hasEveryPermission(accesses: string[]): boolean { + const permissionsList = userStore.getPermissions; + if (Array.isArray(accesses)) { + return permissionsList.every((access: any) => accesses.includes(access.value)); + } + throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`); + } + + /** + * 是否包含其中某个权限 + * @param accesses + * @param accessMap + */ + function hasSomePermission(accesses: string[]): boolean { + const permissionsList = userStore.getPermissions; + if (Array.isArray(accesses)) { + return permissionsList.some((access: any) => accesses.includes(access.value)); + } + throw new Error(`[hasSomePermission]: ${accesses} should be a array !`); + } + + return { hasPermission, hasEveryPermission, hasSomePermission }; +} diff --git a/src/layout/MainLayout.vue b/src/layout/MainLayout.vue index e3c54294..431e84e2 100644 --- a/src/layout/MainLayout.vue +++ b/src/layout/MainLayout.vue @@ -1,6 +1,6 @@ + + +{{ title }} + + + diff --git a/src/layout/component/Menu.vue b/src/layout/component/Menu.vue index fbe59a94..1f8b7557 100644 --- a/src/layout/component/Menu.vue +++ b/src/layout/component/Menu.vue @@ -10,7 +10,18 @@ import { Message, NotebookAndPen, People, - SmartOptimization + SmartOptimization, + Send, + TimedMail, + ThinkingProblem, + WrongUser, + Mark, + KeyboardOne, + Help, + Dashboard, + Workbench, + AppStore, + System, } from '@icon-park/vue-next' defineProps({ @@ -31,6 +42,11 @@ const color = computed(() => { }) const menus = reactive([ +{ + link: '/workplace', + icon: markRaw(Workbench), + title: '工作台' + }, { link: '/message', icon: markRaw(Message), @@ -44,19 +60,64 @@ const menus = reactive([ hotspot: computed(() => userStore.isContactApply || userStore.isGroupApply) }, { - link: '/note', - icon: markRaw(NotebookAndPen), - title: '笔记' + link: '/app', + icon: markRaw(System), + title: '应用中心' + }, + // { + // link: '/plugin', + // icon: markRaw(System), + // title: '插件市场' + // }, + { + link: '/chatbot', + icon: markRaw(SmartOptimization), + title: 'ChatBot' }, // { // link: '/settings', // icon: markRaw(SmartOptimization), // title: 'Ai助手' // }, + { + link: '/qa', + icon: markRaw(ThinkingProblem), + title: '自动问答' + }, + // { + // link: '/groupnotice', + // icon: markRaw(Send), + // title: '群发消息' + // }, + // { + // link: '/statistic', + // icon: markRaw(Mark), + // title: '统计打卡' + // }, + // { + // link: '/note', + // icon: markRaw(NotebookAndPen), + // title: '素材' + // }, + // { + // link: '/whitelist', + // icon: markRaw(WrongUser), + // title: '黑白名单' + // }, + // { + // link: '/keyword', + // icon: markRaw(KeyboardOne), + // title: '关键词' + // }, { link: '/settings', icon: markRaw(SettingTwo), title: '设置' + }, + { + link: '/help', + icon: markRaw(Help), + title: '帮助' } ]) @@ -131,7 +192,7 @@ const isActive = (menu) => {