feat: resolve public bank conflicts

This commit is contained in:
Codex
2026-06-29 13:50:41 +08:00
parent 93ec1a07d2
commit 7fc92986a5
13 changed files with 547 additions and 32 deletions

View File

@@ -13,6 +13,7 @@ import {
loadPublicQuestionBankConflicts,
loadPublicQuestionBanks,
previewContentImport,
resolvePublicQuestionBankConflict,
runImportPostCheck,
syncPublicQuestionBank,
type ContentEntryAdminItem,
@@ -23,6 +24,8 @@ import {
type ImportTemplateItem,
type ImportType,
type ImportSourceFormat,
type PublicQuestionBankConflictItem,
type PublicQuestionBankConflictsResult,
type PublicQuestionBankItem,
} from '@/services/tenantAdmin';
import '../admin.css';
@@ -100,6 +103,10 @@ function safeJsonPreview(value: unknown, maxLength = 360) {
}
}
function conflictItems(value: PublicQuestionBankConflictsResult | null) {
return Array.isArray(value?.conflicts) ? value.conflicts : [];
}
function objectRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
@@ -131,7 +138,7 @@ export default function TenantContentPage() {
const [template, setTemplate] = useState<ImportTemplateItem | null>(null);
const [previewResult, setPreviewResult] = useState<ImportPreviewResult | null>(null);
const [postCheck, setPostCheck] = useState<Record<string, unknown> | null>(null);
const [conflicts, setConflicts] = useState<Record<string, unknown> | null>(null);
const [conflicts, setConflicts] = useState<PublicQuestionBankConflictsResult | null>(null);
const [busy, setBusy] = useState('');
const [error, setError] = useState('');
@@ -393,6 +400,38 @@ export default function TenantContentPage() {
}
}
async function resolveConflict(item: PublicQuestionBankConflictItem, resolution: 'accept_platform' | 'keep_local') {
const adoptionId = conflicts?.adoptionId;
if (!adoptionId || !item.sourceQuestionId) {
setError('缺少冲突上下文,无法处理。');
return;
}
const ok = await confirm(
resolution === 'accept_platform' ? '采纳平台版本' : '保留租户版本',
resolution === 'accept_platform'
? '确认用平台公共题库的新版本覆盖该租户副本?系统会写入新题目版本并记录审计。'
: '确认保留租户本地改写版本?系统会记录该冲突已处理,后续同步不再反复提示同一版本。',
);
if (!ok) return;
setBusy(`resolve:${item.sourceQuestionId}:${resolution}`);
setError('');
try {
await resolvePublicQuestionBankConflict({
adoptionId,
sourceQuestionId: item.sourceQuestionId,
resolution,
});
const payload = await loadPublicQuestionBankConflicts(adoptionId);
setConflicts(payload.item || null);
Taro.showToast({ title: '已处理', icon: 'success' });
reload();
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '冲突处理失败');
} finally {
setBusy('');
}
}
const postCheckSummary = postCheck?.importPostCheck && typeof postCheck.importPostCheck === 'object'
? postCheck.importPostCheck as Record<string, unknown>
: postCheck;
@@ -564,10 +603,24 @@ export default function TenantContentPage() {
<View className='admin-section'>
<Text className='admin-section-title'></Text>
{conflicts ? (
<View className='admin-row'>
<Text className='admin-row-main'> {String(conflicts.status || conflicts.syncStatus || '-')}</Text>
<Text className='admin-row-meta'> {String(conflicts.conflictCount || 0)} · {String(conflicts.lastSyncedAt || '')}</Text>
<Text className='admin-row-meta'>{JSON.stringify(conflicts).slice(0, 300)}</Text>
<View className='admin-list'>
<View className='admin-row'>
<Text className='admin-row-main'> {String(conflicts.status || conflicts.syncStatus || '-')}</Text>
<Text className='admin-row-meta'> {String(conflicts.conflictCount || 0)} · {String(conflicts.lastSyncedAt || '')}</Text>
<Text className='admin-row-meta'></Text>
</View>
{conflictItems(conflicts).slice(0, 20).map((item, index) => (
<View className='admin-row' key={`${item.sourceQuestionId || index}-${item.targetQuestionId || ''}`}>
<Text className='admin-row-main'> {String(item.sourceQuestionId || '').slice(0, 8)}</Text>
<Text className='admin-row-meta'> {String(item.targetQuestionId || '-').slice(0, 8)} · {String(item.sourceHash || '').slice(0, 10)} · {String(item.targetHash || '').slice(0, 10)}</Text>
<Text className='admin-row-meta'> {String(item.previousSourceHash || '-').slice(0, 10)}</Text>
<View className='admin-row-actions'>
<Button className='admin-mini-button primary' loading={busy === `resolve:${item.sourceQuestionId}:accept_platform`} onClick={() => resolveConflict(item, 'accept_platform')}></Button>
<Button className='admin-mini-button' loading={busy === `resolve:${item.sourceQuestionId}:keep_local`} onClick={() => resolveConflict(item, 'keep_local')}></Button>
</View>
</View>
))}
{!conflictItems(conflicts).length ? <View className='admin-empty'></View> : null}
</View>
) : <View className='admin-empty'></View>}
</View>