daily-question 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +24 -17
- package/client.js +158 -99
- package/cordis.patch.yml +1 -1
- package/icon.svg +10 -7
- package/index.js +6 -6
- package/lib/bank.js +53 -36
- package/lib/exam-dates.js +101 -67
- package/lib/generate.js +11 -14
- package/lib/routes.js +49 -86
- package/lib/service.js +72 -72
- package/lib/store.js +3 -8
- package/lib/subjects.js +12 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory(require) {
|
|
4
4
|
const React = require('react')
|
|
5
5
|
const h = React.createElement
|
|
6
|
-
const { useState, useEffect, useRef, useMemo, useCallback } = React
|
|
6
|
+
const { useState, useEffect, useRef, useMemo, useCallback, useId } = React
|
|
7
7
|
|
|
8
8
|
/* ============================== 文案 ============================== */
|
|
9
9
|
const T = {
|
|
@@ -568,9 +568,7 @@ window.__ModuleLoader__.load({
|
|
|
568
568
|
display:flex; flex-direction:column; gap:12px; }
|
|
569
569
|
.kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
|
|
570
570
|
.kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:center; }
|
|
571
|
-
.kd-brand { flex:0 0 40px;
|
|
572
|
-
justify-content:center; border:0; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface));
|
|
573
|
-
color:var(--kd-brand-primary); }
|
|
571
|
+
.kd-brand { flex:0 0 40px; }
|
|
574
572
|
.kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
|
|
575
573
|
.kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
|
|
576
574
|
.kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
|
|
@@ -580,9 +578,10 @@ window.__ModuleLoader__.load({
|
|
|
580
578
|
.kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
|
|
581
579
|
.kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
|
|
582
580
|
gap:2px; text-align:right; }
|
|
583
|
-
.kd-cd-primary { font-size:21px; font-weight:
|
|
581
|
+
.kd-cd-primary, .kd-cd-days { font-size:21px; font-weight:400; color:var(--kd-brand-primary); font-variant-numeric:tabular-nums; line-height:1.25; }
|
|
582
|
+
.kd-cd-days strong { font-size:36px; font-weight:700; }
|
|
584
583
|
.kd-cd-secondary { font-size:12px; color:var(--kd-label-secondary); }
|
|
585
|
-
.kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-
|
|
584
|
+
.kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-primary); color:light-dark(#000, var(--kd-label-primary)); }
|
|
586
585
|
.kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
|
|
587
586
|
border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
|
|
588
587
|
font-size:11.5px; color:var(--kd-label-secondary); }
|
|
@@ -592,7 +591,8 @@ window.__ModuleLoader__.load({
|
|
|
592
591
|
.kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
|
|
593
592
|
padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
|
|
594
593
|
.kd-nav-group { display:flex; flex-direction:column; gap:4px; }
|
|
595
|
-
.kd-nav-group-label {
|
|
594
|
+
.kd-nav-group-label { display:flex; justify-content:space-between; gap:6px;
|
|
595
|
+
font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
|
|
596
596
|
padding:0 8px 4px; }
|
|
597
597
|
.kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
|
|
598
598
|
width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
|
|
@@ -645,6 +645,7 @@ window.__ModuleLoader__.load({
|
|
|
645
645
|
border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
|
|
646
646
|
cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
|
|
647
647
|
.kd-tab:hover { color:var(--kd-label-primary); background:var(--kd-bg-layer-2); }
|
|
648
|
+
.kd-tab:disabled { opacity:.5; cursor:default; background:none; transform:none; }
|
|
648
649
|
.kd-tab.is-active { color:var(--kd-brand-primary); font-weight:600; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface)); }
|
|
649
650
|
|
|
650
651
|
.kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
|
|
@@ -1075,13 +1076,17 @@ window.__ModuleLoader__.load({
|
|
|
1075
1076
|
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
1076
1077
|
}
|
|
1077
1078
|
|
|
1078
|
-
/** 剩余不超过 30
|
|
1079
|
+
/** 剩余不超过 30 天时省略按年、月拆分的时长。 */
|
|
1079
1080
|
function CountdownLines({ countdown }) {
|
|
1080
|
-
const lines =
|
|
1081
|
+
const { year, days, approx, lines = {} } = countdown || {}
|
|
1082
|
+
const showDays = Number.isInteger(days) && days >= 0
|
|
1083
|
+
const duration = days === 0 ? '今日开考' : lines.secondary
|
|
1084
|
+
const spaceDigits = (line = '') => line.replace(/\p{Script=Han}(?=\d)|\d(?=\p{Script=Han})/gu, '$& ')
|
|
1081
1085
|
return h('div', { className: 'kd-cd-lines' },
|
|
1082
|
-
h('div', { className: 'kd-cd-primary' }, lines.primary
|
|
1083
|
-
|
|
1084
|
-
h('div', { className: 'kd-cd-
|
|
1086
|
+
h('div', { className: 'kd-cd-primary' }, showDays ? `距离 ${year} 考研还剩` : spaceDigits(lines.primary)),
|
|
1087
|
+
showDays ? h('div', { className: 'kd-cd-days' }, approx ? '约 ' : null, h('strong', null, String(days)), ' 天') : null,
|
|
1088
|
+
duration ? h('div', { className: 'kd-cd-secondary' }, spaceDigits(duration)) : null,
|
|
1089
|
+
h('div', { className: 'kd-cd-target' }, spaceDigits(lines.target)))
|
|
1085
1090
|
}
|
|
1086
1091
|
|
|
1087
1092
|
/** 年份、日期口径与来源:一条紧凑的状态带,横跨整个头部。 */
|
|
@@ -1095,9 +1100,6 @@ window.__ModuleLoader__.load({
|
|
|
1095
1100
|
const sourceLabel = cd.status === 'official'
|
|
1096
1101
|
? `来源 ${hostOf(sourceUrl) || '官方'}`
|
|
1097
1102
|
: basis ? `依据 ${basis.year} 官方(${hostOf(basis.sourceUrl) || '官方'})` : '无来源记录'
|
|
1098
|
-
const noteText = cd.status === 'estimated' && basis
|
|
1099
|
-
? `按 ${basis.year} 官方日期推后一年估算`
|
|
1100
|
-
: cd.status === 'unverified' ? '官方日期尚未核实' : (cd.note || '')
|
|
1101
1103
|
|
|
1102
1104
|
return h('div', { className: 'kd-exam-strip' },
|
|
1103
1105
|
h('label', { className: 'kd-strip-item' },
|
|
@@ -1117,7 +1119,7 @@ window.__ModuleLoader__.load({
|
|
|
1117
1119
|
: h('span', null, sourceLabel)),
|
|
1118
1120
|
cd.verifiedAt
|
|
1119
1121
|
? h('span', { className: 'kd-strip-item', title: formatDateTime(cd.verifiedAt) },
|
|
1120
|
-
|
|
1122
|
+
`核验成功 ${formatShort(cd.verifiedAt)}`)
|
|
1121
1123
|
: null,
|
|
1122
1124
|
h(Btn, {
|
|
1123
1125
|
small: true,
|
|
@@ -1128,7 +1130,9 @@ window.__ModuleLoader__.load({
|
|
|
1128
1130
|
}, busy ? h('span', { className: 'kd-spinner' }) : h(Icon, { name: 'refresh', size: 13 }), '核验'),
|
|
1129
1131
|
cd.expired
|
|
1130
1132
|
? h('span', { className: 'kd-strip-item kd-strip-warn' }, '目标日期已过,请重新选择考研年份')
|
|
1131
|
-
:
|
|
1133
|
+
: cd.status === 'unverified'
|
|
1134
|
+
? h('span', { className: 'kd-strip-item kd-strip-warn', title: cd.note || '' }, '官方日期尚未核实')
|
|
1135
|
+
: null)
|
|
1132
1136
|
}
|
|
1133
1137
|
|
|
1134
1138
|
/* ============================== 导航 ============================== */
|
|
@@ -1167,25 +1171,29 @@ window.__ModuleLoader__.load({
|
|
|
1167
1171
|
|
|
1168
1172
|
function SubjectNav({ boot, subjectId, onPick, onManage, onBanks }) {
|
|
1169
1173
|
const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
|
|
1174
|
+
const archived = archivedSubjects(boot, subjectId)
|
|
1175
|
+
const subjectButton = (subject) => {
|
|
1176
|
+
const done = Boolean(boot.todayMap?.[subject.id])
|
|
1177
|
+
return h('button', {
|
|
1178
|
+
key: subject.id,
|
|
1179
|
+
type: 'button',
|
|
1180
|
+
className: `kd-nav-item${subject.id === subjectId ? ' is-active' : ''}`,
|
|
1181
|
+
onClick: () => onPick(subject.id),
|
|
1182
|
+
title: subject.name,
|
|
1183
|
+
},
|
|
1184
|
+
h('span', null, subject.name),
|
|
1185
|
+
subject.enabled ? h('span', { className: 'kd-nav-status' },
|
|
1186
|
+
h('span', { className: `kd-dot${done ? '' : ' kd-dot-idle'}` }),
|
|
1187
|
+
done ? '今日已出' : '待出题') : null)
|
|
1188
|
+
}
|
|
1170
1189
|
return h('div', { className: 'kd-nav' },
|
|
1171
1190
|
h('div', { className: 'kd-nav-group' },
|
|
1172
|
-
h('div', { className: 'kd-nav-group-label' }, '
|
|
1173
|
-
enabled.length
|
|
1174
|
-
? enabled.map((subject) => {
|
|
1175
|
-
const done = Boolean(boot.todayMap?.[subject.id])
|
|
1176
|
-
return h('button', {
|
|
1177
|
-
key: subject.id,
|
|
1178
|
-
type: 'button',
|
|
1179
|
-
className: `kd-nav-item${subject.id === subjectId ? ' is-active' : ''}`,
|
|
1180
|
-
onClick: () => onPick(subject.id),
|
|
1181
|
-
title: subject.name,
|
|
1182
|
-
},
|
|
1183
|
-
h('span', null, subject.name),
|
|
1184
|
-
h('span', { className: 'kd-nav-status' },
|
|
1185
|
-
h('span', { className: `kd-dot${done ? '' : ' kd-dot-idle'}` }),
|
|
1186
|
-
done ? '今日已出' : '待出题'))
|
|
1187
|
-
})
|
|
1191
|
+
h('div', { className: 'kd-nav-group-label' }, '已启用科目', h('span', null, String(enabled.length))),
|
|
1192
|
+
enabled.length ? enabled.map(subjectButton)
|
|
1188
1193
|
: h('div', { className: 'kd-msg-meta', style: { padding: '4px 8px' } }, '还没有启用科目')),
|
|
1194
|
+
archived.length ? h('div', { className: 'kd-nav-group' },
|
|
1195
|
+
h('div', { className: 'kd-nav-group-label' }, '已停用科目', h('span', null, String(archived.length))),
|
|
1196
|
+
archived.map(subjectButton)) : null,
|
|
1189
1197
|
h('div', { className: 'kd-nav-spacer' }),
|
|
1190
1198
|
h('div', { className: 'kd-nav-group' },
|
|
1191
1199
|
h('div', { className: 'kd-nav-group-label' }, '设置'),
|
|
@@ -1194,7 +1202,11 @@ window.__ModuleLoader__.load({
|
|
|
1194
1202
|
h(UpdateCheck,{currentVersion:boot.pluginVersion})))
|
|
1195
1203
|
}
|
|
1196
1204
|
|
|
1197
|
-
function
|
|
1205
|
+
function archivedSubjects(boot, subjectId) {
|
|
1206
|
+
return (boot.subjects || []).filter((subject) => !subject.enabled && (boot.counts?.[subject.id]?.all || subject.id === subjectId))
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function TabBar({ kind, counts, onPick, todayDisabled }) {
|
|
1198
1210
|
const items = [
|
|
1199
1211
|
['today', T.tabs.today, null],
|
|
1200
1212
|
['all', T.tabs.all, counts?.all],
|
|
@@ -1206,19 +1218,24 @@ window.__ModuleLoader__.load({
|
|
|
1206
1218
|
key: value,
|
|
1207
1219
|
type: 'button',
|
|
1208
1220
|
className: `kd-tab${kind === value ? ' is-active' : ''}`,
|
|
1221
|
+
disabled: value === 'today' && todayDisabled,
|
|
1209
1222
|
onClick: () => onPick(value),
|
|
1210
1223
|
}, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
|
|
1211
1224
|
}
|
|
1212
1225
|
|
|
1213
1226
|
function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
|
|
1214
1227
|
const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
|
|
1228
|
+
const archived = archivedSubjects(boot, subjectId)
|
|
1229
|
+
const option = (subject) => h('option', { key: subject.id, value: subject.id }, subject.name)
|
|
1215
1230
|
return h('div', { className: 'kd-mobilebar' },
|
|
1216
1231
|
h('select', {
|
|
1217
1232
|
className: 'kd-select',
|
|
1218
1233
|
value: subjectId || '',
|
|
1219
1234
|
'aria-label': '切换科目',
|
|
1220
1235
|
onChange: (event) => onPick(event.target.value),
|
|
1221
|
-
},
|
|
1236
|
+
}, !subjectId ? h('option', { value: '', disabled: true }, '选择科目') : null,
|
|
1237
|
+
enabled.length ? h('optgroup', { label: `已启用科目(${enabled.length})` }, enabled.map(option)) : null,
|
|
1238
|
+
archived.length ? h('optgroup', { label: `已停用科目(${archived.length})` }, archived.map(option)) : null),
|
|
1222
1239
|
// 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
|
|
1223
1240
|
h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
|
|
1224
1241
|
h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
|
|
@@ -1311,12 +1328,13 @@ window.__ModuleLoader__.load({
|
|
|
1311
1328
|
function QuestionPane({
|
|
1312
1329
|
q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
|
|
1313
1330
|
onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
|
|
1314
|
-
onCancel,
|
|
1331
|
+
onCancel, onSkip,
|
|
1315
1332
|
}) {
|
|
1316
1333
|
if (!q) return null
|
|
1317
1334
|
const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
|
|
1318
1335
|
const last = q.lastAttempt
|
|
1319
1336
|
const isChoice = typeof q.isChoice === 'boolean' ? q.isChoice : isChoiceType(q.type)
|
|
1337
|
+
const canSelect = isChoice && (q.canSelect ?? q.options?.length >= 2)
|
|
1320
1338
|
const answerKey = q.answer?.answerKey || ''
|
|
1321
1339
|
const selected = revealed ? (last?.choice || '') : choice
|
|
1322
1340
|
const meta = [q.subjectName, q.typeLabel]
|
|
@@ -1351,11 +1369,12 @@ window.__ModuleLoader__.load({
|
|
|
1351
1369
|
revealed ? h('div', null, q.examAlignment.rationale,
|
|
1352
1370
|
...(q.examAlignment.warnings || []).map((warning,i)=>h('div',{key:i,className:'kd-meta'},warning))) : null) : null,
|
|
1353
1371
|
h('div', { className: 'kd-stem' }, h(Markdown, { text: q.stem })),
|
|
1354
|
-
isChoice &&
|
|
1372
|
+
isChoice && !canSelect ? h('div', { className: 'kd-msg-meta' }, '本题选项不完整,可跳过或查看答案后自行判定。') : null,
|
|
1373
|
+
canSelect
|
|
1355
1374
|
? h('div', { className: 'kd-options' }, q.options.map((option) => {
|
|
1356
1375
|
const isSelected = selected.split('').includes(option.key)
|
|
1357
1376
|
const isRight = revealed && answerKey.split('').includes(option.key)
|
|
1358
|
-
const isWrongPick = revealed && isSelected && !isRight
|
|
1377
|
+
const isWrongPick = revealed && answerKey && isSelected && !isRight
|
|
1359
1378
|
const cls = ['kd-option']
|
|
1360
1379
|
if (isSelected && !revealed) cls.push('is-selected')
|
|
1361
1380
|
if (isRight) cls.push('is-correct')
|
|
@@ -1373,14 +1392,16 @@ window.__ModuleLoader__.load({
|
|
|
1373
1392
|
: null,
|
|
1374
1393
|
revealed ? h(ResultBanner, { attempt: last }) : null,
|
|
1375
1394
|
h('div', { className: 'kd-actions' },
|
|
1376
|
-
!revealed &&
|
|
1395
|
+
!revealed && canSelect
|
|
1377
1396
|
? h(Btn, { variant: 'primary', disabled: busy === 'reveal' || !choice, onClick: () => onReveal('submit') },
|
|
1378
1397
|
busy === 'reveal' ? h('span', { className: 'kd-spinner' }) : null, T.submit)
|
|
1379
1398
|
: null,
|
|
1380
|
-
!revealed && !
|
|
1399
|
+
!revealed && !canSelect
|
|
1381
1400
|
? [
|
|
1382
1401
|
h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
|
|
1383
|
-
|
|
1402
|
+
isChoice
|
|
1403
|
+
? h(Btn, { key: 'skip', disabled: busy === 'reveal', onClick: onSkip }, '跳过本题')
|
|
1404
|
+
: h(Btn, { key: 'giveup', variant: 'danger', disabled: busy === 'reveal', onClick: () => onReveal('giveup') }, T.giveup),
|
|
1384
1405
|
]
|
|
1385
1406
|
: null,
|
|
1386
1407
|
revealed && last?.result === 'pending'
|
|
@@ -1838,8 +1859,13 @@ window.__ModuleLoader__.load({
|
|
|
1838
1859
|
canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
|
|
1839
1860
|
}
|
|
1840
1861
|
|
|
1841
|
-
function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport }) {
|
|
1862
|
+
function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport, committing }) {
|
|
1863
|
+
const [excluded, setExcluded] = useState([])
|
|
1864
|
+
const [previewPage, setPreviewPage] = useState(0)
|
|
1842
1865
|
if (importResult) {
|
|
1866
|
+
const pageSize = 60
|
|
1867
|
+
const pageCount = Math.ceil(importResult.items.length / pageSize)
|
|
1868
|
+
const selectedCount = importResult.usable - excluded.length
|
|
1843
1869
|
return h(Modal, {
|
|
1844
1870
|
title: `${T.preview}(${importResult.name})`,
|
|
1845
1871
|
onClose: onCancelImport,
|
|
@@ -1849,23 +1875,40 @@ window.__ModuleLoader__.load({
|
|
|
1849
1875
|
h(Btn, {
|
|
1850
1876
|
key: 'ok',
|
|
1851
1877
|
variant: 'primary',
|
|
1852
|
-
disabled: !
|
|
1853
|
-
onClick: () => onCommit(importResult),
|
|
1878
|
+
disabled: !selectedCount || committing,
|
|
1879
|
+
onClick: () => onCommit(importResult, excluded),
|
|
1854
1880
|
}, T.commit),
|
|
1855
1881
|
],
|
|
1856
1882
|
},
|
|
1857
1883
|
h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
|
|
1858
|
-
`共识别 ${importResult.stats.total}
|
|
1884
|
+
`共识别 ${importResult.stats.total} 道,可用 ${importResult.usable} 道,已选 ${selectedCount} 道入库。`),
|
|
1859
1885
|
importResult.warnings.length
|
|
1860
1886
|
? h('div', { className: 'kd-verify' },
|
|
1861
1887
|
h('div', null, '解析提示:'),
|
|
1862
1888
|
h('ul', null, importResult.warnings.map((item, index) => h('li', { key: index }, item))))
|
|
1863
1889
|
: null,
|
|
1890
|
+
pageCount > 1
|
|
1891
|
+
? h('div', { className: 'kd-actions' },
|
|
1892
|
+
h(Btn, { small: true, disabled: previewPage === 0, onClick: () => setPreviewPage(previewPage - 1) }, '上一页'),
|
|
1893
|
+
h('span', { className: 'kd-meta' }, `${previewPage + 1} / ${pageCount}`),
|
|
1894
|
+
h(Btn, { small: true, disabled: previewPage + 1 === pageCount, onClick: () => setPreviewPage(previewPage + 1) }, '下一页'))
|
|
1895
|
+
: null,
|
|
1864
1896
|
h('div', { style: { marginTop: '12px' } },
|
|
1865
|
-
importResult.items.slice(
|
|
1897
|
+
importResult.items.slice(previewPage * pageSize, (previewPage + 1) * pageSize).map((item) => h('div', { key: item.index, className: 'kd-preview-item' },
|
|
1866
1898
|
h('div', { className: 'kd-meta' },
|
|
1899
|
+
h('input', {
|
|
1900
|
+
type: 'checkbox',
|
|
1901
|
+
'aria-label': `第 ${item.index} 题入库`,
|
|
1902
|
+
disabled: !item.usable || committing,
|
|
1903
|
+
checked: item.usable && !excluded.includes(item.index),
|
|
1904
|
+
onChange: (event) => {
|
|
1905
|
+
const checked = event.target.checked
|
|
1906
|
+
setExcluded((items) => checked ? items.filter((index) => index !== item.index) : [...items, item.index])
|
|
1907
|
+
},
|
|
1908
|
+
}),
|
|
1867
1909
|
h(Badge, null, `第 ${item.index} 题`),
|
|
1868
1910
|
h(Badge, null, item.typeLabel),
|
|
1911
|
+
item.usable === false ? h(Badge, { tone: 'warn' }, '不入库') : null,
|
|
1869
1912
|
item.answerStatus === 'missing'
|
|
1870
1913
|
? h(Badge, { tone: 'warn' }, T.missing)
|
|
1871
1914
|
: h(Badge, { tone: 'ok' }, '含参考答案(待核验)'),
|
|
@@ -1874,10 +1917,7 @@ window.__ModuleLoader__.load({
|
|
|
1874
1917
|
h(Markdown, { text: (item.stem || '(未识别到题干)').slice(0, 400) })),
|
|
1875
1918
|
item.issues.length
|
|
1876
1919
|
? h('div', { className: 'kd-warn-text', style: { marginTop: '6px' } }, `问题:${item.issues.join(';')}`)
|
|
1877
|
-
: null)))
|
|
1878
|
-
importResult.items.length > 60
|
|
1879
|
-
? h('div', { className: 'kd-msg-meta' }, `仅预览前 60 道,其余 ${importResult.items.length - 60} 道同样会按上述规则入库。`)
|
|
1880
|
-
: null)
|
|
1920
|
+
: null))))
|
|
1881
1921
|
}
|
|
1882
1922
|
|
|
1883
1923
|
return h(Modal, {
|
|
@@ -2079,7 +2119,7 @@ window.__ModuleLoader__.load({
|
|
|
2079
2119
|
if (!previous) return undefined
|
|
2080
2120
|
redoQuestions.current.clear()
|
|
2081
2121
|
const { subjectId: sid, kind: activeKind } = currentView.current
|
|
2082
|
-
if (!sid) return undefined
|
|
2122
|
+
if (!sid || !boot.settings.subjects.includes(sid)) return undefined
|
|
2083
2123
|
if (activeKind === 'today') { loadToday(sid); return undefined }
|
|
2084
2124
|
let cancelled = false
|
|
2085
2125
|
const request=dailyRequest.current
|
|
@@ -2125,8 +2165,9 @@ window.__ModuleLoader__.load({
|
|
|
2125
2165
|
setSubjectId(sid)
|
|
2126
2166
|
setList(null)
|
|
2127
2167
|
rpc('saveSettings', { activeSubject: sid }).catch(() => {})
|
|
2128
|
-
loadToday(sid)
|
|
2129
|
-
|
|
2168
|
+
if (boot.settings.subjects.includes(sid)) loadToday(sid)
|
|
2169
|
+
else loadList(sid, 'all')
|
|
2170
|
+
}, [boot, subjectId, loadToday, loadList])
|
|
2130
2171
|
|
|
2131
2172
|
const openQuestion = useCallback(async (qid) => {
|
|
2132
2173
|
const request = ++dailyRequest.current
|
|
@@ -2168,12 +2209,14 @@ window.__ModuleLoader__.load({
|
|
|
2168
2209
|
if (cancelled) return
|
|
2169
2210
|
setBoot(result)
|
|
2170
2211
|
const preferred = result.settings?.activeSubject
|
|
2171
|
-
const
|
|
2212
|
+
const archived = archivedSubjects(result)
|
|
2213
|
+
const first = preferred && (result.settings.subjects.includes(preferred) || archived.some((subject) => subject.id === preferred))
|
|
2172
2214
|
? preferred
|
|
2173
|
-
: (result.settings.subjects[0] || null)
|
|
2215
|
+
: (result.settings.subjects[0] || archived[0]?.id || null)
|
|
2174
2216
|
setSubjectId(first)
|
|
2175
2217
|
setLoading(false)
|
|
2176
|
-
if (first) loadToday(first)
|
|
2218
|
+
if (first && result.settings.subjects.includes(first)) loadToday(first)
|
|
2219
|
+
else if (first) loadList(first, 'all')
|
|
2177
2220
|
else setView('empty')
|
|
2178
2221
|
}).catch((issue) => {
|
|
2179
2222
|
if (cancelled) return
|
|
@@ -2181,7 +2224,7 @@ window.__ModuleLoader__.load({
|
|
|
2181
2224
|
setLoading(false)
|
|
2182
2225
|
})
|
|
2183
2226
|
return () => { cancelled = true }
|
|
2184
|
-
}, [loadToday])
|
|
2227
|
+
}, [loadToday, loadList])
|
|
2185
2228
|
|
|
2186
2229
|
const currentSubject = useMemo(() => (boot?.subjects || []).find((subject) => subject.id === subjectId) || null,
|
|
2187
2230
|
[boot, subjectId])
|
|
@@ -2189,22 +2232,35 @@ window.__ModuleLoader__.load({
|
|
|
2189
2232
|
const reveal = useCallback(async (mode) => {
|
|
2190
2233
|
if (!question) return
|
|
2191
2234
|
const request = dailyRequest.current
|
|
2235
|
+
const attemptId = attemptRef.current
|
|
2192
2236
|
setBusy('reveal')
|
|
2237
|
+
const showSaved = (saved) => {
|
|
2238
|
+
setQuestion(saved)
|
|
2239
|
+
redoQuestions.current.delete(question.id)
|
|
2240
|
+
setLocalRedo(false)
|
|
2241
|
+
}
|
|
2193
2242
|
try {
|
|
2194
2243
|
const result = await rpc('reveal', {
|
|
2195
2244
|
qid: question.id,
|
|
2196
2245
|
mode,
|
|
2197
2246
|
choice: mode === 'submit' ? choice : '',
|
|
2198
|
-
attemptId
|
|
2247
|
+
attemptId,
|
|
2199
2248
|
})
|
|
2200
|
-
if (request === dailyRequest.current)
|
|
2201
|
-
setQuestion(result.question)
|
|
2202
|
-
redoQuestions.current.delete(question.id)
|
|
2203
|
-
setLocalRedo(false)
|
|
2204
|
-
}
|
|
2249
|
+
if (request === dailyRequest.current) showSaved(result.question)
|
|
2205
2250
|
refreshBoot()
|
|
2206
2251
|
} catch (issue) {
|
|
2207
|
-
|
|
2252
|
+
if (request !== dailyRequest.current) return
|
|
2253
|
+
const saved = await rpc('getQuestion', { qid: question.id, attemptId }).catch(() => null)
|
|
2254
|
+
if (request !== dailyRequest.current) return
|
|
2255
|
+
if (saved?.question?.lastAttempt?.id === attemptId) {
|
|
2256
|
+
showSaved(saved.question)
|
|
2257
|
+
const last = saved.question.lastAttempt
|
|
2258
|
+
const changed = last.mode !== mode || (mode === 'submit' && last.choice !== choice)
|
|
2259
|
+
notify(changed ? '已找回先前保存的作答。本次改选未提交,可点击「重新作答」再提交。' : '已找回本轮保存的作答', changed ? 'warn' : 'ok')
|
|
2260
|
+
refreshBoot()
|
|
2261
|
+
} else {
|
|
2262
|
+
notify(`${issue.message}。未确认本轮是否保存,可再次提交以核对。`, 'warn')
|
|
2263
|
+
}
|
|
2208
2264
|
} finally {
|
|
2209
2265
|
if (request === dailyRequest.current) setBusy('')
|
|
2210
2266
|
}
|
|
@@ -2364,15 +2420,21 @@ window.__ModuleLoader__.load({
|
|
|
2364
2420
|
}, [boot, notify, refreshBoot])
|
|
2365
2421
|
|
|
2366
2422
|
const saveSubjects = async (draft) => {
|
|
2367
|
-
const
|
|
2423
|
+
const keepHistory = boot.counts?.[subjectId]?.all > 0
|
|
2424
|
+
const next = draft.subjects.includes(subjectId) || keepHistory ? subjectId : (draft.subjects[0] || null)
|
|
2368
2425
|
const result = await rpc('saveSettings', { ...draft, activeSubject: next })
|
|
2426
|
+
bootRequest.current++
|
|
2369
2427
|
setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
|
|
2370
2428
|
setModal(null)
|
|
2371
2429
|
notify('科目设置已保存')
|
|
2372
|
-
if (next !== subjectId) {
|
|
2430
|
+
if (next !== subjectId || (kind === 'today' && !result.settings.subjects.includes(next))) {
|
|
2373
2431
|
setSubjectId(next)
|
|
2374
|
-
if (next) loadToday(next)
|
|
2375
|
-
else
|
|
2432
|
+
if (next && result.settings.subjects.includes(next)) loadToday(next)
|
|
2433
|
+
else if (next) loadList(next, 'all')
|
|
2434
|
+
else {
|
|
2435
|
+
dailyRequest.current++
|
|
2436
|
+
setBusy(''); setFail(''); setQuestion(null); setThread(null); setThreadOpen(false); setView('empty')
|
|
2437
|
+
}
|
|
2376
2438
|
}
|
|
2377
2439
|
}
|
|
2378
2440
|
|
|
@@ -2405,10 +2467,10 @@ window.__ModuleLoader__.load({
|
|
|
2405
2467
|
}
|
|
2406
2468
|
}, [subjectId, notify])
|
|
2407
2469
|
|
|
2408
|
-
const commitBank = useCallback(async (result) => {
|
|
2470
|
+
const commitBank = useCallback(async (result, excludedIndexes) => {
|
|
2409
2471
|
setBusy('commit')
|
|
2410
2472
|
try {
|
|
2411
|
-
const saved = await rpc('commitBank', { importId: result.importId, name: result.name, subjectId: subjectId || '' })
|
|
2473
|
+
const saved = await rpc('commitBank', { importId: result.importId, name: result.name, subjectId: subjectId || '', excludedIndexes })
|
|
2412
2474
|
setModal({ kind: 'banks' })
|
|
2413
2475
|
await refreshBoot()
|
|
2414
2476
|
notify(`题库《${saved.bank.name}》已入库,共 ${saved.bank.itemCount} 道题`)
|
|
@@ -2499,7 +2561,7 @@ window.__ModuleLoader__.load({
|
|
|
2499
2561
|
})
|
|
2500
2562
|
}
|
|
2501
2563
|
if (loading) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '正在读取学习数据…')
|
|
2502
|
-
if (!boot?.settings?.subjects?.length) {
|
|
2564
|
+
if (!subjectId && !boot?.settings?.subjects?.length && !archivedSubjects(boot || {}).length) {
|
|
2503
2565
|
return h(EmptyState, {
|
|
2504
2566
|
icon: 'plus',
|
|
2505
2567
|
title: T.noSubjects,
|
|
@@ -2576,6 +2638,7 @@ window.__ModuleLoader__.load({
|
|
|
2576
2638
|
})
|
|
2577
2639
|
},
|
|
2578
2640
|
onReveal: reveal,
|
|
2641
|
+
onSkip: () => kind === 'today' ? loadList(subjectId, 'all') : backToList(),
|
|
2579
2642
|
onJudge: judge,
|
|
2580
2643
|
onFavorite: favorite,
|
|
2581
2644
|
onRedo: () => {
|
|
@@ -2616,7 +2679,9 @@ window.__ModuleLoader__.load({
|
|
|
2616
2679
|
|
|
2617
2680
|
const pickTab = (value) => {
|
|
2618
2681
|
if (value === kind && (value === 'today' || view === 'list')) return
|
|
2619
|
-
if (value === 'today')
|
|
2682
|
+
if (value === 'today') {
|
|
2683
|
+
if (boot?.settings.subjects.includes(subjectId)) loadToday(subjectId)
|
|
2684
|
+
}
|
|
2620
2685
|
else loadList(subjectId, value)
|
|
2621
2686
|
}
|
|
2622
2687
|
const showThread = view === 'detail' && question && threadOpen && !localRedo
|
|
@@ -2627,13 +2692,12 @@ window.__ModuleLoader__.load({
|
|
|
2627
2692
|
h('div', { className: 'kd-header' },
|
|
2628
2693
|
h('div', { className: 'kd-header-top' },
|
|
2629
2694
|
h('div', { className: 'kd-header-left' },
|
|
2630
|
-
h(
|
|
2695
|
+
h(PluginLogo, { size: 40, className: 'kd-brand' }),
|
|
2631
2696
|
h('div', { style: { minWidth: 0 } },
|
|
2632
2697
|
h('h1', { className: 'kd-title' }, T.title),
|
|
2633
2698
|
h('div', { className: 'kd-sub', title: T.subtitle },
|
|
2634
|
-
boot?.today ?
|
|
2635
|
-
|
|
2636
|
-
'每天一道,稳步向前'))),
|
|
2699
|
+
boot?.today ? `今日日期:${formatDate(boot.today)}\u00a0\u00a0` : '',
|
|
2700
|
+
'每日一题,稳步向前'))),
|
|
2637
2701
|
boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
|
|
2638
2702
|
boot ? h(ExamStrip, {
|
|
2639
2703
|
boot,
|
|
@@ -2662,6 +2726,7 @@ window.__ModuleLoader__.load({
|
|
|
2662
2726
|
kind,
|
|
2663
2727
|
counts,
|
|
2664
2728
|
onPick: pickTab,
|
|
2729
|
+
todayDisabled: !boot.settings.subjects.includes(subjectId),
|
|
2665
2730
|
}) : null,
|
|
2666
2731
|
kind !== 'today' && view === 'detail'
|
|
2667
2732
|
? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
|
|
@@ -2699,8 +2764,10 @@ window.__ModuleLoader__.load({
|
|
|
2699
2764
|
: null,
|
|
2700
2765
|
modal?.kind === 'banks' && boot
|
|
2701
2766
|
? h(BanksModal, {
|
|
2767
|
+
key: modal.importResult?.importId || 'banks',
|
|
2702
2768
|
boot,
|
|
2703
2769
|
importResult: modal.importResult,
|
|
2770
|
+
committing: busy === 'commit',
|
|
2704
2771
|
onClose: () => setModal(null),
|
|
2705
2772
|
onPickFile: pickFile,
|
|
2706
2773
|
onDelete: deleteBank,
|
|
@@ -2758,33 +2825,25 @@ window.__ModuleLoader__.load({
|
|
|
2758
2825
|
return matches
|
|
2759
2826
|
}
|
|
2760
2827
|
|
|
2761
|
-
/* ==============================
|
|
2762
|
-
function
|
|
2828
|
+
/* ============================== 插件图标 ============================== */
|
|
2829
|
+
function PluginLogo({ size = 16, className }) {
|
|
2830
|
+
const gradientId = useId()
|
|
2763
2831
|
return h('svg', {
|
|
2764
2832
|
width: size,
|
|
2765
2833
|
height: size,
|
|
2766
|
-
viewBox: '0 0
|
|
2767
|
-
|
|
2834
|
+
viewBox: '0 0 64 64',
|
|
2835
|
+
className,
|
|
2768
2836
|
'aria-hidden': true,
|
|
2769
2837
|
},
|
|
2770
|
-
h('
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
}),
|
|
2775
|
-
h('
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
strokeLinejoin: 'round',
|
|
2780
|
-
}),
|
|
2781
|
-
h('path', {
|
|
2782
|
-
d: 'M8.6 12.6l1.7 1.7 3.5-3.6',
|
|
2783
|
-
stroke: 'currentColor',
|
|
2784
|
-
strokeWidth: 1.8,
|
|
2785
|
-
strokeLinecap: 'round',
|
|
2786
|
-
strokeLinejoin: 'round',
|
|
2787
|
-
}))
|
|
2838
|
+
h('defs', null,
|
|
2839
|
+
h('linearGradient', { id: gradientId, x1: 0, y1: 0, x2: 1, y2: 1 },
|
|
2840
|
+
h('stop', { offset: 0, stopColor: '#5676ff' }),
|
|
2841
|
+
h('stop', { offset: 1, stopColor: '#3b55e8' }))),
|
|
2842
|
+
h('rect', { x: 2, y: 2, width: 60, height: 60, rx: 16, fill: `url(#${gradientId})` }),
|
|
2843
|
+
h('g', { fill: 'none', stroke: '#fff', strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
2844
|
+
h('rect', { x: 15.5, y: 20, width: 33, height: 30, rx: 6.5, strokeWidth: 3.6 }),
|
|
2845
|
+
h('path', { d: 'M23 15.5V23M41 15.5V23', strokeWidth: 3.6 }),
|
|
2846
|
+
h('path', { d: 'm27.4 33.2 5.5-3.9v13.3m-4.4 0h9.2', strokeWidth: 4 })))
|
|
2788
2847
|
}
|
|
2789
2848
|
|
|
2790
2849
|
return {
|
|
@@ -2822,7 +2881,7 @@ window.__ModuleLoader__.load({
|
|
|
2822
2881
|
id: 'daily-question',
|
|
2823
2882
|
order: 20,
|
|
2824
2883
|
label: () => T.title,
|
|
2825
|
-
},
|
|
2884
|
+
}, PluginLogo)),
|
|
2826
2885
|
slots.inject('main', () => slots.register({
|
|
2827
2886
|
name: 'main',
|
|
2828
2887
|
key: 'daily-question',
|
package/cordis.patch.yml
CHANGED
package/icon.svg
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="考研每日一题">
|
|
2
2
|
<defs>
|
|
3
|
-
<linearGradient id="
|
|
4
|
-
<stop offset="0" stop-color="#
|
|
5
|
-
<stop offset="1" stop-color="#
|
|
3
|
+
<linearGradient id="dq-blue" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#5676ff"/>
|
|
5
|
+
<stop offset="1" stop-color="#3b55e8"/>
|
|
6
6
|
</linearGradient>
|
|
7
7
|
</defs>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#dq-blue)"/>
|
|
9
|
+
<g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
+
<rect x="15.5" y="20" width="33" height="30" rx="6.5" stroke-width="3.6"/>
|
|
11
|
+
<path d="M23 15.5V23M41 15.5V23" stroke-width="3.6"/>
|
|
12
|
+
<path d="m27.4 33.2 5.5-3.9v13.3m-4.4 0h9.2" stroke-width="4"/>
|
|
13
|
+
</g>
|
|
11
14
|
</svg>
|
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { registerRoutes } from './lib/routes.js'
|
|
|
6
6
|
*
|
|
7
7
|
* 职责:题库与学习数据的持久化、在线出题 + 独立答案校验、
|
|
8
8
|
* 答案详解的固定版本、按题目隔离的追问线程、考研日期核验,
|
|
9
|
-
*
|
|
9
|
+
* 以及通过宿主 Connection 的鉴权接口向本包的 Client 半提供这些能力。
|
|
10
10
|
*
|
|
11
11
|
* 界面全部由 client.js 以 Slot 组件渲染;这里不提供 HTML 页面。
|
|
12
12
|
*/
|
|
@@ -38,13 +38,13 @@ export function apply(ctx) {
|
|
|
38
38
|
return () => { stopped = true; service.dispose() }
|
|
39
39
|
}, `${PREFIX}.store`)
|
|
40
40
|
|
|
41
|
-
ctx.inject(['
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
logger.warn?.('
|
|
41
|
+
ctx.inject(['connection'], (scoped) => {
|
|
42
|
+
const connection = scoped.connection
|
|
43
|
+
if (typeof connection?.fetch?.register !== 'function') {
|
|
44
|
+
logger.warn?.('宿主 Connection Fetch 不可用,界面接口没有启动')
|
|
45
45
|
return undefined
|
|
46
46
|
}
|
|
47
|
-
return registerRoutes(
|
|
47
|
+
return registerRoutes(connection, service, logger)
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
50
|
|