2025-09-23 19:00:32 +08:00

1058 lines
23 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="my-point-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="积分"
@clickLeft="goBack"
right-text="规则"
@clickRight="showRules"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<!-- 每日签到进度条 -->
<view class="checkin-progress">
<view class="progress-bar">
<view
class="day-circle"
v-for="(day, index) in weekDays"
:key="index"
:class="{ active: day.checked }"
>
<view v-if="day.checked" class="coin-icon"></view>
<view v-else class="empty-icon"></view>
<text class="day-text">{{ day.name }}</text>
<!-- <text class="day-date">{{ day.date ? day.date.slice(5) : '' }}</text> -->
</view>
</view>
<view class="meeting-days">
<text class="meeting-text">今天是我们相识的第{{ meetingDays }}</text>
</view>
</view>
<!-- 积分概览卡片 -->
<view class="points-summary">
<view v-if="loading && totalPoints === 0" class="loading-overlay">
<text class="loading-text">加载中...</text>
</view>
<view v-else class="summary-content">
<view class="summary-item">
<text class="points-number">{{ totalPoints }}</text>
<text class="points-label">积分</text>
</view>
<view class="summary-item">
<text class="consecutive-days">{{ consecutiveDays }}</text>
<text class="consecutive-label">本周连续签到</text>
</view>
<view class="summary-item">
<text class="checkin-count">{{ checkinCount }}</text>
<text class="checkin-label">签到次数</text>
</view>
</view>
</view>
<!-- 交易记录标签页 -->
<view class="transaction-tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'income' }"
@click="switchTab('income')"
>
<text class="tab-text">收益</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'expense' }"
@click="switchTab('expense')"
>
<text class="tab-text">支出</text>
</view>
</view>
<!-- 交易记录列表 -->
<view class="transaction-list">
<view v-if="error" class="error-message">
<text class="error-text">{{ error }}</text>
<view class="retry-btn" @click="retryLoad">重试</view>
</view>
<view v-else-if="loading && getCurrentList().length === 0" class="loading-message">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="getCurrentList().length === 0" class="empty-message">
<text class="empty-text">暂无{{ activeTab === 'income' ? '收益' : '支出' }}记录</text>
</view>
<view v-else>
<view
class="transaction-item"
v-for="(item, index) in getCurrentList()"
:key="index"
>
<view class="transaction-info">
<text class="transaction-type">{{ item.type }}</text>
<text class="transaction-time">{{ item.time }}</text>
</view>
<view class="transaction-amount">
<text class="amount-text" :class="{
positive: item.amount > 0,
negative: item.amount < 0
}">
{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}
</text>
</view>
</view>
<!-- 分页信息 -->
<!-- <view class="pagination-info">
<text class="pagination-text">
{{ getCurrentPagination().currentPage }}{{ getCurrentPagination().totalPages }}
{{ getCurrentPagination().totalRows }}条记录
</text>
</view> -->
<!-- 加载更多按钮 -->
<view v-if="getCurrentPagination().hasMore" class="load-more-container">
<view class="load-more-btn" @click="loadMore" :class="{ loading: loading }">
<text v-if="loading" class="loading-text">加载中...</text>
<text v-else class="load-more-text">加载更多</text>
</view>
</view>
</view>
</view>
<!-- 浮动签到按钮 -->
<!-- <view class="floating-checkin" @click="performCheckin" :class="{ loading: loading }">
<view class="checkin-icon">
<text class="calendar-icon">📅</text>
<text class="checkmark"></text>
</view>
<view class="checkin-text">
<text class="checkin-label">{{ loading ? '签到中...' : '点击签到' }}</text>
<text class="checkin-reward">+2</text>
</view>
</view> -->
<!-- 底部导航 -->
<view class="bottom-nav">
<view class="nav-item" @click="goToPointsCoupon">
<!-- <view class="nav-icon">🎫</view> -->
<text class="nav-text">积分券</text>
</view>
<view class="nav-item active" @click="goToBuyPoints">
<!-- <view class="nav-icon">📷</view> -->
<text class="nav-text">购买积分</text>
</view>
<view class="nav-item" @click="goToPointsMall">
<!-- <view class="nav-icon">🛍</view> -->
<text class="nav-text">积分商城</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import api from '@/api/api.js';
// 响应式数据
const activeTab = ref('income');
const totalPoints = ref(0); // 初始化为0等待接口数据
const consecutiveDays = ref(0); // 连续签到天数
const checkinCount = ref(0); // 总签到次数
const meetingDays = ref(0); // 相识天数
const loading = ref(false);
const error = ref(null);
// 分页相关数据 - 收益
const incomeCurrentPage = ref(1);
const incomePageSize = ref(10);
const incomeTotalRows = ref(0);
const incomeTotalPages = ref(0);
// 分页相关数据 - 支出
const expenseCurrentPage = ref(1);
const expensePageSize = ref(10);
const expenseTotalRows = ref(0);
const expenseTotalPages = ref(0);
// 一周七天数据 - 初始化为未签到状态
const weekDays = ref([
{ name: '日', checked: false, date: '' },
{ name: '一', checked: false, date: '' },
{ name: '二', checked: false, date: '' },
{ name: '三', checked: false, date: '' },
{ name: '四', checked: false, date: '' },
{ name: '五', checked: false, date: '' },
{ name: '六', checked: false, date: '' }
]);
// 交易记录数据 - 初始化为空数组,等待接口数据
const transactionList = ref([]); // 收益记录
const expenseList = ref([]); // 支出记录
// 方法
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
}
});
};
const showRules = () => {
uni.showToast({
title: '积分规则',
icon: 'none'
});
};
const switchTab = (tab) => {
activeTab.value = tab;
// 根据tab切换不同的数据
if (tab === 'income') {
// 显示收益记录
if (transactionList.value.length === 0) {
getBonusPointsList(1, false);
}
} else if (tab === 'expense') {
// 显示支出记录
if (expenseList.value.length === 0) {
getBonusPointsPayList(1, false);
}
}
};
const performCheckin = () => {
if (loading.value) return;
loading.value = true;
// 执行签到逻辑
setTimeout(() => {
uni.showToast({
title: '签到成功 +2积分',
icon: 'none'
});
// 签到成功后刷新数据
getMyBonusPoints();
getBonusPointsList(); // 刷新积分记录列表
loading.value = false;
}, 1000);
};
// 底部导航方法
const goToPointsCoupon = () => {
uni.navigateTo({
url: '/pages_goods/coupon/coupon'
});
// uni.showToast({
// title: '积分券功能开发中',
// icon: 'none'
// });
};
const goToBuyPoints = () => {
uni.navigateTo({
url: '/pages_app/buyPoint/buyPoint'
});
};
const goToPointsMall = () => {
uni.navigateTo({
url: '/pages_goods/pointMall/pointMall'
});
};
const retryLoad = () => {
error.value = null;
loading.value = true;
// 重新加载当前标签页的数据
refreshCurrentTab();
};
// 根据当前标签页获取对应的数据列表
const getCurrentList = () => {
return activeTab.value === 'income' ? transactionList.value : expenseList.value;
};
// 获取当前标签页的分页信息
const getCurrentPagination = () => {
if (activeTab.value === 'income') {
return {
currentPage: incomeCurrentPage.value,
totalPages: incomeTotalPages.value,
totalRows: incomeTotalRows.value,
hasMore: incomeCurrentPage.value < incomeTotalPages.value
};
} else {
return {
currentPage: expenseCurrentPage.value,
totalPages: expenseTotalPages.value,
totalRows: expenseTotalRows.value,
hasMore: expenseCurrentPage.value < expenseTotalPages.value
};
}
};
// 加载更多数据
const loadMore = async () => {
const pagination = getCurrentPagination();
if (!pagination.hasMore || loading.value) return;
const nextPage = pagination.currentPage + 1;
if (activeTab.value === 'income') {
await getBonusPointsList(nextPage, true);
} else {
await getBonusPointsPayList(nextPage, true);
}
};
// 刷新当前标签页数据
const refreshCurrentTab = async () => {
if (activeTab.value === 'income') {
await getBonusPointsList(1, false);
} else {
await getBonusPointsPayList(1, false);
}
};
const getMyBonusPoints = async () => {
try {
loading.value = true;
const res = await api.myBonusPoints();
console.log('积分数据接口返回:', res);
if (res && res.code === 200) {
// 更新积分数据
totalPoints.value = res.data.totalPoints;
consecutiveDays.value = res.data.continuous_day;
checkinCount.value = res.data.totalDay;
meetingDays.value = res.data.gdxzday;
// 更新本周签到状态
updateWeekDaysStatus(res.data.thisWeek);
console.log('数据更新完成:', {
totalPoints: totalPoints.value,
consecutiveDays: consecutiveDays.value,
checkinCount: checkinCount.value,
meetingDays: meetingDays.value
});
} else {
console.error('接口返回错误:', res);
uni.showToast({
title: res?.msg || '获取积分数据失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取积分数据异常:', error);
uni.showToast({
title: '网络异常,请重试',
icon: 'none'
});
} finally {
loading.value = false;
}
};
const updateWeekDaysStatus = (checkedDates) => {
// 获取本周的日期范围
const now = new Date();
const currentDay = now.getDay(); // 当前是星期几 (0-6)
const currentDate = now.getDate(); // 当前日期
// 计算本周的起始日期(周日)
const startOfWeek = new Date(now);
startOfWeek.setDate(currentDate - currentDay);
// 重置所有天数为未签到状态,并设置对应的日期
weekDays.value.forEach((day, index) => {
day.checked = false;
day.date = '';
// 计算对应的日期
const dayDate = new Date(startOfWeek);
dayDate.setDate(startOfWeek.getDate() + index);
day.date = dayDate.toISOString().split('T')[0]; // 格式化为 YYYY-MM-DD
});
// 根据接口返回的日期设置签到状态
if (checkedDates && checkedDates.length > 0) {
checkedDates.forEach(dateStr => {
// 找到对应的星期几并设置为已签到
const targetDate = new Date(dateStr);
const targetDateStr = targetDate.toISOString().split('T')[0];
// 在 weekDays 中找到对应的日期并设置为已签到
const dayIndex = weekDays.value.findIndex(day => day.date === targetDateStr);
if (dayIndex !== -1) {
weekDays.value[dayIndex].checked = true;
}
});
}
console.log('本周签到状态更新完成:', weekDays.value);
};
const getBonusPointsList = async (page = 1, isLoadMore = false) => {
try {
// 只有在列表为空时才显示加载状态
if (transactionList.value.length === 0 && !isLoadMore) {
loading.value = true;
}
const res = await api.bonusPointsList({page: page});
console.log('积分收益接口返回:', res);
if (res && res.code === 200) {
// 处理积分记录列表数据
const listData = res.data.list || [];
// 转换数据格式以匹配现有的transactionList结构
const formattedList = listData.map(item => ({
type: item.score_type_name,
time: item.create_date,
amount: item.score
}));
// 更新交易记录列表
if (isLoadMore) {
transactionList.value = [...transactionList.value, ...formattedList];
} else {
transactionList.value = formattedList;
}
// 更新分页信息
incomeCurrentPage.value = res.data.pageNumber;
incomePageSize.value = res.data.pageSize;
incomeTotalRows.value = res.data.totalRow;
incomeTotalPages.value = res.data.totalPage;
console.log('积分记录列表更新完成:', {
total: res.data.totalRow,
pageSize: res.data.pageSize,
currentPage: res.data.pageNumber,
list: formattedList
});
} else {
console.error('积分收益接口返回错误:', res);
error.value = res?.msg || '获取积分记录失败';
uni.showToast({
title: res?.msg || '获取积分记录失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取积分记录异常:', error);
error.value = '网络异常,请重试';
uni.showToast({
title: '网络异常,请重试',
icon: 'none'
});
} finally {
loading.value = false;
}
};
const getBonusPointsPayList = async (page = 1, isLoadMore = false) => {
try {
const res = await api.bonusPointsPayList({page: page});
console.log('积分支出接口返回:', res);
if (res && res.code === 200) {
// 处理积分支出记录列表数据
const listData = res.data.list || [];
// 转换数据格式以匹配现有的transactionList结构
const formattedList = listData.map(item => ({
type: item.score_type_name,
time: item.create_date,
amount: item.score // 支出为负数
}));
// 更新支出记录列表
if (isLoadMore) {
expenseList.value = [...expenseList.value, ...formattedList];
} else {
expenseList.value = formattedList;
}
// 更新分页信息
expenseCurrentPage.value = res.data.pageNumber;
expensePageSize.value = res.data.pageSize;
expenseTotalRows.value = res.data.totalRow;
expenseTotalPages.value = res.data.totalPage;
console.log('积分支出记录列表更新完成:', {
total: res.data.totalRow,
pageSize: res.data.pageSize,
currentPage: res.data.pageNumber,
list: formattedList
});
} else {
console.error('积分支出接口返回错误:', res);
}
} catch (error) {
console.error('获取积分支出异常:', error);
}
};
onMounted(() => {
// 页面加载时的初始化逻辑
console.log('页面加载完成');
getMyBonusPoints();
getBonusPointsList(1, false);
getBonusPointsPayList(1, false);
});
</script>
<style scoped>
.my-point-page {
min-height: 100vh;
background-color: #f5f5f5;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* 为底部导航留出空间 */
padding-bottom: 160rpx;
}
/* 每日签到进度条 */
.checkin-progress {
background-color: #ffffff;
padding: 30rpx;
}
.progress-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.day-circle {
display: flex;
flex-direction: column;
align-items: center;
width: 80rpx;
transition: transform 0.3s ease;
}
.day-circle:hover {
transform: scale(1.05);
}
.day-circle.active {
color: #e74c3c;
}
.coin-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #eee;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
font-size: 24rpx;
position: relative;
}
.coin-icon::before {
content: '🪙';
font-size: 32rpx;
}
.empty-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #eee;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
font-size: 24rpx;
position: relative;
}
.empty-icon::before {
content: '⚪';
font-size: 32rpx;
}
.day-text {
font-size: 24rpx;
color: #666;
margin-bottom: 5rpx;
}
.day-date {
font-size: 20rpx;
color: #999;
}
.meeting-days {
text-align: center;
padding: 20rpx 0;
}
.meeting-text {
font-size: 26rpx;
color: #999;
}
/* 积分概览卡片 */
.points-summary {
background-color: #ffffff;
margin: 20rpx 30rpx;
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex !important;
flex-direction: row !important;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
box-sizing: border-box;
/* 确保flexbox布局正常工作 */
flex-wrap: nowrap;
overflow: visible;
}
.summary-content {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.points-summary:hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
}
.summary-item {
display: flex !important;
flex-direction: column !important;
align-items: center;
flex: 1;
min-width: 0;
text-align: center;
}
.points-number {
font-size: 48rpx;
font-weight: bold;
color: #e74c3c !important;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 4rpx rgba(231, 76, 60, 0.2);
display: block;
width: 100%;
text-align: center;
}
.consecutive-days {
font-size: 48rpx;
font-weight: bold;
color: #e74c3c !important;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 4rpx rgba(231, 76, 60, 0.2);
display: block;
width: 100%;
text-align: center;
}
.checkin-count {
font-size: 48rpx;
font-weight: bold;
color: #e74c3c !important;
margin-bottom: 10rpx;
text-shadow: 0 2rpx 4rpx rgba(231, 76, 60, 0.2);
display: block;
width: 100%;
text-align: center;
}
.points-label,
.consecutive-label,
.checkin-label {
font-size: 24rpx;
color: #999 !important;
display: block;
width: 100%;
text-align: center;
}
/* 加载覆盖层 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
border-radius: 20rpx;
z-index: 10;
}
.loading-overlay .loading-text {
font-size: 28rpx;
color: #666;
}
/* 交易记录标签页 */
.transaction-tabs {
background-color: #ffffff;
margin: 20rpx 30rpx;
border-radius: 20rpx;
display: flex;
overflow: hidden;
}
.tab-item {
flex: 1;
padding: 30rpx;
text-align: center;
position: relative;
transition: all 0.3s ease;
cursor: pointer;
}
.tab-item:hover {
background-color: #f8f8f8;
}
.tab-item.active {
background-color: #e74c3c;
}
.tab-item.active .tab-text {
color: #ffffff;
}
.tab-text {
font-size: 28rpx;
color: #666;
}
/* 交易记录列表 */
.transaction-list {
background-color: #ffffff;
margin: 0 30rpx 20rpx;
border-radius: 20rpx;
padding: 20rpx 0;
}
.error-message,
.loading-message,
.empty-message {
text-align: center;
padding: 60rpx 30rpx;
}
.error-text {
color: #e74c3c;
font-size: 28rpx;
margin-bottom: 20rpx;
display: block;
}
.retry-btn {
background-color: #e74c3c;
color: #ffffff;
padding: 20rpx 40rpx;
border-radius: 10rpx;
font-size: 26rpx;
display: inline-block;
cursor: pointer;
transition: background-color 0.3s ease;
}
.retry-btn:hover {
background-color: #c0392b;
}
.loading-text,
.empty-text {
color: #999;
font-size: 28rpx;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.3s ease;
}
.transaction-item:hover {
background-color: #f8f8f8;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.transaction-type {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.transaction-time {
font-size: 24rpx;
color: #999;
}
.transaction-amount {
display: flex;
align-items: center;
}
.amount-text {
font-size: 32rpx;
font-weight: bold;
color: #e74c3c;
text-shadow: 0 1rpx 2rpx rgba(231, 76, 60, 0.2);
}
.amount-text.positive {
color: #27ae60;
}
.amount-text.negative {
color: #e74c3c;
}
/* 分页信息 */
.pagination-info {
text-align: center;
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
}
.pagination-text {
font-size: 24rpx;
color: #999;
}
/* 加载更多按钮 */
.load-more-container {
text-align: center;
padding: 20rpx 30rpx 30rpx;
}
.load-more-btn {
background-color: #f8f8f8;
border: 1rpx solid #e0e0e0;
border-radius: 25rpx;
padding: 20rpx 40rpx;
display: inline-block;
cursor: pointer;
transition: all 0.3s ease;
}
.load-more-btn:hover {
background-color: #e8e8e8;
border-color: #d0d0d0;
}
.load-more-btn.loading {
opacity: 0.7;
pointer-events: none;
}
.load-more-text {
font-size: 26rpx;
color: #666;
}
.load-more-btn .loading-text {
font-size: 26rpx;
color: #999;
}
/* 浮动签到按钮 */
.floating-checkin {
position: fixed;
right: 30rpx;
bottom: 200rpx; /* 调整位置,确保在底部导航上方有足够空间 */
background-color: #e74c3c;
border-radius: 50rpx;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 30rpx rgba(231, 76, 60, 0.3);
z-index: 100;
cursor: pointer;
transition: all 0.3s ease;
}
.floating-checkin:hover {
transform: scale(1.05);
box-shadow: 0 12rpx 40rpx rgba(231, 76, 60, 0.4);
}
.floating-checkin.loading {
opacity: 0.7;
pointer-events: none;
}
.floating-checkin.loading .checkin-icon {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.checkin-icon {
position: relative;
margin-right: 20rpx;
}
.calendar-icon {
font-size: 40rpx;
color: #ffffff;
}
.checkmark {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: #27ae60;
color: #ffffff;
border-radius: 50%;
width: 30rpx;
height: 30rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.checkin-text {
display: flex;
flex-direction: column;
align-items: center;
}
.checkin-label {
font-size: 24rpx;
color: #ffffff;
margin-bottom: 5rpx;
}
.checkin-reward {
font-size: 28rpx;
font-weight: bold;
color: #ffffff;
}
/* 底部导航 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
display: flex;
padding: 40rpx 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
z-index: 99;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.nav-item.active {
color: #e74c3c;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: -20rpx;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #e74c3c;
border-radius: 2rpx;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
width: 0;
opacity: 0;
}
to {
width: 40rpx;
opacity: 1;
}
}
.nav-icon {
font-size: 40rpx;
margin-bottom: 10rpx;
}
.nav-text {
font-size: 24rpx;
color: #666;
}
.nav-item.active .nav-text {
color: #e74c3c;
}
/* 页面整体响应式设计 */
@media (max-width: 750rpx) {
.transaction-tabs,
.transaction-list {
margin: 15rpx 20rpx;
}
.floating-checkin {
right: 20rpx;
bottom: 180rpx; /* 响应式下调整位置 */
}
}
</style>