欢迎光临
我们一直在努力

随礼记事本html

prompt
写一个随份子记录的记事本纯html文本,美观精致,兼容pc和手机,可以下拉选择是由,自己脑补常见随份子事由,最多的结婚、生子,升学,升职,乔迁,拜年,白事吧。 随礼人,收礼人,礼金,备注,然后保存。 可以根据随礼人或收礼人筛选随礼明细,自动根据筛选出的明细统计出总随礼金额,今年金额,本月金额

做好了,单文件、零依赖、双击即用,数据存在浏览器本地。

随礼记事本.html

记一笔:事由下拉(17 种,含你点名的结婚 💍、生子 👶、升学 🎓、升职 📈、乔迁 🏠、拜年 🧧、白事 🕯️,另补了满月酒、寿宴、订婚、周岁、开业、参军、退休、探望、回礼、其他)、随礼人、收礼人、礼金、日期、备注。礼金下面有 200/500/600/800/1000/2000 快捷按钮,点一下就填好。

筛选:随礼人、收礼人两个下拉会自动收录你填过的所有人名(不用重复打字),外加事由下拉、关键词搜索(名字和备注都能搜)、时间快捷(今年 / 本月 / 上月)。条件可以叠加,比如"随礼人=我 + 事由=白事"。

统计(跟着筛选结果实时变,不是死数字):
• 总随礼金额 + 笔数

• 今年金额 + 笔数

• 本月金额 + 笔数

• 笔均金额

所以筛"张三"时,上面显示的就是跟张三往来的账,而不是全局账——这点我特意验证过:筛出 1000 元的旧记录后,"今年金额"正确归零。

其他顺手做的
• 明细按月份分组,每月带小计;点 ✎ 可改,✕ 删除后 6 秒内可撤销(防误删)

• 导出 CSV(Excel 可开,带 BOM 不乱码)、备份 JSON、从备份/CSV 导入、清空(二次确认)

• 名字输入会自动补全历史人名;PC 两栏、手机单列,输入框 16px 防 iOS 缩放

验证:构造了跨年/跨月/上月/往来的 6 条数据跑断言——总额 5200、今年 2200、本月 1400、上月 1 笔、组合筛选、金额千分位与两位小数、刷新后从本地恢复,全部通过。

PC端预览:
在这里插入图片描述

手机预览:
在这里插入图片描述

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#f7f3ec">
<title>随礼记事本 · 人情往来账</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{
–paper:#f7f3ec;
–card:#fffdf9;
–ink:#2b2620;
–ink2:#6b6259;
–dim:#9a9088;
–red:#b8352a;
–red2:#d9534a;
–gold:#b8873b;
–line:rgba(43,38,32,.10);
–line2:rgba(43,38,32,.06);
–ok:#2f7a52;
–radius:14px;
–shadow:0 2px 4px rgba(43,38,32,.03),0 8px 24px rgba(43,38,32,.05);
}
html,body{height:100%}
body{
background:
radial-gradient(900px 500px at 8% -8%, rgba(184,53,42,.07), transparent 60%),
radial-gradient(800px 500px at 100% 0%, rgba(184,135,59,.10), transparent 60%),
var(–paper);
color:var(–ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
font-size:15px;line-height:1.5;
-webkit-font-smoothing:antialiased;
}
/* 纸质纹理 */
body::before{
content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.5;
background-image:
repeating-linear-gradient(0deg,rgba(43,38,32,.018) 0 1px,transparent 1px 4px),
repeating-linear-gradient(90deg,rgba(43,38,32,.014) 0 1px,transparent 1px 4px);
}
.wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:16px 14px calc(28px + env(safe-area-inset-bottom));}

/* ===== 页头 ===== */
header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.title{display:flex;align-items:center;gap:11px;min-width:0}
.seal{
width:42px;height:42px;flex:0 0 auto;border-radius:11px;
background:linear-gradient(145deg,var(–red2),var(–red));
color:#fff;display:grid;place-items:center;
font-family:"Songti SC","STSong","SimSun",serif;font-size:22px;font-weight:700;
box-shadow:0 6px 16px rgba(184,53,42,.32),inset 0 1px 0 rgba(255,255,255,.35);
letter-spacing:-1px;
}
.title h1{
font-family:"Songti SC","STSong","Noto Serif SC","SimSun",serif;
font-size:clamp(19px,3.2vw,25px);font-weight:700;letter-spacing:2px;white-space:nowrap;
}
.title p{font-size:11px;color:var(–dim);letter-spacing:3px;margin-top:1px}
.tools{display:flex;gap:7px;flex-wrap:wrap}
.tool{
border:1px solid var(–line);background:var(–card);color:var(–ink2);
height:34px;padding:0 11px;border-radius:10px;font-size:12.5px;cursor:pointer;
display:inline-flex;align-items:center;gap:5px;transition:.16s;box-shadow:0 1px 2px rgba(43,38,32,.04);
font-family:inherit;
}
.tool:hover{border-color:rgba(184,53,42,.35);color:var(–red);transform:translateY(-1px)}
.tool:active{transform:scale(.96)}
.tool.danger:hover{border-color:var(–red);color:var(–red)}

