✨ feat(im): 修一批正确性 / UX 细节:群名 trim 空、敏感词 / 封禁理由空白校验、默认群名计入创建者、ack 后重算会话摘要、文本重试复用 clientMessageId 防重复
parent
73aa578c9b
commit
fead282395
|
|
@ -539,12 +539,17 @@ watch(visible, (v) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 群主:保存群名(走 /im/group/update) */
|
/** 群主:保存群名(走 /im/group/update);trim 后空字符串拒提交,与 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')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 || '群聊'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue