2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
409 lines
17 KiB
HTML
409 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>运营后台 · 简洁版预览</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
--bg: #0f1419;
|
|
--surface: #151b24;
|
|
--line: #252f3d;
|
|
--text: #e8edf4;
|
|
--muted: #8b9cb3;
|
|
--faint: #5c6d82;
|
|
--accent: #4d90ff;
|
|
--ok: #45c88a;
|
|
--warn: #e8b339;
|
|
--sidebar: 200px;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); }
|
|
|
|
.draft-tag {
|
|
position: fixed; top: 12px; right: 16px; z-index: 99;
|
|
font-size: 11px; color: var(--faint);
|
|
}
|
|
|
|
.layout { display: flex; min-height: 100%; }
|
|
|
|
/* sidebar — 5 items only */
|
|
.nav {
|
|
width: var(--sidebar);
|
|
padding: 24px 12px;
|
|
border-right: 1px solid var(--line);
|
|
display: flex; flex-direction: column; gap: 4px;
|
|
position: fixed; inset: 0 auto 0 0;
|
|
}
|
|
.nav-brand {
|
|
padding: 0 12px 28px;
|
|
font-size: 15px; font-weight: 600; letter-spacing: -.02em;
|
|
}
|
|
.nav-brand span { display: block; font-size: 11px; font-weight: 400; color: var(--muted); margin-top: 4px; }
|
|
.nav-btn {
|
|
display: block; width: 100%;
|
|
padding: 10px 12px; border: none; border-radius: 8px;
|
|
background: none; color: var(--muted);
|
|
font-size: 14px; text-align: left; cursor: pointer;
|
|
}
|
|
.nav-btn:hover { color: var(--text); background: rgba(255,255,255,.04); }
|
|
.nav-btn.active { color: var(--text); background: rgba(77,144,255,.12); }
|
|
.nav-foot {
|
|
margin-top: auto; padding: 16px 12px 0;
|
|
font-size: 11px; color: var(--faint);
|
|
}
|
|
|
|
/* main */
|
|
.main {
|
|
flex: 1; margin-left: var(--sidebar);
|
|
display: flex; flex-direction: column; min-width: 0;
|
|
}
|
|
.header {
|
|
height: 56px; padding: 0 32px;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.header h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
.header-actions { display: flex; align-items: center; gap: 12px; }
|
|
.header-actions a {
|
|
font-size: 13px; color: var(--muted); text-decoration: none;
|
|
}
|
|
.header-actions a:hover { color: var(--text); }
|
|
.user { font-size: 13px; color: var(--muted); }
|
|
|
|
.body { padding: 28px 32px 48px; flex: 1; }
|
|
|
|
.page { display: none; max-width: 1080px; }
|
|
.page.active { display: block; }
|
|
|
|
/* stats */
|
|
.metrics {
|
|
display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
|
|
margin-bottom: 32px;
|
|
}
|
|
.metric {
|
|
padding: 16px 0;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.metric-label { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
|
|
.metric-value { font-size: 28px; font-weight: 600; letter-spacing: -.03em; }
|
|
.metric-sub { font-size: 11px; color: var(--faint); margin-top: 6px; }
|
|
|
|
/* inline tabs — only inside page when needed */
|
|
.tabs {
|
|
display: flex; gap: 20px; margin-bottom: 24px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.tab {
|
|
padding: 0 0 10px; margin-bottom: -1px;
|
|
border: none; background: none;
|
|
font-size: 13px; color: var(--muted); cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
.tab-panel { display: none; }
|
|
.tab-panel.active { display: block; }
|
|
|
|
/* toolbar */
|
|
.toolbar {
|
|
display: flex; align-items: center; gap: 12px;
|
|
margin-bottom: 16px; flex-wrap: wrap;
|
|
}
|
|
.toolbar input, .toolbar select {
|
|
padding: 8px 10px; border-radius: 6px;
|
|
border: 1px solid var(--line); background: var(--surface);
|
|
color: var(--text); font-size: 13px;
|
|
}
|
|
.toolbar .grow { flex: 1; min-width: 120px; }
|
|
.btn {
|
|
padding: 8px 14px; border-radius: 6px; font-size: 13px;
|
|
border: 1px solid var(--line); background: var(--surface);
|
|
color: var(--text); cursor: pointer;
|
|
}
|
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
.btn-ghost { background: transparent; }
|
|
|
|
/* table — minimal */
|
|
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.table th {
|
|
text-align: left; padding: 10px 0;
|
|
color: var(--muted); font-weight: 500; font-size: 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.table td {
|
|
padding: 14px 0; border-bottom: 1px solid var(--line);
|
|
}
|
|
.table tr:last-child td { border-bottom: none; }
|
|
.dim { color: var(--muted); font-size: 12px; }
|
|
.tag {
|
|
display: inline-block; padding: 2px 7px; border-radius: 4px;
|
|
font-size: 11px; background: rgba(255,255,255,.06); color: var(--muted);
|
|
}
|
|
.tag.ok { color: var(--ok); background: rgba(69,200,138,.1); }
|
|
.tag.off { color: #f08080; background: rgba(240,128,128,.1); }
|
|
|
|
/* form block */
|
|
.form {
|
|
max-width: 420px;
|
|
display: grid; gap: 14px;
|
|
}
|
|
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
.field input, .field select, .field textarea {
|
|
width: 100%; padding: 9px 10px; border-radius: 6px;
|
|
border: 1px solid var(--line); background: var(--surface);
|
|
color: var(--text); font-size: 13px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 13px; color: var(--muted); margin: 32px 0 12px;
|
|
}
|
|
|
|
.switch-grid {
|
|
display: grid; gap: 0;
|
|
max-width: 480px;
|
|
}
|
|
.switch-row {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 12px 0; border-bottom: 1px solid var(--line);
|
|
font-size: 13px;
|
|
}
|
|
.switch-row:last-child { border-bottom: none; }
|
|
.switch {
|
|
width: 32px; height: 18px; border-radius: 999px;
|
|
background: var(--accent); position: relative; flex-shrink: 0;
|
|
}
|
|
.switch::after {
|
|
content: ''; position: absolute; top: 2px; right: 2px;
|
|
width: 14px; height: 14px; border-radius: 50%; background: #fff;
|
|
}
|
|
.switch.off { background: #3a4555; }
|
|
.switch.off::after { right: auto; left: 2px; }
|
|
|
|
.note {
|
|
margin-top: 40px; padding-top: 20px;
|
|
border-top: 1px solid var(--line);
|
|
font-size: 12px; color: var(--faint); line-height: 1.7; max-width: 560px;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.metrics { grid-template-columns: repeat(2, 1fr); }
|
|
.nav { width: 64px; padding: 16px 8px; }
|
|
.nav-brand span, .nav-btn { font-size: 0; }
|
|
.nav-brand { font-size: 12px; padding-bottom: 16px; }
|
|
.nav-btn { text-align: center; padding: 10px 8px; }
|
|
.nav-foot { display: none; }
|
|
.main { margin-left: 64px; }
|
|
.body { padding: 20px 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="draft-tag">设计预览 · 简洁版</div>
|
|
|
|
<div class="layout">
|
|
<nav class="nav">
|
|
<div class="nav-brand">Ops<span>运营后台</span></div>
|
|
<button class="nav-btn active" data-page="home">概览</button>
|
|
<button class="nav-btn" data-page="users">用户</button>
|
|
<button class="nav-btn" data-page="billing">计费</button>
|
|
<button class="nav-btn" data-page="config">配置</button>
|
|
<button class="nav-btn" data-page="logs">日志</button>
|
|
<div class="nav-foot">admin</div>
|
|
</nav>
|
|
|
|
<div class="main">
|
|
<header class="header">
|
|
<h1 id="pageTitle">概览</h1>
|
|
<div class="header-actions">
|
|
<a href="#">返回用户端</a>
|
|
<span class="user">admin</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="body">
|
|
<!-- 概览:只有数字 + 一张表 -->
|
|
<section class="page active" id="page-home">
|
|
<div class="metrics">
|
|
<div class="metric"><div class="metric-label">用户</div><div class="metric-value">128</div><div class="metric-sub">活跃 7 日</div></div>
|
|
<div class="metric"><div class="metric-label">今日 Token</div><div class="metric-value">2.4M</div></div>
|
|
<div class="metric"><div class="metric-label">今日扣费</div><div class="metric-value">¥386</div></div>
|
|
<div class="metric"><div class="metric-label">余额不足</div><div class="metric-value">7</div><div class="metric-sub">需充值</div></div>
|
|
</div>
|
|
|
|
<div class="section-title">最近动态</div>
|
|
<table class="table">
|
|
<thead><tr><th>时间</th><th>事件</th><th>用户</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="dim">10:22</td><td>Token 扣费 ¥0.42</td><td>@john</td></tr>
|
|
<tr><td class="dim">09:55</td><td>充值 +¥50</td><td>@john</td></tr>
|
|
<tr><td class="dim">09:40</td><td>账户禁用</td><td>@demo</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<!-- 用户:一表 + 新建按钮 -->
|
|
<section class="page" id="page-users">
|
|
<div class="toolbar">
|
|
<input placeholder="搜索用户" style="width:200px" />
|
|
<select><option>全部状态</option></select>
|
|
<span class="grow"></span>
|
|
<button class="btn btn-primary" id="showCreate">新建用户</button>
|
|
</div>
|
|
<table class="table">
|
|
<thead><tr><th>用户</th><th>状态</th><th>余额</th><th></th></tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><div>John</div><div class="dim">@john</div></td>
|
|
<td><span class="tag ok">正常</span></td>
|
|
<td>¥128.50</td>
|
|
<td><button class="btn btn-ghost" style="padding:4px 10px">编辑</button></td>
|
|
</tr>
|
|
<tr>
|
|
<td><div>演示</div><div class="dim">@demo</div></td>
|
|
<td><span class="tag off">禁用</span></td>
|
|
<td>¥0.30</td>
|
|
<td><button class="btn btn-ghost" style="padding:4px 10px">编辑</button></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div id="createForm" style="display:none;margin-top:28px">
|
|
<div class="section-title">新建用户</div>
|
|
<form class="form">
|
|
<div class="field"><label>用户名</label><input /></div>
|
|
<div class="field"><label>密码</label><input type="password" /></div>
|
|
<div class="field"><label>初始余额(元)</label><input value="10" /></div>
|
|
<button type="button" class="btn btn-primary" style="width:fit-content">创建</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 计费:三个 Tab,无多余面板 -->
|
|
<section class="page" id="page-billing">
|
|
<div class="tabs" data-tabs="billing">
|
|
<button class="tab active" data-tab="recharge">充值</button>
|
|
<button class="tab" data-tab="usage">用量</button>
|
|
<button class="tab" data-tab="ledger">流水</button>
|
|
</div>
|
|
<div class="tab-panel active" data-panel="recharge">
|
|
<form class="form">
|
|
<div class="field"><label>用户</label><select><option>@john</option><option>@demo</option></select></div>
|
|
<div class="field"><label>金额(元)</label><input value="50" /></div>
|
|
<div class="field"><label>备注</label><input placeholder="可选" /></div>
|
|
<button type="button" class="btn btn-primary" style="width:fit-content">充值</button>
|
|
</form>
|
|
</div>
|
|
<div class="tab-panel" data-panel="usage">
|
|
<table class="table">
|
|
<thead><tr><th>时间</th><th>用户</th><th>Token</th><th>扣费</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="dim">10:22</td><td>@john</td><td>1200 / 480</td><td>¥0.42</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="tab-panel" data-panel="ledger">
|
|
<table class="table">
|
|
<thead><tr><th>时间</th><th>用户</th><th>类型</th><th>金额</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="dim">09:55</td><td>@john</td><td>充值</td><td>+¥50</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 配置:四个 Tab 合并原 4 个 Settings 区块 -->
|
|
<section class="page" id="page-config">
|
|
<div class="tabs" data-tabs="config">
|
|
<button class="tab active" data-tab="llm">LLM</button>
|
|
<button class="tab" data-tab="cap">权限</button>
|
|
<button class="tab" data-tab="skill">技能</button>
|
|
<button class="tab" data-tab="policy">策略</button>
|
|
</div>
|
|
<div class="tab-panel active" data-panel="llm">
|
|
<div class="toolbar"><span class="grow"></span><button class="btn">同步 goosed</button><button class="btn btn-primary">添加密钥</button></div>
|
|
<table class="table">
|
|
<thead><tr><th>名称</th><th>Provider</th><th>状态</th><th></th></tr></thead>
|
|
<tbody>
|
|
<tr><td>DeepSeek 生产</td><td class="dim">custom_deepseek</td><td><span class="tag ok">使用中</span></td><td><button class="btn btn-ghost" style="padding:4px 10px">编辑</button></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="tab-panel" data-panel="cap">
|
|
<div class="toolbar"><select><option>角色:user</option><option>角色:admin</option></select></div>
|
|
<div class="switch-grid">
|
|
<div class="switch-row"><span>mindspace.read</span><span class="switch"></span></div>
|
|
<div class="switch-row"><span>mindspace.publish</span><span class="switch"></span></div>
|
|
<div class="switch-row"><span>chat.save_page</span><span class="switch off"></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-panel" data-panel="skill">
|
|
<div class="switch-grid">
|
|
<div class="switch-row"><span>static-page-publish</span><span class="switch"></span></div>
|
|
<div class="switch-row"><span>web-research</span><span class="switch off"></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-panel" data-panel="policy">
|
|
<div class="switch-grid">
|
|
<div class="switch-row"><span>daily_token_cap</span><span class="switch"></span></div>
|
|
<div class="switch-row"><span>require_scan</span><span class="switch"></span></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 日志:一张表 -->
|
|
<section class="page" id="page-logs">
|
|
<div class="toolbar">
|
|
<select><option>全部</option><option>充值</option><option>用户</option></select>
|
|
<input type="date" />
|
|
</div>
|
|
<table class="table">
|
|
<thead><tr><th>时间</th><th>操作</th><th>目标</th><th>详情</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="dim">10:05</td><td>充值</td><td>@john</td><td>+¥50</td></tr>
|
|
<tr><td class="dim">09:40</td><td>禁用</td><td>@demo</td><td>—</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<p class="note">
|
|
简洁原则:侧栏 5 项 · 无二级菜单 · 无面包屑/通知/角标 · 页内 Tab 仅用于「计费」「配置」<br>
|
|
现有 Admin API 全部保留,只是 UI 合并。内容审核、实时监控等 Phase 2 再加,不预先占菜单。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const titles = { home: '概览', users: '用户', billing: '计费', config: '配置', logs: '日志' };
|
|
|
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('page-' + btn.dataset.page).classList.add('active');
|
|
document.getElementById('pageTitle').textContent = titles[btn.dataset.page];
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-tabs]').forEach(group => {
|
|
group.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const section = group.closest('.page');
|
|
group.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
section.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
section.querySelector('[data-panel="' + tab.dataset.tab + '"]').classList.add('active');
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById('showCreate')?.addEventListener('click', () => {
|
|
const el = document.getElementById('createForm');
|
|
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|