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

@@ -102,8 +102,18 @@ const EditableCell = <Value,>({
const [draft, setDraft] = useState<unknown>(() =>
normalizeEditableValue(formatValue ? formatValue(value) : value, editor),
);
// 保存成功后短暂显示「撤销」入口:记录保存前的序列化旧值
const [undoMeta, setUndoMeta] = useState<{ serializedPrevious: unknown } | null>(null);
const undoTimerRef = useRef<number | undefined>(undefined);
const enabled = !disabled && (!permission || hasPermission(permission));
useEffect(
() => () => {
window.clearTimeout(undoTimerRef.current);
},
[],
);
const original = useMemo(
() =>
serializeEditableValue(
@@ -133,10 +143,15 @@ const EditableCell = <Value,>({
return true;
}
setSaving(true);
const previousValue = original;
try {
await onSave(parseValue ? parseValue(serialized) : (serialized as Value));
if (activeCell?.id === idRef.current) activeCell = null;
setEditing(false);
// 提供 6 秒内的撤销入口(把旧值再保存一次)
setUndoMeta({ serializedPrevious: previousValue });
window.clearTimeout(undoTimerRef.current);
undoTimerRef.current = window.setTimeout(() => setUndoMeta(null), 6_000);
return true;
} catch (error) {
message.error(getErrorMessage(error, '保存失败'));
@@ -197,10 +212,29 @@ const EditableCell = <Value,>({
if (!saved) return;
}
activeCell = { id: idRef.current, save };
// 重新进入编辑时清掉上一次的撤销入口
window.clearTimeout(undoTimerRef.current);
setUndoMeta(null);
setDraft(normalizeEditableValue(formatValue ? formatValue(value) : value, editor));
setEditing(true);
};
const handleUndo = async () => {
if (!undoMeta) return;
window.clearTimeout(undoTimerRef.current);
setUndoMeta(null);
try {
await onSave(
parseValue
? parseValue(undoMeta.serializedPrevious)
: (undoMeta.serializedPrevious as Value),
);
message.success('已撤销修改');
} catch (error) {
message.error(getErrorMessage(error, '撤销失败'));
}
};
const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.pointerType !== 'touch' || editing) return;
touchStartRef.current = {
@@ -308,7 +342,23 @@ const EditableCell = <Value,>({
{editing ? (
<Spin spinning={saving}>{control}</Spin>
) : (
<Tooltip title={enabled ? '双击编辑,触屏双击编辑' : undefined}>{children}</Tooltip>
<Tooltip title={enabled ? '双击编辑,触屏双击编辑' : undefined}>
<span className="editable-cell-display">
{children}
{undoMeta ? (
<button
type="button"
className="editable-cell-undo"
onClick={(event) => {
event.stopPropagation();
void handleUndo();
}}
>
</button>
) : null}
</span>
</Tooltip>
)}
</div>
);