daily-question 2.0.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 = {
@@ -89,6 +89,7 @@ window.__ModuleLoader__.load({
89
89
  const value = String(method || '')
90
90
  if (!value) return '独立校验'
91
91
  if (value.startsWith('two-step')) return '两步:出题 → 独立视角校验'
92
+ if (value.startsWith('blind-solve')) return '盲解 → 比对 → 分歧主裁'
92
93
  if (value === 'import-parse') return '题库导入解析'
93
94
  if (value === 'online-search') return '官方渠道联网核验'
94
95
  if (value === 'catalog') return '内置官方日期表'
@@ -567,9 +568,7 @@ window.__ModuleLoader__.load({
567
568
  display:flex; flex-direction:column; gap:12px; }
568
569
  .kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
569
570
  .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); }
571
+ .kd-brand { flex:0 0 40px; }
573
572
  .kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
574
573
  .kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
575
574
  .kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
@@ -579,9 +578,10 @@ window.__ModuleLoader__.load({
579
578
  .kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
580
579
  .kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
581
580
  gap:2px; text-align:right; }
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; }
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; }
583
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); }
584
+ .kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-primary); color:light-dark(#000, var(--kd-label-primary)); }
585
585
  .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
586
586
  border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
587
587
  font-size:11.5px; color:var(--kd-label-secondary); }
@@ -591,7 +591,8 @@ window.__ModuleLoader__.load({
591
591
  .kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
592
592
  padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
593
593
  .kd-nav-group { display:flex; flex-direction:column; gap:4px; }
594
- .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);
595
596
  padding:0 8px 4px; }
596
597
  .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
597
598
  width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
@@ -608,6 +609,7 @@ window.__ModuleLoader__.load({
608
609
  .kd-dot { width:6px; height:6px; border-radius:50%; background:var(--kd-state-success-primary); }
609
610
  .kd-dot-idle { background:var(--kd-state-idle-primary); }
610
611
  .kd-nav-spacer { flex:1 1 auto; }
612
+ .kd-update { display:flex; flex-direction:column; gap:4px; }
611
613
  .kd-main { flex:1 1 auto; min-width:0; overflow:hidden; display:flex; flex-direction:column; }
612
614
  .kd-main-toolbar { flex:none; padding:24px 28px 20px; display:flex; align-items:center; gap:12px; }
613
615
  .kd-main-toolbar > .kd-btn { flex:none; align-self:stretch; aspect-ratio:1; padding:0; min-height:0; white-space:nowrap; border-radius:12px; }
@@ -643,6 +645,7 @@ window.__ModuleLoader__.load({
643
645
  border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
644
646
  cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
645
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; }
646
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)); }
647
650
 
648
651
  .kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
