Files
Migu2.0/frontends/apps/simple-platform-vue/src/api/reflection.ts
T

891 lines
31 KiB
TypeScript
Raw Normal View History

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'),
2026-06-23 17:22:40 +08:00
/** 车型编码字段元数据(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[]
}
2026-06-23 17:22:40 +08:00
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[]
}