feat(platform): 合并车辆运维页并优化 3D 画布嵌入体验
将维护策略与车队生命周期并入车辆运维 Tab,补充旧路由重定向与 RBAC 页面 key 迁移;改进 Workspace3D 就绪探测与 wwwroot 静态资源。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
:scope="auth.scope ?? 'Platform'"
|
||||
:token="auth.token ?? ''"
|
||||
:read-only="!!readOnly"
|
||||
:embed-ui="true"
|
||||
:canvas-only="true"
|
||||
:hide-toolbar="true"
|
||||
@pick="onPick"
|
||||
@select="onSelect"
|
||||
@ready="onWorkspaceReady"
|
||||
|
||||
@@ -1,85 +1,106 @@
|
||||
<template>
|
||||
<div class="vehicle-hub-page">
|
||||
<div class="hub-toolbar">
|
||||
<div class="stats-row">
|
||||
<div class="stat">
|
||||
<span class="num">{{ totalCount }}</span>
|
||||
<span class="label">总数</span>
|
||||
</div>
|
||||
<div class="stat online">
|
||||
<span class="num">{{ onlineCount }}</span>
|
||||
<span class="label">在线</span>
|
||||
</div>
|
||||
<div class="stat warn">
|
||||
<span class="num">{{ maintenanceCount }}</span>
|
||||
<span class="label">维护中</span>
|
||||
</div>
|
||||
<div class="stat danger">
|
||||
<span class="num">{{ alarmCount }}</span>
|
||||
<span class="label">报警</span>
|
||||
</div>
|
||||
<div class="stat muted">
|
||||
<span class="num">{{ unreachableCount }}</span>
|
||||
<span class="label">不可达</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" class="hub-tabs">
|
||||
<el-tab-pane name="overview" label="运维总览">
|
||||
<div class="overview-pane">
|
||||
<div class="hub-toolbar">
|
||||
<div class="stats-row">
|
||||
<div class="stat">
|
||||
<span class="num">{{ totalCount }}</span>
|
||||
<span class="label">总数</span>
|
||||
</div>
|
||||
<div class="stat online">
|
||||
<span class="num">{{ onlineCount }}</span>
|
||||
<span class="label">在线</span>
|
||||
</div>
|
||||
<div class="stat warn">
|
||||
<span class="num">{{ maintenanceCount }}</span>
|
||||
<span class="label">维护中</span>
|
||||
</div>
|
||||
<div class="stat danger">
|
||||
<span class="num">{{ alarmCount }}</span>
|
||||
<span class="label">报警</span>
|
||||
</div>
|
||||
<div class="stat muted">
|
||||
<span class="num">{{ unreachableCount }}</span>
|
||||
<span class="label">不可达</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-row">
|
||||
<el-input
|
||||
v-model="search"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索 ID / 名称 / IP"
|
||||
class="search-input"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<el-select v-model="filterState" size="small" clearable placeholder="状态" class="filter-select">
|
||||
<el-option v-for="opt in stateOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filterGroup" size="small" clearable placeholder="群组" class="filter-select">
|
||||
<el-option v-for="g in groupOptions" :key="g" :label="g" :value="g" />
|
||||
</el-select>
|
||||
<el-dropdown :disabled="!canWrite || !selectedIds.length" @command="onBatchCommand">
|
||||
<el-button size="small" :disabled="!canWrite || !selectedIds.length">
|
||||
批量维护
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="online">批量上线</el-dropdown-item>
|
||||
<el-dropdown-item command="offline">批量下线</el-dropdown-item>
|
||||
<el-dropdown-item command="repair">批量现场检修</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" :icon="Refresh" :loading="loading || healthLoading" @click="refreshAll">
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters-row">
|
||||
<el-input
|
||||
v-model="search"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索 ID / 名称 / IP"
|
||||
class="search-input"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<el-select v-model="filterState" size="small" clearable placeholder="状态" class="filter-select">
|
||||
<el-option v-for="opt in stateOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filterGroup" size="small" clearable placeholder="群组" class="filter-select">
|
||||
<el-option v-for="g in groupOptions" :key="g" :label="g" :value="g" />
|
||||
</el-select>
|
||||
<el-dropdown :disabled="!canWrite || !selectedIds.length" @command="onBatchCommand">
|
||||
<el-button size="small" :disabled="!canWrite || !selectedIds.length">
|
||||
批量维护
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="online">批量上线</el-dropdown-item>
|
||||
<el-dropdown-item command="offline">批量下线</el-dropdown-item>
|
||||
<el-dropdown-item command="repair">批量现场检修</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" :icon="Refresh" :loading="loading || healthLoading" @click="refreshAll">
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading && !cardModels.length" class="card-grid">
|
||||
<VehicleHealthCard
|
||||
v-for="v in filteredCards"
|
||||
:key="v.id"
|
||||
:vehicle="v"
|
||||
:selected="selectedId === v.id"
|
||||
:can-write="canWrite"
|
||||
@select="selectedId = $event"
|
||||
@maintenance-changed="refreshAll"
|
||||
/>
|
||||
<el-empty v-if="!filteredCards.length && !loading" description="无匹配车辆" />
|
||||
</div>
|
||||
<div v-loading="loading && !cardModels.length" class="card-grid">
|
||||
<VehicleHealthCard
|
||||
v-for="v in filteredCards"
|
||||
:key="v.id"
|
||||
:vehicle="v"
|
||||
:selected="selectedId === v.id"
|
||||
:can-write="canWrite"
|
||||
@select="selectedId = $event"
|
||||
@maintenance-changed="refreshAll"
|
||||
/>
|
||||
<el-empty v-if="!filteredCards.length && !loading" description="无匹配车辆" />
|
||||
</div>
|
||||
|
||||
<p class="footnote">故障率 = 报警占用时长 ÷ 自上线以来运行时长(SimpleLite 进程内累计)</p>
|
||||
<p class="footnote">故障率 = 报警占用时长 ÷ 自上线以来运行时长(SimpleLite 进程内累计)</p>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="maintenance" label="维护策略" lazy>
|
||||
<div class="config-pane">
|
||||
<VehicleMaintenanceView />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="fleet" label="车队生命周期" lazy>
|
||||
<div class="config-pane">
|
||||
<FleetLifecycleView />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, ArrowDown } from '@element-plus/icons-vue'
|
||||
import VehicleHealthCard from '@/components/fleet/VehicleHealthCard.vue'
|
||||
import VehicleMaintenanceView from '@/views/admin/config/VehicleMaintenanceView.vue'
|
||||
import FleetLifecycleView from '@/views/admin/config/FleetLifecycleView.vue'
|
||||
import { useVehicleHub } from '@/composables/useVehicleHub'
|
||||
import { setVehicleMaintenance, type VehicleMaintenanceMode } from '@/api/vehicleOps'
|
||||
import type { CarState } from '@/types/car'
|
||||
@@ -88,6 +109,24 @@ import { useAuthStore } from '@/stores/auth'
|
||||
const auth = useAuthStore()
|
||||
const canWrite = computed(() => auth.scope === 'Platform' || (auth.effectivePermissions?.allowedOps ?? []).includes('*'))
|
||||
|
||||
// Tab 与 URL ?tab= 同步,支持深链接(旧 /config/vehicle、/config/fleet 已下线,统一进车辆运维)。
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const TAB_NAMES = ['overview', 'maintenance', 'fleet'] as const
|
||||
type TabName = (typeof TAB_NAMES)[number]
|
||||
function readTab(): TabName {
|
||||
const q = route.query.tab
|
||||
return typeof q === 'string' && (TAB_NAMES as readonly string[]).includes(q) ? (q as TabName) : 'overview'
|
||||
}
|
||||
const activeTab = ref<TabName>(readTab())
|
||||
watch(activeTab, (t) => {
|
||||
if (route.query.tab !== t) router.replace({ query: { ...route.query, tab: t } })
|
||||
})
|
||||
watch(() => route.query.tab, () => {
|
||||
const next = readTab()
|
||||
if (next !== activeTab.value) activeTab.value = next
|
||||
})
|
||||
|
||||
const {
|
||||
cardModels,
|
||||
loading,
|
||||
@@ -157,6 +196,42 @@ async function onBatchCommand(cmd: string) {
|
||||
<style scoped>
|
||||
.vehicle-hub-page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hub-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
color: var(--mg-text-muted, rgba(255, 255, 255, 0.65));
|
||||
}
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: var(--mg-text-light, #fff);
|
||||
}
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background-color: var(--mg-accent, var(--el-color-primary));
|
||||
}
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.overview-pane {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -164,6 +239,11 @@ async function onBatchCommand(cmd: string) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.config-pane {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hub-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user