2026-07-13 11:04:55 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-07-23 15:10:13 +08:00
|
|
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
2026-07-13 11:04:55 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-07-23 15:10:13 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
2026-07-13 11:04:55 +08:00
|
|
|
|
import PageCard from '@/components/PageCard.vue'
|
|
|
|
|
|
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
2026-07-23 15:10:13 +08:00
|
|
|
|
import { usePolling } from '@/composables/usePolling'
|
|
|
|
|
|
import {
|
|
|
|
|
|
getDataProcessProgress,
|
|
|
|
|
|
getDataProcessResults,
|
|
|
|
|
|
getDataProcessTask,
|
|
|
|
|
|
publishDataProcess,
|
|
|
|
|
|
restoreDataProcessResult,
|
|
|
|
|
|
updateDataProcessResult,
|
|
|
|
|
|
} from '@/api/modules/dataProcess'
|
|
|
|
|
|
import type {
|
|
|
|
|
|
DataProcessDatasetSplit,
|
|
|
|
|
|
DataProcessPublishPayload,
|
|
|
|
|
|
DataProcessResult,
|
|
|
|
|
|
DataProcessResultStatus,
|
|
|
|
|
|
DataProcessTask,
|
|
|
|
|
|
DataProcessType,
|
|
|
|
|
|
} from '@/types/dataProcess'
|
2026-07-13 11:04:55 +08:00
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const taskId = computed(() => String(route.params.id || ''))
|
|
|
|
|
|
const detail = ref<DataProcessTask | null>(null)
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const loadError = ref('')
|
|
|
|
|
|
const results = ref<DataProcessResult[]>([])
|
|
|
|
|
|
const resultTotal = ref(0)
|
|
|
|
|
|
const resultLoading = ref(false)
|
|
|
|
|
|
const resultError = ref('')
|
2026-07-13 11:04:55 +08:00
|
|
|
|
const keyword = ref('')
|
|
|
|
|
|
const statusFilter = ref('')
|
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
|
const pageSize = ref(10)
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const editingResult = ref<DataProcessResult | null>(null)
|
|
|
|
|
|
const editDialogVisible = ref(false)
|
|
|
|
|
|
const savingResult = ref(false)
|
|
|
|
|
|
const restoringResultId = ref<string | number | null>(null)
|
|
|
|
|
|
const publishDialogVisible = ref(false)
|
|
|
|
|
|
const publishing = ref(false)
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const configExpanded = ref(false)
|
2026-07-25 18:25:34 +08:00
|
|
|
|
const resultCellTooltipOptions = {
|
|
|
|
|
|
popperClass: 'data-process-result-tooltip',
|
|
|
|
|
|
placement: 'top',
|
|
|
|
|
|
showAfter: 300,
|
|
|
|
|
|
} as const
|
2026-07-23 15:10:13 +08:00
|
|
|
|
let resultFilterTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const editForm = reactive({ instruction: '', input: '', output: '' })
|
|
|
|
|
|
const publishForm = reactive<DataProcessPublishPayload>({
|
|
|
|
|
|
dataset_name: '',
|
|
|
|
|
|
dataset_type: 'train',
|
|
|
|
|
|
storage_type: 'local',
|
|
|
|
|
|
split: { train: 80, validation: 10, test: 10 },
|
|
|
|
|
|
format: 'alpaca_jsonl',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const processTypeMap: Record<DataProcessType, string> = {
|
|
|
|
|
|
structured: '结构化数据',
|
|
|
|
|
|
unstructured: '非结构化数据',
|
|
|
|
|
|
external: '外来数据源拉取',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const configLabelMap: Record<string, string> = {
|
|
|
|
|
|
preprocess_options: '预处理规则',
|
|
|
|
|
|
dataset_split: '数据集划分',
|
|
|
|
|
|
generation_model_id: '数据生成模型',
|
|
|
|
|
|
generation_prompt: '生成提示语',
|
|
|
|
|
|
temperature: '生成温度',
|
|
|
|
|
|
max_tokens: '最大输出长度',
|
|
|
|
|
|
json_mode: 'JSON 输出',
|
|
|
|
|
|
quality_filter_enabled: '质量筛选',
|
|
|
|
|
|
filter_low_quality: '过滤低质量内容',
|
|
|
|
|
|
filter_short_content: '过滤过短内容',
|
|
|
|
|
|
min_output_length: '最少输出字数',
|
|
|
|
|
|
semantic_enrichment: '语义增强',
|
|
|
|
|
|
qa_pairs_per_row: '每行生成数量',
|
|
|
|
|
|
qa_pairs_per_chunk: '每切片生成数量',
|
|
|
|
|
|
chunk_method: '切分方式',
|
|
|
|
|
|
chunk_size: '切片长度',
|
|
|
|
|
|
chunk_overlap: '重叠长度',
|
|
|
|
|
|
min_chunk_size: '最小切片长度',
|
2026-07-25 18:00:44 +08:00
|
|
|
|
semantic_breakpoint_percentile: '语义断点百分位',
|
2026-07-23 15:10:13 +08:00
|
|
|
|
preserve_tables: '保留表格',
|
|
|
|
|
|
preserve_code_blocks: '保留代码块',
|
|
|
|
|
|
preserve_lists: '保留列表',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const chunkMethodLabelMap: Record<string, string> = {
|
2026-07-25 18:00:44 +08:00
|
|
|
|
layout_hybrid: '版面结构混合切分',
|
|
|
|
|
|
semantic: '语义切分',
|
2026-07-24 11:28:02 +08:00
|
|
|
|
fixed: '固定 Token',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const preprocessOptionLabelMap: Record<string, string> = {
|
|
|
|
|
|
clean_invalid: '清理无效数据',
|
|
|
|
|
|
detect_structure: '识别表格结构',
|
|
|
|
|
|
deduplicate: '重复数据去重',
|
|
|
|
|
|
normalize_format: '数据格式标准化',
|
|
|
|
|
|
filter_anomaly: '异常数据过滤',
|
|
|
|
|
|
desensitize: '敏感信息脱敏',
|
|
|
|
|
|
clean_invalid_content: '清理无效内容',
|
|
|
|
|
|
detect_document_structure: '识别文档结构',
|
|
|
|
|
|
merge_short_content: '合并过短内容',
|
|
|
|
|
|
filter_low_quality: '过滤低质量内容',
|
|
|
|
|
|
deduplicate_content: '重复内容去重',
|
|
|
|
|
|
preserve_context: '保留上下文',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function numeric(value: unknown) {
|
|
|
|
|
|
const parsed = typeof value === 'number' ? value : Number(value)
|
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : 0
|
2026-07-23 15:10:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value?: string | null) {
|
|
|
|
|
|
if (!value) return '-'
|
|
|
|
|
|
const date = new Date(value)
|
|
|
|
|
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
|
|
|
|
|
|
}
|
2026-07-13 11:04:55 +08:00
|
|
|
|
|
|
|
|
|
|
const retentionRate = computed(() => {
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const outputCount = numeric(detail.value?.output_count)
|
|
|
|
|
|
const denominator = outputCount + numeric(detail.value?.filtered_count)
|
|
|
|
|
|
return denominator
|
|
|
|
|
|
? Number(((outputCount / denominator) * 100).toFixed(1))
|
2026-07-23 15:10:13 +08:00
|
|
|
|
: 0
|
2026-07-13 11:04:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const retentionDenominator = computed(() => (
|
|
|
|
|
|
numeric(detail.value?.output_count) + numeric(detail.value?.filtered_count)
|
|
|
|
|
|
))
|
|
|
|
|
|
const retentionPercentage = computed(() => Math.min(100, Math.max(0, retentionRate.value)))
|
|
|
|
|
|
const progressPercentage = computed(() => (
|
|
|
|
|
|
detail.value?.status === 'completed'
|
|
|
|
|
|
? 100
|
|
|
|
|
|
: Math.min(100, Math.max(0, numeric(detail.value?.progress)))
|
|
|
|
|
|
))
|
|
|
|
|
|
const isUnstructured = computed(() => detail.value?.process_type === 'unstructured')
|
|
|
|
|
|
const sourceFileCount = computed(() => (
|
|
|
|
|
|
numeric(detail.value?.source_file_count) || detail.value?.source_files?.length || 0
|
|
|
|
|
|
))
|
|
|
|
|
|
const previewCount = computed(() => numeric(detail.value?.preview_count))
|
|
|
|
|
|
const inputMetricLabel = computed(() => isUnstructured.value ? '输入切片' : '输入记录')
|
|
|
|
|
|
const inputMetricCount = computed(() => (
|
|
|
|
|
|
isUnstructured.value ? previewCount.value : numeric(detail.value?.input_count)
|
|
|
|
|
|
))
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const sourceDatasetName = computed(() => (
|
|
|
|
|
|
detail.value?.source_dataset_name
|
|
|
|
|
|
|| detail.value?.source_dataset
|
|
|
|
|
|
|| detail.value?.source_files?.map((file) => file.name).join('、')
|
|
|
|
|
|
|| '源文件上传'
|
|
|
|
|
|
))
|
2026-07-25 17:04:21 +08:00
|
|
|
|
const outputDatasets = computed(() => {
|
|
|
|
|
|
const items = detail.value?.output_datasets || []
|
|
|
|
|
|
if (items.length) return items
|
|
|
|
|
|
if (!detail.value?.output_dataset_id) return []
|
|
|
|
|
|
return [{
|
|
|
|
|
|
id: detail.value.output_dataset_id,
|
|
|
|
|
|
name: detail.value.output_dataset_name || detail.value.output_dataset || '训练集',
|
|
|
|
|
|
type: 'train',
|
|
|
|
|
|
}]
|
|
|
|
|
|
})
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const outputDatasetName = computed(() => (
|
2026-07-25 17:04:21 +08:00
|
|
|
|
outputDatasets.value.map((item) => item.name).join('、')
|
2026-07-23 15:10:13 +08:00
|
|
|
|
))
|
2026-07-25 17:04:21 +08:00
|
|
|
|
const outputDatasetBaseName = computed(() => {
|
|
|
|
|
|
const name = String(outputDatasets.value[0]?.name || '').trim()
|
|
|
|
|
|
return name.replace(/-(?:训练集|验证集|测试集)$/, '')
|
|
|
|
|
|
})
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const outputDatasetId = computed(() => detail.value?.output_dataset_id || null)
|
|
|
|
|
|
const creatorName = computed(() => detail.value?.creator_name || detail.value?.creator || '-')
|
|
|
|
|
|
const createTime = computed(() => detail.value?.create_time || detail.value?.created_at)
|
|
|
|
|
|
const startTime = computed(() => detail.value?.start_time || detail.value?.started_at)
|
|
|
|
|
|
const completeTime = computed(() => detail.value?.complete_time || detail.value?.completed_at)
|
|
|
|
|
|
|
|
|
|
|
|
const durationText = computed(() => {
|
|
|
|
|
|
if (detail.value?.duration) return detail.value.duration
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const providedDuration = detail.value?.duration_seconds
|
|
|
|
|
|
let seconds = providedDuration == null ? null : numeric(providedDuration)
|
|
|
|
|
|
if (seconds == null && startTime.value) {
|
|
|
|
|
|
const started = new Date(startTime.value).getTime()
|
|
|
|
|
|
const finished = completeTime.value
|
|
|
|
|
|
? new Date(completeTime.value).getTime()
|
|
|
|
|
|
: detail.value?.status === 'running' ? Date.now() : Number.NaN
|
|
|
|
|
|
if (Number.isFinite(started) && Number.isFinite(finished) && finished >= started) {
|
|
|
|
|
|
seconds = (finished - started) / 1000
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (seconds == null) return detail.value?.status === 'running' ? '处理中' : '-'
|
|
|
|
|
|
const safeSeconds = Math.max(0, Math.round(seconds))
|
|
|
|
|
|
const hours = Math.floor(safeSeconds / 3600)
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const minutes = Math.floor(safeSeconds / 60)
|
|
|
|
|
|
const restSeconds = safeSeconds % 60
|
2026-07-24 16:30:29 +08:00
|
|
|
|
if (hours) return `${hours} 小时 ${minutes % 60} 分 ${restSeconds} 秒`
|
2026-07-23 15:10:13 +08:00
|
|
|
|
return minutes ? `${minutes} 分 ${restSeconds} 秒` : `${restSeconds} 秒`
|
2026-07-13 11:04:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const configRows = computed(() => Object.entries(detail.value?.config || {})
|
2026-07-24 16:30:29 +08:00
|
|
|
|
.filter(([key]) => (
|
|
|
|
|
|
key !== 'generation_model_snapshot'
|
|
|
|
|
|
&& !/(?:password|secret|token|api_key)/i.test(key)
|
|
|
|
|
|
))
|
2026-07-23 15:10:13 +08:00
|
|
|
|
.map(([key, value]) => ({
|
|
|
|
|
|
label: configLabelMap[key] || key.split('_').join(' '),
|
|
|
|
|
|
value: formatConfigValue(key, value),
|
|
|
|
|
|
})))
|
|
|
|
|
|
|
|
|
|
|
|
function formatConfigValue(key: string, value: unknown) {
|
2026-07-24 16:30:29 +08:00
|
|
|
|
if (key === 'generation_model_id') {
|
|
|
|
|
|
const snapshot = detail.value?.config?.generation_model_snapshot
|
|
|
|
|
|
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
|
|
|
|
|
const model = snapshot as Record<string, unknown>
|
|
|
|
|
|
return String(model.name || model.display_name || model.model_name || value || '-')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (key === 'chunk_method' && typeof value === 'string') {
|
|
|
|
|
|
return chunkMethodLabelMap[value] || value
|
|
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (key === 'dataset_split' && value && typeof value === 'object') {
|
|
|
|
|
|
const split = value as Partial<DataProcessDatasetSplit>
|
|
|
|
|
|
return `训练集 ${split.train ?? 0}% / 验证集 ${split.validation ?? 0}% / 测试集 ${split.test ?? 0}%`
|
|
|
|
|
|
}
|
2026-07-24 16:30:29 +08:00
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
|
if (key === 'preprocess_options') {
|
|
|
|
|
|
return value.length
|
|
|
|
|
|
? value.map((item) => preprocessOptionLabelMap[String(item)] || String(item)).join('、')
|
|
|
|
|
|
: '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
return value.length ? value.join('、') : '-'
|
|
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (typeof value === 'boolean') return value ? '是' : '否'
|
|
|
|
|
|
if (value && typeof value === 'object') return JSON.stringify(value)
|
|
|
|
|
|
return value == null || value === '' ? '-' : String(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function processTypeLabel(value?: DataProcessType) {
|
|
|
|
|
|
return value ? processTypeMap[value] || value : '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isActiveStatus(status?: DataProcessTask['status']) {
|
|
|
|
|
|
return status === 'running'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadTask(silent = false) {
|
|
|
|
|
|
if (!silent) loading.value = true
|
|
|
|
|
|
loadError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
detail.value = await getDataProcessTask(taskId.value)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
detail.value = null
|
|
|
|
|
|
loadError.value = '数据处理任务加载失败,任务可能已删除或当前无权访问。'
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!silent) loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadResults() {
|
|
|
|
|
|
resultLoading.value = true
|
|
|
|
|
|
resultError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getDataProcessResults(taskId.value, {
|
|
|
|
|
|
page: currentPage.value,
|
|
|
|
|
|
page_size: pageSize.value,
|
|
|
|
|
|
keyword: keyword.value.trim() || undefined,
|
|
|
|
|
|
status: statusFilter.value || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
results.value = response.items
|
|
|
|
|
|
resultTotal.value = response.total
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
results.value = []
|
|
|
|
|
|
resultTotal.value = 0
|
|
|
|
|
|
resultError.value = '结果明细加载失败,请稍后重试。'
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
resultLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPage() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
await loadTask(true)
|
|
|
|
|
|
if (detail.value) {
|
|
|
|
|
|
await loadResults()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
results.value = []
|
|
|
|
|
|
resultTotal.value = 0
|
|
|
|
|
|
resultError.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
if (isActiveStatus(detail.value?.status)) startPolling()
|
|
|
|
|
|
else stopPolling()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshRuntime() {
|
|
|
|
|
|
if (!detail.value || !isActiveStatus(detail.value.status)) {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const progress = await getDataProcessProgress(taskId.value)
|
|
|
|
|
|
detail.value = {
|
|
|
|
|
|
...detail.value,
|
|
|
|
|
|
status: progress.status,
|
|
|
|
|
|
progress: progress.progress,
|
|
|
|
|
|
input_count: progress.input_count ?? detail.value.input_count,
|
|
|
|
|
|
output_count: progress.output_count ?? detail.value.output_count,
|
|
|
|
|
|
filtered_count: progress.filtered_count ?? detail.value.filtered_count,
|
|
|
|
|
|
duplicate_count: progress.duplicate_count ?? detail.value.duplicate_count,
|
|
|
|
|
|
error_count: progress.error_count ?? detail.value.error_count,
|
|
|
|
|
|
failure_reason: progress.failure_reason ?? detail.value.failure_reason,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isActiveStatus(progress.status)) {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
await Promise.all([loadTask(true), loadResults()])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { start: startPolling, stop: stopPolling } = usePolling(refreshRuntime, 3000, {
|
|
|
|
|
|
immediate: false,
|
2026-07-13 11:04:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
function scheduleResultReload() {
|
|
|
|
|
|
if (currentPage.value !== 1) {
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resultFilterTimer) clearTimeout(resultFilterTimer)
|
|
|
|
|
|
resultFilterTimer = setTimeout(() => void loadResults(), 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resultStatusLabel(status: DataProcessResultStatus) {
|
2026-07-13 11:04:55 +08:00
|
|
|
|
return status === 'valid' ? '有效' : status === 'modified' ? '已修改' : '无效'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
function resultStatusType(status: DataProcessResultStatus) {
|
2026-07-13 11:04:55 +08:00
|
|
|
|
return status === 'valid' ? 'success' : status === 'modified' ? 'warning' : 'danger'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
function qualityScoreLabel(value: DataProcessResult['quality_score']) {
|
|
|
|
|
|
if (value == null) return '-'
|
|
|
|
|
|
const score = value.overall
|
|
|
|
|
|
return Number.isFinite(score) ? Number(score).toFixed(1) : '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function qualityFlagsLabel(value: DataProcessResult['quality_score']) {
|
|
|
|
|
|
return value?.flags?.length ? value.flags.join('、') : '未命中质量规则'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function replaceResult(updated: DataProcessResult) {
|
|
|
|
|
|
const index = results.value.findIndex((item) => item.id === updated.id)
|
|
|
|
|
|
if (index >= 0) results.value.splice(index, 1, updated)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openResultEditor(result: DataProcessResult) {
|
|
|
|
|
|
editingResult.value = result
|
|
|
|
|
|
Object.assign(editForm, {
|
|
|
|
|
|
instruction: result.instruction,
|
|
|
|
|
|
input: result.input,
|
|
|
|
|
|
output: result.output,
|
|
|
|
|
|
})
|
|
|
|
|
|
editDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveResult() {
|
|
|
|
|
|
if (!editingResult.value) return
|
|
|
|
|
|
if (!editForm.instruction.trim() || !editForm.output.trim()) {
|
|
|
|
|
|
ElMessage.warning('Instruction 和 Output 不能为空')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
savingResult.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await updateDataProcessResult(taskId.value, editingResult.value.id, {
|
|
|
|
|
|
instruction: editForm.instruction,
|
|
|
|
|
|
input: editForm.input,
|
|
|
|
|
|
output: editForm.output,
|
|
|
|
|
|
expected_updated_at: editingResult.value.updated_at,
|
|
|
|
|
|
})
|
|
|
|
|
|
replaceResult(updated)
|
|
|
|
|
|
await loadTask(true)
|
|
|
|
|
|
editDialogVisible.value = false
|
|
|
|
|
|
ElMessage.success('结果已保存')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 统一请求层已展示后端返回的失败原因。
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
savingResult.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function restoreResult(result: DataProcessResult) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm('确定恢复为模型最初生成的内容吗?', '恢复生成结果', {
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
confirmButtonText: '恢复',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restoringResultId.value = result.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const restored = await restoreDataProcessResult(taskId.value, result.id)
|
|
|
|
|
|
replaceResult(restored)
|
|
|
|
|
|
await loadTask(true)
|
|
|
|
|
|
ElMessage.success('已恢复生成结果')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 统一请求层已展示后端返回的失败原因。
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
restoringResultId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function configuredSplit(): DataProcessDatasetSplit {
|
|
|
|
|
|
const split = detail.value?.config?.dataset_split
|
|
|
|
|
|
if (!split || typeof split !== 'object') return { train: 80, validation: 10, test: 10 }
|
|
|
|
|
|
const value = split as Partial<DataProcessDatasetSplit>
|
|
|
|
|
|
return {
|
|
|
|
|
|
train: Number(value.train) || 0,
|
|
|
|
|
|
validation: Number(value.validation) || 0,
|
|
|
|
|
|
test: Number(value.test) || 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPublishDialog() {
|
|
|
|
|
|
if (!detail.value) return
|
|
|
|
|
|
if (outputDatasetId.value) {
|
2026-07-25 17:04:21 +08:00
|
|
|
|
void router.push({ path: '/dataset', query: { tab: 'task', task_id: taskId.value } })
|
2026-07-23 15:10:13 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
publishForm.dataset_name = `${detail.value.name}-数据集`
|
|
|
|
|
|
publishForm.split = configuredSplit()
|
|
|
|
|
|
publishDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function publishDataset() {
|
|
|
|
|
|
if (!publishForm.dataset_name.trim()) {
|
|
|
|
|
|
ElMessage.warning('请输入数据集名称')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
publishing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const published = await publishDataProcess(taskId.value, {
|
|
|
|
|
|
...publishForm,
|
|
|
|
|
|
dataset_name: publishForm.dataset_name.trim(),
|
|
|
|
|
|
split: { ...publishForm.split },
|
|
|
|
|
|
})
|
2026-07-25 17:04:21 +08:00
|
|
|
|
const datasets = published.datasets || published.output_datasets || []
|
|
|
|
|
|
ElMessage.success(`已发布 ${datasets.length || 3} 个独立数据集`)
|
2026-07-23 15:10:13 +08:00
|
|
|
|
publishDialogVisible.value = false
|
2026-07-25 17:04:21 +08:00
|
|
|
|
await router.push({ path: '/dataset', query: { tab: 'task', task_id: taskId.value } })
|
2026-07-23 15:10:13 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// 统一请求层已展示后端返回的失败原因。
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
publishing.value = false
|
|
|
|
|
|
}
|
2026-07-13 11:04:55 +08:00
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
|
2026-07-24 21:11:01 +08:00
|
|
|
|
async function resyncPublishedDataset() {
|
|
|
|
|
|
if (!detail.value || !outputDatasetId.value || publishing.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(
|
2026-07-25 17:04:21 +08:00
|
|
|
|
'将按当前 8:1:1 配置同步为训练集、验证集、测试集三个独立数据集。',
|
|
|
|
|
|
'重新同步三个数据集?',
|
2026-07-24 21:11:01 +08:00
|
|
|
|
{ type: 'warning', confirmButtonText: '重新同步', cancelButtonText: '取消' },
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
publishing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const published = await publishDataProcess(taskId.value, {
|
|
|
|
|
|
...publishForm,
|
2026-07-25 17:04:21 +08:00
|
|
|
|
dataset_name: outputDatasetBaseName.value || `${detail.value.name}-数据集`,
|
2026-07-24 21:11:01 +08:00
|
|
|
|
split: configuredSplit(),
|
|
|
|
|
|
})
|
2026-07-25 17:04:21 +08:00
|
|
|
|
const datasets = published.datasets || published.output_datasets || []
|
|
|
|
|
|
ElMessage.success(`训练集、验证集和测试集已同步,共 ${datasets.length || 3} 个数据集`)
|
|
|
|
|
|
await router.push({ path: '/dataset', query: { tab: 'task', task_id: taskId.value } })
|
2026-07-24 21:11:01 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
publishing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
watch([currentPage, pageSize], () => void loadResults())
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadPage)
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
if (resultFilterTimer) clearTimeout(resultFilterTimer)
|
|
|
|
|
|
})
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<PageCard class="data-process-detail-page" v-loading="loading">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<template v-if="detail" #header>
|
|
|
|
|
|
<div class="detail-heading">
|
|
|
|
|
|
<div class="heading-row">
|
|
|
|
|
|
<h1>{{ detail.name }}</h1>
|
|
|
|
|
|
<ModelStatusTag :status="detail.status" />
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="detail.status === 'completed'"
|
|
|
|
|
|
class="publish-button"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="openPublishDialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="fa" :class="outputDatasetId ? 'fa-external-link' : 'fa-database'" />
|
2026-07-25 17:04:21 +08:00
|
|
|
|
{{ outputDatasetId ? '查看三个数据集' : '发布为三个数据集' }}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
</el-button>
|
2026-07-24 21:11:01 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="detail.status === 'completed' && outputDatasetId"
|
|
|
|
|
|
:loading="publishing"
|
|
|
|
|
|
@click="resyncPublishedDataset"
|
|
|
|
|
|
>
|
2026-07-25 17:04:21 +08:00
|
|
|
|
<i class="fa fa-refresh" />重新同步数据集
|
2026-07-24 21:11:01 +08:00
|
|
|
|
</el-button>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<p>{{ detail.description || '暂无任务描述' }}</p>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<dl class="heading-meta">
|
|
|
|
|
|
<div><dt>任务 ID</dt><dd>{{ detail.id }}</dd></div>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div><dt>处理类型</dt><dd>{{ processTypeLabel(detail.process_type) }}</dd></div>
|
|
|
|
|
|
<div><dt>创建人</dt><dd>{{ creatorName }}</dd></div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</dl>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div v-if="loadError" class="not-found-state" role="alert">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<i class="fa fa-exclamation-circle" aria-hidden="true" />
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<h2>无法加载数据处理任务</h2>
|
|
|
|
|
|
<p>{{ loadError }}</p>
|
|
|
|
|
|
<div class="load-state-actions">
|
|
|
|
|
|
<el-button @click="router.push('/data-process')">返回任务列表</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="loadPage">重新加载</el-button>
|
|
|
|
|
|
</div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<template v-else-if="detail">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<section class="metric-grid" aria-label="处理结果概览">
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span>处理耗时</span>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<strong>{{ durationText }}</strong>
|
|
|
|
|
|
<small>{{ completeTime ? `完成于 ${formatDateTime(completeTime)}` : `当前进度 ${progressPercentage}%` }}</small>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<span>{{ inputMetricLabel }}</span>
|
|
|
|
|
|
<strong>{{ inputMetricCount.toLocaleString() }}</strong>
|
|
|
|
|
|
<small>{{ sourceFileCount ? `源文件 ${sourceFileCount} 个:` : '来源:' }}{{ sourceDatasetName }}</small>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span>输出结果</span>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<strong>{{ numeric(detail.output_count).toLocaleString() }}</strong>
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<small>{{ outputDatasetName || '尚未发布为数据集' }}</small>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card is-primary">
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<span>结果保留率</span>
|
|
|
|
|
|
<strong>{{ retentionDenominator ? `${retentionRate}%` : '-' }}</strong>
|
|
|
|
|
|
<el-progress :percentage="retentionPercentage" :show-text="false" :stroke-width="5" />
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div v-if="detail.failure_reason" class="failure-alert" role="alert">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<i class="fa fa-exclamation-triangle" aria-hidden="true" />
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div><strong>处理任务执行失败</strong><p>{{ detail.failure_reason }}</p></div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-grid">
|
|
|
|
|
|
<section class="detail-section" aria-labelledby="runtime-title">
|
|
|
|
|
|
<div class="section-heading">
|
|
|
|
|
|
<div><h2 id="runtime-title">运行信息</h2><p>查看任务执行时间与数据流向</p></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<dl class="info-list">
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div><dt>创建时间</dt><dd>{{ formatDateTime(createTime) }}</dd></div>
|
|
|
|
|
|
<div><dt>开始时间</dt><dd>{{ formatDateTime(startTime) }}</dd></div>
|
|
|
|
|
|
<div><dt>完成时间</dt><dd>{{ formatDateTime(completeTime) }}</dd></div>
|
|
|
|
|
|
<div><dt>处理耗时</dt><dd>{{ durationText }}</dd></div>
|
|
|
|
|
|
<div><dt>源数据集</dt><dd>{{ sourceDatasetName }}</dd></div>
|
2026-07-25 21:55:16 +08:00
|
|
|
|
<div class="output-dataset-row">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<dt>输出数据集</dt>
|
2026-07-25 21:55:16 +08:00
|
|
|
|
<dd class="output-dataset-links">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<el-button
|
2026-07-25 17:04:21 +08:00
|
|
|
|
v-for="dataset in outputDatasets"
|
|
|
|
|
|
:key="String(dataset.id)"
|
2026-07-13 11:04:55 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
link
|
2026-07-25 17:04:21 +08:00
|
|
|
|
@click="router.push(`/dataset/${dataset.id}/preview`)"
|
|
|
|
|
|
>{{ dataset.name }}({{ dataset.count || 0 }} 条)<i class="fa fa-external-link" /></el-button>
|
|
|
|
|
|
<span v-if="!outputDatasets.length">尚未发布为数据集</span>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="detail-section" aria-labelledby="statistics-title">
|
|
|
|
|
|
<div class="section-heading">
|
|
|
|
|
|
<div><h2 id="statistics-title">处理统计</h2><p>查看数据清洗、过滤和输出情况</p></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="statistics-grid">
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<div><span>源文件</span><strong>{{ sourceFileCount.toLocaleString() }}</strong></div>
|
|
|
|
|
|
<div><span>{{ inputMetricLabel }}</span><strong>{{ inputMetricCount.toLocaleString() }}</strong></div>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div><span>成功输出</span><strong>{{ numeric(detail.output_count).toLocaleString() }}</strong></div>
|
|
|
|
|
|
<div><span>过滤数据</span><strong>{{ numeric(detail.filtered_count).toLocaleString() }}</strong></div>
|
|
|
|
|
|
<div><span>重复数据</span><strong>{{ numeric(detail.duplicate_count).toLocaleString() }}</strong></div>
|
|
|
|
|
|
<div><span>异常数据</span><strong>{{ numeric(detail.error_count).toLocaleString() }}</strong></div>
|
|
|
|
|
|
<div><span>执行进度</span><strong>{{ progressPercentage }}%</strong></div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="detail-section config-section" aria-labelledby="config-title">
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<div class="section-heading config-heading">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<div><h2 id="config-title">处理配置</h2><p>任务执行时使用的规则与参数</p></div>
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<div class="config-actions">
|
|
|
|
|
|
<span>共 {{ configRows.length }} 项</span>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
text
|
|
|
|
|
|
:aria-expanded="configExpanded"
|
|
|
|
|
|
aria-controls="config-content"
|
|
|
|
|
|
@click="configExpanded = !configExpanded"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ configExpanded ? '收起' : '展开' }}
|
|
|
|
|
|
<i class="fa" :class="configExpanded ? 'fa-chevron-up' : 'fa-chevron-down'" />
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<el-collapse-transition>
|
|
|
|
|
|
<div v-show="configExpanded" id="config-content" class="config-content">
|
|
|
|
|
|
<dl v-if="configRows.length" class="config-grid">
|
|
|
|
|
|
<div v-for="item in configRows" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
<div v-else class="compact-empty">暂无处理配置</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-collapse-transition>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<section class="detail-section result-section" aria-labelledby="result-title" v-loading="resultLoading">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<div class="result-toolbar">
|
|
|
|
|
|
<div class="section-heading">
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div><h2 id="result-title">结果明细</h2><p>查看、编辑或恢复处理结果</p></div>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div class="result-filters">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="keyword"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
placeholder="搜索指令、输入或输出"
|
|
|
|
|
|
@input="scheduleResultReload"
|
|
|
|
|
|
@clear="scheduleResultReload"
|
|
|
|
|
|
>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<template #prefix><i class="fa fa-search" aria-hidden="true" /></template>
|
|
|
|
|
|
</el-input>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-select v-model="statusFilter" clearable placeholder="全部状态" @change="scheduleResultReload">
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<el-option label="有效" value="valid" />
|
|
|
|
|
|
<el-option label="已修改" value="modified" />
|
|
|
|
|
|
<el-option label="无效" value="invalid" />
|
|
|
|
|
|
</el-select>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-button :loading="resultLoading" @click="loadResults"><i class="fa fa-refresh" /></el-button>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-25 18:25:34 +08:00
|
|
|
|
<el-table
|
|
|
|
|
|
v-if="results.length"
|
|
|
|
|
|
:data="results"
|
|
|
|
|
|
:tooltip-options="resultCellTooltipOptions"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
table-layout="fixed"
|
|
|
|
|
|
>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<el-table-column type="index" label="#" width="56" align="center" />
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-table-column label="指令" min-width="190" show-overflow-tooltip prop="instruction" />
|
|
|
|
|
|
<el-table-column label="输入" min-width="160" show-overflow-tooltip prop="input" />
|
|
|
|
|
|
<el-table-column label="输出" min-width="230" show-overflow-tooltip prop="output" />
|
|
|
|
|
|
<el-table-column label="质量分" width="88" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tooltip :content="qualityFlagsLabel((row as DataProcessResult).quality_score)">
|
|
|
|
|
|
<span>{{ qualityScoreLabel((row as DataProcessResult).quality_score) }}</span>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<el-table-column label="状态" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag :type="resultStatusType(row.status)" size="small">{{ resultStatusLabel(row.status) }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-table-column label="操作" width="130" align="center" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
link
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:disabled="Boolean(outputDatasetId)"
|
|
|
|
|
|
@click="openResultEditor(row as DataProcessResult)"
|
|
|
|
|
|
>编辑</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
link
|
|
|
|
|
|
:disabled="Boolean(outputDatasetId)"
|
|
|
|
|
|
:loading="restoringResultId === row.id"
|
|
|
|
|
|
@click="restoreResult(row as DataProcessResult)"
|
|
|
|
|
|
>恢复</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="result-empty">
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<i class="fa" :class="resultError || detail.status === 'failed' ? 'fa-exclamation-circle' : 'fa-hourglass-half'" aria-hidden="true" />
|
|
|
|
|
|
<strong>{{ resultError || (detail.status === 'completed' ? '没有符合条件的结果' : detail.status === 'failed' ? '任务失败,未生成结果明细' : '处理完成后将在这里展示结果明细') }}</strong>
|
|
|
|
|
|
<span v-if="!resultError">{{ detail.status === 'completed' ? '请调整搜索或筛选条件' : `当前任务状态:${detail.status === 'running' ? '运行中' : '等待中'}` }}</span>
|
|
|
|
|
|
<el-button v-else type="primary" link @click="loadResults">重新加载</el-button>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div v-if="resultTotal" class="result-pagination">
|
|
|
|
|
|
<span>共 {{ resultTotal }} 条结果</span>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-model:current-page="currentPage"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
2026-07-23 15:10:13 +08:00
|
|
|
|
layout="sizes, prev, pager, next"
|
|
|
|
|
|
:page-sizes="[10, 20, 50]"
|
|
|
|
|
|
:total="resultTotal"
|
2026-07-13 11:04:55 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</PageCard>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="editDialogVisible" title="编辑处理结果" width="680px" destroy-on-close>
|
|
|
|
|
|
<el-form label-position="top">
|
|
|
|
|
|
<el-form-item label="Instruction" required>
|
|
|
|
|
|
<el-input v-model="editForm.instruction" type="textarea" :rows="3" maxlength="4000" show-word-limit />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="Input">
|
|
|
|
|
|
<el-input v-model="editForm.input" type="textarea" :rows="3" maxlength="10000" show-word-limit />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="Output" required>
|
|
|
|
|
|
<el-input v-model="editForm.output" type="textarea" :rows="6" maxlength="20000" show-word-limit />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="savingResult" @click="saveResult">保存</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2026-07-25 17:04:21 +08:00
|
|
|
|
<el-dialog v-model="publishDialogVisible" title="发布为三个数据集" width="560px" destroy-on-close>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-form label-position="top">
|
|
|
|
|
|
<el-form-item label="数据集名称" required>
|
|
|
|
|
|
<el-input v-model="publishForm.dataset_name" maxlength="150" show-word-limit />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<div class="publish-form-grid">
|
|
|
|
|
|
<el-form-item label="存储位置">
|
|
|
|
|
|
<el-select v-model="publishForm.storage_type">
|
|
|
|
|
|
<el-option label="平台本地存储" value="local" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="输出格式">
|
|
|
|
|
|
<el-select v-model="publishForm.format">
|
|
|
|
|
|
<el-option label="Alpaca JSONL" value="alpaca_jsonl" />
|
|
|
|
|
|
<el-option label="JSONL" value="jsonl" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
2026-07-25 17:04:21 +08:00
|
|
|
|
:title="`将发布三个独立数据集:训练集 ${publishForm.split.train}% / 验证集 ${publishForm.split.validation}% / 测试集 ${publishForm.split.test}%`"
|
2026-07-23 15:10:13 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="publishDialogVisible = false">取消</el-button>
|
2026-07-25 17:04:21 +08:00
|
|
|
|
<el-button type="primary" :loading="publishing" @click="publishDataset">发布三个数据集</el-button>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-07-13 11:04:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.data-process-detail-page { width: 100%; min-width: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.detail-heading {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
.heading-row { display: flex; align-items: center; gap: 12px; }
|
|
|
|
|
|
h1 { margin: 0; color: #1f2937; font-size: 20px; font-weight: 600; }
|
|
|
|
|
|
> p { margin: 8px 0 0; color: #64748b; font-size: 13px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
.publish-button { margin-left: auto; }
|
|
|
|
|
|
.load-state-actions { display: flex; gap: 10px; }
|
|
|
|
|
|
.compact-empty { padding: 28px 18px; color: #94a3b8; font-size: 13px; text-align: center; }
|
|
|
|
|
|
.publish-form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
|
|
|
|
.publish-form-grid :deep(.el-select) { width: 100%; }
|
|
|
|
|
|
|
2026-07-13 11:04:55 +08:00
|
|
|
|
.heading-meta {
|
|
|
|
|
|
margin: 16px 0 0; display: flex; flex-wrap: wrap; gap: 10px 30px;
|
|
|
|
|
|
div { display: flex; gap: 7px; font-size: 12px; }
|
|
|
|
|
|
dt { color: #94a3b8; }
|
|
|
|
|
|
dd { margin: 0; color: #475569; font-weight: 500; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
|
|
|
|
|
|
.metric-card {
|
|
|
|
|
|
min-height: 116px; padding: 18px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff;
|
|
|
|
|
|
display: flex; flex-direction: column;
|
|
|
|
|
|
> span { color: #64748b; font-size: 12px; }
|
|
|
|
|
|
> strong { margin-top: 10px; color: #1f2937; font-size: 22px; font-weight: 600; }
|
|
|
|
|
|
> small { margin-top: auto; color: #94a3b8; font-size: 12px; }
|
|
|
|
|
|
:deep(.el-progress) { margin-top: auto; }
|
|
|
|
|
|
&.is-primary { border-color: var(--el-color-primary-light-7); background: var(--el-color-primary-light-9); }
|
|
|
|
|
|
&.is-primary > strong { color: var(--primary-color); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.failure-alert {
|
|
|
|
|
|
margin-top: 16px; padding: 14px 16px; border: 1px solid #fecaca; border-radius: 8px; background: #fef2f2;
|
|
|
|
|
|
color: #b91c1c; display: flex; gap: 12px;
|
|
|
|
|
|
> i { margin-top: 2px; }
|
|
|
|
|
|
strong { font-size: 13px; }
|
|
|
|
|
|
p { margin: 4px 0 0; color: #7f1d1d; font-size: 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-grid { margin-top: 16px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
|
|
|
|
|
.detail-section { border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; overflow: hidden; }
|
|
|
|
|
|
.section-heading {
|
|
|
|
|
|
padding: 16px 18px; border-bottom: 1px solid #eef0f3;
|
|
|
|
|
|
h2 { margin: 0; color: #1f2937; font-size: 15px; font-weight: 600; }
|
|
|
|
|
|
p { margin: 4px 0 0; color: #94a3b8; font-size: 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 16:30:29 +08:00
|
|
|
|
.config-heading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
> span { color: #94a3b8; font-size: 12px; }
|
|
|
|
|
|
:deep(.el-button) { gap: 6px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:04:55 +08:00
|
|
|
|
.info-list { margin: 0; padding: 4px 18px 12px; }
|
|
|
|
|
|
.info-list > div {
|
|
|
|
|
|
min-height: 44px; border-bottom: 1px solid #f1f5f9; display: grid; grid-template-columns: 100px minmax(0, 1fr); align-items: center;
|
|
|
|
|
|
&:last-child { border-bottom: 0; }
|
|
|
|
|
|
dt { color: #64748b; font-size: 12px; }
|
|
|
|
|
|
dd { margin: 0; color: #334155; font-size: 13px; text-align: right; }
|
|
|
|
|
|
:deep(.el-button) { height: auto; padding: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 21:55:16 +08:00
|
|
|
|
.info-list > .output-dataset-row {
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
|
|
|
|
|
|
dt { padding-top: 2px; line-height: 24px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.output-dataset-links {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-button) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
line-height: 24px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:04:55 +08:00
|
|
|
|
.statistics-grid { padding: 18px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
|
|
|
|
.statistics-grid > div {
|
|
|
|
|
|
min-height: 78px; padding: 13px; border-radius: 6px; background: #f8fafc; display: flex; flex-direction: column; gap: 8px;
|
|
|
|
|
|
span { color: #64748b; font-size: 12px; }
|
|
|
|
|
|
strong { color: #1f2937; font-size: 18px; font-weight: 600; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-section, .result-section { margin-top: 16px; }
|
|
|
|
|
|
.config-grid { margin: 0; padding: 8px 18px 16px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 40px; }
|
|
|
|
|
|
.config-grid > div { min-height: 48px; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
|
|
|
|
|
.config-grid dt { color: #64748b; font-size: 12px; }
|
2026-07-24 16:30:29 +08:00
|
|
|
|
.config-grid dd { max-width: 68%; margin: 0; color: #334155; font-size: 13px; overflow-wrap: anywhere; text-align: right; }
|
2026-07-13 11:04:55 +08:00
|
|
|
|
|
|
|
|
|
|
.result-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid #eef0f3; }
|
|
|
|
|
|
.result-toolbar .section-heading { border-bottom: 0; }
|
|
|
|
|
|
.result-filters { padding-right: 18px; display: flex; gap: 10px; }
|
|
|
|
|
|
.result-filters :deep(.el-input) { width: 250px; }
|
|
|
|
|
|
.result-filters :deep(.el-select) { width: 120px; }
|
|
|
|
|
|
.result-section :deep(.el-table) { border-radius: 0; }
|
|
|
|
|
|
|
2026-07-25 18:25:34 +08:00
|
|
|
|
:global(.data-process-result-tooltip) {
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
max-width: min(520px, calc(100vw - 32px));
|
|
|
|
|
|
max-height: 240px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:04:55 +08:00
|
|
|
|
.result-empty, .not-found-state {
|
|
|
|
|
|
min-height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #94a3b8;
|
|
|
|
|
|
> i { margin-bottom: 12px; color: var(--el-color-primary-light-5); font-size: 30px; }
|
|
|
|
|
|
strong, h2 { margin: 0; color: #475569; font-size: 14px; font-weight: 500; }
|
|
|
|
|
|
span, p { margin: 6px 0 16px; font-size: 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-pagination { min-height: 56px; padding: 0 18px; border-top: 1px solid #eef0f3; display: flex; align-items: center; justify-content: space-between; }
|
|
|
|
|
|
.result-pagination > span { color: #94a3b8; font-size: 12px; }
|
|
|
|
|
|
.result-pagination :deep(.el-pagination) { margin-top: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1100px) {
|
|
|
|
|
|
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
|
|
|
.detail-grid { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.metric-grid, .config-grid { grid-template-columns: 1fr; }
|
2026-07-23 15:10:13 +08:00
|
|
|
|
.detail-heading .heading-row { align-items: flex-start; flex-wrap: wrap; }
|
|
|
|
|
|
.publish-button { width: 100%; margin-left: 0; }
|
|
|
|
|
|
.publish-form-grid { grid-template-columns: 1fr; gap: 0; }
|
2026-07-13 11:04:55 +08:00
|
|
|
|
.result-toolbar { align-items: stretch; flex-direction: column; }
|
|
|
|
|
|
.result-filters { padding: 0 16px 16px; flex-direction: column; }
|
|
|
|
|
|
.result-filters :deep(.el-input), .result-filters :deep(.el-select) { width: 100%; }
|
|
|
|
|
|
.heading-meta { flex-direction: column; gap: 6px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|