daily-question 1.0.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -116
- package/client.js +667 -716
- package/lib/exam-dates.js +10 -8
- package/lib/generate.js +3 -3
- package/lib/routes.js +15 -14
- package/lib/service.js +58 -14
- package/lib/store.js +3 -3
- package/package.json +2 -2
- package/vendor/mathjax/LICENSE +202 -0
- package/vendor/mathjax/tex-svg.js +34 -0
- package/assets/LICENSE-latin-modern-math.txt +0 -93
- package/assets/latinmodern-math.otf +0 -0
package/client.js
CHANGED
|
@@ -109,351 +109,119 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
return { text: '未作答', tone: 'idle' }
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/* ==============================
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
to: '→', rightarrow: '→', leftarrow: '←', leftrightarrow: '↔', Rightarrow: '⇒', Leftarrow: '⇐',
|
|
123
|
-
Leftrightarrow: '⇔', mapsto: '↦', implies: '⟹', iff: '⟺',
|
|
124
|
-
partial: '∂', nabla: '∇', forall: '∀', exists: '∃', nexists: '∄', neg: '¬', in: '∈', notin: '∉',
|
|
125
|
-
subset: '⊂', subseteq: '⊆', supset: '⊃', supseteq: '⊇', cup: '∪', cap: '∩', emptyset: '∅',
|
|
126
|
-
varnothing: '∅', setminus: '∖', land: '∧', lor: '∨',
|
|
127
|
-
mid: '∣', nmid: '∤', vert: '|', lvert: '|', rvert: '|',
|
|
128
|
-
Vert: '‖', lVert: '‖', rVert: '‖', colon: ':', '|': '‖',
|
|
129
|
-
ldots: '…', cdots: '⋯', vdots: '⋮', ddots: '⋱', dots: '…',
|
|
130
|
-
angle: '∠', perp: '⊥', parallel: '∥', circ: '∘', degree: '°', prime: '′',
|
|
131
|
-
lceil: '⌈', rceil: '⌉', lfloor: '⌊', rfloor: '⌋', langle: '⟨', rangle: '⟩',
|
|
132
|
-
ell: 'ℓ', Re: 'ℜ', Im: 'ℑ', hbar: 'ℏ',
|
|
133
|
-
therefore: '∴', because: '∵', triangle: '△', square: '□',
|
|
134
|
-
bullet: '∙', ni: '∋', backslash: '\\', Box: '□', bigcirc: '○', dagger: '†', ddagger: '‡',
|
|
135
|
-
quad: '\u2003', qquad: '\u2003\u2003',
|
|
136
|
-
'%': '%', '&': '&', '#': '#', '_': '_', '{': '{', '}': '}',
|
|
137
|
-
' ': ' ', ',': '\u2009', ';': '\u2005', ':': '\u2005', '!': '',
|
|
138
|
-
}
|
|
139
|
-
const LATEX_FUNCS = new Set(['sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'arcsin', 'arccos', 'arctan',
|
|
140
|
-
'sinh', 'cosh', 'tanh', 'ln', 'log', 'lg', 'exp', 'max', 'min', 'sup', 'inf', 'det', 'gcd', 'lcm',
|
|
141
|
-
'deg', 'dim', 'ker', 'arg', 'Pr'])
|
|
142
|
-
const LATEX_BIGOPS = {
|
|
143
|
-
sum: '∑', prod: '∏', int: '∫', iint: '∬', iiint: '∭', oint: '∮',
|
|
144
|
-
lim: 'lim', limsup: 'lim sup', liminf: 'lim inf', bigcup: '⋃', bigcap: '⋂',
|
|
145
|
-
}
|
|
146
|
-
const DOUBLE_STRUCK = { R: 'ℝ', N: 'ℕ', Z: 'ℤ', Q: 'ℚ', C: 'ℂ', P: 'ℙ' }
|
|
147
|
-
|
|
148
|
-
/** 连写撇号对应的 prime 字符(1~4 阶,更多阶用 4 阶 + 单撇补)。 */
|
|
149
|
-
const PRIME_CHARS = { 1: '\u2032', 2: '\u2033', 3: '\u2034', 4: '\u2057' }
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 数学模式里的字符修正。只处理公式内部的裸文本(\text / \mathrm 走原样,不受影响):
|
|
153
|
-
*
|
|
154
|
-
* 1. 连字符 `-` 在 LaTeX 数学模式里是**减号**,换成 U+2212 MINUS SIGN。
|
|
155
|
-
* 留成 U+002D 的话,Latin Modern Math 会画成一个短横,看着像分隔符。
|
|
156
|
-
* 2. 撇号 `'`(含中文输入法常见的 ’)是**导数记号**,换成 U+2032 起的 prime
|
|
157
|
-
* 连写:' → ′、'' → ″、''' → ‴、'''' → ⁗。
|
|
158
|
-
* 3. ASCII 字母换成 Unicode 数学斜体码位(U+1D434… 大写、U+1D44E… 小写,
|
|
159
|
-
* 例外 h → U+210E),Latin Modern Math 与 Cambria Math 都完整覆盖,
|
|
160
|
-
* 因此变量是真斜体,而数字、运算符、函数名(\sin 等)保持直立。
|
|
161
|
-
*/
|
|
162
|
-
function toMathText(text) {
|
|
163
|
-
return String(text)
|
|
164
|
-
.replace(/['\u2018\u2019]+/g, (run) => {
|
|
165
|
-
const count = [...run].length
|
|
166
|
-
if (count <= 4) return PRIME_CHARS[count]
|
|
167
|
-
return '\u2057' + '\u2032'.repeat(count - 4)
|
|
168
|
-
})
|
|
169
|
-
.replace(/-/g, '\u2212')
|
|
170
|
-
.replace(/\*/g, '\u2217')
|
|
171
|
-
.replace(/\.{3,}/g, '\u2026')
|
|
172
|
-
.replace(/[A-Za-z]/g, (ch) => {
|
|
173
|
-
if (ch === 'h') return '\u210E'
|
|
174
|
-
const code = ch.charCodeAt(0)
|
|
175
|
-
if (code >= 97 && code <= 122) return String.fromCodePoint(0x1d44e + (code - 97))
|
|
176
|
-
return String.fromCodePoint(0x1d434 + (code - 65))
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function readGroup(cur) {
|
|
181
|
-
// cur.src[cur.i] === '{'
|
|
182
|
-
let depth = 0
|
|
183
|
-
let j = cur.i
|
|
184
|
-
const start = cur.i + 1
|
|
185
|
-
while (j < cur.src.length) {
|
|
186
|
-
const c = cur.src[j]
|
|
187
|
-
if (c === '\\') { j += 2; continue }
|
|
188
|
-
if (c === '{') depth += 1
|
|
189
|
-
else if (c === '}') {
|
|
190
|
-
depth -= 1
|
|
191
|
-
if (depth === 0) {
|
|
192
|
-
cur.i = j + 1
|
|
193
|
-
return cur.src.slice(start, j)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
j += 1
|
|
112
|
+
/* ============================== 公式(MathJax 3) ============================== */
|
|
113
|
+
// MathJax 的 SVG 输出自带字形路径,不依赖任何字体文件;单文件 bundle 随包分发,
|
|
114
|
+
// 由宿主在 /api/daily-question/vendor/tex-svg.js 提供。
|
|
115
|
+
const MATHJAX_SRC = '/api/daily-question/vendor/tex-svg.js'
|
|
116
|
+
const mathState = { status: 'idle' }
|
|
117
|
+
const mathWatchers = new Set()
|
|
118
|
+
|
|
119
|
+
function notifyMath() {
|
|
120
|
+
for (const watcher of [...mathWatchers]) {
|
|
121
|
+
try { watcher() } catch (error) { /* 单个订阅出错不影响其它 */ }
|
|
197
122
|
}
|
|
198
|
-
cur.i = cur.src.length
|
|
199
|
-
return cur.src.slice(start)
|
|
200
123
|
}
|
|
201
124
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const end = cur.src.indexOf(']', cur.i)
|
|
206
|
-
if (end < 0) return null
|
|
207
|
-
const body = cur.src.slice(cur.i + 1, end)
|
|
208
|
-
cur.i = end + 1
|
|
209
|
-
return body
|
|
125
|
+
function subscribeMath(fn) {
|
|
126
|
+
mathWatchers.add(fn)
|
|
127
|
+
return () => { mathWatchers.delete(fn) }
|
|
210
128
|
}
|
|
211
129
|
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return latexNodes(command[0])
|
|
130
|
+
function ensureMathJax() {
|
|
131
|
+
if (mathState.status !== 'idle') return
|
|
132
|
+
mathState.status = 'loading'
|
|
133
|
+
window.MathJax = {
|
|
134
|
+
startup: { typeset: false },
|
|
135
|
+
// 每条公式自带 <defs>,注入 innerHTML 后不依赖全局字体缓存
|
|
136
|
+
svg: { fontCache: 'local', scale: 1, matchFontHeight: false },
|
|
220
137
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
138
|
+
const script = document.createElement('script')
|
|
139
|
+
script.src = MATHJAX_SRC
|
|
140
|
+
script.async = true
|
|
141
|
+
script.onload = () => {
|
|
142
|
+
const startup = window.MathJax && window.MathJax.startup
|
|
143
|
+
const done = () => {
|
|
144
|
+
mathState.status = 'ready'
|
|
145
|
+
ensureMathStylesheet()
|
|
146
|
+
notifyMath()
|
|
147
|
+
}
|
|
148
|
+
const failed = () => { mathState.status = 'failed'; notifyMath() }
|
|
149
|
+
if (startup && startup.promise) startup.promise.then(done, failed)
|
|
150
|
+
else done()
|
|
224
151
|
}
|
|
225
|
-
|
|
226
|
-
|
|
152
|
+
script.onerror = () => { mathState.status = 'failed'; notifyMath() }
|
|
153
|
+
document.head.appendChild(script)
|
|
227
154
|
}
|
|
228
155
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
break
|
|
156
|
+
/** SVG 输出依赖 MathJax 自己生成的样式表;手动调用 tex2svg 时不会自动插,补一次。 */
|
|
157
|
+
function ensureMathStylesheet() {
|
|
158
|
+
try {
|
|
159
|
+
const mathJax = window.MathJax
|
|
160
|
+
if (!mathJax || typeof mathJax.svgStylesheet !== 'function') return
|
|
161
|
+
if (document.getElementById('MJX-SVG-styles')) return
|
|
162
|
+
const sheet = mathJax.svgStylesheet()
|
|
163
|
+
if (sheet) document.head.appendChild(sheet)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// 没有样式表也能显示,只是对齐略差,不阻断渲染
|
|
240
166
|
}
|
|
241
|
-
return { sub, sup }
|
|
242
167
|
}
|
|
243
168
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const wrapCls = env === 'cases' ? 'kd-cases'
|
|
255
|
-
: env === 'bmatrix' ? 'kd-bmatrix'
|
|
256
|
-
: env === 'vmatrix' ? 'kd-vmatrix'
|
|
257
|
-
: env === 'pmatrix' ? 'kd-pmatrix' : 'kd-matrixwrap'
|
|
258
|
-
const open = env === 'cases' ? '{' : env === 'bmatrix' ? '[' : env === 'pmatrix' ? '(' : ''
|
|
259
|
-
const close = env === 'bmatrix' ? ']' : env === 'pmatrix' ? ')' : ''
|
|
260
|
-
const children = []
|
|
261
|
-
if (open) children.push(h('span', { className: 'kd-delim', key: 'open' }, open))
|
|
262
|
-
children.push(table)
|
|
263
|
-
if (close) children.push(h('span', { className: 'kd-delim', key: 'close' }, close))
|
|
264
|
-
return h('span', { className: `kd-matrixbox ${wrapCls}`, key: nextKey('mb') }, children)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function latexNodes(src) {
|
|
268
|
-
const cur = { src: String(src || ''), i: 0 }
|
|
269
|
-
return latexSeq(cur, '')
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function latexSeq(cur, stopChars) {
|
|
273
|
-
const out = []
|
|
274
|
-
let buf = ''
|
|
275
|
-
const flush = () => {
|
|
276
|
-
if (buf) {
|
|
277
|
-
out.push(toMathText(buf))
|
|
278
|
-
buf = ''
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
while (cur.i < cur.src.length) {
|
|
282
|
-
const c = cur.src[cur.i]
|
|
283
|
-
if (stopChars && stopChars.includes(c)) break
|
|
284
|
-
if (c === '}') { buf += c; cur.i += 1; continue }
|
|
285
|
-
if (c === '{') {
|
|
286
|
-
flush()
|
|
287
|
-
out.push(...latexNodes(readGroup(cur)))
|
|
288
|
-
continue
|
|
289
|
-
}
|
|
290
|
-
if (c === '^' || c === '_') {
|
|
291
|
-
flush()
|
|
292
|
-
cur.i += 1
|
|
293
|
-
const arg = readArg(cur)
|
|
294
|
-
out.push(h(c === '^' ? 'sup' : 'sub', { className: c === '^' ? 'kd-sup' : 'kd-sub', key: nextKey('sc') }, arg))
|
|
295
|
-
continue
|
|
296
|
-
}
|
|
297
|
-
if (c === '&') {
|
|
298
|
-
flush()
|
|
299
|
-
cur.i += 1
|
|
300
|
-
out.push(h('span', { className: 'kd-colsep', key: nextKey('cs') }, '\u2003'))
|
|
301
|
-
continue
|
|
302
|
-
}
|
|
303
|
-
if (c === '~') {
|
|
304
|
-
buf += '\u00a0'
|
|
305
|
-
cur.i += 1
|
|
306
|
-
continue
|
|
307
|
-
}
|
|
308
|
-
if (c === '\\') {
|
|
309
|
-
const rest = cur.src.slice(cur.i)
|
|
310
|
-
const command = /^\\([A-Za-z]+)/.exec(rest)
|
|
311
|
-
if (command) {
|
|
312
|
-
flush()
|
|
313
|
-
cur.i += command[0].length
|
|
314
|
-
const node = latexCommand(cur, command[1])
|
|
315
|
-
if (node !== null && node !== undefined) out.push(node)
|
|
316
|
-
continue
|
|
317
|
-
}
|
|
318
|
-
const symbol = rest[1]
|
|
319
|
-
cur.i += symbol === undefined ? 1 : 2
|
|
320
|
-
if (symbol === '\\') {
|
|
321
|
-
flush()
|
|
322
|
-
out.push(h('br', { key: nextKey('br') }))
|
|
323
|
-
continue
|
|
324
|
-
}
|
|
325
|
-
if (LATEX_SYMBOLS[symbol] !== undefined) buf += LATEX_SYMBOLS[symbol]
|
|
326
|
-
else if (symbol !== undefined) buf += symbol
|
|
327
|
-
continue
|
|
328
|
-
}
|
|
329
|
-
if (c === ' ') { buf += ' '; cur.i += 1; continue }
|
|
330
|
-
buf += c
|
|
331
|
-
cur.i += 1
|
|
332
|
-
}
|
|
333
|
-
flush()
|
|
334
|
-
return out
|
|
169
|
+
/** 让组件跟着公式引擎的加载状态重渲染。 */
|
|
170
|
+
function useMathJaxStatus() {
|
|
171
|
+
const [status, setStatus] = useState(mathState.status)
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
ensureMathJax()
|
|
174
|
+
const update = () => setStatus(mathState.status)
|
|
175
|
+
update()
|
|
176
|
+
return subscribeMath(update)
|
|
177
|
+
}, [])
|
|
178
|
+
return status
|
|
335
179
|
}
|
|
336
180
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (name === 'binom' || name === 'dbinom' || name === 'tbinom') {
|
|
359
|
-
const top = readArg(cur)
|
|
360
|
-
const bottom = readArg(cur)
|
|
361
|
-
return h('span', { className: 'kd-binom', key: nextKey('bn') },
|
|
362
|
-
h('span', { className: 'kd-delim', key: 'l' }, '('),
|
|
363
|
-
h('span', { className: 'kd-frac kd-binom-body', key: 'b' },
|
|
364
|
-
h('span', { className: 'kd-frac-n', key: 'n' }, top),
|
|
365
|
-
h('span', { className: 'kd-frac-d', key: 'd' }, bottom)),
|
|
366
|
-
h('span', { className: 'kd-delim', key: 'r' }, ')'))
|
|
367
|
-
}
|
|
368
|
-
if (name === 'sqrt') {
|
|
369
|
-
const index = readOptionalGroup(cur)
|
|
370
|
-
const body = readArg(cur)
|
|
371
|
-
return h('span', { className: 'kd-sqrt', key: nextKey('sq') },
|
|
372
|
-
h('span', { className: 'kd-sqrt-sign', key: 's' }, '√'),
|
|
373
|
-
index ? h('sup', { className: 'kd-sqrt-index', key: 'i' }, latexNodes(index)) : null,
|
|
374
|
-
h('span', { className: 'kd-sqrt-body', key: 'b' }, body))
|
|
375
|
-
}
|
|
376
|
-
if (name === 'text' || name === 'textrm' || name === 'mbox' || name === 'mathrm' || name === 'operatorname') {
|
|
377
|
-
const raw = readGroup(cur)
|
|
378
|
-
return h('span', { className: 'kd-roman', key: nextKey('rm') }, raw.replace(/\\([,;: !])/g, ' '))
|
|
379
|
-
}
|
|
380
|
-
if (name === 'mathbf' || name === 'boldsymbol' || name === 'bm' || name === 'textbf') {
|
|
381
|
-
return h('strong', { className: 'kd-bold', key: nextKey('bd') }, latexNodes(readGroup(cur)))
|
|
382
|
-
}
|
|
383
|
-
if (name === 'mathbb' || name === 'mathcal' || name === 'mathscr') {
|
|
384
|
-
const raw = readGroup(cur).trim()
|
|
385
|
-
return h('span', { className: 'kd-blackboard', key: nextKey('bb') }, DOUBLE_STRUCK[raw] || raw)
|
|
386
|
-
}
|
|
387
|
-
if (name === 'overline' || name === 'bar' || name === 'underline' || name === 'vec'
|
|
388
|
-
|| name === 'hat' || name === 'widehat' || name === 'tilde' || name === 'dot' || name === 'ddot') {
|
|
389
|
-
const body = readArg(cur)
|
|
390
|
-
return h('span', { className: `kd-accent kd-accent-${name}`, key: nextKey('ac') }, body)
|
|
391
|
-
}
|
|
392
|
-
if (name === 'overrightarrow' || name === 'overleftarrow') {
|
|
393
|
-
return h('span', { className: `kd-accent kd-accent-${name}`, key: nextKey('ac') }, readArg(cur))
|
|
394
|
-
}
|
|
395
|
-
// \underbrace{x}_{说明} / \overbrace{x}^{说明}:花括号画不出来,保留内容与标记
|
|
396
|
-
if (name === 'underbrace' || name === 'overbrace') {
|
|
397
|
-
return h('span', { className: 'kd-group', key: nextKey('grp') }, readArg(cur))
|
|
398
|
-
}
|
|
399
|
-
// \overset{上}{基} / \underset{下}{基}:上下堆叠
|
|
400
|
-
if (name === 'overset' || name === 'underset') {
|
|
401
|
-
const mark = readArg(cur)
|
|
402
|
-
const base = readArg(cur)
|
|
403
|
-
return h('span', {
|
|
404
|
-
className: `kd-stack kd-stack-${name === 'overset' ? 'over' : 'under'}`,
|
|
405
|
-
key: nextKey('st'),
|
|
406
|
-
},
|
|
407
|
-
name === 'overset' ? h('span', { className: 'kd-stack-mark', key: 'm' }, mark) : null,
|
|
408
|
-
h('span', { className: 'kd-stack-base', key: 'b' }, base),
|
|
409
|
-
name === 'underset' ? h('span', { className: 'kd-stack-mark', key: 'm' }, mark) : null)
|
|
410
|
-
}
|
|
411
|
-
if (name === 'left' || name === 'right' || name === 'middle') {
|
|
412
|
-
while (cur.i < cur.src.length && cur.src[cur.i] === ' ') cur.i += 1
|
|
413
|
-
const rest = cur.src.slice(cur.i)
|
|
414
|
-
const command = /^\\[A-Za-z]+/.exec(rest)
|
|
415
|
-
let token
|
|
416
|
-
if (command) { token = command[0]; cur.i += command[0].length } else { token = rest[0] || ''; cur.i += 1 }
|
|
417
|
-
const display = token === '.' ? '' : (LATEX_SYMBOLS[token.replace(/^\\/, '')] || token.replace(/^\\/, ''))
|
|
418
|
-
return h('span', { className: 'kd-delim', key: nextKey('dl') }, display)
|
|
419
|
-
}
|
|
420
|
-
if (name === 'begin') {
|
|
421
|
-
const env = readGroup(cur).trim()
|
|
422
|
-
return readMatrix(cur, env)
|
|
423
|
-
}
|
|
424
|
-
if (name === 'displaystyle' || name === 'textstyle' || name === 'limits' || name === 'nolimits' || name === 'notag'
|
|
425
|
-
|| name === 'nonumber' || name === 'label' || name === 'tag') {
|
|
426
|
-
// \tag{...} 之类的排版修饰我们不实现,但也不能把内容原样吐到公式里
|
|
427
|
-
if (name === 'tag' || name === 'label') readGroup(cur)
|
|
428
|
-
return null
|
|
429
|
-
}
|
|
430
|
-
// \big( \Big[ \bigl\{ … 只是放大定界符,实际字符由后面的记号自己渲染
|
|
431
|
-
if (/^(?:big|Big|bigg|Bigg)[lrm]?$/.test(name)) return null
|
|
432
|
-
if (name === 'bmod') return h('span', { className: 'kd-roman', key: nextKey('bm') }, 'mod')
|
|
433
|
-
if (name === 'pmod') {
|
|
434
|
-
return h('span', { className: 'kd-roman', key: nextKey('pm') }, '(', 'mod ',
|
|
435
|
-
...readArg(cur), ')')
|
|
181
|
+
/**
|
|
182
|
+
* 规范化模型输出的 TeX:
|
|
183
|
+
* - 去掉自带的 $ / $$ 包裹;
|
|
184
|
+
* - 过滤能改页面结构或注入链接的宏(公式来自模型输出,不当作可信内容);
|
|
185
|
+
* - 公式块里的裸 \\ 换行在 MathJax 3 需要放进 \displaylines(Typora 也是这么处理的);
|
|
186
|
+
* - 其余交给 MathJax,不再自己做子集解析。
|
|
187
|
+
*/
|
|
188
|
+
const RISKY_TEX = [
|
|
189
|
+
/\\href\s*\{[^{}]*\}/g,
|
|
190
|
+
/\\url\s*\{[^{}]*\}/g,
|
|
191
|
+
/\\(?:style|class|cssId|htmlId|htmlClass|htmlStyle)\s*\{[^{}]*\}/g,
|
|
192
|
+
/\\require\s*\{[^{}]*\}/g,
|
|
193
|
+
/\\includegraphics\s*(?:\[[^\]]*\])?\s*\{[^{}]*\}/g,
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
function normalizeTeX(source, display) {
|
|
197
|
+
let tex = String(source || '').trim()
|
|
198
|
+
tex = tex.replace(/^\${1,2}/, '').replace(/\${1,2}$/, '')
|
|
199
|
+
for (const pattern of RISKY_TEX) tex = tex.replace(pattern, '')
|
|
200
|
+
if (display && /\\\\/.test(tex) && !/\\displaylines/.test(tex) && !/\\begin\{/.test(tex)) {
|
|
201
|
+
tex = `\\displaylines{${tex}}`
|
|
436
202
|
}
|
|
437
|
-
|
|
438
|
-
if (name === 'hline') return null
|
|
439
|
-
if (name === 'space' || name === 'newline') return h('br', { key: nextKey('br') })
|
|
440
|
-
if (LATEX_FUNCS.has(name)) return h('span', { className: 'kd-roman', key: nextKey('fn') }, name)
|
|
441
|
-
if (LATEX_SYMBOLS[name] !== undefined) return LATEX_SYMBOLS[name]
|
|
442
|
-
// 未知命令:保留原始内容并提示,绝不丢弃。
|
|
443
|
-
return h('span', { className: 'kd-latex-unknown', title: `未知的 LaTeX 命令:\\${name}`, key: nextKey('un') }, `\\${name}`)
|
|
203
|
+
return tex
|
|
444
204
|
}
|
|
445
205
|
|
|
446
206
|
function renderMath(source, display) {
|
|
447
|
-
|
|
207
|
+
const tex = normalizeTeX(source, display)
|
|
208
|
+
const key = nextKey('math')
|
|
209
|
+
if (mathState.status !== 'ready') {
|
|
210
|
+
// 引擎还没就绪(或加载失败):先显示原文,就绪后组件会重渲染
|
|
211
|
+
return h('span', { className: 'kd-math-pending', key, title: '公式引擎正在加载' }, tex)
|
|
212
|
+
}
|
|
448
213
|
try {
|
|
449
|
-
|
|
214
|
+
const node = window.MathJax.tex2svg(tex, { display: Boolean(display) })
|
|
215
|
+
const html = node && node.outerHTML
|
|
216
|
+
if (!html) throw new Error('empty svg')
|
|
217
|
+
return h(display ? 'div' : 'span', {
|
|
218
|
+
className: display ? 'kd-mathblock' : 'kd-math',
|
|
219
|
+
key,
|
|
220
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
221
|
+
})
|
|
450
222
|
} catch (error) {
|
|
451
|
-
|
|
223
|
+
return h('span', { className: 'kd-math-error', key, title: '公式无法渲染' }, tex)
|
|
452
224
|
}
|
|
453
|
-
return h(display ? 'div' : 'span', {
|
|
454
|
-
className: display ? 'kd-mathblock' : 'kd-math',
|
|
455
|
-
key: nextKey('math'),
|
|
456
|
-
}, nodes)
|
|
457
225
|
}
|
|
458
226
|
|
|
459
227
|
/* ============================== Markdown ============================== */
|
|
@@ -484,6 +252,10 @@ window.__ModuleLoader__.load({
|
|
|
484
252
|
})
|
|
485
253
|
}
|
|
486
254
|
|
|
255
|
+
/** Markdown 里有转义意义的字符:只有这些才允许把反斜杠吃掉。 */
|
|
256
|
+
const MD_ESCAPABLE = new Set(['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')',
|
|
257
|
+
'#', '+', '-', '.', '!', '>', '|', '~', '<', '$', '"', "'"])
|
|
258
|
+
|
|
487
259
|
function inlineNodes(text) {
|
|
488
260
|
const out = []
|
|
489
261
|
let buffer = ''
|
|
@@ -504,8 +276,16 @@ window.__ModuleLoader__.load({
|
|
|
504
276
|
}
|
|
505
277
|
}
|
|
506
278
|
if (c === '\\' && index + 1 < text.length) {
|
|
507
|
-
|
|
508
|
-
|
|
279
|
+
const next = text[index + 1]
|
|
280
|
+
// 只有 Markdown 里有意义的字符才去掉反斜杠;其余原样保留,
|
|
281
|
+
// 免得把 \nabla、\neq 这类命令悄悄变成 "nabla"、"neq"。
|
|
282
|
+
if (MD_ESCAPABLE.has(next)) {
|
|
283
|
+
buffer += next
|
|
284
|
+
index += 2
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
buffer += c
|
|
288
|
+
index += 1
|
|
509
289
|
continue
|
|
510
290
|
}
|
|
511
291
|
if (c === '`') {
|
|
@@ -634,7 +414,16 @@ window.__ModuleLoader__.load({
|
|
|
634
414
|
}
|
|
635
415
|
|
|
636
416
|
function renderBlocks(source) {
|
|
637
|
-
|
|
417
|
+
// 模型偶尔把换行写成字面量 `\n`(JSON 里多转义了一层),这里先还原成真换行,
|
|
418
|
+
// 否则会渲染成一串多余的字母 n,段落结构也会塌成一行。
|
|
419
|
+
// 用 (?!\w) 保证不会误伤 \nabla / \neq / \theta / \times 这类命令。
|
|
420
|
+
const normalized = String(source || '')
|
|
421
|
+
.replace(/\\r\\n/g, '\n')
|
|
422
|
+
.replace(/\\n(?!\w)/g, '\n')
|
|
423
|
+
.replace(/\\r(?!\w)/g, '\n')
|
|
424
|
+
.replace(/\\t(?!\w)/g, ' ')
|
|
425
|
+
.replace(/\r\n?/g, '\n')
|
|
426
|
+
const lines = normalized.split('\n')
|
|
638
427
|
const out = []
|
|
639
428
|
let index = 0
|
|
640
429
|
while (index < lines.length) {
|
|
@@ -738,13 +527,15 @@ window.__ModuleLoader__.load({
|
|
|
738
527
|
}
|
|
739
528
|
|
|
740
529
|
function Markdown({ text, className }) {
|
|
530
|
+
// 公式引擎加载完成后要重渲染一次,否则首次绘制出来的还是 TeX 原文
|
|
531
|
+
const mathStatus = useMathJaxStatus()
|
|
741
532
|
const nodes = useMemo(() => {
|
|
742
533
|
try {
|
|
743
534
|
return renderBlocks(text)
|
|
744
535
|
} catch (error) {
|
|
745
536
|
return [h('pre', { className: 'kd-code', key: 'raw' }, String(text || ''))]
|
|
746
537
|
}
|
|
747
|
-
}, [text])
|
|
538
|
+
}, [text, mathStatus])
|
|
748
539
|
return h('div', { className: `kd-md ${className || ''}` }, nodes)
|
|
749
540
|
}
|
|
750
541
|
|
|
@@ -752,245 +543,273 @@ window.__ModuleLoader__.load({
|
|
|
752
543
|
/** 由 apply 用 styles.insert 注入时置位;否则由组件渲染 <style> 兜底。 */
|
|
753
544
|
let styleInjected = false
|
|
754
545
|
const CSS = `
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
546
|
+
.kd-page {
|
|
547
|
+
--kd-bg-base:var(--dsw-alias-bg-base, #ffffff);
|
|
548
|
+
--kd-bg-layer-1:var(--dsw-alias-bg-layer-1, #ffffff);
|
|
549
|
+
--kd-bg-layer-2:color-mix(in srgb, var(--kd-ink) 5%, var(--kd-surface));
|
|
550
|
+
--kd-bg-overlay:var(--dsw-alias-bg-overlay, #ffffff);
|
|
551
|
+
--kd-label-primary:var(--dsw-alias-label-primary, #24332f);
|
|
552
|
+
--kd-label-secondary:var(--dsw-alias-label-secondary, #66736e);
|
|
553
|
+
--kd-border-l1:color-mix(in srgb, var(--kd-ink) 9%, var(--kd-surface));
|
|
554
|
+
--kd-border-l2:color-mix(in srgb, var(--kd-ink) 17%, var(--kd-surface));
|
|
555
|
+
--kd-brand-primary:#4d6bfe;
|
|
556
|
+
--kd-state-success-primary:#078443;
|
|
557
|
+
--kd-state-error-primary:#db3144;
|
|
558
|
+
--kd-state-warn-primary:#95702b;
|
|
559
|
+
--kd-state-idle-primary:#85918b;
|
|
560
|
+
--kd-ink:var(--kd-label-primary); --kd-surface:var(--kd-bg-base);
|
|
561
|
+
font-family:inherit; isolation:isolate; height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
|
|
562
|
+
background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); color: var(--kd-label-primary);
|
|
763
563
|
font-size:14px; line-height:1.65; -webkit-font-smoothing:antialiased; box-sizing:border-box; }
|
|
764
564
|
.kd-page *, .kd-page *::before, .kd-page *::after { box-sizing:border-box; }
|
|
765
|
-
.kd-page :focus-visible { outline:2px solid var(--
|
|
766
|
-
.kd-header { padding:
|
|
565
|
+
.kd-page :focus-visible { outline:2px solid var(--kd-brand-primary); outline-offset:2px; }
|
|
566
|
+
.kd-header { padding:20px 28px 12px; background:var(--kd-surface); border-bottom:1px solid var(--kd-border-l1);
|
|
767
567
|
display:flex; flex-direction:column; gap:12px; }
|
|
768
|
-
.kd-header-top { display:flex; gap:
|
|
769
|
-
.kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:
|
|
770
|
-
.kd-brand { flex:0 0
|
|
771
|
-
justify-content:center; border:
|
|
772
|
-
color:var(--
|
|
773
|
-
.kd-title { margin:0; font-size:
|
|
774
|
-
.kd-sub { margin-top:3px; font-size:12px; color:var(--
|
|
568
|
+
.kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
|
|
569
|
+
.kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:center; }
|
|
570
|
+
.kd-brand { flex:0 0 40px; width:40px; height:40px; border-radius:12px; display:flex; align-items:center;
|
|
571
|
+
justify-content:center; border:0; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface));
|
|
572
|
+
color:var(--kd-brand-primary); }
|
|
573
|
+
.kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
|
|
574
|
+
.kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
|
|
775
575
|
.kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
|
|
776
|
-
border:1px solid var(--
|
|
777
|
-
color:var(--
|
|
778
|
-
.kd-select:hover { border-color:var(--
|
|
576
|
+
border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
|
|
577
|
+
color:var(--kd-label-primary); transition:border-color .15s; }
|
|
578
|
+
.kd-select:hover { border-color:var(--kd-brand-primary); }
|
|
779
579
|
.kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
|
|
780
580
|
.kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
|
|
781
581
|
gap:2px; text-align:right; }
|
|
782
|
-
.kd-cd-primary { font-size:
|
|
783
|
-
.kd-cd-secondary { font-size:12px; color:var(--
|
|
784
|
-
.kd-cd-target { font-size:
|
|
785
|
-
.kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px
|
|
786
|
-
border:1px solid var(--
|
|
787
|
-
font-size:11.5px; color:var(--
|
|
582
|
+
.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; }
|
|
583
|
+
.kd-cd-secondary { font-size:12px; color:var(--kd-label-secondary); }
|
|
584
|
+
.kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-secondary); }
|
|
585
|
+
.kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
|
|
586
|
+
border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
|
|
587
|
+
font-size:11.5px; color:var(--kd-label-secondary); }
|
|
788
588
|
.kd-strip-item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
|
789
|
-
.kd-strip-warn { color:var(--
|
|
589
|
+
.kd-strip-warn { color:var(--kd-state-warn-primary); white-space:normal; text-align:left; }
|
|
790
590
|
.kd-shell { flex:1 1 auto; min-height:0; display:flex; }
|
|
791
|
-
.kd-nav { width:
|
|
792
|
-
padding:
|
|
591
|
+
.kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
|
|
592
|
+
padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
|
|
793
593
|
.kd-nav-group { display:flex; flex-direction:column; gap:4px; }
|
|
794
|
-
.kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--
|
|
594
|
+
.kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
|
|
795
595
|
padding:0 8px 4px; }
|
|
796
|
-
.kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:
|
|
797
|
-
width:100%; padding:
|
|
798
|
-
cursor:pointer; color:var(--
|
|
596
|
+
.kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
|
|
597
|
+
width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
|
|
598
|
+
cursor:pointer; color:var(--kd-label-primary); font:inherit; font-size:13px; text-align:left;
|
|
799
599
|
transition:background-color .15s, border-color .15s, color .15s; }
|
|
800
|
-
.kd-nav-item:hover { background:var(--
|
|
801
|
-
.kd-nav-item > span:first-child { min-width:0; overflow
|
|
802
|
-
.kd-nav-item.is-active { background:var(--
|
|
803
|
-
|
|
600
|
+
.kd-nav-item:hover { background:var(--kd-bg-layer-2); }
|
|
601
|
+
.kd-nav-item > span:first-child { min-width:0; overflow-wrap:anywhere; line-height:1.6; }
|
|
602
|
+
.kd-nav-item.is-active { background:color-mix(in srgb, var(--kd-brand-primary) 9%, var(--kd-surface)); border-color:transparent;
|
|
603
|
+
color:var(--kd-brand-primary); font-weight:600; }
|
|
804
604
|
.kd-nav-item .kd-nav-count { flex:0 0 auto; white-space:nowrap; font-size:11px;
|
|
805
|
-
color:var(--
|
|
605
|
+
color:var(--kd-label-secondary); }
|
|
806
606
|
.kd-nav-status { flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; white-space:nowrap;
|
|
807
|
-
font-size:10.5px; color:var(--
|
|
808
|
-
.kd-dot { width:6px; height:6px; border-radius:50%; background:var(--
|
|
809
|
-
.kd-dot-idle { background:var(--
|
|
607
|
+
font-size:10.5px; color:var(--kd-label-secondary); }
|
|
608
|
+
.kd-dot { width:6px; height:6px; border-radius:50%; background:var(--kd-state-success-primary); }
|
|
609
|
+
.kd-dot-idle { background:var(--kd-state-idle-primary); }
|
|
810
610
|
.kd-nav-spacer { flex:1 1 auto; }
|
|
811
|
-
.kd-main { flex:1 1 auto; min-width:0; overflow:
|
|
812
|
-
.kd-main-
|
|
813
|
-
.kd-
|
|
814
|
-
|
|
815
|
-
.kd-
|
|
611
|
+
.kd-main { flex:1 1 auto; min-width:0; overflow:hidden; display:flex; flex-direction:column; }
|
|
612
|
+
.kd-main-toolbar { flex:none; padding:24px 28px 20px; display:flex; align-items:center; gap:12px; }
|
|
613
|
+
.kd-main-toolbar > .kd-btn { flex:none; align-self:stretch; aspect-ratio:1; padding:0; min-height:0; white-space:nowrap; border-radius:12px; }
|
|
614
|
+
.kd-main-scroll { flex:1; min-height:0; overflow:auto; padding:0 28px 48px; scrollbar-gutter:stable; }
|
|
615
|
+
.kd-main-inner { width:100%; margin:0; }
|
|
616
|
+
.kd-thread { position:relative; width:clamp(280px,calc((100% - 204px) * var(--kd-thread-ratio, .33)),55%); flex:0 0 auto; border-left:1px solid var(--kd-border-l1);
|
|
617
|
+
display:flex; flex-direction:column; min-height:0; background:var(--kd-bg-layer-1); }
|
|
618
|
+
.kd-thread-resize { position:absolute; left:-5px; top:0; bottom:0; width:10px; cursor:col-resize; touch-action:none; z-index:2; }
|
|
619
|
+
.kd-thread-resize:hover, .kd-thread-resize:focus-visible { background:color-mix(in srgb,var(--kd-brand-primary) 20%,transparent); }
|
|
620
|
+
.kd-thread-head { padding:20px; border-bottom:1px solid var(--kd-border-l1);
|
|
816
621
|
display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
|
817
622
|
.kd-thread-title { font-size:13px; font-weight:600; }
|
|
818
|
-
.kd-thread-hint { padding:
|
|
819
|
-
color:var(--
|
|
820
|
-
.kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:
|
|
623
|
+
.kd-thread-hint { padding:12px 20px 0; font-size:11px; line-height:1.6;
|
|
624
|
+
color:var(--kd-label-secondary); }
|
|
625
|
+
.kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:18px 20px; display:flex;
|
|
821
626
|
flex-direction:column; gap:12px; }
|
|
822
627
|
.kd-msg { max-width:100%; border-radius:12px; padding:10px 12px; font-size:13px; line-height:1.72; }
|
|
823
|
-
.kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--
|
|
824
|
-
border:1px solid color-mix(in srgb, var(--
|
|
825
|
-
.kd-msg-assistant { align-self:stretch; border:
|
|
826
|
-
|
|
827
|
-
.kd-msg-
|
|
828
|
-
.kd-
|
|
829
|
-
.kd-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
|
|
628
|
+
.kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--kd-brand-primary) 9%, transparent);
|
|
629
|
+
border:1px solid color-mix(in srgb, var(--kd-brand-primary) 26%, transparent); }
|
|
630
|
+
.kd-msg-assistant { align-self:stretch; padding:0; border:0; background:transparent; }
|
|
631
|
+
.kd-msg-meta { font-size:10px; color:var(--kd-label-secondary); margin-top:6px; }
|
|
632
|
+
.kd-msg-failed { border-color:var(--kd-state-error-primary); }
|
|
633
|
+
.kd-thread-foot { border-top:1px solid var(--kd-border-l1); padding:12px 16px; }
|
|
634
|
+
.kd-composer { border:1px solid var(--kd-border-l2); border-radius:12px;
|
|
635
|
+
background:var(--kd-bg-base); padding:6px; transition:border-color .15s, box-shadow .15s; }
|
|
636
|
+
.kd-composer:focus-within { border-color:var(--kd-brand-primary);
|
|
637
|
+
box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
|
|
834
638
|
.kd-textarea { width:100%; min-height:58px; max-height:180px; resize:vertical; padding:6px 8px;
|
|
835
|
-
border:0; background:transparent; color:var(--
|
|
639
|
+
border:0; background:transparent; color:var(--kd-label-primary); font:inherit; font-size:13px; }
|
|
836
640
|
.kd-textarea:focus { outline:none; }
|
|
837
|
-
.kd-tabs { display:flex; gap:
|
|
838
|
-
.kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:
|
|
839
|
-
border:0; background:transparent; color:var(--
|
|
840
|
-
cursor:pointer; border-radius:8px
|
|
841
|
-
.kd-tab:hover { color:var(--
|
|
842
|
-
.kd-tab.is-active { color:var(--
|
|
843
|
-
|
|
844
|
-
border-radius:2px; background:var(--dsw-alias-brand-primary); }
|
|
641
|
+
.kd-tabs { display:flex; gap:4px; width:fit-content; max-width:100%; min-width:0; overflow-x:auto; padding:4px; border:1px solid var(--kd-border-l1); border-radius:12px; background:var(--kd-surface); }
|
|
642
|
+
.kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; white-space:nowrap;
|
|
643
|
+
border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
|
|
644
|
+
cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
|
|
645
|
+
.kd-tab:hover { color:var(--kd-label-primary); background:var(--kd-bg-layer-2); }
|
|
646
|
+
.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)); }
|
|
647
|
+
|
|
845
648
|
.kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
|
|
846
|
-
text-align:center; background:var(--
|
|
847
|
-
.kd-tab.is-active .kd-tab-count { color:var(--
|
|
848
|
-
.kd-card { border:1px solid var(--
|
|
849
|
-
background:var(--
|
|
649
|
+
text-align:center; background:var(--kd-bg-layer-2); color:var(--kd-label-secondary); }
|
|
650
|
+
.kd-tab.is-active .kd-tab-count { color:var(--kd-brand-primary); }
|
|
651
|
+
.kd-card { border:1px solid var(--kd-border-l1); border-radius:14px;
|
|
652
|
+
background:var(--kd-bg-layer-1); padding:28px; box-shadow:0 4px 24px rgb(20 45 35 / 3%); }
|
|
850
653
|
.kd-card + .kd-card { margin-top:14px; }
|
|
851
654
|
.kd-meta { display:flex; gap:8px; flex-wrap:wrap; align-items:center; font-size:12px;
|
|
852
|
-
color:var(--
|
|
655
|
+
color:var(--kd-label-secondary); }
|
|
853
656
|
.kd-meta-dot::before { content:'·'; margin-right:8px; }
|
|
854
657
|
.kd-chips { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
|
|
855
658
|
.kd-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; border-radius:6px;
|
|
856
659
|
font-size:11.5px; line-height:1.7; white-space:nowrap;
|
|
857
|
-
color:var(--
|
|
858
|
-
.kd-tag-brand { color:var(--
|
|
859
|
-
background:color-mix(in srgb, var(--
|
|
660
|
+
color:var(--kd-label-secondary); background:var(--kd-bg-layer-2); }
|
|
661
|
+
.kd-tag-brand { color:var(--kd-brand-primary);
|
|
662
|
+
background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
|
|
860
663
|
.kd-badge { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:999px;
|
|
861
|
-
font-size:11px; line-height:1.6; border:1px solid var(--
|
|
862
|
-
color:var(--
|
|
863
|
-
.kd-badge-ok { color:var(--
|
|
864
|
-
border-color:color-mix(in srgb, var(--
|
|
865
|
-
background:color-mix(in srgb, var(--
|
|
866
|
-
.kd-badge-bad { color:var(--
|
|
867
|
-
border-color:color-mix(in srgb, var(--
|
|
868
|
-
background:color-mix(in srgb, var(--
|
|
869
|
-
.kd-badge-warn { color:var(--
|
|
870
|
-
border-color:color-mix(in srgb, var(--
|
|
871
|
-
background:color-mix(in srgb, var(--
|
|
872
|
-
.kd-badge-brand { color:var(--
|
|
873
|
-
border-color:color-mix(in srgb, var(--
|
|
874
|
-
background:color-mix(in srgb, var(--
|
|
875
|
-
.kd-stem { margin:
|
|
664
|
+
font-size:11px; line-height:1.6; border:1px solid var(--kd-border-l2);
|
|
665
|
+
color:var(--kd-label-secondary); background:var(--kd-bg-layer-1); white-space:nowrap; }
|
|
666
|
+
.kd-badge-ok { color:var(--kd-state-success-primary);
|
|
667
|
+
border-color:color-mix(in srgb, var(--kd-state-success-primary) 34%, transparent);
|
|
668
|
+
background:color-mix(in srgb, var(--kd-state-success-primary) 10%, transparent); }
|
|
669
|
+
.kd-badge-bad { color:var(--kd-state-error-primary);
|
|
670
|
+
border-color:color-mix(in srgb, var(--kd-state-error-primary) 34%, transparent);
|
|
671
|
+
background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
|
|
672
|
+
.kd-badge-warn { color:var(--kd-state-warn-primary);
|
|
673
|
+
border-color:color-mix(in srgb, var(--kd-state-warn-primary) 34%, transparent);
|
|
674
|
+
background:color-mix(in srgb, var(--kd-state-warn-primary) 12%, transparent); }
|
|
675
|
+
.kd-badge-brand { color:var(--kd-brand-primary);
|
|
676
|
+
border-color:color-mix(in srgb, var(--kd-brand-primary) 34%, transparent);
|
|
677
|
+
background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
|
|
678
|
+
.kd-stem { margin:24px 0; font-size:16px; line-height:1.85; }
|
|
876
679
|
.kd-options { display:flex; flex-direction:column; gap:8px; margin:0 0 4px; }
|
|
877
|
-
.kd-option { display:flex; gap:12px; align-items:flex-start; padding:
|
|
878
|
-
border:1px solid var(--
|
|
879
|
-
text-align:left; color:var(--
|
|
680
|
+
.kd-option { display:flex; gap:12px; align-items:flex-start; padding:14px 16px; border-radius:12px;
|
|
681
|
+
border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); cursor:pointer;
|
|
682
|
+
text-align:left; color:var(--kd-label-primary); font:inherit; font-size:14px;
|
|
880
683
|
transition:background-color .15s, border-color .15s; }
|
|
881
|
-
.kd-option:hover:not(:disabled) { border-color:var(--
|
|
882
|
-
background:var(--
|
|
684
|
+
.kd-option:hover:not(:disabled) { border-color:var(--kd-border-l2);
|
|
685
|
+
background:var(--kd-bg-layer-2); }
|
|
883
686
|
.kd-option:disabled { cursor:default; }
|
|
884
|
-
.kd-option.is-selected { border-color:var(--
|
|
885
|
-
background:color-mix(in srgb, var(--
|
|
886
|
-
.kd-option.is-correct { border-color:var(--
|
|
887
|
-
background:color-mix(in srgb, var(--
|
|
888
|
-
.kd-option.is-wrong { border-color:var(--
|
|
889
|
-
background:color-mix(in srgb, var(--
|
|
890
|
-
.kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:
|
|
687
|
+
.kd-option.is-selected { border-color:var(--kd-brand-primary);
|
|
688
|
+
background:color-mix(in srgb, var(--kd-brand-primary) 7%, transparent); }
|
|
689
|
+
.kd-option.is-correct { border-color:color-mix(in srgb, var(--kd-state-success-primary) 45%, var(--kd-surface));
|
|
690
|
+
background:color-mix(in srgb, var(--kd-state-success-primary) 8%, transparent); }
|
|
691
|
+
.kd-option.is-wrong { border-color:var(--kd-state-error-primary);
|
|
692
|
+
background:color-mix(in srgb, var(--kd-state-error-primary) 8%, transparent); }
|
|
693
|
+
.kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:8px; display:inline-flex;
|
|
891
694
|
align-items:center; justify-content:center; font-size:12px; font-weight:600;
|
|
892
|
-
border:1px solid var(--
|
|
893
|
-
.kd-option.is-selected .kd-option-key { color:var(--
|
|
894
|
-
.kd-option.is-correct .kd-option-key { color:var(--
|
|
895
|
-
.kd-option.is-wrong .kd-option-key { color:var(--
|
|
695
|
+
border:1px solid var(--kd-border-l2); background:var(--kd-bg-base); }
|
|
696
|
+
.kd-option.is-selected .kd-option-key { color:var(--kd-brand-primary); border-color:var(--kd-brand-primary); }
|
|
697
|
+
.kd-option.is-correct .kd-option-key { color:var(--kd-state-success-primary); border-color:var(--kd-state-success-primary); }
|
|
698
|
+
.kd-option.is-wrong .kd-option-key { color:var(--kd-state-error-primary); border-color:var(--kd-state-error-primary); }
|
|
896
699
|
.kd-option-text { min-width:0; flex:1 1 auto; }
|
|
897
700
|
.kd-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:16px; }
|
|
898
|
-
.kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:
|
|
701
|
+
.kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:36px;
|
|
899
702
|
padding:0 14px; border-radius:8px; font-family:inherit; font-size:13px; font-weight:500; cursor:pointer;
|
|
900
|
-
border:1px solid var(--
|
|
901
|
-
color:var(--
|
|
703
|
+
border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
|
|
704
|
+
color:var(--kd-label-primary);
|
|
902
705
|
transition:background-color .15s, border-color .15s, color .15s; }
|
|
903
|
-
.kd-btn:hover:not(:disabled) { background:var(--
|
|
706
|
+
.kd-btn:hover:not(:disabled) { background:var(--kd-bg-layer-2); }
|
|
904
707
|
.kd-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
905
|
-
.kd-btn-primary { background:var(--
|
|
906
|
-
.kd-btn-primary:hover:not(:disabled) { background:var(--
|
|
907
|
-
.kd-btn-ghost { border-color:transparent; background:transparent; color:var(--
|
|
908
|
-
.kd-btn-ghost:hover:not(:disabled) { background:var(--
|
|
909
|
-
.kd-btn-danger { color:var(--
|
|
910
|
-
border-color:color-mix(in srgb, var(--
|
|
911
|
-
.kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--
|
|
708
|
+
.kd-btn-primary { background:var(--kd-brand-primary); border-color:var(--kd-brand-primary); color:#fff; }
|
|
709
|
+
.kd-btn-primary:hover:not(:disabled) { background:var(--kd-brand-primary); filter:brightness(1.07); }
|
|
710
|
+
.kd-btn-ghost { border-color:transparent; background:transparent; color:var(--kd-label-secondary); }
|
|
711
|
+
.kd-btn-ghost:hover:not(:disabled) { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
|
|
712
|
+
.kd-btn-danger { color:var(--kd-state-error-primary); background:transparent;
|
|
713
|
+
border-color:color-mix(in srgb, var(--kd-state-error-primary) 40%, transparent); }
|
|
714
|
+
.kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
|
|
912
715
|
.kd-btn-sm { min-height:26px; padding:0 9px; font-size:12px; }
|
|
913
716
|
.kd-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px;
|
|
914
|
-
border-radius:6px; border:0; background:transparent; color:var(--
|
|
717
|
+
border-radius:6px; border:0; background:transparent; color:var(--kd-label-secondary); cursor:pointer;
|
|
915
718
|
transition:background-color .15s, color .15s; }
|
|
916
|
-
.kd-iconbtn:hover { background:var(--
|
|
719
|
+
.kd-iconbtn:hover { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
|
|
917
720
|
.kd-answer { margin-top:6px; }
|
|
918
|
-
.kd-answer-panel { margin-top:18px; border:1px solid var(--
|
|
919
|
-
background:var(--
|
|
721
|
+
.kd-answer-panel { margin-top:18px; border:1px solid var(--kd-border-l1); border-radius:14px;
|
|
722
|
+
background:var(--kd-bg-layer-1); overflow:hidden; }
|
|
920
723
|
.kd-answer-panel-head { display:flex; align-items:center; justify-content:space-between; gap:10px;
|
|
921
|
-
flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--
|
|
922
|
-
background:var(--
|
|
923
|
-
.kd-answer-panel-head .kd-tag { background:var(--
|
|
724
|
+
flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--kd-border-l1);
|
|
725
|
+
background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); font-size:13px; font-weight:600; }
|
|
726
|
+
.kd-answer-panel-head .kd-tag { background:var(--kd-bg-layer-1); font-weight:400; }
|
|
924
727
|
.kd-answer-panel-body { padding:18px; }
|
|
925
728
|
.kd-answer-block + .kd-answer-block { margin-top:18px; padding-top:16px;
|
|
926
|
-
border-top:1px solid var(--
|
|
927
|
-
.kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--
|
|
729
|
+
border-top:1px solid var(--kd-border-l1); }
|
|
730
|
+
.kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
|
|
928
731
|
margin-bottom:6px; }
|
|
929
732
|
.kd-verify { margin-top:14px; padding:11px 13px; border-radius:10px; font-size:12px; line-height:1.7;
|
|
930
|
-
border:1px solid var(--
|
|
931
|
-
color:var(--
|
|
733
|
+
border:1px solid var(--kd-border-l1); background:var(--kd-bg-layer-2);
|
|
734
|
+
color:var(--kd-label-secondary); }
|
|
932
735
|
.kd-verify ul { margin:6px 0 0; padding-left:18px; }
|
|
933
736
|
.kd-list-row { display:flex; gap:14px; align-items:flex-start; width:100%; text-align:left;
|
|
934
|
-
padding:14px 16px; border-radius:12px; border:1px solid var(--
|
|
935
|
-
background:var(--
|
|
737
|
+
padding:14px 16px; border-radius:12px; border:1px solid var(--kd-border-l1);
|
|
738
|
+
background:var(--kd-bg-layer-1); color:var(--kd-label-primary);
|
|
936
739
|
font:inherit; transition:background-color .15s, border-color .15s; }
|
|
740
|
+
.kd-list-row > .kd-btn { flex:none; white-space:nowrap; }
|
|
741
|
+
.kd-list-row[role=button] { cursor:pointer; }
|
|
742
|
+
.kd-list-open { flex:none; display:flex; align-items:center; gap:6px; font-size:12px; color:var(--kd-label-secondary); white-space:nowrap; }
|
|
937
743
|
.kd-list-row + .kd-list-row { margin-top:8px; }
|
|
938
|
-
.kd-list-row:hover { border-color:var(--
|
|
744
|
+
.kd-list-row:hover { border-color:var(--kd-border-l2); background:var(--kd-bg-layer-2); }
|
|
939
745
|
.kd-list-body { min-width:0; flex:1 1 auto; }
|
|
940
|
-
.kd-list-summary { font-size:
|
|
941
|
-
-
|
|
746
|
+
.kd-list-summary { --kd-preview-line-height:28px; font-size:14px; line-height:var(--kd-preview-line-height); min-width:0;
|
|
747
|
+
max-height:calc(2 * var(--kd-preview-line-height)); overflow:hidden; }
|
|
748
|
+
.kd-list-summary .kd-md { line-height:inherit; }
|
|
749
|
+
.kd-list-summary .kd-md > * { display:inline; }
|
|
750
|
+
.kd-list-summary .kd-p, .kd-list-summary .kd-h, .kd-list-summary .kd-mathblock { margin:0; }
|
|
751
|
+
.kd-list-summary .kd-p::after { content:' '; }
|
|
752
|
+
.kd-list-summary .kd-mathblock { display:inline; padding:0; text-align:left; vertical-align:baseline; }
|
|
753
|
+
.kd-list-summary mjx-container[display=true] { margin:0; }
|
|
754
|
+
.kd-list-summary .kd-code-wrap { margin:0; border:0; }
|
|
755
|
+
.kd-list-summary .kd-code-lang { display:none; }
|
|
756
|
+
.kd-list-summary .kd-code { display:inline; padding:0; line-height:inherit; white-space:pre-wrap; }
|
|
757
|
+
.kd-list-summary.is-truncated { mask-image:linear-gradient(#000,#000),linear-gradient(to right,#000 var(--kd-preview-fade-start),transparent var(--kd-preview-fade-end));
|
|
758
|
+
mask-size:100% var(--kd-preview-fade-top),100% calc(100% - var(--kd-preview-fade-top)); mask-position:top,bottom; mask-repeat:no-repeat; }
|
|
942
759
|
.kd-list-meta { margin-top:7px; display:flex; gap:6px; flex-wrap:wrap; font-size:11px;
|
|
943
|
-
color:var(--
|
|
944
|
-
.kd-empty { padding:56px 24px; text-align:center; color:var(--
|
|
760
|
+
color:var(--kd-label-secondary); align-items:center; }
|
|
761
|
+
.kd-empty { padding:56px 24px; text-align:center; color:var(--kd-label-secondary); }
|
|
945
762
|
.kd-empty-icon { width:44px; height:44px; margin:0 auto 14px; border-radius:50%; display:flex;
|
|
946
|
-
align-items:center; justify-content:center; border:1px solid var(--
|
|
947
|
-
background:var(--
|
|
948
|
-
.kd-empty-title { font-size:15px; font-weight:600; color:var(--
|
|
763
|
+
align-items:center; justify-content:center; border:1px solid var(--kd-border-l1);
|
|
764
|
+
background:var(--kd-bg-layer-1); color:var(--kd-label-secondary); }
|
|
765
|
+
.kd-empty-title { font-size:15px; font-weight:600; color:var(--kd-label-primary); margin-bottom:6px; }
|
|
949
766
|
.kd-empty-body { font-size:13px; max-width:460px; margin:0 auto; line-height:1.8; }
|
|
950
767
|
.kd-empty .kd-actions { justify-content:center; }
|
|
951
768
|
.kd-overlay { position:absolute; inset:0; background:rgba(0,0,0,.38); display:flex; align-items:center;
|
|
952
769
|
justify-content:center; padding:24px; z-index:40; backdrop-filter:blur(2px); }
|
|
953
770
|
.kd-modal { width:min(580px,100%); max-height:100%; display:flex; flex-direction:column;
|
|
954
|
-
background:var(--
|
|
955
|
-
border:1px solid var(--
|
|
771
|
+
background:var(--kd-bg-overlay); color:var(--kd-label-primary);
|
|
772
|
+
border:1px solid var(--kd-border-l2); border-radius:16px; overflow:hidden;
|
|
956
773
|
box-shadow:0 16px 48px rgba(0,0,0,.24); }
|
|
957
774
|
.kd-modal-wide { width:min(880px,100%); }
|
|
958
|
-
.kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--
|
|
775
|
+
.kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--kd-border-l1);
|
|
959
776
|
display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
|
960
777
|
.kd-modal-title { font-size:15px; font-weight:650; letter-spacing:.01em; }
|
|
961
778
|
.kd-modal-body { padding:18px 20px; overflow:auto; min-height:0; }
|
|
962
|
-
.kd-modal-foot { padding:14px 20px; border-top:1px solid var(--
|
|
779
|
+
.kd-modal-foot { padding:14px 20px; border-top:1px solid var(--kd-border-l1);
|
|
963
780
|
display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; }
|
|
964
781
|
.kd-field { display:flex; flex-direction:column; gap:6px; margin-bottom:14px; }
|
|
965
|
-
.kd-field-label { font-size:12px; font-weight:500; color:var(--
|
|
966
|
-
.kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--
|
|
967
|
-
background:var(--
|
|
782
|
+
.kd-field-label { font-size:12px; font-weight:500; color:var(--kd-label-secondary); }
|
|
783
|
+
.kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--kd-border-l2);
|
|
784
|
+
background:var(--kd-bg-base); color:var(--kd-label-primary); font:inherit; font-size:13px;
|
|
968
785
|
transition:border-color .15s, box-shadow .15s; }
|
|
969
|
-
.kd-input:focus { outline:none; border-color:var(--
|
|
970
|
-
box-shadow:0 0 0 3px color-mix(in srgb, var(--
|
|
971
|
-
.kd-input::placeholder { color:var(--
|
|
786
|
+
.kd-input:focus { outline:none; border-color:var(--kd-brand-primary);
|
|
787
|
+
box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
|
|
788
|
+
.kd-input::placeholder { color:var(--kd-label-secondary); }
|
|
972
789
|
.kd-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
973
790
|
.kd-row-between { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
|
|
974
|
-
.kd-divider { height:1px; background:var(--
|
|
975
|
-
.kd-preview-item { border:1px solid var(--
|
|
976
|
-
background:var(--
|
|
791
|
+
.kd-divider { height:1px; background:var(--kd-border-l1); margin:16px 0; }
|
|
792
|
+
.kd-preview-item { border:1px solid var(--kd-border-l1); border-radius:12px; padding:12px 14px;
|
|
793
|
+
background:var(--kd-bg-base); }
|
|
977
794
|
.kd-preview-item + .kd-preview-item { margin-top:8px; }
|
|
978
|
-
.kd-warn-text { color:var(--
|
|
979
|
-
.kd-error-text { color:var(--
|
|
795
|
+
.kd-warn-text { color:var(--kd-state-warn-primary); font-size:12px; line-height:1.7; }
|
|
796
|
+
.kd-error-text { color:var(--kd-state-error-primary); font-size:12px; line-height:1.7; }
|
|
980
797
|
.kd-toast { position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:60;
|
|
981
798
|
padding:9px 18px; border-radius:999px; font-size:13px; max-width:80%;
|
|
982
|
-
background:var(--
|
|
799
|
+
background:var(--kd-bg-overlay); border:1px solid var(--kd-border-l2);
|
|
983
800
|
box-shadow:0 8px 24px rgba(0,0,0,.18); }
|
|
984
|
-
.kd-toast-bad { color:var(--
|
|
985
|
-
.kd-toast-ok { color:var(--
|
|
986
|
-
.kd-toast-warn { color:var(--
|
|
987
|
-
.kd-loading { padding:48px 0; text-align:center; color:var(--
|
|
801
|
+
.kd-toast-bad { color:var(--kd-state-error-primary); }
|
|
802
|
+
.kd-toast-ok { color:var(--kd-state-success-primary); }
|
|
803
|
+
.kd-toast-warn { color:var(--kd-state-warn-primary); }
|
|
804
|
+
.kd-loading { padding:48px 0; text-align:center; color:var(--kd-label-secondary); font-size:13px; }
|
|
988
805
|
.kd-spinner { display:inline-block; width:14px; height:14px; border-radius:50%; vertical-align:-2px;
|
|
989
|
-
border:2px solid var(--
|
|
806
|
+
border:2px solid var(--kd-border-l2); border-top-color:var(--kd-brand-primary);
|
|
990
807
|
animation:kd-spin .8s linear infinite; margin-right:8px; }
|
|
991
808
|
@keyframes kd-spin { to { transform:rotate(360deg); } }
|
|
992
|
-
.kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--
|
|
809
|
+
.kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--kd-border-l1);
|
|
993
810
|
overflow:auto; }
|
|
811
|
+
.kd-mobilebar > * { flex:0 0 auto; white-space:nowrap; }
|
|
812
|
+
.kd-mobilebar .kd-select { max-width:180px; }
|
|
994
813
|
.kd-md { line-height:1.78; overflow-wrap:anywhere; }
|
|
995
814
|
.kd-md > *:first-child { margin-top:0; }
|
|
996
815
|
.kd-md > *:last-child { margin-bottom:0; }
|
|
@@ -1000,114 +819,105 @@ window.__ModuleLoader__.load({
|
|
|
1000
819
|
.kd-h4, .kd-h5, .kd-h6 { font-size:14px; }
|
|
1001
820
|
.kd-list { margin:8px 0; padding-left:22px; }
|
|
1002
821
|
.kd-list li { margin:3px 0; }
|
|
1003
|
-
.kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--
|
|
1004
|
-
color:var(--
|
|
1005
|
-
.kd-hr { border:0; border-top:1px solid var(--
|
|
1006
|
-
.kd-link { color:var(--
|
|
822
|
+
.kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--kd-border-l2);
|
|
823
|
+
color:var(--kd-label-secondary); }
|
|
824
|
+
.kd-hr { border:0; border-top:1px solid var(--kd-border-l1); margin:14px 0; }
|
|
825
|
+
.kd-link { color:var(--kd-brand-primary); text-decoration:underline; text-underline-offset:2px; }
|
|
1007
826
|
.kd-inline-code { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
|
|
1008
|
-
padding:1px 5px; border-radius:5px; background:var(--
|
|
1009
|
-
border:1px solid var(--
|
|
1010
|
-
.kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--
|
|
1011
|
-
background:var(--
|
|
1012
|
-
.kd-code-lang { padding:3px 10px; font-size:11px; color:var(--
|
|
1013
|
-
border-bottom:1px solid var(--
|
|
827
|
+
padding:1px 5px; border-radius:5px; background:var(--kd-bg-layer-2);
|
|
828
|
+
border:1px solid var(--kd-border-l1); }
|
|
829
|
+
.kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--kd-border-l1);
|
|
830
|
+
background:var(--kd-bg-layer-2); }
|
|
831
|
+
.kd-code-lang { padding:3px 10px; font-size:11px; color:var(--kd-label-secondary);
|
|
832
|
+
border-bottom:1px solid var(--kd-border-l1); }
|
|
1014
833
|
.kd-code { margin:0; padding:10px 12px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
|
|
1015
834
|
font-size:12.5px; line-height:1.6; white-space:pre; tab-size:2; }
|
|
1016
835
|
.kd-code code { font:inherit; }
|
|
1017
836
|
.kd-table-wrap { overflow:auto; margin:10px 0; }
|
|
1018
837
|
.kd-table { border-collapse:collapse; font-size:13px; min-width:0; }
|
|
1019
|
-
.kd-table th, .kd-table td { border:1px solid var(--
|
|
1020
|
-
.kd-table th { background:var(--
|
|
838
|
+
.kd-table th, .kd-table td { border:1px solid var(--kd-border-l1); padding:5px 10px; text-align:left; }
|
|
839
|
+
.kd-table th { background:var(--kd-bg-layer-2); font-weight:600; }
|
|
1021
840
|
.kd-image { max-width:100%; height:auto; border-radius:8px; display:block; margin:8px 0; }
|
|
1022
841
|
.kd-image-fallback { display:inline-block; font-family:ui-monospace,monospace; font-size:12px;
|
|
1023
|
-
color:var(--
|
|
842
|
+
color:var(--kd-state-warn-primary); border:1px dashed var(--kd-state-warn-primary);
|
|
1024
843
|
border-radius:6px; padding:2px 6px; }
|
|
1025
|
-
.kd-math {
|
|
1026
|
-
.kd-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
.kd-frac-d { border-top:1px solid currentColor; padding:.03em .25em 0; }
|
|
1039
|
-
/* boxed:把最终答案框起来 */
|
|
1040
|
-
.kd-boxed { display:inline-flex; align-items:center; padding:.06em .38em; margin:0 .12em;
|
|
1041
|
-
border:1px solid currentColor; border-radius:3px; }
|
|
1042
|
-
/* overset / underset:在基座上方或下方叠一行小字 */
|
|
1043
|
-
.kd-stack { display:inline-flex; flex-direction:column; align-items:center; vertical-align:middle;
|
|
1044
|
-
line-height:1.1; }
|
|
1045
|
-
.kd-stack-mark { font-size:.72em; }
|
|
1046
|
-
.kd-stack-base { line-height:1.2; }
|
|
1047
|
-
/* underbrace / overbrace 的花括号暂不绘制,只保留内容分组 */
|
|
1048
|
-
.kd-group { display:inline; }
|
|
1049
|
-
/* binom:没有分数线的上下堆叠 */
|
|
1050
|
-
.kd-binom { display:inline-flex; align-items:center; vertical-align:middle; margin:0 .1em; }
|
|
1051
|
-
.kd-binom .kd-frac-d { border-top:0; padding-top:0; }
|
|
1052
|
-
.kd-sup, .kd-sub { font-size:.72em; }
|
|
1053
|
-
.kd-sup { vertical-align:super; }
|
|
1054
|
-
.kd-sub { vertical-align:sub; }
|
|
1055
|
-
.kd-sqrt { display:inline-flex; align-items:flex-start; }
|
|
1056
|
-
.kd-sqrt-sign { font-size:1.05em; }
|
|
1057
|
-
.kd-sqrt-index { font-size:.6em; margin-left:-.35em; }
|
|
1058
|
-
.kd-sqrt-body { border-top:1px solid currentColor; padding:0 .12em; }
|
|
1059
|
-
.kd-op { display:inline-flex; flex-direction:column; align-items:center; vertical-align:middle;
|
|
1060
|
-
margin:0 .2em; line-height:1.15; }
|
|
1061
|
-
.kd-op-sym { font-size:1.35em; line-height:1; }
|
|
1062
|
-
.kd-op-lim { font-size:.62em; }
|
|
1063
|
-
.kd-roman { font-style:normal; }
|
|
1064
|
-
.kd-bold { font-weight:700; font-style:normal; }
|
|
1065
|
-
.kd-blackboard { font-style:normal; }
|
|
1066
|
-
.kd-colsep { display:inline-block; width:.5em; }
|
|
1067
|
-
.kd-matrixbox { display:inline-flex; align-items:center; vertical-align:middle; }
|
|
1068
|
-
.kd-matrix { border-collapse:collapse; }
|
|
1069
|
-
.kd-matrix td { padding:2px 7px; text-align:center; border:0; }
|
|
1070
|
-
.kd-delim { font-size:1.5em; line-height:1; }
|
|
1071
|
-
.kd-cases .kd-matrix td { text-align:left; }
|
|
1072
|
-
.kd-accent-overline, .kd-accent-bar { border-top:1px solid currentColor; }
|
|
1073
|
-
.kd-accent-underline { border-bottom:1px solid currentColor; }
|
|
1074
|
-
.kd-accent-vec { position:relative; }
|
|
1075
|
-
.kd-accent-vec::before { content:'→'; position:absolute; top:-.75em; left:50%; transform:translateX(-50%); font-size:.7em; }
|
|
1076
|
-
.kd-accent-overrightarrow { position:relative; }
|
|
1077
|
-
.kd-accent-overrightarrow::before { content:'→'; position:absolute; top:-.85em; left:0; right:0;
|
|
1078
|
-
text-align:center; font-size:.72em; line-height:1; }
|
|
1079
|
-
.kd-accent-overleftarrow { position:relative; }
|
|
1080
|
-
.kd-accent-overleftarrow::before { content:'←'; position:absolute; top:-.85em; left:0; right:0;
|
|
1081
|
-
text-align:center; font-size:.72em; line-height:1; }
|
|
1082
|
-
.kd-accent-hat::before, .kd-accent-widehat::before { content:'^'; position:absolute; top:-.5em; left:50%; transform:translateX(-50%); font-size:.8em; }
|
|
1083
|
-
.kd-accent-hat, .kd-accent-widehat { position:relative; }
|
|
1084
|
-
.kd-accent-tilde { position:relative; }
|
|
1085
|
-
.kd-accent-tilde::before { content:'~'; position:absolute; top:-.55em; left:50%; transform:translateX(-50%); }
|
|
1086
|
-
.kd-accent-dot { position:relative; }
|
|
1087
|
-
.kd-accent-dot::before { content:'·'; position:absolute; top:-.6em; left:50%; transform:translateX(-50%); }
|
|
1088
|
-
.kd-accent-ddot { position:relative; }
|
|
1089
|
-
.kd-accent-ddot::before { content:'··'; position:absolute; top:-.6em; left:50%; transform:translateX(-50%); }
|
|
1090
|
-
.kd-latex-unknown { color:var(--dsw-alias-state-warn-primary); font-size:.92em; }
|
|
844
|
+
.kd-math, .kd-mathblock { font-size:17px; }
|
|
845
|
+
.kd-math { display:inline-flex; vertical-align:baseline; align-items:baseline; max-width:100%; padding:.25em .1em;
|
|
846
|
+
overflow-x:auto; overflow-y:hidden; }
|
|
847
|
+
.kd-math mjx-container { color:currentColor; flex:none; }
|
|
848
|
+
.kd-mathblock { display:block; max-width:100%; min-width:0; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
|
|
849
|
+
text-align:center; }
|
|
850
|
+
.kd-page .kd-math mjx-container, .kd-page .kd-mathblock mjx-container {
|
|
851
|
+
display:inline-block; font-size:100% !important; max-width:none; }
|
|
852
|
+
.kd-page .kd-math svg, .kd-page .kd-mathblock svg { max-width:none !important; }
|
|
853
|
+
.kd-math-pending { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
|
|
854
|
+
opacity:.55; white-space:pre-wrap; }
|
|
855
|
+
.kd-math-error { color:var(--kd-state-error-primary);
|
|
856
|
+
font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em; }
|
|
1091
857
|
.kd-checkline { display:flex; gap:8px; align-items:flex-start; padding:5px 0; font-size:13px; }
|
|
1092
858
|
.kd-chip { display:inline-flex; align-items:center; gap:2px; padding:0 2px 0 8px; border-radius:999px;
|
|
1093
|
-
border:1px solid var(--
|
|
859
|
+
border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1); font-size:12px; }
|
|
1094
860
|
.kd-chip .kd-checkline { padding:0; }
|
|
1095
861
|
.kd-chip .kd-iconbtn { width:20px; height:20px; }
|
|
862
|
+
.kd-option.is-correct { border-color:#22c55e; }
|
|
863
|
+
.kd-option.is-wrong { border-color:#f35464; }
|
|
864
|
+
.kd-page input[type=checkbox] { accent-color:var(--kd-brand-primary); width:16px; height:16px; flex:none; }
|
|
865
|
+
.kd-btn, .kd-iconbtn, .kd-tab, .kd-nav-item, .kd-option, .kd-list-row {
|
|
866
|
+
transition:background-color .16s, border-color .16s, color .16s, box-shadow .16s, transform .16s; }
|
|
867
|
+
.kd-btn:active:not(:disabled), .kd-iconbtn:active, .kd-tab:active, .kd-nav-item:active { transform:scale(.97); }
|
|
868
|
+
.kd-option:active:not(:disabled) { transform:translateY(1px); }
|
|
869
|
+
.kd-view { animation:kd-enter .2s ease-out; }
|
|
870
|
+
.kd-presence { display:contents; }
|
|
871
|
+
.kd-overlay { animation:kd-fade .16s ease-out; background:rgb(18 27 52 / 28%); backdrop-filter:blur(5px); }
|
|
872
|
+
.kd-modal { animation:kd-enter .18s ease-out; box-shadow:0 24px 80px rgb(12 25 60 / 18%); border-radius:20px; }
|
|
873
|
+
.kd-presence.is-leaving .kd-overlay { animation:kd-fade .16s ease-in reverse both; pointer-events:none; }
|
|
874
|
+
.kd-presence.is-leaving .kd-modal { animation:kd-enter .16s ease-in reverse both; }
|
|
875
|
+
.kd-thread { animation:kd-drawer .2s ease-out; }
|
|
876
|
+
.kd-presence.is-leaving .kd-thread { animation:kd-drawer .16s ease-in reverse both; pointer-events:none; }
|
|
877
|
+
.kd-modal-head { padding:22px 24px; }
|
|
878
|
+
.kd-modal-title { font-size:18px; }
|
|
879
|
+
.kd-modal-body { padding:20px 24px; background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); }
|
|
880
|
+
.kd-modal-foot { padding:16px 24px; }
|
|
881
|
+
.kd-manage-card { padding:16px; border:1px solid var(--kd-border-l1); border-radius:14px; background:var(--kd-bg-base); margin-bottom:10px; }
|
|
882
|
+
.kd-manage-card.is-enabled { border-color:color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface)); }
|
|
883
|
+
.kd-manage-card .kd-checkline { font-weight:550; }
|
|
884
|
+
.kd-manage-card .kd-msg-meta { font-weight:400; line-height:1.7; }
|
|
885
|
+
.kd-custom-subject { padding:20px; border-radius:14px; border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); }
|
|
886
|
+
.kd-field-label { font-size:13px; }
|
|
887
|
+
.kd-input { height:38px; }
|
|
888
|
+
.kd-bulkbar { position:sticky; top:0; z-index:5; display:flex; flex-direction:column; gap:10px; padding:14px;
|
|
889
|
+
margin-bottom:14px; border-radius:14px; border:1px solid color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface));
|
|
890
|
+
background:var(--kd-bg-base); box-shadow:0 5px 20px rgb(20 35 70 / 5%); animation:kd-enter .18s ease-out; }
|
|
891
|
+
.kd-list-row.is-selected { border-color:var(--kd-brand-primary); background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); }
|
|
892
|
+
.kd-list-row input { margin-top:5px; }
|
|
893
|
+
@keyframes kd-fade { from { opacity:0; } to { opacity:1; } }
|
|
894
|
+
@keyframes kd-enter { from { opacity:0; transform:translateY(8px) scale(.99); } to { opacity:1; transform:none; } }
|
|
895
|
+
@keyframes kd-drawer { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }
|
|
1096
896
|
@media (max-width: 1180px) {
|
|
1097
|
-
.kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); z-index:30;
|
|
897
|
+
.kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); flex-basis:auto; z-index:30;
|
|
1098
898
|
box-shadow:-12px 0 32px rgba(0,0,0,.22); }
|
|
899
|
+
.kd-thread-resize { display:none; }
|
|
1099
900
|
}
|
|
1100
901
|
@media (max-width: 860px) {
|
|
1101
902
|
.kd-nav { display:none; }
|
|
1102
903
|
.kd-mobilebar { display:flex; }
|
|
1103
|
-
.kd-main { padding:16px 14px
|
|
904
|
+
.kd-main-toolbar { padding:16px 14px; }
|
|
905
|
+
.kd-main-scroll { padding:0 14px 40px; }
|
|
1104
906
|
.kd-header { padding:14px 14px 12px; gap:10px; }
|
|
907
|
+
.kd-header-left { flex-basis:100%; }
|
|
1105
908
|
.kd-cd-lines { align-items:flex-start; text-align:left; }
|
|
909
|
+
.kd-cd-primary { font-size:19px; }
|
|
910
|
+
.kd-tabs { flex:1; }
|
|
911
|
+
.kd-tab { flex:1 0 auto; justify-content:center; padding:7px 10px; }
|
|
1106
912
|
.kd-strip-item { white-space:normal; }
|
|
1107
913
|
.kd-card { padding:16px 14px; }
|
|
1108
914
|
.kd-answer-panel-body { padding:14px; }
|
|
1109
915
|
.kd-exam-strip { padding:8px 10px; }
|
|
1110
916
|
}
|
|
917
|
+
@media (prefers-reduced-motion: reduce) {
|
|
918
|
+
.kd-page *, .kd-page *::before, .kd-page *::after { transition:none !important; animation:none !important; }
|
|
919
|
+
}
|
|
920
|
+
|
|
1111
921
|
`
|
|
1112
922
|
|
|
1113
923
|
/* ============================== 接口 ============================== */
|
|
@@ -1168,6 +978,7 @@ window.__ModuleLoader__.load({
|
|
|
1168
978
|
check: 'M4 12.5l5 5L20 6.5',
|
|
1169
979
|
x: 'M6 6l12 12M18 6L6 18',
|
|
1170
980
|
chevron: 'M9 6l6 6-6 6',
|
|
981
|
+
'chevron-left': 'M15 6l-6 6 6 6',
|
|
1171
982
|
send: 'M4 12l16-8-6 8 6 8z',
|
|
1172
983
|
book: 'M4 5.5A2.5 2.5 0 0 1 6.5 3H20v16H6.5A2.5 2.5 0 0 0 4 21.5z',
|
|
1173
984
|
plus: 'M12 5v14M5 12h14',
|
|
@@ -1186,13 +997,14 @@ window.__ModuleLoader__.load({
|
|
|
1186
997
|
}, h('path', { d: path }))
|
|
1187
998
|
}
|
|
1188
999
|
|
|
1189
|
-
function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button' }) {
|
|
1000
|
+
function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button', ariaLabel }) {
|
|
1190
1001
|
return h('button', {
|
|
1191
1002
|
type,
|
|
1192
1003
|
className: `kd-btn kd-btn-${variant}${small ? ' kd-btn-sm' : ''}`,
|
|
1193
1004
|
onClick,
|
|
1194
1005
|
disabled,
|
|
1195
1006
|
title,
|
|
1007
|
+
'aria-label': ariaLabel,
|
|
1196
1008
|
}, children)
|
|
1197
1009
|
}
|
|
1198
1010
|
|
|
@@ -1209,6 +1021,17 @@ window.__ModuleLoader__.load({
|
|
|
1209
1021
|
actions ? h('div', { className: 'kd-actions' }, actions) : null)
|
|
1210
1022
|
}
|
|
1211
1023
|
|
|
1024
|
+
function Presence({ children }) {
|
|
1025
|
+
const active = Array.isArray(children) ? children.some(Boolean) : Boolean(children)
|
|
1026
|
+
const [retained, setRetained] = useState(children)
|
|
1027
|
+
useEffect(() => {
|
|
1028
|
+
if (active) { setRetained(children); return undefined }
|
|
1029
|
+
const timer = setTimeout(() => setRetained([]), 160)
|
|
1030
|
+
return () => clearTimeout(timer)
|
|
1031
|
+
}, [children, active])
|
|
1032
|
+
return h('div', { className: 'kd-presence' + (active ? '' : ' is-leaving') }, active ? children : retained)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1212
1035
|
function Modal({ title, children, footer, onClose, wide }) {
|
|
1213
1036
|
useEffect(() => {
|
|
1214
1037
|
const onKey = (event) => {
|
|
@@ -1248,12 +1071,12 @@ window.__ModuleLoader__.load({
|
|
|
1248
1071
|
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
1249
1072
|
}
|
|
1250
1073
|
|
|
1251
|
-
/**
|
|
1074
|
+
/** 剩余不超过 30 天时省略中间的时长行。 */
|
|
1252
1075
|
function CountdownLines({ countdown }) {
|
|
1253
1076
|
const lines = (countdown || {}).lines || {}
|
|
1254
1077
|
return h('div', { className: 'kd-cd-lines' },
|
|
1255
1078
|
h('div', { className: 'kd-cd-primary' }, lines.primary || ''),
|
|
1256
|
-
h('div', { className: 'kd-cd-secondary' }, lines.secondary
|
|
1079
|
+
lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
|
|
1257
1080
|
h('div', { className: 'kd-cd-target' }, lines.target || ''))
|
|
1258
1081
|
}
|
|
1259
1082
|
|
|
@@ -1349,7 +1172,7 @@ window.__ModuleLoader__.load({
|
|
|
1349
1172
|
}, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
|
|
1350
1173
|
}
|
|
1351
1174
|
|
|
1352
|
-
function MobileBar({ boot, subjectId,
|
|
1175
|
+
function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
|
|
1353
1176
|
const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
|
|
1354
1177
|
return h('div', { className: 'kd-mobilebar' },
|
|
1355
1178
|
h('select', {
|
|
@@ -1358,13 +1181,6 @@ window.__ModuleLoader__.load({
|
|
|
1358
1181
|
'aria-label': '切换科目',
|
|
1359
1182
|
onChange: (event) => onPick(event.target.value),
|
|
1360
1183
|
}, enabled.map((subject) => h('option', { key: subject.id, value: subject.id }, subject.name))),
|
|
1361
|
-
['today', 'all', 'favorite', 'wrong'].map((value) => h('button', {
|
|
1362
|
-
key: value,
|
|
1363
|
-
type: 'button',
|
|
1364
|
-
className: `kd-nav-item${kind === value ? ' is-active' : ''}`,
|
|
1365
|
-
style: { width: 'auto' },
|
|
1366
|
-
onClick: () => onTab(value),
|
|
1367
|
-
}, T.tabs[value])),
|
|
1368
1184
|
// 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
|
|
1369
1185
|
h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
|
|
1370
1186
|
h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
|
|
@@ -1456,7 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
1456
1272
|
|
|
1457
1273
|
function QuestionPane({
|
|
1458
1274
|
q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
|
|
1459
|
-
onRevise, revising, onToggleThread, threadOpen,
|
|
1275
|
+
onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
|
|
1460
1276
|
}) {
|
|
1461
1277
|
if (!q) return null
|
|
1462
1278
|
const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
|
|
@@ -1527,9 +1343,7 @@ window.__ModuleLoader__.load({
|
|
|
1527
1343
|
revealed
|
|
1528
1344
|
? h(Btn, { variant: 'ghost', onClick: onRedo, disabled: busy === 'reveal' }, h(Icon, { name: 'refresh', size: 13 }), T.redo)
|
|
1529
1345
|
: null,
|
|
1530
|
-
|
|
1531
|
-
? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup)
|
|
1532
|
-
: null),
|
|
1346
|
+
revealed ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup) : null),
|
|
1533
1347
|
revealed ? h(AnswerBlock, { question: q, onRevise, revising, onRemoveWrong }) : null,
|
|
1534
1348
|
revealed && last?.result === 'pending'
|
|
1535
1349
|
? h('div', { className: 'kd-actions', style: { marginTop: '12px' } },
|
|
@@ -1542,7 +1356,7 @@ window.__ModuleLoader__.load({
|
|
|
1542
1356
|
h(Badge, { tone: q.publishVerification.status === 'passed' ? 'ok' : 'warn' },
|
|
1543
1357
|
q.publishVerification.status === 'passed' ? '试题发布前已通过独立校验' : '试题发布状态:待核验'),
|
|
1544
1358
|
// 校验结论里通常写着答案与关键推导,提交答案前不能展示,否则等于泄题。
|
|
1545
|
-
|
|
1359
|
+
revealed
|
|
1546
1360
|
? ` ${q.publishVerification.summary}`
|
|
1547
1361
|
: ' 结论在提交答案后显示',
|
|
1548
1362
|
`(${methodLabel(q.publishVerification.method)}${q.publishVerification.at ? `,${formatDateTime(q.publishVerification.at)}` : ''})`)
|
|
@@ -1550,7 +1364,59 @@ window.__ModuleLoader__.load({
|
|
|
1550
1364
|
}
|
|
1551
1365
|
|
|
1552
1366
|
/* ============================== 列表 ============================== */
|
|
1553
|
-
function
|
|
1367
|
+
function previewFadePosition(element, lineHeight) {
|
|
1368
|
+
const bounds = element.getBoundingClientRect()
|
|
1369
|
+
const rects = []
|
|
1370
|
+
const document = element.ownerDocument
|
|
1371
|
+
const walker = document.createTreeWalker(element, 4) // 只测量正文文字,公式由 SVG 的外框参与定位。
|
|
1372
|
+
const range = document.createRange()
|
|
1373
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
1374
|
+
if (!node.textContent.trim() || node.parentElement.closest('mjx-container,.kd-code-lang')) continue
|
|
1375
|
+
range.setStart(node, 0)
|
|
1376
|
+
range.setEnd(node, node.textContent.trimEnd().length)
|
|
1377
|
+
rects.push(...range.getClientRects())
|
|
1378
|
+
}
|
|
1379
|
+
for (const node of element.querySelectorAll('svg,img')) rects.push(node.getBoundingClientRect())
|
|
1380
|
+
const visible = rects.filter((rect) => rect.width > 0 && rect.height > 0
|
|
1381
|
+
&& rect.top < bounds.bottom - 1 && rect.bottom > bounds.top
|
|
1382
|
+
&& rect.left < bounds.right && rect.right > bounds.left)
|
|
1383
|
+
if (!visible.length) return null
|
|
1384
|
+
const lineOf = (rect) => Math.max(0, Math.min(1, Math.floor(((rect.top + rect.bottom) / 2 - bounds.top) / lineHeight)))
|
|
1385
|
+
const lastLine = Math.max(...visible.map(lineOf))
|
|
1386
|
+
const end = Math.min(bounds.width, Math.max(...visible.filter((rect) => lineOf(rect) === lastLine).map((rect) => rect.right - bounds.left)))
|
|
1387
|
+
return { top: lastLine * lineHeight, start: Math.max(0, end - 72), end }
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function QuestionPreview({ text }) {
|
|
1391
|
+
const ref = useRef(null)
|
|
1392
|
+
const [truncated, setTruncated] = useState(false)
|
|
1393
|
+
const mathStatus = useMathJaxStatus()
|
|
1394
|
+
useEffect(() => {
|
|
1395
|
+
const element = ref.current
|
|
1396
|
+
if (!element) return undefined
|
|
1397
|
+
const measure = () => {
|
|
1398
|
+
let lineHeight = 28
|
|
1399
|
+
for (const svg of element.querySelectorAll('svg')) lineHeight = Math.max(lineHeight, Math.ceil(svg.getBoundingClientRect().height + 16))
|
|
1400
|
+
element.style.setProperty('--kd-preview-line-height', `${lineHeight}px`)
|
|
1401
|
+
const content = element.firstElementChild
|
|
1402
|
+
const clipped = element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1
|
|
1403
|
+
|| (content && content.scrollHeight > content.clientHeight + 1)
|
|
1404
|
+
const fade = clipped && previewFadePosition(element, lineHeight)
|
|
1405
|
+
if (fade) {
|
|
1406
|
+
for (const [key, value] of Object.entries(fade)) element.style.setProperty(`--kd-preview-fade-${key}`, `${value}px`)
|
|
1407
|
+
}
|
|
1408
|
+
setTruncated(Boolean(fade))
|
|
1409
|
+
}
|
|
1410
|
+
measure()
|
|
1411
|
+
const observer = new ResizeObserver(measure)
|
|
1412
|
+
observer.observe(element)
|
|
1413
|
+
if (element.firstElementChild) observer.observe(element.firstElementChild)
|
|
1414
|
+
return () => observer.disconnect()
|
|
1415
|
+
}, [text, mathStatus])
|
|
1416
|
+
return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, busy }) {
|
|
1554
1420
|
if (!list) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '加载中…')
|
|
1555
1421
|
if (!list.items.length) {
|
|
1556
1422
|
const icon = kind === 'favorite' ? 'star' : kind === 'wrong' ? 'check' : kind === 'all' ? 'book' : 'spark'
|
|
@@ -1562,33 +1428,49 @@ window.__ModuleLoader__.load({
|
|
|
1562
1428
|
}
|
|
1563
1429
|
return h('div', null,
|
|
1564
1430
|
h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
|
|
1565
|
-
h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`)
|
|
1566
|
-
|
|
1431
|
+
h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`),
|
|
1432
|
+
h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
|
|
1433
|
+
managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
|
|
1434
|
+
h('div', { className: 'kd-row' },
|
|
1435
|
+
h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
|
|
1436
|
+
h(Btn, { small: true, disabled: busy, onClick: () => onSelectAll(true) }, '全选'),
|
|
1437
|
+
h(Btn, { small: true, disabled: busy || !selected.length, onClick: () => onSelectAll(false) }, '全不选')),
|
|
1438
|
+
h('div', { className: 'kd-row' },
|
|
1439
|
+
...[['favorite', '收藏'], ['unfavorite', '取消收藏'], ['wrong', '加入错题本'], ['unwrong', '移出错题本'], ['delete', '删除题目']].map(([action, label]) =>
|
|
1440
|
+
h(Btn, { key: action, small: true, variant: action === 'delete' ? 'danger' : 'default', disabled: busy || !selected.length, onClick: () => onBulk(action) }, label)))) : null,
|
|
1441
|
+
list.items.map((item) => h('div', {
|
|
1567
1442
|
key: item.id,
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1443
|
+
className: 'kd-list-row' + (managing && selected.includes(item.id) ? ' is-selected' : ''),
|
|
1444
|
+
role: managing ? undefined : 'button',
|
|
1445
|
+
tabIndex: managing ? undefined : 0,
|
|
1446
|
+
onClick: managing ? undefined : (event) => { if (!event.target.closest('a,input')) onOpen(item.id) },
|
|
1447
|
+
onKeyDown: managing ? undefined : (event) => {
|
|
1448
|
+
if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); onOpen(item.id) }
|
|
1449
|
+
},
|
|
1571
1450
|
},
|
|
1451
|
+
managing ? h('input', { type: 'checkbox', checked: selected.includes(item.id), disabled: busy,
|
|
1452
|
+
'aria-label': '选择题目:' + (item.summary || item.id), onChange: () => onToggle(item.id) }) : null,
|
|
1572
1453
|
h('div', { className: 'kd-list-body' },
|
|
1573
|
-
h(
|
|
1454
|
+
h(QuestionPreview, { text: item.stem || item.summary || '(无题干)' }),
|
|
1574
1455
|
h('div', { className: 'kd-list-meta' },
|
|
1575
1456
|
h(Badge, null, item.typeLabel),
|
|
1576
1457
|
h('span', null, formatDate(item.assignedDate)),
|
|
1577
1458
|
h('span', null, `作答 ${item.attemptCount} 次`),
|
|
1578
1459
|
item.favorite ? h(Badge, { tone: 'brand' }, '收藏') : null,
|
|
1579
|
-
item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '
|
|
1460
|
+
item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '错题 · 已掌握' : '错题') : null,
|
|
1580
1461
|
item.answerStatus !== 'verified' ? h(Badge, { tone: 'warn' }, T.unverified) : null,
|
|
1581
1462
|
h('span', null, `难度 ${item.difficulty}/5`))),
|
|
1582
|
-
h('span', { className: 'kd-
|
|
1463
|
+
!managing ? h('span', { className: 'kd-list-open', 'aria-hidden': true }, '查看题目', h(Icon, { name: 'chevron' })) : null)))
|
|
1583
1464
|
}
|
|
1584
1465
|
|
|
1585
1466
|
/* ============================== 追问侧栏 ============================== */
|
|
1586
|
-
function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow }) {
|
|
1467
|
+
function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow, ratio = 33, onRatioChange }) {
|
|
1587
1468
|
const [draft, setDraft] = useState('')
|
|
1588
1469
|
const [sending, setSending] = useState(false)
|
|
1589
1470
|
const [localError, setLocalError] = useState('')
|
|
1590
1471
|
const [pending, setPending] = useState(null)
|
|
1591
1472
|
const scrollRef = useRef(null)
|
|
1473
|
+
const sendingRef = useRef(false)
|
|
1592
1474
|
const messages = thread?.messages || []
|
|
1593
1475
|
|
|
1594
1476
|
useEffect(() => {
|
|
@@ -1598,7 +1480,8 @@ window.__ModuleLoader__.load({
|
|
|
1598
1480
|
|
|
1599
1481
|
const send = useCallback(async (text, messageId) => {
|
|
1600
1482
|
const value = String(text || '').trim()
|
|
1601
|
-
if (!value) return
|
|
1483
|
+
if (!value || sendingRef.current) return
|
|
1484
|
+
sendingRef.current = true
|
|
1602
1485
|
setSending(true)
|
|
1603
1486
|
setLocalError('')
|
|
1604
1487
|
try {
|
|
@@ -1609,6 +1492,7 @@ window.__ModuleLoader__.load({
|
|
|
1609
1492
|
setLocalError(error.message)
|
|
1610
1493
|
setPending({ id: messageId, text: value })
|
|
1611
1494
|
} finally {
|
|
1495
|
+
sendingRef.current = false
|
|
1612
1496
|
setSending(false)
|
|
1613
1497
|
}
|
|
1614
1498
|
}, [onSend])
|
|
@@ -1644,11 +1528,30 @@ window.__ModuleLoader__.load({
|
|
|
1644
1528
|
h('span', { className: 'kd-spinner' }), T.sending))
|
|
1645
1529
|
}
|
|
1646
1530
|
|
|
1647
|
-
|
|
1531
|
+
const resize = (value) => onRatioChange(Math.max(20, Math.min(55, value)))
|
|
1532
|
+
return h('aside', { className: 'kd-thread', 'aria-label': T.followup, style: { '--kd-thread-ratio': ratio / 100 } },
|
|
1533
|
+
!narrow ? h('div', { className: 'kd-thread-resize', role: 'separator', tabIndex: 0,
|
|
1534
|
+
'aria-label': '调整题目与追问的宽度比例', 'aria-orientation': 'vertical',
|
|
1535
|
+
'aria-valuemin': 20, 'aria-valuemax': 55, 'aria-valuenow': Math.round(ratio),
|
|
1536
|
+
onPointerDown: (event) => { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId) },
|
|
1537
|
+
onPointerMove: (event) => {
|
|
1538
|
+
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
|
|
1539
|
+
const pane = event.currentTarget.parentElement
|
|
1540
|
+
const left = pane.closest('.kd-shell').querySelector('.kd-main').getBoundingClientRect().left
|
|
1541
|
+
const right = pane.getBoundingClientRect().right
|
|
1542
|
+
resize((right - event.clientX) / (right - left) * 100)
|
|
1543
|
+
},
|
|
1544
|
+
onPointerUp: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
|
|
1545
|
+
onPointerCancel: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
|
|
1546
|
+
onKeyDown: (event) => {
|
|
1547
|
+
const value = { ArrowLeft: ratio + 2, ArrowRight: ratio - 2, Home: 20, End: 55 }[event.key]
|
|
1548
|
+
if (value !== undefined) { event.preventDefault(); resize(value) }
|
|
1549
|
+
},
|
|
1550
|
+
}) : null,
|
|
1648
1551
|
h('div', { className: 'kd-thread-head' },
|
|
1649
1552
|
h('div', null,
|
|
1650
1553
|
h('div', { className: 'kd-thread-title' }, T.followup),
|
|
1651
|
-
h('div', { className: 'kd-msg-meta' },
|
|
1554
|
+
h('div', { className: 'kd-msg-meta' }, '围绕当前题目,一起理清思路')),
|
|
1652
1555
|
h('div', { className: 'kd-row' },
|
|
1653
1556
|
h(Btn, {
|
|
1654
1557
|
small: true,
|
|
@@ -1657,9 +1560,7 @@ window.__ModuleLoader__.load({
|
|
|
1657
1560
|
onClick: onClear,
|
|
1658
1561
|
title: '只清空本题的追问记录',
|
|
1659
1562
|
}, T.clearThread),
|
|
1660
|
-
|
|
1661
|
-
? h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' }))
|
|
1662
|
-
: null)),
|
|
1563
|
+
h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' })))),
|
|
1663
1564
|
h('div', { className: 'kd-thread-hint' }, T.followupHint),
|
|
1664
1565
|
h('div', { className: 'kd-thread-scroll', ref: scrollRef },
|
|
1665
1566
|
rows.length ? rows : h('div', { className: 'kd-empty-body' }, '还没有追问。可以问某一步推导为什么成立、某个知识点怎么理解,或有没有别的解法。')),
|
|
@@ -1669,13 +1570,13 @@ window.__ModuleLoader__.load({
|
|
|
1669
1570
|
h('textarea', {
|
|
1670
1571
|
className: 'kd-textarea',
|
|
1671
1572
|
value: draft,
|
|
1672
|
-
placeholder: '围绕这道题继续追问…(
|
|
1573
|
+
placeholder: '围绕这道题继续追问…(Enter 发送,Shift+Enter 换行)',
|
|
1673
1574
|
disabled: sending,
|
|
1674
1575
|
onChange: (event) => setDraft(event.target.value),
|
|
1675
1576
|
onKeyDown: (event) => {
|
|
1676
|
-
if (
|
|
1577
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && !event.nativeEvent?.isComposing && event.keyCode !== 229) {
|
|
1677
1578
|
event.preventDefault()
|
|
1678
|
-
send(draft, newAttemptId())
|
|
1579
|
+
if (!event.repeat) send(draft, newAttemptId())
|
|
1679
1580
|
}
|
|
1680
1581
|
},
|
|
1681
1582
|
}),
|
|
@@ -1689,12 +1590,22 @@ window.__ModuleLoader__.load({
|
|
|
1689
1590
|
}
|
|
1690
1591
|
|
|
1691
1592
|
/* ============================== 弹窗 ============================== */
|
|
1692
|
-
function SubjectsModal({ boot, onClose,
|
|
1593
|
+
function SubjectsModal({ boot, onClose, onDone }) {
|
|
1594
|
+
const [draft, setDraft] = useState(() => ({ subjects: [...boot.settings.subjects],
|
|
1595
|
+
subjectSource: { ...boot.settings.subjectSource }, customSubjects: structuredClone(boot.customSubjects || []) }))
|
|
1596
|
+
const [saving, setSaving] = useState(false)
|
|
1597
|
+
const [saveError, setSaveError] = useState('')
|
|
1693
1598
|
const [name, setName] = useState('')
|
|
1694
1599
|
const [types, setTypes] = useState([])
|
|
1695
1600
|
const [customTypes, setCustomTypes] = useState([])
|
|
1696
1601
|
const [typeDraft, setTypeDraft] = useState('')
|
|
1697
|
-
const enabled = new Set(
|
|
1602
|
+
const enabled = new Set(draft.subjects)
|
|
1603
|
+
const subjects = [...(boot.subjects || []), ...draft.customSubjects.filter((item) => !(boot.subjects || []).some((s) => s.id === item.id)).map((item) => ({
|
|
1604
|
+
...item, typeLabels: item.types.map((type) => ({ single: '单项选择题', multiple: '多项选择题', fill: '填空题', solution: '解答题' })[type] || type), bankItemCount: 0,
|
|
1605
|
+
}))]
|
|
1606
|
+
const close = () => { if (!saving) onClose() }
|
|
1607
|
+
const toggleSubject = (id) => setDraft((current) => ({ ...current,
|
|
1608
|
+
subjects: current.subjects.includes(id) ? current.subjects.filter((value) => value !== id) : [...current.subjects, id] }))
|
|
1698
1609
|
const toggleType = (type) => setTypes((current) => current.includes(type)
|
|
1699
1610
|
? current.filter((item) => item !== type)
|
|
1700
1611
|
: [...current, type])
|
|
@@ -1709,10 +1620,28 @@ window.__ModuleLoader__.load({
|
|
|
1709
1620
|
setCustomTypes((current) => current.filter((item) => item !== value))
|
|
1710
1621
|
setTypes((current) => current.filter((item) => item !== value))
|
|
1711
1622
|
}
|
|
1712
|
-
const
|
|
1623
|
+
const selectedTypes = [...new Set([...types, ...(typeDraft.trim() ? [typeDraft.trim().slice(0, 24)] : [])])]
|
|
1624
|
+
const canAdd = Boolean(name.trim()) && selectedTypes.length > 0
|
|
1625
|
+
const withCustomSubject = (current) => {
|
|
1626
|
+
const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), types: selectedTypes }
|
|
1627
|
+
return { ...current, subjects: [...current.subjects, subject.id], customSubjects: [...current.customSubjects, subject] }
|
|
1628
|
+
}
|
|
1629
|
+
const finish = async () => {
|
|
1630
|
+
if (saving) return
|
|
1631
|
+
if ((name.trim() || types.length || typeDraft.trim()) && !canAdd) {
|
|
1632
|
+
setSaveError('请填写自定义科目名称并至少选择一个题型,或清空未完成的输入。')
|
|
1633
|
+
return
|
|
1634
|
+
}
|
|
1635
|
+
const next = canAdd ? withCustomSubject(draft) : draft
|
|
1636
|
+
setSaving(true)
|
|
1637
|
+
setSaveError('')
|
|
1638
|
+
try { await onDone(next) }
|
|
1639
|
+
catch (error) { setSaveError(error.message || '保存失败,请重试') }
|
|
1640
|
+
finally { setSaving(false) }
|
|
1641
|
+
}
|
|
1713
1642
|
const submit = () => {
|
|
1714
1643
|
if (!canAdd) return
|
|
1715
|
-
|
|
1644
|
+
setDraft(withCustomSubject)
|
|
1716
1645
|
setName('')
|
|
1717
1646
|
setTypes([])
|
|
1718
1647
|
setCustomTypes([])
|
|
@@ -1720,24 +1649,26 @@ window.__ModuleLoader__.load({
|
|
|
1720
1649
|
}
|
|
1721
1650
|
return h(Modal, {
|
|
1722
1651
|
title: T.manageSubjects,
|
|
1723
|
-
onClose,
|
|
1652
|
+
onClose: close,
|
|
1724
1653
|
wide: true,
|
|
1725
1654
|
footer: [
|
|
1726
|
-
h(Btn, { key: '
|
|
1655
|
+
h(Btn, { key: 'cancel', onClick: close, disabled: saving }, T.cancel),
|
|
1656
|
+
h(Btn, { key: 'done', variant: 'primary', onClick: finish, disabled: saving }, saving ? '保存中…' : '完成'),
|
|
1727
1657
|
],
|
|
1728
1658
|
},
|
|
1659
|
+
saveError ? h('div', { className: 'kd-error-text', role: 'alert' }, saveError) : null,
|
|
1660
|
+
h('fieldset', { disabled: saving, style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
|
|
1729
1661
|
h('div', { className: 'kd-meta', style: { marginBottom: '12px' } },
|
|
1730
|
-
'
|
|
1731
|
-
|
|
1662
|
+
'修改后点击右下角「完成」统一保存;取消或关闭会放弃修改。停用科目不会删除历史题目、作答、收藏和追问。'),
|
|
1663
|
+
subjects.map((subject) => h('div', {
|
|
1732
1664
|
key: subject.id,
|
|
1733
|
-
className: 'kd-row-between',
|
|
1734
|
-
style: { padding: '8px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
|
|
1665
|
+
className: 'kd-row-between kd-manage-card' + (enabled.has(subject.id) ? ' is-enabled' : ''),
|
|
1735
1666
|
},
|
|
1736
1667
|
h('label', { className: 'kd-checkline', style: { flex: '1 1 240px', cursor: 'pointer' } },
|
|
1737
1668
|
h('input', {
|
|
1738
1669
|
type: 'checkbox',
|
|
1739
1670
|
checked: enabled.has(subject.id),
|
|
1740
|
-
onChange: () => (
|
|
1671
|
+
onChange: () => toggleSubject(subject.id),
|
|
1741
1672
|
}),
|
|
1742
1673
|
h('span', null,
|
|
1743
1674
|
h('span', null, subject.name),
|
|
@@ -1747,24 +1678,19 @@ window.__ModuleLoader__.load({
|
|
|
1747
1678
|
? h('div', { className: 'kd-row' },
|
|
1748
1679
|
h('select', {
|
|
1749
1680
|
className: 'kd-select',
|
|
1750
|
-
value: subject.
|
|
1681
|
+
value: draft.subjectSource[subject.id] || 'auto',
|
|
1751
1682
|
'aria-label': `${subject.name} 出题来源`,
|
|
1752
|
-
onChange: (event) =>
|
|
1683
|
+
onChange: (event) => { const source = event.target.value; setDraft((current) => ({ ...current, subjectSource: { ...current.subjectSource, [subject.id]: source } })) },
|
|
1753
1684
|
},
|
|
1754
1685
|
h('option', { value: 'auto' }, '自动(优先题库)'),
|
|
1755
1686
|
h('option', { value: 'online' }, '在线出题'),
|
|
1756
1687
|
h('option', { value: 'bank' }, '题库出题')),
|
|
1757
1688
|
subject.bankItemCount
|
|
1758
1689
|
? h('span', { className: 'kd-msg-meta' }, `题库 ${subject.bankItemCount} 题`)
|
|
1759
|
-
: h('span', { className: 'kd-msg-meta' }, '题库为空')
|
|
1760
|
-
h(Btn, {
|
|
1761
|
-
small: true,
|
|
1762
|
-
variant: 'ghost',
|
|
1763
|
-
onClick: () => { onClose(); onPick(subject.id) },
|
|
1764
|
-
}, '查看'))
|
|
1690
|
+
: h('span', { className: 'kd-msg-meta' }, '题库为空'))
|
|
1765
1691
|
: null)),
|
|
1766
1692
|
h('div', { className: 'kd-divider' }),
|
|
1767
|
-
h('div', { className: 'kd-field' },
|
|
1693
|
+
h('div', { className: 'kd-field kd-custom-subject' },
|
|
1768
1694
|
h('div', { className: 'kd-field-label' }, '添加自定义科目'),
|
|
1769
1695
|
h('div', { className: 'kd-row' },
|
|
1770
1696
|
h('input', {
|
|
@@ -1815,7 +1741,7 @@ window.__ModuleLoader__.load({
|
|
|
1815
1741
|
}),
|
|
1816
1742
|
h(Btn, { small: true, disabled: !typeDraft.trim(), onClick: addCustomType }, '添加题型')),
|
|
1817
1743
|
h('div', { className: 'kd-msg-meta', style: { marginTop: '6px' } },
|
|
1818
|
-
canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加')))
|
|
1744
|
+
canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
|
|
1819
1745
|
}
|
|
1820
1746
|
|
|
1821
1747
|
function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport }) {
|
|
@@ -1873,8 +1799,7 @@ window.__ModuleLoader__.load({
|
|
|
1873
1799
|
(boot.banks || []).length
|
|
1874
1800
|
? boot.banks.map((bank) => h('div', {
|
|
1875
1801
|
key: bank.id,
|
|
1876
|
-
className: 'kd-row-between',
|
|
1877
|
-
style: { padding: '9px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
|
|
1802
|
+
className: 'kd-row-between kd-manage-card',
|
|
1878
1803
|
},
|
|
1879
1804
|
h('div', { style: { minWidth: 0 } },
|
|
1880
1805
|
h('div', null, bank.name),
|
|
@@ -1919,13 +1844,13 @@ window.__ModuleLoader__.load({
|
|
|
1919
1844
|
h('div', { className: 'kd-msg-meta' }, '确认后才会替换正式版本;旧版本会保留在版本历史中。'))
|
|
1920
1845
|
}
|
|
1921
1846
|
|
|
1922
|
-
function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger }) {
|
|
1847
|
+
function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger, busy }) {
|
|
1923
1848
|
return h(Modal, {
|
|
1924
1849
|
title,
|
|
1925
1850
|
onClose,
|
|
1926
1851
|
footer: [
|
|
1927
|
-
h(Btn, { key: 'cancel', onClick: onClose }, T.cancel),
|
|
1928
|
-
h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm }, confirmLabel || T.confirm),
|
|
1852
|
+
h(Btn, { key: 'cancel', onClick: onClose, disabled: busy }, T.cancel),
|
|
1853
|
+
h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm, disabled: busy }, busy ? '正在处理…' : (confirmLabel || T.confirm)),
|
|
1929
1854
|
],
|
|
1930
1855
|
}, h('div', { className: 'kd-md' }, message))
|
|
1931
1856
|
}
|
|
@@ -1942,15 +1867,27 @@ window.__ModuleLoader__.load({
|
|
|
1942
1867
|
const [thread, setThread] = useState(null)
|
|
1943
1868
|
const [list, setList] = useState(null)
|
|
1944
1869
|
const [busy, setBusy] = useState('')
|
|
1870
|
+
const [managing, setManaging] = useState(false)
|
|
1871
|
+
const [selected, setSelected] = useState([])
|
|
1872
|
+
const bulkLock = useRef(false)
|
|
1945
1873
|
const [fail, setFail] = useState('')
|
|
1946
1874
|
const [toast, setToast] = useState(null)
|
|
1947
1875
|
const [modal, setModal] = useState(null)
|
|
1948
1876
|
const [choice, setChoice] = useState('')
|
|
1949
1877
|
const [localRedo, setLocalRedo] = useState(false)
|
|
1950
1878
|
const [threadOpen, setThreadOpen] = useState(false)
|
|
1879
|
+
const [threadRatio, setThreadRatio] = useState(33)
|
|
1951
1880
|
const fileRef = useRef(null)
|
|
1952
1881
|
const attemptRef = useRef(newAttemptId())
|
|
1953
1882
|
const toastTimer = useRef(null)
|
|
1883
|
+
const mainScroll = useRef(null)
|
|
1884
|
+
const listScroll = useRef(0)
|
|
1885
|
+
const redoQuestions = useRef(new Set())
|
|
1886
|
+
|
|
1887
|
+
useEffect(() => {
|
|
1888
|
+
if (view === 'list' && list && mainScroll.current) mainScroll.current.scrollTop = listScroll.current
|
|
1889
|
+
if (view === 'detail' && mainScroll.current) mainScroll.current.scrollTop = 0
|
|
1890
|
+
}, [view, list])
|
|
1954
1891
|
|
|
1955
1892
|
const notify = useCallback((message, tone = 'ok') => {
|
|
1956
1893
|
setToast({ message, tone })
|
|
@@ -1974,6 +1911,7 @@ window.__ModuleLoader__.load({
|
|
|
1974
1911
|
if (!sid) return
|
|
1975
1912
|
// 高亮必须跟着视图走:从「全部 / 收藏 / 错题本」切回今日题目时要一起复位
|
|
1976
1913
|
setKind('today')
|
|
1914
|
+
setThreadOpen(false)
|
|
1977
1915
|
setView('detail')
|
|
1978
1916
|
setBusy('daily')
|
|
1979
1917
|
setFail('')
|
|
@@ -1985,6 +1923,7 @@ window.__ModuleLoader__.load({
|
|
|
1985
1923
|
try {
|
|
1986
1924
|
const result = await rpc('ensureDaily', { subjectId: sid })
|
|
1987
1925
|
setQuestion(result.question)
|
|
1926
|
+
setLocalRedo(redoQuestions.current.has(result.question.id))
|
|
1988
1927
|
setThread(result.thread || null)
|
|
1989
1928
|
} catch (issue) {
|
|
1990
1929
|
setFail(issue.message)
|
|
@@ -1995,7 +1934,9 @@ window.__ModuleLoader__.load({
|
|
|
1995
1934
|
|
|
1996
1935
|
const loadList = useCallback(async (sid, nextKind) => {
|
|
1997
1936
|
if (!sid) return
|
|
1937
|
+
listScroll.current = 0
|
|
1998
1938
|
setKind(nextKind)
|
|
1939
|
+
setThreadOpen(false)
|
|
1999
1940
|
setView('list')
|
|
2000
1941
|
setBusy('list')
|
|
2001
1942
|
setList(null)
|
|
@@ -2009,6 +1950,8 @@ window.__ModuleLoader__.load({
|
|
|
2009
1950
|
}
|
|
2010
1951
|
}, [notify])
|
|
2011
1952
|
|
|
1953
|
+
useEffect(() => { setSelected([]); setManaging(false) }, [subjectId, kind, view])
|
|
1954
|
+
|
|
2012
1955
|
const pickSubject = useCallback((sid) => {
|
|
2013
1956
|
if (!sid || sid === subjectId) return
|
|
2014
1957
|
setSubjectId(sid)
|
|
@@ -2018,13 +1961,17 @@ window.__ModuleLoader__.load({
|
|
|
2018
1961
|
}, [subjectId, loadToday])
|
|
2019
1962
|
|
|
2020
1963
|
const openQuestion = useCallback(async (qid) => {
|
|
1964
|
+
listScroll.current = mainScroll.current?.scrollTop || 0
|
|
2021
1965
|
setBusy('question')
|
|
2022
1966
|
try {
|
|
2023
1967
|
const result = await rpc('getQuestion', { qid })
|
|
1968
|
+
const canResume = result.isToday && result.question.answerState === 'revealed'
|
|
1969
|
+
&& result.question.attemptCount > 0 && !redoQuestions.current.has(qid)
|
|
1970
|
+
if (!canResume) redoQuestions.current.add(qid)
|
|
2024
1971
|
setQuestion(result.question)
|
|
2025
1972
|
setThread(result.thread || null)
|
|
2026
1973
|
setView('detail')
|
|
2027
|
-
setLocalRedo(
|
|
1974
|
+
setLocalRedo(!canResume)
|
|
2028
1975
|
setChoice('')
|
|
2029
1976
|
setThreadOpen(false)
|
|
2030
1977
|
attemptRef.current = newAttemptId()
|
|
@@ -2035,6 +1982,15 @@ window.__ModuleLoader__.load({
|
|
|
2035
1982
|
}
|
|
2036
1983
|
}, [notify])
|
|
2037
1984
|
|
|
1985
|
+
const backToList = async () => {
|
|
1986
|
+
setThreadOpen(false)
|
|
1987
|
+
setView('list')
|
|
1988
|
+
try {
|
|
1989
|
+
const result = await rpc('listQuestions', { subjectId, kind })
|
|
1990
|
+
setList(result)
|
|
1991
|
+
} catch (issue) { notify(issue.message, 'bad') }
|
|
1992
|
+
}
|
|
1993
|
+
|
|
2038
1994
|
useEffect(() => {
|
|
2039
1995
|
let cancelled = false
|
|
2040
1996
|
rpc('bootstrap').then((result) => {
|
|
@@ -2070,6 +2026,7 @@ window.__ModuleLoader__.load({
|
|
|
2070
2026
|
attemptId: attemptRef.current,
|
|
2071
2027
|
})
|
|
2072
2028
|
setQuestion(result.question)
|
|
2029
|
+
redoQuestions.current.delete(question.id)
|
|
2073
2030
|
setLocalRedo(false)
|
|
2074
2031
|
refreshBoot()
|
|
2075
2032
|
} catch (issue) {
|
|
@@ -2210,67 +2167,18 @@ window.__ModuleLoader__.load({
|
|
|
2210
2167
|
}
|
|
2211
2168
|
}, [boot, notify, refreshBoot])
|
|
2212
2169
|
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
}],
|
|
2224
|
-
})
|
|
2225
|
-
}
|
|
2226
|
-
const result = await rpc('addSubject', { subjectId: targetId })
|
|
2227
|
-
const fresh = await refreshBoot()
|
|
2228
|
-
notify(`${subject.name} 已启用,系统会为它分配今天的题目`)
|
|
2229
|
-
if (fresh && result) {
|
|
2230
|
-
setSubjectId(targetId)
|
|
2231
|
-
loadToday(targetId)
|
|
2232
|
-
}
|
|
2233
|
-
setModal(null)
|
|
2234
|
-
} catch (issue) {
|
|
2235
|
-
notify(issue.message, 'bad')
|
|
2236
|
-
}
|
|
2237
|
-
}, [boot, notify, refreshBoot, loadToday])
|
|
2238
|
-
|
|
2239
|
-
const removeSubject = useCallback((subject) => {
|
|
2240
|
-
setModal({
|
|
2241
|
-
kind: 'confirm',
|
|
2242
|
-
title: `${T.deleteSubject}:${subject.name}`,
|
|
2243
|
-
message: `确认把「${subject.name}」移出每日出题吗?\n\n- 该科目已出过的题目、收藏、错题与作答记录都会保留在“全部”里。\n- 今天已经分配的题目不会丢失,重新启用后当天仍会复用同一道题。`,
|
|
2244
|
-
confirmLabel: T.deleteSubject,
|
|
2245
|
-
danger: true,
|
|
2246
|
-
onConfirm: async () => {
|
|
2247
|
-
setModal(null)
|
|
2248
|
-
try {
|
|
2249
|
-
const result = await rpc('removeSubject', { subjectId: subject.id })
|
|
2250
|
-
const fresh = await refreshBoot()
|
|
2251
|
-
notify(result.message || '已移出每日出题')
|
|
2252
|
-
if (fresh) {
|
|
2253
|
-
const next = fresh.settings.subjects[0] || null
|
|
2254
|
-
setSubjectId(next)
|
|
2255
|
-
if (next) loadToday(next)
|
|
2256
|
-
else { setQuestion(null); setView('empty') }
|
|
2257
|
-
}
|
|
2258
|
-
} catch (issue) {
|
|
2259
|
-
notify(issue.message, 'bad')
|
|
2260
|
-
}
|
|
2261
|
-
},
|
|
2262
|
-
})
|
|
2263
|
-
}, [notify, refreshBoot, loadToday])
|
|
2264
|
-
|
|
2265
|
-
const changeSource = useCallback(async (id, source) => {
|
|
2266
|
-
try {
|
|
2267
|
-
await rpc('saveSettings', { subjectSource: { [id]: source } })
|
|
2268
|
-
await refreshBoot()
|
|
2269
|
-
notify('出题来源已更新')
|
|
2270
|
-
} catch (issue) {
|
|
2271
|
-
notify(issue.message, 'bad')
|
|
2170
|
+
const saveSubjects = async (draft) => {
|
|
2171
|
+
const next = draft.subjects.includes(subjectId) ? subjectId : (draft.subjects[0] || null)
|
|
2172
|
+
const result = await rpc('saveSettings', { ...draft, activeSubject: next })
|
|
2173
|
+
setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
|
|
2174
|
+
setModal(null)
|
|
2175
|
+
notify('科目设置已保存')
|
|
2176
|
+
if (next !== subjectId) {
|
|
2177
|
+
setSubjectId(next)
|
|
2178
|
+
if (next) loadToday(next)
|
|
2179
|
+
else { setQuestion(null); setThread(null); setThreadOpen(false); setView('empty') }
|
|
2272
2180
|
}
|
|
2273
|
-
}
|
|
2181
|
+
}
|
|
2274
2182
|
|
|
2275
2183
|
const pickFile = useCallback(() => {
|
|
2276
2184
|
if (fileRef.current) {
|
|
@@ -2335,6 +2243,38 @@ window.__ModuleLoader__.load({
|
|
|
2335
2243
|
})
|
|
2336
2244
|
}, [notify, refreshBoot])
|
|
2337
2245
|
|
|
2246
|
+
const bulkQuestions = async (action) => {
|
|
2247
|
+
if (bulkLock.current || !selected.length) return
|
|
2248
|
+
const qids = [...selected]
|
|
2249
|
+
const execute = async (confirmProtected = false) => {
|
|
2250
|
+
if (bulkLock.current) return
|
|
2251
|
+
bulkLock.current = true
|
|
2252
|
+
setBusy('bulk')
|
|
2253
|
+
try {
|
|
2254
|
+
const result = await rpc('bulkQuestions', { qids, action, confirmProtected })
|
|
2255
|
+
if (result.requiresConfirmation) {
|
|
2256
|
+
setModal({ kind: 'confirm', title: '再次确认删除收藏或错题', danger: true,
|
|
2257
|
+
message: `选中的题目中有 ${result.protectedCount} 道已收藏或在错题本中。继续将永久删除所选 ${qids.length} 道题及其作答和追问,无法恢复。确定仍要删除吗?`,
|
|
2258
|
+
confirmLabel: '仍要永久删除', onConfirm: () => execute(true) })
|
|
2259
|
+
return
|
|
2260
|
+
}
|
|
2261
|
+
setModal(null)
|
|
2262
|
+
setSelected([])
|
|
2263
|
+
await loadList(subjectId, kind)
|
|
2264
|
+
await refreshBoot()
|
|
2265
|
+
notify(`已处理 ${result.affected} 道题目`)
|
|
2266
|
+
} catch (issue) {
|
|
2267
|
+
setModal(null)
|
|
2268
|
+
notify(issue.message, 'bad')
|
|
2269
|
+
} finally { bulkLock.current = false; setBusy('') }
|
|
2270
|
+
}
|
|
2271
|
+
if (action === 'delete') {
|
|
2272
|
+
setModal({ kind: 'confirm', title: '删除所选题目', danger: true,
|
|
2273
|
+
message: `将永久删除 ${qids.length} 道题及其作答、收藏、错题标记和追问,无法恢复。题库原文件仍会保留,未来可能重新抽到其中的题目。`,
|
|
2274
|
+
confirmLabel: '确认删除', onConfirm: () => execute() })
|
|
2275
|
+
} else await execute()
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2338
2278
|
const counts = subjectId ? boot?.counts?.[subjectId] : null
|
|
2339
2279
|
const narrow = useMediaQuery('(max-width: 1180px)')
|
|
2340
2280
|
|
|
@@ -2399,6 +2339,11 @@ window.__ModuleLoader__.load({
|
|
|
2399
2339
|
kind,
|
|
2400
2340
|
subjectName: currentSubject?.name || '',
|
|
2401
2341
|
onOpen: openQuestion,
|
|
2342
|
+
managing, selected, busy: busy === 'bulk',
|
|
2343
|
+
onManage: () => { setManaging(!managing); setSelected([]) },
|
|
2344
|
+
onToggle: (id) => setSelected((ids) => ids.includes(id) ? ids.filter((value) => value !== id) : [...ids, id]),
|
|
2345
|
+
onSelectAll: (value) => setSelected(value ? list.items.map((item) => item.id) : []),
|
|
2346
|
+
onBulk: bulkQuestions,
|
|
2402
2347
|
})
|
|
2403
2348
|
}
|
|
2404
2349
|
if (view === 'detail' && question) {
|
|
@@ -2407,7 +2352,6 @@ window.__ModuleLoader__.load({
|
|
|
2407
2352
|
busy,
|
|
2408
2353
|
choice,
|
|
2409
2354
|
localRedo,
|
|
2410
|
-
narrow,
|
|
2411
2355
|
threadOpen,
|
|
2412
2356
|
onSelect: (key) => {
|
|
2413
2357
|
setChoice((current) => {
|
|
@@ -2422,7 +2366,9 @@ window.__ModuleLoader__.load({
|
|
|
2422
2366
|
onJudge: judge,
|
|
2423
2367
|
onFavorite: favorite,
|
|
2424
2368
|
onRedo: () => {
|
|
2369
|
+
redoQuestions.current.add(question.id)
|
|
2425
2370
|
setLocalRedo(true)
|
|
2371
|
+
setThreadOpen(false)
|
|
2426
2372
|
setChoice('')
|
|
2427
2373
|
attemptRef.current = newAttemptId()
|
|
2428
2374
|
},
|
|
@@ -2449,7 +2395,13 @@ window.__ModuleLoader__.load({
|
|
|
2449
2395
|
})
|
|
2450
2396
|
})()
|
|
2451
2397
|
|
|
2452
|
-
const
|
|
2398
|
+
const pickTab = (value) => {
|
|
2399
|
+
if (value === kind && (value === 'today' || view === 'list')) return
|
|
2400
|
+
if (value === 'today') loadToday(subjectId)
|
|
2401
|
+
else loadList(subjectId, value)
|
|
2402
|
+
}
|
|
2403
|
+
const showThread = view === 'detail' && question && threadOpen && !localRedo
|
|
2404
|
+
&& question.answerState === 'revealed' && question.attemptCount > 0
|
|
2453
2405
|
|
|
2454
2406
|
return h('div', { className: 'kd-page' },
|
|
2455
2407
|
styleInjected ? null : h('style', null, CSS),
|
|
@@ -2459,10 +2411,10 @@ window.__ModuleLoader__.load({
|
|
|
2459
2411
|
h('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
|
|
2460
2412
|
h('div', { style: { minWidth: 0 } },
|
|
2461
2413
|
h('h1', { className: 'kd-title' }, T.title),
|
|
2462
|
-
h('div', { className: 'kd-sub' },
|
|
2414
|
+
h('div', { className: 'kd-sub', title: T.subtitle },
|
|
2463
2415
|
boot?.today ? `${formatDate(boot.today)} · ` : '',
|
|
2464
2416
|
boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
|
|
2465
|
-
|
|
2417
|
+
'每天一道,稳步向前'))),
|
|
2466
2418
|
boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
|
|
2467
2419
|
boot ? h(ExamStrip, {
|
|
2468
2420
|
boot,
|
|
@@ -2473,14 +2425,9 @@ window.__ModuleLoader__.load({
|
|
|
2473
2425
|
boot ? h(MobileBar, {
|
|
2474
2426
|
boot,
|
|
2475
2427
|
subjectId,
|
|
2476
|
-
kind,
|
|
2477
2428
|
onPick: pickSubject,
|
|
2478
2429
|
onManage: () => setModal({ kind: 'subjects' }),
|
|
2479
2430
|
onBanks: () => setModal({ kind: 'banks' }),
|
|
2480
|
-
onTab: (value) => {
|
|
2481
|
-
if (value === 'today') loadToday(subjectId)
|
|
2482
|
-
else loadList(subjectId, value)
|
|
2483
|
-
},
|
|
2484
2431
|
}) : null,
|
|
2485
2432
|
h('div', { className: 'kd-shell' },
|
|
2486
2433
|
boot ? h(SubjectNav, {
|
|
@@ -2491,27 +2438,30 @@ window.__ModuleLoader__.load({
|
|
|
2491
2438
|
onBanks: () => setModal({ kind: 'banks' }),
|
|
2492
2439
|
}) : null,
|
|
2493
2440
|
h('main', { className: 'kd-main' },
|
|
2494
|
-
h('div', { className: 'kd-main-
|
|
2441
|
+
h('div', { className: 'kd-main-toolbar' },
|
|
2495
2442
|
boot ? h(TabBar, {
|
|
2496
2443
|
kind,
|
|
2497
2444
|
counts,
|
|
2498
|
-
onPick:
|
|
2499
|
-
if (value === 'today') loadToday(subjectId)
|
|
2500
|
-
else loadList(subjectId, value)
|
|
2501
|
-
},
|
|
2445
|
+
onPick: pickTab,
|
|
2502
2446
|
}) : null,
|
|
2503
|
-
|
|
2504
|
-
|
|
2447
|
+
kind !== 'today' && view === 'detail'
|
|
2448
|
+
? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
|
|
2449
|
+
h('div', { className: 'kd-main-scroll', ref: mainScroll },
|
|
2450
|
+
h('div', { className: 'kd-main-inner' },
|
|
2451
|
+
h('div', { className: 'kd-view', key: `${subjectId}|${kind}|${view}|${question?.id || ''}` }, mainContent)))),
|
|
2452
|
+
h(Presence, null, showThread
|
|
2505
2453
|
? h(ThreadPane, {
|
|
2506
2454
|
thread,
|
|
2507
2455
|
questionId: question.id,
|
|
2508
2456
|
busy,
|
|
2509
2457
|
narrow,
|
|
2458
|
+
ratio: threadRatio,
|
|
2459
|
+
onRatioChange: setThreadRatio,
|
|
2510
2460
|
onSend: sendFollowup,
|
|
2511
2461
|
onClear: clearThread,
|
|
2512
2462
|
onClose: () => setThreadOpen(false),
|
|
2513
2463
|
})
|
|
2514
|
-
: null),
|
|
2464
|
+
: null)),
|
|
2515
2465
|
h('input', {
|
|
2516
2466
|
ref: fileRef,
|
|
2517
2467
|
type: 'file',
|
|
@@ -2519,14 +2469,12 @@ window.__ModuleLoader__.load({
|
|
|
2519
2469
|
style: { display: 'none' },
|
|
2520
2470
|
onChange: onFile,
|
|
2521
2471
|
}),
|
|
2472
|
+
h(Presence, null,
|
|
2522
2473
|
modal?.kind === 'subjects' && boot
|
|
2523
2474
|
? h(SubjectsModal, {
|
|
2524
2475
|
boot,
|
|
2525
2476
|
onClose: () => setModal(null),
|
|
2526
|
-
|
|
2527
|
-
onRemove: removeSubject,
|
|
2528
|
-
onSource: changeSource,
|
|
2529
|
-
onPick: (id) => { setModal(null); pickSubject(id) },
|
|
2477
|
+
onDone: saveSubjects,
|
|
2530
2478
|
})
|
|
2531
2479
|
: null,
|
|
2532
2480
|
modal?.kind === 'banks' && boot
|
|
@@ -2548,13 +2496,16 @@ window.__ModuleLoader__.load({
|
|
|
2548
2496
|
onDiscard: discardRevision,
|
|
2549
2497
|
}) : null,
|
|
2550
2498
|
modal?.kind === 'confirm' ? h(ConfirmModal, {
|
|
2499
|
+
key: modal.title,
|
|
2551
2500
|
title: modal.title,
|
|
2552
2501
|
message: modal.message,
|
|
2553
2502
|
confirmLabel: modal.confirmLabel,
|
|
2554
2503
|
danger: modal.danger,
|
|
2504
|
+
busy: busy === 'bulk',
|
|
2555
2505
|
onConfirm: modal.onConfirm,
|
|
2556
|
-
onClose: () => setModal(null),
|
|
2506
|
+
onClose: () => { if (!bulkLock.current) setModal(null) },
|
|
2557
2507
|
}) : null,
|
|
2508
|
+
),
|
|
2558
2509
|
toast ? h('div', { className: `kd-toast kd-toast-${toast.tone}` }, toast.message) : null,
|
|
2559
2510
|
busy === 'import' || busy === 'commit'
|
|
2560
2511
|
? h('div', { className: 'kd-loading', style: { position: 'absolute', inset: 'auto 0 60px 0' } },
|