/* ===== 统计卡 ===== */
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px}
.stat{
background:var(–card);border:1px solid var(–line);border-radius:var(–radius);
padding:12px 13px;box-shadow:var(–shadow);position:relative;overflow:hidden;
}
.stat::after{
content:"";position:absolute;right:-14px;top:-14px;width:56px;height:56px;border-radius:50%;
background:radial-gradient(circle,rgba(184,53,42,.10),transparent 70%);
}
.stat.gold::after{background:radial-gradient(circle,rgba(184,135,59,.16),transparent 70%)}
.stat h4{font-size:11px;color:var(–dim);font-weight:600;letter-spacing:1.5px;margin-bottom:5px}
.stat .num{
font-size:clamp(19px,2.6vw,26px);font-weight:700;letter-spacing:-.5px;
font-variant-numeric:tabular-nums;color:var(–ink);line-height:1.15;word-break:break-all;
}
.stat.red .num{color:var(–red)}
.stat.gold .num{color:var(–gold)}
.stat .sub{font-size:10.5px;color:var(–dim);margin-top:4px}
.stat .sub b{color:var(–ink2);font-weight:600}

/* ===== 主区 ===== */
.stage{display:grid;grid-template-columns:340px 1fr;gap:14px;align-items:start}
.col{display:flex;flex-direction:column;gap:12px;min-width:0}
.card{
background:var(–card);border:1px solid var(–line);border-radius:var(–radius);
padding:14px;box-shadow:var(–shadow);
}
.card > h3{
font-family:"Songti SC","STSong","Noto Serif SC",serif;
font-size:14.5px;font-weight:700;letter-spacing:1.5px;margin-bottom:11px;
display:flex;align-items:center;gap:7px;
}
.card > h3::before{content:"";width:3px;height:14px;border-radius:2px;background:linear-gradient(var(–red2),var(–gold))}
.card > h3 small{margin-left:auto;font-size:10.5px;color:var(–dim);letter-spacing:.5px;font-weight:500;font-family:inherit}

