import type { DetailUiProtocol, DetailUiScreenProtocol, DetailUiTextBlock, } from "@/lib/detail-ui-protocol"; export type DetailUiValidationIssueType = | "out_of_safe_area" | "block_overlap" | "reading_order" | "typography_hierarchy"; export interface DetailUiValidationIssue { type: DetailUiValidationIssueType; message: string; screenIndex: number; blockIds?: string[]; } export interface DetailUiValidationResult { passed: boolean; issues: DetailUiValidationIssue[]; } function intersects(a: DetailUiTextBlock["frame"], b: DetailUiTextBlock["frame"]) { return ( a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y ); } function validateSingleScreen( screen: DetailUiScreenProtocol, screenSize: { width: number; height: number }, ): DetailUiValidationIssue[] { const issues: DetailUiValidationIssue[] = []; const blocks = [...screen.blocks]; for (const block of blocks) { const safe = screen.safeArea; const blockRight = block.frame.x + block.frame.width; const blockBottom = block.frame.y + block.frame.height; if ( block.frame.x < safe.left || blockRight > screenSize.width - safe.right || block.frame.y < safe.top || blockBottom > safe.bottom ) { issues.push({ type: "out_of_safe_area", screenIndex: screen.index, blockIds: [block.id], message: `屏${screen.index} 的 ${block.type} 超出安全区,需回到可读区域内。`, }); } } for (let i = 0; i < blocks.length; i += 1) { for (let j = i + 1; j < blocks.length; j += 1) { if (intersects(blocks[i]!.frame, blocks[j]!.frame)) { issues.push({ type: "block_overlap", screenIndex: screen.index, blockIds: [blocks[i]!.id, blocks[j]!.id], message: `屏${screen.index} 存在文字块重叠:${blocks[i]!.type} 与 ${blocks[j]!.type}。`, }); } } } const title = blocks.find((block) => block.type === "title"); const subtitle = blocks.find((block) => block.type === "subtitle"); const body = blocks.find((block) => block.type === "body"); if (title && subtitle && title.frame.y > subtitle.frame.y) { issues.push({ type: "reading_order", screenIndex: screen.index, blockIds: [title.id, subtitle.id], message: `屏${screen.index} 的标题和副标题顺序异常,标题必须在副标题上方。`, }); } if (subtitle && body && subtitle.frame.y > body.frame.y) { issues.push({ type: "reading_order", screenIndex: screen.index, blockIds: [subtitle.id, body.id], message: `屏${screen.index} 的副标题和正文顺序异常,副标题必须在正文上方。`, }); } if ( title && subtitle && !(title.style.fontSize > subtitle.style.fontSize) ) { issues.push({ type: "typography_hierarchy", screenIndex: screen.index, blockIds: [title.id, subtitle.id], message: `屏${screen.index} 的标题字号层级不足,标题应明显大于副标题。`, }); } if ( subtitle && body && !(subtitle.style.fontSize > body.style.fontSize) ) { issues.push({ type: "typography_hierarchy", screenIndex: screen.index, blockIds: [subtitle.id, body.id], message: `屏${screen.index} 的副标题字号层级不足,副标题应明显大于正文。`, }); } return issues; } export function validateDetailUiProtocol( protocol?: DetailUiProtocol | null, ): DetailUiValidationResult { if (!protocol) { return { passed: false, issues: [ { type: "reading_order", screenIndex: 0, message: "缺少详情 UI 协议,无法校验排版。", }, ], }; } const issues = protocol.screens.flatMap((screen) => validateSingleScreen(screen, protocol.screenSize), ); return { passed: issues.length === 0, issues, }; }