2026-01-06 17:26:55 +08:00

719 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container">
<!-- 顶部装饰背景 - 漂浮光球 -->
<view class="bg-decoration"></view>
<view class="header-area">
<view class="page-title">我的道具卡</view>
<view class="page-subtitle">My Item Cards</view>
</view>
<!-- Tab栏 - 毛玻璃风格 -->
<view class="tab-bar glass-card">
<view class="tab-item" :class="{ active: currentTab === 0 }" @click="switchTab(0)">
<text class="tab-text">未使用</text>
<view class="tab-indicator" v-if="currentTab === 0"></view>
</view>
<view class="tab-item" :class="{ active: currentTab === 1 }" @click="switchTab(1)">
<text class="tab-text">已使用</text>
<view class="tab-indicator" v-if="currentTab === 1"></view>
</view>
<view class="tab-item" :class="{ active: currentTab === 2 }" @click="switchTab(2)">
<text class="tab-text">已过期</text>
<view class="tab-indicator" v-if="currentTab === 2"></view>
</view>
</view>
<!-- 内容区 -->
<scroll-view
scroll-y
class="content-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 加载状态 -->
<view v-if="loading && list.length === 0" class="loading-state">
<view class="spinner"></view>
<text>加载中...</text>
</view>
<!-- 空状态 -->
<view v-else-if="list.length === 0" class="empty-state">
<text class="empty-icon">🃏</text>
<text class="empty-text">{{ getEmptyText() }}</text>
</view>
<!-- 道具卡列表 -->
<view v-else class="item-list">
<view
v-for="(item, index) in list"
:key="item.id || index"
class="item-ticket"
:class="{ 'used': currentTab === 1, 'expired': currentTab === 2 }"
:style="{ animationDelay: `${index * 0.05}s` }"
>
<!-- 左侧图标区域 -->
<view class="ticket-left">
<view class="card-icon-wrap">
<text class="card-icon">{{ getCardIcon(item.type || item.name) }}</text>
</view>
</view>
<!-- 中间分割线 -->
<view class="ticket-divider">
<view class="divider-notch top"></view>
<view class="divider-dash"></view>
<view class="divider-notch bottom"></view>
</view>
<!-- 右侧信息区域 -->
<view class="ticket-right">
<view class="card-info">
<text class="card-name">{{ item.name || item.title || '道具卡' }}</text>
<text class="card-desc">{{ item.description || item.rules || '可在抽奖时使用' }}</text>
<view class="usage-info" v-if="currentTab === 1">
<text class="card-use-time" v-if="item.used_at">使用时间{{ formatDateTime(item.used_at) }}</text>
<view class="usage-detail" v-if="item.used_activity_name">
<text class="detail-label">使用于</text>
<text class="detail-val">{{ item.used_activity_name }}</text>
<text class="detail-val" v-if="item.used_issue_number"> - 期号 {{ item.used_issue_number }}</text>
</view>
<view class="usage-detail" v-if="item.used_reward_name">
<text class="detail-label">效果</text>
<text class="detail-val highlight">{{ item.used_reward_name }}</text>
</view>
</view>
<view class="usage-info" v-if="currentTab === 2">
<text class="card-use-time" v-if="item.valid_end">过期时间{{ formatDateTime(item.valid_end) }}</text>
</view>
<!-- Unused State: Show Validity -->
<view class="usage-info" v-if="currentTab === 0">
<text class="card-use-time" v-if="item.valid_end">有效期至{{ formatDateTime(item.valid_end) }}</text>
</view>
</view>
<!-- 优化后的按钮位置 -->
<view class="ticket-action-wrapper" v-if="currentTab === 0">
<view class="use-btn" @click.stop="onUseCard(item)">
<text class="btn-text">去使用</text>
<view class="btn-shine"></view>
</view>
</view>
<view class="card-used-badge" v-else-if="currentTab === 1">
<text class="used-text">已使用</text>
</view>
<view class="card-used-badge expired" v-else-if="currentTab === 2">
<text class="used-text">已过期</text>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loading && list.length > 0" class="loading-more">
<view class="spinner"></view>
<text>加载更多...</text>
</view>
<view v-else-if="!hasMore && list.length > 0" class="no-more">- 到底啦 -</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getItemCards } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const list = ref([])
const loading = ref(false)
const isRefreshing = ref(false)
const currentTab = ref(0)
const page = ref(1)
const pageSize = 20
const hasMore = ref(true)
// 获取用户ID
function getUserId() {
return uni.getStorageSync('user_id')
}
function getEmptyText() {
if (currentTab.value === 0) return '暂无可用道具卡'
if (currentTab.value === 1) return '暂无使用记录'
if (currentTab.value === 2) return '暂无过期道具卡'
return ''
}
// 检查登录状态
function checkAuth() {
const token = uni.getStorageSync('token')
const userId = getUserId()
if (!token || !userId) {
uni.showModal({
title: '提示',
content: '请先登录',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return false
}
return true
}
// 格式化日期时间
function formatDateTime(t) {
if (!t) return ''
const d = new Date(t)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const ss = String(d.getSeconds()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`
}
// 获取卡片图标
function getCardIcon(type) {
const t = String(type || '').toLowerCase()
if (t.includes('透视')) return '👁️'
if (t.includes('提示')) return '💡'
if (t.includes('重置')) return '🔄'
if (t.includes('翻倍')) return '✨'
if (t.includes('保护')) return '🛡️'
return '🃏'
}
// 切换Tab
function switchTab(tab) {
if (currentTab.value === tab) return
vibrateShort()
currentTab.value = tab
list.value = []
page.value = 1
hasMore.value = true
fetchData()
}
// 下拉刷新
async function onRefresh() {
isRefreshing.value = true
page.value = 1
hasMore.value = true
await fetchData(false)
isRefreshing.value = false
}
// 加载更多
async function loadMore() {
if (loading.value || !hasMore.value) return
await fetchData(true)
}
// 获取数据
async function fetchData(append = false) {
if (!checkAuth()) return
if (loading.value) return
loading.value = true
try {
const userId = getUserId()
// status: 1=unused, 2=used, 3=expired
const status = currentTab.value === 0 ? 1 : (currentTab.value === 1 ? 2 : 3)
const res = await getItemCards(userId, status, page.value, pageSize)
let items = Array.isArray(res) ? res : (res.list || res.data || [])
if (append) {
list.value = [...list.value, ...items]
} else {
list.value = items
}
if (items.length < pageSize) {
hasMore.value = false
} else {
page.value++
}
} catch (e) {
console.error('获取道具卡失败:', e)
hasMore.value = false
} finally {
loading.value = false
}
}
// 去使用道具卡
function onUseCard(item) {
vibrateShort()
// #ifdef MP-TOUTIAO
// 抖音平台跳转到商城
uni.switchTab({
url: '/pages/shop/index'
})
// #endif
// #ifndef MP-TOUTIAO
// 道具卡通常去首页或指定的活动页
uni.switchTab({
url: '/pages/index/index'
})
// #endif
}
onLoad(() => {
fetchData()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: $bg-page;
position: relative;
overflow: hidden;
}
.header-area {
padding: $spacing-xl $spacing-lg;
padding-top: calc(env(safe-area-inset-top) + 20rpx);
position: relative;
z-index: 1;
}
.page-title {
font-size: 48rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.page-subtitle {
font-size: 24rpx;
color: $text-tertiary;
text-transform: uppercase;
letter-spacing: 2rpx;
font-weight: 600;
}
/* Tab栏 */
.tab-bar {
@extend .glass-card;
display: flex;
margin: 0 $spacing-lg;
padding: 8rpx;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
transition: all 0.3s;
}
.tab-text {
font-size: 28rpx;
color: $text-sub;
font-weight: 500;
}
.tab-item.active .tab-text {
color: $text-main;
font-weight: 700;
}
.tab-indicator {
position: absolute;
bottom: 4rpx;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
background: $brand-primary;
border-radius: 6rpx;
}
/* 内容滚动区 */
.content-scroll {
height: calc(100vh - 280rpx);
padding: $spacing-lg;
position: relative;
z-index: 1;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: $text-tertiary;
font-size: 26rpx;
gap: 16rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-text {
color: $text-tertiary;
font-size: 28rpx;
}
/* 道具卡列表 */
.item-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 票券式卡片 */
.item-ticket {
background: #fff;
border-radius: 16rpx;
display: flex;
overflow: hidden;
box-shadow: $shadow-sm;
position: relative;
animation: fadeInUp 0.5s ease-out backwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.ticket-left {
width: 180rpx;
background: linear-gradient(135deg, #E6F7FF, #fff);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
position: relative;
}
.card-icon-wrap {
width: 90rpx;
height: 90rpx;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 150, 250, 0.1);
}
.card-icon {
font-size: 48rpx;
}
.card-count-badge {
position: absolute;
top: 12rpx;
right: 12rpx;
background: rgba(0, 150, 250, 0.1);
padding: 2rpx 10rpx;
border-radius: 100rpx;
}
.count-num {
font-size: 20rpx;
font-weight: 700;
color: #0096FA;
}
/* 分割线 */
.ticket-divider {
width: 30rpx;
position: relative;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.divider-notch {
width: 24rpx;
height: 24rpx;
background: $bg-page;
border-radius: 50%;
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 2;
}
.divider-notch.top {
top: -12rpx;
}
.divider-notch.bottom {
bottom: -12rpx;
}
.divider-dash {
width: 0;
height: 80%;
border-left: 2rpx dashed #eee;
}
.ticket-right {
flex: 1;
padding: 24rpx;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
position: relative;
}
.card-info {
display: flex;
flex-direction: column;
padding-right: 130rpx;
}
.card-name {
font-size: $font-md;
font-weight: 700;
margin-bottom: 8rpx;
color: $text-main;
}
.card-desc {
font-size: $font-xs;
color: $text-sub;
line-height: 1.4;
}
.card-desc {
font-size: $font-xs;
color: $text-sub;
line-height: 1.4;
}
.usage-info {
margin-top: 16rpx;
padding-top: 12rpx;
border-top: 1rpx dashed #eee;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-use-time {
font-size: 18rpx;
color: $text-tertiary;
}
.usage-detail {
font-size: 20rpx;
color: $text-sub;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.detail-label {
color: $text-tertiary;
}
.detail-val {
font-weight: 500;
margin-left: 4rpx;
&.highlight {
color: $brand-primary;
font-weight: 700;
}
}
/* 按钮样式 */
.ticket-action-wrapper {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
.use-btn {
background: linear-gradient(135deg, #4facfe, #00f2fe);
padding: 12rpx 28rpx;
border-radius: 40rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 150, 250, 0.2);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
&:active {
transform: scale(0.92);
box-shadow: 0 2rpx 10rpx rgba(0, 150, 250, 0.15);
}
}
.btn-text {
color: #fff;
font-size: 24rpx;
font-weight: 700;
letter-spacing: 2rpx;
position: relative;
z-index: 2;
}
.btn-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: skewX(-25deg);
animation: shine 3s infinite;
}
@keyframes shine {
0% { left: -100%; }
20%, 100% { left: 150%; }
}
.card-used-badge {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
background: #F5F5F5;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.used-text {
font-size: 22rpx;
color: $text-tertiary;
}
/* 已使用状态 */
.item-ticket.used {
.ticket-left {
background: #f9f9f9;
}
.card-icon-wrap {
filter: grayscale(1);
opacity: 0.5;
}
.card-name {
color: $text-sub;
}
.card-desc {
color: $text-tertiary;
}
}
/* 已过期状态 */
.item-ticket.expired {
.ticket-left {
background: #fdfdfd;
}
.card-icon-wrap {
filter: grayscale(1) sepia(0.2);
opacity: 0.4;
}
.card-name {
color: $text-tertiary;
text-decoration: line-through;
}
.card-desc {
color: $text-tertiary;
}
.card-used-badge.expired {
background: #f0f0f0;
.used-text {
color: #999;
}
}
}
/* 加载动画 */
.spinner {
width: 28rpx;
height: 28rpx;
border: 3rpx solid $bg-secondary;
border-top-color: $text-tertiary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 加载更多 */
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
color: $text-tertiary;
font-size: 24rpx;
gap: 12rpx;
}
.no-more {
text-align: center;
padding: 40rpx 0;
color: $text-tertiary;
font-size: 24rpx;
opacity: 0.6;
}
</style>