feat(操作统计): 人员筛选改为人名宫格多选 + 表头升降序

- 人员选择从下拉改为移动端风格的人名宫格网格,支持多选(可同时看多人),全部=清空
- 接收/转交/上传备注/总次数 表头点击升降序(三态:无→升→降→无)
- 移除不再使用的 antd Select
This commit is contained in:
2026-08-28 13:31:03 +08:00
parent 477a187fc1
commit 5722fe159a

View File

@ -1,10 +1,11 @@
import { useEffect, useState, useCallback } from "react";
import {
Package, ClipboardList, Bell, TrendingUp, AlertTriangle,
RefreshCw, Loader2, AlertCircle, ArrowRight, Clock, MessageCircle, CheckCircle2, Users,
RefreshCw, Loader2, AlertCircle, ArrowRight, ArrowUp, ArrowDown, ArrowUpDown,
Clock, MessageCircle, CheckCircle2, Users,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Radio, DatePicker, Drawer, Input, Select } from "antd";
import { Radio, DatePicker, Drawer, Input } from "antd";
import dayjs, { type Dayjs } from "dayjs";
import {
fetchDashboardStats, fetchWipTasks, fetchDashboardMessages, fetchCompletedTasks, fetchRejectedTasks, fetchUserOperations,
@ -273,7 +274,8 @@ export default function AdminDashboard() {
const [opsDrawerOpen, setOpsDrawerOpen] = useState(false);
const [opStats, setOpStats] = useState<UserOperation[]>([]);
const [opLoading, setOpLoading] = useState(false);
const [opFilterUser, setOpFilterUser] = useState<string>(""); // 人员选择,'' = 全部
const [opFilterUsers, setOpFilterUsers] = useState<Set<string>>(new Set()); // 多选人员,空 = 全部
const [opSort, setOpSort] = useState<{ key: string; order: "asc" | "desc" } | null>(null);
const navigate = useNavigate();
@ -341,10 +343,29 @@ export default function AdminDashboard() {
const openOpsDrawer = () => {
setOpsDrawerOpen(true);
setOpFilterUser("");
setOpFilterUsers(new Set());
setOpSort(null);
loadUserOperations();
};
// 人员多选切换
const toggleOpFilterUser = (id: string) => {
setOpFilterUsers(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
};
// 表头升降序:无 → 升序 → 降序 → 无
const toggleOpSort = (key: string) => {
setOpSort(prev => {
if (!prev || prev.key !== key) return { key, order: "asc" };
if (prev.order === "asc") return { key, order: "desc" };
return null;
});
};
const onMsgSearch = (value: string) => {
setMsgKeyword(value);
loadMessages(value);
@ -683,19 +704,26 @@ export default function AdminDashboard() {
size="large"
styles={{ body: { padding: 16, background: "#f8fafc" } }}
>
{/* 人员选择 */}
<div className="mb-4 flex items-center gap-2">
<Select
style={{ width: 200 }}
placeholder="选择人员(默认全部)"
value={opFilterUser || undefined}
onChange={setOpFilterUser}
allowClear
options={opStats.map(u => ({ value: u.user_id, label: u.user_name }))}
showSearch
optionFilterProp="label"
/>
<span className="text-xs text-gray-400"> / / · </span>
{/* 人员选择 — 人名宫格多选 */}
<div className="mb-4">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs text-gray-400"> = </span>
<button onClick={() => setOpFilterUsers(new Set())}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${opFilterUsers.size === 0 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}>
</button>
</div>
<div className="grid gap-1.5" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(72px, 1fr))" }}>
{opStats.map(u => {
const active = opFilterUsers.size > 0 && opFilterUsers.has(u.user_id);
return (
<button key={u.user_id} onClick={() => toggleOpFilterUser(u.user_id)}
className={`rounded-lg px-2 py-1.5 text-xs font-medium transition-colors ${active ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}>
{u.user_name}
</button>
);
})}
</div>
</div>
{opLoading ? (
@ -705,32 +733,51 @@ export default function AdminDashboard() {
) : opStats.length === 0 ? (
<div className="py-16 text-center text-sm text-gray-400"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-100 text-xs text-gray-400">
<th className="py-2 pr-3 font-medium">#</th>
<th className="py-2 pr-3 font-medium"></th>
<th className="py-2 pr-3 text-right font-medium"></th>
<th className="py-2 pr-3 text-right font-medium"></th>
<th className="py-2 pr-3 text-right font-medium"></th>
<th className="py-2 text-right font-medium"></th>
</tr>
</thead>
<tbody>
{(opFilterUser ? opStats.filter(u => u.user_id === opFilterUser) : opStats).map((u, idx) => (
<tr key={u.user_id} className="border-b border-gray-50 last:border-0 hover:bg-gray-50/50">
<td className="py-2 pr-3 text-xs text-gray-400">{idx + 1}</td>
<td className="py-2 pr-3 font-medium text-gray-800">{u.user_name}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.receive_count}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.transfer_count}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.record_count}</td>
<td className="py-2 text-right font-bold text-blue-600">{u.total}</td>
</tr>
))}
</tbody>
</table>
</div>
(() => {
// 过滤 + 排序后的数据
let list = opFilterUsers.size > 0 ? opStats.filter(u => opFilterUsers.has(u.user_id)) : opStats;
if (opSort) {
const key = opSort.key;
const getVal = (u: UserOperation) => key === "receive" ? u.receive_count : key === "transfer" ? u.transfer_count : key === "record" ? u.record_count : u.total;
list = [...list].sort((a, b) => opSort!.order === "asc" ? getVal(a) - getVal(b) : getVal(b) - getVal(a));
}
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-100 text-xs text-gray-400">
<th className="py-2 pr-3 font-medium">#</th>
<th className="py-2 pr-3 font-medium"></th>
{(["receive", "transfer", "record", "total"] as const).map(k => {
const label = k === "receive" ? "接收" : k === "transfer" ? "转交" : k === "record" ? "上传备注" : "总次数";
const sorted = opSort?.key === k;
return (
<th key={k} className="cursor-pointer py-2 pr-3 text-right font-medium hover:text-blue-600" onClick={() => toggleOpSort(k)}>
<span className="inline-flex items-center gap-0.5">
{label}
{sorted ? (opSort!.order === "asc" ? <ArrowUp className="h-3 w-3 text-blue-600" /> : <ArrowDown className="h-3 w-3 text-blue-600" />) : <ArrowUpDown className="h-3 w-3 text-gray-300" />}
</span>
</th>
);
})}
</tr>
</thead>
<tbody>
{list.map((u, idx) => (
<tr key={u.user_id} className="border-b border-gray-50 last:border-0 hover:bg-gray-50/50">
<td className="py-2 pr-3 text-xs text-gray-400">{idx + 1}</td>
<td className="py-2 pr-3 font-medium text-gray-800">{u.user_name}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.receive_count}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.transfer_count}</td>
<td className="py-2 pr-3 text-right text-gray-600">{u.record_count}</td>
<td className="py-2 text-right font-bold text-blue-600">{u.total}</td>
</tr>
))}
</tbody>
</table>
</div>
);
})()
)}
</Drawer>
</div>