fix(admin): 编辑消息/切换会话保护、行内编辑撤销与上传进度反馈

- 编辑旧消息会删除其后全部消息时,先弹确认告知影响范围
- 有待发送附件时切换会话,先确认再丢弃(避免静默删除已上传文件)
- EditableCell 保存成功后提供 6 秒内"撤销"入口(把旧值重新保存),
  重新编辑时自动清除
- 导入向导/重新上传显示上传进度条与百分比,不再"假死"
- AI 附件上传透传进度给附件列表
- 租赁合同上传显示按钮 loading 与上传百分比
This commit is contained in:
2026-08-07 17:38:21 +08:00
parent 00e2bc5acf
commit 8dc72d6e1b
9 changed files with 220 additions and 35 deletions

View File

@@ -296,30 +296,46 @@ export function useAiChatMessageActions({
setEditingMessageId(null);
if (content === messageInfo.message.content) return;
setMessage(messageInfo.id, (info) => ({
message: {
...info.message,
content,
metadata: { ...info.message.metadata, edited: true },
},
}));
// 编辑旧消息会删除其后的全部消息并重新生成,需先告知用户
const index = messagesRef.current.findIndex((item) => item.id === messageInfo.id);
if (index >= 0) {
for (const item of messagesRef.current.slice(index + 1)) removeMessage(item.id);
const followingCount = index >= 0 ? messagesRef.current.length - index - 1 : 0;
const doEdit = () => {
setMessage(messageInfo.id, (info) => ({
message: {
...info.message,
content,
metadata: { ...info.message.metadata, edited: true },
},
}));
if (index >= 0) {
for (const item of messagesRef.current.slice(index + 1)) removeMessage(item.id);
}
requestWithStatus({
message: content,
attachmentIds: [],
skillKey: activeConversation?.lockedSkillKey ?? null,
clientRequestId: crypto.randomUUID(),
reasoningEffort: deepThinking ? 'high' : null,
editMessageId: messageId,
});
};
if (followingCount > 0) {
modal.confirm({
title: '编辑消息将删除后续内容',
content: `编辑这条消息会删除其后的 ${followingCount} 条消息并重新生成回答,此操作不可恢复。`,
okText: '继续编辑',
cancelText: '取消',
onOk: doEdit,
});
return;
}
requestWithStatus({
message: content,
attachmentIds: [],
skillKey: activeConversation?.lockedSkillKey ?? null,
clientRequestId: crypto.randomUUID(),
reasoningEffort: deepThinking ? 'high' : null,
editMessageId: messageId,
});
doEdit();
},
[
activeConversation?.lockedSkillKey,
activeId,
deepThinking,
modal,
removeMessage,
requestWithStatus,
setMessage,
@@ -426,7 +442,9 @@ export function useAiChatMessageActions({
return;
}
try {
const uploaded = await aiChatApi.uploadAttachment(file);
const uploaded = await aiChatApi.uploadAttachment(file, (percent) => {
options.onProgress?.({ percent });
});
setAttachments((items) => [...items, uploaded]);
options.onSuccess?.(uploaded, file);
} catch (error) {