feat(Talk): 系统消息居中展示,订阅 talk:conversation 实时同步

talkSocket 新增 talk:conversation 事件;Talk.vue 收到该事件后:解散或本人被移出则直接移除该会话(在会话中同时清空聊天区并提示),其余变更(改名/公告/角色/移出他人)重新拉取会话列表。

消息区区分 MsgType=3 系统消息,居中灰字展示(不带头像与名字);api/talk 补齐 MsgType.System。
This commit is contained in:
TermiNexus committed 2026-10-13 17:21:12 +08:00
1 parent 04535d2dc0
commit dd1ece0a7f
3 files changed
+43 -4

No files matched your search

+3 -3
View File
@@ -18,8 +18,8 @@ export const ASSET_DOWNLOAD_BASE = 'https://api.gvsds.com/API/Asset/Download'
/** 会话类型:0=单聊 1=群聊 */
export const TalkConversationType = { Single: 0, Group: 1 } as const
/** 消息类型:0=文本 1=图片 2=文件(后端契约未显式约定,按此实现,见汇总说明) */
export const TalkMsgType = { Text: 0, Image: 1, File: 2 } as const
/** 消息类型:0=文本 1=图片 2=文件 3=系统(后端 MsgType 对齐 manager.py) */
export const TalkMsgType = { Text: 0, Image: 1, File: 2, System: 3 } as const
/** 群成员角色:0=成员 1=群主 2=管理员(上限由 Entry.GroupAdminLimit 下发) */
export const TalkMemberRole = { Member: 0, Owner: 1, Admin: 2 } as const
@@ -118,7 +118,7 @@ export interface TalkMessage {
ConversationUUID: string
SenderUUID: string
SenderName: string
/** 0=文本 1=图片 2=文件 */
/** 0=文本 1=图片 2=文件 3=系统 */
MsgType: number
Content: string
Attachment: TalkAttachment | null
+2
View File
@@ -19,6 +19,7 @@ export type TalkServerEvent =
| 'talk:message'
| 'talk:read'
| 'talk:typing'
| 'talk:conversation'
| 'talk:pong'
| 'talk:error'
@@ -31,6 +32,7 @@ const SERVER_EVENTS: TalkServerEvent[] = [
'talk:message',
'talk:read',
'talk:typing',
'talk:conversation',
'talk:pong',
'talk:error',
]
+38 -1
View File
@@ -848,6 +848,7 @@ function SetupSocketHandlers(): void {
subscribeTalkEvent('talk:ready', OnSocketReady),
subscribeTalkEvent('talk:message', OnSocketMessage),
subscribeTalkEvent('talk:typing', OnSocketTyping),
subscribeTalkEvent('talk:conversation', OnSocketConversation),
subscribeTalkEvent('talk:error', OnSocketError),
]
}
@@ -870,6 +871,28 @@ function OnSocketMessage(payload: unknown): void {
PatchConversationOnMessage(message)
}
/** talk:conversation:群主/管理员操作后的会话级同步(改名 / 公告 / 角色 / 移出 / 转让 / 解散) */
function OnSocketConversation(payload: unknown): void {
const data = payload as
| { ConversationUUID?: string; Action?: string; Removed?: string[] }
| undefined
if (!data?.ConversationUUID) return
const conversationUUID = data.ConversationUUID
const removedMe = (data.Removed || []).includes(MyUUID.value)
// 解散或本人被移出:直接从列表移除(在会话中则同时清空聊天区)
if (data.Action === 'dissolved' || removedMe) {
if (ActiveUUID.value === conversationUUID) {
ActiveUUID.value = ''
Messages.value = []
}
Conversations.value = Conversations.value.filter((c) => c.ConversationUUID !== conversationUUID)
ElMessage.warning(removedMe ? '你已被移出该群聊' : '该群聊已解散')
return
}
// 其余变更(改名 / 公告 / 角色 / 追加成员等):重新拉取会话,保持成员与权限最新
void LoadConversations()
}
/** talk:typing:仅当前会话且非自己时展示 */
function OnSocketTyping(payload: unknown): void {
const data = payload as { ConversationUUID?: string; UserUUID?: string } | undefined
@@ -1067,7 +1090,11 @@ onBeforeUnmount(() => {
<div v-if="NeedTimeDivider(index)" class="talk-time-divider">
{{ FormatMessageTime(message.CreateTime) }}
</div>
<div class="talk-msg-row" :class="{ self: IsSelf(message) }">
<!-- 系统消息(群主/管理员操作留痕):居中灰字,不带头像 -->
<div v-if="message.MsgType === TalkMsgType.System" class="talk-sysmsg">
{{ message.Content }}
</div>
<div v-else class="talk-msg-row" :class="{ self: IsSelf(message) }">
<TalkAvatar :user-uuid="message.SenderUUID" :name="message.SenderName" size="small" />
<div class="talk-msg-body">
<div v-if="IsGroupChat && !IsSelf(message)" class="talk-msg-sender">
@@ -1512,6 +1539,16 @@ onBeforeUnmount(() => {
margin: 10px 0;
}
/* ---------- 系统消息(群主/管理员操作留痕) ---------- */
.talk-sysmsg {
margin: 8px 0;
text-align: center;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
word-break: break-word;
}
.talk-msg-row {
display: flex;
align-items: flex-start;