ai-chatbot/lib/detail-ui-validator.ts
Your Name 3e4594273b
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add detail ui protocol foundation
2026-03-21 19:30:57 +08:00

150 lines
3.9 KiB
TypeScript

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,
};
}