234 lines
5.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/yifanshang/index'
else if (name.includes('无限赏')) path = '/pages/activity/wuxianshang/index'
else if (name.includes('对对碰')) path = '/pages/activity/duiduipeng/index'
else if (name.includes('爬塔')) path = '/pages/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()
})
</script>
<style scoped>
.page {
min-height: 100vh;
background: #F8F8F8;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: 24rpx;
}
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.activity-item {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
}
.thumb-box {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
height: 0;
background: #f0f0f0;
}
.thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.tag-hot {
position: absolute;
top: 12rpx; left: 12rpx;
background: #333;
color: #FFD700;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 800;
}
.info {
padding: 20rpx 16rpx;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.name {
font-size: 28rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 20rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 76rpx;
line-height: 1.35;
}
.bottom-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-text {
font-size: 24rpx;
color: #FF4D4F;
font-weight: 700;
}
.btn-go {
background: #1A1A1A;
color: #FFD700;
font-size: 24rpx;
font-weight: 900;
padding: 8rpx 24rpx;
border-radius: 999rpx;
}
.loading-wrap {
display: flex; justify-content: center; padding: 100rpx;
}
.spinner {
width: 48rpx; height: 48rpx;
border: 4rpx solid #ddd; border-top-color: #FF9F43;
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;
}
.empty-img {
width: 240rpx;
margin-bottom: 24rpx;
opacity: 0.4;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
</style>