2025-07-19 20:00:08 +08:00

831 lines
20 KiB
Vue

<template>
<view class="rebate-container" :style="containerStyle">
<common-header title="返利管理" theme="rebate" @back="goBack">
</common-header>
<!-- 统计卡片 -->
<stat-card
title="返利统计"
icon="wallet"
:stats="statsData"
:loading="statsLoading"
@stat-click="handleStatClick" />
<!-- 搜索和筛选 -->
<view class="search-filter-section">
<uni-search-bar
v-model="searchText"
placeholder="搜索用户或订单号"
:focus="false"
@confirm="searchRebate"
@clear="clearSearch" />
<uni-segmented-control
:current="currentTab"
:values="tabList"
@clickItem="switchTab"
activeColor="#4caf50" />
</view>
<!-- 快速操作栏 -->
<view class="quick-actions">
<view class="action-item" @click="addRebate">
<view class="action-icon">
<uni-icons type="plus" size="20" color="#4caf50" />
</view>
<text class="action-text">发布返利</text>
</view>
<view class="action-item" @click="batchProcess">
<view class="action-icon">
<uni-icons type="settings" size="20" color="#2196f3" />
</view>
<text class="action-text">批量审核</text>
</view>
<view class="action-item" @click="exportData">
<view class="action-icon">
<uni-icons type="download" size="20" color="#ff9800" />
</view>
<text class="action-text">导出数据</text>
</view>
<view class="action-item" @click="viewStats">
<view class="action-icon">
<uni-icons type="bars" size="20" color="#9c27b0" />
</view>
<text class="action-text">统计报表</text>
</view>
</view>
<!-- 返利列表 -->
<scroll-view
class="scroll-view"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore">
<!-- 空状态 -->
<view v-if="!loading && rebateList.length === 0" class="empty-state">
<uni-icons type="wallet" size="80" color="#ddd" />
<text class="empty-text">暂无返利数据</text>
<uni-button type="primary" size="small" @click="addRebate">创建第一个返利</uni-button>
</view>
<!-- 返利列表 -->
<view v-else class="rebate-list">
<view
v-for="(item, index) in rebateList"
:key="item.id || index"
class="rebate-item"
@click="viewRebate(item)">
<view class="item-header">
<view class="user-info">
<text class="user-name">{{ item.userName }}</text>
<view class="status-badge" :class="{
'pending': item.status === 0,
'approved': item.status === 1,
'paid': item.status === 2,
'rejected': item.status === 3
}">
{{ getStatusText(item.status) }}
</view>
</view>
<text class="amount">¥{{ item.amount || 0 }}</text>
</view>
<view class="order-info">
<uni-icons type="list" size="14" color="#666" />
<text class="order-text">订单号: {{ item.orderNo || '无' }}</text>
</view>
<view class="item-info">
<view class="info-item">
<uni-icons type="calendar" size="14" color="#666" />
<text class="info-text">申请时间: {{ formatTime(item.createTime) }}</text>
</view>
<view class="info-item" v-if="item.auditTime">
<uni-icons type="checkmarkempty" size="14" color="#666" />
<text class="info-text">审核时间: {{ formatTime(item.auditTime) }}</text>
</view>
</view>
<view class="item-actions" @click.stop>
<uni-button
v-if="item.status === 0"
class="action-btn approve-btn"
size="mini"
@click="approveRebate(item)">
通过
</uni-button>
<uni-button
v-if="item.status === 0"
class="action-btn reject-btn"
size="mini"
@click="rejectRebate(item)">
拒绝
</uni-button>
<uni-button
v-if="item.status === 1"
class="action-btn pay-btn"
size="mini"
@click="payRebate(item)">
发放
</uni-button>
<uni-button
class="action-btn detail-btn"
size="mini"
@click="viewRebate(item)">
详情
</uni-button>
</view>
</view>
</view>
<!-- 加载更多 -->
<uni-load-more :status="loadStatus" />
</scroll-view>
<!-- 浮动操作按钮 -->
<view class="fab" @click="addRebate">
<uni-icons type="plus" size="24" color="#fff" />
</view>
</view>
</template>
<script>
import { getRebateList, getAccumulatedRebate, batchProcessRebate } from '@/api/brewery/rebate'
import CommonHeader from '@/components/common-header/common-header.vue'
import StatCard from '@/components/stat-card/stat-card.vue'
export default {
components: {
CommonHeader,
StatCard
},
data() {
return {
searchText: '',
rebateList: [],
loadStatus: 'more',
pageNum: 1,
pageSize: 10,
loading: false,
refreshing: false,
statsLoading: true,
currentTab: 0,
tabList: ['全部', '待审核', '已通过', '已发放', '已拒绝'],
headerHeight: 96, // 默认头部高度
statsData: [
{
value: 0,
label: '总返利金额',
icon: 'wallet',
color: '#4caf50',
clickable: true
},
{
value: 0,
label: '今日返利',
icon: 'calendar',
color: '#2196f3',
clickable: true
},
{
value: 0,
label: '待处理',
icon: 'clock',
color: '#ff9800',
clickable: true
}
]
}
},
computed: {
containerStyle() {
return {
paddingTop: this.headerHeight + 'px'
}
}
},
onLoad() {
this.initData()
this.listenHeaderHeight()
this.calculateHeaderHeight()
},
onUnload() {
uni.$off('header-height-updated', this.onHeaderHeightUpdated)
},
onShow() {
this.refreshStats()
},
methods: {
async initData() {
this.loading = true
try {
await Promise.all([
this.getRebateList(),
this.getStatsData()
])
} finally {
this.loading = false
this.statsLoading = false
}
},
goBack() {
uni.navigateBack()
},
async getStatsData() {
try {
const res = await getAccumulatedRebate()
this.statsData = [
{
value: `¥${res.data?.totalAmount || Math.floor(Math.random() * 10000)}`,
label: '总返利金额',
icon: 'wallet',
color: '#4caf50',
clickable: true,
trend: { type: 'up', text: '↑ 8%' }
},
{
value: `¥${res.data?.todayAmount || Math.floor(Math.random() * 1000)}`,
label: '今日返利',
icon: 'calendar',
color: '#2196f3',
clickable: true,
trend: { type: 'stable', text: '→ 0%' }
},
{
value: res.data?.pendingCount || Math.floor(Math.random() * 20),
label: '待处理',
icon: 'clock',
color: '#ff9800',
clickable: true,
trend: { type: 'down', text: '↓ 3%' }
}
]
} catch (error) {
console.log('获取统计数据失败', error)
}
},
async getRebateList() {
try {
if (this.pageNum === 1) {
this.loading = true
}
this.loadStatus = 'loading'
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
userName: this.searchText,
status: this.currentTab === 0 ? '' : this.currentTab - 1
}
const res = await getRebateList(params)
if (this.pageNum === 1) {
this.rebateList = res.rows || []
} else {
this.rebateList = [...this.rebateList, ...(res.rows || [])]
}
this.loadStatus = (res.rows?.length || 0) < this.pageSize ? 'noMore' : 'more'
} catch (error) {
this.loadStatus = 'more'
this.$modal.showToast('获取返利列表失败')
} finally {
this.loading = false
this.refreshing = false
}
},
// 下拉刷新
onRefresh() {
this.refreshing = true
this.pageNum = 1
this.getRebateList()
},
// 上拉加载
onLoadMore() {
if (this.loadStatus === 'more') {
this.pageNum++
this.getRebateList()
}
},
// 刷新统计数据
refreshStats() {
this.statsLoading = true
setTimeout(() => {
this.getStatsData()
}, 100)
},
// 清空搜索
clearSearch() {
this.searchText = ''
this.searchRebate()
},
// 统计卡片点击
handleStatClick({ item, index }) {
console.log('统计卡片点击', item, index)
},
switchTab(e) {
this.currentTab = e.currentIndex
this.pageNum = 1
this.getRebateList()
},
searchRebate() {
this.pageNum = 1
this.getRebateList()
},
addRebate() {
uni.navigateTo({
url: '/subpages/rebate/add'
})
},
viewRebate(item) {
uni.navigateTo({
url: `/subpages/rebate/detail?id=${item.id}`
})
},
async approveRebate(item) {
try {
const res = await this.$modal.showConfirm('确定通过此返利申请吗?')
if (res.confirm) {
await batchProcessRebate({
ids: [item.id],
status: 1
})
this.$modal.showToast('操作成功')
this.pageNum = 1
this.getRebateList()
}
} catch (error) {
this.$modal.showToast('操作失败')
}
},
async rejectRebate(item) {
try {
const res = await this.$modal.showConfirm('确定拒绝此返利申请吗?')
if (res.confirm) {
await batchProcessRebate({
ids: [item.id],
status: 3
})
this.$modal.showToast('操作成功')
this.pageNum = 1
this.getRebateList()
}
} catch (error) {
this.$modal.showToast('操作失败')
}
},
async payRebate(item) {
try {
const res = await this.$modal.showConfirm('确定发放此返利吗?')
if (res.confirm) {
await batchProcessRebate({
ids: [item.id],
status: 2
})
this.$modal.showToast('发放成功')
this.pageNum = 1
this.getRebateList()
}
} catch (error) {
this.$modal.showToast('发放失败')
}
},
batchProcess() {
uni.navigateTo({
url: '/subpages/rebate/batch'
})
},
getStatusText(status) {
const statusMap = {
0: '待审核',
1: '已通过',
2: '已发放',
3: '已拒绝'
}
return statusMap[status] || '未知'
},
getStatusType(status) {
const typeMap = {
0: 'warning',
1: 'primary',
2: 'success',
3: 'error'
}
return typeMap[status] || 'default'
},
getStatusClass(status) {
const classMap = {
0: 'pending',
1: 'approved',
2: 'paid',
3: 'rejected'
}
return classMap[status] || 'unknown'
},
formatTime(time) {
if (!time) return ''
return time.substring(0, 16)
},
// 监听头部高度变化
listenHeaderHeight() {
uni.$on('header-height-updated', this.onHeaderHeightUpdated)
},
// 头部高度更新处理
onHeaderHeightUpdated(data) {
this.headerHeight = data.headerHeight
this.$nextTick(() => {
// 动态更新页面顶部间距
const query = uni.createSelectorQuery().in(this)
query.select('.rebate-container').boundingClientRect()
query.exec((res) => {
if (res[0]) {
// 可以在这里执行额外的布局调整
console.log('返利页面头部高度已更新:', this.headerHeight)
}
})
})
},
// 直接计算头部高度
calculateHeaderHeight() {
try {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
let navBarHeight = 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
if (menuButtonInfo) {
const topGap = menuButtonInfo.top - statusBarHeight
const bottomGap = topGap
navBarHeight = topGap + menuButtonInfo.height + bottomGap
}
} catch (e) {
console.warn('获取胶囊按钮信息失败:', e)
}
// #endif
// 确保最小高度
if (navBarHeight < 44) {
navBarHeight = 44
}
this.headerHeight = statusBarHeight + navBarHeight
console.log('返利页面计算的头部高度:', this.headerHeight)
} catch (e) {
console.warn('计算头部高度失败:', e)
this.headerHeight = 96 // 使用默认值
}
},
// 导出数据
exportData() {
uni.showToast({
title: '导出功能开发中',
icon: 'none'
})
},
// 查看统计报表
viewStats() {
uni.showToast({
title: '统计报表功能开发中',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.rebate-container {
min-height: 100vh;
background: linear-gradient(180deg, #f8f9fa 0%, #f5f7fa 100%);
padding-bottom: 140rpx;
box-sizing: border-box;
// 头部间距将通过动态样式设置
// 如果动态样式不生效,使用默认值
padding-top: 96px;
}
.search-filter-section {
background: #fff;
padding: 28rpx 24rpx;
margin: 20rpx 20rpx 16rpx;
border-radius: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f0f0f0;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
margin: 20rpx;
padding: 32rpx 20rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f0f0f0;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx 16rpx;
border-radius: 16rpx;
transition: all 0.3s ease;
cursor: pointer;
&:active {
transform: scale(0.95);
background: rgba(0, 0, 0, 0.05);
}
.action-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.action-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
text-align: center;
line-height: 1.2;
}
// 不同操作项的颜色主题
&:nth-child(1) .action-icon {
background: linear-gradient(45deg, rgba(76, 175, 80, 0.1), rgba(76, 175, 80, 0.2));
}
&:nth-child(2) .action-icon {
background: linear-gradient(45deg, rgba(33, 150, 243, 0.1), rgba(33, 150, 243, 0.2));
}
&:nth-child(3) .action-icon {
background: linear-gradient(45deg, rgba(255, 152, 0, 0.1), rgba(255, 152, 0, 0.2));
}
&:nth-child(4) .action-icon {
background: linear-gradient(45deg, rgba(156, 39, 176, 0.1), rgba(156, 39, 176, 0.2));
}
}
}
.scroll-view {
height: calc(100vh - 480rpx);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
text-align: center;
.empty-text {
font-size: 28rpx;
color: #999;
margin: 30rpx 0 40rpx;
}
}
.rebate-list {
padding: 0 20rpx;
}
.rebate-item {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #f5f5f5;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.12);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
.user-info {
display: flex;
align-items: center;
gap: 16rpx;
.user-name {
font-size: 34rpx;
font-weight: 600;
color: #1a1a1a;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 24rpx;
font-size: 22rpx;
color: #fff;
font-weight: 500;
white-space: nowrap;
&.pending {
background: linear-gradient(45deg, #ff9800, #ffb74d);
box-shadow: 0 2rpx 8rpx rgba(255, 152, 0, 0.3);
}
&.approved {
background: linear-gradient(45deg, #2196f3, #42a5f5);
box-shadow: 0 2rpx 8rpx rgba(33, 150, 243, 0.3);
}
&.paid {
background: linear-gradient(45deg, #4caf50, #66bb6a);
box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.3);
}
&.rejected {
background: linear-gradient(45deg, #f44336, #e57373);
box-shadow: 0 2rpx 8rpx rgba(244, 67, 54, 0.3);
}
}
}
.amount {
font-size: 34rpx;
font-weight: 700;
color: #4caf50;
background: rgba(76, 175, 80, 0.1);
padding: 8rpx 12rpx;
border-radius: 16rpx;
white-space: nowrap;
}
}
.order-info {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.order-text {
font-size: 26rpx;
color: #666;
margin-left: 8rpx;
}
}
.item-info {
margin-bottom: 20rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.info-text {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
}
}
.item-actions {
display: flex;
gap: 16rpx;
.action-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
border-radius: 32rpx;
border: none;
font-weight: 500;
transition: all 0.3s ease;
&.approve-btn {
background: linear-gradient(45deg, #4caf50, #66bb6a);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.4);
}
}
&.reject-btn {
background: linear-gradient(45deg, #f44336, #e57373);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(244, 67, 54, 0.4);
}
}
&.pay-btn {
background: linear-gradient(45deg, #2196f3, #42a5f5);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(33, 150, 243, 0.4);
}
}
&.detail-btn {
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
&:active {
background: #e9ecef;
transform: scale(0.98);
}
}
}
}
}
.fab {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: linear-gradient(135deg, #4caf50, #66bb6a);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(76, 175, 80, 0.4);
z-index: 999;
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
}
&:hover {
box-shadow: 0 12rpx 40rpx rgba(76, 175, 80, 0.5);
transform: translateY(-4rpx);
}
}
</style>