[fix]:[FL-212][修复高程文件记录页构建类型错误]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-20 12:00:23 +08:00
parent faa47e26cd
commit 58d6efe6fe
2 changed files with 174 additions and 36 deletions
+18
View File
@@ -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` 前端请求与类型适配,不改变后端接口字段、权限码或数据模型。
+156 -36
View File
@@ -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<CardProps>;
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<T>(response: Response): Promise<T> {
if (!response.ok) {
throw new Error(await readApiError(response));
}
return (await response.json()) as T;
}
async function ensureOkResponse(response: Response): Promise<void> {
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<string | undefined>();
@@ -99,7 +201,7 @@ export default function ElevationRecordsPage() {
const [applyForm] = Form.useForm<ApplyFormValues>();
const [selectedRecord, setSelectedRecord] = useState<ElevationFileRecordSummary | null>(null);
const [previewModalOpen, setPreviewModalOpen] = useState(false);
const [previewData, setPreviewData] = useState<any>(null);
const [previewData, setPreviewData] = useState<ElevationFileRecordPreviewResponse | null>(null);
// Fetch file records
const { data: recordsData, isLoading } = useQuery<FileRecordListResponse>({
@@ -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<FileRecordListResponse>(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<LineListResponse>(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<ElevationFileRecordUploadResponse>(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<ElevationFileRecordTaskResponse>(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<ElevationFileRecordTaskResponse>(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<ElevationFileRecordPreviewResponse>(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"
>
<Select>
<Select.Option value="fill_null_only"></Select.Option>
<Select.Option value="overwrite_all"></Select.Option>
</Select>
<Select
options={[
{ label: "仅填空(只更新空值)", value: "fill_null_only" },
{ label: "全部覆盖(覆盖所有数据)", value: "overwrite_all" },
]}
/>
</Form.Item>
</Form>
</Modal>
@@ -532,7 +649,10 @@ export default function ElevationRecordsPage() {
<Descriptions.Item label="采样点数">{previewData.sampled_points?.toLocaleString()}</Descriptions.Item>
</Descriptions>
<div style={{ height: 600 }}>
<ElevationPreviewCesiumMap previewData={previewData} />
<ElevationPreviewCesiumMap
points={previewData.points}
cells={previewData.cells}
/>
</div>
</div>
)}