Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ python3 -m http.server 8000 --directory ui
1. 仪表盘 Dashboard — 点数余额、本月用量、共享收益、最近交易、点数来源(分组展示有效期:赠送当日有效 / 收益与充值永久)
2. 模型市场 Marketplace — 模型浏览、搜索、厂商筛选、排序(按价格/上下文);可用性标注「多 key · 自动故障转移」(该模型配置多个上游 key,架构 v0.2 路由策略);每行「使用 / 消费」入口 → 聊天 Mock 模拟调用(按模型输出参考价扣小数点数,如 -0.38 点,产生消费交易;余额不足时阻断并提示)
3. 共享管理 Sharing — 默认只显示统计 + 我的共享列表(key 脱敏展示,可暂停/恢复/重新上架/彻底删除);点击"+ 添加 / 上架新 key"展开上架表单(须填 API Key,平台加密托管;分享者只填声明额度,单价由平台按模型定价自动计算并展示参考价),提交成功或取消后自动收起
4. 钱包 Wallet — 点数余额、点数来源(每日赠送 1 点 · 有效期至今日 · 连续第 N 天 / 共 10 天;收益永久;充值永久,充值暂未开放)、充值/提现占位入口;收支明细已去重,统一到【交易记录】(页内提供跳转提示)
4. 钱包 Wallet — 点数余额、点数来源(每日赠送 1 点 · 有效期至今日 · 连续第 N 天 / 共 10 天;收益永久;充值永久)、**充值入口(US-4:模拟流程——输入点数 → 余额增加 → topup 交易记录,永久有效点数;文案注明"演示,真实支付后续接入";提现仍 disabled)**、**申请加额(US-20:企业成员余额低时申请更多点数 → 提交后等待管理员审批,与组织设置开关联动)**;收支明细已去重,统一到【交易记录】(页内提供跳转提示)
5. 交易记录 Transactions — 消费/收益/充值/提现/赠送(gift)唯一明细入口,Tab 筛选 + MRT 风格表格(列排序/列筛选/分页,与 Tab 叠加生效)
6. 设置 Settings — 账户、API Key 管理(完整增删改查:生成带名字、改名、删除需确认「删除后该 key 立即失效」、按名字搜索、一键复制完整 key——列表脱敏展示 atk_live_****xxxx,复制为完整值;file:// 下 clipboard API 受限时降级为选中+Ctrl/Cmd+C)、通知、偏好
7. 管理视图 Admin(管理员 / 运营者角色专属)— Key 池管理(添加/撤销)、成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;保留组织设置卡片,含"关闭外部注册"企业部署开关)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱搜索定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)
7. 管理视图 Admin(管理员 / 运营者角色专属)— Key 池管理(添加/撤销)、成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门;**加额申请审批(US-20:待审批列表 → 批准=成员余额+申请点数 / 驳回)**)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;保留组织设置卡片,含"关闭外部注册"企业部署开关与"成员自助申请加额需管理员审批"开关——关掉后成员申请直接生效)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱搜索定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)

## 文件结构

Expand All @@ -59,7 +59,7 @@ ui/
## 数据说明

- 点数规则:**1 点 ≈ 1 元人民币(CNY)**;CNY 模型直接 1 元 = 1 点,USD 模型按 ~7.2 汇率折算为人民币点数;消费点数可为小数(金额保留 2 位小数)
- 点数机制(v1.7):每日赠送 1 点(当日有效,需当天登录领取,未登录日不发不积累;注册起连续 10 个自然日),钱包显示"今日赠送 +1 · 有效期至今日 · 连续第 N 天 / 共 10 天",先扣有有效期的赠送点数;分享收益与未来充值点数永久有效;**分享收益扣 10% 平台分成(收益 = 消费点数 × 90%)**;公共版当前无充值渠道(充值/提现按钮置灰"暂不支持")
- 点数机制(v1.7):每日赠送 1 点(当日有效,需当天登录领取,未登录日不发不积累;注册起连续 10 个自然日),钱包显示"今日赠送 +1 · 有效期至今日 · 连续第 N 天 / 共 10 天",先扣有有效期的赠送点数;分享收益与未来充值点数永久有效;**分享收益扣 10% 平台分成(收益 = 消费点数 × 90%)**;充值(US-4)为演示模拟流程(真实支付通道后续接入),提现暂不支持
- 上架单价不由分享者手填:单价是模型×厂商的客观属性,平台按模型价格表自动计算(参考单价 = 该模型输出价 点数/1M);模型无定价数据时给出"按默认价"兜底,不报错
- 市场模型带 `multi` 标记:表示该模型配置多个上游 key → 标注「多 key · 自动故障转移」(见 `docs/architecture.md` v0.2 路由策略);消费模拟(聊天 Mock)按 输出参考价 × 0.19M tokens 计费,扣减小数点数并产生 consume 交易(US-6)
- 上架需提交 API Key(password 输入):平台加密托管、仅用于代理调用;共享列表只展示脱敏值(如 sk-****1234,前 3 后 4),不展示明文
Expand Down
6 changes: 6 additions & 0 deletions ui/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ select.input option { background: var(--bg-card); }
}
.modal-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.modal-head h3 { font-size: 16px; margin: 0; }

