bindbox-mini/pages/mine/index.vue
2025-12-15 11:02:37 +08:00

899 lines
28 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="wrap">
<view class="header">
<image class="avatar" :src="avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="profile">
<view class="nickname">{{ nickname || '未登录' }}</view>
<view class="userid">ID{{ userId || '-' }}</view>
</view>
<view class="refresh-btn" @click="refresh" :class="{ 'loading': loading }">
<text class="refresh-icon"></text>
</view>
</view>
<view class="info">
<view class="info-item">
<text class="info-label">手机号</text>
<text class="info-value">{{ phoneNumber || '未绑定' }}</text>
</view>
<view class="info-item">
<text class="info-label">邀请码</text>
<view class="invite-value-wrapper">
<text class="info-value">{{ inviteCode || '-' }}</text>
<button class="share-btn" open-type="share" v-if="inviteCode">分享</button>
</view>
</view>
</view>
<view class="stats">
<view class="stat" @click="showPointsPopup">
<view class="stat-label">积分</view>
<view class="stat-value">{{ pointsBalance }}</view>
</view>
<view class="stat" @click="showCouponsPopup">
<view class="stat-label">优惠券</view>
<view class="stat-value">{{ stats.coupon_count || 0 }}</view>
</view>
<view class="stat" @click="showItemCardsPopup">
<view class="stat-label">道具卡</view>
<view class="stat-value">{{ stats.item_card_count || 0 }}</view>
</view>
</view>
<view v-if="error" class="error">{{ error }}</view>
</view>
<view class="orders">
<view class="orders-title">我的订单</view>
<view class="orders-cats">
<view class="orders-cat" @click="toOrders('pending')">
<view class="orders-cat-title">待付款</view>
</view>
<view class="orders-cat" @click="toOrders('completed')">
<view class="orders-cat-title">已完成</view>
</view>
</view>
</view>
<view class="addresses">
<view class="addresses-title">地址管理</view>
<view class="addresses-entry" @click="toAddresses">
<text class="addresses-text">管理收货地址</text>
<text class="addresses-arrow"></text>
</view>
</view>
<view class="addresses">
<view class="addresses-title">使用帮助</view>
<view class="addresses-entry" @click="toHelp">
<text class="addresses-text">查看协议与说明</text>
<text class="addresses-arrow"></text>
</view>
</view>
<view class="addresses">
<view class="addresses-title">任务中心</view>
<view class="addresses-entry" @click="showTasksPopup">
<text class="addresses-text">查看我的任务</text>
<text class="addresses-arrow"></text>
</view>
</view>
<!-- 积分明细弹窗 -->
<view class="popup-mask" v-if="pointsVisible" @tap="closePointsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">积分明细</text>
<text class="close-btn" @tap="closePointsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMorePoints">
<view v-if="pointsLoading && pointsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!pointsList || pointsList.length === 0" class="status-text">暂无积分记录</view>
<view v-for="(item, index) in pointsList" :key="index" class="point-item">
<view class="point-left">
<text class="point-desc">{{ getActionText(item.action) }}</text>
<text class="point-time">{{ formatDate(item.created_at) }}</text>
</view>
<view class="point-right">
<text class="point-amount" :class="{ 'positive': Number(item.points) > 0, 'negative': Number(item.points) < 0 }">
{{ Number(item.points) > 0 ? '+' : '' }}{{ item.points }}
</text>
</view>
</view>
<view v-if="pointsLoading && pointsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!pointsHasMore && pointsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 优惠券弹窗 -->
<view class="popup-mask" v-if="couponsVisible" @tap="closeCouponsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的优惠券</text>
<text class="close-btn" @tap="closeCouponsPopup">×</text>
</view>
<view class="popup-tabs">
<view class="popup-tab" :class="{ active: couponsTab === 0 }" @tap="switchCouponsTab(0)">未使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 1 }" @tap="switchCouponsTab(1)">已使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 2 }" @tap="switchCouponsTab(2)">去兑换</view>
</view>
<!-- 兑换界面 -->
<view v-if="couponsTab === 2" class="redeem-container">
<input class="redeem-input" v-model="redeemCode" placeholder="请输入兑换码" />
<button class="redeem-btn" @tap="handleRedeem">立即兑换</button>
</view>
<!-- 列表界面 -->
<scroll-view v-else scroll-y class="points-list" @scrolltolower="loadMoreCoupons">
<view v-if="couponsLoading && couponsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!couponsList || couponsList.length === 0" class="status-text">暂无优惠券</view>
<view v-for="(item, index) in couponsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || '优惠券' }}</text>
<text class="coupon-desc">{{ item.rules || item.description || '' }}</text>
<text class="coupon-time">有效期至:{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<view v-if="couponsTab === 0 && (item.remaining !== undefined && item.remaining !== null)" class="coupon-amount-wrapper">
<text class="symbol">¥</text>
<text class="amount-value">{{ (Number(item.remaining || 0) / 100).toFixed(2) }}</text>
</view>
<text class="coupon-status">{{ couponsTab === 0 ? '未使用' : '已使用/过期' }}</text>
</view>
</view>
<view v-if="couponsLoading && couponsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!couponsHasMore && couponsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 道具卡弹窗 -->
<view class="popup-mask" v-if="itemCardsVisible" @tap="closeItemCardsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的道具卡</text>
<text class="close-btn" @tap="closeItemCardsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list">
<view v-if="itemCardsLoading && itemCardsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!itemCardsList || itemCardsList.length === 0" class="status-text">暂无道具卡</view>
<view v-for="(item, index) in itemCardsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || item.title || '道具卡' }}</text>
<text class="coupon-desc">{{ item.description || item.rules || '' }}</text>
<text class="coupon-time" v-if="item.valid_end || item.end_time">有效期至:{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<text class="coupon-status">数量:{{ item.remaining ?? item.count ?? 1 }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 任务中心弹窗 -->
<view class="popup-mask" v-if="tasksVisible" @tap="closeTasksPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">任务中心</text>
<text class="close-btn" @tap="closeTasksPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMoreTasks">
<view v-if="tasksLoading && tasksList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!tasksList || tasksList.length === 0" class="status-text">暂无任务</view>
<view v-for="(task, idx) in tasksList" :key="task.id || idx" class="task-item">
<view class="task-left">
<text class="task-name">{{ task.name || '任务' }}</text>
<text class="task-desc">{{ task.description || '' }}</text>
<text class="task-time">{{ formatStamp(task.start_time) }} - {{ formatStamp(task.end_time) }}</text>
<view class="reward-list" v-if="Array.isArray(task.rewards) && task.rewards.length">
<text class="reward-label">奖励</text>
<view class="reward-tags">
<view class="reward-item" v-for="(rw, ri) in task.rewards" :key="ri">
<text class="reward-type">{{ rw.reward_type || '未知' }}</text>
<text class="reward-qty">×{{ rw.quantity || 0 }}</text>
</view>
</view>
</view>
<view class="tier-list" v-if="Array.isArray(task.tiers) && task.tiers.length">
<text class="tier-label">条件</text>
<view class="tier-tags">
<view class="tier-item" v-for="(tr, ti) in task.tiers" :key="ti">
<text class="tier-text">{{ tr.metric || '-' }} {{ tr.operator || '' }} {{ tr.threshold ?? '' }}</text>
</view>
</view>
</view>
</view>
<view class="task-right">
<text class="task-status">{{ formatStatus(task.status) }}</text>
</view>
</view>
<view v-if="tasksLoading && tasksList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!tasksHasMore && tasksList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow, onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { getUserStats, getPointsBalance, getUserPoints, getUserCoupons, redeemCoupon, getItemCards, getTasks } from '../../api/appUser'
const avatar = ref(uni.getStorageSync('avatar') || '')
const nickname = ref(uni.getStorageSync('nickname') || '')
const userId = ref(uni.getStorageSync('user_id') || '')
const phoneNumber = ref(uni.getStorageSync('phone_number') || '')
const inviteCode = ref(uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '')
const pointsBalance = ref(uni.getStorageSync('points_balance') || 0)
const stats = ref(uni.getStorageSync('user_stats') || {})
const loading = ref(false)
const error = ref('')
// 积分弹窗相关状态
const pointsVisible = ref(false)
const pointsList = ref([])
const pointsLoading = ref(false)
const pointsPage = ref(1)
const pointsPageSize = ref(20)
const pointsHasMore = ref(true)
// 优惠券弹窗相关状态
const couponsVisible = ref(false)
const couponsTab = ref(0) // 0: 未使用, 1: 已使用, 2: 兑换
const couponsList = ref([])
const couponsLoading = ref(false)
const couponsPage = ref(1)
const couponsHasMore = ref(true)
const redeemCode = ref('')
// 道具卡弹窗相关状态
const itemCardsVisible = ref(false)
const itemCardsList = ref([])
const itemCardsLoading = ref(false)
const tasksVisible = ref(false)
const tasksList = ref([])
const tasksLoading = ref(false)
const tasksPage = ref(1)
const tasksPageSize = ref(20)
const tasksHasMore = ref(true)
async function refresh() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
const user_id = uni.getStorageSync('user_id')
loading.value = true
error.value = ''
try {
const s = await getUserStats(user_id)
stats.value = s || {}
uni.setStorageSync('user_stats', stats.value)
const b = await getPointsBalance(user_id)
const balance = b && b.balance !== undefined ? b.balance : b
pointsBalance.value = balance || 0
uni.setStorageSync('points_balance', pointsBalance.value)
} catch (e) {
error.value = e && (e.message || e.errMsg) || '数据获取失败'
} finally {
loading.value = false
}
}
function toPoints() {
uni.navigateTo({ url: '/pages/points/index' })
}
function toOrders(status) {
const s = status === 'completed' ? 'completed' : 'pending'
uni.navigateTo({ url: `/pages/orders/index?status=${s}` })
}
function toAddresses() {
uni.navigateTo({ url: '/pages/address/index' })
}
function toHelp() {
uni.navigateTo({ url: '/pages/help/index' })
}
// 积分弹窗逻辑
function showPointsPopup() {
pointsVisible.value = true
// 如果没有数据,加载第一页
if (pointsList.value.length === 0) {
pointsPage.value = 1
pointsHasMore.value = true
loadPoints()
}
}
function closePointsPopup() {
pointsVisible.value = false
}
async function loadPoints() {
if (pointsLoading.value) return
pointsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getUserPoints(user_id, pointsPage.value, pointsPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) {
list = res.list
total = res.total || 0
} else if (res && Array.isArray(res.data)) {
list = res.data
total = res.total || 0
} else if (Array.isArray(res)) {
list = res
total = res.length
}
if (pointsPage.value === 1) {
pointsList.value = list
} else {
pointsList.value = [...pointsList.value, ...list]
}
if (list.length < pointsPageSize.value || (pointsPage.value * pointsPageSize.value >= total && total > 0)) {
pointsHasMore.value = false
} else {
pointsPage.value += 1
}
if (list.length === 0 && pointsPage.value === 1) {
pointsHasMore.value = false
}
} catch (e) {
console.error('Fetch points error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
pointsLoading.value = false
}
}
function loadMorePoints() {
if (pointsHasMore.value && !pointsLoading.value) {
loadPoints()
}
}
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) return dateStr
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${min}`
}
function getActionText(action) {
const map = {
'manual_add': '商品兑换积分',
'manual_sub': '系统扣除',
'register': '注册奖励',
'lottery_cost': '抽奖消耗',
'checkin': '签到奖励'
}
return map[action] || action || '积分变动'
}
// 优惠券弹窗逻辑
function showCouponsPopup() {
couponsVisible.value = true
couponsTab.value = 0
redeemCode.value = ''
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
function closeCouponsPopup() {
couponsVisible.value = false
}
function switchCouponsTab(index) {
if (couponsTab.value === index) return
couponsTab.value = index
if (index !== 2) {
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
}
async function loadCoupons() {
if (couponsLoading.value) return
couponsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const status = couponsTab.value === 0 ? 0 : 1
const res = await getUserCoupons(user_id, status, couponsPage.value, 20)
let list = []
let total = 0
if (res && Array.isArray(res.list)) {
list = res.list
total = res.total || 0
} else if (res && Array.isArray(res.data)) {
list = res.data
total = res.total || 0
} else if (Array.isArray(res)) {
list = res
total = res.length
}
if (couponsPage.value === 1) {
couponsList.value = list
} else {
couponsList.value = [...couponsList.value, ...list]
}
if (list.length < 20 || (total > 0 && couponsList.value.length >= total)) {
couponsHasMore.value = false
} else {
couponsPage.value += 1
}
if (list.length === 0 && couponsPage.value === 1) {
couponsHasMore.value = false
}
} catch (e) {
console.error('Fetch coupons error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
couponsLoading.value = false
}
}
function loadMoreCoupons() {
if (couponsHasMore.value && !couponsLoading.value && couponsTab.value !== 2) {
loadCoupons()
}
}
async function handleRedeem() {
if (!redeemCode.value) {
uni.showToast({ title: '请输入兑换码', icon: 'none' })
return
}
const user_id = uni.getStorageSync('user_id')
try {
await redeemCoupon(user_id, redeemCode.value)
uni.showToast({ title: '兑换成功', icon: 'success' })
redeemCode.value = ''
refresh() // 刷新用户信息,可能增加了卡券
} catch (e) {
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
}
}
// 道具卡弹窗逻辑
function showItemCardsPopup() {
itemCardsVisible.value = true
loadItemCards()
}
function closeItemCardsPopup() {
itemCardsVisible.value = false
}
async function loadItemCards() {
if (itemCardsLoading.value) return
itemCardsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getItemCards(user_id)
let list = []
if (res && Array.isArray(res.list)) {
list = res.list
} else if (res && Array.isArray(res.data)) {
list = res.data
} else if (Array.isArray(res)) {
list = res
}
itemCardsList.value = list
} catch (e) {
console.error('Fetch item cards error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
itemCardsLoading.value = false
}
}
function showTasksPopup() {
tasksVisible.value = true
if (tasksList.value.length === 0) {
tasksPage.value = 1
tasksHasMore.value = true
loadTasks()
}
}
function closeTasksPopup() { tasksVisible.value = false }
async function loadTasks() {
if (tasksLoading.value) return
tasksLoading.value = true
try {
const res = await getTasks(tasksPage.value, tasksPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
if (tasksPage.value === 1) tasksList.value = list
else tasksList.value = [...tasksList.value, ...list]
if (list.length < tasksPageSize.value || (tasksPage.value * tasksPageSize.value >= total && total > 0)) tasksHasMore.value = false
else tasksPage.value += 1
if (list.length === 0 && tasksPage.value === 1) tasksHasMore.value = false
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
tasksLoading.value = false
}
}
function loadMoreTasks() {
if (tasksHasMore.value && !tasksLoading.value) loadTasks()
}
function formatStamp(ts) {
if (ts === undefined || ts === null || ts === '') return ''
const n = Number(ts)
if (isNaN(n)) return ''
const ms = n > 1e12 ? n : n * 1000
const d = new Date(ms)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const da = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${da} ${h}:${min}`
}
function formatStatus(s) {
const n = Number(s)
if (n === 1) return '进行中'
if (n === 2) return '已结束'
if (n === 0) return '未开始'
return '未知'
}
onShow(() => {
avatar.value = uni.getStorageSync('avatar') || avatar.value
nickname.value = uni.getStorageSync('nickname') || nickname.value
userId.value = uni.getStorageSync('user_id') || userId.value
phoneNumber.value = uni.getStorageSync('phone_number') || phoneNumber.value
const ui = uni.getStorageSync('user_info') || {}
inviteCode.value = uni.getStorageSync('invite_code') || ui.invite_code || inviteCode.value
pointsBalance.value = uni.getStorageSync('points_balance') || pointsBalance.value
const s = uni.getStorageSync('user_stats')
if (s) stats.value = s
refresh()
})
onLoad(() => {
refresh()
})
onShareAppMessage(() => {
return {
title: '邀请你一起来加入这个宝藏小程序',
path: `/pages/index/index?invite_code=${inviteCode.value}`,
imageUrl: '/static/logo.png'
}
})
</script>
<style scoped>
.wrap { padding: 40rpx }
.header { display: flex; align-items: center; margin-bottom: 24rpx }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background-color: #f5f5f5 }
.profile { margin-left: 20rpx; display: flex; flex-direction: column }
.nickname { font-size: 32rpx }
.userid { margin-top: 6rpx; font-size: 24rpx; color: #999 }
.info { display: flex; flex-direction: column; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.info-item { display: flex; justify-content: space-between; margin-bottom: 12rpx }
.info-label { color: #666; font-size: 24rpx }
.info-value { font-size: 28rpx }
.invite-value-wrapper { display: flex; align-items: center; }
.share-btn {
margin-left: 10rpx;
font-size: 24rpx;
padding: 0 20rpx;
height: 48rpx;
line-height: 48rpx;
background-color: #007AFF;
color: #fff;
border-radius: 24rpx;
}
.stats { display: flex; background: #fafafa; border-radius: 12rpx; padding: 20rpx; justify-content: space-between; margin-bottom: 20rpx }
.stat { flex: 1; align-items: center }
.stat-label { color: #666; font-size: 24rpx }
.stat-value { font-size: 36rpx; margin-top: 8rpx }
.orders { background: #fff; border-radius: 12rpx; padding: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); margin-bottom: 20rpx }
.orders-title { font-size: 30rpx; margin-bottom: 12rpx }
.orders-cats { display: flex; justify-content: space-between }
.orders-cat { flex: 1; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx; margin-right: 12rpx }
.orders-cat:last-child { margin-right: 0 }
.orders-cat-title { font-size: 28rpx; text-align: center }
.addresses { background: #fff; border-radius: 12rpx; padding: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); margin-bottom: 20rpx }
.addresses-title { font-size: 30rpx; margin-bottom: 12rpx }
.addresses-entry { display: flex; justify-content: space-between; align-items: center; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx }
.addresses-text { font-size: 28rpx }
.addresses-arrow { font-size: 28rpx; color: #999 }
.refresh-btn { margin-left: auto; padding: 10rpx }
.refresh-icon { font-size: 40rpx; color: #666; display: block }
.loading .refresh-icon { animation: rotate 1s linear infinite }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.error { color: #e43; margin-top: 20rpx }
/* 积分弹窗样式 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 999;
display: flex;
align-items: flex-end;
}
.popup-content {
width: 100%;
height: 70vh;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
flex-direction: column;
}
.popup-header {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #eee;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
}
.close-btn {
font-size: 40rpx;
color: #999;
padding: 0 10rpx;
}
.points-list {
flex: 1;
overflow-y: auto;
padding: 0 30rpx;
box-sizing: border-box; /* 确保 padding 不增加宽度 */
}
.point-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
width: 100%; /* 确保子项占满 */
}
.point-left {
display: flex;
flex-direction: column;
flex: 1; /* 占据剩余空间 */
min-width: 0; /* 关键:允许 flex 子项缩小,防止内容撑开 */
margin-right: 20rpx;
}
.point-desc {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.point-time {
font-size: 24rpx;
color: #999;
}
.point-right {
flex-shrink: 0; /* 防止被压缩 */
margin-left: auto; /* 靠右对齐 */
text-align: right;
max-width: 40%; /* 限制最大宽度 */
}
.point-amount {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
}
.point-amount.positive {
color: #ff4d4f;
}
.point-amount.negative {
color: #52c41a;
}
.status-text {
text-align: center;
color: #999;
margin-top: 60rpx;
font-size: 28rpx;
}
.loading-more, .no-more {
text-align: center;
color: #999;
padding: 20rpx 0;
font-size: 24rpx;
}
/* 优惠券弹窗样式 */
.popup-tabs {
display: flex;
border-bottom: 1rpx solid #eee;
}
.popup-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.popup-tab.active {
color: #007AFF;
font-weight: bold;
}
.popup-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
.redeem-container {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.redeem-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
margin-bottom: 40rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.redeem-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background-color: #007AFF;
color: #fff;
font-size: 32rpx;
border-radius: 40rpx;
}
.coupon-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
margin: 20rpx 0;
background: #f9f9f9;
border-radius: 12rpx;
border: 1rpx solid #eee;
}
.coupon-left {
display: flex;
flex-direction: column;
flex: 1;
}
.coupon-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.coupon-desc {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
.coupon-time {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.coupon-right {
margin-left: 20rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.coupon-amount-wrapper {
color: #ff4d4f;
margin-bottom: 8rpx;
display: flex;
align-items: baseline;
}
.symbol {
font-size: 24rpx;
font-weight: bold;
}
.amount-value {
font-size: 40rpx;
font-weight: bold;
line-height: 1;
}
.coupon-status {
font-size: 26rpx;
color: #999;
}
.task-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 24rpx; margin: 20rpx 0; background: #f9f9f9; border-radius: 12rpx; border: 1rpx solid #eee }
.task-left { flex: 1; display: flex; flex-direction: column }
.task-name { font-size: 30rpx; font-weight: 700; color: #333 }
.task-desc { margin-top: 6rpx; font-size: 24rpx; color: #666 }
.task-time { margin-top: 6rpx; font-size: 22rpx; color: #999 }
.reward-list { margin-top: 10rpx }
.reward-label, .tier-label { font-size: 24rpx; color: #666 }
.reward-tags, .tier-tags { margin-top: 8rpx; display: flex; flex-wrap: wrap; gap: 8rpx }
.reward-item, .tier-item { background: #fff; border: 1rpx solid #eee; border-radius: 999rpx; padding: 8rpx 12rpx; display: flex; align-items: center; gap: 8rpx }
.reward-type { font-size: 24rpx; color: #333 }
.reward-qty { font-size: 22rpx; color: #999 }
.tier-text { font-size: 24rpx; color: #333 }
.task-right { margin-left: 12rpx; display: flex; align-items: center }
.task-status { font-size: 26rpx; color: #007AFF }
</style>