feat(router): v0.2.4 阶段二 管理入口迁移与前端同步
管理入口从 /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)、登录页公开、 各页面引用的静态资源均为新路径且无残留旧路径。
This commit is contained in:
+1
-1
@@ -244,7 +244,7 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"message": "installed",
|
||||
"redirect": "/admin",
|
||||
"redirect": "/dashboard", // v0.3.0: 管理入口由 /admin 迁至 /dashboard
|
||||
"settings": len(settingsKV),
|
||||
"voices": inserted,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>管理后台 · 登录</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">
|
||||
<link rel="stylesheet" href="/dashboard/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/admin-shell.js"></script>
|
||||
<script src="/dashboard/admin-shell.js"></script>
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
const { http, setKey, clearKey, getKey } = window.admin;
|
||||
@@ -34,7 +34,7 @@
|
||||
// 已登录则直接跳 dashboard
|
||||
if (getKey()) {
|
||||
http.get('/admin/overview').then(() => {
|
||||
location.href = '/admin';
|
||||
location.href = '/dashboard';
|
||||
}).catch(() => { clearKey(); });
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
loginErr.value = '';
|
||||
try {
|
||||
await window.admin.login(keyInput.value);
|
||||
location.href = '/admin';
|
||||
location.href = '/dashboard';
|
||||
} catch (e) {
|
||||
loginErr.value = '鉴权失败: ' + (e.response?.data?.error?.message || e.message);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="/dashboard/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<div class="card-title">鉴权 Key</div>
|
||||
<div class="card-sub">客户端调用 <code>/v1/audio/speech</code> 与登录 /admin 共用</div>
|
||||
<div class="card-sub">客户端调用 <code>/v1/audio/speech</code>。管理后台登录改用 <code>admin_key</code>(未配置时回退用本 Key)</div>
|
||||
</div>
|
||||
<span class="pill pill-accent">敏感</span>
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/admin-shell.js"></script>
|
||||
<script src="/dashboard/admin-shell.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
const { http, getKey, setKey, clearKey, toast, mountShell } = window.admin;
|
||||
@@ -236,7 +236,7 @@
|
||||
authKeyInput.value = '';
|
||||
setTimeout(() => {
|
||||
clearKey();
|
||||
location.href = '/admin/login';
|
||||
location.href = '/dashboard/login';
|
||||
}, 200);
|
||||
toast('ok', 'Auth Key 已更新,请用新 Key 重新登录');
|
||||
} catch (e) {
|
||||
@@ -246,7 +246,7 @@
|
||||
const resetSettingsForm = () => { loadSettings(); };
|
||||
|
||||
onMounted(() => {
|
||||
if (!getKey()) { location.href = '/admin/login'; return; }
|
||||
if (!getKey()) { location.href = '/dashboard/login'; return; }
|
||||
mountShell({
|
||||
activeNav: 'settings',
|
||||
overview: overview.value,
|
||||
|
||||
+10
-4
@@ -112,21 +112,27 @@
|
||||
<nav class="nav">
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">概览</div>
|
||||
<a class="nav-item${activeNav === 'dashboard' ? ' active' : ''}" href="/admin">
|
||||
<a class="nav-item${activeNav === 'dashboard' ? ' active' : ''}" href="/dashboard">
|
||||
<span class="nav-icon">▦</span><span>仪表盘</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">管理</div>
|
||||
<a class="nav-item${activeNav === 'voices' ? ' active' : ''}" href="/admin/voices">
|
||||
<a class="nav-item${activeNav === 'voices' ? ' active' : ''}" href="/dashboard/voices">
|
||||
<span class="nav-icon">♪</span><span>音色</span>
|
||||
<span class="nav-badge">${voiceCount}</span>
|
||||
</a>
|
||||
<a class="nav-item${activeNav === 'settings' ? ' active' : ''}" href="/admin/settings">
|
||||
<a class="nav-item${activeNav === 'settings' ? ' active' : ''}" href="/dashboard/settings">
|
||||
<span class="nav-icon">⚙</span><span>设置</span>
|
||||
${corsConfigured ? '' : '<span class="nav-dot" title="CORS 未配置"></span>'}
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">观测</div>
|
||||
<a class="nav-item${activeNav === 'status' ? ' active' : ''}" href="/dashboard/status">
|
||||
<span class="nav-icon">♥</span><span>详细状态</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-foot">
|
||||
<button class="btn-icon" id="btn-reload" title="刷新">↻ 刷新</button>
|
||||
@@ -142,7 +148,7 @@
|
||||
if (btnLogout) btnLogout.addEventListener('click', () => {
|
||||
clearKey();
|
||||
if (typeof options.onLogout === 'function') options.onLogout();
|
||||
else location.href = '/admin/login';
|
||||
else location.href = '/dashboard/login';
|
||||
});
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="/dashboard/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -159,7 +159,7 @@
|
||||
</teleport>
|
||||
</div>
|
||||
|
||||
<script src="/admin/admin-shell.js"></script>
|
||||
<script src="/dashboard/admin-shell.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
const { http, getKey, toast, mountShell } = window.admin;
|
||||
@@ -237,7 +237,7 @@
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!getKey()) { location.href = '/admin/login'; return; }
|
||||
if (!getKey()) { location.href = '/dashboard/login'; return; }
|
||||
mountShell({
|
||||
activeNav: 'voices',
|
||||
overview: overview.value,
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="/dashboard/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/admin-shell.js"></script>
|
||||
<script src="/dashboard/admin-shell.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, onMounted } = Vue;
|
||||
const { http, getKey, formatUptime, shortPath, formatBytes, formatStart, mountShell } = window.admin;
|
||||
@@ -93,7 +93,7 @@
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!getKey()) { location.href = '/admin/login'; return; }
|
||||
if (!getKey()) { location.href = '/dashboard/login'; return; }
|
||||
mountShell({ activeNav: 'dashboard', overview: overview.value, voiceCount: overview.value.voice_count, onReload: loadOverview });
|
||||
loadOverview();
|
||||
});
|
||||
|
||||
+10
-3
@@ -6,6 +6,7 @@
|
||||
<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">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f1a;
|
||||
@@ -148,6 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-flex">
|
||||
<a class="btn" href="/dashboard" style="text-decoration:none;">← 返回控制台</a>
|
||||
<span class="status-badge" :class="health.status === 'ok' ? 'ok' : 'error'">
|
||||
<span class="status-dot" :class="{ live: health.status === 'ok' }"></span>
|
||||
{{ health.status === 'ok' ? '运行中' : (health.status || '加载中') }}
|
||||
@@ -377,8 +379,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/dashboard/admin-shell.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
// v0.3.0:/health 与 /metrics 已收口到鉴权接口,统一走带凭证头的 /api/admin/*
|
||||
const { http, getKey } = window.admin;
|
||||
|
||||
function parseProm(text) {
|
||||
const out = {};
|
||||
@@ -597,16 +602,16 @@
|
||||
|
||||
const fetchHealth = async () => {
|
||||
try {
|
||||
const r = await axios.get("/health");
|
||||
const r = await http.get("/admin/health");
|
||||
health.value = r.data;
|
||||
} catch (e) {
|
||||
error.value = "无法获取健康状态: " + (e.message || "未知错误");
|
||||
error.value = "无法获取健康状态: " + (e.response?.data?.error?.message || e.message || "未知错误");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
try {
|
||||
const r = await axios.get("/metrics");
|
||||
const r = await http.get("/admin/metrics");
|
||||
metrics.value = parseProm(r.data);
|
||||
} catch (e) {
|
||||
// 不覆盖 error,让 health 错误更显眼
|
||||
@@ -650,6 +655,8 @@
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// v0.3.0:本页已收口到管理鉴权,未登录先回登录页
|
||||
if (!getKey()) { location.href = '/dashboard/login'; return; }
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
});
|
||||
|
||||
+5
-5
@@ -202,7 +202,7 @@
|
||||
<div class="field">
|
||||
<label>OpenAI 鉴权 Key <span class="req">*</span></label>
|
||||
<input type="password" v-model="form.auth_key" placeholder="自定,例如 my-secret-key-2024" autocomplete="off">
|
||||
<div class="hint">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。装完可在 <code>/admin → 设置</code> 改。</div>
|
||||
<div class="hint">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/dashboard</code> 的密码。装完可在 <code>/dashboard → 设置</code> 改(可另配独立 <code>admin_key</code> 用于后台登录,避免与业务 Key 共用)。</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>火山 TTS API Key <span class="req">*</span></label>
|
||||
@@ -216,7 +216,7 @@
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<div class="card-title">默认路由与格式</div>
|
||||
<div class="card-sub">未传 voice/format 时使用。装完可在 <code>/admin → 设置</code> 改。</div>
|
||||
<div class="card-sub">未传 voice/format 时使用。装完可在 <code>/dashboard → 设置</code> 改。</div>
|
||||
</div>
|
||||
<span class="step-badge">第 2 步</span>
|
||||
</div>
|
||||
@@ -261,7 +261,7 @@
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<div class="card-title">音色列表 <span style="font-weight: 400; color: var(--text-dim); font-size: 12px;">(至少 1 条)</span></div>
|
||||
<div class="card-sub">填入火山 TTS 音色 ID(S_ 开头的为复刻音色)。可后续在 /admin 中增删。</div>
|
||||
<div class="card-sub">填入火山 TTS 音色 ID(S_ 开头的为复刻音色)。可后续在 /dashboard 中增删。</div>
|
||||
</div>
|
||||
<span class="step-badge">第 3 步</span>
|
||||
</div>
|
||||
@@ -322,7 +322,7 @@
|
||||
<!-- Nav -->
|
||||
<div class="actions">
|
||||
<button class="btn" @click="prev" :disabled="step===1 || submitting">← 上一步</button>
|
||||
<div class="hint-text">提交后立即进入正常模式;后续管理请访问 <code>/admin</code>。</div>
|
||||
<div class="hint-text">提交后立即进入正常模式;后续管理请访问 <code>/dashboard</code>。</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button v-if="step < 4" class="btn btn-primary" @click="next">下一步 →</button>
|
||||
<button v-else class="btn btn-primary" @click="submit" :disabled="submitting">
|
||||
@@ -412,7 +412,7 @@
|
||||
},
|
||||
voices: form.value.voices,
|
||||
});
|
||||
window.location.href = r.data.redirect || '/admin';
|
||||
window.location.href = r.data.redirect || '/dashboard';
|
||||
} catch (e) {
|
||||
const msg = e.response?.data?.error?.message || e.message;
|
||||
errorMsg.value = '安装失败: ' + msg;
|
||||
|
||||
Reference in New Issue
Block a user