/* 模态框操作行 / 充值预设(US-4) */
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px; }
.topup-presets { display: flex; gap: 8px; margin-top: 6px; }
.topup-presets .btn { min-width: 72px; }
.topup-presets .btn.active { background: var(--accent-soft); border-color: rgba(78,205,196,.5); color: var(--accent-text); }
.chat-log {
flex: 1; overflow-y: auto;
background: var(--bg-soft); border: 1px solid var(--border);
Expand Down
61 changes: 58 additions & 3 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ <h3>点数余额 <span class="en">Points balance</span></h3>
<p>1 点 ≈ 1 元 <span class="en">(1 point ≈ 1 CNY)</span> · 消费点数可为小数</p>
</div>
<div class="wallet-actions">
<button class="btn btn-primary" id="topup-btn" disabled>充值 <span class="en">Top up</span></button>
<button class="btn btn-primary" id="topup-btn">充值 <span class="en">Top up</span></button>
<button class="btn btn-ghost" id="raise-btn">申请加额 <span class="en">Request more</span></button>
<button class="btn btn-ghost" id="withdraw-btn" disabled>提现 <span class="en">Withdraw</span></button>
<span class="muted wallet-note">充值 / 提现暂不支持(即将上线)</span>
<span class="muted wallet-note">充值(演示流程,真实支付后续接入) · 提现暂不支持</span>
</div>
</div>
<div class="card">
Expand Down Expand Up @@ -315,6 +316,10 @@ <h2>管理视图 <span class="en">Admin</span></h2>
</table>
</div>
</div>
<div class="card">
<h3>加额申请 <span class="en">Raise requests</span> <span class="muted" style="font-weight:400;font-size:12px">(US-20:成员申请 → 管理员批准 / 驳回)</span></h3>
<div id="raise-requests"></div>
</div>
</div>

<div class="admin-pane hidden" data-admin-pane="usage">
Expand Down Expand Up @@ -350,7 +355,7 @@ <h3>组织设置 <span class="en">Organization settings</span></h3>
<div class="form">
<label>组织名称 <input class="input" value="示例科技 Example Tech"></label>
<label>默认成员配额(点数/月)<input class="input" type="number" value="20000"></label>
<label class="check"><input type="checkbox" checked> 成员自助申请加额需管理员审批</label>
<label class="check"><input type="checkbox" id="org-approval-toggle" checked> 成员自助申请加额需管理员审批</label>
<label class="check"><input type="checkbox"> 关闭外部注册(企业内网部署)</label>
<button class="btn btn-primary">保存设置 <span class="en">(Save)</span></button>
</div>
Expand Down Expand Up @@ -396,6 +401,56 @@ <h3 id="chat-title">使用模型</h3>

<div id="toast" class="toast hidden"></div>

<!-- 充值模拟(US-4:钱包页充值入口 → 输入点数 → 余额增加 + topup 交易记录,永久有效点数) -->
<div id="topup-modal" class="modal-overlay hidden">
<div class="modal">
<div class="modal-head">
<h3>充值点数 <span class="en">Top up</span></h3>
<button type="button" class="btn btn-ghost" data-modal-close="topup-modal">✕</button>
</div>
<p class="muted">演示流程:真实支付通道后续接入。充值获得<strong>永久有效点数</strong>(1 点 ≈ 1 元)。</p>
<div class="form">
<label>选择金额(点数)
<div class="topup-presets">
<button type="button" class="btn btn-ghost" data-topup-amt="100">100</button>
<button type="button" class="btn btn-ghost" data-topup-amt="500">500</button>
<button type="button" class="btn btn-ghost" data-topup-amt="1000">1000</button>
</div>
</label>
<label>或自定义金额
<input id="topup-custom" type="number" min="1" step="0.01" class="input" placeholder="如 300 或 25.5">
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="topup-confirm">确认充值 <span class="en">(Confirm)</span></button>
<button type="button" class="btn btn-ghost" data-modal-close="topup-modal">取消 <span class="en">(Cancel)</span></button>
</div>
</div>
</div>

<!-- 成员申请加额(US-20:余额低时申请更多点数 → 等待管理员审批) -->
<div id="raise-modal" class="modal-overlay hidden">
<div class="modal">
<div class="modal-head">
<h3>申请加额 <span class="en">Request more points</span></h3>
<button type="button" class="btn btn-ghost" data-modal-close="raise-modal">✕</button>
</div>
<p class="muted">余额低时可申请更多点数,提交后等待管理员审批(US-20)。</p>
<div class="form">
<label>申请点数
<input id="raise-amount" type="number" min="1" step="1" class="input" placeholder="如 5000">
</label>
<label>申请原因
<input id="raise-reason" class="input" placeholder="如:本月配额已用完,需要继续开发">
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="raise-confirm">提交申请 <span class="en">(Submit)</span></button>
<button type="button" class="btn btn-ghost" data-modal-close="raise-modal">取消 <span class="en">(Cancel)</span></button>
</div>
</div>
</div>

<script src="js/data.js"></script>
<script src="js/app.js"></script>
</body>
Expand Down
158 changes: 157 additions & 1 deletion ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,132 @@
renderPointSources();
}

