1. 迎宾海报

This commit is contained in:
李琦
2026-08-04 08:45:09 +08:00
parent 91b2658ef0
commit 773c4953b2
6 changed files with 574 additions and 312 deletions

View File

@@ -19,33 +19,47 @@ var (
)
var allowedTemplates = map[string]bool{
"banner-bottom": true,
"banner-top": true,
"banner-center": true,
"banner-names": true,
"banner-bottom": true,
"banner-top": true,
"banner-center": true,
"banner-names": true,
"banner-lower-third": true,
"banner-split-names": true,
"banner-frame-bottom": true,
"banner-minimal": true,
"art-welcome-brush": true,
"art-love-life": true,
"art-gaobai": true,
"art-married": true,
"art-hand-love": true,
"art-like-you": true,
"art-give-home": true,
"art-xi-welcome": true,
"art-wujia": true,
"art-tianci": true,
"art-tianzuo": true,
"art-welcome-v": true,
"art-longest": true,
"art-just-love": true,
"custom": true,
// 旧 key 兼容
"banner-zh": true,
"banner-lower-third": true,
"banner-upper-third": true,
"banner-mid-band": true,
"banner-left-stack": true,
"banner-right-stack": true,
"banner-split-names": true,
"banner-vertical-left": true,
"banner-vertical-right": true,
"banner-hero-name": true,
"banner-date-focus": true,
"banner-welcome-en": true,
"banner-minimal": true,
"banner-xi-corner": true,
"banner-top-bottom": true,
"banner-frame-bottom": true,
"custom": true,
// 旧 key 兼容
"welcome-bottom": true,
"welcome-top": true,
"welcome-left": true,
"welcome-right": true,
"welcome-center": true,
"welcome-bottom": true,
"welcome-top": true,
"welcome-left": true,
"welcome-right": true,
"welcome-center": true,
}
// Register 酒店迎宾海报 CRUD均需管理员

View File

@@ -11,8 +11,14 @@
"@ant-design-vue/pro-layout": "^3.2.5",
"@ant-design/icons-vue": "^7.0.1",
"@fingerprintjs/fingerprintjs": "^5.2.0",
"@fontsource/cinzel": "^5.3.0",
"@fontsource/great-vibes": "^5.3.0",
"@fontsource/liu-jian-mao-cao": "^5.3.0",
"@fontsource/long-cang": "^5.3.0",
"@fontsource/ma-shan-zheng": "^5.3.0",
"@fontsource/pinyon-script": "^5.3.0",
"@fontsource/playfair-display": "^5.3.0",
"@fontsource/zhi-mang-xing": "^5.3.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/head": "^2.0.0",
@@ -588,6 +594,15 @@
"integrity": "sha512-j+2nInkwCQNTJcNhOjvkGM/nLRTuGJTC6xai4quqvUpjob2ssrGwBZjS7k55nOmKvge7qvJT2nS3i/IRvQSTQA==",
"license": "MIT"
},
"node_modules/@fontsource/cinzel": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/cinzel/-/cinzel-5.3.0.tgz",
"integrity": "sha512-KEOYTsrRppW0uRIPWBDPrQgunCS6f8u4nzZxYRBfo9SCknKojLYjK7B0ZoVmJhyVLefz271ZQRF9EOpHZlfhRw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/great-vibes": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/great-vibes/-/great-vibes-5.3.0.tgz",
@@ -597,6 +612,24 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/liu-jian-mao-cao": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/liu-jian-mao-cao/-/liu-jian-mao-cao-5.3.0.tgz",
"integrity": "sha512-OaFm4okm77XoGA9jp1J89tDjdWlzb68yJCUL7LV75QxUWF/qOBRVqmhP7XFFsgcn/PL+nc2dPvRGT/8yit5LZg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/long-cang": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/long-cang/-/long-cang-5.3.0.tgz",
"integrity": "sha512-lCcBJb+LWZpYCVtGvQRiZPy5/EaGPuYLycpJXUIOz28EYGGqhTVWBRqY6ucaW8l1F8gvfjutY1PoAPUrFiP0wg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/ma-shan-zheng": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/ma-shan-zheng/-/ma-shan-zheng-5.3.0.tgz",
@@ -606,6 +639,33 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/pinyon-script": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/pinyon-script/-/pinyon-script-5.3.0.tgz",
"integrity": "sha512-ilhy4l5apzyDkfuKb7PZBUT5Sf6nfxucdXT5TQ0wQDyWtDPlouLy1lQq3lMEPk1lPsHHVrd11EkPnEG8FqDgUA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/playfair-display": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/playfair-display/-/playfair-display-5.3.0.tgz",
"integrity": "sha512-uJSABalvdWc10fEPppQjQocamT21LEyRfzzWfRfqbkPz+sRv+kKpYDKyluBUskvJREkJTos9epj1WjQROGu99Q==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/zhi-mang-xing": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/zhi-mang-xing/-/zhi-mang-xing-5.3.0.tgz",
"integrity": "sha512-V21yCCm5o2PHdXF2P9KKh7uiqL8QFFuII4DRQkpG24tfyDzJPchXKsxkcvKM43xEPvQKdbhHCBbXEFjcm/TUCQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",

View File

@@ -12,8 +12,14 @@
"@ant-design-vue/pro-layout": "^3.2.5",
"@ant-design/icons-vue": "^7.0.1",
"@fingerprintjs/fingerprintjs": "^5.2.0",
"@fontsource/cinzel": "^5.3.0",
"@fontsource/great-vibes": "^5.3.0",
"@fontsource/liu-jian-mao-cao": "^5.3.0",
"@fontsource/long-cang": "^5.3.0",
"@fontsource/ma-shan-zheng": "^5.3.0",
"@fontsource/pinyon-script": "^5.3.0",
"@fontsource/playfair-display": "^5.3.0",
"@fontsource/zhi-mang-xing": "^5.3.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/head": "^2.0.0",

View File

