Compare commits
66 Commits
v1.0.0-alp
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
25
README.md
@@ -24,19 +24,15 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 单聊
|
||||
- [x] 群聊
|
||||
|
||||
#### 通话功能
|
||||
|
||||
- [ ] 语音通话
|
||||
- [ ] 视频通话
|
||||
|
||||
#### 消息类型
|
||||
|
||||
- [x] 文本
|
||||
- [x] 表情
|
||||
- [x] 图片
|
||||
- [ ] 音频
|
||||
- [ ] 视频
|
||||
- [ ] 文件
|
||||
- [x] 语音
|
||||
- [x] 音频
|
||||
- [x] 视频
|
||||
- [x] 文件
|
||||
|
||||
#### 消息功能
|
||||
|
||||
@@ -61,8 +57,7 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 群公告
|
||||
- [x] 群系统消息
|
||||
- [x] 群转让
|
||||
- [ ] 万人大群
|
||||
- [ ] 团队组织群
|
||||
- [ ] 组织群
|
||||
- [ ] 公开群
|
||||
|
||||
#### 通讯录功能
|
||||
@@ -73,6 +68,11 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
- [x] 群分组
|
||||
- [ ] 组织管理
|
||||
|
||||
#### 通话功能
|
||||
|
||||
- [ ] 语音通话
|
||||
- [ ] 视频通话
|
||||
|
||||
#### 会议功能
|
||||
|
||||
- [ ] 语音会议
|
||||
@@ -93,6 +93,7 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
|
||||
- [ ] 大文件传输
|
||||
- [ ] 待办事项
|
||||
- [ ] 管理控制台
|
||||
|
||||
## 项目预览
|
||||
|
||||
@@ -100,6 +101,8 @@ Open AnyLink是一款面向企业的IM即时通讯解决方案,旨在帮助企
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -123,7 +126,7 @@ 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,微信群有效期:4月9日
|
||||
|
||||
## 如何联系我们
|
||||
|
||||
|
||||
|
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 After Width: | Height: | Size: 121 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "anylink-web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -26,8 +26,10 @@
|
||||
"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",
|
||||
|
||||
@@ -7,3 +7,15 @@ export const mtsUploadService = (obj) => {
|
||||
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/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/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/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/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/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 |
@@ -11,7 +11,13 @@ import DeleteButton from '@/components/common/DeleteButton.vue'
|
||||
import EditAvatar from '@/components/common/EditAvatar.vue'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { groupStore, userStore, messageStore, userCardStore, groupCardStore } from '@/stores'
|
||||
import {
|
||||
useGroupStore,
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useUserCardStore,
|
||||
useGroupCardStore
|
||||
} from '@/stores'
|
||||
import SelectUserDialog from '../common/SelectUserDialog.vue'
|
||||
import SingleSelectDialog from '../common/SingleSelectDialog.vue'
|
||||
import {
|
||||
@@ -28,11 +34,11 @@ 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 isShowEditAvatar = ref(false)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -291,7 +297,7 @@ const handleAvatarError = () => {
|
||||
color="#409eff"
|
||||
@click="onVoiceCall"
|
||||
>
|
||||
<Microphone />
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-button"
|
||||
@@ -417,6 +423,7 @@ const handleAvatarError = () => {
|
||||
|
||||
.body {
|
||||
width: 100%;
|
||||
padding: 5px 0 5px 0;
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
@@ -425,7 +432,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,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { Plus, Upload, RefreshLeft, RefreshRight, Refresh } from '@element-plus/icons-vue'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -9,7 +9,7 @@ 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('')
|
||||
@@ -30,8 +30,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
|
||||
@@ -71,6 +71,7 @@ const onUpload = async () => {
|
||||
try {
|
||||
const res = await mtsUploadService({ file: file, storeType: 0 })
|
||||
emit('update:newAvatar', {
|
||||
avatarId: res.data.data.objectId,
|
||||
avatar: res.data.data.originUrl,
|
||||
avatarThumb: res.data.data.thumbUrl
|
||||
})
|
||||
@@ -230,11 +231,19 @@ const onRotateRight = () => {
|
||||
.preview-100 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
:deep(img) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-40 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
:deep(img) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,
|
||||
@@ -19,10 +19,10 @@ 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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,3 +10,22 @@ export const proto = {
|
||||
|
||||
// 和服务端约定好的,第一个消息都是从10001开始的
|
||||
export const BEGIN_MSG_ID = 10001
|
||||
|
||||
// 消息内容类型
|
||||
export const msgContentType = {
|
||||
MIX: 0, // 组合,包含多种类型
|
||||
TEXT: 1, // 文本
|
||||
IMAGE: 2, // 图片
|
||||
RECORDING: 3, // 语音
|
||||
AUDIO: 4, // 音频文件
|
||||
EMOJI: 5, // 视频
|
||||
VIDEO: 6, // 表情
|
||||
DOCUMENT: 7 // 文档
|
||||
}
|
||||
|
||||
// 消息发送状态
|
||||
export const msgSendStatus = {
|
||||
PENDING: 'pending', // 发送中
|
||||
OK: 'ok', // 发送成功
|
||||
FAILED: 'failed' // 发送失败
|
||||
}
|
||||
|
||||
@@ -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.1.0'
|
||||
|
||||
export const LEAVING_AFTER_DURATION = 5 * 60 * 1000
|
||||
export const LOGOUT_AFTER_DURATION = 8 * 60 * 60 * 1000
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -37,7 +36,7 @@ export const onReceiveChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
...readParams
|
||||
})
|
||||
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
await messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
@@ -53,16 +52,9 @@ export const onReceiveChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
}
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
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
|
||||
|
||||
@@ -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,7 +34,7 @@ export const onReceiveGroupChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
...readParams
|
||||
})
|
||||
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
await messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
@@ -51,16 +50,9 @@ export const onReceiveGroupChatMsg = (msgListDiv = null, capacity = null) => {
|
||||
}
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
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()
|
||||
|
||||
@@ -28,7 +27,7 @@ export const onReceiveGroupSystemMsg = (msgListDiv = null, capacity = null) => {
|
||||
})
|
||||
|
||||
// 更新聊天记录
|
||||
messageData.addMsgRecords(sessionId, [
|
||||
await messageData.addMsgRecords(sessionId, [
|
||||
{
|
||||
sessionId: sessionId,
|
||||
msgId: msg.body.msgId,
|
||||
@@ -40,16 +39,9 @@ export const onReceiveGroupSystemMsg = (msgListDiv = null, capacity = null) => {
|
||||
])
|
||||
|
||||
// 如果是当前正打开的会话
|
||||
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,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,7 +1,7 @@
|
||||
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()
|
||||
|
||||
export const playMsgReceive = () => {
|
||||
if (!userData.user.newMsgTips) {
|
||||
|
||||
@@ -227,3 +227,15 @@ 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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || '服务异常')
|
||||
console.error(
|
||||
'The response was not the expected code: ',
|
||||
res.config?.url,
|
||||
res.data?.code,
|
||||
res.data?.desc
|
||||
)
|
||||
return Promise.reject(res.data)
|
||||
},
|
||||
async (err) => {
|
||||
if (err.response?.status === 401) {
|
||||
userStore().clearAt()
|
||||
userStore().clearRt()
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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) => {
|
||||
@@ -11,7 +11,7 @@ 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,
|
||||
@@ -35,7 +35,7 @@ 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,
|
||||
@@ -87,7 +87,7 @@ 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,
|
||||
@@ -111,7 +111,7 @@ 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,
|
||||
@@ -135,7 +135,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 +156,7 @@ export const statusSyncConstructor = (status) => {
|
||||
isExtension: false
|
||||
})
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const body = Body.create({
|
||||
fromId: userData.user.account,
|
||||
fromClient: userData.clientId,
|
||||
|
||||
@@ -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 {
|
||||
@@ -105,7 +105,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--
|
||||
@@ -173,7 +176,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 +332,14 @@ 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, seq, before, after) {
|
||||
const sequence = seq || uuidv4()
|
||||
const data = this.dataConstructor[msgType](sessionId, remoteId, content, sequence)
|
||||
callbackBefore(sequence, data)
|
||||
this.msgIdRefillCallback[sequence] = callbackAfter
|
||||
before(sequence, data)
|
||||
this.msgIdRefillCallback[sequence] = after
|
||||
this.sendAgent(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
60
src/stores/audio.js
Normal file
@@ -0,0 +1,60 @@
|
||||
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({})
|
||||
|
||||
/**
|
||||
* 在同一个session中的audio(id)集合
|
||||
*/
|
||||
const audioInSession = ref({})
|
||||
|
||||
const setAudio = (sessionId, obj) => {
|
||||
audio.value[obj.objectId] = obj
|
||||
if (!audioInSession.value[sessionId]) {
|
||||
audioInSession.value[sessionId] = []
|
||||
}
|
||||
audioInSession.value[sessionId].push(obj.objectId)
|
||||
}
|
||||
|
||||
const preloadAudio = async (sessionId, msgRecords) => {
|
||||
const audioIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const content = item.content
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (
|
||||
(contentJson && contentJson['type'] === msgContentType.RECORDING) ||
|
||||
(contentJson && contentJson['type'] === msgContentType.AUDIO)
|
||||
) {
|
||||
const objectId = contentJson['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(sessionId, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
audio,
|
||||
audioInSession,
|
||||
setAudio,
|
||||
preloadAudio
|
||||
}
|
||||
})
|
||||
57
src/stores/document.js
Normal file
@@ -0,0 +1,57 @@
|
||||
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({})
|
||||
|
||||
/**
|
||||
* 在同一个session中的document(id)集合
|
||||
*/
|
||||
const documentInSession = ref({})
|
||||
|
||||
const setDocument = (sessionId, obj) => {
|
||||
document.value[obj.objectId] = obj
|
||||
if (!documentInSession.value[sessionId]) {
|
||||
documentInSession.value[sessionId] = []
|
||||
}
|
||||
documentInSession.value[sessionId].push(obj.objectId)
|
||||
}
|
||||
|
||||
const preloadDocument = async (sessionId, msgRecords) => {
|
||||
const documentIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const content = item.content
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (contentJson && contentJson['type'] === msgContentType.DOCUMENT) {
|
||||
const objectId = contentJson['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(sessionId, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
documentInSession,
|
||||
setDocument,
|
||||
preloadDocument
|
||||
}
|
||||
})
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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,13 @@
|
||||
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},
|
||||
@@ -71,11 +73,45 @@ export const imageStore = defineStore('anylink-image', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const preloadImage = async (sessionId, msgRecords) => {
|
||||
const imageIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const content = item.content
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (contentJson && contentJson['type'] === msgContentType.IMAGE) {
|
||||
const objectId = contentJson['value']
|
||||
if (!image.value[objectId]) {
|
||||
imageIds.add(objectId)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
image,
|
||||
imageInSession,
|
||||
setImage,
|
||||
imageTrans,
|
||||
loadImageInfoFromContent
|
||||
loadImageInfoFromContent,
|
||||
preloadImage
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,3 +13,6 @@ export * from './search'
|
||||
export * from './userCard'
|
||||
export * from './groupCard'
|
||||
export * from './image'
|
||||
export * from './audio'
|
||||
export * from './video'
|
||||
export * from './document'
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
msgQueryPartitionService
|
||||
} 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
|
||||
*/
|
||||
@@ -120,7 +121,13 @@ export const messageStore = defineStore('anylink-message', () => {
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} msgRecords 新的消息数组
|
||||
*/
|
||||
const addMsgRecords = (sessionId, msgRecords) => {
|
||||
const addMsgRecords = async (sessionId, msgRecords) => {
|
||||
// 预加载消息中的图片和音频
|
||||
await useImageStore().preloadImage(sessionId, msgRecords)
|
||||
await useAudioStore().preloadAudio(sessionId, msgRecords)
|
||||
await useVideoStore().preloadVideo(sessionId, msgRecords)
|
||||
await useDocumentStore().preloadDocument(sessionId, msgRecords)
|
||||
|
||||
if (!msgRecords?.length) return
|
||||
msgRecords.forEach((item) => {
|
||||
if (!msgRecordsList.value[sessionId]) {
|
||||
@@ -215,9 +222,12 @@ 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 addMsgRecords(item, msgList)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
|
||||
57
src/stores/video.js
Normal file
@@ -0,0 +1,57 @@
|
||||
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({})
|
||||
|
||||
/**
|
||||
* 在同一个session中的video(id)集合
|
||||
*/
|
||||
const videoInSession = ref({})
|
||||
|
||||
const setVideo = (sessionId, obj) => {
|
||||
video.value[obj.objectId] = obj
|
||||
if (!videoInSession.value[sessionId]) {
|
||||
videoInSession.value[sessionId] = []
|
||||
}
|
||||
videoInSession.value[sessionId].push(obj.objectId)
|
||||
}
|
||||
|
||||
const preloadVideo = async (sessionId, msgRecords) => {
|
||||
const videoIds = new Set()
|
||||
msgRecords.forEach((item) => {
|
||||
const content = item.content
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (contentJson && contentJson['type'] === msgContentType.VIDEO) {
|
||||
const objectId = contentJson['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(sessionId, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
video,
|
||||
videoInSession,
|
||||
setVideo,
|
||||
preloadVideo
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,13 @@ 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 {
|
||||
useGroupStore,
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useUserCardStore,
|
||||
useGroupCardStore
|
||||
} from '@/stores'
|
||||
import { combineId } from '@/js/utils/common'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
@@ -19,9 +19,9 @@ import { highLightedText } 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)
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
<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'
|
||||
@@ -9,8 +9,8 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return Object.keys(allData.value).length
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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'
|
||||
@@ -9,8 +9,8 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const totalCount = computed(() => {
|
||||
return Object.keys(markData.value).length
|
||||
})
|
||||
|
||||
@@ -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 { 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)
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 } from '@/stores'
|
||||
import router from '@/router'
|
||||
import MyCard from '@/views/layout/components/MyCard.vue'
|
||||
import NaviMenu from '@/views/layout/components/NaviMenu.vue'
|
||||
@@ -30,10 +30,10 @@ 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 isShowMyCard = ref(false)
|
||||
const contactUsRef = ref(null)
|
||||
const sourceCodeRef = ref(null)
|
||||
@@ -150,9 +150,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 +164,6 @@ const onExit = async () => {
|
||||
.then(() => {
|
||||
userLogoutService(userData.user.account).finally(() => {
|
||||
userData.clear()
|
||||
messageData.clear()
|
||||
searchData.clear()
|
||||
wsConnect.closeWs()
|
||||
router.push('/login')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Close, Male, Female } from '@element-plus/icons-vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import default_avatar from '@/assets/image/default_avatar.png'
|
||||
import router from '@/router'
|
||||
|
||||
const props = defineProps(['isShow'])
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const truncatedSignature = computed(() => {
|
||||
const signature = userData.user.signature || '您还没有个性签名。'
|
||||
@@ -37,12 +37,19 @@ const onClickAvatar = () => {
|
||||
router.push('/setting/personal')
|
||||
}
|
||||
|
||||
const showAvatar = ref(userData.user.avatarThumb)
|
||||
const showAvatar = ref(userData.user.avatarThumb || default_avatar)
|
||||
|
||||
const handleAvatarError = () => {
|
||||
showAvatar.value = default_avatar
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userData.user.avatarThumb,
|
||||
(newValue) => {
|
||||
showAvatar.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.isShow,
|
||||
(newValue) => {
|
||||
@@ -89,7 +96,7 @@ watch(
|
||||
.card-dialog {
|
||||
background: linear-gradient(to bottom, #a0cfff, #fff);
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
height: 360px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 20px;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await messageData.loadSessionList()
|
||||
|
||||
@@ -2,14 +2,10 @@ eslint-disable prettier/prettier
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick, watch } from 'vue'
|
||||
import {
|
||||
Microphone,
|
||||
Phone,
|
||||
VideoCamera,
|
||||
MoreFilled,
|
||||
CirclePlus,
|
||||
LocationInformation,
|
||||
Clock,
|
||||
FolderAdd,
|
||||
CreditCard,
|
||||
ArrowDownBold,
|
||||
ArrowUp
|
||||
} from '@element-plus/icons-vue'
|
||||
@@ -17,18 +13,18 @@ import DragLine from '@/components/common/DragLine.vue'
|
||||
import SearchBox from '@/components/search/SearchBox.vue'
|
||||
import AddButton from '@/components/common/AddButton.vue'
|
||||
import SessionItem from '@/views/message/components/SessionItem.vue'
|
||||
import InputTool from '@/views/message/components/InputTool.vue'
|
||||
import InputToolBar from '@/views/message/components/InputToolBar.vue'
|
||||
import InputEditor from '@/views/message/components/InputEditor.vue'
|
||||
import MessageItem from '@/views/message/components/MessageItem.vue'
|
||||
import SessionTag from '@/views/message/components/SessionTag.vue'
|
||||
import SelectUserDialog from '@/components/common/SelectUserDialog.vue'
|
||||
import {
|
||||
userStore,
|
||||
settingStore,
|
||||
messageStore,
|
||||
userCardStore,
|
||||
groupCardStore,
|
||||
groupStore
|
||||
useUserStore,
|
||||
useSettingStore,
|
||||
useMessageStore,
|
||||
useUserCardStore,
|
||||
useGroupCardStore,
|
||||
useGroupStore
|
||||
} from '@/stores'
|
||||
import backgroupImage from '@/assets/svg/messagebx_bg.svg'
|
||||
import {
|
||||
@@ -46,21 +42,20 @@ import { el_loading_options } from '@/const/commonConst'
|
||||
import { combineId, sessionIdConvert } from '@/js/utils/common'
|
||||
import SessionMenu from '@/views/message/components/SessionMenu.vue'
|
||||
import router from '@/router'
|
||||
import { BEGIN_MSG_ID } from '@/const/msgConst'
|
||||
import { BEGIN_MSG_ID, msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||
import EditDialog from '@/components/common/EditDialog.vue'
|
||||
import AddOprMenu from './components/AddOprMenu.vue'
|
||||
import MessageGroupRightSide from './components/MessageGroupRightSide.vue'
|
||||
import EmojiBox from './components/EmojiBox.vue'
|
||||
import EmojiIcon from '@/assets/svg/emoji.svg'
|
||||
import AddOprMenu from '@/views/message/components/AddOprMenu.vue'
|
||||
import MessageGroupRightSide from '@/views/message/components/MessageGroupRightSide.vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import InputRecorder from '@/views/message/components/InputRecorder.vue'
|
||||
import { playMsgSend } from '@/js/utils/audio'
|
||||
|
||||
const userData = userStore()
|
||||
const settingData = settingStore()
|
||||
const messageData = messageStore()
|
||||
const userCardData = userCardStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const groupData = groupStore()
|
||||
const userData = useUserStore()
|
||||
const settingData = useSettingStore()
|
||||
const messageData = useMessageStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
const groupData = useGroupStore()
|
||||
const sessionListRef = ref()
|
||||
|
||||
const asideWidth = ref(0)
|
||||
@@ -72,6 +67,8 @@ const inputBoxHeightMin = 200
|
||||
const inputBoxHeightMax = 500
|
||||
|
||||
const msgListDiv = ref()
|
||||
const disToBottom = ref(0)
|
||||
const nearBottomDis = 50
|
||||
const newMsgTips = ref({
|
||||
isShowTopTips: false,
|
||||
isShowBottomTips: false,
|
||||
@@ -79,7 +76,7 @@ const newMsgTips = ref({
|
||||
firstElement: null
|
||||
})
|
||||
|
||||
const isShowEmojiBox = ref(false)
|
||||
const inputToolBarRef = ref()
|
||||
|
||||
const myAccount = computed(() => {
|
||||
return userData.user.account
|
||||
@@ -180,10 +177,20 @@ const initSession = (sessionId) => {
|
||||
isLoadMoreLoading: false
|
||||
}
|
||||
}
|
||||
isShowRecorder.value = false // 麦克风输入状态重置
|
||||
inputRecorderRef.value?.cancelSend() // 取消音频发送
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位的session的位置
|
||||
* 这里受限sessionListSorted的排序速度,如果定位的时候排序没有完成,定位的位置就不对
|
||||
* @param sessionId
|
||||
*/
|
||||
const locateSession = (sessionId) => {
|
||||
nextTick(() => {
|
||||
let task
|
||||
let count = 0
|
||||
task = setInterval(() => {
|
||||
if (count >= 3) clearInterval(task)
|
||||
const selectedElement = document.querySelector(`#session-item-${sessionIdConvert(sessionId)}`)
|
||||
// 如果被选中元素的上边在scrollTop之的上面,或这在下边在scrollTop+clientHeight的下面(显示不全或者完全没有显示),则需要重新定位
|
||||
// 由于offsetTop和offsetHeight不包含外边距,因此定位存在细小误差,暂不处理
|
||||
@@ -195,7 +202,8 @@ const locateSession = (sessionId) => {
|
||||
) {
|
||||
sessionListRef.value.scrollTop = selectedElement.offsetTop - sessionListRef.value.clientHeight
|
||||
}
|
||||
})
|
||||
count++
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const msgIdsShow = computed(() => {
|
||||
@@ -240,9 +248,9 @@ onMounted(async () => {
|
||||
asideWidth.value = settingData.sessionListDrag[myAccount.value] || 300
|
||||
inputBoxHeight.value = settingData.inputBoxDrag[myAccount.value] || 300
|
||||
|
||||
wsConnect.bindEvent(MsgType.CHAT, onReceiveChatMsg(msgListDiv, capacity)) //绑定接收Chat消息的事件
|
||||
wsConnect.bindEvent(MsgType.GROUP_CHAT, onReceiveGroupChatMsg(msgListDiv, capacity)) //绑定接收GroupChat消息的事件
|
||||
wsConnect.bindGroupSystemMsgEvent(onReceiveGroupSystemMsg(msgListDiv, capacity)) //绑定接收群系统消息事件
|
||||
wsConnect.bindEvent(MsgType.CHAT, onReceiveChatMsg(updateScroll, capacity)) //绑定接收Chat消息的事件
|
||||
wsConnect.bindEvent(MsgType.GROUP_CHAT, onReceiveGroupChatMsg(updateScroll, capacity)) //绑定接收GroupChat消息的事件
|
||||
wsConnect.bindGroupSystemMsgEvent(onReceiveGroupSystemMsg(updateScroll, capacity)) //绑定接收群系统消息事件
|
||||
|
||||
// 这里要接收从其他页面跳转过来传递的sessionId参数
|
||||
const routerSessionId = router.currentRoute.value.query.sessionId
|
||||
@@ -277,10 +285,11 @@ const handleMsgListWheel = async () => {
|
||||
}
|
||||
|
||||
const clientHeight = document.querySelector('.message-main').clientHeight
|
||||
const diffToBottom = msgListDiv.value.scrollHeight - msgListDiv.value.scrollTop - clientHeight
|
||||
// diffToBottom接近50个像素的时候,关闭底部未读tips控件
|
||||
newMsgTips.value.isShowBottomTips = diffToBottom < 50 ? false : newMsgTips.value.isShowBottomTips
|
||||
// isShowReturnBottom.value = diffToBottom > 300 // 控制是否显示"回到底部"的按钮。暂时取消这个提示功能,与消息提示的按钮显得有点重复
|
||||
disToBottom.value = msgListDiv.value.scrollHeight - msgListDiv.value.scrollTop - clientHeight
|
||||
// disToBottom接近50个像素的时候,关闭底部未读tips控件
|
||||
newMsgTips.value.isShowBottomTips =
|
||||
disToBottom.value < nearBottomDis ? false : newMsgTips.value.isShowBottomTips
|
||||
// isShowReturnBottom.value = disToBottom.value > 300 // 控制是否显示"回到底部"的按钮。暂时取消这个提示功能,与消息提示的按钮显得有点重复
|
||||
|
||||
if (newMsgTips.value.firstElement?.getBoundingClientRect().top > 0) {
|
||||
newMsgTips.value.isShowTopTips = false
|
||||
@@ -311,12 +320,10 @@ const sessionListSorted = computed(() => {
|
||||
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_len = b_msgIds?.length
|
||||
if (!b_msgIds_len) return -1
|
||||
const b_lastMsg = messageData.getMsg(b.sessionId, b_msgIds[b_msgIds_len - 1])
|
||||
|
||||
const bTime = new Date(b_lastMsg.msgTime).getTime()
|
||||
const aTIme = new Date(a_lastMsg.msgTime).getTime()
|
||||
if (bTime !== aTIme) {
|
||||
@@ -408,7 +415,7 @@ const pullMsg = async (endMsgId = null) => {
|
||||
const res = await msgChatPullMsgService(params)
|
||||
const msgCount = res.data.data.count
|
||||
if (msgCount > 0) {
|
||||
messageData.addMsgRecords(sessionId, res.data.data.msgList)
|
||||
await messageData.addMsgRecords(sessionId, res.data.data.msgList)
|
||||
}
|
||||
|
||||
if (msgCount < pageSize) {
|
||||
@@ -481,35 +488,35 @@ const handleSendMessage = (content, resendSeq = '') => {
|
||||
return
|
||||
}
|
||||
|
||||
isShowEmojiBox.value = false
|
||||
if (inputToolBarRef.value) inputToolBarRef.value.closeWindow()
|
||||
|
||||
const msg = {
|
||||
sessionId: selectedSessionId.value,
|
||||
fromId: myAccount.value,
|
||||
msgType: selectedSession.value.sessionType,
|
||||
content: content,
|
||||
status: 'pending',
|
||||
status: msgSendStatus.PENDING,
|
||||
msgTime: new Date(),
|
||||
sendTime: new Date()
|
||||
}
|
||||
|
||||
const resendInterval = 2000 //2秒
|
||||
const callbackBefore = (seq, data) => {
|
||||
const before = async (seq, data) => {
|
||||
// 当2s内status如果还是pending中,则重发3次。如果最后还是pending,则把status置为failed
|
||||
setTimeout(() => {
|
||||
if (msg.status === 'pending') {
|
||||
if (msg.status === msgSendStatus.PENDING) {
|
||||
wsConnect.sendAgent(data)
|
||||
setTimeout(() => {
|
||||
if (msg.status === 'pending') {
|
||||
if (msg.status === msgSendStatus.PENDING) {
|
||||
wsConnect.sendAgent(data)
|
||||
setTimeout(() => {
|
||||
if (msg.status === 'pending') {
|
||||
if (msg.status === msgSendStatus.PENDING) {
|
||||
wsConnect.sendAgent(data)
|
||||
setTimeout(() => {
|
||||
if (msg.status === 'pending') {
|
||||
messageData.removeMsgRecord(selectedSessionId.value, msg.msgId)
|
||||
msg.status = 'failed'
|
||||
messageData.addMsgRecords(selectedSessionId.value, [msg])
|
||||
setTimeout(async () => {
|
||||
if (msg.status === msgSendStatus.PENDING) {
|
||||
messageData.removeMsgRecord(msg.sessionId, msg.msgId)
|
||||
msg.status = msgSendStatus.FAILED
|
||||
await messageData.addMsgRecords(msg.sessionId, [msg])
|
||||
ElMessage.error('消息发送失败')
|
||||
}
|
||||
}, resendInterval)
|
||||
@@ -521,42 +528,43 @@ const handleSendMessage = (content, resendSeq = '') => {
|
||||
}, resendInterval)
|
||||
|
||||
messageData.updateSession({
|
||||
sessionId: selectedSessionId.value,
|
||||
sessionId: msg.sessionId,
|
||||
unreadCount: 0, // 最后一条消息是自己发的,因此未读是0
|
||||
draft: '' //草稿意味着要清空
|
||||
})
|
||||
msg.seq = seq
|
||||
msg.msgId = seq //服务器没有回复DELIVERED消息之前,都用seq暂代msgId
|
||||
messageData.removeMsgRecord(selectedSessionId.value, msg.msgId)
|
||||
messageData.addMsgRecords(selectedSessionId.value, [msg])
|
||||
await messageData.addMsgRecords(msg.sessionId, [msg])
|
||||
}
|
||||
|
||||
const callbackAfter = (msgId) => {
|
||||
const after = async (msgId) => {
|
||||
messageData.updateSession({
|
||||
sessionId: selectedSessionId.value,
|
||||
sessionId: msg.sessionId,
|
||||
readMsgId: msgId, // 最后一条消息是自己发的,因此已读更新到刚发的这条消息的msgId
|
||||
readTime: new Date()
|
||||
})
|
||||
messageData.removeMsgRecord(selectedSessionId.value, msg.msgId) //移除seq为key的msg
|
||||
messageData.removeMsgRecord(msg.sessionId, msg.msgId) //移除seq为key的msg
|
||||
msg.msgId = msgId
|
||||
msg.status = 'ok'
|
||||
messageData.addMsgRecords(selectedSessionId.value, [msg]) //添加服务端返回msgId为key的msg
|
||||
if (!messageData.sessionList[selectedSessionId.value].dnd) {
|
||||
msg.status = msgSendStatus.OK
|
||||
await messageData.addMsgRecords(msg.sessionId, [msg]) //添加服务端返回msgId为key的msg
|
||||
if (!messageData.sessionList[msg.sessionId].dnd) {
|
||||
playMsgSend()
|
||||
}
|
||||
}
|
||||
|
||||
wsConnect.sendMsg(
|
||||
selectedSessionId.value,
|
||||
msg.sessionId,
|
||||
showId.value,
|
||||
selectedSession.value.sessionType,
|
||||
content,
|
||||
resendSeq,
|
||||
callbackBefore,
|
||||
callbackAfter
|
||||
before,
|
||||
after
|
||||
)
|
||||
|
||||
locateSession(selectedSessionId.value)
|
||||
capacity.value++
|
||||
msgListReachBottom()
|
||||
locateSession(msg.sessionId)
|
||||
}
|
||||
|
||||
const handleResendMessage = ({ content, seq }) => {
|
||||
@@ -584,11 +592,10 @@ const onLoadMore = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
// MessageItem最后一个msg渲染结束后的动作
|
||||
const renderFinished = () => {
|
||||
setTimeout(() => {
|
||||
const updateScroll = () => {
|
||||
if (disToBottom.value < nearBottomDis) {
|
||||
msgListReachBottom('smooth')
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,13 +603,27 @@ const renderFinished = () => {
|
||||
* @param behavior smooth 平滑的, instant 立即(默认)
|
||||
*/
|
||||
const msgListReachBottom = (behavior = 'instant') => {
|
||||
setTimeout(() => {
|
||||
msgListDiv.value?.scrollTo({
|
||||
top: msgListDiv.value.scrollHeight,
|
||||
behavior: behavior
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
msgListDiv.value.scrollTo({
|
||||
top: msgListDiv.value.scrollHeight,
|
||||
behavior: behavior
|
||||
})
|
||||
newMsgTips.value.isShowBottomTips = false
|
||||
disToBottom.value = 0
|
||||
}, 50)
|
||||
}
|
||||
|
||||
if (msgListDiv.value) {
|
||||
scrollToBottom()
|
||||
} else {
|
||||
const stopWatch = watch(msgListDiv, (newValue) => {
|
||||
if (newValue) {
|
||||
scrollToBottom()
|
||||
stopWatch() // 停止监听
|
||||
}
|
||||
})
|
||||
newMsgTips.value.isShowBottomTips = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const onReturnBottom = () => {
|
||||
@@ -960,6 +981,32 @@ const inputEditorRef = ref()
|
||||
const onSendEmoji = (key) => {
|
||||
inputEditorRef.value.addEmoji(key)
|
||||
}
|
||||
|
||||
const onSendImage = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.IMAGE, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendAudio = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.AUDIO, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendRecording = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.RECORDING, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendVideo = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.VIDEO, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendDocument = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.DOCUMENT, value: objectId }))
|
||||
}
|
||||
|
||||
const inputRecorderRef = ref(null)
|
||||
const isShowRecorder = ref(false)
|
||||
const onShowRecorder = () => {
|
||||
isShowRecorder.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1048,7 +1095,7 @@ const onSendEmoji = (key) => {
|
||||
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '多人语音' : '语音通话'"
|
||||
@click="onVoiceCall"
|
||||
>
|
||||
<Microphone />
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-button"
|
||||
@@ -1109,7 +1156,7 @@ const onSendEmoji = (key) => {
|
||||
@showUserCard="onShowUserCard"
|
||||
@showGroupCard="onShowGroupCard"
|
||||
@resendMsg="handleResendMessage"
|
||||
@renderFinished="renderFinished"
|
||||
@loadFinished="updateScroll"
|
||||
></MessageItem>
|
||||
</div>
|
||||
<el-button
|
||||
@@ -1141,51 +1188,27 @@ const onSendEmoji = (key) => {
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="input-box bdr-t" :style="{ height: inputBoxHeight + 'px' }">
|
||||
<el-container class="input-box-container">
|
||||
<el-container v-if="isShowRecorder">
|
||||
<InputRecorder
|
||||
ref="inputRecorderRef"
|
||||
:sessionId="selectedSessionId"
|
||||
@exit="isShowRecorder = false"
|
||||
@sendRecording="onSendRecording"
|
||||
></InputRecorder>
|
||||
</el-container>
|
||||
<el-container v-else class="input-box-container">
|
||||
<el-header class="input-box-header">
|
||||
<div class="tool-set">
|
||||
<div v-if="!isNotInGroup" class="left-tools">
|
||||
<InputTool tips="表情" @click="isShowEmojiBox = true">
|
||||
<template #iconSlot>
|
||||
<EmojiIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
<InputTool tips="文件" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<FolderAdd />
|
||||
</template>
|
||||
</InputTool>
|
||||
<InputTool tips="代码" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<CreditCard />
|
||||
</template>
|
||||
</InputTool>
|
||||
<InputTool tips="位置" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<LocationInformation />
|
||||
</template>
|
||||
</InputTool>
|
||||
</div>
|
||||
<div class="right-tools">
|
||||
<InputTool tips="聊天记录" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<Clock />
|
||||
</template>
|
||||
</InputTool>
|
||||
</div>
|
||||
</div>
|
||||
<DragLine
|
||||
direction="top"
|
||||
:min="inputBoxHeightMin"
|
||||
:max="inputBoxHeightMax"
|
||||
:origin-size="inputBoxHeight"
|
||||
@drag-update="onInputBoxDragUpdate"
|
||||
></DragLine>
|
||||
<EmojiBox
|
||||
:isShow="isShowEmojiBox"
|
||||
@close="isShowEmojiBox = false"
|
||||
<InputToolBar
|
||||
ref="inputToolBarRef"
|
||||
:sessionId="selectedSessionId"
|
||||
:isShowToolSet="!isNotInGroup"
|
||||
@sendEmoji="onSendEmoji"
|
||||
></EmojiBox>
|
||||
@sendImage="onSendImage"
|
||||
@sendAudio="onSendAudio"
|
||||
@sendVideo="onSendVideo"
|
||||
@sendDocument="onSendDocument"
|
||||
@showRecorder="onShowRecorder"
|
||||
></InputToolBar>
|
||||
</el-header>
|
||||
<el-main class="input-box-main">
|
||||
<div
|
||||
@@ -1211,6 +1234,13 @@ const onSendEmoji = (key) => {
|
||||
></InputEditor>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<DragLine
|
||||
direction="top"
|
||||
:min="inputBoxHeightMin"
|
||||
:max="inputBoxHeightMax"
|
||||
:origin-size="inputBoxHeight"
|
||||
@drag-update="onInputBoxDragUpdate"
|
||||
></DragLine>
|
||||
</div>
|
||||
</div>
|
||||
<MessageGroupRightSide
|
||||
@@ -1440,26 +1470,13 @@ const onSendEmoji = (key) => {
|
||||
.input-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.input-box-header {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
.tool-set {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.left-tools {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.right-tools {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-box-main {
|
||||
|
||||
62
src/views/message/components/AudioMsgBox.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import AudioFileIcon from '@/assets/svg/audiofile.svg'
|
||||
import { formatFileSize } from '@/js/utils/common'
|
||||
|
||||
const props = defineProps(['url', 'fileName', 'size'])
|
||||
const emits = defineEmits(['load'])
|
||||
|
||||
const formatSize = computed(() => {
|
||||
return formatFileSize(props.size)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emits('load') //向父组件暴露load事件
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="audio-msg-wrapper">
|
||||
<AudioFileIcon />
|
||||
<div class="main">
|
||||
<span class="file-name text-ellipsis" :title="props.fileName || '未知'">
|
||||
{{ props.fileName || '未知' }}
|
||||
</span>
|
||||
<div class="footer">
|
||||
<div class="size" :title="formatSize">{{ formatSize }}</div>
|
||||
<a :href="props.url" :download="props.fileName">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-msg-wrapper {
|
||||
padding: 4px 8px 4px 8px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.svg-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
gap: 8px;
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
src/views/message/components/DocumentMsgBox.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { formatFileSize } from '@/js/utils/common'
|
||||
import DocumentIcon from '@/assets/svg/document.svg'
|
||||
import ArchiveIcon from '@/assets/svg/archive.svg'
|
||||
import FileTemplateIcon from '@/assets/svg/filetemplate.svg'
|
||||
|
||||
const props = defineProps(['url', 'fileName', 'contentType', 'size'])
|
||||
const emits = defineEmits(['load'])
|
||||
|
||||
const iconMap = {
|
||||
'text/csv': 'CSV',
|
||||
'application/msword': 'DOC',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
|
||||
'text/html': 'HTML',
|
||||
'application/pdf': 'PDF',
|
||||
'application/vnd.ms-powerpoint': 'PPT',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
|
||||
'text/plain': 'TXT',
|
||||
'application/vnd.ms-works': 'WPS',
|
||||
'application/vnd.ms-excel': 'XLS',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
|
||||
'application/zip': ArchiveIcon,
|
||||
'application/vnd.rar': ArchiveIcon,
|
||||
'application/x-zip-compressed': ArchiveIcon,
|
||||
'application/x-7z-compressed': ArchiveIcon,
|
||||
'application/x-tar': ArchiveIcon,
|
||||
'application/gzip': ArchiveIcon,
|
||||
'application/x-bzip2': ArchiveIcon
|
||||
}
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
return iconMap[props.contentType] || DocumentIcon
|
||||
})
|
||||
|
||||
const formatSize = computed(() => {
|
||||
return formatFileSize(props.size)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emits('load') //向父组件暴露load事件
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-msg-wrapper">
|
||||
<div v-if="typeof iconComponent === 'string'" class="file-template">
|
||||
<FileTemplateIcon></FileTemplateIcon>
|
||||
<span class="extension">{{ iconComponent }}</span>
|
||||
</div>
|
||||
<component v-else :is="iconComponent" />
|
||||
<div class="main">
|
||||
<span class="file-name text-ellipsis" :title="props.fileName || '未知'">
|
||||
{{ props.fileName || '未知' }}
|
||||
</span>
|
||||
<div class="footer">
|
||||
<div class="size" :title="formatSize">{{ formatSize }}</div>
|
||||
<a :href="props.url" :download="props.fileName">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.document-msg-wrapper {
|
||||
padding: 4px 8px 4px 8px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.file-template {
|
||||
position: relative;
|
||||
|
||||
.extension {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: -2px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
gap: 8px;
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,9 +16,16 @@ const clickListener = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isShowDialog.value = false
|
||||
document.removeEventListener('click', clickListener)
|
||||
document.removeEventListener('keydown', handleEscKey)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -30,10 +37,12 @@ watch(
|
||||
tabOption.value = 'system'
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', clickListener)
|
||||
document.addEventListener('keydown', handleEscKey)
|
||||
}, 0)
|
||||
} else {
|
||||
// 父组件通过props.isShow关闭时也要注销掉listener
|
||||
document.removeEventListener('click', clickListener)
|
||||
document.removeEventListener('keydown', handleEscKey)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -63,7 +72,7 @@ const onSelectEmoji = (key) => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.emoji-box {
|
||||
width: 472px;
|
||||
width: 480px;
|
||||
height: 240px;
|
||||
padding: 0 5px 0 5px;
|
||||
position: absolute;
|
||||
|
||||
102
src/views/message/components/ImageMsgBox.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ElImage } from 'element-plus'
|
||||
import { formatFileSize } from '@/js/utils/common'
|
||||
|
||||
const props = defineProps(['url', 'imgId', 'srcList', 'initialIndex', 'fileName', 'size'])
|
||||
const emits = defineEmits(['load'])
|
||||
|
||||
const onLoad = (e) => {
|
||||
const img = e.target
|
||||
const ratio = img.naturalWidth / img.naturalHeight
|
||||
const maxRatio = 300 / 200 // 最大宽高比
|
||||
|
||||
// 如果图片尺寸在限制范围内,保持原始尺寸
|
||||
if (img.naturalWidth <= 300 && img.naturalHeight <= 200) {
|
||||
img.style.width = img.naturalWidth + 'px'
|
||||
img.style.height = img.naturalHeight + 'px'
|
||||
} else if (ratio > maxRatio) {
|
||||
// 如果图片更宽,以宽度为基准
|
||||
img.style.width = '300px'
|
||||
img.style.height = 'auto'
|
||||
} else {
|
||||
// 如果图片更高,以高度为基准
|
||||
img.style.height = '200px'
|
||||
img.style.width = 'auto'
|
||||
}
|
||||
|
||||
emits('load') //向父组件暴露load事件
|
||||
}
|
||||
|
||||
const formatSize = computed(() => {
|
||||
return formatFileSize(props.size)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-msg-wrapper">
|
||||
<el-image
|
||||
:src="props.url"
|
||||
:alt="props.imgId"
|
||||
:preview-src-list="props.srcList"
|
||||
:initial-index="props.initialIndex"
|
||||
:infinite="false"
|
||||
:lazy="false"
|
||||
fit="contain"
|
||||
@load="onLoad"
|
||||
>
|
||||
</el-image>
|
||||
<div v-if="props.fileName || props.size > 0" class="info">
|
||||
<span class="name item text-ellipsis" :title="props.fileName">
|
||||
{{ props.fileName || '' }}
|
||||
</span>
|
||||
<span class="size item text-ellipsis" :title="formatSize">
|
||||
{{ formatSize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-msg-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.el-image {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5));
|
||||
position: absolute;
|
||||
|
||||
.item {
|
||||
max-width: 40%;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.size {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { QuillEditor, Delta, Quill } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { messageStore, imageStore } from '@/stores'
|
||||
import { useMessageStore, useImageStore } from '@/stores'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { emojiTrans, getEmojiHtml } from '@/js/utils/emojis'
|
||||
import { base64ToFile } from '@/js/utils/common'
|
||||
@@ -12,8 +12,8 @@ import { el_loading_options } from '@/const/commonConst'
|
||||
|
||||
const props = defineProps(['sessionId', 'draft'])
|
||||
const emit = defineEmits(['sendMessage'])
|
||||
const messageData = messageStore()
|
||||
const imageData = imageStore()
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
|
||||
const editorRef = ref()
|
||||
|
||||
@@ -36,6 +36,18 @@ onMounted(async () => {
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
let content = await getContent()
|
||||
// 草稿若没发生变动,则不触发存储
|
||||
const draft = messageData.sessionList[props.sessionId]?.draft
|
||||
if (content && draft && content !== draft) {
|
||||
messageData.updateSession({
|
||||
sessionId: props.sessionId,
|
||||
draft: content
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (editorRef.value) {
|
||||
document.querySelector('.ql-editor').classList.remove('my-scrollbar')
|
||||
|
||||
273
src/views/message/components/InputRecorder.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { Microphone } from '@element-plus/icons-vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { useAudioStore } from '@/stores'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const props = defineProps(['sessionId'])
|
||||
const emit = defineEmits(['exit', 'sendRecording'])
|
||||
|
||||
const audioData = useAudioStore()
|
||||
const spaceDown = ref(false) // 空格键是否被按下
|
||||
const isRecord = ref(false) // 是否开始录音
|
||||
const isCancel = ref(false) // 取消发送
|
||||
const mediaRecorder = ref(null)
|
||||
const recordedChunks = ref([])
|
||||
const recordBlob = ref(null)
|
||||
const recordType = 'audio/webm;codecs=opus'
|
||||
const fileSuffix = 'webm'
|
||||
let recordStart = 0 // 录制开始时间
|
||||
let recordDuration = 0 // 录制时长
|
||||
const dynamicDuration = ref(0)
|
||||
let dynamicDurationInterval = null
|
||||
|
||||
let timer
|
||||
const handleKeyDown = async (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (isRecord.value) {
|
||||
cancelSend()
|
||||
} else {
|
||||
emit('exit')
|
||||
}
|
||||
} else if (event.key === ' ' && !spaceDown.value) {
|
||||
event.preventDefault()
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
isRecord.value = true
|
||||
spaceDown.value = true
|
||||
isCancel.value = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
if (event.key === ' ') {
|
||||
clearTimeout(timer)
|
||||
|
||||
if (spaceDown.value) {
|
||||
event.preventDefault()
|
||||
isRecord.value = false
|
||||
spaceDown.value = false
|
||||
dynamicDuration.value = 0
|
||||
clearInterval(dynamicDurationInterval)
|
||||
stopRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleExit = () => {
|
||||
emit('exit')
|
||||
}
|
||||
|
||||
const cancelSend = () => {
|
||||
isRecord.value = false
|
||||
isCancel.value = true
|
||||
dynamicDuration.value = 0
|
||||
clearInterval(dynamicDurationInterval)
|
||||
recordedChunks.value = []
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
const startRecording = async () => {
|
||||
// 检查是否有麦克风授权
|
||||
const permission = await navigator.permissions.query({ name: 'microphone' })
|
||||
const initPermissionState = permission.state
|
||||
|
||||
if (permission.state === 'denied') {
|
||||
ElMessage.warning('您拒绝授权麦克风,无法发送语音')
|
||||
return
|
||||
}
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then((mediaStream) => {
|
||||
mediaRecorder.value = new MediaRecorder(mediaStream)
|
||||
// 初次授权要弹出窗口,空格键可能已经弹起,因此先不录音直接返回
|
||||
if (initPermissionState === 'prompt') {
|
||||
// 授权时跳出授权窗口会使监听按键弹起的事件失效,状态需要手动更新
|
||||
isRecord.value = false
|
||||
spaceDown.value = false
|
||||
stopRecording()
|
||||
return
|
||||
}
|
||||
|
||||
mediaRecorder.value.onstart = () => {
|
||||
recordStart = new Date().getTime()
|
||||
dynamicDurationInterval = setInterval(() => {
|
||||
dynamicDuration.value = Math.floor((new Date().getTime() - recordStart) / 1000)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
mediaRecorder.value.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
recordedChunks.value.push(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
mediaRecorder.value.onstop = () => {
|
||||
recordDuration = new Date().getTime() - recordStart
|
||||
recordBlob.value = new Blob(recordedChunks.value, { type: recordType })
|
||||
if (!isCancel.value) {
|
||||
// 语音时长过短不予处理,单位ms
|
||||
if (recordDuration > 1000) {
|
||||
uploadRecord()
|
||||
} else {
|
||||
ElMessage.warning('语音时长过短')
|
||||
}
|
||||
}
|
||||
|
||||
recordedChunks.value = []
|
||||
recordStart = 0
|
||||
recordDuration = 0
|
||||
}
|
||||
|
||||
mediaRecorder.value.start()
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户不授权,也要把状态手动更新
|
||||
isRecord.value = false
|
||||
spaceDown.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorder.value) {
|
||||
if (mediaRecorder.value.state !== 'inactive') {
|
||||
mediaRecorder.value.stop()
|
||||
}
|
||||
const stream = mediaRecorder.value.stream
|
||||
stream.getTracks().forEach((track) => track.stop()) // 停止 MediaStream 中的所有音轨
|
||||
}
|
||||
}
|
||||
|
||||
const uploadRecord = () => {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
const fileName = `${uuidv4()}.${fileSuffix}`
|
||||
const file = new File([recordBlob.value], fileName, { type: recordType })
|
||||
mtsUploadService({ file, storeType: 1, duration: Math.floor(recordDuration / 1000) })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
audioData.setAudio(props.sessionId, res.data.data) // 缓存audio的数据
|
||||
emit('sendRecording', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatDynamicDuration = computed(() => {
|
||||
if (!dynamicDuration.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const minutes = Math.floor(dynamicDuration.value / 60)
|
||||
const seconds = Math.floor(dynamicDuration.value % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
})
|
||||
|
||||
watch(
|
||||
() => isRecord.value,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
await startRecording()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
cancelSend
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener('keyup', handleKeyUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
window.removeEventListener('keyup', handleKeyUp)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="audio-recorder">
|
||||
<div class="tips" style="height: 20px">{{ formatDynamicDuration }}</div>
|
||||
<div class="recorder-icon-wrapper">
|
||||
<Microphone class="recorder-icon" />
|
||||
<div v-show="isRecord" class="sound-wave"></div>
|
||||
</div>
|
||||
<span v-if="isRecord" class="tips">
|
||||
松开发送,按Esc键或点击
|
||||
<span @click="cancelSend" class="button-text">取消发送</span>
|
||||
</span>
|
||||
<span v-else class="tips">
|
||||
长按空格键说话,按Esc键或点击
|
||||
<span @click="handleExit" class="button-text">退出</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-recorder {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.recorder-icon-wrapper {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 16px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, #90c0f3 30%, #409eff 70%);
|
||||
box-shadow: 0 0 10px rgba(64, 158, 255, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.recorder-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sound-wave {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(64, 158, 255, 0.5);
|
||||
animation: soundVibration 0.5s infinite alternate;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
|
||||
.button-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes soundVibration {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,7 @@ const props = defineProps(['tips'])
|
||||
.tool-icon-wrapper {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
209
src/views/message/components/InputToolBar.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Clock, Microphone } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import EmojiIcon from '@/assets/svg/emoji.svg'
|
||||
import FileIcon from '@/assets/svg/file.svg'
|
||||
import ImageIcon from '@/assets/svg/image.svg'
|
||||
import CodeIcon from '@/assets/svg/code.svg'
|
||||
import VoteIcon from '@/assets/svg/vote.svg'
|
||||
import EmojiBox from './EmojiBox.vue'
|
||||
import InputTool from '@/views/message/components/InputTool.vue'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import {
|
||||
useMessageStore,
|
||||
useImageStore,
|
||||
useAudioStore,
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const props = defineProps(['sessionId', 'isShowToolSet'])
|
||||
const emit = defineEmits([
|
||||
'sendEmoji',
|
||||
'sendImage',
|
||||
'sendAudio',
|
||||
'sendVideo',
|
||||
'sendDocument',
|
||||
'showRecorder'
|
||||
])
|
||||
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
const isShowEmojiBox = ref(false)
|
||||
|
||||
const onSelectedFile = (file) => {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
if (file.raw.type && file.raw.type.startsWith('image/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
imageData.setImage(props.sessionId, res.data.data) // 缓存image数据
|
||||
emit('sendImage', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else if (file.raw.type && file.raw.type.startsWith('audio/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
audioData.setAudio(props.sessionId, res.data.data) // 缓存audio的数据
|
||||
emit('sendAudio', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else if (file.raw.type && file.raw.type.startsWith('video/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
videoData.setVideo(props.sessionId, res.data.data) // 缓存video的数据
|
||||
emit('sendVideo', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
documentData.setDocument(props.sessionId, res.data.data) // 缓存video的数据
|
||||
emit('sendDocument', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onSendEmoji = (key) => {
|
||||
emit('sendEmoji', key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关掉bar上弹出的窗口
|
||||
*/
|
||||
const closeWindow = () => {
|
||||
isShowEmojiBox.value = false
|
||||
}
|
||||
|
||||
const showRecorder = () => {
|
||||
emit('showRecorder')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
closeWindow
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-set">
|
||||
<div v-if="props.isShowToolSet" class="left-tools">
|
||||
<InputTool tips="表情" @click="isShowEmojiBox = true">
|
||||
<template #iconSlot>
|
||||
<EmojiIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="onSelectedFile"
|
||||
>
|
||||
<template #trigger>
|
||||
<InputTool tips="图片">
|
||||
<template #iconSlot>
|
||||
<ImageIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-upload :auto-upload="false" :show-file-list="false" :on-change="onSelectedFile">
|
||||
<template #trigger>
|
||||
<InputTool tips="文件">
|
||||
<template #iconSlot>
|
||||
<FileIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
</template>
|
||||
</el-upload>
|
||||
<InputTool tips="语音消息" @click="showRecorder">
|
||||
<template #iconSlot>
|
||||
<Microphone />
|
||||
</template>
|
||||
</InputTool>
|
||||
<InputTool tips="代码" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<CodeIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
<!-- <InputTool tips="位置" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<LocationInformation />
|
||||
</template>
|
||||
</InputTool> -->
|
||||
<InputTool
|
||||
v-if="messageData.sessionList[props.sessionId].sessionType === MsgType.GROUP_CHAT"
|
||||
tips="群投票"
|
||||
@click="ElMessage.warning('功能开发中')"
|
||||
>
|
||||
<template #iconSlot>
|
||||
<VoteIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
</div>
|
||||
<div class="right-tools">
|
||||
<InputTool tips="聊天记录" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<Clock />
|
||||
</template>
|
||||
</InputTool>
|
||||
</div>
|
||||
</div>
|
||||
<EmojiBox
|
||||
:isShow="isShowEmojiBox"
|
||||
@close="isShowEmojiBox = false"
|
||||
@sendEmoji="onSendEmoji"
|
||||
></EmojiBox>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tool-set {
|
||||
height: 42px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.left-tools {
|
||||
display: flex;
|
||||
|
||||
// 调整文件按钮选中之后的颜色,默认是rgb(64, 158, 255)
|
||||
:deep(.el-upload) {
|
||||
color: #000;
|
||||
fill: #000;
|
||||
outline-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.right-tools {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, markRaw } from 'vue'
|
||||
import { ChatDotRound, Tickets, Microphone, VideoCamera, Mute } from '@element-plus/icons-vue'
|
||||
import {
|
||||
ChatDotRound,
|
||||
Tickets,
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
Mute,
|
||||
Phone
|
||||
} from '@element-plus/icons-vue'
|
||||
import AtIcon from '@/assets/svg/at.svg'
|
||||
import adminIcon from '@/assets/svg/administrator.svg'
|
||||
import DeleteIcon from '@/assets/svg/delete.svg'
|
||||
import TransferIcon from '@/assets/svg/transfer.svg'
|
||||
import { userStore, groupStore } from '@/stores'
|
||||
import { useUserStore, useGroupStore } from '@/stores'
|
||||
|
||||
const props = defineProps(['groupId', 'account'])
|
||||
const emit = defineEmits(['selectMenu'])
|
||||
|
||||
const userData = userStore()
|
||||
const groupData = groupStore()
|
||||
const userData = useUserStore()
|
||||
const groupData = useGroupStore()
|
||||
|
||||
const myAccount = computed(() => userData.user.account)
|
||||
|
||||
@@ -63,7 +70,7 @@ const lable_atTa = ref({
|
||||
const lable_voiceCall = ref({
|
||||
label: 'voiceCall',
|
||||
desc: '语音通话',
|
||||
icon: markRaw(Microphone)
|
||||
icon: markRaw(Phone)
|
||||
})
|
||||
const lable_videoCall = ref({
|
||||
label: 'videoCall',
|
||||
|
||||
@@ -2,18 +2,24 @@
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { Edit, Search } from '@element-plus/icons-vue'
|
||||
import { userStore, settingStore, messageStore, groupStore, groupCardStore } from '@/stores'
|
||||
import {
|
||||
useUserStore,
|
||||
useSettingStore,
|
||||
useMessageStore,
|
||||
useGroupStore,
|
||||
useGroupCardStore
|
||||
} from '@/stores'
|
||||
import DragLine from '@/components/common/DragLine.vue'
|
||||
import GroupMembersTable from '@/components/common/GroupMembersTable.vue'
|
||||
|
||||
const props = defineProps(['sessionId'])
|
||||
const emit = defineEmits(['showGroupCard', 'openSession'])
|
||||
|
||||
const userData = userStore()
|
||||
const settingData = settingStore()
|
||||
const messageData = messageStore()
|
||||
const groupData = groupStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const userData = useUserStore()
|
||||
const settingData = useSettingStore()
|
||||
const messageData = useMessageStore()
|
||||
const groupData = useGroupStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
|
||||
const msgGroupRightSideWidth = ref(0)
|
||||
const announcementInSideHeight = ref(0)
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, h, createApp, watch, nextTick, reactive } from 'vue'
|
||||
import { ElImage } from 'element-plus'
|
||||
import { WarningFilled } from '@element-plus/icons-vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { userStore, messageStore, groupStore, groupCardStore, imageStore } from '@/stores'
|
||||
import {
|
||||
useUserStore,
|
||||
useMessageStore,
|
||||
useGroupStore,
|
||||
useGroupCardStore,
|
||||
useImageStore,
|
||||
useAudioStore,
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import { messageSysShowTime, showTimeFormat, jsonParseSafe } from '@/js/utils/common'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { emojis } from '@/js/utils/emojis'
|
||||
import { msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||
import RecordingMsgBox from '@/views/message/components/RecordingMsgBox.vue'
|
||||
import ImageMsgBox from '@/views/message/components/ImageMsgBox.vue'
|
||||
import AudioMsgBox from '@/views/message/components/AudioMsgBox.vue'
|
||||
import VideoMsgBox from '@/views/message/components/VideoMsgBox.vue'
|
||||
import DocumentMsgBox from '@/views/message/components/DocumentMsgBox.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'sessionId',
|
||||
@@ -20,26 +34,19 @@ const props = defineProps([
|
||||
'hasNoMoreMsg',
|
||||
'isLoadMoreLoading'
|
||||
])
|
||||
const emit = defineEmits([
|
||||
'loadMore',
|
||||
'showUserCard',
|
||||
'showGroupCard',
|
||||
'resendMsg',
|
||||
'renderFinished'
|
||||
])
|
||||
const emit = defineEmits(['loadMore', 'showUserCard', 'showGroupCard', 'resendMsg', 'loadFinished'])
|
||||
|
||||
const userData = userStore()
|
||||
const messageData = messageStore()
|
||||
const groupData = groupStore()
|
||||
const groupCardData = groupCardStore()
|
||||
const imageData = imageStore()
|
||||
const userData = useUserStore()
|
||||
const messageData = useMessageStore()
|
||||
const groupData = useGroupStore()
|
||||
const groupCardData = useGroupCardStore()
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
|
||||
onMounted(() => {
|
||||
rendering().then(() => {
|
||||
if (props.lastMsgId === props.msgId) {
|
||||
emit('renderFinished')
|
||||
}
|
||||
})
|
||||
rendering()
|
||||
})
|
||||
|
||||
let app = null
|
||||
@@ -57,8 +64,49 @@ const rendering = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态渲染消息内容
|
||||
* @param content 消息内容
|
||||
*/
|
||||
const renderComponent = async (content) => {
|
||||
await imageData.loadImageInfoFromContent(props.sessionId, content)
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (!contentJson) {
|
||||
return await renderMix(content)
|
||||
}
|
||||
|
||||
const type = contentJson['type']
|
||||
const value = contentJson['value']
|
||||
if (!type || !value) {
|
||||
return await renderMix(content)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case msgContentType.MIX:
|
||||
return await renderMix(value)
|
||||
case msgContentType.TEXT:
|
||||
return renderText(value)
|
||||
case msgContentType.RECORDING:
|
||||
return renderRecording(value)
|
||||
case msgContentType.AUDIO:
|
||||
return renderAudio(value)
|
||||
case msgContentType.IMAGE:
|
||||
return renderImage(value)
|
||||
case msgContentType.EMOJI:
|
||||
return renderEmoji(value)
|
||||
case msgContentType.VIDEO:
|
||||
return renderVideo(value)
|
||||
case msgContentType.DOCUMENT:
|
||||
return renderDocument(value)
|
||||
default:
|
||||
return h('div', [])
|
||||
}
|
||||
}
|
||||
|
||||
const renderText = (content) => {
|
||||
return h('span', content)
|
||||
}
|
||||
|
||||
const renderMix = async (content) => {
|
||||
if (!content) return h('div', [])
|
||||
let contentArray = []
|
||||
//匹配内容中的图片
|
||||
@@ -71,42 +119,125 @@ const renderComponent = async (content) => {
|
||||
})
|
||||
})
|
||||
|
||||
const elements = contentArray.map((item) => {
|
||||
return contentArray.map((item) => {
|
||||
if (item.startsWith('{') && item.endsWith('}')) {
|
||||
const imgId = item.slice(1, -1)
|
||||
const url = imageData.image[imgId]?.originUrl
|
||||
if (url) {
|
||||
const imgIdList = imageData.imageInSession[props.sessionId].sort((a, b) => a - b)
|
||||
const srcList = imgIdList.map((item) => imageData.image[item].originUrl)
|
||||
return h(ElImage, {
|
||||
src: url,
|
||||
alt: `{${imgId}}`,
|
||||
fit: 'cover',
|
||||
previewSrcList: srcList,
|
||||
initialIndex: imgIdList.indexOf(imgId),
|
||||
infinite: false
|
||||
})
|
||||
} else {
|
||||
return h('span', item)
|
||||
}
|
||||
return renderImage(item.slice(1, -1), false)
|
||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
||||
const emojiId = `[${item.slice(1, -1)}]`
|
||||
const url = emojis[emojiId]
|
||||
if (url) {
|
||||
return h('img', {
|
||||
class: 'emoji',
|
||||
src: url,
|
||||
alt: emojiId,
|
||||
title: item.slice(1, -1)
|
||||
})
|
||||
} else {
|
||||
return h('span', item)
|
||||
}
|
||||
return renderEmoji(item.slice(1, -1))
|
||||
} else {
|
||||
return h('span', item)
|
||||
}
|
||||
})
|
||||
return elements
|
||||
}
|
||||
|
||||
const renderEmoji = (content) => {
|
||||
const emojiId = `[${content}]`
|
||||
const url = emojis[emojiId]
|
||||
if (url) {
|
||||
return h('img', {
|
||||
class: 'emoji',
|
||||
src: url,
|
||||
alt: emojiId,
|
||||
title: content,
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderVideo = (content) => {
|
||||
const videoId = content
|
||||
const url = videoData.video[videoId]?.url
|
||||
if (url) {
|
||||
return h(VideoMsgBox, {
|
||||
videoId,
|
||||
url,
|
||||
fileName: videoData.video[videoId].fileName,
|
||||
size: videoData.video[videoId].size,
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderImage = (content, ishowInfo = true) => {
|
||||
const imgId = content
|
||||
const url = imageData.image[imgId]?.thumbUrl
|
||||
if (url) {
|
||||
const imgIdList = imageData.imageInSession[props.sessionId].sort((a, b) => a - b)
|
||||
const srcList = imgIdList.map((item) => imageData.image[item].originUrl)
|
||||
return h(ImageMsgBox, {
|
||||
url,
|
||||
imgId,
|
||||
srcList,
|
||||
initialIndex: imgIdList.indexOf(imgId),
|
||||
fileName: ishowInfo ? imageData.image[imgId].fileName : '',
|
||||
size: ishowInfo ? imageData.image[imgId].size : '',
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderRecording = (content) => {
|
||||
const audioId = content
|
||||
const url = audioData.audio[audioId]?.url
|
||||
const duration = audioData.audio[audioId]?.duration
|
||||
if (url) {
|
||||
return h(RecordingMsgBox, {
|
||||
audioUrl: import.meta.env.VITE_OSS_CORS_FLAG + url,
|
||||
duration: duration,
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderAudio = (content) => {
|
||||
const audioId = content
|
||||
const url = audioData.audio[audioId]?.url
|
||||
if (url) {
|
||||
return h(AudioMsgBox, {
|
||||
url: import.meta.env.VITE_OSS_CORS_FLAG + url,
|
||||
fileName: audioData.audio[audioId].fileName,
|
||||
size: audioData.audio[audioId].size,
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderDocument = (content) => {
|
||||
const documentId = content
|
||||
const url = documentData.document[documentId]?.url
|
||||
if (url) {
|
||||
return h(DocumentMsgBox, {
|
||||
url: import.meta.env.VITE_OSS_CORS_FLAG + url,
|
||||
fileName: documentData.document[documentId].fileName,
|
||||
contentType: documentData.document[documentId].documentType,
|
||||
size: documentData.document[documentId].size,
|
||||
onLoad: () => {
|
||||
emit('loadFinished')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return h('span', `[${content}]`)
|
||||
}
|
||||
}
|
||||
|
||||
const msg = computed(() => {
|
||||
@@ -114,7 +245,7 @@ const msg = computed(() => {
|
||||
})
|
||||
|
||||
const msgStatus = computed(() => {
|
||||
return msg.value.status || 'ok'
|
||||
return msg.value.status || msgSendStatus.OK
|
||||
})
|
||||
|
||||
const isSystemMsg = computed(() => {
|
||||
@@ -355,16 +486,28 @@ const systemMsgContent = computed(() => {
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateAvatar(content)}</div>`
|
||||
case MsgType.SYS_GROUP_SET_ADMIN:
|
||||
case MsgType.SYS_GROUP_CANCEL_ADMIN:
|
||||
return `<div style="text-align: center;">${getSysGroupChangeRoleMsgTips(msg.value.msgType, content)}</div>`
|
||||
return `<div style="text-align: center;">${getSysGroupChangeRoleMsgTips(
|
||||
msg.value.msgType,
|
||||
content
|
||||
)}</div>`
|
||||
case MsgType.SYS_GROUP_SET_ALL_MUTED:
|
||||
case MsgType.SYS_GROUP_CANCEL_ALL_MUTED:
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateAllMuted(msg.value.msgType, content)}</div>`
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateAllMuted(
|
||||
msg.value.msgType,
|
||||
content
|
||||
)}</div>`
|
||||
case MsgType.SYS_GROUP_SET_JOIN_APPROVAL:
|
||||
case MsgType.SYS_GROUP_CANCEL_JOIN_APPROVAL:
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateJoinApproval(msg.value.msgType, content)}</div>`
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateJoinApproval(
|
||||
msg.value.msgType,
|
||||
content
|
||||
)}</div>`
|
||||
case MsgType.SYS_GROUP_SET_HISTORY_BROWSE:
|
||||
case MsgType.SYS_GROUP_CANCEL_HISTORY_BROWSE:
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateHistoryBrowse(msg.value.msgType, content)}</div>`
|
||||
return `<div style="text-align: center;">${getSysGroupUpdateHistoryBrowse(
|
||||
msg.value.msgType,
|
||||
content
|
||||
)}</div>`
|
||||
case MsgType.SYS_GROUP_OWNER_TRANSFER:
|
||||
return `<div style="text-align: center;">${getSysGroupOwnerTransfer(content)}</div>`
|
||||
case MsgType.SYS_GROUP_UPDATE_MEMBER_MUTED:
|
||||
@@ -403,7 +546,7 @@ const myMsgIsRead = computed(() => {
|
||||
})
|
||||
|
||||
const isShowLoadMore = computed(() => {
|
||||
// 这里用弱等于“==”,左边是数字,右边是string
|
||||
// 这里用弱等于"==",左边是数字,右边是string
|
||||
if (msg.value.msgId == props.firstMsgId && !props.hasNoMoreMsg) {
|
||||
return true
|
||||
} else {
|
||||
@@ -411,7 +554,7 @@ const isShowLoadMore = computed(() => {
|
||||
}
|
||||
})
|
||||
const isShowNoMoreMsg = computed(() => {
|
||||
// 这里用弱等于“==”,左边是数字,右边是string
|
||||
// 这里用弱等于"==",左边是数字,右边是string
|
||||
if (msg.value.msgId == props.firstMsgId && props.hasNoMoreMsg) {
|
||||
return true
|
||||
} else {
|
||||
@@ -548,13 +691,13 @@ watch(
|
||||
</el-header>
|
||||
<el-main class="message-content">
|
||||
<div
|
||||
v-if="msgStatus === 'pending'"
|
||||
v-if="msgStatus === msgSendStatus.PENDING"
|
||||
class="my-message-status my-message-status-pending"
|
||||
>
|
||||
<div class="loading-circular" v-loading="true"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="msgStatus === 'failed'"
|
||||
v-else-if="msgStatus === msgSendStatus.FAILED"
|
||||
class="my-message-status my-message-status-failed"
|
||||
>
|
||||
<el-icon color="red" title="点击重发" @click="onResendMsg"
|
||||
@@ -565,7 +708,7 @@ watch(
|
||||
<div v-if="myMsgIsRead" class="remote_read"></div>
|
||||
<div v-else class="remote_unread"></div>
|
||||
</div>
|
||||
<div class="div-content" :id="`div-content-${msg.msgId}`"></div>
|
||||
<div class="div-content" :id="`div-content-${msg.msgId}`">内容加载中...</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-main>
|
||||
@@ -597,7 +740,7 @@ watch(
|
||||
<span>{{ msgTime }}</span>
|
||||
</el-header>
|
||||
<el-main class="message-content">
|
||||
<div class="div-content" :id="`div-content-${msg.msgId}`"></div>
|
||||
<div class="div-content" :id="`div-content-${msg.msgId}`">内容加载中...</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-main>
|
||||
|
||||
139
src/views/message/components/RecordingMsgBox.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElIcon, ElMessage } from 'element-plus'
|
||||
import PlayIcon from '@/assets/svg/play.svg'
|
||||
import PauseIcon from '@/assets/svg/pause.svg'
|
||||
import { AVWaveform } from 'vue-audio-visual'
|
||||
|
||||
const props = defineProps(['audioUrl', 'duration'])
|
||||
const emits = defineEmits(['load'])
|
||||
|
||||
const waveformRef = ref(null)
|
||||
const isPlaying = ref(false)
|
||||
const audioDuration = ref(null)
|
||||
|
||||
// 格式化时间
|
||||
const formatDuration = computed(() => {
|
||||
if (!audioDuration.value) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
const minutes = Math.floor(audioDuration.value / 60)
|
||||
const seconds = Math.floor(audioDuration.value % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
})
|
||||
|
||||
const playAudio = async () => {
|
||||
const audioPlayer = waveformRef.value.querySelector('audio')
|
||||
if (audioPlayer) {
|
||||
try {
|
||||
await audioPlayer.play()
|
||||
} catch (error) {
|
||||
ElMessage.error('音频播放遇到问题')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pauseAudio = async () => {
|
||||
const audioPlayer = waveformRef.value.querySelector('audio')
|
||||
if (audioPlayer) {
|
||||
try {
|
||||
await audioPlayer.pause()
|
||||
} catch (error) {
|
||||
ElMessage.error('音频暂停遇到问题')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放/暂停音频
|
||||
const togglePlay = async () => {
|
||||
if (isPlaying.value) {
|
||||
pauseAudio()
|
||||
} else {
|
||||
playAudio()
|
||||
}
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const audioPlayer = waveformRef.value.querySelector('audio')
|
||||
if (audioPlayer) {
|
||||
// 监听播放事件
|
||||
audioPlayer.addEventListener('play', () => {
|
||||
isPlaying.value = true
|
||||
})
|
||||
|
||||
// 监听暂停事件
|
||||
audioPlayer.addEventListener('pause', () => {
|
||||
isPlaying.value = false
|
||||
})
|
||||
|
||||
// 监听音频元数据加载完成事件
|
||||
audioPlayer.addEventListener('loadedmetadata', () => {
|
||||
// 媒体文件中,语音消息没有duration信息,所以要从服务端返回的字段获取
|
||||
if (audioPlayer.duration !== Infinity) {
|
||||
audioDuration.value = audioPlayer.duration
|
||||
} else {
|
||||
audioDuration.value = props.duration
|
||||
}
|
||||
})
|
||||
}
|
||||
emits('load') //触发load事件
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="waveformRef" class="audio-player">
|
||||
<div class="play-button" @click="togglePlay">
|
||||
<el-icon v-if="isPlaying"><PauseIcon /></el-icon>
|
||||
<el-icon v-else><PlayIcon /></el-icon>
|
||||
</div>
|
||||
|
||||
<AVWaveform
|
||||
:src="props.audioUrl"
|
||||
:audio-controls="false"
|
||||
:playtime="false"
|
||||
:canv-width="120"
|
||||
:canv-height="40"
|
||||
:playtime-slider-color="`#409eff`"
|
||||
></AVWaveform>
|
||||
|
||||
<span class="time">{{ formatDuration }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(canvas) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #c6e2ff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,10 +6,11 @@ import SessionTag from './SessionTag.vue'
|
||||
import { jsonParseSafe, sessionShowTime } from '@/js/utils/common'
|
||||
import { Top, MuteNotification } from '@element-plus/icons-vue'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { userStore, messageStore, groupStore } from '@/stores'
|
||||
import { useUserStore, useMessageStore, useGroupStore } from '@/stores'
|
||||
import { msgChatCloseSessionService } from '@/api/message'
|
||||
import router from '@/router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||
|
||||
const props = defineProps([
|
||||
'sessionId',
|
||||
@@ -25,9 +26,9 @@ const emit = defineEmits([
|
||||
'noneSelected',
|
||||
'showUpdateMarkDialog'
|
||||
])
|
||||
const messageData = messageStore()
|
||||
const groupData = groupStore()
|
||||
const userData = userStore()
|
||||
const messageData = useMessageStore()
|
||||
const groupData = useGroupStore()
|
||||
const userData = useUserStore()
|
||||
const myAccount = computed(() => userData.user.account)
|
||||
const sessionInfo = computed(() => {
|
||||
return messageData.sessionList[props.sessionId]
|
||||
@@ -216,55 +217,78 @@ const getGroupChatMsgTips = (content) => {
|
||||
|
||||
const showDetailContent = computed(() => {
|
||||
if (isShowDraft.value) {
|
||||
return sessionInfo.value.draft
|
||||
return sessionInfo.value.draft?.replace(/\{\d+\}/g, '[图片]') // 把内容中的`{xxxxxx}`格式的图片统一转成`[图片]`
|
||||
} else {
|
||||
const replaceContent = lastMsg.value.content?.replace(/\{\d+\}/g, '{图片}') // 把内容中的`{xxxxxx}`格式的图片统一转成`{图片}`
|
||||
if (replaceContent) {
|
||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
||||
const content = jsonParseSafe(replaceContent)
|
||||
switch (lastMsg.value.msgType) {
|
||||
case MsgType.SYS_GROUP_CREATE:
|
||||
return getSysGroupCreateMsgTips(content)
|
||||
case MsgType.SYS_GROUP_ADD_MEMBER:
|
||||
return getSysGroupAddMemberMsgTips(content)
|
||||
case MsgType.SYS_GROUP_DEL_MEMBER:
|
||||
return getSysGroupDelMemberMsgTips(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_ANNOUNCEMENT:
|
||||
return getSysGroupUpdateAnnouncement(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_NAME:
|
||||
return getSysGroupUpdateName(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_AVATAR:
|
||||
return getSysGroupUpdateAvatar(content)
|
||||
case MsgType.SYS_GROUP_SET_ADMIN:
|
||||
case MsgType.SYS_GROUP_CANCEL_ADMIN:
|
||||
return getSysGroupChangeRoleMsgTips(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_ALL_MUTED:
|
||||
case MsgType.SYS_GROUP_CANCEL_ALL_MUTED:
|
||||
return getSysGroupUpdateAllMuted(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_JOIN_APPROVAL:
|
||||
case MsgType.SYS_GROUP_CANCEL_JOIN_APPROVAL:
|
||||
return getSysGroupUpdateJoinApproval(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_HISTORY_BROWSE:
|
||||
case MsgType.SYS_GROUP_CANCEL_HISTORY_BROWSE:
|
||||
return getSysGroupUpdateHistoryBrowse(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_OWNER_TRANSFER:
|
||||
return getSysGroupOwnerTransfer(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_MEMBER_MUTED:
|
||||
return getSysGroupUpdateMemberMuted(content)
|
||||
case MsgType.SYS_GROUP_LEAVE:
|
||||
return getSysGroupLeave(content)
|
||||
case MsgType.SYS_GROUP_DROP:
|
||||
return getSysGroupDrop(content)
|
||||
case MsgType.GROUP_CHAT:
|
||||
return getGroupChatMsgTips(replaceContent)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
if (!lastMsg.value.content) {
|
||||
return '...'
|
||||
}
|
||||
|
||||
const jsonContent = jsonParseSafe(lastMsg.value.content)
|
||||
let template
|
||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
||||
if (jsonContent['type'] == msgContentType.IMAGE) {
|
||||
template = '[图片]'
|
||||
} else if (jsonContent['type'] == msgContentType.AUDIO) {
|
||||
template = '[音频]'
|
||||
} else if (jsonContent['type'] == msgContentType.RECORDING) {
|
||||
template = '[语音]'
|
||||
} else if (jsonContent['type'] == msgContentType.VIDEO) {
|
||||
template = '[视频]'
|
||||
} else if (jsonContent['type'] == msgContentType.DOCUMENT) {
|
||||
template = '[文件]'
|
||||
} else {
|
||||
return replaceContent
|
||||
template = jsonContent['value']
|
||||
}
|
||||
|
||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
||||
return getGroupChatMsgTips(template)
|
||||
} else {
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
||||
const content = jsonParseSafe(lastMsg.value.content)
|
||||
switch (lastMsg.value.msgType) {
|
||||
case MsgType.SYS_GROUP_CREATE:
|
||||
return getSysGroupCreateMsgTips(content)
|
||||
case MsgType.SYS_GROUP_ADD_MEMBER:
|
||||
return getSysGroupAddMemberMsgTips(content)
|
||||
case MsgType.SYS_GROUP_DEL_MEMBER:
|
||||
return getSysGroupDelMemberMsgTips(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_ANNOUNCEMENT:
|
||||
return getSysGroupUpdateAnnouncement(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_NAME:
|
||||
return getSysGroupUpdateName(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_AVATAR:
|
||||
return getSysGroupUpdateAvatar(content)
|
||||
case MsgType.SYS_GROUP_SET_ADMIN:
|
||||
case MsgType.SYS_GROUP_CANCEL_ADMIN:
|
||||
return getSysGroupChangeRoleMsgTips(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_ALL_MUTED:
|
||||
case MsgType.SYS_GROUP_CANCEL_ALL_MUTED:
|
||||
return getSysGroupUpdateAllMuted(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_JOIN_APPROVAL:
|
||||
case MsgType.SYS_GROUP_CANCEL_JOIN_APPROVAL:
|
||||
return getSysGroupUpdateJoinApproval(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_SET_HISTORY_BROWSE:
|
||||
case MsgType.SYS_GROUP_CANCEL_HISTORY_BROWSE:
|
||||
return getSysGroupUpdateHistoryBrowse(lastMsg.value.msgType, content)
|
||||
case MsgType.SYS_GROUP_OWNER_TRANSFER:
|
||||
return getSysGroupOwnerTransfer(content)
|
||||
case MsgType.SYS_GROUP_UPDATE_MEMBER_MUTED:
|
||||
return getSysGroupUpdateMemberMuted(content)
|
||||
case MsgType.SYS_GROUP_LEAVE:
|
||||
return getSysGroupLeave(content)
|
||||
case MsgType.SYS_GROUP_DROP:
|
||||
return getSysGroupDrop(content)
|
||||
case MsgType.GROUP_CHAT:
|
||||
return getGroupChatMsgTips(lastMsg.value.content.replace(/\{\d+\}/g, '[图片]'))
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
return '...'
|
||||
return lastMsg.value.content.replace(/\{\d+\}/g, '[图片]')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -278,6 +302,7 @@ const isShowUnread = computed(() => {
|
||||
sessionInfo.value.sessionType === MsgType.CHAT &&
|
||||
!isShowDraft.value &&
|
||||
lastMsg.value?.fromId === myAccount.value &&
|
||||
(lastMsg.value.status === undefined || lastMsg.value.status === msgSendStatus.OK) &&
|
||||
+sessionInfo.value?.remoteRead < +lastMsgId.value
|
||||
) {
|
||||
return true
|
||||
@@ -291,6 +316,7 @@ const isShowRead = computed(() => {
|
||||
sessionInfo.value.sessionType === MsgType.CHAT &&
|
||||
!isShowDraft.value &&
|
||||
lastMsg.value?.fromId === myAccount.value &&
|
||||
(lastMsg.value.status === undefined || lastMsg.value.status === msgSendStatus.OK) &&
|
||||
+sessionInfo.value?.remoteRead === +lastMsgId.value
|
||||
) {
|
||||
return true
|
||||
@@ -299,6 +325,19 @@ const isShowRead = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isShowUnSend = computed(() => {
|
||||
if (
|
||||
sessionInfo.value.sessionType === MsgType.CHAT &&
|
||||
!isShowDraft.value &&
|
||||
lastMsg.value?.fromId === myAccount.value &&
|
||||
lastMsg.value?.status === msgSendStatus.FAILED
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const isShowUnreadCount = computed(() => {
|
||||
return sessionInfo.value.unreadCount > 0
|
||||
})
|
||||
@@ -429,8 +468,9 @@ defineExpose({
|
||||
>[{{ sessionInfo.unreadCount > 99 ? '99+' : sessionInfo.unreadCount }}条]</span
|
||||
>
|
||||
<span v-if="isShowDraft" class="draft">[草稿]</span>
|
||||
<span v-if="isShowUnread" class="unread-or-read">[未读]</span>
|
||||
<span v-if="isShowRead" class="unread-or-read">[已读]</span>
|
||||
<span v-else-if="isShowUnread" class="unread-or-read">[未读]</span>
|
||||
<span v-else-if="isShowRead" class="unread-or-read">[已读]</span>
|
||||
<span v-else-if="isShowUnSend" class="unread-or-read">[未送达]</span>
|
||||
<span class="detail text-ellipsis"> {{ showDetailContent }}</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { Top, Bottom, MuteNotification, Bell, CircleClose, Edit } from '@element-plus/icons-vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
const props = defineProps(['sessionId'])
|
||||
const emit = defineEmits(['selectMenu', 'closeMenu'])
|
||||
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
const top = computed(() => {
|
||||
if (props.sessionId) {
|
||||
|
||||
112
src/views/message/components/VideoMsgBox.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import Player from 'xgplayer'
|
||||
import 'xgplayer/dist/index.min.css'
|
||||
import { formatFileSize } from '@/js/utils/common'
|
||||
|
||||
const props = defineProps(['videoId', 'url', 'fileName', 'size'])
|
||||
const emits = defineEmits(['load'])
|
||||
|
||||
const isLoaded = ref(false)
|
||||
const isClickPlay = ref(false)
|
||||
const videoWrapperRef = ref(null)
|
||||
|
||||
const formatSize = computed(() => {
|
||||
return formatFileSize(props.size)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const player = new Player({
|
||||
id: `msg-xgplayer-${props.videoId}`,
|
||||
url: props.url,
|
||||
fluid: true,
|
||||
autoplay: false,
|
||||
lang: 'zh-cn',
|
||||
download: true,
|
||||
keyShortcut: false
|
||||
})
|
||||
|
||||
// 监听播放开始事件
|
||||
player.on('play', () => {
|
||||
isClickPlay.value = true
|
||||
})
|
||||
|
||||
// 监听播放ready事件
|
||||
player.on('ready', () => {
|
||||
// 监听视频元数据加载完成事件
|
||||
const videoElement = player.root.querySelector('video')
|
||||
videoElement.addEventListener('loadedmetadata', () => {
|
||||
const videoWidth = videoElement.videoWidth
|
||||
const videoHeight = videoElement.videoHeight
|
||||
const maxWidth = 480
|
||||
const maxHeight = 270
|
||||
let newWidth = videoWidth
|
||||
let newHeight = videoHeight
|
||||
|
||||
// 判断是横屏还是竖屏
|
||||
if (videoWidth > videoHeight) {
|
||||
// 横屏视频,宽度固定为 480,高度按比例计算
|
||||
newWidth = maxWidth
|
||||
newHeight = Math.floor((maxWidth / videoWidth) * videoHeight)
|
||||
} else {
|
||||
// 竖屏视频,高度固定为 270,宽度按比例计算
|
||||
newHeight = maxHeight
|
||||
newWidth = Math.floor((maxHeight / videoHeight) * videoWidth)
|
||||
}
|
||||
|
||||
videoWrapperRef.value.style.width = `${newWidth}px`
|
||||
videoWrapperRef.value.style.height = `${newHeight}px`
|
||||
videoWrapperRef.value.style.padding = 0
|
||||
|
||||
isLoaded.value = true
|
||||
emits('load') //向父组件暴露load事件
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="video-msg-wrapper">
|
||||
<div v-show="isLoaded" ref="videoWrapperRef" :id="`msg-xgplayer-${props.videoId}`"></div>
|
||||
<div v-if="!isClickPlay && (props.fileName || props.size > 0)" class="info">
|
||||
<span class="name item text-ellipsis" :title="props.fileName">
|
||||
{{ props.fileName || '' }}
|
||||
</span>
|
||||
<span class="size item text-ellipsis" :title="formatSize">
|
||||
{{ formatSize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.video-msg-wrapper {
|
||||
position: relative;
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5));
|
||||
position: absolute;
|
||||
|
||||
.item {
|
||||
max-width: 40%;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.size {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { User, Key, Bell } from '@element-plus/icons-vue'
|
||||
import { messageStore } from '@/stores'
|
||||
import { useMessageStore } from '@/stores'
|
||||
|
||||
const messageData = messageStore()
|
||||
const messageData = useMessageStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await messageData.loadSessionList()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { userModifySelfService } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const form = ref()
|
||||
const formModel = ref({
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import { ref } from 'vue'
|
||||
import { userModifyPassword } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const form = ref()
|
||||
const formModel = ref({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { userModifySelfService } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const form = ref()
|
||||
const formModel = ref({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { userModifySelfService } from '@/api/user'
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
|
||||
const isNewMsgTips = ref()
|
||||
const isSendMsgTips = ref()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import { userModifySelfService } from '@/api/user'
|
||||
import defaultImg from '@/assets/image/select_avatar.jpg'
|
||||
@@ -9,9 +9,10 @@ import { maskPhoneNum, showTimeFormatDay } from '@/js/utils/common'
|
||||
import EditAvatar from '@/components/common/EditAvatar.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
// 准备表单数据
|
||||
const formModel = ref({})
|
||||
const avatarUrl = ref(userData.user.avatar)
|
||||
const isLoading = ref(false)
|
||||
const isShowEditAvatar = ref(false)
|
||||
|
||||
@@ -22,13 +23,13 @@ onMounted(async () => {
|
||||
})
|
||||
})
|
||||
|
||||
const onNewAvatar = ({ avatar, avatarThumb }) => {
|
||||
formModel.value.avatar = avatar
|
||||
formModel.value.avatarThumb = avatarThumb
|
||||
const onNewAvatar = ({ avatarId, avatar }) => {
|
||||
formModel.value.avatarId = avatarId
|
||||
avatarUrl.value = avatar
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
if (!isSomeChanged()) {
|
||||
if (!isSomeOneChanged()) {
|
||||
ElMessage.warning('您还没有修改任何信息哦!')
|
||||
return
|
||||
}
|
||||
@@ -44,14 +45,13 @@ const onSave = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const isSomeChanged = () => {
|
||||
const isSomeOneChanged = () => {
|
||||
return !(
|
||||
formModel.value.nickName === userData.user.nickName &&
|
||||
formModel.value.gender === userData.user.gender &&
|
||||
formModel.value.birthday === showTimeFormatDay(userData.user.birthday) &&
|
||||
formModel.value.signature === userData.user.signature &&
|
||||
formModel.value.avatar === userData.user.avatar &&
|
||||
formModel.value.avatarThumb === userData.user.avatarThumb
|
||||
formModel.value.avatarId === userData.user.avatarId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ const displayPhone = computed(() => {
|
||||
<el-container class="el-container__body">
|
||||
<el-aside width="240px">
|
||||
<img
|
||||
:src="formModel.avatar || defaultImg"
|
||||
:src="avatarUrl || defaultImg"
|
||||
alt="图片加载错误"
|
||||
@click="isShowEditAvatar = true"
|
||||
style="text-align: center; border-radius: 10px"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { maskPhoneNum } from '@/js/utils/common'
|
||||
import EditEmail from '@/views/setting/components/EditEmail.vue'
|
||||
import EditPassword from '@/views/setting/components/EditPassword.vue'
|
||||
import EditPhone from '@/views/setting/components/EditPhone.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const userData = userStore()
|
||||
const userData = useUserStore()
|
||||
const isShowEditPassword = ref(false)
|
||||
const isShowEditPhone = ref(false)
|
||||
const isShowEditEmail = ref(false)
|
||||
|
||||
@@ -28,7 +28,7 @@ export default defineConfig({
|
||||
},
|
||||
'/oss/': {
|
||||
// 获取图片的请求
|
||||
target: 'https://bk0528.oss-cn-beijing.aliyuncs.com', // 对象存储oss的源
|
||||
target: 'http://127.0.0.1:9001', // 对象存储oss的源
|
||||
changeOrigin: true, // 修改源
|
||||
rewrite: (path) => path.replace(/^\/oss\//, '')
|
||||
}
|
||||
|
||||