Files
writer-work-flow/apps/web/lib/foreshadow/useForeshadow.ts
Yaojia Wang c6651b74b9 fix(web): stale-closure + focus trap + 去边界强转 + a11y/性能打磨 + 类型化客户端
P1-6 useStyleLearn/useKimiOauth 修 stale-closure 依赖。
P1-7 CommandPalette/Drawer 加 focus trap(新 lib/a11y/focusTrap)。
P1-8 SSE 解析与 server.ts 去 as 强转改类型守卫/运行时校验。
P2 删冗余强转、开 noUncheckedIndexedAccess、Toast 清理+稳定 key、列表 key 稳定化、
  rel=noopener、useMemo 大文本、ConflictCard role=group、useInjection 加锁、
  client.test 真断言。
codegen 重生成 schema.d.ts + 消费 DimensionEntry/ReviewConflictView 强类型。
2026-06-21 19:32:49 +02:00

96 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import type { ForeshadowView } from "@/lib/api/types";
import {
buildRegisterRequest,
buildTransitionRequest,
extractReason,
validationReasonMessage,
type RegisterInput,
type TransitionInput,
} from "./board";
export interface UseForeshadow {
items: ForeshadowView[];
busy: boolean;
register: (input: RegisterInput) => Promise<boolean>;
transition: (code: string, input: TransitionInput) => Promise<boolean>;
}
// 伏笔登记/状态变更(乐观更新 + 回滚 + Toast
// register成功后把返回行追加进 items422 duplicate → 友好提示,不改 items。
// transition先乐观替换该行 status失败回滚到旧 items 并 toast读 details.reason
export function useForeshadow(initial: ForeshadowView[]): UseForeshadow {
const [items, setItems] = useState<ForeshadowView[]>(initial);
const [busy, setBusy] = useState(false);
const toast = useToast();
const register = useCallback<UseForeshadow["register"]>(
async (input) => {
setBusy(true);
try {
const { data, error } = await api.POST(
"/projects/{project_id}/foreshadow",
{
params: { path: { project_id: input.projectId } },
body: buildRegisterRequest(input),
},
);
if (error || !data) {
toast(validationReasonMessage(extractReason(error)), "error");
return false;
}
setItems((prev) => [...prev, data]);
toast(`已登记伏笔 ${data.code}`, "success");
return true;
} finally {
setBusy(false);
}
},
[toast],
);
const transition = useCallback<UseForeshadow["transition"]>(
async (code, input) => {
const snapshot = items;
// 乐观:本地先改 status若给了 toStatus
if (input.toStatus) {
const next = input.toStatus;
setItems((prev) =>
prev.map((it) => (it.code === code ? { ...it, status: next } : it)),
);
}
setBusy(true);
try {
const { data, error } = await api.PATCH(
"/projects/{project_id}/foreshadow/{code}",
{
params: {
path: { project_id: input.projectId, code },
},
body: buildTransitionRequest(input),
},
);
if (error || !data) {
setItems(snapshot); // 回滚
toast(validationReasonMessage(extractReason(error)), "error");
return false;
}
// 用服务端权威行替换status + progress 都对齐)。
setItems((prev) => prev.map((it) => (it.code === code ? data : it)));
toast(`已更新 ${data.code}`, "success");
return true;
} finally {
setBusy(false);
}
},
[items, toast],
);
return { items, busy, register, transition };
}