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 parseTemplate } from '@vue/compiler-dom' import { parse as parseSfc } from '@vue/compiler-sfc' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const [globalStyles, routerSource, mainLayoutSource, trainingLogSource, trainingOverviewSource, fineTuneCreateSource] = await Promise.all([ readFile(path.resolve(scriptDir, '../src/styles/index.scss'), 'utf8'), readFile(path.resolve(scriptDir, '../src/router/index.ts'), 'utf8'), readFile(path.resolve(scriptDir, '../src/layouts/MainLayout.vue'), 'utf8'), readFile(path.resolve(scriptDir, '../src/views/system/TrainingLogView.vue'), 'utf8'), readFile(path.resolve(scriptDir, '../src/views/system/training-log/TrainingTaskOverview.vue'), 'utf8'), readFile(path.resolve(scriptDir, '../src/views/fine-tune/FineTuneCreateView.vue'), 'utf8'), ]) function extractCssBlock(css, marker) { const markerIndex = css.indexOf(marker) assert.notEqual(markerIndex, -1, `未找到样式规则:${marker}`) const openBrace = css.indexOf('{', markerIndex) assert.notEqual(openBrace, -1, `样式规则缺少左花括号:${marker}`) let depth = 0 for (let index = openBrace; index < css.length; index += 1) { if (css[index] === '{') depth += 1 if (css[index] === '}') depth -= 1 if (depth === 0) return css.slice(openBrace + 1, index) } assert.fail(`样式规则缺少右花括号:${marker}`) } function findElements(node, predicate, result = []) { if (node?.type === 1 && predicate(node)) result.push(node) for (const child of node?.children || []) findElements(child, predicate, result) return result } function staticAttribute(node, name) { const prop = node.props.find((item) => item.type === 6 && item.name === name) return prop?.value?.content } function boundAttribute(node, name) { const prop = node.props.find( (item) => item.type === 7 && item.name === 'bind' && item.arg?.content === name, ) return prop?.exp?.content } 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 selfSurfaceRoutes = [ 'fine-tune', 'model-eval', 'model-inference', 'model-inference/chat/:id', 'model-manage', 'data-process', 'data-process/create', 'dataset', 'compute', 'user-settings', ] for (const routePath of selfSurfaceRoutes) { const routeBlock = extractRouteBlock(routerSource, routePath) assert.match( routeBlock, /meta:\s*\{[^}]*pageSurface:\s*['"]self['"][^}]*\}/, `列表路由 /${routePath} 未声明 pageSurface: 'self'`, ) } assert.equal( routerSource.match(/pageSurface:\s*['"]self['"]/g)?.length, selfSurfaceRoutes.length, "只能给指定的自带白色表面的列表路由声明 pageSurface: 'self'", ) const rootBlock = extractCssBlock(globalStyles, ':root') assert.match(rootBlock, /--app-shell-bg:\s*#f3f5f8;/, '全局样式缺少灰色外层背景 token') assert.match(rootBlock, /--app-page-bg:\s*#ffffff;/, '全局样式缺少白色页面画布 token') assert.match(rootBlock, /--app-surface-bg:\s*#ffffff;/, '全局样式缺少统一内容表面 token') const bodyBlock = extractCssBlock(globalStyles, '\nbody {') assert.match(bodyBlock, /background-color:\s*var\(--app-shell-bg\);/, 'body 未使用灰色外层背景') const cardBlock = extractCssBlock(globalStyles, '.el-card {') assert.match(cardBlock, /background-color:\s*var\(--app-surface-bg\);/, '全局卡片未使用统一内容表面') const mainLayoutDescriptor = parseSfc(mainLayoutSource).descriptor const mainLayoutScript = mainLayoutDescriptor.scriptSetup?.content || '' const mainLayoutStyle = mainLayoutDescriptor.styles.map((item) => item.content).join('\n') const mainLayoutTemplate = mainLayoutDescriptor.template?.content || '' const mainLayoutAst = parseTemplate(mainLayoutTemplate) assert.match(mainLayoutScript, /import\s*\{[^}]*\buseRoute\b[^}]*\}\s*from\s*['"]vue-router['"]/, '主布局未引入 useRoute') assert.match(mainLayoutScript, /const\s+route\s*=\s*useRoute\(\)/, '主布局未获取当前路由') const pageCanvases = findElements( mainLayoutAst, (node) => staticAttribute(node, 'class')?.split(/\s+/).includes('page-canvas'), ) assert.equal(pageCanvases.length, 1, '主布局必须且只能提供一个全局白色页面画布') assert.match( boundAttribute(pageCanvases[0], 'class') || '', /['"]is-self-surface['"]\s*:\s*route\.meta\.pageSurface\s*===\s*['"]self['"]/, '主布局未根据 route.meta.pageSurface 为页面画布添加 is-self-surface 类', ) assert.equal( findElements(pageCanvases[0], (node) => node.tag === 'router-view').length, 1, '所有业务路由必须渲染在全局页面画布内部', ) assert.equal( findElements(mainLayoutAst, (node) => node.tag.toLowerCase() === 'transition').length, 0, '主布局仍包含会造成列表与二级页面表面错位的页面级 Transition', ) assert.doesNotMatch( mainLayoutStyle, /\.fade-(?:enter|leave)-(?:active|from|to)/, '主布局仍包含页面级透明度转场样式', ) const layoutContentBlock = extractCssBlock(mainLayoutStyle, '.layout-content') assert.match(layoutContentBlock, /background-color:\s*var\(--app-shell-bg\);/, '主内容区未使用灰色外层背景') const pageCanvasBlock = extractCssBlock(mainLayoutStyle, '\n.page-canvas {') assert.match(pageCanvasBlock, /flex:\s*1 0 auto;/, '白色页面画布没有铺满可用高度') assert.match(pageCanvasBlock, /padding:\s*24px;/, '白色页面画布缺少统一内容内边距') assert.match(pageCanvasBlock, /border-radius:\s*16px;/, '白色页面画布圆角与参考不一致') assert.match(pageCanvasBlock, /background-color:\s*var\(--app-page-bg\);/, '全局页面画布未使用白色背景') const selfSurfaceBlock = extractCssBlock(mainLayoutStyle, '.page-canvas.is-self-surface') assert.match(selfSurfaceBlock, /padding:\s*0;/, '自带表面的页面仍保留全局画布内边距') assert.match(selfSurfaceBlock, /border-radius:\s*0;/, '自带表面的页面仍保留全局画布圆角') assert.match(selfSurfaceBlock, /background-color:\s*transparent;/, '自带表面的页面未透出灰色应用背景') assert.match(selfSurfaceBlock, /box-shadow:\s*none;/, '自带表面的页面仍保留全局画布阴影') assert.match( mainLayoutStyle, /\.page-canvas:not\(\.is-self-surface\)\s*>\s*:deep\(\.page-card-host\s*>\s*\.page-card\)/, '显式根卡片宿主内的 PageCard 未被识别为页面根卡片', ) assert.doesNotMatch( mainLayoutStyle, /:deep\(\*\s*>\s*\.page-card\)/, '通用层级选择器会误伤训练日志等页面的内部业务卡片', ) assert.match( fineTuneCreateSource, /class=["'][^"']*\bfine-tune-create\b[^"']*\bpage-card-host\b[^"']*["']/, '创建训练任务页未显式标记根 PageCard 宿主', ) const rootPageCardBlock = extractCssBlock( mainLayoutStyle, '.page-canvas:not(.is-self-surface) > :deep(.page-card)', ) assert.match(rootPageCardBlock, /margin-bottom:\s*0;/, '页面根卡片仍在白色画布内保留额外外边距') assert.match(rootPageCardBlock, /background-color:\s*transparent;/, '页面根卡片仍形成第二层白色背景') assert.match(rootPageCardBlock, /border-radius:\s*0\s*!important;/, '页面根卡片仍形成第二层圆角边界') assert.match(rootPageCardBlock, /box-shadow:\s*none\s*!important;/, '页面根卡片仍形成重复卡片层级') const trainingLogStyle = [trainingLogSource, trainingOverviewSource] .flatMap((source) => parseSfc(source).descriptor.styles.map((item) => item.content)) .join('\n') const businessSurfaceBlock = extractCssBlock(trainingLogStyle, '.task-overview') assert.match( businessSurfaceBlock, /background:\s*var\(--app-surface-bg\);/, '任务、数据集和运行概况未使用统一白色内容表面', ) console.log('全局页面背景与内容表面回归检查通过')