From 58d6efe6fe6c1461a4a8466539a82776d1a5a231 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Sat, 20 Jun 2026 12:00:23 +0800 Subject: [PATCH] =?UTF-8?q?[fix]:[FL-212][=E4=BF=AE=E5=A4=8D=E9=AB=98?= =?UTF-8?q?=E7=A8=8B=E6=96=87=E4=BB=B6=E8=AE=B0=E5=BD=95=E9=A1=B5=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- memory/2026-06-20.md | 18 ++ web/src/app/admin/elevation-records/page.tsx | 192 +++++++++++++++---- 2 files changed, 174 insertions(+), 36 deletions(-) diff --git a/memory/2026-06-20.md b/memory/2026-06-20.md index e8e04b2..c586bfe 100644 --- a/memory/2026-06-20.md +++ b/memory/2026-06-20.md @@ -391,3 +391,21 @@ - 风险与关注点: - 改动仅影响系统参数页前端交互组织方式,不改变后端接口字段、权限码或参数状态语义。 + +# Work Log - 高程文件记录页构建修复(FL-212) + +- 背景: + - CI 在 `web/src/app/admin/elevation-records/page.tsx` TypeScript 阶段失败,原因是页面仍按旧接口读取 `useAuth().api`。 + +- 本次处理: + - 高程文件记录页请求链路改为使用当前 `AuthContextValue` 暴露的 `fetchWithAuth`。 + - 将高程记录、线路列表、上传、删除、分析、地形生成、回填和预览请求统一对齐 `/api/v1/...` 路径。 + - 补齐页面内响应类型、错误处理 helper,并修正同页残留的 AntD `Card` / `Select.Option` / Cesium 预览 props 类型问题。 + +- 验证: + - 基线:`npm run build:web` 复现失败,错误为 `Property 'api' does not exist on type 'AuthContextValue'`。 + - 修改后:`npm --workspace web exec eslint src/app/admin/elevation-records/page.tsx --max-warnings=0` 通过。 + - 修改后:`npm run build:web` 通过,仍仅输出既有 multiple lockfiles 与 middleware/proxy 迁移提示。 + +- 风险与关注点: + - 改动仅影响 `/admin/elevation-records` 前端请求与类型适配,不改变后端接口字段、权限码或数据模型。 diff --git a/web/src/app/admin/elevation-records/page.tsx b/web/src/app/admin/elevation-records/page.tsx index 973026f..8cb3053 100644 --- a/web/src/app/admin/elevation-records/page.tsx +++ b/web/src/app/admin/elevation-records/page.tsx @@ -1,24 +1,25 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import type { ComponentType } from "react"; +import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Alert, Button, - Card, + Card as AntdCard, Descriptions, Dropdown, Form, Input, InputNumber, Modal, - Popconfirm, Select, Space, Table, Tag, Upload, message, + type CardProps, type UploadFile, } from "antd"; import { MoreOutlined, UploadOutlined } from "@ant-design/icons"; @@ -26,7 +27,6 @@ import type { ColumnsType } from "antd/es/table"; import { useAuth } from "@/components/auth-provider"; import { ElevationPreviewCesiumMap } from "@/components/elevation-preview-cesium-map"; -import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; @@ -61,6 +61,89 @@ type FileRecordListResponse = { total: number; }; +type LineSummary = { + id: string; + code: string | null; + name: string | null; +}; + +type LineListResponse = { + items?: LineSummary[]; +}; + +type ElevationPreviewPoint = { + longitude: number; + latitude: number; + altitude_m: number; +}; + +type ElevationPreviewCell = { + min_longitude: number; + max_longitude: number; + min_latitude: number; + max_latitude: number; + altitude_m: number; +}; + +type ElevationPreviewDiagnostics = { + source_crs: string | null; + source_bounds_min_x: number | null; + source_bounds_max_x: number | null; + source_bounds_min_y: number | null; + source_bounds_max_y: number | null; + wgs84_bounds_min_lon: number | null; + wgs84_bounds_max_lon: number | null; + wgs84_bounds_min_lat: number | null; + wgs84_bounds_max_lat: number | null; + raster_width: number | null; + raster_height: number | null; + target_samples: number | null; + sampling_step: number | null; + scanned_candidates: number | null; + valid_preview_count: number | null; + skip_read_error: number; + skip_masked: number; + skip_nodata: number; + skip_nonfinite: number; + skip_sample_transform_error: number; + sample_tx_first_error: string | null; + skip_sample_out_of_range: number; + skip_cell_transform_error: number; + skip_cell_out_of_range: number; +}; + +type ElevationFileRecordPreviewResponse = { + record: ElevationFileRecordSummary; + preview_mode: "point_cloud" | "terrain_grid"; + total_points: number; + sampled_points: number; + points: ElevationPreviewPoint[]; + cells: ElevationPreviewCell[]; + diagnostics: ElevationPreviewDiagnostics | null; + warnings: string[]; +}; + +type ElevationFileRecordTaskResponse = { + record: ElevationFileRecordSummary; + task_id: string | null; + queued: boolean; + detail: string | null; + warnings: string[]; +}; + +type ElevationFileRecordUploadResponse = { + record: ElevationFileRecordSummary; + queued: boolean; + detail: string | null; + warnings: string[]; +}; + +type UploadFormValues = { + source?: string; + resolution_m?: number; + notes?: string; +}; + type ApplyFormValues = { line_id: string; file_record_id: string; @@ -73,6 +156,8 @@ const DEFAULT_APPLY_FORM: ApplyFormValues = { mode: "fill_null_only", }; +const Card = AntdCard as unknown as ComponentType; + function statusTagColor(status: string): string { if (status === "success" || status === "active" || status === "ready") return "green"; if (status === "running" || status === "processing") return "blue"; @@ -87,8 +172,25 @@ function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } +async function readJsonResponse(response: Response): Promise { + if (!response.ok) { + throw new Error(await readApiError(response)); + } + return (await response.json()) as T; +} + +async function ensureOkResponse(response: Response): Promise { + if (!response.ok) { + throw new Error(await readApiError(response)); + } +} + +function readMutationError(error: unknown, fallback: string): string { + return error instanceof Error && error.message ? error.message : fallback; +} + export default function ElevationRecordsPage() { - const { api } = useAuth(); + const { fetchWithAuth } = useAuth(); const queryClient = useQueryClient(); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState(); @@ -99,7 +201,7 @@ export default function ElevationRecordsPage() { const [applyForm] = Form.useForm(); const [selectedRecord, setSelectedRecord] = useState(null); const [previewModalOpen, setPreviewModalOpen] = useState(false); - const [previewData, setPreviewData] = useState(null); + const [previewData, setPreviewData] = useState(null); // Fetch file records const { data: recordsData, isLoading } = useQuery({ @@ -108,8 +210,9 @@ export default function ElevationRecordsPage() { const params = new URLSearchParams(); if (keyword) params.append("keyword", keyword); if (statusFilter) params.append("status", statusFilter); - const res = await api.get(`/elevation/records?${params.toString()}`); - return res.data; + const query = params.toString(); + const response = await fetchWithAuth(`/api/v1/elevation/records${query ? `?${query}` : ""}`); + return readJsonResponse(response); }, }); @@ -117,8 +220,8 @@ export default function ElevationRecordsPage() { const { data: linesData } = useQuery({ queryKey: ["lines"], queryFn: async () => { - const res = await api.get("/lines?limit=1000"); - return res.data; + const response = await fetchWithAuth("/api/v1/lines?limit=1000"); + return readJsonResponse(response); }, }); @@ -129,7 +232,7 @@ export default function ElevationRecordsPage() { // Upload mutation const uploadMutation = useMutation({ - mutationFn: async (values: any) => { + mutationFn: async (values: UploadFormValues) => { const formData = new FormData(); if (fileList.length === 0) { throw new Error("请选择文件"); @@ -140,10 +243,11 @@ export default function ElevationRecordsPage() { if (values.notes) formData.append("notes", values.notes); formData.append("trigger_analysis", "true"); - const res = await api.post("/elevation/records", formData, { - headers: { "Content-Type": "multipart/form-data" }, + const response = await fetchWithAuth("/api/v1/elevation/records", { + method: "POST", + body: formData, }); - return res.data; + return readJsonResponse(response); }, onSuccess: () => { message.success("文件上传成功"); @@ -153,59 +257,70 @@ export default function ElevationRecordsPage() { queryClient.invalidateQueries({ queryKey: ["elevation-records"] }); }, onError: (error) => { - message.error(readApiError(error) || "上传失败"); + message.error(readMutationError(error, "上传失败")); }, }); // Delete mutation const deleteMutation = useMutation({ mutationFn: async (id: string) => { - await api.delete(`/elevation/records/${id}`); + const response = await fetchWithAuth(`/api/v1/elevation/records/${id}`, { + method: "DELETE", + }); + await ensureOkResponse(response); }, onSuccess: () => { message.success("删除成功"); queryClient.invalidateQueries({ queryKey: ["elevation-records"] }); }, onError: (error) => { - message.error(readApiError(error) || "删除失败"); + message.error(readMutationError(error, "删除失败")); }, }); // Analyze mutation const analyzeMutation = useMutation({ mutationFn: async (id: string) => { - const res = await api.post(`/elevation/records/${id}/analyze`); - return res.data; + const response = await fetchWithAuth(`/api/v1/elevation/records/${id}/analyze`, { + method: "POST", + }); + return readJsonResponse(response); }, onSuccess: () => { message.success("分析任务已提交"); queryClient.invalidateQueries({ queryKey: ["elevation-records"] }); }, onError: (error) => { - message.error(readApiError(error) || "分析失败"); + message.error(readMutationError(error, "分析失败")); }, }); // Terrain build mutation const terrainMutation = useMutation({ mutationFn: async (id: string) => { - const res = await api.post(`/elevation/records/${id}/terrain/build`); - return res.data; + const response = await fetchWithAuth(`/api/v1/elevation/records/${id}/terrain/build`, { + method: "POST", + }); + return readJsonResponse(response); }, onSuccess: () => { message.success("地形瓦片任务已提交"); queryClient.invalidateQueries({ queryKey: ["elevation-records"] }); }, onError: (error) => { - message.error(readApiError(error) || "地形生成失败"); + message.error(readMutationError(error, "地形生成失败")); }, }); // Apply mutation const applyMutation = useMutation({ mutationFn: async (values: ApplyFormValues) => { - const res = await api.post("/elevation/jobs/apply-line", values); - return res.data; + const response = await fetchWithAuth("/api/v1/elevation/jobs/apply-line", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }); + return readJsonResponse(response); }, onSuccess: () => { message.success("回填任务已创建"); @@ -213,22 +328,22 @@ export default function ElevationRecordsPage() { applyForm.resetFields(); }, onError: (error) => { - message.error(readApiError(error) || "创建任务失败"); + message.error(readMutationError(error, "创建任务失败")); }, }); // Preview mutation const previewMutation = useMutation({ mutationFn: async (id: string) => { - const res = await api.get(`/elevation/records/${id}/preview?max_points=1500`); - return res.data; + const response = await fetchWithAuth(`/api/v1/elevation/records/${id}/preview?max_points=1500`); + return readJsonResponse(response); }, onSuccess: (data) => { setPreviewData(data); setPreviewModalOpen(true); }, onError: (error) => { - message.error(readApiError(error) || "预览失败"); + message.error(readMutationError(error, "预览失败")); }, }); @@ -490,8 +605,8 @@ export default function ElevationRecordsPage() { placeholder="选择线路" showSearch optionFilterProp="label" - options={linesData?.items?.map((line: any) => ({ - label: `${line.code} - ${line.name}`, + options={linesData?.items?.map((line) => ({ + label: `${line.code ?? "-"} - ${line.name ?? "-"}`, value: line.id, }))} /> @@ -503,10 +618,12 @@ export default function ElevationRecordsPage() { rules={[{ required: true }]} initialValue="fill_null_only" > - +