console
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>极简记账 · 收支明细与总结</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #f3f5f9; min-height: 100vh; display: flex; align-items: flex-start; justify-content: center;
padding: 1.5rem; background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.8) 0%, transparent 40%);
}
.app-container {
max-width: 1000px; width: 100%; background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border-radius: 32px; box-shadow: 0 25px 40px -15px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.6) inset;
padding: 2rem; border: 1px solid rgba(255, 255, 255, 0.7); margin-top: 2rem; margin-bottom: 2rem;
}
/* 顶部导航标签 */
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; background: #e2e8f0; padding: 0.4rem; border-radius: 40px; width: fit-content; }
.tab-btn {
padding: 0.6rem 1.5rem; border-radius: 30px; border: none; background: transparent;
font-weight: 600; font-size: 0.95rem; color: #64748b; cursor: pointer; transition: 0.2s; font-family: inherit;
}
.tab-btn.active { background: white; color: #1e3a8a; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
.page-section { display: none; }
.page-section.active { display: block; animation: fadeSlide 0.3s ease; }
/* 头部:时间与汇总 */
.header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; gap: 1rem; }
.datetime {
display: flex; align-items: center; gap: 0.75rem; background: rgba(255, 255, 255, 0.6);
padding: 0.6rem 1.3rem; border-radius: 60px; border: 1px solid rgba(200, 215, 240, 0.6);
font-weight: 500; font-size: 0.95rem; color: #1e293b;
}
.datetime i { color: #3b82f6; font-size: 1.1rem; }
/* 数据管理按钮 */
.data-actions { display: flex; gap: 0.5rem; }
.btn-mini {
padding: 0.5rem 1rem; border-radius: 20px; border: 1px solid #cbd5e1; background: white;
color: #475569; font-size: 0.8rem; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 0.4rem;
}
.btn-mini:hover { background: #f8fafc; border-color: #94a3b8; }
/* 添加表单 */
.add-section { background: #ffffffd9; border-radius: 24px; padding: 1.5rem; border: 1px solid rgba(255, 255, 255, 0.9); margin-bottom: 1.5rem; }
.form-row { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; }
.form-group { flex: 1 1 140px; display: flex; flex-direction: column; gap: 0.4rem; }
.form-group label { font-size: 0.8rem; font-weight: 600; color: #475569; }
.form-group input, .form-group select {
padding: 0.7rem 1rem; border-radius: 12px; border: 1.5px solid #e2e8f0;
font-family: inherit; font-size: 0.95rem; outline: none; transition: 0.15s; width: 100%;
}
.form-group input:focus, .form-group select:focus { border-color: #3b82f6; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); }
.subitems-area { margin-top: 1rem; background: #f8fafc; padding: 1rem; border-radius: 16px; border: 1px dashed #cbd5e1; }
.subitems-header { display: flex; justify-content: space-between; margin-bottom: 0.75rem; font-size: 0.85rem; font-weight: 600; color: #334155; }
.subitem-row { display: flex; gap: 0.6rem; margin-bottom: 0.6rem; align-items: center; }
.subitem-row input { padding: 0.5rem 0.8rem; border-radius: 10px; border: 1px solid #e2e8f0; flex: 1; font-size: 0.9rem; outline: none; }
.subitem-row input:focus { border-color: #3b82f6; }
.subitem-row .sub-amount { max-width: 100px; text-align: right; }
.btn-icon { background: white; border: 1px solid #e2e8f0; border-radius: 10px; width: 34px; height: 34px; cursor: pointer; color: #64748b; flex-shrink: 0; }
.btn-icon:hover { background: #fee2e2; color: #b91c1c; }
.add-sub-btn {
background: transparent; border: 1px dashed #94a3b8; border-radius: 10px; padding: 0.5rem;
font-size: 0.85rem; color: #3b82f6; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 0.4rem; cursor: pointer; width: 100%;
}
.btn-submit {
background: #1e3a8a; color: white; border: none; border-radius: 40px; padding: 0.85rem 2rem;
font-weight: 600; font-size: 0.95rem; display: flex; align-items: center; gap: 0.6rem; cursor: pointer;
box-shadow: 0 8px 18px -6px rgba(30, 58, 138, 0.3); margin-left: auto; margin-top: 1rem;
}
.btn-submit:hover { background: #1e40af; transform: translateY(-1px); }
/* 记录列表 */
.list-header { display: flex; justify-content: space-between; margin-bottom: 1rem; align-items: center; }
.list-header h2 { font-size: 1.25rem; color: #0f172a; display: flex; align-items: center; gap: 0.5rem; }
.record-list { display: flex; flex-direction: column; gap: 0.8rem; max-height: 500px; overflow-y: auto; padding-right: 0.3rem; }
.record-item { background: white; border-radius: 16px; padding: 1rem 1.2rem; display: flex; flex-wrap: wrap; gap: 1rem; border: 1px solid #f1f5f9; transition: 0.15s; }
.record-item:hover { border-color: #dbeafe; }
.record-date-badge { background: #eff6ff; color: #1e3a8a; padding: 0.3rem 0.8rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; height: fit-content; }
.record-main { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; }
.record-top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 0.5rem; }
.record-desc { font-weight: 600; color: #0f172a; display: flex; align-items: center; gap: 0.5rem; }
.category-tag { font-size: 0.7rem; background: #eef2ff; color: #4f46e5; padding: 0.2rem 0.6rem; border-radius: 10px; }
.record-amount { font-weight: 700; font-size: 1.15rem; }
.income-text { color: #0b8e5e; } .expense-text { color: #b91c1c; }
.subitems-list { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; padding-top: 0.5rem; border-top: 1px dashed #e2e8f0; font-size: 0.85rem; color: #475569; }
.subitem-tag { display: flex; align-items: center; gap: 0.3rem; background: #f8fafc; padding: 0.2rem 0.6rem; border-radius: 20px; border: 1px solid #e9eef3; }
.delete-record { background: transparent; border: none; color: #cbd5e1; cursor: pointer; font-size: 1rem; padding: 0.4rem; border-radius: 8px; align-self: center; }
.delete-record:hover { color: #ef4444; background: #fee2e2; }
/* 总结页面样式 */
.summary-controls { display: flex; flex-wrap: wrap; gap: 1rem; background: white; padding: 1.5rem; border-radius: 20px; border: 1px solid #e2e8f0; margin-bottom: 1.5rem; align-items: flex-end; }
.quick-filters { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; width: 100%; }
.filter-btn { padding: 0.5rem 1rem; border-radius: 20px; border: 1px solid #cbd5e1; background: #f8fafc; color: #475569; cursor: pointer; font-size: 0.9rem; transition: 0.2s; }
.filter-btn.active { background: #3b82f6; color: white; border-color: #3b82f6; }
.custom-date-group { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.custom-date-group input { padding: 0.5rem; border-radius: 8px; border: 1px solid #cbd5e1; font-family: inherit; }
.summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.sum-card { background: white; padding: 1.5rem; border-radius: 20px; border: 1px solid #f1f5f9; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.02); }
.sum-card .label { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; font-weight: 500; }
.sum-card .value { font-size: 1.8rem; font-weight: 700; }
.detail-section { background: white; padding: 1.5rem; border-radius: 20px; border: 1px solid #f1f5f9; }
.detail-section h3 { margin-bottom: 1rem; font-size: 1.1rem; color: #1e293b; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.5rem; }
.detail-row { display: flex; justify-content: space-between; padding: 0.6rem 0; border-bottom: 1px dashed #f1f5f9; font-size: 0.95rem; color: #334155; }
.detail-row:last-child { border-bottom: none; }
.empty-state { text-align: center; padding: 2rem; color: #94a3b8; background: #f8fafc; border-radius: 16px; border: 1px dashed #cbd5e1; }
@keyframes fadeSlide { 0% { opacity: 0; transform: translateY(6px); } 100% { opacity: 1; transform: translateY(0); } }
@media (max-width: 600px) {
.app-container { padding: 1.2rem; border-radius: 20px; margin-top: 1rem; }
.form-row { flex-direction: column; align-items: stretch; }
.btn-submit { width: 100%; justify-content: center; }
.custom-date-group { flex-direction: column; align-items: stretch; width: 100%; }
.custom-date-group input { width: 100%; }
}
</style>
</head>
<body>
<div class="app-container">
<!-- 标签页切换 -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('record')"><i class="fas fa-pen"></i> 记一笔</button>
<button class="tab-btn" onclick="switchTab('summary')"><i class="fas fa-chart-pie"></i> 数据总结</button>
</div>
<!-- ================= 记账页面 ================= -->
<div id="page-record" class="page-section active">
<div class="header">
<div class="datetime" id="liveDatetime">
<i class="far fa-calendar-alt"></i>
<span id="datetimeText">加载中...</span>
</div>
<div class="data-actions">
<button class="btn-mini" onclick="exportData()" title="将数据存为文件"><i class="fas fa-file-export"></i> 导出</button>
<button class="btn-mini" onclick="document.getElementById('importFile').click()" title="从文件恢复数据"><i class="fas fa-file-import"></i> 导入</button>
<input type="file" id="importFile" accept=".json" style="display: none;" onchange="importData(event)">
</div>
</div>
<div class="add-section">
<div class="form-row">
<div class="form-group" style="flex: 2 1 200px;">
<label>描述</label>
<input type="text" id="descInput" placeholder="例如:超市购物" maxlength="30">
</div>
<div class="form-group">
<label>类型</label>
<select id="typeSelect">
<option value="expense">支出</option>
<option value="income">收入</option>
</select>
</div>
<div class="form-group">
<label>总金额</label>
<input type="number" id="amountInput" placeholder="0.00" min="0.01" step="0.01">
</div>
<div class="form-group">
<label>日期</label>
<input type="date" id="dateInput">
</div>
</div>
<div class="subitems-area">
<div class="subitems-header">
<span><i class="fas fa-list-ul"></i> 子项明细(可选)</span>
<span style="font-size:0.75rem; font-weight:400; color:#64748b;">合计 ≤ 总金额</span>
</div>
<div class="subitem-list" id="subitemList"></div>
<button class="add-sub-btn" type="button" onclick="addSubRow()">
<i class="fas fa-plus-circle"></i> 添加子项
</button>
</div>
<button class="btn-submit" id="addRecordBtn"><i class="fas fa-check-circle"></i> 保存记录</button>
</div>
<div class="list-header">
<h2><i class="fas fa-receipt"></i> 收支明细</h2>
<span style="font-size:0.8rem; background:#f1f5f9; padding:0.2rem 0.8rem; border-radius:40px; color:#475569;" id="recordCount">0 笔</span>
</div>
<div class="record-list" id="recordList"></div>
</div>
<!-- ================= 总结页面 ================= -->
<div id="page-summary" class="page-section">
<div class="summary-controls">
<div class="quick-filters">
<button class="filter-btn" onclick="setQuickFilter('week', this)">本周</button>
<button class="filter-btn active" onclick="setQuickFilter('month', this)">本月</button>
<button class="filter-btn" onclick="setQuickFilter('year', this)">本年</button>
<button class="filter-btn" onclick="setQuickFilter('all', this)">全部</button>
</div>
<div class="custom-date-group">
<span style="font-size:0.9rem; color:#475569; font-weight:500;">自定义:</span>
<input type="date" id="startDate">
<span>至</span>
<input type="date" id="endDate">
<button class="btn-mini" style="padding: 0.5rem 1.2rem;" onclick="applyCustomFilter()">查询</button>
</div>
</div>
<div class="summary-cards">
<div class="sum-card">
<div class="label">总收入</div>
<div class="value income-text" id="sumIncome">¥0.00</div>
</div>
<div class="sum-card">
<div class="label">总支出</div>
<div class="value expense-text" id="sumExpense">¥0.00</div>
</div>
<div class="sum-card">
<div class="label">结余</div>
<div class="value" id="sumBalance">¥0.00</div>
</div>
</div>
<div class="detail-section">
<h3><i class="fas fa-list-alt"></i> 收支明细汇总</h3>
<div id="sumDetails">
<div class="empty-state">暂无数据</div>
</div>
</div>
</div>
</div>
<script>
// ---------------- 数据管理 ----------------
let records = JSON.parse(localStorage.getItem('jizhang_records_v2')) || [];
function saveRecords() {
localStorage.setItem('jizhang_records_v2', JSON.stringify(records));
}
// ---------------- UI 切换 ----------------
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.page-section').forEach(sec => sec.classList.remove('active'));
if(tabName === 'record') {
document.querySelector('.tab-btn:nth-child(1)').classList.add('active');
document.getElementById('page-record').classList.add('active');
} else {
document.querySelector('.tab-btn:nth-child(2)').classList.add('active');
document.getElementById('page-summary').classList.add('active');
// 默认展示本月总结
setQuickFilter('month', document.querySelector('.filter-btn.active') || document.querySelector('.filter-btn:nth-child(2)'));
}
}
// ---------------- 工具函数 ----------------
function formatMoney(value) {
return '¥' + Number(value).toFixed(2);
}
function generateId() {
return Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
function getTodayStr() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
// ---------------- 时间显示 ----------------
function updateDatetime() {
const now = new Date();
const options = { month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
document.getElementById('datetimeText').textContent = now.toLocaleDateString('zh-CN', options);
}
setInterval(updateDatetime, 1000);
updateDatetime();
// ---------------- 子项操作 ----------------
function addSubRow(desc = '', amount = '') {
const list = document.getElementById('subitemList');
const row = document.createElement('div');
row.className = 'subitem-row';
row.innerHTML = `
<input type="text" class="sub-desc" placeholder="子项名称(如零食)" value="${desc}" maxlength="20">
<input type="number" class="sub-amount" placeholder="金额" value="${amount}" min="0.01" step="0.01">
<button class="btn-icon" type="button" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
`;
list.appendChild(row);
}
function resetSubitems() {
document.getElementById('subitemList').innerHTML = '';
addSubRow();
}
resetSubitems();
// ---------------- 记账功能 ----------------
document.getElementById('addRecordBtn').addEventListener('click', () => {
const desc = document.getElementById('descInput').value.trim();
const type = document.getElementById('typeSelect').value;
const amount = parseFloat(document.getElementById('amountInput').value);
const date = document.getElementById('dateInput').value;
if (!desc || isNaN(amount) || amount <= 0 || !date) {
alert('请正确填写描述、金额和日期!');
return;
}
const subitems = [];
let subTotal = 0;
document.querySelectorAll('.subitem-row').forEach(row => {
const sDesc = row.querySelector('.sub-desc').value.trim();
const sAmt = parseFloat(row.querySelector('.sub-amount').value);
if (sDesc && !isNaN(sAmt) && sAmt > 0) {
subitems.push({ desc: sDesc, amount: sAmt });
subTotal += sAmt;
}
});
if (subitems.length > 0 && subTotal > amount + 0.01) {
alert(`子项总金额 ${formatMoney(subTotal)} 超过了总金额 ${formatMoney(amount)},请调整。`);
return;
}
records.push({ id: generateId(), desc, type, amount, date, subitems });
saveRecords();
// 重置表单
document.getElementById('descInput').value = '';
document.getElementById('amountInput').value = '';
document.getElementById('dateInput').value = getTodayStr();
resetSubitems();
renderRecordList();
});
// 初始日期设为今天
document.getElementById('dateInput').value = getTodayStr();
// ---------------- 渲染记录列表 ----------------
function renderRecordList() {
const listEl = document.getElementById('recordList');
document.getElementById('recordCount').textContent = records.length + ' 笔';
if (records.length === 0) {
listEl.innerHTML = `<div class="empty-state">还没有任何记录,快去记一笔吧!</div>`;
return;
}
const sorted = [...records].sort((a, b) => b.date.localeCompare(a.date));
let html = '';
sorted.forEach(rec => {
const isIncome = rec.type === 'income';
const sign = isIncome ? '+' : '-';
const amountClass = isIncome ? 'income-text' : 'expense-text';
const subHtml = rec.subitems.length > 0
? `<div class="subitems-list">${rec.subitems.map(s => `<span class="subitem-tag"><i class="fas fa-circle" style="font-size:6px; color:#94a3b8;"></i> ${s.desc} ${formatMoney(s.amount)}</span>`).join('')}</div>`
: '';
html += `
<div class="record-item">
<div class="record-date-badge">${rec.date}</div>
<div class="record-main">
<div class="record-top">
<div class="record-desc">${rec.desc} <span class="category-tag">${isIncome ? '收入' : '支出'}</span></div>
<div class="record-amount ${amountClass}">${sign} ${formatMoney(rec.amount)}</div>
</div>
${subHtml}
</div>
<button class="delete-record" onclick="deleteRecord('${rec.id}')"><i class="fas fa-trash-alt"></i></button>
</div>
`;
});
listEl.innerHTML = html;
}
function deleteRecord(id) {
if (confirm('确定删除这条记录吗?')) {
records = records.filter(r => r.id !== id);
saveRecords();
renderRecordList();
}
}
// ---------------- 总结功能核心 ----------------
let currentFilter = 'month'; // week, month, year, all, custom
function setQuickFilter(type, btn) {
currentFilter = type;
if(btn) {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
const today = new Date();
let start = '', end = '';
if (type === 'week') {
// 获取本周一
const day = today.getDay() || 7;
const monday = new Date(today);
monday.setDate(today.getDate() - day + 1);
start = monday.toISOString().slice(0, 10);
end = getTodayStr();
} else if (type === 'month') {
start = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-01`;
end = getTodayStr();
} else if (type === 'year') {
start = `${today.getFullYear()}-01-01`;
end = getTodayStr();
} else if (type === 'all') {
start = '1970-01-01';
end = '2099-12-31';
}
document.getElementById('startDate').value = start;
document.getElementById('endDate').value = end;
calculateSummary(start, end);
}
function applyCustomFilter() {
const start = document.getElementById('startDate').value;
const end = document.getElementById('endDate').value;
if (!start || !end) { alert('请选择完整的开始和结束日期'); return; }
if (start > end) { alert('开始日期不能晚于结束日期'); return; }
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
calculateSummary(start, end);
}
function calculateSummary(start, end) {
let totalIncome = 0;
let totalExpense = 0;
const incomeDetails = {};
const expenseDetails = {};
records.forEach(rec => {
if (rec.date >= start && rec.date <= end) {
if (rec.type === 'income') {
totalIncome += rec.amount;
incomeDetails[rec.desc] = (incomeDetails[rec.desc] || 0) + rec.amount;
} else {
totalExpense += rec.amount;
expenseDetails[rec.desc] = (expenseDetails[rec.desc] || 0) + rec.amount;
}
}
});
// 更新卡片
document.getElementById('sumIncome').textContent = formatMoney(totalIncome);
document.getElementById('sumExpense').textContent = formatMoney(totalExpense);
const balance = totalIncome - totalExpense;
const balanceEl = document.getElementById('sumBalance');
balanceEl.textContent = formatMoney(balance);
balanceEl.className = 'value ' + (balance >= 0 ? 'income-text' : 'expense-text');
// 更新明细列表
const detailEl = document.getElementById('sumDetails');
if (Object.keys(incomeDetails).length === 0 && Object.keys(expenseDetails).length === 0) {
detailEl.innerHTML = `<div class="empty-state">该时间段内没有记录</div>`;
return;
}
let detailHtml = '';
if (Object.keys(incomeDetails).length > 0) {
detailHtml += `<div style="font-weight:600; margin: 0.5rem 0; color:#0b8e5e;">收入项</div>`;
for (let [k, v] of Object.entries(incomeDetails).sort((a,b) => b[1]-a[1])) {
detailHtml += `<div class="detail-row"><span>${k}</span><span class="income-text">${formatMoney(v)}</span></div>`;
}
}
if (Object.keys(expenseDetails).length > 0) {
detailHtml += `<div style="font-weight:600; margin: 1rem 0 0.5rem; color:#b91c1c;">支出项</div>`;
for (let [k, v] of Object.entries(expenseDetails).sort((a,b) => b[1]-a[1])) {
detailHtml += `<div class="detail-row"><span>${k}</span><span class="expense-text">${formatMoney(v)}</span></div>`;
}
}
detailEl.innerHTML = detailHtml;
}
// ---------------- 数据导入/导出 ----------------
function exportData() {
if (records.length === 0) { alert('暂无数据可导出'); return; }
const dataStr = JSON.stringify(records, null, 2);
const blob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `记账备份_${getTodayStr()}.json`;
a.click();
URL.revokeObjectURL(url);
}
function importData(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const imported = JSON.parse(e.target.result);
if (Array.isArray(imported)) {
if (confirm(`读取到 ${imported.length} 条记录。\\n是否与当前数据进行合并?(点击取消将覆盖当前数据)`)) {
// 合并去重
const existingIds = new Set(records.map(r => r.id));
imported.forEach(r => { if(!existingIds.has(r.id)) records.push(r); });
} else {
// 覆盖
records = imported;
}
saveRecords();
renderRecordList();
alert('导入成功!');
} else {
alert('文件格式错误:不是有效的记账数据');
}
} catch (err) {
alert('解析文件失败,请确保是导出的 JSON 文件');
}
};
reader.readAsText(file);
event.target.value = ''; // 清空 input,允许重复选文件
}
// ---------------- 初始化 ----------------
renderRecordList();
</script>
</body>
</html>