Files
Migu2.0/frontends/apps/simple-platform-vue/src/api/reflection.ts
T
zhaowei.huangandCursor 8299c3dc5c 增强地图编辑 CAD 能力与反射对象管理体验。
补充对齐/批量建站 API,并优化反射面板、历史栈与字段管理相关交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 11:32:30 +08:00

891 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import http from './http'
import { ElMessageBox } from 'element-plus'
import {
mockReflectionAssemblies,
mockReflectionBundle,
mockReflectionDeleteField,
mockReflectionExecute,
mockReflectionFields,
mockReflectionKinds,
mockReflectionMethods,
mockReflectionMethodsByType,
mockReflectionObjects,
mockReflectionSetField,
mockReflectionStatus
} from '@/mock/data/reflection'
// 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关,避免 PROJECTION/USE 双命名造成
// .env 改一个忘改另一个的诡异半 mock 状态。env.d.ts 已声明 VITE_USE_MOCK 类型。
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
/**
* 与 SimpleLite `ReflectionApiController` 一一对应的前端胶水。
* 路径前缀:`/sl/projection/reflection`(迷榖平台 YARP 反代下游 → SimpleLite EmbedIO)。
*
* 该客户端覆盖了 SimpleLite 主体以及通过 plugins/*.dll 动态加载的 Standard 等
* 插件中所有标注了 `MethodMember` 的方法与所有继承自 Car/Mission/CarProgram/Site/Track
* 的子类型,平台前端可据此动态渲染列表、属性、动作按钮。
*/
const BASE = '/sl/projection/reflection'
export type ReflectionKind =
| 'car'
| 'vehicle'
| 'mission'
| 'process'
| 'site'
| 'track'
| 'map'
| 'special'
| 'script'
// 顶级合成 kind:等于 site + track + special 的并集,由 SimpleLite 后端 /objects/scene 直接返回,
// 每行带 subKind 字段,前端 bundle / execute / setField 走对应底层 kind。
| 'scene'
export interface ReflectionEnvelope<T> {
success: boolean
code: number
data: T | null
message: string
}
export interface ReflectionParam {
name: string
typeName: string
hasDefault: boolean
defaultValue?: string | null
}
export interface ReflectionMethod {
methodName: string
label?: string | null
description?: string | null
hint?: string | null
returnType: string
hasParams: boolean
params: ReflectionParam[]
requiresPlatformConfirm?: boolean
confirmMessage?: string | null
}
export interface ReflectionObject {
id: number
name: string
typeName: string
layer?: string | null
status?: string | null
summary?: string | null
/** 当顶级 kind 是合成 kind(如 "scene")时,此字段标记底层真实 kind。 */
subKind?: string | null
}
export interface ReflectionKv {
key: string
value: string
locked?: boolean
/** "typed" = 强类型 [FieldMember],不可删;"dynamic" = Prop.fields 动态字段,可删。 */
source?: 'typed' | 'dynamic'
/** 后端字段的 .NET 类型名(String / Single / Int32 / Boolean 等),用于前端渲染对应控件。 */
typeName?: string
}
export interface ReflectionTypeMethods {
typeName: string
fullTypeName?: string
typeLabel?: string
assemblyName: string
methods: ReflectionMethod[]
}
export interface ReflectionAssembly {
name: string
version?: string
location?: string
}
/** 通过 GET /reflection/types/{kind} 拿到的可创建子类型行(进程 / 脚本 / 车辆管理面板的「新建」下拉用)。 */
export interface ReflectionCreatableType {
typeName: string
shortName: string
label: string
assemblyName: string
}
/** GET /reflection/plugins 返回的单个插件元信息。 */
export interface PluginEntry {
name: string
dllPath: string
assemblyName: string
assemblyVersion: string
collectible: boolean
loadedTypes: number
loadedAt: string
missionTypes: number
carTypes: number
}
export interface ReflectionKindMeta {
kind: ReflectionKind
count: number
label: string
/** "primary" 表示顶级 5 分类;undefined / 其他视为底层 kind。 */
group?: string
}
export interface ReflectionSubKindMeta extends ReflectionKindMeta {
parent: ReflectionKind
}
export interface ReflectionSelection {
kind: ReflectionKind | null
id: number
name: string
typeName?: string
}
export interface ScriptSourcePayload {
id: number
name: string
typeName: string
state?: string | null
script: string
}
export interface ScriptExceptionStatusPayload {
id: number
name: string
typeName: string
car: string
state?: string | null
exception: string
notifies: string
report: string
}
// ──────────────────────────────────────────────────────────────────────────
// 地图监控可见性配置(与后端 MonitorVisibilityConfig / MonitorVisibilityForKind 对应)
//
// `config` = 管理员勾选的白名单。空数组 = 显示全部;非空 = 仅显示列表中的 key。
// `available` = 当前 SimpleLite 进程里能扫描到的全部可勾选项(基类 + 已加载子类 +
// 运行时实例的 Prop.fields / status 反射键)。是 GET 返回的副产物,
// POST 写入时不需要、也不该回传。
// ──────────────────────────────────────────────────────────────────────────
export type MonitorVisibilityKind = 'car' | 'site' | 'track'
export interface MonitorVisibilityForKind {
fields: string[]
status: string[]
methods: string[]
}
export interface MonitorVisibilityMap {
car: MonitorVisibilityForKind
site: MonitorVisibilityForKind
track: MonitorVisibilityForKind
}
/** POST /monitor-config 请求体:三组白名单 + 可选按车型动作。 */
export interface MonitorConfigSaveBody extends MonitorVisibilityMap {
carActionByType?: Record<string, string[]>
}
export interface MonitorConfigPayload {
config: MonitorVisibilityMap & { carActionByType?: Record<string, string[]> }
available: MonitorVisibilityMap
}
const MOCK_MONITOR_STORAGE_KEY = 'simple-platform-mock-monitor-config'
function loadMockMonitorConfig(): MonitorConfigSaveBody {
try {
const raw = localStorage.getItem(MOCK_MONITOR_STORAGE_KEY)
if (raw) return JSON.parse(raw) as MonitorConfigSaveBody
} catch { /* ignore */ }
return emptyMonitorVisibilityMap()
}
function saveMockMonitorConfig(cfg: MonitorConfigSaveBody) {
try {
localStorage.setItem(MOCK_MONITOR_STORAGE_KEY, JSON.stringify(cfg))
} catch { /* ignore */ }
}
export function emptyMonitorVisibilityForKind(): MonitorVisibilityForKind {
return { fields: [], status: [], methods: [] }
}
export function emptyMonitorVisibilityMap(): MonitorVisibilityMap {
return {
car: emptyMonitorVisibilityForKind(),
site: emptyMonitorVisibilityForKind(),
track: emptyMonitorVisibilityForKind()
}
}
// 语义上 available 与 config 同构(都是按 kind 分组的三组 key 列表),所以共用一个工厂。
// 单独保留命名是为了表达"业务含义不同"——可勾选全集 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 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>,
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 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 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())
: get<{ kinds: ReflectionKindMeta[]; subKinds?: ReflectionSubKindMeta[] }>('/kinds'),
listAssemblies: () => MOCK
? Promise.resolve(mockReflectionAssemblies())
: get<ReflectionAssembly[]>('/assemblies'),
listObjects: (kind: ReflectionKind) => MOCK
? Promise.resolve(mockReflectionObjects(kind))
: get<ReflectionObject[]>(`/objects/${kind}`),
/** 列出当前可实例化的子类型(用于「新建」下拉);mock 模式直接给空。 */
listCreatableTypes: (kind: ReflectionKind): Promise<ReflectionCreatableType[]> => MOCK
? Promise.resolve([])
: get<ReflectionCreatableType[]>(`/types/${kind}`),
/**
* 创建一条对象。底层 POST `/objects/{kind}?...`,所有 extras 字段都作为 query 传给后端。
*
* 三类典型用法:
* - process/script + UiDiscoveryCache 子类:`createObject('process', 'TaskFlow')`
* - carDummyCar 兜底,typeName 可空):`createObject('car', '', { name: 'AGV-1', x: 0, y: 0 })`
* - site/track/image/text/model`createObject('site', '', { x: 100, y: 200, name: 'A' })`
*
* 后端 BuildAndPersist 会做完整的字段类型转换;前端只负责传字符串。
*/
createObject: (
kind: ReflectionKind,
typeName?: string,
extras?: Record<string, string | number | boolean | undefined>
) => {
if (MOCK) return Promise.resolve({ kind, id: -1, typeName: typeName ?? '' })
const params: Record<string, string | number | boolean> = {}
if (typeName) params.typeName = typeName
if (extras) {
for (const [k, v] of Object.entries(extras)) {
if (v === undefined || v === null || v === '') continue
params[k] = v
}
}
return post<{ kind: string; id: number; typeName: string }>(`/objects/${kind}`, params)
},
/** 删除一条对象(process / script / car / site / track / special)。 */
deleteObject: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve({ kind, id, deleted: true })
: del<{ kind: string; id: number; deleted: boolean }>(`/objects/${kind}/${id}`),
/**
* 触发 SimpleLite 重新扫描 ./plugins 目录、加载新增 dll 并重建 UiDiscoveryCache。
* 已加载的 dll 不会重复加载。
* 返回:本次发现的 dll 总数、新加载的 assembly 数、当前可创建的 mission/car 类型计数。
*/
reloadPlugins: () => MOCK
? Promise.resolve({ totalDlls: 0, newlyLoaded: 0, failed: 0, missionTypes: 0, carTypes: 0 })
: post<{ totalDlls: number; newlyLoaded: number; failed: number; missionTypes: number; carTypes: number }>(
'/plugins/reload'
),
/** 列出当前已加载的所有 collectible 插件(PluginManager 跟踪范围内)。 */
listPlugins: () => MOCK
? Promise.resolve<PluginEntry[]>([])
: get<PluginEntry[]>('/plugins'),
/**
* 卸载一个 collectible 插件。
* 失败原因常见:仍有 Mission / Car 实例占用插件类型 → 409。
*/
unloadPlugin: (name: string) => MOCK
? Promise.resolve({ name, message: 'mock', missionTypes: 0, carTypes: 0 })
: post<{ name: string; message: string; missionTypes: number; carTypes: number }>(
`/plugins/${encodeURIComponent(name)}/unload`
),
listMethods: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionMethods(kind))
: get<ReflectionMethod[]>(`/methods/${kind}/${id}`),
listMethodsByType: (kind: ReflectionKind) => MOCK
? Promise.resolve(mockReflectionMethodsByType(kind))
: get<ReflectionTypeMethods[]>(`/methods-by-type/${kind}`),
getStatus: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionStatus(kind, id))
: get<ReflectionKv[]>(`/status/${kind}/${id}`),
getFields: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionFields(kind, id))
: get<ReflectionKv[]>(`/fields/${kind}/${id}`),
setField: (kind: ReflectionKind, id: number, field: string, value: string) => MOCK
? Promise.resolve(mockReflectionSetField(kind, id, field, value))
: post<{ kind: ReflectionKind; id: number; field: string; value: string }>(
`/fields/${kind}/${id}/${encodeURIComponent(field)}`,
{ value }
),
deleteField: (kind: ReflectionKind, id: number, field: string) => MOCK
? Promise.resolve(mockReflectionDeleteField(kind, id, field))
: del<{ kind: ReflectionKind; id: number; field: string }>(
`/fields/${kind}/${id}/${encodeURIComponent(field)}`
),
getBundle: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionBundle(kind, id))
: get<{
kind: string
id: number
typeName: string
fullTypeName?: string
assembly: string
summary: ReflectionObject
methods: ReflectionMethod[]
status: ReflectionKv[]
/** 扁平 key→value 兼容旧组件。 */
fields: Record<string, string>
/** 带 source/locked/typeName 的字段表,新版「对象管理」面板用。 */
fieldList?: ReflectionKv[]
}>(`/bundle/${kind}/${id}`),
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)]))
))
: executeWithPlatformConfirm<ReflectionExecuteResult>(
`/execute/${kind}/${id}/${encodeURIComponent(method)}`,
params,
opts
),
/** 车辆前往指定站点(Web「去某地」;不依赖 SimpleUI.GetPoint)。 */
gotoCarSite: (carId: number, siteId: number) => MOCK
? Promise.resolve({ carId, siteId, message: `mock goto ${carId} -> ${siteId}` })
: post<{ carId: number; siteId: number; message: string }>(
`/car/${carId}/goto-site`,
{ siteId }
),
getScriptSource: (id: number): Promise<ScriptSourcePayload> => MOCK
? Promise.resolve({
id,
name: `MockScript#${id}`,
typeName: 'CarProgram',
state: 'Running',
script: '// mock script'
})
: get<ScriptSourcePayload>(`/scripts/${id}/source`),
getScriptExceptionStatus: (id: number): Promise<ScriptExceptionStatusPayload> => MOCK
? Promise.resolve({
id,
name: `MockScript#${id}`,
typeName: 'CarProgram',
car: 'AGV-1(#1)',
state: 'Running',
exception: '(无)',
notifies: '(无)',
report: '=== CarProgram 异常状态报告 ===\nstate : Running\n\n--- exception ---\n(无)'
})
: get<ScriptExceptionStatusPayload>(`/scripts/${id}/exception-status`),
// ──────────────────────────────────────────────────────────────────────────
// 地图监控配置(Configuration.conf.monitorVisibility
// /monitor-config GET 现有配置 + 各 kind 可勾选 fields/status/methods 全集
// /monitor-config POST body JSON 全量覆盖
// 空白名单 = 显示全部;非空 = 仅显示列表中的 key。
// ──────────────────────────────────────────────────────────────────────────
getMonitorConfig: (): Promise<MonitorConfigPayload> => MOCK
? Promise.resolve({
config: loadMockMonitorConfig(),
available: emptyMonitorAvailableMap()
})
: get<MonitorConfigPayload>('/monitor-config'),
saveMonitorConfig: async (cfg: MonitorConfigSaveBody): Promise<MonitorConfigSaveBody> => {
if (MOCK) {
saveMockMonitorConfig(cfg)
return cfg
}
const { data } = await http.post<ReflectionEnvelope<MonitorConfigSaveBody>>(
`${BASE}/monitor-config`,
cfg
)
if (!data?.success) throw new Error(data?.message ?? 'saveMonitorConfig failed')
return data.data as MonitorConfigSaveBody
},
// 选中同步:让 SimpleLite 3D 场景同步高亮被点击对象
getSelection: () => MOCK
? Promise.resolve<ReflectionSelection>({ kind: null, id: 0, name: '' })
: get<ReflectionSelection>('/selection'),
setSelection: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve({ kind, id, name: `mock(${kind}#${id})` })
: post<{ kind: ReflectionKind; id: number; name: string }>('/selection', { kind, id }),
clearSelection: () => MOCK
? Promise.resolve({ cleared: true })
: post<{ cleared: boolean }>('/selection/clear'),
// ──────────────────────────────────────────────────────────────────────────
// 项目属性(Scene.conf 单例)
// /project/fields GET → 列出 Scene.conf 全部 [FieldMember] 字段
// /project/fields/{key} POST → ?value=xxx 写入单字段
// /project/save POST → 把内存项目(含修改后的 conf)写回 JSON
// ──────────────────────────────────────────────────────────────────────────
getProjectFields: (): Promise<ProjectPropertiesPayload> => MOCK
? Promise.resolve({
target: 'Scene.conf',
lastLoadedPath: null,
autoloadPath: null,
fields: []
})
: get<ProjectPropertiesPayload>('/project/fields'),
setProjectField: (field: string, value: string) => MOCK
? Promise.resolve({ field, value })
: post<{ field: string; value: string }>(
`/project/fields/${encodeURIComponent(field)}`,
{ value }
),
/** 保存当前项目到磁盘。path 为空则后端用 LastLoadedPath / Configuration.conf.autoload。 */
saveProject: (path?: string) => MOCK
? Promise.resolve({ path: path ?? '(mock)' })
: post<{ path: string }>('/project/save', path ? { path } : undefined),
// ──────────────────────────────────────────────────────────────────────────
// 核心配置(simple.json / Configuration.conf
// /app-config/fields GET → 列出 Configuration.conf 全部 [FieldMember]
// /app-config/fields/{key} POST → ?value=xxx 写入单字段
// /app-config/save POST → 调 Configuration.ToFile("simple.json")
// ──────────────────────────────────────────────────────────────────────────
getAppConfigFields: (): Promise<AppConfigPayload> => MOCK
? Promise.resolve({ target: 'Configuration.conf', savePath: 'simple.json', fields: [] })
: get<AppConfigPayload>('/app-config/fields'),
setAppConfigField: (field: string, value: string) => MOCK
? Promise.resolve({ field, value })
: post<{ field: string; value: string }>(
`/app-config/fields/${encodeURIComponent(field)}`,
{ value }
),
saveAppConfig: () => MOCK
? Promise.resolve({ path: 'simple.json' })
: post<{ path: string }>('/app-config/save'),
// ──────────────────────────────────────────────────────────────────────────
// 车型样式(WorkspaceCarStylesByType / WorkspaceAlarmColorScheme
// /viewport-style GET 站点/路径视口 Painter 样式
// /viewport-style PATCH 部分更新并立即重绘画布
getViewportStyle: () => MOCK
? Promise.resolve({
site: {
drawScale: 2.5,
dotRadiusNormal: 4,
dotRadiusSelected: 6,
selectionRingM: 0.14,
colorNormalArgb: 0xffffffff,
colorSelectedArgb: 0xffff0000,
labelColorNormalArgb: 0xffffffff,
labelColorSelectedArgb: 0xffff0000
},
track: {
drawScale: 1.5,
widthNormal: 1.2,
widthSelected: 2.5,
showDirectionArrows: true,
colorNormalArgb: 0xff00ffff,
colorSelectedArgb: 0xffff0000
}
} satisfies ViewportStylePayload)
: get<ViewportStylePayload>('/viewport-style'),
patchViewportStyle: (patch: ViewportStylePatch) => MOCK
? Promise.resolve({
site: {
drawScale: 2.5,
dotRadiusNormal: 4,
dotRadiusSelected: 6,
selectionRingM: 0.14,
colorNormalArgb: 0xffffffff,
colorSelectedArgb: 0xffff0000,
labelColorNormalArgb: 0xffffffff,
labelColorSelectedArgb: 0xffff0000
},
track: {
drawScale: 1.5,
widthNormal: 1.2,
widthSelected: 2.5,
showDirectionArrows: true,
colorNormalArgb: 0xff00ffff,
colorSelectedArgb: 0xffff0000
}
} satisfies ViewportStylePayload)
: patchJson<ViewportStylePayload>('/viewport-style', sanitizeViewportPatch(patch)),
// /car-style/types GET 列出所有 Car 子类型样式
// /car-style/{typeFullName} GET 单个车型当前样式
// /car-style/{typeFullName} POST body JSON 全量覆盖
// /car-style/{typeFullName} DELETE 恢复为默认
// /car-style/alarm-colors GET 7 种报警键 → 颜色
// /car-style/alarm-colors POST body JSON 全量覆盖映射
// /car-style/save POST 写回 simple.json
// ──────────────────────────────────────────────────────────────────────────
getCarStyleTypes: () => MOCK
? Promise.resolve<CarStyleTypesPayload>({ globalDefault: defaultCarStyleDto(), types: [] })
: get<CarStyleTypesPayload>('/car-style/types'),
/** 车型编码字段元数据(site/track/plan/car 四类字段及默认值)。 */
getCarTypeCoderFields: () => MOCK
? Promise.resolve<CarTypeCoderFieldsRow[]>([])
: get<CarTypeCoderFieldsRow[]>('/car-types/coder-fields'),
getCarStyle: (typeFullName: string) => MOCK
? Promise.resolve(defaultCarStyleDto())
: get<CarStyleDto>(`/car-style/${encodeURIComponent(typeFullName)}`),
/** 用 axios 直接以 JSON body 提交,避免拼 query。 */
putCarStyle: async (typeFullName: string, body: CarStyleDto) => {
if (MOCK) return body
const { data } = await http.post<ReflectionEnvelope<CarStyleDto>>(
`${BASE}/car-style/${encodeURIComponent(typeFullName)}`,
body
)
if (!data?.success) throw new Error(data?.message ?? 'putCarStyle failed')
return data.data as CarStyleDto
},
deleteCarStyle: (typeFullName: string) => MOCK
? Promise.resolve({ typeFullName, removed: true })
: del<{ typeFullName: string; removed: boolean }>(`/car-style/${encodeURIComponent(typeFullName)}`),
getAlarmColors: () => MOCK
? Promise.resolve<AlarmColorsPayload>({ defaultKeys: [], entries: [] })
: get<AlarmColorsPayload>('/car-style/alarm-colors'),
putAlarmColors: async (palette: Record<string, number>) => {
if (MOCK) return { count: Object.keys(palette).length }
const { data } = await http.post<ReflectionEnvelope<{ count: number }>>(
`${BASE}/car-style/alarm-colors`,
palette
)
if (!data?.success) throw new Error(data?.message ?? 'putAlarmColors failed')
return data.data as { count: number }
},
saveCarStyle: () => MOCK
? Promise.resolve({ path: 'simple.json' })
: post<{ path: string }>('/car-style/save')
}
/** 与 SimpleLite 视口样式对话框 / Configuration.conf.viewport 同步。 */
export interface ViewportStyleSite {
drawScale: number
dotRadiusNormal: number
dotRadiusSelected: number
selectionRingM: number
colorNormalArgb: number
colorSelectedArgb: number
labelColorNormalArgb: number
labelColorSelectedArgb: number
}
export interface ViewportStyleTrack {
drawScale: number
widthNormal: number
widthSelected: number
showDirectionArrows: boolean
colorNormalArgb: number
colorSelectedArgb: number
}
export interface ViewportStylePayload {
site: ViewportStyleSite
track: ViewportStyleTrack
}
export type ViewportStylePatch = {
site?: Partial<ViewportStyleSite>
track?: Partial<ViewportStyleTrack>
}
/** 将 ARGB 规范为无符号 32 位,避免 JSON 序列化成负数导致后端 UInt32 反序列化失败。 */
export function normalizeArgb(argb: number): number {
if (!Number.isFinite(argb)) return 0
return argb >>> 0
}
export function argbToHex(argb: number): string {
const u = normalizeArgb(argb)
const a = (u >>> 24) & 0xff
const r = (u >>> 16) & 0xff
const g = (u >>> 8) & 0xff
const b = u & 0xff
if (a >= 255) return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
return `#${a.toString(16).padStart(2, '0')}${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
export function hexToArgb(hex: string): number {
const h = hex.replace('#', '').trim()
if (h.length === 6) {
const r = parseInt(h.slice(0, 2), 16)
const g = parseInt(h.slice(2, 4), 16)
const b = parseInt(h.slice(4, 6), 16)
return normalizeArgb((0xff << 24) | (r << 16) | (g << 8) | b)
}
if (h.length === 8) {
const a = parseInt(h.slice(0, 2), 16)
const r = parseInt(h.slice(2, 4), 16)
const g = parseInt(h.slice(4, 6), 16)
const b = parseInt(h.slice(6, 8), 16)
return normalizeArgb((a << 24) | (r << 16) | (g << 8) | b)
}
return 0xffffffff
}
const VIEWPORT_SITE_ARGB_KEYS = [
'colorNormalArgb',
'colorSelectedArgb',
'labelColorNormalArgb',
'labelColorSelectedArgb'
] as const
const VIEWPORT_TRACK_ARGB_KEYS = ['colorNormalArgb', 'colorSelectedArgb'] as const
/** PATCH 前把视口样式中的 ARGB 字段转为 UInt32 可接受的非负整数。 */
export function sanitizeViewportPatch(patch: ViewportStylePatch): ViewportStylePatch {
const out: ViewportStylePatch = {}
if (patch.site) {
const site = { ...patch.site }
for (const k of VIEWPORT_SITE_ARGB_KEYS) {
if (site[k] != null) site[k] = normalizeArgb(site[k]!)
}
out.site = site
}
if (patch.track) {
const track = { ...patch.track }
for (const k of VIEWPORT_TRACK_ARGB_KEYS) {
if (track[k] != null) track[k] = normalizeArgb(track[k]!)
}
out.track = track
}
return out
}
export function normalizeViewportPayload(raw: ViewportStylePayload): ViewportStylePayload {
return {
site: {
...raw.site,
colorNormalArgb: normalizeArgb(raw.site.colorNormalArgb),
colorSelectedArgb: normalizeArgb(raw.site.colorSelectedArgb),
labelColorNormalArgb: normalizeArgb(raw.site.labelColorNormalArgb),
labelColorSelectedArgb: normalizeArgb(raw.site.labelColorSelectedArgb)
},
track: {
...raw.track,
colorNormalArgb: normalizeArgb(raw.track.colorNormalArgb),
colorSelectedArgb: normalizeArgb(raw.track.colorSelectedArgb)
}
}
}
export interface CarStyleDto {
bodyLengthM: number
bodyWidthM: number
bodyColorArgb: number
outlineColorArgb: number
labelColorArgb: number
showLabel: boolean
modelPath: string
}
export interface CarStyleTypeRow {
typeName: string
shortName: string
label: string
assemblyName: string
hasOverride: boolean
style: CarStyleDto
}
export interface CarStyleTypesPayload {
globalDefault: CarStyleDto
types: CarStyleTypeRow[]
}
export interface CoderFieldDef {
name: string
typeName: string
defaultValue: unknown
}
export interface CoderFieldGroup {
typeName: string
shortName: string
assemblyName: string
baseTypeName?: string
fields: CoderFieldDef[]
}
export interface CarTypeCoderFieldsRow {
typeName: string
shortName: string
label: string
assemblyName: string
siteFields: CoderFieldGroup
trackFields: CoderFieldGroup
planFields: CoderFieldGroup
carFields: CoderFieldGroup
}
export interface AlarmColorEntry {
key: string
colorArgb: number
isDefault: boolean
}
export interface AlarmColorsPayload {
defaultKeys: string[]
entries: AlarmColorEntry[]
}
function defaultCarStyleDto(): CarStyleDto {
return {
bodyLengthM: 0.64,
bodyWidthM: 0.42,
bodyColorArgb: 0xffffffff,
outlineColorArgb: 0xff2a2a2a,
labelColorArgb: 0xffffffff,
showLabel: true,
modelPath: ''
}
}
export interface ProjectPropertyRow {
key: string
label: string
value: string
typeName: string
locked: boolean
}
export interface ProjectPropertiesPayload {
target: string
lastLoadedPath: string | null
autoloadPath: string | null
fields: ProjectPropertyRow[]
}
export interface AppConfigPayload {
target: string
savePath: string
fields: ProjectPropertyRow[]
}