import { useEffect, useState, useCallback } from "react"; import { Package, ClipboardList, Bell, TrendingUp, AlertTriangle, RefreshCw, Loader2, AlertCircle, ArrowRight, Clock, MessageCircle, } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { Radio, DatePicker, Drawer, Input } from "antd"; import dayjs, { type Dayjs } from "dayjs"; import { fetchDashboardStats, fetchWipTasks, fetchDashboardMessages, type DashboardStats, type WipTask, type ProductMessageItem, } from "../../services/dashboardApi"; const { RangePicker } = DatePicker; // ─── 时间筛选选项 ───────────────────────────────────────── type DateRangeKey = "today" | "7d" | "30d" | "custom"; function rangeToParams(key: DateRangeKey, customRange: [Dayjs, Dayjs] | null) { if (key === "custom" && customRange) { return { since: customRange[0].startOf("day").toISOString(), until: customRange[1].endOf("day").toISOString(), }; } const since = dayjs().startOf("day"); if (key === "7d") return { since: since.subtract(7, "day").toISOString() }; if (key === "30d") return { since: since.subtract(30, "day").toISOString() }; // today return { since: since.toISOString() }; } // ─── 进度条(支持3或4段) ───────────────────────────────── function ProgressBar({ a, b, c, total, labels, d }: { a: number; b: number; c: number; total: number; labels: [string, string, string]; d?: number; }) { if (total === 0) return
暂无数据
; const pct = (n: number) => Math.round((n / total) * 100); const segs = [ { n: a, color: "bg-amber-400", label: labels[0] }, { n: b, color: "bg-blue-500", label: labels[1] }, { n: c, color: "bg-emerald-500", label: labels[2] }, ...(d !== undefined ? [{ n: d, color: "bg-red-400", label: "已驳回" }] : []), ].filter(s => s.n > 0); return (
{segs.map((s, i) => (
))}
{segs.map((s, i) => ( {s.label} {s.n}({pct(s.n)}%) ))}
); } // ─── 滞留时间 SLA 预警颜色 ──────────────────────────────── function durationColor(h: number) { if (h >= 48) return "text-red-600 bg-red-100 font-bold"; if (h >= 24) return "text-orange-600 bg-orange-50"; return "text-emerald-600 bg-emerald-50"; } function durationLabel(h: number) { if (h >= 48) return `${Math.round(h / 24)}天`; if (h >= 24) return `${Math.round(h / 24)}天`; if (h >= 1) return `${h}小时`; return `${Math.round(h * 60)}分钟`; } function WipRow({ t }: { t: WipTask }) { const nav = useNavigate(); const handleClick = () => { if (t.product_sn) { nav(`/admin/tasks?sn=${t.product_sn}`); } }; return (
{/* 主信息行:状态 + 任务名 + 负责人 */}
{t.status === "WIP" ? "进行中" : "待接收"} {t.task_name} | 负责人: {t.assignee} {/* 滞留时间 */} {durationLabel(t.duration_hours)}
{/* 附加信息行 */}
{t.material_name || "未知设备"} | {t.spec_model || "无规格"} | 序列号: {t.external_serial || "未录入"} | 身份证: {t.product_sn} {t.received_at && {t.received_at}}
); } // ─── 留言列表项(卡片式) ───────────────────────────────── const AVATAR_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444", "#06b6d4"]; function MsgRow({ m }: { m: ProductMessageItem }) { const t = m.created_at ? dayjs(m.created_at).format("YYYY-MM-DD HH:mm") : ""; const initial = (m.operator_name || "?").charAt(0); const color = AVATAR_COLORS[initial.charCodeAt(0) % AVATAR_COLORS.length]; return (
{/* 头像 */}
{initial}
{/* 内容 */}
{m.operator_name} {t}

{m.content}

