From 0386fdf61ec719d89bd6450cc8665703f575170a Mon Sep 17 00:00:00 2001 From: Desert <2864963532@qq.com> Date: Fri, 4 Jul 2025 16:05:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.build | 2 +- .env.build-test | 2 +- .env.serve-dev | 2 +- .env.serve-test | 2 +- package.json | 67 +++--- src/api/user.ts | 6 +- src/layout/app-main/Navbar.vue | 9 - src/router/index.ts | 37 +++ src/utils/smCrypto.ts | 10 + src/views/login/index.vue | 19 +- src/views/user-item/index.vue | 13 + ts-out-dir/src/router/index.js | 417 ++++++++++++++++++--------------- vitest.config.ts | 3 +- 13 files changed, 335 insertions(+), 254 deletions(-) create mode 100644 src/utils/smCrypto.ts create mode 100644 src/views/user-item/index.vue diff --git a/.env.build b/.env.build index 542d502..3f11cd0 100644 --- a/.env.build +++ b/.env.build @@ -2,7 +2,7 @@ #notes: login use mock api VITE_APP_ENV = 'dev' -VITE_APP_BASE_URL = '' +VITE_APP_BASE_URL = http://8.217.50.96:10001 #image or oss address VITE_APP_IMAGE_URL = '' diff --git a/.env.build-test b/.env.build-test index dcd5eda..93db0e4 100644 --- a/.env.build-test +++ b/.env.build-test @@ -2,7 +2,7 @@ #notes: login use mock api VITE_APP_ENV = 'dev' -VITE_APP_BASE_URL = '' +VITE_APP_BASE_URL = http://8.217.50.96:10001 #image or oss address VITE_APP_IMAGE_URL = '' diff --git a/.env.serve-dev b/.env.serve-dev index 542d502..3f11cd0 100644 --- a/.env.serve-dev +++ b/.env.serve-dev @@ -2,7 +2,7 @@ #notes: login use mock api VITE_APP_ENV = 'dev' -VITE_APP_BASE_URL = '' +VITE_APP_BASE_URL = http://8.217.50.96:10001 #image or oss address VITE_APP_IMAGE_URL = '' diff --git a/.env.serve-test b/.env.serve-test index 542d502..3f11cd0 100644 --- a/.env.serve-test +++ b/.env.serve-test @@ -2,7 +2,7 @@ #notes: login use mock api VITE_APP_ENV = 'dev' -VITE_APP_BASE_URL = '' +VITE_APP_BASE_URL = http://8.217.50.96:10001 #image or oss address VITE_APP_IMAGE_URL = '' diff --git a/package.json b/package.json index 9995150..3d00ef8 100644 --- a/package.json +++ b/package.json @@ -24,84 +24,83 @@ "vue": "^3.4.14" }, "dependencies": { - "@element-plus/icons-vue": "^2.0.4", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@element-plus/icons-vue": "^2.3.1", "axios": "1.6.5", - "codemirror": "^6.0.1", + "codemirror": "^6.0.2", "echarts": "5.3.2", "element-plus": "2.5.3", - "js-error-collection": "^1.0.7", - "json-editor-vue3": "^1.0.8", + "js-error-collection": "^1.0.8", + "json-editor-vue3": "^1.1.1", "mitt": "3.0.0", "moment-mini": "2.22.1", "nprogress": "0.2.0", "path": "0.12.7", "path-browserify": "^1.0.1", - "path-to-regexp": "^6.2.1", - "pinia": "^2.0.16", + "path-to-regexp": "^6.3.0", + "pinia": "^2.3.1", "pinia-plugin-persistedstate": "2.3.0", "screenfull": "^6.0.2", - "sortablejs": "^1.15.0", - "vue": "^3.4.14", + "sortablejs": "^1.15.6", + "vue": "^3.5.17", "vue-clipboard3": "^2.0.0", "vue-codemirror": "^6.1.1", "vue-i18n": "9.1.10", - "vue-router": "^4.1.5", - "@codemirror/lang-javascript": "^6.1.0", - "@codemirror/theme-one-dark": "^6.1.0" + "vue-router": "^4.5.1" }, "devDependencies": { "@babel/eslint-parser": "7.16.3", "@originjs/vite-plugin-commonjs": "^1.0.3", "@types/mockjs": "1.0.10", - "@types/node": "^17.0.35", - "@types/path-browserify": "^1.0.0", - "@types/sortablejs": "^1.15.0", + "@types/node": "^17.0.45", + "@types/path-browserify": "^1.0.3", + "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "5.30.0", "@typescript-eslint/parser": "5.30.0", - "@vitejs/plugin-legacy": "^5.2.0", - "@vitejs/plugin-vue": "^5.0.3", + "@vitejs/plugin-legacy": "^5.4.3", + "@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vitest/coverage-c8": "^0.33.0", - "@vitest/ui": "^1.2.0", + "@vitest/ui": "^1.6.1", "@vue/cli-plugin-unit-jest": "4.5.17", "@vue/cli-service": "5.0.8", - "@vue/test-utils": "^2.0.2", - "@vueuse/core": "^8.7.5", + "@vue/test-utils": "^2.4.6", + "@vueuse/core": "^8.9.4", "eslint": "8.18.0", "eslint-config-prettier": "8.5.0", "eslint-define-config": "1.5.1", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.26.0", - "eslint-plugin-jsonc": "^2.3.0", - "eslint-plugin-markdown": "^3.0.0", + "eslint-plugin-jsonc": "^2.20.1", + "eslint-plugin-markdown": "^3.0.1", "eslint-plugin-prettier": "4.1.0", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unicorn": "^43.0.2", "eslint-plugin-vue": "9.1.1", "husky": "7.0.2", "jsdom": "16.4.0", - "jsonc-eslint-parser": "^2.1.0", + "jsonc-eslint-parser": "^2.4.0", "majestic": "1.8.1", "mockjs": "1.1.0", "prettier": "2.2.1", "resize-observer-polyfill": "^1.5.1", - "rollup-plugin-visualizer": "^5.8.3", + "rollup-plugin-visualizer": "^5.14.0", "sass": "1.77.6", "svg-sprite-loader": "6.0.11", - "typescript": "^4.7.2", - "unocss": "^0.58.3", - "unplugin-auto-import": "^0.11.2", - "unplugin-vue-components": "^0.22.8", - - "unplugin-vue-define-options": "^0.6.1", - "vite": "^5.0.11", - "vite-plugin-html": "^3.2.0", - "vite-plugin-mkcert": "^1.7.2", - "vite-plugin-mock": "^3.0.1", + "typescript": "^4.9.5", + "unocss": "^0.58.9", + "unplugin-auto-import": "^0.11.5", + "unplugin-vue-components": "^0.22.12", + "unplugin-vue-define-options": "^0.6.2", + "vite": "^5.4.19", + "vite-plugin-html": "^3.2.2", + "vite-plugin-mkcert": "^1.17.8", + "vite-plugin-mock": "^3.0.2", "vite-plugin-style-import": "1.2.1", "vite-plugin-svg-icons": "^2.0.1", "vitest": "^0.22.1", - "vue-tsc": "^0.34.16" + "vue-tsc": "^0.34.17" }, "pnpm": { "peerDependencyRules": { diff --git a/src/api/user.ts b/src/api/user.ts index 1029b74..c45abb8 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,5 +1,5 @@ //获取用户信息 -import axiosReq from 'axios' +import axiosReq from '@/utils/axios-req'; // export const userInfoReq = (): Promise => { // return new Promise((resolve) => { // const reqConfig = { @@ -16,8 +16,8 @@ import axiosReq from 'axios' //登录 export const loginReq = (subForm) => { return axiosReq({ - url: '/mock/login', - params: subForm, + url: 'sys/auth/login', + data: subForm, method: 'post' }) } diff --git a/src/layout/app-main/Navbar.vue b/src/layout/app-main/Navbar.vue index ca90f84..31cee57 100644 --- a/src/layout/app-main/Navbar.vue +++ b/src/layout/app-main/Navbar.vue @@ -30,15 +30,6 @@ {{ langTitle('Home') }} - - {{ langTitle('Github') }} - - - {{ langTitle('low-code-platform') }} - - - {{ langTitle('office-doc') }} - {{ langTitle('login out') }} diff --git a/src/router/index.ts b/src/router/index.ts index 473c742..7dc5c81 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -3,6 +3,28 @@ import basicDemo from './modules/basic-demo' import type { RouterTypes } from '~/basic' import Layout from '@/layout/index.vue' + +const userList = [ + { + path: '/user-item:1', + name: 'UserItem1', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户01' } + }, + { + path: '/user-item:2', + name: 'UserItem2', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户02' } + }, + { + path: '/user-item:3', + name: 'UserItem3', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户03' } + } +] + export const constantRoutes: RouterTypes = [ { path: '/redirect', @@ -44,6 +66,13 @@ export const constantRoutes: RouterTypes = [ } ] }, + { + path: '/userCenter', + component: Layout, + alwaysShow: true, + meta: { title: '用户管理', elSvgIcon: 'Setting' }, + children: userList + }, { path: '/setting-switch', component: Layout, @@ -132,6 +161,14 @@ export const asyncRoutes: RouterTypes = [ // 404 page must be placed at the end !!! ] +/**指定预加载某个页面 */ +export function preloadPage(name) { + const page = constantRoutes.find((page) => page.name === name); + if (page) { + page.component(false); + } +} + const router = createRouter({ history: createWebHashHistory(), scrollBehavior: () => ({ top: 0 }), diff --git a/src/utils/smCrypto.ts b/src/utils/smCrypto.ts new file mode 100644 index 0000000..af6e194 --- /dev/null +++ b/src/utils/smCrypto.ts @@ -0,0 +1,10 @@ +// const publicKey = '040a302b5e4b961afb3908a4ae191266ac5866be100fc52e3b8dba9707c8620e64ae790ceffc3bfbf262dc098d293dd3e303356cb91b54861c767997799d2f0060' + +// /** +// * sm2加密 +// * @param data 待加密数据 +// * @return 加密后的数据 +// */ +// export const sm2Encrypt = (data: string): string => { +// return '04' + sm2.doEncrypt(data, publicKey, 1) +// } diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 5a23a15..a27ced5 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -4,12 +4,12 @@

{{ settings.title }}

