fix(data): 增加全局保存失败保护

This commit is contained in:
Script Generator
2026-07-03 19:36:18 +08:00
parent 516a5f1b63
commit b4a3c990b4
17 changed files with 727 additions and 132 deletions

View File

@@ -0,0 +1,81 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { AlertTriangle, X } from 'lucide-react';
import {
SERVER_DATA_SAVE_ERROR_EVENT,
type ServerDataKey,
type ServerDataSaveErrorDetail,
} from '@/lib/server-data';
const KEY_LABELS: Record<ServerDataKey, string> = {
'products-overview': '产品/项目/版本',
requirements: '需求池',
'version-plans': '版本计划',
'dev-tasks': '开发任务',
'test-cases': '测试用例',
bugs: 'BUG',
members: '成员/账号',
'task-categories': '任务类型',
'task-worklogs': '工时日志',
'work-activities': '工作动态',
'xiaobao-risk-insights': '小宝预警解读',
'xiaobao-risk-snapshots': '小宝预警快照',
'xiaobao-warning-views': '小宝预警查看状态',
overtime: '加班记录',
};
export function ServerDataSaveErrorBanner() {
const [detail, setDetail] = useState<ServerDataSaveErrorDetail | null>(null);
const dismissTimer = useRef<number | null>(null);
useEffect(() => {
function clearDismissTimer() {
if (!dismissTimer.current) return;
window.clearTimeout(dismissTimer.current);
dismissTimer.current = null;
}
function handleSaveError(event: Event) {
const next = (event as CustomEvent<ServerDataSaveErrorDetail>).detail;
if (!next?.key) return;
clearDismissTimer();
setDetail(next);
dismissTimer.current = window.setTimeout(() => setDetail(null), 10_000);
}
window.addEventListener(SERVER_DATA_SAVE_ERROR_EVENT, handleSaveError);
return () => {
clearDismissTimer();
window.removeEventListener(SERVER_DATA_SAVE_ERROR_EVENT, handleSaveError);
};
}, []);
if (!detail) return null;
return (
<div
role="alert"
className="fixed right-4 top-4 z-[100] flex w-[min(420px,calc(100vw-32px))] items-start gap-3 rounded-lg border border-red-200 bg-white px-4 py-3 text-left shadow-lg"
>
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
<div className="min-w-0 flex-1">
<p className="text-[13px] font-semibold text-red-700"></p>
<p className="mt-0.5 text-[12px] leading-5 text-red-600">
{KEY_LABELS[detail.key]} API/
</p>
{detail.message && (
<p className="mt-1 truncate text-[11px] text-red-500">{detail.message}</p>
)}
</div>
<button
type="button"
onClick={() => setDetail(null)}
className="rounded-md p-1 text-red-400 hover:bg-red-50 hover:text-red-600"
aria-label="关闭保存失败提示"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
);
}