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/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; width:40px; height:40px; border-radius:12px; display:flex; align-items:center;
572
- justify-content:center; border:0; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface));
573
- color:var(--kd-brand-primary); }
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:650; color:var(--kd-brand-primary); font-variant-numeric:tabular-nums; letter-spacing:.01em; line-height:1.25; }
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-secondary); }
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 { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
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 = (countdown || {}).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
- lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
1084
- h('div', { className: 'kd-cd-target' }, lines.target || ''))
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
- `核验 ${formatShort(cd.verifiedAt)}`)
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
- : (noteText ? h('span', { className: 'kd-strip-item kd-strip-warn', title: cd.note || '' }, noteText) : null))
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 TabBar({ kind, counts, onPick }) {
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
- }, enabled.map((subject) => h('option', { key: subject.id, value: subject.id }, subject.name))),
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 && q.options?.length
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 && isChoice
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 && !isChoice
1399
+ !revealed && !canSelect
1381
1400
  ? [
1382
1401
  h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
1383
- h(Btn, { key: 'giveup', variant: 'danger', disabled: busy === 'reveal', onClick: () => onReveal('giveup') }, T.giveup),
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: !importResult.usable,
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} 道,其中带答案 ${importResult.stats.withAnswer} 道,可用 ${importResult.usable} 道。`),
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(0, 60).map((item) => h('div', { key: item.index, className: 'kd-preview-item' },
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
- }, [subjectId, loadToday])
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 first = preferred && result.settings.subjects.includes(preferred)
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: attemptRef.current,
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
- notify(issue.message, 'bad')
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 next = draft.subjects.includes(subjectId) ? subjectId : (draft.subjects[0] || null)
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 { setQuestion(null); setThread(null); setThreadOpen(false); setView('empty') }
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') loadToday(subjectId)
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('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
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 ? `${formatDate(boot.today)} · ` : '',
2635
- boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
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 SidebarIcon({ size = 16 }) {
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 24 24',
2767
- fill: 'none',
2834
+ viewBox: '0 0 64 64',
2835
+ className,
2768
2836
  'aria-hidden': true,
2769
2837
  },
2770
- h('path', {
2771
- d: 'M4.6 2h14.8A2.6 2.6 0 0 1 22 4.6v14.8A2.6 2.6 0 0 1 19.4 22H4.6A2.6 2.6 0 0 1 2 19.4V4.6A2.6 2.6 0 0 1 4.6 2Z',
2772
- fill: 'currentColor',
2773
- opacity: 0.16,
2774
- }),
2775
- h('path', {
2776
- d: 'M8 5.2c.5 0 .9.4.9.9v.9h6.2v-.9a.9.9 0 0 1 1.8 0v.9h.3c.9 0 1.6.7 1.6 1.6v8.6c0 .9-.7 1.6-1.6 1.6H6.8c-.9 0-1.6-.7-1.6-1.6V8.6c0-.9.7-1.6 1.6-1.6h.3v-.9c0-.5.4-.9.9-.9Z',
2777
- stroke: 'currentColor',
2778
- strokeWidth: 1.6,
2779
- strokeLinejoin: 'round',
2780
- }),
2781
- h('path', {
2782
- d: 'M8.6 12.6l1.7 1.7 3.5-3.6',
2783
- stroke: 'currentColor',
2784
- strokeWidth: 1.8,
2785
- strokeLinecap: 'round',
2786
- strokeLinejoin: 'round',
2787
- }))
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
- }, SidebarIcon)),
2884
+ }, PluginLogo)),
2826
2885
  slots.inject('main', () => slots.register({
2827
2886
  name: 'main',
2828
2887
  key: 'daily-question',
package/cordis.patch.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  # 考研每日一题 bundle patch
2
2
  #
3
3
  # 插入一个宿主插件行:持有题库、答题记录、追问线程与考研日期核验,
4
- # 并通过 ctx.webServer 暴露 /api/daily-question/* 供本包的 Client 半调用。
4
+ # 并通过宿主 Connection 注册鉴权接口供本包的 Client 半调用。
5
5
  - insert:
6
6
  - id: daily-question
7
7
  name: daily-question
package/icon.svg CHANGED
@@ -1,11 +1,14 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" role="img" aria-label="考研每日一题">
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="kdGrad" x1="0" y1="0" x2="1" y2="1">
4
- <stop offset="0" stop-color="#4d6bfe"/>
5
- <stop offset="1" stop-color="#7a4dfe"/>
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
- <path fill="url(#kdGrad)" d="M4.6 2h14.8A2.6 2.6 0 0 1 22 4.6v14.8A2.6 2.6 0 0 1 19.4 22H4.6A2.6 2.6 0 0 1 2 19.4V4.6A2.6 2.6 0 0 1 4.6 2Z"/>
9
- <path fill="#fff" d="M7 5.2c.5 0 .9.4.9.9v.9h8.2v-.9a.9.9 0 0 1 1.8 0v.9h.3c.9 0 1.6.7 1.6 1.6v8.6c0 .9-.7 1.6-1.6 1.6H5.8c-.9 0-1.6-.7-1.6-1.6V8.6c0-.9.7-1.6 1.6-1.6h.3v-.9c0-.5.4-.9.9-.9Zm10.9 4.4H6.1v6.9c0 .1 0 .1.1.1h11.6c.1 0 .1 0 .1-.1V9.6Z"/>
10
- <path fill="#fff" d="M10.3 13.6a.8.8 0 0 1 1.1 0l.9.9 2.1-2.1a.8.8 0 0 1 1.1 1.1l-2.6 2.6a.8.8 0 0 1-1.1 0l-1.5-1.5a.8.8 0 0 1 0-1Z"/>
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
- * 以及把这些能力通过 ctx.webServer 的同源接口暴露给本包的 Client 半。
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(['webServer'], (scoped) => {
42
- const webServer = scoped.webServer
43
- if (!webServer || typeof webServer.register !== 'function') {
44
- logger.warn?.('webServer 不可用,界面接口没有启动')
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(webServer, service, logger)
47
+ return registerRoutes(connection, service, logger)
48
48
  })
49
49
  }
50
50