import { computed, nextTick, ref, type Reactive, type Ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { getDataProcessPreview, getDataProcessSourceContent, getDataProcessTask, regenerateDataProcessTask, } from '@/api/modules/dataProcess' import type { DataProcessPreviewItem, DataProcessRegeneratePayload, DataProcessTask, } from '@/types/dataProcess' import { createStructuredOptionsFromConfig, createUnstructuredOptionsFromConfig, } from './dataProcessCreateState' import { 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 loadSourceContent(taskId: string, fileId: string | number) { const chunks: string[] = [] let startLine = 1 while (true) { const source = await getDataProcessSourceContent(taskId, fileId, { start_line: startLine, line_count: 10_000, }) chunks.push(source.content || '') if (!source.has_more) break const nextLine = Number(source.end_line || startLine) + 1 if (nextLine <= startLine) break startLine = nextLine } // source_content_lines 已保留原始换行;分页之间直接拼接,避免凭空增加空行并破坏偏移。 return chunks.join('') } 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 router = useRouter() const sourceTaskId = computed(() => ( route.name === 'data-process-regenerate' ? String(route.params.id || '') : '' )) const isRegeneration = computed(() => Boolean(sourceTaskId.value)) 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 loadSourceContent(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 = sourceType 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 } 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 } async function returnToDetail() { if (!sourceTaskId.value) return false await router.replace({ name: 'data-process-detail', params: { id: sourceTaskId.value } }) return true } function leaveWarning(generationStatus: string) { if (isRegeneration.value && regenerationPrepared.value && generationStatus === 'idle') { return '重新生成配置已保存,但尚未开始生成;现在返回详情不会替换原生成结果或已发布数据,可稍后继续。' } if (isRegeneration.value && regenerationPrepared.value) { return '重新生成已开始;离开页面不会停止服务端处理,已提交的修改不会撤销。' } return '当前存在未保存修改,离开后这些修改将不会保留。' } return { sourceTaskId, isRegeneration, originalProcessType, originalTaskUpdatedAt, regenerationPrepared, hydrating, initializationError, hydrateWorkspace, loadSource, confirmPreviewConfigChange, prepareRegeneration, confirmStartGeneration, returnToDetail, leaveWarning, } }