From bb4c5b39d2f77afb96df87301f85a5b7c310f6ab Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 14 Jan 2026 19:52:37 +0800 Subject: [PATCH] feat: add tenant health panel --- .../superadmin/src/service/TenantService.js | 34 +++ .../src/views/superadmin/Tenants.vue | 211 +++++++++++++++++- 2 files changed, 235 insertions(+), 10 deletions(-) diff --git a/frontend/superadmin/src/service/TenantService.js b/frontend/superadmin/src/service/TenantService.js index 6ad194c..8043d2d 100644 --- a/frontend/superadmin/src/service/TenantService.js +++ b/frontend/superadmin/src/service/TenantService.js @@ -41,6 +41,40 @@ export const TenantService = { items: normalizeItems(data?.items) }; }, + async listTenantHealth({ page, limit, id, user_id, name, code, status, expired_at_from, expired_at_to, created_at_from, created_at_to, sortField, sortOrder } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + id, + user_id, + name, + code, + status, + expired_at_from: iso(expired_at_from), + expired_at_to: iso(expired_at_to), + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + if (sortField && sortOrder) { + if (sortOrder === 1) query.asc = sortField; + if (sortOrder === -1) query.desc = sortField; + } + + const data = await requestJson('/super/v1/tenants/health', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + }, async createTenant({ code, name, admin_user_id, duration } = {}) { return requestJson('/super/v1/tenants', { method: 'POST', diff --git a/frontend/superadmin/src/views/superadmin/Tenants.vue b/frontend/superadmin/src/views/superadmin/Tenants.vue index 513da86..851ca1b 100644 --- a/frontend/superadmin/src/views/superadmin/Tenants.vue +++ b/frontend/superadmin/src/views/superadmin/Tenants.vue @@ -27,6 +27,15 @@ const createdAtTo = ref(null); const sortField = ref('id'); const sortOrder = ref(-1); +const healthDialogVisible = ref(false); +const healthLoading = ref(false); +const healthItems = ref([]); +const healthTotal = ref(0); +const healthPage = ref(1); +const healthRows = ref(10); +const healthSortField = ref('tenant_id'); +const healthSortOrder = ref(-1); + const tenantUsersDialogVisible = ref(false); const tenantUsersLoading = ref(false); const tenantUsersTenant = ref(null); @@ -63,6 +72,12 @@ function formatCny(amountInCents) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount); } +function formatPercent(value) { + const rate = Number(value); + if (!Number.isFinite(rate)) return '-'; + return `${(rate * 100).toFixed(1)}%`; +} + function getStatusSeverity(status) { switch (status) { case 'verified': @@ -95,21 +110,53 @@ function getExpiryDaysInfo(expiredAt) { return { daysLeft: deltaDays, tooltipText, textClass }; } +function getHealthSeverity(level) { + switch (level) { + case 'healthy': + return 'success'; + case 'warning': + return 'warn'; + case 'risk': + return 'danger'; + default: + return 'secondary'; + } +} + +function formatHealthLevel(level) { + switch (level) { + case 'healthy': + return '健康'; + case 'warning': + return '预警'; + case 'risk': + return '风险'; + default: + return level || '-'; + } +} + +function buildTenantFilters() { + return { + id: tenantID.value || undefined, + user_id: ownerUserID.value || undefined, + name: nameKeyword.value, + code: codeKeyword.value, + status: status.value, + expired_at_from: expiredAtFrom.value || undefined, + expired_at_to: expiredAtTo.value || undefined, + created_at_from: createdAtFrom.value || undefined, + created_at_to: createdAtTo.value || undefined + }; +} + async function loadTenants() { loading.value = true; try { const result = await TenantService.listTenants({ page: page.value, limit: rows.value, - id: tenantID.value || undefined, - user_id: ownerUserID.value || undefined, - name: nameKeyword.value, - code: codeKeyword.value, - status: status.value, - expired_at_from: expiredAtFrom.value || undefined, - expired_at_to: expiredAtTo.value || undefined, - created_at_from: createdAtFrom.value || undefined, - created_at_to: createdAtTo.value || undefined, + ...buildTenantFilters(), sortField: sortField.value, sortOrder: sortOrder.value }); @@ -127,6 +174,56 @@ async function loadTenants() { } } +function normalizeHealthSortField(field) { + if (field === 'tenant_id') return 'id'; + return field; +} + +async function loadTenantHealth() { + healthLoading.value = true; + try { + const result = await TenantService.listTenantHealth({ + page: healthPage.value, + limit: healthRows.value, + ...buildTenantFilters(), + sortField: normalizeHealthSortField(healthSortField.value), + sortOrder: healthSortOrder.value + }); + healthItems.value = result.items; + healthTotal.value = result.total; + } catch (error) { + toast.add({ + severity: 'error', + summary: '加载失败', + detail: error?.message || '无法加载租户健康概览', + life: 4000 + }); + } finally { + healthLoading.value = false; + } +} + +function openHealthDialog() { + healthDialogVisible.value = true; + healthPage.value = 1; + healthRows.value = 10; + healthSortField.value = 'tenant_id'; + healthSortOrder.value = -1; + loadTenantHealth(); +} + +function onHealthPage(event) { + healthPage.value = (event.page ?? 0) + 1; + healthRows.value = event.rows ?? healthRows.value; + loadTenantHealth(); +} + +function onHealthSort(event) { + healthSortField.value = event.sortField ?? healthSortField.value; + healthSortOrder.value = event.sortOrder ?? healthSortOrder.value; + loadTenantHealth(); +} + function onSearch() { page.value = 1; loadTenants(); @@ -380,7 +477,10 @@ onMounted(() => {

租户列表

-
@@ -506,6 +606,97 @@ onMounted(() => { + + + 健康指标基于当前筛选条件聚合,退款率=退款订单/已支付订单。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +