"use client"; import { FormEvent, ReactNode, useEffect, useMemo, useState } from "react"; type Row = Record; type ConsoleData = { people: Row[]; workshops: Row[]; devices: Row[]; weighings: Row[]; events: Row[]; reconciliations: Row[]; traces: Row[]; tags: Row[]; alerts: Row[]; settings: Record; logs: Row[]; serverTime: number; }; type PageKey = | "dashboard" | "weighings" | "events" | "reconcile" | "trace" | "assets" | "alerts" | "settings" | "simulator"; type RoleKey = "warehouse" | "worker" | "admin"; const pageMeta: Record = { dashboard: { name: "工作台", en: "Dashboard", icon: "◫" }, weighings: { name: "废料称重", en: "Weighing records", icon: "▦" }, events: { name: "事件列表", en: "Events", icon: "◎" }, reconcile: { name: "地磅核对", en: "Reconciliation", icon: "⚖" }, trace: { name: "导体追溯", en: "Cable trace", icon: "⌁" }, assets: { name: "人员设备", en: "People & devices", icon: "♙" }, alerts: { name: "告警中心", en: "Alerts", icon: "!" }, settings: { name: "系统设置", en: "Settings", icon: "⌘" }, simulator: { name: "终端模拟", en: "Terminal simulator", icon: "▣" }, }; const roleMeta: Record = { warehouse: { name: "仓管员", person: "张三", pages: ["dashboard", "weighings", "events", "reconcile", "assets", "simulator"], }, worker: { name: "产线工人", person: "李四", pages: ["dashboard", "trace", "assets", "simulator"], }, admin: { name: "管理员", person: "王工", pages: Object.keys(pageMeta) as PageKey[], }, }; const navGroups: Array<{ title: string; pages: PageKey[] }> = [ { title: "总览", pages: ["dashboard"] }, { title: "废料库回收", pages: ["weighings", "events", "reconcile"] }, { title: "导体称重", pages: ["trace"] }, { title: "运营管理", pages: ["assets", "alerts"] }, { title: "系统", pages: ["settings"] }, { title: "演示工具", pages: ["simulator"] }, ]; const n = (row: Row, key: string) => Number(row[key] ?? 0); const s = (row: Row, key: string) => String(row[key] ?? ""); function formatTime(value: string | number | null, includeDate = false) { const date = new Date(Number(value ?? 0)); if (Number.isNaN(date.getTime())) return "—"; return new Intl.DateTimeFormat("zh-CN", { ...(includeDate ? { month: "2-digit", day: "2-digit" } : {}), hour: "2-digit", minute: "2-digit", hour12: false, }).format(date); } function Status({ value }: { value: string }) { const level = /超标|严重|失败|离线|停用|损坏|异常/.test(value) ? "bad" : /警告|预警|处理中|待推送|需更新|未用/.test(value) ? "warn" : /在线|正常|成功|已同步|已确认|已录入|在用|已自动|已推送/.test(value) ? "ok" : "muted"; return {value}; } function Card({ title, subtitle, tools, children, className = "", }: { title?: string; subtitle?: string; tools?: ReactNode; children: ReactNode; className?: string; }) { return (
{(title || tools) && (
{title &&

{title}

} {subtitle &&

{subtitle}

}
{tools &&
{tools}
}
)} {children}
); } function DataTable({ columns, rows, }: { columns: Array<{ key: string; label: string; render?: (row: Row) => ReactNode }>; rows: Row[]; }) { return (
{columns.map((column) => )} {rows.length === 0 ? ( ) : rows.map((row, index) => ( {columns.map((column) => ( ))} ))}
{column.label}
没有符合条件的数据
{column.render ? column.render(row) : String(row[column.key] ?? "—")}
); } function SearchBox({ value, onChange, placeholder = "搜索…", }: { value: string; onChange: (value: string) => void; placeholder?: string; }) { return ( ); } function exportCsv(filename: string, rows: Row[]) { if (!rows.length) return; const headers = Object.keys(rows[0]); const escape = (value: unknown) => `"${String(value ?? "").replaceAll('"', '""')}"`; const csv = "\ufeff" + [headers.map(escape).join(","), ...rows.map((row) => headers.map((key) => escape(row[key])).join(","))].join("\n"); const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" })); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); URL.revokeObjectURL(url); } export function AdminConsole() { const [data, setData] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const [page, setPage] = useState("dashboard"); const [role, setRole] = useState("admin"); const [query, setQuery] = useState(""); const [tab, setTab] = useState("records"); const [toast, setToast] = useState(""); const [clock, setClock] = useState(new Date()); const [modal, setModal] = useState<"workshop" | "tag" | "threshold" | null>(null); const [simStep, setSimStep] = useState(0); async function load() { setLoading(true); try { const response = await fetch("/api/console", { cache: "no-store" }); const payload = await response.json() as { code: number; message?: string; data?: ConsoleData }; if (!response.ok || !payload.data) throw new Error(payload.message || "数据加载失败"); setData(payload.data); setError(""); } catch (cause) { setError(cause instanceof Error ? cause.message : "数据加载失败"); } finally { setLoading(false); } } useEffect(() => { load(); const timer = window.setInterval(() => setClock(new Date()), 1_000); return () => window.clearInterval(timer); }, []); useEffect(() => { setQuery(""); if (page === "trace") setTab("trace"); else if (page === "assets") setTab("people"); else if (page === "settings") setTab("accounts"); else if (page === "weighings") setTab("records"); }, [page]); async function action(name: string, payload: Record = {}, message = "操作成功") { try { const response = await fetch("/api/console", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: name, payload }), }); const result = await response.json() as { code: number; message?: string; data?: ConsoleData }; if (!response.ok || result.code !== 0) throw new Error(result.message || "操作失败"); if (result.data) setData(result.data); setToast(message); window.setTimeout(() => setToast(""), 2_800); } catch (cause) { setToast(cause instanceof Error ? cause.message : "操作失败"); window.setTimeout(() => setToast(""), 3_600); } } function switchRole(nextRole: RoleKey) { setRole(nextRole); if (!roleMeta[nextRole].pages.includes(page)) setPage("dashboard"); setToast(`已切换为${roleMeta[nextRole].name}视角`); window.setTimeout(() => setToast(""), 2_200); } if (loading && !data) { return (

智能回收运营平台

正在连接本地数据服务…

); } if (!data) { return (
!

数据服务暂不可用

{error}

); } const visiblePages = roleMeta[role].pages; const openAlerts = data.alerts.filter((item) => s(item, "status") !== "已关闭").length; const onlineDevices = data.devices.filter((item) => s(item, "status") === "在线").length; const title = pageMeta[page]; return (

{title.name}

{title.en}
服务正常
{clock.toLocaleTimeString("zh-CN", { hour12: false })} {clock.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit", weekday: "short" })}
{(Object.keys(roleMeta) as RoleKey[]).map((key) => ( ))}
{page === "dashboard" && } {page === "weighings" && ( )} {page === "events" && } {page === "reconcile" && } {page === "trace" && ( setModal("tag")} /> )} {page === "assets" && ( )} {page === "alerts" && setModal("threshold")} />} {page === "settings" && ( setModal("workshop")} /> )} {page === "simulator" && }
{modal && ( setModal(null)} onSubmit={async (values) => { if (modal === "workshop") await action("workshop.add", values, "车间已新增"); if (modal === "tag") await action("tag.add", values, "RFID 标签已新增"); if (modal === "threshold") await action("settings.save", values, "告警阈值已保存"); setModal(null); }} /> )} {toast &&
{toast}
}
); } function Dashboard({ data, role, setPage }: { data: ConsoleData; role: RoleKey; setPage: (page: PageKey) => void }) { const todayTotal = data.weighings.reduce((total, row) => total + n(row, "weight"), 0); const netTotal = data.traces.reduce((total, row) => total + n(row, "net"), 0); const pending = data.reconciliations.filter((row) => s(row, "verdict") !== "正常").length; const deviceOnline = data.devices.filter((row) => s(row, "status") === "在线").length; const showWaste = role !== "worker"; const kpis = showWaste ? [ ["今日废料投放", `${todayTotal.toFixed(1)} kg`, "按已接收记录实时汇总", "orange"], ["称重次数", `${data.weighings.length} 次`, "原子称重记录", "blue"], ["待核对", `${pending} 条`, "警告与超标记录", "amber"], ["设备在线率", `${deviceOnline}/${data.devices.length}`, "最近心跳状态", "green"], ] : [ ["今日称重轴数", `${data.traces.length} 轴`, "导体追溯记录", "blue"], ["净重累计", `${netTotal.toFixed(1)} kg`, "毛重减皮重", "green"], ["MES 待推送", `${data.traces.filter((row) => s(row, "mes_status") !== "已推送").length} 条`, "自动重试队列", "amber"], ["RFID 标签", `${data.tags.length} 个`, "在用与备用标签", "orange"], ]; return (
你好,{roleMeta[role].person}

{roleMeta[role].name}视角 · 今日运营概览

{kpis.map(([label, value, note, color]) => (
{label} {value} {note}
))}
{showWaste ? (
12.48kg

采样稳定 · 精度 ±10g

操作人
张三
机台
MC-101
类别
废铜
来源
小秤台
) : ( `${n(row, "net").toFixed(1)} kg` }, { key: "mes_status", label: "MES", render: (row) => }, ]} /> )}
{data.devices.slice(0, 6).map((device) => (
{s(device, "id")}{s(device, "type")} {formatTime(device.last_seen)}
))}
{[54, 72, 61, 86, 66, 78, 93].map((height, index) => (
{["一", "二", "三", "四", "五", "六", "日"][index]}
))}
{data.weighings.slice(0, 5).map((row) => (
{s(row, "operator")} · {s(row, "machine")} · {s(row, "category")} {n(row, "weight").toFixed(2)} kg
))}
); } function Weighings({ data, query, setQuery, tab, setTab }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void }) { const rows = useMemo(() => { const key = query.toLowerCase(); return data.weighings.filter((row) => !key || [row.operator, row.machine, row.category, row.event_id].some((value) => String(value ?? "").toLowerCase().includes(key))); }, [data.weighings, query]); const categoryTotals = ["废铜", "废电线", "废铝", "废锡"].map((category) => ({ category, value: rows.filter((row) => s(row, "category") === category).reduce((total, row) => total + n(row, "weight"), 0), })); return ( exportCsv("废料称重记录.csv", rows)}>导出 CSV} >
{tab === "records" ? ( formatTime(row.created_at, true) }, { key: "operator", label: "操作人" }, { key: "machine", label: "关联机台" }, { key: "category", label: "废料类别" }, { key: "source", label: "来源设备" }, { key: "weight", label: "精确重量", render: (row) => {n(row, "weight").toFixed(2)} kg }, { key: "event_id", label: "所属事件" }, { key: "status", label: "状态", render: (row) => }, ]} /> ) : (
{categoryTotals.map((item) => (
{item.category} {item.value.toFixed(1)} kg
))}
)}
); } function EventsPage({ data, query, setQuery }: { data: ConsoleData; query: string; setQuery: (value: string) => void }) { const rows = data.events.filter((row) => !query || `${s(row, "id")}${s(row, "operator")}`.toLowerCase().includes(query.toLowerCase())); return ( exportCsv("事件列表.csv", rows)}>导出 CSV}>
formatTime(row.entered_at, true) }, { key: "left_at", label: "离门时间", render: (row) => formatTime(row.left_at) }, { key: "duration", label: "历时", render: (row) => `${Math.max(1, Math.round((n(row, "left_at") - n(row, "entered_at")) / 60_000))} 分钟` }, { key: "atomic_count", label: "原子笔数" }, { key: "scale_total", label: "台秤总重", render: (row) => `${n(row, "scale_total").toFixed(2)} kg` }, { key: "verdict", label: "判定", render: (row) => }, ]} />
); } function Reconcile({ data }: { data: ConsoleData }) { return ( exportCsv("地磅核对.csv", data.reconciliations)}>导出 CSV}> `${n(row, "small_scale").toFixed(2)} kg` }, { key: "floor_scale", label: "地磅增量", render: (row) => `${n(row, "floor_scale").toFixed(2)} kg` }, { key: "difference", label: "偏差值", render: (row) => `${n(row, "difference").toFixed(2)} kg` }, { key: "difference_rate", label: "偏差率", render: (row) => `${n(row, "difference_rate").toFixed(2)}%` }, { key: "verdict", label: "判定", render: (row) => }, { key: "storage_status", label: "入库状态", render: (row) => }, ]} /> ); } function TracePage({ data, query, setQuery, tab, setTab, action, openTag }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void; action: (name: string, payload?: Record, message?: string) => Promise; openTag: () => void }) { const rows = data.traces.filter((row) => !query || `${s(row, "task_id")}${s(row, "rfid")}${s(row, "batch")}`.toLowerCase().includes(query.toLowerCase())); return (
{tab === "trace" ? : }
{tab === "trace" ? formatTime(row.created_at, true) }, { key: "workshop", label: "所属车间" }, { key: "task_id", label: "任务号" }, { key: "rfid", label: "RFID 标签" }, { key: "tare", label: "皮重", render: (row) => `${n(row, "tare").toFixed(2)} kg` }, { key: "gross", label: "毛重", render: (row) => `${n(row, "gross").toFixed(2)} kg` }, { key: "net", label: "净重", render: (row) => {n(row, "net").toFixed(2)} kg }, { key: "machine", label: "机台" }, { key: "batch", label: "批次号" }, { key: "mes_status", label: "MES 推送", render: (row) => }, ]} /> : }, { key: "name", label: "名称" }, { key: "last_checked", label: "最后检测", render: (row) => row.last_checked ? formatTime(row.last_checked, true) : "—" }, { key: "actions", label: "操作", render: (row) => (
) }, ]} />}
); } function Assets({ data, query, setQuery, tab, setTab, role, action }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void; role: RoleKey; action: (name: string, payload?: Record, message?: string) => Promise }) { const people = data.people.filter((row) => !query || `${s(row, "name")}${s(row, "id")}`.toLowerCase().includes(query.toLowerCase())); return (
{tab === "people" && } {tab === "people" && role === "admin" && }
{tab === "people" && role === "admin" ? ( ) : s(row, "workshop") }, { key: "role", label: "角色" }, { key: "face_status", label: "人脸状态", render: (row) => }, { key: "sync_status", label: "同步状态", render: (row) => }, { key: "active", label: "账号状态", render: (row) => }, { key: "actions", label: "操作", render: (row) => role === "admin" ? (
) : "只读" }, ]} />} {tab === "machines" && }, { key: "mes", label: "MES 同步", render: (row) => }, ]} />} {tab === "devices" && (
{data.devices.map((device) => (
{s(device, "id")}

{s(device, "type")}

{s(device, "ip")} · 心跳 {s(device, "heartbeat_seconds")}s

{n(device, "fill_percent") > 0 && ( <>
满溢率 {n(device, "fill_percent")}% · {s(device, "category")} )}
最后心跳 {formatTime(device.last_seen)}
))}
)}
); } function AlertsPage({ data, role, action, openThreshold }: { data: ConsoleData; role: RoleKey; action: (name: string, payload?: Record, message?: string) => Promise; openThreshold: () => void }) { return ( 阈值设置}>
{data.alerts.map((alert) => (
!

{s(alert, "type")}

{s(alert, "description")}

{formatTime(alert.created_at, true)} · {s(alert, "source")} · {s(alert, "reference")}
{role === "admin" && s(alert, "status") !== "已关闭" && (
)}
))}
); } function SettingsPage({ data, tab, setTab, action, openWorkshop }: { data: ConsoleData; tab: string; setTab: (value: string) => void; action: (name: string, payload?: Record, message?: string) => Promise; openWorkshop: () => void }) { const tabs = [["accounts", "账号与权限"], ["workshops", "车间字典"], ["monitor", "设备监控"], ["mes", "MES 接口"], ["logs", "操作日志"]]; return (
{tabs.map(([key, label]) => )}
{tab === "accounts" && } {tab === "workshops" && ( <>
车间主数据字典
`${data.people.filter((person) => s(person, "workshop") === s(row, "name")).length} 人` }, { key: "actions", label: "操作", render: (row) =>
}, ]} /> )} {tab === "monitor" && `${s(row, "heartbeat_seconds")} 秒` }, { key: "last_seen", label: "最后心跳", render: (row) => formatTime(row.last_seen, true) }, { key: "status", label: "状态", render: (row) => }, ]} />} {tab === "mes" && } {tab === "logs" && formatTime(row.created_at, true) }, { key: "actor", label: "操作人" }, { key: "action", label: "动作" }, { key: "details", label: "详情" }, { key: "level", label: "级别", render: (row) => }, ]} />}
); } function MesSettings({ settings, action }: { settings: Record; action: (name: string, payload?: Record, message?: string) => Promise }) { const [url, setUrl] = useState(settings.mes_url ?? ""); const [timeout, setTimeoutValue] = useState(settings.mes_timeout ?? "10"); const [testing, setTesting] = useState(false); return (
{!testing && }
); } function Simulator({ step, setStep }: { step: number; setStep: (value: number) => void }) { const wasteSteps = [ ["身份验证", "请将面部对准摄像头,也可输入工号", "模拟识别成功 · 张三"], ["门锁已开启", "请进入投放,最长停留 05:00", "模拟投放完成并关门"], ["投放完成", "台秤 38.50kg · 地磅 39.00kg · 偏差 1.3%", "重新开始"], ]; const cableSteps = [ ["导体轴称重", "RFID 全程关联生产任务与批次", "开始一次称重"], ["一次称重完成", "RFID E280-6102-0345 · 皮重 152.30kg", "模拟二次称重"], ["MES 上报成功", "净重 115.80kg · 条码已打印", "重新开始"], ]; return (
{[["废料库入库终端", wasteSteps, "orange"], ["导体称重终端", cableSteps, "blue"]].map(([name, steps, color], index) => { const items = steps as string[][]; const current = index === 0 ? step % 3 : Math.floor(step / 3) % 3; return ( ); })}
); } function Modal({ type, settings, onClose, onSubmit }: { type: "workshop" | "tag" | "threshold"; settings: Record; onClose: () => void; onSubmit: (values: Record) => Promise }) { const [saving, setSaving] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); setSaving(true); const values = Object.fromEntries(new FormData(event.currentTarget).entries()) as Record; await onSubmit(values); setSaving(false); } return (
{ if (event.currentTarget === event.target) onClose(); }}>

{type === "workshop" ? "新增车间" : type === "tag" ? "添加 RFID 标签" : "告警阈值设置"}

{type === "workshop" && <> } {type === "tag" && } {type === "threshold" && <> }
); }