2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
468 lines
21 KiB
HTML
468 lines
21 KiB
HTML
<!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>
|