├── src
├── assets
│ ├── css
│ │ ├── index.less
│ │ ├── base.css
│ │ └── theme.less
│ ├── images
│ │ ├── login.png
│ │ ├── logo.png
│ │ ├── none.png
│ │ ├── login1.png
│ │ ├── login2.png
│ │ └── github_login_icon.png
│ └── svg
│ │ ├── index.vue
│ │ └── icons
│ │ ├── file.svg
│ │ ├── gitee.svg
│ │ ├── audio.svg
│ │ ├── smile.svg
│ │ ├── addUser.svg
│ │ ├── add.svg
│ │ └── github.svg
├── directive
│ ├── type.ts
│ ├── customMenu
│ │ ├── index.ts
│ │ └── index.vue
│ └── index.ts
├── api
│ ├── request.ts
│ ├── login
│ │ ├── type.ts
│ │ └── index.ts
│ ├── session
│ │ ├── index.ts
│ │ └── type.ts
│ ├── chat
│ │ ├── index.ts
│ │ └── type.ts
│ ├── group
│ │ ├── index.ts
│ │ └── type.ts
│ ├── friend
│ │ ├── index.ts
│ │ └── type.ts
│ └── http
│ │ └── index.ts
├── views
│ ├── about
│ │ └── index.vue
│ ├── home
│ │ └── index.vue
│ ├── address
│ │ └── index.vue
│ ├── login
│ │ └── index.vue
│ └── session
│ │ └── index.vue
├── hooks
│ ├── useTheme.ts
│ └── usePoint.ts
├── env.d.ts
├── utils
│ ├── storage.ts
│ ├── session.ts
│ ├── index.ts
│ └── socket.ts
├── main.ts
├── layout
│ ├── components
│ │ └── UserInfo.vue
│ └── index.vue
├── App.vue
├── router
│ └── index.ts
├── components
│ ├── emoji
│ │ ├── Emoji.vue
│ │ └── emoji.ts
│ ├── LogoutDialog.vue
│ ├── UserMsg.vue
│ ├── MyAudio.vue
│ ├── AddFriend.vue
│ ├── Voice.vue
│ └── AddGroup.vue
└── store
│ ├── index.ts
│ ├── user.ts
│ └── session.ts
├── .vscode
└── extensions.json
├── public
└── favicon.ico
├── tsconfig.node.json
├── .gitignore
├── index.html
├── .env.production
├── .env.development
├── tsconfig.json
├── package.json
├── README.md
├── vite.config.ts
└── demo.html
/src/assets/css/index.less:
--------------------------------------------------------------------------------
1 | @mainColor: #828cf6;
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/directive/type.ts:
--------------------------------------------------------------------------------
1 | export interface menuType {
2 | name: string
3 | method: Function
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/images/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/login.png
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/images/none.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/none.png
--------------------------------------------------------------------------------
/src/assets/images/login1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/login1.png
--------------------------------------------------------------------------------
/src/assets/images/login2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/login2.png
--------------------------------------------------------------------------------
/src/assets/images/github_login_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IM-Tools/im-web-client/HEAD/src/assets/images/github_login_icon.png
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/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/views/about/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {{ msg }}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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/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';
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Go Chat
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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'
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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'
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/assets/svg/index.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
23 |
24 |
33 |
--------------------------------------------------------------------------------
/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/assets/svg/icons/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/icons/gitee.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/assets/svg/icons/audio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
27 | 你的账号在异地登录了
28 |
29 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/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/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/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/layout/components/UserInfo.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
![]()
13 |
14 |
15 |
16 | {{ userInfo.name }}
17 |
18 |
19 | {{ userInfo.email }}
20 |
21 |
22 |
23 |
24 |
25 |
58 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # im-services web客户端
2 |
3 | [](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 | ```
--------------------------------------------------------------------------------
/src/components/emoji/Emoji.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | -
13 | {{ emo.char}}
14 |
15 |
16 |
17 |
18 |
19 |
65 |
--------------------------------------------------------------------------------
/src/assets/svg/icons/smile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/icons/addUser.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/svg/icons/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/customMenu/index.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
73 |
74 |
97 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 动画特效
8 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/components/LogoutDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
安全提醒
4 |
你的账号在其他地方登录,请确定账号是否安全!
5 |
6 |
7 |
登录地址:
8 |
9 | {{ logoutInfo && logoutInfo.pro }}-{{ logoutInfo && logoutInfo.city }}
10 |
11 |
12 |
13 |
登录ip:
14 |
{{ logoutInfo && logoutInfo.ip }}
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
33 |
100 |
--------------------------------------------------------------------------------
/src/components/UserMsg.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
24 |
25 |
26 |
![]()
27 |
28 |
29 |
30 |
昵称:
31 |
{{ userMsg?.name }}
32 |
33 |
34 |
邮箱:
35 |
{{ userMsg?.email }}
36 |
37 |
38 |
39 |
40 |
41 |
备注名:
42 |
{{ userMsg?.note || '暂无' }}
43 |
44 |
45 |
来源:
46 |
{{ userMsg?.source || '未知' }}
47 |
48 |
49 |
50 |
51 |
52 |
110 |
--------------------------------------------------------------------------------
/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/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/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/components/MyAudio.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
39 |
46 |
{{ duration }}''
47 |
48 |
49 |
50 |
51 |
180 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
![]()
49 |
50 |
51 |
52 |
53 |
54 |
55 | -
60 |
61 |
62 |
63 | -
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
169 |
--------------------------------------------------------------------------------
/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/components/AddFriend.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
添加好友
58 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 查询
69 |
70 |
71 |
72 |
73 |
好友推荐
74 |
75 |
76 | -
77 |
78 |
![]()
79 |
80 |
81 |
{{ item.name }}
82 |
性别:{{ ['未知', '男', '女'][item.sex] }}
83 |
+好友
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
210 |
--------------------------------------------------------------------------------
/src/components/Voice.vue:
--------------------------------------------------------------------------------
1 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
102 |
103 |
104 |
105 |
106 |
228 |
--------------------------------------------------------------------------------
/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/components/AddGroup.vue:
--------------------------------------------------------------------------------
1 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
184 |
185 |
186 |
187 | -
188 |
189 |
190 |
![]()
191 |
192 |
{{ user.Users.name }}
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | {{
205 | selectUser.length > 0
206 | ? `您已选择${selectUser.length}位用户`
207 | : '请勾选需要添加的用户'
208 | }}
209 |
210 |
211 |
212 |
213 |
214 | -
215 |
216 |
217 |
![]()
218 |
219 |
{{ user.Users.name }}
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
236 |
237 |
群名称:
238 |
239 |
240 |
241 |
242 |
243 |
描述:
244 |
245 |
251 |
252 |
253 |
268 |
269 |
群密码:
270 |
271 |
276 |
277 |
278 |
279 |
280 |
294 |
295 |
296 |
297 |
298 |
525 |
--------------------------------------------------------------------------------
/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/views/address/index.vue:
--------------------------------------------------------------------------------
1 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
新的朋友
158 |
162 |
163 |
164 |
165 |
新的朋友
166 |
167 |
168 |
169 | -
178 |
179 |
![]()
180 |
181 |
182 |
{{ item.Users.name }}
183 |
184 |
185 |
186 |
187 | -
196 |
197 |
![]()
198 |
199 |
200 |
{{ item.Users.name }}
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
![]()
213 |
214 |
215 |
{{ userMessage.Users.name }}
216 |
邮箱:{{ userMessage.Users.email }}
217 |
218 | 性别:{{
219 | ['未知', '男', '女'][userMessage.Users.sex as number]
220 | }}
221 |
222 |
223 |
224 |
225 | - 昵称: {{ userMessage.note || '暂无' }}
226 | -
227 | 最后登录时间:
228 | {{ userMessage.Users.last_login_time }}
229 |
230 |
231 |
232 | 发消息
233 |
234 |
235 |
236 |
237 |

238 |
239 |
暂无选择信息
240 |
241 |
242 |
243 |
新的朋友
244 |
245 |
246 | -
247 |
248 |
![]()
249 |
250 |
251 |
{{ item.users.name }}
252 |
{{ item.information }}
253 |
254 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
281 |
282 |
283 |
284 |
587 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |

251 |
252 |
253 |
260 |
261 |
268 |
269 |
270 |
277 |
278 |
279 |
280 |
295 |
296 |
第三方登录
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |

317 |
318 |
注册账号
319 |
320 |
321 |
328 |
329 |
336 |
337 |
338 |
345 |
346 |
347 |
354 |
355 |
356 |
363 |
364 |
365 |
366 |
373 |
374 | 获取验证码
377 |
378 |
379 |
380 |
381 |
382 |
397 |
398 |
399 |

400 |
401 |
402 |
403 | -
408 | {{ item.name }}
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
617 |
--------------------------------------------------------------------------------
/src/views/session/index.vue:
--------------------------------------------------------------------------------
1 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
374 |
375 |
378 |
379 |
380 | -
389 |
390 |
![]()
391 |
392 |
393 | {{
394 | item.last_message?.num == 0 ? '' : item.last_message?.num
395 | }}
397 |
398 |
399 |
400 | {{ item.note || item.name }}
401 |
402 | {{ item.last_message ? item.last_message.time : defaultTime }}
403 |
404 |
405 |
406 | {{ item.last_message ? item.last_message.content : '开始聊天' }}
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 | -
424 |
425 |
![]()
426 |
427 |
428 | {{
429 | item.last_message?.num == 0 ? '' : item.last_message?.num
430 | }}
432 |
433 |
434 |
435 | {{ item.note || item.name }}
436 |
437 | {{ item.last_message ? item.last_message.time : defaultTime }}
438 |
439 |
440 |
441 | {{ item.last_message ? item.last_message.content : '开始聊天' }}
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
455 | {{ selectSession?.name }}
456 | ···
457 |
458 |
459 |
467 |
468 | 查看更多消息
471 | 加载中
472 |
473 | -
474 |
475 | {{ getChatPotinTime(item.created_at) }}
476 |
477 |
478 | {{ item.msg }}
479 |
480 |
485 |
486 |
![]()
491 |
![]()
492 |
493 |
494 |
495 | {{ item.channel_type === 2 ? item.Users.name : '' }}
496 |
497 |
498 | {{ item.channel_type === 2 ? userInfo.name : '' }}
499 |
500 |
501 | {{ item.msg }}
502 |
503 |
504 |
508 |
509 |
515 |
516 |
523 |
524 |
531 |
532 |
{{ getFileName(item.msg) }}
533 |
534 |
535 | {{ getFileType(item.msg)[1] }}
536 |
537 |
538 |
539 |
544 |
545 |
{{ getFileName(item.msg) }}
546 |
547 |
548 | {{ getFileType(item.msg)[1] }}
549 |
550 |
551 |
552 |
553 |
554 |
557 |
558 |
559 |
568 |
569 |
570 |
601 |
602 |
608 |
609 |
610 | 发送
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
![]()
621 |
622 |
{{ selectSession?.name }}
623 |
624 |
628 |
629 |
630 |
631 |
638 |
639 |
640 |
641 |
642 |
![]()
643 |
644 |
{{ item.users.name }}
645 |
646 |
654 |
655 |
656 |
657 |
群名称
658 |
{{ selectSession?.Groups?.name }}
659 |
660 |
661 |
群描述
662 |
{{ selectSession?.Groups?.info || '暂无' }}
663 |
664 |
665 |
备注
666 |
{{ selectSession?.note || '暂无' }}
667 |
668 |
669 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
687 |
688 |
689 |
690 |
691 |
1324 |
--------------------------------------------------------------------------------