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 structuredOptions: Ref unstructuredOptions: Ref uploadedFiles: Ref previewItems: Ref selectedPreviewFileId: Ref selectedPreviewId: Ref selectedPreviewIdsByFile: Ref> previewSignature: Ref dirty: Ref 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(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() 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, ) { 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, ) { // 第一次 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, syncPreviewChanges: () => Promise, ) { 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, } }