Files
sun 7b8f557f54 feat(router): v0.3.0 阶段二 管理入口迁移与前端同步
管理入口从 /admin 迁至 /dashboard:
- /dashboard、/dashboard/voices、/dashboard/settings 为新入口
- 原挂在 /dashboard 上的服务状态预览页(health.html)迁至 /dashboard/status,
  侧边栏新增「观测 → 详细状态」入口
- /admin 及子路径 301 永久重定向到对应新路径(页面用 301 是正确的)
- 静态资源同时注册 /dashboard/ 与 /admin/ 两套路径,
  避免过渡期出现"页面能开但丢样式"

前端同步:
- admin-shell.js:侧边栏导航、登出、登录跳转改指 /dashboard/*
- admin.html / admin-login.html / admin-voices.html / admin-settings.html:
  静态资源引用与登录跳转改指 /dashboard/*
- health.html:改用 admin-shell 的 http 实例并改调 /admin/health、/admin/metrics
  (该实例自动附加 Authorization: Bearer;原实现用裸 axios,不带凭证),
  并加登录校验与「返回控制台」入口
- setup.html:安装完成跳转目标改为 /dashboard,页面内 /admin 文案同步更新
- 各页面的 API 调用路径无需修改:axios baseURL 为 /api,
  故 /voices、/admin/overview 等自动落成 /api/admin/*

controller/setup.go:安装成功响应的 redirect 字段由 /admin 改为 /dashboard。

验证:go build / go vet / go test ./... -count=1 全绿;
另用真实服务器端到端验证:旧入口 301 全部正确、静态资源新旧路径均 200、
管理页面内容协商(html 200 / 非 html 401)、登录页公开、
各页面引用的静态资源均为新路径且无残留旧路径。
2026-10-04 01:17:26 +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="/dashboard/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="/dashboard/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 = '/dashboard/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>