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/CHANGELOG.md +41 -0
- package/LICENSE +21 -21
- package/README.md +110 -25
- package/client.js +432 -153
- package/cordis.patch.yml +1 -1
- package/icon.svg +10 -7
- package/index.js +8 -8
- package/lib/async.js +17 -0
- package/lib/bank.js +56 -39
- package/lib/exam-dates.js +101 -67
- package/lib/generate.js +131 -114
- package/lib/generation-history.js +67 -0
- package/lib/llm-json.js +17 -8
- package/lib/pdf-worker.js +5 -0
- package/lib/pdf.js +156 -34
- package/lib/reference-reader.js +178 -0
- package/lib/reference-sources.js +83 -0
- package/lib/references.js +159 -0
- package/lib/research.js +166 -0
- package/lib/routes.js +51 -86
- package/lib/service.js +258 -109
- package/lib/store.js +3 -8
- package/lib/subjects.js +31 -0
- package/lib/tutor.js +6 -5
- package/lib/updates.js +49 -0
- package/package.json +4 -2
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;
|
|
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:
|
|
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-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
1080
|
-
h('div', { className: 'kd-cd-
|
|
1086
|
+
h('div', { className: 'kd-cd-primary' }, showDays ? `距离 ${year} 考研还剩` : spaceDigits(lines.primary)),
|
|
1087
|
+
showDays ? h('div', { className: 'kd-cd-days' }, approx ? '约 ' : null, h('strong', null, String(days)), ' 天') : null,
|
|
1088
|
+
duration ? h('div', { className: 'kd-cd-secondary' }, spaceDigits(duration)) : null,
|
|
1089
|
+
h('div', { className: 'kd-cd-target' }, spaceDigits(lines.target)))
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
},
|
|
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' },
|
|
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 &&
|
|
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 &&
|
|
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 && !
|
|
1399
|
+
!revealed && !canSelect
|
|
1331
1400
|
? [
|
|
1332
1401
|
h(Btn, { key: 'done', variant: 'primary', disabled: busy === 'reveal', onClick: () => onReveal('done') }, T.done),
|
|
1333
|
-
|
|
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: !
|
|
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}
|
|
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(
|
|
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
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2247
|
+
attemptId,
|
|
2027
2248
|
})
|
|
2028
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2059
|
-
|
|
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
|
-
|
|
2086
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2383
|
-
|
|
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')
|
|
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(
|
|
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 ?
|
|
2416
|
-
|
|
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
|
|
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
|
|
2547
|
-
|
|
2834
|
+
viewBox: '0 0 64 64',
|
|
2835
|
+
className,
|
|
2548
2836
|
'aria-hidden': true,
|
|
2549
2837
|
},
|
|
2550
|
-
h('
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
}),
|
|
2555
|
-
h('
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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
|
-
},
|
|
2884
|
+
}, PluginLogo)),
|
|
2606
2885
|
slots.inject('main', () => slots.register({
|
|
2607
2886
|
name: 'main',
|
|
2608
2887
|
key: 'daily-question',
|