refactor(ui): embed delivery records into the service ledger

Replace the separate delivery records tab with a per-service dropdown column in the ledger table that opens the existing detail dialog with one-time credential claim. Sync now refreshes ledger, container services and delivery records together; errors surface via message toast. components.d.ts regenerated (dropdown components in, tabs out).
This commit is contained in:
Hungerdream
2026-07-29 13:50:29 +08:00
parent 244f6adfb8
commit 5715379a26
2 changed files with 62 additions and 123 deletions
+59 -121
View File
@@ -5,15 +5,8 @@
<h1>服务管理</h1>
<p>同步基础服务实例台账,统一查看服务名 / 服务 IP / 业务标签 / 实例数</p>
</div>
<el-button type="primary" :loading="serviceLoading || deliveryLoading" @click="refreshCurrentTab">同步</el-button>
<el-button type="primary" :loading="serviceLoading || deliveryLoading" @click="refreshServices">同步</el-button>
</div>
<el-tabs v-model="activeTab" class="management-tabs">
<el-tab-pane label="服务台账" name="services" />
<el-tab-pane label="交付记录" name="delivery" />
</el-tabs>
<template v-if="activeTab === 'services'">
<div class="stat-row">
<div class="stat-card">
<div class="label">接入 Datacenter</div>
@@ -84,12 +77,13 @@
<th>实例数</th>
<th>健康实例</th>
<th>示例 IP</th>
<th>交付记录</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-if="!serviceLoading && filteredServices.length === 0">
<td colspan="7" class="text-dim">当前业务线暂无基础服务实例</td>
<td colspan="8" class="text-dim">当前业务线暂无基础服务实例</td>
</tr>
<tr v-for="service in filteredServices" :key="service.name" class="tr-hover">
<td class="strong mono">{{ service.name }}</td>
@@ -98,6 +92,31 @@
<td class="mono">{{ service.instances }}</td>
<td class="mono">{{ service.healthy }}</td>
<td class="mono">{{ service.ip }}</td>
<td>
<el-dropdown
v-if="deliveryTasksForService(service.name).length"
trigger="click"
popper-class="delivery-record-dropdown"
@command="openDeliveryRecordById"
>
<el-button link type="primary" :icon="Tickets">查看 {{ deliveryTasksForService(service.name).length }} 条</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="task in deliveryTasksForService(service.name)"
:key="task.id"
:command="task.id"
>
<span class="delivery-dropdown-item">
<strong>{{ formatDeliveryTime(task.created_at, true) }}</strong>
<small>{{ deliveryStatusText(task.status) }} · {{ credentialStatusText(task) }}</small>
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-else class="text-dim">-</span>
</td>
<td :class="['status-text', service.statusClass]">● {{ service.status }}</td>
</tr>
</tbody>
@@ -157,61 +176,6 @@
</div>
</div>
</div>
</template>
<template v-else>
<div class="delivery-toolbar">
<el-input v-model="deliverySearch" placeholder="搜索实例名称 / 任务编号 / 主机" clearable />
<el-select v-model="deliveryStatusFilter" placeholder="全部交付状态">
<el-option label="全部交付状态" value="" />
<el-option v-for="(label, value) in deliveryStatusLabels" :key="value" :label="label" :value="value" />
</el-select>
</div>
<div class="panel delivery-record-panel">
<div class="panel-head">
<div>
<h3>基础服务交付记录</h3>
<span class="meta">共 {{ filteredDeliveryRecords.length }} 条 · 当前业务线:{{ currentName }}</span>
</div>
<span class="meta">{{ deliveryLoading ? '同步中...' : deliveryMetaText }}</span>
</div>
<div class="panel-body">
<table>
<thead>
<tr>
<th>实例名称</th>
<th>任务编号</th>
<th>连接地址</th>
<th>命名空间</th>
<th>交付状态</th>
<th>凭证</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="!deliveryLoading && filteredDeliveryRecords.length === 0">
<td colspan="8" class="delivery-empty">暂无交付记录</td>
</tr>
<tr v-for="task in filteredDeliveryRecords" :key="task.id" class="tr-hover">
<td><strong class="mono">{{ task.instance_name }}</strong></td>
<td class="mono">{{ task.id }}</td>
<td class="mono">{{ deliveryEndpoint(task) }}</td>
<td class="mono">{{ task.namespace }}</td>
<td><span class="tag" :class="deliveryStatusClass(task.status)">{{ deliveryStatusText(task.status) }}</span></td>
<td><span class="tag" :class="credentialStatusClass(task)">{{ credentialStatusText(task) }}</span></td>
<td class="mono">{{ formatDeliveryTime(task.created_at) }}</td>
<td class="delivery-table-actions">
<el-button link type="primary" @click="openDeliveryDetail(task)">查看详情</el-button>
<el-button v-if="credentialEligible(task)" link type="warning" @click="openDeliveryDetail(task)">领取凭证</el-button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<el-dialog v-model="deliveryDetailVisible" width="640px" align-center destroy-on-close>
<template #header>
@@ -270,7 +234,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { CircleCheck, CopyDocument, Download } from '@element-plus/icons-vue'
import { CircleCheck, CopyDocument, Download, Tickets } from '@element-plus/icons-vue'
import { containerServiceApi, emptyContainerServiceSummary, type ContainerServiceSummary, type ContainerWorkload } from '@/api/containerService'
import { deliveryApi, type DeliveryTask, type DeploymentCredential, type MySQLServiceLedgerItem } from '@/api/delivery'
import { useBusinessLineStore } from '@/stores/businessLine'
@@ -296,12 +260,8 @@ const services = ref<ServiceLedgerRow[]>([])
const serviceLoading = ref(false)
const serviceError = ref('')
const activeTab = ref('services')
const deliveryRecords = ref<DeliveryTask[]>([])
const deliveryLoading = ref(false)
const deliveryError = ref('')
const deliverySearch = ref('')
const deliveryStatusFilter = ref('')
const deliveryDetailVisible = ref(false)
const selectedDeliveryTask = ref<DeliveryTask>()
const revealedCredentials = ref<DeploymentCredential[]>([])
@@ -350,18 +310,6 @@ const deliveryStatusLabels: Record<string, string> = {
rollback_acknowledged: '已确认清理',
}
const deliveryMetaText = computed(() => deliveryError.value || '数据源:delivery_tasks')
const filteredDeliveryRecords = computed(() => {
const keyword = deliverySearch.value.trim().toLowerCase()
return deliveryRecords.value.filter((task) => {
const matchesKeyword = !keyword || [task.instance_name, task.id, task.target_host, task.target_host_ip]
.some((value) => value?.toLowerCase().includes(keyword))
const matchesStatus = !deliveryStatusFilter.value || task.status === deliveryStatusFilter.value
return matchesKeyword && matchesStatus
})
})
const credentialClipboardText = computed(() =>
revealedCredentials.value.map((item) => `${item.username}@${item.host} ${item.password}`).join('\n'),
)
@@ -373,25 +321,21 @@ const loadDeliveryRecords = async () => {
return
}
deliveryLoading.value = true
deliveryError.value = ''
try {
const items = await deliveryApi.listTasks({ businessLineId, component: 'mysql' })
deliveryRecords.value = items.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))
} catch (error) {
deliveryRecords.value = []
deliveryError.value = error instanceof Error ? error.message : '交付记录同步失败'
ElMessage.error(error instanceof Error ? error.message : '交付记录同步失败')
} finally {
deliveryLoading.value = false
}
}
const refreshCurrentTab = () => {
if (activeTab.value === 'delivery') {
loadDeliveryRecords()
} else {
loadServiceLedger()
loadContainerServices()
}
const refreshServices = () => {
loadServiceLedger()
loadContainerServices()
loadDeliveryRecords()
}
function openDeliveryDetail(task: DeliveryTask) {
@@ -400,6 +344,15 @@ function openDeliveryDetail(task: DeliveryTask) {
deliveryDetailVisible.value = true
}
function openDeliveryRecordById(id: string) {
const task = deliveryRecords.value.find((item) => item.id === id)
if (task) openDeliveryDetail(task)
}
function deliveryTasksForService(serviceName: string) {
return deliveryRecords.value.filter((task) => task.instance_name === serviceName)
}
function credentialEligible(task: DeliveryTask) {
return ['finished', 'register_failed'].includes(task.status) && !consumedCredentialTasks.value.has(task.id)
}
@@ -564,43 +517,28 @@ watch(deliveryDetailVisible, (visible) => {
</script>
<style scoped>
/* 台账部分样式在 global.css 中定义;以下为交付记录专属样式 */
.management-tabs {
margin-bottom: 14px;
}
.management-tabs :deep(.el-tabs__header) {
margin: 0;
}
.delivery-toolbar {
/* 台账部分样式在 global.css 中定义;以下为交付详情专属样式 */
:global(.delivery-dropdown-item) {
display: grid;
grid-template-columns: minmax(280px, 1fr) 220px;
gap: 12px;
margin-bottom: 14px;
gap: 3px;
min-width: 220px;
padding: 3px 0;
}
.delivery-record-panel .panel-head > div {
display: grid;
gap: 4px;
:global(.delivery-dropdown-item strong) {
color: var(--text-hi);
font-size: 12px;
}
.delivery-record-panel .panel-body {
overflow-x: auto;
}
.delivery-record-panel table {
min-width: 1120px;
}
.delivery-table-actions {
white-space: nowrap;
}
.delivery-empty {
padding: 48px 16px;
:global(.delivery-dropdown-item small) {
color: var(--text-dim);
text-align: center;
font-size: 10.5px;
}
:global(.delivery-record-dropdown .el-dropdown-menu) {
max-height: min(420px, calc(100vh - 32px));
overflow-y: auto;
overscroll-behavior: contain;
}
.delivery-dialog-head,