feat: 评测模块新增详情页与创建向导

新增 EvalTaskDetail 与逐样本结果类型,Mock 与适配器补充评测详情接口,路由注册详情页并将维度创建并入向导;EvalCreateView 改为分步向导(任务配置、规则设置、维度表单),新增 EvalDetailView 展示综合得分与样本判定,列表补充详情入口,配套两份回归脚本。
This commit is contained in:
caoxiaozhu
2026-07-12 15:39:59 +08:00
parent c0f5f4a30a
commit 71ce26fff6
15 changed files with 1912 additions and 303 deletions

View File

@@ -0,0 +1,148 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const sourceRoot = path.resolve(scriptDir, '../src')
const evalViewDir = path.join(sourceRoot, 'views/eval')
const [
createSource,
taskStepSource,
ruleStepSource,
listSource,
routerSource,
apiSource,
] = await Promise.all([
readFile(path.join(evalViewDir, 'EvalCreateView.vue'), 'utf8'),
readFile(path.join(evalViewDir, 'create/EvalTaskSetupStep.vue'), 'utf8'),
readFile(path.join(evalViewDir, 'create/EvalRuleSetupStep.vue'), 'utf8'),
readFile(path.join(evalViewDir, 'EvalView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'api/modules/eval.ts'), 'utf8'),
])
function extractRouteBlock(source, routePath) {
const pathPattern = new RegExp(`path:\\s*['"]${routePath.replaceAll('/', '\\/')}['"]`)
const pathIndex = source.search(pathPattern)
assert.notEqual(pathIndex, -1, `未找到路由 ${routePath}`)
const openBrace = source.lastIndexOf('{', pathIndex)
assert.notEqual(openBrace, -1, `路由 ${routePath} 缺少左花括号`)
let depth = 0
for (let index = openBrace; index < source.length; index += 1) {
if (source[index] === '{') depth += 1
if (source[index] === '}') depth -= 1
if (depth === 0) return source.slice(openBrace + 1, index)
}
assert.fail(`路由 ${routePath} 缺少右花括号`)
}
const createDimensionRoute = extractRouteBlock(routerSource, 'model-eval/dimension/create')
assert.match(
createDimensionRoute,
/redirect:\s*['"]\/model-eval\/create['"]/,
'独立创建维度入口必须收敛到新建评测向导',
)
assert.doesNotMatch(
createDimensionRoute,
/DimensionCreateView/,
'独立创建维度路由不应继续加载旧创建页',
)
const editDimensionRoute = extractRouteBlock(routerSource, 'model-eval/dimension/:id/edit')
assert.match(
editDimensionRoute,
/DimensionCreateView\.vue/,
'编辑维度路由必须继续复用 DimensionCreateView.vue',
)
const dimensionTabStart = listSource.indexOf('<!-- 评测维度 -->')
assert.notEqual(dimensionTabStart, -1, '评测列表缺少维度标签页')
const dimensionTabSource = listSource.slice(dimensionTabStart)
assert.doesNotMatch(dimensionTabSource, /create-text=["']添加维度["']/, '维度标签页不应保留“添加维度”按钮')
assert.doesNotMatch(dimensionTabSource, /@create=["']handleCreateClick["']/, '维度标签页不应保留独立创建事件')
assert.match(
apiSource,
/createDimension\s*=\s*\([^)]*\)\s*=>\s*[\s\S]*?post<\{\s*id:\s*string\s*\|\s*number\s*\}>\(['"]\/dimension['"]\s*,\s*data\)/,
'createDimension 必须声明返回包含 string | number 类型 id',
)
assert.match(createSource, /EvalTaskSetupStep/, '向导缺少任务配置子组件')
assert.match(createSource, /EvalRuleSetupStep/, '向导缺少评测规则子组件')
assert.match(
createSource,
/任务配置[\s\S]*?评测规则/,
'评测创建向导必须按“任务配置、评测规则”定义两个步骤',
)
assert.match(createSource, /currentStep\s*=\s*ref\(0\)/, '评测创建向导缺少当前步骤状态')
assert.match(createSource, /class=["']custom-wizard-steps["']/, '评测向导未复用数据处理的自定义步骤条结构')
assert.match(createSource, /class=["']step-connector["']/, '评测向导步骤条缺少连接线')
assert.match(createSource, /:aria-current=/, '步骤条缺少当前步骤的可访问性绑定')
assert.match(createSource, /currentStep === index \? 'step' : undefined/, '步骤条当前步骤语义不正确')
assert.doesNotMatch(createSource, /<el-steps\b|<el-step\b/, '评测向导不应继续使用 Element Plus 默认步骤条')
assert.match(
createSource,
/<EvalTaskSetupStep[\s\S]*?v-if=["'][^"']*currentStep[^"']*0[^"']*["']/,
'任务配置子组件没有绑定第一步',
)
assert.match(
createSource,
/<EvalRuleSetupStep[\s\S]*?v-else/,
'评测规则子组件没有绑定第二步',
)
assert.match(taskStepSource, /prop=["']dataset_id["']/, '数据集字段缺少表单校验标识')
assert.match(
`${createSource}\n${taskStepSource}`,
/data_source[\s\S]*?dataset[\s\S]*?dataset_id/,
'选择评测数据集时必须校验 dataset_id',
)
assert.match(createSource, /['"]baseline['"]/, '向导缺少基线评测分支')
assert.match(
`${createSource}\n${ruleStepSource}`,
/已有维度|existing/,
'评测规则步骤缺少使用已有维度的分支',
)
assert.match(
`${createSource}\n${ruleStepSource}`,
/新建(?:评测)?(?:维度|规则)|create/,
'评测规则步骤缺少新建维度的分支',
)
assert.match(
createSource,
/await\s+createDimension\([\s\S]*?await\s+startEval\(/,
'最终提交必须先创建新维度,再使用维度 id 启动评测',
)
assert.match(
createSource,
/createdDimension(?:\.value)?\.id|createdDimensionId|dimensionResult\.id/,
'启动评测前必须读取新建维度返回的 id',
)
assert.match(
createSource,
/eval_type[\s\S]*?baseline[\s\S]*?(?:dimension_id|createDimension)/,
'基线评测必须有明确的不创建自定义维度分支',
)
assert.match(
createSource,
/ruleMode[\s\S]*?['"]existing['"]/,
'已有维度模式必须有独立的提交分支',
)
assert.match(createSource, /:loading=["']submitting["']/, '最终提交按钮缺少 loading 状态')
assert.match(createSource, /:disabled=["'][^"']*submitting/, '提交期间必须禁用重复操作')
assert.match(createSource, /class=["'][^"']*wizard-footer/, '评测向导缺少统一底部操作栏')
assert.match(
createSource,
/\.wizard-footer\s*\{[\s\S]*?display:\s*flex[\s\S]*?justify-content:\s*flex-end/,
'底部操作栏必须保持企业表单常用的右对齐布局',
)
console.log('评测创建向导回归检查通过')

View File

@@ -0,0 +1,99 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const sourceRoot = path.resolve(scriptDir, '../src')
const [routerSource, listSource, detailSource, apiSource, mockSource] = await Promise.all([
readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'views/eval/EvalView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'views/eval/EvalDetailView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'api/modules/eval.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'mock/adapter.ts'), 'utf8'),
])
function extractRouteBlock(source, routePath) {
const pathPattern = new RegExp(`path:\\s*['"]${routePath.replaceAll('/', '\\/')}['"]`)
const pathIndex = source.search(pathPattern)
assert.notEqual(pathIndex, -1, `未找到路由 ${routePath}`)
const openBrace = source.lastIndexOf('{', pathIndex)
assert.notEqual(openBrace, -1, `路由 ${routePath} 缺少左花括号`)
let depth = 0
for (let index = openBrace; index < source.length; index += 1) {
if (source[index] === '{') depth += 1
if (source[index] === '}') depth -= 1
if (depth === 0) return source.slice(openBrace + 1, index)
}
assert.fail(`路由 ${routePath} 缺少右花括号`)
}
const detailRoute = extractRouteBlock(routerSource, 'model-eval/:id')
assert.match(detailRoute, /name:\s*['"]model-eval-detail['"]/, '评测详情路由名称不正确')
assert.match(
detailRoute,
/component:\s*\(\)\s*=>\s*import\(['"]@\/views\/eval\/EvalDetailView\.vue['"]\)/,
'评测详情路由未加载 EvalDetailView.vue',
)
assert.match(
listSource,
/router\.push\(\s*\{\s*name:\s*['"]model-eval-detail['"]\s*,\s*params:\s*\{\s*id:\s*row\.id\s*\}\s*\}\s*\)/,
'评测任务详情按钮必须使用命名路由并携带任务 id',
)
assert.doesNotMatch(listSource, /详情功能开发中/, '评测任务详情入口仍是占位提示')
assert.match(apiSource, /export\s+const\s+getEvalDetail\b/, '评测 API 缺少 getEvalDetail')
assert.match(
apiSource,
/get(?:<[^>]+>)?\(`\/model-eval\/\$\{id\}`\)/,
'getEvalDetail 未请求 GET /model-eval/:id',
)
assert.ok(
mockSource.includes('/^\\/model-eval\\/([^/]+)$/'),
'Mock 未按任务 id 匹配 /model-eval/:id',
)
assert.match(mockSource, /method\s*===\s*['"]get['"]/, '评测详情 Mock 未处理 GET 请求')
for (const className of [
'overall-review',
'score-hero',
'improvement-list',
'sample-results-table',
'dimension-summary',
]) {
assert.match(
detailSource,
new RegExp(`class=["'][^"']*\\b${className}\\b`),
`评测详情页缺少 .${className} 结构`,
)
}
for (const label of ['输入', '标准答案', '模型回答', '得分', '判定']) {
assert.match(
detailSource,
new RegExp(`(?:label=["'][^"']*${label}[^"']*["']|>${label}<)`),
`样本结果列表缺少“${label}”语义`,
)
}
assert.match(detailSource, /\bloading\b/, '评测详情页缺少加载状态')
assert.match(detailSource, /\berror\b/, '评测详情页缺少错误状态')
assert.match(
detailSource,
/(?:el-empty|empty-text|样本结果为空|暂无样本|暂无数据)/,
'评测详情页缺少空状态',
)
assert.match(detailSource, /<el-pagination\b/, '评测详情页缺少样本分页')
for (const marketingLabel of ['OVERALL SCORE', 'LLM REVIEW', 'DIMENSIONS', 'SAMPLE RESULTS']) {
assert.doesNotMatch(detailSource, new RegExp(marketingLabel), `企业详情页不应保留展示型英文标签:${marketingLabel}`)
}
assert.doesNotMatch(detailSource, /linear-gradient\s*\(/, '企业详情页不应使用大面积渐变背景')
assert.match(detailSource, /grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)/, '桌面端概览或维度区应采用紧凑四列布局')
console.log('评测详情页回归检查通过')