feat(操作统计): 人员筛选改为人名宫格多选 + 表头升降序
- 人员选择从下拉改为移动端风格的人名宫格网格,支持多选(可同时看多人),全部=清空 - 接收/转交/上传备注/总次数 表头点击升降序(三态:无→升→降→无) - 移除不再使用的 antd Select
This commit is contained in:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user