feat(操作统计): 点击接收/转交/上传备注数字下钻查看明细
- 表格中接收/转交/上传备注数字改为可点击(蓝色下划线),点击打开明细抽屉 - 明细展示:任务名/备注/设备/身份证/时间,跟随顶部时间筛选
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user