统一 Airshelf 界面组件与图标

This commit is contained in:
iye
2026-05-27 12:29:41 +08:00
parent ea335587d2
commit 086d92991e
47 changed files with 750 additions and 450 deletions
+120 -117
View File
@@ -2,25 +2,26 @@
<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">
<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.35fr) minmax(0, 1fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
@media (max-width: 980px) { .top-grid { grid-template-columns: 1fr; } }
.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: 24px 28px;
padding: 26px 28px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
display: flex;
flex-direction: column;
gap: 22px;
gap: 24px;
min-width: 0;
min-height: 246px;
}
.balance-banner::before, .balance-banner::after,
.balance-banner > .corner-tr, .balance-banner > .corner-bl {
@@ -40,75 +41,59 @@
.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 { 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-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; }
.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; }
.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: 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-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: -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; }
.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: 1fr 1fr; gap: 8px; }
.pay-btn-row .btn { width: 100%; }
.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; 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); }
.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; }
@@ -244,37 +229,42 @@
<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="1.5" 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 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">
<h3>快速充值</h3>
<div class="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
<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"><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 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">
<input class="input" id="custom-amt" placeholder="自定义金额(最低 ¥50)">
<div class="pay-title">自定义金额</div>
<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>
<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>
@@ -282,10 +272,10 @@
<!-- 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>
<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: 总览 ===== -->
@@ -500,7 +490,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<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: '消费' }] });
@@ -688,7 +679,7 @@ function renderMembers() {
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'">
<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>
@@ -803,19 +794,54 @@ document.getElementById('bills-f-reset').addEventListener('click', () => {
/* ─── 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');
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', () => {
document.querySelectorAll('.recharge-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
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 ─── */
@@ -845,29 +871,6 @@ 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();