2026-07-25 22:41:06 +08:00
|
|
|
import { computed, nextTick, ref, type Reactive, type Ref } from 'vue'
|
2026-07-27 10:43:48 +08:00
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-07-25 22:41:06 +08:00
|
|
|
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<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 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()
|
2026-07-27 10:43:48 +08:00
|
|
|
const router = useRouter()
|
2026-07-25 22:41:06 +08:00
|
|
|
const sourceTaskId = computed(() => (
|
|
|
|
|
route.name === 'data-process-regenerate' ? String(route.params.id || '') : ''
|
|
|
|
|
))
|
|
|
|
|
const isRegeneration = computed(() => Boolean(sourceTaskId.value))
|
|
|
|
|
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 loadSourceContent(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 = 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<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: '确认修改切分配置?',
|
2026-07-27 10:43:48 +08:00
|
|
|
message: '修改预处理或切分配置后,将按新配置重新切分。在点击“开始生成”前,原生成结果和已发布数据会继续保留。',
|
2026-07-25 22:41:06 +08:00
|
|
|
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
|
2026-07-27 10:43:48 +08:00
|
|
|
? `重新生成配置已保存,但工作区恢复失败:${error.message}`
|
|
|
|
|
: '重新生成配置已保存,但工作区恢复失败,请重试加载原任务'
|
2026-07-25 22:41:06 +08:00
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
return regenerated
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 10:43:48 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 '当前存在未保存修改,离开后这些修改将不会保留。'
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 22:41:06 +08:00
|
|
|
return {
|
|
|
|
|
sourceTaskId,
|
|
|
|
|
isRegeneration,
|
|
|
|
|
originalProcessType,
|
|
|
|
|
originalTaskUpdatedAt,
|
|
|
|
|
regenerationPrepared,
|
|
|
|
|
hydrating,
|
|
|
|
|
initializationError,
|
|
|
|
|
hydrateWorkspace,
|
|
|
|
|
loadSource,
|
|
|
|
|
confirmPreviewConfigChange,
|
|
|
|
|
prepareRegeneration,
|
2026-07-27 10:43:48 +08:00
|
|
|
confirmStartGeneration,
|
|
|
|
|
returnToDetail,
|
|
|
|
|
leaveWarning,
|
2026-07-25 22:41:06 +08:00
|
|
|
}
|
|
|
|
|
}
|