All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
882 lines
53 KiB
HTML
882 lines
53 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>消费 · Airshelf</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
|
||
<style>
|
||
/* ─── 顶部:左右布局(余额 banner + 快速充值)─── */
|
||
.top-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(480px, .85fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
|
||
@media (max-width: 1120px) { .top-grid { grid-template-columns: 1fr; } }
|
||
|
||
.balance-banner {
|
||
background: var(--accent-black);
|
||
color: var(--accent-white);
|
||
padding: 26px 28px;
|
||
position: relative;
|
||
border: 1px solid var(--accent-black);
|
||
border-radius: var(--r-md);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
min-width: 0;
|
||
min-height: 246px;
|
||
}
|
||
.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: 16px 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-foot { margin-top: auto; padding-top: 2px; }
|
||
.balance-meter { height: 5px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); overflow: hidden; }
|
||
.balance-meter > span { display: block; height: 100%; width: 5.4%; background: var(--heat); border-radius: inherit; }
|
||
.balance-foot-meta { display: flex; justify-content: space-between; gap: 14px; margin-top: 8px; color: rgba(255,255,255,.46); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||
|
||
/* ─── 快速充值 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; min-height: 246px; }
|
||
.topup-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; }
|
||
.topup-head h3 { margin-bottom: 5px; }
|
||
.topup-head .desc { margin-bottom: 0; }
|
||
.topup-selected { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); white-space: nowrap; padding-top: 2px; }
|
||
|
||
.recharge-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
||
.recharge-card { min-height: 76px; border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 8px; text-align: center; cursor: pointer; background: var(--surface); position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: background var(--t-base), border-color var(--t-base), box-shadow var(--t-base), transform var(--t-fast); }
|
||
.recharge-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
|
||
.recharge-card:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--heat-40); }
|
||
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); box-shadow: inset 0 0 0 1px var(--heat); }
|
||
.recharge-card.selected::after { content: '✓'; position: absolute; top: 6px; right: 7px; width: 15px; height: 15px; border-radius: 50%; display: grid; place-items: center; background: var(--heat); color: var(--accent-white); font-size: 10px; line-height: 1; }
|
||
.recharge-card .amt { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
|
||
.recharge-card .gift { font-size: 10px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); white-space: nowrap; }
|
||
.recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }
|
||
.recharge-card .ribbon { position: absolute; top: 6px; left: 7px; font-family: var(--font-mono); font-size: 9px; padding: 1px 5px; background: var(--heat); color: var(--accent-white); letter-spacing: .03em; font-weight: 600; border-radius: var(--r-sm); }
|
||
.pay-row { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
|
||
.pay-title { font-size: 12px; font-weight: 600; color: var(--accent-black); line-height: 1.2; }
|
||
.pay-row .input { width: 100%; box-sizing: border-box; height: 38px; }
|
||
.pay-btn-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||
.pay-method-btn { height: 38px; border-radius: var(--r-pill); display: inline-flex; justify-content: center; align-items: center; gap: 8px; background: var(--surface); color: var(--accent-black); border-color: var(--border-faint); font-weight: 500; }
|
||
.pay-method-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
|
||
.pay-method-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--black-alpha-16); }
|
||
.pay-logo { width: 18px; height: 18px; border-radius: 6px; display: inline-block; flex: 0 0 18px; overflow: hidden; }
|
||
.pay-logo img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: inherit; }
|
||
@media (max-width: 720px) {
|
||
.recharge-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
|
||
/* ─── Tab strip ─── */
|
||
.billing-tabs { display: flex; align-items: flex-end; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 24px 0 18px; padding: 0 2px; overflow-x: auto; scrollbar-width: none; }
|
||
.billing-tabs::-webkit-scrollbar { display: none; }
|
||
.billing-tabs .tab { display: inline-flex; align-items: center; flex: 0 0 auto; gap: 6px; background: transparent; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r-md) var(--r-md) 0 0; margin-bottom: -1px; padding: 10px 14px; font-size: 13px; font-weight: 500; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; letter-spacing: 0; user-select: none; transition: color var(--t-base), background var(--t-base), border-color var(--t-base); }
|
||
.billing-tabs .tab:hover { color: var(--accent-black); background: var(--black-alpha-4); }
|
||
.billing-tabs .tab:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--heat-40); }
|
||
.billing-tabs .tab.active { color: var(--accent-black); border-bottom-color: var(--heat); font-weight: 600; }
|
||
.billing-tabs .tab .count { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding: 1px 7px; background: var(--black-alpha-4); border-radius: var(--r-sm); letter-spacing: .04em; }
|
||
.billing-tabs .tab.active .count { background: var(--heat-12); 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-foot">
|
||
<div class="balance-meter" aria-label="本月额度使用率 5.4%"><span></span></div>
|
||
<div class="balance-foot-meta">
|
||
<span>团队月剩余 ¥2,837.40</span>
|
||
<span>使用率 5.4%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右:快速充值 -->
|
||
<div class="pane topup-pane">
|
||
<div class="topup-head">
|
||
<div>
|
||
<h3>快速充值</h3>
|
||
<div class="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
|
||
</div>
|
||
<div class="topup-selected" id="topup-selected-label">已选 ¥500</div>
|
||
</div>
|
||
<div class="recharge-row">
|
||
<div class="recharge-card" data-amt="100" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
|
||
<div class="recharge-card selected" data-amt="500" role="button" tabindex="0" aria-pressed="true"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
|
||
<div class="recharge-card" data-amt="1000" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
|
||
<div class="recharge-card" data-amt="3000" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
|
||
</div>
|
||
<div class="pay-row">
|
||
<div class="pay-title">自定义金额</div>
|
||
<input class="input" id="custom-amt" placeholder="最低 ¥50,可输入任意金额">
|
||
<div class="pay-btn-row">
|
||
<button class="btn pay-method-btn pay-wechat" onclick="openTopup('wechat')" aria-label="微信支付">
|
||
<span class="pay-logo" aria-hidden="true"><img src="assets/pay-wechat.png" alt=""></span>
|
||
微信支付
|
||
</button>
|
||
<button class="btn pay-method-btn pay-alipay" onclick="openTopup('alipay')" aria-label="支付宝">
|
||
<span class="pay-logo" aria-hidden="true"><img src="assets/pay-alipay.png" alt=""></span>
|
||
支付宝
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab strip -->
|
||
<div class="billing-tabs" role="tablist">
|
||
<button class="tab active" type="button" data-tab="overview" role="tab" aria-selected="true" aria-controls="panel-overview">总览</button>
|
||
<button class="tab" type="button" data-tab="by-project" role="tab" aria-selected="false" aria-controls="panel-by-project">项目 <span class="count">8</span></button>
|
||
<button class="tab" type="button" data-tab="by-member" role="tab" aria-selected="false" aria-controls="panel-by-member">成员 <span class="count">5</span></button>
|
||
<button class="tab" type="button" data-tab="bills" role="tab" aria-selected="false" aria-controls="panel-bills">账单流水 <span class="count">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.5" 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/icons.js?v=2026052608"></script>
|
||
<script src="assets/shell.js?v=2026052607"></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;">
|
||
<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', () => {
|
||
const targetId = 'panel-' + tab.dataset.tab;
|
||
document.querySelectorAll('.billing-tabs .tab').forEach(t => {
|
||
const active = t === tab;
|
||
t.classList.toggle('active', active);
|
||
t.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
document.querySelectorAll('.tab-panel').forEach(p => {
|
||
p.classList.toggle('active', p.id === targetId);
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ─── 快速充值卡选择 ─── */
|
||
function setRechargeCard(card) {
|
||
document.querySelectorAll('.recharge-card').forEach(c => {
|
||
const active = c === card;
|
||
c.classList.toggle('selected', active);
|
||
c.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||
});
|
||
const label = document.getElementById('topup-selected-label');
|
||
if (label && card) label.textContent = '已选 ¥' + Number(card.dataset.amt).toLocaleString('zh-CN');
|
||
}
|
||
document.querySelectorAll('.recharge-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
setRechargeCard(card);
|
||
document.getElementById('custom-amt').value = '';
|
||
});
|
||
card.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
setRechargeCard(card);
|
||
document.getElementById('custom-amt').value = '';
|
||
}
|
||
});
|
||
});
|
||
document.getElementById('custom-amt').addEventListener('input', e => {
|
||
const raw = e.target.value.trim();
|
||
const label = document.getElementById('topup-selected-label');
|
||
if (!raw) {
|
||
const selected = document.querySelector('.recharge-card.selected');
|
||
if (label && selected) label.textContent = '已选 ¥' + Number(selected.dataset.amt).toLocaleString('zh-CN');
|
||
return;
|
||
}
|
||
document.querySelectorAll('.recharge-card').forEach(c => {
|
||
c.classList.remove('selected');
|
||
c.setAttribute('aria-pressed', 'false');
|
||
});
|
||
if (label) label.textContent = Number(raw) >= 50 ? '自定义 ¥' + Number(raw).toLocaleString('zh-CN') : '最低 ¥50';
|
||
});
|
||
|
||
/* ─── 充值 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('充值成功', '余额已更新 · 可开发票');
|
||
}
|
||
|
||
/* ─── 初始化 ─── */
|
||
renderTrend();
|
||
renderProjects();
|
||
renderMembers();
|
||
renderBills();
|
||
</script>
|
||
</body>
|
||
</html>
|