6097dacbd3
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m0s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m8s
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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m53s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m26s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m43s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m57s
CI/CD Pipeline / Validate - Style (push) Successful in 6m7s
CI/CD Pipeline / Integration Tests (push) Successful in 6m46s
AI Code Review / AI Code Review (pull_request) Successful in 7m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 10m49s
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m53s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m2s
CI/CD Pipeline / Unit Tests (push) Successful in 13m21s
CI/CD Pipeline / Validate - Security (push) Successful in 17m21s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
feat(web): 标题模板内嵌面板——移除弹窗、卡片放大(#2016) - TitleStylePanel 内联网格替代弹窗选择 - 卡片放大(90px 预览区 + 底部 meta + 紫色 radio dot + hover 操作) - 新建/编辑保留 Modal 编辑器,新建时带入当前样式 - 智能剪辑+AI数字人双入口同步改造 - 响应式 2-4 列自适应
126 lines
5.9 KiB
TypeScript
126 lines
5.9 KiB
TypeScript
/**
|
||
* 标题样式工具(#2001 / 模板系统 #2003)
|
||
*
|
||
* - snake_case TitleStyleConfig ↔ camelCase TitleSettings 互转
|
||
* - preset 归一化预览(修复"标题"两字大小不一)
|
||
*/
|
||
import type { TitleStyleConfig } from "./types"
|
||
import type { TitleSettings } from "../../pages/generate/types"
|
||
import { TITLE_PRESETS } from "./constants"
|
||
import { DEFAULT_TITLE_SETTINGS_FULL } from "../../pages/generate/types"
|
||
import type { TitleTemplate } from "./template-types"
|
||
|
||
/** snake_case TitleStyleConfig → camelCase TitleSettings(仅覆盖已知字段) */
|
||
export function titleStyleConfigToCamel(s: Partial<TitleStyleConfig>): Partial<TitleSettings> {
|
||
const out: Partial<TitleSettings> = {}
|
||
if (s.font != null) out.font = s.font
|
||
if (s.size != null) out.size = s.size
|
||
if (s.color != null) out.color = s.color
|
||
if (s.bold != null) out.bold = s.bold
|
||
if (s.italic != null) out.italic = s.italic
|
||
if (s.position != null) out.position = s.position as TitleSettings["position"]
|
||
if (s.pos_x != null) out.posX = s.pos_x
|
||
if (s.pos_y != null) out.posY = s.pos_y
|
||
if (s.line_height != null) out.lineHeight = s.line_height
|
||
if (s.margin_top != null) out.marginTop = s.margin_top
|
||
if (s.max_chars_per_line != null) out.maxCharsPerLine = s.max_chars_per_line
|
||
if (s.stroke != null) out.stroke = s.stroke
|
||
if (s.stroke_width != null) out.strokeWidth = s.stroke_width
|
||
if (s.stroke_color != null) out.strokeColor = s.stroke_color
|
||
if (s.shadow != null) out.shadow = s.shadow
|
||
if (s.shadow_offset_x != null) out.shadowOffsetX = s.shadow_offset_x
|
||
if (s.shadow_offset_y != null) out.shadowOffsetY = s.shadow_offset_y
|
||
if (s.shadow_blur != null) out.shadowBlur = s.shadow_blur
|
||
if (s.shadow_color != null) out.shadowColor = s.shadow_color
|
||
if (s.bg_enabled != null) out.bgEnabled = s.bg_enabled
|
||
if (s.bg_color != null) out.bgColor = s.bg_color
|
||
if (s.bg_padding != null) out.bgPadding = s.bg_padding
|
||
if (s.bg_radius != null) out.bgRadius = s.bg_radius
|
||
if (s.line_overrides != null) out.lineOverrides = s.line_overrides
|
||
return out
|
||
}
|
||
|
||
/** camelCase TitleSettings patch → snake_case TitleStyleConfig patch */
|
||
export function camelToTitleStyleConfig(p: Partial<TitleSettings>): Partial<TitleStyleConfig> {
|
||
const out: Partial<TitleStyleConfig> = {}
|
||
if (p.font != null) out.font = p.font
|
||
if (p.size != null) out.size = p.size
|
||
if (p.color != null) out.color = p.color
|
||
if (p.bold != null) out.bold = p.bold
|
||
if (p.italic != null) out.italic = p.italic
|
||
if (p.position != null) out.position = p.position as TitleStyleConfig["position"]
|
||
if (p.posX != null) out.pos_x = p.posX
|
||
if (p.posY != null) out.pos_y = p.posY
|
||
if (p.lineHeight != null) out.line_height = p.lineHeight
|
||
if (p.marginTop != null) out.margin_top = p.marginTop
|
||
if (p.maxCharsPerLine != null) out.max_chars_per_line = p.maxCharsPerLine
|
||
if (p.stroke != null) out.stroke = p.stroke
|
||
if (p.strokeWidth != null) out.stroke_width = p.strokeWidth
|
||
if (p.strokeColor != null) out.stroke_color = p.strokeColor
|
||
if (p.shadow != null) out.shadow = p.shadow
|
||
if (p.shadowOffsetX != null) out.shadow_offset_x = p.shadowOffsetX
|
||
if (p.shadowOffsetY != null) out.shadow_offset_y = p.shadowOffsetY
|
||
if (p.shadowBlur != null) out.shadow_blur = p.shadowBlur
|
||
if (p.shadowColor != null) out.shadow_color = p.shadowColor
|
||
if (p.bgEnabled != null) out.bg_enabled = p.bgEnabled
|
||
if (p.bgColor != null) out.bg_color = p.bgColor
|
||
if (p.bgPadding != null) out.bg_padding = p.bgPadding
|
||
if (p.bgRadius != null) out.bg_radius = p.bgRadius
|
||
if (p.lineOverrides != null) out.line_overrides = p.lineOverrides
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* 把 preset style(snake_case)归一化为固定字号的 TitleSettings,
|
||
* 用于"预设卡片"缩略预览——所有卡片视觉上"标题"两字大小一致,便于辨识。
|
||
* 描边/阴影/背景padding 按 fixedSize / 原始 size 比例缩放,避免粗描边爆框。
|
||
*/
|
||
export function buildPresetPreviewSettings(
|
||
base: TitleSettings,
|
||
presetKey: string,
|
||
fixedSize = 56,
|
||
): TitleSettings {
|
||
const preset = TITLE_PRESETS.find((p) => p.key === presetKey)
|
||
if (!preset) return base
|
||
const origSize = preset.style.size ?? fixedSize
|
||
const ratio = fixedSize / origSize
|
||
const scale = (v: number | undefined, fallback: number): number =>
|
||
v != null ? Math.round(v * ratio) : fallback
|
||
return {
|
||
...base,
|
||
...titleStyleConfigToCamel(preset.style),
|
||
size: fixedSize,
|
||
strokeWidth: scale(preset.style.stroke_width, base.strokeWidth) ?? base.strokeWidth,
|
||
shadowOffsetX: scale(preset.style.shadow_offset_x, base.shadowOffsetX) ?? base.shadowOffsetX,
|
||
shadowOffsetY: scale(preset.style.shadow_offset_y, base.shadowOffsetY) ?? base.shadowOffsetY,
|
||
shadowBlur: scale(preset.style.shadow_blur, base.shadowBlur) ?? base.shadowBlur,
|
||
bgPadding: scale(preset.style.bg_padding, base.bgPadding) ?? base.bgPadding,
|
||
lineOverrides: [],
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 把 TitleTemplate 渲染为完整 TitleSettings(带默认值),用于卡片预览。
|
||
* 与模板选择器中保持一致,抽出共用。
|
||
*/
|
||
export function templateToPreviewSettings(t: TitleTemplate, fixedSize = 48): TitleSettings {
|
||
const base: TitleSettings = {
|
||
...DEFAULT_TITLE_SETTINGS_FULL,
|
||
...titleStyleConfigToCamel(t.style),
|
||
}
|
||
// 预览时用固定字号保证所有卡片字大小一致;描边/阴影/padding按比例缩放
|
||
const origSize = t.style.size ?? base.size
|
||
if (origSize && origSize !== fixedSize) {
|
||
const ratio = fixedSize / origSize
|
||
base.size = fixedSize
|
||
if (base.strokeWidth != null)
|
||
base.strokeWidth = Math.max(1, Math.round(base.strokeWidth * ratio))
|
||
if (base.shadowOffsetX != null) base.shadowOffsetX = Math.round(base.shadowOffsetX * ratio)
|
||
if (base.shadowOffsetY != null) base.shadowOffsetY = Math.round(base.shadowOffsetY * ratio)
|
||
if (base.shadowBlur != null) base.shadowBlur = Math.round(base.shadowBlur * ratio)
|
||
if (base.bgPadding != null) base.bgPadding = Math.round(base.bgPadding * ratio)
|
||
}
|
||
base.lineOverrides = []
|
||
return base
|
||
}
|