657 lines
18 KiB
Vue
657 lines
18 KiB
Vue
<template>
|
||
<scroll-view class="page" scroll-y>
|
||
<view class="banner" v-if="detail.banner">
|
||
<image class="banner-img" :src="detail.banner" mode="widthFix" />
|
||
</view>
|
||
<view class="header">
|
||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||
<view class="meta" v-if="detail.price_draw !== undefined">参与价:¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
|
||
</view>
|
||
|
||
<view class="issues" v-if="showIssues">
|
||
<view class="issues-title">期数</view>
|
||
<view v-if="issues.length" class="issues-list">
|
||
<picker-view class="issue-picker" :value="[selectedIssueIndex]" @change="onIssueChange">
|
||
<picker-view-column>
|
||
<view class="picker-item" v-for="it in issues" :key="it.id">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
|
||
<view class="tabs">
|
||
<view class="tab" :class="{ active: tabActive === 'pool' }" @click="tabActive = 'pool'">本机奖池</view>
|
||
<view class="tab" :class="{ active: tabActive === 'records' }" @click="tabActive = 'records'">中奖记录</view>
|
||
</view>
|
||
|
||
<view v-show="tabActive === 'pool'">
|
||
<view class="rewards-grid" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
|
||
<view v-for="(rw, idx) in rewardsMap[currentIssueId]" :key="rw.id"
|
||
class="reward-card animate-stagger"
|
||
:style="{ '--delay': idx * 0.05 + 's' }">
|
||
<view class="card-header">
|
||
<text class="card-title">{{ rw.title }}</text>
|
||
<text v-if="rw.boss" class="badge-boss">BOSS</text>
|
||
</view>
|
||
<view class="image-wrapper">
|
||
<image v-if="rw.image" class="reward-image" :src="rw.image" mode="aspectFill" />
|
||
<text class="prob-tag absolute-tag">概率 {{ rw.percent }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="empty-state" v-else>
|
||
<text class="empty-icon">📭</text>
|
||
<text class="empty-text">暂无奖励配置</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-show="tabActive === 'records'">
|
||
<view class="records-list" v-if="winRecords.length">
|
||
<view v-for="(it, idx) in winRecords" :key="it.id"
|
||
class="record-item animate-stagger"
|
||
:style="{ '--delay': idx * 0.05 + 's' }">
|
||
<image class="record-img" :src="it.image" mode="aspectFill" />
|
||
<view class="record-info">
|
||
<view class="record-title">{{ it.title }}</view>
|
||
<view class="record-meta">
|
||
<text class="record-count">x{{ it.count }}</text>
|
||
<text v-if="it.percent !== undefined">占比 {{ it.percent }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="empty-state" v-else>
|
||
<text class="empty-icon">📝</text>
|
||
<text class="empty-text">暂无中奖记录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="issues-empty">暂无期数</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="float-bar">
|
||
<button class="action-btn primary" @click="onParticipate">
|
||
立即参与
|
||
<view class="btn-shine"></view>
|
||
</button>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import ElCard from '../../../components/ElCard.vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } from '../../../api/appUser'
|
||
|
||
const detail = ref({})
|
||
const statusText = ref('')
|
||
const issues = ref([])
|
||
const rewardsMap = ref({})
|
||
const currentIssueId = ref('')
|
||
const selectedIssueIndex = ref(0)
|
||
const showIssues = computed(() => (detail.value && detail.value.status !== 2))
|
||
const activityId = ref('')
|
||
const tabActive = ref('pool')
|
||
const winRecords = ref([])
|
||
|
||
function statusToText(s) {
|
||
if (s === 1) return '进行中'
|
||
if (s === 0) return '未开始'
|
||
if (s === 2) return '已结束'
|
||
return String(s || '')
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
const data = await getActivityDetail(id)
|
||
detail.value = data || {}
|
||
statusText.value = statusToText(detail.value.status)
|
||
}
|
||
|
||
function unwrap(list) {
|
||
if (Array.isArray(list)) return list
|
||
const obj = list || {}
|
||
const data = obj.data || {}
|
||
const arr = obj.list || obj.items || data.list || data.items || data
|
||
return Array.isArray(arr) ? arr : []
|
||
}
|
||
|
||
function cleanUrl(u) {
|
||
const s = String(u || '').trim()
|
||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||
if (m && m[0]) return m[0]
|
||
return s.replace(/[`'\"]/g, '').trim()
|
||
}
|
||
|
||
function truthy(v) {
|
||
if (typeof v === 'boolean') return v
|
||
const s = String(v || '').trim().toLowerCase()
|
||
if (!s) return false
|
||
return s === '1' || s === 'true' || s === 'yes' || s === 'y' || s === '是' || s === 'boss是真的' || s === 'boss' || s === '大boss'
|
||
}
|
||
function detectBoss(i) {
|
||
return truthy(i.is_boss) || truthy(i.boss) || truthy(i.isBoss) || truthy(i.boss_true) || truthy(i.boss_is_true) || truthy(i.bossText) || truthy(i.tag)
|
||
}
|
||
|
||
function normalizeIssues(list) {
|
||
const arr = unwrap(list)
|
||
return arr.map((i, idx) => ({
|
||
id: i.id ?? String(idx),
|
||
title: i.title ?? i.name ?? '',
|
||
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
|
||
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
|
||
}))
|
||
}
|
||
function normalizeRewards(list) {
|
||
const arr = unwrap(list)
|
||
const items = arr.map((i, idx) => ({
|
||
id: i.product_id ?? i.id ?? String(idx),
|
||
title: i.name ?? i.title ?? '',
|
||
image: cleanUrl(i.product_image ?? i.image ?? i.img ?? i.pic ?? i.banner ?? ''),
|
||
weight: Number(i.weight) || 0,
|
||
boss: detectBoss(i)
|
||
}))
|
||
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
||
const enriched = items.map(it => ({
|
||
...it,
|
||
percent: total > 0 ? Math.round((it.weight / total) * 1000) / 10 : 0
|
||
}))
|
||
enriched.sort((a, b) => (b.percent - a.percent))
|
||
return enriched
|
||
}
|
||
function normalizeWinRecords(list) {
|
||
const arr = unwrap(list)
|
||
return arr.map((i, idx) => ({
|
||
id: i.id ?? i.record_id ?? i.product_id ?? String(idx),
|
||
title: i.title ?? i.name ?? i.product_name ?? '',
|
||
image: cleanUrl(i.image ?? i.img ?? i.pic ?? i.product_image ?? ''),
|
||
count: Number(i.count ?? i.total ?? i.qty ?? 1) || 1,
|
||
percent: i.percent !== undefined ? Math.round(Number(i.percent) * 10) / 10 : undefined
|
||
}))
|
||
}
|
||
function isFresh(ts) {
|
||
const now = Date.now()
|
||
const v = Number(ts || 0)
|
||
return now - v < 24 * 60 * 60 * 1000
|
||
}
|
||
function getRewardCache() {
|
||
const obj = uni.getStorageSync('reward_cache_v1') || {}
|
||
return typeof obj === 'object' && obj ? obj : {}
|
||
}
|
||
async function fetchRewardsForIssues(activityId) {
|
||
const list = issues.value || []
|
||
const cache = getRewardCache()
|
||
const act = cache[activityId] || {}
|
||
const toFetch = []
|
||
list.forEach(it => {
|
||
const c = act[it.id]
|
||
if (c && isFresh(c.ts) && Array.isArray(c.value)) {
|
||
rewardsMap.value = { ...(rewardsMap.value || {}), [it.id]: c.value }
|
||
} else {
|
||
toFetch.push(it)
|
||
}
|
||
})
|
||
if (!toFetch.length) return
|
||
const promises = toFetch.map(it => getActivityIssueRewards(activityId, it.id))
|
||
const results = await Promise.allSettled(promises)
|
||
const nextAct = { ...act }
|
||
results.forEach((res, i) => {
|
||
const issueId = toFetch[i] && toFetch[i].id
|
||
if (!issueId) return
|
||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
|
||
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
|
||
nextAct[issueId] = { value, ts: Date.now() }
|
||
})
|
||
cache[activityId] = nextAct
|
||
uni.setStorageSync('reward_cache_v1', cache)
|
||
}
|
||
|
||
async function fetchIssues(id) {
|
||
const data = await getActivityIssues(id)
|
||
issues.value = normalizeIssues(data)
|
||
const latestId = pickLatestIssueId(issues.value)
|
||
setSelectedById(latestId)
|
||
await fetchRewardsForIssues(id)
|
||
}
|
||
|
||
async function fetchWinRecords(activityId) {
|
||
try {
|
||
const data = await getActivityWinRecords(activityId, 1, 50)
|
||
winRecords.value = normalizeWinRecords(data)
|
||
} catch (e) {
|
||
winRecords.value = []
|
||
}
|
||
}
|
||
|
||
function pickLatestIssueId(list) {
|
||
const arr = Array.isArray(list) ? list : []
|
||
let latest = arr[arr.length - 1] && arr[arr.length - 1].id
|
||
let maxNo = -Infinity
|
||
arr.forEach(i => {
|
||
const n = Number(i.no)
|
||
if (!Number.isNaN(n) && Number.isFinite(n) && n > maxNo) {
|
||
maxNo = n
|
||
latest = i.id
|
||
}
|
||
})
|
||
return latest || (arr[0] && arr[0].id) || ''
|
||
}
|
||
function setSelectedById(id) {
|
||
const arr = issues.value || []
|
||
const idx = Math.max(0, arr.findIndex(x => x && x.id === id))
|
||
selectedIssueIndex.value = idx
|
||
const cur = arr[idx]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
}
|
||
function onIssueChange(e) {
|
||
const v = e && e.detail && e.detail.value
|
||
const idx = Array.isArray(v) ? (v[0] || 0) : 0
|
||
const arr = issues.value || []
|
||
const bounded = Math.min(Math.max(0, idx), arr.length - 1)
|
||
selectedIssueIndex.value = bounded
|
||
const cur = arr[bounded]
|
||
currentIssueId.value = (cur && cur.id) || ''
|
||
}
|
||
|
||
function onPreviewBanner() {
|
||
const url = detail.value.banner || ''
|
||
if (url) uni.previewImage({ urls: [url], current: url })
|
||
}
|
||
|
||
async function onParticipate() {
|
||
const aid = activityId.value || ''
|
||
const iid = currentIssueId.value || ''
|
||
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
||
|
||
uni.showLoading({ title: '抽选中...' })
|
||
try {
|
||
const res = await drawActivityIssue(aid, iid)
|
||
uni.hideLoading()
|
||
const obj = res || {}
|
||
const data = obj.data || obj.result || obj.reward || obj.item || obj
|
||
const name = String((data && (data.title || data.name || data.product_name)) || '未知奖励')
|
||
const img = String((data && (data.image || data.img || data.pic || data.product_image)) || '')
|
||
uni.showModal({ title: '抽选结果', content: '恭喜获得:' + name, showCancel: false, success: () => { if (img) uni.previewImage({ urls: [img], current: img }) } })
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
const msg = String((err && (err.message || err.msg)) || '抽选失败')
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
}
|
||
}
|
||
|
||
onLoad((opts) => {
|
||
const id = (opts && opts.id) || ''
|
||
if (id) {
|
||
activityId.value = id
|
||
fetchDetail(id)
|
||
fetchIssues(id)
|
||
fetchWinRecords(id)
|
||
}
|
||
ensureElCard()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
对对碰活动页面 - 高级设计重构 (SCSS Integration)
|
||
============================================ */
|
||
|
||
.page-container {
|
||
min-height: 100vh;
|
||
background: $bg-page;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 背景装饰 */
|
||
.bg-decoration {
|
||
position: absolute;
|
||
top: 0; left: 0; width: 100%; height: 100%;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
}
|
||
.orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(80rpx);
|
||
opacity: 0.6;
|
||
}
|
||
.orb-1 {
|
||
width: 500rpx; height: 500rpx;
|
||
background: radial-gradient(circle, rgba($brand-primary, 0.2) 0%, transparent 70%);
|
||
top: -100rpx; left: -100rpx;
|
||
}
|
||
.orb-2 {
|
||
width: 600rpx; height: 600rpx;
|
||
background: radial-gradient(circle, rgba($accent-gold, 0.15) 0%, transparent 70%);
|
||
bottom: -100rpx; right: -100rpx;
|
||
}
|
||
|
||
.page-content {
|
||
flex: 1;
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
/* Banner */
|
||
.banner-wrapper {
|
||
margin: $spacing-md $spacing-lg;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-lg;
|
||
position: relative;
|
||
animation: fadeInDown 0.6s ease-out;
|
||
}
|
||
.banner-img {
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
.banner-shadow {
|
||
position: absolute;
|
||
bottom: 0; left: 0; width: 100%; height: 40%;
|
||
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
|
||
}
|
||
|
||
/* Header */
|
||
.header-section {
|
||
padding: 0 $spacing-lg;
|
||
margin-bottom: $spacing-lg;
|
||
text-align: center;
|
||
animation: fadeIn 0.8s ease-out;
|
||
}
|
||
.title-row {
|
||
margin-bottom: $spacing-sm;
|
||
}
|
||
.title-text {
|
||
font-size: $font-xxl;
|
||
font-weight: 900;
|
||
background: $gradient-brand;
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
display: inline-block;
|
||
}
|
||
.price-tag {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
background: rgba($bg-card, 0.6);
|
||
padding: $spacing-xs $spacing-lg;
|
||
border-radius: $radius-round;
|
||
backdrop-filter: blur(20rpx);
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
.price-label { font-size: $font-sm; color: $text-sub; margin-right: $spacing-xs; }
|
||
.price-symbol { font-size: $font-sm; color: $brand-primary; font-weight: 700; }
|
||
.price-value { font-size: $font-xl; color: $brand-primary; font-weight: 900; font-family: 'DIN Alternate', sans-serif; }
|
||
|
||
/* Glass Card */
|
||
.glass-card {
|
||
margin: 0 $spacing-lg $spacing-lg;
|
||
background: rgba($bg-card, 0.8);
|
||
backdrop-filter: blur(40rpx);
|
||
border-radius: $radius-xl;
|
||
padding: $spacing-lg;
|
||
box-shadow: $shadow-card;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||
animation: fadeInUp 0.6s ease-out 0.2s backwards;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $spacing-md;
|
||
padding: 0 4rpx;
|
||
}
|
||
.section-title {
|
||
font-size: $font-lg;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
position: relative;
|
||
padding-left: 20rpx;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0; top: 50%; transform: translateY(-50%);
|
||
width: 8rpx; height: 32rpx;
|
||
background: $gradient-brand;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
.issue-indicator {
|
||
font-size: $font-sm;
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 4rpx $spacing-md;
|
||
border-radius: $radius-round;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Custom Picker */
|
||
.custom-picker {
|
||
height: 280rpx;
|
||
background: rgba($bg-secondary, 0.5);
|
||
border-radius: $radius-lg;
|
||
margin-bottom: $spacing-lg;
|
||
overflow: hidden;
|
||
}
|
||
.picker-item {
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $spacing-md;
|
||
}
|
||
.picker-text { font-size: $font-lg; color: $text-main; font-weight: 600; }
|
||
.picker-status {
|
||
font-size: $font-xs; color: $text-sub; background: rgba(0,0,0,0.05); padding: 2rpx $spacing-sm; border-radius: $radius-sm;
|
||
|
||
&.status-active { background: #D1FAE5; color: #059669; }
|
||
}
|
||
|
||
/* Modern Tabs */
|
||
.modern-tabs {
|
||
display: flex;
|
||
background: $bg-secondary;
|
||
padding: 8rpx;
|
||
border-radius: $radius-lg;
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
.tab-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: $spacing-md 0;
|
||
font-size: $font-md;
|
||
color: $text-sub;
|
||
border-radius: $radius-md;
|
||
font-weight: 600;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
|
||
&.active {
|
||
background: #FFFFFF;
|
||
color: $brand-primary;
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
}
|
||
.active-dot {
|
||
width: 8rpx; height: 8rpx;
|
||
background: $brand-primary;
|
||
border-radius: 50%;
|
||
position: absolute;
|
||
bottom: 8rpx; left: 50%; transform: translateX(-50%);
|
||
}
|
||
|
||
/* Rewards Grid */
|
||
.rewards-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: $spacing-lg;
|
||
}
|
||
.reward-card {
|
||
background: #FFFFFF;
|
||
border-radius: $radius-lg;
|
||
padding: $spacing-lg;
|
||
box-shadow: $shadow-sm;
|
||
border: 1rpx solid rgba(0,0,0,0.03);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: $spacing-md;
|
||
height: 44rpx;
|
||
}
|
||
.card-title {
|
||
font-size: $font-md;
|
||
color: $text-main;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-right: 8rpx;
|
||
}
|
||
.badge-boss {
|
||
font-size: $font-xs;
|
||
background: $gradient-gold;
|
||
color: #78350F;
|
||
padding: 2rpx $spacing-sm;
|
||
border-radius: $radius-sm;
|
||
font-weight: 800;
|
||
flex-shrink: 0;
|
||
}
|
||
.card-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.image-wrapper {
|
||
width: 100%;
|
||
padding-bottom: 100%;
|
||
position: relative;
|
||
background: $bg-secondary;
|
||
border-radius: $radius-md;
|
||
overflow: hidden;
|
||
margin-bottom: $spacing-sm;
|
||
}
|
||
.reward-image {
|
||
position: absolute;
|
||
top: 0; left: 0; width: 100%; height: 100%;
|
||
}
|
||
.prob-tag {
|
||
position: absolute;
|
||
top: 8rpx; left: 8rpx;
|
||
font-size: $font-xs;
|
||
color: #fff;
|
||
background: rgba(0,0,0,0.6);
|
||
backdrop-filter: blur(4rpx);
|
||
padding: 4rpx $spacing-sm;
|
||
border-radius: $radius-sm;
|
||
z-index: 2;
|
||
}
|
||
|
||
/* Records List */
|
||
.records-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $spacing-lg;
|
||
}
|
||
.record-item {
|
||
display: flex;
|
||
background: #FFFFFF;
|
||
padding: $spacing-lg;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $shadow-sm;
|
||
align-items: center;
|
||
}
|
||
.record-img {
|
||
width: 100rpx; height: 100rpx;
|
||
border-radius: $radius-md;
|
||
background: $bg-secondary;
|
||
margin-right: $spacing-lg;
|
||
}
|
||
.record-info {
|
||
flex: 1;
|
||
}
|
||
.record-title {
|
||
font-size: $font-md;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-xs;
|
||
}
|
||
.record-meta {
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
font-size: $font-sm;
|
||
color: $text-sub;
|
||
}
|
||
.record-count {
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
padding: 2rpx $spacing-sm;
|
||
border-radius: $radius-sm;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 80rpx 0;
|
||
color: $text-placeholder;
|
||
}
|
||
.empty-icon { font-size: 80rpx; margin-bottom: $spacing-lg; opacity: 0.5; }
|
||
.empty-text { font-size: $font-md; }
|
||
|
||
/* Float Bar */
|
||
.float-bar {
|
||
position: fixed;
|
||
left: 0; right: 0; bottom: 0;
|
||
padding: $spacing-lg $spacing-xl;
|
||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.9);
|
||
backdrop-filter: blur(20px);
|
||
box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.05);
|
||
z-index: 100;
|
||
animation: slideUp 0.4s ease-out backwards;
|
||
}
|
||
.action-btn {
|
||
height: 96rpx;
|
||
border-radius: $radius-round;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: $font-xl;
|
||
font-weight: 800;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.2s;
|
||
|
||
&.primary {
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
box-shadow: $shadow-warm;
|
||
}
|
||
|
||
&:active { transform: scale(0.98); }
|
||
}
|
||
.btn-shine {
|
||
position: absolute;
|
||
top: 0; left: -100%; width: 50%; height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||
transform: skewX(-20deg);
|
||
animation: shine 3s infinite;
|
||
}
|
||
|
||
/* Animation Utilities */
|
||
.animate-stagger {
|
||
animation: fadeInUp 0.5s ease-out backwards;
|
||
animation-delay: var(--delay, 0s);
|
||
}
|
||
</style>
|