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