From 214c3ae2bf8cb811bef7fb0825a75f8bf4d96744 Mon Sep 17 00:00:00 2001 From: argszero Date: Thu, 13 Aug 2026 21:57:04 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(ui):=20AITokenPool=20=E9=9D=99?= =?UTF-8?q?=E6=80=81=20HTML=20UI=20=E5=8E=9F=E5=9E=8B=EF=BC=889=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20+=20=E7=99=BB=E5=BD=95=E9=A1=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 纯 HTML+CSS+JS 原型,无框架/无构建/无外部依赖,覆盖: - 公共版:仪表盘 / 模型市场(筛选排序)/ 共享管理(上架表单)/ 钱包 / 交易记录 / 设置 - 企业版:企业管理台(Key 池/员工/用量报表/组织)/ 员工自助面板 - 登录页(邮箱 + 企业 SSO 占位) 深色主题 + 强调色 #4ecdc4;mock 数据折算自 data/models.example.json(1 USD = 1000 点)。 ui/index.html 双击即可浏览;node --check + DOM 冒烟测试通过。 --- ui/README.md | 52 +++++ ui/css/style.css | 326 +++++++++++++++++++++++++++++++ ui/index.html | 354 +++++++++++++++++++++++++++++++++ ui/js/app.js | 496 +++++++++++++++++++++++++++++++++++++++++++++++ ui/js/data.js | 124 ++++++++++++ 5 files changed, 1352 insertions(+) create mode 100644 ui/README.md create mode 100644 ui/css/style.css create mode 100644 ui/index.html create mode 100644 ui/js/app.js create mode 100644 ui/js/data.js diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..bacb6da --- /dev/null +++ b/ui/README.md @@ -0,0 +1,52 @@ +# AITokenPool UI 原型(Static HTML Prototype) + +纯静态 HTML + CSS + JS 原型,无框架、无构建工具、无外部 CDN 依赖。 +浏览器直接打开即可浏览与演示。 + +## 如何浏览 + +**方式一(推荐)**:双击 `index.html`,用浏览器打开即可。 + +**方式二**:本地起一个静态服务器(可选,避免 file:// 下的个别限制): + +```bash +python3 -m http.server 8000 --directory ui +# 然后访问 http://localhost:8000 +``` + +## 页面清单 + +登录页(统一入口,占位): + +- 邮箱登录 + 企业 SSO 占位按钮 +- 登录前可切换 公共版 / 企业版,进入对应演示模式 + +公共版(共享市场): + +1. 仪表盘 Dashboard — 点数余额、本月用量、共享收益、最近交易 +2. 模型市场 Marketplace — 模型浏览、搜索、厂商筛选、排序(按价格/上下文) +3. 共享管理 Sharing — 上架 key 表单(可填写并提交)、我的共享列表(暂停/恢复/重新上架) +4. 钱包 Wallet — 点数余额、充值/提现占位入口、收支明细(分页) +5. 交易记录 Transactions — 消费/收益/充值/提现,Tab 筛选 + 分页 +6. 设置 Settings — 账户、API Key 管理(可生成)、通知、偏好 + +企业版(Admin / Enterprise): + +7. 企业管理台 Admin — Key 池管理(添加/撤销)、员工管理(配额调整)、用量报表(按模型/员工)、组织设置 +8. 员工自助面板 Employee Portal — 我的点数、可用模型、申请加额(占位) + +## 文件结构 + +``` +ui/ +├── index.html # 入口(登录页 + 应用外壳 + 全部视图) +├── css/style.css # 设计系统(深色主题 · 强调色 #4ecdc4 · 响应式预留) +├── js/data.js # 内嵌 mock 数据(模型价格、交易、共享、员工等) +├── js/app.js # 交互逻辑(导航、筛选、表单、分页、Toast) +└── README.md # 本文件 +``` + +## 数据说明 + +- 点数规则:1 USD = 1000 点;模型价格折算自 `data/models.example.json`(CNY 按 ~7.2 汇率示例折算) +- 全部数据为前端常量(mock),无后端、无真实调用 diff --git a/ui/css/style.css b/ui/css/style.css new file mode 100644 index 0000000..c5a607c --- /dev/null +++ b/ui/css/style.css @@ -0,0 +1,326 @@ +/* ============================================================ + AITokenPool UI Prototype — 设计系统 + 深色主基调 · 克制配色 · 强调色 #4ecdc4(青) + ============================================================ */ + +:root { + --bg: #0e1116; + --bg-soft: #141922; + --bg-card: #1a2029; + --bg-hover: #222a36; + --border: #2a3342; + --text: #e6ebf2; + --text-dim: #9aa7b8; + --text-mute: #67748a; + --accent: #4ecdc4; + --accent-soft: rgba(78, 205, 196, 0.12); + --accent-text: #6fe0d8; + --ok: #3ecf8e; + --warn: #e5b45b; + --danger: #e5666a; + --radius: 12px; + --radius-sm: 8px; + --sidebar-w: 240px; + --shadow: 0 8px 24px rgba(0,0,0,.35); + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { height: 100%; } + +body { + font-family: var(--font); + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.6; +} + +.hidden { display: none !important; } +.muted { color: var(--text-mute); } +.en { color: var(--text-mute); font-size: 12px; font-weight: 400; } + +/* ---------- 通用组件 ---------- */ + +.btn { + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text); + padding: 8px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 14px; + transition: all .15s; +} +.btn:hover { background: var(--bg-hover); border-color: #3a4658; } +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #062021; + font-weight: 600; +} +.btn-primary:hover { background: #63ded6; } +.btn-ghost { background: transparent; } +.btn-block { width: 100%; } + +.input, select.input { + width: 100%; + background: var(--bg-soft); + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius-sm); + padding: 9px 12px; + font-size: 14px; + outline: none; + transition: border-color .15s; +} +.input:focus { border-color: var(--accent); } +select.input option { background: var(--bg-card); } + +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; +} +.card h3 { font-size: 15px; margin-bottom: 14px; color: var(--text); } + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 16px; + margin-top: 16px; +} +.settings-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + margin-bottom: 4px; +} +.stat { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; +} +.stat .label { color: var(--text-mute); font-size: 12px; } +.stat .value { font-size: 24px; font-weight: 700; margin-top: 4px; } +.stat .sub { color: var(--text-dim); font-size: 12px; } +.stat.accent .value { color: var(--accent-text); } + +.table-wrap { overflow-x: auto; } +.table-wrap.compact { margin-top: 4px; } +.table { width: 100%; border-collapse: collapse; font-size: 13px; } +.table th { + text-align: left; + color: var(--text-mute); + font-weight: 500; + font-size: 12px; + padding: 8px 12px; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} +.table td { padding: 10px 12px; border-bottom: 1px solid rgba(42,51,66,.5); white-space: nowrap; } +.table tbody tr:hover { background: var(--bg-hover); } + +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + border: 1px solid var(--border); + color: var(--text-dim); +} +.badge.ok { color: var(--ok); border-color: rgba(62,207,142,.4); background: rgba(62,207,142,.08); } +.badge.warn { color: var(--warn); border-color: rgba(229,180,91,.4); background: rgba(229,180,91,.08); } +.badge.danger { color: var(--danger); border-color: rgba(229,102,106,.4); background: rgba(229,102,106,.08); } +.badge.accent { color: var(--accent-text); border-color: rgba(78,205,196,.4); background: var(--accent-soft); } +.badge.dim { color: var(--text-mute); } + +.toolbar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; } +.toolbar .input { max-width: 240px; } +.toolbar-count { align-self: center; color: var(--text-mute); font-size: 12px; margin-left: auto; } + +.tabs { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; } +.tab { + border: 1px solid var(--border); + background: transparent; + color: var(--text-dim); + padding: 7px 16px; + border-radius: 999px; + cursor: pointer; + font-size: 13px; +} +.tab:hover { color: var(--text); } +.tab.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text); } + +.pager { display: flex; gap: 6px; align-items: center; justify-content: flex-end; margin-top: 12px; } +.pager button { + min-width: 30px; + height: 30px; + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text-dim); + border-radius: var(--radius-sm); + cursor: pointer; +} +.pager button.active { border-color: var(--accent); color: var(--accent-text); } +.pager span { color: var(--text-mute); font-size: 12px; } + +.form { display: flex; flex-direction: column; gap: 12px; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.form label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--text-dim); } +.form label.check { flex-direction: row; align-items: center; gap: 8px; } +.form label.check input { accent-color: var(--accent); } + +.mini-list { display: flex; flex-direction: column; gap: 8px; } +.mini-item { + display: flex; justify-content: space-between; align-items: center; + gap: 10px; padding: 10px 12px; + background: var(--bg-soft); border: 1px solid var(--border); + border-radius: var(--radius-sm); +} +.mini-item .t { font-weight: 600; font-size: 13px; } +.mini-item .d { color: var(--text-mute); font-size: 12px; } +.mini-item .r { text-align: right; } +.mini-item .r .pts { color: var(--accent-text); font-weight: 700; } + +.bar-list { display: flex; flex-direction: column; gap: 12px; } +.bar-row .bar-label { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 4px; } +.bar-row .bar-label .n { color: var(--text-mute); } +.bar { + height: 8px; border-radius: 999px; + background: var(--bg-hover); overflow: hidden; +} +.bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--accent), #7ee8b8); border-radius: 999px; } + +/* ---------- 登录页 ---------- */ + +.login-view { + min-height: 100vh; + display: flex; align-items: center; justify-content: center; + padding: 24px; + background: + radial-gradient(600px 300px at 20% 0%, rgba(78,205,196,.08), transparent 60%), + radial-gradient(600px 400px at 90% 100%, rgba(78,205,196,.06), transparent 60%), + var(--bg); +} +.login-card { + width: 100%; max-width: 400px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 32px; + box-shadow: var(--shadow); +} +.login-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; } +.login-brand h1 { font-size: 20px; } +.login-brand p { color: var(--text-mute); font-size: 12px; } + +.logo { + width: 40px; height: 40px; border-radius: 10px; + background: linear-gradient(135deg, #4ecdc4, #2a9d8f); + color: #062021; font-weight: 800; + display: flex; align-items: center; justify-content: center; + font-size: 16px; flex-shrink: 0; +} + +.mode-switch { display: flex; gap: 8px; margin-bottom: 18px; } +.mode-btn { + flex: 1; padding: 9px 8px; border-radius: var(--radius-sm); + border: 1px solid var(--border); background: transparent; color: var(--text-dim); + cursor: pointer; font-size: 12.5px; text-align: center; +} +.mode-btn.active { border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft); } + +.login-form { display: flex; flex-direction: column; gap: 12px; } +.login-form label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--text-dim); } +.login-divider { display: flex; align-items: center; gap: 12px; margin: 16px 0; color: var(--text-mute); font-size: 12px; } +.login-divider::before, .login-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); } +.login-foot { margin-top: 16px; text-align: center; font-size: 13px; color: var(--text-dim); } +.login-foot a { color: var(--accent-text); text-decoration: none; } +.login-tip { margin-top: 14px; text-align: center; color: var(--text-mute); font-size: 11px; } + +/* ---------- 应用布局 ---------- */ + +.app { display: flex; height: 100vh; overflow: hidden; } + +.sidebar { + width: var(--sidebar-w); + flex-shrink: 0; + background: var(--bg-soft); + border-right: 1px solid var(--border); + display: flex; flex-direction: column; + padding: 14px 12px; +} +.sidebar-top { display: flex; align-items: center; gap: 10px; padding: 2px 6px 14px; border-bottom: 1px solid var(--border); } +.brand-text { display: flex; flex-direction: column; line-height: 1.3; } +.brand-text strong { font-size: 14px; } +.brand-text span { color: var(--text-mute); font-size: 11px; } + +.nav { flex: 1; padding: 14px 0; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; } +.nav-group { color: var(--text-mute); font-size: 11px; padding: 10px 8px 4px; } +.nav-item { + display: flex; align-items: center; gap: 10px; + padding: 9px 10px; border-radius: var(--radius-sm); + color: var(--text-dim); cursor: pointer; font-size: 13.5px; + border: none; background: transparent; text-align: left; width: 100%; +} +.nav-item .ico { width: 18px; text-align: center; font-size: 15px; } +.nav-item:hover { background: var(--bg-hover); color: var(--text); } +.nav-item.active { background: var(--accent-soft); color: var(--accent-text); font-weight: 600; } + +.sidebar-bottom { border-top: 1px solid var(--border); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; } +.user-chip { + display: flex; align-items: center; gap: 10px; + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius-sm); padding: 10px; +} +.user-chip .avatar { + width: 32px; height: 32px; border-radius: 50%; + background: linear-gradient(135deg, #4ecdc4, #2a9d8f); + color: #062021; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.user-chip strong { font-size: 13px; display: block; } +.user-chip span { color: var(--text-mute); font-size: 11px; } +.user-chip b { color: var(--accent-text); } + +.main { flex: 1; overflow-y: auto; padding: 28px 32px 48px; } + +.page-head { margin-bottom: 18px; } +.page-head h2 { font-size: 22px; } +.page-head p { color: var(--text-mute); margin-top: 2px; } + +.wallet-hero { display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap; } +.balance-big { font-size: 42px; font-weight: 800; color: var(--accent-text); line-height: 1.2; } +.wallet-actions { display: flex; gap: 10px; } + +.toast { + position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%); + background: var(--bg-card); border: 1px solid var(--accent); + color: var(--text); padding: 12px 22px; border-radius: 999px; + box-shadow: var(--shadow); z-index: 999; font-size: 14px; + animation: toast-in .2s ease; +} +@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } } + +/* ---------- 响应式预留 ---------- */ + +@media (max-width: 900px) { + .sidebar { width: 64px; padding: 12px 8px; } + .sidebar .brand-text, .sidebar .nav-item .label, .sidebar .user-chip > div:last-child, + .sidebar #toggle-mode-btn, .sidebar #logout-btn { display: none; } + .nav-item { justify-content: center; } + .main { padding: 20px 16px 40px; } + .form-row { grid-template-columns: 1fr; } +} + +@media (max-width: 560px) { + .stat-grid { grid-template-columns: 1fr 1fr; } + .card-grid { grid-template-columns: 1fr; } +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..eaf9908 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,354 @@ + + + + + +AITokenPool — AI Token 共享平台 + + + + + +
+ +
+ + + + + + + + + + diff --git a/ui/js/app.js b/ui/js/app.js new file mode 100644 index 0000000..a85a041 --- /dev/null +++ b/ui/js/app.js @@ -0,0 +1,496 @@ +/* ============================================================ + AITokenPool UI Prototype — 交互逻辑(纯原生 JS) + 无框架 / 无构建 / 无外部依赖 + ============================================================ */ + +(function () { + "use strict"; + + const D = window.ATDATA; + const $ = (sel) => document.querySelector(sel); + const $$ = (sel) => Array.from(document.querySelectorAll(sel)); + const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); + + let mode = "market"; // market | enterprise + let activeView = "dashboard"; + let txTab = "all", txPage = 1, walletPage = 1; + const TX_PAGE = 5; + + /* ---------------- 工具 ---------------- */ + + function toast(msg) { + const el = $("#toast"); + el.textContent = msg; + el.classList.remove("hidden"); + clearTimeout(toast._t); + toast._t = setTimeout(() => el.classList.add("hidden"), 2400); + } + + function badge(status, labels) { + return '' + + esc(labels[status] ? labels[status].text : status) + ""; + } + + /* ---------------- 导航 ---------------- */ + + const NAV = { + market: [ + { g: "公共版", items: [ + { id: "dashboard", ico: "📊", label: "仪表盘 Dashboard" }, + { id: "marketplace", ico: "🛒", label: "模型市场 Marketplace" }, + { id: "sharing", ico: "🔗", label: "共享管理 Sharing" }, + { id: "wallet", ico: "👛", label: "钱包 Wallet" }, + { id: "transactions", ico: "🧾", label: "交易记录 Transactions" }, + ]}, + { g: "系统", items: [ + { id: "settings", ico: "⚙️", label: "设置 Settings" }, + ]}, + ], + enterprise: [ + { g: "企业版", items: [ + { id: "admin", ico: "🛠️", label: "企业管理台 Admin" }, + { id: "employee", ico: "🧑‍💻", label: "员工自助面板 Portal" }, + ]}, + { g: "系统", items: [ + { id: "settings", ico: "⚙️", label: "设置 Settings" }, + ]}, + ], + }; + + const VIEW_TITLE = { + dashboard: "仪表盘 Dashboard", marketplace: "模型市场 Marketplace", sharing: "共享管理 Sharing", + wallet: "钱包 Wallet", transactions: "交易记录 Transactions", settings: "设置 Settings", + admin: "企业管理台 Admin", employee: "员工自助面板 Employee Portal", + }; + + function renderNav() { + const nav = $("#nav"); + nav.innerHTML = ""; + NAV[mode].forEach((group) => { + const g = document.createElement("div"); + g.className = "nav-group"; + g.textContent = group.g; + nav.appendChild(g); + group.items.forEach((item) => { + const b = document.createElement("button"); + b.className = "nav-item" + (item.id === activeView ? " active" : ""); + b.dataset.view = item.id; + b.innerHTML = '' + item.ico + '' + esc(item.label) + ""; + b.addEventListener("click", () => switchView(item.id)); + nav.appendChild(b); + }); + }); + $("#mode-label").textContent = mode === "market" ? "公共版 · 共享市场" : "企业版 · Admin"; + } + + function switchView(id) { + activeView = id; + $$(".view").forEach((v) => v.classList.add("hidden")); + $("#view-" + id).classList.remove("hidden"); + renderNav(); + renderView(id); + $("#main").scrollTop = 0; + } + + /* ---------------- 视图渲染 ---------------- */ + + function renderView(id) { + if (id === "dashboard") renderDashboard(); + else if (id === "marketplace") renderMarketplace(); + else if (id === "sharing") renderSharing(); + else if (id === "wallet") renderWallet(); + else if (id === "transactions") renderTransactions(); + else if (id === "settings") renderSettings(); + else if (id === "admin") renderAdmin(); + else if (id === "employee") renderEmployee(); + } + + /* --- 仪表盘 --- */ + + function renderDashboard() { + const txs = D.TRANSACTIONS; + const monthUse = txs.filter((t) => t.type === "consume").reduce((a, t) => a + Math.abs(t.pts), 0); + const monthEarn = txs.filter((t) => t.type === "earn").reduce((a, t) => a + t.pts, 0); + const recent = txs.slice(0, 5); + + $("#dash-stats").innerHTML = [ + stat("点数余额 Points", D.fmt(D.USER.balance), "1 USD = 1000 点", "accent"), + stat("本月用量 Usage", D.fmt(monthUse) + " 点", "共 " + txs.filter((t) => t.type === "consume").length + " 笔消费"), + stat("共享收益 Earnings", "+" + D.fmt(monthEarn) + " 点", D.SHARINGS.filter((s) => s.status === "on").length + " 个 key 上架中"), + stat("交易笔数 Trades", txs.length + " 笔", "含充值 / 提现 / 消费 / 收益"), + ].join(""); + + $("#dash-recent").innerHTML = recent.map((t) => + '
' + esc(t.detail) + "
" + + '
' + esc(t.time) + " · " + esc(t.partner) + "
" + + '
' + (t.pts > 0 ? "+" : "") + D.fmt(t.pts) + "
" + ).join("") + (recent.length ? "" : '

暂无交易

'); + + const on = D.SHARINGS.filter((s) => s.status === "on"); + $("#dash-sharings").innerHTML = on.map((s) => + '
' + esc(s.model) + "
" + + '
已用 ' + D.fmt(s.used) + " / " + D.fmt(s.quota) + " 点 · 单价 " + D.fmt(s.price) + " 点/1M
" + + '
+' + D.fmt(s.earned) + "
累计收益
" + ).join("") + (on.length ? "" : '

还没有上架的 key

'); + } + + function stat(label, value, sub, cls) { + return '
' + esc(label) + + '
' + value + "
" + esc(sub) + "
"; + } + + /* --- 模型市场 --- */ + + function renderMarketplace() { + const q = ($("#mk-search").value || "").toLowerCase(); + const prov = $("#mk-provider").value; + const sort = $("#mk-sort").value; + + let list = D.MARKET.filter((m) => + (!q || m.model.toLowerCase().includes(q) || m.provider.toLowerCase().includes(q)) && + (!prov || m.provider === prov) + ); + if (sort === "price-asc") list = [...list].sort((a, b) => a.in - b.in); + else if (sort === "price-desc") list = [...list].sort((a, b) => b.in - a.in); + else if (sort === "ctx-desc") list = [...list].sort((a, b) => b.ctx - a.ctx); + + $("#mk-count").textContent = list.length + " 个在售 key"; + $("#mk-body").innerHTML = list.length ? list.map((m) => + "" + esc(m.provider) + "" + esc(m.model) + "" + + "" + D.fmt(m.in) + " 点" + D.fmt(m.out) + " 点" + + "" + D.ctxFmt(m.ctx) + "" + + "" + (m.avail ? '可用' : '繁忙') + "" + + "成功率 " + m.success + "%" + ).join("") : '没有匹配的模型'; + } + + /* --- 共享管理 --- */ + + const SHARE_STATUS = { + on: { text: "上架中", cls: "ok" }, + paused: { text: "已暂停", cls: "warn" }, + off: { text: "已下线", cls: "dim" }, + }; + + function renderSharing() { + const on = D.SHARINGS.filter((s) => s.status === "on"); + const totalEarned = D.SHARINGS.reduce((a, s) => a + s.earned, 0); + const totalUsed = D.SHARINGS.reduce((a, s) => a + s.used, 0); + + $("#share-stats").innerHTML = [ + stat("上架中 Listings", on.length + " 个", D.SHARINGS.length + " 个历史"), + stat("累计收益 Earnings", "+" + D.fmt(totalEarned) + " 点", "全部 key"), + stat("已用量 Used", D.fmt(totalUsed) + " 点", D.SHARINGS.reduce((a, s) => a + s.quota, 0) + " 点总额度"), + ].join(""); + + // 表单下拉 + const selP = $("#sf-provider"); + if (!selP.dataset.init) { + selP.innerHTML = '' + D.PROVIDERS.map((p) => '").join(""); + const selM = $("#sf-model"); + const fillModels = () => { + const p = selP.value; + selM.innerHTML = '' + D.MODELS.filter((m) => !p || m.provider === p) + .map((m) => '").join(""); + }; + selP.addEventListener("change", fillModels); + selP.dataset.init = "1"; + fillModels(); + } + + $("#share-body").innerHTML = D.SHARINGS.map((s, i) => + "" + esc(s.model) + "" + + "" + D.fmt(s.used) + " / " + D.fmt(s.quota) + "" + + "" + D.fmt(s.price) + " 点/1M" + + "+" + D.fmt(s.earned) + " 点" + + "" + badge(s.status, SHARE_STATUS) + "" + + "" + ).join(""); + } + + function toggleSharing(i) { + const s = D.SHARINGS[i]; + if (s.status === "on") { s.status = "paused"; toast("已暂停 " + s.model + " 的共享"); } + else if (s.status === "paused") { s.status = "on"; toast("已恢复 " + s.model + " 的共享"); } + else { s.status = "on"; s.quota = 50000; s.used = 0; toast("已重新上架 " + s.model); } + renderSharing(); + if (activeView === "dashboard") renderDashboard(); + } + + /* --- 钱包 --- */ + + function renderWallet() { + $("#side-balance").textContent = D.fmt(D.USER.balance); + $("#wallet-balance").textContent = D.fmt(D.USER.balance); + + const list = D.TRANSACTIONS; + const pages = Math.max(1, Math.ceil(list.length / TX_PAGE)); + if (walletPage > pages) walletPage = pages; + const rows = list.slice((walletPage - 1) * TX_PAGE, walletPage * TX_PAGE); + + $("#wallet-body").innerHTML = rows.map((t) => + "" + esc(t.time) + "" + esc(t.typeName || t.type) + "" + + "" + esc(t.partner) + "" + esc(t.detail) + "" + + "" + + (t.pts > 0 ? "+" : "") + D.fmt(t.pts) + "" + ).join(""); + + pager(pages, walletPage, (p) => { walletPage = p; renderWallet(); }, $("#wallet-pager")); + } + + /* --- 交易记录 --- */ + + const TX_TYPE = { + consume: "消费", earn: "收益", topup: "充值", withdraw: "提现", + }; + + function renderTransactions() { + $$("#tx-tabs .tab").forEach((b) => b.classList.toggle("active", b.dataset.txTab === txTab)); + let list = D.TRANSACTIONS; + if (txTab === "consume") list = list.filter((t) => t.type === "consume"); + else if (txTab === "earn") list = list.filter((t) => t.type === "earn"); + + const pages = Math.max(1, Math.ceil(list.length / TX_PAGE)); + if (txPage > pages) txPage = pages; + const rows = list.slice((txPage - 1) * TX_PAGE, txPage * TX_PAGE); + + $("#tx-body").innerHTML = rows.map((t) => + "" + esc(t.time) + "" + + "" + (t.type === "earn" ? '收益' : t.type === "consume" ? '消费' : '' + esc(TX_TYPE[t.type] || t.type) + "") + "" + + "" + esc(t.partner) + "" + esc(t.tokens) + "" + + "" + (t.pts > 0 ? "+" : "") + D.fmt(t.pts) + "" + + "" + (t.status === "处理中" ? '' + esc(t.status) + "" : esc(t.status)) + "" + ).join(""); + + pager(pages, txPage, (p) => { txPage = p; renderTransactions(); }, $("#tx-pager")); + } + + function pager(pages, cur, go, container) { + if (pages <= 1) { container.innerHTML = ""; return; } + let html = ""; + for (let i = 1; i <= pages; i++) { + html += '"; + } + html += "" + cur + " / " + pages + ""; + container.innerHTML = html; + Array.from(container.querySelectorAll("button[data-p]")).forEach((b) => + b.addEventListener("click", () => go(Number(b.dataset.p)))); + } + + /* --- 设置 --- */ + + function renderSettings() { + $("#api-keys").innerHTML = D.API_KEYS.map((k) => + '
' + esc(k.name) + "
" + + '
' + esc(k.id) + " · 创建于 " + esc(k.created) + "
" + + '
最近使用 ' + esc(k.last) + "
" + ).join(""); + } + + /* --- 企业管理台 --- */ + + const KEY_STATUS = { + ok: { text: "可用", cls: "ok" }, + limit: { text: "达限额", cls: "warn" }, + exhausted: { text: "已用尽", cls: "danger" }, + revoked: { text: "已撤销", cls: "dim" }, + }; + + function renderAdmin() { + const tab = $("#admin-tabs .tab.active").dataset.adminTab; + $$(".admin-pane").forEach((p) => p.classList.toggle("hidden", p.dataset.adminPane !== tab)); + + if (tab === "keys") { + const q = ($("#ak-search").value || "").toLowerCase(); + const rows = D.KEYS.filter((k) => !q || k.model.includes(q) || k.provider.includes(q) || k.key.includes(q)); + $("#keys-body").innerHTML = rows.map((k) => + "" + esc(k.provider) + "" + esc(k.model) + "" + + "" + esc(k.key) + "" + + "" + D.fmt(k.used) + " / " + D.fmt(k.quota) + "" + + "" + badge(k.status, KEY_STATUS) + "" + + "" + ).join(""); + } else if (tab === "employees") { + const total = D.EMPLOYEES.reduce((a, e) => a + e.quota, 0); + const used = D.EMPLOYEES.reduce((a, e) => a + e.used, 0); + $("#emp-stats").innerHTML = [ + stat("员工数 Members", D.EMPLOYEES.length + " 人", "2 个部门"), + stat("总配额 Quota", D.fmt(total) + " 点", "月"), + stat("已用 Usage", D.fmt(used) + " 点", Math.round((used / total) * 100) + "% 消耗率"), + stat("剩余 Remain", D.fmt(total - used) + " 点", "按员工分配"), + ].join(""); + $("#emp-body").innerHTML = D.EMPLOYEES.map((e, i) => + "" + esc(e.name) + "" + esc(e.dept) + "" + + "" + D.fmt(e.used) + " / " + D.fmt(e.quota) + "" + + "" + D.fmt(e.quota - e.used) + "" + + "" + (e.used / e.quota > 0.9 ? '接近限额' : '正常') + "" + + "" + ).join(""); + } else if (tab === "usage") { + const maxM = Math.max(...D.USAGE_MODEL.map((u) => u.pts)); + const maxE = Math.max(...D.USAGE_EMP.map((u) => u.pts)); + $("#usage-model").innerHTML = D.USAGE_MODEL.map((u) => barRow(u.name, u.pts, maxM, "点")).join(""); + $("#usage-emp").innerHTML = D.USAGE_EMP.map((u) => barRow(u.name, u.pts, maxE, "点")).join(""); + } + } + + function barRow(name, pts, max, unit) { + const pct = Math.round((pts / max) * 100); + return '
' + esc(name) + '' + D.fmt(pts) + " " + unit + "
" + + '
'; + } + + /* --- 员工自助面板 --- */ + + function renderEmployee() { + const emp = D.EMPLOYEES[0]; // 当前登录员工视角 + $("#emp-portal-stats").innerHTML = [ + stat("我的点数 My points", D.fmt(emp.quota - emp.used), "配额 " + D.fmt(emp.quota) + " 点/月", "accent"), + stat("本月已用 Used", D.fmt(emp.used) + " 点", Math.round((emp.used / emp.quota) * 100) + "%"), + stat("可用模型 Models", D.MODELS.length + " 个", "全部厂商"), + stat("待审批申请", "1 个", "上笔申请 5000 点"), + ].join(""); + + $("#emp-models").innerHTML = D.MODELS.slice(0, 6).map((m) => + '
' + esc(m.model) + "
" + + '
' + esc(m.provider) + " · " + D.ctxFmt(m.ctx) + " 上下文
" + + '
' + D.fmt(m.in) + "
点/1M 输入
" + ).join("") + '

…共 ' + D.MODELS.length + " 个模型可用

"; + } + + /* ---------------- 事件 ---------------- */ + + function bindEvents() { + // 登录 + $("#login-form").addEventListener("submit", (e) => { + e.preventDefault(); + $("#login-view").classList.add("hidden"); + $("#app").classList.remove("hidden"); + switchView(mode === "market" ? "dashboard" : "admin"); + toast("欢迎回来,阿零(演示账号)"); + }); + + // 登录页模式切换 + $$("#login-mode-switch .mode-btn").forEach((b) => { + b.addEventListener("click", () => { + $$("#login-mode-switch .mode-btn").forEach((x) => x.classList.remove("active")); + b.classList.add("active"); + mode = b.dataset.mode; + }); + }); + + $("#sso-btn").addEventListener("click", () => toast("企业 SSO 为占位入口(静态原型)")); + + // 应用内模式切换 + $("#toggle-mode-btn").addEventListener("click", () => { + mode = mode === "market" ? "enterprise" : "market"; + switchView(mode === "market" ? "dashboard" : "admin"); + toast("已切换到" + (mode === "market" ? "公共版 · 共享市场" : "企业版 · Admin")); + }); + + $("#logout-btn").addEventListener("click", () => { + $("#app").classList.add("hidden"); + $("#login-view").classList.remove("hidden"); + toast("已退出(静态演示)"); + }); + + // 市场筛选 + $("#mk-search").addEventListener("input", renderMarketplace); + $("#mk-provider").addEventListener("change", renderMarketplace); + $("#mk-sort").addEventListener("change", renderMarketplace); + if (!$("#mk-provider").dataset.init) { + $("#mk-provider").innerHTML = '' + D.PROVIDERS.map((p) => '").join(""); + $("#mk-provider").dataset.init = "1"; + } + + // 共享上架表单 + $("#share-form").addEventListener("submit", (e) => { + e.preventDefault(); + const model = $("#sf-model").value; + const quota = Number($("#sf-quota").value || 0); + const price = Number($("#sf-price").value || 0); + if (!model || quota <= 0) { toast("请选择模型并填写有效额度"); return; } + D.SHARINGS.unshift({ id: Date.now(), model, quota, used: 0, price, earned: 0, status: "on" }); + renderSharing(); + if (activeView === "dashboard") renderDashboard(); + toast("已上架 " + model + "(额度 " + D.fmt(quota) + " 点)"); + e.target.reset(); + const p = $("#sf-provider"); p.value = ""; p.dispatchEvent(new Event("change")); + $("#sf-quota").value = 5000; $("#sf-price").value = 280; + }); + + // 共享列表操作(事件委托) + $("#share-body").addEventListener("click", (e) => { + const b = e.target.closest("[data-share-toggle]"); + if (b) toggleSharing(Number(b.dataset.shareToggle)); + }); + + // 钱包按钮 + $("#topup-btn").addEventListener("click", () => toast("充值入口为占位(静态原型)")); + $("#withdraw-btn").addEventListener("click", () => toast("提现入口为占位(静态原型)")); + + // 交易 Tab + $$("#tx-tabs .tab").forEach((b) => b.addEventListener("click", () => { txTab = b.dataset.txTab; txPage = 1; renderTransactions(); })); + + // API Key 生成 + $("#new-api-key-btn").addEventListener("click", () => { + const id = "atk_live_" + Math.random().toString(16).slice(2, 10) + "…" + Math.random().toString(16).slice(2, 6); + D.API_KEYS.unshift({ id, name: "新 Key(未命名)", created: "2026-08-13", last: "从未" }); + renderSettings(); + toast("已生成新 API Key(演示)"); + }); + + // 管理台 Tabs + $$("#admin-tabs .tab").forEach((b) => b.addEventListener("click", () => { + $$("#admin-tabs .tab").forEach((x) => x.classList.remove("active")); + b.classList.add("active"); + renderAdmin(); + })); + + $("#add-key-btn").addEventListener("click", () => { + const m = D.MODELS[Math.floor(Math.random() * D.MODELS.length)]; + D.KEYS.unshift({ id: Date.now(), provider: m.provider, model: m.model, key: "sk-****-new1", quota: 100000, used: 0, status: "ok" }); + renderAdmin(); + toast("已添加上游 Key(演示:" + m.model + ")"); + }); + + $("#ak-search").addEventListener("input", renderAdmin); + + // 管理台事件委托(撤销 key / 员工加额) + $("#keys-body").addEventListener("click", (e) => { + const b = e.target.closest("[data-key-revoke]"); + if (!b) return; + const k = D.KEYS.find((x) => x.id === Number(b.dataset.keyRevoke)); + if (!k) return; + k.status = k.status === "revoked" ? "ok" : "revoked"; + renderAdmin(); + toast(k.status === "revoked" ? "已撤销 " + k.model + " 的 key" : "已恢复 " + k.model + " 的 key"); + }); + + $("#emp-body").addEventListener("click", (e) => { + const b = e.target.closest("[data-emp-add]"); + if (!b) return; + D.EMPLOYEES[Number(b.dataset.empAdd)].quota += 5000; + renderAdmin(); + toast("已为员工 +5000 点配额"); + }); + + // 员工申请加额 + $("#req-btn").addEventListener("click", () => { + const n = Number($("#req-quota").value || 0); + if (n <= 0) { toast("请填写申请点数"); return; } + toast("申请已提交(" + D.fmt(n) + " 点),等待管理员审批"); + }); + } + + /* ---------------- 初始化 ---------------- */ + + document.addEventListener("DOMContentLoaded", () => { + renderNav(); + bindEvents(); + renderView("dashboard"); + $("#side-balance").textContent = D.fmt(D.USER.balance); + }); +})(); diff --git a/ui/js/data.js b/ui/js/data.js new file mode 100644 index 0000000..d838e62 --- /dev/null +++ b/ui/js/data.js @@ -0,0 +1,124 @@ +/* ============================================================ + AITokenPool UI Prototype — 内嵌 mock 数据 + 点数规则:1 USD = 1000 点;CNY 按 ~7.2 CNY/USD 折算(示例) + ============================================================ */ + +(function () { + "use strict"; + + const USD = (usd) => Math.round(usd * 1000); + const CNY = (cny) => Math.round((cny / 7.2) * 1000); + + // 模型价格(来自 data/models.example.json,折算为点数 / 1M tokens) + const MODELS = [ + { provider: "deepseek", model: "deepseek-v4-pro", in: USD(0.435), out: USD(0.87), ctx: 1048576, max: 384000, tag: "推理" }, + { provider: "deepseek", model: "deepseek-v4-flash", in: USD(0.14), out: USD(0.28), ctx: 1048576, max: 384000, tag: "通用" }, + { provider: "zhipu", model: "glm-5.2", in: CNY(8.0), out: CNY(28.0), ctx: 1048576, max: null, tag: "通用" }, + { provider: "openai", model: "gpt-5.5-pro", in: USD(30.0), out: USD(180.0), ctx: 1050000, max: null, tag: "旗舰" }, + { provider: "anthropic",model: "claude-opus-4.7", in: USD(5.0), out: USD(25.0), ctx: 1000000, max: null, tag: "旗舰" }, + { provider: "google", model: "gemini-3.1-pro", in: USD(2.0), out: USD(12.0), ctx: 1048576, max: null, tag: "多模态" }, + { provider: "xai", model: "grok-4.6", in: USD(2.0), out: USD(6.0), ctx: 500000, max: null, tag: "通用" }, + { provider: "moonshot", model: "kimi-k3", in: CNY(20.0), out: CNY(100.0), ctx: 1048576, max: null, tag: "长文" }, + { provider: "bytedance-ark", model: "doubao-seed-2.1-pro", in: CNY(6.0), out: CNY(30.0), ctx: 262144, max: null, tag: "通用" }, + { provider: "minimax", model: "minimax-m3", in: USD(0.3), out: USD(1.2), ctx: 1048576, max: null, tag: "轻量" }, + ]; + + // 可用性(市场展示用) + MODELS.forEach((m) => { + m.availability = Math.random() < 0.85 ? "available" : "busy"; + }); + MODELS[2].availability = "available"; // glm + MODELS[4].availability = "busy"; // claude 演示"繁忙" + + const fmt = (n) => n.toLocaleString("zh-CN"); + const ctxFmt = (n) => (n >= 1000000 ? (n / 1000000).toFixed(1) + "M" : fmt(n)); + + window.ATDATA = { + fmt, ctxFmt, + + MODELS, + + PROVIDERS: [...new Set(MODELS.map((m) => m.provider))], + + // 当前用户(公共版) + USER: { name: "阿零", email: "demo@aitokenpool.local", balance: 12480 }, + + // 最近交易 / 收支明细(wallet 与 transactions 共用) + TRANSACTIONS: [ + { id: 1, time: "08-13 20:15", type: "consume", partner: "deepseek-v4-flash", detail: "消费 · 用 shared key", tokens: "1.2M", pts: -320, status: "成功" }, + { id: 2, time: "08-13 19:02", type: "earn", partner: "glm-5.2", detail: "收益 · 我的 key 被消费", tokens: "860K", pts: +420, status: "入账" }, + { id: 3, time: "08-13 17:44", type: "consume", partner: "gpt-5.5-pro", detail: "消费 · 用 shared key", tokens: "45K", pts: -860, status: "成功" }, + { id: 4, time: "08-13 15:10", type: "earn", partner: "kimi-k3", detail: "收益 · 我的 key 被消费", tokens: "1.5M", pts: +980, status: "入账" }, + { id: 5, time: "08-13 11:36", type: "topup", partner: "—", detail: "充值", tokens: "—", pts: +5000, status: "成功" }, + { id: 6, time: "08-12 21:08", type: "consume", partner: "gemini-3.1-pro", detail: "消费 · 用 shared key", tokens: "2.1M", pts: -310, status: "成功" }, + { id: 7, time: "08-12 14:22", type: "earn", partner: "deepseek-v4-flash", detail: "收益 · 我的 key 被消费", tokens: "3.0M", pts: +210, status: "入账" }, + { id: 8, time: "08-12 09:41", type: "consume", partner: "claude-opus-4.7", detail: "消费 · 用 shared key", tokens: "22K", pts: -640, status: "成功" }, + { id: 9, time: "08-11 18:30", type: "withdraw", partner: "—", detail: "提现(占位)", tokens: "—", pts: -2000, status: "处理中" }, + { id: 10, time: "08-11 10:12", type: "earn", partner: "glm-5.2", detail: "收益 · 我的 key 被消费", tokens: "700K", pts: +390, status: "入账" }, + { id: 11, time: "08-10 16:05", type: "consume", partner: "doubao-seed-2.1-pro", detail: "消费 · 用 shared key", tokens: "540K", pts: -260, status: "成功" }, + { id: 12, time: "08-10 08:50", type: "earn", partner: "minimax-m3", detail: "收益 · 我的 key 被消费", tokens: "2.4M", pts: +180, status: "入账" }, + ], + + // 我的共享 + SHARINGS: [ + { id: 1, model: "glm-5.2", quota: 100000, used: 32800, price: 420, earned: 1790, status: "on" }, + { id: 2, model: "deepseek-v4-flash", quota: 80000, used: 51200, price: 280, earned: 1240, status: "on" }, + { id: 3, model: "kimi-k3", quota: 50000, used: 800, price: 380, earned: 60, status: "paused" }, + { id: 4, model: "minimax-m3", quota: 20000, used: 20000, price: 160, earned: 480, status: "off" }, + ], + + // 市场在售 key(公共版浏览) + MARKET: [ + { id: 1, provider: "deepseek", model: "deepseek-v4-flash", in: USD(0.14), out: USD(0.28), ctx: 1048576, avail: true, success: 99.2 }, + { id: 2, provider: "zhipu", model: "glm-5.2", in: CNY(8.0), out: CNY(28.0), ctx: 1048576, avail: true, success: 98.6 }, + { id: 3, provider: "openai", model: "gpt-5.5-pro", in: USD(30.0), out: USD(180.0), ctx: 1050000, avail: true, success: 97.1 }, + { id: 4, provider: "anthropic",model: "claude-opus-4.7", in: USD(5.0), out: USD(25.0), ctx: 1000000, avail: false, success: 95.4 }, + { id: 5, provider: "google", model: "gemini-3.1-pro", in: USD(2.0), out: USD(12.0), ctx: 1048576, avail: true, success: 98.9 }, + { id: 6, provider: "moonshot", model: "kimi-k3", in: CNY(20.0), out: CNY(100.0), ctx: 1048576, avail: true, success: 96.8 }, + { id: 7, provider: "xai", model: "grok-4.6", in: USD(2.0), out: USD(6.0), ctx: 500000, avail: true, success: 93.7 }, + ], + + // 我的 API Keys(设置页) + API_KEYS: [ + { id: "atk_live_9f2c1a…e41b", name: "本地 CLI 工具", created: "2026-08-01", last: "08-13 20:11" }, + { id: "atk_live_3b88d0…77aa", name: "CI / 自动化", created: "2026-07-22", last: "08-12 15:40" }, + ], + + // —— 企业版 —— + + // Key 池 + KEYS: [ + { id: 1, provider: "openai", model: "gpt-5.5-pro", key: "sk-****-8f2a", quota: 300000, used: 214000, status: "ok" }, + { id: 2, provider: "anthropic",model: "claude-opus-4.7", key: "sk-****-1c77", quota: 200000, used: 62000, status: "ok" }, + { id: 3, provider: "deepseek", model: "deepseek-v4-flash", key: "sk-****-90bb", quota: 500000, used: 461000, status: "limit" }, + { id: 4, provider: "zhipu", model: "glm-5.2", key: "sk-****-44e9", quota: 150000, used: 150000, status: "exhausted" }, + { id: 5, provider: "google", model: "gemini-3.1-pro", key: "sk-****-77d2", quota: 180000, used: 15000, status: "ok" }, + { id: 6, provider: "moonshot", model: "kimi-k3", key: "sk-****-ab31", quota: 100000, used: 0, status: "revoked" }, + ], + + // 员工 + EMPLOYEES: [ + { id: 1, name: "陈默", dept: "研发", quota: 20000, used: 12400 }, + { id: 2, name: "林小满", dept: "研发", quota: 20000, used: 19850 }, + { id: 3, name: "苏航", dept: "产品", quota: 15000, used: 3200 }, + { id: 4, name: "周雨", dept: "市场", quota: 10000, used: 9800 }, + { id: 5, name: "何明", dept: "设计", quota: 8000, used: 2100 }, + ], + + // 用量报表 + USAGE_MODEL: [ + { name: "gpt-5.5-pro", pts: 18600 }, + { name: "deepseek-v4-flash", pts: 13200 }, + { name: "glm-5.2", pts: 9800 }, + { name: "claude-opus-4.7", pts: 7400 }, + { name: "gemini-3.1-pro", pts: 3100 }, + ], + USAGE_EMP: [ + { name: "林小满", pts: 19850 }, + { name: "陈默", pts: 12400 }, + { name: "周雨", pts: 9800 }, + { name: "苏航", pts: 3200 }, + { name: "何明", pts: 2100 }, + ], + }; +})(); From dfba0aae576efce1e8c23a5a1a4a5e1b676acac1 Mon Sep 17 00:00:00 2001 From: argszero Date: Thu, 13 Aug 2026 22:04:09 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(ui):=20=E4=BF=AE=E6=AD=A3=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E6=A8=A1=E5=9E=8B=20=E2=80=94=20=E4=B8=80=E5=A5=97?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E4=B8=A4=E7=A7=8D=E9=83=A8=E7=BD=B2=EF=BC=8C?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=8C=BA=E5=88=86=E8=80=8C=E9=9D=9E=E5=8F=8C?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除登录页/侧边栏的公共版·企业版模式切换(login-mode-switch / toggle-mode) - 移除独立 Employee Portal 页面;员工即普通用户,用同一套界面 - Admin 收敛为管理员角色视图(Key 池 / 成员管理 / 用量报表 / 组织设置,含'关闭外部注册'企业部署开关) - 单一登录入口,角色由账号决定 - 更新 README 说明'一套产品、两种部署场景、角色区分'模型 --- ui/README.md | 29 +++++++------ ui/css/style.css | 17 ++++---- ui/index.html | 51 ++++++----------------- ui/js/app.js | 103 +++++++++++++---------------------------------- 4 files changed, 67 insertions(+), 133 deletions(-) diff --git a/ui/README.md b/ui/README.md index bacb6da..56eefe0 100644 --- a/ui/README.md +++ b/ui/README.md @@ -3,11 +3,23 @@ 纯静态 HTML + CSS + JS 原型,无框架、无构建工具、无外部 CDN 依赖。 浏览器直接打开即可浏览与演示。 +## 产品模型(重要) + +**一套产品,两种部署场景,角色区分**: + +- **公共版**:部署在公网,任何人都能注册、分享 key、赚点数、消费别人共享的 key; +- **企业版**:部署在企业内网,管理员(IT)把采购的 key 放进池子、给成员分配点数; +- 两者的**功能集合完全相同**(市场 / 共享 / 钱包 / 交易 / 设置),不存在"企业版专属功能"; +- 差异只在**谁会主动分享**(公共版人人可分享;企业版只有管理员有动力采购/分享 key); +- **角色是权限差异,不是产品差异**:管理员额外拥有"管理视图"(key 池 / 成员点数 / 用量报表 / 组织设置),普通用户没有;员工就是普通用户,用同一套界面。 + +本原型据此实现:单一登录入口(角色由账号决定),一套导航(仪表盘 / 模型市场 / 共享管理 / 钱包 / 交易记录 / 设置 + 管理员角色视图)。 + ## 如何浏览 **方式一(推荐)**:双击 `index.html`,用浏览器打开即可。 -**方式二**:本地起一个静态服务器(可选,避免 file:// 下的个别限制): +**方式二**:本地起一个静态服务器(可选): ```bash python3 -m http.server 8000 --directory ui @@ -16,12 +28,9 @@ python3 -m http.server 8000 --directory ui ## 页面清单 -登录页(统一入口,占位): +登录页:单一入口(邮箱登录 + 企业 SSO 占位),不做公共版/企业版二选一。 -- 邮箱登录 + 企业 SSO 占位按钮 -- 登录前可切换 公共版 / 企业版,进入对应演示模式 - -公共版(共享市场): +同一套界面(角色视图): 1. 仪表盘 Dashboard — 点数余额、本月用量、共享收益、最近交易 2. 模型市场 Marketplace — 模型浏览、搜索、厂商筛选、排序(按价格/上下文) @@ -29,11 +38,7 @@ python3 -m http.server 8000 --directory ui 4. 钱包 Wallet — 点数余额、充值/提现占位入口、收支明细(分页) 5. 交易记录 Transactions — 消费/收益/充值/提现,Tab 筛选 + 分页 6. 设置 Settings — 账户、API Key 管理(可生成)、通知、偏好 - -企业版(Admin / Enterprise): - -7. 企业管理台 Admin — Key 池管理(添加/撤销)、员工管理(配额调整)、用量报表(按模型/员工)、组织设置 -8. 员工自助面板 Employee Portal — 我的点数、可用模型、申请加额(占位) +7. 管理视图 Admin(管理员角色专属)— Key 池管理(添加/撤销)、成员管理(配额调整)、用量报表(按模型/成员)、组织设置(含"关闭外部注册"企业部署开关) ## 文件结构 @@ -41,7 +46,7 @@ python3 -m http.server 8000 --directory ui ui/ ├── index.html # 入口(登录页 + 应用外壳 + 全部视图) ├── css/style.css # 设计系统(深色主题 · 强调色 #4ecdc4 · 响应式预留) -├── js/data.js # 内嵌 mock 数据(模型价格、交易、共享、员工等) +├── js/data.js # 内嵌 mock 数据(模型价格、交易、共享、成员等) ├── js/app.js # 交互逻辑(导航、筛选、表单、分页、Toast) └── README.md # 本文件 ``` diff --git a/ui/css/style.css b/ui/css/style.css index c5a607c..6925aae 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -229,13 +229,7 @@ select.input option { background: var(--bg-card); } font-size: 16px; flex-shrink: 0; } -.mode-switch { display: flex; gap: 8px; margin-bottom: 18px; } -.mode-btn { - flex: 1; padding: 9px 8px; border-radius: var(--radius-sm); - border: 1px solid var(--border); background: transparent; color: var(--text-dim); - cursor: pointer; font-size: 12.5px; text-align: center; -} -.mode-btn.active { border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft); } +.mode-switch { display: none; } .login-form { display: flex; flex-direction: column; gap: 12px; } .login-form label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--text-dim); } @@ -271,6 +265,15 @@ select.input option { background: var(--bg-card); } border: none; background: transparent; text-align: left; width: 100%; } .nav-item .ico { width: 18px; text-align: center; font-size: 15px; } +.nav-tag { + margin-left: auto; + font-size: 10px; + padding: 1px 7px; + border-radius: 999px; + border: 1px solid rgba(78,205,196,.4); + color: var(--accent-text); + background: var(--accent-soft); +} .nav-item:hover { background: var(--bg-hover); color: var(--text); } .nav-item.active { background: var(--accent-soft); color: var(--accent-text); font-weight: 600; } diff --git a/ui/index.html b/ui/index.html index eaf9908..2c12a94 100644 --- a/ui/index.html +++ b/ui/index.html @@ -15,13 +15,9 @@

AITokenPool

-

AI Token 共享平台 · 企业版 / 公共版

+

AI Token 共享平台 · 一套产品,两种部署场景

-
- - -
- - diff --git a/ui/js/app.js b/ui/js/app.js index 566a75b..e98806c 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -348,8 +348,6 @@ toast("欢迎回来,阿零(演示账号)"); }); - $("#sso-btn").addEventListener("click", () => toast("企业 SSO 为占位入口(静态原型)")); - $("#logout-btn").addEventListener("click", () => { $("#app").classList.add("hidden"); $("#login-view").classList.remove("hidden"); From dbe31b17dfcc80f7a7f782b42a90b4ff7c9f52b0 Mon Sep 17 00:00:00 2001 From: argszero Date: Fri, 14 Aug 2026 12:25:10 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat(ui):=20=E4=B8=8A=E6=9E=B6=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=8E=BB=E6=8E=89=E6=89=8B=E5=B7=A5=E5=8D=95=E4=BB=B7?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E4=B8=BA=E6=8C=89=E6=A8=A1=E5=9E=8B=E5=AE=9A?= =?UTF-8?q?=E4=BB=B7=E8=87=AA=E5=8A=A8=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 宿主 rant 2026-08-14T12:23:14:单价是模型×厂商的客观属性, 分享者只填声明额度;选择模型后自动显示参考单价(输出价点数/1M), 无定价数据时给出默认价兜底。种子数据 SHARINGS price 同步为自动值。 --- ui/README.md | 3 ++- ui/css/style.css | 13 +++++++++++++ ui/index.html | 4 ++-- ui/js/app.js | 31 +++++++++++++++++++++++++++---- ui/js/data.js | 8 ++++---- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/ui/README.md b/ui/README.md index 481010c..62c08dd 100644 --- a/ui/README.md +++ b/ui/README.md @@ -34,7 +34,7 @@ python3 -m http.server 8000 --directory ui 1. 仪表盘 Dashboard — 点数余额、本月用量、共享收益、最近交易 2. 模型市场 Marketplace — 模型浏览、搜索、厂商筛选、排序(按价格/上下文) -3. 共享管理 Sharing — 上架 key 表单(可填写并提交)、我的共享列表(暂停/恢复/重新上架) +3. 共享管理 Sharing — 上架 key 表单(分享者只填声明额度,单价由平台按模型定价自动计算并展示参考价)、我的共享列表(暂停/恢复/重新上架) 4. 钱包 Wallet — 点数余额、充值/提现占位入口、收支明细(分页) 5. 交易记录 Transactions — 消费/收益/充值/提现,Tab 筛选 + 分页 6. 设置 Settings — 账户、API Key 管理(可生成)、通知、偏好 @@ -54,4 +54,5 @@ ui/ ## 数据说明 - 点数规则:1 USD = 1000 点;模型价格折算自 `data/models.example.json`(CNY 按 ~7.2 汇率示例折算) +- 上架单价不由分享者手填:单价是模型×厂商的客观属性,平台按模型价格表自动计算(参考单价 = 该模型输出价 点数/1M);模型无定价数据时给出"按默认价"兜底,不报错 - 全部数据为前端常量(mock),无后端、无真实调用 diff --git a/ui/css/style.css b/ui/css/style.css index 6925aae..cb27c24 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -77,6 +77,19 @@ body { transition: border-color .15s; } .input:focus { border-color: var(--accent); } + +/* 自动参考单价(只读展示,由平台按模型定价计算) */ +.price-view { + display: flex; + align-items: center; + min-height: 38px; + padding: 9px 12px; + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + background: var(--bg-soft); + color: var(--text-dim); + font-size: 13px; +} select.input option { background: var(--bg-card); } .card { diff --git a/ui/index.html b/ui/index.html index da4f2b2..e1951cb 100644 --- a/ui/index.html +++ b/ui/index.html @@ -131,8 +131,8 @@

上架新 key List a key

-