2026-06-08 18:15:54 +08:00
|
|
|
|
import type { AxiosError } from 'axios'
|
2026-05-29 18:16:34 +08:00
|
|
|
|
import http from './http'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 与 SimpleLite `MapEditApiController` 对应的前端胶水。
|
|
|
|
|
|
* 路径前缀:`/sl/projection/map-edit`(YARP → SimpleLite `/projection/map-edit`)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 也覆盖 AI 服务配置 `/sl/projection/ai-config`(GET / POST),由 AiConfigController 提供。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所有方法都通过统一信封 `{ success, code, data, message }` 返回;调用方拿到的是 `data` 部分,
|
|
|
|
|
|
* 失败时直接抛 Error,让上层 try/catch 或 ElMessage.error 统一处理。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const BASE = '/sl/projection/map-edit'
|
|
|
|
|
|
const AI_BASE = '/sl/projection/ai-config'
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapEditEnvelope<T> {
|
|
|
|
|
|
success: boolean
|
|
|
|
|
|
code: number
|
|
|
|
|
|
data: T | null
|
|
|
|
|
|
message: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type MapEditKind =
|
|
|
|
|
|
| 'site'
|
|
|
|
|
|
| 'track'
|
|
|
|
|
|
| 'bezier'
|
|
|
|
|
|
| 'arc'
|
|
|
|
|
|
| 'nurbs'
|
|
|
|
|
|
| 'image'
|
|
|
|
|
|
| 'text'
|
|
|
|
|
|
| 'model'
|
|
|
|
|
|
| 'special'
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateSitePayload {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
z?: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
[k: string]: unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateTrackPayload {
|
|
|
|
|
|
siteA: number
|
|
|
|
|
|
siteB: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
[k: string]: unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateImagePayload {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
imagePath: string
|
|
|
|
|
|
size?: number
|
|
|
|
|
|
angle?: number
|
|
|
|
|
|
opacity?: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateTextPayload {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
content: string
|
|
|
|
|
|
color?: string
|
|
|
|
|
|
size?: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateModelPayload {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
modelPath: string
|
|
|
|
|
|
scale?: number
|
|
|
|
|
|
rotation?: number
|
|
|
|
|
|
zOffset?: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateCarPayload {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
/** 朝向,弧度。可省略,默认 0。 */
|
|
|
|
|
|
theta?: number
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface BatchOp {
|
|
|
|
|
|
action: 'create' | 'delete' | 'patch'
|
|
|
|
|
|
kind?: MapEditKind | string
|
|
|
|
|
|
id?: number
|
|
|
|
|
|
data?: Record<string, unknown>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface PickResult {
|
|
|
|
|
|
x: number
|
|
|
|
|
|
y: number
|
|
|
|
|
|
siteId: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface DashboardSummary {
|
|
|
|
|
|
cars: number
|
|
|
|
|
|
onlineCars: number
|
|
|
|
|
|
alarmingCars: number
|
|
|
|
|
|
sites: number
|
|
|
|
|
|
tracks: number
|
|
|
|
|
|
specials: number
|
|
|
|
|
|
maps: number
|
|
|
|
|
|
missions: number
|
|
|
|
|
|
timestamp: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AssetUploadResult {
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
savedPath: string
|
|
|
|
|
|
/** 相对 url,前端拼上 webVRender host 即可显示。 */
|
|
|
|
|
|
url: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ViewFilterSnapshot {
|
|
|
|
|
|
selectFilter: { sites: boolean; tracks: boolean; cars: boolean; decor: boolean }
|
|
|
|
|
|
alignSnap: { sites: boolean; cars: boolean; tracks: boolean }
|
|
|
|
|
|
showViewport: { sceneLabels: boolean; scenePrimitives: boolean; cars: boolean }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 部分更新;只传你要改的字段即可。后端不会覆盖未传字段。 */
|
|
|
|
|
|
export interface ViewFilterPatch {
|
|
|
|
|
|
selectFilter?: Partial<ViewFilterSnapshot['selectFilter']>
|
|
|
|
|
|
alignSnap?: Partial<ViewFilterSnapshot['alignSnap']>
|
|
|
|
|
|
showViewport?: Partial<ViewFilterSnapshot['showViewport']>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AiMapGenerateRequest {
|
|
|
|
|
|
prompt: string
|
|
|
|
|
|
mode?: 'sites' | 'tracks' | 'sites+tracks' | 'full'
|
|
|
|
|
|
bounds?: [number, number, number, number]
|
|
|
|
|
|
referenceImageBase64?: string
|
|
|
|
|
|
layer?: string
|
|
|
|
|
|
constraints?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AiMapGenerateResult {
|
|
|
|
|
|
created: Array<{ tool: string; kind: string; id: number; typeName?: string; error?: string }>
|
|
|
|
|
|
assistantText?: string
|
|
|
|
|
|
usedTools: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AiConfig {
|
|
|
|
|
|
endpoint: string
|
|
|
|
|
|
apiKey: string
|
|
|
|
|
|
model: string
|
|
|
|
|
|
systemPrompt: string
|
|
|
|
|
|
temperature: number
|
|
|
|
|
|
maxTokens: number
|
|
|
|
|
|
timeoutSec: number
|
|
|
|
|
|
/** 仅 GET 返回时携带:apiKey 非空 → true。 */
|
|
|
|
|
|
configured?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function unwrap<T>(promise: Promise<{ data: MapEditEnvelope<T> }>): Promise<T> {
|
|
|
|
|
|
const { data } = await promise
|
|
|
|
|
|
if (!data?.success) throw new Error(data?.message ?? 'request failed')
|
|
|
|
|
|
return data.data as T
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const mapEditApi = {
|
|
|
|
|
|
// 对象创建/删除
|
|
|
|
|
|
createSite: (p: CreateSitePayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'site'; id: number; name?: string }>(http.post(`${BASE}/objects/site`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
createTrack: (kind: 'track' | 'bezier' | 'arc' | 'nurbs', p: CreateTrackPayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'track'; id: number; typeName: string }>(http.post(`${BASE}/objects/${kind}`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
createImage: (p: CreateImagePayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'image'; id: number }>(http.post(`${BASE}/objects/image`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
createText: (p: CreateTextPayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'text'; id: number }>(http.post(`${BASE}/objects/text`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
createModel: (p: CreateModelPayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'model'; id: number }>(http.post(`${BASE}/objects/model`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
/** 直接创建一辆模拟车 (DummyCar),不经过画布 pick。 */
|
|
|
|
|
|
createCar: (p: CreateCarPayload) =>
|
|
|
|
|
|
unwrap<{ kind: 'car'; id: number; typeName: string }>(http.post(`${BASE}/objects/car`, p)),
|
|
|
|
|
|
|
|
|
|
|
|
deleteObject: (kind: MapEditKind | string, id: number) =>
|
|
|
|
|
|
unwrap<{ kind: string; id: number; deleted: boolean }>(http.delete(`${BASE}/objects/${kind}/${id}`)),
|
|
|
|
|
|
|
|
|
|
|
|
batch: (ops: BatchOp[]) =>
|
|
|
|
|
|
unwrap<{ count: number; results: unknown[] }>(http.post(`${BASE}/objects/batch`, { ops })),
|
|
|
|
|
|
|
|
|
|
|
|
copyFieldsTo: (kind: MapEditKind | string, id: number, fieldNames: string[], targets: Array<{ kind: string; id: number }>) =>
|
|
|
|
|
|
unwrap<{ copied: number }>(http.post(`${BASE}/objects/${kind}/${id}/fields/copy-to`, { fieldNames, targets })),
|
|
|
|
|
|
|
|
|
|
|
|
// 拾取 / 仪表盘
|
|
|
|
|
|
pick: () => unwrap<PickResult>(http.post(`${BASE}/pick`)),
|
|
|
|
|
|
|
|
|
|
|
|
dashboardSummary: () =>
|
|
|
|
|
|
unwrap<DashboardSummary>(http.get(`${BASE}/dashboard/summary`)),
|
|
|
|
|
|
|
2026-06-12 23:00:47 +08:00
|
|
|
|
// 资产上传:JSON body 传 base64(后端按 JSON 解析;勿手动设 multipart 头 —— body 并非 multipart)。
|
2026-05-29 18:16:34 +08:00
|
|
|
|
uploadAsset: (filename: string, dataBase64: string) =>
|
2026-06-12 23:00:47 +08:00
|
|
|
|
unwrap<AssetUploadResult>(http.post(`${BASE}/assets/upload`, { filename, data: dataBase64 })),
|
2026-05-29 18:16:34 +08:00
|
|
|
|
|
|
|
|
|
|
// AI 生图
|
|
|
|
|
|
aiMapGenerate: (req: AiMapGenerateRequest) =>
|
|
|
|
|
|
unwrap<AiMapGenerateResult>(http.post(`${BASE}/ai/map-generate`, req)),
|
|
|
|
|
|
|
|
|
|
|
|
projectSave: (path?: string) =>
|
|
|
|
|
|
unwrap<{ path: string; savedAt: string }>(http.post(`${BASE}/project/save`, { path: path ?? '' })),
|
|
|
|
|
|
|
|
|
|
|
|
projectLoad: (path: string) =>
|
|
|
|
|
|
unwrap<{ path: string; sites: number; tracks: number; specials: number; missions: number }>(
|
|
|
|
|
|
http.post(`${BASE}/project/load`, { path })
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
projectCurrent: () =>
|
|
|
|
|
|
unwrap<{ sites: number; tracks: number; specials: number; maps: number; missions: number; timestamp: string; cwd: string }>(
|
|
|
|
|
|
http.get(`${BASE}/project/current`)
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
projectBrowse: (dir?: string) =>
|
|
|
|
|
|
unwrap<{
|
|
|
|
|
|
dir: string
|
|
|
|
|
|
parent: string | null
|
|
|
|
|
|
subdirs: Array<{ name: string; fullPath: string }>
|
|
|
|
|
|
jsons: Array<{ name: string; fullPath: string; sizeBytes: number; modified: string }>
|
|
|
|
|
|
}>(http.get(`${BASE}/project/browse`, { params: dir ? { dir } : {} })),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 在 SimpleLite 桌面进程上弹 Windows 原生「打开文件」对话框,
|
|
|
|
|
|
* 让用户挑一个项目 JSON。返回 { path, cancelled };cancelled=true 时 path 为 null。
|
|
|
|
|
|
* initialDir 默认 SimpleLite 程序工作目录。
|
|
|
|
|
|
*/
|
|
|
|
|
|
projectNativePickOpen: (initialDir?: string) =>
|
|
|
|
|
|
unwrap<{ path: string | null; cancelled: boolean }>(
|
|
|
|
|
|
http.post(`${BASE}/project/native-pick-open`, { initialDir: initialDir ?? '' })
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 弹 Windows 原生「另存为」对话框。返回 { path, cancelled }。
|
|
|
|
|
|
* suggestedFileName 默认 project.current.json。
|
|
|
|
|
|
*/
|
|
|
|
|
|
projectNativePickSave: (initialDir?: string, suggestedFileName?: string) =>
|
|
|
|
|
|
unwrap<{ path: string | null; cancelled: boolean }>(
|
|
|
|
|
|
http.post(`${BASE}/project/native-pick-save`, {
|
|
|
|
|
|
initialDir: initialDir ?? '',
|
|
|
|
|
|
suggestedFileName: suggestedFileName ?? 'project.current.json'
|
|
|
|
|
|
})
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
sampleSitesAlongTrack: (trackId: number, opts: { spacingMm: number; includeEndpoints?: boolean; layer?: string }) =>
|
|
|
|
|
|
unwrap<{ created: number; ids: number[] }>(
|
|
|
|
|
|
http.post(`${BASE}/objects/track/${trackId}/sample-sites`, opts)
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 读取后端「过滤」菜单当前的三组开关。编辑器载入时拉一次,让前端 reactive 状态
|
|
|
|
|
|
* 与 SimpleLite 真实开关一致(避免「前端勾着、后端没改」的偏差)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
getViewFilter: () => unwrap<ViewFilterSnapshot>(http.get(`${BASE}/view-filter`)),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 部分更新「过滤」菜单。只传要改的字段即可,未传字段保持原状。
|
|
|
|
|
|
* 后端写入对应 SimpleUI 静态字段 + 触发 SimpleSceneRenderer.NotifyStateChanged()
|
|
|
|
|
|
* 立即重画工作区,对 SelectFilter 变化还会同步 CycleGUI 引擎层 SelectObject 模式。
|
|
|
|
|
|
*/
|
|
|
|
|
|
setViewFilter: (patch: ViewFilterPatch) =>
|
|
|
|
|
|
unwrap<ViewFilterSnapshot>(http.post(`${BASE}/view-filter`, patch))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const aiConfigApi = {
|
|
|
|
|
|
get: () => unwrap<AiConfig>(http.get(`${AI_BASE}/`)),
|
|
|
|
|
|
save: (cfg: AiConfig) => unwrap<{ saved: boolean }>(http.post(`${AI_BASE}/`, cfg))
|
|
|
|
|
|
}
|
2026-06-03 09:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 地图管理(固定文件夹 + 按名称列表 / 保存 / 删除 / 使用 / 编辑打开)
|
|
|
|
|
|
// 对应 SimpleLite MapEditApiController 的 /map-edit/maps* 接口。
|
|
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapListItem {
|
|
|
|
|
|
/** 地图名称(文件名去掉 .json 后缀),列表展示与各操作都以它为标识。 */
|
|
|
|
|
|
name: string
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
sizeBytes: number
|
|
|
|
|
|
modified: string
|
|
|
|
|
|
/** 是否为当前项目默认使用的地图(列表高亮)。 */
|
|
|
|
|
|
isCurrent: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapListResult {
|
|
|
|
|
|
directory: string
|
|
|
|
|
|
currentFileName: string | null
|
|
|
|
|
|
maps: MapListItem[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapLoadSummary {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
path: string
|
|
|
|
|
|
sites: number
|
|
|
|
|
|
tracks: number
|
|
|
|
|
|
specials: number
|
|
|
|
|
|
missions: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface SceneTaskBusyCar {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
name: string
|
|
|
|
|
|
reasons: string[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface SceneTaskStatus {
|
|
|
|
|
|
hasTask: boolean
|
|
|
|
|
|
busyCount: number
|
|
|
|
|
|
busyCars: SceneTaskBusyCar[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapSaveResult {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
path: string
|
|
|
|
|
|
savedAt: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 18:15:54 +08:00
|
|
|
|
export interface MapContentResult {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
content: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 09:37:07 +08:00
|
|
|
|
/** 保存结果:success 正常返回 data;conflict=true 表示同名地图已存在,调用方应弹「替换」确认。 */
|
|
|
|
|
|
export type MapSaveOutcome =
|
|
|
|
|
|
| { ok: true; data: MapSaveResult }
|
|
|
|
|
|
| { ok: false; conflict: boolean; message: string }
|
|
|
|
|
|
|
|
|
|
|
|
export interface MapMergeResult {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
fileName: string
|
|
|
|
|
|
path: string
|
|
|
|
|
|
/** 底图 = 合并时的「当前使用地图」名。 */
|
|
|
|
|
|
baseMap: string
|
|
|
|
|
|
/** 合并进来的源地图名(去重、且不含当前地图自身,按叠加顺序)。 */
|
|
|
|
|
|
sources: string[]
|
|
|
|
|
|
sourceCount: number
|
|
|
|
|
|
/** target 与当前地图同名 → 本次合并覆盖了当前地图。 */
|
|
|
|
|
|
overwroteCurrent: boolean
|
|
|
|
|
|
/** 合并后统计(用于结果提示)。 */
|
|
|
|
|
|
sites: number
|
|
|
|
|
|
tracks: number
|
|
|
|
|
|
specials: number
|
|
|
|
|
|
missions: number
|
|
|
|
|
|
savedAt: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 合并结果:conflict=true 仅表示目标地图同名已存在,调用方应弹「替换」确认后重试。 */
|
|
|
|
|
|
export type MapMergeOutcome =
|
|
|
|
|
|
| { ok: true; data: MapMergeResult }
|
|
|
|
|
|
| { ok: false; conflict: boolean; message: string }
|
|
|
|
|
|
|
|
|
|
|
|
export const mapsApi = {
|
|
|
|
|
|
list: () => unwrap<MapListResult>(http.get(`${BASE}/maps`)),
|
|
|
|
|
|
|
2026-06-08 18:15:54 +08:00
|
|
|
|
/** 读取固定地图文件夹内某张地图的 JSON 原文(MiGu.Server MapsContentController)。 */
|
|
|
|
|
|
readContent: (name: string) =>
|
|
|
|
|
|
http.get<MapContentResult>(
|
|
|
|
|
|
`/maps/${encodeURIComponent(name)}/content`
|
|
|
|
|
|
).then((r) => r.data),
|
|
|
|
|
|
|
2026-06-03 09:37:07 +08:00
|
|
|
|
sceneTaskStatus: () => unwrap<SceneTaskStatus>(http.get(`${BASE}/maps/scene-task-status`)),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 保存当前场景为固定文件夹内的地图。overwrite=false 且同名已存在时后端回 409,
|
|
|
|
|
|
* 这里翻译为 { ok:false, conflict:true },让调用方弹「是否替换原地图」确认框。
|
|
|
|
|
|
*/
|
|
|
|
|
|
async save(name: string, overwrite = false): Promise<MapSaveOutcome> {
|
2026-06-08 18:15:54 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await http.post<MapEditEnvelope<MapSaveResult>>(`${BASE}/maps/save`, { name, overwrite })
|
|
|
|
|
|
if (data?.success) return { ok: true, data: data.data as MapSaveResult }
|
|
|
|
|
|
return { ok: false, conflict: data?.code === 409, message: data?.message ?? '保存失败' }
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const ax = err as AxiosError<MapEditEnvelope<MapSaveResult>>
|
|
|
|
|
|
const body = ax.response?.data
|
|
|
|
|
|
if (ax.response?.status === 409 || body?.code === 409) {
|
|
|
|
|
|
return { ok: false, conflict: true, message: body?.message ?? '地图已存在' }
|
|
|
|
|
|
}
|
|
|
|
|
|
throw err
|
|
|
|
|
|
}
|
2026-06-03 09:37:07 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/** 加载指定地图到场景以供编辑(不校验任务、不改当前使用地图)。 */
|
|
|
|
|
|
open: (name: string) => unwrap<MapLoadSummary>(http.post(`${BASE}/maps/open`, { name })),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设为当前使用地图并加载。后端切换前会校验场景无车辆任务;存在任务回 409,
|
|
|
|
|
|
* unwrap 会抛出携带后端友好文案的 Error,调用方 try/catch 提示即可。
|
|
|
|
|
|
*/
|
|
|
|
|
|
use: (name: string) => unwrap<MapLoadSummary>(http.post(`${BASE}/maps/use`, { name })),
|
|
|
|
|
|
|
|
|
|
|
|
delete: (name: string) =>
|
|
|
|
|
|
unwrap<{ name: string; fileName: string; deleted: boolean }>(
|
|
|
|
|
|
http.delete(`${BASE}/maps/${encodeURIComponent(name)}`)
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
/** 重命名 maps 目录下的地图文件(from / to 均为不含扩展名的地图名)。 */
|
|
|
|
|
|
rename: (from: string, to: string) =>
|
|
|
|
|
|
unwrap<{ from: string; to: string; fileName: string; path: string }>(
|
|
|
|
|
|
http.post(`${BASE}/maps/rename`, { from, to })
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把选中的地图合并进「当前使用地图」后另存为新地图(语义同桌面端「合并」= SimpleProject.ImportFile)。
|
|
|
|
|
|
* sources 为要合并进来的源地图名数组(≥1,当前地图自身会被后端忽略),以「当前使用地图」为底图依次叠加。
|
|
|
|
|
|
* target 与当前地图同名 + overwrite 即覆盖当前地图。
|
|
|
|
|
|
* 目标同名且 overwrite=false 时后端回 409 → { ok:false, conflict:true },调用方弹「替换」确认;
|
|
|
|
|
|
* 当前未设置使用地图(400)/ 场景内有车辆任务(409)时 message 不含「已存在」,conflict=false,调用方直接提示。
|
|
|
|
|
|
*/
|
|
|
|
|
|
async merge(sources: string[], target: string, overwrite = false): Promise<MapMergeOutcome> {
|
2026-06-12 23:00:47 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await http.post<MapEditEnvelope<MapMergeResult>>(`${BASE}/maps/merge`, {
|
|
|
|
|
|
sources,
|
|
|
|
|
|
target,
|
|
|
|
|
|
overwrite
|
|
|
|
|
|
})
|
|
|
|
|
|
if (data?.success) return { ok: true, data: data.data as MapMergeResult }
|
|
|
|
|
|
const conflict = data?.code === 409 && (data?.message ?? '').includes('已存在')
|
|
|
|
|
|
return { ok: false, conflict, message: data?.message ?? '合并失败' }
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 与 save() 对齐:后端以 HTTP 409 状态码返回时同样翻译为 conflict,
|
|
|
|
|
|
// 让调用方能弹「是否替换」确认而非直接报错。
|
|
|
|
|
|
const ax = err as AxiosError<MapEditEnvelope<MapMergeResult>>
|
|
|
|
|
|
const body = ax.response?.data
|
|
|
|
|
|
if (ax.response?.status === 409 || body?.code === 409) {
|
|
|
|
|
|
const message = body?.message ?? '目标地图已存在'
|
|
|
|
|
|
return { ok: false, conflict: message.includes('已存在'), message }
|
|
|
|
|
|
}
|
|
|
|
|
|
throw err
|
|
|
|
|
|
}
|
2026-06-03 09:37:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|