feat(subsystem): 子系统导航增加可展开子 Tab 与详情页

- AppSidebar: 统一入口组下子系统导航改为可展开列表,展示 6 个子系统子 Tab
- 每个子 Tab 带功能描述前缀(如 多集群发布平台 Wayne)便于用户定位
- 新建 SubsystemDetail.vue 统一详情模板(统计卡片 + 门户卡片 + 打开按钮)
- 新增路由 /subsystem/detail/:name
- subsystem API mock 数据补充 label、domain、category 字段
- SubsystemCard 概览卡片同步显示 label + name 格式
This commit is contained in:
2026-07-17 11:20:17 +08:00
parent a185fac1b2
commit 506752747d
5 changed files with 316 additions and 26 deletions
+33 -8
View File
@@ -5,9 +5,12 @@ import { redirectToSSO } from '@/utils/sso'
export interface Subsystem {
id: number
name: string
label: string
description: string
icon: string
url: string
domain: string
category: string
status: 'integrated' | 'integrating'
sso_enabled: boolean
}
@@ -21,54 +24,72 @@ const mockSubsystems: Subsystem[] = [
{
id: 1,
name: 'Wayne',
description: '多集群容器管理平台',
label: '多集群发布平台',
description: '业务容器发布、命名空间与配额管理,复用 Wayne 原生多租户能力。',
icon: 'W',
url: 'https://wayne.qiniu.com',
domain: 'wayne.xinfra.internal',
category: '容器管理',
status: 'integrated',
sso_enabled: true,
},
{
id: 2,
name: 'CloudDM',
description: '数据库管理与SQL审核',
label: 'SQL 审核平台',
description: '数据库 SQL 上线统一审核,支持 LDAP 用户组到角色的自动映射。',
icon: 'DM',
url: 'https://clouddm.qiniu.com',
domain: 'clouddm.xinfra.internal',
category: '数据库',
status: 'integrated',
sso_enabled: true,
},
{
id: 3,
name: 'CacheCloud',
description: 'Redis 云管理平台',
label: 'Redis 管理平台',
description: 'Redis 实例全生命周期管理,登录与监控运维统一入口。',
icon: 'CC',
url: 'https://cachecloud.qiniu.com',
status: 'integrated',
domain: 'cachecloud.xinfra.internal',
category: '缓存',
status: 'integrating',
sso_enabled: true,
},
{
id: 4,
name: 'Apollo',
description: '配置中心',
label: '配置中心',
description: '统一配置管理,按机房 Cluster 隔离,支持灰度发布、版本回滚与变更审计。',
icon: 'AP',
url: 'https://apollo.xinfra.internal',
status: 'integrated',
domain: 'apollo.xinfra.internal',
category: '配置中心',
status: 'integrating',
sso_enabled: true,
},
{
id: 5,
name: 'qpass',
description: '密码管理平台',
label: '七牛统一运维平台',
description: '夜莺、Zabbix、VictoriaMetrics 告警统一收敛与值班通知,按 P0/P1 分级推送。',
icon: 'QP',
url: 'https://qpass.xinfra.internal',
domain: 'qpass.xinfra.internal',
category: '告警值班',
status: 'integrated',
sso_enabled: true,
},
{
id: 6,
name: 'Grafana',
description: '监控可视化平台',
label: '指标可视化平台',
description: '对接 VictoriaMetrics 数据源,K8s / 容器 / 业务指标统一仪表盘展示。',
icon: 'GF',
url: 'https://grafana.xinfra.internal',
domain: 'grafana.xinfra.internal',
category: '可观测性',
status: 'integrated',
sso_enabled: true,
},
@@ -83,6 +104,10 @@ export const subsystemApi = {
})
},
getSubsystemByName(name: string): Subsystem | undefined {
return mockSubsystems.find((s) => s.name.toLowerCase() === name.toLowerCase())
},
async getSSOUrl(id: number): Promise<ApiResponse<SSOResponse>> {
const subsystem = mockSubsystems.find((s) => s.id === id)
if (!subsystem) {
+100 -1
View File
@@ -35,9 +35,28 @@
</div>
<div class="nav-group">
<div class="nav-label">统一入口</div>
<router-link to="/subsystem" class="nav-item" active-class="active">
<div class="nav-item nav-expand" :class="{ active: isPortalActive }">
<router-link to="/subsystem" class="expand-label">
<span class="ic">↗</span>子系统导航
</router-link>
<span class="expand-toggle" @click="portalExpanded = !portalExpanded">
{{ portalExpanded ? '▾' : '▸' }}
</span>
</div>
<div v-show="portalExpanded" class="nav-children">
<router-link
v-for="sys in subsystems"
:key="sys.name"
:to="`/subsystem/detail/${sys.name.toLowerCase()}`"
class="nav-item nav-child"
active-class="active"
>
<span class="child-dot" :style="{ background: dotColor(sys.icon) }"></span>{{ sys.label }} {{ sys.name }}
</router-link>
<router-link to="/subsystem" class="nav-item nav-child" active-class="active">
<span class="ic" style="font-size:12px;">▦</span>全部总览
</router-link>
</div>
<router-link to="/subsystem/authz" class="nav-item" active-class="active">
<span class="ic">▦</span>子系统赋权
</router-link>
@@ -58,6 +77,36 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const portalExpanded = ref(true)
const subsystems = [
{ name: 'Wayne', label: '多集群发布平台', icon: 'W' },
{ name: 'CloudDM', label: 'SQL 审核平台', icon: 'DM' },
{ name: 'CacheCloud', label: 'Redis 管理平台', icon: 'CC' },
{ name: 'Apollo', label: '配置中心', icon: 'AP' },
{ name: 'qpass', label: '七牛统一运维平台', icon: 'QP' },
{ name: 'Grafana', label: '指标可视化平台', icon: 'GF' },
]
const isPortalActive = computed(() => {
return route.path.startsWith('/subsystem/detail/') || route.path === '/subsystem'
})
function dotColor(icon: string): string {
const colors: Record<string, string> = {
W: 'var(--tag-blue-text)',
DM: 'var(--tag-green-text)',
CC: 'var(--err)',
AP: 'var(--tag-blue-text)',
QP: 'var(--tag-amber-text)',
GF: 'var(--warn)',
}
return colors[icon] || 'var(--text-dim)'
}
</script>
<style scoped>
@@ -117,6 +166,10 @@
border-color: var(--accent);
}
.nav-item.active .expand-label {
color: var(--accent);
}
.nav-item .badge {
margin-left: auto;
font-family: var(--mono);
@@ -142,4 +195,50 @@
color: var(--text-dim);
line-height: 1.6;
}
.nav-expand {
justify-content: flex-start;
padding: 0;
user-select: none;
}
.expand-label {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
padding: 10px;
text-decoration: none;
color: inherit;
}
.expand-toggle {
padding: 10px;
font-size: 12px;
opacity: 0.6;
cursor: pointer;
border-radius: 0 6px 6px 0;
}
.expand-toggle:hover {
opacity: 1;
background: var(--bg-panel-2);
}
.nav-children {
padding-left: 8px;
}
.nav-child {
padding: 7px 10px 7px 26px;
font-size: 14px;
gap: 8px;
}
.child-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
</style>
+3 -15
View File
@@ -5,11 +5,11 @@
{{ system.icon }}
</div>
<div>
<h4>{{ system.name }}</h4>
<div class="ext-sub">{{ system.description }}</div>
<h4>{{ system.label }} {{ system.name }}</h4>
<div class="ext-sub">{{ system.domain }}</div>
</div>
</div>
<p>{{ description }}</p>
<p>{{ system.description }}</p>
<div class="sso-row">
<span class="sso-dot" :class="{ warn: system.status === 'integrating' }"></span>
{{ system.status === 'integrated' ? 'LDAP 原生配置接入 · 零代码' : 'LDAP 接入改造中' }}
@@ -49,18 +49,6 @@ const iconColor = computed(() => {
return colors[props.system.icon] || 'var(--text-mid)'
})
const description = computed(() => {
const descs: Record<string, string> = {
Wayne: '业务容器发布、命名空间与配额管理,复用 Wayne 原生多租户能力。',
CloudDM: '数据库 SQL 上线统一审核,支持 LDAP 用户组到角色的自动映射。',
CacheCloud: 'Redis 实例全生命周期管理,登录与监控运维统一入口。',
Apollo: '统一配置管理,按机房 Cluster 隔离,支持灰度发布、版本回滚与变更审计。',
qpass: '夜莺、Zabbix、VictoriaMetrics 告警统一收敛与值班通知,按 P0/P1 分级推送。',
Grafana: '对接 VictoriaMetrics 数据源,K8s / 容器 / 业务指标统一仪表盘展示。',
}
return descs[props.system.name] || ''
})
const handleClick = async () => {
if (props.system.sso_enabled) {
const { data } = await subsystemApi.getSSOUrl(props.system.id)
+6
View File
@@ -34,6 +34,12 @@ const router = createRouter({
component: () => import('@/views/subsystem/Navigation.vue'),
meta: { title: '子系统导航' },
},
{
path: 'subsystem/detail/:name',
name: 'SubsystemDetail',
component: () => import('@/views/subsystem/SubsystemDetail.vue'),
meta: { title: '子系统详情' },
},
{
path: 'subsystem/authz',
name: 'SubsystemAuthz',
@@ -0,0 +1,172 @@
<template>
<div v-if="system">
<div class="page-head">
<div>
<h1>{{ system.name }}</h1>
<p>{{ system.description }} · {{ system.domain }}</p>
</div>
<span class="env-pill" :class="system.status === 'integrated' ? '' : 'warn'">
{{ system.status === 'integrated' ? '● 已接入' : '● 接入中' }}
</span>
</div>
<div class="stat-row">
<div class="stat-card">
<div class="label">接入状态</div>
<div class="value" :class="system.status === 'integrated' ? 'accent' : 'warn'">
{{ system.status === 'integrated' ? '已接入' : '接入中' }}
</div>
<div class="delta">{{ system.category }}</div>
</div>
<div class="stat-card">
<div class="label">SSO 登录</div>
<div class="value" :class="system.sso_enabled ? 'accent' : ''">
{{ system.sso_enabled ? '已启用' : '未启用' }}
</div>
<div class="delta">LDAP 原生配置 · 同账号同密码</div>
</div>
<div class="stat-card">
<div class="label">访问域名</div>
<div class="value" style="font-size:14px;">{{ system.domain }}</div>
<div class="delta">内网 DNS 解析</div>
</div>
</div>
<div class="panel">
<div class="panel-head">
<h3>统一入口</h3>
<span class="meta">{{ system.name }} · LDAP 单点登录</span>
</div>
<div class="panel-body" style="padding:16px;">
<div class="ext-card" style="max-width:560px;">
<div class="ext-top">
<div class="ext-logo" :style="{ background: bgColor, color: iconColor }">
{{ system.icon }}
</div>
<div>
<h4>{{ system.name }}</h4>
<div class="ext-sub">{{ system.domain }}</div>
</div>
</div>
<p>{{ detailDesc }}</p>
<div class="sso-row">
<span class="sso-dot" :class="{ warn: system.status === 'integrating' }"></span>
{{ system.status === 'integrated' ? 'LDAP 原生配置接入 · 已上线' : 'LDAP 接入改造中' }}
</div>
<button class="btn btn-primary" style="margin-top:8px;align-self:flex-start;" @click="openPortal">
打开 {{ system.name }} ↗
</button>
</div>
</div>
</div>
</div>
<div v-else class="page-head">
<div><h1>子系统未找到</h1><p>请从左侧导航选择一个子系统</p></div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { subsystemApi, type Subsystem } from '@/api/subsystem'
import { getToken } from '@/utils/auth'
import { redirectToSSO } from '@/utils/sso'
const route = useRoute()
const system = computed<Subsystem | undefined>(() => {
const name = route.params.name as string
return subsystemApi.getSubsystemByName(name)
})
const bgColor = computed(() => {
if (!system.value) return 'var(--bg-panel-2)'
const colors: Record<string, string> = {
W: 'var(--logo-w-bg)',
DM: 'var(--logo-dm-bg)',
CC: 'var(--logo-cc-bg)',
AP: 'var(--logo-ap-bg)',
QP: 'var(--logo-qp-bg)',
GF: 'var(--logo-gf-bg)',
}
return colors[system.value.icon] || 'var(--bg-panel-2)'
})
const iconColor = computed(() => {
if (!system.value) return 'var(--text-mid)'
const colors: Record<string, string> = {
W: 'var(--tag-blue-text)',
DM: 'var(--tag-green-text)',
CC: 'var(--err)',
AP: 'var(--tag-blue-text)',
QP: 'var(--tag-amber-text)',
GF: 'var(--warn)',
}
return colors[system.value.icon] || 'var(--text-mid)'
})
const detailDesc = computed(() => {
if (!system.value) return ''
const descs: Record<string, string> = {
Wayne: '业务容器发布、命名空间与配额管理,复用 Wayne 原生多租户能力。所有集群的容器发布均可通过此入口统一操作。',
CloudDM: '数据库 SQL 上线统一审核,支持 LDAP 用户组到角色的自动映射。变更记录可追溯,审计合规。',
CacheCloud: 'Redis 实例全生命周期管理,登录与监控运维统一入口。按业务线隔离实例,统一密码管理。',
Apollo: '统一管理多机房 Config Service Cluster 的配置发布、灰度、回滚与变更审计,所有机房配置项均可在此单一入口检索与编辑。',
qpass: '夜莺、Zabbix、VictoriaMetrics 告警统一收敛与值班通知,按 P0/P1 分级推送。值班表自动轮转。',
Grafana: '对接 VictoriaMetrics 数据源,K8s / 容器 / 业务指标统一仪表盘展示。预置核心看板,支持自定义。',
}
return descs[system.value.name] || system.value.description
})
const openPortal = async () => {
if (!system.value) return
if (system.value.sso_enabled) {
try {
const { data } = await subsystemApi.getSSOUrl(system.value.id)
window.location.assign(data.sso_url)
} catch {
// 错误已由 request 拦截器统一处理
}
} else {
window.open(system.value.url, '_blank')
}
}
</script>
<style scoped>
.env-pill {
font-size: 13px;
color: var(--accent);
background: var(--accent-dim);
padding: 4px 14px;
border-radius: 20px;
white-space: nowrap;
}
.env-pill.warn {
color: var(--warn);
background: rgba(255, 180, 0, 0.1);
}
.panel-body .ext-card {
display: flex;
flex-direction: column;
gap: 10px;
}
.btn-primary {
background: var(--accent);
color: #fff;
border: none;
padding: 8px 18px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-primary:hover {
opacity: 0.85;
}
</style>