Files
YG_FT/frontend/src/views/data-process/create/useDataProcessRegeneration.ts
2026-07-30 16:54:00 +08:00

239 lines
9.2 KiB
TypeScript

import { computed, nextTick, ref, type Reactive, type Ref } from 'vue'
import { useRoute } from 'vue-router'
import {
getDataProcessPreview,
getDataProcessTask,
regenerateDataProcessTask,
} from '@/api/modules/dataProcess'
import type {
DataProcessPreviewItem,
DataProcessRegeneratePayload,
DataProcessTask,
} from '@/types/dataProcess'
import {
createStructuredOptionsFromConfig,
createUnstructuredOptionsFromConfig,
} from './dataProcessCreateState'
import {
loadCanonicalSourceContent,
mapDataProcessSourceFile,
} from './useDataProcessSourceUpload'
import type {
PreviewItem,
ProcessType,
StructuredProcessOptions,
UnstructuredProcessOptions,
UploadedDataFile,
} from './types'
interface ConfirmOptions {
title: string
message: string
confirmText: string
cancelText: string
tone: 'warning'
}
interface RegenerationBindings {
task: Reactive<{ name: string; description: string }>
processType: Ref<ProcessType>
structuredOptions: Ref<StructuredProcessOptions>
unstructuredOptions: Ref<UnstructuredProcessOptions>
uploadedFiles: Ref<UploadedDataFile[]>
previewItems: Ref<PreviewItem[]>
selectedPreviewFileId: Ref<string | null>
selectedPreviewId: Ref<string | null>
selectedPreviewIdsByFile: Ref<Record<string, string>>
previewSignature: Ref<string>
dirty: Ref<boolean>
buildPreviewConfigSignature: () => string
buildPreviewSignature: () => string
mapPreviewItem: (item: DataProcessPreviewItem) => PreviewItem
resetDownstream: () => void
}
async function loadAllPreviews(taskId: string, mapPreviewItem: RegenerationBindings['mapPreviewItem']) {
const first = await getDataProcessPreview(taskId, { page: 1, page_size: 500 })
const items = [...first.items]
const pages = Math.ceil(first.total / first.page_size)
for (let page = 2; page <= pages; page += 1) {
const next = await getDataProcessPreview(taskId, { page, page_size: 500 })
items.push(...next.items)
}
return items.map(mapPreviewItem)
}
export function useDataProcessRegeneration(bindings: RegenerationBindings) {
const route = useRoute()
const isRegeneration = computed(() => route.name === 'data-process-regenerate')
const isWorkflowResume = computed(() => route.name === 'data-process-workflow')
const sourceTaskId = computed(() => (
isRegeneration.value || isWorkflowResume.value ? String(route.params.id || '') : ''
))
const originalProcessType = ref<ProcessType | null>(null)
const originalTaskUpdatedAt = ref('')
const regenerationPrepared = ref(false)
const originalPreviewConfigSignature = ref('')
const confirmedPreviewConfigSignature = ref('')
const hydrating = ref(false)
const initializationError = ref('')
async function hydrateWorkspace(task: DataProcessTask, preservePreviews: boolean) {
const taskId = String(task.id)
bindings.uploadedFiles.value = await Promise.all((task.source_files || []).map(async (file) => (
mapDataProcessSourceFile(file, await loadCanonicalSourceContent(taskId, file.id))
)))
bindings.previewItems.value = preservePreviews
? await loadAllPreviews(taskId, bindings.mapPreviewItem)
: []
const configSignature = bindings.buildPreviewConfigSignature()
const previewCounts = new Map<string, number>()
for (const item of bindings.previewItems.value) {
previewCounts.set(item.sourceFileId, (previewCounts.get(item.sourceFileId) || 0) + 1)
}
for (const file of bindings.uploadedFiles.value) {
const count = previewCounts.get(String(file.sourceFileId)) || 0
file.previewCount = count
file.previewStatus = preservePreviews && count > 0 ? 'success' : 'waiting'
file.previewProgress = preservePreviews && count > 0 ? 100 : 0
file.previewConfigSignature = preservePreviews && count > 0 ? configSignature : undefined
}
bindings.selectedPreviewFileId.value = String(bindings.uploadedFiles.value[0]?.uid ?? '') || null
bindings.selectedPreviewId.value = bindings.selectedPreviewFileId.value
? bindings.previewItems.value.find((item) => (
item.sourceFileId === bindings.selectedPreviewFileId.value
))?.id ?? null
: null
bindings.selectedPreviewIdsByFile.value = (
bindings.selectedPreviewFileId.value && bindings.selectedPreviewId.value
) ? { [bindings.selectedPreviewFileId.value]: bindings.selectedPreviewId.value } : {}
bindings.previewSignature.value = preservePreviews && bindings.previewItems.value.length
? bindings.buildPreviewSignature()
: ''
bindings.resetDownstream()
}
async function loadSource() {
if (!sourceTaskId.value) return
hydrating.value = true
initializationError.value = ''
try {
const sourceTask = await getDataProcessTask(sourceTaskId.value)
const sourceType = sourceTask.process_type as ProcessType
originalProcessType.value = isRegeneration.value ? sourceType : null
originalTaskUpdatedAt.value = sourceTask.updated_at
bindings.task.name = sourceTask.name
bindings.task.description = sourceTask.description || ''
bindings.processType.value = sourceType
const config = sourceTask.config || {}
bindings.structuredOptions.value = createStructuredOptionsFromConfig(config)
bindings.unstructuredOptions.value = createUnstructuredOptionsFromConfig(config)
originalPreviewConfigSignature.value = bindings.buildPreviewConfigSignature()
confirmedPreviewConfigSignature.value = ''
await hydrateWorkspace(sourceTask, true)
await nextTick()
bindings.dirty.value = false
return sourceTask
} catch (error) {
initializationError.value = error instanceof Error
? error.message
: '原数据处理任务加载失败,请返回详情页后重试'
} finally {
hydrating.value = false
}
}
async function confirmPreviewConfigChange(
openConfirm: (options: ConfirmOptions) => Promise<boolean | undefined>,
) {
if (!isRegeneration.value) return true
if (!bindings.previewItems.value.length) return true
const currentSignature = bindings.buildPreviewConfigSignature()
const changed = currentSignature !== originalPreviewConfigSignature.value
if (!changed || currentSignature === confirmedPreviewConfigSignature.value) return true
const confirmed = await openConfirm({
title: '确认修改切分配置?',
message: '修改预处理或切分配置后,将按新配置重新切分。在点击“开始生成”前,原生成结果和已发布数据会继续保留。',
confirmText: '确认并继续',
cancelText: '返回检查',
tone: 'warning',
})
if (confirmed) confirmedPreviewConfigSignature.value = currentSignature
return Boolean(confirmed)
}
async function prepareRegeneration(
payload: Omit<DataProcessRegeneratePayload, 'expected_updated_at'>,
) {
// 第一次 prepare 后,上传、切分、编辑预览和生成都会推进任务版本。
// 再次提交前读取同一任务的最新版本,仍由后端事务处理读取后的并发竞争。
if (regenerationPrepared.value) {
const latestTask = await getDataProcessTask(sourceTaskId.value)
originalTaskUpdatedAt.value = latestTask.updated_at || originalTaskUpdatedAt.value
}
const regenerated = await regenerateDataProcessTask(sourceTaskId.value, {
...payload,
expected_updated_at: originalTaskUpdatedAt.value,
})
originalTaskUpdatedAt.value = regenerated.task.updated_at || originalTaskUpdatedAt.value
originalPreviewConfigSignature.value = bindings.buildPreviewConfigSignature()
confirmedPreviewConfigSignature.value = ''
regenerationPrepared.value = true
bindings.dirty.value = true
try {
const regeneratedTask = regenerated.task.source_files
? regenerated.task
: await getDataProcessTask(regenerated.task.id)
originalTaskUpdatedAt.value = regeneratedTask.updated_at || originalTaskUpdatedAt.value
hydrating.value = true
try {
await hydrateWorkspace(regeneratedTask, !regenerated.preview_invalidated)
} finally {
hydrating.value = false
}
} catch (error) {
initializationError.value = error instanceof Error
? `重新生成配置已保存,但工作区恢复失败:${error.message}`
: '重新生成配置已保存,但工作区恢复失败,请重试加载原任务'
throw error
}
return regenerated
}
async function confirmStartGeneration(
openConfirm: (options: ConfirmOptions) => Promise<boolean | undefined>,
syncPreviewChanges: () => Promise<void>,
) {
if (isRegeneration.value) {
const confirmed = await openConfirm({
title: '开始重新生成?',
message: '点击开始后,当前生成结果将被替换。已发布数据集会继续保留,直到重新发布。',
confirmText: '开始生成',
cancelText: '继续检查',
tone: 'warning',
})
if (!confirmed) return false
}
await syncPreviewChanges()
return true
}
return {
sourceTaskId,
isRegeneration,
isWorkflowResume,
originalProcessType,
originalTaskUpdatedAt,
regenerationPrepared,
hydrating,
initializationError,
hydrateWorkspace,
loadSource,
confirmPreviewConfigChange,
prepareRegeneration,
confirmStartGeneration,
}
}