1. 后台看礼物记录
This commit is contained in:
@@ -215,6 +215,8 @@ func registerAPI(api *gin.RouterGroup) {
|
||||
api.GET("/gift", handleGiftStatus)
|
||||
api.POST("/gift", handleCreateGift)
|
||||
api.GET("/gift/stats", handleGiftStats)
|
||||
api.GET("/gift/list", handleGiftList)
|
||||
api.DELETE("/gift/:id", handleDeleteGift)
|
||||
api.GET("/gift/costs", handleGetGiftCosts)
|
||||
api.POST("/gift/costs", handleSaveGiftCosts)
|
||||
api.POST("/cash-gift", handleCreateCashGift)
|
||||
@@ -724,6 +726,42 @@ func handleGiftStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func handleGiftList(c *gin.Context) {
|
||||
if !utils.RequireAdmin(c) {
|
||||
return
|
||||
}
|
||||
var list []models.SiteGift
|
||||
db.Order("created_at desc, id desc").Find(&list)
|
||||
c.JSON(200, gin.H{
|
||||
"code": 200,
|
||||
"data": gin.H{
|
||||
"list": list,
|
||||
"count": len(list),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteGift(c *gin.Context) {
|
||||
if !utils.RequireAdmin(c) {
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(400, gin.H{"error": "缺少 ID"})
|
||||
return
|
||||
}
|
||||
res := db.Delete(&models.SiteGift{}, id)
|
||||
if res.Error != nil {
|
||||
c.JSON(500, gin.H{"error": "删除失败"})
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(404, gin.H{"error": "记录不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"code": 200, "msg": "已删除"})
|
||||
}
|
||||
|
||||
func handleGetGiftCosts(c *gin.Context) {
|
||||
if !utils.RequireAdmin(c) {
|
||||
return
|
||||
|
||||
@@ -154,6 +154,14 @@ export function getGiftStats() {
|
||||
return service.get('/gift/stats')
|
||||
}
|
||||
|
||||
export function getGiftList() {
|
||||
return service.get('/gift/list')
|
||||
}
|
||||
|
||||
export function deleteGift(id) {
|
||||
return service.delete(`/gift/${id}`)
|
||||
}
|
||||
|
||||
export function getGiftCosts() {
|
||||
return service.get('/gift/costs')
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
<div class="text-sm text-[#2c2c2c]">礼物统计</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-0.5">各礼物收到次数 · 合计 {{ giftStats.total }}</div>
|
||||
</div>
|
||||
<a-button size="small" :loading="giftStatsLoading" @click="loadGiftStats">
|
||||
<a-button size="small" :loading="giftStatsLoading || giftListLoading" @click="() => Promise.all([loadGiftStats(), loadGiftList()])">
|
||||
<i class="fa-solid fa-rotate-right mr-1"></i>刷新
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -234,6 +234,41 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8 mb-3 gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="text-sm text-[#2c2c2c]">礼物明细</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-0.5">共 {{ giftList.length }} 条送礼记录</div>
|
||||
</div>
|
||||
<a-button size="small" :loading="giftListLoading" @click="loadGiftList">
|
||||
<i class="fa-solid fa-rotate-right mr-1"></i>刷新列表
|
||||
</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
class="admin-table"
|
||||
:columns="giftListColumns"
|
||||
:data-source="giftList"
|
||||
:loading="giftListLoading"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
:pagination="{ pageSize: 10, hideOnSinglePage: true }"
|
||||
:scroll="{ x: 720 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'gift_type'">
|
||||
<span class="admin-pill admin-pill--rose">{{ giftTypeLabel(record.gift_type) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'cost'">
|
||||
<span>{{ record.cost || 0 }} 赞</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
<span class="text-[12px] text-[#8A8680]">{{ formatTime(record.created_at) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-button size="small" danger type="text" @click="onDeleteGift(record)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1301,7 +1336,7 @@ import { CanvasRenderer } from 'echarts/renderers'
|
||||
import {
|
||||
getConfigSection, saveConfigSection, uploadImage, getRsvpList, deleteRsvp, getUploadConfig, saveUploadConfig,
|
||||
getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitList,
|
||||
getPosterConfig, savePosterConfig, getGiftStats, saveGiftCosts,
|
||||
getPosterConfig, savePosterConfig, getGiftStats, getGiftList, deleteGift, saveGiftCosts,
|
||||
getCashGiftList, updateCashGift, deleteCashGift, submitCashGift,
|
||||
getCashGiftNotes, saveCashGiftNotes,
|
||||
getCashGiftViewPass, saveCashGiftViewPass,
|
||||
@@ -1311,7 +1346,7 @@ import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
|
||||
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
|
||||
import { GIFT_TYPES, emptyGiftCosts, DEFAULT_GIFT_COSTS } from '@/utils/giftTypes'
|
||||
import { GIFT_TYPES, GIFT_LABEL_MAP, emptyGiftCosts, DEFAULT_GIFT_COSTS } from '@/utils/giftTypes'
|
||||
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
|
||||
import {
|
||||
defaultPosterBundle, normalizePosterBundle, normalizePosterConfig,
|
||||
@@ -1451,8 +1486,10 @@ const danmakuFilter = ref('pending')
|
||||
const visitStatsLoading = ref(false)
|
||||
const visitListLoading = ref(false)
|
||||
const giftStatsLoading = ref(false)
|
||||
const giftListLoading = ref(false)
|
||||
const giftCostsSaving = ref(false)
|
||||
const giftTypeList = GIFT_TYPES
|
||||
const giftList = ref([])
|
||||
const giftStats = reactive({
|
||||
total: 0,
|
||||
counts: Object.fromEntries(GIFT_TYPES.map((g) => [g.key, 0])),
|
||||
@@ -2261,6 +2298,39 @@ async function loadGiftStats() {
|
||||
}
|
||||
}
|
||||
|
||||
const giftListColumns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 120 },
|
||||
{ title: '礼物', dataIndex: 'gift_type', key: 'gift_type', width: 120 },
|
||||
{ title: '消耗', dataIndex: 'cost', key: 'cost', width: 90 },
|
||||
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||
{ title: '操作', key: 'actions', width: 90 },
|
||||
]
|
||||
const giftTypeLabel = (key) => GIFT_LABEL_MAP[key] || key || '—'
|
||||
|
||||
async function loadGiftList() {
|
||||
giftListLoading.value = true
|
||||
try {
|
||||
const res = await getGiftList()
|
||||
const data = res.data || {}
|
||||
giftList.value = Array.isArray(data.list) ? data.list : (Array.isArray(data) ? data : [])
|
||||
} catch (e) {
|
||||
giftList.value = []
|
||||
message.error(e?.message || '加载礼物明细失败')
|
||||
} finally {
|
||||
giftListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteGift(record) {
|
||||
try {
|
||||
await deleteGift(record.id)
|
||||
message.success('已删除')
|
||||
await Promise.all([loadGiftList(), loadGiftStats()])
|
||||
} catch (e) {
|
||||
message.error(e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVisitStats() {
|
||||
visitStatsLoading.value = true
|
||||
try {
|
||||
@@ -2330,7 +2400,7 @@ async function onTabActivate(tab) {
|
||||
return
|
||||
}
|
||||
if (tab === 'gifts') {
|
||||
await loadGiftStats()
|
||||
await Promise.all([loadGiftStats(), loadGiftList()])
|
||||
return
|
||||
}
|
||||
if (tab === 'visits') {
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
<template>
|
||||
<div class="wrapper relative w-full min-h-[100dvh] bg-[#Fdfbf7] overflow-x-hidden text-[#2c2c2c]">
|
||||
|
||||
<!-- 揭幕前 Loading:主题色囍字 + 雅致装饰 -->
|
||||
<div
|
||||
v-if="showBootLoading"
|
||||
class="boot-loading"
|
||||
:class="{ 'is-leaving': bootLeaving }"
|
||||
aria-busy="true"
|
||||
aria-label="加载中"
|
||||
>
|
||||
<div class="boot-decor" aria-hidden="true">
|
||||
<i class="boot-glow boot-glow-1"></i>
|
||||
<i class="boot-glow boot-glow-2"></i>
|
||||
<i class="boot-glow boot-glow-3"></i>
|
||||
<span class="boot-petal boot-petal-1">❀</span>
|
||||
<span class="boot-petal boot-petal-2">❀</span>
|
||||
<span class="boot-petal boot-petal-3">✦</span>
|
||||
<span class="boot-petal boot-petal-4">✦</span>
|
||||
<span class="boot-spark boot-spark-1"></span>
|
||||
<span class="boot-spark boot-spark-2"></span>
|
||||
<span class="boot-spark boot-spark-3"></span>
|
||||
<span class="boot-spark boot-spark-4"></span>
|
||||
<span class="boot-spark boot-spark-5"></span>
|
||||
</div>
|
||||
<div class="boot-frame boot-frame-tl" aria-hidden="true"></div>
|
||||
<div class="boot-frame boot-frame-tr" aria-hidden="true"></div>
|
||||
<div class="boot-frame boot-frame-bl" aria-hidden="true"></div>
|
||||
<div class="boot-frame boot-frame-br" aria-hidden="true"></div>
|
||||
|
||||
<div class="boot-stage">
|
||||
<span class="boot-kicker calligraphy">WEDDING INVITATION</span>
|
||||
<div class="boot-rule" aria-hidden="true"><span></span><i>✦</i><span></span></div>
|
||||
<div class="boot-seal-wrap">
|
||||
<i class="boot-seal-ring boot-seal-ring-a" aria-hidden="true"></i>
|
||||
<i class="boot-seal-ring boot-seal-ring-b" aria-hidden="true"></i>
|
||||
<div class="boot-seal calligraphy">囍</div>
|
||||
</div>
|
||||
<div class="boot-rule boot-rule-flip" aria-hidden="true"><span></span><i>✦</i><span></span></div>
|
||||
<p class="boot-names calligraphy-strong">{{ data.groom }}<span>&</span>{{ data.bride }}</p>
|
||||
<p class="boot-hint calligraphy">请柬装载中</p>
|
||||
<div class="boot-dots" aria-hidden="true"><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 入场揭幕帘幕(请柬封面) -->
|
||||
<div v-show="showIntro" class="curtain-root" :class="[{ 'is-opening': introOpening }, introExitClass]" @click="openInvitation">
|
||||
<div class="curtain-bg" :style="curtainBg"></div>
|
||||
@@ -852,6 +894,7 @@ const audio = useAudio()
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)
|
||||
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0)
|
||||
const isSnapping = ref(false)
|
||||
const showBootLoading = ref(true), bootLeaving = ref(false)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMapOptions = ref(false)
|
||||
const showOpenInBrowser = ref(false)
|
||||
const showBlessingDrawer = ref(false)
|
||||
@@ -1480,9 +1523,29 @@ const openImagePreview = async (src) => {
|
||||
}
|
||||
|
||||
const effectsActive = computed(() =>
|
||||
!isDrawerOpen.value && !showBlessingDrawer.value && !showGiftPanel.value && !previewVisible.value && !showMapOptions.value && !showOpenInBrowser.value && !showIntro.value
|
||||
!showBootLoading.value
|
||||
&& !isDrawerOpen.value && !showBlessingDrawer.value && !showGiftPanel.value && !previewVisible.value && !showMapOptions.value && !showOpenInBrowser.value && !showIntro.value
|
||||
)
|
||||
|
||||
const preloadImage = (url) => new Promise((resolve) => {
|
||||
const src = String(url || '').trim()
|
||||
if (!src) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const img = new Image()
|
||||
img.onload = img.onerror = () => resolve()
|
||||
img.src = optimizeImageUrl(src)
|
||||
})
|
||||
|
||||
const finishBootLoading = async () => {
|
||||
if (!showBootLoading.value || bootLeaving.value) return
|
||||
bootLeaving.value = true
|
||||
await new Promise((r) => setTimeout(r, 480))
|
||||
showBootLoading.value = false
|
||||
bootLeaving.value = false
|
||||
}
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const pageScrollTop = () => {
|
||||
if (iosTransformActive && freeScrollOffset >= 0) return freeScrollOffset
|
||||
@@ -1733,6 +1796,14 @@ const completeIntro = () => {
|
||||
scrollPageToTop()
|
||||
showIntro.value = false
|
||||
introOpening.value = false
|
||||
flushPendingEntranceFx()
|
||||
scheduleAutoScrollStart()
|
||||
}
|
||||
|
||||
/** Loading / 揭幕期间先积压,结束后再播 */
|
||||
const shouldDeferEntranceFx = () => showBootLoading.value || showIntro.value
|
||||
|
||||
const flushPendingEntranceFx = () => {
|
||||
if (pendingLikeBurst > 0) {
|
||||
const n = pendingLikeBurst
|
||||
pendingLikeBurst = 0
|
||||
@@ -1745,7 +1816,6 @@ const completeIntro = () => {
|
||||
nextTick(() => giftEffectRef.value?.playSequence(list, { max: 12 }))
|
||||
}
|
||||
}
|
||||
scheduleAutoScrollStart()
|
||||
}
|
||||
|
||||
const openInvitation = () => {
|
||||
@@ -2014,8 +2084,8 @@ const loadLikeStatus = async () => {
|
||||
const count = Number(res.data?.count) || 0
|
||||
likeCount.value = count
|
||||
if (count <= 0) return
|
||||
// 揭幕中则等结束后再播,避免帘幕后白播
|
||||
if (showIntro.value) pendingLikeBurst = count
|
||||
// Loading / 揭幕中则等结束后再播,避免帘幕后白播
|
||||
if (shouldDeferEntranceFx()) pendingLikeBurst = count
|
||||
else nextTick(() => likeBurstRef.value?.burst(count, { durationMs: 2800, maxVisual: 10 }))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
@@ -2028,7 +2098,7 @@ const loadGiftStatus = async () => {
|
||||
const recent = Array.isArray(res.data?.recent) ? res.data.recent : []
|
||||
const aggregated = aggregateGiftRecent(recent)
|
||||
if (!aggregated.length) return
|
||||
if (showIntro.value) pendingGiftReplay.value = aggregated
|
||||
if (shouldDeferEntranceFx()) pendingGiftReplay.value = aggregated
|
||||
else if (showGiftFx.value) nextTick(() => giftEffectRef.value?.playSequence(aggregated, { max: 12 }))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
@@ -2059,6 +2129,8 @@ const handleLike = async (count = 1) => {
|
||||
onMounted(async () => {
|
||||
if ('scrollRestoration' in history) history.scrollRestoration = 'manual'
|
||||
window.scrollTo(0, 0)
|
||||
const bootStartedAt = Date.now()
|
||||
const BOOT_MIN_MS = 1100
|
||||
|
||||
try {
|
||||
const res = await getConfig()
|
||||
@@ -2106,7 +2178,18 @@ onMounted(async () => {
|
||||
loadGiftStatus()
|
||||
sendVisitBeacon()
|
||||
|
||||
// Loading 期间预热封面,避免揭幕后闪空图
|
||||
await preloadImage(data.value.heroImg)
|
||||
|
||||
const remain = BOOT_MIN_MS - (Date.now() - bootStartedAt)
|
||||
if (remain > 0) await new Promise((r) => setTimeout(r, remain))
|
||||
|
||||
// 先挂上揭幕帘,再淡出 Loading,避免中间一帧露出正文
|
||||
if (data.value.introEnabled) showIntro.value = true
|
||||
await nextTick()
|
||||
await finishBootLoading()
|
||||
// 未开启揭幕时,Loading 结束后再播积压特效
|
||||
if (!data.value.introEnabled) flushPendingEntranceFx()
|
||||
|
||||
const pickedUrl = pickMusicUrl(
|
||||
data.value.musicList,
|
||||
@@ -3385,6 +3468,264 @@ watch(
|
||||
width: 20px; height: 1px; background: rgba(168,140,107,0.45);
|
||||
}
|
||||
|
||||
/* ---------- 揭幕前 Loading(象牙金主题) ---------- */
|
||||
.boot-loading {
|
||||
--boot-gold: #a88c6b;
|
||||
--boot-gold-soft: #b89a78;
|
||||
--boot-ink: #5b4a36;
|
||||
--boot-cream: #fbf6ef;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 42%, rgba(255, 252, 246, 0.96), transparent 55%),
|
||||
radial-gradient(ellipse at 18% 78%, rgba(184, 154, 120, 0.14), transparent 42%),
|
||||
radial-gradient(ellipse at 82% 18%, rgba(168, 140, 107, 0.12), transparent 40%),
|
||||
linear-gradient(165deg, #fdfbf7 0%, #f7efe4 46%, #eddcc6 100%);
|
||||
transition: opacity 0.48s ease, visibility 0.48s ease, transform 0.48s ease;
|
||||
}
|
||||
.boot-loading.is-leaving {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.boot-decor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.boot-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(28px);
|
||||
opacity: 0.55;
|
||||
animation: boot-glow-drift 5.5s ease-in-out infinite;
|
||||
}
|
||||
.boot-glow-1 {
|
||||
width: 180px; height: 180px; left: 8%; top: 18%;
|
||||
background: rgba(184, 154, 120, 0.28);
|
||||
}
|
||||
.boot-glow-2 {
|
||||
width: 220px; height: 220px; right: 4%; bottom: 16%;
|
||||
background: rgba(168, 140, 107, 0.22);
|
||||
animation-delay: -2s;
|
||||
}
|
||||
.boot-glow-3 {
|
||||
width: 140px; height: 140px; left: 42%; top: 8%;
|
||||
background: rgba(255, 248, 238, 0.7);
|
||||
animation-delay: -3.4s;
|
||||
}
|
||||
.boot-petal {
|
||||
position: absolute;
|
||||
color: var(--boot-gold);
|
||||
opacity: 0.28;
|
||||
font-size: 18px;
|
||||
animation: boot-petal-float 4.8s ease-in-out infinite;
|
||||
}
|
||||
.boot-petal-1 { left: 14%; top: 28%; font-size: 22px; animation-delay: 0s; }
|
||||
.boot-petal-2 { right: 16%; top: 34%; font-size: 16px; animation-delay: -1.2s; }
|
||||
.boot-petal-3 { left: 20%; bottom: 22%; font-size: 14px; opacity: 0.22; animation-delay: -2.1s; }
|
||||
.boot-petal-4 { right: 18%; bottom: 26%; font-size: 13px; opacity: 0.2; animation-delay: -3s; }
|
||||
.boot-spark {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--boot-gold-soft);
|
||||
box-shadow: 0 0 8px rgba(184, 154, 120, 0.55);
|
||||
animation: boot-twinkle 2.4s ease-in-out infinite;
|
||||
}
|
||||
.boot-spark-1 { left: 22%; top: 18%; animation-delay: 0s; }
|
||||
.boot-spark-2 { right: 24%; top: 22%; animation-delay: 0.4s; }
|
||||
.boot-spark-3 { left: 28%; bottom: 18%; animation-delay: 0.9s; }
|
||||
.boot-spark-4 { right: 26%; bottom: 20%; animation-delay: 1.3s; }
|
||||
.boot-spark-5 { left: 50%; top: 14%; margin-left: -2px; animation-delay: 1.7s; }
|
||||
.boot-frame {
|
||||
position: absolute;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-color: rgba(168, 140, 107, 0.38);
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
opacity: 0;
|
||||
animation: boot-frame-in 0.7s ease 0.15s forwards;
|
||||
}
|
||||
.boot-frame-tl { top: 22px; left: 22px; border-top-width: 1px; border-left-width: 1px; }
|
||||
.boot-frame-tr { top: 22px; right: 22px; border-top-width: 1px; border-right-width: 1px; }
|
||||
.boot-frame-bl { bottom: 22px; left: 22px; border-bottom-width: 1px; border-left-width: 1px; }
|
||||
.boot-frame-br { bottom: 22px; right: 22px; border-bottom-width: 1px; border-right-width: 1px; }
|
||||
.boot-stage {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 28px;
|
||||
text-align: center;
|
||||
animation: boot-stage-in 0.75s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
.boot-kicker {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.48em;
|
||||
padding-left: 0.48em;
|
||||
color: var(--boot-gold);
|
||||
font-weight: 700;
|
||||
margin-bottom: 18px;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.boot-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
color: var(--boot-gold-soft);
|
||||
font-size: 10px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.boot-rule span {
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(168, 140, 107, 0.55));
|
||||
}
|
||||
.boot-rule span:last-child {
|
||||
background: linear-gradient(90deg, rgba(168, 140, 107, 0.55), transparent);
|
||||
}
|
||||
.boot-rule-flip { margin-top: 22px; margin-bottom: 0; }
|
||||
.boot-seal-wrap {
|
||||
position: relative;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.boot-seal-ring {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.boot-seal-ring-a {
|
||||
inset: -10px;
|
||||
border-width: 1.5px;
|
||||
border-top-color: rgba(168, 140, 107, 0.72);
|
||||
border-right-color: rgba(184, 154, 120, 0.18);
|
||||
animation: boot-ring-spin 1.8s linear infinite;
|
||||
}
|
||||
.boot-seal-ring-b {
|
||||
inset: -2px;
|
||||
border-width: 1px;
|
||||
border-bottom-color: rgba(184, 154, 120, 0.55);
|
||||
border-left-color: rgba(168, 140, 107, 0.12);
|
||||
animation: boot-ring-spin-rev 2.6s linear infinite;
|
||||
}
|
||||
.boot-seal {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--boot-gold);
|
||||
background:
|
||||
radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.65), transparent 46%),
|
||||
linear-gradient(160deg, #fffaf3, #f3e6d6);
|
||||
border: 1.5px solid rgba(168, 140, 107, 0.55);
|
||||
box-shadow:
|
||||
inset 0 0 0 5px rgba(168, 140, 107, 0.08),
|
||||
0 12px 32px rgba(120, 90, 55, 0.14);
|
||||
animation:
|
||||
boot-seal-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) both,
|
||||
boot-seal-breathe 2.2s ease-in-out 0.85s infinite;
|
||||
}
|
||||
.boot-names {
|
||||
margin: 20px 0 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--boot-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
.boot-names span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
font-style: italic;
|
||||
color: var(--boot-gold-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
.boot-hint {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.38em;
|
||||
padding-left: 0.38em;
|
||||
color: rgba(128, 106, 77, 0.78);
|
||||
}
|
||||
.boot-dots {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.boot-dots i {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--boot-gold);
|
||||
opacity: 0.28;
|
||||
animation: boot-dot-pulse 1.15s ease-in-out infinite;
|
||||
}
|
||||
.boot-dots i:nth-child(2) { animation-delay: 0.18s; }
|
||||
.boot-dots i:nth-child(3) { animation-delay: 0.36s; }
|
||||
|
||||
@keyframes boot-stage-in {
|
||||
from { opacity: 0; transform: translateY(12px) scale(0.98); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@keyframes boot-frame-in {
|
||||
from { opacity: 0; transform: scale(0.92); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@keyframes boot-seal-in {
|
||||
0% { opacity: 0; transform: scale(0.55) rotate(-8deg); }
|
||||
65% { opacity: 1; transform: scale(1.06) rotate(1.5deg); }
|
||||
100% { opacity: 1; transform: scale(1) rotate(0); }
|
||||
}
|
||||
@keyframes boot-seal-breathe {
|
||||
0%, 100% { transform: scale(1); box-shadow: inset 0 0 0 5px rgba(168, 140, 107, 0.08), 0 12px 32px rgba(120, 90, 55, 0.14); }
|
||||
50% { transform: scale(1.035); box-shadow: inset 0 0 0 5px rgba(168, 140, 107, 0.12), 0 14px 36px rgba(120, 90, 55, 0.18); }
|
||||
}
|
||||
@keyframes boot-ring-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes boot-ring-spin-rev {
|
||||
to { transform: rotate(-360deg); }
|
||||
}
|
||||
@keyframes boot-glow-drift {
|
||||
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||
50% { transform: translate3d(10px, -12px, 0) scale(1.08); }
|
||||
}
|
||||
@keyframes boot-petal-float {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.22; }
|
||||
50% { transform: translateY(-10px) rotate(8deg); opacity: 0.4; }
|
||||
}
|
||||
@keyframes boot-twinkle {
|
||||
0%, 100% { opacity: 0.15; transform: scale(0.7); }
|
||||
50% { opacity: 0.9; transform: scale(1.25); }
|
||||
}
|
||||
@keyframes boot-dot-pulse {
|
||||
0%, 100% { opacity: 0.22; transform: translateY(0); }
|
||||
50% { opacity: 0.95; transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
/* ---------- 入场揭幕(雅致象牙金) ---------- */
|
||||
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: var(--font-sans); background: #fbf6ef; perspective: 1200px; }
|
||||
.curtain-bg {
|
||||
|
||||
Reference in New Issue
Block a user