fix(dataset): 展示并切换三路数据切分

This commit is contained in:
caoxiaozhu
2026-07-24 21:11:01 +08:00
parent 9cb77c251a
commit 9114f3d4c7
6 changed files with 193 additions and 11 deletions

View File

@@ -10,6 +10,7 @@ const previewComponentPaths = [
'DatasetRecordTable.vue',
'DatasetRecordEditorDialog.vue',
'DatasetRawPreview.vue',
'DatasetSplitNavigator.vue',
].map((name) => path.join(previewComponentsDir, name))
const apiPath = path.join(root, 'src/api/modules/dataset.ts')
const adapterPath = path.join(root, 'src/mock/adapter.ts')
@@ -50,12 +51,16 @@ function expect(condition, message) {
}
expect(previewLogicSource.split('\n').length < 520, 'DatasetPreviewView.vue 的逻辑与模板应保持在 520 行以内')
for (const componentName of ['DatasetVersionBar', 'DatasetRecordTable', 'DatasetRecordEditorDialog', 'DatasetRawPreview']) {
for (const componentName of ['DatasetVersionBar', 'DatasetRecordTable', 'DatasetRecordEditorDialog', 'DatasetRawPreview', 'DatasetSplitNavigator']) {
expect(source.includes(`import ${componentName}`), `主页面未导入叶子组件:${componentName}`)
expect(source.includes(`<${componentName}`), `主页面未挂载叶子组件:${componentName}`)
}
expect(source.includes('class="preview-workspace"'), '详情页应提供数据文件工作区')
expect(!source.includes('class="file-pane"'), '单文件数据集详情不应展示文件选择侧栏')
expect(previewSurfaceSource.includes('class="split-file-tabs"'), '多切分数据集应提供训练/验证/测试文件切换')
expect(source.includes('@select="handleFileChange"'), '切换数据切分时应加载对应文件版本')
expect(previewSurfaceSource.includes('splitCounts.train'), '数据集概览应显示训练集数量')
expect(previewSurfaceSource.includes('splitCounts.validation'), '数据集概览应显示验证集数量')
expect(previewSurfaceSource.includes('splitCounts.test'), '数据集概览应显示测试集数量')
expect(previewSurfaceSource.includes('class="code-line"'), '内容查看器应提供逐行展示')
expect(previewSurfaceSource.includes('class="line-number"'), '内容查看器应显示行号')
expect(source.includes('previewLoading'), '切换文件时应提供独立加载状态')

View File

@@ -63,6 +63,8 @@ export interface DatasetFile {
id?: string
name: string
size?: string | number
record_count?: number
split?: 'train' | 'validation' | 'test' | null
[k: string]: any
}
@@ -78,6 +80,7 @@ export interface DatasetItem {
description?: string
create_time?: string
files?: DatasetFile[]
split_counts?: { train: number; validation: number; test: number }
}
export interface DatasetVersion {

View File

@@ -448,6 +448,33 @@ async function publishDataset() {
}
}
async function resyncPublishedDataset() {
if (!detail.value || !outputDatasetId.value || publishing.value) return
try {
await ElMessageBox.confirm(
'将按当前 8:1:1 配置重新生成训练、验证、测试三个切分文件,数据集 ID 保持不变。',
'重新同步数据切分?',
{ type: 'warning', confirmButtonText: '重新同步', cancelButtonText: '取消' },
)
} catch {
return
}
publishing.value = true
try {
const published = await publishDataProcess(taskId.value, {
...publishForm,
dataset_name: outputDatasetName.value || `${detail.value.name}-数据集`,
split: configuredSplit(),
})
ElMessage.success('训练集、验证集和测试集已重新同步')
const datasetId = published.dataset?.id || outputDatasetId.value
await router.push(`/dataset/${datasetId}/preview`)
} finally {
publishing.value = false
}
}
watch([currentPage, pageSize], () => void loadResults())
onMounted(loadPage)
@@ -472,6 +499,13 @@ onBeforeUnmount(() => {
<i class="fa" :class="outputDatasetId ? 'fa-external-link' : 'fa-database'" />
{{ outputDatasetId ? '查看输出数据集' : '发布为数据集' }}
</el-button>
<el-button
v-if="detail.status === 'completed' && outputDatasetId"
:loading="publishing"
@click="resyncPublishedDataset"
>
<i class="fa fa-refresh" />重新同步切分
</el-button>
</div>
<p>{{ detail.description || '暂无任务描述' }}</p>
<dl class="heading-meta">
@@ -717,7 +751,7 @@ onBeforeUnmount(() => {
<el-alert
type="info"
:closable="false"
:title="`数据集划分:训练集 ${publishForm.split.train}% / 验证集 ${publishForm.split.validation}% / 测试集 ${publishForm.split.test}%`"
:title="`将生成三个独立文件:训练集 ${publishForm.split.train}% / 验证集 ${publishForm.split.validation}% / 测试集 ${publishForm.split.test}%`"
/>
</el-form>
<template #footer>

View File

@@ -21,6 +21,17 @@ const filteredDataList = computed(() => {
return dataList.value.filter((item) => item.source !== 'task')
})
const splitLabels = [
{ key: 'train', label: '训练', type: 'primary' },
{ key: 'validation', label: '验证', type: 'warning' },
{ key: 'test', label: '测试', type: 'success' },
] as const
function hasSplitCounts(row: Partial<DatasetItem>) {
return row.source === 'task'
&& Object.values(row.split_counts || {}).some((value) => Number(value) > 0)
}
async function loadData() {
loading.value = true
try {
@@ -87,9 +98,17 @@ onMounted(loadData)
<el-table-column label="数据集名称" prop="name" align="center" />
<el-table-column label="数据类型" align="center" width="110">
<template #default="{ row }">
<el-tag type="primary" size="small">
{{ DATASET_TYPE_MAP[String(row.type).toLowerCase()] || row.type || '-' }}
<div v-if="hasSplitCounts(row)" class="split-tags">
<el-tag
v-for="item in splitLabels"
:key="item.key"
:type="item.type"
size="small"
>
{{ item.label }} {{ row.split_counts?.[item.key] || 0 }}
</el-tag>
</div>
<el-tag v-else type="primary" size="small">{{ DATASET_TYPE_MAP[String(row.type).toLowerCase()] || row.type || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="存储位置" align="center" width="110">
@@ -167,4 +186,11 @@ onMounted(loadData)
font-weight: 600;
}
}
.split-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
}
</style>

View File

@@ -20,6 +20,7 @@ import DatasetVersionBar from './preview/DatasetVersionBar.vue'
import DatasetRecordTable from './preview/DatasetRecordTable.vue'
import DatasetRecordEditorDialog from './preview/DatasetRecordEditorDialog.vue'
import DatasetRawPreview from './preview/DatasetRawPreview.vue'
import DatasetSplitNavigator from './preview/DatasetSplitNavigator.vue'
import type { EditField } from './preview/types'
import type { DatasetFile, DatasetItem, DatasetVersion } from '@/types'
@@ -53,6 +54,7 @@ let versionRequestId = 0
const files = computed<DatasetFile[]>(() => dataset.value?.files || [])
const selectedFile = computed(() => files.value.find((file) => fileKey(file) === selectedFileId.value))
const splitCounts = computed(() => dataset.value?.split_counts || { train: 0, validation: 0, test: 0 })
const totalLines = computed(() => previewContent.value ? previewContent.value.split('\n').length : 0)
const recordResult = computed(() => parseDatasetRecords(previewContent.value, selectedFile.value?.name || ''))
const isRecordFile = computed(() => recordResult.value.supported)
@@ -81,6 +83,13 @@ function fileKey(file: DatasetFile) {
return String(file.id || file.name)
}
async function handleFileChange(file: DatasetFile) {
if (fileKey(file) === selectedFileId.value) return
if (!await confirmDiscardChanges('切换数据切分后,当前样本的未保存修改将丢失。')) return
selectedFileId.value = fileKey(file)
await loadVersions(file)
}
async function loadDataset() {
loading.value = true
try {
@@ -400,6 +409,12 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
<div class="overview-item overview-item-wide"><span>创建时间</span><strong>{{ createdAt }}</strong></div>
</section>
<DatasetSplitNavigator
v-if="Object.values(splitCounts).some((value) => value > 0)"
:files="files" :selected-file-id="selectedFileId" :split-counts="splitCounts"
@select="handleFileChange"
/>
<DatasetVersionBar
v-model="selectedVersionId"
:versions="versions"
@@ -488,10 +503,7 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
</section>
<DatasetRecordEditorDialog
:model-value="editorVisible"
:record="editingRecord"
:edit-fields="editFields"
:raw-draft="rawDraft"
:model-value="editorVisible" :record="editingRecord" :edit-fields="editFields" :raw-draft="rawDraft"
:saving-record="savingRecord"
:has-draft-changes="hasEditorDraftChanges"
:next-version-number="nextVersionNumber"
@@ -500,7 +512,6 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
@update:field="updateEditField"
@update:raw-draft="rawDraft = $event"
/>
<AppConfirmDialog ref="confirmDialogRef" />
</PageCard>
</template>

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import type { DatasetFile } from '@/types'
defineProps<{
files: DatasetFile[]
selectedFileId: string
splitCounts: { train: number; validation: number; test: number }
}>()
const emit = defineEmits<{ select: [file: DatasetFile] }>()
function fileKey(file: DatasetFile) {
return String(file.id || file.name)
}
function splitLabel(split?: DatasetFile['split']) {
if (!split) return '数据文件'
return { train: '训练集', validation: '验证集', test: '测试集' }[split]
}
function splitTagType(split?: DatasetFile['split']) {
if (!split) return 'info'
return ({ train: 'primary', validation: 'warning', test: 'success' } as const)[split]
}
</script>
<template>
<section class="split-summary" aria-label="数据切分统计">
<div class="split-stat train"><span>训练集</span><strong>{{ splitCounts.train.toLocaleString() }}</strong></div>
<div class="split-stat validation"><span>验证集</span><strong>{{ splitCounts.validation.toLocaleString() }}</strong></div>
<div class="split-stat test"><span>测试集</span><strong>{{ splitCounts.test.toLocaleString() }}</strong></div>
</section>
<nav v-if="files.length > 1" class="split-file-tabs" aria-label="数据切分文件">
<button
v-for="file in files"
:key="fileKey(file)"
type="button"
class="split-file-tab"
:class="{ active: fileKey(file) === selectedFileId }"
@click="emit('select', file)"
>
<el-tag :type="splitTagType(file.split)" size="small">{{ splitLabel(file.split) }}</el-tag>
<strong>{{ (file.record_count || 0).toLocaleString() }} </strong>
<span>{{ file.name }}</span>
</button>
</nav>
</template>
<style scoped lang="scss">
.split-summary,
.split-file-tabs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.split-stat {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border: 1px solid #e5eaf2;
border-radius: 10px;
background: #fbfcfe;
color: #667085;
}
.split-stat strong { font-size: 18px; }
.split-stat.train strong { color: #4f46e5; }
.split-stat.validation strong { color: #d97706; }
.split-stat.test strong { color: #059669; }
.split-file-tab {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: center;
gap: 8px;
min-width: 0;
padding: 12px 14px;
border: 1px solid #e5eaf2;
border-radius: 10px;
background: #fff;
color: #344054;
cursor: pointer;
text-align: left;
}
.split-file-tab:hover,
.split-file-tab.active {
border-color: #818cf8;
background: #f7f7ff;
}
.split-file-tab span:last-child {
overflow: hidden;
color: #8a94a6;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>