1076 lines
61 KiB
HTML
1076 lines
61 KiB
HTML
<!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>
|