恢复滚动方案
This commit is contained in:
@@ -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
@@ -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};
|
||||
@@ -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};
|
||||
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
@@ -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>
|
||||
@@ -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
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
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
@@ -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>
|
||||
@@ -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 |
@@ -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
@@ -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};
|
||||
@@ -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};
|
||||
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
@@ -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>
|
||||
@@ -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
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
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
@@ -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>
|
||||
@@ -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
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
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
@@ -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>
|
||||
@@ -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.
@@ -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>
|
||||
|
||||
@@ -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-6(24px×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;
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// 带时长的平滑滚动(easeInOutQuad),duration 由后台 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); }
|
||||
|
||||
Reference in New Issue
Block a user