更新页面效果
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.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}
|
||||
@@ -0,0 +1 @@
|
||||
import{x as k,d as y,e as t,q as a,y as o,z as n,A as z,B as A,b as q,k as c,p as B,r as C,C as p}from"./index-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};
|
||||
@@ -0,0 +1 @@
|
||||
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
514
vite-tailwindcss/dist-chk-1785466613/assets/index-CZ-hSlJt.js
Normal file
514
vite-tailwindcss/dist-chk-1785466613/assets/index-CZ-hSlJt.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
14
vite-tailwindcss/dist-chk-1785466613/index.html
Normal file
14
vite-tailwindcss/dist-chk-1785466613/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>婚礼请柬 · Wedding Invitation</title>
|
||||
<script type="module" crossorigin src="/assets/index-CZ-hSlJt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bh11Omyp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
1
vite-tailwindcss/dist-chk-1785466613/vite.svg
Normal file
1
vite-tailwindcss/dist-chk-1785466613/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -7,18 +7,18 @@
|
||||
<span class="text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 单图(满幅带留白卡纸) -->
|
||||
<!-- 单图(默认居中卡幅 / 可配置宽度铺满) -->
|
||||
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
|
||||
class="w-full max-w-[300px] mx-auto aspect-[4/5] mt-6">
|
||||
<div :class="frameClass"><img :src="page.img1" /></div>
|
||||
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
|
||||
<div :class="singleFrameClass"><img :src="page.img1" /></div>
|
||||
</div>
|
||||
|
||||
<!-- 错落双图 -->
|
||||
<div v-else-if="page.type === 'overlap'" class="relative w-full h-[50vh] mt-4">
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }" class="absolute top-0 right-4 w-[70%] h-[65%] z-10" :class="frameClass">
|
||||
<!-- 错落双图(高度 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 top-0 right-0 w-[68%] h-[72%] z-10" :class="frameClass">
|
||||
<img :src="page.img1" />
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-4 left-4 w-[60%] h-[60%] z-20" :class="frameClass">
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-0 left-0 w-[60%] h-[60%] z-20" :class="frameClass">
|
||||
<img :src="page.img2" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,6 +89,23 @@ const props = defineProps({
|
||||
// 边框样式:前台按 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') {
|
||||
// 铺满 + 保持原始比例 → 高度由图片自然撑开;铺满 + 不保持 → 固定 4:5 裁切
|
||||
return props.page.singleKeepRatio ? {} : { 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' : '')
|
||||
)
|
||||
|
||||
/* ---------- 轮播状态 ---------- */
|
||||
const index = ref(0)
|
||||
const trackRef = ref(null)
|
||||
@@ -120,6 +137,9 @@ function polaroidTransform(i) {
|
||||
<style scoped>
|
||||
.gallery { width: 100%; }
|
||||
|
||||
/* 错落双图:高度随视口缩放并封顶,保证标题+图片总高始终落在页高预算内 */
|
||||
.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; }
|
||||
|
||||
@@ -6,6 +6,11 @@ export const BORDER_STYLES = [
|
||||
{ value: 'rounded', label: '柔圆角' },
|
||||
{ value: 'double', label: '双线框' },
|
||||
{ value: 'gold', label: '古典金框' },
|
||||
{ value: 'shadow', label: '阴影浮起' },
|
||||
{ value: 'ink', label: '墨黑描边' },
|
||||
{ value: 'card', label: '悬浮卡片' },
|
||||
{ value: 'lace', label: '虚线文艺' },
|
||||
{ value: 'glow', label: '柔光金晕' },
|
||||
]
|
||||
|
||||
export function borderClass(style) {
|
||||
|
||||
@@ -26,44 +26,76 @@
|
||||
}
|
||||
|
||||
/* ---------- 飘落花瓣(优雅:柔和渐变 + 轻微摇曳) ---------- */
|
||||
/* 性能约定:摇曳动画一律作用在内层元素的 transform 上,
|
||||
外层只跑位移动画 —— 全程合成层处理,不触发布局。 */
|
||||
@keyframes petalFall {
|
||||
0% { transform: translate3d(0, -12vh, 0) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 0.85; }
|
||||
90% { opacity: 0.7; }
|
||||
10% { opacity: 0.8; }
|
||||
90% { opacity: 0.65; }
|
||||
100% { transform: translate3d(6vw, 112vh, 0) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
@keyframes petalDrift {
|
||||
0%, 100% { margin-left: -14px; }
|
||||
50% { margin-left: 14px; }
|
||||
0%, 100% { transform: translateX(-12px) rotate(-7deg); }
|
||||
50% { transform: translateX(12px) rotate(7deg); }
|
||||
}
|
||||
.petal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
animation:
|
||||
petalFall var(--p-dur, 12s) linear infinite,
|
||||
petalDrift var(--p-sway, 6s) ease-in-out infinite alternate;
|
||||
animation: petalFall var(--p-dur, 12s) linear infinite;
|
||||
}
|
||||
.petal-inner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
animation: petalDrift var(--p-sway, 6s) ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* ---------- 漂浮爱心(全屏自然上升,随机分布) ---------- */
|
||||
/* ---------- 漂浮爱心(全屏自然上升,三层景深) ---------- */
|
||||
@keyframes heartFloat {
|
||||
0% { transform: translateY(20px) translateX(0) scale(0.7); opacity: 0; }
|
||||
12% { opacity: 0.8; }
|
||||
85% { opacity: 0.6; }
|
||||
100% { transform: translateY(-118vh) translateX(24px) scale(1.1); opacity: 0; }
|
||||
0% { transform: translate3d(0, 20px, 0) scale(0.7); opacity: 0; }
|
||||
12% { opacity: var(--h-opacity, 0.7); }
|
||||
85% { opacity: calc(var(--h-opacity, 0.7) * 0.75); }
|
||||
100% { transform: translate3d(24px, -118vh, 0) scale(1.08); opacity: 0; }
|
||||
}
|
||||
@keyframes heartSway {
|
||||
0%, 100% { margin-left: -16px; }
|
||||
50% { margin-left: 16px; }
|
||||
0%, 100% { transform: translateX(-14px) scale(0.97); }
|
||||
50% { transform: translateX(14px) scale(1.03); }
|
||||
}
|
||||
.heart-bubble {
|
||||
position: absolute;
|
||||
will-change: transform, opacity;
|
||||
animation:
|
||||
heartFloat var(--float-duration, 18s) linear infinite,
|
||||
heartSway var(--sway-duration, 5s) ease-in-out infinite alternate;
|
||||
animation: heartFloat var(--float-duration, 18s) linear infinite;
|
||||
}
|
||||
.heart-inner {
|
||||
display: inline-block;
|
||||
will-change: transform;
|
||||
animation: heartSway var(--sway-duration, 5s) ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* ---------- 背景柔光层(淡金/淡粉光斑,缓慢漂移,不喧宾夺主) ---------- */
|
||||
.bg-glow { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
|
||||
.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%);
|
||||
animation: glowDrift1 17s ease-in-out infinite alternate;
|
||||
}
|
||||
.bg-glow .g2 {
|
||||
width: 260px; height: 260px; bottom: -80px; left: -60px;
|
||||
background: radial-gradient(circle, rgba(244, 194, 194, 0.42), transparent 68%);
|
||||
animation: glowDrift2 21s ease-in-out infinite alternate;
|
||||
}
|
||||
.bg-glow .g3 {
|
||||
width: 210px; height: 210px; top: 38%; left: 58%;
|
||||
background: radial-gradient(circle, rgba(212, 175, 55, 0.16), transparent 70%);
|
||||
animation: glowDrift3 24s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes glowDrift1 { to { transform: translate(-34px, 26px) scale(1.08); } }
|
||||
@keyframes glowDrift2 { to { transform: translate(30px, -24px) scale(1.06); } }
|
||||
@keyframes glowDrift3 { to { transform: translate(-26px, 20px) scale(1.12); } }
|
||||
|
||||
/* ---------- 心跳脉冲 ---------- */
|
||||
@keyframes pulseHeart {
|
||||
|
||||
@@ -69,3 +69,57 @@
|
||||
box-shadow: 0 0 0 4px #fff, 0 0 0 5px #e7d6bb, 0 8px 22px rgba(120, 90, 60, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 阴影浮起(无边框,大柔影悬浮感) */
|
||||
.gf-shadow {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 18px 44px rgba(96, 72, 50, 0.2), 0 4px 12px rgba(96, 72, 50, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 墨黑描边(简约深色细框) */
|
||||
.gf-ink {
|
||||
padding: 6px;
|
||||
border: 1.5px solid #3a3733;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 悬浮卡片(厚白边 + 大圆角 + 层叠影) */
|
||||
.gf-card {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(168, 140, 107, 0.18);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 6px rgba(120, 90, 60, 0.08), 0 16px 36px rgba(120, 90, 60, 0.16);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 虚线文艺(dashed 描边 + 内白边) */
|
||||
.gf-lace {
|
||||
padding: 7px;
|
||||
border: 1.5px dashed #b89a78;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 12px rgba(120, 90, 60, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 柔光金晕(浅金边 + 外发光) */
|
||||
.gf-glow {
|
||||
padding: 5px;
|
||||
border: 1px solid #e7d3ae;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 3px rgba(231, 211, 174, 0.35), 0 0 26px rgba(201, 169, 120, 0.45), 0 6px 16px rgba(120, 90, 60, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 辅助:图片保持原始宽高比(单图铺满+保比例时使用,覆盖 gf-frame 的 height:100%) */
|
||||
.gf-natural {
|
||||
height: auto !important;
|
||||
}
|
||||
.gf-natural > img {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,22 @@
|
||||
</a-radio-group>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">整页模式会带来更有仪式感的逐屏切换效果。</div>
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="自由滚动速度(滚动间隔)">
|
||||
<div class="flex gap-3 items-center">
|
||||
<a-slider v-model:value="cfg.freeScrollInterval" :min="10" :max="500" class="flex-1" />
|
||||
<a-input-number v-model:value="cfg.freeScrollInterval" :min="10" :max="500" addon-after="ms" class="w-[120px]" />
|
||||
</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">间隔越小滚动越快,默认 50ms。仅在「自由滚动」模式下生效。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="页面切换速度(动画时长)">
|
||||
<div class="flex gap-3 items-center">
|
||||
<a-slider v-model:value="cfg.pageSwitchDuration" :min="200" :max="3000" :step="100" class="flex-1" />
|
||||
<a-input-number v-model:value="cfg.pageSwitchDuration" :min="200" :max="3000" :step="100" addon-after="ms" class="w-[120px]" />
|
||||
</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">整页/分屏模式下自动翻页的动画时长,默认 800ms。</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a-form-item label="飘落花瓣">
|
||||
<a-switch v-model:checked="cfg.petalEnabled" />
|
||||
@@ -185,6 +201,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单图版式的宽度设置(逐页配置) -->
|
||||
<div v-if="page.type==='single'" class="grid grid-cols-1 md:grid-cols-2 gap-x-6 items-start">
|
||||
<a-form-item label="图片宽度">
|
||||
<a-radio-group v-model:value="page.singleWidth">
|
||||
<a-radio value="default">默认(居中卡幅)</a-radio>
|
||||
<a-radio value="full">宽度铺满</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="page.singleWidth==='full'" label="保持原始宽高比">
|
||||
<a-switch v-model:checked="page.singleKeepRatio" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">开启:按图片原始比例显示;关闭:固定 4:5 比例裁切铺满。</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="小标题 (英文)">
|
||||
<a-input v-model:value="page.subtitle" />
|
||||
@@ -295,6 +324,7 @@ function defaultConfig() {
|
||||
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }],
|
||||
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
freeScrollInterval: 50, pageSwitchDuration: 800,
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
@@ -354,6 +384,9 @@ function ensurePageShape(p) {
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
p.borderStyle = p.borderStyle || 'mat'
|
||||
// 单图宽度设置(仅 single 版式使用,逐页配置)
|
||||
p.singleWidth = p.singleWidth === 'full' ? 'full' : 'default'
|
||||
if (typeof p.singleKeepRatio !== 'boolean') p.singleKeepRatio = true
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -482,6 +515,9 @@ onMounted(async () => {
|
||||
}
|
||||
Object.assign(cfg, loaded)
|
||||
cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape)
|
||||
// 数值型参数兜底(兼容旧配置)
|
||||
cfg.freeScrollInterval = Number(cfg.freeScrollInterval) || 50
|
||||
cfg.pageSwitchDuration = Number(cfg.pageSwitchDuration) || 800
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
if (adminStore.isAdmin) loadRsvp()
|
||||
|
||||
@@ -19,22 +19,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 飘落花瓣层 -->
|
||||
<!-- 背景柔光层(淡金/淡粉光斑,缓慢漂移) -->
|
||||
<div class="bg-glow" aria-hidden="true"><i class="g1"></i><i class="g2"></i><i class="g3"></i></div>
|
||||
|
||||
<!-- 飘落花瓣层(外层位移 + 内层摇曳,全 transform 不触发布局) -->
|
||||
<div v-if="data.petalEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="p in petals" :key="'p'+p.id" class="petal"
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, animationDelay: p.delay, '--p-dur': p.duration, '--p-sway': p.sway }">
|
||||
<svg viewBox="0 0 100 100" class="w-full h-full" :style="{ filter: 'drop-shadow(0 2px 3px rgba(150,110,90,0.12))' }">
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, animationDelay: p.delay, '--p-dur': p.duration }">
|
||||
<svg viewBox="0 0 100 100" class="petal-inner" :style="{ '--p-sway': p.sway, filter: p.filter }">
|
||||
<path d="M50 3 C28 26 20 62 50 97 C80 62 72 26 50 3 Z" :fill="p.color" opacity="0.9" />
|
||||
<path d="M50 9 C50 36 50 70 50 91" stroke="rgba(255,255,255,0.55)" stroke-width="1.4" fill="none" opacity="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 漂浮爱心层 -->
|
||||
<!-- 漂浮爱心层(三层景深:远小淡模糊 / 近大清晰) -->
|
||||
<div v-if="data.bubbleEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="b in bubbles" :key="'b'+b.id"
|
||||
class="heart-bubble flex items-center justify-center"
|
||||
:style="{ left: b.left, top: b.top, color: b.color, fontSize: b.size+'px', opacity: b.opacity, animationDelay: b.delay, '--float-duration': b.floatDuration, '--sway-duration': b.swayDuration }">♥</div>
|
||||
:style="{ left: b.left, top: b.top, color: b.color, fontSize: b.size+'px', animationDelay: b.delay, '--float-duration': b.floatDuration, '--h-opacity': b.opacity }">
|
||||
<span class="heart-inner" :style="{ '--sway-duration': b.swayDuration, filter: b.filter }">♥</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部滚动进度条 -->
|
||||
@@ -72,9 +77,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主滚动区域 -->
|
||||
<!-- 主滚动区域(注意:不要加 scroll-smooth,它会劫持 JS 滚动赋值导致卡顿) -->
|
||||
<div ref="scrollContainerRef"
|
||||
class="w-full h-full overflow-y-auto no-scrollbar relative scroll-smooth"
|
||||
class="w-full h-full overflow-y-auto no-scrollbar relative"
|
||||
:class="data.scrollMode === 'snap' ? 'snap-y snap-mandatory' : ''"
|
||||
@scroll="handleScroll" @touchstart="handleUserInteraction" @touchmove="handleUserInteraction"
|
||||
@wheel="handleUserInteraction" @mousedown="handleUserInteraction">
|
||||
@@ -223,6 +228,8 @@ import PhotoGallery from '@/components/PhotoGallery.vue'
|
||||
const DEFAULTS = {
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
musicList: [], activeMusicUrl: '',
|
||||
freeScrollInterval: 50, // 自由滚动速度:滚动间隔 ms(越小越快)
|
||||
pageSwitchDuration: 800, // 页面切换速度:翻页动画时长 ms
|
||||
}
|
||||
|
||||
const data = ref({
|
||||
@@ -247,22 +254,59 @@ const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(fal
|
||||
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
|
||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null
|
||||
|
||||
const loveColors = ['#a88c6b', '#e8a5b2', '#f4c2c2', '#d4af37']
|
||||
const petalColors = ['#f7d9e0', '#fbeee3', '#f3c6d3', '#efd9c4', '#f9e7d6']
|
||||
const bubbles = Array.from({ length: 15 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', top: Math.random() * 100 + '%',
|
||||
color: loveColors[i % 4], size: 13 + Math.random() * 10, opacity: 0.25 + Math.random() * 0.15,
|
||||
floatDuration: (14 + Math.random() * 10) + 's', swayDuration: (4 + Math.random() * 2) + 's',
|
||||
delay: '-' + (Math.random() * 18).toFixed(1) + 's',
|
||||
}))
|
||||
const petals = Array.from({ length: 18 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', size: 10 + Math.random() * 14,
|
||||
opacity: 0.4 + Math.random() * 0.4, color: petalColors[i % petalColors.length],
|
||||
duration: (9 + Math.random() * 8) + 's', delay: (Math.random() * 10) + 's',
|
||||
sway: (4 + Math.random() * 4) + 's',
|
||||
}))
|
||||
// 后台可配置的滚动参数(带默认值兜底,防止脏数据)
|
||||
const freeInterval = computed(() => {
|
||||
const v = Number(data.value.freeScrollInterval)
|
||||
return v >= 10 && v <= 1000 ? v : 50
|
||||
})
|
||||
const switchDuration = computed(() => {
|
||||
const v = Number(data.value.pageSwitchDuration)
|
||||
return v >= 0 && v <= 5000 ? v : 800
|
||||
})
|
||||
|
||||
// 装饰元素配色(柔化后的低饱和金粉系,与主题协调不抢眼)
|
||||
const loveColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
|
||||
const petalColors = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
|
||||
|
||||
// 爱心三层景深:远(小/淡/模糊) 中 近(大/清晰),营造纵深而不杂乱
|
||||
const heartTiers = [
|
||||
{ n: 5, size: [10, 13], opacity: [0.1, 0.16], blur: 1.6, dur: [20, 26] }, // 远景
|
||||
{ n: 4, size: [14, 17], opacity: [0.16, 0.24], blur: 0.7, dur: [16, 21] }, // 中景
|
||||
{ n: 3, size: [18, 22], opacity: [0.24, 0.32], blur: 0, dur: [13, 17] }, // 近景
|
||||
]
|
||||
let heartId = 0
|
||||
const bubbles = heartTiers.flatMap((t) =>
|
||||
Array.from({ length: t.n }).map(() => {
|
||||
const id = heartId++
|
||||
return {
|
||||
id,
|
||||
left: Math.random() * 100 + '%', top: Math.random() * 100 + '%',
|
||||
color: loveColors[id % loveColors.length],
|
||||
size: t.size[0] + Math.random() * (t.size[1] - t.size[0]),
|
||||
opacity: +(t.opacity[0] + Math.random() * (t.opacity[1] - t.opacity[0])).toFixed(2),
|
||||
filter: t.blur ? `blur(${t.blur}px)` : 'none',
|
||||
floatDuration: (t.dur[0] + Math.random() * (t.dur[1] - t.dur[0])).toFixed(1) + 's',
|
||||
swayDuration: (4.5 + Math.random() * 2.5).toFixed(1) + 's',
|
||||
delay: '-' + (Math.random() * 20).toFixed(1) + 's',
|
||||
}
|
||||
})
|
||||
)
|
||||
// 花瓣:两层景深,远处更小更淡带轻微模糊
|
||||
const petals = Array.from({ length: 16 }).map((_, i) => {
|
||||
const far = i % 3 === 0
|
||||
return {
|
||||
id: i, left: Math.random() * 100 + '%',
|
||||
size: far ? 8 + Math.random() * 7 : 13 + Math.random() * 11,
|
||||
opacity: far ? 0.3 + Math.random() * 0.15 : 0.42 + Math.random() * 0.3,
|
||||
color: petalColors[i % petalColors.length],
|
||||
filter: far ? 'blur(1.2px) drop-shadow(0 2px 3px rgba(150,110,90,0.08))' : 'drop-shadow(0 2px 3px rgba(150,110,90,0.12))',
|
||||
duration: (far ? 13 : 9) + Math.random() * 8 + 's',
|
||||
delay: (Math.random() * 10) + 's',
|
||||
sway: (4 + Math.random() * 4) + 's',
|
||||
}
|
||||
})
|
||||
|
||||
const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` }))
|
||||
|
||||
@@ -285,18 +329,37 @@ const handleScroll = () => {
|
||||
|
||||
const handleUserInteraction = () => {
|
||||
isInteracting.value = true
|
||||
// 用户接管时立即取消程序化的翻页动画,避免与人争抢滚动
|
||||
cancelAnimationFrame(animId)
|
||||
clearTimeout(pauseTimer)
|
||||
pauseTimer = setTimeout(() => (isInteracting.value = false), 3000)
|
||||
}
|
||||
|
||||
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
|
||||
|
||||
// 带时长的平滑滚动(easeInOutQuad),duration 由后台 pageSwitchDuration 控制
|
||||
const smoothScrollTo = (el, target, duration) => {
|
||||
cancelAnimationFrame(animId)
|
||||
const start = el.scrollTop, dist = target - start
|
||||
if (!dist) return
|
||||
if (duration <= 0) { el.scrollTop = 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
|
||||
el.scrollTop = start + dist * e
|
||||
if (p < 1) animId = requestAnimationFrame(tick)
|
||||
}
|
||||
animId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
const goNextSection = () => {
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
const top = el.scrollTop
|
||||
const next = sectionEls().find((e) => e.offsetTop > top + 12)
|
||||
if (next) el.scrollTo({ top: next.offsetTop, behavior: 'smooth' })
|
||||
else isAutoScroll.value = false
|
||||
// 循环逻辑:滚到最后一屏后回到第一屏继续轮播
|
||||
smoothScrollTo(el, next ? next.offsetTop : 0, switchDuration.value)
|
||||
}
|
||||
|
||||
const startAutoScroll = () => {
|
||||
@@ -305,15 +368,14 @@ const startAutoScroll = () => {
|
||||
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value) goNextSection()
|
||||
}, 4500)
|
||||
} else {
|
||||
const step = () => {
|
||||
// 自由滚动:setInterval 按后台配置的间隔匀速推进(间隔越小越快)
|
||||
freeTimer = setInterval(() => {
|
||||
const el = scrollContainerRef.value
|
||||
if (el && isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value) {
|
||||
el.scrollTop += 0.8
|
||||
if (el.scrollTop >= el.scrollHeight - el.clientHeight - 5) isAutoScroll.value = false
|
||||
el.scrollTop += 1.2
|
||||
if (el.scrollTop >= el.scrollHeight - el.clientHeight - 5) el.scrollTop = 0
|
||||
}
|
||||
rafId = requestAnimationFrame(step)
|
||||
}
|
||||
step()
|
||||
}, freeInterval.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +440,9 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(animId)
|
||||
clearInterval(snapTimer)
|
||||
clearInterval(freeTimer)
|
||||
clearTimeout(pauseTimer)
|
||||
clearTimeout(introTimer)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user