AI 生成 UI 的质量评测从像素对齐到交互一致性的多维评估框架一、AI 生成 UI 的评测困境——看起来像不等于用起来对当 AI 从设计稿生成前端代码时传统的肉眼比对评测方式暴露出严重不足。某次评测中AI 生成的登录页面在视觉上与设计稿高度相似——色彩、字号、间距几乎一致但测试发现密码输入框缺少typepassword属性登录按钮没有绑定任何事件表单没有form标签包裹Tab 键的焦点顺序与视觉顺序不一致。这些看不见的问题在视觉评测中完全被忽略但在真实使用中会导致功能缺失和可访问性违规。更隐蔽的问题出现在响应式场景中。AI 生成的代码在 1440px 宽度下还原度达 95%但在 768px 下布局完全崩溃——因为 AI 只看到了设计稿的桌面端版本没有推断出响应式断点规则。这些案例表明AI 生成 UI 的质量评测需要一套多维度的量化框架而非单一的相似度指标。二、AI UI 评测的多维框架——从视觉到交互的五层评估模型flowchart TD A[AI 生成 UI 评测框架] -- B[第一层视觉还原度] A -- C[第二层结构语义度] A -- D[第三层交互一致性] A -- E[第四层可访问性合规] A -- F[第五层可维护性评分] B -- B1[像素对齐率] B -- B2[色彩偏差 ΔE] B -- B3[间距偏差] C -- C1[HTML 语义标签使用率] C -- C2[ARIA 属性完整度] C -- C3[DOM 层级深度] D -- D1[焦点顺序一致性] D -- D2[交互状态覆盖度] D -- D3[响应式断点正确性] E -- E1[WCAG AA 达标率] E -- E2[键盘可达性] E -- E3[屏幕阅读器兼容性] F -- F1[Token 合规率] F -- F2[组件化程度] F -- F3[代码重复率] style A fill:#f3e5f5,stroke:#6a1b9a style B fill:#e8eaf6,stroke:#283593 style C fill:#e8f5e9,stroke:#2e7d32 style D fill:#fff3e0,stroke:#ef6c00 style E fill:#fce4ec,stroke:#c62828 style F fill:#e0f7fa,stroke:#00695c2.1 视觉还原度——像素级的量化比对视觉还原度的核心指标是像素对齐率生成页面与设计稿之间相同位置像素值完全匹配的比例。但纯粹的像素比对过于严格——一个border-radius: 12px被渲染为11.8px亚像素渲染不应被判定为错误。因此需要引入容差机制色彩容差 ΔE 2人眼不可感知间距容差 ±1px字号容差 ±0.5px。2.2 结构语义度——代码的骨骼质量视觉还原只衡量皮相结构语义度衡量骨骼。一个div堆砌的页面和一个语义标签丰富的页面视觉上可能完全一致但后者的 SEO 表现、可访问性和可维护性远优于前者。核心指标包括语义标签使用率nav、main、article等替代div的比例、ARIA 属性完整度、DOM 层级深度。2.3 交互一致性——看不见的行为评测交互一致性是当前 AI UI 评测中最薄弱的环节。它关注的是焦点顺序是否与视觉顺序一致hover/focus/active 状态是否完整响应式断点是否正确这些无法通过截图比对发现需要运行时测试。三、生产级 AI UI 评测系统——代码实现3.1 视觉还原度评测器/** * 视觉还原度评测器 * 通过截图比对计算像素对齐率 */ class VisualRegressionEvaluator { private tolerance: PixelTolerance; constructor(tolerance?: PartialPixelTolerance) { // 默认容差——人眼不可感知的偏差范围 this.tolerance { colorDeltaE: 2, // CIE ΔE 2 为不可感知 spacingPixels: 1, // 间距容差 ±1px fontSizePixels: 0.5, // 字号容差 ±0.5px ...tolerence, }; } /** * 比对两张截图的像素差异 * param designScreenshot 设计稿截图的 ImageData * param generatedScreenshot 生成页面的 ImageData * returns 逐像素比对结果 */ compare( designScreenshot: ImageData, generatedScreenshot: ImageData ): VisualComparisonResult { const { width, height } designScreenshot; // 尺寸不一致直接返回失败 if (width ! generatedScreenshot.width || height ! generatedScreenshot.height) { return { pixelAlignmentRate: 0, mismatchedPixels: width * height, totalPixels: width * height, diffRegions: [{ type: size-mismatch, bounds: { x: 0, y: 0, width, height } }], }; } let matchedPixels 0; const totalPixels width * height; const diffRegions: DiffRegion[] []; // 逐像素比对 for (let y 0; y height; y) { for (let x 0; x width; x) { const idx (y * width x) * 4; const designR designScreenshot.data[idx]; const designG designScreenshot.data[idx 1]; const designB designScreenshot.data[idx 2]; const designA designScreenshot.data[idx 3]; const genR generatedScreenshot.data[idx]; const genG generatedScreenshot.data[idx 1]; const genB generatedScreenshot.data[idx 2]; const genA generatedScreenshot.data[idx 3]; // 计算 CIE ΔE 色差 const deltaE this.calculateDeltaE( { r: designR, g: designG, b: designB }, { r: genR, g: genG, b: genB } ); // 透明度差异 const alphaDiff Math.abs(designA - genA); if (deltaE this.tolerance.colorDeltaE alphaDiff 5) { matchedPixels; } else { // 记录差异区域 diffRegions.push({ type: deltaE this.tolerance.colorDeltaE ? color-mismatch : alpha-mismatch, bounds: { x, y, width: 1, height: 1 }, deltaE, designColor: rgba(${designR},${designG},${designB},${designA}), generatedColor: rgba(${genR},${genG},${genB},${genA}), }); } } } // 合并相邻差异区域减少碎片化报告 const mergedRegions this.mergeDiffRegions(diffRegions); return { pixelAlignmentRate: matchedPixels / totalPixels, mismatchedPixels: totalPixels - matchedPixels, totalPixels, diffRegions: mergedRegions, }; } /** * 计算 CIE ΔE2000 色差简化版 * 生产环境应使用完整 ΔE2000 公式 */ private calculateDeltaE(c1: RGB, c2: RGB): number { // 转换为 Lab 色彩空间 const lab1 this.rgbToLab(c1); const lab2 this.rgbToLab(c2); // CIE76 简化公式ΔE2000 计算复杂此处用 CIE76 近似 const deltaL lab1.l - lab2.l; const deltaA lab1.a - lab2.a; const deltaB lab1.b - lab2.b; return Math.sqrt(deltaL * deltaL deltaA * deltaA deltaB * deltaB); } /** * RGB 转 Lab 色彩空间 */ private rgbToLab(rgb: RGB): Lab { // 步骤 1RGB - 线性 RGB - XYZ const r this.linearize(rgb.r / 255); const g this.linearize(rgb.g / 255); const b this.linearize(rgb.b / 255); const x (r * 0.4124 g * 0.3576 b * 0.1805) / 0.95047; const y (r * 0.2126 g * 0.7152 b * 0.0722) / 1.00000; const z (r * 0.0193 g * 0.1192 b * 0.9505) / 1.08883; // 步骤 2XYZ - Lab const fx this.labF(x); const fy this.labF(y); const fz this.labF(z); return { l: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz), }; } private linearize(c: number): number { return c 0.04045 ? Math.pow((c 0.055) / 1.055, 2.4) : c / 12.92; } private labF(t: number): number { return t 0.008856 ? Math.cbrt(t) : (7.787 * t 16 / 116); } /** * 合并相邻差异区域 * 将距离 5px 的差异区域合并为一个减少碎片化 */ private mergeDiffRegions(regions: DiffRegion[]): DiffRegion[] { if (regions.length 0) return []; // 按坐标排序 const sorted [...regions].sort((a, b) { const posA a.bounds.y * 10000 a.bounds.x; const posB b.bounds.y * 10000 b.bounds.x; return posA - posB; }); const merged: DiffRegion[] [sorted[0]]; for (let i 1; i sorted.length; i) { const last merged[merged.length - 1]; const current sorted[i]; // 如果距离足够近合并 const distance Math.abs(current.bounds.x - last.bounds.x) Math.abs(current.bounds.y - last.bounds.y); if (distance 5) { last.bounds.width Math.max( last.bounds.width, current.bounds.x - last.bounds.x current.bounds.width ); last.bounds.height Math.max( last.bounds.height, current.bounds.y - last.bounds.y current.bounds.height ); } else { merged.push(current); } } return merged; } } // 类型定义 interface PixelTolerance { colorDeltaE: number; spacingPixels: number; fontSizePixels: number; } interface RGB { r: number; g: number; b: number; } interface Lab { l: number; a: number; b: number; } interface DiffRegion { type: string; bounds: { x: number; y: number; width: number; height: number }; deltaE?: number; designColor?: string; generatedColor?: string; } interface VisualComparisonResult { pixelAlignmentRate: number; mismatchedPixels: number; totalPixels: number; diffRegions: DiffRegion[]; }3.2 交互一致性评测器/** * 交互一致性评测器 * 通过 Puppeteer 运行时测试检测交互行为 */ class InteractionConsistencyEvaluator { /** * 评测焦点顺序一致性 * 逐个 Tab记录焦点元素的视觉位置检查是否从上到下、从左到右 */ async evaluateFocusOrder(page: any): PromiseFocusOrderReport { const focusSequence: FocusPosition[] []; // 获取所有可聚焦元素 const focusableSelectors [ a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex-1]), ].join(, ); const elements await page.$$eval(focusableSelectors, (els) els.map((el, index) { const rect el.getBoundingClientRect(); return { index, tag: el.tagName.toLowerCase(), text: (el.textContent || ).slice(0, 30), x: rect.left rect.width / 2, y: rect.top rect.height / 2, width: rect.width, height: rect.height, }; }) ); // 检查焦点顺序是否与视觉顺序一致 const issues: FocusOrderIssue[] []; for (let i 1; i elements.length; i) { const prev elements[i - 1]; const curr elements[i]; // 如果当前元素在视觉上位于前一个元素的上方说明焦点顺序与视觉顺序不一致 if (curr.y prev.y - 10 curr.x prev.x) { issues.push({ severity: error, message: 焦点顺序与视觉顺序不一致: ${prev.tag} ${prev.text} → ${curr.tag} ${curr.text}, prevElement: prev, currElement: curr, suggestion: 调整 DOM 顺序或使用 tabindex 修正焦点顺序, }); } } return { totalFocusableElements: elements.length, issues, score: Math.max(0, 100 - issues.length * 10), }; } /** * 评测交互状态覆盖度 * 检查组件是否定义了 hover / focus / active / disabled 状态 */ async evaluateStateCoverage(page: any): PromiseStateCoverageReport { const interactiveElements await page.$$eval( button, a, input, select, (els) els.map((el) { const tag el.tagName.toLowerCase(); const computed window.getComputedStyle(el); const hasDisabled el.hasAttribute(disabled) || computed.pointerEvents none; return { tag, text: (el.textContent || ).slice(0, 30), hasHoverState: false, // 需要运行时检测 hasFocusStyle: computed.outlineStyle ! none || computed.outlineWidth ! 0px || computed.boxShadow.includes(focus), isDisabled: hasDisabled, }; }) ); const issues: StateIssue[] []; for (const el of interactiveElements) { // 检查 focus 状态 if (!el.hasFocusStyle !el.isDisabled) { issues.push({ severity: warning, element: ${el.tag}: ${el.text}, missingState: focus, suggestion: 添加 :focus-visible 样式确保键盘用户可见焦点, }); } } return { totalElements: interactiveElements.length, issues, score: Math.max(0, 100 - issues.length * 5), }; } } interface FocusPosition { index: number; tag: string; text: string; x: number; y: number; width: number; height: number; } interface FocusOrderIssue { severity: error | warning; message: string; prevElement: FocusPosition; currElement: FocusPosition; suggestion: string; } interface FocusOrderReport { totalFocusableElements: number; issues: FocusOrderIssue[]; score: number; } interface StateIssue { severity: error | warning; element: string; missingState: string; suggestion: string; } interface StateCoverageReport { totalElements: number; issues: StateIssue[]; score: number; }3.3 综合评分与报告生成/** * AI UI 评测综合评分器 * 将五个维度的评分加权汇总 */ class AIUIEvaluationOrchestrator { // 各维度权重——根据项目优先级调整 private weights { visual: 0.25, // 视觉还原度 semantic: 0.20, // 结构语义度 interaction: 0.25, // 交互一致性 accessibility: 0.20, // 可访问性合规 maintainability: 0.10, // 可维护性 }; /** * 执行完整评测 */ async evaluate(input: EvaluationInput): PromiseEvaluationReport { const visual await this.evaluateVisual(input); const semantic this.evaluateSemantic(input.html); const interaction await this.evaluateInteraction(input.page); const accessibility await this.evaluateAccessibility(input.html); const maintainability this.evaluateMaintainability(input.html, input.css); // 加权汇总 const overallScore visual.score * this.weights.visual semantic.score * this.weights.semantic interaction.score * this.weights.interaction accessibility.score * this.weights.accessibility maintainability.score * this.weights.maintainability; return { overallScore: Math.round(overallScore), dimensions: { visual, semantic, interaction, accessibility, maintainability }, recommendation: this.getRecommendation(overallScore), blockingIssues: this.collectBlockingIssues([ visual, semantic, interaction, accessibility, maintainability, ]), }; } private getRecommendation(score: number): string { if (score 90) return 可直接进入人工审查阶段; if (score 75) return 需修复关键问题后进入人工审查; if (score 60) return 需大幅修正建议重新生成; return 质量不达标建议调整生成策略后重新生成; } private collectBlockingIssues(dimensions: DimensionResult[]): BlockingIssue[] { return dimensions.flatMap(d d.issues .filter(i i.severity error) .map(i ({ dimension: d.name, ...i })) ); } }四、AI UI 评测框架的架构权衡——精度、成本与覆盖度4.1 像素比对的精度与性能逐像素比对在 1440×900 的截图上需要比较 1,296,000 个像素每个像素需要 RGB→Lab 转换和 ΔE 计算单次比对耗时约 2-5 秒。对于需要评测 50 页面的项目总耗时可能达到 2-4 分钟。优化方案是先进行降采样比对1/4 分辨率只在差异区域进行全分辨率比对。4.2 交互评测的环境依赖交互一致性评测依赖 Puppeteer 等浏览器自动化工具这意味着评测环境必须具备完整的浏览器运行时。在 CI 环境中Puppeteer 的安装和配置可能增加构建时间 30-60 秒。此外hover 状态的模拟依赖 CSS:hover伪类某些浏览器实现可能不一致。4.3 评分权重的项目依赖五个维度的权重应根据项目类型调整。营销页面应提高视觉还原度权重0.35降低可维护性权重0.05内部工具应提高交互一致性权重0.35降低视觉还原度权重0.15。当前框架使用固定权重未来可支持项目级配置。4.4 禁用场景以下场景不建议使用该评测框架纯静态展示页面交互和可访问性维度无意义设计稿本身不规范的场景与错误的基准对齐比不对齐更危险AI 生成原型阶段原型质量要求低于交付标准过早评测浪费资源。五、总结AI 生成 UI 的质量评测需要从单一的视觉比对扩展为五维评估模型视觉还原度、结构语义度、交互一致性、可访问性合规和可维护性。视觉还原度通过 CIE ΔE 色差和像素对齐率量化交互一致性通过焦点顺序和状态覆盖度检测可访问性通过 WCAG 标准校验。综合评分采用加权汇总权重应根据项目类型调整。评测框架的定位是质量门禁——在 AI 生成代码进入人工审查之前自动拦截不达标的结果将审查精力集中在真正需要人工判断的维度上。
AI 生成 UI 的质量评测:从像素对齐到交互一致性的多维评估框架
AI 生成 UI 的质量评测从像素对齐到交互一致性的多维评估框架一、AI 生成 UI 的评测困境——看起来像不等于用起来对当 AI 从设计稿生成前端代码时传统的肉眼比对评测方式暴露出严重不足。某次评测中AI 生成的登录页面在视觉上与设计稿高度相似——色彩、字号、间距几乎一致但测试发现密码输入框缺少typepassword属性登录按钮没有绑定任何事件表单没有form标签包裹Tab 键的焦点顺序与视觉顺序不一致。这些看不见的问题在视觉评测中完全被忽略但在真实使用中会导致功能缺失和可访问性违规。更隐蔽的问题出现在响应式场景中。AI 生成的代码在 1440px 宽度下还原度达 95%但在 768px 下布局完全崩溃——因为 AI 只看到了设计稿的桌面端版本没有推断出响应式断点规则。这些案例表明AI 生成 UI 的质量评测需要一套多维度的量化框架而非单一的相似度指标。二、AI UI 评测的多维框架——从视觉到交互的五层评估模型flowchart TD A[AI 生成 UI 评测框架] -- B[第一层视觉还原度] A -- C[第二层结构语义度] A -- D[第三层交互一致性] A -- E[第四层可访问性合规] A -- F[第五层可维护性评分] B -- B1[像素对齐率] B -- B2[色彩偏差 ΔE] B -- B3[间距偏差] C -- C1[HTML 语义标签使用率] C -- C2[ARIA 属性完整度] C -- C3[DOM 层级深度] D -- D1[焦点顺序一致性] D -- D2[交互状态覆盖度] D -- D3[响应式断点正确性] E -- E1[WCAG AA 达标率] E -- E2[键盘可达性] E -- E3[屏幕阅读器兼容性] F -- F1[Token 合规率] F -- F2[组件化程度] F -- F3[代码重复率] style A fill:#f3e5f5,stroke:#6a1b9a style B fill:#e8eaf6,stroke:#283593 style C fill:#e8f5e9,stroke:#2e7d32 style D fill:#fff3e0,stroke:#ef6c00 style E fill:#fce4ec,stroke:#c62828 style F fill:#e0f7fa,stroke:#00695c2.1 视觉还原度——像素级的量化比对视觉还原度的核心指标是像素对齐率生成页面与设计稿之间相同位置像素值完全匹配的比例。但纯粹的像素比对过于严格——一个border-radius: 12px被渲染为11.8px亚像素渲染不应被判定为错误。因此需要引入容差机制色彩容差 ΔE 2人眼不可感知间距容差 ±1px字号容差 ±0.5px。2.2 结构语义度——代码的骨骼质量视觉还原只衡量皮相结构语义度衡量骨骼。一个div堆砌的页面和一个语义标签丰富的页面视觉上可能完全一致但后者的 SEO 表现、可访问性和可维护性远优于前者。核心指标包括语义标签使用率nav、main、article等替代div的比例、ARIA 属性完整度、DOM 层级深度。2.3 交互一致性——看不见的行为评测交互一致性是当前 AI UI 评测中最薄弱的环节。它关注的是焦点顺序是否与视觉顺序一致hover/focus/active 状态是否完整响应式断点是否正确这些无法通过截图比对发现需要运行时测试。三、生产级 AI UI 评测系统——代码实现3.1 视觉还原度评测器/** * 视觉还原度评测器 * 通过截图比对计算像素对齐率 */ class VisualRegressionEvaluator { private tolerance: PixelTolerance; constructor(tolerance?: PartialPixelTolerance) { // 默认容差——人眼不可感知的偏差范围 this.tolerance { colorDeltaE: 2, // CIE ΔE 2 为不可感知 spacingPixels: 1, // 间距容差 ±1px fontSizePixels: 0.5, // 字号容差 ±0.5px ...tolerence, }; } /** * 比对两张截图的像素差异 * param designScreenshot 设计稿截图的 ImageData * param generatedScreenshot 生成页面的 ImageData * returns 逐像素比对结果 */ compare( designScreenshot: ImageData, generatedScreenshot: ImageData ): VisualComparisonResult { const { width, height } designScreenshot; // 尺寸不一致直接返回失败 if (width ! generatedScreenshot.width || height ! generatedScreenshot.height) { return { pixelAlignmentRate: 0, mismatchedPixels: width * height, totalPixels: width * height, diffRegions: [{ type: size-mismatch, bounds: { x: 0, y: 0, width, height } }], }; } let matchedPixels 0; const totalPixels width * height; const diffRegions: DiffRegion[] []; // 逐像素比对 for (let y 0; y height; y) { for (let x 0; x width; x) { const idx (y * width x) * 4; const designR designScreenshot.data[idx]; const designG designScreenshot.data[idx 1]; const designB designScreenshot.data[idx 2]; const designA designScreenshot.data[idx 3]; const genR generatedScreenshot.data[idx]; const genG generatedScreenshot.data[idx 1]; const genB generatedScreenshot.data[idx 2]; const genA generatedScreenshot.data[idx 3]; // 计算 CIE ΔE 色差 const deltaE this.calculateDeltaE( { r: designR, g: designG, b: designB }, { r: genR, g: genG, b: genB } ); // 透明度差异 const alphaDiff Math.abs(designA - genA); if (deltaE this.tolerance.colorDeltaE alphaDiff 5) { matchedPixels; } else { // 记录差异区域 diffRegions.push({ type: deltaE this.tolerance.colorDeltaE ? color-mismatch : alpha-mismatch, bounds: { x, y, width: 1, height: 1 }, deltaE, designColor: rgba(${designR},${designG},${designB},${designA}), generatedColor: rgba(${genR},${genG},${genB},${genA}), }); } } } // 合并相邻差异区域减少碎片化报告 const mergedRegions this.mergeDiffRegions(diffRegions); return { pixelAlignmentRate: matchedPixels / totalPixels, mismatchedPixels: totalPixels - matchedPixels, totalPixels, diffRegions: mergedRegions, }; } /** * 计算 CIE ΔE2000 色差简化版 * 生产环境应使用完整 ΔE2000 公式 */ private calculateDeltaE(c1: RGB, c2: RGB): number { // 转换为 Lab 色彩空间 const lab1 this.rgbToLab(c1); const lab2 this.rgbToLab(c2); // CIE76 简化公式ΔE2000 计算复杂此处用 CIE76 近似 const deltaL lab1.l - lab2.l; const deltaA lab1.a - lab2.a; const deltaB lab1.b - lab2.b; return Math.sqrt(deltaL * deltaL deltaA * deltaA deltaB * deltaB); } /** * RGB 转 Lab 色彩空间 */ private rgbToLab(rgb: RGB): Lab { // 步骤 1RGB - 线性 RGB - XYZ const r this.linearize(rgb.r / 255); const g this.linearize(rgb.g / 255); const b this.linearize(rgb.b / 255); const x (r * 0.4124 g * 0.3576 b * 0.1805) / 0.95047; const y (r * 0.2126 g * 0.7152 b * 0.0722) / 1.00000; const z (r * 0.0193 g * 0.1192 b * 0.9505) / 1.08883; // 步骤 2XYZ - Lab const fx this.labF(x); const fy this.labF(y); const fz this.labF(z); return { l: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz), }; } private linearize(c: number): number { return c 0.04045 ? Math.pow((c 0.055) / 1.055, 2.4) : c / 12.92; } private labF(t: number): number { return t 0.008856 ? Math.cbrt(t) : (7.787 * t 16 / 116); } /** * 合并相邻差异区域 * 将距离 5px 的差异区域合并为一个减少碎片化 */ private mergeDiffRegions(regions: DiffRegion[]): DiffRegion[] { if (regions.length 0) return []; // 按坐标排序 const sorted [...regions].sort((a, b) { const posA a.bounds.y * 10000 a.bounds.x; const posB b.bounds.y * 10000 b.bounds.x; return posA - posB; }); const merged: DiffRegion[] [sorted[0]]; for (let i 1; i sorted.length; i) { const last merged[merged.length - 1]; const current sorted[i]; // 如果距离足够近合并 const distance Math.abs(current.bounds.x - last.bounds.x) Math.abs(current.bounds.y - last.bounds.y); if (distance 5) { last.bounds.width Math.max( last.bounds.width, current.bounds.x - last.bounds.x current.bounds.width ); last.bounds.height Math.max( last.bounds.height, current.bounds.y - last.bounds.y current.bounds.height ); } else { merged.push(current); } } return merged; } } // 类型定义 interface PixelTolerance { colorDeltaE: number; spacingPixels: number; fontSizePixels: number; } interface RGB { r: number; g: number; b: number; } interface Lab { l: number; a: number; b: number; } interface DiffRegion { type: string; bounds: { x: number; y: number; width: number; height: number }; deltaE?: number; designColor?: string; generatedColor?: string; } interface VisualComparisonResult { pixelAlignmentRate: number; mismatchedPixels: number; totalPixels: number; diffRegions: DiffRegion[]; }3.2 交互一致性评测器/** * 交互一致性评测器 * 通过 Puppeteer 运行时测试检测交互行为 */ class InteractionConsistencyEvaluator { /** * 评测焦点顺序一致性 * 逐个 Tab记录焦点元素的视觉位置检查是否从上到下、从左到右 */ async evaluateFocusOrder(page: any): PromiseFocusOrderReport { const focusSequence: FocusPosition[] []; // 获取所有可聚焦元素 const focusableSelectors [ a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex-1]), ].join(, ); const elements await page.$$eval(focusableSelectors, (els) els.map((el, index) { const rect el.getBoundingClientRect(); return { index, tag: el.tagName.toLowerCase(), text: (el.textContent || ).slice(0, 30), x: rect.left rect.width / 2, y: rect.top rect.height / 2, width: rect.width, height: rect.height, }; }) ); // 检查焦点顺序是否与视觉顺序一致 const issues: FocusOrderIssue[] []; for (let i 1; i elements.length; i) { const prev elements[i - 1]; const curr elements[i]; // 如果当前元素在视觉上位于前一个元素的上方说明焦点顺序与视觉顺序不一致 if (curr.y prev.y - 10 curr.x prev.x) { issues.push({ severity: error, message: 焦点顺序与视觉顺序不一致: ${prev.tag} ${prev.text} → ${curr.tag} ${curr.text}, prevElement: prev, currElement: curr, suggestion: 调整 DOM 顺序或使用 tabindex 修正焦点顺序, }); } } return { totalFocusableElements: elements.length, issues, score: Math.max(0, 100 - issues.length * 10), }; } /** * 评测交互状态覆盖度 * 检查组件是否定义了 hover / focus / active / disabled 状态 */ async evaluateStateCoverage(page: any): PromiseStateCoverageReport { const interactiveElements await page.$$eval( button, a, input, select, (els) els.map((el) { const tag el.tagName.toLowerCase(); const computed window.getComputedStyle(el); const hasDisabled el.hasAttribute(disabled) || computed.pointerEvents none; return { tag, text: (el.textContent || ).slice(0, 30), hasHoverState: false, // 需要运行时检测 hasFocusStyle: computed.outlineStyle ! none || computed.outlineWidth ! 0px || computed.boxShadow.includes(focus), isDisabled: hasDisabled, }; }) ); const issues: StateIssue[] []; for (const el of interactiveElements) { // 检查 focus 状态 if (!el.hasFocusStyle !el.isDisabled) { issues.push({ severity: warning, element: ${el.tag}: ${el.text}, missingState: focus, suggestion: 添加 :focus-visible 样式确保键盘用户可见焦点, }); } } return { totalElements: interactiveElements.length, issues, score: Math.max(0, 100 - issues.length * 5), }; } } interface FocusPosition { index: number; tag: string; text: string; x: number; y: number; width: number; height: number; } interface FocusOrderIssue { severity: error | warning; message: string; prevElement: FocusPosition; currElement: FocusPosition; suggestion: string; } interface FocusOrderReport { totalFocusableElements: number; issues: FocusOrderIssue[]; score: number; } interface StateIssue { severity: error | warning; element: string; missingState: string; suggestion: string; } interface StateCoverageReport { totalElements: number; issues: StateIssue[]; score: number; }3.3 综合评分与报告生成/** * AI UI 评测综合评分器 * 将五个维度的评分加权汇总 */ class AIUIEvaluationOrchestrator { // 各维度权重——根据项目优先级调整 private weights { visual: 0.25, // 视觉还原度 semantic: 0.20, // 结构语义度 interaction: 0.25, // 交互一致性 accessibility: 0.20, // 可访问性合规 maintainability: 0.10, // 可维护性 }; /** * 执行完整评测 */ async evaluate(input: EvaluationInput): PromiseEvaluationReport { const visual await this.evaluateVisual(input); const semantic this.evaluateSemantic(input.html); const interaction await this.evaluateInteraction(input.page); const accessibility await this.evaluateAccessibility(input.html); const maintainability this.evaluateMaintainability(input.html, input.css); // 加权汇总 const overallScore visual.score * this.weights.visual semantic.score * this.weights.semantic interaction.score * this.weights.interaction accessibility.score * this.weights.accessibility maintainability.score * this.weights.maintainability; return { overallScore: Math.round(overallScore), dimensions: { visual, semantic, interaction, accessibility, maintainability }, recommendation: this.getRecommendation(overallScore), blockingIssues: this.collectBlockingIssues([ visual, semantic, interaction, accessibility, maintainability, ]), }; } private getRecommendation(score: number): string { if (score 90) return 可直接进入人工审查阶段; if (score 75) return 需修复关键问题后进入人工审查; if (score 60) return 需大幅修正建议重新生成; return 质量不达标建议调整生成策略后重新生成; } private collectBlockingIssues(dimensions: DimensionResult[]): BlockingIssue[] { return dimensions.flatMap(d d.issues .filter(i i.severity error) .map(i ({ dimension: d.name, ...i })) ); } }四、AI UI 评测框架的架构权衡——精度、成本与覆盖度4.1 像素比对的精度与性能逐像素比对在 1440×900 的截图上需要比较 1,296,000 个像素每个像素需要 RGB→Lab 转换和 ΔE 计算单次比对耗时约 2-5 秒。对于需要评测 50 页面的项目总耗时可能达到 2-4 分钟。优化方案是先进行降采样比对1/4 分辨率只在差异区域进行全分辨率比对。4.2 交互评测的环境依赖交互一致性评测依赖 Puppeteer 等浏览器自动化工具这意味着评测环境必须具备完整的浏览器运行时。在 CI 环境中Puppeteer 的安装和配置可能增加构建时间 30-60 秒。此外hover 状态的模拟依赖 CSS:hover伪类某些浏览器实现可能不一致。4.3 评分权重的项目依赖五个维度的权重应根据项目类型调整。营销页面应提高视觉还原度权重0.35降低可维护性权重0.05内部工具应提高交互一致性权重0.35降低视觉还原度权重0.15。当前框架使用固定权重未来可支持项目级配置。4.4 禁用场景以下场景不建议使用该评测框架纯静态展示页面交互和可访问性维度无意义设计稿本身不规范的场景与错误的基准对齐比不对齐更危险AI 生成原型阶段原型质量要求低于交付标准过早评测浪费资源。五、总结AI 生成 UI 的质量评测需要从单一的视觉比对扩展为五维评估模型视觉还原度、结构语义度、交互一致性、可访问性合规和可维护性。视觉还原度通过 CIE ΔE 色差和像素对齐率量化交互一致性通过焦点顺序和状态覆盖度检测可访问性通过 WCAG 标准校验。综合评分采用加权汇总权重应根据项目类型调整。评测框架的定位是质量门禁——在 AI 生成代码进入人工审查之前自动拦截不达标的结果将审查精力集中在真正需要人工判断的维度上。