Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
783 lines (764 loc) · 52 KB
/
Copy pathindex.html
File metadata and controls
783 lines (764 loc) · 52 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
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
<!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="按性价比排序的人生指南:498 条建议,覆盖长寿防病、意外急救、省钱理财、防骗与法律红线、失业兜底、创业风险、恋爱婚育、出国与技能。每条写明成本、收益、证据等级和原始出处,可按钱、时间、毅力三个成本维度检索。">
<meta name="keywords" content="长寿,健康,循证,总死亡率,省钱,理财,防骗,急救,法律常识,失业救助,劳动仲裁,创业风险,性价比">
<meta name="author" content="高性价比人生指南">
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1">
<meta name="theme-color" content="#3451b2" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1b1b1f" media="(prefers-color-scheme: dark)">
<link rel="canonical" href="https://eternity4719.github.io/HowToLiveBetter/">
<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">
<meta property="og:type" content="website">
<meta property="og:site_name" content="高性价比人生指南">
<meta property="og:locale" content="zh_CN">
<meta property="og:url" content="https://eternity4719.github.io/HowToLiveBetter/">
<meta property="og:title" content="高性价比人生指南">
<meta property="og:description" content="按性价比排序的人生指南:498 条建议,覆盖长寿防病、意外急救、省钱理财、防骗与法律红线、失业兜底、创业风险、恋爱婚育、出国与技能。每条写明成本、收益、证据等级和原始出处,可按钱、时间、毅力三个成本维度检索。">
<meta property="og:image" content="https://eternity4719.github.io/HowToLiveBetter/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="高性价比人生指南">
<meta name="twitter:description" content="按性价比排序的人生指南:498 条建议,覆盖长寿防病、意外急救、省钱理财、防骗与法律红线、失业兜底、创业风险、恋爱婚育、出国与技能。每条写明成本、收益、证据等级和原始出处,可按钱、时间、毅力三个成本维度检索。">
<meta name="twitter:image" content="https://eternity4719.github.io/HowToLiveBetter/og.png">
<script type="application/ld+json">
{"@context":"https://schema.org","@graph":[
{"@type":"WebSite","@id":"https://eternity4719.github.io/HowToLiveBetter/#website","url":"https://eternity4719.github.io/HowToLiveBetter/","name":"高性价比人生指南","description":"按性价比排序的人生指南:498 条建议,覆盖长寿防病、意外急救、省钱理财、防骗与法律红线、失业兜底、创业风险、恋爱婚育、出国与技能。每条写明成本、收益、证据等级和原始出处,可按钱、时间、毅力三个成本维度检索。","inLanguage":"zh-CN","potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://eternity4719.github.io/HowToLiveBetter/?q={search_term_string}"},"query-input":"required name=search_term_string"}},
{"@type":"Book","@id":"https://eternity4719.github.io/HowToLiveBetter/#book","name":"高性价比人生指南","url":"https://eternity4719.github.io/HowToLiveBetter/","inLanguage":"zh-CN","bookFormat":"https://schema.org/EBook","numberOfPages":498,"license":"https://unlicense.org/","abstract":"按性价比排序的人生指南:498 条建议,覆盖长寿防病、意外急救、省钱理财、防骗与法律红线、失业兜底、创业风险、恋爱婚育、出国与技能。每条写明成本、收益、证据等级和原始出处,可按钱、时间、毅力三个成本维度检索。","about":["健康","长寿","个人理财","急救","法律常识","消费者保护"],"isAccessibleForFree":true}
]}
</script>
<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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#ffffff; --bg-alt:#f6f6f7; --bg-soft:#f6f6f7; --bg-mute:#f1f1f2; --bg-elv:#ffffff;
--divider:#e2e2e3; --gutter:#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-3:#5672cd; --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);
--mark:rgba(234,179,8,.32);
--shadow:0 1px 2px rgba(0,0,0,.04),0 1px 2px rgba(0,0,0,.06);
--font:"Inter",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;
--mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
--nav-h:64px; --side-w:272px;
color-scheme:light;
}
html.dark{
--bg:#1b1b1f; --bg-alt:#161618; --bg-soft:#202127; --bg-mute:#2b2b2f; --bg-elv:#202127;
--divider:#2e2e32; --gutter:#000000;
--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-3:#3e63dd; --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);
--mark:rgba(234,179,8,.28);
--shadow:0 1px 2px rgba(0,0,0,.3);
color-scheme:dark;
}
*{box-sizing:border-box}
html{scroll-padding-top:calc(var(--nav-h) + 16px)}
body{margin:0;background:var(--bg);color:var(--t1);font:14px/1.7 var(--font);-webkit-font-smoothing:antialiased}
a{color:var(--brand-1);text-decoration:none;transition:color .25s}
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}
mark{background:var(--mark);color:inherit;border-radius:2px;padding:0 1px}
[hidden]{display:none!important}
/* nav */
.nav{position:fixed;top:0;left:0;right:0;height:var(--nav-h);z-index:30;background:var(--bg);border-bottom:1px solid var(--divider)}
.nav-in{height:100%;display:flex;align-items:center;gap:16px;padding:0 24px}
.title{display:flex;align-items:center;gap:10px;font-weight:600;font-size:16px;color:var(--t1);white-space:nowrap}
.title:hover{text-decoration:none;color:var(--t1)}
.logo{width:24px;height:24px;border-radius:6px;background:linear-gradient(135deg,var(--brand-3),var(--brand-1));display:grid;place-items:center;color:#fff;font-size:12px;font-weight:700}
.logo svg{width:15px;height:15px}
.search{flex:1;max-width:520px;margin-left:auto;position:relative}
.search input{width:100%;height:40px;padding:0 44px 0 40px;border-radius:8px;border:1px solid transparent;background:var(--bg-alt);color:var(--t1);font:inherit;font-size:14px;transition:border-color .25s,background .25s}
.search input:hover{border-color:var(--divider)}
.search input:focus{outline:0;border-color:var(--brand-1);background:var(--bg)}
.search input::placeholder{color:var(--t3)}
.search svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);width:18px;height:18px;fill:none;stroke:var(--t3);stroke-width:2;pointer-events:none}
.search kbd{position:absolute;right:10px;top:50%;transform:translateY(-50%);font:500 11px/1 var(--font);color:var(--t3);border:1px solid var(--divider);border-radius:4px;padding:3px 6px;background:var(--bg-elv);pointer-events:none}
.nav-r{display:flex;align-items:center;gap:8px}
.icon-btn{width:36px;height:36px;border-radius:8px;display:grid;place-items:center;color:var(--t2);transition:background .25s,color .25s}
.icon-btn:hover{background:var(--bg-soft);color:var(--t1)}
.icon-btn svg{width:20px;height:20px;fill:currentColor}
.switch{position:relative;width:40px;height:22px;border-radius:11px;border:1px solid var(--divider);background:var(--bg-mute);transition:border-color .25s,background .25s;flex:none;margin-left:8px}
.switch:hover{border-color:var(--t3)}
.switch .knob{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background:var(--bg-elv);box-shadow:var(--shadow);transition:transform .25s;display:grid;place-items:center}
html.dark .switch .knob{transform:translateX(18px)}
.switch svg{width:12px;height:12px;fill:var(--t2)}
.switch .sun{display:block} .switch .moon{display:none}
html.dark .switch .sun{display:none} html.dark .switch .moon{display:block}
.menu-btn{display:none}
/* sidebar */
.sidebar{position:fixed;top:var(--nav-h);bottom:0;left:0;width:var(--side-w);background:var(--bg-alt);padding:32px 32px 96px;overflow-y:auto;overscroll-behavior:contain;z-index:20}
.group{padding-bottom:20px;margin-bottom:20px;border-bottom:1px solid var(--divider)}
.group:last-of-type{border-bottom:0}
.gt{font-size:14px;font-weight:700;color:var(--t1);line-height:24px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline}
.gt small{font-weight:400;font-size:12px;color:var(--t3)}
.sec-links{display:flex;flex-direction:column}
.sec-link{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:4px 0;font-size:14px;font-weight:500;color:var(--t2);line-height:24px;text-align:left;transition:color .25s;border-radius:0}
.sec-link:hover{color:var(--t1)}
.sec-link[aria-pressed="true"]{color:var(--brand-1)}
.sec-link i{font-style:normal;font-size:12px;color:var(--t3);font-variant-numeric:tabular-nums}
.sec-link.all{padding-bottom:8px;margin-bottom:4px;border-bottom:1px solid var(--divider)}
.chips{display:flex;flex-wrap:wrap;gap:6px;padding-top:6px}
.chip{border:1px solid var(--divider);background:var(--bg-elv);color:var(--t2);border-radius:999px;padding:3px 11px;font-size:12px;font-weight:500;line-height:18px;transition:all .2s}
.chip:hover{border-color:var(--brand-3);color:var(--t1)}
.chip[aria-pressed="true"]{background:var(--brand-soft);border-color:var(--brand-1);color:var(--brand-1)}
.toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--t2);padding:4px 0;cursor:pointer;font-weight:500}
.toggle input{accent-color:var(--brand-1);width:15px;height:15px;margin:0}
.reset{display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:6px 14px;border:1px solid var(--divider);border-radius:20px;font-size:13px;font-weight:500;color:var(--t2);background:var(--bg-elv);transition:all .25s}
.reset:hover{border-color:var(--brand-1);color:var(--brand-1)}
.doc-links{margin-top:8px;font-size:14px;color:var(--t3)}
.doc-links a{margin-right:4px}
.hint{font-size:12px;color:var(--t3);line-height:1.6;margin-top:16px}
.hint p{margin:4px 0}
.backdrop{display:none}
/* content */
.content{padding:calc(var(--nav-h) + 40px) 48px 128px calc(var(--side-w) + 48px)}
.doc{max-width:900px;margin:0 auto}
.doc-head{margin-bottom:28px}
.doc-head h1{font-size:32px;line-height:40px;font-weight:700;letter-spacing:-.02em;margin:0 0 8px}
.doc-head p{margin:0;color:var(--t2);font-size:16px}
.doc-head .stat{color:var(--t3);font-size:14px;margin-top:6px}
.doc-head .stat b{color:var(--brand-1);font-weight:600}
.sec-block{margin-top:40px}
.sec-h{display:flex;align-items:baseline;gap:12px;padding-top:24px;border-top:1px solid var(--divider);margin-bottom:8px}
.sec-h h2{font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.02em;margin:0}
.sec-h .shown{margin-left:auto;font-size:13px;color:var(--t3);font-variant-numeric:tabular-nums}
.intro{margin:0 0 20px;color:var(--t2);font-size:14px;max-width:72ch}
.card{background:var(--bg-soft);border:1px solid transparent;border-radius:12px;padding:22px 24px 18px;margin-bottom:16px;transition:border-color .25s,background .25s;content-visibility:auto;contain-intrinsic-size:300px;contain-intrinsic-size:auto 300px}
.card:hover{border-color:var(--divider)}
.card-h{display:flex;align-items:flex-start;gap:10px;margin-bottom:10px}
.card-h h3{margin:0;font-size:18px;line-height:28px;font-weight:600;letter-spacing:-.01em;flex:1;min-width:0;overflow-wrap:anywhere}
.card-h .idx{color:var(--t3);font-size:14px;line-height:28px;font-variant-numeric:tabular-nums;min-width:22px}
.card-h .anchor{opacity:0;color:var(--brand-1);font-size:16px;line-height:28px;font-weight:500;transition:opacity .2s}
.card:hover .anchor,.anchor:focus-visible{opacity:1}
.badges{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
.badge{display:inline-block;border-radius:10px;padding:0 10px;line-height:20px;font-size:12px;font-weight:600;white-space:nowrap;transform:translateY(0)}
.badge.tip{background:var(--brand-soft);color:var(--brand-1)}
.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.danger{background:var(--red-soft);color:var(--red-1)}
.badge.warn{background:var(--yellow-soft);color:var(--yellow-1)}
.badge.plain{background:var(--gray-soft);color:var(--t2);font-weight:500}
.badge.r3{background:var(--brand-soft);color:var(--brand-1)}
.badge.r2{background:var(--green-soft);color:var(--green-1)}
.badge.r1{background:var(--gray-soft);color:var(--gray-1)}
.human{margin:0 0 14px;padding:10px 14px;border-left:2px solid var(--brand-1);background:var(--brand-soft);border-radius:0 6px 6px 0;font-size:15px;line-height:1.75;color:var(--t1);overflow-wrap:anywhere}
.rows{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:14px;line-height:1.75}
.rows .k{color:var(--t3);font-weight:500;font-size:13px;line-height:1.75;padding-top:1px}
.rows .v{color:var(--t1);min-width:0;overflow-wrap:anywhere}
.rows .v.note{color:var(--t2)}
.src{margin-top:14px;border-top:1px solid var(--divider);padding-top:10px}
.src summary{list-style:none;cursor:pointer;font-size:13px;font-weight:500;color:var(--t2);display:flex;align-items:center;gap:6px;user-select:none}
.src summary::-webkit-details-marker{display:none}
.src summary::before{content:"";width:6px;height:6px;border-right:1.5px solid var(--t3);border-bottom:1.5px solid var(--t3);transform:rotate(-45deg);transition:transform .2s;margin-right:2px}
.src[open] summary::before{transform:rotate(45deg)}
.src .body{padding-top:8px;font-size:13px;line-height:1.75;color:var(--t2);overflow-wrap:anywhere}
abbr.term{text-decoration:none;border-bottom:1px dotted var(--t3);cursor:help}
abbr.term:hover{border-bottom-color:var(--brand-1);color:var(--brand-1)}
.pop{position:absolute;z-index:40;background:var(--bg-elv);border:1px solid var(--divider);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.14);padding:10px 12px;font-size:13px;line-height:1.6;color:var(--t2)}
.pop b{display:block;color:var(--t1);font-weight:600;margin-bottom:2px}
.gloss{margin-top:16px;border:1px solid var(--divider);border-radius:8px;padding:0 16px;font-size:13px;color:var(--t2)}
.gloss summary{cursor:pointer;padding:10px 0;font-weight:500;color:var(--t2);list-style:none;display:flex;align-items:center;gap:6px}
.gloss summary::-webkit-details-marker{display:none}
.gloss summary::before{content:"";width:6px;height:6px;border-right:1.5px solid var(--t3);border-bottom:1.5px solid var(--t3);transform:rotate(-45deg);transition:transform .2s;margin-right:2px}
.gloss[open] summary::before{transform:rotate(45deg)}
.gloss p{margin:0 0 8px}
.gloss dl{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0 0 14px}
.gloss dt{font-weight:600;color:var(--t1);white-space:nowrap}
.gloss dd{margin:0}
@media (max-width:960px){.gloss dl{grid-template-columns:1fr;gap:2px} .gloss dd{margin-bottom:8px}}
.empty{border:1px dashed var(--divider);border-radius:12px;padding:40px 24px;text-align:center;color:var(--t2);margin-top:24px}
.empty button{margin-top:12px;padding:7px 16px;border:1px solid var(--divider);border-radius:20px;font-weight:500;background:var(--bg-elv)}
.empty button:hover{border-color:var(--brand-1);color:var(--brand-1)}
.status{color:var(--t3);padding:40px 0;text-align:center}
.status code{font-family:var(--mono);font-size:13px;background:var(--bg-mute);padding:2px 6px;border-radius:4px}
.foot{margin-top:64px;padding-top:24px;border-top:1px solid var(--divider);color:var(--t3);font-size:13px;line-height:1.7}
@media (max-width:960px){
:root{--nav-h:56px}
.nav-in{padding:0 16px;gap:10px}
.title span{display:none}
.search{max-width:none}
.search kbd{display:none}
.menu-btn{display:grid}
.sidebar{transform:translateX(-100%);transition:transform .25s;box-shadow:none;padding:24px 24px 96px;width:min(var(--side-w),86vw)}
.sidebar.open{transform:none;box-shadow:0 8px 24px rgba(0,0,0,.2)}
.backdrop{display:block;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:15;opacity:0;pointer-events:none;transition:opacity .25s}
.backdrop.on{opacity:1;pointer-events:auto}
.content{padding:calc(var(--nav-h) + 24px) 20px 96px}
.doc-head h1{font-size:26px;line-height:34px}
.card{padding:18px 16px 14px;border-radius:10px}
.card-h h3{font-size:16px;line-height:24px}
.card-h .idx{line-height:24px}
.human{padding:9px 12px;font-size:14px;margin-bottom:12px}
.rows{grid-template-columns:1fr;gap:4px}
.rows .k{padding-top:6px}
.rows .k:first-child{padding-top:0}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<noscript>
<div style="max-width:760px;margin:0 auto;padding:32px 20px;line-height:1.7">
<h1>高性价比人生指南</h1>
<p>用最少的钱、时间、精力和毅力,换回最多的寿命、金钱、时间和人身自由。全书 31 节 498 条,每条写明成本、收益、证据等级和原始出处。收益按受益人分四档算:你自己、配偶与直系亲属、朋友同事、陌生人,档次越低回报期望越小、风险越要写清。</p>
<p>本页的筛选功能需要 JavaScript。正文可直接阅读:<a href="README.md">README.md</a> 里的目录,正文在 <a href="book/">book/</a>。</p>
<ol>
<li>不要早死:交通、火灾、燃气、毒蘑菇、溺水、跌落、疫苗、癌症筛查、心理危机、家庭应急装备</li>
<li>不要慢慢死:烟酒、运动、睡眠、饮食、久坐</li>
<li>不要浪费精力:睡眠、打断、多任务、决策疲劳、人际负债</li>
<li>不要浪费时间:沉没成本、拖延、会议、通勤</li>
<li>不要浪费钱:订阅、彩票、利息、保险、基金费率、公积金提取、车险、预付款、直播带货、医保个人账户</li>
<li>反面清单:看起来性价比高但其实不高的东西</li>
<li>没钱的时候怎么活:失业保险、劳动仲裁、救助站、低保、医保、求职</li>
<li>别把自己搭进去:交通事故、被骗止付、彩礼、婚前财产、担保、反诈、被执行与失信名单、养犬责任</li>
<li>普通人容易踩的法律红线:谣言、兼职洗钱、伪造材料骗贷、高空抛物、偷拍、赌博</li>
<li>恋爱和结婚划不划算:择偶策略、纠缠的红线、异地恋、登记流程、婚检、健康账、钱账、退出成本</li>
<li>程序员和技术人容易踩的红线:外挂、爬虫、删库、带走源码、竞业</li>
<li>创业与做生意:本钱、担保、主体选择、注册登记、许可证、纳税申报与零申报、涉税诈骗、用人、退场</li>
<li>紧急情况:心脏骤停、大出血、过敏性休克、癫痫、低血糖、触电、一氧化碳、化学品灼伤、火灾、溺水、迷路、失温、蛇咬、地震、雷击、高原病</li>
<li>账号与信息安全:二次验证、密码、SIM 卡、手机丢失、App 权限、查阅与删除权</li>
<li>租房与买房:押金、腾退、中介代收、资金监管、买卖不破租赁、交易资金专户、隔断房</li>
<li>得了慢性病之后怎么活:服药依从、门诊慢特病跨省结算、复查、长期处方、并发症筛查</li>
<li>家里有老人:意定监护、遗嘱形式、账户与话术、长期护理保险</li>
<li>养孩子划不划算:育儿补贴、产假与生育津贴、时间账、钱账</li>
<li>在职、离职和工伤:加班费、年休假、试用期;职业病危害告知与体检、粉尘噪声防护;N、代通知金、2N、留证;工伤认定时限、劳动能力鉴定、工亡待遇</li>
<li>刚出生的孩子怎么带:安全睡眠、乙肝首针、母乳与辅食、冲奶水温、发热就医红线</li>
<li>出国、旅行与境外安全:安全提醒级别、12308、领事保护的边界、境外医疗保险、境外高薪招聘骗局、境外驾照</li>
<li>怎么放松:娱乐场所和减压:安全出口、明码标价、涉毒红线、别人递的东西、网吧实名;运动、正念、呼吸、社交、绿地</li>
<li>学什么技能划算:教育回报率、山寨证书、培训补贴、抗自动化的维度、技能等级</li>
<li>看病:怎么少花钱少走弯路:分级诊疗与转诊、起付线连续计算、报销比例差、预留号源、异地就医、病历留存</li>
<li>人走了以后要办什么:殡葬基础项目清单、价格违法、中介备案、公积金余额与社保待遇、死者个人信息权利</li>
<li>做一个网站或平台:支付结算红线、ICP 许可与备案、直播与视听资质、平台核验与涉税报送、内容治理、实名、未成年人、通知删除、数据出境、服务器选型</li>
</ol>
</div>
</noscript>
<header class="nav">
<div class="nav-in">
<button class="icon-btn menu-btn" id="menu" aria-label="打开筛选" aria-expanded="false"><svg viewBox="0 0 24 24"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg></button>
<a class="title" href="./"><span class="logo" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M6 12.5l4 4 8-9.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>高性价比人生指南</span></a>
<label class="search">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5" stroke-linecap="round"/></svg>
<input id="q" type="search" placeholder="搜索条目" autocomplete="off" aria-label="搜索条目">
<kbd>/</kbd>
</label>
<div class="nav-r">
<a class="icon-btn" href="README.md" title="查看 README.md" aria-label="README"><svg viewBox="0 0 24 24"><path d="M6 2h9l5 5v15H6zm8 1.5V8h4.5zM8 12h8v1.5H8zm0 3.5h8V17H8z"/></svg></a>
<a class="icon-btn" href="https://github.com/eternity4719/HowToLiveBetter" target="_blank" rel="noopener" title="在 GitHub 上查看源仓库" aria-label="GitHub 仓库"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0C17.3 5.6 18.3 6 18.3 6c.6 1.7.2 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"/></svg></a>
<button class="switch" id="theme" aria-label="切换深色模式" title="切换深色模式"><span class="knob"><svg class="sun" viewBox="0 0 24 24"><path d="M12 4a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 13a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1zm8-6a1 1 0 1 1 0 2h-1a1 1 0 1 1 0-2zM5 11a1 1 0 1 1 0 2H4a1 1 0 1 1 0-2zm12.07-5.07a1 1 0 0 1 0 1.41l-.7.71a1 1 0 1 1-1.42-1.41l.71-.71a1 1 0 0 1 1.41 0zM8.05 16.95a1 1 0 0 1 0 1.41l-.7.71a1 1 0 1 1-1.42-1.41l.71-.71a1 1 0 0 1 1.41 0zm9.02 0l.7.71a1 1 0 1 1-1.41 1.41l-.71-.7a1 1 0 0 1 1.42-1.42zM6.34 5.93l.71.71a1 1 0 0 1-1.41 1.41l-.71-.7a1 1 0 0 1 1.41-1.42zM12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8z"/></svg><svg class="moon" viewBox="0 0 24 24"><path d="M12.1 22c-5.5 0-10-4.5-10-10 0-4.8 3.4-8.9 8-9.8.5-.1.9.4.7.9A8 8 0 0 0 20.9 13c.5-.2 1 .2.9.7-.9 4.7-5 8.3-9.7 8.3z"/></svg></span></button>
</div>
</div>
</header>
<div class="backdrop" id="backdrop"></div>
<div class="pop" id="pop" role="tooltip" hidden><b></b><span></span></div>
<aside class="sidebar" id="sidebar">
<div class="group">
<div class="gt">章节</div>
<div class="sec-links" id="f-sec" data-dim="sec"></div>
</div>
<div class="group">
<div class="gt">性价比 <small>作者判断,同口径内可比</small></div>
<div class="chips" data-dim="ratio">
<button class="chip" data-v="极高" aria-pressed="false">极高</button>
<button class="chip" data-v="高" aria-pressed="false">高</button>
<button class="chip" data-v="一般" aria-pressed="false">一般</button>
</div>
</div>
<div class="group">
<div class="gt">换回什么 <small>口径,不跨口径比较</small></div>
<div class="chips" data-dim="lens">
<button class="chip" data-v="死亡率" aria-pressed="false">寿命</button>
<button class="chip" data-v="金钱" aria-pressed="false">钱</button>
<button class="chip" data-v="时间" aria-pressed="false">时间精力</button>
<button class="chip" data-v="自由" aria-pressed="false">人身自由</button>
</div>
</div>
<div class="group">
<div class="gt">证据等级 <small>荟萃/RCT · 有研究 · 共识</small></div>
<div class="chips" data-dim="grade">
<button class="chip" data-v="A" aria-pressed="false">A 级</button>
<button class="chip" data-v="B" aria-pressed="false">B 级</button>
<button class="chip" data-v="C" aria-pressed="false">C 级</button>
</div>
</div>
<div class="group">
<div class="gt">花钱</div>
<div class="chips" data-dim="money">
<button class="chip" data-v="0" aria-pressed="false">不花钱</button>
<button class="chip" data-v="少" aria-pressed="false">少</button>
<button class="chip" data-v="多" aria-pressed="false">多</button>
</div>
</div>
<div class="group">
<div class="gt">花时间</div>
<div class="chips" data-dim="time">
<button class="chip" data-v="少" aria-pressed="false">顺手</button>
<button class="chip" data-v="中" aria-pressed="false">几小时</button>
<button class="chip" data-v="多" aria-pressed="false">每天占用</button>
</div>
</div>
<div class="group">
<div class="gt">要毅力</div>
<div class="chips" data-dim="will">
<button class="chip" data-v="否" aria-pressed="false">不用</button>
<button class="chip" data-v="些" aria-pressed="false">一点</button>
<button class="chip" data-v="是" aria-pressed="false">很多</button>
</div>
</div>
<div class="group">
<label class="toggle"><input type="checkbox" id="f-dispute"> 只看标了争议的</label>
<label class="toggle"><input type="checkbox" id="f-todo"> 只看有待核实的</label>
<button class="reset" id="reset">清空筛选</button>
</div>
<div class="hint">
<p>章节一次只看一个,成本和等级可多选。同一组多选是「或」,不同组之间是「且」。</p>
<p>每节内按性价比从高到低排,检索不改变顺序。</p>
<p>正文与标签来自仓库 <a href="book/">book/</a> 下的 30 个文件,改正文即改这里。</p>
</div>
</aside>
<main class="content">
<div class="doc">
<div class="doc-head">
<h1>高性价比人生指南</h1>
<p>每一条都回答两个问题:花掉什么,换回什么。</p>
<p>换回的算在谁头上也分档:你自己最高,其次配偶和直系亲属,再次朋友同事,陌生人最低——最低不等于零,只是回报的指望小、要连风险一起看。</p>
<div class="stat" aria-live="polite">当前显示 <b id="cnt">–</b> 条,共 <span id="tot">–</span> 条</div>
<div class="doc-links">长文:<a href="https://github.com/eternity4719/HowToLiveBetter/blob/main/docs/%E9%81%87%E5%88%B0%E9%99%8C%E7%94%9F%E4%BA%BA%E5%87%BA%E4%BA%8B%E8%AF%A5%E4%B8%8D%E8%AF%A5%E5%81%9C.md">遇到陌生人出事该不该停</a> · <a href="https://github.com/eternity4719/HowToLiveBetter/blob/main/docs/%E5%AE%B6%E5%BA%AD%E5%BA%94%E6%80%A5%E8%A3%85%E5%A4%87%E6%B8%85%E5%8D%95.md">家庭应急装备清单</a> · <a href="https://github.com/eternity4719/HowToLiveBetter/blob/main/docs/%E7%BB%93%E5%A9%9A%E5%88%92%E4%B8%8D%E5%88%92%E7%AE%97.md">结婚划不划算</a> · <a href="https://github.com/eternity4719/HowToLiveBetter/blob/main/docs/%E5%81%9A%E5%B9%B3%E5%8F%B0%E8%A6%81%E5%8A%9E%E5%93%AA%E4%BA%9B%E8%AF%81.md">做平台要办哪些证</a> · <a href="https://github.com/eternity4719/HowToLiveBetter/tree/main/docs/%E6%A0%B8%E5%AE%9E%E8%AE%B0%E5%BD%95">来源核实记录</a></div>
<details class="gloss" id="gloss"><summary>看不懂的缩写和名词</summary><p>正文里带虚线的词,点一下或把鼠标放上去就有解释。全表如下。</p><dl></dl></details>
</div>
<div id="list">
<div class="status" id="status">正在读取正文 …</div>
<div class="empty" id="empty" hidden>
没有匹配的条目。去掉一个筛选条件,或换个更短的关键词。
<div><button id="reset2">清空筛选</button></div>
</div>
</div>
<div class="foot">数字口径以条目内标注为准:第 1、2 节是 ACM 或特定死因,第 3、4 节是精力/时间,第 5 节是金钱,第 7 节是金钱与保障,第 8、9 节是金钱与人身自由,互不换算。来源核实过程见 <a href="https://github.com/eternity4719/HowToLiveBetter/tree/main/docs/%E6%A0%B8%E5%AE%9E%E8%AE%B0%E5%BD%95">docs/核实记录</a>。Unlicense,公有领域。</div>
</div>
</main>
<script>
/* ---------- 调试面板:地址后加 ?perf=1 打开,实测各步耗时 ---------- */
const PERF = (function(){
const off = { on:false, log(){}, mark(){} };
if (!/[?&]perf=1/.test(location.search)) return off;
const box = document.createElement('div');
box.style.cssText = 'position:fixed;right:10px;bottom:10px;z-index:99;width:300px;max-height:50vh;overflow:auto;background:rgba(0,0,0,.85);color:#8fe;font:12px/1.5 ui-monospace,Consolas,monospace;padding:8px 10px;border-radius:8px;white-space:pre-wrap;pointer-events:auto';
const rows = [];
const put = (s) => { rows.unshift(s); if (rows.length > 60) rows.pop(); box.textContent = rows.join('\n'); };
const api = {
on: true,
log(name, t0, extra){ const ms = performance.now() - t0; put(`${ms >= 16 ? '⚠' : '·'} ${name} ${ms.toFixed(1)}ms${extra ? ' ' + extra : ''}`); },
mark(s){ put('· ' + s); }
};
addEventListener('DOMContentLoaded', () => document.body.appendChild(box), {once:true});
if (document.readyState !== 'loading') document.body.appendChild(box);
// 展开/收起 <details> 的重排代价:强制同步布局来量
document.addEventListener('toggle', ev => {
if (!ev.target.matches('details')) return;
const t0 = performance.now();
void document.documentElement.scrollHeight; // 强制走一遍布局
api.log((ev.target.open ? '展开' : '收起') + '重排', t0);
}, true);
// 掉帧:连续两帧间隔超过 32ms 就记一条
let last = performance.now(), worst = 0, since = last;
const tick = (t) => { const d = t - last; last = t; if (d > worst) worst = d;
if (t - since > 1000){ if (worst > 32) put(`⚠ 最长帧 ${worst.toFixed(0)}ms`); worst = 0; since = t; }
requestAnimationFrame(tick); };
requestAnimationFrame(tick);
try { new PerformanceObserver(l => { for (const e of l.getEntries()) put(`⚠ 长任务 ${e.duration.toFixed(0)}ms`); })
.observe({entryTypes:['longtask']}); } catch (e) {}
return api;
})();
/* ---------- 解析 README ---------- */
function parseReadme(md){
const lines = md.split(/\r?\n/);
const sections = [];
let sec = null, entry = null;
const flush = () => { if (entry && sec) { sec.entries.push(entry); } entry = null; };
for (const raw of lines){
const line = raw.trimEnd();
let m;
if ((m = /^#{1,2} (\d+)\. (.+)$/.exec(line))) { flush(); sec = {n:m[1], title:m[2].trim(), intro:'', entries:[]}; sections.push(sec); continue; }
if (/^#{1,2} /.test(line)) { flush(); sec = null; continue; }
if (!sec) continue;
if ((m = /^### (\d+)\. (.+)$/.exec(line))) { flush(); entry = {sec:sec.n, n:m[1], title:m[2].trim(), cost:'', human:'', gain:'', grade:'', src:'', note:'', money:'', time:'', will:'', level:'', lens:''}; continue; }
if ((m = /^<!--\s*成本标签:\s*(.*?)\s*-->/.exec(line)) && entry){
for (const kv of m[1].split(/\s+/)){ const [k,v] = kv.split('='); if(k==='钱') entry.money=v; if(k==='时间') entry.time=v; if(k==='毅力') entry.will=v; if(k==='收益') entry.level=v; if(k==='口径') entry.lens=v; }
continue;
}
if (entry){
if ((m = /^- 成本:(.*)$/.exec(line))) entry.cost = m[1];
else if ((m = /^- 说人话:(.*)$/.exec(line))) entry.human = m[1];
else if ((m = /^- 收益:(.*)$/.exec(line))) entry.gain = m[1];
else if ((m = /^- 证据等级:\s*([ABC])/.exec(line))) entry.grade = m[1];
else if ((m = /^- 来源:(.*)$/.exec(line))) entry.src = m[1];
else if ((m = /^- 备注:(.*)$/.exec(line))) entry.note = m[1];
continue;
}
if (line && !sec.intro) sec.intro = line;
}
flush();
for (const s of sections) for (const e of s.entries){
e.dispute = /^争议/.test(e.note);
e.todo = /待核实|TODO/.test(e.src + e.gain + e.note + e.cost);
e.cs = (COST_W.money[e.money] ?? 0) + (COST_W.time[e.time] ?? 0) + (COST_W.will[e.will] ?? 0);
e.ratio = e.level === '大' ? (e.cs === 0 ? '极高' : (e.cs <= 2 ? '高' : '一般'))
: e.level === '中' ? (e.cs === 0 ? '高' : '一般') : '一般';
e.hay = [e.title, e.human, e.cost, e.gain, e.note, e.src, e.grade].join('\n').toLowerCase();
}
return sections;
}
/* ---------- 术语表 ---------- */
let GLOSS = []; // [{term, meaning, re}]
function parseGlossary(md){
const m = /^## 读懂数字[^\n]*\n([\s\S]*?)(?=^## )/m.exec(md);
if (!m) return [];
const rows = [];
for (const line of m[1].split(/\r?\n/)){
const c = /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/.exec(line);
if (!c || c[1] === '术语' || /^-+$/.test(c[1])) continue;
for (const t of c[1].split('、')){
const term = t.trim(); if (!term) continue;
rows.push({term, meaning: c[2], latin: /^[A-Za-z0-9%. ]+$/.test(term)});
}
}
rows.sort((a,b) => b.term.length - a.term.length);
// 合并成一条正则:长词在前保证最长匹配,拉丁术语要求前后不是字母数字
const escRe = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const alt = (list) => list.map(g => escRe(g.term)).join('|');
const parts = [];
const latin = rows.filter(g => g.latin), cjk = rows.filter(g => !g.latin);
if (latin.length) parts.push(`(?<![A-Za-z0-9])(?:${alt(latin)})(?![A-Za-z0-9])`);
if (cjk.length) parts.push(`(?:${alt(cjk)})`);
GLOSS_RE = new RegExp(parts.join('|'), 'g');
GLOSS_BY = Object.fromEntries(rows.map(g => [g.term, g]));
return rows;
}
let GLOSS_RE = null, GLOSS_BY = {};
function termify(root){
if (!GLOSS_RE) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes = []; let n; while ((n = walker.nextNode())) if (!n.parentElement.closest('a,abbr')) nodes.push(n);
for (const node of nodes){
const s = node.nodeValue;
GLOSS_RE.lastIndex = 0;
if (!GLOSS_RE.test(s)) continue;
GLOSS_RE.lastIndex = 0;
const frag = document.createDocumentFragment(); let pos = 0, m;
while ((m = GLOSS_RE.exec(s))){
const g = GLOSS_BY[m[0]]; if (!g) continue;
if (m.index > pos) frag.appendChild(document.createTextNode(s.slice(pos, m.index)));
const ab = document.createElement('abbr'); ab.className = 'term'; ab.textContent = m[0]; ab.title = g.meaning; ab.tabIndex = 0; ab.dataset.term = g.term;
frag.appendChild(ab); pos = m.index + m[0].length;
}
if (pos < s.length) frag.appendChild(document.createTextNode(s.slice(pos)));
node.parentNode.replaceChild(frag, node);
}
}
function showTerm(ab){
const pop = document.getElementById('pop');
pop.querySelector('b').textContent = ab.dataset.term;
pop.querySelector('span').textContent = ab.title;
pop.hidden = false;
const r = ab.getBoundingClientRect(); const w = Math.min(360, innerWidth - 24);
let x = r.left + r.width/2 - w/2; x = Math.max(12, Math.min(x, innerWidth - w - 12));
pop.style.width = w + 'px'; pop.style.left = x + 'px';
const below = r.bottom + 8 + pop.offsetHeight < innerHeight;
pop.style.top = (below ? r.bottom + 8 : r.top - 8 - pop.offsetHeight) + scrollY + 'px';
}
/* ---------- 文本渲染:链接 + 高亮 ---------- */
const URL_RE = /<?(https?:\/\/[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,=%]+)>?/g;
function trimUrl(u){
let t = '';
for (;;){
const ch = u.slice(-1);
const unbalanced = ch === ')' && (u.split('(').length < u.split(')').length);
if (!/[)。,,]/.test(ch) && !unbalanced) break;
t = ch + t; u = u.slice(0, -1);
}
return [u, t];
}
// Markdown 链接 [文字](地址) 或裸 URL;仓库内的相对路径指向 GitHub 上的源文件
const REPO_BLOB = 'https://github.com/eternity4719/HowToLiveBetter/blob/main/';
const MD_LINK_RE = /\[([^\]\n]+)\]\(([^)\s]+)\)/;
const LINK_RE = new RegExp(MD_LINK_RE.source + '|' + URL_RE.source, 'g');
function hrefOf(p){
if (/^(https?:|mailto:|#)/.test(p)) return p;
return REPO_BLOB + p.replace(/^(\.\.?\/)+/, '').split('/').map(encodeURIComponent).join('/');
}
function mkLink(href, text){
const a = document.createElement('a'); a.href = href; a.target = '_blank'; a.rel = 'noopener'; a.textContent = text;
return a;
}
function renderText(el, text, terms, withTerms = true){
el.textContent = '';
let last = 0; LINK_RE.lastIndex = 0; let m;
const pushText = (s) => { if (s) highlightInto(el, s, terms); };
while ((m = LINK_RE.exec(text))){
pushText(text.slice(last, m.index));
if (m[3]){
const [u, tail] = trimUrl(m[3]);
el.appendChild(mkLink(u, u)); pushText(tail);
} else {
el.appendChild(mkLink(hrefOf(m[2]), m[1]));
}
last = m.index + m[0].length;
}
pushText(text.slice(last));
if (withTerms) termify(el);
}
function highlightInto(el, s, terms){
if (!terms.length){ el.appendChild(document.createTextNode(s)); return; }
const lower = s.toLowerCase(); let i = 0;
while (i < s.length){
let best = -1, bl = 0;
for (const t of terms){ const k = lower.indexOf(t, i); if (k !== -1 && (best === -1 || k < best)) { best = k; bl = t.length; } }
if (best === -1){ el.appendChild(document.createTextNode(s.slice(i))); break; }
if (best > i) el.appendChild(document.createTextNode(s.slice(i, best)));
const mk = document.createElement('mark'); mk.textContent = s.slice(best, best+bl); el.appendChild(mk);
i = best + bl;
}
}
const COST_W = { money:{'0':0,'少':1,'多':2}, time:{'少':0,'中':1,'多':2}, will:{'否':0,'些':1,'是':2} };
const LENS_LABEL = {'死亡率':'换寿命','金钱':'换钱','时间':'换时间精力','自由':'换人身自由'};
/* ---------- 构建 DOM ---------- */
const LABEL = { money:{'0':'不花钱','少':'花少量钱','多':'花不少钱'}, time:{'少':'顺手','中':'花几小时','多':'每天占时间'}, will:{'否':'不用毅力','些':'要一点毅力','是':'要很多毅力'} };
let CARDS = [], BLOCKS = [];
// 卡片文字只在「上屏 + 搜索词变了」时重渲染,离屏的先记账,滚进来再补
let CUR_TERMS = [], CUR_KEY = '';
const CARD_OF = new WeakMap();
const CARD_IO = new IntersectionObserver(list => {
const t0 = performance.now(); let n = 0;
for (const it of list){
const card = CARD_OF.get(it.target); if (!card) continue;
card.vis = it.isIntersecting;
if (card.vis && card.key !== CUR_KEY) { n++; renderCard(card, CUR_TERMS, CUR_KEY); }
}
if (n) PERF.log('滚动补渲染', t0, n + ' 张');
}, {rootMargin:'600px 0px'});
function esc(s){ return s.replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch])); }
function build(sections){
const list = document.getElementById('list');
const fsec = document.getElementById('f-sec');
const allBtn = document.createElement('button');
allBtn.className = 'sec-link all'; allBtn.dataset.v = ''; allBtn.setAttribute('aria-pressed','true');
allBtn.innerHTML = `<span>全部章节</span><i>${sections.reduce((n,s)=>n+s.entries.length,0)}</i>`;
fsec.appendChild(allBtn);
for (const s of sections){
const b = document.createElement('button'); b.className='sec-link'; b.dataset.v = s.n; b.setAttribute('aria-pressed','false');
b.innerHTML = `<span>${s.n}. ${esc(s.title)}</span><i>${s.entries.length}</i>`; fsec.appendChild(b);
const block = document.createElement('section'); block.className='sec-block'; block.dataset.sec = s.n; block.id = 'sec-'+s.n;
block.innerHTML = `<div class="sec-h"><h2>${s.n}. ${esc(s.title)}</h2><span class="shown"><span class="k"></span> / ${s.entries.length}</span></div><p class="intro"></p>`;
renderText(block.querySelector('.intro'), s.intro, [], false);
for (const e of s.entries){
const id = `e-${e.sec}-${e.n}`;
const c = document.createElement('article'); c.className='card'; c.id = id;
c.innerHTML = `
<div class="card-h"><span class="idx">${e.n}</span><h3 class="t"></h3><a class="anchor" href="#${id}" aria-label="本条链接">#</a></div>
<div class="badges"></div>
<p class="human"></p>
<div class="rows">
<div class="k">成本</div><div class="v cost"></div>
<div class="k">收益</div><div class="v gain"></div>
<div class="k">备注</div><div class="v note"></div>
</div>
<details class="src"><summary>来源</summary><div class="body"></div></details>`;
const badges = c.querySelector('.badges');
const add = (cls, text) => { const t=document.createElement('span'); t.className='badge '+cls; t.textContent=text; badges.appendChild(t); };
if (e.ratio) { const t = document.createElement('span'); t.className = 'badge r' + ({'极高':'3','高':'2','一般':'1'}[e.ratio]);
t.textContent = '性价比 ' + e.ratio;
t.title = '作者判断:由收益量级(' + e.level + ')和三项成本合成,只在同一口径(' + LENS_LABEL[e.lens] + ')内可比,与证据等级无关';
badges.appendChild(t); }
add(e.grade, `证据 ${e.grade} 级`);
if (e.lens) add('plain', LENS_LABEL[e.lens] || e.lens);
for (const d of ['money','time','will']) if (e[d]) add('plain', LABEL[d][e[d]] || e[d]);
if (e.dispute) add('danger', '争议');
if (e.todo) add('warn', '含待核实');
block.appendChild(c);
const card = {e, el:c, key:null, vis:false, f:{t:c.querySelector('.t'), cost:c.querySelector('.cost'), human:c.querySelector('.human'), gain:c.querySelector('.gain'), note:c.querySelector('.note'), src:c.querySelector('.src .body'), details:c.querySelector('.src')}};
renderCard(card, [], '');
CARDS.push(card); CARD_OF.set(c, card); CARD_IO.observe(c);
}
list.appendChild(block);
BLOCKS.push({n:s.n, el:block, k:block.querySelector('.shown .k')});
}
document.getElementById('tot').textContent = CARDS.length;
}
function buildGlossary(){
const box = document.getElementById('gloss'); if (!GLOSS.length) { box.hidden = true; return; }
const dl = box.querySelector('dl');
for (const g of [...GLOSS].sort((a,b) => a.term.localeCompare(b.term, 'zh'))){
const dt = document.createElement('dt'); dt.textContent = g.term;
const dd = document.createElement('dd'); dd.textContent = g.meaning;
dl.appendChild(dt); dl.appendChild(dd);
}
}
/* ---------- 筛选状态 ---------- */
const DIMS = ['sec','ratio','lens','grade','money','time','will'];
const state = { q:'', sec:new Set(), ratio:new Set(), lens:new Set(), grade:new Set(), money:new Set(), time:new Set(), will:new Set(), dispute:false, todo:false };
function readUrl(){
const p = new URLSearchParams(location.search);
state.q = p.get('q') || '';
for (const d of DIMS) state[d] = new Set((p.get(d)||'').split(',').filter(Boolean));
state.dispute = p.get('dispute') === '1'; state.todo = p.get('todo') === '1';
}
function writeUrl(){
const p = new URLSearchParams();
if (state.q) p.set('q', state.q);
for (const d of DIMS) if (state[d].size) p.set(d, [...state[d]].join(','));
if (state.dispute) p.set('dispute','1'); if (state.todo) p.set('todo','1');
if (PERF.on) p.set('perf','1');
const qs = p.toString(); history.replaceState(null, '', (qs ? '?'+qs : location.pathname) + location.hash);
}
function syncControls(){
document.getElementById('q').value = state.q;
document.querySelectorAll('[data-dim] [data-v]').forEach(b => b.setAttribute('aria-pressed', String(state[b.parentElement.dataset.dim].has(b.dataset.v))));
const allBtn = document.querySelector('#f-sec .all');
if (allBtn) allBtn.setAttribute('aria-pressed', String(state.sec.size === 0));
document.getElementById('f-dispute').checked = state.dispute;
document.getElementById('f-todo').checked = state.todo;
}
// 文字只在搜索词变化时重渲染;筛选按钮只切换显隐,不碰卡片内容
function renderCard(card, terms, key){
const {e, f} = card;
renderText(f.t, e.title, terms);
renderText(f.cost, e.cost, terms);
f.human.hidden = !e.human;
if (e.human) renderText(f.human, e.human, terms);
renderText(f.gain, e.gain, terms);
renderText(f.note, e.note, terms);
renderText(f.src, e.src, terms);
const inBody = (e.title+e.human+e.cost+e.gain+e.note).toLowerCase();
f.details.open = terms.length > 0 && terms.some(t => e.src.toLowerCase().includes(t)) && !terms.some(t => inBody.includes(t));
card.key = key;
}
function apply(){
const t0 = performance.now();
const terms = state.q.toLowerCase().split(/\s+/).filter(Boolean);
const key = terms.join(' ');
CUR_TERMS = terms; CUR_KEY = key;
let shown = 0, rerender = 0; const perSec = {};
for (const card of CARDS){
const {e, el} = card;
let ok = true;
if (state.sec.size && !state.sec.has(e.sec)) ok = false;
if (ok && state.ratio.size && !state.ratio.has(e.ratio)) ok = false;
if (ok && state.lens.size && !state.lens.has(e.lens)) ok = false;
if (ok && state.grade.size && !state.grade.has(e.grade)) ok = false;
if (ok && state.money.size && !state.money.has(e.money)) ok = false;
if (ok && state.time.size && !state.time.has(e.time)) ok = false;
if (ok && state.will.size && !state.will.has(e.will)) ok = false;
if (ok && state.dispute && !e.dispute) ok = false;
if (ok && state.todo && !e.todo) ok = false;
if (ok && terms.length && !terms.every(t => e.hay.includes(t))) ok = false;
if (el.hidden === ok) el.hidden = !ok;
if (ok){
shown++; perSec[e.sec] = (perSec[e.sec]||0)+1;
if (card.vis && card.key !== key) { rerender++; renderCard(card, terms, key); }
}
}
for (const b of BLOCKS){ const n = perSec[b.n]||0; if (b.el.hidden !== (n === 0)) b.el.hidden = n === 0; b.k.textContent = n; }
document.getElementById('cnt').textContent = shown;
document.getElementById('empty').hidden = shown !== 0;
syncControls(); writeUrl();
PERF.log('apply', t0, `命中 ${shown} 条,重渲染 ${rerender} 张`);
}
/* ---------- 事件 ---------- */
function wire(){
const sb = document.getElementById('sidebar'), bd = document.getElementById('backdrop'), mb = document.getElementById('menu');
const setMenu = (on) => { sb.classList.toggle('open', on); bd.classList.toggle('on', on); mb.setAttribute('aria-expanded', String(on)); };
const toTop = () => { setMenu(false); scrollTo({ top: 0, behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); };
document.querySelectorAll('[data-dim]').forEach(g => g.addEventListener('click', ev => {
const b = ev.target.closest('[data-v]'); if (!b) return;
const dim = g.dataset.dim, v = b.dataset.v, set = state[dim];
if (dim === 'sec'){
// 章节单选:再点一次已选中的不取消,免得双击退回全部章节;要看全部点「全部章节」
if (v === '') set.clear(); else { set.clear(); set.add(v); }
} else {
set.has(v) ? set.delete(v) : set.add(v);
}
apply(); toTop();
}));
document.getElementById('f-dispute').addEventListener('change', e => { state.dispute = e.target.checked; apply(); toTop(); });
document.getElementById('f-todo').addEventListener('change', e => { state.todo = e.target.checked; apply(); toTop(); });
let tm; document.getElementById('q').addEventListener('input', e => { clearTimeout(tm); tm = setTimeout(() => { state.q = e.target.value.trim(); apply(); }, 120); });
const reset = () => { state.q=''; for (const d of DIMS) state[d].clear(); state.dispute=false; state.todo=false; apply(); toTop(); };
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('reset2').addEventListener('click', reset);
document.addEventListener('keydown', e => {
if (e.key === '/' && !/INPUT|TEXTAREA/.test(document.activeElement.tagName)) { e.preventDefault(); document.getElementById('q').focus(); }
if (e.key === 'Escape') { if (document.activeElement.id === 'q') document.activeElement.blur(); setMenu(false); }
});
// 术语弹层
const pop = document.getElementById('pop');
document.addEventListener('click', e => {
const ab = e.target.closest('abbr.term');
if (ab) { e.preventDefault(); showTerm(ab); return; }
if (!e.target.closest('#pop')) pop.hidden = true;
});
document.addEventListener('keydown', e => { if (e.key === 'Enter' && document.activeElement.matches?.('abbr.term')) showTerm(document.activeElement); });
addEventListener('scroll', () => { pop.hidden = true; }, {passive:true});
// 侧栏(移动端抽屉)
mb.addEventListener('click', () => setMenu(!sb.classList.contains('open')));
bd.addEventListener('click', () => setMenu(false));
// 深色模式
document.getElementById('theme').addEventListener('click', () => {
const dark = !document.documentElement.classList.contains('dark');
document.documentElement.classList.toggle('dark', dark);
try { localStorage.setItem('theme', dark ? 'dark' : 'light'); } catch (e) {}
});
}
/* ---------- 启动 ---------- */
(async function init(){
const status = document.getElementById('status');
try {
if (location.protocol === 'file:') throw new Error('file');
const r = await fetch('README.md', {cache:'no-cache'});
if (!r.ok) throw new Error('http '+r.status);
const md = await r.text();
const files = [...new Set(Array.from(md.matchAll(/\]\((book\/[^)]+\.md)\)/g), m => m[1]))].sort();
if (!files.length) throw new Error('empty');
const parts = await Promise.all(files.map(async f => {
const res = await fetch(f, {cache:'no-cache'});
if (!res.ok) throw new Error('http '+res.status+':'+f);
return res.text();
}));
const t1 = performance.now();
const sections = parseReadme(parts.join('\n\n'));
if (!sections.length) throw new Error('empty');
GLOSS = parseGlossary(md);
PERF.log('解析正文', t1, files.length + ' 个文件');
status.remove();
const t2 = performance.now(); build(sections); PERF.log('建 DOM', t2, CARDS.length + ' 张卡');
buildGlossary(); readUrl(); wire(); apply();
PERF.mark(`DOM 节点 ${document.getElementsByTagName('*').length},术语 ${GLOSS.length}`);
if (location.hash){
const target = document.getElementById(location.hash.slice(1));
target?.scrollIntoView();
// 卡片是 content-visibility:auto,首次定位后高度才准,下一帧再对一次
if (target) requestAnimationFrame(() => target.scrollIntoView());
}
} catch (err) {
status.innerHTML = err.message === 'file'
? '这个页面要通过 http 打开才能读取正文。在仓库目录运行 <code>python -m http.server</code>,然后访问 <code>http://localhost:8000/</code>;或直接用 GitHub Pages 地址。'
: `读取正文失败(${esc(String(err.message))})。确认 README.md 和 book/ 目录都在 index.html 旁边。`;
}
})();
</script>
</body>
</html>