a00031e100
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 / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
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 / Frontend Lint (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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 14s
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 Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker 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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m17s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m46s
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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m25s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m11s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m35s
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 - Style (push) Successful in 4m59s
CI/CD Pipeline / Integration Tests (push) Successful in 5m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 6m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Validate - Security (push) Successful in 9m4s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m10s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 10m10s
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
AI Code Review / AI Code Review (pull_request) Successful in 10m56s
CI/CD Pipeline / Unit Tests (push) Successful in 13m26s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
127 lines
5.8 KiB
TypeScript
127 lines
5.8 KiB
TypeScript
/**
|
||
* 标题样式工具(#2001 / 模板系统 #2003)
|
||
*
|
||
* - snake_case TitleStyleConfig <-> camelCase TitleStyleSettings 互转
|
||
* - preset 归一化预览(修复"标题"两字大小不一)
|
||
* - template -> preview settings 转换
|
||
*/
|
||
import type { TitleStyleConfig } from "./types"
|
||
import type { TitleStyleSettings } from "./settings"
|
||
import { DEFAULT_TITLE_STYLE_SETTINGS } from "./settings"
|
||
import { TITLE_PRESETS } from "./constants"
|
||
import type { TitleTemplate } from "./template-types"
|
||
|
||
/** snake_case TitleStyleConfig -> camelCase TitleStyleSettings(仅覆盖已知字段) */
|
||
export function titleStyleConfigToCamel(s: Partial<TitleStyleConfig>): Partial<TitleStyleSettings> {
|
||
const out: Partial<TitleStyleSettings> = {}
|
||
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
|
||
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 TitleStyleSettings patch -> snake_case TitleStyleConfig patch */
|
||
export function camelToTitleStyleConfig(p: Partial<TitleStyleSettings>): 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)归一化为固定字号的 TitleStyleSettings,
|
||
* 用于"预设卡片"缩略预览——所有卡片视觉上"标题"两字大小一致,便于辨识。
|
||
* 描边/阴影/背景padding 按 fixedSize / 原始 size 比例缩放,避免粗描边爆框。
|
||
*/
|
||
export function buildPresetPreviewSettings(
|
||
base: TitleStyleSettings,
|
||
presetKey: string,
|
||
fixedSize = 56,
|
||
): TitleStyleSettings {
|
||
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 {
|
||
...DEFAULT_TITLE_STYLE_SETTINGS,
|
||
...base,
|
||
...titleStyleConfigToCamel(preset.style),
|
||
size: fixedSize,
|
||
strokeWidth: scale(preset.style.stroke_width, base.strokeWidth),
|
||
shadowOffsetX: scale(preset.style.shadow_offset_x, base.shadowOffsetX),
|
||
shadowOffsetY: scale(preset.style.shadow_offset_y, base.shadowOffsetY),
|
||
shadowBlur: scale(preset.style.shadow_blur, base.shadowBlur),
|
||
bgPadding: scale(preset.style.bg_padding, base.bgPadding),
|
||
lineOverrides: [],
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 把 TitleTemplate 渲染为完整 TitleStyleSettings(带默认值),用于卡片预览。
|
||
*/
|
||
export function templateToPreviewSettings(t: TitleTemplate, fixedSize = 48): TitleStyleSettings {
|
||
const base: TitleStyleSettings = {
|
||
...DEFAULT_TITLE_STYLE_SETTINGS,
|
||
...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
|
||
}
|