msg.content消息结构重构

This commit is contained in:
bob
2025-05-17 12:21:50 +08:00
parent ecfa623a72
commit a2017be96a
26 changed files with 1080 additions and 948 deletions

View File

@@ -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 // 合并转发消息
} }
// 消息发送状态 // 消息发送状态

View File

@@ -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
View 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
]
}

View File

@@ -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 })

View File

@@ -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)

View File

@@ -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

View File

@@ -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'

View File

@@ -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 {

View File

@@ -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
} }
}) })

View File

@@ -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
} }
}) })

View File

@@ -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
} }
}) })

View File

@@ -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)
} }
/** /**

View File

@@ -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
} }
}) })

View File

@@ -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)

View File

@@ -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"

View File

@@ -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 >&times;</span> <span >&times;</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 {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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; /* 兼容性更好的换行 */
} }
} }
} }

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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 '...'
} }
} }
}) })