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.
- package/CHANGELOG.md +17 -0
- package/README.md +38 -26
- package/client.js +250 -144
- package/cordis.patch.yml +1 -1
- package/icon.svg +10 -7
- package/index.js +6 -6
- package/lib/bank.js +53 -36
- package/lib/exam-dates.js +101 -67
- package/lib/generate.js +62 -25
- package/lib/generation-history.js +8 -0
- package/lib/paper-types.js +47 -0
- package/lib/reference-reader.js +11 -8
- package/lib/references.js +72 -26
- package/lib/research.js +22 -13
- package/lib/routes.js +62 -95
- package/lib/service.js +73 -72
- package/lib/store.js +3 -8
- package/lib/subjects.js +12 -0
- package/package.json +2 -2
- package/vendor/mathjax/a11y/assistive-mml.js +1 -0
- package/vendor/mathjax/input/tex/extensions/action.js +1 -0
- package/vendor/mathjax/input/tex/extensions/ams.js +1 -0
- package/vendor/mathjax/input/tex/extensions/amscd.js +1 -0
- package/vendor/mathjax/input/tex/extensions/autoload.js +1 -0
- package/vendor/mathjax/input/tex/extensions/bbm.js +1 -0
- package/vendor/mathjax/input/tex/extensions/bboldx.js +1 -0
- package/vendor/mathjax/input/tex/extensions/bbox.js +1 -0
- package/vendor/mathjax/input/tex/extensions/begingroup.js +1 -0
- package/vendor/mathjax/input/tex/extensions/boldsymbol.js +1 -0
- package/vendor/mathjax/input/tex/extensions/braket.js +1 -0
- package/vendor/mathjax/input/tex/extensions/bussproofs.js +1 -0
- package/vendor/mathjax/input/tex/extensions/cancel.js +1 -0
- package/vendor/mathjax/input/tex/extensions/cases.js +1 -0
- package/vendor/mathjax/input/tex/extensions/centernot.js +1 -0
- package/vendor/mathjax/input/tex/extensions/color.js +1 -0
- package/vendor/mathjax/input/tex/extensions/colortbl.js +1 -0
- package/vendor/mathjax/input/tex/extensions/colorv2.js +1 -0
- package/vendor/mathjax/input/tex/extensions/configmacros.js +1 -0
- package/vendor/mathjax/input/tex/extensions/dsfont.js +1 -0
- package/vendor/mathjax/input/tex/extensions/empheq.js +1 -0
- package/vendor/mathjax/input/tex/extensions/enclose.js +1 -0
- package/vendor/mathjax/input/tex/extensions/extpfeil.js +1 -0
- package/vendor/mathjax/input/tex/extensions/fontsizev3.js +1 -0
- package/vendor/mathjax/input/tex/extensions/gensymb.js +1 -0
- package/vendor/mathjax/input/tex/extensions/html.js +1 -0
- package/vendor/mathjax/input/tex/extensions/mathtools.js +1 -0
- package/vendor/mathjax/input/tex/extensions/mhchem.js +34 -0
- package/vendor/mathjax/input/tex/extensions/newcommand.js +1 -0
- package/vendor/mathjax/input/tex/extensions/noerrors.js +1 -0
- package/vendor/mathjax/input/tex/extensions/noundefined.js +1 -0
- package/vendor/mathjax/input/tex/extensions/physics.js +1 -0
- package/vendor/mathjax/input/tex/extensions/require.js +1 -0
- package/vendor/mathjax/input/tex/extensions/setoptions.js +1 -0
- package/vendor/mathjax/input/tex/extensions/tagformat.js +1 -0
- package/vendor/mathjax/input/tex/extensions/texhtml.js +1 -0
- package/vendor/mathjax/input/tex/extensions/textcomp.js +1 -0
- package/vendor/mathjax/input/tex/extensions/textmacros.js +1 -0
- package/vendor/mathjax/input/tex/extensions/unicode.js +1 -0
- package/vendor/mathjax/input/tex/extensions/units.js +1 -0
- package/vendor/mathjax/input/tex/extensions/upgreek.js +1 -0
- package/vendor/mathjax/input/tex/extensions/verb.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/PUA.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/accents-b-i.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/accents.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/arabic.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/arrows.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/braille-d.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/braille.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/calligraphic.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/cherokee.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/cyrillic-ss.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/cyrillic.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/devanagari.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/double-struck.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/fraktur.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/greek-ss.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/greek.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/hebrew.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/latin-b.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/latin-bi.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/latin-i.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/latin.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/marrows.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/math.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/monospace-ex.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/monospace-l.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/monospace.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/mshapes.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/phonetics-ss.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/phonetics.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif-b.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif-bi.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif-ex.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif-i.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif-r.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/sans-serif.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/script.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/shapes.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/symbols-b-i.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/symbols.js +1 -0
- package/vendor/mathjax/newcm/svg/dynamic/variants.js +1 -0
- 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
|
|
114
|
-
|
|
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
|
-
|
|
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'
|
|
165
|
+
svg: { fontCache: 'local' },
|
|
138
166
|
}
|
|
139
167
|
const script = document.createElement('script')
|
|
140
|
-
script.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 = () =>
|
|
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
|
-
* -
|
|
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
|
-
|
|
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;
|
|
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:
|
|
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-
|
|
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 {
|
|
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
|
|
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] {
|
|
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
|
|
847
|
-
.kd-
|
|
848
|
-
|
|
849
|
-
.kd-math
|
|
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 =
|
|
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
|
-
|
|
1084
|
-
h('div', { className: 'kd-cd-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
-
},
|
|
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(
|
|
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 &&
|
|
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 &&
|
|
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 && !
|
|
1436
|
+
!revealed && !canSelect
|
|
1381
1437
|
? [
|
|
1382
1438
|
h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
|
|
1383
|
-
|
|
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
|
-
|
|
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]} · 共 ${
|
|
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:
|
|
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: !
|
|
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}
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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')
|
|
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(
|
|
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 ?
|
|
2635
|
-
|
|
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
|
|
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
|
|
2767
|
-
|
|
2881
|
+
viewBox: '0 0 64 64',
|
|
2882
|
+
className,
|
|
2768
2883
|
'aria-hidden': true,
|
|
2769
2884
|
},
|
|
2770
|
-
h('
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
}),
|
|
2775
|
-
h('
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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
|
-
},
|
|
2931
|
+
}, PluginLogo)),
|
|
2826
2932
|
slots.inject('main', () => slots.register({
|
|
2827
2933
|
name: 'main',
|
|
2828
2934
|
key: 'daily-question',
|