feat(操作统计): 点击接收/转交/上传备注数字下钻查看明细
- 表格中接收/转交/上传备注数字改为可点击(蓝色下划线),点击打开明细抽屉 - 明细展示:任务名/备注/设备/身份证/时间,跟随顶部时间筛选
This commit is contained in:
@ -8,8 +8,8 @@ import { useNavigate } from "react-router-dom";
|
||||
import { Radio, DatePicker, Drawer, Input } from "antd";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import {
|
||||
fetchDashboardStats, fetchWipTasks, fetchDashboardMessages, fetchCompletedTasks, fetchRejectedTasks, fetchUserOperations,
|
||||
type DashboardStats, type WipTask, type ProductMessageItem, type CompletedTask, type RejectedTask, type UserOperation,
|
||||
fetchDashboardStats, fetchWipTasks, fetchDashboardMessages, fetchCompletedTasks, fetchRejectedTasks, fetchUserOperations, fetchOperationDetail,
|
||||
type DashboardStats, type WipTask, type ProductMessageItem, type CompletedTask, type RejectedTask, type UserOperation, type OperationDetail,
|
||||
} from "../../services/dashboardApi";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
@ -276,6 +276,11 @@ export default function AdminDashboard() {
|
||||
const [opLoading, setOpLoading] = useState(false);
|
||||
const [opFilterUsers, setOpFilterUsers] = useState<Set<string>>(new Set()); // 多选人员,空 = 全部
|
||||
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();
|
||||
|
||||
@ -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) => {
|
||||
setMsgKeyword(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">
|
||||
<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 pr-3 text-right">
|
||||
<button onClick={() => openOpDetail(u, "receive", "接收")} className="cursor-pointer text-blue-600 hover:text-blue-800 hover:underline">{u.receive_count}</button>
|
||||
</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>
|
||||
</tr>
|
||||
))}
|
||||
@ -789,6 +812,39 @@ export default function AdminDashboard() {
|
||||
})()
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,6 +63,14 @@ export interface UserOperation {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface OperationDetail {
|
||||
task_name: string;
|
||||
product_sn: string;
|
||||
material_name: string;
|
||||
remark: string | null;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface PersonDevice {
|
||||
product_id: string;
|
||||
serial_number: string;
|
||||
@ -159,6 +167,16 @@ export async function fetchUserOperations(since?: string, until?: string): Promi
|
||||
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[]> {
|
||||
const { data } = await api.get<PersonWorkload[]>("/dashboard/people-workload");
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user