@@ -690,7 +693,7 @@ window.__ModuleLoader__.load({
690
693
  background:color-mix(in srgb, var(--kd-state-success-primary) 8%, transparent); }
691
694
  .kd-option.is-wrong { border-color:var(--kd-state-error-primary);
692
695
  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;
696
+ .kd-option-key { flex:0 0 auto; align-self:center; width:24px; height:24px; border-radius:8px; display:inline-flex;
694
697
  align-items:center; justify-content:center; font-size:12px; font-weight:600;
695
698
  border:1px solid var(--kd-border-l2); background:var(--kd-bg-base); }
696
699
  .kd-option.is-selected .kd-option-key { color:var(--kd-brand-primary); border-color:var(--kd-brand-primary); }
@@ -926,10 +929,12 @@ window.__ModuleLoader__.load({
926
929
  try {
927
930
  response = await fetch('/api/daily-question/rpc', {
928
931
  method: 'POST',
932
+ signal: AbortSignal.timeout(op === 'ensureDaily' || op === 'revise' ? 16*60*1000 : op === 'followup' ? 6*60*1000 : 60_000),
929
933
  headers: { 'content-type': 'application/json' },
930
934
  body: JSON.stringify({ op, args: args || {} }),
931
935
  })
932
936
  } catch (error) {
937
+ if (error.name === 'TimeoutError') throw new Error('等待服务响应超时,请重试')
933
938
  throw new Error(`无法连接插件后端(${error.message})。请确认「考研每日一题」插件已启用。`)
934
939
  }
935
940
  let payload = null
@@ -1071,13 +1076,17 @@ window.__ModuleLoader__.load({
1071
1076
  return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
1072
1077
  }
1073
1078
 
1074
- /** 剩余不超过 30 天时省略中间的时长行。 */
1079
+ /** 剩余不超过 30 天时省略按年、月拆分的时长。 */
1075
1080
  function CountdownLines({ countdown }) {
1076
- 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, '$& ')
1077
1085
  return h('div', { className: 'kd-cd-lines' },
1078
- h('div', { className: 'kd-cd-primary' }, lines.primary || ''),
1079
- lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
1080
- 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)))
1081
1090
  }
1082
1091
 
1083
1092
  /** 年份、日期口径与来源:一条紧凑的状态带,横跨整个头部。 */
@@ -1091,9 +1100,6 @@ window.__ModuleLoader__.load({
1091
1100
  const sourceLabel = cd.status === 'official'
1092
1101
  ? `来源 ${hostOf(sourceUrl) || '官方'}`
1093
1102
  : basis ? `依据 ${basis.year} 官方(${hostOf(basis.sourceUrl) || '官方'})` : '无来源记录'
1094
- const noteText = cd.status === 'estimated' && basis
1095
- ? `按 ${basis.year} 官方日期推后一年估算`
1096
- : cd.status === 'unverified' ? '官方日期尚未核实' : (cd.note || '')
1097
1103
 
1098
1104
  return h('div', { className: 'kd-exam-strip' },
1099
1105
  h('label', { className: 'kd-strip-item' },
@@ -1113,7 +1119,7 @@ window.__ModuleLoader__.load({
1113
1119
  : h('span', null, sourceLabel)),
1114
1120
  cd.verifiedAt
1115
1121
  ? h('span', { className: 'kd-strip-item', title: formatDateTime(cd.verifiedAt) },
1116
- `核验 ${formatShort(cd.verifiedAt)}`)
1122
+ `核验成功 ${formatShort(cd.verifiedAt)}`)
1117
1123
  : null,
1118
1124
  h(Btn, {
1119
1125
  small: true,
@@ -1124,39 +1130,83 @@ window.__ModuleLoader__.load({
1124
1130
  }, busy ? h('span', { className: 'kd-spinner' }) : h(Icon, { name: 'refresh', size: 13 }), '核验'),
1125
1131
  cd.expired
1126
1132
  ? h('span', { className: 'kd-strip-item kd-strip-warn' }, '目标日期已过,请重新选择考研年份')
1127
- : (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)
1128
1136
  }
1129
1137
 
1130
1138
  /* ============================== 导航 ============================== */
1139
+ function UpdateCheck({ currentVersion }) {
1140
+ const [state,setState]=useState({busy:true,result:null,error:''})
1141
+ const checking=useRef(false)
1142
+ const active=useRef(true)
1143
+ const check=async(force=false)=>{
1144
+ if(checking.current)return
1145
+ checking.current=true
1146
+ setState((value)=>({...value,busy:true,error:''}))
1147
+ try {
1148
+ const result=await rpc('checkUpdates',{force})
1149
+ if(active.current)setState({busy:false,result,error:''})
1150
+ } catch(error) {
1151
+ if(active.current)setState((value)=>({...value,busy:false,error:error.message}))
1152
+ } finally {checking.current=false}
1153
+ }
1154
+ useEffect(()=>{
1155
+ active.current=true
1156
+ const automatic=()=>{if(!window.document.hidden)check(false)}
1157
+ automatic()
1158
+ const timer=window.setInterval(automatic,6*60*60*1000)
1159
+ window.addEventListener('focus',automatic)
1160
+ return ()=>{active.current=false;window.clearInterval(timer);window.removeEventListener('focus',automatic)}
1161
+ },[])
1162
+ return h('div',{className:'kd-update'},
1163
+ h(Btn,{variant:'ghost',disabled:state.busy,onClick:()=>check(true)},
1164
+ state.busy?h('span',{className:'kd-spinner'}):h(Icon,{name:'refresh'}),state.busy?'正在检查…':'检查更新'),
1165
+ h('div',{className:'kd-msg-meta',style:{padding:'0 8px'}},`当前版本 v${currentVersion||state.result?.currentVersion||'—'}`),
1166
+ h('div',{className:'kd-msg-meta',role:'status','aria-live':'polite',title:state.error||'',style:{padding:'4px 8px',overflowWrap:'anywhere'}},
1167
+ state.error?'暂时无法检查,请重试':state.result?.updateAvailable
1168
+ ? h('a',{className:'kd-link',href:state.result.packageUrl,target:'_blank',rel:'noreferrer noopener'},`发现新版本 v${state.result.latestVersion} ↗`)
1169
+ : state.result?'已是最新版本':null))
1170
+ }
1171
+
1131
1172
  function SubjectNav({ boot, subjectId, onPick, onManage, onBanks }) {
1132
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
+ }
1133
1189
  return h('div', { className: 'kd-nav' },
1134
1190
  h('div', { className: 'kd-nav-group' },
1135
- h('div', { className: 'kd-nav-group-label' }, '科目'),
1136
- enabled.length
1137
- ? enabled.map((subject) => {
1138
- const done = Boolean(boot.todayMap?.[subject.id])
1139
- return h('button', {
1140
- key: subject.id,
1141
- type: 'button',
1142
- className: `kd-nav-item${subject.id === subjectId ? ' is-active' : ''}`,
1143
- onClick: () => onPick(subject.id),
1144
- title: subject.name,
1145
- },
1146
- h('span', null, subject.name),
1147
- h('span', { className: 'kd-nav-status' },
1148
- h('span', { className: `kd-dot${done ? '' : ' kd-dot-idle'}` }),
1149
- done ? '今日已出' : '待出题'))
1150
- })
1191
+ h('div', { className: 'kd-nav-group-label' }, '已启用科目', h('span', null, String(enabled.length))),
1192
+ enabled.length ? enabled.map(subjectButton)
1151
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,
1152
1197
  h('div', { className: 'kd-nav-spacer' }),
1153
1198
  h('div', { className: 'kd-nav-group' },
1154
1199
  h('div', { className: 'kd-nav-group-label' }, '设置'),
1155
1200
  h(Btn, { variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus' }), T.manageSubjects),
1156
- h(Btn, { variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book' }), T.manageBanks)))
1201
+ h(Btn, { variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book' }), T.manageBanks),
1202
+ h(UpdateCheck,{currentVersion:boot.pluginVersion})))
1203
+ }
1204
+
1205
+ function archivedSubjects(boot, subjectId) {
1206
+ return (boot.subjects || []).filter((subject) => !subject.enabled && (boot.counts?.[subject.id]?.all || subject.id === subjectId))
1157
1207
  }
1158
1208
 
1159
- function TabBar({ kind, counts, onPick }) {
1209
+ function TabBar({ kind, counts, onPick, todayDisabled }) {
1160
1210
  const items = [
1161
1211
  ['today', T.tabs.today, null],
1162
1212
  ['all', T.tabs.all, counts?.all],
@@ -1168,19 +1218,24 @@ window.__ModuleLoader__.load({
1168
1218
  key: value,
1169
1219
  type: 'button',
1170
1220
  className: `kd-tab${kind === value ? ' is-active' : ''}`,
1221
+ disabled: value === 'today' && todayDisabled,
1171
1222
  onClick: () => onPick(value),
1172
1223
  }, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
1173
1224
  }
1174
1225
 
1175
1226
  function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
1176
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)
1177
1230
  return h('div', { className: 'kd-mobilebar' },
1178
1231
  h('select', {
1179
1232
  className: 'kd-select',
1180
1233
  value: subjectId || '',
1181
1234
  'aria-label': '切换科目',
1182
1235
  onChange: (event) => onPick(event.target.value),
1183
- }, 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),
1184
1239
  // 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
1185
1240
  h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
1186
1241
  h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
@@ -1273,11 +1328,13 @@ window.__ModuleLoader__.load({
1273
1328
  function QuestionPane({
1274
1329
  q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
1275
1330
  onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
1331
+ onCancel, onSkip,
1276
1332
  }) {
1277
1333
  if (!q) return null
1278
1334
  const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
1279
1335
  const last = q.lastAttempt
1280
1336
  const isChoice = typeof q.isChoice === 'boolean' ? q.isChoice : isChoiceType(q.type)
1337
+ const canSelect = isChoice && (q.canSelect ?? q.options?.length >= 2)
1281
1338
  const answerKey = q.answer?.answerKey || ''
1282
1339
  const selected = revealed ? (last?.choice || '') : choice
1283
1340
  const meta = [q.subjectName, q.typeLabel]
@@ -1286,9 +1343,9 @@ window.__ModuleLoader__.load({
1286
1343
  h('div', { className: 'kd-chips' },
1287
1344
  h('span', { className: 'kd-tag kd-tag-brand' }, q.typeLabel),
1288
1345
  h('span', { className: 'kd-tag' }, q.subjectName),
1289
- h('span', { className: 'kd-tag' }, `难度 ${q.difficulty}/5`),
1346
+ h('span', { className: 'kd-tag', title: q.examAlignment ? '独立评审参考历年真题给出的难度,不是考生正确率统计' : '原始难度标签,尚无真题对照评审' }, `难度 ${q.difficulty}/5`),
1290
1347
  h('span', { className: 'kd-tag' }, formatDate(q.assignedDate)),
1291
- h('span', { className: 'kd-tag' }, `作答 ${q.attemptCount} 次`),
1348
+ h('span', { className: 'kd-tag' }, `累计作答 ${q.attemptCount} 次`),
1292
1349
  h('span', { className: 'kd-tag' }, q.source?.kind === 'bank' ? q.source.label : '在线出题'),
1293
1350
  q.answerStatus === 'verified'
1294
1351
  ? h(Badge, { tone: 'ok' }, T.verified)
@@ -1300,12 +1357,24 @@ window.__ModuleLoader__.load({
1300
1357
  ? h('div', { className: 'kd-meta', style: { marginTop: '10px' } },
1301
1358
  h('span', null, `${T.knowledge}:${q.knowledge.join('、')}`))
1302
1359
  : null,
1360
+ q.examAlignment ? h('details', { className: 'kd-verify' },
1361
+ h('summary', null, `范围与难度依据 · 大纲 ${q.examAlignment.outlineYear} · 独立真题对照`),
1362
+ h('div', null, q.examAlignment.note),
1363
+ h('div', null, `参考考试:${q.examAlignment.basisSubject?.name || q.subjectName} · 检索时间:${formatDateTime(q.examAlignment.checkedAt)}`),
1364
+ ...(q.examAlignment.sources || []).map((source) => h('div', { key: source.id },
1365
+ h('a', { className: 'kd-link', href: source.url, target: '_blank', rel: 'noreferrer noopener' },
1366
+ `${source.year} ${source.title}(${source.official ? '官方来源' : source.kind === 'outline' ? source.verificationLabel || '非官方大纲转载' : '非官方真题转载'})`),
1367
+ source.coverage ? h('div', null, source.coverage) : null,
1368
+ source.publisherUrl ? h('a', { className:'kd-link', href:source.publisherUrl, target:'_blank', rel:'noreferrer noopener' }, `出版社版本记录 · ISBN ${source.isbn}`) : null)),
1369
+ revealed ? h('div', null, q.examAlignment.rationale,
1370
+ ...(q.examAlignment.warnings || []).map((warning,i)=>h('div',{key:i,className:'kd-meta'},warning))) : null) : null,
1303
1371
  h('div', { className: 'kd-stem' }, h(Markdown, { text: q.stem })),
1304
- isChoice && q.options?.length
1372
+ isChoice && !canSelect ? h('div', { className: 'kd-msg-meta' }, '本题选项不完整,可跳过或查看答案后自行判定。') : null,
1373
+ canSelect
1305
1374
  ? h('div', { className: 'kd-options' }, q.options.map((option) => {
1306
1375
  const isSelected = selected.split('').includes(option.key)
1307
1376
  const isRight = revealed && answerKey.split('').includes(option.key)
1308
- const isWrongPick = revealed && isSelected && !isRight
1377
+ const isWrongPick = revealed && answerKey && isSelected && !isRight
1309
1378
  const cls = ['kd-option']
1310
1379
  if (isSelected && !revealed) cls.push('is-selected')
1311
1380
  if (isRight) cls.push('is-correct')
@@ -1323,14 +1392,16 @@ window.__ModuleLoader__.load({
1323
1392
  : null,
1324
1393
  revealed ? h(ResultBanner, { attempt: last }) : null,
1325
1394
  h('div', { className: 'kd-actions' },
1326
- !revealed && isChoice
1395
+ !revealed && canSelect
1327
1396
  ? h(Btn, { variant: 'primary', disabled: busy === 'reveal' || !choice, onClick: () => onReveal('submit') },
1328
1397
  busy === 'reveal' ? h('span', { className: 'kd-spinner' }) : null, T.submit)
1329
1398
  : null,
1330
- !revealed && !isChoice
1399
+ !revealed && !canSelect
1331
1400
  ? [
1332
1401
  h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
1333
- 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),
1334
1405
  ]
1335
1406
  : null,
1336
1407
  revealed && last?.result === 'pending'
@@ -1345,6 +1416,7 @@ window.__ModuleLoader__.load({
1345
1416
  : null,
1346
1417
  revealed ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup) : null),
1347
1418
  revealed ? h(AnswerBlock, { question: q, onRevise, revising, onRemoveWrong }) : null,
1419
+ revising && onCancel ? h(Btn,{variant:'ghost',onClick:onCancel},'取消修订') : null,
1348
1420
  revealed && last?.result === 'pending'
1349
1421
  ? h('div', { className: 'kd-actions', style: { marginTop: '12px' } },
1350
1422
  h('span', { className: 'kd-meta' }, '这道题由你自己判定(系统不会替你把“作答完毕”当成答对):'),
@@ -1416,7 +1488,7 @@ window.__ModuleLoader__.load({
1416
1488
  return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
1417
1489
  }
1418
1490
 
1419
- function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, busy }) {
1491
+ function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, onPage, busy }) {
1420
1492
  if (!list) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '加载中…')
1421
1493
  if (!list.items.length) {
1422
1494
  const icon = kind === 'favorite' ? 'star' : kind === 'wrong' ? 'check' : kind === 'all' ? 'book' : 'spark'
@@ -1428,8 +1500,12 @@ window.__ModuleLoader__.load({
1428
1500
  }
1429
1501
  return h('div', null,
1430
1502
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1431
- h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`),
1503
+ h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.total ?? list.items.length} 道`),
1432
1504
  h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
1505
+ list.totalPages>1?h('div',{className:'kd-row',style:{marginBottom:'12px'},'aria-label':'题目分页'},
1506
+ h(Btn,{small:true,disabled:busy||list.page<=1,onClick:()=>onPage(list.page-1)},'上一页'),
1507
+ h('span',{className:'kd-meta'},`第 ${list.page} / ${list.totalPages} 页`),
1508
+ h(Btn,{small:true,disabled:busy||list.page>=list.totalPages,onClick:()=>onPage(list.page+1)},'下一页')):null,
1433
1509
  managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
1434
1510
  h('div', { className: 'kd-row' },
1435
1511
  h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
@@ -1464,13 +1540,14 @@ window.__ModuleLoader__.load({
1464
1540
  }
1465
1541
 
1466
1542
  /* ============================== 追问侧栏 ============================== */
1467
- function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow, ratio = 33, onRatioChange }) {
1543
+ function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, onCancel, narrow, ratio = 33, onRatioChange }) {
1468
1544
  const [draft, setDraft] = useState('')
1469
1545
  const [sending, setSending] = useState(false)
1470
1546
  const [localError, setLocalError] = useState('')
1471
1547
  const [pending, setPending] = useState(null)
1472
1548
  const scrollRef = useRef(null)
1473
1549
  const sendingRef = useRef(false)
1550
+ const sendingMessage = useRef(null)
1474
1551
  const messages = thread?.messages || []
1475
1552
 
1476
1553
  useEffect(() => {
@@ -1482,6 +1559,7 @@ window.__ModuleLoader__.load({
1482
1559
  const value = String(text || '').trim()
1483
1560
  if (!value || sendingRef.current) return
1484
1561
  sendingRef.current = true
1562
+ sendingMessage.current = messageId
1485
1563
  setSending(true)
1486
1564
  setLocalError('')
1487
1565
  try {
@@ -1493,6 +1571,7 @@ window.__ModuleLoader__.load({
1493
1571
  setPending({ id: messageId, text: value })
1494
1572
  } finally {
1495
1573
  sendingRef.current = false
1574
+ sendingMessage.current = null
1496
1575
  setSending(false)
1497
1576
  }
1498
1577
  }, [onSend])
@@ -1581,6 +1660,7 @@ window.__ModuleLoader__.load({
1581
1660
  },
1582
1661
  }),
1583
1662
  h('div', { className: 'kd-row', style: { justifyContent: 'flex-end', marginTop: '2px' } },
1663
+ sending && onCancel ? h(Btn,{small:true,variant:'ghost',onClick:()=>onCancel(sendingMessage.current)},'取消回复') : null,
1584
1664
  h(Btn, {
1585
1665
  variant: 'primary',
1586
1666
  small: true,
@@ -1599,9 +1679,27 @@ window.__ModuleLoader__.load({
1599
1679
  const [types, setTypes] = useState([])
1600
1680
  const [customTypes, setCustomTypes] = useState([])
1601
1681
  const [typeDraft, setTypeDraft] = useState('')
1682
+ const [referenceSubjectId, setReferenceSubjectId] = useState('auto')
1683
+ const [referenceExamName, setReferenceExamName] = useState('')
1684
+ const [referenceExamCode, setReferenceExamCode] = useState('')
1685
+ const [subjectCode, setSubjectCode] = useState('')
1686
+ const referenceExams = boot.referenceExams || (boot.subjects || []).filter((s) => !s.custom)
1687
+ const referenceEditor = (value, change, label) => h('div', { className:'kd-field', style:{ marginBottom:0 } },
1688
+ h('select', { className:'kd-select', 'aria-label':label, value:value.referenceSubjectId || 'auto',
1689
+ onChange:(event)=>change({referenceSubjectId:event.target.value}) },
1690
+ h('option', {value:'auto'}, '自动检索与核验(按科目名称和代码)'),
1691
+ ...referenceExams.map((exam)=>h('option',{key:exam.id,value:exam.id},`${exam.name}${exam.code ? `(${exam.code})` : ''}`)),
1692
+ h('option',{value:'independent'},'另行指定参考考试名称…')),
1693
+ !value.referenceSubjectId || value.referenceSubjectId === 'auto' ? h('input',{className:'kd-input','aria-label':`${label}代码(考试科目或专业)`,
1694
+ placeholder:'考试科目或专业代码(选填)',value:value.code || '',maxLength:20,onChange:(event)=>change({code:event.target.value})}) : null,
1695
+ value.referenceSubjectId === 'independent' ? h('div',{className:'kd-row'},
1696
+ h('input',{className:'kd-input','aria-label':`${label}名称`,placeholder:'考试名称,院校自命题请包含院校',value:value.referenceExamName || '',maxLength:100,
1697
+ onChange:(event)=>change({referenceExamName:event.target.value})}),
1698
+ h('input',{className:'kd-input','aria-label':`${label}代码`,placeholder:'科目代码(选填)',value:value.referenceExamCode || '',maxLength:20,style:{width:'150px'},
1699
+ onChange:(event)=>change({referenceExamCode:event.target.value})})) : null)
1602
1700
  const enabled = new Set(draft.subjects)
1603
1701
  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,
1702
+ ...item, custom: true, typeLabels: item.types.map((type) => ({ single: '单项选择题', multiple: '多项选择题', fill: '填空题', solution: '解答题' })[type] || type), bankItemCount: 0,
1605
1703
  }))]
1606
1704
  const close = () => { if (!saving) onClose() }
1607
1705
  const toggleSubject = (id) => setDraft((current) => ({ ...current,
@@ -1621,18 +1719,22 @@ window.__ModuleLoader__.load({
1621
1719
  setTypes((current) => current.filter((item) => item !== value))
1622
1720
  }
1623
1721
  const selectedTypes = [...new Set([...types, ...(typeDraft.trim() ? [typeDraft.trim().slice(0, 24)] : [])])]
1624
- const canAdd = Boolean(name.trim()) && selectedTypes.length > 0
1722
+ const canAdd = Boolean(name.trim()) && selectedTypes.length > 0 && (referenceSubjectId !== 'independent' || Boolean(referenceExamName.trim()))
1625
1723
  const withCustomSubject = (current) => {
1626
- const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), types: selectedTypes }
1724
+ const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), code:subjectCode.trim(), types: selectedTypes, referenceSubjectId, referenceExamName:referenceExamName.trim(), referenceExamCode:referenceExamCode.trim() }
1627
1725
  return { ...current, subjects: [...current.subjects, subject.id], customSubjects: [...current.customSubjects, subject] }
1628
1726
  }
1629
1727
  const finish = async () => {
1630
1728
  if (saving) return
1631
- if ((name.trim() || types.length || typeDraft.trim()) && !canAdd) {
1632
- setSaveError('请填写自定义科目名称并至少选择一个题型,或清空未完成的输入。')
1729
+ if ((name.trim() || types.length || typeDraft.trim() || (referenceSubjectId && referenceSubjectId !== 'auto') || referenceExamName.trim() || referenceExamCode.trim() || subjectCode.trim()) && !canAdd) {
1730
+ setSaveError('请填写自定义科目名称并至少选择一个题型;选择独立参考考试时,还需填写考试名称。或清空未完成的输入。')
1633
1731
  return
1634
1732
  }
1635
1733
  const next = canAdd ? withCustomSubject(draft) : draft
1734
+ if (next.customSubjects.some((s)=>s.referenceSubjectId === 'independent' && !s.referenceExamName?.trim())) {
1735
+ setSaveError('独立参考考试请填写准确的考试名称(院校自命题请包含院校名称)。')
1736
+ return
1737
+ }
1636
1738
  setSaving(true)
1637
1739
  setSaveError('')
1638
1740
  try { await onDone(next) }
@@ -1646,6 +1748,10 @@ window.__ModuleLoader__.load({
1646
1748
  setTypes([])
1647
1749
  setCustomTypes([])
1648
1750
  setTypeDraft('')
1751
+ setReferenceSubjectId('auto')
1752
+ setReferenceExamName('')
1753
+ setReferenceExamCode('')
1754
+ setSubjectCode('')
1649
1755
  }
1650
1756
  return h(Modal, {
1651
1757
  title: T.manageSubjects,
@@ -1676,6 +1782,8 @@ window.__ModuleLoader__.load({
1676
1782
  h('div', { className: 'kd-msg-meta' }, subject.typeLabels.join(' / ')))),
1677
1783
  enabled.has(subject.id)
1678
1784
  ? h('div', { className: 'kd-row' },
1785
+ subject.custom ? referenceEditor(draft.customSubjects.find((s)=>s.id === subject.id) || subject,
1786
+ (patch)=>setDraft((current)=>({...current,customSubjects:current.customSubjects.map((s)=>s.id === subject.id ? {...s,...patch} : s)})),`${subject.name} 参考考试科目`) : null,
1679
1787
  h('select', {
1680
1788
  className: 'kd-select',
1681
1789
  value: draft.subjectSource[subject.id] || 'auto',
@@ -1692,6 +1800,13 @@ window.__ModuleLoader__.load({
1692
1800
  h('div', { className: 'kd-divider' }),
1693
1801
  h('div', { className: 'kd-field kd-custom-subject' },
1694
1802
  h('div', { className: 'kd-field-label' }, '添加自定义科目'),
1803
+ referenceEditor({referenceSubjectId,referenceExamName,referenceExamCode,code:subjectCode},(patch)=>{
1804
+ if (patch.referenceSubjectId !== undefined) setReferenceSubjectId(patch.referenceSubjectId)
1805
+ if (patch.referenceExamName !== undefined) setReferenceExamName(patch.referenceExamName)
1806
+ if (patch.referenceExamCode !== undefined) setReferenceExamCode(patch.referenceExamCode)
1807
+ if (patch.code !== undefined) setSubjectCode(patch.code)
1808
+ },'自定义科目的参考考试'),
1809
+ h('div',{className:'kd-meta'},'默认由模型按名称和代码自主检索、独立核验大纲及两年真题。专业代码不等于考试科目代码;信息不足时可在「另行指定参考考试名称」补充院校和准确考试名称,资料不足不会发布。'),
1695
1810
  h('div', { className: 'kd-row' },
1696
1811
  h('input', {
1697
1812
  className: 'kd-input',
@@ -1744,8 +1859,13 @@ window.__ModuleLoader__.load({
1744
1859
  canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
1745
1860
  }
1746
1861
 
1747
- 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)
1748
1865
  if (importResult) {
1866
+ const pageSize = 60
1867
+ const pageCount = Math.ceil(importResult.items.length / pageSize)
1868
+ const selectedCount = importResult.usable - excluded.length
1749
1869
  return h(Modal, {
1750
1870
  title: `${T.preview}(${importResult.name})`,
1751
1871
  onClose: onCancelImport,
@@ -1755,23 +1875,40 @@ window.__ModuleLoader__.load({
1755
1875
  h(Btn, {
1756
1876
  key: 'ok',
1757
1877
  variant: 'primary',
1758
- disabled: !importResult.usable,
1759
- onClick: () => onCommit(importResult),
1878
+ disabled: !selectedCount || committing,
1879
+ onClick: () => onCommit(importResult, excluded),
1760
1880
  }, T.commit),
1761
1881
  ],
1762
1882
  },
1763
1883
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1764
- `共识别 ${importResult.stats.total} 道,其中带答案 ${importResult.stats.withAnswer} 道,可用 ${importResult.usable} 道。`),
1884
+ `共识别 ${importResult.stats.total} 道,可用 ${importResult.usable} 道,已选 ${selectedCount} 道入库。`),
1765
1885
  importResult.warnings.length
1766
1886
  ? h('div', { className: 'kd-verify' },
1767
1887
  h('div', null, '解析提示:'),
1768
1888
  h('ul', null, importResult.warnings.map((item, index) => h('li', { key: index }, item))))
1769
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,
1770
1896
  h('div', { style: { marginTop: '12px' } },
1771
- 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' },
1772
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
+ }),
1773
1909
  h(Badge, null, `第 ${item.index} 题`),
1774
1910
  h(Badge, null, item.typeLabel),
1911
+ item.usable === false ? h(Badge, { tone: 'warn' }, '不入库') : null,
1775
1912
  item.answerStatus === 'missing'
1776
1913
  ? h(Badge, { tone: 'warn' }, T.missing)
1777
1914
  : h(Badge, { tone: 'ok' }, '含参考答案(待核验)'),
@@ -1780,10 +1917,7 @@ window.__ModuleLoader__.load({
1780
1917
  h(Markdown, { text: (item.stem || '(未识别到题干)').slice(0, 400) })),
1781
1918
  item.issues.length
1782
1919
  ? h('div', { className: 'kd-warn-text', style: { marginTop: '6px' } }, `问题:${item.issues.join(';')}`)
1783
- : null))),
1784
- importResult.items.length > 60
1785
- ? h('div', { className: 'kd-msg-meta' }, `仅预览前 60 道,其余 ${importResult.items.length - 60} 道同样会按上述规则入库。`)
1786
- : null)
1920
+ : null))))
1787
1921
  }
1788
1922
 
1789
1923
  return h(Modal, {
@@ -1856,6 +1990,35 @@ window.__ModuleLoader__.load({
1856
1990
  }
1857
1991
 
1858
1992
  /* ============================== 主页面 ============================== */
1993
+ function watchDayChange(snapshot, onSnapshot) {
1994
+ let timer, stopped = false, checking = false
1995
+ const schedule = (retry = false) => {
1996
+ const delay = Date.parse(snapshot.nextDayAt) - Date.parse(snapshot.now)
1997
+ timer = window.setTimeout(check, retry || !Number.isFinite(delay) ? 60_000 : Math.max(1000, delay))
1998
+ }
1999
+ const check = async () => {
2000
+ if (stopped || checking) return
2001
+ checking = true
2002
+ window.clearTimeout(timer)
2003
+ let retry = false
2004
+ try {
2005
+ const fresh = await rpc('bootstrap')
2006
+ if (!stopped) { snapshot = fresh; onSnapshot(fresh) }
2007
+ } catch { retry = true }
2008
+ finally { checking = false; if (!stopped) schedule(retry) }
2009
+ }
2010
+ const onVisible = () => { if (window.document.visibilityState === 'visible') check() }
2011
+ schedule()
2012
+ window.addEventListener('focus', check)
2013
+ window.document.addEventListener('visibilitychange', onVisible)
2014
+ return () => {
2015
+ stopped = true
2016
+ window.clearTimeout(timer)
2017
+ window.removeEventListener('focus', check)
2018
+ window.document.removeEventListener('visibilitychange', onVisible)
2019
+ }
2020
+ }
2021
+
1859
2022
  function Page() {
1860
2023
  const [boot, setBoot] = useState(null)
1861
2024
  const [loading, setLoading] = useState(true)
@@ -1883,6 +2046,13 @@ window.__ModuleLoader__.load({
1883
2046
  const mainScroll = useRef(null)
1884
2047
  const listScroll = useRef(0)
1885
2048
  const redoQuestions = useRef(new Set())
2049
+ const dailyRequest = useRef(0)
2050
+ const lastDay = useRef(null)
2051
+ const currentView = useRef(null)
2052
+ const bootRequest = useRef(0)
2053
+ const threadRequest = useRef(0)
2054
+ const selectionRequest = useRef(0)
2055
+ currentView.current = { subjectId, kind, view, page:list?.page||1, loading:busy==='list' }
1886
2056
 
1887
2057
  useEffect(() => {
1888
2058
  if (view === 'list' && list && mainScroll.current) mainScroll.current.scrollTop = listScroll.current
@@ -1898,9 +2068,10 @@ window.__ModuleLoader__.load({
1898
2068
  useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current) }, [])
1899
2069
 
1900
2070
  const refreshBoot = useCallback(async () => {
2071
+ const request = ++bootRequest.current
1901
2072
  try {
1902
2073
  const result = await rpc('bootstrap')
1903
- setBoot(result)
2074
+ if (request === bootRequest.current) setBoot(result)
1904
2075
  return result
1905
2076
  } catch {
1906
2077
  return null
@@ -1909,6 +2080,7 @@ window.__ModuleLoader__.load({
1909
2080
 
1910
2081
  const loadToday = useCallback(async (sid) => {
1911
2082
  if (!sid) return
2083
+ const request = ++dailyRequest.current
1912
2084
  // 高亮必须跟着视图走:从「全部 / 收藏 / 错题本」切回今日题目时要一起复位
1913
2085
  setKind('today')
1914
2086
  setThreadOpen(false)
@@ -1922,31 +2094,67 @@ window.__ModuleLoader__.load({
1922
2094
  attemptRef.current = newAttemptId()
1923
2095
  try {
1924
2096
  const result = await rpc('ensureDaily', { subjectId: sid })
1925
- setQuestion(result.question)
1926
- setLocalRedo(redoQuestions.current.has(result.question.id))
1927
- setThread(result.thread || null)
2097
+ if (request === dailyRequest.current) {
2098
+ setQuestion(result.question)
2099
+ setLocalRedo(redoQuestions.current.has(result.question.id))
2100
+ setThread(result.thread || null)
2101
+ }
2102
+ await refreshBoot()
1928
2103
  } catch (issue) {
1929
- setFail(issue.message)
2104
+ if (request === dailyRequest.current) setFail(issue.message)
1930
2105
  } finally {
1931
- setBusy('')
2106
+ if (request === dailyRequest.current) setBusy('')
1932
2107
  }
1933
- }, [])
2108
+ }, [refreshBoot])
2109
+
2110
+ useEffect(() => {
2111
+ if (!boot?.today) return undefined
2112
+ return watchDayChange(boot, setBoot)
2113
+ }, [boot?.today])
1934
2114
 
1935
- const loadList = useCallback(async (sid, nextKind) => {
2115
+ useEffect(() => {
2116
+ if (!boot?.today || lastDay.current === boot.today) return undefined
2117
+ const previous = lastDay.current
2118
+ lastDay.current = boot.today
2119
+ if (!previous) return undefined
2120
+ redoQuestions.current.clear()
2121
+ const { subjectId: sid, kind: activeKind } = currentView.current
2122
+ if (!sid || !boot.settings.subjects.includes(sid)) return undefined
2123
+ if (activeKind === 'today') { loadToday(sid); return undefined }
2124
+ let cancelled = false
2125
+ const request=dailyRequest.current
2126
+ rpc('ensureDaily', { subjectId: sid }).then(async () => {
2127
+ if (cancelled) return
2128
+ await refreshBoot()
2129
+ const current = currentView.current
2130
+ if (cancelled || request!==dailyRequest.current || current.loading || current.subjectId !== sid || current.kind === 'today' || current.view !== 'list') return
2131
+ const result = await rpc('listQuestions', { subjectId: sid, kind: current.kind, page:current.page })
2132
+ const latest = currentView.current
2133
+ if (!cancelled && request===dailyRequest.current && !latest.loading && latest.page===current.page && latest.subjectId === sid && latest.kind === current.kind && latest.view === 'list') {
2134
+ listScroll.current = mainScroll.current?.scrollTop || 0
2135
+ setList(result)
2136
+ }
2137
+ }).catch((issue) => { if (!cancelled) notify(`今日题目更新失败:${issue.message}`, 'warn') })
2138
+ return () => { cancelled = true }
2139
+ }, [boot?.today, loadToday, refreshBoot, notify])
2140
+
2141
+ const loadList = useCallback(async (sid, nextKind, page = 1) => {
1936
2142
  if (!sid) return
2143
+ const request = ++dailyRequest.current
1937
2144
  listScroll.current = 0
1938
2145
  setKind(nextKind)
1939
2146
  setThreadOpen(false)
1940
2147
  setView('list')
2148
+ setFail('')
1941
2149
  setBusy('list')
1942
2150
  setList(null)
1943
2151
  try {
1944
- const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind })
1945
- setList(result)
2152
+ const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind, page })
2153
+ if (request === dailyRequest.current) setList(result)
1946
2154
  } catch (issue) {
1947
- notify(issue.message, 'bad')
2155
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1948
2156
  } finally {
1949
- setBusy('')
2157
+ if (request === dailyRequest.current) setBusy('')
1950
2158
  }
1951
2159
  }, [notify])
1952
2160
 
@@ -1957,14 +2165,17 @@ window.__ModuleLoader__.load({
1957
2165
  setSubjectId(sid)
1958
2166
  setList(null)
1959
2167
  rpc('saveSettings', { activeSubject: sid }).catch(() => {})
1960
- loadToday(sid)
1961
- }, [subjectId, loadToday])
2168
+ if (boot.settings.subjects.includes(sid)) loadToday(sid)
2169
+ else loadList(sid, 'all')
2170
+ }, [boot, subjectId, loadToday, loadList])
1962
2171
 
1963
2172
  const openQuestion = useCallback(async (qid) => {
2173
+ const request = ++dailyRequest.current
1964
2174
  listScroll.current = mainScroll.current?.scrollTop || 0
1965
2175
  setBusy('question')
1966
2176
  try {
1967
2177
  const result = await rpc('getQuestion', { qid })
2178
+ if (request !== dailyRequest.current) return
1968
2179
  const canResume = result.isToday && result.question.answerState === 'revealed'
1969
2180
  && result.question.attemptCount > 0 && !redoQuestions.current.has(qid)
1970
2181
  if (!canResume) redoQuestions.current.add(qid)
@@ -1976,19 +2187,20 @@ window.__ModuleLoader__.load({
1976
2187
  setThreadOpen(false)
1977
2188
  attemptRef.current = newAttemptId()
1978
2189
  } catch (issue) {
1979
- notify(issue.message, 'bad')
2190
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
1980
2191
  } finally {
1981
- setBusy('')
2192
+ if (request === dailyRequest.current) setBusy('')
1982
2193
  }
1983
2194
  }, [notify])
1984
2195
 
1985
2196
  const backToList = async () => {
2197
+ const request = ++dailyRequest.current
1986
2198
  setThreadOpen(false)
1987
2199
  setView('list')
1988
2200
  try {
1989
- const result = await rpc('listQuestions', { subjectId, kind })
1990
- setList(result)
1991
- } catch (issue) { notify(issue.message, 'bad') }
2201
+ const result = await rpc('listQuestions', { subjectId, kind, page:list?.page||1 })
2202
+ if (request === dailyRequest.current) setList(result)
2203
+ } catch (issue) { if (request === dailyRequest.current) notify(issue.message, 'bad') }
1992
2204
  }
1993
2205
 
1994
2206
  useEffect(() => {
@@ -1997,12 +2209,14 @@ window.__ModuleLoader__.load({
1997
2209
  if (cancelled) return
1998
2210
  setBoot(result)
1999
2211
  const preferred = result.settings?.activeSubject
2000
- 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))
2001
2214
  ? preferred
2002
- : (result.settings.subjects[0] || null)
2215
+ : (result.settings.subjects[0] || archived[0]?.id || null)
2003
2216
  setSubjectId(first)
2004
2217
  setLoading(false)
2005
- if (first) loadToday(first)
2218
+ if (first && result.settings.subjects.includes(first)) loadToday(first)
2219
+ else if (first) loadList(first, 'all')
2006
2220
  else setView('empty')
2007
2221
  }).catch((issue) => {
2008
2222
  if (cancelled) return
@@ -2010,64 +2224,87 @@ window.__ModuleLoader__.load({
2010
2224
  setLoading(false)
2011
2225
  })
2012
2226
  return () => { cancelled = true }
2013
- }, [loadToday])
2227
+ }, [loadToday, loadList])
2014
2228
 
2015
2229
  const currentSubject = useMemo(() => (boot?.subjects || []).find((subject) => subject.id === subjectId) || null,
2016
2230
  [boot, subjectId])
2017
2231
 
2018
2232
  const reveal = useCallback(async (mode) => {
2019
2233
  if (!question) return
2234
+ const request = dailyRequest.current
2235
+ const attemptId = attemptRef.current
2020
2236
  setBusy('reveal')
2237
+ const showSaved = (saved) => {
2238
+ setQuestion(saved)
2239
+ redoQuestions.current.delete(question.id)
2240
+ setLocalRedo(false)
2241
+ }
2021
2242
  try {
2022
2243
  const result = await rpc('reveal', {
2023
2244
  qid: question.id,
2024
2245
  mode,
2025
2246
  choice: mode === 'submit' ? choice : '',
2026
- attemptId: attemptRef.current,
2247
+ attemptId,
2027
2248
  })
2028
- setQuestion(result.question)
2029
- redoQuestions.current.delete(question.id)
2030
- setLocalRedo(false)
2249
+ if (request === dailyRequest.current) showSaved(result.question)
2031
2250
  refreshBoot()
2032
2251
  } catch (issue) {
2033
- 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
+ }
2034
2264
  } finally {
2035
- setBusy('')
2265
+ if (request === dailyRequest.current) setBusy('')
2036
2266
  }
2037
2267
  }, [question, choice, notify, refreshBoot])
2038
2268
 
2039
2269
  const judge = useCallback(async (verdict) => {
2040
2270
  if (!question) return
2271
+ if (!question.lastAttempt?.id) { notify('缺少本次作答记录,请重新打开题目后判定','bad'); return }
2272
+ const request = dailyRequest.current
2041
2273
  setBusy('judge')
2042
2274
  try {
2043
- const result = await rpc('judge', { qid: question.id, verdict })
2044
- setQuestion(result.question)
2275
+ const result = await rpc('judge', { qid: question.id, verdict, attemptId:question.lastAttempt.id })
2276
+ if (request === dailyRequest.current) setQuestion(result.question)
2045
2277
  refreshBoot()
2046
2278
  } catch (issue) {
2047
- notify(issue.message, 'bad')
2279
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2048
2280
  } finally {
2049
- setBusy('')
2281
+ if (request === dailyRequest.current) setBusy('')
2050
2282
  }
2051
2283
  }, [question, notify, refreshBoot])
2052
2284
 
2053
2285
  const favorite = useCallback(async () => {
2054
2286
  if (!question) return
2287
+ const request = dailyRequest.current
2055
2288
  setBusy('favorite')
2056
2289
  try {
2057
2290
  const result = await rpc('favorite', { qid: question.id, value: !question.favorite })
2058
- setQuestion(result.question)
2059
- notify(question.favorite ? '已取消收藏' : '已加入收藏')
2291
+ if (request === dailyRequest.current) {
2292
+ setQuestion(result.question)
2293
+ notify(question.favorite ? '已取消收藏' : '已加入收藏')
2294
+ }
2060
2295
  refreshBoot()
2061
2296
  } catch (issue) {
2062
- notify(issue.message, 'bad')
2297
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2063
2298
  } finally {
2064
- setBusy('')
2299
+ if (request === dailyRequest.current) setBusy('')
2065
2300
  }
2066
2301
  }, [question, notify, refreshBoot])
2067
2302
 
2068
2303
  const sendFollowup = useCallback(async (text, messageId) => { if (!question) throw new Error('没有打开任何题目')
2304
+ const request = dailyRequest.current
2305
+ const threadVersion = ++threadRequest.current
2069
2306
  const result = await rpc('followup', { qid: question.id, message: text, clientMessageId: messageId })
2070
- setThread(result.thread)
2307
+ if (request === dailyRequest.current && threadVersion === threadRequest.current) setThread(result.thread)
2071
2308
  }, [question])
2072
2309
 
2073
2310
  const clearThread = useCallback(async () => {
@@ -2079,11 +2316,15 @@ window.__ModuleLoader__.load({
2079
2316
  confirmLabel: '清空',
2080
2317
  danger: true,
2081
2318
  onConfirm: async () => {
2319
+ const request = dailyRequest.current
2320
+ const threadVersion = ++threadRequest.current
2082
2321
  setModal(null)
2083
2322
  try {
2084
2323
  const result = await rpc('clearThread', { qid: question.id })
2085
- setThread(result.thread)
2086
- notify('本题追问已清空')
2324
+ if (request === dailyRequest.current && threadVersion === threadRequest.current) {
2325
+ setThread(result.thread)
2326
+ notify('本题追问已清空')
2327
+ }
2087
2328
  } catch (issue) {
2088
2329
  notify(issue.message, 'bad')
2089
2330
  }
@@ -2091,47 +2332,58 @@ window.__ModuleLoader__.load({
2091
2332
  })
2092
2333
  }, [question, notify])
2093
2334
 
2335
+ const cancelWork = async (kind, qid, clientMessageId) => {
2336
+ try { await rpc('cancelTask',{kind,subjectId,qid,clientMessageId}) }
2337
+ catch (issue) { notify(issue.message,'bad') }
2338
+ }
2339
+
2094
2340
  const revise = useCallback(async () => {
2095
2341
  if (!question) return
2342
+ const request = dailyRequest.current
2096
2343
  setBusy('revise')
2097
2344
  try {
2098
2345
  const result = await rpc('revise', { qid: question.id })
2346
+ if (request !== dailyRequest.current) return
2099
2347
  setQuestion(result.question)
2100
2348
  setModal({ kind: 'revision' })
2101
2349
  } catch (issue) {
2102
- notify(issue.message, 'bad')
2350
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2103
2351
  } finally {
2104
- setBusy('')
2352
+ if (request === dailyRequest.current) setBusy('')
2105
2353
  }
2106
2354
  }, [question, notify])
2107
2355
 
2108
2356
  const confirmRevision = useCallback(async () => {
2109
2357
  if (!question?.pendingRevision) return
2358
+ const request = dailyRequest.current
2110
2359
  setBusy('revise')
2111
2360
  try {
2112
2361
  const result = await rpc('confirmRevision', {
2113
2362
  qid: question.id,
2114
2363
  candidateId: question.pendingRevision.candidateId,
2115
2364
  })
2365
+ if (request !== dailyRequest.current) return
2116
2366
  setQuestion(result.question)
2117
2367
  setModal(null)
2118
2368
  notify('已替换为通过校验的新版本,旧版本已保留')
2119
2369
  } catch (issue) {
2120
- notify(issue.message, 'bad')
2370
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2121
2371
  } finally {
2122
- setBusy('')
2372
+ if (request === dailyRequest.current) setBusy('')
2123
2373
  }
2124
2374
  }, [question, notify])
2125
2375
 
2126
2376
  const discardRevision = useCallback(async () => {
2127
2377
  if (!question) return
2378
+ const request = dailyRequest.current
2128
2379
  setModal(null)
2129
2380
  try {
2130
2381
  const result = await rpc('discardRevision', { qid: question.id })
2382
+ if (request !== dailyRequest.current) return
2131
2383
  setQuestion(result.question)
2132
2384
  notify('已放弃候选版本,正式答案保持不变')
2133
2385
  } catch (issue) {
2134
- notify(issue.message, 'bad')
2386
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2135
2387
  }
2136
2388
  }, [question, notify])
2137
2389
 
@@ -2168,15 +2420,21 @@ window.__ModuleLoader__.load({
2168
2420
  }, [boot, notify, refreshBoot])
2169
2421
 
2170
2422
  const saveSubjects = async (draft) => {
2171
- 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)
2172
2425
  const result = await rpc('saveSettings', { ...draft, activeSubject: next })
2426
+ bootRequest.current++
2173
2427
  setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
2174
2428
  setModal(null)
2175
2429
  notify('科目设置已保存')
2176
- if (next !== subjectId) {
2430
+ if (next !== subjectId || (kind === 'today' && !result.settings.subjects.includes(next))) {
2177
2431
  setSubjectId(next)
2178
- if (next) loadToday(next)
2179
- 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
+ }
2180
2438
  }
2181
2439
  }
2182
2440
 
@@ -2209,10 +2467,10 @@ window.__ModuleLoader__.load({
2209
2467
  }
2210
2468
  }, [subjectId, notify])
2211
2469
 
2212
- const commitBank = useCallback(async (result) => {
2470
+ const commitBank = useCallback(async (result, excludedIndexes) => {
2213
2471
  setBusy('commit')
2214
2472
  try {
2215
- 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 })
2216
2474
  setModal({ kind: 'banks' })
2217
2475
  await refreshBoot()
2218
2476
  notify(`题库《${saved.bank.name}》已入库,共 ${saved.bank.itemCount} 道题`)
@@ -2245,13 +2503,15 @@ window.__ModuleLoader__.load({
2245
2503
 
2246
2504
  const bulkQuestions = async (action) => {
2247
2505
  if (bulkLock.current || !selected.length) return
2506
+ const request = dailyRequest.current
2248
2507
  const qids = [...selected]
2249
2508
  const execute = async (confirmProtected = false) => {
2250
- if (bulkLock.current) return
2509
+ if (bulkLock.current || request !== dailyRequest.current) return
2251
2510
  bulkLock.current = true
2252
2511
  setBusy('bulk')
2253
2512
  try {
2254
2513
  const result = await rpc('bulkQuestions', { qids, action, confirmProtected })
2514
+ if (request !== dailyRequest.current) { refreshBoot(); return }
2255
2515
  if (result.requiresConfirmation) {
2256
2516
  setModal({ kind: 'confirm', title: '再次确认删除收藏或错题', danger: true,
2257
2517
  message: `选中的题目中有 ${result.protectedCount} 道已收藏或在错题本中。继续将永久删除所选 ${qids.length} 道题及其作答和追问,无法恢复。确定仍要删除吗?`,
@@ -2260,13 +2520,14 @@ window.__ModuleLoader__.load({
2260
2520
  }
2261
2521
  setModal(null)
2262
2522
  setSelected([])
2263
- await loadList(subjectId, kind)
2523
+ const loading = loadList(subjectId, kind, list?.page||1)
2524
+ const listRequest = dailyRequest.current
2525
+ await loading
2264
2526
  await refreshBoot()
2265
- notify(`已处理 ${result.affected} 道题目`)
2527
+ if (listRequest === dailyRequest.current) notify(`已处理 ${result.affected} 道题目`)
2266
2528
  } catch (issue) {
2267
- setModal(null)
2268
- notify(issue.message, 'bad')
2269
- } finally { bulkLock.current = false; setBusy('') }
2529
+ if (request === dailyRequest.current) { setModal(null); notify(issue.message, 'bad') }
2530
+ } finally { bulkLock.current = false; if (request === dailyRequest.current) setBusy('') }
2270
2531
  }
2271
2532
  if (action === 'delete') {
2272
2533
  setModal({ kind: 'confirm', title: '删除所选题目', danger: true,
@@ -2276,6 +2537,18 @@ window.__ModuleLoader__.load({
2276
2537
  }
2277
2538
 
2278
2539
  const counts = subjectId ? boot?.counts?.[subjectId] : null
2540
+ const selectAllQuestions = async (value) => {
2541
+ if(bulkLock.current)return
2542
+ const request=dailyRequest.current,selection=++selectionRequest.current
2543
+ const isCurrent=()=>request===dailyRequest.current&&selection===selectionRequest.current
2544
+ if(!value){setSelected([]);setBusy(current=>current==='selection'?'':current);return}
2545
+ setBusy('selection')
2546
+ try {
2547
+ const result=await rpc('listQuestions',{subjectId,kind,idsOnly:true})
2548
+ if(isCurrent())setSelected(result.ids)
2549
+ } catch(issue){if(isCurrent())notify(issue.message,'bad')}
2550
+ finally{if(isCurrent())setBusy(current=>current==='selection'?'':current)}
2551
+ }
2279
2552
  const narrow = useMediaQuery('(max-width: 1180px)')
2280
2553
 
2281
2554
  const mainContent = (() => {
@@ -2288,7 +2561,7 @@ window.__ModuleLoader__.load({
2288
2561
  })
2289
2562
  }
2290
2563
  if (loading) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '正在读取学习数据…')
2291
- if (!boot?.settings?.subjects?.length) {
2564
+ if (!subjectId && !boot?.settings?.subjects?.length && !archivedSubjects(boot || {}).length) {
2292
2565
  return h(EmptyState, {
2293
2566
  icon: 'plus',
2294
2567
  title: T.noSubjects,
@@ -2308,7 +2581,8 @@ window.__ModuleLoader__.load({
2308
2581
  h('span', { className: 'kd-spinner' }),
2309
2582
  T.generating,
2310
2583
  h('div', { className: 'kd-msg-meta', style: { marginTop: '8px' } },
2311
- '题目会先生成,再由另一个独立视角重新求解并校验;校验不通过就不会发布。首次出题可能需要几十秒。'))
2584
+ '先出题,再仅凭题干和选项盲解并比对答案;分歧交由主裁,仍无法确定就重新出题或阻止发布。首次出题可能需要较长时间。'),
2585
+ h(Btn,{variant:'ghost',onClick:()=>cancelWork('daily')},'取消生成'))
2312
2586
  }
2313
2587
  if (fail) {
2314
2588
  return h(EmptyState, {
@@ -2339,10 +2613,11 @@ window.__ModuleLoader__.load({
2339
2613
  kind,
2340
2614
  subjectName: currentSubject?.name || '',
2341
2615
  onOpen: openQuestion,
2342
- managing, selected, busy: busy === 'bulk',
2616
+ managing, selected, busy: busy === 'bulk' || busy === 'selection',
2343
2617
  onManage: () => { setManaging(!managing); setSelected([]) },
2344
2618
  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) : []),
2619
+ onSelectAll: selectAllQuestions,
2620
+ onPage: (page) => loadList(subjectId,kind,page),
2346
2621
  onBulk: bulkQuestions,
2347
2622
  })
2348
2623
  }
@@ -2363,9 +2638,12 @@ window.__ModuleLoader__.load({
2363
2638
  })
2364
2639
  },
2365
2640
  onReveal: reveal,
2641
+ onSkip: () => kind === 'today' ? loadList(subjectId, 'all') : backToList(),
2366
2642
  onJudge: judge,
2367
2643
  onFavorite: favorite,
2368
2644
  onRedo: () => {
2645
+ dailyRequest.current++
2646
+ setBusy('')
2369
2647
  redoQuestions.current.add(question.id)
2370
2648
  setLocalRedo(true)
2371
2649
  setThreadOpen(false)
@@ -2373,17 +2651,21 @@ window.__ModuleLoader__.load({
2373
2651
  attemptRef.current = newAttemptId()
2374
2652
  },
2375
2653
  onRevise: revise,
2654
+ onCancel: () => cancelWork('revise',question.id),
2376
2655
  revising: busy === 'revise',
2377
2656
  onToggleThread: () => setThreadOpen((value) => !value),
2378
2657
  onRemoveWrong: async () => {
2379
2658
  if (!question) return
2659
+ const request = dailyRequest.current
2380
2660
  try {
2381
2661
  const result = await rpc('removeFromWrongBook', { qid: question.id })
2382
- setQuestion(result.question)
2383
- notify('已移出错题本,历史作答记录仍然保留')
2662
+ if (request === dailyRequest.current) {
2663
+ setQuestion(result.question)
2664
+ notify('已移出错题本,历史作答记录仍然保留')
2665
+ }
2384
2666
  refreshBoot()
2385
2667
  } catch (issue) {
2386
- notify(issue.message, 'bad')
2668
+ if (request === dailyRequest.current) notify(issue.message, 'bad')
2387
2669
  }
2388
2670
  },
2389
2671
  })
@@ -2397,7 +2679,9 @@ window.__ModuleLoader__.load({
2397
2679
 
2398
2680
  const pickTab = (value) => {
2399
2681
  if (value === kind && (value === 'today' || view === 'list')) return
2400
- if (value === 'today') loadToday(subjectId)
2682
+ if (value === 'today') {
2683
+ if (boot?.settings.subjects.includes(subjectId)) loadToday(subjectId)
2684
+ }
2401
2685
  else loadList(subjectId, value)
2402
2686
  }
2403
2687
  const showThread = view === 'detail' && question && threadOpen && !localRedo
@@ -2408,13 +2692,12 @@ window.__ModuleLoader__.load({
2408
2692
  h('div', { className: 'kd-header' },
2409
2693
  h('div', { className: 'kd-header-top' },
2410
2694
  h('div', { className: 'kd-header-left' },
2411
- h('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
2695
+ h(PluginLogo, { size: 40, className: 'kd-brand' }),
2412
2696
  h('div', { style: { minWidth: 0 } },
2413
2697
  h('h1', { className: 'kd-title' }, T.title),
2414
2698
  h('div', { className: 'kd-sub', title: T.subtitle },
2415
- boot?.today ? `${formatDate(boot.today)} · ` : '',
2416
- boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
2417
- '每天一道,稳步向前'))),
2699
+ boot?.today ? `今日日期:${formatDate(boot.today)}\u00a0\u00a0` : '',
2700
+ '每日一题,稳步向前'))),
2418
2701
  boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
2419
2702
  boot ? h(ExamStrip, {
2420
2703
  boot,
@@ -2443,6 +2726,7 @@ window.__ModuleLoader__.load({
2443
2726
  kind,
2444
2727
  counts,
2445
2728
  onPick: pickTab,
2729
+ todayDisabled: !boot.settings.subjects.includes(subjectId),
2446
2730
  }) : null,
2447
2731
  kind !== 'today' && view === 'detail'
2448
2732
  ? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
@@ -2459,6 +2743,7 @@ window.__ModuleLoader__.load({
2459
2743
  onRatioChange: setThreadRatio,
2460
2744
  onSend: sendFollowup,
2461
2745
  onClear: clearThread,
2746
+ onCancel: (messageId) => cancelWork('followup',question.id,messageId),
2462
2747
  onClose: () => setThreadOpen(false),
2463
2748
  })
2464
2749
  : null)),
@@ -2479,8 +2764,10 @@ window.__ModuleLoader__.load({
2479
2764
  : null,
2480
2765
  modal?.kind === 'banks' && boot
2481
2766
  ? h(BanksModal, {
2767
+ key: modal.importResult?.importId || 'banks',
2482
2768
  boot,
2483
2769
  importResult: modal.importResult,
2770
+ committing: busy === 'commit',
2484
2771
  onClose: () => setModal(null),
2485
2772
  onPickFile: pickFile,
2486
2773
  onDelete: deleteBank,
@@ -2538,33 +2825,25 @@ window.__ModuleLoader__.load({
2538
2825
  return matches
2539
2826
  }
2540
2827
 
2541
- /* ============================== 侧栏入口 ============================== */
2542
- function SidebarIcon({ size = 16 }) {
2828
+ /* ============================== 插件图标 ============================== */
2829
+ function PluginLogo({ size = 16, className }) {
2830
+ const gradientId = useId()
2543
2831
  return h('svg', {
2544
2832
  width: size,
2545
2833
  height: size,
2546
- viewBox: '0 0 24 24',
2547
- fill: 'none',
2834
+ viewBox: '0 0 64 64',
2835
+ className,
2548
2836
  'aria-hidden': true,
2549
2837
  },
2550
- h('path', {
2551
- 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',
2552
- fill: 'currentColor',
2553
- opacity: 0.16,
2554
- }),
2555
- h('path', {
2556
- 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',
2557
- stroke: 'currentColor',
2558
- strokeWidth: 1.6,
2559
- strokeLinejoin: 'round',
2560
- }),
2561
- h('path', {
2562
- d: 'M8.6 12.6l1.7 1.7 3.5-3.6',
2563
- stroke: 'currentColor',
2564
- strokeWidth: 1.8,
2565
- strokeLinecap: 'round',
2566
- strokeLinejoin: 'round',
2567
- }))
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 })))
2568
2847
  }
2569
2848
 
2570
2849
  return {
@@ -2602,7 +2881,7 @@ window.__ModuleLoader__.load({
2602
2881
  id: 'daily-question',
2603
2882
  order: 20,
2604
2883
  label: () => T.title,
2605
- }, SidebarIcon)),
2884
+ }, PluginLogo)),
2606
2885
  slots.inject('main', () => slots.register({
2607
2886
  name: 'main',
2608
2887
  key: 'daily-question',