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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user