diff --git a/frontends/apps/simple-platform-vue/src/components/ConfigPageBase.vue b/frontends/apps/simple-platform-vue/src/components/ConfigPageBase.vue index 0db98eb..7f00bbe 100644 --- a/frontends/apps/simple-platform-vue/src/components/ConfigPageBase.vue +++ b/frontends/apps/simple-platform-vue/src/components/ConfigPageBase.vue @@ -142,10 +142,10 @@ onMounted(() => reload()) diff --git a/frontends/apps/simple-platform-vue/src/components/reflection/ReflectionManagerPanel.vue b/frontends/apps/simple-platform-vue/src/components/reflection/ReflectionManagerPanel.vue index ef7bc10..20961a3 100644 --- a/frontends/apps/simple-platform-vue/src/components/reflection/ReflectionManagerPanel.vue +++ b/frontends/apps/simple-platform-vue/src/components/reflection/ReflectionManagerPanel.vue @@ -1011,7 +1011,7 @@ onMounted(refresh) display: flex; flex-direction: column; gap: 12px; - color: rgba(232, 215, 245, 0.92); + color: var(--mg-text-light); } .ref-mgr-header { display: flex; @@ -1019,7 +1019,7 @@ onMounted(refresh) justify-content: space-between; gap: 12px; padding: 6px 0 4px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); + border-bottom: 1px solid var(--mg-divider); } .ref-mgr-title { margin: 0; @@ -1038,7 +1038,7 @@ onMounted(refresh) .ref-type-asm { margin-left: 6px; - color: rgba(180, 160, 220, 0.6); + color: var(--mg-text-dim); font-size: 11px; } @@ -1073,7 +1073,7 @@ onMounted(refresh) justify-content: space-between; margin-bottom: 14px; padding-bottom: 10px; - border-bottom: 1px dashed rgba(255, 255, 255, 0.10); + border-bottom: 1px dashed var(--mg-veil-border); } .detail-title { display: flex; @@ -1086,7 +1086,7 @@ onMounted(refresh) gap: 8px; } .detail-id { - color: rgba(232, 215, 245, 0.65); + color: var(--mg-text-dim); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12.5px; } @@ -1111,7 +1111,7 @@ onMounted(refresh) letter-spacing: 0.4px; } .field-empty { - color: rgba(232, 215, 245, 0.55); + color: var(--mg-text-dim); font-size: 12px; padding: 6px 2px; } @@ -1124,7 +1124,7 @@ onMounted(refresh) .status-value--array { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; - color: rgba(210, 190, 255, 0.95); + color: var(--mg-text-muted); } .method-grid { display: grid; @@ -1237,12 +1237,12 @@ onMounted(refresh) .field-num :deep(.el-input-number) { width: 100%; } :deep(.el-descriptions__title) { - color: rgba(255, 255, 255, 0.85) !important; + color: var(--mg-text-light) !important; font-size: 13px; font-weight: 600; } -:deep(.el-descriptions__label) { color: rgba(232, 215, 245, 0.85); } -:deep(.el-descriptions__content) { color: #fff; } +:deep(.el-descriptions__label) { color: var(--mg-text-muted); } +:deep(.el-descriptions__content) { color: var(--mg-text-light); } :deep(.el-table) { background: transparent; } @@ -1251,13 +1251,40 @@ onMounted(refresh) :deep(.el-table th.el-table__cell), :deep(.el-table td.el-table__cell) { background: transparent !important; - color: rgba(232, 215, 245, 0.92); - border-color: rgba(255, 255, 255, 0.08); + color: var(--mg-text-light); + border-color: var(--mg-divider); } :deep(.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell) { - background: rgba(255, 255, 255, 0.04) !important; + background: var(--mg-veil-1) !important; } :deep(.el-table__body tr.current-row > td.el-table__cell) { background: rgba(170, 110, 250, 0.20) !important; } + +/* ── 浅紫主题:卡片变白后,半透明渐变动作按钮上的白字会看不清, + 改为实色紫底白字(对比 ~6.9:1,过 WCAG AA)。scoped 限本组件,不影响地图编辑画布。 ── */ +:root[data-theme="fame-lavender"] .method-btn, +:root[data-theme="fame-lavender"] .header-action-btn { + background: #7c3aed; + border-color: #7c3aed; + color: #fff; +} +:root[data-theme="fame-lavender"] .method-btn:hover:not(:disabled), +:root[data-theme="fame-lavender"] .header-action-btn:hover { + background: #6d28d9; + border-color: #6d28d9; + box-shadow: 0 4px 14px rgba(124, 58, 237, 0.35); +} +:root[data-theme="fame-lavender"] .method-btn-icon { color: rgba(255, 255, 255, 0.85); } +:root[data-theme="fame-lavender"] .method-btn-tag { background: rgba(255, 255, 255, 0.28); } +:root[data-theme="fame-lavender"] .add-field-btn { + background: rgba(124, 58, 237, 0.12); + border-color: rgba(124, 58, 237, 0.5); + color: #6d28d9; +} +:root[data-theme="fame-lavender"] .add-field-btn:hover { + background: rgba(124, 58, 237, 0.22); + border-color: rgba(124, 58, 237, 0.7); + color: #5b21b6; +} diff --git a/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue b/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue index ea3c2d7..101f19e 100644 --- a/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue +++ b/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue @@ -39,7 +39,7 @@ -
+
操作
-
+ +
@@ -140,7 +158,7 @@
-
+
动作
@@ -258,11 +248,6 @@ function batteryLevel(p: number): 'low' | 'mid' | 'high' { return 'high' } -function shortType(typeName: string): string { - const idx = typeName.lastIndexOf('.') - return idx >= 0 ? typeName.slice(idx + 1) : typeName -} - const filteredCars = computed(() => { const q = search.value.trim().toLowerCase() const tokens = q ? q.split(/\s+/).filter(Boolean) : [] @@ -312,8 +297,8 @@ function detailIdForCar(car: Car): string { flex: none; } .overview-item { - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.12); + background: var(--mg-veil-1); + border: 1px solid var(--mg-veil-border); border-radius: 8px; padding: 6px 4px; text-align: center; @@ -326,16 +311,16 @@ function detailIdForCar(car: Car): string { .overview-num { font-size: 18px; font-weight: 600; - color: #fff; + color: var(--mg-text-light); line-height: 1.2; } .overview-num.online { color: var(--mg-accent); text-shadow: 0 0 12px rgba(var(--mg-accent-rgb), 0.55); } -.overview-num.running { color: #b6e7a3; text-shadow: 0 0 12px rgba(103, 194, 58, 0.45); } +.overview-num.running { color: var(--mg-status-success); text-shadow: 0 0 12px rgba(var(--mg-status-success-rgb), 0.45); } .overview-num.charging { color: var(--mg-primary-hover); text-shadow: 0 0 12px rgba(var(--mg-primary-hover-rgb), 0.55); } -.overview-num.fault { color: #ff9bb8; text-shadow: 0 0 12px rgba(245, 108, 108, 0.5); } +.overview-num.fault { color: var(--mg-status-danger); text-shadow: 0 0 12px rgba(var(--mg-status-danger-rgb), 0.5); } .overview-label { font-size: 11px; - color: rgba(255, 255, 255, 0.7); + color: var(--mg-text-muted); margin-top: 2px; } @@ -358,14 +343,14 @@ function detailIdForCar(car: Car): string { } .vehicle-row { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - padding: 8px 10px; + background: var(--mg-veil-1); + border: 1px solid var(--mg-veil-border); + border-radius: 6px; + padding: 6px 8px; cursor: pointer; display: flex; flex-direction: column; - gap: 4px; + gap: 3px; transition: all 0.18s ease; } .vehicle-row:hover { @@ -381,25 +366,20 @@ function detailIdForCar(car: Car): string { .vehicle-head { display: flex; align-items: center; - justify-content: space-between; - gap: 8px; -} -.vehicle-id-name { - display: flex; - align-items: baseline; gap: 6px; - min-width: 0; } .vid { - font-size: 12px; - color: rgba(255, 255, 255, 0.7); + font-size: 11.5px; + color: var(--mg-text-muted); font-family: ui-monospace, 'SFMono-Regular', 'Menlo', monospace; flex: none; } .vname { - color: #fff; + flex: 1; + min-width: 0; + color: var(--mg-text-light); font-weight: 600; - font-size: 13px; + font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -422,7 +402,7 @@ function detailIdForCar(car: Car): string { .dot-charging { color: var(--mg-primary-hover); box-shadow: 0 0 8px var(--mg-primary-hover); } .dot-paused { color: #e6a23c; box-shadow: 0 0 6px #e6a23c; } .dot-fault { color: #ff7396; box-shadow: 0 0 8px #ff7396; animation: pulse 0.9s infinite; } -.dot-offline { color: rgba(255, 255, 255, 0.35); } +.dot-offline { color: var(--mg-text-faint); } .dot-idle { color: var(--mg-accent); box-shadow: 0 0 6px rgba(var(--mg-accent-rgb), 0.6); } @keyframes pulse { @@ -430,55 +410,54 @@ function detailIdForCar(car: Car): string { 50% { opacity: 0.35; } } -.vehicle-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - font-size: 11px; - line-height: 1.3; -} -.meta-item { white-space: nowrap; } - -.battery-row, -.mission-row { +.vehicle-sub { display: flex; align-items: center; - gap: 8px; - font-size: 12px; + gap: 10px; + font-size: 11.5px; } -.battery-label, -.mission-label { - width: 32px; - color: rgba(255, 255, 255, 0.65); - flex: none; +.battery-mini { + display: flex; + align-items: center; + gap: 6px; + flex: 0 0 44%; + min-width: 0; +} +.mission-mini { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; } .battery-bar { flex: 1; min-width: 0; } .battery-bar :deep(.el-progress-bar__outer) { - background: rgba(255, 255, 255, 0.1); + background: var(--mg-veil-3); } .battery-text { - width: 38px; + width: 34px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; + font-size: 11.5px; flex: none; } -.battery-text.low { color: #f56c6c; } -.battery-text.mid { color: #e6a23c; } -.battery-text.high { color: #67c23a; } +.battery-text.low { color: var(--mg-status-danger); } +.battery-text.mid { color: var(--mg-status-warning); } +.battery-text.high { color: var(--mg-status-success); } .mission-name { flex: 1; min-width: 0; - color: #fff; + color: var(--mg-text-light); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.mission-name.muted { color: rgba(255, 255, 255, 0.5); } +.mission-name.muted { color: var(--mg-text-dim); } .mission-tag { flex: none; } .empty { @@ -486,5 +465,5 @@ function detailIdForCar(car: Car): string { padding: 24px 0; font-size: 12px; } -.muted { color: rgba(255, 255, 255, 0.7); } +.muted { color: var(--mg-text-muted); } diff --git a/frontends/apps/simple-platform-vue/src/components/workbench/WorkbenchSidePanel.vue b/frontends/apps/simple-platform-vue/src/components/workbench/WorkbenchSidePanel.vue index 5f781c8..4697d47 100644 --- a/frontends/apps/simple-platform-vue/src/components/workbench/WorkbenchSidePanel.vue +++ b/frontends/apps/simple-platform-vue/src/components/workbench/WorkbenchSidePanel.vue @@ -162,15 +162,15 @@ onMounted(loadList) .workbench-panel :deep(.el-tabs__item), .workbench-panel :deep(.el-table .cell), .workbench-panel :deep(.el-input__inner) { - color: #fff; + color: var(--mg-text-light); } -.toolbar-hint { color: rgba(255, 255, 255, 0.88); } -.muted { color: rgba(255, 255, 255, 0.75); } +.toolbar-hint { color: var(--mg-text-muted); } +.muted { color: var(--mg-text-muted); } :deep(.el-table th.el-table__cell) { - color: rgba(255, 255, 255, 0.9) !important; + color: var(--mg-text-muted) !important; } :deep(.is-selected-row) td { background: rgba(var(--mg-primary-hover-rgb), 0.28) !important; - color: #fff !important; + color: var(--mg-text-light) !important; } diff --git a/frontends/apps/simple-platform-vue/src/styles/theme.css b/frontends/apps/simple-platform-vue/src/styles/theme.css index d14d814..6d0d763 100644 --- a/frontends/apps/simple-platform-vue/src/styles/theme.css +++ b/frontends/apps/simple-platform-vue/src/styles/theme.css @@ -115,6 +115,24 @@ --el-color-primary-dark-2: var(--mg-primary-dark-2); --mg-transition: all .25s cubic-bezier(.25, .8, .25, 1); + + /* ── 通用「内嵌表面」veil 令牌 ── + * 面板里的嵌套行 / 子卡 / 统计项常用 rgba(255,255,255,.0x) 当作浅色微透表面, + * 在深色主题里是「白色微透」效果;但 fame-lavender 浅紫主题切到白底后这些白veil + * 直接消失(白叠白)。把它们抽成令牌:深色主题保持白微透,fame-lavender 翻成紫微透, + * 这样所有引用处自动双主题可见,且不影响 6 套深色主题观感。 */ + --mg-veil-1: rgba(255, 255, 255, 0.05); + --mg-veil-2: rgba(255, 255, 255, 0.08); + --mg-veil-3: rgba(255, 255, 255, 0.12); + --mg-veil-border: rgba(255, 255, 255, 0.12); + --mg-veil-border-hi: rgba(255, 255, 255, 0.20); + + /* ── 面板「动作按钮」令牌 ── + * 深色主题:半透明紫玻璃底 + 白字;fame-lavender:实色紫底 + 白字(对比足够)。 */ + --mg-action-bg: rgba(76, 29, 149, 0.45); + --mg-action-bg-hover: rgba(109, 40, 217, 0.75); + --mg-action-border: rgba(167, 139, 250, 0.55); + --mg-action-border-hi: rgba(196, 181, 253, 0.9); } html { @@ -920,6 +938,19 @@ a:hover { color: rgba(var(--mg-accent-rgb), 0.85); } --mg-status-info-rgb: 29, 78, 216; --mg-status-idle: #64748b; --mg-status-idle-rgb: 100, 116, 139; + + /* 内嵌表面 veil:浅紫主题翻成紫色微透,保证白底卡片内的嵌套行 / 子卡可见 */ + --mg-veil-1: rgba(124, 58, 237, 0.05); + --mg-veil-2: rgba(124, 58, 237, 0.10); + --mg-veil-3: rgba(124, 58, 237, 0.16); + --mg-veil-border: rgba(124, 58, 237, 0.16); + --mg-veil-border-hi: rgba(124, 58, 237, 0.32); + + /* 动作按钮:实色紫底 + 白字(对比 ~6.9:1,WCAG AA 通过) */ + --mg-action-bg: #7c3aed; + --mg-action-bg-hover: #6d28d9; + --mg-action-border: #7c3aed; + --mg-action-border-hi: #6d28d9; } /* body:纯净紫白渐变(更柔和、长时间阅读不刺眼) */ diff --git a/frontends/apps/simple-platform-vue/src/utils/carActionExecute.ts b/frontends/apps/simple-platform-vue/src/utils/carActionExecute.ts index 6305e7a..cca9aeb 100644 --- a/frontends/apps/simple-platform-vue/src/utils/carActionExecute.ts +++ b/frontends/apps/simple-platform-vue/src/utils/carActionExecute.ts @@ -47,14 +47,41 @@ export async function pickTargetSiteOnMap(): Promise { } } +function isHttp404(e: unknown): boolean { + if (e && typeof e === 'object' && 'response' in e) { + const status = (e as { response?: { status?: number } }).response?.status + if (status === 404) return true + } + const msg = e instanceof Error ? e.message : String(e) + return /status code 404/i.test(msg) +} + export async function executeCarGotoSite(carId: number, siteId: number): Promise { try { const r = await reflectionApi.gotoCarSite(carId, siteId) ElMessage.success(r.message || `已下发前往站点 #${siteId}`) return true } catch (e) { - ElMessage.error(`前往站点失败:${e instanceof Error ? e.message : String(e)}`) - return false + if (!isHttp404(e)) { + ElMessage.error(`前往站点失败:${e instanceof Error ? e.message : String(e)}`) + return false + } + // goto-site 路由未加载(旧 DLL):回退 execute WebGotoSite(DummyCar / Car 上均有) + try { + const r2 = await reflectionApi.execute('car', carId, 'WebGotoSite', { siteId }) + ElMessage.success(r2.returnValue ? `已执行:${r2.returnValue}` : `已下发前往站点 #${siteId}`) + return true + } catch (e2) { + const msg2 = e2 instanceof Error ? e2.message : String(e2) + const needDeploy = + /WebGotoSite|goto-site|不存在可调用方法/i.test(msg2) + ElMessage.error( + needDeploy + ? `${msg2}。当前 SimpleLite 仍是旧版 DLL:请先完全关闭 SimpleLite 窗口,再在 PowerShell 执行 Migu2.0/scripts/redeploy-simplelite.ps1,然后重新登录;或调用 POST /api/health/simplelite/restart-for-update` + : `前往站点失败:${msg2}` + ) + return false + } } } diff --git a/frontends/apps/simple-platform-vue/src/views/admin/CadToolbarView.vue b/frontends/apps/simple-platform-vue/src/views/admin/CadToolbarView.vue index 4c69734..8d6f4df 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/CadToolbarView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/CadToolbarView.vue @@ -63,13 +63,13 @@ function run(t: CadTool) { diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue index a40d487..709e1a3 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue @@ -22,7 +22,7 @@ :host="vrHost" :scope="auth.scope ?? 'Platform'" :token="auth.token ?? ''" - :read-only="false" + :read-only="!!readOnly" :embed-ui="true" @pick="onPick" @select="onSelect" @@ -35,17 +35,31 @@ + @@ -54,6 +68,7 @@ :refresh-key="tick" :cars="cars" :missions="missions" + :read-only="!!readOnly" /> @@ -65,27 +80,39 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import Workspace3D from '@/components/Workspace3D.vue' import VehicleMonitorPanel from '@/components/workbench/VehicleMonitorPanel.vue' +import MissionListPanel from '@/components/workbench/MissionListPanel.vue' import MonitorSelectionPanel from '@/components/workbench/MonitorSelectionPanel.vue' import FloatingAlarmStack from '@/components/map-monitor/FloatingAlarmStack.vue' import WorkspaceCanvasToolbar from '@/components/workspace/WorkspaceCanvasToolbar.vue' import { ElMessage } from 'element-plus' import { useAuthStore } from '@/stores/auth' import { listCars, listMissions } from '@/api/projection' +import { listDeliveries } from '@/api/delivery' import { useProjectionStream, type StreamEvent } from '@/composables/useProjectionStream' import type { AlarmEvent, SelectionDetailEvent } from '@/composables/useMapEditStream' import { reflectionApi } from '@/api/reflection' import type { Car } from '@/types/car' import type { Mission } from '@/types/mission' +import type { DeliveryTask } from '@/types/delivery' import type { SelectedObjectRef } from '@/types/workbench' +defineProps<{ + /** 只读模式(运营端复用 MapMonitorView 时传 true):3D 不可编辑,选中信息面板动作改用运维白名单。 */ + readOnly?: boolean +}>() + const auth = useAuthStore() const vrHost = (import.meta.env.VITE_VRENDER_HOST as string | undefined) ?? 'localhost:8223' const cars = ref([]) const missions = ref([]) +const deliveries = ref([]) +const deliveryListOpts = ref({ includeFinished: true, includeAborted: true }) const selection = ref(null) const tick = ref(0) const refreshing = ref(false) +const workbenchTab = ref<'vehicle' | 'mission'>('vehicle') +const selectedDeliveryId = ref(null) const workspaceRef = ref | null>(null) /** @@ -122,8 +149,16 @@ function onAlarmAck(_a: AlarmEvent) { let pollTimer: ReturnType | null = null const onlineCars = computed(() => cars.value.filter((c) => c.state !== 'offline').length) -const runningMissions = computed(() => missions.value.filter((m) => m.status === 'running').length) -const queuedMissions = computed(() => missions.value.filter((m) => m.status === 'queued').length) +const runningMissions = computed(() => + deliveries.value.length > 0 + ? deliveries.value.filter((d) => d.statusCode === 'Fetching' || d.statusCode === 'Putting').length + : missions.value.filter((m) => m.status === 'running').length +) +const queuedMissions = computed(() => + deliveries.value.length > 0 + ? deliveries.value.filter((d) => d.statusCode === 'Waiting').length + : missions.value.filter((m) => m.status === 'queued').length +) const selectedVehicleId = computed(() => { if (!selection.value || selection.value.kind !== 'vehicle') return null @@ -168,6 +203,7 @@ function sameSelection(a: SelectedObjectRef | null, b: SelectedObjectRef | null) /** 把解析结果写入侧栏 selection;仅当对象变化时刷新详情,避免选中闪烁。 */ function applyParsedSelection(parsed: { kind: 'vehicle' | 'site' | 'track'; id: string }, rawName?: string) { + selectedDeliveryId.value = null let next: SelectedObjectRef if (parsed.kind === 'vehicle') { const car = (rawName ? findSelectedCar(rawName) : undefined) @@ -188,6 +224,7 @@ function applyParsedSelection(parsed: { kind: 'vehicle' | 'site' | 'track'; id: } function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, names?: readonly string[]) { + selectedDeliveryId.value = null if (kind === 'car') { const sid = String(id) const rawName = names?.find((n) => RX_CAR.test(n)) ?? `Car-${id}` @@ -293,6 +330,7 @@ async function fallbackSelectionFromBackend() { } async function onVehicleSelect(ref: SelectedObjectRef) { + selectedDeliveryId.value = null if (!sameSelection(selection.value, ref)) { selection.value = ref tick.value++ @@ -306,6 +344,49 @@ async function onVehicleSelect(ref: SelectedObjectRef) { } } +async function onDeliverySelect(task: DeliveryTask) { + selectedDeliveryId.value = task.id + if (task.carId != null) { + const car = cars.value.find((c) => c.rawId === task.carId || detailIdForCar(c) === String(task.carId)) + if (car) { + await onVehicleSelect({ kind: 'vehicle', id: detailIdForCar(car), name: car.name }) + return + } + try { + await reflectionApi.setSelection('car', task.carId) + selection.value = { + kind: 'vehicle', + id: String(task.carId), + name: task.carName ?? `Vehicle ${task.carId}` + } + tick.value++ + } catch (err) { + ElMessage.error(`同步 3D 选中失败:${(err as Error).message}`) + } + return + } + selection.value = { + kind: 'delivery', + id: String(task.id), + name: `${task.srcLabel} → ${task.dstLabel}` + } + tick.value++ +} + +function onDeliveryListRefresh(opts: { includeFinished: boolean; includeAborted: boolean }) { + deliveryListOpts.value = opts + void refreshDeliveries() +} + +async function refreshDeliveries() { + try { + deliveries.value = await listDeliveries(deliveryListOpts.value) + } catch (err) { + console.warn('[MapMonitor] refreshDeliveries failed', err) + deliveries.value = [] + } +} + function findSelectedCar(name: string): Car | undefined { const parsedId = parseWorkspaceCarId(name) return cars.value.find((c) => @@ -328,8 +409,10 @@ function detailIdForCar(car: Car): string { async function refreshAll() { refreshing.value = true try { - cars.value = await listCars() - missions.value = await listMissions() + const [carList, missionList] = await Promise.all([listCars(), listMissions()]) + cars.value = carList + missions.value = missionList + await refreshDeliveries() } catch (err) { console.warn('[MapMonitor] refreshAll failed', err) } finally { @@ -385,7 +468,11 @@ onMounted(async () => { await refreshAll() pollTimer = setInterval(() => { // SSE 已连但 projection/cars 曾 502 时 connected 仍为 true,需在车列表为空时继续轮询 - if (!stream.connected.value || cars.value.length === 0) void refreshAll() + if (!stream.connected.value || cars.value.length === 0) { + void refreshAll() + } else if (workbenchTab.value === 'mission') { + void refreshDeliveries() + } }, 3000) }) @@ -411,8 +498,8 @@ onUnmounted(() => { flex-direction: column; min-height: 0; } -.workbench-card { flex: 1.2; } -.detail-card { flex: 1; } +.workbench-card { flex: 1; min-height: 0; } +.detail-card { flex: 1; min-height: 0; } .workbench-card :deep(.el-card__body), .detail-card :deep(.el-card__body) { flex: 1; @@ -421,10 +508,39 @@ onUnmounted(() => { display: flex; flex-direction: column; } -.muted { color: rgba(255, 255, 255, 0.78); font-size: 12px; } +.muted { color: var(--mg-text-muted); font-size: 12px; } .workbench-card :deep(.el-card__header), .detail-card :deep(.el-card__header) { - color: #fff; + color: var(--mg-text-light); +} +.workbench-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; +} +.workbench-tabs { + flex: 1; + min-width: 0; +} +.workbench-tabs :deep(.el-tabs__header) { + margin: 0; +} +.workbench-tabs :deep(.el-tabs__nav-wrap::after) { + display: none; +} +.workbench-tabs :deep(.el-tabs__item) { + color: var(--mg-text-muted); + padding: 0 10px; + height: 32px; + line-height: 32px; +} +.workbench-tabs :deep(.el-tabs__item.is-active) { + color: var(--mg-text-light); +} +.workbench-tabs :deep(.el-tabs__active-bar) { + background-color: var(--mg-accent, #8ec8fc); } :deep(.el-row.main-row) { height: 100%; } :deep(.el-col-16 > div) { height: 100%; min-height: 0; } diff --git a/frontends/apps/simple-platform-vue/src/views/admin/config/MapMonitorConfigView.vue b/frontends/apps/simple-platform-vue/src/views/admin/config/MapMonitorConfigView.vue index b7921db..47f51ae 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/config/MapMonitorConfigView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/config/MapMonitorConfigView.vue @@ -144,22 +144,22 @@ onMounted(() => reload()) diff --git a/frontends/apps/simple-platform-vue/src/views/monitor/MonitorMapView.vue b/frontends/apps/simple-platform-vue/src/views/monitor/MonitorMapView.vue index 988c8ac..a753831 100644 --- a/frontends/apps/simple-platform-vue/src/views/monitor/MonitorMapView.vue +++ b/frontends/apps/simple-platform-vue/src/views/monitor/MonitorMapView.vue @@ -1,131 +1,19 @@ - -