本次同步 09-12 至 09-28 的代码变更,主要内容: 新增功能 - 修改密码:后端新增 ChangePasswordRequest DTO 与UserController/UserService 对应接口,前端新增 ChangePasswordDialog 并在 Header 接入入口 - 文件预览组件重构:file-preview 目录拆分出 VideoPreview(支持 blobUrl 直传)、 ImagePreview、AudioPreview,index.tsx 统一调度各类型预览器 - 新增 my_ops 算子模块:markitdown_parser、mineru_pdf_parser、text_quality_evaluator - 新增 tools/minio-file-server MinIO 文件服务 功能增强 - 数据清洗:cleaning_task_service/scheduler/routes 重构, data_juicer_executor 与 wrapper 增强,OperatorTable/LogsTable/TaskDetail 同步调整 - 数据标注:annotation sync.py、project.py 接口与schema 更新 - 数据采集、算子市场、首页与菜单路由相关页面调整 - DataJuicer 算子接入(useOperatorOperations、dj_ops_schema) 结构调整 - deepflow/ 更名为 deep-flow/(419 个文件重命名) - 新增 helm 部署编排、GitHub Actions 之外的自建镜像构建脚本 未纳入本次提交 - scripts-backup/、runtime/python-executor-backup/、components_backup/(备份目录) - *.backup*、__pycache__ 产物 - zh_core_web_sm-3.8.0-py3-none-any.whl(46MB 构建产物) - DataX/(内嵌独立仓库,远程亦未收录其源码)
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
import { Card, Badge, Button, Modal, Table, Tag } from "antd";
|
||
import type { ColumnsType } from "antd/es/table";
|
||
import { SearchControls } from "@/components/SearchControls";
|
||
import { queryExecutionLogUsingPost } from "../collection.apis";
|
||
import useFetchData from "@/hooks/useFetchData";
|
||
import { useEffect, useState } from "react";
|
||
import { TaskExecution } from "@/pages/DataCollection/collection.model.ts";
|
||
import { mapTaskExecution } from "@/pages/DataCollection/collection.const.ts";
|
||
import { queryExecutionLogFileByIdUsingGet } from "../collection.apis";
|
||
import { FileTextOutlined } from "@ant-design/icons";
|
||
import { useTranslation } from "react-i18next";
|
||
import { appendTokenToUrl } from "@/utils/request";
|
||
|
||
export default function Execution({ taskId }: { taskId?: string }) {
|
||
const { t } = useTranslation();
|
||
const [dateRange, setDateRange] = useState<[any, any] | null>(null);
|
||
const [logOpen, setLogOpen] = useState(false);
|
||
const [logLoading, setLogLoading] = useState(false);
|
||
const [logTitle, setLogTitle] = useState<string>("");
|
||
const [logContent, setLogContent] = useState<string>("");
|
||
const [logFilename, setLogFilename] = useState<string>("");
|
||
const [logBlobUrl, setLogBlobUrl] = useState<string>("");
|
||
|
||
const filterOptions = [
|
||
{
|
||
key: "status",
|
||
label: t("dataCollection.execution.filters.statusFilter"),
|
||
options: [
|
||
{ value: "all", label: t("dataCollection.execution.filters.allStatus") },
|
||
{ value: "RUNNING", label: t("dataCollection.execution.filters.running") },
|
||
{ value: "COMPLETED", label: t("dataCollection.execution.filters.success") },
|
||
{ value: "FAILED", label: t("dataCollection.execution.filters.failed") },
|
||
],
|
||
},
|
||
];
|
||
|
||
const formatDuration = (seconds?: number) => {
|
||
if (seconds === undefined || seconds === null) return t("common.placeholders.empty");
|
||
const total = Math.max(0, Math.floor(seconds));
|
||
if (total < 60) return `${total}${t("dataCollection.execution.duration.secondsSuffix")}`;
|
||
const min = Math.floor(total / 60);
|
||
const sec = total % 60;
|
||
return `${min}${t("dataCollection.execution.duration.minutesSuffix")}${sec}${t("dataCollection.execution.duration.secondsSuffix")}`;
|
||
};
|
||
|
||
const handleReset = () => {
|
||
setDateRange(null);
|
||
setSearchParams({
|
||
keyword: "",
|
||
filter: {
|
||
type: [],
|
||
status: [],
|
||
tags: [],
|
||
},
|
||
current: 1,
|
||
pageSize: 10,
|
||
start_time: undefined,
|
||
end_time: undefined,
|
||
});
|
||
};
|
||
|
||
const {
|
||
loading,
|
||
tableData,
|
||
pagination,
|
||
searchParams,
|
||
setSearchParams,
|
||
handleFiltersChange,
|
||
handleKeywordChange,
|
||
} = useFetchData<TaskExecution>(
|
||
(params) => {
|
||
const { keyword, start_time, end_time, status, ...rest } = params || {};
|
||
return queryExecutionLogUsingPost({
|
||
...rest,
|
||
task_id: taskId || undefined,
|
||
task_name: keyword || undefined,
|
||
status: status || undefined,
|
||
start_time,
|
||
end_time,
|
||
});
|
||
},
|
||
(execution) => mapTaskExecution(execution, t),
|
||
30000,
|
||
false,
|
||
[],
|
||
0
|
||
);
|
||
|
||
useEffect(() => {
|
||
setSearchParams((prev) => ({
|
||
...prev,
|
||
current: 1,
|
||
}));
|
||
}, [taskId, setSearchParams, t]);
|
||
|
||
const handleViewLog = async (record: TaskExecution) => {
|
||
setLogOpen(true);
|
||
setLogLoading(true);
|
||
setLogTitle(`${record.taskName} / ${record.id}`);
|
||
setLogContent("");
|
||
setLogFilename("");
|
||
if (logBlobUrl) {
|
||
URL.revokeObjectURL(logBlobUrl);
|
||
setLogBlobUrl("");
|
||
}
|
||
try {
|
||
const { blob, filename } = await queryExecutionLogFileByIdUsingGet(record.id);
|
||
setLogFilename(filename);
|
||
const url = URL.createObjectURL(blob);
|
||
setLogBlobUrl(url);
|
||
const text = await blob.text();
|
||
setLogContent(text);
|
||
} catch (e: any) {
|
||
setLogContent(e?.data?.detail || e?.message || t("dataCollection.execution.messages.loadLogFailed"));
|
||
} finally {
|
||
setLogLoading(false);
|
||
}
|
||
};
|
||
|
||
const columns: ColumnsType<any> = [
|
||
{
|
||
title: t("dataCollection.execution.columns.taskName"),
|
||
dataIndex: "taskName",
|
||
key: "taskName",
|
||
fixed: "left",
|
||
render: (text: string) => (
|
||
<span style={{ fontWeight: 500 }}>{text}</span>
|
||
),
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.status"),
|
||
dataIndex: "status",
|
||
key: "status",
|
||
render: (status: any) => ((
|
||
<Tag color={status?.color || "default"}>{status?.label || status}</Tag>
|
||
)
|
||
),
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.startTime"),
|
||
dataIndex: "startedAt",
|
||
key: "startedAt",
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.endTime"),
|
||
dataIndex: "completedAt",
|
||
key: "completedAt",
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.duration"),
|
||
dataIndex: "durationSeconds",
|
||
key: "durationSeconds",
|
||
render: (v?: number) => formatDuration(v),
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.errorMessage"),
|
||
dataIndex: "errorMessage",
|
||
key: "errorMessage",
|
||
render: (msg?: string) =>
|
||
msg ? (
|
||
<span style={{ color: "#f5222d" }} title={msg}>
|
||
{msg}
|
||
</span>
|
||
) : (
|
||
<span style={{ color: "#bbb" }}>{t("common.placeholders.empty")}</span>
|
||
),
|
||
},
|
||
{
|
||
title: t("dataCollection.execution.columns.actions"),
|
||
key: "action",
|
||
fixed: "right",
|
||
width: 120,
|
||
render: (_: any, record: TaskExecution) => (
|
||
<Button
|
||
type="link"
|
||
icon={<FileTextOutlined />}
|
||
onClick={() => handleViewLog(record)}
|
||
>
|
||
{t("dataCollection.execution.actions.viewLog")}
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
{/* Filter Controls */}
|
||
<div className="flex items-center justify-between gap-4">
|
||
<SearchControls
|
||
searchTerm={searchParams.keyword}
|
||
onSearchChange={handleKeywordChange}
|
||
filters={filterOptions}
|
||
selectedFilters={searchParams.filter}
|
||
onFiltersChange={handleFiltersChange}
|
||
showViewToggle={false}
|
||
onClearFilters={() => {
|
||
setDateRange(null);
|
||
setSearchParams((prev) => ({
|
||
...prev,
|
||
filter: { ...prev.filter, status: [] },
|
||
current: 1,
|
||
keyword: "",
|
||
start_time: undefined,
|
||
end_time: undefined,
|
||
}));
|
||
}}
|
||
showDatePicker
|
||
dateRange={dateRange as any}
|
||
onDateChange={(date) => {
|
||
// 自动修正日期顺序:确保开始时间 <= 结束时间
|
||
if (date && date[0] && date[1] && date[0].isAfter(date[1])) {
|
||
// 如果开始时间晚于结束时间,交换它们
|
||
date = [date[1], date[0]];
|
||
}
|
||
setDateRange(date as any);
|
||
// 转换为不带时区的 ISO 格式字符串(YYYY-MM-DDTHH:mm:ss)
|
||
const toLocalISOString = (d: any) => {
|
||
if (!d) return undefined;
|
||
const pad = (n: number) => String(n).padStart(2, '0');
|
||
return `${d.year()}-${pad(d.month() + 1)}-${pad(d.date())}T${pad(d.hour())}:${pad(d.minute())}:${pad(d.second())}`;
|
||
};
|
||
setSearchParams((prev) => ({
|
||
...prev,
|
||
current: 1,
|
||
start_time: toLocalISOString(date?.[0]),
|
||
end_time: toLocalISOString(date?.[1]),
|
||
}));
|
||
}}
|
||
onReload={handleReset}
|
||
searchPlaceholder={t("dataCollection.execution.filters.searchPlaceholder")}
|
||
className="flex-1"
|
||
/>
|
||
</div>
|
||
<Card>
|
||
<Table
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={tableData}
|
||
rowKey="id"
|
||
pagination={pagination}
|
||
scroll={{ x: "max-content" }}
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
title={logTitle || t("dataCollection.execution.modal.title")}
|
||
open={logOpen}
|
||
onCancel={() => {
|
||
setLogOpen(false);
|
||
if (logBlobUrl) {
|
||
URL.revokeObjectURL(logBlobUrl);
|
||
setLogBlobUrl("");
|
||
}
|
||
}}
|
||
footer={
|
||
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
|
||
<div style={{ color: "#6b7280", fontSize: 12 }}>{logFilename || ""}</div>
|
||
<div style={{ display: "flex", gap: 8 }}>
|
||
{logBlobUrl ? (
|
||
<Button
|
||
onClick={() => {
|
||
const a = document.createElement("a");
|
||
a.href = logBlobUrl;
|
||
a.download = logFilename || "execution.log";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
}}
|
||
>
|
||
{t("dataCollection.execution.modal.downloadLog")}
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
type="primary"
|
||
onClick={() => {
|
||
setLogOpen(false);
|
||
if (logBlobUrl) {
|
||
URL.revokeObjectURL(logBlobUrl);
|
||
setLogBlobUrl("");
|
||
}
|
||
}}
|
||
>
|
||
{t("dataCollection.execution.modal.close")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
}
|
||
width={900}
|
||
>
|
||
<div
|
||
style={{
|
||
background: "#0b1020",
|
||
color: "#e5e7eb",
|
||
borderRadius: 8,
|
||
padding: 12,
|
||
maxHeight: "60vh",
|
||
overflow: "auto",
|
||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||
fontSize: 12,
|
||
lineHeight: 1.5,
|
||
whiteSpace: "pre-wrap",
|
||
wordBreak: "break-word",
|
||
}}
|
||
>
|
||
{logLoading
|
||
? t("dataCollection.execution.modal.loading")
|
||
: (logContent || t("dataCollection.execution.modal.empty"))}
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|