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 即时聊天 -GitHub stars badge GitHub forks badge GitHub license badge +# Chat Studio 对话工作台 -### 项目介绍 -Lumen IM 是一个网页版在线聊天项目,前端使用 Naive UI + Vue3,后端采用 GO 开发。 +[![GitHub Pages](https://img.shields.io/badge/GitHub%20Pages-im2.vlist.cc-brightgreen.svg)](https://atorber.github.io/chat-studio/) GitHub stars badge GitHub forks badge GitHub license badge + +## 项目介绍 + +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; +![image](https://github.com/atorber/chat-studio/assets/19552906/9c7ec288-b364-491a-a9db-eebc04a578d6) - 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 +[![Star History Chart](https://api.star-history.com/svg?repos=atorber/chat-studio&type=Date)](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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + + + 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 @@ + + 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 @@ - 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 @@
 
+
+
+
+
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) => {