fix: 升级了一些视觉、各种单图相册效果、日历效果、多种模板任选

This commit is contained in:
2026-08-14 21:02:24 +08:00
parent 5e609d5ee4
commit 1914dad39a
22 changed files with 6929 additions and 330 deletions

View File

@@ -10,13 +10,18 @@ const props = defineProps({
bubbleEnabled: { type: Boolean, default: true },
/** 预览/抽屉等遮罩时关闭粒子绘制 */
active: { type: Boolean, default: true },
/** 主题 fx 色板注入:空则用默认暖彩 */
petalPalette: { type: Array, default: null },
heartPalette: { type: Array, default: null },
})
const canvasRef = ref(null)
const petals = []
const hearts = []
const petalColors = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
const heartColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
const DEFAULT_PETALS = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
const DEFAULT_HEARTS = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
const petalColorsOf = () => (props.petalPalette?.length ? props.petalPalette : DEFAULT_PETALS)
const heartColorsOf = () => (props.heartPalette?.length ? props.heartPalette : DEFAULT_HEARTS)
let ctx = null
let rafId = 0
@@ -39,7 +44,8 @@ function resetPetal(p, initial = false) {
p.rotation = rand(0, Math.PI * 2)
p.rotationSpeed = rand(-0.9, 0.9)
p.opacity = rand(0.28, 0.7)
p.color = petalColors[Math.floor(rand(0, petalColors.length))]
const pc = petalColorsOf()
p.color = pc[Math.floor(rand(0, pc.length))]
}
function resetHeart(h, initial = false) {
@@ -50,7 +56,8 @@ function resetHeart(h, initial = false) {
h.sway = rand(8, 26)
h.phase = rand(0, Math.PI * 2)
h.opacity = rand(0.1, 0.32)
h.color = heartColors[Math.floor(rand(0, heartColors.length))]
const hc = heartColorsOf()
h.color = hc[Math.floor(rand(0, hc.length))]
}
function seedParticles() {

View File

@@ -30,7 +30,7 @@ const GLOW_BLUR = isIOS ? 4 : 10
const GLOW_ALPHA = 0.55
const DPR_CAP = isIOS ? 1.5 : 2
/** 多色爱心:主色 */
/** 多色爱心:主色(可被主题 fx 色板覆盖) */
const HEART_COLORS = [
'#ff2442', '#ff2d55', '#ff4770', '#ff6b8a',
'#f472b6', '#ec4899', '#e879f9', '#c084fc',
@@ -47,6 +47,14 @@ const GLOW_COLORS = [
'#fdba74', '#f5d0b0', '#ffffff',
]
const props = defineProps({
/** 主题色板注入:空则用默认 */
colors: { type: Array, default: null },
glowColors: { type: Array, default: null },
})
const heartPalette = () => (props.colors?.length ? props.colors : HEART_COLORS)
const glowPalette = () => (props.glowColors?.length ? props.glowColors : GLOW_COLORS)
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]
const rand = (min, max) => min + Math.random() * (max - min)
@@ -199,8 +207,8 @@ function createHeart(x, y, now) {
birthTime: now,
maxAge: rand(HEART_MIN_LIFE, HEART_MAX_LIFE),
baseSize: rand(HEART_MIN_SIZE, HEART_MAX_SIZE),
color: pick(HEART_COLORS),
glowColor: pick(GLOW_COLORS),
color: pick(heartPalette()),
glowColor: pick(glowPalette()),
peakScale: rand(0.75, 1.35),
riseSpeed: rand(RISE_SPEED_MIN, RISE_SPEED_MAX),
driftAmp: rand(DRIFT_AMP_MIN, DRIFT_AMP_MAX),

View File

@@ -214,8 +214,8 @@
:class="{ 'g-heart-done': heartDone }"
>
<div
v-for="(slot, imgIdx) in HEART_SLOTS"
:key="imgIdx"
v-for="(slot, imgIdx) in heartSlots"
:key="`${page.heartLayout || 'classic'}-${imgIdx}`"
class="g-heart-slot"
:class="{ 'g-heart-placeholder': imgIdx >= shownCount }"
:style="heartSlotStyle(slot)"
@@ -225,7 +225,7 @@
v-reveal="{ variant: heartReveal(imgIdx), immediate: true }"
class="absolute inset-0 g-heart-cell"
:class="frameClass"
:style="heartCellShape(imgIdx)"
:style="heartCellShape(slot)"
>
<img
:src="displayAt(imgIdx)"
@@ -309,7 +309,7 @@
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { borderClass, borderStyleMeta } from '@/composables/borderStyles'
import { createSequentialMount } from '@/composables/useSequentialMount'
import { HEART_SLOTS, heartSlotStyle, heartFocusCssAt, heartCellShape } from '@/composables/heartCollage'
import { heartSlotsFor, heartSlotStyle, heartFocusCssAt, heartCellShape } from '@/composables/heartCollage'
import SectionHeading from '@/components/themes/SectionHeading.vue'
const NINE_STEP_MS = 300
@@ -735,16 +735,21 @@ function polaroidTransform(i) {
}
/** 爱心:左右对称入场,中轴与心尖用缩放 */
/** 当前爱心拼接方案的槽位page.heartLayout 可选classic 默认) */
const heartSlots = computed(() => heartSlotsFor(props.page))
function heartReveal(i) {
if (i === 5 || i === 12) return 'scale'
if (i === 0 || i === 2 || i === 3 || i === 4 || i === 9) return 'left'
if (i === 1 || i === 6 || i === 7 || i === 8 || i === 11) return 'right'
return 'up'
// 按槽位横向位置决定入场方向:左半从左入、右半从右入、中轴缩放
const slot = heartSlots.value[i]
if (!slot) return 'up'
const cx = slot.l + slot.w / 2
if (cx > 42 && cx < 58) return 'scale'
return cx <= 42 ? 'left' : 'right'
}
/** 爱心拼合完成:触发一次心跳 + 小心心飘散 */
const heartDone = computed(() =>
props.page?.type === 'heart-collage' && shownCount.value >= HEART_SLOTS.length
props.page?.type === 'heart-collage' && shownCount.value >= heartSlots.value.length
)
</script>

View File

@@ -201,6 +201,156 @@ html[data-theme='pixel'] .sh-note {
font-size: 10px;
}
/* —— 水墨:楷体 + 墨横 + 朱砂点 —— */
html[data-theme='inkwash'] .sh-kicker {
font-family: var(--font-kai);
letter-spacing: 0.4em;
}
html[data-theme='inkwash'] .sh-title {
font-family: var(--font-kai);
letter-spacing: 0.36em;
text-indent: 0.36em;
}
html[data-theme='inkwash'] .sh-rule-bottom {
display: block;
width: 46px;
height: 3px;
background: linear-gradient(90deg, transparent, var(--c-ink) 30%, var(--c-ink) 70%, transparent);
border-radius: 2px;
}
/* —— 电影:场记板 SCENE —— */
html[data-theme='cinema'] .sh-index {
display: block;
font-family: var(--font-number);
font-style: normal;
font-size: 13px;
letter-spacing: 0.2em;
color: var(--c-on-primary);
background: var(--c-primary);
padding: 3px 10px;
border-radius: 3px;
transform: rotate(-2deg);
}
html[data-theme='cinema'] .sh-index::before {
content: 'SCENE ';
}
html[data-theme='cinema'] .sh-title {
font-family: var(--font-serif);
font-weight: 700;
letter-spacing: 0.2em;
}
html[data-theme='cinema'] .sh-kicker {
font-family: var(--font-cinzel);
}
/* —— 胶片:帧号标记 —— */
html[data-theme='kodak'] .sh-index {
display: block;
font-family: var(--font-number);
font-style: normal;
font-size: 12px;
letter-spacing: 0.16em;
color: #ff9a3c;
text-shadow: 0 0 6px rgba(255, 130, 30, 0.55);
}
html[data-theme='kodak'] .sh-index::before {
content: '▸ ';
}
html[data-theme='kodak'] .sh-index::after {
content: 'A';
}
html[data-theme='kodak'] .sh-title {
font-weight: 700;
letter-spacing: 0.14em;
}
/* —— 文艺:罗马分幕 —— */
html[data-theme='arthouse'] .sh {
gap: 10px;
margin-bottom: 30px;
}
html[data-theme='arthouse'] .sh-kicker {
font-family: var(--font-display);
letter-spacing: 0.56em;
}
html[data-theme='arthouse'] .sh-title {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.3em;
text-indent: 0.3em;
}
html[data-theme='arthouse'] .sh-rule-bottom {
display: block;
width: 1px;
height: 22px;
background: rgba(var(--c-line-rgb), 0.6);
}
/* —— 旅行:邮戳圆章 —— */
html[data-theme='passport'] .sh-index {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-family: var(--font-number);
font-style: normal;
font-size: 13px;
color: var(--c-primary);
border: 2px solid rgba(var(--c-primary-rgb), 0.55);
border-radius: 50%;
transform: rotate(-10deg);
}
html[data-theme='passport'] .sh-kicker {
font-family: var(--font-number);
letter-spacing: 0.34em;
}
html[data-theme='passport'] .sh-title {
letter-spacing: 0.22em;
font-weight: 600;
}
/* —— 手账:标签贴 + 胶带 —— */
html[data-theme='scrapbook'] .sh-title {
font-family: var(--font-longcang);
padding: 6px 20px;
background: var(--c-card);
border: 1px dashed rgba(var(--c-primary-rgb), 0.45);
border-radius: 6px;
transform: rotate(-1.4deg);
box-shadow: 0 6px 16px rgba(69, 58, 48, 0.1);
}
html[data-theme='scrapbook'] .sh-kicker {
font-family: var(--font-liujian);
text-transform: none;
letter-spacing: 0.18em;
}
/* —— Y2K窗口标题条 —— */
html[data-theme='y2k'] .sh-title {
font-weight: 800;
letter-spacing: 0.1em;
background: linear-gradient(180deg, #7a4ae0 10%, #4a90e0 50%, #e04ad0 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
html[data-theme='y2k'] .sh-kicker {
font-family: var(--font-pixel);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--c-line);
text-transform: none;
}
html[data-theme='y2k'] .sh-rule-bottom {
display: block;
width: 60px;
height: 4px;
border-radius: 2px;
background: repeating-linear-gradient(90deg, #7a4ae0 0 8px, #4adce0 8px 16px);
}
/* —— 极简主义:小字 + 一条细线 + 大留白 —— */
html[data-theme='zen'] .sh {
gap: 12px;

View File

@@ -0,0 +1,98 @@
<template>
<div class="ca-root">
<span v-reveal="'down'" class="ca-chapter">CHAPITRE UN</span>
<div class="ca-mid">
<i v-reveal="'down'" class="ca-line" aria-hidden="true"></i>
<div v-reveal="{ variant: 'scale', delay: 180 }" class="ca-photo">
<img :src="heroImg" class="ca-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<i v-reveal="'up'" class="ca-line" aria-hidden="true"></i>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="ca-copy">
<span class="ca-names">{{ groom }} <em>et</em> {{ bride }}</span>
<span class="ca-date">{{ date }} {{ lunar }}</span>
</div>
</div>
</template>
<script setup>
/** 文艺影调封面:严格对称构图、上下轴线、法式分幕字 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.ca-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
padding: max(40px, env(safe-area-inset-top)) 30px 48px;
background: var(--c-bg);
overflow: hidden;
}
.ca-chapter {
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 0.66em;
text-indent: 0.66em;
color: var(--c-muted);
}
.ca-mid {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.ca-line {
width: 1px;
height: 30px;
background: rgba(var(--c-line-rgb), 0.7);
}
.ca-photo {
width: min(58vw, 215px);
aspect-ratio: 4 / 5;
overflow: hidden;
}
.ca-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: saturate(0.82) contrast(1.03);
}
.ca-copy {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
.ca-names {
font-family: var(--font-display);
font-size: clamp(23px, 7vw, 30px);
letter-spacing: 0.16em;
color: var(--c-ink);
}
.ca-names em {
font-style: italic;
font-size: 0.62em;
color: var(--c-muted);
margin: 0 0.3em;
}
.ca-date {
font-family: var(--font-serif);
font-size: 10.5px;
letter-spacing: 0.3em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="cc-root">
<div v-reveal="'down'" class="cc-top">
<span class="cc-badge">NOW SHOWING</span>
<span class="cc-studio">LOVE PICTURES 出品</span>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cc-poster">
<img :src="heroImg" class="cc-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="cc-poster-veil" aria-hidden="true"></span>
<span class="cc-corner" aria-hidden="true">彩色 · 宽银幕</span>
</div>
<div v-reveal="{ variant: 'up', delay: 280 }" class="cc-title-wrap">
<span class="cc-title">THE WEDDING</span>
<span class="cc-starring">领衔主演</span>
<span class="cc-names">{{ groom }} · {{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="cc-release">
<i aria-hidden="true"></i>
<span>{{ date }} 隆重献映 · {{ lunar }}</span>
<i aria-hidden="true"></i>
</div>
</div>
</template>
<script setup>
/** 复古电影海报封面NOW SHOWING 角标 + 主演字幕 + 上映日期 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cc-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
padding: max(24px, env(safe-area-inset-top)) 26px 34px;
background:
var(--t-bg-texture),
radial-gradient(ellipse at 50% 0%, rgba(212, 162, 74, 0.16), transparent 55%),
var(--c-bg);
overflow: hidden;
}
.cc-top {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.cc-badge {
padding: 4px 10px;
font-family: var(--font-cinzel);
font-size: 10px;
letter-spacing: 0.24em;
color: var(--c-on-primary);
background: var(--c-primary);
border-radius: 3px;
transform: rotate(-3deg);
box-shadow: 2px 3px 0 rgba(58, 44, 34, 0.25);
}
.cc-studio {
font-family: var(--font-serif);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cc-poster {
position: relative;
width: min(70vw, 260px);
aspect-ratio: 3 / 4;
border: 3px solid var(--c-ink);
outline: 1px solid rgba(var(--c-line-rgb), 0.8);
outline-offset: 5px;
overflow: hidden;
box-shadow: 0 18px 44px rgba(58, 44, 34, 0.3);
}
.cc-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: sepia(0.16) saturate(1.12) contrast(1.04);
}
.cc-poster-veil {
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 62%, rgba(38, 24, 16, 0.42));
pointer-events: none;
}
.cc-corner {
position: absolute;
right: 8px;
bottom: 8px;
font-family: var(--font-serif);
font-size: 9px;
letter-spacing: 0.16em;
color: rgba(248, 240, 226, 0.92);
}
.cc-title-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
}
.cc-title {
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(30px, 9.4vw, 42px);
letter-spacing: 0.1em;
color: var(--c-primary);
text-shadow: 2px 2px 0 rgba(212, 162, 74, 0.4);
}
.cc-starring {
font-family: var(--font-serif);
font-size: 10px;
letter-spacing: 0.5em;
text-indent: 0.5em;
color: var(--c-muted);
}
.cc-names {
font-family: var(--font-serif);
font-weight: 700;
font-size: clamp(19px, 5.6vw, 24px);
letter-spacing: 0.2em;
color: var(--c-ink);
}
.cc-release {
display: flex;
align-items: center;
gap: 12px;
font-family: var(--font-serif);
font-size: 11px;
letter-spacing: 0.16em;
color: var(--c-ink-soft);
}
.cc-release i {
width: 34px;
height: 1px;
background: rgba(var(--c-line-rgb), 0.8);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col items-center justify-between py-12 flex-1 w-full relative border-[0.5px] border-[var(--c-primary)]/20 bg-[var(--c-card)]/40">
<div class="flex flex-col items-center justify-between py-12 flex-1 w-full relative border-[0.5px] border-[var(--c-primary)]/20 bg-[var(--c-card)]/20">
<div v-reveal="'down'" class="text-center mt-6 z-10">
<span class="cover-kicker calligraphy text-[10px] tracking-[0.5em] uppercase text-[var(--c-primary)] font-bold block">Wedding Invitation</span>
<div class="w-6 h-[0.5px] bg-[var(--c-primary)] mx-auto opacity-50 mt-4"></div>

View File

@@ -0,0 +1,114 @@
<template>
<div class="ci-root">
<svg class="ci-mount" viewBox="0 0 400 160" preserveAspectRatio="none" aria-hidden="true">
<path d="M0 128C60 84 120 108 170 92 230 72 260 108 320 96 360 88 380 104 400 96V160H0Z" fill="var(--c-primary)" fill-opacity="0.1" />
<path d="M0 144C80 112 150 136 220 118 290 100 340 128 400 116V160H0Z" fill="var(--c-primary)" fill-opacity="0.16" />
</svg>
<span v-reveal="'down'" class="ci-kicker"> </span>
<div v-reveal="{ variant: 'scale', delay: 160 }" class="ci-photo">
<img :src="heroImg" class="ci-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div v-reveal="{ variant: 'up', delay: 280 }" class="ci-names">
<span class="vertical-text">{{ groom }}</span>
<b class="ci-seal" aria-hidden="true"></b>
<span class="vertical-text">{{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 360 }" class="ci-date">{{ date }} · {{ lunar }}</div>
</div>
</template>
<script setup>
/** 水墨山水封面:宣纸底 + 泼墨山影 + 竖排名 + 朱砂印 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.ci-root {
position: relative;
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 26px;
padding: max(30px, env(safe-area-inset-top)) 26px 90px;
background: var(--c-bg);
overflow: hidden;
}
.ci-mount {
position: absolute;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 150px;
pointer-events: none;
}
.ci-kicker {
font-family: var(--font-kai);
font-size: 13px;
letter-spacing: 0.6em;
text-indent: 0.6em;
color: var(--c-muted);
}
.ci-photo {
width: min(62vw, 235px);
aspect-ratio: 3 / 4;
padding: 8px;
background: var(--c-card);
border: 1px solid rgba(var(--c-primary-rgb), 0.28);
box-shadow: 0 16px 40px rgba(42, 46, 42, 0.14);
}
.ci-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: saturate(0.85) contrast(1.02);
}
.ci-names {
display: flex;
align-items: flex-start;
gap: 20px;
font-family: var(--font-kai);
font-size: clamp(21px, 6vw, 26px);
letter-spacing: 0.3em;
color: var(--c-ink);
}
.ci-names .vertical-text {
writing-mode: vertical-rl;
max-height: 120px;
}
.ci-seal {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 6px;
font-family: var(--font-kai);
font-size: 15px;
font-weight: 400;
letter-spacing: 0;
color: #fdf6f0;
background: #b5493f;
border-radius: 3px;
}
.ci-date {
font-family: var(--font-serif);
font-size: 11px;
letter-spacing: 0.24em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,127 @@
<template>
<div class="ck-root">
<div v-reveal="'down'" class="ck-strip" aria-hidden="true">
<i></i><i></i><i></i>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="ck-frame">
<span class="ck-holes ck-holes-top" aria-hidden="true"></span>
<div class="ck-photo">
<img :src="heroImg" class="ck-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="ck-stamp">{{ dateStamp }}</span>
</div>
<span class="ck-holes ck-holes-bottom" aria-hidden="true"></span>
<span class="ck-label">WEDDING FILM · 400</span>
</div>
<div v-reveal="{ variant: 'up', delay: 300 }" class="ck-names">{{ groom }} {{ bride }}</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="ck-sub">{{ date }} · {{ lunar }}</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
/** 胶片日记封面:齿孔胶片框 + 复古日期水印 + 柯达色条 */
const props = defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
/** 提取 'YY MM DD 形式的复古水印 */
const dateStamp = computed(() => {
const m = /(\d{4})年(\d{1,2})月(\d{1,2})日/.exec(props.date || '')
if (!m) return "'26 09 04"
return `'${m[1].slice(2)} ${m[2].padStart(2, '0')} ${m[3].padStart(2, '0')}`
})
</script>
<style scoped>
.ck-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: max(28px, env(safe-area-inset-top)) 26px 40px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
/* 柯达色条 */
.ck-strip {
display: flex;
gap: 4px;
}
.ck-strip i {
width: 26px;
height: 8px;
border-radius: 2px;
}
.ck-strip i:nth-child(1) { background: #e0782e; }
.ck-strip i:nth-child(2) { background: #d84438; }
.ck-strip i:nth-child(3) { background: #e8b02e; }
/* 胶片框 */
.ck-frame {
width: min(76vw, 280px);
background: #201d18;
border-radius: 8px;
padding: 6px 10px 8px;
box-shadow: 0 18px 42px rgba(56, 48, 42, 0.34);
}
.ck-holes {
display: block;
height: 9px;
margin: 4px 2px;
background-image: repeating-linear-gradient(90deg, rgba(250, 246, 236, 0.9) 0 10px, transparent 10px 21px);
border-radius: 2px;
}
.ck-photo {
position: relative;
overflow: hidden;
}
.ck-img {
width: 100%;
aspect-ratio: 3 / 4;
object-fit: cover;
display: block;
filter: sepia(0.18) saturate(1.15) contrast(1.02) brightness(1.02);
}
/* 复古橙色日期水印 */
.ck-stamp {
position: absolute;
right: 8px;
bottom: 8px;
font-family: var(--font-number);
font-size: 15px;
letter-spacing: 0.08em;
color: #ff9a3c;
text-shadow: 0 0 7px rgba(255, 130, 30, 0.75);
}
.ck-label {
display: block;
text-align: center;
padding-top: 3px;
font-size: 8.5px;
letter-spacing: 0.3em;
color: rgba(250, 246, 236, 0.6);
}
.ck-names {
font-size: clamp(21px, 6.4vw, 27px);
font-weight: 600;
letter-spacing: 0.14em;
color: var(--c-ink);
}
.ck-sub {
font-size: 11px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,209 @@
<template>
<div class="cpp-root">
<div v-reveal="'down'" class="cpp-head">
<span class="cpp-air">LOVE AIRLINES</span>
<span class="cpp-class">FIRST CLASS</span>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cpp-ticket">
<div class="cpp-photo">
<img :src="heroImg" class="cpp-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div class="cpp-info">
<div class="cpp-route">
<span class="cpp-port"><b>SGL</b><i>单身</i></span>
<span class="cpp-plane" aria-hidden="true">
<svg viewBox="0 0 90 20"><path d="M2 12h30M58 12h30" stroke="var(--c-line)" stroke-width="1" stroke-dasharray="3 3" fill="none"/><path d="M40 10l12 2-12 2 3-2z" fill="var(--c-primary)"/></svg>
</span>
<span class="cpp-port"><b>MRD</b><i>已婚</i></span>
</div>
<div class="cpp-row">
<span class="cpp-cell"><i>PASSENGERS</i><b>{{ groom }} / {{ bride }}</b></span>
</div>
<div class="cpp-row">
<span class="cpp-cell"><i>DATE</i><b>{{ date }}</b></span>
<span class="cpp-cell"><i>GATE</i><b>FOREVER</b></span>
</div>
<div class="cpp-barcode" aria-hidden="true"><i v-for="n in 22" :key="n"></i></div>
</div>
<span class="cpp-stamp" aria-hidden="true">APPROVED </span>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="cpp-foot">{{ lunar }} · 一起飞往余生</div>
</div>
</template>
<script setup>
/** 环球旅行封面:登机牌(单身 → 已婚 航线、乘客栏、条码、盖章) */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cpp-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 26px;
padding: max(28px, env(safe-area-inset-top)) 22px 40px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
.cpp-head {
width: min(100%, 330px);
display: flex;
justify-content: space-between;
font-family: var(--font-number);
font-size: 11px;
letter-spacing: 0.26em;
color: var(--c-primary);
}
.cpp-class {
color: var(--c-line);
}
.cpp-ticket {
position: relative;
width: min(100%, 330px);
border-radius: 12px;
background: var(--c-card);
border: 1px solid rgba(var(--c-primary-rgb), 0.3);
box-shadow: 0 16px 40px rgba(44, 54, 64, 0.16);
overflow: hidden;
}
/* 登机牌撕边 */
.cpp-ticket::before,
.cpp-ticket::after {
content: '';
position: absolute;
top: 58%;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--c-bg);
border: 1px solid rgba(var(--c-primary-rgb), 0.3);
}
.cpp-ticket::before { left: -9px; }
.cpp-ticket::after { right: -9px; }
.cpp-photo {
height: 168px;
overflow: hidden;
}
.cpp-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cpp-info {
padding: 14px 16px 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.cpp-route {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.cpp-port {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.cpp-port b {
font-family: var(--font-number);
font-size: 24px;
letter-spacing: 0.08em;
color: var(--c-primary);
}
.cpp-port i {
font-style: normal;
font-size: 9px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cpp-plane {
flex: 1;
}
.cpp-plane svg {
width: 100%;
height: 20px;
display: block;
}
.cpp-row {
display: flex;
gap: 14px;
border-top: 1px dashed rgba(var(--c-primary-rgb), 0.25);
padding-top: 9px;
}
.cpp-cell {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.cpp-cell i {
font-style: normal;
font-family: var(--font-number);
font-size: 8px;
letter-spacing: 0.22em;
color: var(--c-muted);
}
.cpp-cell b {
font-size: 13px;
letter-spacing: 0.06em;
color: var(--c-ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cpp-barcode {
display: flex;
align-items: flex-end;
gap: 2px;
height: 20px;
margin-top: 2px;
}
.cpp-barcode i {
width: 2px;
height: 100%;
background: var(--c-ink);
opacity: 0.8;
}
.cpp-barcode i:nth-child(2n) { height: 72%; width: 1px; }
.cpp-barcode i:nth-child(3n) { height: 86%; width: 3px; }
/* 邮戳 */
.cpp-stamp {
position: absolute;
right: 12px;
top: 130px;
padding: 5px 10px;
font-family: var(--font-number);
font-size: 11px;
letter-spacing: 0.14em;
color: #b5493f;
border: 2px solid #b5493f;
border-radius: 6px;
transform: rotate(-12deg);
opacity: 0.85;
background: rgba(255, 252, 246, 0.55);
}
.cpp-foot {
font-size: 11px;
letter-spacing: 0.24em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<div class="cs-root">
<span v-reveal="'down'" class="cs-kicker"> my wedding journal </span>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cs-photo-wrap">
<span class="cs-tape cs-tape-l" aria-hidden="true"></span>
<span class="cs-tape cs-tape-r" aria-hidden="true"></span>
<div class="cs-photo">
<img :src="heroImg" class="cs-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="cs-caption">us </span>
</div>
<span class="cs-sticker cs-sticker-1" aria-hidden="true"></span>
<span class="cs-sticker cs-sticker-2" aria-hidden="true"></span>
</div>
<div v-reveal="{ variant: 'up', delay: 300 }" class="cs-names">
<span>{{ groom }}</span>
<em>&amp;</em>
<span>{{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="cs-date-tag">
<span>{{ date }}</span>
<small>{{ lunar }}</small>
</div>
</div>
</template>
<script setup>
/** 手账拼贴封面:胶带斜贴拍立得 + 贴纸 + 手写标签 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cs-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: max(28px, env(safe-area-inset-top)) 26px 42px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
.cs-kicker {
font-family: var(--font-liujian);
font-size: 14px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cs-photo-wrap {
position: relative;
transform: rotate(-2.5deg);
}
.cs-photo {
width: min(64vw, 240px);
padding: 10px 10px 34px;
background: var(--c-card);
box-shadow: 0 12px 32px rgba(69, 58, 48, 0.18);
border: 1px solid rgba(69, 58, 48, 0.08);
}
.cs-img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
.cs-caption {
display: block;
padding-top: 8px;
text-align: center;
font-family: var(--font-liujian);
font-size: 15px;
color: var(--c-ink-soft);
}
.cs-tape {
position: absolute;
width: 74px;
height: 24px;
background: rgba(138, 168, 138, 0.5);
box-shadow: 0 1px 4px rgba(69, 58, 48, 0.16);
z-index: 2;
}
.cs-tape-l { top: -12px; left: -22px; transform: rotate(-38deg); }
.cs-tape-r { top: -10px; right: -24px; transform: rotate(32deg); background: rgba(216, 168, 72, 0.45); }
.cs-sticker {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
box-shadow: 0 2px 6px rgba(69, 58, 48, 0.24);
}
.cs-sticker-1 {
right: -16px;
bottom: 22px;
width: 34px;
height: 34px;
font-size: 15px;
background: #c47a5a;
transform: rotate(10deg);
}
.cs-sticker-2 {
left: -14px;
bottom: 52px;
width: 26px;
height: 26px;
font-size: 12px;
background: #8aa88a;
transform: rotate(-14deg);
}
.cs-names {
display: flex;
align-items: baseline;
gap: 12px;
font-family: var(--font-longcang);
font-size: clamp(26px, 8vw, 34px);
color: var(--c-ink);
}
.cs-names em {
font-style: normal;
font-size: 0.6em;
color: var(--c-primary);
}
.cs-date-tag {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 18px;
background: var(--c-card);
border: 1px dashed rgba(var(--c-primary-rgb), 0.5);
border-radius: 8px;
transform: rotate(1.6deg);
font-family: var(--font-liujian);
}
.cs-date-tag span {
font-size: 14px;
color: var(--c-ink-soft);
}
.cs-date-tag small {
font-size: 11px;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="cy-root">
<div v-reveal="{ variant: 'scale', delay: 100 }" class="cy-window">
<div class="cy-titlebar">
<span class="cy-title-text">wedding.exe</span>
<span class="cy-btns" aria-hidden="true"><i></i><i></i><i></i></span>
</div>
<div class="cy-body">
<div class="cy-photo">
<img :src="heroImg" class="cy-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div class="cy-names">{{ groom }} {{ bride }}</div>
<div class="cy-date">{{ date }} {{ lunar }}</div>
<div class="cy-progress" aria-hidden="true">
<span class="cy-progress-label">loading love 99%</span>
<span class="cy-progress-track"><i></i></span>
</div>
</div>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="cy-cursor" aria-hidden="true">
<span class="cy-star"></span>
<span class="cy-hint">click to continue</span>
</div>
</div>
</template>
<script setup>
/** 千禧蒸汽波封面:复古 UI 窗口(标题栏/加载条)+ 渐变镜面字 + 星星光标 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cy-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
padding: max(28px, env(safe-area-inset-top)) 24px 44px;
background:
var(--t-bg-texture),
linear-gradient(160deg, #ece8f8 0%, #e0ecf8 48%, #f4e4f4 100%);
overflow: hidden;
}
.cy-window {
width: min(100%, 330px);
border: 2px solid #9088b8;
border-radius: 10px;
background: var(--c-card);
box-shadow:
inset 1px 1px 0 rgba(255, 255, 255, 0.9),
6px 8px 0 rgba(122, 74, 224, 0.18),
0 20px 46px rgba(84, 74, 112, 0.22);
overflow: hidden;
}
.cy-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 10px;
background: linear-gradient(90deg, #7a4ae0, #4a90e0 55%, #4adce0);
color: #fff;
}
.cy-title-text {
font-family: var(--font-pixel);
font-size: 11px;
letter-spacing: 0.1em;
}
.cy-btns {
display: flex;
gap: 4px;
}
.cy-btns i {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-style: normal;
background: rgba(255, 255, 255, 0.92);
color: #544a70;
border-radius: 3px;
}
.cy-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 18px 18px 20px;
}
.cy-photo {
width: 100%;
border: 2px solid #b8b0d8;
border-radius: 6px;
padding: 4px;
background: #fff;
}
.cy-img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
border-radius: 3px;
filter: saturate(1.15) contrast(1.02);
}
/* 渐变镜面字 */
.cy-names {
font-size: clamp(23px, 7vw, 29px);
font-weight: 800;
letter-spacing: 0.08em;
background: linear-gradient(180deg, #7a4ae0 8%, #4a90e0 44%, #4adce0 58%, #e04ad0 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 2px 0 rgba(255, 255, 255, 0.8)) drop-shadow(0 4px 10px rgba(122, 74, 224, 0.3));
}
.cy-date {
font-family: var(--font-pixel);
font-size: 11px;
letter-spacing: 0.08em;
color: var(--c-ink-soft);
}
.cy-progress {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.cy-progress-label {
font-family: var(--font-pixel);
font-size: 9px;
color: var(--c-muted);
}
.cy-progress-track {
display: block;
height: 12px;
border: 2px solid #9088b8;
border-radius: 4px;
padding: 1px;
background: #fff;
}
.cy-progress-track i {
display: block;
width: 99%;
height: 100%;
border-radius: 2px;
background: repeating-linear-gradient(90deg, #7a4ae0 0 8px, #4adce0 8px 16px);
animation: cyLoad 1.6s steps(8) infinite;
}
@keyframes cyLoad {
0% { background-position: 0 0; }
100% { background-position: 32px 0; }
}
.cy-cursor {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.cy-star {
font-size: 20px;
color: #7a4ae0;
animation: cyTwinkle 1.2s steps(2) infinite;
}
@keyframes cyTwinkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.85); }
}
.cy-hint {
font-family: var(--font-pixel);
font-size: 9px;
letter-spacing: 0.16em;
color: var(--c-muted);
}
</style>