feat(service-catalog): 对齐基础服务目录交互逻辑

- 更新 6 张服务卡片数据(Wayne/MySQL/openresty/PgSQL/Redis/接入新服务)
- 容器服务(Wayne)点击打开发布弹窗(业务线/命名空间/集群/规格/服务名)
- MySQL 点击打开标准化交付弹窗(架构/规格/版本/实例名 + YAML 预览)
- openresty 可点击但提示"功能对接中"
- PgSQL/Redis 置灰不可点击,提示"功能规划中"
- 接入新服务置灰(0.5)无响应
- 弹窗确认后 toast 提示并跳转任务中心
This commit is contained in:
2026-07-17 10:37:52 +08:00
parent 0dd098213c
commit f327f41733
+442 -18
View File
@@ -3,59 +3,304 @@
<div class="page-head"> <div class="page-head">
<div> <div>
<h1>基础服务目录</h1> <h1>基础服务目录</h1>
<p>点击卡片,通过封装好的 ansible-playbook 一键部署标准化基础服务</p> <p>点击卡片,通过封装好的 ansible-playbook 一键标准化交付,完成后自动注册进对应子系统</p>
</div> </div>
</div> </div>
<div class="svc-grid"> <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-top">
<div class="svc-ic" :style="{ background: service.bgColor, color: service.iconColor }"> <div class="svc-ic" :style="{ background: service.bgColor, color: service.iconColor }">
{{ service.icon }} {{ service.icon }}
</div> </div>
<h4>{{ service.name }}</h4> <h4 :style="service.disabled && service.icon === '+' ? { color: 'var(--text-dim)' } : {}">{{ service.name }}</h4>
</div> </div>
<div class="svc-desc">{{ service.description }}</div> <div class="svc-desc">{{ service.description }}</div>
<div class="svc-foot"> <div class="svc-foot">
<span class="playbook">{{ service.playbook }}</span> <span class="playbook">{{ service.playbook }}</span>
<span>{{ service.version }}</span> <span :style="service.statusColor ? { color: service.statusColor } : {}">{{ service.version }}</span>
</div> </div>
</div> </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">&times;</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">&times;</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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const services = ref([ const router = useRouter()
{ 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' }, interface Service {
{ name: 'openresty', icon: 'Or', bgColor: 'var(--logo-dm-bg)', iconColor: 'var(--tag-green-text)', description: '容器化部署在 K8s 内,作为业务边缘网关,支持灰度路由配置。', playbook: 'roles/openresty-deploy', version: 'v1.25' }, name: string
{ name: 'dpvs', icon: 'Dp', bgColor: 'var(--logo-qp-bg)', iconColor: 'var(--tag-amber-text)', description: '四层负载均衡,直接管理宿主机网络命名空间,不纳入容器编排。', playbook: 'roles/dpvs-deploy', version: 'v1.9' }, icon: string
{ name: 'PgSQL', icon: 'Pg', bgColor: 'var(--tag-purple-bg)', iconColor: 'var(--tag-purple-text)', description: '流复制主从架构,规划中 · 第二期接入 open-cdm 统一审核体系。', playbook: 'roles/pgsql-deploy', version: '规划中' }, 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> </script>
<style scoped> <style scoped>
/* .page-head, .svc-grid 已在 global.css 中定义 */
.svc-card { .svc-card {
background: var(--bg-panel); background: var(--bg-panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-lg); border-radius: 8px;
padding: 16px; padding: 16px;
cursor: pointer; cursor: pointer;
transition: border-color 0.15s; transition: border-color 0.15s;
} }
.svc-card:hover { .svc-card:hover:not(.disabled) {
border-color: var(--hover-border); border-color: #3A4356;
}
.svc-card.disabled {
cursor: not-allowed;
} }
.svc-top { .svc-top {
@@ -102,4 +347,183 @@ h4 {
font-family: var(--mono); font-family: var(--mono);
color: var(--text-mid); 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> </style>