Repository navigation
Expand file tree
/
Copy pathselfcheck.html
More file actions
450 lines (434 loc) · 29.1 KB
/
Copy pathselfcheck.html
File metadata and controls
450 lines (434 loc) · 29.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>生活自查 · 高性价比人生指南</title>
<meta name="description" content="回答一组是否题,看看书里哪些事你还没做到:哪一块漏得最多、先做哪几件、哪些一次办完就行。每条单独给出证据和出处,不汇总成「少活几年」。">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%233451b2'/%3E%3Cpath d='M17 33l10 11 20-24' fill='none' stroke='white' stroke-width='7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
<script>(function(){try{var s=localStorage.getItem('theme');var d=s?s==='dark':matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark')}catch(e){}})();</script>
<style>
/* 配色、字体与 index.html 同一套(VitePress 默认主题) */
:root{
--bg:#ffffff; --bg-alt:#f6f6f7; --bg-soft:#f6f6f7; --bg-mute:#f1f1f2;
--divider:#e2e2e3;
--t1:rgba(60,60,67,1); --t2:rgba(60,60,67,.78); --t3:rgba(60,60,67,.56);
--brand-1:#3451b2; --brand-2:#3a5ccc; --brand-soft:rgba(100,108,255,.14);
--green-1:#18794e; --green-soft:rgba(16,185,129,.14);
--yellow-1:#915930; --yellow-soft:rgba(234,179,8,.14);
--red-1:#b8272c; --red-soft:rgba(244,63,94,.14);
--gray-1:#565a5f; --gray-soft:rgba(142,150,170,.14);
--font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;
color-scheme:light;
}
html.dark{
--bg:#1b1b1f; --bg-alt:#161618; --bg-soft:#202127; --bg-mute:#2b2b2f;
--divider:#2e2e32;
--t1:rgba(255,255,245,.86); --t2:rgba(235,235,245,.6); --t3:rgba(235,235,245,.38);
--brand-1:#a8b1ff; --brand-2:#5c73e7; --brand-soft:rgba(100,108,255,.16);
--green-1:#3dd68c; --green-soft:rgba(16,185,129,.16);
--yellow-1:#f9b44e; --yellow-soft:rgba(234,179,8,.16);
--red-1:#f66f81; --red-soft:rgba(244,63,94,.16);
--gray-1:#a4a8ae; --gray-soft:rgba(142,150,170,.16);
color-scheme:dark;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--t1);font:15px/1.75 var(--font);-webkit-font-smoothing:antialiased}
a{color:var(--brand-1);text-decoration:none}
a:hover{color:var(--brand-2);text-decoration:underline;text-underline-offset:2px}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
:focus-visible{outline:2px solid var(--brand-1);outline-offset:2px;border-radius:4px}
[hidden]{display:none!important}
.nav{position:sticky;top:0;z-index:10;height:56px;background:var(--bg);border-bottom:1px solid var(--divider);display:flex;align-items:center;gap:12px;padding:0 24px}
.nav .title{font-weight:600;font-size:16px;color:var(--t1)}
.nav .title:hover{text-decoration:none}
.nav .sp{flex:1}
.nav .back{font-size:14px;font-weight:500}
.wrap{max-width:760px;margin:0 auto;padding:32px 24px 96px}
h1{font-size:28px;line-height:1.3;margin:0 0 12px;font-weight:600;letter-spacing:-.01em}
.lead{color:var(--t2);margin:0 0 28px}
h2{font-size:18px;font-weight:600;margin:36px 0 4px;padding-top:24px;border-top:1px solid var(--divider)}
h2 small{font-weight:400;font-size:13px;color:var(--t3);margin-left:8px}
.q{display:flex;align-items:center;gap:16px;padding:12px 0;border-top:1px solid var(--divider)}
h2+.q{border-top:0}
.q .txt{flex:1;min-width:0}
.q .txt small{display:block;color:var(--t3);font-size:13px;line-height:1.5}
.seg{display:inline-flex;flex:none;border:1px solid var(--divider);border-radius:8px;overflow:hidden;background:var(--bg-soft)}
.seg button{padding:5px 12px;font-size:14px;color:var(--t2);border-left:1px solid var(--divider);white-space:nowrap;transition:background .2s,color .2s}
.seg button:first-child{border-left:0}
.seg button:hover{color:var(--t1)}
.seg button[aria-pressed="true"]{background:var(--brand-soft);color:var(--brand-1);font-weight:600}
.seg button.no[aria-pressed="true"]{background:var(--red-soft);color:var(--red-1)}
.seg button.yes[aria-pressed="true"]{background:var(--green-soft);color:var(--green-1)}
.bar{position:sticky;bottom:0;margin-top:32px;padding:14px 0;background:var(--bg);border-top:1px solid var(--divider);display:flex;align-items:center;gap:16px}
.bar .prog{flex:1;color:var(--t2);font-size:14px}
.btn{border-radius:20px;padding:0 20px;line-height:38px;font-weight:600;font-size:14px;background:var(--brand-1);color:#fff;transition:background .2s}
html.dark .btn{color:#1b1b1f}
.btn:hover{background:var(--brand-2)}
.btn.ghost{background:var(--bg-mute);color:var(--t1)}
.btn.ghost:hover{background:var(--divider)}
.sum{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:8px 0 12px}
.tile{background:var(--bg-soft);border-radius:10px;padding:14px 16px}
.tile b{display:block;font-size:26px;line-height:1.2;font-variant-numeric:tabular-nums}
.tile span{font-size:13px;color:var(--t2)}
.tile.r3 b{color:var(--brand-1)} .tile.r2 b{color:var(--green-1)}
.note{font-size:13.5px;color:var(--t2);background:var(--bg-soft);border-radius:8px;padding:12px 16px;margin:20px 0}
.note b{color:var(--t1)}
.card{border:1px solid var(--divider);border-radius:12px;padding:16px 20px;margin:12px 0;background:var(--bg)}
.card .h{display:flex;gap:10px;align-items:baseline}
.card .h b{flex:1;font-size:16px;font-weight:600;line-height:1.5}
.card .h a{flex:none;font-size:13px;white-space:nowrap}
.card .human{margin:8px 0 10px;color:var(--t1)}
.card .cost{font-size:13.5px;color:var(--t2);margin-top:6px}
.card .also{margin-top:10px;padding-top:10px;border-top:1px dashed var(--divider);font-size:13.5px;color:var(--t2)}
.badges{display:flex;flex-wrap:wrap;gap:6px}
.badge{display:inline-block;border-radius:10px;padding:0 10px;line-height:20px;font-size:12px;font-weight:600;white-space:nowrap}
.badge.A{background:var(--green-soft);color:var(--green-1)}
.badge.B{background:var(--yellow-soft);color:var(--yellow-1)}
.badge.C{background:var(--gray-soft);color:var(--gray-1)}
.badge.r3{background:var(--brand-soft);color:var(--brand-1)}
.badge.r2{background:var(--green-soft);color:var(--green-1)}
.badge.r1,.badge.plain{background:var(--gray-soft);color:var(--gray-1)}
.badge.warn{background:var(--yellow-soft);color:var(--yellow-1)}
.verdict{font-size:16px;line-height:1.85;margin:0 0 20px}
.verdict b{font-weight:600}
.mods{display:flex;flex-wrap:wrap;gap:8px}
.mods button{border:1px solid var(--divider);border-radius:16px;padding:3px 14px;font-size:14px;color:var(--t2);background:var(--bg-soft)}
.mods button[aria-pressed="true"]{border-color:var(--brand-1);background:var(--brand-soft);color:var(--brand-1);font-weight:600}
.hbar{display:grid;grid-template-columns:8.5em 1fr 4.5em;align-items:center;gap:12px;padding:6px 0;font-size:14px}
.hbar .nm{color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hbar .tr{height:10px;border-radius:5px;background:var(--bg-mute);overflow:hidden}
.hbar .fl{height:100%;border-radius:0 4px 4px 0;background:var(--brand-1)}
.hbar .v{color:var(--t2);text-align:right;font-variant-numeric:tabular-nums}
.top3{margin:8px 0 0;padding-left:22px}
.top3 li{margin:6px 0}
.top3 .why{color:var(--t2);font-size:13.5px}
.card{scroll-margin-top:72px}
.done-list{color:var(--t2);font-size:14px;padding-left:20px}
#status{color:var(--t2);padding:40px 0;text-align:center}
@media (max-width:640px){
.nav{padding:0 16px}
.wrap{padding:24px 16px 80px}
h1{font-size:24px}
.q{flex-direction:column;align-items:stretch;gap:8px}
.seg{align-self:flex-start}
.sum{grid-template-columns:repeat(2,1fr)}
.hbar{grid-template-columns:7.5em 1fr 3.8em;gap:8px}
}
</style>
</head>
<body>
<header class="nav">
<a class="title" href="./">高性价比人生指南</a>
<span class="sp"></span>
<a class="back" href="./">回检索页</a>
</header>
<main class="wrap">
<div id="status">正在读取正文 …</div>
<section id="ask" hidden>
<h1>生活自查</h1>
<p class="lead">回答下面的问题,看看书里哪些事你还没做到。结果会告诉你哪一块漏得最多、先做哪几件、哪些一次办完就行。每条单独给出证据和出处,不汇总成一个分数。</p>
<div id="qs"></div>
<div class="bar">
<span class="prog" id="prog"></span>
<button class="btn ghost" id="reset" type="button">清空</button>
<button class="btn" id="go" type="button">看结果</button>
</div>
</section>
<section id="out" hidden>
<h1>自查结果</h1>
<p class="verdict" id="verdict"></p>
<div class="sum" id="sum"></div>
<h2>哪一块漏得最多<small>按做到的比例从低到高排</small></h2>
<div id="bars"></div>
<div id="todo"></div>
<div class="note"><b>为什么不算「少活几年」:</b>每条的数字来自不同研究、不同人群,而且抽烟、不运动、睡得差这些习惯常常一起出现。把各条的风险乘到一起会重复计算,算出来的年数会偏大,偏多少也说不清。所以每条只给它自己那份证据。</div>
<h2 id="done-h">已经做到的</h2>
<ul class="done-list" id="done"></ul>
<div class="bar">
<span class="prog">答案只存在你这台设备的浏览器里。</span>
<button class="btn ghost" id="back" type="button">改答案</button>
</div>
</section>
</main>
<script>
/* 题目表。sec + key 按「第几节、标题里含哪段字」找条目,不写条号,插条目顺延时不会指歪;
找不到就在控制台报错并跳过这题。
when(p) 按基本情况决定这题问不问;also 是没做到时顺带列出的相关条目。 */
const PROFILE = [
{id:'age', text:'你的年龄', opts:[['<30','30 岁以下'],['30','30–44'],['45','45–59'],['60','60 岁以上']]},
{id:'sex', text:'你的性别', opts:[['m','男'],['f','女']]},
{id:'kid', text:'家里有 4 岁以下的孩子吗', opts:[['y','有'],['n','没有']]},
];
const ageAtLeast = (p, a) => ({'<30':20,'30':30,'45':45,'60':60})[p.age] >= a;
const GROUPS = [
{name:'烟、酒和槟榔', mod:'身体', qs:[
{sec:2, key:'戒烟,越早越好', text:'不抽烟,或者已经戒了', also:[{sec:2, key:'先去拿药'},{sec:2, key:'戒烟门诊'}]},
{sec:2, key:'少喝或不喝酒', text:'不喝酒,或者很少喝'},
{sec:2, key:'不嚼槟榔', text:'不嚼槟榔'},
]},
{name:'吃喝', mod:'身体', qs:[
{sec:2, key:'含糖饮料', text:'基本不喝奶茶、可乐、果汁这类含糖饮料'},
{sec:2, key:'加工肉', text:'很少吃火腿、香肠、培根、午餐肉'},
{sec:2, key:'全谷物', text:'主食里有一部分是糙米、燕麦、全麦这类全谷物'},
{sec:2, key:'5 份', text:'每天吃够一斤左右的水果和蔬菜'},
{sec:2, key:'坚果', text:'每天吃一小把坚果'},
{sec:2, key:'低钠盐', text:'家里做饭用低钠盐'},
{sec:2, key:'滚烫', text:'热茶热汤放一放再喝,不喝滚烫的'},
{sec:2, key:'抽油烟机', text:'炒菜时从开火到做完一直开着抽油烟机', hint:'不做饭可以跳过'},
]},
{name:'动、睡和晒太阳', mod:'身体', qs:[
{sec:2, key:'7000', text:'每天走到 7000 步左右,或者每周快走累计 150 分钟以上'},
{sec:2, key:'力量训练', text:'每周做点力量训练(俯卧撑、深蹲、器械都算)'},
{sec:2, key:'别连着坐太久', text:'不会连着坐好几个小时不起身'},
{sec:2, key:'每晚睡 7', text:'每晚睡 7 小时左右,作息基本固定'},
{sec:2, key:'晒晒太阳', text:'白天会出门见见太阳,不是整天待在屋里'},
{sec:2, key:'BMI', text:'体重在正常范围(BMI 20–25)', hint:'BMI = 体重(公斤) ÷ 身高(米)²'},
{sec:2, key:'认真刷牙', text:'每天认真刷牙、清一次牙缝,缺了牙会去补'},
{sec:1, key:'平衡和腿部力量', text:'练过平衡和腿部力量,浴室楼梯装了扶手防滑', when:p => ageAtLeast(p,60)},
]},
{name:'出门和家里的安全', mod:'身体', qs:[
{sec:1, key:'系安全带', text:'坐车时前排后排都系安全带'},
{sec:1, key:'不酒驾', text:'开车不超速、不酒驾', hint:'不开车可以跳过'},
{sec:1, key:'戴头盔', text:'骑电动车、摩托车戴头盔并扣好', hint:'不骑可以跳过'},
{sec:1, key:'电动自行车不推进楼道', text:'电动车不推进楼道,不在家里充电', hint:'没有电动车可以跳过'},
{sec:1, key:'烟雾报警器', text:'家里装了烟雾报警器'},
{sec:1, key:'燃气软管', text:'燃气软管和灶具到期换过,没自己改过管道', hint:'家里不用燃气可以跳过'},
{sec:1, key:'野生蘑菇', text:'不采、不买、不吃野生蘑菇'},
{sec:1, key:'安全套', text:'和不固定的伴侣发生关系,全程用安全套', hint:'没有这种情况可以跳过'},
{sec:1, key:'安全座椅', text:'孩子坐车用安全座椅,不抱在怀里', when:p => p.kid === 'y'},
{sec:1, key:'限位器', text:'窗户和阳台装了限位器或护栏', when:p => p.kid === 'y'},
]},
{name:'吃药', mod:'身体', qs:[
{sec:34, key:'对乙酰氨基酚', text:'同时吃两种感冒药或止痛药之前,会先看成分表'},
{sec:34, key:'普通感冒别找医生要抗生素', text:'普通感冒不找医生要抗生素'},
{sec:34, key:'头孢、甲硝唑', text:'吃头孢、甲硝唑期间和停药后一周不喝酒'},
{sec:2, key:'有高血压、高血脂', text:'有高血压、高血脂的,在按医嘱规律吃药', hint:'没有这些病可以跳过'},
]},
{name:'检查和疫苗', mod:'身体', qs:[
{sec:1, key:'量血压', text:'最近一年量过血压,高了在吃药控制'},
{sec:1, key:'空腹血糖', text:'查过空腹血糖', when:p => ageAtLeast(p,30)},
{sec:1, key:'乙肝两对半', text:'查过乙肝两对半,没抗体的打了疫苗'},
{sec:1, key:'幽门螺杆菌', text:'查过幽门螺杆菌'},
{sec:1, key:'HPV 疫苗', text:'打过 HPV 疫苗', when:p => p.sex === 'f'},
{sec:1, key:'宫颈癌筛查', text:'做过宫颈癌筛查', when:p => p.sex === 'f' && ageAtLeast(p,30)},
{sec:1, key:'乳腺癌筛查', text:'近两年做过乳腺钼靶', when:p => p.sex === 'f' && ageAtLeast(p,45)},
{sec:1, key:'结直肠癌筛查', text:'做过结直肠癌筛查(大便潜血或肠镜)', when:p => ageAtLeast(p,45)},
{sec:1, key:'流感疫苗', text:'每年打流感疫苗', when:p => ageAtLeast(p,60)},
{sec:1, key:'带状疱疹疫苗', text:'打过带状疱疹疫苗', when:p => ageAtLeast(p,60)},
{sec:1, key:'肺炎球菌疫苗', text:'打过肺炎球菌疫苗', when:p => ageAtLeast(p,60)},
{sec:1, key:'骨密度', text:'做过骨密度检查', when:p => p.sex === 'f' && ageAtLeast(p,60)},
]},
{name:'急救常识', mod:'急救常识', qs:[
{sec:13, key:'有人倒地没呼吸', text:'看到有人倒地没呼吸,知道要立刻用力按压胸口,并让旁人找 AED'},
{sec:13, key:'突然嘴歪', text:'知道突然嘴歪、一侧胳膊没劲要直接打 120,不自己开车去'},
{sec:13, key:'胸口压着疼', text:'知道胸口压着疼超过 15 分钟不缓解要打 120'},
{sec:13, key:'有人噎住', text:'会给噎住的人做拍背加腹部冲击'},
{sec:13, key:'烫伤后', text:'知道烫伤先用凉的流水冲 20 分钟,不抹牙膏酱油'},
{sec:13, key:'被狗、猫咬伤', text:'被猫狗咬伤或抓破皮,会先冲洗再当天去打疫苗'},
{sec:13, key:'误服', text:'知道误服清洁剂、农药不催吐,带上瓶子去医院'},
{sec:13, key:'火灾时', text:'知道火灾时贴地走、先摸门、走楼梯不坐电梯'},
{sec:13, key:'婴儿噎住', text:'会给不满 1 岁的婴儿做拍背加按胸', when:p => p.kid === 'y'},
]},
{name:'精力和情绪', mod:'精力', qs:[
{sec:3, key:'关掉非必要通知', text:'关掉了手机上不必要的通知,工作时手机放在看不到的地方'},
{sec:3, key:'固定起床时间', text:'周末也基本按平时的时间起床'},
{sec:3, key:'下午两点以后', text:'下午两点以后不喝咖啡、浓茶'},
{sec:3, key:'到点就睡', text:'不会为了打游戏、刷短视频熬夜'},
{sec:3, key:'睡前一小时', text:'睡前一小时不看手机'},
{sec:3, key:'别人过得怎么样', text:'不会每天刷同龄人的动态'},
{sec:3, key:'49 小时', text:'每周工作时间基本不超过 50 小时', hint:'没在上班可以跳过'},
{sec:3, key:'学会拒绝', text:'不想接的请求能说出「不」'},
{sec:22, key:'定期和人见面', text:'定期和朋友家人见面,不是心情差了才找人'},
]},
{name:'钱', mod:'钱', qs:[
{sec:5, key:'自动续费', text:'关掉了不用的自动续费'},
{sec:5, key:'个税汇算', text:'每年做个税汇算,该填的专项附加扣除都填了', hint:'没有工资收入可以跳过'},
{sec:5, key:'最低还款', text:'不用信用卡最低还款,不开消费分期和消费贷'},
{sec:5, key:'先存出', text:'存了 3 到 6 个月生活费的应急金,能随时取'},
{sec:5, key:'高收益', text:'看到「保本」「高收益」「稳赚」会直接走开'},
{sec:5, key:'家庭共济', text:'在医保 App 上办了家庭共济', hint:'没有职工医保可以跳过'},
{sec:5, key:'三者险', text:'开车的话,三者险买够了', hint:'没有车可以跳过'},
{sec:5, key:'定期寿险', text:'家里有人靠你的收入过日子,你买了定期寿险', hint:'家里没人靠你收入可以跳过'},
{sec:5, key:'宽基指数', text:'长期投资的钱放在宽基指数基金,而不是主动基金或个股', hint:'没有投资可以跳过'},
]},
{name:'账号与防骗', mod:'账号与防骗', qs:[
{sec:14, key:'二次验证', text:'邮箱、支付、社交账号都开了二次验证'},
{sec:14, key:'邮箱密码单独', text:'邮箱密码单独一个,没在别的网站用过'},
{sec:14, key:'锁屏密码', text:'手机设了锁屏密码,SIM 卡设了 PIN 码'},
{sec:8, key:'通讯录里的旧号码', text:'家人视频或电话要你转账,会先挂断,用通讯录里的旧号码打回去核实'},
{sec:8, key:'不把银行卡', text:'从不把银行卡、手机卡、支付账号借给别人'},
{sec:8, key:'征信报告', text:'查过自己的征信报告'},
{sec:8, key:'签字前', text:'签字前会把纸看完,不在空白纸上签'},
]},
];
const MODS = ['身体', '急救常识', '精力', '钱', '账号与防骗'];
const modOn = m => ans['mod:' + m] !== 'off';
/* 解析:和 index.html 的 parseReadme 同一套格式,只取这里用得到的字段 */
const COST_W = { money:{'0':0,'少':1,'多':2}, time:{'少':0,'中':1,'多':2}, will:{'否':0,'些':1,'是':2} };
function parse(md){
const out = []; let sec = null, e = null; let m;
for (const raw of md.split(/\r?\n/)){
const line = raw.trimEnd();
if ((m = /^#{1,2} (\d+)\. /.exec(line))) { sec = +m[1]; e = null; continue; }
if ((m = /^### (\d+)\. (.+)$/.exec(line)) && sec) { e = {sec, n:+m[1], title:m[2].trim(), human:'', cost:'', grade:'', note:''}; out.push(e); continue; }
if (!e) continue;
if ((m = /^\x3c!--\s*成本标签:\s*(.*?)\s*-->/.exec(line))){
for (const kv of m[1].split(/\s+/)){ const [k,v] = kv.split('='); e[{'钱':'money','时间':'time','毅力':'will','收益':'level','口径':'lens'}[k]] = v; }
}
else if ((m = /^- 说人话:(.*)$/.exec(line))) e.human = m[1];
else if ((m = /^- 成本:(.*)$/.exec(line))) e.cost = m[1];
else if ((m = /^- 证据等级:\s*([ABC])/.exec(line))) e.grade = m[1];
else if ((m = /^- 备注:(.*)$/.exec(line))) e.note = m[1];
}
for (const x of out){
x.cs = (COST_W.money[x.money] ?? 0) + (COST_W.time[x.time] ?? 0) + (COST_W.will[x.will] ?? 0);
x.ratio = x.level === '大' ? (x.cs === 0 ? '极高' : (x.cs <= 2 ? '高' : '一般'))
: x.level === '中' ? (x.cs === 0 ? '高' : '一般') : '一般';
x.dispute = /^争议/.test(x.note);
}
return out;
}
const plain = s => s.replace(/\*\*/g, '').replace(/\\([*_])/g, '$1').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
const esc = s => s.replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch]));
let ENTRIES = [];
const find = (sec, key) => {
const hit = ENTRIES.find(x => x.sec === sec && x.title.includes(key));
if (!hit) console.error(`自查题找不到条目:第 ${sec} 节,标题含「${key}」`);
return hit;
};
/* 答案:只存本机浏览器,取不到也照常能用 */
const STORE = 'selfcheck-v1';
let ans = {};
try { ans = JSON.parse(localStorage.getItem(STORE) || '{}'); } catch (e) {}
const save = () => { try { localStorage.setItem(STORE, JSON.stringify(ans)); } catch (e) {} };
const profile = () => Object.fromEntries(PROFILE.map(p => [p.id, ans['p:'+p.id]]));
const profileDone = () => PROFILE.every(p => ans['p:'+p.id]);
const qid = q => q.sec + ':' + q.key;
const shown = (q, p) => q.entry && modOn(q.g.mod) && (!q.when || (profileDone() && q.when(p)));
const activeQs = () => { const p = profile(); return GROUPS.flatMap(g => g.qs).filter(q => shown(q, p)); };
function seg(name, opts, cur){
return `<div class="seg" role="group">` + opts.map(([v, t, cls]) =>
`<button type="button" data-k="${esc(name)}" data-v="${v}" class="${cls||''}" aria-pressed="${cur === v}">${t}</button>`).join('') + `</div>`;
}
const YN = [['y','做到了','yes'],['n','没做到','no'],['s','跳过']];
function renderAsk(){
const p = profile();
let h = `<h2 style="border-top:0;padding-top:0;margin-top:0">基本情况<small>决定问哪些题</small></h2>`;
for (const f of PROFILE) h += `<div class="q"><div class="txt">${f.text}</div>${seg('p:'+f.id, f.opts, ans['p:'+f.id])}</div>`;
h += `<div class="q"><div class="txt">想查哪几块<small>点一下取消,不查的整块不出题</small></div><div class="mods">`
+ MODS.map(m => `<button type="button" data-mod="${m}" aria-pressed="${modOn(m)}">${m}</button>`).join('') + `</div></div>`;
for (const g of GROUPS){
const qs = g.qs.filter(q => shown(q, p));
if (!qs.length) continue;
h += `<h2>${g.name}</h2>`;
for (const q of qs) h += `<div class="q"><div class="txt">${esc(q.text)}${q.hint ? `<small>${esc(q.hint)}</small>` : ''}</div>${seg(qid(q), YN, ans[qid(q)])}</div>`;
}
document.getElementById('qs').innerHTML = h;
const qs = activeQs(), n = qs.filter(q => ans[qid(q)]).length;
document.getElementById('prog').textContent = profileDone() ? `已答 ${n} / ${qs.length} 题` : '先填基本情况';
}
const RANK = {'极高':0,'高':1,'一般':2}, LV = {'大':0,'中':1,'小':2}, GR = {A:0,B:1,C:2};
function card(e, also){
const r = {'极高':'r3','高':'r2','一般':'r1'}[e.ratio];
const lens = e.lens ? `<span class="badge plain">口径 ${e.lens}</span>` : '';
const alsoH = also.length ? `<div class="also">相关做法:` + also.map(a =>
`<a href="./#e-${a.sec}-${a.n}">${esc(plain(a.title))}</a>`).join(';') + `</div>` : '';
return `<div class="card" id="c-${e.sec}-${e.n}">
<div class="h"><b>${esc(plain(e.title))}</b><a href="./#e-${e.sec}-${e.n}">第 ${e.sec} 节第 ${e.n} 条 →</a></div>
${e.human ? `<div class="human">${esc(plain(e.human))}</div>` : ''}
<div class="badges"><span class="badge ${r}">性价比 ${e.ratio}</span><span class="badge ${e.grade}">证据 ${e.grade} 级</span><span class="badge plain">收益 ${e.level}</span>${lens}${e.dispute ? '<span class="badge warn">有争议</span>' : ''}</div>
<div class="cost">要花的:${esc(plain(e.cost))}</div>${alsoH}
</div>`;
}
/* 没做到的事按「要不要改习惯」分三堆:成本标签里的毅力一项直接决定 */
const PILES = [
{will:'否', name:'一次办完就行', sub:'做一次或者记住就够,不用改习惯'},
{will:'些', name:'要改一个习惯', sub:'要忍一点不适,坚持几周就成了习惯'},
{will:'是', name:'要长期坚持', sub:'和惯性长期对着干,最难,挑一件慢慢来'},
];
const WILL = {'否':0,'些':1,'是':2};
const byValue = (a, b) => RANK[a.entry.ratio] - RANK[b.entry.ratio] || LV[a.entry.level] - LV[b.entry.level]
|| WILL[a.entry.will] - WILL[b.entry.will] || GR[a.entry.grade] - GR[b.entry.grade] || a.entry.cs - b.entry.cs;
function renderOut(){
const qs = activeQs();
const no = qs.filter(q => ans[qid(q)] === 'n').sort(byValue), yes = qs.filter(q => ans[qid(q)] === 'y');
const answered = no.length + yes.length;
// 各块做到的比例,只算答了「做到 / 没做到」的题,跳过的不进分母
const groups = GROUPS.map(g => {
const y = yes.filter(q => q.g === g).length, n = no.filter(q => q.g === g).length;
return {g, y, t:y + n, pct: y + n ? y / (y + n) : 1};
}).filter(r => r.t).sort((a, b) => a.pct - b.pct || b.t - a.t);
document.getElementById('bars').innerHTML = groups.map(r =>
`<div class="hbar" title="${esc(r.g.name)}:答了 ${r.t} 题,做到 ${r.y} 项"><span class="nm">${esc(r.g.name)}</span>`
+ `<span class="tr"><span class="fl" style="display:block;width:${(r.pct * 100).toFixed(0)}%"></span></span>`
+ `<span class="v">${r.y} / ${r.t}</span></div>`).join('');
const easy = no.filter(q => q.entry.will === '否'), free = easy.filter(q => q.entry.money === '0');
const good = no.filter(q => q.entry.ratio !== '一般'), disp = no.filter(q => q.entry.dispute);
const weak = groups.find(r => r.t >= 2 && r.pct < 1);
let v = `你答了 ${answered} 题,做到 <b>${yes.length}</b> 项,没做到 <b>${no.length}</b> 项。`;
if (!no.length) v += '书里这些事你都做到了。';
if (weak) v += `漏得最多的是「${esc(weak.g.name)}」,${weak.t} 项里做到 ${weak.y} 项。`;
if (easy.length) v += `没做到的里有 <b>${easy.length}</b> 项一次办完就行,不用改习惯` + (free.length ? `,其中 ${free.length} 项不花钱。` : '。');
if (disp.length) v += `有 ${disp.length} 项证据有争议,先看原条目的备注再决定。`;
document.getElementById('verdict').innerHTML = v;
document.getElementById('sum').innerHTML =
`<div class="tile"><b>${answered ? Math.round(yes.length / answered * 100) : 0}%</b><span>答过的题里做到的比例</span></div>` +
`<div class="tile"><b>${no.length}</b><span>还没做到</span></div>` +
`<div class="tile r3"><b>${good.length}</b><span>其中性价比高或极高</span></div>` +
`<div class="tile r2"><b>${easy.length}</b><span>其中一次办完就行</span></div>`;
// 先做这三件:性价比最高的排前;同档里先挑不用改习惯的
const top = no.slice(0, 3);
const alsoOf = q => (q.also || []).map(a => find(a.sec, a.key)).filter(Boolean);
let h = '';
if (top.length) h += `<h2>先做这${top.length === 3 ? '三' : top.length === 2 ? '两' : '一'}件<small>没做到的里性价比最高的</small></h2><ol class="top3">`
+ top.map(q => `<li><a href="#c-${q.entry.sec}-${q.entry.n}">${esc(plain(q.entry.title))}</a> <span class="why">性价比${q.entry.ratio},证据 ${q.entry.grade} 级,${PILES.find(p => p.will === q.entry.will)?.name ?? ''}</span></li>`).join('') + '</ol>';
for (const p of PILES){
const list = no.filter(q => q.entry.will === p.will);
if (!list.length) continue;
h += `<h2>${p.name}(${list.length} 项)<small>${p.sub}</small></h2>` + list.map(q => card(q.entry, alsoOf(q))).join('');
}
document.getElementById('todo').innerHTML = h;
document.getElementById('done').innerHTML = yes.map(q =>
`<li><a href="./#e-${q.entry.sec}-${q.entry.n}">${esc(plain(q.entry.title))}</a></li>`).join('');
document.getElementById('done-h').hidden = !yes.length;
}
function show(which){
document.getElementById('ask').hidden = which !== 'ask';
document.getElementById('out').hidden = which !== 'out';
scrollTo(0, 0);
}
document.getElementById('qs').addEventListener('click', ev => {
const mb = ev.target.closest('button[data-mod]');
if (mb) { ans['mod:' + mb.dataset.mod] = modOn(mb.dataset.mod) ? 'off' : undefined; save(); renderAsk(); return; }
const b = ev.target.closest('button[data-k]'); if (!b) return;
ans[b.dataset.k] = ans[b.dataset.k] === b.dataset.v ? undefined : b.dataset.v;
save(); renderAsk();
});
document.getElementById('go').addEventListener('click', () => {
if (!profileDone()) { document.getElementById('prog').textContent = '先把基本情况三题填上'; return; }
renderOut(); show('out');
});
document.getElementById('back').addEventListener('click', () => show('ask'));
document.getElementById('reset').addEventListener('click', () => { ans = {}; save(); renderAsk(); });
(async function init(){
const status = document.getElementById('status');
try {
const EMBED = window.__CORPUS__;
const readme = EMBED ? EMBED.readme : await (await fetch('README.md', {cache:'no-cache'})).text();
const files = [...new Set(Array.from(readme.matchAll(/\]\((book\/[^)]+\.md)\)/g), m => m[1]))]
.filter(f => GROUPS.some(g => g.qs.some(q => f.startsWith(`book/${String(q.sec).padStart(2,'0')}-`))));
const parts = await Promise.all(files.map(async f => EMBED ? EMBED.parts[f] : (await fetch(f, {cache:'no-cache'})).text()));
ENTRIES = parse(parts.join('\n\n'));
for (const g of GROUPS) for (const q of g.qs) { q.g = g; q.entry = find(q.sec, q.key); }
status.remove();
renderAsk(); show('ask');
} catch (err) {
status.textContent = location.protocol === 'file:'
? '这个页面要通过 http 打开才能读取正文。'
: `读取正文失败:${err.message}`;
}
})();
</script>
</body>
</html>