493 lines
16 KiB
TypeScript
493 lines
16 KiB
TypeScript
import type { DetailUiGlobalStyle } from "@/lib/detail-ui-protocol";
|
|
import type { DetailUiDesignSkillSummary } from "@/lib/detail-ui-design-skill";
|
|
import type { UiScreenDocument, UiScreenLayer } from "@/lib/ui-screen/schema";
|
|
|
|
export const DETAIL_BRAND_STYLE_BOARD_VERSION = "detail-brand-style-board/v1";
|
|
|
|
type DetailStyleBoardBrief = {
|
|
productName?: string;
|
|
brandTone?: string;
|
|
coreNeed?: string;
|
|
usageScenario?: string;
|
|
coreSellingPoint?: string;
|
|
};
|
|
|
|
type PaletteSwatch = {
|
|
id: string;
|
|
name: string;
|
|
role: string;
|
|
hex: string;
|
|
};
|
|
|
|
function normalizeText(value: unknown, fallback = "") {
|
|
return String(value || "").replace(/\s+/g, " ").trim() || fallback;
|
|
}
|
|
function extractHexColors(value: unknown) {
|
|
const matched = normalizeText(value).match(/#[0-9a-fA-F]{6}\b/g) || [];
|
|
return Array.from(new Set(matched.map((item) => item.toLowerCase()))).slice(0, 6);
|
|
}
|
|
|
|
function hashText(value: string) {
|
|
let hash = 0;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
function chooseBasePalette(params: {
|
|
language: "zh" | "en";
|
|
brief?: DetailStyleBoardBrief;
|
|
globalStyle?: DetailUiGlobalStyle;
|
|
}): PaletteSwatch[] {
|
|
const extracted = extractHexColors(params.globalStyle?.palette);
|
|
if (extracted.length >= 3) {
|
|
const fallbacks = ["#f8fafc", "#0f172a", "#0071e3", "#14b8a6", "#f59e0b"];
|
|
const colors = [...extracted, ...fallbacks].slice(0, 5);
|
|
return [
|
|
{ id: "primary", name: "Primary", role: "主行动/重点卖点", hex: colors[0]! },
|
|
{ id: "ink", name: "Ink", role: "标题/强文字", hex: colors[1]! },
|
|
{ id: "surface", name: "Surface", role: "面板/留白", hex: colors[2]! },
|
|
{ id: "accent", name: "Accent", role: "证据/对比", hex: colors[3]! },
|
|
{ id: "warm", name: "Warm", role: "价格/提醒", hex: colors[4]! },
|
|
];
|
|
}
|
|
|
|
const tone = normalizeText(params.brief?.brandTone).toLowerCase();
|
|
const seed = `${params.brief?.productName || ""} ${params.brief?.coreSellingPoint || ""} ${tone}`;
|
|
const presetIndex = hashText(seed) % 5;
|
|
const reliable = /专业|可靠|信任|稳|medical|pro|trust|reliable|clinical/.test(tone);
|
|
const premium = /高级|高端|轻奢|premium|luxury|minimal|质感/.test(tone);
|
|
const friendly = /亲和|温暖|活泼|friendly|warm|soft|生活/.test(tone);
|
|
const tech = /科技|智能|未来|tech|smart|digital|ai/.test(tone);
|
|
|
|
if (premium) {
|
|
return [
|
|
{ id: "primary", name: "Obsidian", role: "主标题/高级底色", hex: "#111827" },
|
|
{ id: "ink", name: "Graphite", role: "正文文字", hex: "#1f2937" },
|
|
{ id: "surface", name: "Porcelain", role: "大面积背景", hex: "#f8fafc" },
|
|
{ id: "accent", name: "Champagne", role: "重点证明/质感线", hex: "#d6a756" },
|
|
{ id: "support", name: "Mist Blue", role: "辅助信息", hex: "#dbeafe" },
|
|
];
|
|
}
|
|
|
|
if (friendly) {
|
|
return [
|
|
{ id: "primary", name: "Coral", role: "主 CTA/亲和重点", hex: "#e85d75" },
|
|
{ id: "ink", name: "Cocoa Ink", role: "标题文字", hex: "#312e2b" },
|
|
{ id: "surface", name: "Milk", role: "干净背景", hex: "#fff7ed" },
|
|
{ id: "accent", name: "Fresh Mint", role: "舒适/安心", hex: "#2dd4bf" },
|
|
{ id: "support", name: "Sky", role: "辅助信息", hex: "#dbeafe" },
|
|
];
|
|
}
|
|
|
|
if (tech) {
|
|
return [
|
|
{ id: "primary", name: "Electric Blue", role: "主行动/科技重点", hex: "#2563eb" },
|
|
{ id: "ink", name: "Deep Navy", role: "标题文字", hex: "#0f172a" },
|
|
{ id: "surface", name: "Ice", role: "背景/面板", hex: "#eff6ff" },
|
|
{ id: "accent", name: "Signal Green", role: "性能/通过", hex: "#22c55e" },
|
|
{ id: "support", name: "Violet", role: "辅助强调", hex: "#8b5cf6" },
|
|
];
|
|
}
|
|
|
|
if (reliable || presetIndex <= 2) {
|
|
return [
|
|
{ id: "primary", name: "Apple Blue", role: "主 CTA/核心卖点", hex: "#0071e3" },
|
|
{ id: "ink", name: "Slate Ink", role: "标题/正文", hex: "#0f172a" },
|
|
{ id: "surface", name: "Cloud", role: "大面积留白", hex: "#f5f5f7" },
|
|
{ id: "accent", name: "Teal Proof", role: "证据/安心", hex: "#14b8a6" },
|
|
{ id: "support", name: "Amber", role: "提醒/价格", hex: "#f59e0b" },
|
|
];
|
|
}
|
|
|
|
return [
|
|
{ id: "primary", name: "Blue", role: "主 CTA/核心卖点", hex: "#2563eb" },
|
|
{ id: "ink", name: "Ink", role: "标题/正文", hex: "#111827" },
|
|
{ id: "surface", name: "Surface", role: "背景/卡片", hex: "#f8fafc" },
|
|
{ id: "accent", name: "Emerald", role: "证明/通过", hex: "#10b981" },
|
|
{ id: "support", name: "Rose", role: "提醒/差异", hex: "#f43f5e" },
|
|
];
|
|
}
|
|
|
|
function hexToRgb(hex: string) {
|
|
const value = hex.replace("#", "");
|
|
return {
|
|
r: Number.parseInt(value.slice(0, 2), 16),
|
|
g: Number.parseInt(value.slice(2, 4), 16),
|
|
b: Number.parseInt(value.slice(4, 6), 16),
|
|
};
|
|
}
|
|
|
|
function contrastText(hex: string) {
|
|
const { r, g, b } = hexToRgb(hex);
|
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
return luminance > 0.62 ? "#0f172a" : "#ffffff";
|
|
}
|
|
|
|
function textLayer(params: {
|
|
id: string;
|
|
text: string;
|
|
fontSize: number;
|
|
fontWeight?: number | string;
|
|
color?: string;
|
|
lineHeight?: number;
|
|
textAlign?: "left" | "center" | "right";
|
|
}): UiScreenLayer {
|
|
return {
|
|
id: params.id,
|
|
kind: "text",
|
|
text: params.text,
|
|
style: {
|
|
fontSize: params.fontSize,
|
|
fontWeight: params.fontWeight || 500,
|
|
lineHeight: params.lineHeight || 1.25,
|
|
color: params.color || "#0f172a",
|
|
textAlign: params.textAlign,
|
|
letterSpacing: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
function badgeLayer(params: {
|
|
id: string;
|
|
label: string;
|
|
tone?: "primary" | "secondary" | "neutral" | "success";
|
|
}): UiScreenLayer {
|
|
return {
|
|
id: params.id,
|
|
kind: "badge",
|
|
label: params.label,
|
|
tone: params.tone || "neutral",
|
|
style: {
|
|
fontSize: 14,
|
|
fontWeight: 700,
|
|
paddingX: 12,
|
|
paddingY: 7,
|
|
},
|
|
};
|
|
}
|
|
|
|
function colorSwatchLayer(swatch: PaletteSwatch): UiScreenLayer {
|
|
const textColor = contrastText(swatch.hex);
|
|
return {
|
|
id: `swatch-${swatch.id}`,
|
|
kind: "card",
|
|
name: swatch.name,
|
|
style: {
|
|
background: swatch.hex,
|
|
color: textColor,
|
|
borderRadius: 10,
|
|
padding: 16,
|
|
gap: 8,
|
|
boxShadow: "0 10px 26px rgba(15,23,42,0.08)",
|
|
},
|
|
children: [
|
|
textLayer({
|
|
id: `swatch-${swatch.id}-name`,
|
|
text: swatch.name,
|
|
fontSize: 18,
|
|
fontWeight: 700,
|
|
color: textColor,
|
|
}),
|
|
textLayer({
|
|
id: `swatch-${swatch.id}-hex`,
|
|
text: swatch.hex.toUpperCase(),
|
|
fontSize: 15,
|
|
fontWeight: 650,
|
|
color: textColor,
|
|
}),
|
|
textLayer({
|
|
id: `swatch-${swatch.id}-role`,
|
|
text: swatch.role,
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
color: textColor,
|
|
lineHeight: 1.35,
|
|
}),
|
|
],
|
|
};
|
|
}
|
|
|
|
function sectionFrame(params: {
|
|
id: string;
|
|
bounds: { x: number; y: number; width: number; height: number };
|
|
children: UiScreenLayer[];
|
|
gap?: number;
|
|
background?: string;
|
|
}): UiScreenLayer {
|
|
return {
|
|
id: params.id,
|
|
kind: "stack",
|
|
bounds: params.bounds,
|
|
direction: "column",
|
|
align: "stretch",
|
|
justify: "start",
|
|
style: {
|
|
background: params.background || "rgba(255,255,255,0.82)",
|
|
borderColor: "rgba(15,23,42,0.08)",
|
|
borderWidth: 1,
|
|
borderRadius: 12,
|
|
padding: 22,
|
|
gap: params.gap || 14,
|
|
boxShadow: "0 12px 30px rgba(15,23,42,0.08)",
|
|
},
|
|
children: params.children,
|
|
};
|
|
}
|
|
|
|
export function buildDetailBrandStyleBoard(params: {
|
|
language: "zh" | "en";
|
|
prompt: string;
|
|
brief?: DetailStyleBoardBrief;
|
|
globalStyle?: DetailUiGlobalStyle;
|
|
designSkill?: DetailUiDesignSkillSummary;
|
|
}): UiScreenDocument {
|
|
const isZh = params.language === "zh";
|
|
const palette = chooseBasePalette({
|
|
language: params.language,
|
|
brief: params.brief,
|
|
globalStyle: params.globalStyle,
|
|
});
|
|
const primary = palette[0]?.hex || "#0071e3";
|
|
const ink = palette[1]?.hex || "#0f172a";
|
|
const surface = palette[2]?.hex || "#f5f5f7";
|
|
const accent = palette[3]?.hex || "#14b8a6";
|
|
const productName = normalizeText(params.brief?.productName, isZh ? "详情页品牌" : "Detail Brand");
|
|
const coreNeed = normalizeText(params.brief?.coreNeed, isZh ? "核心购买理由" : "Core buying reason");
|
|
const brandTone = normalizeText(params.brief?.brandTone, isZh ? "克制、专业、可上线" : "restrained, professional, production-ready");
|
|
const fontFamily =
|
|
normalizeText(params.globalStyle?.typography?.fontFamily) ||
|
|
"Noto Sans SC, PingFang SC, Microsoft YaHei, Inter, Arial, sans-serif";
|
|
const checklist = params.designSkill?.reviewChecklist?.length
|
|
? params.designSkill.reviewChecklist.slice(0, 4)
|
|
: isZh
|
|
? ["主体完整不被遮挡", "UI 覆盖小于45%", "三层字体层级统一", "不生成假文字/假按钮"]
|
|
: ["Subject unobstructed", "UI coverage under 45%", "Three consistent type levels", "No fake image text/UI"];
|
|
|
|
return {
|
|
version: "ui-screen/v1",
|
|
id: `detail-brand-style-board-${Date.now()}`,
|
|
title: isZh ? "品牌 UI 配色板" : "Brand UI Style Board",
|
|
width: 790,
|
|
height: 1080,
|
|
tokens: {
|
|
colors: {
|
|
primary,
|
|
ink,
|
|
surface,
|
|
accent,
|
|
},
|
|
typography: {
|
|
title: { fontFamily, fontSize: 54, fontWeight: 700, lineHeight: 1.08 },
|
|
subtitle: { fontFamily, fontSize: 28, fontWeight: 600, lineHeight: 1.2 },
|
|
body: { fontFamily, fontSize: 18, fontWeight: 500, lineHeight: 1.42 },
|
|
},
|
|
radii: {
|
|
panel: 12,
|
|
card: 10,
|
|
pill: 999,
|
|
},
|
|
shadows: {
|
|
soft: "0 12px 30px rgba(15,23,42,0.08)",
|
|
},
|
|
},
|
|
layers: [
|
|
{
|
|
id: "board-background",
|
|
kind: "frame",
|
|
bounds: { x: 0, y: 0, width: 790, height: 1080 },
|
|
style: {
|
|
background: `linear-gradient(180deg, ${surface} 0%, #ffffff 52%, rgba(248,250,252,0.96) 100%)`,
|
|
},
|
|
},
|
|
{
|
|
id: "board-header",
|
|
kind: "stack",
|
|
bounds: { x: 48, y: 44, width: 694, height: 154 },
|
|
direction: "column",
|
|
align: "start",
|
|
justify: "start",
|
|
style: { gap: 12, fontFamily },
|
|
children: [
|
|
badgeLayer({
|
|
id: "board-badge",
|
|
label: isZh ? "品牌 UI 基准 / 先锁定再生成" : "Brand UI Baseline / Lock Before Generate",
|
|
tone: "primary",
|
|
}),
|
|
textLayer({
|
|
id: "board-title",
|
|
text: isZh ? `${productName} 视觉系统` : `${productName} Visual System`,
|
|
fontSize: 50,
|
|
fontWeight: 700,
|
|
lineHeight: 1.08,
|
|
color: ink,
|
|
}),
|
|
textLayer({
|
|
id: "board-subtitle",
|
|
text: isZh ? `${brandTone};围绕「${coreNeed}」建立统一详情页语言。` : `${brandTone}; build one detail-page language around "${coreNeed}".`,
|
|
fontSize: 21,
|
|
fontWeight: 500,
|
|
lineHeight: 1.34,
|
|
color: "#475569",
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
id: "palette-section",
|
|
kind: "grid",
|
|
bounds: { x: 48, y: 234, width: 694, height: 208 },
|
|
columns: 5,
|
|
style: { gap: 12 },
|
|
children: palette.map(colorSwatchLayer),
|
|
},
|
|
sectionFrame({
|
|
id: "type-section",
|
|
bounds: { x: 48, y: 480, width: 694, height: 202 },
|
|
children: [
|
|
textLayer({
|
|
id: "type-section-title",
|
|
text: isZh ? "字体层级" : "Typography",
|
|
fontSize: 22,
|
|
fontWeight: 700,
|
|
color: ink,
|
|
}),
|
|
textLayer({
|
|
id: "type-hero",
|
|
text: isZh ? "主标题:一眼说清为什么值得买" : "Hero: Say why it is worth buying",
|
|
fontSize: 40,
|
|
fontWeight: 700,
|
|
lineHeight: 1.08,
|
|
color: ink,
|
|
}),
|
|
textLayer({
|
|
id: "type-subtitle",
|
|
text: isZh ? "副标题:一句结果句,承接核心需求" : "Subtitle: one result sentence tied to the need",
|
|
fontSize: 25,
|
|
fontWeight: 600,
|
|
color: "#334155",
|
|
}),
|
|
textLayer({
|
|
id: "type-body",
|
|
text: isZh ? "正文控制在 2-4 行,解释场景、证据和转化理由,不堆满信息。" : "Body stays within 2-4 lines: scene, proof, and conversion reason without clutter.",
|
|
fontSize: 18,
|
|
fontWeight: 500,
|
|
lineHeight: 1.42,
|
|
color: "#64748b",
|
|
}),
|
|
],
|
|
}),
|
|
sectionFrame({
|
|
id: "component-section",
|
|
bounds: { x: 48, y: 714, width: 694, height: 142 },
|
|
children: [
|
|
textLayer({
|
|
id: "component-title",
|
|
text: isZh ? "组件基调" : "Component Tone",
|
|
fontSize: 22,
|
|
fontWeight: 700,
|
|
color: ink,
|
|
}),
|
|
{
|
|
id: "component-row",
|
|
kind: "grid",
|
|
columns: 3,
|
|
style: { gap: 12 },
|
|
children: [
|
|
{
|
|
id: "component-cta",
|
|
kind: "button",
|
|
label: isZh ? "主行动按钮" : "Primary CTA",
|
|
tone: "primary",
|
|
style: {
|
|
background: primary,
|
|
color: contrastText(primary),
|
|
borderRadius: 999,
|
|
paddingX: 18,
|
|
paddingY: 12,
|
|
fontSize: 15,
|
|
fontWeight: 700,
|
|
},
|
|
},
|
|
{
|
|
id: "component-proof",
|
|
kind: "badge",
|
|
label: isZh ? "证据标签" : "Proof Badge",
|
|
tone: "success",
|
|
style: {
|
|
background: accent,
|
|
color: contrastText(accent),
|
|
borderRadius: 999,
|
|
paddingX: 14,
|
|
paddingY: 9,
|
|
fontSize: 14,
|
|
fontWeight: 700,
|
|
},
|
|
},
|
|
{
|
|
id: "component-panel",
|
|
kind: "card",
|
|
style: {
|
|
background: "#ffffff",
|
|
borderColor: "rgba(15,23,42,0.10)",
|
|
borderWidth: 1,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
boxShadow: "0 10px 24px rgba(15,23,42,0.08)",
|
|
},
|
|
children: [
|
|
textLayer({
|
|
id: "component-panel-label",
|
|
text: isZh ? "轻面板 / 不挡主体" : "Light panel / keep subject clear",
|
|
fontSize: 14,
|
|
fontWeight: 650,
|
|
color: ink,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
sectionFrame({
|
|
id: "rules-section",
|
|
bounds: { x: 48, y: 888, width: 694, height: 140 },
|
|
background: "rgba(15,23,42,0.92)",
|
|
children: [
|
|
textLayer({
|
|
id: "rules-title",
|
|
text: isZh ? "后续详情页必须遵守" : "Rules For The Following Screens",
|
|
fontSize: 21,
|
|
fontWeight: 700,
|
|
color: "#f8fafc",
|
|
}),
|
|
{
|
|
id: "rules-grid",
|
|
kind: "grid",
|
|
columns: 2,
|
|
style: { gap: 10 },
|
|
children: checklist.map((item, index) =>
|
|
textLayer({
|
|
id: `rule-${index + 1}`,
|
|
text: `0${index + 1}. ${item}`,
|
|
fontSize: 15,
|
|
fontWeight: 550,
|
|
lineHeight: 1.34,
|
|
color: "#e2e8f0",
|
|
})
|
|
),
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
metadata: {
|
|
source: "detail-brand-style-board",
|
|
detailBrandStyleBoardVersion: DETAIL_BRAND_STYLE_BOARD_VERSION,
|
|
prompt: normalizeText(params.prompt).slice(0, 800),
|
|
globalStyle: params.globalStyle || null,
|
|
designSkill: params.designSkill || null,
|
|
palette,
|
|
},
|
|
};
|
|
}
|