feat(web): 配置中心聚合页、路由迁移与权限页 key 同步

将原十余个独立配置页聚合为 调度策略/设备与库位/业务与集成/运维与回放/系统与权限 等聚合页
(Tab 与 URL ?tab= 同步),旧路径以 redirect 兼容深链接;AppShell 菜单与 mock/rbac 的页面 key
同步后端 PageCatalog;Dashboard 快捷入口改指聚合页对应 tab;新增 TaskTemplateView 取代并删除
MissionEditorView / OrchestrationView。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-06-08 16:11:02 +08:00
co-authored by Cursor
parent 655d50e04d
commit c0a33723ca
12 changed files with 481 additions and 149 deletions
@@ -154,26 +154,18 @@ const ADMIN_MENU: MenuItem[] = [
{ path: '/admin/cars', label: '车辆管理', key: 'admin-cars' },
{ path: '/admin/processes', label: '进程管理', key: 'admin-processes' },
{ path: '/admin/scripts', label: '脚本管理', key: 'admin-scripts' },
{ path: '/admin/missions', label: '任务编排', key: 'admin-missions' }
{ path: '/admin/task-templates', label: '任务编排', key: 'admin-task-templates' }
]
},
{ path: '/admin/playback', label: '调度回放', icon: Notebook, key: 'admin-playback' },
{
path: '/admin/config', label: '平台配置中心', icon: Setting,
children: [
{ path: '/admin/config/system', label: '系统级配置', key: 'admin-config-system' },
{ path: '/admin/config/integrations', label: '外部系统对接', key: 'admin-config-integrations' },
{ path: '/admin/config/routing', label: '路径规划', key: 'admin-config-routing' },
{ path: '/admin/config/strategy', label: '调度策略', key: 'admin-config-strategy' },
{ path: '/admin/config/vehicle-hub', label: '车辆运维', key: 'admin-vehicle-hub' },
{ path: '/admin/config/charge', label: '充电策略', key: 'admin-config-charge' },
{ path: '/admin/config/task', label: '任务分配', key: 'admin-config-task' },
{ path: '/admin/config/traffic', label: '交通管制', key: 'admin-config-traffic' },
{ path: '/admin/config/auth', label: '权限与角色', key: 'admin-config-auth' },
{ path: '/admin/config/device', label: '设备接入', key: 'admin-config-device' },
{ path: '/admin/config/scenario', label: '场景模板', key: 'admin-config-scenario' },
{ path: '/admin/config/location', label: '库位管理', key: 'admin-config-location' },
{ path: '/admin/config/ops', label: '运营维护', key: 'admin-config-ops' },
{ path: '/admin/config/widget', label: '自定义控件', key: 'admin-config-widget' }
{ path: '/admin/config/facility', label: '设备与库位', key: 'admin-config-facility' },
{ path: '/admin/config/business', label: '业务与集成', key: 'admin-config-business' },
{ path: '/admin/config/ops-center', label: '运维与回放', key: 'admin-config-ops-center' },
{ path: '/admin/config/system-center', label: '系统与权限', key: 'admin-config-system-center' }
]
}
]
@@ -12,7 +12,6 @@ import type {
const PAGES: PageDef[] = [
{ key: 'admin-dashboard', label: '总览', group: '概览', scope: 'Platform' },
{ key: 'admin-map-monitor', label: '地图监控', group: '概览', scope: 'Platform' },
{ key: 'admin-playback', label: '调度回放', group: '概览', scope: 'Platform' },
{ key: 'admin-maps', label: '地图管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-map-editor', label: '地图编辑', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-project-properties', label: '项目属性', group: '设计与编排', scope: 'Platform' },
@@ -20,21 +19,13 @@ const PAGES: PageDef[] = [
{ key: 'admin-cars', label: '车辆管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-processes', label: '进程管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-scripts', label: '脚本管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-missions', label: '任务编排', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-config-system', label: '系统级配置', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-integrations', label: '外部系统对接', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-routing', label: '路径规划', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-task-templates', label: '任务编排', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-config-strategy', label: '调度策略', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-vehicle-hub', label: '车辆运维', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-charge', label: '充电策略', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-task', label: '任务分配', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-traffic', label: '交通管制', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-auth', label: '权限与角色', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-device', label: '设备接入', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-scenario', label: '场景模板', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-location', label: '库位管理', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-ops', label: '运营维护', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-widget', label: '自定义控件', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-map-monitor', label: '地图监控配置', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-facility', label: '设备与库位', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-business', label: '业务与集成', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-ops-center', label: '运维与回放', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-system-center', label: '系统与权限', group: '平台配置中心', scope: 'Platform' },
{ key: 'monitor-dashboard', label: '运营总览', group: '运营监控', scope: 'RCSMonitor' },
{ key: 'monitor-vehicle-hub', label: '车辆运维', group: '运营监控', scope: 'RCSMonitor' },
{ key: 'monitor-map', label: '地图监控', group: '运营监控', scope: 'RCSMonitor' },
@@ -145,7 +136,23 @@ function sanitizePages(pages: string[]): string[] {
const valid = new Set(PAGES.map((p) => p.key))
const legacy: Record<string, string> = {
'admin-config-vehicle': 'admin-vehicle-hub',
'admin-config-fleet': 'admin-vehicle-hub'
'admin-config-fleet': 'admin-vehicle-hub',
// 会话 16:配置中心收敛为 6 个聚合页,旧页面 Key 迁移到对应聚合页 Key。
'admin-config-routing': 'admin-config-strategy',
'admin-config-task': 'admin-config-strategy',
'admin-config-traffic': 'admin-config-strategy',
'admin-config-charge': 'admin-config-strategy',
'admin-config-device': 'admin-config-facility',
'admin-config-location': 'admin-config-facility',
'admin-config-integrations': 'admin-config-business',
'admin-config-scenario': 'admin-config-business',
'admin-config-widget': 'admin-config-business',
'admin-playback': 'admin-config-ops-center',
'admin-config-ops': 'admin-config-ops-center',
'admin-config-logs': 'admin-config-ops-center',
'admin-config-map-monitor': 'admin-config-ops-center',
'admin-config-system': 'admin-config-system-center',
'admin-config-auth': 'admin-config-system-center'
}
return [...new Set(pages.map((p) => legacy[p] ?? p).filter((p) => valid.has(p)))]
}
@@ -36,27 +36,36 @@ const routes: RouteRecordRaw[] = [
{ path: 'cars', name: 'admin-cars', component: () => import('@/views/admin/CarPanelView.vue'), meta: { title: '车辆管理' } },
{ path: 'processes', name: 'admin-processes', component: () => import('@/views/admin/ProcessPanelView.vue'), meta: { title: '进程管理' } },
{ path: 'scripts', name: 'admin-scripts', component: () => import('@/views/admin/ScriptPanelView.vue'), meta: { title: '脚本管理' } },
{ path: 'missions', name: 'admin-missions', component: () => import('@/views/admin/MissionEditorView.vue'), meta: { title: '任务编排' } },
{ path: 'task-templates', name: 'admin-task-templates', component: () => import('@/views/admin/TaskTemplateView.vue'), meta: { title: '任务编排' } },
{ path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } },
{ path: 'playback', name: 'admin-playback', component: () => import('@/views/admin/PlaybackView.vue'), meta: { title: '调度回放' } },
{ path: 'config/system', name: 'admin-config-system', component: () => import('@/views/admin/config/SystemConfigView.vue'), meta: { title: '系统级配置' } },
{ path: 'config/integrations', name: 'admin-config-integrations', component: () => import('@/views/admin/config/ExternalIntegrationView.vue'), meta: { title: '外部系统对接' } },
{ path: 'config/routing', name: 'admin-config-routing', component: () => import('@/views/admin/config/RoutingPolicyView.vue'), meta: { title: '路径规划策略' } },
// ── 平台配置中心:聚合页(每个聚合页一个 page key = route.name,对齐后端 PageCatalog)。
// 原十余个独立配置页按业务收敛为下列 6 个入口,子页改为聚合页内的 tab。 ──
{ path: 'config/strategy', name: 'admin-config-strategy', component: () => import('@/views/admin/config/StrategyConfigView.vue'), meta: { title: '调度策略' } },
{ path: 'config/vehicle-hub', name: 'admin-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
{ path: 'config/facility', name: 'admin-config-facility', component: () => import('@/views/admin/config/FacilityConfigView.vue'), meta: { title: '设备与库位' } },
{ path: 'config/business', name: 'admin-config-business', component: () => import('@/views/admin/config/BusinessConfigView.vue'), meta: { title: '业务与集成' } },
{ path: 'config/ops-center', name: 'admin-config-ops-center', component: () => import('@/views/admin/config/OpsCenterView.vue'), meta: { title: '运维与回放' } },
{ path: 'config/system-center', name: 'admin-config-system-center', component: () => import('@/views/admin/config/SystemCenterView.vue'), meta: { title: '系统与权限' } },
// ── 旧路径深链接兼容:redirect 到聚合页对应 tab(无 name → 不计入受权限管理的页面)。 ──
{ path: 'playback', redirect: { path: '/admin/config/ops-center', query: { tab: 'playback' } } },
{ path: 'vehicle-hub', redirect: '/admin/config/vehicle-hub' },
// 旧独立配置页已合并进车辆运维 Tab,保留深链接兼容。
{ path: 'config/vehicle', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'maintenance' } } },
{ path: 'config/fleet', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'fleet' } } },
{ path: 'config/charge', name: 'admin-config-charge', component: () => import('@/views/admin/config/ChargePolicyView.vue'), meta: { title: '充电逻辑' } },
{ path: 'config/task', name: 'admin-config-task', component: () => import('@/views/admin/config/TaskAllocationView.vue'), meta: { title: '任务分配' } },
{ path: 'config/traffic', name: 'admin-config-traffic', component: () => import('@/views/admin/config/TrafficRuleView.vue'), meta: { title: '交通管制' } },
{ path: 'config/auth', name: 'admin-config-auth', component: () => import('@/views/admin/config/AuthRoleView.vue'), meta: { title: '权限角色' } },
{ path: 'config/device', name: 'admin-config-device', component: () => import('@/views/admin/config/DeviceHubView.vue'), meta: { title: '设备接入' } },
{ path: 'config/scenario', name: 'admin-config-scenario', component: () => import('@/views/admin/config/ScenarioTemplateView.vue'), meta: { title: '场景模板' } },
{ path: 'config/location', name: 'admin-config-location', component: () => import('@/views/admin/config/LocationView.vue'), meta: { title: '库位管理' } },
{ path: 'config/ops', name: 'admin-config-ops', component: () => import('@/views/admin/config/OpsConfigView.vue'), meta: { title: '运营维护' } },
{ path: 'config/widget', name: 'admin-config-widget', component: () => import('@/views/admin/config/CustomWidgetView.vue'), meta: { title: '自定义控件' } },
{ path: 'config/map-monitor', name: 'admin-config-map-monitor', component: () => import('@/views/admin/config/MapMonitorConfigView.vue'), meta: { title: '地图监控配置' } }
{ path: 'config/routing', redirect: { path: '/admin/config/strategy', query: { tab: 'routing' } } },
{ path: 'config/task', redirect: { path: '/admin/config/strategy', query: { tab: 'task' } } },
{ path: 'config/traffic', redirect: { path: '/admin/config/strategy', query: { tab: 'traffic' } } },
{ path: 'config/charge', redirect: { path: '/admin/config/strategy', query: { tab: 'charge' } } },
{ path: 'config/device', redirect: { path: '/admin/config/facility', query: { tab: 'device' } } },
{ path: 'config/location', redirect: { path: '/admin/config/facility', query: { tab: 'location' } } },
{ path: 'config/integrations', redirect: { path: '/admin/config/business', query: { tab: 'integrations' } } },
{ path: 'config/scenario', redirect: { path: '/admin/config/business', query: { tab: 'scenario' } } },
{ path: 'config/widget', redirect: { path: '/admin/config/business', query: { tab: 'widget' } } },
{ path: 'config/ops', redirect: { path: '/admin/config/ops-center', query: { tab: 'ops' } } },
{ path: 'config/logs', redirect: { path: '/admin/config/ops-center', query: { tab: 'logs' } } },
{ path: 'config/map-monitor', redirect: { path: '/admin/config/ops-center', query: { tab: 'map-monitor' } } },
{ path: 'config/system', redirect: { path: '/admin/config/system-center', query: { tab: 'system' } } },
{ path: 'config/auth', redirect: { path: '/admin/config/system-center', query: { tab: 'auth' } } }
]
},
{
@@ -392,12 +392,12 @@ const activeMissionCount = computed(() =>
interface QuickEntry { key: string; label: string; icon: unknown; hint?: string; primary?: boolean; to?: string; action?: () => void }
const quickEntries = computed<QuickEntry[]>(() => [
{ key: 'platform-config', label: '平台配置', icon: Setting, primary: true, hint: '进入地图编辑 / 平台搭建(map-editor', to: '/admin/map-editor' },
{ key: 'mission', label: '任务编排', icon: Operation, to: '/admin/missions' },
{ key: 'mission', label: '任务编排', icon: Operation, to: '/admin/task-templates' },
{ key: 'cars', label: 'AGV 配置', icon: Van, to: '/admin/cars' },
{ key: 'auth', label: '权限配置', icon: Avatar, to: '/admin/config/auth' },
{ key: 'system', label: '系统配置', icon: Tools, to: '/admin/config/system' },
{ key: 'ops', label: '异常处理', icon: Warning, to: '/admin/config/ops' },
{ key: 'tasks', label: '任务管理', icon: List, to: '/admin/config/task' },
{ key: 'auth', label: '权限配置', icon: Avatar, to: '/admin/config/system-center?tab=auth' },
{ key: 'system', label: '系统配置', icon: Tools, to: '/admin/config/system-center?tab=system' },
{ key: 'ops', label: '异常处理', icon: Warning, to: '/admin/config/ops-center?tab=ops' },
{ key: 'tasks', label: '任务管理', icon: List, to: '/admin/config/strategy?tab=task' },
{ key: 'add', label: '添加', icon: Plus, hint: '自定义快捷入口(待实现)', action: () => { ElMessage.info('自定义快捷入口 — 即将在 v1.8 中开放') } }
])
function onQuickClick(item: QuickEntry) {
@@ -1,31 +0,0 @@
<template>
<div class="mission-page">
<ReflectionManagerPanel
kind="process"
kind-label="任务"
title="任务编排(Mission / 进程,含插件 MissionType 实例化)"
empty-text="当前没有任务点击右上角新建任务从已加载的 MissionType 中选一个实例化"
/>
</div>
</template>
<script setup lang="ts">
/**
* /admin/missions 路由的页面入口。
*
* 由于「任务」与「进程」在后端 ReflectionApiController 共用 kind=process / mission
* UiDiscoveryCache.MissionCreatableTypes 同源),这里直接用 ReflectionManagerPanel
* 接入,按下「新建任务」会列出所有 MissionType 子类(含 taskflow / trigger)。
*
* 旧版本的 DataTablePro + listMissions mock 已经下线,所有数据走 SimpleLite 反射 API。
*/
import ReflectionManagerPanel from '@/components/reflection/ReflectionManagerPanel.vue'
</script>
<style scoped>
.mission-page {
height: calc(100vh - 56px);
padding: 12px 14px;
box-sizing: border-box;
}
</style>
@@ -1,67 +0,0 @@
<template>
<div class="orchestration-page">
<el-tabs v-model="tab" class="orch-tabs" type="border-card" @tab-change="onTabChange">
<el-tab-pane label="车辆" name="cars">
<CarPanelView v-if="tab === 'cars'" />
</el-tab-pane>
<el-tab-pane label="进程" name="process">
<MissionEditorView v-if="tab === 'process'" />
</el-tab-pane>
<el-tab-pane label="脚本" name="script">
<ReflectionKindTable
v-if="tab === 'script'"
kind="script"
empty-text="脚本继承 CarProgram 的子类型未发现可暴露条目"
/>
</el-tab-pane>
<el-tab-pane label="任务流" name="taskflow">
<ReflectionKindTable
v-if="tab === 'taskflow'"
kind="mission"
extra-filter="taskflow"
empty-text="未发现任务流条目"
/>
</el-tab-pane>
<el-tab-pane label="触发器" name="trigger">
<ReflectionKindTable
v-if="tab === 'trigger'"
kind="mission"
extra-filter="trigger"
empty-text="未发现触发器条目"
/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import CarPanelView from '@/views/admin/CarPanelView.vue'
import MissionEditorView from '@/views/admin/MissionEditorView.vue'
import ReflectionKindTable from '@/components/orchestration/ReflectionKindTable.vue'
const tab = ref<'cars' | 'process' | 'script' | 'taskflow' | 'trigger'>('cars')
function onTabChange(name: string | number) {
tab.value = name as typeof tab.value
}
onMounted(() => {
const cached = sessionStorage.getItem('orchestration.tab')
if (cached) tab.value = cached as typeof tab.value
})
</script>
<style scoped>
.orchestration-page {
height: calc(100vh - 56px);
padding: 12px;
}
.orch-tabs {
height: 100%;
}
.orch-tabs :deep(.el-tabs__content) {
height: calc(100% - 40px);
overflow: auto;
}
</style>
@@ -0,0 +1,27 @@
<template>
<div class="task-template-page">
<WorkflowEditor />
</div>
</template>
<script setup lang="ts">
/**
* 任务编排页(路由 /admin/task-templates):承接 workflow 可视化编排组件。
*
* 原独立的 /admin/missionsMissionEditor / 进程实例管理)入口已下线并合并至此,
* 这里专注任务流程的可视化编排、模板设计、导入导出和本地流程库。
*/
import WorkflowEditor from '@/components/workflow/WorkflowEditor.vue'
</script>
<style scoped>
.task-template-page {
height: calc(100vh - 56px);
padding: 12px 14px;
box-sizing: border-box;
}
.task-template-page :deep(.workflow-editor) {
height: 100%;
}
</style>
@@ -0,0 +1,79 @@
<template>
<div class="config-hub-page">
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane name="integrations" label="外部系统对接">
<div class="config-pane"><ExternalIntegrationView /></div>
</el-tab-pane>
<el-tab-pane name="scenario" label="场景模板" lazy>
<div class="config-pane"><ScenarioTemplateView /></div>
</el-tab-pane>
<el-tab-pane name="widget" label="自定义控件" lazy>
<div class="config-pane"><CustomWidgetView /></div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ExternalIntegrationView from '@/views/admin/config/ExternalIntegrationView.vue'
import ScenarioTemplateView from '@/views/admin/config/ScenarioTemplateView.vue'
import CustomWidgetView from '@/views/admin/config/CustomWidgetView.vue'
// 聚合页:对外接口、业务场景建模与自定义控件。Tab 与 URL ?tab= 同步。
const route = useRoute()
const router = useRouter()
const TAB_NAMES = ['integrations', 'scenario', 'widget'] 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) : 'integrations'
}
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
})
</script>
<style scoped>
.config-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;
overflow: auto;
}
:deep(.el-tab-pane) { height: 100%; }
.config-pane {
height: 100%;
overflow: auto;
}
: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);
}
</style>
@@ -0,0 +1,75 @@
<template>
<div class="config-hub-page">
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane name="device" label="设备接入">
<div class="config-pane"><DeviceHubView /></div>
</el-tab-pane>
<el-tab-pane name="location" label="库位管理" lazy>
<div class="config-pane"><LocationView /></div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import DeviceHubView from '@/views/admin/config/DeviceHubView.vue'
import LocationView from '@/views/admin/config/LocationView.vue'
// 聚合页:第三方设备接入与库位管理。Tab 与 URL ?tab= 同步。
const route = useRoute()
const router = useRouter()
const TAB_NAMES = ['device', 'location'] 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) : 'device'
}
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
})
</script>
<style scoped>
.config-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;
overflow: auto;
}
:deep(.el-tab-pane) { height: 100%; }
.config-pane {
height: 100%;
overflow: auto;
}
: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);
}
</style>
@@ -0,0 +1,83 @@
<template>
<div class="config-hub-page">
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane name="playback" label="调度回放">
<div class="config-pane"><PlaybackView /></div>
</el-tab-pane>
<el-tab-pane name="ops" label="运营维护" lazy>
<div class="config-pane"><OpsConfigView /></div>
</el-tab-pane>
<el-tab-pane name="logs" label="日志管理" lazy>
<div class="config-pane"><LogManagementView /></div>
</el-tab-pane>
<el-tab-pane name="map-monitor" label="地图监控配置" lazy>
<div class="config-pane"><MapMonitorConfigView /></div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PlaybackView from '@/views/admin/PlaybackView.vue'
import OpsConfigView from '@/views/admin/config/OpsConfigView.vue'
import LogManagementView from '@/views/admin/LogManagementView.vue'
import MapMonitorConfigView from '@/views/admin/config/MapMonitorConfigView.vue'
// 聚合页:运维与回放(调度回放从顶级菜单移入此处)。Tab 与 URL ?tab= 同步。
const route = useRoute()
const router = useRouter()
const TAB_NAMES = ['playback', 'ops', 'logs', 'map-monitor'] 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) : 'playback'
}
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
})
</script>
<style scoped>
.config-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;
overflow: auto;
}
:deep(.el-tab-pane) { height: 100%; }
.config-pane {
height: 100%;
overflow: auto;
}
: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);
}
</style>
@@ -0,0 +1,83 @@
<template>
<div class="config-hub-page">
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane name="routing" label="路径规划">
<div class="config-pane"><RoutingPolicyView /></div>
</el-tab-pane>
<el-tab-pane name="task" label="任务分配" lazy>
<div class="config-pane"><TaskAllocationView /></div>
</el-tab-pane>
<el-tab-pane name="traffic" label="交通管制" lazy>
<div class="config-pane"><TrafficRuleView /></div>
</el-tab-pane>
<el-tab-pane name="charge" label="充电策略" lazy>
<div class="config-pane"><ChargePolicyView /></div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import RoutingPolicyView from '@/views/admin/config/RoutingPolicyView.vue'
import TaskAllocationView from '@/views/admin/config/TaskAllocationView.vue'
import TrafficRuleView from '@/views/admin/config/TrafficRuleView.vue'
import ChargePolicyView from '@/views/admin/config/ChargePolicyView.vue'
// 聚合页:调度运行策略统一入口。Tab 与 URL ?tab= 同步,旧的独立配置路由 redirect 到此页对应 tab。
const route = useRoute()
const router = useRouter()
const TAB_NAMES = ['routing', 'task', 'traffic', 'charge'] 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) : 'routing'
}
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
})
</script>
<style scoped>
.config-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;
overflow: auto;
}
:deep(.el-tab-pane) { height: 100%; }
.config-pane {
height: 100%;
overflow: auto;
}
: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);
}
</style>
@@ -0,0 +1,75 @@
<template>
<div class="config-hub-page">
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane name="system" label="系统级配置">
<div class="config-pane"><SystemConfigView /></div>
</el-tab-pane>
<el-tab-pane name="auth" label="权限与角色" lazy>
<div class="config-pane"><AuthRoleView /></div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SystemConfigView from '@/views/admin/config/SystemConfigView.vue'
import AuthRoleView from '@/views/admin/config/AuthRoleView.vue'
// 聚合页:系统级配置与权限角色。Tab 与 URL ?tab= 同步。
const route = useRoute()
const router = useRouter()
const TAB_NAMES = ['system', 'auth'] 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) : 'system'
}
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
})
</script>
<style scoped>
.config-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;
overflow: auto;
}
:deep(.el-tab-pane) { height: 100%; }
.config-pane {
height: 100%;
overflow: auto;
}
: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);
}
</style>