由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - Roles 多组勾选、Schedules 月视图新建、useDirtyGuard 稳定引用 - JinshujuMatchModal success:false、Attendance store 订阅、EditableCell Enter、DynamicChart 空数据、渲染期 ref - AttachmentsTab 附件预览不再 window.open(防存储型 XSS)+ 请求乱序防护 - 考勤 late 计入出勤率与主状态 Reviewed-by: OCR (open-codereview.ai)
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
import { useCallback, useRef, useState } from 'react';
|
||
import { useLayoutEffect } from 'react';
|
||
import type { XAgentCommand_v0_9 } from '@ant-design/x-card';
|
||
|
||
/**
|
||
* 提交状态管理:收敛 DynamicForm / DynamicReview 中重复的
|
||
* submitting / submitted / error 状态与「防重复提交 + 失败可重试」逻辑。
|
||
*
|
||
* 用法:
|
||
* const { submitting, submitted, error, run } = useSubmissionState();
|
||
* const handleSubmit = (values) => run(async () => { await onSubmit(values); });
|
||
*/
|
||
export function useSubmissionState() {
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [submitted, setSubmitted] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const submittingRef = useRef(false);
|
||
|
||
const run = useCallback(async (task: () => Promise<void> | void) => {
|
||
if (submittingRef.current) return;
|
||
submittingRef.current = true;
|
||
setSubmitting(true);
|
||
setError(null);
|
||
try {
|
||
await task();
|
||
setSubmitted(true);
|
||
} catch (reason) {
|
||
setError(reason instanceof Error ? reason.message : '提交失败,请稍后重试');
|
||
} finally {
|
||
submittingRef.current = false;
|
||
setSubmitting(false);
|
||
}
|
||
}, []);
|
||
|
||
const reset = useCallback(() => {
|
||
setSubmitted(false);
|
||
setError(null);
|
||
}, []);
|
||
|
||
return { submitting, submitted, error, run, reset };
|
||
}
|
||
|
||
/**
|
||
* A2UI surface 的 XCard commands 增量更新生命周期:
|
||
* 每个 surface 只创建一次,后续通过 updateDataModel / updateComponents 增量更新。
|
||
*
|
||
* 用法:
|
||
* const { pushCommands } = useXCardSurface(surfaceId);
|
||
* useEffect(() => {
|
||
* pushCommands([
|
||
* { version: 'v0.9', createSurface: { surfaceId, catalogId } },
|
||
* { version: 'v0.9', updateDataModel: { surfaceId, path: '/x', value } },
|
||
* { version: 'v0.9', updateComponents: { surfaceId, components } },
|
||
* ]);
|
||
* }, [value]);
|
||
*/
|
||
export function useXCardSurface(surfaceId: string) {
|
||
const commandsRef = useRef<XAgentCommand_v0_9[]>([]);
|
||
const [commands, setCommands] = useState<XAgentCommand_v0_9[]>([]);
|
||
const idRef = useRef<string>('');
|
||
|
||
const pushCommands = useCallback(
|
||
(cmds: XAgentCommand_v0_9[]) => {
|
||
if (cmds.length === 0) return;
|
||
// 同一 surface 的 createSurface 命令只允许出现一次,自动去重
|
||
const hasSurface = commandsRef.current.some(
|
||
(c) => 'createSurface' in c && c.createSurface.surfaceId === surfaceId,
|
||
);
|
||
const filtered = hasSurface
|
||
? cmds.filter((c) => !('createSurface' in c))
|
||
: cmds;
|
||
commandsRef.current = [...commandsRef.current, ...filtered];
|
||
setCommands([...commandsRef.current]);
|
||
},
|
||
[surfaceId],
|
||
);
|
||
|
||
const surfaceKey = surfaceId;
|
||
// 渲染期保持纯函数:ref 变更放到 layout effect 里
|
||
useLayoutEffect(() => {
|
||
if (idRef.current !== surfaceKey) {
|
||
// 组件复用到新 surface 时,清空历史命令重新初始化
|
||
commandsRef.current = [];
|
||
idRef.current = surfaceKey;
|
||
}
|
||
}, [surfaceKey]);
|
||
|
||
return { commands, pushCommands };
|
||
}
|