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}
基于时间筛选后的已完成数
{/* ═══ 在制品 + 快捷入口 ═══ */}
当前在制品(实时)
共 {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) => (
))}
)}
)}
);
}