Compare commits
165 Commits
v1.0.0-alp
...
v1.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
048adef4ea | ||
|
|
44ff5b0134 | ||
|
|
01195ffebe | ||
|
|
0a12d37778 | ||
|
|
a011501089 | ||
|
|
ddbf6614e7 | ||
|
|
16b46fe6f8 | ||
|
|
890c1f9fd6 | ||
|
|
88bc4f3efb | ||
|
|
5c0b255092 | ||
|
|
e3d01fc035 | ||
|
|
9880f813f8 | ||
|
|
a2017be96a | ||
|
|
ecfa623a72 | ||
|
|
e63ac508f3 | ||
|
|
ad0d0ad7a0 | ||
|
|
653bbccb24 | ||
|
|
748423b2b9 | ||
|
|
5ac2d69c43 | ||
|
|
da93d05d67 | ||
|
|
83376a2a6a | ||
|
|
fd5a645bba | ||
|
|
7618b68713 | ||
|
|
b8dd1a8d04 | ||
|
|
9db1dc2c3a | ||
|
|
76dfcecf2b | ||
|
|
673dcd044b | ||
|
|
ea8dabb9d8 | ||
|
|
edf5fe4418 | ||
|
|
93a66e760f | ||
|
|
d9cc186fc1 | ||
|
|
92e73fe094 | ||
|
|
b1039b9c6f | ||
|
|
e187025d4e | ||
|
|
6673804208 | ||
|
|
0f2167015e | ||
|
|
75d37d0ceb | ||
|
|
94bfe0e957 | ||
|
|
98b66780c3 | ||
|
|
5763438082 | ||
|
|
6e7e72d92d | ||
|
|
84b113eb44 | ||
|
|
958be10181 | ||
|
|
e357429d3d | ||
|
|
240b0560dc | ||
|
|
58e46fa09f | ||
|
|
f88b6a0388 | ||
|
|
ea1e74fd08 | ||
|
|
41947e9060 | ||
|
|
0fd2a4d4cb | ||
|
|
81fdfda734 | ||
|
|
f8a949b4a8 | ||
|
|
bfecd911fe | ||
|
|
e500498b9e | ||
|
|
4af374f703 | ||
|
|
90b2144c6e | ||
|
|
9c8c4d905b | ||
|
|
9ff8e82eb9 | ||
|
|
b0d085b36c | ||
|
|
5237d3044d | ||
|
|
b19bd8e05f | ||
|
|
7791d34f30 | ||
|
|
9a04c5f86d | ||
|
|
22e95f59bb | ||
|
|
10c47f4389 | ||
|
|
1aab44784a | ||
|
|
93665b14c8 | ||
|
|
cf6f31e45e | ||
|
|
0b977df45b | ||
|
|
959cbaf394 | ||
|
|
6d88195590 | ||
|
|
8ed65c1358 | ||
|
|
96f15c1022 | ||
|
|
6d9ec0e260 | ||
|
|
43c7dc0532 | ||
|
|
dda01d9bb9 | ||
|
|
6648805595 | ||
|
|
35dfd305a7 | ||
|
|
a20f71ab9f | ||
|
|
e5be8bd266 | ||
|
|
b1c51774e3 | ||
|
|
2c1393e298 | ||
|
|
bccf1e1844 | ||
|
|
cfd8c9cc16 | ||
|
|
1a043add28 | ||
|
|
5db0b07be5 | ||
|
|
4d1f8cc1c2 | ||
|
|
390d983986 | ||
|
|
cd6864587a | ||
|
|
51a5fc5445 | ||
|
|
2e8c834fe6 | ||
|
|
34a4ad40e8 | ||
|
|
bda15e13e4 | ||
|
|
026065f2f1 | ||
|
|
06750ddf78 | ||
|
|
351562d02d | ||
|
|
304c1864e0 | ||
|
|
f8f0fe3790 | ||
|
|
b39e38de4a | ||
|
|
bdb221f123 | ||
|
|
5a5bea368c | ||
|
|
1139b1b14a | ||
|
|
3db0ba1cee | ||
|
|
9988e77455 | ||
|
|
c26d0ff4ed | ||
|
|
c4e24cb0ad | ||
|
|
e2c2eba8d1 | ||
|
|
da22ce103b | ||
|
|
c178b98cf7 | ||
|
|
336d8a39d2 | ||
|
|
48387b74c7 | ||
|
|
0b39027d9d | ||
|
|
f976934b9d | ||
|
|
3524649a10 | ||
|
|
0cea21d51e | ||
|
|
d4a5d72cba | ||
|
|
153d007e8e | ||
|
|
2565828114 | ||
|
|
f8a1a16513 | ||
|
|
7532cda953 | ||
|
|
12a1e27081 | ||
|
|
99df91750f | ||
|
|
d8697b045c | ||
|
|
7d909640ff | ||
|
|
87fdf836e0 | ||
|
|
8fd384e1eb | ||
|
|
90909f078f | ||
|
|
5aa13e0c68 | ||
|
|
5ddd002de3 | ||
|
|
cb7d3def41 | ||
|
|
e62ede16dc | ||
|
|
155ee79011 | ||
|
|
56fb3bcdf6 | ||
|
|
7280721a4f | ||
|
|
a918678543 | ||
|
|
658822588a | ||
|
|
e3e1158486 | ||
|
|
7362c5e451 | ||
|
|
edcf8efe34 | ||
|
|
e8cdf6de96 | ||
|
|
dced8fcb4d | ||
|
|
8f9b748560 | ||
|
|
a4bb96a6bc | ||
|
|
0117d1adf0 | ||
|
|
86e6adb7b8 | ||
|
|
a332753328 | ||
|
|
4d57c5200d | ||
|
|
6370d89517 | ||
|
|
4ebbbe5773 | ||
|
|
b0f17bac22 | ||
|
|
e73ba1185d | ||
|
|
7b133ce74e | ||
|
|
c7634c1c0a | ||
|
|
c8bfd3e070 | ||
|
|
9ac47d90d8 | ||
|
|
d35ac6453b | ||
|
|
184489e95c | ||
|
|
8cc5d35a10 | ||
|
|
b926ad7ce4 | ||
|
|
b363564229 | ||
|
|
115fed7fac | ||
|
|
e8a9a5d1de | ||
|
|
8662f377a6 | ||
|
|
1e4b4a5132 | ||
|
|
9d741fae59 |
@@ -9,7 +9,10 @@ module.exports = {
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
@@ -27,6 +30,7 @@ module.exports = {
|
||||
{
|
||||
ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
|
||||
}
|
||||
]
|
||||
],
|
||||
'vue/jsx-uses-vars': 'error' // 确保 JSX 中使用的变量被正确识别
|
||||
}
|
||||
}
|
||||
|
||||
37
README.md
@@ -24,19 +24,15 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 单聊
|
||||
- [x] 群聊
|
||||
|
||||
#### 通话功能
|
||||
|
||||
- [ ] 语音通话
|
||||
- [ ] 视频通话
|
||||
|
||||
#### 消息类型
|
||||
|
||||
- [x] 文本
|
||||
- [x] 表情
|
||||
- [x] 图片
|
||||
- [ ] 音频
|
||||
- [ ] 视频
|
||||
- [ ] 文件
|
||||
- [x] 语音
|
||||
- [x] 音频
|
||||
- [x] 视频
|
||||
- [x] 文件
|
||||
|
||||
#### 消息功能
|
||||
|
||||
@@ -46,10 +42,12 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 多端在线
|
||||
- [x] 多端同步
|
||||
- [x] 已读未读
|
||||
- [ ] 历史消息
|
||||
- [ ] @消息
|
||||
- [ ] 消息撤回
|
||||
- [ ] 消息引用
|
||||
- [x] @消息
|
||||
- [x] 消息撤回
|
||||
- [x] 消息删除
|
||||
- [x] 消息引用
|
||||
- [x] 消息转发
|
||||
- [x] 历史消息
|
||||
- [ ] 消息加入待办
|
||||
|
||||
#### 群组功能
|
||||
@@ -61,9 +59,6 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 群公告
|
||||
- [x] 群系统消息
|
||||
- [x] 群转让
|
||||
- [ ] 万人大群
|
||||
- [ ] 团队组织群
|
||||
- [ ] 公开群
|
||||
|
||||
#### 通讯录功能
|
||||
|
||||
@@ -71,7 +66,11 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 联系人分组
|
||||
- [x] 群备注
|
||||
- [x] 群分组
|
||||
- [ ] 组织管理
|
||||
|
||||
#### 通话功能
|
||||
|
||||
- [ ] 语音通话
|
||||
- [ ] 视频通话
|
||||
|
||||
#### 会议功能
|
||||
|
||||
@@ -93,6 +92,7 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
|
||||
- [ ] 大文件传输
|
||||
- [ ] 待办事项
|
||||
- [ ] 管理控制台
|
||||
|
||||
## 项目预览
|
||||
|
||||
@@ -100,6 +100,8 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -121,9 +123,8 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
## 交流社群
|
||||
|
||||
<img src="doc/image/qq_group.jpg" alt="QQ交流社群" width="30%" />
|
||||
<img src="doc/image/wx_group.png" alt="微信交流社群" width="30%" />
|
||||
|
||||
QQ群号:825505574,微信群有效期:3月24日
|
||||
QQ群:825505574
|
||||
|
||||
## 如何联系我们
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 488 KiB |
BIN
doc/image/img_5.png
Normal file
|
After Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "anylink-web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -23,16 +23,21 @@
|
||||
"element-plus": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.29",
|
||||
"vue-audio-visual": "^3.0.10",
|
||||
"vue-cropper": "^1.1.1",
|
||||
"vue-router": "^4.3.3"
|
||||
"vue-router": "^4.3.3",
|
||||
"xgplayer": "^3.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/runtime-dom": "^3.5.13",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"husky": "^8.0.0",
|
||||
|
||||
@@ -12,6 +12,22 @@ export const msgChatPullMsgService = (obj) => {
|
||||
return request.get('/chat/pullMsg', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatHistoryService = (obj) => {
|
||||
return request.get('/chat/history', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatRevokeMsgService = (obj) => {
|
||||
return request.post('/chat/revokeMsg', obj)
|
||||
}
|
||||
|
||||
export const msgChatDeleteMsgService = (obj) => {
|
||||
return request.post('/chat/deleteMsg', obj)
|
||||
}
|
||||
|
||||
export const msgAtService = () => {
|
||||
return request.get('/chat/queryAt')
|
||||
}
|
||||
|
||||
export const msgChatCreateSessionService = (obj) => {
|
||||
return request.post('/chat/createSession', obj)
|
||||
}
|
||||
@@ -20,6 +36,10 @@ export const msgChatQuerySessionService = (obj) => {
|
||||
return request.get('/chat/querySession', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatQueryMessagesService = (obj) => {
|
||||
return request.get('/chat/queryMessages', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatCloseSessionService = (obj) => {
|
||||
return request.post('/chat/closeSession', obj)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,93 @@
|
||||
import request from '@/js/utils/request'
|
||||
|
||||
export const mtsUploadService = (obj) => {
|
||||
return request.postForm('/mts/upload', obj)
|
||||
export const mtsUploadServiceForImage = async (requestBody, { originFile, thumbFile }) => {
|
||||
const res = await request.postForm('/mts/getUploadUrl', requestBody)
|
||||
const scope = res.data.data.scope
|
||||
const objectId = res.data.data.objectId
|
||||
const originUrl = res.data.data.originUrl
|
||||
const thumbUrl = res.data.data.thumbUrl
|
||||
if (scope === 1 && originUrl && thumbUrl) {
|
||||
// 如果文件之前已经上传过,直接获取下载地址
|
||||
return res
|
||||
} else {
|
||||
const uploadOriginUrl = res.data.data.uploadOriginUrl
|
||||
const uploadThumbUrl = res.data.data.uploadThumbUrl
|
||||
// 2 上传原图
|
||||
const originResponse = await fetch(uploadOriginUrl, {
|
||||
method: 'PUT',
|
||||
body: originFile,
|
||||
headers: {
|
||||
'Content-Type': originFile.type || 'application/octet-stream' // 设置 Content-Type
|
||||
}
|
||||
})
|
||||
|
||||
if (!originResponse.ok) {
|
||||
throw new Error('原图上传失败')
|
||||
}
|
||||
|
||||
// 3 上传缩略图,如果原图和缩略图一样,就不上传
|
||||
if (uploadThumbUrl !== uploadOriginUrl) {
|
||||
const thumbResponse = await fetch(uploadThumbUrl, {
|
||||
method: 'PUT',
|
||||
body: thumbFile,
|
||||
headers: {
|
||||
'Content-Type': thumbFile.type || 'application/octet-stream' // 设置 Content-Type
|
||||
}
|
||||
})
|
||||
|
||||
if (!thumbResponse.ok) {
|
||||
throw new Error('缩略图上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 4 上报服务端上传成功,服务端返回预签名下载URL
|
||||
const reportResponse = await request.postForm('/mts/reportUploaded', { objectId })
|
||||
return reportResponse
|
||||
}
|
||||
}
|
||||
|
||||
export const mtsUploadService = async (requestBody, { originFile }) => {
|
||||
// 1 获取上传的预签名URL
|
||||
const res = await request.postForm('/mts/getUploadUrl', requestBody)
|
||||
const scope = res.data.data.scope
|
||||
const objectId = res.data.data.objectId
|
||||
const downloadUrl = res.data.data.downloadUrl
|
||||
if (scope === 1 && downloadUrl) {
|
||||
// 如果文件之前已经上传过,直接过的下载地址
|
||||
return res
|
||||
} else {
|
||||
const uploadUrl = res.data.data.uploadUrl
|
||||
// 2 上传文件
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: originFile,
|
||||
headers: {
|
||||
'Content-Type': originFile.type || 'application/octet-stream' // 设置 Content-Type
|
||||
}
|
||||
})
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error('文件上传失败')
|
||||
}
|
||||
|
||||
// 3 上报服务端上传成功,服务端返回预签名下载URL
|
||||
const reportResponse = await request.postForm('/mts/reportUploaded', { objectId })
|
||||
return reportResponse
|
||||
}
|
||||
}
|
||||
|
||||
export const mtsImageService = (obj) => {
|
||||
return request.get('/mts/image', { params: obj })
|
||||
}
|
||||
|
||||
export const mtsAudioService = (obj) => {
|
||||
return request.get('/mts/audio', { params: obj })
|
||||
}
|
||||
|
||||
export const mtsVideoService = (obj) => {
|
||||
return request.get('/mts/video', { params: obj })
|
||||
}
|
||||
|
||||
export const mtsDocumentService = (obj) => {
|
||||
return request.get('/mts/document', { params: obj })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '@/js/utils/request'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { CLIENT_TYPE, CLIENT_NAME, CLIENT_VERSION } from '@/const/userConst'
|
||||
import { encryptPasswordObj, encryptDoublePasswordObj } from '@/js/utils/crypto'
|
||||
|
||||
@@ -7,7 +7,7 @@ export const userRegisterService = async ({ account, nickName, password }) => {
|
||||
const obj = await encryptPasswordObj(account, password)
|
||||
return request.post('/user/register', {
|
||||
account: account,
|
||||
clientId: userStore().clientId,
|
||||
clientId: useUserStore().clientId,
|
||||
nickName: nickName,
|
||||
...obj
|
||||
})
|
||||
@@ -17,7 +17,7 @@ export const userNonceService = ({ account }) => {
|
||||
return request.get('/user/nonce', {
|
||||
params: {
|
||||
account: account,
|
||||
clientId: userStore().clientId
|
||||
clientId: useUserStore().clientId
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export const userForgetService = async (obj) => {
|
||||
const passwordObjObj = await encryptPasswordObj(obj.account, obj.password)
|
||||
delete obj.password
|
||||
return request.post('/user/forget', {
|
||||
clientId: userStore().clientId,
|
||||
clientId: useUserStore().clientId,
|
||||
...obj,
|
||||
...passwordObjObj
|
||||
})
|
||||
@@ -44,7 +44,7 @@ export const userLoginService = async ({ account, password }) => {
|
||||
const obj = await encryptPasswordObj(account, password)
|
||||
return request.post('/user/login', {
|
||||
account: account,
|
||||
clientId: userStore().clientId,
|
||||
clientId: useUserStore().clientId,
|
||||
...obj
|
||||
})
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export const userModifySelfService = (obj) => {
|
||||
export const userModifyPassword = async ({ account, oldPasswordStr, newPasswordStr }) => {
|
||||
const obj = await encryptDoublePasswordObj(account, oldPasswordStr, newPasswordStr)
|
||||
return request.post('/user/modifyPwd', {
|
||||
clientId: userStore().clientId,
|
||||
clientId: useUserStore().clientId,
|
||||
...obj
|
||||
})
|
||||
}
|
||||
|
||||
1
src/assets/svg/archive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742995579543" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="33949" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M923.2 352H100.8C62.4 352 32 321.6 32 283.2V132.8C32 94.4 62.4 64 100.8 64h824C961.6 64 992 94.4 992 132.8v152c0 36.8-30.4 67.2-68.8 67.2z" fill="#55C7F7" p-id="33950"></path><path d="M923.2 672H100.8C62.4 672 32 641.6 32 603.2V420.8C32 382.4 62.4 352 100.8 352h824c38.4 0 68.8 30.4 68.8 68.8v184c-1.6 36.8-32 67.2-70.4 67.2z" fill="#F95F5D" p-id="33951"></path><path d="M923.2 960H100.8C62.4 960 32 929.6 32 891.2v-152C32 702.4 62.4 672 100.8 672h824c38.4 0 68.8 30.4 68.8 68.8v152c-1.6 36.8-32 67.2-70.4 67.2z" fill="#7ECF3B" p-id="33952"></path><path d="M624 32v960H400V32z" fill="#FDAF42" p-id="33953"></path><path d="M632 616h-240c-22.4 0-40-17.6-40-40v-128c0-22.4 17.6-40 40-40h240c22.4 0 40 17.6 40 40v128c0 22.4-17.6 40-40 40z m-232-48h224v-112H400v112z" fill="#FFFFFF" p-id="33954"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/svg/audiofile.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742888981247" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10532" id="mx_n_1742888981248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M945.587697 291.972171v653.744416c0 42.592457-35.23398 77.439766-78.306847 77.439766H156.812889C113.740022 1023.156353 78.506042 988.309044 78.506042 945.716587V78.916148C78.506042 36.323691 113.751739 1.476382 156.812889 1.476382h503.141706" fill="#409eff" p-id="10533" data-spm-anchor-id="a313x.search_index.0.i24.20d13a81QCpEmN" class=""></path><path d="M659.954595 1.476382L945.587697 291.983889H691.884291s-25.778103-9.830831-31.929696-39.335042z" fill="#f5f5f5" p-id="10534"></path><path d="M666.153057 358.268766l-266.53387 41.549616a5.143903 5.143903 0 0 0-2.343464 0.632735h-9.502746c-6.456243 0-11.881362 4.757232-11.881362 11.096302v284.379348a73.44416 73.44416 0 0 0-35.316002-8.881729c-39.053826 0-70.632003 29.480776-70.632003 65.945076s31.636763 65.945075 70.632003 65.945075 70.632003-29.480776 70.632003-65.945075c0-2.530941-0.339802-5.389967-0.339802-7.932626a10.05346 10.05346 0 0 0 0.339802-3.163676V510.781399l247.551813-40.893446v160.093739a73.39729 73.39729 0 0 0-35.339436-8.870011c-39.053826 0-70.632003 29.480776-70.632003 65.945075s31.636763 65.945075 70.632003 65.945075 70.632003-29.480776 70.632003-65.945075c0-2.530941-0.339802-5.389967-0.339803-7.920908a10.10033 10.10033 0 0 0 0.339803-3.175394V368.439399c0-5.389967-4.077627-9.830831-9.842549-10.779934a10.70963 10.70963 0 0 0-7.135848-1.26547l-6.11644 0.949103h-0.679605z m0 0" fill="#F5F5F5" p-id="10535"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/assets/svg/cancle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746605979357" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5945" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M850.538343 895.516744c-11.494799 0-22.988574-4.386914-31.763424-13.161764L141.103692 204.669426c-17.548678-17.534352-17.548678-45.992497 0-63.525825 17.548678-17.548678 45.977147-17.548678 63.525825 0l677.671227 677.685553c17.548678 17.534352 17.548678 45.992497 0 63.525825C873.526917 891.128807 862.032118 895.516744 850.538343 895.516744z" fill="#000000" p-id="5946"></path><path d="M172.867116 895.516744c-11.494799 0-22.988574-4.386914-31.763424-13.161764-17.548678-17.534352-17.548678-45.992497 0-63.525825l677.671227-677.685553c17.548678-17.548678 45.977147-17.548678 63.525825 0 17.548678 17.534352 17.548678 45.992497 0 63.525825L204.629517 882.354979C195.85569 891.128807 184.360891 895.516744 172.867116 895.516744z" fill="#000000" p-id="5947"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/svg/code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742894306389" class="svg-icon" viewBox="0 0 1027 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11488" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.5859375" height="200"><path d="M321.828571 226.742857c-14.628571-14.628571-36.571429-14.628571-51.2 0L7.314286 482.742857c-14.628571 14.628571-14.628571 36.571429 0 51.2l256 256c14.628571 14.628571 36.571429 14.628571 51.2 0 14.628571-14.628571 14.628571-36.571429 0-51.2L87.771429 512l234.057142-234.057143c7.314286-14.628571 7.314286-36.571429 0-51.2z m263.314286 0c-14.628571 0-36.571429 7.314286-43.885714 29.257143l-131.657143 497.371429c-7.314286 21.942857 7.314286 36.571429 29.257143 43.885714s36.571429-7.314286 43.885714-29.257143l131.657143-497.371429c7.314286-14.628571-7.314286-36.571429-29.257143-43.885714z m431.542857 256l-256-256c-14.628571-14.628571-36.571429-14.628571-51.2 0-14.628571 14.628571-14.628571 36.571429 0 51.2L936.228571 512l-234.057142 234.057143c-14.628571 14.628571-14.628571 36.571429 0 51.2 14.628571 14.628571 36.571429 14.628571 51.2 0l256-256c14.628571-14.628571 14.628571-43.885714 7.314285-58.514286z" fill="" p-id="11489"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/svg/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745740018080" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3474" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M394.666667 106.666667h448a74.666667 74.666667 0 0 1 74.666666 74.666666v448a74.666667 74.666667 0 0 1-74.666666 74.666667H394.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V181.333333a74.666667 74.666667 0 0 1 74.666667-74.666666z m0 64a10.666667 10.666667 0 0 0-10.666667 10.666666v448a10.666667 10.666667 0 0 0 10.666667 10.666667h448a10.666667 10.666667 0 0 0 10.666666-10.666667V181.333333a10.666667 10.666667 0 0 0-10.666666-10.666666H394.666667z m245.333333 597.333333a32 32 0 0 1 64 0v74.666667a74.666667 74.666667 0 0 1-74.666667 74.666666H181.333333a74.666667 74.666667 0 0 1-74.666666-74.666666V394.666667a74.666667 74.666667 0 0 1 74.666666-74.666667h74.666667a32 32 0 0 1 0 64h-74.666667a10.666667 10.666667 0 0 0-10.666666 10.666667v448a10.666667 10.666667 0 0 0 10.666666 10.666666h448a10.666667 10.666667 0 0 0 10.666667-10.666666v-74.666667z" fill="#000000" p-id="3475"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/svg/deletemsg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745740451964" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22293" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M781.28 851.36a58.56 58.56 0 0 1-58.56 58.56H301.28a58.72 58.72 0 0 1-58.56-58.56V230.4h538.56z m-421.6-725.92a11.84 11.84 0 0 1 12-12h281.28a11.84 11.84 0 0 1 12 12V160H359.68zM956.8 160H734.72v-34.56a81.76 81.76 0 0 0-81.76-81.76H371.68a82.08 82.08 0 0 0-81.76 81.76V160H67.2a35.36 35.36 0 0 0 0 70.56h105.12v620.8a128.96 128.96 0 0 0 128.96 128.96h421.44a128.96 128.96 0 0 0 128.96-128.96V230.4H956.8a35.2 35.2 0 0 0 35.2-35.2 34.56 34.56 0 0 0-35.2-35.2zM512 804.16a35.2 35.2 0 0 0 35.2-35.36V393.92a35.2 35.2 0 1 0-70.4 0V768.8a35.2 35.2 0 0 0 35.2 35.36m-164.32 0a35.36 35.36 0 0 0 35.36-35.36V393.92a35.36 35.36 0 1 0-70.56 0V768.8a36.32 36.32 0 0 0 35.2 35.36m328.64 0a35.36 35.36 0 0 0 35.2-35.36V393.92a35.36 35.36 0 1 0-70.56 0V768.8a35.36 35.36 0 0 0 35.36 35.36" fill="#000000" p-id="22294"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 972 B After Width: | Height: | Size: 972 B |
1
src/assets/svg/document.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="eLpfKCFis3W1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="svg-icon" viewBox="0 0 1024 1024" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" project-id="3158866e09674fdba1f410069e2041ca" export-id="664c0c49075e4a0caa3d6954772a62f4" cached="false"><path d="M945.587697,291.972171v653.744416c0,42.592457-35.23398,77.439766-78.306847,77.439766h-710.467961c-43.072867,0-78.306847-34.847309-78.306847-77.439766v-866.800439c0-42.592457,35.245697-77.439766,78.306847-77.439766h503.141706" fill="#409eff"/><path d="M659.954595,1.476382L945.587697,291.983889h-253.703406c0,0-25.778103-9.830831-31.929696-39.335042v-251.172465Z" fill="#f5f5f5"/><path d="M254.91983,471.097609c65.115255-.000001,463.752463,0,463.752463,0c68.644911,0,68.644911,95.054852,0,95.054852s-398.637208,0-463.752463,0-65.115255-95.054866,0-95.054852Z" transform="translate(32.61514-32.932245)" fill="#fff" stroke="rgba(0,0,0,0)" stroke-width="2.048"/><path d="M254.91983,471.097609c65.115255-.000001,463.752463,0,463.752463,0c68.644911,0,68.644911,95.054852,0,95.054852s-398.637208,0-463.752463,0-65.115255-95.054866,0-95.054852Z" transform="translate(32.61514 300.088283)" fill="#fff" stroke="rgba(0,0,0,0)" stroke-width="2.048"/><path d="M254.91983,471.097609c65.115255-.000001,463.752463,0,463.752463,0c68.644911,0,68.644911,95.054852,0,95.054852s-398.637208,0-463.752463,0-65.115255-95.054866,0-95.054852Z" transform="translate(32.61514 133.578019)" fill="#fff" stroke="rgba(0,0,0,0)" stroke-width="2.048"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/assets/svg/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742886980365" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7852" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M907.636364 791.272727c0 32.093091-26.088727 58.181818-58.181819 58.181818h-674.90909A58.228364 58.228364 0 0 1 116.363636 791.272727V314.181818h733.090909c32.093091 0 58.181818 26.088727 58.181819 58.181818v418.909091zM116.363636 197.818182c0-6.4 5.236364-11.636364 11.636364-11.636364h305.058909c4.096 0 7.936 2.187636 10.030546 5.725091l30.999272 52.456727H116.363636v-46.545454z m733.090909 46.545454H555.170909l-51.968-87.970909A81.780364 81.780364 0 0 0 433.058909 116.363636H128C83.083636 116.363636 46.545455 152.901818 46.545455 197.818182V791.272727c0 70.562909 57.437091 128 128 128h674.90909c70.562909 0 128-57.437091 128-128V372.363636c0-70.562909-57.437091-128-128-128z" fill="#000" p-id="7853"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/svg/filetemplate.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="eMzCWX3bu2x1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="svg-icon" viewBox="0 0 1024 1024" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" project-id="3158866e09674fdba1f410069e2041ca" export-id="c57776a9e1bf40d190c4b69a8f6ad9e3" cached="false"><path d="M945.587697,291.972171v653.744416c0,42.592457-35.23398,77.439766-78.306847,77.439766h-710.467961c-43.072867,0-78.306847-34.847309-78.306847-77.439766v-866.800439c0-42.592457,35.245697-77.439766,78.306847-77.439766h503.141706" fill="#409eff"/><path d="M659.954595,1.476382L945.587697,291.983889h-253.703406c0,0-25.778103-9.830831-31.929696-39.335042v-251.172465Z" fill="#f5f5f5"/></svg>
|
||||
|
After Width: | Height: | Size: 716 B |
1
src/assets/svg/forward.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745740069030" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4754" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M832 250.352L468.215 612.354c-15.66 15.582-40.986 15.52-56.569-0.14-15.582-15.659-15.52-40.985 0.14-56.568L777.222 192H626c-22.091 0-40-17.909-40-40s17.909-40 40-40h174c61.856 0 112 50.144 112 112v174c0 22.091-17.909 40-40 40s-40-17.909-40-40V250.352z m0 339.909c0-22.092 17.909-40 40-40s40 17.908 40 40V800c0 61.856-50.144 112-112 112H224c-61.856 0-112-50.144-112-112V224c0-61.856 50.144-112 112-112h209.74c22.09 0 40 17.909 40 40s-17.91 40-40 40H224c-17.673 0-32 14.327-32 32v576c0 17.673 14.327 32 32 32h576c17.673 0 32-14.327 32-32V590.26z" fill="#000000" p-id="4755"></path></svg>
|
||||
|
After Width: | Height: | Size: 922 B |
1
src/assets/svg/forwardobo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746604353367" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4613" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M933.686613 826.823111c0 33.28-26.908444 60.245333-60.131555 60.245333H151.350613a60.188444 60.188444 0 0 1-60.188444-60.245333H0.879502c-9.102222 93.809778 53.930667 150.641778 120.376889 150.641778h782.392889A120.433778 120.433778 0 0 0 1024.026169 856.974222v-180.679111h-90.339556v150.528zM978.91328 0H587.688391a45.169778 45.169778 0 0 0 0 90.282667h297.244445L446.490169 529.123556a45.169778 45.169778 0 0 0 63.829333 63.886222l423.367111-423.765334v267.434667a45.169778 45.169778 0 0 0 90.225778 0V45.169778a44.942222 44.942222 0 0 0-44.942222-45.169778z" fill="#000000" p-id="4614"></path><path d="M0.026169 102.4m42.666667 0l426.666666 0q42.666667 0 42.666667 42.666667l0 0q0 42.666667-42.666667 42.666666l-426.666666 0q-42.666667 0-42.666667-42.666666l0 0q0-42.666667 42.666667-42.666667Z" fill="#000000" p-id="4615"></path><path d="M0.026169 327.395556m42.666667 0l290.133333 0q42.666667 0 42.666667 42.666666l0 0q0 42.666667-42.666667 42.666667l-290.133333 0q-42.666667 0-42.666667-42.666667l0 0q0-42.666667 42.666667-42.666666Z" fill="#000000" p-id="4616"></path><path d="M0.026169 552.334222m42.666667 0l290.133333 0q42.666667 0 42.666667 42.666667l0 0q0 42.666667-42.666667 42.666667l-290.133333 0q-42.666667 0-42.666667-42.666667l0 0q0-42.666667 42.666667-42.666667Z" fill="#000000" p-id="4617"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/assets/svg/image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742886914618" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3037" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M896 160H128c-17.67 0-32 14.33-32 32v640c0 17.67 14.33 32 32 32h768c17.67 0 32-14.33 32-32V192c0-17.67-14.33-32-32-32z m-32 64v472.01l-227.2-170.4c-16.97-12.72-40.62-12.72-57.59 0l-98.58 73.92L349.2 494.39c-16.47-13.19-39.27-14.19-56.81-2.39L160 581.02V224h704zM160 800V658.16l158.36-106.48 161.02 128.8L608 584l256 192v24H160z" fill="#000" p-id="3038"></path><path d="M704 480c52.94 0 96-43.06 96-96s-43.06-96-96-96-96 43.06-96 96 43.06 96 96 96z m0-128c17.64 0 32 14.36 32 32s-14.36 32-32 32-32-14.36-32-32 14.36-32 32-32z" fill="#000" p-id="3039"></path></svg>
|
||||
|
After Width: | Height: | Size: 900 B |
1
src/assets/svg/imageloadfailed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744875720033" class="svg-icon" viewBox="0 0 1412 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9979" xmlns:xlink="http://www.w3.org/1999/xlink" width="275.78125" height="200"><path d="M1393.516743 51.782272a71.326417 71.326417 0 0 0-49.610701-22.527848L803.837949 8.24491l-40.906759 89.140366 56.231346 167.72301-101.675454 190.815821 41.436411 153.634277 102.540552 131.794976 148.690862-135.767363c6.956091-6.355819 16.189684-9.622004 25.599828-9.268903 9.392489 0.353101 18.449531 4.466729 24.717075 11.352199l191.168921 204.392557a35.310107 35.310107 0 0 1 6.267544 38.699878 35.857414 35.857414 0 0 1-33.774117 20.479862l-523.878409-20.303312-26.129479 74.151226 28.389326 66.912653 602.213882 23.075155a71.785448 71.785448 0 0 0 51.199656-18.767322 69.790427 69.790427 0 0 0 22.810329-49.028084l33.597567-844.917905a69.084225 69.084225 0 0 0-18.802632-50.581729zM1006.517966 439.257736c-59.020845-2.242192-104.976949-51.446826-102.717103-109.779124 2.259847-58.314642 52.047098-103.741096 111.067943-101.516559 59.020845 2.259847 104.994604 51.464482 102.717103 109.779124-2.259847 58.332297-52.047098 103.741096-111.067943 101.516559z m-388.234631 487.049966l18.09643-77.752856-425.345554 28.901323A35.769139 35.769139 0 0 1 176.553891 858.0003a35.115902 35.115902 0 0 1 5.049346-38.894084l315.125053-357.691388a35.804449 35.804449 0 0 1 25.952929-12.023091 36.08693 36.08693 0 0 1 26.623821 10.822548l105.506601 108.755131-45.444108-115.976048 80.083323-200.084724-73.798124-160.184302L646.443146 0 66.351046 39.564975C26.980277 42.319164-2.591938 75.863766 0.179905 114.757849l58.844294 843.717361a69.419671 69.419671 0 0 0 24.205079 48.162987 72.562271 72.562271 0 0 0 51.711652 17.213677l517.963965-35.274797-34.65687-62.28703z" p-id="9980"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/svg/multiselect.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745740175552" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7413" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M727.984 393.184a31.968 31.968 0 0 0-45.248 0.256L449.44 629.248l-103.28-106.112a32 32 0 1 0-45.888 44.608l126.032 129.504c0.048 0.096 0.192 0.096 0.256 0.192 0.064 0.064 0.096 0.192 0.16 0.256 2.016 1.984 4.512 3.2 6.88 4.544 1.248 0.672 2.24 1.792 3.52 2.304a31.728 31.728 0 0 0 24.064 0.064c1.232-0.512 2.208-1.536 3.392-2.176 2.4-1.344 4.896-2.528 6.944-4.544 0.064-0.064 0.096-0.192 0.192-0.256 0.064-0.096 0.16-0.128 0.256-0.192l256.224-259.008a32 32 0 0 0-0.224-45.248zM832.992 928h-640c-52.928 0-96-43.072-96-96V192c0-52.928 43.072-96 96-96h640c52.928 0 96 43.072 96 96v640c0 52.928-43.056 96-96 96z m-640-768c-17.632 0-32 14.368-32 32v640c0 17.664 14.368 32 32 32h640a32 32 0 0 0 32-32V192c0-17.632-14.336-32-32-32h-640z" fill="#000000" p-id="7414"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/svg/pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742561362047" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7934" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M682.666667 213.333333a42.666667 42.666667 0 0 1 42.666666 42.666667v512a42.666667 42.666667 0 0 1-85.333333 0V256a42.666667 42.666667 0 0 1 42.666667-42.666667zM341.333333 213.333333a42.666667 42.666667 0 0 1 42.666667 42.666667v512a42.666667 42.666667 0 0 1-85.333333 0V256a42.666667 42.666667 0 0 1 42.666666-42.666667z" fill="#000" p-id="7935"></path></svg>
|
||||
|
After Width: | Height: | Size: 698 B |
1
src/assets/svg/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742561328036" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6809" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M213.333333 65.386667a85.333333 85.333333 0 0 1 43.904 12.16L859.370667 438.826667a85.333333 85.333333 0 0 1 0 146.346666L257.237333 946.453333A85.333333 85.333333 0 0 1 128 873.28V150.72a85.333333 85.333333 0 0 1 85.333333-85.333333z m0 64a21.333333 21.333333 0 0 0-21.184 18.837333L192 150.72v722.56a21.333333 21.333333 0 0 0 30.101333 19.456l2.197334-1.152L826.453333 530.282667a21.333333 21.333333 0 0 0 2.048-35.178667l-2.048-1.386667L224.298667 132.416A21.333333 21.333333 0 0 0 213.333333 129.386667z" fill="#000" p-id="6810"></path></svg>
|
||||
|
After Width: | Height: | Size: 883 B |
1
src/assets/svg/quote.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745740304487" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16699" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M309.44 997.376h-92.8l1.6-131.648-83.456 1.472-5.504-1.28a186.752 186.752 0 0 1-82.816-42.88l-4.48-4.608a190.4 190.4 0 0 1-41.088-84.928L0 724.224l1.024-555.2c6.464-30.848 20.608-60.032 40.768-84.352l4.48-4.672A181.824 181.824 0 0 1 129.728 36.864l9.92-1.088 756.608 1.536c29.184 7.68 56.896 22.016 80.064 41.408l4.864 4.736c21.888 24.576 36.48 54.656 42.112 87.104l0.704 8.064-0.96 554.816c-6.4 30.976-20.544 60.16-40.832 84.544l-4.544 4.736a184.256 184.256 0 0 1-83.136 43.008l-10.368 1.088-385.152-1.792-189.568 132.352z m3.072-226.432l-1.344 111.68 158.72-110.848 408.96 1.92a92.16 92.16 0 0 0 33.92-17.92c8.576-10.944 14.784-23.68 18.176-37.12V183.104a83.712 83.712 0 0 0-17.088-35.2 115.648 115.648 0 0 0-35.968-19.008H145.216a88.832 88.832 0 0 0-33.856 17.792 101.952 101.952 0 0 0-18.24 37.184v535.168c3.328 13.376 9.536 25.984 17.984 36.8 9.92 8.32 21.504 14.4 33.984 17.984l167.424-2.88z" fill="#000000" p-id="16700" data-spm-anchor-id="a313x.search_index.0.i23.4a633a81AeDDIC" class=""></path><path d="M353.088 262.4h70.4v66.432s-106.368 59.2-105.92 132.416H423.68v165.504H211.84V428.16A296.768 296.768 0 0 1 353.088 262.4zM677.44 262.4h70.4v66.432s-106.368 59.2-105.92 132.416h105.92v165.504H536.256V428.16a296.832 296.832 0 0 1 141.184-165.76z" fill="#000000" p-id="16701" data-spm-anchor-id="a313x.search_index.0.i24.4a633a81AeDDIC" class="selected"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/assets/svg/revoke.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745741368623" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23509" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M237.303467 377.216l113.152 106.026667c16.584533 16.763733 33.6512 43.933867 17.066666 60.693333-16.597333 16.759467-39.227733 16.759467-55.816533 0L138.368 368.3968c-13.162667-13.2608-14.122667-34.491733-0.96-47.752533l174.301867-178.2784c16.5888-16.759467 39.223467-16.759467 55.812266 0s-0.477867 43.933867-17.066666 60.689066L238.775467 313.216h380.881066c153.211733 0 276.343467 132.881067 276.343467 285.738667 0 152.853333-123.136 298.845867-276.343467 298.845866H213.457067c-23.317333 0-42.88-10.824533-42.88-34.133333 0-23.313067 19.562667-29.870933 42.88-29.870933h402.816c102.762667 0 215.714133-132.322133 215.714133-234.845867s-112.951467-221.725867-215.714133-221.725867H237.303467z" fill="#000" p-id="23510"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/svg/videofile.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745812557561" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3575" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M160 0h512l256 256v704c0 35.3472-28.6528 64-64 64H160c-35.3472 0-64-28.6528-64-64V64c0-35.3472 28.6528-64 64-64z" fill="#409eff" p-id="3576" data-spm-anchor-id="a313x.search_index.0.i4.41e13a81lwq0Kw" class=""></path><path d="M702.2976 579.2896l-298.5664 177.984c-19.9488 12.0192-45.3312-2.4128-45.3312-25.856v-355.968c0-22.848 25.3824-37.2736 45.3312-25.856l298.56 177.984c19.3408 12.032 19.3408 40.288 0 51.712z" fill="#FFFFFF" p-id="3577" data-spm-anchor-id="a313x.search_index.0.i3.41e13a81lwq0Kw" class=""></path><path d="M672 0l256 256h-192c-35.3472 0-64-28.6528-64-64V0z" fill="#f5f5f5" p-id="3578" data-spm-anchor-id="a313x.search_index.0.i0.41e13a81lwq0Kw" class=""></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/svg/videoloadfailed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744876866180" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13144" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M876.416 272.137H707.763L840.277 91.69c18.065-24.047 12.05-60.135-12.049-81.186C804.13-7.552 768-1.545 746.914 22.519L566.212 269.133h-72.286l-271.06-168.431c-27.103-15.027-63.241-9.02-78.303 18.048-12.049 18.048-12.049 33.084-9.037 48.12 3.013 15.035 12.05 27.076 24.09 36.087l114.44 69.171H147.585C66.261 272.137 0 338.304 0 419.498v457.114C0 957.815 66.261 1024 147.584 1024h728.832C957.73 1024 1024 957.815 1024 876.62V419.5c0-81.195-66.27-147.362-147.584-147.362zM150.579 572.894l-3.004-15.044c-3.02-21.06 9.037-42.112 30.115-45.133l162.636-30.063c21.078-3.004 42.155 9.037 45.167 30.063l3.021 15.053c3.012 21.051-9.037 42.103-30.114 45.124l-162.637 30.054c-21.077 3.021-42.163-9.002-45.184-30.054z m472.858 228.548c-15.07 9.028-30.114 15.053-54.221 15.053-18.065 0-36.13-9.028-51.183-27.076-3.02-3.021-6.033-3.021-9.045 0-36.139 36.087-93.372 33.083-126.498-6.016-15.053-21.06-24.09-42.104-24.09-69.163 0-15.053 12.05-24.073 24.09-24.073 12.049 0 24.09 9.029 24.09 24.073 0 12.023 3.02 27.06 12.048 36.079 12.05 18.03 33.135 21.052 48.188 6.024 12.04-9.028 18.074-21.051 18.074-36.096v-6.007c0-27.076 51.2-27.076 51.2 0 0 12.023 3.003 24.047 12.032 36.079 18.082 21.052 42.163 21.052 57.233-3.004 6.024-9.02 9.045-21.052 9.045-33.075 0-12.04 9.037-21.077 18.065-24.073s21.086 3.004 27.102 12.032c3.03 3.004 3.03 6.016 3.03 12.041 3.003 36.079-9.046 66.15-39.16 87.202zM876.416 557.85l-3.02 15.044c-3.005 21.052-24.082 36.079-45.168 30.054l-159.616-30.054c-21.086-3.02-36.147-24.073-30.114-45.124l3.004-15.053c3.02-21.035 24.098-36.088 45.184-30.063l159.616 30.063c21.077 3.02 33.135 24.072 30.114 45.133z m0 0" p-id="13145"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/svg/vote.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742894802568" class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4325" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M256.410578 940.384405H91.122722c-15.462716 0-27.999168-12.536452-27.999168-27.999168v-592.645304c0-15.462716 12.536452-27.999168 27.999168-27.999168h165.287856c15.462716 0 27.999168 12.536452 27.999168 27.999168v592.645304c0 15.462716-12.536452 27.999168-27.999168 27.999168zM119.12189 884.386069h109.28952V347.740125H119.12189v536.645944zM595.723103 940.384405H430.435247c-15.462716 0-27.999168-12.536452-27.999168-27.999168V500.341018c0-15.462716 12.536452-27.999168 27.999168-27.999168h165.287856c15.462716 0 27.999168 12.536452 27.999168 27.999168v412.044219c0 15.462716-12.536452 27.999168-27.999168 27.999168z m-137.288688-55.998336h109.28952v-356.045883h-109.28952v356.045883zM928.92508 940.384405H763.62289c-15.462716 0-27.999168-12.536452-27.999168-27.999168V111.852305c0-15.462716 12.536452-27.999168 27.999168-27.999169h165.30219c15.462716 0 27.999168 12.536452 27.999168 27.999169v800.533956c0 15.461692-12.537476 27.998144-27.999168 27.998144z m-137.303022-55.998336h109.303854V139.851473H791.622058v744.534596z" fill="#000" p-id="4326"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, ArrowLeft, ArrowRight, Edit, Check } from '@element-plus/icons-vue'
|
||||
import { Search, ArrowLeft, ArrowRight, Edit } from '@element-plus/icons-vue'
|
||||
import { el_loading_options, PARTITION_TYPE } from '@/const/commonConst'
|
||||
import GroupItem from '@/components/item/GroupItem.vue'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
@@ -9,11 +9,17 @@ import GroupAvatarIcon from '@/components/common/GroupAvatarIcon.vue'
|
||||
import AddButton from '@/components/common/AddButton.vue'
|
||||
import DeleteButton from '@/components/common/DeleteButton.vue'
|
||||
import EditAvatar from '@/components/common/EditAvatar.vue'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import { combineId, smartMatch } from '@/js/utils/common'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { groupStore, userStore, messageStore, userCardStore, groupCardStore } from '@/stores'
|
||||
import SelectUserDialog from '../common/SelectUserDialog.vue'
|
||||
import SingleSelectDialog from '../common/SingleSelectDialog.vue'
|
||||
import {
|
||||
useGroupStore,
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useUserCardStore,
|
||||
useGroupCardStore
|
||||
} from '@/stores'
|
||||
import SelectUserDialog from '@/components/common/SelectUserDialog.vue'
|
||||
import SelectUserSingleDialog from '@/components/common/SelectUserSingleDialog.vue'
|
||||
import {
|
||||
groupAddMembersService,
|
||||
groupDelMembersService,
|
||||
@@ -28,13 +34,13 @@ import router from '@/router'
|
||||
import GroupMembersTable from '../common/GroupMembersTable.vue'
|
||||
import { msgChatCreateSessionService } from '@/api/message'
|
||||
|
||||
const groupData = groupStore()
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const groupData = useGroupStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
const isShowSelectDialog = ref(false)
|
||||
const isShowSingleSelectDialog = ref(false)
|
||||
const isShowSelectUserSingleDialog = ref(false)
|
||||
const isShowEditAvatar = ref(false)
|
||||
const myAccount = computed(() => userData.user.account)
|
||||
const newGroupName = ref('')
|
||||
@@ -155,7 +161,7 @@ const validMembersSorted = computed(() => {
|
||||
data.push(item)
|
||||
} else {
|
||||
if (
|
||||
item.nickName.toLowerCase().includes(memberSearchKey.value.toLowerCase()) ||
|
||||
smartMatch(item.nickName, memberSearchKey.value) ||
|
||||
item.account === memberSearchKey.value
|
||||
) {
|
||||
data.push(item)
|
||||
@@ -217,6 +223,8 @@ const onShowUserCard = (account) => {
|
||||
...messageData.sessionList[sessionId].objectInfo,
|
||||
nickName: res.data.data.nickName,
|
||||
signature: res.data.data.signature,
|
||||
avatarId: res.data.data.avatarId,
|
||||
avatar: res.data.data.avatar,
|
||||
avatarThumb: res.data.data.avatarThumb,
|
||||
gender: res.data.data.gender,
|
||||
phoneNum: res.data.data.phoneNum,
|
||||
@@ -357,18 +365,18 @@ const onReturnInfo = () => {
|
||||
groupCardData.setShowModel('info')
|
||||
}
|
||||
|
||||
const onNewAvatar = ({ avatar, avatarThumb }) => {
|
||||
const onNewAvatar = ({ avatarId, avatar, avatarThumb }) => {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
groupUpdateInfoService({
|
||||
groupId: groupCardData.groupId,
|
||||
avatar: avatar,
|
||||
avatarThumb: avatarThumb
|
||||
avatarId: avatarId
|
||||
})
|
||||
.then(() => {
|
||||
groupData.setGroupInfo({
|
||||
groupId: groupCardData.groupId,
|
||||
groupInfo: {
|
||||
...groupInfo.value,
|
||||
avatarId: avatarId,
|
||||
avatar: avatar,
|
||||
avatarThumb: avatarThumb
|
||||
}
|
||||
@@ -653,7 +661,7 @@ const onConfirmSingleSelect = (selected) => {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isShowSingleSelectDialog.value = false
|
||||
isShowSelectUserSingleDialog.value = false
|
||||
loadingInstance.close()
|
||||
})
|
||||
}
|
||||
@@ -681,7 +689,7 @@ const onChangePartition = () => {
|
||||
:modelValue="groupCardData.isShow"
|
||||
:direction="'rtl'"
|
||||
:size="385"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
modal-class="group-card-modal"
|
||||
:show-close="false"
|
||||
@close="groupCardData.setClosed()"
|
||||
@@ -870,14 +878,14 @@ const onChangePartition = () => {
|
||||
>
|
||||
<span
|
||||
style="font-size: 14px; cursor: pointer"
|
||||
@click="isShowSingleSelectDialog = true"
|
||||
@click="isShowSelectUserSingleDialog = true"
|
||||
>转移群主</span
|
||||
>
|
||||
<el-button
|
||||
:icon="ArrowRight"
|
||||
size="small"
|
||||
circle
|
||||
@click="isShowSingleSelectDialog = true"
|
||||
@click="isShowSelectUserSingleDialog = true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -969,7 +977,11 @@ const onChangePartition = () => {
|
||||
<div
|
||||
style="width: 240px; display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<el-select v-model="newPartitionId" placeholder="请选择分组" style="width: 200px">
|
||||
<el-select
|
||||
v-model="newPartitionId"
|
||||
placeholder="请选择分组"
|
||||
@change="onChangePartition()"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in Object.values(partitions)"
|
||||
:key="item.partitionId"
|
||||
@@ -977,14 +989,6 @@ const onChangePartition = () => {
|
||||
:value="item.partitionId"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="Check"
|
||||
size="small"
|
||||
title="确认"
|
||||
circle
|
||||
@click="onChangePartition()"
|
||||
></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1033,8 +1037,8 @@ const onChangePartition = () => {
|
||||
</div>
|
||||
</template>
|
||||
</SelectUserDialog>
|
||||
<SingleSelectDialog
|
||||
v-model="isShowSingleSelectDialog"
|
||||
<SelectUserSingleDialog
|
||||
v-model="isShowSelectUserSingleDialog"
|
||||
:options="validMembersSorted"
|
||||
:disabledOptionIds="new Array(myAccount)"
|
||||
@showUserCard="onShowUserCard"
|
||||
@@ -1043,7 +1047,7 @@ const onChangePartition = () => {
|
||||
<template #title>
|
||||
<div style="font-size: 16px; font-weight: bold; white-space: nowrap">转移群主</div>
|
||||
</template>
|
||||
</SingleSelectDialog>
|
||||
</SelectUserSingleDialog>
|
||||
<EditAvatar
|
||||
v-model="isShowEditAvatar"
|
||||
:model="'group'"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import {
|
||||
Close,
|
||||
Male,
|
||||
@@ -7,20 +7,20 @@ import {
|
||||
Check,
|
||||
Edit,
|
||||
ChatRound,
|
||||
Microphone,
|
||||
Phone,
|
||||
VideoCamera
|
||||
} from '@element-plus/icons-vue'
|
||||
import default_avatar from '@/assets/image/default_avatar.png'
|
||||
import { userStore, messageStore, userCardStore } from '@/stores'
|
||||
import { useUserStore, useMessageStore, useUserCardStore } from '@/stores'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { msgChatCreateSessionService } from '@/api/message'
|
||||
import router from '@/router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
|
||||
const sessionId = computed(() => {
|
||||
return combineId(userData.user.account, userCardData.userInfo?.account)
|
||||
@@ -135,12 +135,18 @@ const onVideoCall = () => {
|
||||
ElMessage.warning('功能开发中')
|
||||
}
|
||||
|
||||
const showAvatar = ref(userCardData.userInfo.avatarThumb || default_avatar)
|
||||
const showAvatar = ref(userCardData.userInfo.avatarThumb)
|
||||
|
||||
const handleAvatarError = () => {
|
||||
console.log('handleAvatarError')
|
||||
showAvatar.value = default_avatar
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userCardData.userInfo.avatarThumb,
|
||||
(newValue) => {
|
||||
showAvatar.value = newValue || default_avatar
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -151,10 +157,6 @@ const handleAvatarError = () => {
|
||||
:show-close="false"
|
||||
@close="onClose"
|
||||
>
|
||||
<template #header>
|
||||
<div style="background-color: red"></div>
|
||||
</template>
|
||||
|
||||
<div class="user-card" @click.self="preventClose($event)">
|
||||
<div class="header">
|
||||
<el-icon class="close-button" @click="onClose"><Close /></el-icon>
|
||||
@@ -291,7 +293,7 @@ const handleAvatarError = () => {
|
||||
color="#409eff"
|
||||
@click="onVoiceCall"
|
||||
>
|
||||
<Microphone />
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-button"
|
||||
@@ -417,6 +419,7 @@ const handleAvatarError = () => {
|
||||
|
||||
.body {
|
||||
width: 100%;
|
||||
padding: 5px 0 5px 0;
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
@@ -425,7 +428,7 @@ const handleAvatarError = () => {
|
||||
|
||||
.signature {
|
||||
width: 80%;
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -77,6 +77,7 @@ onUnmounted(() => {
|
||||
.drag-line {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover,
|
||||
&.drag_resizing {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { Plus, Upload, RefreshLeft, RefreshRight, Refresh } from '@element-plus/icons-vue'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { Plus, Check, RefreshLeft, RefreshRight, Refresh } from '@element-plus/icons-vue'
|
||||
import { mtsUploadServiceForImage } from '@/api/mts'
|
||||
import { getMd5 } from '@/js/utils/file'
|
||||
import { prehandleImage } from '@/js/utils/image'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
|
||||
const props = defineProps(['modelValue', 'model', 'groupInfo'])
|
||||
const emit = defineEmits(['update:modelValue', 'update:newAvatar'])
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const cropper = ref()
|
||||
const srcImg = ref('')
|
||||
@@ -17,6 +18,7 @@ const previewImg = ref('')
|
||||
const isLoading = ref(false)
|
||||
const fileName = ref('')
|
||||
const resetData = ref({})
|
||||
const currentBlobUrl = ref('') // 新增变量,用于存储当前的 Blob URL
|
||||
|
||||
const avatar = computed(() => {
|
||||
if (props.model === 'user') {
|
||||
@@ -30,8 +32,8 @@ const avatar = computed(() => {
|
||||
|
||||
// 打开的时候触发
|
||||
const onOpen = () => {
|
||||
fileName.value = avatar.value?.split('/').pop()
|
||||
srcImg.value = import.meta.env.VITE_OSS_CORS_FLAG + avatar.value
|
||||
fileName.value = avatar.value?.split('/').pop().split('?')[0]
|
||||
srcImg.value = avatar.value ? import.meta.env.VITE_OSS_CORS_FLAG + avatar.value : avatar.value
|
||||
previewImg.value = srcImg.value
|
||||
resetData.value = {
|
||||
previewImg: previewImg.value
|
||||
@@ -41,19 +43,31 @@ const onOpen = () => {
|
||||
// 关闭的时候触发
|
||||
const onClose = () => {
|
||||
isLoading.value = false
|
||||
// 释放当前的 Blob URL
|
||||
if (currentBlobUrl.value) {
|
||||
URL.revokeObjectURL(currentBlobUrl.value)
|
||||
currentBlobUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 选择了文件触发
|
||||
const onSelected = (file) => {
|
||||
// 释放之前的 Blob URL
|
||||
if (currentBlobUrl.value) {
|
||||
URL.revokeObjectURL(currentBlobUrl.value)
|
||||
currentBlobUrl.value = ''
|
||||
}
|
||||
|
||||
fileName.value = file.name
|
||||
srcImg.value = URL.createObjectURL(file.raw)
|
||||
previewImg.value = srcImg.value
|
||||
currentBlobUrl.value = srcImg.value // 存储当前的 Blob URL
|
||||
resetData.value = {
|
||||
previewImg: previewImg.value
|
||||
}
|
||||
}
|
||||
|
||||
const onUpload = async () => {
|
||||
const onSave = async () => {
|
||||
cropper.value.getCropBlob(async (blob) => {
|
||||
const lastDotIndex = fileName.value.lastIndexOf('.')
|
||||
const prefix = fileName.value.substring(0, lastDotIndex)
|
||||
@@ -69,13 +83,30 @@ const onUpload = async () => {
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await mtsUploadService({ file: file, storeType: 0 })
|
||||
const md5 = await getMd5(file)
|
||||
const prehandleImageObj = await prehandleImage(file)
|
||||
const files = {
|
||||
originFile: file,
|
||||
thumbFile: prehandleImageObj.thumbFile
|
||||
}
|
||||
const requestBody = {
|
||||
storeType: 0,
|
||||
md5,
|
||||
fileName: file.name,
|
||||
fileRawType: file.type,
|
||||
size: file.size,
|
||||
originWidth: prehandleImageObj.originWidth,
|
||||
originHeight: prehandleImageObj.originHeight,
|
||||
thumbWidth: prehandleImageObj.thumbWidth,
|
||||
thumbHeight: prehandleImageObj.thumbHeight
|
||||
}
|
||||
const res = await mtsUploadServiceForImage(requestBody, files)
|
||||
emit('update:newAvatar', {
|
||||
avatarId: res.data.data.objectId,
|
||||
avatar: res.data.data.originUrl,
|
||||
avatarThumb: res.data.data.thumbUrl
|
||||
})
|
||||
emit('update:modelValue', false)
|
||||
ElMessage.success('头像上传成功')
|
||||
emit('update:modelValue', false) //关闭窗口
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
} finally {
|
||||
@@ -167,6 +198,7 @@ const onRotateRight = () => {
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="onSelected"
|
||||
accept="image/*"
|
||||
style="display: flex"
|
||||
>
|
||||
<template #trigger>
|
||||
@@ -174,13 +206,13 @@ const onRotateRight = () => {
|
||||
</template>
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="Upload"
|
||||
:icon="Check"
|
||||
size="large"
|
||||
@click="onUpload"
|
||||
@click="onSave"
|
||||
:loading="isLoading"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
上传头像
|
||||
保存头像
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -230,11 +262,19 @@ const onRotateRight = () => {
|
||||
.preview-100 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
:deep(img) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-40 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
:deep(img) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@ watch([() => props.isShow, () => props.defaultInput], ([newIsShow, newDefaultInp
|
||||
:modal="false"
|
||||
:top="'40vh'"
|
||||
:width="'360px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@close="onClose"
|
||||
>
|
||||
|
||||
@@ -17,6 +17,8 @@ const avatarSize = computed(() => {
|
||||
return 50
|
||||
case 'small':
|
||||
return 30
|
||||
case 'tiny':
|
||||
return 24
|
||||
case 'default':
|
||||
default:
|
||||
return 40
|
||||
@@ -31,6 +33,8 @@ const svgSize = computed(() => {
|
||||
return 30
|
||||
case 'small':
|
||||
return 18
|
||||
case 'tiny':
|
||||
return 16
|
||||
case 'default':
|
||||
default:
|
||||
return 24
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed } from 'vue'
|
||||
import { Mute } from '@element-plus/icons-vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { userStore, groupStore, messageStore, userCardStore } from '@/stores'
|
||||
import { useUserStore, useGroupStore, useMessageStore, useUserCardStore } from '@/stores'
|
||||
import {
|
||||
groupUpdateMuteService,
|
||||
groupChangeRoleService,
|
||||
@@ -11,18 +11,18 @@ import {
|
||||
groupOwnerTransferService
|
||||
} from '@/api/group'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import { combineId, smartMatch } from '@/js/utils/common'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import MemberMenu from '@/views/message/components/MemberMenu.vue'
|
||||
import MenuMember from '@/views/message/components/MenuMember.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const props = defineProps(['groupId', 'memberSearchKey'])
|
||||
const emit = defineEmits(['openSession'])
|
||||
|
||||
const userData = userStore()
|
||||
const groupData = groupStore()
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const userData = useUserStore()
|
||||
const groupData = useGroupStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
|
||||
const myAccount = computed(() => userData.user.account)
|
||||
|
||||
@@ -50,7 +50,7 @@ const validMembersSorted = computed(() => {
|
||||
data.push(item)
|
||||
} else {
|
||||
if (
|
||||
item.nickName.toLowerCase().includes(props.memberSearchKey.toLowerCase()) ||
|
||||
smartMatch(item.nickName, props.memberSearchKey) ||
|
||||
item.account === props.memberSearchKey
|
||||
) {
|
||||
data.push(item)
|
||||
@@ -400,7 +400,7 @@ const onSelectMenu = (item) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MemberMenu :groupId="props.groupId" :account="showMenuAccount" @selectMenu="onSelectMenu">
|
||||
<MenuMember :groupId="props.groupId" :account="showMenuAccount" @selectMenu="onSelectMenu">
|
||||
<el-table
|
||||
class="group-members-table"
|
||||
:data="validMembersSorted"
|
||||
@@ -491,7 +491,7 @@ const onSelectMenu = (item) => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</MemberMenu>
|
||||
</MenuMember>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Search, Close } from '@element-plus/icons-vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { groupSearchGroupInfoService } from '@/api/group'
|
||||
import GroupItem from '../item/GroupItem.vue'
|
||||
import { smartMatch } from '@/js/utils/common'
|
||||
|
||||
/**
|
||||
* disabledOptions: 排除项的群ID,比如已经选过了某些群,那么这么群组应该在待选项里被禁用
|
||||
@@ -43,10 +44,7 @@ const optionKeys = computed(() => {
|
||||
const data = []
|
||||
Object.keys(optionsAll.value).forEach((key) => {
|
||||
const item = optionsAll.value[key]
|
||||
if (
|
||||
item.groupId === searchKey.value ||
|
||||
item.groupName.toLowerCase().includes(searchKey.value.toLowerCase())
|
||||
) {
|
||||
if (item.groupId === searchKey.value || smartMatch(item.groupName, searchKey.value)) {
|
||||
data.push(key)
|
||||
}
|
||||
})
|
||||
@@ -120,7 +118,7 @@ const onRemoveSelectedItem = (index) => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
305
src/components/common/SelectSessionDialog.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { Search, Close } from '@element-plus/icons-vue'
|
||||
import SessionTitleItem from '@/components/item/SessionTitleItem.vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { userQueryService, userQueryByNickService } from '@/api/user'
|
||||
import { combineId, smartMatch } from '@/js/utils/common'
|
||||
import { useUserStore, useMessageStore } from '@/stores'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps(['isShow', 'sessionListSortedKey'])
|
||||
const emit = defineEmits(['update:isShow', 'showUserCard', 'showGroupCard', 'confirm', 'close'])
|
||||
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
const selected = ref([])
|
||||
|
||||
const myAccount = computed(() => {
|
||||
return userData.user.account
|
||||
})
|
||||
|
||||
const searchKey = ref('')
|
||||
const optionsFromServer = ref({})
|
||||
|
||||
const optionsAll = computed(() => {
|
||||
return {
|
||||
...messageData.sessionList,
|
||||
...optionsFromServer.value
|
||||
}
|
||||
})
|
||||
|
||||
const optionKeys = computed(() => {
|
||||
const allKeys = [...props.sessionListSortedKey, ...Object.keys(optionsFromServer.value)]
|
||||
if (!searchKey.value) {
|
||||
return allKeys
|
||||
} else {
|
||||
const data = []
|
||||
allKeys.forEach((key) => {
|
||||
const item = optionsAll.value[key]
|
||||
if (
|
||||
item.sessionType === MsgType.CHAT &&
|
||||
(item.objectInfo.account === searchKey.value ||
|
||||
smartMatch(item.objectInfo.nickName, searchKey.value) ||
|
||||
smartMatch(item.mark, searchKey.value))
|
||||
) {
|
||||
data.push(key)
|
||||
} else if (
|
||||
item.sessionType === MsgType.GROUP_CHAT &&
|
||||
(item.objectInfo.groupId === searchKey.value ||
|
||||
smartMatch(item.objectInfo.groupName, searchKey.value) ||
|
||||
smartMatch(item.mark, searchKey.value))
|
||||
) {
|
||||
data.push(key)
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
let timer
|
||||
const onQuery = () => {
|
||||
if (!searchKey.value) return
|
||||
|
||||
clearTimeout(timer)
|
||||
const key = searchKey.value //在异步执行中,变量禁止使用响应式,因为在将来执行的时候响应式数据随时会发生改变
|
||||
timer = setTimeout(async () => {
|
||||
userQueryByNickService({ keyWords: key }).then((res) => {
|
||||
res.data.data?.forEach((item) => {
|
||||
const sessionId = combineId(myAccount.value, item.account)
|
||||
if (!messageData.sessionList[sessionId]) {
|
||||
// 这里先不create Session,点击确认转发才create Session
|
||||
optionsFromServer.value[sessionId] = {}
|
||||
optionsFromServer.value[sessionId].sessionId = sessionId
|
||||
optionsFromServer.value[sessionId].sessionType = MsgType.CHAT
|
||||
optionsFromServer.value[sessionId].mark = ''
|
||||
optionsFromServer.value[sessionId].objectInfo = item
|
||||
}
|
||||
})
|
||||
})
|
||||
const sessionId = combineId(myAccount.value, key)
|
||||
if (!messageData.sessionList[sessionId]) {
|
||||
userQueryService({ account: key }).then((res) => {
|
||||
if (res.data.data) {
|
||||
// 这里先不create Session,点击确认转发才create Session
|
||||
optionsFromServer.value[sessionId] = {}
|
||||
optionsFromServer.value[sessionId].sessionId = sessionId
|
||||
optionsFromServer.value[sessionId].sessionType = MsgType.CHAT
|
||||
optionsFromServer.value[sessionId].mark = ''
|
||||
optionsFromServer.value[sessionId].objectInfo = res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const onShowUserCard = (account) => {
|
||||
emit('showUserCard', { account })
|
||||
}
|
||||
|
||||
const onShowGroupCard = (groupId) => {
|
||||
emit('showGroupCard', { groupId })
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
if (selected.value.length === 0) {
|
||||
ElMessage.warning('您还没有选择目标会话')
|
||||
} else {
|
||||
const data = []
|
||||
selected.value.forEach((account) => {
|
||||
data.push(optionsAll.value[account])
|
||||
})
|
||||
emit('confirm', data)
|
||||
}
|
||||
}
|
||||
|
||||
const onOpen = () => {
|
||||
searchKey.value = ''
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
selected.value = []
|
||||
optionsFromServer.value = {}
|
||||
emit('update:isShow', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const onCancle = () => {
|
||||
emit('update:isShow', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const onClearSelected = () => {
|
||||
selected.value = []
|
||||
}
|
||||
|
||||
const onRemoveSelectedItem = (index) => {
|
||||
selected.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
class="select-dialog"
|
||||
:model-value="props.isShow"
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
<div class="main bdr-t bdr-b bdr-l bdr-r">
|
||||
<div class="left bdr-r">
|
||||
<el-input
|
||||
v-model.trim="searchKey"
|
||||
placeholder="搜索:昵称/账号/备注/群名称"
|
||||
:prefix-icon="Search"
|
||||
:clearable="true"
|
||||
@input="onQuery"
|
||||
/>
|
||||
<div v-if="optionKeys.length > 0" class="my-scrollbar" style="flex: 1; overflow-y: scroll">
|
||||
<el-checkbox-group v-model="selected">
|
||||
<el-checkbox v-for="item in optionKeys" :key="item" :value="item">
|
||||
<SessionTitleItem
|
||||
:session="optionsAll[item]"
|
||||
:keyWords="searchKey"
|
||||
@showUserCard="onShowUserCard"
|
||||
@showGroupCard="onShowGroupCard"
|
||||
></SessionTitleItem>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<HashNoData v-else></HashNoData>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="head bdr-b">
|
||||
<div style="font-size: 13px; color: gray">
|
||||
{{ `已选择:${selected.length} 个会话` }}
|
||||
</div>
|
||||
<el-button type="info" size="small" @click="onClearSelected" plain>清空</el-button>
|
||||
</div>
|
||||
<div v-if="selected.length > 0" class="my-scrollbar" style="flex: 1; overflow-y: scroll">
|
||||
<div class="selected-item" v-for="(item, index) in selected" :key="index">
|
||||
<SessionTitleItem
|
||||
:session="optionsAll[item]"
|
||||
@showUserCard="onShowUserCard"
|
||||
@showGroupCard="onShowGroupCard"
|
||||
></SessionTitleItem>
|
||||
<el-button :icon="Close" size="small" circle @click="onRemoveSelectedItem(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<HashNoData v-else></HashNoData>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="info" @click="onCancle" plain>取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm" plain>确认</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
height: 360px;
|
||||
margin: 10px 0 10px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.left {
|
||||
width: 49%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-checkbox {
|
||||
height: 45px;
|
||||
margin: 0 2px 2px 0;
|
||||
padding: 0 10px 0 10px;
|
||||
border-radius: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
background-color: #dedfe0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-checked {
|
||||
background-color: #dedfe0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.head {
|
||||
height: 30px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
height: 45px;
|
||||
margin: 0 0 2px 0;
|
||||
padding: 0 10px 0 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: black;
|
||||
--close-button-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background: #dedfe0;
|
||||
--close-button-color: auto;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border: none;
|
||||
color: var(--close-button-color);
|
||||
background-color: var(--close-button-background-color);
|
||||
|
||||
&:hover {
|
||||
--close-button-background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import { Search, Close } from '@element-plus/icons-vue'
|
||||
import ContactItem from '@/components/item/ContactItem.vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { userQueryService, userQueryByNickService } from '@/api/user'
|
||||
import { smartMatch } from '@/js/utils/common'
|
||||
|
||||
/**
|
||||
* disabledOptions: 排除项的账号数组,比如已经选过了某些用户,那么这么用户应该在待选项里被禁用
|
||||
@@ -43,10 +44,7 @@ const optionKeys = computed(() => {
|
||||
const data = []
|
||||
Object.keys(optionsAll.value).forEach((key) => {
|
||||
const item = optionsAll.value[key]
|
||||
if (
|
||||
item.account === searchKey.value ||
|
||||
item.nickName.toLowerCase().includes(searchKey.value.toLowerCase())
|
||||
) {
|
||||
if (item.account === searchKey.value || smartMatch(item.nickName, searchKey.value)) {
|
||||
data.push(key)
|
||||
}
|
||||
})
|
||||
@@ -123,7 +121,7 @@ const onRemoveSelectedItem = (index) => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { smartMatch } from '@/js/utils/common'
|
||||
|
||||
const props = defineProps(['modelValue', 'options', 'disabledOptionIds'])
|
||||
const emit = defineEmits(['update:modelValue', 'showUserCard', 'confirm'])
|
||||
@@ -16,10 +17,7 @@ const showOptions = computed(() => {
|
||||
} else {
|
||||
const data = []
|
||||
props.options.forEach((item) => {
|
||||
if (
|
||||
item.account === searchKey.value ||
|
||||
item.nickName.toLowerCase().includes(searchKey.value.toLowerCase())
|
||||
) {
|
||||
if (item.account === searchKey.value || smartMatch(item.nickName, searchKey.value)) {
|
||||
data.push(item)
|
||||
}
|
||||
})
|
||||
@@ -71,7 +69,7 @@ const onConfirm = () => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'300px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="height: 460px; border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import { getAvatarColor, getFontColor } from '@/js/utils/common'
|
||||
import { STATUS } from '@/const/userConst'
|
||||
import default_avatar from '@/assets/image/default_avatar.png'
|
||||
import { ElAvatar } from 'element-plus'
|
||||
|
||||
const props = defineProps(['showName', 'showId', 'showAvatarThumb', 'userStatus', 'size'])
|
||||
|
||||
@@ -12,12 +13,28 @@ const avatarSize = computed(() => {
|
||||
return 50
|
||||
case 'small':
|
||||
return 30
|
||||
case 'tiny':
|
||||
return 24
|
||||
case 'default':
|
||||
default:
|
||||
return 40
|
||||
}
|
||||
})
|
||||
|
||||
const avatarFontSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'large':
|
||||
return 20
|
||||
case 'small':
|
||||
return 16
|
||||
case 'tiny':
|
||||
return 14
|
||||
case 'default':
|
||||
default:
|
||||
return 18
|
||||
}
|
||||
})
|
||||
|
||||
const statusCircleSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'large':
|
||||
@@ -87,7 +104,7 @@ watch(
|
||||
<span
|
||||
class="first-char-box"
|
||||
v-else-if="firstChar"
|
||||
:style="{ backgroundColor: avatarColor, color: fontColor }"
|
||||
:style="{ backgroundColor: avatarColor, color: fontColor, fontSize: avatarFontSize + 'px' }"
|
||||
>
|
||||
{{ firstChar }}
|
||||
</span>
|
||||
@@ -112,7 +129,6 @@ watch(
|
||||
position: relative;
|
||||
|
||||
.first-char-box {
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
141
src/components/item/SessionTitleItem.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import GroupAvatarIcon from '@/components/common/GroupAvatarIcon.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { useGroupStore } from '@/stores'
|
||||
import { highLightedText } from '@/js/utils/common'
|
||||
|
||||
/**
|
||||
* objectInfo:对象详情
|
||||
* keyWords:搜索关键字,用于高亮显示检索的关键字
|
||||
*/
|
||||
const props = defineProps(['session', 'keyWords'])
|
||||
const emit = defineEmits(['showUserCard', 'showGroupCard'])
|
||||
|
||||
const groupData = useGroupStore()
|
||||
|
||||
const showName = computed(() => {
|
||||
let name = ''
|
||||
if (props.session.sessionType === MsgType.CHAT) {
|
||||
name = props.session.objectInfo.nickName
|
||||
} else if (props.session.sessionType === MsgType.GROUP_CHAT) {
|
||||
name = props.session.objectInfo.groupName
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
return props.session.mark ? `${props.session.mark}(${name})` : name
|
||||
})
|
||||
|
||||
const showId = computed(() => {
|
||||
if (props.session.sessionType === MsgType.CHAT) {
|
||||
return props.session.objectInfo.account
|
||||
} else if (props.session.sessionType === MsgType.GROUP_CHAT) {
|
||||
return props.session.objectInfo.groupId
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const onShowUserCard = (e) => {
|
||||
e.preventDefault()
|
||||
emit('showUserCard', showId.value)
|
||||
}
|
||||
|
||||
const onShowGroupCard = (e) => {
|
||||
e.preventDefault()
|
||||
emit('showGroupCard', showId.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="session-wrapper">
|
||||
<div v-if="props.session.sessionType === MsgType.CHAT" class="user-session">
|
||||
<UserAvatarIcon
|
||||
class="user-session-avatar"
|
||||
:showName="showName"
|
||||
:showId="showId"
|
||||
:showAvatarThumb="props.session.objectInfo.avatarThumb"
|
||||
:size="'small'"
|
||||
@click="onShowUserCard"
|
||||
></UserAvatarIcon>
|
||||
<div class="user-session-info">
|
||||
<span
|
||||
class="name text-ellipsis"
|
||||
:title="showName"
|
||||
v-html="highLightedText(showName, props.keyWords, '#409eff')"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="id"
|
||||
:title="showId"
|
||||
v-html="highLightedText(showId, props.keyWords, '#409eff', 'full')"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.session.sessionType === MsgType.GROUP_CHAT" class="group-session">
|
||||
<GroupAvatarIcon
|
||||
class="group-session-avatar"
|
||||
:avatarThumb="groupData.groupInfoList[props.session.objectInfo.groupId].avatarThumb"
|
||||
:size="'small'"
|
||||
@click="onShowGroupCard"
|
||||
></GroupAvatarIcon>
|
||||
<div class="group-session-info">
|
||||
<span
|
||||
class="name text-ellipsis"
|
||||
:title="showName"
|
||||
v-html="highLightedText(showName, props.keyWords, '#409eff')"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="id"
|
||||
:title="showId"
|
||||
v-html="highLightedText(showId, props.keyWords, '#409eff', 'full')"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.session-wrapper {
|
||||
padding: 2px 0 2px 5px;
|
||||
|
||||
.user-session {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
||||
.user-session-info {
|
||||
max-width: 165px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
user-select: text;
|
||||
|
||||
.id {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-session {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
||||
.group-session-info {
|
||||
max-width: 165px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
user-select: text;
|
||||
|
||||
.id {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,12 +2,12 @@
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import ContactItem from '@/components/item/ContactItem.vue'
|
||||
import GroupItem from '@/components/item/GroupItem.vue'
|
||||
import { searchStore } from '@/stores'
|
||||
import { useSearchStore } from '@/stores'
|
||||
|
||||
const props = defineProps(['searchTab', 'keyWords'])
|
||||
const emit = defineEmits(['showContactCard', 'showGroupCard', 'openSession'])
|
||||
|
||||
const searchData = searchStore()
|
||||
const searchData = useSearchStore()
|
||||
|
||||
const onShowContactCard = (contactInfo) => {
|
||||
emit('showContactCard', contactInfo)
|
||||
|
||||
@@ -3,13 +3,13 @@ import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { userQueryService, userQueryByNickService } from '@/api/user'
|
||||
import { groupSearchGroupInfoService } from '@/api/group'
|
||||
import { searchStore } from '@/stores'
|
||||
import { useSearchStore } from '@/stores'
|
||||
import ResultBox from './ResultBox.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const emit = defineEmits(['showContactCard', 'showGroupCard', 'openSession'])
|
||||
|
||||
const searchData = searchStore()
|
||||
const searchData = useSearchStore()
|
||||
const inputRef = ref()
|
||||
const keyWords = ref('')
|
||||
const isShowSearchDialog = ref(false)
|
||||
@@ -114,7 +114,7 @@ watch(searchTab, () => {
|
||||
v-model="isShowSearchDialog"
|
||||
:show-close="false"
|
||||
:modal="false"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
@open="onOpen"
|
||||
>
|
||||
<template #header>
|
||||
|
||||
@@ -10,3 +10,51 @@ export const proto = {
|
||||
|
||||
// 和服务端约定好的,第一个消息都是从10001开始的
|
||||
export const BEGIN_MSG_ID = 10001
|
||||
|
||||
/**
|
||||
* 消息内容类型
|
||||
* MIX类型为TEXT,EMOJI,SCREENSHOT,AT,QUOTE的组合
|
||||
*/
|
||||
export const msgContentType = {
|
||||
TEXT: 0b0000000000000001, // 文本
|
||||
EMOJI: 0b0000000000000010, // 表情
|
||||
SCREENSHOT: 0b0000000000000100, // 截图
|
||||
AT: 0b0000000000001000, // @
|
||||
QUOTE: 0b0000000000010000, // 引用
|
||||
|
||||
IMAGE: 0b0000001000000000, // 图片
|
||||
RECORDING: 0b0000010000000000, // 语音
|
||||
AUDIO: 0b0000100000000000, // 音频文件
|
||||
VIDEO: 0b0001000000000000, // 视频
|
||||
DOCUMENT: 0b0010000000000000, // 文档
|
||||
FORWARD: 0b0100000000000000 // 合并转发消息
|
||||
}
|
||||
|
||||
// 消息发送状态
|
||||
export const msgSendStatus = {
|
||||
PENDING: 'pending', // 发送中
|
||||
OK: 'ok', // 发送成功
|
||||
FAILED: 'failed', // 发送失败
|
||||
UPLOAD_FAILED: 'uploadFailed' // 文件上传失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息中文件的上传状态
|
||||
* 目前没有实现上传状态以及上传进度的效果
|
||||
*/
|
||||
export const msgFileUploadStatus = {
|
||||
UPLOAD_DEFAULT: 0, // 默认状态,不上传
|
||||
UPLOADING: 1, // 上传中
|
||||
UPLOAD_SUCCESS: 2, // 上传成功
|
||||
UPLOAD_FAILED: 3 // 上传失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息撤回时间限制 10分钟
|
||||
*/
|
||||
export const MSG_REVOKE_TIME_LIMIT = 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* 消息撤回后能重新编辑的时间限制 2分钟
|
||||
*/
|
||||
export const MSG_REEDIT_TIME_LIMIT = 2 * 60 * 1000
|
||||
|
||||
4
src/const/mtsConst.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 缩略图最大大小
|
||||
*/
|
||||
export const THUMB_IMAGE_MAX = 100 * 1024
|
||||
@@ -1,6 +1,6 @@
|
||||
export const CLIENT_TYPE = 2
|
||||
export const CLIENT_NAME = 'web'
|
||||
export const CLIENT_VERSION = '1.0.0'
|
||||
export const CLIENT_VERSION = '1.5.0'
|
||||
|
||||
export const LEAVING_AFTER_DURATION = 5 * 60 * 1000
|
||||
export const LOGOUT_AFTER_DURATION = 8 * 60 * 60 * 1000
|
||||
|
||||
@@ -4,3 +4,6 @@ export * from './receiveStatusRes'
|
||||
export * from './receiveGroupChatMsg'
|
||||
export * from './receiveGroupChatReadMsg'
|
||||
export * from './receiveGroupSystemMsg'
|
||||
export * from './receiveAtMsg'
|
||||
export * from './receiveRevokeMsg'
|
||||
export * from './receiveDeleteMsg'
|
||||
|
||||
18
src/js/event/receiveAtMsg.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useMessageStore } from '@/stores'
|
||||
import { jsonParseSafe } from '../utils/common'
|
||||
|
||||
export const onReceiveAtMsg = () => {
|
||||
return (msg) => {
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const content = jsonParseSafe(msg.body.content)
|
||||
messageData.addAtRecords(sessionId, {
|
||||
msgId: msg.body.msgId,
|
||||
sessionId,
|
||||
fromId: msg.body.fromId,
|
||||
groupId: msg.body.groupId,
|
||||
referMsgId: content.referMsgId,
|
||||
msgTime: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
import { msgChatCreateSessionService } from '@/api/message'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { playMsgReceive } from '../utils/audio'
|
||||
|
||||
export const onReceiveChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
export const onReceiveChatMsg = (updateScroll, capacity) => {
|
||||
return async (msg) => {
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const now = new Date()
|
||||
|
||||
@@ -31,38 +30,41 @@ export const onReceiveChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
}
|
||||
: {}
|
||||
|
||||
// 对方发的消息,把remoteRead更新到最新
|
||||
const remoteReadParams =
|
||||
msg.body.fromId !== msg.body.toId
|
||||
? {
|
||||
remoteRead: msg.body.msgId
|
||||
}
|
||||
: {}
|
||||
|
||||
messageData.updateSession({
|
||||
sessionId: sessionId,
|
||||
unreadCount: messageData.sessionList[sessionId].unreadCount + 1,
|
||||
...readParams
|
||||
...readParams,
|
||||
...remoteReadParams
|
||||
})
|
||||
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: MsgType.CHAT,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
])
|
||||
const showMsg = {
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: MsgType.CHAT,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
await messageData.preloadResource([showMsg])
|
||||
messageData.addMsgRecords(sessionId, [showMsg])
|
||||
messageData.updateMsgKeySort(sessionId)
|
||||
|
||||
if (!messageData.sessionList[sessionId].dnd) {
|
||||
playMsgReceive()
|
||||
}
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
if (msgListDiv && capacity && messageData.selectedSessionId === sessionId) {
|
||||
const scrollHeight = msgListDiv.value?.scrollHeight
|
||||
const clientHeight = document.querySelector('.show-message-box')?.clientHeight
|
||||
capacity.value += 1 //接收一条消息,展示列表的容量就+1
|
||||
nextTick(() => {
|
||||
// 如果滚动条触底,接收到新消息时继续保持触底
|
||||
if (scrollHeight - msgListDiv.value?.scrollTop - clientHeight < 1) {
|
||||
msgListDiv.value.scrollTop = msgListDiv.value?.scrollHeight
|
||||
}
|
||||
})
|
||||
if (messageData.selectedSessionId === sessionId) {
|
||||
updateScroll()
|
||||
capacity.value++ //接收一条消息,展示列表的容量就+1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
export const onReceiveChatReadMsg = () => {
|
||||
return async (msg) => {
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
messageData.updateSession({
|
||||
sessionId: msg.body.sessionId,
|
||||
remoteRead: msg.body.content
|
||||
|
||||
13
src/js/event/receiveDeleteMsg.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
export const onReceiveDeleteMsg = () => {
|
||||
return (msg) => {
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const deleteMsgIds = msg.body.content
|
||||
|
||||
deleteMsgIds.split(',').forEach((item) => {
|
||||
messageData.removeMsgRecord(sessionId, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { msgChatQuerySessionService } from '@/api/message'
|
||||
import { playMsgReceive } from '../utils/audio'
|
||||
|
||||
export const onReceiveGroupChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
export const onReceiveGroupChatMsg = (updateScroll, capacity) => {
|
||||
return async (msg) => {
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const now = new Date()
|
||||
|
||||
@@ -35,32 +34,26 @@ export const onReceiveGroupChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
...readParams
|
||||
})
|
||||
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: MsgType.GROUP_CHAT,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
])
|
||||
const showMsg = {
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: MsgType.GROUP_CHAT,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
await messageData.preloadResource([showMsg])
|
||||
messageData.addMsgRecords(sessionId, [showMsg])
|
||||
messageData.updateMsgKeySort(sessionId)
|
||||
|
||||
if (!messageData.sessionList[sessionId].dnd) {
|
||||
playMsgReceive()
|
||||
}
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
if (msgListDiv && capacity && messageData.selectedSessionId === sessionId) {
|
||||
const scrollHeight = msgListDiv.value?.scrollHeight
|
||||
const clientHeight = document.querySelector('.show-message-box')?.clientHeight
|
||||
capacity.value += 1 //接收一条消息,展示列表的容量就+1
|
||||
nextTick(() => {
|
||||
// 如果滚动条触底,接收到新消息时继续保持触底
|
||||
if (scrollHeight - msgListDiv.value?.scrollTop - clientHeight < 1) {
|
||||
msgListDiv.value.scrollTop = msgListDiv.value?.scrollHeight
|
||||
}
|
||||
})
|
||||
if (messageData.selectedSessionId === sessionId) {
|
||||
updateScroll()
|
||||
capacity.value++ //接收一条消息,展示列表的容量就+1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
export const onReceiveGroupChatReadMsg = () => {
|
||||
return async (msg) => {
|
||||
if (msg.body.fromId === msg.body.toId) {
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
const now = new Date()
|
||||
messageData.updateSession({
|
||||
sessionId: msg.body.sessionId,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { messageStore, groupStore } from '@/stores'
|
||||
import { useMessageStore, useGroupStore } from '@/stores'
|
||||
import { msgChatQuerySessionService } from '@/api/message'
|
||||
import { groupInfoService } from '@/api/group'
|
||||
|
||||
export const onReceiveGroupSystemMsg = (msgListDiv = null, capacity = null) => {
|
||||
return (msg) => {
|
||||
const messageData = messageStore()
|
||||
const groupData = groupStore()
|
||||
export const onReceiveGroupSystemMsg = (updateScroll, capacity) => {
|
||||
return async (msg) => {
|
||||
const messageData = useMessageStore()
|
||||
const groupData = useGroupStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const now = new Date()
|
||||
|
||||
@@ -27,29 +26,22 @@ export const onReceiveGroupSystemMsg = (msgListDiv = null, capacity = null) => {
|
||||
})
|
||||
})
|
||||
|
||||
// 更新聊天记录
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: msg.header.msgType,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
])
|
||||
const showMsg = {
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
fromId: msg.body.fromId,
|
||||
msgType: msg.header.msgType,
|
||||
content: msg.body.content,
|
||||
msgTime: now
|
||||
}
|
||||
await messageData.preloadResource([showMsg])
|
||||
messageData.addMsgRecords(sessionId, [showMsg])
|
||||
messageData.updateMsgKeySort(sessionId)
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
if (msgListDiv && capacity && messageData.selectedSessionId === sessionId) {
|
||||
const scrollHeight = msgListDiv.value?.scrollHeight
|
||||
const clientHeight = document.querySelector('.show-message-box')?.clientHeight
|
||||
capacity.value += 1 //接收一条消息,展示列表的容量就+1
|
||||
nextTick(() => {
|
||||
// 如果滚动条触底,接收到新消息时继续保持触底
|
||||
if (scrollHeight - msgListDiv.value?.scrollTop - clientHeight < 1) {
|
||||
msgListDiv.value.scrollTop = msgListDiv.value?.scrollHeight
|
||||
}
|
||||
})
|
||||
if (messageData.selectedSessionId === sessionId) {
|
||||
updateScroll()
|
||||
capacity.value++ //接收一条消息,展示列表的容量就+1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
src/js/event/receiveRevokeMsg.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
export const onReceiveRevokeMsg = () => {
|
||||
return (msg) => {
|
||||
const messageData = useMessageStore()
|
||||
const sessionId = msg.body.sessionId
|
||||
const revokeMsgId = msg.body.content
|
||||
messageData.revokeMsgRcord(sessionId, revokeMsgId)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { userStore, messageStore, groupStore } from '@/stores'
|
||||
import { useUserStore, useMessageStore, useGroupStore } from '@/stores'
|
||||
import { combineId, jsonParseSafe } from '@/js/utils/common'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
export const onReceiveStatusResMsg = () => {
|
||||
return async (msg) => {
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const groupData = groupStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const groupData = useGroupStore()
|
||||
|
||||
// 1. 更新本账号的多端下的最终状态
|
||||
const content = jsonParseSafe(msg.body.content)
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import msgReceive from '@/assets/audio/msgreceive.mp3'
|
||||
import msgSend from '@/assets/audio/msgsend.mp3'
|
||||
import { userStore } from '@/stores'
|
||||
const userData = userStore()
|
||||
import { useUserStore } from '@/stores'
|
||||
const userData = useUserStore()
|
||||
|
||||
let playMsgReceiveTimer
|
||||
export const playMsgReceive = () => {
|
||||
if (!userData.user.newMsgTips) {
|
||||
return
|
||||
}
|
||||
const audio = new Audio(msgReceive)
|
||||
audio.play().catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
clearTimeout(playMsgReceiveTimer)
|
||||
playMsgReceiveTimer = setTimeout(() => {
|
||||
const audio = new Audio(msgReceive)
|
||||
audio.play().catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
let playMsgSendTimer
|
||||
export const playMsgSend = () => {
|
||||
if (!userData.user.sendMsgTips) {
|
||||
return
|
||||
}
|
||||
const audio = new Audio(msgSend)
|
||||
audio.play().catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
|
||||
clearTimeout(playMsgSendTimer)
|
||||
playMsgSendTimer = setTimeout(() => {
|
||||
const audio = new Audio(msgSend)
|
||||
audio.play().catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
|
||||
export const maskPhoneNum = (str) => {
|
||||
if (str.length < 7) {
|
||||
return '*'
|
||||
@@ -162,6 +164,16 @@ export const showTimeFormatDay = (datatime) => {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
export const showDurationFormat = (duration) => {
|
||||
if (!duration) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration / 60)
|
||||
const seconds = Math.floor(duration % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export const combineId = (fromId, toId) => {
|
||||
if (fromId < toId) {
|
||||
return fromId + '@' + toId
|
||||
@@ -227,3 +239,69 @@ export const base64ToFile = (base64Data, fileName) => {
|
||||
}
|
||||
return new File([u8arr], fileName, { type: mimeType })
|
||||
}
|
||||
|
||||
export const formatFileSize = (size) => {
|
||||
if (!size) {
|
||||
return ''
|
||||
} else if (size < 1024) {
|
||||
return size + ' B'
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + ' KB'
|
||||
} else {
|
||||
return (size / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多功能匹配:忽略大小写,字符匹配,拼音匹配,拼音缩写匹配
|
||||
* @param {*} content 匹配内容
|
||||
* @param {*} key 关键字
|
||||
* @returns
|
||||
*/
|
||||
export const smartMatch = (content, key) => {
|
||||
const lowerKey = key.toLowerCase()
|
||||
const lowerContent = content.toLowerCase()
|
||||
const pinyinFull = getFullPinyin(content)
|
||||
const pinyinInitials = getInitialsPinyin(content)
|
||||
return (
|
||||
lowerContent.includes(lowerKey) ||
|
||||
pinyinFull.includes(lowerKey) ||
|
||||
pinyinInitials.includes(lowerKey)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础匹配:忽略大小写
|
||||
* @param {*} content 匹配内容
|
||||
* @param {*} key 关键字
|
||||
* @returns
|
||||
*/
|
||||
export const baseMatch = (content, key) => {
|
||||
const lowerKey = key.toLowerCase()
|
||||
const lowerContent = content.toLowerCase()
|
||||
return lowerContent.includes(lowerKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 汉字转全拼(小写,无空格)
|
||||
* @param {*} name
|
||||
* @returns
|
||||
*/
|
||||
const getFullPinyin = (name) => {
|
||||
return pinyin(name, { toneType: 'none', type: 'string' }).replaceAll(' ', '').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拼音首字母(小写,无空格)
|
||||
* @param {*} name
|
||||
* @returns
|
||||
*/
|
||||
const getInitialsPinyin = (name) => {
|
||||
return pinyin(name, {
|
||||
pattern: 'first',
|
||||
toneType: 'none',
|
||||
type: 'array'
|
||||
})
|
||||
.join('')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
@@ -97,25 +97,3 @@ export const getEmojiHtml = (emojiId) => {
|
||||
return emojiId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把消息内容中的"[xxx]"替换为img html元素
|
||||
* @param {*} content 消息内容
|
||||
*/
|
||||
export const emojiTrans = (content) => {
|
||||
const pattern = /\[(.*?)\]/g
|
||||
const matches = content.match(pattern)
|
||||
if (!matches || matches.length === 0) {
|
||||
return content
|
||||
}
|
||||
|
||||
new Set(matches).forEach((item) => {
|
||||
const url = emojis[item]
|
||||
if (url) {
|
||||
const emojiHtml = `<img class='emoji' alt='${item}':' title='${item.slice(1, -1)}' src='${url}'>`
|
||||
content = content.replaceAll(item, emojiHtml)
|
||||
}
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
17
src/js/utils/file.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
export const getMd5 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsArrayBuffer(file)
|
||||
reader.onload = (e) => {
|
||||
const arrayBuffer = e.target.result
|
||||
const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer)
|
||||
const md5 = CryptoJS.MD5(wordArray).toString()
|
||||
resolve(md5)
|
||||
}
|
||||
reader.onerror = () => {
|
||||
reject(new Error('读取文件md5值失败'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 流控:在duration时间内只允许task任务被执行countLimit次
|
||||
* @param {*} task 待执行的任务Promise,可以是请求或者其他
|
||||
@@ -11,8 +9,7 @@ export const flowLimiteWrapper = (task, countLimit, duration) => {
|
||||
let count = 0
|
||||
return async () => {
|
||||
if (count >= countLimit) {
|
||||
ElMessage.warning('请求太过频繁,请稍后再试')
|
||||
return Promise.reject(new Error('REQUEST_LIMITED')) // 返回一个拒绝的 Promise
|
||||
return Promise.reject(new Error('请求太过频繁,请稍后再试')) // 返回一个拒绝的 Promise
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
91
src/js/utils/image.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { THUMB_IMAGE_MAX } from '@/const/mtsConst'
|
||||
|
||||
/**
|
||||
* 生成缩略图
|
||||
* @param {*}
|
||||
* @returns 缩略图file对象,原图宽高,缩略图宽高
|
||||
*/
|
||||
export const prehandleImage = async (blob, originalWidth = null, originalHeight = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onload = () => {
|
||||
const img = new Image()
|
||||
img.src = reader.result
|
||||
img.onload = () => {
|
||||
let width = originalWidth !== null ? originalWidth : img.width
|
||||
let height = originalHeight !== null ? originalHeight : img.height
|
||||
let thumbWidth = img.width
|
||||
let thumbHeight = img.height
|
||||
|
||||
if (blob.size <= THUMB_IMAGE_MAX) {
|
||||
resolve({
|
||||
blob,
|
||||
originWidth: width,
|
||||
originHeight: height,
|
||||
thumbWidth,
|
||||
thumbHeight
|
||||
})
|
||||
}
|
||||
|
||||
let accuracy = getAccuracy(blob.size)
|
||||
const scaleRatio = Math.sqrt(accuracy) // 根据 accuracy的平方根 计算缩放比例
|
||||
// 等比率缩放宽高
|
||||
thumbWidth = Math.floor(thumbWidth * scaleRatio)
|
||||
thumbHeight = Math.floor(thumbHeight * scaleRatio)
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvas.width = thumbWidth
|
||||
canvas.height = thumbHeight
|
||||
ctx.drawImage(img, 0, 0, thumbWidth, thumbHeight)
|
||||
|
||||
canvas.toBlob(
|
||||
async (blob) => {
|
||||
if (blob) {
|
||||
if (blob.size <= THUMB_IMAGE_MAX) {
|
||||
const thumbFile = new File([blob], blob.name, {
|
||||
type: blob.type
|
||||
})
|
||||
resolve({
|
||||
thumbFile,
|
||||
originWidth: width,
|
||||
originHeight: height,
|
||||
thumbWidth,
|
||||
thumbHeight
|
||||
})
|
||||
} else {
|
||||
const result = await prehandleImage(blob, width, height)
|
||||
resolve(result)
|
||||
}
|
||||
} else {
|
||||
reject(new Error('生成缩略图遇到了问题'))
|
||||
}
|
||||
},
|
||||
blob.type,
|
||||
accuracy
|
||||
)
|
||||
}
|
||||
img.onerror = () => {
|
||||
reject(new Error('加载图片失败'))
|
||||
}
|
||||
}
|
||||
reader.onerror = () => {
|
||||
reject(new Error('读取图片失败'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 自动调节精度(经验数值)
|
||||
function getAccuracy(size) {
|
||||
let accuracy
|
||||
if (size < 1024 * 1024) {
|
||||
accuracy = 0.6
|
||||
} else if (size < 2047 * 1024) {
|
||||
accuracy = 0.5
|
||||
} else if (size < 3275 * 1024) {
|
||||
accuracy = 0.45
|
||||
} else {
|
||||
accuracy = 0.4
|
||||
}
|
||||
return accuracy
|
||||
}
|
||||
217
src/js/utils/message.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { jsonParseSafe, showDurationFormat } from './common'
|
||||
import { useImageStore, useAudioStore, useVideoStore, useDocumentStore } from '@/stores'
|
||||
import { emojis } from './emojis'
|
||||
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
|
||||
export const showSimplifyMsgContent = (content) => {
|
||||
const arr = jsonParseSafe(content)
|
||||
if (!arr) {
|
||||
return content
|
||||
}
|
||||
|
||||
let simplifyContent = ''
|
||||
|
||||
for (const item of arr) {
|
||||
if (!item.type || !item.value) {
|
||||
return content
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case msgContentType.TEXT:
|
||||
case msgContentType.EMOJI:
|
||||
simplifyContent = simplifyContent + item.value
|
||||
break
|
||||
case msgContentType.AT:
|
||||
simplifyContent = simplifyContent + `@${item.value.nickName} `
|
||||
break
|
||||
case msgContentType.SCREENSHOT:
|
||||
simplifyContent = simplifyContent + `[截图]`
|
||||
break
|
||||
case msgContentType.QUOTE:
|
||||
simplifyContent = simplifyContent + '[引用]'
|
||||
break
|
||||
case msgContentType.RECORDING:
|
||||
simplifyContent =
|
||||
simplifyContent + `[语音] ${showDurationFormat(audioData.audio[item.value]?.duration)}`
|
||||
break
|
||||
case msgContentType.IMAGE:
|
||||
simplifyContent = simplifyContent + `[图片] ${imageData.image[item.value]?.fileName}`
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
simplifyContent = simplifyContent + `[音频] ${audioData.audio[item.value]?.fileName}`
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
simplifyContent = simplifyContent + `[视频] ${videoData.video[item.value]?.fileName}`
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
simplifyContent = simplifyContent + `[文件] ${documentData.document[item.value]?.fileName}`
|
||||
break
|
||||
case msgContentType.FORWARD:
|
||||
simplifyContent = simplifyContent + '[聊天记录]'
|
||||
break
|
||||
|
||||
default:
|
||||
simplifyContent = simplifyContent + item.value
|
||||
break
|
||||
}
|
||||
}
|
||||
return simplifyContent
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容字符串是否匹配消息结构
|
||||
*/
|
||||
export const isMatchMsgStruct = (contentStr) => {
|
||||
const contentArr = jsonParseSafe(contentStr)
|
||||
if (!contentArr || !Array.isArray(contentArr) || contentArr.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const item of contentArr) {
|
||||
const { type, value } = item
|
||||
if (!type || !value) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case msgContentType.TEXT:
|
||||
break
|
||||
case msgContentType.EMOJI:
|
||||
if (!(value in emojis)) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
case msgContentType.SCREENSHOT:
|
||||
case msgContentType.IMAGE:
|
||||
case msgContentType.RECORDING:
|
||||
case msgContentType.AUDIO:
|
||||
case msgContentType.VIDEO:
|
||||
case msgContentType.DOCUMENT:
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
case msgContentType.AT: {
|
||||
const { account, nickName } = value
|
||||
if (!account || !nickName) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
case msgContentType.QUOTE: {
|
||||
const { msgId, nickName } = value
|
||||
if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
case msgContentType.FORWARD: {
|
||||
const { sessionId, data } = value
|
||||
if (!sessionId || !data) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of data) {
|
||||
const { msgId, nickName } = item
|
||||
if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为 MIX 类型
|
||||
* @param {*} type
|
||||
* @returns
|
||||
*/
|
||||
export const isMixType = (type) => {
|
||||
const MIX_CANDIDATES =
|
||||
msgContentType.TEXT |
|
||||
msgContentType.EMOJI |
|
||||
msgContentType.SCREENSHOT |
|
||||
msgContentType.AT |
|
||||
msgContentType.QUOTE
|
||||
|
||||
return type <= MIX_CANDIDATES
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有包含图片的type集合
|
||||
* @returns
|
||||
*/
|
||||
export const imageTypes = () => {
|
||||
return [
|
||||
msgContentType.IMAGE,
|
||||
msgContentType.SCREENSHOT,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.QUOTE,
|
||||
|
||||
msgContentType.SCREENSHOT |
|
||||
msgContentType.TEXT |
|
||||
msgContentType.EMOJI |
|
||||
msgContentType.AT |
|
||||
msgContentType.QUOTE
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有包含Quote的type集合
|
||||
* @returns
|
||||
*/
|
||||
export const quoteTypes = () => {
|
||||
return [
|
||||
msgContentType.QUOTE,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.TEXT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI,
|
||||
msgContentType.QUOTE | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.SCREENSHOT,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.AT,
|
||||
|
||||
msgContentType.QUOTE |
|
||||
msgContentType.TEXT |
|
||||
msgContentType.EMOJI |
|
||||
msgContentType.AT |
|
||||
msgContentType.SCREENSHOT
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import { generateSign } from './crypto'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@@ -23,7 +23,7 @@ const instance = axios.create({
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
async (config) => {
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const traceId = uuidv4()
|
||||
if (config.url === '/user/refreshToken') {
|
||||
const token = userData.getRefreshToken()
|
||||
@@ -56,18 +56,27 @@ instance.interceptors.response.use(
|
||||
if (res.data.code === 0) {
|
||||
return res
|
||||
}
|
||||
|
||||
ElMessage.error(res.data.desc || '服务异常')
|
||||
return Promise.reject(res.data)
|
||||
console.error(
|
||||
'The response was not the expected code: ',
|
||||
res.config?.url,
|
||||
res.data?.code,
|
||||
res.data?.desc
|
||||
)
|
||||
return Promise.reject(res)
|
||||
},
|
||||
async (err) => {
|
||||
if (err.response?.status === 401) {
|
||||
userStore().clearAt()
|
||||
userStore().clearRt()
|
||||
ElMessage.error('您还未登录,请先登录')
|
||||
useUserStore().clearAt()
|
||||
useUserStore().clearRt()
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
router.push('/login')
|
||||
} else {
|
||||
ElMessage.error(err.response?.message || '服务异常')
|
||||
console.error(
|
||||
'The request was failed: ',
|
||||
err.config?.url,
|
||||
err.response?.status,
|
||||
err.response?.message
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
|
||||
21
src/js/utils/video.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export const prehandleVideo = async (blob) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const video = document.createElement('video')
|
||||
video.preload = 'metadata'
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
resolve({
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
})
|
||||
}
|
||||
|
||||
video.onerror = () => {
|
||||
reject(new Error('视频文件元数据加载失败'))
|
||||
}
|
||||
|
||||
video.src = url
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Msg, Header, MsgType, Body } from '@/proto/msg'
|
||||
import { proto } from '@/const/msgConst'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const chatConstructor = (sessionId, toId, content, seq) => {
|
||||
export const chatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
|
||||
const header = Header.create({
|
||||
magic: proto.magic,
|
||||
version: proto.version,
|
||||
@@ -11,14 +11,15 @@ export const chatConstructor = (sessionId, toId, content, seq) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
toId: toId,
|
||||
toId: remoteId,
|
||||
sessionId: sessionId,
|
||||
content: content,
|
||||
seq: seq
|
||||
contentType: contentType,
|
||||
seq: sequence
|
||||
})
|
||||
const chatMsg = Msg.create({ header: header, body: body })
|
||||
const payload = Msg.encode(chatMsg).finish()
|
||||
@@ -27,7 +28,7 @@ export const chatConstructor = (sessionId, toId, content, seq) => {
|
||||
return data
|
||||
}
|
||||
|
||||
export const groupChatConstructor = (sessionId, groupId, content, seq) => {
|
||||
export const groupChatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
|
||||
const header = Header.create({
|
||||
magic: proto.magic,
|
||||
version: proto.version,
|
||||
@@ -35,14 +36,15 @@ export const groupChatConstructor = (sessionId, groupId, content, seq) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
sessionId: sessionId,
|
||||
groupId: groupId,
|
||||
groupId: remoteId,
|
||||
content: content,
|
||||
seq: seq
|
||||
contentType: contentType,
|
||||
seq: sequence
|
||||
})
|
||||
const msg = Msg.create({ header: header, body: body })
|
||||
const payload = Msg.encode(msg).finish()
|
||||
@@ -79,7 +81,7 @@ export const helloConstructor = () => {
|
||||
return data
|
||||
}
|
||||
|
||||
export const chatReadConstructor = (sessionId, toId, content) => {
|
||||
export const chatReadConstructor = ({ sessionId, remoteId, content }) => {
|
||||
const header = Header.create({
|
||||
magic: proto.magic,
|
||||
version: proto.version,
|
||||
@@ -87,11 +89,11 @@ export const chatReadConstructor = (sessionId, toId, content) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
toId: toId,
|
||||
toId: remoteId,
|
||||
sessionId: sessionId,
|
||||
content: content,
|
||||
seq: uuidv4()
|
||||
@@ -103,7 +105,7 @@ export const chatReadConstructor = (sessionId, toId, content) => {
|
||||
return data
|
||||
}
|
||||
|
||||
export const groupChatReadConstructor = (sessionId, groupId, content) => {
|
||||
export const groupChatReadConstructor = ({ sessionId, remoteId, content }) => {
|
||||
const header = Header.create({
|
||||
magic: proto.magic,
|
||||
version: proto.version,
|
||||
@@ -111,11 +113,11 @@ export const groupChatReadConstructor = (sessionId, groupId, content) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
toId: groupId,
|
||||
toId: remoteId,
|
||||
sessionId: sessionId,
|
||||
content: content,
|
||||
seq: uuidv4()
|
||||
@@ -135,7 +137,7 @@ export const statusReqConstructor = (accounts) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
@@ -156,7 +158,7 @@ export const statusSyncConstructor = (status) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
@@ -169,6 +171,30 @@ export const statusSyncConstructor = (status) => {
|
||||
return data
|
||||
}
|
||||
|
||||
export const atConstructor = ({ sessionId, remoteId, content, sequence }) => {
|
||||
const header = Header.create({
|
||||
magic: proto.magic,
|
||||
version: proto.version,
|
||||
msgType: MsgType.AT,
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
sessionId: sessionId,
|
||||
groupId: remoteId,
|
||||
content: content,
|
||||
seq: sequence
|
||||
})
|
||||
const chatMsg = Msg.create({ header: header, body: body })
|
||||
const payload = Msg.encode(chatMsg).finish()
|
||||
const data = encodePayload(payload)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送前对长度编码,配合服务端解决半包黏包问题
|
||||
* @param {*} payload
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Msg, MsgType } from '@/proto/msg'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { generateSign } from '../utils/crypto'
|
||||
import {
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
statusReqConstructor,
|
||||
statusSyncConstructor,
|
||||
groupChatConstructor,
|
||||
groupChatReadConstructor
|
||||
groupChatReadConstructor,
|
||||
atConstructor
|
||||
} from './constructor'
|
||||
import {
|
||||
onReceiveStatusResMsg,
|
||||
@@ -18,7 +19,10 @@ import {
|
||||
onReceiveChatReadMsg,
|
||||
onReceiveGroupChatMsg,
|
||||
onReceiveGroupChatReadMsg,
|
||||
onReceiveGroupSystemMsg
|
||||
onReceiveGroupSystemMsg,
|
||||
onReceiveAtMsg,
|
||||
onReceiveRevokeMsg,
|
||||
onReceiveDeleteMsg
|
||||
} from '@/js/event'
|
||||
|
||||
class WsConnect {
|
||||
@@ -105,7 +109,10 @@ class WsConnect {
|
||||
},
|
||||
[MsgType.DELIVERED]: (deliveredMsg) => {
|
||||
this.msgIdRefillCallback[deliveredMsg.body.seq](deliveredMsg.body.msgId)
|
||||
delete this.msgIdRefillCallback[deliveredMsg.body.seq]
|
||||
setTimeout(() => {
|
||||
// 不能立即删除,因为有可能重发消息还会用到
|
||||
delete this.msgIdRefillCallback[deliveredMsg.body.seq]
|
||||
}, 30000)
|
||||
},
|
||||
[MsgType.HEART_BEAT]: () => {
|
||||
if (this.heartBeat.healthPoint > 0) this.heartBeat.healthPoint--
|
||||
@@ -115,6 +122,9 @@ class WsConnect {
|
||||
[MsgType.CHAT_READ]: onReceiveChatReadMsg(),
|
||||
[MsgType.GROUP_CHAT]: onReceiveGroupChatMsg(),
|
||||
[MsgType.GROUP_CHAT_READ]: onReceiveGroupChatReadMsg(),
|
||||
[MsgType.AT]: onReceiveAtMsg(),
|
||||
[MsgType.REVOKE]: onReceiveRevokeMsg(),
|
||||
[MsgType.DELETE]: onReceiveDeleteMsg(),
|
||||
[MsgType.SYS_GROUP_CREATE]: onReceiveGroupSystemMsg(),
|
||||
[MsgType.SYS_GROUP_ADD_MEMBER]: onReceiveGroupSystemMsg(),
|
||||
[MsgType.SYS_GROUP_DEL_MEMBER]: onReceiveGroupSystemMsg(),
|
||||
@@ -146,7 +156,8 @@ class WsConnect {
|
||||
[MsgType.GROUP_CHAT]: groupChatConstructor,
|
||||
[MsgType.GROUP_CHAT_READ]: groupChatReadConstructor,
|
||||
[MsgType.STATUS_REQ]: statusReqConstructor,
|
||||
[MsgType.STATUS_SYNC]: statusSyncConstructor
|
||||
[MsgType.STATUS_SYNC]: statusSyncConstructor,
|
||||
[MsgType.AT]: atConstructor
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +184,7 @@ class WsConnect {
|
||||
return
|
||||
}
|
||||
// console.log('create websocket')
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const token = await userData.getAccessToken()
|
||||
const traceId = uuidv4()
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000)
|
||||
@@ -329,14 +340,20 @@ class WsConnect {
|
||||
* @param {*} msgType
|
||||
* @param {*} content
|
||||
* @param {*} seq
|
||||
* @param {*} callbackBefore 发送前的处理,用于展示发送前状态
|
||||
* @param {*} callbackAfter 发送后(接收MsgType.DELIVERED时)的处理,用于展示发送后状态
|
||||
* @param {*} before 发送前的处理,用于展示发送前状态
|
||||
* @param {*} after 发送后(接收MsgType.DELIVERED时)的处理,用于展示发送后状态
|
||||
*/
|
||||
sendMsg(sessionId, remoteId, msgType, content, seq, callbackBefore, callbackAfter) {
|
||||
sendMsg(sessionId, remoteId, msgType, content, contentType, seq, before, after) {
|
||||
const sequence = seq || uuidv4()
|
||||
const data = this.dataConstructor[msgType](sessionId, remoteId, content, sequence)
|
||||
callbackBefore(sequence, data)
|
||||
this.msgIdRefillCallback[sequence] = callbackAfter
|
||||
const data = this.dataConstructor[msgType]({
|
||||
sessionId,
|
||||
remoteId,
|
||||
content,
|
||||
contentType,
|
||||
sequence
|
||||
})
|
||||
before(data)
|
||||
this.msgIdRefillCallback[sequence] = after
|
||||
this.sendAgent(data)
|
||||
}
|
||||
|
||||
|
||||
91
src/models/message.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { msgSendStatus } from '@/const/msgConst'
|
||||
|
||||
/**
|
||||
* 消息渲染及缓存时用到实体类,收录所有可能用到的属性,目前作为参考用,并未实际调用
|
||||
*/
|
||||
class Message {
|
||||
/**
|
||||
* 会话内唯一消息Id
|
||||
*/
|
||||
msgId
|
||||
|
||||
/**
|
||||
* 消息序列号
|
||||
*/
|
||||
seq
|
||||
|
||||
/**
|
||||
* 消息所属的会话ID
|
||||
*/
|
||||
sessionId
|
||||
|
||||
/**
|
||||
* 消息发送ID
|
||||
*/
|
||||
fromId
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
msgType
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
content
|
||||
|
||||
/**
|
||||
* 消息状态:发送中,发送成功,发送失败
|
||||
*/
|
||||
status
|
||||
|
||||
/**
|
||||
* 接收消息的时间
|
||||
*/
|
||||
msgTime
|
||||
|
||||
/**
|
||||
* 消息发送的时间,发送消息时才需要填
|
||||
*/
|
||||
sendTime
|
||||
|
||||
/**
|
||||
* 消息中文件的上传状态
|
||||
*/
|
||||
uploadStatus
|
||||
|
||||
/**
|
||||
* 消息中文件的上传进度
|
||||
*/
|
||||
uploadProgress
|
||||
|
||||
constructor(
|
||||
sessionId,
|
||||
fromId,
|
||||
msgType,
|
||||
content,
|
||||
contentType,
|
||||
msgTime,
|
||||
sendTime = undefined,
|
||||
msgId = undefined,
|
||||
seq = undefined,
|
||||
status = msgSendStatus.PENDING,
|
||||
uploadStatus = undefined,
|
||||
uploadProgress = undefined
|
||||
) {
|
||||
this.msgId = msgId
|
||||
this.seq = seq
|
||||
this.sessionId = sessionId
|
||||
this.fromId = fromId
|
||||
this.msgType = msgType
|
||||
this.content = content
|
||||
this.contentType = contentType
|
||||
this.status = status
|
||||
this.msgTime = msgTime
|
||||
this.sendTime = sendTime
|
||||
this.uploadStatus = uploadStatus
|
||||
this.uploadProgress = uploadProgress
|
||||
}
|
||||
}
|
||||
|
||||
export { Message }
|
||||
@@ -293,6 +293,9 @@ export const Msg = ($root.Msg = (() => {
|
||||
* @property {number} STATUS_REQ=8 STATUS_REQ value
|
||||
* @property {number} STATUS_RES=9 STATUS_RES value
|
||||
* @property {number} STATUS_SYNC=10 STATUS_SYNC value
|
||||
* @property {number} AT=11 AT value
|
||||
* @property {number} REVOKE=12 REVOKE value
|
||||
* @property {number} DELETE=13 DELETE value
|
||||
* @property {number} SYS_GROUP_CREATE=21 SYS_GROUP_CREATE value
|
||||
* @property {number} SYS_GROUP_ADD_MEMBER=22 SYS_GROUP_ADD_MEMBER value
|
||||
* @property {number} SYS_GROUP_DEL_MEMBER=23 SYS_GROUP_DEL_MEMBER value
|
||||
@@ -329,6 +332,9 @@ export const MsgType = ($root.MsgType = (() => {
|
||||
values[(valuesById[8] = 'STATUS_REQ')] = 8
|
||||
values[(valuesById[9] = 'STATUS_RES')] = 9
|
||||
values[(valuesById[10] = 'STATUS_SYNC')] = 10
|
||||
values[(valuesById[11] = 'AT')] = 11
|
||||
values[(valuesById[12] = 'REVOKE')] = 12
|
||||
values[(valuesById[13] = 'DELETE')] = 13
|
||||
values[(valuesById[21] = 'SYS_GROUP_CREATE')] = 21
|
||||
values[(valuesById[22] = 'SYS_GROUP_ADD_MEMBER')] = 22
|
||||
values[(valuesById[23] = 'SYS_GROUP_DEL_MEMBER')] = 23
|
||||
@@ -543,6 +549,9 @@ export const Header = ($root.Header = (() => {
|
||||
case 8:
|
||||
case 9:
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 13:
|
||||
case 21:
|
||||
case 22:
|
||||
case 23:
|
||||
@@ -635,6 +644,18 @@ export const Header = ($root.Header = (() => {
|
||||
case 10:
|
||||
message.msgType = 10
|
||||
break
|
||||
case 'AT':
|
||||
case 11:
|
||||
message.msgType = 11
|
||||
break
|
||||
case 'REVOKE':
|
||||
case 12:
|
||||
message.msgType = 12
|
||||
break
|
||||
case 'DELETE':
|
||||
case 13:
|
||||
message.msgType = 13
|
||||
break
|
||||
case 'SYS_GROUP_CREATE':
|
||||
case 21:
|
||||
message.msgType = 21
|
||||
@@ -798,6 +819,7 @@ export const Body = ($root.Body = (() => {
|
||||
* @property {string|null} [groupId] Body groupId
|
||||
* @property {number|Long|null} [msgId] Body msgId
|
||||
* @property {string|null} [content] Body content
|
||||
* @property {number|null} [contentType] Body contentType
|
||||
* @property {string|null} [seq] Body seq
|
||||
* @property {string|null} [sessionId] Body sessionId
|
||||
*/
|
||||
@@ -815,21 +837,23 @@ export const Body = ($root.Body = (() => {
|
||||
* | 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
||||
* | 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
||||
* | 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
||||
* | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
* | 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||
* | 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
|
||||
* | 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
* |10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||
* +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
||||
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX
|
||||
* +---+--------------+------------+------------+-------------+------------+
|
||||
* | 1 | fromId | M | M | M | - |
|
||||
* | 2 | fromClient | M | M | M | - |
|
||||
* | 3 | toId | - | - | - | - |
|
||||
* | 4 | toClient | - | - | - | - |
|
||||
* | 5 | groupId | - | - | - | M |
|
||||
* | 6 | msgId | - | - | - | M |
|
||||
* | 7 | content | M | M | M | M |
|
||||
* | 8 | seq | - | - | - | - |
|
||||
* | 9 | sessionId | - | - | - | M |
|
||||
* +---+--------------+------------+------------+-------------+------------+
|
||||
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||
* | 1 | fromId | M | M | M | - | M | M | M | M |
|
||||
* | 2 | fromClient | M | M | M | - | M | M | - | - |
|
||||
* | 3 | toId | - | - | - | - | - | M | o | M |
|
||||
* | 4 | toClient | - | - | - | - | - | M | - | M |
|
||||
* | 5 | groupId | - | - | - | M | M | M | o | - |
|
||||
* | 6 | msgId | - | - | - | M | - | M | M | M |
|
||||
* | 7 | content | M | M | M | M | M | M | M | - |
|
||||
* | 8 | contentType | - | - | - | - | - | - | - | - |
|
||||
* | 9 | seq | - | - | - | - | M | M | - | - |
|
||||
* |10 | sessionId | - | - | - | M | M | M | M | M |
|
||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||
* @implements IBody
|
||||
* @constructor
|
||||
* @param {IBody=} [properties] Properties to set
|
||||
@@ -896,6 +920,14 @@ export const Body = ($root.Body = (() => {
|
||||
*/
|
||||
Body.prototype.content = null
|
||||
|
||||
/**
|
||||
* Body contentType.
|
||||
* @member {number|null|undefined} contentType
|
||||
* @memberof Body
|
||||
* @instance
|
||||
*/
|
||||
Body.prototype.contentType = null
|
||||
|
||||
/**
|
||||
* Body seq.
|
||||
* @member {string|null|undefined} seq
|
||||
@@ -957,6 +989,12 @@ export const Body = ($root.Body = (() => {
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
})
|
||||
|
||||
// Virtual OneOf for proto3 optional field
|
||||
Object.defineProperty(Body.prototype, '_contentType', {
|
||||
get: $util.oneOfGetter(($oneOfFields = ['contentType'])),
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
})
|
||||
|
||||
// Virtual OneOf for proto3 optional field
|
||||
Object.defineProperty(Body.prototype, '_seq', {
|
||||
get: $util.oneOfGetter(($oneOfFields = ['seq'])),
|
||||
@@ -1006,10 +1044,12 @@ export const Body = ($root.Body = (() => {
|
||||
writer.uint32(/* id 6, wireType 0 =*/ 48).int64(message.msgId)
|
||||
if (message.content != null && Object.hasOwnProperty.call(message, 'content'))
|
||||
writer.uint32(/* id 7, wireType 2 =*/ 58).string(message.content)
|
||||
if (message.contentType != null && Object.hasOwnProperty.call(message, 'contentType'))
|
||||
writer.uint32(/* id 8, wireType 0 =*/ 64).int32(message.contentType)
|
||||
if (message.seq != null && Object.hasOwnProperty.call(message, 'seq'))
|
||||
writer.uint32(/* id 8, wireType 2 =*/ 66).string(message.seq)
|
||||
writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.seq)
|
||||
if (message.sessionId != null && Object.hasOwnProperty.call(message, 'sessionId'))
|
||||
writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.sessionId)
|
||||
writer.uint32(/* id 10, wireType 2 =*/ 82).string(message.sessionId)
|
||||
return writer
|
||||
}
|
||||
|
||||
@@ -1073,10 +1113,14 @@ export const Body = ($root.Body = (() => {
|
||||
break
|
||||
}
|
||||
case 8: {
|
||||
message.seq = reader.string()
|
||||
message.contentType = reader.int32()
|
||||
break
|
||||
}
|
||||
case 9: {
|
||||
message.seq = reader.string()
|
||||
break
|
||||
}
|
||||
case 10: {
|
||||
message.sessionId = reader.string()
|
||||
break
|
||||
}
|
||||
@@ -1150,6 +1194,10 @@ export const Body = ($root.Body = (() => {
|
||||
properties._content = 1
|
||||
if (!$util.isString(message.content)) return 'content: string expected'
|
||||
}
|
||||
if (message.contentType != null && message.hasOwnProperty('contentType')) {
|
||||
properties._contentType = 1
|
||||
if (!$util.isInteger(message.contentType)) return 'contentType: integer expected'
|
||||
}
|
||||
if (message.seq != null && message.hasOwnProperty('seq')) {
|
||||
properties._seq = 1
|
||||
if (!$util.isString(message.seq)) return 'seq: string expected'
|
||||
@@ -1187,6 +1235,7 @@ export const Body = ($root.Body = (() => {
|
||||
object.msgId.high >>> 0
|
||||
).toNumber()
|
||||
if (object.content != null) message.content = String(object.content)
|
||||
if (object.contentType != null) message.contentType = object.contentType | 0
|
||||
if (object.seq != null) message.seq = String(object.seq)
|
||||
if (object.sessionId != null) message.sessionId = String(object.sessionId)
|
||||
return message
|
||||
@@ -1240,6 +1289,10 @@ export const Body = ($root.Body = (() => {
|
||||
object.content = message.content
|
||||
if (options.oneofs) object._content = 'content'
|
||||
}
|
||||
if (message.contentType != null && message.hasOwnProperty('contentType')) {
|
||||
object.contentType = message.contentType
|
||||
if (options.oneofs) object._contentType = 'contentType'
|
||||
}
|
||||
if (message.seq != null && message.hasOwnProperty('seq')) {
|
||||
object.seq = message.seq
|
||||
if (options.oneofs) object._seq = 'seq'
|
||||
|
||||
@@ -18,6 +18,9 @@ enum MsgType {
|
||||
STATUS_REQ = 8; //连接状态查询请求
|
||||
STATUS_RES = 9; //连接状态响应
|
||||
STATUS_SYNC = 10; //端侧的连接状态同步给云端(比如在线,离开)
|
||||
AT = 11; //@消息
|
||||
REVOKE = 12; //撤回消息
|
||||
DELETE = 13; //删除消息
|
||||
|
||||
SYS_GROUP_CREATE = 21; //系统消息之创建群组
|
||||
SYS_GROUP_ADD_MEMBER = 22; //系统消息之添加群组成员
|
||||
@@ -62,21 +65,23 @@ message Header {
|
||||
| 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
||||
| 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
||||
| 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
||||
| 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
| 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||
| 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
|
||||
| 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||
|10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||
+---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
||||
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX
|
||||
+---+--------------+------------+------------+-------------+------------+
|
||||
| 1 | fromId | M | M | M | - |
|
||||
| 2 | fromClient | M | M | M | - |
|
||||
| 3 | toId | - | - | - | - |
|
||||
| 4 | toClient | - | - | - | - |
|
||||
| 5 | groupId | - | - | - | M |
|
||||
| 6 | msgId | - | - | - | M |
|
||||
| 7 | content | M | M | M | M |
|
||||
| 8 | seq | - | - | - | - |
|
||||
| 9 | sessionId | - | - | - | M |
|
||||
+---+--------------+------------+------------+-------------+------------+
|
||||
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||
| 1 | fromId | M | M | M | - | M | M | M | M |
|
||||
| 2 | fromClient | M | M | M | - | M | M | - | - |
|
||||
| 3 | toId | - | - | - | - | - | M | o | M |
|
||||
| 4 | toClient | - | - | - | - | - | M | - | M |
|
||||
| 5 | groupId | - | - | - | M | M | M | o | - |
|
||||
| 6 | msgId | - | - | - | M | - | M | M | M |
|
||||
| 7 | content | M | M | M | M | M | M | M | - |
|
||||
| 8 | contentType | - | - | - | - | - | - | - | - |
|
||||
| 9 | seq | - | - | - | - | M | M | - | - |
|
||||
|10 | sessionId | - | - | - | M | M | M | M | M |
|
||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||
*/
|
||||
message Body {
|
||||
optional string fromId = 1;
|
||||
@@ -86,8 +91,9 @@ message Body {
|
||||
optional string groupId = 5;
|
||||
optional int64 msgId = 6; //服务端生成的消息ID,会话内单调递增,可用于消息排序
|
||||
optional string content = 7;
|
||||
optional string seq = 8; //客户端生成的序列号ID,会话内唯一,可用于消息去重
|
||||
optional string sessionId = 9; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
|
||||
optional int32 contentType = 8;
|
||||
optional string seq = 9; //客户端生成的序列号ID,会话内唯一,可用于消息去重
|
||||
optional string sessionId = 10; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
|
||||
}
|
||||
|
||||
message Extension {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -167,7 +167,7 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const isLogin = await userData.isLogin()
|
||||
|
||||
if (!isLogin) {
|
||||
|
||||
72
src/stores/audio.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { mtsAudioService } from '@/api/mts'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { jsonParseSafe } from '@/js/utils/common'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// audio的缓存数据,不持久化存储
|
||||
export const useAudioStore = defineStore('anylink-audio', () => {
|
||||
/**
|
||||
* {
|
||||
* objectId_01: {objectId: objectId_01, url: xxx},
|
||||
* objectId_02: {objectId: objectId_02, url: xxx},
|
||||
* }
|
||||
*/
|
||||
const audio = ref({})
|
||||
|
||||
const setAudio = (obj) => {
|
||||
audio.value[obj.objectId] = obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地对象ID到服务器对象ID的映射
|
||||
* 在某些场景下需要通过本地对象ID找到服务器对象ID,例如复制刚刚发送的媒体消息
|
||||
*
|
||||
*/
|
||||
const localServerMap = ref({})
|
||||
|
||||
const setLocalServerMap = (localObjectId, serverObjectId) => {
|
||||
localServerMap.value[localObjectId] = serverObjectId
|
||||
}
|
||||
|
||||
const preloadAudioFromMsgList = async (msgRecords) => {
|
||||
const audioIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const aar = jsonParseSafe(item.content)
|
||||
aar.forEach((item) => {
|
||||
if (item.type === msgContentType.AUDIO || item.type === msgContentType.RECORDING) {
|
||||
const objectId = item.value
|
||||
if (!audio.value[objectId]) {
|
||||
audioIds.add(objectId)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (audioIds.size > 0) {
|
||||
const res = await mtsAudioService({ objectIds: [...audioIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setAudio(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
Object.values(audio.value).forEach((item) => {
|
||||
if (item.downloadUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(item.downloadUrl)
|
||||
}
|
||||
})
|
||||
|
||||
audio.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
audio,
|
||||
setAudio,
|
||||
localServerMap,
|
||||
setLocalServerMap,
|
||||
preloadAudioFromMsgList,
|
||||
clear
|
||||
}
|
||||
})
|
||||
72
src/stores/document.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { mtsDocumentService } from '@/api/mts'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { jsonParseSafe } from '@/js/utils/common'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// document的缓存数据,不持久化存储
|
||||
export const useDocumentStore = defineStore('anylink-document', () => {
|
||||
/**
|
||||
* {
|
||||
* objectId_01: {objectId: objectId_01, url: xxx},
|
||||
* objectId_02: {objectId: objectId_02, url: xxx},
|
||||
* }
|
||||
*/
|
||||
const document = ref({})
|
||||
|
||||
const setDocument = (obj) => {
|
||||
document.value[obj.objectId] = obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地对象ID到服务器对象ID的映射
|
||||
* 在某些场景下需要通过本地对象ID找到服务器对象ID,例如复制刚刚发送的媒体消息
|
||||
*
|
||||
*/
|
||||
const localServerMap = ref({})
|
||||
|
||||
const setLocalServerMap = (localObjectId, serverObjectId) => {
|
||||
localServerMap.value[localObjectId] = serverObjectId
|
||||
}
|
||||
|
||||
const preloadDocumentFromMsgList = async (msgRecords) => {
|
||||
const documentIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const aar = jsonParseSafe(item.content)
|
||||
aar.forEach((item) => {
|
||||
if (item.type === msgContentType.DOCUMENT) {
|
||||
const objectId = item.value
|
||||
if (!document.value[objectId]) {
|
||||
documentIds.add(objectId)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (documentIds.size > 0) {
|
||||
const res = await mtsDocumentService({ objectIds: [...documentIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setDocument(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
Object.values(document.value).forEach((item) => {
|
||||
if (item.downloadUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(item.downloadUrl)
|
||||
}
|
||||
})
|
||||
|
||||
document.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
setDocument,
|
||||
localServerMap,
|
||||
setLocalServerMap,
|
||||
preloadDocumentFromMsgList,
|
||||
clear
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import { groupInfoListService } from '@/api/group'
|
||||
|
||||
// group群组相关的缓存数据,不持久化存储
|
||||
export const groupStore = defineStore('anylink-group', () => {
|
||||
export const useGroupStore = defineStore('anylink-group', () => {
|
||||
/**
|
||||
* 和我有关的所有群组,格式:{groupId_1: groupInfo_1, groupId_2: groupInfo_2}
|
||||
*/
|
||||
@@ -16,7 +16,7 @@ export const groupStore = defineStore('anylink-group', () => {
|
||||
const groupMembersList = ref({})
|
||||
|
||||
/**
|
||||
* 获取有效的成员数组,意思是刨除inStatu不等于0的
|
||||
* 获取有效的成员数组,意思是刨除inStatus不等于0的
|
||||
* @param {*} groupId
|
||||
*/
|
||||
const getValidGroupMembers = (groupId) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import {} from '@/api/group'
|
||||
|
||||
// groupCard群组详情卡片相关的缓存数据,不持久化存储
|
||||
export const groupCardStore = defineStore('anylink-groupCard', () => {
|
||||
export const useGroupCardStore = defineStore('anylink-groupCard', () => {
|
||||
const isShow = ref(false)
|
||||
|
||||
const groupId = ref('')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { mtsImageService } from '@/api/mts'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { jsonParseSafe } from '@/js/utils/common'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const pattern = /\{[a-f0-9]+\}/g
|
||||
|
||||
// image的缓存数据,不持久化存储
|
||||
export const imageStore = defineStore('anylink-image', () => {
|
||||
export const useImageStore = defineStore('anylink-image', () => {
|
||||
/**
|
||||
* {
|
||||
* objectId_01: {objectId: objectId_01, originUrl: xxx, thumbUrl: xxx},
|
||||
@@ -15,67 +15,111 @@ export const imageStore = defineStore('anylink-image', () => {
|
||||
const image = ref({})
|
||||
|
||||
/**
|
||||
* 在同一个session中的image(id)集合
|
||||
* 在同一个session中的需要渲染的image对象
|
||||
*
|
||||
* {
|
||||
* sessionId_01: {objectId_x: {objectId: objectId_x, originUrl: xxx, thumbUrl: xxx}...},
|
||||
* sessionId_02: {objectId_x: {objectId: objectId_x, originUrl: xxx, thumbUrl: xxx}...},
|
||||
* }
|
||||
*/
|
||||
const imageInSession = ref({})
|
||||
|
||||
const setImage = (sessionId, obj) => {
|
||||
const setImage = (obj) => {
|
||||
image.value[obj.objectId] = obj
|
||||
}
|
||||
|
||||
const setImageInSession = (sessionId, obj) => {
|
||||
if (!imageInSession.value[sessionId]) {
|
||||
imageInSession.value[sessionId] = []
|
||||
imageInSession.value[sessionId] = {}
|
||||
}
|
||||
imageInSession.value[sessionId].push(obj.objectId)
|
||||
imageInSession.value[sessionId][obj.objectId] = obj
|
||||
}
|
||||
|
||||
const imageTrans = (content, maxWidth = 400, maxHeight = 300) => {
|
||||
const matches = content.match(pattern)
|
||||
if (!matches || matches.length === 0) {
|
||||
return content
|
||||
const clearImageInSession = (sessionId) => {
|
||||
if (imageInSession.value[sessionId]) {
|
||||
imageInSession.value[sessionId] = {}
|
||||
}
|
||||
|
||||
new Set(matches).forEach((item) => {
|
||||
let startIndex = item.indexOf('{')
|
||||
let endIndex = item.indexOf('}')
|
||||
const objectId = item.slice(startIndex + 1, endIndex)
|
||||
const thumbUrl = image.value[objectId]?.thumbUrl
|
||||
const originUrl = image.value[objectId]?.originUrl
|
||||
if (thumbUrl) {
|
||||
const imageHtml =
|
||||
`<img class="image" alt="{${objectId}}" src="${thumbUrl}" data-origin-url="${originUrl}" ` +
|
||||
`style="max-width: ${maxWidth}px; max-height: ${maxHeight}px; width: auto; height: auto;cursor: pointer;">`
|
||||
content = content.replaceAll(item, imageHtml)
|
||||
}
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const loadImageInfoFromContent = async (sessionId, content) => {
|
||||
/**
|
||||
* 本地对象ID到服务器对象ID的映射
|
||||
* 在某些场景下需要通过本地对象ID找到服务器对象ID,例如复制刚刚发送的媒体消息
|
||||
*
|
||||
*/
|
||||
const localServerMap = ref({})
|
||||
|
||||
const setLocalServerMap = (localObjectId, serverObjectId) => {
|
||||
localServerMap.value[localObjectId] = serverObjectId
|
||||
}
|
||||
|
||||
const preloadImageFromMsg = async (content) => {
|
||||
if (!content) return
|
||||
|
||||
const imageIds = new Set()
|
||||
const matches = content.match(pattern)
|
||||
if (matches && matches.length > 0) {
|
||||
matches.forEach((item) => {
|
||||
let startIndex = item.indexOf('{')
|
||||
let endIndex = item.indexOf('}')
|
||||
const objectId = item.slice(startIndex + 1, endIndex)
|
||||
const aar = jsonParseSafe(content)
|
||||
aar.forEach((item) => {
|
||||
if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
|
||||
const objectId = item.value
|
||||
if (!image.value[objectId]) {
|
||||
imageIds.add(objectId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (imageIds.size > 0) {
|
||||
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setImage(sessionId, item) // 缓存image数据
|
||||
setImage(item) // 缓存image数据
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const preloadImageFromMsgList = async (msgRecords) => {
|
||||
const imageIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const aar = jsonParseSafe(item.content)
|
||||
aar.forEach((item) => {
|
||||
if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
|
||||
const objectId = item.value
|
||||
if (!image.value[objectId]) {
|
||||
imageIds.add(objectId)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (imageIds.size > 0) {
|
||||
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setImage(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
Object.values(image.value).forEach((item) => {
|
||||
if (item.originUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(item.originUrl)
|
||||
}
|
||||
|
||||
if (item.thumbUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(item.thumbUrl)
|
||||
}
|
||||
})
|
||||
|
||||
image.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
image,
|
||||
imageInSession,
|
||||
localServerMap,
|
||||
setLocalServerMap,
|
||||
setImage,
|
||||
imageTrans,
|
||||
loadImageInfoFromContent
|
||||
setImageInSession,
|
||||
clearImageInSession,
|
||||
preloadImageFromMsg,
|
||||
preloadImageFromMsgList,
|
||||
clear
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,3 +13,7 @@ export * from './search'
|
||||
export * from './userCard'
|
||||
export * from './groupCard'
|
||||
export * from './image'
|
||||
export * from './audio'
|
||||
export * from './video'
|
||||
export * from './document'
|
||||
export * from './menu'
|
||||
|
||||
16
src/stores/menu.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useMenuStore = defineStore('anylink-menu', () => {
|
||||
const activeMenu = ref('') // 当前激活的菜单组件名称
|
||||
|
||||
// 设置当前激活的菜单
|
||||
const setActiveMenu = (menuName) => {
|
||||
activeMenu.value = menuName
|
||||
}
|
||||
|
||||
return {
|
||||
activeMenu,
|
||||
setActiveMenu
|
||||
}
|
||||
})
|
||||
@@ -3,12 +3,14 @@ import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
msgUpdateSessionService,
|
||||
msgChatSessionListService,
|
||||
msgQueryPartitionService
|
||||
msgQueryPartitionService,
|
||||
msgAtService
|
||||
} from '@/api/message'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useImageStore, useAudioStore, useVideoStore, useDocumentStore } from '@/stores'
|
||||
|
||||
// 消息功能相关需要缓存的数据,不持久化存储
|
||||
export const messageStore = defineStore('anylink-message', () => {
|
||||
export const useMessageStore = defineStore('anylink-message', () => {
|
||||
/**
|
||||
* message页面当前被选中的sessionId
|
||||
*/
|
||||
@@ -29,13 +31,13 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
* 格式:
|
||||
* {
|
||||
* sessionId_1: {
|
||||
* msgId_1: {msgId: msgId_1, fromId: xxx,...},
|
||||
* msgId_2: {msgId: msgId_2, fromId: xxx,...},
|
||||
* msgKey_1: {msgId: msgId_1, fromId: xxx,...}, //msgKey取自msgId,msgId在发消息之后会更新,但msgKey不会
|
||||
* msgKey_2: {msgId: msgId_2, fromId: xxx,...},
|
||||
* ...
|
||||
* }
|
||||
* sessionId_2: {
|
||||
* msgId_a: {msgId: msgId_a, fromId: xxx,...},
|
||||
* msgId_b: {msgId: msgId_b, fromId: xxx,...},
|
||||
* msgKey_a: {msgId: msgId_a, fromId: xxx,...},
|
||||
* msgKey_b: {msgId: msgId_b, fromId: xxx,...},
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
@@ -44,15 +46,26 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
const msgRecordsList = ref({})
|
||||
|
||||
/**
|
||||
* 会话消息ID排序后的数组,只存msgId,方便顺序查找
|
||||
* sessionList中同一会话下的msgKey排序后的数组
|
||||
* 格式:
|
||||
* {
|
||||
* sessionId_1: [msgId_1, msgId_2...],
|
||||
* sessionId_2: [msgId_a, msgId_b...]
|
||||
* sessionId_1: [msgKey_1, msgKey_2...],
|
||||
* sessionId_2: [msgKey_a, msgKey_b...]
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
const msgIdSortArray = ref({})
|
||||
const msgKeySortedArray = ref({})
|
||||
|
||||
/**
|
||||
* @ 消息存储
|
||||
* 格式:
|
||||
* {
|
||||
* sessionId_1: [{msgId:xxx, ...}, {msgId:xxx, ...}],
|
||||
* sessionId_2: [{msgId:xxx, ...}, {msgId:xxx, ...}]
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
const atRecordsList = ref({})
|
||||
|
||||
const addSession = (session) => {
|
||||
sessionList.value[session.sessionId] = session
|
||||
@@ -116,7 +129,34 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话列表中加入新的消息数组
|
||||
* 预加载消息中的媒体资源
|
||||
* @param {*} sessionId
|
||||
* @param {*} msgRecords
|
||||
*/
|
||||
const preloadResource = async (msgRecords) => {
|
||||
await useImageStore().preloadImageFromMsgList(msgRecords)
|
||||
await useAudioStore().preloadAudioFromMsgList(msgRecords)
|
||||
await useVideoStore().preloadVideoFromMsgList(msgRecords)
|
||||
await useDocumentStore().preloadDocumentFromMsgList(msgRecords)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新msgKey排序
|
||||
* @param {*} sessionId 会话id
|
||||
*/
|
||||
const updateMsgKeySort = (sessionId) => {
|
||||
// 更新排序
|
||||
const msgs = msgRecordsList.value[sessionId]
|
||||
const array = Object.keys(msgs).sort((a, b) => {
|
||||
const timeA = new Date(msgs[a].sendTime || msgs[a].msgTime).getTime()
|
||||
const timeB = new Date(msgs[b].sendTime || msgs[b].msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
msgKeySortedArray.value[sessionId] = array
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话列表中加入新的消息数组(预加载资源)
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} msgRecords 新的消息数组
|
||||
*/
|
||||
@@ -124,46 +164,46 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
if (!msgRecords?.length) return
|
||||
msgRecords.forEach((item) => {
|
||||
if (!msgRecordsList.value[sessionId]) {
|
||||
msgRecordsList.value[sessionId] = {}
|
||||
msgRecordsList.value[sessionId] = ref({})
|
||||
}
|
||||
msgRecordsList.value[sessionId][item.msgId] = item
|
||||
msgRecordsList.value[sessionId][item.msgId] = ref(item)
|
||||
})
|
||||
|
||||
// 更新排序
|
||||
const array = Object.values(msgRecordsList.value[sessionId])
|
||||
array.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
msgIdSortArray.value[sessionId] = array.map((item) => item.msgId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除某个消息:消息已发出后,用正式消息替换temp消息场景
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} msgId 消息id
|
||||
* @param {*} msgKey 消息id
|
||||
*/
|
||||
const removeMsgRecord = (sessionId, msgId) => {
|
||||
if (msgRecordsList.value[sessionId] && msgId in msgRecordsList.value[sessionId]) {
|
||||
delete msgRecordsList.value[sessionId][msgId]
|
||||
|
||||
// 更新排序
|
||||
const array = Object.values(msgRecordsList.value[sessionId])
|
||||
array.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
msgIdSortArray.value[sessionId] = array.map((item) => item.msgId)
|
||||
const removeMsgRecord = (sessionId, msgKey) => {
|
||||
if (msgRecordsList.value[sessionId] && msgKey in msgRecordsList.value[sessionId]) {
|
||||
msgRecordsList.value[sessionId][msgKey].delete = true
|
||||
}
|
||||
}
|
||||
|
||||
const getMsg = (sessionId, msgId) => {
|
||||
if (!msgRecordsList.value[sessionId] || !msgRecordsList.value[sessionId][msgId]) {
|
||||
return {}
|
||||
const revokeMsgRcord = (sessionId, msgKey) => {
|
||||
if (msgRecordsList.value[sessionId] && msgKey in msgRecordsList.value[sessionId]) {
|
||||
msgRecordsList.value[sessionId][msgKey].revoke = true
|
||||
}
|
||||
return msgRecordsList.value[sessionId][msgId]
|
||||
}
|
||||
|
||||
const getMsg = (sessionId, msgKey) => {
|
||||
if (!msgRecordsList.value[sessionId] || !msgRecordsList.value[sessionId][msgKey]) {
|
||||
return ref({})
|
||||
}
|
||||
return msgRecordsList.value[sessionId][msgKey]
|
||||
}
|
||||
|
||||
const updateMsg = (sessionId, msgKey, obj) => {
|
||||
if (!msgRecordsList.value[sessionId] || !msgRecordsList.value[sessionId][msgKey]) {
|
||||
return
|
||||
}
|
||||
|
||||
if ('msgId' in obj) msgRecordsList.value[sessionId][msgKey].msgId = obj.msgId
|
||||
if ('status' in obj) msgRecordsList.value[sessionId][msgKey].status = obj.status
|
||||
if ('msgTime' in obj) msgRecordsList.value[sessionId][msgKey].msgTime = obj.msgTime
|
||||
if ('sendTime' in obj) msgRecordsList.value[sessionId][msgKey].sendTime = obj.sendTime
|
||||
updateMsgKeySort(sessionId)
|
||||
}
|
||||
|
||||
const totalUnReadCount = computed(() => {
|
||||
@@ -173,6 +213,22 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* atRecordsList消息列表中加入新的@ 消息数组
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} at 新的@ 消息
|
||||
*/
|
||||
const addAtRecords = (sessionId, at) => {
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!atRecordsList.value[sessionId]) {
|
||||
atRecordsList.value[sessionId] = []
|
||||
}
|
||||
atRecordsList.value[sessionId].push(at)
|
||||
}
|
||||
|
||||
const el = document.getElementsByTagName('title')[0]
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
let task = null
|
||||
@@ -215,9 +271,14 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
const loadSessionList = async () => {
|
||||
if (!Object.keys(sessionList.value).length) {
|
||||
const res = await msgChatSessionListService()
|
||||
Object.keys(res.data.data).forEach((item) => {
|
||||
Object.keys(res.data.data).forEach(async (item) => {
|
||||
addSession(res.data.data[item].session)
|
||||
addMsgRecords(item, res.data.data[item].msgList)
|
||||
const msgList = res.data.data[item].msgList
|
||||
if (msgList) {
|
||||
await preloadResource(msgList)
|
||||
addMsgRecords(item, msgList)
|
||||
updateMsgKeySort(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,6 +303,16 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadAt = async () => {
|
||||
msgAtService().then((res) => {
|
||||
if (res.data.data) {
|
||||
res.data.data.forEach((item) => {
|
||||
addAtRecords(item.sessionId, item)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addPartition = (obj) => {
|
||||
partitions.value[obj.partitionId] = obj
|
||||
}
|
||||
@@ -258,12 +329,19 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
deleteSession,
|
||||
updateSession,
|
||||
loadSessionList,
|
||||
atRecordsList,
|
||||
addAtRecords,
|
||||
loadAt,
|
||||
|
||||
msgRecordsList,
|
||||
msgIdSortArray,
|
||||
msgKeySortedArray,
|
||||
preloadResource,
|
||||
updateMsgKeySort,
|
||||
addMsgRecords,
|
||||
removeMsgRecord,
|
||||
revokeMsgRcord,
|
||||
getMsg,
|
||||
updateMsg,
|
||||
|
||||
partitions,
|
||||
loadPartitions,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 不持久化存储
|
||||
export const searchStore = defineStore('anylink-search', () => {
|
||||
export const useSearchStore = defineStore('anylink-search', () => {
|
||||
const keywords = ref('')
|
||||
|
||||
const setKeywords = (words) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 界面设置相关需要缓存的设置
|
||||
export const settingStore = defineStore(
|
||||
export const useSettingStore = defineStore(
|
||||
'anylink-setting',
|
||||
() => {
|
||||
const sessionListDrag = ref(0)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { userInfoService } from '@/api/user'
|
||||
import { refreshToken } from '@/api/user'
|
||||
|
||||
// 用户模块
|
||||
export const userStore = defineStore(
|
||||
export const useUserStore = defineStore(
|
||||
'anylink-user',
|
||||
() => {
|
||||
const at = ref({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import {} from '@/api/group'
|
||||
|
||||
// userCard用户详情卡片相关的缓存数据,不持久化存储
|
||||
export const userCardStore = defineStore('anylink-userCard', () => {
|
||||
export const useUserCardStore = defineStore('anylink-userCard', () => {
|
||||
const isShow = ref(false)
|
||||
|
||||
const userInfo = ref({})
|
||||
|
||||
72
src/stores/video.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { mtsVideoService } from '@/api/mts'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { jsonParseSafe } from '@/js/utils/common'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// video的缓存数据,不持久化存储
|
||||
export const useVideoStore = defineStore('anylink-video', () => {
|
||||
/**
|
||||
* {
|
||||
* objectId_01: {objectId: objectId_01, url: xxx},
|
||||
* objectId_02: {objectId: objectId_02, url: xxx},
|
||||
* }
|
||||
*/
|
||||
const video = ref({})
|
||||
|
||||
const setVideo = (obj) => {
|
||||
video.value[obj.objectId] = obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地对象ID到服务器对象ID的映射
|
||||
* 在某些场景下需要通过本地对象ID找到服务器对象ID,例如复制刚刚发送的媒体消息
|
||||
*
|
||||
*/
|
||||
const localServerMap = ref({})
|
||||
|
||||
const setLocalServerMap = (localObjectId, serverObjectId) => {
|
||||
localServerMap.value[localObjectId] = serverObjectId
|
||||
}
|
||||
|
||||
const preloadVideoFromMsgList = async (msgRecords) => {
|
||||
const videoIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const aar = jsonParseSafe(item.content)
|
||||
aar.forEach((item) => {
|
||||
if (item.type === msgContentType.VIDEO) {
|
||||
const objectId = item.value
|
||||
if (!video.value[objectId]) {
|
||||
videoIds.add(objectId)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (videoIds.size > 0) {
|
||||
const res = await mtsVideoService({ objectIds: [...videoIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setVideo(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
Object.values(video.value).forEach((item) => {
|
||||
if (item.downloadUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(item.downloadUrl)
|
||||
}
|
||||
})
|
||||
|
||||
video.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
video,
|
||||
setVideo,
|
||||
localServerMap,
|
||||
setLocalServerMap,
|
||||
preloadVideoFromMsgList,
|
||||
clear
|
||||
}
|
||||
})
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
userVerifyCaptchaService,
|
||||
userForgetService
|
||||
} from '@/api/user.js'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { generateClientId } from '@/js/utils/common'
|
||||
import { flowLimiteWrapper } from '@/js/utils/flowLimite'
|
||||
|
||||
@@ -99,7 +99,7 @@ const rules = {
|
||||
]
|
||||
}
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const register = async () => {
|
||||
if (demoFlag) {
|
||||
@@ -587,15 +587,6 @@ watch(tabMode, () => {
|
||||
<div class="row">
|
||||
<span class="item">©2024 - 2025 Open AnyLink</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p class="item" style="margin: 0">
|
||||
<img src="@/assets/image/beian-logo.png" width="16" />
|
||||
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=61011602000694" target="_blank">
|
||||
陕公网安备61011602000694号
|
||||
</a>
|
||||
</p>
|
||||
<a class="item" href="https://beian.miit.gov.cn/" target="_blank">陕ICP备2025059454号-2</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,9 @@ import contactListIcon from '@/assets/svg/contactList.svg'
|
||||
import groupIcon from '@/assets/svg/group.svg'
|
||||
import organizationIcon from '@/assets/svg/organization.svg'
|
||||
import { onMounted } from 'vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await messageData.loadSessionList()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ChatRound, Microphone, VideoCamera } from '@element-plus/icons-vue'
|
||||
import { ChatRound, Phone, VideoCamera } from '@element-plus/icons-vue'
|
||||
import GroupItem from '@/components/item/GroupItem.vue'
|
||||
import router from '@/router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -57,7 +57,7 @@ const onVideoCall = () => {
|
||||
color="#409eff"
|
||||
@click="onVoiceCall"
|
||||
>
|
||||
<Microphone />
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-button"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { CirclePlus, Delete, Edit } from '@element-plus/icons-vue'
|
||||
import { useMenuStore } from '@/stores'
|
||||
|
||||
const emit = defineEmits(['selectMenu'])
|
||||
|
||||
const menuData = useMenuStore()
|
||||
const menuName = 'GroupPartitionOprMenu' // 菜单唯一标识
|
||||
|
||||
const menu = computed(() => {
|
||||
return [
|
||||
{
|
||||
@@ -31,23 +35,34 @@ const x = ref(0)
|
||||
const y = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
containerRef.value?.addEventListener('contextmenu', handleSessionMenu)
|
||||
containerRef.value?.addEventListener('contextmenu', handleShowMenu)
|
||||
document.addEventListener('keydown', handleEscEvent)
|
||||
document.addEventListener('click', closeMenu) //在其他地方的click事件要能关闭菜单
|
||||
document.addEventListener('contextmenu', closeMenu) //在其他地方的菜单事件也要能关闭菜单
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
containerRef.value?.removeEventListener('contextmenu', handleSessionMenu)
|
||||
containerRef.value?.removeEventListener('contextmenu', handleShowMenu)
|
||||
document.removeEventListener('keydown', handleEscEvent)
|
||||
document.removeEventListener('click', closeMenu)
|
||||
document.removeEventListener('contextmenu', closeMenu)
|
||||
})
|
||||
|
||||
const handleSessionMenu = (e) => {
|
||||
// 监听菜单状态变化
|
||||
watch(
|
||||
() => menuData.activeMenu,
|
||||
(newVal) => {
|
||||
if (newVal !== menuName && isShowMenu.value) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleShowMenu = (e) => {
|
||||
e.preventDefault() //阻止浏览器默认行为
|
||||
e.stopPropagation() // 阻止冒泡
|
||||
isShowMenu.value = true
|
||||
menuData.setActiveMenu(menuName)
|
||||
x.value = e.clientX
|
||||
y.value = e.clientY
|
||||
|
||||
@@ -74,7 +89,7 @@ const handleClick = (item) => {
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSessionMenu
|
||||
handleShowMenu
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,14 @@ import { Search, Edit, Delete, Check, Close } from '@element-plus/icons-vue'
|
||||
import AddButton from '@/components/common/AddButton.vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import SelectUserDialog from '@/components/common/SelectUserDialog.vue'
|
||||
import { groupStore, userStore, messageStore, userCardStore, groupCardStore } from '@/stores'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import {
|
||||
useGroupStore,
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useUserCardStore,
|
||||
useGroupCardStore
|
||||
} from '@/stores'
|
||||
import { combineId, smartMatch } from '@/js/utils/common'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { el_loading_options, PARTITION_TYPE } from '@/const/commonConst'
|
||||
@@ -15,11 +21,11 @@ import { MsgType } from '@/proto/msg'
|
||||
|
||||
const props = defineProps(['tab', 'params'])
|
||||
|
||||
const groupData = groupStore()
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const groupData = useGroupStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
const searchKey = ref('')
|
||||
const searchData = ref([])
|
||||
const isShowSelectDialog = ref(false)
|
||||
@@ -106,12 +112,10 @@ const showData = computed(() => {
|
||||
Object.values(initData.value).forEach((item) => {
|
||||
// 1.放群名称和群ID,或群备注的匹配结果
|
||||
if (
|
||||
item.groupName.toLowerCase().includes(searchKey.value.toLowerCase()) ||
|
||||
smartMatch(item.groupName, searchKey.value) ||
|
||||
item.groupId === searchKey.value ||
|
||||
(props.tab === 'mark' &&
|
||||
messageData.sessionList[item.groupId].mark
|
||||
.toLowerCase()
|
||||
.includes(searchKey.value.toLowerCase()))
|
||||
smartMatch(messageData.sessionList[item.groupId].mark, searchKey.value))
|
||||
) {
|
||||
item['sortMark'] = '1' // 让群名称和群ID的匹配结果放在前面, 因为群成员的匹配结果会滞后出现,如果不排序在出现的时候页面数据刷新变化很大
|
||||
data.push(item)
|
||||
@@ -149,7 +153,7 @@ const searchResultTips = computed(() => {
|
||||
let nickNameMatchCnt = {} // 对同一个群的搜索结果个数计数
|
||||
searchData.value.forEach((item) => {
|
||||
const regex = new RegExp(searchKey.value, 'gi')
|
||||
if (item.nickName.toLowerCase().includes(searchKey.value.toLowerCase())) {
|
||||
if (smartMatch(item.nickName, searchKey.value)) {
|
||||
if (item.groupId in nickNameMatchCnt) {
|
||||
nickNameMatchCnt[item.groupId] = nickNameMatchCnt[item.groupId] + 1
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { Search, MoreFilled } from '@element-plus/icons-vue'
|
||||
import SubCommon from '../components/SubCommon.vue'
|
||||
import { messageStore, groupCardStore, groupStore } from '@/stores'
|
||||
import { useMessageStore, useGroupCardStore, useGroupStore } from '@/stores'
|
||||
import { PARTITION_TYPE } from '@/const/commonConst'
|
||||
import {
|
||||
msgCreatePartitionService,
|
||||
@@ -15,13 +15,13 @@ import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import PartitionOprMenu from '@/views/contactList/group/components/PartitionOprMenu.vue'
|
||||
import EditDialog from '@/components/common/EditDialog.vue'
|
||||
import SelectGroupDialog from '@/components/common/SelectGroupDialog.vue'
|
||||
import { highLightedText } from '@/js/utils/common'
|
||||
import { highLightedText, smartMatch } from '@/js/utils/common'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { groupInfoService } from '@/api/group'
|
||||
|
||||
const messageData = messageStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const groupData = groupStore()
|
||||
const messageData = useMessageStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
const groupData = useGroupStore()
|
||||
|
||||
const partitionSearchKey = ref('')
|
||||
const isShowAddPartitionDialog = ref(false)
|
||||
@@ -77,7 +77,7 @@ const partitionsBySearch = computed(() => {
|
||||
} else {
|
||||
const data = {}
|
||||
Object.values(partitions.value).forEach((item) => {
|
||||
if (item.partitionName.toLowerCase().includes(partitionSearchKey.value.toLowerCase())) {
|
||||
if (smartMatch(item.partitionName, partitionSearchKey.value)) {
|
||||
data[item.partitionId] = item
|
||||
}
|
||||
})
|
||||
@@ -195,7 +195,7 @@ const onCustomContextMenu = (partitionId) => {
|
||||
|
||||
const showOperationMenu = (e, partitionId) => {
|
||||
showOprMenuPartitionId.value = partitionId
|
||||
oprMenuRef.value.handleSessionMenu(e)
|
||||
oprMenuRef.value.handleShowMenu(e)
|
||||
}
|
||||
|
||||
const onShowGroupCardFromSelectDialog = async (groupId) => {
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, computed } from 'vue'
|
||||
import {
|
||||
ChatRound,
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
Edit,
|
||||
Delete,
|
||||
Check,
|
||||
Close
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ChatRound, Phone, VideoCamera, Edit, Delete, Check, Close } from '@element-plus/icons-vue'
|
||||
import ContactItem from '@/components/item/ContactItem.vue'
|
||||
import { sessionShowTime } from '@/js/utils/common'
|
||||
import router from '@/router'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps(['type', 'session', 'partitions', 'keyWords'])
|
||||
const emit = defineEmits(['showUserCard'])
|
||||
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
const markEditing = ref(false)
|
||||
const newMark = ref('')
|
||||
@@ -27,14 +18,6 @@ const markEditRef = ref()
|
||||
const partitioEditing = ref(false)
|
||||
const newPartitionId = ref(props.session.partitionId)
|
||||
|
||||
const lastMsg = computed(() => {
|
||||
const msgIds = messageData.msgIdSortArray[props.session.sessionId]
|
||||
if (!msgIds?.length) {
|
||||
return {}
|
||||
}
|
||||
return messageData.getMsg(props.session.sessionId, msgIds[msgIds.length - 1])
|
||||
})
|
||||
|
||||
const onShowCard = () => {
|
||||
emit('showUserCard', {
|
||||
sessionId: props.session.sessionId,
|
||||
@@ -129,12 +112,6 @@ const onVideoCall = () => {
|
||||
style="width: 200px"
|
||||
></ContactItem>
|
||||
<div class="diff-display">
|
||||
<div v-if="props.type === 'all'" class="all">
|
||||
<div class="tips-block">{{ sessionShowTime(lastMsg.msgTime) }}</div>
|
||||
<div class="all-content text-ellipsis" :title="lastMsg.content">
|
||||
{{ lastMsg.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.type === 'mark'" class="mark">
|
||||
<div class="tips-block">备注</div>
|
||||
<div v-if="!markEditing" class="mark-content-wrapper">
|
||||
@@ -281,7 +258,7 @@ const onVideoCall = () => {
|
||||
color="#409eff"
|
||||
@click="onVoiceCall"
|
||||
>
|
||||
<Microphone />
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-button"
|
||||
@@ -333,17 +310,6 @@ const onVideoCall = () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.all {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.all-content {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mark {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { CirclePlus, Delete, Edit } from '@element-plus/icons-vue'
|
||||
import { useMenuStore } from '@/stores'
|
||||
|
||||
const emit = defineEmits(['selectMenu'])
|
||||
|
||||
const menuData = useMenuStore()
|
||||
const menuName = 'UserPartitionOprMenu' // 菜单唯一标识
|
||||
|
||||
const menu = computed(() => {
|
||||
return [
|
||||
{
|
||||
@@ -31,23 +35,34 @@ const x = ref(0)
|
||||
const y = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
containerRef.value?.addEventListener('contextmenu', handleSessionMenu)
|
||||
containerRef.value?.addEventListener('contextmenu', handleShowMenu)
|
||||
document.addEventListener('keydown', handleEscEvent)
|
||||
document.addEventListener('click', closeMenu) //在其他地方的click事件要能关闭菜单
|
||||
document.addEventListener('contextmenu', closeMenu) //在其他地方的菜单事件也要能关闭菜单
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
containerRef.value?.removeEventListener('contextmenu', handleSessionMenu)
|
||||
containerRef.value?.removeEventListener('contextmenu', handleShowMenu)
|
||||
document.removeEventListener('keydown', handleEscEvent)
|
||||
document.removeEventListener('click', closeMenu)
|
||||
document.removeEventListener('contextmenu', closeMenu)
|
||||
})
|
||||
|
||||
const handleSessionMenu = (e) => {
|
||||
// 监听菜单状态变化
|
||||
watch(
|
||||
() => menuData.activeMenu,
|
||||
(newVal) => {
|
||||
if (newVal !== menuName && isShowMenu.value) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleShowMenu = (e) => {
|
||||
e.preventDefault() //阻止浏览器默认行为
|
||||
e.stopPropagation() // 阻止冒泡
|
||||
isShowMenu.value = true
|
||||
menuData.setActiveMenu(menuName)
|
||||
x.value = e.clientX
|
||||
y.value = e.clientY
|
||||
|
||||
@@ -74,7 +89,7 @@ const handleClick = (item) => {
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSessionMenu
|
||||
handleShowMenu
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { messageStore, userCardStore } from '@/stores'
|
||||
import { useMessageStore, useUserCardStore } from '@/stores'
|
||||
import ContactListUserItem from '@/views/contactList/user/components/ContactListUserItem.vue'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { smartMatch } from '@/js/utils/common'
|
||||
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return Object.keys(allData.value).length
|
||||
@@ -30,7 +31,7 @@ const allData = computed(() => {
|
||||
data.push(item)
|
||||
} else {
|
||||
if (
|
||||
item.objectInfo.nickName.toLowerCase().includes(searchKey.value.toLowerCase()) ||
|
||||
smartMatch(item.objectInfo.nickName, searchKey.value) ||
|
||||
item.objectInfo.account === searchKey.value
|
||||
) {
|
||||
data.push(item)
|
||||
@@ -43,12 +44,12 @@ const allData = computed(() => {
|
||||
return data
|
||||
} else {
|
||||
return data.sort((a, b) => {
|
||||
const a_msgIds = messageData.msgIdSortArray[a.sessionId]
|
||||
const a_msgIds = messageData.msgKeySortedArray[a.sessionId]
|
||||
const a_msgIds_len = a_msgIds?.length
|
||||
if (!a_msgIds_len) return 1
|
||||
const a_lastMsg = messageData.getMsg(a.sessionId, a_msgIds[a_msgIds_len - 1])
|
||||
|
||||
const b_msgIds = messageData.msgIdSortArray[b.sessionId]
|
||||
const b_msgIds = messageData.msgKeySortedArray[b.sessionId]
|
||||
const b_msgIds_len = b_msgIds?.length
|
||||
if (!b_msgIds_len) return -1
|
||||
const b_lastMsg = messageData.getMsg(b.sessionId, b_msgIds[b_msgIds_len - 1])
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { messageStore, userCardStore } from '@/stores'
|
||||
import { useMessageStore, useUserCardStore } from '@/stores'
|
||||
import ContactListUserItem from '@/views/contactList/user/components/ContactListUserItem.vue'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { smartMatch } from '@/js/utils/common'
|
||||
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const totalCount = computed(() => {
|
||||
return Object.keys(markData.value).length
|
||||
})
|
||||
@@ -30,9 +31,9 @@ const markData = computed(() => {
|
||||
data.push(item)
|
||||
} else {
|
||||
if (
|
||||
item.objectInfo.nickName.toLowerCase().includes(markSearchKey.value.toLowerCase()) ||
|
||||
smartMatch(item.objectInfo.nickName, markSearchKey.value) ||
|
||||
item.objectInfo.account === markSearchKey.value ||
|
||||
item.mark.toLowerCase().includes(markSearchKey.value.toLowerCase())
|
||||
smartMatch(item.mark, markSearchKey.value)
|
||||
) {
|
||||
data.push(item)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,16 @@ import {
|
||||
} from '@/api/message'
|
||||
import { PARTITION_TYPE } from '@/const/commonConst'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { messageStore, userStore, userCardStore } from '@/stores'
|
||||
import { useMessageStore, useUserStore, useUserCardStore } from '@/stores'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import SelectUserDialog from '@/components/common/SelectUserDialog.vue'
|
||||
import { combineId, highLightedText } from '@/js/utils/common'
|
||||
import { combineId, highLightedText, smartMatch } from '@/js/utils/common'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const messageData = messageStore()
|
||||
const userData = userStore()
|
||||
const userCardData = userCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const userData = useUserStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const partitionSearchKey = ref('')
|
||||
const userSearchKey = ref('')
|
||||
const isShowAddPartitionDialog = ref(false)
|
||||
@@ -73,7 +73,7 @@ const detailData = computed(() => {
|
||||
data.push(item)
|
||||
} else {
|
||||
if (
|
||||
item.objectInfo.nickName.toLowerCase().includes(userSearchKey.value.toLowerCase()) ||
|
||||
smartMatch(item.objectInfo.nickName, userSearchKey.value) ||
|
||||
item.objectInfo.account === userSearchKey.value
|
||||
)
|
||||
data.push(item)
|
||||
@@ -100,7 +100,7 @@ const partitionsBySearch = computed(() => {
|
||||
} else {
|
||||
const data = {}
|
||||
Object.values(partitions.value).forEach((item) => {
|
||||
if (item.partitionName.toLowerCase().includes(partitionSearchKey.value.toLowerCase())) {
|
||||
if (smartMatch(item.partitionName, partitionSearchKey.value)) {
|
||||
data[item.partitionId] = item
|
||||
}
|
||||
})
|
||||
@@ -198,7 +198,7 @@ const onCustomContextMenu = (partitionId) => {
|
||||
|
||||
const showOperationMenu = (e, partitionId) => {
|
||||
showOprMenuPartitionId.value = partitionId
|
||||
oprMenuRef.value.handleSessionMenu(e)
|
||||
oprMenuRef.value.handleShowMenu(e)
|
||||
}
|
||||
|
||||
const onShowAddSessionByButton = (partitionId) => {
|
||||
|
||||
@@ -8,7 +8,16 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import ContactUs from '@/views/layout/components/ContactUs.vue'
|
||||
import { userStore, messageStore, searchStore, groupStore } from '@/stores'
|
||||
import {
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useSearchStore,
|
||||
useGroupStore,
|
||||
useImageStore,
|
||||
useAudioStore,
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import router from '@/router'
|
||||
import MyCard from '@/views/layout/components/MyCard.vue'
|
||||
import NaviMenu from '@/views/layout/components/NaviMenu.vue'
|
||||
@@ -30,10 +39,14 @@ import githubIcon from '@/assets/svg/github.svg'
|
||||
import SourceCode from '@/views/layout/components/SourceCode.vue'
|
||||
|
||||
const myAvatar = ref()
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const searchData = searchStore()
|
||||
const groupData = groupStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const searchData = useSearchStore()
|
||||
const groupData = useGroupStore()
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
const isShowMyCard = ref(false)
|
||||
const contactUsRef = ref(null)
|
||||
const sourceCodeRef = ref(null)
|
||||
@@ -87,6 +100,10 @@ onUnmounted(() => {
|
||||
userData.clear()
|
||||
messageData.clear()
|
||||
searchData.clear()
|
||||
imageData.clear()
|
||||
audioData.clear()
|
||||
videoData.clear()
|
||||
documentData.clear()
|
||||
wsConnect.closeWs()
|
||||
})
|
||||
|
||||
@@ -150,9 +167,6 @@ const autoLogout = () => {
|
||||
autoLogoutTimer = setTimeout(() => {
|
||||
userLogoutService(userData.user.account).finally(() => {
|
||||
userData.clear()
|
||||
messageData.clear()
|
||||
searchData.clear()
|
||||
wsConnect.closeWs()
|
||||
router.push('/login')
|
||||
})
|
||||
}, LOGOUT_AFTER_DURATION)
|
||||
@@ -167,9 +181,6 @@ const onExit = async () => {
|
||||
.then(() => {
|
||||
userLogoutService(userData.user.account).finally(() => {
|
||||
userData.clear()
|
||||
messageData.clear()
|
||||
searchData.clear()
|
||||
wsConnect.closeWs()
|
||||
router.push('/login')
|
||||
})
|
||||
})
|
||||
|
||||