daily-question 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +38 -26
  3. package/client.js +250 -144
  4. package/cordis.patch.yml +1 -1
  5. package/icon.svg +10 -7
  6. package/index.js +6 -6
  7. package/lib/bank.js +53 -36
  8. package/lib/exam-dates.js +101 -67
  9. package/lib/generate.js +62 -25
  10. package/lib/generation-history.js +8 -0
  11. package/lib/paper-types.js +47 -0
  12. package/lib/reference-reader.js +11 -8
  13. package/lib/references.js +72 -26
  14. package/lib/research.js +22 -13
  15. package/lib/routes.js +62 -95
  16. package/lib/service.js +73 -72
  17. package/lib/store.js +3 -8
  18. package/lib/subjects.js +12 -0
  19. package/package.json +2 -2
  20. package/vendor/mathjax/a11y/assistive-mml.js +1 -0
  21. package/vendor/mathjax/input/tex/extensions/action.js +1 -0
  22. package/vendor/mathjax/input/tex/extensions/ams.js +1 -0
  23. package/vendor/mathjax/input/tex/extensions/amscd.js +1 -0
  24. package/vendor/mathjax/input/tex/extensions/autoload.js +1 -0
  25. package/vendor/mathjax/input/tex/extensions/bbm.js +1 -0
  26. package/vendor/mathjax/input/tex/extensions/bboldx.js +1 -0
  27. package/vendor/mathjax/input/tex/extensions/bbox.js +1 -0
  28. package/vendor/mathjax/input/tex/extensions/begingroup.js +1 -0
  29. package/vendor/mathjax/input/tex/extensions/boldsymbol.js +1 -0
  30. package/vendor/mathjax/input/tex/extensions/braket.js +1 -0
  31. package/vendor/mathjax/input/tex/extensions/bussproofs.js +1 -0
  32. package/vendor/mathjax/input/tex/extensions/cancel.js +1 -0
  33. package/vendor/mathjax/input/tex/extensions/cases.js +1 -0
  34. package/vendor/mathjax/input/tex/extensions/centernot.js +1 -0
  35. package/vendor/mathjax/input/tex/extensions/color.js +1 -0
  36. package/vendor/mathjax/input/tex/extensions/colortbl.js +1 -0
  37. package/vendor/mathjax/input/tex/extensions/colorv2.js +1 -0
  38. package/vendor/mathjax/input/tex/extensions/configmacros.js +1 -0
  39. package/vendor/mathjax/input/tex/extensions/dsfont.js +1 -0
  40. package/vendor/mathjax/input/tex/extensions/empheq.js +1 -0
  41. package/vendor/mathjax/input/tex/extensions/enclose.js +1 -0
  42. package/vendor/mathjax/input/tex/extensions/extpfeil.js +1 -0
  43. package/vendor/mathjax/input/tex/extensions/fontsizev3.js +1 -0
  44. package/vendor/mathjax/input/tex/extensions/gensymb.js +1 -0
  45. package/vendor/mathjax/input/tex/extensions/html.js +1 -0
  46. package/vendor/mathjax/input/tex/extensions/mathtools.js +1 -0
  47. package/vendor/mathjax/input/tex/extensions/mhchem.js +34 -0
  48. package/vendor/mathjax/input/tex/extensions/newcommand.js +1 -0
  49. package/vendor/mathjax/input/tex/extensions/noerrors.js +1 -0
  50. package/vendor/mathjax/input/tex/extensions/noundefined.js +1 -0
  51. package/vendor/mathjax/input/tex/extensions/physics.js +1 -0
  52. package/vendor/mathjax/input/tex/extensions/require.js +1 -0
  53. package/vendor/mathjax/input/tex/extensions/setoptions.js +1 -0
  54. package/vendor/mathjax/input/tex/extensions/tagformat.js +1 -0
  55. package/vendor/mathjax/input/tex/extensions/texhtml.js +1 -0
  56. package/vendor/mathjax/input/tex/extensions/textcomp.js +1 -0
  57. package/vendor/mathjax/input/tex/extensions/textmacros.js +1 -0
  58. package/vendor/mathjax/input/tex/extensions/unicode.js +1 -0
  59. package/vendor/mathjax/input/tex/extensions/units.js +1 -0
  60. package/vendor/mathjax/input/tex/extensions/upgreek.js +1 -0
  61. package/vendor/mathjax/input/tex/extensions/verb.js +1 -0
  62. package/vendor/mathjax/newcm/svg/dynamic/PUA.js +1 -0
  63. package/vendor/mathjax/newcm/svg/dynamic/accents-b-i.js +1 -0
  64. package/vendor/mathjax/newcm/svg/dynamic/accents.js +1 -0
  65. package/vendor/mathjax/newcm/svg/dynamic/arabic.js +1 -0
  66. package/vendor/mathjax/newcm/svg/dynamic/arrows.js +1 -0
  67. package/vendor/mathjax/newcm/svg/dynamic/braille-d.js +1 -0
  68. package/vendor/mathjax/newcm/svg/dynamic/braille.js +1 -0
  69. package/vendor/mathjax/newcm/svg/dynamic/calligraphic.js +1 -0
  70. package/vendor/mathjax/newcm/svg/dynamic/cherokee.js +1 -0
  71. package/vendor/mathjax/newcm/svg/dynamic/cyrillic-ss.js +1 -0
  72. package/vendor/mathjax/newcm/svg/dynamic/cyrillic.js +1 -0
  73. package/vendor/mathjax/newcm/svg/dynamic/devanagari.js +1 -0
  74. package/vendor/mathjax/newcm/svg/dynamic/double-struck.js +1 -0
  75. package/vendor/mathjax/newcm/svg/dynamic/fraktur.js +1 -0
  76. package/vendor/mathjax/newcm/svg/dynamic/greek-ss.js +1 -0
  77. package/vendor/mathjax/newcm/svg/dynamic/greek.js +1 -0
  78. package/vendor/mathjax/newcm/svg/dynamic/hebrew.js +1 -0
  79. package/vendor/mathjax/newcm/svg/dynamic/latin-b.js +1 -0
  80. package/vendor/mathjax/newcm/svg/dynamic/latin-bi.js +1 -0
  81. package/vendor/mathjax/newcm/svg/dynamic/latin-i.js +1 -0
  82. package/vendor/mathjax/newcm/svg/dynamic/latin.js +1 -0
  83. package/vendor/mathjax/newcm/svg/dynamic/marrows.js +1 -0
  84. package/vendor/mathjax/newcm/svg/dynamic/math.js +1 -0
  85. package/vendor/mathjax/newcm/svg/dynamic/monospace-ex.js +1 -0
  86. package/vendor/mathjax/newcm/svg/dynamic/monospace-l.js +1 -0
  87. package/vendor/mathjax/newcm/svg/dynamic/monospace.js +1 -0
  88. package/vendor/mathjax/newcm/svg/dynamic/mshapes.js +1 -0
  89. package/vendor/mathjax/newcm/svg/dynamic/phonetics-ss.js +1 -0
  90. package/vendor/mathjax/newcm/svg/dynamic/phonetics.js +1 -0
  91. package/vendor/mathjax/newcm/svg/dynamic/sans-serif-b.js +1 -0
  92. package/vendor/mathjax/newcm/svg/dynamic/sans-serif-bi.js +1 -0
  93. package/vendor/mathjax/newcm/svg/dynamic/sans-serif-ex.js +1 -0
  94. package/vendor/mathjax/newcm/svg/dynamic/sans-serif-i.js +1 -0
  95. package/vendor/mathjax/newcm/svg/dynamic/sans-serif-r.js +1 -0
  96. package/vendor/mathjax/newcm/svg/dynamic/sans-serif.js +1 -0
  97. package/vendor/mathjax/newcm/svg/dynamic/script.js +1 -0
  98. package/vendor/mathjax/newcm/svg/dynamic/shapes.js +1 -0
  99. package/vendor/mathjax/newcm/svg/dynamic/symbols-b-i.js +1 -0
  100. package/vendor/mathjax/newcm/svg/dynamic/symbols.js +1 -0
  101. package/vendor/mathjax/newcm/svg/dynamic/variants.js +1 -0
  102. package/vendor/mathjax/tex-svg.js +1 -34
