fix: 自由创作完成后同步余额通知
This commit is contained in:
@@ -218,6 +218,13 @@ export function App() {
|
|||||||
if (data) setUnreadCount(data.unread_count);
|
if (data) setUnreadCount(data.unread_count);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 自由创作自行管理长视频轮询;任务终态后只需同步顶栏依赖的两项全局数据,
|
||||||
|
// 不必像通用 action 一样重拉商品、项目和素材列表。
|
||||||
|
const refreshFreeCreateShell = useCallback(() => {
|
||||||
|
void api.billingSummary().then((summary) => setBilling(summary)).catch(() => {});
|
||||||
|
void reloadNotifications();
|
||||||
|
}, [reloadNotifications]);
|
||||||
|
|
||||||
// YYX#row22:拉未读生成任务汇总(导航胶囊 + 商品角标)
|
// YYX#row22:拉未读生成任务汇总(导航胶囊 + 商品角标)
|
||||||
const reloadAiUnread = useCallback(async () => {
|
const reloadAiUnread = useCallback(async () => {
|
||||||
const data = await api.aiTasksUnread().catch(() => null);
|
const data = await api.aiTasksUnread().catch(() => null);
|
||||||
@@ -925,7 +932,7 @@ export function App() {
|
|||||||
case "assetFactory":
|
case "assetFactory":
|
||||||
return <AssetFactoryPage navigate={navigate} />;
|
return <AssetFactoryPage navigate={navigate} />;
|
||||||
case "freeCreate":
|
case "freeCreate":
|
||||||
return <FreeCreatePage modelConfigs={modelConfigs} onNotify={(type, text) => setNotice({ type, text })} />;
|
return <FreeCreatePage modelConfigs={modelConfigs} onNotify={(type, text) => setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} />;
|
||||||
case "imageOptimize":
|
case "imageOptimize":
|
||||||
return <ImageWorkbenchPage mode="image" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} />;
|
return <ImageWorkbenchPage mode="image" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} />;
|
||||||
case "modelPhoto":
|
case "modelPhoto":
|
||||||
|
|||||||
@@ -47,15 +47,20 @@ function pollDelay(count: number): number {
|
|||||||
let refKeySeq = 0;
|
let refKeySeq = 0;
|
||||||
const nextRefKey = () => `ref-${Date.now()}-${refKeySeq++}`;
|
const nextRefKey = () => `ref-${Date.now()}-${refKeySeq++}`;
|
||||||
|
|
||||||
export function FreeCreatePage({ modelConfigs, onNotify }: {
|
export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled }: {
|
||||||
modelConfigs: ModelConfig[];
|
modelConfigs: ModelConfig[];
|
||||||
onNotify: (type: "success" | "error" | "info", text: string) => void;
|
onNotify: (type: "success" | "error" | "info", text: string) => void;
|
||||||
|
onTaskSettled: () => void;
|
||||||
}) {
|
}) {
|
||||||
// App 每次渲染会重建 onNotify 箭头函数;用 ref 稳定身份,否则依赖它的 effect(首屏拉取/轮询)
|
// App 每次渲染会重建 onNotify 箭头函数;用 ref 稳定身份,否则依赖它的 effect(首屏拉取/轮询)
|
||||||
// 会随 App 任意 setState(如 30s 未读轮询)反复重跑 → 任务流无谓全量刷新(实测踩坑)。
|
// 会随 App 任意 setState(如 30s 未读轮询)反复重跑 → 任务流无谓全量刷新(实测踩坑)。
|
||||||
const onNotifyRef = useRef(onNotify);
|
const onNotifyRef = useRef(onNotify);
|
||||||
onNotifyRef.current = onNotify;
|
onNotifyRef.current = onNotify;
|
||||||
const notify = useCallback((type: "success" | "error" | "info", text: string) => onNotifyRef.current(type, text), []);
|
const notify = useCallback((type: "success" | "error" | "info", text: string) => onNotifyRef.current(type, text), []);
|
||||||
|
// 同 onNotify:App 外壳重渲染时回调身份会变化,不能让任务列表/轮询 effect 因此重启。
|
||||||
|
const onTaskSettledRef = useRef(onTaskSettled);
|
||||||
|
onTaskSettledRef.current = onTaskSettled;
|
||||||
|
const refreshGlobalShell = useCallback(() => onTaskSettledRef.current(), []);
|
||||||
const videoConfigs = useMemo(
|
const videoConfigs = useMemo(
|
||||||
() => modelConfigs.filter((c) => c.capability === "video" && FC_MODELS.some((m) => m.name === c.name)),
|
() => modelConfigs.filter((c) => c.capability === "video" && FC_MODELS.some((m) => m.name === c.name)),
|
||||||
[modelConfigs]
|
[modelConfigs]
|
||||||
@@ -128,6 +133,8 @@ export function FreeCreatePage({ modelConfigs, onNotify }: {
|
|||||||
} else {
|
} else {
|
||||||
stopPolling(id);
|
stopPolling(id);
|
||||||
setProgress((prev) => { const next = { ...prev }; delete next[id]; sessionStorage.setItem(PROGRESS_KEY, JSON.stringify(next)); return next; });
|
setProgress((prev) => { const next = { ...prev }; delete next[id]; sessionStorage.setItem(PROGRESS_KEY, JSON.stringify(next)); return next; });
|
||||||
|
// 任务结算会改变余额,并产生已有的生成/计费消息;同步 App 外壳即可,页面任务流仍由 patchTask 管理。
|
||||||
|
refreshGlobalShell();
|
||||||
if (data.task.status === "succeeded") notify("success", "视频生成完成");
|
if (data.task.status === "succeeded") notify("success", "视频生成完成");
|
||||||
else if (data.task.status === "failed") notify("error", data.task.error_message || "视频生成失败");
|
else if (data.task.status === "failed") notify("error", data.task.error_message || "视频生成失败");
|
||||||
}
|
}
|
||||||
@@ -137,7 +144,7 @@ export function FreeCreatePage({ modelConfigs, onNotify }: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
pollTimersRef.current.set(id, window.setTimeout(() => void tick(), pollDelay(pollCountsRef.current.get(id) || 0)));
|
pollTimersRef.current.set(id, window.setTimeout(() => void tick(), pollDelay(pollCountsRef.current.get(id) || 0)));
|
||||||
}, [patchTask, stopPolling, notify]);
|
}, [patchTask, stopPolling, notify, refreshGlobalShell]);
|
||||||
|
|
||||||
// 平滑进度动画:每 2s 给在途任务 +0.6~1.6%,封顶 95,sessionStorage 持久(刷新可续)
|
// 平滑进度动画:每 2s 给在途任务 +0.6~1.6%,封顶 95,sessionStorage 持久(刷新可续)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -332,7 +339,12 @@ export function FreeCreatePage({ modelConfigs, onNotify }: {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
if (isInFlight(data.task.status)) schedulePoll(data.task.id);
|
if (isInFlight(data.task.status)) schedulePoll(data.task.id);
|
||||||
|
else {
|
||||||
|
// 极快失败/成功时不会进入 schedulePoll,也要同步一次全局余额与消息徽标。
|
||||||
|
refreshGlobalShell();
|
||||||
|
if (data.task.status === "succeeded") notify("success", "视频生成完成");
|
||||||
else if (data.task.status === "failed") notify("error", data.task.error_message || "创建任务失败");
|
else if (data.task.status === "failed") notify("error", data.task.error_message || "创建任务失败");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== localId));
|
setTasks((prev) => prev.filter((t) => t.id !== localId));
|
||||||
@@ -341,7 +353,7 @@ export function FreeCreatePage({ modelConfigs, onNotify }: {
|
|||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [schedulePoll, notify]);
|
}, [schedulePoll, notify, refreshGlobalShell]);
|
||||||
|
|
||||||
const handleSend = useCallback(async () => {
|
const handleSend = useCallback(async () => {
|
||||||
const prompt = promptRef.current?.getText() || "";
|
const prompt = promptRef.current?.getText() || "";
|
||||||
|
|||||||
Reference in New Issue
Block a user