补充对齐/批量建站 API,并优化反射面板、历史栈与字段管理相关交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
891 lines
31 KiB
TypeScript
891 lines
31 KiB
TypeScript
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')`
|
||
* - car(DummyCar 兜底,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[]
|
||
}
|