diff --git a/frontends/apps/simple-platform-vue/src/api/reflection.ts b/frontends/apps/simple-platform-vue/src/api/reflection.ts index 9cdf478..5dee0e2 100644 --- a/frontends/apps/simple-platform-vue/src/api/reflection.ts +++ b/frontends/apps/simple-platform-vue/src/api/reflection.ts @@ -243,6 +243,12 @@ async function del(path: string): Promise { return data.data as T } +async function patchJson(path: string, body: unknown): Promise { + const { data } = await http.patch>(`${BASE}${path}`, body) + if (!data?.success) throw new Error(data?.message ?? `reflection PATCH ${path} failed`) + return data.data as T +} + export const reflectionApi = { listKinds: () => MOCK ? Promise.resolve(mockReflectionKinds()) @@ -500,6 +506,55 @@ export const reflectionApi = { // ────────────────────────────────────────────────────────────────────────── // 车型样式(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('/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('/viewport-style', sanitizeViewportPatch(patch)), + // /car-style/types GET 列出所有 Car 子类型样式 // /car-style/{typeFullName} GET 单个车型当前样式 // /car-style/{typeFullName} POST body JSON 全量覆盖 @@ -551,6 +606,117 @@ export const reflectionApi = { : 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 + track?: Partial +} + +/** 将 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 diff --git a/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue b/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue index 9896d09..deb2040 100644 --- a/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue +++ b/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue @@ -136,6 +136,11 @@ function onMessage(ev: MessageEvent) { } else if (msg.type === 'workspace.select' && Array.isArray(msg.payload)) { lastSelect.value = msg.payload as string[] emit('select', msg.payload as string[]) + } else if (msg.type === 'workspace.shortcut' && msg.payload && typeof msg.payload === 'object') { + const action = (msg.payload as { action?: string }).action + if (action === 'undo' || action === 'redo' || action === 'delete') { + window.dispatchEvent(new CustomEvent('workspace-shortcut', { detail: { action } })) + } } } diff --git a/frontends/apps/simple-platform-vue/src/components/map-editor/EditPropertyPanel.vue b/frontends/apps/simple-platform-vue/src/components/map-editor/EditPropertyPanel.vue index bbe32af..6a0ab98 100644 --- a/frontends/apps/simple-platform-vue/src/components/map-editor/EditPropertyPanel.vue +++ b/frontends/apps/simple-platform-vue/src/components/map-editor/EditPropertyPanel.vue @@ -220,32 +220,181 @@ - - - 为「新建站点 / 新建路径」预设默认样式与字段。保存后下次创建即生效。 +
+ + 视口样式与 SimpleLite「视口样式」对话框同步,修改后即时重绘画布;下方「新建默认」仅影响下次创建对象。 - 站点 - - - - - - - - - - 路径 - - - - - - - - - - 保存默认 - + +
+ + 请选中站点或路径 + 在画布上选中一个站点或路径后,可在此调整对应视口样式。 +
+ + + + + +
+ + 加载视口样式… +
+
@@ -280,7 +429,15 @@