[feat]:[FL-200][ATP模型管理功能优化]

移除ATP模型管理中版本和执行记录的展示功能:
- 列表页:移除"当前版本"列及版本/运行次数统计
- 详情页:移除版本列表卡片、新建版本、编辑版本、激活版本功能
- 详情页:移除运行记录卡片、运行/Dry Run功能
- 保留模型基本信息展示(编码、状态、业务维度、说明)
- 保留当前版本详情、目录文件清单展示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-27 22:49:00 +08:00
parent 91c9877e0a
commit 27ea9d7c8d
2 changed files with 9 additions and 487 deletions
+9 -465
View File
@@ -2,27 +2,19 @@
import Link from "next/link";
import { useParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import {
Alert,
App,
Button,
Descriptions,
Empty,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Table,
Tag,
Typography,
Upload,
} from "antd";
import type { ColumnsType } from "antd/es/table";
import type { UploadFile } from "antd/es/upload/interface";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useMemo } from "react";
import { AdminPageLoading } from "@/components/admin-page-loading";
import { useAuth } from "@/components/auth-provider";
@@ -32,7 +24,6 @@ import { readApiError } from "@/lib/api";
import {
getAtpAssetStatusDisplay,
getAtpReleaseStatusDisplay,
getAtpRunStatusDisplay,
getAtpRunnerKindLabel,
} from "@/lib/atp-asset-display";
import type {
@@ -40,99 +31,19 @@ import type {
AtpAssetFileListResponse,
AtpAssetReleaseDetail,
AtpAssetReleaseListResponse,
AtpAssetReleaseSummary,
AtpAssetRunDetail,
AtpAssetRunListResponse,
AtpAssetRunSummary,
AtpAssetSummary,
} from "@/types/auth";
type ReleaseFormValues = {
release_tag: string;
status: "draft" | "released" | "archived";
};
type RunFormValues = {
dry_run: boolean;
timeout_seconds: number | null;
extra_args_text: string;
};
const EMPTY_RELEASE_FORM: ReleaseFormValues = {
release_tag: "",
status: "released",
};
const EMPTY_RUN_FORM: RunFormValues = {
dry_run: true,
timeout_seconds: null,
extra_args_text: "",
};
function formatDateTime(value: string | null | undefined): string {
if (!value) {
return "-";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString("zh-CN", { hour12: false });
}
function toReleaseFormValues(item: AtpAssetReleaseSummary): ReleaseFormValues {
return {
release_tag: item.release_tag ?? "",
status: item.status,
};
}
function buildReleasePatch(values: ReleaseFormValues) {
return {
release_tag: values.release_tag.trim() || null,
status: values.status,
};
}
function buildRunPayload(values: RunFormValues) {
return {
dry_run: values.dry_run,
timeout_seconds: values.timeout_seconds || null,
extra_args: values.extra_args_text
.split(/\s+/)
.map((item) => item.trim())
.filter(Boolean),
};
}
export default function AtpAssetDetailPage() {
const { message } = App.useApp();
const { user, initializing, fetchWithAuth, hasPermission } = useAuth();
const queryClient = useQueryClient();
const params = useParams<{ id: string }>();
const assetId = typeof params?.id === "string" ? params.id : "";
const [releaseForm] = Form.useForm<ReleaseFormValues>();
const [runForm] = Form.useForm<RunFormValues>();
const [releaseModalOpen, setReleaseModalOpen] = useState(false);
const [runModalOpen, setRunModalOpen] = useState(false);
const [editingRelease, setEditingRelease] = useState<AtpAssetReleaseSummary | null>(null);
const [selectedReleaseIdState, setSelectedReleaseIdState] = useState("");
const [releaseArchiveFileList, setReleaseArchiveFileList] = useState<UploadFile[]>([]);
const canRead = hasPermission("atp.read") || hasPermission("atp.run") || hasPermission("atp.manage");
const canRun = hasPermission("atp.run") || hasPermission("atp.manage");
const canManage = hasPermission("atp.manage");
const refreshAtpData = useCallback(() => {
void queryClient.invalidateQueries({ queryKey: ["atp-asset-detail", assetId] });
void queryClient.invalidateQueries({ queryKey: ["atp-asset-releases", assetId] });
if (selectedReleaseIdState) {
void queryClient.invalidateQueries({ queryKey: ["atp-release-detail", selectedReleaseIdState] });
void queryClient.invalidateQueries({ queryKey: ["atp-release-files", selectedReleaseIdState] });
void queryClient.invalidateQueries({ queryKey: ["atp-release-runs", selectedReleaseIdState] });
}
}, [queryClient, assetId, selectedReleaseIdState]);
// This function is called by WebSocket updates - kept for compatibility
}, []);
useTopicSubscription(
"admin.atp-assets",
@@ -166,10 +77,7 @@ export default function AtpAssetDetailPage() {
});
const releases = releasesQuery.data?.items ?? [];
const selectedReleaseId =
selectedReleaseIdState && releases.some((item) => item.id === selectedReleaseIdState)
? selectedReleaseIdState
: (releases.find((item) => item.is_active)?.id ?? releases[0]?.id ?? "");
const selectedReleaseId = releases.find((item) => item.is_active)?.id ?? releases[0]?.id ?? "";
const selectedRelease = releases.find((item) => item.id === selectedReleaseId) ?? null;
const releaseDetailQuery = useQuery({
@@ -196,183 +104,6 @@ export default function AtpAssetDetailPage() {
},
});
const runsQuery = useQuery({
queryKey: ["atp-release-runs", selectedReleaseId],
enabled: Boolean(user && canRead && selectedReleaseId),
queryFn: async () => {
const response = await fetchWithAuth(`/api/v1/atp/releases/${selectedReleaseId}/runs`);
if (!response.ok) {
throw new Error(await readApiError(response));
}
return (await response.json()) as AtpAssetRunListResponse;
},
});
const saveReleaseMutation = useMutation({
mutationFn: async (values: ReleaseFormValues) => {
if (editingRelease) {
const response = await fetchWithAuth(`/api/v1/atp/releases/${editingRelease.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildReleasePatch(values)),
});
if (!response.ok) {
throw new Error(await readApiError(response));
}
return (await response.json()) as AtpAssetReleaseDetail;
}
const archiveFile = releaseArchiveFileList[0]?.originFileObj;
if (!(archiveFile instanceof File)) {
throw new Error("请上传版本 ZIP 包");
}
const formData = new FormData();
formData.append("release_tag", values.release_tag.trim());
formData.append("archive", archiveFile);
const response = await fetchWithAuth(`/api/v1/atp/assets/${assetId}/releases/upload`, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(await readApiError(response));
}
// 后端返回 {task_id, status},表示异步处理
return (await response.json()) as { task_id: string; status: string };
},
onSuccess: (result) => {
setReleaseModalOpen(false);
setEditingRelease(null);
setReleaseArchiveFileList([]);
releaseForm.resetFields();
if ("task_id" in result) {
// 异步上传:提示用户等待,WebSocket 会自动刷新数据
message.success("版本上传任务已提交,正在后台处理");
} else {
// 同步更新:立即刷新数据并选中新版本
void queryClient.invalidateQueries({ queryKey: ["atp-asset-detail", assetId] });
void queryClient.invalidateQueries({ queryKey: ["atp-asset-releases", assetId] });
void queryClient.invalidateQueries({ queryKey: ["atp-release-detail", result.id] });
void queryClient.invalidateQueries({ queryKey: ["atp-release-files", result.id] });
void queryClient.invalidateQueries({ queryKey: ["atp-release-runs", result.id] });
setSelectedReleaseIdState(result.id);
message.success(editingRelease ? "版本已更新" : "版本已创建");
}
},
onError: (candidate) => {
message.error(candidate instanceof Error ? candidate.message : "保存版本失败");
},
});
const activateMutation = useMutation({
mutationFn: async (releaseId: string) => {
const response = await fetchWithAuth(`/api/v1/atp/releases/${releaseId}/activate`, { method: "POST" });
if (!response.ok) {
throw new Error(await readApiError(response));
}
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["atp-asset-detail", assetId] });
void queryClient.invalidateQueries({ queryKey: ["atp-asset-releases", assetId] });
message.success("已切换当前激活版本");
},
onError: (candidate) => {
message.error(candidate instanceof Error ? candidate.message : "激活版本失败");
},
});
const runMutation = useMutation({
mutationFn: async (values: RunFormValues) => {
const response = await fetchWithAuth(`/api/v1/atp/releases/${selectedReleaseId}/runs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildRunPayload(values)),
});
if (!response.ok) {
throw new Error(await readApiError(response));
}
return (await response.json()) as AtpAssetRunDetail;
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["atp-release-runs", selectedReleaseId] });
setRunModalOpen(false);
runForm.resetFields();
message.success("运行任务已提交");
},
onError: (candidate) => {
message.error(candidate instanceof Error ? candidate.message : "提交运行任务失败");
},
});
const releaseColumns = useMemo<ColumnsType<AtpAssetReleaseSummary>>(
() => [
{
title: "版本",
key: "release",
render: (_, item) => (
<Space direction="vertical" size={0}>
<Typography.Text strong>{item.release_tag || `r${item.release_no}`}</Typography.Text>
<Typography.Text type="secondary">
{getAtpRunnerKindLabel(item.runner_kind)} / {item.storage_mount_code}
</Typography.Text>
</Space>
),
},
{
title: "存储目录",
dataIndex: "storage_root_path",
render: (value: string) => <Typography.Text code>{value}</Typography.Text>,
},
{
title: "状态",
key: "status",
render: (_, item) => {
const display = getAtpReleaseStatusDisplay(item.status);
return (
<Space wrap>
<Tag color={display.color}>{display.label}</Tag>
{item.is_active ? <Tag color="green"></Tag> : null}
{item.scenario_code ? <Tag color="blue">{item.scenario_code}</Tag> : null}
</Space>
);
},
},
{
title: "更新时间",
dataIndex: "update_date",
render: (value: string) => formatDateTime(value),
},
{
title: "操作",
key: "actions",
render: (_, item) => (
<Space wrap>
<Button size="small" type={item.id === selectedReleaseId ? "primary" : "default"} onClick={() => setSelectedReleaseIdState(item.id)}>
</Button>
<Button
size="small"
disabled={!canManage}
onClick={() => {
setEditingRelease(item);
setReleaseArchiveFileList([]);
releaseForm.setFieldsValue(toReleaseFormValues(item));
setReleaseModalOpen(true);
}}
>
</Button>
<Button size="small" disabled={!canManage || item.is_active} onClick={() => void activateMutation.mutateAsync(item.id)}>
</Button>
</Space>
),
},
],
[activateMutation, canManage, releaseForm, selectedReleaseId],
);
const fileColumns = useMemo<ColumnsType<AtpAssetFileEntry>>(
() => [
{
@@ -394,49 +125,6 @@ export default function AtpAssetDetailPage() {
[],
);
const runColumns = useMemo<ColumnsType<AtpAssetRunSummary>>(
() => [
{
title: "状态",
key: "status",
render: (_, item) => {
const display = getAtpRunStatusDisplay(item.status);
return (
<Space direction="vertical" size={0}>
<Tag color={display.color}>{display.label}</Tag>
<Typography.Text type="secondary">{formatDateTime(item.create_date)}</Typography.Text>
</Space>
);
},
},
{
title: "执行信息",
key: "execution",
render: (_, item) => (
<Space direction="vertical" size={0}>
<Typography.Text>
{getAtpRunnerKindLabel(item.runner_kind)} / {item.engine_mode}
</Typography.Text>
<Typography.Text type="secondary">
{item.timeout_seconds}s / exit {item.exit_code ?? "-"}
</Typography.Text>
</Space>
),
},
{
title: "日志尺寸",
key: "logs",
render: (_, item) => `${item.stdout_size} / ${item.stderr_size} B`,
},
{
title: "错误",
dataIndex: "error_message",
render: (value: string | null) => value || "-",
},
],
[],
);
if (initializing) {
return <AdminPageLoading tip="加载 ATP 模型详情中..." minHeightClassName="min-h-[280px]" />;
}
@@ -480,23 +168,9 @@ export default function AtpAssetDetailPage() {
<Card
title={asset.name}
extra={
<Space wrap>
<Link href="/admin/atp-models">
<Button></Button>
</Link>
<Button
type="primary"
disabled={!canManage}
onClick={() => {
setEditingRelease(null);
setReleaseArchiveFileList([]);
releaseForm.setFieldsValue(EMPTY_RELEASE_FORM);
setReleaseModalOpen(true);
}}
>
</Button>
</Space>
<Link href="/admin/atp-models">
<Button></Button>
</Link>
}
>
<Space direction="vertical" size={16} style={{ width: "100%" }}>
@@ -508,9 +182,7 @@ export default function AtpAssetDetailPage() {
<Descriptions.Item label="电压等级">{asset.voltage_level || "-"}</Descriptions.Item>
<Descriptions.Item label="塔型">{asset.tower_type || "-"}</Descriptions.Item>
<Descriptions.Item label="场景">{asset.scene_type || "-"}</Descriptions.Item>
<Descriptions.Item label="当前激活版本">
{asset.active_release_tag || (asset.active_release_no ? `r${asset.active_release_no}` : "-")}
</Descriptions.Item>
<Descriptions.Item label="避雷器装设组合">{asset.arrester_config || "-"}</Descriptions.Item>
<Descriptions.Item label="说明" span={2}>
{asset.description || "-"}
</Descriptions.Item>
@@ -518,35 +190,8 @@ export default function AtpAssetDetailPage() {
</Space>
</Card>
<Card title="版本列表">
{releasesQuery.error instanceof Error ? (
<Alert type="error" showIcon message="版本列表加载失败" description={releasesQuery.error.message} />
) : (
<Table<AtpAssetReleaseSummary>
rowKey="id"
loading={releasesQuery.isLoading}
columns={releaseColumns}
dataSource={releases}
locale={{ emptyText: "暂无版本" }}
pagination={false}
scroll={{ x: 1080 }}
/>
)}
</Card>
<Card
title={selectedRelease ? `当前版本:${selectedRelease.release_tag || `r${selectedRelease.release_no}`}` : "当前版本"}
extra={
<Button
disabled={!selectedReleaseId || !canRun}
onClick={() => {
runForm.setFieldsValue(EMPTY_RUN_FORM);
setRunModalOpen(true);
}}
>
/ Dry Run
</Button>
}
>
{!selectedRelease ? (
<Empty description="请选择一个版本" />
@@ -608,107 +253,6 @@ export default function AtpAssetDetailPage() {
/>
)}
</Card>
<Card title="运行记录">
{runsQuery.error instanceof Error ? (
<Alert type="error" showIcon message="运行记录加载失败" description={runsQuery.error.message} />
) : (
<Table<AtpAssetRunSummary>
rowKey="id"
loading={runsQuery.isLoading}
columns={runColumns}
dataSource={runsQuery.data?.items ?? []}
locale={{ emptyText: selectedReleaseId ? "当前版本暂无运行记录" : "请先选择版本" }}
pagination={false}
scroll={{ x: 980 }}
/>
)}
</Card>
<Modal
title={editingRelease ? "编辑版本" : "新建版本"}
open={releaseModalOpen}
onCancel={() => {
setReleaseModalOpen(false);
setEditingRelease(null);
setReleaseArchiveFileList([]);
releaseForm.resetFields();
}}
onOk={() => void releaseForm.submit()}
confirmLoading={saveReleaseMutation.isPending}
destroyOnClose
width={720}
>
<Form<ReleaseFormValues>
form={releaseForm}
layout="vertical"
initialValues={EMPTY_RELEASE_FORM}
onFinish={(values) => void saveReleaseMutation.mutateAsync(values)}
>
<Form.Item name="release_tag" label="版本标签" rules={[{ required: true, message: "请输入版本标签" }]}>
<Input placeholder="如 220-raoji3-v1" />
</Form.Item>
{editingRelease ? (
<Form.Item name="status" label="状态" rules={[{ required: true, message: "请选择状态" }]}>
<Select
options={[
{ value: "draft", label: "草稿" },
{ value: "released", label: "已发布" },
{ value: "archived", label: "归档" },
]}
/>
</Form.Item>
) : (
<Form.Item label="版本 ZIP 包" required>
<Upload
accept=".zip,application/zip,application/x-zip-compressed"
beforeUpload={() => false}
fileList={releaseArchiveFileList}
maxCount={1}
onChange={(info) => setReleaseArchiveFileList(info.fileList.slice(-1))}
>
<Button> ZIP </Button>
</Upload>
<Typography.Text type="secondary"> ZIP </Typography.Text>
</Form.Item>
)}
</Form>
</Modal>
<Modal
title="运行版本"
open={runModalOpen}
onCancel={() => {
setRunModalOpen(false);
runForm.resetFields();
}}
onOk={() => void runForm.submit()}
confirmLoading={runMutation.isPending}
destroyOnClose
>
<Form<RunFormValues>
form={runForm}
layout="vertical"
initialValues={EMPTY_RUN_FORM}
onFinish={(values) => void runMutation.mutateAsync(values)}
>
<Form.Item name="dry_run" label="Dry Run">
<Select
options={[
{ value: true, label: "是" },
{ value: false, label: "否" },
]}
/>
</Form.Item>
<Form.Item name="timeout_seconds" label="超时时间(秒)">
<InputNumber min={1} style={{ width: "100%" }} />
</Form.Item>
<Form.Item name="extra_args_text" label="附加参数">
<Input placeholder="多个参数用空格分隔" />
</Form.Item>
</Form>
</Modal>
</Space>
);
}
-22
View File
@@ -489,19 +489,6 @@ export default function AtpModelsPage() {
</Space>
),
},
{
title: "当前版本",
key: "release",
width: 160,
render: (_, item) => (
<Space direction="vertical" size={0}>
<Typography.Text>{item.active_release_tag || (item.active_release_no ? `r${item.active_release_no}` : "-")}</Typography.Text>
<Typography.Text type="secondary">
{item.release_count} / {item.run_count}
</Typography.Text>
</Space>
),
},
{
title: "更新时间",
key: "update_date",
@@ -629,15 +616,6 @@ export default function AtpModelsPage() {
<Tag>{item.arrester_config || "未设置避雷器"}</Tag>
</Space>
</div>
<div className="admin-atp-models-model-card-field">
<Typography.Text type="secondary"></Typography.Text>
<Space direction="vertical" size={0}>
<Typography.Text>{item.active_release_tag || (item.active_release_no ? `r${item.active_release_no}` : "-")}</Typography.Text>
<Typography.Text type="secondary">
{item.release_count} / {item.run_count}
</Typography.Text>
</Space>
</div>
<div className="admin-atp-models-model-card-field">
<Typography.Text type="secondary"></Typography.Text>
<Typography.Text>{formatDateTime(item.update_date)}</Typography.Text>