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/CHANGELOG.md +32 -0
- package/LICENSE +21 -21
- package/README.md +251 -114
- package/client.js +804 -361
- package/index.js +2 -2
- package/lib/async.js +17 -0
- package/lib/bank.js +3 -3
- package/lib/exam-dates.js +10 -8
- package/lib/generate.js +124 -104
- package/lib/generation-history.js +67 -0
- package/lib/llm-json.js +17 -8
- package/lib/pdf-worker.js +5 -0
- package/lib/pdf.js +156 -34
- package/lib/reference-reader.js +178 -0
- package/lib/reference-sources.js +83 -0
- package/lib/references.js +159 -0
- package/lib/research.js +166 -0
- package/lib/routes.js +3 -0
- package/lib/service.js +253 -60
- package/lib/store.js +3 -3
- package/lib/subjects.js +19 -0
- package/lib/tutor.js +6 -5
- package/lib/updates.js +49 -0
- package/package.json +4 -2
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 {
|
|
547
|
-
|
|
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(--
|
|
551
|
-
.kd-header { padding:
|
|
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:
|
|
554
|
-
.kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:
|
|
555
|
-
.kd-brand { flex:0 0
|
|
556
|
-
justify-content:center; border:
|
|
557
|
-
color:var(--
|
|
558
|
-
.kd-title { margin:0; font-size:
|
|
559
|
-
.kd-sub { margin-top:3px; font-size:12px; color:var(--
|
|
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(--
|
|
562
|
-
color:var(--
|
|
563
|
-
.kd-select:hover { border-color:var(--
|
|
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:
|
|
568
|
-
.kd-cd-secondary { font-size:12px; color:var(--
|
|
569
|
-
.kd-cd-target { font-size:
|
|
570
|
-
.kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px
|
|
571
|
-
border:1px solid var(--
|
|
572
|
-
font-size:11.5px; color:var(--
|
|
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(--
|
|
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:
|
|
577
|
-
padding:
|
|
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(--
|
|
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:
|
|
582
|
-
width:100%; padding:
|
|
583
|
-
cursor:pointer; color:var(--
|
|
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(--
|
|
586
|
-
.kd-nav-item > span:first-child { min-width:0; overflow
|
|
587
|
-
.kd-nav-item.is-active { background:var(--
|
|
588
|
-
|
|
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(--
|
|
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(--
|
|
593
|
-
.kd-dot { width:6px; height:6px; border-radius:50%; background:var(--
|
|
594
|
-
.kd-dot-idle { background:var(--
|
|
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-
|
|
597
|
-
.kd-main
|
|
598
|
-
.kd-
|
|
599
|
-
|
|
600
|
-
.kd-
|
|
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:
|
|
604
|
-
color:var(--
|
|
605
|
-
.kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:
|
|
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(--
|
|
609
|
-
border:1px solid color-mix(in srgb, var(--
|
|
610
|
-
.kd-msg-assistant { align-self:stretch; border:
|
|
611
|
-
|
|
612
|
-
.kd-msg-
|
|
613
|
-
.kd-
|
|
614
|
-
.kd-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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(--
|
|
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:
|
|
623
|
-
.kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:
|
|
624
|
-
border:0; background:transparent; color:var(--
|
|
625
|
-
cursor:pointer; border-radius:8px
|
|
626
|
-
.kd-tab:hover { color:var(--
|
|
627
|
-
.kd-tab.is-active { color:var(--
|
|
628
|
-
|
|
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(--
|
|
632
|
-
.kd-tab.is-active .kd-tab-count { color:var(--
|
|
633
|
-
.kd-card { border:1px solid var(--
|
|
634
|
-
background:var(--
|
|
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(--
|
|
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(--
|
|
643
|
-
.kd-tag-brand { color:var(--
|
|
644
|
-
background:color-mix(in srgb, var(--
|
|
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(--
|
|
647
|
-
color:var(--
|
|
648
|
-
.kd-badge-ok { color:var(--
|
|
649
|
-
border-color:color-mix(in srgb, var(--
|
|
650
|
-
background:color-mix(in srgb, var(--
|
|
651
|
-
.kd-badge-bad { color:var(--
|
|
652
|
-
border-color:color-mix(in srgb, var(--
|
|
653
|
-
background:color-mix(in srgb, var(--
|
|
654
|
-
.kd-badge-warn { color:var(--
|
|
655
|
-
border-color:color-mix(in srgb, var(--
|
|
656
|
-
background:color-mix(in srgb, var(--
|
|
657
|
-
.kd-badge-brand { color:var(--
|
|
658
|
-
border-color:color-mix(in srgb, var(--
|
|
659
|
-
background:color-mix(in srgb, var(--
|
|
660
|
-
.kd-stem { margin:
|
|
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:
|
|
663
|
-
border:1px solid var(--
|
|
664
|
-
text-align:left; color:var(--
|
|
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(--
|
|
667
|
-
background:var(--
|
|
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(--
|
|
670
|
-
background:color-mix(in srgb, var(--
|
|
671
|
-
.kd-option.is-correct { border-color:var(--
|
|
672
|
-
background:color-mix(in srgb, var(--
|
|
673
|
-
.kd-option.is-wrong { border-color:var(--
|
|
674
|
-
background:color-mix(in srgb, var(--
|
|
675
|
-
.kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:
|
|
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(--
|
|
678
|
-
.kd-option.is-selected .kd-option-key { color:var(--
|
|
679
|
-
.kd-option.is-correct .kd-option-key { color:var(--
|
|
680
|
-
.kd-option.is-wrong .kd-option-key { color:var(--
|
|
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:
|
|
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(--
|
|
686
|
-
color:var(--
|
|
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(--
|
|
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(--
|
|
691
|
-
.kd-btn-primary:hover:not(:disabled) { background:var(--
|
|
692
|
-
.kd-btn-ghost { border-color:transparent; background:transparent; color:var(--
|
|
693
|
-
.kd-btn-ghost:hover:not(:disabled) { background:var(--
|
|
694
|
-
.kd-btn-danger { color:var(--
|
|
695
|
-
border-color:color-mix(in srgb, var(--
|
|
696
|
-
.kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
704
|
-
background:var(--
|
|
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(--
|
|
707
|
-
background:var(--
|
|
708
|
-
.kd-answer-panel-head .kd-tag { background:var(--
|
|
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(--
|
|
712
|
-
.kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--
|
|
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(--
|
|
716
|
-
color:var(--
|
|
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(--
|
|
720
|
-
background:var(--
|
|
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(--
|
|
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:
|
|
726
|
-
-
|
|
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(--
|
|
729
|
-
.kd-empty { padding:56px 24px; text-align:center; color:var(--
|
|
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(--
|
|
732
|
-
background:var(--
|
|
733
|
-
.kd-empty-title { font-size:15px; font-weight:600; color:var(--
|
|
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(--
|
|
740
|
-
border:1px solid var(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
751
|
-
.kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--
|
|
752
|
-
background:var(--
|
|
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(--
|
|
755
|
-
box-shadow:0 0 0 3px color-mix(in srgb, var(--
|
|
756
|
-
.kd-input::placeholder { color:var(--
|
|
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(--
|
|
760
|
-
.kd-preview-item { border:1px solid var(--
|
|
761
|
-
background:var(--
|
|
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(--
|
|
764
|
-
.kd-error-text { color:var(--
|
|
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(--
|
|
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(--
|
|
770
|
-
.kd-toast-ok { color:var(--
|
|
771
|
-
.kd-toast-warn { color:var(--
|
|
772
|
-
.kd-loading { padding:48px 0; text-align:center; color:var(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
789
|
-
color:var(--
|
|
790
|
-
.kd-hr { border:0; border-top:1px solid var(--
|
|
791
|
-
.kd-link { color:var(--
|
|
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(--
|
|
794
|
-
border:1px solid var(--
|
|
795
|
-
.kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--
|
|
796
|
-
background:var(--
|
|
797
|
-
.kd-code-lang { padding:3px 10px; font-size:11px; color:var(--
|
|
798
|
-
border-bottom:1px solid var(--
|
|
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(--
|
|
805
|
-
.kd-table th { background:var(--
|
|
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(--
|
|
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 {
|
|
811
|
-
.kd-math
|
|
812
|
-
|
|
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-
|
|
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(--
|
|
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(--
|
|
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
|
|
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,
|
|
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,
|
|
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' },
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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(
|
|
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 ? '
|
|
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-
|
|
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
|
-
|
|
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' },
|
|
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
|
-
|
|
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: '围绕这道题继续追问…(
|
|
1631
|
+
placeholder: '围绕这道题继续追问…(Enter 发送,Shift+Enter 换行)',
|
|
1401
1632
|
disabled: sending,
|
|
1402
1633
|
onChange: (event) => setDraft(event.target.value),
|
|
1403
1634
|
onKeyDown: (event) => {
|
|
1404
|
-
if (
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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: () => (
|
|
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.
|
|
1768
|
+
value: draft.subjectSource[subject.id] || 'auto',
|
|
1479
1769
|
'aria-label': `${subject.name} 出题来源`,
|
|
1480
|
-
onChange: (event) =>
|
|
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
|
-
|
|
1716
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1801
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
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
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
2165
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
2660
|
+
h('div', { className: 'kd-main-toolbar' },
|
|
2223
2661
|
boot ? h(TabBar, {
|
|
2224
2662
|
kind,
|
|
2225
2663
|
counts,
|
|
2226
|
-
onPick:
|
|
2227
|
-
if (value === 'today') loadToday(subjectId)
|
|
2228
|
-
else loadList(subjectId, value)
|
|
2229
|
-
},
|
|
2664
|
+
onPick: pickTab,
|
|
2230
2665
|
}) : null,
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
-
|
|
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' } },
|