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 修复
This commit is contained in:
iye
2026-05-22 19:35:36 +08:00
parent f420af2069
commit 04335f3269
25 changed files with 13042 additions and 2700 deletions
+301 -35
View File
@@ -52,6 +52,34 @@
.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; }
@@ -85,10 +113,10 @@
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ─── 总览 · 趋势 + 阶段分布 ─── */
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: start; }
/* ─── 总览 · 趋势 + 阶段分布 (两栏等高) ─── */
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }
.trend-pane { padding: 18px 20px 14px; }
.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; }
@@ -96,7 +124,7 @@
.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; height: 170px; padding: 6px 4px 2px; position: relative; }
.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; }
@@ -218,7 +246,17 @@
</div>
<div class="balance-actions">
<button class="btn btn-lg" onclick="openTopup()">充值</button>
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', 'CSV · /billing/export')">导出账单</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>
@@ -256,11 +294,11 @@
<div class="pane trend-pane">
<div class="trend-head">
<h3>消费趋势</h3>
<span class="sub">// 近 14 天 · 单位 ¥</span>
<span class="sub" id="trend-sub">// 近 14 天 · 单位 ¥</span>
<span class="spacer"></span>
<button class="chip active"></button>
<button class="chip"></button>
<button class="chip"></button>
<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">
@@ -314,10 +352,20 @@
<!-- ===== Tab 2: 按项目 ===== -->
<div class="tab-panel" id="panel-by-project">
<div class="filter-bar">
<select><option>全部状态</option><option>进行中</option><option>已完成</option><option>已归档</option></select>
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
<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"><b style="color:var(--accent-black);">8</b> 个项目 · 当月消耗 ¥162.60</span>
<span class="ct" id="proj-count"><b style="color:var(--accent-black);">0</b> 个项目 · 消耗 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
@@ -327,22 +375,35 @@
<th>所属成员</th>
<th>当前阶段</th>
<th>状态</th>
<th style="text-align:right;">当月消耗</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><option>全部角色</option><option>超管</option><option>团管</option><option>成员</option></select>
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
<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"><b style="color:var(--accent-black);">5</b> 人 · 当月合计 ¥319.00</span>
<span class="ct" id="mem-count"><b style="color:var(--accent-black);">0</b> 人 · 合计 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
@@ -350,7 +411,7 @@
<th>成员</th>
<th>角色</th>
<th>已完成项目</th>
<th>当月已用 / 月度额度</th>
<th>已用 / 月度额度</th>
<th>最近活跃</th>
</tr>
</thead>
@@ -358,16 +419,39 @@
<!-- 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><option>全部阶段</option><option>视频片段</option><option>故事板</option><option>基础资产</option><option>脚本 LLM</option><option>充值</option><option>导出</option></select>
<select><option>全部成员</option><option>小李</option><option>张运营</option><option>王小姐</option><option>陈策划</option></select>
<select><option>近 30 天</option><option>近 7 天</option><option>本月</option><option>全部</option></select>
<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 style="color:var(--accent-black);">28</b> · <button class="chip" style="height:24px;font-size:11px;margin-left:4px;" onclick="Shell.toast('导出 CSV', '/billing/export?range=30d')">导出 CSV</button></span>
<span class="ct"><b id="bills-count" style="color:var(--accent-black);">0</b></span>
</div>
<table class="billing-table">
<thead>
@@ -384,6 +468,9 @@
<!-- 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 -->
@@ -427,6 +514,24 @@ const TREND_DAYS = [
{ 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 },
@@ -484,26 +589,61 @@ 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(...TREND_DAYS.map(d => d.v));
bars.innerHTML = TREND_DAYS.map(d => {
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('');
xax.innerHTML = TREND_DAYS.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
const sum = TREND_DAYS.reduce((s, d) => s + d.v, 0);
// 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 / TREND_DAYS.length);
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');
tb.innerHTML = PROJECTS_BILL.map(p => {
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>
@@ -511,25 +651,49 @@ function renderProjects() {
<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(-p.amount)}">${p.amount === 0 ? '¥0.00' : '-' + fmtMoney(p.amount)}</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');
tb.innerHTML = MEMBERS_BILL.map(m => {
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 pct = m.monthly > 0 ? (m.used / m.monthly * 100) : 0;
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(m.used)}</strong>
<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>
@@ -537,12 +701,62 @@ function renderMembers() {
</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');
tb.innerHTML = BILLS.map(b => {
const list = getFilteredBills();
tb.innerHTML = list.map(b => {
const r = ROLE_META[b.role];
return `
<tr>
@@ -555,8 +769,37 @@ function renderBills() {
</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', () => {
@@ -602,6 +845,29 @@ function topupDone() {
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();