fix: 升级了一些视觉
This commit is contained in:
@@ -51,13 +51,13 @@ const { toasts, dismiss } = useToast()
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(253, 251, 247, 0.96);
|
||||
border: 0.5px solid rgba(168, 140, 107, 0.35);
|
||||
border: 0.5px solid rgba(var(--c-primary-rgb), 0.35);
|
||||
box-shadow: 0 12px 32px rgba(92, 74, 53, 0.16);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #5c4a35;
|
||||
}
|
||||
.app-toast.is-success {
|
||||
border-color: rgba(168, 140, 107, 0.5);
|
||||
border-color: rgba(var(--c-primary-rgb), 0.5);
|
||||
}
|
||||
.app-toast.is-error {
|
||||
border-color: rgba(196, 120, 110, 0.55);
|
||||
@@ -73,8 +73,8 @@ const { toasts, dismiss } = useToast()
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
font-size: 11px;
|
||||
background: rgba(168, 140, 107, 0.14);
|
||||
color: #a88c6b;
|
||||
background: rgba(var(--c-primary-rgb), 0.14);
|
||||
color: var(--c-primary);
|
||||
}
|
||||
.app-toast.is-error .app-toast-icon {
|
||||
background: rgba(196, 120, 110, 0.16);
|
||||
|
||||
143
vite-tailwindcss/src/components/BorderStylePicker.vue
Normal file
143
vite-tailwindcss/src/components/BorderStylePicker.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="bsp">
|
||||
<div v-for="group in BORDER_STYLE_GROUPS" :key="group.key" class="bsp-group">
|
||||
<div class="bsp-group-title">{{ group.label }}</div>
|
||||
<div class="bsp-grid">
|
||||
<button
|
||||
v-for="b in group.items"
|
||||
:key="b.value"
|
||||
type="button"
|
||||
class="bsp-cell"
|
||||
:class="{ active: modelValue === b.value }"
|
||||
:title="b.label"
|
||||
@click="$emit('update:modelValue', b.value)"
|
||||
>
|
||||
<span class="bsp-thumb-box" :style="{ height: thumbHeight(b) / 2 + 'px' }">
|
||||
<span
|
||||
class="bsp-thumb"
|
||||
:class="{ 'gf-shape-shadow': b.shaped }"
|
||||
:style="{ height: thumbHeight(b) + 'px' }"
|
||||
>
|
||||
<span class="bsp-frame" :class="borderClass(b.value)">
|
||||
<img :src="sample" alt="" loading="lazy" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="bsp-name">{{ b.label }}</span>
|
||||
<i v-if="modelValue === b.value" class="bsp-check fa-solid fa-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BORDER_STYLE_GROUPS, borderClass } from '@/composables/borderStyles'
|
||||
|
||||
defineProps({
|
||||
modelValue: { type: String, default: 'mat' },
|
||||
/** 缩略示例图 */
|
||||
sample: {
|
||||
type: String,
|
||||
default: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=200&q=80',
|
||||
},
|
||||
})
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
/** 缩略图按 2 倍渲染再 scale(0.5),保证固定像素边框细节比例正确;高度随样式推荐宽高比 */
|
||||
const THUMB_W = 120
|
||||
function thumbHeight(b) {
|
||||
const parts = String(b.ratio || '4 / 5').split('/')
|
||||
const w = parseFloat(parts[0]) || 4
|
||||
const h = parseFloat(parts[1]) || 5
|
||||
return Math.round((THUMB_W * h) / w)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bsp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.bsp-group-title {
|
||||
font-size: 11px;
|
||||
color: var(--c-primary);
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.bsp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.bsp-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 10px 4px 7px;
|
||||
border: 1px solid rgba(var(--c-primary-rgb), 0.14);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
.bsp-cell:hover {
|
||||
border-color: rgba(var(--c-primary-rgb), 0.4);
|
||||
background: #fbf9f4;
|
||||
}
|
||||
.bsp-cell.active {
|
||||
border-color: var(--c-primary);
|
||||
background: var(--c-bg);
|
||||
box-shadow: 0 0 0 2px rgba(var(--c-primary-rgb), 0.18);
|
||||
}
|
||||
.bsp-thumb-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
}
|
||||
.bsp-thumb {
|
||||
flex: none;
|
||||
width: 120px;
|
||||
transform: scale(0.5);
|
||||
transform-origin: top center;
|
||||
}
|
||||
.bsp-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* 缩略图太窄,邮戳 SVG 会压住 POST CARD 字样,仅保留字样 */
|
||||
.bsp-frame.gf-postcard::after {
|
||||
display: none;
|
||||
}
|
||||
.bsp-name {
|
||||
font-size: 10px;
|
||||
color: #6b6863;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bsp-cell.active .bsp-name {
|
||||
color: var(--c-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.bsp-check {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-primary);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(120, 90, 60, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -10,25 +10,22 @@
|
||||
v-for="row in rows"
|
||||
:key="row.id"
|
||||
class="danmaku-item"
|
||||
:class="{
|
||||
'danmaku-item--grad': row.isGradient,
|
||||
'danmaku-item--grad-light': row.isGradient && row.textLight,
|
||||
'danmaku-item--grad-dark': row.isGradient && !row.textLight,
|
||||
}"
|
||||
:class="{ 'danmaku-item--grad': row.isGradient }"
|
||||
:style="row.style"
|
||||
@animationend="onEnded(row.id)"
|
||||
>
|
||||
<span class="danmaku-name" :style="{ color: row.color }">{{ row.name }}</span>
|
||||
<span class="danmaku-sep" :style="{ color: row.color }">:</span>
|
||||
<span class="danmaku-text" :style="{ color: row.color }">{{ row.content }}</span>
|
||||
<!-- 渐变款:颜色落在文字上(clip 渐变字),底子是通透白纱 -->
|
||||
<span
|
||||
v-if="showTime && row.timeLabel"
|
||||
class="danmaku-time"
|
||||
:class="{
|
||||
'danmaku-time--on-light': row.isGradient && row.textLight,
|
||||
'danmaku-time--on-dark': row.isGradient && !row.textLight,
|
||||
}"
|
||||
>{{ row.timeLabel }}</span>
|
||||
v-if="row.textGradient"
|
||||
class="danmaku-body danmaku-body--grad"
|
||||
:style="{ backgroundImage: row.textGradient }"
|
||||
><span class="danmaku-name">{{ row.name }}</span><span class="danmaku-sep">:</span><span class="danmaku-text">{{ row.content }}</span></span>
|
||||
<template v-else>
|
||||
<span class="danmaku-name" :style="{ color: row.color }">{{ row.name }}</span>
|
||||
<span class="danmaku-sep" :style="{ color: row.color }">:</span>
|
||||
<span class="danmaku-text" :style="{ color: row.color }">{{ row.content }}</span>
|
||||
</template>
|
||||
<span v-if="showTime && row.timeLabel" class="danmaku-time">{{ row.timeLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -173,7 +170,6 @@ const buildRow = (item, { force = false } = {}) => {
|
||||
const totalDelay = delay + slot.extraDelay
|
||||
// 轨道分布在当前弹幕层内部(由 area 控制层高度)
|
||||
const topPct = TRACKS <= 1 ? 50 : 12 + (slot.track / (TRACKS - 1)) * 70
|
||||
const isGradient = typeof tint.background === 'string' && tint.background.includes('gradient')
|
||||
|
||||
const timeLabel = force
|
||||
? '刚刚'
|
||||
@@ -184,8 +180,8 @@ const buildRow = (item, { force = false } = {}) => {
|
||||
name: item.name,
|
||||
content: item.content,
|
||||
color: resolveDanmakuColor(item.color),
|
||||
textLight: !!tint.textLight,
|
||||
isGradient,
|
||||
isGradient: !!tint.textGradient,
|
||||
textGradient: tint.textGradient || '',
|
||||
timeLabel,
|
||||
style: {
|
||||
top: `${topPct}%`,
|
||||
@@ -377,7 +373,7 @@ defineExpose({ refresh: fetchList, pushItem })
|
||||
white-space: nowrap;
|
||||
padding: 5px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(168, 140, 107, 0.28);
|
||||
border: 1px solid rgba(var(--c-primary-rgb), 0.28);
|
||||
box-shadow: 0 4px 14px rgba(88, 68, 42, 0.08);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.06em;
|
||||
@@ -389,14 +385,18 @@ defineExpose({ refresh: fetchList, pushItem })
|
||||
max-width: none;
|
||||
}
|
||||
.danmaku-item--grad {
|
||||
backdrop-filter: saturate(1.2) blur(8px);
|
||||
backdrop-filter: saturate(1.15) blur(7px);
|
||||
font-weight: 500;
|
||||
}
|
||||
.danmaku-item--grad-light {
|
||||
text-shadow: 0 1px 2px rgba(20, 10, 20, 0.45), 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
/* 渐变字:颜色裁切进文字,底子保持通透 */
|
||||
.danmaku-body--grad {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.danmaku-item--grad-dark {
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
.danmaku-body--grad .danmaku-sep,
|
||||
.danmaku-body--grad .danmaku-text {
|
||||
opacity: 1;
|
||||
}
|
||||
.danmaku-name { font-weight: 600; }
|
||||
.danmaku-sep { opacity: 0.9; }
|
||||
@@ -407,12 +407,6 @@ defineExpose({ refresh: fetchList, pushItem })
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.danmaku-time--on-light {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
.danmaku-time--on-dark {
|
||||
color: rgba(74, 52, 32, 0.72);
|
||||
}
|
||||
@keyframes danmaku-fly {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(calc(-100vw - 100%)); }
|
||||
|
||||
1135
vite-tailwindcss/src/components/GiftEffect3D.vue
Normal file
1135
vite-tailwindcss/src/components/GiftEffect3D.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-[96px] h-[96px] rounded-xl border border-dashed border-[#a88c6b]/40 bg-[#Fdfbf7] overflow-hidden cursor-pointer hover:border-[#a88c6b] transition-colors group"
|
||||
class="relative w-[96px] h-[96px] rounded-xl border border-dashed border-[var(--c-primary)]/40 bg-[var(--c-paper)] overflow-hidden cursor-pointer hover:border-[var(--c-primary)] transition-colors group"
|
||||
@click="$emit('pick')"
|
||||
>
|
||||
<img v-if="modelValue" :src="modelValue" class="w-full h-full object-cover" />
|
||||
<div v-else class="w-full h-full flex flex-col items-center justify-center text-[#a88c6b]/70 gap-1">
|
||||
<div v-else class="w-full h-full flex flex-col items-center justify-center text-[var(--c-primary)]/70 gap-1">
|
||||
<i class="fa-solid fa-image text-lg"></i>
|
||||
<span class="text-[10px]">{{ placeholder }}</span>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-black/35 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-[10px]">
|
||||
<i class="fa-solid fa-images mr-1"></i>{{ uploading ? '上传中' : '选择图片' }}
|
||||
</div>
|
||||
<div v-if="label" class="absolute left-1 bottom-1 bg-white/80 text-[#a88c6b] text-[9px] px-1 rounded">{{ label }}</div>
|
||||
<div v-if="label" class="absolute left-1 bottom-1 bg-white/80 text-[var(--c-primary)] text-[9px] px-1 rounded">{{ label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@cancel="$emit('update:open', false)"
|
||||
>
|
||||
<div class="mp-toolbar">
|
||||
<a-button type="primary" class="!bg-[#a88c6b]" :loading="uploading" @click="triggerUpload">
|
||||
<a-button type="primary" class="!bg-[var(--c-primary)]" :loading="uploading" @click="triggerUpload">
|
||||
<i class="fa-solid fa-cloud-arrow-up mr-1"></i>上传新图
|
||||
</a-button>
|
||||
<a-button :loading="loading" @click="reloadFirstPage">
|
||||
@@ -153,12 +153,12 @@ watch(
|
||||
}
|
||||
.mp-hint {
|
||||
font-size: 12px;
|
||||
color: #8a8680;
|
||||
color: var(--c-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
.mp-empty {
|
||||
text-align: center;
|
||||
color: #8a8680;
|
||||
color: var(--c-muted);
|
||||
padding: 48px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -170,8 +170,8 @@ watch(
|
||||
}
|
||||
.mp-item {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(168, 140, 107, 0.28);
|
||||
background: #fdfbf7;
|
||||
border: 1px solid rgba(var(--c-primary-rgb), 0.28);
|
||||
background: var(--c-paper);
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@@ -181,21 +181,21 @@ watch(
|
||||
}
|
||||
.mp-item:hover,
|
||||
.mp-item.active {
|
||||
border-color: #a88c6b;
|
||||
box-shadow: 0 0 0 2px rgba(168, 140, 107, 0.2);
|
||||
border-color: var(--c-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--c-primary-rgb), 0.2);
|
||||
}
|
||||
.mp-item img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 88px;
|
||||
object-fit: cover;
|
||||
background: #f0ebe3;
|
||||
background: var(--c-bg-deep);
|
||||
}
|
||||
.mp-name {
|
||||
display: block;
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
color: #8a8680;
|
||||
color: var(--c-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<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>
|
||||
<span class="gallery-subtitle text-[9px] text-[#8A8680] first-letter:uppercase lowercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
|
||||
<span class="gallery-desc text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
|
||||
<!-- 标题区:完整模板主题用统一章节头,风格包保留经典排版 -->
|
||||
<SectionHeading
|
||||
v-if="layoutTheme"
|
||||
v-reveal="'up'"
|
||||
:index="pageIndex"
|
||||
:kicker="page.subtitle"
|
||||
:title="page.title"
|
||||
:note="page.desc"
|
||||
/>
|
||||
<div v-else v-reveal="'up'" class="text-center mb-5 px-4">
|
||||
<span class="gallery-title calligraphy text-xl text-[var(--c-primary)] tracking-[0.3em] font-bold block mb-2">{{ page.title }}</span>
|
||||
<span class="gallery-subtitle text-[9px] text-[var(--c-muted)] first-letter:uppercase lowercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
|
||||
<span class="gallery-desc text-[12px] text-[var(--c-muted)] 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 }"
|
||||
<div v-if="page.type === 'single'" v-reveal="{ variant: singleRevealVariant, delay: 100 }"
|
||||
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
|
||||
<div :class="singleFrameClass">
|
||||
<img :src="displayImg1" class="preview-img" @click.stop="previewImage(originalImg1)" />
|
||||
<span v-if="showPostmark" class="gf-postmark" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,8 +206,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 爱心拼接:绝对定位心形轮廓 + 串行挂载 -->
|
||||
<div v-else-if="page.type === 'heart-collage'" ref="multiRootRef" class="g-heart mt-6">
|
||||
<!-- 爱心拼接:绝对定位心形轮廓 + 串行挂载;拼合完成后心跳一次并飘散小心心 -->
|
||||
<div
|
||||
v-else-if="page.type === 'heart-collage'"
|
||||
ref="multiRootRef"
|
||||
class="g-heart mt-6"
|
||||
:class="{ 'g-heart-done': heartDone }"
|
||||
>
|
||||
<div
|
||||
v-for="(slot, imgIdx) in HEART_SLOTS"
|
||||
:key="imgIdx"
|
||||
@@ -211,6 +225,7 @@
|
||||
v-reveal="{ variant: heartReveal(imgIdx), immediate: true }"
|
||||
class="absolute inset-0 g-heart-cell"
|
||||
:class="frameClass"
|
||||
:style="heartCellShape(imgIdx)"
|
||||
>
|
||||
<img
|
||||
:src="displayAt(imgIdx)"
|
||||
@@ -221,17 +236,81 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="heartDone">
|
||||
<span class="g-heart-spark g-heart-spark-1" aria-hidden="true">♥</span>
|
||||
<span class="g-heart-spark g-heart-spark-2" aria-hidden="true">♥</span>
|
||||
<span class="g-heart-spark g-heart-spark-3" aria-hidden="true">♥</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式:{{ page.type }}</div>
|
||||
<!-- 三联屏:三竖条并排,中列上浮错落 -->
|
||||
<div v-else-if="page.type === 'triptych'" class="g-triptych mt-6">
|
||||
<div
|
||||
v-for="(_, imgIdx) in (page.images || []).slice(0, 3)"
|
||||
:key="imgIdx"
|
||||
v-reveal="{ variant: imgIdx === 0 ? 'left' : imgIdx === 2 ? 'right' : 'up', delay: imgIdx * 130 }"
|
||||
class="g-triptych-item"
|
||||
:class="[frameClass, { 'g-triptych-mid': imgIdx === 1 }]"
|
||||
>
|
||||
<img :src="displayAt(imgIdx)" class="preview-img g-cover-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 胶片连拍:深色胶片条 + 横滑帧 -->
|
||||
<div v-else-if="page.type === 'filmstrip'" v-reveal="{ variant: 'up', delay: 100 }" class="g-filmstrip mt-6">
|
||||
<div class="g-filmstrip-track no-scrollbar">
|
||||
<div
|
||||
v-for="(_, imgIdx) in (page.images || [])"
|
||||
:key="imgIdx"
|
||||
class="g-filmstrip-frame"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img :src="displayAt(imgIdx)" class="preview-img g-cover-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 扇形叠影:五张卡入场后从底部枢轴展开成扇 -->
|
||||
<div v-else-if="page.type === 'fan'" v-reveal="{ variant: 'scale', delay: 120 }" class="g-fan mt-4">
|
||||
<div
|
||||
v-for="(_, imgIdx) in (page.images || []).slice(0, 5)"
|
||||
:key="imgIdx"
|
||||
class="g-fan-item"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img :src="displayAt(imgIdx)" class="preview-img g-cover-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 杂志拼贴:横竖交错三行 grid + 串行挂载 -->
|
||||
<div v-else-if="page.type === 'mosaic'" ref="multiRootRef" class="g-mosaic mt-6">
|
||||
<div
|
||||
v-for="(_, imgIdx) in (page.images || []).slice(0, 6)"
|
||||
:key="imgIdx"
|
||||
class="g-mosaic-slot"
|
||||
:class="[`g-mosaic-area-${imgIdx + 1}`, { 'g-mosaic-placeholder': imgIdx >= shownCount }]"
|
||||
>
|
||||
<div
|
||||
v-if="imgIdx < shownCount"
|
||||
v-reveal="{ variant: imgIdx % 2 === 0 ? 'left' : 'right', immediate: true }"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img :src="displayAt(imgIdx)" class="preview-img g-cover-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-[var(--c-muted)] text-[12px] mt-6">未识别的版式:{{ page.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
import { borderClass, borderStyleMeta } from '@/composables/borderStyles'
|
||||
import { createSequentialMount } from '@/composables/useSequentialMount'
|
||||
import { HEART_SLOTS, heartSlotStyle, heartFocusCssAt } from '@/composables/heartCollage'
|
||||
import { HEART_SLOTS, heartSlotStyle, heartFocusCssAt, heartCellShape } from '@/composables/heartCollage'
|
||||
import SectionHeading from '@/components/themes/SectionHeading.vue'
|
||||
|
||||
const NINE_STEP_MS = 300
|
||||
const NINE_SETTLE_MS = 400
|
||||
@@ -264,12 +343,17 @@ const SEQUENTIAL_SELF_TYPES = new Set([
|
||||
'nine-grid',
|
||||
'one-large-five-small',
|
||||
'heart-collage',
|
||||
'mosaic',
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||
nineGridAnimate: { type: Boolean, default: false },
|
||||
/** 完整模板主题(magazine/newspaper/pixel/zen):标题区换统一章节头 */
|
||||
layoutTheme: { type: Boolean, default: false },
|
||||
/** 章节序号(完整模板的编号章节头用) */
|
||||
pageIndex: { type: Number, default: 0 },
|
||||
})
|
||||
const emit = defineEmits(['preview-image', 'ready'])
|
||||
|
||||
@@ -475,7 +559,7 @@ const runSequentialByType = async () => {
|
||||
await runOlfsSequential()
|
||||
return
|
||||
}
|
||||
if (type === 'masonry' || type === 'polaroid' || type === 'heart-collage') {
|
||||
if (type === 'masonry' || type === 'polaroid' || type === 'heart-collage' || type === 'mosaic') {
|
||||
await runMultiSequential()
|
||||
}
|
||||
}
|
||||
@@ -592,15 +676,29 @@ function getRevealConfig(imgIdx) {
|
||||
}
|
||||
}
|
||||
|
||||
const singleWrapClass = computed(() =>
|
||||
props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
|
||||
/** 当前边框元数据(分组/是否异形裁切/推荐宽高比) */
|
||||
const singleStyleMeta = computed(() => borderStyleMeta(props.page.borderStyle))
|
||||
/** 明信片·异形/横幅风景分组用飞入入场,其余保持缩放入场 */
|
||||
const singleRevealVariant = computed(() =>
|
||||
['postcard', 'scenic'].includes(singleStyleMeta.value?.group) ? 'postcard' : 'scale'
|
||||
)
|
||||
/** 邮戳盖章:仅 明信片/邮票 两种边框叠加 */
|
||||
const showPostmark = computed(() => ['postcard', 'stamp'].includes(props.page.borderStyle))
|
||||
|
||||
const singleWrapClass = computed(() => [
|
||||
props.page.singleWidth === 'full'
|
||||
? 'max-w-none'
|
||||
: (singleStyleMeta.value?.group === 'scenic' ? 'max-w-[340px]' : 'max-w-[300px]'),
|
||||
// mask 裁切类的投影由外层 drop-shadow 提供(box-shadow 会被 mask 裁掉)
|
||||
singleStyleMeta.value?.shaped ? 'gf-shape-shadow' : '',
|
||||
].filter(Boolean).join(' '))
|
||||
const singleWrapStyle = computed(() => {
|
||||
const ratio = singleStyleMeta.value?.ratio || '4 / 5'
|
||||
if (props.page.singleWidth === 'full') {
|
||||
const bleed = { width: 'calc(100% + 48px)', marginLeft: '-24px', marginRight: '-24px' }
|
||||
return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: '4 / 5' }
|
||||
return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: ratio }
|
||||
}
|
||||
return { aspectRatio: '4 / 5' }
|
||||
return { aspectRatio: ratio }
|
||||
})
|
||||
const singleFrameClass = computed(() =>
|
||||
frameClass.value + (props.page.singleWidth === 'full' && props.page.singleKeepRatio ? ' gf-natural' : '')
|
||||
@@ -644,6 +742,11 @@ function heartReveal(i) {
|
||||
return 'up'
|
||||
}
|
||||
|
||||
/** 爱心拼合完成:触发一次心跳 + 小心心飘散 */
|
||||
const heartDone = computed(() =>
|
||||
props.page?.type === 'heart-collage' && shownCount.value >= HEART_SLOTS.length
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -753,15 +856,15 @@ function heartReveal(i) {
|
||||
.g-arrow {
|
||||
position: absolute; top: 50%; transform: translateY(-50%);
|
||||
width: 34px; height: 34px; border-radius: 9999px; border: none;
|
||||
background: rgba(255,255,255,0.78); color: #a88c6b; font-size: 20px; line-height: 1;
|
||||
background: rgba(255,255,255,0.78); color: var(--c-primary); font-size: 20px; line-height: 1;
|
||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.12); backdrop-filter: blur(6px);
|
||||
}
|
||||
.g-prev { left: 8px; }
|
||||
.g-next { right: 8px; }
|
||||
.g-dots { display: flex; justify-content: center; gap: 6px; margin-top: 10px; }
|
||||
.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-dot { width: 7px; height: 7px; border-radius: 9999px; border: none; background: rgba(var(--c-primary-rgb),0.3); padding: 0; cursor: pointer; transition: all 0.3s; }
|
||||
.g-dot.active { background: var(--c-primary); width: 18px; border-radius: 9999px; }
|
||||
|
||||
/* 爱心拼接:绝对定位块状心形 + 淡囍底纹 */
|
||||
.g-heart {
|
||||
@@ -816,6 +919,7 @@ function heartReveal(i) {
|
||||
}
|
||||
.g-heart-cell {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
transition: transform 0.35s ease, box-shadow 0.35s ease;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
@@ -832,6 +936,41 @@ function heartReveal(i) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 拼合完成:整颗心轻跳两下(transform 不影响 absolute 子块布局) */
|
||||
.g-heart-done {
|
||||
animation: gHeartBeat 1.15s ease-in-out 0.2s 1;
|
||||
}
|
||||
@keyframes gHeartBeat {
|
||||
0%, 100% { transform: scale(1); }
|
||||
18% { transform: scale(1.035); }
|
||||
36% { transform: scale(1); }
|
||||
54% { transform: scale(1.02); }
|
||||
70% { transform: scale(1); }
|
||||
}
|
||||
/* 完成后从心口飘散的小心心 */
|
||||
.g-heart-spark {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
color: #d9868d;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
animation: gHeartSpark 1.6s ease-out forwards;
|
||||
}
|
||||
.g-heart-spark-1 { left: 20%; top: 24%; font-size: 15px; animation-delay: 0.35s; }
|
||||
.g-heart-spark-2 { right: 17%; top: 20%; font-size: 11px; animation-delay: 0.55s; }
|
||||
.g-heart-spark-3 { left: 47%; top: 7%; font-size: 13px; animation-delay: 0.75s; }
|
||||
@keyframes gHeartSpark {
|
||||
0% { opacity: 0; transform: translateY(8px) scale(0.5); }
|
||||
25% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: translateY(-36px) scale(1.2) rotate(14deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.g-heart-done { animation: none; }
|
||||
.g-heart-spark { display: none; }
|
||||
}
|
||||
|
||||
.g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; }
|
||||
.g-polaroid-slot {
|
||||
width: 42%;
|
||||
@@ -849,5 +988,122 @@ function heartReveal(i) {
|
||||
.g-polaroid-item:hover { z-index: 10; }
|
||||
.g-polaroid-item img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }
|
||||
|
||||
/* 通用:新版式内图片铺满裁切 */
|
||||
.g-cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 三联屏:三竖条,中列上浮(用 margin 错位,避免与 v-reveal 的 transform 冲突) */
|
||||
.g-triptych {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 14px 4px 0;
|
||||
}
|
||||
.g-triptych-item {
|
||||
aspect-ratio: 10 / 17;
|
||||
min-width: 0;
|
||||
}
|
||||
.g-triptych-mid {
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
/* 胶片连拍:深色胶片条 + 上下齿孔 + 横滑帧 */
|
||||
.g-filmstrip {
|
||||
position: relative;
|
||||
background: #26231f;
|
||||
border-radius: 10px;
|
||||
padding: 20px 10px;
|
||||
box-shadow: 0 14px 30px rgba(30, 24, 18, 0.28);
|
||||
}
|
||||
.g-filmstrip::before,
|
||||
.g-filmstrip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 8px;
|
||||
background-image: repeating-linear-gradient(90deg, rgba(253, 251, 247, 0.8) 0 10px, transparent 10px 20px);
|
||||
border-radius: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.g-filmstrip::before { top: 6px; }
|
||||
.g-filmstrip::after { bottom: 6px; }
|
||||
.g-filmstrip-track {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.g-filmstrip-frame {
|
||||
flex: 0 0 76%;
|
||||
min-width: 76%;
|
||||
aspect-ratio: 4 / 5;
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
/* 扇形叠影:五张卡绝对定位于底部枢轴,入场后展开成扇 */
|
||||
.g-fan {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
margin-inline: auto;
|
||||
aspect-ratio: 1 / 1.04;
|
||||
}
|
||||
.g-fan-item {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 3%;
|
||||
width: 56%;
|
||||
aspect-ratio: 3 / 4;
|
||||
transform-origin: 50% 94%;
|
||||
transform: translateX(-50%) rotate(0deg);
|
||||
transition: transform 0.9s cubic-bezier(0.34, 1.45, 0.5, 1);
|
||||
}
|
||||
/* 入场(容器 reveal--in)后按序展开 */
|
||||
.reveal--in .g-fan-item:nth-child(1) { transform: translateX(-50%) rotate(-26deg); transition-delay: 0.08s; }
|
||||
.reveal--in .g-fan-item:nth-child(2) { transform: translateX(-50%) rotate(-13deg); transition-delay: 0.16s; }
|
||||
.reveal--in .g-fan-item:nth-child(3) { transform: translateX(-50%) rotate(0deg); transition-delay: 0.24s; }
|
||||
.reveal--in .g-fan-item:nth-child(4) { transform: translateX(-50%) rotate(13deg); transition-delay: 0.32s; }
|
||||
.reveal--in .g-fan-item:nth-child(5) { transform: translateX(-50%) rotate(26deg); transition-delay: 0.4s; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.g-fan-item { transition: none; }
|
||||
}
|
||||
|
||||
/* 杂志拼贴:横竖交错三行 grid */
|
||||
.g-mosaic {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
grid-template-areas: 'a b b' 'c c d' 'e f f';
|
||||
gap: 7px;
|
||||
aspect-ratio: 3 / 3.1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.g-mosaic-slot {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
contain: layout paint;
|
||||
}
|
||||
.g-mosaic-area-1 { grid-area: a; }
|
||||
.g-mosaic-area-2 { grid-area: b; }
|
||||
.g-mosaic-area-3 { grid-area: c; }
|
||||
.g-mosaic-area-4 { grid-area: d; }
|
||||
.g-mosaic-area-5 { grid-area: e; }
|
||||
.g-mosaic-area-6 { grid-area: f; }
|
||||
.g-mosaic-placeholder {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user