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:
@@ -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>:剩余 > ¥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>
|
||||
@@ -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>
|
||||
@@ -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 人余额 < ¥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> 独立 Shell,URL 带 <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>
|
||||
Reference in New Issue
Block a user