Files
xiaocai/docs/mockup/index.html
wangxiaogang c7f29aa7a7 feat: 小菜记账 v1.0 - 完整功能实现
核心功能:
- 记账CRUD(支出/收入/分类/备注/日期)
- 统计分析(概览/分类占比/每日趋势)
- 预算管理(按月设置/进度条/超支提醒)
- 数据导出CSV

安全与认证:
- HMAC-SHA256签名token认证
- 用户数据隔离
- 输入验证与错误处理
- CORS配置

前端优化:
- 骨架屏加载
- 账单按日期分组
- 预算页面重构(快捷预设+Numpad)
- SvgIcon组件(H5+微信双端适配)
- 下拉刷新

后端优化:
- 共享日期工具函数
- 数据库连接池优化
- 健康检查端点
- 优雅关闭处理

技术栈:
- 前端:Uni-app (Vue 3 + Pinia)
- 后端:Node.js + Express + MySQL
2026-05-29 16:14:15 +08:00

1639 lines
60 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>小菜记账 — UI 精细稿</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Caveat:wght@400;600&display=swap" rel="stylesheet">
<style>
/* ==================== RESET ==================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-tap-highlight-color: transparent; }
svg { flex-shrink: 0; }
/* ==================== DESIGN TOKENS ==================== */
:root {
--primary: #FF8C69;
--primary-light: #FFB89A;
--primary-dark: #E67355;
--secondary: #6C9BCF;
--accent: #FFD166;
--bg: #FFF8F0;
--surface: #FFFFFF;
--surface-warm: #FFF0E6;
--text: #2D1B1B;
--text-sec: #8B7E7E;
--text-muted: #BFB3B3;
--success: #7BC67E;
--warning: #FFB347;
--danger: #FF6B6B;
--border: #F0E0D6;
--shadow-clay: 4px 4px 12px rgba(45,27,27,0.08), inset -2px -2px 6px rgba(45,27,27,0.04);
--shadow-clay-hover: 6px 6px 16px rgba(45,27,27,0.12), inset -2px -2px 6px rgba(45,27,27,0.04);
--shadow-clay-pressed: inset 2px 2px 6px rgba(45,27,27,0.1);
--shadow-clay-float: 8px 8px 24px rgba(45,27,27,0.1), inset -3px -3px 8px rgba(45,27,27,0.05);
--shadow-bottom: 0 -2px 12px rgba(45,27,27,0.06);
--font-title: 'Fredoka', 'PingFang SC', sans-serif;
--font-body: 'Nunito', 'PingFang SC', sans-serif;
--font-hand: 'Caveat', cursive;
--nav-h: 56px;
--safe-b: 34px;
--page-h: 812px;
--page-w: 375px;
}
/* ==================== PHONE FRAME ==================== */
body {
background: #E8DDD4;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 32px 16px;
font-family: var(--font-body);
color: var(--text);
}
.phone-frame {
width: var(--page-w);
height: var(--page-h);
background: var(--bg);
border-radius: 40px;
overflow: hidden;
position: relative;
box-shadow:
0 20px 60px rgba(45,27,27,0.15),
0 4px 16px rgba(45,27,27,0.1),
inset 0 0 0 2px rgba(255,255,255,0.3);
}
/* Status bar */
.status-bar {
height: 44px;
padding: 12px 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-title);
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.status-bar .time { font-variant-numeric: tabular-nums; }
.status-bar .icons { display: flex; gap: 6px; }
.status-bar .icons svg { width: 16px; height: 16px; }
/* Pages container */
.pages-wrap {
height: calc(var(--page-h) - 44px);
position: relative;
}
.page {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
transform: translateY(8px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding-bottom: calc(var(--nav-h) + var(--safe-b) + 16px);
}
.page::-webkit-scrollbar { display: none; }
.page.active {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
/* ==================== SHARED COMPONENTS ==================== */
/* Clay Card */
.clay-card {
background: var(--surface);
border-radius: 20px;
border: 2px solid var(--border);
box-shadow: var(--shadow-clay);
}
/* Bottom Nav */
.bottom-nav {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: calc(var(--nav-h) + var(--safe-b));
padding-bottom: var(--safe-b);
background: var(--surface);
box-shadow: var(--shadow-bottom);
border-radius: 20px 20px 0 0;
display: flex;
align-items: center;
justify-content: space-around;
z-index: 100;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
min-width: 44px;
min-height: 44px;
justify-content: center;
}
.nav-item svg { width: 24px; height: 24px; stroke: var(--text-muted); fill: none; stroke-width: 2; transition: 0.2s; }
.nav-item span { font: 500 10px/1 var(--font-body); color: var(--text-muted); transition: 0.2s; }
.nav-item.active svg { stroke: var(--primary); fill: var(--primary); fill-opacity: 0.15; }
.nav-item.active span { color: var(--primary); font-weight: 600; }
.nav-add {
width: 52px; height: 52px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 16px rgba(255,140,105,0.4);
margin-top: -12px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.nav-add:active { transform: scale(0.93); box-shadow: 0 2px 8px rgba(255,140,105,0.3); }
.nav-add svg { width: 28px; height: 28px; stroke: #fff; fill: none; stroke-width: 2.5; }
/* ==================== PAGE 1: HOME ==================== */
.home-header {
padding: 8px 20px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.home-header .avatar {
width: 36px; height: 36px;
border-radius: 50%;
border: 2px solid var(--border);
background: var(--surface-warm);
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.home-header .avatar svg { width: 20px; height: 20px; stroke: var(--text-sec); }
.home-header .greeting {
font: 600 18px/1.3 var(--font-title);
color: var(--text);
}
.home-header .bell {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.home-header .bell svg { width: 22px; height: 22px; stroke: var(--text-sec); }
/* Overview Card */
.overview-card {
margin: 0 20px;
padding: 24px;
}
.overview-card .label {
font: 400 14px/1 var(--font-body);
color: var(--text-sec);
margin-bottom: 6px;
}
.overview-card .amount {
font: 600 36px/1.2 var(--font-title);
color: var(--text);
font-variant-numeric: tabular-nums;
letter-spacing: 0.5px;
}
.overview-card .amount .currency {
font-size: 22px;
font-weight: 500;
color: var(--text-sec);
margin-right: 2px;
}
/* Budget bar */
.budget-bar-wrap { margin-top: 16px; }
.budget-bar {
height: 8px; border-radius: 4px;
background: var(--border);
overflow: hidden;
}
.budget-bar .fill {
height: 100%; border-radius: 4px;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
width: 65%;
transition: width 0.6s ease-out;
}
.budget-info {
display: flex; justify-content: space-between;
margin-top: 8px;
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
}
.budget-info .pct {
font: 500 14px/1 var(--font-title);
color: var(--primary);
}
/* Mini stats */
.mini-stats {
display: flex; gap: 12px; margin-top: 20px;
}
.mini-stat {
flex: 1;
padding: 12px;
border-radius: 12px;
background: var(--bg);
text-align: center;
}
.mini-stat .label {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
margin-bottom: 4px;
}
.mini-stat .value {
font: 600 18px/1.2 var(--font-title);
font-variant-numeric: tabular-nums;
}
.mini-stat .value.income { color: var(--success); }
.mini-stat .value.expense { color: var(--danger); }
/* Quick actions */
.quick-actions {
display: flex; gap: 12px;
padding: 0 20px;
margin-top: 20px;
}
.quick-btn {
flex: 1;
height: 72px;
border-radius: 16px;
background: var(--surface);
border: 2px solid var(--border);
box-shadow: var(--shadow-clay);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.quick-btn:active {
transform: scale(0.97);
box-shadow: var(--shadow-clay-pressed);
}
.quick-btn svg { width: 26px; height: 26px; stroke-width: 2; }
.quick-btn span { font: 500 12px/1 var(--font-body); color: var(--text); }
.quick-btn.income svg { stroke: var(--success); }
.quick-btn.expense svg { stroke: var(--danger); }
.quick-btn.transfer svg { stroke: var(--secondary); }
/* Transactions */
.section-header {
display: flex; justify-content: space-between; align-items: center;
padding: 24px 20px 12px;
}
.section-header .title {
font: 500 14px/1 var(--font-body);
color: var(--text-sec);
}
.section-header .more {
font: 400 12px/1 var(--font-body);
color: var(--primary);
cursor: pointer;
}
.tx-date {
padding: 8px 20px;
font: 500 13px/1 var(--font-body);
color: var(--text-sec);
}
.tx-date:not(:first-child) { margin-top: 8px; }
.tx-item {
display: flex; align-items: center; gap: 12px;
padding: 14px 20px;
cursor: pointer;
transition: background 0.15s;
}
.tx-item:active { background: var(--surface-warm); }
.tx-item .icon-wrap {
width: 40px; height: 40px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.tx-item .icon-wrap svg { width: 20px; height: 20px; stroke-width: 2; }
.tx-item .info { flex: 1; min-width: 0; }
.tx-item .info .name {
font: 500 14px/1.3 var(--font-body);
color: var(--text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tx-item .info .meta {
font: 400 12px/1.3 var(--font-body);
color: var(--text-sec);
margin-top: 2px;
}
.tx-item .amount {
font: 600 16px/1 var(--font-title);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.tx-item .amount.expense { color: var(--text); }
.tx-item .amount.income { color: var(--success); }
.tx-divider {
height: 1px;
background: var(--border);
margin: 0 20px;
}
/* ==================== PAGE 2: ADD RECORD ==================== */
.add-header {
padding: 8px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.add-header .close {
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.add-header .close svg { width: 20px; height: 20px; stroke: var(--text-sec); }
.add-header .title { font: 500 16px/1 var(--font-body); color: var(--text); }
.add-header .save {
font: 600 14px/1 var(--font-body);
color: var(--primary);
cursor: pointer;
}
/* Toggle */
.type-toggle-wrap { display: flex; justify-content: center; margin: 12px 0; }
.type-toggle {
display: inline-flex;
background: var(--surface-warm);
border-radius: 12px;
padding: 4px;
position: relative;
}
.type-toggle .tab {
width: 80px; height: 36px;
display: flex; align-items: center; justify-content: center;
font: 400 14px/1 var(--font-body);
color: var(--text-sec);
cursor: pointer;
border-radius: 10px;
position: relative;
z-index: 1;
transition: color 0.2s;
}
.type-toggle .tab.active {
color: var(--primary);
font-weight: 600;
}
.type-toggle .slider {
position: absolute;
top: 4px; left: 4px;
width: 80px; height: 36px;
background: var(--surface);
border-radius: 10px;
box-shadow: 2px 2px 8px rgba(45,27,27,0.08);
transition: transform 0.2s ease;
}
.type-toggle.income .slider { transform: translateX(80px); }
/* Amount input */
.amount-display {
text-align: center;
padding: 32px 20px 24px;
}
.amount-display .currency {
font: 500 24px/1 var(--font-title);
color: var(--text-muted);
margin-right: 4px;
}
.amount-display .digits {
font: 700 48px/1.2 var(--font-title);
color: var(--text);
font-variant-numeric: tabular-nums;
letter-spacing: 1px;
display: inline;
}
.amount-display .cursor {
display: inline-block;
width: 2px; height: 40px;
background: var(--primary);
margin-left: 2px;
vertical-align: middle;
animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }
/* Category grid */
.category-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 0 20px;
max-height: 240px;
overflow-y: auto;
}
.category-grid::-webkit-scrollbar { display: none; }
.cat-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 0;
cursor: pointer;
transition: transform 0.1s;
-webkit-tap-highlight-color: transparent;
}
.cat-item:active { transform: scale(0.95); }
.cat-item .cat-icon {
width: 44px; height: 44px;
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
border: 2px solid transparent;
}
.cat-item .cat-icon svg { width: 22px; height: 22px; stroke-width: 2; transition: transform 0.2s; }
.cat-item .cat-name {
font: 400 11px/1 var(--font-body);
color: var(--text-sec);
transition: color 0.2s;
}
.cat-item.selected .cat-icon {
border-color: var(--primary);
background: #FFE8E0;
}
.cat-item.selected .cat-icon svg { transform: scale(1.1); }
.cat-item.selected .cat-name { color: var(--primary); font-weight: 600; }
/* Extra fields */
.extra-fields {
padding: 16px 20px;
display: flex; flex-direction: column; gap: 8px;
}
.extra-row {
height: 48px;
background: var(--bg);
border-radius: 12px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.extra-row .left {
display: flex; align-items: center; gap: 8px;
}
.extra-row .left svg { width: 18px; height: 18px; stroke: var(--text-sec); }
.extra-row .left span { font: 400 14px/1 var(--font-body); color: var(--text); }
.extra-row > svg { width: 16px; height: 16px; stroke: var(--text-muted); flex-shrink: 0; }
/* Numpad */
.numpad {
padding: 16px 16px 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.numpad-key {
height: 48px;
border-radius: 12px;
background: var(--bg);
display: flex; align-items: center; justify-content: center;
font: 500 22px/1 var(--font-title);
color: var(--text);
cursor: pointer;
transition: background 0.1s;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.numpad-key:active { background: var(--border); }
.numpad-key.fn {
background: #FFE8E0;
color: var(--primary);
}
.numpad-key.fn:active { background: #FFD0C0; }
.numpad-key.fn svg { width: 22px; height: 22px; stroke: var(--primary); }
.numpad-key.eq {
background: var(--primary);
color: #fff;
}
.numpad-key.eq:active { background: var(--primary-dark); }
.submit-btn {
margin: 12px 16px;
height: 52px;
border-radius: 16px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
display: flex; align-items: center; justify-content: center;
font: 600 18px/1 var(--font-body);
color: #fff;
box-shadow: 0 4px 16px rgba(255,140,105,0.3);
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
gap: 8px;
}
.submit-btn:active { transform: scale(0.97); box-shadow: 0 2px 8px rgba(255,140,105,0.2); }
.submit-btn svg { width: 20px; height: 20px; stroke: #fff; }
/* ==================== PAGE 3: STATS ==================== */
.stats-header {
padding: 8px 20px 12px;
text-align: center;
font: 600 18px/1.3 var(--font-title);
color: var(--text);
}
.month-picker {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
padding: 8px 20px;
}
.month-picker .arrow {
width: 44px; height: 44px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.month-picker .arrow svg { width: 22px; height: 22px; stroke: var(--text-sec); }
.month-picker .month-text {
font: 500 16px/1 var(--font-title);
color: var(--text);
}
/* Stats overview */
.stats-overview {
margin: 16px 20px 0;
padding: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
}
.stats-overview .so-item .label {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
margin-bottom: 6px;
}
.stats-overview .so-item .value {
font: 600 20px/1.2 var(--font-title);
color: var(--text);
font-variant-numeric: tabular-nums;
}
/* Donut chart area */
.chart-card {
margin: 16px 20px 0;
padding: 20px;
}
.chart-card .chart-title {
font: 500 16px/1 var(--font-body);
color: var(--text);
margin-bottom: 16px;
}
.donut-wrap {
display: flex;
flex-direction: column;
align-items: center;
}
.donut-svg { width: 180px; height: 180px; }
.donut-center {
text-align: center;
margin-top: -110px;
margin-bottom: 30px;
pointer-events: none;
}
.donut-center .total-label {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
}
.donut-center .total-value {
font: 600 20px/1.3 var(--font-title);
color: var(--text);
}
.chart-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px 20px;
margin-top: 8px;
}
.legend-item {
display: flex; align-items: center; gap: 6px;
}
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
}
.legend-item span {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
}
.legend-item .pct {
font: 500 12px/1 var(--font-title);
color: var(--text);
}
/* Line chart */
.line-chart-wrap { position: relative; }
.line-chart-svg { width: 100%; height: 160px; }
/* Rank list */
.rank-list { padding: 0 0 4px; }
.rank-item {
display: flex; align-items: center; gap: 12px;
margin-bottom: 16px;
}
.rank-item:last-child { margin-bottom: 0; }
.rank-num {
width: 24px;
font: 600 20px/1 var(--font-title);
text-align: center;
}
.rank-num.n1 { color: var(--primary); }
.rank-num.n2 { color: var(--accent); }
.rank-num.n3 { color: var(--secondary); }
.rank-icon {
width: 36px; height: 36px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.rank-icon svg { width: 18px; height: 18px; stroke-width: 2; }
.rank-info { flex: 1; min-width: 0; }
.rank-info .name {
font: 500 14px/1 var(--font-body);
color: var(--text);
}
.rank-bar {
height: 6px; border-radius: 3px;
background: var(--border);
margin-top: 6px;
overflow: hidden;
}
.rank-bar .fill {
height: 100%; border-radius: 3px;
transition: width 0.6s ease-out;
}
.rank-info .meta {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
margin-top: 4px;
}
.rank-amount {
font: 600 16px/1 var(--font-title);
color: var(--text);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* ==================== PAGE 4: PROFILE ==================== */
.profile-card {
margin: 0 20px;
padding: 24px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-radius: 20px;
box-shadow: var(--shadow-clay-float);
border: none;
display: flex;
align-items: center;
gap: 16px;
}
.profile-card .p-avatar {
width: 64px; height: 64px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.5);
background: rgba(255,255,255,0.25);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.profile-card .p-avatar svg { width: 32px; height: 32px; stroke: #fff; stroke-width: 1.5; }
.profile-card .p-info .name {
font: 600 20px/1.3 var(--font-title);
color: #fff;
}
.profile-card .p-info .tagline {
font: 400 14px/1.3 var(--font-body);
color: rgba(255,255,255,0.8);
margin-top: 2px;
}
.profile-card .p-info .streak {
font: 400 12px/1 var(--font-body);
color: rgba(255,255,255,0.65);
margin-top: 8px;
}
/* Quick stats row */
.quick-stats {
display: flex; gap: 12px;
padding: 0 20px;
margin-top: 16px;
}
.qs-card {
flex: 1;
padding: 16px 12px;
background: var(--surface);
border-radius: 16px;
border: 2px solid var(--border);
box-shadow: var(--shadow-clay);
text-align: center;
}
.qs-card .label {
font: 400 12px/1 var(--font-body);
color: var(--text-sec);
margin-bottom: 4px;
}
.qs-card .value {
font: 600 16px/1.2 var(--font-title);
color: var(--text);
font-variant-numeric: tabular-nums;
}
/* Menu list */
.menu-section { margin: 20px 20px 0; }
.menu-card {
background: var(--surface);
border-radius: 20px;
border: 2px solid var(--border);
box-shadow: var(--shadow-clay);
overflow: hidden;
}
.menu-item {
height: 56px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid var(--border);
}
.menu-item:last-child { border-bottom: none; }
.menu-item:active { background: var(--bg); }
.menu-item .left {
display: flex; align-items: center; gap: 12px;
}
.menu-item .left .mi-icon {
width: 32px; height: 32px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
}
.menu-item .left .mi-icon svg { width: 18px; height: 18px; stroke-width: 2; }
.menu-item .left .mi-label {
font: 500 14px/1 var(--font-body);
color: var(--text);
}
.menu-item > svg { width: 16px; height: 16px; stroke: var(--text-muted); flex-shrink: 0; }
/* Toggle switch */
.toggle-switch {
width: 48px; height: 28px;
border-radius: 14px;
background: var(--border);
position: relative;
cursor: pointer;
transition: background 0.2s;
}
.toggle-switch.on { background: var(--primary); }
.toggle-switch .knob {
width: 22px; height: 22px;
border-radius: 50%;
background: #fff;
position: absolute;
top: 3px; left: 3px;
transition: transform 0.2s;
box-shadow: 1px 1px 4px rgba(0,0,0,0.1);
}
.toggle-switch.on .knob { transform: translateX(20px); }
/* ==================== ANIMATIONS ==================== */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.stagger > * {
animation: fadeUp 0.3s ease-out backwards;
}
.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 50ms; }
.stagger > *:nth-child(3) { animation-delay: 100ms; }
.stagger > *:nth-child(4) { animation-delay: 150ms; }
.stagger > *:nth-child(5) { animation-delay: 200ms; }
.stagger > *:nth-child(6) { animation-delay: 250ms; }
@media (prefers-reduced-motion: reduce) {
.stagger > * { animation: none; }
.page { transition: none; }
}
/* ==================== TABS (stats) ==================== */
.stats-tabs {
display: flex;
justify-content: center;
margin: 0 20px;
}
.stats-tab-bar {
display: inline-flex;
background: var(--surface-warm);
border-radius: 12px;
padding: 4px;
}
.stats-tab-bar .stab {
padding: 8px 20px;
border-radius: 10px;
font: 400 14px/1 var(--font-body);
color: var(--text-sec);
cursor: pointer;
transition: all 0.2s;
}
.stats-tab-bar .stab.active {
background: var(--surface);
color: var(--primary);
font-weight: 600;
box-shadow: 2px 2px 8px rgba(45,27,27,0.06);
}
</style>
</head>
<body>
<div class="phone-frame">
<!-- Status Bar -->
<div class="status-bar">
<span class="time">9:41</span>
<span class="icons">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 9l4-4 4 4"/><path d="M5 5v14"/><path d="M11 19h4a2 2 0 002-2V7a2 2 0 00-2-2h-4"/><path d="M11 19l-4-4 4-4"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6.7 6.7L1 12l5.7 5.3"/><path d="M17.3 6.7L23 12l-5.7 5.3"/><path d="M14 4l-4 16"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M23 10v4"/></svg>
</span>
</div>
<div class="pages-wrap">
<!-- ==================== PAGE 1: HOME ==================== -->
<div class="page active" id="page-home">
<!-- Header -->
<div class="home-header">
<div class="avatar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2"/></svg>
</div>
<div class="greeting">早上好,小菜</div>
<div class="bell">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</div>
</div>
<!-- Overview Card -->
<div class="overview-card clay-card">
<div class="label">本月支出</div>
<div class="amount"><span class="currency">¥</span>3,280.00</div>
<div class="budget-bar-wrap">
<div class="budget-bar"><div class="fill"></div></div>
<div class="budget-info">
<span>预算 ¥5,000 · 剩余 ¥1,720</span>
<span class="pct">65%</span>
</div>
</div>
<div class="mini-stats">
<div class="mini-stat">
<div class="label">收入</div>
<div class="value income">+¥8,000</div>
</div>
<div class="mini-stat">
<div class="label">支出</div>
<div class="value expense">-¥3,280</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions stagger">
<div class="quick-btn income">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>
<span>记收入</span>
</div>
<div class="quick-btn expense">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/></svg>
<span>记支出</span>
</div>
<div class="quick-btn transfer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M7 23l-4-4 4-4"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
<span>转账</span>
</div>
</div>
<!-- Transactions -->
<div class="section-header">
<span class="title">近期账单</span>
<span class="more">查看全部</span>
</div>
<div class="stagger">
<div class="tx-date">今天</div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
</div>
<div class="info">
<div class="name">午餐</div>
<div class="meta">餐饮 · 12:30</div>
</div>
<div class="amount expense">-¥35.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
</div>
<div class="info">
<div class="name">咖啡</div>
<div class="meta">餐饮 · 09:15</div>
</div>
<div class="amount expense">-¥18.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>
</div>
<div class="info">
<div class="name">地铁充值</div>
<div class="meta">交通 · 08:45</div>
</div>
<div class="amount expense">-¥50.00</div>
</div>
<div class="tx-date">昨天</div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
</div>
<div class="info">
<div class="name">工资</div>
<div class="meta">收入 · 10:00</div>
</div>
<div class="amount income">+¥8,000.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div class="info">
<div class="name">快递</div>
<div class="meta">交通 · 15:42</div>
</div>
<div class="amount expense">-¥28.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
</div>
<div class="info">
<div class="name">超市采购</div>
<div class="meta">购物 · 18:20</div>
</div>
<div class="amount expense">-¥156.00</div>
</div>
</div>
</div>
<!-- ==================== PAGE 2: ADD ==================== -->
<div class="page" id="page-add">
<div class="add-header">
<div class="close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</div>
<span class="title">记一笔</span>
<span class="save">保存</span>
</div>
<div class="type-toggle-wrap">
<div class="type-toggle" id="typeToggle">
<div class="slider"></div>
<div class="tab active" data-type="expense" onclick="switchType('expense')">支出</div>
<div class="tab" data-type="income" onclick="switchType('income')">收入</div>
</div>
</div>
<div class="amount-display">
<span class="currency">¥</span>
<span class="digits">35.00</span>
<span class="cursor"></span>
</div>
<div class="category-grid stagger">
<div class="cat-item selected" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
</div>
<span class="cat-name">餐饮</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/></svg>
</div>
<span class="cat-name">饮品</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
</div>
<span class="cat-name">购物</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<span class="cat-name">交通</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</div>
<span class="cat-name">住房</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FCE4EC">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF6B6B"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
</div>
<span class="cat-name">医疗</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#E8EAF6">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>
</div>
<span class="cat-name">教育</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#F3E5F5">
<svg viewBox="0 0 24 24" fill="none" stroke="#B39DDB"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
</div>
<span class="cat-name">娱乐</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FFF8E1">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFD166"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
</div>
<span class="cat-name">服饰</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#FCE4EC">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
</div>
<span class="cat-name">美容</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
</div>
<span class="cat-name">通讯</span>
</div>
<div class="cat-item" onclick="selectCat(this)">
<div class="cat-icon" style="background:#F5F5F5">
<svg viewBox="0 0 24 24" fill="none" stroke="#BFB3B3"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
</div>
<span class="cat-name">更多</span>
</div>
</div>
<div class="extra-fields">
<div class="extra-row">
<div class="left">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span>今天 (5月27日)</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="extra-row">
<div class="left">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
<span>添加备注...</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
<div class="numpad">
<div class="numpad-key">1</div>
<div class="numpad-key">2</div>
<div class="numpad-key">3</div>
<div class="numpad-key fn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
<div class="numpad-key">4</div>
<div class="numpad-key">5</div>
<div class="numpad-key">6</div>
<div class="numpad-key fn">+</div>
<div class="numpad-key">7</div>
<div class="numpad-key">8</div>
<div class="numpad-key">9</div>
<div class="numpad-key fn">-</div>
<div class="numpad-key">.</div>
<div class="numpad-key">0</div>
<div class="numpad-key fn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 4H8l-7 8 7 8h13a2 2 0 002-2V6a2 2 0 00-2-2z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/></svg>
</div>
<div class="numpad-key eq">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
<div class="submit-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
记好了
</div>
</div>
<!-- ==================== PAGE 3: STATS ==================== -->
<div class="page" id="page-stats">
<div class="stats-header">统计分析</div>
<div class="month-picker">
<div class="arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</div>
<span class="month-text">2026年5月</span>
<div class="arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
<div class="stats-tabs">
<div class="stats-tab-bar">
<div class="stab active">支出</div>
<div class="stab">收入</div>
</div>
</div>
<!-- Overview -->
<div class="stats-overview clay-card">
<div class="so-item">
<div class="label">总支出</div>
<div class="value">¥3,280</div>
</div>
<div class="so-item">
<div class="label">日均</div>
<div class="value">¥122</div>
</div>
<div class="so-item">
<div class="label">笔数</div>
<div class="value">28笔</div>
</div>
</div>
<!-- Donut Chart -->
<div class="chart-card clay-card">
<div class="chart-title">分类占比</div>
<div class="donut-wrap">
<svg class="donut-svg" viewBox="0 0 200 200">
<!-- Donut segments -->
<circle cx="100" cy="100" r="70" fill="none" stroke="#FF8C69" stroke-width="28"
stroke-dasharray="167.6 272.4" stroke-dashoffset="0"
transform="rotate(-90 100 100)" stroke-linecap="round"/>
<circle cx="100" cy="100" r="70" fill="none" stroke="#FFD166" stroke-width="28"
stroke-dasharray="96.8 343.2" stroke-dashoffset="-167.6"
transform="rotate(-90 100 100)" stroke-linecap="round"/>
<circle cx="100" cy="100" r="70" fill="none" stroke="#6C9BCF" stroke-width="28"
stroke-dasharray="66 374" stroke-dashoffset="-264.4"
transform="rotate(-90 100 100)" stroke-linecap="round"/>
<circle cx="100" cy="100" r="70" fill="none" stroke="#7BC67E" stroke-width="28"
stroke-dasharray="52.8 387.2" stroke-dashoffset="-330.4"
transform="rotate(-90 100 100)" stroke-linecap="round"/>
<circle cx="100" cy="100" r="70" fill="none" stroke="#BFB3B3" stroke-width="28"
stroke-dasharray="56.4 383.6" stroke-dashoffset="-383.2"
transform="rotate(-90 100 100)" stroke-linecap="round"/>
</svg>
<div class="donut-center">
<div class="total-label">总支出</div>
<div class="total-value">¥3,280</div>
</div>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-dot" style="background:#FF8C69"></div><span>餐饮</span><span class="pct">38%</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#FFD166"></div><span>购物</span><span class="pct">22%</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#6C9BCF"></div><span>交通</span><span class="pct">15%</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#7BC67E"></div><span>住房</span><span class="pct">12%</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#BFB3B3"></div><span>其他</span><span class="pct">13%</span></div>
</div>
</div>
<!-- Line Chart -->
<div class="chart-card clay-card">
<div class="chart-title">每日趋势</div>
<div class="line-chart-wrap">
<svg class="line-chart-svg" viewBox="0 0 300 160">
<!-- Grid lines -->
<line x1="30" y1="10" x2="30" y2="140" stroke="#F0E0D6" stroke-width="1"/>
<line x1="30" y1="140" x2="290" y2="140" stroke="#F0E0D6" stroke-width="1"/>
<line x1="30" y1="105" x2="290" y2="105" stroke="#F0E0D6" stroke-width="1" stroke-dasharray="4"/>
<line x1="30" y1="70" x2="290" y2="70" stroke="#F0E0D6" stroke-width="1" stroke-dasharray="4"/>
<line x1="30" y1="35" x2="290" y2="35" stroke="#F0E0D6" stroke-width="1" stroke-dasharray="4"/>
<!-- Y labels -->
<text x="25" y="144" text-anchor="end" fill="#BFB3B3" font-size="10" font-family="Fredoka">0</text>
<text x="25" y="109" text-anchor="end" fill="#BFB3B3" font-size="10" font-family="Fredoka">50</text>
<text x="25" y="74" text-anchor="end" fill="#BFB3B3" font-size="10" font-family="Fredoka">100</text>
<text x="25" y="39" text-anchor="end" fill="#BFB3B3" font-size="10" font-family="Fredoka">150</text>
<!-- Area fill -->
<path d="M45,100 L80,85 L115,95 L150,60 L185,70 L220,40 L255,55 L275,50 L275,140 L45,140 Z"
fill="url(#areaGrad)" opacity="0.3"/>
<!-- Line -->
<path d="M45,100 L80,85 L115,95 L150,60 L185,70 L220,40 L255,55 L275,50"
fill="none" stroke="#FF8C69" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Dots -->
<circle cx="45" cy="100" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="80" cy="85" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="115" cy="95" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="150" cy="60" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="185" cy="70" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="220" cy="40" r="5" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="255" cy="55" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<circle cx="275" cy="50" r="4" fill="#FF8C69" stroke="#fff" stroke-width="2"/>
<!-- X labels -->
<text x="45" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">1</text>
<text x="80" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">5</text>
<text x="115" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">10</text>
<text x="150" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">15</text>
<text x="185" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">20</text>
<text x="220" y="155" text-anchor="middle" fill="#FF8C69" font-size="10" font-family="Fredoka" font-weight="600">25</text>
<text x="255" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">27</text>
<text x="275" y="155" text-anchor="middle" fill="#BFB3B3" font-size="10" font-family="Fredoka">30</text>
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FF8C69" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#FF8C69" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
</div>
</div>
<!-- Rank -->
<div class="chart-card clay-card" style="margin-bottom:20px">
<div class="chart-title">分类排行</div>
<div class="rank-list stagger">
<div class="rank-item">
<div class="rank-num n1">1</div>
<div class="rank-icon" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
</div>
<div class="rank-info">
<div class="name">餐饮</div>
<div class="rank-bar"><div class="fill" style="width:100%;background:#FF8C69"></div></div>
<div class="meta">12笔 · 38%</div>
</div>
<div class="rank-amount">¥1,246</div>
</div>
<div class="rank-item">
<div class="rank-num n2">2</div>
<div class="rank-icon" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
</div>
<div class="rank-info">
<div class="name">购物</div>
<div class="rank-bar"><div class="fill" style="width:58%;background:#FFD166"></div></div>
<div class="meta">5笔 · 22%</div>
</div>
<div class="rank-amount">¥722</div>
</div>
<div class="rank-item">
<div class="rank-num n3">3</div>
<div class="rank-icon" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div class="rank-info">
<div class="name">交通</div>
<div class="rank-bar"><div class="fill" style="width:39%;background:#6C9BCF"></div></div>
<div class="meta">8笔 · 15%</div>
</div>
<div class="rank-amount">¥492</div>
</div>
<div class="rank-item">
<div class="rank-num">4</div>
<div class="rank-icon" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</div>
<div class="rank-info">
<div class="name">住房</div>
<div class="rank-bar"><div class="fill" style="width:32%;background:#7BC67E"></div></div>
<div class="meta">2笔 · 12%</div>
</div>
<div class="rank-amount">¥394</div>
</div>
</div>
</div>
</div>
<!-- ==================== PAGE 4: BILLS ==================== -->
<div class="page" id="page-bills">
<div class="stats-header">全部账单</div>
<div class="stats-tabs" style="margin-bottom:12px">
<div class="stats-tab-bar">
<div class="stab active">全部</div>
<div class="stab">支出</div>
<div class="stab">收入</div>
</div>
</div>
<div class="tx-date">今天 · 5月27日</div>
<div class="stagger">
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
</div>
<div class="info">
<div class="name">午餐</div>
<div class="meta">餐饮 · 12:30</div>
</div>
<div class="amount expense">-¥35.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/></svg>
</div>
<div class="info">
<div class="name">咖啡</div>
<div class="meta">餐饮 · 09:15</div>
</div>
<div class="amount expense">-¥18.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div class="info">
<div class="name">地铁充值</div>
<div class="meta">交通 · 08:45</div>
</div>
<div class="amount expense">-¥50.00</div>
</div>
</div>
<div class="tx-date" style="margin-top:8px">昨天 · 5月26日</div>
<div class="stagger">
<div class="tx-item">
<div class="icon-wrap" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
</div>
<div class="info">
<div class="name">工资</div>
<div class="meta">收入 · 10:00</div>
</div>
<div class="amount income">+¥8,000.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div class="info">
<div class="name">快递</div>
<div class="meta">交通 · 15:42</div>
</div>
<div class="amount expense">-¥28.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
</div>
<div class="info">
<div class="name">超市采购</div>
<div class="meta">购物 · 18:20</div>
</div>
<div class="amount expense">-¥156.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/></svg>
</div>
<div class="info">
<div class="name">下午茶</div>
<div class="meta">餐饮 · 14:30</div>
</div>
<div class="amount expense">-¥25.00</div>
</div>
</div>
<div class="tx-date" style="margin-top:8px">5月25日 · 周六</div>
<div class="stagger">
<div class="tx-item">
<div class="icon-wrap" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
</div>
<div class="info">
<div class="name">网购衣服</div>
<div class="meta">购物 · 20:15</div>
</div>
<div class="amount expense">-¥299.00</div>
</div>
<div class="tx-divider"></div>
<div class="tx-item">
<div class="icon-wrap" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
</div>
<div class="info">
<div class="name">电影票</div>
<div class="meta">娱乐 · 19:00</div>
</div>
<div class="amount expense">-¥78.00</div>
</div>
</div>
</div>
<!-- ==================== PAGE 5: PROFILE ==================== -->
<div class="page" id="page-profile">
<div class="stats-header">我的</div>
<div class="profile-card">
<div class="p-avatar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2"/></svg>
</div>
<div class="p-info">
<div class="name">小菜</div>
<div class="tagline">记账小能手</div>
<div class="streak">已记账 128 天 · 累计 526 笔</div>
</div>
</div>
<div class="quick-stats stagger">
<div class="qs-card">
<div class="label">本月</div>
<div class="value">¥3,280</div>
</div>
<div class="qs-card">
<div class="label">本季</div>
<div class="value">¥9,840</div>
</div>
<div class="qs-card">
<div class="label">本年</div>
<div class="value">¥38,500</div>
</div>
</div>
<div class="menu-section stagger">
<div class="menu-card">
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 000 4h4a2 2 0 010 4H8"/><path d="M12 6v2m0 8v2"/></svg>
</div>
<span class="mi-label">预算设置</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#FFF0E6">
<svg viewBox="0 0 24 24" fill="none" stroke="#FF8C69"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
</div>
<span class="mi-label">分类管理</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</div>
<span class="mi-label">数据导出</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#E8F5E9">
<svg viewBox="0 0 24 24" fill="none" stroke="#7BC67E"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
</div>
<span class="mi-label">数据备份</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
<div class="menu-section stagger">
<div class="menu-card">
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#F3E5F5">
<svg viewBox="0 0 24 24" fill="none" stroke="#B39DDB"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
</div>
<span class="mi-label">深色模式</span>
</div>
<div class="toggle-switch" onclick="this.classList.toggle('on')"><div class="knob"></div></div>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#FFF3E0">
<svg viewBox="0 0 24 24" fill="none" stroke="#FFB347"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</div>
<span class="mi-label">记账提醒</span>
</div>
<div class="toggle-switch on" onclick="this.classList.toggle('on')"><div class="knob"></div></div>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#E3F2FD">
<svg viewBox="0 0 24 24" fill="none" stroke="#6C9BCF"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
</div>
<span class="mi-label">语言</span>
</div>
<span style="font:400 13px/1 var(--font-body);color:var(--text-sec);display:flex;align-items:center;gap:4px">
中文
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</span>
</div>
<div class="menu-item">
<div class="left">
<div class="mi-icon" style="background:#F5F5F5">
<svg viewBox="0 0 24 24" fill="none" stroke="#BFB3B3"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</div>
<span class="mi-label">关于小菜</span>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
</div>
<!-- ==================== BOTTOM NAV ==================== -->
<div class="bottom-nav">
<div class="nav-item active" data-page="home" onclick="switchPage('home')">
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<span>首页</span>
</div>
<div class="nav-item" data-page="stats" onclick="switchPage('stats')">
<svg viewBox="0 0 24 24"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
<span>统计</span>
</div>
<div class="nav-add" onclick="switchPage('add')">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="nav-item" data-page="bills" onclick="switchPage('bills')">
<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span>账单</span>
</div>
<div class="nav-item" data-page="profile" onclick="switchPage('profile')">
<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>我的</span>
</div>
</div>
</div>
</div>
<script>
function switchPage(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById('page-' + page).classList.add('active');
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const activeNav = document.querySelector('.nav-item[data-page="' + page + '"]');
if (activeNav) activeNav.classList.add('active');
}
function switchType(type) {
const toggle = document.getElementById('typeToggle');
const tabs = toggle.querySelectorAll('.tab');
tabs.forEach(t => t.classList.remove('active'));
if (type === 'income') {
toggle.classList.add('income');
tabs[1].classList.add('active');
} else {
toggle.classList.remove('income');
tabs[0].classList.add('active');
}
}
function selectCat(el) {
document.querySelectorAll('.cat-item').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
}
</script>
</body>
</html>