package/client.js CHANGED
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
3
3
  factory(require) {
4
4
  const React = require('react')
5
5
  const h = React.createElement
6
- const { useState, useEffect, useRef, useMemo, useCallback } = React
6
+ const { useState, useEffect, useRef, useMemo, useCallback, useId } = React
7
7
 
8
8
  /* ============================== 文案 ============================== */
9
9
  const T = {
@@ -110,10 +110,8 @@ window.__ModuleLoader__.load({
110
110
  return { text: '未作答', tone: 'idle' }
111
111
  }
112
112
 
113
- /* ============================== 公式(MathJax 3) ============================== */
114
- // MathJax 的 SVG 输出自带字形路径,不依赖任何字体文件;单文件 bundle 随包分发,
115
- // 由宿主在 /api/daily-question/vendor/tex-svg.js 提供。
116
- const MATHJAX_SRC = '/api/daily-question/vendor/tex-svg.js'
113
+ /* ============================== 公式(MathJax 4) ============================== */
114
+ const MATHJAX_PATH = '/api/daily-question/vendor/mathjax-4.1.3'
117
115
  const mathState = { status: 'idle' }
118
116
  const mathWatchers = new Set()
119
117
 
@@ -131,26 +129,59 @@ window.__ModuleLoader__.load({
131
129
  function ensureMathJax() {
132
130
  if (mathState.status !== 'idle') return
133
131
  mathState.status = 'loading'
132
+ const assetRoot = new URL(MATHJAX_PATH, document.baseURI).href
133
+ const failed = (error) => {
134
+ mathState.status = 'failed'
135
+ console.warn('[daily-question] 公式引擎启动失败', error)
136
+ notifyMath()
137
+ }
134
138
  window.MathJax = {
135
- startup: { typeset: false },
139
+ loader: { paths: { mathjax: assetRoot }, load: ['a11y/assistive-mml'] },
140
+ startup: {
141
+ typeset: false,
142
+ ready() {
143
+ const { startup } = window.MathJax
144
+ startup.defaultReady()
145
+ // 旧菜单设置可能重新开启依赖外部资源的语音组件。
146
+ startup.document.options.enableEnrichment = false
147
+ startup.document.options.enableAssistiveMml = true
148
+ startup.output.options.displayOverflow = 'overflow'
149
+ startup.output.options.linebreaks.inline = false
150
+ },
151
+ },
152
+ options: {
153
+ enableMenu: false,
154
+ menuOptions: { settings: { enrich: false, assistiveMml: true, breakInline: false, overflow: 'Overflow' } },
155
+ },
156
+ tex: { packages: { '[-]': ['require', 'html', 'texhtml', 'setoptions'] } },
157
+ output: {
158
+ font: 'mathjax-newcm',
159
+ fontPath: assetRoot + '/newcm',
160
+ minScale: 1,
161
+ displayOverflow: 'overflow',
162
+ linebreaks: { inline: false },
163
+ },
136
164
  // 每条公式自带 <defs>,注入 innerHTML 后不依赖全局字体缓存
137
- svg: { fontCache: 'local', scale: 1, matchFontHeight: false },
165
+ svg: { fontCache: 'local' },
138
166
  }
139
167
  const script = document.createElement('script')
140
- script.src = MATHJAX_SRC
168
+ script.src = assetRoot + '/tex-svg.js'
141
169
  script.async = true
142
170
  script.onload = () => {
143
171
  const startup = window.MathJax && window.MathJax.startup
144
172
  const done = () => {
173
+ if (typeof window.MathJax?.tex2svgPromise !== 'function') {
174
+ failed(new Error('MathJax SVG 转换接口不可用'))
175
+ return
176
+ }
145
177
  mathState.status = 'ready'
146
178
  ensureMathStylesheet()
147
179
  notifyMath()
148
180
  }
149
- const failed = () => { mathState.status = 'failed'; notifyMath() }
150
181
  if (startup && startup.promise) startup.promise.then(done, failed)
151
182
  else done()
152
183
  }
153
- script.onerror = () => { mathState.status = 'failed'; notifyMath() }
184
+ script.onerror = () => failed(new Error('无法加载 ' + script.src))
154
185
  document.head.appendChild(script)
155
186
  }
156
187
 
@@ -183,7 +214,7 @@ window.__ModuleLoader__.load({
183
214
  * 规范化模型输出的 TeX:
184
215
  * - 去掉自带的 $ / $$ 包裹;
185
216
  * - 过滤能改页面结构或注入链接的宏(公式来自模型输出,不当作可信内容);
186
- * - 公式块里的裸 \\ 换行在 MathJax 3 需要放进 \displaylines(Typora 也是这么处理的);
217
+ * - 兼容旧题公式块中的裸 \\ 换行;
187
218
  * - 其余交给 MathJax,不再自己做子集解析。
188
219
  */
189
220
  const RISKY_TEX = [
@@ -204,25 +235,35 @@ window.__ModuleLoader__.load({
204
235
  return tex
205
236
  }
206
237
 
238
+ function MathFormula({ tex, display }) {
239
+ const [html, setHtml] = useState(null)
240
+ useEffect(() => {
241
+ let active = true
242
+ window.MathJax.tex2svgPromise(tex, { display }).then(
243
+ (node) => { if (active) setHtml(node?.outerHTML || '') },
244
+ () => { if (active) setHtml('') },
245
+ )
246
+ return () => { active = false }
247
+ }, [tex, display])
248
+ const tag = display ? 'div' : 'span'
249
+ const className = display ? 'kd-mathblock' : 'kd-math'
250
+ return html ? h(tag, { className, dangerouslySetInnerHTML: { __html: html } })
251
+ : h(tag, { className }, mathFallback(tex, html === '' ? '公式无法渲染' : ''))
252
+ }
253
+
254
+ function mathFallback(tex, error, key) {
255
+ return h('span', {
256
+ className: error ? 'kd-math-error' : 'kd-math-pending',
257
+ title: error || '公式引擎正在加载',
258
+ key,
259
+ }, tex)
260
+ }
261
+
207
262
  function renderMath(source, display) {
208
263
  const tex = normalizeTeX(source, display)
209
264
  const key = nextKey('math')
210
- if (mathState.status !== 'ready') {
211
- // 引擎还没就绪(或加载失败):先显示原文,就绪后组件会重渲染
212
- return h('span', { className: 'kd-math-pending', key, title: '公式引擎正在加载' }, tex)
213
- }
214
- try {
215
- const node = window.MathJax.tex2svg(tex, { display: Boolean(display) })
216
- const html = node && node.outerHTML
217
- if (!html) throw new Error('empty svg')
218
- return h(display ? 'div' : 'span', {
219
- className: display ? 'kd-mathblock' : 'kd-math',
220
- key,
221
- dangerouslySetInnerHTML: { __html: html },
222
- })
223
- } catch (error) {
224
- return h('span', { className: 'kd-math-error', key, title: '公式无法渲染' }, tex)
225
- }
265
+ return mathState.status === 'ready' ? h(MathFormula, { tex, display: Boolean(display), key })
266
+ : mathFallback(tex, mathState.status === 'failed' ? '公式引擎加载失败。若刚更新插件,请重启 DSH 应用和宿主。' : '', key)
226
267
  }
227
268
 
228
269
  /* ============================== Markdown ============================== */
@@ -568,9 +609,7 @@ window.__ModuleLoader__.load({
568
609
  display:flex; flex-direction:column; gap:12px; }
569
610
  .kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
570
611
  .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); }
612
+ .kd-brand { flex:0 0 40px; }
574
613
  .kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
575
614
  .kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
576
615
  .kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
@@ -580,9 +619,10 @@ window.__ModuleLoader__.load({
580
619
  .kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
581
620
  .kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
582
621
  gap:2px; text-align:right; }
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; }
622
+ .kd-cd-primary, .kd-cd-days { font-size:21px; font-weight:400; color:var(--kd-brand-primary); font-variant-numeric:tabular-nums; line-height:1.25; }
623
+ .kd-cd-days strong { font-size:36px; font-weight:700; }
584
624
  .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); }
625
+ .kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-primary); color:light-dark(#000, var(--kd-label-primary)); }
586
626
  .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
587
627
  border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
588
628
  font-size:11.5px; color:var(--kd-label-secondary); }
@@ -592,7 +632,8 @@ window.__ModuleLoader__.load({
592
632
  .kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
593
633
  padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
594
634
  .kd-nav-group { display:flex; flex-direction:column; gap:4px; }
595
- .kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
635
+ .kd-nav-group-label { display:flex; justify-content:space-between; gap:6px;
636
+ font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
596
637
  padding:0 8px 4px; }
597
638
  .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
598
639
  width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
@@ -645,6 +686,7 @@ window.__ModuleLoader__.load({
645
686
  border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
646
687
  cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
647
688
  .kd-tab:hover { color:var(--kd-label-primary); background:var(--kd-bg-layer-2); }
689
+ .kd-tab:disabled { opacity:.5; cursor:default; background:none; transform:none; }
648
690
  .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
691
 
650
692
  .kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
@@ -740,7 +782,7 @@ window.__ModuleLoader__.load({
740
782
  background:var(--kd-bg-layer-1); color:var(--kd-label-primary);
741
783
  font:inherit; transition:background-color .15s, border-color .15s; }
742
784
  .kd-list-row > .kd-btn { flex:none; white-space:nowrap; }
743
- .kd-list-row[role=button] { cursor:pointer; }
785
+ .kd-list-row { cursor:pointer; }
744
786
  .kd-list-open { flex:none; display:flex; align-items:center; gap:6px; font-size:12px; color:var(--kd-label-secondary); white-space:nowrap; }
745
787
  .kd-list-row + .kd-list-row { margin-top:8px; }
746
788
  .kd-list-row:hover { border-color:var(--kd-border-l2); background:var(--kd-bg-layer-2); }
@@ -752,7 +794,7 @@ window.__ModuleLoader__.load({
752
794
  .kd-list-summary .kd-p, .kd-list-summary .kd-h, .kd-list-summary .kd-mathblock { margin:0; }
753
795
  .kd-list-summary .kd-p::after { content:' '; }
754
796
  .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; }
797
+ .kd-list-summary .kd-mathblock > mjx-container[display=true] { display:inline; min-width:0; width:auto; }
756
798
  .kd-list-summary .kd-code-wrap { margin:0; border:0; }
757
799
  .kd-list-summary .kd-code-lang { display:none; }
758
800
  .kd-list-summary .kd-code { display:inline; padding:0; line-height:inherit; white-space:pre-wrap; }
@@ -843,15 +885,10 @@ window.__ModuleLoader__.load({
843
885
  .kd-image-fallback { display:inline-block; font-family:ui-monospace,monospace; font-size:12px;
844
886
  color:var(--kd-state-warn-primary); border:1px dashed var(--kd-state-warn-primary);
845
887
  border-radius:6px; padding:2px 6px; }
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;
851
- text-align:center; }
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; }
888
+ .kd-math { display:inline; font-size:1em; }
889
+ .kd-mathblock { max-width:100%; min-width:0; margin:1em 0; overflow-x:auto; text-align:center; }
890
+ .kd-mathblock > mjx-container[display=true] { min-width:100%; width:max-content; margin:0; }
891
+ .kd-math svg, .kd-mathblock svg { max-width:none; }
855
892
  .kd-math-pending { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
856
893
  opacity:.55; white-space:pre-wrap; }
857
894
  .kd-math-error { color:var(--kd-state-error-primary);
@@ -1075,13 +1112,17 @@ window.__ModuleLoader__.load({
1075
1112
  return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
1076
1113
  }
1077
1114
 
1078
- /** 剩余不超过 30 天时省略中间的时长行。 */
1115
+ /** 剩余不超过 30 天时省略按年、月拆分的时长。 */
1079
1116
  function CountdownLines({ countdown }) {
1080
- const lines = (countdown || {}).lines || {}
1117
+ const { year, days, approx, lines = {} } = countdown || {}
1118
+ const showDays = Number.isInteger(days) && days >= 0
1119
+ const duration = days === 0 ? '今日开考' : lines.secondary
1120
+ const spaceDigits = (line = '') => line.replace(/\p{Script=Han}(?=\d)|\d(?=\p{Script=Han})/gu, '$& ')
1081
1121
  return h('div', { className: 'kd-cd-lines' },
1082
- h('div', { className: 'kd-cd-primary' }, lines.primary || ''),
1083
- lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
1084
- h('div', { className: 'kd-cd-target' }, lines.target || ''))
1122
+ h('div', { className: 'kd-cd-primary' }, showDays ? `距离 ${year} 考研还剩` : spaceDigits(lines.primary)),
1123
+ showDays ? h('div', { className: 'kd-cd-days' }, approx ? '约 ' : null, h('strong', null, String(days)), ' 天') : null,
1124
+ duration ? h('div', { className: 'kd-cd-secondary' }, spaceDigits(duration)) : null,
1125
+ h('div', { className: 'kd-cd-target' }, spaceDigits(lines.target)))
1085
1126
  }
1086
1127
 
1087
1128
  /** 年份、日期口径与来源:一条紧凑的状态带,横跨整个头部。 */
@@ -1095,13 +1136,9 @@ window.__ModuleLoader__.load({
1095
1136
  const sourceLabel = cd.status === 'official'
1096
1137
  ? `来源 ${hostOf(sourceUrl) || '官方'}`
1097
1138
  : basis ? `依据 ${basis.year} 官方(${hostOf(basis.sourceUrl) || '官方'})` : '无来源记录'
1098
- const noteText = cd.status === 'estimated' && basis
1099
- ? `按 ${basis.year} 官方日期推后一年估算`
1100
- : cd.status === 'unverified' ? '官方日期尚未核实' : (cd.note || '')
1101
1139
 
1102
1140
  return h('div', { className: 'kd-exam-strip' },
1103
1141
  h('label', { className: 'kd-strip-item' },
1104
- h('span', null, T.year),
1105
1142
  h('select', {
1106
1143
  className: 'kd-select kd-select-sm',
1107
1144
  value: boot.settings.targetYear,
@@ -1117,7 +1154,7 @@ window.__ModuleLoader__.load({
1117
1154
  : h('span', null, sourceLabel)),
1118
1155
  cd.verifiedAt
1119
1156
  ? h('span', { className: 'kd-strip-item', title: formatDateTime(cd.verifiedAt) },
1120
- `核验 ${formatShort(cd.verifiedAt)}`)
1157
+ `核验成功 ${formatShort(cd.verifiedAt)}`)
1121
1158
  : null,
1122
1159
  h(Btn, {
1123
1160
  small: true,
@@ -1128,7 +1165,9 @@ window.__ModuleLoader__.load({
1128
1165
  }, busy ? h('span', { className: 'kd-spinner' }) : h(Icon, { name: 'refresh', size: 13 }), '核验'),
1129
1166
  cd.expired
1130
1167
  ? h('span', { className: 'kd-strip-item kd-strip-warn' }, '目标日期已过,请重新选择考研年份')
1131
- : (noteText ? h('span', { className: 'kd-strip-item kd-strip-warn', title: cd.note || '' }, noteText) : null))
1168
+ : cd.status === 'unverified'
1169
+ ? h('span', { className: 'kd-strip-item kd-strip-warn', title: cd.note || '' }, '官方日期尚未核实')
1170
+ : null)
1132
1171
  }
1133
1172
 
1134
1173
  /* ============================== 导航 ============================== */
@@ -1167,25 +1206,29 @@ window.__ModuleLoader__.load({
1167
1206
 
1168
1207
  function SubjectNav({ boot, subjectId, onPick, onManage, onBanks }) {
1169
1208
  const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
1209
+ const archived = archivedSubjects(boot, subjectId)
1210
+ const subjectButton = (subject) => {
1211
+ const done = Boolean(boot.todayMap?.[subject.id])
1212
+ return h('button', {
1213
+ key: subject.id,
1214
+ type: 'button',
1215
+ className: `kd-nav-item${subject.id === subjectId ? ' is-active' : ''}`,
1216
+ onClick: () => onPick(subject.id),
1217
+ title: subject.name,
1218
+ },
1219
+ h('span', null, subject.name),
1220
+ subject.enabled ? h('span', { className: 'kd-nav-status' },
1221
+ h('span', { className: `kd-dot${done ? '' : ' kd-dot-idle'}` }),
1222
+ done ? '今日已出' : '待出题') : null)
1223
+ }
1170
1224
  return h('div', { className: 'kd-nav' },
1171
1225
  h('div', { className: 'kd-nav-group' },
1172
- h('div', { className: 'kd-nav-group-label' }, '科目'),
1173
- enabled.length
1174
- ? enabled.map((subject) => {
1175
- const done = Boolean(boot.todayMap?.[subject.id])
1176
- return h('button', {
1177
- key: subject.id,
1178
- type: 'button',
1179
- className: `kd-nav-item${subject.id === subjectId ? ' is-active' : ''}`,
1180
- onClick: () => onPick(subject.id),
1181
- title: subject.name,
1182
- },
1183
- h('span', null, subject.name),
1184
- h('span', { className: 'kd-nav-status' },
1185
- h('span', { className: `kd-dot${done ? '' : ' kd-dot-idle'}` }),
1186
- done ? '今日已出' : '待出题'))
1187
- })
1226
+ h('div', { className: 'kd-nav-group-label' }, '已启用科目', h('span', null, String(enabled.length))),
1227
+ enabled.length ? enabled.map(subjectButton)
1188
1228
  : h('div', { className: 'kd-msg-meta', style: { padding: '4px 8px' } }, '还没有启用科目')),
1229
+ archived.length ? h('div', { className: 'kd-nav-group' },
1230
+ h('div', { className: 'kd-nav-group-label' }, '已停用科目', h('span', null, String(archived.length))),
1231
+ archived.map(subjectButton)) : null,
1189
1232
  h('div', { className: 'kd-nav-spacer' }),
1190
1233
  h('div', { className: 'kd-nav-group' },
1191
1234
  h('div', { className: 'kd-nav-group-label' }, '设置'),
@@ -1194,7 +1237,11 @@ window.__ModuleLoader__.load({
1194
1237
  h(UpdateCheck,{currentVersion:boot.pluginVersion})))
1195
1238
  }
1196
1239
 
1197
- function TabBar({ kind, counts, onPick }) {
1240
+ function archivedSubjects(boot, subjectId) {
1241
+ return (boot.subjects || []).filter((subject) => !subject.enabled && (boot.counts?.[subject.id]?.all || subject.id === subjectId))
1242
+ }
1243
+
1244
+ function TabBar({ kind, counts, onPick, todayDisabled }) {
1198
1245
  const items = [
1199
1246
  ['today', T.tabs.today, null],
1200
1247
  ['all', T.tabs.all, counts?.all],
@@ -1206,19 +1253,24 @@ window.__ModuleLoader__.load({
1206
1253
  key: value,
1207
1254
  type: 'button',
1208
1255
  className: `kd-tab${kind === value ? ' is-active' : ''}`,
1256
+ disabled: value === 'today' && todayDisabled,
1209
1257
  onClick: () => onPick(value),
1210
1258
  }, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
1211
1259
  }
1212
1260
 
1213
1261
  function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
1214
1262
  const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
1263
+ const archived = archivedSubjects(boot, subjectId)
1264
+ const option = (subject) => h('option', { key: subject.id, value: subject.id }, subject.name)
1215
1265
  return h('div', { className: 'kd-mobilebar' },
1216
1266
  h('select', {
1217
1267
  className: 'kd-select',
1218
1268
  value: subjectId || '',
1219
1269
  'aria-label': '切换科目',
1220
1270
  onChange: (event) => onPick(event.target.value),
1221
- }, enabled.map((subject) => h('option', { key: subject.id, value: subject.id }, subject.name))),
1271
+ }, !subjectId ? h('option', { value: '', disabled: true }, '选择科目') : null,
1272
+ enabled.length ? h('optgroup', { label: `已启用科目(${enabled.length})` }, enabled.map(option)) : null,
1273
+ archived.length ? h('optgroup', { label: `已停用科目(${archived.length})` }, archived.map(option)) : null),
1222
1274
  // 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
1223
1275
  h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
1224
1276
  h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
@@ -1311,21 +1363,24 @@ window.__ModuleLoader__.load({
1311
1363
  function QuestionPane({
1312
1364
  q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
1313
1365
  onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
1314
- onCancel,
1366
+ onCancel, onSkip,
1315
1367
  }) {
1316
1368
  if (!q) return null
1317
1369
  const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
1318
1370
  const last = q.lastAttempt
1319
1371
  const isChoice = typeof q.isChoice === 'boolean' ? q.isChoice : isChoiceType(q.type)
1372
+ const canSelect = isChoice && (q.canSelect ?? q.options?.length >= 2)
1320
1373
  const answerKey = q.answer?.answerKey || ''
1321
1374
  const selected = revealed ? (last?.choice || '') : choice
1375
+ const difficultyUnverified = q.examAlignment?.status === 'difficulty-unverified'
1322
1376
  const meta = [q.subjectName, q.typeLabel]
1323
1377
  return h('div', null,
1324
1378
  h('div', { className: 'kd-card' },
1325
1379
  h('div', { className: 'kd-chips' },
1326
1380
  h('span', { className: 'kd-tag kd-tag-brand' }, q.typeLabel),
1327
1381
  h('span', { className: 'kd-tag' }, q.subjectName),
1328
- h('span', { className: 'kd-tag', title: q.examAlignment ? '独立评审参考历年真题给出的难度,不是考生正确率统计' : '原始难度标签,尚无真题对照评审' }, `难度 ${q.difficulty}/5`),
1382
+ difficultyUnverified ? h(Badge, { tone: 'warn' }, '未通过真题难度校验')
1383
+ : h('span', { className: 'kd-tag', title: q.examAlignment ? '独立评审参考历年真题给出的难度' : '原始难度标签,尚无真题对照评审' }, `难度 ${q.difficulty}/5`),
1329
1384
  h('span', { className: 'kd-tag' }, formatDate(q.assignedDate)),
1330
1385
  h('span', { className: 'kd-tag' }, `累计作答 ${q.attemptCount} 次`),
1331
1386
  h('span', { className: 'kd-tag' }, q.source?.kind === 'bank' ? q.source.label : '在线出题'),
@@ -1340,7 +1395,7 @@ window.__ModuleLoader__.load({
1340
1395
  h('span', null, `${T.knowledge}:${q.knowledge.join('、')}`))
1341
1396
  : null,
1342
1397
  q.examAlignment ? h('details', { className: 'kd-verify' },
1343
- h('summary', null, `范围与难度依据 · 大纲 ${q.examAlignment.outlineYear} · 独立真题对照`),
1398
+ h('summary', null, difficultyUnverified ? `范围校验依据 · 大纲 ${q.examAlignment.outlineYear}` : `范围与难度依据 · 大纲 ${q.examAlignment.outlineYear} · 独立真题对照`),
1344
1399
  h('div', null, q.examAlignment.note),
1345
1400
  h('div', null, `参考考试:${q.examAlignment.basisSubject?.name || q.subjectName} · 检索时间:${formatDateTime(q.examAlignment.checkedAt)}`),
1346
1401
  ...(q.examAlignment.sources || []).map((source) => h('div', { key: source.id },
@@ -1351,11 +1406,12 @@ window.__ModuleLoader__.load({
1351
1406
  revealed ? h('div', null, q.examAlignment.rationale,
1352
1407
  ...(q.examAlignment.warnings || []).map((warning,i)=>h('div',{key:i,className:'kd-meta'},warning))) : null) : null,
1353
1408
  h('div', { className: 'kd-stem' }, h(Markdown, { text: q.stem })),
1354
- isChoice && q.options?.length
1409
+ isChoice && !canSelect ? h('div', { className: 'kd-msg-meta' }, '本题选项不完整,可跳过或查看答案后自行判定。') : null,
1410
+ canSelect
1355
1411
  ? h('div', { className: 'kd-options' }, q.options.map((option) => {
1356
1412
  const isSelected = selected.split('').includes(option.key)
1357
1413
  const isRight = revealed && answerKey.split('').includes(option.key)
1358
- const isWrongPick = revealed && isSelected && !isRight
1414
+ const isWrongPick = revealed && answerKey && isSelected && !isRight
1359
1415
  const cls = ['kd-option']
1360
1416
  if (isSelected && !revealed) cls.push('is-selected')
1361
1417
  if (isRight) cls.push('is-correct')
@@ -1373,14 +1429,16 @@ window.__ModuleLoader__.load({
1373
1429
  : null,
1374
1430
  revealed ? h(ResultBanner, { attempt: last }) : null,
1375
1431
  h('div', { className: 'kd-actions' },
1376
- !revealed && isChoice
1432
+ !revealed && canSelect
1377
1433
  ? h(Btn, { variant: 'primary', disabled: busy === 'reveal' || !choice, onClick: () => onReveal('submit') },
1378
1434
  busy === 'reveal' ? h('span', { className: 'kd-spinner' }) : null, T.submit)
1379
1435
  : null,
1380
- !revealed && !isChoice
1436
+ !revealed && !canSelect
1381
1437
  ? [
1382
1438
  h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
1383
- h(Btn, { key: 'giveup', variant: 'danger', disabled: busy === 'reveal', onClick: () => onReveal('giveup') }, T.giveup),
1439
+ isChoice
1440
+ ? h(Btn, { key: 'skip', disabled: busy === 'reveal', onClick: onSkip }, '跳过本题')
1441
+ : h(Btn, { key: 'giveup', variant: 'danger', disabled: busy === 'reveal', onClick: () => onReveal('giveup') }, T.giveup),
1384
1442
  ]
1385
1443
  : null,
1386
1444
  revealed && last?.result === 'pending'
@@ -1462,7 +1520,9 @@ window.__ModuleLoader__.load({
1462
1520
  const observer = new ResizeObserver(measure)
1463
1521
  observer.observe(element)
1464
1522
  if (element.firstElementChild) observer.observe(element.firstElementChild)
1465
- return () => observer.disconnect()
1523
+ const mutations = new MutationObserver(measure)
1524
+ mutations.observe(element, { childList: true, subtree: true })
1525
+ return () => { observer.disconnect(); mutations.disconnect() }
1466
1526
  }, [text, mathStatus])
1467
1527
  return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
1468
1528
  }
@@ -1477,9 +1537,10 @@ window.__ModuleLoader__.load({
1477
1537
  body: T.tabEmpty[kind] || '暂无内容',
1478
1538
  })
1479
1539
  }
1540
+ const total = list.total ?? list.items.length
1480
1541
  return h('div', null,
1481
1542
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1482
- h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.total ?? list.items.length} 道`),
1543
+ h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${total} 道`),
1483
1544
  h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
1484
1545
  list.totalPages>1?h('div',{className:'kd-row',style:{marginBottom:'12px'},'aria-label':'题目分页'},
1485
1546
  h(Btn,{small:true,disabled:busy||list.page<=1,onClick:()=>onPage(list.page-1)},'上一页'),
@@ -1488,7 +1549,7 @@ window.__ModuleLoader__.load({
1488
1549
  managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
1489
1550
  h('div', { className: 'kd-row' },
1490
1551
  h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
1491
- h(Btn, { small: true, disabled: busy, onClick: () => onSelectAll(true) }, '全选'),
1552
+ h(Btn, { small: true, disabled: busy || selected.length === total, onClick: () => onSelectAll(true) }, '全选'),
1492
1553
  h(Btn, { small: true, disabled: busy || !selected.length, onClick: () => onSelectAll(false) }, '全不选')),
1493
1554
  h('div', { className: 'kd-row' },
1494
1555
  ...[['favorite', '收藏'], ['unfavorite', '取消收藏'], ['wrong', '加入错题本'], ['unwrong', '移出错题本'], ['delete', '删除题目']].map(([action, label]) =>
@@ -1498,7 +1559,14 @@ window.__ModuleLoader__.load({
1498
1559
  className: 'kd-list-row' + (managing && selected.includes(item.id) ? ' is-selected' : ''),
1499
1560
  role: managing ? undefined : 'button',
1500
1561
  tabIndex: managing ? undefined : 0,
1501
- onClick: managing ? undefined : (event) => { if (!event.target.closest('a,input')) onOpen(item.id) },
1562
+ onClick: (event) => {
1563
+ if (event.target.closest('a,input')) return
1564
+ if (managing) {
1565
+ if (!busy) onToggle(item.id)
1566
+ } else {
1567
+ onOpen(item.id)
1568
+ }
1569
+ },
1502
1570
  onKeyDown: managing ? undefined : (event) => {
1503
1571
  if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); onOpen(item.id) }
1504
1572
  },
@@ -1514,7 +1582,7 @@ window.__ModuleLoader__.load({
1514
1582
  item.favorite ? h(Badge, { tone: 'brand' }, '收藏') : null,
1515
1583
  item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '错题 · 已掌握' : '错题') : null,
1516
1584
  item.answerStatus !== 'verified' ? h(Badge, { tone: 'warn' }, T.unverified) : null,
1517
- h('span', null, `难度 ${item.difficulty}/5`))),
1585
+ item.difficultyUnverified ? h(Badge, { tone: 'warn' }, '未通过真题难度校验') : h('span', null, `难度 ${item.difficulty}/5`))),
1518
1586
  !managing ? h('span', { className: 'kd-list-open', 'aria-hidden': true }, '查看题目', h(Icon, { name: 'chevron' })) : null)))
1519
1587
  }
1520
1588
 
@@ -1838,8 +1906,13 @@ window.__ModuleLoader__.load({
1838
1906
  canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
1839
1907
  }
1840
1908
 
1841
- function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport }) {
1909
+ function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport, committing }) {
1910
+ const [excluded, setExcluded] = useState([])
1911
+ const [previewPage, setPreviewPage] = useState(0)
1842
1912
  if (importResult) {
1913
+ const pageSize = 60
1914
+ const pageCount = Math.ceil(importResult.items.length / pageSize)
1915
+ const selectedCount = importResult.usable - excluded.length
1843
1916
  return h(Modal, {
1844
1917
  title: `${T.preview}(${importResult.name})`,
1845
1918
  onClose: onCancelImport,
@@ -1849,23 +1922,40 @@ window.__ModuleLoader__.load({
1849
1922
  h(Btn, {
1850
1923
  key: 'ok',
1851
1924
  variant: 'primary',
1852
- disabled: !importResult.usable,
1853
- onClick: () => onCommit(importResult),
1925
+ disabled: !selectedCount || committing,
1926
+ onClick: () => onCommit(importResult, excluded),
1854
1927
  }, T.commit),
1855
1928
  ],
1856
1929
  },
1857
1930
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1858
- `共识别 ${importResult.stats.total} 道,其中带答案 ${importResult.stats.withAnswer} 道,可用 ${importResult.usable} 道。`),
1931
+ `共识别 ${importResult.stats.total} 道,可用 ${importResult.usable} 道,已选 ${selectedCount} 道入库。`),
1859
1932
  importResult.warnings.length
1860
1933
  ? h('div', { className: 'kd-verify' },
1861
1934
  h('div', null, '解析提示:'),
1862
1935
  h('ul', null, importResult.warnings.map((item, index) => h('li', { key: index }, item))))
1863
1936
  : null,
1937
+ pageCount > 1
1938
+ ? h('div', { className: 'kd-actions' },
1939
+ h(Btn, { small: true, disabled: previewPage === 0, onClick: () => setPreviewPage(previewPage - 1) }, '上一页'),
1940
+ h('span', { className: 'kd-meta' }, `${previewPage + 1} / ${pageCount}`),
1941
+ h(Btn, { small: true, disabled: previewPage + 1 === pageCount, onClick: () => setPreviewPage(previewPage + 1) }, '下一页'))
1942
+ : null,
1864
1943
  h('div', { style: { marginTop: '12px' } },
1865
- importResult.items.slice(0, 60).map((item) => h('div', { key: item.index, className: 'kd-preview-item' },
1944
+ importResult.items.slice(previewPage * pageSize, (previewPage + 1) * pageSize).map((item) => h('div', { key: item.index, className: 'kd-preview-item' },
1866
1945
  h('div', { className: 'kd-meta' },
1946
+ h('input', {
1947
+ type: 'checkbox',
1948
+ 'aria-label': `第 ${item.index} 题入库`,
1949
+ disabled: !item.usable || committing,
1950
+ checked: item.usable && !excluded.includes(item.index),
1951
+ onChange: (event) => {
1952
+ const checked = event.target.checked
1953
+ setExcluded((items) => checked ? items.filter((index) => index !== item.index) : [...items, item.index])
1954
+ },
1955
+ }),
1867
1956
  h(Badge, null, `第 ${item.index} 题`),
1868
1957
  h(Badge, null, item.typeLabel),
1958
+ item.usable === false ? h(Badge, { tone: 'warn' }, '不入库') : null,
1869
1959
  item.answerStatus === 'missing'
1870
1960
  ? h(Badge, { tone: 'warn' }, T.missing)
1871
1961
  : h(Badge, { tone: 'ok' }, '含参考答案(待核验)'),
@@ -1874,10 +1964,7 @@ window.__ModuleLoader__.load({
1874
1964
  h(Markdown, { text: (item.stem || '(未识别到题干)').slice(0, 400) })),
1875
1965
  item.issues.length
1876
1966
  ? h('div', { className: 'kd-warn-text', style: { marginTop: '6px' } }, `问题:${item.issues.join(';')}`)
1877
- : null))),
1878
- importResult.items.length > 60
1879
- ? h('div', { className: 'kd-msg-meta' }, `仅预览前 60 道,其余 ${importResult.items.length - 60} 道同样会按上述规则入库。`)
1880
- : null)
1967
+ : null))))
1881
1968
  }
1882
1969
 
1883
1970
  return h(Modal, {
@@ -2079,7 +2166,7 @@ window.__ModuleLoader__.load({
2079
2166
  if (!previous) return undefined
2080
2167
  redoQuestions.current.clear()
2081
2168
  const { subjectId: sid, kind: activeKind } = currentView.current
2082
- if (!sid) return undefined
2169
+ if (!sid || !boot.settings.subjects.includes(sid)) return undefined
2083
2170
  if (activeKind === 'today') { loadToday(sid); return undefined }
2084
2171
  let cancelled = false
2085
2172
  const request=dailyRequest.current
@@ -2125,8 +2212,9 @@ window.__ModuleLoader__.load({
2125
2212
  setSubjectId(sid)
2126
2213
  setList(null)
2127
2214
  rpc('saveSettings', { activeSubject: sid }).catch(() => {})
2128
- loadToday(sid)
2129
- }, [subjectId, loadToday])
2215
+ if (boot.settings.subjects.includes(sid)) loadToday(sid)
2216
+ else loadList(sid, 'all')
2217
+ }, [boot, subjectId, loadToday, loadList])
2130
2218
 
2131
2219
  const openQuestion = useCallback(async (qid) => {
2132
2220
  const request = ++dailyRequest.current
@@ -2168,12 +2256,14 @@ window.__ModuleLoader__.load({
2168
2256
  if (cancelled) return
2169
2257
  setBoot(result)
2170
2258
  const preferred = result.settings?.activeSubject
2171
- const first = preferred && result.settings.subjects.includes(preferred)
2259
+ const archived = archivedSubjects(result)
2260
+ const first = preferred && (result.settings.subjects.includes(preferred) || archived.some((subject) => subject.id === preferred))
2172
2261
  ? preferred
2173
- : (result.settings.subjects[0] || null)
2262
+ : (result.settings.subjects[0] || archived[0]?.id || null)
2174
2263
  setSubjectId(first)
2175
2264
  setLoading(false)
2176
- if (first) loadToday(first)
2265
+ if (first && result.settings.subjects.includes(first)) loadToday(first)
2266
+ else if (first) loadList(first, 'all')
2177
2267
  else setView('empty')
2178
2268
  }).catch((issue) => {
2179
2269
  if (cancelled) return
@@ -2181,7 +2271,7 @@ window.__ModuleLoader__.load({
2181
2271
  setLoading(false)
2182
2272
  })
2183
2273
  return () => { cancelled = true }
2184
- }, [loadToday])
2274
+ }, [loadToday, loadList])
2185
2275
 
2186
2276
  const currentSubject = useMemo(() => (boot?.subjects || []).find((subject) => subject.id === subjectId) || null,
2187
2277
  [boot, subjectId])
@@ -2189,22 +2279,35 @@ window.__ModuleLoader__.load({
2189
2279
  const reveal = useCallback(async (mode) => {
2190
2280
  if (!question) return
2191
2281
  const request = dailyRequest.current
2282
+ const attemptId = attemptRef.current
2192
2283
  setBusy('reveal')
2284
+ const showSaved = (saved) => {
2285
+ setQuestion(saved)
2286
+ redoQuestions.current.delete(question.id)
2287
+ setLocalRedo(false)
2288
+ }
2193
2289
  try {
2194
2290
  const result = await rpc('reveal', {
2195
2291
  qid: question.id,
2196
2292
  mode,
2197
2293
  choice: mode === 'submit' ? choice : '',
2198
- attemptId: attemptRef.current,
2294
+ attemptId,
2199
2295
  })
2200
- if (request === dailyRequest.current) {
2201
- setQuestion(result.question)
2202
- redoQuestions.current.delete(question.id)
2203
- setLocalRedo(false)
2204
- }
2296
+ if (request === dailyRequest.current) showSaved(result.question)
2205
2297
  refreshBoot()
2206
2298
  } catch (issue) {
2207
- notify(issue.message, 'bad')
2299
+ if (request !== dailyRequest.current) return
2300
+ const saved = await rpc('getQuestion', { qid: question.id, attemptId }).catch(() => null)
2301
+ if (request !== dailyRequest.current) return
2302
+ if (saved?.question?.lastAttempt?.id === attemptId) {
2303
+ showSaved(saved.question)
2304
+ const last = saved.question.lastAttempt
2305
+ const changed = last.mode !== mode || (mode === 'submit' && last.choice !== choice)
2306
+ notify(changed ? '已找回先前保存的作答。本次改选未提交,可点击「重新作答」再提交。' : '已找回本轮保存的作答', changed ? 'warn' : 'ok')
2307
+ refreshBoot()
2308
+ } else {
2309
+ notify(`${issue.message}。未确认本轮是否保存,可再次提交以核对。`, 'warn')
2310
+ }
2208
2311
  } finally {
2209
2312
  if (request === dailyRequest.current) setBusy('')
2210
2313
  }
@@ -2364,15 +2467,21 @@ window.__ModuleLoader__.load({
2364
2467
  }, [boot, notify, refreshBoot])
2365
2468
 
2366
2469
  const saveSubjects = async (draft) => {
2367
- const next = draft.subjects.includes(subjectId) ? subjectId : (draft.subjects[0] || null)
2470
+ const keepHistory = boot.counts?.[subjectId]?.all > 0
2471
+ const next = draft.subjects.includes(subjectId) || keepHistory ? subjectId : (draft.subjects[0] || null)
2368
2472
  const result = await rpc('saveSettings', { ...draft, activeSubject: next })
2473
+ bootRequest.current++
2369
2474
  setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
2370
2475
  setModal(null)
2371
2476
  notify('科目设置已保存')
2372
- if (next !== subjectId) {
2477
+ if (next !== subjectId || (kind === 'today' && !result.settings.subjects.includes(next))) {
2373
2478
  setSubjectId(next)
2374
- if (next) loadToday(next)
2375
- else { setQuestion(null); setThread(null); setThreadOpen(false); setView('empty') }
2479
+ if (next && result.settings.subjects.includes(next)) loadToday(next)
2480
+ else if (next) loadList(next, 'all')
2481
+ else {
2482
+ dailyRequest.current++
2483
+ setBusy(''); setFail(''); setQuestion(null); setThread(null); setThreadOpen(false); setView('empty')
2484
+ }
2376
2485
  }
2377
2486
  }
2378
2487
 
@@ -2405,10 +2514,10 @@ window.__ModuleLoader__.load({
2405
2514
  }
2406
2515
  }, [subjectId, notify])
2407
2516
 
2408
- const commitBank = useCallback(async (result) => {
2517
+ const commitBank = useCallback(async (result, excludedIndexes) => {
2409
2518
  setBusy('commit')
2410
2519
  try {
2411
- const saved = await rpc('commitBank', { importId: result.importId, name: result.name, subjectId: subjectId || '' })
2520
+ const saved = await rpc('commitBank', { importId: result.importId, name: result.name, subjectId: subjectId || '', excludedIndexes })
2412
2521
  setModal({ kind: 'banks' })
2413
2522
  await refreshBoot()
2414
2523
  notify(`题库《${saved.bank.name}》已入库,共 ${saved.bank.itemCount} 道题`)
@@ -2499,7 +2608,7 @@ window.__ModuleLoader__.load({
2499
2608
  })
2500
2609
  }
2501
2610
  if (loading) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '正在读取学习数据…')
2502
- if (!boot?.settings?.subjects?.length) {
2611
+ if (!subjectId && !boot?.settings?.subjects?.length && !archivedSubjects(boot || {}).length) {
2503
2612
  return h(EmptyState, {
2504
2613
  icon: 'plus',
2505
2614
  title: T.noSubjects,
@@ -2576,6 +2685,7 @@ window.__ModuleLoader__.load({
2576
2685
  })
2577
2686
  },
2578
2687
  onReveal: reveal,
2688
+ onSkip: () => kind === 'today' ? loadList(subjectId, 'all') : backToList(),
2579
2689
  onJudge: judge,
2580
2690
  onFavorite: favorite,
2581
2691
  onRedo: () => {
@@ -2616,7 +2726,9 @@ window.__ModuleLoader__.load({
2616
2726
 
2617
2727
  const pickTab = (value) => {
2618
2728
  if (value === kind && (value === 'today' || view === 'list')) return
2619
- if (value === 'today') loadToday(subjectId)
2729
+ if (value === 'today') {
2730
+ if (boot?.settings.subjects.includes(subjectId)) loadToday(subjectId)
2731
+ }
2620
2732
  else loadList(subjectId, value)
2621
2733
  }
2622
2734
  const showThread = view === 'detail' && question && threadOpen && !localRedo
@@ -2627,13 +2739,12 @@ window.__ModuleLoader__.load({
2627
2739
  h('div', { className: 'kd-header' },
2628
2740
  h('div', { className: 'kd-header-top' },
2629
2741
  h('div', { className: 'kd-header-left' },
2630
- h('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
2742
+ h(PluginLogo, { size: 40, className: 'kd-brand' }),
2631
2743
  h('div', { style: { minWidth: 0 } },
2632
2744
  h('h1', { className: 'kd-title' }, T.title),
2633
2745
  h('div', { className: 'kd-sub', title: T.subtitle },
2634
- boot?.today ? `${formatDate(boot.today)} · ` : '',
2635
- boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
2636
- '每天一道,稳步向前'))),
2746
+ boot?.today ? `今日日期:${formatDate(boot.today)}\u00a0\u00a0` : '',
2747
+ '每日一题,稳步向前'))),
2637
2748
  boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
2638
2749
  boot ? h(ExamStrip, {
2639
2750
  boot,
@@ -2662,6 +2773,7 @@ window.__ModuleLoader__.load({
2662
2773
  kind,
2663
2774
  counts,
2664
2775
  onPick: pickTab,
2776
+ todayDisabled: !boot.settings.subjects.includes(subjectId),
2665
2777
  }) : null,
2666
2778
  kind !== 'today' && view === 'detail'
2667
2779
  ? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
@@ -2699,8 +2811,10 @@ window.__ModuleLoader__.load({
2699
2811
  : null,
2700
2812
  modal?.kind === 'banks' && boot
2701
2813
  ? h(BanksModal, {
2814
+ key: modal.importResult?.importId || 'banks',
2702
2815
  boot,
2703
2816
  importResult: modal.importResult,
2817
+ committing: busy === 'commit',
2704
2818
  onClose: () => setModal(null),
2705
2819
  onPickFile: pickFile,
2706
2820
  onDelete: deleteBank,
@@ -2758,33 +2872,25 @@ window.__ModuleLoader__.load({
2758
2872
  return matches
2759
2873
  }
2760
2874
 
2761
- /* ============================== 侧栏入口 ============================== */
2762
- function SidebarIcon({ size = 16 }) {
2875
+ /* ============================== 插件图标 ============================== */
2876
+ function PluginLogo({ size = 16, className }) {
2877
+ const gradientId = useId()
2763
2878
  return h('svg', {
2764
2879
  width: size,
2765
2880
  height: size,
2766
- viewBox: '0 0 24 24',
2767
- fill: 'none',
2881
+ viewBox: '0 0 64 64',
2882
+ className,
2768
2883
  'aria-hidden': true,
2769
2884
  },
2770
- h('path', {
2771
- d: 'M4.6 2h14.8A2.6 2.6 0 0 1 22 4.6v14.8A2.6 2.6 0 0 1 19.4 22H4.6A2.6 2.6 0 0 1 2 19.4V4.6A2.6 2.6 0 0 1 4.6 2Z',
2772
- fill: 'currentColor',
2773
- opacity: 0.16,
2774
- }),
2775
- h('path', {
2776
- d: 'M8 5.2c.5 0 .9.4.9.9v.9h6.2v-.9a.9.9 0 0 1 1.8 0v.9h.3c.9 0 1.6.7 1.6 1.6v8.6c0 .9-.7 1.6-1.6 1.6H6.8c-.9 0-1.6-.7-1.6-1.6V8.6c0-.9.7-1.6 1.6-1.6h.3v-.9c0-.5.4-.9.9-.9Z',
2777
- stroke: 'currentColor',
2778
- strokeWidth: 1.6,
2779
- strokeLinejoin: 'round',
2780
- }),
2781
- h('path', {
2782
- d: 'M8.6 12.6l1.7 1.7 3.5-3.6',
2783
- stroke: 'currentColor',
2784
- strokeWidth: 1.8,
2785
- strokeLinecap: 'round',
2786
- strokeLinejoin: 'round',
2787
- }))
2885
+ h('defs', null,
2886
+ h('linearGradient', { id: gradientId, x1: 0, y1: 0, x2: 1, y2: 1 },
2887
+ h('stop', { offset: 0, stopColor: '#5676ff' }),
2888
+ h('stop', { offset: 1, stopColor: '#3b55e8' }))),
2889
+ h('rect', { x: 2, y: 2, width: 60, height: 60, rx: 16, fill: `url(#${gradientId})` }),
2890
+ h('g', { fill: 'none', stroke: '#fff', strokeLinecap: 'round', strokeLinejoin: 'round' },
2891
+ h('rect', { x: 15.5, y: 20, width: 33, height: 30, rx: 6.5, strokeWidth: 3.6 }),
2892
+ h('path', { d: 'M23 15.5V23M41 15.5V23', strokeWidth: 3.6 }),
2893
+ h('path', { d: 'm27.4 33.2 5.5-3.9v13.3m-4.4 0h9.2', strokeWidth: 4 })))
2788
2894
  }
2789
2895
 
2790
2896
  return {
@@ -2822,7 +2928,7 @@ window.__ModuleLoader__.load({
2822
2928
  id: 'daily-question',
2823
2929
  order: 20,
2824
2930
  label: () => T.title,
2825
- }, SidebarIcon)),
2931
+ }, PluginLogo)),
2826
2932
  slots.inject('main', () => slots.register({
2827
2933
  name: 'main',
2828
2934
  key: 'daily-question',