├── .env ├── .env.development ├── .env.production ├── .env.testing ├── .eslintrc.cjs ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── apply │ │ └── index.js │ ├── auth │ │ └── index.js │ ├── conversation │ │ └── index.js │ ├── expression │ │ └── index.js │ ├── file │ │ └── index.js │ ├── friend │ │ └── index.js │ ├── grouping │ │ └── index.js │ ├── index.js │ ├── media │ │ └── index.js │ ├── message │ │ └── index.js │ ├── room │ │ └── index.js │ └── user │ │ └── index.js ├── assets │ ├── fonts │ │ ├── AppleChancery.ttf │ │ ├── BarbaraHand.ttf │ │ └── JoinedUp.ttf │ ├── images │ │ ├── gitee.png │ │ ├── github.png │ │ ├── group.png │ │ ├── logo.png │ │ ├── official-account-qr-code.png │ │ ├── official-account.png │ │ ├── qq.png │ │ ├── wechat-qr-code.png │ │ └── wechat.png │ └── sass │ │ ├── _animation.scss │ │ ├── _element.scss │ │ ├── _font.scss │ │ ├── _global.scss │ │ ├── _normalize.scss │ │ ├── _nprogress.scss │ │ ├── _root.scss │ │ ├── _transition.scss │ │ ├── _variable.scss │ │ └── index.scss ├── common │ ├── constants │ │ ├── file.js │ │ └── index.js │ ├── enums │ │ ├── apply.js │ │ ├── index.js │ │ ├── media.js │ │ ├── message.js │ │ ├── user.js │ │ └── websocket.js │ ├── props │ │ └── index.js │ ├── rules │ │ └── user.js │ └── utils │ │ ├── index.js │ │ ├── regular.js │ │ ├── storage.js │ │ └── websocket.js ├── components │ ├── apply-friend-dialog │ │ ├── components │ │ │ └── form-ui │ │ │ │ ├── index.js │ │ │ │ └── index.vue │ │ └── index.vue │ ├── avatar-upload │ │ └── index.vue │ ├── avatar │ │ └── index.vue │ ├── brand │ │ └── index.vue │ ├── captcha-input │ │ └── index.vue │ ├── card │ │ └── index.vue │ ├── context-menu │ │ └── index.vue │ ├── countdown-button │ │ └── index.vue │ ├── countdown │ │ └── index.vue │ ├── empty │ │ └── index.vue │ ├── filing │ │ └── index.vue │ ├── loading │ │ └── index.vue │ ├── message-send-status │ │ └── index.vue │ ├── online-dot │ │ └── index.vue │ ├── panel │ │ └── index.vue │ ├── router │ │ └── index.vue │ ├── search-dialog │ │ ├── components │ │ │ ├── group-list-panel │ │ │ │ └── index.vue │ │ │ ├── user-card │ │ │ │ └── index.vue │ │ │ └── user-list-panel │ │ │ │ └── index.vue │ │ └── index.vue │ ├── timer │ │ └── index.vue │ ├── upload │ │ └── index.vue │ ├── user-dialog │ │ └── index.vue │ └── user-panel │ │ └── index.vue ├── directive │ ├── index.js │ └── longpress │ │ └── index.js ├── hooks │ ├── bind-exposed.js │ ├── debounce-ref.js │ └── model.js ├── main.js ├── router │ └── index.js ├── stores │ ├── index.js │ ├── modules │ │ ├── apply.js │ │ ├── auth.js │ │ ├── conversation.js │ │ ├── expression.js │ │ ├── grouping.js │ │ ├── media.js │ │ ├── room.js │ │ ├── user.js │ │ └── websocket.js │ └── root.js └── views │ ├── apply │ ├── components │ │ ├── apply-card │ │ │ └── index.vue │ │ ├── apply-panel │ │ │ └── index.vue │ │ └── pass-dialog │ │ │ ├── components │ │ │ └── form-ui │ │ │ │ ├── index.js │ │ │ │ └── index.vue │ │ │ └── index.vue │ └── index.vue │ ├── conversation │ ├── components │ │ ├── conversation-card │ │ │ └── index.vue │ │ ├── editor │ │ │ ├── components │ │ │ │ ├── audio │ │ │ │ │ └── index.vue │ │ │ │ ├── expression │ │ │ │ │ └── index.vue │ │ │ │ ├── file │ │ │ │ │ └── index.vue │ │ │ │ ├── image │ │ │ │ │ └── index.vue │ │ │ │ ├── video-call │ │ │ │ │ └── index.vue │ │ │ │ └── voice-call │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── group-user-panel │ │ │ └── index.vue │ │ ├── group-user │ │ │ └── index.vue │ │ ├── message-panel │ │ │ └── index.vue │ │ └── message │ │ │ ├── components │ │ │ ├── audio-message │ │ │ │ └── index.vue │ │ │ ├── file-message │ │ │ │ └── index.vue │ │ │ ├── image-message │ │ │ │ └── index.vue │ │ │ └── text-message │ │ │ │ └── index.vue │ │ │ └── index.vue │ └── index.vue │ ├── friend │ ├── components │ │ ├── friend-crad │ │ │ └── index.vue │ │ ├── friend-panel │ │ │ └── index.vue │ │ └── headbar │ │ │ └── index.vue │ └── index.vue │ ├── group │ └── index.vue │ ├── layout │ ├── components │ │ ├── media-dialog │ │ │ ├── components │ │ │ │ ├── operation │ │ │ │ │ └── index.vue │ │ │ │ ├── status │ │ │ │ │ └── index.vue │ │ │ │ └── user-box │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── promote │ │ │ └── index.vue │ │ ├── sidebar │ │ │ ├── components │ │ │ │ ├── edit-email-dialog │ │ │ │ │ ├── index.vue │ │ │ │ │ └── ui │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.vue │ │ │ │ ├── edit-info-dialog │ │ │ │ │ ├── index.vue │ │ │ │ │ └── ui │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.vue │ │ │ │ ├── publicize │ │ │ │ │ └── index.vue │ │ │ │ └── tabbar │ │ │ │ │ ├── components │ │ │ │ │ ├── tab-apply │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── tab-conversation │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── tab-exit │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── tab-friend │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── tab-group │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── tab │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ └── websocket │ │ │ └── index.vue │ └── index.vue │ └── login │ ├── components │ ├── login-form │ │ ├── index.vue │ │ └── ui │ │ │ ├── index.js │ │ │ └── index.vue │ ├── other │ │ ├── index.vue │ │ └── qq │ │ │ └── index.vue │ ├── panel │ │ └── index.vue │ └── register-form │ │ ├── index.vue │ │ └── ui │ │ ├── index.js │ │ └── index.vue │ └── index.vue └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | # 项目启动端口 2 | VITE_PORT = '9585' 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/.env.development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/.env.production -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/.env.testing -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | root: true, 4 | 'extends': [ 5 | 'plugin:vue/vue3-essential', 6 | 'eslint:recommended', 7 | './.eslintrc-auto-import.json', 8 | ], 9 | env: { 10 | browser: true, 11 | node: true, 12 | es6: true, 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 'latest' 16 | }, 17 | rules: { 18 | 'vue/multi-word-component-names': [0], 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | dist/ 18 | dist.zip 19 | chatterbox/ 20 | chatterbox.zip 21 | deploy/ 22 | .eslintrc-auto-import.json 23 | 24 | /cypress/videos/ 25 | /cypress/screenshots/ 26 | 27 | # Editor directories and files 28 | .vscode/* 29 | !.vscode/extensions.json 30 | .idea 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GuMingChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | chatterbox 4 | 5 |

6 | 7 |

8 |

9 | Chatterbox(话匣子) 10 |

11 |

12 | 13 |

14 | 15 | vue 16 | 17 | 18 | element-plus 19 | 20 |

21 | 22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

