├── .env.development ├── .env.production ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── demo.html ├── index.html ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── chat │ │ ├── index.ts │ │ └── type.ts │ ├── friend │ │ ├── index.ts │ │ └── type.ts │ ├── group │ │ ├── index.ts │ │ └── type.ts │ ├── http │ │ └── index.ts │ ├── login │ │ ├── index.ts │ │ └── type.ts │ ├── request.ts │ └── session │ │ ├── index.ts │ │ └── type.ts ├── assets │ ├── css │ │ ├── base.css │ │ ├── index.less │ │ └── theme.less │ ├── images │ │ ├── github_login_icon.png │ │ ├── login.png │ │ ├── login1.png │ │ ├── login2.png │ │ ├── logo.png │ │ └── none.png │ └── svg │ │ ├── icons │ │ ├── add.svg │ │ ├── addUser.svg │ │ ├── audio.svg │ │ ├── file.svg │ │ ├── gitee.svg │ │ ├── github.svg │ │ └── smile.svg │ │ └── index.vue ├── components │ ├── AddFriend.vue │ ├── AddGroup.vue │ ├── LogoutDialog.vue │ ├── MyAudio.vue │ ├── UserMsg.vue │ ├── Voice.vue │ └── emoji │ │ ├── Emoji.vue │ │ └── emoji.ts ├── directive │ ├── customMenu │ │ ├── index.ts │ │ └── index.vue │ ├── index.ts │ └── type.ts ├── env.d.ts ├── hooks │ ├── usePoint.ts │ └── useTheme.ts ├── layout │ ├── components │ │ └── UserInfo.vue │ └── index.vue ├── main.ts ├── router │ └── index.ts ├── store │ ├── index.ts │ ├── session.ts │ └── user.ts ├── utils │ ├── index.ts │ ├── session.ts │ ├── socket.ts │ └── storage.ts └── views │ ├── about │ └── index.vue │ ├── address │ └── index.vue │ ├── home │ └── index.vue │ ├── login │ └── index.vue │ └── session │ └── index.vue ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | # 环境名称 2 | VITE_APP_NODE_ENV = 'development' 3 | # 后台访问地址 4 | # VITE_APP_BASE_API = 'http://127.0.0.1:8000/api' 5 | # VITE_APP_WS_API = 'ws://127.0.0.1:8000/im/connect?token=' 6 | VITE_APP_BASE_API = 'https://im.pltrue.top/api' 7 | VITE_APP_WS_API = 'wss://im.pltrue.top/im/connect?token=' 8 | VITE_APP_CLIENT_ID= '77fc33d207bafb6006a6' 9 | VITE_APP_REDIRECT_URL = 'https://chat.pltrue.top/login?login_type=github' 10 | VITE_APP_GITEE_CLIENT_ID= '1fcf7fa2c24188b2b63366aa8e9ea0aedd0234cf5685489e2be6572eab39eac9' 11 | VITE_APP_GITEE_REDIRECT_URL = 'https://chat.pltrue.top/login?login_type=gitee' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 环境名称 2 | VITE_APP_NODE_ENV = 'production' 3 | # 后台访问地址 4 | VITE_APP_BASE_API = 'https://im.pltrue.top/api' 5 | VITE_APP_WS_API = 'wss://im.pltrue.top/im/connect?token=' 6 | VITE_APP_CLIENT_ID= '77fc33d207bafb6006a6' 7 | VITE_APP_REDIRECT_URL = 'https://chat.pltrue.top/login?login_type=github' 8 | 9 | VITE_APP_GITEE_CLIENT_ID= '1fcf7fa2c24188b2b63366aa8e9ea0aedd0234cf5685489e2be6572eab39eac9' 10 | VITE_APP_GITEE_REDIRECT_URL = 'https://chat.pltrue.top/login?login_type=gitee' -------------------------------------------------------------------------------- /.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 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | tool.html -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # im-services web客户端 2 | 3 | [![OSCS Status](https://www.oscs1024.com/platform/badge/IM-Tools/Im-Services.svg?size=small)](https://www.oscs1024.com/project/IM-Tools/Im-Services?ref=badge_small) 4 | 5 | 6 | ### 技术栈 :Vue 3 + TypeScript + Vite 7 | 8 | ### 本地环境配置 `.env.development` 9 | ```shell 10 | # 环境名称 11 | VITE_APP_NODE_ENV = 'development' 12 | # 后台访问地址 13 | # VITE_APP_BASE_API = 'http://127.0.0.1:8000/api' 14 | # VITE_APP_WS_API = 'ws://127.0.0.1:8000/im/connect?token=' 15 | VITE_APP_BASE_API = 'http://im.pltrue.top/api' 16 | VITE_APP_WS_API = 'ws://im.pltrue.top/im/connect?token=' 17 | VITE_APP_CLIENT_ID= '77fc33d207bafb6006a6' 18 | VITE_APP_REDIRECT_URL = 'http://chat.pltrue.top/login?login_type=github' 19 | VITE_APP_GITEE_CLIENT_ID= '1fcf7fa2c24188b2b63366aa8e9ea0aedd0234cf5685489e2be6572eab39eac9' 20 | VITE_APP_GITEE_REDIRECT_URL = 'http://chat.pltrue.top/login?login_type=gitee' 21 | ``` 22 | 23 | ### 启动 24 | 25 | ```shell 26 | yarn install 27 | yarn run dev 28 | ``` 29 | 30 | ### 配置 `.env.production` 31 | ```shell 32 | # 环境名称 33 | VITE_APP_NODE_ENV = 'production' 34 | # 后台访问地址 35 | VITE_APP_BASE_API = 'http://im.pltrue.top/api' 36 | VITE_APP_WS_API = 'ws://im.pltrue.top/im/connect?token=' 37 | VITE_APP_CLIENT_ID= '77fc33d207bafb6006a6' 38 | VITE_APP_REDIRECT_URL = 'http://chat.pltrue.top/login?login_type=github' 39 | 40 | VITE_APP_GITEE_CLIENT_ID= '1fcf7fa2c24188b2b63366aa8e9ea0aedd0234cf5685489e2be6572eab39eac9' 41 | VITE_APP_GITEE_REDIRECT_URL = 'http://chat.pltrue.top/login?login_type=gitee' 42 | ``` 43 | 44 | ### 打包 45 | 46 | ```shell 47 | yarn run build 48 | ``` -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 动画特效 8 | 67 | 68 | 69 |
70 | 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Go Chat 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im-client-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@vueuse/core": "^8.9.4", 12 | "axios": "^0.27.2", 13 | "element-plus": "^2.2.9", 14 | "js-audio-recorder": "^1.0.7", 15 | "less": "^4.1.3", 16 | "less-loader": "^11.0.0", 17 | "livekit-client": "^1.2.10", 18 | "normalize.css": "^8.0.1", 19 | "pinia": "^2.0.14", 20 | "push.js": "^1.0.12", 21 | "vue": "^3.2.25", 22 | "vue-router": "4", 23 | "vue3-discordpicker": "^1.1.0" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^18.0.3", 27 | "@vitejs/plugin-vue": "^2.3.3", 28 | "fast-glob": "^3.2.11", 29 | "typescript": "^4.5.4", 30 | "vite": "^2.9.9", 31 | "vite-plugin-svg-icons": "^2.0.1", 32 | "vue-tsc": "^0.34.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /src/api/chat/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../request' 2 | import { 3 | chatData, 4 | sendChatData, 5 | chatRecordType, 6 | chatItemType, 7 | chatGroupType, 8 | } from './type' 9 | // 获取私聊消息 10 | export function chatMessage(params: chatData) { 11 | return request.get>('/messages', { params }) 12 | } 13 | // 获取群聊消息 14 | export function chatGroupMessage(params: chatData) { 15 | return request.get>('/messages/groups', { 16 | params, 17 | }) 18 | } 19 | // 发送消息 20 | export function sendChatMessage(data: sendChatData) { 21 | return request.post('/messages/private', data) 22 | } 23 | // 文件上传 24 | export function uploadFile(data: { file: any }) { 25 | return request.post('/upload/file', data) 26 | } 27 | -------------------------------------------------------------------------------- /src/api/chat/type.ts: -------------------------------------------------------------------------------- 1 | export interface chatData { 2 | page: number | string 3 | pageSize: number | string 4 | to_id: number | string 5 | } 6 | 7 | export interface sendChatData { 8 | msg_client_id: number | string //客户端唯一值 用时间戳生成 9 | msg_type: number //1.文本 2.语音 3.文件 10 | to_id: number //推送人 11 | channel_type: number //1.私聊 2.频道 3.广播 12 | data?: string 13 | message: string 14 | } 15 | 16 | export interface chatItemType { 17 | created_at: string 18 | data: string 19 | form_id: number 20 | id: number 21 | is_read: number 22 | msg: string 23 | msg_type: number 24 | status: number 25 | to_id: number 26 | isShowTime?: boolean 27 | channel_type: number 28 | Users: { 29 | avatar: string 30 | email: string 31 | id: number 32 | name: string 33 | } 34 | } 35 | 36 | export interface chatGroupType { 37 | ClientMessageId: number 38 | FormId: number 39 | GroupId: number 40 | Id: number 41 | Message: string 42 | MessageId: number 43 | SendTime: number 44 | MsgType: number 45 | Users: { 46 | avatar: string 47 | email: string 48 | id: number 49 | name: string 50 | } 51 | } 52 | export interface chatRecordType { 53 | list: T[] 54 | mate: { 55 | page: number 56 | pageSize: number 57 | total: number 58 | } 59 | id?: number 60 | from_id?: number 61 | } 62 | -------------------------------------------------------------------------------- /src/api/friend/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../request' 2 | import type { recordFriend, requestListType, userType, friendRecordType, friendType,noFriendType} from './type' 3 | // 获取好友列表 4 | export function friendList(params?: Object) { 5 | return request.get('/friends', { params }) 6 | } 7 | //添加好友 8 | export function recordFriend(data: recordFriend) { 9 | return request.post('/friends/record', data) 10 | } 11 | //删除好友 12 | export function deleteFriend(id: number) { 13 | return request.delete(`/friends/${id}`) 14 | } 15 | // 获取好友详情 16 | export function getFriendDetails(params: number) { 17 | return request.get(`/user/${params}`) 18 | } 19 | // 获取好友请求列表 20 | export function friendRecordList(params?: Object) { 21 | return request.get[]>('/friends/record', { params }) 22 | } 23 | //同意/拒绝好友请求 24 | export function friendRecord(data: friendRecordType) { 25 | return request.put>('/friends/record', data) 26 | } 27 | // 查询非好友列表 28 | export function getUserList(params?: { email: any}){ 29 | return request.get('/friends/userQuery', { params }) 30 | } 31 | -------------------------------------------------------------------------------- /src/api/friend/type.ts: -------------------------------------------------------------------------------- 1 | export interface recordFriend { 2 | to_id: number | string 3 | information: string 4 | } 5 | 6 | export interface userType { 7 | avatar: string 8 | id: number 9 | name: string 10 | age?: number 11 | client_type?: number 12 | email?: string 13 | last_login_time?: string 14 | sex?: number 15 | status?: number 16 | uid?: string 17 | bio?: string 18 | } 19 | export interface requestListType { 20 | created_at: string 21 | form_id: number 22 | id: number 23 | information: number | string 24 | to_id: number 25 | status: number 26 | users: T 27 | } 28 | 29 | export interface friendRecordType { 30 | id: number 31 | status: number 32 | } 33 | 34 | export interface friendType { 35 | created_at: string 36 | form_id: number 37 | id: number 38 | note: string 39 | to_id: number 40 | status: number 41 | top_time: string 42 | uid: string 43 | update_at: string 44 | Users: T 45 | } 46 | 47 | export interface noFriendType{ 48 | avatar: string, 49 | bio: string, 50 | email: string, 51 | id: number, 52 | name: string, 53 | sex: number, 54 | status: number 55 | } -------------------------------------------------------------------------------- /src/api/group/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../request' 2 | import type { 3 | createGroupDataType, 4 | groupUserType, 5 | groupInfoType, 6 | removeType, 7 | } from './type' 8 | // 获取群聊用户信息 9 | export function getGroupUserInfo(params: { id: number }) { 10 | return request.get>(`/groups/users/${params.id}`) 11 | } 12 | //创建群聊 13 | export function createGroup(data: createGroupDataType) { 14 | return request.post('/groups/store', data) 15 | } 16 | 17 | //申请加入群聊 18 | export function applyAddGroup(data: { id: number }) { 19 | return request.post(`/groups/applyJoin/${data.id}`) 20 | } 21 | // 退出群聊 22 | export function deleteGroup(params: { id: number }) { 23 | return request.delete(`/groups/${params.id}`) 24 | } 25 | // 邀请或者移除用户入群 26 | export function createOrRemoveUser(data: removeType) { 27 | return request.post('/groups/createOrRemoveUser', data) 28 | } 29 | -------------------------------------------------------------------------------- /src/api/group/type.ts: -------------------------------------------------------------------------------- 1 | export interface createGroupDataType { 2 | name: string 3 | info: string 4 | avatar: string 5 | password: string 6 | is_pwd: number 7 | theme: string 8 | select_user: number[] 9 | } 10 | 11 | export interface groupUserType { 12 | avatar: string 13 | created_at: string 14 | group_id: number 15 | id: number 16 | name: string 17 | remark: string 18 | user_id: number 19 | users: { 20 | age: number, 21 | avatar: string, 22 | email: string, 23 | name: string, 24 | id: number 25 | } 26 | } 27 | 28 | export interface groupInfoTyep{ 29 | avatar: string 30 | created_at: string 31 | id: number 32 | info: string 33 | name: string 34 | user_id: number, 35 | is_pwd: number 36 | } 37 | export interface groupInfoType{ 38 | group_users: T[], 39 | groups: groupInfoTyep 40 | } 41 | 42 | 43 | export interface removeType{ 44 | select_user: number[] 45 | group_id: number 46 | type: number 47 | } -------------------------------------------------------------------------------- /src/api/http/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' 3 | import { ElMessage, ElNotification } from 'element-plus' 4 | import { getStorage } from '@/utils/storage' 5 | class Request { 6 | // axios 实例 7 | instance: AxiosInstance 8 | // baseConfig: AxiosRequestConfig = { baseURL: "/api", timeout: 60000 }; 9 | constructor(config: AxiosRequestConfig) { 10 | // 使用axios.create创建axios实例 11 | this.instance = axios.create(config) 12 | // this.instance = axios.create(Object.assign(this.baseConfig, config)); 13 | this.instance.interceptors.request.use( 14 | (config: AxiosRequestConfig) => { 15 | // 一般会请求拦截里面加token 16 | const token = getStorage('token') 17 | config.headers!.Authorization = 'Bearer ' + token 18 | let { data = {}, method } = config 19 | if (method === 'post' || method === 'put') { 20 | const formData = new FormData() 21 | Object.keys(data).forEach((item) => { 22 | // console.log(data, data[item] instanceof Array, data[item]) 23 | if (data[item] instanceof Array) { 24 | data[item].forEach((el: any) => { 25 | formData.append(item + '[]', el) 26 | }) 27 | } else { 28 | formData.append(item, data[item]) 29 | } 30 | }) 31 | config.data = formData 32 | } 33 | return config 34 | }, 35 | (err: any) => { 36 | return Promise.reject(err) 37 | } 38 | ) 39 | 40 | this.instance.interceptors.response.use( 41 | (res: AxiosResponse) => { 42 | // 直接返回res,当然你也可以只返回res.data 43 | if (Object.prototype.toString.apply(res.data) === '[object Blob]') { 44 | return res.data 45 | } 46 | const { data, code, message } = res.data 47 | if (code === 200) { 48 | return data 49 | } else { 50 | ElNotification({ 51 | title: 'Warning', 52 | message: message, 53 | type: 'warning', 54 | }) 55 | return Promise.reject(res) 56 | } 57 | }, 58 | (err: any) => { 59 | ElMessage({ 60 | message: err.message, 61 | type: 'error', 62 | }) 63 | // 请求错误 64 | return Promise.reject(err.response) 65 | } 66 | ) 67 | } 68 | 69 | // 定义请求方法 70 | public request(config: AxiosRequestConfig): Promise { 71 | return this.instance.request(config) 72 | } 73 | 74 | public get(url: string, config?: AxiosRequestConfig): Promise { 75 | return this.instance.get(url, config) 76 | } 77 | 78 | public post( 79 | url: string, 80 | data?: any, 81 | config?: AxiosRequestConfig 82 | ): Promise { 83 | return this.instance.post(url, data, config) 84 | } 85 | 86 | public put( 87 | url: string, 88 | data?: any, 89 | config?: AxiosRequestConfig 90 | ): Promise { 91 | console.log(555) 92 | return this.instance.put(url, data, config) 93 | } 94 | 95 | public delete(url: string, config?: AxiosRequestConfig): Promise { 96 | return this.instance.delete(url, config) 97 | } 98 | } 99 | 100 | export default Request 101 | -------------------------------------------------------------------------------- /src/api/login/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../request' 2 | import { loginData, registerData, emailData } from './type' 3 | // 登录接口 4 | export function login(data: loginData) { 5 | return request.post('/auth/login', data) 6 | } 7 | export function oauthLogin(params?: Object) { 8 | return request.get('/auth/githubLogin', {params}) 9 | } 10 | //注册 11 | export function registerede(data: registerData) { 12 | return request.post('/auth/registered', data) 13 | } 14 | // 邮箱验证码 15 | export function sendEmailCode(data: emailData) { 16 | return request.post('/auth/sendEmailCode', data) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/api/login/type.ts: -------------------------------------------------------------------------------- 1 | export interface loginData { 2 | email: string, 3 | password: string | number 4 | } 5 | 6 | export interface registerData { 7 | email: string, 8 | name: string, 9 | password: string | number, 10 | password_repeat: string | number, 11 | code: string | number, 12 | email_type?:string | number, 13 | } 14 | 15 | export interface emailData { 16 | email: string, 17 | email_type: string | number 18 | } -------------------------------------------------------------------------------- /src/api/request.ts: -------------------------------------------------------------------------------- 1 | import Request from "./http"; 2 | const baseURL = import.meta.env.VITE_APP_NODE_ENV === 'production' ? import.meta.env.VITE_APP_BASE_API : '/api' 3 | const request = new Request({ 4 | baseURL, 5 | timeout: 20000 6 | }) 7 | 8 | export default request -------------------------------------------------------------------------------- /src/api/session/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../request' 2 | import type { userType, sessionType, sessionData, groupType } from './type' 3 | // 获取会话列表 4 | export function sessionList(params?: Object) { 5 | return request.get[]>('/sessions', { 6 | params, 7 | }) 8 | } 9 | //创建会话 10 | export function createSession(data: sessionData) { 11 | return request.post>('/sessions', data) 12 | } 13 | // 移除会话 14 | export function removeSession(id: number) { 15 | return request.delete(`/sessions/${id}`) 16 | } 17 | -------------------------------------------------------------------------------- /src/api/session/type.ts: -------------------------------------------------------------------------------- 1 | export interface sessionData { 2 | id: number, 3 | type: number 4 | } 5 | export interface userType{ 6 | age: number 7 | avatar: string 8 | bio: string 9 | client_type: number 10 | email: string 11 | id: number 12 | last_login_time: string 13 | name: string 14 | sex: number 15 | status: number 16 | } 17 | export interface groupType{ 18 | avatar: string 19 | id: number 20 | created_at: string 21 | name: string 22 | hot: number 23 | user_id: number, 24 | is_pwd: number, 25 | info: string 26 | } 27 | export interface sessionType { 28 | id: number, 29 | name: string, 30 | note: string, 31 | created_at: string, 32 | avatar: string, 33 | top_time: string, 34 | status: number, 35 | to_id: number, 36 | form_id: number, 37 | group_id?: number, 38 | channel_type: number, 39 | top_status: number, 40 | Users?: T, 41 | Groups?: K, 42 | value?: any, 43 | last_message: { 44 | content: string, 45 | time: string, 46 | isPoint?: boolean, 47 | num?: number 48 | } 49 | } -------------------------------------------------------------------------------- /src/assets/css/base.css: -------------------------------------------------------------------------------- 1 | body, 2 | div, 3 | p, 4 | span, 5 | a, 6 | img, 7 | dl, 8 | dt, 9 | dd, 10 | ul, 11 | ol, 12 | li, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | table, 20 | thead, 21 | tbody, 22 | tr, 23 | td, 24 | th { 25 | padding: 0; 26 | margin: 0; 27 | -webkit-touch-callout: none; /*系统默认菜单被禁用*/ 28 | -webkit-user-select: none; /*webkit浏览器*/ 29 | -khtml-user-select: none; /*早期浏览器*/ 30 | -moz-user-select: none; /*火狐*/ 31 | -ms-user-select: none; /*IE10*/ 32 | user-select: none; 33 | } 34 | ul, 35 | ol { 36 | list-style: none; 37 | } 38 | 39 | ::-webkit-scrollbar { 40 | width: 5px; /*高宽分别对应横竖滚动条的尺寸*/ 41 | height: 1px; 42 | right: 10px; 43 | } 44 | ::-webkit-scrollbar-thumb { 45 | /*滚动条里面小方块*/ 46 | border-radius: 5px; 47 | box-shadow: inset 0 0 5px #e1e3e6; 48 | background: #e1e3e6; 49 | } 50 | ::-webkit-scrollbar-track { 51 | /*滚动条里面轨道*/ 52 | box-shadow: inset 0 0 5px rgba(231, 229, 229, 0.2); 53 | border-radius: 3px; 54 | background: #ededed; 55 | } 56 | input { 57 | user-select: auto; 58 | -webkit-user-select: auto; /*webkit浏览器*/ 59 | } 60 | textarea { 61 | user-select: auto; 62 | -webkit-user-select: auto; /*webkit浏览器*/ 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/css/index.less: -------------------------------------------------------------------------------- 1 | @mainColor: #828cf6; -------------------------------------------------------------------------------- /src/assets/css/theme.less: -------------------------------------------------------------------------------- 1 | // 黑色主题 2 | .dark { 3 | --theme-color: #fff; 4 | --theme-bgColor: #36393f; 5 | --nav-color: #202225; 6 | --content-color: #2f3136; 7 | --icon-color: #f2f3f5; 8 | --icon-select-color: #828cf6; 9 | --login-bgColor: #2f3136; 10 | --select-userColor:hsla(217,calc(var(--saturation-factor, 1)*7.6%),33.5%,0.6); 11 | --size-color:hsl(216,calc(var(--saturation-factor, 1)*3.7%),73.5%); 12 | --search-color:#202225; 13 | --border-color:#2f3133; 14 | --title-color:hsl(0,calc(var(--saturation-factor, 1)*0%),100%); 15 | --user-timeColor:#777; 16 | --chat-timeColor:#eee; 17 | --cart-timeColor:#eee; 18 | --dialog-bgColor:#4c4e53; 19 | --input-bgColor:#141515; 20 | --select-inputColor:#fff; 21 | } 22 | // 亮色主题 23 | .light { 24 | --theme-color: #333; 25 | --theme-bgColor: #f6f6f7; 26 | --nav-color: #e3e5e8; 27 | --content-color: #f2f3f5; 28 | --icon-color: #2f3136; 29 | --icon-select-color: #515de2; 30 | --login-bgColor: #828cf6; 31 | --select-userColor:#cfcfcf; 32 | --size-color:#555; 33 | --search-color:#f9f9f9; 34 | --border-color:#e8eaec; 35 | --title-color:#202225; 36 | --user-timeColor:#777; 37 | --chat-timeColor:#eee; 38 | --dialog-bgColor:#fff; 39 | --input-bgColor:#eceef4; 40 | --select-inputColor:#fff; 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/images/github_login_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/github_login_icon.png -------------------------------------------------------------------------------- /src/assets/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/login.png -------------------------------------------------------------------------------- /src/assets/images/login1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/login1.png -------------------------------------------------------------------------------- /src/assets/images/login2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/login2.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IM-Tools/im-web-client/68962a3791ca6a429566ca7b1d788f55bc091c64/src/assets/images/none.png -------------------------------------------------------------------------------- /src/assets/svg/icons/add.svg: -------------------------------------------------------------------------------- 1 | 11 | 15 | -------------------------------------------------------------------------------- /src/assets/svg/icons/addUser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/icons/audio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/icons/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/icons/gitee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/icons/github.svg: -------------------------------------------------------------------------------- 1 | 11 | 15 | -------------------------------------------------------------------------------- /src/assets/svg/icons/smile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 24 | 33 | -------------------------------------------------------------------------------- /src/components/AddFriend.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 91 | 92 | 210 | -------------------------------------------------------------------------------- /src/components/AddGroup.vue: -------------------------------------------------------------------------------- 1 | 171 | 172 | 297 | 298 | 525 | -------------------------------------------------------------------------------- /src/components/LogoutDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 100 | -------------------------------------------------------------------------------- /src/components/MyAudio.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 50 | 51 | 180 | -------------------------------------------------------------------------------- /src/components/UserMsg.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 51 | 52 | 110 | -------------------------------------------------------------------------------- /src/components/Voice.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 105 | 106 | 228 | -------------------------------------------------------------------------------- /src/components/emoji/Emoji.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 65 | -------------------------------------------------------------------------------- /src/components/emoji/emoji.ts: -------------------------------------------------------------------------------- 1 | const appData = [ 2 | { 3 | "codes": "1F600", 4 | "char": "😀", 5 | "name": "grinning face" 6 | }, 7 | { 8 | "codes": "1F603", 9 | "char": "😃", 10 | "name": "grinning face with big eyes" 11 | }, 12 | { 13 | "codes": "1F604", 14 | "char": "😄", 15 | "name": "grinning face with smiling eyes" 16 | }, 17 | { 18 | "codes": "1F601", 19 | "char": "😁", 20 | "name": "beaming face with smiling eyes" 21 | }, 22 | { 23 | "codes": "1F606", 24 | "char": "😆", 25 | "name": "grinning squinting face" 26 | }, 27 | { 28 | "codes": "1F605", 29 | "char": "😅", 30 | "name": "grinning face with sweat" 31 | }, 32 | { 33 | "codes": "1F923", 34 | "char": "🤣", 35 | "name": "rolling on the floor laughing" 36 | }, 37 | { 38 | "codes": "1F602", 39 | "char": "😂", 40 | "name": "face with tears of joy" 41 | }, 42 | { 43 | "codes": "1F642", 44 | "char": "🙂", 45 | "name": "slightly smiling face" 46 | }, 47 | { 48 | "codes": "1F643", 49 | "char": "🙃", 50 | "name": "upside-down face" 51 | }, 52 | { 53 | "codes": "1F609", 54 | "char": "😉", 55 | "name": "winking face" 56 | }, 57 | { 58 | "codes": "1F60A", 59 | "char": "😊", 60 | "name": "smiling face with smiling eyes" 61 | }, 62 | { 63 | "codes": "1F607", 64 | "char": "😇", 65 | "name": "smiling face with halo" 66 | }, 67 | { 68 | "codes": "1F970", 69 | "char": "🥰", 70 | "name": "smiling face with hearts" 71 | }, 72 | { 73 | "codes": "1F60D", 74 | "char": "😍", 75 | "name": "smiling face with heart-eyes" 76 | }, 77 | { 78 | "codes": "1F929", 79 | "char": "🤩", 80 | "name": "star-struck" 81 | }, 82 | { 83 | "codes": "1F618", 84 | "char": "😘", 85 | "name": "face blowing a kiss" 86 | }, 87 | { 88 | "codes": "1F617", 89 | "char": "😗", 90 | "name": "kissing face" 91 | }, 92 | { 93 | "codes": "1F61A", 94 | "char": "😚", 95 | "name": "kissing face with closed eyes" 96 | }, 97 | { 98 | "codes": "1F619", 99 | "char": "😙", 100 | "name": "kissing face with smiling eyes" 101 | }, 102 | { 103 | "codes": "1F44B", 104 | "char": "👋", 105 | "name": "waving hand" 106 | }, 107 | { 108 | "codes": "1F91A", 109 | "char": "🤚", 110 | "name": "raised back of hand" 111 | }, 112 | { 113 | "codes": "1F590", 114 | "char": "🖐", 115 | "name": "hand with fingers splayed" 116 | }, 117 | { 118 | "codes": "270B", 119 | "char": "✋", 120 | "name": "raised hand" 121 | }, 122 | { 123 | "codes": "1F596", 124 | "char": "🖖", 125 | "name": "vulcan salute" 126 | }, 127 | { 128 | "codes": "1F44C", 129 | "char": "👌", 130 | "name": "OK hand" 131 | }, 132 | { 133 | "codes": "1F90F", 134 | "char": "🤏", 135 | "name": "pinching hand" 136 | }, 137 | { 138 | "codes": "270C", 139 | "char": "✌", 140 | "name": "victory hand" 141 | }, 142 | { 143 | "codes": "1F91E", 144 | "char": "🤞", 145 | "name": "crossed fingers" 146 | }, 147 | { 148 | "codes": "1F91F", 149 | "char": "🤟", 150 | "name": "love-you gesture" 151 | }, 152 | { 153 | "codes": "1F918", 154 | "char": "🤘", 155 | "name": "sign of the horns" 156 | }, 157 | { 158 | "codes": "1F919", 159 | "char": "🤙", 160 | "name": "call me hand" 161 | }, 162 | { 163 | "codes": "1F448", 164 | "char": "👈", 165 | "name": "backhand index pointing left" 166 | }, 167 | { 168 | "codes": "1F449", 169 | "char": "👉", 170 | "name": "backhand index pointing right" 171 | }, 172 | { 173 | "codes": "1F446", 174 | "char": "👆", 175 | "name": "backhand index pointing up" 176 | }, 177 | { 178 | "codes": "1F595", 179 | "char": "🖕", 180 | "name": "middle finger" 181 | }, 182 | { 183 | "codes": "1F447", 184 | "char": "👇", 185 | "name": "backhand index pointing down" 186 | }, 187 | { 188 | "codes": "261D FE0F", 189 | "char": "☝️", 190 | "name": "index pointing up" 191 | }, 192 | { 193 | "codes": "1F44D", 194 | "char": "👍", 195 | "name": "thumbs up" 196 | }, 197 | { 198 | "codes": "1F44E", 199 | "char": "👎", 200 | "name": "thumbs down" 201 | }, 202 | { 203 | "codes": "270A", 204 | "char": "✊", 205 | "name": "raised fist" 206 | }, 207 | { 208 | "codes": "1F44A", 209 | "char": "👊", 210 | "name": "oncoming fist" 211 | }, 212 | { 213 | "codes": "1F91B", 214 | "char": "🤛", 215 | "name": "left-facing fist" 216 | }, 217 | { 218 | "codes": "1F91C", 219 | "char": "🤜", 220 | "name": "right-facing fist" 221 | } 222 | 223 | ] 224 | 225 | export default appData; -------------------------------------------------------------------------------- /src/directive/customMenu/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import customMenu from './index.vue' 3 | 4 | export const CustomMenuBox = (props: any) => { 5 | // 获取组件 6 | const instance = createApp(customMenu, { ...props }) 7 | // 提供父元素 8 | const parent = document.createElement('div') 9 | instance.mount(parent) 10 | document.body.appendChild(parent) 11 | return { instance, parent } 12 | } 13 | -------------------------------------------------------------------------------- /src/directive/customMenu/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 74 | 97 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CustomMenuBox } from './customMenu/index' 3 | let instance: any = null 4 | export const customMenu = (app: any) => { 5 | app.directive('customMenu', { 6 | // 当被绑定的元素插入到 DOM 中时…… 7 | mounted(el: any, binding: any) { 8 | console.log('binding',binding); 9 | // 取消默认的浏览器自带右键菜单 10 | window.oncontextmenu = function (e) { 11 | e.preventDefault() 12 | } 13 | el.oncontextmenu = function (e: any) { 14 | // 防止重复显示 15 | if (instance) { 16 | document.body.removeChild(instance.parent) 17 | instance.instance.unmount() 18 | instance = null 19 | } 20 | const positionX = e.pageX 21 | const positionY = e.pageY 22 | // 判断显示置顶,还是取消置顶 23 | // if(binding.value.val.top_status === 0){ 24 | // binding.value.menuLists[0].name = '置顶' 25 | // } else{ 26 | // binding.value.menuLists[0].name = '取消置顶' 27 | // } 28 | instance = CustomMenuBox({ 29 | show: true, 30 | position: { positionX, positionY }, 31 | menuList: binding.value.menuLists, 32 | val: binding.value.val 33 | }) 34 | } 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/directive/type.ts: -------------------------------------------------------------------------------- 1 | export interface menuType { 2 | name: string 3 | method: Function 4 | } 5 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | declare module 'push.js'; -------------------------------------------------------------------------------- /src/hooks/usePoint.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | function getPoint(e: any) { 3 | let x = 0, y= 0 4 | if(e.pageX + 300 > window.innerWidth){ 5 | x = e.pageX - 300 6 | } else{ 7 | x = e.pageX 8 | } 9 | if(e.pageY + 180 > window.innerHeight){ 10 | y = e.pageX - 180 11 | } else{ 12 | y = e.pageY 13 | } 14 | const point = { 15 | x: x + 'px', 16 | y: y + 'px' 17 | } 18 | return point 19 | } 20 | return { 21 | getPoint 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { mainStore } from '@/store'; 2 | export default function useTheme(){ 3 | const store = mainStore() 4 | // 主题列表 5 | const themeList = store.themeList 6 | function changeThemeColor(theme: string){ 7 | store.setTheme(theme) 8 | } 9 | return{ 10 | themeList, 11 | changeThemeColor 12 | } 13 | } -------------------------------------------------------------------------------- /src/layout/components/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 58 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 81 | 82 | 169 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | // 引入pinia 3 | import { createPinia } from "pinia"; 4 | // 创建 Pinia 实例 5 | const pinia = createPinia(); 6 | // 导入路由 7 | import router from "./router"; 8 | // 引入 ElementPlus 9 | import ElementPlus from 'element-plus' 10 | import 'element-plus/dist/index.css' 11 | // 引入基本样式 12 | import 'normalize.css/normalize.css' 13 | import '@/assets/css/base.css' 14 | import '@/assets/css/theme.less' 15 | // svg引入 16 | import svgIcon from '@/assets/svg/index.vue' 17 | import 'virtual:svg-icons-register' 18 | // 指令 19 | import { customMenu } from '@/directive/index' 20 | // vue实例 21 | import App from './App.vue' 22 | const app = createApp(App) 23 | app.component('svg-icon', svgIcon) 24 | app.use(pinia) 25 | app.use(router) 26 | app.use(customMenu) 27 | app.use(ElementPlus) 28 | app.mount('#app') 29 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' 2 | import { getStorage } from '@/utils/storage' 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | redirect: '/session', 7 | }, 8 | { 9 | path: '/login', 10 | name: 'Login', 11 | component: () => import('../views/login/index.vue'), 12 | }, 13 | { 14 | path: '/about', 15 | name: 'About', 16 | component: () => import('../views/about/index.vue'), 17 | meta: { 18 | isNav: true, 19 | }, 20 | }, 21 | { 22 | path: '/home', 23 | name: 'Home', 24 | component: () => import('../views/home/index.vue'), 25 | meta: { 26 | isNav: true, 27 | }, 28 | }, 29 | { 30 | path: '/address', 31 | name: 'Address', 32 | component: () => import('../views/address/index.vue'), 33 | meta: { 34 | isNav: true, 35 | }, 36 | }, 37 | { 38 | path: '/session', 39 | name: 'Session', 40 | component: () => import('../views/session/index.vue'), 41 | meta: { 42 | isNav: true, 43 | }, 44 | }, 45 | ] 46 | 47 | const router = createRouter({ 48 | history: createWebHistory(), //process.env.BASE_URL 49 | routes, 50 | }) 51 | // 路由守卫 52 | router.beforeEach((to, from, next) => { 53 | const token = getStorage('token') 54 | if (to.path === '/login' && token) { 55 | next('/') 56 | return 57 | } 58 | if(to.path !== '/login' && !token){ 59 | next('/login') 60 | return 61 | } 62 | next() 63 | }) 64 | export default router 65 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getStorage, setStorage, removeStorage } from '@/utils/storage' 3 | import { sessionStore } from './session' 4 | export { sessionStore } 5 | import { userStore } from './user' 6 | export { userStore } 7 | import { closeWs } from '@/utils/socket' 8 | export const mainStore = defineStore('main', { 9 | state: () => { 10 | return { 11 | themeList: [ 12 | { 13 | name: '亮色主题', 14 | class: 'light', 15 | id: 1, 16 | }, 17 | { 18 | name: '暗色主题', 19 | class: 'dark', 20 | id: 1, 21 | }, 22 | ], 23 | themeSelect: getStorage('theme') || 'light', 24 | token: getStorage('token') || '', 25 | userInfo: getStorage('userInfo', 'object') || {}, 26 | pointType: getStorage('pointType', 'object') || { 27 | session: false, 28 | address: false, 29 | }, 30 | isLogout: false, 31 | logoutInfo: getStorage('logoutInfo'), 32 | isPermission: false, 33 | } 34 | }, 35 | actions: { 36 | setTheme(theme: string) { 37 | setStorage('theme', theme) 38 | this.themeSelect = theme 39 | }, 40 | changPoint(key: string, type: boolean) { 41 | this.pointType[key] = type 42 | setStorage('pointType', this.pointType) 43 | }, 44 | setToken(token: string) { 45 | setStorage('token', token) 46 | this.token = token 47 | }, 48 | async setUserInfo(data: Object) { 49 | setStorage('userInfo', data) 50 | this.userInfo = data 51 | }, 52 | setLogoutInfo(data: Object) { 53 | setStorage('logoutInfo', data) 54 | this.logoutInfo = data 55 | }, 56 | clearMessage(){ 57 | this.isLogout = false 58 | }, 59 | changePermission(permission: boolean) { 60 | this.isPermission = permission 61 | }, 62 | logOut(isLogout: boolean = false) { 63 | this.isLogout = isLogout 64 | closeWs() 65 | this.token = '' 66 | this.userInfo = {} 67 | removeStorage('token') 68 | removeStorage('userInfo') 69 | const sessionStores = sessionStore() 70 | sessionStores.init() 71 | const userStores = userStore() 72 | userStores.init() 73 | }, 74 | }, 75 | }) 76 | -------------------------------------------------------------------------------- /src/store/session.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { sessionList, removeSession, createSession } from '@/api/session' 3 | import { chatMessage, chatGroupMessage } from '@/api/chat' 4 | import { timestampChange } from '@/utils' 5 | import type { userType, sessionType, groupType } from '@/api/session/type' 6 | import type { chatRecordType, chatItemType } from '@/api/chat/type' 7 | import router from '@/router' 8 | import { getStorage, setStorage, removeStorage } from '@/utils/storage' 9 | import { getPointMsg } from '@/utils/session' 10 | import { mainStore } from './index' 11 | 12 | export const sessionStore = defineStore('sessionStore', { 13 | state: () => { 14 | const scrollType: boolean = true 15 | const sessionList: sessionType[] = 16 | getStorage('sessionList', 'object') || [] 17 | const querySessionList: sessionType[] = [] 18 | const selectSession: sessionType | null = 19 | getStorage('selectSession', 'object') || null 20 | const chattingRecords: chatRecordType | null = 21 | getStorage('chattingRecords', 'object') || null 22 | const total: number = 0 23 | const isShowMoreBtn: boolean = false 24 | return { 25 | sessionList, 26 | selectSession, 27 | chattingRecords, 28 | scrollType, 29 | total, 30 | isShowMoreBtn, 31 | page: 1, 32 | querySessionList, 33 | } 34 | }, 35 | actions: { 36 | // 是否滚动 37 | startScroll() { 38 | this.scrollType = !this.scrollType 39 | }, 40 | // 查询会话 41 | getQuerySessionList(search: string, clear?: boolean) { 42 | if (clear) { 43 | this.querySessionList = [] 44 | return 45 | } 46 | this.querySessionList = this.sessionList.filter((item) => { 47 | return ( 48 | item.name.indexOf(search) !== -1 || 49 | (item.Users && item.Users.email.indexOf(search) !== -1) 50 | ) 51 | }) 52 | }, 53 | // 获取会话列表 54 | async setSessionList() { 55 | if (this.sessionList[0]) { 56 | return 57 | } 58 | const res = await sessionList() 59 | setStorage('sessionList', res) 60 | if (!Array.isArray(res)) { 61 | return 62 | } 63 | this.sessionList = res.map((item) => { 64 | const time = new Date() 65 | const option = { 66 | content: '开始聊天', 67 | time: timestampChange(time, 'HH:mm'), 68 | } 69 | item.last_message = option 70 | return item 71 | }) 72 | if (!this.selectSession) { 73 | this.selectSession = this.sessionList[0] 74 | setStorage('selectSession', this.selectSession) 75 | this.setChattingRecords(this.selectSession) 76 | } 77 | }, 78 | // 改变会话列表 79 | changeSessionList(session: sessionType, type: string) { 80 | if (type === 'add') { 81 | let isAdd: boolean = false 82 | this.sessionList.forEach((item) => { 83 | if (item.id === session.id) { 84 | isAdd = true 85 | } 86 | }) 87 | if (!isAdd) { 88 | this.sessionList.push(session) 89 | setStorage('sessionList', this.sessionList) 90 | } 91 | // 改变选中的会话 92 | this.selectSession = session 93 | this.setChattingRecords(session) 94 | setStorage('selectSession', session) 95 | router.push('/session') 96 | } 97 | if (type === 'delete') { 98 | const idx: number = this.sessionList.findIndex((item) => { 99 | return item.id === session.id 100 | }) 101 | // 移除对应会话 102 | if (idx >= 0) { 103 | removeSession(session.id).then(() => { 104 | this.sessionList.splice(idx, 1) 105 | setStorage('sessionList', this.sessionList) 106 | // 判断是否是选中的会话 107 | console.log('判断是否是选中的会话', this.selectSession, session.id) 108 | 109 | if (this.selectSession && this.selectSession.id === session.id) { 110 | if (this.sessionList[0]) { 111 | this.selectSession = this.sessionList[0] 112 | setStorage('selectSession', this.selectSession) 113 | return this.setChattingRecords(this.selectSession) 114 | } 115 | this.selectSession = null 116 | setStorage('selectSession', '') 117 | // 删除对应聊天记录 118 | this.chattingRecords = null 119 | setStorage('chattingRecords', '') 120 | return new Promise((resolve) => { 121 | resolve(true) 122 | }) 123 | } 124 | }) 125 | } 126 | } 127 | if (type === 'agree') { 128 | let isAdd: boolean = false 129 | this.sessionList.forEach((item) => { 130 | if (item.id === session.id) { 131 | isAdd = true 132 | } 133 | }) 134 | if (!isAdd) { 135 | this.sessionList.push(session) 136 | setStorage('sessionList', this.sessionList) 137 | } 138 | } 139 | return new Promise((resolve) => { 140 | resolve(true) 141 | }) 142 | }, 143 | // 改变私聊提示消息 144 | async changeSessionPoint(message: chatItemType) { 145 | const idx: number = this.sessionList.findIndex((item) => { 146 | const userStore = mainStore() 147 | const res = 148 | item.to_id === message.to_id && item.form_id === userStore.userInfo.id 149 | return ( 150 | res || 151 | (item.to_id === message.form_id && item.form_id === message.to_id) 152 | ) 153 | }) 154 | // 有会话记录 155 | if (idx >= 0) { 156 | const time = new Date(message.created_at) 157 | if ( 158 | this.sessionList[idx].to_id === message.form_id && 159 | this.sessionList[idx].form_id === message.to_id 160 | ) { 161 | // 判断是不是选中的会话 162 | if ( 163 | !this.selectSession || 164 | this.sessionList[idx].id !== this.selectSession.id 165 | ) { 166 | const isPoint = this.sessionList[idx].last_message ? this.sessionList[idx].last_message.isPoint: undefined 167 | const num = this.sessionList[idx].last_message ? this.sessionList[idx].last_message.num: undefined 168 | let number = 1 169 | if (isPoint && num) { 170 | number = num + 1 171 | } 172 | const lastMessage = { 173 | content: getPointMsg(message.msg_type, message.msg), 174 | time: timestampChange(time, 'HH:mm'), 175 | isPoint: true, 176 | num: number, 177 | } 178 | this.sessionList[idx].last_message = lastMessage 179 | setStorage('sessionList', this.sessionList) 180 | return 181 | } 182 | } 183 | const lastMessage = { 184 | content: getPointMsg(message.msg_type, message.msg), 185 | time: timestampChange(time, 'HH:mm'), 186 | } 187 | this.sessionList[idx].last_message = lastMessage 188 | setStorage('sessionList', this.sessionList) 189 | } else { 190 | // 没有会话记录则创建会话 191 | const result = await createSession({ 192 | id: message.form_id, 193 | type: 1, 194 | }) 195 | const time = new Date(message.created_at) 196 | const session = Object.assign(result, { 197 | last_message: { 198 | content: getPointMsg(message.msg_type, message.msg), 199 | time: timestampChange(time, 'HH:mm'), 200 | isPoint: true, 201 | num: 1, 202 | }, 203 | }) 204 | this.changeSessionList(session, 'add') 205 | } 206 | }, 207 | // 改变群聊提示消息 208 | async changeGroupPoint(message: chatItemType){ 209 | const idx: number = this.sessionList.findIndex( item => { 210 | return item.group_id === message.to_id 211 | }) 212 | if(idx >= 0){ 213 | const time = new Date(message.created_at) 214 | // 判断是不是选中的会话 215 | if ( 216 | !this.selectSession || 217 | this.sessionList[idx].id !== this.selectSession.id 218 | ){ 219 | const isPoint = this.sessionList[idx].last_message ? this.sessionList[idx].last_message.isPoint: undefined 220 | const num = this.sessionList[idx].last_message ? this.sessionList[idx].last_message.num: undefined 221 | let number = 1 222 | if (isPoint && num) { 223 | number = num + 1 224 | } 225 | const lastMessage = { 226 | content: getPointMsg(message.msg_type, message.msg), 227 | time: timestampChange(time, 'HH:mm'), 228 | isPoint: true, 229 | num: number, 230 | } 231 | this.sessionList[idx].last_message = lastMessage 232 | setStorage('sessionList', this.sessionList) 233 | return 234 | } 235 | const lastMessage = { 236 | content: getPointMsg(message.msg_type, message.msg), 237 | time: timestampChange(time, 'HH:mm'), 238 | } 239 | this.sessionList[idx].last_message = lastMessage 240 | setStorage('sessionList', this.sessionList) 241 | } 242 | }, 243 | // 设置选中会话 244 | setSelectSession(session: sessionType) { 245 | this.selectSession = session 246 | setStorage('selectSession', this.selectSession) 247 | // 清除选中会话提示 248 | const idx: number = this.sessionList.findIndex((item) => { 249 | return item.id === session.id 250 | }) 251 | const times = new Date() 252 | const { content, time } = this.sessionList[idx].last_message || { 253 | content: '开始聊天', 254 | time: timestampChange(times, 'HH:mm'), 255 | } 256 | const lastMessage = { 257 | content: content, 258 | time: time, 259 | isPoint: false, 260 | num: 0, 261 | } 262 | this.sessionList[idx].last_message = lastMessage 263 | setStorage('sessionList', this.sessionList) 264 | }, 265 | // 获取聊天记录 266 | async setChattingRecords(session: sessionType) { 267 | const data = { 268 | page: '', 269 | pageSize: 20, 270 | to_id: session.to_id, 271 | } 272 | if(session.channel_type === 1){ 273 | const message = await chatMessage(data) 274 | this.total = message.mate.total 275 | if (this.total <= this.page * 20) { 276 | this.isShowMoreBtn = false 277 | } else { 278 | this.isShowMoreBtn = true 279 | } 280 | const isArray = message.list instanceof Array 281 | const chatRecord = { 282 | list: isArray ? message.list : [], 283 | mate: message.mate || {}, 284 | id: session.to_id, 285 | from_id: session.form_id, 286 | } 287 | this.chattingRecords = chatRecord 288 | setStorage('chattingRecords', this.chattingRecords) 289 | }else{ 290 | data.to_id = session.group_id || 0 291 | const message = await chatGroupMessage(data) 292 | this.total = message.mate.total 293 | if (this.total <= this.page * 20) { 294 | this.isShowMoreBtn = false 295 | } else { 296 | this.isShowMoreBtn = true 297 | } 298 | const isArray = message.list instanceof Array 299 | let list:chatItemType[] = [] 300 | if(isArray){ 301 | 302 | list = message.list.map( item => { 303 | const time = new Date(item.SendTime*1000) 304 | const option = { 305 | Users: item.Users, 306 | channel_type: 2, 307 | created_at: timestampChange(time), 308 | data: '', 309 | form_id: item.FormId, 310 | id: item.Id, 311 | is_read: 0, 312 | msg: item.Message, 313 | msg_type: item.MsgType, 314 | to_id: item.GroupId, 315 | status: 1, 316 | } 317 | return option 318 | }) 319 | } 320 | const chatRecord = { 321 | list: isArray ? list : [], 322 | mate: message.mate || {}, 323 | id: session.to_id, 324 | from_id: session.form_id, 325 | } 326 | this.chattingRecords = chatRecord 327 | setStorage('chattingRecords', this.chattingRecords) 328 | } 329 | return true 330 | }, 331 | // 更改聊天记录/更多聊天记录 332 | async moreRecord( 333 | session: sessionType, 334 | chatId: number 335 | ) { 336 | this.page++ 337 | const message = await chatMessage({ 338 | page: chatId, 339 | pageSize: 20, 340 | to_id: session.to_id, 341 | }) 342 | if (this.total <= this.page * 20) { 343 | this.isShowMoreBtn = false 344 | } else { 345 | this.isShowMoreBtn = true 346 | } 347 | const isArray = message.list instanceof Array 348 | const chatRecord = { 349 | list: 350 | isArray && this.chattingRecords 351 | ? message.list.concat(this.chattingRecords.list) 352 | : !isArray && this.chattingRecords 353 | ? this.chattingRecords.list 354 | : [], 355 | mate: message.mate || {}, 356 | id: session.to_id, 357 | from_id: session.form_id, 358 | } 359 | this.chattingRecords = chatRecord 360 | setStorage('chattingRecords', this.chattingRecords) 361 | return true 362 | }, 363 | // 发送和接收聊天记录 364 | changeChattingRecords(message: chatItemType) { 365 | if (this.selectSession && this.chattingRecords) { 366 | const userStore = mainStore() 367 | if(message.channel_type === 1){ 368 | // 接收消息 369 | if ( 370 | message.form_id === this.selectSession.to_id && 371 | message.to_id === this.selectSession.form_id 372 | ) { 373 | this.chattingRecords.list.push(message) 374 | setStorage('chattingRecords', this.chattingRecords) 375 | } 376 | // 发送消息 377 | if ( 378 | message.form_id === userStore.userInfo.id && 379 | message.to_id === this.selectSession.to_id 380 | ) { 381 | this.chattingRecords.list.push(message) 382 | setStorage('chattingRecords', this.chattingRecords) 383 | } 384 | this.changeSessionPoint(message) 385 | } 386 | if(message.channel_type === 2){ 387 | // 发送消息 388 | if(message.to_id === this.selectSession.group_id){ 389 | this.chattingRecords.list.push(message) 390 | setStorage('chattingRecords', this.chattingRecords) 391 | } 392 | // 接收用户加入消息 393 | if(message.form_id === this.selectSession.group_id && message.msg_type === 6){ 394 | this.chattingRecords.list.push(message) 395 | setStorage('chattingRecords', this.chattingRecords) 396 | } 397 | this.changeGroupPoint(message) 398 | } 399 | } 400 | 401 | return new Promise((resolve) => { 402 | resolve(true) 403 | }) 404 | }, 405 | // 判断是否存在会话 存在则移除 406 | checkSession(id: number) { 407 | const session = this.sessionList.filter((item) => { 408 | return item.to_id === id 409 | }) 410 | if (!session[0]) { 411 | return 412 | } 413 | // 移除对应会话 414 | this.changeSessionList(session[0], 'delete') 415 | }, 416 | init() { 417 | this.sessionList = [] 418 | this.selectSession = null 419 | this.chattingRecords = null 420 | removeStorage('sessionList') 421 | removeStorage('selectSession') 422 | removeStorage('chattingRecords') 423 | }, 424 | }, 425 | getters: { 426 | // 判断是否展示聊天时间 427 | chattingRecordsList(state) { 428 | if (!state.chattingRecords) { 429 | return state.chattingRecords 430 | } 431 | const recordsList = JSON.parse(JSON.stringify(state.chattingRecords)) 432 | recordsList.list = [] 433 | if (!state.chattingRecords.list[0]) { 434 | return [] 435 | } 436 | let timesStr = state.chattingRecords.list[0].created_at 437 | let time = new Date(timesStr).getTime() 438 | state.chattingRecords.list.forEach((item, index) => { 439 | if (index === 0) { 440 | item.isShowTime = true 441 | } 442 | const timeMs = new Date(item.created_at).getTime() 443 | if (timeMs - time > 120 * 1000) { 444 | item.isShowTime = true 445 | } 446 | recordsList.list.push(item) 447 | time = new Date(item.created_at).getTime() 448 | }) 449 | return recordsList 450 | }, 451 | }, 452 | }) 453 | -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { friendList, deleteFriend } from '@/api/friend' 3 | import { getStorage, setStorage, removeStorage } from '@/utils/storage' 4 | import type { friendType, userType } from '@/api/friend/type' 5 | import { sessionStore } from './session' 6 | export const userStore = defineStore('user', { 7 | state: () => { 8 | const userList: friendType[] = 9 | getStorage('userList', 'object') || [] 10 | const queryUserList: friendType[] = [] 11 | const selectUser: friendType | null = 12 | getStorage('selectUser', 'object') || {} 13 | const selectName: string = getStorage('selectName') || 'newFriend' 14 | return { 15 | userList, 16 | selectUser, 17 | selectName, 18 | queryUserList, 19 | } 20 | }, 21 | actions: { 22 | getQueryUserList(search: string, clear?: boolean) { 23 | if (clear) { 24 | this.queryUserList = [] 25 | return 26 | } 27 | this.queryUserList = this.userList.filter((item) => { 28 | return ( 29 | item.Users.name.indexOf(search) !== -1 || 30 | (item.Users.email && item.Users.email.indexOf(search) !== -1) 31 | ) 32 | }) 33 | }, 34 | setUserList() { 35 | if (this.userList[0]) { 36 | return new Promise((resolve) => { 37 | resolve(true) 38 | }) 39 | } 40 | friendList().then((res) => { 41 | this.userList = res 42 | setStorage('userList', res) 43 | return new Promise((resolve) => { 44 | resolve(true) 45 | }) 46 | }) 47 | return new Promise((resolve) => { 48 | resolve(true) 49 | }) 50 | }, 51 | changeUserList(user: friendType, type: string = 'add') { 52 | if (type === 'add') { 53 | let isAdd: boolean = false 54 | this.userList.forEach((item) => { 55 | if (item.id === user.id) { 56 | isAdd = true 57 | } 58 | }) 59 | if (!isAdd) { 60 | this.userList.push(user) 61 | setStorage('userList', this.userList) 62 | } 63 | } 64 | if (type === 'delete') { 65 | const idx: number = this.userList.findIndex((item) => { 66 | return item.id === user.id 67 | }) 68 | if (idx >= 0) { 69 | this.userList.splice(idx, 1) 70 | setStorage('userList', this.userList) 71 | } 72 | if (this.selectUser && user.id === this.selectUser.id) { 73 | this.selectUser = null 74 | setStorage('selectUser', '') 75 | } 76 | deleteFriend(user.Users.id).then(() => { 77 | console.log('删除成功') 78 | const otherStore = sessionStore() 79 | otherStore.checkSession(user.Users.id) 80 | }) 81 | } 82 | }, 83 | setSelectUser(user: friendType) { 84 | this.selectUser = user 85 | setStorage('selectUser', user) 86 | }, 87 | setSelectName(name: string) { 88 | this.selectName = name 89 | setStorage('selectName', name) 90 | }, 91 | init() { 92 | this.userList = [] 93 | this.selectUser = null 94 | removeStorage('userList') 95 | removeStorage('selectUser') 96 | }, 97 | }, 98 | }) 99 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // 判断邮箱 2 | export function checkEmail(str: String): Boolean { 3 | const string = str.replace(/\s| /g, '') //先去除用户输入的无效字符 4 | const reg = /^[a-zA-Z0-9_-]+@([a-zA-Z0-9]+\.)+(com|cn|net|org)$/; 5 | if (reg.test(string)) { 6 | return true; 7 | } else { 8 | return false; 9 | } 10 | } 11 | // 获取年月日时分秒 周 时间 12 | export function timestampChange(timestamp: any, format?: string): string { 13 | let dateStr = format || 'yyyy-MM-dd HH:mm:ss' 14 | // 获取完整年份(4位) 15 | const year = timestamp.getFullYear() 16 | // 获取当前年份(2位) 17 | const yearTwo = timestamp.getYear() 18 | // 获取当前月份(0-11,0代表1月) 19 | const month = timestamp.getMonth() + 1 20 | // 获取当前日(1-31) 21 | const day = timestamp.getDate() 22 | // 获取当前星期X(0-6,0代表星期天) 23 | let week = timestamp.getDay() 24 | // 获取当前小时数(0-23) 25 | const hours = timestamp.getHours() 26 | // 获取当前分钟数(0-59) 27 | const minutes = timestamp.getMinutes() 28 | // 获取当前秒数(0-59) 29 | const seconds = timestamp.getSeconds() 30 | // 获取当前毫秒数(0-999) 31 | const milliseconds = timestamp.getMilliseconds() 32 | switch (week) { 33 | case 0: 34 | week = '星期天' 35 | break 36 | case 1: 37 | week = '星期一' 38 | break 39 | case 2: 40 | week = '星期二' 41 | break 42 | case 3: 43 | week = '星期三' 44 | break 45 | case 4: 46 | week = '星期四' 47 | break 48 | case 5: 49 | week = '星期五' 50 | break 51 | case 6: 52 | week = '星期六' 53 | } 54 | // 年 55 | dateStr = dateStr.replace(/yyyy|YYYY/, year) 56 | dateStr = dateStr.replace(/yy{2}|YY{2}/, yearTwo) 57 | // 月 58 | dateStr = dateStr.replace(/MM/, month < 10 ? '0' + month : month) 59 | dateStr = dateStr.replace(/M{1}/, month) 60 | // 日 61 | dateStr = dateStr.replace(/DD|dd/, day < 10 ? '0' + day : day) 62 | dateStr = dateStr.replace(/D{1}|d{1}/, day) 63 | // 时 64 | dateStr = dateStr.replace(/HH|hh/, hours < 10 ? '0' + hours : hours) 65 | dateStr = dateStr.replace(/H{1}|h{1}/, hours) 66 | // 分 67 | dateStr = dateStr.replace(/mm/, minutes < 10 ? '0' + minutes : minutes) 68 | dateStr = dateStr.replace(/m{1}/, minutes) 69 | // 秒 70 | dateStr = dateStr.replace(/SS|ss/, seconds < 10 ? '0' + seconds : seconds) 71 | dateStr = dateStr.replace(/S{1}|s{1}/, seconds) 72 | // 毫秒 73 | dateStr = dateStr.replace(/MS|ms/, milliseconds) 74 | // 周 75 | dateStr = dateStr.replace(/W|w|week/, week) 76 | return dateStr 77 | } 78 | // 画头像 79 | export function drawAvatar(imgList: string[], callBack: Function){ 80 | const canvas = document.createElement("canvas") 81 | const context = canvas.getContext("2d") 82 | const sub = imgList.length 83 | let step = 3 84 | if(sub <= 4){ 85 | canvas.width = 100 86 | canvas.height = 100 87 | step = 2 88 | } else{ 89 | canvas.width = 150 90 | canvas.height = 150 91 | } 92 | let num = 0 93 | imgList.forEach( (imgUrl,index) => { 94 | const img = new Image() 95 | img.src = imgUrl 96 | img.width = 50 97 | img.height = 50 98 | img.setAttribute('crossOrigin', 'anonymous') 99 | img.onload = () => { 100 | num++ 101 | context && context.drawImage(img, 50*(index%step), 50*(parseInt(index/step + '')), 50, 50) 102 | if(num === sub){ 103 | const url = canvas.toDataURL('image/png') 104 | // callBack(url) 105 | canvas.toBlob((blob) => { 106 | callBack(url, blob) 107 | }) 108 | } 109 | } 110 | }) 111 | } -------------------------------------------------------------------------------- /src/utils/session.ts: -------------------------------------------------------------------------------- 1 | // 获取文件类型 2 | export function getFileType(url: string): Array { 3 | const type = url.substring(url.lastIndexOf('.') + 1) 4 | const imgType = ['gif', 'jpg', 'jpeg', 'png', 'svg', 'apng', 'webp'] 5 | if (imgType.indexOf(type) !== -1) { 6 | // 图片 7 | return ['image', type] 8 | } else { 9 | // 其他文件 10 | return ['other', type] 11 | } 12 | } 13 | // 获取会话列表提示消息 14 | export function getPointMsg(msgType: number = 1, message: string): string { 15 | if (msgType === 1) { 16 | return message 17 | } 18 | if (msgType === 3) { 19 | return getFileType(message)[0] === 'image' ? '图片...' : '文件...' 20 | } 21 | if (msgType === 5) { 22 | return '语音...' 23 | } 24 | return '' 25 | } -------------------------------------------------------------------------------- /src/utils/socket.ts: -------------------------------------------------------------------------------- 1 | import { useWebSocket } from '@vueuse/core' 2 | import { sessionStore, userStore, mainStore } from '@/store' 3 | // import { computed } from '@vue/reactivity' 4 | import { timestampChange } from '@/utils' 5 | import { getFriendDetails } from '@/api/friend' 6 | import { getStorage } from '@/utils/storage' 7 | import { useRoute, useRouter } from 'vue-router' 8 | import Push from 'push.js' 9 | interface socketOptions { 10 | close: Function 11 | ws: object 12 | send: Function 13 | } 14 | let wsObj: socketOptions | null = null 15 | function closeWs() { 16 | const route = useRoute() 17 | wsObj && wsObj.close() 18 | wsObj = null 19 | } 20 | function initWebsocket(openBack: Function, closeBack: Function) { 21 | if (wsObj) { 22 | return 23 | } 24 | const store = sessionStore() 25 | 26 | const usersStore = userStore() 27 | const mainStores = mainStore() 28 | const route = useRoute() 29 | const router = useRouter() 30 | const { status, data, send, open, close } = useWebSocket( 31 | import.meta.env.VITE_APP_WS_API + getStorage('token'), 32 | { 33 | autoReconnect: { 34 | retries: 10, 35 | delay: 1500, 36 | onFailed() { 37 | console.log('websocket重连失败') 38 | closeBack && closeBack() 39 | wsObj = null 40 | }, 41 | }, 42 | onConnected(ws) { 43 | // 心跳 44 | setInterval(() => { 45 | send('{"msg_code": 1004,"message":"ping"}') 46 | }, 10000) 47 | wsObj = { ws, close, send } 48 | openBack && openBack() 49 | }, 50 | async onMessage(ws, event) { 51 | if (!event.data) { 52 | return 53 | } 54 | console.log(event.data) 55 | 56 | const message = JSON.parse(event.data) 57 | switch (message.msg_code) { 58 | case 200: 59 | console.log('聊天消息', message, route.path) 60 | if (route.path !== '/session') { 61 | mainStores.changPoint('session', true) 62 | } 63 | const time = new Date() 64 | let Users = { 65 | avatar: '', 66 | email: '', 67 | id: -1, 68 | name: '', 69 | } 70 | if (message.channel_type === 1) { 71 | const res = await getFriendDetails(message.form_id) 72 | Users = { 73 | avatar: res.avatar, 74 | email: res.email, 75 | id: res.id, 76 | name: res.name, 77 | } 78 | // console.log('psuh') 79 | mainStores.isPermission && Push.create(res.name + '给你发来一条消息', { 80 | body: message.message, 81 | requireInteraction: false, 82 | icon: res.avatar, 83 | timeout: 600000, 84 | }) 85 | } else { 86 | const users = JSON.parse(message.data) 87 | Users = { 88 | avatar: users.avatar, 89 | email: users.email, 90 | id: users.id, 91 | name: users.name, 92 | } 93 | mainStores.isPermission && Push.create(users.name + '给你发来一条消息', { 94 | body: message.message, 95 | requireInteraction: false, 96 | icon: users.avatar, 97 | timeout: 600000, 98 | }) 99 | } 100 | const chatMsg = { 101 | Users: Users, 102 | created_at: timestampChange(time), 103 | data: message.data, 104 | form_id: message.form_id, 105 | id: time.getTime() + 1, 106 | is_read: 0, 107 | msg: message.message, 108 | msg_type: message.msg_type, 109 | to_id: message.to_id, 110 | channel_type: message.channel_type, 111 | status: 1, 112 | } 113 | // 聊天记录 114 | const result = store.changeChattingRecords(chatMsg) 115 | result.then(() => { 116 | store.startScroll() 117 | }) 118 | break 119 | case 1000: 120 | console.log('添加好友请求', message, route) 121 | if (route.path !== '/address') { 122 | mainStores.changPoint('address', true) 123 | } 124 | break 125 | case 1001: 126 | console.log('同意好友请求', message) 127 | if (route.path !== '/session') { 128 | mainStores.changPoint('session', true) 129 | } 130 | const times = new Date() 131 | const list = { 132 | id: message.id, 133 | name: message.users.name, 134 | note: '', 135 | created_at: message.created_at, 136 | avatar: message.users.avatar, 137 | top_time: '', 138 | status: message.status, 139 | to_id: message.to_id, 140 | form_id: message.form_id, 141 | top_status: 0, 142 | channel_type: 1, 143 | Users: { 144 | age: 0, 145 | avatar: message.users.avatar, 146 | bio: '', 147 | client_type: 1, 148 | email: '', 149 | id: message.users.id, 150 | last_login_time: '', 151 | name: message.users.name, 152 | sex: 0, 153 | status: message.status, 154 | }, 155 | last_message: { 156 | content: '添加好友成功', 157 | time: timestampChange(times, 'HH:mm'), 158 | isPoint: true, 159 | num: 1, 160 | }, 161 | } 162 | store.changeSessionList(list, 'agree') 163 | const user = { 164 | created_at: message.created_at, 165 | form_id: message.form_id, 166 | id: message.form_id, 167 | note: message.note, 168 | to_id: message.to_id, 169 | status: message.status, 170 | top_time: message.top_time, 171 | uid: '', 172 | update_at: '', 173 | Users: { 174 | avatar: message.users.avatar, 175 | id: message.users.id, 176 | name: message.users.name, 177 | }, 178 | } 179 | usersStore.changeUserList(user, 'add') 180 | break 181 | case 1002: 182 | console.log('拒绝好友请求', message) 183 | break 184 | case 1003: 185 | console.log('非好友关系', message) 186 | break 187 | case 2000: 188 | console.log('用户离线', message) 189 | break 190 | case 2001: 191 | console.log('用户上线', message) 192 | break 193 | case 2003: 194 | console.log('群聊通知', message) 195 | if (route.path !== '/session') { 196 | mainStores.changPoint('session', true) 197 | } 198 | const timeNow = new Date() 199 | const session = { 200 | id: message.sessions.id, 201 | name: message.sessions.name, 202 | note: message.sessions.note, 203 | created_at: message.sessions.created_at, 204 | avatar: message.sessions.avatar, 205 | top_time: message.sessions.top_time, 206 | status: message.sessions.status, 207 | to_id: message.sessions.to_id, 208 | form_id: message.sessions.form_id, 209 | group_id: message.sessions.group_id, 210 | top_status: message.sessions.top_status, 211 | channel_type: 2, 212 | Groups: message.sessions.Groups, 213 | last_message: { 214 | content: '添加好友成功', 215 | time: timestampChange(timeNow, 'HH:mm'), 216 | isPoint: true, 217 | num: 1, 218 | }, 219 | } 220 | store.changeSessionList(session, 'add') 221 | break 222 | case 2004: 223 | // console.log('其他地方登录',message); 224 | mainStores.setLogoutInfo(message) 225 | mainStores.logOut(true) 226 | router.push({ path: '/login' }) 227 | break 228 | } 229 | }, 230 | } 231 | ) 232 | } 233 | 234 | export { initWebsocket, closeWs } 235 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export function getStorage(key: string, type: string = 'string') { 2 | const str = sessionStorage.getItem(key) 3 | if (type === 'string') { 4 | return str 5 | } else { 6 | return str ? JSON.parse(str) : str 7 | } 8 | } 9 | 10 | export function setStorage(key: string, value: string | Object) { 11 | if (typeof value === 'string') { 12 | sessionStorage.setItem(key, value) 13 | } else { 14 | sessionStorage.setItem(key, JSON.stringify(value)) 15 | } 16 | } 17 | 18 | export function removeStorage(key: string) { 19 | sessionStorage.removeItem(key) 20 | } 21 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/address/index.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 283 | 284 | 587 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 38 | 39 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 244 | 245 | 415 | 416 | 617 | -------------------------------------------------------------------------------- /src/views/session/index.vue: -------------------------------------------------------------------------------- 1 | 357 | 358 | 690 | 691 | 1324 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true, 15 | "paths": { 16 | "@/*":["./src/*"], 17 | "@/api/*": ["./src/api/*"], 18 | "@/store/*": ["./src/store/*"], 19 | "@/pinia/*": ["./src/pinia/*"], 20 | } 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import { resolve } from 'path' 3 | import vue from '@vitejs/plugin-vue' 4 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(({ mode }) => { 7 | const env = loadEnv(mode, __dirname) 8 | return { 9 | plugins: [ 10 | vue(), 11 | createSvgIconsPlugin({ 12 | // 指定要缓存的文件夹 13 | iconDirs: [resolve(process.cwd(), 'src/assets/svg/icons')], 14 | // 指定symbolId格式 15 | symbolId: 'icon-[name]', 16 | }), 17 | ], 18 | resolve: { 19 | alias: { 20 | '@': resolve(__dirname, './src'), 21 | }, 22 | }, 23 | server: { 24 | port: 3000, 25 | open: false, 26 | https: false, 27 | cors: true, 28 | proxy: { 29 | '/api': { 30 | secure: false, 31 | // ws: true, 32 | target: env.VITE_APP_BASE_API, 33 | changeOrigin: true, 34 | rewrite: (path) => path.replace(/^\/api/, ''), 35 | }, 36 | }, 37 | }, 38 | build: { 39 | chunkSizeWarningLimit: 1500, 40 | rollupOptions: { 41 | output: { 42 | manualChunks(id) { 43 | if (id.includes('node_modules')) { 44 | return id 45 | .toString() 46 | .split('node_modules/')[1] 47 | .split('/')[0] 48 | .toString() 49 | } 50 | }, 51 | chunkFileNames: (chunkInfo) => { 52 | const facadeModuleId = chunkInfo.facadeModuleId 53 | ? chunkInfo.facadeModuleId.split('/') 54 | : [] 55 | const fileName = 56 | facadeModuleId[facadeModuleId.length - 2] || '[name]' 57 | return `js/${fileName}/[name].[hash].js` 58 | }, 59 | }, 60 | }, 61 | }, 62 | } 63 | }) 64 | --------------------------------------------------------------------------------