Merge pull request #82 from 2311719626/feature/frontend-bl-scoped-delivery
feat(frontend): v3→v4 前端对齐重构 review完没有问题仅微调前端代码
This commit is contained in:
Vendored
+1
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
AppMain: typeof import('./src/components/Layout/AppMain.vue')['default']
|
||||
AppSidebar: typeof import('./src/components/Layout/AppSidebar.vue')['default']
|
||||
AuditLogTable: typeof import('./src/components/AuditLogTable.vue')['default']
|
||||
BusinessLineSwitcher: typeof import('./src/components/BusinessLineSwitcher.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
|
||||
@@ -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: '七牛云内部系统。',
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="bl-switch" ref="switchRef">
|
||||
<div class="bl-trigger" @click="toggleMenu">
|
||||
<div
|
||||
class="bl-ic"
|
||||
:style="{ background: currentBL?.iconBg, color: currentBL?.iconColor }"
|
||||
>
|
||||
{{ currentBL?.iconText }}
|
||||
</div>
|
||||
<span class="bl-name-trigger">{{ currentBL?.name || '未选择' }}</span>
|
||||
<span class="bl-role" v-if="currentBL">{{ currentBL.role }} · 授权 {{ authCount }} 子系统</span>
|
||||
<span class="caret">▼</span>
|
||||
</div>
|
||||
<Transition name="bl-menu">
|
||||
<div class="bl-menu" v-show="menuVisible">
|
||||
<div class="bl-menu-label">切换业务线(仅展示被授权范围)</div>
|
||||
<div
|
||||
v-for="bl in businessLines"
|
||||
:key="bl.id"
|
||||
class="bl-opt"
|
||||
:class="{ sel: bl.id === currentBL?.id, locked: !bl.authorized }"
|
||||
@click="handleSelect(bl)"
|
||||
>
|
||||
<div
|
||||
class="bl-ic"
|
||||
:style="{ background: bl.iconBg, color: bl.iconColor }"
|
||||
>
|
||||
{{ bl.iconText }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="bl-name">{{ bl.name }}</div>
|
||||
<div class="bl-sub">{{ bl.ou }} · {{ bl.role }}</div>
|
||||
</div>
|
||||
<span v-if="bl.authorized" class="bl-check">✓</span>
|
||||
<span v-else class="bl-lock">🔒 无权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useBusinessLineStore, type BusinessLine } from '@/stores/businessLine'
|
||||
|
||||
const blStore = useBusinessLineStore()
|
||||
|
||||
const businessLines = computed(() => blStore.businessLines)
|
||||
const currentBL = computed(() => blStore.currentBL)
|
||||
const authCount = computed(() => blStore.authorizedBLs.length)
|
||||
|
||||
const menuVisible = ref(false)
|
||||
const switchRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function toggleMenu() {
|
||||
menuVisible.value = !menuVisible.value
|
||||
}
|
||||
|
||||
function handleSelect(bl: BusinessLine) {
|
||||
if (!bl.authorized) return
|
||||
blStore.switchBL(bl.id)
|
||||
menuVisible.value = false
|
||||
ElMessage.success(`已切换业务线:${bl.name}(资源与告警按授权范围过滤)`)
|
||||
}
|
||||
|
||||
function onClickOutside(e: MouseEvent) {
|
||||
if (switchRef.value && !switchRef.value.contains(e.target as Node)) {
|
||||
menuVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bl-switch {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bl-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
background: var(--bg-panel-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
color: var(--text-hi);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.bl-trigger:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.bl-trigger .bl-ic {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bl-trigger .caret {
|
||||
color: var(--text-dim);
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.bl-trigger .bl-role {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.bl-name-trigger {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.bl-menu {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
z-index: 60;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.bl-menu-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 8px 10px 4px;
|
||||
}
|
||||
|
||||
.bl-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.bl-opt:hover {
|
||||
background: var(--bg-panel-2);
|
||||
}
|
||||
|
||||
.bl-opt.sel {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.bl-opt .bl-ic {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bl-opt .bl-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-hi);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bl-opt .bl-sub {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.bl-opt .bl-check {
|
||||
margin-left: auto;
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bl-opt.sel .bl-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bl-opt.locked {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bl-opt.locked .bl-lock {
|
||||
margin-left: auto;
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 下拉动画 */
|
||||
.bl-menu-enter-active,
|
||||
.bl-menu-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.bl-menu-enter-from,
|
||||
.bl-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
xinfra <span class="sub">统一基础设施平台</span>
|
||||
</div>
|
||||
<span class="env-pill">● 生产环境</span>
|
||||
<BusinessLineSwitcher />
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<el-icon><Search /></el-icon>
|
||||
@@ -23,7 +24,7 @@
|
||||
<div class="avatar">{{ userInitial }}</div>
|
||||
<div>
|
||||
<div>{{ user?.display_name || '未登录' }}</div>
|
||||
<div class="ldap-tag">LDAP · {{ user?.business_line || '' }}业务线</div>
|
||||
<div class="ldap-tag">LDAP · {{ user?.business_line || '' }}</div>
|
||||
</div>
|
||||
<el-button class="logout-button" text @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
@@ -36,6 +37,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import BusinessLineSwitcher from '@/components/BusinessLineSwitcher.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { logout } = useAuth()
|
||||
|
||||
@@ -7,46 +7,59 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">机房 & 网络</div>
|
||||
<router-link to="/resource/status" class="nav-item" active-class="active">
|
||||
<span class="ic">⬡</span>资源状态看板
|
||||
<div class="nav-label">业务交付</div>
|
||||
<router-link to="/service/catalog" class="nav-item" active-class="active">
|
||||
<span class="ic">▤</span>基础服务
|
||||
</router-link>
|
||||
<router-link to="/cluster" class="nav-item" active-class="active">
|
||||
<span class="ic">◆</span>集群 / 节点<span class="badge">7</span>
|
||||
<router-link to="/tasks" class="nav-item" active-class="active">
|
||||
<span class="ic">▧</span>任务日志<span class="badge">2</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">资源管理</div>
|
||||
<div class="nav-label">资源纳管</div>
|
||||
<router-link to="/resource/mgmt" class="nav-item" active-class="active">
|
||||
<span class="ic">▥</span>资源管理<span class="badge">CMDB</span>
|
||||
</router-link>
|
||||
<router-link to="/service/mgmt" class="nav-item" active-class="active">
|
||||
<span class="ic">◈</span>服务管理<span class="badge">Consul</span>
|
||||
<router-link to="/cluster" class="nav-item" active-class="active">
|
||||
<span class="ic">◆</span>集群与容器<span class="badge">3</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">服务交付</div>
|
||||
<router-link to="/service/catalog" class="nav-item" active-class="active">
|
||||
<span class="ic">▤</span>基础服务目录
|
||||
</router-link>
|
||||
<router-link to="/tasks" class="nav-item" active-class="active">
|
||||
<span class="ic">▧</span>任务中心<span class="badge">2</span>
|
||||
</router-link>
|
||||
<router-link to="/config" class="nav-item" active-class="active">
|
||||
<span class="ic">⚙</span>配置中心管理<span class="badge">Apollo</span>
|
||||
<router-link to="/tenants" class="nav-item" active-class="active">
|
||||
<span class="ic">▦</span>业务配额
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">可观测性</div>
|
||||
<router-link to="/monitor" class="nav-item" active-class="active">
|
||||
<span class="ic">◎</span>监控 / 日志 / 告警
|
||||
<span class="ic">◎</span>监控
|
||||
</router-link>
|
||||
<router-link to="/alert" class="nav-item" active-class="active">
|
||||
<span class="ic">⚡</span>告警
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">统一入口</div>
|
||||
<router-link to="/subsystem" class="nav-item" active-class="active">
|
||||
<span class="ic">↗</span>子系统导航
|
||||
</router-link>
|
||||
<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>
|
||||
@@ -61,12 +74,41 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="sidebar-foot">
|
||||
xinfra v3 一期 · 七机房容器化<br>RKE2 · Calico BGP · Ceph · LAS<br>AWS TGW + WireGuard · LDAP 认证
|
||||
</div>
|
||||
</aside>
|
||||
</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>
|
||||
@@ -126,6 +168,10 @@
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-item.active .expand-label {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-item .badge {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
@@ -151,4 +197,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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
@@ -68,7 +74,13 @@ const router = createRouter({
|
||||
path: 'cluster',
|
||||
name: 'Cluster',
|
||||
component: () => import('@/views/cluster/ClusterList.vue'),
|
||||
meta: { title: '集群 / 节点' },
|
||||
meta: { title: '集群与容器' },
|
||||
},
|
||||
{
|
||||
path: 'tenants',
|
||||
name: 'Tenants',
|
||||
component: () => import('@/views/resource/Tenants.vue'),
|
||||
meta: { title: '业务配额' },
|
||||
},
|
||||
{
|
||||
path: 'service/catalog',
|
||||
@@ -98,7 +110,13 @@ const router = createRouter({
|
||||
path: 'monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/monitor/Monitor.vue'),
|
||||
meta: { title: '监控 / 日志 / 告警' },
|
||||
meta: { title: '监控' },
|
||||
},
|
||||
{
|
||||
path: 'alert',
|
||||
name: 'Alert',
|
||||
component: () => import('@/views/monitor/Alert.vue'),
|
||||
meta: { title: '告警' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: string
|
||||
name: string
|
||||
ou: string
|
||||
role: string
|
||||
iconText: string
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
authorized: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'xinfra-current-bl'
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
const MOCK_BUSINESS_LINES: BusinessLine[] = [
|
||||
{
|
||||
id: 'las',
|
||||
name: 'LAS 业务线',
|
||||
ou: 'ou=las',
|
||||
role: 'SRE',
|
||||
iconText: 'LA',
|
||||
iconBg: '#1A1430',
|
||||
iconColor: '#C9A6FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'kodo',
|
||||
name: 'Kodo 业务线',
|
||||
ou: 'ou=kodo',
|
||||
role: '研发',
|
||||
iconText: 'KO',
|
||||
iconBg: '#0E1C2C',
|
||||
iconColor: '#8EC8FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'lingxi',
|
||||
name: '灵矽 业务线',
|
||||
ou: 'ou=lingxi',
|
||||
role: '未授权',
|
||||
iconText: 'LX',
|
||||
iconBg: '#241B0A',
|
||||
iconColor: '#FFC97A',
|
||||
authorized: false,
|
||||
},
|
||||
{
|
||||
id: 'ltoken',
|
||||
name: 'LTOKEN 业务线',
|
||||
ou: 'ou=ltoken',
|
||||
role: '研发',
|
||||
iconText: 'LT',
|
||||
iconBg: '#1A2A1A',
|
||||
iconColor: '#A6FFB0',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'maas',
|
||||
name: 'MAAS 业务线',
|
||||
ou: 'ou=maas',
|
||||
role: '未授权',
|
||||
iconText: 'MA',
|
||||
iconBg: '#2A1A2A',
|
||||
iconColor: '#FFB0E0',
|
||||
authorized: false,
|
||||
},
|
||||
]
|
||||
|
||||
function loadSavedBL(): BusinessLine | null {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved) return JSON.parse(saved)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>(MOCK_BUSINESS_LINES)
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
|
||||
// 默认选中第一个已授权的业务线
|
||||
if (!currentBL.value) {
|
||||
currentBL.value = businessLines.value.find((bl) => bl.authorized) || null
|
||||
}
|
||||
|
||||
const authorizedBLs = computed(() =>
|
||||
businessLines.value.filter((bl) => bl.authorized),
|
||||
)
|
||||
|
||||
function switchBL(id: string) {
|
||||
const bl = businessLines.value.find((b) => b.id === id)
|
||||
if (!bl || !bl.authorized) return
|
||||
currentBL.value = bl
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
}
|
||||
|
||||
return {
|
||||
businessLines,
|
||||
currentBL,
|
||||
authorizedBLs,
|
||||
switchBL,
|
||||
}
|
||||
})
|
||||
@@ -2,10 +2,10 @@
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>集群 / 节点</h1>
|
||||
<p>按机房切分的 RKE2 集群,Calico BGP 扁平网络</p>
|
||||
<h1>集群与容器</h1>
|
||||
<p>统一建设容器池 · RKE2 上建 Wayne 后新服务的标准发布流程 · Calico BGP 扁平网络</p>
|
||||
</div>
|
||||
<el-button type="primary">+ 加入新节点</el-button>
|
||||
<el-button type="primary">同步</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>告警</h1>
|
||||
<p>夜莺(Nightingale)统一告警引擎 · qpass 推送</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cols-2-equal">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>当前告警</h3>
|
||||
<span class="meta">实时</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="tr-hover">
|
||||
<td>硬件告警</td>
|
||||
<td class="status-text warn">● 2 条</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td>业务告警</td>
|
||||
<td class="status-text ok">● 0 条</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td>今日已推送</td>
|
||||
<td class="mono strong">5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>告警通道</h3>
|
||||
<span class="meta">qpass</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="tr-hover">
|
||||
<td>推送状态</td>
|
||||
<td class="status-text ok">● 正常</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td>通道类型</td>
|
||||
<td>企业微信 · 钉钉 · 短信</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>告警记录</h3>
|
||||
<span class="meta">近 24h</span>
|
||||
</div>
|
||||
<div class="panel-body log-stream">
|
||||
<div v-for="(alert, index) in alerts" :key="index" :class="['task-log-line', alert.class]">
|
||||
<span class="t">{{ alert.time }}</span>{{ alert.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const alerts = ref([
|
||||
{ time: '07:38:55', message: '[Zabbix] yzh-las-014 风扇转速低于阈值(FAN_6: 1920 RPM)', class: '' },
|
||||
{ time: '07:38:55', message: '[Zabbix] yzh-las-014 风扇转速低于阈值(FAN_5: 1880 RPM)', class: '' },
|
||||
{ time: '07:30:00', message: '[qpass] 告警已合并推送:1 条硬件告警 + 0 条业务告警', class: '' },
|
||||
{ time: '06:15:22', message: '[Nightingale] sg-las-007 CPU 使用率恢复(当前 42%)', class: 'ok' },
|
||||
{ time: '05:48:10', message: '[Nightingale] sg-las-007 CPU 使用率超过 90% 持续 5 分钟', class: '' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -2,8 +2,8 @@
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>监控 / 日志 / 告警</h1>
|
||||
<p>VictoriaMetrics 指标 · Zabbix 硬件状态 · ELK 日志中心 · 夜莺(Nightingale)统一告警引擎对接 qpass</p>
|
||||
<h1>监控</h1>
|
||||
<p>VictoriaMetrics 指标 · Zabbix 硬件状态</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,32 +67,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>日志 + 统一告警</h3>
|
||||
<span class="meta">Filebeat → Kafka → ES → Kibana · VM + Zabbix → qpass</span>
|
||||
</div>
|
||||
<div class="panel-body log-stream">
|
||||
<div v-for="(log, index) in logs" :key="index" :class="['task-log-line', log.class]">
|
||||
<span class="t">{{ log.time }}</span>{{ log.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const logs = ref([
|
||||
{ time: '07:39:10', message: 'CMDB 采集器同步完成:xs291 · 华东-杭州下沙机房', class: '' },
|
||||
{ time: '07:38:55', message: '[Zabbix] 告警:yzh-las-014 风扇转速低于阈值(FAN_6: 1920 RPM)', class: '' },
|
||||
{ time: '07:38:02', message: '[VictoriaMetrics] 七机房抓取任务全部正常', class: 'ok' },
|
||||
{ time: '07:35:41', message: '[ELK] sg-las-007 应用日志接入 Kafka topic:las-sg-app', class: '' },
|
||||
{ time: '07:30:00', message: '[qpass] 告警已合并推送:1 条硬件告警 + 0 条业务告警', class: '' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .page-head, .cols-2-equal, .panel, table, .status-text, .log-stream, .task-log-line 已在 global.css 中定义 */
|
||||
/* .page-head, .cols-2-equal, .panel, table, .status-text 已在 global.css 中定义 */
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1>资源管理</h1>
|
||||
<p>统一纳管物理机与虚机资源 · 基础数据来自 SINA CMDB</p>
|
||||
</div>
|
||||
<el-button type="primary">+ 创建虚机</el-button>
|
||||
<el-button type="primary">同步</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>业务配额</h1>
|
||||
<p>按业务线划分资源配额与隔离策略 · RBAC 权限随业务线自动具备</p>
|
||||
</div>
|
||||
<el-button type="primary">+ 新增业务线</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>业务线列表</h3>
|
||||
<span class="meta">按业务线隔离命名空间、节点标签与配额</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>业务线</th>
|
||||
<th>标识</th>
|
||||
<th>命名空间</th>
|
||||
<th>节点数</th>
|
||||
<th>CPU 配额</th>
|
||||
<th>内存配额</th>
|
||||
<th>状态</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tenant in tenants" :key="tenant.key" class="tr-hover">
|
||||
<td class="strong">{{ tenant.name }}</td>
|
||||
<td class="mono">{{ tenant.key }}</td>
|
||||
<td class="mono">{{ tenant.namespace }}</td>
|
||||
<td class="mono">{{ tenant.nodes }}</td>
|
||||
<td>
|
||||
<span class="bar-wrap">
|
||||
<span class="bar-fill" :class="tenant.cpuClass" :style="{ width: tenant.cpuPct + '%' }"></span>
|
||||
</span>
|
||||
{{ tenant.cpuPct }}%
|
||||
</td>
|
||||
<td>
|
||||
<span class="bar-wrap">
|
||||
<span class="bar-fill" :class="tenant.memClass" :style="{ width: tenant.memPct + '%' }"></span>
|
||||
</span>
|
||||
{{ tenant.memPct }}%
|
||||
</td>
|
||||
<td :class="['status-text', tenant.statusClass]">● {{ tenant.status }}</td>
|
||||
<td>
|
||||
<el-button text>详情</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-note" style="max-width: 720px;">
|
||||
<b>配额与隔离策略:</b>每个业务线绑定独立的 K8s namespace 与 node-label,资源配额通过 ResourceQuota 限制 CPU / 内存上限。
|
||||
RBAC 角色(viewer / operator / admin)随业务线自动具备,子系统(Wayne / CloudDM)凭 SSO 身份免登进入。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const tenants = ref([
|
||||
{
|
||||
name: 'LAS',
|
||||
key: 'las',
|
||||
namespace: 'ns-las-prod',
|
||||
nodes: 12,
|
||||
cpuPct: 64,
|
||||
cpuClass: '',
|
||||
memPct: 71,
|
||||
memClass: '',
|
||||
status: '正常',
|
||||
statusClass: 'ok',
|
||||
},
|
||||
{
|
||||
name: 'KODO',
|
||||
key: 'kodo',
|
||||
namespace: 'ns-kodo-prod',
|
||||
nodes: 8,
|
||||
cpuPct: 45,
|
||||
cpuClass: '',
|
||||
memPct: 52,
|
||||
memClass: '',
|
||||
status: '正常',
|
||||
statusClass: 'ok',
|
||||
},
|
||||
{
|
||||
name: 'PILI',
|
||||
key: 'pili',
|
||||
namespace: 'ns-pili-prod',
|
||||
nodes: 6,
|
||||
cpuPct: 82,
|
||||
cpuClass: 'warn',
|
||||
memPct: 78,
|
||||
memClass: '',
|
||||
status: '资源告警',
|
||||
statusClass: 'warn',
|
||||
},
|
||||
{
|
||||
name: 'QVM',
|
||||
key: 'qvm',
|
||||
namespace: 'ns-qvm-prod',
|
||||
nodes: 4,
|
||||
cpuPct: 23,
|
||||
cpuClass: '',
|
||||
memPct: 31,
|
||||
memClass: '',
|
||||
status: '正常',
|
||||
statusClass: 'ok',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 公共样式已在 global.css 中定义 */
|
||||
</style>
|
||||
@@ -3,59 +3,304 @@
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>基础服务目录</h1>
|
||||
<p>点击卡片,通过封装好的 ansible-playbook 一键部署标准化基础服务</p>
|
||||
<p>点击卡片,通过封装好的 ansible-playbook 一键标准化交付,完成后自动注册进对应子系统</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="svc-grid">
|
||||
<div v-for="service in services" :key="service.name" class="svc-card" @click="handleDeploy(service)">
|
||||
<div
|
||||
v-for="service in services"
|
||||
:key="service.name"
|
||||
class="svc-card"
|
||||
:class="{ disabled: service.disabled }"
|
||||
:style="service.disabled ? { opacity: service.opacity } : {}"
|
||||
@click="handleCardClick(service)"
|
||||
>
|
||||
<div class="svc-top">
|
||||
<div class="svc-ic" :style="{ background: service.bgColor, color: service.iconColor }">
|
||||
{{ service.icon }}
|
||||
</div>
|
||||
<h4>{{ service.name }}</h4>
|
||||
<h4 :style="service.disabled && service.icon === '+' ? { color: 'var(--text-dim)' } : {}">{{ service.name }}</h4>
|
||||
</div>
|
||||
<div class="svc-desc">{{ service.description }}</div>
|
||||
<div class="svc-foot">
|
||||
<span class="playbook">{{ service.playbook }}</span>
|
||||
<span>{{ service.version }}</span>
|
||||
<span :style="service.statusColor ? { color: service.statusColor } : {}">{{ service.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗:发布服务 · 跳 Wayne -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showWayneModal" class="overlay" @click.self="showWayneModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-head">
|
||||
<h3>发布服务 · 跳 Wayne</h3>
|
||||
<button class="close" @click="showWayneModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>业务线</label>
|
||||
<select v-model="wayneForm.bl">
|
||||
<option value="las">las(当前)</option>
|
||||
<option value="kodo">kodo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>命名空间</label>
|
||||
<select v-model="wayneForm.namespace">
|
||||
<option value="ns-las-prod">ns-las-prod</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>目标集群</label>
|
||||
<select v-model="wayneForm.cluster">
|
||||
<option value="rke2-sh-prod-01">rke2-sh-prod-01(华东)</option>
|
||||
<option value="rke2-bj-prod-01">rke2-bj-prod-01(华北)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>规格</label>
|
||||
<select v-model="wayneForm.spec">
|
||||
<option value="2C4G">2C 4G × 3 副本</option>
|
||||
<option value="4C8G">4C 8G × 3 副本</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>服务名称</label>
|
||||
<input v-model="wayneForm.serviceName" placeholder="las-search-api" />
|
||||
</div>
|
||||
<div class="tooltip-note">
|
||||
凭 SSO 身份免登进入 <b>Wayne</b>,在 ns-las-prod 下部署服务;权限随业务线 / 角色自动具备,仅可操作被授权 namespace。
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-ghost" @click="showWayneModal = false">取消</button>
|
||||
<button class="btn btn-primary" @click="handleWayneSubmit">跳 Wayne 发布 ↗</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 弹窗:数据库标准化交付 · MySQL -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showMysqlModal" class="overlay" @click.self="showMysqlModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-head">
|
||||
<h3>数据库标准化交付 · MySQL</h3>
|
||||
<button class="close" @click="showMysqlModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>业务线</label>
|
||||
<select v-model="mysqlForm.bl">
|
||||
<option value="las">las(当前)</option>
|
||||
<option value="kodo">kodo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>架构模式</label>
|
||||
<select v-model="mysqlForm.topology">
|
||||
<option value="1m2r">一主两从</option>
|
||||
<option value="1m1r">一主一从</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>规格</label>
|
||||
<select v-model="mysqlForm.spec">
|
||||
<option value="8C32G">8C 32G · 500G SSD(Ceph RBD)</option>
|
||||
<option value="16C64G">16C 64G · 1T SSD</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>版本</label>
|
||||
<select v-model="mysqlForm.version">
|
||||
<option value="8.0.36">MySQL 8.0.36</option>
|
||||
<option value="5.7.44">MySQL 5.7.44</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>实例名称</label>
|
||||
<input v-model="mysqlForm.instanceName" placeholder="mysql-las-billing-02" />
|
||||
</div>
|
||||
<div class="yaml-preview"><span class="c"># ansible-runner 调用预览</span>
|
||||
<span class="k">playbook</span>: roles/mysql-deploy
|
||||
<span class="k">tenant</span>: <span class="s">{{ mysqlForm.bl }}</span>
|
||||
<span class="k">topology</span>: <span class="s">{{ mysqlForm.topology === '1m2r' ? '1-master-2-replica' : '1-master-1-replica' }}</span>
|
||||
<span class="k">node_selector</span>: <span class="s">business-line={{ mysqlForm.bl }}</span>
|
||||
<span class="k">storage_class</span>: <span class="s">ceph-rbd-ssd</span>
|
||||
<span class="k">register_to</span>: <span class="s">clouddm</span> <span class="c"># 自动注册数据源,跳 CloudDM 上线</span></div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-ghost" @click="showMysqlModal = false">取消</button>
|
||||
<button class="btn btn-primary" @click="handleMysqlSubmit">确认交付</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const services = ref([
|
||||
{ name: 'MySQL', icon: 'My', bgColor: 'var(--tag-blue-bg)', iconColor: 'var(--tag-blue-text)', description: '一主多从,自动注册进 open-cdm 数据源,支持 LDAP 账号统一审核 SQL 上线。', playbook: 'roles/mysql-deploy', version: 'v8.0.36' },
|
||||
{ name: 'Redis', icon: 'Rd', bgColor: 'var(--logo-cc-bg)', iconColor: 'var(--err)', description: '支持 Standalone / Sentinel / Cluster,部署完成后自动注册进 CacheCloud。', playbook: 'roles/redis-deploy', version: 'v7.2' },
|
||||
{ name: 'openresty', icon: 'Or', bgColor: 'var(--logo-dm-bg)', iconColor: 'var(--tag-green-text)', description: '容器化部署在 K8s 内,作为业务边缘网关,支持灰度路由配置。', playbook: 'roles/openresty-deploy', version: 'v1.25' },
|
||||
{ name: 'dpvs', icon: 'Dp', bgColor: 'var(--logo-qp-bg)', iconColor: 'var(--tag-amber-text)', description: '四层负载均衡,直接管理宿主机网络命名空间,不纳入容器编排。', playbook: 'roles/dpvs-deploy', version: 'v1.9' },
|
||||
{ name: 'PgSQL', icon: 'Pg', bgColor: 'var(--tag-purple-bg)', iconColor: 'var(--tag-purple-text)', description: '流复制主从架构,规划中 · 第二期接入 open-cdm 统一审核体系。', playbook: 'roles/pgsql-deploy', version: '规划中' },
|
||||
const router = useRouter()
|
||||
|
||||
interface Service {
|
||||
name: string
|
||||
icon: string
|
||||
bgColor: string
|
||||
iconColor: string
|
||||
description: string
|
||||
playbook: string
|
||||
version: string
|
||||
statusColor?: string
|
||||
disabled?: boolean
|
||||
opacity?: number
|
||||
modal?: string
|
||||
}
|
||||
|
||||
const services = ref<Service[]>([
|
||||
{
|
||||
name: '容器服务(Wayne)',
|
||||
icon: 'Wy',
|
||||
bgColor: '#1C2A3A',
|
||||
iconColor: '#7FB8FF',
|
||||
description: '在 RKE2 容器池标准发布业务服务,跳 Wayne 免登管理多集群与命名空间。',
|
||||
playbook: 'wayne · ns-las-prod',
|
||||
version: '',
|
||||
modal: 'wayne',
|
||||
},
|
||||
{
|
||||
name: 'MySQL',
|
||||
icon: 'My',
|
||||
bgColor: '#1C2E4A',
|
||||
iconColor: '#7FB8FF',
|
||||
description: '一主多从标准化交付,完成后自动注册进 CloudDM / open-cdm 数据源,统一审核 SQL 上线。',
|
||||
playbook: 'roles/mysql-deploy',
|
||||
version: 'v8.0.36',
|
||||
modal: 'mysql',
|
||||
},
|
||||
{
|
||||
name: 'openresty',
|
||||
icon: 'Or',
|
||||
bgColor: '#1C3A2E',
|
||||
iconColor: '#7FFFC2',
|
||||
description: '容器化部署在 K8s 内,作为业务边缘网关,支持灰度路由配置。',
|
||||
playbook: 'roles/openresty-deploy',
|
||||
version: 'v1.25',
|
||||
modal: 'openresty',
|
||||
},
|
||||
{
|
||||
name: 'PgSQL',
|
||||
icon: 'Pg',
|
||||
bgColor: '#2E1C3A',
|
||||
iconColor: '#C9A6FF',
|
||||
description: '流复制主从架构,规划中 · 接入 CloudDM 统一审核体系。',
|
||||
playbook: 'roles/pgsql-deploy',
|
||||
version: '规划中',
|
||||
statusColor: 'var(--text-dim)',
|
||||
disabled: true,
|
||||
opacity: 0.8,
|
||||
},
|
||||
{
|
||||
name: 'Redis',
|
||||
icon: 'Rd',
|
||||
bgColor: '#3A1C28',
|
||||
iconColor: '#FF8E9C',
|
||||
description: 'Redis 管理由 CacheCloud 承载,规划接入后开放标准化交付。',
|
||||
playbook: 'CacheCloud',
|
||||
version: '规划接入',
|
||||
disabled: true,
|
||||
opacity: 0.6,
|
||||
},
|
||||
{
|
||||
name: '接入新服务',
|
||||
icon: '+',
|
||||
bgColor: '#262C38',
|
||||
iconColor: 'var(--text-dim)',
|
||||
description: '封装新的 ansible-playbook,注册进基础服务目录',
|
||||
playbook: '',
|
||||
version: '',
|
||||
disabled: true,
|
||||
opacity: 0.5,
|
||||
},
|
||||
])
|
||||
|
||||
const handleDeploy = (service: any) => {
|
||||
ElMessage.success(`${service.name} 部署任务已提交`)
|
||||
// 弹窗状态
|
||||
const showWayneModal = ref(false)
|
||||
const showMysqlModal = ref(false)
|
||||
|
||||
const wayneForm = reactive({
|
||||
bl: 'las',
|
||||
namespace: 'ns-las-prod',
|
||||
cluster: 'rke2-sh-prod-01',
|
||||
spec: '2C4G',
|
||||
serviceName: 'las-search-api',
|
||||
})
|
||||
|
||||
const mysqlForm = reactive({
|
||||
bl: 'las',
|
||||
topology: '1m2r',
|
||||
spec: '8C32G',
|
||||
version: '8.0.36',
|
||||
instanceName: 'mysql-las-billing-02',
|
||||
})
|
||||
|
||||
const handleCardClick = (service: Service) => {
|
||||
if (service.disabled) {
|
||||
if (service.name === '接入新服务') return
|
||||
ElMessage.warning(`${service.name} 功能规划中,敬请期待`)
|
||||
return
|
||||
}
|
||||
if (service.modal === 'wayne') {
|
||||
showWayneModal.value = true
|
||||
} else if (service.modal === 'mysql') {
|
||||
showMysqlModal.value = true
|
||||
} else if (service.modal === 'openresty') {
|
||||
ElMessage.info('openresty 部署功能对接中,敬请期待')
|
||||
}
|
||||
}
|
||||
|
||||
const handleWayneSubmit = () => {
|
||||
showWayneModal.value = false
|
||||
ElMessage.success('已跳转 Wayne 并提交发布任务')
|
||||
router.push({ name: 'Tasks' })
|
||||
}
|
||||
|
||||
const handleMysqlSubmit = () => {
|
||||
showMysqlModal.value = false
|
||||
ElMessage.success('MySQL 标准化交付已提交,完成后自动注册 CloudDM')
|
||||
router.push({ name: 'Tasks' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .page-head, .svc-grid 已在 global.css 中定义 */
|
||||
|
||||
.svc-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.svc-card:hover {
|
||||
border-color: var(--hover-border);
|
||||
.svc-card:hover:not(.disabled) {
|
||||
border-color: #3A4356;
|
||||
}
|
||||
|
||||
.svc-card.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.svc-top {
|
||||
@@ -102,4 +347,183 @@ h4 {
|
||||
font-family: var(--mono);
|
||||
color: var(--text-mid);
|
||||
}
|
||||
|
||||
/* ========== overlay / modal ========== */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(7, 9, 13, 0.72);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
padding-top: 6vh;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 560px;
|
||||
max-width: 92vw;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--line-soft, var(--line));
|
||||
}
|
||||
|
||||
.modal-head h3 {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
|
||||
.modal-head .close {
|
||||
margin-left: auto;
|
||||
color: var(--text-dim);
|
||||
font-size: 18px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.modal-foot {
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid var(--line-soft, var(--line));
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ========== form fields ========== */
|
||||
.field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.field select,
|
||||
.field input {
|
||||
width: 100%;
|
||||
background: var(--bg-panel-2, var(--bg-panel));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
color: var(--text-hi);
|
||||
font-size: 12.5px;
|
||||
font-family: var(--sans, inherit);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field select:focus,
|
||||
.field input:focus {
|
||||
border-color: var(--accent-dim, var(--accent));
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ========== yaml preview ========== */
|
||||
.yaml-preview {
|
||||
background: #0B0D12;
|
||||
border: 1px solid var(--line-soft, var(--line));
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
color: #8EC8FF;
|
||||
line-height: 1.7;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.yaml-preview .k {
|
||||
color: #7FB8FF;
|
||||
}
|
||||
|
||||
.yaml-preview .s {
|
||||
color: #FFC97A;
|
||||
}
|
||||
|
||||
.yaml-preview .c {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ========== tooltip note ========== */
|
||||
.tooltip-note {
|
||||
background: #0F261D;
|
||||
border: 1px solid var(--accent-dim, var(--accent));
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
color: var(--text-mid);
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tooltip-note b {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ========== buttons ========== */
|
||||
.btn {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-panel-2, var(--bg-panel));
|
||||
color: var(--text-hi);
|
||||
padding: 8px 14px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: #3A4356;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #06150F;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #55E8B3;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-mid);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: var(--text-hi);
|
||||
background: var(--bg-panel-2, var(--bg-panel));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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: '七牛云内部系统。',
|
||||
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>
|
||||
Reference in New Issue
Block a user