From 078e621783430fe582fa73644a15a9d9bda4f670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Tue, 4 Aug 2026 08:19:30 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BF=AE=E5=A4=8D=E6=B5=B7=E6=8A=A5?= =?UTF-8?q?=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hunliji-api/configsection/section.go | 4 +- hunliji-api/posterconfig/poster.go | 6 +- .../src/composables/posterDefaults.js | 23 +- vite-tailwindcss/src/router/index.js | 25 +- .../src/views/admin/AdminEditor.vue | 278 ++++++++++++++++-- vite-tailwindcss/src/views/hb/index.vue | 30 +- .../src/views/hb/templates/SplitPoster.vue | 16 +- 7 files changed, 332 insertions(+), 50 deletions(-) diff --git a/hunliji-api/configsection/section.go b/hunliji-api/configsection/section.go index 8640617..7bdce4e 100644 --- a/hunliji-api/configsection/section.go +++ b/hunliji-api/configsection/section.go @@ -22,7 +22,9 @@ var ( var configSectionKeys = map[string][]string{ "basic": { - "groom", "bride", "date", "lunar", "calendarDate", "hotel", "address", "heroImg", "endImg", + "groom", "bride", "date", "lunar", "calendarDate", "hotel", "address", + "heroImg", "heroImgResized", "heroImgResizedMeta", + "endImg", "endImgResized", "endImgResizedMeta", }, "copy": { "formalText1", "formalText2", "formalPageHeight", diff --git a/hunliji-api/posterconfig/poster.go b/hunliji-api/posterconfig/poster.go index d7b1681..0782a93 100644 --- a/hunliji-api/posterconfig/poster.go +++ b/hunliji-api/posterconfig/poster.go @@ -23,7 +23,7 @@ func Register(api *gin.RouterGroup, database *gorm.DB, adminGuard func(*gin.Cont api.POST("/poster/config", handleSavePosterConfig) } -// resolveSide 返回 "1"(男方)、"2"(女方)或 "3"(回门宴);无法识别返回空 +// resolveSide 返回 "1"(男方)、"2"(女方)或 "3"(新人);无法识别返回空 func resolveSide(raw string) string { v := strings.ToLower(strings.TrimSpace(raw)) switch v { @@ -31,7 +31,7 @@ func resolveSide(raw string) string { return "1" case "2", "female", "nv", "f", "bride": return "2" - case "3", "hm", "hmy", "return", "huimen": + case "3", "xr", "hm", "hmy", "return", "huimen", "couple", "xinren", "新人": return "3" default: return "" @@ -156,7 +156,7 @@ func normalizeBundle(raw map[string]interface{}) map[string]interface{} { if f := takeSide(raw, "2", "female"); len(f) > 0 { out["2"] = normalizeSideLayouts(f) } - if h := takeSide(raw, "3", "hm", "hmy"); len(h) > 0 { + if h := takeSide(raw, "3", "xr", "hm", "hmy"); len(h) > 0 { out["3"] = normalizeSideLayouts(h) } return out diff --git a/vite-tailwindcss/src/composables/posterDefaults.js b/vite-tailwindcss/src/composables/posterDefaults.js index 5ffdff3..6ffc8a9 100644 --- a/vite-tailwindcss/src/composables/posterDefaults.js +++ b/vite-tailwindcss/src/composables/posterDefaults.js @@ -1,9 +1,8 @@ -/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv),3=回门宴(/hb-hm) */ - +/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv),3=新人模板(/hb-xr) */ export const POSTER_SIDES = [ { key: 1, keyStr: '1', route: '/hb-n', label: '男方', short: 'n' }, { key: 2, keyStr: '2', route: '/hb-nv', label: '女方', short: 'nv' }, - { key: 3, keyStr: '3', route: '/hb-hm', label: '回门宴', short: 'hm' }, + { key: 3, keyStr: '3', route: '/hb-xr', label: '新人模板', short: 'xr' }, ] /** 布局:1=竖版,2=横版(URL 后缀 /1 /2) */ @@ -118,7 +117,7 @@ export function resolvePosterSide(input) { if (input === 2 || input === '2') return 2 if (input === 3 || input === '3') return 3 const v = String(input || '').trim().toLowerCase() - if (v === 'hm' || v === 'hmy' || v === 'return' || v === 'huimen') return 3 + if (v === 'xr' || v === 'hm' || v === 'hmy' || v === 'return' || v === 'huimen' || v === 'xinren' || v === 'couple') return 3 if (v === 'female' || v === 'nv' || v === 'f' || v === 'bride') return 2 if (v === 'male' || v === 'n' || v === 'm' || v === 'groom') return 1 return 0 @@ -264,23 +263,23 @@ export function defaultSide2PosterConfig() { } } -/** 回门宴(3) 默认:9月7日 */ +/** 新人模板(3) 默认 */ export function defaultSide3PosterConfig() { return { ...baseFields(3), side: 3, template: 'classic', - title: '回门宴邀请函', - pageTitle: '💕李琦❤️李逸烁 · 回门宴邀请', - pageDescription: '💍公历2026年9月7日(农历七月廿六)· 李琦 & 李逸烁 回门宴,诚邀您光临。', - footer: '—回门宴请 ♥ 恭请光临—', + title: '婚礼邀请函', + pageTitle: '💕李琦❤️李逸烁 · 新人婚礼邀请', + pageDescription: '💍公历2026年9月7日(农历七月廿六)· 李琦 & 李逸烁 结婚典礼,诚邀您光临。', + footer: '—诚挚邀请 ♥ 恭请光临—', sonLabel: '新郎:', sonName: '李琦', daughterInLawLabel: '新娘:', daughterInLawName: '李逸烁', groomName: '李琦', brideName: '李逸烁', - verticalSlogan: '回门之喜', + verticalSlogan: '喜结连理', subSlogan1: '琴瑟和鸣', subSlogan2: '永结同心', dateText: '2026年9月7日 星期一', @@ -290,11 +289,11 @@ export function defaultSide3PosterConfig() { loadingEffect: 'redSeal', lines: [ '各位亲朋好友', - '新婚燕尔 回门宴请', + '良辰吉日 新人成婚', '良辰已定 吉日待访', '兹定于2026年9月7日 星期一', '农历七月廿六 中午11:08分', - '为新婚夫妇举办回门宴', + '为新婚夫妇举办结婚典礼', '敬备薄酒 诚邀您携家人光临', '地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', '李琦 & 李逸烁 敬邀', diff --git a/vite-tailwindcss/src/router/index.js b/vite-tailwindcss/src/router/index.js index 83285a2..99afebd 100644 --- a/vite-tailwindcss/src/router/index.js +++ b/vite-tailwindcss/src/router/index.js @@ -33,15 +33,32 @@ const routes = [ component: () => import('@/views/hb/index.vue'), meta: { posterSide: 2 }, }, + { + path: '/hb-xr', + redirect: '/hb-xr/1', + }, + { + path: '/hb-xr/:layout(1|2)', + name: 'PosterCouple', + component: () => import('@/views/hb/index.vue'), + meta: { posterSide: 3 }, + }, + // 旧路径兼容 { path: '/hb-hm', - redirect: '/hb-hm/1', + redirect: '/hb-xr/1', }, { path: '/hb-hm/:layout(1|2)', - name: 'PosterReturnBanquet', - component: () => import('@/views/hb/index.vue'), - meta: { posterSide: 3 }, + redirect: (to) => `/hb-xr/${to.params.layout || 1}`, + }, + { + path: '/xr', + redirect: '/hb-xr/1', + }, + { + path: '/xr/:layout(1|2)', + redirect: (to) => `/hb-xr/${to.params.layout || 1}`, }, { path: '/lijin', diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 932fda5..b353a6d 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -653,14 +653,14 @@ - +
@@ -862,11 +862,17 @@
- - + +
+ + +
- - + +
+ + +
@@ -882,20 +888,47 @@ - -
- - 保存海报配置 - - - 打开 {{ currentPosterPreviewPath }} 预览 - -
+
+
+
+
+ {{ currentPosterSide.label }} · {{ currentPosterLayout.label }} + {{ currentPosterPreviewPath }} +
+
+ + 撤回导入 + + + 复制 + + + 导入 + + + 预览 + + + 保存 + +
+
+
+
@@ -1419,7 +1452,7 @@ import { metaHasFileSize, } from '@/composables/resizeImage' import { - defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout, + defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout, normalizePosterConfig, LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_LAYOUTS, POSTER_TEMPLATES, MOBILE_LAYOUTS, HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, layoutKey, posterSideMeta, posterLayoutMeta, posterRoute, @@ -1616,6 +1649,12 @@ const uploadCfg = reactive(defaultUploadConfig()) const posterSideTab = ref('1') const posterLayoutTab = ref('1') const posterBundle = reactive(defaultPosterBundle()) +const posterImporting = ref(false) +const posterCopying = ref(false) +/** 本会话最近一次复制的模板,导入时优先/兜底使用 */ +let posterTemplateCache = null +/** 导入前快照,用于撤回 { s, l, config } */ +const posterUndoSnapshot = ref(null) const posterCfg = computed(() => { const s = sideKey(posterSideTab.value) const l = layoutKey(posterLayoutTab.value) @@ -1737,7 +1776,7 @@ const posterNameLabels = computed(() => { return { left: '女儿称谓 / 姓名', right: '女婿称谓 / 姓名', leftPh: '女儿:', rightPh: '女婿:' } } if (s === '3') { - return { left: '新郎称谓 / 姓名', right: '新娘称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' } + return { left: '新人左侧称谓 / 姓名', right: '新人右侧称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' } } return { left: '儿子称谓 / 姓名', right: '儿媳称谓 / 姓名', leftPh: '儿子:', rightPh: '儿媳:' } }) @@ -2026,9 +2065,17 @@ async function batchResizeBasicImages() { console.warn('[resize-basic]', slot.key, err) } } - if (fail === 0) message.success(`已生成 ${ok} 张展示图`) - else if (ok > 0) message.warning(`成功 ${ok} 张,失败 ${fail} 张(常见原因:跨域无法读取原图)`) - else message.error('缩放上传全部失败,请确认原图可跨域读取') + if (ok > 0) { + try { + await saveConfigSection('basic', pickSectionData('basic')) + if (fail === 0) message.success(`已生成并保存 ${ok} 张展示图`) + else message.warning(`成功 ${ok} 张并已保存,失败 ${fail} 张`) + } catch (e) { + message.warning(`已生成 ${ok} 张展示图,但自动保存失败:${e?.message || '请手动保存'}`) + } + } else { + message.error('缩放上传全部失败,请确认原图可跨域读取') + } } finally { resizingBasic.value = false resizeProgress.value = '' @@ -2285,6 +2332,7 @@ async function onSavePosterConfig() { posterBundle['1'] = bundle['1'] posterBundle['2'] = bundle['2'] posterBundle['3'] = bundle['3'] + posterUndoSnapshot.value = null message.success('海报配置已保存') } catch (e) { message.error(e?.message || '海报配置保存失败') @@ -2293,6 +2341,141 @@ async function onSavePosterConfig() { } } +function buildPosterTemplatePayload() { + const s = sideKey(posterSideTab.value) + const l = layoutKey(posterLayoutTab.value) + const slot = ensurePosterSlot(s, l) + return { + version: 1, + kind: 'poster-slot', + side: Number(s), + layout: Number(l), + sideLabel: posterSideMeta(s).label, + layoutLabel: posterLayoutMeta(l).label, + route: posterRoute(s, l), + config: JSON.parse(JSON.stringify(slot)), + } +} + +async function copyPosterTemplate() { + posterCopying.value = true + try { + const payload = buildPosterTemplatePayload() + posterTemplateCache = payload + const text = JSON.stringify(payload, null, 2) + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + } else { + const ta = document.createElement('textarea') + ta.value = text + ta.style.position = 'fixed' + ta.style.left = '-9999px' + document.body.appendChild(ta) + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) + } + message.success(`已复制「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」`) + } catch (e) { + message.error(e?.message || '复制失败,请检查浏览器剪贴板权限') + } finally { + posterCopying.value = false + } +} + +function extractPosterImportConfig(parsed) { + if (!parsed || typeof parsed !== 'object') return null + if (parsed.kind === 'poster-slot' && parsed.config && typeof parsed.config === 'object') { + return parsed.config + } + if (parsed.config && typeof parsed.config === 'object' && !Array.isArray(parsed.config)) { + return parsed.config + } + const s = sideKey(posterSideTab.value) + const l = layoutKey(posterLayoutTab.value) + const sideObj = parsed[s] || parsed[Number(s)] + if (sideObj && typeof sideObj === 'object') { + const slot = sideObj[l] || sideObj[Number(l)] + if (slot && typeof slot === 'object') return slot + } + if ('template' in parsed || 'heroImg' in parsed || 'lines' in parsed || 'sonName' in parsed || 'groomName' in parsed) { + return parsed + } + return null +} + +async function readPosterImportSource() { + // 1) 剪贴板 2) 本会话刚复制的缓存 + try { + if (navigator?.clipboard?.readText) { + const clip = String(await navigator.clipboard.readText() || '').trim() + if (clip) { + try { + const parsed = JSON.parse(clip) + if (extractPosterImportConfig(parsed)) return parsed + } catch { /* not json */ } + } + } + } catch { /* no permission */ } + if (posterTemplateCache && extractPosterImportConfig(posterTemplateCache)) { + return posterTemplateCache + } + return null +} + +function applyImportedConfig(rawCfg) { + const s = sideKey(posterSideTab.value) + const l = layoutKey(posterLayoutTab.value) + posterUndoSnapshot.value = { + s, + l, + config: JSON.parse(JSON.stringify(ensurePosterSlot(s, l))), + } + const next = normalizePosterConfig(rawCfg, s, l) + next.side = Number(s) + next.layout = Number(l) + if (next.sonName && !next.groomName) next.groomName = next.sonName + if (next.daughterInLawName && !next.brideName) next.brideName = next.daughterInLawName + if (next.groomName && !next.sonName) next.sonName = next.groomName + if (next.brideName && !next.daughterInLawName) next.daughterInLawName = next.brideName + posterBundle[s][l] = next +} + +async function importPosterTemplateDirect() { + posterImporting.value = true + try { + const parsed = await readPosterImportSource() + if (!parsed) { + message.warning('请先复制模板,再点导入') + return + } + const rawCfg = extractPosterImportConfig(parsed) + if (!rawCfg) { + message.error('无法识别的模板数据') + return + } + applyImportedConfig(rawCfg) + message.success(`已导入到「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」,可撤回或保存`) + } catch (e) { + message.error(e?.message || '导入失败') + } finally { + posterImporting.value = false + } +} + +function undoPosterImport() { + const snap = posterUndoSnapshot.value + if (!snap?.config) { + message.info('没有可撤回的导入') + return + } + posterSideTab.value = snap.s + posterLayoutTab.value = snap.l + posterBundle[snap.s][snap.l] = snap.config + posterUndoSnapshot.value = null + message.success('已撤回导入') +} + function pickSectionData(section) { const keys = SECTION_KEYS[section] || [] const data = {} @@ -3514,4 +3697,57 @@ onUnmounted(() => { pointer-events: none; z-index: 1; } + +.poster-action-bar-spacer { + height: 72px; +} +.poster-action-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 40; + padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px)); + background: rgba(255, 255, 255, 0.94); + backdrop-filter: blur(10px); + border-top: 1px solid rgba(168, 140, 107, 0.2); + box-shadow: 0 -8px 24px rgba(88, 68, 42, 0.08); +} +.poster-action-bar-inner { + max-width: 1220px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} +.admin-shell--top ~ .poster-action-bar .poster-action-bar-inner, +.admin-shell--top .poster-action-bar-inner { + max-width: 80vw; +} +.poster-action-bar-meta { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + font-size: 12px; + color: #5c5348; +} +.poster-action-bar-path { + font-size: 11px; + color: #8A8680; +} +.poster-action-bar-btns { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; + margin-left: auto; +} +@media (max-width: 720px) { + .poster-action-bar-meta { width: 100%; } + .poster-action-bar-btns { width: 100%; justify-content: stretch; } + .poster-action-bar-btns :deep(.ant-btn) { flex: 1 1 auto; } +} diff --git a/vite-tailwindcss/src/views/hb/index.vue b/vite-tailwindcss/src/views/hb/index.vue index dd2666d..dac3c90 100644 --- a/vite-tailwindcss/src/views/hb/index.vue +++ b/vite-tailwindcss/src/views/hb/index.vue @@ -127,7 +127,7 @@
- +