feat(frontend): 实现仪表盘和子系统导航
页面: - views/dashboard/Index.vue,首页仪表盘(资源大盘、集群拓扑、最近任务) - views/subsystem/Navigation.vue,子系统导航页(6 个卡片网格展示) 组件: - components/SubsystemCard.vue,子系统卡片组件(图标、名称、状态、SSO 跳转) API: - api/subsystem.ts,子系统相关接口(获取列表、SSO URL) 子系统:Wayne、CloudDM、CacheCloud、Apollo、qpass、Grafana
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import request from './request'
|
||||
|
||||
export interface Subsystem {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
url: string
|
||||
status: 'integrated' | 'integrating'
|
||||
sso_enabled: boolean
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockSubsystems: Subsystem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Wayne',
|
||||
description: '多集群容器管理平台',
|
||||
icon: 'W',
|
||||
url: 'https://wayne.qiniu.com',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'CloudDM',
|
||||
description: '数据库管理与SQL审核',
|
||||
icon: 'DM',
|
||||
url: 'https://clouddm.qiniu.com',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'CacheCloud',
|
||||
description: 'Redis 云管理平台',
|
||||
icon: 'CC',
|
||||
url: 'https://cachecloud.qiniu.com',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Apollo',
|
||||
description: '配置中心',
|
||||
icon: 'AP',
|
||||
url: 'https://apollo.xinfra.internal',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'qpass',
|
||||
description: '密码管理平台',
|
||||
icon: 'QP',
|
||||
url: 'https://qpass.xinfra.internal',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Grafana',
|
||||
description: '监控可视化平台',
|
||||
icon: 'GF',
|
||||
url: 'https://grafana.xinfra.internal',
|
||||
status: 'integrated',
|
||||
sso_enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const subsystemApi = {
|
||||
// 获取子系统列表
|
||||
getSubsystems(): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: mockSubsystems,
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
|
||||
// 获取 SSO 跳转 URL
|
||||
getSSOUrl(id: number): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const subsystem = mockSubsystems.find((s) => s.id === id)
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
sso_url: subsystem?.url + '/sso/callback?code=mock-code&state=mock-state',
|
||||
expires_in: 300,
|
||||
},
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="ext-card" @click="handleClick">
|
||||
<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.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ description }}</p>
|
||||
<div class="sso-row">
|
||||
<span class="sso-dot" :class="{ warn: system.status === 'integrating' }"></span>
|
||||
{{ system.status === 'integrated' ? 'LDAP 原生配置接入 · 零代码' : 'LDAP 接入改造中' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { subsystemApi, type Subsystem } from '@/api/subsystem'
|
||||
|
||||
const props = defineProps<{
|
||||
system: Subsystem
|
||||
}>()
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
W: '#1C2A3A',
|
||||
DM: '#1C3A2E',
|
||||
CC: '#3A1C28',
|
||||
AP: '#1C2A3A',
|
||||
QP: '#241B0A',
|
||||
GF: '#1B202B',
|
||||
}
|
||||
return colors[props.system.icon] || '#262C38'
|
||||
})
|
||||
|
||||
const iconColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
W: '#7FB8FF',
|
||||
DM: '#7FFFC2',
|
||||
CC: '#FF8E9C',
|
||||
AP: '#8EC8FF',
|
||||
QP: '#FFC97A',
|
||||
GF: '#F2B544',
|
||||
}
|
||||
return colors[props.system.icon] || '#9AA3B5'
|
||||
})
|
||||
|
||||
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)
|
||||
window.open(data.sso_url, '_blank')
|
||||
} else {
|
||||
window.open(props.system.url, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ext-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.ext-card:hover {
|
||||
border-color: #3A4356;
|
||||
}
|
||||
|
||||
.ext-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ext-logo {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--mono);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ext-sub {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: var(--text-mid);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sso-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.sso-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.sso-dot.warn {
|
||||
background: var(--warn);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>资源大盘</h1>
|
||||
<p>跨机房 / 多云容器资源统一视图 · 最近更新于 12 秒前</p>
|
||||
</div>
|
||||
<el-button @click="refresh">⟳ 刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="label">RKE2 集群</div>
|
||||
<div class="value">3</div>
|
||||
<div class="delta">3 机房在线</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">节点总数</div>
|
||||
<div class="value">128</div>
|
||||
<div class="delta up">↑ 6 本周新增</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">CPU 已分配</div>
|
||||
<div class="value">61%</div>
|
||||
<div class="delta">2,048 / 3,360 核</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">组件实例</div>
|
||||
<div class="value">214</div>
|
||||
<div class="delta">MySQL 38 · Redis 92 · 其他 84</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">进行中任务</div>
|
||||
<div class="value" style="color: var(--warn)">2</div>
|
||||
<div class="delta warn">1 个待关注</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cols-2">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>集群拓扑 · 按机房 / 业务线标签</h3>
|
||||
<span class="meta">node-label 调度视图</span>
|
||||
</div>
|
||||
<div class="topo">
|
||||
<div class="topo-cluster">
|
||||
<div class="tname">
|
||||
<span class="tag zone-a">IDC-华北机房</span>
|
||||
<span class="mono" style="color: var(--text-dim)">46节点</span>
|
||||
</div>
|
||||
<div class="node-grid">
|
||||
<div v-for="i in 24" :key="i" :class="['node-cell', i <= 18 ? 'a' : '']"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topo-cluster">
|
||||
<div class="tname">
|
||||
<span class="tag zone-b">IDC-华东机房</span>
|
||||
<span class="mono" style="color: var(--text-dim)">52节点</span>
|
||||
</div>
|
||||
<div class="node-grid">
|
||||
<div v-for="i in 24" :key="i" :class="['node-cell', i <= 20 ? 'b' : '']"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topo-cluster">
|
||||
<div class="tname">
|
||||
<span class="tag zone-c">阿里云-华南</span>
|
||||
<span class="mono" style="color: var(--text-dim)">30节点</span>
|
||||
</div>
|
||||
<div class="node-grid">
|
||||
<div v-for="i in 18" :key="i" :class="['node-cell', i <= 12 ? 'c' : '']"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><span class="node-cell a inline"></span>Kodo业务线</span>
|
||||
<span><span class="node-cell b inline"></span>LAS业务线</span>
|
||||
<span><span class="node-cell c inline"></span>灵矽业务线</span>
|
||||
<span><span class="node-cell empty inline"></span>空闲</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>最近任务</h3>
|
||||
<router-link to="/tasks" class="meta">查看全部 →</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<tr class="tr-hover">
|
||||
<td class="status-text ok">● 成功</td>
|
||||
<td class="strong">MySQL 主从部署</td>
|
||||
<td class="mono">2m14s</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td class="status-text warn">● 执行中</td>
|
||||
<td class="strong">RKE2 节点加入</td>
|
||||
<td class="mono">38s</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td class="status-text ok">● 成功</td>
|
||||
<td class="strong">Redis Cluster 部署</td>
|
||||
<td class="mono">3m02s</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td class="status-text err">● 失败</td>
|
||||
<td class="strong">openresty 网关部署</td>
|
||||
<td class="mono">41s</td>
|
||||
</tr>
|
||||
<tr class="tr-hover">
|
||||
<td class="status-text ok">● 成功</td>
|
||||
<td class="strong">业务线标签同步</td>
|
||||
<td class="mono">5s</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const refresh = () => {
|
||||
// 刷新数据
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
font-size: 19px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-family: var(--mono);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-card .delta {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-card .delta.up {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-card .delta.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.cols-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.panel-head h3 {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-head .meta {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.topo {
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.topo-cluster {
|
||||
flex: 1;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.topo-cluster .tname {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.node-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.node-cell {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 3px;
|
||||
background: #1E2532;
|
||||
border: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.node-cell.a {
|
||||
background: #13261F;
|
||||
border-color: var(--accent-dim);
|
||||
}
|
||||
|
||||
.node-cell.b {
|
||||
background: #1A1430;
|
||||
border-color: #3A2C5C;
|
||||
}
|
||||
|
||||
.node-cell.c {
|
||||
background: #241B0A;
|
||||
border-color: #5C4A1C;
|
||||
}
|
||||
|
||||
.node-cell.empty {
|
||||
background: transparent;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.node-cell.inline {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0 16px 14px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
padding: 9px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
color: var(--text-mid);
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr.tr-hover:hover td {
|
||||
background: var(--bg-panel-2);
|
||||
}
|
||||
|
||||
td.strong {
|
||||
color: var(--text-hi);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10.5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text-mid);
|
||||
}
|
||||
|
||||
.tag.zone-a {
|
||||
color: #8EC8FF;
|
||||
border-color: #1C3A5C;
|
||||
background: #0E1C2C;
|
||||
}
|
||||
|
||||
.tag.zone-b {
|
||||
color: #C9A6FF;
|
||||
border-color: #3A2C5C;
|
||||
background: #1A1430;
|
||||
}
|
||||
|
||||
.tag.zone-c {
|
||||
color: #FFC97A;
|
||||
border-color: #5C4A1C;
|
||||
background: #241B0A;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-text.ok {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status-text.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.status-text.err {
|
||||
color: var(--err);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>子系统导航</h1>
|
||||
<p>统一 LDAP 账号,点击即用免二次登录(同账号同密码)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ext-grid">
|
||||
<SubsystemCard
|
||||
v-for="system in subsystems"
|
||||
:key="system.id"
|
||||
:system="system"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { subsystemApi, type Subsystem } from '@/api/subsystem'
|
||||
import SubsystemCard from '@/components/SubsystemCard.vue'
|
||||
|
||||
const subsystems = ref<Subsystem[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await subsystemApi.getSubsystems()
|
||||
subsystems.value = data
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
font-size: 19px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.ext-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user