Files
ftb-project-management/apps/web/components/ServerDataSaveErrorBanner.tsx
2026-07-03 19:36:18 +08:00

82 lines
2.7 KiB
TypeScript
Raw 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 { 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>
);
}