Files
Volcano-Engine-TTS-UI/router/admin.html
T
sun 9245c1339a refactor(admin): 把单文件 admin.html (1075 行) 拆成多页 SPA,URL 路由切换
原 admin.html 是 1075 行的单文件 SPA:登录/仪表盘/音色/设置 4 个面板
挤在一个 Vue 实例里 tab 切换(login -> app shell -> 3 个 section)。
文件太大,样式 + script + template 混在一起,改一个面板要 scroll 整页找位置。

拆分后:
  router/admin.html         5.4 KB  Dashboard (重定向目标,默认页)
  router/admin-login.html   2.1 KB  登录页 (无 sidebar)
  router/admin-voices.html  13.3 KB 音色管理 + Add modal
  router/admin-settings.html 14.1 KB 系统设置
  router/admin.css          19.8 KB 共享样式 (4 个页面 + admin-shell.js 共用)
  router/admin-shell.js     6.9 KB  共享:http client (401 自动清 key)、toast 系统、
                                login helper、sidebar 渲染、format helpers

最大的页面 14 KB,平均 10 KB,各页只负责自己逻辑。

技术细节:
- 栈保持: Vue 3 + Axios 走 CDN,纯静态 HTML,零构建
- URL 路由: /admin /admin/login /admin/voices /admin/settings,
  router.go 加 4 条 ServeFile 路由 + 2 条静态资源路由
  (/admin/admin.css /admin/admin-shell.js)
- 抽 serveAdmin(body []byte) http.HandlerFunc helper 避免 4 段重复
- 鉴权: 各页 onMounted 检 sessionStorage.ttsAdminKey,无则 location.href
  跳 /admin/login;http client 401 响应拦截器清 key,下个请求被跳到 login
- Sidebar 一致: shell.js mountShell({activeNav, overview, voiceCount, ...})
  渲染 <aside> + 高亮 active 项,各页只传 activeNav 不重复维护 DOM
- 静态资源 Content-Type 显式设置:text/css / application/javascript

未变更:
- 不动 /api/* 后端接口
- 不动 /setup /dashboard /health /v1/audio/speech 路由
- 鉴权语义: HTML 公开 / API 鉴权 (与原版一致)
- 不引入构建工具 / 不引入新依赖

验证 (PowerShell 起真 server + setup API + curl):
  /admin                          200  5433 bytes  text/html
  /admin/login                    200  2082 bytes  text/html
  /admin/voices                   200 13282 bytes  text/html
  /admin/settings                 200 14110 bytes  text/html
  /admin/admin.css                200 19815 bytes  text/css
  /admin/admin-shell.js           200  6887 bytes  application/javascript
  /api/admin/overview (无 auth)   401
  /api/admin/overview (有 auth)   200
  各页内容 marker 命中 (filteredVoices / corsForm / voice_count / login-card)
2026-09-21 23:20:59 +08:00

107 lines
5.3 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>火山 TTS · 仪表盘</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.6.7/axios.min.js"></script>
<link rel="stylesheet" href="/admin/admin.css">
</head>
<body>
<div id="app">
<div class="app-shell">
<aside id="admin-sidebar"></aside>
<main class="main">
<header class="page-head">
<div>
<h1 class="page-title">仪表盘</h1>
<p class="page-sub">运行状态、版本与资源总览</p>
</div>
</header>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-top">
<span class="stat-label">运行模式</span>
<span class="pill" :class="overview.installed ? 'pill-success' : 'pill-accent'">
<span class="pill-dot"></span>
{{ overview.installed ? '已就绪' : '待配置' }}
</span>
</div>
<div class="stat-value" :class="overview.installed ? 'text-success' : 'text-accent'">{{ overview.mode || '—' }}</div>
</div>
<div class="stat-card">
<div class="stat-top"><span class="stat-label">音色</span></div>
<div class="stat-value">{{ overview.voice_count ?? 0 }}<span class="stat-unit">条</span></div>
<div class="stat-foot">
<span class="text-success">{{ overview.voice_enabled_count ?? 0 }}</span> 启用 ·
<span class="text-dim">{{ (overview.voice_count ?? 0) - (overview.voice_enabled_count ?? 0) }}</span> 停用
</div>
</div>
<div class="stat-card">
<div class="stat-top"><span class="stat-label">运行时间</span></div>
<div class="stat-value">{{ formatUptime(overview.uptime_seconds) }}</div>
<div class="stat-foot text-dim">{{ formatStart(overview.start_time) }}</div>
</div>
<div class="stat-card">
<div class="stat-top"><span class="stat-label">数据库</span></div>
<div class="stat-value mono" style="font-size: 15px; word-break: break-all;">{{ shortPath(overview.db_path) || '—' }}</div>
<div class="stat-foot text-dim mono" style="font-size: 10px; word-break: break-all;">{{ overview.db_path || '' }}</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div>
<div class="card-title">系统信息</div>
<div class="card-sub">运行版本与资源</div>
</div>
</div>
<dl class="kv">
<div class="kv-row"><dt>Commit</dt><dd class="mono">{{ overview.commit || 'dev' }}</dd></div>
<div class="kv-row"><dt>版本</dt><dd class="mono">{{ overview.version || '—' }}</dd></div>
<div class="kv-row"><dt>运行模式</dt><dd>{{ overview.mode || '—' }}</dd></div>
<div class="kv-row"><dt>数据库路径</dt><dd class="mono text-dim">{{ overview.db_path || '—' }}</dd></div>
<div class="kv-row"><dt>锁文件</dt><dd class="mono text-dim">{{ overview.lock_path || '—' }}</dd></div>
<div class="kv-row">
<dt>内存</dt>
<dd class="mono text-dim">
goroutines {{ overview.memory?.goroutines ?? '—' }} · heap {{ formatBytes(overview.memory?.heap_alloc) }}
</dd>
</div>
</dl>
</div>
</main>
</div>
</div>
<script src="/admin/admin-shell.js"></script>
<script>
const { createApp, ref, onMounted } = Vue;
const { http, getKey, formatUptime, shortPath, formatBytes, formatStart, mountShell } = window.admin;
createApp({
setup() {
const overview = ref({});
const loadOverview = async () => {
try { overview.value = (await http.get('/admin/overview')).data; }
catch (e) {
window.admin.toast('err', '加载概览失败: ' + e.message);
}
};
onMounted(() => {
if (!getKey()) { location.href = '/admin/login'; return; }
mountShell({ activeNav: 'dashboard', overview: overview.value, voiceCount: overview.value.voice_count, onReload: loadOverview });
loadOverview();
});
return { overview, formatUptime, shortPath, formatBytes, formatStart, loadOverview };
},
}).mount('#app');
</script>
</body>
</html>