feat: 批量导入支持记录三方 id,实现增量更新,存量覆盖

This commit is contained in:
richarjiang
2026-05-12 10:26:08 +08:00
parent e2fd091b4e
commit e0b88e68e9
5 changed files with 127 additions and 53 deletions

View File

@@ -29,6 +29,7 @@ type ItemStatus = 'pending' | 'uploading' | 'creating' | 'done' | 'error'
interface ParsedItem {
id: string // riddle_id 或文件夹名,仅用于 React key
riddleId?: string
folderName: string
// 本地文件引用
referenceFile?: File
@@ -114,7 +115,11 @@ function buildFilePathMap(files: FileList): Map<string, FileWithPath> {
}
function validateItem(item: ParsedItem) {
if (!item.answer) {
if (item.riddleId !== undefined && !item.riddleId) {
item.parseError = '未解析到 riddle_id'
} else if (item.riddleId && item.riddleId.length > 128) {
item.parseError = 'riddle_id 长度不能超过 128'
} else if (!item.answer) {
item.parseError = '未解析到 answer'
} else if (item.answer.length > ANSWER_MAX_LENGTH) {
item.parseError = `答案超过 ${ANSWER_MAX_LENGTH} 字(${item.answer.length}`
@@ -123,6 +128,20 @@ function validateItem(item: ParsedItem) {
}
}
function markDuplicateRiddleIds(items: ParsedItem[]) {
const counts = new Map<string, number>()
items.forEach((item) => {
if (!item.riddleId) return
counts.set(item.riddleId, (counts.get(item.riddleId) || 0) + 1)
})
items.forEach((item) => {
if (item.riddleId && counts.get(item.riddleId)! > 1) {
item.parseError = `riddle_id 重复: ${item.riddleId}`
}
})
}
async function parseConfigJson(
jsonFile: FileWithPath,
fileByPath: Map<string, FileWithPath>
@@ -139,16 +158,18 @@ async function parseConfigJson(
const config = raw as RiddleConfigItem
if (!config || typeof config !== 'object') return
const riddleId = (config.riddle_id || '').trim()
const referencePath = config.reference_image || ''
const riddlePath = config.riddle_image || ''
const reference = fileByPath.get(joinPath(jsonDir, referencePath)) || fileByPath.get(normalizePath(referencePath))
const riddle = fileByPath.get(joinPath(jsonDir, riddlePath)) || fileByPath.get(normalizePath(riddlePath))
const id = config.riddle_id || `${jsonFile.name}-${index + 1}`
const folderName = config.riddle_id || referencePath.split('/').slice(-2, -1)[0] || `${index + 1}`
const id = `${riddleId || jsonFile.name}-${index + 1}`
const folderName = riddleId || referencePath.split('/').slice(-2, -1)[0] || `${index + 1}`
const hints = Array.isArray(config.hints) ? config.hints : []
const item: ParsedItem = {
id,
riddleId,
folderName,
referenceFile: reference,
riddleFile: riddle,
@@ -167,6 +188,8 @@ async function parseConfigJson(
items.push(item)
})
markDuplicateRiddleIds(items)
return items
}
@@ -234,6 +257,7 @@ async function parseFolders(files: FileList): Promise<ParsedItem[]> {
const text = await metadata.text()
const json = JSON.parse(text) as RiddleMetadata
const plan = json.plan || {}
item.riddleId = (plan.riddle_id || '').trim() || undefined
item.anchorText = truncate(plan.anchor_text, 500)
item.answer = (plan.answer || '').trim()
item.punchline = (
@@ -375,9 +399,10 @@ export function BatchImportDialog({
uploadToCos(it.riddleFile, it.riddleFile.name),
])
updateItem(it.id, { status: 'creating', statusMessage: '创建关卡…' })
updateItem(it.id, { status: 'creating', statusMessage: '导入关卡…' })
const payload = {
...(it.riddleId ? { riddleId: it.riddleId } : {}),
image1Url,
image1Description: it.anchorText || '',
image2Url,
@@ -398,7 +423,7 @@ export function BatchImportDialog({
const err = await res.json().catch(() => ({}))
throw new Error(err.error || `HTTP ${res.status}`)
}
updateItem(it.id, { status: 'done', statusMessage: '已创建' })
updateItem(it.id, { status: 'done', statusMessage: '已导入' })
} catch (e) {
updateItem(it.id, {
status: 'error',
@@ -498,10 +523,10 @@ export function BatchImportDialog({
{isRunning ? (
<>
<Spinner size="sm" className="mr-2" />
</>
) : (
`确认创建 (${items.filter((i) => i.status !== 'done' && !i.parseError).length})`
`确认导入 (${items.filter((i) => i.status !== 'done' && !i.parseError).length})`
)}
</Button>
</DialogFooter>
@@ -531,7 +556,7 @@ function ItemCard({ index, item, disabled, onChange, onRemove }: ItemCardProps)
case 'done':
return (
<span className="flex items-center gap-1 text-green-600 text-xs">
<CheckCircle2 className="h-4 w-4" />
<CheckCircle2 className="h-4 w-4" /> {item.statusMessage || '已导入'}
</span>
)
case 'error':
@@ -549,7 +574,7 @@ function ItemCard({ index, item, disabled, onChange, onRemove }: ItemCardProps)
)
}
return (
<span className="text-muted-foreground text-xs"></span>
<span className="text-muted-foreground text-xs"></span>
)
}
})()