Files
xiaoxia-saas/apps/web/src/components/title/constants.ts
T
xiaoxia 3beb65143b
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m36s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m40s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m9s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 3m41s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m52s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 28s
AI Code Review / AI Code Review (pull_request) Successful in 6m45s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 6m3s
fix(title): 修复爆款预设缩略图色块/缺字回归bug
根因(PR#2054字体重构引入):
1. Canvas fillStyle泄漏:bgEnabled预设画完背景矩形后未重置fillStyle为文字颜色,
   fillText用bgColor画字导致黑底/黄底/蓝底预设只显示色块。
2. 字体栈顺序:优设标题黑/抖音美好体把ZCOOL庆科黄油体/快乐体(Google Fonts仅400字重)
   排在Noto Sans SC(已加载400/500/700/900)之前,Canvas请求bold时无法合成粗体导致
   中文渲染异常(缺字/方框/乱码)。

修复:
- TitleMiniPreview:画完bg矩形后显式 ctx.fillStyle = settings.color;
  升级Web Font等待为async模式:await fonts.ready + fonts.load(),确保Canvas
  绘制时字体已加载到位。
- constants.ts:调整优设标题黑/抖音美好体family顺序,将Noto Sans SC(bold可用)
  前置到ZCOOL之前;ZCOOL保留作风格兜底。

验证:tsc/eslint/vitest 620 passed,Chrome headless截图10个预设全部正确渲染'标题'。
2026-09-27 10:15:08 +08:00

393 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 标题样式相关常量(#2001)
* - 字体列表(新增4款爆款字体)
* - 色板(常用标题字色/描边色/背景色)
* - 预设样式方案(10 个,含抖音爆款黄)
*/
import type { TitleStyleConfig } from "./types"
/* ── 字体选项(#2001:新增优设标题黑/阿里普惠体Bold/抖音美好体/思源黑体Heavy) ── */
export interface FontOption {
value: string
label: string
/** CSS font-family 栈 */
family: string
/** 爆款/常用标签 */
tag?: "hot" | "new"
}
export const FONT_OPTIONS: FontOption[] = [
{
value: "优设标题黑",
label: "优设标题黑",
// 原版"优设标题黑"为商用字体非开源;优先本地已安装字体,兜底用 Noto Sans SC(Google Fonts 已加载 wght@900,保证 bold 字重可用),再用 ZCOOL 庆科黄油体作风格兜底
family:
'"YouSheBiaoTiHei","YouShe Title Black","Noto Sans SC","ZCOOL QingKe HuangYou","PingFang SC","Microsoft YaHei",sans-serif',
tag: "hot",
},
{
value: "阿里普惠体Bold",
label: "阿里普惠体Bold",
// 阿里普惠体需从阿里官网下载;兜底用 Noto Sans SC 900(同等字重,已在 Google Fonts wght@400;500;700;900 加载)
family:
'"Alibaba PuHuiTi","Alibaba PuHuiTi Bold","Alibaba Sans","Noto Sans SC",system-ui,"PingFang SC","Microsoft YaHei",sans-serif',
tag: "hot",
},
{
value: "抖音美好体",
label: "抖音美好体",
// 抖音美好体为版权字体;兜底用 Noto Sans SC(确保 bold 字重可用),再用 ZCOOL KuaiLe(站酷快乐体,圆润卡通风格近似)
family:
'"Douyin Sans","DouyinSans","Noto Sans SC","ZCOOL KuaiLe","PingFang SC","Microsoft YaHei",sans-serif',
tag: "hot",
},
{
value: "思源黑体Heavy",
label: "思源黑体Heavy",
family:
'"Noto Sans SC","Source Han Sans SC","Source Han Sans CN Heavy","PingFang SC","Microsoft YaHei",sans-serif',
tag: "new",
},
{
value: "思源黑体",
label: "思源黑体",
family: '"Noto Sans SC","Source Han Sans SC","PingFang SC","Microsoft YaHei",sans-serif',
},
{
value: "思源宋体",
label: "思源宋体",
family: '"Noto Serif SC","Source Han Serif SC","Songti SC","SimSun",serif',
},
{
value: "苹方",
label: "苹方",
family:
'"PingFang SC",-apple-system,blinkmacsystemfont,"Helvetica Neue","Noto Sans SC",sans-serif',
},
{
value: "微软雅黑",
label: "微软雅黑",
family: '"Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif',
},
{
value: "楷体",
label: "楷体",
family: '"KaiTi","STKaiti","DFKai-SB","Kaiti SC",serif',
},
{
value: "站酷小薇体",
label: "站酷小薇体",
family: '"ZCOOL XiaoWei","Noto Serif SC",serif',
},
{
value: "马善政毛笔",
label: "马善政毛笔",
family: '"Ma Shan Zheng","STXingkai","KaiTi",cursive',
},
{
value: "龙藏体",
label: "龙藏体",
family: '"Long Cang","STXingkai","KaiTi",cursive',
},
{
value: "流江毛笔草",
label: "流江毛笔草",
family: '"Liu Jian Mao Cao","STXingkai",cursive',
},
{
value: "志莽行书",
label: "志莽行书",
family: '"Zhi Mang Xing","STXingkai",cursive',
},
]
/** 根据中文名取 font-family 栈(找不到回退思源黑体) */
export function getFontFamily(font: string): string {
const f = FONT_OPTIONS.find((x) => x.value === font)
if (f) return f.family
return FONT_OPTIONS[4].family // 思源黑体
}
/* ── 色板 ── */
/** 标题字色(常用爆款色) */
export const TITLE_COLOR_PALETTE: string[] = [
"#ffffff",
"#000000",
"#ffd700", // 抖音黄
"#ff2d55", // 抖音红
"#ff4081",
"#00e5ff",
"#d4a843",
"#ffa500",
"#52c41a",
"#1890ff",
"#7c3aed",
"#ff6b35",
]
/** 描边色(黑/白/灰为主) */
export const STROKE_COLOR_PALETTE: string[] = [
"#000000",
"#ffffff",
"#333333",
"#555555",
"#8b0000",
"#001f3f",
]
/** 背景色(带透明度) */
export const BG_COLOR_PALETTE: string[] = [
"rgba(0,0,0,0.5)",
"rgba(0,0,0,0.7)",
"rgba(0,0,0,0.3)",
"rgba(255,215,0,0.9)",
"rgba(255,45,85,0.85)",
"rgba(124,58,237,0.85)",
"rgba(24,144,255,0.85)",
"rgba(82,196,26,0.85)",
]
/* ── 预设样式方案(10 个,含抖音爆款黄) ── */
export interface TitlePreset {
key: string
label: string
emoji: string
/** 应用时覆盖到 TitleStyleConfig 的字段(其他字段保持当前值) */
style: Partial<TitleStyleConfig>
}
const BASE: Partial<TitleStyleConfig> = {
position: "bottom",
line_overrides: [],
cover_title_config: null,
}
export const TITLE_PRESETS: TitlePreset[] = [
{
key: "douyin_hot",
label: "抖音爆款黄",
emoji: "🔥",
style: {
...BASE,
font: "优设标题黑",
size: 80,
color: "#ffd700",
bold: true,
italic: false,
stroke: true,
stroke_width: 8,
stroke_color: "#000000",
shadow: true,
shadow_offset_x: 3,
shadow_offset_y: 3,
shadow_blur: 6,
shadow_color: "rgba(0,0,0,0.6)",
bg_enabled: false,
line_height: 1.25,
max_chars_per_line: 8,
},
},
{
key: "classic_white",
label: "经典白字黑描边",
emoji: "⚪",
style: {
...BASE,
font: "思源黑体Heavy",
size: 56,
color: "#ffffff",
bold: true,
italic: false,
stroke: true,
stroke_width: 5,
stroke_color: "#000000",
shadow: false,
bg_enabled: false,
line_height: 1.2,
max_chars_per_line: 10,
},
},
{
key: "red_bold",
label: "醒目红字",
emoji: "🔴",
style: {
...BASE,
font: "优设标题黑",
size: 72,
color: "#ff2d55",
bold: true,
italic: false,
stroke: true,
stroke_width: 6,
stroke_color: "#ffffff",
shadow: true,
shadow_offset_x: 2,
shadow_offset_y: 2,
shadow_blur: 5,
shadow_color: "rgba(0,0,0,0.5)",
bg_enabled: false,
line_height: 1.2,
max_chars_per_line: 9,
},
},
{
key: "black_gold",
label: "黑金质感",
emoji: "🟡",
style: {
...BASE,
font: "思源宋体",
size: 52,
color: "#d4a843",
bold: true,
italic: false,
stroke: false,
shadow: true,
shadow_offset_x: 2,
shadow_offset_y: 2,
shadow_blur: 8,
shadow_color: "rgba(0,0,0,0.8)",
bg_enabled: false,
line_height: 1.25,
max_chars_per_line: 10,
},
},
{
key: "neon_blue",
label: "霓虹发光",
emoji: "💙",
style: {
...BASE,
font: "阿里普惠体Bold",
size: 60,
color: "#00e5ff",
bold: true,
italic: false,
stroke: false,
shadow: true,
shadow_offset_x: 0,
shadow_offset_y: 0,
shadow_blur: 16,
shadow_color: "#00e5ff",
bg_enabled: false,
line_height: 1.2,
max_chars_per_line: 10,
},
},
{
key: "bg_black",
label: "黑底白字",
emoji: "⬛",
style: {
...BASE,
font: "思源黑体Heavy",
size: 52,
color: "#ffffff",
bold: true,
italic: false,
stroke: false,
shadow: false,
bg_enabled: true,
bg_color: "rgba(0,0,0,0.7)",
bg_padding: 16,
bg_radius: 8,
line_height: 1.3,
max_chars_per_line: 10,
},
},
{
key: "bg_yellow",
label: "黄底黑字",
emoji: "🟨",
style: {
...BASE,
font: "抖音美好体",
size: 56,
color: "#000000",
bold: true,
italic: false,
stroke: false,
shadow: false,
bg_enabled: true,
bg_color: "rgba(255,215,0,0.95)",
bg_padding: 14,
bg_radius: 6,
line_height: 1.2,
max_chars_per_line: 9,
},
},
{
key: "sweet_pink",
label: "温柔甜美粉",
emoji: "🌸",
style: {
...BASE,
font: "阿里普惠体Bold",
size: 50,
color: "#ff4081",
bold: false,
italic: false,
stroke: true,
stroke_width: 4,
stroke_color: "#ffffff",
shadow: true,
shadow_offset_x: 2,
shadow_offset_y: 2,
shadow_blur: 4,
shadow_color: "rgba(255,64,129,0.4)",
bg_enabled: false,
line_height: 1.3,
max_chars_per_line: 11,
},
},
{
key: "business_dark",
label: "商务深色",
emoji: "💼",
style: {
...BASE,
font: "思源黑体",
size: 44,
color: "#ffffff",
bold: false,
italic: false,
stroke: false,
shadow: true,
shadow_offset_x: 1,
shadow_offset_y: 1,
shadow_blur: 3,
shadow_color: "rgba(0,0,0,0.8)",
bg_enabled: true,
bg_color: "rgba(24,144,255,0.85)",
bg_padding: 12,
bg_radius: 4,
line_height: 1.3,
max_chars_per_line: 12,
},
},
{
key: "minimal_clean",
label: "极简无描边",
emoji: "✨",
style: {
...BASE,
font: "苹方",
size: 48,
color: "#ffffff",
bold: true,
italic: false,
stroke: false,
shadow: false,
bg_enabled: false,
line_height: 1.3,
max_chars_per_line: 10,
},
},
]
/** 根据 key 获取预设 */
export function getTitlePreset(key: string): TitlePreset | undefined {
return TITLE_PRESETS.find((p) => p.key === key)
}