-
-
-
-
在岗人员
+ {/* ═══ 实时模式 ═══ */}
+ {mode === "realtime" && (
+ <>
+
+
+
+
+
当前负责人
+
+
{workloads.length} 人
+
当前有在制品的负责人
+
+
+
+
{totalDevices} 台
+
按唯一产品去重统计;并发设备会在多个负责人名下各显示一次
+
-
{workloads.length} 人
-
持有在制品的负责人数
-
-
-
-
{totalDevices} 台
-
一台设备若被多人并发处理,会在多人名下各计一次
-
-
- {/* 加载 / 错误 / 空态 */}
- {loading && (
-
-
-
- )}
- {!loading && error && (
-
- )}
- {!loading && !error && workloads.length === 0 && (
-
🎉 当前无人在制品
- )}
+ {loading && (
+
+
+
+ )}
+ {!loading && error && (
+
+ )}
+ {!loading && !error && workloads.length === 0 && (
+
🎉 当前无人在制品
+ )}
- {/* 人员列表 */}
- {!loading && !error && workloads.length > 0 && (
-
- {workloads.map((w) => {
- const isOpen = expanded.has(w.assignee_id);
- const initial = (w.assignee_name || w.assignee_id || "?").charAt(0);
- const color = AVATAR_COLORS[(initial.charCodeAt(0) || 0) % AVATAR_COLORS.length];
- return (
-
- {/* 人员头部 */}
-
+ {!loading && !error && workloads.length > 0 && (
+
+ {workloads.map((w) => {
+ const isOpen = expanded.has(w.assignee_id);
+ const initial = (w.assignee_name || w.assignee_id || "?").charAt(0);
+ const color = AVATAR_COLORS[(initial.charCodeAt(0) || 0) % AVATAR_COLORS.length];
+ return (
+
+
- {/* 展开明细 */}
- {isOpen && (
-
- {w.devices.length === 0 ? (
-
无设备
- ) : (
-
- {w.devices.map((d) => (
-
navigate(`/admin/tasks?sn=${d.serial_number}`)}
- className="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-100 bg-white px-4 py-2.5 transition-shadow hover:border-blue-200 hover:shadow-md"
- >
-
- {d.task_status === "WIP" ? "进行中" : "待接收"}
-
-
{d.material_name || "未知设备"}
-
{d.spec_model || "无规格"}
-
序列号: {d.external_serial || "未录入"}
-
身份证: {d.serial_number}
+ {isOpen && (
+
+ {w.devices.length === 0 ? (
+
无设备
+ ) : (
+
+ {w.devices.map((d) => (
+
navigate(`/admin/tasks?sn=${d.serial_number}`)}
+ className="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-100 bg-white px-4 py-2.5 transition-shadow hover:border-blue-200 hover:shadow-md"
+ >
+
+ {d.task_status === "WIP" ? "进行中" : "待接收"}
+
+ {d.material_name || "未知设备"}
+ {d.spec_model || "无规格"}
+ 序列号: {d.external_serial || "未录入"}
+ 身份证: {d.serial_number}
+
+
+
+ {durationLabel(d.duration_hours)}
+
+ {d.received_at && {d.received_at}}
+
+
+ ))}
- ))}
+ )}
)}
- )}
-
- );
- })}
-
+ );
+ })}
+
+ )}
+ >
)}
+
+ {/* ═══ 历史模式(工时台账) ═══ */}
+ {mode === "history" && (
+ <>
+ {/* 概览 */}
+
+
+
+
{totalHistoryDevices} 台
+
按唯一身份证去重统计
+
+
+
+
+
工序记录
+
+
{records.length} 条
+
含进行中与已完成
+
+
+
+ {/* 搜索栏 */}
+
+
+
setHistoryRange(dates as [Dayjs, Dayjs] | null)}
+ disabledDate={(d) => d.isAfter(dayjs(), "day")}
+ style={{ width: 240 }}
+ placeholder={["开始", "结束"]}
+ />
+
+
+
+
+
+ } onClick={handleExport}>
+ 导出
+
+
+
+
+
+
+ {/* 表格 */}
+ {historyError && (
+
+ )}
+
+
+ rowKey="task_id"
+ columns={columns}
+ dataSource={records}
+ loading={historyLoading}
+ size="small"
+ pagination={{ pageSize, showTotal: (t) => `共 ${t} 条` }}
+ />
+
+ >
+ )}
+
+ {/* 记录详情弹窗 */}
+ setRecordDetail((s) => ({ ...s, open: false }))}
+ footer={null}
+ width={560}
+ >
+ {recordDetail.records.length === 0 ? (
+ 暂无记录
+ ) : (
+ ({
+ color: "gray",
+ children: (
+
+
{r.created_at ? dayjs(r.created_at).format("MM-DD HH:mm") : ""}
+
{r.remark || "—"}
+ {r.images && r.images.length > 0 && (
+
+ {r.images.map((img, j) => (
+
+ ))}
+
+ )}
+
+ ),
+ }))}
+ />
+ )}
+
);
}
diff --git a/frontend/src/services/dashboardApi.ts b/frontend/src/services/dashboardApi.ts
index 2f56cdb..ae197a0 100644
--- a/frontend/src/services/dashboardApi.ts
+++ b/frontend/src/services/dashboardApi.ts
@@ -46,6 +46,8 @@ export interface PersonDevice {
material_name: string;
spec_model: string;
task_status: string;
+ duration_hours: number;
+ received_at: string | null;
}
export interface PersonWorkload {
@@ -55,6 +57,32 @@ export interface PersonWorkload {
devices: PersonDevice[];
}
+export interface PersonHistoryRecord {
+ task_id: string;
+ task_name: string;
+ assignee_id: string;
+ assignee_name: string;
+ product_sn: string;
+ external_serial: string | null;
+ material_name: string;
+ spec_model: string;
+ status: string;
+ received_at: string | null;
+ completed_at: string | null;
+ duration_hours: number;
+ latest_valid_remark: string | null;
+ record_count: number;
+}
+
+export interface PeopleHistoryQuery {
+ since?: string;
+ until?: string;
+ assignee_id?: string;
+ spec_model?: string;
+ product_sn?: string;
+ task_name?: string;
+}
+
export interface ProductMessageItem {
id: string;
content: string;
@@ -96,6 +124,34 @@ export async function fetchPeopleWorkload(): Promise {
return data;
}
+export async function fetchPeopleHistory(query: PeopleHistoryQuery = {}): Promise {
+ const params: Record = {};
+ for (const [k, v] of Object.entries(query)) {
+ if (v) params[k] = v;
+ }
+ const { data } = await api.get("/dashboard/people-history", { params });
+ return data;
+}
+
+export async function downloadPeopleHistory(query: PeopleHistoryQuery = {}): Promise {
+ const params: Record = {};
+ for (const [k, v] of Object.entries(query)) {
+ if (v) params[k] = v;
+ }
+ const res = await api.get("/dashboard/people-history/export", {
+ params,
+ responseType: "blob",
+ });
+ const url = window.URL.createObjectURL(res.data as Blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "people_history.csv";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+}
+
export async function fetchDashboardMessages(
keyword = "", skip = 0, limit = 30,
): Promise {