增强地图编辑 CAD 能力与反射对象管理体验。
补充对齐/批量建站 API,并优化反射面板、历史栈与字段管理相关交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -122,8 +122,17 @@ export interface AssetUploadResult {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type SelectTrackMode = 'all' | 'straight' | 'curve'
|
||||
|
||||
export interface ViewFilterSnapshot {
|
||||
selectFilter: { sites: boolean; tracks: boolean; cars: boolean; decor: boolean }
|
||||
selectFilter: {
|
||||
sites: boolean
|
||||
tracks: boolean
|
||||
cars: boolean
|
||||
decor: boolean
|
||||
/** 路径子类型:all=全部;straight=仅 UITrack;curve=贝塞尔/弧/NURBS */
|
||||
trackMode?: SelectTrackMode
|
||||
}
|
||||
alignSnap: { sites: boolean; cars: boolean; tracks: boolean }
|
||||
showViewport: { sceneLabels: boolean; scenePrimitives: boolean; cars: boolean }
|
||||
}
|
||||
@@ -195,11 +204,68 @@ export const mapEditApi = {
|
||||
batch: (ops: BatchOp[]) =>
|
||||
unwrap<{ count: number; results: unknown[] }>(http.post(`${BASE}/objects/batch`, { ops })),
|
||||
|
||||
/**
|
||||
* CAD 相对包围盒对齐(SimpleLite CadAlignService)。
|
||||
* mode: left|right|top|bottom|centerH|centerV|center|distributeH|distributeV
|
||||
*/
|
||||
cadAlign: (mode: string, targets: Array<{ kind: string; id: number }>) =>
|
||||
unwrap<{
|
||||
mode: string
|
||||
count: number
|
||||
previous: Array<{ kind: string; id: number; x: number; y: number }>
|
||||
updated: Array<{ kind: string; id: number; x: number; y: number }>
|
||||
}>(http.post(`${BASE}/cad/align`, { mode, targets })),
|
||||
|
||||
/** 横向/纵向等距建站(CadBatchGenerateService.PlanLinear)。 */
|
||||
cadBatchLinear: (p: {
|
||||
horizontal: boolean
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
count: number
|
||||
layer?: string
|
||||
namePrefix?: string
|
||||
}) =>
|
||||
unwrap<{ count: number; created: Array<{ kind: string; id: number }> }>(
|
||||
http.post(`${BASE}/cad/batch-linear`, p)
|
||||
),
|
||||
|
||||
/** 矩阵建站(CadBatchGenerateService.PlanMatrix)。 */
|
||||
cadBatchMatrix: (p: {
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
rows: number
|
||||
cols: number
|
||||
layer?: string
|
||||
namePrefix?: string
|
||||
}) =>
|
||||
unwrap<{ count: number; created: Array<{ kind: string; id: number }> }>(
|
||||
http.post(`${BASE}/cad/batch-matrix`, p)
|
||||
),
|
||||
|
||||
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`)),
|
||||
/** 场景已有自定义字段键(Ctrl+I /「复制字段」弹窗候选)。 */
|
||||
sceneFieldKeys: () =>
|
||||
unwrap<{ keys: string[] }>(http.get(`${BASE}/scene-field-keys`)),
|
||||
|
||||
/** 对多个目标批量写入同一字段值(对齐桌面 Ctrl+I)。 */
|
||||
batchSetField: (key: string, value: string, targets: Array<{ kind: string; id: number }>) =>
|
||||
unwrap<{ affected: number }>(http.post(`${BASE}/batch-set-field`, { key, value, targets })),
|
||||
|
||||
/** 端点落在给定站点上的路径(删站点会级联删除;撤销前需一并快照)。 */
|
||||
tracksTouchingSites: (siteIds: number[]) =>
|
||||
unwrap<{ tracks: Array<{ id: number; typeName: string; siteA: number; siteB: number }> }>(
|
||||
http.post(`${BASE}/tracks-touching-sites`, { siteIds })
|
||||
),
|
||||
|
||||
// 拾取 / 仪表盘。snap=false 时落点用原始鼠标坐标(文本/图片/站点);布线取端点默认 true。
|
||||
pick: (opts?: { snap?: boolean }) =>
|
||||
unwrap<PickResult>(http.post(`${BASE}/pick`, { snap: opts?.snap ?? true })),
|
||||
|
||||
dashboardSummary: () =>
|
||||
unwrap<DashboardSummary>(http.get(`${BASE}/dashboard/summary`)),
|
||||
|
||||
@@ -4,11 +4,16 @@ import type { Site, Track } from '@/types/map'
|
||||
import type { Car, CarState } from '@/types/car'
|
||||
import type { Mission, MissionStatus } from '@/types/mission'
|
||||
import { mockSites, mockTracks, mockCars, mockMissions } from '@/mock/server'
|
||||
import { applyRuntimeEnrichment, deriveCarState } from '@/utils/carRuntime'
|
||||
|
||||
/** 设为 true 时使用本地 Mock;默认走 YARP → SimpleLite :8222。
|
||||
* 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关(替代旧的 VITE_PROJECTION_MOCK)。 */
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
/** 车体 status 缓存:电量 + 告警信号,避免 3s 轮询打爆 reflection。 */
|
||||
const STATUS_CACHE_TTL_MS = 4000
|
||||
const statusCache = new Map<number, { at: number; rows: Array<{ key: string; value: string }> | null }>()
|
||||
|
||||
export async function listSites(): Promise<Site[]> {
|
||||
if (MOCK) return mockSites()
|
||||
const { data } = await http.get<Site[]>('/sl/projection/sites')
|
||||
@@ -22,14 +27,7 @@ export async function listTracks(): Promise<Track[]> {
|
||||
}
|
||||
|
||||
function mapStatusToCarState(status?: string | null): CarState {
|
||||
if (!status) return 'idle'
|
||||
const s = status.toLowerCase()
|
||||
if (/fault|error|failed|故障|异常|失联|超时|检修/.test(s)) return 'fault'
|
||||
if (/charg|充电/.test(s)) return 'charging'
|
||||
if (/pause|暂停|挂起/.test(s)) return 'paused'
|
||||
if (/offline|离线/.test(s)) return 'offline'
|
||||
if (/run|busy|working|运行|工作|执行|忙/.test(s)) return 'running'
|
||||
return 'idle'
|
||||
return deriveCarState({ state: 'idle', lstatus: status ?? undefined }, null)
|
||||
}
|
||||
|
||||
function mapStatusToMissionStatus(status?: string | null): MissionStatus {
|
||||
@@ -53,10 +51,11 @@ function carFromReflection(row: ReflectionObject): Car {
|
||||
x: 0,
|
||||
y: 0,
|
||||
theta: 0,
|
||||
batterySoc: 0.8,
|
||||
batterySoc: 0,
|
||||
state: mapStatusToCarState(row.status),
|
||||
lastUpdate: new Date().toISOString(),
|
||||
group: row.layer ?? undefined
|
||||
group: row.layer ?? undefined,
|
||||
lstatus: row.status ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,17 +81,52 @@ async function listMissionsFromReflection(): Promise<Mission[]> {
|
||||
return rows.map(missionFromReflection)
|
||||
}
|
||||
|
||||
async function loadCarStatus(carId: number): Promise<Array<{ key: string; value: string }> | null> {
|
||||
const cached = statusCache.get(carId)
|
||||
const now = Date.now()
|
||||
if (cached && now - cached.at < STATUS_CACHE_TTL_MS) return cached.rows
|
||||
try {
|
||||
const status = await reflectionApi.getStatus('car', carId)
|
||||
const rows = (status ?? []).map((r) => ({ key: r.key, value: r.value }))
|
||||
statusCache.set(carId, { at: now, rows })
|
||||
return rows
|
||||
} catch {
|
||||
statusCache.set(carId, { at: now, rows: cached?.rows ?? null })
|
||||
return cached?.rows ?? null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 纠正投影里写死的 batterySoc=0.8,以及把「正常但未初始化」误标成 fault 的 state。
|
||||
* 数据源:反射 status(车体_Soc / AlarmLevel / driveStatus)。
|
||||
*/
|
||||
async function enrichCarsRuntime(cars: Car[]): Promise<Car[]> {
|
||||
return Promise.all(
|
||||
cars.map(async (car) => {
|
||||
const id = car.rawId ?? Number(String(car.id).replace(/^C/i, ''))
|
||||
// 无车体 status 时也先按 lstatus 纠一次(「正常但未初始化」→ idle)
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return applyRuntimeEnrichment(car, null)
|
||||
}
|
||||
const rows = await loadCarStatus(id)
|
||||
return applyRuntimeEnrichment(car, rows)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/** 优先走 SimpleLite /projection/cars;502 或空列表时回退 reflection 对象列表(与 3D 场景同源)。 */
|
||||
export async function listCars(): Promise<Car[]> {
|
||||
if (MOCK) return mockCars()
|
||||
try {
|
||||
const { data } = await http.get<Car[]>('/sl/projection/cars')
|
||||
if (Array.isArray(data) && data.length > 0) return data
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
return enrichCarsRuntime(data)
|
||||
}
|
||||
const fallback = await listCarsFromReflection()
|
||||
if (fallback.length > 0) return fallback
|
||||
return Array.isArray(data) ? data : []
|
||||
if (fallback.length > 0) return enrichCarsRuntime(fallback)
|
||||
return Array.isArray(data) ? enrichCarsRuntime(data) : []
|
||||
} catch {
|
||||
return listCarsFromReflection()
|
||||
return enrichCarsRuntime(await listCarsFromReflection())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import http from './http'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
mockReflectionAssemblies,
|
||||
mockReflectionBundle,
|
||||
@@ -64,6 +65,8 @@ export interface ReflectionMethod {
|
||||
returnType: string
|
||||
hasParams: boolean
|
||||
params: ReflectionParam[]
|
||||
requiresPlatformConfirm?: boolean
|
||||
confirmMessage?: string | null
|
||||
}
|
||||
|
||||
export interface ReflectionObject {
|
||||
@@ -225,30 +228,94 @@ export function emptyMonitorVisibilityMap(): MonitorVisibilityMap {
|
||||
// 单独保留命名是为了表达"业务含义不同"——可勾选全集 vs 已勾选白名单。
|
||||
export const emptyMonitorAvailableMap = emptyMonitorVisibilityMap
|
||||
|
||||
export class ReflectionApiError extends Error {
|
||||
code: number
|
||||
data: unknown
|
||||
|
||||
constructor(message: string, code: number, data?: unknown) {
|
||||
super(message)
|
||||
this.name = 'ReflectionApiError'
|
||||
this.code = code
|
||||
this.data = data ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReflectionExecuteResult {
|
||||
returnValue?: string | null
|
||||
accepted?: boolean
|
||||
completed?: boolean
|
||||
}
|
||||
|
||||
/** 根据 execute 返回区分「已完成」与「已受理(后台继续)」文案。 */
|
||||
export function formatReflectionExecuteMessage(
|
||||
label: string,
|
||||
result: ReflectionExecuteResult
|
||||
): string {
|
||||
if (result.returnValue) return `已执行:${result.returnValue}`
|
||||
if (result.accepted && result.completed === false) {
|
||||
return `已受理:${label}(后台继续执行,请稍后在 SimpleLite 查看结果)`
|
||||
}
|
||||
if (result.accepted) return `已执行 ${label}`
|
||||
return `已执行 ${label}`
|
||||
}
|
||||
|
||||
export interface ReflectionExecuteOptions {
|
||||
/** 已在平台侧完成二次确认时带上,对应后端 X-Platform-Confirmed: 1 */
|
||||
platformConfirmed?: boolean
|
||||
}
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const { data } = await http.get<ReflectionEnvelope<T>>(`${BASE}${path}`)
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function post<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T> {
|
||||
const { data } = await http.post<ReflectionEnvelope<T>>(`${BASE}${path}`, null, { params })
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
async function post<T>(
|
||||
path: string,
|
||||
params?: Record<string, string | number | boolean>,
|
||||
headers?: Record<string, string>
|
||||
): Promise<T> {
|
||||
const { data } = await http.post<ReflectionEnvelope<T>>(`${BASE}${path}`, null, { params, headers })
|
||||
if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function del<T>(path: string): Promise<T> {
|
||||
const { data } = await http.delete<ReflectionEnvelope<T>>(`${BASE}${path}`)
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function patchJson<T>(path: string, body: unknown): Promise<T> {
|
||||
const { data } = await http.patch<ReflectionEnvelope<T>>(`${BASE}${path}`, body)
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection PATCH ${path} failed`)
|
||||
if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection PATCH ${path} failed`, data?.code ?? 500, data?.data)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function executeWithPlatformConfirm<T>(
|
||||
path: string,
|
||||
params?: Record<string, string | number | boolean>,
|
||||
opts?: ReflectionExecuteOptions
|
||||
): Promise<T> {
|
||||
const headers = opts?.platformConfirmed ? { 'X-Platform-Confirmed': '1' } : undefined
|
||||
try {
|
||||
return await post<T>(path, params, headers)
|
||||
} catch (e) {
|
||||
if (e instanceof ReflectionApiError && e.code === 428 && !opts?.platformConfirmed) {
|
||||
const confirmMessage =
|
||||
(e.data as { confirmMessage?: string | null } | null)?.confirmMessage?.trim()
|
||||
|| '此操作需要确认'
|
||||
await ElMessageBox.confirm(confirmMessage, '确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
return await post<T>(path, params, { 'X-Platform-Confirmed': '1' })
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const reflectionApi = {
|
||||
listKinds: () => MOCK
|
||||
? Promise.resolve(mockReflectionKinds())
|
||||
@@ -371,16 +438,23 @@ export const reflectionApi = {
|
||||
fieldList?: ReflectionKv[]
|
||||
}>(`/bundle/${kind}/${id}`),
|
||||
|
||||
execute: (kind: ReflectionKind, id: number, method: string, params?: Record<string, string | number | boolean>) => MOCK
|
||||
execute: (
|
||||
kind: ReflectionKind,
|
||||
id: number,
|
||||
method: string,
|
||||
params?: Record<string, string | number | boolean>,
|
||||
opts?: ReflectionExecuteOptions
|
||||
) => MOCK
|
||||
? Promise.resolve(mockReflectionExecute(
|
||||
kind,
|
||||
id,
|
||||
method,
|
||||
Object.fromEntries(Object.entries(params ?? {}).map(([k, v]) => [k, String(v)]))
|
||||
))
|
||||
: post<{ returnValue: string }>(
|
||||
: executeWithPlatformConfirm<ReflectionExecuteResult>(
|
||||
`/execute/${kind}/${id}/${encodeURIComponent(method)}`,
|
||||
params
|
||||
params,
|
||||
opts
|
||||
),
|
||||
|
||||
/** 车辆前往指定站点(Web「去某地」;不依赖 SimpleUI.GetPoint)。 */
|
||||
|
||||
@@ -34,10 +34,15 @@ export interface ToolbarRecordingState {
|
||||
isRecording: boolean
|
||||
mode: string
|
||||
isPlaying: boolean
|
||||
isAutoPlaying?: boolean
|
||||
speed?: number
|
||||
elapsedMs: number
|
||||
durationMs?: number
|
||||
frameCount: number
|
||||
currentFrameIndex?: number
|
||||
droppedFrames: number
|
||||
currentRecordingFile: string | null
|
||||
currentPlaybackFile?: string | null
|
||||
recordingsDirectory: string
|
||||
entries: ToolbarRecordingEntry[]
|
||||
}
|
||||
@@ -122,6 +127,20 @@ export const workspaceToolbarApi = {
|
||||
|
||||
stopPlayback: () => unwrap<ToolbarState>(http.post(`${BASE}/playback/stop`)),
|
||||
|
||||
playPause: () => unwrap<ToolbarState>(http.post(`${BASE}/playback/play-pause`, {})),
|
||||
|
||||
seekElapsed: (elapsedMs: number) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/playback/seek`, { elapsedMs, ElapsedMs: elapsedMs })),
|
||||
|
||||
seekFrame: (frameIndex: number) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/playback/seek`, { frameIndex, FrameIndex: frameIndex })),
|
||||
|
||||
step: (delta = 1) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/playback/step`, { delta, Delta: delta })),
|
||||
|
||||
setSpeed: (speed: number) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/playback/speed`, { speed, Speed: speed })),
|
||||
|
||||
toggleViewMode: () => unwrap<ToolbarState>(http.post(`${BASE}/view/toggle`)),
|
||||
|
||||
setCameraFollow: (carId: number | null, enabled: boolean) =>
|
||||
|
||||
Reference in New Issue
Block a user