更新页面效果
This commit is contained in:
@@ -30,3 +30,68 @@
|
||||
验证:
|
||||
- 构建通过(EXIT=0)。
|
||||
- 浏览器实测:注入测试 token 进入后台,相册页管理标签下「页面高度」控件正常渲染。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31(第三版)六项视觉/交互优化
|
||||
|
||||
用户提出 6 项改进,全部实现并通过 `vite build`(EXIT=0):
|
||||
|
||||
1. **图册边框样式配置+预览**:新增 `borderStyle` 字段(none/thin/mat/rounded/double/gold)。
|
||||
- 新建 `src/styles/frames.css`(全局 `gf-frame gf-*` 类,前台后台预览共用)并 import 到 `main.js`。
|
||||
- 新建 `src/composables/borderStyles.js` 导出 `BORDER_STYLES` 与 `borderClass()`。
|
||||
- `PhotoGallery.vue` 各版式图片套用 `frameClass`(polaroid 保留自身边框不受其影响)。
|
||||
- `AdminEditor.vue` 相册页卡片新增「边框样式」下拉 + 实时预览小图;`defaultConfig`/`ensurePageShape` 默认 `borderStyle:'mat'`。
|
||||
|
||||
2. **花瓣优雅化**:`animations.css` 重写 `petalFall`+新增 `petalDrift`(轻微横向摇曳);`index.vue` 花瓣改 SVG 花瓣形(带渐变描边叶脉 + 柔和投影),移除方形 `border-radius`。
|
||||
|
||||
3. **修复爱心只在底部**:原 `.heart-bubble` 固定 `bottom:0` 导致全聚底部。改为随机 `top` 分布 + 负 `animation-delay` 错峰,配合 `heartFloat` 全屏自然上升循环。
|
||||
|
||||
4. **音乐自动播放+授权模态框**:`useAudio.js` 新增 `attemptAutoplay()`(返回是否成功)。`index.vue` 启动时尝试播放,被拦截则弹出优雅授权模态框(“轻触开启”),点击授权播放;保留 `armAutoplay` 首次交互兜底。
|
||||
|
||||
5. **入场揭幕优雅化**:`index.vue` 重设 curtain 为象牙金双开门(柔和渐变+丝光+细金线),徽标改为衬线姓名+❀+细金分隔线+“轻触开启请柬”,去除厚重棕色玻璃卡。
|
||||
|
||||
6. **回执按钮+中文标签+单选标签**:回执 CTA 改为优雅金边胶囊按钮;模态框标题改「出席回执/敬请回复」,label 改中文(姓名/出席情况/祝福语);出席选择由下拉改为单选标签(chip,选中填金)。
|
||||
|
||||
验证:`vite preview` 起服务 curl 返回 200,app 挂载正常。注:Go 后端若改动需重启才能生效(本次未改后端)。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31(第四版)音乐播放流程收紧
|
||||
|
||||
需求:音乐必须先尝试自动播放,播放失败才弹授权模态框(不要提前弹)。
|
||||
|
||||
改动(仅 `src/views/index/index.vue`):
|
||||
- `onMounted` 中先 `attemptAutoplay()`;成功则不弹框、不挂手势监听;失败才 `armAutoplay()` 挂「首次交互解锁」兜底。
|
||||
- 新增 `pendingMusicAuth` 标记:若开启揭幕(introEnabled),失败时不立即弹框,而是等用户点击揭幕(一次有效手势会触发 armAutoplay 直接播放)后,在 `openInvitation` 末尾用 `setTimeout(0)` 二次确认仍未播放才弹授权框;无揭幕则失败立即弹框。
|
||||
- 避免揭幕期间授权框与帘幕重叠、以及用户点击揭幕本可播放却仍弹框的冗余体验。
|
||||
|
||||
验证:`vite build --outDir dist-chk-<timestamp>` 通过(EXIT=0)。本机安全删除损坏,临时构建目录需手动清理。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31(第五版)修复三处问题
|
||||
|
||||
用户反馈 3 个问题,已修复:
|
||||
|
||||
1. **点击入场揭幕不自动播放**:原仅靠 `armAutoplay` 的 window pointerdown 监听,不够可靠。`index.vue` 的 `openInvitation` 改为在揭幕点击(明确用户手势)里直接 `audio.play()`,保证点击即播放;`onMusicAuthSkip`(暂不开启)改为挂起 `armAutoplay` 兜底。
|
||||
|
||||
2. **错落双图(overlap)图片超出可视范围**:根因 `src/styles/frames.css` 的 `.gf-frame` 写了 `width:100%;height:100%`,且在 Tailwind 工具类之后加载,层叠覆盖了 overlap 包装上的 `w-[70%] h-[65%]` 等尺寸,图片被撑满并 `right-4` 偏移而溢出。已移除 `.gf-frame` 的 width/height(改由包装工具类/网格控制尺寸),保留 `position/background/overflow`。构建通过。
|
||||
|
||||
3. **/api/admin/login 返回 404**:后端 `main.go` 路由本就存在(134 行),404 是因宿主机上跑的是旧 Go 二进制。`hunliji-api` 目录 `go build -o hunliji-api.exe` 重编译并重启(占用 8080 的旧进程被杀,新进程监听)。验证:8080 直连与 8888 vite 代理均返回 200 + token。默认账号 admin/admin123。
|
||||
|
||||
注意:前端改动后用户需刷新 8888 页面(HMR 已生效);本机安全删除损坏,临时 `dist-chk-*` 与 `hunliji-api/api-run.log` 需手动清理。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31(第六版)音乐播放流程再修正
|
||||
|
||||
用户澄清:不要去掉授权模态框,但要严格「先静默尝试自动播放 → 失败才弹框」。还原掉之前第四版的 `pendingMusicAuth` 延迟逻辑。
|
||||
|
||||
改动(仅 `src/views/index/index.vue`):
|
||||
- 移除 `pendingMusicAuth` 变量及相关分支。
|
||||
- `onMounted`:`attemptAutoplay()` 失败即 `showMusicAuth = true`(立即弹框)。
|
||||
- `openInvitation`:保留揭幕点击时 `audio.play()`(用户手势兜底,解决「点击揭幕不播放」)。
|
||||
- 授权模态框模板/样式保持不变(轻触开启 → 播放;暂不开启 → 挂 `armAutoplay` 兜底)。
|
||||
|
||||
验证:`vite build --outDir dist-chk-<timestamp>` 通过(EXIT=0)。无残留 `pendingMusicAuth` 引用。
|
||||
|
||||
20
hunliji-api/api-run.log
Normal file
20
hunliji-api/api-run.log
Normal file
@@ -0,0 +1,20 @@
|
||||
数据库初始化成功,表结构已同步,上传目录已就绪
|
||||
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
|
||||
|
||||
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
|
||||
- using env: export GIN_MODE=release
|
||||
- using code: gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
[GIN-debug] GET /uploads/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (4 handlers)
|
||||
[GIN-debug] HEAD /uploads/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (4 handlers)
|
||||
[GIN-debug] POST /api/admin/login --> main.main.func1 (4 handlers)
|
||||
[GIN-debug] GET /api/config --> main.main.func2 (4 handlers)
|
||||
[GIN-debug] POST /api/config --> main.main.func3 (4 handlers)
|
||||
[GIN-debug] POST /api/rsvp --> main.main.func4 (4 handlers)
|
||||
[GIN-debug] POST /api/upload --> main.main.func5 (4 handlers)
|
||||
[GIN-debug] GET /api/rsvp/list --> main.main.func6 (4 handlers)
|
||||
婚礼纪后端 API 已在 http://localhost:8080 启动
|
||||
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
|
||||
Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
|
||||
[GIN-debug] Listening and serving HTTP on :8080
|
||||
[GIN-debug] [ERROR] listen tcp :8080: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
|
||||
BIN
hunliji-api/hunliji-api.exe
Normal file
BIN
hunliji-api/hunliji-api.exe
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
hunliji-api/uploads/1785462317_4112586452.mp3
Normal file
BIN
hunliji-api/uploads/1785462317_4112586452.mp3
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
.admin-tabs[data-v-8b21252d] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{y as k,d as y,e as t,x as a,z as o,A as n,B as z,C as A,b as q,k as c,q as B,r as C,D as p}from"./index-HKNS6H1K.js";const N={class:"min-h-screen w-full flex items-center justify-center bg-[#Fdfbf7] relative overflow-hidden"},S={class:"relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up"},V={class:"mt-6 text-center"},U={__name:"AdminLogin",setup(F){const m=z(),b=A(),f=k(),i=C(!1),r=B({account:"",password:""}),x=async()=>{var u,e;i.value=!0;try{await f.login(r),p.success("登录成功"),m.replace(b.query.redirect||"/admin")}catch(s){const l=((e=(u=s==null?void 0:s.response)==null?void 0:u.data)==null?void 0:e.error)||(s==null?void 0:s.message)||"登录失败";p.error(l)}finally{i.value=!1}};return(u,e)=>{const s=n("a-input"),l=n("a-form-item"),g=n("a-input-password"),v=n("a-button"),_=n("a-form"),w=n("router-link");return q(),y("div",N,[e[7]||(e[7]=t("div",{class:"absolute -top-24 -right-24 w-80 h-80 rounded-full bg-[#a88c6b]/10 blur-3xl"},null,-1)),e[8]||(e[8]=t("div",{class:"absolute -bottom-24 -left-24 w-80 h-80 rounded-full bg-[#e8a5b2]/10 blur-3xl"},null,-1)),e[9]||(e[9]=t("div",{class:"absolute inset-0 pointer-events-none opacity-[0.04]",style:{"background-image":"radial-gradient(#a88c6b 1px, transparent 1px)","background-size":"22px 22px"}},null,-1)),t("div",S,[e[6]||(e[6]=t("div",{class:"text-center mb-8"},[t("div",{class:"text-3xl mb-3 text-[#a88c6b] font-serif"},"❀"),t("h1",{class:"text-xl tracking-[0.3em] text-[#2c2c2c] font-light"},"婚礼纪 · 后台"),t("p",{class:"text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase"},"Admin Console")],-1)),a(_,{model:r,layout:"vertical",onFinish:x},{default:o(()=>[a(l,{name:"account",rules:[{required:!0,message:"请输入账号"}]},{default:o(()=>[a(s,{value:r.account,"onUpdate:value":e[0]||(e[0]=d=>r.account=d),size:"large",placeholder:"管理员账号"},{prefix:o(()=>[...e[2]||(e[2]=[t("i",{class:"fa-solid fa-user text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(l,{name:"password",rules:[{required:!0,message:"请输入密码"}]},{default:o(()=>[a(g,{value:r.password,"onUpdate:value":e[1]||(e[1]=d=>r.password=d),size:"large",placeholder:"密码"},{prefix:o(()=>[...e[3]||(e[3]=[t("i",{class:"fa-solid fa-lock text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(v,{type:"primary","html-type":"submit",size:"large",block:"",loading:i.value,class:"!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b] !rounded-xl !tracking-[0.2em]"},{default:o(()=>[...e[4]||(e[4]=[c(" 登 录 ",-1)])]),_:1},8,["loading"])]),_:1},8,["model"]),t("div",V,[a(w,{to:"/",class:"text-[12px] text-[#a88c6b] hover:underline"},{default:o(()=>[...e[5]||(e[5]=[c("返回请柬预览",-1)])]),_:1})])])])}}};export{U as default};
|
||||
@@ -0,0 +1 @@
|
||||
const n=[{value:"none",label:"无边框"},{value:"thin",label:"细金线"},{value:"mat",label:"白卡纸"},{value:"rounded",label:"柔圆角"},{value:"double",label:"双线框"},{value:"gold",label:"古典金框"}];function r(e){return"gf-frame gf-"+(n.some(a=>a.value===e)?e:"mat")}const u=(e,l)=>{const a=e.__vccOpts||e;for(const[o,t]of l)a[o]=t;return a};export{n as B,u as _,r as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
514
vite-tailwindcss/dist-chk-1785462736/assets/index-HKNS6H1K.js
Normal file
514
vite-tailwindcss/dist-chk-1785462736/assets/index-HKNS6H1K.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
14
vite-tailwindcss/dist-chk-1785462736/index.html
Normal file
14
vite-tailwindcss/dist-chk-1785462736/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>婚礼请柬 · Wedding Invitation</title>
|
||||
<script type="module" crossorigin src="/assets/index-HKNS6H1K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DjNxM5ZE.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
1
vite-tailwindcss/dist-chk-1785462736/vite.svg
Normal file
1
vite-tailwindcss/dist-chk-1785462736/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.admin-tabs[data-v-8b21252d] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}
|
||||
@@ -0,0 +1 @@
|
||||
import{y as k,d as y,e as t,x as a,z as o,A as n,B as z,C as A,b as q,k as c,q as B,r as C,D as p}from"./index-Cy2RptFJ.js";const N={class:"min-h-screen w-full flex items-center justify-center bg-[#Fdfbf7] relative overflow-hidden"},S={class:"relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up"},V={class:"mt-6 text-center"},U={__name:"AdminLogin",setup(F){const m=z(),b=A(),f=k(),i=C(!1),r=B({account:"",password:""}),x=async()=>{var u,e;i.value=!0;try{await f.login(r),p.success("登录成功"),m.replace(b.query.redirect||"/admin")}catch(s){const l=((e=(u=s==null?void 0:s.response)==null?void 0:u.data)==null?void 0:e.error)||(s==null?void 0:s.message)||"登录失败";p.error(l)}finally{i.value=!1}};return(u,e)=>{const s=n("a-input"),l=n("a-form-item"),g=n("a-input-password"),v=n("a-button"),_=n("a-form"),w=n("router-link");return q(),y("div",N,[e[7]||(e[7]=t("div",{class:"absolute -top-24 -right-24 w-80 h-80 rounded-full bg-[#a88c6b]/10 blur-3xl"},null,-1)),e[8]||(e[8]=t("div",{class:"absolute -bottom-24 -left-24 w-80 h-80 rounded-full bg-[#e8a5b2]/10 blur-3xl"},null,-1)),e[9]||(e[9]=t("div",{class:"absolute inset-0 pointer-events-none opacity-[0.04]",style:{"background-image":"radial-gradient(#a88c6b 1px, transparent 1px)","background-size":"22px 22px"}},null,-1)),t("div",S,[e[6]||(e[6]=t("div",{class:"text-center mb-8"},[t("div",{class:"text-3xl mb-3 text-[#a88c6b] font-serif"},"❀"),t("h1",{class:"text-xl tracking-[0.3em] text-[#2c2c2c] font-light"},"婚礼纪 · 后台"),t("p",{class:"text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase"},"Admin Console")],-1)),a(_,{model:r,layout:"vertical",onFinish:x},{default:o(()=>[a(l,{name:"account",rules:[{required:!0,message:"请输入账号"}]},{default:o(()=>[a(s,{value:r.account,"onUpdate:value":e[0]||(e[0]=d=>r.account=d),size:"large",placeholder:"管理员账号"},{prefix:o(()=>[...e[2]||(e[2]=[t("i",{class:"fa-solid fa-user text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(l,{name:"password",rules:[{required:!0,message:"请输入密码"}]},{default:o(()=>[a(g,{value:r.password,"onUpdate:value":e[1]||(e[1]=d=>r.password=d),size:"large",placeholder:"密码"},{prefix:o(()=>[...e[3]||(e[3]=[t("i",{class:"fa-solid fa-lock text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(v,{type:"primary","html-type":"submit",size:"large",block:"",loading:i.value,class:"!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b] !rounded-xl !tracking-[0.2em]"},{default:o(()=>[...e[4]||(e[4]=[c(" 登 录 ",-1)])]),_:1},8,["loading"])]),_:1},8,["model"]),t("div",V,[a(w,{to:"/",class:"text-[12px] text-[#a88c6b] hover:underline"},{default:o(()=>[...e[5]||(e[5]=[c("返回请柬预览",-1)])]),_:1})])])])}}};export{U as default};
|
||||
@@ -0,0 +1 @@
|
||||
const n=[{value:"none",label:"无边框"},{value:"thin",label:"细金线"},{value:"mat",label:"白卡纸"},{value:"rounded",label:"柔圆角"},{value:"double",label:"双线框"},{value:"gold",label:"古典金框"}];function r(e){return"gf-frame gf-"+(n.some(a=>a.value===e)?e:"mat")}const u=(e,l)=>{const a=e.__vccOpts||e;for(const[o,t]of l)a[o]=t;return a};export{n as B,u as _,r as b};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
514
vite-tailwindcss/dist-chk-1785463334/assets/index-Cy2RptFJ.js
Normal file
514
vite-tailwindcss/dist-chk-1785463334/assets/index-Cy2RptFJ.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
14
vite-tailwindcss/dist-chk-1785463334/index.html
Normal file
14
vite-tailwindcss/dist-chk-1785463334/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>婚礼请柬 · Wedding Invitation</title>
|
||||
<script type="module" crossorigin src="/assets/index-Cy2RptFJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B8bH8RjL.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
1
vite-tailwindcss/dist-chk-1785463334/vite.svg
Normal file
1
vite-tailwindcss/dist-chk-1785463334/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
.admin-tabs[data-v-8b21252d] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-8b21252d] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{x as k,d as y,e as t,q as a,y as o,z as n,A as z,B as A,b as q,k as c,p as B,r as C,C as p}from"./index-J-2DY0cU.js";const N={class:"min-h-screen w-full flex items-center justify-center bg-[#Fdfbf7] relative overflow-hidden"},S={class:"relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up"},V={class:"mt-6 text-center"},U={__name:"AdminLogin",setup(F){const m=z(),b=A(),f=k(),i=C(!1),r=B({account:"",password:""}),x=async()=>{var u,e;i.value=!0;try{await f.login(r),p.success("登录成功"),m.replace(b.query.redirect||"/admin")}catch(s){const l=((e=(u=s==null?void 0:s.response)==null?void 0:u.data)==null?void 0:e.error)||(s==null?void 0:s.message)||"登录失败";p.error(l)}finally{i.value=!1}};return(u,e)=>{const s=n("a-input"),l=n("a-form-item"),g=n("a-input-password"),v=n("a-button"),_=n("a-form"),w=n("router-link");return q(),y("div",N,[e[7]||(e[7]=t("div",{class:"absolute -top-24 -right-24 w-80 h-80 rounded-full bg-[#a88c6b]/10 blur-3xl"},null,-1)),e[8]||(e[8]=t("div",{class:"absolute -bottom-24 -left-24 w-80 h-80 rounded-full bg-[#e8a5b2]/10 blur-3xl"},null,-1)),e[9]||(e[9]=t("div",{class:"absolute inset-0 pointer-events-none opacity-[0.04]",style:{"background-image":"radial-gradient(#a88c6b 1px, transparent 1px)","background-size":"22px 22px"}},null,-1)),t("div",S,[e[6]||(e[6]=t("div",{class:"text-center mb-8"},[t("div",{class:"text-3xl mb-3 text-[#a88c6b] font-serif"},"❀"),t("h1",{class:"text-xl tracking-[0.3em] text-[#2c2c2c] font-light"},"婚礼纪 · 后台"),t("p",{class:"text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase"},"Admin Console")],-1)),a(_,{model:r,layout:"vertical",onFinish:x},{default:o(()=>[a(l,{name:"account",rules:[{required:!0,message:"请输入账号"}]},{default:o(()=>[a(s,{value:r.account,"onUpdate:value":e[0]||(e[0]=d=>r.account=d),size:"large",placeholder:"管理员账号"},{prefix:o(()=>[...e[2]||(e[2]=[t("i",{class:"fa-solid fa-user text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(l,{name:"password",rules:[{required:!0,message:"请输入密码"}]},{default:o(()=>[a(g,{value:r.password,"onUpdate:value":e[1]||(e[1]=d=>r.password=d),size:"large",placeholder:"密码"},{prefix:o(()=>[...e[3]||(e[3]=[t("i",{class:"fa-solid fa-lock text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(v,{type:"primary","html-type":"submit",size:"large",block:"",loading:i.value,class:"!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b] !rounded-xl !tracking-[0.2em]"},{default:o(()=>[...e[4]||(e[4]=[c(" 登 录 ",-1)])]),_:1},8,["loading"])]),_:1},8,["model"]),t("div",V,[a(w,{to:"/",class:"text-[12px] text-[#a88c6b] hover:underline"},{default:o(()=>[...e[5]||(e[5]=[c("返回请柬预览",-1)])]),_:1})])])])}}};export{U as default};
|
||||
@@ -0,0 +1 @@
|
||||
const n=[{value:"none",label:"无边框"},{value:"thin",label:"细金线"},{value:"mat",label:"白卡纸"},{value:"rounded",label:"柔圆角"},{value:"double",label:"双线框"},{value:"gold",label:"古典金框"}];function r(e){return"gf-frame gf-"+(n.some(a=>a.value===e)?e:"mat")}const u=(e,l)=>{const a=e.__vccOpts||e;for(const[o,t]of l)a[o]=t;return a};export{n as B,u as _,r as b};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
14
vite-tailwindcss/dist-chk-1785464315/index.html
Normal file
14
vite-tailwindcss/dist-chk-1785464315/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>婚礼请柬 · Wedding Invitation</title>
|
||||
<script type="module" crossorigin src="/assets/index-J-2DY0cU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B8bH8RjL.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
1
vite-tailwindcss/dist-chk-1785464315/vite.svg
Normal file
1
vite-tailwindcss/dist-chk-1785464315/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.admin-tabs[data-v-0b99ae25] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-0b99ae25] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-0b99ae25] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}
|
||||
@@ -1 +0,0 @@
|
||||
import{y as k,d as y,e as t,l as a,m as o,q as n,z,A,b as q,j as c,s as B,r as C,B as p}from"./index-BLDegpD4.js";const N={class:"min-h-screen w-full flex items-center justify-center bg-[#Fdfbf7] relative overflow-hidden"},S={class:"relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up"},V={class:"mt-6 text-center"},R={__name:"AdminLogin",setup(j){const m=z(),b=A(),f=k(),i=C(!1),r=B({account:"",password:""}),x=async()=>{var u,e;i.value=!0;try{await f.login(r),p.success("登录成功"),m.replace(b.query.redirect||"/admin")}catch(s){const l=((e=(u=s==null?void 0:s.response)==null?void 0:u.data)==null?void 0:e.error)||(s==null?void 0:s.message)||"登录失败";p.error(l)}finally{i.value=!1}};return(u,e)=>{const s=n("a-input"),l=n("a-form-item"),g=n("a-input-password"),v=n("a-button"),_=n("a-form"),w=n("router-link");return q(),y("div",N,[e[7]||(e[7]=t("div",{class:"absolute -top-24 -right-24 w-80 h-80 rounded-full bg-[#a88c6b]/10 blur-3xl"},null,-1)),e[8]||(e[8]=t("div",{class:"absolute -bottom-24 -left-24 w-80 h-80 rounded-full bg-[#e8a5b2]/10 blur-3xl"},null,-1)),e[9]||(e[9]=t("div",{class:"absolute inset-0 pointer-events-none opacity-[0.04]",style:{"background-image":"radial-gradient(#a88c6b 1px, transparent 1px)","background-size":"22px 22px"}},null,-1)),t("div",S,[e[6]||(e[6]=t("div",{class:"text-center mb-8"},[t("div",{class:"text-3xl mb-3 text-[#a88c6b] font-serif"},"❀"),t("h1",{class:"text-xl tracking-[0.3em] text-[#2c2c2c] font-light"},"婚礼纪 · 后台"),t("p",{class:"text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase"},"Admin Console")],-1)),a(_,{model:r,layout:"vertical",onFinish:x},{default:o(()=>[a(l,{name:"account",rules:[{required:!0,message:"请输入账号"}]},{default:o(()=>[a(s,{value:r.account,"onUpdate:value":e[0]||(e[0]=d=>r.account=d),size:"large",placeholder:"管理员账号"},{prefix:o(()=>[...e[2]||(e[2]=[t("i",{class:"fa-solid fa-user text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(l,{name:"password",rules:[{required:!0,message:"请输入密码"}]},{default:o(()=>[a(g,{value:r.password,"onUpdate:value":e[1]||(e[1]=d=>r.password=d),size:"large",placeholder:"密码"},{prefix:o(()=>[...e[3]||(e[3]=[t("i",{class:"fa-solid fa-lock text-[#a88c6b]"},null,-1)])]),_:1},8,["value"])]),_:1}),a(v,{type:"primary","html-type":"submit",size:"large",block:"",loading:i.value,class:"!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b] !rounded-xl !tracking-[0.2em]"},{default:o(()=>[...e[4]||(e[4]=[c(" 登 录 ",-1)])]),_:1},8,["loading"])]),_:1},8,["model"]),t("div",V,[a(w,{to:"/",class:"text-[12px] text-[#a88c6b] hover:underline"},{default:o(()=>[...e[5]||(e[5]=[c("返回请柬预览",-1)])]),_:1})])])])}}};export{R as default};
|
||||
@@ -1 +0,0 @@
|
||||
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,51 +9,52 @@
|
||||
|
||||
<!-- 单图(满幅带留白卡纸) -->
|
||||
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
|
||||
class="w-full max-w-[300px] mx-auto aspect-[4/5] bg-white p-2 shadow-sm border-[0.5px] border-[#a88c6b]/20 relative mt-6">
|
||||
<img :src="page.img1" class="w-full h-full object-cover" />
|
||||
class="w-full max-w-[300px] mx-auto aspect-[4/5] mt-6">
|
||||
<div :class="frameClass"><img :src="page.img1" /></div>
|
||||
</div>
|
||||
|
||||
<!-- 错落双图 -->
|
||||
<div v-else-if="page.type === 'overlap'" class="relative w-full h-[50vh] mt-4">
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }" class="absolute top-0 right-4 w-[70%] h-[65%] shadow-md bg-white p-1 z-10 border border-[#a88c6b]/10">
|
||||
<img :src="page.img1" class="w-full h-full object-cover" />
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }" class="absolute top-0 right-4 w-[70%] h-[65%] z-10" :class="frameClass">
|
||||
<img :src="page.img1" />
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-4 left-4 w-[60%] h-[60%] shadow-lg bg-white p-1 z-20 border border-[#a88c6b]/10">
|
||||
<img :src="page.img2" class="w-full h-full object-cover" />
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-4 left-4 w-[60%] h-[60%] z-20" :class="frameClass">
|
||||
<img :src="page.img2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一大五小 -->
|
||||
<div v-else-if="page.type === 'one-large-five-small'"
|
||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative overflow-hidden bg-[#Fdfbf7] shadow-sm">
|
||||
<img :src="page.images[0]" class="w-full h-full object-cover hover:scale-105 transition-transform duration-1000" />
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
||||
<img :src="page.images[0]" class="hover:scale-105 transition-transform duration-1000" />
|
||||
</div>
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative bg-[#Fdfbf7]">
|
||||
<img :src="page.images[i]" class="w-full h-full object-cover" />
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
||||
<img :src="page.images[i]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 九宫格 -->
|
||||
<div v-else-if="page.type === 'nine-grid'" class="grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 60 }"
|
||||
class="aspect-square bg-[#Fdfbf7] relative rounded-sm overflow-hidden">
|
||||
<img :src="img" class="w-full h-full object-cover hover:scale-105 transition-transform duration-700" />
|
||||
class="aspect-square relative" :class="frameClass">
|
||||
<img :src="img" class="hover:scale-105 transition-transform duration-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 瀑布流(错落多列) -->
|
||||
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }" class="g-masonry-item">
|
||||
<img :src="img" class="w-full block" />
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
||||
class="g-masonry-item" :class="frameClass">
|
||||
<img :src="img" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 轮播滑动 -->
|
||||
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
|
||||
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide">
|
||||
<img :src="img" class="w-full h-full object-cover" />
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
|
||||
<img :src="img" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张">‹</button>
|
||||
@@ -64,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍立得拼贴 -->
|
||||
<!-- 拍立得拼贴(自带边框,不受 borderStyle 影响) -->
|
||||
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
|
||||
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
|
||||
@@ -78,12 +79,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
})
|
||||
|
||||
// 边框样式:前台按 page.borderStyle 套用全局 gf-* 类
|
||||
const frameClass = computed(() => borderClass(props.page.borderStyle))
|
||||
|
||||
/* ---------- 轮播状态 ---------- */
|
||||
const index = ref(0)
|
||||
const trackRef = ref(null)
|
||||
@@ -117,8 +122,7 @@ function polaroidTransform(i) {
|
||||
|
||||
/* 瀑布流 */
|
||||
.g-masonry { column-count: 2; column-gap: 10px; }
|
||||
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; overflow: hidden; background: #Fdfbf7; box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
|
||||
.g-masonry-item img { display: block; }
|
||||
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; }
|
||||
|
||||
/* 轮播 */
|
||||
.g-carousel-track {
|
||||
|
||||
14
vite-tailwindcss/src/composables/borderStyles.js
Normal file
14
vite-tailwindcss/src/composables/borderStyles.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// 相册边框样式定义:供前台 PhotoGallery 渲染与后台 AdminEditor 配置/预览共用
|
||||
export const BORDER_STYLES = [
|
||||
{ value: 'none', label: '无边框' },
|
||||
{ value: 'thin', label: '细金线' },
|
||||
{ value: 'mat', label: '白卡纸' },
|
||||
{ value: 'rounded', label: '柔圆角' },
|
||||
{ value: 'double', label: '双线框' },
|
||||
{ value: 'gold', label: '古典金框' },
|
||||
]
|
||||
|
||||
export function borderClass(style) {
|
||||
const v = BORDER_STYLES.some((b) => b.value === style) ? style : 'mat'
|
||||
return 'gf-frame gf-' + v
|
||||
}
|
||||
@@ -62,6 +62,18 @@ export function useAudio() {
|
||||
}
|
||||
|
||||
const play = () => playUrl(activeUrl.value)
|
||||
|
||||
// 尝试自动播放,返回是否成功(被浏览器拦截则返回 false,由调用方决定是否弹授权框)
|
||||
const attemptAutoplay = async () => {
|
||||
if (!activeUrl.value) return false
|
||||
try {
|
||||
await playUrl(activeUrl.value)
|
||||
return isPlaying.value
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (audio) audio.pause()
|
||||
isPlaying.value = false
|
||||
@@ -98,5 +110,5 @@ export function useAudio() {
|
||||
}
|
||||
})
|
||||
|
||||
return { isPlaying, isReady, tracks, activeUrl, currentName, setTracks, setActive, play, pause, toggle, armAutoplay }
|
||||
return { isPlaying, isReady, tracks, activeUrl, currentName, setTracks, setActive, play, pause, toggle, attemptAutoplay, armAutoplay }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import './style.css'
|
||||
import './styles/animations.css'
|
||||
import './styles/frames.css'
|
||||
import App from './App.vue'
|
||||
import '@fortawesome/fontawesome-free/css/all.css' // 引入所有样式
|
||||
// 注册路由
|
||||
|
||||
@@ -25,36 +25,41 @@
|
||||
.reveal { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; }
|
||||
}
|
||||
|
||||
/* ---------- 飘落花瓣 ---------- */
|
||||
/* ---------- 飘落花瓣(优雅:柔和渐变 + 轻微摇曳) ---------- */
|
||||
@keyframes petalFall {
|
||||
0% { transform: translate3d(0, -12vh, 0) rotate(0deg); opacity: 0; }
|
||||
8% { opacity: 0.9; }
|
||||
90% { opacity: 0.85; }
|
||||
100% { transform: translate3d(8vw, 112vh, 0) rotate(420deg); opacity: 0; }
|
||||
10% { opacity: 0.85; }
|
||||
90% { opacity: 0.7; }
|
||||
100% { transform: translate3d(6vw, 112vh, 0) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
@keyframes petalDrift {
|
||||
0%, 100% { margin-left: -14px; }
|
||||
50% { margin-left: 14px; }
|
||||
}
|
||||
.petal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
animation: petalFall linear infinite;
|
||||
animation:
|
||||
petalFall var(--p-dur, 12s) linear infinite,
|
||||
petalDrift var(--p-sway, 6s) ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* ---------- 漂浮爱心 ---------- */
|
||||
/* ---------- 漂浮爱心(全屏自然上升,随机分布) ---------- */
|
||||
@keyframes heartFloat {
|
||||
0% { transform: translateY(110%) translateX(0) scale(0.8); opacity: 0; }
|
||||
10% { opacity: 0.85; }
|
||||
50% { transform: translateY(45%) translateX(14px) scale(1); }
|
||||
90% { opacity: 0.7; }
|
||||
100% { transform: translateY(-15%) translateX(-10px) scale(1.1); opacity: 0; }
|
||||
0% { transform: translateY(20px) translateX(0) scale(0.7); opacity: 0; }
|
||||
12% { opacity: 0.8; }
|
||||
85% { opacity: 0.6; }
|
||||
100% { transform: translateY(-118vh) translateX(24px) scale(1.1); opacity: 0; }
|
||||
}
|
||||
@keyframes heartSway {
|
||||
0% { margin-left: -18px; }
|
||||
100% { margin-left: 18px; }
|
||||
0%, 100% { margin-left: -16px; }
|
||||
50% { margin-left: 16px; }
|
||||
}
|
||||
.heart-bubble {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
will-change: transform, opacity;
|
||||
animation:
|
||||
heartFloat var(--float-duration, 18s) linear infinite,
|
||||
heartSway var(--sway-duration, 5s) ease-in-out infinite alternate;
|
||||
|
||||
71
vite-tailwindcss/src/styles/frames.css
Normal file
71
vite-tailwindcss/src/styles/frames.css
Normal file
@@ -0,0 +1,71 @@
|
||||
/* ============================================================
|
||||
相册边框样式(全局)
|
||||
- 前台 PhotoGallery 按 page.borderStyle 套用
|
||||
- 后台 AdminEditor 预览复用同一套类
|
||||
结构约定:<div class="gf-frame gf-xxx"><img/></div>
|
||||
gf-frame 负责定位与裁切,gf-xxx 负责边框/卡纸/投影
|
||||
============================================================ */
|
||||
.gf-frame {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gf-frame > img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 无边框 */
|
||||
.gf-none {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 细金线 */
|
||||
.gf-thin {
|
||||
padding: 0;
|
||||
border: 1px solid #cdb89c;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(120, 90, 60, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 白卡纸(经典裱框) */
|
||||
.gf-mat {
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 6px 20px rgba(120, 90, 60, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 柔圆角 */
|
||||
.gf-rounded {
|
||||
padding: 0;
|
||||
border: 1px solid rgba(168, 140, 107, 0.4);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 24px rgba(120, 90, 60, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 双线框 */
|
||||
.gf-double {
|
||||
padding: 6px;
|
||||
border: 3px double #b89a78;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 16px rgba(120, 90, 60, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 古典金框(外白 + 金线双层) */
|
||||
.gf-gold {
|
||||
padding: 7px;
|
||||
border: 2px solid #c9a978;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 4px #fff, 0 0 0 5px #e7d6bb, 0 8px 22px rgba(120, 90, 60, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
@@ -170,6 +170,21 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 items-start">
|
||||
<a-form-item label="边框样式">
|
||||
<a-select v-model:value="page.borderStyle">
|
||||
<a-select-option v-for="b in BORDER_STYLES" :key="b.value" :value="b.value">{{ b.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11px] text-[#a88c6b]">预览</span>
|
||||
<div class="w-[70px] h-[88px] shrink-0">
|
||||
<div :class="borderClass(page.borderStyle)" style="width:100%;height:100%">
|
||||
<img src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=200&q=80" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="小标题 (英文)">
|
||||
<a-input v-model:value="page.subtitle" />
|
||||
@@ -258,6 +273,7 @@ import { message } from 'ant-design-vue'
|
||||
import { getConfig, saveConfig, uploadImage, getRsvpList } from '@/api/wedding'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
|
||||
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
@@ -280,10 +296,10 @@ function defaultConfig() {
|
||||
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6), height: '100vh' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9), height: '100vh' },
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6), height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9), height: '100vh', borderStyle: 'mat' },
|
||||
],
|
||||
schedule: [
|
||||
{ time: '16:00', event: '签到迎宾', desc: 'Welcome' },
|
||||
@@ -337,6 +353,7 @@ function ensurePageShape(p) {
|
||||
}
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
p.borderStyle = p.borderStyle || 'mat'
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="wrapper relative w-full h-[100vh] bg-[#Fdfbf7] overflow-hidden text-[#2c2c2c]">
|
||||
|
||||
<!-- 入场揭幕帘幕(双开门 + 印章徽标) -->
|
||||
<!-- 入场揭幕帘幕(雅致双开门 + 徽标) -->
|
||||
<div v-show="showIntro" class="curtain-root" :class="{ 'is-opening': introOpening }" @click="openInvitation">
|
||||
<div class="curtain-bg" :style="curtainBg"></div>
|
||||
<div class="curtain-veil"></div>
|
||||
<div class="curtain-door curtain-door-left"></div>
|
||||
<div class="curtain-door curtain-door-right"></div>
|
||||
<div class="curtain-door curtain-door-left"><span class="door-sheen"></span></div>
|
||||
<div class="curtain-door curtain-door-right"><span class="door-sheen"></span></div>
|
||||
<div class="curtain-emblem">
|
||||
<div class="emblem-card">
|
||||
<span class="emblem-label">WEDDING INVITATION</span>
|
||||
<div class="emblem-seal">❀</div>
|
||||
<div class="emblem-names">{{ data.groom }} <span class="amp">&</span> {{ data.bride }}</div>
|
||||
<div class="emblem-inner">
|
||||
<span class="emblem-floral">❀</span>
|
||||
<span class="emblem-kicker">WEDDING INVITATION</span>
|
||||
<div class="emblem-names">{{ data.groom }}<span class="amp">&</span>{{ data.bride }}</div>
|
||||
<div class="emblem-rule"></div>
|
||||
<div class="emblem-date">{{ data.date }}</div>
|
||||
<div class="emblem-hint"><span class="dot"></span> 点击开启请柬</div>
|
||||
<div class="emblem-hint"><span class="dot"></span>轻触开启请柬</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,14 +22,19 @@
|
||||
<!-- 飘落花瓣层 -->
|
||||
<div v-if="data.petalEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="p in petals" :key="'p'+p.id" class="petal"
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, background: p.color, animationDuration: p.duration, animationDelay: p.delay, transform: `rotate(${p.rotate}deg)` }"></div>
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, animationDelay: p.delay, '--p-dur': p.duration, '--p-sway': p.sway }">
|
||||
<svg viewBox="0 0 100 100" class="w-full h-full" :style="{ filter: 'drop-shadow(0 2px 3px rgba(150,110,90,0.12))' }">
|
||||
<path d="M50 3 C28 26 20 62 50 97 C80 62 72 26 50 3 Z" :fill="p.color" opacity="0.9" />
|
||||
<path d="M50 9 C50 36 50 70 50 91" stroke="rgba(255,255,255,0.55)" stroke-width="1.4" fill="none" opacity="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 漂浮爱心层 -->
|
||||
<div v-if="data.bubbleEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="b in bubbles" :key="'b'+b.id"
|
||||
class="heart-bubble flex items-center justify-center"
|
||||
:style="{ left: b.left, color: b.color, fontSize: b.size+'px', opacity: b.opacity, '--float-duration': b.floatDuration, '--sway-duration': b.swayDuration }">♥</div>
|
||||
:style="{ left: b.left, top: b.top, color: b.color, fontSize: b.size+'px', opacity: b.opacity, animationDelay: b.delay, '--float-duration': b.floatDuration, '--sway-duration': b.swayDuration }">♥</div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部滚动进度条 -->
|
||||
@@ -66,9 +72,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 低调管理入口 -->
|
||||
<router-link to="/login" class="absolute bottom-4 right-4 z-50 text-[10px] text-[#a88c6b]/50 hover:text-[#a88c6b] tracking-widest">管理后台</router-link>
|
||||
|
||||
<!-- 主滚动区域 -->
|
||||
<div ref="scrollContainerRef"
|
||||
class="w-full h-full overflow-y-auto no-scrollbar relative scroll-smooth"
|
||||
@@ -165,10 +168,11 @@
|
||||
<span class="text-[11px] text-[#8A8680] font-light tracking-[0.1em] leading-loose block max-w-[240px] mx-auto whitespace-pre-wrap">{{ data.address }}</span>
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 300 }" class="flex flex-col gap-4 w-full px-8 max-w-[280px]">
|
||||
<div @click="isDrawerOpen = true" class="w-full bg-[#a88c6b] text-white py-4 text-[11px] tracking-[0.2em] flex items-center justify-center gap-3 shadow-md uppercase rounded-full cursor-pointer hover:bg-[#967b5c] transition-all">
|
||||
<img src="https://api.iconify.design/lucide:send.svg?color=%23ffffff" class="w-3.5 h-3.5" />
|
||||
<span>填写出席回执</span>
|
||||
</div>
|
||||
<button class="rsvp-cta" @click="isDrawerOpen = true">
|
||||
<span class="rsvp-cta-deco">❀</span>
|
||||
<span class="rsvp-cta-text">填写出席回执</span>
|
||||
<span class="rsvp-cta-line"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,26 +182,24 @@
|
||||
<div v-if="isDrawerOpen" class="absolute inset-0 z-[60] bg-black/40 backdrop-blur-sm transition-opacity duration-500" @click="closeDrawer"></div>
|
||||
<div class="absolute bottom-0 left-0 w-full bg-white rounded-t-[2.5rem] shadow-2xl transition-transform duration-500 z-[70] p-10 flex flex-col" :style="{ transform: isDrawerOpen ? 'translateY(0)' : 'translateY(100%)' }">
|
||||
<div @click="closeDrawer" class="absolute top-6 right-6 p-2 text-[#a88c6b] cursor-pointer hover:text-[#2c2c2c] transition-colors text-xl">✕</div>
|
||||
<span class="text-xl text-center text-[#a88c6b] mb-1 tracking-[0.3em] font-light mt-2 uppercase">Guest RSVP</span>
|
||||
<span class="text-center text-[10px] text-[#8A8680] mb-10 uppercase tracking-[0.4em]">R . S . V . P</span>
|
||||
<span class="text-xl text-center text-[#a88c6b] mb-1 tracking-[0.3em] font-light mt-2">出席回执</span>
|
||||
<span class="text-center text-[10px] text-[#8A8680] mb-10 tracking-[0.4em]">敬 请 回 复</span>
|
||||
|
||||
<div v-if="!isSubmitted" class="space-y-6">
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Name</span>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest pl-1">姓名</span>
|
||||
<input v-model="rsvpForm.name" placeholder="您的姓名" class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] focus:outline-none focus:border-[#a88c6b] shadow-[inset_0_2px_4px_rgba(0,0,0,0.02)]" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Attendance</span>
|
||||
<div @click="isSelectOpen = !isSelectOpen" class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] flex items-center justify-between cursor-pointer">
|
||||
<span>{{ rsvpOptions.find(o => o.value === rsvpForm.guest_count)?.label || '请选择出席人数' }}</span>
|
||||
<span class="text-[#a88c6b] transition-transform" :style="{ transform: isSelectOpen ? 'rotate(180deg)' : '' }">▼</span>
|
||||
</div>
|
||||
<div v-show="isSelectOpen" class="absolute left-10 right-10 bottom-full mb-3 bg-white border border-[#a88c6b]/20 rounded-2xl shadow-2xl overflow-hidden z-[80]">
|
||||
<div v-for="opt in rsvpOptions" :key="opt.value" @click="selectRsvpCount(opt.value)" class="px-5 py-4 text-[13px] hover:bg-[#Fdfbf7] cursor-pointer" :class="rsvpForm.guest_count === opt.value ? 'text-[#a88c6b] bg-[#Fdfbf7]/50' : ''">{{ opt.label }}</div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-2 tracking-widest pl-1">出席情况</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button v-for="opt in rsvpOptions" :key="opt.value" type="button"
|
||||
class="rsvp-tag" :class="{ active: rsvpForm.guest_count === opt.value }"
|
||||
@click="rsvpForm.guest_count = opt.value">{{ opt.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Wishes</span>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest pl-1">祝福语</span>
|
||||
<textarea v-model="rsvpForm.wishes" placeholder="写下您的祝福..." class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] h-24 focus:outline-none resize-none shadow-[inset_0_2px_4px_rgba(0,0,0,0.02)]"></textarea>
|
||||
</div>
|
||||
<div @click="handleSubmitRsvp" class="w-full bg-[#a88c6b] text-white py-4 rounded-full text-center tracking-[0.3em] text-[12px] cursor-pointer hover:bg-[#967b5c] transition-all shadow-md">确认发送</div>
|
||||
@@ -208,6 +210,7 @@
|
||||
<span class="text-[13px] text-[#8A8680] leading-loose tracking-widest font-light">回执已妥投,期待与您共享喜悦</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -228,19 +231,19 @@ const data = ref({
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80',
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: Array.from({ length: 6 }, () => 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=600&q=80') },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: Array.from({ length: 9 }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80') },
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', borderStyle: 'mat' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', borderStyle: 'mat' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: Array.from({ length: 6 }, () => 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=600&q=80'), borderStyle: 'mat' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: Array.from({ length: 9 }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80'), borderStyle: 'mat' },
|
||||
],
|
||||
schedule: [ { time: '16:00', event: '签到迎宾', desc: 'Welcome' }, { time: '17:08', event: '仪式开始', desc: 'Ceremony' }, { time: '18:00', event: '敬备喜宴', desc: 'Banquet' } ],
|
||||
...DEFAULTS,
|
||||
})
|
||||
|
||||
const audio = useAudio()
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSelectOpen = ref(false), isSubmitted = ref(false)
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)
|
||||
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0), parallax = ref(0)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMusicAuth = ref(false)
|
||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||
const rsvpOptions = [ { value: '1', label: '1 人出席' }, { value: '2', label: '2 人出席' }, { value: '3', label: '3 人及以上' }, { value: '0', label: '遗憾缺席' } ]
|
||||
|
||||
@@ -248,8 +251,18 @@ let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null
|
||||
|
||||
const loveColors = ['#a88c6b', '#e8a5b2', '#f4c2c2', '#d4af37']
|
||||
const petalColors = ['#f7d9e0', '#fbeee3', '#f3c6d3', '#efd9c4', '#f9e7d6']
|
||||
const bubbles = Array.from({ length: 15 }).map((_, i) => ({ id: i, left: Math.random() * 100 + '%', color: loveColors[i % 4], size: 14 + Math.random() * 10, opacity: 0.2, floatDuration: 14 + Math.random() * 10 + 's', swayDuration: 4 + Math.random() * 2 + 's' }))
|
||||
const petals = Array.from({ length: 18 }).map((_, i) => ({ id: i, left: Math.random() * 100 + '%', size: 10 + Math.random() * 14, opacity: 0.4 + Math.random() * 0.4, color: petalColors[i % petalColors.length], duration: 9 + Math.random() * 8 + 's', delay: Math.random() * 10 + 's', rotate: Math.random() * 360 }))
|
||||
const bubbles = Array.from({ length: 15 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', top: Math.random() * 100 + '%',
|
||||
color: loveColors[i % 4], size: 13 + Math.random() * 10, opacity: 0.25 + Math.random() * 0.15,
|
||||
floatDuration: (14 + Math.random() * 10) + 's', swayDuration: (4 + Math.random() * 2) + 's',
|
||||
delay: '-' + (Math.random() * 18).toFixed(1) + 's',
|
||||
}))
|
||||
const petals = Array.from({ length: 18 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', size: 10 + Math.random() * 14,
|
||||
opacity: 0.4 + Math.random() * 0.4, color: petalColors[i % petalColors.length],
|
||||
duration: (9 + Math.random() * 8) + 's', delay: (Math.random() * 10) + 's',
|
||||
sway: (4 + Math.random() * 4) + 's',
|
||||
}))
|
||||
|
||||
const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` }))
|
||||
|
||||
@@ -259,8 +272,8 @@ const pageHeightStyle = (page) => {
|
||||
return h === 'auto' ? { minHeight: 'auto' } : { minHeight: h }
|
||||
}
|
||||
|
||||
const selectRsvpCount = (val) => { rsvpForm.guest_count = val; isSelectOpen.value = false }
|
||||
const closeDrawer = () => { isDrawerOpen.value = false; isSelectOpen.value = false }
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const authorizeMusic = () => { audio.play(); showMusicAuth.value = false }
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = scrollContainerRef.value
|
||||
@@ -315,6 +328,13 @@ const openInvitation = () => {
|
||||
if (introOpening.value) return
|
||||
introOpening.value = true
|
||||
introTimer = setTimeout(() => { showIntro.value = false }, 1000)
|
||||
// 点击揭幕是明确的用户手势:直接尝试播放背景音乐(最可靠的自动播放路径)
|
||||
if (data.value.musicList.length) audio.play()
|
||||
}
|
||||
const onMusicAuthSkip = () => {
|
||||
showMusicAuth.value = false
|
||||
// 用户暂不开启:挂起首次交互解锁,后续任意点击仍可播放
|
||||
audio.armAutoplay()
|
||||
}
|
||||
const selectTrack = (url) => { audio.setActive(url); showTrackList.value = false }
|
||||
|
||||
@@ -342,10 +362,14 @@ onMounted(async () => {
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
|
||||
audio.setTracks(data.value.musicList, data.value.activeMusicUrl)
|
||||
// 先静默尝试自动播放;被浏览器拦截(无用户手势)时,再弹出授权模态框
|
||||
if (data.value.musicList.length) {
|
||||
const autoOk = await audio.attemptAutoplay()
|
||||
if (!autoOk) showMusicAuth.value = true
|
||||
}
|
||||
if (data.value.introEnabled) {
|
||||
showIntro.value = true
|
||||
introTimer = setTimeout(() => openInvitation(), 3600)
|
||||
audio.armAutoplay()
|
||||
}
|
||||
|
||||
startAutoScroll()
|
||||
@@ -369,46 +393,77 @@ onUnmounted(() => {
|
||||
.vertical-text { writing-mode: vertical-rl; letter-spacing: 0.4em; text-orientation: upright; }
|
||||
.page-section { min-height: 100vh; width: 100%; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
/* 花瓣形状 */
|
||||
.petal { border-radius: 0 100% 0 100%; }
|
||||
.heart-bubble { animation: heartFloat var(--float-duration, 18s) linear infinite, heartSway var(--sway-duration, 5s) ease-in-out infinite alternate; position: absolute; bottom: 0; }
|
||||
/* 花瓣形状由 SVG 决定,无需额外形状样式 */
|
||||
|
||||
/* ---------- 入场揭幕 ---------- */
|
||||
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: "PingFang SC", sans-serif; }
|
||||
/* ---------- 入场揭幕(雅致象牙金) ---------- */
|
||||
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: "PingFang SC", sans-serif; background: #fbf6ef; }
|
||||
.curtain-bg {
|
||||
position: absolute; inset: -24px; background-size: cover; background-position: center;
|
||||
filter: blur(14px) brightness(0.5) saturate(1.1); transform: scale(1.12);
|
||||
filter: blur(16px) brightness(0.7) saturate(1.05); transform: scale(1.12);
|
||||
}
|
||||
.curtain-veil { position: absolute; inset: 0; background: radial-gradient(circle at 50% 42%, rgba(0,0,0,0.15), rgba(0,0,0,0.5)); }
|
||||
.curtain-veil { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(251,246,239,0.35), rgba(232,214,194,0.25)); }
|
||||
.curtain-door {
|
||||
position: absolute; top: 0; bottom: 0; width: 50%; z-index: 2;
|
||||
background: linear-gradient(160deg, #b89a78 0%, #a88c6b 45%, #8a704f 100%);
|
||||
box-shadow: inset 0 0 60px rgba(0,0,0,0.25);
|
||||
transition: transform 1s cubic-bezier(0.76, 0, 0.24, 1);
|
||||
background: linear-gradient(160deg, #fbf6ef 0%, #f5e9dc 45%, #ecd9c4 100%);
|
||||
box-shadow: inset 0 0 50px rgba(184,154,120,0.12);
|
||||
transition: transform 1.4s cubic-bezier(0.76, 0, 0.24, 1);
|
||||
}
|
||||
.curtain-door::after { content: ''; position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(255,255,255,0.18); }
|
||||
.curtain-door::before { content: ''; position: absolute; top: 0; bottom: 0; width: 1px; background: linear-gradient(180deg, transparent, rgba(184,154,120,0.5), transparent); }
|
||||
.curtain-door-left { left: 0; }
|
||||
.curtain-door-right { right: 0; }
|
||||
.curtain-door-right::after { right: 0; }
|
||||
.curtain-door-left::before { right: 0; }
|
||||
.curtain-door-right::before { left: 0; }
|
||||
.door-sheen { position: absolute; inset: 0; background: linear-gradient(115deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.25) 100%); }
|
||||
.curtain-root.is-opening .curtain-door-left { transform: translateX(-100%); }
|
||||
.curtain-root.is-opening .curtain-door-right { transform: translateX(100%); }
|
||||
.curtain-emblem {
|
||||
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 3;
|
||||
transition: opacity 0.7s ease, transform 0.9s ease; animation: emblemIn 1s ease both;
|
||||
transition: opacity 0.8s ease, transform 0.9s ease; animation: emblemIn 1.1s ease both;
|
||||
}
|
||||
.curtain-root.is-opening .curtain-emblem { opacity: 0; transform: scale(1.3); }
|
||||
.emblem-card { text-align: center; color: #fff; padding: 30px 34px; border-radius: 18px;
|
||||
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); backdrop-filter: blur(8px);
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||
.emblem-label { display: block; letter-spacing: 0.55em; font-size: 10px; opacity: 0.85; text-transform: uppercase; }
|
||||
.emblem-seal { width: 66px; height: 66px; margin: 16px auto; border: 1px solid rgba(255,255,255,0.6); border-radius: 9999px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 24px; color: #fff; }
|
||||
.emblem-names { font-family: "Noto Serif SC", serif; font-size: 26px; letter-spacing: 0.15em; font-weight: 300; }
|
||||
.emblem-names .amp { font-style: italic; opacity: 0.85; margin: 0 6px; }
|
||||
.emblem-date { font-size: 11px; letter-spacing: 0.3em; opacity: 0.8; margin-top: 10px; }
|
||||
.emblem-hint { margin-top: 22px; font-size: 11px; letter-spacing: 0.3em; opacity: 0.85;
|
||||
.curtain-root.is-opening .curtain-emblem { opacity: 0; transform: scale(1.25); }
|
||||
.emblem-inner { text-align: center; color: #5b4a36; padding: 10px; }
|
||||
.emblem-floral { display: block; font-size: 22px; color: #b89a78; margin-bottom: 14px; }
|
||||
.emblem-kicker { display: block; letter-spacing: 0.6em; font-size: 9px; color: #b89a78; text-transform: uppercase; margin-bottom: 18px; padding-left: 0.6em; }
|
||||
.emblem-names { font-family: "Noto Serif SC", serif; font-size: 30px; letter-spacing: 0.18em; font-weight: 300; color: #4a3f30; }
|
||||
.emblem-names .amp { font-style: italic; color: #b89a78; margin: 0 8px; }
|
||||
.emblem-rule { width: 46px; height: 1px; background: linear-gradient(90deg, transparent, #b89a78, transparent); margin: 18px auto; }
|
||||
.emblem-date { font-size: 11px; letter-spacing: 0.3em; color: #8a7458; }
|
||||
.emblem-hint { margin-top: 26px; font-size: 11px; letter-spacing: 0.3em; color: #a08a66;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.emblem-hint .dot { width: 6px; height: 6px; border-radius: 9999px; background: #fff; animation: hintPulse 1.6s infinite; }
|
||||
.emblem-hint .dot { width: 6px; height: 6px; border-radius: 9999px; background: #b89a78; animation: hintPulse 1.6s infinite; }
|
||||
|
||||
/* ---------- 回执 CTA 按钮 ---------- */
|
||||
.rsvp-cta {
|
||||
position: relative; width: 100%; padding: 16px 0; border-radius: 9999px;
|
||||
border: 1px solid #c9a978; background: rgba(255,255,255,0.6);
|
||||
color: #8a704f; font-size: 12px; letter-spacing: 0.28em; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; gap: 10px;
|
||||
backdrop-filter: blur(8px); transition: all .35s ease;
|
||||
box-shadow: 0 6px 18px rgba(184,154,120,0.12);
|
||||
}
|
||||
.rsvp-cta:hover { background: #a88c6b; color: #fff; border-color: #a88c6b; box-shadow: 0 10px 26px rgba(184,154,120,0.28); }
|
||||
.rsvp-cta-deco { font-size: 13px; opacity: .9; }
|
||||
.rsvp-cta-line { width: 18px; height: 1px; background: currentColor; opacity: .6; }
|
||||
|
||||
/* ---------- 出席情况单选标签 ---------- */
|
||||
.rsvp-tag {
|
||||
flex: 1 1 auto; min-width: 72px; padding: 10px 12px; border-radius: 12px;
|
||||
border: 1px solid rgba(168,140,107,0.35); background: #Fdfbf7; color: #6b6863;
|
||||
font-size: 12px; letter-spacing: 0.1em; cursor: pointer; transition: all .25s ease;
|
||||
}
|
||||
.rsvp-tag:hover { border-color: #a88c6b; }
|
||||
.rsvp-tag.active { background: #a88c6b; border-color: #a88c6b; color: #fff; box-shadow: 0 4px 12px rgba(168,140,107,0.25); }
|
||||
|
||||
/* ---------- 音乐授权模态框 ---------- */
|
||||
.music-auth-card { width: 240px; background: #fff; border-radius: 22px; padding: 28px 24px 22px; text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.28); border: 1px solid rgba(168,140,107,0.2); }
|
||||
.music-auth-note { font-size: 26px; color: #a88c6b; margin-bottom: 12px; }
|
||||
.music-auth-title { font-size: 16px; color: #4a3f30; letter-spacing: 0.2em; margin-bottom: 6px; }
|
||||
.music-auth-sub { font-size: 11px; color: #8A8680; letter-spacing: 0.15em; margin-bottom: 20px; }
|
||||
.music-auth-btn { width: 100%; padding: 11px 0; border-radius: 9999px; background: #a88c6b; color: #fff;
|
||||
font-size: 12px; letter-spacing: 0.2em; cursor: pointer; border: none; transition: background .3s; }
|
||||
.music-auth-btn:hover { background: #967b5c; }
|
||||
.music-auth-skip { margin-top: 10px; background: none; border: none; color: #b0a99e; font-size: 11px; letter-spacing: 0.1em; cursor: pointer; }
|
||||
|
||||
/* ---------- 音乐曲目弹层 ---------- */
|
||||
.music-wrap { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user