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
@@ -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>