Files
nexus/Project/fonrey/UI_DESIGN/平台管理后台_UI.html
2026-05-02 11:35:20 +08:00

1076 lines
61 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1366" />
<title>Fonrey 平台管理后台 · 静态原型</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA',
100: '#CCFBF1',
200: '#99F6E4',
500: '#14B8A6',
600: '#0F766E',
700: '#115E59',
800: '#134E4A'
},
neutral: {
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
600: '#475569',
700: '#334155',
800: '#1E293B',
900: '#0F172A'
},
success: { 50: '#F0FDF4', 600: '#16A34A' },
warning: { 50: '#FFFBEB', 600: '#D97706' },
danger: { 50: '#FEF2F2', 600: '#DC2626' },
info: { 50: '#EFF6FF', 600: '#2563EB' }
}
}
}
};
</script>
<style>
body { background: #F8FAFC; color: #0F172A; }
[x-cloak] { display: none !important; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 2px 8px;
font-size: 11px;
line-height: 16px;
white-space: nowrap;
}
.status-active { background: #F0FDF4; color: #16A34A; }
.status-warning { background: #FFFBEB; color: #D97706; }
.status-danger { background: #FEF2F2; color: #DC2626; }
.status-neutral { background: #F1F5F9; color: #475569; }
.status-info { background: #EFF6FF; color: #2563EB; }
.action-link { font-size: 12px; color: #2563EB; }
.action-link:hover { text-decoration: underline; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
</style>
</head>
<body class="text-sm antialiased" x-data="platformAdminPage()" x-init="init()" @keydown.escape.window="closeAllOverlays()">
<!-- Top Bar -->
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
<span class="text-base font-semibold text-white">Fonrey Platform</span>
</div>
<div class="flex-1 px-4">
<p class="text-sm text-primary-100">平台管理后台 · 页面切换统一通过左侧 Sidebar 导航</p>
</div>
<div class="flex items-center gap-1 px-4 shrink-0">
<button class="relative p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="平台告警">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-danger-600"></span>
</button>
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="帮助中心">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 17.25h.008v.008H12v-.008Zm-.255-3.745c.285-.274.645-.455 1.034-.695.55-.34 1.221-.756 1.221-1.81A2.5 2.5 0 0 0 9 11.25"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 3.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5Z"/></svg>
</button>
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">PA</div>
<span class="text-sm font-medium text-primary-100">平台管理员</span>
</div>
</div>
</header>
<!-- Sidebar -->
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white overflow-y-auto">
<nav class="p-3 space-y-0.5">
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">Platform Admin</div>
<template x-for="item in sidebarItems" :key="item.key">
<button
class="w-full text-left flex items-center justify-between gap-2 px-2 py-1.5 rounded-md"
:class="mainTab === item.key ? 'bg-primary-50 text-primary-700 font-medium' : 'text-neutral-700 hover:bg-neutral-100'"
@click="switchMainTab(item.key)"
>
<span x-text="item.label"></span>
<span class="text-[11px] text-neutral-400" x-show="item.badge" x-text="item.badge"></span>
</button>
</template>
</nav>
</aside>
<!-- Main -->
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
<div class="mx-auto max-w-[1760px] space-y-4">
<!-- Header -->
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<div class="flex items-start justify-between gap-4">
<div>
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
<a href="#" class="hover:text-neutral-700">平台管理</a>
<span>/</span>
<span class="text-neutral-900" x-text="activeMainTabLabel"></span>
</nav>
<h1 class="text-xl font-semibold text-neutral-900">平台管理后台</h1>
<p class="text-xs text-neutral-500 mt-1">面向平台管理员的统一控制台:租户、版本、备份、客户端发布、监控告警、审计与管理员安全</p>
</div>
<div class="flex items-center gap-2">
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-50" @click="notify('已打开系统操作手册(原型)')">操作手册</button>
<button class="px-3 py-1.5 rounded-md border border-info-600 text-info-600 bg-white hover:bg-info-50" @click="notify('已同步最新平台状态(原型)')">刷新全局状态</button>
</div>
</div>
</section>
<!-- Dashboard -->
<template x-if="mainTab === 'dashboard'">
<section class="space-y-4">
<section class="grid grid-cols-6 gap-4">
<template x-for="card in dashboardCards" :key="card.key">
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500" x-text="card.label"></p>
<p class="mt-2 text-2xl font-semibold tabular-nums" x-text="card.value"></p>
<p class="mt-1 text-xs" :class="card.deltaType === 'up' ? 'text-success-600' : card.deltaType === 'down' ? 'text-danger-600' : 'text-neutral-500'" x-text="card.delta"></p>
</article>
</template>
</section>
<section class="grid grid-cols-3 gap-4">
<article class="col-span-2 bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">系统健康面板</h3>
<span class="text-xs text-neutral-500">最近 5 分钟</span>
</header>
<div class="p-4 grid grid-cols-5 gap-3">
<template x-for="s in serviceHealth" :key="s.name">
<div class="rounded-lg border border-neutral-200 p-3 bg-neutral-50">
<div class="flex items-center justify-between">
<span class="text-xs text-neutral-500" x-text="s.name"></span>
<span class="status-badge" :class="s.status === 'healthy' ? 'status-active' : s.status === 'warn' ? 'status-warning' : 'status-danger'" x-text="s.statusLabel"></span>
</div>
<p class="mt-2 text-sm font-medium" x-text="s.metric"></p>
</div>
</template>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">近期告警24h</h3>
<button class="action-link" @click="switchMainTab('monitoring')">查看全部</button>
</header>
<div class="p-3 space-y-2 max-h-[292px] overflow-y-auto">
<template x-for="a in alerts" :key="a.id">
<div class="rounded-md border px-3 py-2" :class="a.level === 'critical' ? 'border-danger-600 bg-danger-50' : a.level === 'warning' ? 'border-warning-600 bg-warning-50' : 'border-info-600 bg-info-50'">
<p class="text-xs font-medium" x-text="a.title"></p>
<p class="text-[11px] text-neutral-500 mt-0.5" x-text="a.time"></p>
</div>
</template>
</div>
</article>
</section>
<section class="grid grid-cols-3 gap-4">
<article class="bg-white border border-neutral-200 rounded-lg">
<header class="px-4 py-3 border-b border-neutral-200"><h3 class="font-semibold">客户端覆盖</h3></header>
<div class="p-4 space-y-3 text-xs">
<div class="flex items-center justify-between"><span class="text-neutral-500">活跃安装24h</span><span class="font-semibold tabular-nums">18,936</span></div>
<div class="flex items-center justify-between"><span class="text-neutral-500">最新版本覆盖</span><span class="font-semibold tabular-nums">84.7%</span></div>
<div class="flex items-center justify-between"><span class="text-neutral-500">落后租户数</span><span class="font-semibold tabular-nums text-warning-600">12</span></div>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg">
<header class="px-4 py-3 border-b border-neutral-200"><h3 class="font-semibold">高危操作最近10条</h3></header>
<div class="p-3 space-y-2 text-xs max-h-[180px] overflow-y-auto">
<template x-for="log in riskyOps" :key="log.id">
<div class="rounded border border-neutral-200 px-2 py-1.5">
<p class="font-medium" x-text="log.action"></p>
<p class="text-neutral-500" x-text="`${log.operator} · ${log.time}`"></p>
</div>
</template>
</div>
</article>
<article class="bg-white border border-dashed border-neutral-300 rounded-lg">
<header class="px-4 py-3 border-b border-dashed border-neutral-300 flex items-center justify-between">
<h3 class="font-semibold text-neutral-600">Dashboard 扩展位</h3>
<span class="text-xs text-neutral-400">widget-slot-b</span>
</header>
<div class="p-4 text-xs text-neutral-500">预留给后续运营看板:如留存、续费转化、工单 SLA 等模块。</div>
</article>
</section>
<section class="bg-white border border-dashed border-neutral-300 rounded-lg p-4 text-xs text-neutral-500">
<div class="flex items-center justify-between">
<span>widget-slot-footer全宽扩展位</span>
<button class="action-link" @click="notify('已登记新增 dashboard 需求(原型)')">新增看板模块</button>
</div>
</section>
</section>
</template>
<!-- Tenants -->
<template x-if="mainTab === 'tenants'">
<section class="space-y-4">
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
<div class="grid grid-cols-6 gap-3">
<input x-model.trim="tenantFilters.keyword" type="text" placeholder="公司名称 / Tenant Code / 联系邮箱" class="px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
<select x-model="tenantFilters.status" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
<option value="">状态(全选)</option>
<option value="active">Active</option>
<option value="suspended">Suspended</option>
<option value="pending_delete">Pending Delete</option>
<option value="failed">Failed</option>
</select>
<select x-model="tenantFilters.plan" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
<option value="">套餐(全选)</option>
<option value="Basic">Basic</option>
<option value="Professional">Professional</option>
<option value="Enterprise">Enterprise</option>
</select>
<label class="flex items-center gap-2 px-3 py-2 rounded-md border border-neutral-200 bg-neutral-50 text-xs text-neutral-600">
<input type="checkbox" class="rounded border-neutral-300" x-model="tenantFilters.expiringSoon" />
15天内到期
</label>
<label class="flex items-center gap-2 px-3 py-2 rounded-md border border-neutral-200 bg-neutral-50 text-xs text-neutral-600">
<input type="checkbox" class="rounded border-neutral-300" x-model="tenantFilters.userLimitFull" />
用户数已满/超限
</label>
<div class="flex items-center justify-end gap-2">
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700">查询</button>
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="resetTenantFilters()">清空条件</button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="text-xs text-neutral-500"><span class="tabular-nums" x-text="filteredTenants.length"></span> 个租户</div>
<div class="flex items-center gap-2">
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-50" @click="notify('已创建导出任务(原型)')">导出租户</button>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('打开新建租户流程(原型)')">+ 新建租户</button>
</div>
</div>
</section>
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">公司名称</th>
<th class="px-3 py-2 text-left">Tenant Code</th>
<th class="px-3 py-2 text-left">套餐</th>
<th class="px-3 py-2 text-left">状态</th>
<th class="px-3 py-2 text-left">License 到期</th>
<th class="px-3 py-2 text-left">活跃用户 / 上限</th>
<th class="px-3 py-2 text-left">最新版本覆盖</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-if="filteredTenants.length === 0">
<tr><td colspan="8" class="px-3 py-8 text-center text-neutral-400">暂无匹配租户</td></tr>
</template>
<template x-for="t in filteredTenants" :key="t.id">
<tr>
<td class="px-3 py-2" x-text="t.name"></td>
<td class="px-3 py-2 tabular-nums" x-text="t.code"></td>
<td class="px-3 py-2" x-text="t.plan"></td>
<td class="px-3 py-2">
<span class="status-badge" :class="t.status==='active' ? 'status-active' : t.status==='suspended' ? 'status-warning' : t.status==='pending_delete' ? 'status-danger' : 'status-neutral'" x-text="tenantStatusLabel(t.status)"></span>
</td>
<td class="px-3 py-2 tabular-nums" x-text="t.paidUntil"></td>
<td class="px-3 py-2 tabular-nums" x-text="`${t.activeUsers}/${t.licenseLimit}`"></td>
<td class="px-3 py-2 tabular-nums" x-text="t.coverage"></td>
<td class="px-3 py-2 whitespace-nowrap">
<button class="action-link" @click="openTenantDetail(t)">详情</button>
<button class="action-link ml-2" @click="openDangerAction('挂起租户', t)">挂起</button>
<button class="action-link ml-2" @click="notify(`已触发 ${t.name} 手动备份(原型)`)">备份</button>
<button class="action-link ml-2" @click="notify(`已创建 ${t.name} 数据导出任务(原型)`)">导出</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
<!-- System Versions -->
<template x-if="mainTab === 'versions'">
<section class="space-y-4">
<section class="grid grid-cols-3 gap-4">
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">平台当前版本</p>
<p class="mt-2 text-2xl font-semibold">v2.6.1</p>
<p class="mt-1 text-xs text-neutral-500">最后升级2026-05-01 23:10</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">待升级租户</p>
<p class="mt-2 text-2xl font-semibold text-warning-600 tabular-nums">18</p>
<p class="mt-1 text-xs text-neutral-500">含升级失败 3 个</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">灰度批次进度</p>
<p class="mt-2 text-2xl font-semibold tabular-nums">2 / 4</p>
<p class="mt-1 text-xs text-neutral-500">当前批执行中,健康门控已启用</p>
</article>
</section>
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">租户升级版本状态</h3>
<div class="flex items-center gap-2">
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="notify('已打开升级包管理(原型)')">升级包管理</button>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('已进入灰度升级向导(原型)')">发起灰度升级</button>
<button class="px-3 py-1.5 rounded-md border border-danger-600 text-danger-600 hover:bg-danger-50" @click="openDangerAction('系统回滚', { id: 'upgrade-event-20260502' })">一键回滚</button>
</div>
</header>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">租户名称</th>
<th class="px-3 py-2 text-left">当前数据版本</th>
<th class="px-3 py-2 text-left">上次升级时间</th>
<th class="px-3 py-2 text-left">升级状态</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="u in upgradeRows" :key="u.id">
<tr>
<td class="px-3 py-2" x-text="u.name"></td>
<td class="px-3 py-2" x-text="u.version"></td>
<td class="px-3 py-2 tabular-nums" x-text="u.lastUpgrade"></td>
<td class="px-3 py-2">
<span class="status-badge" :class="u.status==='latest' ? 'status-active' : u.status==='upgrading' ? 'status-info' : u.status==='failed' ? 'status-danger' : 'status-warning'" x-text="upgradeStatusLabel(u.status)"></span>
</td>
<td class="px-3 py-2">
<button class="action-link" @click="notify(`查看 ${u.name} 升级详情(原型)`)">详情</button>
<button class="action-link ml-2" @click="notify(`重试 ${u.name} 升级任务(原型)`)">重试</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
<!-- Backups -->
<template x-if="mainTab === 'backups'">
<section class="space-y-4">
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<h3 class="font-semibold mb-3">全局备份策略</h3>
<div class="grid grid-cols-4 gap-3">
<label class="block text-xs text-neutral-500">频率
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="backupConfig.frequency">
<option value="hourly">每小时</option>
<option value="daily">每日</option>
<option value="weekly">每周</option>
</select>
</label>
<label class="block text-xs text-neutral-500">执行时间窗口
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" x-model="backupConfig.window" placeholder="02:00-05:00" />
</label>
<label class="block text-xs text-neutral-500">保留数量
<input type="number" min="1" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" x-model.number="backupConfig.retention" />
</label>
<label class="block text-xs text-neutral-500">存储目标
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="backupConfig.storage">
<option value="r2">Cloudflare R2</option>
<option value="s3">Amazon S3</option>
<option value="gcs">Google Cloud Storage</option>
<option value="local">Local</option>
</select>
</label>
</div>
<div class="mt-3 flex items-center justify-end gap-2">
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="notify('已重置备份策略修改(原型)')">取消</button>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('备份策略已保存(原型)')">保存策略</button>
</div>
</section>
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">备份任务列表</h3>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('已触发全局备份任务(原型)')">手动触发备份</button>
</header>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">任务ID</th>
<th class="px-3 py-2 text-left">租户</th>
<th class="px-3 py-2 text-left">触发方式</th>
<th class="px-3 py-2 text-left">状态</th>
<th class="px-3 py-2 text-left">大小</th>
<th class="px-3 py-2 text-left">开始时间</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="b in backupJobs" :key="b.id">
<tr>
<td class="px-3 py-2 tabular-nums" x-text="b.id"></td>
<td class="px-3 py-2" x-text="b.tenant"></td>
<td class="px-3 py-2" x-text="b.trigger"></td>
<td class="px-3 py-2">
<span class="status-badge" :class="b.status==='success' ? 'status-active' : b.status==='failed' ? 'status-danger' : b.status==='in_progress' ? 'status-info' : 'status-neutral'" x-text="backupStatusLabel(b.status)"></span>
</td>
<td class="px-3 py-2 tabular-nums" x-text="b.size"></td>
<td class="px-3 py-2 tabular-nums" x-text="b.startedAt"></td>
<td class="px-3 py-2">
<button class="action-link" @click="notify(`查看 ${b.id} 详情(原型)`)">详情</button>
<button class="action-link ml-2" x-show="b.status==='failed'" @click="notify(`重试 ${b.id}(原型)`)">重试</button>
<button class="action-link ml-2" @click="openDangerAction('数据恢复', b)">恢复</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
<!-- Client Releases -->
<template x-if="mainTab === 'client-releases'">
<section class="space-y-4">
<section class="grid grid-cols-3 gap-4">
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">当前发布版本</p>
<p class="text-2xl font-semibold mt-2">v1.8.2</p>
<p class="text-xs text-neutral-500 mt-1">发布于 2026-05-01 14:20</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">24h 下载量</p>
<p class="text-2xl font-semibold mt-2 tabular-nums">4,821</p>
<p class="text-xs text-success-600 mt-1">较昨日 +8.4%</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">强制升级影响租户</p>
<p class="text-2xl font-semibold mt-2 tabular-nums text-warning-600">12</p>
<p class="text-xs text-neutral-500 mt-1">建议分批通知</p>
</article>
</section>
<section class="grid grid-cols-3 gap-4">
<article class="col-span-2 bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">客户端版本列表</h3>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('打开新增版本表单(原型)')">+ 新增版本</button>
</header>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">版本号</th>
<th class="px-3 py-2 text-left">类型</th>
<th class="px-3 py-2 text-left">状态</th>
<th class="px-3 py-2 text-left">发布时间</th>
<th class="px-3 py-2 text-left">下载量</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="r in clientReleases" :key="r.id">
<tr>
<td class="px-3 py-2" x-text="r.version"></td>
<td class="px-3 py-2" x-text="r.releaseType"></td>
<td class="px-3 py-2">
<span class="status-badge" :class="r.status==='published' ? 'status-active' : r.status==='draft' ? 'status-neutral' : 'status-warning'" x-text="releaseStatusLabel(r.status)"></span>
</td>
<td class="px-3 py-2 tabular-nums" x-text="r.publishedAt"></td>
<td class="px-3 py-2 tabular-nums" x-text="r.downloadCount"></td>
<td class="px-3 py-2">
<button class="action-link" @click="notify(`编辑 ${r.version}(原型)`)">编辑</button>
<button class="action-link ml-2" @click="notify(`发布 ${r.version}(原型)`)">发布</button>
<button class="action-link ml-2" @click="openDangerAction('客户端版本下线', r)">下线</button>
<button class="action-link ml-2" @click="openDangerAction('强制更新推送', r)">强制更新</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200"><h3 class="font-semibold">版本分布24h 活跃)</h3></header>
<div class="p-4 space-y-2 text-xs">
<template x-for="d in versionDistribution" :key="d.version">
<div>
<div class="flex items-center justify-between mb-1"><span x-text="d.version"></span><span class="tabular-nums" x-text="d.percent"></span></div>
<div class="h-2 rounded bg-neutral-100 overflow-hidden">
<div class="h-full bg-primary-600" :style="`width:${d.percent}`"></div>
</div>
</div>
</template>
</div>
</article>
</section>
</section>
</template>
<!-- Monitoring -->
<template x-if="mainTab === 'monitoring'">
<section class="space-y-4">
<section class="grid grid-cols-4 gap-4">
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">API 可用性</p>
<p class="mt-2 text-2xl font-semibold tabular-nums">99.94%</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">平均响应P95</p>
<p class="mt-2 text-2xl font-semibold tabular-nums">1.42s</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">慢查询24h</p>
<p class="mt-2 text-2xl font-semibold tabular-nums text-warning-600">27</p>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<p class="text-xs text-neutral-500">异常请求24h</p>
<p class="mt-2 text-2xl font-semibold tabular-nums text-danger-600">14</p>
</article>
</section>
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">告警规则</h3>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('打开新增告警规则(原型)')">+ 新增规则</button>
</header>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">规则名称</th>
<th class="px-3 py-2 text-left">指标</th>
<th class="px-3 py-2 text-left">阈值</th>
<th class="px-3 py-2 text-left">通知渠道</th>
<th class="px-3 py-2 text-left">状态</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="rule in alertRules" :key="rule.id">
<tr>
<td class="px-3 py-2" x-text="rule.name"></td>
<td class="px-3 py-2" x-text="rule.metric"></td>
<td class="px-3 py-2" x-text="rule.threshold"></td>
<td class="px-3 py-2" x-text="rule.channel"></td>
<td class="px-3 py-2"><span class="status-badge" :class="rule.enabled ? 'status-active' : 'status-neutral'" x-text="rule.enabled ? '启用' : '停用'"></span></td>
<td class="px-3 py-2">
<button class="action-link" @click="notify(`编辑 ${rule.name}(原型)`)">编辑</button>
<button class="action-link ml-2" @click="notify(`查看 ${rule.name} 告警历史(原型)`)">历史</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
<!-- Audit Logs -->
<template x-if="mainTab === 'audit'">
<section class="space-y-4">
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<div class="grid grid-cols-6 gap-3">
<input x-model.trim="auditFilters.operator" type="text" placeholder="操作人" class="px-3 py-2 rounded-md border border-neutral-300" />
<input x-model="auditFilters.target" type="text" placeholder="操作对象" class="px-3 py-2 rounded-md border border-neutral-300" />
<select x-model="auditFilters.action" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
<option value="">操作类型(全选)</option>
<option value="create">创建</option>
<option value="update">修改</option>
<option value="delete">删除</option>
<option value="danger">高危</option>
</select>
<select x-model="auditFilters.result" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
<option value="">结果(全选)</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
<input x-model="auditFilters.dateRange" type="text" placeholder="时间范围近7天" class="px-3 py-2 rounded-md border border-neutral-300" />
<div class="flex items-center justify-end gap-2">
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700">查询</button>
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="resetAuditFilters()">重置</button>
</div>
</div>
<div class="mt-3 flex justify-end">
<button class="px-3 py-1.5 rounded-md border border-info-600 text-info-600 bg-white hover:bg-info-50" @click="notify('已创建审计日志 CSV 导出任务(原型)')">导出 CSV</button>
</div>
</section>
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">操作人</th>
<th class="px-3 py-2 text-left">操作时间</th>
<th class="px-3 py-2 text-left">对象类型/ID</th>
<th class="px-3 py-2 text-left">内容摘要</th>
<th class="px-3 py-2 text-left">结果</th>
<th class="px-3 py-2 text-left">来源 IP</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="row in filteredAuditLogs" :key="row.id">
<tr>
<td class="px-3 py-2" x-text="row.operator"></td>
<td class="px-3 py-2 tabular-nums" x-text="row.time"></td>
<td class="px-3 py-2" x-text="row.target"></td>
<td class="px-3 py-2" x-text="row.summary"></td>
<td class="px-3 py-2">
<span class="status-badge" :class="row.result === 'success' ? 'status-active' : 'status-danger'" x-text="row.result === 'success' ? '成功' : '失败'"></span>
</td>
<td class="px-3 py-2 tabular-nums" x-text="row.ip"></td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
<!-- Admin Settings -->
<template x-if="mainTab === 'settings'">
<section class="space-y-4">
<section class="bg-white border border-neutral-200 rounded-lg overflow-hidden">
<header class="px-4 py-3 border-b border-neutral-200 flex items-center justify-between">
<h3 class="font-semibold">管理员账号与安全设置</h3>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('打开新增管理员弹窗(原型)')">+ 新增管理员</button>
</header>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">姓名</th>
<th class="px-3 py-2 text-left">角色</th>
<th class="px-3 py-2 text-left">MFA</th>
<th class="px-3 py-2 text-left">IP 白名单</th>
<th class="px-3 py-2 text-left">活跃会话</th>
<th class="px-3 py-2 text-left">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white">
<template x-for="a in admins" :key="a.id">
<tr>
<td class="px-3 py-2" x-text="a.name"></td>
<td class="px-3 py-2" x-text="a.role"></td>
<td class="px-3 py-2"><span class="status-badge" :class="a.mfa ? 'status-active' : 'status-warning'" x-text="a.mfa ? '已启用' : '未启用'"></span></td>
<td class="px-3 py-2" x-text="a.ipPolicy"></td>
<td class="px-3 py-2 tabular-nums" x-text="a.sessions"></td>
<td class="px-3 py-2 whitespace-nowrap">
<button class="action-link" @click="notify(`编辑 ${a.name}(原型)`)">编辑</button>
<button class="action-link ml-2" @click="notify(`管理 ${a.name} 的 MFA 设备(原型)`)">MFA设备</button>
<button class="action-link ml-2" @click="openDangerAction('强制登出管理员会话', a)">强制登出</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<h3 class="font-semibold mb-3">角色权限矩阵</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead class="bg-neutral-50 border border-neutral-200 text-neutral-500">
<tr>
<th class="px-3 py-2 text-left">页面/操作</th>
<th class="px-3 py-2 text-center">Platform Admin</th>
<th class="px-3 py-2 text-center">运营人员</th>
<th class="px-3 py-2 text-center">只读审计员</th>
</tr>
</thead>
<tbody class="bg-white border-x border-b border-neutral-200 divide-y divide-neutral-100">
<template x-for="m in permissionMatrix" :key="m.action">
<tr>
<td class="px-3 py-2" x-text="m.action"></td>
<td class="px-3 py-2 text-center" x-text="m.admin"></td>
<td class="px-3 py-2 text-center" x-text="m.ops"></td>
<td class="px-3 py-2 text-center" x-text="m.auditor"></td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
</section>
</template>
</div>
</main>
<!-- Tenant Detail Drawer -->
<div class="fixed inset-0 z-40" x-show="tenantDrawerOpen" x-cloak>
<div class="absolute inset-0 bg-neutral-900/35" @click="tenantDrawerOpen = false"></div>
<aside class="absolute right-0 top-0 h-full w-[760px] bg-white border-l border-neutral-200 shadow-xl flex flex-col" x-show="tenantDrawerOpen" x-transition>
<header class="h-14 px-5 border-b border-neutral-200 flex items-center justify-between shrink-0">
<div>
<h3 class="text-base font-semibold">租户详情</h3>
<p class="text-xs text-neutral-500" x-text="selectedTenant ? selectedTenant.name : ''"></p>
</div>
<button class="p-1 rounded hover:bg-neutral-100" aria-label="关闭" @click="tenantDrawerOpen = false">
<svg class="w-5 h-5 text-neutral-500" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</header>
<section class="flex-1 overflow-y-auto p-5 text-xs space-y-4" x-show="selectedTenant">
<article class="rounded-md border border-neutral-200 p-4 space-y-3">
<h4 class="text-sm font-semibold text-neutral-900">基本信息</h4>
<div class="grid grid-cols-2 gap-4">
<div class="rounded-md border border-neutral-200 p-3"><p class="text-neutral-500">Tenant Code</p><p class="mt-1 font-medium" x-text="selectedTenant?.code"></p></div>
<div class="rounded-md border border-neutral-200 p-3"><p class="text-neutral-500">套餐</p><p class="mt-1 font-medium" x-text="selectedTenant?.plan"></p></div>
<div class="rounded-md border border-neutral-200 p-3"><p class="text-neutral-500">License 到期</p><p class="mt-1 font-medium" x-text="selectedTenant?.paidUntil"></p></div>
<div class="rounded-md border border-neutral-200 p-3"><p class="text-neutral-500">访问链接</p><p class="mt-1 font-medium truncate">https://app.fonrey.com/?tenant=<span x-text="selectedTenant?.code"></span></p></div>
</div>
</article>
<article class="rounded-md border border-neutral-200 p-4 space-y-2">
<h4 class="text-sm font-semibold text-neutral-900">用户管理</h4>
<p class="text-neutral-500">支持新增/替换 Tenant Admin支持重置密码。</p>
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="notify('打开 Tenant Admin 维护弹窗(原型)')">维护 Tenant Admin</button>
</article>
<article class="rounded-md border border-neutral-200 p-4 space-y-2">
<h4 class="text-sm font-semibold text-neutral-900">套餐信息</h4>
<p class="text-neutral-500">当前套餐:<span x-text="selectedTenant?.plan"></span>,支持立即生效或下一账期生效。</p>
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="notify('打开套餐升级向导(原型)')">调整套餐</button>
</article>
<article class="rounded-md border border-neutral-200 p-4 space-y-2">
<h4 class="text-sm font-semibold text-neutral-900">租户监控</h4>
<p class="text-neutral-500">CPU/内存、存储、API 调用与慢查询指标一览。</p>
</article>
<article class="rounded-md border border-neutral-200 p-4 space-y-2">
<h4 class="text-sm font-semibold text-neutral-900">备份记录</h4>
<p class="text-neutral-500">可查看租户最近备份与恢复记录。</p>
<button class="px-3 py-1.5 rounded-md border border-danger-600 text-danger-600 hover:bg-danger-50" @click="openDangerAction('数据恢复', selectedTenant)">恢复数据</button>
</article>
<article class="rounded-md border border-neutral-200 p-4 space-y-2">
<h4 class="text-sm font-semibold text-neutral-900">操作历史</h4>
<template x-for="h in tenantHistory" :key="h.id">
<div class="border border-neutral-100 rounded px-3 py-2">
<p class="font-medium" x-text="h.action"></p>
<p class="text-neutral-500" x-text="h.meta"></p>
</div>
</template>
</article>
</section>
</aside>
</div>
<!-- Danger Action Modal (2-step + MFA) -->
<div class="fixed inset-0 z-50" x-show="dangerModalOpen" x-cloak>
<div class="absolute inset-0 bg-neutral-900/45" @click="closeDangerModal()"></div>
<div class="absolute inset-0 flex items-center justify-center p-4">
<div class="w-[560px] bg-white border border-neutral-200 rounded-lg shadow-xl overflow-hidden" x-show="dangerModalOpen" x-transition>
<header class="px-5 py-4 border-b border-neutral-200">
<h3 class="text-base font-semibold" x-text="dangerStep === 1 ? '确认执行高危操作?' : 'MFA 验证' "></h3>
<p class="text-xs text-neutral-500 mt-1" x-text="dangerStep === 1 ? '该操作可能影响在线租户与数据可用性,请确认后继续。' : '请输入 6 位动态码,完成二次验证后执行。'"></p>
</header>
<div class="px-5 py-4 text-xs space-y-3" x-show="dangerStep === 1">
<div class="rounded-md border border-danger-600 bg-danger-50 p-3">
<p class="font-medium text-danger-600" x-text="dangerAction?.label"></p>
<p class="text-neutral-600 mt-1">目标对象:<span class="font-medium" x-text="dangerAction?.targetText"></span></p>
</div>
<label class="block text-neutral-500">操作原因(必填)
<textarea x-model.trim="dangerReason" rows="3" maxlength="200" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 resize-none" placeholder="请说明执行该操作的原因"></textarea>
</label>
</div>
<div class="px-5 py-4 text-xs space-y-3" x-show="dangerStep === 2">
<label class="block text-neutral-500">MFA 动态码
<input x-model.trim="mfaCode" type="text" inputmode="numeric" maxlength="6" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 tracking-[0.25em] tabular-nums" placeholder="000000" />
</label>
<p class="text-danger-600" x-show="dangerError" x-text="dangerError"></p>
</div>
<footer class="px-5 py-3 border-t border-neutral-200 flex items-center justify-end gap-2">
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="closeDangerModal()">取消</button>
<button class="px-3 py-1.5 rounded-md border border-danger-600 text-danger-600 hover:bg-danger-50" x-show="dangerStep === 1" @click="gotoMfaStep()">继续并验证</button>
<button class="px-3 py-1.5 rounded-md bg-danger-600 text-white hover:bg-danger-700" x-show="dangerStep === 2" @click="confirmDangerAction()">确认执行</button>
</footer>
</div>
</div>
</div>
<!-- Toast -->
<div class="fixed right-5 bottom-5 z-[60]" x-show="toastOpen" x-cloak>
<div class="bg-neutral-900 text-white text-xs px-3 py-2 rounded-md shadow-lg" x-text="toastMessage"></div>
</div>
<script>
function platformAdminPage() {
return {
mainTab: 'dashboard',
sidebarItems: [
{ key: 'dashboard', label: '概览看板' },
{ key: 'tenants', label: '租户管理', badge: '15天到期 9' },
{ key: 'versions', label: '系统版本', badge: '失败 3' },
{ key: 'backups', label: '备份与恢复', badge: '进行中 4' },
{ key: 'client-releases', label: '客户端发布' },
{ key: 'monitoring', label: '监控与告警', badge: '告警 6' },
{ key: 'audit', label: '审计日志' },
{ key: 'settings', label: '管理员设置' }
],
dashboardCards: [
{ key: 'total', label: '总租户数', value: '326', delta: '较上月 +12', deltaType: 'up' },
{ key: 'active', label: '活跃租户', value: '302', delta: '活跃率 92.6%', deltaType: 'flat' },
{ key: 'new', label: '本月新增', value: '18', delta: '较上月 +5', deltaType: 'up' },
{ key: 'expiring', label: '15天内到期', value: '9', delta: '需运营跟进', deltaType: 'down' },
{ key: 'api', label: 'API 调用24h', value: '4.82M', delta: '峰值 221/s', deltaType: 'flat' },
{ key: 'storage', label: 'R2 存储', value: '7.3TB', delta: '较上周 +0.2TB', deltaType: 'up' }
],
serviceHealth: [
{ name: 'Django', status: 'healthy', statusLabel: '正常', metric: 'RT 122ms' },
{ name: 'PostgreSQL', status: 'healthy', statusLabel: '正常', metric: 'CPU 42%' },
{ name: 'Redis', status: 'healthy', statusLabel: '正常', metric: 'Hit 96.2%' },
{ name: 'Celery', status: 'warn', statusLabel: '拥塞', metric: '积压 47' },
{ name: 'R2', status: 'healthy', statusLabel: '正常', metric: '可用 99.99%' }
],
alerts: [
{ id: 1, level: 'critical', title: '租户 S0921 升级批次失败,状态 halted', time: '2026-05-02 09:12' },
{ id: 2, level: 'warning', title: 'R2 下载错误率超过阈值 2.5%', time: '2026-05-02 08:47' },
{ id: 3, level: 'warning', title: '备份任务 BK-20260502-114 执行超时', time: '2026-05-02 08:15' },
{ id: 4, level: 'info', title: '新版本 v1.8.2 发布完成', time: '2026-05-01 14:20' }
],
riskyOps: [
{ id: 1, action: '系统回滚 · 事件 UE-20260501-17', operator: '陈泽', time: '10:12' },
{ id: 2, action: '强制升级推送 · v1.8.2', operator: '刘洋', time: '09:45' },
{ id: 3, action: '数据恢复 · S0087', operator: '王璐', time: '09:22' }
],
tenantFilters: {
keyword: '',
status: '',
plan: '',
expiringSoon: false,
userLimitFull: false
},
tenants: [
{ id: 1, name: '上海嘉禾地产', code: 'SHGJH000129', plan: 'Enterprise', status: 'active', paidUntil: '2026-07-12', activeUsers: 168, licenseLimit: 200, coverage: '92.1%' },
{ id: 2, name: '深圳德新置业', code: 'SZDXZ000341', plan: 'Professional', status: 'suspended', paidUntil: '2026-05-08', activeUsers: 52, licenseLimit: 50, coverage: '63.4%' },
{ id: 3, name: '杭州居和房产', code: 'HZJHF000087', plan: 'Basic', status: 'active', paidUntil: '2026-05-11', activeUsers: 45, licenseLimit: 50, coverage: '78.9%' },
{ id: 4, name: '南京启盈置业', code: 'NJQYZ000512', plan: 'Professional', status: 'pending_delete', paidUntil: '2026-06-01', activeUsers: 21, licenseLimit: 60, coverage: '44.2%' }
],
upgradeRows: [
{ id: 'u1', name: '上海嘉禾地产', version: 'tenant-v2.6.1', lastUpgrade: '2026-05-01 23:30', status: 'latest' },
{ id: 'u2', name: '深圳德新置业', version: 'tenant-v2.5.8', lastUpgrade: '2026-04-25 00:20', status: 'pending' },
{ id: 'u3', name: '南京启盈置业', version: 'tenant-v2.5.7', lastUpgrade: '2026-04-24 23:55', status: 'failed' },
{ id: 'u4', name: '杭州居和房产', version: 'tenant-v2.6.0', lastUpgrade: '2026-05-01 22:12', status: 'upgrading' }
],
backupConfig: { frequency: 'daily', window: '02:00-05:00', retention: 30, storage: 'r2' },
backupJobs: [
{ id: 'BK-20260502-114', tenant: '全局', trigger: '定时', status: 'in_progress', size: '—', startedAt: '2026-05-02 09:00' },
{ id: 'BK-20260502-110', tenant: '上海嘉禾地产', trigger: '手动', status: 'success', size: '8.6GB', startedAt: '2026-05-02 07:40' },
{ id: 'BK-20260502-102', tenant: '深圳德新置业', trigger: '定时', status: 'failed', size: '5.2GB', startedAt: '2026-05-02 02:00' }
],
clientReleases: [
{ id: 'r1', version: '1.8.2', releaseType: 'force', status: 'published', publishedAt: '2026-05-01 14:20', downloadCount: '28,301' },
{ id: 'r2', version: '1.8.1', releaseType: 'normal', status: 'archived', publishedAt: '2026-04-20 11:10', downloadCount: '43,912' },
{ id: 'r3', version: '1.9.0-rc1', releaseType: 'normal', status: 'draft', publishedAt: '-', downloadCount: '0' }
],
versionDistribution: [
{ version: 'v1.8.2', percent: '84.7%' },
{ version: 'v1.8.1', percent: '11.9%' },
{ version: 'v1.7.x', percent: '3.4%' }
],
alertRules: [
{ id: 1, name: 'API 错误率告警', metric: '5xx_ratio', threshold: '> 1% 持续 5m', channel: '邮件 + Webhook', enabled: true },
{ id: 2, name: '慢查询告警', metric: 'slow_query_count', threshold: '> 30/小时', channel: '邮件', enabled: true },
{ id: 3, name: '备份失败告警', metric: 'backup_failed', threshold: '>= 1 次', channel: 'Webhook', enabled: true }
],
auditFilters: { operator: '', target: '', action: '', result: '', dateRange: '近7天' },
auditLogs: [
{ id: 1, operator: '陈泽', time: '2026-05-02 09:12', target: 'upgrade_events / UE-20260502-17', action: 'danger', summary: '执行系统回滚', result: 'success', ip: '10.2.4.16' },
{ id: 2, operator: '刘洋', time: '2026-05-02 08:58', target: 'client_releases / 1.8.2', action: 'update', summary: '设置强制升级', result: 'success', ip: '10.2.5.31' },
{ id: 3, operator: '王璐', time: '2026-05-02 08:10', target: 'backup_records / BK-20260502-102', action: 'danger', summary: '发起数据恢复失败MFA错误', result: 'failed', ip: '10.2.7.22' }
],
admins: [
{ id: 1, name: '陈泽', role: 'Platform Admin', mfa: true, ipPolicy: '10.2.0.0/16', sessions: 2 },
{ id: 2, name: '刘洋', role: '运营人员', mfa: true, ipPolicy: '10.2.0.0/16', sessions: 1 },
{ id: 3, name: '王璐', role: '只读审计员', mfa: false, ipPolicy: '10.2.7.0/24', sessions: 1 }
],
permissionMatrix: [
{ action: '租户创建/挂起/恢复', admin: '✅', ops: '✅', auditor: '❌' },
{ action: '硬删除租户', admin: '✅', ops: '❌', auditor: '❌' },
{ action: '数据恢复', admin: '✅', ops: '❌', auditor: '❌' },
{ action: '系统升级/回滚', admin: '✅', ops: '❌(只读)', auditor: '❌' },
{ action: '审计日志查看与导出', admin: '✅', ops: '✅', auditor: '✅' }
],
tenantDrawerOpen: false,
selectedTenant: null,
tenantHistory: [
{ id: 1, action: '调整 License 用户上限150 → 200', meta: '陈泽 · 2026-04-28 16:32' },
{ id: 2, action: '触发全量备份任务 BK-20260425-090', meta: '刘洋 · 2026-04-25 02:01' },
{ id: 3, action: '更新 Tenant Admin 联系方式', meta: '王璐 · 2026-04-22 11:09' }
],
dangerModalOpen: false,
dangerStep: 1,
dangerAction: null,
dangerReason: '',
mfaCode: '',
dangerError: '',
toastOpen: false,
toastMessage: '',
toastTimer: null,
init() {},
get activeMainTabLabel() {
const hit = this.sidebarItems.find(t => t.key === this.mainTab);
return hit ? hit.label : '概览看板';
},
switchMainTab(tab) {
this.mainTab = tab;
},
tenantStatusLabel(status) {
return ({ active: 'Active', suspended: 'Suspended', pending_delete: 'Pending Delete', failed: 'Failed' })[status] || status;
},
upgradeStatusLabel(status) {
return ({ latest: '最新', pending: '待升级', upgrading: '升级中', failed: '升级失败' })[status] || status;
},
backupStatusLabel(status) {
return ({ pending: '待执行', in_progress: '进行中', success: '成功', failed: '失败' })[status] || status;
},
releaseStatusLabel(status) {
return ({ draft: '草稿', published: '已发布', archived: '已下线' })[status] || status;
},
get filteredTenants() {
const keyword = this.tenantFilters.keyword.trim().toLowerCase();
return this.tenants.filter(t => {
const matchKeyword = !keyword || [t.name, t.code].some(v => String(v).toLowerCase().includes(keyword));
const matchStatus = !this.tenantFilters.status || t.status === this.tenantFilters.status;
const matchPlan = !this.tenantFilters.plan || t.plan === this.tenantFilters.plan;
const matchExpiring = !this.tenantFilters.expiringSoon || this.daysUntil(t.paidUntil) <= 15;
const matchLimit = !this.tenantFilters.userLimitFull || t.activeUsers >= t.licenseLimit;
return matchKeyword && matchStatus && matchPlan && matchExpiring && matchLimit;
});
},
get filteredAuditLogs() {
const op = this.auditFilters.operator.trim().toLowerCase();
const tg = this.auditFilters.target.trim().toLowerCase();
return this.auditLogs.filter(l => {
const matchOp = !op || l.operator.toLowerCase().includes(op);
const matchTarget = !tg || l.target.toLowerCase().includes(tg);
const matchAction = !this.auditFilters.action || l.action === this.auditFilters.action;
const matchResult = !this.auditFilters.result || l.result === this.auditFilters.result;
return matchOp && matchTarget && matchAction && matchResult;
});
},
daysUntil(dateText) {
const now = new Date('2026-05-02T00:00:00');
const d = new Date(dateText + 'T00:00:00');
return Math.ceil((d - now) / (1000 * 60 * 60 * 24));
},
resetTenantFilters() {
this.tenantFilters = { keyword: '', status: '', plan: '', expiringSoon: false, userLimitFull: false };
},
resetAuditFilters() {
this.auditFilters = { operator: '', target: '', action: '', result: '', dateRange: '近7天' };
},
openTenantDetail(tenant) {
this.selectedTenant = tenant;
this.tenantDrawerOpen = true;
},
openDangerAction(label, targetObj) {
this.dangerAction = {
label,
targetText: targetObj?.name || targetObj?.id || targetObj?.version || '目标对象'
};
this.dangerReason = '';
this.mfaCode = '';
this.dangerError = '';
this.dangerStep = 1;
this.dangerModalOpen = true;
},
gotoMfaStep() {
if (!this.dangerReason) {
this.dangerError = '请先填写操作原因';
return;
}
this.dangerError = '';
this.dangerStep = 2;
},
confirmDangerAction() {
if (!/^\d{6}$/.test(this.mfaCode)) {
this.dangerError = 'MFA 验证失败,请输入 6 位动态码';
return;
}
this.notify(`${this.dangerAction.label} 已提交并记录审计日志(原型)`);
this.closeDangerModal();
},
closeDangerModal() {
this.dangerModalOpen = false;
this.dangerStep = 1;
this.dangerReason = '';
this.mfaCode = '';
this.dangerError = '';
},
closeAllOverlays() {
this.tenantDrawerOpen = false;
this.closeDangerModal();
},
notify(msg) {
this.toastMessage = msg;
this.toastOpen = true;
if (this.toastTimer) clearTimeout(this.toastTimer);
this.toastTimer = setTimeout(() => {
this.toastOpen = false;
}, 2200);
}
}
}
</script>
</body>
</html>