fix(frontend): 对齐数据任务详情状态

This commit is contained in:
caoxiaozhu
2026-07-27 10:03:52 +08:00
parent b14b2ecf22
commit b4927a8952
6 changed files with 112 additions and 57 deletions

View File

@@ -58,7 +58,12 @@ assert.match(detailSource, /path: '\/dataset', query: \{ tab: 'task', task_id: t
assert.match(detailSource, /published\.datasets \|\| published\.output_datasets/, '发布成功后未读取三个独立数据集')
assert.match(detailSource, /v-for="dataset in outputDatasets"/, '任务详情未展示三个输出数据集入口')
assert.match(detailSource, /const hasPublishedOutputs = computed\(\(\) => outputDatasets\.value\.length > 0\)/, '详情页没有独立识别仍然存在的已发布数据集')
assert.match(detailSource, /v-if="hasPublishedOutputs && !outputDatasetId"[\s\S]*?当前生成结果尚未重新发布,以下为上次已发布数据集,仍可正常使用/, '重新生成任务没有说明上次发布数据集仍然可用')
assert.match(detailSource, /const outputDatasets = computed\(\(\) => \{[\s\S]*?return detail\.value\?\.output_datasets \|\| \[\][\s\S]*?\}\)/, '详情页不应用输出指针伪造训练集')
assert.doesNotMatch(detailSource, /name: detail\.value\.output_dataset_name[\s\S]*?type: 'train'/, '详情页仍在伪造训练集')
assert.match(detailSource, /const hasCurrentPublishedDataset = computed\(\(\) => \([\s\S]*?outputDatasetId\.value[\s\S]*?outputDatasets\.value\.some[\s\S]*?String\(dataset\.id\) === String\(outputDatasetId\.value\)/, '当前轮发布状态没有校验指针对应的真实数据集')
assert.match(detailSource, /dataProcessDisplayStatus\(\{[\s\S]*?output_dataset_id: hasCurrentPublishedDataset\.value \? outputDatasetId\.value : null/, '详情六态没有使用经过校验的当前发布指针')
assert.match(detailSource, /v-if="hasPublishedOutputs && !hasCurrentPublishedDataset"[\s\S]*?当前轮尚未发布,以下为上次已发布数据集,仍可正常使用/, '重新生成或指针异常任务没有说明上次发布数据集仍然可用')
assert.match(detailSource, /已发布数据集不可用,请重新发布/, '已发布指针失效时没有可恢复提示')
assert.match(detailSource, /class="output-dataset-row"[\s\S]*?class="output-dataset-cell"[\s\S]*?class="output-dataset-links"/, '输出数据集没有独立的多行对齐布局')
assert.match(detailSource, /\.output-dataset-links[\s\S]*?display:\s*inline-grid[\s\S]*?grid-template-columns:\s*max-content[\s\S]*?text-align:\s*left/, '输出数据集名称没有统一左边缘')
assert.match(detailSource, /\.output-dataset-links[\s\S]*?\.el-button[\s\S]*?width:\s*100%[\s\S]*?margin-left:\s*0/, '输出数据集链接没有清除默认间距或统一宽度')
@@ -68,7 +73,7 @@ assert.match(detailSource, /将发布三个独立数据集/, '发布说明仍未
assert.match(detailSource, /function startRegeneration\(\)[\s\S]*?name: 'data-process-regenerate'[\s\S]*?params: \{ id: taskId\.value \}/, '重新生成按钮没有携带原任务 ID 进入命名路由')
assert.match(detailSource, /const canRegenerate = computed\(\(\) => \{[\s\S]*?status === 'pending'[\s\S]*?status === 'failed'[\s\S]*?status === 'stopped'[\s\S]*?status === 'completed'[\s\S]*?outputDatasetId\.value[\s\S]*?hasPublishedOutputs\.value/, '详情页没有覆盖指针已清空但旧发布数据集仍存在的重新生成任务')
assert.match(detailSource, /v-if="canRegenerate"[\s\S]*?@click="startRegeneration"[\s\S]*?重新生成/, '可恢复任务没有收敛为单一重新生成入口')
assert.match(detailSource, /v-if="detail\.status === 'completed' && !outputDatasetId"[\s\S]*?@click="openPublishDialog"[\s\S]*?发布为三个数据集/, '未发布完成任务没有保留首次发布入口')
assert.match(detailSource, /v-if="detail\.status === 'completed' && !hasCurrentPublishedDataset"[\s\S]*?@click="openPublishDialog"[\s\S]*?发布为三个数据集/, '未发布或发布指针失效的完成任务没有保留发布入口')
assert.doesNotMatch(detailSource, /查看三个数据集|重新同步数据集|resyncPublishedDataset/, '已发布任务仍保留旧查看或同步按钮')
assert.doesNotMatch(detailSource, /label="数据集类型"/, '发布三个数据集时不应再选择单一数据集类型')
assert.match(detailSource, /无法加载数据处理任务/, '未知任务或加载失败缺少明确错误状态')
@@ -84,6 +89,14 @@ assert.match(detailSource, /outputCount \+ numeric\(detail\.value\?\.filtered_co
assert.doesNotMatch(detailSource, /numeric\(detail\.value\?\.output_count\) \/ inputCount/, '结果保留率仍在用输出结果除以源文件记录数')
assert.match(detailSource, /:percentage="retentionPercentage"/, '结果保留率进度条仍绑定任务执行进度')
assert.match(detailSource, /preview_count/, '非结构化任务没有展示实际输入切片数')
assert.match(detailSource, /import \{ dataProcessDisplayStatus \} from '@\/utils\/dataProcessStatus\.js'/, '详情页没有复用数据处理六态映射')
assert.doesNotMatch(detailSource, /ModelStatusTag/, '详情页仍在使用无法区分生成与发布的通用状态组件')
assert.match(detailSource, /<el-tag :type="displayStatus\.type"[\s\S]*?displayStatus\.label/, '详情头部没有展示数据处理六态')
assert.match(detailSource, /resultEmptyDescription[\s\S]*?displayStatus\.value\.label/, '结果空态没有复用数据处理六态')
assert.doesNotMatch(detailSource, /当前任务状态:\$\{detail\.status === 'running' \? '运行中' : '等待中'\}/, '结果空态仍会把停止任务显示为等待中')
assert.match(detailSource, /inputMetricCount\.toLocaleString\(\) \}\} \{\{ inputMetricUnit \}\}/, '输入切片或记录数量缺少单位')
assert.match(detailSource, /sourceFileCount\.toLocaleString\(\) \}\} 个/, '源文件数量缺少个数单位')
assert.match(detailSource, /<span>生成结果<\/span><strong>\{\{ numeric\(detail\.output_count\)\.toLocaleString\(\) \}\} 条<\/strong>/, '生成结果数量缺少条数单位或仍误称成功输出')
assert.match(detailSource, /const configExpanded = ref\(false\)/, '处理配置没有默认收起')
assert.match(detailSource, /:aria-expanded="configExpanded"/, '处理配置折叠按钮缺少无障碍状态')
assert.match(detailSource, /<el-collapse-transition>[\s\S]*?v-show="configExpanded"/, '处理配置没有折叠过渡或内容状态')

View File

@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { parse as parseSfc } from '@vue/compiler-sfc'
import { dataProcessDisplayStatus } from '../src/utils/dataProcessStatus.js'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const sourceRoot = path.resolve(scriptDir, '../src')
@@ -31,20 +32,20 @@ assert.match(source, /function documentCountLabel\(task: DataProcessTask\)[\s\S]
assert.match(source, /label="生成个数"[\s\S]*?generatedCountLabel\(row as DataProcessTask\)/, '任务列表没有展示真实生成数量')
assert.match(source, /function generatedCountLabel\(task: DataProcessTask\)[\s\S]*?`\$\{safeCount\(task\.output_count\)\} 条`/, '生成数量没有使用 output_count 或缺少单位')
assert.doesNotMatch(source, /label="源数据集"|label="输出数据集"/, '任务列表仍使用旧的数据集列标题')
for (const label of ['待生成', '生成中', '已生成', '已发布', '生成失败', '已停止']) {
assert.ok(source.includes(`label: '${label}'`), `任务状态缺少“${label}`)
}
assert.match(source, /task\.status === 'completed'[\s\S]*?task\.output_dataset_id[\s\S]*?已发布[\s\S]*?已生成/, '完成任务没有按当前轮输出指针区分已生成与已发布')
assert.doesNotMatch(source, /task\.output_datasets\?\.length[\s\S]*?已发布/, '上一轮保留的数据集不应将当前轮标记为已发布')
for (const [label, type] of [
['待生成', 'info'],
['生成中', 'primary'],
['已生成', 'warning'],
['已发布', 'success'],
['生成失败', 'danger'],
['已停止', 'info'],
]) {
assert.ok(source.includes(`label: '${label}', type: '${type}'`), `任务状态“${label}”颜色应为 ${type}`)
assert.match(source, /import \{ dataProcessDisplayStatus \} from '@\/utils\/dataProcessStatus\.js'/, '任务列表没有复用数据处理六态映射')
assert.match(source, /dataProcessDisplayStatus\(row as DataProcessTask\)\.type[\s\S]*?dataProcessDisplayStatus\(row as DataProcessTask\)\.label/, '任务列表状态标签没有使用公共映射')
const statusCases = [
[{ status: 'pending' }, { label: '待生成', type: 'info' }],
[{ status: 'running' }, { label: '生成中', type: 'primary' }],
[{ status: 'completed' }, { label: '已生成', type: 'warning' }],
[{ status: 'completed', output_datasets: [{ id: 'old-train' }] }, { label: '已生成', type: 'warning' }],
[{ status: 'completed', output_dataset_id: 'current-train' }, { label: '已发布', type: 'success' }],
[{ status: 'failed' }, { label: '生成失败', type: 'danger' }],
[{ status: 'stopped' }, { label: '已停止', type: 'info' }],
]
for (const [task, expected] of statusCases) {
assert.deepEqual(dataProcessDisplayStatus(task), expected, `任务六态映射错误:${task.status}`)
}
assert.match(apiSource, /export (?:async )?function getDataProcessTasks/, 'API 模块缺少任务列表方法')

View File

@@ -0,0 +1,13 @@
import type { DataProcessStatus } from '@/types/dataProcess'
export type DataProcessStatusTagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
export interface DataProcessStatusLike {
status?: DataProcessStatus
output_dataset_id?: string | number | null
}
export function dataProcessDisplayStatus(task: DataProcessStatusLike): {
label: string
type: DataProcessStatusTagType
}

View File

@@ -0,0 +1,18 @@
/**
* 将数据处理任务的执行状态与当前轮发布指针合并为用户可理解的状态。
* 上一轮保留的 output_datasets 不参与判断,避免将尚未重新发布的当前轮误标为已发布。
*
* @param {{ status?: string, output_dataset_id?: string | number | null }} task
* @returns {{ label: string, type: 'primary' | 'success' | 'warning' | 'danger' | 'info' }}
*/
export function dataProcessDisplayStatus(task) {
if (task.status === 'running') return { label: '生成中', type: 'primary' }
if (task.status === 'completed') {
return task.output_dataset_id
? { label: '已发布', type: 'success' }
: { label: '已生成', type: 'warning' }
}
if (task.status === 'failed') return { label: '生成失败', type: 'danger' }
if (task.status === 'stopped') return { label: '已停止', type: 'info' }
return { label: '待生成', type: 'info' }
}

View File

@@ -3,7 +3,6 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import PageCard from '@/components/PageCard.vue'
import ModelStatusTag from '@/components/ModelStatusTag.vue'
import { usePolling } from '@/composables/usePolling'
import {
getDataProcessProgress,
@@ -21,6 +20,7 @@ import type {
DataProcessTask,
DataProcessType,
} from '@/types/dataProcess'
import { dataProcessDisplayStatus } from '@/utils/dataProcessStatus.js'
const route = useRoute()
const router = useRouter()
@@ -155,20 +155,41 @@ const sourceDatasetName = computed(() => (
|| '源文件上传'
))
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',
}]
return detail.value?.output_datasets || []
})
const outputDatasetName = computed(() => (
outputDatasets.value.map((item) => item.name).join('、')
))
const outputDatasetId = computed(() => detail.value?.output_dataset_id || null)
const hasPublishedOutputs = computed(() => outputDatasets.value.length > 0)
const hasCurrentPublishedDataset = computed(() => (
Boolean(outputDatasetId.value)
&& outputDatasets.value.some((dataset) => String(dataset.id) === String(outputDatasetId.value))
))
const displayStatus = computed(() => dataProcessDisplayStatus({
status: detail.value?.status || 'pending',
output_dataset_id: hasCurrentPublishedDataset.value ? outputDatasetId.value : null,
}))
const inputMetricUnit = computed(() => isUnstructured.value ? '个' : '条')
const outputSummary = computed(() => {
if (hasCurrentPublishedDataset.value) return outputDatasetName.value || '当前轮已发布'
if (outputDatasetId.value) return '已发布数据集不可用,请重新发布'
if (hasPublishedOutputs.value) {
return `当前轮尚未发布;上次已发布 ${outputDatasets.value.length} 个数据集,仍可正常使用`
}
return detail.value?.status === 'completed' ? '当前轮尚未发布' : '尚未发布为数据集'
})
const resultEmptyTitle = computed(() => {
if (detail.value?.status === 'completed') return '没有符合条件的结果'
if (detail.value?.status === 'failed') return '任务生成失败,未生成结果明细'
if (detail.value?.status === 'stopped') return '任务已停止,暂无生成结果'
return '处理完成后将在这里展示结果明细'
})
const resultEmptyDescription = computed(() => (
detail.value?.status === 'completed'
? '请调整搜索或筛选条件'
: `当前任务状态:${displayStatus.value.label}`
))
const canRegenerate = computed(() => {
const status = detail.value?.status
return status === 'pending'
@@ -479,9 +500,11 @@ onBeforeUnmount(() => {
<div class="detail-heading">
<div class="heading-row">
<h1>{{ detail.name }}</h1>
<ModelStatusTag :status="detail.status" />
<el-tag :type="displayStatus.type" size="small" effect="light">
{{ displayStatus.label }}
</el-tag>
<el-button
v-if="detail.status === 'completed' && !outputDatasetId"
v-if="detail.status === 'completed' && !hasCurrentPublishedDataset"
class="publish-button"
type="primary"
@click="openPublishDialog"
@@ -525,13 +548,13 @@ onBeforeUnmount(() => {
</div>
<div class="metric-card">
<span>{{ inputMetricLabel }}</span>
<strong>{{ inputMetricCount.toLocaleString() }}</strong>
<strong>{{ inputMetricCount.toLocaleString() }} {{ inputMetricUnit }}</strong>
<small>{{ sourceFileCount ? `源文件 ${sourceFileCount} 个:` : '来源:' }}{{ sourceDatasetName }}</small>
</div>
<div class="metric-card">
<span>输出结果</span>
<strong>{{ numeric(detail.output_count).toLocaleString() }}</strong>
<small>{{ outputDatasetName || '尚未发布为数据集' }}</small>
<strong>{{ numeric(detail.output_count).toLocaleString() }} </strong>
<small>{{ outputSummary }}</small>
</div>
<div class="metric-card is-primary">
<span>结果保留率</span>
@@ -560,9 +583,9 @@ onBeforeUnmount(() => {
<dt>输出数据集</dt>
<dd class="output-dataset-cell">
<p
v-if="hasPublishedOutputs && !outputDatasetId"
v-if="hasPublishedOutputs && !hasCurrentPublishedDataset"
class="output-dataset-hint"
>当前生成结果尚未重新发布以下为上次已发布数据集仍可正常使用</p>
>当前轮尚未发布以下为上次已发布数据集仍可正常使用</p>
<div v-if="outputDatasets.length" class="output-dataset-links">
<el-button
v-for="dataset in outputDatasets"
@@ -572,6 +595,7 @@ onBeforeUnmount(() => {
@click="router.push(`/dataset/${dataset.id}/preview`)"
>{{ dataset.name }}{{ dataset.count || 0 }} <i class="fa fa-external-link" /></el-button>
</div>
<span v-else-if="outputDatasetId">已发布数据集不可用请重新发布</span>
<span v-else>尚未发布为数据集</span>
</dd>
</div>
@@ -583,12 +607,12 @@ onBeforeUnmount(() => {
<div><h2 id="statistics-title">处理统计</h2><p>查看数据清洗过滤和输出情况</p></div>
</div>
<div class="statistics-grid">
<div><span>源文件</span><strong>{{ sourceFileCount.toLocaleString() }}</strong></div>
<div><span>{{ inputMetricLabel }}</span><strong>{{ inputMetricCount.toLocaleString() }}</strong></div>
<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>{{ sourceFileCount.toLocaleString() }} </strong></div>
<div><span>{{ inputMetricLabel }}</span><strong>{{ inputMetricCount.toLocaleString() }} {{ inputMetricUnit }}</strong></div>
<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>
</div>
</section>
@@ -687,8 +711,8 @@ onBeforeUnmount(() => {
<div v-else class="result-empty">
<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>
<strong>{{ resultError || resultEmptyTitle }}</strong>
<span v-if="!resultError">{{ resultEmptyDescription }}</span>
<el-button v-else type="primary" link @click="loadResults">重新加载</el-button>
</div>

View File

@@ -5,8 +5,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue'
import { deleteDataProcessTask, getDataProcessTasks } from '@/api/modules/dataProcess'
import type { DataProcessTask, DataProcessType } from '@/types/dataProcess'
type TaskStatusTagType = 'success' | 'warning' | 'info' | 'danger' | 'primary'
import { dataProcessDisplayStatus } from '@/utils/dataProcessStatus.js'
const processTypeMap: Record<DataProcessType, string> = {
structured: '结构化数据',
@@ -87,19 +86,6 @@ function generatedCountLabel(task: DataProcessTask) {
return `${safeCount(task.output_count)}`
}
/** 将当前轮生成状态与发布指针合并为用户可理解的列表状态。 */
function taskDisplayStatus(task: DataProcessTask): { label: string; type: TaskStatusTagType } {
if (task.status === 'running') return { label: '生成中', type: 'primary' }
if (task.status === 'completed') {
return task.output_dataset_id
? { label: '已发布', type: 'success' }
: { label: '已生成', type: 'warning' }
}
if (task.status === 'failed') return { label: '生成失败', type: 'danger' }
if (task.status === 'stopped') return { label: '已停止', type: 'info' }
return { label: '待生成', type: 'info' }
}
onMounted(loadData)
</script>
@@ -123,10 +109,10 @@ onMounted(loadData)
<el-table-column label="任务状态" align="center" width="110">
<template #default="{ row }">
<el-tag
:type="taskDisplayStatus(row as DataProcessTask).type"
:type="dataProcessDisplayStatus(row as DataProcessTask).type"
size="small"
effect="light"
>{{ taskDisplayStatus(row as DataProcessTask).label }}</el-tag>
>{{ dataProcessDisplayStatus(row as DataProcessTask).label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="处理类型" align="center" width="140">