daily-question 1.1.0 → 2.1.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.
package/client.js CHANGED
@@ -89,6 +89,7 @@ window.__ModuleLoader__.load({
89
89
  const value = String(method || '')
90
90
  if (!value) return '独立校验'
91
91
  if (value.startsWith('two-step')) return '两步:出题 → 独立视角校验'
92
+ if (value.startsWith('blind-solve')) return '盲解 → 比对 → 分歧主裁'
92
93
  if (value === 'import-parse') return '题库导入解析'
93
94
  if (value === 'online-search') return '官方渠道联网核验'
94
95
  if (value === 'catalog') return '内置官方日期表'
@@ -133,7 +134,7 @@ window.__ModuleLoader__.load({
133
134
  window.MathJax = {
134
135
  startup: { typeset: false },
135
136
  // 每条公式自带 <defs>,注入 innerHTML 后不依赖全局字体缓存
136
- svg: { fontCache: 'local' },
137
+ svg: { fontCache: 'local', scale: 1, matchFontHeight: false },
137
138
  }
138
139
  const script = document.createElement('script')
139
140
  script.src = MATHJAX_SRC
@@ -543,239 +544,274 @@ window.__ModuleLoader__.load({
543
544
  /** 由 apply 用 styles.insert 注入时置位;否则由组件渲染 <style> 兜底。 */
544
545
  let styleInjected = false
545
546
  const CSS = `
546
- .kd-page { height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
547
- background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
547
+ .kd-page {
548
+ --kd-bg-base:var(--dsw-alias-bg-base, #ffffff);
549
+ --kd-bg-layer-1:var(--dsw-alias-bg-layer-1, #ffffff);
550
+ --kd-bg-layer-2:color-mix(in srgb, var(--kd-ink) 5%, var(--kd-surface));
551
+ --kd-bg-overlay:var(--dsw-alias-bg-overlay, #ffffff);
552
+ --kd-label-primary:var(--dsw-alias-label-primary, #24332f);
553
+ --kd-label-secondary:var(--dsw-alias-label-secondary, #66736e);
554
+ --kd-border-l1:color-mix(in srgb, var(--kd-ink) 9%, var(--kd-surface));
555
+ --kd-border-l2:color-mix(in srgb, var(--kd-ink) 17%, var(--kd-surface));
556
+ --kd-brand-primary:#4d6bfe;
557
+ --kd-state-success-primary:#078443;
558
+ --kd-state-error-primary:#db3144;
559
+ --kd-state-warn-primary:#95702b;
560
+ --kd-state-idle-primary:#85918b;
561
+ --kd-ink:var(--kd-label-primary); --kd-surface:var(--kd-bg-base);
562
+ font-family:inherit; isolation:isolate; height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
563
+ background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); color: var(--kd-label-primary);
548
564
  font-size:14px; line-height:1.65; -webkit-font-smoothing:antialiased; box-sizing:border-box; }
549
565
  .kd-page *, .kd-page *::before, .kd-page *::after { box-sizing:border-box; }
550
- .kd-page :focus-visible { outline:2px solid var(--dsw-alias-brand-primary); outline-offset:2px; }
551
- .kd-header { padding:18px 24px 14px; border-bottom:1px solid var(--dsw-alias-border-l1);
566
+ .kd-page :focus-visible { outline:2px solid var(--kd-brand-primary); outline-offset:2px; }
567
+ .kd-header { padding:20px 28px 12px; background:var(--kd-surface); border-bottom:1px solid var(--kd-border-l1);
552
568
  display:flex; flex-direction:column; gap:12px; }
553
- .kd-header-top { display:flex; gap:24px; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; }
554
- .kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:flex-start; }
555
- .kd-brand { flex:0 0 34px; width:34px; height:34px; border-radius:10px; display:flex; align-items:center;
556
- justify-content:center; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
557
- color:var(--dsw-alias-brand-primary); }
558
- .kd-title { margin:0; font-size:18px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
559
- .kd-sub { margin-top:3px; font-size:12px; color:var(--dsw-alias-label-secondary); line-height:1.6; }
569
+ .kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
570
+ .kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:center; }
571
+ .kd-brand { flex:0 0 40px; width:40px; height:40px; border-radius:12px; display:flex; align-items:center;
572
+ justify-content:center; border:0; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface));
573
+ color:var(--kd-brand-primary); }
574
+ .kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
575
+ .kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
560
576
  .kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
561
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
562
- color:var(--dsw-alias-label-primary); transition:border-color .15s; }
563
- .kd-select:hover { border-color:var(--dsw-alias-brand-primary); }
577
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
578
+ color:var(--kd-label-primary); transition:border-color .15s; }
579
+ .kd-select:hover { border-color:var(--kd-brand-primary); }
564
580
  .kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
565
581
  .kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
566
582
  gap:2px; text-align:right; }
567
- .kd-cd-primary { font-size:24px; font-weight:700; letter-spacing:.01em; line-height:1.25; }
568
- .kd-cd-secondary { font-size:12px; color:var(--dsw-alias-label-secondary); }
569
- .kd-cd-target { font-size:13.5px; font-weight:500; }
570
- .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 12px;
571
- border:1px solid var(--dsw-alias-border-l1); border-radius:10px; background:var(--dsw-alias-bg-layer-1);
572
- font-size:11.5px; color:var(--dsw-alias-label-secondary); }
583
+ .kd-cd-primary { font-size:21px; font-weight:650; color:var(--kd-brand-primary); font-variant-numeric:tabular-nums; letter-spacing:.01em; line-height:1.25; }
584
+ .kd-cd-secondary { font-size:12px; color:var(--kd-label-secondary); }
585
+ .kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-secondary); }
586
+ .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
587
+ border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
588
+ font-size:11.5px; color:var(--kd-label-secondary); }
573
589
  .kd-strip-item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
574
- .kd-strip-warn { color:var(--dsw-alias-state-warn-primary); white-space:normal; text-align:left; }
590
+ .kd-strip-warn { color:var(--kd-state-warn-primary); white-space:normal; text-align:left; }
575
591
  .kd-shell { flex:1 1 auto; min-height:0; display:flex; }
576
- .kd-nav { width:212px; flex:0 0 212px; border-right:1px solid var(--dsw-alias-border-l1);
577
- padding:16px 12px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
592
+ .kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
593
+ padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
578
594
  .kd-nav-group { display:flex; flex-direction:column; gap:4px; }
579
- .kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--dsw-alias-label-secondary);
595
+ .kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
580
596
  padding:0 8px 4px; }
581
- .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:8px;
582
- width:100%; padding:8px 10px; border-radius:9px; border:1px solid transparent; background:transparent;
583
- cursor:pointer; color:var(--dsw-alias-label-primary); font:inherit; font-size:13px; text-align:left;
597
+ .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
598
+ width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
599
+ cursor:pointer; color:var(--kd-label-primary); font:inherit; font-size:13px; text-align:left;
584
600
  transition:background-color .15s, border-color .15s, color .15s; }
585
- .kd-nav-item:hover { background:var(--dsw-alias-bg-layer-2); }
586
- .kd-nav-item > span:first-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
587
- .kd-nav-item.is-active { background:var(--dsw-alias-bg-layer-2); border-color:var(--dsw-alias-border-l2);
588
- font-weight:600; box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary); }
601
+ .kd-nav-item:hover { background:var(--kd-bg-layer-2); }
602
+ .kd-nav-item > span:first-child { min-width:0; overflow-wrap:anywhere; line-height:1.6; }
603
+ .kd-nav-item.is-active { background:color-mix(in srgb, var(--kd-brand-primary) 9%, var(--kd-surface)); border-color:transparent;
604
+ color:var(--kd-brand-primary); font-weight:600; }
589
605
  .kd-nav-item .kd-nav-count { flex:0 0 auto; white-space:nowrap; font-size:11px;
590
- color:var(--dsw-alias-label-secondary); }
606
+ color:var(--kd-label-secondary); }
591
607
  .kd-nav-status { flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; white-space:nowrap;
592
- font-size:10.5px; color:var(--dsw-alias-label-secondary); }
593
- .kd-dot { width:6px; height:6px; border-radius:50%; background:var(--dsw-alias-state-success-primary); }
594
- .kd-dot-idle { background:var(--dsw-alias-state-idle-primary); }
608
+ font-size:10.5px; color:var(--kd-label-secondary); }
609
+ .kd-dot { width:6px; height:6px; border-radius:50%; background:var(--kd-state-success-primary); }
610
+ .kd-dot-idle { background:var(--kd-state-idle-primary); }
595
611
  .kd-nav-spacer { flex:1 1 auto; }
596
- .kd-main { flex:1 1 auto; min-width:0; overflow:auto; padding:20px 24px 48px; }
597
- .kd-main-inner { max-width:900px; margin:0 auto; }
598
- .kd-thread { width:340px; flex:0 0 340px; border-left:1px solid var(--dsw-alias-border-l1);
599
- display:flex; flex-direction:column; min-height:0; background:var(--dsw-alias-bg-layer-1); }
600
- .kd-thread-head { padding:13px 16px; border-bottom:1px solid var(--dsw-alias-border-l1);
612
+ .kd-update { display:flex; flex-direction:column; gap:4px; }
613
+ .kd-main { flex:1 1 auto; min-width:0; overflow:hidden; display:flex; flex-direction:column; }
614
+ .kd-main-toolbar { flex:none; padding:24px 28px 20px; display:flex; align-items:center; gap:12px; }
615
+ .kd-main-toolbar > .kd-btn { flex:none; align-self:stretch; aspect-ratio:1; padding:0; min-height:0; white-space:nowrap; border-radius:12px; }
616
+ .kd-main-scroll { flex:1; min-height:0; overflow:auto; padding:0 28px 48px; scrollbar-gutter:stable; }
617
+ .kd-main-inner { width:100%; margin:0; }
618
+ .kd-thread { position:relative; width:clamp(280px,calc((100% - 204px) * var(--kd-thread-ratio, .33)),55%); flex:0 0 auto; border-left:1px solid var(--kd-border-l1);
619
+ display:flex; flex-direction:column; min-height:0; background:var(--kd-bg-layer-1); }
620
+ .kd-thread-resize { position:absolute; left:-5px; top:0; bottom:0; width:10px; cursor:col-resize; touch-action:none; z-index:2; }
621
+ .kd-thread-resize:hover, .kd-thread-resize:focus-visible { background:color-mix(in srgb,var(--kd-brand-primary) 20%,transparent); }
622
+ .kd-thread-head { padding:20px; border-bottom:1px solid var(--kd-border-l1);
601
623
  display:flex; align-items:center; justify-content:space-between; gap:8px; }
602
624
  .kd-thread-title { font-size:13px; font-weight:600; }
603
- .kd-thread-hint { padding:10px 16px 0; font-size:11px; line-height:1.6;
604
- color:var(--dsw-alias-label-secondary); }
605
- .kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:14px 16px; display:flex;
625
+ .kd-thread-hint { padding:12px 20px 0; font-size:11px; line-height:1.6;
626
+ color:var(--kd-label-secondary); }
627
+ .kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:18px 20px; display:flex;
606
628
  flex-direction:column; gap:12px; }
607
629
  .kd-msg { max-width:100%; border-radius:12px; padding:10px 12px; font-size:13px; line-height:1.72; }
608
- .kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 9%, transparent);
609
- border:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 26%, transparent); }
610
- .kd-msg-assistant { align-self:stretch; border:1px solid var(--dsw-alias-border-l1);
611
- background:var(--dsw-alias-bg-base); }
612
- .kd-msg-meta { font-size:10px; color:var(--dsw-alias-label-secondary); margin-top:6px; }
613
- .kd-msg-failed { border-color:var(--dsw-alias-state-error-primary); }
614
- .kd-thread-foot { border-top:1px solid var(--dsw-alias-border-l1); padding:12px 16px; }
615
- .kd-composer { border:1px solid var(--dsw-alias-border-l2); border-radius:12px;
616
- background:var(--dsw-alias-bg-base); padding:6px; transition:border-color .15s, box-shadow .15s; }
617
- .kd-composer:focus-within { border-color:var(--dsw-alias-brand-primary);
618
- box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
630
+ .kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--kd-brand-primary) 9%, transparent);
631
+ border:1px solid color-mix(in srgb, var(--kd-brand-primary) 26%, transparent); }
632
+ .kd-msg-assistant { align-self:stretch; padding:0; border:0; background:transparent; }
633
+ .kd-msg-meta { font-size:10px; color:var(--kd-label-secondary); margin-top:6px; }
634
+ .kd-msg-failed { border-color:var(--kd-state-error-primary); }
635
+ .kd-thread-foot { border-top:1px solid var(--kd-border-l1); padding:12px 16px; }
636
+ .kd-composer { border:1px solid var(--kd-border-l2); border-radius:12px;
637
+ background:var(--kd-bg-base); padding:6px; transition:border-color .15s, box-shadow .15s; }
638
+ .kd-composer:focus-within { border-color:var(--kd-brand-primary);
639
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
619
640
  .kd-textarea { width:100%; min-height:58px; max-height:180px; resize:vertical; padding:6px 8px;
620
- border:0; background:transparent; color:var(--dsw-alias-label-primary); font:inherit; font-size:13px; }
641
+ border:0; background:transparent; color:var(--kd-label-primary); font:inherit; font-size:13px; }
621
642
  .kd-textarea:focus { outline:none; }
622
- .kd-tabs { display:flex; gap:2px; margin-bottom:16px; border-bottom:1px solid var(--dsw-alias-border-l1); }
623
- .kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:8px 12px 11px;
624
- border:0; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:13px;
625
- cursor:pointer; border-radius:8px 8px 0 0; transition:color .15s, background-color .15s; }
626
- .kd-tab:hover { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-2); }
627
- .kd-tab.is-active { color:var(--dsw-alias-label-primary); font-weight:600; }
628
- .kd-tab.is-active::after { content:''; position:absolute; left:10px; right:10px; bottom:-1px; height:2px;
629
- border-radius:2px; background:var(--dsw-alias-brand-primary); }
643
+ .kd-tabs { display:flex; gap:4px; width:fit-content; max-width:100%; min-width:0; overflow-x:auto; padding:4px; border:1px solid var(--kd-border-l1); border-radius:12px; background:var(--kd-surface); }
644
+ .kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; white-space:nowrap;
645
+ border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
646
+ cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
647
+ .kd-tab:hover { color:var(--kd-label-primary); background:var(--kd-bg-layer-2); }
648
+ .kd-tab.is-active { color:var(--kd-brand-primary); font-weight:600; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface)); }
649
+
630
650
  .kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
631
- text-align:center; background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-secondary); }
632
- .kd-tab.is-active .kd-tab-count { color:var(--dsw-alias-brand-primary); }
633
- .kd-card { border:1px solid var(--dsw-alias-border-l1); border-radius:14px;
634
- background:var(--dsw-alias-bg-layer-1); padding:20px 22px; }
651
+ text-align:center; background:var(--kd-bg-layer-2); color:var(--kd-label-secondary); }
652
+ .kd-tab.is-active .kd-tab-count { color:var(--kd-brand-primary); }
653
+ .kd-card { border:1px solid var(--kd-border-l1); border-radius:14px;
654
+ background:var(--kd-bg-layer-1); padding:28px; box-shadow:0 4px 24px rgb(20 45 35 / 3%); }
635
655
  .kd-card + .kd-card { margin-top:14px; }
636
656
  .kd-meta { display:flex; gap:8px; flex-wrap:wrap; align-items:center; font-size:12px;
637
- color:var(--dsw-alias-label-secondary); }
657
+ color:var(--kd-label-secondary); }
638
658
  .kd-meta-dot::before { content:'·'; margin-right:8px; }
639
659
  .kd-chips { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
640
660
  .kd-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; border-radius:6px;
641
661
  font-size:11.5px; line-height:1.7; white-space:nowrap;
642
- color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-2); }
643
- .kd-tag-brand { color:var(--dsw-alias-brand-primary);
644
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
662
+ color:var(--kd-label-secondary); background:var(--kd-bg-layer-2); }
663
+ .kd-tag-brand { color:var(--kd-brand-primary);
664
+ background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
645
665
  .kd-badge { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:999px;
646
- font-size:11px; line-height:1.6; border:1px solid var(--dsw-alias-border-l2);
647
- color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-1); white-space:nowrap; }
648
- .kd-badge-ok { color:var(--dsw-alias-state-success-primary);
649
- border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 34%, transparent);
650
- background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); }
651
- .kd-badge-bad { color:var(--dsw-alias-state-error-primary);
652
- border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 34%, transparent);
653
- background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
654
- .kd-badge-warn { color:var(--dsw-alias-state-warn-primary);
655
- border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 34%, transparent);
656
- background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); }
657
- .kd-badge-brand { color:var(--dsw-alias-brand-primary);
658
- border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 34%, transparent);
659
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
660
- .kd-stem { margin:14px 0 18px; font-size:15px; line-height:1.85; }
666
+ font-size:11px; line-height:1.6; border:1px solid var(--kd-border-l2);
667
+ color:var(--kd-label-secondary); background:var(--kd-bg-layer-1); white-space:nowrap; }
668
+ .kd-badge-ok { color:var(--kd-state-success-primary);
669
+ border-color:color-mix(in srgb, var(--kd-state-success-primary) 34%, transparent);
670
+ background:color-mix(in srgb, var(--kd-state-success-primary) 10%, transparent); }
671
+ .kd-badge-bad { color:var(--kd-state-error-primary);
672
+ border-color:color-mix(in srgb, var(--kd-state-error-primary) 34%, transparent);
673
+ background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
674
+ .kd-badge-warn { color:var(--kd-state-warn-primary);
675
+ border-color:color-mix(in srgb, var(--kd-state-warn-primary) 34%, transparent);
676
+ background:color-mix(in srgb, var(--kd-state-warn-primary) 12%, transparent); }
677
+ .kd-badge-brand { color:var(--kd-brand-primary);
678
+ border-color:color-mix(in srgb, var(--kd-brand-primary) 34%, transparent);
679
+ background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
680
+ .kd-stem { margin:24px 0; font-size:16px; line-height:1.85; }
661
681
  .kd-options { display:flex; flex-direction:column; gap:8px; margin:0 0 4px; }
662
- .kd-option { display:flex; gap:12px; align-items:flex-start; padding:12px 14px; border-radius:10px;
663
- border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-base); cursor:pointer;
664
- text-align:left; color:var(--dsw-alias-label-primary); font:inherit; font-size:14px;
682
+ .kd-option { display:flex; gap:12px; align-items:flex-start; padding:14px 16px; border-radius:12px;
683
+ border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); cursor:pointer;
684
+ text-align:left; color:var(--kd-label-primary); font:inherit; font-size:14px;
665
685
  transition:background-color .15s, border-color .15s; }
666
- .kd-option:hover:not(:disabled) { border-color:var(--dsw-alias-border-l2);
667
- background:var(--dsw-alias-bg-layer-2); }
686
+ .kd-option:hover:not(:disabled) { border-color:var(--kd-border-l2);
687
+ background:var(--kd-bg-layer-2); }
668
688
  .kd-option:disabled { cursor:default; }
669
- .kd-option.is-selected { border-color:var(--dsw-alias-brand-primary);
670
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 7%, transparent); }
671
- .kd-option.is-correct { border-color:var(--dsw-alias-state-success-primary);
672
- background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); }
673
- .kd-option.is-wrong { border-color:var(--dsw-alias-state-error-primary);
674
- background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
675
- .kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:999px; display:inline-flex;
689
+ .kd-option.is-selected { border-color:var(--kd-brand-primary);
690
+ background:color-mix(in srgb, var(--kd-brand-primary) 7%, transparent); }
691
+ .kd-option.is-correct { border-color:color-mix(in srgb, var(--kd-state-success-primary) 45%, var(--kd-surface));
692
+ background:color-mix(in srgb, var(--kd-state-success-primary) 8%, transparent); }
693
+ .kd-option.is-wrong { border-color:var(--kd-state-error-primary);
694
+ background:color-mix(in srgb, var(--kd-state-error-primary) 8%, transparent); }
695
+ .kd-option-key { flex:0 0 auto; align-self:center; width:24px; height:24px; border-radius:8px; display:inline-flex;
676
696
  align-items:center; justify-content:center; font-size:12px; font-weight:600;
677
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-base); }
678
- .kd-option.is-selected .kd-option-key { color:var(--dsw-alias-brand-primary); border-color:var(--dsw-alias-brand-primary); }
679
- .kd-option.is-correct .kd-option-key { color:var(--dsw-alias-state-success-primary); border-color:var(--dsw-alias-state-success-primary); }
680
- .kd-option.is-wrong .kd-option-key { color:var(--dsw-alias-state-error-primary); border-color:var(--dsw-alias-state-error-primary); }
697
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-base); }
698
+ .kd-option.is-selected .kd-option-key { color:var(--kd-brand-primary); border-color:var(--kd-brand-primary); }
699
+ .kd-option.is-correct .kd-option-key { color:var(--kd-state-success-primary); border-color:var(--kd-state-success-primary); }
700
+ .kd-option.is-wrong .kd-option-key { color:var(--kd-state-error-primary); border-color:var(--kd-state-error-primary); }
681
701
  .kd-option-text { min-width:0; flex:1 1 auto; }
682
702
  .kd-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:16px; }
683
- .kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:32px;
703
+ .kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:36px;
684
704
  padding:0 14px; border-radius:8px; font-family:inherit; font-size:13px; font-weight:500; cursor:pointer;
685
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
686
- color:var(--dsw-alias-label-primary);
705
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
706
+ color:var(--kd-label-primary);
687
707
  transition:background-color .15s, border-color .15s, color .15s; }
688
- .kd-btn:hover:not(:disabled) { background:var(--dsw-alias-bg-layer-2); }
708
+ .kd-btn:hover:not(:disabled) { background:var(--kd-bg-layer-2); }
689
709
  .kd-btn:disabled { opacity:.5; cursor:not-allowed; }
690
- .kd-btn-primary { background:var(--dsw-alias-brand-primary); border-color:var(--dsw-alias-brand-primary); color:#fff; }
691
- .kd-btn-primary:hover:not(:disabled) { background:var(--dsw-alias-brand-primary); filter:brightness(1.07); }
692
- .kd-btn-ghost { border-color:transparent; background:transparent; color:var(--dsw-alias-label-secondary); }
693
- .kd-btn-ghost:hover:not(:disabled) { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); }
694
- .kd-btn-danger { color:var(--dsw-alias-state-error-primary); background:transparent;
695
- border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent); }
696
- .kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
710
+ .kd-btn-primary { background:var(--kd-brand-primary); border-color:var(--kd-brand-primary); color:#fff; }
711
+ .kd-btn-primary:hover:not(:disabled) { background:var(--kd-brand-primary); filter:brightness(1.07); }
712
+ .kd-btn-ghost { border-color:transparent; background:transparent; color:var(--kd-label-secondary); }
713
+ .kd-btn-ghost:hover:not(:disabled) { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
714
+ .kd-btn-danger { color:var(--kd-state-error-primary); background:transparent;
715
+ border-color:color-mix(in srgb, var(--kd-state-error-primary) 40%, transparent); }
716
+ .kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
697
717
  .kd-btn-sm { min-height:26px; padding:0 9px; font-size:12px; }
698
718
  .kd-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px;
699
- border-radius:6px; border:0; background:transparent; color:var(--dsw-alias-label-secondary); cursor:pointer;
719
+ border-radius:6px; border:0; background:transparent; color:var(--kd-label-secondary); cursor:pointer;
700
720
  transition:background-color .15s, color .15s; }
701
- .kd-iconbtn:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); }
721
+ .kd-iconbtn:hover { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
702
722
  .kd-answer { margin-top:6px; }
703
- .kd-answer-panel { margin-top:18px; border:1px solid var(--dsw-alias-border-l1); border-radius:14px;
704
- background:var(--dsw-alias-bg-layer-1); overflow:hidden; }
723
+ .kd-answer-panel { margin-top:18px; border:1px solid var(--kd-border-l1); border-radius:14px;
724
+ background:var(--kd-bg-layer-1); overflow:hidden; }
705
725
  .kd-answer-panel-head { display:flex; align-items:center; justify-content:space-between; gap:10px;
706
- flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--dsw-alias-border-l1);
707
- background:var(--dsw-alias-bg-layer-2); font-size:13px; font-weight:600; }
708
- .kd-answer-panel-head .kd-tag { background:var(--dsw-alias-bg-layer-1); font-weight:400; }
726
+ flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--kd-border-l1);
727
+ background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); font-size:13px; font-weight:600; }
728
+ .kd-answer-panel-head .kd-tag { background:var(--kd-bg-layer-1); font-weight:400; }
709
729
  .kd-answer-panel-body { padding:18px; }
710
730
  .kd-answer-block + .kd-answer-block { margin-top:18px; padding-top:16px;
711
- border-top:1px solid var(--dsw-alias-border-l1); }
712
- .kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--dsw-alias-label-secondary);
731
+ border-top:1px solid var(--kd-border-l1); }
732
+ .kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
713
733
  margin-bottom:6px; }
714
734
  .kd-verify { margin-top:14px; padding:11px 13px; border-radius:10px; font-size:12px; line-height:1.7;
715
- border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-2);
716
- color:var(--dsw-alias-label-secondary); }
735
+ border:1px solid var(--kd-border-l1); background:var(--kd-bg-layer-2);
736
+ color:var(--kd-label-secondary); }
717
737
  .kd-verify ul { margin:6px 0 0; padding-left:18px; }
718
738
  .kd-list-row { display:flex; gap:14px; align-items:flex-start; width:100%; text-align:left;
719
- padding:14px 16px; border-radius:12px; border:1px solid var(--dsw-alias-border-l1);
720
- background:var(--dsw-alias-bg-layer-1); cursor:pointer; color:var(--dsw-alias-label-primary);
739
+ padding:14px 16px; border-radius:12px; border:1px solid var(--kd-border-l1);
740
+ background:var(--kd-bg-layer-1); color:var(--kd-label-primary);
721
741
  font:inherit; transition:background-color .15s, border-color .15s; }
742
+ .kd-list-row > .kd-btn { flex:none; white-space:nowrap; }
743
+ .kd-list-row[role=button] { cursor:pointer; }
744
+ .kd-list-open { flex:none; display:flex; align-items:center; gap:6px; font-size:12px; color:var(--kd-label-secondary); white-space:nowrap; }
722
745
  .kd-list-row + .kd-list-row { margin-top:8px; }
723
- .kd-list-row:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
746
+ .kd-list-row:hover { border-color:var(--kd-border-l2); background:var(--kd-bg-layer-2); }
724
747
  .kd-list-body { min-width:0; flex:1 1 auto; }
725
- .kd-list-summary { font-size:13.5px; line-height:1.7; overflow:hidden; display:-webkit-box;
726
- -webkit-line-clamp:2; -webkit-box-orient:vertical; }
748
+ .kd-list-summary { --kd-preview-line-height:28px; font-size:14px; line-height:var(--kd-preview-line-height); min-width:0;
749
+ max-height:calc(2 * var(--kd-preview-line-height)); overflow:hidden; }
750
+ .kd-list-summary .kd-md { line-height:inherit; }
751
+ .kd-list-summary .kd-md > * { display:inline; }
752
+ .kd-list-summary .kd-p, .kd-list-summary .kd-h, .kd-list-summary .kd-mathblock { margin:0; }
753
+ .kd-list-summary .kd-p::after { content:' '; }
754
+ .kd-list-summary .kd-mathblock { display:inline; padding:0; text-align:left; vertical-align:baseline; }
755
+ .kd-list-summary mjx-container[display=true] { margin:0; }
756
+ .kd-list-summary .kd-code-wrap { margin:0; border:0; }
757
+ .kd-list-summary .kd-code-lang { display:none; }
758
+ .kd-list-summary .kd-code { display:inline; padding:0; line-height:inherit; white-space:pre-wrap; }
759
+ .kd-list-summary.is-truncated { mask-image:linear-gradient(#000,#000),linear-gradient(to right,#000 var(--kd-preview-fade-start),transparent var(--kd-preview-fade-end));
760
+ mask-size:100% var(--kd-preview-fade-top),100% calc(100% - var(--kd-preview-fade-top)); mask-position:top,bottom; mask-repeat:no-repeat; }
727
761
  .kd-list-meta { margin-top:7px; display:flex; gap:6px; flex-wrap:wrap; font-size:11px;
728
- color:var(--dsw-alias-label-secondary); align-items:center; }
729
- .kd-empty { padding:56px 24px; text-align:center; color:var(--dsw-alias-label-secondary); }
762
+ color:var(--kd-label-secondary); align-items:center; }
763
+ .kd-empty { padding:56px 24px; text-align:center; color:var(--kd-label-secondary); }
730
764
  .kd-empty-icon { width:44px; height:44px; margin:0 auto 14px; border-radius:50%; display:flex;
731
- align-items:center; justify-content:center; border:1px solid var(--dsw-alias-border-l1);
732
- background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-secondary); }
733
- .kd-empty-title { font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); margin-bottom:6px; }
765
+ align-items:center; justify-content:center; border:1px solid var(--kd-border-l1);
766
+ background:var(--kd-bg-layer-1); color:var(--kd-label-secondary); }
767
+ .kd-empty-title { font-size:15px; font-weight:600; color:var(--kd-label-primary); margin-bottom:6px; }
734
768
  .kd-empty-body { font-size:13px; max-width:460px; margin:0 auto; line-height:1.8; }
735
769
  .kd-empty .kd-actions { justify-content:center; }
736
770
  .kd-overlay { position:absolute; inset:0; background:rgba(0,0,0,.38); display:flex; align-items:center;
737
771
  justify-content:center; padding:24px; z-index:40; backdrop-filter:blur(2px); }
738
772
  .kd-modal { width:min(580px,100%); max-height:100%; display:flex; flex-direction:column;
739
- background:var(--dsw-alias-bg-overlay); color:var(--dsw-alias-label-primary);
740
- border:1px solid var(--dsw-alias-border-l2); border-radius:16px; overflow:hidden;
773
+ background:var(--kd-bg-overlay); color:var(--kd-label-primary);
774
+ border:1px solid var(--kd-border-l2); border-radius:16px; overflow:hidden;
741
775
  box-shadow:0 16px 48px rgba(0,0,0,.24); }
742
776
  .kd-modal-wide { width:min(880px,100%); }
743
- .kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--dsw-alias-border-l1);
777
+ .kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--kd-border-l1);
744
778
  display:flex; align-items:center; justify-content:space-between; gap:10px; }
745
779
  .kd-modal-title { font-size:15px; font-weight:650; letter-spacing:.01em; }
746
780
  .kd-modal-body { padding:18px 20px; overflow:auto; min-height:0; }
747
- .kd-modal-foot { padding:14px 20px; border-top:1px solid var(--dsw-alias-border-l1);
781
+ .kd-modal-foot { padding:14px 20px; border-top:1px solid var(--kd-border-l1);
748
782
  display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; }
749
783
  .kd-field { display:flex; flex-direction:column; gap:6px; margin-bottom:14px; }
750
- .kd-field-label { font-size:12px; font-weight:500; color:var(--dsw-alias-label-secondary); }
751
- .kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2);
752
- background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); font:inherit; font-size:13px;
784
+ .kd-field-label { font-size:12px; font-weight:500; color:var(--kd-label-secondary); }
785
+ .kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--kd-border-l2);
786
+ background:var(--kd-bg-base); color:var(--kd-label-primary); font:inherit; font-size:13px;
753
787
  transition:border-color .15s, box-shadow .15s; }
754
- .kd-input:focus { outline:none; border-color:var(--dsw-alias-brand-primary);
755
- box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
756
- .kd-input::placeholder { color:var(--dsw-alias-label-secondary); }
788
+ .kd-input:focus { outline:none; border-color:var(--kd-brand-primary);
789
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
790
+ .kd-input::placeholder { color:var(--kd-label-secondary); }
757
791
  .kd-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
758
792
  .kd-row-between { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
759
- .kd-divider { height:1px; background:var(--dsw-alias-border-l1); margin:16px 0; }
760
- .kd-preview-item { border:1px solid var(--dsw-alias-border-l1); border-radius:12px; padding:12px 14px;
761
- background:var(--dsw-alias-bg-base); }
793
+ .kd-divider { height:1px; background:var(--kd-border-l1); margin:16px 0; }
794
+ .kd-preview-item { border:1px solid var(--kd-border-l1); border-radius:12px; padding:12px 14px;
795
+ background:var(--kd-bg-base); }
762
796
  .kd-preview-item + .kd-preview-item { margin-top:8px; }
763
- .kd-warn-text { color:var(--dsw-alias-state-warn-primary); font-size:12px; line-height:1.7; }
764
- .kd-error-text { color:var(--dsw-alias-state-error-primary); font-size:12px; line-height:1.7; }
797
+ .kd-warn-text { color:var(--kd-state-warn-primary); font-size:12px; line-height:1.7; }
798
+ .kd-error-text { color:var(--kd-state-error-primary); font-size:12px; line-height:1.7; }
765
799
  .kd-toast { position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:60;
766
800
  padding:9px 18px; border-radius:999px; font-size:13px; max-width:80%;
767
- background:var(--dsw-alias-bg-overlay); border:1px solid var(--dsw-alias-border-l2);
801
+ background:var(--kd-bg-overlay); border:1px solid var(--kd-border-l2);
768
802
  box-shadow:0 8px 24px rgba(0,0,0,.18); }
769
- .kd-toast-bad { color:var(--dsw-alias-state-error-primary); }
770
- .kd-toast-ok { color:var(--dsw-alias-state-success-primary); }
771
- .kd-toast-warn { color:var(--dsw-alias-state-warn-primary); }
772
- .kd-loading { padding:48px 0; text-align:center; color:var(--dsw-alias-label-secondary); font-size:13px; }
803
+ .kd-toast-bad { color:var(--kd-state-error-primary); }
804
+ .kd-toast-ok { color:var(--kd-state-success-primary); }
805
+ .kd-toast-warn { color:var(--kd-state-warn-primary); }
806
+ .kd-loading { padding:48px 0; text-align:center; color:var(--kd-label-secondary); font-size:13px; }
773
807
  .kd-spinner { display:inline-block; width:14px; height:14px; border-radius:50%; vertical-align:-2px;
774
- border:2px solid var(--dsw-alias-border-l2); border-top-color:var(--dsw-alias-brand-primary);
808
+ border:2px solid var(--kd-border-l2); border-top-color:var(--kd-brand-primary);
775
809
  animation:kd-spin .8s linear infinite; margin-right:8px; }
776
810
  @keyframes kd-spin { to { transform:rotate(360deg); } }
777
- .kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--dsw-alias-border-l1);
811
+ .kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--kd-border-l1);
778
812
  overflow:auto; }
813
+ .kd-mobilebar > * { flex:0 0 auto; white-space:nowrap; }
814
+ .kd-mobilebar .kd-select { max-width:180px; }
779
815
  .kd-md { line-height:1.78; overflow-wrap:anywhere; }
780
816
  .kd-md > *:first-child { margin-top:0; }
781
817
  .kd-md > *:last-child { margin-bottom:0; }
@@ -785,57 +821,105 @@ window.__ModuleLoader__.load({
785
821
  .kd-h4, .kd-h5, .kd-h6 { font-size:14px; }
786
822
  .kd-list { margin:8px 0; padding-left:22px; }
787
823
  .kd-list li { margin:3px 0; }
788
- .kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--dsw-alias-border-l2);
789
- color:var(--dsw-alias-label-secondary); }
790
- .kd-hr { border:0; border-top:1px solid var(--dsw-alias-border-l1); margin:14px 0; }
791
- .kd-link { color:var(--dsw-alias-brand-primary); text-decoration:underline; text-underline-offset:2px; }
824
+ .kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--kd-border-l2);
825
+ color:var(--kd-label-secondary); }
826
+ .kd-hr { border:0; border-top:1px solid var(--kd-border-l1); margin:14px 0; }
827
+ .kd-link { color:var(--kd-brand-primary); text-decoration:underline; text-underline-offset:2px; }
792
828
  .kd-inline-code { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
793
- padding:1px 5px; border-radius:5px; background:var(--dsw-alias-bg-layer-2);
794
- border:1px solid var(--dsw-alias-border-l1); }
795
- .kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--dsw-alias-border-l1);
796
- background:var(--dsw-alias-bg-layer-2); }
797
- .kd-code-lang { padding:3px 10px; font-size:11px; color:var(--dsw-alias-label-secondary);
798
- border-bottom:1px solid var(--dsw-alias-border-l1); }
829
+ padding:1px 5px; border-radius:5px; background:var(--kd-bg-layer-2);
830
+ border:1px solid var(--kd-border-l1); }
831
+ .kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--kd-border-l1);
832
+ background:var(--kd-bg-layer-2); }
833
+ .kd-code-lang { padding:3px 10px; font-size:11px; color:var(--kd-label-secondary);
834
+ border-bottom:1px solid var(--kd-border-l1); }
799
835
  .kd-code { margin:0; padding:10px 12px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
800
836
  font-size:12.5px; line-height:1.6; white-space:pre; tab-size:2; }
801
837
  .kd-code code { font:inherit; }
802
838
  .kd-table-wrap { overflow:auto; margin:10px 0; }
803
839
  .kd-table { border-collapse:collapse; font-size:13px; min-width:0; }
804
- .kd-table th, .kd-table td { border:1px solid var(--dsw-alias-border-l1); padding:5px 10px; text-align:left; }
805
- .kd-table th { background:var(--dsw-alias-bg-layer-2); font-weight:600; }
840
+ .kd-table th, .kd-table td { border:1px solid var(--kd-border-l1); padding:5px 10px; text-align:left; }
841
+ .kd-table th { background:var(--kd-bg-layer-2); font-weight:600; }
806
842
  .kd-image { max-width:100%; height:auto; border-radius:8px; display:block; margin:8px 0; }
807
843
  .kd-image-fallback { display:inline-block; font-family:ui-monospace,monospace; font-size:12px;
808
- color:var(--dsw-alias-state-warn-primary); border:1px dashed var(--dsw-alias-state-warn-primary);
844
+ color:var(--kd-state-warn-primary); border:1px dashed var(--kd-state-warn-primary);
809
845
  border-radius:6px; padding:2px 6px; }
810
- .kd-math { display:inline-block; vertical-align:middle; max-width:100%; }
811
- .kd-math mjx-container { color:currentColor; }
812
- .kd-mathblock { display:block; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
846
+ .kd-math, .kd-mathblock { font-size:17px; }
847
+ .kd-math { display:inline-flex; vertical-align:baseline; align-items:baseline; max-width:100%; padding:.25em .1em;
848
+ overflow-x:auto; overflow-y:hidden; }
849
+ .kd-math mjx-container { color:currentColor; flex:none; }
850
+ .kd-mathblock { display:block; max-width:100%; min-width:0; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
813
851
  text-align:center; }
814
- .kd-mathblock mjx-container { display:inline-block; max-width:100%; }
852
+ .kd-page .kd-math mjx-container, .kd-page .kd-mathblock mjx-container {
853
+ display:inline-block; font-size:100% !important; max-width:none; }
854
+ .kd-page .kd-math svg, .kd-page .kd-mathblock svg { max-width:none !important; }
815
855
  .kd-math-pending { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
816
856
  opacity:.55; white-space:pre-wrap; }
817
- .kd-math-error { color:var(--dsw-alias-state-error-primary);
857
+ .kd-math-error { color:var(--kd-state-error-primary);
818
858
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em; }
819
859
  .kd-checkline { display:flex; gap:8px; align-items:flex-start; padding:5px 0; font-size:13px; }
820
860
  .kd-chip { display:inline-flex; align-items:center; gap:2px; padding:0 2px 0 8px; border-radius:999px;
821
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); font-size:12px; }
861
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1); font-size:12px; }
822
862
  .kd-chip .kd-checkline { padding:0; }
823
863
  .kd-chip .kd-iconbtn { width:20px; height:20px; }
864
+ .kd-option.is-correct { border-color:#22c55e; }
865
+ .kd-option.is-wrong { border-color:#f35464; }
866
+ .kd-page input[type=checkbox] { accent-color:var(--kd-brand-primary); width:16px; height:16px; flex:none; }
867
+ .kd-btn, .kd-iconbtn, .kd-tab, .kd-nav-item, .kd-option, .kd-list-row {
868
+ transition:background-color .16s, border-color .16s, color .16s, box-shadow .16s, transform .16s; }
869
+ .kd-btn:active:not(:disabled), .kd-iconbtn:active, .kd-tab:active, .kd-nav-item:active { transform:scale(.97); }
870
+ .kd-option:active:not(:disabled) { transform:translateY(1px); }
871
+ .kd-view { animation:kd-enter .2s ease-out; }
872
+ .kd-presence { display:contents; }
873
+ .kd-overlay { animation:kd-fade .16s ease-out; background:rgb(18 27 52 / 28%); backdrop-filter:blur(5px); }
874
+ .kd-modal { animation:kd-enter .18s ease-out; box-shadow:0 24px 80px rgb(12 25 60 / 18%); border-radius:20px; }
875
+ .kd-presence.is-leaving .kd-overlay { animation:kd-fade .16s ease-in reverse both; pointer-events:none; }
876
+ .kd-presence.is-leaving .kd-modal { animation:kd-enter .16s ease-in reverse both; }
877
+ .kd-thread { animation:kd-drawer .2s ease-out; }
878
+ .kd-presence.is-leaving .kd-thread { animation:kd-drawer .16s ease-in reverse both; pointer-events:none; }
879
+ .kd-modal-head { padding:22px 24px; }
880
+ .kd-modal-title { font-size:18px; }
881
+ .kd-modal-body { padding:20px 24px; background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); }
882
+ .kd-modal-foot { padding:16px 24px; }
883
+ .kd-manage-card { padding:16px; border:1px solid var(--kd-border-l1); border-radius:14px; background:var(--kd-bg-base); margin-bottom:10px; }
884
+ .kd-manage-card.is-enabled { border-color:color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface)); }
885
+ .kd-manage-card .kd-checkline { font-weight:550; }
886
+ .kd-manage-card .kd-msg-meta { font-weight:400; line-height:1.7; }
887
+ .kd-custom-subject { padding:20px; border-radius:14px; border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); }
888
+ .kd-field-label { font-size:13px; }
889
+ .kd-input { height:38px; }
890
+ .kd-bulkbar { position:sticky; top:0; z-index:5; display:flex; flex-direction:column; gap:10px; padding:14px;
891
+ margin-bottom:14px; border-radius:14px; border:1px solid color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface));
892
+ background:var(--kd-bg-base); box-shadow:0 5px 20px rgb(20 35 70 / 5%); animation:kd-enter .18s ease-out; }
893
+ .kd-list-row.is-selected { border-color:var(--kd-brand-primary); background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); }
894
+ .kd-list-row input { margin-top:5px; }
895
+ @keyframes kd-fade { from { opacity:0; } to { opacity:1; } }
896
+ @keyframes kd-enter { from { opacity:0; transform:translateY(8px) scale(.99); } to { opacity:1; transform:none; } }
897
+ @keyframes kd-drawer { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }
824
898
  @media (max-width: 1180px) {
825
- .kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); z-index:30;
899
+ .kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); flex-basis:auto; z-index:30;
826
900
  box-shadow:-12px 0 32px rgba(0,0,0,.22); }
901
+ .kd-thread-resize { display:none; }
827
902
  }
828
903
  @media (max-width: 860px) {
829
904
  .kd-nav { display:none; }
830
905
  .kd-mobilebar { display:flex; }
831
- .kd-main { padding:16px 14px 40px; }
906
+ .kd-main-toolbar { padding:16px 14px; }
907
+ .kd-main-scroll { padding:0 14px 40px; }
832
908
  .kd-header { padding:14px 14px 12px; gap:10px; }
909
+ .kd-header-left { flex-basis:100%; }
833
910
  .kd-cd-lines { align-items:flex-start; text-align:left; }
911
+ .kd-cd-primary { font-size:19px; }
912
+ .kd-tabs { flex:1; }
913
+ .kd-tab { flex:1 0 auto; justify-content:center; padding:7px 10px; }
834
914
  .kd-strip-item { white-space:normal; }
835
915
  .kd-card { padding:16px 14px; }
836
916
  .kd-answer-panel-body { padding:14px; }
837
917
  .kd-exam-strip { padding:8px 10px; }
838
918
  }
919
+ @media (prefers-reduced-motion: reduce) {
920
+ .kd-page *, .kd-page *::before, .kd-page *::after { transition:none !important; animation:none !important; }
921
+ }
922
+
839
923
  `
840
924
 
841
925
  /* ============================== 接口 ============================== */
@@ -844,10 +928,12 @@ window.__ModuleLoader__.load({
844
928
  try {
845
929
  response = await fetch('/api/daily-question/rpc', {
846
930
  method: 'POST',
931
+ signal: AbortSignal.timeout(op === 'ensureDaily' || op === 'revise' ? 16*60*1000 : op === 'followup' ? 6*60*1000 : 60_000),
847
932
  headers: { 'content-type': 'application/json' },
848
933
  body: JSON.stringify({ op, args: args || {} }),
849
934
  })
850
935
  } catch (error) {
936
+ if (error.name === 'TimeoutError') throw new Error('等待服务响应超时,请重试')
851
937
  throw new Error(`无法连接插件后端(${error.message})。请确认「考研每日一题」插件已启用。`)
852
938
  }
853
939
  let payload = null
@@ -896,6 +982,7 @@ window.__ModuleLoader__.load({
896
982
  check: 'M4 12.5l5 5L20 6.5',
897
983
  x: 'M6 6l12 12M18 6L6 18',
898
984
  chevron: 'M9 6l6 6-6 6',
985
+ 'chevron-left': 'M15 6l-6 6 6 6',
899
986
  send: 'M4 12l16-8-6 8 6 8z',
900
987
  book: 'M4 5.5A2.5 2.5 0 0 1 6.5 3H20v16H6.5A2.5 2.5 0 0 0 4 21.5z',
901
988
  plus: 'M12 5v14M5 12h14',
@@ -914,13 +1001,14 @@ window.__ModuleLoader__.load({
914
1001
  }, h('path', { d: path }))
915
1002
  }
916
1003
 
917
- function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button' }) {
1004
+ function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button', ariaLabel }) {
918
1005
  return h('button', {
919
1006
  type,
920
1007
  className: `kd-btn kd-btn-${variant}${small ? ' kd-btn-sm' : ''}`,
921
1008
  onClick,
922
1009
  disabled,
923
1010
  title,
1011
+ 'aria-label': ariaLabel,
924
1012
  }, children)
925
1013
  }
926
1014
 
@@ -937,6 +1025,17 @@ window.__ModuleLoader__.load({
937
1025
  actions ? h('div', { className: 'kd-actions' }, actions) : null)
938
1026
  }
939
1027
 
1028
+ function Presence({ children }) {
1029
+ const active = Array.isArray(children) ? children.some(Boolean) : Boolean(children)
1030
+ const [retained, setRetained] = useState(children)
1031
+ useEffect(() => {
1032
+ if (active) { setRetained(children); return undefined }
1033
+ const timer = setTimeout(() => setRetained([]), 160)
1034
+ return () => clearTimeout(timer)
1035
+ }, [children, active])
1036
+ return h('div', { className: 'kd-presence' + (active ? '' : ' is-leaving') }, active ? children : retained)
1037
+ }
1038
+
940
1039
  function Modal({ title, children, footer, onClose, wide }) {
941
1040
  useEffect(() => {
942
1041
  const onKey = (event) => {
@@ -976,12 +1075,12 @@ window.__ModuleLoader__.load({
976
1075
  return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
977
1076
  }
978
1077
 
979
- /** 严格的三行倒计时。 */
1078
+ /** 剩余不超过 30 天时省略中间的时长行。 */
980
1079
  function CountdownLines({ countdown }) {
981
1080
  const lines = (countdown || {}).lines || {}
982
1081
  return h('div', { className: 'kd-cd-lines' },
983
1082
  h('div', { className: 'kd-cd-primary' }, lines.primary || ''),
984
- h('div', { className: 'kd-cd-secondary' }, lines.secondary || ''),
1083
+ lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
985
1084
  h('div', { className: 'kd-cd-target' }, lines.target || ''))
986
1085
  }
987
1086
 
@@ -1033,6 +1132,39 @@ window.__ModuleLoader__.load({
1033
1132
  }
1034
1133
 
1035
1134
  /* ============================== 导航 ============================== */
1135
+ function UpdateCheck({ currentVersion }) {
1136
+ const [state,setState]=useState({busy:true,result:null,error:''})
1137
+ const checking=useRef(false)
1138
+ const active=useRef(true)
1139
+ const check=async(force=false)=>{
1140
+ if(checking.current)return
1141
+ checking.current=true
1142
+ setState((value)=>({...value,busy:true,error:''}))
1143
+ try {
1144
+ const result=await rpc('checkUpdates',{force})
1145
+ if(active.current)setState({busy:false,result,error:''})
1146
+ } catch(error) {
1147
+ if(active.current)setState((value)=>({...value,busy:false,error:error.message}))
1148
+ } finally {checking.current=false}
1149
+ }
1150
+ useEffect(()=>{
1151
+ active.current=true
1152
+ const automatic=()=>{if(!window.document.hidden)check(false)}
1153
+ automatic()
1154
+ const timer=window.setInterval(automatic,6*60*60*1000)
1155
+ window.addEventListener('focus',automatic)
1156
+ return ()=>{active.current=false;window.clearInterval(timer);window.removeEventListener('focus',automatic)}
1157
+ },[])
1158
+ return h('div',{className:'kd-update'},
1159
+ h(Btn,{variant:'ghost',disabled:state.busy,onClick:()=>check(true)},
1160
+ state.busy?h('span',{className:'kd-spinner'}):h(Icon,{name:'refresh'}),state.busy?'正在检查…':'检查更新'),
1161
+ h('div',{className:'kd-msg-meta',style:{padding:'0 8px'}},`当前版本 v${currentVersion||state.result?.currentVersion||'—'}`),
1162
+ h('div',{className:'kd-msg-meta',role:'status','aria-live':'polite',title:state.error||'',style:{padding:'4px 8px',overflowWrap:'anywhere'}},
1163
+ state.error?'暂时无法检查,请重试':state.result?.updateAvailable
1164
+ ? h('a',{className:'kd-link',href:state.result.packageUrl,target:'_blank',rel:'noreferrer noopener'},`发现新版本 v${state.result.latestVersion} ↗`)
1165
+ : state.result?'已是最新版本':null))
1166
+ }
1167
+
1036
1168
  function SubjectNav({ boot, subjectId, onPick, onManage, onBanks }) {
1037
1169
  const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
1038
1170
  return h('div', { className: 'kd-nav' },
@@ -1058,7 +1190,8 @@ window.__ModuleLoader__.load({
1058
1190
  h('div', { className: 'kd-nav-group' },
1059
1191
  h('div', { className: 'kd-nav-group-label' }, '设置'),
1060
1192
  h(Btn, { variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus' }), T.manageSubjects),
1061
- h(Btn, { variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book' }), T.manageBanks)))
1193
+ h(Btn, { variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book' }), T.manageBanks),
1194
+ h(UpdateCheck,{currentVersion:boot.pluginVersion})))
1062
1195
  }
1063
1196
 
1064
1197
  function TabBar({ kind, counts, onPick }) {
@@ -1077,7 +1210,7 @@ window.__ModuleLoader__.load({
1077
1210
  }, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
1078
1211
  }
1079
1212
 
1080
- function MobileBar({ boot, subjectId, kind, onPick, onTab, onManage, onBanks }) {
1213
+ function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
1081
1214
  const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
1082
1215
  return h('div', { className: 'kd-mobilebar' },
1083
1216
  h('select', {
@@ -1086,13 +1219,6 @@ window.__ModuleLoader__.load({
1086
1219
  'aria-label': '切换科目',
1087
1220
  onChange: (event) => onPick(event.target.value),
1088
1221
  }, enabled.map((subject) => h('option', { key: subject.id, value: subject.id }, subject.name))),
1089
- ['today', 'all', 'favorite', 'wrong'].map((value) => h('button', {
1090
- key: value,
1091
- type: 'button',
1092
- className: `kd-nav-item${kind === value ? ' is-active' : ''}`,
1093
- style: { width: 'auto' },
1094
- onClick: () => onTab(value),
1095
- }, T.tabs[value])),
1096
1222
  // 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
1097
1223
  h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
1098
1224
  h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
@@ -1184,7 +1310,8 @@ window.__ModuleLoader__.load({
1184
1310
 
1185
1311
  function QuestionPane({
1186
1312
  q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
1187
- onRevise, revising, onToggleThread, threadOpen, narrow, onRemoveWrong,
1313
+ onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
1314
+ onCancel,
1188
1315
  }) {
1189
1316
  if (!q) return null
1190
1317
  const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
@@ -1198,9 +1325,9 @@ window.__ModuleLoader__.load({
1198
1325
  h('div', { className: 'kd-chips' },
1199
1326
  h('span', { className: 'kd-tag kd-tag-brand' }, q.typeLabel),
1200
1327
  h('span', { className: 'kd-tag' }, q.subjectName),
1201
- h('span', { className: 'kd-tag' }, `难度 ${q.difficulty}/5`),
1328
+ h('span', { className: 'kd-tag', title: q.examAlignment ? '独立评审参考历年真题给出的难度,不是考生正确率统计' : '原始难度标签,尚无真题对照评审' }, `难度 ${q.difficulty}/5`),
1202
1329
  h('span', { className: 'kd-tag' }, formatDate(q.assignedDate)),
1203
- h('span', { className: 'kd-tag' }, `作答 ${q.attemptCount} 次`),
1330
+ h('span', { className: 'kd-tag' }, `累计作答 ${q.attemptCount} 次`),
1204
1331
  h('span', { className: 'kd-tag' }, q.source?.kind === 'bank' ? q.source.label : '在线出题'),
1205
1332
  q.answerStatus === 'verified'
1206
1333
  ? h(Badge, { tone: 'ok' }, T.verified)
@@ -1212,6 +1339,17 @@ window.__ModuleLoader__.load({
1212
1339
  ? h('div', { className: 'kd-meta', style: { marginTop: '10px' } },
1213
1340
  h('span', null, `${T.knowledge}:${q.knowledge.join('、')}`))
1214
1341
  : null,
1342
+ q.examAlignment ? h('details', { className: 'kd-verify' },
1343
+ h('summary', null, `范围与难度依据 · 大纲 ${q.examAlignment.outlineYear} · 独立真题对照`),
1344
+ h('div', null, q.examAlignment.note),
1345
+ h('div', null, `参考考试:${q.examAlignment.basisSubject?.name || q.subjectName} · 检索时间:${formatDateTime(q.examAlignment.checkedAt)}`),
1346
+ ...(q.examAlignment.sources || []).map((source) => h('div', { key: source.id },
1347
+ h('a', { className: 'kd-link', href: source.url, target: '_blank', rel: 'noreferrer noopener' },
1348
+ `${source.year} ${source.title}(${source.official ? '官方来源' : source.kind === 'outline' ? source.verificationLabel || '非官方大纲转载' : '非官方真题转载'})`),
1349
+ source.coverage ? h('div', null, source.coverage) : null,
1350
+ source.publisherUrl ? h('a', { className:'kd-link', href:source.publisherUrl, target:'_blank', rel:'noreferrer noopener' }, `出版社版本记录 · ISBN ${source.isbn}`) : null)),
1351
+ revealed ? h('div', null, q.examAlignment.rationale,
1352
+ ...(q.examAlignment.warnings || []).map((warning,i)=>h('div',{key:i,className:'kd-meta'},warning))) : null) : null,
1215
1353
  h('div', { className: 'kd-stem' }, h(Markdown, { text: q.stem })),
1216
1354
  isChoice && q.options?.length
1217
1355
  ? h('div', { className: 'kd-options' }, q.options.map((option) => {
@@ -1255,10 +1393,9 @@ window.__ModuleLoader__.load({
1255
1393
  revealed
1256
1394
  ? h(Btn, { variant: 'ghost', onClick: onRedo, disabled: busy === 'reveal' }, h(Icon, { name: 'refresh', size: 13 }), T.redo)
1257
1395
  : null,
1258
- narrow
1259
- ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup)
1260
- : null),
1396
+ revealed ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup) : null),
1261
1397
  revealed ? h(AnswerBlock, { question: q, onRevise, revising, onRemoveWrong }) : null,
1398
+ revising && onCancel ? h(Btn,{variant:'ghost',onClick:onCancel},'取消修订') : null,
1262
1399
  revealed && last?.result === 'pending'
1263
1400
  ? h('div', { className: 'kd-actions', style: { marginTop: '12px' } },
1264
1401
  h('span', { className: 'kd-meta' }, '这道题由你自己判定(系统不会替你把“作答完毕”当成答对):'),
@@ -1270,7 +1407,7 @@ window.__ModuleLoader__.load({
1270
1407
  h(Badge, { tone: q.publishVerification.status === 'passed' ? 'ok' : 'warn' },
1271
1408
  q.publishVerification.status === 'passed' ? '试题发布前已通过独立校验' : '试题发布状态:待核验'),
1272
1409
  // 校验结论里通常写着答案与关键推导,提交答案前不能展示,否则等于泄题。
1273
- q.answerState === 'revealed'
1410
+ revealed
1274
1411
  ? ` ${q.publishVerification.summary}`
1275
1412
  : ' 结论在提交答案后显示',
1276
1413
  `(${methodLabel(q.publishVerification.method)}${q.publishVerification.at ? `,${formatDateTime(q.publishVerification.at)}` : ''})`)
@@ -1278,7 +1415,59 @@ window.__ModuleLoader__.load({
1278
1415
  }
1279
1416
 
1280
1417
  /* ============================== 列表 ============================== */
1281
- function ListPane({ list, kind, subjectName, onOpen }) {
1418
+ function previewFadePosition(element, lineHeight) {
1419
+ const bounds = element.getBoundingClientRect()
1420
+ const rects = []
1421
+ const document = element.ownerDocument
1422
+ const walker = document.createTreeWalker(element, 4) // 只测量正文文字,公式由 SVG 的外框参与定位。
1423
+ const range = document.createRange()
1424
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1425
+ if (!node.textContent.trim() || node.parentElement.closest('mjx-container,.kd-code-lang')) continue
1426
+ range.setStart(node, 0)
1427
+ range.setEnd(node, node.textContent.trimEnd().length)
1428
+ rects.push(...range.getClientRects())
1429
+ }
1430
+ for (const node of element.querySelectorAll('svg,img')) rects.push(node.getBoundingClientRect())
1431
+ const visible = rects.filter((rect) => rect.width > 0 && rect.height > 0
1432
+ && rect.top < bounds.bottom - 1 && rect.bottom > bounds.top
1433
+ && rect.left < bounds.right && rect.right > bounds.left)
1434
+ if (!visible.length) return null
1435
+ const lineOf = (rect) => Math.max(0, Math.min(1, Math.floor(((rect.top + rect.bottom) / 2 - bounds.top) / lineHeight)))
1436
+ const lastLine = Math.max(...visible.map(lineOf))
1437
+ const end = Math.min(bounds.width, Math.max(...visible.filter((rect) => lineOf(rect) === lastLine).map((rect) => rect.right - bounds.left)))
1438
+ return { top: lastLine * lineHeight, start: Math.max(0, end - 72), end }
1439
+ }
1440
+
1441
+ function QuestionPreview({ text }) {
1442
+ const ref = useRef(null)
1443
+ const [truncated, setTruncated] = useState(false)
1444
+ const mathStatus = useMathJaxStatus()
1445
+ useEffect(() => {
1446
+ const element = ref.current
1447
+ if (!element) return undefined
1448
+ const measure = () => {
1449
+ let lineHeight = 28
1450
+ for (const svg of element.querySelectorAll('svg')) lineHeight = Math.max(lineHeight, Math.ceil(svg.getBoundingClientRect().height + 16))
1451
+ element.style.setProperty('--kd-preview-line-height', `${lineHeight}px`)
1452
+ const content = element.firstElementChild
1453
+ const clipped = element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1
1454
+ || (content && content.scrollHeight > content.clientHeight + 1)
1455
+ const fade = clipped && previewFadePosition(element, lineHeight)
1456
+ if (fade) {
1457
+ for (const [key, value] of Object.entries(fade)) element.style.setProperty(`--kd-preview-fade-${key}`, `${value}px`)
1458
+ }
1459
+ setTruncated(Boolean(fade))
1460
+ }
1461
+ measure()
1462
+ const observer = new ResizeObserver(measure)
1463
+ observer.observe(element)
1464
+ if (element.firstElementChild) observer.observe(element.firstElementChild)
1465
+ return () => observer.disconnect()
1466
+ }, [text, mathStatus])
1467
+ return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
1468
+ }
1469
+
1470
+ function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, onPage, busy }) {
1282
1471
  if (!list) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '加载中…')
1283
1472
  if (!list.items.length) {
1284
1473
  const icon = kind === 'favorite' ? 'star' : kind === 'wrong' ? 'check' : kind === 'all' ? 'book' : 'spark'
@@ -1290,33 +1479,54 @@ window.__ModuleLoader__.load({
1290
1479
  }
1291
1480
  return h('div', null,
1292
1481
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1293
- h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`)),
1294
- list.items.map((item) => h('button', {
1482
+ h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.total ?? list.items.length} 道`),
1483
+ h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
1484
+ list.totalPages>1?h('div',{className:'kd-row',style:{marginBottom:'12px'},'aria-label':'题目分页'},
1485
+ h(Btn,{small:true,disabled:busy||list.page<=1,onClick:()=>onPage(list.page-1)},'上一页'),
1486
+ h('span',{className:'kd-meta'},`第 ${list.page} / ${list.totalPages} 页`),
1487
+ h(Btn,{small:true,disabled:busy||list.page>=list.totalPages,onClick:()=>onPage(list.page+1)},'下一页')):null,
1488
+ managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
1489
+ h('div', { className: 'kd-row' },
1490
+ h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
1491
+ h(Btn, { small: true, disabled: busy, onClick: () => onSelectAll(true) }, '全选'),
1492
+ h(Btn, { small: true, disabled: busy || !selected.length, onClick: () => onSelectAll(false) }, '全不选')),
1493
+ h('div', { className: 'kd-row' },
1494
+ ...[['favorite', '收藏'], ['unfavorite', '取消收藏'], ['wrong', '加入错题本'], ['unwrong', '移出错题本'], ['delete', '删除题目']].map(([action, label]) =>
1495
+ h(Btn, { key: action, small: true, variant: action === 'delete' ? 'danger' : 'default', disabled: busy || !selected.length, onClick: () => onBulk(action) }, label)))) : null,
1496
+ list.items.map((item) => h('div', {
1295
1497
  key: item.id,
1296
- type: 'button',
1297
- className: 'kd-list-row',
1298
- onClick: () => onOpen(item.id),
1498
+ className: 'kd-list-row' + (managing && selected.includes(item.id) ? ' is-selected' : ''),
1499
+ role: managing ? undefined : 'button',
1500
+ tabIndex: managing ? undefined : 0,
1501
+ onClick: managing ? undefined : (event) => { if (!event.target.closest('a,input')) onOpen(item.id) },
1502
+ onKeyDown: managing ? undefined : (event) => {
1503
+ if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); onOpen(item.id) }
1504
+ },
1299
1505
  },
1506
+ managing ? h('input', { type: 'checkbox', checked: selected.includes(item.id), disabled: busy,
1507
+ 'aria-label': '选择题目:' + (item.summary || item.id), onChange: () => onToggle(item.id) }) : null,
1300
1508
  h('div', { className: 'kd-list-body' },
1301
- h('div', { className: 'kd-list-summary' }, item.summary || '(无题干)'),
1509
+ h(QuestionPreview, { text: item.stem || item.summary || '(无题干)' }),
1302
1510
  h('div', { className: 'kd-list-meta' },
1303
1511
  h(Badge, null, item.typeLabel),
1304
1512
  h('span', null, formatDate(item.assignedDate)),
1305
1513
  h('span', null, `作答 ${item.attemptCount} 次`),
1306
1514
  item.favorite ? h(Badge, { tone: 'brand' }, '收藏') : null,
1307
- item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '曾答错 · 已掌握' : '错题') : null,
1515
+ item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '错题 · 已掌握' : '错题') : null,
1308
1516
  item.answerStatus !== 'verified' ? h(Badge, { tone: 'warn' }, T.unverified) : null,
1309
1517
  h('span', null, `难度 ${item.difficulty}/5`))),
1310
- h('span', { className: 'kd-iconbtn' }, h(Icon, { name: 'chevron' })))))
1518
+ !managing ? h('span', { className: 'kd-list-open', 'aria-hidden': true }, '查看题目', h(Icon, { name: 'chevron' })) : null)))
1311
1519
  }
1312
1520
 
1313
1521
  /* ============================== 追问侧栏 ============================== */
1314
- function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow }) {
1522
+ function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, onCancel, narrow, ratio = 33, onRatioChange }) {
1315
1523
  const [draft, setDraft] = useState('')
1316
1524
  const [sending, setSending] = useState(false)
1317
1525
  const [localError, setLocalError] = useState('')
1318
1526
  const [pending, setPending] = useState(null)
1319
1527
  const scrollRef = useRef(null)
1528
+ const sendingRef = useRef(false)
1529
+ const sendingMessage = useRef(null)
1320
1530
  const messages = thread?.messages || []
1321
1531
 
1322
1532
  useEffect(() => {
@@ -1326,7 +1536,9 @@ window.__ModuleLoader__.load({
1326
1536
 
1327
1537
  const send = useCallback(async (text, messageId) => {
1328
1538
  const value = String(text || '').trim()
1329
- if (!value) return
1539
+ if (!value || sendingRef.current) return
1540
+ sendingRef.current = true
1541
+ sendingMessage.current = messageId
1330
1542
  setSending(true)
1331
1543
  setLocalError('')
1332
1544
  try {
@@ -1337,6 +1549,8 @@ window.__ModuleLoader__.load({
1337
1549
  setLocalError(error.message)
1338
1550
  setPending({ id: messageId, text: value })
1339
1551
  } finally {
1552
+ sendingRef.current = false
1553
+ sendingMessage.current = null
1340
1554
  setSending(false)
1341
1555
  }
1342
1556
  }, [onSend])
@@ -1372,11 +1586,30 @@ window.__ModuleLoader__.load({
1372
1586
  h('span', { className: 'kd-spinner' }), T.sending))
1373
1587
  }
1374
1588
 
1375
- return h('aside', { className: 'kd-thread', 'aria-label': T.followup },
1589
+ const resize = (value) => onRatioChange(Math.max(20, Math.min(55, value)))
1590
+ return h('aside', { className: 'kd-thread', 'aria-label': T.followup, style: { '--kd-thread-ratio': ratio / 100 } },
1591
+ !narrow ? h('div', { className: 'kd-thread-resize', role: 'separator', tabIndex: 0,
1592
+ 'aria-label': '调整题目与追问的宽度比例', 'aria-orientation': 'vertical',
1593
+ 'aria-valuemin': 20, 'aria-valuemax': 55, 'aria-valuenow': Math.round(ratio),
1594
+ onPointerDown: (event) => { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId) },
1595
+ onPointerMove: (event) => {
1596
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
1597
+ const pane = event.currentTarget.parentElement
1598
+ const left = pane.closest('.kd-shell').querySelector('.kd-main').getBoundingClientRect().left
1599
+ const right = pane.getBoundingClientRect().right
1600
+ resize((right - event.clientX) / (right - left) * 100)
1601
+ },
1602
+ onPointerUp: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
1603
+ onPointerCancel: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
1604
+ onKeyDown: (event) => {
1605
+ const value = { ArrowLeft: ratio + 2, ArrowRight: ratio - 2, Home: 20, End: 55 }[event.key]
1606
+ if (value !== undefined) { event.preventDefault(); resize(value) }
1607
+ },
1608
+ }) : null,
1376
1609
  h('div', { className: 'kd-thread-head' },
1377
1610
  h('div', null,
1378
1611
  h('div', { className: 'kd-thread-title' }, T.followup),
1379
- h('div', { className: 'kd-msg-meta' }, `题目 ${String(questionId || '').slice(0, 14)}`)),
1612
+ h('div', { className: 'kd-msg-meta' }, '围绕当前题目,一起理清思路')),
1380
1613
  h('div', { className: 'kd-row' },
1381
1614
  h(Btn, {
1382
1615
  small: true,
@@ -1385,9 +1618,7 @@ window.__ModuleLoader__.load({
1385
1618
  onClick: onClear,
1386
1619
  title: '只清空本题的追问记录',
1387
1620
  }, T.clearThread),
1388
- narrow
1389
- ? h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' }))
1390
- : null)),
1621
+ h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' })))),
1391
1622
  h('div', { className: 'kd-thread-hint' }, T.followupHint),
1392
1623
  h('div', { className: 'kd-thread-scroll', ref: scrollRef },
1393
1624
  rows.length ? rows : h('div', { className: 'kd-empty-body' }, '还没有追问。可以问某一步推导为什么成立、某个知识点怎么理解,或有没有别的解法。')),
@@ -1397,17 +1628,18 @@ window.__ModuleLoader__.load({
1397
1628
  h('textarea', {
1398
1629
  className: 'kd-textarea',
1399
1630
  value: draft,
1400
- placeholder: '围绕这道题继续追问…(Ctrl+Enter 发送)',
1631
+ placeholder: '围绕这道题继续追问…(Enter 发送,Shift+Enter 换行)',
1401
1632
  disabled: sending,
1402
1633
  onChange: (event) => setDraft(event.target.value),
1403
1634
  onKeyDown: (event) => {
1404
- if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
1635
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && !event.nativeEvent?.isComposing && event.keyCode !== 229) {
1405
1636
  event.preventDefault()
1406
- send(draft, newAttemptId())
1637
+ if (!event.repeat) send(draft, newAttemptId())
1407
1638
  }
1408
1639
  },
1409
1640
  }),
1410
1641
  h('div', { className: 'kd-row', style: { justifyContent: 'flex-end', marginTop: '2px' } },
1642
+ sending && onCancel ? h(Btn,{small:true,variant:'ghost',onClick:()=>onCancel(sendingMessage.current)},'取消回复') : null,
1411
1643
  h(Btn, {
1412
1644
  variant: 'primary',
1413
1645
  small: true,
@@ -1417,12 +1649,40 @@ window.__ModuleLoader__.load({
1417
1649
  }
1418
1650
 
1419
1651
  /* ============================== 弹窗 ============================== */
1420
- function SubjectsModal({ boot, onClose, onAdd, onRemove, onSource, onPick }) {
1652
+ function SubjectsModal({ boot, onClose, onDone }) {
1653
+ const [draft, setDraft] = useState(() => ({ subjects: [...boot.settings.subjects],
1654
+ subjectSource: { ...boot.settings.subjectSource }, customSubjects: structuredClone(boot.customSubjects || []) }))
1655
+ const [saving, setSaving] = useState(false)
1656
+ const [saveError, setSaveError] = useState('')
1421
1657
  const [name, setName] = useState('')
1422
1658
  const [types, setTypes] = useState([])
1423
1659
  const [customTypes, setCustomTypes] = useState([])
1424
1660
  const [typeDraft, setTypeDraft] = useState('')
1425
- const enabled = new Set(boot.settings.subjects)
1661
+ const [referenceSubjectId, setReferenceSubjectId] = useState('auto')
1662
+ const [referenceExamName, setReferenceExamName] = useState('')
1663
+ const [referenceExamCode, setReferenceExamCode] = useState('')
1664
+ const [subjectCode, setSubjectCode] = useState('')
1665
+ const referenceExams = boot.referenceExams || (boot.subjects || []).filter((s) => !s.custom)
1666
+ const referenceEditor = (value, change, label) => h('div', { className:'kd-field', style:{ marginBottom:0 } },
1667
+ h('select', { className:'kd-select', 'aria-label':label, value:value.referenceSubjectId || 'auto',
1668
+ onChange:(event)=>change({referenceSubjectId:event.target.value}) },
1669
+ h('option', {value:'auto'}, '自动检索与核验(按科目名称和代码)'),
1670
+ ...referenceExams.map((exam)=>h('option',{key:exam.id,value:exam.id},`${exam.name}${exam.code ? `(${exam.code})` : ''}`)),
1671
+ h('option',{value:'independent'},'另行指定参考考试名称…')),
1672
+ !value.referenceSubjectId || value.referenceSubjectId === 'auto' ? h('input',{className:'kd-input','aria-label':`${label}代码(考试科目或专业)`,
1673
+ placeholder:'考试科目或专业代码(选填)',value:value.code || '',maxLength:20,onChange:(event)=>change({code:event.target.value})}) : null,
1674
+ value.referenceSubjectId === 'independent' ? h('div',{className:'kd-row'},
1675
+ h('input',{className:'kd-input','aria-label':`${label}名称`,placeholder:'考试名称,院校自命题请包含院校',value:value.referenceExamName || '',maxLength:100,
1676
+ onChange:(event)=>change({referenceExamName:event.target.value})}),
1677
+ h('input',{className:'kd-input','aria-label':`${label}代码`,placeholder:'科目代码(选填)',value:value.referenceExamCode || '',maxLength:20,style:{width:'150px'},
1678
+ onChange:(event)=>change({referenceExamCode:event.target.value})})) : null)
1679
+ const enabled = new Set(draft.subjects)
1680
+ const subjects = [...(boot.subjects || []), ...draft.customSubjects.filter((item) => !(boot.subjects || []).some((s) => s.id === item.id)).map((item) => ({
1681
+ ...item, custom: true, typeLabels: item.types.map((type) => ({ single: '单项选择题', multiple: '多项选择题', fill: '填空题', solution: '解答题' })[type] || type), bankItemCount: 0,
1682
+ }))]
1683
+ const close = () => { if (!saving) onClose() }
1684
+ const toggleSubject = (id) => setDraft((current) => ({ ...current,
1685
+ subjects: current.subjects.includes(id) ? current.subjects.filter((value) => value !== id) : [...current.subjects, id] }))
1426
1686
  const toggleType = (type) => setTypes((current) => current.includes(type)
1427
1687
  ? current.filter((item) => item !== type)
1428
1688
  : [...current, type])
@@ -1437,35 +1697,63 @@ window.__ModuleLoader__.load({
1437
1697
  setCustomTypes((current) => current.filter((item) => item !== value))
1438
1698
  setTypes((current) => current.filter((item) => item !== value))
1439
1699
  }
1440
- const canAdd = Boolean(name.trim()) && types.length > 0
1700
+ const selectedTypes = [...new Set([...types, ...(typeDraft.trim() ? [typeDraft.trim().slice(0, 24)] : [])])]
1701
+ const canAdd = Boolean(name.trim()) && selectedTypes.length > 0 && (referenceSubjectId !== 'independent' || Boolean(referenceExamName.trim()))
1702
+ const withCustomSubject = (current) => {
1703
+ const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), code:subjectCode.trim(), types: selectedTypes, referenceSubjectId, referenceExamName:referenceExamName.trim(), referenceExamCode:referenceExamCode.trim() }
1704
+ return { ...current, subjects: [...current.subjects, subject.id], customSubjects: [...current.customSubjects, subject] }
1705
+ }
1706
+ const finish = async () => {
1707
+ if (saving) return
1708
+ if ((name.trim() || types.length || typeDraft.trim() || (referenceSubjectId && referenceSubjectId !== 'auto') || referenceExamName.trim() || referenceExamCode.trim() || subjectCode.trim()) && !canAdd) {
1709
+ setSaveError('请填写自定义科目名称并至少选择一个题型;选择独立参考考试时,还需填写考试名称。或清空未完成的输入。')
1710
+ return
1711
+ }
1712
+ const next = canAdd ? withCustomSubject(draft) : draft
1713
+ if (next.customSubjects.some((s)=>s.referenceSubjectId === 'independent' && !s.referenceExamName?.trim())) {
1714
+ setSaveError('独立参考考试请填写准确的考试名称(院校自命题请包含院校名称)。')
1715
+ return
1716
+ }
1717
+ setSaving(true)
1718
+ setSaveError('')
1719
+ try { await onDone(next) }
1720
+ catch (error) { setSaveError(error.message || '保存失败,请重试') }
1721
+ finally { setSaving(false) }
1722
+ }
1441
1723
  const submit = () => {
1442
1724
  if (!canAdd) return
1443
- onAdd({ name: name.trim(), types })
1725
+ setDraft(withCustomSubject)
1444
1726
  setName('')
1445
1727
  setTypes([])
1446
1728
  setCustomTypes([])
1447
1729
  setTypeDraft('')
1730
+ setReferenceSubjectId('auto')
1731
+ setReferenceExamName('')
1732
+ setReferenceExamCode('')
1733
+ setSubjectCode('')
1448
1734
  }
1449
1735
  return h(Modal, {
1450
1736
  title: T.manageSubjects,
1451
- onClose,
1737
+ onClose: close,
1452
1738
  wide: true,
1453
1739
  footer: [
1454
- h(Btn, { key: 'close', onClick: onClose }, T.close),
1740
+ h(Btn, { key: 'cancel', onClick: close, disabled: saving }, T.cancel),
1741
+ h(Btn, { key: 'done', variant: 'primary', onClick: finish, disabled: saving }, saving ? '保存中…' : '完成'),
1455
1742
  ],
1456
1743
  },
1744
+ saveError ? h('div', { className: 'kd-error-text', role: 'alert' }, saveError) : null,
1745
+ h('fieldset', { disabled: saving, style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
1457
1746
  h('div', { className: 'kd-meta', style: { marginBottom: '12px' } },
1458
- '勾选要每天出题的科目。取消勾选只会移出每日出题,该科目已出过的题目、收藏、错题与作答记录都会保留。'),
1459
- (boot.subjects || []).map((subject) => h('div', {
1747
+ '修改后点击右下角「完成」统一保存;取消或关闭会放弃修改。停用科目不会删除历史题目、作答、收藏和追问。'),
1748
+ subjects.map((subject) => h('div', {
1460
1749
  key: subject.id,
1461
- className: 'kd-row-between',
1462
- style: { padding: '8px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
1750
+ className: 'kd-row-between kd-manage-card' + (enabled.has(subject.id) ? ' is-enabled' : ''),
1463
1751
  },
1464
1752
  h('label', { className: 'kd-checkline', style: { flex: '1 1 240px', cursor: 'pointer' } },
1465
1753
  h('input', {
1466
1754
  type: 'checkbox',
1467
1755
  checked: enabled.has(subject.id),
1468
- onChange: () => (enabled.has(subject.id) ? onRemove(subject) : onAdd(subject)),
1756
+ onChange: () => toggleSubject(subject.id),
1469
1757
  }),
1470
1758
  h('span', null,
1471
1759
  h('span', null, subject.name),
@@ -1473,27 +1761,31 @@ window.__ModuleLoader__.load({
1473
1761
  h('div', { className: 'kd-msg-meta' }, subject.typeLabels.join(' / ')))),
1474
1762
  enabled.has(subject.id)
1475
1763
  ? h('div', { className: 'kd-row' },
1764
+ subject.custom ? referenceEditor(draft.customSubjects.find((s)=>s.id === subject.id) || subject,
1765
+ (patch)=>setDraft((current)=>({...current,customSubjects:current.customSubjects.map((s)=>s.id === subject.id ? {...s,...patch} : s)})),`${subject.name} 参考考试科目`) : null,
1476
1766
  h('select', {
1477
1767
  className: 'kd-select',
1478
- value: subject.source,
1768
+ value: draft.subjectSource[subject.id] || 'auto',
1479
1769
  'aria-label': `${subject.name} 出题来源`,
1480
- onChange: (event) => onSource(subject.id, event.target.value),
1770
+ onChange: (event) => { const source = event.target.value; setDraft((current) => ({ ...current, subjectSource: { ...current.subjectSource, [subject.id]: source } })) },
1481
1771
  },
1482
1772
  h('option', { value: 'auto' }, '自动(优先题库)'),
1483
1773
  h('option', { value: 'online' }, '在线出题'),
1484
1774
  h('option', { value: 'bank' }, '题库出题')),
1485
1775
  subject.bankItemCount
1486
1776
  ? h('span', { className: 'kd-msg-meta' }, `题库 ${subject.bankItemCount} 题`)
1487
- : h('span', { className: 'kd-msg-meta' }, '题库为空'),
1488
- h(Btn, {
1489
- small: true,
1490
- variant: 'ghost',
1491
- onClick: () => { onClose(); onPick(subject.id) },
1492
- }, '查看'))
1777
+ : h('span', { className: 'kd-msg-meta' }, '题库为空'))
1493
1778
  : null)),
1494
1779
  h('div', { className: 'kd-divider' }),
1495
- h('div', { className: 'kd-field' },
1780
+ h('div', { className: 'kd-field kd-custom-subject' },
1496
1781
  h('div', { className: 'kd-field-label' }, '添加自定义科目'),
1782
+ referenceEditor({referenceSubjectId,referenceExamName,referenceExamCode,code:subjectCode},(patch)=>{
1783
+ if (patch.referenceSubjectId !== undefined) setReferenceSubjectId(patch.referenceSubjectId)
1784
+ if (patch.referenceExamName !== undefined) setReferenceExamName(patch.referenceExamName)
1785
+ if (patch.referenceExamCode !== undefined) setReferenceExamCode(patch.referenceExamCode)
1786
+ if (patch.code !== undefined) setSubjectCode(patch.code)
1787
+ },'自定义科目的参考考试'),
1788
+ h('div',{className:'kd-meta'},'默认由模型按名称和代码自主检索、独立核验大纲及两年真题。专业代码不等于考试科目代码;信息不足时可在「另行指定参考考试名称」补充院校和准确考试名称,资料不足不会发布。'),
1497
1789
  h('div', { className: 'kd-row' },
1498
1790
  h('input', {
1499
1791
  className: 'kd-input',
@@ -1543,7 +1835,7 @@ window.__ModuleLoader__.load({
1543
1835
  }),
1544
1836
  h(Btn, { small: true, disabled: !typeDraft.trim(), onClick: addCustomType }, '添加题型')),
1545
1837
  h('div', { className: 'kd-msg-meta', style: { marginTop: '6px' } },
1546
- canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加')))
1838
+ canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
1547
1839
  }
1548
1840
 
1549
1841
  function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport }) {
@@ -1601,8 +1893,7 @@ window.__ModuleLoader__.load({
1601
1893
  (boot.banks || []).length
1602
1894
  ? boot.banks.map((bank) => h('div', {
1603
1895
  key: bank.id,
1604
- className: 'kd-row-between',
1605
- style: { padding: '9px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
1896
+ className: 'kd-row-between kd-manage-card',
1606
1897
  },
1607
1898
  h('div', { style: { minWidth: 0 } },
1608
1899
  h('div', null, bank.name),
@@ -1647,18 +1938,47 @@ window.__ModuleLoader__.load({
1647
1938
  h('div', { className: 'kd-msg-meta' }, '确认后才会替换正式版本;旧版本会保留在版本历史中。'))
1648
1939
  }
1649
1940
 
1650
- function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger }) {
1941
+ function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger, busy }) {
1651
1942
  return h(Modal, {
1652
1943
  title,
1653
1944
  onClose,
1654
1945
  footer: [
1655
- h(Btn, { key: 'cancel', onClick: onClose }, T.cancel),
1656
- h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm }, confirmLabel || T.confirm),
1946
+ h(Btn, { key: 'cancel', onClick: onClose, disabled: busy }, T.cancel),
1947
+ h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm, disabled: busy }, busy ? '正在处理…' : (confirmLabel || T.confirm)),
1657
1948
  ],
1658
1949
  }, h('div', { className: 'kd-md' }, message))
1659
1950
  }
1660
1951
 
1661
1952
  /* ============================== 主页面 ============================== */
1953
+ function watchDayChange(snapshot, onSnapshot) {
1954
+ let timer, stopped = false, checking = false
1955
+ const schedule = (retry = false) => {
1956
+ const delay = Date.parse(snapshot.nextDayAt) - Date.parse(snapshot.now)
1957
+ timer = window.setTimeout(check, retry || !Number.isFinite(delay) ? 60_000 : Math.max(1000, delay))
1958
+ }
1959
+ const check = async () => {
1960
+ if (stopped || checking) return
1961
+ checking = true
1962
+ window.clearTimeout(timer)
1963
+ let retry = false
1964
+ try {
1965
+ const fresh = await rpc('bootstrap')
1966
+ if (!stopped) { snapshot = fresh; onSnapshot(fresh) }
1967
+ } catch { retry = true }
1968
+ finally { checking = false; if (!stopped) schedule(retry) }
1969
+ }
1970
+ const onVisible = () => { if (window.document.visibilityState === 'visible') check() }
1971
+ schedule()
1972
+ window.addEventListener('focus', check)
1973
+ window.document.addEventListener('visibilitychange', onVisible)
1974
+ return () => {
1975
+ stopped = true
1976
+ window.clearTimeout(timer)
1977
+ window.removeEventListener('focus', check)
1978
+ window.document.removeEventListener('visibilitychange', onVisible)
1979
+ }
1980
+ }
1981
+
1662
1982
  function Page() {
1663
1983
  const [boot, setBoot] = useState(null)
1664
1984
  const [loading, setLoading] = useState(true)
@@ -1670,15 +1990,34 @@ window.__ModuleLoader__.load({
1670
1990
  const [thread, setThread] = useState(null)
1671
1991
  const [list, setList] = useState(null)
1672
1992
  const [busy, setBusy] = useState('')
1993
+ const [managing, setManaging] = useState(false)
1994
+ const [selected, setSelected] = useState([])
1995
+ const bulkLock = useRef(false)
1673
1996
  const [fail, setFail] = useState('')
1674
1997
  const [toast, setToast] = useState(null)
1675
1998
  const [modal, setModal] = useState(null)
1676
1999
  const [choice, setChoice] = useState('')
1677
2000
  const [localRedo, setLocalRedo] = useState(false)
1678
2001
  const [threadOpen, setThreadOpen] = useState(false)
2002
+ const [threadRatio, setThreadRatio] = useState(33)
1679
2003
  const fileRef = useRef(null)
1680
2004
  const attemptRef = useRef(newAttemptId())
1681
2005
  const toastTimer = useRef(null)
2006
+ const mainScroll = useRef(null)
2007
+ const listScroll = useRef(0)
2008
+ const redoQuestions = useRef(new Set())
2009
+ const dailyRequest = useRef(0)
2010
+ const lastDay = useRef(null)
2011
+ const currentView = useRef(null)
2012
+ const bootRequest = useRef(0)
2013
+ const threadRequest = useRef(0)
2014
+ const selectionRequest = useRef(0)
2015
+ currentView.current = { subjectId, kind, view, page:list?.page||1, loading:busy==='list' }
2016
+
2017
+ useEffect(() => {
2018
+ if (view === 'list' && list && mainScroll.current) mainScroll.current.scrollTop = listScroll.current
2019
+ if (view === 'detail' && mainScroll.current) mainScroll.current.scrollTop = 0
2020
+ }, [view, list])
1682
2021
 
1683
2022
  const notify = useCallback((message, tone = 'ok') => {
1684
2023
  setToast({ message, tone })
@@ -1689,9 +2028,10 @@ window.__ModuleLoader__.load({
1689
2028
  useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current) }, [])
1690
2029
 
1691
2030
  const refreshBoot = useCallback(async () => {
2031
+ const request = ++bootRequest.current
1692
2032
  try {
1693
2033
  const result = await rpc('bootstrap')
1694
- setBoot(result)
2034
+ if (request === bootRequest.current) setBoot(result)
1695
2035
  return result
1696
2036
  } catch {
1697
2037
  return null
@@ -1700,8 +2040,10 @@ window.__ModuleLoader__.load({
1700
2040
 
1701
2041
  const loadToday = useCallback(async (sid) => {
1702
2042
  if (!sid) return
2043
+ const request = ++dailyRequest.current
1703
2044
  // 高亮必须跟着视图走:从「全部 / 收藏 / 错题本」切回今日题目时要一起复位
1704
2045
  setKind('today')
2046
+ setThreadOpen(false)
1705
2047
  setView('detail')
1706
2048
  setBusy('daily')
1707
2049
  setFail('')
@@ -1712,31 +2054,72 @@ window.__ModuleLoader__.load({
1712
2054
  attemptRef.current = newAttemptId()
1713
2055
  try {
1714
2056
  const result = await rpc('ensureDaily', { subjectId: sid })
1715
- setQuestion(result.question)
1716
- setThread(result.thread || null)
2057
+ if (request === dailyRequest.current) {
2058
+ setQuestion(result.question)
2059
+ setLocalRedo(redoQuestions.current.has(result.question.id))
2060
+ setThread(result.thread || null)
2061
+ }
2062
+ await refreshBoot()
1717
2063
  } catch (issue) {
1718
- setFail(issue.message)
2064
+ if (request === dailyRequest.current) setFail(issue.message)
1719
2065
  } finally {
1720
- setBusy('')
2066
+ if (request === dailyRequest.current) setBusy('')
1721
2067
  }
1722
- }, [])
2068
+ }, [refreshBoot])
2069
+
2070
+ useEffect(() => {
2071
+ if (!boot?.today) return undefined
2072
+ return watchDayChange(boot, setBoot)
2073
+ }, [boot?.today])
1723
2074
 
1724
- const loadList = useCallback(async (sid, nextKind) => {
2075
+ useEffect(() => {
2076
+ if (!boot?.today || lastDay.current === boot.today) return undefined
2077
+ const previous = lastDay.current
2078
+ lastDay.current = boot.today
2079
+ if (!previous) return undefined
2080
+ redoQuestions.current.clear()
2081
+ const { subjectId: sid, kind: activeKind } = currentView.current
2082
+ if (!sid) return undefined
2083
+ if (activeKind === 'today') { loadToday(sid); return undefined }
2084
+ let cancelled = false
2085
+ const request=dailyRequest.current
2086
+ rpc('ensureDaily', { subjectId: sid }).then(async () => {
2087
+ if (cancelled) return
2088
+ await refreshBoot()
2089
+ const current = currentView.current
2090
+ if (cancelled || request!==dailyRequest.current || current.loading || current.subjectId !== sid || current.kind === 'today' || current.view !== 'list') return
2091
+ const result = await rpc('listQuestions', { subjectId: sid, kind: current.kind, page:current.page })
2092
+ const latest = currentView.current
2093
+ if (!cancelled && request===dailyRequest.current && !latest.loading && latest.page===current.page && latest.subjectId === sid && latest.kind === current.kind && latest.view === 'list') {
2094
+ listScroll.current = mainScroll.current?.scrollTop || 0
2095
+ setList(result)
2096
+ }
2097
+ }).catch((issue) => { if (!cancelled) notify(`今日题目更新失败:${issue.message}`, 'warn') })
2098
+ return () => { cancelled = true }
2099
+ }, [boot?.today, loadToday, refreshBoot, notify])
2100
+
2101
+ const loadList = useCallback(async (sid, nextKind, page = 1) => {
1725
2102
  if (!sid) return
2103
+ const request = ++dailyRequest.current
2104
+ listScroll.current = 0
1726
2105
  setKind(nextKind)
2106
+ setThreadOpen(false)
1727
2107
  setView('list')
2108
+ setFail('')
1728
2109
  setBusy('list')
1729
2110
  setList(null)
1730
2111
  try {
1731
- const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind })
1732
- setList(result)
2112
+ const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind, page })
2113
+ if (request === dailyRequest.current) setList(result)
1733
2114
  } catch (issue) {
1734
- notify(issue.message, 'bad')
2115
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1735
2116
  } finally {
1736
- setBusy('')
2117
+ if (request === dailyRequest.current) setBusy('')
1737
2118
  }
1738
2119
  }, [notify])
1739
2120
 
2121
+ useEffect(() => { setSelected([]); setManaging(false) }, [subjectId, kind, view])
2122
+
1740
2123
  const pickSubject = useCallback((sid) => {
1741
2124
  if (!sid || sid === subjectId) return
1742
2125
  setSubjectId(sid)
@@ -1746,23 +2129,39 @@ window.__ModuleLoader__.load({
1746
2129
  }, [subjectId, loadToday])
1747
2130
 
1748
2131
  const openQuestion = useCallback(async (qid) => {
2132
+ const request = ++dailyRequest.current
2133
+ listScroll.current = mainScroll.current?.scrollTop || 0
1749
2134
  setBusy('question')
1750
2135
  try {
1751
2136
  const result = await rpc('getQuestion', { qid })
2137
+ if (request !== dailyRequest.current) return
2138
+ const canResume = result.isToday && result.question.answerState === 'revealed'
2139
+ && result.question.attemptCount > 0 && !redoQuestions.current.has(qid)
2140
+ if (!canResume) redoQuestions.current.add(qid)
1752
2141
  setQuestion(result.question)
1753
2142
  setThread(result.thread || null)
1754
2143
  setView('detail')
1755
- setLocalRedo(false)
2144
+ setLocalRedo(!canResume)
1756
2145
  setChoice('')
1757
2146
  setThreadOpen(false)
1758
2147
  attemptRef.current = newAttemptId()
1759
2148
  } catch (issue) {
1760
- notify(issue.message, 'bad')
2149
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1761
2150
  } finally {
1762
- setBusy('')
2151
+ if (request === dailyRequest.current) setBusy('')
1763
2152
  }
1764
2153
  }, [notify])
1765
2154
 
2155
+ const backToList = async () => {
2156
+ const request = ++dailyRequest.current
2157
+ setThreadOpen(false)
2158
+ setView('list')
2159
+ try {
2160
+ const result = await rpc('listQuestions', { subjectId, kind, page:list?.page||1 })
2161
+ if (request === dailyRequest.current) setList(result)
2162
+ } catch (issue) { if (request === dailyRequest.current) notify(issue.message, 'bad') }
2163
+ }
2164
+
1766
2165
  useEffect(() => {
1767
2166
  let cancelled = false
1768
2167
  rpc('bootstrap').then((result) => {
@@ -1789,6 +2188,7 @@ window.__ModuleLoader__.load({
1789
2188
 
1790
2189
  const reveal = useCallback(async (mode) => {
1791
2190
  if (!question) return
2191
+ const request = dailyRequest.current
1792
2192
  setBusy('reveal')
1793
2193
  try {
1794
2194
  const result = await rpc('reveal', {
@@ -1797,48 +2197,58 @@ window.__ModuleLoader__.load({
1797
2197
  choice: mode === 'submit' ? choice : '',
1798
2198
  attemptId: attemptRef.current,
1799
2199
  })
1800
- setQuestion(result.question)
1801
- setLocalRedo(false)
2200
+ if (request === dailyRequest.current) {
2201
+ setQuestion(result.question)
2202
+ redoQuestions.current.delete(question.id)
2203
+ setLocalRedo(false)
2204
+ }
1802
2205
  refreshBoot()
1803
2206
  } catch (issue) {
1804
2207
  notify(issue.message, 'bad')
1805
2208
  } finally {
1806
- setBusy('')
2209
+ if (request === dailyRequest.current) setBusy('')
1807
2210
  }
1808
2211
  }, [question, choice, notify, refreshBoot])
1809
2212
 
1810
2213
  const judge = useCallback(async (verdict) => {
1811
2214
  if (!question) return
2215
+ if (!question.lastAttempt?.id) { notify('缺少本次作答记录,请重新打开题目后判定','bad'); return }
2216
+ const request = dailyRequest.current
1812
2217
  setBusy('judge')
1813
2218
  try {
1814
- const result = await rpc('judge', { qid: question.id, verdict })
1815
- setQuestion(result.question)
2219
+ const result = await rpc('judge', { qid: question.id, verdict, attemptId:question.lastAttempt.id })
2220
+ if (request === dailyRequest.current) setQuestion(result.question)
1816
2221
  refreshBoot()
1817
2222
  } catch (issue) {
1818
- notify(issue.message, 'bad')
2223
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1819
2224
  } finally {
1820
- setBusy('')
2225
+ if (request === dailyRequest.current) setBusy('')
1821
2226
  }
1822
2227
  }, [question, notify, refreshBoot])
1823
2228
 
1824
2229
  const favorite = useCallback(async () => {
1825
2230
  if (!question) return
2231
+ const request = dailyRequest.current
1826
2232
  setBusy('favorite')
1827
2233
  try {
1828
2234
  const result = await rpc('favorite', { qid: question.id, value: !question.favorite })
1829
- setQuestion(result.question)
1830
- notify(question.favorite ? '已取消收藏' : '已加入收藏')
2235
+ if (request === dailyRequest.current) {
2236
+ setQuestion(result.question)
2237
+ notify(question.favorite ? '已取消收藏' : '已加入收藏')
2238
+ }
1831
2239
  refreshBoot()
1832
2240
  } catch (issue) {
1833
- notify(issue.message, 'bad')
2241
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1834
2242
  } finally {
1835
- setBusy('')
2243
+ if (request === dailyRequest.current) setBusy('')
1836
2244
  }
1837
2245
  }, [question, notify, refreshBoot])
1838
2246
 
1839
2247
  const sendFollowup = useCallback(async (text, messageId) => { if (!question) throw new Error('没有打开任何题目')
2248
+ const request = dailyRequest.current
2249
+ const threadVersion = ++threadRequest.current
1840
2250
  const result = await rpc('followup', { qid: question.id, message: text, clientMessageId: messageId })
1841
- setThread(result.thread)
2251
+ if (request === dailyRequest.current && threadVersion === threadRequest.current) setThread(result.thread)
1842
2252
  }, [question])
1843
2253
 
1844
2254
  const clearThread = useCallback(async () => {
@@ -1850,11 +2260,15 @@ window.__ModuleLoader__.load({
1850
2260
  confirmLabel: '清空',
1851
2261
  danger: true,
1852
2262
  onConfirm: async () => {
2263
+ const request = dailyRequest.current
2264
+ const threadVersion = ++threadRequest.current
1853
2265
  setModal(null)
1854
2266
  try {
1855
2267
  const result = await rpc('clearThread', { qid: question.id })
1856
- setThread(result.thread)
1857
- notify('本题追问已清空')
2268
+ if (request === dailyRequest.current && threadVersion === threadRequest.current) {
2269
+ setThread(result.thread)
2270
+ notify('本题追问已清空')
2271
+ }
1858
2272
  } catch (issue) {
1859
2273
  notify(issue.message, 'bad')
1860
2274
  }
@@ -1862,47 +2276,58 @@ window.__ModuleLoader__.load({
1862
2276
  })
1863
2277
  }, [question, notify])
1864
2278
 
2279
+ const cancelWork = async (kind, qid, clientMessageId) => {
2280
+ try { await rpc('cancelTask',{kind,subjectId,qid,clientMessageId}) }
2281
+ catch (issue) { notify(issue.message,'bad') }
2282
+ }
2283
+
1865
2284
  const revise = useCallback(async () => {
1866
2285
  if (!question) return
2286
+ const request = dailyRequest.current
1867
2287
  setBusy('revise')
1868
2288
  try {
1869
2289
  const result = await rpc('revise', { qid: question.id })
2290
+ if (request !== dailyRequest.current) return
1870
2291
  setQuestion(result.question)
1871
2292
  setModal({ kind: 'revision' })
1872
2293
  } catch (issue) {
1873
- notify(issue.message, 'bad')
2294
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1874
2295
  } finally {
1875
- setBusy('')
2296
+ if (request === dailyRequest.current) setBusy('')
1876
2297
  }
1877
2298
  }, [question, notify])
1878
2299
 
1879
2300
  const confirmRevision = useCallback(async () => {
1880
2301
  if (!question?.pendingRevision) return
2302
+ const request = dailyRequest.current
1881
2303
  setBusy('revise')
1882
2304
  try {
1883
2305
  const result = await rpc('confirmRevision', {
1884
2306
  qid: question.id,
1885
2307
  candidateId: question.pendingRevision.candidateId,
1886
2308
  })
2309
+ if (request !== dailyRequest.current) return
1887
2310
  setQuestion(result.question)
1888
2311
  setModal(null)
1889
2312
  notify('已替换为通过校验的新版本,旧版本已保留')
1890
2313
  } catch (issue) {
1891
- notify(issue.message, 'bad')
2314
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1892
2315
  } finally {
1893
- setBusy('')
2316
+ if (request === dailyRequest.current) setBusy('')
1894
2317
  }
1895
2318
  }, [question, notify])
1896
2319
 
1897
2320
  const discardRevision = useCallback(async () => {
1898
2321
  if (!question) return
2322
+ const request = dailyRequest.current
1899
2323
  setModal(null)
1900
2324
  try {
1901
2325
  const result = await rpc('discardRevision', { qid: question.id })
2326
+ if (request !== dailyRequest.current) return
1902
2327
  setQuestion(result.question)
1903
2328
  notify('已放弃候选版本,正式答案保持不变')
1904
2329
  } catch (issue) {
1905
- notify(issue.message, 'bad')
2330
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1906
2331
  }
1907
2332
  }, [question, notify])
1908
2333
 
@@ -1938,67 +2363,18 @@ window.__ModuleLoader__.load({
1938
2363
  }
1939
2364
  }, [boot, notify, refreshBoot])
1940
2365
 
1941
- const addSubject = useCallback(async (subject) => {
1942
- try {
1943
- let targetId = subject.id
1944
- if (!targetId) {
1945
- targetId = `custom_${Date.now().toString(36)}`
1946
- await rpc('saveSettings', {
1947
- customSubjects: [...(boot?.customSubjects || []), {
1948
- id: targetId,
1949
- name: subject.name,
1950
- types: subject.types,
1951
- }],
1952
- })
1953
- }
1954
- const result = await rpc('addSubject', { subjectId: targetId })
1955
- const fresh = await refreshBoot()
1956
- notify(`${subject.name} 已启用,系统会为它分配今天的题目`)
1957
- if (fresh && result) {
1958
- setSubjectId(targetId)
1959
- loadToday(targetId)
1960
- }
1961
- setModal(null)
1962
- } catch (issue) {
1963
- notify(issue.message, 'bad')
1964
- }
1965
- }, [boot, notify, refreshBoot, loadToday])
1966
-
1967
- const removeSubject = useCallback((subject) => {
1968
- setModal({
1969
- kind: 'confirm',
1970
- title: `${T.deleteSubject}:${subject.name}`,
1971
- message: `确认把「${subject.name}」移出每日出题吗?\n\n- 该科目已出过的题目、收藏、错题与作答记录都会保留在“全部”里。\n- 今天已经分配的题目不会丢失,重新启用后当天仍会复用同一道题。`,
1972
- confirmLabel: T.deleteSubject,
1973
- danger: true,
1974
- onConfirm: async () => {
1975
- setModal(null)
1976
- try {
1977
- const result = await rpc('removeSubject', { subjectId: subject.id })
1978
- const fresh = await refreshBoot()
1979
- notify(result.message || '已移出每日出题')
1980
- if (fresh) {
1981
- const next = fresh.settings.subjects[0] || null
1982
- setSubjectId(next)
1983
- if (next) loadToday(next)
1984
- else { setQuestion(null); setView('empty') }
1985
- }
1986
- } catch (issue) {
1987
- notify(issue.message, 'bad')
1988
- }
1989
- },
1990
- })
1991
- }, [notify, refreshBoot, loadToday])
1992
-
1993
- const changeSource = useCallback(async (id, source) => {
1994
- try {
1995
- await rpc('saveSettings', { subjectSource: { [id]: source } })
1996
- await refreshBoot()
1997
- notify('出题来源已更新')
1998
- } catch (issue) {
1999
- notify(issue.message, 'bad')
2366
+ const saveSubjects = async (draft) => {
2367
+ const next = draft.subjects.includes(subjectId) ? subjectId : (draft.subjects[0] || null)
2368
+ const result = await rpc('saveSettings', { ...draft, activeSubject: next })
2369
+ setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
2370
+ setModal(null)
2371
+ notify('科目设置已保存')
2372
+ if (next !== subjectId) {
2373
+ setSubjectId(next)
2374
+ if (next) loadToday(next)
2375
+ else { setQuestion(null); setThread(null); setThreadOpen(false); setView('empty') }
2000
2376
  }
2001
- }, [notify, refreshBoot])
2377
+ }
2002
2378
 
2003
2379
  const pickFile = useCallback(() => {
2004
2380
  if (fileRef.current) {
@@ -2063,7 +2439,54 @@ window.__ModuleLoader__.load({
2063
2439
  })
2064
2440
  }, [notify, refreshBoot])
2065
2441
 
2442
+ const bulkQuestions = async (action) => {
2443
+ if (bulkLock.current || !selected.length) return
2444
+ const request = dailyRequest.current
2445
+ const qids = [...selected]
2446
+ const execute = async (confirmProtected = false) => {
2447
+ if (bulkLock.current || request !== dailyRequest.current) return
2448
+ bulkLock.current = true
2449
+ setBusy('bulk')
2450
+ try {
2451
+ const result = await rpc('bulkQuestions', { qids, action, confirmProtected })
2452
+ if (request !== dailyRequest.current) { refreshBoot(); return }
2453
+ if (result.requiresConfirmation) {
2454
+ setModal({ kind: 'confirm', title: '再次确认删除收藏或错题', danger: true,
2455
+ message: `选中的题目中有 ${result.protectedCount} 道已收藏或在错题本中。继续将永久删除所选 ${qids.length} 道题及其作答和追问,无法恢复。确定仍要删除吗?`,
2456
+ confirmLabel: '仍要永久删除', onConfirm: () => execute(true) })
2457
+ return
2458
+ }
2459
+ setModal(null)
2460
+ setSelected([])
2461
+ const loading = loadList(subjectId, kind, list?.page||1)
2462
+ const listRequest = dailyRequest.current
2463
+ await loading
2464
+ await refreshBoot()
2465
+ if (listRequest === dailyRequest.current) notify(`已处理 ${result.affected} 道题目`)
2466
+ } catch (issue) {
2467
+ if (request === dailyRequest.current) { setModal(null); notify(issue.message, 'bad') }
2468
+ } finally { bulkLock.current = false; if (request === dailyRequest.current) setBusy('') }
2469
+ }
2470
+ if (action === 'delete') {
2471
+ setModal({ kind: 'confirm', title: '删除所选题目', danger: true,
2472
+ message: `将永久删除 ${qids.length} 道题及其作答、收藏、错题标记和追问,无法恢复。题库原文件仍会保留,未来可能重新抽到其中的题目。`,
2473
+ confirmLabel: '确认删除', onConfirm: () => execute() })
2474
+ } else await execute()
2475
+ }
2476
+
2066
2477
  const counts = subjectId ? boot?.counts?.[subjectId] : null
2478
+ const selectAllQuestions = async (value) => {
2479
+ if(bulkLock.current)return
2480
+ const request=dailyRequest.current,selection=++selectionRequest.current
2481
+ const isCurrent=()=>request===dailyRequest.current&&selection===selectionRequest.current
2482
+ if(!value){setSelected([]);setBusy(current=>current==='selection'?'':current);return}
2483
+ setBusy('selection')
2484
+ try {
2485
+ const result=await rpc('listQuestions',{subjectId,kind,idsOnly:true})
2486
+ if(isCurrent())setSelected(result.ids)
2487
+ } catch(issue){if(isCurrent())notify(issue.message,'bad')}
2488
+ finally{if(isCurrent())setBusy(current=>current==='selection'?'':current)}
2489
+ }
2067
2490
  const narrow = useMediaQuery('(max-width: 1180px)')
2068
2491
 
2069
2492
  const mainContent = (() => {
@@ -2096,7 +2519,8 @@ window.__ModuleLoader__.load({
2096
2519
  h('span', { className: 'kd-spinner' }),
2097
2520
  T.generating,
2098
2521
  h('div', { className: 'kd-msg-meta', style: { marginTop: '8px' } },
2099
- '题目会先生成,再由另一个独立视角重新求解并校验;校验不通过就不会发布。首次出题可能需要几十秒。'))
2522
+ '先出题,再仅凭题干和选项盲解并比对答案;分歧交由主裁,仍无法确定就重新出题或阻止发布。首次出题可能需要较长时间。'),
2523
+ h(Btn,{variant:'ghost',onClick:()=>cancelWork('daily')},'取消生成'))
2100
2524
  }
2101
2525
  if (fail) {
2102
2526
  return h(EmptyState, {
@@ -2127,6 +2551,12 @@ window.__ModuleLoader__.load({
2127
2551
  kind,
2128
2552
  subjectName: currentSubject?.name || '',
2129
2553
  onOpen: openQuestion,
2554
+ managing, selected, busy: busy === 'bulk' || busy === 'selection',
2555
+ onManage: () => { setManaging(!managing); setSelected([]) },
2556
+ onToggle: (id) => setSelected((ids) => ids.includes(id) ? ids.filter((value) => value !== id) : [...ids, id]),
2557
+ onSelectAll: selectAllQuestions,
2558
+ onPage: (page) => loadList(subjectId,kind,page),
2559
+ onBulk: bulkQuestions,
2130
2560
  })
2131
2561
  }
2132
2562
  if (view === 'detail' && question) {
@@ -2135,7 +2565,6 @@ window.__ModuleLoader__.load({
2135
2565
  busy,
2136
2566
  choice,
2137
2567
  localRedo,
2138
- narrow,
2139
2568
  threadOpen,
2140
2569
  onSelect: (key) => {
2141
2570
  setChoice((current) => {
@@ -2150,22 +2579,30 @@ window.__ModuleLoader__.load({
2150
2579
  onJudge: judge,
2151
2580
  onFavorite: favorite,
2152
2581
  onRedo: () => {
2582
+ dailyRequest.current++
2583
+ setBusy('')
2584
+ redoQuestions.current.add(question.id)
2153
2585
  setLocalRedo(true)
2586
+ setThreadOpen(false)
2154
2587
  setChoice('')
2155
2588
  attemptRef.current = newAttemptId()
2156
2589
  },
2157
2590
  onRevise: revise,
2591
+ onCancel: () => cancelWork('revise',question.id),
2158
2592
  revising: busy === 'revise',
2159
2593
  onToggleThread: () => setThreadOpen((value) => !value),
2160
2594
  onRemoveWrong: async () => {
2161
2595
  if (!question) return
2596
+ const request = dailyRequest.current
2162
2597
  try {
2163
2598
  const result = await rpc('removeFromWrongBook', { qid: question.id })
2164
- setQuestion(result.question)
2165
- notify('已移出错题本,历史作答记录仍然保留')
2599
+ if (request === dailyRequest.current) {
2600
+ setQuestion(result.question)
2601
+ notify('已移出错题本,历史作答记录仍然保留')
2602
+ }
2166
2603
  refreshBoot()
2167
2604
  } catch (issue) {
2168
- notify(issue.message, 'bad')
2605
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2169
2606
  }
2170
2607
  },
2171
2608
  })
@@ -2177,7 +2614,13 @@ window.__ModuleLoader__.load({
2177
2614
  })
2178
2615
  })()
2179
2616
 
2180
- const showThread = view === 'detail' && question && (!narrow || threadOpen)
2617
+ const pickTab = (value) => {
2618
+ if (value === kind && (value === 'today' || view === 'list')) return
2619
+ if (value === 'today') loadToday(subjectId)
2620
+ else loadList(subjectId, value)
2621
+ }
2622
+ const showThread = view === 'detail' && question && threadOpen && !localRedo
2623
+ && question.answerState === 'revealed' && question.attemptCount > 0
2181
2624
 
2182
2625
  return h('div', { className: 'kd-page' },
2183
2626
  styleInjected ? null : h('style', null, CSS),
@@ -2187,10 +2630,10 @@ window.__ModuleLoader__.load({
2187
2630
  h('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
2188
2631
  h('div', { style: { minWidth: 0 } },
2189
2632
  h('h1', { className: 'kd-title' }, T.title),
2190
- h('div', { className: 'kd-sub' },
2633
+ h('div', { className: 'kd-sub', title: T.subtitle },
2191
2634
  boot?.today ? `${formatDate(boot.today)} · ` : '',
2192
2635
  boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
2193
- T.subtitle))),
2636
+ '每天一道,稳步向前'))),
2194
2637
  boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
2195
2638
  boot ? h(ExamStrip, {
2196
2639
  boot,
@@ -2201,14 +2644,9 @@ window.__ModuleLoader__.load({
2201
2644
  boot ? h(MobileBar, {
2202
2645
  boot,
2203
2646
  subjectId,
2204
- kind,
2205
2647
  onPick: pickSubject,
2206
2648
  onManage: () => setModal({ kind: 'subjects' }),
2207
2649
  onBanks: () => setModal({ kind: 'banks' }),
2208
- onTab: (value) => {
2209
- if (value === 'today') loadToday(subjectId)
2210
- else loadList(subjectId, value)
2211
- },
2212
2650
  }) : null,
2213
2651
  h('div', { className: 'kd-shell' },
2214
2652
  boot ? h(SubjectNav, {
@@ -2219,27 +2657,31 @@ window.__ModuleLoader__.load({
2219
2657
  onBanks: () => setModal({ kind: 'banks' }),
2220
2658
  }) : null,
2221
2659
  h('main', { className: 'kd-main' },
2222
- h('div', { className: 'kd-main-inner' },
2660
+ h('div', { className: 'kd-main-toolbar' },
2223
2661
  boot ? h(TabBar, {
2224
2662
  kind,
2225
2663
  counts,
2226
- onPick: (value) => {
2227
- if (value === 'today') loadToday(subjectId)
2228
- else loadList(subjectId, value)
2229
- },
2664
+ onPick: pickTab,
2230
2665
  }) : null,
2231
- mainContent)),
2232
- showThread
2666
+ kind !== 'today' && view === 'detail'
2667
+ ? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
2668
+ h('div', { className: 'kd-main-scroll', ref: mainScroll },
2669
+ h('div', { className: 'kd-main-inner' },
2670
+ h('div', { className: 'kd-view', key: `${subjectId}|${kind}|${view}|${question?.id || ''}` }, mainContent)))),
2671
+ h(Presence, null, showThread
2233
2672
  ? h(ThreadPane, {
2234
2673
  thread,
2235
2674
  questionId: question.id,
2236
2675
  busy,
2237
2676
  narrow,
2677
+ ratio: threadRatio,
2678
+ onRatioChange: setThreadRatio,
2238
2679
  onSend: sendFollowup,
2239
2680
  onClear: clearThread,
2681
+ onCancel: (messageId) => cancelWork('followup',question.id,messageId),
2240
2682
  onClose: () => setThreadOpen(false),
2241
2683
  })
2242
- : null),
2684
+ : null)),
2243
2685
  h('input', {
2244
2686
  ref: fileRef,
2245
2687
  type: 'file',
@@ -2247,14 +2689,12 @@ window.__ModuleLoader__.load({
2247
2689
  style: { display: 'none' },
2248
2690
  onChange: onFile,
2249
2691
  }),
2692
+ h(Presence, null,
2250
2693
  modal?.kind === 'subjects' && boot
2251
2694
  ? h(SubjectsModal, {
2252
2695
  boot,
2253
2696
  onClose: () => setModal(null),
2254
- onAdd: addSubject,
2255
- onRemove: removeSubject,
2256
- onSource: changeSource,
2257
- onPick: (id) => { setModal(null); pickSubject(id) },
2697
+ onDone: saveSubjects,
2258
2698
  })
2259
2699
  : null,
2260
2700
  modal?.kind === 'banks' && boot
@@ -2276,13 +2716,16 @@ window.__ModuleLoader__.load({
2276
2716
  onDiscard: discardRevision,
2277
2717
  }) : null,
2278
2718
  modal?.kind === 'confirm' ? h(ConfirmModal, {
2719
+ key: modal.title,
2279
2720
  title: modal.title,
2280
2721
  message: modal.message,
2281
2722
  confirmLabel: modal.confirmLabel,
2282
2723
  danger: modal.danger,
2724
+ busy: busy === 'bulk',
2283
2725
  onConfirm: modal.onConfirm,
2284
- onClose: () => setModal(null),
2726
+ onClose: () => { if (!bulkLock.current) setModal(null) },
2285
2727
  }) : null,
2728
+ ),
2286
2729
  toast ? h('div', { className: `kd-toast kd-toast-${toast.tone}` }, toast.message) : null,
2287
2730
  busy === 'import' || busy === 'commit'
2288
2731
  ? h('div', { className: 'kd-loading', style: { position: 'absolute', inset: 'auto 0 60px 0' } },