feat(操作统计): 点击接收/转交/上传备注数字下钻查看明细

- 表格中接收/转交/上传备注数字改为可点击(蓝色下划线),点击打开明细抽屉
- 明细展示:任务名/备注/设备/身份证/时间,跟随顶部时间筛选
This commit is contained in:
2026-08-28 13:36:28 +08:00
parent 7205de369a
commit d0fc8cdecf
2 changed files with 79 additions and 5 deletions

View File

@ -8,8 +8,8 @@ import { useNavigate } from "react-router-dom";
import { Radio, DatePicker, Drawer, Input } 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, fetchOperationDetail,
type DashboardStats, type WipTask, type ProductMessageItem, type CompletedTask, type RejectedTask, type UserOperation, type DashboardStats, type WipTask, type ProductMessageItem, type CompletedTask, type RejectedTask, type UserOperation, type OperationDetail,
} from "../../services/dashboardApi"; } from "../../services/dashboardApi";
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
@ -276,6 +276,11 @@ export default function AdminDashboard() {
const [opLoading, setOpLoading] = useState(false); const [opLoading, setOpLoading] = useState(false);
const [opFilterUsers, setOpFilterUsers] = useState<Set<string>>(new Set()); // 多选人员,空 = 全部 const [opFilterUsers, setOpFilterUsers] = useState<Set<string>>(new Set()); // 多选人员,空 = 全部
const [opSort, setOpSort] = useState<{ key: string; order: "asc" | "desc" } | null>(null); const [opSort, setOpSort] = useState<{ key: string; order: "asc" | "desc" } | null>(null);
// 操作明细下钻
const [opDetailOpen, setOpDetailOpen] = useState(false);
const [opDetailList, setOpDetailList] = useState<OperationDetail[]>([]);
const [opDetailLoading, setOpDetailLoading] = useState(false);
const [opDetailTitle, setOpDetailTitle] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
@ -369,6 +374,18 @@ export default function AdminDashboard() {
}); });
}; };
// 点击数字下钻查看明细
const openOpDetail = (user: UserOperation, actionType: string, label: string) => {
setOpDetailOpen(true);
setOpDetailTitle(`${user.user_name} · ${label}`);
setOpDetailLoading(true);
const { since, until } = rangeToParams(dateKey, customRange);
fetchOperationDetail(user.user_id, actionType, since, until)
.then(setOpDetailList)
.catch(() => setOpDetailList([]))
.finally(() => setOpDetailLoading(false));
};
const onMsgSearch = (value: string) => { const onMsgSearch = (value: string) => {
setMsgKeyword(value); setMsgKeyword(value);
loadMessages(value); loadMessages(value);
@ -776,9 +793,15 @@ export default function AdminDashboard() {
<tr key={u.user_id} className="border-b border-gray-50 last:border-0 hover:bg-gray-50/50"> <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 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 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">
<td className="py-2 pr-3 text-right text-gray-600">{u.transfer_count}</td> <button onClick={() => openOpDetail(u, "receive", "接收")} className="cursor-pointer text-blue-600 hover:text-blue-800 hover:underline">{u.receive_count}</button>
<td className="py-2 pr-3 text-right text-gray-600">{u.record_count}</td> </td>
<td className="py-2 pr-3 text-right">
<button onClick={() => openOpDetail(u, "transfer", "转交")} className="cursor-pointer text-blue-600 hover:text-blue-800 hover:underline">{u.transfer_count}</button>
</td>
<td className="py-2 pr-3 text-right">
<button onClick={() => openOpDetail(u, "record", "上传备注")} className="cursor-pointer text-blue-600 hover:text-blue-800 hover:underline">{u.record_count}</button>
</td>
<td className="py-2 text-right font-bold text-blue-600">{u.total}</td> <td className="py-2 text-right font-bold text-blue-600">{u.total}</td>
</tr> </tr>
))} ))}
@ -789,6 +812,39 @@ export default function AdminDashboard() {
})() })()
)} )}
</Drawer> </Drawer>
{/* ═══ 操作明细抽屉 ═══ */}
<Drawer
title={<span className="text-base font-bold">📋 {opDetailTitle} <span className="font-normal text-gray-400">{opDetailList.length} 条</span></span>}
open={opDetailOpen}
onClose={() => setOpDetailOpen(false)}
size="large"
styles={{ body: { padding: 16, background: "#f8fafc" } }}
>
{opDetailLoading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
</div>
) : opDetailList.length === 0 ? (
<div className="py-16 text-center text-sm text-gray-400">该时段暂无相关记录</div>
) : (
<div>
{opDetailList.map((d, i) => (
<div key={i} className="mb-2 rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-800">{d.task_name}</span>
<span className="shrink-0 text-xs text-gray-400">{d.time ? dayjs(d.time).format("YYYY-MM-DD HH:mm") : ""}</span>
</div>
{d.remark && <p className="mt-1 text-xs leading-relaxed text-gray-600">{d.remark}</p>}
<div className="mt-1.5 text-[11px] text-gray-400">
<span>{d.material_name || "未知设备"}</span>
{d.product_sn && <span className="ml-2 font-mono">身份证: {d.product_sn}</span>}
</div>
</div>
))}
</div>
)}
</Drawer>
</div> </div>
); );
} }

View File

@ -63,6 +63,14 @@ export interface UserOperation {
total: number; total: number;
} }
export interface OperationDetail {
task_name: string;
product_sn: string;
material_name: string;
remark: string | null;
time: string;
}
export interface PersonDevice { export interface PersonDevice {
product_id: string; product_id: string;
serial_number: string; serial_number: string;
@ -159,6 +167,16 @@ export async function fetchUserOperations(since?: string, until?: string): Promi
return data; return data;
} }
export async function fetchOperationDetail(
userId: string, actionType: string, since?: string, until?: string,
): Promise<OperationDetail[]> {
const params: Record<string, string> = { user_id: userId, action_type: actionType };
if (since) params.since = since;
if (until) params.until = until;
const { data } = await api.get<OperationDetail[]>("/dashboard/user-operations/detail", { params });
return data;
}
export async function fetchPeopleWorkload(): Promise<PersonWorkload[]> { export async function fetchPeopleWorkload(): Promise<PersonWorkload[]> {
const { data } = await api.get<PersonWorkload[]>("/dashboard/people-workload"); const { data } = await api.get<PersonWorkload[]>("/dashboard/people-workload");
return data; return data;