36 | 37 | ### 简介 38 | 39 | 🎈[Chatterbox(话匣子)](https://github.com/gmingchen/chatterbox)是`im-vue`重构后的即时聊天系统🆕。 40 | 41 | 🎃目前前端只有基于 [vue3](https://github.com/vuejs/vue-next)、[element-plus](https://github.com/element-plus/element-plus) 实现的相关内容,后续会分别实现 `react`、`h5`版本。 42 | 🤿后端是基于 __`java`__ 的 __`springboot`__ 、 __`netty`__ 实现。 43 | 44 | 🔔比较关键的技术点是通过 `Websocket` 实现了消息的实时传递 和 通过 `RTCPeerConnection` 实现语音通话、视频通话。 45 | 46 | [![Star History](https://api.star-history.com/svg?repos=gmingchen/chatterbox&type=Date)](https://api.star-history.com/svg?repos=gmingchen/chatterbox&type=Date) 47 | 48 | ###### 已内置如下功能: 49 | - [X] 邮箱登录、注册、个人信息编辑 50 | - [X] 用户搜索 51 | - [X] 好友申请 52 | - [X] 好友私聊、群聊 53 | - [X] 文字消息 54 | - [X] 图片消息 55 | - [X] 音频消息 56 | - [X] 文件消息 57 | - [X] 好友通话 58 | - [X] 语音通话 59 | - [X] 视频通话 60 | 61 | 🏷️🏷️🏷️后续会 __`持续迭代更新`__,点个 ⭐star 不错过更多的功能更新😎。 62 | 63 | ### 在线预览 64 | > ☀️ 65 | > [👉 在线预览 👀](https://chatterbox.gumingchen.icu) 66 | > 67 | > 服务器比较low,访问有点慢!等有条件了再加配!😬 68 | > 69 | > 如果觉得还不错的话,请点个 ⭐star 支持一下吧,这将是对我最大的支持和鼓励☕! 70 | > 🌙 71 | 72 | > ⚠️ 73 | > 如果想要旧版本相关内容请移步👉` [old分支](https://github.com/gmingchen/chatterbox/tree/old) 74 | > 🛑 75 | 76 | #### 演示图片 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
演示图片演示图片
演示图片演示图片
演示图片演示图片
演示图片演示图片
演示图片演示图片
演示图片
103 | 104 | ### 开发 105 | > ⚠️ 106 | > 前提条件: 已安装 18.3 或更高版本的 Node.js ` 107 | > 建议不要用直接使用 cnpm 安装,可以通过配置 registry 来解决 npm 安装速度慢或中断的问题。 108 | > 🛑 109 | ```bash 110 | 111 | # 克隆项目 112 | git clone https://github.com/gmingchen/chatterbox.git 113 | 114 | # 进入项目目录 115 | cd chatterbox 116 | 117 | # 安装依赖 118 | npm install 119 | 120 | # 启动服务 121 | npm run dev 122 | 123 | # 发布 124 | npm run build 125 | ``` 126 | 127 | ### 关于作者 128 | Hi there, I'm [Slipper](https://github.com/gmingchen)(拖孩)👋. Thank you for your attention ⭐! 129 | I'm a code enthusiast who has been working in the IT industry for many years. 130 | I like open source and all interesting things and want to try to do it. 131 | I want to be an interesting person and create something that can be remembered by others. 132 | If you want to write code with me, you can contact me for internal promotion. 133 | 134 | - 🔭 I’m currently working on [万店掌](https://www.ovopark.com/) 135 | - 📫 How to reach me: ```🐧1240235512``` ```🛰️Gy1240235512``` ```📪gumingchen@foxmail.com``` 136 | - 🌏 How to follow me: [Github](https://github.com/gmingchen) [Gitee](https://gitee.com/shychen) [掘金](https://juejin.cn/user/4103845398710846) [简书](https://www.jianshu.com/u/81a5a02678d3) 137 | - ❤️ I like playing 🎮, sleeping in 🛌 and coding 👨‍💻. 138 | 139 | ![Github stats](https://github-readme-stats.vercel.app/api?username=gmingchen&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515&include_all_commits=true&hide=["contribs"]) 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 152 | 155 | 158 | 161 | 162 |
公众号个人微信交流群摸鱼群
150 | 公众号:loafer-man 151 | 153 | 微信:Gy1240235512 154 | 156 | 交流群 157 | 159 | 摸鱼群 160 |
163 | 164 | > 🤑 165 | > 如果有需要完整代码的可以加作者微信📨,联系作者👦 166 | > 🎯不免费,有偿💸获取完整代码 167 | > 📃开发文档暂时没有编写,空闲了会补上的哦🎮 168 | > 💰 169 | 170 | ### 捐赠 171 | >💖 172 | >如果你觉得这个项目帮助到了你,你可以帮作者买一杯热饮表示鼓励 ☕ 173 | >🦀🦀 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 184 | 187 | 188 |
微信捐赠支付宝捐赠
182 | 微信捐赠 183 | 185 | 支付宝捐赠 186 |
189 | 190 | ### 其它开源项目 191 | 192 | [vue3-element-plus-admin](https://github.com/gmingchen/vue3-element-plus-admin) 193 | 194 | 是一个管理后台基础功能框架,基于 [vue3](https://github.com/vuejs/vue-next) 、 [element-plus](https://github.com/element-plus/element-plus) 和 [typescript](https://github.com/microsoft/TypeScript) 实现。内置了 i18n 国际化,动态路由,权限验证。-[私活神器] 195 | 196 | [java-admin-base](https://github.com/gmingchen/java-admin-base) 197 | 198 | 是一个管理后台基础功能框架 [base-refactoring](https://github.com/gmingchen/vue3-element-plus-admin/tree/base-refactoring) 分支的后端代码,基于 __`java`__ 的 __`springboot`__ 199 | 200 | [nod-server](https://github.com/gmingchen/node-server) 201 | 是一个基于 node 开发的后端服务框架,只要你会 SQL 就也可以写接口了,再也不用看后端的脸色了。 202 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chatterbox 8 | 69 | 78 | 79 | 80 |
81 |
82 | 83 | C 84 | h 85 | a 86 | t 87 | t 88 | e 89 | r 90 | b 91 | o 92 | x 93 |
94 |
95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatterbox", 3 | "version": "1.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "deploy": "vite build --mode production && cross-env NODE_ENV=production node ./deploy", 10 | "preview": "vite preview", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" 12 | }, 13 | "dependencies": { 14 | "@element-plus/icons-vue": "^2.3.1", 15 | "axios": "^1.6.8", 16 | "element-plus": "^2.7.2", 17 | "js-cookie": "^3.0.5", 18 | "nprogress": "^0.2.0", 19 | "pinia": "^2.1.7", 20 | "vue": "^3.4.21", 21 | "vue-router": "^4.3.0" 22 | }, 23 | "devDependencies": { 24 | "@iconify-json/ep": "^1.1.15", 25 | "@iconify-json/ic": "^1.1.17", 26 | "@vitejs/plugin-vue": "^5.0.4", 27 | "@vitejs/plugin-vue-jsx": "^3.1.0", 28 | "cross-env": "^7.0.3", 29 | "eslint": "^8.57.0", 30 | "eslint-plugin-vue": "^9.23.0", 31 | "ora": "^5.4.1", 32 | "qs": "^6.12.1", 33 | "sass": "^1.77.1", 34 | "sass-loader": "^14.2.1", 35 | "scp2": "^0.5.0", 36 | "unplugin-auto-import": "^0.17.6", 37 | "unplugin-icons": "^0.19.0", 38 | "unplugin-vue-components": "^0.27.0", 39 | "vite": "^5.2.8", 40 | "vite-plugin-vue-devtools": "^7.0.25" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 25 | 26 | 35 | -------------------------------------------------------------------------------- /src/api/apply/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 好友申请 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function applyFriendApi(data) { 9 | return service({ 10 | url: '/apply/friend', 11 | method: 'post', 12 | data 13 | }) 14 | } 15 | 16 | /** 17 | * 审核好友 18 | * @param {*} params 19 | * @returns 20 | */ 21 | export function reviewFriendApi(data) { 22 | return service({ 23 | url: '/apply/friend/review', 24 | method: 'post', 25 | data 26 | }) 27 | } 28 | 29 | 30 | /** 31 | * 审核列表 32 | * @param {*} params 33 | * @returns 34 | */ 35 | export function pageApi(params) { 36 | return service({ 37 | url: '/apply/page', 38 | method: 'get', 39 | params 40 | }) 41 | } 42 | 43 | /** 44 | * 待审核数量 45 | * @returns 46 | */ 47 | export function auditCountApi() { 48 | return service({ 49 | url: '/apply/audit/count', 50 | method: 'get' 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/api/auth/index.js: -------------------------------------------------------------------------------- 1 | import service from '../' 2 | 3 | /** 4 | * 注册验证码 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function registerCaptchaApi(params) { 9 | return service({ 10 | url: '/auth/captcha/register', 11 | method: 'get', 12 | params 13 | }) 14 | } 15 | 16 | /** 17 | * 注册 18 | * @param {*} params 19 | * @returns 20 | */ 21 | export function registerApi(data) { 22 | return service({ 23 | url: '/auth/register', 24 | method: 'post', 25 | data 26 | }) 27 | } 28 | 29 | /** 30 | * 登录验证码 31 | * @param {*} params 32 | * @returns 33 | */ 34 | export function loginCaptchaApi(params) { 35 | return service({ 36 | url: '/auth/captcha/login', 37 | method: 'get', 38 | params 39 | }) 40 | } 41 | 42 | /** 43 | * 登录 44 | * @param {*} params 45 | * @returns 46 | */ 47 | export function loginApi(data) { 48 | return service({ 49 | url: '/auth/login', 50 | method: 'post', 51 | data 52 | }) 53 | } 54 | 55 | /** 56 | * QQ登录 57 | * @param {*} params 58 | * @returns 59 | */ 60 | export function loginQQApi(params) { 61 | return service({ 62 | url: '/auth/login/qq', 63 | method: 'get', 64 | params 65 | }) 66 | } 67 | 68 | 69 | /** 70 | * 获取当前用户信息 71 | * @param {*} params 72 | * @returns 73 | */ 74 | export function userInfoApi() { 75 | return service({ 76 | url: '/auth/user/info', 77 | method: 'get', 78 | }) 79 | } 80 | 81 | /** 82 | * 退出登录 83 | * @param {*} params 84 | * @returns 85 | */ 86 | export function logoutApi(data) { 87 | return service({ 88 | url: '/auth/logoff', 89 | method: 'post', 90 | data 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /src/api/conversation/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 会话列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function listApi() { 9 | return service({ 10 | url: '/conversation/list', 11 | method: 'get', 12 | }) 13 | } 14 | 15 | /** 16 | * 新增会话 17 | * @param {*} params 18 | * @returns 19 | */ 20 | export function createApi(data) { 21 | return service({ 22 | url: '/conversation/create', 23 | method: 'post', 24 | data 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/api/expression/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 表情选择列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function listApi() { 9 | return service({ 10 | url: '/expression/select', 11 | method: 'get', 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/api/file/index.js: -------------------------------------------------------------------------------- 1 | import service from '../' 2 | import { ContentType } from '@enums' 3 | 4 | /** 5 | * 参数处理 6 | * @param {*} params 参数 7 | * @returns 8 | */ 9 | const paramsHandle = (params) => { 10 | const formData = new FormData() 11 | for (const key in params) { 12 | formData.append(key, params[key]) 13 | } 14 | return formData 15 | } 16 | /** 17 | * service统一处理 18 | * @param {*} url api 19 | * @param {*} params 参数 20 | * @returns 21 | */ 22 | const serviceHandle = (url, params) => { 23 | const data = paramsHandle(params) 24 | return service({ 25 | url, 26 | method: 'post', 27 | data, 28 | headers: { 29 | 'Content-Type': ContentType.UPLOAD 30 | } 31 | }) 32 | } 33 | 34 | const uploadAvatar = '/file/upload/avatar' 35 | /** 36 | * 上传头像 37 | * @param {*} params 38 | * @returns 39 | */ 40 | export function uploadAvatarApi(params) { 41 | return serviceHandle(uploadAvatar, params) 42 | } 43 | /** 44 | * 上传头像 45 | */ 46 | export function uploadAvatarUrl() { 47 | return `${ service.defaults.baseURL }${ uploadAvatar }` 48 | } 49 | 50 | const uploadImage = '/file/upload/image' 51 | /** 52 | * 上传图片消息 53 | * @param {*} params 54 | * @returns 55 | */ 56 | export function uploadImageApi(params) { 57 | return serviceHandle(uploadImage, params) 58 | } 59 | /** 60 | * 上传图片消息 61 | */ 62 | export function uploadImageUrl() { 63 | return `${ service.defaults.baseURL }${ uploadImage }` 64 | } 65 | 66 | const uploadFile = '/file/upload/file' 67 | /** 68 | * 上传图片消息 69 | * @param {*} params 70 | * @returns 71 | */ 72 | export function uploadFileApi(params) { 73 | return serviceHandle(uploadFile, params) 74 | } 75 | /** 76 | * 上传图片消息 77 | */ 78 | export function uploadFileUrl() { 79 | return `${ service.defaults.baseURL }${ uploadFile }` 80 | } 81 | 82 | const uploadAudio = '/file/upload/audio' 83 | /** 84 | * 上传音频消息 85 | * @param {*} params 86 | * @returns 87 | */ 88 | export function uploadAudioApi(blob) { 89 | const formData = new FormData() 90 | formData.append('file', blob,'.mp3'); 91 | return service({ 92 | url: uploadAudio, 93 | method: 'post', 94 | data: formData, 95 | headers: { 96 | 'Content-Type': ContentType.UPLOAD 97 | } 98 | }) 99 | } 100 | /** 101 | * 上传音频消息 102 | */ 103 | export function uploadAudioUrl() { 104 | return `${ service.defaults.baseURL }${ uploadAudio }` 105 | } -------------------------------------------------------------------------------- /src/api/friend/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 分组好友列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function listApi() { 9 | return service({ 10 | url: '/grouping/friend', 11 | method: 'get', 12 | }) 13 | } 14 | 15 | /** 16 | * 删除好友 17 | * @param {*} params 18 | * @returns 19 | */ 20 | export function deleteApi(data) { 21 | return service({ 22 | url: '/friend/delete', 23 | method: 'post', 24 | data 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/api/grouping/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 分组好友列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function listApi() { 9 | return service({ 10 | url: '/grouping/friend', 11 | method: 'get', 12 | }) 13 | } 14 | 15 | 16 | /** 17 | * 分组选择列表 18 | * @param {*} params 19 | * @returns 20 | */ 21 | export function selectListApi() { 22 | return service({ 23 | url: '/grouping/select', 24 | method: 'get', 25 | }) 26 | } -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import axios from 'axios' 3 | import qs from 'qs' 4 | import router from '@/router' 5 | import { ElMessage } from 'element-plus' 6 | 7 | import { MAPPING, CONTENT_TYPE, SUCCESS_CODE, TIME_OUT, AUTH_KEY } from '@constants' 8 | import { ContentType } from '@enums' 9 | import { blob2Json } from '@utils' 10 | 11 | /** 12 | * @description: 异常消息提示 13 | * @param {string} string 14 | * @return {*} 15 | * @author: gumingchen 16 | */ 17 | const prompt = (message) => { 18 | ElMessage({ 19 | message: message, 20 | type: 'warning', 21 | duration: 3000 22 | }) 23 | } 24 | 25 | /** 26 | * @description: code处理 27 | * @param {number} code 28 | * @param {string} msg 29 | * @return {*} 30 | * @author: gumingchen 31 | */ 32 | const codeHandle = (code, message) => { 33 | switch (code) { 34 | case 4006: 35 | case 4007: 36 | router.replace({ name: 'login' }) 37 | prompt(message) 38 | useRootStore().clearData() 39 | break 40 | case 401: 41 | router.replace({ 42 | name: '401' 43 | }) 44 | break 45 | case 404: 46 | router.replace({ 47 | name: '404' 48 | }) 49 | break 50 | case 500: 51 | // router.replace({ 52 | // name: '500' 53 | // }) 54 | break 55 | default: 56 | prompt(message, false) 57 | break 58 | } 59 | } 60 | 61 | /** 62 | * @description: axios创建 63 | * @param {*} 64 | * @return {*} 65 | * @author: gumingchen 66 | */ 67 | const service = axios.create({ 68 | baseURL: `${ MAPPING }`, 69 | withCredentials: true, 70 | timeout: TIME_OUT, 71 | headers: { 72 | 'Content-Type': CONTENT_TYPE 73 | } 74 | }) 75 | 76 | /** 77 | * @description: axios请求拦截器 78 | * @param {*} 79 | * @return {*} 80 | * @author: gumingchen 81 | */ 82 | service.interceptors.request.use( 83 | config => { 84 | const { token } = useAuthStore() 85 | // 设置 token 86 | if (token.trim()) { 87 | config.headers[AUTH_KEY] = token.trim() 88 | } 89 | if (config.data) { 90 | if (config.headers['Content-Type'] === ContentType.FORM) { 91 | config.data = qs.stringify(config.data) 92 | } 93 | } 94 | return config 95 | }, 96 | error => { 97 | console.log(error) // for debug 98 | return Promise.reject(error) 99 | } 100 | ) 101 | 102 | /** 103 | * @description: axios响应拦截器 104 | * @param {*} 105 | * @return {*} 106 | * @author: gumingchen 107 | */ 108 | service.interceptors.response.use( 109 | async response => { 110 | if (response.headers['content-type'] === ContentType.STREAM) { 111 | if (!response.data.code) { 112 | return { 113 | blob: response.data, 114 | name: decodeURI(response.headers['content-disposition'].replace('attachment;filename=', '')) 115 | } 116 | } else { 117 | return response.data || null 118 | } 119 | } 120 | const { responseType } = response.config 121 | if (responseType === 'blob') { 122 | response.data = await blob2Json(response.data) 123 | } 124 | const { code, message } = response.data 125 | if (!SUCCESS_CODE.includes(code)) { 126 | codeHandle(code, message) 127 | return null 128 | } 129 | return response.data || null 130 | }, 131 | error => { 132 | if (error && error.response) { 133 | switch (error.response.status) { 134 | case 400: 135 | console.log('错误请求') 136 | break 137 | case 401: 138 | console.log('未授权,请重新登录') 139 | break 140 | case 403: 141 | console.log('拒绝访问') 142 | break 143 | case 404: 144 | console.log('请求错误,未找到该资源') 145 | break 146 | case 405: 147 | console.log('请求方法未允许') 148 | break 149 | case 408: 150 | console.log('请求超时') 151 | break 152 | case 411: 153 | console.log('需要知道长度') 154 | break 155 | case 413: 156 | console.log('请求的实体太大') 157 | break 158 | case 414: 159 | console.log('请求的URL太长') 160 | break 161 | case 415: 162 | console.log('不支持的媒体类型') 163 | break 164 | case 500: 165 | console.log('服务器端出错') 166 | break 167 | case 501: 168 | console.log('网络未实现') 169 | break 170 | case 502: 171 | console.log('网络错误') 172 | break 173 | case 503: 174 | console.log('服务不可用') 175 | break 176 | case 504: 177 | console.log('网络超时') 178 | break 179 | case 505: 180 | console.log('http版本不支持该请求') 181 | break 182 | default: 183 | console.log(`连接错误${ error.response.status }`) 184 | } 185 | } else { 186 | console.log('连接到服务器失败') 187 | // router.replace({ 188 | // name: '500' 189 | // }) 190 | } 191 | return Promise.reject(error) 192 | } 193 | ) 194 | 195 | export default service 196 | -------------------------------------------------------------------------------- /src/api/media/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 语音请求 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function voiceCallApi(data) { 9 | return service({ 10 | url: '/media/voice/call', 11 | method: 'post', 12 | data 13 | }) 14 | } 15 | /** 16 | * 取消语音请求 17 | * @param {*} params 18 | * @returns 19 | */ 20 | export function voiceCancelApi(data) { 21 | return service({ 22 | url: '/media/voice/cancel', 23 | method: 'post', 24 | data 25 | }) 26 | } 27 | /** 28 | * 接受语音请求 29 | * @param {*} params 30 | * @returns 31 | */ 32 | export function voiceAcceptApi(data) { 33 | return service({ 34 | url: '/media/voice/accept', 35 | method: 'post', 36 | data 37 | }) 38 | } 39 | /** 40 | * 拒绝语音请求 41 | * @param {*} params 42 | * @returns 43 | */ 44 | export function voiceRejectApi(data) { 45 | return service({ 46 | url: '/media/voice/reject', 47 | method: 'post', 48 | data 49 | }) 50 | } 51 | /** 52 | * 挂断语音通话 53 | * @param {*} params 54 | * @returns 55 | */ 56 | export function voiceCloseApi(data) { 57 | return service({ 58 | url: '/media/voice/close', 59 | method: 'post', 60 | data 61 | }) 62 | } 63 | 64 | /** 65 | * 视频请求 66 | * @param {*} params 67 | * @returns 68 | */ 69 | export function videoCallApi(data) { 70 | return service({ 71 | url: '/media/video/call', 72 | method: 'post', 73 | data 74 | }) 75 | } 76 | /** 77 | * 取消视频请求 78 | * @param {*} params 79 | * @returns 80 | */ 81 | export function videoCancelApi(data) { 82 | return service({ 83 | url: '/media/video/cancel', 84 | method: 'post', 85 | data 86 | }) 87 | } 88 | /** 89 | * 接受视频请求 90 | * @param {*} params 91 | * @returns 92 | */ 93 | export function videoAcceptApi(data) { 94 | return service({ 95 | url: '/media/video/accept', 96 | method: 'post', 97 | data 98 | }) 99 | } 100 | /** 101 | * 拒绝视频请求 102 | * @param {*} params 103 | * @returns 104 | */ 105 | export function videoRejectApi(data) { 106 | return service({ 107 | url: '/media/video/reject', 108 | method: 'post', 109 | data 110 | }) 111 | } 112 | /** 113 | * 挂断视频通话 114 | * @param {*} params 115 | * @returns 116 | */ 117 | export function videoCloseApi(data) { 118 | return service({ 119 | url: '/media/video/close', 120 | method: 'post', 121 | data 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /src/api/message/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 消息分页列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function pageApi(params) { 9 | return service({ 10 | url: '/message/page/id', 11 | method: 'get', 12 | params 13 | }) 14 | } 15 | 16 | /** 17 | * 发送消息 18 | * @param {*} data 19 | * @returns 20 | */ 21 | export function sendApi(data) { 22 | return service({ 23 | url: '/message/create', 24 | method: 'post', 25 | data 26 | }) 27 | } -------------------------------------------------------------------------------- /src/api/room/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 群房间用户分页列表 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function roomGroupUserPageApi(params) { 9 | return service({ 10 | url: '/roomGroupUser/page', 11 | method: 'get', 12 | params 13 | }) 14 | } 15 | 16 | /** 17 | * 群房间用户数量 18 | * @param {*} params 19 | * @returns 20 | */ 21 | export function roomGroupUserCountApi(params) { 22 | return service({ 23 | url: '/roomGroupUser/count', 24 | method: 'get', 25 | params 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/api/user/index.js: -------------------------------------------------------------------------------- 1 | import service from '..' 2 | 3 | /** 4 | * 更新基本信息 5 | * @param {*} params 6 | * @returns 7 | */ 8 | export function updateApi(data) { 9 | return service({ 10 | url: '/user/update', 11 | method: 'post', 12 | data 13 | }) 14 | } 15 | 16 | /** 17 | * 更新邮箱验证码 18 | * @param {*} params 19 | * @returns 20 | */ 21 | export function updateEmailCaptchaApi(params) { 22 | return service({ 23 | url: '/user/update/email/captcha', 24 | method: 'get', 25 | params 26 | }) 27 | } 28 | 29 | /** 30 | * 更新邮箱 31 | * @param {*} params 32 | * @returns 33 | */ 34 | export function updateEmailApi(data) { 35 | return service({ 36 | url: '/user/update/email', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | /** 43 | * 获取用户列表 44 | * @param {*} params 45 | * @returns 46 | */ 47 | export function getUserListApi(params) { 48 | return service({ 49 | url: '/user/search', 50 | method: 'get', 51 | params 52 | }) 53 | } 54 | 55 | /** 56 | * 获取用户信息 57 | * @param {*} params 58 | * @returns 59 | */ 60 | export function getUserInfoApi(params) { 61 | return service({ 62 | url: '/user/info', 63 | method: 'get', 64 | params 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/fonts/AppleChancery.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/fonts/AppleChancery.ttf -------------------------------------------------------------------------------- /src/assets/fonts/BarbaraHand.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/fonts/BarbaraHand.ttf -------------------------------------------------------------------------------- /src/assets/fonts/JoinedUp.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/fonts/JoinedUp.ttf -------------------------------------------------------------------------------- /src/assets/images/gitee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/gitee.png -------------------------------------------------------------------------------- /src/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/github.png -------------------------------------------------------------------------------- /src/assets/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/group.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/official-account-qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/official-account-qr-code.png -------------------------------------------------------------------------------- /src/assets/images/official-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/official-account.png -------------------------------------------------------------------------------- /src/assets/images/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/qq.png -------------------------------------------------------------------------------- /src/assets/images/wechat-qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/wechat-qr-code.png -------------------------------------------------------------------------------- /src/assets/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmingchen/chatterbox/fdef8d4bc876acdf7dcf4b294cd4cfb73098329b/src/assets/images/wechat.png -------------------------------------------------------------------------------- /src/assets/sass/_animation.scss: -------------------------------------------------------------------------------- 1 | 2 | // 旋转 3 | @keyframes rotate { 4 | from { 5 | transform: rotate(0deg) 6 | } 7 | to { 8 | transform: rotate(360deg) 9 | } 10 | } 11 | 12 | // 点 13 | @keyframes dots { 14 | 33.33% { 15 | content: "."; 16 | } 17 | 66.67% { 18 | content: ".."; 19 | } 20 | 100% { 21 | content: "..."; 22 | } 23 | } -------------------------------------------------------------------------------- /src/assets/sass/_element.scss: -------------------------------------------------------------------------------- 1 | @forward "element-plus/theme-chalk/src/common/var.scss" with ( 2 | $colors: ( 3 | 'white': #fff, 4 | 'black': #000, 5 | 'primary': ( 6 | 'base': #409eff, 7 | ), 8 | 'success': ( 9 | 'base': #67c23a, 10 | ), 11 | 'warning': ( 12 | 'base': #e6a23c, 13 | ), 14 | 'danger': ( 15 | 'base': #f56c6c, 16 | ), 17 | 'error': ( 18 | 'base': #f56c6c, 19 | ), 20 | 'info': ( 21 | 'base': #909399, 22 | ), 23 | ), 24 | $text-color: ( 25 | ( 26 | 'primary': #303133, 27 | 'regular': #606266, 28 | 'secondary': #909399, 29 | 'placeholder': #a8abb2, 30 | 'disabled': #c0c4cc, 31 | ) 32 | ), 33 | ); 34 | 35 | @import "element-plus/theme-chalk/src/index.scss"; 36 | @import 'element-plus/theme-chalk/dark/css-vars.css'; 37 | 38 | .el-dialog { 39 | border-radius: var(--box-border-radius) !important; 40 | background-color: var(--wrap-background-color) !important; 41 | } 42 | .el-icon { 43 | font-style: normal; 44 | } 45 | -------------------------------------------------------------------------------- /src/assets/sass/_font.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'BarbaraHand'; 3 | src: url("../fonts/BarbaraHand.ttf") format('truetype'); 4 | } 5 | @font-face { 6 | font-family: 'AppleChancery'; 7 | src: url("../fonts/AppleChancery.ttf") format('truetype'); 8 | } 9 | @font-face { 10 | font-family: 'JoinedUp'; 11 | src: url("../fonts/JoinedUp.ttf") format('truetype'); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/sass/_global.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * todo: n->none 没有 3 | * todo: t->top 上 4 | * todo: r->right 右 5 | * todo: b->bottom 下 6 | * todo: l->left 左 7 | * todo: a->auto 自动 8 | */ 9 | 10 | * { 11 | // 怪异盒子模型 12 | box-sizing: border-box; 13 | // 允许在单词内换行 14 | word-break: break-all; 15 | } 16 | 17 | html, body { 18 | height: 100%; 19 | } 20 | 21 | // 通用ul li 22 | ul { 23 | margin: 0; 24 | padding: 0; 25 | li { 26 | list-style-type: none; 27 | } 28 | } 29 | 30 | // todo: 后期若没有那么多可以改为 @each 31 | // 高度 宽度 32 | .height-full { 33 | height: 100% !important; 34 | } 35 | .min-height-full { 36 | min-height: 100% !important; 37 | } 38 | .height-unset { 39 | height: unset !important; 40 | } 41 | .width-full { 42 | width: 100% !important; 43 | } 44 | @for $i from 0 through 1000 { 45 | .height-#{$i} { 46 | height: #{$i}px; 47 | } 48 | .min-height-#{$i} { 49 | min-height: #{$i}px; 50 | } 51 | .max-height-#{$i} { 52 | max-height: #{$i}px; 53 | } 54 | .width-#{$i} { 55 | width: #{$i}px; 56 | } 57 | .min-width-#{$i} { 58 | min-width: #{$i}px; 59 | } 60 | .max-width-#{$i} { 61 | max-width: #{$i}px; 62 | } 63 | } 64 | // 字体大小 65 | @for $i from 12 through 60 { 66 | .font-size-#{$i} { 67 | font-size: #{$i}px; 68 | } 69 | } 70 | // 居中方式 71 | @each $var in left, center, right { 72 | .text-align-#{$var} { 73 | text-align: #{$var}; 74 | } 75 | } 76 | // 外边框 77 | @for $i from 0 through 200 { 78 | .margin-#{$i} { 79 | margin: #{$i}px; 80 | } 81 | .margin-#{$i}-a { 82 | margin: #{$i}px auto; 83 | } 84 | .margin-#{$i}-n { 85 | margin: #{$i}px 0px; 86 | } 87 | .margin-n-#{$i} { 88 | margin: 0px #{$i}px; 89 | } 90 | .margin_t-#{$i} { 91 | margin-top: #{$i}px; 92 | } 93 | .margin_r-#{$i} { 94 | margin-right: #{$i}px; 95 | } 96 | .margin_b-#{$i} { 97 | margin-bottom: #{$i}px; 98 | } 99 | .margin_l-#{$i} { 100 | margin-left: #{$i}px; 101 | } 102 | } 103 | // 内边框 104 | @for $i from 1 through 200 { 105 | .padding-#{$i} { 106 | padding: #{$i}px; 107 | } 108 | .padding-#{$i}-n { 109 | padding: #{$i}px 0px; 110 | } 111 | .padding-n-#{$i} { 112 | padding: 0px #{$i}px; 113 | } 114 | .padding_t-#{$i} { 115 | padding-top: #{$i}px; 116 | } 117 | .padding_r-#{$i} { 118 | padding-right: #{$i}px; 119 | } 120 | .padding_b-#{$i} { 121 | padding-bottom: #{$i}px; 122 | } 123 | .padding_l-#{$i} { 124 | padding-left: #{$i}px; 125 | } 126 | } 127 | // 文字省略 128 | .ellipse { 129 | display: -webkit-box; 130 | overflow: hidden; 131 | text-overflow: ellipsis; 132 | word-break: break-all; 133 | -webkit-box-orient: vertical; 134 | -webkit-line-clamp: 1; 135 | } 136 | @for $i from 1 through 5 { 137 | .ellipse-#{$i} { 138 | display: -webkit-box; 139 | overflow: hidden; 140 | text-overflow: ellipsis; 141 | word-break: break-all; 142 | -webkit-box-orient: vertical; 143 | -webkit-line-clamp: #{$i}; 144 | } 145 | } 146 | // 鼠标显示 手指 147 | .cursor-pointer { 148 | cursor: pointer; 149 | }; 150 | // overflow 151 | $overflow: visible, hidden, scroll, auto, inherit; 152 | @each $var in $overflow { 153 | .overflow-#{$var} { 154 | overflow: #{$var}; 155 | } 156 | } 157 | 158 | // 居中 159 | .position-center { 160 | position: absolute; 161 | top: 50%; 162 | left: 50%; 163 | transform: translate(-50%, -50%); 164 | } 165 | 166 | // todo: flex 布局 167 | $directions: row, row-reverse, column, column-reverse; 168 | $wraps: nowrap, wrap, wrap-reverse; 169 | $justifyContents: flex-start, flex-end, center, space-between, space-around; 170 | $alignItems: flex-start, flex-end, center, baseline, stretch; 171 | $alignContents: flex-start, flex-end, center, space-between, space-around, stretch; 172 | 173 | $alignSelfs: auto, flex-start, flex-end, center, baseline, stretch; 174 | 175 | .ff { 176 | display: flex; 177 | } 178 | 179 | // todo: 容器属性 180 | .flex { 181 | display: flex; 182 | display: -webkit-flex; 183 | display: -moz-box; 184 | display: -ms-flexbox; 185 | // flex-direction 186 | &_d { 187 | @each $var in $directions { 188 | &-#{$var} { 189 | @extend .flex; 190 | flex-direction: #{$var}; 191 | } 192 | } 193 | } 194 | // flex-wrap 195 | &_w { 196 | @each $var in $wraps { 197 | &-#{$var} { 198 | @extend .flex; 199 | flex-wrap: #{$var}; 200 | } 201 | } 202 | } 203 | // justify-content 204 | &_j_c { 205 | @each $var in $justifyContents { 206 | &-#{$var} { 207 | @extend .flex; 208 | justify-content: #{$var}; 209 | } 210 | &_a_i-#{$var} { 211 | @extend .flex; 212 | justify-content: #{$var}; 213 | align-items: #{$var}; 214 | } 215 | } 216 | } 217 | // align-items 218 | &_a_i { 219 | @each $var in $alignItems { 220 | &-#{$var} { 221 | @extend .flex; 222 | align-items: #{$var}; 223 | } 224 | } 225 | } 226 | // align-content 227 | &_a_c { 228 | @each $var in $alignContents { 229 | &-#{$var} { 230 | @extend .flex; 231 | align-content: #{$var}; 232 | } 233 | } 234 | } 235 | // todo: 子元素属性 236 | &-item { 237 | // order 238 | @for $i from -20 to 20 { 239 | &_o-#{$i} { 240 | order: #{$i}; 241 | } 242 | } 243 | // flex: flex-grow flex-shrink flex-basis 244 | &_f-a { 245 | flex: auto; 246 | } 247 | &_f-n { 248 | flex: none; 249 | } 250 | @for $i from 0 to 10 { 251 | &_f-#{$i} { 252 | flex: #{$i}; 253 | } 254 | // @for $j from 1 to 10 { 255 | // @for $k from 1 to 10 { 256 | // &_f-#{$i}-#{$j}-#{$k} { 257 | // flex: #{&i} #{&j} #{&k}; 258 | // } 259 | // } 260 | // } 261 | } 262 | // align-self 263 | @each $var in $alignSelfs { 264 | &_a_s-#{$var} { 265 | align-self: #{$var}; 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/assets/sass/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! https://github.com/necolas/normalize.css/blob/master/normalize.css 参考地址*/ 2 | 3 | /* Document========================================================================== */ 4 | /** 5 | * 1. 在所有浏览器中更正行高. 6 | * 2. 在iOS中更改方向后,防止调整字体大小. 7 | */ 8 | html { 9 | line-height: 1.15; /* 1 */ 10 | -webkit-text-size-adjust: 100%; /* 2 */ 11 | } 12 | 13 | 14 | /* Sections========================================================================== */ 15 | /** 16 | * 删除所有浏览器中的边距 17 | */ 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /** 23 | * 在IE中一致地渲染`main`元素 24 | */ 25 | main { 26 | display: block; 27 | } 28 | 29 | /** 30 | * 更正`section`和``中`h1`元素的字体大小和边距 31 | * Chrome文章,Firefox和Safari中的文章背景 32 | */ 33 | h1 { 34 | font-size: 2em; 35 | margin: 0.67em 0; 36 | } 37 | 38 | /* Grouping content========================================================================== */ 39 | /** 40 | * 1. 在Firefox中添加正确的大小调整大小 41 | * 2. 在Edge和IE中显示溢出 42 | */ 43 | hr { 44 | box-sizing: content-box; /* 1 */ 45 | height: 0; /* 1 */ 46 | overflow: visible; /* 2 */ 47 | } 48 | 49 | /** 50 | * 1. 纠正所有浏览器中字体大小的继承和缩放 51 | * 2. 纠正所有浏览器中奇怪的'em`字体大小 52 | */ 53 | pre { 54 | font-family: monospace, monospace; /* 1 */ 55 | font-size: 1em; /* 2 */ 56 | } 57 | 58 | 59 | /* Text-level semantics========================================================================== */ 60 | /** 61 | * 删除IE 10中活动链接的灰色背景 62 | */ 63 | a { 64 | background-color: transparent; 65 | } 66 | 67 | /** 68 | * 1. 删除Chrome 57中的底部边框 69 | * 2. 在Chrome,Edge,IE,Opera和Safari中添加正确的文本装饰 70 | */ 71 | abbr[title] { 72 | border-bottom: none; /* 1 */ 73 | text-decoration: underline; /* 2 */ 74 | text-decoration: underline dotted; /* 2 */ 75 | } 76 | 77 | /** 78 | * 在Chrome,Edge和Safari中添加正确的字体粗细 79 | */ 80 | b, 81 | strong { 82 | font-weight: bolder; 83 | } 84 | 85 | /** 86 | * 1. 纠正所有浏览器中字体大小的继承和缩放 87 | * 2. 纠正所有浏览器中奇怪的'em`字体大小 88 | */ 89 | code, kbd, samp { 90 | font-family: monospace, monospace; /* 1 */ 91 | font-size: 1em; /* 2 */ 92 | } 93 | 94 | /** 95 | * 在所有浏览器中添加正确的字体大小. 96 | */ 97 | small { 98 | font-size: 80%; 99 | } 100 | 101 | /** 102 | * 防止`sub`和`sup`元素影响所有浏览器行高 103 | */ 104 | sub, sup { 105 | font-size: 75%; 106 | line-height: 0; 107 | position: relative; 108 | vertical-align: baseline; 109 | } 110 | sub { 111 | bottom: -0.25em; 112 | } 113 | sup { 114 | top: -0.5em; 115 | } 116 | 117 | 118 | /* Embedded content========================================================================== */ 119 | /** 120 | * 删除IE 10中链接内图像的边框 121 | */ 122 | img { 123 | border-style: none; 124 | } 125 | 126 | 127 | /* Forms========================================================================== */ 128 | /** 129 | * 1. 更改所有浏览器的字体样式 130 | * 2. 删除Firefox和Safari中的边距 131 | */ 132 | button, input, optgroup, select, textarea { 133 | font-family: inherit; /* 1 */ 134 | font-size: 100%; /* 1 */ 135 | line-height: 1.15; /* 1 */ 136 | margin: 0; /* 2 */ 137 | } 138 | 139 | /** 140 | * 在IE中显示溢出 141 | * 1. 在Edge中显示溢出 142 | */ 143 | button, input { /* 1 */ 144 | overflow: visible; 145 | } 146 | 147 | /** 148 | * 删除Edge,Firefox和IE中文本转换的继承 149 | * 1. 删除Firefox中文本转换的继承 150 | */ 151 | button, select { /* 1 */ 152 | text-transform: none; 153 | } 154 | 155 | /** 156 | * 纠正无法在iOS和Safari中设置可点击类型的样式 157 | */ 158 | button, 159 | [type="button"], 160 | [type="reset"], 161 | [type="submit"] { 162 | -webkit-appearance: button; 163 | } 164 | 165 | /** 166 | * 删除Firefox中的内边框和填充。 167 | */ 168 | button::-moz-focus-inner, 169 | [type="button"]::-moz-focus-inner, 170 | [type="reset"]::-moz-focus-inner, 171 | [type="submit"]::-moz-focus-inner { 172 | border-style: none; 173 | padding: 0; 174 | } 175 | 176 | /** 177 | * 恢复之前规则未设置的焦点样式。 178 | */ 179 | button:-moz-focusring, 180 | [type="button"]:-moz-focusring, 181 | [type="reset"]:-moz-focusring, 182 | [type="submit"]:-moz-focusring { 183 | outline: 1px dotted ButtonText; 184 | } 185 | 186 | /** 187 | * 更正Firefox中的填充 188 | */ 189 | fieldset { 190 | padding: 0.35em 0.75em 0.625em; 191 | } 192 | 193 | /** 194 | * 1. 更正Edge和IE中的文本换行 195 | * 2. 纠正IE中`fieldset`元素的颜色继承 196 | * 3. 删除填充,以便开发人员在零填充时不会被捕获所有浏览器中的`fieldset`元素 197 | */ 198 | legend { 199 | box-sizing: border-box; /* 1 */ 200 | color: inherit; /* 2 */ 201 | display: table; /* 1 */ 202 | max-width: 100%; /* 1 */ 203 | padding: 0; /* 3 */ 204 | white-space: normal; /* 1 */ 205 | } 206 | 207 | /** 208 | * 在Chrome,Firefox和Opera中添加正确的垂直对齐方式 209 | */ 210 | progress { 211 | vertical-align: baseline; 212 | } 213 | 214 | /** 215 | * 删除IE 10+中的默认垂直滚动条 216 | */ 217 | textarea { 218 | overflow: auto; 219 | } 220 | 221 | /** 222 | * 1. 在IE 10中添加正确的大小调整大小 223 | * 2. 删除IE 10中的填充 224 | */ 225 | [type="checkbox"], 226 | [type="radio"] { 227 | box-sizing: border-box; /* 1 */ 228 | padding: 0; /* 2 */ 229 | } 230 | 231 | /** 232 | * 更正Chrome中增量和减量按钮的光标样式 233 | */ 234 | [type="number"]::-webkit-inner-spin-button, 235 | [type="number"]::-webkit-outer-spin-button { 236 | height: auto; 237 | } 238 | 239 | /** 240 | * 1. 纠正Chrome和Safari中的奇怪外观 241 | * 2. 更正Safari中的轮廓样式 242 | */ 243 | [type="search"] { 244 | -webkit-appearance: textfield; /* 1 */ 245 | outline-offset: -2px; /* 2 */ 246 | } 247 | 248 | /** 249 | * 在macOS上删除Chrome和Safari中的内部填充 250 | */ 251 | [type="search"]::-webkit-search-decoration { 252 | -webkit-appearance: none; 253 | } 254 | 255 | /** 256 | * 1. 纠正无法在iOS和Safari中设置可点击类型的样式 257 | * 2. 在Safari中将字体属性更改为`inherit` 258 | */ 259 | ::-webkit-file-upload-button { 260 | -webkit-appearance: button; /* 1 */ 261 | font: inherit; /* 2 */ 262 | } 263 | 264 | 265 | /* Interactive========================================================================== */ 266 | /* 267 | * 在Edge,IE 10+和Firefox中添加正确的显示 268 | */ 269 | details { 270 | display: block; 271 | } 272 | 273 | /* 274 | * 在所有浏览器中添加正确的显示 275 | */ 276 | summary { 277 | display: list-item; 278 | } 279 | 280 | 281 | /* Misc========================================================================== */ 282 | /** 283 | * 在IE 10+中添加正确的显示 284 | */ 285 | template { 286 | display: none; 287 | } 288 | 289 | /** 290 | * 在IE 10中添加正确的显示 291 | */ 292 | [hidden] { 293 | display: none; 294 | } 295 | -------------------------------------------------------------------------------- /src/assets/sass/_nprogress.scss: -------------------------------------------------------------------------------- 1 | @import 'nprogress/nprogress.css'; 2 | 3 | #nprogress { 4 | .bar { 5 | background-color: var(--el-color-primary); 6 | } 7 | .spinner-icon { 8 | border-top-color: var(--el-color-primary); 9 | border-left-color: var(--el-color-primary); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/sass/_root.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --box-border-radius: 10px; 3 | --wrap-background-color: #272a37; 4 | --tabbar-background-color: #323644; 5 | --tabbar-hover-color: #484d5f; 6 | --card-background-color: var(--tabbar-background-color); 7 | --card-hover-background-color: #484d5f; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/sass/_transition.scss: -------------------------------------------------------------------------------- 1 | // 浅入浅出 2 | .shallow-in-out-enter-active, 3 | .shallow-in-out-leave-active { 4 | transition: opacity 0.3s; 5 | } 6 | .shallow-in-out-enter, 7 | .shallow-in-out-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | // 左浅入右浅出 12 | .left-in-right-out-enter-active, 13 | .left-in-right-out-leave-active { 14 | transition: all 0.3s; 15 | } 16 | .left-in-right-out-enter-active { 17 | opacity: 0; 18 | transform: translateX(-20px); 19 | } 20 | .left-in-right-out-enter-to { 21 | opacity: 1; 22 | transform: translateX(0px); 23 | } 24 | .left-in-right-out-leave-to { 25 | opacity: 0; 26 | transform: translateX(20px); 27 | } 28 | 29 | // 右浅入浅出 30 | .right-in-out-move, 31 | .right-in-out-enter-active, 32 | .right-in-out-leave-active { 33 | transition: all 0.3s; 34 | } 35 | .right-in-out-enter-active { 36 | transition-delay: 0.3s; 37 | opacity: 0; 38 | transform: translateX(50px); 39 | } 40 | .right-in-out-enter-to { 41 | opacity: 1; 42 | transform: translateX(0px); 43 | } 44 | .right-in-out-leave-to { 45 | opacity: 0; 46 | transform: translateX(50px); 47 | } 48 | 49 | 50 | .move-move, 51 | .move-enter-active, 52 | .move-leave-active { 53 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 54 | } 55 | .move-leave-active { 56 | position: absolute; 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/sass/_variable.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/sass/index.scss: -------------------------------------------------------------------------------- 1 | @import "normalize"; /* 页面标准化文件导入 */ 2 | @import "element"; /* element */ 3 | @import "font"; /* 字体 */ 4 | @import "transition"; 5 | @import "animation"; 6 | @import "global"; 7 | @import "nprogress"; 8 | @import "root"; 9 | -------------------------------------------------------------------------------- /src/common/constants/file.js: -------------------------------------------------------------------------------- 1 | // 接受的图片类型 2 | export const IMAGE_ACCEPT = ['IMAGE/JPG', 'IMAGE/PNG', 'IMAGE/GIF', 'IMAGE/JPEG'] 3 | -------------------------------------------------------------------------------- /src/common/constants/index.js: -------------------------------------------------------------------------------- 1 | import { ContentType, AuthKey, StorageType, SuccessCode, RequestMapping, WebsocketMapping, ModelBinding } from '@enums' 2 | 3 | // request Mapping 4 | export const MAPPING = RequestMapping.CHATTERBOX 5 | // websocket Mapping 6 | export const WEBSOCKET_MAPPING = RequestMapping.CHATTERBOX + WebsocketMapping.WEBSOCKET 7 | // 请求数据类型 8 | export const CONTENT_TYPE = ContentType.JSON 9 | // 请求超时时长 10 | export const TIME_OUT = 50000 11 | // 访问秘钥 存储 12 | export const AUTH_KEY = AuthKey.TOKEN 13 | // 秘钥本地存储类型 14 | export const AUTH_STORAGE = StorageType.COOKIE 15 | // 请求成功响应code 16 | export const SUCCESS_CODE = [SuccessCode.ZERO, SuccessCode.TWO_HUNDRED] 17 | // 双向绑定方法名 18 | export const MODEL_NAME = 'modelValue' 19 | export const UPDATE_MODEL_EVENT = ModelBinding.MODEL_VALUE 20 | -------------------------------------------------------------------------------- /src/common/enums/apply.js: -------------------------------------------------------------------------------- 1 | export const APPLY_STATUS = { 2 | AUDIT: 0, 3 | PASS: 1, 4 | REJECT: 2, 5 | } 6 | export const applyStatusList = [ 7 | { label: '待审核', value: APPLY_STATUS.AUDIT }, 8 | { label: '已通过', value: APPLY_STATUS.PASS }, 9 | { label: '已拒绝', value: APPLY_STATUS.REJECT }, 10 | ] 11 | 12 | export const APPLY_TYPE = { 13 | FRIEND: 0, // 申请加好友 14 | GROUP: 1, // 申请加群 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/common/enums/index.js: -------------------------------------------------------------------------------- 1 | // 请求头-内容类型 2 | export const ContentType = { 3 | JSON: 'application/json;charset=UTF-8', 4 | FORM: 'application/x-www-form-urlencoded;charset=UTF-8', 5 | UPLOAD: 'multipart/form-data', 6 | STREAM: 'application/octet-stream' 7 | } 8 | // 令牌键值 9 | export const AuthKey = { 10 | TOKEN: 'token', 11 | ACCESS: 'access' 12 | } 13 | // 本地存储类型 14 | export const StorageType = { 15 | COOKIE: 'cookie', 16 | SESSION: 'sessionStorage', 17 | LOCAL: 'localStorage' 18 | } 19 | // 请求成功状态码 20 | export const SuccessCode = { 21 | ZERO: 0, 22 | TWO_HUNDRED: 200 23 | } 24 | // 请求 mapping 25 | export const RequestMapping = { 26 | CHATTERBOX: '/chatterbox', 27 | SLIPPER: '/slipper', 28 | API: '/api' 29 | } 30 | // websocket mapping 31 | export const WebsocketMapping = { 32 | WEBSOCKET: '/websocket', 33 | } 34 | // 双向绑定名 35 | export const ModelBinding = { 36 | MODEL_VALUE: 'update:modelValue', 37 | MODEL_EVENT: 'update:modelEvent' 38 | } 39 | -------------------------------------------------------------------------------- /src/common/enums/media.js: -------------------------------------------------------------------------------- 1 | export const MEDIA_TYPE = { 2 | VOICE: 1, // 语音 3 | VIDEO: 2, // 视频 4 | } 5 | 6 | export const MEDIA_STATUS = { 7 | INVITING: 1, // 邀请中 8 | REJECTED: 2, // 被拒绝 9 | 10 | CALLING: 3, // 被呼叫中 11 | CANCELED: 4, // 被取消 12 | 13 | UNANSWERED: 5, // 未接听 14 | 15 | ING: 6, // 进行中 16 | 17 | HANGUP: 7, // 挂断 18 | CLOSED: 8, // 中断 19 | } 20 | -------------------------------------------------------------------------------- /src/common/enums/message.js: -------------------------------------------------------------------------------- 1 | export const MESSAGE_TYPE = { 2 | TEXT: 0, 3 | IMAGE: 1, 4 | AUDIO: 2, 5 | FILE: 3, 6 | } 7 | 8 | export const messageTypeList = [ 9 | { label: '文本', value: MESSAGE_TYPE.TEXT }, 10 | { label: '图片', value: MESSAGE_TYPE.IMAGE }, 11 | { label: '语音', value: MESSAGE_TYPE.AUDIO }, 12 | { label: '文件', value: MESSAGE_TYPE.FILE }, 13 | ] 14 | 15 | export const MESSAGE_SEND_STATUS = { 16 | PENDING: 1, // 发送中 17 | SUCCESS: 2, // 发送成功 18 | FAIL: 3, // 发送失败 19 | } 20 | -------------------------------------------------------------------------------- /src/common/enums/user.js: -------------------------------------------------------------------------------- 1 | export const SEX = { 2 | FEMALE: 0, 3 | MALE: 1, 4 | UNKNOWN: 2, 5 | } 6 | export const sexList = [ 7 | { label: '男', value: SEX.MALE }, 8 | { label: '女', value: SEX.FEMALE }, 9 | { label: '保密', value: SEX.UNKNOWN }, 10 | ] 11 | 12 | export const EDIT_TYPE = { 13 | INFO: 'info', 14 | EMAIL: 'email', 15 | } 16 | 17 | export const ONLINE_STATUS = { 18 | OFFLINE: 0, 19 | ONLINE: 1, 20 | } 21 | -------------------------------------------------------------------------------- /src/common/enums/websocket.js: -------------------------------------------------------------------------------- 1 | export const WEBSOCKET_TYPE = { 2 | HEARTBEAT: 0, // 心跳 3 | PRIVATE_CHAT_MESSAGE: 1, // 私聊消息 4 | GROUP_CHAT_MESSAGE: 2, // 群聊消息 5 | 6 | FRIEND_APPLY: 3, // 好友申请 7 | PASS_FRIEND_APPLY: 4, // 通过好友申请 8 | REJECT_FRIEND_APPLY: 5, // 拒绝好友申请 9 | DELETE_FRIEND: 6, // 删除好友 10 | 11 | JOIN_GROUP: 11, // 加入群聊 12 | EXIT_GROUP: 12, // 退出群聊 13 | 14 | VOICE_APPLY: 13, // 语音请求 15 | VOICE_CANCEL: 14, // 取消语音请求 16 | VOICE_ACCEPT: 15, // 接听语音 17 | VOICE_REJECT: 16, // 拒绝语音 18 | VOICE_CLOSE: 17, // 关闭语音 19 | 20 | VIDEO_APPLY: 18, // 视频请求 21 | VIDEO_CANCEL: 19, // 取消视频请求 22 | VIDEO_ACCEPT: 20, // 接听视频 23 | VIDEO_REJECT: 21, // 拒绝视频 24 | VIDEO_CLOSE: 22, // 关闭视频 25 | 26 | USER_ONLINE: 23, // 用户上线 27 | USER_OFFLINE: 24, // 用户下线 28 | } 29 | -------------------------------------------------------------------------------- /src/common/props/index.js: -------------------------------------------------------------------------------- 1 | export const form = { 2 | type: Object, 3 | default: () => ({}) 4 | } 5 | 6 | export const loading = { 7 | type: Boolean, 8 | default: () => false 9 | } 10 | -------------------------------------------------------------------------------- /src/common/rules/user.js: -------------------------------------------------------------------------------- 1 | import { isEmail } from '@utils/regular.js' 2 | 3 | /* 昵称 */ 4 | export const nickname = [ 5 | { required: true, message: '请输入昵称', trigger: 'blur' } 6 | ] 7 | /* 性别 */ 8 | export const sex = [ 9 | { required: true, message: '请选择性别', trigger: 'change' } 10 | ] 11 | /* 邮箱 */ 12 | const checkEmail = (_rule, value, callback) => { 13 | if (!isEmail(value)) { 14 | callback(new Error('请输入正确的邮箱地址')) 15 | } 16 | callback() 17 | } 18 | export const email = [ 19 | { required: true, message: '请输入邮箱地址', trigger: 'blur' }, 20 | { validator: checkEmail, trigger: 'blur' } 21 | ] 22 | /* 验证码 */ 23 | export const captcha = [ 24 | { required: true, message: '请输入验证码', trigger: 'blur' } 25 | ] 26 | -------------------------------------------------------------------------------- /src/common/utils/regular.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 邮箱 3 | * @param {*} input 4 | * @return {*} 5 | * @author: gumingchen 6 | */ 7 | export function isEmail(input) { 8 | const reg = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/ 9 | return reg.test(input) 10 | } 11 | -------------------------------------------------------------------------------- /src/common/utils/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 本地存储 3 | * @Author: gumingchen 4 | * @Email: 1240235512@qq.com 5 | * @Date: 2020-12-28 16:25:18 6 | * @LastEditors: gumingchen 7 | * @LastEditTime: 2021-04-30 14:01:54 8 | */ 9 | import cookie from 'js-cookie' 10 | import { AUTH_KEY, AUTH_STORAGE } from '@constants' 11 | import { StorageType } from '@enums' 12 | 13 | /** 14 | * @description: 本地存储、获取、清除 15 | * @param {String} key 存储键值 16 | * @param {String} value 存储值 17 | * @param {String} storage 存储位置 18 | * @return {*} 19 | * @author: gumingchen 20 | */ 21 | export function set(key, value = '', storage) { 22 | switch (storage) { 23 | case StorageType.COOKIE: 24 | cookie.set(key, value) 25 | break 26 | case StorageType.SESSION: 27 | sessionStorage.setItem(key, value) 28 | break 29 | case StorageType.LOCAL: 30 | localStorage.setItem(key, value) 31 | break 32 | default: 33 | cookie.set(key, value) 34 | break 35 | } 36 | } 37 | export function get(key, storage) { 38 | let result 39 | switch (storage) { 40 | case StorageType.COOKIE: 41 | result = cookie.get(key) 42 | break 43 | case StorageType.SESSION: 44 | result = sessionStorage.getItem(key) 45 | break 46 | case StorageType.LOCAL: 47 | result = localStorage.getItem(key) 48 | break 49 | default: 50 | result = cookie.get(key) 51 | break 52 | } 53 | return result 54 | } 55 | export function clear(key, storage) { 56 | switch (storage) { 57 | case StorageType.COOKIE: 58 | cookie.remove(key) 59 | break 60 | case StorageType.SESSION: 61 | sessionStorage.removeItem(key) 62 | break 63 | case StorageType.LOCAL: 64 | localStorage.removeItem(key) 65 | break 66 | default: 67 | cookie.remove(key) 68 | break 69 | } 70 | } 71 | 72 | /** 73 | * @description: token-存储、获取、清除 74 | * @param {*} 75 | * @return {*} 76 | * @author: gumingchen 77 | */ 78 | export function getAuth() { 79 | return JSON.parse(get(AUTH_KEY, AUTH_STORAGE) || '{}') 80 | } 81 | export function setAuth(auth) { 82 | set(AUTH_KEY, JSON.stringify(auth), AUTH_STORAGE) 83 | } 84 | export function clearAuth() { 85 | clear(AUTH_KEY, AUTH_STORAGE) 86 | } 87 | -------------------------------------------------------------------------------- /src/common/utils/websocket.js: -------------------------------------------------------------------------------- 1 | import { WEBSOCKET_TYPE } from '@enums/websocket' 2 | 3 | export default class WebsocketClass { 4 | /** 5 | * @description: 初始化参数 6 | * @param {*} url ws资源路径 7 | * @param {*} callback 服务端信息回调 8 | * @return {*} 9 | * @author: gumingchen 10 | */ 11 | constructor(url, callback) { 12 | this.url = url 13 | this.callback = callback 14 | this.ws = null // websocket 对象 15 | this.status = 0 // 连接状态: 0-关闭 1-连接 2-手动关闭 16 | this.ping = 10000 // 心跳时长 17 | this.pingInterval = null // 心跳定时器 18 | this.reconnect = 5000 // 重连间隔 19 | } 20 | 21 | /** 22 | * @description: 连接 23 | * @param {*} 24 | * @return {*} 25 | * @author: gumingchen 26 | */ 27 | connect() { 28 | this.ws = new WebSocket(this.url) 29 | // 监听socket连接 30 | this.ws.onopen = () => { 31 | this.status = 1 32 | this.heartHandler() 33 | } 34 | // 监听socket消息 35 | this.ws.onmessage = (e) => { 36 | this.callback(JSON.parse(e.data)) 37 | } 38 | // 监听socket错误信息 39 | this.ws.onerror = (e) => { 40 | console.log(e) 41 | } 42 | // 监听socket关闭 43 | this.ws.onclose = (e) => { 44 | this.onClose(e) 45 | } 46 | } 47 | 48 | /** 49 | * @description: 发送消息 50 | * @param {*} data 51 | * @return {*} 52 | * @author: gumingchen 53 | */ 54 | send(data) { 55 | return this.ws.send(JSON.stringify(data)) 56 | } 57 | 58 | /** 59 | * @description: 关闭weibsocket 主动关闭不会触发重连 60 | * @param {*} 61 | * @return {*} 62 | * @author: gumingchen 63 | */ 64 | close() { 65 | this.status = 2 66 | this.ws.close() 67 | } 68 | 69 | /** 70 | * @description: socket关闭事件 71 | * @param {*} 72 | * @return {*} 73 | * @author: gumingchen 74 | */ 75 | onClose(e) { 76 | console.error(e) 77 | this.status = this.status === 2 ? this.status : 0 78 | setTimeout(() => { 79 | if (this.status === 0) { 80 | this.connect() 81 | } 82 | }, this.reconnect) 83 | } 84 | 85 | /** 86 | * @description: 心跳机制 87 | * @param {*} 88 | * @return {*} 89 | * @author: gumingchen 90 | */ 91 | heartHandler() { 92 | const data = { 93 | type: WEBSOCKET_TYPE.HEARTBEAT 94 | } 95 | this.pingInterval = setInterval(() => { 96 | if (this.status === 1) { 97 | this.ws.send(JSON.stringify(data)) 98 | } else { 99 | clearInterval(this.pingInterval) 100 | } 101 | }, this.ping) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/apply-friend-dialog/components/form-ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | 3 | export const props = { 4 | form, 5 | loading, 6 | groupings: { type: Array, default: () => [] } 7 | } 8 | 9 | export const rules = { 10 | groupingId: [{ required: true, message: '请选择分组', trigger: 'change' }], 11 | content: [{ required: true, message: '请输入申请内容', trigger: 'blur' }], 12 | } 13 | -------------------------------------------------------------------------------- /src/components/apply-friend-dialog/components/form-ui/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 50 | 51 | -------------------------------------------------------------------------------- /src/components/apply-friend-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 94 | 95 | 98 | ./components/form-ui -------------------------------------------------------------------------------- /src/components/avatar-upload/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | 51 | 75 | -------------------------------------------------------------------------------- /src/components/avatar/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 36 | -------------------------------------------------------------------------------- /src/components/brand/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 38 | -------------------------------------------------------------------------------- /src/components/captcha-input/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /src/components/card/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | 51 | 67 | -------------------------------------------------------------------------------- /src/components/context-menu/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 81 | 82 | 100 | -------------------------------------------------------------------------------- /src/components/countdown-button/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 48 | 49 | 62 | -------------------------------------------------------------------------------- /src/components/countdown/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /src/components/empty/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /src/components/filing/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /src/components/loading/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /src/components/message-send-status/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /src/components/online-dot/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 32 | -------------------------------------------------------------------------------- /src/components/panel/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/components/router/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /src/components/search-dialog/components/group-list-panel/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /src/components/search-dialog/components/user-card/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /src/components/search-dialog/components/user-list-panel/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 89 | 90 | 99 | -------------------------------------------------------------------------------- /src/components/search-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | 42 | -------------------------------------------------------------------------------- /src/components/timer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /src/components/upload/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /src/components/user-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 37 | -------------------------------------------------------------------------------- /src/components/user-panel/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | 30 | 32 | -------------------------------------------------------------------------------- /src/directive/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install: function (app) { 3 | const directives = import.meta.glob('./**/index.js', { eager: true }) 4 | for (const key in directives) { 5 | if (key === './index.js') return 6 | const directive = directives[key] 7 | const name = key.replace(/\.\/|\/index.js/g, '') 8 | app.directive(name, directive.default || directive) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/directive/longpress/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 长按 3 | * @param {*} 4 | * @return {*} 5 | * @author: gumingchen 6 | */ 7 | export default { 8 | beforeMount(el, binding) { 9 | const callback = binding.value; 10 | if (typeof callback !== 'function') { 11 | return console.warn('[Directive warn]: Invalid value: validation failed for value. Must be a function.') 12 | } 13 | el.$duration = binding.arg || 2000; // 获取长按时长, 默认3秒执行长按事件 14 | let timer = null; 15 | const add = (event) => { 16 | const { type, button } = event 17 | if (type === 'click' && button !== 0) return; 18 | event.preventDefault(); 19 | if (timer === null) { 20 | timer = setTimeout(() => { 21 | callback(); 22 | timer = null; 23 | }, el.$duration) 24 | } 25 | } 26 | const cancel = () => { 27 | if (timer !== null) { 28 | clearTimeout(timer); 29 | timer = null; 30 | } 31 | } 32 | // 添加计时器 33 | el.addEventListener('mousedown', add); 34 | el.addEventListener('touchstart', add); 35 | // 取消计时器 36 | el.addEventListener('click', cancel); 37 | el.addEventListener('mouseout', cancel); 38 | el.addEventListener('touchend', cancel) 39 | el.addEventListener('touchcancel', cancel) 40 | }, 41 | updated(el, binding) { 42 | // 可以实时更新时长 43 | el.$duration = binding.arg; 44 | }, 45 | unmounted(el) { 46 | el.removeEventListener('mousedown', () => { }); 47 | el.removeEventListener('touchstart', () => { }); 48 | el.removeEventListener('click', () => { }); 49 | el.removeEventListener('mouseout', () => { }); 50 | el.removeEventListener('touchend', () => { }); 51 | el.removeEventListener('touchcancel', () => { }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/hooks/bind-exposed.js: -------------------------------------------------------------------------------- 1 | export default function bindExposed(ref) { 2 | const instance = getCurrentInstance() 3 | if (ref.value.$.exposed) { 4 | const entries = Object.entries(ref.value.$.exposed) 5 | for (const [key, value] of entries) { 6 | if (instance.exposed) { 7 | instance.exposed[key] = value 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/hooks/debounce-ref.js: -------------------------------------------------------------------------------- 1 | export default function debounceRef(value, delay = 1000) { 2 | let timer 3 | return customRef((track, trigger) => ({ 4 | get() { 5 | track() 6 | return value 7 | }, 8 | set(val) { 9 | clearTimeout(timer) 10 | timer = setTimeout(() => { 11 | value = val 12 | trigger() 13 | }, delay) 14 | } 15 | })) 16 | } -------------------------------------------------------------------------------- /src/hooks/model.js: -------------------------------------------------------------------------------- 1 | import { MODEL_NAME, UPDATE_MODEL_EVENT } from '@constants' 2 | 3 | export default function (props, key) { 4 | const vm = getCurrentInstance().proxy 5 | return computed({ 6 | get() { 7 | return props[key || MODEL_NAME] 8 | }, 9 | set(value) { 10 | const event = key ? `update:${ key }` : UPDATE_MODEL_EVENT 11 | vm.$emit(event, value) 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from './App.vue' 4 | import router from './router' 5 | import pinia from './stores' 6 | 7 | import '@/assets/sass/index.scss' // 全局样式 8 | import Directive from '@/directive' // 自定义指令 9 | 10 | const app = createApp(App) 11 | 12 | app.use(router) 13 | .use(pinia) 14 | .use(Directive) 15 | .mount('#app') 16 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import NProgress from 'nprogress' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { path: '/', redirect: { name: 'login' }, meta: { name: '重定向' } }, 8 | { 9 | path: '/login', 10 | name: 'login', 11 | component: () => import('../views/login/index.vue') 12 | }, 13 | { 14 | path: '/layout', 15 | name: 'layout', 16 | component: () => import('../views/layout/index.vue'), 17 | children: [ 18 | { 19 | path: '/conversation', 20 | name: 'conversation', 21 | component: () => import('../views/conversation/index.vue') 22 | }, 23 | { 24 | path: '/friend', 25 | name: 'friend', 26 | component: () => import('../views/friend/index.vue') 27 | }, 28 | { 29 | path: '/group', 30 | name: 'group', 31 | component: () => import('../views/group/index.vue') 32 | }, 33 | { 34 | path: '/apply', 35 | name: 'apply', 36 | component: () => import('../views/apply/index.vue') 37 | }, 38 | ], 39 | beforeEnter: async (to, from, next) => { 40 | const authStore = useAuthStore() 41 | if (authStore.validateToken()) { 42 | await useUserStore().getUserInfo() 43 | next() 44 | } else { 45 | const rootStore = useRootStore() 46 | rootStore.clearData() 47 | next({ name: 'login', replace: true }) 48 | } 49 | } 50 | } 51 | ] 52 | }) 53 | 54 | router.beforeEach(async (to, _from, next) => { 55 | NProgress.start() 56 | 57 | const authStore = useAuthStore() 58 | if (to.name === 'login' && authStore.validateToken()) { 59 | next({ name: 'conversation', replace: true }) 60 | } else { 61 | next() 62 | } 63 | }) 64 | 65 | router.afterEach(() => { 66 | NProgress.done() 67 | }) 68 | 69 | export default router 70 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const pinia = createPinia() 4 | 5 | export default pinia -------------------------------------------------------------------------------- /src/stores/modules/apply.js: -------------------------------------------------------------------------------- 1 | import { clearJson } from '@utils' 2 | 3 | import { auditCountApi, pageApi } from '@/api/apply' 4 | 5 | export const useApplyStore = defineStore('apply', { 6 | state: () => ({ 7 | auditCount: 0, 8 | active: null, 9 | list: [], 10 | }), 11 | actions: { 12 | /** 13 | * 获取待审核数量 14 | */ 15 | async getAuditCount() { 16 | const r = await auditCountApi() 17 | if (r) { 18 | this.auditCount = r.data 19 | } 20 | }, 21 | /** 22 | * 获取消息列表 23 | * @param {*} size 数据量 24 | * @returns 25 | */ 26 | async getList(size = 10) { 27 | let lastId = '' 28 | const length = this.list.length 29 | if (length) { 30 | lastId = this.list[length - 1].id 31 | } 32 | const r = await pageApi({ lastId, size }) 33 | if (r) { 34 | this.list.push(...r.data) 35 | return r.data 36 | } 37 | }, 38 | /** 39 | * 新增需求 40 | * @param {*} apply 41 | */ 42 | addApply(apply) { 43 | this.list.unshift(apply) 44 | this.auditCount += 1 45 | }, 46 | /** 47 | * 设置选中 48 | * @param {*} apply 49 | */ 50 | setActive({ id }) { 51 | const apply = this.list.find(item => item.id === id) 52 | this.active = apply 53 | }, 54 | /** 55 | * 设置状态 56 | * @param {*} id 57 | * @param {*} status 58 | */ 59 | setStatus(id, status) { 60 | const apply = this.list.find(item => item.id === id) 61 | apply.status = status 62 | this.auditCount = this.auditCount > 0 ? this.auditCount - 1 : 0 63 | }, 64 | /** 65 | * 更新用户在线状态 66 | * @param {*} userId 用户ID 67 | * @param {*} online 在线状态 68 | */ 69 | updateUserOnline(userId, online) { 70 | for (let i = 0; i < this.list.length; i++) { 71 | const { user } = this.list[i]; 72 | if (user.id === userId) { 73 | user.online = online 74 | } 75 | } 76 | }, 77 | /** 78 | * 清除数据 79 | */ 80 | clear() { 81 | clearJson(this.$state) 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /src/stores/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { dayjs } from 'element-plus' 2 | import { clearJson } from '@utils' 3 | import { getAuth, setAuth, clearAuth } from '@utils/storage' 4 | 5 | import { loginApi, registerApi, loginQQApi, logoutApi } from '@/api/auth' 6 | 7 | const auth = getAuth() 8 | 9 | export const useAuthStore = defineStore('auth', { 10 | state: () => ({ 11 | userId: '', 12 | token: '', 13 | expiredAt: '', 14 | ...auth 15 | }), 16 | actions: { 17 | /** 18 | * 登入系统 19 | * @param {*} api 接口 20 | * @param {*} params 参数 21 | * @returns 22 | */ 23 | async sign(api, params) { 24 | const r = await api(params) 25 | if (r) { 26 | setAuth(r.data) 27 | this.$state = r.data 28 | } 29 | return r 30 | }, 31 | /** 32 | * 登录 33 | * @param {*} params 34 | * @returns 35 | */ 36 | login(params) { 37 | return this.sign(loginApi, params) 38 | }, 39 | /** 40 | * 注册 41 | * @param {*} params 42 | * @returns 43 | */ 44 | async register(params) { 45 | return this.sign(registerApi, params) 46 | }, 47 | /** 48 | * QQ登录 49 | * @param {*} params 50 | * @returns 51 | */ 52 | async qqLogin(params) { 53 | return this.sign(loginQQApi, params) 54 | }, 55 | /** 56 | * 退出登录 57 | */ 58 | async logout() { 59 | const r = await logoutApi() 60 | return r 61 | }, 62 | /** 63 | * 校验token 是否过期 64 | */ 65 | validateToken() { 66 | const { token, expiredAt } = this.$state 67 | if (!token.trim() || dayjs(expiredAt).valueOf() < +new Date()) { 68 | return false 69 | } 70 | return true 71 | }, 72 | /** 73 | * 清除数据 74 | */ 75 | clear() { 76 | clearAuth() 77 | clearJson(this.$state) 78 | } 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /src/stores/modules/conversation.js: -------------------------------------------------------------------------------- 1 | import { clearJson } from '@utils' 2 | 3 | import { listApi } from '@/api/conversation' 4 | 5 | export const useConversationStore = defineStore('conversation', { 6 | state: () => ({ 7 | active: null, 8 | list: [], 9 | }), 10 | getters: { 11 | hasUnread: ({ list }) => { 12 | return list.some(item => item.unread > 0) 13 | } 14 | }, 15 | actions: { 16 | /** 17 | * 获取会话列表 18 | * @param {*} params 19 | * @returns 20 | */ 21 | async getList() { 22 | const r = await listApi() 23 | if (r) { 24 | this.$state.list = r.data 25 | } 26 | }, 27 | /** 28 | * 新增会话 存在则更新消息 29 | * @param {*} conversation 30 | */ 31 | addConversation(conversation) { 32 | const { roomId, message } = conversation 33 | const exist = this.list.find(item => item.roomId === roomId) 34 | if (exist) { 35 | exist.message = message 36 | } else { 37 | this.list.unshift(conversation) 38 | } 39 | }, 40 | /** 41 | * 更新消息 42 | * @param {*} id 消息ID 43 | * @param {*} conversation 会话 44 | */ 45 | updateMessage(messageId, conversation){ 46 | for (let i = 0; i < this.list.length; i++) { 47 | const row = this.list[i]; 48 | if (messageId === row.message.id){ 49 | row.message = conversation.message 50 | return 51 | } 52 | } 53 | }, 54 | /** 55 | * 设置未读 56 | * @param {*} id 57 | */ 58 | setUnread(id) { 59 | const conversation = this.list.find(item => item.id === id) 60 | if (conversation.unread) { 61 | conversation.unread += 1 62 | } else { 63 | conversation.unread = 1 64 | } 65 | }, 66 | /** 67 | * 设置已读 68 | * @param {*} id 69 | * @returns 70 | */ 71 | setRead(id) { 72 | const conversation = this.list.find(item => item.id === id) 73 | conversation.unread = 0 74 | return conversation 75 | }, 76 | /** 77 | * 设置选中 78 | * @param {*} conversation 79 | */ 80 | setActive({ id }) { 81 | const conversation = this.setRead(id) 82 | this.active = conversation 83 | }, 84 | /** 85 | * 更新用户在线状态 86 | * @param {*} userId 用户ID 87 | * @param {*} online 在线状态 88 | */ 89 | updateUserOnline(userId, online) { 90 | for (let i = 0; i < this.list.length; i++) { 91 | const { friend } = this.list[i]; 92 | if (friend && friend.userId === userId) { 93 | friend.online = online 94 | break 95 | } 96 | } 97 | }, 98 | /** 99 | * 清除数据 100 | */ 101 | clear() { 102 | clearJson(this.$state) 103 | } 104 | } 105 | }) 106 | -------------------------------------------------------------------------------- /src/stores/modules/expression.js: -------------------------------------------------------------------------------- 1 | import { clearJson } from '@utils' 2 | 3 | import { listApi } from '@/api/expression' 4 | 5 | export const useExpressionStore = defineStore('expression', { 6 | state: () => ({ 7 | list: [], 8 | }), 9 | actions: { 10 | /** 11 | * 获取表情列表 12 | * @param {*} params 13 | * @returns 14 | */ 15 | async getList() { 16 | const r = await listApi() 17 | if (r) { 18 | this.$state.list = r.data 19 | } 20 | }, 21 | /** 22 | * 清除数据 23 | */ 24 | clear() { 25 | clearJson(this.$state) 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/stores/modules/grouping.js: -------------------------------------------------------------------------------- 1 | import { clearJson } from '@utils' 2 | 3 | import { listApi } from '@/api/grouping' 4 | 5 | export const useGroupingStore = defineStore('grouping', { 6 | state: () => ({ 7 | active: null, 8 | list: [], 9 | keyword: '' 10 | }), 11 | getters: { 12 | filterList: (state) => { 13 | const result = [] 14 | const { list, keyword } = state 15 | list.forEach(grouping => { 16 | const exist = grouping.friends.filter(({ nickname , remark }) => nickname.includes(keyword) || remark.includes(keyword)) 17 | exist.sort((a, b) => { 18 | return b.online - a.online 19 | }) 20 | result.push({ 21 | ...grouping, 22 | friends: exist 23 | }) 24 | }) 25 | return result 26 | } 27 | }, 28 | actions: { 29 | /** 30 | * 获取分组好友列表 31 | * @param {*} params 32 | * @returns 33 | */ 34 | async getList() { 35 | const r = await listApi() 36 | if (r) { 37 | this.$state.list = r.data 38 | } 39 | }, 40 | /** 41 | * 新增好友 42 | * @param {*} params 43 | */ 44 | addFriend(grouping) { 45 | const { id, friends } = grouping 46 | const exist = this.list.find(item => item.id === id) 47 | if (exist) { 48 | exist.friends.push(...friends) 49 | } else { 50 | this.list.push(grouping) 51 | } 52 | }, 53 | /** 54 | * 移除好友 55 | * @param {*} userId 56 | */ 57 | removeFriend(userId) { 58 | let flag = false, groupIndex = '', friendIndex = '' 59 | 60 | outer:for (let i = 0; i < this.list.length; i++) { 61 | const grouping = this.list[i]; 62 | for (let j = 0; j < grouping.friends.length; j++) { 63 | const friend = grouping.friends[j]; 64 | if (friend.userId === userId) { 65 | flag = true 66 | groupIndex = i 67 | friendIndex = j 68 | break outer 69 | } 70 | } 71 | } 72 | 73 | if (flag) { 74 | this.list[groupIndex].friends.splice(friendIndex, 1); 75 | } 76 | }, 77 | /** 78 | * 设置选中 79 | * @param {*} friend 80 | */ 81 | setActive({ id }) { 82 | let friend = null 83 | for (let i = 0; i < this.list.length; i++) { 84 | const { friends } = this.list[i]; 85 | friend = friends.find(item => item.id === id) 86 | if (friend) { 87 | break 88 | } 89 | } 90 | this.active = friend 91 | }, 92 | /** 93 | * 更新用户在线状态 94 | * @param {*} userId 用户ID 95 | * @param {*} online 在线状态 96 | */ 97 | updateUserOnline(userId, online) { 98 | for (let i = 0; i < this.list.length; i++) { 99 | const { friends } = this.list[i]; 100 | inner:for (let j = 0; j < friends.length; j++) { 101 | const friend = friends[j]; 102 | if (friend.userId === userId) { 103 | friend.online = online 104 | break inner; 105 | } 106 | } 107 | } 108 | }, 109 | /** 110 | * 清除数据 111 | */ 112 | clear() { 113 | clearJson(this.$state) 114 | } 115 | } 116 | }) 117 | -------------------------------------------------------------------------------- /src/stores/modules/room.js: -------------------------------------------------------------------------------- 1 | import { ONLINE_STATUS } from '@enums/user' 2 | import { clearJson } from '@utils' 3 | 4 | import { pageApi } from '@/api/message' 5 | import { roomGroupUserPageApi, roomGroupUserCountApi } from '@/api/room' 6 | 7 | export const useRoomStore = defineStore('room', { 8 | state: () => ({ 9 | list: [], 10 | }), 11 | actions: { 12 | /** 13 | * 获取消息列表 14 | * @param {*} roomId 房间ID 15 | * @param {*} lastId 最后一个ID 16 | * @param {*} size 数据量 17 | * @returns 18 | */ 19 | async getMessageList(roomId, lastId, size = 10) { 20 | const r = await pageApi({ roomId, lastId, size }) 21 | if (r) { 22 | const messageList = r.data.reverse() 23 | const room = this.list.find(room => room.id === roomId) 24 | if (room) { 25 | const { messages } = room 26 | messages.unshift(...messageList) 27 | } else { 28 | this.list.push({ 29 | id: roomId, 30 | messages: messageList, 31 | userTotalCount: 0, 32 | userOnlineCount: 0, 33 | users: [] 34 | }) 35 | } 36 | return messageList 37 | } 38 | }, 39 | /** 40 | * 更新消息 41 | * @param {*} id 消息ID 42 | * @param {*} conversation 会话 43 | */ 44 | updateMessage(messageId, conversation){ 45 | const { roomId, message } = conversation 46 | for (let i = 0; i < this.list.length; i++) { 47 | const { messages } = this.list[i]; 48 | for (let j = 0; j < messages.length; j++) { 49 | if (messageId === messages[j].id) { 50 | messages[j] = message 51 | return 52 | } 53 | } 54 | } 55 | }, 56 | /** 57 | * 获取房间用户 58 | * @param {*} roomId 房间ID 59 | * @param {*} lastId 最后一个ID 60 | * @param {*} size 数据量 61 | * @returns 62 | */ 63 | async getUserList(roomId, lastId, size = 40) { 64 | const r = await roomGroupUserPageApi({ roomId, lastId, size }) 65 | if (r) { 66 | const userList = r.data 67 | const room = this.list.find(room => room.id === roomId) 68 | if (room) { 69 | const { users } = room 70 | users.push(...userList) 71 | } else { 72 | this.list.push({ 73 | id: roomId, 74 | messages: [], 75 | userTotalCount: 0, 76 | userOnlineCount: 0, 77 | users: userList 78 | }) 79 | } 80 | return userList 81 | } 82 | }, 83 | /** 84 | * 获取房间用户数量 85 | * @param {*} roomId 房间ID 86 | * @returns 87 | */ 88 | async getUserCount(roomId) { 89 | const r = await roomGroupUserCountApi({ roomId }) 90 | if (r) { 91 | const { totalCount, onlineCount } = r.data 92 | const room = this.list.find(room => room.id === roomId) 93 | if (room) { 94 | room.userTotalCount = totalCount 95 | room.userOnlineCount = onlineCount 96 | } else { 97 | this.list.push({ 98 | id: roomId, 99 | messages: [], 100 | userTotalCount: totalCount, 101 | userOnlineCount: onlineCount, 102 | users: [], 103 | }) 104 | } 105 | return r.data 106 | } 107 | }, 108 | 109 | /** 110 | * 新增房间 存在则新增消息 不存在则获取消息列表 111 | * @param {*} roomId 房间ID 112 | * @param {*} message 消息 113 | */ 114 | addRoom(roomId, message) { 115 | const room = this.list.find(item => item.id === roomId) 116 | if (room) { 117 | room.messages.push(message) 118 | } else { 119 | this.getMessageList(roomId) 120 | } 121 | }, 122 | /** 123 | * 添加用户 124 | */ 125 | addUser(user) { 126 | const room = this.list.find(room => room.id === user.roomId) 127 | if (room) { 128 | const { users } = room 129 | const { roomUserId } = users[users.length - 1] 130 | if (user.roomUserId === roomUserId + 1) { 131 | users.push(user) 132 | } 133 | room.userTotalCount += 1 134 | room.userOnlineCount += 1 135 | } 136 | }, 137 | /** 138 | * 更新用户信息 139 | * @param {*} id 房间ID 140 | * @param {*} nickname 消息 141 | */ 142 | updateUser(user){ 143 | this.list.forEach(room => { 144 | const { users } = room 145 | for (let i = 0; i < users.length; i++) { 146 | const item = users[i]; 147 | if (item.id === user.id) { 148 | item.nickname = user.nickname, 149 | item.avatar = user.avatar, 150 | item.sex = user.sex 151 | break 152 | } 153 | } 154 | }); 155 | }, 156 | /** 157 | * 更新用户在线状态 158 | * @param {*} userId 用户ID 159 | * @param {*} online 在线状态 160 | */ 161 | updateUserOnline(userId, online) { 162 | for (let i = 0; i < this.list.length; i++) { 163 | const room = this.list[i]; 164 | const { users } = room 165 | inner:for (let j = 0; j < users.length; j++) { 166 | const user = users[j]; 167 | if (user.id === userId) { 168 | user.online = online 169 | if (online === ONLINE_STATUS.ONLINE) { 170 | room.userOnlineCount += 1 171 | } else if (online === ONLINE_STATUS.OFFLINE) { 172 | room.userOnlineCount -= 1 173 | } 174 | break inner; 175 | } 176 | } 177 | } 178 | }, 179 | /** 180 | * 清除数据 181 | */ 182 | clear() { 183 | clearJson(this.$state) 184 | } 185 | } 186 | }) 187 | -------------------------------------------------------------------------------- /src/stores/modules/user.js: -------------------------------------------------------------------------------- 1 | import { clearJson } from '@utils' 2 | 3 | import { userInfoApi } from '@/api/auth' 4 | 5 | export const useUserStore = defineStore('user', { 6 | state: () => ({ 7 | id: '', 8 | nickname: '', 9 | avatar: '', 10 | sex: '', 11 | online: '', 12 | email: '', 13 | lastAt: '', 14 | status: '', 15 | createdAt: '', 16 | token: '', 17 | expireAt: '' 18 | }), 19 | actions: { 20 | /** 21 | * 登录 22 | * @param {*} params 23 | * @returns 24 | */ 25 | async getUserInfo() { 26 | const r = await userInfoApi() 27 | if (r) { 28 | this.$state = r.data 29 | } 30 | return r ? r.data : null 31 | }, 32 | 33 | /** 34 | * 清除数据 35 | */ 36 | clear() { 37 | clearJson(this.$state) 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/stores/modules/websocket.js: -------------------------------------------------------------------------------- 1 | import WebsocketClass from '@utils/websocket' 2 | import { getWebsocketOrigin } from '@utils' 3 | import { WEBSOCKET_MAPPING } from '@constants' 4 | import { WEBSOCKET_TYPE } from '@enums/websocket' 5 | 6 | export const useWebsocketStore = defineStore('websocket', { 7 | state: () => ({ 8 | response: null, 9 | socket: null 10 | }), 11 | getters: {}, 12 | actions: { 13 | /** 14 | * 初始化websocket 15 | */ 16 | init() { 17 | if (!this.socket) { 18 | const url = `${ getWebsocketOrigin() }${ WEBSOCKET_MAPPING }/${ useAuthStore().token }` 19 | this.socket = new WebsocketClass(url, data => { 20 | if (data && data.type === WEBSOCKET_TYPE.HEARTBEAT) { 21 | return 22 | } 23 | this.response = data 24 | console.log('🚲~~:', data) 25 | }) 26 | this.socket.connect() 27 | } 28 | }, 29 | /** 30 | * 发送信息 31 | * @param {*} data 32 | */ 33 | send(params) { 34 | this.socket.send(params) 35 | }, 36 | /** 37 | * 手动断开websocket 38 | */ 39 | close() { 40 | if (this.socket) { 41 | this.socket.close() 42 | } 43 | this.response = null 44 | this.socket = null 45 | } 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /src/stores/root.js: -------------------------------------------------------------------------------- 1 | import { useApplyStore } from "./modules/apply" 2 | 3 | export const useRootStore = defineStore('root', { 4 | state: () => ({}), 5 | getters: {}, 6 | actions: { 7 | /** 8 | * 新增消息 9 | * @param {*} conversation 10 | */ 11 | addMessage(conversation) { 12 | const { roomId, message } = conversation 13 | useConversationStore().addConversation(conversation) 14 | useRoomStore().addRoom(roomId, message) 15 | }, 16 | /** 17 | * 新增未读消息 18 | * @param {*} conversation 19 | */ 20 | addUnreadMessage(conversation) { 21 | this.addMessage(conversation) 22 | useConversationStore().setUnread(conversation.id) 23 | }, 24 | /** 25 | * 更新消息状态 26 | */ 27 | updateMessage(id, conversation, sendStatus){ 28 | conversation.message.sendStatus = sendStatus 29 | useConversationStore().updateMessage(id, conversation) 30 | useRoomStore().updateMessage(id, conversation) 31 | }, 32 | /** 33 | * 更新好友,群用户在线状态 34 | * @param {*} userId 用户ID 35 | * @param {*} online 在线状态 36 | */ 37 | updateUserOnline(userId, online) { 38 | useConversationStore().updateUserOnline(userId, online) 39 | useGroupingStore().updateUserOnline(userId, online) 40 | useRoomStore().updateUserOnline(userId, online) 41 | useApplyStore().updateUserOnline(userId, online) 42 | }, 43 | /** 44 | * 清除用户数据 用户信息 45 | * @param {*} 46 | */ 47 | clearUserData() { 48 | useUserStore().clear() 49 | }, 50 | /** 51 | * 清除所有数据 52 | * @param {*} param0 53 | */ 54 | clearData() { 55 | this.clearUserData() 56 | useAuthStore().clear() 57 | } 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/views/apply/components/apply-card/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 75 | 76 | 89 | -------------------------------------------------------------------------------- /src/views/apply/components/apply-panel/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 111 | 112 | 122 | -------------------------------------------------------------------------------- /src/views/apply/components/pass-dialog/components/form-ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | 3 | export const props = { 4 | form, 5 | loading, 6 | groupings: { type: Array, default: () => [] } 7 | } 8 | 9 | export const rules = { 10 | groupingId: [{ required: true, message: '请选择分组', trigger: 'change' }], 11 | } 12 | -------------------------------------------------------------------------------- /src/views/apply/components/pass-dialog/components/form-ui/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 47 | 48 | -------------------------------------------------------------------------------- /src/views/apply/components/pass-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 93 | 94 | -------------------------------------------------------------------------------- /src/views/apply/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /src/views/conversation/components/conversation-card/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 88 | 89 | 100 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/audio/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 64 | 65 | 75 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/expression/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/file/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/image/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/video-call/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/views/conversation/components/editor/components/voice-call/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/views/conversation/components/group-user-panel/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 124 | 125 | 135 | -------------------------------------------------------------------------------- /src/views/conversation/components/group-user/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /src/views/conversation/components/message-panel/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 191 | 192 | 210 | -------------------------------------------------------------------------------- /src/views/conversation/components/message/components/audio-message/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 73 | 74 | 130 | -------------------------------------------------------------------------------- /src/views/conversation/components/message/components/file-message/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /src/views/conversation/components/message/components/image-message/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /src/views/conversation/components/message/components/text-message/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /src/views/conversation/components/message/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 97 | 98 | 127 | -------------------------------------------------------------------------------- /src/views/conversation/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 37 | -------------------------------------------------------------------------------- /src/views/friend/components/friend-crad/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 47 | 48 | 50 | -------------------------------------------------------------------------------- /src/views/friend/components/friend-panel/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 62 | 63 | 73 | -------------------------------------------------------------------------------- /src/views/friend/components/headbar/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/views/friend/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 41 | 42 | 67 | -------------------------------------------------------------------------------- /src/views/group/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /src/views/layout/components/media-dialog/components/operation/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 93 | 94 | 98 | -------------------------------------------------------------------------------- /src/views/layout/components/media-dialog/components/status/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 46 | 47 | 57 | -------------------------------------------------------------------------------- /src/views/layout/components/media-dialog/components/user-box/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/views/layout/components/media-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 54 | 55 | 78 | -------------------------------------------------------------------------------- /src/views/layout/components/promote/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 63 | 64 | 72 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-email-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-email-dialog/ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | import { email, captcha, } from '@rules/user' 3 | 4 | export const props = { form, loading } 5 | 6 | export const rules = { originalEmail: email, newEmail: email, captcha, } 7 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-email-dialog/ui/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | 42 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-info-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 53 | 54 | 56 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-info-dialog/ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | import { nickname, sex, email, captcha, } from '@rules/user' 3 | 4 | export const props = { form, loading } 5 | 6 | export const rules = { nickname, sex, email, captcha, } 7 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/edit-info-dialog/ui/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 48 | 49 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/publicize/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab-apply/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab-conversation/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab-exit/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab-friend/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab-group/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/components/tab/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 46 | 47 | 67 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/components/tabbar/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 23 | -------------------------------------------------------------------------------- /src/views/layout/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /src/views/layout/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 34 | -------------------------------------------------------------------------------- /src/views/login/components/login-form/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | 41 | 43 | -------------------------------------------------------------------------------- /src/views/login/components/login-form/ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | import { email, captcha } from '@rules/user' 3 | 4 | export const props = { form, loading } 5 | 6 | export const rules = { email, captcha } 7 | -------------------------------------------------------------------------------- /src/views/login/components/login-form/ui/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 42 | 43 | -------------------------------------------------------------------------------- /src/views/login/components/other/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/views/login/components/other/qq/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 52 | 53 | 55 | -------------------------------------------------------------------------------- /src/views/login/components/panel/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /src/views/login/components/register-form/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | 45 | 47 | -------------------------------------------------------------------------------- /src/views/login/components/register-form/ui/index.js: -------------------------------------------------------------------------------- 1 | import { form, loading } from '@props' 2 | import { nickname, sex, email, captcha, } from '@rules/user' 3 | 4 | export const props = { form, loading } 5 | 6 | export const rules = { nickname, sex, email, captcha, } 7 | -------------------------------------------------------------------------------- /src/views/login/components/register-form/ui/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 54 | 55 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | 30 | 34 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig, loadEnv } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import VueDevTools from 'vite-plugin-vue-devtools' 7 | 8 | import AutoImport from 'unplugin-auto-import/vite' 9 | import Components from 'unplugin-vue-components/vite' 10 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 11 | import Icons from 'unplugin-icons/vite' 12 | import IconsResolver from 'unplugin-icons/resolver' 13 | 14 | const aliasPath = (path) => { 15 | return fileURLToPath(new URL(path, import.meta.url)) 16 | } 17 | 18 | // https://vitejs.dev/config/ 19 | export default defineConfig(({ mode }) => { 20 | const env = loadEnv(mode, process.cwd(), 'VITE_') 21 | 22 | return { 23 | plugins: [ 24 | vue(), 25 | vueJsx(), 26 | VueDevTools(), 27 | AutoImport({ 28 | imports: ['vue', 'vue-router', 'pinia'], 29 | resolvers: [ElementPlusResolver(), IconsResolver({ prefix: 'Icon' })], 30 | dirs: ['src/stores/*'], 31 | eslintrc: { 32 | enabled: true, 33 | filepath: './.eslintrc-auto-import.json', 34 | globalsPropValue: true 35 | } 36 | }), 37 | Components({ 38 | resolvers: [ElementPlusResolver(), IconsResolver({ prefix: false, enabledCollections: ['ep'] })] 39 | }), 40 | Icons({ 41 | autoInstall: true, 42 | }), 43 | ], 44 | // 项目根目录(index.html 文件所在的位置)。可以是一个绝对路径,或者一个相对于该配置文件本身的相对路径。 45 | root: process.cwd(), 46 | // 公共基础路径。 47 | base: './', 48 | // 作为静态资源服务的文件夹。 49 | publicDir: 'public', 50 | // 存储缓存文件的目录。 51 | cacheDir: 'node_modules/.vite', 52 | resolve: { 53 | // 路径别名 54 | alias: { 55 | '@': aliasPath('./src'), 56 | '@utils': aliasPath('./src/common/utils'), 57 | '@enums': aliasPath('./src/common/enums'), 58 | '@constants': aliasPath('./src/common/constants'), 59 | '@props': aliasPath('./src/common/props'), 60 | '@rules': aliasPath('./src/common/rules'), 61 | } 62 | }, 63 | css: { 64 | // 在开发过程中是否启用 sourcemap。 65 | devSourcemap: true 66 | }, 67 | // 控制台输出的级别。 68 | logLevel: 'info', 69 | // 避免 Vite 清屏而错过在终端中打印某些关键信息。 70 | clearScreen: true, 71 | // 以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。 72 | envPrefix: 'VITE_', 73 | // 开发服务器选项 74 | server: { 75 | // 指定服务器应该监听哪个 IP 地址。 76 | host: true, 77 | // 服务器端口。 78 | port: env.VITE_PORT, 79 | // 若端口已被占用则会直接退出 80 | strictPort: true, 81 | // 自动打开浏览器。 82 | open: false, 83 | // 代理。 84 | proxy: { 85 | '^/chatterbox/websocket': { 86 | target: 'https://chatterbox.gumingchen.icu', 87 | // target: 'http://localhost:8831', 88 | changeOrigin: true, 89 | ws: true 90 | }, 91 | '^/chatterbox': { 92 | target: 'https://chatterbox.gumingchen.icu', 93 | // target: 'http://localhost:8830', 94 | changeOrigin: true, 95 | }, 96 | '^/resource': { 97 | target: 'https://chatterbox.gumingchen.icu', 98 | // target: 'http://localhost:8830', 99 | changeOrigin: true, 100 | } 101 | }, 102 | // 为开发服务器配置 CORS。 103 | cors: true 104 | }, 105 | // 构建选项 106 | build: { 107 | // 设置最终构建的浏览器兼容目标。 108 | target: 'modules', 109 | // 决定是否自动注入 module preload 的 polyfill。 110 | // polyfillModulePreload: true, 111 | modulePreload: { 112 | polyfill: true 113 | }, 114 | // 指定输出路径(相对于 项目根目录). 115 | outDir: 'dist', 116 | // 指定生成静态资源的存放路径(相对于 build.outDir)。 117 | assetsDir: 'assets', 118 | // 小于此阈值的导入或引用资源将内联为 base64 编码 119 | assetsInlineLimit: 'assets', 120 | // 启用/禁用 CSS 代码拆分。 121 | cssCodeSplit: true, 122 | // 构建后是否生成 source map 文件。 boolean | 'inline' | 'hidden' 123 | sourcemap: false, 124 | // chunk 大小警告的限制(以 kbs 为单位)。 125 | chunkSizeWarningLimit: 500 126 | } 127 | } 128 | }) 129 | --------------------------------------------------------------------------------