feat(workspace): 画布 2D/3D 切换与车辆相机跟随
- workspaceToolbar API 新增 view/toggle 与 camera/follow - 画布底栏新增 2D/3D 切换、自动跟随按钮,组件卸载时自动停止跟随 - 地图监控页将选中车辆 id 透传给底栏,并接入监控配置缓存 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
@ready="onWorkspaceReady"
|
||||
/>
|
||||
<FloatingAlarmStack :alarms="alarms" @locate="onAlarmLocate" @ack="onAlarmAck" />
|
||||
<WorkspaceCanvasToolbar />
|
||||
<WorkspaceCanvasToolbar :follow-car-id="selectedFollowCarId" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8" class="side-col">
|
||||
@@ -95,6 +95,7 @@ import type { Car } from '@/types/car'
|
||||
import type { Mission } from '@/types/mission'
|
||||
import type { DeliveryTask } from '@/types/delivery'
|
||||
import type { SelectedObjectRef } from '@/types/workbench'
|
||||
import { fetchMonitorConfigCached, invalidateMonitorConfigCache } from '@/utils/monitorConfigCache'
|
||||
|
||||
defineProps<{
|
||||
/** 只读模式(运营端复用 MapMonitorView 时传 true):3D 不可编辑,选中信息面板动作改用运维白名单。 */
|
||||
@@ -167,6 +168,21 @@ const selectedVehicleId = computed(() => {
|
||||
return match?.id ?? null
|
||||
})
|
||||
|
||||
/** 供底栏「自动跟随」使用的车辆 numeric id(与 reflection setSelection 一致)。 */
|
||||
const selectedFollowCarId = computed(() => {
|
||||
if (!selection.value || selection.value.kind !== 'vehicle') return null
|
||||
const sid = selection.value.id
|
||||
const n = Number(sid)
|
||||
if (Number.isFinite(n)) return n
|
||||
const match = cars.value.find((c) => c.id === sid || detailIdForCar(c) === sid)
|
||||
if (match?.rawId != null) return match.rawId
|
||||
if (match) {
|
||||
const parsed = Number(detailIdForCar(match))
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
function onPick(_p: { x: number; y: number }) { /* 坐标由 3D 选中对象驱动 */ }
|
||||
|
||||
// SimpleLite 3D 端发送的对象命名规则(见 SimpleUI.cs / UISite.cs / TrackUiHelper):
|
||||
@@ -220,7 +236,6 @@ function applyParsedSelection(parsed: { kind: 'vehicle' | 'site' | 'track'; id:
|
||||
}
|
||||
if (sameSelection(selection.value, next)) return
|
||||
selection.value = next
|
||||
tick.value++
|
||||
}
|
||||
|
||||
function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, names?: readonly string[]) {
|
||||
@@ -234,7 +249,6 @@ function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, na
|
||||
: { kind: 'vehicle' as const, id: sid, name: `Vehicle ${sid}` }
|
||||
if (!sameSelection(selection.value, next)) {
|
||||
selection.value = next
|
||||
tick.value++
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -250,7 +264,6 @@ function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, na
|
||||
}
|
||||
if (!sameSelection(selection.value, next)) {
|
||||
selection.value = next
|
||||
tick.value++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +278,6 @@ function onSelectionDetailFromStream(e: SelectionDetailEvent) {
|
||||
const nameN = Array.isArray(e.names) ? e.names.length : 0
|
||||
if (siteN + trackN + carN + nameN === 0) {
|
||||
selection.value = null
|
||||
tick.value++
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,7 +345,6 @@ async function onVehicleSelect(ref: SelectedObjectRef) {
|
||||
selectedDeliveryId.value = null
|
||||
if (!sameSelection(selection.value, ref)) {
|
||||
selection.value = ref
|
||||
tick.value++
|
||||
}
|
||||
const id = Number(ref.id)
|
||||
if (!Number.isFinite(id)) return
|
||||
@@ -359,7 +370,6 @@ async function onDeliverySelect(task: DeliveryTask) {
|
||||
id: String(task.carId),
|
||||
name: task.carName ?? `Vehicle ${task.carId}`
|
||||
}
|
||||
tick.value++
|
||||
} catch (err) {
|
||||
ElMessage.error(`同步 3D 选中失败:${(err as Error).message}`)
|
||||
}
|
||||
@@ -370,7 +380,6 @@ async function onDeliverySelect(task: DeliveryTask) {
|
||||
id: String(task.id),
|
||||
name: `${task.srcLabel} → ${task.dstLabel}`
|
||||
}
|
||||
tick.value++
|
||||
}
|
||||
|
||||
function onDeliveryListRefresh(opts: { includeFinished: boolean; includeAborted: boolean }) {
|
||||
@@ -416,7 +425,6 @@ async function refreshAll() {
|
||||
} catch (err) {
|
||||
console.warn('[MapMonitor] refreshAll failed', err)
|
||||
} finally {
|
||||
tick.value++
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
@@ -456,7 +464,8 @@ function onStreamEvent(e: StreamEvent) {
|
||||
}
|
||||
break
|
||||
case 'monitor-config-updated':
|
||||
// 运营维护保存后 SimpleLite 广播,刷新选中面板动作列表
|
||||
// 运营维护保存后刷新动作白名单(勿在 refreshAll 里 tick++,避免轮询导致选中面板反复重载)
|
||||
invalidateMonitorConfigCache()
|
||||
tick.value++
|
||||
break
|
||||
// 'snapshot-tick' 故意不触发 refreshAll —— 见上面注释。
|
||||
@@ -465,6 +474,7 @@ function onStreamEvent(e: StreamEvent) {
|
||||
stream.on(onStreamEvent)
|
||||
|
||||
onMounted(async () => {
|
||||
void fetchMonitorConfigCached()
|
||||
await refreshAll()
|
||||
pollTimer = setInterval(() => {
|
||||
// SSE 已连但 projection/cars 曾 502 时 connected 仍为 true,需在车列表为空时继续轮询
|
||||
@@ -492,14 +502,29 @@ onUnmounted(() => {
|
||||
.kpi-row { flex: none; }
|
||||
.main-row { flex: 1; min-height: 0; }
|
||||
.main-row > .el-col { display: flex; flex-direction: column; min-height: 0; }
|
||||
.side-col { gap: 12px; }
|
||||
.side-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.side-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 上下各占约一半,与任务列表 tab 时一致,保证「选中信息」始终可见 */
|
||||
.workbench-card {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: 52%;
|
||||
}
|
||||
.detail-card {
|
||||
flex: 1 1 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user