/* --- 充值模拟(US-4:钱包充值入口 → 模拟支付 → 余额增加 + topup 交易,永久有效点数) --- */

function openTopup() {
$("#topup-custom").value = "";
$$("#topup-modal .topup-presets .btn").forEach((b) => b.classList.remove("active"));
$("#topup-modal").classList.remove("hidden");
$("#topup-custom").focus();
}

function closeTopup() {
$("#topup-modal").classList.add("hidden");
}

function confirmTopup() {
const preset = document.querySelector("#topup-modal .topup-presets .btn.active");
const customRaw = $("#topup-custom").value;
let amt;
if (preset && !customRaw) amt = Number(preset.dataset.topupAmt);
else {
const raw = String(customRaw).trim();
amt = Math.round(Number(raw) * 100) / 100;
if (!raw || isNaN(amt) || amt <= 0) { toast("请输入大于 0 的充值点数(未生效)"); return; }
}
D.USER.balance = Math.round((D.USER.balance + amt) * 100) / 100;
const top = D.POINT_SOURCES.find((s) => s.kind === "topup");
if (top) top.pts = Math.round((top.pts + amt) * 100) / 100;
D.TRANSACTIONS.unshift({
id: Date.now(), time: nowTime(), type: "topup", partner: "—",
detail: "充值 · 模拟支付(永久有效 · 演示,真实支付后续接入)", tokens: "—", pts: +amt, status: "成功",
});
$("#side-balance").textContent = D.fmt(D.USER.balance);
renderWallet();
closeTopup();
toast("充值成功 +" + D.fmt(amt) + " 点(永久有效 · 演示)");
}

/* --- 成员申请加额(US-20:余额低时申请更多点数 → 管理员审批开关联动) --- */

function openRaise() {
$("#raise-amount").value = "";
$("#raise-reason").value = "";
$("#raise-modal").classList.remove("hidden");
$("#raise-amount").focus();
}

function closeRaise() {
$("#raise-modal").classList.add("hidden");
}

function confirmRaise() {
const rawAmt = String($("#raise-amount").value).trim();
const reason = String($("#raise-reason").value).trim();
const amt = Number(rawAmt);
if (!rawAmt || !Number.isInteger(amt) || amt <= 0) { toast("请输入正整数申请点数(未生效)"); return; }
if (!reason) { toast("请填写申请原因(未生效)"); return; }
// 与组织设置「成员自助申请加额需管理员审批」开关联动
const needApproval = $("#org-approval-toggle") && $("#org-approval-toggle").checked;
if (needApproval) {
D.RAISE_REQUESTS.unshift({
id: Date.now(), user: D.USER.name, email: D.USER.email, amount: amt, reason, status: "pending", time: nowTime(),
});
closeRaise();
toast("已提交申请 +" + D.fmt(amt) + " 点,等待管理员审批");
} else {
D.USER.balance = Math.round((D.USER.balance + amt) * 100) / 100;
D.TRANSACTIONS.unshift({
id: Date.now(), time: nowTime(), type: "topup", partner: "管理员",
detail: "加额 · 申请直接生效(审批开关已关闭)", tokens: "—", pts: +amt, status: "成功",
});
$("#side-balance").textContent = D.fmt(D.USER.balance);
renderWallet();
closeRaise();
toast("审批开关已关闭,申请直接生效 +" + D.fmt(amt) + " 点");
}
}

/* --- 管理员:加额申请审批(US-20) --- */

const RAISE_STATUS = {
pending: { text: "待审批", cls: "warn" },
approved: { text: "已批准", cls: "ok" },
rejected: { text: "已驳回", cls: "dim" },
};