@@ -1,15 +1,40 @@
/** 酒店迎宾易拉宝:婚礼欢迎海报模板与文字图层(非按贵宾制) */
/** 酒店迎宾易拉宝:普通文字 + 艺术字模板 */
/** 字体预设key 存库css 用于渲染 */
export const TEXT_FONTS = [
{ value: 'serif', label: '衬线正文', css: 'var(--font-serif)', sample: '新郎新娘' },
{ value: 'sans', label: '无衬线', css: 'var(--font-sans)', sample: 'WEDDING' },
{ value: 'display', label: '西文衬线', css: 'var(--font-display)', sample: 'Wedding' },
{ value: 'cinzel', label: '典雅大写', css: 'var(--font-cinzel)', sample: 'WEDDING' },
{ value: 'script', label: '英文手写', css: 'var(--font-calligraphy)', sample: 'Wedding' },
{ value: 'mashan', label: '马善政体', css: 'var(--font-mashan)', sample: '囍' },
{ value: 'brush', label: '芝麻行书', css: 'var(--font-brush)', sample: '我们结婚啦' },
{ value: 'longcang', label: '龙藏体', css: 'var(--font-longcang)', sample: '吾家有喜' },
{ value: 'liujian', label: '刘建毛草', css: 'var(--font-liujian)', sample: '以爱之名' },
{ value: 'kai', label: '楷体', css: 'var(--font-kai)', sample: '欢迎光临' },
]
const FONT_MAP = Object.fromEntries(TEXT_FONTS.map((f) => [f.value, f]))
export function resolveFontCss(key) {
return FONT_MAP[key]?.css || FONT_MAP.serif.css
}
export function fontLabel(key) {
return FONT_MAP[key]?.label || key || '衬线正文'
}
/**
* zones: 模态预览用的文字占位区域(百分比)
* texts 由 defaultTextsForTemplate 提供,可与 zones 大致对应
* zones: 模态预览占位
* artistic: 是否艺术字模板(分组展示)
*/
export const HOTEL_WELCOME_TEMPLATES = [
// —— 普通文字 ——
{
value: 'banner-bottom',
label: '底部信息',
group: '经典',
desc: '全幅婚纱照 + 底部 WEDDING / 姓名 / 日期',
group: '普通文字',
desc: 'WEDDING / 姓名 / 日期叠底部',
zones: [
{ x: 18, y: 66, w: 64, h: 7, label: 'WEDDING' },
{ x: 22, y: 76, w: 56, h: 6, label: '姓名' },
@@ -20,8 +45,8 @@ export const HOTEL_WELCOME_TEMPLATES = [
{
value: 'banner-top',
label: '顶部欢迎',
group: '经典',
desc: '顶部 Welcome 与姓名,适合下半画面主体',
group: '普通文字',
desc: '顶部 Welcome 与姓名',
zones: [
{ x: 16, y: 8, w: 68, h: 5, label: 'Welcome' },
{ x: 20, y: 16, w: 60, h: 7, label: '姓名' },
@@ -31,8 +56,8 @@ export const HOTEL_WELCOME_TEMPLATES = [
{
value: 'banner-center',
label: '居中大字',
group: '经典',
desc: '画面中部 WEDDING 与新人名',
group: '普通文字',
desc: '中部 WEDDING + 姓名',
zones: [
{ x: 14, y: 40, w: 72, h: 9, label: 'WEDDING' },
{ x: 22, y: 52, w: 56, h: 6, label: '姓名' },
@@ -42,217 +67,278 @@ export const HOTEL_WELCOME_TEMPLATES = [
{
value: 'banner-names',
label: '姓名主视觉',
group: '经典',
desc: '新人姓名作主标题,辅以日期场地',
group: '普通文字',
desc: '姓名作主标题',
zones: [
{ x: 28, y: 56, w: 44, h: 4, label: 'OUR WEDDING' },
{ x: 30, y: 64, w: 40, h: 7, label: '新郎' },
{ x: 46, y: 74, w: 8, h: 4, label: '&' },
{ x: 30, y: 81, w: 40, h: 7, label: '新娘' },
{ x: 22, y: 92, w: 56, h: 4, label: '日期·场地' },
],
},
{
value: 'banner-zh',
label: '中式欢迎',
group: '中式',
desc: '囍 + 中文欢迎语 + 姓名日期',
zones: [
{ x: 42, y: 8, w: 16, h: 8, label: '囍' },
{ x: 16, y: 70, w: 68, h: 6, label: '欢迎光临' },
{ x: 22, y: 80, w: 56, h: 6, label: '姓名' },
{ x: 24, y: 90, w: 52, h: 4, label: '日期' },
{ x: 22, y: 92, w: 56, h: 4, label: '日期' },
],
},
{
value: 'banner-lower-third',
label: '下三分之一',
group: '分区',
desc: '文字集中在画面底部三分之一',
group: '普通文字',
desc: '文字集中在底部',
zones: [
{ x: 20, y: 70, w: 60, h: 5, label: 'Welcome' },
{ x: 18, y: 78, w: 64, h: 8, label: '姓名' },
{ x: 30, y: 90, w: 40, h: 4, label: '日期' },
],
},
{
value: 'banner-upper-third',
label: '上三分之一',
group: '分区',
desc: '文字集中在顶部三分之一',
zones: [
{ x: 22, y: 8, w: 56, h: 8, label: 'WEDDING' },
{ x: 20, y: 20, w: 60, h: 6, label: '姓名' },
{ x: 30, y: 30, w: 40, h: 4, label: '日期' },
],
},
{
value: 'banner-mid-band',
label: '中部横带',
group: '分区',
desc: '中部一条信息带:标题 + 姓名 + 日期',
zones: [
{ x: 10, y: 44, w: 80, h: 5, label: 'WEDDING' },
{ x: 18, y: 52, w: 64, h: 6, label: '姓名' },
{ x: 28, y: 61, w: 44, h: 4, label: '日期' },
],
},
{
value: 'banner-left-stack',
label: '左侧堆叠',
group: '对齐',
desc: '文字左对齐堆叠在左侧',
zones: [
{ x: 8, y: 68, w: 42, h: 5, label: 'WEDDING' },
{ x: 8, y: 76, w: 48, h: 6, label: '姓名' },
{ x: 8, y: 86, w: 36, h: 4, label: '日期' },
{ x: 8, y: 92, w: 40, h: 4, label: '场地' },
],
},
{
value: 'banner-right-stack',
label: '右侧堆叠',
group: '对齐',
desc: '文字右对齐堆叠在右侧',
zones: [
{ x: 50, y: 68, w: 42, h: 5, label: 'WEDDING' },
{ x: 44, y: 76, w: 48, h: 6, label: '姓名' },
{ x: 56, y: 86, w: 36, h: 4, label: '日期' },
{ x: 52, y: 92, w: 40, h: 4, label: '场地' },
],
},
{
value: 'banner-split-names',
label: '左右姓名',
group: '对齐',
desc: '新郎 / 新娘分列两侧,中间 &',
group: '普通文字',
desc: '新郎新娘分列',
zones: [
{ x: 22, y: 72, w: 22, h: 7, label: '新郎' },
{ x: 46, y: 74, w: 8, h: 5, label: '&' },
{ x: 56, y: 72, w: 22, h: 7, label: '新娘' },
{ x: 28, y: 86, w: 44, h: 4, label: '日期' },
{ x: 24, y: 92, w: 52, h: 4, label: 'Welcome' },
],
},
{
value: 'banner-vertical-left',
label: '左侧竖排',
group: '竖排',
desc: '左侧竖排中文欢迎与姓名',
zones: [
{ x: 8, y: 28, w: 8, h: 36, label: '欢迎', vertical: true },
{ x: 20, y: 30, w: 10, h: 40, label: '姓名', vertical: true },
{ x: 34, y: 38, w: 8, h: 24, label: '日期', vertical: true },
],
},
{
value: 'banner-vertical-right',
label: '右侧竖排',
group: '竖排',
desc: '右侧竖排中文欢迎与姓名',
zones: [
{ x: 84, y: 28, w: 8, h: 36, label: '欢迎', vertical: true },
{ x: 70, y: 30, w: 10, h: 40, label: '姓名', vertical: true },
{ x: 58, y: 38, w: 8, h: 24, label: '日期', vertical: true },
],
},
{
value: 'banner-hero-name',
label: '超大姓名',
group: '强调',
desc: '姓名超大居中偏下,副标题精简',
zones: [
{ x: 12, y: 62, w: 76, h: 12, label: '新郎 & 新娘' },
{ x: 28, y: 78, w: 44, h: 4, label: 'WEDDING' },
{ x: 32, y: 86, w: 36, h: 4, label: '日期' },
],
},
{
value: 'banner-date-focus',
label: '日期主视觉',
group: '强调',
desc: '大号日期居中,姓名与标题辅之',
zones: [
{ x: 24, y: 58, w: 52, h: 5, label: 'OUR WEDDING' },
{ x: 16, y: 68, w: 68, h: 10, label: '2026.10.01' },
{ x: 22, y: 82, w: 56, h: 6, label: '姓名' },
{ x: 28, y: 92, w: 44, h: 4, label: '场地' },
],
},
{
value: 'banner-welcome-en',
label: '英文欢迎',
group: '强调',
desc: '英文 Welcome 为主,姓名日期居下',
zones: [
{ x: 10, y: 64, w: 80, h: 6, label: 'Welcome to' },
{ x: 14, y: 73, w: 72, h: 8, label: 'our Wedding' },
{ x: 22, y: 85, w: 56, h: 5, label: '姓名' },
{ x: 32, y: 93, w: 36, h: 3, label: '日期' },
],
},
{
value: 'banner-minimal',
label: '极简底部',
group: '极简',
desc: '仅底部两行:姓名 + 日期',
zones: [
{ x: 24, y: 82, w: 52, h: 6, label: '姓名' },
{ x: 32, y: 92, w: 36, h: 4, label: '日期' },
],
},
{
value: 'banner-xi-corner',
label: '角标囍',
group: '中式',
desc: '角落囍字 + 底部婚礼信息',
zones: [
{ x: 8, y: 6, w: 14, h: 8, label: '囍' },
{ x: 18, y: 72, w: 64, h: 6, label: 'WEDDING' },
{ x: 22, y: 82, w: 56, h: 6, label: '姓名' },
{ x: 28, y: 92, w: 44, h: 4, label: '日期' },
],
},
{
value: 'banner-top-bottom',
label: '上下呼应',
group: '分区',
desc: '顶部标题 + 底部姓名日期',
zones: [
{ x: 18, y: 8, w: 64, h: 6, label: 'WEDDING' },
{ x: 24, y: 16, w: 52, h: 4, label: 'Welcome' },
{ x: 20, y: 80, w: 60, h: 7, label: '姓名' },
{ x: 30, y: 91, w: 40, h: 4, label: '日期' },
],
},
{
value: 'banner-frame-bottom',
label: '底栏卡片',
group: '分区',
desc: '底部信息卡:标题 / 姓名 / 日期场地',
group: '普通文字',
desc: '底部信息卡式排版',
zones: [
{ x: 12, y: 68, w: 76, h: 5, label: 'OUR WEDDING DAY' },
{ x: 12, y: 68, w: 76, h: 5, label: 'OUR WEDDING' },
{ x: 16, y: 76, w: 68, h: 7, label: '姓名' },
{ x: 20, y: 86, w: 60, h: 4, label: '日期' },
{ x: 24, y: 92, w: 52, h: 4, label: '场地' },
],
},
{
value: 'banner-minimal',
label: '极简底部',
group: '普通文字',
desc: '仅姓名 + 日期',
zones: [
{ x: 24, y: 82, w: 52, h: 6, label: '姓名' },
{ x: 32, y: 92, w: 36, h: 4, label: '日期' },
],
},
// —— 艺术字(参考易拉宝常见排版)——
{
value: 'art-welcome-brush',
label: '笔刷欢迎',
group: '艺术字',
artistic: true,
desc: '顶部书法「欢迎来我们的婚礼」+ 底部西文姓名',
zones: [
{ x: 12, y: 10, w: 76, h: 14, label: '欢迎来…', art: true },
{ x: 22, y: 78, w: 56, h: 5, label: '姓名' },
{ x: 20, y: 86, w: 60, h: 4, label: 'WELCOME' },
{ x: 32, y: 93, w: 36, h: 3, label: '日期' },
],
},
{
value: 'art-love-life',
label: '以爱之名',
group: '艺术字',
artistic: true,
desc: '书法主句 + 底部 Welcome / 姓名',
zones: [
{ x: 14, y: 12, w: 72, h: 16, label: '以爱之名', art: true },
{ x: 24, y: 78, w: 52, h: 5, label: '姓名' },
{ x: 18, y: 87, w: 64, h: 4, label: 'WELCOME' },
{ x: 30, y: 93, w: 40, h: 3, label: '日期' },
],
},
{
value: 'art-gaobai',
label: '告白',
group: '艺术字',
artistic: true,
desc: '英文手写 Wedding Day + 大字「告白」',
zones: [
{ x: 18, y: 38, w: 50, h: 6, label: 'Wedding Day', art: true },
{ x: 22, y: 48, w: 36, h: 16, label: '告白', art: true },
{ x: 24, y: 86, w: 52, h: 5, label: '姓名' },
{ x: 30, y: 93, w: 40, h: 3, label: '日期' },
],
},
{
value: 'art-married',
label: '我们结婚啦',
group: '艺术字',
artistic: true,
desc: 'WEDDING 底字 + 笔刷「我们结婚啦」叠字',
zones: [
{ x: 10, y: 14, w: 80, h: 10, label: 'WEDDING' },
{ x: 14, y: 18, w: 72, h: 14, label: '我们结婚啦', art: true },
{ x: 18, y: 36, w: 64, h: 4, label: '【欢迎参加】' },
{ x: 20, y: 44, w: 60, h: 5, label: '姓名' },
{ x: 22, y: 88, w: 56, h: 4, label: '日期·酒店' },
],
},
{
value: 'art-hand-love',
label: '携手热爱',
group: '艺术字',
artistic: true,
desc: '行草主句「携手热爱,奔赴山海」',
zones: [
{ x: 10, y: 14, w: 80, h: 14, label: '携手热爱…', art: true },
{ x: 20, y: 32, w: 60, h: 4, label: '【欢迎参加】' },
{ x: 22, y: 40, w: 56, h: 5, label: '姓名' },
{ x: 24, y: 88, w: 52, h: 4, label: '日期·酒店' },
],
},
{
value: 'art-like-you',
label: '就是喜欢你',
group: '艺术字',
artistic: true,
desc: '金色笔刷主句 + 底部信息',
zones: [
{ x: 12, y: 16, w: 76, h: 14, label: '就是喜欢你', art: true },
{ x: 20, y: 34, w: 60, h: 4, label: '【欢迎参加】' },
{ x: 22, y: 42, w: 56, h: 5, label: '姓名' },
{ x: 24, y: 88, w: 52, h: 4, label: '日期·酒店' },
],
},
{
value: 'art-give-home',
label: '给她一个家',
group: '艺术字',
artistic: true,
desc: '描边 WELCOME + 现代感中文主句',
zones: [
{ x: 12, y: 12, w: 76, h: 6, label: 'WELCOME' },
{ x: 16, y: 22, w: 68, h: 12, label: '给她一个家', art: true },
{ x: 20, y: 38, w: 60, h: 4, label: '【欢迎参加】' },
{ x: 22, y: 46, w: 56, h: 5, label: '姓名' },
{ x: 24, y: 88, w: 52, h: 4, label: '日期·酒店' },
],
},
{
value: 'art-xi-welcome',
label: '囍·中式',
group: '艺术字',
artistic: true,
desc: '大囍 + 书法欢迎语',
zones: [
{ x: 38, y: 8, w: 24, h: 12, label: '囍', art: true },
{ x: 14, y: 68, w: 72, h: 10, label: '欢迎光临', art: true },
{ x: 22, y: 82, w: 56, h: 6, label: '姓名' },
{ x: 26, y: 92, w: 48, h: 4, label: '日期' },
],
},
{
value: 'art-wujia',
label: '吾家有喜',
group: '艺术字',
artistic: true,
desc: '竖排书法主句 + 欢迎与日期',
zones: [
{ x: 18, y: 22, w: 14, h: 48, label: '吾家有喜', art: true, vertical: true },
{ x: 40, y: 30, w: 10, h: 36, label: '欢迎', vertical: true },
{ x: 58, y: 72, w: 32, h: 5, label: '姓名' },
{ x: 58, y: 82, w: 32, h: 4, label: '日期' },
],
},
{
value: 'art-tianci',
label: '天赐良缘',
group: '艺术字',
artistic: true,
desc: '竖排书法 + 新人信息',
zones: [
{ x: 16, y: 20, w: 14, h: 50, label: '天赐良缘', art: true, vertical: true },
{ x: 38, y: 28, w: 10, h: 40, label: '婚礼', vertical: true },
{ x: 55, y: 70, w: 36, h: 5, label: '姓名' },
{ x: 55, y: 80, w: 36, h: 4, label: '日期' },
{ x: 55, y: 88, w: 36, h: 4, label: '酒店' },
],
},
{
value: 'art-tianzuo',
label: '天作之合',
group: '艺术字',
artistic: true,
desc: '四字方阵艺术字 + 欢迎语',
zones: [
{ x: 18, y: 18, w: 28, h: 12, label: '天', art: true },
{ x: 50, y: 18, w: 28, h: 12, label: '作', art: true },
{ x: 18, y: 34, w: 28, h: 12, label: '之', art: true },
{ x: 50, y: 34, w: 28, h: 12, label: '合', art: true },
{ x: 22, y: 72, w: 56, h: 5, label: '欢迎参加' },
{ x: 24, y: 82, w: 52, h: 5, label: '姓名' },
{ x: 28, y: 92, w: 44, h: 4, label: '日期' },
],
},
{
value: 'art-welcome-v',
label: '竖排欢迎',
group: '艺术字',
artistic: true,
desc: '左侧竖排书法欢迎全文',
zones: [
{ x: 10, y: 18, w: 12, h: 58, label: '欢迎参加…', art: true, vertical: true },
{ x: 30, y: 22, w: 8, h: 28, label: 'Welcome', vertical: true },
{ x: 48, y: 78, w: 42, h: 5, label: '姓名' },
{ x: 48, y: 88, w: 42, h: 4, label: '日期' },
],
},
{
value: 'art-longest',
label: '最长的告白',
group: '艺术字',
artistic: true,
desc: '笔刷主句 + 英文手写叠字',
zones: [
{ x: 12, y: 12, w: 76, h: 12, label: '最长的告白', art: true },
{ x: 16, y: 24, w: 68, h: 6, label: 'Accompanying', art: true },
{ x: 22, y: 78, w: 56, h: 5, label: '姓名' },
{ x: 26, y: 88, w: 48, h: 4, label: '日期' },
],
},
{
value: 'art-just-love',
label: '就爱这样的你',
group: '艺术字',
artistic: true,
desc: '现代笔刷主句 + 括号欢迎语',
zones: [
{ x: 12, y: 14, w: 76, h: 14, label: '就爱这样的你', art: true },
{ x: 18, y: 32, w: 64, h: 4, label: '【欢迎参加】' },
{ x: 20, y: 40, w: 60, h: 5, label: '姓名' },
{ x: 22, y: 88, w: 56, h: 4, label: '日期·酒店' },
],
},
{
value: 'custom',
label: '自定义空白',
group: '自定义',
desc: '无预设文字,自行添加并拖拽',
desc: '无预设,自行添加普通字 / 艺术字并拖拽',
zones: [],
},
]
/** 旧模板 key → 新 key兼容已保存数据 */
const LEGACY_TEMPLATE_MAP = {
'welcome-bottom': 'banner-bottom',
'welcome-top': 'banner-top',
'welcome-center': 'banner-center',
'welcome-left': 'banner-vertical-left',
'welcome-right': 'banner-vertical-right',
'welcome-left': 'art-welcome-v',
'welcome-right': 'banner-split-names',
'banner-zh': 'art-xi-welcome',
'banner-upper-third': 'banner-top',
'banner-mid-band': 'banner-center',
'banner-left-stack': 'banner-lower-third',
'banner-right-stack': 'banner-lower-third',
'banner-vertical-left': 'art-welcome-v',
'banner-vertical-right': 'art-wujia',
'banner-hero-name': 'banner-names',
'banner-date-focus': 'banner-frame-bottom',
'banner-welcome-en': 'banner-bottom',
'banner-xi-corner': 'art-xi-welcome',
'banner-top-bottom': 'banner-top',
}
export function resolveTemplate(value) {
@@ -286,6 +372,7 @@ function uid() {
}
export function createTextLayer(partial = {}) {
const fontKey = FONT_MAP[partial.fontFamily] ? partial.fontFamily : 'serif'
return {
id: partial.id || uid(),
content: partial.content ?? 'WEDDING',
@@ -293,12 +380,14 @@ export function createTextLayer(partial = {}) {
y: clampPct(partial.y ?? 70),
fontSize: Number(partial.fontSize) > 0 ? Number(partial.fontSize) : 28,
color: partial.color || '#ffffff',
fontFamily: partial.fontFamily || 'serif',
fontFamily: fontKey,
fontWeight: partial.fontWeight || '600',
align: partial.align || 'center',
letterSpacing: Number.isFinite(Number(partial.letterSpacing)) ? Number(partial.letterSpacing) : 2,
shadow: partial.shadow !== false,
vertical: !!partial.vertical,
stroke: !!partial.stroke,
strokeColor: partial.strokeColor || 'rgba(255,255,255,0.35)',
}
}
@@ -308,143 +397,179 @@ export function clampPct(n) {
return Math.min(100, Math.max(0, v))
}
/** 各模板预设:婚礼迎宾易拉宝文案位(百分比坐标,可再拖拽) */
function L(partial) {
return createTextLayer(partial)
}
/** 各模板预设文案(含艺术字体) */
export function defaultTextsForTemplate(template) {
switch (resolveTemplate(template)) {
case 'banner-top':
return [
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 10, fontSize: 18, letterSpacing: 3, fontWeight: '400' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 18, fontSize: 34, fontWeight: '700', letterSpacing: 4 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 26, fontSize: 16, letterSpacing: 4, fontWeight: '400' }),
L({ content: 'Welcome to our Wedding', x: 50, y: 10, fontSize: 18, letterSpacing: 3, fontWeight: '400', fontFamily: 'display' }),
L({ content: '新郎 & 新娘', x: 50, y: 18, fontSize: 34, fontWeight: '700', letterSpacing: 4, fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 26, fontSize: 16, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
]
case 'banner-center':
return [
createTextLayer({ content: 'WEDDING', x: 50, y: 42, fontSize: 42, letterSpacing: 12, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 54, fontSize: 28, letterSpacing: 6 }),
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 62, fontSize: 14, letterSpacing: 2, fontWeight: '400' }),
L({ content: 'WEDDING', x: 50, y: 42, fontSize: 42, letterSpacing: 12, fontWeight: '700', fontFamily: 'cinzel' }),
L({ content: '新郎 & 新娘', x: 50, y: 54, fontSize: 28, letterSpacing: 6, fontFamily: 'serif' }),
L({ content: 'Welcome to our Wedding', x: 50, y: 62, fontSize: 14, letterSpacing: 2, fontWeight: '400', fontFamily: 'display' }),
]
case 'banner-names':
return [
createTextLayer({ content: 'OUR WEDDING', x: 50, y: 58, fontSize: 14, letterSpacing: 8, fontWeight: '400' }),
createTextLayer({ content: '新郎', x: 50, y: 68, fontSize: 36, fontWeight: '700', letterSpacing: 10 }),
createTextLayer({ content: '&', x: 50, y: 76, fontSize: 22, fontWeight: '400' }),
createTextLayer({ content: '新娘', x: 50, y: 84, fontSize: 36, fontWeight: '700', letterSpacing: 10 }),
createTextLayer({ content: '2026.10.01 · 婚礼酒店', x: 50, y: 93, fontSize: 14, letterSpacing: 3, fontWeight: '400' }),
]
case 'banner-zh':
return [
createTextLayer({ content: '囍', x: 50, y: 12, fontSize: 36, color: '#c45c5c', letterSpacing: 0, shadow: false }),
createTextLayer({ content: '欢迎光临我们的婚礼', x: 50, y: 72, fontSize: 22, letterSpacing: 6 }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 82, fontSize: 32, fontWeight: '700', letterSpacing: 4 }),
createTextLayer({ content: '谨定于 2026年10月1日', x: 50, y: 91, fontSize: 14, letterSpacing: 2, fontWeight: '400' }),
L({ content: 'OUR WEDDING', x: 50, y: 58, fontSize: 14, letterSpacing: 8, fontWeight: '400', fontFamily: 'cinzel' }),
L({ content: '新郎', x: 50, y: 68, fontSize: 36, fontWeight: '700', letterSpacing: 10, fontFamily: 'serif' }),
L({ content: '&', x: 50, y: 76, fontSize: 22, fontWeight: '400', fontFamily: 'script' }),
L({ content: '新娘', x: 50, y: 84, fontSize: 36, fontWeight: '700', letterSpacing: 10, fontFamily: 'serif' }),
L({ content: '2026.10.01 · 婚礼酒店', x: 50, y: 93, fontSize: 14, letterSpacing: 3, fontWeight: '400', fontFamily: 'sans' }),
]
case 'banner-lower-third':
return [
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 72, fontSize: 16, letterSpacing: 2, fontWeight: '400' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 81, fontSize: 32, fontWeight: '700', letterSpacing: 5 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 91, fontSize: 15, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-upper-third':
return [
createTextLayer({ content: 'WEDDING', x: 50, y: 10, fontSize: 38, letterSpacing: 12, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 22, fontSize: 26, letterSpacing: 5 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 32, fontSize: 15, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-mid-band':
return [
createTextLayer({ content: 'WEDDING', x: 50, y: 46, fontSize: 28, letterSpacing: 14, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 55, fontSize: 26, letterSpacing: 6 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 63, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-left-stack':
return [
createTextLayer({ content: 'WEDDING', x: 12, y: 70, fontSize: 18, letterSpacing: 6, align: 'left', fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 12, y: 78, fontSize: 26, letterSpacing: 3, align: 'left', fontWeight: '700' }),
createTextLayer({ content: '2026.10.01', x: 12, y: 87, fontSize: 14, letterSpacing: 2, align: 'left', fontWeight: '400' }),
createTextLayer({ content: '婚礼酒店', x: 12, y: 93, fontSize: 13, letterSpacing: 2, align: 'left', fontWeight: '400' }),
]
case 'banner-right-stack':
return [
createTextLayer({ content: 'WEDDING', x: 88, y: 70, fontSize: 18, letterSpacing: 6, align: 'right', fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 88, y: 78, fontSize: 26, letterSpacing: 3, align: 'right', fontWeight: '700' }),
createTextLayer({ content: '2026.10.01', x: 88, y: 87, fontSize: 14, letterSpacing: 2, align: 'right', fontWeight: '400' }),
createTextLayer({ content: '婚礼酒店', x: 88, y: 93, fontSize: 13, letterSpacing: 2, align: 'right', fontWeight: '400' }),
L({ content: 'Welcome to our Wedding', x: 50, y: 72, fontSize: 16, letterSpacing: 2, fontWeight: '400', fontFamily: 'display' }),
L({ content: '新郎 & 新娘', x: 50, y: 81, fontSize: 32, fontWeight: '700', letterSpacing: 5, fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 91, fontSize: 15, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
]
case 'banner-split-names':
return [
createTextLayer({ content: '新郎', x: 28, y: 74, fontSize: 30, fontWeight: '700', letterSpacing: 8 }),
createTextLayer({ content: '&', x: 50, y: 75, fontSize: 20, fontWeight: '400' }),
createTextLayer({ content: '新娘', x: 72, y: 74, fontSize: 30, fontWeight: '700', letterSpacing: 8 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 87, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 93, fontSize: 12, letterSpacing: 2, fontWeight: '400' }),
]
case 'banner-vertical-left':
return [
createTextLayer({ content: '欢迎光临', x: 10, y: 42, fontSize: 18, vertical: true, letterSpacing: 10 }),
createTextLayer({ content: '新郎新娘', x: 22, y: 45, fontSize: 28, vertical: true, fontWeight: '700', letterSpacing: 8 }),
createTextLayer({ content: '2026.10.01', x: 34, y: 48, fontSize: 14, vertical: true, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-vertical-right':
return [
createTextLayer({ content: '欢迎光临', x: 90, y: 42, fontSize: 18, vertical: true, letterSpacing: 10 }),
createTextLayer({ content: '新郎新娘', x: 78, y: 45, fontSize: 28, vertical: true, fontWeight: '700', letterSpacing: 8 }),
createTextLayer({ content: '2026.10.01', x: 66, y: 48, fontSize: 14, vertical: true, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-hero-name':
return [
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 68, fontSize: 40, fontWeight: '700', letterSpacing: 6 }),
createTextLayer({ content: 'WEDDING', x: 50, y: 80, fontSize: 16, letterSpacing: 10, fontWeight: '400' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 88, fontSize: 15, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-date-focus':
return [
createTextLayer({ content: 'OUR WEDDING', x: 50, y: 60, fontSize: 14, letterSpacing: 8, fontWeight: '400' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 72, fontSize: 40, fontWeight: '700', letterSpacing: 6 }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 84, fontSize: 24, letterSpacing: 4 }),
createTextLayer({ content: '婚礼酒店', x: 50, y: 93, fontSize: 13, letterSpacing: 3, fontWeight: '400' }),
]
case 'banner-welcome-en':
return [
createTextLayer({ content: 'Welcome to', x: 50, y: 66, fontSize: 20, letterSpacing: 4, fontWeight: '400' }),
createTextLayer({ content: 'our Wedding', x: 50, y: 76, fontSize: 34, letterSpacing: 3, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 87, fontSize: 22, letterSpacing: 4 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 94, fontSize: 13, letterSpacing: 3, fontWeight: '400' }),
]
case 'banner-minimal':
return [
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 84, fontSize: 26, letterSpacing: 5, fontWeight: '700' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 93, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-xi-corner':
return [
createTextLayer({ content: '囍', x: 12, y: 10, fontSize: 32, color: '#c45c5c', letterSpacing: 0, shadow: false, align: 'left' }),
createTextLayer({ content: 'WEDDING', x: 50, y: 74, fontSize: 28, letterSpacing: 12, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 84, fontSize: 26, letterSpacing: 5 }),
createTextLayer({ content: '2026.10.01', x: 50, y: 93, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
]
case 'banner-top-bottom':
return [
createTextLayer({ content: 'WEDDING', x: 50, y: 10, fontSize: 30, letterSpacing: 12, fontWeight: '700' }),
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 18, fontSize: 13, letterSpacing: 2, fontWeight: '400' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 82, fontSize: 30, letterSpacing: 5, fontWeight: '700' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 92, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
L({ content: '新郎', x: 28, y: 74, fontSize: 30, fontWeight: '700', letterSpacing: 8, fontFamily: 'serif' }),
L({ content: '&', x: 50, y: 75, fontSize: 20, fontWeight: '400', fontFamily: 'script' }),
L({ content: '新娘', x: 72, y: 74, fontSize: 30, fontWeight: '700', letterSpacing: 8, fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 87, fontSize: 14, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
L({ content: 'Welcome to our Wedding', x: 50, y: 93, fontSize: 12, letterSpacing: 2, fontWeight: '400', fontFamily: 'display' }),
]
case 'banner-frame-bottom':
return [
createTextLayer({ content: 'OUR WEDDING DAY', x: 50, y: 70, fontSize: 13, letterSpacing: 6, fontWeight: '400' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 79, fontSize: 30, letterSpacing: 5, fontWeight: '700' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 88, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
createTextLayer({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 3, fontWeight: '400' }),
L({ content: 'OUR WEDDING DAY', x: 50, y: 70, fontSize: 13, letterSpacing: 6, fontWeight: '400', fontFamily: 'cinzel' }),
L({ content: '新郎 & 新娘', x: 50, y: 79, fontSize: 30, letterSpacing: 5, fontWeight: '700', fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 88, fontSize: 14, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 3, fontWeight: '400', fontFamily: 'kai' }),
]
case 'banner-minimal':
return [
L({ content: '新郎 & 新娘', x: 50, y: 84, fontSize: 26, letterSpacing: 5, fontWeight: '700', fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 93, fontSize: 14, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
]
case 'art-welcome-brush':
return [
L({ content: '欢迎来我们的婚礼', x: 50, y: 16, fontSize: 48, letterSpacing: 4, fontWeight: '400', fontFamily: 'brush', shadow: true }),
L({ content: 'Mr. 新郎 & Miss. 新娘', x: 50, y: 80, fontSize: 18, letterSpacing: 2, fontFamily: 'display', fontWeight: '400' }),
L({ content: 'WELCOME TO OUR WEDDING', x: 50, y: 88, fontSize: 12, letterSpacing: 4, fontFamily: 'cinzel', fontWeight: '400' }),
L({ content: '2026.10.01', x: 50, y: 94, fontSize: 13, letterSpacing: 3, fontFamily: 'sans', fontWeight: '400' }),
]
case 'art-love-life':
return [
L({ content: '以爱之名 度一生', x: 50, y: 18, fontSize: 46, letterSpacing: 6, fontWeight: '400', fontFamily: 'liujian', shadow: true }),
L({ content: '新郎 & 新娘', x: 50, y: 80, fontSize: 20, letterSpacing: 4, fontFamily: 'serif' }),
L({ content: 'WELCOME TO OUR WEDDING', x: 50, y: 88, fontSize: 11, letterSpacing: 4, fontFamily: 'cinzel', fontWeight: '400' }),
L({ content: '2026.10.01 · 婚礼酒店', x: 50, y: 94, fontSize: 12, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
]
case 'art-gaobai':
return [
L({ content: 'Wedding Day', x: 38, y: 42, fontSize: 28, letterSpacing: 2, fontWeight: '400', fontFamily: 'script', color: '#e8c87a' }),
L({ content: '告白', x: 40, y: 56, fontSize: 64, letterSpacing: 8, fontWeight: '400', fontFamily: 'brush', shadow: true }),
L({ content: '新郎 & 新娘', x: 50, y: 86, fontSize: 18, letterSpacing: 4, fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 93, fontSize: 13, letterSpacing: 3, fontFamily: 'sans', fontWeight: '400' }),
]
case 'art-married':
return [
L({ content: 'WEDDING', x: 50, y: 18, fontSize: 52, letterSpacing: 10, fontWeight: '700', fontFamily: 'cinzel', color: 'rgba(255,255,255,0.35)', shadow: false, stroke: true }),
L({ content: '我们结婚啦', x: 50, y: 24, fontSize: 44, letterSpacing: 4, fontWeight: '400', fontFamily: 'brush', color: '#ff6b3d', shadow: true }),
L({ content: '【欢迎参加我们的婚礼】', x: 50, y: 38, fontSize: 14, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎:XXX & 新娘:XXX', x: 50, y: 46, fontSize: 16, letterSpacing: 2, fontFamily: 'serif', fontWeight: '400' }),
L({ content: '10月01日 12:00', x: 50, y: 88, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'art-hand-love':
return [
L({ content: '携手热爱,奔赴山海', x: 50, y: 18, fontSize: 40, letterSpacing: 4, fontWeight: '400', fontFamily: 'liujian', shadow: true }),
L({ content: '【欢迎参加我们的婚礼】', x: 50, y: 34, fontSize: 14, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎:XXX & 新娘:XXX', x: 50, y: 42, fontSize: 16, letterSpacing: 2, fontFamily: 'serif', fontWeight: '400' }),
L({ content: '10月01日 12:00', x: 50, y: 88, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'art-like-you':
return [
L({ content: '就是喜欢你', x: 50, y: 20, fontSize: 48, letterSpacing: 6, fontWeight: '400', fontFamily: 'longcang', color: '#f0d48a', shadow: true }),
L({ content: '【欢迎参加我们的婚礼】', x: 50, y: 36, fontSize: 14, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎:XXX & 新娘:XXX', x: 50, y: 44, fontSize: 16, letterSpacing: 2, fontFamily: 'serif', fontWeight: '400' }),
L({ content: '10月01日 12:00', x: 50, y: 88, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'art-give-home':
return [
L({ content: 'WELCOME TO OUR WEDDING', x: 50, y: 14, fontSize: 16, letterSpacing: 5, fontWeight: '700', fontFamily: 'cinzel', stroke: true, shadow: false }),
L({ content: '给她一个家', x: 50, y: 26, fontSize: 42, letterSpacing: 8, fontWeight: '400', fontFamily: 'mashan', shadow: true }),
L({ content: '【欢迎参加我们的婚礼】', x: 50, y: 40, fontSize: 14, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎:XXX & 新娘:XXX', x: 50, y: 48, fontSize: 16, letterSpacing: 2, fontFamily: 'serif', fontWeight: '400' }),
L({ content: '10月01日 12:00', x: 50, y: 88, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'art-xi-welcome':
return [
L({ content: '囍', x: 50, y: 12, fontSize: 56, color: '#e8c87a', letterSpacing: 0, shadow: false, fontFamily: 'mashan', fontWeight: '400' }),
L({ content: '欢迎光临我们的婚礼', x: 50, y: 72, fontSize: 30, letterSpacing: 4, fontFamily: 'brush', fontWeight: '400' }),
L({ content: '新郎 & 新娘', x: 50, y: 84, fontSize: 24, letterSpacing: 4, fontFamily: 'serif' }),
L({ content: '谨定于 2026年10月1日', x: 50, y: 93, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'art-wujia':
return [
L({ content: '吾家有喜', x: 22, y: 42, fontSize: 42, letterSpacing: 14, fontFamily: 'longcang', color: '#e8c87a', vertical: true, fontWeight: '400' }),
L({ content: '欢迎参加', x: 38, y: 40, fontSize: 18, letterSpacing: 10, fontFamily: 'kai', vertical: true, fontWeight: '400' }),
L({ content: '新郎 & 新娘', x: 72, y: 74, fontSize: 18, letterSpacing: 2, fontFamily: 'serif', align: 'left' }),
L({ content: '2026.10.01', x: 72, y: 84, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400', align: 'left' }),
]
case 'art-tianci':
return [
L({ content: '天赐良缘', x: 20, y: 42, fontSize: 40, letterSpacing: 14, fontFamily: 'longcang', color: '#e8c87a', vertical: true, fontWeight: '400' }),
L({ content: '吾家有喜', x: 36, y: 40, fontSize: 18, letterSpacing: 10, fontFamily: 'kai', vertical: true, fontWeight: '400' }),
L({ content: '新郎 & 新娘', x: 72, y: 72, fontSize: 18, letterSpacing: 2, fontFamily: 'serif', align: 'left' }),
L({ content: '2026年10月1日', x: 72, y: 82, fontSize: 13, letterSpacing: 1, fontFamily: 'sans', fontWeight: '400', align: 'left' }),
L({ content: '婚礼酒店', x: 72, y: 90, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400', align: 'left' }),
]
case 'art-tianzuo':
return [
L({ content: '天', x: 32, y: 22, fontSize: 48, fontFamily: 'longcang', color: '#e8c87a', fontWeight: '400', letterSpacing: 0 }),
L({ content: '作', x: 68, y: 22, fontSize: 48, fontFamily: 'longcang', color: '#e8c87a', fontWeight: '400', letterSpacing: 0 }),
L({ content: '之', x: 32, y: 40, fontSize: 48, fontFamily: 'longcang', color: '#e8c87a', fontWeight: '400', letterSpacing: 0 }),
L({ content: '合', x: 68, y: 40, fontSize: 48, fontFamily: 'longcang', color: '#e8c87a', fontWeight: '400', letterSpacing: 0 }),
L({ content: '欢迎参加我们的婚礼', x: 50, y: 72, fontSize: 16, letterSpacing: 4, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎 & 新娘', x: 50, y: 82, fontSize: 20, letterSpacing: 4, fontFamily: 'serif' }),
L({ content: '2026.10.01', x: 50, y: 92, fontSize: 13, letterSpacing: 3, fontFamily: 'sans', fontWeight: '400' }),
]
case 'art-welcome-v':
return [
L({ content: '欢迎参加我们的婚礼', x: 14, y: 45, fontSize: 28, letterSpacing: 12, fontFamily: 'brush', vertical: true, fontWeight: '400' }),
L({ content: 'Welcome', x: 28, y: 36, fontSize: 16, letterSpacing: 6, fontFamily: 'script', vertical: true, fontWeight: '400', color: '#e8c87a' }),
L({ content: '新郎 & 新娘', x: 70, y: 80, fontSize: 18, letterSpacing: 2, fontFamily: 'serif', align: 'left' }),
L({ content: '2026.10.01', x: 70, y: 90, fontSize: 13, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400', align: 'left' }),
]
case 'art-longest':
return [
L({ content: '最长的告白', x: 50, y: 16, fontSize: 44, letterSpacing: 6, fontFamily: 'brush', fontWeight: '400', shadow: true }),
L({ content: 'Accompanying You', x: 50, y: 28, fontSize: 22, letterSpacing: 2, fontFamily: 'script', color: '#e07a7a', fontWeight: '400' }),
L({ content: '新郎 & 新娘', x: 50, y: 80, fontSize: 20, letterSpacing: 4, fontFamily: 'serif' }),
L({ content: '2026.10.01 · 婚礼酒店', x: 50, y: 90, fontSize: 13, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
]
case 'art-just-love':
return [
L({ content: '就爱这样的你', x: 50, y: 18, fontSize: 42, letterSpacing: 4, fontFamily: 'liujian', color: '#7eb6e8', fontWeight: '400', shadow: true }),
L({ content: '【欢迎参加我们的婚礼】', x: 50, y: 34, fontSize: 14, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
L({ content: '新郎:XXX & 新娘:XXX', x: 50, y: 42, fontSize: 16, letterSpacing: 2, fontFamily: 'serif', fontWeight: '400' }),
L({ content: '10月01日 12:00', x: 50, y: 88, fontSize: 14, letterSpacing: 2, fontFamily: 'sans', fontWeight: '400' }),
L({ content: '婚礼酒店', x: 50, y: 94, fontSize: 13, letterSpacing: 2, fontFamily: 'kai', fontWeight: '400' }),
]
case 'custom':
return []
case 'banner-bottom':
default:
return [
createTextLayer({ content: 'WEDDING', x: 50, y: 68, fontSize: 36, letterSpacing: 14, fontWeight: '700' }),
createTextLayer({ content: '新郎 & 新娘', x: 50, y: 78, fontSize: 26, letterSpacing: 6 }),
createTextLayer({ content: 'Welcome to our Wedding', x: 50, y: 86, fontSize: 13, letterSpacing: 2, fontWeight: '400' }),
createTextLayer({ content: '2026.10.01', x: 50, y: 93, fontSize: 14, letterSpacing: 4, fontWeight: '400' }),
L({ content: 'WEDDING', x: 50, y: 68, fontSize: 36, letterSpacing: 14, fontWeight: '700', fontFamily: 'cinzel' }),
L({ content: '新郎 & 新娘', x: 50, y: 78, fontSize: 26, letterSpacing: 6, fontFamily: 'serif' }),
L({ content: 'Welcome to our Wedding', x: 50, y: 86, fontSize: 13, letterSpacing: 2, fontWeight: '400', fontFamily: 'display' }),
L({ content: '2026.10.01', x: 50, y: 93, fontSize: 14, letterSpacing: 4, fontWeight: '400', fontFamily: 'sans' }),
]
}
}

View File

@@ -1,14 +1,26 @@
@import "tailwindcss";
@import "@fontsource/great-vibes/400.css";
@import "@fontsource/ma-shan-zheng/400.css";
@import "@fontsource/zhi-mang-xing/400.css";
@import "@fontsource/long-cang/400.css";
@import "@fontsource/liu-jian-mao-cao/400.css";
@import "@fontsource/playfair-display/400.css";
@import "@fontsource/playfair-display/700.css";
@import "@fontsource/pinyon-script/400.css";
@import "@fontsource/cinzel/400.css";
@import "@fontsource/cinzel/700.css";
:root {
--font-sans: "Avenir Next", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
--font-serif: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", "SimSun", serif;
--font-display: "Didot", "Bodoni 72", "Cormorant Garamond", "Times New Roman", serif;
--font-display: "Playfair Display", "Didot", "Bodoni 72", "Cormorant Garamond", "Times New Roman", serif;
--font-kai: "Kaiti SC", "STKaiti", "KaiTi", "FangSong", serif;
--font-brush: "HanziPen SC", "STXingkai", "华文行楷", "FZKai-Z03S", "Kaiti SC", "STKaiti", "KaiTi", "FangSong", serif;
--font-calligraphy: "Great Vibes", "Ma Shan Zheng", cursive, serif;
--font-brush: "Zhi Mang Xing", "Liu Jian Mao Cao", "HanziPen SC", "STXingkai", "华文行楷", "Kaiti SC", serif;
--font-calligraphy: "Great Vibes", "Pinyon Script", "Ma Shan Zheng", cursive, serif;
--font-mashan: "Ma Shan Zheng", "Zhi Mang Xing", cursive, serif;
--font-longcang: "Long Cang", "Ma Shan Zheng", cursive, serif;
--font-liujian: "Liu Jian Mao Cao", "Zhi Mang Xing", cursive, serif;
--font-cinzel: "Cinzel", "Playfair Display", serif;
--font-number: "Cochin", "Georgia", "Times New Roman", serif;
}

View File

@@ -170,9 +170,15 @@
<a-button size="small" danger type="text" class="!px-1" @click.stop="removeText(t.id)">删除</a-button>
</div>
<a-input v-model:value="t.content" placeholder="文字内容" class="mb-2" :maxlength="80" />
<a-form-item label="字体" class="!mb-2">
<a-select v-model:value="t.fontFamily" class="w-full" :options="fontSelectOptions" />
<div class="hw-font-sample mt-1" :style="{ fontFamily: resolveFontCss(t.fontFamily) }">
{{ fontSample(t.fontFamily) }}
</div>
</a-form-item>
<div class="grid grid-cols-2 gap-2">
<a-form-item label="字号" class="!mb-2">
<a-input-number v-model:value="t.fontSize" :min="10" :max="120" class="w-full" />
<a-input-number v-model:value="t.fontSize" :min="10" :max="160" class="w-full" />
</a-form-item>
<a-form-item label="字距" class="!mb-2">
<a-input-number v-model:value="t.letterSpacing" :min="-2" :max="40" class="w-full" />
@@ -194,9 +200,10 @@
/>
</a-form-item>
</div>
<div class="flex gap-4 mb-1">
<div class="flex gap-4 mb-1 flex-wrap">
<a-checkbox v-model:checked="t.vertical">竖排</a-checkbox>
<a-checkbox v-model:checked="t.shadow">阴影</a-checkbox>
<a-checkbox v-model:checked="t.stroke">描边</a-checkbox>
</div>
<div class="text-[10px] text-[#8A8680]">位置 X {{ Math.round(t.x) }}% · Y {{ Math.round(t.y) }}%</div>
</div>
@@ -278,12 +285,15 @@
v-for="(z, i) in tpl.zones"
:key="i"
class="hw-tpl-zone"
:class="{ vertical: z.vertical }"
:class="{ vertical: z.vertical, art: z.art }"
:style="zoneStyle(z)"
>{{ z.label }}</span>
<span v-if="!tpl.zones?.length" class="hw-tpl-preview-empty">自定义空白</span>
</div>
<div class="hw-tpl-card-label">{{ tpl.label }}</div>
<div class="hw-tpl-card-label">
{{ tpl.label }}
<span v-if="tpl.artistic" class="hw-tpl-art-tag">艺术字</span>
</div>
<div class="hw-tpl-card-desc">{{ tpl.desc }}</div>
</button>
</div>
@@ -312,10 +322,12 @@ import {
import ImageField from '@/components/ImageField.vue'
import MediaPickerModal from '@/components/MediaPickerModal.vue'
import {
TEXT_FONTS,
clampPct,
createTextLayer,
defaultTextsForTemplate,
normalizeHotelWelcomeConfig,
resolveFontCss,
resolveTemplate,
serializeHotelWelcomeConfig,
templateGroups,
@@ -363,6 +375,11 @@ const columns = [
const currentTemplateMeta = computed(() => templateMeta(detail.value?.template))
const createTemplateMeta = computed(() => templateMeta(createForm.template))
const currentTemplateDesc = computed(() => currentTemplateMeta.value?.desc || '')
const fontSelectOptions = TEXT_FONTS.map((f) => ({ value: f.value, label: f.label }))
function fontSample(key) {
return TEXT_FONTS.find((f) => f.value === key)?.sample || 'Aa'
}
function zoneStyle(z) {
return {
@@ -559,20 +576,23 @@ function textStyle(t) {
: align === 'right'
? 'translate(-100%, -50%)'
: 'translate(-50%, -50%)'
return {
const style = {
left: `${clampPct(t.x)}%`,
top: `${clampPct(t.y)}%`,
transform,
fontSize: `${t.fontSize || 24}px`,
color: t.color || '#fff',
fontFamily: t.fontFamily === 'serif'
? '"Ma Shan Zheng", "Songti SC", serif'
: t.fontFamily || 'serif',
fontFamily: resolveFontCss(t.fontFamily),
fontWeight: t.fontWeight || '600',
letterSpacing: `${t.letterSpacing || 0}px`,
textAlign: align,
writingMode: t.vertical ? 'vertical-rl' : 'horizontal-tb',
}
if (t.stroke) {
style.webkitTextStroke = `1px ${t.strokeColor || 'rgba(255,255,255,0.35)'}`
style.paintOrder = 'stroke fill'
}
return style
}
function onTextPointerDown(e, t) {
@@ -970,10 +990,35 @@ defineExpose({ loadList, refresh: loadList })
white-space: nowrap;
letter-spacing: 0.12em;
}
.hw-tpl-zone.art {
background: rgba(232, 200, 122, 0.9);
color: #5a3d1a;
font-family: var(--font-brush);
font-size: 10px;
}
.hw-tpl-card-label {
font-size: 12px;
color: #2c2c2c;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.hw-tpl-art-tag {
font-size: 10px;
font-weight: 500;
color: #9a4d3a;
background: rgba(196, 92, 92, 0.12);
border: 1px solid rgba(196, 92, 92, 0.28);
border-radius: 999px;
padding: 0 6px;
line-height: 16px;
}
.hw-font-sample {
font-size: 18px;
line-height: 1.2;
color: #7a6550;
}
.hw-tpl-card-desc {
margin-top: 2px;