prts-moonlit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/assets/boy.jpg +0 -0
  2. package/assets/card.jpg +0 -0
  3. package/assets/crew.jpg +0 -0
  4. package/assets/face.jpg +0 -0
  5. package/assets/fig.jpg +0 -0
  6. package/assets/grn.jpg +0 -0
  7. package/assets/hero.jpg +0 -0
  8. package/assets/hero.png +0 -0
  9. package/assets/scene.jpg +0 -0
  10. package/assets/taro.jpg +0 -0
  11. package/assets/taro78/k00.jpg +0 -0
  12. package/assets/taro78/k01.jpg +0 -0
  13. package/assets/taro78/k02.jpg +0 -0
  14. package/assets/taro78/k03.jpg +0 -0
  15. package/assets/taro78/k04.jpg +0 -0
  16. package/assets/taro78/k05.jpg +0 -0
  17. package/assets/taro78/k06.jpg +0 -0
  18. package/assets/taro78/k07.jpg +0 -0
  19. package/assets/taro78/k08.jpg +0 -0
  20. package/assets/taro78/k09.jpg +0 -0
  21. package/assets/taro78/k10.jpg +0 -0
  22. package/assets/taro78/k11.jpg +0 -0
  23. package/assets/taro78/k12.jpg +0 -0
  24. package/assets/taro78/k13.jpg +0 -0
  25. package/assets/taro78/k14.jpg +0 -0
  26. package/assets/taro78/k15.jpg +0 -0
  27. package/assets/taro78/k16.jpg +0 -0
  28. package/assets/taro78/k17.jpg +0 -0
  29. package/assets/taro78/k18.jpg +0 -0
  30. package/assets/taro78/k19.jpg +0 -0
  31. package/assets/taro78/k20.jpg +0 -0
  32. package/assets/taro78/k21.jpg +0 -0
  33. package/assets/taro78/k22.jpg +0 -0
  34. package/assets/taro78/k23.jpg +0 -0
  35. package/assets/taro78/k24.jpg +0 -0
  36. package/assets/taro78/k25.jpg +0 -0
  37. package/assets/taro78/k26.jpg +0 -0
  38. package/assets/taro78/k27.jpg +0 -0
  39. package/assets/taro78/k28.jpg +0 -0
  40. package/assets/taro78/k29.jpg +0 -0
  41. package/assets/taro78/k30.jpg +0 -0
  42. package/assets/taro78/k31.jpg +0 -0
  43. package/assets/taro78/k32.jpg +0 -0
  44. package/assets/taro78/k33.jpg +0 -0
  45. package/assets/taro78/k34.jpg +0 -0
  46. package/assets/taro78/k35.jpg +0 -0
  47. package/assets/taro78/k36.jpg +0 -0
  48. package/assets/taro78/k37.jpg +0 -0
  49. package/assets/taro78/k38.jpg +0 -0
  50. package/assets/taro78/k39.jpg +0 -0
  51. package/assets/taro78/k40.jpg +0 -0
  52. package/assets/taro78/k41.jpg +0 -0
  53. package/assets/taro78/k42.jpg +0 -0
  54. package/assets/taro78/k43.jpg +0 -0
  55. package/assets/taro78/k44.jpg +0 -0
  56. package/assets/taro78/k45.jpg +0 -0
  57. package/assets/taro78/k46.jpg +0 -0
  58. package/assets/taro78/k47.jpg +0 -0
  59. package/assets/taro78/k48.jpg +0 -0
  60. package/assets/taro78/k49.jpg +0 -0
  61. package/assets/taro78/k50.jpg +0 -0
  62. package/assets/taro78/k51.jpg +0 -0
  63. package/assets/taro78/k52.jpg +0 -0
  64. package/assets/taro78/k53.jpg +0 -0
  65. package/assets/taro78/k54.jpg +0 -0
  66. package/assets/taro78/k55.jpg +0 -0
  67. package/assets/taro78/k56.jpg +0 -0
  68. package/assets/taro78/k57.jpg +0 -0
  69. package/assets/taro78/k58.jpg +0 -0
  70. package/assets/taro78/k59.jpg +0 -0
  71. package/assets/taro78/k60.jpg +0 -0
  72. package/assets/taro78/k61.jpg +0 -0
  73. package/assets/taro78/k62.jpg +0 -0
  74. package/assets/taro78/k63.jpg +0 -0
  75. package/assets/taro78/k64.jpg +0 -0
  76. package/assets/taro78/k65.jpg +0 -0
  77. package/assets/taro78/k66.jpg +0 -0
  78. package/assets/taro78/k67.jpg +0 -0
  79. package/assets/taro78/k68.jpg +0 -0
  80. package/assets/taro78/k69.jpg +0 -0
  81. package/assets/taro78/k70.jpg +0 -0
  82. package/assets/taro78/k71.jpg +0 -0
  83. package/assets/taro78/k72.jpg +0 -0
  84. package/assets/taro78/k73.jpg +0 -0
  85. package/assets/taro78/k74.jpg +0 -0
  86. package/assets/taro78/k75.jpg +0 -0
  87. package/assets/taro78/k76.jpg +0 -0
  88. package/assets/taro78/k77.jpg +0 -0
  89. package/bgm.mp3 +0 -0
  90. package/gacha.html +1043 -0
  91. package/index.html +2757 -0
  92. package/ledger.html +1003 -0
  93. package/package.json +6 -0
