恢复滚动方案

This commit is contained in:
李琦
2026-08-01 14:55:21 +08:00
parent a6dec89d09
commit f65000336a
97 changed files with 98 additions and 3197 deletions

View File

@@ -1 +0,0 @@
.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

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<!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>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.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}

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<!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>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +0,0 @@
.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

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<!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>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.admin-tabs[data-v-2abef6e9] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-2abef6e9] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-2abef6e9] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}

View File

@@ -1 +0,0 @@
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-CZ-hSlJt.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};

View File

@@ -1 +0,0 @@
const v=[{value:"none",label:"无边框"},{value:"thin",label:"细金线"},{value:"mat",label:"白卡纸"},{value:"rounded",label:"柔圆角"},{value:"double",label:"双线框"},{value:"gold",label:"古典金框"},{value:"shadow",label:"阴影浮起"},{value:"ink",label:"墨黑描边"},{value:"card",label:"悬浮卡片"},{value:"lace",label:"虚线文艺"},{value:"glow",label:"柔光金晕"}];function t(l){return"gf-frame gf-"+(v.some(a=>a.value===l)?l:"mat")}const b=(l,e)=>{const a=l.__vccOpts||l;for(const[o,u]of e)a[o]=u;return a};export{v as B,b as _,t as b};

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

View File

@@ -1,14 +0,0 @@
<!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-CZ-hSlJt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bh11Omyp.css">
</head>
<body>
<div id="app"></div>
</body>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.admin-tabs[data-v-a7a3ebcb] .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#a88c6b}.admin-tabs[data-v-a7a3ebcb] .ant-tabs-ink-bar{background:#a88c6b}.admin-tabs[data-v-a7a3ebcb] .ant-tabs-tab:hover .ant-tabs-tab-btn{color:#967b5c}

View File

@@ -1 +0,0 @@
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-B7Bp0mE9.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};

View File

@@ -1 +0,0 @@
const v=[{value:"none",label:"无边框"},{value:"thin",label:"细金线"},{value:"mat",label:"白卡纸"},{value:"rounded",label:"柔圆角"},{value:"double",label:"双线框"},{value:"gold",label:"古典金框"},{value:"shadow",label:"阴影浮起"},{value:"ink",label:"墨黑描边"},{value:"card",label:"悬浮卡片"},{value:"lace",label:"虚线文艺"},{value:"glow",label:"柔光金晕"}];function t(l){return"gf-frame gf-"+(v.some(a=>a.value===l)?l:"mat")}const b=(l,e)=>{const a=l.__vccOpts||l;for(const[o,u]of e)a[o]=u;return a};export{v as B,b as _,t as b};

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

View File

@@ -1,14 +0,0 @@
<!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-B7Bp0mE9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-gh4sePj7.css">
</head>
<body>
<div id="app"></div>
</body>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View File

