fix: 在制品按滞留时间降序 + 留言抽屉UI美化
1. 在制品排序: nulls_first → nulls_last 已接收任务按receipt_at升序(最早=滞留最久)排最前 未接收任务(null)排最后 2. 留言抽屉UI重设计: - 卡片式布局: 彩色头像圆+边框阴影+hover效果 - 头像颜色按姓名首字自动分配7色 - Input.Search替代普通Input+Search图标 - 灰色背景区分卡片, 时间格式改为YYYY-MM-DD HH:mm - 手动分页按钮替代Antd分页器
This commit is contained in:
@ -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 (
|
||||
<List.Item>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">{m.operator_name}</span>
|
||||
<span className="text-[11px] text-gray-400">{t}</span>
|
||||
<div className="mb-3 rounded-xl border border-gray-100 bg-white p-4 shadow-sm transition-shadow hover:shadow-md">
|
||||
<div className="flex gap-3">
|
||||
{/* 头像 */}
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{initial}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">{m.content}</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-gray-400">
|
||||
<Tag color="blue" className="text-[10px] leading-tight">{m.product_sn}</Tag>
|
||||
<span className="truncate">{m.material_name}</span>
|
||||
{/* 内容 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-800">{m.operator_name}</span>
|
||||
<span className="shrink-0 text-xs text-gray-400">{t}</span>
|
||||
</div>
|
||||
<p className="mb-2 text-sm leading-relaxed text-gray-600">{m.content}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag color="blue" className="m-0 text-xs">{m.product_sn}</Tag>
|
||||
{m.material_name && (
|
||||
<span className="truncate text-xs text-gray-400">{m.material_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -372,43 +387,53 @@ export default function AdminDashboard() {
|
||||
|
||||
{/* ═══ 留言抽屉 ═══ */}
|
||||
<Drawer
|
||||
title={`💬 协同留言板(全厂 · ${msgTotal} 条)`}
|
||||
title={<span className="text-base font-bold">💬 协同留言板 <span className="font-normal text-gray-400">全厂 · {msgTotal} 条</span></span>}
|
||||
open={msgDrawerOpen}
|
||||
onClose={() => setMsgDrawerOpen(false)}
|
||||
width={520}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
width={560}
|
||||
styles={{ body: { padding: 16, background: "#f8fafc" } }}
|
||||
>
|
||||
<div className="px-4 pt-4">
|
||||
<Input
|
||||
prefix={<Search className="h-4 w-4 text-gray-400" />}
|
||||
placeholder="搜索 SN码 / 物料名称 / 留言人 / 内容"
|
||||
value={msgKeyword}
|
||||
onChange={e => setMsgKeyword(e.target.value)}
|
||||
onPressEnter={() => onMsgSearch(msgKeyword)}
|
||||
allowClear
|
||||
onClear={() => onMsgSearch("")}
|
||||
/>
|
||||
</div>
|
||||
<List
|
||||
className="mt-3 px-4"
|
||||
loading={msgLoading}
|
||||
dataSource={msgData}
|
||||
locale={{ emptyText: "暂无留言记录" }}
|
||||
renderItem={(item: ProductMessageItem) => <MsgRow m={item} />}
|
||||
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} 条`,
|
||||
}}
|
||||
<Input.Search
|
||||
placeholder="搜索 SN码 / 物料名称 / 留言人 / 内容"
|
||||
value={msgKeyword}
|
||||
onChange={e => setMsgKeyword(e.target.value)}
|
||||
onSearch={onMsgSearch}
|
||||
allowClear
|
||||
onClear={() => onMsgSearch("")}
|
||||
enterButton
|
||||
className="mb-4"
|
||||
/>
|
||||
{msgLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : msgData.length === 0 ? (
|
||||
<div className="py-16 text-center text-sm text-gray-400">
|
||||
{msgKeyword ? "未找到匹配的留言" : "暂无留言记录"}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{msgData.map(m => <MsgRow key={m.id} m={m} />)}
|
||||
{/* 简易分页 */}
|
||||
{msgTotal > 30 && (
|
||||
<div className="mt-4 flex items-center justify-center gap-2">
|
||||
{Array.from({ length: Math.ceil(msgTotal / 30) }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => {
|
||||
fetchDashboardMessages(msgKeyword, i * 30, 30)
|
||||
.then(res => { setMsgData(res.items); setMsgTotal(res.total); })
|
||||
.catch(() => {});
|
||||
}}
|
||||
className="rounded-md border border-gray-200 px-3 py-1 text-xs text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user