/* ===== 表单 ===== */
.field{margin-bottom:10px}
.field label{display:block;font-size:11.5px;color:var(–ink2);font-weight:600;margin-bottom:4px;letter-spacing:.5px}
.field label i{color:var(–red);font-style:normal;margin-left:2px}
input,select,textarea,button{font-family:inherit;font-size:16px}
.ipt{
width:100%;height:40px;padding:0 11px;border-radius:10px;
border:1px solid var(–line);background:#fff;color:var(–ink);
outline:none;transition:.16s;font-size:16px;
}
.ipt:focus{border-color:var(–red);box-shadow:0 0 0 3px rgba(184,53,42,.10)}
.ipt::placeholder{color:#c3bcb3}
textarea.ipt{height:56px;padding:9px 11px;resize:none;line-height:1.5}
select.ipt{
appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:30px;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4.5L6 8.5L10 4.5' stroke='%239a9088' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
background-repeat:no-repeat;background-position:right 11px center;
}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.row3{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.quick{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px}
.quick button{
border:1px solid var(–line);background:#fff;color:var(–ink2);
height:27px;padding:0 9px;border-radius:8px;font-size:12px;cursor:pointer;transition:.14s;
}
.quick button:hover{border-color:var(–gold);color:var(–gold);background:rgba(184,135,59,.06)}
.quick button:active{transform:scale(.94)}
.btn{
width:100%;height:44px;border:0;border-radius:11px;cursor:pointer;
font-size:15px;font-weight:700;letter-spacing:3px;color:#fff;
background:linear-gradient(135deg,var(–red2),var(–red));
box-shadow:0 6px 18px rgba(184,53,42,.28),inset 0 1px 0 rgba(255,255,255,.25);
transition:.16s;margin-top:3px;
}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 24px rgba(184,53,42,.34)}
.btn:active{transform:scale(.985)}
.btn.ghost{
background:#fff;color:var(–ink2);border:1px solid var(–line);
box-shadow:none;font-weight:600;letter-spacing:1px;height:38px;font-size:13.5px;
}
.btn.ghost:hover{color:var(–red);border-color:rgba(184,53,42,.4)}
.form-actions{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center}
.form-actions .btn.ghost{width:auto;padding:0 16px}

/* ===== 筛选 ===== */
.filters{display:grid;gap:9px}
.chips{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
.chip{
border:1px solid var(–line);background:#fff;color:var(–ink2);
height:28px;padding:0 11px;border-radius:20px;font-size:12px;cursor:pointer;transition:.14s;
}
.chip:hover{border-color:var(–red);color:var(–red)}
.chip.on{background:var(–red);border-color:var(–red);color:#fff}

/* ===== 明细 ===== */
.list-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
.month-bar{
display:flex;align-items:baseline;gap:8px;padding:12px 0 6px;
font-size:12px;color:var(–dim);letter-spacing:1px;
}
.month-bar:first-child{padding-top:2px}
.month-bar .line{flex:1;height:1px;background:var(–line2)}
.month-bar .msum{font-weight:700;color:var(–gold);font-variant-numeric:tabular-nums}
.item{
display:flex;align-items:center;gap:11px;padding:10px 11px;border-radius:12px;
border:1px solid var(–line2);background:#fff;margin-bottom:7px;transition:.16s;position:relative;
}
.item:hover{border-color:rgba(184,53,42,.25);box-shadow:0 4px 14px rgba(43,38,32,.06);transform:translateX(2px)}
.ic{
width:38px;height:38px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;font-size:19px;
background:linear-gradient(145deg,#fdf6ec,#f6ecdd);
}
.body{flex:1;min-width:0}
.body .l1{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.tag{
font-size:10.5px;padding:1px 7px;border-radius:6px;background:rgba(184,53,42,.09);color:var(–red);
font-weight:600;letter-spacing:.5px;flex:0 0 auto;
}
.tag.g{background:rgba(184,135,59,.13);color:var(–gold)}
.tag.n{background:rgba(43,38,32,.07);color:var(–ink2)}
.who{font-size:14px;font-weight:600;letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.arrow{color:var(–dim);font-size:12px}
.body .l2{font-size:11.5px;color:var(–dim);margin-top:2px;display:flex;gap:8px;flex-wrap:wrap}
.body .note{
font-size:11.5px;color:var(–ink2);margin-top:3px;background:#faf7f2;border-radius:6px;
padding:2px 7px;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.amt{
flex:0 0 auto;text-align:right;font-size:17px;font-weight:700;color:var(–red);
font-variant-numeric:tabular-nums;letter-spacing:-.3px;
}
.amt small{display:block;font-size:10px;color:var(–dim);font-weight:500;letter-spacing:0}
.ops{display:flex;gap:4px;flex:0 0 auto;opacity:.45;transition:.16s}
.item:hover .ops{opacity:1}
.ops button{
width:28px;height:28px;border-radius:8px;border:1px solid var(–line);background:#fff;
cursor:pointer;font-size:12px;color:var(–ink2);display:grid;place-items:center;transition:.14s;padding:0;
}
.ops button:hover{border-color:var(–red);color:var(–red)}
.empty{text-align:center;padding:34px 16px;color:var(–dim)}
.empty .em{font-size:38px;opacity:.35;margin-bottom:8px}
.empty p{font-size:13px;line-height:1.9}

/* ===== toast ===== */
.toast{
position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom));transform:translate(-50%,20px);
background:rgba(43,38,32,.94);color:#fff;padding:11px 18px;border-radius:12px;font-size:13.5px;
box-shadow:0 10px 30px rgba(43,38,32,.3);z-index:99;opacity:0;pointer-events:none;transition:.26s;
display:flex;align-items:center;gap:12px;max-width:calc(100vw – 32px);backdrop-filter:blur(6px);
}
.toast.show{opacity:1;transform:translate(-50%,0);pointer-events:auto}
.toast button{
border:0;background:rgba(255,255,255,.16);color:#fff;padding:5px 12px;border-radius:8px;
cursor:pointer;font-size:12.5px;font-weight:600;
}
.toast button:hover{background:rgba(255,255,255,.28)}

/* ===== 弹层确认 ===== */
.mask{
position:fixed;inset:0;background:rgba(43,38,32,.42);z-index:100;display:grid;place-items:center;
opacity:0;pointer-events:none;transition:.2s;backdrop-filter:blur(3px);padding:20px;
}
.mask.show{opacity:1;pointer-events:auto}
.dlg{
background:var(–card);border-radius:16px;padding:20px;max-width:330px;width:100%;
box-shadow:0 20px 60px rgba(43,38,32,.3);text-align:center;
transform:scale(.94);transition:.2s;
}
.mask.show .dlg{transform:scale(1)}
.dlg h3{font-family:"Songti SC","STSong",serif;font-size:17px;letter-spacing:1px;margin-bottom:8px}
.dlg p{font-size:13px;color:var(–ink2);line-height:1.8;margin-bottom:16px}
.dlg .row{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.dlg button{height:40px;border-radius:10px;border:1px solid var(–line);background:#fff;cursor:pointer;font-size:14px;font-weight:600;color:var(–ink2)}
.dlg button.pri{background:linear-gradient(135deg,var(–red2),var(–red));color:#fff;border:0}

@media (max-width:860px){
.stage{grid-template-columns:1fr}
.stats{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:420px){
.wrap{padding:12px 11px calc(24px + env(safe-area-inset-bottom))}
.stats{gap:8px}
.stat{padding:10px 11px}
.tool{padding:0 9px;font-size:12px}
.title small{display:none}
}
@media (hover:none){
.ops{opacity:1}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="title">
<div class="seal"></div>
<div>
<h1>随礼记事本</h1>
<p>人情往来 · 一笔记清</p>
</div>
</div>
<div class="tools">
<button class="tool" id="btnCsv" title="导出 CSV(可用 Excel 打开)">📄 导出</button>
<button class="tool" id="btnBak" title="备份为 JSON 文件">💾 备份</button>
<button class="tool" id="btnImp" title="从备份或 CSV 导入">📥 导入</button>
<button class="tool danger" id="btnClr" title="清空全部记录">🗑 清空</button>
<input type="file" id="fileIn" accept=".json,.csv,application/json,text/csv" hidden>
</div>
</header>

<!– 统计 –>
<section class="stats">
<div class="stat red">
<h4>总随礼金额</h4>
<div class="num" id="sumAll">¥0</div>
<div class="sub"><b id="cntAll">0</b></div>
</div>
<div class="stat gold">
<h4>今年金额</h4>
<div class="num" id="sumYear">¥0</div>
<div class="sub"><b id="cntYear">0</b> 笔 · <span id="yearLabel"></span></div>
</div>
<div class="stat">
<h4>本月金额</h4>
<div class="num" id="sumMonth">¥0</div>
<div class="sub"><b id="cntMonth">0</b> 笔 · <span id="monthLabel"></span></div>
</div>
<div class="stat">
<h4>笔均金额</h4>
<div class="num" id="sumAvg">¥0</div>
<div class="sub"><b id="cntRecent">0</b> 笔均值</div>
</div>
</section>

<div class="stage">
<!– 左列 –>
<div class="col">
<div class="card">
<h3><span id="formTitle">记一笔</span> <small id="formHint">随出随收都记下</small></h3>
<div class="field">
<label>事由 <i>*</i></label>
<select class="ipt" id="fReason"></select>
</div>
<div class="row2">
<div class="field">
<label>随礼人 <i>*</i></label>
<input class="ipt" id="fFrom" list="nameList" placeholder="谁随的" autocomplete="off">
</div>
<div class="field">
<label>收礼人 <i>*</i></label>
<input class="ipt" id="fTo" list="nameList" placeholder="随给谁" autocomplete="off">
</div>
</div>
<div class="row3">
<div class="field">
<label>礼金(元)<i>*</i></label>
<input class="ipt" id="fAmount" type="number" inputmode="decimal" min="0" step="1" placeholder="0">
</div>
<div class="field">
<label>日期</label>
<input class="ipt" id="fDate" type="date">
</div>
</div>
<div class="quick" id="quickAmt"></div>
<div class="field" style="margin-top:10px">
<label>备注</label>
<textarea class="ipt" id="fNote" placeholder="如:同事小王 / 酒店宴会厅 / 已回礼" maxlength="120"></textarea>
</div>
<div class="form-actions">
<button class="btn" id="btnSave">记 一 笔</button>
<button class="btn ghost" id="btnCancel" style="display:none">取消</button>
</div>
<datalist id="nameList"></datalist>
</div>

<div class="card">
<h3>筛选明细 <small id="filterHint">全部</small></h3>
<div class="filters">
<div class="row2">
<div>
<label style="display:block;font-size:11.5px;color:var(–ink2);font-weight:600;margin-bottom:4px">随礼人</label>
<select class="ipt" id="qFrom"></select>
</div>
<div>
<label style="display:block;font-size:11.5px;color:var(–ink2);font-weight:600;margin-bottom:4px">收礼人</label>
<select class="ipt" id="qTo"></select>
</div>
</div>
<div>
<label style="display:block;font-size:11.5px;color:var(–ink2);font-weight:600;margin-bottom:4px">事由</label>
<select class="ipt" id="qReason"></select>
</div>
<div>
<label style="display:block;font-size:11.5px;color:var(–ink2);font-weight:600;margin-bottom:4px">关键词</label>
<input class="ipt" id="qKey" placeholder="搜名字 / 备注" autocomplete="off">
</div>
<div class="chips" id="rangeChips">
<button class="chip on" data-range="all">全部时间</button>
<button class="chip" data-range="year">今年</button>
<button class="chip" data-range="month">本月</button>
<button class="chip" data-range="lastmonth">上月</button>
</div>
<button class="btn ghost" id="btnReset">重置筛选</button>
</div>
</div>
</div>

<!– 右列:明细 –>
<div class="col">
<div class="card">
<h3>随礼明细 <small id="listHint"></small></h3>
<div id="list"></div>
</div>
</div>
</div>
</div>

<div class="toast" id="toast"><span id="toastMsg"></span><button id="toastAct" style="display:none">撤销</button></div>

<div class="mask" id="mask">
<div class="dlg">
<h3 id="dlgTitle">确认</h3>
<p id="dlgText"></p>
<div class="row">
<button id="dlgNo">取消</button>
<button id="dlgYes" class="pri">确定</button>
</div>
</div>
</div>

<script>
(function(){
'use strict';

/* ================= 事由字典 ================= */
const REASONS = [
{k:'结婚', e:'💍', t:'r'},
{k:'生子', e:'👶', t:'r'},
{k:'满月酒', e:'🍼', t:'r'},
{k:'升学', e:'🎓', t:'g'},
{k:'升职', e:'📈', t:'g'},
{k:'乔迁', e:'🏠', t:'g'},
{k:'拜年', e:'🧧', t:'r'},
{k:'白事', e:'🕯️', t:'n'},
{k:'寿宴', e:'🎂', t:'r'},
{k:'订婚', e:'💐', t:'r'},
{k:'周岁', e:'🎈', t:'r'},
{k:'开业', e:'🎉', t:'g'},
{k:'参军', e:'🎖️', t:'g'},
{k:'退休', e:'🌅', t:'n'},
{k:'探望', e:'🍎', t:'n'},
{k:'回礼', e:'🔁', t:'n'},
{k:'其他', e:'📝', t:'n'}
];
const RMAP = {};
REASONS.forEach(r=>RMAP[r.k]=r);
const QUICK = [200,500,600,800,1000,2000];
const KEY = 'suili_records_v1';

/* ================= 工具 ================= */
const $ = id => document.getElementById(id);
const pad = n => (n<10?'0':'') + n;
function todayStr(){ const d=new Date(); return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }
function ym(s){ return s.slice(0,7); }
function yr(s){ return s.slice(0,4); }
function fmt(n){
const v = Math.round(n*100)/100;
let s = v.toFixed(2).replace(/\\.00$/,'');
const parts = s.split('.');
parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');
return '¥' + parts.join('.');
}
function esc(s){
return String(s).replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function uid(){ return Date.now().toString(36) + Math.random().toString(36).slice(2,7); }
function monthLabel(m){ const [y,mm]=m.split('-'); return y+'年'+(+mm)+'月'; }

/* ================= 数据 ================= */
let records = [];
let filter = {from:'', to:'', reason:'', key:'', range:'all'};
let editingId = null;
let pendingDel = null, toastTimer = null;

function load(){
try{
const raw = localStorage.getItem(KEY);
records = raw ? JSON.parse(raw) : [];
if(!Array.isArray(records)) records = [];
}catch(e){ records = []; }
records.forEach(r=>{
if(!r.id) r.id = uid();
if(!r.date) r.date = todayStr();
r.amount = Number(r.amount) || 0;
r.from = r.from || '';
r.to = r.to || '';
r.note = r.note || '';
if(!RMAP[r.reason]) r.reason = '其他';
});
}
function save(){
try{ localStorage.setItem(KEY, JSON.stringify(records)); }
catch(e){ toast('保存失败:浏览器存储不可用'); }
}
function sortRecords(){
records.sort((a,b)=> a.date < b.date ? 1 : (a.date > b.date ? 1 : 0));
}

/* ================= 统计 ================= */
function inRange(r, range){
if(range === 'all') return true;
const now = new Date(), y = now.getFullYear(), m = pad(now.getMonth()+1);
const cy = String(y), cm = cy + '-' + m;
if(range === 'year') return yr(r.date) === cy;
if(range === 'month') return ym(r.date) === cm;
if(range === 'lastmonth'){
let pm = now.getMonth() 1, py = y;
if(pm < 0){ pm = 11; py = y 1; }
return ym(r.date) === py + '-' + pad(pm+1);
}
return true;
}
function filtered(){
const k = filter.key.trim().toLowerCase();
return records.filter(r=>{
if(filter.from && r.from !== filter.from) return false;
if(filter.to && r.to !== filter.to) return false;
if(filter.reason && r.reason !== filter.reason) return false;
if(!inRange(r, filter.range)) return false;
if(k){
const hay = (r.from+' '+r.to+' '+r.note+' '+r.reason).toLowerCase();
if(hay.indexOf(k) < 0) return false;
}
return true;
});
}
function sum(list){ return list.reduce((s,r)=>s + (Number(r.amount)||0), 0); }

function renderStats(list){
const now = new Date(), cy = String(now.getFullYear()), cm = cy + '-' + pad(now.getMonth()+1);
const yearList = list.filter(r=>yr(r.date) === cy);
const monthList = list.filter(r=>ym(r.date) === cm);
$('sumAll').textContent = fmt(sum(list));
$('cntAll').textContent = list.length;
$('sumYear').textContent = fmt(sum(yearList));
$('cntYear').textContent = yearList.length;
$('sumMonth').textContent = fmt(sum(monthList));
$('cntMonth').textContent = monthList.length;
$('sumAvg').textContent = list.length ? fmt(sum(list)/list.length) : '¥0';
$('cntRecent').textContent= list.length;
$('yearLabel').textContent = cy + '年';
$('monthLabel').textContent = now.getFullYear() + '年' + (now.getMonth()+1) + '月';
}

/* ================= 渲染 ================= */
function fillSelect(sel, options, placeholder){
const cur = sel.value;
sel.innerHTML = '';
const o = document.createElement('option');
o.value = ''; o.textContent = placeholder;
sel.appendChild(o);
options.forEach(v=>{
const op = document.createElement('option');
op.value = v; op.textContent = v;
sel.appendChild(op);
});
if(options.indexOf(cur) >= 0 || cur === '') sel.value = cur;
}
function allNames(){
const s = new Set();
records.forEach(r=>{ if(r.from) s.add(r.from); if(r.to) s.add(r.to); });
return Array.from(s).sort((a,b)=>a.localeCompare(b,'zh-CN'));
}
function renderSelects(){
const names = allNames();
$('nameList').innerHTML = names.map(n=>'<option value="'+esc(n)+'"></option>').join('');
fillSelect($('qFrom'), names, '全部随礼人');
fillSelect($('qTo'), names, '全部收礼人');
fillSelect($('qReason'), REASONS.map(r=>r.k), '全部事由');
}
function renderList(){
const list = filtered();
const box = $('list');
renderStats(list);

const bits = [];
if(filter.from) bits.push('随礼人 ' + filter.from);
if(filter.to) bits.push('收礼人 ' + filter.to);
if(filter.reason) bits.push(filter.reason);
if(filter.range !== 'all') bits.push({year:'今年',month:'本月',lastmonth:'上月'}[filter.range]);
if(filter.key.trim()) bits.push('“' + filter.key.trim() + '”');
const active = bits.length > 0;
$('filterHint').textContent = active ? bits.join(' · ') : '全部';
$('listHint').textContent = list.length ? '共 ' + list.length + ' 笔 · ' + fmt(sum(list)) : '';

if(!list.length){
box.innerHTML = '<div class="empty"><div class="em">🧾</div><p>' +
(records.length ? '没有符合条件的记录<br>换个筛选条件试试' : '还没有记录<br>在左边记下第一笔随礼吧') +
'</p></div>';
return;
}

let html = '', lastMonth = '';
list.forEach(r=>{
const m = ym(r.date);
if(m !== lastMonth){
lastMonth = m;
const mList = list.filter(x=>ym(x.date) === m);
html += '<div class="month-bar"><span>' + monthLabel(m) + '</span>' +
'<span class="line"></span><span>' + mList.length + ' 笔</span>' +
'<span class="msum">' + fmt(sum(mList)) + '</span></div>';
}
const meta = RMAP[r.reason] || {e:'📝', t:'n'};
html += '<div class="item" data-id="' + r.id + '">' +
'<div class="ic">' + meta.e + '</div>' +
'<div class="body">' +
'<div class="l1"><span class="tag ' + meta.t + '">' + esc(r.reason) + '</span>' +
'<span class="who">' + esc(r.from) + '</span>' +
'<span class="arrow">→</span>' +
'<span class="who">' + esc(r.to) + '</span></div>' +
'<div class="l2"><span>' + esc(r.date) + '</span></div>' +
(r.note ? '<div class="note">📎 ' + esc(r.note) + '</div>' : '') +
'</div>' +
'<div class="amt">' + fmt(r.amount) + '</div>' +
'<div class="ops">' +
'<button data-act="edit" title="编辑">✎</button>' +
'<button data-act="del" title="删除">✕</button>' +
'</div></div>';
});
box.innerHTML = html;
}

/* ================= 表单 ================= */
function initForm(){
const sel = $('fReason');
sel.innerHTML = REASONS.map(r=>'<option value="' + r.k + '">' + r.e + ' ' + r.k + '</option>').join('');
$('fDate').value = todayStr();
$('quickAmt').innerHTML = QUICK.map(v=>'<button type="button" data-amt="' + v + '">' + v + '</button>').join('');
}
function clearForm(){
editingId = null;
$('fReason').selectedIndex = 0;
$('fFrom').value = ''; $('fTo').value = '';
$('fAmount').value = ''; $('fNote').value = '';
$('fDate').value = todayStr();
$('formTitle').textContent = '记一笔';
$('formHint').textContent = '随出随收都记下';
$('btnSave').textContent = '记 一 笔';
$('btnCancel').style.display = 'none';
}
function startEdit(id){
const r = records.find(x=>x.id === id);
if(!r) return;
editingId = id;
$('fReason').value = r.reason;
$('fFrom').value = r.from;
$('fTo').value = r.to;
$('fAmount').value = r.amount;
$('fDate').value = r.date;
$('fNote').value = r.note;
$('formTitle').textContent = '修改记录';
$('formHint').textContent = '改完记得保存';
$('btnSave').textContent = '保 存';
$('btnCancel').style.display = '';
if(window.innerWidth <= 860) window.scrollTo({top:0, behavior:'smooth'});
$('fFrom').focus();
}
function submit(){
const reason = $('fReason').value;
const from = $('fFrom').value.trim();
const to = $('fTo').value.trim();
const amount = parseFloat($('fAmount').value);
const date = $('fDate').value || todayStr();
const note = $('fNote').value.trim();
if(!from){ toast('请填写随礼人'); $('fFrom').focus(); return; }
if(!to){ toast('请填写收礼人'); $('fTo').focus(); return; }
if(!(amount > 0)){ toast('请填写正确的礼金金额'); $('fAmount').focus(); return; }

if(editingId){
const r = records.find(x=>x.id === editingId);
if(r){ Object.assign(r, {reason, from, to, amount, date, note}); }
toast('已保存修改');
}else{
records.push({id:uid(), reason, from, to, amount, date, note});
toast('记好啦 · ' + from + ' → ' + to + ' ' + fmt(amount));
}
sortRecords(); save(); clearForm(); renderSelects(); renderList();
}

/* ================= 删除 / 撤销 ================= */
function delRecord(id){
const idx = records.findIndex(x=>x.id === id);
if(idx < 0) return;
pendingDel = {rec:records[idx], idx:idx};
records.splice(idx,1);
save(); renderSelects(); renderList();
toast('已删除「' + pendingDel.rec.from + ' → ' + pendingDel.rec.to + '」', '撤销', ()=>{
if(!pendingDel) return;
records.splice(Math.min(pendingDel.idx, records.length), 0, pendingDel.rec);
pendingDel = null; save(); renderSelects(); renderList(); toast('已恢复');
});
}
function toast(msg, btnText, onAct){
const t = $('toast'), b = $('toastAct');
$('toastMsg').textContent = msg;
if(btnText){
b.style.display = ''; b.textContent = btnText;
b.onclick = ()=>{ hideToast(); if(onAct) onAct(); };
}else{
b.style.display = 'none'; b.onclick = null;
}
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(hideToast, 4200);
}
function hideToast(){ $('toast').classList.remove('show'); }

/* ================= 对话框 ================= */
let dlgCb = null;
function confirmDlg(title, text, cb){
$('dlgTitle').textContent = title;
$('dlgText').textContent = text;
dlgCb = cb;
$('mask').classList.add('show');
}
$('dlgNo').onclick = ()=>{ $('mask').classList.remove('show'); dlgCb = null; };
$('dlgYes').onclick = ()=>{ $('mask').classList.remove('show'); const f = dlgCb; dlgCb = null; if(f) f(); };
$('mask').onclick = e=>{ if(e.target === $('mask')) $('dlgNo').click(); };

/* ================= 导入导出 ================= */
function download(name, content, type){
const blob = new Blob([content], {type:type});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = name;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
setTimeout(()=>URL.revokeObjectURL(url), 1500);
}
function toCsv(){
const head = ['日期','事由','随礼人','收礼人','礼金','备注'];
const rows = records.map(r=>[r.date, r.reason, r.from, r.to, r.amount, r.note]);
const q = v => '"' + String(v).replace(/"/g,'""') + '"';
return '\\ufeff' + [head].concat(rows).map(row=>row.map(q).join(',')).join('\\r\\n');
}
function parseCsv(text){
const lines = text.replace(/^\\ufeff/,'').split(/\\r?\\n/).filter(l=>l.trim());
const out = [];
const split = line=>{
const res = []; let cur = '', q = false;
for(let i=0;i<line.length;i++){
const c = line[i];
if(c === '"'){ if(q && line[i+1] === '"'){ cur += '"'; i++; } else q = !q; }
else if(c === ',' && !q){ res.push(cur); cur = ''; }
else cur += c;
}
res.push(cur);
return res;
};
lines.forEach((line, i)=>{
const c = split(line);
if(i === 0 && (c[0]||'').indexOf('日期') >= 0) return;
if(c.length < 5) return;
out.push({
id:uid(),
date:(c[0]||'').trim() || todayStr(),
reason:RMAP[(c[1]||'').trim()] ? (c[1]||'').trim() : '其他',
from:(c[2]||'').trim(), to:(c[3]||'').trim(),
amount:Number(c[4]) || 0, note:(c[5]||'').trim()
});
});
return out.filter(r=>r.from && r.to);
}

/* ================= 事件 ================= */
$('btnSave').onclick = submit;
$('btnCancel').onclick = ()=>{ clearForm(); toast('已取消修改'); };
$('fAmount').addEventListener('keydown', e=>{ if(e.key === 'Enter') submit(); });
$('fNote').addEventListener('keydown', e=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); submit(); } });
$('quickAmt').onclick = e=>{
const b = e.target.closest('button'); if(!b) return;
$('fAmount').value = b.dataset.amt;
};

$('list').addEventListener('click', e=>{
const b = e.target.closest('button'); if(!b) return;
const id = e.target.closest('.item').dataset.id;
if(b.dataset.act === 'edit') startEdit(id);
else if(b.dataset.act === 'del') delRecord(id);
});

$('qFrom').onchange = e=>{ filter.from = e.target.value; renderList(); };
$('qTo').onchange = e=>{ filter.to = e.target.value; renderList(); };
$('qReason').onchange = e=>{ filter.reason = e.target.value; renderList(); };
$('qKey').oninput = e=>{ filter.key = e.target.value; renderList(); };
$('rangeChips').onclick = e=>{
const c = e.target.closest('.chip'); if(!c) return;
Array.from($('rangeChips').children).forEach(x=>x.classList.remove('on'));
c.classList.add('on');
filter.range = c.dataset.range;
renderList();
};
$('btnReset').onclick = ()=>{
filter = {from:'', to:'', reason:'', key:'', range:'all'};
$('qFrom').value = ''; $('qTo').value = ''; $('qReason').value = ''; $('qKey').value = '';
Array.from($('rangeChips').children).forEach((x,i)=>x.classList.toggle('on', i === 0));
renderList();
toast('已重置筛选');
};

$('btnCsv').onclick = ()=>{
if(!records.length){ toast('还没有记录可导出'); return; }
download('随礼记录_' + todayStr() + '.csv', toCsv(), 'text/csv;charset=utf-8');
toast('已导出 CSV(' + records.length + ' 笔)');
};
$('btnBak').onclick = ()=>{
if(!records.length){ toast('还没有记录可备份'); return; }
download('随礼备份_' + todayStr() + '.json', JSON.stringify(records, null, 2), 'application/json');
toast('已生成备份文件');
};
$('btnImp').onclick = ()=> $('fileIn').click();
$('fileIn').onchange = e=>{
const f = e.target.files[0]; if(!f) return;
const fr = new FileReader();
fr.onload = ()=>{
try{
let incoming = null;
if(/\\.json$/i.test(f.name)){
incoming = JSON.parse(fr.result);
if(!Array.isArray(incoming)) throw new Error('格式不符');
incoming = incoming.map(r=>({
id:uid(), date:r.date || todayStr(),
reason:RMAP[r.reason] ? r.reason : '其他',
from:String(r.from || '').trim(), to:String(r.to || '').trim(),
amount:Number(r.amount) || 0, note:String(r.note || '').trim()
})).filter(r=>r.from && r.to);
}else{
incoming = parseCsv(String(fr.result));
}
if(!incoming.length){ toast('没有识别到有效记录'); return; }
confirmDlg('导入记录', '将 ' + incoming.length + ' 笔记录追加到现有 ' + records.length + ' 笔之后,是否继续?', ()=>{
records = records.concat(incoming);
sortRecords(); save(); renderSelects(); renderList();
toast('已导入 ' + incoming.length + ' 笔');
});
}catch(err){ toast('导入失败:文件格式不正确'); }
};
fr.readAsText(f, 'utf-8');
e.target.value = '';
};
$('btnClr').onclick = ()=>{
if(!records.length){ toast('当前没有记录'); return; }
confirmDlg('清空全部', '将删除全部 ' + records.length + ' 笔记录,删除后无法恢复。建议先备份。', ()=>{
records = []; pendingDel = null;
save(); renderSelects(); renderList();
toast('已清空全部记录');
});
};

/* ================= 启动 ================= */
load();
sortRecords();
initForm();
renderSelects();
renderList();

// 调试/测试入口
window.__app = {
get records(){ return records; },
get filter(){ return filter; },
filtered, sum, fmt, inRange,
add:r=>{ records.push(Object.assign({id:uid()}, r)); sortRecords(); save(); renderSelects(); renderList(); }
};
})();
</script>
</body>
</html>

赞(0)
未经允许不得转载:171主机测评 » 随礼记事本html
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址