303 lines
7.8 KiB
Vue

<template>
<view class="page">
<scroll-view class="content" scroll-y>
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
<view v-else-if="filteredActivities.length > 0" class="activity-grid">
<view class="activity-item" v-for="a in filteredActivities" :key="a.id" @tap="onActivityTap(a)">
<view class="thumb-box">
<image class="thumb" :src="a.image" mode="aspectFill" />
<view class="tag-hot">HOT</view>
</view>
<view class="info">
<view class="name">{{ a.title }}</view>
<view class="bottom-row">
<text class="price-text">{{ a.category_name }} · {{ a.subtitle }}</text>
<view class="btn-go">GO</view>
</view>
</view>
</view>
</view>
<view v-else class="empty">
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
<text class="empty-text">暂无{{ title }}活动</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { request, authRequest } from '@/utils/request.js'
const title = ref('')
const categoryTarget = ref('')
const activities = ref([])
const loading = ref(false)
const filteredActivities = computed(() => {
if (!categoryTarget.value) return activities.value
const target = categoryTarget.value.trim()
return activities.value.filter(a => {
const cat = (a.category_name || '').trim()
return cat === target || cat.includes(target)
})
})
function apiGet(url) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url })
}
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 buildSubtitle(i) {
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
if (base) return base
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `¥${(Number(i.price_draw || 0) / 100).toFixed(2)}` : ''
return price
}
async function loadData() {
loading.value = true
try {
const res = await apiGet('/api/app/activities')
let list = []
if (Array.isArray(res)) list = res
else if (res && (Array.isArray(res.list) || Array.isArray(res.data))) list = res.list || res.data
activities.value = list.map((i, idx) => ({
id: i.id ?? String(idx),
image: cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
title: (i.title ?? i.name ?? '').replace(/无限赏|一番赏|对对碰|爬塔/g, '').trim(),
subtitle: buildSubtitle(i),
category_name: i.category_name ?? i.categoryName ?? '',
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
})).filter(i => i.image || i.title)
} catch (e) {
activities.value = []
} finally {
loading.value = false
}
}
function onActivityTap(a) {
const name = (a.category_name || '').trim()
const id = a.id
let path = ''
// Navigate to DETAIL, not list
if (name.includes('一番赏')) path = '/pages-activity/activity/yifanshang/index'
else if (name.includes('无限赏')) path = '/pages-activity/activity/wuxianshang/index'
else if (name.includes('对对碰')) path = '/pages-activity/activity/duiduipeng/index'
else if (name.includes('爬塔')) path = '/pages-activity/activity/pata/index'
if (path && id) {
uni.navigateTo({ url: `${path}?id=${id}` })
return
}
if (a.link && /^\/.+/.test(a.link)) {
uni.navigateTo({ url: a.link })
}
}
onLoad((opts) => {
if (opts && opts.category) {
categoryTarget.value = decodeURIComponent(opts.category)
title.value = categoryTarget.value
uni.setNavigationBarTitle({ title: categoryTarget.value })
}
loadData()
})
// 分享功能
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
onShareAppMessage(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: `${title.value || '精彩活动'} - 柯大鸭潮玩`,
path: `/pages/index/index?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
onShareTimeline(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: `${title.value || '精彩活动'} - 柯大鸭潮玩`,
query: `invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: $bg-page;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: $spacing-lg;
}
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $spacing-md;
}
.activity-item {
background: #fff;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-sm;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
&:active {
transform: scale(0.98);
box-shadow: $shadow-xs;
}
}
.thumb-box {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
height: 0;
background: $bg-secondary;
}
.thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
background: linear-gradient(90deg, $bg-secondary 25%, #e8e8e8 50%, $bg-secondary 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.tag-hot {
position: absolute;
top: 16rpx; left: 16rpx;
background: rgba(255, 69, 58, 0.9);
color: #fff;
font-size: 20rpx;
padding: 6rpx 14rpx;
border-radius: 8rpx;
font-weight: 800;
letter-spacing: 1rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 69, 58, 0.3);
backdrop-filter: blur(4rpx);
}
.info {
padding: 20rpx 20rpx;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
height: 80rpx;
}
.bottom-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.price-text {
font-size: 22rpx;
color: $text-secondary;
max-width: 70%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-go {
background: $brand-primary;
color: #fff;
font-size: 20rpx;
font-weight: 900;
padding: 6rpx 20rpx;
border-radius: 100rpx;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.9);
background: darken($brand-primary, 5%);
}
}
.loading-wrap {
display: flex; justify-content: center; padding: 100rpx;
}
.spinner {
width: 48rpx; height: 48rpx;
border: 4rpx solid $border-color-light;
border-top-color: $brand-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
animation: fadeInUp 0.5s ease-out;
}
.empty-img {
width: 240rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
color: $text-secondary;
font-size: 28rpx;
}
/* ============================================
动画增强
============================================ */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* 卡片交错入场 */
@for $i from 1 through 10 {
.activity-item:nth-child(#{$i}) {
animation: fadeInUp 0.4s ease-out #{$i * 0.05}s both;
}
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
</style>