feat: 数据处理任务详情页
新增 DataProcessDetailView,列表查看详情入口跳转到详情页,路由注册 data-process/:id,配套回归脚本与 npm 脚本注册(含数据转换与数据处理详情两个测试脚本)。
This commit is contained in:
@@ -11,9 +11,11 @@
|
|||||||
"test:default-dashboard": "node scripts/regression-default-dashboard.mjs",
|
"test:default-dashboard": "node scripts/regression-default-dashboard.mjs",
|
||||||
"test:dashboard": "node scripts/regression-dashboard.mjs",
|
"test:dashboard": "node scripts/regression-dashboard.mjs",
|
||||||
"test:data-process-list": "node scripts/regression-data-process-list.mjs",
|
"test:data-process-list": "node scripts/regression-data-process-list.mjs",
|
||||||
|
"test:data-process-detail": "node scripts/regression-data-process-detail.mjs",
|
||||||
"test:data-process-wizard": "node scripts/regression-data-process-wizard.mjs",
|
"test:data-process-wizard": "node scripts/regression-data-process-wizard.mjs",
|
||||||
"test:dataset-task-tab": "node scripts/regression-dataset-task-tab.mjs",
|
"test:dataset-task-tab": "node scripts/regression-dataset-task-tab.mjs",
|
||||||
"test:dataset-preview": "node scripts/regression-dataset-preview.mjs",
|
"test:dataset-preview": "node scripts/regression-dataset-preview.mjs",
|
||||||
|
"test:data-convert": "node scripts/regression-data-convert.mjs",
|
||||||
"test:eval-create": "node scripts/regression-eval-create-wizard.mjs",
|
"test:eval-create": "node scripts/regression-eval-create-wizard.mjs",
|
||||||
"test:eval-detail": "node scripts/regression-eval-detail.mjs",
|
"test:eval-detail": "node scripts/regression-eval-detail.mjs",
|
||||||
"test:model-manage": "node scripts/regression-model-manage.mjs",
|
"test:model-manage": "node scripts/regression-model-manage.mjs",
|
||||||
|
|||||||
55
frontend/scripts/regression-data-process-detail.mjs
Normal file
55
frontend/scripts/regression-data-process-detail.mjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { parse as parseSfc } from '@vue/compiler-sfc'
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const sourceRoot = path.resolve(scriptDir, '../src')
|
||||||
|
const [detailSource, listSource, routerSource] = await Promise.all([
|
||||||
|
readFile(path.join(sourceRoot, 'views/data-process/DataProcessDetailView.vue'), 'utf8'),
|
||||||
|
readFile(path.join(sourceRoot, 'views/data-process/DataProcessListView.vue'), 'utf8'),
|
||||||
|
readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const { descriptor, errors } = parseSfc(detailSource, { filename: 'DataProcessDetailView.vue' })
|
||||||
|
assert.equal(errors.length, 0, `数据处理详情页无法解析:${errors[0]}`)
|
||||||
|
assert.ok(descriptor.template?.content.trim(), '数据处理详情页缺少可渲染模板')
|
||||||
|
|
||||||
|
assert.match(routerSource, /path:\s*['"]data-process\/:id['"]/, '缺少数据处理详情动态路由')
|
||||||
|
assert.match(routerSource, /name:\s*['"]data-process-detail['"]/, '数据处理详情路由缺少名称')
|
||||||
|
assert.match(routerSource, /DataProcessDetailView\.vue/, '数据处理详情路由未加载详情页面')
|
||||||
|
assert.match(routerSource, /title:\s*['"]数据处理详情['"]/, '数据处理详情路由标题不正确')
|
||||||
|
|
||||||
|
assert.match(listSource, /name:\s*['"]data-process-detail['"]/, '列表详情按钮未使用详情命名路由')
|
||||||
|
assert.match(listSource, /params:\s*\{\s*id:\s*taskId\s*\}/, '列表详情按钮未传递任务 ID')
|
||||||
|
assert.doesNotMatch(listSource, /查看详情功能开发中/, '详情按钮仍保留开发中提示')
|
||||||
|
|
||||||
|
for (const requiredCopy of [
|
||||||
|
'处理耗时',
|
||||||
|
'开始时间',
|
||||||
|
'完成时间',
|
||||||
|
'输入数据',
|
||||||
|
'输出结果',
|
||||||
|
'处理统计',
|
||||||
|
'处理配置',
|
||||||
|
'结果明细',
|
||||||
|
]) {
|
||||||
|
assert.match(detailSource, new RegExp(requiredCopy), `详情页缺少必要信息:${requiredCopy}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const status of ['completed', 'running', 'pending', 'failed']) {
|
||||||
|
assert.match(detailSource, new RegExp(`status:\\s*['"]${status}['"]`), `详情 Mock 缺少 ${status} 状态`)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(detailSource, /const completedResults:\s*ResultRow\[\]/, '完成任务缺少结果明细 Mock')
|
||||||
|
assert.match(detailSource, /:data="paginatedResults"/, '结果表格未绑定分页后的处理结果')
|
||||||
|
assert.match(detailSource, /v-model="keyword"/, '结果明细缺少搜索能力')
|
||||||
|
assert.match(detailSource, /v-model="statusFilter"/, '结果明细缺少状态筛选')
|
||||||
|
assert.match(detailSource, /router\.push\(`\/dataset\/\$\{detail\.outputDatasetId\}\/preview`\)/, '输出数据集未接入预览入口')
|
||||||
|
assert.match(detailSource, /未找到数据处理任务/, '未知任务 ID 缺少明确空状态')
|
||||||
|
assert.match(detailSource, /width:\s*100%/, '详情页没有铺满内容区域')
|
||||||
|
assert.doesNotMatch(detailSource, /^\s*max-width:\s*\d+px/m, '详情页不应使用固定最大宽度')
|
||||||
|
assert.doesNotMatch(detailSource, /\b(?:password|secret|token)\b/i, '详情页不得展示敏感凭据字段')
|
||||||
|
|
||||||
|
console.log('数据处理任务详情 UI 回归检查通过')
|
||||||
@@ -133,6 +133,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
|
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
|
||||||
meta: { title: '新建数据处理任务', pageSurface: 'self' },
|
meta: { title: '新建数据处理任务', pageSurface: 'self' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'data-process/:id',
|
||||||
|
name: 'data-process-detail',
|
||||||
|
component: () => import('@/views/data-process/DataProcessDetailView.vue'),
|
||||||
|
meta: { title: '数据处理详情' },
|
||||||
|
},
|
||||||
// 数据集管理
|
// 数据集管理
|
||||||
{
|
{
|
||||||
path: 'dataset',
|
path: 'dataset',
|
||||||
|
|||||||
428
frontend/src/views/data-process/DataProcessDetailView.vue
Normal file
428
frontend/src/views/data-process/DataProcessDetailView.vue
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import PageCard from '@/components/PageCard.vue'
|
||||||
|
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
||||||
|
|
||||||
|
interface ResultRow {
|
||||||
|
id: string
|
||||||
|
instruction: string
|
||||||
|
input: string
|
||||||
|
output: string
|
||||||
|
status: 'valid' | 'modified' | 'invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessDetail {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: string
|
||||||
|
description: string
|
||||||
|
processType: string
|
||||||
|
dataType: string
|
||||||
|
sourceDataset: string
|
||||||
|
outputDataset?: string
|
||||||
|
outputDatasetId?: number
|
||||||
|
creator: string
|
||||||
|
createTime: string
|
||||||
|
startTime?: string
|
||||||
|
completeTime?: string
|
||||||
|
duration?: string
|
||||||
|
progress: number
|
||||||
|
inputCount: number
|
||||||
|
outputCount: number
|
||||||
|
filteredCount: number
|
||||||
|
duplicateCount: number
|
||||||
|
errorCount: number
|
||||||
|
config: Array<{ label: string; value: string }>
|
||||||
|
results: ResultRow[]
|
||||||
|
failureReason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedResults: ResultRow[] = [
|
||||||
|
{
|
||||||
|
id: 'result-001',
|
||||||
|
instruction: '用户询问如何修改订单收货地址,应如何回复?',
|
||||||
|
input: '订单已经提交,但还没有发货。',
|
||||||
|
output: '您好,订单发货前可以在订单详情中申请修改收货地址,提交后请等待系统审核。',
|
||||||
|
status: 'valid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'result-002',
|
||||||
|
instruction: '概括用户的退款诉求。',
|
||||||
|
input: '商品收到后发现破损,希望尽快退货退款。',
|
||||||
|
output: '用户因商品破损申请退货退款,并希望尽快处理。',
|
||||||
|
status: 'modified',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'result-003',
|
||||||
|
instruction: '判断咨询所属业务类型。',
|
||||||
|
input: '会员积分什么时候到账?',
|
||||||
|
output: '会员权益 / 积分到账咨询',
|
||||||
|
status: 'valid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'result-004',
|
||||||
|
instruction: '生成简洁的客服回复。',
|
||||||
|
input: '优惠券显示已过期,但昨天还能使用。',
|
||||||
|
output: '您好,请提供优惠券名称和订单信息,我们将为您核实有效期及使用记录。',
|
||||||
|
status: 'valid',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const detailMap: Record<string, ProcessDetail> = {
|
||||||
|
'183921': {
|
||||||
|
id: '183921',
|
||||||
|
name: '客服问答数据清洗',
|
||||||
|
status: 'completed',
|
||||||
|
description: '清洗客服对话中的无效记录、重复问答和格式异常内容,生成可用于模型训练的标准数据集。',
|
||||||
|
processType: '去重清洗',
|
||||||
|
dataType: '结构化数据',
|
||||||
|
sourceDataset: '客服对话原始集',
|
||||||
|
outputDataset: '客服对话清洗集',
|
||||||
|
outputDatasetId: 7,
|
||||||
|
creator: '管理员',
|
||||||
|
createTime: '2026-07-08 14:23:00',
|
||||||
|
startTime: '2026-07-08 14:23:18',
|
||||||
|
completeTime: '2026-07-08 14:28:42',
|
||||||
|
duration: '5 分 24 秒',
|
||||||
|
progress: 100,
|
||||||
|
inputCount: 19068,
|
||||||
|
outputCount: 18240,
|
||||||
|
filteredCount: 186,
|
||||||
|
duplicateCount: 642,
|
||||||
|
errorCount: 0,
|
||||||
|
config: [
|
||||||
|
{ label: '预处理规则', value: '清理无效数据、结构检测、内容去重、格式标准化' },
|
||||||
|
{ label: '输出格式', value: 'Alpaca JSONL' },
|
||||||
|
{ label: '数据集划分', value: '训练集 80% / 验证集 10% / 测试集 10%' },
|
||||||
|
{ label: '处理引擎', value: 'DataFlow Engine v2.3' },
|
||||||
|
],
|
||||||
|
results: completedResults,
|
||||||
|
},
|
||||||
|
'492015': {
|
||||||
|
id: '492015', name: '指令微调数据构造', status: 'running',
|
||||||
|
description: '从通用语料中构造指令微调训练样本。', processType: '指令构造', dataType: '非结构化数据',
|
||||||
|
sourceDataset: '通用语料库', outputDataset: 'SFT 指令集', outputDatasetId: 8, creator: '管理员',
|
||||||
|
createTime: '2026-07-09 09:10:00', startTime: '2026-07-09 09:10:21', duration: '处理中', progress: 68,
|
||||||
|
inputCount: 18500, outputCount: 8568, filteredCount: 425, duplicateCount: 192, errorCount: 8,
|
||||||
|
config: [
|
||||||
|
{ label: '切分方式', value: '语义切分' }, { label: '切片长度', value: '800 Tokens,重叠 80 Tokens' },
|
||||||
|
{ label: '生成数量', value: '每个切片生成 2 组问答' }, { label: '处理引擎', value: 'DataFlow Engine v2.3' },
|
||||||
|
], results: [],
|
||||||
|
},
|
||||||
|
'731948': {
|
||||||
|
id: '731948', name: '敏感信息脱敏处理', status: 'pending', description: '识别并脱敏用户反馈数据中的敏感字段。',
|
||||||
|
processType: '脱敏处理', dataType: '结构化数据', sourceDataset: '用户反馈数据', outputDataset: '用户反馈脱敏集',
|
||||||
|
outputDatasetId: 9, creator: '管理员', createTime: '2026-07-09 16:45:00', duration: '等待执行', progress: 0,
|
||||||
|
inputCount: 9340, outputCount: 0, filteredCount: 0, duplicateCount: 0, errorCount: 0,
|
||||||
|
config: [
|
||||||
|
{ label: '脱敏范围', value: '姓名、手机号、身份证号、地址' }, { label: '替换方式', value: '掩码替换' },
|
||||||
|
{ label: '输出格式', value: 'JSONL' }, { label: '处理引擎', value: 'DataFlow Engine v2.3' },
|
||||||
|
], results: [],
|
||||||
|
},
|
||||||
|
'582012': {
|
||||||
|
id: '582012', name: '多轮对话拼接', status: 'failed', description: '将单轮问答按会话标识拼接为多轮对话数据。',
|
||||||
|
processType: '格式转换', dataType: '结构化数据', sourceDataset: '单轮问答集', creator: '管理员',
|
||||||
|
createTime: '2026-07-10 08:30:00', startTime: '2026-07-10 08:30:16', completeTime: '2026-07-10 08:31:04',
|
||||||
|
duration: '48 秒', progress: 37, inputCount: 7520, outputCount: 2780, filteredCount: 24, duplicateCount: 0, errorCount: 1,
|
||||||
|
config: [
|
||||||
|
{ label: '会话字段', value: 'conversation_id' }, { label: '排序字段', value: 'message_time' },
|
||||||
|
{ label: '最大轮次', value: '20 轮' }, { label: '处理引擎', value: 'DataFlow Engine v2.3' },
|
||||||
|
], results: [], failureReason: '第 2 个源文件缺少 conversation_id 字段,无法继续执行会话拼接。',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const keyword = ref('')
|
||||||
|
const statusFilter = ref('')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const taskId = computed(() => String(route.params.id || ''))
|
||||||
|
const detail = computed(() => detailMap[taskId.value])
|
||||||
|
|
||||||
|
const retentionRate = computed(() => {
|
||||||
|
if (!detail.value?.inputCount) return 0
|
||||||
|
return Number(((detail.value.outputCount / detail.value.inputCount) * 100).toFixed(1))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredResults = computed(() => {
|
||||||
|
const normalizedKeyword = keyword.value.trim().toLowerCase()
|
||||||
|
return (detail.value?.results || []).filter((item) => {
|
||||||
|
const matchesStatus = !statusFilter.value || item.status === statusFilter.value
|
||||||
|
const matchesKeyword = !normalizedKeyword
|
||||||
|
|| [item.instruction, item.input, item.output].some((text) => text.toLowerCase().includes(normalizedKeyword))
|
||||||
|
return matchesStatus && matchesKeyword
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const paginatedResults = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
return filteredResults.value.slice(start, start + pageSize.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function resultStatusLabel(status: ResultRow['status']) {
|
||||||
|
return status === 'valid' ? '有效' : status === 'modified' ? '已修改' : '无效'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultStatusType(status: ResultRow['status']) {
|
||||||
|
return status === 'valid' ? 'success' : status === 'modified' ? 'warning' : 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPage() {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageCard class="data-process-detail-page">
|
||||||
|
<template v-if="detail" #header>
|
||||||
|
<div class="detail-heading">
|
||||||
|
<div class="heading-row">
|
||||||
|
<h1>{{ detail.name }}</h1>
|
||||||
|
<ModelStatusTag :status="detail.status" />
|
||||||
|
</div>
|
||||||
|
<p>{{ detail.description }}</p>
|
||||||
|
<dl class="heading-meta">
|
||||||
|
<div><dt>任务 ID</dt><dd>{{ detail.id }}</dd></div>
|
||||||
|
<div><dt>处理类型</dt><dd>{{ detail.processType }}</dd></div>
|
||||||
|
<div><dt>数据类型</dt><dd>{{ detail.dataType }}</dd></div>
|
||||||
|
<div><dt>创建人</dt><dd>{{ detail.creator }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="!detail" class="not-found-state">
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true" />
|
||||||
|
<h2>未找到数据处理任务</h2>
|
||||||
|
<p>任务可能已被删除,或当前链接已失效。</p>
|
||||||
|
<el-button type="primary" @click="router.push('/data-process')">返回任务列表</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<section class="metric-grid" aria-label="处理结果概览">
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>处理耗时</span>
|
||||||
|
<strong>{{ detail.duration || '尚未开始' }}</strong>
|
||||||
|
<small>{{ detail.completeTime ? `完成于 ${detail.completeTime}` : `当前进度 ${detail.progress}%` }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>输入数据</span>
|
||||||
|
<strong>{{ detail.inputCount.toLocaleString() }}</strong>
|
||||||
|
<small>来源:{{ detail.sourceDataset }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>输出结果</span>
|
||||||
|
<strong>{{ detail.outputCount.toLocaleString() }}</strong>
|
||||||
|
<small>{{ detail.outputDataset || '尚未生成输出数据集' }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card is-primary">
|
||||||
|
<span>数据保留率</span>
|
||||||
|
<strong>{{ detail.inputCount ? `${retentionRate}%` : '-' }}</strong>
|
||||||
|
<el-progress :percentage="detail.progress" :show-text="false" :stroke-width="5" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div v-if="detail.failureReason" class="failure-alert" role="alert">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true" />
|
||||||
|
<div><strong>处理任务执行失败</strong><p>{{ detail.failureReason }}</p></div>
|
||||||
|
</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">
|
||||||
|
<div><dt>创建时间</dt><dd>{{ detail.createTime }}</dd></div>
|
||||||
|
<div><dt>开始时间</dt><dd>{{ detail.startTime || '尚未开始' }}</dd></div>
|
||||||
|
<div><dt>完成时间</dt><dd>{{ detail.completeTime || '尚未完成' }}</dd></div>
|
||||||
|
<div><dt>处理耗时</dt><dd>{{ detail.duration || '-' }}</dd></div>
|
||||||
|
<div><dt>源数据集</dt><dd>{{ detail.sourceDataset }}</dd></div>
|
||||||
|
<div>
|
||||||
|
<dt>输出数据集</dt>
|
||||||
|
<dd>
|
||||||
|
<el-button
|
||||||
|
v-if="detail.outputDataset && detail.status === 'completed'"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="router.push(`/dataset/${detail.outputDatasetId}/preview`)"
|
||||||
|
>{{ detail.outputDataset }} <i class="fa fa-external-link" /></el-button>
|
||||||
|
<span v-else>{{ detail.outputDataset || '尚未生成' }}</span>
|
||||||
|
</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">
|
||||||
|
<div><span>原始数据</span><strong>{{ detail.inputCount.toLocaleString() }}</strong></div>
|
||||||
|
<div><span>成功输出</span><strong>{{ detail.outputCount.toLocaleString() }}</strong></div>
|
||||||
|
<div><span>过滤数据</span><strong>{{ detail.filteredCount.toLocaleString() }}</strong></div>
|
||||||
|
<div><span>重复数据</span><strong>{{ detail.duplicateCount.toLocaleString() }}</strong></div>
|
||||||
|
<div><span>异常数据</span><strong>{{ detail.errorCount.toLocaleString() }}</strong></div>
|
||||||
|
<div><span>执行进度</span><strong>{{ detail.progress }}%</strong></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="detail-section config-section" aria-labelledby="config-title">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div><h2 id="config-title">处理配置</h2><p>任务执行时使用的规则与参数</p></div>
|
||||||
|
</div>
|
||||||
|
<dl class="config-grid">
|
||||||
|
<div v-for="item in detail.config" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="detail-section result-section" aria-labelledby="result-title">
|
||||||
|
<div class="result-toolbar">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div><h2 id="result-title">结果明细</h2><p>查看处理完成后的数据内容与校验状态</p></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="detail.results.length" class="result-filters">
|
||||||
|
<el-input v-model="keyword" clearable placeholder="搜索指令、输入或输出" @input="resetPage">
|
||||||
|
<template #prefix><i class="fa fa-search" aria-hidden="true" /></template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="statusFilter" clearable placeholder="全部状态" @change="resetPage">
|
||||||
|
<el-option label="有效" value="valid" />
|
||||||
|
<el-option label="已修改" value="modified" />
|
||||||
|
<el-option label="无效" value="invalid" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-if="filteredResults.length" :data="paginatedResults" row-key="id" table-layout="fixed">
|
||||||
|
<el-table-column type="index" label="#" width="56" align="center" />
|
||||||
|
<el-table-column label="指令" min-width="210" show-overflow-tooltip prop="instruction" />
|
||||||
|
<el-table-column label="输入" min-width="190" show-overflow-tooltip prop="input" />
|
||||||
|
<el-table-column label="输出" min-width="260" show-overflow-tooltip prop="output" />
|
||||||
|
<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>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div v-else class="result-empty">
|
||||||
|
<i class="fa" :class="detail.status === 'failed' ? 'fa-exclamation-circle' : 'fa-hourglass-half'" aria-hidden="true" />
|
||||||
|
<strong>{{ detail.status === 'completed' ? '没有符合条件的结果' : detail.status === 'failed' ? '任务失败,未生成结果明细' : '处理完成后将在这里展示结果明细' }}</strong>
|
||||||
|
<span>{{ detail.status === 'completed' ? '请调整搜索或筛选条件' : `当前任务状态:${detail.status === 'running' ? '运行中' : '等待中'}` }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="filteredResults.length" class="result-pagination">
|
||||||
|
<span>共 {{ filteredResults.length }} 条结果</span>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="filteredResults.length"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</PageCard>
|
||||||
|
</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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
.config-grid dd { margin: 0; color: #334155; font-size: 13px; text-align: right; }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
.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>
|
||||||
@@ -60,13 +60,14 @@ function handleCreate() {
|
|||||||
router.push('/data-process/create')
|
router.push('/data-process/create')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查看任务详情(功能开发中) */
|
/** 查看任务详情 */
|
||||||
function viewDetail(_row: DataProcessTask) {
|
function viewDetail(row: unknown) {
|
||||||
ElMessage.info('查看详情功能开发中...')
|
const taskId = (row as { id: string | number }).id
|
||||||
|
router.push({ name: 'data-process-detail', params: { id: taskId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除任务(功能开发中) */
|
/** 删除任务(功能开发中) */
|
||||||
function handleDelete(_row: DataProcessTask) {
|
function handleDelete(_row: unknown) {
|
||||||
ElMessage.info('删除功能开发中...')
|
ElMessage.info('删除功能开发中...')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user