AirShelf/电商AI平台/account.html
iye 04335f3269
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索
- model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框
- model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除)
- model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批)
- model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃
- model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段
- image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通
- image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64
- asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列
- pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
2026-05-22 19:35:36 +08:00

879 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>消费 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<style>
/* ─── 顶部:左右布局(余额 banner + 快速充值)─── */
.top-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
@media (max-width: 980px) { .top-grid { grid-template-columns: 1fr; } }
.balance-banner {
background: var(--accent-black);
color: var(--accent-white);
padding: 24px 28px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
display: flex;
flex-direction: column;
gap: 22px;
min-width: 0;
}
.balance-banner::before, .balance-banner::after,
.balance-banner > .corner-tr, .balance-banner > .corner-bl {
content: ''; position: absolute; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
background-size: contain; pointer-events: none;
}
.balance-banner::before { top: -7px; left: -7px; }
.balance-banner::after { bottom: -7px; right: -7px; }
.balance-banner > .corner-tr { top: -7px; right: -7px; }
.balance-banner > .corner-bl { bottom: -7px; left: -7px; }
/* 主余额 · 突出展示 */
.balance-hero { display: flex; flex-direction: column; gap: 4px; }
.balance-hero .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.balance-hero .v { font-size: 38px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.balance-hero .meta { font-size: 11.5px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; }
/* 子统计 · 月限额 / 已用 */
.balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 14px 0 0; border-top: 1px solid rgba(255,255,255,.1); }
.balance-sub .col { min-width: 0; }
.balance-sub .lbl { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,.5); letter-spacing: .06em; text-transform: uppercase; }
.balance-sub .v { font-size: 18px; font-weight: 700; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; }
.balance-sub .meta { font-size: 10.5px; color: rgba(255,255,255,.42); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
.balance-actions { display: flex; gap: 8px; margin-top: auto; }
.balance-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); flex: 1; }
.balance-actions .btn:hover { background: var(--background-base); }
.balance-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); flex: 1; }
.balance-actions .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
/* 导出菜单 */
.balance-actions .export-menu {
position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
min-width: 220px;
background: var(--surface); color: var(--accent-black);
border: 1px solid var(--border-faint); border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.16);
padding: 6px;
display: flex; flex-direction: column; gap: 2px;
}
.balance-actions .export-menu[hidden] { display: none; }
.balance-actions .export-menu button {
background: transparent; border: 0; padding: 8px 10px;
display: grid; grid-template-columns: 22px 1fr; gap: 8px;
align-items: center; cursor: pointer;
border-radius: var(--r-sm);
font-family: inherit; font-size: 13px; color: var(--accent-black);
text-align: left;
}
.balance-actions .export-menu button:hover { background: var(--background-lighter); }
.balance-actions .export-menu button .ic {
width: 22px; height: 22px; display: grid; place-items: center;
font-family: var(--font-mono); color: var(--heat); font-size: 14px; font-weight: 700;
background: var(--heat-12); border-radius: var(--r-sm);
}
.balance-actions .export-menu button .t { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.balance-actions .export-menu button .t .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 2px; }
/* ─── 快速充值 pane(右栏)─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.pane .desc { font-size: 11.5px; color: var(--black-alpha-48); margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
.topup-pane { display: flex; flex-direction: column; padding: 20px 22px; margin-bottom: 0; }
.recharge-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.recharge-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; text-align: center; cursor: pointer; background: var(--surface); position: relative; transition: background var(--t-base), border-color var(--t-base); }
.recharge-card:hover { background: var(--background-lighter); }
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); }
.recharge-card.selected::before, .recharge-card.selected::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23FA5D19'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
.recharge-card.selected::before { top: -7px; left: -7px; }
.recharge-card.selected::after { bottom: -7px; right: -7px; }
.recharge-card .amt { font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; }
.recharge-card .gift { font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); }
.recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }
.recharge-card .ribbon { position: absolute; top: -8px; right: 8px; font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--heat); color: var(--accent-white); letter-spacing: .04em; font-weight: 600; border-radius: var(--r-sm); }
.pay-row { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 14px; }
.pay-row .input { width: 100%; box-sizing: border-box; height: 38px; }
.pay-btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pay-btn-row .btn { width: 100%; }
/* ─── Tab strip ─── */
.billing-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 26px 0 18px; padding: 0 2px; }
.billing-tabs .tab { background: none; border: 0; padding: 10px 16px 11px; font-size: 13px; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; border-bottom: 2px solid transparent; position: relative; top: 1px; letter-spacing: .01em; transition: color var(--t-base); }
.billing-tabs .tab:hover { color: var(--accent-black); }
.billing-tabs .tab.active { color: var(--accent-black); font-weight: 600; border-bottom-color: var(--heat); }
.billing-tabs .tab .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-32); margin-left: 4px; letter-spacing: .02em; font-weight: 400; }
.billing-tabs .tab.active .mono { color: var(--heat); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ─── 总览 · 趋势 + 阶段分布 (两栏等高) ─── */
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }
.trend-pane { padding: 18px 20px 14px; display: flex; flex-direction: column; }
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
.trend-head h3 { margin-bottom: 0; }
.trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.trend-head .spacer { flex: 1; }
.trend-head .chip { font-family: var(--font-mono); font-size: 10.5px; padding: 3px 8px; border: 1px solid var(--border-faint); border-radius: var(--r-pill); color: var(--black-alpha-56); cursor: pointer; }
.trend-head .chip.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; min-height: 170px; flex: 1; padding: 6px 4px 2px; position: relative; }
.trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }
.trend-chart .bar { background: var(--background-lighter); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; }
.trend-chart .bar > span { display: block; width: 100%; background: var(--heat); border-radius: 2px 2px 0 0; }
.trend-chart .bar:hover > span { background: var(--accent-black); }
.trend-chart .bar.peak > span { background: var(--accent-black); }
.trend-chart .x-axis { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-32); text-align: center; letter-spacing: .02em; }
.trend-foot { display: flex; gap: 14px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-faint); font-size: 12px; }
.trend-foot .item { display: flex; align-items: baseline; gap: 6px; }
.trend-foot .item .k { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
.trend-foot .item .v { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-black); }
.trend-foot .item .v.warn { color: #B45309; }
/* ─── 阶段分布 ─── */
.stage-pane .usage-line { display: flex; justify-content: space-between; padding: 4px 0 4px; font-size: 12.5px; }
.stage-pane .usage-line .k { color: var(--accent-black); }
.stage-pane .usage-line .v { font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }
.stage-pane .usage-bar { height: 4px; background: var(--background-lighter); border-radius: 2px; margin: 4px 0 10px; overflow: hidden; }
.stage-pane .usage-bar > span { display: block; height: 100%; transition: width .3s ease; }
.stage-pane .total { display: flex; justify-content: space-between; padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--border-faint); font-size: 13px; font-weight: 600; }
.stage-pane .total .v { font-variant-numeric: tabular-nums; }
/* ─── 扣费规则 + 四层预检 ─── */
.rule-pane .rule-list { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7; }
.rule-pane .rule-list strong { color: var(--accent-black); font-weight: 600; }
.rule-pane .mono-acc { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; font-size: 11.5px; border-radius: var(--r-sm); }
.quota-rules { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-faint); }
.quota-rules .qr-head { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 10px; }
.quota-rules .step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: baseline; margin-bottom: 6px; font-size: 12.5px; color: var(--accent-black); }
.quota-rules .step .num { width: 20px; height: 20px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; }
.quota-rules .step .formula { font-family: var(--font-mono); font-size: 11.5px; color: var(--heat); background: var(--heat-12); padding: 0 4px; border-radius: var(--r-sm); }
/* ─── 表格通用 ─── */
.billing-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.billing-table th, .billing-table td { padding: 11px 14px; text-align: left; font-size: 12.5px; border-bottom: 1px solid var(--border-faint); }
.billing-table thead th { background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.billing-table tbody tr:last-child td { border-bottom: 0; }
.billing-table tbody tr:hover { background: var(--background-lighter); }
.billing-table .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.billing-table .neg { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-black); text-align: right; }
.billing-table .pos { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-forest); text-align: right; }
.billing-table .zero { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--black-alpha-32); text-align: right; }
.billing-table .muted { color: var(--black-alpha-56); font-size: 11.5px; }
.billing-table .ref { color: var(--black-alpha-48); font-size: 10.5px; font-family: var(--font-mono); }
.billing-table .who { display: inline-flex; align-items: center; gap: 8px; }
.billing-table .who .av { width: 24px; height: 24px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: inline-grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--accent-black); }
.billing-table .role-pill { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: var(--r-pill); font-size: 10.5px; font-weight: 500; }
.billing-table .role-pill .dot { width: 5px; height: 5px; border-radius: 50%; }
.billing-table .role-super { background: var(--heat-12); color: var(--heat); }
.billing-table .role-super .dot { background: var(--heat); }
.billing-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
.billing-table .role-admin .dot { background: #1E40AF; }
.billing-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
.billing-table .role-member .dot { background: var(--black-alpha-56); }
.billing-table .status-tag { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; border-radius: var(--r-sm); letter-spacing: .04em; }
.billing-table .status-tag.ok { background: rgba(66,195,102,.12); color: var(--accent-forest); }
.billing-table .status-tag.wip { background: var(--heat-12); color: var(--heat); }
.billing-table .status-tag.fail { background: rgba(235,52,36,.10); color: var(--accent-crimson); }
.billing-table .progress-mini { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 8px; }
.billing-table .progress-mini > span { display: block; height: 100%; background: var(--heat); }
/* ─── 流水筛选条 ─── */
.filter-bar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.filter-bar select, .filter-bar input { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12.5px; font-family: inherit; color: var(--accent-black); }
.filter-bar select { padding-right: 24px; }
.filter-bar .spacer { flex: 1; }
.filter-bar .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* 充值 modal */
.topup-modal { width: min(460px, 92vw); }
.topup-modal .topup-qr { aspect-ratio: 1; max-width: 220px; margin: 4px auto 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; position: relative; overflow: hidden; }
.topup-modal .topup-qr::before {
content: ''; position: absolute; inset: 18px;
background-image: linear-gradient(45deg, var(--accent-black) 25%, transparent 25%),
linear-gradient(-45deg, var(--accent-black) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--accent-black) 75%),
linear-gradient(-45deg, transparent 75%, var(--accent-black) 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
opacity: .14;
}
.topup-modal .topup-qr .center { position: relative; z-index: 1; background: var(--surface); padding: 10px 12px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .02em; text-align: center; }
.topup-modal .topup-info { text-align: center; font-size: 13px; color: var(--black-alpha-56); margin-bottom: 6px; }
.topup-modal .topup-amt { text-align: center; font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--heat); margin-bottom: 6px; }
.topup-modal .topup-note { text-align: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; margin-bottom: 4px; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>消费</h1>
<div class="sub"><span class="mono">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div>
</div>
</div>
<!-- 顶部:余额 banner(左)+ 快速充值(右)左右布局 -->
<div class="top-grid">
<!-- 左:余额 banner -->
<div class="balance-banner">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="balance-hero">
<div class="lbl">团队余额</div>
<div class="v">¥327.40</div>
<div class="meta">// 充值累加 · 不重置</div>
</div>
<div class="balance-sub">
<div class="col">
<div class="lbl">本月限额</div>
<div class="v">¥3,000.00</div>
<div class="meta">// 按自然月重置</div>
</div>
<div class="col">
<div class="lbl">当月已用</div>
<div class="v">¥162.60</div>
<div class="meta">// 占比 5.4% · 健康</div>
</div>
</div>
<div class="balance-actions">
<button class="btn btn-lg" onclick="openTopup()">充值</button>
<div class="export-wrap" style="flex:1; position:relative;">
<button class="btn btn-ghost btn-lg" id="export-trigger" type="button" style="width:100%;">
导出账单
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px; vertical-align:-1px;"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="export-menu" id="export-menu" hidden>
<button type="button" data-fmt="csv"><span class="ic"></span><span class="t">CSV<span class="d">// 全部明细 · Excel 兼容</span></span></button>
<button type="button" data-fmt="xlsx"><span class="ic"></span><span class="t">XLSX<span class="d">// 含格式 + 公式</span></span></button>
<button type="button" data-fmt="pdf"><span class="ic"></span><span class="t">PDF<span class="d">// 含发票抬头</span></span></button>
</div>
</div>
</div>
</div>
<!-- 右:快速充值 -->
<div class="pane topup-pane">
<h3>快速充值</h3>
<div class="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
<div class="recharge-row">
<div class="recharge-card" data-amt="100"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
<div class="recharge-card selected" data-amt="500"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
<div class="recharge-card" data-amt="1000"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
<div class="recharge-card" data-amt="3000"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
</div>
<div class="pay-row">
<input class="input" id="custom-amt" placeholder="自定义金额(最低 ¥50)">
<div class="pay-btn-row">
<button class="btn btn-primary" onclick="openTopup('wechat')">微信支付</button>
<button class="btn" onclick="openTopup('alipay')">支付宝</button>
</div>
</div>
</div>
</div>
<!-- Tab strip -->
<div class="billing-tabs" role="tablist">
<button class="tab active" data-tab="overview" role="tab">总览 <span class="mono">// 当月趋势 + 阶段分布</span></button>
<button class="tab" data-tab="by-project" role="tab">按项目 <span class="mono">// 8 个</span></button>
<button class="tab" data-tab="by-member" role="tab">按成员 <span class="mono">// 5 人</span></button>
<button class="tab" data-tab="bills" role="tab">账单流水 <span class="mono">// 30 天</span></button>
</div>
<!-- ===== Tab 1: 总览 ===== -->
<div class="tab-panel active" id="panel-overview">
<div class="overview-grid">
<div class="pane trend-pane">
<div class="trend-head">
<h3>消费趋势</h3>
<span class="sub" id="trend-sub">// 近 14 天 · 单位 ¥</span>
<span class="spacer"></span>
<button class="chip active" data-grain="day"></button>
<button class="chip" data-grain="week"></button>
<button class="chip" data-grain="month"></button>
</div>
<div class="trend-chart">
<div class="bars" id="trend-bars">
<!-- JS 注入 14 根柱 -->
</div>
<div class="x-axis" id="trend-xaxis">
<!-- JS 注入日期 -->
</div>
</div>
<div class="trend-foot">
<div class="item"><span class="k">14 天合计</span><span class="v" id="trend-sum">¥0.00</span></div>
<div class="item"><span class="k">日均</span><span class="v" id="trend-avg">¥0.00</span></div>
<div class="item"><span class="k">峰值</span><span class="v warn" id="trend-peak">¥0.00</span></div>
</div>
</div>
<div class="pane stage-pane">
<h3>本月按阶段分布</h3>
<div class="desc">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>
<div class="usage-line"><span class="k">视频片段(Seedance)</span><span class="v">¥98.40</span></div>
<div class="usage-bar"><span style="width:60%; background:var(--heat);"></span></div>
<div class="usage-line"><span class="k">故事板(image-2)</span><span class="v">¥36.00</span></div>
<div class="usage-bar"><span style="width:22%; background:var(--accent-forest);"></span></div>
<div class="usage-line"><span class="k">基础资产</span><span class="v">¥21.00</span></div>
<div class="usage-bar"><span style="width:13%; background:var(--black-alpha-56);"></span></div>
<div class="usage-line"><span class="k">脚本 LLM</span><span class="v">¥7.20</span></div>
<div class="usage-bar"><span style="width:5%; background:var(--black-alpha-32);"></span></div>
<div class="total"><span>合计</span><span class="v">¥162.60</span></div>
</div>
</div>
<div class="pane rule-pane" style="margin-top: 16px;">
<h3>扣费 + 四层额度预检规则</h3>
<div class="desc">// PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>
<div class="rule-list">
<strong>① 失败不扣</strong>:模型超时 / 内容审核拦截 / 生成异常一律不扣费。<br>
<strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br>
<strong>③ 仅在你点击 <span class="mono-acc">[ 确认通过 ]</span> 时入账</strong><br>
<strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。
</div>
<div class="quota-rules">
<div class="qr-head">// 任务确认前 · 四层额度预检(任一不通过即拦截)</div>
<div class="step"><span class="num">1</span><span><strong>个人日剩余</strong> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
<div class="step"><span class="num">2</span><span><strong>个人月剩余</strong> ≥ 同上</span></div>
<div class="step"><span class="num">3</span><span><strong>团队月剩余</strong> ≥ 同上</span></div>
<div class="step"><span class="num">4</span><span><strong>团队总余额</strong> ≥ 同上</span></div>
</div>
</div>
</div>
<!-- ===== Tab 2: 按项目 ===== -->
<div class="tab-panel" id="panel-by-project">
<div class="filter-bar">
<select id="proj-f-status">
<option value="all">全部状态</option>
<option value="wip">进行中</option>
<option value="ok">已完成</option>
<option value="fail">失败 · 待重跑</option>
</select>
<select id="proj-f-range">
<option value="month">本月</option>
<option value="30d">近 30 天</option>
<option value="90d">近 90 天</option>
</select>
<button class="chip" id="proj-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct" id="proj-count"><b style="color:var(--accent-black);">0</b> 个项目 · 消耗 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
<tr>
<th>项目</th>
<th>商品</th>
<th>所属成员</th>
<th>当前阶段</th>
<th>状态</th>
<th style="text-align:right;">消耗</th>
</tr>
</thead>
<tbody id="proj-body">
<!-- JS 注入 -->
</tbody>
</table>
<div id="proj-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有项目 · 试试调整状态 / 时间范围
</div>
</div>
<!-- ===== Tab 3: 按成员 ===== -->
<div class="tab-panel" id="panel-by-member">
<div class="filter-bar">
<select id="mem-f-role">
<option value="all">全部角色</option>
<option value="super">超管</option>
<option value="admin">团管</option>
<option value="member">成员</option>
</select>
<select id="mem-f-range">
<option value="month">本月</option>
<option value="30d">近 30 天</option>
<option value="90d">近 90 天</option>
</select>
<button class="chip" id="mem-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct" id="mem-count"><b style="color:var(--accent-black);">0</b> 人 · 合计 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
<tr>
<th>成员</th>
<th>角色</th>
<th>已完成项目</th>
<th>已用 / 月度额度</th>
<th>最近活跃</th>
</tr>
</thead>
<tbody id="member-body">
<!-- JS 注入 -->
</tbody>
</table>
<div id="mem-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有成员 · 试试调整角色 / 时间范围
</div>
</div>
<!-- ===== Tab 4: 账单流水 ===== -->
<div class="tab-panel" id="panel-bills">
<div class="filter-bar">
<select id="bills-f-stage">
<option value="all">全部阶段</option>
<option value="视频片段">视频片段</option>
<option value="故事板">故事板</option>
<option value="基础资产">基础资产</option>
<option value="脚本 LLM">脚本 LLM</option>
<option value="充值">充值</option>
<option value="导出">导出</option>
</select>
<select id="bills-f-member">
<option value="all">全部成员</option>
<option value="李">小李</option>
<option value="张">张运营</option>
<option value="王">王小姐</option>
<option value="陈">陈策划</option>
</select>
<select id="bills-f-range">
<option value="30d">近 30 天</option>
<option value="7d">近 7 天</option>
<option value="month">本月</option>
<option value="all">全部</option>
</select>
<button class="chip" id="bills-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct"><b id="bills-count" style="color:var(--accent-black);">0</b></span>
</div>
<table class="billing-table">
<thead>
<tr>
<th>时间</th>
<th>项目 / 类型</th>
<th>详情</th>
<th>成员</th>
<th>状态</th>
<th style="text-align:right;">金额</th>
</tr>
</thead>
<tbody id="bills-body">
<!-- JS 注入 -->
</tbody>
</table>
<div id="bills-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有账单 · 试试调整阶段 / 成员 / 时间范围
</div>
</div>
<!-- 充值 modal -->
<div class="modal-bg" id="topup-bg" onclick="if(event.target===this)Shell.closeModal('topup-bg')">
<div class="modal topup-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
</div>
<div class="ti">扫码支付<span id="topup-channel-label">// 微信支付</span></div>
</div>
<div class="modal-b">
<div class="topup-info">支付金额</div>
<div class="topup-amt" id="topup-amt">¥500.00</div>
<div class="topup-note" id="topup-bonus">// 含 ¥30 赠送 · 实到账 ¥530</div>
<div class="topup-qr">
<div class="center" id="topup-channel-name">微信扫码<br><span style="color:var(--black-alpha-32);">/topup/wx/TX...</span></div>
</div>
<div style="text-align:center; font-family:var(--font-mono); font-size:11px; color:var(--black-alpha-48); letter-spacing:.02em;">// 5 分钟内有效 · 到账后自动关闭</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('topup-bg')">取消</button>
<button class="btn btn-primary" type="button" onclick="topupDone()">已完成支付</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消费' }] });
/* ============================================================
Mock 数据
============================================================ */
const TREND_DAYS = [
{ d: '05.08', v: 12.40 }, { d: '05.09', v: 18.80 }, { d: '05.10', v: 6.20 }, { d: '05.11', v: 0 },
{ d: '05.12', v: 4.50 }, { d: '05.13', v: 22.10 }, { d: '05.14', v: 14.60 }, { d: '05.15', v: 9.30 },
{ d: '05.16', v: 28.40 }, { d: '05.17', v: 13.80 }, { d: '05.18', v: 8.20 }, { d: '05.19', v: 11.50 },
{ d: '05.20', v: 19.40 }, { d: '05.21', v: 7.80 },
];
// 8 周(以「W18~W21 + 前 4 周」做演示),按 7 天合计估算
const TREND_WEEKS = [
{ d: 'W14', v: 64.20 }, { d: 'W15', v: 92.80 }, { d: 'W16', v: 118.40 }, { d: 'W17', v: 78.60 },
{ d: 'W18', v: 102.30 }, { d: 'W19', v: 138.20 }, { d: 'W20', v: 86.40 }, { d: 'W21', v: 27.20 },
];
// 6 个月 demo,自然月聚合估算 + 当月真实合计 162.60
const TREND_MONTHS = [
{ d: '2025-12', v: 384.20 }, { d: '2026-01', v: 528.40 }, { d: '2026-02', v: 296.80 },
{ d: '2026-03', v: 412.00 }, { d: '2026-04', v: 348.60 }, { d: '2026-05', v: 162.60 },
];
const TREND_DATA = { day: TREND_DAYS, week: TREND_WEEKS, month: TREND_MONTHS };
const TREND_LABEL = {
day: { sub: '// 近 14 天 · 单位 ¥', sumLbl: '14 天合计', avgLbl: '日均' },
week: { sub: '// 近 8 周 · 单位 ¥', sumLbl: '8 周合计', avgLbl: '周均' },
month: { sub: '// 近 6 月 · 单位 ¥', sumLbl: '6 月合计', avgLbl: '月均' },
};
let _trendGrain = 'day';
const PROJECTS_BILL = [
{ name: '补水面膜 · v3', product: '透真补水面膜', owner: '李', role: 'super', stage: 'Stage 3 故事板', stagePct: 60, status: 'wip', statusLabel: '进行中', amount: 48.20 },
{ name: '透真防晒 · 通勤对比', product: '透真防晒', owner: '李', role: 'super', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 32.60 },
{ name: '蓝牙耳机 · 开箱测评', product: 'Pro 4 蓝牙耳机', owner: '张', role: 'admin', stage: 'Stage 4 视频', stagePct: 80, status: 'wip', statusLabel: '进行中', amount: 28.40 },
{ name: '速食面 · 加班场景', product: '熊猫速食面', owner: '陈', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 12.80 },
{ name: '春日新品 · 立体口红', product: '凝彩立体口红', owner: '李', role: 'super', stage: 'Stage 2 资产', stagePct: 40, status: 'wip', statusLabel: '进行中', amount: 18.30 },
{ name: '咖啡冻干 · 早八', product: '冷萃咖啡冻干', owner: '王', role: 'member', stage: 'Stage 3 故事板', stagePct: 60, status: 'fail', statusLabel: '失败 · 待重跑', amount: 0 },
{ name: '瑜伽裤 · 通勤穿搭', product: '透气速干瑜伽裤', owner: '王', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 14.80 },
{ name: '保温杯 · 户外随行', product: '316 保温杯', owner: '张', role: 'admin', stage: 'Stage 1 脚本', stagePct: 20, status: 'wip', statusLabel: '进行中', amount: 7.50 },
];
const MEMBERS_BILL = [
{ av: '李', name: '小李', role: 'super', projectsDone: 14, used: 162.60, monthly: 10000, lastActive: '15 分钟前' },
{ av: '张', name: '张运营', role: 'admin', projectsDone: 8, used: 98.40, monthly: 6000, lastActive: '10 分钟前' },
{ av: '王', name: '王小姐', role: 'member', projectsDone: 4, used: 45.20, monthly: 2000, lastActive: '28 分钟前' },
{ av: '陈', name: '陈策划', role: 'member', projectsDone: 1, used: 12.80, monthly: 2000, lastActive: '4 小时前' },
{ av: '林', name: '林新人', role: 'member', projectsDone: 0, used: 0.00, monthly: 2000, lastActive: '尚未激活' },
];
const BILLS = [
{ ts: '05.21 14:32', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 1 · 1 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.21 14:08', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 2 · 1 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.20 21:42', proj: '蓝牙耳机 · 开箱测评', type: '视频片段', detail: 'Seedance · 场 3 · 6 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -1.20 },
{ ts: '05.20 18:21', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 整段 · 6 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.20 },
{ ts: '05.20 16:00', proj: '蓝牙耳机 · 开箱测评', type: '故事板', detail: 'image-2 · 整张重跑 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.20 11:02', proj: '充值', type: '充值', detail: '微信支付 · TX2024052011021Z', who: '李', role: 'super', status: 'ok', statusLabel: '到账', amount: 500.00 },
{ ts: '05.19 18:08', proj: '速食面 · 加班场景', type: '故事板', detail: 'image-2 · 场 1', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.30 },
{ ts: '05.19 16:08', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 1', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.19 14:02', proj: '补水面膜 · v3', type: '脚本 LLM', detail: '2.4k tokens · AI 全生', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.04 },
{ ts: '05.19 13:38', proj: '补水面膜 · v3', type: '基础资产', detail: 'image-2 · 5 张', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.05 },
{ ts: '05.19 11:18', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.19 11:12', proj: '速食面 · 加班场景', type: '基础资产', detail: 'image-2 · 2 张', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.42 },
{ ts: '05.18 15:42', proj: '咖啡冻干 · 早八', type: '故事板', detail: 'image-2 · 场 3', who: '王', role: 'member', status: 'fail', statusLabel: '失败不扣', amount: 0 },
{ ts: '05.18 09:42', proj: '蓝牙耳机 · 开箱测评', type: '基础资产', detail: 'image-2 · 4 张', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.84 },
{ ts: '05.17 14:38', proj: '蓝牙耳机 · 开箱测评', type: '脚本 LLM', detail: '1.8k tokens · 自带粘贴', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.03 },
{ ts: '05.17 10:30', proj: '瑜伽裤 · 通勤穿搭', type: '导出', detail: '1080×1920 · 9:16 · 38s', who: '王', role: 'member', status: 'ok', statusLabel: '免费', amount: 0 },
{ ts: '05.17 10:08', proj: '瑜伽裤 · 通勤穿搭', type: '视频片段', detail: 'Seedance · 整段 · 5 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -3.20 },
{ ts: '05.16 19:38', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 4 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.80 },
{ ts: '05.16 11:42', proj: '透真防晒 · 通勤对比', type: '故事板', detail: 'image-2 · 场 2', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.45 },
{ ts: '05.15 16:08', proj: '透真防晒 · 通勤对比', type: '基础资产', detail: 'image-2 · 3 张', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.63 },
];
const ROLE_META = {
super: { label: '超管', cls: 'role-super' },
admin: { label: '团管', cls: 'role-admin' },
member: { label: '成员', cls: 'role-member' },
};
function fmtMoney(n) { return '¥' + Math.abs(n).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
function amtStr(n) {
if (n === 0) return '¥0.00';
return (n > 0 ? '+' : '-') + fmtMoney(n);
}
function amtCls(n) { return n > 0 ? 'pos' : (n === 0 ? 'zero' : 'neg'); }
/* ─── 趋势柱 ─── */
function renderTrend() {
const data = TREND_DATA[_trendGrain];
const meta = TREND_LABEL[_trendGrain];
const bars = document.getElementById('trend-bars');
const xax = document.getElementById('trend-xaxis');
const max = Math.max(...data.map(d => d.v));
// bars 的 grid-template-columns 默认是 repeat(14, 1fr),需要按数据长度动态调整
bars.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
xax.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
bars.innerHTML = data.map(d => {
const h = max > 0 ? (d.v / max * 100) : 0;
const isPeak = d.v === max;
return `<div class="bar${isPeak ? ' peak' : ''}" title="${d.d} · ${fmtMoney(d.v)}"><span style="height: ${h.toFixed(1)}%"></span></div>`;
}).join('');
// x 轴标签:日 = 隔列显示 MM·DD 的 DD 部分;周/月 = 全显示
if (_trendGrain === 'day') {
xax.innerHTML = data.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
} else if (_trendGrain === 'week') {
xax.innerHTML = data.map(d => `<span>${d.d}</span>`).join('');
} else {
xax.innerHTML = data.map(d => `<span>${d.d.slice(5)}</span>`).join('');
}
const sum = data.reduce((s, d) => s + d.v, 0);
document.getElementById('trend-sub').textContent = meta.sub;
document.getElementById('trend-sum').textContent = fmtMoney(sum);
document.getElementById('trend-avg').textContent = fmtMoney(sum / data.length);
document.getElementById('trend-peak').textContent = fmtMoney(max);
// 旁标 label
document.querySelectorAll('.trend-foot .item').forEach((it, idx) => {
const k = it.querySelector('.k');
if (idx === 0 && k) k.textContent = meta.sumLbl;
else if (idx === 1 && k) k.textContent = meta.avgLbl;
});
}
/* ─── 趋势 日/周/月 切换 ─── */
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
_trendGrain = chip.dataset.grain;
renderTrend();
});
});
/* ─── 按项目 表格 + 筛选 ─── */
const PROJ_FILTER = { status: 'all', range: 'month' };
const RANGE_MULT = { 'month': 1, '30d': 1, '90d': 2.6 }; // 演示用,30/90 天放大倍率(月 = 当月真实)
const RANGE_LBL = { 'month': '当月', '30d': '近 30 天', '90d': '近 90 天' };
function renderProjects() {
const tb = document.getElementById('proj-body');
const mult = RANGE_MULT[PROJ_FILTER.range] || 1;
const list = PROJECTS_BILL.filter(p => PROJ_FILTER.status === 'all' || p.status === PROJ_FILTER.status);
tb.innerHTML = list.map(p => {
const r = ROLE_META[p.role];
const amt = p.amount * mult;
return `
<tr>
<td><a href="pipeline.html?product=${encodeURIComponent(p.product)}" style="color:var(--accent-black);text-decoration:none;font-weight:500;">${p.name}</a></td>
<td class="muted">${p.product}</td>
<td><span class="who"><span class="av">${p.owner}</span><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></span></td>
<td><span class="muted">${p.stage}</span><span class="progress-mini"><span style="width:${p.stagePct}%"></span></span></td>
<td><span class="status-tag ${p.status}">${p.statusLabel}</span></td>
<td class="${amtCls(-amt)}">${amt === 0 ? '¥0.00' : '-' + fmtMoney(amt)}</td>
</tr>
`;
}).join('');
const sum = list.reduce((s, p) => s + p.amount * mult, 0);
document.getElementById('proj-count').innerHTML =
`共 <b style="color:var(--accent-black);">${list.length}</b> 个项目 · ${RANGE_LBL[PROJ_FILTER.range]}消耗 ${fmtMoney(sum)}`;
const empty = document.getElementById('proj-empty');
const tbl = document.querySelector('#panel-by-project .billing-table');
empty.style.display = list.length === 0 ? '' : 'none';
tbl.style.display = list.length === 0 ? 'none' : '';
const isDef = PROJ_FILTER.status === 'all' && PROJ_FILTER.range === 'month';
document.getElementById('proj-f-reset').style.display = isDef ? 'none' : '';
}
['status', 'range'].forEach(key => {
const sel = document.getElementById('proj-f-' + key);
sel.addEventListener('change', () => { PROJ_FILTER[key] = sel.value; renderProjects(); });
});
document.getElementById('proj-f-reset').addEventListener('click', () => {
PROJ_FILTER.status = 'all'; PROJ_FILTER.range = 'month';
document.getElementById('proj-f-status').value = 'all';
document.getElementById('proj-f-range').value = 'month';
renderProjects();
});
/* ─── 按成员 表格 + 筛选 ─── */
const MEM_FILTER = { role: 'all', range: 'month' };
function renderMembers() {
const tb = document.getElementById('member-body');
const mult = RANGE_MULT[MEM_FILTER.range] || 1;
const list = MEMBERS_BILL.filter(m => MEM_FILTER.role === 'all' || m.role === MEM_FILTER.role);
tb.innerHTML = list.map(m => {
const r = ROLE_META[m.role];
const used = m.used * mult;
const pct = m.monthly > 0 ? (used / m.monthly * 100) : 0;
return `
<tr style="cursor:pointer;" onclick="location.href='team.html'">
<td><span class="who"><span class="av">${m.av}</span><strong style="font-weight:500;">${m.name}</strong></span></td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td>${m.projectsDone}</td>
<td>
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(used)}</strong>
<span class="muted"> / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}%</span>
<span class="progress-mini"><span style="width:${Math.min(100, pct)}%; background:${pct >= 85 ? '#B45309' : 'var(--heat)'};"></span></span>
</td>
<td><span class="ts">${m.lastActive}</span></td>
</tr>
`;
}).join('');
const sum = list.reduce((s, m) => s + m.used * mult, 0);
document.getElementById('mem-count').innerHTML =
`共 <b style="color:var(--accent-black);">${list.length}</b> 人 · ${RANGE_LBL[MEM_FILTER.range]}合计 ${fmtMoney(sum)}`;
const empty = document.getElementById('mem-empty');
const tbl = document.querySelector('#panel-by-member .billing-table');
empty.style.display = list.length === 0 ? '' : 'none';
tbl.style.display = list.length === 0 ? 'none' : '';
const isDef = MEM_FILTER.role === 'all' && MEM_FILTER.range === 'month';
document.getElementById('mem-f-reset').style.display = isDef ? 'none' : '';
}
['role', 'range'].forEach(key => {
const sel = document.getElementById('mem-f-' + key);
sel.addEventListener('change', () => { MEM_FILTER[key] = sel.value; renderMembers(); });
});
document.getElementById('mem-f-reset').addEventListener('click', () => {
MEM_FILTER.role = 'all'; MEM_FILTER.range = 'month';
document.getElementById('mem-f-role').value = 'all';
document.getElementById('mem-f-range').value = 'month';
renderMembers();
});
/* ─── 账单流水:筛选 + 表格渲染 ─── */
const BILLS_FILTER = { stage: 'all', member: 'all', range: '30d' };
// demo "今天" = 05.21,所有 ts 都是 MM.DD 格式,这里基于这一假定算时间区间
const TODAY_MD = '05.21';
function mdToDay(md) {
// 把 "MM.DD" 当成 2026 年的日子换算成 1970 epoch ms,只用来比较先后
const [m, d] = md.split('.').map(Number);
return Date.UTC(2026, (m || 1) - 1, d || 1);
}
const TODAY_MS = mdToDay(TODAY_MD);
function passRange(ts, range) {
if (range === 'all') return true;
const md = ts.slice(0, 5); // "05.21"
if (range === 'month') return md.startsWith(TODAY_MD.slice(0, 3));
const diff = TODAY_MS - mdToDay(md);
if (range === '7d') return diff <= 6 * 86400000;
if (range === '30d') return diff <= 29 * 86400000;
return true;
}
function getFilteredBills() {
return BILLS.filter(b => {
if (BILLS_FILTER.stage !== 'all' && b.type !== BILLS_FILTER.stage) return false;
if (BILLS_FILTER.member !== 'all' && b.who !== BILLS_FILTER.member) return false;
if (!passRange(b.ts, BILLS_FILTER.range)) return false;
return true;
});
}
function renderBills() {
const tb = document.getElementById('bills-body');
const list = getFilteredBills();
tb.innerHTML = list.map(b => {
const r = ROLE_META[b.role];
return `
<tr>
<td class="ts">${b.ts}</td>
<td><strong style="font-weight:500;">${b.proj}</strong><br><span class="muted">${b.type}</span></td>
<td class="muted">${b.detail}</td>
<td><span class="who"><span class="av">${b.who}</span></span></td>
<td><span class="status-tag ${b.status}">${b.statusLabel}</span></td>
<td class="${amtCls(b.amount)}">${b.amount === 0 ? '¥0.00' : (b.amount > 0 ? '+' + fmtMoney(b.amount) : '-' + fmtMoney(b.amount))}</td>
</tr>
`;
}).join('');
document.getElementById('bills-count').textContent = list.length;
const empty = document.getElementById('bills-empty');
const table = document.querySelector('#panel-bills .billing-table');
if (list.length === 0) {
empty.style.display = '';
table.style.display = 'none';
} else {
empty.style.display = 'none';
table.style.display = '';
}
// 「清除筛选」按钮:任何一个非默认就显示
const isDefault = BILLS_FILTER.stage === 'all' && BILLS_FILTER.member === 'all' && BILLS_FILTER.range === '30d';
document.getElementById('bills-f-reset').style.display = isDefault ? 'none' : '';
}
/* ─── 筛选绑定 ─── */
['stage', 'member', 'range'].forEach(key => {
const sel = document.getElementById('bills-f-' + key);
sel.addEventListener('change', () => {
BILLS_FILTER[key] = sel.value;
renderBills();
});
});
document.getElementById('bills-f-reset').addEventListener('click', () => {
BILLS_FILTER.stage = 'all'; BILLS_FILTER.member = 'all'; BILLS_FILTER.range = '30d';
document.getElementById('bills-f-stage').value = 'all';
document.getElementById('bills-f-member').value = 'all';
document.getElementById('bills-f-range').value = '30d';
renderBills();
});
/* ─── Tab 切换 ─── */
document.querySelectorAll('.billing-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.billing-tabs .tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
});
});
/* ─── 快速充值卡选择 ─── */
document.querySelectorAll('.recharge-card').forEach(card => {
card.addEventListener('click', () => {
document.querySelectorAll('.recharge-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
});
});
/* ─── 充值 modal ─── */
function openTopup(channel) {
const selected = document.querySelector('.recharge-card.selected');
const customRaw = document.getElementById('custom-amt').value.trim();
const custom = Number(customRaw);
let amt = 500, bonus = 30;
if (custom >= 50) { amt = custom; bonus = 0; }
else if (selected) {
amt = Number(selected.dataset.amt);
const bonusEl = selected.querySelector('.gift.bonus');
bonus = bonusEl ? Number(bonusEl.textContent.replace(/\D/g, '')) : 0;
}
document.getElementById('topup-amt').textContent = fmtMoney(amt);
document.getElementById('topup-bonus').textContent = bonus > 0
? `// 含 ¥${bonus} 赠送 · 实到账 ¥${(amt + bonus).toFixed(2)}`
: '// 无赠送';
const isAlipay = channel === 'alipay';
document.getElementById('topup-channel-label').textContent = isAlipay ? '// 支付宝' : '// 微信支付';
document.getElementById('topup-channel-name').innerHTML = (isAlipay ? '支付宝扫码' : '微信扫码') +
'<br><span style="color:var(--black-alpha-32);">/topup/' + (isAlipay ? 'ali' : 'wx') + '/TX' + Date.now() + '</span>';
Shell.openModal('topup-bg');
}
function topupDone() {
Shell.closeModal('topup-bg');
Shell.toast('充值成功', '余额已更新 · 可开发票');
}
/* ─── 导出菜单 ─── */
(function bindExport() {
const trigger = document.getElementById('export-trigger');
const menu = document.getElementById('export-menu');
if (!trigger || !menu) return;
const FMT_LABEL = { csv: 'CSV', xlsx: 'XLSX', pdf: 'PDF' };
trigger.addEventListener('click', e => {
e.stopPropagation();
menu.hidden = !menu.hidden;
});
menu.querySelectorAll('button[data-fmt]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const fmt = b.dataset.fmt;
menu.hidden = true;
Shell.toast('正在生成 ' + FMT_LABEL[fmt], '完成后会发送到注册邮箱');
});
});
document.addEventListener('click', () => { if (!menu.hidden) menu.hidden = true; });
// Esc 关闭
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !menu.hidden) menu.hidden = true; });
})();
/* ─── 初始化 ─── */
renderTrend();
renderProjects();
renderMembers();
renderBills();
</script>
</body>
</html>