- +
- +
@@ -26,7 +26,7 @@ v-model="subForm.password" :type="passwordType" name="password" - placeholder="password(123456)" + placeholder="密码" @keyup.enter="handleLogin" /> @@ -36,7 +36,7 @@
{{ tipMessage }}
@@ -55,8 +55,8 @@ const { settings } = useBasicStore() const formRules = useElement().formRules //form const subForm = reactive({ - keyword: 'panda', - password: '123456' + username: '', + password: '' }) const state:any = reactive({ otherQuery: {}, @@ -100,10 +100,13 @@ const router = useRouter() const basicStore = useBasicStore() const loginFunc = () => { - loginReq(subForm) + loginReq({ + username: subForm.username, + // password: sm2Encrypt(subForm.password) + }) .then(({ data }) => { elMessage('登录成功') - basicStore.setToken(data?.jwtToken) + basicStore.setToken(data?.access_token) router.push('/') }) .catch((err) => { diff --git a/src/views/user-item/index.vue b/src/views/user-item/index.vue new file mode 100644 index 0000000..e4c645a --- /dev/null +++ b/src/views/user-item/index.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/ts-out-dir/src/router/index.js b/ts-out-dir/src/router/index.js index cace407..1832d1d 100644 --- a/ts-out-dir/src/router/index.js +++ b/ts-out-dir/src/router/index.js @@ -1,203 +1,230 @@ -import { createRouter, createWebHashHistory } from 'vue-router'; -import Layout from '@/layout/index.vue'; +import { createRouter, createWebHashHistory } from 'vue-router' +import Layout from '@/layout/index.vue' + +const userList = [ + { + path: '/user-item:1', + name: 'UserItem1', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户01' } + }, + { + path: '/user-item:2', + name: 'UserItem2', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户02' } + }, + { + path: '/user-item:3', + name: 'UserItem3', + component: () => import('@/views/user-item/index.vue'), + meta: { title: '用户03' } + } +] + export const constantRoutes = [ - { - path: '/redirect', - component: Layout, - hidden: true, - children: [ - { - path: '/redirect/:path(.*)', - component: () => import('@/views/redirect') - } - ] - }, - { - path: '/login', - component: () => import('@/views/login/index.vue'), - hidden: true - }, - { - path: '/404', - component: () => import('@/views/error-page/404.vue'), - hidden: true - }, - { - path: '/401', - component: () => import('@/views/error-page/401.vue'), - hidden: true - }, - { - path: '/', - component: Layout, - redirect: '/dashboard', - children: [ - { - path: 'dashboard', - name: 'Dashboard', - component: () => import('@/views/dashboard/index.vue'), - meta: { title: 'Dashboard', elSvgIcon: 'Fold' } - } - ] - }, - { - path: '/setting-switch', - component: Layout, - children: [ - { - path: 'index', - component: () => import('@/views/setting-switch/index.vue'), - name: 'SettingSwitch', - meta: { title: 'Setting Switch', icon: 'example', affix: true } - } - ] + { + path: '/redirect', + component: Layout, + hidden: true, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect') + } + ] + }, + { + path: '/login', + component: () => import('@/views/login/index.vue'), + hidden: true + }, + { + path: '/404', + component: () => import('@/views/error-page/404.vue'), + hidden: true + }, + { + path: '/401', + component: () => import('@/views/error-page/401.vue'), + hidden: true + }, + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/dashboard/index.vue'), + meta: { title: 'Dashboard', elSvgIcon: 'Fold' } + } + ] + }, + { + path: '/userCenter', + component: Layout, + alwaysShow: true, + meta: { title: '用户管理', elSvgIcon: 'Setting' }, + children: userList + }, + { + path: '/setting-switch', + component: Layout, + children: [ + { + path: 'index', + component: () => import('@/views/setting-switch/index.vue'), + name: 'SettingSwitch', + meta: { title: 'Setting Switch', icon: 'example', affix: true } + } + ] + }, + { + path: '/error-collection', + component: Layout, + meta: { title: 'Error Collection', icon: 'eye' }, + alwaysShow: true, + children: [ + { + path: 'error-collection-table-query', + component: () => import('@/views/error-collection/ErrorCollectionTableQuery.vue'), + name: 'ErrorCollectionTableQuery', + meta: { title: 'Index' } + }, + { + path: 'error-log-test', + component: () => import('@/views/error-log/ErrorLogTest.vue'), + name: 'ErrorLogTest', + meta: { title: 'ErrorLog Test' } + } + ] + }, + { + path: '/nested', + component: Layout, + redirect: '/nested/menu1', + name: 'Nested', + meta: { + title: 'Nested', + icon: 'nested' }, - { - path: '/error-collection', - component: Layout, - meta: { title: 'Error Collection', icon: 'eye' }, - alwaysShow: true, + children: [ + { + path: 'menu1', + component: () => import('@/views/nested/menu1/index.vue'), + name: 'Menu1', + meta: { title: 'Menu1' }, children: [ - { - path: 'error-collection-table-query', - component: () => import('@/views/error-collection/ErrorCollectionTableQuery.vue'), - name: 'ErrorCollectionTableQuery', - meta: { title: 'Index' } - }, - { - path: 'error-log-test', - component: () => import('@/views/error-log/ErrorLogTest.vue'), - name: 'ErrorLogTest', - meta: { title: 'ErrorLog Test' } - } + { + path: 'menu1-1', + component: () => import('@/views/nested/menu1/menu1-1/index.vue'), + name: 'Menu1-1', + meta: { title: 'Menu1-1' } + }, + { + path: 'menu1-2', + component: () => import('@/views/nested/menu1/menu1-2/index.vue'), + name: 'Menu1-2', + meta: { title: 'Menu1-2' }, + children: [ + { + path: 'menu1-2-1', + component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1/index.vue'), + name: 'Menu1-2-1', + meta: { title: 'Menu1-2-1' } + }, + { + path: 'menu1-2-2', + component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2/index.vue'), + name: 'Menu1-2-2', + meta: { title: 'Menu1-2-2' } + } + ] + }, + { + path: 'menu1-3', + component: () => import('@/views/nested/menu1/menu1-3/index.vue'), + name: 'Menu1-3', + meta: { title: 'Menu1-3' } + } ] + }, + { + path: 'menu2', + component: () => import('@/views/nested/menu2/index.vue'), + name: 'Menu2', + meta: { title: 'menu2' } + } + ] + }, + { + path: '/external-link', + component: Layout, + children: [ + { + component: () => {}, + path: 'https://github.com/jzfai/vue3-admin-ts.git', + meta: { title: 'External Link', icon: 'link' } + } + ] + } +] +export const roleCodeRoutes = [ + { + path: '/roles-codes', + component: Layout, + redirect: '/roles-codes/page', + alwaysShow: true, + name: 'Permission', + meta: { + title: 'Permission', + icon: 'lock', + roles: ['admin', 'editor'] }, - { - path: '/nested', - component: Layout, - redirect: '/nested/menu1', - name: 'Nested', + children: [ + { + path: 'index', + component: () => import('@/views/roles-codes/index.vue'), + name: 'RolesCodes', meta: { - title: 'Nested', - icon: 'nested' - }, - children: [ - { - path: 'menu1', - component: () => import('@/views/nested/menu1/index.vue'), - name: 'Menu1', - meta: { title: 'Menu1' }, - children: [ - { - path: 'menu1-1', - component: () => import('@/views/nested/menu1/menu1-1/index.vue'), - name: 'Menu1-1', - meta: { title: 'Menu1-1' } - }, - { - path: 'menu1-2', - component: () => import('@/views/nested/menu1/menu1-2/index.vue'), - name: 'Menu1-2', - meta: { title: 'Menu1-2' }, - children: [ - { - path: 'menu1-2-1', - component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1/index.vue'), - name: 'Menu1-2-1', - meta: { title: 'Menu1-2-1' } - }, - { - path: 'menu1-2-2', - component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2/index.vue'), - name: 'Menu1-2-2', - meta: { title: 'Menu1-2-2' } - } - ] - }, - { - path: 'menu1-3', - component: () => import('@/views/nested/menu1/menu1-3/index.vue'), - name: 'Menu1-3', - meta: { title: 'Menu1-3' } - } - ] - }, - { - path: 'menu2', - component: () => import('@/views/nested/menu2/index.vue'), - name: 'Menu2', - meta: { title: 'menu2' } - } - ] - }, - { - path: '/external-link', - component: Layout, - children: [ - { - component: () => { }, - path: 'https://github.com/jzfai/vue3-admin-ts.git', - meta: { title: 'External Link', icon: 'link' } - } - ] - } -]; -export const roleCodeRoutes = [ - { - path: '/roles-codes', - component: Layout, - redirect: '/roles-codes/page', - alwaysShow: true, - name: 'Permission', + title: 'index' + } + }, + { + path: 'roleIndex', + component: () => import('@/views/roles-codes/role-index.vue'), + name: 'RoleIndex', meta: { - title: 'Permission', - icon: 'lock', - roles: ['admin', 'editor'] - }, - children: [ - { - path: 'index', - component: () => import('@/views/roles-codes/index.vue'), - name: 'RolesCodes', - meta: { - title: 'index' - } - }, - { - path: 'roleIndex', - component: () => import('@/views/roles-codes/role-index.vue'), - name: 'RoleIndex', - meta: { - title: 'Role Index', - roles: ['admin'] - } - }, - { - path: 'code-index', - component: () => import('@/views/roles-codes/code-index.vue'), - name: 'CodeIndex', - meta: { - title: 'Code Index', - code: 16 - } - }, - { - path: 'button-permission', - component: () => import('@/views/roles-codes/button-permission.vue'), - name: 'ButtonPermission', - meta: { - title: 'Button Permission' - } - } - ] - } -]; -export const asyncRoutes = [ - { path: '/:catchAll(.*)', name: 'CatchAll', redirect: '/404', hidden: true } -]; + title: 'Role Index', + roles: ['admin'] + } + }, + { + path: 'code-index', + component: () => import('@/views/roles-codes/code-index.vue'), + name: 'CodeIndex', + meta: { + title: 'Code Index', + code: 16 + } + }, + { + path: 'button-permission', + component: () => import('@/views/roles-codes/button-permission.vue'), + name: 'ButtonPermission', + meta: { + title: 'Button Permission' + } + } + ] + } +] +export const asyncRoutes = [{ path: '/:catchAll(.*)', name: 'CatchAll', redirect: '/404', hidden: true }] const router = createRouter({ - history: createWebHashHistory(), - scrollBehavior: () => ({ top: 0 }), - routes: constantRoutes -}); -export default router; + history: createWebHashHistory(), + scrollBehavior: () => ({ top: 0 }), + routes: constantRoutes +}) +export default router diff --git a/vitest.config.ts b/vitest.config.ts index 1e715fb..5b0e2be 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,8 @@ import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import VueJsx from '@vitejs/plugin-vue-jsx' import DefineOptions from 'unplugin-vue-define-options/vite' - +// 确保 esbuild 以兼容方式加载 +import * as esbuild from 'esbuild' export default defineConfig({ // @ts-ignore plugins: [Vue(), VueJsx(), DefineOptions()], From 8685a1f488d5071feb9734287ea0dccad9d6e5a9 Mon Sep 17 00:00:00 2001 From: Desert <2864963532@qq.com> Date: Fri, 4 Jul 2025 17:13:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E8=BF=81=E7=A7=BB=E5=B7=AE=E5=AD=98?= =?UTF-8?q?=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslintrc/.eslintrc-auto-import.json | 1 + package.json | 3 + src/api/user.ts | 8 + src/im-sdk/BaseAdapter.ts | 35 + src/im-sdk/ImDataCenter.ts | 214 +++++ src/im-sdk/assets/Camera.svg | 12 + src/im-sdk/assets/CameraClose.svg | 11 + src/im-sdk/assets/ConnectedCall.svg | 7 + src/im-sdk/assets/ConnectedVideo.svg | 5 + src/im-sdk/assets/Gift.svg | 18 + src/im-sdk/assets/More.svg | 11 + src/im-sdk/assets/NotConnectedCall.svg | 5 + src/im-sdk/assets/NotConnectedVideo.svg | 5 + src/im-sdk/assets/VectorPause.svg | 3 + src/im-sdk/assets/Vectorplay.svg | 3 + src/im-sdk/assets/accept.svg | 6 + src/im-sdk/assets/acceptVideo.svg | 12 + src/im-sdk/assets/alreadyFollowing.svg | 21 + src/im-sdk/assets/backImg.svg | 12 + src/im-sdk/assets/bigCamera.svg | 8 + src/im-sdk/assets/blockImg.svg | 8 + src/im-sdk/assets/callMenu.svg | 7 + src/im-sdk/assets/callVideo.svg | 12 + src/im-sdk/assets/callVoice.svg | 8 + src/im-sdk/assets/cancel.svg | 8 + src/im-sdk/assets/cancelBtn.svg | 8 + src/im-sdk/assets/emoji.svg | 10 + src/im-sdk/assets/expand.svg | 11 + src/im-sdk/assets/finish.svg | 6 + src/im-sdk/assets/full.svg | 91 ++ src/im-sdk/assets/giftCall.svg | 12 + src/im-sdk/assets/gold.svg | 9 + src/im-sdk/assets/hollow.svg | 84 ++ src/im-sdk/assets/keyboard.svg | 14 + src/im-sdk/assets/leftLove.svg | 9 + src/im-sdk/assets/loading.svg | 13 + src/im-sdk/assets/man.svg | 12 + src/im-sdk/assets/microphone.svg | 7 + src/im-sdk/assets/microphoneClose.svg | 13 + src/im-sdk/assets/moreBtn.svg | 10 + src/im-sdk/assets/noData.svg | 45 + src/im-sdk/assets/pauseVoice.svg | 3 + src/im-sdk/assets/payMenu.svg | 6 + src/im-sdk/assets/picMenu.svg | 5 + src/im-sdk/assets/playVoice.svg | 3 + src/im-sdk/assets/read.svg | 7 + src/im-sdk/assets/refuse.svg | 6 + src/im-sdk/assets/remark.svg | 15 + src/im-sdk/assets/report.svg | 14 + src/im-sdk/assets/reversalTools.svg | 15 + src/im-sdk/assets/rightLove.svg | 9 + src/im-sdk/assets/scaleTools.svg | 10 + src/im-sdk/assets/send.svg | 7 + src/im-sdk/assets/sendFail.svg | 14 + src/im-sdk/assets/sendImg.svg | 8 + src/im-sdk/assets/sendV.svg | 10 + src/im-sdk/assets/sendVBtn.svg | 10 + src/im-sdk/assets/shutVideo.svg | 8 + src/im-sdk/assets/speaker.svg | 8 + src/im-sdk/assets/speakerClose.svg | 11 + src/im-sdk/assets/toolbar.svg | 9 + src/im-sdk/assets/videoMenu.svg | 7 + src/im-sdk/assets/voice.svg | 10 + src/im-sdk/assets/woman.svg | 15 + src/im-sdk/callDataCenter.ts | 835 ++++++++++++++++++ src/im-sdk/components/CacheImg.vue | 21 + src/im-sdk/components/Chat.vue | 146 +++ src/im-sdk/components/Conversation.vue | 71 ++ src/im-sdk/config/default.ts | 18 + src/im-sdk/config/index.ts | 34 + src/im-sdk/db/chatDB.ts | 104 +++ src/im-sdk/entity/ConversationController.ts | 335 +++++++ src/im-sdk/entity/MessageBuffer.ts | 34 + src/im-sdk/manager/avatarManager.ts | 32 + src/im-sdk/manager/errorManager.ts | 61 ++ src/im-sdk/types.ts | 191 ++++ src/im-sdk/utils/emojis.ts | 322 +++++++ src/im-sdk/utils/index.ts | 66 ++ src/im-sdk/utils/message.ts | 47 + src/im-sdk/utils/timestamp.ts | 37 + src/im-sdk/widget/ChatItem.vue | 38 + src/im-sdk/widget/ChatMenu.vue | 290 ++++++ src/im-sdk/widget/ConversationItem.vue | 150 ++++ src/im-sdk/widget/SendStatus.vue | 35 + src/im-sdk/widget/TimeCost.vue | 35 + src/im-sdk/widget/chatNav/ChatNav.vue | 108 +++ src/im-sdk/widget/chatNav/ChatNavCP.vue | 166 ++++ .../widget/menuModule/ChatCallVideo.vue | 19 + .../widget/menuModule/ChatCallVoice.vue | 18 + src/im-sdk/widget/menuModule/EmojiMap.vue | 79 ++ .../widget/menuModule/SendPrivateImg.vue | 68 ++ .../widget/menuModule/SendPrivatePayImg.vue | 12 + .../widget/menuModule/Voice/VioceRecorder.ts | 87 ++ src/im-sdk/widget/menuModule/VoiceBtn.vue | 239 +++++ src/im-sdk/widget/messages/CallMessage.vue | 201 +++++ src/im-sdk/widget/messages/ImageMessage.vue | 87 ++ .../widget/messages/LocationMessage.vue | 14 + src/im-sdk/widget/messages/TextMessage.vue | 87 ++ src/im-sdk/widget/messages/UnknownMessage.vue | 13 + src/im-sdk/widget/messages/VideoMessage.vue | 85 ++ .../messages/Voice/ViocePlayController.ts | 67 ++ src/im-sdk/widget/messages/VoiceMessage.vue | 92 ++ src/store/basic.ts | 2 + src/third/huanxin/Adapter.ts | 530 +++++++++++ src/third/huanxin/api.ts | 37 + src/third/huanxin/index.ts | 79 ++ src/third/huanxin/utils.ts | 127 +++ src/utils/smCrypto.ts | 20 +- src/views/login/index.vue | 8 +- typings/auto-imports.d.ts | 1 + 110 files changed, 6144 insertions(+), 11 deletions(-) create mode 100644 src/im-sdk/BaseAdapter.ts create mode 100644 src/im-sdk/ImDataCenter.ts create mode 100644 src/im-sdk/assets/Camera.svg create mode 100644 src/im-sdk/assets/CameraClose.svg create mode 100644 src/im-sdk/assets/ConnectedCall.svg create mode 100644 src/im-sdk/assets/ConnectedVideo.svg create mode 100644 src/im-sdk/assets/Gift.svg create mode 100644 src/im-sdk/assets/More.svg create mode 100644 src/im-sdk/assets/NotConnectedCall.svg create mode 100644 src/im-sdk/assets/NotConnectedVideo.svg create mode 100644 src/im-sdk/assets/VectorPause.svg create mode 100644 src/im-sdk/assets/Vectorplay.svg create mode 100644 src/im-sdk/assets/accept.svg create mode 100644 src/im-sdk/assets/acceptVideo.svg create mode 100644 src/im-sdk/assets/alreadyFollowing.svg create mode 100644 src/im-sdk/assets/backImg.svg create mode 100644 src/im-sdk/assets/bigCamera.svg create mode 100644 src/im-sdk/assets/blockImg.svg create mode 100644 src/im-sdk/assets/callMenu.svg create mode 100644 src/im-sdk/assets/callVideo.svg create mode 100644 src/im-sdk/assets/callVoice.svg create mode 100644 src/im-sdk/assets/cancel.svg create mode 100644 src/im-sdk/assets/cancelBtn.svg create mode 100644 src/im-sdk/assets/emoji.svg create mode 100644 src/im-sdk/assets/expand.svg create mode 100644 src/im-sdk/assets/finish.svg create mode 100644 src/im-sdk/assets/full.svg create mode 100644 src/im-sdk/assets/giftCall.svg create mode 100644 src/im-sdk/assets/gold.svg create mode 100644 src/im-sdk/assets/hollow.svg create mode 100644 src/im-sdk/assets/keyboard.svg create mode 100644 src/im-sdk/assets/leftLove.svg create mode 100644 src/im-sdk/assets/loading.svg create mode 100644 src/im-sdk/assets/man.svg create mode 100644 src/im-sdk/assets/microphone.svg create mode 100644 src/im-sdk/assets/microphoneClose.svg create mode 100644 src/im-sdk/assets/moreBtn.svg create mode 100644 src/im-sdk/assets/noData.svg create mode 100644 src/im-sdk/assets/pauseVoice.svg create mode 100644 src/im-sdk/assets/payMenu.svg create mode 100644 src/im-sdk/assets/picMenu.svg create mode 100644 src/im-sdk/assets/playVoice.svg create mode 100644 src/im-sdk/assets/read.svg create mode 100644 src/im-sdk/assets/refuse.svg create mode 100644 src/im-sdk/assets/remark.svg create mode 100644 src/im-sdk/assets/report.svg create mode 100644 src/im-sdk/assets/reversalTools.svg create mode 100644 src/im-sdk/assets/rightLove.svg create mode 100644 src/im-sdk/assets/scaleTools.svg create mode 100644 src/im-sdk/assets/send.svg create mode 100644 src/im-sdk/assets/sendFail.svg create mode 100644 src/im-sdk/assets/sendImg.svg create mode 100644 src/im-sdk/assets/sendV.svg create mode 100644 src/im-sdk/assets/sendVBtn.svg create mode 100644 src/im-sdk/assets/shutVideo.svg create mode 100644 src/im-sdk/assets/speaker.svg create mode 100644 src/im-sdk/assets/speakerClose.svg create mode 100644 src/im-sdk/assets/toolbar.svg create mode 100644 src/im-sdk/assets/videoMenu.svg create mode 100644 src/im-sdk/assets/voice.svg create mode 100644 src/im-sdk/assets/woman.svg create mode 100644 src/im-sdk/callDataCenter.ts create mode 100644 src/im-sdk/components/CacheImg.vue create mode 100644 src/im-sdk/components/Chat.vue create mode 100644 src/im-sdk/components/Conversation.vue create mode 100644 src/im-sdk/config/default.ts create mode 100644 src/im-sdk/config/index.ts create mode 100644 src/im-sdk/db/chatDB.ts create mode 100644 src/im-sdk/entity/ConversationController.ts create mode 100644 src/im-sdk/entity/MessageBuffer.ts create mode 100644 src/im-sdk/manager/avatarManager.ts create mode 100644 src/im-sdk/manager/errorManager.ts create mode 100644 src/im-sdk/types.ts create mode 100644 src/im-sdk/utils/emojis.ts create mode 100644 src/im-sdk/utils/index.ts create mode 100644 src/im-sdk/utils/message.ts create mode 100644 src/im-sdk/utils/timestamp.ts create mode 100644 src/im-sdk/widget/ChatItem.vue create mode 100644 src/im-sdk/widget/ChatMenu.vue create mode 100644 src/im-sdk/widget/ConversationItem.vue create mode 100644 src/im-sdk/widget/SendStatus.vue create mode 100644 src/im-sdk/widget/TimeCost.vue create mode 100644 src/im-sdk/widget/chatNav/ChatNav.vue create mode 100644 src/im-sdk/widget/chatNav/ChatNavCP.vue create mode 100644 src/im-sdk/widget/menuModule/ChatCallVideo.vue create mode 100644 src/im-sdk/widget/menuModule/ChatCallVoice.vue create mode 100644 src/im-sdk/widget/menuModule/EmojiMap.vue create mode 100644 src/im-sdk/widget/menuModule/SendPrivateImg.vue create mode 100644 src/im-sdk/widget/menuModule/SendPrivatePayImg.vue create mode 100644 src/im-sdk/widget/menuModule/Voice/VioceRecorder.ts create mode 100644 src/im-sdk/widget/menuModule/VoiceBtn.vue create mode 100644 src/im-sdk/widget/messages/CallMessage.vue create mode 100644 src/im-sdk/widget/messages/ImageMessage.vue create mode 100644 src/im-sdk/widget/messages/LocationMessage.vue create mode 100644 src/im-sdk/widget/messages/TextMessage.vue create mode 100644 src/im-sdk/widget/messages/UnknownMessage.vue create mode 100644 src/im-sdk/widget/messages/VideoMessage.vue create mode 100644 src/im-sdk/widget/messages/Voice/ViocePlayController.ts create mode 100644 src/im-sdk/widget/messages/VoiceMessage.vue create mode 100644 src/third/huanxin/Adapter.ts create mode 100644 src/third/huanxin/api.ts create mode 100644 src/third/huanxin/index.ts create mode 100644 src/third/huanxin/utils.ts diff --git a/eslintrc/.eslintrc-auto-import.json b/eslintrc/.eslintrc-auto-import.json index e25503e..1ade879 100644 --- a/eslintrc/.eslintrc-auto-import.json +++ b/eslintrc/.eslintrc-auto-import.json @@ -77,6 +77,7 @@ "shallowReadonly": true, "shallowRef": true, "sleepTimeout": true, + "sm2Encrypt": true, "storeToRefs": true, "toRaw": true, "toRef": true, diff --git a/package.json b/package.json index 3d00ef8..3102e38 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@element-plus/icons-vue": "^2.3.1", "axios": "1.6.5", "codemirror": "^6.0.2", + "easemob-websdk": "^4.15.1", "echarts": "5.3.2", "element-plus": "2.5.3", "js-error-collection": "^1.0.8", @@ -42,6 +43,7 @@ "pinia": "^2.3.1", "pinia-plugin-persistedstate": "2.3.0", "screenfull": "^6.0.2", + "sm-crypto": "^0.3.13", "sortablejs": "^1.15.6", "vue": "^3.5.17", "vue-clipboard3": "^2.0.0", @@ -67,6 +69,7 @@ "@vue/cli-service": "5.0.8", "@vue/test-utils": "^2.4.6", "@vueuse/core": "^8.9.4", + "esbuild": "^0.19.11", "eslint": "8.18.0", "eslint-config-prettier": "8.5.0", "eslint-define-config": "1.5.1", diff --git a/src/api/user.ts b/src/api/user.ts index c45abb8..41c09b2 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -29,3 +29,11 @@ export const loginOutReq = () => { method: 'post' }) } + +// 获取用户数据 +export const getUserInfoReq = () => { + return axiosReq({ + url: '/sys/user/info', + method: 'get' + }) +} \ No newline at end of file diff --git a/src/im-sdk/BaseAdapter.ts b/src/im-sdk/BaseAdapter.ts new file mode 100644 index 0000000..35bd74d --- /dev/null +++ b/src/im-sdk/BaseAdapter.ts @@ -0,0 +1,35 @@ +import MessageBuffer from "./entity/MessageBuffer"; +import { + BaseMessage, + ConversationData, + ImUserData, + MessageData, + MessageTempData, +} from "./types"; + +export interface LoaderListener { + onLogin(): void; + onMessage(message: MessageData): void; + onConnected(): void; + onDisconnected(): void; +} + +export interface BaseAdapter { + listener: LoaderListener; + login(userId: string, token: string): Promise; + logout(): void; + addListener(listener: LoaderListener): void; + + fetchConversations(): Promise; + /**需要返回从旧到新的 */ + fetchMessageList( + userId: string, + cursor: string + ): Promise<{ cursor: string; list: MessageData[]; isEnd?: boolean }>; + + sendMessage(message: MessageTempData): Promise; + + buildConversation(base: ConversationData): ConversationData; + + fetchUser(imId: string): Promise; +} diff --git a/src/im-sdk/ImDataCenter.ts b/src/im-sdk/ImDataCenter.ts new file mode 100644 index 0000000..036cec6 --- /dev/null +++ b/src/im-sdk/ImDataCenter.ts @@ -0,0 +1,214 @@ +import { reactive, ref } from 'vue'; +import { BaseAdapter, LoaderListener } from './BaseAdapter'; +import { ChatDB, getChatDB } from './db/chatDB'; +import { ConversationController } from './entity/ConversationController'; +import MessageBuffer from './entity/MessageBuffer'; +import { ConversationData, ImUserData, MessageData } from './types'; +import eventManager, { ImErrorCode } from './manager/errorManager'; + +interface Values { + mine?: ImUserData; + /**连接状态,可以用来显示在会话列表顶部显示网络状态 */ + connected: boolean; +} +export class ImDataCenter implements LoaderListener { + private adapter!: BaseAdapter; + private db?: ChatDB; + private userId?: string | number; + conversations: ConversationData[] = reactive([]); + /**外面需要的响应式数据 */ + data = reactive({ + connected: false, + }); + + private _loaded = false; + /**会话管理列表 */ + private conversationControllerMap: Record = + {}; + + private messageBuffer?: MessageBuffer; + + setAdapter(adapter: BaseAdapter) { + this.adapter = adapter; + this.adapter.addListener(this); + } + async onLogin() { + await this.adapter + .fetchConversations() + .catch((e) => { + eventManager.emit({ + code: ImErrorCode.loadRemoteConversationError, + error: e, + }); + return []; + }) + .then((list) => this.onLoadRemoteConversation(list)); + Object.values(this.conversationControllerMap).forEach((controller) => + controller.onLogin() + ); + this.messageBuffer?.onReady(); + } + onMessage(message: MessageData): void { + this.messageBuffer?.onMessage(message); + } + onConnected(): void { + this.data.connected = true; + } + onDisconnected(): void { + this.data.connected = false; + } + + login(userId: string, token: string) { + if (this.userId === userId) { + return; + } + this.db = getChatDB(userId); + this.messageBuffer = new MessageBuffer(this.db, this.dispatchMessage); + this.adapter.login(userId, token); + Object.values(this.conversationControllerMap).forEach((controller) => { + controller.updateDb(this.db!); + controller.loadLocalMessage(); + }); + this.loadLocalConversation(); + this.adapter.fetchUser(userId).then((resp) => { + this.data.mine = resp; + }); + this._loaded = true; + } + logout() { + this.adapter.logout(); + this.db?.close(); + this.userId = undefined; + this.db = undefined; + this.conversations.length = 0; + this.conversationControllerMap = {}; + this._loaded = false; + } + + private dispatchMessage = (message: MessageData) => { + const conversation = this.getConversation(message.target, true); + conversation!.onRecieve(message); + }; + + getConversation(id: string, autoAlloc?: boolean) { + const conversationController = this.conversationControllerMap[id]; + if (conversationController) { + return conversationController; + } + if (autoAlloc) { + const base: ConversationData = { + id, + type: 'singleChat', + isPinned: false, + unReadCount: 0, + time: Date.now(), + }; + const conversation = this.adapter.buildConversation(base); + this.conversations.unshift(conversation); + const convRef = this.conversations[0]; + this.loadUserData(convRef); + const controller = new ConversationController( + this.db!, + convRef, + this.adapter, + this.delaySort + ); + this.conversationControllerMap[id] = controller; + if (this._loaded) { + controller.loadLocalMessage(); + } + return controller; + } + return undefined; + } + + private async loadLocalConversation() { + if (!this.db) { + return console.warn('数据库未就绪,不应该查询本地聊天记录'); + } + const list = await this.db.conversations.toArray(); + list.forEach((conversation) => { + const conv = this.conversationControllerMap[conversation.id]; + if (conv) { + conv.update(conversation); + conv.loadLocalMessage(); + } else { + this.conversations.push(conversation); + const convRef = this.conversations[this.conversations.length - 1]; + this.loadUserData(convRef); + const controller = new ConversationController( + this.db!, + convRef, + this.adapter, + this.delaySort + ); + this.conversationControllerMap[conversation.id] = controller; + controller.loadLocalMessage(); + } + }); + } + + private onLoadRemoteConversation(list: ConversationData[]) { + list.forEach((conversation) => { + const ctr = this.conversationControllerMap[conversation.id] + if(ctr){ + ctr.update(conversation) + }else{ + this.conversations.push(conversation); + // 必须要这样 + const convRef = this.conversations[this.conversations.length - 1]; + this.loadUserData(convRef); + this.db?.saveConversation(conversation); + const controller = new ConversationController( + this.db!, + convRef, + this.adapter, + this.delaySort + ); + controller.loadLocalMessage(); + this.conversationControllerMap[conversation.id] = controller; + } + }); + this.sortConversation(); + } + /**对会话排序 */ + sortConversation() { + this.conversations.sort((a, b) => { + // pin 的会话优先显示 + const divPin = Number(b.isPinned) - Number(a.isPinned); + if (divPin !== 0) { + return divPin; + } + return b.time - a.time; + }); + } + + private sortPromise?: Promise; + delaySort = (id?: string) => { + if (this.conversations[0]?.id === id) { + return; + } + if (!this.sortPromise) { + this.sortPromise = Promise.resolve().then(() => { + this.sortConversation(); + this.sortPromise = undefined; + }); + } + }; + + private loadUserData(conversation: ConversationData) { + // if (conversation.user) return; + if (conversation.type !== 'singleChat') return; + this.adapter.fetchUser(conversation.id).then((user) => { + conversation.user = user; + }); + } + + get totalUnread() { + return this.conversations.reduce((ret, conversation) => { + return ret + conversation.unReadCount; + }, 0); + } +} + +export default new ImDataCenter(); diff --git a/src/im-sdk/assets/Camera.svg b/src/im-sdk/assets/Camera.svg new file mode 100644 index 0000000..eb67b77 --- /dev/null +++ b/src/im-sdk/assets/Camera.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/CameraClose.svg b/src/im-sdk/assets/CameraClose.svg new file mode 100644 index 0000000..775238a --- /dev/null +++ b/src/im-sdk/assets/CameraClose.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/im-sdk/assets/ConnectedCall.svg b/src/im-sdk/assets/ConnectedCall.svg new file mode 100644 index 0000000..b54095b --- /dev/null +++ b/src/im-sdk/assets/ConnectedCall.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/ConnectedVideo.svg b/src/im-sdk/assets/ConnectedVideo.svg new file mode 100644 index 0000000..0644269 --- /dev/null +++ b/src/im-sdk/assets/ConnectedVideo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/im-sdk/assets/Gift.svg b/src/im-sdk/assets/Gift.svg new file mode 100644 index 0000000..5cdb98b --- /dev/null +++ b/src/im-sdk/assets/Gift.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/More.svg b/src/im-sdk/assets/More.svg new file mode 100644 index 0000000..2b2edd8 --- /dev/null +++ b/src/im-sdk/assets/More.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/im-sdk/assets/NotConnectedCall.svg b/src/im-sdk/assets/NotConnectedCall.svg new file mode 100644 index 0000000..3e7aaa7 --- /dev/null +++ b/src/im-sdk/assets/NotConnectedCall.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/im-sdk/assets/NotConnectedVideo.svg b/src/im-sdk/assets/NotConnectedVideo.svg new file mode 100644 index 0000000..04ec2f1 --- /dev/null +++ b/src/im-sdk/assets/NotConnectedVideo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/im-sdk/assets/VectorPause.svg b/src/im-sdk/assets/VectorPause.svg new file mode 100644 index 0000000..86d6d02 --- /dev/null +++ b/src/im-sdk/assets/VectorPause.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/im-sdk/assets/Vectorplay.svg b/src/im-sdk/assets/Vectorplay.svg new file mode 100644 index 0000000..ef57c61 --- /dev/null +++ b/src/im-sdk/assets/Vectorplay.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/im-sdk/assets/accept.svg b/src/im-sdk/assets/accept.svg new file mode 100644 index 0000000..26191e7 --- /dev/null +++ b/src/im-sdk/assets/accept.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/im-sdk/assets/acceptVideo.svg b/src/im-sdk/assets/acceptVideo.svg new file mode 100644 index 0000000..0ba7218 --- /dev/null +++ b/src/im-sdk/assets/acceptVideo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/alreadyFollowing.svg b/src/im-sdk/assets/alreadyFollowing.svg new file mode 100644 index 0000000..76f7408 --- /dev/null +++ b/src/im-sdk/assets/alreadyFollowing.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/backImg.svg b/src/im-sdk/assets/backImg.svg new file mode 100644 index 0000000..4b4b836 --- /dev/null +++ b/src/im-sdk/assets/backImg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/bigCamera.svg b/src/im-sdk/assets/bigCamera.svg new file mode 100644 index 0000000..63f3f5e --- /dev/null +++ b/src/im-sdk/assets/bigCamera.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/blockImg.svg b/src/im-sdk/assets/blockImg.svg new file mode 100644 index 0000000..e77074b --- /dev/null +++ b/src/im-sdk/assets/blockImg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/callMenu.svg b/src/im-sdk/assets/callMenu.svg new file mode 100644 index 0000000..fdf958e --- /dev/null +++ b/src/im-sdk/assets/callMenu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/callVideo.svg b/src/im-sdk/assets/callVideo.svg new file mode 100644 index 0000000..acbac8f --- /dev/null +++ b/src/im-sdk/assets/callVideo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/callVoice.svg b/src/im-sdk/assets/callVoice.svg new file mode 100644 index 0000000..5346ed2 --- /dev/null +++ b/src/im-sdk/assets/callVoice.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/cancel.svg b/src/im-sdk/assets/cancel.svg new file mode 100644 index 0000000..7fa6460 --- /dev/null +++ b/src/im-sdk/assets/cancel.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/cancelBtn.svg b/src/im-sdk/assets/cancelBtn.svg new file mode 100644 index 0000000..245c62b --- /dev/null +++ b/src/im-sdk/assets/cancelBtn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/emoji.svg b/src/im-sdk/assets/emoji.svg new file mode 100644 index 0000000..b9735c5 --- /dev/null +++ b/src/im-sdk/assets/emoji.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/expand.svg b/src/im-sdk/assets/expand.svg new file mode 100644 index 0000000..5e28a97 --- /dev/null +++ b/src/im-sdk/assets/expand.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/im-sdk/assets/finish.svg b/src/im-sdk/assets/finish.svg new file mode 100644 index 0000000..dc27ea3 --- /dev/null +++ b/src/im-sdk/assets/finish.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/im-sdk/assets/full.svg b/src/im-sdk/assets/full.svg new file mode 100644 index 0000000..2da5fce --- /dev/null +++ b/src/im-sdk/assets/full.svg @@ -0,0 +1,91 @@ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/im-sdk/assets/giftCall.svg b/src/im-sdk/assets/giftCall.svg new file mode 100644 index 0000000..4d33c87 --- /dev/null +++ b/src/im-sdk/assets/giftCall.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/gold.svg b/src/im-sdk/assets/gold.svg new file mode 100644 index 0000000..83e7626 --- /dev/null +++ b/src/im-sdk/assets/gold.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/im-sdk/assets/hollow.svg b/src/im-sdk/assets/hollow.svg new file mode 100644 index 0000000..5844efe --- /dev/null +++ b/src/im-sdk/assets/hollow.svg @@ -0,0 +1,84 @@ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/im-sdk/assets/keyboard.svg b/src/im-sdk/assets/keyboard.svg new file mode 100644 index 0000000..dbda0ed --- /dev/null +++ b/src/im-sdk/assets/keyboard.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/leftLove.svg b/src/im-sdk/assets/leftLove.svg new file mode 100644 index 0000000..74153b9 --- /dev/null +++ b/src/im-sdk/assets/leftLove.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/im-sdk/assets/loading.svg b/src/im-sdk/assets/loading.svg new file mode 100644 index 0000000..2be57ce --- /dev/null +++ b/src/im-sdk/assets/loading.svg @@ -0,0 +1,13 @@ + + + + + + +
+
+ +
+ + +
diff --git a/src/im-sdk/assets/man.svg b/src/im-sdk/assets/man.svg new file mode 100644 index 0000000..6d98412 --- /dev/null +++ b/src/im-sdk/assets/man.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/im-sdk/assets/microphone.svg b/src/im-sdk/assets/microphone.svg new file mode 100644 index 0000000..a2a08de --- /dev/null +++ b/src/im-sdk/assets/microphone.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/microphoneClose.svg b/src/im-sdk/assets/microphoneClose.svg new file mode 100644 index 0000000..220624a --- /dev/null +++ b/src/im-sdk/assets/microphoneClose.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/moreBtn.svg b/src/im-sdk/assets/moreBtn.svg new file mode 100644 index 0000000..11fcded --- /dev/null +++ b/src/im-sdk/assets/moreBtn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/noData.svg b/src/im-sdk/assets/noData.svg new file mode 100644 index 0000000..6a5a398 --- /dev/null +++ b/src/im-sdk/assets/noData.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/pauseVoice.svg b/src/im-sdk/assets/pauseVoice.svg new file mode 100644 index 0000000..f0db6ff --- /dev/null +++ b/src/im-sdk/assets/pauseVoice.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/im-sdk/assets/payMenu.svg b/src/im-sdk/assets/payMenu.svg new file mode 100644 index 0000000..6f1ee49 --- /dev/null +++ b/src/im-sdk/assets/payMenu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/im-sdk/assets/picMenu.svg b/src/im-sdk/assets/picMenu.svg new file mode 100644 index 0000000..52db8e9 --- /dev/null +++ b/src/im-sdk/assets/picMenu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/im-sdk/assets/playVoice.svg b/src/im-sdk/assets/playVoice.svg new file mode 100644 index 0000000..7b8bdb3 --- /dev/null +++ b/src/im-sdk/assets/playVoice.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/im-sdk/assets/read.svg b/src/im-sdk/assets/read.svg new file mode 100644 index 0000000..cbf9391 --- /dev/null +++ b/src/im-sdk/assets/read.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/refuse.svg b/src/im-sdk/assets/refuse.svg new file mode 100644 index 0000000..2043e3e --- /dev/null +++ b/src/im-sdk/assets/refuse.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/im-sdk/assets/remark.svg b/src/im-sdk/assets/remark.svg new file mode 100644 index 0000000..ca3b6c5 --- /dev/null +++ b/src/im-sdk/assets/remark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/report.svg b/src/im-sdk/assets/report.svg new file mode 100644 index 0000000..4c877a7 --- /dev/null +++ b/src/im-sdk/assets/report.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/reversalTools.svg b/src/im-sdk/assets/reversalTools.svg new file mode 100644 index 0000000..49a9a62 --- /dev/null +++ b/src/im-sdk/assets/reversalTools.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/rightLove.svg b/src/im-sdk/assets/rightLove.svg new file mode 100644 index 0000000..3180d06 --- /dev/null +++ b/src/im-sdk/assets/rightLove.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/im-sdk/assets/scaleTools.svg b/src/im-sdk/assets/scaleTools.svg new file mode 100644 index 0000000..ffdb2f0 --- /dev/null +++ b/src/im-sdk/assets/scaleTools.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/send.svg b/src/im-sdk/assets/send.svg new file mode 100644 index 0000000..bedab56 --- /dev/null +++ b/src/im-sdk/assets/send.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/sendFail.svg b/src/im-sdk/assets/sendFail.svg new file mode 100644 index 0000000..2194451 --- /dev/null +++ b/src/im-sdk/assets/sendFail.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/im-sdk/assets/sendImg.svg b/src/im-sdk/assets/sendImg.svg new file mode 100644 index 0000000..0452965 --- /dev/null +++ b/src/im-sdk/assets/sendImg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/sendV.svg b/src/im-sdk/assets/sendV.svg new file mode 100644 index 0000000..9dbbbac --- /dev/null +++ b/src/im-sdk/assets/sendV.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/sendVBtn.svg b/src/im-sdk/assets/sendVBtn.svg new file mode 100644 index 0000000..223c898 --- /dev/null +++ b/src/im-sdk/assets/sendVBtn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/shutVideo.svg b/src/im-sdk/assets/shutVideo.svg new file mode 100644 index 0000000..e2fd2a6 --- /dev/null +++ b/src/im-sdk/assets/shutVideo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/speaker.svg b/src/im-sdk/assets/speaker.svg new file mode 100644 index 0000000..d0661e4 --- /dev/null +++ b/src/im-sdk/assets/speaker.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/im-sdk/assets/speakerClose.svg b/src/im-sdk/assets/speakerClose.svg new file mode 100644 index 0000000..c07ba3d --- /dev/null +++ b/src/im-sdk/assets/speakerClose.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/im-sdk/assets/toolbar.svg b/src/im-sdk/assets/toolbar.svg new file mode 100644 index 0000000..79eacdd --- /dev/null +++ b/src/im-sdk/assets/toolbar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/im-sdk/assets/videoMenu.svg b/src/im-sdk/assets/videoMenu.svg new file mode 100644 index 0000000..63ea0f8 --- /dev/null +++ b/src/im-sdk/assets/videoMenu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/im-sdk/assets/voice.svg b/src/im-sdk/assets/voice.svg new file mode 100644 index 0000000..343e449 --- /dev/null +++ b/src/im-sdk/assets/voice.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/im-sdk/assets/woman.svg b/src/im-sdk/assets/woman.svg new file mode 100644 index 0000000..b4c2ae5 --- /dev/null +++ b/src/im-sdk/assets/woman.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/im-sdk/callDataCenter.ts b/src/im-sdk/callDataCenter.ts new file mode 100644 index 0000000..19d9d08 --- /dev/null +++ b/src/im-sdk/callDataCenter.ts @@ -0,0 +1,835 @@ +import { reactive } from 'vue'; +import ImDataCenter from './ImDataCenter'; +import errorManager, { ImErrorCode } from './manager/errorManager'; +import { + CallAction, + CallMessage, + CallState, + CallType, + ImUserData, + MessageData, + MessageState, +} from './types'; +import { doWhile } from './utils'; + +/** + ** 呼叫流程 ** + caller --------------------------------------> callee + ----------callout--------------->响铃 + <-------------busy------- + <-------------refuse------- + <-------------accept(创建房间并进入)------- + ----------确定进入房间onEnter,计时---------------> +*/ + +export interface CallListener { + onLogin(): void; + onCall: (data: CallMessage) => void; + onCancel: (data: CallMessage) => void; + /**被对方接受 */ + onAccepted: (data: CallMessage) => void; + /**被对方拒绝 */ + onRefused: (data: CallMessage) => void; + /**对方忙碌 */ + onBusy: (data: CallMessage) => void; + /**完成通话 */ + onFinished: (data: CallMessage) => void; + onEnter: (data: CallMessage) => void; + /**视频链接超时 */ + onTimeout: (data: CallMessage) => void; + /**视频转语音 */ + onVideo2Vioce: (data: CallMessage) => void; + /**异常中止,用于修复状态 */ + onAbort: (data: CallMessage) => void; +} +export interface CallAdapter { + callListener: CallListener; + login(userId: string, token: string): Promise; + logout(): void; + /**回应对方 */ + react( + data: CallMessage, + action: CallAction + ): Promise<{ id: string; success: boolean }>; + addCallListener(listener: CallListener): void; + getChannel(): Promise; + joinChannel(channel: string, callType: CallType): Promise; + exitChannel(): void; + fetchUser(imId: string): Promise; +} + +interface CallData { + state: CallState; + isCallOut: boolean; + /**聊天类型 */ + type: CallType; + mine?: ImUserData; + target?: ImUserData; +} +class CallDataCenter implements CallListener { + private adapter!: CallAdapter; + private isLogin = false; + + userId!: string; + token!: string; + + /**是不是群聊 */ + isGroup = false; + /**目标用户,如果是群的话,就是聊id */ + target?: string; + /**房间成员 */ + users: string[] = []; + /**响铃的时间 */ + ringTime = 0; + + message?: CallMessage; + + data: CallData = reactive({ + state: CallState.idle, + isCallOut: false, + type: 'video', + }); + + /**电话等待时间 */ + static ringTime = 30000; + /**视频语音流等待时间 */ + static streamWaitTime = 10000; + + /**定时器 */ + private timer?: NodeJS.Timeout; + + /**计时判断对方是否进入视频流,如果没有进入则主动关闭 */ + private streamTimer?: NodeJS.Timeout; + /**等待对方流的接入 */ + waitSteam = false; + + constructor() {} + + login(userId: string, token: string) { + this.userId = userId; + this.token = token; + this.init(); + this.adapter.login(userId, token); + this.isLogin = true; + this.adapter.fetchUser(this.userId).then((resp) => { + this.data.mine = resp; + }); + } + async logout() { + if (this.data.state === CallState.calling) { + if (this.message) { + this.sendMessage(this.message, CallAction.cancel); + } + } else if (this.data.state === CallState.alerting) { + if (this.message) { + this.sendMessage(this.message, CallAction.refuse); + } + } else if (this.data.state === CallState.talking) { + if (this.message) { + this.sendMessage(this.message, CallAction.finish); + } + } + this.adapter.logout(); + this.isLogin = false; + this.reset(); + } + onLogin() { + if (!this.message) return; + this.target = this.message.target; + this.data.type = this.message.callType; + this.adapter.fetchUser(this.message.target).then((resp) => { + this.data.target = resp; + }); + + if (this.message.result === 'calling') { + this.data.state = CallState.calling; + this.data.isCallOut = true; + this.ringTime = this.message.time; + const leftTime = CallDataCenter.ringTime - (Date.now() - this.ringTime); + this.timer = setTimeout(() => { + this.timer = undefined; + this.cancel(); + }, leftTime); + } else if (this.message.result === 'ring') { + this.data.state = CallState.alerting; + this.data.isCallOut = false; + this.ringTime = this.message.time; + this.timer = setTimeout(() => { + this.timer = undefined; + this.refuse(); + }, CallDataCenter.ringTime - (Date.now() - this.ringTime)); + } else if ( + this.message.result === 'success' || + this.message.result === 'wait' + ) { + this.data.state = CallState.talking; + const channel = this.message.channel; + if (channel) { + this.adapter + .joinChannel(channel, this.message.callType) + .then((success) => { + if (!success) { + // 通知恢复通话失败,自动中止 + errorManager.emit({ + code: ImErrorCode.resumeCallFailed, + data: { + state: CallState.alerting, + target: this.target, + message: this.message, + }, + }); + this.finish(); + } + }); + } else { + this.finish(); + } + } else { + this.reset(); + } + } + + async init() { + // 查询上一次通话信息,重建通话状态 + const lastCallMessage = this.readTemp(); + if (lastCallMessage) { + if ( + ['busy', 'cancel', 'failed', 'finish', 'refuse'].includes( + lastCallMessage.result + ) + ) { + return; + } + this.message = lastCallMessage; + } + } + + setAdapter(adapter: CallAdapter) { + this.adapter = adapter; + this.adapter.addCallListener(this); + } + private reset() { + this.clearWaitTimer(); + if (this.streamTimer) { + clearTimeout(this.streamTimer); + } + this.streamTimer = undefined; + this.target = undefined; + this.data.state = CallState.idle; + this.message = undefined; + this.saveTemp(); + } + /**呼叫对方 */ + async call(target: string, type: CallType) { + if (this.data.state !== CallState.idle) { + // 不是空闲状态不应该呼叫对方 + return; + } + this.data.type = type; + this.data.isCallOut = true; + const message: MessageData = { + id: '', + target, + from: this.userId, + to: target, + time: Date.now(), + isRead: false, + state: MessageState.sending, + type: 'call', + result: 'calling', + callId: '', + callType: type, + start: Date.now(), + }; + this.message = message; + this.target = target; + + this.adapter.fetchUser(this.message.target).then((resp) => { + this.data.target = resp; + }); + // 暂时不要这个了,不然会看见正在呼叫中的消息不好删除 + // ImDataCenter.onMessage({ ...message }); + const ret = await this.sendMessage(message, CallAction.callout); + if (ret.success) { + this.data.state = CallState.calling; + message.id = ret.id; + message.state = MessageState.sending; + message.callId = ret.id; + ImDataCenter.onMessage({ ...message }); + this.saveTemp(); + this.ringTime = Date.now(); + this.timer = setTimeout(() => { + this.timer = undefined; + this.cancel(); + }, CallDataCenter.ringTime); + } else { + this.reset(); + } + } + private clearWaitTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } + } + /**取消请求 */ + async cancel() { + // 兼容一下,如果点取消的同时,收到确定,就直接到完成 + if (this.data.state === CallState.talking) { + return this.finish(); + } + if (this.data.state !== CallState.calling) { + // 不是响铃状态不应该取消呼叫 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) return; + if (message.type === 'call') { + message.result = 'cancel'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + } + this.reset(); + // 重试直到超时 + doWhile(async () => { + if (!this.isLogin) return true; + if (Date.now() - this.ringTime > CallDataCenter.ringTime) { + return true; + } + return (await this.sendMessage(message, CallAction.cancel)).success; + }, 1000); + } + /**接受对方 */ + async accept() { + const target = this.target; + const message = this.message; + if (!target || !message) return; + // 考虑时间,比如在waitime提前一点,不然另外边可能认为已经挂了 + if (this.data.state === CallState.alerting) { + const channel = await this.adapter.getChannel(); + if (!channel) { + // 抛出事件,获取房间号失败,请重试 + errorManager.emit({ code: ImErrorCode.getRoomIdFailed }); + message.result = 'abort'; + ImDataCenter.onMessage({ ...message }); + this.sendMessage(message, CallAction.abort); + this.adapter.exitChannel(); + return; + } + const success = await this.adapter.joinChannel(channel, message.callType); + if (!success) { + // 抛出事件,进入房间失败失败,请重试 + errorManager.emit({ + code: ImErrorCode.enterRoomFailed, + data: { channel, callType: message.callType }, + }); + message.result = 'abort'; + ImDataCenter.onMessage({ ...message }); + this.sendMessage(message, CallAction.abort); + this.adapter.exitChannel(); + this.reset(); + return; + } + if (this.data.state !== CallState.alerting) { + // 加入房间的途中,被对方cancel了 + this.adapter.exitChannel(); + this.reset(); + return; + } + this.data.state = CallState.talking; + message.result = 'wait'; + message.time = Date.now(); + message.channel = channel; + ImDataCenter.onMessage({ ...message }); + this.saveTemp(); + this.clearWaitTimer(); + // 重试直到超时 + await doWhile(async () => { + if (this.data.state !== CallState.talking) return true; + return (await this.sendMessage(message, CallAction.accept)).success; + }, 1000); + this.waitSteam = true; + this.streamTimer = setTimeout(() => { + this.streamTimer = undefined; + this.timeout(); + }, CallDataCenter.streamWaitTime); + } else { + // 没有在响铃中,不应该触发主动调用拒绝 + } + } + /**拒绝对方 */ + async refuse() { + if (this.data.state === CallState.alerting) { + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.alerting, + action: 'refuse', + target, + message, + }, + }); + return; + } + this.data.state = CallState.idle; + if (message.type === 'call') { + message.result = 'refuse'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + } + const time = this.ringTime; + this.reset(); + // 重试直到超时 + await doWhile(async () => { + if (!this.isLogin) return true; + if (Date.now() - time > CallDataCenter.ringTime) { + return true; + } + return (await this.sendMessage(message, CallAction.refuse)).success; + }, 1000); + } else { + // 没有在响铃中,不应该触发主动调用拒绝 + } + } + /**进入房间超时 */ + async timeout() { + if (this.data.state !== CallState.talking) { + // 都没有进入通话,不应该有超时 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { state: CallState.talking, action: 'timeout', target, message }, + }); + return; + } + message.result = 'failed'; + message.time = Date.now(); + message.end = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.adapter.exitChannel(); + this.reset(); + await doWhile(async () => { + if (!this.isLogin) return true; + if (this.data.state !== CallState.idle) return true; + return (await this.sendMessage(message, CallAction.timeout)).success; + }, 1000); + } + /**结束通话 */ + async finish() { + if (this.data.state !== CallState.talking) { + // 都没有进入通话,不应该有结束的 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { state: CallState.talking, action: 'finish', target, message }, + }); + return; + } + message.result = 'finish'; + message.time = Date.now(); + this.reset(); + message.end = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.adapter.exitChannel(); + await doWhile(async () => { + if (!this.isLogin) return true; + if (this.data.state !== CallState.idle) return true; + return (await this.sendMessage(message, CallAction.finish)).success; + }, 1000); + } + /**视频转语音通话 */ + async video2voice() { + if (this.data.state !== CallState.talking) { + // 都没有进入通话,不应该有视频切换 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.talking, + action: 'video2voice', + target, + message, + }, + }); + return; + } + this.sendMessage(message, CallAction.video2vioce); + } + // 以下为潍坊给的事件 + /**收到呼叫 */ + async onCall(data: CallMessage) { + const message: CallMessage = { + id: data.id, + target: data.from!, + from: data.from!, + to: this.userId, + time: Date.now(), + isRead: false, + state: MessageState.sended, + type: 'call', + result: 'ring', + callType: data.callType, + callId: data.id, + }; + this.data.type = data.callType; + if (this.data.state === CallState.idle) { + this.target = data.target; + this.data.isCallOut = false; + this.ringTime = data.time; + this.data.state = CallState.alerting; + this.message = message; + ImDataCenter.onMessage({ ...message }); + this.saveTemp(); + this.timer = setTimeout(() => { + this.timer = undefined; + this.refuse(); // todo 先暂时用拒绝,应该是无人应答才对 + }, CallDataCenter.ringTime - (Date.now() - data.time)); + this.adapter.fetchUser(this.message.target).then((resp) => { + this.data.target = resp; + }); + } else if (this.target !== data.target) { + await doWhile(async () => { + if (!this.isLogin) return true; + return (await this.sendMessage(message, CallAction.busy)).success; + }, 1000); + } else { + // 说明是同一个人呢发过来的多次呼叫,忽略 + } + } + /**发起方取消 */ + async onCancel(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.alerting) { + // 说明刚刚发起方已经取消了,或者超时了 + return; + } + if (data.from !== this.target) { + // 防御性代码 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { state: CallState.calling, action: 'onCancel', target, message }, + }); + return; + } + message.result = 'cancel'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.reset(); + await doWhile(async () => { + if (!this.isLogin) return true; + return (await this.sendMessage(message, CallAction.cancel)).success; + }, 1000); + } + /**被对方接受 */ + async onAccepted(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.calling) { + // 说明刚刚发起方已经取消了,或者超时了 + return; + } + if (data.from !== this.target) { + // 防御性代码 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.calling, + action: 'onAccepted', + target, + message, + }, + }); + return; + } + message.channel = data.channel; + if (!message.channel) { + // 抛出异常,通道异常不存在 + errorManager.emit({ + code: ImErrorCode.roomIdNotFound, + data: { state: CallState.calling, target, message }, + }); + return; + } + this.clearWaitTimer(); + const success = await this.adapter.joinChannel( + message.channel, + message.callType + ); + if (!success) { + // 抛出事件,进入房间失败失败,请重试 + errorManager.emit({ + code: ImErrorCode.joinRoomFailed, + data: { state: CallState.calling, target, message }, + }); + message.result = 'abort'; + ImDataCenter.onMessage({ ...message }); + this.sendMessage(message, CallAction.abort); + this.adapter.exitChannel(); + this.reset(); + return; + } + if (this.data.state !== CallState.calling) { + // 加入房间的间隙中,被取消了 + this.adapter.exitChannel(); + this.reset(); + return; + } + message.result = 'success'; + message.time = Date.now(); + message.start = Date.now(); + this.data.state = CallState.talking; + ImDataCenter.onMessage({ ...message }); + this.saveTemp(); + await doWhile(async () => { + if (!this.isLogin) return true; + return (await this.sendMessage(message, CallAction.enter)).success; + }, 1000); + } + /**被对方拒绝 */ + onRefused(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.calling) { + // 说明刚刚发起方已经取消了,或者超时了 + return; + } + if (data.from !== this.target) { + // 防御性代码 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.calling, + action: 'onRefused', + target, + message, + }, + }); + return; + } + message.result = 'refuse'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.reset(); + } + /**被对方拒绝 */ + onBusy(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.calling) { + // 说明刚刚发起方已经取消了,或者超时了 + return; + } + if (data.from !== this.target) { + // 防御性代码 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { state: CallState.calling, action: 'onBusy', target, message }, + }); + return; + } + message.result = 'busy'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.reset(); + } + /**完成通话 */ + onFinished(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.talking) { + // 说明我可能主动关闭了 + return; + } + if (data.from !== this.target) { + // 防御性代码 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.talking, + action: 'onFinished', + target, + message, + }, + }); + return; + } + message.end = data.end; + message.result = 'finish'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.adapter.exitChannel(); + this.reset(); + } + + onEnter(data: CallMessage) { + this.checkAbort(data); + if (this.data.state !== CallState.talking) { + // 如果不是会话状态 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { state: CallState.talking, action: 'onEnter', target, message }, + }); + return; + } + if (data.from === this.target) { + this.waitSteam = false; + clearTimeout(this.streamTimer); + this.streamTimer = undefined; + + message.start = data.start; + message.result = 'success'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.saveTemp(); + } + } + onTimeout(data: CallMessage) { + if (this.data.state !== CallState.talking) { + // 如果不是会话状态 + + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.talking, + action: 'onTimeout', + target, + message, + }, + }); + return; + } + this.adapter.exitChannel(); + if (data.from === this.target) { + this.waitSteam = false; + clearTimeout(this.streamTimer); + this.streamTimer = undefined; + + message.result = 'failed'; + message.time = Date.now(); + ImDataCenter.onMessage({ ...message }); + this.reset(); + } + } + onAbort(data: CallMessage) { + if (this.data.state !== CallState.idle) { + // 直接终止 + const message = this.message; + if (message) { + // 说明可能是其他的历史消息来的 + if (data.target !== message.target) return; + message.result = 'abort'; + ImDataCenter.onMessage({ ...message }); + } + this.reset(); + this.adapter.exitChannel(); + } + } + onVideo2Vioce(data: CallMessage) { + if (this.data.state !== CallState.talking) { + // 如果不是会话状态 + return; + } + const target = this.target; + const message = this.message; + if (!target || !message) { + errorManager.emit({ + code: ImErrorCode.callDataException, + data: { + state: CallState.talking, + action: 'onVideo2Vioce', + target, + message, + }, + }); + return; + } + if (data.from === this.target) { + this.data.type = 'audio'; + } + } + + private sendMessage(message: CallMessage, action: CallAction) { + // 需要在这里调个头,原因是本地的消息要保持说明是谁给谁打的,但是发送出去的消息是我发给对面 + return this.adapter.react( + { ...message, from: this.userId, to: message.target! }, + action + ); + } + + private checkAbort(data: CallMessage) { + if (data.from !== this.target) { + data.result = "abort" + this.sendMessage(data, CallAction.abort); + } + } + + private saveTemp() { + if (this.message) { + localStorage.setItem( + `CALL-MESSAGE:${this.userId}`, + JSON.stringify(this.message) + ); + } else { + localStorage.removeItem(`CALL-MESSAGE:${this.userId}`); + } + } + private readTemp() { + const str = localStorage.getItem(`CALL-MESSAGE:${this.userId}`); + if (str) { + try { + return JSON.parse(str) as CallMessage; + } catch (error) { + // 忽略 + } + } + } +} +/**视频语音通话的数据管理 */ +export default new CallDataCenter(); diff --git a/src/im-sdk/components/CacheImg.vue b/src/im-sdk/components/CacheImg.vue new file mode 100644 index 0000000..711cf76 --- /dev/null +++ b/src/im-sdk/components/CacheImg.vue @@ -0,0 +1,21 @@ + + diff --git a/src/im-sdk/components/Chat.vue b/src/im-sdk/components/Chat.vue new file mode 100644 index 0000000..d6c3b23 --- /dev/null +++ b/src/im-sdk/components/Chat.vue @@ -0,0 +1,146 @@ + + + + diff --git a/src/im-sdk/components/Conversation.vue b/src/im-sdk/components/Conversation.vue new file mode 100644 index 0000000..9becd08 --- /dev/null +++ b/src/im-sdk/components/Conversation.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/im-sdk/config/default.ts b/src/im-sdk/config/default.ts new file mode 100644 index 0000000..7a9de87 --- /dev/null +++ b/src/im-sdk/config/default.ts @@ -0,0 +1,18 @@ +import SendPrivateImg from '@/im-sdk/widget/menuModule/SendPrivateImg.vue'; +import SendPrivatePayImg from '@/im-sdk/widget/menuModule/SendPrivatePayImg.vue'; +import ChatItem from '../widget/ChatItem.vue'; +import ConversationItem from '../widget/ConversationItem.vue'; +import ChatCallVideo from '../widget/menuModule/ChatCallVideo.vue'; +import ChatCallVoice from '../widget/menuModule/ChatCallVoice.vue'; +import type { Component } from 'vue'; + +export const ConversationItemRender = ConversationItem; + +export const MessageItemRender = ChatItem; + +export const ChatMenus: Component<{ id: string }>[] = [ + SendPrivateImg, + ChatCallVoice, + ChatCallVideo, + // SendPrivatePayImg, +]; diff --git a/src/im-sdk/config/index.ts b/src/im-sdk/config/index.ts new file mode 100644 index 0000000..73c9faa --- /dev/null +++ b/src/im-sdk/config/index.ts @@ -0,0 +1,34 @@ +/**im的配置 */ + +import type { Component } from 'vue'; +import { + ConversationData, + ImUserData, + MessageData, + MessageTempData, +} from '../types'; + +export type MessageRenderType = Pick['type']; + +export enum HooksKey {} +class ImConfig { + /**聊天时候的菜单 */ + chatMenus: Component<{ id: string }>[] = []; + + conversationRender?: Component<{ data: ConversationData }>; + + messageItemRender?: Component<{ data: MessageData }>; + + /**进入与谁的聊天 */ + onChatWith?: (user?: string) => void; + /**查看某人资料 */ + onViewUser?: (user?: ImUserData) => void; + /**发送消息前校验,如果返回false,则中止发送 */ + onBeforeSendMessage?: (m: MessageTempData) => Promise; + onAfterSendMessage?: (m: MessageData) => void; + + /**会话列表加载完成,可以用来做预加载聊天的代码 */ + onLoadConversationView?: () => void; +} + +export default new ImConfig(); diff --git a/src/im-sdk/db/chatDB.ts b/src/im-sdk/db/chatDB.ts new file mode 100644 index 0000000..2d2f0d7 --- /dev/null +++ b/src/im-sdk/db/chatDB.ts @@ -0,0 +1,104 @@ +// db/chatDB.ts +import Dexie, { Table } from 'dexie'; +import { + MessageData, + ConversationData, + ReadMessage, + ClearUnreadMessage, +} from '../types'; +import { toRaw } from 'vue'; + +interface DelaySaver { + promise?: Promise; + unsaves: T[]; +} + +const dbMap: Record = {}; +export class ChatDB extends Dexie { + messages!: Table; // 主键为 message.id + conversations!: Table; // 主键为 conversation.id + + private id: string; + + constructor(dbId: string) { + super(dbId); + this.id = dbId; + this.version(1).stores({ + messages: 'id, target, time, to, [target+time]', + conversations: 'id, type', + }); + } + close(closeOptions?: { disableAutoOpen: boolean } | undefined): void { + super.close(closeOptions); + delete dbMap[this.id]; + } + /**延迟到微任务完成后再批量保存,这样可以将循环中的处理后置 */ + private messageSaver: DelaySaver = { + unsaves: [], + }; + private readMessages: ReadMessage[] = []; + private clearUnreadMessage?: ClearUnreadMessage; + saveMessage(message: MessageData) { + /**如果没有id,说明是临时消息,先不用保存到本地 */ + if (!message.id) return; + if (message.type === 'read') { + this.readMessages.push(toRaw(message)); + } else if (message.type === 'clearUnread') { + this.clearUnreadMessage = message; + } else { + this.messageSaver.unsaves.push(toRaw(message)); + } + if (!this.messageSaver.promise) { + this.messageSaver.promise = Promise.resolve().then(() => { + this.messageSaver.promise = undefined; + this.messages.bulkPut(this.messageSaver.unsaves); + this.messageSaver.unsaves = []; + this.readMessages.forEach((message) => { + this.messages + .where('id') + .equals(message.messageId) + .modify({ isRead: true }); + }); + this.readMessages = []; + const message = this.clearUnreadMessage; + if (message) { + this.messages + .where(['target', 'time']) // 使用复合索引 + .between( + [message.target, 0], + [message.target, message.time], + true, + true + ) + .modify({ isRead: true }); + } + this.clearUnreadMessage = undefined; + }); + } + } + /**延迟到微任务完成后再批量保存,这样可以将循环中的处理后置 */ + private conversationSaver: DelaySaver = { + unsaves: [], + }; + saveConversation(conversation: ConversationData) { + this.conversationSaver.unsaves.push(toRaw(conversation)); + if (!this.conversationSaver.promise) { + this.conversationSaver.promise = Promise.resolve().then(() => { + this.conversationSaver.promise = undefined; + this.conversations.bulkPut(this.conversationSaver.unsaves); + this.conversationSaver.unsaves = []; + }); + } + } +} + +export function getChatDB(userId: number | string) { + const key = `user-${userId}`; + const db = dbMap[key]; + if (db) { + return db; + } + const newDb = new ChatDB(key); + dbMap[key] = newDb; + return newDb; +} diff --git a/src/im-sdk/entity/ConversationController.ts b/src/im-sdk/entity/ConversationController.ts new file mode 100644 index 0000000..474dbbc --- /dev/null +++ b/src/im-sdk/entity/ConversationController.ts @@ -0,0 +1,335 @@ +import { reactive } from 'vue'; +import { BaseAdapter } from '../BaseAdapter'; +import config from '../config'; +import { ChatDB } from '../db/chatDB'; +import { + BaseMessage, + ClearUnreadMessage, + ConversationData, + MessageData, + MessageState, + MessageTempData, + ReadMessage, +} from '../types'; +import { readImageFile, readVideoFile } from '../utils'; +import { generateBrief } from '../utils/message'; +import { toRaw } from 'vue'; +import errorManager, { ImErrorCode } from '../manager/errorManager'; + +const INT_MAX = 2 ** 32; +export class ConversationController { + messages: MessageData[] = reactive([]); + private db: ChatDB; + conversation: ConversationData; + private adapter: BaseAdapter; + private sorter: (id?: string) => void; + + private timeCousr = INT_MAX * 1000; + private loadMoreId = 0; + private noMore = false; + private isInited = false; + private isLogin = false; + + /**我的未读列表,用来记录我是否读取过,避免重复发送 */ + private unReadSet = new Set(); + + constructor( + db: ChatDB, + conversation: ConversationData, + adapter: BaseAdapter, + sorter: (id?: string) => void + ) { + this.db = db; + this.conversation = conversation; + this.adapter = adapter; + this.sorter = sorter; + } + updateDb(db: ChatDB) { + this.db = db; + } + + async loadLocalMessage() { + if (this.isInited) return; + this.isInited = true; + const dataList = await this.loadLocalHistory(); + this.timeCousr = dataList[0]?.time || 0; + this.messages.unshift(...dataList); + if (this.isLogin) { + if (this.messages.length < 1) { + this.loadMore(); + } + } + } + + onLogin() { + if (this.messages.length < 1 && this.isInited) { + this.loadMore(); + } + this.isLogin = true; + } + + private async loadLocalHistory() { + const list = await this.db.messages + .where(['target', 'time']) // 使用复合索引 + .between( + [this.conversation.id, 0], + [this.conversation.id, this.timeCousr], + true, + false + ) + .reverse() // **关键步骤:将结果按 time 倒序排列** + .limit(50) + .toArray(); + return list.reverse(); + } + + async loadMore() { + if (this.noMore) { + return; + } + const id = ++this.loadMoreId; + const dataList = await this.loadLocalHistory(); + if (id !== this.loadMoreId) { + // 说明是重复请求,丢弃上一次的请求 + return; + } + if (dataList.length > 0) { + this.timeCousr = dataList[0].time; + this.messages.unshift(...dataList); + } else { + // 远程拉取,并保存 + try { + const resp = await this.adapter.fetchMessageList( + this.conversation.id, + this.messages[0]?.id || '' + ); + if (resp.list) { + this.messages.unshift(...resp.list); + resp.list.forEach((message) => { + this.db.saveMessage(message); + }); + } + this.noMore = !!resp.isEnd; + } catch (error) { + errorManager.emit({ + code: ImErrorCode.loadRemoteMessageError, + error: error, + }); + } + } + } + + private async sendMessage(temp: MessageTempData) { + if (config.onBeforeSendMessage) { + try { + const isOk = await config.onBeforeSendMessage(temp); + if (!isOk) { + return; + } + } catch (error) { + console.log('执行消息发送前置检查失败'); + } + } + this.messages.push(temp.message); + let ret: MessageData | undefined; + try { + ret = await this.adapter.sendMessage(temp); + } catch (error) { + errorManager.emit({ + code: ImErrorCode.sendMessageError, + data: temp.message, + error, + }); + return; + } + const index = this.messages.lastIndexOf(temp.message); + if (ret) { + if (index >= 0) { + // 替换原本的临时消息 + this.messages.splice(index, 1, ret); + } + if (temp.blob && 'url' in temp.message) { + if (temp.message.url?.startsWith('blob')) { + URL.revokeObjectURL(temp.message.url); + } + } + this.db.saveMessage(ret); + this.onConversationMessage(ret); + config.onAfterSendMessage?.(ret); + // 调整排序 + this.sorter(this.conversation.id); + } else { + errorManager.emit({ + code: ImErrorCode.sendMessageFailed, + data: temp.message, + }); + // 发送失败,先简单处理 todo + if (index >= 0) { + // 替换原本的临时消息 + this.messages.splice(index, 1); + + if (temp.blob && 'url' in temp.message) { + if (temp.message.url?.startsWith('blob')) { + URL.revokeObjectURL(temp.message.url); + } + } + } + } + } + + /**给会话设置最新的数据 */ + private onConversationMessage(message: MessageData) { + this.conversation.time = message.time; + this.conversation.last = generateBrief(message); + this.db.saveConversation(toRaw(this.conversation)); + } + + clearUnread() { + if (this.conversation.unReadCount < 1) return; + + const base: BaseMessage = this.createBaseMessage(); + const message: ClearUnreadMessage = { + id: '', + ...base, + type: 'clearUnread', + }; + return this.adapter.sendMessage({ message }).then(() => { + this.conversation.unReadCount = 0; + this.unReadSet.clear(); + this.db.saveConversation(toRaw(this.conversation)); + }); + } + + private createBaseMessage(): BaseMessage { + return { + target: this.conversation.id, + to: this.conversation.id, + time: Date.now(), + isRead: false, + state: MessageState.sending, + }; + } + + /**单独阅读了某条消息 */ + async sendReadMessage(target: MessageData) { + if (!this.unReadSet.has(target.id)) { + return; + } + const base: BaseMessage = this.createBaseMessage(); + const message: ReadMessage = { + id: '', + ...base, + type: 'read', + messageId: target.id, + }; + await this.adapter.sendMessage({ message }).then(() => { + this.conversation.unReadCount--; + this.unReadSet.delete(target.id); + this.db.saveConversation(toRaw(this.conversation)); + }); + } + + async sendTextMessage(msg: string, ext?: Record) { + const base: BaseMessage = this.createBaseMessage(); + const message: MessageData = { + id: '', + ...base, + type: 'txt', + msg, + ext, + }; + await this.sendMessage({ message }); + } + async sendImageMessage(file: File, ext?: Record) { + const base: BaseMessage = this.createBaseMessage(); + const info = await readImageFile(file); + const message: MessageData = { + id: '', + ...base, + type: 'img', + url: info.url, + width: info.width, + height: info.height, + length: file.size, + ext, + }; + await this.sendMessage({ message, blob: file }); + } + sendAudioMessage(file: Blob, duration: number, ext?: Record) { + const base: BaseMessage = this.createBaseMessage(); + const message: MessageData = { + id: '', + ...base, + type: 'audio', + url: URL.createObjectURL(file), + duration, + length: file.size, + ext, + }; + return this.sendMessage({ message, blob: file }); + } + async sendVideoMessage(file: File, ext?: Record) { + const base: BaseMessage = this.createBaseMessage(); + const info = await readVideoFile(file); + const message: MessageData = { + id: '', + ...base, + type: 'video', + url: info.url, + width: info.width, + height: info.height, + length: file.size, + duration: info.duration, + ext, + }; + await this.sendMessage({ message, blob: file }); + } + + onRecieve(data: MessageData) { + // 读取消息 + if (data.type === 'clearUnread') { + this.messages.forEach((message) => { + if (message.time < data.time) { + message.isRead = true; + } + }); + } else if (data.type === 'read') { + const message = this.messages.find( + (message) => message.id === data.messageId + ); + if (message) { + message.isRead = true; + } + } else { + if (data.type === 'call') { + const oldMessage = this.messages.find((m) => m.id === data.id); + if (oldMessage) { + Object.assign(oldMessage, data); + this.onConversationMessage(data); + this.sorter(this.conversation.id); + return; + } + } + this.messages.push(data); + this.conversation.unReadCount++; + this.unReadSet.add(data.id); + this.onConversationMessage(data); + // 调整排序 + this.sorter(this.conversation.id); + } + } + onMessageUpdate(data: MessageData) { + this.db.saveMessage(data); + if (data.id === this.messages[this.messages.length - 1]?.id) { + this.onConversationMessage(data); + } + } + + /**更新会话,同时可以判断是否需要拉取远端消息列表 */ + update(conversation: ConversationData) { + if (conversation.time > this.conversation.time) { + Object.assign(this.conversation, conversation); + this.db.saveConversation(this.conversation) + } + } +} diff --git a/src/im-sdk/entity/MessageBuffer.ts b/src/im-sdk/entity/MessageBuffer.ts new file mode 100644 index 0000000..5c935a2 --- /dev/null +++ b/src/im-sdk/entity/MessageBuffer.ts @@ -0,0 +1,34 @@ +import { ChatDB } from "../db/chatDB"; +import { MessageData } from "../types"; + +export type PipeFun = (message: MessageData) => void; +/**消息缓冲管理 + * 当远程的消息未初始化完的时候,新来的消息先到缓冲,这样可以保障消息的顺序性 + */ +export default class MessageBuffer { + private buffer: MessageData[] = []; + private isLoaded = false; + private pipe: PipeFun; + + private db: ChatDB; + + constructor(db: ChatDB, pipe: PipeFun) { + this.db = db; + this.pipe = pipe; + } + + onReady() { + this.isLoaded = true; + this.buffer.forEach((message) => this.pipe(message)); + this.buffer = []; + } + + onMessage(message: MessageData) { + this.db.saveMessage(message); + if (this.isLoaded) { + this.pipe(message); + } else { + this.buffer.push(message); + } + } +} diff --git a/src/im-sdk/manager/avatarManager.ts b/src/im-sdk/manager/avatarManager.ts new file mode 100644 index 0000000..bff6420 --- /dev/null +++ b/src/im-sdk/manager/avatarManager.ts @@ -0,0 +1,32 @@ +function createImagePlaceholder() { + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + return canvas.toDataURL(); +} + +class AvatarManager { + cache: Record | undefined> = {}; + defaultImg = createImagePlaceholder(); + async getImage(url?: string) { + if (!url) { + return this.defaultImg; + } + const image = this.cache[url]; + if (image) { + return image; + } + const p = new Promise((rs, rj) => { + fetch(url) + .then((r) => r.blob()) + .then((b) => rs(URL.createObjectURL(b))) + .catch((e) => { + this.cache[url] = undefined; + rj(e); + }); + }); + this.cache[url] = p; + return p; + } +} +export default new AvatarManager(); diff --git a/src/im-sdk/manager/errorManager.ts b/src/im-sdk/manager/errorManager.ts new file mode 100644 index 0000000..dd06581 --- /dev/null +++ b/src/im-sdk/manager/errorManager.ts @@ -0,0 +1,61 @@ +export interface ImEventData { + code: number; + data?: any; + error?: any; +} +type EventHandler = (data: ImEventData) => void; + +export enum ImErrorCode { + /**加载远程会话信息失败 */ + loadRemoteConversationError = 1, + /**加载远程聊天信息失败 */ + loadRemoteMessageError, + /**发送消息错误 */ + sendMessageError, + /**发送消息失败 */ + sendMessageFailed, + /**音视频聊天获取房间号失败 */ + getRoomIdFailed, + /**进入房间失败 */ + enterRoomFailed, + /**通话数据异常 */ + callDataException, + /**room不存在,在收到对方同意进入房间,但是对方没有生成房间id */ + roomIdNotFound, + /**加入房间失败 */ + joinRoomFailed, + /**恢复通话失败 */ + resumeCallFailed, +} + +class ImErrorEmitter { + private events: Set; + + constructor() { + this.events = new Set(); + } + + // 订阅事件 + on(handler: EventHandler): void { + this.events.add(handler); + } + + // 取消订阅事件 + off(handler: EventHandler): void { + this.events.delete(handler); + } + + // 触发事件 + emit(data: ImEventData): void { + const handlersCopy = Array.from(this.events); + for (const handler of handlersCopy) { + try { + handler(data); + } catch (error) { + console.log('执行im的监听时间异常', error); + } + } + } +} + +export default new ImErrorEmitter(); diff --git a/src/im-sdk/types.ts b/src/im-sdk/types.ts new file mode 100644 index 0000000..982253b --- /dev/null +++ b/src/im-sdk/types.ts @@ -0,0 +1,191 @@ +export type MsgType = + | 'txt' + | 'img' + | 'audio' + | 'video' + | 'file' + | 'loc' + | 'delivery' + | 'read' + | 'custom' + | 'call'; + +export type ConversationType = 'singleChat' | 'groupChat'; + +export enum MessageState { + sending = 0, + sended, + failed, + read, +} + +export interface MessageBrief { + /**消息id */ + id: string; + /**缩略信息,用于会话列表展示 */ + brief: string; + /**发送者 */ + sender?: string; +} +export interface ConversationData { + /**聊天对象,用户id或者群id */ + id: string; + type: ConversationType; + /**置顶 */ + isPinned: boolean; + unReadCount: number; + last?: MessageBrief; + time: number; + /**拓展的自定义数据 */ + ext?: Record; + user?: ImUserData; +} +export interface BaseMessage { + /**聊天对象 */ + target: string; + /**如果target===to,说明是我发给对方 */ + to: string; + time: number; + isRead: boolean; + state: MessageState; +} +export interface TxtContent { + type: 'txt'; + msg: string; +} +export interface ImgContent { + type: 'img'; + url?: string; + width: number; + height: number; + length: number; +} +export interface AudioContent { + type: 'audio'; + url?: string; + /**时长 */ + duration: number; + /**文件大小 */ + length: number; +} +export interface VideoContent { + type: 'video'; + url?: string; + width: number; + height: number; + /**时长 */ + duration: number; + /**文件大小 */ + length: number; +} +interface ReadContent { + type: 'read'; + messageId: string; +} +export type CallResult = + | 'calling' + | 'ring' + | 'cancel' + | 'refuse' + | 'busy' + | 'wait' + | 'success' + | 'finish' + | 'abort' //异常中止,往往是为了修复状态 + | 'failed'; // 通话失败,往往是超时没有进入房间 +export type CallType = 'audio' | 'video'; +export interface CallContent { + type: 'call'; + /**通话id,多个通话消息共用同一个消息id,方便更新 */ + callId: string; + result: CallResult; + callType: CallType; + /**通话通道,进入的房间 */ + channel?: string; + /**通话开始时间 */ + start?: number; + /**通话结束时间 */ + end?: number; +} +export interface ClearUnreadContent { + type: 'clearUnread'; +} +export type MessageContent = + | TxtContent + | ImgContent + | AudioContent + | VideoContent + | ReadContent + | CallContent + | ClearUnreadContent; +export interface MessageCommon { + id: string; + /**聊天对象,对应的是会话id */ + target: string; + /**如果是群聊,则会有这个字段 */ + from?: string; + /**如果target===to,说明是我发给对方 */ + to: string; + time: number; + /**我是否读取过 */ + isRead: boolean; + state: MessageState; + ext?: Record; + /**回复的消息id */ + replay?: string; +} +export type MessageData = MessageCommon & MessageContent; +export type ReadMessage = MessageCommon & ReadContent; +export type ClearUnreadMessage = MessageCommon & ClearUnreadContent; + +export interface MessageTempData { + message: MessageData; + blob?: Blob; +} + +/**用户系统,要做用户的头像URL.create */ +export interface ImUserData { + /**im的用户id */ + imId: string; + /**原本系统的用户id */ + userId: string; + name: string; + avatar: string; + /**不要存太多拓展的了,会存数据库 */ + [key: string]: any; +} + +export enum CallState { + /**闲置 */ + idle = 0, + /**请求中 */ + calling, + /**响铃 */ + alerting, + /**通话中 */ + talking, +} + +export enum CallAction { + /**对外呼叫 */ + callout = 0, + /**取消呼叫 */ + cancel, + /**接受 */ + accept, + /**拒绝 */ + refuse, + /**忙碌 */ + busy, + /**进入房间 */ + enter, + /**进入房间超时 */ + timeout, + /**通话完成 */ + finish, + /**异常终止,用来修复交互状态 */ + abort, + /**视频转语音 */ + video2vioce, +} +export type CallMessage = MessageCommon & CallContent; diff --git a/src/im-sdk/utils/emojis.ts b/src/im-sdk/utils/emojis.ts new file mode 100644 index 0000000..c2e2019 --- /dev/null +++ b/src/im-sdk/utils/emojis.ts @@ -0,0 +1,322 @@ +const emojis = [ + '😀', + '😃', + '😄', + '😁', + '😆', + '😅', + '🤣', + '😂', + '🙂', + '🙃', + '😉', + '😊', + '😇', + '😍', + '🤩', + '😘', + '😗', + '😚', + '😙', + '😋', + '😛', + '😜', + '🤪', + '😝', + '🤑', + '🤗', + '🤭', + '🤫', + '🤔', + '🤐', + '🤨', + '😐', + '😑', + '😶', + '😏', + '😒', + '🙄', + '😬', + '🤥', + '😌', + '😔', + '😪', + '🤤', + '😴', + '😷', + '🤒', + '🤕', + '🤢', + '🤮', + '🤧', + '😵', + '🤯', + '🤠', + '😎', + '🤓', + '🧐', + '😕', + '😟', + '🙁', + '😮', + '😯', + '😲', + '😳', + '😦', + '😧', + '😨', + '😰', + '😥', + '😢', + '😭', + '😱', + '😖', + '😣', + '😞', + '😓', + '😩', + '😫', + '😤', + '😡', + '😠', + '🤬', + '😈', + '👿', + '💀', + '💩', + '🤡', + '👹', + '👺', + '👻', + '👽', + '👾', + '🤖', + '😺', + '😸', + '😹', + '😻', + '😼', + '😽', + '🙀', + '😿', + '😾', + '💋', + '👋', + '🤚', + '🖐', + '✋', + '🖖', + '👌', + '🤞', + '🤟', + '🤘', + '🤙', + '👈', + '👉', + '👆', + '🖕', + '👇', + '👍', + '👎', + '✊', + '👊', + '🤛', + '🤜', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '💅', + '🤳', + '💪', + '👂', + '👃', + '🧠', + '👀', + '👁', + '👅', + '👄', + '👶', + '🧒', + '👦', + '👧', + '🧑', + '👱', + '👨', + '🧔', + '👱‍', + '👨‍', + '👨‍', + '👩', + '👱‍', + '👩‍', + '👩‍', + '👩‍', + '👩‍', + '🧓', + '👴', + '👵', + '🙍', + '🙅', + '🙆', + '💁', + '🙋', + '🙇', + '🙇‍', + '🙇‍', + '🤦', + '🤷', + '🤷‍', + '🤷‍', + '👨‍⚕️', + '👩‍⚕️', + '👨‍🎓', + '👩‍🎓', + '👨‍🏫', + '👩‍🏫', + '👨‍⚖️', + '👩‍⚖️', + '👨‍🌾', + '👩‍🌾', + '👨‍🍳', + '👩‍🍳', + '👨‍🔧', + '👩‍🔧', + '👨‍🏭', + '👩‍🏭', + '👨‍💼', + '👩‍💼', + '👨‍🔬', + '👩‍🔬', + '👨‍💻', + '👩‍💻', + '👨‍🎤', + '👩‍🎤', + '👨‍🎨', + '👩‍🎨', + '👨‍✈️', + '👩‍✈️', + '👨‍🚀', + '👩‍🚀', + '👨‍🚒', + '👩‍🚒', + '👮', + '👮‍♂️', + '👮‍♀️', + '🕵', + '🕵️‍♂️', + '🕵️‍♀️', + '💂', + '💂‍', + '💂‍', + '👷', + '👷‍', + '👷‍', + '🤴', + '👸', + '👳', + '👳‍', + '👳‍', + '👲', + '🧕', + '🤵', + '👰', + '🤰', + '🤱', + '👼', + '🎅', + '🤶', + '🧙', + '🧚', + '🧛', + '🧜', + '🧝', + '🧞', + '🧟', + '💆', + '💇', + '🚶', + '🏃', + '💃', + '🕺', + '🕴', + '👯', + '🧖', + '🧖‍', + '🧖‍', + '🧘', + '👭', + '👫', + '👬', + '💏', + '👨‍', + '👩‍', + '💑', + '👨‍', + '👩‍', + '👪', + '👨‍👩‍👦', + '👨‍👩‍👧', + '👨‍👩‍👧‍👦', + '👨‍👩‍👦‍👦', + '👨‍👩‍👧‍👧', + '👨‍👨‍👦', + '👨‍👨‍👧', + '👨‍👨‍👧‍👦', + '👩‍👩‍👦', + '👩‍👩‍👧', + '👩‍👩‍👧‍👦', + '👩‍👩‍👦‍👦', + '👩‍👩‍👧‍👧', + '👨‍👦', + '👨‍👦‍👦', + '👨‍👧', + '👨‍👧‍👦', + '👨‍👧‍👧', + '👩‍👦', + '👩‍👦‍👦', + '👩‍👧', + '👩‍👧‍👦', + '👩‍👧‍👧', + '🗣', + '👤', + '👥', + '👣', + '🌂', + '☂', + '👓', + '🕶', + '👔', + '👕', + '👖', + '🧣', + '🧤', + '🧥', + '🧦', + '👗', + '👘', + '👙', + '👚', + '👛', + '👜', + '👝', + '🎒', + '👞', + '👟', + '👠', + '👡', + '👢', + '👑', + '👒', + '🎩', + '🎓', + '🧢', + '⛑', + '💄', + '💍', + '💼', +] + +export default emojis diff --git a/src/im-sdk/utils/index.ts b/src/im-sdk/utils/index.ts new file mode 100644 index 0000000..ff94086 --- /dev/null +++ b/src/im-sdk/utils/index.ts @@ -0,0 +1,66 @@ +export function readImageFile(file: File): Promise<{ url: string; width: number; height: number }> { + return new Promise((resolve, reject) => { + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + console.log(img.naturalWidth, img.naturalHeight, '图片宽高'); + resolve({ + url, + width: img.naturalWidth, + height: img.naturalHeight, + }); + }; + img.onerror = (err) => { + reject(new Error('图片加载失败')); + }; + + img.src = url; + }); +} +export function readVideoFile(file: File) { + return new Promise<{ + url: string; + width: number; + height: number; + duration: number; + }>((resolve, reject) => { + // 创建Blob + const blob = new Blob([file], { type: file.type }); + + // 创建预览URL + const video = document.createElement("video"); + video.src = URL.createObjectURL(blob); + + video.onloadedmetadata = () => { + resolve({ + url: video.src, + width: video.videoWidth, + height: video.videoHeight, + duration: video.duration, + }); + }; + + video.onerror = () => { + URL.revokeObjectURL(video.src); + reject(new Error("视频加载失败")); + }; + }); +} + +/** + * 完成action直到成功 + * @param action + * @param delay 执行间隙 + */ +export async function doWhile( + action: () => Promise, + delay: number = 0 +) { + while (true) { + const ret = await action(); + if (ret) { + break; + } + await new Promise((rs) => setTimeout(rs, delay)); + } +} diff --git a/src/im-sdk/utils/message.ts b/src/im-sdk/utils/message.ts new file mode 100644 index 0000000..771b62c --- /dev/null +++ b/src/im-sdk/utils/message.ts @@ -0,0 +1,47 @@ +import { $t } from '@/locales'; +import { MessageBrief, MessageData } from '../types'; + +export function generateBrief(message: MessageData) { + let brief = ''; + if (message.type === 'txt') { + brief = message.msg; + } else if (message.type === 'img') { + brief = $t('message.imgBrief'); + } else if (message.type === 'audio') { + brief = $t('message.audioBrief'); + } else if (message.type === 'video') { + brief = $t('message.videoBrief'); + } else if (message.type === 'call') { + if (message.callType === 'audio') { + brief = $t('call.audioCall'); + } else { + brief = $t('call.videoCall'); + } + } else { + brief = $t('message.otherBrief'); + } + const data: MessageBrief = { + id: message.id, + brief, + sender: message.from, + }; + return data; +} + +export function getAudioDuration(file: File): Promise { + return new Promise((resolve, reject) => { + const audio = document.createElement('audio'); + audio.preload = 'metadata'; + + audio.onloadedmetadata = () => { + URL.revokeObjectURL(audio.src); // 清理 blob URL + resolve(audio.duration); + }; + + audio.onerror = (e) => { + reject(new Error('Failed to load audio metadata')); + }; + + audio.src = URL.createObjectURL(file); + }); +} diff --git a/src/im-sdk/utils/timestamp.ts b/src/im-sdk/utils/timestamp.ts new file mode 100644 index 0000000..b415068 --- /dev/null +++ b/src/im-sdk/utils/timestamp.ts @@ -0,0 +1,37 @@ +import dayjs from "dayjs"; + +export const TimeHoursMinters = (timestamp: number) => { + const date = new Date(timestamp); + + // 自动使用系统本地时区 + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const hhmm = `${hours}:${minutes}`; + return hhmm; +}; + + +// 处理时间戳为年月日 +export function renderTimeToYear(time?: string | number) { + if (!time) { + return "--:--"; + } + const date = dayjs(Number(time)); + const currentYear = dayjs().year(); + if (date.year() < currentYear) { + return date.format("YYYY-MM-DD HH:mm"); + } else { + return date.format("MM-DD HH:mm"); + } +} + +export function formatDuration(ms: number): string { + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const pad = (n: number) => n.toString().padStart(2, '0'); + + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; +} diff --git a/src/im-sdk/widget/ChatItem.vue b/src/im-sdk/widget/ChatItem.vue new file mode 100644 index 0000000..5004d37 --- /dev/null +++ b/src/im-sdk/widget/ChatItem.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/im-sdk/widget/ChatMenu.vue b/src/im-sdk/widget/ChatMenu.vue new file mode 100644 index 0000000..8ed7082 --- /dev/null +++ b/src/im-sdk/widget/ChatMenu.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/im-sdk/widget/ConversationItem.vue b/src/im-sdk/widget/ConversationItem.vue new file mode 100644 index 0000000..9852dde --- /dev/null +++ b/src/im-sdk/widget/ConversationItem.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/src/im-sdk/widget/SendStatus.vue b/src/im-sdk/widget/SendStatus.vue new file mode 100644 index 0000000..3ef46b8 --- /dev/null +++ b/src/im-sdk/widget/SendStatus.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/im-sdk/widget/TimeCost.vue b/src/im-sdk/widget/TimeCost.vue new file mode 100644 index 0000000..6b37a09 --- /dev/null +++ b/src/im-sdk/widget/TimeCost.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/im-sdk/widget/chatNav/ChatNav.vue b/src/im-sdk/widget/chatNav/ChatNav.vue new file mode 100644 index 0000000..5566e0d --- /dev/null +++ b/src/im-sdk/widget/chatNav/ChatNav.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/im-sdk/widget/chatNav/ChatNavCP.vue b/src/im-sdk/widget/chatNav/ChatNavCP.vue new file mode 100644 index 0000000..54236f6 --- /dev/null +++ b/src/im-sdk/widget/chatNav/ChatNavCP.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/ChatCallVideo.vue b/src/im-sdk/widget/menuModule/ChatCallVideo.vue new file mode 100644 index 0000000..a202f1e --- /dev/null +++ b/src/im-sdk/widget/menuModule/ChatCallVideo.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/ChatCallVoice.vue b/src/im-sdk/widget/menuModule/ChatCallVoice.vue new file mode 100644 index 0000000..8cd339a --- /dev/null +++ b/src/im-sdk/widget/menuModule/ChatCallVoice.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/EmojiMap.vue b/src/im-sdk/widget/menuModule/EmojiMap.vue new file mode 100644 index 0000000..35f7135 --- /dev/null +++ b/src/im-sdk/widget/menuModule/EmojiMap.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/SendPrivateImg.vue b/src/im-sdk/widget/menuModule/SendPrivateImg.vue new file mode 100644 index 0000000..6e0c72b --- /dev/null +++ b/src/im-sdk/widget/menuModule/SendPrivateImg.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/SendPrivatePayImg.vue b/src/im-sdk/widget/menuModule/SendPrivatePayImg.vue new file mode 100644 index 0000000..e58f638 --- /dev/null +++ b/src/im-sdk/widget/menuModule/SendPrivatePayImg.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/src/im-sdk/widget/menuModule/Voice/VioceRecorder.ts b/src/im-sdk/widget/menuModule/Voice/VioceRecorder.ts new file mode 100644 index 0000000..073c5d4 --- /dev/null +++ b/src/im-sdk/widget/menuModule/Voice/VioceRecorder.ts @@ -0,0 +1,87 @@ +import { $t } from "@/locales"; +import message from "@/utils/message"; +import BenzAMRRecorder from "benz-amr-recorder"; + +interface VoiceRecord { + blob: Blob | null; + duration: number; +} + +export class VioceRecorder { + /**定时器 */ + private timer?: NodeJS.Timeout; + + time = 0; + + private recorder = new BenzAMRRecorder(); + + private maxDuration = 0; + + private onEmit?: (data: VoiceRecord) => void; + private onProgress?: (duration: number) => void; + + /**是否进入录制中 */ + private isEnterRecord = false; + + constructor(maxDuration: number, onProgress?: (duration: number) => void) { + this.maxDuration = maxDuration; + this.onProgress = onProgress; + } + + start(onEmit: (data: VoiceRecord) => void) { + this.onEmit = onEmit; + return this.recorder + .initWithRecord() + .then(() => { + this.recorder.startRecord(); + this.time = Date.now(); + this.isEnterRecord = true; + this.timer = setInterval(this.update, 100); + return true; + }) + .catch((e) => { + message.error($t("common.recordError")); + return false; + }); + } + + abort() { + if (this.recorder.isRecording()) { + this.recorder.cancelRecord(); + } + this.onEmit = undefined; + } + + async finish() { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + if (!this.isEnterRecord) { + return; + } + try { + if (this.recorder.isRecording()) { + await this.recorder.finishRecord(); + } + const blob = this.recorder.getBlob(); + this.onEmit?.({ + blob, + duration: this.recorder.getDuration(), + }); + } catch (error) { + message.error($t("common.recordErrornot")); + } + } + + update = () => { + const dt = Date.now() - this.time; + if (dt >= this.maxDuration) { + this.recorder.finishRecord(); + message.error("已达到最大录制时间"); + clearInterval(this.timer); + this.timer = undefined; + } + this.onProgress?.(dt); + }; +} diff --git a/src/im-sdk/widget/menuModule/VoiceBtn.vue b/src/im-sdk/widget/menuModule/VoiceBtn.vue new file mode 100644 index 0000000..ac08a32 --- /dev/null +++ b/src/im-sdk/widget/menuModule/VoiceBtn.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/src/im-sdk/widget/messages/CallMessage.vue b/src/im-sdk/widget/messages/CallMessage.vue new file mode 100644 index 0000000..1a0bb32 --- /dev/null +++ b/src/im-sdk/widget/messages/CallMessage.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/im-sdk/widget/messages/ImageMessage.vue b/src/im-sdk/widget/messages/ImageMessage.vue new file mode 100644 index 0000000..db0c166 --- /dev/null +++ b/src/im-sdk/widget/messages/ImageMessage.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/im-sdk/widget/messages/LocationMessage.vue b/src/im-sdk/widget/messages/LocationMessage.vue new file mode 100644 index 0000000..3639e95 --- /dev/null +++ b/src/im-sdk/widget/messages/LocationMessage.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/src/im-sdk/widget/messages/TextMessage.vue b/src/im-sdk/widget/messages/TextMessage.vue new file mode 100644 index 0000000..8dea0c6 --- /dev/null +++ b/src/im-sdk/widget/messages/TextMessage.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/im-sdk/widget/messages/UnknownMessage.vue b/src/im-sdk/widget/messages/UnknownMessage.vue new file mode 100644 index 0000000..d6b3e46 --- /dev/null +++ b/src/im-sdk/widget/messages/UnknownMessage.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/im-sdk/widget/messages/VideoMessage.vue b/src/im-sdk/widget/messages/VideoMessage.vue new file mode 100644 index 0000000..f8c80b3 --- /dev/null +++ b/src/im-sdk/widget/messages/VideoMessage.vue @@ -0,0 +1,85 @@ + + + + diff --git a/src/im-sdk/widget/messages/Voice/ViocePlayController.ts b/src/im-sdk/widget/messages/Voice/ViocePlayController.ts new file mode 100644 index 0000000..afd24c3 --- /dev/null +++ b/src/im-sdk/widget/messages/Voice/ViocePlayController.ts @@ -0,0 +1,67 @@ +// useAudioPlayer.ts +import { reactive } from 'vue'; +import BenzAMRRecorder from 'benz-amr-recorder'; + +const state = reactive({ + isPlaying: false, + playMsgId: '', +}); + +let armRec: BenzAMRRecorder | null = null; + +const playAudio = async (msgId: string, url: string) => { + if (state.isPlaying && state.playMsgId === msgId) { + armRec?.playOrPauseOrResume(); + return; + } + + if (state.isPlaying && armRec) { + armRec.stop(); + state.isPlaying = false; + state.playMsgId = ''; + armRec = null; + } + + armRec = new BenzAMRRecorder(); + + armRec.onPlay(() => { + console.log('onPlay 触发'); + state.isPlaying = true; + state.playMsgId = msgId; + }); + + armRec.onPause(() => { + state.isPlaying = false; + state.playMsgId = ''; + }); + + armRec.onStop(() => { + state.isPlaying = false; + state.playMsgId = ''; + armRec = null; + }); + try { + await armRec.initWithUrl(url); + armRec.playOrPauseOrResume(); + } catch (err) { + console.error('音频加载失败', err); + return; + } +}; + +const stopAudio = () => { + if (armRec) { + armRec.stop(); + armRec = null; + state.isPlaying = false; + state.playMsgId = ''; + } +}; + +export function useAudioPlayer() { + return { + audioPlayStatus: state, + playAudio, + stopAudio, + }; +} diff --git a/src/im-sdk/widget/messages/VoiceMessage.vue b/src/im-sdk/widget/messages/VoiceMessage.vue new file mode 100644 index 0000000..6980421 --- /dev/null +++ b/src/im-sdk/widget/messages/VoiceMessage.vue @@ -0,0 +1,92 @@ + + + + diff --git a/src/store/basic.ts b/src/store/basic.ts index e5dc1f7..3867e09 100644 --- a/src/store/basic.ts +++ b/src/store/basic.ts @@ -54,6 +54,8 @@ export const useBasicStore = defineStore('basic', { }) }, setUserInfo({ userInfo, roles, codes }) { + console.log(userInfo, roles, codes,'223'); + const { username, avatar } = userInfo this.$patch((state) => { state.roles = roles diff --git a/src/third/huanxin/Adapter.ts b/src/third/huanxin/Adapter.ts new file mode 100644 index 0000000..d80ad91 --- /dev/null +++ b/src/third/huanxin/Adapter.ts @@ -0,0 +1,530 @@ +import { BaseAdapter, LoaderListener } from '@/im-sdk/BaseAdapter'; +import { CallAdapter, CallListener } from '@/im-sdk/callDataCenter'; +import { + CallAction, + CallMessage, + CallType, + ConversationData, + ImUserData, + MessageData, + MessageTempData, +} from '@/im-sdk/types'; +import { generateBrief } from '@/im-sdk/utils/message'; +// import userManager from '@/manager/user'; +import { + default as easemobChat, + EasemobChat, + default as WebIM, +} from 'easemob-websdk'; +import { + callDataConvert, + CallDataExt, + CmdCallMessageAction, + messageConvert, +} from './utils'; +import { uuid } from './api'; +export class Adapter implements BaseAdapter, CallAdapter { + imConn!: EasemobChat.Connection; + + userId!: string; + token!: string; + private ready: Promise; + private _readyResolver!: (conn: EasemobChat.Connection) => void; + + constructor() { + this.ready = new Promise((resolve) => { + this._readyResolver = resolve; + }); + } + listener!: LoaderListener; + addListener(listener: LoaderListener): void { + this.listener = listener; + } + + init(appKey: string) { + this.imConn = new WebIM.connection({ + appKey, + }); + this.startListen(); + } + startListen() { + // 这里没有用implement的原因是回调函数内部以其他对象作为this调用过this.xxx,会导致this指向异常 + const listener = { + onConnected: this.onConnected.bind(this), + onDisconnected: this.onDisconnected.bind(this), + onTextMessage: this.onCommonMessage.bind(this), + onImageMessage: this.onCommonMessage.bind(this), + onCmdMessage: this.onCmdMessage.bind(this), + onAudioMessage: this.onCommonMessage.bind(this), + onLocationMessage: this.onLocationMessage.bind(this), + onFileMessage: this.onFileMessage.bind(this), + onCustomMessage: this.onCustomMessage.bind(this), + onVideoMessage: this.onCommonMessage.bind(this), + onPresence: this.onPresence.bind(this), + onContactInvited: this.onContactInvited.bind(this), + onContactDeleted: this.onContactDeleted.bind(this), + onContactAdded: this.onContactAdded.bind(this), + onContactRefuse: this.onContactRefuse.bind(this), + onContactAgreed: this.onContactAgreed.bind(this), + onGroupEvent: this.onGroupEvent.bind(this), + onOnline: this.onOnline.bind(this), + onOffline: this.onOffline.bind(this), + onError: this.onError.bind(this), + onRecallMessage: this.onRecallMessage.bind(this), + onReceivedMessage: this.onReceivedMessage.bind(this), + onDeliveredMessage: this.onDeliveredMessage.bind(this), + onReadMessage: this.onCommonMessage.bind(this), + onChannelMessage: this.onCommonMessage.bind(this), + }; + this.imConn.addEventHandler('eventName', listener); + } + fetchUser(imId: string): Promise { + return Promise.resolve({ + imId, + userId: 'defaultUserId', + avatar: 'defaultAvatarUrl', + name: 'defaultName', + }); + } + private loginPromise?: Promise; + async login(userId: string, token: string) { + if (this.loginPromise) { + return this.loginPromise; + } + this.userId = userId; + this.token = token; + this.loginPromise = new Promise(async (rs) => { + const result = await this.imConn.open({ + user: userId, + accessToken: token, + }); + this.listener?.onLogin(); + this.callListener.onLogin(); + this.loginPromise = undefined; + if (result) { + return rs(true); + } + return rs(false); + }); + return this.loginPromise; + } + logout(): void { + if (!this.userId) return; + this.userId = ''; + this.token = ''; + // 放到微任务末尾来操作 + Promise.resolve().then(() => { + this.imConn.close(); + }); + } + async fetchConversations(): Promise { + const result: ConversationData[] = []; + await this.loopGetConversation('', result, []); + return result; + } + private async loopGetConversation( + cursor: string, + collection: ConversationData[], + blackList: string[] + ) { + const pageSize = 50; + const conn = await this.ready; + const resp = await conn.getServerConversations({ pageSize, cursor }); + if (resp.data) { + resp.data.conversations.forEach((conversation) => { + if (!blackList.includes(conversation.conversationId)) { + const conv: ConversationData = { + id: conversation.conversationId, + type: conversation.conversationType, + isPinned: conversation.isPinned, + unReadCount: conversation.unReadCount, + time: 0, + }; + if (conversation.lastMessage) { + const lastMessage = messageConvert( + conversation.lastMessage as EasemobChat.MessagesType, + this.userId + ); + if (lastMessage) { + conv.last = generateBrief(lastMessage); + conv.time = lastMessage.time; + } + } + collection.push(conv); + } + }); + if (resp.data.conversations.length === 50) { + // 可能还有,就继续拉 + await this.loopGetConversation(resp.data.cursor, collection, blackList); + } + } + } + async fetchMessageList( + userId: string, + cursor: string + ): Promise<{ cursor: string; list: MessageData[]; isEnd?: boolean }> { + const conn = await this.ready; + const resp = await conn.getHistoryMessages({ + targetId: userId, + pageSize: 50, + cursor, + chatType: 'singleChat', + searchDirection: 'up', + }); + + return { + cursor: resp.cursor || '', + isEnd: resp.isLast, + list: resp.messages + .map((message) => messageConvert(message, this.userId)) + .filter((v) => v) as MessageData[], + }; + } + async sendMessage( + message: MessageTempData + ): Promise { + try { + const conn = await this.ready; + if (message.message.type === 'txt') { + const body = WebIM.message.create({ + chatType: 'singleChat', // 会话类型,设置为单聊。 + type: 'txt', // 消息类型。 + from: this.userId, + to: message.message.to, // 消息接收方(用户 ID)。 + msg: message.message.msg, // 消息内容。 + ext: message.message.ext, + }); + const res = await conn.send(body); + if (res.message) { + return messageConvert(res.message, this.userId); + } + } else if (message.message.type === 'img') { + const file = message.blob as File; + const body = WebIM.message.create({ + // 消息类型。 + type: 'img', + file: { + data: message.blob as File, + url: easemobChat.utils.parseDownloadResponse(message.message.url), + filename: file.name, + filetype: file.type, + }, + from: this.userId, + to: message.message.to, + chatType: 'singleChat', + thumbnailHeight: 50, + thumbnailWidth: 50, + width: message.message.width, + height: message.message.height, + file_length: message.message.length, + ext: message.message.ext, + }); + const res = await conn.send(body); + if (res.message) { + return messageConvert(res.message, this.userId); + } + } else if (message.message.type === 'audio') { + const file = message.blob as File; + const body = WebIM.message.create({ + // 消息类型。 + type: 'audio', + file: { + data: file, + filename: file.name, + filetype: file.type, + url: easemobChat.utils.parseDownloadResponse(message.message.url), + }, + filename: file.name, + length: message.message.duration, + from: this.userId, + to: message.message.to, + chatType: 'singleChat', + ext: message.message.ext, + }); + const res = await conn.send(body); + if (res.message) { + return messageConvert(res.message, this.userId); + } + } else if (message.message.type === 'video') { + const file = message.blob as File; + const body = WebIM.message.create({ + // 消息类型。 + type: 'video', + file: { + data: message.blob as File, + url: message.message.url || '', + filename: file.name, + filetype: file.type, + }, + filename: file.name, + length: message.message.duration, + from: this.userId, + to: message.message.to, + chatType: 'singleChat', + ext: { + width: message.message.width, + height: message.message.height, + ...message.message.ext, + }, + }); + const res = await conn.send(body); + if (res.message) { + return messageConvert(res.message, this.userId); + } + } else if (message.message.type === 'read') { + const body = WebIM.message.create({ + // 消息类型。 + type: 'read', + from: this.userId, + to: message.message.to, + chatType: 'singleChat', + id: message.message.messageId, + }); + await conn.send(body); + } else if (message.message.type === 'clearUnread') { + const body = WebIM.message.create({ + chatType: 'singleChat', // 会话类型,设置为单聊。 + type: 'channel', // 消息类型。 + to: message.message.to, + }); + await conn.send(body); + } + } catch (error) { + console.error('发送消息失败', error); + } + } + /**外部定制会话结构,可以在extra里面添加数据 */ + buildConversation(base: ConversationData): ConversationData { + return base; + } + /**通用消息处理 */ + onCommonMessage(message: EasemobChat.MessagesType) { + const ret = messageConvert(message, this.userId); + if (!ret) return; + this.listener?.onMessage(ret); + } + + ////////以下是回调--------------- + onConnected(message?: EasemobChat.ErrorEvent) { + this._readyResolver(this.imConn); + this.listener?.onConnected(); + } + // SDK 与环信服务器断开连接。 + onDisconnected(message?: EasemobChat.ErrorEvent) { + this.listener?.onDisconnected(); + this.ready = new Promise((resolve) => { + this._readyResolver = resolve; + }); + //断开回调触发后,如果业务登录状态为true则说明异常断开需要重新登录 + if (!this.token) { + this.imConn.close(); + } else { + //执行通过token,重新登录 + this.login(this.userId, this.token); + } + } + + // 当前用户收到透传消息。 + onCmdMessage(msg: EasemobChat.CmdMsgBody) { + this.onCallData(msg); + } + // 当前用户收到位置消息。 + onLocationMessage(message: EasemobChat.LocationMsgBody) {} + // 当前用户收到文件消息。 + onFileMessage(message: EasemobChat.FileMsgBody) {} + // 当前用户收到自定义消息。 + onCustomMessage(message: EasemobChat.CustomMsgBody) {} + // 当前用户订阅的其他用户的在线状态更新。 + onPresence(message: EasemobChat.PresenceMsg) {} + // 当前用户收到好友邀请。 + onContactInvited(msg: EasemobChat.ContactMsgBody) {} + // 联系人被删除。 + onContactDeleted(msg: EasemobChat.ContactMsgBody) {} + // 新增联系人。 + onContactAdded(msg: EasemobChat.ContactMsgBody) {} + // 当前用户发送的好友请求被拒绝。 + onContactRefuse(msg: EasemobChat.ContactMsgBody) {} + // 当前用户发送的好友请求被同意。 + onContactAgreed(msg: EasemobChat.ContactMsgBody) {} + // 当前用户收到群组邀请。 + onGroupEvent(message: EasemobChat.GroupEvent) {} + // 本机网络连接成功。 + onOnline() {} + // 本机网络掉线。 + onOffline() {} + // 调用过程中出现错误。 + onError(message: EasemobChat.ErrorEvent) { + if (message.type === 206) { + // 其他地方登录了,这里自动退出 + // todo 这里还不能直接调用user.logout,会导致token也丢失,在开发时候会导致异常刷新token + } + } + // 当前用户收到的消息被消息发送方撤回。 + onRecallMessage(message: EasemobChat.RecallMsgBody) {} + // 当前用户发送的消息被接收方收到。 + onReceivedMessage(message: EasemobChat.ReceivedMsgBody) {} + // 当前用户收到消息送达回执。 + onDeliveredMessage(message: EasemobChat.DeliveryMsgBody) {} + + // 拉黑用户 + addUsersToBlocklist(imUserId: string) { + this.imConn.addUsersToBlocklist({ name: imUserId }); + } + + // 取消拉黑 + removeUserFromBlocklist(imUserId: string) { + this.imConn.removeUserFromBlocklist({ name: imUserId }); + } + + // 删除服务端会话列表 + deleteConversation() { + this.imConn.deleteAllMessagesAndConversations(); + } + + //////////语音聊天相关 + callListener!: CallListener; + async react( + data: CallMessage, + action: CallAction + ): Promise<{ id: string; success: boolean }> { + try { + const conn = await this.ready; + const ext: CallDataExt = { + action, + result: data.result, + callId: data.callId, + type: data.callType, + channel: data.channel, + start: data.start, + end: data.end, + }; + switch (action) { + case CallAction.callout: { + break; + } + case CallAction.accept: { + break; + } + case CallAction.refuse: { + break; + } + case CallAction.busy: { + break; + } + case CallAction.cancel: { + break; + } + case CallAction.enter: { + break; + } + case CallAction.finish: { + break; + } + case CallAction.video2vioce: { + break; + } + case CallAction.timeout: { + break; + } + } + const body = WebIM.message.create({ + chatType: 'singleChat', // 会话类型,设置为单聊。 + type: 'cmd', // 消息类型。 + from: data.from, + to: data.to, // 消息接收方(用户 ID)。 + action: CmdCallMessageAction, + ext, + }); + const res = await conn.send(body); + const message = res.message; + if (message) { + return { + success: true, + id: message.id, + }; + } + } catch (error) { + console.log('视频聊天交互失败', error); + } + return { + success: false, + id: '', + }; + } + addCallListener(listener: CallListener): void { + this.callListener = listener; + } + + onCallData(message: EasemobChat.CmdMsgBody) { + const data = callDataConvert(message, this.userId); + console.log('收到通话消息', data,message); + + if (data) { + const ext = message.ext as CallDataExt; + + switch (ext.action) { + case CallAction.callout: { + this.callListener.onCall(data); + break; + } + case CallAction.accept: { + this.callListener.onAccepted(data); + break; + } + case CallAction.busy: { + this.callListener.onBusy(data); + break; + } + case CallAction.cancel: { + this.callListener.onCancel(data); + break; + } + case CallAction.refuse: { + this.callListener.onRefused(data); + break; + } + case CallAction.enter: { + this.callListener.onEnter(data); + break; + } + case CallAction.timeout: { + this.callListener.onTimeout(data); + break; + } + case CallAction.finish: { + this.callListener.onFinished(data); + break; + } + case CallAction.video2vioce: { + this.callListener.onVideo2Vioce(data); + break; + } + case CallAction.abort: { + this.callListener.onAbort(data); + break; + } + } + } + } + async getChannel(): Promise { + return uuid(); + } + async joinChannel(channel: string, callType: CallType): Promise { + // try { + // const conn = await this.ready; + // const success = await CallRoom.getAuthToken(conn, this.userId, channel); + // if (!success) return false; + // await CallRoom.join(channel, callType); + // return true; + // } catch (error) { + // console.log('加入聊天频道失败', error); + // return false; + // } + return true; + } + exitChannel(): void { + // CallRoom.exit(); + } +} +const imAdapter = new Adapter(); +export default imAdapter; diff --git a/src/third/huanxin/api.ts b/src/third/huanxin/api.ts new file mode 100644 index 0000000..8f87891 --- /dev/null +++ b/src/third/huanxin/api.ts @@ -0,0 +1,37 @@ +import { EasemobChat } from "easemob-websdk"; + +export interface ChannelParams { + userId: string; + channel: string; +} +export interface ChannelResp { + accessToken: string; + agoraUserId: string; +} +export function getRtcToken( + imConn: EasemobChat.Connection, + payload: ChannelParams +): Promise { + const { userId, channel } = payload; + const myHeaders = new Headers(); + myHeaders.append("authorization", `Bearer ${imConn.context.accessToken}`); + const requestOptions: RequestInit = { + method: "GET", + headers: myHeaders, + redirect: "follow", + }; + return fetch( + `${ + imConn.apiUrl + }/token/rtcToken/v1?userAccount=${userId}&channelName=${channel}&appkey=${encodeURIComponent( + imConn.appKey + )}`, + requestOptions + ).then((response) => response.json()); +} +export function uuid() { + var temp_url = URL.createObjectURL(new Blob()); + var uuid = temp_url.toString(); // blob:https://xxx.com/b250d159-e1b6-4a87-9002-885d90033be3 + URL.revokeObjectURL(temp_url); + return uuid.substr(uuid.lastIndexOf("/") + 1); +} diff --git a/src/third/huanxin/index.ts b/src/third/huanxin/index.ts new file mode 100644 index 0000000..7caed30 --- /dev/null +++ b/src/third/huanxin/index.ts @@ -0,0 +1,79 @@ +import callDataCenter from '@/im-sdk/callDataCenter' +import config from '@/im-sdk/config' +import { ChatMenus, ConversationItemRender, MessageItemRender } from '@/im-sdk/config/default' +import ImDataCenter from '@/im-sdk/ImDataCenter' +import { CallState, ImUserData, MessageTempData } from '@/im-sdk/types' +import { generateBrief } from '@/im-sdk/utils/message' +import router, { preloadPage } from '@/router' +// import message from '@/utils/message' +import imAdapter from './Adapter' + + +/**支持从系统用户id跳转到聊天 */ +export function chatWithUserId(userId: string) {} +export function initIm() { + ImDataCenter.setAdapter(imAdapter) + callDataCenter.setAdapter(imAdapter) + + config.conversationRender = ConversationItemRender + config.messageItemRender = MessageItemRender + + config.chatMenus = ChatMenus + + config.onChatWith = (user?: string) => { + if (user) { + router.push({ + name: 'ChatView', + query: { id: user.toLocaleLowerCase() } + }) + } + } + /**预加载聊天界面 */ + config.onLoadConversationView = () => { + preloadPage('ChatView') + } + + // 发消息前判断 + config.onBeforeSendMessage = async (messageTemp: MessageTempData) => { + const controller = ImDataCenter.getConversation(messageTemp.message.target) + const conversation = controller?.conversation + + // 若无用户信息,默认允许发送(如系统会话等) + const toUserId = conversation?.user?.userId + if (!toUserId) return true + + // 当前登录用户 ID + const fromUserId = ImDataCenter.data.mine?.userId + if (!fromUserId) { + // message.error($t('common.userInfoMissing')) + return false + } + + // 不能自己给自己发送消息 + if (toUserId === fromUserId) { + // message.error($t('common.oneByOne')) + return false + } + + // 获取消息简要内容 + const brief = generateBrief(messageTemp.message).brief; + try { + messageTemp.message.ext = Object.assign({}, messageTemp.message.ext,brief) + return true + } catch (error) { + // message.error($t('common.failSend')) + return false + } + } + + // 获取用户信息 + config.onViewUser = async (user?: ImUserData) => { + router.push({ + path: '/mineinfo', + query: { + userId: user?.userId + } + }) + } + +} diff --git a/src/third/huanxin/utils.ts b/src/third/huanxin/utils.ts new file mode 100644 index 0000000..d186470 --- /dev/null +++ b/src/third/huanxin/utils.ts @@ -0,0 +1,127 @@ +import { + CallAction, + CallMessage, + CallResult, + CallType, + MessageCommon, + MessageData, + MessageState, +} from '@/im-sdk/types'; +import { EasemobChat } from 'easemob-websdk'; + +export function messageConvert( + message: EasemobChat.MessagesType, + selfId: string +): MessageData | undefined { + // todo 目前只处理了单个的聊天 + const base: MessageCommon = { + target: message.to === selfId ? message.from! : message.to, + id: message.id, + from: message.from, + to: message.to, + time: 0, + isRead: false, + state: MessageState.sending, + }; + if (message.type === 'txt') { + return { + ...base, + type: 'txt', + msg: message.msg, + ext: message.ext, + isRead: !!message.isRead, + time: message.time, + state: MessageState.sended, + }; + } else if (message.type === 'img') { + return { + ...base, + type: 'img', + url: message.url, + width: message.width || 0, + height: message.height || 0, + length: message.file_length || 0, + ext: message.ext, + isRead: !!message.isRead, + time: message.time, + state: MessageState.sended, + }; + } else if (message.type === 'audio') { + return { + ...base, + type: 'audio', + url: message.url, + duration: message.length || 0, + length: message.file_length || 0, + ext: message.ext, + isRead: !!message.isRead, + time: message.time, + state: MessageState.sended, + }; + } else if (message.type === 'video') { + return { + ...base, + type: 'video', + url: message.url, + width: message.ext?.width || 0, + height: message.ext?.height || 0, + duration: message.length || 0, + length: message.file_length || 0, + ext: message.ext, + isRead: !!message.isRead, + time: message.time, + state: MessageState.sended, + }; + } else if (message.type === 'read') { + return { + ...base, + type: 'read', + messageId: message.mid!, + }; + } else if (message.type === 'channel') { + return { + ...base, + type: 'clearUnread', + time: message.time, + }; + } +} +export interface CallDataExt { + callId: string; + action: CallAction; + type: CallType; + channel?: string; + start?: number; + end?: number; + result: CallResult; +} +export const CmdCallMessageAction = 'rtcCall'; + +export function callDataConvert( + message: EasemobChat.CmdMsgBody, + selfId: string +): CallMessage | undefined { + if (message.action !== CmdCallMessageAction) { + return; + } + const ext = message.ext as CallDataExt; + const base: MessageCommon = { + target: message.to === selfId ? message.from! : message.to, + id: message.id, + from: message.from, + to: message.to, + time: Date.now(), + isRead: false, + state: MessageState.sending, + }; + return { + ...base, + type: 'call', + callId: ext.callId, + callType: ext.type, + result: ext.result, + channel: ext.channel, + start: ext.start, + end: ext.end, + }; +} diff --git a/src/utils/smCrypto.ts b/src/utils/smCrypto.ts index af6e194..1733369 100644 --- a/src/utils/smCrypto.ts +++ b/src/utils/smCrypto.ts @@ -1,10 +1,12 @@ -// const publicKey = '040a302b5e4b961afb3908a4ae191266ac5866be100fc52e3b8dba9707c8620e64ae790ceffc3bfbf262dc098d293dd3e303356cb91b54861c767997799d2f0060' +import { sm2 } from 'sm-crypto' -// /** -// * sm2加密 -// * @param data 待加密数据 -// * @return 加密后的数据 -// */ -// export const sm2Encrypt = (data: string): string => { -// return '04' + sm2.doEncrypt(data, publicKey, 1) -// } +const publicKey = '040a302b5e4b961afb3908a4ae191266ac5866be100fc52e3b8dba9707c8620e64ae790ceffc3bfbf262dc098d293dd3e303356cb91b54861c767997799d2f0060' + +/** + * sm2加密 + * @param data 待加密数据 + * @return 加密后的数据 + */ +export const sm2Encrypt = (data: string): string => { + return '04' + sm2.doEncrypt(data, publicKey, 1) +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index a27ced5..13b97ed 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -47,7 +47,8 @@ import { onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useBasicStore } from '@/store/basic' import { elMessage, useElement } from '@/hooks/use-element' -import { loginReq } from '@/api/user' +import { getUserInfoReq, loginReq } from '@/api/user' +import { sm2Encrypt } from '@/utils/smCrypto' /* listen router change and set the query */ const { settings } = useBasicStore() @@ -102,11 +103,14 @@ const basicStore = useBasicStore() const loginFunc = () => { loginReq({ username: subForm.username, - // password: sm2Encrypt(subForm.password) + password: sm2Encrypt(subForm.password) }) .then(({ data }) => { elMessage('登录成功') basicStore.setToken(data?.access_token) + getUserInfoReq().then(({ data }) => { + basicStore.setUserInfo(data) + }) router.push('/') }) .catch((err) => { diff --git a/typings/auto-imports.d.ts b/typings/auto-imports.d.ts index 78d0691..80909b7 100644 --- a/typings/auto-imports.d.ts +++ b/typings/auto-imports.d.ts @@ -78,6 +78,7 @@ declare global { const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import('vue')['shallowRef'] const sleepTimeout: typeof import('../src/hooks/use-common')['sleepTimeout'] + const sm2Encrypt: typeof import('../src/utils/smCrypto')['sm2Encrypt'] const storeToRefs: typeof import('pinia/dist/pinia')['storeToRefs'] const toRaw: typeof import('vue')['toRaw'] const toRef: typeof import('vue')['toRef']