{m.material_name && ( {m.material_name} · 序列号: {m.external_serial || "未录入"} )} 身份证: {m.product_sn}
); } // ─── 主组件 ─────────────────────────────────────────────── export default function AdminDashboard() { const [stats, setStats] = useState(null); const [wipTasks, setWipTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // 时间筛选 const [dateKey, setDateKey] = useState("today"); const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null); // 留言抽屉 const [msgDrawerOpen, setMsgDrawerOpen] = useState(false); const [msgKeyword, setMsgKeyword] = useState(""); const [msgData, setMsgData] = useState([]); const [msgTotal, setMsgTotal] = useState(0); const [msgLoading, setMsgLoading] = useState(false); const navigate = useNavigate(); // ── 加载主数据 ── const loadData = useCallback((key: DateRangeKey, range: [Dayjs, Dayjs] | null) => { setLoading(true); const { since, until } = rangeToParams(key, range); Promise.all([ fetchDashboardStats(since, until), fetchWipTasks(20), ]) .then(([s, w]) => { setStats(s); setWipTasks(w); setError(null); }) .catch(() => setError("加载失败,请确认后端已启动")) .finally(() => setLoading(false)); }, []); useEffect(() => { loadData(dateKey, customRange); }, [dateKey, customRange]); // ── 加载留言 ── const loadMessages = useCallback(async (kw: string) => { setMsgLoading(true); try { const res = await fetchDashboardMessages(kw, 0, 50); setMsgData(res.items); setMsgTotal(res.total); } catch { /* ignore */ } finally { setMsgLoading(false); } }, []); const openMsgDrawer = () => { setMsgDrawerOpen(true); setMsgKeyword(""); loadMessages(""); }; const onMsgSearch = (value: string) => { setMsgKeyword(value); loadMessages(value); }; // ── 加载态 ── if (loading && !stats) { return (
); } if (error || !stats) { return (
{error || "数据为空"}
); } return (
{/* ═══ 页头 + 时间筛选器 ═══ */}

📊 生产管理看板

产品 = 物理实体(身份证)| 任务 = 工序节点

{/* 时间筛选 */} { setDateKey(e.target.value); setCustomRange(null); }} size="small" optionType="button" buttonStyle="solid" > 今天 近7天 近30天 自定义 {dateKey === "custom" && ( setCustomRange(dates as [Dayjs, Dayjs] | null)} style={{ width: 240 }} placeholder={["开始", "结束"]} /> )}
{/* 提示:已完结受时间筛选 */} {dateKey !== "today" && (
📐 当前时间筛选仅影响已完成/已驳回计数,在制品和总数始终为实时快照
)} {/* ═══ 4 卡片 ═══ */}
{/* 产品流转 */}

📦 产品流转

{stats.products_total} 个

实时快照(不受时间筛选影响)

{/* 任务状态 */}

📋 任务状态

{stats.tasks_total} 个

PENDING/WIP 实时 | COMPLETED 按时段

{/* 品质 & 留言 */}

⚠️ 品质与协同

{stats.tasks_rejected + stats.tasks_rework}

驳回/返工

{/* 留言 — 可点击打开抽屉 */}
{/* 完成率 */}

✅ 流转完成率

{stats.tasks_total > 0 ? Math.round((stats.tasks_completed / stats.tasks_total) * 100) : 0}%

{stats.tasks_completed}/{stats.tasks_total}

0 ? (stats.tasks_completed / stats.tasks_total) * 251 : 0} 251`} strokeLinecap="round" />

基于时间筛选后的已完成数

{/* ═══ 在制品 + 快捷入口 ═══ */}

当前在制品(实时)

共 {wipTasks.length} 个
{wipTasks.length === 0 ? (
🎉 暂无滞留任务
) : (
状态 任务 · 负责人 滞留
{wipTasks.map((t, i) => )}
)}
{/* 快捷入口 */}

⚡ 快捷入口

{/* ═══ 留言抽屉 ═══ */} 💬 协同留言板 全厂 · {msgTotal} 条} open={msgDrawerOpen} onClose={() => setMsgDrawerOpen(false)} size="large" styles={{ body: { padding: 16, background: "#f8fafc" } }} > setMsgKeyword(e.target.value)} onSearch={onMsgSearch} allowClear onClear={() => onMsgSearch("")} enterButton className="mb-4" /> {msgLoading ? (
) : msgData.length === 0 ? (
{msgKeyword ? "未找到匹配的留言" : "暂无留言记录"}
) : (
{msgData.map(m => )} {/* 简易分页 */} {msgTotal > 30 && (
{Array.from({ length: Math.ceil(msgTotal / 30) }, (_, i) => ( ))}
)}
)}
); }