150 lines
3.9 KiB
TypeScript
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,
|
|
};
|
|
}
|