mirror of
https://gitee.com/lijingbo-2021/open-anylink-web.git
synced 2025-12-30 11:02:25 +00:00
msg.content消息结构重构
This commit is contained in:
@@ -11,17 +11,23 @@ export const proto = {
|
|||||||
// 和服务端约定好的,第一个消息都是从10001开始的
|
// 和服务端约定好的,第一个消息都是从10001开始的
|
||||||
export const BEGIN_MSG_ID = 10001
|
export const BEGIN_MSG_ID = 10001
|
||||||
|
|
||||||
// 消息内容类型
|
/**
|
||||||
|
* 消息内容类型
|
||||||
|
* MIX类型为TEXT,EMOJI,SCREENSHOT,AT,QUOTE的组合
|
||||||
|
*/
|
||||||
export const msgContentType = {
|
export const msgContentType = {
|
||||||
MIX: 0, // 组合,包含多种类型
|
TEXT: 0b0000000000000001, // 文本
|
||||||
TEXT: 1, // 文本
|
EMOJI: 0b0000000000000010, // 表情
|
||||||
IMAGE: 2, // 图片
|
SCREENSHOT: 0b0000000000000100, // 截图
|
||||||
RECORDING: 3, // 语音
|
AT: 0b0000000000001000, // @
|
||||||
AUDIO: 4, // 音频文件
|
QUOTE: 0b0000000000010000, // 引用
|
||||||
EMOJI: 5, // 表情
|
|
||||||
VIDEO: 6, // 视频
|
IMAGE: 0b0000001000000000, // 图片
|
||||||
DOCUMENT: 7, // 文档
|
RECORDING: 0b0000010000000000, // 语音
|
||||||
FORWARD_TOGETHER: 10 // 合并转发消息
|
AUDIO: 0b0000100000000000, // 音频文件
|
||||||
|
VIDEO: 0b0001000000000000, // 视频
|
||||||
|
DOCUMENT: 0b0010000000000000, // 文档
|
||||||
|
FORWARD_TOGETHER: 0b0100000000000000 // 合并转发消息
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息发送状态
|
// 消息发送状态
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ export const showTimeFormatDay = (datatime) => {
|
|||||||
return `${year}-${month}-${day}`
|
return `${year}-${month}-${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const showDurationFormat = (duration) => {
|
||||||
|
if (!duration) {
|
||||||
|
return '0:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(duration / 60)
|
||||||
|
const seconds = Math.floor(duration % 60)
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
export const combineId = (fromId, toId) => {
|
export const combineId = (fromId, toId) => {
|
||||||
if (fromId < toId) {
|
if (fromId < toId) {
|
||||||
return fromId + '@' + toId
|
return fromId + '@' + toId
|
||||||
|
|||||||
188
src/js/utils/message.js
Normal file
188
src/js/utils/message.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { msgContentType } from '@/const/msgConst'
|
||||||
|
import { jsonParseSafe, showDurationFormat } from './common'
|
||||||
|
import { useImageStore, useAudioStore, useVideoStore, useDocumentStore } from '@/stores'
|
||||||
|
import { emojis } from './emojis'
|
||||||
|
|
||||||
|
const imageData = useImageStore()
|
||||||
|
const audioData = useAudioStore()
|
||||||
|
const videoData = useVideoStore()
|
||||||
|
const documentData = useDocumentStore()
|
||||||
|
|
||||||
|
export const showSimplifyMsgContent = (content) => {
|
||||||
|
const arr = jsonParseSafe(content)
|
||||||
|
if (!arr) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
let simplifyContent = ''
|
||||||
|
|
||||||
|
for (const item of arr) {
|
||||||
|
if (!item.type || !item.value) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case msgContentType.TEXT:
|
||||||
|
case msgContentType.EMOJI:
|
||||||
|
simplifyContent = simplifyContent + item.value
|
||||||
|
break
|
||||||
|
case msgContentType.AT:
|
||||||
|
simplifyContent = simplifyContent + `@${item.value.nickName} `
|
||||||
|
break
|
||||||
|
case msgContentType.SCREENSHOT:
|
||||||
|
simplifyContent = simplifyContent + `[截图]`
|
||||||
|
break
|
||||||
|
case msgContentType.QUOTE:
|
||||||
|
simplifyContent = simplifyContent + '[引用]'
|
||||||
|
break
|
||||||
|
case msgContentType.RECORDING:
|
||||||
|
simplifyContent =
|
||||||
|
simplifyContent + `[语音] ${showDurationFormat(audioData.audio[item.value].duration)}`
|
||||||
|
break
|
||||||
|
case msgContentType.IMAGE:
|
||||||
|
simplifyContent = simplifyContent + `[图片] ${imageData.image[item.value].fileName}`
|
||||||
|
break
|
||||||
|
case msgContentType.AUDIO:
|
||||||
|
simplifyContent = simplifyContent + `[音频] ${audioData.audio[item.value].fileName}`
|
||||||
|
break
|
||||||
|
case msgContentType.VIDEO:
|
||||||
|
simplifyContent = simplifyContent + `[视频] ${videoData.video[item.value].fileName}`
|
||||||
|
break
|
||||||
|
case msgContentType.DOCUMENT:
|
||||||
|
simplifyContent = simplifyContent + `[文件] ${documentData.document[item.value].fileName}`
|
||||||
|
break
|
||||||
|
case msgContentType.FORWARD_TOGETHER:
|
||||||
|
simplifyContent = simplifyContent + '[聊天记录]'
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
simplifyContent = simplifyContent + item.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return simplifyContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容字符串是否匹配消息结构
|
||||||
|
*/
|
||||||
|
export const isMatchMsgStruct = (contentStr) => {
|
||||||
|
const contentArr = jsonParseSafe(contentStr)
|
||||||
|
if (!contentArr || !Array.isArray(contentArr) || contentArr.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of contentArr) {
|
||||||
|
const { type, value } = item
|
||||||
|
if (!type || !value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case msgContentType.TEXT:
|
||||||
|
break
|
||||||
|
case msgContentType.EMOJI:
|
||||||
|
if (!(value in emojis)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case msgContentType.SCREENSHOT:
|
||||||
|
case msgContentType.IMAGE:
|
||||||
|
case msgContentType.RECORDING:
|
||||||
|
case msgContentType.AUDIO:
|
||||||
|
case msgContentType.VIDEO:
|
||||||
|
case msgContentType.DOCUMENT:
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case msgContentType.AT: {
|
||||||
|
const { account, nickName } = value
|
||||||
|
if (!account || !nickName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case msgContentType.QUOTE: {
|
||||||
|
const { msgId, nickName } = value
|
||||||
|
if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case msgContentType.FORWARD_TOGETHER: {
|
||||||
|
const { sessionId, data } = value
|
||||||
|
if (!sessionId || !data) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const { msgId, nickName } = item
|
||||||
|
if (!msgId || !nickName || !/^\d+$/.test(msgId)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为 MIX 类型
|
||||||
|
* @param {*} type
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isMixType = (type) => {
|
||||||
|
const MIX_CANDIDATES =
|
||||||
|
msgContentType.TEXT |
|
||||||
|
msgContentType.EMOJI |
|
||||||
|
msgContentType.SCREENSHOT |
|
||||||
|
msgContentType.AT |
|
||||||
|
msgContentType.QUOTE
|
||||||
|
|
||||||
|
return type <= MIX_CANDIDATES
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有包含图片的type集合
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const imageTypes = () => {
|
||||||
|
return [
|
||||||
|
msgContentType.IMAGE,
|
||||||
|
msgContentType.SCREENSHOT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.EMOJI,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.AT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.QUOTE,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.QUOTE,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.QUOTE,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.AT || msgContentType.QUOTE,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI || msgContentType.AT,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT || msgContentType.QUOTE,
|
||||||
|
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT || msgContentType.QUOTE,
|
||||||
|
|
||||||
|
msgContentType.SCREENSHOT ||
|
||||||
|
msgContentType.TEXT ||
|
||||||
|
msgContentType.EMOJI ||
|
||||||
|
msgContentType.QUOTE,
|
||||||
|
|
||||||
|
msgContentType.SCREENSHOT ||
|
||||||
|
msgContentType.TEXT ||
|
||||||
|
msgContentType.EMOJI ||
|
||||||
|
msgContentType.AT ||
|
||||||
|
msgContentType.QUOTE
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { proto } from '@/const/msgConst'
|
|||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
|
export const chatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
|
||||||
const header = Header.create({
|
const header = Header.create({
|
||||||
magic: proto.magic,
|
magic: proto.magic,
|
||||||
version: proto.version,
|
version: proto.version,
|
||||||
@@ -18,6 +18,7 @@ export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
|
|||||||
toId: remoteId,
|
toId: remoteId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
content: content,
|
content: content,
|
||||||
|
contentType: contentType,
|
||||||
seq: sequence
|
seq: sequence
|
||||||
})
|
})
|
||||||
const chatMsg = Msg.create({ header: header, body: body })
|
const chatMsg = Msg.create({ header: header, body: body })
|
||||||
@@ -27,7 +28,7 @@ export const chatConstructor = ({ sessionId, remoteId, content, sequence }) => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const groupChatConstructor = ({ sessionId, remoteId, content, sequence }) => {
|
export const groupChatConstructor = ({ sessionId, remoteId, content, contentType, sequence }) => {
|
||||||
const header = Header.create({
|
const header = Header.create({
|
||||||
magic: proto.magic,
|
magic: proto.magic,
|
||||||
version: proto.version,
|
version: proto.version,
|
||||||
@@ -42,6 +43,7 @@ export const groupChatConstructor = ({ sessionId, remoteId, content, sequence })
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
groupId: remoteId,
|
groupId: remoteId,
|
||||||
content: content,
|
content: content,
|
||||||
|
contentType: contentType,
|
||||||
seq: sequence
|
seq: sequence
|
||||||
})
|
})
|
||||||
const msg = Msg.create({ header: header, body: body })
|
const msg = Msg.create({ header: header, body: body })
|
||||||
|
|||||||
@@ -343,9 +343,15 @@ class WsConnect {
|
|||||||
* @param {*} before 发送前的处理,用于展示发送前状态
|
* @param {*} before 发送前的处理,用于展示发送前状态
|
||||||
* @param {*} after 发送后(接收MsgType.DELIVERED时)的处理,用于展示发送后状态
|
* @param {*} after 发送后(接收MsgType.DELIVERED时)的处理,用于展示发送后状态
|
||||||
*/
|
*/
|
||||||
sendMsg(sessionId, remoteId, msgType, content, seq, before, after) {
|
sendMsg(sessionId, remoteId, msgType, content, contentType, seq, before, after) {
|
||||||
const sequence = seq || uuidv4()
|
const sequence = seq || uuidv4()
|
||||||
const data = this.dataConstructor[msgType]({ sessionId, remoteId, content, sequence })
|
const data = this.dataConstructor[msgType]({
|
||||||
|
sessionId,
|
||||||
|
remoteId,
|
||||||
|
content,
|
||||||
|
contentType,
|
||||||
|
sequence
|
||||||
|
})
|
||||||
before(data)
|
before(data)
|
||||||
this.msgIdRefillCallback[sequence] = after
|
this.msgIdRefillCallback[sequence] = after
|
||||||
this.sendAgent(data)
|
this.sendAgent(data)
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class Message {
|
|||||||
fromId,
|
fromId,
|
||||||
msgType,
|
msgType,
|
||||||
content,
|
content,
|
||||||
|
contentType,
|
||||||
msgTime,
|
msgTime,
|
||||||
sendTime = undefined,
|
sendTime = undefined,
|
||||||
msgId = undefined,
|
msgId = undefined,
|
||||||
@@ -78,6 +79,7 @@ class Message {
|
|||||||
this.fromId = fromId
|
this.fromId = fromId
|
||||||
this.msgType = msgType
|
this.msgType = msgType
|
||||||
this.content = content
|
this.content = content
|
||||||
|
this.contentType = contentType
|
||||||
this.status = status
|
this.status = status
|
||||||
this.msgTime = msgTime
|
this.msgTime = msgTime
|
||||||
this.sendTime = sendTime
|
this.sendTime = sendTime
|
||||||
|
|||||||
@@ -819,6 +819,7 @@ export const Body = ($root.Body = (() => {
|
|||||||
* @property {string|null} [groupId] Body groupId
|
* @property {string|null} [groupId] Body groupId
|
||||||
* @property {number|Long|null} [msgId] Body msgId
|
* @property {number|Long|null} [msgId] Body msgId
|
||||||
* @property {string|null} [content] Body content
|
* @property {string|null} [content] Body content
|
||||||
|
* @property {number|null} [contentType] Body contentType
|
||||||
* @property {string|null} [seq] Body seq
|
* @property {string|null} [seq] Body seq
|
||||||
* @property {string|null} [sessionId] Body sessionId
|
* @property {string|null} [sessionId] Body sessionId
|
||||||
*/
|
*/
|
||||||
@@ -836,8 +837,9 @@ export const Body = ($root.Body = (() => {
|
|||||||
* | 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
* | 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
||||||
* | 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
* | 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
||||||
* | 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
* | 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
||||||
* | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
* | 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
|
||||||
* | 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
* | 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||||
|
* |10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||||
* +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
* +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
||||||
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
* NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
||||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||||
@@ -848,8 +850,9 @@ export const Body = ($root.Body = (() => {
|
|||||||
* | 5 | groupId | - | - | - | M | M | M | o | - |
|
* | 5 | groupId | - | - | - | M | M | M | o | - |
|
||||||
* | 6 | msgId | - | - | - | M | - | M | M | M |
|
* | 6 | msgId | - | - | - | M | - | M | M | M |
|
||||||
* | 7 | content | M | M | M | M | M | M | M | - |
|
* | 7 | content | M | M | M | M | M | M | M | - |
|
||||||
* | 8 | seq | - | - | - | - | M | M | - | - |
|
* | 8 | contentType | - | - | - | - | - | - | - | - |
|
||||||
* | 9 | sessionId | - | - | - | M | M | M | M | M |
|
* | 9 | seq | - | - | - | - | M | M | - | - |
|
||||||
|
* |10 | sessionId | - | - | - | M | M | M | M | M |
|
||||||
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
* +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||||
* @implements IBody
|
* @implements IBody
|
||||||
* @constructor
|
* @constructor
|
||||||
@@ -917,6 +920,14 @@ export const Body = ($root.Body = (() => {
|
|||||||
*/
|
*/
|
||||||
Body.prototype.content = null
|
Body.prototype.content = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body contentType.
|
||||||
|
* @member {number|null|undefined} contentType
|
||||||
|
* @memberof Body
|
||||||
|
* @instance
|
||||||
|
*/
|
||||||
|
Body.prototype.contentType = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body seq.
|
* Body seq.
|
||||||
* @member {string|null|undefined} seq
|
* @member {string|null|undefined} seq
|
||||||
@@ -978,6 +989,12 @@ export const Body = ($root.Body = (() => {
|
|||||||
set: $util.oneOfSetter($oneOfFields)
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(Body.prototype, '_contentType', {
|
||||||
|
get: $util.oneOfGetter(($oneOfFields = ['contentType'])),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
})
|
||||||
|
|
||||||
// Virtual OneOf for proto3 optional field
|
// Virtual OneOf for proto3 optional field
|
||||||
Object.defineProperty(Body.prototype, '_seq', {
|
Object.defineProperty(Body.prototype, '_seq', {
|
||||||
get: $util.oneOfGetter(($oneOfFields = ['seq'])),
|
get: $util.oneOfGetter(($oneOfFields = ['seq'])),
|
||||||
@@ -1027,10 +1044,12 @@ export const Body = ($root.Body = (() => {
|
|||||||
writer.uint32(/* id 6, wireType 0 =*/ 48).int64(message.msgId)
|
writer.uint32(/* id 6, wireType 0 =*/ 48).int64(message.msgId)
|
||||||
if (message.content != null && Object.hasOwnProperty.call(message, 'content'))
|
if (message.content != null && Object.hasOwnProperty.call(message, 'content'))
|
||||||
writer.uint32(/* id 7, wireType 2 =*/ 58).string(message.content)
|
writer.uint32(/* id 7, wireType 2 =*/ 58).string(message.content)
|
||||||
|
if (message.contentType != null && Object.hasOwnProperty.call(message, 'contentType'))
|
||||||
|
writer.uint32(/* id 8, wireType 0 =*/ 64).int32(message.contentType)
|
||||||
if (message.seq != null && Object.hasOwnProperty.call(message, 'seq'))
|
if (message.seq != null && Object.hasOwnProperty.call(message, 'seq'))
|
||||||
writer.uint32(/* id 8, wireType 2 =*/ 66).string(message.seq)
|
writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.seq)
|
||||||
if (message.sessionId != null && Object.hasOwnProperty.call(message, 'sessionId'))
|
if (message.sessionId != null && Object.hasOwnProperty.call(message, 'sessionId'))
|
||||||
writer.uint32(/* id 9, wireType 2 =*/ 74).string(message.sessionId)
|
writer.uint32(/* id 10, wireType 2 =*/ 82).string(message.sessionId)
|
||||||
return writer
|
return writer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,10 +1113,14 @@ export const Body = ($root.Body = (() => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 8: {
|
case 8: {
|
||||||
message.seq = reader.string()
|
message.contentType = reader.int32()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 9: {
|
case 9: {
|
||||||
|
message.seq = reader.string()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 10: {
|
||||||
message.sessionId = reader.string()
|
message.sessionId = reader.string()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1171,6 +1194,10 @@ export const Body = ($root.Body = (() => {
|
|||||||
properties._content = 1
|
properties._content = 1
|
||||||
if (!$util.isString(message.content)) return 'content: string expected'
|
if (!$util.isString(message.content)) return 'content: string expected'
|
||||||
}
|
}
|
||||||
|
if (message.contentType != null && message.hasOwnProperty('contentType')) {
|
||||||
|
properties._contentType = 1
|
||||||
|
if (!$util.isInteger(message.contentType)) return 'contentType: integer expected'
|
||||||
|
}
|
||||||
if (message.seq != null && message.hasOwnProperty('seq')) {
|
if (message.seq != null && message.hasOwnProperty('seq')) {
|
||||||
properties._seq = 1
|
properties._seq = 1
|
||||||
if (!$util.isString(message.seq)) return 'seq: string expected'
|
if (!$util.isString(message.seq)) return 'seq: string expected'
|
||||||
@@ -1208,6 +1235,7 @@ export const Body = ($root.Body = (() => {
|
|||||||
object.msgId.high >>> 0
|
object.msgId.high >>> 0
|
||||||
).toNumber()
|
).toNumber()
|
||||||
if (object.content != null) message.content = String(object.content)
|
if (object.content != null) message.content = String(object.content)
|
||||||
|
if (object.contentType != null) message.contentType = object.contentType | 0
|
||||||
if (object.seq != null) message.seq = String(object.seq)
|
if (object.seq != null) message.seq = String(object.seq)
|
||||||
if (object.sessionId != null) message.sessionId = String(object.sessionId)
|
if (object.sessionId != null) message.sessionId = String(object.sessionId)
|
||||||
return message
|
return message
|
||||||
@@ -1261,6 +1289,10 @@ export const Body = ($root.Body = (() => {
|
|||||||
object.content = message.content
|
object.content = message.content
|
||||||
if (options.oneofs) object._content = 'content'
|
if (options.oneofs) object._content = 'content'
|
||||||
}
|
}
|
||||||
|
if (message.contentType != null && message.hasOwnProperty('contentType')) {
|
||||||
|
object.contentType = message.contentType
|
||||||
|
if (options.oneofs) object._contentType = 'contentType'
|
||||||
|
}
|
||||||
if (message.seq != null && message.hasOwnProperty('seq')) {
|
if (message.seq != null && message.hasOwnProperty('seq')) {
|
||||||
object.seq = message.seq
|
object.seq = message.seq
|
||||||
if (options.oneofs) object._seq = 'seq'
|
if (options.oneofs) object._seq = 'seq'
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ message Header {
|
|||||||
| 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
| 5 | groupId | - | - | - | - | M | M | - | M | - | todo | todo |
|
||||||
| 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
| 6 | msgId | - | - | - | M | - | M | O | O | M | todo | todo |
|
||||||
| 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
| 7 | content | - | - | M | M | M | M | M | M | - | todo | todo |
|
||||||
| 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
| 8 | contentType | - | - | M | M | M | M | - | - | - | todo | todo |
|
||||||
| 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
| 9 | seq | - | - | M | M | M | M | O | O | M | todo | todo |
|
||||||
|
|10 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo |
|
||||||
+---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
+---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+
|
||||||
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE DELETE
|
||||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||||
@@ -77,8 +78,9 @@ message Header {
|
|||||||
| 5 | groupId | - | - | - | M | M | M | o | - |
|
| 5 | groupId | - | - | - | M | M | M | o | - |
|
||||||
| 6 | msgId | - | - | - | M | - | M | M | M |
|
| 6 | msgId | - | - | - | M | - | M | M | M |
|
||||||
| 7 | content | M | M | M | M | M | M | M | - |
|
| 7 | content | M | M | M | M | M | M | M | - |
|
||||||
| 8 | seq | - | - | - | - | M | M | - | - |
|
| 8 | contentType | - | - | - | - | - | - | - | - |
|
||||||
| 9 | sessionId | - | - | - | M | M | M | M | M |
|
| 9 | seq | - | - | - | - | M | M | - | - |
|
||||||
|
|10 | sessionId | - | - | - | M | M | M | M | M |
|
||||||
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
+---+--------------+------------+------------+-------------+------------+---------+---------+-----------+-----------+
|
||||||
*/
|
*/
|
||||||
message Body {
|
message Body {
|
||||||
@@ -89,8 +91,9 @@ message Body {
|
|||||||
optional string groupId = 5;
|
optional string groupId = 5;
|
||||||
optional int64 msgId = 6; //服务端生成的消息ID,会话内单调递增,可用于消息排序
|
optional int64 msgId = 6; //服务端生成的消息ID,会话内单调递增,可用于消息排序
|
||||||
optional string content = 7;
|
optional string content = 7;
|
||||||
optional string seq = 8; //客户端生成的序列号ID,会话内唯一,可用于消息去重
|
optional int32 contentType = 8;
|
||||||
optional string sessionId = 9; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
|
optional string seq = 9; //客户端生成的序列号ID,会话内唯一,可用于消息去重
|
||||||
|
optional string sessionId = 10; //MsgType=SENDER_SYNC需带上该字段,因为此时fromId和toId都是发送端的账号,无法识别是哪个session
|
||||||
}
|
}
|
||||||
|
|
||||||
message Extension {
|
message Extension {
|
||||||
|
|||||||
@@ -18,20 +18,18 @@ export const useAudioStore = defineStore('anylink-audio', () => {
|
|||||||
audio.value[obj.objectId] = obj
|
audio.value[obj.objectId] = obj
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadAudio = async (msgRecords) => {
|
const preloadAudioFromMsgList = async (msgRecords) => {
|
||||||
const audioIds = new Set()
|
const audioIds = new Set()
|
||||||
msgRecords.forEach((item) => {
|
msgRecords.forEach((item) => {
|
||||||
const content = item.content
|
const aar = jsonParseSafe(item.content)
|
||||||
const contentJson = jsonParseSafe(content)
|
aar.forEach((item) => {
|
||||||
if (
|
if (item.type === msgContentType.AUDIO || item.type === msgContentType.RECORDING) {
|
||||||
(contentJson && contentJson['type'] === msgContentType.RECORDING) ||
|
const objectId = item.value
|
||||||
(contentJson && contentJson['type'] === msgContentType.AUDIO)
|
if (!audio.value[objectId]) {
|
||||||
) {
|
audioIds.add(objectId)
|
||||||
const objectId = contentJson['value']
|
}
|
||||||
if (!audio.value[objectId]) {
|
|
||||||
audioIds.add(objectId)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (audioIds.size > 0) {
|
if (audioIds.size > 0) {
|
||||||
@@ -55,7 +53,7 @@ export const useAudioStore = defineStore('anylink-audio', () => {
|
|||||||
return {
|
return {
|
||||||
audio,
|
audio,
|
||||||
setAudio,
|
setAudio,
|
||||||
preloadAudio,
|
preloadAudioFromMsgList,
|
||||||
clear
|
clear
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ export const useDocumentStore = defineStore('anylink-document', () => {
|
|||||||
document.value[obj.objectId] = obj
|
document.value[obj.objectId] = obj
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadDocument = async (msgRecords) => {
|
const preloadDocumentFromMsgList = async (msgRecords) => {
|
||||||
const documentIds = new Set()
|
const documentIds = new Set()
|
||||||
msgRecords.forEach((item) => {
|
msgRecords.forEach((item) => {
|
||||||
const content = item.content
|
const aar = jsonParseSafe(item.content)
|
||||||
const contentJson = jsonParseSafe(content)
|
aar.forEach((item) => {
|
||||||
if (contentJson && contentJson['type'] === msgContentType.DOCUMENT) {
|
if (item.type === msgContentType.DOCUMENT) {
|
||||||
const objectId = contentJson['value']
|
const objectId = item.value
|
||||||
if (!document.value[objectId]) {
|
if (!document.value[objectId]) {
|
||||||
documentIds.add(objectId)
|
documentIds.add(objectId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (documentIds.size > 0) {
|
if (documentIds.size > 0) {
|
||||||
@@ -52,7 +53,7 @@ export const useDocumentStore = defineStore('anylink-document', () => {
|
|||||||
return {
|
return {
|
||||||
document,
|
document,
|
||||||
setDocument,
|
setDocument,
|
||||||
preloadDocument,
|
preloadDocumentFromMsgList,
|
||||||
clear
|
clear
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { jsonParseSafe } from '@/js/utils/common'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const pattern = /\{[a-f0-9]+\}/g
|
|
||||||
|
|
||||||
// image的缓存数据,不持久化存储
|
// image的缓存数据,不持久化存储
|
||||||
export const useImageStore = defineStore('anylink-image', () => {
|
export const useImageStore = defineStore('anylink-image', () => {
|
||||||
/**
|
/**
|
||||||
@@ -43,19 +41,20 @@ export const useImageStore = defineStore('anylink-image', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadImageInfoFromContent = async (content) => {
|
const preloadImageFromMsg = async (content) => {
|
||||||
|
if (!content) return
|
||||||
|
|
||||||
const imageIds = new Set()
|
const imageIds = new Set()
|
||||||
const matches = content.match(pattern)
|
const aar = jsonParseSafe(content)
|
||||||
if (matches && matches.length > 0) {
|
aar.forEach((item) => {
|
||||||
matches.forEach((item) => {
|
if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
|
||||||
let startIndex = item.indexOf('{')
|
const objectId = item.value
|
||||||
let endIndex = item.indexOf('}')
|
|
||||||
const objectId = item.slice(startIndex + 1, endIndex)
|
|
||||||
if (!image.value[objectId]) {
|
if (!image.value[objectId]) {
|
||||||
imageIds.add(objectId)
|
imageIds.add(objectId)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
if (imageIds.size > 0) {
|
if (imageIds.size > 0) {
|
||||||
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
||||||
res.data.data.forEach((item) => {
|
res.data.data.forEach((item) => {
|
||||||
@@ -64,29 +63,18 @@ export const useImageStore = defineStore('anylink-image', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadImage = async (msgRecords) => {
|
const preloadImageFromMsgList = async (msgRecords) => {
|
||||||
const imageIds = new Set()
|
const imageIds = new Set()
|
||||||
msgRecords.forEach((item) => {
|
msgRecords.forEach((item) => {
|
||||||
const content = item.content
|
const aar = jsonParseSafe(item.content)
|
||||||
const contentJson = jsonParseSafe(content)
|
aar.forEach((item) => {
|
||||||
if (contentJson && contentJson['type'] === msgContentType.IMAGE) {
|
if (item.type === msgContentType.SCREENSHOT || item.type === msgContentType.IMAGE) {
|
||||||
const objectId = contentJson['value']
|
const objectId = item.value
|
||||||
if (!image.value[objectId]) {
|
if (!image.value[objectId]) {
|
||||||
imageIds.add(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) {
|
if (imageIds.size > 0) {
|
||||||
@@ -117,8 +105,8 @@ export const useImageStore = defineStore('anylink-image', () => {
|
|||||||
setImage,
|
setImage,
|
||||||
setImageInSession,
|
setImageInSession,
|
||||||
clearImageInSession,
|
clearImageInSession,
|
||||||
loadImageInfoFromContent,
|
preloadImageFromMsg,
|
||||||
preloadImage,
|
preloadImageFromMsgList,
|
||||||
clear
|
clear
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -134,10 +134,10 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
|||||||
* @param {*} msgRecords
|
* @param {*} msgRecords
|
||||||
*/
|
*/
|
||||||
const preloadResource = async (msgRecords) => {
|
const preloadResource = async (msgRecords) => {
|
||||||
await useImageStore().preloadImage(msgRecords)
|
await useImageStore().preloadImageFromMsgList(msgRecords)
|
||||||
await useAudioStore().preloadAudio(msgRecords)
|
await useAudioStore().preloadAudioFromMsgList(msgRecords)
|
||||||
await useVideoStore().preloadVideo(msgRecords)
|
await useVideoStore().preloadVideoFromMsgList(msgRecords)
|
||||||
await useDocumentStore().preloadDocument(msgRecords)
|
await useDocumentStore().preloadDocumentFromMsgList(msgRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ export const useVideoStore = defineStore('anylink-video', () => {
|
|||||||
video.value[obj.objectId] = obj
|
video.value[obj.objectId] = obj
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadVideo = async (msgRecords) => {
|
const preloadVideoFromMsgList = async (msgRecords) => {
|
||||||
const videoIds = new Set()
|
const videoIds = new Set()
|
||||||
msgRecords.forEach((item) => {
|
msgRecords.forEach((item) => {
|
||||||
const content = item.content
|
const aar = jsonParseSafe(item.content)
|
||||||
const contentJson = jsonParseSafe(content)
|
aar.forEach((item) => {
|
||||||
if (contentJson && contentJson['type'] === msgContentType.VIDEO) {
|
if (item.type === msgContentType.VIDEO) {
|
||||||
const objectId = contentJson['value']
|
const objectId = item.value
|
||||||
if (!video.value[objectId]) {
|
if (!video.value[objectId]) {
|
||||||
videoIds.add(objectId)
|
videoIds.add(objectId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (videoIds.size > 0) {
|
if (videoIds.size > 0) {
|
||||||
@@ -52,7 +53,7 @@ export const useVideoStore = defineStore('anylink-video', () => {
|
|||||||
return {
|
return {
|
||||||
video,
|
video,
|
||||||
setVideo,
|
setVideo,
|
||||||
preloadVideo,
|
preloadVideoFromMsgList,
|
||||||
clear
|
clear
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { onReceiveChatMsg, onReceiveGroupChatMsg, onReceiveGroupSystemMsg } from
|
|||||||
import { userQueryService } from '@/api/user'
|
import { userQueryService } from '@/api/user'
|
||||||
import { ElLoading, ElMessage } from 'element-plus'
|
import { ElLoading, ElMessage } from 'element-plus'
|
||||||
import { el_loading_options } from '@/const/commonConst'
|
import { el_loading_options } from '@/const/commonConst'
|
||||||
import { combineId, sessionIdConvert } from '@/js/utils/common'
|
import { combineId, jsonParseSafe, sessionIdConvert } from '@/js/utils/common'
|
||||||
import MenuSession from '@/views/message/components/MenuSession.vue'
|
import MenuSession from '@/views/message/components/MenuSession.vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { BEGIN_MSG_ID, msgContentType, msgSendStatus } from '@/const/msgConst'
|
import { BEGIN_MSG_ID, msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||||
@@ -120,29 +120,45 @@ const unreadAtRecords = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleShowHighlight = (msgId) => {
|
const handleShowHighlight = (msgId) => {
|
||||||
const readElement = document.querySelector(
|
let targetKey = msgId
|
||||||
|
let element = document.querySelector(
|
||||||
`#message-item-${sessionIdConvert(selectedSessionId.value)}-${msgId}`
|
`#message-item-${sessionIdConvert(selectedSessionId.value)}-${msgId}`
|
||||||
)
|
) // 先拿msgId定位到元素,如果不行再用msgKey
|
||||||
if (!readElement) {
|
if (!element) {
|
||||||
ElMessage.success('请加载更多消息后查找')
|
// 用msgId逆向找msgKey
|
||||||
} else {
|
for (const msgKey of msgKeysShow.value) {
|
||||||
const msgListRect = msgListDiv.value.getBoundingClientRect()
|
const msg = messageData.getMsg(selectedSessionId.value, msgKey)
|
||||||
const rect = readElement.getBoundingClientRect()
|
if (msg.msgId == msgId) {
|
||||||
// 判断 readElement 是否在 msgListDiv 的视口内
|
targetKey = msgKey
|
||||||
const isInViewport = rect.top >= msgListRect.top && rect.bottom <= msgListRect.bottom
|
element = document.querySelector(
|
||||||
if (!isInViewport) {
|
`#message-item-${sessionIdConvert(selectedSessionId.value)}-${msgKey}`
|
||||||
nextTick(() => {
|
)
|
||||||
msgListDiv.value.scrollTo({
|
break
|
||||||
top: msgListDiv.value.scrollTop - (msgListRect.top - rect.top),
|
}
|
||||||
behavior: 'smooth'
|
}
|
||||||
})
|
|
||||||
})
|
if (!element) {
|
||||||
|
ElMessage.success('请加载更多消息后查找')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
highlightedMsgIds.value.add(msgId + '')
|
|
||||||
setTimeout(() => {
|
|
||||||
highlightedMsgIds.value.delete(msgId + '')
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const msgListRect = msgListDiv.value.getBoundingClientRect()
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
// 判断 element 是否在 msgListDiv 的视口内
|
||||||
|
const isInViewport = rect.top >= msgListRect.top && rect.bottom <= msgListRect.bottom
|
||||||
|
if (!isInViewport) {
|
||||||
|
nextTick(() => {
|
||||||
|
msgListDiv.value.scrollTo({
|
||||||
|
top: msgListDiv.value.scrollTop - (msgListRect.top - rect.top),
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
highlightedMsgIds.value.add(targetKey + '')
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightedMsgIds.value.delete(targetKey + '')
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReadAt = () => {
|
const handleReadAt = () => {
|
||||||
@@ -563,7 +579,7 @@ const sendRead = () => {
|
|||||||
selectedSession.value.sessionType === MsgType.CHAT
|
selectedSession.value.sessionType === MsgType.CHAT
|
||||||
? MsgType.CHAT_READ
|
? MsgType.CHAT_READ
|
||||||
: MsgType.GROUP_CHAT_READ
|
: MsgType.GROUP_CHAT_READ
|
||||||
wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', '', () => {})
|
wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', 0, '', () => {})
|
||||||
// 更新本地缓存的已读位置
|
// 更新本地缓存的已读位置
|
||||||
messageData.updateSession({
|
messageData.updateSession({
|
||||||
sessionId: selectedSessionId.value,
|
sessionId: selectedSessionId.value,
|
||||||
@@ -577,7 +593,7 @@ const sendRead = () => {
|
|||||||
/**
|
/**
|
||||||
* 处理发送转发的消息
|
* 处理发送转发的消息
|
||||||
*/
|
*/
|
||||||
const handleSendForwardMsg = async ({ session, content }) => {
|
const handleSendForwardMsg = async ({ session, content, contentType }) => {
|
||||||
if (session.sessionType === MsgType.GROUP_CHAT && session.leave) {
|
if (session.sessionType === MsgType.GROUP_CHAT && session.leave) {
|
||||||
ElMessage.warning('您已离开该群或群已被解散')
|
ElMessage.warning('您已离开该群或群已被解散')
|
||||||
return
|
return
|
||||||
@@ -609,12 +625,13 @@ const handleSendForwardMsg = async ({ session, content }) => {
|
|||||||
const seq = uuidv4()
|
const seq = uuidv4()
|
||||||
const msg = {
|
const msg = {
|
||||||
msgId: seq,
|
msgId: seq,
|
||||||
seq: seq,
|
seq,
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
fromId: myAccount.value,
|
fromId: myAccount.value,
|
||||||
remoteId: session.remoteId,
|
remoteId: session.remoteId,
|
||||||
msgType: session.sessionType,
|
msgType: session.sessionType,
|
||||||
content: content,
|
content,
|
||||||
|
contentType,
|
||||||
status: msgSendStatus.PENDING,
|
status: msgSendStatus.PENDING,
|
||||||
msgTime: new Date(),
|
msgTime: new Date(),
|
||||||
sendTime: new Date()
|
sendTime: new Date()
|
||||||
@@ -657,13 +674,22 @@ const handleSendForwardMsg = async ({ session, content }) => {
|
|||||||
const after = (msgId) => {
|
const after = (msgId) => {
|
||||||
messageData.updateMsg(msg.sessionId, msg.msgId, { msgId, status: msgSendStatus.OK })
|
messageData.updateMsg(msg.sessionId, msg.msgId, { msgId, status: msgSendStatus.OK })
|
||||||
}
|
}
|
||||||
wsConnect.sendMsg(msg.sessionId, msg.remoteId, msg.msgType, msg.content, msg.seq, before, after)
|
wsConnect.sendMsg(
|
||||||
|
msg.sessionId,
|
||||||
|
msg.remoteId,
|
||||||
|
msg.msgType,
|
||||||
|
msg.content,
|
||||||
|
msg.contentType,
|
||||||
|
msg.seq,
|
||||||
|
before,
|
||||||
|
after
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送时先添加本地消息,可以立即渲染
|
* 发送时先添加本地消息,可以立即渲染
|
||||||
*/
|
*/
|
||||||
const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
|
const handleLocalMsg = ({ content, contentType, fn }) => {
|
||||||
const seq = uuidv4()
|
const seq = uuidv4()
|
||||||
const msg = {
|
const msg = {
|
||||||
msgId: seq,
|
msgId: seq,
|
||||||
@@ -671,10 +697,8 @@ const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
|
|||||||
sessionId: selectedSessionId.value,
|
sessionId: selectedSessionId.value,
|
||||||
fromId: myAccount.value,
|
fromId: myAccount.value,
|
||||||
msgType: selectedSession.value.sessionType,
|
msgType: selectedSession.value.sessionType,
|
||||||
content:
|
content,
|
||||||
contentType === msgContentType.MIX
|
contentType,
|
||||||
? content
|
|
||||||
: JSON.stringify({ type: contentType, value: objectId }),
|
|
||||||
status: msgSendStatus.PENDING,
|
status: msgSendStatus.PENDING,
|
||||||
msgTime: new Date(),
|
msgTime: new Date(),
|
||||||
sendTime: new Date()
|
sendTime: new Date()
|
||||||
@@ -693,7 +717,7 @@ const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
|
|||||||
fn(msg)
|
fn(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSendMessage = ({ msg, at }) => {
|
const handleSendMessage = ({ msg, content, at }) => {
|
||||||
if (isNotInGroup.value) {
|
if (isNotInGroup.value) {
|
||||||
ElMessage.warning('您已离开该群或群已被解散')
|
ElMessage.warning('您已离开该群或群已被解散')
|
||||||
return
|
return
|
||||||
@@ -754,7 +778,8 @@ const handleSendMessage = ({ msg, at }) => {
|
|||||||
msg.sessionId,
|
msg.sessionId,
|
||||||
showId.value,
|
showId.value,
|
||||||
selectedSession.value.sessionType,
|
selectedSession.value.sessionType,
|
||||||
msg.content,
|
content,
|
||||||
|
msg.contentType,
|
||||||
msg.seq,
|
msg.seq,
|
||||||
before,
|
before,
|
||||||
after
|
after
|
||||||
@@ -774,10 +799,9 @@ const handleResendMessage = (msg) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const toSendAtList = []
|
const toSendAtList = []
|
||||||
msg.content.split(/(<.*?>)/).forEach((item) => {
|
jsonParseSafe(msg.content).forEach((item) => {
|
||||||
if (item && item.startsWith('<') && item.endsWith('>')) {
|
if (item.type === msgContentType.AT) {
|
||||||
const index = item.indexOf('-')
|
const account = item.value.account
|
||||||
const account = item.slice(1, index) // 第一个字符是<,所以起点从1开始
|
|
||||||
if (account == 0) {
|
if (account == 0) {
|
||||||
toSendAtList.push(account)
|
toSendAtList.push(account)
|
||||||
} else {
|
} else {
|
||||||
@@ -787,6 +811,7 @@ const handleResendMessage = (msg) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
handleSendMessage({ msg, at: toSendAtList })
|
handleSendMessage({ msg, at: toSendAtList })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +825,7 @@ const handleSendAt = (at, sessionId, referMsgId) => {
|
|||||||
contentObj.isAtAll = true
|
contentObj.isAtAll = true
|
||||||
} else {
|
} else {
|
||||||
contentObj.isAtAll = false
|
contentObj.isAtAll = false
|
||||||
contentObj.atList = at.filter((item) => groupMembers.value[item]) // 过滤脏数据
|
contentObj.atList = at.filter((item) => groupMembers.value[item]) // 过滤不是当前合法群成员的数据
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentObj.isAtAll || (!contentObj.isAtAll && contentObj.atList.length > 0)) {
|
if (contentObj.isAtAll || (!contentObj.isAtAll && contentObj.atList.length > 0)) {
|
||||||
@@ -809,6 +834,7 @@ const handleSendAt = (at, sessionId, referMsgId) => {
|
|||||||
showId.value,
|
showId.value,
|
||||||
MsgType.AT,
|
MsgType.AT,
|
||||||
JSON.stringify(contentObj),
|
JSON.stringify(contentObj),
|
||||||
|
0,
|
||||||
null,
|
null,
|
||||||
() => {},
|
() => {},
|
||||||
() => {}
|
() => {}
|
||||||
@@ -1386,7 +1412,8 @@ const handleConfirmForwardMsg = async (sessions) => {
|
|||||||
const msg = messageData.getMsg(selectedSessionId.value, msgKey)
|
const msg = messageData.getMsg(selectedSessionId.value, msgKey)
|
||||||
await handleSendForwardMsg({
|
await handleSendForwardMsg({
|
||||||
session: item,
|
session: item,
|
||||||
content: msg.content
|
content: msg.content,
|
||||||
|
contentType: msg.contentType
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (showForwardMsgDialogTitle.value === '合并转发') {
|
} else if (showForwardMsgDialogTitle.value === '合并转发') {
|
||||||
@@ -1411,13 +1438,16 @@ const handleConfirmForwardMsg = async (sessions) => {
|
|||||||
})
|
})
|
||||||
await handleSendForwardMsg({
|
await handleSendForwardMsg({
|
||||||
session: item,
|
session: item,
|
||||||
content: JSON.stringify({
|
content: JSON.stringify([
|
||||||
type: msgContentType.FORWARD_TOGETHER,
|
{
|
||||||
value: {
|
type: msgContentType.FORWARD_TOGETHER,
|
||||||
sessionId: selectedSessionId.value,
|
value: {
|
||||||
data: [...msgs]
|
sessionId: selectedSessionId.value,
|
||||||
|
data: [...msgs]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
]),
|
||||||
|
contentType: msgContentType.FORWARD_TOGETHER
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1513,7 +1543,7 @@ const onConfirmSelect = async (selected) => {
|
|||||||
|
|
||||||
const inputEditorRef = ref()
|
const inputEditorRef = ref()
|
||||||
const onSendEmoji = (key) => {
|
const onSendEmoji = (key) => {
|
||||||
inputEditorRef.value.addEmoji(key)
|
inputEditorRef.value?.addEmoji(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputRecorderRef = ref(null)
|
const inputRecorderRef = ref(null)
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
|
|||||||
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
||||||
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
||||||
import { emojis } from '@/js/utils/emojis'
|
import { emojis } from '@/js/utils/emojis'
|
||||||
import { MsgType } from '@/proto/msg'
|
|
||||||
import { msgChatQueryMessagesService } from '@/api/message'
|
import { msgChatQueryMessagesService } from '@/api/message'
|
||||||
|
import { showSimplifyMsgContent } from '@/js/utils/message'
|
||||||
|
|
||||||
const props = defineProps(['isShow', 'title', 'sessionId', 'msgs', 'tier'])
|
const props = defineProps(['isShow', 'title', 'sessionId', 'msgs', 'tier'])
|
||||||
const emit = defineEmits(['update:isShow', 'showUserCard', 'close'])
|
const emit = defineEmits(['update:isShow', 'showUserCard', 'close'])
|
||||||
@@ -38,13 +38,14 @@ const audioData = useAudioStore()
|
|||||||
const videoData = useVideoStore()
|
const videoData = useVideoStore()
|
||||||
const documentData = useDocumentStore()
|
const documentData = useDocumentStore()
|
||||||
|
|
||||||
const msgsFromServer = ref({})
|
const forwardMsgs = ref({})
|
||||||
|
const quoteMsg = ref({})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const loadingInstance = ElLoading.service(el_loading_options)
|
const loadingInstance = ElLoading.service(el_loading_options)
|
||||||
try {
|
try {
|
||||||
await messageData.preloadResource(props.msgs)
|
await messageData.preloadResource(props.msgs)
|
||||||
await loadForwardTogetherMsgs()
|
await loadRelatedMsg()
|
||||||
} finally {
|
} finally {
|
||||||
loadingInstance.close()
|
loadingInstance.close()
|
||||||
}
|
}
|
||||||
@@ -60,60 +61,58 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadForwardTogetherMsgs = async () => {
|
const loadRelatedMsg = async () => {
|
||||||
for (const msg of props.msgs) {
|
for (const msg of props.msgs) {
|
||||||
const content = msg.content
|
const content = msg.content
|
||||||
const contentJson = jsonParseSafe(content)
|
const arr = jsonParseSafe(content)
|
||||||
if (!contentJson) {
|
if (!arr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = contentJson['type']
|
for (const item of arr) {
|
||||||
const value = contentJson['value']
|
if (item.type === msgContentType.QUOTE) {
|
||||||
if (!type || !value) {
|
// 先从本地消息缓存中获取
|
||||||
continue
|
const msgFromStore = messageData.getMsg(msg.sessionId, item.value.msgId)
|
||||||
} else {
|
if (!msgFromStore.msgId) {
|
||||||
if (type === msgContentType.FORWARD_TOGETHER) {
|
// 如果本地消息缓存中没有,再去服务器查询
|
||||||
let res
|
const res = await msgChatQueryMessagesService({
|
||||||
try {
|
sessionId: msg.sessionId,
|
||||||
const msgIds = value.data
|
msgIds: item.value.msgId
|
||||||
.map((item) => {
|
|
||||||
return item.msgId
|
|
||||||
})
|
|
||||||
.join(',')
|
|
||||||
res = await msgChatQueryMessagesService({
|
|
||||||
sessionId: value.sessionId,
|
|
||||||
msgIds
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgs = res.data.data
|
if (res.data.data && res.data.data.length > 0) {
|
||||||
if (!res.data.data || res.data.data.length == 0) {
|
quoteMsg.value[msg.msgId] = res.data.data[0]
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// value.data(取里面的nickName) 和 msgs合一
|
|
||||||
const newMsgs = {}
|
|
||||||
msgs.forEach((item) => {
|
|
||||||
newMsgs[item.msgId] = item
|
|
||||||
})
|
|
||||||
value.data.forEach((item) => {
|
|
||||||
if (item.msgId in newMsgs) {
|
|
||||||
newMsgs[item.msgId] = {
|
|
||||||
...newMsgs[item.msgId],
|
|
||||||
...item
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
|
quoteMsg.value[msg.msgId] = msgFromStore
|
||||||
|
}
|
||||||
|
} else if (item.type === msgContentType.FORWARD_TOGETHER) {
|
||||||
|
if (!forwardMsgs.value[msg.msgId]) {
|
||||||
|
forwardMsgs.value[msg.msgId] = []
|
||||||
|
}
|
||||||
|
|
||||||
msgsFromServer.value[msg.msgId] = Object.values(newMsgs).sort((a, b) => {
|
const forwatdMsgIds = item.value.data.map((item) => item.msgId)
|
||||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
const toQueryMsgIds = []
|
||||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
for (const msgId of forwatdMsgIds) {
|
||||||
return timeA - timeB
|
// 先从本地消息缓存中获取
|
||||||
})
|
const msgFromStore = messageData.getMsg(item.value.sessionId, msgId)
|
||||||
|
if (!msgFromStore.msgId) {
|
||||||
|
// 如果本地消息缓存中没有,再去服务器查询
|
||||||
|
toQueryMsgIds.push(msgId)
|
||||||
|
} else {
|
||||||
|
forwardMsgs.value[msg.msgId].push(msgFromStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toQueryMsgIds.length > 0) {
|
||||||
|
const res = await msgChatQueryMessagesService({
|
||||||
|
sessionId: item.value.sessionId,
|
||||||
|
msgIds: toQueryMsgIds.join(',')
|
||||||
|
})
|
||||||
|
res.data.data.forEach((item) => {
|
||||||
|
forwardMsgs.value[msg.msgId].push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,98 +129,62 @@ const isMyAccount = (account) => {
|
|||||||
const renderContent = ({ msg }) => {
|
const renderContent = ({ msg }) => {
|
||||||
const content = msg.content
|
const content = msg.content
|
||||||
const msgId = msg.msgId
|
const msgId = msg.msgId
|
||||||
const msgType = msg.msgType
|
const arr = jsonParseSafe(content)
|
||||||
const contentJson = jsonParseSafe(content)
|
// 不允许非结构化的content
|
||||||
if (!contentJson) {
|
if (!arr) {
|
||||||
return renderMix(content, msgType)
|
return <span></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = contentJson['type']
|
return arr.map((item) => {
|
||||||
const value = contentJson['value']
|
if (!item.type || !item.value) {
|
||||||
if (!type || !value) {
|
return <span></span>
|
||||||
return renderMix(content, msgType)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case msgContentType.MIX:
|
|
||||||
return renderMix(value, msgType)
|
|
||||||
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, msgId)
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
return renderDocument(value)
|
|
||||||
case msgContentType.FORWARD_TOGETHER:
|
|
||||||
return renderForwardTogether(msgId)
|
|
||||||
default:
|
|
||||||
return <span>{content}</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMix = (content, msgType) => {
|
|
||||||
if (!content) return h('div', [])
|
|
||||||
let contentArray = []
|
|
||||||
|
|
||||||
// 1. 先匹配quote引用内容
|
|
||||||
content.split(/(「\{.*?\}」)/).forEach((item) => {
|
|
||||||
if (item.startsWith('「{') && item.endsWith('}」')) {
|
|
||||||
// quote引用内容直接添加如数组
|
|
||||||
contentArray.push(item)
|
|
||||||
} else {
|
|
||||||
//2. 匹配内容中的图片
|
|
||||||
item.split(/(\{\d+\})/).forEach((item) => {
|
|
||||||
//3. 匹配内容中的表情
|
|
||||||
item.split(/(\[.*?\])/).forEach((item) => {
|
|
||||||
item.split(/(<.*?>)/).forEach((item) => {
|
|
||||||
if (item) {
|
|
||||||
contentArray.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return contentArray.map((item) => {
|
switch (item.type) {
|
||||||
if (item.startsWith('{') && item.endsWith('}')) {
|
case msgContentType.TEXT:
|
||||||
return renderImage(item.slice(1, -1), true)
|
return renderText(item.value)
|
||||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
case msgContentType.EMOJI:
|
||||||
return renderEmoji(item.slice(1, -1))
|
return renderEmoji(item.value)
|
||||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
case msgContentType.SCREENSHOT:
|
||||||
return renderAt(item.slice(1, -1), msgType)
|
return renderImage(item.value, true)
|
||||||
} else if (item.startsWith('「{') && item.endsWith('}」')) {
|
case msgContentType.AT:
|
||||||
return renderQuote(item.slice(1, -1))
|
return renderAt(item.value)
|
||||||
} else {
|
case msgContentType.QUOTE:
|
||||||
return <span>{item.trim()}</span>
|
return renderQuote(item.value, msgId)
|
||||||
|
|
||||||
|
case msgContentType.IMAGE:
|
||||||
|
return renderImage(item.value)
|
||||||
|
case msgContentType.RECORDING:
|
||||||
|
return renderRecording(item.value)
|
||||||
|
case msgContentType.AUDIO:
|
||||||
|
return renderAudio(item.value)
|
||||||
|
case msgContentType.VIDEO:
|
||||||
|
return renderVideo(item.value, msgId)
|
||||||
|
case msgContentType.DOCUMENT:
|
||||||
|
return renderDocument(item.value)
|
||||||
|
case msgContentType.FORWARD_TOGETHER:
|
||||||
|
return renderForwardTogether(item.value, msgId)
|
||||||
|
default:
|
||||||
|
return <span></span>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderText = (content) => {
|
const renderText = (text) => {
|
||||||
return <span>{content}</span>
|
return <span>{text}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderRecording = (content) => {
|
const renderRecording = (audioId) => {
|
||||||
const audioId = content
|
|
||||||
const url = audioData.audio[audioId]?.downloadUrl
|
const url = audioData.audio[audioId]?.downloadUrl
|
||||||
const duration = audioData.audio[audioId]?.duration
|
const duration = audioData.audio[audioId]?.duration
|
||||||
if (url) {
|
if (url) {
|
||||||
return <MsgBoxRecording audioUrl={url} duration={duration}></MsgBoxRecording>
|
return <MsgBoxRecording audioUrl={url} duration={duration}></MsgBoxRecording>
|
||||||
} else {
|
} else {
|
||||||
return <span>{content}</span>
|
return <span>{'[语音]'}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAudio = (content) => {
|
const renderAudio = (audioId) => {
|
||||||
const audioId = content
|
|
||||||
const url = audioData.audio[audioId]?.downloadUrl
|
const url = audioData.audio[audioId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
@@ -232,40 +195,37 @@ const renderAudio = (content) => {
|
|||||||
></MsgBoxAudio>
|
></MsgBoxAudio>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <span>{content}</span>
|
return <span>{`[${audioId}]`}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderEmoji = (content) => {
|
const renderEmoji = (emojiId) => {
|
||||||
const emojiId = `[${content}]`
|
|
||||||
const url = emojis[emojiId]
|
const url = emojis[emojiId]
|
||||||
if (url) {
|
if (url) {
|
||||||
return <img class={'emoji'} src={url} alt={emojiId} title={content}></img>
|
return <img class={'emoji'} src={url} alt={emojiId} title={emojiId.slice(1, -1)}></img>
|
||||||
} else {
|
} else {
|
||||||
return <span>{content}</span>
|
return <span>{emojiId}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderImage = (content, isForMix = false) => {
|
const renderImage = (imgId, isScreenShot = false) => {
|
||||||
const imgId = content
|
|
||||||
imageData.setImageInSession(props.sessionId, imageData.image[imgId])
|
|
||||||
if (imageData.image[imgId]) {
|
if (imageData.image[imgId]) {
|
||||||
|
imageData.setImageInSession(props.sessionId, imageData.image[imgId])
|
||||||
return (
|
return (
|
||||||
<MsgBoxImage
|
<MsgBoxImage
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
imgId={imgId}
|
imgId={imgId}
|
||||||
isForMix={isForMix}
|
isScreenShot={isScreenShot}
|
||||||
thumbWidth={imageData.image[imgId].thumbWidth}
|
thumbWidth={imageData.image[imgId].thumbWidth}
|
||||||
thumbHeight={imageData.image[imgId].thumbHeight}
|
thumbHeight={imageData.image[imgId].thumbHeight}
|
||||||
></MsgBoxImage>
|
></MsgBoxImage>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <span>{isForMix ? content : `[${content}]`}</span>
|
return <span>{`[${imgId}]`}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderVideo = (content, msgId) => {
|
const renderVideo = (videoId, msgId) => {
|
||||||
const videoId = content
|
|
||||||
const url = videoData.video[videoId]?.downloadUrl
|
const url = videoData.video[videoId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
@@ -280,12 +240,11 @@ const renderVideo = (content, msgId) => {
|
|||||||
></MsgBoxVideo>
|
></MsgBoxVideo>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <span>{content}</span>
|
return <span>{`[${videoId}]`}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDocument = (content) => {
|
const renderDocument = (documentId) => {
|
||||||
const documentId = content
|
|
||||||
const url = documentData.document[documentId]?.downloadUrl
|
const url = documentData.document[documentId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
@@ -297,18 +256,42 @@ const renderDocument = (content) => {
|
|||||||
></MsgBoxDocument>
|
></MsgBoxDocument>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <span>{content}</span>
|
return <span>{`[${documentId}]`}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderForwardTogether = (msgId) => {
|
const renderForwardTogether = (forwardContent, msgId) => {
|
||||||
const title = '聊天记录'
|
const msgs = forwardMsgs.value[msgId]
|
||||||
const msgsSorted = msgsFromServer.value[msgId]
|
if (!msgs) {
|
||||||
|
return <div class={'forward-together'}></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardContent(取里面的nickName) 和 msgs合一
|
||||||
|
const newMsgs = {}
|
||||||
|
msgs.forEach((item) => {
|
||||||
|
newMsgs[item.msgId] = item
|
||||||
|
})
|
||||||
|
forwardContent.data.forEach((item) => {
|
||||||
|
if (item.msgId in newMsgs) {
|
||||||
|
newMsgs[item.msgId] = {
|
||||||
|
...newMsgs[item.msgId],
|
||||||
|
...item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const msgsSorted = Object.values(newMsgs).sort((a, b) => {
|
||||||
|
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||||
|
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||||
|
return timeA - timeB
|
||||||
|
})
|
||||||
|
|
||||||
if (!msgsSorted) {
|
if (!msgsSorted) {
|
||||||
return <div class={'forward-together'}></div>
|
return <div class={'forward-together'}></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = '聊天记录'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'forward-together'}
|
class={'forward-together'}
|
||||||
@@ -343,9 +326,7 @@ const renderForwardTogether = (msgId) => {
|
|||||||
<div key={index} class={'msg-item'}>
|
<div key={index} class={'msg-item'}>
|
||||||
<span class={'msg-item-nickname'}>{msg.nickName || msg.fromId}</span>
|
<span class={'msg-item-nickname'}>{msg.nickName || msg.fromId}</span>
|
||||||
<span>{':'}</span>
|
<span>{':'}</span>
|
||||||
<span class={'msg-item-content'}>
|
<span class={'msg-item-content'}>{showSimplifyMsgContent(msg.content)}</span>
|
||||||
{showMsgContentInForwardTogether(msg.content)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -356,108 +337,29 @@ const renderForwardTogether = (msgId) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAt = (content, msgType) => {
|
const renderAt = (atContent) => {
|
||||||
const index = content.indexOf('-')
|
return <span>{`@${atContent.nickName} `}</span>
|
||||||
if (index !== -1) {
|
|
||||||
const nickName = content.slice(index + 1)
|
|
||||||
if (msgType === MsgType.GROUP_CHAT && nickName) {
|
|
||||||
return <span>{`@${nickName} `}</span>
|
|
||||||
} else {
|
|
||||||
return <span>{`<${content}>`}</span>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return <span>{`<${content}>`}</span>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderQuote = (quoteContent) => {
|
const renderQuote = (quoteContent, msgId) => {
|
||||||
const { nickName, content, msgTime } = jsonParseSafe(quoteContent)
|
const { nickName } = quoteContent
|
||||||
let showContent = content || ''
|
const { content, msgTime } = quoteMsg.value[msgId]
|
||||||
if (content) {
|
? quoteMsg.value[msgId]
|
||||||
const defaultContent = content.replace(/<(?:.*?)-(.*?)>/g, '@$1').replace(/\{\d+\}/g, '[图片]')
|
: { content: '', msgTime: '' }
|
||||||
showContent = defaultContent
|
|
||||||
const contentJson = jsonParseSafe(defaultContent)
|
|
||||||
if (contentJson) {
|
|
||||||
const type = contentJson['type']
|
|
||||||
const objectId = contentJson['value']
|
|
||||||
switch (type) {
|
|
||||||
case msgContentType.RECORDING:
|
|
||||||
showContent = '[语音]'
|
|
||||||
break
|
|
||||||
case msgContentType.AUDIO:
|
|
||||||
showContent = `[音频] ${audioData.audio[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.IMAGE:
|
|
||||||
showContent = `[图片] ${imageData.image[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.VIDEO:
|
|
||||||
showContent = `[视频] ${videoData.video[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
showContent = `[文档] ${documentData.document[objectId].fileName}`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 和InputEditor.vue中的结构保持一致,使用相同class可以复用样式
|
// 和InputEditor.vue中的结构保持一致,使用相同class可以复用样式
|
||||||
return (
|
return (
|
||||||
<div class={'quote-block'}>
|
<div class={'quote-block'}>
|
||||||
<div class={'quote-wrapper'}>
|
<div class={'quote-wrapper'}>
|
||||||
<div class={'quote-sender'}>
|
<div class={'quote-sender'}>
|
||||||
<span class="quote-nickName">{nickName + ' '}</span>
|
<span class="quote-nickName">{nickName}</span>
|
||||||
<span class={'quote-msgTime'}>{msgTime + ':'}</span>
|
<span class={'quote-msgTime'}>{` ${showTimeFormat(msgTime)}:`}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class={'quote-content'}>{showContent}</span>
|
<span class={'quote-content'}>{showSimplifyMsgContent(content)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMsgContentInForwardTogether = (content) => {
|
|
||||||
const jsonContent = jsonParseSafe(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 if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
|
||||||
template = '[聊天记录]'
|
|
||||||
} else {
|
|
||||||
template = jsonContent['value']
|
|
||||||
}
|
|
||||||
return template
|
|
||||||
} else {
|
|
||||||
return content
|
|
||||||
.replace(/\{\d+\}/g, '[图片]')
|
|
||||||
.replace(/(「\{.*?\}」)/, '[引用]')
|
|
||||||
.split(/(<.*?>)/)
|
|
||||||
.map((item) => {
|
|
||||||
const sliceStr = item.slice(1, -1)
|
|
||||||
const index = sliceStr.indexOf('-')
|
|
||||||
if (index !== -1) {
|
|
||||||
const nickName = sliceStr.slice(index + 1)
|
|
||||||
if (nickName) {
|
|
||||||
return `@${nickName}`
|
|
||||||
} else {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
emit('update:isShow', false)
|
emit('update:isShow', false)
|
||||||
emit('close')
|
emit('close')
|
||||||
@@ -494,6 +396,7 @@ const onShowUserCard = (account) => {
|
|||||||
class="dialog-msg-list"
|
class="dialog-msg-list"
|
||||||
:model-value="props.isShow"
|
:model-value="props.isShow"
|
||||||
:modal="false"
|
:modal="false"
|
||||||
|
draggable
|
||||||
:width="'600px'"
|
:width="'600px'"
|
||||||
:top="`${30 + (props.tier || 0)}vh`"
|
:top="`${30 + (props.tier || 0)}vh`"
|
||||||
:z-index="1000"
|
:z-index="1000"
|
||||||
|
|||||||
@@ -20,14 +20,35 @@ import { prehandleImage } from '@/js/utils/image'
|
|||||||
import { MsgType } from '@/proto/msg'
|
import { MsgType } from '@/proto/msg'
|
||||||
import AtList from '@/views/message/components/AtList.vue'
|
import AtList from '@/views/message/components/AtList.vue'
|
||||||
import AgreeBeforeSend from '@/views/message/components/AgreeBeforeSend.vue'
|
import AgreeBeforeSend from '@/views/message/components/AgreeBeforeSend.vue'
|
||||||
|
import { isMatchMsgStruct, showSimplifyMsgContent } from '@/js/utils/message'
|
||||||
|
import { msgChatQueryMessagesService } from '@/api/message'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理粘贴格式问题
|
* 处理复制/粘贴结构化数据
|
||||||
*/
|
*/
|
||||||
const Clipboard = Quill.import('modules/clipboard')
|
const Clipboard = Quill.import('modules/clipboard')
|
||||||
class PlainClipboard extends Clipboard {
|
class PlainClipboard extends Clipboard {
|
||||||
onPaste(range, { text }) {
|
onPaste(range, data) {
|
||||||
handlePaste(range, text)
|
if (!data.html) {
|
||||||
|
handlePaste(range, data.text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(data.html, 'text/html')
|
||||||
|
const elements = doc.querySelectorAll('[data-quill-custom]') // 查找所有具有 data-quill-custom 属性的元素
|
||||||
|
if (elements.length > 0) {
|
||||||
|
// 取第一个匹配的元素
|
||||||
|
const encodedData = elements[0].getAttribute('data-quill-custom')
|
||||||
|
const decodedData = decodeURIComponent(encodedData)
|
||||||
|
handlePaste(range, decodedData)
|
||||||
|
} else {
|
||||||
|
handlePaste(range, data.text) // 降级方案
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCopy(range) {
|
||||||
|
return handleCopy(range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Quill.register(
|
Quill.register(
|
||||||
@@ -96,53 +117,21 @@ class QuoteBlock extends Embed {
|
|||||||
super.remove()
|
super.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
static create({ account, nickName, msgId, content, msgTime }) {
|
static create({ account, nickName, msgKey, msgId, content, msgTime }) {
|
||||||
const node = super.create()
|
const node = super.create()
|
||||||
node.dataset.account = account
|
node.dataset.account = account
|
||||||
node.dataset.nickName = nickName
|
node.dataset.nickName = nickName
|
||||||
|
node.dataset.msgKey = msgKey
|
||||||
node.dataset.msgId = msgId
|
node.dataset.msgId = msgId
|
||||||
node.dataset.msgTime = showTimeFormat(msgTime)
|
node.dataset.msgTime = showTimeFormat(msgTime)
|
||||||
node.dataset.content = content
|
node.dataset.content = content
|
||||||
.split(/(「\{.*?\}」)/)
|
|
||||||
.filter((item) => !(item.startsWith('「{') && item.endsWith('}」'))) // 引用的引用不予展示
|
|
||||||
.join('')
|
|
||||||
|
|
||||||
const defaultContent = node.dataset.content
|
|
||||||
.replace(/<(?:.*?)-(.*?)>/g, '@$1')
|
|
||||||
.replace(/\{\d+\}/g, '[图片]')
|
|
||||||
const contentJson = jsonParseSafe(defaultContent)
|
|
||||||
let showContent = defaultContent
|
|
||||||
if (contentJson) {
|
|
||||||
const type = contentJson['type']
|
|
||||||
const objectId = contentJson['value']
|
|
||||||
switch (type) {
|
|
||||||
case msgContentType.RECORDING:
|
|
||||||
showContent = '[语音]'
|
|
||||||
break
|
|
||||||
case msgContentType.AUDIO:
|
|
||||||
showContent = `[音频] ${audioData.audio[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.IMAGE:
|
|
||||||
showContent = `[图片] ${imageData.image[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.VIDEO:
|
|
||||||
showContent = `[视频] ${videoData.video[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
showContent = `[文档] ${documentData.document[objectId].fileName}`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.innerHTML = `
|
node.innerHTML = `
|
||||||
<div class="quote-wrapper">
|
<div class="quote-wrapper">
|
||||||
<div class="quote-sender">
|
<div class="quote-sender">
|
||||||
<span class="quote-nickName">${node.dataset.nickName} </span>
|
<span class="quote-nickName">${node.dataset.nickName}</span>
|
||||||
<span class="quote-msgTime">${node.dataset.msgTime}:</span>
|
<span class="quote-msgTime">${node.dataset.msgTime}:</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="quote-content">${showContent}</span>
|
<span class="quote-content">${showSimplifyMsgContent(node.dataset.content)}</span>
|
||||||
<button type="button" class="quote-close-btn">
|
<button type="button" class="quote-close-btn">
|
||||||
<span >×</span>
|
<span >×</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -155,6 +144,7 @@ class QuoteBlock extends Embed {
|
|||||||
return {
|
return {
|
||||||
account: node.dataset.account,
|
account: node.dataset.account,
|
||||||
nickName: node.dataset.nickName,
|
nickName: node.dataset.nickName,
|
||||||
|
msgKey: node.dataset.msgKey,
|
||||||
msgId: node.dataset.msgId,
|
msgId: node.dataset.msgId,
|
||||||
content: node.dataset.content,
|
content: node.dataset.content,
|
||||||
msgTime: node.dataset.msgTime
|
msgTime: node.dataset.msgTime
|
||||||
@@ -207,8 +197,8 @@ onMounted(async () => {
|
|||||||
toSendAtList.value = []
|
toSendAtList.value = []
|
||||||
// 给组件增加滚动条样式
|
// 给组件增加滚动条样式
|
||||||
document.querySelector('.ql-editor').classList.add('my-scrollbar')
|
document.querySelector('.ql-editor').classList.add('my-scrollbar')
|
||||||
await imageData.loadImageInfoFromContent(props.draft)
|
await imageData.preloadImageFromMsg(props.draft)
|
||||||
renderContent(props.draft) // 渲染草稿
|
await renderContent(props.draft) // 渲染草稿
|
||||||
quill.value.on('composition-start', () => {
|
quill.value.on('composition-start', () => {
|
||||||
// 当用户使用拼音输入法开始输入汉字时,这个事件就会被触发
|
// 当用户使用拼音输入法开始输入汉字时,这个事件就会被触发
|
||||||
quill.value.root.dataset.placeholder = ''
|
quill.value.root.dataset.placeholder = ''
|
||||||
@@ -333,10 +323,10 @@ onBeforeUnmount(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(contentObj.contentFromLocal.join('').trim())
|
fn(JSON.stringify(contentObj.contentFromLocal.filter((item) => item)))
|
||||||
|
|
||||||
callbacks.allUploadedSuccessFn = () => {
|
callbacks.allUploadedSuccessFn = () => {
|
||||||
fn(contentObj.contentFromServer.join('').trim())
|
fn(JSON.stringify(contentObj.contentFromServer.filter((item) => item)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -383,8 +373,8 @@ const cursorProtectForQuote = () => {
|
|||||||
*/
|
*/
|
||||||
const parseContent = async (callbacks) => {
|
const parseContent = async (callbacks) => {
|
||||||
const delta = quill.value.getContents()
|
const delta = quill.value.getContents()
|
||||||
let contentFromLocal = new Array(delta.ops.length).fill('')
|
let contentFromLocal = new Array(delta.ops.length).fill('') // 这里用new Array + index填充方式,而不用push,是为了保证内容的顺序
|
||||||
let contentFromServer = new Array(delta.ops.length).fill('')
|
let contentFromServer = new Array(delta.ops.length).fill('') // contentFromServer更新了某些服务端返回的数据
|
||||||
let needUploadCount = 0 // 需要上传的图片个数
|
let needUploadCount = 0 // 需要上传的图片个数
|
||||||
let uploadedTotalCount = 0 // 已发上传请求的图片个数,包括上传成功和失败
|
let uploadedTotalCount = 0 // 已发上传请求的图片个数,包括上传成功和失败
|
||||||
let uploadSuccessCount = 0 // 已经上传成功的图片个数
|
let uploadSuccessCount = 0 // 已经上传成功的图片个数
|
||||||
@@ -405,39 +395,64 @@ const parseContent = async (callbacks) => {
|
|||||||
const insert = op.insert
|
const insert = op.insert
|
||||||
if (insert && typeof insert === 'string') {
|
if (insert && typeof insert === 'string') {
|
||||||
// 文本
|
// 文本
|
||||||
contentFromLocal[index] = insert
|
let contentText = {}
|
||||||
contentFromServer[index] = insert
|
if (index === delta.ops.length - 1) {
|
||||||
|
const lastInsert = insert.endsWith('\n') ? insert.slice(0, -1) : insert // 去除最后一个换行符
|
||||||
|
if (lastInsert) {
|
||||||
|
contentText = {
|
||||||
|
type: msgContentType.TEXT,
|
||||||
|
value: lastInsert
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentText = {
|
||||||
|
type: msgContentType.TEXT,
|
||||||
|
value: insert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentFromLocal[index] = contentText
|
||||||
|
contentFromServer[index] = contentText
|
||||||
} else if (insert && insert.atMention) {
|
} else if (insert && insert.atMention) {
|
||||||
// 处理用于@的自定义Blot
|
// 处理用于@的自定义Blot
|
||||||
const { account, nickName } = insert.atMention
|
const { account, nickName } = insert.atMention
|
||||||
contentFromLocal[index] = `<${account}-${nickName}>`
|
const contentAt = { type: msgContentType.AT, value: { account, nickName } }
|
||||||
contentFromServer[index] = `<${account}-${nickName}>`
|
contentFromLocal[index] = contentAt
|
||||||
|
contentFromServer[index] = contentAt
|
||||||
} else if (insert && insert.quoteBlock) {
|
} else if (insert && insert.quoteBlock) {
|
||||||
// 处理用于引用的自定义Blot
|
// 处理用于引用的自定义Blot
|
||||||
const quoteContent = JSON.stringify({
|
contentFromLocal[index] = {
|
||||||
account: insert.quoteBlock.account,
|
type: msgContentType.QUOTE,
|
||||||
nickName: insert.quoteBlock.nickName,
|
value: {
|
||||||
msgId: insert.quoteBlock.msgId,
|
nickName: insert.quoteBlock.nickName,
|
||||||
content: insert.quoteBlock.content,
|
msgId: insert.quoteBlock.msgKey // 注意这里的区别
|
||||||
msgTime: insert.quoteBlock.msgTime
|
}
|
||||||
})
|
}
|
||||||
contentFromLocal[index] = `「${quoteContent}」`
|
contentFromServer[index] = {
|
||||||
contentFromServer[index] = `「${quoteContent}」`
|
type: msgContentType.QUOTE,
|
||||||
|
value: {
|
||||||
|
nickName: insert.quoteBlock.nickName,
|
||||||
|
msgId: insert.quoteBlock.msgId // 注意这里的区别
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (insert && insert.image) {
|
} else if (insert && insert.image) {
|
||||||
const alt = op.attributes?.alt
|
const alt = op.attributes?.alt
|
||||||
if (alt && alt.startsWith('[') && alt.endsWith(']')) {
|
if (alt && alt.startsWith('[') && alt.endsWith(']')) {
|
||||||
// 表情id
|
// 表情id
|
||||||
contentFromLocal[index] = alt
|
const contentEmoji = { type: msgContentType.EMOJI, value: alt }
|
||||||
contentFromServer[index] = alt
|
contentFromLocal[index] = contentEmoji
|
||||||
|
contentFromServer[index] = contentEmoji
|
||||||
} else if (alt && alt.startsWith('{') && alt.endsWith('}')) {
|
} else if (alt && alt.startsWith('{') && alt.endsWith('}')) {
|
||||||
// 图片id
|
// 已有objectId的截图,说明是已上传过服务端的
|
||||||
contentFromLocal[index] = alt
|
const contentSceenShot = { type: msgContentType.SCREENSHOT, value: alt.slice(1, -1) }
|
||||||
contentFromServer[index] = alt
|
contentFromLocal[index] = contentSceenShot
|
||||||
|
contentFromServer[index] = contentSceenShot
|
||||||
} else if (insert.image.startsWith('data:') && insert.image.includes('base64')) {
|
} else if (insert.image.startsWith('data:') && insert.image.includes('base64')) {
|
||||||
// base64编码的图片
|
// 截图的原始base64编码的图片
|
||||||
const file = base64ToFile(insert.image, uuidv4()) // base64转file
|
const file = base64ToFile(insert.image, uuidv4()) // base64转file
|
||||||
const tempObjectId = new Date().getTime()
|
const tempObjectId = new Date().getTime()
|
||||||
contentFromLocal[index] = `{${tempObjectId}}`
|
contentFromLocal[index] = { type: msgContentType.SCREENSHOT, value: tempObjectId }
|
||||||
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||||
const md5 = await getMd5(file)
|
const md5 = await getMd5(file)
|
||||||
const prehandleImageObj = await prehandleImage(file)
|
const prehandleImageObj = await prehandleImage(file)
|
||||||
@@ -473,7 +488,10 @@ const parseContent = async (callbacks) => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
imageData.setImage(res.data.data) // 缓存image数据
|
imageData.setImage(res.data.data) // 缓存image数据
|
||||||
uploadSuccessCount++
|
uploadSuccessCount++
|
||||||
contentFromServer[index] = `{${res.data.data.objectId}}`
|
contentFromServer[index] = {
|
||||||
|
type: msgContentType.SCREENSHOT,
|
||||||
|
value: res.data.data.objectId
|
||||||
|
}
|
||||||
callbacks.someOneUploadedSuccessFn()
|
callbacks.someOneUploadedSuccessFn()
|
||||||
if (uploadSuccessCount === needUploadCount) {
|
if (uploadSuccessCount === needUploadCount) {
|
||||||
callbacks.allUploadedSuccessFn()
|
callbacks.allUploadedSuccessFn()
|
||||||
@@ -487,8 +505,9 @@ const parseContent = async (callbacks) => {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 当文本处理
|
// 当文本处理
|
||||||
contentFromLocal[index] = insert
|
const contentText = { type: msgContentType.TEXT, value: insert }
|
||||||
contentFromServer[index] = insert
|
contentFromLocal[index] = contentText
|
||||||
|
contentFromServer[index] = contentText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,21 +544,20 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
callbacks.allUploadedSuccessFn = () => {
|
callbacks.allUploadedSuccessFn = () => {
|
||||||
fn(contentObj.contentFromServer.join('').trim())
|
// JSON.stringify(contentObj.contentFromServer.filter((item) => item))在空值时返回'[]''
|
||||||
|
let inputContent = JSON.stringify(contentObj.contentFromServer.filter((item) => item))
|
||||||
|
fn(inputContent === '[]' ? '' : inputContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(contentObj.contentFromLocal.join('').trim())
|
// JSON.stringify(contentObj.contentFromLocal.filter((item) => item))在空值时返回'[]''
|
||||||
|
let inputContent = JSON.stringify(contentObj.contentFromLocal.filter((item) => item))
|
||||||
|
fn(inputContent === '[]' ? '' : inputContent)
|
||||||
|
|
||||||
renderContent(messageData.sessionList[newSessionId].draft || '') // 切换session时渲染新session的草稿
|
await renderContent(messageData.sessionList[newSessionId].draft || '') // 切换session时渲染新session的草稿
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 实现消息复制的效果,步骤如下
|
|
||||||
// 1. 拷贝原消息中的content内容
|
|
||||||
// 2. 粘贴时自动调用renderContent渲染内容
|
|
||||||
// 3. 渲染时保存复制内容
|
|
||||||
// 4. 发送时使用保存的复制内容
|
|
||||||
const pasteObj = {
|
const pasteObj = {
|
||||||
content: null,
|
content: null,
|
||||||
contentType: null,
|
contentType: null,
|
||||||
@@ -556,161 +574,235 @@ const clearPasteObj = () => {
|
|||||||
pasteObj.url = null
|
pasteObj.url = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = (range, content) => {
|
/**
|
||||||
if (!content) {
|
* 处理复制
|
||||||
|
*/
|
||||||
|
const handleCopy = ({ index, length }) => {
|
||||||
|
const delta = quill.value.getContents(index, length)
|
||||||
|
|
||||||
|
const clipboardContent = []
|
||||||
|
let clipboardText = ''
|
||||||
|
|
||||||
|
for (let index = 0; index < delta.ops.length; index++) {
|
||||||
|
const op = delta.ops[index]
|
||||||
|
const insert = op.insert
|
||||||
|
if (insert && typeof insert === 'string') {
|
||||||
|
// 文本
|
||||||
|
clipboardContent.push({
|
||||||
|
type: msgContentType.TEXT,
|
||||||
|
value: insert
|
||||||
|
})
|
||||||
|
clipboardText += insert
|
||||||
|
} else if (insert && insert.image) {
|
||||||
|
const alt = op.attributes?.alt
|
||||||
|
if (alt && alt.startsWith('[') && alt.endsWith(']')) {
|
||||||
|
// 表情
|
||||||
|
clipboardContent.push({ type: msgContentType.EMOJI, value: alt })
|
||||||
|
} else if (alt && alt.startsWith('{') && alt.endsWith('}')) {
|
||||||
|
// 已有objectId的截图,复制原消息粘贴的,撤回重新编辑,从草稿渲染
|
||||||
|
clipboardContent.push({ type: msgContentType.SCREENSHOT, value: alt.slice(1, -1) })
|
||||||
|
} else if (insert.image.startsWith('data:') && insert.image.includes('base64')) {
|
||||||
|
// 截图后原始的base64编码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 在html自定义属性data-quill-custom中传递clipboardContent结构化数据
|
||||||
|
html: `<div data-quill-custom=${encodeURIComponent(JSON.stringify(clipboardContent))}></div>`,
|
||||||
|
text: clipboardText // 纯文本
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理粘贴
|
||||||
|
* @param range
|
||||||
|
*/
|
||||||
|
const handlePaste = (range, text) => {
|
||||||
|
if (!text) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonContent = jsonParseSafe(content)
|
if (!isMatchMsgStruct(text)) {
|
||||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
const delta = new Delta().retain(range.index).delete(range.length).insert(text)
|
||||||
clearPasteObj()
|
quill.value.updateContents(delta, Quill.sources.USER)
|
||||||
pasteObj.content = content
|
quill.value.setSelection(delta.length() - range.length, Quill.sources.USER)
|
||||||
pasteObj.contentType = jsonContent['type']
|
return
|
||||||
const fileId = jsonContent['value']
|
|
||||||
switch (pasteObj.contentType) {
|
|
||||||
case msgContentType.IMAGE:
|
|
||||||
pasteObj.fileName = imageData.image[fileId]?.fileName
|
|
||||||
pasteObj.fileSize = imageData.image[fileId]?.size
|
|
||||||
pasteObj.url = imageData.image[fileId]?.thumbUrl
|
|
||||||
break
|
|
||||||
case msgContentType.AUDIO:
|
|
||||||
pasteObj.fileName = audioData.audio[fileId]?.fileName
|
|
||||||
pasteObj.fileSize = audioData.audio[fileId]?.size
|
|
||||||
break
|
|
||||||
case msgContentType.VIDEO:
|
|
||||||
pasteObj.fileName = videoData.video[fileId]?.fileName
|
|
||||||
pasteObj.fileSize = videoData.video[fileId]?.size
|
|
||||||
break
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
pasteObj.fileName = documentData.document[fileId]?.fileName
|
|
||||||
pasteObj.fileSize = documentData.document[fileId]?.size
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件确实存在才发送
|
|
||||||
if (pasteObj.fileName) {
|
|
||||||
showAgreeDialog.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const delta = new Delta().retain(range.index).delete(range.length).insert(content)
|
const delta = new Delta().retain(range.index).delete(range.length)
|
||||||
|
|
||||||
|
const arr = jsonParseSafe(text)
|
||||||
|
for (const item of arr) {
|
||||||
|
const { type, value } = item
|
||||||
|
if (
|
||||||
|
type === msgContentType.IMAGE ||
|
||||||
|
type === msgContentType.AUDIO ||
|
||||||
|
type === msgContentType.VIDEO ||
|
||||||
|
type === msgContentType.DOCUMENT
|
||||||
|
) {
|
||||||
|
clearPasteObj()
|
||||||
|
pasteObj.content = item
|
||||||
|
pasteObj.contentType = type
|
||||||
|
const fileId = value
|
||||||
|
switch (type) {
|
||||||
|
case msgContentType.IMAGE:
|
||||||
|
pasteObj.fileName = imageData.image[fileId]?.fileName
|
||||||
|
pasteObj.fileSize = imageData.image[fileId]?.size
|
||||||
|
pasteObj.url = imageData.image[fileId]?.thumbUrl
|
||||||
|
break
|
||||||
|
case msgContentType.AUDIO:
|
||||||
|
pasteObj.fileName = audioData.audio[fileId]?.fileName
|
||||||
|
pasteObj.fileSize = audioData.audio[fileId]?.size
|
||||||
|
break
|
||||||
|
case msgContentType.VIDEO:
|
||||||
|
pasteObj.fileName = videoData.video[fileId]?.fileName
|
||||||
|
pasteObj.fileSize = videoData.video[fileId]?.size
|
||||||
|
break
|
||||||
|
case msgContentType.DOCUMENT:
|
||||||
|
pasteObj.fileName = documentData.document[fileId]?.fileName
|
||||||
|
pasteObj.fileSize = documentData.document[fileId]?.size
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件确实存在才发送
|
||||||
|
if (pasteObj.fileName) {
|
||||||
|
showAgreeDialog.value = true
|
||||||
|
return // 这四种类型的数组只能有1个元素,所以直接return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (type) {
|
||||||
|
case msgContentType.TEXT:
|
||||||
|
delta.insert(value)
|
||||||
|
break
|
||||||
|
case msgContentType.EMOJI: {
|
||||||
|
const emojiUrl = emojis[value]
|
||||||
|
if (emojiUrl) {
|
||||||
|
delta.insert({ image: emojiUrl }, { alt: value })
|
||||||
|
} else {
|
||||||
|
delta.insert(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case msgContentType.SCREENSHOT: {
|
||||||
|
const imageUrl = imageData.image[value]?.originUrl
|
||||||
|
if (imageUrl) {
|
||||||
|
delta.insert({ image: imageUrl }, { alt: `{${value}}` }) // 添加区别于emoji表情alt的符号,方便parse时识别
|
||||||
|
} else {
|
||||||
|
delta.insert(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case msgContentType.AT: {
|
||||||
|
const { account, nickName } = value
|
||||||
|
toSendAtList.value.push(account)
|
||||||
|
delta.insert({ atMention: { account, nickName } })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case msgContentType.QUOTE:
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
quill.value.updateContents(delta, Quill.sources.USER)
|
quill.value.updateContents(delta, Quill.sources.USER)
|
||||||
quill.value.setSelection(delta.length() - range.length, Quill.sources.USER)
|
quill.value.setSelection(delta.length() - range.length, Quill.sources.USER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把输入框的字符串内容渲染成富媒体内容
|
* 输入框从空状态渲染可视内容
|
||||||
* @param content 字符串内容
|
* 1. 渲染草稿
|
||||||
|
* 2. 消息撤回后的重新编辑
|
||||||
|
* @param content json结构化内容的字符串
|
||||||
*/
|
*/
|
||||||
const renderContent = (content) => {
|
const renderContent = async (content) => {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
quill.value.setText('')
|
quill.value.setText('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentArray = []
|
const arr = jsonParseSafe(content)
|
||||||
// 先匹配quote引用内容
|
// 不允许非结构化的content
|
||||||
content.split(/(「\{.*?\}」)/).forEach((item) => {
|
if (!arr) {
|
||||||
if (item.startsWith('「{') && item.endsWith('}」')) {
|
quill.value.setText('')
|
||||||
// quote引用内容直接添加如数组
|
return
|
||||||
contentArray.push(item)
|
}
|
||||||
} else {
|
|
||||||
//匹配内容中的图片
|
|
||||||
item.split(/(\{\d+\})/).forEach((item) => {
|
|
||||||
//匹配内容中的表情
|
|
||||||
item.split(/(\[.*?\])/).forEach((item) => {
|
|
||||||
//匹配内容中的@
|
|
||||||
item.split(/(<.*?>)/).forEach((item) => {
|
|
||||||
if (item) {
|
|
||||||
contentArray.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建一个新的 Delta 对象
|
// 创建一个新的 Delta 对象
|
||||||
const delta = new Delta()
|
const delta = new Delta()
|
||||||
contentArray.map((item) => {
|
for (const item of arr) {
|
||||||
if (item.startsWith('{') && item.endsWith('}')) {
|
if (!item.type || !item.value) {
|
||||||
const imageId = item.slice(1, -1)
|
delta.insert('')
|
||||||
const imageUrl = imageData.image[imageId]?.originUrl
|
}
|
||||||
if (imageUrl) {
|
|
||||||
delta.insert({ image: imageUrl }, { alt: item })
|
switch (item.type) {
|
||||||
} else {
|
case msgContentType.TEXT:
|
||||||
delta.insert(item)
|
delta.insert(item.value)
|
||||||
}
|
break
|
||||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
case msgContentType.EMOJI: {
|
||||||
const emojiUrl = emojis[item]
|
const emojiUrl = emojis[item.value]
|
||||||
if (emojiUrl) {
|
if (emojiUrl) {
|
||||||
delta.insert({ image: emojiUrl }, { alt: item })
|
delta.insert({ image: emojiUrl }, { alt: item.value })
|
||||||
} else {
|
|
||||||
delta.insert(item)
|
|
||||||
}
|
|
||||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
|
||||||
const content = item.slice(1, -1)
|
|
||||||
const index = content.indexOf('-')
|
|
||||||
if (index !== -1) {
|
|
||||||
const account = content.slice(0, index)
|
|
||||||
const nickName = content.slice(index + 1)
|
|
||||||
if (nickName) {
|
|
||||||
toSendAtList.value.push(account)
|
|
||||||
delta.insert({ atMention: { account, nickName } })
|
|
||||||
} else {
|
} else {
|
||||||
delta.insert(item)
|
delta.insert(item.value)
|
||||||
}
|
}
|
||||||
} else {
|
break
|
||||||
delta.insert(item)
|
|
||||||
}
|
}
|
||||||
} else if (item.startsWith('「{') && item.endsWith('}」')) {
|
case msgContentType.SCREENSHOT: {
|
||||||
const quoteContent = item.slice(1, -1)
|
const imageUrl = imageData.image[item.value]?.originUrl
|
||||||
const { account, nickName, msgId, content, msgTime } = jsonParseSafe(quoteContent)
|
if (imageUrl) {
|
||||||
let showContent = content || ''
|
delta.insert({ image: imageUrl }, { alt: `{${item.value}}` }) // 添加区别于emoji表情alt的符号,方便parse时识别
|
||||||
if (content) {
|
} else {
|
||||||
const defaultContent = content
|
delta.insert(item.value)
|
||||||
.replace(/<(?:.*?)-(.*?)>/g, '@$1')
|
}
|
||||||
.replace(/\{\d+\}/g, '[图片]')
|
break
|
||||||
showContent = defaultContent
|
}
|
||||||
const contentJson = jsonParseSafe(defaultContent)
|
case msgContentType.AT: {
|
||||||
if (contentJson) {
|
const { account, nickName } = item.value
|
||||||
const type = contentJson['type']
|
toSendAtList.value.push(account)
|
||||||
const objectId = contentJson['value']
|
delta.insert({ atMention: { account, nickName } })
|
||||||
switch (type) {
|
break
|
||||||
case msgContentType.RECORDING:
|
}
|
||||||
showContent = '[语音]'
|
case msgContentType.QUOTE: {
|
||||||
break
|
// 先从本地消息缓存中获取
|
||||||
case msgContentType.AUDIO:
|
let msg = messageData.getMsg(props.sessionId, item.value.msgId)
|
||||||
showContent = `[音频] ${audioData.audio[objectId].fileName}`
|
if (!msg) {
|
||||||
break
|
// 如果本地消息缓存中没有,再去服务器查询
|
||||||
case msgContentType.IMAGE:
|
const res = await msgChatQueryMessagesService({
|
||||||
showContent = `[图片] ${imageData.image[objectId].fileName}`
|
sessionId: props.sessionId,
|
||||||
break
|
msgIds: [item.value.msgId]
|
||||||
case msgContentType.VIDEO:
|
})
|
||||||
showContent = `[视频] ${videoData.video[objectId].fileName}`
|
|
||||||
break
|
if (res.data.data && res.data.data.length > 0) {
|
||||||
case msgContentType.DOCUMENT:
|
msg = res.data.data[0]
|
||||||
showContent = `[文档] ${documentData.document[objectId].fileName}`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delta.insert({
|
||||||
|
quoteBlock: {
|
||||||
|
account: msg.fromId,
|
||||||
|
nickName: item.value.nickName,
|
||||||
|
msgId: msg.msgId,
|
||||||
|
content: showSimplifyMsgContent(msg.content),
|
||||||
|
msgTime: msg.msgTime
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
}
|
}
|
||||||
delta.insert({ quoteBlock: { account, nickName, msgId, content: showContent, msgTime } })
|
default:
|
||||||
} else {
|
delta.insert('')
|
||||||
delta.insert(item)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
quill.value.setText('') // 清空编辑器内容
|
quill.value.setText('') // 清空编辑器内容
|
||||||
quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容
|
quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容
|
||||||
quill.value.setSelection(quill.value.getLength(), 0, Quill.sources.USER) // 设置光标位置
|
quill.value.setSelection(quill.value.getLength(), 0, Quill.sources.USER) // 设置光标位置
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Enter发送
|
||||||
|
*/
|
||||||
const handleEnter = async () => {
|
const handleEnter = async () => {
|
||||||
if (isShowAtList.value) {
|
if (isShowAtList.value) {
|
||||||
return
|
return
|
||||||
@@ -726,12 +818,22 @@ const handleEnter = async () => {
|
|||||||
? { contentFromLocal: [pasteObj.content], contentFromServer: [pasteObj.content] }
|
? { contentFromLocal: [pasteObj.content], contentFromServer: [pasteObj.content] }
|
||||||
: await parseContent(callbacks)
|
: await parseContent(callbacks)
|
||||||
|
|
||||||
const content = contentObj.contentFromLocal.join('').trim()
|
let textLength = 0
|
||||||
if (!content) {
|
let contentType = 0
|
||||||
|
contentObj.contentFromLocal.forEach((item) => {
|
||||||
|
if (item.type === msgContentType.TEXT) {
|
||||||
|
textLength = textLength + item.value.length
|
||||||
|
}
|
||||||
|
contentType = contentType | item.type
|
||||||
|
})
|
||||||
|
|
||||||
|
if (contentType === 0) {
|
||||||
ElMessage.warning('请勿发送空内容')
|
ElMessage.warning('请勿发送空内容')
|
||||||
quill.value.setText('')
|
quill.value.setText('')
|
||||||
return
|
return
|
||||||
} else if (content.length > 3000) {
|
}
|
||||||
|
|
||||||
|
if (textLength > 3000) {
|
||||||
ElMessage.warning('发送内容请不要超过3000个字')
|
ElMessage.warning('发送内容请不要超过3000个字')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -739,8 +841,8 @@ const handleEnter = async () => {
|
|||||||
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||||
let msg = {}
|
let msg = {}
|
||||||
emit('saveLocalMsg', {
|
emit('saveLocalMsg', {
|
||||||
contentType: msgContentType.MIX,
|
contentType: contentType,
|
||||||
content: content,
|
content: JSON.stringify(contentObj.contentFromLocal.filter((item) => item)),
|
||||||
fn: (result) => {
|
fn: (result) => {
|
||||||
msg = result
|
msg = result
|
||||||
}
|
}
|
||||||
@@ -752,7 +854,8 @@ const handleEnter = async () => {
|
|||||||
uploadProgress: 0
|
uploadProgress: 0
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
emit('sendMessage', { msg, at: toSendAtList.value })
|
const content = JSON.stringify(contentObj.contentFromServer.filter((item) => item))
|
||||||
|
emit('sendMessage', { msg, content, at: toSendAtList.value }) // content 要更新后发给服务端,和saveLocalMsg的本地消息由些许差异
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback:每成功上传一个图片,更新一下进度
|
// callback:每成功上传一个图片,更新一下进度
|
||||||
@@ -771,14 +874,14 @@ const handleEnter = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// callback:所有图片均上传,则发送消息
|
// callback:所有图片均上传,则发送消息
|
||||||
const atTargets = toSendAtList.value
|
const atTargets = toSendAtList.value // 异步函数里避免调用响应式数据
|
||||||
callbacks.allUploadedSuccessFn = () => {
|
callbacks.allUploadedSuccessFn = () => {
|
||||||
messageData.updateMsg(msg.sessionId, msg.msgId, {
|
messageData.updateMsg(msg.sessionId, msg.msgId, {
|
||||||
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
||||||
uploadProgress: 100
|
uploadProgress: 100
|
||||||
})
|
})
|
||||||
msg.content = contentObj.contentFromServer.join('').trim()
|
const content = JSON.stringify(contentObj.contentFromServer.filter((item) => item))
|
||||||
emit('sendMessage', { msg, atTargets })
|
emit('sendMessage', { msg, content, at: atTargets })
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPasteObj()
|
clearPasteObj()
|
||||||
@@ -849,13 +952,13 @@ const onSelectedAtTarget = ({ account, nickName }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reeditFromRevoke = (content) => {
|
const reeditFromRevoke = async (content) => {
|
||||||
quill.value.setText('') // 清空编辑器内容
|
quill.value.setText('') // 清空编辑器内容
|
||||||
quill.value.setSelection(0, 0, Quill.sources.SILENT) // 设置光标位置
|
quill.value.setSelection(0, 0, Quill.sources.SILENT) // 设置光标位置
|
||||||
renderContent(content)
|
await renderContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertQuote = ({ account, nickName, msgId, content, msgTime }) => {
|
const insertQuote = ({ account, nickName, msgKey, msgId, content, msgTime }) => {
|
||||||
// 1. 保存原始选择范围
|
// 1. 保存原始选择范围
|
||||||
quill.value.focus() // 先使 Quill 编辑器获取焦点,否则无法获取Selection
|
quill.value.focus() // 先使 Quill 编辑器获取焦点,否则无法获取Selection
|
||||||
const originalRange = quill.value.getSelection()
|
const originalRange = quill.value.getSelection()
|
||||||
@@ -874,7 +977,7 @@ const insertQuote = ({ account, nickName, msgId, content, msgTime }) => {
|
|||||||
quill.value.insertEmbed(
|
quill.value.insertEmbed(
|
||||||
0,
|
0,
|
||||||
'quoteBlock',
|
'quoteBlock',
|
||||||
{ account, nickName, msgId, content, msgTime },
|
{ account, nickName, msgKey, msgId, content, msgTime },
|
||||||
Quill.sources.USER
|
Quill.sources.USER
|
||||||
)
|
)
|
||||||
quill.value.insertText(1, '\n', Quill.sources.SILENT)
|
quill.value.insertText(1, '\n', Quill.sources.SILENT)
|
||||||
@@ -993,6 +1096,7 @@ img {
|
|||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: gray;
|
color: gray;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-content {
|
.quote-content {
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ const uploadRecord = async () => {
|
|||||||
})
|
})
|
||||||
let msg = {}
|
let msg = {}
|
||||||
emit('saveLocalMsg', {
|
emit('saveLocalMsg', {
|
||||||
|
content: JSON.stringify([{ type: msgContentType.RECORDING, value: tempObjectId }]),
|
||||||
contentType: msgContentType.RECORDING,
|
contentType: msgContentType.RECORDING,
|
||||||
objectId: tempObjectId,
|
|
||||||
fn: (result) => {
|
fn: (result) => {
|
||||||
msg = result
|
msg = result
|
||||||
}
|
}
|
||||||
@@ -194,11 +194,13 @@ const uploadRecord = async () => {
|
|||||||
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
||||||
uploadProgress: 100
|
uploadProgress: 100
|
||||||
})
|
})
|
||||||
msg.content = JSON.stringify({
|
const content = JSON.stringify([
|
||||||
type: msgContentType.RECORDING,
|
{
|
||||||
value: res.data.data.objectId
|
type: msgContentType.RECORDING,
|
||||||
})
|
value: res.data.data.objectId
|
||||||
emit('sendMessage', { msg })
|
}
|
||||||
|
])
|
||||||
|
emit('sendMessage', { msg, content })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ const onConfirmSendFile = () => {
|
|||||||
// 写本地消息
|
// 写本地消息
|
||||||
let msg = {}
|
let msg = {}
|
||||||
emit('saveLocalMsg', {
|
emit('saveLocalMsg', {
|
||||||
|
content: JSON.stringify([{ type: contentType, value: selectedFile.uid }]),
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
objectId: selectedFile.uid,
|
|
||||||
fn: (result) => {
|
fn: (result) => {
|
||||||
msg = result
|
msg = result
|
||||||
}
|
}
|
||||||
@@ -134,8 +134,8 @@ const onConfirmSendFile = () => {
|
|||||||
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
uploadStatus: msgFileUploadStatus.UPLOAD_SUCCESS,
|
||||||
uploadProgress: 100
|
uploadProgress: 100
|
||||||
})
|
})
|
||||||
msg.content = JSON.stringify({ type: contentType, value: res.data.data.objectId })
|
const content = JSON.stringify([{ type: contentType, value: res.data.data.objectId }])
|
||||||
emit('sendMessage', { msg }) // 上传完成后发网络消息
|
emit('sendMessage', { msg, content }) // 上传完成后发网络消息
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ defineExpose({
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-shadow: 2px 2px 20px gray;
|
box-shadow: 2px 2px 20px gray;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import CopyIcon from '@/assets/svg/copy.svg'
|
|||||||
import MultiselectIcon from '@/assets/svg/multiselect.svg'
|
import MultiselectIcon from '@/assets/svg/multiselect.svg'
|
||||||
import RevokeIcon from '@/assets/svg/revoke.svg'
|
import RevokeIcon from '@/assets/svg/revoke.svg'
|
||||||
import { useUserStore, useMenuStore } from '@/stores'
|
import { useUserStore, useMenuStore } from '@/stores'
|
||||||
import { jsonParseSafe } from '@/js/utils/common'
|
|
||||||
import { MSG_REVOKE_TIME_LIMIT, msgContentType, msgSendStatus } from '@/const/msgConst'
|
import { MSG_REVOKE_TIME_LIMIT, msgContentType, msgSendStatus } from '@/const/msgConst'
|
||||||
|
|
||||||
const props = defineProps(['msg'])
|
const props = defineProps(['msg'])
|
||||||
@@ -26,17 +25,7 @@ const myAccount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const contentType = computed(() => {
|
const contentType = computed(() => {
|
||||||
const contentJson = jsonParseSafe(props.msg.content)
|
return props.msg.contentType
|
||||||
if (!contentJson) {
|
|
||||||
return msgContentType.MIX
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = contentJson['type']
|
|
||||||
if (!type) {
|
|
||||||
return msgContentType.MIX
|
|
||||||
} else {
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const msgStatus = computed(() => {
|
const msgStatus = computed(() => {
|
||||||
@@ -53,7 +42,10 @@ const menu = computed(() => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (contentType.value !== msgContentType.RECORDING) {
|
if (
|
||||||
|
contentType.value !== msgContentType.RECORDING &&
|
||||||
|
contentType.value !== msgContentType.FORWARD_TOGETHER
|
||||||
|
) {
|
||||||
o.push({
|
o.push({
|
||||||
label: 'copy',
|
label: 'copy',
|
||||||
desc: '复制',
|
desc: '复制',
|
||||||
@@ -199,6 +191,7 @@ const handleClick = (item) => {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-shadow: 2px 2px 20px gray;
|
box-shadow: 2px 2px 20px gray;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ const handleClick = (item) => {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-shadow: 2px 2px 20px gray;
|
box-shadow: 2px 2px 20px gray;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const handleClick = (item) => {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
box-shadow: 2px 2px 20px gray;
|
box-shadow: 2px 2px 20px gray;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
msgChatRevokeMsgService
|
msgChatRevokeMsgService
|
||||||
} from '@/api/message'
|
} from '@/api/message'
|
||||||
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
||||||
|
import { showSimplifyMsgContent } from '@/js/utils/message'
|
||||||
|
|
||||||
const props = defineProps([
|
const props = defineProps([
|
||||||
'sessionId',
|
'sessionId',
|
||||||
@@ -65,18 +66,76 @@ const audioData = useAudioStore()
|
|||||||
const videoData = useVideoStore()
|
const videoData = useVideoStore()
|
||||||
const documentData = useDocumentStore()
|
const documentData = useDocumentStore()
|
||||||
|
|
||||||
|
const forwardMsgs = ref([])
|
||||||
|
const quoteMsg = ref({})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await rendering()
|
await loadRelatedMsg()
|
||||||
|
rendering()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载和本消息相关的消息:引用消息,合并转发消息
|
||||||
|
*/
|
||||||
|
const loadRelatedMsg = async () => {
|
||||||
|
const arr = jsonParseSafe(msg.value.content)
|
||||||
|
if (!arr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item.type === msgContentType.QUOTE) {
|
||||||
|
// 先从本地消息缓存中获取
|
||||||
|
const msg = messageData.getMsg(props.sessionId, item.value.msgId)
|
||||||
|
if (!msg.msgId) {
|
||||||
|
// 如果本地消息缓存中没有,再去服务器查询
|
||||||
|
const res = await msgChatQueryMessagesService({
|
||||||
|
sessionId: props.sessionId,
|
||||||
|
msgIds: item.value.msgId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.data && res.data.data.length > 0) {
|
||||||
|
quoteMsg.value = res.data.data[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quoteMsg.value = msg
|
||||||
|
}
|
||||||
|
} else if (item.type === msgContentType.FORWARD_TOGETHER) {
|
||||||
|
const forwatdMsgIds = item.value.data.map((item) => item.msgId)
|
||||||
|
|
||||||
|
const toQueryMsgIds = []
|
||||||
|
for (const msgId of forwatdMsgIds) {
|
||||||
|
// 先从本地消息缓存中获取
|
||||||
|
const msg = messageData.getMsg(props.sessionId, msgId)
|
||||||
|
if (!msg.msgId) {
|
||||||
|
// 如果本地消息缓存中没有,再去服务器查询
|
||||||
|
toQueryMsgIds.push(msgId)
|
||||||
|
} else {
|
||||||
|
forwardMsgs.value.push(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toQueryMsgIds.length > 0) {
|
||||||
|
const res = await msgChatQueryMessagesService({
|
||||||
|
sessionId: item.value.sessionId,
|
||||||
|
msgIds: toQueryMsgIds.join(',')
|
||||||
|
})
|
||||||
|
res.data.data.forEach((item) => {
|
||||||
|
forwardMsgs.value.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let app = null
|
let app = null
|
||||||
const rendering = async () => {
|
const rendering = () => {
|
||||||
const msgContent = document.querySelector(`#div-content-${msg.value.msgId}`)
|
const msgContent = document.querySelector(`#div-content-${msg.value.msgId}`)
|
||||||
if (msgContent) {
|
if (msgContent) {
|
||||||
if (app) {
|
if (app) {
|
||||||
app.unmount()
|
app.unmount()
|
||||||
}
|
}
|
||||||
const vnode = await renderComponent(msg.value.content)
|
const vnode = renderComponent(msg.value.content)
|
||||||
app = createApp({
|
app = createApp({
|
||||||
render: () => vnode
|
render: () => vnode
|
||||||
})
|
})
|
||||||
@@ -88,117 +147,55 @@ const rendering = async () => {
|
|||||||
* 动态渲染消息内容
|
* 动态渲染消息内容
|
||||||
* @param content 消息内容
|
* @param content 消息内容
|
||||||
*/
|
*/
|
||||||
const renderComponent = async (content) => {
|
const renderComponent = (content) => {
|
||||||
const contentJson = jsonParseSafe(content)
|
const arr = jsonParseSafe(content)
|
||||||
if (!contentJson) {
|
|
||||||
return renderMix(content)
|
// 不允许非结构化的content
|
||||||
|
if (!arr) {
|
||||||
|
return h('span', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = contentJson['type']
|
return arr.map((item) => {
|
||||||
const value = contentJson['value']
|
if (!item.type || !item.value) {
|
||||||
if (!type || !value) {
|
return h('span', '')
|
||||||
return renderMix(content)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case msgContentType.MIX:
|
case msgContentType.TEXT:
|
||||||
return renderMix(value)
|
return renderText(item.value)
|
||||||
case msgContentType.TEXT:
|
case msgContentType.EMOJI:
|
||||||
return renderText(value)
|
return renderEmoji(item.value)
|
||||||
case msgContentType.RECORDING:
|
case msgContentType.SCREENSHOT:
|
||||||
return renderRecording(value)
|
return renderImage(item.value, true)
|
||||||
case msgContentType.AUDIO:
|
case msgContentType.AT:
|
||||||
return renderAudio(value)
|
return renderAt(item.value)
|
||||||
case msgContentType.IMAGE:
|
case msgContentType.QUOTE:
|
||||||
return renderImage(value)
|
return renderQuote(item.value)
|
||||||
case msgContentType.EMOJI:
|
|
||||||
return renderEmoji(value)
|
|
||||||
case msgContentType.VIDEO:
|
|
||||||
return renderVideo(value)
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
return renderDocument(value)
|
|
||||||
case msgContentType.FORWARD_TOGETHER:
|
|
||||||
return await renderForwardTogether(value)
|
|
||||||
default:
|
|
||||||
return h('span', content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderText = (content) => {
|
case msgContentType.IMAGE:
|
||||||
return h('span', content)
|
return renderImage(item.value)
|
||||||
}
|
case msgContentType.RECORDING:
|
||||||
|
return renderRecording(item.value)
|
||||||
const renderMix = (content) => {
|
case msgContentType.AUDIO:
|
||||||
if (!content) return h('div', [])
|
return renderAudio(item.value)
|
||||||
let contentArray = []
|
case msgContentType.VIDEO:
|
||||||
|
return renderVideo(item.value)
|
||||||
// 1. 先匹配quote引用内容
|
case msgContentType.DOCUMENT:
|
||||||
content.split(/(「\{.*?\}」)/).forEach((item) => {
|
return renderDocument(item.value)
|
||||||
if (item.startsWith('「{') && item.endsWith('}」')) {
|
case msgContentType.FORWARD_TOGETHER:
|
||||||
// quote引用内容直接添加如数组
|
return renderForwardTogether(item.value)
|
||||||
contentArray.push(item)
|
default:
|
||||||
} else {
|
return h('span', content)
|
||||||
//2. 匹配内容中的图片
|
|
||||||
item.split(/(\{\d+\})/).forEach((item) => {
|
|
||||||
//3. 匹配内容中的表情
|
|
||||||
item.split(/(\[.*?\])/).forEach((item) => {
|
|
||||||
item.split(/(<.*?>)/).forEach((item) => {
|
|
||||||
if (item) {
|
|
||||||
contentArray.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return contentArray.map((item) => {
|
const renderText = (text) => {
|
||||||
if (item.startsWith('{') && item.endsWith('}')) {
|
return h('span', text)
|
||||||
return renderImage(item.slice(1, -1), true)
|
|
||||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
|
||||||
return renderEmoji(item.slice(1, -1))
|
|
||||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
|
||||||
return renderAt(item.slice(1, -1))
|
|
||||||
} else if (item.startsWith('「{') && item.endsWith('}」')) {
|
|
||||||
return renderQuote(item.slice(1, -1))
|
|
||||||
} else {
|
|
||||||
return h('span', item.trim())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderQuote = (quoteContent) => {
|
const renderQuote = (quoteContent) => {
|
||||||
const { msgId, nickName, content, msgTime } = jsonParseSafe(quoteContent)
|
const { nickName, msgId } = quoteContent
|
||||||
let showContent = content || ''
|
|
||||||
if (content) {
|
|
||||||
const defaultContent = content.replace(/<(?:.*?)-(.*?)>/g, '@$1').replace(/\{\d+\}/g, '[图片]')
|
|
||||||
showContent = defaultContent
|
|
||||||
const contentJson = jsonParseSafe(defaultContent)
|
|
||||||
if (contentJson) {
|
|
||||||
const type = contentJson['type']
|
|
||||||
const objectId = contentJson['value']
|
|
||||||
switch (type) {
|
|
||||||
case msgContentType.RECORDING:
|
|
||||||
showContent = '[语音]'
|
|
||||||
break
|
|
||||||
case msgContentType.AUDIO:
|
|
||||||
showContent = `[音频] ${audioData.audio[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.IMAGE:
|
|
||||||
showContent = `[图片] ${imageData.image[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.VIDEO:
|
|
||||||
showContent = `[视频] ${videoData.video[objectId].fileName}`
|
|
||||||
break
|
|
||||||
case msgContentType.DOCUMENT:
|
|
||||||
showContent = `[文档] ${documentData.document[objectId].fileName}`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
// 和InputEditor.vue中的结构保持一致,使用相同class可以复用样式
|
// 和InputEditor.vue中的结构保持一致,使用相同class可以复用样式
|
||||||
'div',
|
'div',
|
||||||
@@ -217,84 +214,25 @@ const renderQuote = (quoteContent) => {
|
|||||||
[
|
[
|
||||||
h('div', { class: 'quote-sender' }, [
|
h('div', { class: 'quote-sender' }, [
|
||||||
h('span', { class: 'quote-nickName' }, nickName + ' '),
|
h('span', { class: 'quote-nickName' }, nickName + ' '),
|
||||||
h('span', { class: 'quote-msgTime' }, msgTime + ':')
|
h('span', { class: 'quote-msgTime' }, showTimeFormat(quoteMsg.value.msgTime) + ':')
|
||||||
]),
|
]),
|
||||||
h('span', { class: 'quote-content' }, showContent)
|
h('span', { class: 'quote-content' }, showSimplifyMsgContent(quoteMsg.value.content))
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMsgContentInForwardTogether = (content) => {
|
const renderForwardTogether = (forwardContent) => {
|
||||||
const jsonContent = jsonParseSafe(content)
|
if (!forwardMsgs.value || forwardMsgs.value.length == 0) {
|
||||||
let template
|
return h('span', '')
|
||||||
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 if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
|
||||||
template = '[聊天记录]'
|
|
||||||
} else {
|
|
||||||
template = jsonContent['value']
|
|
||||||
}
|
|
||||||
return template
|
|
||||||
} else {
|
|
||||||
return content
|
|
||||||
.replace(/\{\d+\}/g, '[图片]')
|
|
||||||
.replace(/(「\{.*?\}」)/, '[引用]')
|
|
||||||
.split(/(<.*?>)/)
|
|
||||||
.map((item) => {
|
|
||||||
const sliceStr = item.slice(1, -1)
|
|
||||||
const index = sliceStr.indexOf('-')
|
|
||||||
if (index !== -1) {
|
|
||||||
const nickName = sliceStr.slice(index + 1)
|
|
||||||
if (nickName) {
|
|
||||||
return `@${nickName}`
|
|
||||||
} else {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderForwardTogether = async (content) => {
|
|
||||||
let res
|
|
||||||
try {
|
|
||||||
const msgIds = content.data
|
|
||||||
.map((item) => {
|
|
||||||
return item.msgId
|
|
||||||
})
|
|
||||||
.join(',')
|
|
||||||
res = await msgChatQueryMessagesService({
|
|
||||||
sessionId: content.sessionId,
|
|
||||||
msgIds
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return h('span', content)
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgs = res.data.data
|
|
||||||
if (!res.data.data || res.data.data.length == 0) {
|
|
||||||
return h('span', content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 把content.data(取里面的nickName) 和 msgs合一
|
// 把content.data(取里面的nickName) 和 msgs合一
|
||||||
const newMsgs = {}
|
const newMsgs = {}
|
||||||
msgs.forEach((item) => {
|
forwardMsgs.value.forEach((item) => {
|
||||||
newMsgs[item.msgId] = item
|
newMsgs[item.msgId] = item
|
||||||
})
|
})
|
||||||
content.data.forEach((item) => {
|
forwardContent.data.forEach((item) => {
|
||||||
if (item.msgId in newMsgs) {
|
if (item.msgId in newMsgs) {
|
||||||
newMsgs[item.msgId] = {
|
newMsgs[item.msgId] = {
|
||||||
...newMsgs[item.msgId],
|
...newMsgs[item.msgId],
|
||||||
@@ -303,15 +241,16 @@ const renderForwardTogether = async (content) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const title =
|
|
||||||
(msgs[0].msgType === MsgType.GROUP_CHAT ? '群聊' : nickNameFromMsg.value) + '的聊天记录'
|
|
||||||
|
|
||||||
const msgsSorted = Object.values(newMsgs).sort((a, b) => {
|
const msgsSorted = Object.values(newMsgs).sort((a, b) => {
|
||||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||||
return timeA - timeB
|
return timeA - timeB
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const title =
|
||||||
|
(forwardMsgs.value[0].msgType === MsgType.GROUP_CHAT ? '群聊' : nickNameFromMsg.value) +
|
||||||
|
'的聊天记录'
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
@@ -351,7 +290,7 @@ const renderForwardTogether = async (content) => {
|
|||||||
return h('div', { class: 'msg-item', key: index }, [
|
return h('div', { class: 'msg-item', key: index }, [
|
||||||
h('span', { class: 'msg-item-nickname' }, msg.nickName || msg.fromId),
|
h('span', { class: 'msg-item-nickname' }, msg.nickName || msg.fromId),
|
||||||
h('span', ':'),
|
h('span', ':'),
|
||||||
h('span', { class: 'msg-item-content' }, showMsgContentInForwardTogether(msg.content))
|
h('span', { class: 'msg-item-content' }, showSimplifyMsgContent(msg.content))
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -361,45 +300,33 @@ const renderForwardTogether = async (content) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAt = (content) => {
|
const renderAt = (atContent) => {
|
||||||
const index = content.indexOf('-')
|
const style = {
|
||||||
if (index !== -1) {
|
color: '#337ECC',
|
||||||
const account = content.slice(0, index)
|
fontWeight:
|
||||||
const nickName = content.slice(index + 1)
|
atContent.account === myAccount.value || atContent.account === '0' ? 'bold' : 'normal'
|
||||||
if (messageData.sessionList[props.sessionId].sessionType === MsgType.GROUP_CHAT && nickName) {
|
|
||||||
const style = {
|
|
||||||
color: '#337ECC',
|
|
||||||
fontWeight: account === myAccount.value || account === '0' ? 'bold' : 'normal'
|
|
||||||
}
|
|
||||||
return h('span', { style }, `@${nickName} `)
|
|
||||||
} else {
|
|
||||||
return h('span', `<${content}>`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return h('span', `<${content}>`)
|
|
||||||
}
|
}
|
||||||
|
return h('span', { style }, `@${atContent.nickName} `)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderEmoji = (content) => {
|
const renderEmoji = (emojiId) => {
|
||||||
const emojiId = `[${content}]`
|
|
||||||
const url = emojis[emojiId]
|
const url = emojis[emojiId]
|
||||||
if (url) {
|
if (url) {
|
||||||
return h('img', {
|
return h('img', {
|
||||||
class: 'emoji',
|
class: 'emoji',
|
||||||
src: url,
|
src: url,
|
||||||
alt: emojiId,
|
alt: emojiId,
|
||||||
title: content,
|
title: emojiId.slice(1, -1),
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
emit('loadFinished')
|
emit('loadFinished')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', `[${content}]`)
|
return h('span', emojiId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderVideo = (content) => {
|
const renderVideo = (videoId) => {
|
||||||
const videoId = content
|
|
||||||
const url = videoData.video[videoId]?.downloadUrl
|
const url = videoData.video[videoId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return h(MsgBoxVideo, {
|
return h(MsgBoxVideo, {
|
||||||
@@ -415,19 +342,18 @@ const renderVideo = (content) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', `[${content}]`)
|
return h('span', `[${videoId}]`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderImage = (content, isForMix = false) => {
|
const renderImage = (imgId, isScreenShot = false) => {
|
||||||
const imgId = content
|
|
||||||
if (imageData.image[imgId]) {
|
if (imageData.image[imgId]) {
|
||||||
// 只要这里渲染,就收集该session下的所有image,用于preview-src-list
|
// 只要这里渲染,就收集该session下的所有image,用于preview-src-list
|
||||||
imageData.setImageInSession(props.sessionId, imageData.image[imgId])
|
imageData.setImageInSession(props.sessionId, imageData.image[imgId])
|
||||||
return h(MsgBoxImage, {
|
return h(MsgBoxImage, {
|
||||||
sessionId: props.sessionId,
|
sessionId: props.sessionId,
|
||||||
imgId,
|
imgId,
|
||||||
isForMix,
|
isScreenShot,
|
||||||
thumbWidth: imageData.image[imgId].thumbWidth,
|
thumbWidth: imageData.image[imgId].thumbWidth,
|
||||||
thumbHeight: imageData.image[imgId].thumbHeight,
|
thumbHeight: imageData.image[imgId].thumbHeight,
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
@@ -435,12 +361,11 @@ const renderImage = (content, isForMix = false) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', isForMix ? `{${content}}` : `[${content}]`)
|
return h('span', `[${imgId}]`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderRecording = (content) => {
|
const renderRecording = (audioId) => {
|
||||||
const audioId = content
|
|
||||||
const url = audioData.audio[audioId]?.downloadUrl
|
const url = audioData.audio[audioId]?.downloadUrl
|
||||||
const duration = audioData.audio[audioId]?.duration
|
const duration = audioData.audio[audioId]?.duration
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -452,12 +377,11 @@ const renderRecording = (content) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', `[${content}]`)
|
return h('span', '[语音]')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAudio = (content) => {
|
const renderAudio = (audioId) => {
|
||||||
const audioId = content
|
|
||||||
const url = audioData.audio[audioId]?.downloadUrl
|
const url = audioData.audio[audioId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return h(MsgBoxAudio, {
|
return h(MsgBoxAudio, {
|
||||||
@@ -469,12 +393,11 @@ const renderAudio = (content) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', `[${content}]`)
|
return h('span', `[${audioId}]`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDocument = (content) => {
|
const renderDocument = (documentId) => {
|
||||||
const documentId = content
|
|
||||||
const url = documentData.document[documentId]?.downloadUrl
|
const url = documentData.document[documentId]?.downloadUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
return h(MsgBoxDocument, {
|
return h(MsgBoxDocument, {
|
||||||
@@ -487,25 +410,10 @@ const renderDocument = (content) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return h('span', `[${content}]`)
|
return h('span', `[${documentId}]`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = computed(() => {
|
|
||||||
const contentJson = jsonParseSafe(msg.value.content)
|
|
||||||
if (!contentJson) {
|
|
||||||
return msgContentType.MIX
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = contentJson['type']
|
|
||||||
const value = contentJson['value']
|
|
||||||
if (!type || !value) {
|
|
||||||
return msgContentType.MIX
|
|
||||||
} else {
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const msg = computed(() => {
|
const msg = computed(() => {
|
||||||
return messageData.getMsg(props.sessionId, props.msgKey)
|
return messageData.getMsg(props.sessionId, props.msgKey)
|
||||||
})
|
})
|
||||||
@@ -892,13 +800,11 @@ const isDelete = computed(() => {
|
|||||||
return msg.value.delete
|
return msg.value.delete
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否支持撤回重新编辑
|
||||||
|
*/
|
||||||
const isReedit = computed(() => {
|
const isReedit = computed(() => {
|
||||||
const contentJson = jsonParseSafe(msg.value.content)
|
const type = msg.value.contentType
|
||||||
if (!contentJson) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = contentJson['type']
|
|
||||||
if (
|
if (
|
||||||
type === msgContentType.IMAGE ||
|
type === msgContentType.IMAGE ||
|
||||||
type === msgContentType.RECORDING ||
|
type === msgContentType.RECORDING ||
|
||||||
@@ -964,10 +870,34 @@ const onSelectMenuMsgItem = async (label) => {
|
|||||||
switch (label) {
|
switch (label) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(msg.value.content)
|
let text = ''
|
||||||
|
const arr = jsonParseSafe(msg.value.content)
|
||||||
|
if (arr && Array.isArray(arr)) {
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item.type === msgContentType.TEXT) {
|
||||||
|
text += item.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipboardItem = new ClipboardItem({
|
||||||
|
'text/html': new Blob(
|
||||||
|
[`<div data-quill-custom=${encodeURIComponent(msg.value.content)}></div>`],
|
||||||
|
{
|
||||||
|
type: 'text/html'
|
||||||
|
}
|
||||||
|
), // 在html自定义属性data-quill-custom中传递clipboardContent结构化数据
|
||||||
|
'text/plain': new Blob([text], { type: 'text/plain' }) // 纯文本
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.write([clipboardItem])
|
||||||
|
} catch (error) {
|
||||||
|
await navigator.clipboard.writeText(text) // 降级方案:仅写入纯文本
|
||||||
|
}
|
||||||
ElMessage.success('已复制到剪贴板')
|
ElMessage.success('已复制到剪贴板')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('复制出错')
|
ElMessage.error('复制出错 ', error)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'revoke':
|
case 'revoke':
|
||||||
@@ -1023,6 +953,7 @@ const onSelectMenuMsgItem = async (label) => {
|
|||||||
props.inputEditorRef?.insertQuote({
|
props.inputEditorRef?.insertQuote({
|
||||||
account: msg.value.fromId,
|
account: msg.value.fromId,
|
||||||
nickName: nickNameFromMsg.value,
|
nickName: nickNameFromMsg.value,
|
||||||
|
msgKey: props.msgKey, // 引用本地缓存消息的时候用
|
||||||
msgId: msg.value.msgId, // 引用要用msg.value.msgId
|
msgId: msg.value.msgId, // 引用要用msg.value.msgId
|
||||||
content: msg.value.content,
|
content: msg.value.content,
|
||||||
msgTime: msg.value.msgTime
|
msgTime: msg.value.msgTime
|
||||||
@@ -1058,9 +989,12 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 哪些情况在多选模式下是禁选的
|
||||||
|
*/
|
||||||
const multiSelectOptionDisabled = computed(() => {
|
const multiSelectOptionDisabled = computed(() => {
|
||||||
return (
|
return (
|
||||||
contentType.value === msgContentType.RECORDING ||
|
msg.value.contentType === msgContentType.RECORDING ||
|
||||||
isSystemMsg.value ||
|
isSystemMsg.value ||
|
||||||
isRevoke.value ||
|
isRevoke.value ||
|
||||||
isDelete.value ||
|
isDelete.value ||
|
||||||
@@ -1468,6 +1402,8 @@ const handleItemClick = () => {
|
|||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word; /* 长单词或URL强制换行 */
|
||||||
|
overflow-wrap: break-word; /* 兼容性更好的换行 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import { formatFileSize } from '@/js/utils/common'
|
|||||||
import { useImageStore } from '@/stores'
|
import { useImageStore } from '@/stores'
|
||||||
import ImageloadfailedIcon from '@/assets/svg/imageloadfailed.svg'
|
import ImageloadfailedIcon from '@/assets/svg/imageloadfailed.svg'
|
||||||
|
|
||||||
const props = defineProps(['sessionId', 'imgId', 'isForMix', 'thumbWidth', 'thumbHeight'])
|
const props = defineProps(['sessionId', 'imgId', 'isScreenShot', 'thumbWidth', 'thumbHeight'])
|
||||||
const emits = defineEmits(['load'])
|
const emits = defineEmits(['load'])
|
||||||
|
|
||||||
const imageData = useImageStore()
|
const imageData = useImageStore()
|
||||||
|
|
||||||
const maxWidth = computed(() => {
|
const maxWidth = computed(() => {
|
||||||
return props.isForMix ? Math.min(props.thumbWidth, 360) : 360
|
return props.isScreenShot ? Math.min(props.thumbWidth, 360) : 360
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxHeight = computed(() => {
|
const maxHeight = computed(() => {
|
||||||
return props.isForMix ? Math.min(props.thumbHeight, 270) : 270
|
return props.isScreenShot ? Math.min(props.thumbHeight, 270) : 270
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderWidth = computed(() => {
|
const renderWidth = computed(() => {
|
||||||
@@ -65,11 +65,11 @@ const initialIndex = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fileName = computed(() => {
|
const fileName = computed(() => {
|
||||||
return props.isForMix ? '' : imageData.image[props.imgId]?.fileName
|
return props.isScreenShot ? '' : imageData.image[props.imgId]?.fileName
|
||||||
})
|
})
|
||||||
|
|
||||||
const size = computed(() => {
|
const size = computed(() => {
|
||||||
return props.isForMix ? '' : imageData.image[props.imgId]?.size
|
return props.isScreenShot ? '' : imageData.image[props.imgId]?.size
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatSize = computed(() => {
|
const formatSize = computed(() => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElIcon, ElMessage } from 'element-plus'
|
import { ElIcon, ElMessage } from 'element-plus'
|
||||||
import PlayIcon from '@/assets/svg/play.svg'
|
import PlayIcon from '@/assets/svg/play.svg'
|
||||||
import PauseIcon from '@/assets/svg/pause.svg'
|
import PauseIcon from '@/assets/svg/pause.svg'
|
||||||
import { AVWaveform } from 'vue-audio-visual'
|
import { AVWaveform } from 'vue-audio-visual'
|
||||||
|
import { showDurationFormat } from '@/js/utils/common'
|
||||||
|
|
||||||
const props = defineProps(['audioUrl', 'duration'])
|
const props = defineProps(['audioUrl', 'duration'])
|
||||||
const emits = defineEmits(['load'])
|
const emits = defineEmits(['load'])
|
||||||
@@ -12,17 +13,6 @@ const waveformRef = ref(null)
|
|||||||
const isPlaying = ref(false)
|
const isPlaying = ref(false)
|
||||||
const audioDuration = ref(null)
|
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 playAudio = async () => {
|
||||||
const audioPlayer = waveformRef.value.querySelector('audio')
|
const audioPlayer = waveformRef.value.querySelector('audio')
|
||||||
if (audioPlayer) {
|
if (audioPlayer) {
|
||||||
@@ -98,7 +88,7 @@ onMounted(() => {
|
|||||||
:playtime-slider-color="`#409eff`"
|
:playtime-slider-color="`#409eff`"
|
||||||
></AVWaveform>
|
></AVWaveform>
|
||||||
|
|
||||||
<span class="time">{{ formatDuration }}</span>
|
<span class="time">{{ showDurationFormat(audioDuration) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||||
import GroupAvatarIcon from '@/components/common/GroupAvatarIcon.vue'
|
import GroupAvatarIcon from '@/components/common/GroupAvatarIcon.vue'
|
||||||
import SessionTag from './SessionTag.vue'
|
import SessionTag from './SessionTag.vue'
|
||||||
import { jsonParseSafe, sessionShowTime } from '@/js/utils/common'
|
import { sessionShowTime } from '@/js/utils/common'
|
||||||
import { Top, MuteNotification } from '@element-plus/icons-vue'
|
import { Top, MuteNotification } from '@element-plus/icons-vue'
|
||||||
import { MsgType } from '@/proto/msg'
|
import { MsgType } from '@/proto/msg'
|
||||||
import { useUserStore, useMessageStore, useGroupStore } from '@/stores'
|
import { useUserStore, useMessageStore, useGroupStore } from '@/stores'
|
||||||
import { msgChatCloseSessionService } from '@/api/message'
|
import { msgChatCloseSessionService } from '@/api/message'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { msgContentType, msgSendStatus } from '@/const/msgConst'
|
import { msgSendStatus } from '@/const/msgConst'
|
||||||
|
import { showSimplifyMsgContent } from '@/js/utils/message'
|
||||||
|
|
||||||
const props = defineProps([
|
const props = defineProps([
|
||||||
'sessionId',
|
'sessionId',
|
||||||
@@ -217,122 +218,55 @@ const getGroupChatMsgTips = (content) => {
|
|||||||
|
|
||||||
const showDetailContent = computed(() => {
|
const showDetailContent = computed(() => {
|
||||||
if (isShowDraft.value) {
|
if (isShowDraft.value) {
|
||||||
let formatDraft = sessionInfo.value.draft
|
return showSimplifyMsgContent(sessionInfo.value.draft)
|
||||||
?.replace(/\{\d+\}/g, '[图片]') // 把内容中的`{xxxxxx}`格式的图片统一转成`[图片]`
|
|
||||||
.replace(/(「\{.*?\}」)/, '[引用]') // 把内容中的`「xxxxxx」`格式的图片统一转成`[图片]`
|
|
||||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
|
||||||
formatDraft = formatDraft
|
|
||||||
.split(/(<.*?>)/)
|
|
||||||
.map((item) => {
|
|
||||||
const sliceStr = item.slice(1, -1)
|
|
||||||
const index = sliceStr.indexOf('-')
|
|
||||||
if (index !== -1) {
|
|
||||||
const nickName = sliceStr.slice(index + 1)
|
|
||||||
if (nickName) {
|
|
||||||
return `@${nickName}`
|
|
||||||
} else {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
return formatDraft
|
|
||||||
} else {
|
} else {
|
||||||
if (!lastMsg.value.content) {
|
if (!lastMsg.value.content) {
|
||||||
return '...'
|
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 if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
|
||||||
template = '[聊天记录]'
|
|
||||||
} else {
|
|
||||||
template = jsonContent['value']
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
|
||||||
return getGroupChatMsgTips(template)
|
|
||||||
} else {
|
|
||||||
return template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
if (sessionInfo.value.sessionType === MsgType.GROUP_CHAT) {
|
||||||
let content = jsonParseSafe(lastMsg.value.content)
|
|
||||||
switch (lastMsg.value.msgType) {
|
switch (lastMsg.value.msgType) {
|
||||||
case MsgType.SYS_GROUP_CREATE:
|
case MsgType.SYS_GROUP_CREATE:
|
||||||
return getSysGroupCreateMsgTips(content)
|
return getSysGroupCreateMsgTips(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_ADD_MEMBER:
|
case MsgType.SYS_GROUP_ADD_MEMBER:
|
||||||
return getSysGroupAddMemberMsgTips(content)
|
return getSysGroupAddMemberMsgTips(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_DEL_MEMBER:
|
case MsgType.SYS_GROUP_DEL_MEMBER:
|
||||||
return getSysGroupDelMemberMsgTips(content)
|
return getSysGroupDelMemberMsgTips(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_UPDATE_ANNOUNCEMENT:
|
case MsgType.SYS_GROUP_UPDATE_ANNOUNCEMENT:
|
||||||
return getSysGroupUpdateAnnouncement(content)
|
return getSysGroupUpdateAnnouncement(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_UPDATE_NAME:
|
case MsgType.SYS_GROUP_UPDATE_NAME:
|
||||||
return getSysGroupUpdateName(content)
|
return getSysGroupUpdateName(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_UPDATE_AVATAR:
|
case MsgType.SYS_GROUP_UPDATE_AVATAR:
|
||||||
return getSysGroupUpdateAvatar(content)
|
return getSysGroupUpdateAvatar(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_SET_ADMIN:
|
case MsgType.SYS_GROUP_SET_ADMIN:
|
||||||
case MsgType.SYS_GROUP_CANCEL_ADMIN:
|
case MsgType.SYS_GROUP_CANCEL_ADMIN:
|
||||||
return getSysGroupChangeRoleMsgTips(lastMsg.value.msgType, content)
|
return getSysGroupChangeRoleMsgTips(lastMsg.value.msgType, lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_SET_ALL_MUTED:
|
case MsgType.SYS_GROUP_SET_ALL_MUTED:
|
||||||
case MsgType.SYS_GROUP_CANCEL_ALL_MUTED:
|
case MsgType.SYS_GROUP_CANCEL_ALL_MUTED:
|
||||||
return getSysGroupUpdateAllMuted(lastMsg.value.msgType, content)
|
return getSysGroupUpdateAllMuted(lastMsg.value.msgType, lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_SET_JOIN_APPROVAL:
|
case MsgType.SYS_GROUP_SET_JOIN_APPROVAL:
|
||||||
case MsgType.SYS_GROUP_CANCEL_JOIN_APPROVAL:
|
case MsgType.SYS_GROUP_CANCEL_JOIN_APPROVAL:
|
||||||
return getSysGroupUpdateJoinApproval(lastMsg.value.msgType, content)
|
return getSysGroupUpdateJoinApproval(lastMsg.value.msgType, lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_SET_HISTORY_BROWSE:
|
case MsgType.SYS_GROUP_SET_HISTORY_BROWSE:
|
||||||
case MsgType.SYS_GROUP_CANCEL_HISTORY_BROWSE:
|
case MsgType.SYS_GROUP_CANCEL_HISTORY_BROWSE:
|
||||||
return getSysGroupUpdateHistoryBrowse(lastMsg.value.msgType, content)
|
return getSysGroupUpdateHistoryBrowse(lastMsg.value.msgType, lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_OWNER_TRANSFER:
|
case MsgType.SYS_GROUP_OWNER_TRANSFER:
|
||||||
return getSysGroupOwnerTransfer(content)
|
return getSysGroupOwnerTransfer(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_UPDATE_MEMBER_MUTED:
|
case MsgType.SYS_GROUP_UPDATE_MEMBER_MUTED:
|
||||||
return getSysGroupUpdateMemberMuted(content)
|
return getSysGroupUpdateMemberMuted(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_LEAVE:
|
case MsgType.SYS_GROUP_LEAVE:
|
||||||
return getSysGroupLeave(content)
|
return getSysGroupLeave(lastMsg.value.content)
|
||||||
case MsgType.SYS_GROUP_DROP:
|
case MsgType.SYS_GROUP_DROP:
|
||||||
return getSysGroupDrop(content)
|
return getSysGroupDrop(lastMsg.value.content)
|
||||||
case MsgType.GROUP_CHAT:
|
case MsgType.GROUP_CHAT:
|
||||||
//格式化图片内容
|
return getGroupChatMsgTips(showSimplifyMsgContent(lastMsg.value.content))
|
||||||
content = lastMsg.value.content
|
|
||||||
.replace(/\{\d+\}/g, '[图片]')
|
|
||||||
.replace(/(「\{.*?\}」)/, '[引用]')
|
|
||||||
//格式化@内容
|
|
||||||
content = content
|
|
||||||
.split(/(<.*?>)/)
|
|
||||||
.map((item) => {
|
|
||||||
const sliceStr = item.slice(1, -1)
|
|
||||||
const index = sliceStr.indexOf('-')
|
|
||||||
if (index !== -1) {
|
|
||||||
const nickName = sliceStr.slice(index + 1)
|
|
||||||
if (nickName) {
|
|
||||||
return `@${nickName}`
|
|
||||||
} else {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
return getGroupChatMsgTips(content)
|
|
||||||
default:
|
default:
|
||||||
return ''
|
return '...'
|
||||||
}
|
}
|
||||||
|
} else if (sessionInfo.value.sessionType === MsgType.CHAT) {
|
||||||
|
return showSimplifyMsgContent(lastMsg.value.content)
|
||||||
} else {
|
} else {
|
||||||
return lastMsg.value.content.replace(/\{\d+\}/g, '[图片]').replace(/(「\{.*?\}」)/, '[引用]')
|
return '...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user