function renderRaiseRequests() {
const el = $("#raise-requests");
if (!el) return;
el.innerHTML = D.RAISE_REQUESTS.length ? '<div class="table-wrap compact"><table class="table"><thead><tr><th>成员</th><th>申请点数</th><th>原因</th><th>状态</th><th></th></tr></thead><tbody>' +
D.RAISE_REQUESTS.map((r, i) =>
"<tr><td><strong>" + esc(r.user) + "</strong><div class='muted' style='font-size:11px'>" + esc(r.email) + "</div></td>" +
"<td>+" + D.fmt(r.amount) + " 点</td>" +
"<td>" + esc(r.reason) + "</td>" +
"<td>" + badge(r.status, RAISE_STATUS) + "</td>" +
"<td>" + (r.status === "pending"
? "<button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-raise-approve='" + i + "'>批准</button> " +
"<button class='btn btn-danger' style='padding:4px 10px;font-size:12px' data-raise-reject='" + i + "'>驳回</button>"
: '<span class="muted" style="font-size:12px">' + esc(r.time) + "</span>") + "</td></tr>"
).join("") + "</tbody></table></div>"
: '<p class="muted">暂无加额申请</p>';
}

function approveRaise(i) {
const r = D.RAISE_REQUESTS[i];
if (!r || r.status !== "pending") return;
r.status = "approved";
// 批准 → 成员余额 + 申请点数(演示:若为当前登录用户则同步 D.USER.balance)
if (r.email === D.USER.email) {
D.USER.balance = Math.round((D.USER.balance + r.amount) * 100) / 100;
D.TRANSACTIONS.unshift({
id: Date.now(), time: nowTime(), type: "topup", partner: "管理员",
detail: "加额 · 管理员批准申请(" + r.reason + ")", tokens: "—", pts: +r.amount, status: "成功",
});
$("#side-balance").textContent = D.fmt(D.USER.balance);
}
renderRaiseRequests();
toast("已批准「" + r.user + "」申请 +" + D.fmt(r.amount) + " 点");
}

function rejectRaise(i) {
const r = D.RAISE_REQUESTS[i];
if (!r || r.status !== "pending") return;
r.status = "rejected";
renderRaiseRequests();
toast("已驳回「" + r.user + "」的加额申请");
}

/* --- 交易记录 --- */

const TX_TYPE = {
Expand Down Expand Up @@ -585,6 +711,7 @@
"<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-emp-dept='" + i + "'>改部门</button> " +
"<button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-emp-topup='" + i + "'>充值</button></td></tr>"
).join("");
renderRaiseRequests();
} 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));
Expand Down Expand Up @@ -942,7 +1069,28 @@
if (d) deleteSharing(Number(d.dataset.shareDelete));
});

// 钱包按钮(充值/提现 disabled + pointer-events:none,点击不生效;文案见 index.html wallet-note)
// 钱包按钮(充值:US-4 模拟流程;申请加额:US-20;提现仍 disabled)
$("#topup-btn").addEventListener("click", openTopup);
$("#raise-btn").addEventListener("click", openRaise);
$("#topup-confirm").addEventListener("click", confirmTopup);
$("#raise-confirm").addEventListener("click", confirmRaise);
$$("#topup-modal .topup-presets .btn").forEach((b) =>
b.addEventListener("click", () => {
$$("#topup-modal .topup-presets .btn").forEach((x) => x.classList.remove("active"));
b.classList.add("active");
$("#topup-custom").value = "";
})
);
$$("[data-modal-close]").forEach((b) =>
b.addEventListener("click", () => {
const id = b.dataset.modalClose;
const m = document.getElementById(id);
if (m) m.classList.add("hidden");
})
);
["topup-modal", "raise-modal"].forEach((id) => {
document.getElementById(id).addEventListener("click", (e) => { if (e.target === document.getElementById(id)) document.getElementById(id).classList.add("hidden"); });
});
// 钱包页提示 → 跳转交易记录(明细统一入口)
$("#wallet-goto-tx").addEventListener("click", () => switchView("transactions"));

Expand Down Expand Up @@ -1019,6 +1167,14 @@
toast("已给 " + emp.name + " 充值 " + D.fmt(amt) + " 点");
});

// 加额申请审批(US-20:批准 → 成员余额+申请点数;驳回 → 仅更新状态)
$("#raise-requests").addEventListener("click", (e) => {
const ap = e.target.closest("[data-raise-approve]");
if (ap) { approveRaise(Number(ap.dataset.raiseApprove)); return; }
const rj = e.target.closest("[data-raise-reject]");
if (rj) rejectRaise(Number(rj.dataset.raiseReject));
});

// 组织管理:部门搜索 / 添加 / 编辑 / 删除(事件委托)
$("#od-search").addEventListener("input", renderAdmin);

Expand Down
Loading
Loading