fix(admin): 编辑消息/切换会话保护、行内编辑撤销与上传进度反馈
- 编辑旧消息会删除其后全部消息时,先弹确认告知影响范围 - 有待发送附件时切换会话,先确认再丢弃(避免静默删除已上传文件) - EditableCell 保存成功后提供 6 秒内"撤销"入口(把旧值重新保存), 重新编辑时自动清除 - 导入向导/重新上传显示上传进度条与百分比,不再"假死" - AI 附件上传透传进度给附件列表 - 租赁合同上传显示按钮 loading 与上传百分比
This commit is contained in:
@@ -189,11 +189,32 @@ const AiChatDrawer: React.FC<AiChatDrawerProps> = ({ open, onClose, onRequesting
|
||||
|
||||
const switchConversation = useCallback(
|
||||
(key: string) => {
|
||||
discardPendingAttachments();
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
setActiveConversationKey(key);
|
||||
const doSwitch = () => {
|
||||
discardPendingAttachments();
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
setActiveConversationKey(key);
|
||||
};
|
||||
// 有待发送的附件时先确认,避免静默删除已上传文件
|
||||
if (uploadItems.length > 0) {
|
||||
modal.confirm({
|
||||
title: '切换会话将丢弃未发送的附件',
|
||||
content: `当前有 ${uploadItems.length} 个已上传但未发送的附件,切换会话后将被删除,此操作不可恢复。`,
|
||||
okText: '切换并丢弃',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '留在当前会话',
|
||||
onOk: doSwitch,
|
||||
});
|
||||
return;
|
||||
}
|
||||
doSwitch();
|
||||
},
|
||||
[discardPendingAttachments, isMobile, setActiveConversationKey],
|
||||
[
|
||||
discardPendingAttachments,
|
||||
isMobile,
|
||||
modal,
|
||||
setActiveConversationKey,
|
||||
uploadItems.length,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -30,12 +30,19 @@ export const aiChatApi = {
|
||||
`${basePath}/${conversationId}/messages/${messageId}`,
|
||||
)
|
||||
).data,
|
||||
uploadAttachment: async (file: File): Promise<AiAttachment> => {
|
||||
uploadAttachment: async (
|
||||
file: File,
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<AiAttachment> => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return (
|
||||
await api.post<AiApiResponse<AiAttachment>>('/ai/chat/attachments', form, {
|
||||
timeout: 120_000,
|
||||
onUploadProgress: (event) => {
|
||||
if (!onProgress || !event.total) return;
|
||||
onProgress(Math.min(Math.round((event.loaded / event.total) * 100), 100));
|
||||
},
|
||||
})
|
||||
).data;
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user