daily-question 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -21
- package/README.md +98 -20
- package/client.js +281 -61
- package/index.js +2 -2
- package/lib/async.js +17 -0
- package/lib/bank.js +3 -3
- package/lib/generate.js +121 -101
- 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 +2 -0
- package/lib/service.js +199 -50
- package/lib/subjects.js +19 -0
- package/lib/tutor.js +6 -5
- package/lib/updates.js +49 -0
- package/package.json +4 -2
package/client.js
CHANGED
|
@@ -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 '内置官方日期表'
|
|
@@ -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; }
|
|
@@ -690,7 +692,7 @@ window.__ModuleLoader__.load({
|
|
|
690
692
|
background:color-mix(in srgb, var(--kd-state-success-primary) 8%, transparent); }
|
|
691
693
|
.kd-option.is-wrong { border-color:var(--kd-state-error-primary);
|
|
692
694
|
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;
|
|
695
|
+
.kd-option-key { flex:0 0 auto; align-self:center; width:24px; height:24px; border-radius:8px; display:inline-flex;
|
|
694
696
|
align-items:center; justify-content:center; font-size:12px; font-weight:600;
|
|
695
697
|
border:1px solid var(--kd-border-l2); background:var(--kd-bg-base); }
|
|
696
698
|
.kd-option.is-selected .kd-option-key { color:var(--kd-brand-primary); border-color:var(--kd-brand-primary); }
|
|
@@ -926,10 +928,12 @@ window.__ModuleLoader__.load({
|
|
|
926
928
|
try {
|
|
927
929
|
response = await fetch('/api/daily-question/rpc', {
|
|
928
930
|
method: 'POST',
|
|
931
|
+
signal: AbortSignal.timeout(op === 'ensureDaily' || op === 'revise' ? 16*60*1000 : op === 'followup' ? 6*60*1000 : 60_000),
|
|
929
932
|
headers: { 'content-type': 'application/json' },
|
|
930
933
|
body: JSON.stringify({ op, args: args || {} }),
|
|
931
934
|
})
|
|
932
935
|
} catch (error) {
|
|
936
|
+
if (error.name === 'TimeoutError') throw new Error('等待服务响应超时,请重试')
|
|
933
937
|
throw new Error(`无法连接插件后端(${error.message})。请确认「考研每日一题」插件已启用。`)
|
|
934
938
|
}
|
|
935
939
|
let payload = null
|
|
@@ -1128,6 +1132,39 @@ window.__ModuleLoader__.load({
|
|
|
1128
1132
|
}
|
|
1129
1133
|
|
|
1130
1134
|
/* ============================== 导航 ============================== */
|
|
1135
|
+
function UpdateCheck({ currentVersion }) {
|
|
1136
|
+
const [state,setState]=useState({busy:true,result:null,error:''})
|
|
1137
|
+
const checking=useRef(false)
|
|
1138
|
+
const active=useRef(true)
|
|
1139
|
+
const check=async(force=false)=>{
|
|
1140
|
+
if(checking.current)return
|
|
1141
|
+
checking.current=true
|
|
1142
|
+
setState((value)=>({...value,busy:true,error:''}))
|
|
1143
|
+
try {
|
|
1144
|
+
const result=await rpc('checkUpdates',{force})
|
|
1145
|
+
if(active.current)setState({busy:false,result,error:''})
|
|
1146
|
+
} catch(error) {
|
|
1147
|
+
if(active.current)setState((value)=>({...value,busy:false,error:error.message}))
|
|
1148
|
+
} finally {checking.current=false}
|
|
1149
|
+
}
|
|
1150
|
+
useEffect(()=>{
|
|
1151
|
+
active.current=true
|
|
1152
|
+
const automatic=()=>{if(!window.document.hidden)check(false)}
|
|
1153
|
+
automatic()
|
|
1154
|
+
const timer=window.setInterval(automatic,6*60*60*1000)
|
|
1155
|
+
window.addEventListener('focus',automatic)
|
|
1156
|
+
return ()=>{active.current=false;window.clearInterval(timer);window.removeEventListener('focus',automatic)}
|
|
1157
|
+
},[])
|
|
1158
|
+
return h('div',{className:'kd-update'},
|
|
1159
|
+
h(Btn,{variant:'ghost',disabled:state.busy,onClick:()=>check(true)},
|
|
1160
|
+
state.busy?h('span',{className:'kd-spinner'}):h(Icon,{name:'refresh'}),state.busy?'正在检查…':'检查更新'),
|
|
1161
|
+
h('div',{className:'kd-msg-meta',style:{padding:'0 8px'}},`当前版本 v${currentVersion||state.result?.currentVersion||'—'}`),
|
|
1162
|
+
h('div',{className:'kd-msg-meta',role:'status','aria-live':'polite',title:state.error||'',style:{padding:'4px 8px',overflowWrap:'anywhere'}},
|
|
1163
|
+
state.error?'暂时无法检查,请重试':state.result?.updateAvailable
|
|
1164
|
+
? h('a',{className:'kd-link',href:state.result.packageUrl,target:'_blank',rel:'noreferrer noopener'},`发现新版本 v${state.result.latestVersion} ↗`)
|
|
1165
|
+
: state.result?'已是最新版本':null))
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1131
1168
|
function SubjectNav({ boot, subjectId, onPick, onManage, onBanks }) {
|
|
1132
1169
|
const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
|
|
1133
1170
|
return h('div', { className: 'kd-nav' },
|
|
@@ -1153,7 +1190,8 @@ window.__ModuleLoader__.load({
|
|
|
1153
1190
|
h('div', { className: 'kd-nav-group' },
|
|
1154
1191
|
h('div', { className: 'kd-nav-group-label' }, '设置'),
|
|
1155
1192
|
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)
|
|
1193
|
+
h(Btn, { variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book' }), T.manageBanks),
|
|
1194
|
+
h(UpdateCheck,{currentVersion:boot.pluginVersion})))
|
|
1157
1195
|
}
|
|
1158
1196
|
|
|
1159
1197
|
function TabBar({ kind, counts, onPick }) {
|
|
@@ -1273,6 +1311,7 @@ window.__ModuleLoader__.load({
|
|
|
1273
1311
|
function QuestionPane({
|
|
1274
1312
|
q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
|
|
1275
1313
|
onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
|
|
1314
|
+
onCancel,
|
|
1276
1315
|
}) {
|
|
1277
1316
|
if (!q) return null
|
|
1278
1317
|
const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
|
|
@@ -1286,9 +1325,9 @@ window.__ModuleLoader__.load({
|
|
|
1286
1325
|
h('div', { className: 'kd-chips' },
|
|
1287
1326
|
h('span', { className: 'kd-tag kd-tag-brand' }, q.typeLabel),
|
|
1288
1327
|
h('span', { className: 'kd-tag' }, q.subjectName),
|
|
1289
|
-
h('span', { className: 'kd-tag' }, `难度 ${q.difficulty}/5`),
|
|
1328
|
+
h('span', { className: 'kd-tag', title: q.examAlignment ? '独立评审参考历年真题给出的难度,不是考生正确率统计' : '原始难度标签,尚无真题对照评审' }, `难度 ${q.difficulty}/5`),
|
|
1290
1329
|
h('span', { className: 'kd-tag' }, formatDate(q.assignedDate)),
|
|
1291
|
-
h('span', { className: 'kd-tag' },
|
|
1330
|
+
h('span', { className: 'kd-tag' }, `累计作答 ${q.attemptCount} 次`),
|
|
1292
1331
|
h('span', { className: 'kd-tag' }, q.source?.kind === 'bank' ? q.source.label : '在线出题'),
|
|
1293
1332
|
q.answerStatus === 'verified'
|
|
1294
1333
|
? h(Badge, { tone: 'ok' }, T.verified)
|
|
@@ -1300,6 +1339,17 @@ window.__ModuleLoader__.load({
|
|
|
1300
1339
|
? h('div', { className: 'kd-meta', style: { marginTop: '10px' } },
|
|
1301
1340
|
h('span', null, `${T.knowledge}:${q.knowledge.join('、')}`))
|
|
1302
1341
|
: null,
|
|
1342
|
+
q.examAlignment ? h('details', { className: 'kd-verify' },
|
|
1343
|
+
h('summary', null, `范围与难度依据 · 大纲 ${q.examAlignment.outlineYear} · 独立真题对照`),
|
|
1344
|
+
h('div', null, q.examAlignment.note),
|
|
1345
|
+
h('div', null, `参考考试:${q.examAlignment.basisSubject?.name || q.subjectName} · 检索时间:${formatDateTime(q.examAlignment.checkedAt)}`),
|
|
1346
|
+
...(q.examAlignment.sources || []).map((source) => h('div', { key: source.id },
|
|
1347
|
+
h('a', { className: 'kd-link', href: source.url, target: '_blank', rel: 'noreferrer noopener' },
|
|
1348
|
+
`${source.year} ${source.title}(${source.official ? '官方来源' : source.kind === 'outline' ? source.verificationLabel || '非官方大纲转载' : '非官方真题转载'})`),
|
|
1349
|
+
source.coverage ? h('div', null, source.coverage) : null,
|
|
1350
|
+
source.publisherUrl ? h('a', { className:'kd-link', href:source.publisherUrl, target:'_blank', rel:'noreferrer noopener' }, `出版社版本记录 · ISBN ${source.isbn}`) : null)),
|
|
1351
|
+
revealed ? h('div', null, q.examAlignment.rationale,
|
|
1352
|
+
...(q.examAlignment.warnings || []).map((warning,i)=>h('div',{key:i,className:'kd-meta'},warning))) : null) : null,
|
|
1303
1353
|
h('div', { className: 'kd-stem' }, h(Markdown, { text: q.stem })),
|
|
1304
1354
|
isChoice && q.options?.length
|
|
1305
1355
|
? h('div', { className: 'kd-options' }, q.options.map((option) => {
|
|
@@ -1345,6 +1395,7 @@ window.__ModuleLoader__.load({
|
|
|
1345
1395
|
: null,
|
|
1346
1396
|
revealed ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup) : null),
|
|
1347
1397
|
revealed ? h(AnswerBlock, { question: q, onRevise, revising, onRemoveWrong }) : null,
|
|
1398
|
+
revising && onCancel ? h(Btn,{variant:'ghost',onClick:onCancel},'取消修订') : null,
|
|
1348
1399
|
revealed && last?.result === 'pending'
|
|
1349
1400
|
? h('div', { className: 'kd-actions', style: { marginTop: '12px' } },
|
|
1350
1401
|
h('span', { className: 'kd-meta' }, '这道题由你自己判定(系统不会替你把“作答完毕”当成答对):'),
|
|
@@ -1416,7 +1467,7 @@ window.__ModuleLoader__.load({
|
|
|
1416
1467
|
return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
|
|
1417
1468
|
}
|
|
1418
1469
|
|
|
1419
|
-
function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, busy }) {
|
|
1470
|
+
function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, onPage, busy }) {
|
|
1420
1471
|
if (!list) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '加载中…')
|
|
1421
1472
|
if (!list.items.length) {
|
|
1422
1473
|
const icon = kind === 'favorite' ? 'star' : kind === 'wrong' ? 'check' : kind === 'all' ? 'book' : 'spark'
|
|
@@ -1428,8 +1479,12 @@ window.__ModuleLoader__.load({
|
|
|
1428
1479
|
}
|
|
1429
1480
|
return h('div', null,
|
|
1430
1481
|
h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
|
|
1431
|
-
h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`),
|
|
1482
|
+
h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.total ?? list.items.length} 道`),
|
|
1432
1483
|
h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
|
|
1484
|
+
list.totalPages>1?h('div',{className:'kd-row',style:{marginBottom:'12px'},'aria-label':'题目分页'},
|
|
1485
|
+
h(Btn,{small:true,disabled:busy||list.page<=1,onClick:()=>onPage(list.page-1)},'上一页'),
|
|
1486
|
+
h('span',{className:'kd-meta'},`第 ${list.page} / ${list.totalPages} 页`),
|
|
1487
|
+
h(Btn,{small:true,disabled:busy||list.page>=list.totalPages,onClick:()=>onPage(list.page+1)},'下一页')):null,
|
|
1433
1488
|
managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
|
|
1434
1489
|
h('div', { className: 'kd-row' },
|
|
1435
1490
|
h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
|
|
@@ -1464,13 +1519,14 @@ window.__ModuleLoader__.load({
|
|
|
1464
1519
|
}
|
|
1465
1520
|
|
|
1466
1521
|
/* ============================== 追问侧栏 ============================== */
|
|
1467
|
-
function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow, ratio = 33, onRatioChange }) {
|
|
1522
|
+
function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, onCancel, narrow, ratio = 33, onRatioChange }) {
|
|
1468
1523
|
const [draft, setDraft] = useState('')
|
|
1469
1524
|
const [sending, setSending] = useState(false)
|
|
1470
1525
|
const [localError, setLocalError] = useState('')
|
|
1471
1526
|
const [pending, setPending] = useState(null)
|
|
1472
1527
|
const scrollRef = useRef(null)
|
|
1473
1528
|
const sendingRef = useRef(false)
|
|
1529
|
+
const sendingMessage = useRef(null)
|
|
1474
1530
|
const messages = thread?.messages || []
|
|
1475
1531
|
|
|
1476
1532
|
useEffect(() => {
|
|
@@ -1482,6 +1538,7 @@ window.__ModuleLoader__.load({
|
|
|
1482
1538
|
const value = String(text || '').trim()
|
|
1483
1539
|
if (!value || sendingRef.current) return
|
|
1484
1540
|
sendingRef.current = true
|
|
1541
|
+
sendingMessage.current = messageId
|
|
1485
1542
|
setSending(true)
|
|
1486
1543
|
setLocalError('')
|
|
1487
1544
|
try {
|
|
@@ -1493,6 +1550,7 @@ window.__ModuleLoader__.load({
|
|
|
1493
1550
|
setPending({ id: messageId, text: value })
|
|
1494
1551
|
} finally {
|
|
1495
1552
|
sendingRef.current = false
|
|
1553
|
+
sendingMessage.current = null
|
|
1496
1554
|
setSending(false)
|
|
1497
1555
|
}
|
|
1498
1556
|
}, [onSend])
|
|
@@ -1581,6 +1639,7 @@ window.__ModuleLoader__.load({
|
|
|
1581
1639
|
},
|
|
1582
1640
|
}),
|
|
1583
1641
|
h('div', { className: 'kd-row', style: { justifyContent: 'flex-end', marginTop: '2px' } },
|
|
1642
|
+
sending && onCancel ? h(Btn,{small:true,variant:'ghost',onClick:()=>onCancel(sendingMessage.current)},'取消回复') : null,
|
|
1584
1643
|
h(Btn, {
|
|
1585
1644
|
variant: 'primary',
|
|
1586
1645
|
small: true,
|
|
@@ -1599,9 +1658,27 @@ window.__ModuleLoader__.load({
|
|
|
1599
1658
|
const [types, setTypes] = useState([])
|
|
1600
1659
|
const [customTypes, setCustomTypes] = useState([])
|
|
1601
1660
|
const [typeDraft, setTypeDraft] = useState('')
|
|
1661
|
+
const [referenceSubjectId, setReferenceSubjectId] = useState('auto')
|
|
1662
|
+
const [referenceExamName, setReferenceExamName] = useState('')
|
|
1663
|
+
const [referenceExamCode, setReferenceExamCode] = useState('')
|
|
1664
|
+
const [subjectCode, setSubjectCode] = useState('')
|
|
1665
|
+
const referenceExams = boot.referenceExams || (boot.subjects || []).filter((s) => !s.custom)
|
|
1666
|
+
const referenceEditor = (value, change, label) => h('div', { className:'kd-field', style:{ marginBottom:0 } },
|
|
1667
|
+
h('select', { className:'kd-select', 'aria-label':label, value:value.referenceSubjectId || 'auto',
|
|
1668
|
+
onChange:(event)=>change({referenceSubjectId:event.target.value}) },
|
|
1669
|
+
h('option', {value:'auto'}, '自动检索与核验(按科目名称和代码)'),
|
|
1670
|
+
...referenceExams.map((exam)=>h('option',{key:exam.id,value:exam.id},`${exam.name}${exam.code ? `(${exam.code})` : ''}`)),
|
|
1671
|
+
h('option',{value:'independent'},'另行指定参考考试名称…')),
|
|
1672
|
+
!value.referenceSubjectId || value.referenceSubjectId === 'auto' ? h('input',{className:'kd-input','aria-label':`${label}代码(考试科目或专业)`,
|
|
1673
|
+
placeholder:'考试科目或专业代码(选填)',value:value.code || '',maxLength:20,onChange:(event)=>change({code:event.target.value})}) : null,
|
|
1674
|
+
value.referenceSubjectId === 'independent' ? h('div',{className:'kd-row'},
|
|
1675
|
+
h('input',{className:'kd-input','aria-label':`${label}名称`,placeholder:'考试名称,院校自命题请包含院校',value:value.referenceExamName || '',maxLength:100,
|
|
1676
|
+
onChange:(event)=>change({referenceExamName:event.target.value})}),
|
|
1677
|
+
h('input',{className:'kd-input','aria-label':`${label}代码`,placeholder:'科目代码(选填)',value:value.referenceExamCode || '',maxLength:20,style:{width:'150px'},
|
|
1678
|
+
onChange:(event)=>change({referenceExamCode:event.target.value})})) : null)
|
|
1602
1679
|
const enabled = new Set(draft.subjects)
|
|
1603
1680
|
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,
|
|
1681
|
+
...item, custom: true, typeLabels: item.types.map((type) => ({ single: '单项选择题', multiple: '多项选择题', fill: '填空题', solution: '解答题' })[type] || type), bankItemCount: 0,
|
|
1605
1682
|
}))]
|
|
1606
1683
|
const close = () => { if (!saving) onClose() }
|
|
1607
1684
|
const toggleSubject = (id) => setDraft((current) => ({ ...current,
|
|
@@ -1621,18 +1698,22 @@ window.__ModuleLoader__.load({
|
|
|
1621
1698
|
setTypes((current) => current.filter((item) => item !== value))
|
|
1622
1699
|
}
|
|
1623
1700
|
const selectedTypes = [...new Set([...types, ...(typeDraft.trim() ? [typeDraft.trim().slice(0, 24)] : [])])]
|
|
1624
|
-
const canAdd = Boolean(name.trim()) && selectedTypes.length > 0
|
|
1701
|
+
const canAdd = Boolean(name.trim()) && selectedTypes.length > 0 && (referenceSubjectId !== 'independent' || Boolean(referenceExamName.trim()))
|
|
1625
1702
|
const withCustomSubject = (current) => {
|
|
1626
|
-
const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), types: selectedTypes }
|
|
1703
|
+
const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), code:subjectCode.trim(), types: selectedTypes, referenceSubjectId, referenceExamName:referenceExamName.trim(), referenceExamCode:referenceExamCode.trim() }
|
|
1627
1704
|
return { ...current, subjects: [...current.subjects, subject.id], customSubjects: [...current.customSubjects, subject] }
|
|
1628
1705
|
}
|
|
1629
1706
|
const finish = async () => {
|
|
1630
1707
|
if (saving) return
|
|
1631
|
-
if ((name.trim() || types.length || typeDraft.trim()) && !canAdd) {
|
|
1632
|
-
setSaveError('
|
|
1708
|
+
if ((name.trim() || types.length || typeDraft.trim() || (referenceSubjectId && referenceSubjectId !== 'auto') || referenceExamName.trim() || referenceExamCode.trim() || subjectCode.trim()) && !canAdd) {
|
|
1709
|
+
setSaveError('请填写自定义科目名称并至少选择一个题型;选择独立参考考试时,还需填写考试名称。或清空未完成的输入。')
|
|
1633
1710
|
return
|
|
1634
1711
|
}
|
|
1635
1712
|
const next = canAdd ? withCustomSubject(draft) : draft
|
|
1713
|
+
if (next.customSubjects.some((s)=>s.referenceSubjectId === 'independent' && !s.referenceExamName?.trim())) {
|
|
1714
|
+
setSaveError('独立参考考试请填写准确的考试名称(院校自命题请包含院校名称)。')
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1636
1717
|
setSaving(true)
|
|
1637
1718
|
setSaveError('')
|
|
1638
1719
|
try { await onDone(next) }
|
|
@@ -1646,6 +1727,10 @@ window.__ModuleLoader__.load({
|
|
|
1646
1727
|
setTypes([])
|
|
1647
1728
|
setCustomTypes([])
|
|
1648
1729
|
setTypeDraft('')
|
|
1730
|
+
setReferenceSubjectId('auto')
|
|
1731
|
+
setReferenceExamName('')
|
|
1732
|
+
setReferenceExamCode('')
|
|
1733
|
+
setSubjectCode('')
|
|
1649
1734
|
}
|
|
1650
1735
|
return h(Modal, {
|
|
1651
1736
|
title: T.manageSubjects,
|
|
@@ -1676,6 +1761,8 @@ window.__ModuleLoader__.load({
|
|
|
1676
1761
|
h('div', { className: 'kd-msg-meta' }, subject.typeLabels.join(' / ')))),
|
|
1677
1762
|
enabled.has(subject.id)
|
|
1678
1763
|
? h('div', { className: 'kd-row' },
|
|
1764
|
+
subject.custom ? referenceEditor(draft.customSubjects.find((s)=>s.id === subject.id) || subject,
|
|
1765
|
+
(patch)=>setDraft((current)=>({...current,customSubjects:current.customSubjects.map((s)=>s.id === subject.id ? {...s,...patch} : s)})),`${subject.name} 参考考试科目`) : null,
|
|
1679
1766
|
h('select', {
|
|
1680
1767
|
className: 'kd-select',
|
|
1681
1768
|
value: draft.subjectSource[subject.id] || 'auto',
|
|
@@ -1692,6 +1779,13 @@ window.__ModuleLoader__.load({
|
|
|
1692
1779
|
h('div', { className: 'kd-divider' }),
|
|
1693
1780
|
h('div', { className: 'kd-field kd-custom-subject' },
|
|
1694
1781
|
h('div', { className: 'kd-field-label' }, '添加自定义科目'),
|
|
1782
|
+
referenceEditor({referenceSubjectId,referenceExamName,referenceExamCode,code:subjectCode},(patch)=>{
|
|
1783
|
+
if (patch.referenceSubjectId !== undefined) setReferenceSubjectId(patch.referenceSubjectId)
|
|
1784
|
+
if (patch.referenceExamName !== undefined) setReferenceExamName(patch.referenceExamName)
|
|
1785
|
+
if (patch.referenceExamCode !== undefined) setReferenceExamCode(patch.referenceExamCode)
|
|
1786
|
+
if (patch.code !== undefined) setSubjectCode(patch.code)
|
|
1787
|
+
},'自定义科目的参考考试'),
|
|
1788
|
+
h('div',{className:'kd-meta'},'默认由模型按名称和代码自主检索、独立核验大纲及两年真题。专业代码不等于考试科目代码;信息不足时可在「另行指定参考考试名称」补充院校和准确考试名称,资料不足不会发布。'),
|
|
1695
1789
|
h('div', { className: 'kd-row' },
|
|
1696
1790
|
h('input', {
|
|
1697
1791
|
className: 'kd-input',
|
|
@@ -1856,6 +1950,35 @@ window.__ModuleLoader__.load({
|
|
|
1856
1950
|
}
|
|
1857
1951
|
|
|
1858
1952
|
/* ============================== 主页面 ============================== */
|
|
1953
|
+
function watchDayChange(snapshot, onSnapshot) {
|
|
1954
|
+
let timer, stopped = false, checking = false
|
|
1955
|
+
const schedule = (retry = false) => {
|
|
1956
|
+
const delay = Date.parse(snapshot.nextDayAt) - Date.parse(snapshot.now)
|
|
1957
|
+
timer = window.setTimeout(check, retry || !Number.isFinite(delay) ? 60_000 : Math.max(1000, delay))
|
|
1958
|
+
}
|
|
1959
|
+
const check = async () => {
|
|
1960
|
+
if (stopped || checking) return
|
|
1961
|
+
checking = true
|
|
1962
|
+
window.clearTimeout(timer)
|
|
1963
|
+
let retry = false
|
|
1964
|
+
try {
|
|
1965
|
+
const fresh = await rpc('bootstrap')
|
|
1966
|
+
if (!stopped) { snapshot = fresh; onSnapshot(fresh) }
|
|
1967
|
+
} catch { retry = true }
|
|
1968
|
+
finally { checking = false; if (!stopped) schedule(retry) }
|
|
1969
|
+
}
|
|
1970
|
+
const onVisible = () => { if (window.document.visibilityState === 'visible') check() }
|
|
1971
|
+
schedule()
|
|
1972
|
+
window.addEventListener('focus', check)
|
|
1973
|
+
window.document.addEventListener('visibilitychange', onVisible)
|
|
1974
|
+
return () => {
|
|
1975
|
+
stopped = true
|
|
1976
|
+
window.clearTimeout(timer)
|
|
1977
|
+
window.removeEventListener('focus', check)
|
|
1978
|
+
window.document.removeEventListener('visibilitychange', onVisible)
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1859
1982
|
function Page() {
|
|
1860
1983
|
const [boot, setBoot] = useState(null)
|
|
1861
1984
|
const [loading, setLoading] = useState(true)
|
|
@@ -1883,6 +2006,13 @@ window.__ModuleLoader__.load({
|
|
|
1883
2006
|
const mainScroll = useRef(null)
|
|
1884
2007
|
const listScroll = useRef(0)
|
|
1885
2008
|
const redoQuestions = useRef(new Set())
|
|
2009
|
+
const dailyRequest = useRef(0)
|
|
2010
|
+
const lastDay = useRef(null)
|
|
2011
|
+
const currentView = useRef(null)
|
|
2012
|
+
const bootRequest = useRef(0)
|
|
2013
|
+
const threadRequest = useRef(0)
|
|
2014
|
+
const selectionRequest = useRef(0)
|
|
2015
|
+
currentView.current = { subjectId, kind, view, page:list?.page||1, loading:busy==='list' }
|
|
1886
2016
|
|
|
1887
2017
|
useEffect(() => {
|
|
1888
2018
|
if (view === 'list' && list && mainScroll.current) mainScroll.current.scrollTop = listScroll.current
|
|
@@ -1898,9 +2028,10 @@ window.__ModuleLoader__.load({
|
|
|
1898
2028
|
useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current) }, [])
|
|
1899
2029
|
|
|
1900
2030
|
const refreshBoot = useCallback(async () => {
|
|
2031
|
+
const request = ++bootRequest.current
|
|
1901
2032
|
try {
|
|
1902
2033
|
const result = await rpc('bootstrap')
|
|
1903
|
-
setBoot(result)
|
|
2034
|
+
if (request === bootRequest.current) setBoot(result)
|
|
1904
2035
|
return result
|
|
1905
2036
|
} catch {
|
|
1906
2037
|
return null
|
|
@@ -1909,6 +2040,7 @@ window.__ModuleLoader__.load({
|
|
|
1909
2040
|
|
|
1910
2041
|
const loadToday = useCallback(async (sid) => {
|
|
1911
2042
|
if (!sid) return
|
|
2043
|
+
const request = ++dailyRequest.current
|
|
1912
2044
|
// 高亮必须跟着视图走:从「全部 / 收藏 / 错题本」切回今日题目时要一起复位
|
|
1913
2045
|
setKind('today')
|
|
1914
2046
|
setThreadOpen(false)
|
|
@@ -1922,31 +2054,67 @@ window.__ModuleLoader__.load({
|
|
|
1922
2054
|
attemptRef.current = newAttemptId()
|
|
1923
2055
|
try {
|
|
1924
2056
|
const result = await rpc('ensureDaily', { subjectId: sid })
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2057
|
+
if (request === dailyRequest.current) {
|
|
2058
|
+
setQuestion(result.question)
|
|
2059
|
+
setLocalRedo(redoQuestions.current.has(result.question.id))
|
|
2060
|
+
setThread(result.thread || null)
|
|
2061
|
+
}
|
|
2062
|
+
await refreshBoot()
|
|
1928
2063
|
} catch (issue) {
|
|
1929
|
-
setFail(issue.message)
|
|
2064
|
+
if (request === dailyRequest.current) setFail(issue.message)
|
|
1930
2065
|
} finally {
|
|
1931
|
-
setBusy('')
|
|
2066
|
+
if (request === dailyRequest.current) setBusy('')
|
|
1932
2067
|
}
|
|
1933
|
-
}, [])
|
|
2068
|
+
}, [refreshBoot])
|
|
2069
|
+
|
|
2070
|
+
useEffect(() => {
|
|
2071
|
+
if (!boot?.today) return undefined
|
|
2072
|
+
return watchDayChange(boot, setBoot)
|
|
2073
|
+
}, [boot?.today])
|
|
2074
|
+
|
|
2075
|
+
useEffect(() => {
|
|
2076
|
+
if (!boot?.today || lastDay.current === boot.today) return undefined
|
|
2077
|
+
const previous = lastDay.current
|
|
2078
|
+
lastDay.current = boot.today
|
|
2079
|
+
if (!previous) return undefined
|
|
2080
|
+
redoQuestions.current.clear()
|
|
2081
|
+
const { subjectId: sid, kind: activeKind } = currentView.current
|
|
2082
|
+
if (!sid) return undefined
|
|
2083
|
+
if (activeKind === 'today') { loadToday(sid); return undefined }
|
|
2084
|
+
let cancelled = false
|
|
2085
|
+
const request=dailyRequest.current
|
|
2086
|
+
rpc('ensureDaily', { subjectId: sid }).then(async () => {
|
|
2087
|
+
if (cancelled) return
|
|
2088
|
+
await refreshBoot()
|
|
2089
|
+
const current = currentView.current
|
|
2090
|
+
if (cancelled || request!==dailyRequest.current || current.loading || current.subjectId !== sid || current.kind === 'today' || current.view !== 'list') return
|
|
2091
|
+
const result = await rpc('listQuestions', { subjectId: sid, kind: current.kind, page:current.page })
|
|
2092
|
+
const latest = currentView.current
|
|
2093
|
+
if (!cancelled && request===dailyRequest.current && !latest.loading && latest.page===current.page && latest.subjectId === sid && latest.kind === current.kind && latest.view === 'list') {
|
|
2094
|
+
listScroll.current = mainScroll.current?.scrollTop || 0
|
|
2095
|
+
setList(result)
|
|
2096
|
+
}
|
|
2097
|
+
}).catch((issue) => { if (!cancelled) notify(`今日题目更新失败:${issue.message}`, 'warn') })
|
|
2098
|
+
return () => { cancelled = true }
|
|
2099
|
+
}, [boot?.today, loadToday, refreshBoot, notify])
|
|
1934
2100
|
|
|
1935
|
-
const loadList = useCallback(async (sid, nextKind) => {
|
|
2101
|
+
const loadList = useCallback(async (sid, nextKind, page = 1) => {
|
|
1936
2102
|
if (!sid) return
|
|
2103
|
+
const request = ++dailyRequest.current
|
|
1937
2104
|
listScroll.current = 0
|
|
1938
2105
|
setKind(nextKind)
|
|
1939
2106
|
setThreadOpen(false)
|
|
1940
2107
|
setView('list')
|
|
2108
|
+
setFail('')
|
|
1941
2109
|
setBusy('list')
|
|
1942
2110
|
setList(null)
|
|
1943
2111
|
try {
|
|
1944
|
-
const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind })
|
|
1945
|
-
setList(result)
|
|
2112
|
+
const result = await rpc('listQuestions', { subjectId: sid, kind: nextKind, page })
|
|
2113
|
+
if (request === dailyRequest.current) setList(result)
|
|
1946
2114
|
} catch (issue) {
|
|
1947
|
-
notify(issue.message, 'bad')
|
|
2115
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
1948
2116
|
} finally {
|
|
1949
|
-
setBusy('')
|
|
2117
|
+
if (request === dailyRequest.current) setBusy('')
|
|
1950
2118
|
}
|
|
1951
2119
|
}, [notify])
|
|
1952
2120
|
|
|
@@ -1961,10 +2129,12 @@ window.__ModuleLoader__.load({
|
|
|
1961
2129
|
}, [subjectId, loadToday])
|
|
1962
2130
|
|
|
1963
2131
|
const openQuestion = useCallback(async (qid) => {
|
|
2132
|
+
const request = ++dailyRequest.current
|
|
1964
2133
|
listScroll.current = mainScroll.current?.scrollTop || 0
|
|
1965
2134
|
setBusy('question')
|
|
1966
2135
|
try {
|
|
1967
2136
|
const result = await rpc('getQuestion', { qid })
|
|
2137
|
+
if (request !== dailyRequest.current) return
|
|
1968
2138
|
const canResume = result.isToday && result.question.answerState === 'revealed'
|
|
1969
2139
|
&& result.question.attemptCount > 0 && !redoQuestions.current.has(qid)
|
|
1970
2140
|
if (!canResume) redoQuestions.current.add(qid)
|
|
@@ -1976,19 +2146,20 @@ window.__ModuleLoader__.load({
|
|
|
1976
2146
|
setThreadOpen(false)
|
|
1977
2147
|
attemptRef.current = newAttemptId()
|
|
1978
2148
|
} catch (issue) {
|
|
1979
|
-
notify(issue.message, 'bad')
|
|
2149
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
1980
2150
|
} finally {
|
|
1981
|
-
setBusy('')
|
|
2151
|
+
if (request === dailyRequest.current) setBusy('')
|
|
1982
2152
|
}
|
|
1983
2153
|
}, [notify])
|
|
1984
2154
|
|
|
1985
2155
|
const backToList = async () => {
|
|
2156
|
+
const request = ++dailyRequest.current
|
|
1986
2157
|
setThreadOpen(false)
|
|
1987
2158
|
setView('list')
|
|
1988
2159
|
try {
|
|
1989
|
-
const result = await rpc('listQuestions', { subjectId, kind })
|
|
1990
|
-
setList(result)
|
|
1991
|
-
} catch (issue) { notify(issue.message, 'bad') }
|
|
2160
|
+
const result = await rpc('listQuestions', { subjectId, kind, page:list?.page||1 })
|
|
2161
|
+
if (request === dailyRequest.current) setList(result)
|
|
2162
|
+
} catch (issue) { if (request === dailyRequest.current) notify(issue.message, 'bad') }
|
|
1992
2163
|
}
|
|
1993
2164
|
|
|
1994
2165
|
useEffect(() => {
|
|
@@ -2017,6 +2188,7 @@ window.__ModuleLoader__.load({
|
|
|
2017
2188
|
|
|
2018
2189
|
const reveal = useCallback(async (mode) => {
|
|
2019
2190
|
if (!question) return
|
|
2191
|
+
const request = dailyRequest.current
|
|
2020
2192
|
setBusy('reveal')
|
|
2021
2193
|
try {
|
|
2022
2194
|
const result = await rpc('reveal', {
|
|
@@ -2025,49 +2197,58 @@ window.__ModuleLoader__.load({
|
|
|
2025
2197
|
choice: mode === 'submit' ? choice : '',
|
|
2026
2198
|
attemptId: attemptRef.current,
|
|
2027
2199
|
})
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2200
|
+
if (request === dailyRequest.current) {
|
|
2201
|
+
setQuestion(result.question)
|
|
2202
|
+
redoQuestions.current.delete(question.id)
|
|
2203
|
+
setLocalRedo(false)
|
|
2204
|
+
}
|
|
2031
2205
|
refreshBoot()
|
|
2032
2206
|
} catch (issue) {
|
|
2033
2207
|
notify(issue.message, 'bad')
|
|
2034
2208
|
} finally {
|
|
2035
|
-
setBusy('')
|
|
2209
|
+
if (request === dailyRequest.current) setBusy('')
|
|
2036
2210
|
}
|
|
2037
2211
|
}, [question, choice, notify, refreshBoot])
|
|
2038
2212
|
|
|
2039
2213
|
const judge = useCallback(async (verdict) => {
|
|
2040
2214
|
if (!question) return
|
|
2215
|
+
if (!question.lastAttempt?.id) { notify('缺少本次作答记录,请重新打开题目后判定','bad'); return }
|
|
2216
|
+
const request = dailyRequest.current
|
|
2041
2217
|
setBusy('judge')
|
|
2042
2218
|
try {
|
|
2043
|
-
const result = await rpc('judge', { qid: question.id, verdict })
|
|
2044
|
-
setQuestion(result.question)
|
|
2219
|
+
const result = await rpc('judge', { qid: question.id, verdict, attemptId:question.lastAttempt.id })
|
|
2220
|
+
if (request === dailyRequest.current) setQuestion(result.question)
|
|
2045
2221
|
refreshBoot()
|
|
2046
2222
|
} catch (issue) {
|
|
2047
|
-
notify(issue.message, 'bad')
|
|
2223
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2048
2224
|
} finally {
|
|
2049
|
-
setBusy('')
|
|
2225
|
+
if (request === dailyRequest.current) setBusy('')
|
|
2050
2226
|
}
|
|
2051
2227
|
}, [question, notify, refreshBoot])
|
|
2052
2228
|
|
|
2053
2229
|
const favorite = useCallback(async () => {
|
|
2054
2230
|
if (!question) return
|
|
2231
|
+
const request = dailyRequest.current
|
|
2055
2232
|
setBusy('favorite')
|
|
2056
2233
|
try {
|
|
2057
2234
|
const result = await rpc('favorite', { qid: question.id, value: !question.favorite })
|
|
2058
|
-
|
|
2059
|
-
|
|
2235
|
+
if (request === dailyRequest.current) {
|
|
2236
|
+
setQuestion(result.question)
|
|
2237
|
+
notify(question.favorite ? '已取消收藏' : '已加入收藏')
|
|
2238
|
+
}
|
|
2060
2239
|
refreshBoot()
|
|
2061
2240
|
} catch (issue) {
|
|
2062
|
-
notify(issue.message, 'bad')
|
|
2241
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2063
2242
|
} finally {
|
|
2064
|
-
setBusy('')
|
|
2243
|
+
if (request === dailyRequest.current) setBusy('')
|
|
2065
2244
|
}
|
|
2066
2245
|
}, [question, notify, refreshBoot])
|
|
2067
2246
|
|
|
2068
2247
|
const sendFollowup = useCallback(async (text, messageId) => { if (!question) throw new Error('没有打开任何题目')
|
|
2248
|
+
const request = dailyRequest.current
|
|
2249
|
+
const threadVersion = ++threadRequest.current
|
|
2069
2250
|
const result = await rpc('followup', { qid: question.id, message: text, clientMessageId: messageId })
|
|
2070
|
-
setThread(result.thread)
|
|
2251
|
+
if (request === dailyRequest.current && threadVersion === threadRequest.current) setThread(result.thread)
|
|
2071
2252
|
}, [question])
|
|
2072
2253
|
|
|
2073
2254
|
const clearThread = useCallback(async () => {
|
|
@@ -2079,11 +2260,15 @@ window.__ModuleLoader__.load({
|
|
|
2079
2260
|
confirmLabel: '清空',
|
|
2080
2261
|
danger: true,
|
|
2081
2262
|
onConfirm: async () => {
|
|
2263
|
+
const request = dailyRequest.current
|
|
2264
|
+
const threadVersion = ++threadRequest.current
|
|
2082
2265
|
setModal(null)
|
|
2083
2266
|
try {
|
|
2084
2267
|
const result = await rpc('clearThread', { qid: question.id })
|
|
2085
|
-
|
|
2086
|
-
|
|
2268
|
+
if (request === dailyRequest.current && threadVersion === threadRequest.current) {
|
|
2269
|
+
setThread(result.thread)
|
|
2270
|
+
notify('本题追问已清空')
|
|
2271
|
+
}
|
|
2087
2272
|
} catch (issue) {
|
|
2088
2273
|
notify(issue.message, 'bad')
|
|
2089
2274
|
}
|
|
@@ -2091,47 +2276,58 @@ window.__ModuleLoader__.load({
|
|
|
2091
2276
|
})
|
|
2092
2277
|
}, [question, notify])
|
|
2093
2278
|
|
|
2279
|
+
const cancelWork = async (kind, qid, clientMessageId) => {
|
|
2280
|
+
try { await rpc('cancelTask',{kind,subjectId,qid,clientMessageId}) }
|
|
2281
|
+
catch (issue) { notify(issue.message,'bad') }
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2094
2284
|
const revise = useCallback(async () => {
|
|
2095
2285
|
if (!question) return
|
|
2286
|
+
const request = dailyRequest.current
|
|
2096
2287
|
setBusy('revise')
|
|
2097
2288
|
try {
|
|
2098
2289
|
const result = await rpc('revise', { qid: question.id })
|
|
2290
|
+
if (request !== dailyRequest.current) return
|
|
2099
2291
|
setQuestion(result.question)
|
|
2100
2292
|
setModal({ kind: 'revision' })
|
|
2101
2293
|
} catch (issue) {
|
|
2102
|
-
notify(issue.message, 'bad')
|
|
2294
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2103
2295
|
} finally {
|
|
2104
|
-
setBusy('')
|
|
2296
|
+
if (request === dailyRequest.current) setBusy('')
|
|
2105
2297
|
}
|
|
2106
2298
|
}, [question, notify])
|
|
2107
2299
|
|
|
2108
2300
|
const confirmRevision = useCallback(async () => {
|
|
2109
2301
|
if (!question?.pendingRevision) return
|
|
2302
|
+
const request = dailyRequest.current
|
|
2110
2303
|
setBusy('revise')
|
|
2111
2304
|
try {
|
|
2112
2305
|
const result = await rpc('confirmRevision', {
|
|
2113
2306
|
qid: question.id,
|
|
2114
2307
|
candidateId: question.pendingRevision.candidateId,
|
|
2115
2308
|
})
|
|
2309
|
+
if (request !== dailyRequest.current) return
|
|
2116
2310
|
setQuestion(result.question)
|
|
2117
2311
|
setModal(null)
|
|
2118
2312
|
notify('已替换为通过校验的新版本,旧版本已保留')
|
|
2119
2313
|
} catch (issue) {
|
|
2120
|
-
notify(issue.message, 'bad')
|
|
2314
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2121
2315
|
} finally {
|
|
2122
|
-
setBusy('')
|
|
2316
|
+
if (request === dailyRequest.current) setBusy('')
|
|
2123
2317
|
}
|
|
2124
2318
|
}, [question, notify])
|
|
2125
2319
|
|
|
2126
2320
|
const discardRevision = useCallback(async () => {
|
|
2127
2321
|
if (!question) return
|
|
2322
|
+
const request = dailyRequest.current
|
|
2128
2323
|
setModal(null)
|
|
2129
2324
|
try {
|
|
2130
2325
|
const result = await rpc('discardRevision', { qid: question.id })
|
|
2326
|
+
if (request !== dailyRequest.current) return
|
|
2131
2327
|
setQuestion(result.question)
|
|
2132
2328
|
notify('已放弃候选版本,正式答案保持不变')
|
|
2133
2329
|
} catch (issue) {
|
|
2134
|
-
notify(issue.message, 'bad')
|
|
2330
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2135
2331
|
}
|
|
2136
2332
|
}, [question, notify])
|
|
2137
2333
|
|
|
@@ -2245,13 +2441,15 @@ window.__ModuleLoader__.load({
|
|
|
2245
2441
|
|
|
2246
2442
|
const bulkQuestions = async (action) => {
|
|
2247
2443
|
if (bulkLock.current || !selected.length) return
|
|
2444
|
+
const request = dailyRequest.current
|
|
2248
2445
|
const qids = [...selected]
|
|
2249
2446
|
const execute = async (confirmProtected = false) => {
|
|
2250
|
-
if (bulkLock.current) return
|
|
2447
|
+
if (bulkLock.current || request !== dailyRequest.current) return
|
|
2251
2448
|
bulkLock.current = true
|
|
2252
2449
|
setBusy('bulk')
|
|
2253
2450
|
try {
|
|
2254
2451
|
const result = await rpc('bulkQuestions', { qids, action, confirmProtected })
|
|
2452
|
+
if (request !== dailyRequest.current) { refreshBoot(); return }
|
|
2255
2453
|
if (result.requiresConfirmation) {
|
|
2256
2454
|
setModal({ kind: 'confirm', title: '再次确认删除收藏或错题', danger: true,
|
|
2257
2455
|
message: `选中的题目中有 ${result.protectedCount} 道已收藏或在错题本中。继续将永久删除所选 ${qids.length} 道题及其作答和追问,无法恢复。确定仍要删除吗?`,
|
|
@@ -2260,13 +2458,14 @@ window.__ModuleLoader__.load({
|
|
|
2260
2458
|
}
|
|
2261
2459
|
setModal(null)
|
|
2262
2460
|
setSelected([])
|
|
2263
|
-
|
|
2461
|
+
const loading = loadList(subjectId, kind, list?.page||1)
|
|
2462
|
+
const listRequest = dailyRequest.current
|
|
2463
|
+
await loading
|
|
2264
2464
|
await refreshBoot()
|
|
2265
|
-
notify(`已处理 ${result.affected} 道题目`)
|
|
2465
|
+
if (listRequest === dailyRequest.current) notify(`已处理 ${result.affected} 道题目`)
|
|
2266
2466
|
} catch (issue) {
|
|
2267
|
-
setModal(null)
|
|
2268
|
-
|
|
2269
|
-
} finally { bulkLock.current = false; setBusy('') }
|
|
2467
|
+
if (request === dailyRequest.current) { setModal(null); notify(issue.message, 'bad') }
|
|
2468
|
+
} finally { bulkLock.current = false; if (request === dailyRequest.current) setBusy('') }
|
|
2270
2469
|
}
|
|
2271
2470
|
if (action === 'delete') {
|
|
2272
2471
|
setModal({ kind: 'confirm', title: '删除所选题目', danger: true,
|
|
@@ -2276,6 +2475,18 @@ window.__ModuleLoader__.load({
|
|
|
2276
2475
|
}
|
|
2277
2476
|
|
|
2278
2477
|
const counts = subjectId ? boot?.counts?.[subjectId] : null
|
|
2478
|
+
const selectAllQuestions = async (value) => {
|
|
2479
|
+
if(bulkLock.current)return
|
|
2480
|
+
const request=dailyRequest.current,selection=++selectionRequest.current
|
|
2481
|
+
const isCurrent=()=>request===dailyRequest.current&&selection===selectionRequest.current
|
|
2482
|
+
if(!value){setSelected([]);setBusy(current=>current==='selection'?'':current);return}
|
|
2483
|
+
setBusy('selection')
|
|
2484
|
+
try {
|
|
2485
|
+
const result=await rpc('listQuestions',{subjectId,kind,idsOnly:true})
|
|
2486
|
+
if(isCurrent())setSelected(result.ids)
|
|
2487
|
+
} catch(issue){if(isCurrent())notify(issue.message,'bad')}
|
|
2488
|
+
finally{if(isCurrent())setBusy(current=>current==='selection'?'':current)}
|
|
2489
|
+
}
|
|
2279
2490
|
const narrow = useMediaQuery('(max-width: 1180px)')
|
|
2280
2491
|
|
|
2281
2492
|
const mainContent = (() => {
|
|
@@ -2308,7 +2519,8 @@ window.__ModuleLoader__.load({
|
|
|
2308
2519
|
h('span', { className: 'kd-spinner' }),
|
|
2309
2520
|
T.generating,
|
|
2310
2521
|
h('div', { className: 'kd-msg-meta', style: { marginTop: '8px' } },
|
|
2311
|
-
'
|
|
2522
|
+
'先出题,再仅凭题干和选项盲解并比对答案;分歧交由主裁,仍无法确定就重新出题或阻止发布。首次出题可能需要较长时间。'),
|
|
2523
|
+
h(Btn,{variant:'ghost',onClick:()=>cancelWork('daily')},'取消生成'))
|
|
2312
2524
|
}
|
|
2313
2525
|
if (fail) {
|
|
2314
2526
|
return h(EmptyState, {
|
|
@@ -2339,10 +2551,11 @@ window.__ModuleLoader__.load({
|
|
|
2339
2551
|
kind,
|
|
2340
2552
|
subjectName: currentSubject?.name || '',
|
|
2341
2553
|
onOpen: openQuestion,
|
|
2342
|
-
managing, selected, busy: busy === 'bulk',
|
|
2554
|
+
managing, selected, busy: busy === 'bulk' || busy === 'selection',
|
|
2343
2555
|
onManage: () => { setManaging(!managing); setSelected([]) },
|
|
2344
2556
|
onToggle: (id) => setSelected((ids) => ids.includes(id) ? ids.filter((value) => value !== id) : [...ids, id]),
|
|
2345
|
-
onSelectAll:
|
|
2557
|
+
onSelectAll: selectAllQuestions,
|
|
2558
|
+
onPage: (page) => loadList(subjectId,kind,page),
|
|
2346
2559
|
onBulk: bulkQuestions,
|
|
2347
2560
|
})
|
|
2348
2561
|
}
|
|
@@ -2366,6 +2579,8 @@ window.__ModuleLoader__.load({
|
|
|
2366
2579
|
onJudge: judge,
|
|
2367
2580
|
onFavorite: favorite,
|
|
2368
2581
|
onRedo: () => {
|
|
2582
|
+
dailyRequest.current++
|
|
2583
|
+
setBusy('')
|
|
2369
2584
|
redoQuestions.current.add(question.id)
|
|
2370
2585
|
setLocalRedo(true)
|
|
2371
2586
|
setThreadOpen(false)
|
|
@@ -2373,17 +2588,21 @@ window.__ModuleLoader__.load({
|
|
|
2373
2588
|
attemptRef.current = newAttemptId()
|
|
2374
2589
|
},
|
|
2375
2590
|
onRevise: revise,
|
|
2591
|
+
onCancel: () => cancelWork('revise',question.id),
|
|
2376
2592
|
revising: busy === 'revise',
|
|
2377
2593
|
onToggleThread: () => setThreadOpen((value) => !value),
|
|
2378
2594
|
onRemoveWrong: async () => {
|
|
2379
2595
|
if (!question) return
|
|
2596
|
+
const request = dailyRequest.current
|
|
2380
2597
|
try {
|
|
2381
2598
|
const result = await rpc('removeFromWrongBook', { qid: question.id })
|
|
2382
|
-
|
|
2383
|
-
|
|
2599
|
+
if (request === dailyRequest.current) {
|
|
2600
|
+
setQuestion(result.question)
|
|
2601
|
+
notify('已移出错题本,历史作答记录仍然保留')
|
|
2602
|
+
}
|
|
2384
2603
|
refreshBoot()
|
|
2385
2604
|
} catch (issue) {
|
|
2386
|
-
notify(issue.message, 'bad')
|
|
2605
|
+
if (request === dailyRequest.current) notify(issue.message, 'bad')
|
|
2387
2606
|
}
|
|
2388
2607
|
},
|
|
2389
2608
|
})
|
|
@@ -2459,6 +2678,7 @@ window.__ModuleLoader__.load({
|
|
|
2459
2678
|
onRatioChange: setThreadRatio,
|
|
2460
2679
|
onSend: sendFollowup,
|
|
2461
2680
|
onClear: clearThread,
|
|
2681
|
+
onCancel: (messageId) => cancelWork('followup',question.id,messageId),
|
|
2462
2682
|
onClose: () => setThreadOpen(false),
|
|
2463
2683
|
})
|
|
2464
2684
|
: null)),
|