feat: 批量导入支持记录三方 id,实现增量更新,存量覆盖
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user