feat(im): 修一批正确性 / UX 细节:群名 trim 空、敏感词 / 封禁理由空白校验、默认群名计入创建者、ack 后重算会话摘要、文本重试复用 clientMessageId 防重复

im
YunaiV 2026-05-21 15:57:46 +08:00
parent 73aa578c9b
commit fead282395
6 changed files with 27 additions and 15 deletions

View File

@ -539,12 +539,17 @@ watch(visible, (v) => {
} }
}) })
/** 群主:保存群名(走 /im/group/update */ /** 群主:保存群名(走 /im/group/updatetrim 后空字符串拒提交,与 saveGroupRemark 行为对齐 */
async function saveName() { async function saveName() {
if (!props.group) { if (!props.group) {
return return
} }
await updateGroup({ id: props.group.id, name: editName.value }) const trimmed = editName.value.trim()
if (!trimmed) {
message.warning('群名称不能为空')
return
}
await updateGroup({ id: props.group.id, name: trimmed })
namePopoverVisible.value = false namePopoverVisible.value = false
message.success('保存成功') message.success('保存成功')
emit('reload') emit('reload')

View File

@ -939,9 +939,9 @@ async function handleRecall() {
* 失败消息点击重试 * 失败消息点击重试
* *
* - 媒体消息image / file / voice / video_localFile 在内存就重走 uploadAndSendMedia重新上传 + 占位 + 进度 * - 媒体消息image / file / voice / video_localFile 在内存就重走 uploadAndSendMedia重新上传 + 占位 + 进度
* - 文本消息移除 FAILED 占位 + 用原 content 走一遍 sendRaw 新建占位 * - 文本消息复用原 clientMessageId + status 回滚到 SENDING existingClientMessageId 路径让服务端按 cmid 幂等
* *
* 媒体类型若 _localFile 已丢理论上 IDB 恢复阶段就被 drop进不到这里保险起见仍走文本兜底则按 sendRaw 重发 * 媒体类型若 _localFile 已丢理论上 IDB 恢复阶段就被 drop进不到这里保险起见仍走文本兜底则按文本路径重发
* 后端拒绝失效 blob URL 时再次 FAILED用户可右键删除 * 后端拒绝失效 blob URL 时再次 FAILED用户可右键删除
* *
* 不还原原 receipt群回执是发送时的扩展选项不会持久化到 message强行猜测可能与原意不符 * 不还原原 receipt群回执是发送时的扩展选项不会持久化到 message强行猜测可能与原意不符
@ -983,13 +983,13 @@ async function handleResend() {
} }
} }
// / _localFile content sendRaw // / _localFile FAILED SENDING clientMessageId cmid
conversationStore.removeMessage(conversation.type, conversation.targetId, { conversationStore.patchMessage(conversation.type, conversation.targetId, message.clientMessageId, {
id: message.id, status: ImMessageStatus.SENDING
clientMessageId: message.clientMessageId
}) })
await sendRaw(message.type, message.content, { await sendRaw(message.type, message.content, {
atUserIds: message.atUserIds atUserIds: message.atUserIds,
existingClientMessageId: message.clientMessageId
}) })
} }

View File

@ -657,11 +657,17 @@ export const useConversationStore = defineStore('imConversationStore', {
if (!conversation) { if (!conversation) {
return return
} }
const message = conversation.messages.find((item) => item.clientMessageId === clientMessageId) const messageIndex = conversation.messages.findIndex(
if (!message) { (item) => item.clientMessageId === clientMessageId
)
if (messageIndex < 0) {
return return
} }
applyServerMessageUpdate(message, updates) applyServerMessageUpdate(conversation.messages[messageIndex], updates)
// ack 命中末尾消息时按服务端 sendTime / content 重算会话摘要,让会话列表跟着权威值排序
if (messageIndex === conversation.messages.length - 1) {
recomputeConversationLast(conversation)
}
if (updates.id) { if (updates.id) {
this.updateMaxId(conversationType, updates.id) this.updateMaxId(conversationType, updates.id)
} }

View File

@ -34,7 +34,7 @@ const formLoading = ref(false) // 提交的加载中
const formData = reactive({ id: 0, groupName: '', reason: '' }) // const formData = reactive({ id: 0, groupName: '', reason: '' }) //
const formRef = ref() // Ref const formRef = ref() // Ref
const formRules = { const formRules = {
reason: [{ required: true, message: '封禁原因不能为空', trigger: 'blur' }] reason: [{ required: true, whitespace: true, message: '封禁原因不能为空', trigger: 'blur' }]
} }
/** 打开弹窗 */ /** 打开弹窗 */

View File

@ -54,7 +54,7 @@ const formData = ref({
status: CommonStatusEnum.ENABLE status: CommonStatusEnum.ENABLE
}) })
const formRules = reactive({ const formRules = reactive({
word: [{ required: true, message: '敏感词不能为空', trigger: 'blur' }], word: [{ required: true, whitespace: true, message: '敏感词不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // Ref const formRef = ref() // Ref

View File

@ -10,7 +10,8 @@ export function buildDefaultGroupName(members: FriendLite[]): string {
const names = members.slice(0, 4).map((m) => m.displayName || m.nickname || '') const names = members.slice(0, 4).map((m) => m.displayName || m.nickname || '')
const head = names.filter(Boolean).join('、') const head = names.filter(Boolean).join('、')
if (members.length > 4) { if (members.length > 4) {
return `${head}${members.length}` // members 只含被选好友,+1 把创建者也计入实际成员数
return `${head}${members.length + 1}`
} }
return head || '群聊' return head || '群聊'
} }