diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index 0581912..1766d85 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -131,7 +131,7 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]: select(Task, Product.serial_number) .join(Product, Task.product_id == Product.id) .where(Task.status.in_([TASK_STATUS_PENDING, TASK_STATUS_WIP])) - .order_by(Task.received_at.asc().nulls_first(), Task.created_at.asc()) + .order_by(Task.received_at.asc().nulls_last(), Task.created_at.asc()) .limit(limit) ) result = await db.execute(stmt) diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx index 8c0b69f..26f4843 100644 --- a/frontend/src/pages/admin/AdminDashboard.tsx +++ b/frontend/src/pages/admin/AdminDashboard.tsx @@ -1,10 +1,10 @@ import { useEffect, useState, useCallback } from "react"; import { Package, ClipboardList, Bell, TrendingUp, AlertTriangle, - RefreshCw, Loader2, AlertCircle, ArrowRight, Clock, MessageCircle, Search, + RefreshCw, Loader2, AlertCircle, ArrowRight, Clock, MessageCircle, } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import { Radio, DatePicker, Drawer, Input, List, Tag } from "antd"; +import { Radio, DatePicker, Drawer, Input, Tag } from "antd"; import dayjs, { type Dayjs } from "dayjs"; import { fetchDashboardStats, fetchWipTasks, fetchDashboardMessages, @@ -102,23 +102,38 @@ function WipRow({ t }: { t: WipTask }) { ); } -// ─── 留言列表项 ─────────────────────────────────────────── +// ─── 留言列表项(卡片式) ───────────────────────────────── +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("MM-DD HH:mm") : ""; + 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 ( - -
-
- {m.operator_name} - {t} +
+
+ {/* 头像 */} +
+ {initial}
-
{m.content}
-
- {m.product_sn} - {m.material_name} + {/* 内容 */} +
+
+ {m.operator_name} + {t} +
+

{m.content}

+
+ {m.product_sn} + {m.material_name && ( + {m.material_name} + )} +
- +
); } @@ -372,43 +387,53 @@ export default function AdminDashboard() { {/* ═══ 留言抽屉 ═══ */} 💬 协同留言板 全厂 · {msgTotal} 条} open={msgDrawerOpen} onClose={() => setMsgDrawerOpen(false)} - width={520} - styles={{ body: { padding: 0 } }} + width={560} + styles={{ body: { padding: 16, background: "#f8fafc" } }} > -
- } - placeholder="搜索 SN码 / 物料名称 / 留言人 / 内容" - value={msgKeyword} - onChange={e => setMsgKeyword(e.target.value)} - onPressEnter={() => onMsgSearch(msgKeyword)} - allowClear - onClear={() => onMsgSearch("")} - /> -
- } - pagination={{ - total: msgTotal, - pageSize: 30, - size: "small", - onChange: (page, size) => { - loadMessages(msgKeyword); - // simplified: re-fetch with skip - fetchDashboardMessages(msgKeyword, (page - 1) * size, size) - .then(res => { setMsgData(res.items); setMsgTotal(res.total); }) - .catch(() => {}); - }, - showTotal: (t) => `共 ${t} 条`, - }} + 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) => ( + + ))} +
+ )} +
+ )}
);