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() {
if (!props.group) {
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
message.success('保存成功')
emit('reload')

View File

@ -939,9 +939,9 @@ async function handleRecall() {
* 失败消息点击重试
*
* - 媒体消息image / file / voice / video_localFile 在内存就重走 uploadAndSendMedia重新上传 + 占位 + 进度
* - 文本消息移除 FAILED 占位 + 用原 content 走一遍 sendRaw 新建占位
* - 文本消息复用原 clientMessageId + status 回滚到 SENDING existingClientMessageId 路径让服务端按 cmid 幂等
*
* 媒体类型若 _localFile 已丢理论上 IDB 恢复阶段就被 drop进不到这里保险起见仍走文本兜底则按 sendRaw 重发
* 媒体类型若 _localFile 已丢理论上 IDB 恢复阶段就被 drop进不到这里保险起见仍走文本兜底则按文本路径重发
* 后端拒绝失效 blob URL 时再次 FAILED用户可右键删除
*
* 不还原原 receipt群回执是发送时的扩展选项不会持久化到 message强行猜测可能与原意不符
@ -983,13 +983,13 @@ async function handleResend() {
}
}
// / _localFile content sendRaw
conversationStore.removeMessage(conversation.type, conversation.targetId, {
id: message.id,
clientMessageId: message.clientMessageId
// / _localFile FAILED SENDING clientMessageId cmid
conversationStore.patchMessage(conversation.type, conversation.targetId, message.clientMessageId, {
status: ImMessageStatus.SENDING
})
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) {
return
}
const message = conversation.messages.find((item) => item.clientMessageId === clientMessageId)
if (!message) {
const messageIndex = conversation.messages.findIndex(
(item) => item.clientMessageId === clientMessageId
)
if (messageIndex < 0) {
return
}
applyServerMessageUpdate(message, updates)
applyServerMessageUpdate(conversation.messages[messageIndex], updates)
// ack 命中末尾消息时按服务端 sendTime / content 重算会话摘要,让会话列表跟着权威值排序
if (messageIndex === conversation.messages.length - 1) {
recomputeConversationLast(conversation)
}
if (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 formRef = ref() // Ref
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
})
const formRules = reactive({
word: [{ required: true, message: '敏感词不能为空', trigger: 'blur' }],
word: [{ required: true, whitespace: true, message: '敏感词不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
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 head = names.filter(Boolean).join('、')
if (members.length > 4) {
return `${head}${members.length}`
// members 只含被选好友,+1 把创建者也计入实际成员数
return `${head}${members.length + 1}`
}
return head || '群聊'
}