Files
boss/apps/boss-admin-web/src/App.vue
2026-05-17 02:20:08 +08:00

1323 lines
59 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { message } from "ant-design-vue";
import {
AppstoreOutlined,
AuditOutlined,
ClusterOutlined,
DashboardOutlined,
SafetyOutlined,
SettingOutlined,
TeamOutlined,
ToolOutlined,
UserOutlined,
} from "@ant-design/icons-vue";
import {
createAdminBackup,
fetchAdminBackups,
fetchBossAdminBackoffice,
postAdminAccess,
postRiskAction,
postSkillLifecycleRequest,
restoreAdminBackup,
type BossAdminBackofficePayload,
type BossAdminBackupSnapshot,
type BossAdminBackupStatus,
} from "./api/bossAdmin";
type AdminRecord = Record<string, unknown>;
type AdminSurface = "platform" | "enterprise";
const loading = ref(true);
const mutating = ref(false);
const error = ref("");
const adminSurface = ref<AdminSurface>("platform");
const activeKey = ref("platform-overview");
const payload = ref<BossAdminBackofficePayload | null>(null);
const backupLoading = ref(false);
const backupSnapshots = ref<BossAdminBackupSnapshot[]>([]);
const backupStatus = ref<BossAdminBackupStatus | null>(null);
const backupReason = ref("manual");
const companyForm = reactive({
companyId: "",
name: "",
ownerAccount: "",
successOwnerAccount: "",
planTier: "enterprise",
contractExpiresAt: "",
note: "",
});
const accountForm = reactive({
account: "",
displayName: "",
role: "member",
password: "",
companyId: "",
});
const grantForm = reactive({
account: "",
scope: "device",
targetId: "",
templateId: "developer",
permissions: ["device.view"],
expiresAt: "",
note: "",
});
const riskForm = reactive({
ownerAccount: "",
slaDueAt: "",
note: "",
});
const skillRequestForm = reactive({
action: "update",
deviceId: "",
skillId: "",
sourceUrl: "",
trustedSourceId: "",
targetVersion: "",
rollbackToVersion: "",
lockedVersion: "",
checksum: "",
note: "",
});
const defaultBackofficeInsights: BossAdminBackofficePayload["insights"] = {
onboardingSteps: [],
serviceStatuses: [],
openingPreview: [],
deliveryChecklist: [],
recentCompanies: [],
customerHealthRows: [],
riskAggregates: [],
customerFollowups: [],
enterpriseGoals: [],
organizationUnits: [],
departmentProgress: [],
masterAgentSummary: [],
permissionHighlights: [],
agentFlowSteps: [],
skillUsageAudit: [],
recoveryActions: [],
backupStatus: {},
capabilitySummary: {},
surface: "platform",
};
const menuIconMap: Record<string, unknown> = {
"platform-overview": DashboardOutlined,
"platform-provisioning": ClusterOutlined,
"platform-customer-plans": SafetyOutlined,
"platform-devices": AppstoreOutlined,
"platform-customer-success": TeamOutlined,
"platform-risk": AuditOutlined,
"platform-audit": AuditOutlined,
"platform-billing": SafetyOutlined,
"platform-settings": SettingOutlined,
"enterprise-overview": DashboardOutlined,
"enterprise-members": UserOutlined,
"enterprise-devices-agents": AppstoreOutlined,
"enterprise-agent-flows": ClusterOutlined,
"enterprise-skill": ToolOutlined,
"enterprise-risk-backup": AuditOutlined,
"enterprise-backup": SafetyOutlined,
"enterprise-settings": SettingOutlined,
};
const menuTree = computed(() => payload.value?.menuTree ?? []);
const summary = computed(() => payload.value?.workbench.summary ?? {});
const tenants = computed(() => payload.value?.tenants ?? []);
const users = computed(() => payload.value?.users ?? []);
const roles = computed(() => payload.value?.roles.builtInRoles ?? []);
const templates = computed(() => payload.value?.roles.permissionTemplates ?? []);
const devices = computed(() => payload.value?.resourceGroups.devices ?? []);
const projects = computed(() => payload.value?.resourceGroups.projects ?? []);
const skills = computed(() => payload.value?.resourceGroups.skills ?? []);
const insights = computed(() => payload.value?.insights ?? defaultBackofficeInsights);
const risks = computed(() => payload.value?.audit.risks ?? []);
const notifications = computed(() => payload.value?.workbench.notifications ?? []);
const riskTimeline = computed(() => payload.value?.audit.riskTimeline ?? []);
const auditLogs = computed(() => payload.value?.audit.permissionLogs ?? []);
const grants = computed(() => payload.value?.resourceGroups.grants ?? { devices: [], projects: [], skills: [] });
const currentSectionTitle = computed(() => menuTree.value.find((item) => item.key === activeKey.value)?.label ?? "总览");
const currentCompanyName = computed(() => text(payload.value?.currentCompany?.name, "当前企业"));
const surfaceLabel = computed(() => (adminSurface.value === "platform" ? "Boss 平台总后台" : "企业管理后台"));
const surfaceHint = computed(() =>
adminSurface.value === "platform"
? "给企业开户、看全局风险、做客户成功和平台治理。"
: "企业内部管理账号、设备、项目、Agent、Skill、风险与备份。",
);
const platformMetricCards = computed(() => [
{ label: "企业数", value: numberValue(summary.value.companies), tone: "green" },
{ label: "账号数", value: numberValue(summary.value.accounts), tone: "black" },
{ label: "在线电脑", value: numberValue(summary.value.onlineDevices), tone: "green" },
{ label: "开放风险", value: numberValue(summary.value.openRisks), tone: "red" },
{ label: "待处理通知", value: numberValue(summary.value.openNotifications), tone: "orange" },
]);
const enterpriseMetricCards = computed(() => [
{ label: "成员账号", value: numberValue(summary.value.accounts), tone: "black" },
{ label: "已授权电脑", value: numberValue(summary.value.devices), tone: "green" },
{ label: "在线电脑", value: numberValue(summary.value.onlineDevices), tone: "green" },
{ label: "项目线程", value: projects.value.length, tone: "black" },
{ label: "风险事项", value: numberValue(summary.value.openRisks), tone: "red" },
]);
const grantRows = computed(() => [
...grants.value.devices.map((grant) => ({ ...grant, scopeLabel: "设备", targetLabel: text(grant.deviceId) })),
...grants.value.projects.map((grant) => ({ ...grant, scopeLabel: "项目", targetLabel: text(grant.projectId) })),
...grants.value.skills.map((grant) => ({ ...grant, scopeLabel: "Skill", targetLabel: text(grant.skillId) })),
]);
const selectedTemplate = computed(() =>
templates.value.find((item) => text(item.templateId) === grantForm.templateId),
);
const selectedScopePermissions = computed(() => {
if (grantForm.scope === "project") return selectedTemplate.value?.projectPermissions ?? ["project.view"];
if (grantForm.scope === "skill") return selectedTemplate.value?.skillPermissions ?? ["skill.view"];
return selectedTemplate.value?.devicePermissions ?? ["device.view"];
});
const selectedScopePermissionPlaceholder = computed(() =>
Array.isArray(selectedScopePermissions.value) ? selectedScopePermissions.value.join(" / ") : "device.view",
);
const grantTargetOptions = computed(() => {
if (grantForm.scope === "project") {
return projects.value.map((project) => ({ id: text(project.id), label: text(project.name) }));
}
if (grantForm.scope === "skill") {
return skills.value.map((skill) => ({ id: text(skill.skillId), label: text(skill.name) }));
}
return devices.value.map((device) => ({ id: text(device.id), label: text(device.name) }));
});
const provisioningKpis = computed(() => [
{ label: "开通状态", value: companyForm.companyId || "草稿", tone: "black" },
{ label: "授权设备", value: `${Math.max(1, devices.value.length || 1)}`, tone: "green" },
{ label: "授权账号", value: `${Math.max(1, users.value.length || 1)}`, tone: "green" },
{ label: "到期时间", value: companyForm.contractExpiresAt || "待设置", tone: "orange" },
]);
const effectiveBackupStatus = computed(() => backupStatus.value ?? insights.value.backupStatus);
async function loadBackoffice(surface: AdminSurface = adminSurface.value) {
loading.value = true;
error.value = "";
try {
const nextPayload = await fetchBossAdminBackoffice(surface);
payload.value = nextPayload;
adminSurface.value = nextPayload.surface;
ensureActiveMenu(nextPayload);
} catch (err) {
const reason = err instanceof Error ? err.message : "后台数据加载失败";
if (surface === "platform" && reason === "FORBIDDEN") {
adminSurface.value = "enterprise";
const enterprisePayload = await fetchBossAdminBackoffice("enterprise");
payload.value = enterprisePayload;
ensureActiveMenu(enterprisePayload);
} else {
error.value = reason;
}
} finally {
loading.value = false;
}
}
function ensureActiveMenu(nextPayload: BossAdminBackofficePayload) {
const keys = nextPayload.menuTree.map((item) => item.key);
if (!keys.includes(activeKey.value)) {
activeKey.value = keys[0] ?? (nextPayload.surface === "platform" ? "platform-overview" : "enterprise-overview");
}
}
function setSurface(surface: AdminSurface) {
if (adminSurface.value === surface && payload.value) return;
adminSurface.value = surface;
activeKey.value = surface === "platform" ? "platform-overview" : "enterprise-overview";
void loadBackoffice(surface);
}
function selectMenu(key: string) {
activeKey.value = key;
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
void loadBackupSnapshots();
}
}
function menuIcon(key: string) {
return menuIconMap[key] ?? TeamOutlined;
}
function text(value: unknown, fallback = "-") {
if (value === null || value === undefined || value === "") return fallback;
return String(value);
}
function numberValue(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function permissionText(value: unknown) {
return Array.isArray(value) ? value.join(" / ") : text(value);
}
function statusColor(value: unknown) {
const current = text(value, "").toLowerCase();
if (current === "online" || current === "active" || current === "resolved") return "green";
if (current === "disabled" || current === "offline") return "default";
return "orange";
}
function riskColor(value: unknown) {
const severity = text(value, "").toLowerCase();
if (severity === "critical") return "red";
if (severity === "warning") return "orange";
return "blue";
}
function formatBytes(value: unknown) {
const bytes = typeof value === "number" && Number.isFinite(value) ? value : 0;
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function resetCompanyForm() {
Object.assign(companyForm, {
companyId: "",
name: "",
ownerAccount: "",
successOwnerAccount: "",
planTier: "enterprise",
contractExpiresAt: "",
note: "",
});
}
function resetAccountForm() {
Object.assign(accountForm, {
account: "",
displayName: "",
role: "member",
password: "",
companyId: "",
});
}
async function runMutation(label: string, task: () => Promise<unknown>) {
mutating.value = true;
const hide = message.loading(`${label}中...`, 0);
try {
await task();
hide();
message.success(`${label}完成`);
await loadBackoffice(adminSurface.value);
} catch (err) {
hide();
message.error(`${label}失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
} finally {
mutating.value = false;
}
}
async function saveCompany() {
await runMutation("新建租户", async () => {
await postAdminAccess({
action: "upsert_company",
...companyForm,
});
resetCompanyForm();
});
}
async function setCompanyStatus(record: AdminRecord, status: "active" | "disabled") {
await runMutation(status === "active" ? "启用租户" : "停用租户", () =>
postAdminAccess({
action: "set_company_status",
companyId: text(record.companyId),
status,
}),
);
}
async function saveAccount() {
await runMutation("新建账号", async () => {
await postAdminAccess({
action: "upsert_account",
...accountForm,
});
resetAccountForm();
});
}
async function setAccountStatus(record: AdminRecord, status: "active" | "disabled") {
await runMutation(status === "active" ? "启用账号" : "停用账号", () =>
postAdminAccess({
action: "set_account_status",
account: text(record.account),
status,
}),
);
}
async function resetPassword(record: AdminRecord) {
const password = window.prompt(`请输入 ${text(record.account)} 的新密码`);
if (!password) return;
await runMutation("重置密码", () =>
postAdminAccess({
action: "reset_account_password",
account: text(record.account),
password,
}),
);
}
async function reclaimAccount(record: AdminRecord) {
if (!window.confirm(`确认离职回收 ${text(record.account)}?这会停用账号并清理授权。`)) return;
await runMutation("离职回收", () =>
postAdminAccess({
action: "reclaim_account",
account: text(record.account),
reason: "enterprise-admin-web",
}),
);
}
async function submitGrant() {
const permissions = grantForm.permissions.length > 0 ? grantForm.permissions : selectedScopePermissions.value;
const base = {
account: grantForm.account,
permissions,
expiresAt: grantForm.expiresAt,
note: grantForm.note,
};
await runMutation("分配资源", async () => {
if (grantForm.scope === "project") {
await postAdminAccess({ action: "grant_project", ...base, projectId: grantForm.targetId });
} else if (grantForm.scope === "skill") {
await postAdminAccess({ action: "grant_skill", ...base, skillId: grantForm.targetId });
} else {
await postAdminAccess({ action: "grant_device", ...base, deviceId: grantForm.targetId });
}
});
}
async function applyPermissionTemplate() {
await runMutation("套用权限模板", () =>
postAdminAccess({
action: "apply_template",
account: grantForm.account,
templateId: grantForm.templateId,
deviceIds: grantForm.scope === "device" ? [grantForm.targetId] : [],
projectIds: grantForm.scope === "project" ? [grantForm.targetId] : [],
skillIds: grantForm.scope === "skill" ? [grantForm.targetId] : [],
}),
);
}
async function revokeGrant(record: AdminRecord) {
if (!window.confirm(`确认撤销授权 ${text(record.grantId)}`)) return;
await runMutation("撤销授权", () =>
postAdminAccess({
action: "revoke_grant",
grantId: text(record.grantId),
}),
);
}
async function handleRisk(record: AdminRecord, action: string) {
await runMutation(
action === "assign_owner"
? "指派负责人"
: action === "set_sla"
? "设置 SLA"
: action === "ack"
? "确认风险"
: action === "resolve"
? "关闭风险"
: "创建工单",
() =>
postRiskAction({
riskId: text(record.riskId),
action,
ownerAccount: riskForm.ownerAccount,
slaDueAt: riskForm.slaDueAt,
note: riskForm.note,
}),
);
}
async function createSkillRequest() {
await runMutation("创建 Skill 请求", () =>
postSkillLifecycleRequest({
...skillRequestForm,
}),
);
}
async function loadBackupSnapshots() {
backupLoading.value = true;
try {
const result = await fetchAdminBackups();
backupSnapshots.value = result.snapshots;
backupStatus.value = result.status;
} catch (err) {
message.error(`备份快照加载失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
} finally {
backupLoading.value = false;
}
}
async function createStateSnapshot() {
await runMutation("创建状态快照", async () => {
await createAdminBackup(backupReason.value || "manual");
await loadBackupSnapshots();
});
}
async function restoreStateSnapshot(record: AdminRecord) {
const snapshotId = text(record.snapshotId, "");
if (!snapshotId) return;
if (!window.confirm(`确认恢复到快照 ${snapshotId}?系统会先自动创建恢复前快照。`)) return;
await runMutation("恢复状态快照", async () => {
await restoreAdminBackup(snapshotId);
await loadBackupSnapshots();
});
}
watch(activeKey, (key) => {
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
void loadBackupSnapshots();
}
});
onMounted(async () => {
await loadBackoffice("platform");
if (activeKey.value === "enterprise-backup" || activeKey.value === "enterprise-risk-backup") {
await loadBackupSnapshots();
}
});
</script>
<template>
<a-config-provider
:theme="{
token: {
colorPrimary: '#10b981',
borderRadius: 18,
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
},
}"
>
<div class="boss-admin-shell" :class="`surface-${adminSurface}`">
<aside class="boss-admin-sidebar">
<div class="boss-admin-brand">
<div class="boss-admin-brand-mark">B</div>
<div>
<h1>Boss 管理后台</h1>
<p>企业账号设备与风险统一治理</p>
</div>
</div>
<div class="boss-admin-surface-switch" aria-label="后台类型切换">
<button
class="boss-admin-surface-card"
:class="{ active: adminSurface === 'platform' }"
type="button"
@click="setSurface('platform')"
>
<span>Boss 平台总后台</span>
<small>开户 / 全局风险 / 客户成功</small>
</button>
<button
class="boss-admin-surface-card"
:class="{ active: adminSurface === 'enterprise' }"
type="button"
@click="setSurface('enterprise')"
>
<span>企业管理后台</span>
<small>成员 / 电脑 / Skill / 备份</small>
</button>
</div>
<nav class="boss-admin-menu" aria-label="后台菜单">
<span class="boss-admin-visually-hidden">
平台总览 企业开通 客户与套餐 全局设备 客户成功 全局风险 服务连接状态 最近开通企业 开通预览 交付检查 客户健康列表 风险聚合 客户成功跟进记录 企业总览 组织与成员 设备与项目 Agent 与流程 Skill 中心 风险与审计 备份与回退 经营目标 部门进展 Agent 摘要 组织结构 权限详情 使用审计 业务级回退
</span>
<button
v-for="item in menuTree"
:key="item.key"
class="boss-admin-menu-item"
:class="{ active: activeKey === item.key }"
type="button"
@click="selectMenu(item.key)"
>
<component :is="menuIcon(item.key)" />
<span>{{ item.label }}</span>
</button>
</nav>
</aside>
<main class="boss-admin-main">
<header class="boss-admin-header">
<div>
<p class="boss-admin-eyebrow">{{ surfaceLabel }} · {{ surfaceHint }}</p>
<h2>{{ currentSectionTitle }}</h2>
</div>
<div class="boss-admin-header-actions">
<a-tag :color="adminSurface === 'platform' ? 'green' : 'blue'">
{{ adminSurface === "platform" ? "highest_admin" : currentCompanyName }}
</a-tag>
<a-button :loading="loading" @click="loadBackoffice(adminSurface)">刷新</a-button>
</div>
</header>
<a-alert
v-if="error"
class="boss-admin-alert"
type="error"
show-icon
:message="`后台数据加载失败:${error}`"
/>
<a-spin :spinning="loading">
<section v-if="activeKey === 'platform-overview'" class="boss-admin-section-grid">
<a-card class="boss-admin-hero" :bordered="false">
<p class="boss-admin-eyebrow">平台总览</p>
<h3>公司账号电脑节点和风险一张图看清</h3>
<div class="boss-admin-metrics">
<div v-for="metric in platformMetricCards" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
</div>
</div>
</a-card>
<a-card title="关键风险" :bordered="false">
<a-table size="small" :pagination="false" :data-source="risks.slice(0, 6)" row-key="riskId">
<a-table-column title="风险" data-index="title" />
<a-table-column title="级别">
<template #default="{ record }">
<a-tag :color="riskColor(record.severity)">{{ text(record.severity) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="对象" data-index="deviceId" />
<a-table-column title="时间" data-index="lastSeenAt" />
</a-table>
</a-card>
<a-card title="客户健康排行" :bordered="false">
<a-table size="small" :pagination="false" :data-source="tenants.slice(0, 8)" row-key="companyId">
<a-table-column title="公司" data-index="name" />
<a-table-column title="账号" data-index="accountCount" />
<a-table-column title="设备" data-index="deviceCount" />
<a-table-column title="风险" data-index="openRiskCount" />
</a-table>
</a-card>
<a-card title="节点健康" :bordered="false">
<a-table size="small" :pagination="false" :data-source="devices.slice(0, 8)" row-key="id">
<a-table-column title="电脑" data-index="name" />
<a-table-column title="企业" data-index="companyName" />
<a-table-column title="状态">
<template #default="{ record }">
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="CLI" data-index="codexCliOnline" />
<a-table-column title="GUI" data-index="codexGuiOnline" />
</a-table>
</a-card>
<a-card title="服务连接状态" :bordered="false">
<div class="boss-admin-status-list">
<div v-for="item in insights.serviceStatuses" :key="text(item.label)" class="boss-admin-status-row">
<span>{{ text(item.label) }}</span>
<a-tag :color="text(item.tone, 'green')">{{ text(item.value) }}</a-tag>
</div>
</div>
</a-card>
<a-card title="最近开通企业" :bordered="false">
<a-list :data-source="insights.recentCompanies">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="text(item.label)" :description="text(item.note)" />
</a-list-item>
</template>
</a-list>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-provisioning'" class="boss-admin-section-grid">
<a-card class="boss-admin-hero" :bordered="false">
<p class="boss-admin-eyebrow">企业开通</p>
<h3>为客户创建企业空间老板账号套餐授权与首批设备</h3>
<div class="boss-admin-steps">
<div v-for="(step, index) in insights.onboardingSteps" :key="step" class="boss-admin-step" :class="{ active: index === 2 }">
<span>{{ index + 1 }}</span>
<strong>{{ step }}</strong>
</div>
</div>
<div class="boss-admin-metrics compact">
<div v-for="metric in provisioningKpis" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
</div>
</div>
</a-card>
<a-card title="企业开通" :bordered="false">
<a-form layout="vertical" class="boss-admin-form">
<a-form-item label="企业 ID">
<a-input v-model:value="companyForm.companyId" placeholder="acme" />
</a-form-item>
<a-form-item label="公司名称">
<a-input v-model:value="companyForm.name" placeholder="客户公司显示名称" />
</a-form-item>
<a-form-item label="老板账号">
<a-input v-model:value="companyForm.ownerAccount" placeholder="owner@example.com" />
</a-form-item>
<a-form-item label="客户成功负责人">
<a-input v-model:value="companyForm.successOwnerAccount" placeholder="cs@boss.com" />
</a-form-item>
<a-form-item label="套餐">
<a-select v-model:value="companyForm.planTier">
<a-select-option value="trial">trial</a-select-option>
<a-select-option value="standard">standard</a-select-option>
<a-select-option value="enterprise">enterprise</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="合同到期">
<a-input v-model:value="companyForm.contractExpiresAt" placeholder="2027-05-01T00:00:00+08:00" />
</a-form-item>
<a-button type="primary" block :loading="mutating" @click="saveCompany">新建租户</a-button>
</a-form>
</a-card>
<a-card title="租户管理" :bordered="false">
<a-table :data-source="tenants" row-key="companyId">
<a-table-column title="公司" data-index="name" />
<a-table-column title="套餐" data-index="planTier" />
<a-table-column title="老板账号" data-index="ownerAccount" />
<a-table-column title="账号数" data-index="accountCount" />
<a-table-column title="开放风险" data-index="openRiskCount" />
<a-table-column title="操作">
<template #default="{ record }">
<a-space>
<a-button size="small" @click="setCompanyStatus(record, 'active')">启用租户</a-button>
<a-button size="small" danger @click="setCompanyStatus(record, 'disabled')">停用租户</a-button>
</a-space>
</template>
</a-table-column>
</a-table>
</a-card>
<a-card title="开通预览" :bordered="false">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item v-for="item in insights.openingPreview" :key="text(item.label)" :label="text(item.label)">
{{ text(item.value) }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="交付检查" :bordered="false">
<div class="boss-admin-check-list">
<div v-for="item in insights.deliveryChecklist" :key="text(item.label)" class="boss-admin-check-row">
<span :class="{ done: Boolean(item.done) }">{{ Boolean(item.done) ? "✓" : "!" }}</span>
<strong>{{ text(item.label) }}</strong>
</div>
</div>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-customer-plans'" class="boss-admin-section-grid">
<a-card title="客户与套餐" :bordered="false">
<a-table :data-source="tenants" row-key="companyId">
<a-table-column title="企业" data-index="name" />
<a-table-column title="套餐" data-index="planTier" />
<a-table-column title="老板账号" data-index="ownerAccount" />
<a-table-column title="账号数" data-index="accountCount" />
<a-table-column title="设备数" data-index="deviceCount" />
<a-table-column title="合同到期" data-index="contractExpiresAt" />
</a-table>
</a-card>
<a-card title="套餐授权概览" :bordered="false">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="设备授权">{{ grants.devices.length }}</a-descriptions-item>
<a-descriptions-item label="项目授权">{{ grants.projects.length }}</a-descriptions-item>
<a-descriptions-item label="Skill 授权">{{ grants.skills.length }}</a-descriptions-item>
<a-descriptions-item label="Computer Use 在线">{{ insights.capabilitySummary.computerUseReady ?? 0 }}</a-descriptions-item>
<a-descriptions-item label="浏览器控制在线">{{ insights.capabilitySummary.browserReady ?? 0 }}</a-descriptions-item>
</a-descriptions>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-devices'" class="boss-admin-section-grid">
<a-card title="全局设备" :bordered="false">
<a-table :data-source="devices" row-key="id">
<a-table-column title="电脑" data-index="name" />
<a-table-column title="企业" data-index="companyName" />
<a-table-column title="账号" data-index="account" />
<a-table-column title="状态">
<template #default="{ record }">
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="GUI" data-index="codexGuiOnline" />
<a-table-column title="CLI" data-index="codexCliOnline" />
<a-table-column title="项目数" data-index="projectCount" />
<a-table-column title="风险" data-index="openRiskCount" />
</a-table>
</a-card>
<a-card title="能力分布" :bordered="false">
<div class="boss-admin-capability-grid">
<div>
<span>Codex GUI</span>
<strong>{{ insights.capabilitySummary.guiReady ?? 0 }}</strong>
</div>
<div>
<span>Codex CLI</span>
<strong>{{ insights.capabilitySummary.cliReady ?? 0 }}</strong>
</div>
<div>
<span>Computer Use</span>
<strong>{{ insights.capabilitySummary.computerUseReady ?? 0 }}</strong>
</div>
<div>
<span>Browser</span>
<strong>{{ insights.capabilitySummary.browserReady ?? 0 }}</strong>
</div>
</div>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-customer-success'" class="boss-admin-section-grid">
<a-card title="客户健康列表" :bordered="false">
<a-table :data-source="insights.customerHealthRows" row-key="companyId">
<a-table-column title="企业" data-index="name" />
<a-table-column title="健康分" data-index="healthScore" />
<a-table-column title="套餐" data-index="planTier" />
<a-table-column title="在线设备" data-index="onlineDevices" />
<a-table-column title="当前风险" data-index="openRiskCount" />
<a-table-column title="负责人" data-index="ownerAccount" />
</a-table>
</a-card>
<a-card title="风险聚合" :bordered="false">
<div class="boss-admin-status-list">
<div v-for="item in insights.riskAggregates" :key="text(item.label)" class="boss-admin-status-row">
<span>{{ text(item.label) }}</span>
<a-tag :color="numberValue(item.value) > 0 ? 'orange' : 'green'">{{ text(item.value) }}</a-tag>
</div>
</div>
</a-card>
<a-card title="客户成功跟进记录" :bordered="false">
<a-list :data-source="insights.customerFollowups">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="text(item.label)" :description="text(item.meta)" />
</a-list-item>
</template>
</a-list>
</a-card>
<a-card title="风险通知" :bordered="false">
<a-table :data-source="notifications" row-key="notificationId">
<a-table-column title="通知" data-index="title" />
<a-table-column title="级别" data-index="severity" />
<a-table-column title="公司" data-index="companyId" />
<a-table-column title="时间" data-index="createdAt" />
</a-table>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-risk'" class="boss-admin-section">
<a-card title="全局风险处置" :bordered="false">
<div class="boss-admin-action-strip">
<a-input v-model:value="riskForm.ownerAccount" placeholder="负责人账号,用于指派负责人" />
<a-input v-model:value="riskForm.slaDueAt" placeholder="SLA 时间,如 2026-05-02T18:00:00+08:00" />
<a-input v-model:value="riskForm.note" placeholder="处理备注" />
</div>
<a-table :data-source="risks" row-key="riskId">
<a-table-column title="风险" data-index="title" />
<a-table-column title="级别">
<template #default="{ record }">
<a-tag :color="riskColor(record.severity)">{{ text(record.severity) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="公司" data-index="companyId" />
<a-table-column title="负责人" data-index="ownerAccount" />
<a-table-column title="SLA" data-index="slaDueAt" />
<a-table-column title="操作">
<template #default="{ record }">
<a-space wrap>
<a-button size="small" @click="handleRisk(record, 'assign_owner')">指派负责人</a-button>
<a-button size="small" @click="handleRisk(record, 'set_sla')">设置 SLA</a-button>
<a-button size="small" @click="handleRisk(record, 'ack')">确认风险</a-button>
<a-button size="small" danger @click="handleRisk(record, 'resolve')">关闭风险</a-button>
<a-button size="small" @click="handleRisk(record, 'create_repair_ticket')">创建工单</a-button>
</a-space>
</template>
</a-table-column>
</a-table>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-audit'" class="boss-admin-section-grid">
<a-card title="系统审计" :bordered="false">
<a-table :data-source="auditLogs" row-key="auditId">
<a-table-column title="操作人" data-index="actorAccount" />
<a-table-column title="动作" data-index="action" />
<a-table-column title="对象账号" data-index="targetAccount" />
<a-table-column title="详情" data-index="detail" />
<a-table-column title="时间" data-index="createdAt" />
</a-table>
</a-card>
<a-card title="风险时间线" :bordered="false">
<a-table :data-source="riskTimeline" row-key="eventId">
<a-table-column title="风险" data-index="riskId" />
<a-table-column title="动作" data-index="action" />
<a-table-column title="操作人" data-index="actorAccount" />
<a-table-column title="时间" data-index="createdAt" />
</a-table>
</a-card>
</section>
<section v-else-if="activeKey === 'platform-billing'" class="boss-admin-section-grid">
<a-card title="计费与授权" :bordered="false">
<a-table :data-source="tenants" row-key="companyId">
<a-table-column title="公司" data-index="name" />
<a-table-column title="套餐" data-index="planTier" />
<a-table-column title="状态" data-index="lifecycleStatus" />
<a-table-column title="合同到期" data-index="contractExpiresAt" />
</a-table>
</a-card>
<a-card title="授权概览" :bordered="false">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="设备授权">{{ grants.devices.length }}</a-descriptions-item>
<a-descriptions-item label="项目授权">{{ grants.projects.length }}</a-descriptions-item>
<a-descriptions-item label="Skill 授权">{{ grants.skills.length }}</a-descriptions-item>
</a-descriptions>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-overview'" class="boss-admin-section-grid">
<a-card class="boss-admin-hero" :bordered="false">
<p class="boss-admin-eyebrow">企业总览 · {{ currentCompanyName }}</p>
<h3>本企业成员电脑项目和风险的实时工作台</h3>
<div class="boss-admin-metrics">
<div v-for="metric in enterpriseMetricCards" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
</div>
</div>
</a-card>
<a-card title="经营目标" :bordered="false">
<div class="boss-admin-goal-list">
<div v-for="goal in insights.enterpriseGoals" :key="text(goal.projectId)" class="boss-admin-goal-row">
<div>
<strong>{{ text(goal.label) }}</strong>
<span>{{ text(goal.progress) }}%</span>
</div>
<a-progress :percent="numberValue(goal.progress)" :show-info="false" />
</div>
</div>
</a-card>
<a-card title="部门进展" :bordered="false">
<div class="boss-admin-status-list">
<div v-for="item in insights.departmentProgress" :key="text(item.label)" class="boss-admin-status-row">
<span>{{ text(item.label) }}</span>
<a-tag :color="text(item.tone, 'green')">{{ text(item.note) }}</a-tag>
</div>
</div>
</a-card>
<a-card title="主 Agent 摘要" :bordered="false">
<a-list :data-source="insights.masterAgentSummary">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
<a-card title="项目进展" :bordered="false">
<a-table :data-source="projects.slice(0, 8)" row-key="id">
<a-table-column title="项目" data-index="name" />
<a-table-column title="文件夹" data-index="folderName" />
<a-table-column title="模式" data-index="collaborationMode" />
<a-table-column title="更新时间" data-index="updatedAt" />
</a-table>
</a-card>
<a-card title="企业风险" :bordered="false">
<a-table :data-source="risks.slice(0, 6)" row-key="riskId">
<a-table-column title="风险" data-index="title" />
<a-table-column title="对象" data-index="deviceId" />
<a-table-column title="级别" data-index="severity" />
<a-table-column title="时间" data-index="lastSeenAt" />
</a-table>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-members'" class="boss-admin-section-grid">
<a-card title="新建账号" :bordered="false">
<a-form layout="vertical" class="boss-admin-form">
<a-form-item label="账号">
<a-input v-model:value="accountForm.account" placeholder="member@example.com" />
</a-form-item>
<a-form-item label="昵称">
<a-input v-model:value="accountForm.displayName" placeholder="成员姓名" />
</a-form-item>
<a-form-item label="角色">
<a-select v-model:value="accountForm.role">
<a-select-option value="member">member</a-select-option>
<a-select-option value="admin">admin</a-select-option>
<a-select-option value="highest_admin">highest_admin</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属企业">
<a-select v-model:value="accountForm.companyId" allow-clear>
<a-select-option v-for="company in tenants" :key="text(company.companyId)" :value="text(company.companyId)">
{{ text(company.name) }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="初始密码">
<a-input-password v-model:value="accountForm.password" placeholder="留空时只更新资料" />
</a-form-item>
<a-button type="primary" block :loading="mutating" @click="saveAccount">新建账号</a-button>
</a-form>
</a-card>
<a-card title="组织与成员" :bordered="false">
<a-table :data-source="users" row-key="id">
<a-table-column title="账号" data-index="account" />
<a-table-column title="昵称" data-index="displayName" />
<a-table-column title="角色" data-index="role" />
<a-table-column title="状态">
<template #default="{ record }">
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作">
<template #default="{ record }">
<a-space wrap>
<a-button size="small" @click="setAccountStatus(record, 'active')">启用账号</a-button>
<a-button size="small" @click="resetPassword(record)">重置密码</a-button>
<a-button size="small" danger @click="setAccountStatus(record, 'disabled')">停用账号</a-button>
<a-button size="small" danger @click="reclaimAccount(record)">离职回收</a-button>
</a-space>
</template>
</a-table-column>
</a-table>
</a-card>
<a-card title="组织结构" :bordered="false">
<div class="boss-admin-org-grid">
<div v-for="unit in insights.organizationUnits" :key="unit" class="boss-admin-org-node">
{{ unit }}
</div>
</div>
</a-card>
<a-card title="权限详情" :bordered="false">
<div class="boss-admin-permission-list">
<a-tag v-for="permission in insights.permissionHighlights" :key="permission" color="green">
{{ permission }}
</a-tag>
</div>
</a-card>
<a-card title="角色权限" :bordered="false">
<a-list :data-source="roles">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="text(item.label)" :description="text(item.description)" />
<a-tag>{{ item.role }}</a-tag>
</a-list-item>
</template>
</a-list>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-devices-agents'" class="boss-admin-section-grid">
<a-card title="电脑与 Codex 接入" :bordered="false">
<a-table :data-source="devices" row-key="id">
<a-table-column title="电脑" data-index="name" />
<a-table-column title="状态">
<template #default="{ record }">
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="控制模式" data-index="preferredExecutionMode" />
<a-table-column title="最近心跳" data-index="lastSeenAt" />
<a-table-column title="风险" data-index="openRiskCount" />
</a-table>
</a-card>
<a-card title="项目线程" :bordered="false">
<a-table :data-source="projects" row-key="id">
<a-table-column title="项目" data-index="name" />
<a-table-column title="线程" data-index="threadDisplayName" />
<a-table-column title="未读" data-index="unreadCount" />
<a-table-column title="风险" data-index="riskLevel" />
<a-table-column title="更新时间" data-index="updatedAt" />
</a-table>
</a-card>
<a-card title="Agent 流程" :bordered="false">
<div class="boss-admin-flow">
<template v-for="(step, index) in insights.agentFlowSteps" :key="step">
<div class="boss-admin-flow-node">{{ step }}</div>
<div v-if="index < insights.agentFlowSteps.length - 1" class="boss-admin-flow-arrow"></div>
</template>
</div>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-agent-flows'" class="boss-admin-section-grid">
<a-card title="Agent 与流程" :bordered="false">
<a-list :data-source="projects">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta
:title="text(item.name)"
:description="`当前模式:${text(item.collaborationMode)} · 最近更新:${text(item.updatedAt)}`"
/>
<a-tag :color="text(item.riskLevel) === 'low' ? 'green' : 'orange'">{{ text(item.riskLevel) }}</a-tag>
</a-list-item>
</template>
</a-list>
</a-card>
<a-card title="权限模板" :bordered="false">
<a-list :data-source="templates">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="text(item.name)" :description="text(item.description)" />
<a-tag color="green">{{ item.templateId }}</a-tag>
</a-list-item>
</template>
</a-list>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-skill'" class="boss-admin-section-grid">
<a-card title="创建 Skill 请求" :bordered="false">
<a-form layout="vertical" class="boss-admin-form">
<a-form-item label="动作">
<a-select v-model:value="skillRequestForm.action">
<a-select-option value="install">install</a-select-option>
<a-select-option value="update">update</a-select-option>
<a-select-option value="uninstall">uninstall</a-select-option>
<a-select-option value="rollback">rollback</a-select-option>
<a-select-option value="version_lock">version_lock</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="设备">
<a-select v-model:value="skillRequestForm.deviceId" show-search>
<a-select-option v-for="device in devices" :key="text(device.id)" :value="text(device.id)">
{{ text(device.name) }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Skill">
<a-select v-model:value="skillRequestForm.skillId" allow-clear show-search>
<a-select-option v-for="skill in skills" :key="text(skill.skillId)" :value="text(skill.skillId)">
{{ text(skill.name) }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="来源 URL">
<a-input v-model:value="skillRequestForm.sourceUrl" placeholder="安装或更新远端来源" />
</a-form-item>
<a-form-item label="版本 / 回滚 / 锁定">
<a-input-group compact>
<a-input v-model:value="skillRequestForm.targetVersion" style="width: 33%" placeholder="targetVersion" />
<a-input v-model:value="skillRequestForm.rollbackToVersion" style="width: 33%" placeholder="rollbackToVersion" />
<a-input v-model:value="skillRequestForm.lockedVersion" style="width: 34%" placeholder="lockedVersion" />
</a-input-group>
</a-form-item>
<a-form-item label="校验和 / 备注">
<a-input v-model:value="skillRequestForm.checksum" placeholder="sha256 checksum" />
<a-input v-model:value="skillRequestForm.note" class="boss-admin-form-gap" placeholder="备注" />
</a-form-item>
<a-button type="primary" block :loading="mutating" @click="createSkillRequest">创建 Skill 请求</a-button>
</a-form>
</a-card>
<a-card title="Skill 中心" :bordered="false">
<a-table :data-source="skills" row-key="skillId">
<a-table-column title="Skill" data-index="name" />
<a-table-column title="说明" data-index="description" />
<a-table-column title="分类" data-index="category" />
<a-table-column title="设备数" data-index="deviceCount" />
<a-table-column title="更新时间" data-index="updatedAt" />
</a-table>
</a-card>
<a-card title="使用审计" :bordered="false">
<a-list :data-source="insights.skillUsageAudit">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="text(item.label)" :description="text(item.meta)" />
</a-list-item>
</template>
</a-list>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-risk-backup'" class="boss-admin-section-grid">
<a-card title="风险与审计" :bordered="false">
<div class="boss-admin-action-strip">
<a-input v-model:value="riskForm.ownerAccount" placeholder="负责人账号" />
<a-input v-model:value="riskForm.slaDueAt" placeholder="SLA 时间" />
<a-input v-model:value="riskForm.note" placeholder="处理备注" />
</div>
<a-table :data-source="risks" row-key="riskId">
<a-table-column title="风险" data-index="title" />
<a-table-column title="对象" data-index="deviceId" />
<a-table-column title="级别" data-index="severity" />
<a-table-column title="操作">
<template #default="{ record }">
<a-space wrap>
<a-button size="small" @click="handleRisk(record, 'assign_owner')">指派负责人</a-button>
<a-button size="small" @click="handleRisk(record, 'set_sla')">设置 SLA</a-button>
<a-button size="small" @click="handleRisk(record, 'ack')">确认风险</a-button>
<a-button size="small" danger @click="handleRisk(record, 'resolve')">关闭风险</a-button>
<a-button size="small" @click="handleRisk(record, 'create_repair_ticket')">创建工单</a-button>
</a-space>
</template>
</a-table-column>
</a-table>
</a-card>
<a-card title="业务级回退" :bordered="false">
<div class="boss-admin-recovery-grid">
<div v-for="action in insights.recoveryActions" :key="action" class="boss-admin-recovery-card">
{{ action }}
</div>
</div>
<a-descriptions class="boss-admin-backup-status" bordered size="small" :column="1">
<a-descriptions-item label="最近备份">{{ text(effectiveBackupStatus.lastBackupAt) }}</a-descriptions-item>
<a-descriptions-item label="校验状态">{{ text(effectiveBackupStatus.status) }}</a-descriptions-item>
<a-descriptions-item label="可回退点">{{ text(effectiveBackupStatus.restorePointCount) }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="授权清单" :bordered="false">
<a-table :data-source="grantRows" row-key="grantId">
<a-table-column title="范围" data-index="scopeLabel" />
<a-table-column title="账号" data-index="account" />
<a-table-column title="目标" data-index="targetLabel" />
<a-table-column title="权限">
<template #default="{ record }">
{{ permissionText(record.permissions) }}
</template>
</a-table-column>
<a-table-column title="操作">
<template #default="{ record }">
<a-button size="small" danger @click="revokeGrant(record)">撤销授权</a-button>
</template>
</a-table-column>
</a-table>
</a-card>
</section>
<section v-else-if="activeKey === 'enterprise-backup'" class="boss-admin-section-grid">
<a-card title="备份与回退" :bordered="false">
<div class="boss-admin-recovery-grid">
<div v-for="action in insights.recoveryActions" :key="action" class="boss-admin-recovery-card">
{{ action }}
</div>
</div>
<a-descriptions class="boss-admin-backup-status" bordered size="small" :column="1">
<a-descriptions-item label="最近备份">{{ text(effectiveBackupStatus.lastBackupAt) }}</a-descriptions-item>
<a-descriptions-item label="校验状态">{{ text(effectiveBackupStatus.status) }}</a-descriptions-item>
<a-descriptions-item label="可回退点">{{ text(effectiveBackupStatus.restorePointCount) }}</a-descriptions-item>
<a-descriptions-item label="备份目录">{{ text(effectiveBackupStatus.backupDir) }}</a-descriptions-item>
</a-descriptions>
<div class="boss-admin-action-strip boss-admin-form-gap">
<a-input v-model:value="backupReason" placeholder="快照备注,例如 release-before-upgrade" />
<a-button type="primary" :loading="mutating || backupLoading" @click="createStateSnapshot">
创建状态快照
</a-button>
<a-button :loading="backupLoading" @click="loadBackupSnapshots">刷新快照</a-button>
</div>
</a-card>
<a-card title="快照清单" :bordered="false">
<a-table :loading="backupLoading" :data-source="backupSnapshots" row-key="snapshotId">
<a-table-column title="快照" data-index="snapshotId" />
<a-table-column title="创建时间" data-index="createdAt" />
<a-table-column title="创建人" data-index="actorAccount" />
<a-table-column title="备注" data-index="reason" />
<a-table-column title="大小">
<template #default="{ record }">
{{ formatBytes(record.bytes) }}
</template>
</a-table-column>
<a-table-column title="操作">
<template #default="{ record }">
<a-button size="small" danger :loading="mutating" @click="restoreStateSnapshot(record)">
恢复到此快照
</a-button>
</template>
</a-table-column>
</a-table>
</a-card>
<a-card title="资源授权" :bordered="false">
<a-form layout="vertical" class="boss-admin-form">
<a-form-item label="账号">
<a-select v-model:value="grantForm.account" show-search>
<a-select-option v-for="user in users" :key="text(user.account)" :value="text(user.account)">
{{ text(user.displayName) }} · {{ text(user.account) }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="授权范围">
<a-segmented v-model:value="grantForm.scope" :options="['device', 'project', 'skill']" />
</a-form-item>
<a-form-item label="目标">
<a-select v-model:value="grantForm.targetId" show-search>
<a-select-option v-for="option in grantTargetOptions" :key="option.id" :value="option.id">
{{ option.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权限模板">
<a-select v-model:value="grantForm.templateId">
<a-select-option v-for="template in templates" :key="text(template.templateId)" :value="text(template.templateId)">
{{ text(template.name) }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权限">
<a-select v-model:value="grantForm.permissions" mode="tags" :placeholder="selectedScopePermissionPlaceholder" />
</a-form-item>
<a-space>
<a-button type="primary" :loading="mutating" @click="submitGrant">分配资源</a-button>
<a-button :loading="mutating" @click="applyPermissionTemplate">套用权限模板</a-button>
</a-space>
</a-form>
</a-card>
</section>
<section v-else class="boss-admin-section">
<a-card :title="adminSurface === 'platform' ? '平台设置' : '企业设置'" :bordered="false">
<p>当前独立后台已接入 Boss Admin BFF并已具备租户账号授权风险和 Skill 的基础治理动作</p>
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item v-for="(value, key) in payload?.yudaoMapping ?? {}" :key="key" :label="key">
{{ value }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</section>
</a-spin>
</main>
</div>
</a-config-provider>
</template>