From 0f62776cbdb6ecaef93a74b5961990266e8a58ee Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Sat, 20 Jun 2026 15:23:20 +0800 Subject: [PATCH] =?UTF-8?q?[feat]:[FL-214][fl-analysis=20=E9=98=B2?= =?UTF-8?q?=E9=9B=B7=E8=AE=A1=E7=AE=97=E9=A1=B5=E9=9D=A2UI=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E9=87=8D=E6=9E=84]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- .gitignore | 1 + memory/2026-06-20.md | 20 + web/src/app/admin/fl-analysis/page.tsx | 1730 ++--------------- .../fl-analysis/create-job-form.tsx | 261 +++ .../components/fl-analysis/detail-modal.tsx | 351 ++++ .../components/fl-analysis/job-card-list.tsx | 90 + .../fl-analysis/job-detail-panel.tsx | 248 +++ .../fl-analysis/mitigation-modal.tsx | 127 ++ .../components/fl-analysis/report-modal.tsx | 123 ++ .../components/fl-analysis/result-table.tsx | 253 +++ .../components/fl-analysis/scenario-modal.tsx | 148 ++ web/src/components/fl-analysis/types.ts | 356 ++++ 12 files changed, 2174 insertions(+), 1534 deletions(-) create mode 100644 web/src/components/fl-analysis/create-job-form.tsx create mode 100644 web/src/components/fl-analysis/detail-modal.tsx create mode 100644 web/src/components/fl-analysis/job-card-list.tsx create mode 100644 web/src/components/fl-analysis/job-detail-panel.tsx create mode 100644 web/src/components/fl-analysis/mitigation-modal.tsx create mode 100644 web/src/components/fl-analysis/report-modal.tsx create mode 100644 web/src/components/fl-analysis/result-table.tsx create mode 100644 web/src/components/fl-analysis/scenario-modal.tsx create mode 100644 web/src/components/fl-analysis/types.ts diff --git a/.gitignore b/.gitignore index 6a81cb1..ab42b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* .turbo/ +graphify-out/ # Python .venv/ diff --git a/memory/2026-06-20.md b/memory/2026-06-20.md index d5b0dee..1ec3196 100644 --- a/memory/2026-06-20.md +++ b/memory/2026-06-20.md @@ -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` 前端视图组织与交互入口,不改变后端接口、请求/响应字段、权限判断或任务创建/启动语义。 diff --git a/web/src/app/admin/fl-analysis/page.tsx b/web/src/app/admin/fl-analysis/page.tsx index c1e867b..63ab150 100644 --- a/web/src/app/admin/fl-analysis/page.tsx +++ b/web/src/app/admin/fl-analysis/page.tsx @@ -5,395 +5,51 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Alert, Button, - Checkbox, - Descriptions, - Empty, + Drawer, Form, - Input, - InputNumber, - Modal, - Select, Space, - Spin, - Table, - Tag, Typography, message, } from "antd"; -import type { ColumnsType } from "antd/es/table"; import { useAuth } from "@/components/auth-provider"; import { Card } from "@/components/ui-antd"; +import { CreateJobForm } from "@/components/fl-analysis/create-job-form"; +import { DetailModal } from "@/components/fl-analysis/detail-modal"; +import { JobDetailPanel } from "@/components/fl-analysis/job-detail-panel"; +import { JobListCard } from "@/components/fl-analysis/job-card-list"; +import { MitigationModal } from "@/components/fl-analysis/mitigation-modal"; +import { ReportModal } from "@/components/fl-analysis/report-modal"; +import { ResultTable } from "@/components/fl-analysis/result-table"; +import { ScenarioModal } from "@/components/fl-analysis/scenario-modal"; +import { + CREATE_JOB_DEFAULTS, + formatExternalAdapter, + formatJobType, + mitigationMode, + readDownloadFilename, + waveformJobType, + type CreateJobFormValues, + type MitigationFormValues, + type ReportFormValues, + type ScenarioFormValues, +} from "@/components/fl-analysis/types"; import { readApiError } from "@/lib/api"; -import { readLinePreparation } from "@/lib/line-preparation"; import type { AtpEngineStatusResponse, AtpModelListResponse, - AtpModelSummary, FlAnalysisJobDetail, FlAnalysisJobListResponse, - FlAnalysisJobSummary, FlAnalysisTowerResultListResponse, FlAnalysisTowerResultSummary, LineListResponse, - LineSummary, } from "@/types/auth"; -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; -}; - -type MitigationFormValues = { - job_name: string; - non_construction: boolean; -}; - -type ScenarioFormValues = { - job_name: string; - base_job_id: string; -}; - -type ReportFormValues = { - job_name: string; -}; - -type ReasonDetail = { - code: string; - label: string; - value?: unknown; - standard_value?: unknown; - grade?: number | null; - triggered?: boolean; -}; - -type MitigationAction = { - code: string; - label: string; - summary: string; - current_value?: unknown; - target_value?: unknown; - unit?: string; - phases?: string[]; -}; - -type WorkflowRange = { - min?: number | null; - max?: number | null; - step?: number | null; -}; - -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; -}; - -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; -}; - -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; -}; - -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; -}; - -type MultiPhaseResult = { - phase_count: number; - label: string; - flashover_phase?: string | null; - counterstrike_withstand_ka?: number | null; - trip_rate?: number | null; -}; - -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, -}; - -function formatRiskLevel(value: string | null | undefined): string { - if (value === "high") return "高风险"; - if (value === "medium") return "中风险"; - if (value === "low") return "低风险"; - return value || "-"; -} - -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 || "-"; -} - -function formatCurrentWaveform(value: string | null | undefined): string { - if (value === "heidler") return "Heidler"; - if (value === "double_slope") return "双斜角"; - if (value === "double_exponential") return "双指数"; - return value || "-"; -} - -function formatFlashoverMethod(value: string | null | undefined): string { - if (value === "guideline") return "规程法"; - if (value === "intersection") return "相交法"; - if (value === "leader_development") return "先导发展法"; - return value || "-"; -} - -function formatAltitudeCorrection(value: string | null | undefined): string { - if (value === "none") return "无"; - if (value === "formula1") return "推荐公式1"; - if (value === "formula2") return "推荐公式2"; - return value || "-"; -} - -function formatInducedVoltageFormula(value: string | null | undefined): string { - if (value === "formula1") return "公式1"; - if (value === "formula2") return "公式2"; - return value || "-"; -} - -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 || "-"; -} - -function riskColor(value: string | null | undefined): string { - if (value === "high") return "red"; - if (value === "medium") return "orange"; - if (value === "low") return "green"; - return "default"; -} - -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"; -} - -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 stringifyJson(value: unknown): string { - try { - return JSON.stringify(value ?? {}, null, 2); - } catch { - return "{}"; - } -} - -function readObject(value: unknown): Record { - if (value && typeof value === "object" && !Array.isArray(value)) { - return value as Record; - } - return {}; -} - -function readString(record: Record, key: string): string { - const value = record[key]; - if (typeof value === "string") { - return value; - } - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - return "-"; -} - -function readOptionalString(record: Record, 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; -} - -function readOptionalNumber(record: Record, key: string): number | null { - const value = record[key]; - return typeof value === "number" ? value : null; -} - -function preparationColor(ready: boolean): string { - return ready ? "green" : "red"; -} - -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; -} - -function readArray(value: unknown): T[] { - return Array.isArray(value) ? (value as T[]) : []; -} - -function readReasonDetails(row: FlAnalysisTowerResultSummary | null): ReasonDetail[] { - const value = row ? readObject(row.result_json).reason_details : null; - return readArray(value); -} - -function readMitigationActions(row: FlAnalysisTowerResultSummary | null): MitigationAction[] { - const value = row ? readObject(row.result_json).mitigation_actions : null; - return readArray(value); -} - -function readWorkflow(row: FlAnalysisTowerResultSummary | null): WorkflowSummary { - return readObject(row ? readObject(row.result_json).workflow : null) as WorkflowSummary; -} - -function readSelectedCase(row: FlAnalysisTowerResultSummary | null): SelectedCase { - return readObject(row ? readObject(row.result_json).selected_case : null) as SelectedCase; -} - -function readScanPoints(row: FlAnalysisTowerResultSummary | null): ScanPoint[] { - return readArray(row ? readObject(row.result_json).scan_points : null); -} - -function readPhaseResults(row: FlAnalysisTowerResultSummary | null): PhaseResult[] { - return readArray(row ? readObject(row.result_json).phase_results : null); -} - -function readMultiPhaseResults(row: FlAnalysisTowerResultSummary | null): MultiPhaseResult[] { - return readArray(row ? readObject(row.result_json).multi_phase_results : null); -} - -function readCurrentRisk(row: FlAnalysisTowerResultSummary): string | null { - const value = readObject(row.result_json).current_risk_level; - return typeof value === "string" ? value : null; -} - -function readCurrentScore(row: FlAnalysisTowerResultSummary): number | null { - const value = readObject(row.result_json).current_score; - return typeof value === "number" ? value : null; -} - -function mitigationMode(job: FlAnalysisJobDetail | FlAnalysisJobSummary | null): boolean { - if (!job) { - return false; - } - const options = readObject(job.execution_options_json); - return Boolean(options.non_construction); -} - -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; -} - -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; -} - -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}`; -} - export default function AdminFlAnalysisPage() { const { user, initializing, fetchWithAuth, hasPermission } = useAuth(); const queryClient = useQueryClient(); const [selectedJobId, setSelectedJobId] = useState(null); + const [createDrawerOpen, setCreateDrawerOpen] = useState(false); const [detailRow, setDetailRow] = useState(null); const [mitigationModalOpen, setMitigationModalOpen] = useState(false); const [scenarioModalOpen, setScenarioModalOpen] = useState(false); @@ -410,6 +66,7 @@ export default function AdminFlAnalysisPage() { const selectedLineId = Form.useWatch("line_id", createJobForm); const selectedCreateJobType = Form.useWatch("job_type", createJobForm) ?? CREATE_JOB_DEFAULTS.job_type; const selectedExternalAdapter = Form.useWatch("external_adapter", createJobForm) ?? CREATE_JOB_DEFAULTS.external_adapter; + const selectedAtpModelId = Form.useWatch("atp_model_id", createJobForm) ?? ""; const canRead = hasPermission("line.read") || hasPermission("line.manage"); const canManage = hasPermission("line.manage") || hasPermission("tower.manage"); @@ -464,12 +121,14 @@ export default function AdminFlAnalysisPage() { }, }); + const jobs = useMemo(() => jobsQuery.data?.items ?? [], [jobsQuery.data?.items]); const selectedJob = useMemo(() => { if (!selectedJobId) { - return jobsQuery.data?.items[0] ?? null; + return jobs[0] ?? null; } - return jobsQuery.data?.items.find((item) => item.id === selectedJobId) ?? null; - }, [jobsQuery.data?.items, selectedJobId]); + return jobs.find((item) => item.id === selectedJobId) ?? null; + }, [jobs, selectedJobId]); + const selectedJobCardId = selectedJob?.id ?? null; const selectedWaveformJobType = waveformJobType(selectedJob); const selectedJobDetailQuery = useQuery({ @@ -496,42 +155,45 @@ export default function AdminFlAnalysisPage() { }, }); + const towerRows = useMemo(() => towerResultsQuery.data?.items ?? [], [towerResultsQuery.data?.items]); const candidateMitigationRows = useMemo( - () => (towerResultsQuery.data?.items ?? []).filter((item) => item.risk_level !== "low"), - [towerResultsQuery.data?.items], + () => towerRows.filter((item) => item.risk_level !== "low"), + [towerRows], ); const candidateScenarioRows = useMemo(() => { if (selectedJob?.job_type !== "mitigation") { return []; } - return (towerResultsQuery.data?.items ?? []).filter((item) => item.risk_level !== "low"); - }, [selectedJob?.job_type, towerResultsQuery.data?.items]); + return towerRows.filter((item) => item.risk_level !== "low"); + }, [selectedJob?.job_type, towerRows]); const candidateReportRows = useMemo(() => { - const rows = towerResultsQuery.data?.items ?? []; if (selectedJob?.job_type === "mitigation") { - return rows; + return towerRows; } - return rows.filter((item) => item.risk_level !== "low"); - }, [selectedJob?.job_type, towerResultsQuery.data?.items]); + return towerRows.filter((item) => item.risk_level !== "low"); + }, [selectedJob?.job_type, towerRows]); const candidateScenarioBaseJobs = useMemo(() => { if (!selectedJob) { return []; } - return (jobsQuery.data?.items ?? []).filter( + return jobs.filter( (item) => item.line_id === selectedJob.line_id && item.status === "success" && ["normal", "tongtiao"].includes(item.job_type), ); - }, [jobsQuery.data?.items, selectedJob]); + }, [jobs, selectedJob]); const atpModels = useMemo(() => atpModelsQuery.data?.items ?? [], [atpModelsQuery.data]); - const selectedAtpModelId = Form.useWatch("atp_model_id", createJobForm) ?? ""; const selectedAtpModel = useMemo( () => atpModels.find((item) => item.id === selectedAtpModelId) ?? null, [atpModels, selectedAtpModelId], ); + const selectedLine = useMemo(() => { + return linesQuery.data?.items.find((item) => item.id === selectedLineId) ?? null; + }, [linesQuery.data?.items, selectedLineId]); + useEffect(() => { const firstLine = linesQuery.data?.items[0]; if (firstLine && !createJobForm.getFieldValue("line_id")) { @@ -656,6 +318,7 @@ export default function AdminFlAnalysisPage() { onSuccess: async (job) => { await invalidateFlAnalysisQueries(); setSelectedJobId(job.id); + setCreateDrawerOpen(false); messageApi.success(`${formatJobType(job.job_type, mitigationMode(job))}任务已创建并启动`); createJobForm.setFieldsValue({ job_name: "" }); }, @@ -848,316 +511,14 @@ export default function AdminFlAnalysisPage() { }, }); - const resultColumns = useMemo>(() => { - const columns: ColumnsType = [ - { - 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") { - columns.push( - { - title: "当前风险", - key: "current_risk_level", - width: 110, - render: (_value, row) => {formatRiskLevel(readCurrentRisk(row))}, - }, - { - title: "预期风险", - dataIndex: "risk_level", - width: 110, - render: (value: string | null) => {formatRiskLevel(value)}, - }, - ); - } else { - columns.push({ - title: "风险等级", - dataIndex: "risk_level", - width: 120, - render: (value: string | null) => {formatRiskLevel(value)}, - }); - } - - if (selectedWaveformJobType === "normal" || selectedWaveformJobType === "tongtiao") { - columns.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") { - columns.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") { - columns.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") ?? "-", - }, - ); - } - - columns.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") { - columns.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); - }, - }, - ); - } - - columns.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) => ( - - ), - }, - ); - - return columns; - }, [selectedJob?.job_type, selectedWaveformJobType]); - - const reasonDetailColumns: ColumnsType = [ - { 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 ? : ), - }, - ]; - - const mitigationActionColumns: ColumnsType = [ - { 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 = [ - { 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) => {formatRiskLevel(value)}, - }, - { 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 = [ - { 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 = [ - { 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 ?? "-", - }, - ]; - - const selectedLine = useMemo(() => { - return linesQuery.data?.items.find((item) => item.id === selectedLineId) ?? null; - }, [linesQuery.data?.items, selectedLineId]); - const selectedLinePreparation = useMemo(() => readLinePreparation(selectedLine), [selectedLine]); - const externalAdapterActive = selectedExternalAdapter === "atp" || selectedExternalAdapter === "wine"; + const selectedJobDetail = selectedJobDetailQuery.data ?? null; + const selectedJobDetailWaveformType = waveformJobType(selectedJobDetail ?? selectedJob); + const canCreateMitigation = selectedJob?.job_type === "risk"; + const canCreateScenario = selectedJob?.job_type === "mitigation"; + const canCreateReport = selectedJob?.job_type === "risk" || selectedJob?.job_type === "mitigation"; + const canDownloadResults = selectedJob?.job_type !== "report" + && selectedJob?.status === "success" + && towerRows.length > 0; const engineMode = engineQuery.data?.mode; const adapterOptions = [ { @@ -1176,10 +537,11 @@ export default function AdminFlAnalysisPage() { disabled: !canReadAtp || engineMode === "native" || engineQuery.data?.available === false, }, ] as const; + const externalAdapterActive = selectedExternalAdapter === "atp" || selectedExternalAdapter === "wine"; const workflowExecutionMessage = externalAdapterActive ? engineQuery.data?.available ? `当前将通过 ${formatExternalAdapter(selectedExternalAdapter)} 链路执行 ATP 模型,并把外部结果回填到任务明细。` - : `当前已选择 ${formatExternalAdapter(selectedExternalAdapter)},但 ATP 引擎不可用:${engineQuery.data?.error || "请先检查执行器配置" }` + : `当前已选择 ${formatExternalAdapter(selectedExternalAdapter)},但 ATP 引擎不可用:${engineQuery.data?.error || "请先检查执行器配置"}` : `当前按 ${formatJobType(selectedCreateJobType)} 口径生成规则近似版结果;切换到 ATP/Wine 适配器后会走真实外部执行链路。`; if (!initializing && !user) { @@ -1190,895 +552,195 @@ export default function AdminFlAnalysisPage() { return ; } - const selectedJobDetail = selectedJobDetailQuery.data ?? null; - 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 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 detailExternalExecution = readObject(detailResultObject.external_execution); - const sourceJobId = readOptionalString(selectedJobExecutionOptions, "source_job_id"); - const scenarioBaseJobName = readOptionalString(selectedJobExecutionOptions, "base_job_name"); - const scenarioBaseJobType = readOptionalString(selectedJobExecutionOptions, "base_job_type"); - const canCreateMitigation = selectedJob?.job_type === "risk"; - const canCreateScenario = selectedJob?.job_type === "mitigation"; - const canCreateReport = selectedJob?.job_type === "risk" || selectedJob?.job_type === "mitigation"; - 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"); - const canDownloadResults = selectedJob?.job_type !== "report" - && selectedJob?.status === "success" - && (towerResultsQuery.data?.items.length ?? 0) > 0; - return ( <> {contextHolder} - +
防雷分析与改造 - 支持源端“普通计算 / 同跳计算 / 风险评估 / 措施推荐 / 加装避雷器复算 / 报告生成”工作流。普通计算和同跳计算可按 ATP/Wine 外部链路执行,也可退回规则近似版;报告任务会自动并入已关联的复算结果表。 + 支持普通计算、同跳计算、风险评估、措施推荐、加装避雷器复算与报告生成工作流。
- - {jobsQuery.error ? ( - - ) : null} - {canManage ? ( - - form={createJobForm} - layout="vertical" - initialValues={CREATE_JOB_DEFAULTS} - onFinish={(values) => { - createJobMutation.mutate(values); + - -
- - {selectedLine ? ( - - {[ - 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 ( - - {`${item.label}${suffix} ${item.tower_ready_count}/${item.tower_total_count}${preparedAt ? ` @ ${formatDateTime(preparedAt)}` : ""}`} - - ); - })} -
- } - /> - ) : null} - - {selectedCreateJobType === "normal" || selectedCreateJobType === "tongtiao" ? ( - <> - - {atpModelsQuery.error && externalAdapterActive ? ( - - ) : null} - {externalAdapterActive ? ( -
- - - - - - - - ({ - value: item.id, - label: `${item.line_name || item.line_code || item.id} / ${formatJobType(item.job_type, Boolean(readObject(item.execution_options_json).non_construction))} / ${item.status}`, - }))} - onChange={(value) => { - setSelectedJobId(value); - }} - allowClear - showSearch - optionFilterProp="label" - style={{ minWidth: 420, maxWidth: 760 }} - /> - +
- {!selectedJob ? ( - - {jobsQuery.isLoading ? : } - - ) : ( + {jobsQuery.error ? ( + + ) : null} + + + + {selectedJob ? ( <> - - {selectedJobDetailQuery.isLoading ? ( - - ) : selectedJobDetailQuery.error ? ( - - ) : selectedJobDetail ? ( - <> - - {selectedJobDetail.job_name || "-"} - - {selectedJobDetail.status} - - {selectedJobDetail.line_name || selectedJobDetail.line_code || "-"} - {formatJobType(selectedJobDetail.job_type, mitigationMode(selectedJobDetail))} - {selectedJobDetail.result_tower_count} - {formatDateTime(selectedJobDetail.finished_at)} - - {stringifyJson(selectedJobDetail.result_summary_json?.risk_counts ?? {})} - - {selectedJobDetail.job_type === "mitigation" ? ( - <> - {sourceJobId || "-"} - {String(selectedTowerCount(selectedJobDetail) || "-")} - {mitigationMode(selectedJobDetail) ? "非建线" : "常规建线"} - - {String(readObject(selectedJobDetail.result_summary_json).arrester_required_count ?? "-")} - - - ) : selectedJobDetail.job_type === "scenario" ? ( - <> - {sourceJobId || "-"} - {String(selectedTowerCount(selectedJobDetail) || "-")} - - {scenarioBaseJobType ? formatJobType(scenarioBaseJobType) : "-"} - - {scenarioBaseJobName || "-"} - - ) : selectedJobDetail.job_type === "report" ? ( - <> - - {reportSourceJobType ? formatJobType(reportSourceJobType) : "-"} - {reportSourceJobName ? ` / ${reportSourceJobName}` : ""} - - - {String(readObject(selectedJobDetail.result_summary_json).selected_tower_count ?? "-")} - - {reportMitigationJobName || "未关联"} - {reportScenarioJobName || "未关联"} - {reportDocumentName || "-"} - - ) : selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? ( - <> - {formatExternalAdapter(selectedJobDetail.external_adapter)} - - {selectedJobExternalModelName || selectedJobExternalModelCode || "-"} - {selectedJobExternalModelName && selectedJobExternalModelCode ? ` / ${selectedJobExternalModelCode}` : ""} - - - {typeof selectedJobExternalVersionNo === "number" ? `v${selectedJobExternalVersionNo}` : "-"} - - {String(selectedJobDetail.result_summary_json?.score_average ?? "-")} - {String(readObject(selectedJobDetail.result_summary_json).scan_point_average ?? "-")} - {formatCurrentWaveform(selectedJobWorkflow?.current_waveform)} - {formatFlashoverMethod(selectedJobWorkflow?.flashover_method)} - {formatAltitudeCorrection(selectedJobWorkflow?.altitude_correction)} - {formatInducedVoltageFormula(selectedJobWorkflow?.induced_voltage_formula)} - {formatRangeSummary(selectedJobWorkflow?.head_time_range_us)} - {formatRangeSummary(selectedJobWorkflow?.tail_time_range_us)} - - ) : ( - <> - {String(selectedJobDetail.result_summary_json?.score_average ?? "-")} - {String(selectedJobDetail.external_adapter || "-")} - - )} - - - {canManage || selectedJob.job_type === "report" ? ( - - {canManage ? ( - - ) : null} - {canManage && canCreateMitigation ? ( - - ) : null} - {canManage && canCreateScenario ? ( - - ) : null} - {canManage && canCreateReport ? ( - - ) : null} - {selectedJob.job_type !== "report" ? ( - - ) : null} - {selectedJob.job_type === "report" ? ( - - ) : null} - - ) : null} - - ) : ( - - )} - + { + if (selectedJob) { + startJobMutation.mutate(selectedJob.id); + } + }} + onOpenMitigation={openMitigationJobModal} + onOpenScenario={openScenarioJobModal} + onOpenReport={openReportJobModal} + onDownloadResults={() => { + downloadResultsMutation.mutate({ + jobId: selectedJob.id, + runId: selectedJobDetail?.latest_run_id ?? selectedJob.latest_run_id, + jobType: selectedJob.job_type, + }); + }} + onDownloadReport={() => { + downloadReportMutation.mutate(selectedJob.id); + }} + /> - {towerResultsQuery.isLoading ? ( - - ) : towerResultsQuery.error ? ( - - ) : (towerResultsQuery.data?.items.length ?? 0) === 0 ? ( - - ) : ( - - rowKey="id" - columns={resultColumns} - dataSource={towerResultsQuery.data?.items ?? []} - pagination={{ pageSize: 20, showSizeChanger: false, hideOnSinglePage: false }} - scroll={{ x: 1400 }} - /> - )} + { + setDetailRow(row); + setDetailModalOpen(true); + }} + /> - )} + ) : null}
- { + if (createJobMutation.isPending) { + return; + } + setCreateDrawerOpen(false); + }} + destroyOnHidden={false} + > + ({ ...item }))} + atpModels={atpModels} + atpModelsLoading={atpModelsQuery.isLoading} + atpModelsError={atpModelsQuery.error} + selectedAtpModel={selectedAtpModel} + engineQueryData={engineQuery.data} + workflowExecutionMessage={workflowExecutionMessage} + submitting={createJobMutation.isPending} + onSubmit={(values) => { + createJobMutation.mutate(values); + }} + /> + + + { + detailRow={detailRow} + selectedJob={selectedJob} + selectedJobDetail={selectedJobDetail} + onClose={() => { setDetailModalOpen(false); setDetailRow(null); }} - footer={null} - width={980} - > - {!detailRow ? ( - - ) : ( - - - {detailRow.summary_text || "-"} - - {formatRiskLevel(detailRow.risk_level)} - - {selectedJob?.job_type === "mitigation" ? ( - <> - - {formatRiskLevel(readCurrentRisk(detailRow))} - - {readString(detailResultObject, "recommendation_result")} - - ) : selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? ( - <> - - {typeof detailSelectedCase.head_time_us === "number" && typeof detailSelectedCase.tail_time_us === "number" - ? `${detailSelectedCase.head_time_us}/${detailSelectedCase.tail_time_us}` - : "-"} - - {String(detailWorkflow.scan_point_count ?? "-")} - {formatCurrentWaveform(detailWorkflow.current_waveform)} - {formatFlashoverMethod(detailWorkflow.flashover_method)} - {formatAltitudeCorrection(detailWorkflow.altitude_correction)} - {formatInducedVoltageFormula(detailWorkflow.induced_voltage_formula)} - {formatRangeSummary(detailWorkflow.head_time_range_us)} - {formatRangeSummary(detailWorkflow.tail_time_range_us)} - - {formatExternalAdapter(readOptionalString(detailExternalExecution, "adapter"))} - - - {readOptionalString(detailExternalExecution, "model_name") || readOptionalString(detailExternalExecution, "model_code") || "-"} - - - {typeof readOptionalNumber(detailExternalExecution, "version_no") === "number" - ? `v${readOptionalNumber(detailExternalExecution, "version_no")}` - : "-"} - - {selectedJobDetailWaveformType === "tongtiao" ? ( - <> - {readOptionalString(detailResultObject, "dominant_phase_set") ?? "-"} - {readOptionalString(detailResultObject, "flashover_phase") ?? "-"} - - ) : null} - - ) : null} - - {readString(detailResultObject, "cause_analysis")} - - - {readString(detailResultObject, "mitigation_recommendation")} - - - {selectedJob?.job_type === "mitigation" - ? String(readCurrentScore(detailRow) ?? "-") - : String(readOptionalNumber(detailResultObject, "score") ?? "-")} - - - {selectedJob?.job_type === "mitigation" - ? String(readOptionalNumber(detailResultObject, "expected_score") ?? "-") - : String(readOptionalNumber(detailResultObject, "score") ?? "-")} - - + /> - - 原因细项 - - {(reasonDetails.length ?? 0) === 0 ? ( - - ) : ( - - rowKey="code" - columns={reasonDetailColumns} - dataSource={reasonDetails} - pagination={false} - size="small" - /> - )} - - {selectedJobDetailWaveformType === "normal" || selectedJobDetailWaveformType === "tongtiao" ? ( - <> - - 波形扫描 - - {scanPoints.length === 0 ? ( - - ) : ( - - rowKey={(row) => `${row.head_time_us ?? "-"}-${row.tail_time_us ?? "-"}`} - columns={scanPointColumns} - dataSource={scanPoints} - pagination={false} - size="small" - scroll={{ x: 1000 }} - /> - )} - - ) : null} - - {selectedJobDetailWaveformType === "tongtiao" ? ( - <> - - 相别结果 - - {phaseResults.length === 0 ? ( - - ) : ( - - rowKey="phase" - columns={phaseResultColumns} - dataSource={phaseResults} - pagination={false} - size="small" - scroll={{ x: 1200 }} - /> - )} - - - 多相结果 - - {multiPhaseResults.length === 0 ? ( - - ) : ( - - rowKey="label" - columns={multiPhaseColumns} - dataSource={multiPhaseResults} - pagination={false} - size="small" - scroll={{ x: 900 }} - /> - )} - - ) : null} - - - 改造动作 - - {mitigationActions.length === 0 ? ( - - ) : ( - - rowKey="code" - columns={mitigationActionColumns} - dataSource={mitigationActions} - pagination={false} - size="small" - /> - )} - - )} - - - { + createMitigationMutation.mutate(values); + }} onCancel={() => { - if (createMitigationMutation.isPending) { - return; - } setMitigationModalOpen(false); }} - onOk={() => { - mitigationForm.submit(); - }} - > - - - - form={mitigationForm} - layout="vertical" - onFinish={(values) => { - createMitigationMutation.mutate(values); - }} - initialValues={{ non_construction: false }} - > - - - - - 按“非建线”模式生成措施推荐 - - + /> - {(candidateMitigationRows.length ?? 0) === 0 ? ( - - ) : ( - <> - - 已命中 {candidateMitigationRows.length} 座中高风险杆塔。默认全选,可按需缩小推荐范围。 - - - rowKey="tower_id" - size="small" - pagination={{ pageSize: 8, showSizeChanger: false, hideOnSinglePage: false }} - rowSelection={{ - selectedRowKeys: selectedMitigationTowerIds, - onChange: (keys) => { - setSelectedMitigationTowerIds(keys.map((item) => String(item))); - }, - }} - columns={[ - { title: "杆塔号", dataIndex: "tower_no", width: 120 }, - { - title: "风险等级", - dataIndex: "risk_level", - width: 120, - render: (value: string | null) => {formatRiskLevel(value)}, - }, - { - 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={candidateMitigationRows} - scroll={{ x: 1000 }} - /> - - )} - - - - { + createScenarioMutation.mutate(values); + }} onCancel={() => { - if (createScenarioMutation.isPending) { - return; - } setScenarioModalOpen(false); }} - onOk={() => { - scenarioForm.submit(); - }} - > - - - - form={scenarioForm} - layout="vertical" - onFinish={(values) => { - createScenarioMutation.mutate(values); - }} - > - - - - - - - - - {(candidateReportRows.length ?? 0) === 0 ? ( - - ) : ( - <> - - 已命中 {candidateReportRows.length} 座可纳入报告的杆塔。默认全选,可按需缩小报告范围。 - - - rowKey="tower_id" - size="small" - pagination={{ pageSize: 8, showSizeChanger: false, hideOnSinglePage: false }} - rowSelection={{ - selectedRowKeys: selectedReportTowerIds, - onChange: (keys) => { - setSelectedReportTowerIds(keys.map((item) => String(item))); - }, - }} - columns={[ - { title: "杆塔号", dataIndex: "tower_no", width: 120 }, - { - title: "风险等级", - dataIndex: "risk_level", - width: 120, - render: (value: string | null) => {formatRiskLevel(value)}, - }, - { - 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={candidateReportRows} - scroll={{ x: 1000 }} - /> - - )} - - + /> ); } diff --git a/web/src/components/fl-analysis/create-job-form.tsx b/web/src/components/fl-analysis/create-job-form.tsx new file mode 100644 index 0000000..f97874d --- /dev/null +++ b/web/src/components/fl-analysis/create-job-form.tsx @@ -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; + 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={form} + layout="vertical" + initialValues={CREATE_JOB_DEFAULTS} + onFinish={onSubmit} + > +
+ + + + {selectedJobType === "normal" || selectedJobType === "tongtiao" ? ( + + + + + + +
+ + {selectedLine ? ( + + {[ + 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 ( + + {`${item.label}${suffix} ${item.tower_ready_count}/${item.tower_total_count}${preparedAt ? ` @ ${formatDateTime(preparedAt)}` : ""}`} + + ); + })} + + } + /> + ) : null} + + {selectedJobType === "normal" || selectedJobType === "tongtiao" ? ( + <> + + {atpModelsError && externalAdapterActive ? ( + + ) : null} + {externalAdapterActive ? ( +
+ + + + + + + + + + + 按“非建线”模式生成措施推荐 + + + + {rows.length === 0 ? ( + + ) : ( + <> + + 已命中 {rows.length} 座中高风险杆塔。默认全选,可按需缩小推荐范围。 + + + 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) => {formatRiskLevel(value)}, + }, + { + 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 }} + /> + + )} + + + ); +} diff --git a/web/src/components/fl-analysis/report-modal.tsx b/web/src/components/fl-analysis/report-modal.tsx new file mode 100644 index 0000000..530b7e5 --- /dev/null +++ b/web/src/components/fl-analysis/report-modal.tsx @@ -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; + 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 ( + { + if (submitting) { + return; + } + onCancel(); + }} + onOk={() => { + form.submit(); + }} + > + + + + form={form} + layout="vertical" + onFinish={onSubmit} + > + + + + + + {rows.length === 0 ? ( + + ) : ( + <> + + 已命中 {rows.length} 座可纳入报告的杆塔。默认全选,可按需缩小报告范围。 + + + 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) => {formatRiskLevel(value)}, + }, + { + 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 }} + /> + + )} + + + ); +} diff --git a/web/src/components/fl-analysis/result-table.tsx b/web/src/components/fl-analysis/result-table.tsx new file mode 100644 index 0000000..a057ae6 --- /dev/null +++ b/web/src/components/fl-analysis/result-table.tsx @@ -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>(() => { + const resultColumns: ColumnsType = [ + { + 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) => {formatRiskLevel(readCurrentRisk(row))}, + }, + { + title: "预期风险", + dataIndex: "risk_level", + width: 110, + render: (value: string | null) => {formatRiskLevel(value)}, + }, + ); + } else { + resultColumns.push({ + title: "风险等级", + dataIndex: "risk_level", + width: 120, + render: (value: string | null) => {formatRiskLevel(value)}, + }); + } + + 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) => ( + + ), + }, + ); + + return resultColumns; + }, [onOpenDetail, selectedJob?.job_type, selectedWaveformJobType]); + + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + if (rows.length === 0) { + return ; + } + + return ( + + rowKey="id" + columns={columns} + dataSource={rows} + pagination={{ pageSize: 20, showSizeChanger: false, hideOnSinglePage: false }} + scroll={{ x: 1400 }} + /> + ); +} diff --git a/web/src/components/fl-analysis/scenario-modal.tsx b/web/src/components/fl-analysis/scenario-modal.tsx new file mode 100644 index 0000000..fbb3171 --- /dev/null +++ b/web/src/components/fl-analysis/scenario-modal.tsx @@ -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; + 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 ( + { + if (submitting) { + return; + } + onCancel(); + }} + onOk={() => { + form.submit(); + }} + > + + + + form={form} + layout="vertical" + onFinish={onSubmit} + > + + + + +