package/ledger.html ADDED
@@ -0,0 +1,1003 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1, user-scalable=no">
6
+ <meta name="theme-color" content="#04060f">
7
+ <meta name="color-scheme" content="dark">
8
+ <title>小账本</title>
9
+ <!--BUNDLED: data.js + app.js 已内联,改数据请编辑 data.js 后重跑 sync_from_photos.py-->
10
+ <style>
11
+ :root{
12
+ --blue:#1b5cff; --blue-m:#0b2a8f; --blue-d:#061748; --ink:#04060f;
13
+ --paper:#f2f7ff; --lime:#d9f838; --out:#ff5c5c; --in:#5fe08a;
14
+ --dim:rgba(242,247,255,.62); --line:rgba(242,247,255,.12);
15
+ color-scheme:dark;
16
+ }
17
+ *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
18
+ html,body{margin:0;padding:0}
19
+ body{
20
+ font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",Arial,sans-serif;
21
+ background:var(--ink);color:var(--paper);font-size:15px;line-height:1.4;
22
+ padding-bottom:112px;overflow-x:hidden;-webkit-font-smoothing:antialiased;
23
+ }
24
+ /* 背景层:斜向蓝渐变 + 放射线 + 网点 */
25
+ .bg{position:fixed;inset:0;z-index:0;pointer-events:none;
26
+ background:linear-gradient(160deg,var(--blue-m) 0%,var(--blue) 34%,var(--blue-d) 62%,var(--ink) 100%)}
27
+ .bg:after{content:'';position:absolute;inset:-20%;
28
+ background:repeating-conic-gradient(from 0deg at 82% 4%, rgba(0,0,0,.42) 0 .55deg, transparent .55deg 6.2deg)}
29
+ .dots{position:fixed;inset:0;z-index:1;pointer-events:none;
30
+ background-image:radial-gradient(circle,#04123c 1.3px,transparent 1.4px);background-size:7px 7px;opacity:.45}
31
+ .wrap{position:relative;z-index:2;max-width:560px;margin:0 auto;padding:0 16px}
32
+
33
+ /* ===== 标题 ===== */
34
+ .hd{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;padding:calc(16px + env(safe-area-inset-top)) 2px 10px}
35
+ .hd h1{margin:0;font-size:32px;font-weight:900;font-style:italic;letter-spacing:-.04em;line-height:1.15}
36
+ .hd h1 span{display:inline-block;background:var(--paper);color:var(--ink);padding:2px 12px 5px;
37
+ transform:skewX(-10deg);box-shadow:5px 5px 0 var(--ink), 7px 7px 0 var(--lime)}
38
+ .hd h1 span i{font-style:normal;display:inline-block;transform:skewX(10deg)}
39
+ .sound{width:42px;height:42px;border:2px solid var(--ink);background:var(--paper);color:var(--ink);
40
+ transform:skewX(-10deg);font-size:16px;display:grid;place-items:center;cursor:pointer;
41
+ box-shadow:4px 4px 0 var(--ink);flex:none}
42
+ .sound i{font-style:normal;transform:skewX(10deg);display:inline-block}
43
+ .hd-r{display:flex;align-items:center;gap:10px}
44
+ .homeb{height:42px;padding:0 13px;border:2px solid var(--ink);background:var(--paper);color:var(--ink);
45
+ transform:skewX(-10deg);font:inherit;font-weight:900;font-size:13px;letter-spacing:.05em;
46
+ display:grid;place-items:center;cursor:pointer;box-shadow:4px 4px 0 var(--ink);flex:none}
47
+ .homeb i{font-style:normal;transform:skewX(10deg);display:inline-block}
48
+ .homeb:active{transform:skewX(-10deg) translate(2px,2px);box-shadow:2px 2px 0 var(--ink)}
49
+
50
+ /* ===== 汇总卡 ===== */
51
+ .summary{
52
+ background:linear-gradient(150deg,rgba(4,6,15,.94),rgba(6,23,72,.94));
53
+ border:2px solid var(--ink);box-shadow:6px 6px 0 var(--ink);
54
+ clip-path:polygon(0 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%);
55
+ padding:14px 16px 0;margin:6px 0 14px;
56
+ }
57
+ .sum-top{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
58
+ .sum-top .k{font-size:11px;letter-spacing:.24em;color:var(--lime);font-weight:800;font-style:italic;flex:none}
59
+ .bal{margin:0;font-size:42px;font-weight:900;font-style:italic;letter-spacing:-.045em;
60
+ font-variant-numeric:tabular-nums;text-align:right;word-break:break-all;
61
+ text-shadow:3px 3px 0 rgba(4,6,15,.9)}
62
+ .bal small{font-size:22px;font-weight:800;color:var(--dim);margin-right:2px;letter-spacing:0;font-style:normal}
63
+ .sum-row{display:flex;margin-top:12px;border-top:1px solid var(--line)}
64
+ .sum-row>div{flex:1;min-width:0;padding:11px 0 14px}
65
+ .sum-row>div+div{border-left:1px solid var(--line);padding-left:12px}
66
+ .sum-row>div:not(:last-child){padding-right:10px}
67
+ .sum-row .k{display:block;font-size:10px;color:var(--dim);font-weight:700;letter-spacing:.1em;margin-bottom:2px}
68
+ .sum-row b{font-size:16px;font-weight:800;font-style:italic;font-variant-numeric:tabular-nums;letter-spacing:-.02em}
69
+
70
+ /* ===== 主卡片(tabs + 内容) ===== */
71
+ .card{
72
+ background:rgba(4,6,15,.86);border:2px solid var(--ink);box-shadow:6px 6px 0 var(--ink);
73
+ clip-path:polygon(0 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%);
74
+ padding:14px 14px 16px;
75
+ }
76
+ .card section{display:none}
77
+ .card section.on{display:block}
78
+
79
+ /* ===== 顶部 Tab ===== */
80
+ .tabs{display:flex;gap:6px;margin-bottom:14px}
81
+ .tabs button{
82
+ flex:1;border:2px solid var(--ink);background:rgba(242,247,255,.08);color:var(--dim);
83
+ padding:9px 0;font-size:13px;font-weight:800;font-style:italic;font-family:inherit;
84
+ cursor:pointer;transform:skewX(-8deg);letter-spacing:.04em;
85
+ }
86
+ .tabs button span{display:inline-block;transform:skewX(8deg)}
87
+ .tabs button.on{background:var(--paper);color:var(--ink);box-shadow:3px 3px 0 var(--lime)}
88
+
89
+ /* ===== 支出/收入 分段 ===== */
90
+ .seg{display:flex;gap:6px;margin:0 0 14px}
91
+ .seg button{
92
+ flex:1;border:2px solid var(--ink);background:rgba(242,247,255,.08);color:var(--dim);
93
+ padding:10px 0;font-size:15px;font-weight:900;font-style:italic;font-family:inherit;
94
+ cursor:pointer;transform:skewX(-8deg);
95
+ }
96
+ .seg button span{display:inline-block;transform:skewX(8deg)}
97
+ .seg button.on-out{background:var(--out);color:#fff;box-shadow:3px 3px 0 var(--ink)}
98
+ .seg button.on-in{background:var(--in);color:#04130a;box-shadow:3px 3px 0 var(--ink)}
99
+
100
+ /* ===== 金额输入 ===== */
101
+ .amt-card{display:flex;align-items:baseline;gap:6px;border-bottom:3px solid var(--lime);
102
+ padding:2px 2px 8px;margin:0 0 2px}
103
+ .amt-card>span{font-size:26px;font-weight:900;font-style:italic;color:var(--lime);flex:none}
104
+ .amt-card input{
105
+ flex:1;min-width:0;border:0;outline:0;background:transparent;font-size:38px;font-weight:900;
106
+ font-style:italic;letter-spacing:-.03em;font-family:inherit;color:var(--paper);
107
+ font-variant-numeric:tabular-nums;padding:2px 0;
108
+ }
109
+ .amt-card input::placeholder{color:rgba(242,247,255,.25)}
110
+
111
+ .lbl{font-size:10.5px;letter-spacing:.18em;font-weight:800;font-style:italic;color:var(--lime);
112
+ margin:16px 2px 9px;text-transform:uppercase}
113
+
114
+ /* ===== 分类 chips ===== */
115
+ .chips{display:flex;flex-wrap:wrap;gap:7px}
116
+ .chips button{
117
+ border:2px solid var(--ink);background:rgba(242,247,255,.1);color:var(--paper);
118
+ padding:9px 12px;font-size:13px;font-weight:800;font-style:italic;font-family:inherit;
119
+ cursor:pointer;box-shadow:3px 3px 0 rgba(4,6,15,.9);
120
+ }
121
+ .chips button.on{background:var(--lime);color:var(--ink);box-shadow:3px 3px 0 var(--ink)}
122
+
123
+ /* ===== 备注/日期 分组 ===== */
124
+ .group{background:rgba(242,247,255,.06);border:2px solid var(--ink);margin-top:14px;
125
+ box-shadow:3px 3px 0 rgba(4,6,15,.9)}
126
+ .row{display:flex;align-items:center;gap:10px;padding:0 12px;min-height:50px}
127
+ .row+.row{border-top:1px solid var(--line)}
128
+ .row .rk{font-size:13px;color:var(--dim);font-weight:700;flex:none;letter-spacing:.05em}
129
+ input.txt{flex:1;min-width:0;border:0;outline:0;background:transparent;font-size:15px;
130
+ font-family:inherit;color:var(--paper);padding:13px 0}
131
+ input.txt::placeholder{color:rgba(242,247,255,.4)}
132
+ input.rdate{margin-left:auto;width:auto;max-width:60%;flex:0 0 auto;border:0;outline:0;
133
+ background:transparent;font-size:15px;font-family:inherit;color:var(--lime);
134
+ text-align:right;padding:13px 0;-webkit-appearance:none;appearance:none;font-weight:700}
135
+ input.rdate::-webkit-date-and-time-value{text-align:right}
136
+
137
+ /* ===== 保存 ===== */
138
+ .save{width:100%;margin:18px 0 2px;border:3px solid var(--ink);padding:15px;
139
+ background:var(--lime);color:var(--ink);font-size:17px;font-weight:900;font-style:italic;
140
+ font-family:inherit;cursor:pointer;letter-spacing:.08em;transform:skewX(-6deg);
141
+ box-shadow:5px 5px 0 var(--ink)}
142
+ .save span{display:inline-block;transform:skewX(6deg)}
143
+
144
+ /* ===== 月份栏 ===== */
145
+ .month-bar{display:flex;align-items:center;justify-content:space-between;margin:2px 0 10px}
146
+ .month-bar b{font-size:15px;font-weight:900;font-style:italic;letter-spacing:.02em}
147
+ .month-bar button{
148
+ border:2px solid var(--ink);background:var(--lime);color:var(--ink);width:34px;height:32px;
149
+ font-size:16px;font-weight:900;font-family:inherit;cursor:pointer;transform:skewX(-10deg);
150
+ box-shadow:3px 3px 0 var(--ink);line-height:1;
151
+ }
152
+
153
+ /* ===== 明细 ===== */
154
+ .day-h{display:flex;justify-content:space-between;gap:10px;font-size:11.5px;color:var(--dim);
155
+ font-weight:800;font-style:italic;letter-spacing:.03em;margin:16px 2px 6px;
156
+ border-left:4px solid var(--lime);padding-left:8px}
157
+ .item{display:flex;align-items:center;gap:11px;padding:10px 2px;position:relative;
158
+ border-bottom:1px solid var(--line)}
159
+ .item:last-child{border-bottom:none}
160
+ .ico{width:36px;height:36px;flex:none;display:flex;align-items:center;justify-content:center;
161
+ font-size:17px;background:var(--paper);box-shadow:3px 3px 0 var(--ink)}
162
+ .item .mid{flex:1;min-width:0}
163
+ .item .t{font-size:15px;font-weight:800;font-style:italic;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
164
+ .item .n{font-size:11px;color:var(--dim);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.03em}
165
+ .item .v{font-size:16px;font-weight:900;font-style:italic;font-variant-numeric:tabular-nums;flex:none;letter-spacing:-.02em}
166
+ .tag{display:inline-block;background:rgba(217,248,56,.16);color:var(--lime);padding:1px 6px;
167
+ font-size:10px;font-weight:800;font-style:italic;margin-right:2px}
168
+ .del{width:28px;height:28px;flex:none;border:2px solid var(--ink);cursor:pointer;
169
+ background:rgba(242,247,255,.08);color:var(--dim);font-size:13px;line-height:1;
170
+ font-family:inherit;padding:0}
171
+ .del:active{background:rgba(255,92,92,.2);color:var(--out)}
172
+
173
+ .empty{text-align:center;color:var(--dim);font-size:14px;padding:40px 10px;font-style:italic}
174
+ .empty div{font-size:36px;margin-bottom:8px;opacity:.7}
175
+
176
+ /* ===== 统计 ===== */
177
+ .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:4px 0 2px}
178
+ .stat{background:rgba(242,247,255,.06);border:2px solid var(--ink);padding:12px 13px;
179
+ box-shadow:3px 3px 0 rgba(4,6,15,.9)}
180
+ .stat .k{font-size:10px;color:var(--dim);font-weight:700;letter-spacing:.08em}
181
+ .stat .v{font-size:20px;font-weight:900;font-style:italic;margin-top:5px;
182
+ font-variant-numeric:tabular-nums;letter-spacing:-.03em}
183
+ .catrow{margin:14px 2px}
184
+ .catrow .top{display:flex;justify-content:space-between;gap:10px;font-size:13.5px;
185
+ font-weight:700;margin-bottom:7px}
186
+ .catrow .top i{font-style:italic;color:var(--dim);font-weight:600}
187
+ .bar{height:9px;background:rgba(242,247,255,.12);overflow:hidden;transform:skewX(-14deg)}
188
+ .bar i{display:block;height:100%}
189
+
190
+ /* ===== 回收站 ===== */
191
+ .trashbar{margin-top:16px;border-top:1px solid var(--line);padding-top:12px}
192
+ .trashbar .lbl{margin:0 0 9px}
193
+ .tb-row{display:flex;flex-wrap:wrap;gap:8px}
194
+ .ghost{border:2px solid var(--ink);background:rgba(242,247,255,.08);color:var(--lime);
195
+ padding:9px 13px;font-size:12.5px;font-weight:800;font-style:italic;font-family:inherit;
196
+ cursor:pointer;box-shadow:3px 3px 0 rgba(4,6,15,.9)}
197
+
198
+ /* ===== 底部工具栏 ===== */
199
+ .dock{position:fixed;left:0;right:0;bottom:0;display:flex;gap:8px;
200
+ padding:8px 12px calc(8px + env(safe-area-inset-bottom));background:rgba(4,6,15,.95);
201
+ border-top:3px solid var(--ink);z-index:9;max-width:560px;margin:0 auto}
202
+ .dock button{flex:1;border:2px solid var(--ink);padding:12px 6px;font-size:14px;font-weight:900;
203
+ font-style:italic;color:var(--ink);background:var(--lime);font-family:inherit;cursor:pointer;
204
+ transform:skewX(-8deg);letter-spacing:.04em}
205
+ .dock button span{display:inline-block;transform:skewX(8deg)}
206
+ .dock button.dngr{background:var(--out);color:#fff;box-shadow:3px 3px 0 var(--ink)}
207
+
208
+ /* ===== Toast ===== */
209
+ .toast{position:fixed;left:50%;bottom:96px;transform:translateX(-50%) skewX(-10deg);
210
+ background:var(--lime);color:var(--ink);padding:10px 20px;font-size:13.5px;font-weight:900;
211
+ font-style:italic;opacity:0;pointer-events:none;z-index:99;max-width:80%;text-align:center;
212
+ white-space:nowrap;box-shadow:4px 4px 0 var(--ink)}
213
+ .toast.on{opacity:1}
214
+
215
+ /* ===== 确认弹窗 ===== */
216
+ .modal{position:fixed;inset:0;background:rgba(4,6,15,.6);display:none;align-items:center;
217
+ justify-content:center;padding:24px;z-index:200;backdrop-filter:blur(3px)}
218
+ .modal.on{display:flex}
219
+ .mbox{width:280px;max-width:100%;background:var(--blue-d);border:3px solid var(--ink);
220
+ box-shadow:7px 7px 0 var(--ink);text-align:center}
221
+ .mtitle{font-size:16px;font-weight:900;font-style:italic;padding:18px 16px 0;color:var(--lime)}
222
+ .mtext{font-size:13px;color:var(--dim);line-height:1.5;padding:8px 16px 16px;white-space:pre-line}
223
+ .mrow{display:flex;border-top:3px solid var(--ink)}
224
+ .mbtn{flex:1;border:0;background:transparent;padding:13px 6px;font-size:16px;font-weight:900;
225
+ font-style:italic;font-family:inherit;color:var(--paper);cursor:pointer}
226
+ .mbtn+.mbtn{border-left:3px solid var(--ink)}
227
+ .mbtn.cancel{color:var(--dim)}
228
+ .mbtn.danger{color:var(--out)}
229
+
230
+ /* ===== 过场 ===== */
231
+ .wipe{position:fixed;top:0;bottom:0;left:0;width:62%;z-index:80;display:none;pointer-events:none;
232
+ background:linear-gradient(90deg,rgba(217,248,56,0),var(--lime) 45%,var(--paper) 55%,rgba(27,92,255,0));
233
+ opacity:0}
234
+ .burst{position:fixed;inset:0;z-index:90;display:none;align-items:center;justify-content:center;pointer-events:none}
235
+ .burst .flash{position:absolute;inset:0;background:var(--paper);opacity:0}
236
+ .burst .word{position:relative;font-size:52px;font-weight:900;font-style:italic;letter-spacing:-.05em;
237
+ color:var(--lime);text-shadow:5px 5px 0 var(--ink);opacity:0;
238
+ transform:skewX(-9deg) rotate(-8deg);transform-origin:center}
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div class="bg"></div>
243
+ <div class="dots"></div>
244
+
245
+ <div class="wrap">
246
+ <header class="hd">
247
+ <h1><span><i>小账本</i></span></h1>
248
+ <div class="hd-r">
249
+ <button class="homeb" id="backBtn" title="回终端主页(PRTS)"><i>← 终端主页</i></button>
250
+ <button class="sound" id="soundBtn" title="音效开关"><i>♪</i></button>
251
+ </div>
252
+ </header>
253
+
254
+ <section class="summary">
255
+ <div class="sum-top">
256
+ <span class="k">本月结余</span>
257
+ <p class="bal" id="balTxt"><small>¥</small>0.00</p>
258
+ </div>
259
+ <div class="sum-row">
260
+ <div><span class="k">支出</span><b id="outTxt">¥0.00</b></div>
261
+ <div><span class="k">收入</span><b id="inTxt">¥0.00</b></div>
262
+ <div><span class="k">笔数</span><b id="cntTxt">0</b></div>
263
+ </div>
264
+ </section>
265
+
266
+ <div class="card">
267
+ <div class="tabs">
268
+ <button class="on" data-tab="add"><span>记账</span></button>
269
+ <button data-tab="list"><span>明细</span></button>
270
+ <button data-tab="stat"><span>统计</span></button>
271
+ </div>
272
+
273
+ <section id="tab-add" class="on">
274
+ <div class="seg">
275
+ <button id="btnOut" class="on-out"><span>支出</span></button>
276
+ <button id="btnIn"><span>收入</span></button>
277
+ </div>
278
+ <div class="amt-card">
279
+ <span>¥</span>
280
+ <input id="amt" type="number" inputmode="decimal" placeholder="0.00" step="0.01">
281
+ </div>
282
+ <div class="lbl">分类</div>
283
+ <div class="chips" id="chips"></div>
284
+ <div class="group">
285
+ <div class="row"><input class="txt" id="note" placeholder="备注(可选),如:食堂午饭" maxlength="30"></div>
286
+ <div class="row"><span class="rk">日期</span><input id="date" type="date" class="rdate"></div>
287
+ </div>
288
+ <button class="save" id="saveBtn"><span>保存这笔</span></button>
289
+ </section>
290
+
291
+ <section id="tab-list">
292
+ <div class="month-bar">
293
+ <button id="prevM">‹</button>
294
+ <b id="monthLbl"></b>
295
+ <button id="nextM">›</button>
296
+ </div>
297
+ <div id="listBox"></div>
298
+ </section>
299
+
300
+ <section id="tab-stat">
301
+ <div class="month-bar">
302
+ <button id="prevS">‹</button>
303
+ <b id="statLbl"></b>
304
+ <button id="nextS">›</button>
305
+ </div>
306
+ <div class="stat-grid">
307
+ <div class="stat"><div class="k">本月支出</div><div class="v" id="sOut" style="color:var(--out)">¥0</div></div>
308
+ <div class="stat"><div class="k">本月收入</div><div class="v" id="sIn" style="color:var(--in)">¥0</div></div>
309
+ <div class="stat"><div class="k">日均支出</div><div class="v" id="sAvg">¥0</div></div>
310
+ <div class="stat"><div class="k">最大单笔</div><div class="v" id="sMax">¥0</div></div>
311
+ </div>
312
+ <div class="lbl">支出构成</div>
313
+ <div id="catBox"></div>
314
+ </section>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="dock">
319
+ <button id="expBtn"><span>导出 CSV</span></button>
320
+ <button id="clrBtn" class="dngr"><span>清空数据</span></button>
321
+ </div>
322
+
323
+ <div class="toast" id="toast"></div>
324
+
325
+ <div class="modal" id="modal">
326
+ <div class="mbox">
327
+ <div class="mtitle"></div>
328
+ <div class="mtext"></div>
329
+ <div class="mrow">
330
+ <button class="mbtn cancel">取消</button>
331
+ <button class="mbtn ok">确定</button>
332
+ </div>
333
+ </div>
334
+ </div>
335
+
336
+ <div class="wipe" id="wipe"></div>
337
+ <div class="burst" id="burst"><div class="flash"></div><div class="word">記録完了!</div></div>
338
+
339
+ <script>
340
+ /* ===== 内联数据(原 data.js)===== */
341
+ /* 小账本 · 数据文件 */
342
+
343
+ /* 自动导入源:已启用(2026-09-25 主人要求导入这些账单)。
344
+ 每次打开账本时,这里的记录会自动并入;id 相同的不会重复;
345
+ 已被删进回收站的 id 不会被回灌。 */
346
+ window.PENDING_IMPORT = [{"id": "auto_101c82976e0b", "type": "out", "amount": 39.47, "cat": "其他", "note": "京东自营旗舰店(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.8, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_e2b66a222efd", "type": "out", "amount": 506.8, "cat": "其他", "note": "支付宝-*信…息技术有限公司(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_5097e75d131e", "type": "out", "amount": 28.9, "cat": "购物", "note": "支付宝-深圳市闪魔数码科技集团有限公(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_0481a45c4e6d", "type": "out", "amount": 44.0, "cat": "娱乐", "note": "财付通-Apple(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_9f9f6d94d23c", "type": "out", "amount": 50.0, "cat": "学习", "note": "支付宝-杭州深度求索人工智能基础技术(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_277eea1c05a9", "type": "out", "amount": 39.4, "cat": "餐饮", "note": "支付宝-上海拉扎斯信息科技有限公司(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_78ca2d1e437a", "type": "in", "amount": 4.49, "cat": "其他", "note": "银联入账(7707)", "date": "2026-09-24", "ts": 1790254397000, "src": "银行短信", "conf": 0.95, "asset": "9D981081-C14E-4CE2-8492-6E92B4152E55/L0/001"}, {"id": "auto_6f35ce49913c", "type": "out", "amount": 24.0, "cat": "餐饮", "note": "俺来也点餐", "date": "2026-09-24", "ts": 1790254390000, "src": "校园卡", "conf": 0.9, "asset": "BFDD0872-9E40-4813-80F8-884223A69520/L0/001"}, {"id": "auto_da55595cdd44", "type": "out", "amount": 9.2, "cat": "餐饮", "note": "三食堂一楼", "date": "2026-09-24", "ts": 1790254390000, "src": "校园卡", "conf": 0.9, "asset": "BFDD0872-9E40-4813-80F8-884223A69520/L0/001"}, {"id": "auto_3a8c170f5877", "type": "out", "amount": 22.9, "cat": "餐饮", "note": "支付宝-北京三快在线科技有限公司(7707)", "date": "2026-09-25", "ts": 1790307300000, "src": "银行短信", "conf": 0.95, "asset": "DDB8D256-EE87-4997-AF99-29828D339255/L0/001"}, {"id": "auto_d675016b7b70", "type": "out", "amount": 30.0, "cat": "交通", "note": "支付宝-上海公共交通卡股份有限公司(7707)", "date": "2026-09-25", "ts": 1790319720000, "src": "银行短信", "conf": 0.95, "asset": "DDB8D256-EE87-4997-AF99-29828D339255/L0/001"}, {"id": "auto_d8a3be74f08e", "type": "out", "amount": 50.0, "cat": "鲸鲸", "note": "主人给鲸鲸续饭 · 支付宝-杭州深度求索(7707)", "date": "2026-09-26", "ts": 1790399100000, "src": "银行短信", "conf": 0.9, "asset": "5DDDECCA-647A-47A1-BD42-369FD42F5FF5/L0/001/L0/001"}];
347
+
348
+ /* 文件级备份:上面的记录已并入自动导入源,这里留空(原件备份见 shared/ledger/data.js.backup) */
349
+ window.LEDGER_BACKUP = [];
350
+
351
+ </script>
352
+ <script>
353
+ /* ===== 内联逻辑(原 app.js)===== */
354
+ /* 小账本 · app.js */
355
+ (function () {
356
+ 'use strict';
357
+
358
+ var KEY = 'minis_ledger_v1';
359
+ var TRASH_KEY = 'minis_ledger_trash_v1';
360
+
361
+ var CATS = {
362
+ out: [
363
+ { n: '餐饮', e: '🍚', c: '#f4566b' },
364
+ { n: '交通', e: '🚌', c: '#f79b3e' },
365
+ { n: '购物', e: '🛍️', c: '#a855f7' },
366
+ { n: '学习', e: '📚', c: '#3b6cf6' },
367
+ { n: '娱乐', e: '🎮', c: '#ec4899' },
368
+ { n: '日用', e: '🧴', c: '#14b8a6' },
369
+ { n: '医疗', e: '💊', c: '#ef4444' },
370
+ { n: '鲸鲸', e: '🐋', c: '#1f9dbe' },
371
+ { n: '其他', e: '📦', c: '#94a3b8' }
372
+ ],
373
+ in: [
374
+ { n: '生活费', e: '💰', c: '#12b886' },
375
+ { n: '兼职', e: '💼', c: '#0ea5e9' },
376
+ { n: '奖学金', e: '🏆', c: '#f59e0b' },
377
+ { n: '红包', e: '🧧', c: '#ef4444' },
378
+ { n: '其他', e: '📦', c: '#94a3b8' }
379
+ ]
380
+ };
381
+
382
+ var S = {
383
+ records: [],
384
+ trash: [],
385
+ type: 'out',
386
+ cat: null,
387
+ tab: 'add',
388
+ // 明细/统计的当前月份(从今天起)
389
+ listYear: null,
390
+ listMonth: null,
391
+ statYear: null,
392
+ statMonth: null
393
+ };
394
+
395
+ /* ---------- 存储 ---------- */
396
+ function loadTrash() {
397
+ try {
398
+ var t = JSON.parse(localStorage.getItem(TRASH_KEY) || '[]');
399
+ S.trash = Array.isArray(t) ? t : [];
400
+ } catch (e) { S.trash = []; }
401
+ }
402
+ function saveTrash() {
403
+ try { localStorage.setItem(TRASH_KEY, JSON.stringify(S.trash)); } catch (e) {}
404
+ }
405
+
406
+ function load() {
407
+ loadTrash();
408
+ try {
409
+ var raw = localStorage.getItem(KEY);
410
+ S.records = raw ? JSON.parse(raw) : [];
411
+ if (!Array.isArray(S.records)) S.records = [];
412
+ } catch (e) { S.records = []; }
413
+
414
+ // 被删过的 id(回收站里的)永不回灌
415
+ var dead = {};
416
+ S.trash.forEach(function (r) { if (r && r.id) dead[r.id] = 1; });
417
+ if (Object.keys(dead).length) {
418
+ S.records = S.records.filter(function (r) { return !dead[r.id]; });
419
+ }
420
+
421
+ // 合并由脚本自动导入的账单(data.js 的 window.PENDING_IMPORT,默认为空)
422
+ var inc = window.PENDING_IMPORT;
423
+ if (Array.isArray(inc) && inc.length) {
424
+ var seen = {};
425
+ S.records.forEach(function (r) { seen[r.id] = 1; });
426
+ var added = 0;
427
+ inc.forEach(function (r) {
428
+ if (!r || !r.id || seen[r.id] || dead[r.id]) return;
429
+ if (!r.type || !r.amount || !r.date) return;
430
+ seen[r.id] = 1;
431
+ S.records.push(r);
432
+ added++;
433
+ });
434
+ if (added) {
435
+ save();
436
+ setTimeout(function () { toast('自动导入 ' + added + ' 笔账单'); }, 400);
437
+ }
438
+ }
439
+ }
440
+ function save() {
441
+ try { localStorage.setItem(KEY, JSON.stringify(S.records)); } catch (e) {}
442
+ }
443
+
444
+ /* ---------- 工具 ---------- */
445
+ function money(n) {
446
+ return (Math.round(n * 100) / 100).toFixed(2);
447
+ }
448
+ function todayStr() {
449
+ var d = new Date();
450
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
451
+ }
452
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
453
+ function catOf(type, name) {
454
+ var arr = CATS[type];
455
+ for (var i = 0; i < arr.length; i++) if (arr[i].n === name) return arr[i];
456
+ return { n: name, e: '📦', c: '#94a3b8' };
457
+ }
458
+ function ym(dstr) { return dstr.slice(0, 7); }
459
+ function toast(msg) {
460
+ var t = document.getElementById('toast');
461
+ t.textContent = msg;
462
+ t.classList.add('on');
463
+ clearTimeout(t._h);
464
+ t._h = setTimeout(function () { t.classList.remove('on'); }, 1500);
465
+ }
466
+ function $(id) { return document.getElementById(id); }
467
+
468
+ // 页内确认弹窗:WebView 里 confirm() 会被静默忽略,必须自绘
469
+ function ask(title, text, onOk, okLabel, danger) {
470
+ var m = document.getElementById('modal');
471
+ if (!m) { onOk(); return; }
472
+ m.querySelector('.mtitle').textContent = title;
473
+ m.querySelector('.mtext').textContent = text || '';
474
+ var ok = m.querySelector('.mbtn.ok');
475
+ ok.textContent = okLabel || '确定';
476
+ ok.className = 'mbtn ok' + (danger ? ' danger' : '');
477
+ m.classList.add('on');
478
+ m.querySelector('.mbtn.cancel').onclick = function () { m.classList.remove('on'); };
479
+ ok.onclick = function () { m.classList.remove('on'); onOk(); };
480
+ m.onclick = function (e) { if (e.target === m) m.classList.remove('on'); };
481
+ }
482
+
483
+ /* ---------- 渲染:顶部汇总(当月)---------- */
484
+ function renderHero() {
485
+ var m = ym(todayStr());
486
+ var out = 0, inc = 0, cnt = 0;
487
+ S.records.forEach(function (r) {
488
+ if (ym(r.date) !== m) return;
489
+ cnt++;
490
+ if (r.type === 'out') out += r.amount; else inc += r.amount;
491
+ });
492
+ $('balTxt').innerHTML = '<small>¥</small>' + money(inc - out);
493
+ $('outTxt').textContent = '¥' + money(out);
494
+ $('inTxt').textContent = '¥' + money(inc);
495
+ $('cntTxt').textContent = cnt;
496
+ }
497
+
498
+ /* ---------- 渲染:分类 chips ---------- */
499
+ function renderChips() {
500
+ var box = $('chips');
501
+ box.innerHTML = '';
502
+ CATS[S.type].forEach(function (c) {
503
+ var b = document.createElement('button');
504
+ b.textContent = c.e + ' ' + c.n;
505
+ if (S.cat === c.n) b.className = 'on';
506
+ b.onclick = function () { S.cat = c.n; renderChips(); };
507
+ box.appendChild(b);
508
+ });
509
+ if (!S.cat || !CATS[S.type].some(function (x) { return x.n === S.cat; })) {
510
+ S.cat = CATS[S.type][0].n;
511
+ renderChips();
512
+ }
513
+ }
514
+
515
+ /* ---------- 渲染:明细 ---------- */
516
+ function renderList() {
517
+ var y = S.listYear, mo = S.listMonth;
518
+ $('monthLbl').textContent = y + '年' + (mo + 1) + '月';
519
+ var key = y + '-' + pad(mo + 1);
520
+ var rows = S.records.filter(function (r) { return ym(r.date) === key; })
521
+ .sort(function (a, b) { return a.date < b.date ? 1 : (a.date > b.date ? -1 : b.ts - a.ts); });
522
+
523
+ var box = $('listBox');
524
+ if (!rows.length) {
525
+ box.innerHTML = '<div class="empty"><div>🗒️</div>这个月还没有记录</div>' + trashHtml();
526
+ bindTrash();
527
+ return;
528
+ }
529
+ var total = 0;
530
+ rows.forEach(function (r) { if (r.type === 'out') total += r.amount; else total -= r.amount; });
531
+
532
+ var html = '';
533
+ var curDay = '';
534
+ rows.forEach(function (r) {
535
+ if (r.date !== curDay) {
536
+ curDay = r.date;
537
+ var d = new Date(r.date + 'T00:00:00');
538
+ var wd = '日一二三四五六'[d.getDay()];
539
+ html += '<div class="day-h"><span>' + r.date.slice(5).replace('-', '月') + '日 周' + wd +
540
+ '</span><span>' + daySum(r.date) + '</span></div>';
541
+ }
542
+ var c = catOf(r.type, r.cat);
543
+ var sign = r.type === 'out' ? '-' : '+';
544
+ var col = r.type === 'out' ? 'var(--out)' : 'var(--in)';
545
+ var meta = [];
546
+ if (r.src) meta.push('<span class="tag">' + esc(r.src) + '</span>');
547
+ if (r.note) meta.push(esc(r.note)); else meta.push(c.n);
548
+ html += '<div class="item">' +
549
+ '<div class="ico" style="background:' + c.c + '22;color:' + c.c + '">' + c.e + '</div>' +
550
+ '<div class="mid"><div class="t">' + esc(r.cat) + '</div>' +
551
+ '<div class="n">' + meta.join(' · ') + '</div></div>' +
552
+ '<div class="v" style="color:' + col + '">' + sign + '¥' + money(r.amount) + '</div>' +
553
+ '<button class="del" data-id="' + r.id + '">✕</button>' +
554
+ '</div>';
555
+ });
556
+ html += '<div class="day-h" style="margin-top:18px"><span>本月结余</span><span style="color:' +
557
+ (total >= 0 ? 'var(--in)' : 'var(--out)') + '">¥' + money(-total) + '</span></div>';
558
+
559
+ var bakn = (window.LEDGER_BACKUP && window.LEDGER_BACKUP.length) || 0;
560
+ if (S.trash.length || bakn) {
561
+ html += '<div class="trashbar"><div class="lbl">回收站</div><div class="tb-row">';
562
+ if (S.trash.length) {
563
+ html += '<button class="ghost" id="undoBtn">恢复已删除的 ' + S.trash.length + ' 笔</button>';
564
+ html += '<button class="ghost" id="purgeBtn">彻底清空</button>';
565
+ }
566
+ if (bakn) html += '<button class="ghost" id="bakBtn">从文件备份恢复</button>';
567
+ html += '</div></div>';
568
+ }
569
+ box.innerHTML = html;
570
+ bindTrash();
571
+
572
+ box.querySelectorAll('.del').forEach(function (b) {
573
+ b.onclick = function () {
574
+ var id = b.getAttribute('data-id');
575
+ var rec = null;
576
+ S.records = S.records.filter(function (r) {
577
+ if (r.id === id) { rec = r; return false; }
578
+ return true;
579
+ });
580
+ if (rec) { S.trash.push(rec); saveTrash(); }
581
+ save(); renderAll(); toast('已删除(可在回收站恢复)');
582
+ };
583
+ });
584
+ }
585
+
586
+ function trashHtml() {
587
+ var bakn = (window.LEDGER_BACKUP && window.LEDGER_BACKUP.length) || 0;
588
+ if (!S.trash.length && !bakn) return '';
589
+ var h = '<div class="trashbar"><div class="lbl">回收站</div><div class="tb-row">';
590
+ if (S.trash.length) {
591
+ h += '<button class="ghost" id="undoBtn">恢复已删除的 ' + S.trash.length + ' 笔</button>';
592
+ h += '<button class="ghost" id="purgeBtn">彻底清空</button>';
593
+ }
594
+ if (bakn) h += '<button class="ghost" id="bakBtn">从文件备份恢复</button>';
595
+ return h + '</div></div>';
596
+ }
597
+
598
+ function bindTrash() {
599
+ var ub = document.getElementById('undoBtn');
600
+ if (ub) ub.onclick = restoreTrash;
601
+ var pb = document.getElementById('purgeBtn');
602
+ if (pb) pb.onclick = purgeTrash;
603
+ var bb = document.getElementById('bakBtn');
604
+ if (bb) bb.onclick = restoreBackup;
605
+ }
606
+
607
+ function daySum(date) {
608
+ var t = 0;
609
+ S.records.forEach(function (r) {
610
+ if (r.date !== date) return;
611
+ if (r.type === 'out') t += r.amount; else t -= r.amount;
612
+ });
613
+ return (t > 0 ? '支出 ¥' : (t < 0 ? '收入 ¥' : '')) + (t === 0 ? '' : money(Math.abs(t)));
614
+ }
615
+ function esc(s) {
616
+ return String(s).replace(/[&<>"]/g, function (m) {
617
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[m];
618
+ });
619
+ }
620
+
621
+ /* ---------- 渲染:统计 ---------- */
622
+ function renderStat() {
623
+ var y = S.statYear, mo = S.statMonth;
624
+ $('statLbl').textContent = y + '年' + (mo + 1) + '月';
625
+ var key = y + '-' + pad(mo + 1);
626
+ var rows = S.records.filter(function (r) { return ym(r.date) === key; });
627
+
628
+ var out = 0, inc = 0, max = 0, byCat = {};
629
+ rows.forEach(function (r) {
630
+ if (r.type === 'out') {
631
+ out += r.amount;
632
+ byCat[r.cat] = (byCat[r.cat] || 0) + r.amount;
633
+ if (r.amount > max) max = r.amount;
634
+ } else inc += r.amount;
635
+ });
636
+ var days = new Date(y, mo + 1, 0).getDate();
637
+ var isThisMonth = (todayStr().slice(0, 7) === key);
638
+ var used = isThisMonth ? new Date().getDate() : days;
639
+
640
+ $('sOut').textContent = '¥' + money(out);
641
+ $('sIn').textContent = '¥' + money(inc);
642
+ $('sAvg').textContent = '¥' + money(used ? out / used : 0);
643
+ $('sMax').textContent = '¥' + money(max);
644
+
645
+ var list = Object.keys(byCat).map(function (k) {
646
+ return { n: k, v: byCat[k] };
647
+ }).sort(function (a, b) { return b.v - a.v; });
648
+
649
+ var box = $('catBox');
650
+ if (!list.length) {
651
+ box.innerHTML = '<div class="empty"><div>📊</div>这个月还没有支出</div>';
652
+ return;
653
+ }
654
+ var mx = list[0].v;
655
+ var html = '';
656
+ list.forEach(function (it) {
657
+ var c = catOf('out', it.n);
658
+ var pct = out ? (it.v / out * 100).toFixed(0) : 0;
659
+ html += '<div class="catrow"><div class="top"><span>' + c.e + ' ' + esc(it.n) +
660
+ ' <i>' + pct + '%</i></span><span>¥' + money(it.v) + '</span></div>' +
661
+ '<div class="bar"><i style="width:' + Math.max(4, it.v / mx * 100) + '%;background:' + c.c + '"></i></div></div>';
662
+ });
663
+ box.innerHTML = html;
664
+ }
665
+
666
+ function renderAll() { renderHero(); renderList(); renderStat(); }
667
+
668
+ /* ---------- 事件 ---------- */
669
+ function bindTabs() {
670
+ document.querySelectorAll('.tabs button').forEach(function (b) {
671
+ b.onclick = function () {
672
+ document.querySelectorAll('.tabs button').forEach(function (x) { x.classList.remove('on'); });
673
+ b.classList.add('on');
674
+ S.tab = b.getAttribute('data-tab');
675
+ document.querySelectorAll('section').forEach(function (s) { s.classList.remove('on'); });
676
+ $('tab-' + S.tab).classList.add('on');
677
+ };
678
+ });
679
+ }
680
+
681
+ function bindForm() {
682
+ $('btnOut').onclick = function () { setType('out'); };
683
+ $('btnIn').onclick = function () { setType('in'); };
684
+ function setType(t) {
685
+ S.type = t;
686
+ S.cat = CATS[t][0].n;
687
+ $('btnOut').className = t === 'out' ? 'on-out' : '';
688
+ $('btnIn').className = t === 'in' ? 'on-in' : '';
689
+ renderChips();
690
+ }
691
+
692
+ $('saveBtn').onclick = function () {
693
+ var v = parseFloat($('amt').value);
694
+ if (!v || v <= 0) { toast('请填写金额'); $('amt').focus(); return; }
695
+ if (v > 9999999) { toast('金额太大啦'); return; }
696
+ S.records.push({
697
+ id: Date.now() + '' + Math.floor(Math.random() * 1000),
698
+ type: S.type,
699
+ amount: Math.round(v * 100) / 100,
700
+ cat: S.cat,
701
+ note: $('note').value.trim(),
702
+ date: $('date').value || todayStr(),
703
+ ts: Date.now()
704
+ });
705
+ save();
706
+ $('amt').value = '';
707
+ $('note').value = '';
708
+ renderAll();
709
+ toast('已记录 ✓');
710
+ $('amt').focus();
711
+ };
712
+ $('amt').addEventListener('keydown', function (e) {
713
+ if (e.key === 'Enter') $('saveBtn').click();
714
+ });
715
+ }
716
+
717
+ function stepMonth(which, dir) {
718
+ var yk = which === 'list' ? 'listYear' : 'statYear';
719
+ var mk = which === 'list' ? 'listMonth' : 'statMonth';
720
+ var y = S[yk], m = S[mk] + dir;
721
+ if (m < 0) { m = 11; y--; }
722
+ if (m > 11) { m = 0; y++; }
723
+ S[yk] = y; S[mk] = m;
724
+ if (which === 'list') renderList(); else renderStat();
725
+ }
726
+
727
+ function bindMonths() {
728
+ $('prevM').onclick = function () { stepMonth('list', -1); };
729
+ $('nextM').onclick = function () { stepMonth('list', 1); };
730
+ $('prevS').onclick = function () { stepMonth('stat', -1); };
731
+ $('nextS').onclick = function () { stepMonth('stat', 1); };
732
+ }
733
+
734
+ function exportCSV() {
735
+ if (!S.records.length) { toast('还没有数据'); return; }
736
+ var rows = [['日期', '类型', '分类', '金额', '备注']];
737
+ S.records.slice().sort(function (a, b) { return a.date < b.date ? -1 : 1; })
738
+ .forEach(function (r) {
739
+ rows.push([r.date, r.type === 'out' ? '支出' : '收入', r.cat, r.amount.toFixed(2), r.note || '']);
740
+ });
741
+ var csv = '\ufeff' + rows.map(function (r) {
742
+ return r.map(function (x) { return '"' + String(x).replace(/"/g, '""') + '"'; }).join(',');
743
+ }).join('\r\n');
744
+
745
+ var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
746
+ var a = document.createElement('a');
747
+ a.href = URL.createObjectURL(blob);
748
+ a.download = '小账本_' + todayStr() + '.csv';
749
+ document.body.appendChild(a);
750
+ a.click();
751
+ setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500);
752
+ toast('已导出 CSV');
753
+ }
754
+
755
+ /* ---------- 回收站 ---------- */
756
+ function mergeIn(list, msg) {
757
+ var seen = {};
758
+ S.records.forEach(function (r) { seen[r.id] = 1; });
759
+ var n = 0;
760
+ (list || []).forEach(function (r) {
761
+ if (!r || !r.id || !r.type || !r.amount || !r.date || seen[r.id]) return;
762
+ seen[r.id] = 1; S.records.push(r); n++;
763
+ });
764
+ save(); renderAll();
765
+ toast(n ? msg + ' ' + n + ' 笔' : '没有可恢复的记录');
766
+ }
767
+ function restoreTrash() {
768
+ if (!S.trash.length) return;
769
+ var t = S.trash;
770
+ S.trash = []; saveTrash();
771
+ mergeIn(t, '已恢复');
772
+ }
773
+ function purgeTrash() {
774
+ if (!S.trash.length) return;
775
+ ask('彻底清空回收站?', '共 ' + S.trash.length + ' 笔,删除后无法恢复。', function () {
776
+ S.trash = []; saveTrash(); renderAll(); toast('回收站已清空');
777
+ }, '彻底删除', true);
778
+ }
779
+ function restoreBackup() {
780
+ mergeIn(window.LEDGER_BACKUP, '已从文件备份恢复');
781
+ }
782
+
783
+ function clearAll() {
784
+ if (!S.records.length) { toast('没有数据可清空'); return; }
785
+ ask('清空全部记录?', '共 ' + S.records.length + ' 笔,会移入回收站,\n可在「明细」页恢复。', function () {
786
+ S.trash = S.trash.concat(S.records);
787
+ S.records = [];
788
+ saveTrash(); save(); renderAll(); toast('已清空(回收站可恢复)');
789
+ }, '清空', true);
790
+ }
791
+
792
+ /* ---------- 回终端主页 ---------- */
793
+ function goHome() {
794
+ try{window.__bgmFade&&window.__bgmFade()}catch(e){}
795
+ var p = location.pathname || '';
796
+ var u = 'index.html';
797
+ setTimeout(function () { location.href = u; }, 380);
798
+ }
799
+
800
+ /* ---------- 初始化 ---------- */
801
+ function init() {
802
+ var d = new Date();
803
+ S.listYear = S.statYear = d.getFullYear();
804
+ S.listMonth = S.statMonth = d.getMonth();
805
+ S.type = 'out';
806
+ S.cat = CATS.out[0].n;
807
+
808
+ $('date').value = todayStr();
809
+ // 明细/统计默认展示当前月
810
+ $('monthLbl').textContent = '';
811
+ $('statLbl').textContent = '';
812
+
813
+ load();
814
+ bindTabs(); bindForm(); bindMonths();
815
+ $('expBtn').onclick = exportCSV;
816
+ $('clrBtn').onclick = clearAll;
817
+ var bb = $('backBtn'); if (bb) bb.onclick = goHome;
818
+ renderChips();
819
+ renderAll();
820
+ }
821
+
822
+ if (document.readyState === 'loading') {
823
+ document.addEventListener('DOMContentLoaded', init);
824
+ } else init();
825
+ })();
826
+
827
+ </script>
828
+ <script>
829
+ /* ===== 附加增强:音效 + 过场(不改 app.js 逻辑,纯叠加) ===== */
830
+ (function () {
831
+ var AC = null, soundOn = true;
832
+ function ac() {
833
+ if (!AC) { try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { AC = null; } }
834
+ if (AC && AC.state === 'suspended') AC.resume();
835
+ return AC;
836
+ }
837
+ function tone(f, d, type, vol) {
838
+ var c = ac(); if (!c || !soundOn) return;
839
+ var o = c.createOscillator(), g = c.createGain();
840
+ o.type = type || 'triangle'; o.frequency.value = f; g.gain.value = 0;
841
+ o.connect(g); g.connect(c.destination);
842
+ var t = c.currentTime;
843
+ g.gain.linearRampToValueAtTime(vol || 0.06, t + 0.01);
844
+ g.gain.exponentialRampToValueAtTime(0.0001, t + (d || 0.08));
845
+ o.start(t); o.stop(t + (d || 0.08) + 0.03);
846
+ }
847
+ function sting() { tone(660, .09, 'triangle', .06); setTimeout(function () { tone(990, .09); }, 90); setTimeout(function () { tone(1320, .16); }, 180); }
848
+ function tick() { tone(1150, .03, 'square', .025); }
849
+ function tw(dur, cb, done) {
850
+ var t0 = Date.now(), id = setInterval(function () {
851
+ var p = Math.min(1, (Date.now() - t0) / dur); cb(p);
852
+ if (p >= 1) { clearInterval(id); if (done) done(); }
853
+ }, 12);
854
+ }
855
+ function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
856
+
857
+ function sweep() {
858
+ var w = document.getElementById('wipe');
859
+ w.style.display = 'block';
860
+ tw(360, function (p) {
861
+ w.style.transform = 'translateX(' + (-140 + p * 280).toFixed(1) + '%) skewX(-14deg)';
862
+ w.style.opacity = (p < .12 ? p * 8.3 : (p > .88 ? (1 - p) * 8.3 : 1)).toFixed(2);
863
+ }, function () { w.style.display = 'none'; });
864
+ }
865
+ function burst() {
866
+ var b = document.getElementById('burst'), flash = b.querySelector('.flash'), word = b.querySelector('.word');
867
+ b.style.display = 'flex';
868
+ tw(180, function (p) { flash.style.opacity = (0.8 * (1 - p)).toFixed(3); });
869
+ tw(280, function (p) {
870
+ var e = easeOut(p);
871
+ word.style.opacity = Math.min(1, p * 3).toFixed(2);
872
+ word.style.transform = 'skewX(-9deg) rotate(' + (-30 + e * 22).toFixed(1) + 'deg) scale(' + (0.5 + e * 0.6).toFixed(3) + ')';
873
+ }, function () {
874
+ setTimeout(function () {
875
+ tw(320, function (p) {
876
+ var e = easeOut(p);
877
+ word.style.opacity = (1 - p).toFixed(2);
878
+ word.style.transform = 'skewX(-9deg) rotate(' + (-8 + e * 9).toFixed(1) + 'deg) scale(' + (1.1 + e * 0.3).toFixed(3) + ') translateY(' + (-e * 30).toFixed(1) + 'px)';
879
+ }, function () {
880
+ b.style.display = 'none'; word.style.opacity = '0'; flash.style.opacity = '0';
881
+ });
882
+ }, 240);
883
+ });
884
+ }
885
+
886
+ var soundBtn = document.getElementById('soundBtn');
887
+ soundBtn.addEventListener('click', function () {
888
+ soundOn = !soundOn;
889
+ soundBtn.innerHTML = '<i>' + (soundOn ? '♪' : '✕') + '</i>';
890
+ if (soundOn) tone(880, .06, 'triangle', .05);
891
+ });
892
+
893
+ document.addEventListener('click', function (e) {
894
+ var el = e.target;
895
+ if (!el || !el.closest) return;
896
+ if (el.closest('.tabs button')) { tick(); sweep(); }
897
+ else if (el.closest('.seg button')) { tick(); }
898
+ else if (el.closest('.chips button')) { tick(); }
899
+ else if (el.closest('.month-bar button')) { tick(); }
900
+ else if (el.closest('.del')) { tick(); }
901
+ else if (el.closest('#saveBtn')) {
902
+ var v = parseFloat(document.getElementById('amt').value);
903
+ if (v > 0 && v <= 9999999) { sting(); setTimeout(burst, 60); }
904
+ }
905
+ }, true);
906
+ })();
907
+ </script>
908
+
909
+ <audio id="bgm" loop preload="none"></audio>
910
+ <div id="bgmBtn" title="BGM">♪</div>
911
+ <style>#bgmBtn{position:fixed;left:14px;bottom:14px;z-index:120;width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:2px solid #4cc9ff;background:rgba(7,12,24,.92);color:#4cc9ff;font-size:18px;cursor:pointer;user-select:none;-webkit-user-select:none;box-shadow:3px 3px 0 rgba(0,0,0,.45);font-family:ui-monospace,Menlo,monospace}#bgmBtn.on{background:#4cc9ff;color:#071018}</style>
912
+ <script>
913
+ /* ===== BGM ENGINE v2 · Color Your Night · 跨页续播 ===== */
914
+ (function(){
915
+ var au=document.getElementById('bgm');
916
+ var btn=document.querySelector('#bgmBtn,[data-bgm],#ci-bgm');
917
+ if(!au)return;
918
+ var K='whale.bgm',KP='whale.bgm.pos';
919
+ var want=localStorage.getItem(K)!=='off';
920
+ var pos0=parseFloat(localStorage.getItem(KP)||'0');if(isNaN(pos0)||pos0<0)pos0=0;
921
+ var abuf=null,dbuf=null,bURL='',node=null,ctx=null,gn=null,fbT=0,pend=0,decoding=0,warmOnly=0;
922
+ var elemTried=sessionStorage.getItem('whale.bgm.ef')==='1'?1:0;
923
+ var waT0=0,waOff=0;
924
+ function lit(s){if(btn)btn.classList.toggle('on',!!s)}
925
+ function playing(){return !!node||(!au.paused&&au.readyState>0)}
926
+ function curPos(){
927
+ if(node&&ctx&&dbuf)return (((ctx.currentTime-waT0)+waOff)%dbuf.duration)||0;
928
+ return au.currentTime||0;
929
+ }
930
+ function savePos(){try{localStorage.setItem(KP,curPos().toFixed(2))}catch(e){}}
931
+ window.addEventListener('pagehide',savePos);
932
+ setInterval(function(){if(playing())savePos()},1000);
933
+ if(btn){btn.addEventListener('click',function(e){
934
+ if(e&&e.preventDefault)e.preventDefault();
935
+ if(playing()){want=false;localStorage.setItem(K,'off');stopAll()}
936
+ else{want=true;localStorage.setItem(K,'on');ensure()}
937
+ });}
938
+ function fadeElem(){try{au.volume=0}catch(e){}var v=0,iv=setInterval(function(){v=Math.min(.6,v+.05);try{au.volume=v}catch(e){};if(v>=.6)clearInterval(iv)},70)}
939
+ function startElem(){
940
+ au.src=bURL;au.load();
941
+ var ap=function(){try{au.currentTime=pos0}catch(e){pos0=0}};
942
+ if(au.readyState>=1)ap();else au.addEventListener('loadedmetadata',ap,{once:true});
943
+ var p=au.play();if(p&&p.catch)p.catch(function(){});
944
+ clearTimeout(fbT);
945
+ fbT=setTimeout(function(){
946
+ if(au.readyState<3||au.paused){try{au.pause()}catch(e){};elemTried=1;try{sessionStorage.setItem('whale.bgm.ef','1')}catch(e){};toWA()}
947
+ },3200);
948
+ }
949
+ function toWA(){
950
+ if(!dbuf){
951
+ if(!abuf||decoding){pend=1;return}
952
+ try{
953
+ var AC=window.AudioContext||window.webkitAudioContext;if(!AC)return;
954
+ ctx=ctx||new AC();if(ctx.resume)ctx.resume();
955
+ decoding=1;
956
+ ctx.decodeAudioData(abuf.slice(0),function(b){decoding=0;dbuf=b;if(warmOnly){warmOnly=0;if(pend&&want){pend=0;ensure()}}else{startNode()}},function(){decoding=0});
957
+ }catch(e){decoding=0}
958
+ return;
959
+ }
960
+ startNode();
961
+ }
962
+ function startNode(){
963
+ try{
964
+ if(!gn){gn=ctx.createGain();gn.connect(ctx.destination)}
965
+ if(ctx.state!=='running'&&ctx.resume)ctx.resume();
966
+ if(node){try{node.stop()}catch(e){};node=null}
967
+ if(dbuf.duration&&pos0>dbuf.duration-0.6)pos0=0;
968
+ node=ctx.createBufferSource();node.buffer=dbuf;node.loop=true;
969
+ node.connect(gn);gn.gain.value=0;
970
+ waT0=ctx.currentTime;waOff=pos0||0;
971
+ node.start(0,waOff);
972
+ var v=0,iv=setInterval(function(){v=Math.min(.6,v+.05);gn.gain.value=v;if(v>=.6)clearInterval(iv)},70);
973
+ lit(1);try{sessionStorage.setItem('whale.bgm.ef','1')}catch(e){}
974
+ }catch(e){}
975
+ }
976
+ function stopAll(){clearTimeout(fbT);savePos();
977
+ try{au.pause()}catch(e){}
978
+ try{if(node){node.stop();node=null}}catch(e){}
979
+ lit(0);
980
+ }
981
+ function ensure(){
982
+ if(playing())return;
983
+ if(abuf&&elemTried){toWA();return}
984
+ if(bURL){startElem();return}
985
+ pend=1;
986
+ }
987
+ au.addEventListener('playing',function(){lit(1);clearTimeout(fbT);fadeElem()});
988
+ au.addEventListener('pause',function(){if(!node)lit(0)});
989
+ document.addEventListener('visibilitychange',function(){if(!document.hidden){if(ctx&&ctx.state!=='running'&&ctx.resume){try{ctx.resume()}catch(e){}}if(want&&!playing())ensure()}});
990
+ function kick(){if(ctx&&ctx.state!=='running'&&ctx.resume){try{ctx.resume()}catch(e){}}if(!want||playing())return;if(bURL)ensure();else pend=1}
991
+ document.addEventListener('pointerdown',kick,true);
992
+ au.addEventListener('playing',function(){document.removeEventListener('pointerdown',kick,true)});
993
+ fetch('bgm.mp3').then(function(r){return r.arrayBuffer()}).then(function(a){
994
+ abuf=a;bURL=URL.createObjectURL(new Blob([a],{type:'audio/mpeg'}));
995
+ if(elemTried&&want){warmOnly=1;toWA()}
996
+ if(want&&!playing()){pend=0;ensure()}
997
+ }).catch(function(){});
998
+ if(want){setTimeout(function(){if(!playing()&&bURL){ensure()}},400)}
999
+ window.__bgmFade=function(){savePos();try{if(node&&gn){var g0=gn.gain.value,t0=Date.now();var iv=setInterval(function(){var k=Math.max(0,1-(Date.now()-t0)/500);gn.gain.value=g0*k;if(k<=0)clearInterval(iv)},60)}}catch(e){}};
1000
+ })();
1001
+ </script>
1002
+ </body>
1003
+ </html>