Files
rent/apps/miniapp/src/pages/invite/cashbacks.vue
T
2026-05-11 11:24:49 +08:00

832 lines
18 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-cashbacks">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="navbar-left" @tap="goBack">
<u-icon name="arrow-left" :size="20" color="#1A1A1A" />
</view>
<text class="navbar-title">返现明细</text>
<view class="navbar-right"></view>
</view>
</view>
<!-- 顶部统计卡片 -->
<view class="stats-header">
<view class="stats-main">
<view class="stats-icon">
<u-icon name="rmb-circle-fill" :size="32" color="#FF6B35" />
</view>
<view class="stats-content">
<text class="stats-label">累计返现收益</text>
<view class="stats-amount-row">
<text class="stats-symbol">¥</text>
<text class="stats-amount">{{ totalCashback }}</text>
</view>
</view>
</view>
<view class="stats-footer">
<view class="stats-item">
<text class="item-value">{{ records.length }}</text>
<text class="item-label">返现笔数</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="item-value">{{ uniqueUsers }}</text>
<text class="item-label">贡献用户</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="item-value">{{ avgCashback }}</text>
<text class="item-label">平均返现</text>
</view>
</view>
</view>
<!-- 时间筛选 -->
<view class="time-filter">
<view
v-for="tab in timeTabs"
:key="tab.value"
class="time-tab"
:class="{ active: currentTime === tab.value }"
@tap="changeTime(tab.value)"
>
<text class="tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- 返现列表 -->
<scroll-view
scroll-y
class="cashback-list"
@scrolltolower="loadMore"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<view class="list-container">
<view
v-for="item in filteredRecords"
:key="item.id"
class="cashback-item"
@tap="viewOrderDetail(item)"
>
<!-- 左侧图标 -->
<view class="item-icon" :class="getIconClass(item.orderIndex)">
<u-icon name="rmb-circle-fill" :size="24" color="#ffffff" />
</view>
<!-- 中间内容 -->
<view class="item-content">
<view class="content-top">
<text class="order-title">订单返现</text>
<view class="order-badges">
<view class="badge" :class="getBadgeClass(item.orderIndex)">
<text class="badge-text">{{ getOrderLabel(item.orderIndex) }}</text>
</view>
</view>
</view>
<view class="content-middle">
<view class="order-info-row">
<u-icon name="list" :size="12" color="#999" />
<text class="order-no">{{ formatOrderNo(item.orderNo) }}</text>
</view>
<view class="order-detail-row">
<text class="detail-item">订单金额 ¥{{ item.orderAmount }}</text>
<text class="detail-divider">|</text>
<text class="detail-item">返现 {{ (item.rate * 100).toFixed(1) }}%</text>
</view>
</view>
<view class="content-bottom">
<view class="time-row">
<u-icon name="clock" :size="12" color="#CCCCCC" />
<text class="time-text">{{ formatTime(item.createdAt) }}</text>
</view>
</view>
</view>
<!-- 右侧金额 -->
<view class="item-amount">
<text class="amount-value">+{{ item.amount }}</text>
<text class="amount-unit"></text>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-wrapper">
<view class="loading-spinner">
<view class="spinner-dot"></view>
<view class="spinner-dot"></view>
<view class="spinner-dot"></view>
</view>
<text class="loading-text">加载中...</text>
</view>
<!-- 没有更多 -->
<view v-if="!hasMore && records.length > 0" class="no-more">
<view class="no-more-line"></view>
<text class="no-more-text">已经到底了</text>
<view class="no-more-line"></view>
</view>
<!-- 空状态 -->
<view v-if="records.length === 0 && !loading" class="empty-state">
<view class="empty-icon">
<u-icon name="rmb-circle" :size="80" color="#E5E5E5" />
</view>
<text class="empty-title">暂无返现记录</text>
<text class="empty-desc">邀请好友下单后返现收益将在此展示</text>
<view class="empty-action" @tap="goInvite">
<u-icon name="share-fill" :size="16" color="#ffffff" />
<text class="action-text">去邀请好友</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getCashbackRecords } from '@/api/user/invite';
const records = ref<any[]>([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const refreshing = ref(false);
const currentTime = ref('all');
const timeTabs = [
{ label: '全部', value: 'all' },
{ label: '本月', value: 'month' },
{ label: '本周', value: 'week' },
];
// 计算总返现
const totalCashback = computed(() => {
const total = records.value.reduce((sum, item) => sum + Number(item.amount || 0), 0);
return total.toFixed(2);
});
// 计算唯一用户数
const uniqueUsers = computed(() => {
const userIds = new Set(records.value.map(item => item.inviteeId));
return userIds.size;
});
// 计算平均返现
const avgCashback = computed(() => {
if (records.value.length === 0) return '0.00';
const avg = Number(totalCashback.value) / records.value.length;
return avg.toFixed(2);
});
// 时间筛选
const filteredRecords = computed(() => {
if (currentTime.value === 'all') return records.value;
const now = new Date();
return records.value.filter(item => {
const itemDate = new Date(item.createdAt);
if (currentTime.value === 'week') {
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return itemDate >= weekAgo;
} else if (currentTime.value === 'month') {
return itemDate.getMonth() === now.getMonth() &&
itemDate.getFullYear() === now.getFullYear();
}
return true;
});
});
onMounted(() => {
fetchRecords(true);
});
async function fetchRecords(reset = false) {
if (loading.value) return;
if (!reset && !hasMore.value) return;
loading.value = true;
if (reset) {
page.value = 1;
hasMore.value = true;
}
try {
const res = await getCashbackRecords({ page: page.value, pageSize: 20 });
const list = res.data?.list || [];
records.value = reset ? list : [...records.value, ...list];
hasMore.value = list.length >= 20;
page.value++;
} catch (e) {
console.error('获取返现记录失败:', e);
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
} finally {
loading.value = false;
refreshing.value = false;
}
}
function loadMore() {
fetchRecords();
}
function onRefresh() {
refreshing.value = true;
fetchRecords(true);
}
function changeTime(value: string) {
currentTime.value = value;
}
function formatTime(dateStr: string) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - d.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return `今天 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
} else if (days === 1) {
return `昨天 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
} else if (days < 7) {
return `${days}天前`;
} else {
return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
}
function formatOrderNo(orderNo: string) {
if (!orderNo) return '';
// 只显示订单号的前6位和后4位
if (orderNo.length > 12) {
return `${orderNo.substring(0, 6)}...${orderNo.substring(orderNo.length - 4)}`;
}
return orderNo;
}
function getOrderLabel(index: number) {
if (index === 1) return '首单';
if (index === 2) return '第2单';
if (index === 3) return '第3单';
return `${index}`;
}
function getIconClass(index: number) {
if (index === 1) return 'icon-gold';
if (index <= 3) return 'icon-orange';
return 'icon-blue';
}
function getBadgeClass(index: number) {
if (index === 1) return 'badge-gold';
if (index <= 3) return 'badge-orange';
return 'badge-blue';
}
function viewOrderDetail(item: any) {
uni.showToast({
title: `订单${item.orderNo}返现¥${item.amount}`,
icon: 'none',
duration: 2000,
});
}
function goInvite() {
uni.navigateBack();
}
function goBack() {
uni.navigateBack();
}
</script>
<style lang="scss" scoped>
@import '@/static/styles/design-tokens.scss';
@import '@/static/styles/mixins.scss';
.page-cashbacks {
min-height: 100vh;
background: linear-gradient(180deg, #FFF5F0 0%, #F5F7FA 100%);
display: flex;
flex-direction: column;
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
background: #ffffff;
padding: 0 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.navbar-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-left,
.navbar-right {
width: 80rpx;
display: flex;
align-items: center;
}
.navbar-left {
justify-content: flex-start;
padding: 12rpx;
margin-left: -12rpx;
transition: opacity 0.3s ease;
&:active {
opacity: 0.6;
}
}
.navbar-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
/* ========== 顶部统计卡片 ========== */
.stats-header {
margin: 24rpx 24rpx 16rpx;
background: linear-gradient(135deg, #FF8C5A 0%, #FF6B35 100%);
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.25);
position: relative;
&::before {
content: '';
position: absolute;
top: -60rpx;
right: -60rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
}
.stats-main {
padding: 32rpx 32rpx 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
position: relative;
z-index: 1;
}
.stats-icon {
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stats-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.stats-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
}
.stats-amount-row {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.stats-symbol {
font-size: 36rpx;
color: #ffffff;
font-weight: 600;
}
.stats-amount {
font-size: 56rpx;
color: #ffffff;
font-weight: 700;
line-height: 1;
}
.stats-footer {
padding: 24rpx 32rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
z-index: 1;
}
.stats-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.item-value {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.item-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
}
.stats-divider {
width: 2rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.3);
}
/* ========== 时间筛选 ========== */
.time-filter {
margin: 0 24rpx 16rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 8rpx;
display: flex;
gap: 8rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.time-tab {
flex: 1;
padding: 16rpx 24rpx;
border-radius: 12rpx;
text-align: center;
transition: all 0.3s ease;
&:active {
transform: scale(0.96);
}
&.active {
background: linear-gradient(135deg, #FFF5F0 0%, #FFE8DD 100%);
}
}
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
.time-tab.active & {
color: #FF6B35;
font-weight: 600;
}
}
/* ========== 返现列表 ========== */
.cashback-list {
flex: 1;
overflow-y: auto;
}
.list-container {
padding: 0 24rpx 24rpx;
}
.cashback-item {
background: #ffffff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6rpx;
height: 100%;
background: linear-gradient(180deg, #FF8C5A 0%, #FF6B35 100%);
}
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
}
/* ========== 左侧图标 ========== */
.item-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.icon-gold {
background: linear-gradient(135deg, #FFE58F 0%, #FFD666 100%);
}
.icon-orange {
background: linear-gradient(135deg, #FFD591 0%, #FFC069 100%);
}
.icon-blue {
background: linear-gradient(135deg, #91D5FF 0%, #69C0FF 100%);
}
/* ========== 中间内容 ========== */
.item-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
min-width: 0;
}
.content-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.order-title {
font-size: 30rpx;
font-weight: 600;
color: #1A1A1A;
}
.order-badges {
display: flex;
gap: 8rpx;
}
.badge {
padding: 4rpx 12rpx;
border-radius: 8rpx;
border: 1rpx solid;
}
.badge-gold {
background: linear-gradient(135deg, #FFF7E6 0%, #FFEFCC 100%);
border-color: #FFD666;
}
.badge-orange {
background: linear-gradient(135deg, #FFF7E6 0%, #FFE8CC 100%);
border-color: #FFC069;
}
.badge-blue {
background: linear-gradient(135deg, #E6F7FF 0%, #BAE7FF 100%);
border-color: #91D5FF;
}
.badge-text {
font-size: 20rpx;
font-weight: 600;
}
.badge-gold .badge-text {
color: #D48806;
}
.badge-orange .badge-text {
color: #D46B08;
}
.badge-blue .badge-text {
color: #0050B3;
}
.content-middle {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.order-info-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.order-no {
font-size: 24rpx;
color: #999;
font-family: 'Courier New', monospace;
}
.order-detail-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.detail-item {
font-size: 24rpx;
color: #666;
}
.detail-divider {
font-size: 24rpx;
color: #E5E5E5;
}
.content-bottom {
display: flex;
align-items: center;
}
.time-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.time-text {
font-size: 22rpx;
color: #CCCCCC;
}
/* ========== 右侧金额 ========== */
.item-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4rpx;
flex-shrink: 0;
}
.amount-value {
font-size: 40rpx;
font-weight: 700;
color: #FF6B35;
line-height: 1;
}
.amount-unit {
font-size: 22rpx;
color: #FF8C5A;
font-weight: 500;
}
/* ========== 加载状态 ========== */
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
padding: 48rpx 0;
}
.loading-spinner {
display: flex;
align-items: center;
gap: 12rpx;
}
.spinner-dot {
width: 12rpx;
height: 12rpx;
background: #FF6B35;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.loading-text {
font-size: 24rpx;
color: #999;
}
/* ========== 没有更多 ========== */
.no-more {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
padding: 48rpx 0;
}
.no-more-line {
width: 80rpx;
height: 2rpx;
background: linear-gradient(90deg, transparent 0%, #E5E5E5 50%, transparent 100%);
}
.no-more-text {
font-size: 24rpx;
color: #CCCCCC;
}
/* ========== 空状态 ========== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 48rpx;
}
.empty-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #F5F5F5 0%, #E8E8E8 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #666;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #999;
text-align: center;
line-height: 1.6;
margin-bottom: 48rpx;
}
.empty-action {
display: flex;
align-items: center;
gap: 12rpx;
padding: 24rpx 48rpx;
background: linear-gradient(135deg, #FF8C5A 0%, #FF6B35 100%);
border-radius: 48rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.96);
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.4);
}
}
.action-text {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
</style>