Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.

Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-15 15:04:43 -07:00
commit 2e14873f2d
272 changed files with 64133 additions and 0 deletions
+506
View File
@@ -0,0 +1,506 @@
<!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 {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color-scheme: dark;
--bg: #0f1419;
--surface: #121820;
--elevated: #1c2735;
--border: #243041;
--text: #e7ecf3;
--muted: #8fa3bf;
--faint: #5f738d;
--accent: #3d8bfd;
--remain: #3dd6c5;
--remain-glow: rgba(61, 214, 197, 0.55);
--spent: #e8a44a;
--spent-glow: rgba(232, 164, 74, 0.45);
--warn: #ddb945;
--danger: #ef7b7b;
--danger-glow: rgba(239, 123, 123, 0.5);
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--text); }
body { padding: 24px; line-height: 1.5; }
h1 { font-size: 20px; margin: 0 0 6px; }
.lead { color: var(--muted); font-size: 14px; margin: 0 0 24px; max-width: 720px; }
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.panel h2 { font-size: 15px; margin: 0 0 4px; }
.panel p.desc { font-size: 13px; color: var(--muted); margin: 0 0 16px; }
.controls {
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
margin-bottom: 20px; padding: 14px 16px;
background: var(--elevated); border-radius: 10px;
}
.controls label { font-size: 13px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
.controls input[type="range"] { width: 160px; }
.controls output { font-size: 13px; min-width: 72px; }
.state-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.state-btns button {
padding: 6px 12px; border-radius: 8px; border: 1px solid var(--border);
background: var(--bg); color: var(--muted); font-size: 12px; cursor: pointer;
}
.state-btns button.active { border-color: var(--accent); color: var(--text); background: #152033; }
/* ===== 模拟 Chat Header ===== */
.mock-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; border-bottom: 1px solid var(--border);
background: var(--bg); border-radius: 10px 10px 0 0;
}
.mock-header-left { display: flex; align-items: center; gap: 10px; }
.avatar { width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, #2458a6, #3d8bfd); }
.mock-title { font-weight: 700; font-size: 18px; }
.mock-sub { font-size: 12px; color: var(--muted); }
.mock-actions { display: flex; align-items: center; gap: 8px; }
.ghost-btn {
border: none; border-radius: 10px; padding: 10px 14px; font-size: 14px;
background: var(--elevated); color: #c9d7ea; cursor: pointer;
}
/* 当前方案 */
.balance-pill-current {
padding: 6px 10px; border-radius: 999px;
background: var(--elevated); color: #9ec5ff; font-size: 12px;
border: 1px solid transparent; cursor: pointer;
}
/* ===== 方案 A:纯圆环灯管(推荐) ===== */
.balance-ring-btn {
position: relative;
width: 40px; height: 40px;
border: none; background: transparent; cursor: pointer;
padding: 0; border-radius: 50%;
flex-shrink: 0;
}
.balance-ring-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.balance-ring-svg {
width: 40px; height: 40px;
transform: rotate(-90deg);
filter: drop-shadow(0 0 4px var(--ring-glow, var(--remain-glow)));
}
.balance-ring-track {
fill: none;
stroke: #1a2838;
stroke-width: 4;
}
.balance-ring-spent {
fill: none;
stroke: var(--spent);
stroke-width: 4;
stroke-linecap: round;
transition: stroke-dashoffset 0.35s ease;
filter: drop-shadow(0 0 3px var(--spent-glow));
}
.balance-ring-remain {
fill: none;
stroke: var(--ring-color, var(--remain));
stroke-width: 4;
stroke-linecap: round;
transition: stroke-dashoffset 0.35s ease, stroke 0.2s;
filter: drop-shadow(0 0 4px var(--ring-glow, var(--remain-glow)));
}
.balance-ring-btn.low .balance-ring-remain {
--ring-color: var(--warn);
--ring-glow: rgba(221, 185, 69, 0.5);
}
.balance-ring-btn.empty .balance-ring-remain {
--ring-color: var(--danger);
--ring-glow: var(--danger-glow);
animation: pulse-empty 1.8s ease-in-out infinite;
}
@keyframes pulse-empty {
0%, 100% { opacity: 1; filter: drop-shadow(0 0 4px var(--danger-glow)); }
50% { opacity: 0.55; filter: drop-shadow(0 0 8px var(--danger-glow)); }
}
.balance-ring-center {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 600; color: var(--muted);
pointer-events: none;
}
.balance-ring-btn.low .balance-ring-center { color: var(--warn); }
.balance-ring-btn.empty .balance-ring-center { color: var(--danger); font-size: 9px; }
/* ===== 方案 B:圆环 + 悬停/点击浮层 ===== */
.balance-popover-wrap { position: relative; }
.balance-popover {
display: none;
position: absolute; top: calc(100% + 10px); right: 0;
width: 220px; padding: 14px;
background: #171f2b; border: 1px solid var(--border);
border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,0.45);
z-index: 10;
}
.balance-popover-wrap:hover .balance-popover,
.balance-popover-wrap.open .balance-popover { display: block; }
.balance-popover h4 { margin: 0 0 10px; font-size: 13px; font-weight: 600; }
.balance-popover-row {
display: flex; justify-content: space-between; align-items: center;
font-size: 12px; margin-bottom: 6px;
}
.balance-popover-row .label { color: var(--muted); display: flex; align-items: center; gap: 6px; }
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot.remain { background: var(--remain); box-shadow: 0 0 6px var(--remain-glow); }
.dot.spent { background: var(--spent); box-shadow: 0 0 6px var(--spent-glow); }
.balance-popover-bar {
height: 6px; border-radius: 999px; background: #1a2838;
overflow: hidden; display: flex; margin: 10px 0 12px;
}
.balance-popover-bar .spent-part { background: var(--spent); }
.balance-popover-bar .remain-part { background: var(--remain); }
.balance-popover-cta {
width: 100%; padding: 8px; border: none; border-radius: 8px;
background: var(--accent); color: #fff; font-size: 13px; cursor: pointer;
}
/* ===== 方案 C:胶囊微环 ===== */
.balance-capsule {
display: flex; align-items: center; gap: 8px;
padding: 4px 10px 4px 4px;
border-radius: 999px; border: 1px solid var(--border);
background: var(--elevated); cursor: pointer;
transition: border-color 0.2s;
}
.balance-capsule:hover { border-color: rgba(61, 214, 197, 0.35); }
.balance-capsule.low:hover { border-color: rgba(221, 185, 69, 0.4); }
.balance-capsule.empty:hover { border-color: rgba(239, 123, 123, 0.4); }
.balance-capsule-ring { width: 28px; height: 28px; flex-shrink: 0; }
.balance-capsule-ring svg { width: 28px; height: 28px; transform: rotate(-90deg); }
.balance-capsule-ring .balance-ring-track { stroke-width: 3; }
.balance-capsule-ring .balance-ring-spent,
.balance-capsule-ring .balance-ring-remain { stroke-width: 3; }
.balance-capsule-text { font-size: 12px; color: var(--muted); white-space: nowrap; }
.balance-capsule-text strong { color: var(--text); font-weight: 600; }
.compare-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
@media (max-width: 800px) { .compare-row { grid-template-columns: 1fr; } }
.legend {
display: flex; gap: 16px; flex-wrap: wrap;
font-size: 12px; color: var(--muted); margin-top: 12px;
}
.legend span { display: flex; align-items: center; gap: 6px; }
.note {
font-size: 12px; color: var(--faint);
border-left: 3px solid var(--border);
padding-left: 12px; margin-top: 16px;
}
.tag {
display: inline-block; padding: 2px 8px; border-radius: 6px;
font-size: 11px; font-weight: 600; margin-left: 8px;
background: rgba(61, 214, 197, 0.12); color: var(--remain);
}
.tag.alt { background: rgba(61, 139, 253, 0.12); color: #9ec5ff; }
.recharge-preview {
margin-top: 12px; padding: 14px; border-radius: 10px;
background: var(--elevated); border: 1px solid var(--border);
}
.recharge-preview h4 { margin: 0 0 10px; font-size: 13px; }
.recharge-ring-large {
display: flex; align-items: center; gap: 20px;
}
.recharge-ring-large svg { width: 88px; height: 88px; transform: rotate(-90deg); }
.recharge-ring-large .balance-ring-track { stroke-width: 6; }
.recharge-ring-large .balance-ring-spent,
.recharge-ring-large .balance-ring-remain { stroke-width: 6; }
.recharge-stats { font-size: 13px; }
.recharge-stats div { margin-bottom: 4px; color: var(--muted); }
.recharge-stats strong { color: var(--text); font-size: 15px; }
</style>
</head>
<body>
<h1>余额展示 · 圆环灯管方案</h1>
<p class="lead">
替代 header 里直白的「余额 ¥12.80」。用圆环灯管表达<strong>累计额度</strong><strong>已消耗</strong>比例,剩余部分用另一色发光段表示。点击/悬停可看具体金额。
</p>
<div class="controls">
<label>
累计充值(总额)
<input type="range" id="totalSlider" min="500" max="20000" step="100" value="5000" />
<output id="totalOut">¥50.00</output>
</label>
<label>
当前余额(剩余)
<input type="range" id="remainSlider" min="0" max="5000" step="10" value="1280" />
<output id="remainOut">¥12.80</output>
</label>
<div class="state-btns">
<button type="button" data-preset="healthy" class="active">健康</button>
<button type="button" data-preset="low">余额低</button>
<button type="button" data-preset="empty">已耗尽</button>
</div>
</div>
<!-- 当前 vs 推荐 -->
<div class="panel">
<h2>对比:当前 vs 方案 A(推荐)</h2>
<p class="desc">方案 A 仅显示圆环 + 剩余百分比,不直接暴露金额,适合 header 紧凑场景。</p>
<div class="compare-row">
<div>
<div class="mock-header" style="border-radius:10px">
<div class="mock-header-left">
<div class="avatar"></div>
<div>
<div class="mock-title">John</div>
<div class="mock-sub">新对话</div>
</div>
</div>
<div class="mock-actions">
<span class="balance-pill-current" id="currentPill">余额 ¥12.80</span>
<button class="ghost-btn">我的空间</button>
</div>
</div>
<p class="note">当前:金额完全外露,像账单条目。</p>
</div>
<div>
<div class="mock-header" style="border-radius:10px">
<div class="mock-header-left">
<div class="avatar"></div>
<div>
<div class="mock-title">John</div>
<div class="mock-sub">新对话</div>
</div>
</div>
<div class="mock-actions">
<div class="balance-popover-wrap" id="variantA">
<button type="button" class="balance-ring-btn" aria-label="账户额度">
<svg class="balance-ring-svg" viewBox="0 0 40 40" aria-hidden="true">
<circle class="balance-ring-track" cx="20" cy="20" r="16" />
<circle class="balance-ring-spent" cx="20" cy="20" r="16"
stroke-dasharray="0 100.53" stroke-dashoffset="0" data-role="spent" />
<circle class="balance-ring-remain" cx="20" cy="20" r="16"
stroke-dasharray="0 100.53" stroke-dashoffset="0" data-role="remain" />
</svg>
<span class="balance-ring-center" data-role="pct">26%</span>
</button>
<div class="balance-popover">
<h4>账户额度</h4>
<div class="balance-popover-row">
<span class="label"><span class="dot remain"></span>剩余</span>
<strong data-role="pop-remain">¥12.80</strong>
</div>
<div class="balance-popover-row">
<span class="label"><span class="dot spent"></span>已消耗</span>
<strong data-role="pop-spent">¥37.20</strong>
</div>
<div class="balance-popover-row">
<span class="label" style="color:var(--faint)">累计充值</span>
<span data-role="pop-total">¥50.00</span>
</div>
<div class="balance-popover-bar" data-role="bar"></div>
<button type="button" class="balance-popover-cta">充值</button>
</div>
</div>
<button class="ghost-btn">我的空间</button>
</div>
</div>
<div class="legend">
<span><span class="dot remain"></span>青绿 = 剩余可用</span>
<span><span class="dot spent"></span>琥珀 = 已消耗</span>
</div>
<p class="note">方案 A:圆环总长 = 累计充值;两段颜色从 12 点顺时针排列。中心显示剩余占比。</p>
</div>
</div>
</div>
<!-- 方案 B -->
<div class="panel">
<h2>方案 B:圆环 + 点击固定浮层<span class="tag alt">移动端友好</span></h2>
<p class="desc">与 A 相同圆环,但浮层通过点击打开(避免手机无 hover)。适合需要明确金额时再展开。</p>
<div class="mock-header" style="border-radius:10px; max-width:480px">
<div class="mock-header-left">
<div class="avatar"></div>
<div><div class="mock-title">John</div><div class="mock-sub">新对话</div></div>
</div>
<div class="mock-actions">
<div class="balance-popover-wrap" id="variantB">
<button type="button" class="balance-ring-btn" aria-label="账户额度" data-click-toggle>
<svg class="balance-ring-svg" viewBox="0 0 40 40"><circle class="balance-ring-track" cx="20" cy="20" r="16" /><circle class="balance-ring-spent" cx="20" cy="20" r="16" data-role="spent" /><circle class="balance-ring-remain" cx="20" cy="20" r="16" data-role="remain" /></svg>
<span class="balance-ring-center" data-role="pct">26%</span>
</button>
<div class="balance-popover">
<h4>账户额度</h4>
<div class="balance-popover-row"><span class="label"><span class="dot remain"></span>剩余</span><strong data-role="pop-remain">¥12.80</strong></div>
<div class="balance-popover-row"><span class="label"><span class="dot spent"></span>已消耗</span><strong data-role="pop-spent">¥37.20</strong></div>
<div class="balance-popover-bar" data-role="bar"></div>
<button type="button" class="balance-popover-cta">去充值</button>
</div>
</div>
</div>
</div>
<p class="note">点击圆环切换浮层;低余额时环色变黄,耗尽时红色脉冲。</p>
</div>
<!-- 方案 C -->
<div class="panel">
<h2>方案 C:胶囊微环<span class="tag">折中</span></h2>
<p class="desc">保留简短文案「可用 26%」,圆环缩小嵌在胶囊左侧,比纯数字含蓄但仍有一行提示。</p>
<div class="mock-header" style="border-radius:10px; max-width:520px">
<div class="mock-header-left">
<div class="avatar"></div>
<div><div class="mock-title">John</div><div class="mock-sub">新对话</div></div>
</div>
<div class="mock-actions">
<div class="balance-capsule" id="variantC">
<div class="balance-capsule-ring">
<svg viewBox="0 0 40 40"><circle class="balance-ring-track" cx="20" cy="20" r="16" /><circle class="balance-ring-spent" cx="20" cy="20" r="16" data-role="spent" /><circle class="balance-ring-remain" cx="20" cy="20" r="16" data-role="remain" /></svg>
</div>
<span class="balance-capsule-text">可用 <strong data-role="pct">26%</strong></span>
</div>
</div>
</div>
</div>
<!-- 充值弹窗里的放大版 -->
<div class="panel">
<h2>充值弹窗 · 大号圆环(配套)</h2>
<p class="desc">弹窗内可用 88px 大圆环 + 三项明细,与 header 小环视觉语言一致。</p>
<div class="recharge-preview">
<h4>账户充值</h4>
<div class="recharge-ring-large" id="rechargePreview">
<svg viewBox="0 0 40 40">
<circle class="balance-ring-track" cx="20" cy="20" r="16" />
<circle class="balance-ring-spent" cx="20" cy="20" r="16" data-role="spent" />
<circle class="balance-ring-remain" cx="20" cy="20" r="16" data-role="remain" />
</svg>
<div class="recharge-stats">
<div>剩余 <strong data-role="pop-remain">¥12.80</strong></div>
<div>已消耗 <strong data-role="pop-spent">¥37.20</strong></div>
<div>累计充值 <strong data-role="pop-total">¥50.00</strong></div>
</div>
</div>
</div>
</div>
<!-- 数据与实现说明 -->
<div class="panel">
<h2>数据口径 & 实现要点</h2>
<p class="desc" style="margin-bottom:8px">
<strong>圆环语义</strong>:外圈总长 = 累计充值总额;琥珀段 = 已消耗;青绿段 = 当前余额。<br>
<strong>计算公式</strong>:已消耗 = 累计充值 − 当前余额(需后端返回 <code>totalRechargeCents</code>,或从流水汇总)。<br>
<strong>状态色</strong>:剩余 &gt; ¥1 青绿;≤ ¥1 琥珀警告;≤ ¥0 红色脉冲 + 点击直达充值。<br>
<strong>无障碍</strong><code>aria-label="账户额度,剩余 26%"</code>,浮层内展示具体金额。<br>
<strong>推荐</strong>Header 用 <strong>方案 A</strong>(纯圆环 + hover 浮层);移动端同一组件点击展开(方案 B 行为)。
</p>
</div>
<script>
const CIRC = 2 * Math.PI * 16; // r=16
const fmt = (cents) => '¥' + (cents / 100).toFixed(2);
const totalSlider = document.getElementById('totalSlider');
const remainSlider = document.getElementById('remainSlider');
const totalOut = document.getElementById('totalOut');
const remainOut = document.getElementById('remainOut');
const currentPill = document.getElementById('currentPill');
const variants = [
document.getElementById('variantA'),
document.getElementById('variantB'),
document.getElementById('variantC'),
document.getElementById('rechargePreview'),
].filter(Boolean);
function updateRing(root, total, remain) {
if (!root) return;
const spent = Math.max(0, total - remain);
const pct = total > 0 ? Math.round((remain / total) * 100) : 0;
const spentLen = total > 0 ? (spent / total) * CIRC : 0;
const remainLen = total > 0 ? (remain / total) * CIRC : 0;
root.querySelectorAll('[data-role="spent"]').forEach((el) => {
el.setAttribute('stroke-dasharray', `${spentLen} ${CIRC}`);
el.setAttribute('stroke-dashoffset', '0');
});
root.querySelectorAll('[data-role="remain"]').forEach((el) => {
el.setAttribute('stroke-dasharray', `${remainLen} ${CIRC}`);
el.setAttribute('stroke-dashoffset', `${-spentLen}`);
});
root.querySelectorAll('[data-role="pct"]').forEach((el) => {
el.textContent = total <= 0 ? '—' : (remain <= 0 ? '0%' : pct + '%');
});
root.querySelectorAll('[data-role="pop-remain"]').forEach((el) => { el.textContent = fmt(remain); });
root.querySelectorAll('[data-role="pop-spent"]').forEach((el) => { el.textContent = fmt(spent); });
root.querySelectorAll('[data-role="pop-total"]').forEach((el) => { el.textContent = fmt(total); });
const bar = root.querySelector('[data-role="bar"]');
if (bar && total > 0) {
const spentPct = (spent / total) * 100;
const remainPct = (remain / total) * 100;
bar.innerHTML = `<div class="spent-part" style="width:${spentPct}%"></div><div class="remain-part" style="width:${remainPct}%"></div>`;
}
const btn = root.querySelector('.balance-ring-btn') || root.querySelector('.balance-capsule');
if (btn) {
btn.classList.remove('low', 'empty');
if (remain <= 0) btn.classList.add('empty');
else if (remain <= 100) btn.classList.add('low');
}
}
function render() {
let total = Number(totalSlider.value);
let remain = Number(remainSlider.value);
if (remain > total) {
remain = total;
remainSlider.value = String(total);
}
remainSlider.max = String(total);
totalOut.textContent = fmt(total);
remainOut.textContent = fmt(remain);
currentPill.textContent = '余额 ' + fmt(remain);
variants.forEach((v) => updateRing(v, total, remain));
}
totalSlider.addEventListener('input', render);
remainSlider.addEventListener('input', render);
document.querySelectorAll('[data-preset]').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-preset]').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
const p = btn.dataset.preset;
if (p === 'healthy') { totalSlider.value = '5000'; remainSlider.value = '1280'; }
if (p === 'low') { totalSlider.value = '5000'; remainSlider.value = '80'; }
if (p === 'empty') { totalSlider.value = '5000'; remainSlider.value = '0'; }
render();
});
});
document.querySelectorAll('[data-click-toggle]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const wrap = btn.closest('.balance-popover-wrap');
wrap.classList.toggle('open');
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.balance-popover-wrap.open').forEach((w) => w.classList.remove('open'));
});
render();
</script>
</body>
</html>
+408
View File
@@ -0,0 +1,408 @@
<!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>
+467
View File
@@ -0,0 +1,467 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Console · 架构方案预览</title>
<style>
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color-scheme: dark;
--bg: #0c1016;
--panel: #121820;
--panel-2: #171f2b;
--line: #243041;
--text: #edf2f8;
--muted: #8fa3bf;
--faint: #5a6d84;
--accent: #4d93ff;
--accent-dim: rgba(77, 147, 255, 0.14);
--ok: #42c88a;
--warn: #ddb945;
--danger: #ef7b7b;
--index-w: 300px;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); }
/* ===== 新架构:Console Shell(非传统后台菜单) ===== */
.console { display: flex; flex-direction: column; height: 100%; }
.top {
height: 52px; padding: 0 20px;
display: flex; align-items: center; gap: 16px;
border-bottom: 1px solid var(--line);
background: var(--panel);
}
.logo { font-size: 14px; font-weight: 600; white-space: nowrap; }
.logo span { color: var(--muted); font-weight: 400; margin-left: 8px; }
.top-search {
flex: 1; max-width: 420px;
padding: 8px 12px; border-radius: 8px;
border: 1px solid var(--line); background: var(--bg);
color: var(--text); font-size: 13px;
}
.top-search::placeholder { color: var(--faint); }
.top-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.link { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
.link:hover { color: var(--text); }
.gear {
width: 34px; height: 34px; border-radius: 8px;
border: 1px solid var(--line); background: var(--bg);
color: var(--muted); cursor: pointer;
}
.gear:hover { color: var(--text); border-color: #355070; }
.workspace { flex: 1; display: flex; min-height: 0; }
/* 左:用户索引(主导航对象,不是功能菜单) */
.index {
width: var(--index-w);
border-right: 1px solid var(--line);
background: var(--panel);
display: flex; flex-direction: column;
}
.index-head {
padding: 14px 14px 10px;
border-bottom: 1px solid var(--line);
}
.index-head input {
width: 100%; padding: 8px 10px; border-radius: 8px;
border: 1px solid var(--line); background: var(--bg);
color: var(--text); font-size: 13px;
}
.index-meta {
margin-top: 8px; font-size: 11px; color: var(--faint);
display: flex; justify-content: space-between;
}
.index-list { flex: 1; overflow: auto; }
.user-row {
display: block; width: 100%; text-align: left;
padding: 12px 14px; border: none; border-bottom: 1px solid rgba(36,48,65,.5);
background: none; color: inherit; cursor: pointer;
}
.user-row:hover { background: rgba(255,255,255,.03); }
.user-row.active { background: var(--accent-dim); border-left: 3px solid var(--accent); padding-left: 11px; }
.user-row .name { font-size: 13px; font-weight: 500; }
.user-row .sub { font-size: 11px; color: var(--muted); margin-top: 3px; }
.user-row .sub.warn { color: var(--warn); }
.index-foot {
padding: 10px 14px; border-top: 1px solid var(--line);
font-size: 11px; color: var(--faint);
}
/* 右:工作区(随选中用户切换模式) */
.stage { flex: 1; overflow: auto; padding: 28px 32px 40px; }
.mode-label {
font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--faint); margin-bottom: 8px;
}
.mode-title { font-size: 22px; font-weight: 600; margin: 0 0 6px; letter-spacing: -.02em; }
.mode-desc { font-size: 13px; color: var(--muted); margin: 0 0 24px; }
/* 工作台模式 */
.pulse {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--line);
}
.pulse-item .n { font-size: 26px; font-weight: 600; }
.pulse-item .l { font-size: 12px; color: var(--muted); margin-top: 4px; }
.queue { max-width: 640px; }
.queue h3 { font-size: 13px; font-weight: 600; margin: 0 0 12px; color: var(--muted); }
.queue-item {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 0; border-bottom: 1px solid var(--line);
font-size: 13px; cursor: pointer;
}
.queue-item:hover .q-action { color: var(--accent); }
.q-action { color: var(--faint); font-size: 12px; }
/* 用户模式 */
.ctx-bar {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
margin-bottom: 20px;
}
.ctx-tabs { display: flex; gap: 4px; }
.ctx-tab {
padding: 7px 14px; border-radius: 999px; border: none;
background: none; color: var(--muted); font-size: 13px; cursor: pointer;
}
.ctx-tab.active { background: var(--accent-dim); color: var(--text); }
.ctx-actions { margin-left: auto; display: flex; gap: 8px; }
.btn {
padding: 7px 12px; border-radius: 7px; font-size: 12px;
border: 1px solid var(--line); background: var(--panel-2); color: var(--text); cursor: pointer;
}
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-danger { color: var(--danger); border-color: rgba(239,123,123,.35); }
.ctx-panel { display: none; max-width: 720px; }
.ctx-panel.active { display: block; }
.kv { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; font-size: 13px; margin-bottom: 24px; }
.kv .k { color: var(--muted); }
.kv .v.mono { font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
.inline-form { display: grid; gap: 12px; max-width: 360px; margin-top: 8px; }
.inline-form label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 5px; }
.inline-form input, .inline-form select {
width: 100%; padding: 9px 10px; border-radius: 7px;
border: 1px solid var(--line); background: var(--panel); color: var(--text); font-size: 13px;
}
.timeline { font-size: 13px; }
.timeline-row {
display: grid; grid-template-columns: 72px 1fr auto;
gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line);
}
.timeline-row .t { color: var(--faint); font-size: 12px; }
.perm-list { max-width: 480px; }
.perm-row {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid var(--line); font-size: 13px;
}
.sw { width: 30px; height: 16px; border-radius: 999px; background: var(--accent); position: relative; }
.sw::after { content:''; position:absolute; top:2px; right:2px; width:12px; height:12px; border-radius:50%; background:#fff; }
.sw.off { background:#3a4555; }
.sw.off::after { right:auto; left:2px; }
/* 系统抽屉(全局配置,不占主导航) */
.overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.55);
display: none; align-items: stretch; justify-content: flex-end; z-index: 50;
}
.overlay.open { display: flex; }
.drawer {
width: min(480px, 100vw); background: var(--panel);
border-left: 1px solid var(--line);
display: flex; flex-direction: column;
}
.drawer-head {
padding: 16px 18px; border-bottom: 1px solid var(--line);
display: flex; justify-content: space-between; align-items: center;
}
.drawer-head h2 { margin: 0; font-size: 15px; }
.drawer-tabs { display: flex; gap: 6px; padding: 12px 18px 0; border-bottom: 1px solid var(--line); }
.drawer-tab {
padding: 8px 12px 10px; border: none; background: none;
color: var(--muted); font-size: 12px; cursor: pointer;
border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.drawer-tab.active { color: var(--text); border-bottom-color: var(--accent); }
.drawer-body { padding: 18px; overflow: auto; flex: 1; }
.drawer-panel { display: none; }
.drawer-panel.active { display: block; }
.arch-note {
margin-top: 36px; padding: 16px 18px;
border: 1px solid #2a4060; border-radius: 10px;
background: rgba(18, 30, 48, .6);
max-width: 680px; font-size: 12px; color: var(--muted); line-height: 1.75;
}
.arch-note strong { color: #9ec5ff; }
.hidden { display: none !important; }
@media (max-width: 800px) {
.index { width: 100%; max-height: 40vh; border-right: none; border-bottom: 1px solid var(--line); }
.workspace { flex-direction: column; }
.pulse { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="console">
<!-- 顶栏:只有搜索 + 系统入口,没有功能菜单 -->
<header class="top">
<div class="logo">Console<span>运营</span></div>
<input class="top-search" id="globalSearch" placeholder="搜索用户、流水 ID… 回车定位" />
<div class="top-actions">
<button class="link" type="button">返回用户端</button>
<button class="gear" type="button" id="openSystem" title="系统配置"></button>
</div>
</header>
<div class="workspace">
<!-- 左栏 = 用户索引(不是菜单) -->
<aside class="index">
<div class="index-head">
<input id="filterUsers" placeholder="筛选用户…" />
<div class="index-meta"><span id="userCount">4 位用户</span><span>按最近活跃</span></div>
</div>
<div class="index-list" id="userList"></div>
<div class="index-foot">点选用户 → 右侧进入该用户工作区</div>
</aside>
<!-- 右栏 = 双模式工作区 -->
<main class="stage" id="stage">
<!-- 模式 A:未选用户 → 工作台 -->
<div id="modeDesk">
<div class="mode-label">模式 A · 工作台</div>
<h1 class="mode-title">今日平台</h1>
<p class="mode-desc">未选中用户时,只看全局态势与待处理事项</p>
<div class="pulse">
<div class="pulse-item"><div class="n">128</div><div class="l">7 日活跃</div></div>
<div class="pulse-item"><div class="n">¥386</div><div class="l">今日扣费</div></div>
<div class="pulse-item"><div class="n">7</div><div class="l">余额不足</div></div>
</div>
<div class="queue">
<h3>待处理</h3>
<div class="queue-item" data-pick="demo">
<span>@demo 余额 ¥0.30,账户已禁用</span>
<span class="q-action">去处理 →</span>
</div>
<div class="queue-item" data-pick="john">
<span>@john 等 6 人余额 &lt; ¥1</span>
<span class="q-action">批量查看 →</span>
</div>
</div>
<div class="arch-note">
<strong>新架构要点</strong><br>
① 取消「功能侧栏菜单」——运营动作围绕<strong>用户对象</strong>展开,左栏是用户索引不是模块导航<br>
② 右栏两种模式:<strong>工作台</strong>(无人选中)/ <strong>用户工作区</strong>(选中后)<br>
③ LLM、全局策略进<strong>系统抽屉</strong>(⚙),低频配置不占主界面<br>
④ 路由建议:<code>/ops</code> 独立 ShellURL 带 <code>?u=john</code> 可 deep link
</div>
</div>
<!-- 模式 B:选中用户 → 上下文工作区 -->
<div id="modeUser" class="hidden">
<div class="mode-label">模式 B · 用户工作区</div>
<h1 class="mode-title" id="ctxName">@john</h1>
<p class="mode-desc" id="ctxMeta">John · 正常 · ¥128.50</p>
<div class="ctx-bar">
<div class="ctx-tabs">
<button class="ctx-tab active" data-ctx="profile">资料</button>
<button class="ctx-tab" data-ctx="billing">计费</button>
<button class="ctx-tab" data-ctx="perm">权限</button>
<button class="ctx-tab" data-ctx="log">记录</button>
</div>
<div class="ctx-actions">
<button class="btn btn-primary" type="button">充值</button>
<button class="btn btn-danger" type="button">禁用</button>
</div>
</div>
<div class="ctx-panel active" data-ctx-panel="profile">
<div class="kv">
<div class="k">用户名</div><div class="v" id="f-username">john</div>
<div class="k">显示名</div><div class="v" id="f-display">John</div>
<div class="k">状态</div><div class="v" id="f-status">active</div>
<div class="k">工作目录</div><div class="v mono" id="f-workspace">/data/.../john</div>
<div class="k">Space</div><div class="v">2.1 / 5 MB</div>
</div>
<button class="btn" type="button">重置密码</button>
</div>
<div class="ctx-panel" data-ctx-panel="billing">
<div class="inline-form">
<div><label>充值金额(元)</label><input value="50" /></div>
<div><label>备注</label><input placeholder="写入流水" /></div>
<button class="btn btn-primary" type="button" style="width:fit-content">确认充值</button>
</div>
<h3 style="font-size:13px;color:var(--muted);margin:28px 0 8px">最近计费</h3>
<div class="timeline">
<div class="timeline-row"><span class="t">10:22</span><span>Token 扣费</span><span>−¥0.42</span></div>
<div class="timeline-row"><span class="t">昨天</span><span>运营充值</span><span>+¥50</span></div>
</div>
</div>
<div class="ctx-panel" data-ctx-panel="perm">
<p style="font-size:12px;color:var(--muted);margin:0 0 12px">相对角色默认的覆盖项</p>
<div class="perm-list">
<div class="perm-row"><span>mindspace.publish</span><span class="sw"></span></div>
<div class="perm-row"><span>chat.save_page</span><span class="sw"></span></div>
<div class="perm-row"><span>static-page-publish</span><span class="sw off"></span></div>
</div>
</div>
<div class="ctx-panel" data-ctx-panel="log">
<div class="timeline">
<div class="timeline-row"><span class="t">10:22</span><span>对话扣费 1680 tokens</span><span></span></div>
<div class="timeline-row"><span class="t">09:01</span><span>登录成功</span><span></span></div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 系统抽屉:LLM / 全局策略 / 平台参数 -->
<div class="overlay" id="systemOverlay">
<aside class="drawer">
<div class="drawer-head">
<h2>系统配置</h2>
<button class="link" type="button" id="closeSystem">关闭</button>
</div>
<div class="drawer-tabs">
<button class="drawer-tab active" data-sys="llm">LLM</button>
<button class="drawer-tab" data-sys="defaults">默认策略</button>
<button class="drawer-tab" data-sys="platform">平台</button>
</div>
<div class="drawer-body">
<div class="drawer-panel active" data-sys-panel="llm">
<div class="perm-row"><span>DeepSeek 生产</span><span style="font-size:11px;color:var(--ok)">使用中</span></div>
<div class="perm-row"><span>备用 OpenAI</span><button class="btn" style="padding:4px 8px">选用</button></div>
<button class="btn btn-primary" style="margin-top:14px" type="button">同步 goosed</button>
</div>
<div class="drawer-panel" data-sys-panel="defaults">
<p style="font-size:12px;color:var(--muted);margin:0 0 10px">角色 user 的默认 Capabilities / Skills / Policies</p>
<div class="perm-row"><span>mindspace.read</span><span class="sw"></span></div>
<div class="perm-row"><span>require_scan</span><span class="sw"></span></div>
</div>
<div class="drawer-panel" data-sys-panel="platform">
<div class="inline-form">
<div><label>新用户赠送(元)</label><input value="10" /></div>
<div><label>Space 默认 MB</label><input value="5" /></div>
<button class="btn btn-primary" type="button" style="width:fit-content">保存</button>
</div>
</div>
</div>
</aside>
</div>
<script>
const USERS = [
{ id: 'john', name: 'John', status: 'active', balance: '128.50', warn: false, workspace: '/data/mindspace/users/john' },
{ id: 'demo', name: '演示', status: 'disabled', balance: '0.30', warn: true, workspace: '/data/mindspace/users/demo' },
{ id: 'alice', name: 'Alice', status: 'active', balance: '0.80', warn: true, workspace: '/data/mindspace/users/alice' },
{ id: 'admin', name: '管理员', status: 'active', balance: '—', warn: false, workspace: '/data/mindspace/users/admin', role: 'admin' },
];
let selected = null;
function renderUsers(filter = '') {
const q = filter.toLowerCase();
const list = USERS.filter(u => u.id.includes(q) || u.name.toLowerCase().includes(q));
const el = document.getElementById('userList');
el.innerHTML = list.map(u => `
<button class="user-row${selected === u.id ? ' active' : ''}" data-id="${u.id}" type="button">
<div class="name">${u.name}</div>
<div class="sub${u.warn ? ' warn' : ''}">@${u.id} · ¥${u.balance}${u.warn ? ' · 余额低' : ''}</div>
</button>
`).join('');
document.getElementById('userCount').textContent = list.length + ' 位用户';
el.querySelectorAll('.user-row').forEach(btn => {
btn.addEventListener('click', () => selectUser(btn.dataset.id));
});
}
function selectUser(id) {
selected = id;
const u = USERS.find(x => x.id === id);
if (!u) return;
document.getElementById('modeDesk').classList.add('hidden');
document.getElementById('modeUser').classList.remove('hidden');
document.getElementById('ctxName').textContent = '@' + u.id;
document.getElementById('ctxMeta').textContent = `${u.name} · ${u.status === 'active' ? '正常' : '禁用'} · ¥${u.balance}`;
document.getElementById('f-username').textContent = u.id;
document.getElementById('f-display').textContent = u.name;
document.getElementById('f-status').textContent = u.status;
document.getElementById('f-workspace').textContent = u.workspace;
renderUsers(document.getElementById('filterUsers').value);
history.replaceState(null, '', '?u=' + id);
}
function clearUser() {
selected = null;
document.getElementById('modeDesk').classList.remove('hidden');
document.getElementById('modeUser').classList.add('hidden');
renderUsers(document.getElementById('filterUsers').value);
history.replaceState(null, '', location.pathname);
}
document.getElementById('filterUsers').addEventListener('input', e => renderUsers(e.target.value));
document.getElementById('globalSearch').addEventListener('keydown', e => {
if (e.key === 'Enter') {
const q = e.target.value.replace('@', '').trim().toLowerCase();
const hit = USERS.find(u => u.id.includes(q) || u.name.toLowerCase().includes(q));
if (hit) selectUser(hit.id);
}
});
document.querySelectorAll('.queue-item').forEach(el => {
el.addEventListener('click', () => selectUser(el.dataset.pick));
});
document.querySelectorAll('.ctx-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.ctx-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.ctx-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`[data-ctx-panel="${tab.dataset.ctx}"]`).classList.add('active');
});
});
document.getElementById('openSystem').addEventListener('click', () => document.getElementById('systemOverlay').classList.add('open'));
document.getElementById('closeSystem').addEventListener('click', () => document.getElementById('systemOverlay').classList.remove('open'));
document.getElementById('systemOverlay').addEventListener('click', e => {
if (e.target.id === 'systemOverlay') document.getElementById('systemOverlay').classList.remove('open');
});
document.querySelectorAll('.drawer-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.drawer-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.drawer-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`[data-sys-panel="${tab.dataset.sys}"]`).classList.add('active');
});
});
// 双击顶栏标题返回工作台
document.querySelector('.logo').addEventListener('dblclick', clearUser);
renderUsers();
const params = new URLSearchParams(location.search);
if (params.get('u')) selectUser(params.get('u'));
</script>
</body>
</html>