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