[feat]:[FL-214][fl-analysis 防雷计算页面UI交互重构]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -12,6 +12,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.turbo/
|
||||
graphify-out/
|
||||
|
||||
# Python
|
||||
.venv/
|
||||
|
||||
@@ -469,3 +469,23 @@
|
||||
|
||||
- 风险与关注点:
|
||||
- 任务监控数据仍来自既有 Flower 批量接口,本次只改变移动端前端渲染批次和滚动加载交互,不改变接口字段、权限或任务筛选规则。
|
||||
|
||||
# Work Log - 防雷分析页面 UI/交互重构(FL-214)
|
||||
|
||||
- 背景:
|
||||
- `/admin/fl-analysis` 页面原先将创建表单、任务选择、详情、结果表和 4 类弹窗全部集中在单个 `page.tsx` 内,初始页面负载过重。
|
||||
|
||||
- 本次处理:
|
||||
- 新增 `web/src/components/fl-analysis/` 组件目录,拆出创建表单、任务卡片列表、任务详情面板、结果表、详情弹窗、措施推荐/复算/报告弹窗和共享类型/格式化函数。
|
||||
- 创建表单改为默认收起,由“新建任务”按钮打开 Drawer;创建成功后关闭 Drawer 并选中新任务。
|
||||
- 任务选择从 Select 改为卡片网格,展示状态、任务类型、线路、创建/完成时间和杆塔统计。
|
||||
- 任务详情操作按钮按“执行 / 下游任务 / 导出下载”分组展示。
|
||||
|
||||
- 验证:
|
||||
- 基线:`npm --workspace web exec eslint src/app/admin/fl-analysis/page.tsx --max-warnings=0` 通过。
|
||||
- 基线:`npm --workspace web exec tsc --noEmit --pretty false` 通过。
|
||||
- 修改后:`npm --workspace web exec eslint src/app/admin/fl-analysis/page.tsx src/components/fl-analysis --max-warnings=0` 通过。
|
||||
- 修改后:`npm --workspace web exec tsc --noEmit --pretty false` 通过。
|
||||
|
||||
- 风险与关注点:
|
||||
- 改动仅影响 `/admin/fl-analysis` 前端视图组织与交互入口,不改变后端接口、请求/响应字段、权限判断或任务创建/启动语义。
|
||||
|
||||
+196
-1534
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Button, Form, Input, InputNumber, Select, Space, Tag } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
|
||||
import { readLinePreparation } from "@/lib/line-preparation";
|
||||
import type {
|
||||
AtpEngineStatusResponse,
|
||||
AtpModelSummary,
|
||||
LineSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
CREATE_JOB_DEFAULTS,
|
||||
formatDateTime,
|
||||
formatExternalAdapter,
|
||||
formatJobType,
|
||||
preparationColor,
|
||||
readObject,
|
||||
readOptionalNumber,
|
||||
readOptionalString,
|
||||
type CreateJobFormValues,
|
||||
} from "./types";
|
||||
|
||||
type AdapterOption = {
|
||||
value: "placeholder" | "atp" | "wine";
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type CreateJobFormProps = {
|
||||
form: FormInstance<CreateJobFormValues>;
|
||||
lines: LineSummary[];
|
||||
linesLoading: boolean;
|
||||
selectedLine: LineSummary | null;
|
||||
selectedJobType: CreateJobFormValues["job_type"];
|
||||
selectedExternalAdapter: CreateJobFormValues["external_adapter"];
|
||||
adapterOptions: AdapterOption[];
|
||||
atpModels: AtpModelSummary[];
|
||||
atpModelsLoading: boolean;
|
||||
atpModelsError: unknown;
|
||||
selectedAtpModel: AtpModelSummary | null;
|
||||
engineQueryData: AtpEngineStatusResponse | undefined;
|
||||
workflowExecutionMessage: string;
|
||||
submitting: boolean;
|
||||
onSubmit: (values: CreateJobFormValues) => void;
|
||||
};
|
||||
|
||||
export function CreateJobForm({
|
||||
form,
|
||||
lines,
|
||||
linesLoading,
|
||||
selectedLine,
|
||||
selectedJobType,
|
||||
selectedExternalAdapter,
|
||||
adapterOptions,
|
||||
atpModels,
|
||||
atpModelsLoading,
|
||||
atpModelsError,
|
||||
selectedAtpModel,
|
||||
engineQueryData,
|
||||
workflowExecutionMessage,
|
||||
submitting,
|
||||
onSubmit,
|
||||
}: CreateJobFormProps) {
|
||||
const selectedLinePreparation = readLinePreparation(selectedLine);
|
||||
const externalAdapterActive = selectedExternalAdapter === "atp" || selectedExternalAdapter === "wine";
|
||||
|
||||
return (
|
||||
<Form<CreateJobFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={CREATE_JOB_DEFAULTS}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<Form.Item
|
||||
name="line_id"
|
||||
label="线路"
|
||||
rules={[{ required: true, message: "请选择线路" }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择线路"
|
||||
loading={linesLoading}
|
||||
options={lines.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name || item.code} / ${item.code}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="job_type" label="任务类型" rules={[{ required: true, message: "请选择任务类型" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "normal", label: "普通计算" },
|
||||
{ value: "tongtiao", label: "同跳计算" },
|
||||
{ value: "risk", label: "风险评估" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{selectedJobType === "normal" || selectedJobType === "tongtiao" ? (
|
||||
<Form.Item
|
||||
name="external_adapter"
|
||||
label="执行适配器"
|
||||
rules={[{ required: true, message: "请选择执行适配器" }]}
|
||||
>
|
||||
<Select options={adapterOptions.map((item) => ({ ...item }))} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item name="job_name" label="任务名">
|
||||
<Input
|
||||
placeholder={selectedLine
|
||||
? `${selectedLine.name || selectedLine.code}-${formatJobType(selectedJobType)}`
|
||||
: `${formatJobType(selectedJobType)}任务`}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label=" ">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={submitting}
|
||||
disabled={!selectedLine || !selectedLinePreparation.all_ready}
|
||||
className="w-full"
|
||||
>
|
||||
创建并启动{formatJobType(selectedJobType)}任务
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{selectedLine ? (
|
||||
<Alert
|
||||
type={selectedLinePreparation.all_ready ? "success" : "warning"}
|
||||
showIcon
|
||||
message={selectedLinePreparation.all_ready ? "参数准备已完成" : `当前线路缺少:${selectedLinePreparation.missing_items.join("、")}`}
|
||||
description={
|
||||
<Space size={[8, 8]} wrap>
|
||||
{[
|
||||
selectedLinePreparation.lightning_current,
|
||||
selectedLinePreparation.lightning_density,
|
||||
selectedLinePreparation.ground_slope,
|
||||
].map((item) => {
|
||||
const source = readObject(item.source);
|
||||
const preparedAt = readOptionalString(source, "prepared_at");
|
||||
const currentA = readOptionalNumber(readObject(item.values), "current_a");
|
||||
const currentB = readOptionalNumber(readObject(item.values), "current_b");
|
||||
const suffix = item.key === "lightning_current" && currentA !== null && currentB !== null
|
||||
? ` (${currentA.toFixed(3)} / ${currentB.toFixed(3)})`
|
||||
: "";
|
||||
return (
|
||||
<Tag key={item.key} color={preparationColor(item.ready)}>
|
||||
{`${item.label}${suffix} ${item.tower_ready_count}/${item.tower_total_count}${preparedAt ? ` @ ${formatDateTime(preparedAt)}` : ""}`}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selectedJobType === "normal" || selectedJobType === "tongtiao" ? (
|
||||
<>
|
||||
<Alert
|
||||
type={externalAdapterActive && engineQueryData?.available === false ? "warning" : "info"}
|
||||
showIcon
|
||||
message={workflowExecutionMessage}
|
||||
/>
|
||||
{atpModelsError && externalAdapterActive ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={atpModelsError instanceof Error ? atpModelsError.message : "ATP 模型列表加载失败"}
|
||||
/>
|
||||
) : null}
|
||||
{externalAdapterActive ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Form.Item
|
||||
name="atp_model_id"
|
||||
label="ATP模型"
|
||||
rules={[{ required: true, message: "请选择 ATP 模型" }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
loading={atpModelsLoading}
|
||||
placeholder="选择 ATP 模型"
|
||||
options={atpModels.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} / ${item.code}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={`执行模式:${engineQueryData ? formatExternalAdapter(engineQueryData.mode === "wine" ? "wine" : "atp") : "-"}`}
|
||||
description={selectedAtpModel ? `当前模型:${selectedAtpModel.name} / ${selectedAtpModel.code}。执行时默认使用该模型的当前模板。` : "从 ATP 模型管理中选择可用模板。"}
|
||||
className="md:col-span-1 xl:col-span-2"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<Form.Item name="current_waveform" label="雷电流波形">
|
||||
<Select
|
||||
options={[
|
||||
{ value: "heidler", label: "Heidler" },
|
||||
{ value: "double_slope", label: "双斜角" },
|
||||
{ value: "double_exponential", label: "双指数" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="flashover_method" label="闪络判据">
|
||||
<Select
|
||||
options={[
|
||||
{ value: "guideline", label: "规程法" },
|
||||
{ value: "intersection", label: "相交法" },
|
||||
{ value: "leader_development", label: "先导发展法" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="altitude_correction" label="海拔修正">
|
||||
<Select
|
||||
options={[
|
||||
{ value: "none", label: "无" },
|
||||
{ value: "formula1", label: "推荐公式1" },
|
||||
{ value: "formula2", label: "推荐公式2" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="induced_voltage_formula" label="感应电压公式">
|
||||
<Select
|
||||
options={[
|
||||
{ value: "formula1", label: "公式1" },
|
||||
{ value: "formula2", label: "公式2" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="head_time_min_us" label="波头时间最小(μs)">
|
||||
<InputNumber min={0.1} step={0.1} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="head_time_max_us" label="波头时间最大(μs)">
|
||||
<InputNumber min={0.1} step={0.1} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="head_time_step_us" label="波头步长(μs)">
|
||||
<InputNumber min={0.05} step={0.05} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="tail_time_min_us" label="波尾时间最小(μs)">
|
||||
<InputNumber min={1} step={1} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="tail_time_max_us" label="波尾时间最大(μs)">
|
||||
<InputNumber min={1} step={1} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="tail_time_step_us" label="波尾步长(μs)">
|
||||
<InputNumber min={0.5} step={0.5} precision={2} className="w-full" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
"use client";
|
||||
|
||||
import { Descriptions, Empty, Modal, Space, Table, Tag, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobDetail,
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatAltitudeCorrection,
|
||||
formatCurrentWaveform,
|
||||
formatExternalAdapter,
|
||||
formatFlashoverMethod,
|
||||
formatInducedVoltageFormula,
|
||||
formatRangeSummary,
|
||||
formatRiskLevel,
|
||||
readCurrentRisk,
|
||||
readCurrentScore,
|
||||
readMitigationActions,
|
||||
readMultiPhaseResults,
|
||||
readObject,
|
||||
readOptionalNumber,
|
||||
readOptionalString,
|
||||
readPhaseResults,
|
||||
readReasonDetails,
|
||||
readScanPoints,
|
||||
readSelectedCase,
|
||||
readString,
|
||||
readWorkflow,
|
||||
riskColor,
|
||||
waveformJobType,
|
||||
type MitigationAction,
|
||||
type MultiPhaseResult,
|
||||
type PhaseResult,
|
||||
type ReasonDetail,
|
||||
type ScanPoint,
|
||||
} from "./types";
|
||||
|
||||
const reasonDetailColumns: ColumnsType<ReasonDetail> = [
|
||||
{ title: "因子", dataIndex: "label", width: 180 },
|
||||
{
|
||||
title: "当前值",
|
||||
key: "value",
|
||||
render: (_value, row) => (row.value === undefined || row.value === null ? "-" : String(row.value)),
|
||||
},
|
||||
{
|
||||
title: "标准值",
|
||||
key: "standard_value",
|
||||
render: (_value, row) => (row.standard_value === undefined || row.standard_value === null ? "-" : String(row.standard_value)),
|
||||
},
|
||||
{
|
||||
title: "档次",
|
||||
dataIndex: "grade",
|
||||
width: 90,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "是否命中",
|
||||
dataIndex: "triggered",
|
||||
width: 100,
|
||||
render: (value: boolean | undefined) => (value ? <Tag color="red">是</Tag> : <Tag>否</Tag>),
|
||||
},
|
||||
];
|
||||
|
||||
const mitigationActionColumns: ColumnsType<MitigationAction> = [
|
||||
{ title: "动作", dataIndex: "label", width: 160 },
|
||||
{ title: "建议", dataIndex: "summary" },
|
||||
{
|
||||
title: "当前值",
|
||||
key: "current_value",
|
||||
render: (_value, row) => (row.current_value === undefined || row.current_value === null ? "-" : String(row.current_value)),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "目标值",
|
||||
key: "target_value",
|
||||
render: (_value, row) => (row.target_value === undefined || row.target_value === null ? "-" : String(row.target_value)),
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const scanPointColumns: ColumnsType<ScanPoint> = [
|
||||
{ title: "波头(μs)", dataIndex: "head_time_us", width: 100, render: (value: number | null | undefined) => value ?? "-" },
|
||||
{ title: "波尾(μs)", dataIndex: "tail_time_us", width: 100, render: (value: number | null | undefined) => value ?? "-" },
|
||||
{
|
||||
title: "风险",
|
||||
dataIndex: "risk_level",
|
||||
width: 100,
|
||||
render: (value: string | null | undefined) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
},
|
||||
{ title: "得分", dataIndex: "score", width: 90, render: (value: number | null | undefined) => value ?? "-" },
|
||||
{
|
||||
title: "反击跳闸率",
|
||||
dataIndex: "counterstrike_trip_rate",
|
||||
width: 120,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "绕击跳闸率",
|
||||
dataIndex: "shielding_trip_rate",
|
||||
width: 120,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "闪络相",
|
||||
dataIndex: "flashover_phase",
|
||||
width: 160,
|
||||
render: (value: string | null | undefined) => value || "-",
|
||||
},
|
||||
];
|
||||
|
||||
const phaseResultColumns: ColumnsType<PhaseResult> = [
|
||||
{ title: "相别", dataIndex: "phase", width: 120 },
|
||||
{ title: "回路", dataIndex: "circuit", width: 90, render: (value: string | null | undefined) => value || "-" },
|
||||
{
|
||||
title: "A/B/C绕击耐雷水平(kA)",
|
||||
dataIndex: "shielding_withstand_ka",
|
||||
width: 180,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "A/B/C绕击跳闸率",
|
||||
dataIndex: "shielding_trip_rate",
|
||||
width: 140,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "反击耐雷水平(kA)",
|
||||
dataIndex: "counterstrike_withstand_ka",
|
||||
width: 140,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "反击跳闸率",
|
||||
dataIndex: "counterstrike_trip_rate",
|
||||
width: 120,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
];
|
||||
|
||||
const multiPhaseColumns: ColumnsType<MultiPhaseResult> = [
|
||||
{ title: "相数组合", dataIndex: "label", width: 100 },
|
||||
{ title: "闪络相", dataIndex: "flashover_phase", render: (value: string | null | undefined) => value || "-" },
|
||||
{
|
||||
title: "反击耐雷水平(kA)",
|
||||
dataIndex: "counterstrike_withstand_ka",
|
||||
width: 140,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "跳闸率",
|
||||
dataIndex: "trip_rate",
|
||||
width: 120,
|
||||
render: (value: number | null | undefined) => value ?? "-",
|
||||
},
|
||||
];
|
||||
|
||||
type DetailModalProps = {
|
||||
open: boolean;
|
||||
detailRow: FlAnalysisTowerResultSummary | null;
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
selectedJobDetail: FlAnalysisJobDetail | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function DetailModal({
|
||||
open,
|
||||
detailRow,
|
||||
selectedJob,
|
||||
selectedJobDetail,
|
||||
onClose,
|
||||
}: DetailModalProps) {
|
||||
const detailResultObject = readObject(detailRow?.result_json);
|
||||
const reasonDetails = readReasonDetails(detailRow);
|
||||
const mitigationActions = readMitigationActions(detailRow);
|
||||
const scanPoints = readScanPoints(detailRow);
|
||||
const phaseResults = readPhaseResults(detailRow);
|
||||
const multiPhaseResults = readMultiPhaseResults(detailRow);
|
||||
const detailWorkflow = readWorkflow(detailRow);
|
||||
const detailSelectedCase = readSelectedCase(detailRow);
|
||||
const selectedJobDetailWaveformType = waveformJobType(selectedJobDetail ?? selectedJob);
|
||||
const detailExternalExecution = readObject(detailResultObject.external_execution);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={detailRow ? `${selectedJob?.job_type === "mitigation" ? "高风险原因" : "计算详情"} - ${detailRow.tower_no}` : "计算详情"}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={980}
|
||||
>
|
||||
{!detailRow ? (
|
||||
<Empty description="暂无杆塔详情" />
|
||||
) : (
|
||||
<Space direction="vertical" size={16} className="flex w-full">
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="当前结论">{detailRow.summary_text || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label={selectedJob?.job_type === "mitigation" ? "预期风险/风险等级" : "风险等级"}>
|
||||
<Tag color={riskColor(detailRow.risk_level)}>{formatRiskLevel(detailRow.risk_level)}</Tag>
|
||||
</Descriptions.Item>
|
||||
{selectedJob?.job_type === "mitigation" ? (
|
||||
<>
|
||||
<Descriptions.Item label="当前风险">
|
||||
<Tag color={riskColor(readCurrentRisk(detailRow))}>{formatRiskLevel(readCurrentRisk(detailRow))}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="改造结论">{readString(detailResultObject, "recommendation_result")}</Descriptions.Item>
|
||||
</>
|
||||
) : selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? (
|
||||
<>
|
||||
<Descriptions.Item label="最不利点(μs)">
|
||||
{typeof detailSelectedCase.head_time_us === "number" && typeof detailSelectedCase.tail_time_us === "number"
|
||||
? `${detailSelectedCase.head_time_us}/${detailSelectedCase.tail_time_us}`
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="扫描点数">{String(detailWorkflow.scan_point_count ?? "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="雷电流波形">{formatCurrentWaveform(detailWorkflow.current_waveform)}</Descriptions.Item>
|
||||
<Descriptions.Item label="闪络判据">{formatFlashoverMethod(detailWorkflow.flashover_method)}</Descriptions.Item>
|
||||
<Descriptions.Item label="海拔修正">{formatAltitudeCorrection(detailWorkflow.altitude_correction)}</Descriptions.Item>
|
||||
<Descriptions.Item label="感应电压公式">{formatInducedVoltageFormula(detailWorkflow.induced_voltage_formula)}</Descriptions.Item>
|
||||
<Descriptions.Item label="波头范围">{formatRangeSummary(detailWorkflow.head_time_range_us)}</Descriptions.Item>
|
||||
<Descriptions.Item label="波尾范围">{formatRangeSummary(detailWorkflow.tail_time_range_us)}</Descriptions.Item>
|
||||
<Descriptions.Item label="执行适配器">
|
||||
{formatExternalAdapter(readOptionalString(detailExternalExecution, "adapter"))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ATP模型">
|
||||
{readOptionalString(detailExternalExecution, "model_name") || readOptionalString(detailExternalExecution, "model_code") || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模型版本">
|
||||
{typeof readOptionalNumber(detailExternalExecution, "version_no") === "number"
|
||||
? `v${readOptionalNumber(detailExternalExecution, "version_no")}`
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
{selectedJobDetailWaveformType === "tongtiao" ? (
|
||||
<>
|
||||
<Descriptions.Item label="主导相组">{readOptionalString(detailResultObject, "dominant_phase_set") ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="闪络相">{readOptionalString(detailResultObject, "flashover_phase") ?? "-"}</Descriptions.Item>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<Descriptions.Item label="原因分析" span={2}>
|
||||
{readString(detailResultObject, "cause_analysis")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="措施建议" span={2}>
|
||||
{readString(detailResultObject, "mitigation_recommendation")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前得分">
|
||||
{selectedJob?.job_type === "mitigation"
|
||||
? String(readCurrentScore(detailRow) ?? "-")
|
||||
: String(readOptionalNumber(detailResultObject, "score") ?? "-")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="预期得分/得分">
|
||||
{selectedJob?.job_type === "mitigation"
|
||||
? String(readOptionalNumber(detailResultObject, "expected_score") ?? "-")
|
||||
: String(readOptionalNumber(detailResultObject, "score") ?? "-")}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
原因细项
|
||||
</Typography.Title>
|
||||
{reasonDetails.length === 0 ? (
|
||||
<Empty description="暂无细项分级" />
|
||||
) : (
|
||||
<Table<ReasonDetail>
|
||||
rowKey="code"
|
||||
columns={reasonDetailColumns}
|
||||
dataSource={reasonDetails}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
波形扫描
|
||||
</Typography.Title>
|
||||
{scanPoints.length === 0 ? (
|
||||
<Empty description="当前结果未生成扫描点" />
|
||||
) : (
|
||||
<Table<ScanPoint>
|
||||
rowKey={(row) => `${row.head_time_us ?? "-"}-${row.tail_time_us ?? "-"}`}
|
||||
columns={scanPointColumns}
|
||||
dataSource={scanPoints}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{selectedJobDetailWaveformType === "tongtiao" ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
相别结果
|
||||
</Typography.Title>
|
||||
{phaseResults.length === 0 ? (
|
||||
<Empty description="当前结果未生成相别结果" />
|
||||
) : (
|
||||
<Table<PhaseResult>
|
||||
rowKey="phase"
|
||||
columns={phaseResultColumns}
|
||||
dataSource={phaseResults}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
多相结果
|
||||
</Typography.Title>
|
||||
{multiPhaseResults.length === 0 ? (
|
||||
<Empty description="当前结果未生成多相结果" />
|
||||
) : (
|
||||
<Table<MultiPhaseResult>
|
||||
rowKey="label"
|
||||
columns={multiPhaseColumns}
|
||||
dataSource={multiPhaseResults}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
改造动作
|
||||
</Typography.Title>
|
||||
{mitigationActions.length === 0 ? (
|
||||
<Empty description="当前结果未生成改造动作" />
|
||||
) : (
|
||||
<Table<MitigationAction>
|
||||
rowKey="code"
|
||||
columns={mitigationActionColumns}
|
||||
dataSource={mitigationActions}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { Empty, Spin, Space, Tag, Typography } from "antd";
|
||||
|
||||
import { Card } from "@/components/ui-antd";
|
||||
import type { FlAnalysisJobSummary } from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatDateTime,
|
||||
formatJobType,
|
||||
mitigationMode,
|
||||
statusColor,
|
||||
} from "./types";
|
||||
|
||||
type JobCardListProps = {
|
||||
jobs: FlAnalysisJobSummary[];
|
||||
selectedJobId: string | null;
|
||||
loading: boolean;
|
||||
onSelect: (jobId: string) => void;
|
||||
};
|
||||
|
||||
export function JobCardList({ jobs, selectedJobId, loading, onSelect }: JobCardListProps) {
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
if (jobs.length === 0) {
|
||||
return <Empty description="暂无防雷分析任务" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{jobs.map((job) => {
|
||||
const selected = job.id === selectedJobId;
|
||||
const lineName = job.line_name || job.line_code || "-";
|
||||
return (
|
||||
<button
|
||||
key={job.id}
|
||||
type="button"
|
||||
className={`w-full cursor-pointer rounded-lg border bg-white p-3 text-left transition hover:border-blue-400 hover:shadow-sm ${
|
||||
selected ? "border-blue-500 shadow-sm ring-1 ring-blue-500" : "border-gray-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelect(job.id);
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={8} className="flex w-full">
|
||||
<Space size={[6, 6]} wrap>
|
||||
<Tag color={statusColor(job.status)}>{job.status}</Tag>
|
||||
<Tag>{formatJobType(job.job_type, mitigationMode(job))}</Tag>
|
||||
</Space>
|
||||
<div>
|
||||
<Typography.Text strong className="line-clamp-1">
|
||||
{job.job_name || lineName}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="block line-clamp-1">
|
||||
{lineName}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-gray-500">
|
||||
<span>创建:{formatDateTime(job.create_date)}</span>
|
||||
<span>完成:{formatDateTime(job.finished_at)}</span>
|
||||
<span>杆塔:{job.total_tower_count}</span>
|
||||
<span>结果:{job.result_tower_count}</span>
|
||||
</div>
|
||||
</Space>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JobListCard(props: JobCardListProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Space direction="vertical" size={12} className="flex w-full">
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
任务列表
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
点击卡片查看任务详情与杆塔结果。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<JobCardList {...props} />
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Button, Descriptions, Divider, Empty, Space, Spin, Tag } from "antd";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobDetail,
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatAltitudeCorrection,
|
||||
formatCurrentWaveform,
|
||||
formatDateTime,
|
||||
formatExternalAdapter,
|
||||
formatFlashoverMethod,
|
||||
formatInducedVoltageFormula,
|
||||
formatJobType,
|
||||
formatRangeSummary,
|
||||
mitigationMode,
|
||||
readObject,
|
||||
readOptionalNumber,
|
||||
readOptionalString,
|
||||
selectedTowerCount,
|
||||
statusColor,
|
||||
type WorkflowSummary,
|
||||
} from "./types";
|
||||
|
||||
type JobDetailPanelProps = {
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
selectedJobDetail: FlAnalysisJobDetail | null;
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
canManage: boolean;
|
||||
canCreateMitigation: boolean;
|
||||
canCreateScenario: boolean;
|
||||
canCreateReport: boolean;
|
||||
canDownloadResults: boolean;
|
||||
selectedJobDetailWaveformType: "normal" | "tongtiao" | null;
|
||||
candidateMitigationRows: FlAnalysisTowerResultSummary[];
|
||||
candidateScenarioRows: FlAnalysisTowerResultSummary[];
|
||||
candidateScenarioBaseJobs: FlAnalysisJobSummary[];
|
||||
candidateReportRows: FlAnalysisTowerResultSummary[];
|
||||
startPending: boolean;
|
||||
downloadResultsPending: boolean;
|
||||
downloadReportPending: boolean;
|
||||
onStart: () => void;
|
||||
onOpenMitigation: () => void;
|
||||
onOpenScenario: () => void;
|
||||
onOpenReport: () => void;
|
||||
onDownloadResults: () => void;
|
||||
onDownloadReport: () => void;
|
||||
};
|
||||
|
||||
export function JobDetailPanel({
|
||||
selectedJob,
|
||||
selectedJobDetail,
|
||||
loading,
|
||||
error,
|
||||
canManage,
|
||||
canCreateMitigation,
|
||||
canCreateScenario,
|
||||
canCreateReport,
|
||||
canDownloadResults,
|
||||
selectedJobDetailWaveformType,
|
||||
candidateMitigationRows,
|
||||
candidateScenarioRows,
|
||||
candidateScenarioBaseJobs,
|
||||
candidateReportRows,
|
||||
startPending,
|
||||
downloadResultsPending,
|
||||
downloadReportPending,
|
||||
onStart,
|
||||
onOpenMitigation,
|
||||
onOpenScenario,
|
||||
onOpenReport,
|
||||
onDownloadResults,
|
||||
onDownloadReport,
|
||||
}: JobDetailPanelProps) {
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={error instanceof Error ? error.message : "任务详情加载失败"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedJobDetail || !selectedJob) {
|
||||
return <Empty description="暂无任务详情" />;
|
||||
}
|
||||
|
||||
const selectedJobExecutionOptions = readObject(selectedJobDetail.execution_options_json);
|
||||
const selectedJobSummary = readObject(selectedJobDetail.result_summary_json);
|
||||
const selectedJobWorkflow = readObject(selectedJobDetail.result_summary_json).workflow as WorkflowSummary | undefined;
|
||||
const selectedJobExternalModelCode = readOptionalString(selectedJobSummary, "external_model_code");
|
||||
const selectedJobExternalModelName = readOptionalString(selectedJobSummary, "external_model_name");
|
||||
const selectedJobExternalVersionNo = readOptionalNumber(selectedJobSummary, "external_version_no");
|
||||
const sourceJobId = readOptionalString(selectedJobExecutionOptions, "source_job_id");
|
||||
const scenarioBaseJobName = readOptionalString(selectedJobExecutionOptions, "base_job_name");
|
||||
const scenarioBaseJobType = readOptionalString(selectedJobExecutionOptions, "base_job_type");
|
||||
const reportSourceJobType = readOptionalString(selectedJobSummary, "source_job_type");
|
||||
const reportSourceJobName = readOptionalString(selectedJobSummary, "source_job_name");
|
||||
const reportMitigationJobName = readOptionalString(selectedJobSummary, "mitigation_job_name");
|
||||
const reportScenarioJobName = readOptionalString(selectedJobSummary, "scenario_job_name");
|
||||
const reportDocumentName = readOptionalString(selectedJobSummary, "document_filename");
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={16} className="flex w-full">
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="任务名称">{selectedJobDetail.job_name || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="任务状态">
|
||||
<Tag color={statusColor(selectedJobDetail.status)}>{selectedJobDetail.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="线路">{selectedJobDetail.line_name || selectedJobDetail.line_code || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="任务类型">{formatJobType(selectedJobDetail.job_type, mitigationMode(selectedJobDetail))}</Descriptions.Item>
|
||||
<Descriptions.Item label="结果杆塔数">{selectedJobDetail.result_tower_count}</Descriptions.Item>
|
||||
<Descriptions.Item label="完成时间">{formatDateTime(selectedJobDetail.finished_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="风险计数" span={2}>
|
||||
{JSON.stringify(selectedJobDetail.result_summary_json?.risk_counts ?? {}, null, 2)}
|
||||
</Descriptions.Item>
|
||||
{selectedJobDetail.job_type === "mitigation" ? (
|
||||
<>
|
||||
<Descriptions.Item label="前驱风险任务">{sourceJobId || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="选塔数">{String(selectedTowerCount(selectedJobDetail) || "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="模式">{mitigationMode(selectedJobDetail) ? "非建线" : "常规建线"}</Descriptions.Item>
|
||||
<Descriptions.Item label="需装避雷器数">
|
||||
{String(readObject(selectedJobDetail.result_summary_json).arrester_required_count ?? "-")}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
) : selectedJobDetail.job_type === "scenario" ? (
|
||||
<>
|
||||
<Descriptions.Item label="前驱措施任务">{sourceJobId || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="选塔数">{String(selectedTowerCount(selectedJobDetail) || "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="复用计算口径">
|
||||
{scenarioBaseJobType ? formatJobType(scenarioBaseJobType) : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="基线计算任务">{scenarioBaseJobName || "-"}</Descriptions.Item>
|
||||
</>
|
||||
) : selectedJobDetail.job_type === "report" ? (
|
||||
<>
|
||||
<Descriptions.Item label="报告来源">
|
||||
{reportSourceJobType ? formatJobType(reportSourceJobType) : "-"}
|
||||
{reportSourceJobName ? ` / ${reportSourceJobName}` : ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="选塔数">
|
||||
{String(readObject(selectedJobDetail.result_summary_json).selected_tower_count ?? "-")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联措施任务">{reportMitigationJobName || "未关联"}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联复算任务">{reportScenarioJobName || "未关联"}</Descriptions.Item>
|
||||
<Descriptions.Item label="文档名" span={2}>{reportDocumentName || "-"}</Descriptions.Item>
|
||||
</>
|
||||
) : selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? (
|
||||
<>
|
||||
<Descriptions.Item label="适配器">{formatExternalAdapter(selectedJobDetail.external_adapter)}</Descriptions.Item>
|
||||
<Descriptions.Item label="ATP模型">
|
||||
{selectedJobExternalModelName || selectedJobExternalModelCode || "-"}
|
||||
{selectedJobExternalModelName && selectedJobExternalModelCode ? ` / ${selectedJobExternalModelCode}` : ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模型版本">
|
||||
{typeof selectedJobExternalVersionNo === "number" ? `v${selectedJobExternalVersionNo}` : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平均得分">{String(selectedJobDetail.result_summary_json?.score_average ?? "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="平均扫描点">{String(readObject(selectedJobDetail.result_summary_json).scan_point_average ?? "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="雷电流波形">{formatCurrentWaveform(selectedJobWorkflow?.current_waveform)}</Descriptions.Item>
|
||||
<Descriptions.Item label="闪络判据">{formatFlashoverMethod(selectedJobWorkflow?.flashover_method)}</Descriptions.Item>
|
||||
<Descriptions.Item label="海拔修正">{formatAltitudeCorrection(selectedJobWorkflow?.altitude_correction)}</Descriptions.Item>
|
||||
<Descriptions.Item label="感应电压公式">{formatInducedVoltageFormula(selectedJobWorkflow?.induced_voltage_formula)}</Descriptions.Item>
|
||||
<Descriptions.Item label="波头范围">{formatRangeSummary(selectedJobWorkflow?.head_time_range_us)}</Descriptions.Item>
|
||||
<Descriptions.Item label="波尾范围">{formatRangeSummary(selectedJobWorkflow?.tail_time_range_us)}</Descriptions.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Descriptions.Item label="平均得分">{String(selectedJobDetail.result_summary_json?.score_average ?? "-")}</Descriptions.Item>
|
||||
<Descriptions.Item label="适配器">{String(selectedJobDetail.external_adapter || "-")}</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
{canManage || selectedJob.job_type === "report" ? (
|
||||
<Space size={12} wrap split={<Divider type="vertical" />}>
|
||||
{canManage ? (
|
||||
<Space wrap>
|
||||
<Button onClick={onStart} loading={startPending}>
|
||||
{selectedJob.status === "success" ? "重新执行任务" : "启动任务"}
|
||||
</Button>
|
||||
</Space>
|
||||
) : null}
|
||||
{canManage && (canCreateMitigation || canCreateScenario || canCreateReport) ? (
|
||||
<Space wrap>
|
||||
{canCreateMitigation ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={candidateMitigationRows.length === 0}
|
||||
onClick={onOpenMitigation}
|
||||
>
|
||||
生成措施推荐
|
||||
</Button>
|
||||
) : null}
|
||||
{canCreateScenario ? (
|
||||
<Button
|
||||
disabled={candidateScenarioRows.length === 0 || candidateScenarioBaseJobs.length === 0}
|
||||
onClick={onOpenScenario}
|
||||
>
|
||||
加装避雷器复算
|
||||
</Button>
|
||||
) : null}
|
||||
{canCreateReport ? (
|
||||
<Button
|
||||
disabled={candidateReportRows.length === 0}
|
||||
onClick={onOpenReport}
|
||||
>
|
||||
生成报告
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null}
|
||||
<Space wrap>
|
||||
{selectedJob.job_type !== "report" ? (
|
||||
<Button
|
||||
onClick={onDownloadResults}
|
||||
loading={downloadResultsPending}
|
||||
disabled={!canDownloadResults}
|
||||
>
|
||||
导出结果
|
||||
</Button>
|
||||
) : null}
|
||||
{selectedJob.job_type === "report" ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onDownloadReport}
|
||||
loading={downloadReportPending}
|
||||
disabled={selectedJob.status !== "success"}
|
||||
>
|
||||
下载报告
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</Space>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Checkbox, Empty, Form, Input, Modal, Space, Table, Tag, Typography } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatRiskLevel,
|
||||
readObject,
|
||||
readString,
|
||||
riskColor,
|
||||
type MitigationFormValues,
|
||||
} from "./types";
|
||||
|
||||
type MitigationModalProps = {
|
||||
open: boolean;
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
rows: FlAnalysisTowerResultSummary[];
|
||||
selectedTowerIds: string[];
|
||||
form: FormInstance<MitigationFormValues>;
|
||||
submitting: boolean;
|
||||
onSelectedTowerIdsChange: (towerIds: string[]) => void;
|
||||
onSubmit: (values: MitigationFormValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function MitigationModal({
|
||||
open,
|
||||
selectedJob,
|
||||
rows,
|
||||
selectedTowerIds,
|
||||
form,
|
||||
submitting,
|
||||
onSelectedTowerIdsChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: MitigationModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={selectedJob ? `措施推荐 - ${selectedJob.job_name || selectedJob.line_name || selectedJob.line_code}` : "措施推荐"}
|
||||
open={open}
|
||||
width={1080}
|
||||
confirmLoading={submitting}
|
||||
okText="创建并启动措施任务"
|
||||
onCancel={() => {
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={16} className="flex w-full">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="源端迁移口径:仅允许从已有风险结果中选择高风险杆塔,生成措施推荐任务。"
|
||||
/>
|
||||
<Form<MitigationFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
initialValues={{ non_construction: false }}
|
||||
>
|
||||
<Form.Item
|
||||
name="job_name"
|
||||
label="任务名称"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="措施推荐任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="non_construction" valuePropName="checked">
|
||||
<Checkbox>按“非建线”模式生成措施推荐</Checkbox>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<Empty description="当前风险任务没有中高风险杆塔,无法生成措施推荐任务" />
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text type="secondary">
|
||||
已命中 {rows.length} 座中高风险杆塔。默认全选,可按需缩小推荐范围。
|
||||
</Typography.Text>
|
||||
<Table<FlAnalysisTowerResultSummary>
|
||||
rowKey="tower_id"
|
||||
size="small"
|
||||
pagination={{ pageSize: 8, showSizeChanger: false, hideOnSinglePage: false }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedTowerIds,
|
||||
onChange: (keys) => {
|
||||
onSelectedTowerIdsChange(keys.map((item) => String(item)));
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{ title: "杆塔号", dataIndex: "tower_no", width: 120 },
|
||||
{
|
||||
title: "风险等级",
|
||||
dataIndex: "risk_level",
|
||||
width: 120,
|
||||
render: (value: string | null) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "高风险原因",
|
||||
key: "cause_analysis",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "cause_analysis"),
|
||||
},
|
||||
{
|
||||
title: "当前建议",
|
||||
key: "mitigation_recommendation",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "mitigation_recommendation"),
|
||||
},
|
||||
]}
|
||||
dataSource={rows}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Empty, Form, Input, Modal, Space, Table, Tag, Typography } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatRiskLevel,
|
||||
readObject,
|
||||
readString,
|
||||
riskColor,
|
||||
type ReportFormValues,
|
||||
} from "./types";
|
||||
|
||||
type ReportModalProps = {
|
||||
open: boolean;
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
rows: FlAnalysisTowerResultSummary[];
|
||||
selectedTowerIds: string[];
|
||||
form: FormInstance<ReportFormValues>;
|
||||
submitting: boolean;
|
||||
onSelectedTowerIdsChange: (towerIds: string[]) => void;
|
||||
onSubmit: (values: ReportFormValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function ReportModal({
|
||||
open,
|
||||
selectedJob,
|
||||
rows,
|
||||
selectedTowerIds,
|
||||
form,
|
||||
submitting,
|
||||
onSelectedTowerIdsChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ReportModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={selectedJob ? `报告生成 - ${selectedJob.job_name || selectedJob.line_name || selectedJob.line_code}` : "报告生成"}
|
||||
open={open}
|
||||
width={1080}
|
||||
confirmLoading={submitting}
|
||||
okText="创建并启动报告任务"
|
||||
onCancel={() => {
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={16} className="flex w-full">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="源端迁移口径:报告任务挂靠在已完成的风险评估或措施推荐结果上,并允许按杆塔缩小纳入报告的范围;若已存在关联的加装避雷器复算任务,报告会自动并入“采取措施后的计算结果表”。"
|
||||
/>
|
||||
<Form<ReportFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="job_name"
|
||||
label="任务名称"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="报告任务名称" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<Empty description="当前任务没有可纳入报告的杆塔结果" />
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text type="secondary">
|
||||
已命中 {rows.length} 座可纳入报告的杆塔。默认全选,可按需缩小报告范围。
|
||||
</Typography.Text>
|
||||
<Table<FlAnalysisTowerResultSummary>
|
||||
rowKey="tower_id"
|
||||
size="small"
|
||||
pagination={{ pageSize: 8, showSizeChanger: false, hideOnSinglePage: false }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedTowerIds,
|
||||
onChange: (keys) => {
|
||||
onSelectedTowerIdsChange(keys.map((item) => String(item)));
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{ title: "杆塔号", dataIndex: "tower_no", width: 120 },
|
||||
{
|
||||
title: "风险等级",
|
||||
dataIndex: "risk_level",
|
||||
width: 120,
|
||||
render: (value: string | null) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "高风险原因",
|
||||
key: "cause_analysis",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "cause_analysis"),
|
||||
},
|
||||
{
|
||||
title: "措施建议",
|
||||
key: "mitigation_recommendation",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "mitigation_recommendation"),
|
||||
},
|
||||
]}
|
||||
dataSource={rows}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Alert, Button, Empty, Spin, Table, Tag } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatRiskLevel,
|
||||
readCurrentRisk,
|
||||
readCurrentScore,
|
||||
readObject,
|
||||
readOptionalNumber,
|
||||
readOptionalString,
|
||||
readSelectedCase,
|
||||
readString,
|
||||
riskColor,
|
||||
} from "./types";
|
||||
|
||||
type ResultTableProps = {
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
selectedWaveformJobType: "normal" | "tongtiao" | null;
|
||||
rows: FlAnalysisTowerResultSummary[];
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
onOpenDetail: (row: FlAnalysisTowerResultSummary) => void;
|
||||
};
|
||||
|
||||
export function ResultTable({
|
||||
selectedJob,
|
||||
selectedWaveformJobType,
|
||||
rows,
|
||||
loading,
|
||||
error,
|
||||
onOpenDetail,
|
||||
}: ResultTableProps) {
|
||||
const columns = useMemo<ColumnsType<FlAnalysisTowerResultSummary>>(() => {
|
||||
const resultColumns: ColumnsType<FlAnalysisTowerResultSummary> = [
|
||||
{
|
||||
title: "序号",
|
||||
dataIndex: "seq_no",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "杆塔号",
|
||||
dataIndex: "tower_no",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "塔型",
|
||||
dataIndex: "tower_type",
|
||||
width: 100,
|
||||
render: (value: string | null) => value || "-",
|
||||
},
|
||||
];
|
||||
|
||||
if (selectedJob?.job_type === "mitigation") {
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "当前风险",
|
||||
key: "current_risk_level",
|
||||
width: 110,
|
||||
render: (_value, row) => <Tag color={riskColor(readCurrentRisk(row))}>{formatRiskLevel(readCurrentRisk(row))}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "预期风险",
|
||||
dataIndex: "risk_level",
|
||||
width: 110,
|
||||
render: (value: string | null) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
resultColumns.push({
|
||||
title: "风险等级",
|
||||
dataIndex: "risk_level",
|
||||
width: 120,
|
||||
render: (value: string | null) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedWaveformJobType === "normal" || selectedWaveformJobType === "tongtiao") {
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "最不利点(μs)",
|
||||
key: "selected_case",
|
||||
width: 160,
|
||||
render: (_value, row) => {
|
||||
const selectedCase = readSelectedCase(row);
|
||||
const head = selectedCase.head_time_us;
|
||||
const tail = selectedCase.tail_time_us;
|
||||
if (typeof head !== "number" || typeof tail !== "number") {
|
||||
return "-";
|
||||
}
|
||||
return `${head}/${tail}`;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedWaveformJobType === "normal") {
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "反击耐雷水平(kA)",
|
||||
key: "counterstrike_withstand_ka",
|
||||
width: 140,
|
||||
render: (_value, row) => readOptionalNumber(readObject(row.result_json), "counterstrike_withstand_ka") ?? "-",
|
||||
},
|
||||
{
|
||||
title: "反击跳闸率",
|
||||
key: "counterstrike_trip_rate",
|
||||
width: 120,
|
||||
render: (_value, row) => readOptionalNumber(readObject(row.result_json), "counterstrike_trip_rate") ?? "-",
|
||||
},
|
||||
{
|
||||
title: "绕击耐雷水平(kA)",
|
||||
key: "shielding_withstand_ka",
|
||||
width: 140,
|
||||
render: (_value, row) => readOptionalNumber(readObject(row.result_json), "shielding_withstand_ka") ?? "-",
|
||||
},
|
||||
{
|
||||
title: "绕击跳闸率",
|
||||
key: "shielding_trip_rate",
|
||||
width: 120,
|
||||
render: (_value, row) => readOptionalNumber(readObject(row.result_json), "shielding_trip_rate") ?? "-",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedWaveformJobType === "tongtiao") {
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "主导相组",
|
||||
key: "dominant_phase_set",
|
||||
width: 120,
|
||||
render: (_value, row) => readOptionalString(readObject(row.result_json), "dominant_phase_set") ?? "-",
|
||||
},
|
||||
{
|
||||
title: "闪络相",
|
||||
key: "flashover_phase",
|
||||
width: 160,
|
||||
render: (_value, row) => readOptionalString(readObject(row.result_json), "flashover_phase") ?? "-",
|
||||
},
|
||||
{
|
||||
title: "同跳跳闸率",
|
||||
key: "counterstrike_trip_rate",
|
||||
width: 120,
|
||||
render: (_value, row) => readOptionalNumber(readObject(row.result_json), "counterstrike_trip_rate") ?? "-",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "综合结论",
|
||||
dataIndex: "summary_text",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "高风险原因",
|
||||
key: "cause_analysis",
|
||||
ellipsis: true,
|
||||
render: (_value, row) => readString(readObject(row.result_json), "cause_analysis"),
|
||||
},
|
||||
{
|
||||
title: "措施建议",
|
||||
key: "mitigation_recommendation",
|
||||
ellipsis: true,
|
||||
render: (_value, row) => readString(readObject(row.result_json), "mitigation_recommendation"),
|
||||
},
|
||||
);
|
||||
|
||||
if (selectedJob?.job_type === "mitigation") {
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "改造结论",
|
||||
key: "recommendation_result",
|
||||
width: 140,
|
||||
render: (_value, row) => readString(readObject(row.result_json), "recommendation_result"),
|
||||
},
|
||||
{
|
||||
title: "当前得分",
|
||||
key: "current_score",
|
||||
width: 100,
|
||||
render: (_value, row) => {
|
||||
const value = readCurrentScore(row);
|
||||
return value === null ? "-" : String(value);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
resultColumns.push(
|
||||
{
|
||||
title: "得分",
|
||||
key: "score",
|
||||
width: 90,
|
||||
render: (_value, row) => {
|
||||
const value = readObject(row.result_json).score;
|
||||
return typeof value === "number" ? value : "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "详情",
|
||||
key: "actions",
|
||||
width: 110,
|
||||
render: (_value, row) => (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onOpenDetail(row);
|
||||
}}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return resultColumns;
|
||||
}, [onOpenDetail, selectedJob?.job_type, selectedWaveformJobType]);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={error instanceof Error ? error.message : "结果表加载失败"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <Empty description="当前任务暂无分级结果" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table<FlAnalysisTowerResultSummary>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
pagination={{ pageSize: 20, showSizeChanger: false, hideOnSinglePage: false }}
|
||||
scroll={{ x: 1400 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Space, Table, Tag, Typography } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
|
||||
import type {
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
import {
|
||||
formatDateTime,
|
||||
formatJobType,
|
||||
formatRiskLevel,
|
||||
readObject,
|
||||
readString,
|
||||
riskColor,
|
||||
type ScenarioFormValues,
|
||||
} from "./types";
|
||||
|
||||
type ScenarioModalProps = {
|
||||
open: boolean;
|
||||
selectedJob: FlAnalysisJobSummary | null;
|
||||
rows: FlAnalysisTowerResultSummary[];
|
||||
baseJobs: FlAnalysisJobSummary[];
|
||||
selectedTowerIds: string[];
|
||||
form: FormInstance<ScenarioFormValues>;
|
||||
submitting: boolean;
|
||||
onSelectedTowerIdsChange: (towerIds: string[]) => void;
|
||||
onSubmit: (values: ScenarioFormValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function ScenarioModal({
|
||||
open,
|
||||
selectedJob,
|
||||
rows,
|
||||
baseJobs,
|
||||
selectedTowerIds,
|
||||
form,
|
||||
submitting,
|
||||
onSelectedTowerIdsChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ScenarioModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={selectedJob ? `加装避雷器复算 - ${selectedJob.job_name || selectedJob.line_name || selectedJob.line_code}` : "加装避雷器复算"}
|
||||
open={open}
|
||||
width={1080}
|
||||
confirmLoading={submitting}
|
||||
okText="创建并启动复算任务"
|
||||
onCancel={() => {
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={16} className="flex w-full">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="源端迁移口径:仅允许从措施推荐结果中继续选择仍为中高风险的杆塔,并复用一次已完成的普通/同跳计算链路,执行“补装避雷器后”的独立复算。"
|
||||
/>
|
||||
<Form<ScenarioFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="job_name"
|
||||
label="任务名称"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="加装避雷器复算任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="base_job_id"
|
||||
label="复用计算任务"
|
||||
rules={[{ required: true, message: "请选择复用的普通计算或同跳计算任务" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择已成功完成的普通计算或同跳计算任务"
|
||||
options={baseJobs.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.job_name || item.line_name || item.line_code || item.id} / ${formatJobType(item.job_type)} / ${formatDateTime(item.finished_at)}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{baseJobs.length === 0 ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="当前线路还没有可复用的普通计算或同跳计算成功任务。请先完成一次基线计算,再创建加装避雷器复算任务。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<Empty description="当前措施任务没有仍需复算的中高风险杆塔" />
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text type="secondary">
|
||||
已命中 {rows.length} 座仍为中高风险的杆塔。默认全选,可按需缩小复算范围。
|
||||
</Typography.Text>
|
||||
<Table<FlAnalysisTowerResultSummary>
|
||||
rowKey="tower_id"
|
||||
size="small"
|
||||
pagination={{ pageSize: 8, showSizeChanger: false, hideOnSinglePage: false }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedTowerIds,
|
||||
onChange: (keys) => {
|
||||
onSelectedTowerIdsChange(keys.map((item) => String(item)));
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{ title: "杆塔号", dataIndex: "tower_no", width: 120 },
|
||||
{
|
||||
title: "预期风险",
|
||||
dataIndex: "risk_level",
|
||||
width: 120,
|
||||
render: (value: string | null) => <Tag color={riskColor(value)}>{formatRiskLevel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "高风险原因",
|
||||
key: "cause_analysis",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "cause_analysis"),
|
||||
},
|
||||
{
|
||||
title: "当前动作",
|
||||
key: "mitigation_recommendation",
|
||||
render: (_value, row) => readString(readObject(row.result_json), "mitigation_recommendation"),
|
||||
},
|
||||
]}
|
||||
dataSource={rows}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import type {
|
||||
FlAnalysisJobDetail,
|
||||
FlAnalysisJobSummary,
|
||||
FlAnalysisTowerResultSummary,
|
||||
} from "@/types/auth";
|
||||
|
||||
export type CreateJobFormValues = {
|
||||
job_name: string;
|
||||
line_id: string;
|
||||
job_type: "normal" | "tongtiao" | "risk";
|
||||
external_adapter: "placeholder" | "wine" | "atp";
|
||||
atp_model_id: string;
|
||||
current_waveform: "heidler" | "double_slope" | "double_exponential";
|
||||
flashover_method: "guideline" | "intersection" | "leader_development";
|
||||
altitude_correction: "none" | "formula1" | "formula2";
|
||||
induced_voltage_formula: "formula1" | "formula2";
|
||||
head_time_min_us: number;
|
||||
head_time_max_us: number;
|
||||
head_time_step_us: number;
|
||||
tail_time_min_us: number;
|
||||
tail_time_max_us: number;
|
||||
tail_time_step_us: number;
|
||||
};
|
||||
|
||||
export type MitigationFormValues = {
|
||||
job_name: string;
|
||||
non_construction: boolean;
|
||||
};
|
||||
|
||||
export type ScenarioFormValues = {
|
||||
job_name: string;
|
||||
base_job_id: string;
|
||||
};
|
||||
|
||||
export type ReportFormValues = {
|
||||
job_name: string;
|
||||
};
|
||||
|
||||
export type ReasonDetail = {
|
||||
code: string;
|
||||
label: string;
|
||||
value?: unknown;
|
||||
standard_value?: unknown;
|
||||
grade?: number | null;
|
||||
triggered?: boolean;
|
||||
};
|
||||
|
||||
export type MitigationAction = {
|
||||
code: string;
|
||||
label: string;
|
||||
summary: string;
|
||||
current_value?: unknown;
|
||||
target_value?: unknown;
|
||||
unit?: string;
|
||||
phases?: string[];
|
||||
};
|
||||
|
||||
export type WorkflowRange = {
|
||||
min?: number | null;
|
||||
max?: number | null;
|
||||
step?: number | null;
|
||||
};
|
||||
|
||||
export type WorkflowSummary = {
|
||||
current_waveform?: string;
|
||||
flashover_method?: string;
|
||||
altitude_correction?: string;
|
||||
induced_voltage_formula?: string;
|
||||
head_time_range_us?: WorkflowRange;
|
||||
tail_time_range_us?: WorkflowRange;
|
||||
scan_point_count?: number;
|
||||
};
|
||||
|
||||
export type ScanPoint = {
|
||||
head_time_us?: number | null;
|
||||
tail_time_us?: number | null;
|
||||
risk_level?: string | null;
|
||||
score?: number | null;
|
||||
counterstrike_withstand_ka?: number | null;
|
||||
counterstrike_trip_rate?: number | null;
|
||||
shielding_withstand_ka?: number | null;
|
||||
shielding_trip_rate?: number | null;
|
||||
flashover_phase?: string | null;
|
||||
dominant_phase_set?: string | null;
|
||||
};
|
||||
|
||||
export type SelectedCase = {
|
||||
head_time_us?: number | null;
|
||||
tail_time_us?: number | null;
|
||||
risk_level?: string | null;
|
||||
score?: number | null;
|
||||
flashover_phase?: string | null;
|
||||
dominant_phase_set?: string | null;
|
||||
};
|
||||
|
||||
export type PhaseResult = {
|
||||
phase: string;
|
||||
circuit?: string | null;
|
||||
shielding_withstand_ka?: number | null;
|
||||
shielding_trip_rate?: number | null;
|
||||
counterstrike_withstand_ka?: number | null;
|
||||
counterstrike_trip_rate?: number | null;
|
||||
};
|
||||
|
||||
export type MultiPhaseResult = {
|
||||
phase_count: number;
|
||||
label: string;
|
||||
flashover_phase?: string | null;
|
||||
counterstrike_withstand_ka?: number | null;
|
||||
trip_rate?: number | null;
|
||||
};
|
||||
|
||||
export const CREATE_JOB_DEFAULTS: CreateJobFormValues = {
|
||||
job_name: "",
|
||||
line_id: "",
|
||||
job_type: "normal",
|
||||
external_adapter: "placeholder",
|
||||
atp_model_id: "",
|
||||
current_waveform: "heidler",
|
||||
flashover_method: "intersection",
|
||||
altitude_correction: "none",
|
||||
induced_voltage_formula: "formula1",
|
||||
head_time_min_us: 2.6,
|
||||
head_time_max_us: 2.6,
|
||||
head_time_step_us: 0.1,
|
||||
tail_time_min_us: 50,
|
||||
tail_time_max_us: 50,
|
||||
tail_time_step_us: 1,
|
||||
};
|
||||
|
||||
export function formatRiskLevel(value: string | null | undefined): string {
|
||||
if (value === "high") return "高风险";
|
||||
if (value === "medium") return "中风险";
|
||||
if (value === "low") return "低风险";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function formatJobType(jobType: string, nonConstruction = false): string {
|
||||
if (jobType === "risk") return "风险评估";
|
||||
if (jobType === "mitigation") return nonConstruction ? "措施推荐(非建线)" : "措施推荐";
|
||||
if (jobType === "normal") return "普通计算";
|
||||
if (jobType === "tongtiao") return "同跳计算";
|
||||
if (jobType === "report") return "报告";
|
||||
if (jobType === "scenario") return "加装避雷器复算";
|
||||
return jobType || "-";
|
||||
}
|
||||
|
||||
export function formatCurrentWaveform(value: string | null | undefined): string {
|
||||
if (value === "heidler") return "Heidler";
|
||||
if (value === "double_slope") return "双斜角";
|
||||
if (value === "double_exponential") return "双指数";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function formatFlashoverMethod(value: string | null | undefined): string {
|
||||
if (value === "guideline") return "规程法";
|
||||
if (value === "intersection") return "相交法";
|
||||
if (value === "leader_development") return "先导发展法";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function formatAltitudeCorrection(value: string | null | undefined): string {
|
||||
if (value === "none") return "无";
|
||||
if (value === "formula1") return "推荐公式1";
|
||||
if (value === "formula2") return "推荐公式2";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function formatInducedVoltageFormula(value: string | null | undefined): string {
|
||||
if (value === "formula1") return "公式1";
|
||||
if (value === "formula2") return "公式2";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function formatExternalAdapter(value: string | null | undefined): string {
|
||||
if (value === "wine") return "Wine / ATP";
|
||||
if (value === "atp") return "原生 ATP";
|
||||
if (value === "placeholder") return "规则近似";
|
||||
if (value === "custom") return "自定义";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
export function riskColor(value: string | null | undefined): string {
|
||||
if (value === "high") return "red";
|
||||
if (value === "medium") return "orange";
|
||||
if (value === "low") return "green";
|
||||
return "default";
|
||||
}
|
||||
|
||||
export function statusColor(value: string | null | undefined): string {
|
||||
if (value === "success") return "green";
|
||||
if (value === "failed" || value === "blocked") return "red";
|
||||
if (value === "running") return "blue";
|
||||
if (value === "queued") return "cyan";
|
||||
return "default";
|
||||
}
|
||||
|
||||
export 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 });
|
||||
}
|
||||
|
||||
export function stringifyJson(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value ?? {}, null, 2);
|
||||
} catch {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
export function readObject(value: unknown): Record<string, unknown> {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function readString(record: Record<string, unknown>, key: string): string {
|
||||
const value = record[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
export function readOptionalString(record: Record<string, unknown>, key: string): string | null {
|
||||
const value = record[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function readOptionalNumber(record: Record<string, unknown>, key: string): number | null {
|
||||
const value = record[key];
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
|
||||
export function preparationColor(ready: boolean): string {
|
||||
return ready ? "green" : "red";
|
||||
}
|
||||
|
||||
export function readDownloadFilename(headerValue: string | null, fallback: string): string {
|
||||
if (!headerValue) {
|
||||
return fallback;
|
||||
}
|
||||
const utf8Matched = /filename\*=UTF-8''([^;]+)/i.exec(headerValue);
|
||||
if (utf8Matched?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Matched[1]);
|
||||
} catch {
|
||||
return utf8Matched[1];
|
||||
}
|
||||
}
|
||||
const matched = /filename="?([^"]+)"?/i.exec(headerValue);
|
||||
return matched?.[1] ?? fallback;
|
||||
}
|
||||
|
||||
export function readArray<T>(value: unknown): T[] {
|
||||
return Array.isArray(value) ? (value as T[]) : [];
|
||||
}
|
||||
|
||||
export function readReasonDetails(row: FlAnalysisTowerResultSummary | null): ReasonDetail[] {
|
||||
const value = row ? readObject(row.result_json).reason_details : null;
|
||||
return readArray<ReasonDetail>(value);
|
||||
}
|
||||
|
||||
export function readMitigationActions(row: FlAnalysisTowerResultSummary | null): MitigationAction[] {
|
||||
const value = row ? readObject(row.result_json).mitigation_actions : null;
|
||||
return readArray<MitigationAction>(value);
|
||||
}
|
||||
|
||||
export function readWorkflow(row: FlAnalysisTowerResultSummary | null): WorkflowSummary {
|
||||
return readObject(row ? readObject(row.result_json).workflow : null) as WorkflowSummary;
|
||||
}
|
||||
|
||||
export function readSelectedCase(row: FlAnalysisTowerResultSummary | null): SelectedCase {
|
||||
return readObject(row ? readObject(row.result_json).selected_case : null) as SelectedCase;
|
||||
}
|
||||
|
||||
export function readScanPoints(row: FlAnalysisTowerResultSummary | null): ScanPoint[] {
|
||||
return readArray<ScanPoint>(row ? readObject(row.result_json).scan_points : null);
|
||||
}
|
||||
|
||||
export function readPhaseResults(row: FlAnalysisTowerResultSummary | null): PhaseResult[] {
|
||||
return readArray<PhaseResult>(row ? readObject(row.result_json).phase_results : null);
|
||||
}
|
||||
|
||||
export function readMultiPhaseResults(row: FlAnalysisTowerResultSummary | null): MultiPhaseResult[] {
|
||||
return readArray<MultiPhaseResult>(row ? readObject(row.result_json).multi_phase_results : null);
|
||||
}
|
||||
|
||||
export function readCurrentRisk(row: FlAnalysisTowerResultSummary): string | null {
|
||||
const value = readObject(row.result_json).current_risk_level;
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
|
||||
export function readCurrentScore(row: FlAnalysisTowerResultSummary): number | null {
|
||||
const value = readObject(row.result_json).current_score;
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
|
||||
export function mitigationMode(job: FlAnalysisJobDetail | FlAnalysisJobSummary | null): boolean {
|
||||
if (!job) {
|
||||
return false;
|
||||
}
|
||||
const options = readObject(job.execution_options_json);
|
||||
return Boolean(options.non_construction);
|
||||
}
|
||||
|
||||
export function waveformJobType(job: FlAnalysisJobDetail | FlAnalysisJobSummary | null): "normal" | "tongtiao" | null {
|
||||
if (!job) {
|
||||
return null;
|
||||
}
|
||||
if (job.job_type === "normal" || job.job_type === "tongtiao") {
|
||||
return job.job_type;
|
||||
}
|
||||
if (job.job_type === "scenario") {
|
||||
const baseJobType = readOptionalString(readObject(job.execution_options_json), "base_job_type");
|
||||
return baseJobType === "tongtiao" ? "tongtiao" : "normal";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function selectedTowerCount(job: FlAnalysisJobDetail | null): number {
|
||||
if (!job) {
|
||||
return 0;
|
||||
}
|
||||
const value = readObject(job.execution_options_json).selected_tower_ids;
|
||||
return Array.isArray(value) ? value.length : 0;
|
||||
}
|
||||
|
||||
export function formatRangeSummary(range: WorkflowRange | undefined): string {
|
||||
if (!range) {
|
||||
return "-";
|
||||
}
|
||||
const min = typeof range.min === "number" ? range.min : null;
|
||||
const max = typeof range.max === "number" ? range.max : null;
|
||||
const step = typeof range.step === "number" ? range.step : null;
|
||||
if (min === null || max === null || step === null) {
|
||||
return "-";
|
||||
}
|
||||
return `${min} ~ ${max} / 步长 ${step}`;
|
||||
}
|
||||
Reference in New Issue
Block a user