@@ -8,8 +8,8 @@ import { onMounted, onUnmounted, watch, ref } from 'vue'
const props = defineProps({
petalEnabled: { type: Boolean, default: true },
bubbleEnabled: { type: Boolean, default: true },
/** 预览/抽屉等遮罩时关闭粒子绘制 */
active: { type: Boolean, default: true },
performanceMode: { type: Boolean, default: false },
})
const canvasRef = ref(null)
@@ -24,7 +24,6 @@ let width = 0
let height = 0
let dpr = 1
let lastTime = 0
let lastPaint = 0
let resizeObserver = null
let reduceMotion = false
@@ -55,16 +54,14 @@ function resetHeart(h, initial = false) {
}
function seedParticles() {
const petalCount = props.performanceMode ? 8 : 18
const heartCount = props.performanceMode ? 5 : 12
petals.length = 0
hearts.length = 0
for (let i = 0; i < petalCount; i += 1) {
for (let i = 0; i < 18; i += 1) {
const p = {}
resetPetal(p, true)
petals.push(p)
}
for (let i = 0; i < heartCount; i += 1) {
for (let i = 0; i < 12; i += 1) {
const h = {}
resetHeart(h, true)
hearts.push(h)
@@ -77,7 +74,7 @@ function resize() {
const rect = canvas.getBoundingClientRect()
width = Math.max(1, rect.width)
height = Math.max(1, rect.height)
dpr = props.performanceMode ? 1 : Math.min(window.devicePixelRatio || 1, 1.5)
dpr = Math.min(window.devicePixelRatio || 1, 2)
canvas.width = Math.round(width * dpr)
canvas.height = Math.round(height * dpr)
ctx = canvas.getContext('2d')
@@ -130,15 +127,8 @@ function tick(now) {
return
}
const frameInterval = props.performanceMode ? 50 : 16.7
if (lastPaint && now - lastPaint < frameInterval) {
rafId = requestAnimationFrame(tick)
return
}
const delta = Math.min(48, now - (lastTime || now)) / 1000
lastTime = now
lastPaint = now
ctx.clearRect(0, 0, width, height)
if (props.petalEnabled) {
@@ -164,7 +154,6 @@ function tick(now) {
function start() {
cancelAnimationFrame(rafId)
lastTime = 0
lastPaint = 0
if (!props.active || reduceMotion || (!props.petalEnabled && !props.bubbleEnabled)) {
ctx?.clearRect(0, 0, width, height)
rafId = 0
@@ -181,13 +170,7 @@ onMounted(() => {
start()
})
watch(
() => [props.petalEnabled, props.bubbleEnabled, props.active, props.performanceMode],
() => {
resize()
start()
}
)
watch(() => [props.petalEnabled, props.bubbleEnabled, props.active], start)
onUnmounted(() => {
cancelAnimationFrame(rafId)
@@ -203,7 +186,5 @@ onUnmounted(() => {
width: 100%;
height: 100vh;
pointer-events: none;
contain: strict;
transform: translateZ(0);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div ref="galleryRef" class="gallery">
<div class="gallery">
<!-- 标题区 -->
<div v-reveal="'up'" class="text-center mb-5 px-4">
<span class="gallery-title calligraphy text-xl text-[#a88c6b] tracking-[0.3em] font-bold block mb-2">{{ page.title }}</span>
@@ -10,18 +10,20 @@
<!-- 单图默认居中卡幅 / 可配置宽度铺满 -->
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
<div :class="singleFrameClass"><img :src="page.img1" class="preview-img" decoding="async" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" /></div>
<div :class="singleFrameClass">
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
</div>
</div>
<!-- 错落双图交错拼贴左上大图 + 右下压角叠放微旋转clamp 封顶不溢出 -->
<!-- 错落双图 -->
<div v-else-if="page.type === 'overlap'" class="overlap-wrap relative w-full mt-2">
<div v-reveal="{ variant: 'left', delay: 100 }"
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2" :class="frameClass">
<img :src="page.img1" class="preview-img" decoding="async" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" />
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
</div>
<div v-reveal="{ variant: 'right', delay: 300 }"
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3" :class="frameClass">
<img :src="page.img2" class="preview-img" decoding="async" @click.stop="previewImage(page.img2)" @load="notifyLayoutChange" />
<img :src="page.img2" class="preview-img" @click.stop="previewImage(page.img2)" />
</div>
</div>
@@ -29,27 +31,27 @@
<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" :class="frameClass">
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" decoding="async" @click.stop="previewImage(page.images[0])" @load="notifyLayoutChange" />
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
</div>
<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]" class="preview-img" decoding="async" @click.stop="previewImage(page.images[i])" @load="notifyLayoutChange" />
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
</div>
</div>
<!-- 九宫格 -->
<!-- 九宫格保留方向性 v-reveal 新动画 -->
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
v-reveal="getRevealConfig(imgIdx)"
class="aspect-square relative" :class="frameClass">
<img :src="img" class="nine-grid-img preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" />
</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" :class="frameClass">
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
</div>
</div>
@@ -57,7 +59,7 @@
<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" :class="frameClass">
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
</div>
</div>
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张"></button>
@@ -68,15 +70,14 @@
</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) }">
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
</div>
</div>
<!-- 兜底 -->
<div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式{{ page.type }}</div>
</div>
</template>
@@ -84,48 +85,28 @@
<script setup>
import { ref, computed } from 'vue'
import { borderClass } from '@/composables/borderStyles'
import { useImageWarmup } from '@/composables/useImageWarmup'
const props = defineProps({
page: { type: Object, required: true },
})
const emit = defineEmits(['preview-image', 'layout-change', 'warmup-change'])
const emit = defineEmits(['preview-image'])
const galleryRef = ref(null)
useImageWarmup(galleryRef, {
onBusyChange: (busy) => emit('warmup-change', busy),
})
// 边框样式:前台按 page.borderStyle 套用全局 gf-* 类
const frameClass = computed(() => borderClass(props.page.borderStyle))
/* ---------- 单图宽度模式 ---------- */
// singleWidth: 'default'(居中卡幅) | 'full'(宽度铺满)singleKeepRatio: 铺满时是否保持原始宽高比
const singleWrapClass = computed(() =>
props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
)
const singleWrapStyle = computed(() => {
if (props.page.singleWidth === 'full') {
// 沾满宽度:负边距抵消 page-section 的 p-624px×2图片贴到屏幕两缘
const bleed = { width: 'calc(100% + 48px)', marginLeft: '-24px', marginRight: '-24px' }
// 保持原始比例 → 高度由图片自然撑开;否则固定 4:5 裁切
return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: '4 / 5' }
}
return { aspectRatio: '4 / 5' }
})
// 保持原始比例时 img 需要自然高度gf-natural 覆盖 gf-frame 的 height:100%
const singleFrameClass = computed(() =>
frameClass.value + (props.page.singleWidth === 'full' && props.page.singleKeepRatio ? ' gf-natural' : '')
)
/** 仅高度可能随图片加载变化的版式才通知父级重算 scroll metrics */
const needsLayoutNotify = computed(() => {
if (props.page.height === 'auto') return true
if (props.page.type === 'single' && props.page.singleWidth === 'full' && props.page.singleKeepRatio) return true
return false
})
/* ---------- 轮播状态 ---------- */
const index = ref(0)
const trackRef = ref(null)
@@ -147,12 +128,6 @@ function previewImage(src) {
if (src) emit('preview-image', src)
}
function notifyLayoutChange() {
if (!needsLayoutNotify.value) return
emit('layout-change')
}
/* ---------- 拍立得旋转/位移 ---------- */
const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]
const polaroidShift = [0, 10, -8, 6, -10, 8, 4, -6]
function polaroidTransform(i) {
@@ -165,23 +140,15 @@ function getRevealConfig(imgIdx) {
const row = Math.floor(imgIdx / 3)
const col = imgIdx % 3
// 正中心:用 scale 代替 blur避免大图 filter 动画与解码叠峰
if (imgIdx === 4) {
return {
variant: 'scale',
delay: 200,
}
return { variant: 'scale', delay: 200 }
}
// 中间列:上 → down下 → up
if (col === 1) {
return {
variant: row === 0 ? 'down' : 'up',
delay: 80 + imgIdx * 50,
}
}
// 左右列
return {
variant: col === 0 ? 'left' : 'right',
delay: 80 + ((imgIdx + 80) * 20),
@@ -208,14 +175,11 @@ function getRevealConfig(imgIdx) {
}
.nine-grid-img:hover { transform: translateZ(0) scale(1.03); }
/* 错落双图:高度随视口缩放并封顶,保证标题+图片总高始终落在页高预算内 */
.overlap-wrap { height: clamp(320px, 54vh, 460px); }
/* 瀑布流 */
.g-masonry { column-count: 2; column-gap: 10px; }
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; }
/* 轮播 */
.g-carousel-track {
display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
border-radius: 10px; scrollbar-width: none;
@@ -235,7 +199,6 @@ function getRevealConfig(imgIdx) {
.g-dot { width: 7px; height: 7px; border-radius: 9999px; border: none; background: rgba(168,140,107,0.3); padding: 0; cursor: pointer; transition: all 0.3s; }
.g-dot.active { background: #a88c6b; width: 18px; border-radius: 9999px; }
/* 拍立得 */
.g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; }
.g-polaroid-item {
width: 42%; background: #fff; padding: 8px 8px 24px; border-radius: 2px;

View File

@@ -1,60 +0,0 @@
import { onMounted, onUnmounted } from 'vue'
/**
* 提前约 1.5 屏预热区块内图片解码,避免滚入视口时与 free-scroll rAF 撞车。
* onBusyChange(true/false) 供父级短暂降低 canvas 粒子负载。
*/
export function useImageWarmup(rootRef, { onBusyChange } = {}) {
let observer = null
let busy = false
let warmToken = 0
const setBusy = (next) => {
if (busy === next) return
busy = next
onBusyChange?.(next)
}
const warmImages = async (el) => {
const token = ++warmToken
setBusy(true)
const imgs = el.querySelectorAll('img[src]')
await Promise.allSettled(
[...imgs].map((img) => {
if (typeof img.decode === 'function') {
return img.decode().catch(() => {})
}
if (img.complete) return Promise.resolve()
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true })
img.addEventListener('error', resolve, { once: true })
})
})
)
if (token === warmToken) setBusy(false)
}
onMounted(() => {
const el = rootRef.value
if (!el || typeof IntersectionObserver === 'undefined') return
const scrollRoot = el.closest('.scrollable-area-body')
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) warmImages(el)
else {
warmToken += 1
setBusy(false)
}
},
{ root: scrollRoot, rootMargin: '150% 0px', threshold: 0 }
)
observer.observe(el)
})
onUnmounted(() => {
warmToken += 1
observer?.disconnect()
observer = null
setBusy(false)
})
}

View File

@@ -17,9 +17,6 @@ const VARIANTS = {
blur: 'reveal-blur',
}
/** 同一滚动根只解析一次,避免每个 v-reveal 挂载都 getComputedStyle 爬树 */
let cachedScrollRoot = undefined
function parseBinding(value) {
if (!value) return { variant: 'up', delay: 0 }
if (typeof value === 'string') return { variant: value, delay: 0 }
@@ -29,36 +26,6 @@ function parseBinding(value) {
}
}
function findScrollRoot(el) {
// 请柬主滚动容器(避免被轮播 overflow-x 等内层滚动误匹配)
const named = el.closest?.('.scrollable-area-body')
if (named) return named
let parent = el.parentElement
while (parent && parent !== document.body) {
const style = window.getComputedStyle(parent)
const overflowY = style.overflowY
if (overflowY === 'auto' || overflowY === 'scroll') {
return parent
}
parent = parent.parentElement
}
return null
}
function getScrollRoot(el) {
if (cachedScrollRoot !== undefined) return cachedScrollRoot
cachedScrollRoot = findScrollRoot(el)
return cachedScrollRoot
}
function clearWillChange(el) {
el.style.willChange = 'auto'
}
function hasImageContent(el) {
return el.tagName === 'IMG' || !!el.querySelector?.('img')
}
const reveal = {
mounted(el, binding) {
const { variant, delay } = parseBinding(binding.value)
@@ -66,37 +33,15 @@ const reveal = {
el.classList.add(VARIANTS[variant] || 'reveal-up')
if (delay) el.style.transitionDelay = `${delay}ms`
// 含图片的节点不做 filter 过渡(大图 + blur 极贵),保留 opacity/transform
const imageHost = hasImageContent(el)
if (imageHost) {
el.classList.add('reveal--no-filter')
if (variant === 'blur') {
el.classList.remove('reveal-blur')
el.classList.add('reveal-scale')
}
}
// 已处于视口(如首屏)时立即显示,避免永远不可见
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.style.willChange = imageHost ? 'opacity, transform' : 'opacity, transform, filter'
el.classList.add('reveal--in')
observer.unobserve(el)
const onEnd = (e) => {
if (e.target !== el) return
clearWillChange(el)
el.removeEventListener('transitionend', onEnd)
}
el.addEventListener('transitionend', onEnd)
// 兜底:无 transition 或已被 reduce-motion 关掉时仍清 will-change
setTimeout(() => {
clearWillChange(el)
el.removeEventListener('transitionend', onEnd)
}, 1400 + (delay || 0))
}
},
{ threshold: 0.12, root: getScrollRoot(el), rootMargin: '0px 0px -8% 0px' }
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
)
observer.observe(el)
el.__revealObserver = observer
@@ -106,7 +51,6 @@ const reveal = {
el.__revealObserver.disconnect()
delete el.__revealObserver
}
clearWillChange(el)
},
}

View File

@@ -10,6 +10,7 @@
opacity 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
filter 1.1s ease;
will-change: opacity, transform, filter;
}
.reveal-up { transform: translateY(48px); }
.reveal-down { transform: translateY(-48px); }
@@ -20,15 +21,6 @@
.reveal--in { opacity: 1; transform: none; filter: none; }
/* 图片宿主:不做 filter 过渡,避免大图 decode + blur 叠峰 */
.reveal--no-filter {
transition:
opacity 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
filter: none !important;
}
.reveal--no-filter.reveal--in { filter: none !important; }
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; }
}
@@ -85,8 +77,7 @@
/* ---------- 背景柔光层(淡金/淡粉光斑,缓慢漂移,不喧宾夺主) ---------- */
.bg-glow { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
/* 柔光靠 radial-gradient 自身衰减,不再叠 CSS filter:blur滚动时绘制成本很高 */
.bg-glow i { position: absolute; border-radius: 50%; }
.bg-glow i { position: absolute; border-radius: 50%; filter: blur(46px); will-change: transform; }
.bg-glow .g1 {
width: 300px; height: 300px; top: -90px; right: -70px;
background: radial-gradient(circle, rgba(233, 206, 168, 0.5), transparent 68%);

View File

@@ -1,5 +1,5 @@
<template>
<div class="wrapper relative w-full h-[100vh] bg-[#Fdfbf7] overflow-hidden text-[#2c2c2c]">
<div class="wrapper relative w-full min-h-[100vh] bg-[#Fdfbf7] overflow-x-hidden text-[#2c2c2c]">
<!-- 入场揭幕帘幕请柬封面 -->
<div v-show="showIntro" class="curtain-root" :class="[{ 'is-opening': introOpening }, introExitClass]" @click="openInvitation">
@@ -34,14 +34,13 @@
:petal-enabled="data.petalEnabled"
:bubble-enabled="data.bubbleEnabled"
:active="effectsActive"
:performance-mode="effectsPerformanceMode"
/>
<!-- 顶部滚动进度条 -->
<div ref="progressBarRef" class="absolute top-0 left-0 w-full h-[2px] bg-[#a88c6b] z-[55] origin-left transform-gpu scale-x-0"></div>
<div class="fixed top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
<!-- 右上角控制台 -->
<div class="absolute top-6 right-5 z-50 flex flex-col items-end gap-4">
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
<!-- 音乐控制 -->
<div class="music-wrap">
<div class="flex flex-col gap-3 items-end">
@@ -75,9 +74,7 @@
<!-- 主滚动区域注意不要加 scroll-smooth它会劫持 JS 滚动赋值导致卡顿 -->
<div ref="scrollContainerRef"
class="w-full h-full overflow-y-auto no-scrollbar relative scrollable-area-body"
:class="data.scrollMode === 'snap' ? 'snap-y snap-mandatory' : ''"
@scroll="handleScroll"
class="w-full min-h-[100vh] no-scrollbar relative"
@touchstart="handleUserInteraction" @touchmove="handleUserInteraction"
@wheel="handleUserInteraction" @mousedown="handleUserInteraction">
@@ -132,12 +129,12 @@
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
<PhotoGallery :page="page" @preview-image="openImagePreview" @layout-change="scheduleScrollMetricsUpdateDebounced" @warmup-change="onGalleryWarmupChange" />
<PhotoGallery :page="page" @preview-image="openImagePreview" />
</div>
</div>
<!-- 婚礼日历 -->
<div ref="calendarPageRef" class="page-section wedding-calendar-page px-6 py-10 relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
<div class="page-section wedding-calendar-page px-6 py-10 relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
<div v-reveal="'up'" class="calendar-card">
<div class="calendar-kicker">Save The Date</div>
<div class="calendar-title">
@@ -160,12 +157,12 @@
<span v-if="day === weddingDay" class="heart-ring" aria-hidden="true">
<svg viewBox="0 0 120 96" role="img">
<defs>
<filter v-if="calendarRoughActive" id="calendar-heart-rough" x="-18%" y="-18%" width="136%" height="136%">
<filter id="calendar-heart-rough" x="-18%" y="-18%" width="136%" height="136%">
<feTurbulence type="fractalNoise" baseFrequency="0.045" numOctaves="3" seed="11" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="2.8" />
</filter>
</defs>
<g :filter="calendarRoughActive ? 'url(#calendar-heart-rough)' : undefined">
<g filter="url(#calendar-heart-rough)">
<path class="heart-brush heart-brush-shadow" d="M54 82 C31 70 12 50 13 29 C14 10 35 7 49 27 C56 37 58 49 59 56 C64 37 80 10 99 20 C113 28 105 51 90 66 C80 76 70 81 64 83" />
<path class="heart-brush heart-brush-main" d="M54 82 C31 70 12 50 13 29 C14 10 35 7 49 27 C56 37 58 49 59 56 C64 37 80 10 99 20 C113 28 105 51 90 66 C80 76 70 81 64 83" />
<path class="heart-brush heart-brush-tail" d="M94 61 C87 62 82 58 78 52" />
@@ -226,7 +223,7 @@
<span>With Love</span>
<i></i>
</div>
<div ref="endingPhotoRef" v-reveal="{ variant: 'scale', delay: 100 }" class="ending-photo">
<div v-reveal="{ variant: 'scale', delay: 100 }" class="ending-photo">
<img :src="endImage" class="w-full h-full bg-[#F7F6F2] object-cover" loading="lazy" decoding="async" alt="婚礼尾页照片" />
</div>
<div v-reveal="{ variant: 'up', delay: 180 }" class="ending-copy">
@@ -313,7 +310,6 @@
import { ref, reactive, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { getAlbumImages, getConfig, submitRsvp } from '@/api/wedding'
import { useAudio } from '@/composables/useAudio'
import { useImageWarmup } from '@/composables/useImageWarmup'
import CanvasEffects from '@/components/CanvasEffects.vue'
import PhotoGallery from '@/components/PhotoGallery.vue'
@@ -332,38 +328,25 @@ const data = ref({"date": "2026年9月4日 星期五", "bride": "李逸烁", "gr
const audio = useAudio()
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)
const isInteracting = ref(false), scrollContainerRef = ref(null), progressBarRef = ref(null), progress = ref(0)
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0)
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMapOptions = ref(false)
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
const albumPreviewImages = ref([])
const maxScrollTop = ref(0)
const calendarPageRef = ref(null)
const calendarRoughActive = ref(false)
const endingPhotoRef = ref(null)
const galleryWarmupCount = ref(0)
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: '遗憾缺席' } ]
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null, metricsRaf = null
let metricsDebounceTimer = null
let calendarRoughObserver = null
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
let freeScrollLastTs = 0
let freeScrollCarry = 0
let programmaticScroll = false
let progressPaintTs = 0
const BOTTOM_RETURN_DELAY = 30000
const FREE_SCROLL_BASE_STEP = 1.2
const PROGRESS_PAINT_INTERVAL = 1000 / 12
const METRICS_DEBOUNCE_MS = 120
// 后台可配置的滚动参数(带默认值兜底,防止脏数据)
const freeInterval = computed(() => {
const v = Number(data.value.freeScrollInterval)
return v >= 10 && v <= 1000 ? v : 50
})
const freeScrollSignature = computed(() => `${data.value.scrollMode}:${freeInterval.value}`)
const restartAutoScroll = () => {
if (isAutoScroll.value) startAutoScroll()
if (snapTimer || rafId) startAutoScroll()
}
const switchDuration = computed(() => {
const v = Number(data.value.pageSwitchDuration)
@@ -413,7 +396,6 @@ const calendarDays = computed(() => {
return [...cells, ...Array.from({ length: trailing }, () => null)]
})
// 页面高度:支持 100vh / 80vh / 60vh / auto / 自定义 vh
const sectionHeightStyle = (height) => {
const h = height || '100vh'
return h === 'auto' ? { minHeight: 'auto' } : { minHeight: h }
@@ -459,14 +441,9 @@ const handlePreviewVisibleChange = (visible) => {
if (visible) {
clearBottomReturn()
cancelAnimationFrame(animId)
animId = null
stopFreeScrollLoop()
return
}
freeScrollLastTs = 0
freeScrollCarry = 0
if (isAutoScroll.value && isAtBottom()) scheduleBottomReturn(true)
else maybeResumeFreeScroll()
}
const albumPreviewOptions = computed(() => ({
@@ -487,127 +464,34 @@ const openImagePreview = async (src) => {
}
previewIndex.value = index
previewVisible.value = true
freeScrollCarry = 0
clearBottomReturn()
cancelAnimationFrame(animId)
animId = null
stopFreeScrollLoop()
}
const isAutoScrollBlocked = () =>
isInteracting.value || isDrawerOpen.value || previewVisible.value || showMapOptions.value || showIntro.value
const effectsActive = computed(() => !isDrawerOpen.value && !previewVisible.value && !showMapOptions.value && !showIntro.value)
const effectsPerformanceMode = computed(() =>
(data.value.scrollMode === 'free' && isAutoScroll.value) || galleryWarmupCount.value > 0
const effectsActive = computed(() =>
!isDrawerOpen.value && !previewVisible.value && !showMapOptions.value && !showIntro.value
)
const onGalleryWarmupChange = (busy) => {
galleryWarmupCount.value = Math.max(0, galleryWarmupCount.value + (busy ? 1 : -1))
}
useImageWarmup(endingPhotoRef, { onBusyChange: onGalleryWarmupChange })
const updateScrollMetrics = () => {
const el = scrollContainerRef.value
if (!el) return 0
const max = Math.max(0, el.scrollHeight - el.clientHeight)
maxScrollTop.value = max
return max
}
const paintProgressBar = (scrollTop, force = false) => {
const now = performance.now()
if (!force && now - progressPaintTs < PROGRESS_PAINT_INTERVAL) return
progressPaintTs = now
const max = maxScrollTop.value
const p = max > 0 ? Math.min(1, scrollTop / max) : 0
progress.value = p
if (progressBarRef.value) progressBarRef.value.style.transform = `scaleX(${p})`
}
/** 程序化改 scrollTop避免同步 @scroll 再走布局/响应式热路径 */
const setProgrammaticScrollTop = (el, top) => {
programmaticScroll = true
el.scrollTop = top
programmaticScroll = false
paintProgressBar(top)
}
const applyScrollSideEffects = () => {
const el = scrollContainerRef.value
if (!el) return
paintProgressBar(el.scrollTop, true)
const closeDrawer = () => { isDrawerOpen.value = false }
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
const pageScrollMax = () => Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
const handleScroll = () => {
const max = pageScrollMax()
progress.value = max > 0 ? pageScrollTop() / max : 0
if (isAtBottom()) scheduleBottomReturn()
else clearBottomReturn()
}
const flushScrollMetrics = () => {
cancelAnimationFrame(metricsRaf)
metricsRaf = requestAnimationFrame(() => {
metricsRaf = null
updateScrollMetrics()
applyScrollSideEffects()
})
}
const scheduleScrollMetricsUpdate = () => {
clearTimeout(metricsDebounceTimer)
metricsDebounceTimer = null
flushScrollMetrics()
}
const scheduleScrollMetricsUpdateDebounced = () => {
clearTimeout(metricsDebounceTimer)
metricsDebounceTimer = setTimeout(() => {
metricsDebounceTimer = null
flushScrollMetrics()
}, METRICS_DEBOUNCE_MS)
}
const closeDrawer = () => { isDrawerOpen.value = false }
const handleScroll = () => {
if (programmaticScroll) return
const el = scrollContainerRef.value
if (!el) return
if (maxScrollTop.value <= 0) updateScrollMetrics()
applyScrollSideEffects()
}
const stopFreeScrollLoop = () => {
cancelAnimationFrame(rafId)
rafId = null
}
const maybeResumeFreeScroll = () => {
if (data.value.scrollMode !== 'free') return
if (!isAutoScroll.value || isAutoScrollBlocked() || rafId || bottomReturnTimer) return
startAutoScroll()
}
const handleUserInteraction = () => {
isInteracting.value = true
// 用户接管时立即取消程序化的翻页动画,避免与人争抢滚动
cancelAnimationFrame(animId)
animId = null
stopFreeScrollLoop()
clearTimeout(pauseTimer)
if (isAtBottom()) scheduleBottomReturn(true)
pauseTimer = setTimeout(() => {
isInteracting.value = false
maybeResumeFreeScroll()
}, 3000)
pauseTimer = setTimeout(() => (isInteracting.value = false), 3000)
}
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
const isAtBottom = () => {
const el = scrollContainerRef.value
if (!el) return false
let max = maxScrollTop.value
if (max <= 0) max = updateScrollMetrics()
return max - el.scrollTop <= 8
}
const isAtBottom = () => pageScrollMax() - pageScrollTop() <= 8
const clearBottomReturn = () => {
clearTimeout(bottomReturnTimer)
@@ -615,14 +499,12 @@ const clearBottomReturn = () => {
}
const scheduleBottomReturn = (reset = false) => {
const el = scrollContainerRef.value
if (!el || !isAtBottom() || !isAutoScroll.value || isAutoScrollBlocked()) return
if (!isAtBottom() || !isAutoScroll.value || previewVisible.value || showMapOptions.value) return
if (bottomReturnTimer && !reset) return
clearBottomReturn()
stopFreeScrollLoop()
bottomReturnTimer = setTimeout(() => {
bottomReturnTimer = null
if (!isAtBottom() || !isAutoScroll.value || isAutoScrollBlocked()) return
if (!isAtBottom() || !isAutoScroll.value || isDrawerOpen.value || previewVisible.value) return
if (isInteracting.value) {
scheduleBottomReturn(true)
return
@@ -631,45 +513,23 @@ const scheduleBottomReturn = (reset = false) => {
}, BOTTOM_RETURN_DELAY)
}
const onProgrammaticScrollDone = () => {
applyScrollSideEffects()
maybeResumeFreeScroll()
}
// 带时长的平滑滚动easeInOutQuadduration 由后台 pageSwitchDuration 控制
const smoothScrollTo = (target, duration) => {
const el = scrollContainerRef.value
if (!el) return
cancelAnimationFrame(animId)
animId = null
const start = el.scrollTop, dist = target - start
if (!dist) {
onProgrammaticScrollDone()
return
}
if (duration <= 0) {
setProgrammaticScrollTop(el, target)
onProgrammaticScrollDone()
return
}
const start = pageScrollTop(), dist = target - start
if (!dist) return
if (duration <= 0) { window.scrollTo(0, target); return }
const t0 = performance.now()
const tick = (now) => {
const p = Math.min(1, (now - t0) / duration)
const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2
setProgrammaticScrollTop(el, start + dist * e)
window.scrollTo(0, start + dist * e)
if (p < 1) animId = requestAnimationFrame(tick)
else {
animId = null
onProgrammaticScrollDone()
}
}
animId = requestAnimationFrame(tick)
}
const goNextSection = () => {
const el = scrollContainerRef.value
if (!el) return
const top = el.scrollTop
const top = pageScrollTop()
const next = sectionEls().find((e) => e.offsetTop > top + 12)
if (next) {
smoothScrollTo(next.offsetTop, switchDuration.value)
@@ -682,52 +542,34 @@ const startAutoScroll = () => {
clearTimeout(scrollStartTimer)
clearInterval(snapTimer)
clearInterval(freeTimer)
stopFreeScrollLoop()
cancelAnimationFrame(rafId)
rafId = null
if (data.value.scrollMode === 'snap') {
if (isAutoScroll.value && !isAutoScrollBlocked()) goNextSection()
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value) goNextSection()
snapTimer = setInterval(() => {
if (isAutoScroll.value && !isAutoScrollBlocked()) goNextSection()
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value) goNextSection()
}, 4500)
return
}
if (!isAutoScroll.value || isAutoScrollBlocked() || bottomReturnTimer) return
if (maxScrollTop.value <= 0) updateScrollMetrics()
freeScrollLastTs = 0
freeScrollCarry = 0
const tickFreeScroll = (now) => {
const el = scrollContainerRef.value
if (!el || !isAutoScroll.value || isAutoScrollBlocked()) {
rafId = null
return
}
const dt = freeScrollLastTs ? Math.min(24, now - freeScrollLastTs) : 16.7
freeScrollLastTs = now
const max = maxScrollTop.value
if (max <= 0) {
rafId = requestAnimationFrame(tickFreeScroll)
return
}
if (max - el.scrollTop <= 8) {
scheduleBottomReturn()
rafId = null
return
}
freeScrollCarry += FREE_SCROLL_BASE_STEP * (dt / freeInterval.value)
if (freeScrollCarry >= 0.35) {
const currentTop = el.scrollTop
const nextTop = Math.min(max, currentTop + freeScrollCarry)
setProgrammaticScrollTop(el, nextTop)
freeScrollCarry = Math.max(0, freeScrollCarry - (nextTop - currentTop))
if (freeScrollCarry > 4) freeScrollCarry = 0
if (max - nextTop <= 8) {
scheduleBottomReturn()
rafId = null
return
} else {
freeScrollLastTs = 0
const tickFreeScroll = (now) => {
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
freeScrollLastTs = now
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value && !showMapOptions.value) {
if (isAtBottom()) {
scheduleBottomReturn()
} else {
window.scrollTo(0, pageScrollTop() + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
if (isAtBottom()) scheduleBottomReturn()
}
}
rafId = requestAnimationFrame(tickFreeScroll)
}
rafId = requestAnimationFrame(tickFreeScroll)
}
rafId = requestAnimationFrame(tickFreeScroll)
}
const syncPageScrollMode = () => {
document.documentElement.classList.toggle('page-scroll-snap', data.value.scrollMode === 'snap')
}
const scheduleAutoScrollStart = () => {
@@ -744,30 +586,20 @@ const completeIntro = () => {
const toggleAutoScroll = () => {
isAutoScroll.value = !isAutoScroll.value
if (!isAutoScroll.value) {
clearBottomReturn()
stopFreeScrollLoop()
cancelAnimationFrame(animId)
animId = null
return
}
if (isAtBottom()) scheduleBottomReturn(true)
else startAutoScroll()
if (isAutoScroll.value && isAtBottom()) scheduleBottomReturn(true)
if (!isAutoScroll.value) clearBottomReturn()
}
// 入场揭幕:开门动画
const openInvitation = () => {
if (introOpening.value) return
clearTimeout(introTimer)
introOpening.value = true
introTimer = setTimeout(() => completeIntro(), introExitDuration.value)
// 点击揭幕是明确的用户手势:直接尝试播放背景音乐(最可靠的自动播放路径)
if (data.value.musicList.length) audio.play()
}
const selectTrack = (url) => { audio.setActive(url); showTrackList.value = false }
const mapKeyword = computed(() => `${data.value.hotel || ''} ${data.value.address || ''}`.replace(/\s+/g, ' ').trim())
const isMobileDevice = () => /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent)
const isIOSDevice = () => /iPhone|iPad|iPod/i.test(navigator.userAgent)
@@ -816,11 +648,13 @@ const handleSubmitRsvp = async () => {
}
onMounted(async () => {
if ('scrollRestoration' in history) history.scrollRestoration = 'manual'
window.scrollTo(0, 0)
try {
const res = await getConfig()
if (res.data && res.data !== '{}') {
const loaded = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
// 兼容旧版单链接 musicUrl
if (loaded.musicUrl && !(loaded.musicList && loaded.musicList.length)) {
loaded.musicList = [{ url: loaded.musicUrl, name: '背景音乐' }]
loaded.activeMusicUrl = loaded.musicUrl
@@ -836,19 +670,9 @@ onMounted(async () => {
}
} catch (e) { /* 使用默认配置 */ }
syncPageScrollMode()
albumLoaded.value = false
albumPreviewImages.value = localAlbumImages()
await nextTick()
scheduleScrollMetricsUpdate()
window.addEventListener('resize', scheduleScrollMetricsUpdate, { passive: true })
if (calendarPageRef.value) {
calendarRoughObserver = new IntersectionObserver(
([entry]) => { calendarRoughActive.value = !!entry?.isIntersecting },
{ root: scrollContainerRef.value, rootMargin: '20% 0px', threshold: 0.01 }
)
calendarRoughObserver.observe(calendarPageRef.value)
}
if (data.value.introEnabled) showIntro.value = true
@@ -864,33 +688,36 @@ onMounted(async () => {
scheduleAutoScrollStart()
}
scheduleScrollMetricsUpdate()
await nextTick()
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
})
onUnmounted(() => {
stopFreeScrollLoop()
document.documentElement.classList.remove('page-scroll-snap')
window.removeEventListener('scroll', handleScroll)
cancelAnimationFrame(rafId)
cancelAnimationFrame(animId)
cancelAnimationFrame(metricsRaf)
clearTimeout(metricsDebounceTimer)
clearInterval(snapTimer)
clearInterval(freeTimer)
clearTimeout(pauseTimer)
clearTimeout(introTimer)
clearTimeout(scrollStartTimer)
clearBottomReturn()
calendarRoughObserver?.disconnect()
calendarRoughObserver = null
window.removeEventListener('resize', scheduleScrollMetricsUpdate)
})
watch(() => data.value.scrollMode, syncPageScrollMode)
watch(freeScrollSignature, restartAutoScroll)
watch(
() => [isDrawerOpen.value, previewVisible.value, showMapOptions.value],
() => {
if (isAutoScrollBlocked()) stopFreeScrollLoop()
else maybeResumeFreeScroll()
if (isDrawerOpen.value || previewVisible.value || showMapOptions.value) {
cancelAnimationFrame(animId)
clearBottomReturn()
}
}
)
</script>
<style scoped>
@@ -902,16 +729,11 @@ watch(
.album-preview-registry {
display: none;
}
.wrapper .scrollable-area-body {
width: 100%;
box-sizing: border-box;
:global(html.page-scroll-snap) {
scroll-snap-type: y mandatory;
}
@media (min-width: 1024px) {
.wrapper .scrollable-area-body {
max-width: 1200px;
margin: 0 auto;
}
:global(html.page-scroll-snap body) {
scroll-snap-type: y mandatory;
}
.wrapper .font-serif { font-family: var(--font-serif); }
.wrapper .font-sans { font-family: var(--font-sans); }