@@ -1912,6 +1952,7 @@ import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
import MediaPickerModal from '@/components/MediaPickerModal.vue'
import HotelWelcomePosterAdmin from '@/views/admin/HotelWelcomePosterAdmin.vue'
+import ThanksLetterAdmin from '@/views/admin/ThanksLetterAdmin.vue'
import BorderStylePicker from '@/components/BorderStylePicker.vue'
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
import { SITE_THEME_GROUPS, applySiteTheme } from '@/utils/themes'
@@ -1933,6 +1974,9 @@ import {
HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, layoutKey,
posterSideMeta, posterLayoutMeta, posterRoute,
} from '@/composables/posterDefaults'
+import {
+ THANKS_SIDES, thanksRoute as thanksRouteFor,
+} from '@/composables/thanksLetterDefaults'
import {
HEART_LAYOUTS, HEART_MASK_CSS, heartSlotsFor, heartLayoutMeta, normalizeHeartLayout,
heartSlotStyle, heartSlotLabel, heartFocusAt, heartFocusCssAt,
@@ -1947,7 +1991,7 @@ const adminStore = useAdminStore()
const TAB_KEY = 'wedding_admin_active_tab'
const MENU_LAYOUT_KEY = 'wedding_admin_menu_layout'
const CONFIG_SECTIONS = ['basic', 'copy', 'theme', 'visual', 'music', 'photos', 'schedule']
-const VALID_TABS = ['overview', 'gifts', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'hotelWelcome', 'rsvp', 'cash', 'danmaku']
+const VALID_TABS = ['overview', 'gifts', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'hotelWelcome', 'thanksLetter', 'rsvp', 'cash', 'danmaku']
/** 后台菜单分组 */
const adminMenuGroups = [
@@ -1988,6 +2032,7 @@ const adminMenuGroups = [
items: [
{ key: 'poster', label: '海报', icon: 'fa-solid fa-scroll' },
{ key: 'hotelWelcome', label: '迎宾易拉宝', icon: 'fa-solid fa-hotel' },
+ { key: 'thanksLetter', label: '答谢函', icon: 'fa-solid fa-hand-holding-heart' },
{ key: 'upload', label: '上传设置', icon: 'fa-solid fa-cloud-arrow-up' },
],
},
@@ -2003,6 +2048,7 @@ const SECTION_KEYS = {
'groom', 'bride', 'date', 'lunar', 'calendarDate', 'hotel', 'address',
'heroImg', 'heroImgResized', 'heroImgResizedMeta',
'endImg', 'endImgResized', 'endImgResizedMeta',
+ 'thanksEntryEnabled',
],
copy: ['formalText1', 'formalText2', 'formalPageHeight'],
theme: ['siteTheme', 'sectionIndexEnabled', 'calendarStyle', 'endingFrameStyle'],
@@ -2035,6 +2081,7 @@ function readStoredMenuLayout() {
const activeTab = ref(readStoredTab())
const menuLayout = ref(readStoredMenuLayout())
const hotelWelcomeRef = ref(null)
+const thanksLetterRef = ref(null)
watch(menuLayout, (v) => {
try { localStorage.setItem(MENU_LAYOUT_KEY, v) } catch { /* ignore */ }
})
@@ -3431,6 +3478,7 @@ function applySectionData(section, loaded) {
loaded.heroImgResizedMeta = loaded.heroImgResizedMeta || ''
loaded.endImgResized = loaded.endImgResized || ''
loaded.endImgResizedMeta = loaded.endImgResizedMeta || ''
+ if (typeof loaded.thanksEntryEnabled !== 'boolean') loaded.thanksEntryEnabled = true
}
if (section === 'copy') {
loaded.formalPageHeight = loaded.formalPageHeight || '100vh'
@@ -3884,6 +3932,10 @@ async function onTabActivate(tab) {
hotelWelcomeRef.value?.refresh?.()
return
}
+ if (tab === 'thanksLetter') {
+ thanksLetterRef.value?.refresh?.()
+ return
+ }
if (tab === 'rsvp') {
await loadRsvp()
return
@@ -3902,6 +3954,10 @@ function openPosterPreview(side = 1, layout = 1) {
window.open(posterRoute(side, layout), '_blank')
}
+function openThanksPreview(side = 1) {
+ window.open(thanksRouteFor(side), '_blank')
+}
+
function onLogout() {
recomputeDirtySections()
const doLogout = () => {
diff --git a/vite-tailwindcss/src/views/admin/ThanksDeckEditor.vue b/vite-tailwindcss/src/views/admin/ThanksDeckEditor.vue
new file mode 100644
index 0000000..88ea00b
--- /dev/null
+++ b/vite-tailwindcss/src/views/admin/ThanksDeckEditor.vue
@@ -0,0 +1,518 @@
+
+
+
+
+
+
+
![]()
+
暂无底图
+
+
+
+
+
+
+
+
+
+ {{ t.content || ' ' }}
+
+
+
+ 点击文字选中并拖拽 · 点击空白取消选中 · 文字位置为全屏百分比
+
+
+
+
+
+
+
+
+
+
焦点
+
+
+
+ {{ Math.round(deck.bgFocusX) }}% · {{ Math.round(deck.bgFocusY) }}%
+
+
+ 左右滑块调横向焦点,上下裁切时用纵向焦点保住主体
+
+
+
+
+
+
+ {{ lo.label }}
+
+
+ {{ recapLayoutMeta(deck.layout).desc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
文字图层({{ deck.texts.length }})
+
+ 添加文字
+
+
+
+
+ 暂无文字,可「应用风格预设」或手动添加
+
+
+
+
+
+
+ {{ t.content || '(空)' }}
+
+
+ {{ Math.round(t.fontSize) }}px
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vite-tailwindcss/src/views/admin/ThanksLetterAdmin.vue b/vite-tailwindcss/src/views/admin/ThanksLetterAdmin.vue
new file mode 100644
index 0000000..71c1110
--- /dev/null
+++ b/vite-tailwindcss/src/views/admin/ThanksLetterAdmin.vue
@@ -0,0 +1,542 @@
+
+
+
+
+
+
+
婚礼答谢函
+
+ 男 / 女 / 新人三套独立 · 信在前 → 回顾全屏幻灯片 → 致谢尾页
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+
+
加载中…
+
+
+
+
+
全局设置
+
+
+
+
+
+
+ {{ thanksStyleMeta(cfg.style).desc }}。切风格立即生效;文字排版可用下方按钮套用该风格预设。
+
+
+
+
+ 应用「{{ thanksStyleMeta(cfg.style).label }}」默认文字排版
+
+ 重置信页 / 回顾页标题 / 尾页的预设文案(图片与已上传底图保留)
+
+
+
+
+
+
+ {{ animPackMeta.desc }}
+
+
+
+
+
+
+
+
+
+
+
+ 曲目来自「背景音乐」页;清空则不播放。
+
+
+
+
+
+
+
+
+
+
+
+
+
回顾幻灯片(全屏上下翻页,{{ cfg.recap.pages.length }} 页)
+
+
+
+ 第 {{ i + 1 }} 页 · {{ recapLayoutMeta(p.layout).label }}
+
+
+
+ 添加页
+
+
+
+
+
+
+
+ 上移
+
+
+ 下移
+
+
+ 删除本页
+
+
宾客上下滑动翻页;自动轮播时按间隔逐页推进
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ thanksSideMeta(cfg?.side || 1).label }} · {{ sectionLabel }}
+ {{ currentRoute }}
+ 已停用
+
+
+
+
+
+
+
+
+
+
diff --git a/vite-tailwindcss/src/views/dx/index.vue b/vite-tailwindcss/src/views/dx/index.vue
new file mode 100644
index 0000000..a70f32a
--- /dev/null
+++ b/vite-tailwindcss/src/views/dx/index.vue
@@ -0,0 +1,916 @@
+
+
+
+
+
+
+ ❀
+ ❀
+ ❀
+ ♥
+ ✦
+
+
+
+
+
+
+
+
谢
+
婚礼答谢函
+
+
轻触打开答谢函
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
{{ t.content || '' }}
+
+
+
+
+
+
+ {{ pageIndex + 1 }}/{{ deckPages.length }}
+
+
+
+
+
+ 上滑查看婚礼回顾
+
+
+
+
+
+
+
+
+
+
![照片预览]()
+
+
+
+
+
+
+
diff --git a/vite-tailwindcss/src/views/hb/index.vue b/vite-tailwindcss/src/views/hb/index.vue
index a5410ef..74bc838 100644
--- a/vite-tailwindcss/src/views/hb/index.vue
+++ b/vite-tailwindcss/src/views/hb/index.vue
@@ -156,6 +156,17 @@
+
+
+
海报暂未开放
@@ -223,6 +234,12 @@ function goInvite() {
else router.push(path)
}
+function goThanks() {
+ const path = cfg.thanksPath || '/dx-xr'
+ if (path.startsWith('http')) window.location.href = path
+ else router.push(path)
+}
+
function upsertMeta(attr, key, content) {
if (!content) return
let el = document.head.querySelector(`meta[${attr}="${key}"]`)
@@ -824,6 +841,29 @@ onUnmounted(() => {
.hb-music-fab.is-playing .fa-pause {
animation: none;
}
+.hb-thanks-link {
+ position: fixed;
+ left: 50%;
+ bottom: calc(14px + env(safe-area-inset-bottom, 0px));
+ transform: translateX(-50%);
+ z-index: 26;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 8px 18px;
+ border-radius: 999px;
+ border: 1px solid rgba(var(--hb-gold-rgb), 0.6);
+ background: rgba(var(--hb-panel-rgb), 0.55);
+ color: var(--hb-gold-soft);
+ font-size: 12px;
+ letter-spacing: 0.18em;
+ cursor: pointer;
+ backdrop-filter: blur(6px);
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
+}
+.hb-thanks-link:hover {
+ filter: brightness(1.1);
+}
@keyframes hb-music-spin {
from { filter: brightness(1); }
50% { filter: brightness(1.12); }
diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue
index edd2a11..cf10505 100644
--- a/vite-tailwindcss/src/views/index/index.vue
+++ b/vite-tailwindcss/src/views/index/index.vue
@@ -917,6 +917,14 @@
导航
+