504 lines
12 KiB
Vue
504 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="order-list-page">
|
|||
|
|
<!-- 自定义导航栏 -->
|
|||
|
|
<view class="custom-nav safe-area-top">
|
|||
|
|
<view class="nav-content">
|
|||
|
|
<view class="nav-btn" @click="goBack">
|
|||
|
|
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
|
|||
|
|
</view>
|
|||
|
|
<text class="nav-title">我的订单</text>
|
|||
|
|
<view class="nav-btn" @click="refreshData">
|
|||
|
|
<uni-icons type="refresh" size="20" color="#64748B"></uni-icons>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 筛选标签 -->
|
|||
|
|
<view class="filter-bar">
|
|||
|
|
<view
|
|||
|
|
v-for="tab in tabs"
|
|||
|
|
:key="tab.value"
|
|||
|
|
class="filter-item"
|
|||
|
|
:class="{ active: activeTab === tab.value }"
|
|||
|
|
@click="switchTab(tab.value)"
|
|||
|
|
>
|
|||
|
|
<text>{{tab.label}}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<scroll-view
|
|||
|
|
scroll-y
|
|||
|
|
class="page-content"
|
|||
|
|
@scrolltolower="loadMore"
|
|||
|
|
refresher-enabled
|
|||
|
|
:refresher-triggered="refreshing"
|
|||
|
|
@refresherrefresh="onRefresh"
|
|||
|
|
>
|
|||
|
|
<!-- 订单列表 -->
|
|||
|
|
<view class="order-list" v-if="filteredOrders.length > 0">
|
|||
|
|
<view
|
|||
|
|
v-for="(order, index) in filteredOrders"
|
|||
|
|
:key="order.id"
|
|||
|
|
class="order-card"
|
|||
|
|
@click="viewOrderDetail(order)"
|
|||
|
|
>
|
|||
|
|
<view class="order-header">
|
|||
|
|
<view class="order-info">
|
|||
|
|
<text class="order-no">订单号:{{order.orderNo}}</text>
|
|||
|
|
<text class="order-time">{{formatDateTime(order.createTime)}}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="order-status" :class="order.status">
|
|||
|
|
{{getStatusText(order.status)}}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="order-body">
|
|||
|
|
<view class="plan-info">
|
|||
|
|
<text class="plan-name">{{order.planName || order.subscriptionPlan?.name || '-'}}</text>
|
|||
|
|
<text class="plan-duration">{{order.months}} 个月</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="price-info">
|
|||
|
|
<view v-if="order.amount !== order.actualAmount" class="original-price">
|
|||
|
|
¥{{order.amount}}
|
|||
|
|
</view>
|
|||
|
|
<view class="actual-price">¥{{order.actualAmount}}</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="order-footer" v-if="order.status === 'pending'">
|
|||
|
|
<button class="action-btn cancel" @click.stop="cancelOrder(order)">取消订单</button>
|
|||
|
|
<button class="action-btn pay" @click.stop="payOrder(order)">去支付</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 加载更多 -->
|
|||
|
|
<view class="load-more" v-if="hasMore">
|
|||
|
|
<text v-if="loading">加载中...</text>
|
|||
|
|
<text v-else @click="loadMore">加载更多</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 空状态 -->
|
|||
|
|
<view class="empty-state" v-else>
|
|||
|
|
<uni-icons type="wallet-filled" size="80" color="#E4E7ED"></uni-icons>
|
|||
|
|
<text class="empty-title">暂无订单</text>
|
|||
|
|
<text class="empty-desc">您还没有创建任何订单</text>
|
|||
|
|
<button class="empty-action" @click="goToPlanSelect">去购买套餐</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { billingApi } from '@/api/index.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
tabs: [
|
|||
|
|
{ label: '全部', value: 'all' },
|
|||
|
|
{ label: '待支付', value: 'pending' },
|
|||
|
|
{ label: '已支付', value: 'paid' },
|
|||
|
|
{ label: '已取消', value: 'cancelled' }
|
|||
|
|
],
|
|||
|
|
activeTab: 'all',
|
|||
|
|
orders: [],
|
|||
|
|
page: 1,
|
|||
|
|
pageSize: 10,
|
|||
|
|
hasMore: true,
|
|||
|
|
loading: false,
|
|||
|
|
refreshing: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
filteredOrders() {
|
|||
|
|
if (this.activeTab === 'all') {
|
|||
|
|
return this.orders
|
|||
|
|
}
|
|||
|
|
return this.orders.filter(order => order.status === this.activeTab)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad() {
|
|||
|
|
this.loadOrders()
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
this.refreshData()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
async loadOrders() {
|
|||
|
|
if (this.loading || !this.hasMore) return
|
|||
|
|
this.loading = true
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await billingApi.getOrders({
|
|||
|
|
page: this.page,
|
|||
|
|
pageSize: this.pageSize
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
const newOrders = res.data.list || []
|
|||
|
|
if (this.page === 1) {
|
|||
|
|
this.orders = newOrders
|
|||
|
|
} else {
|
|||
|
|
this.orders = [...this.orders, ...newOrders]
|
|||
|
|
}
|
|||
|
|
this.hasMore = newOrders.length >= this.pageSize
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载订单失败:', error)
|
|||
|
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
|||
|
|
} finally {
|
|||
|
|
this.loading = false
|
|||
|
|
this.refreshing = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
switchTab(value) {
|
|||
|
|
this.activeTab = value
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onRefresh() {
|
|||
|
|
this.refreshing = true
|
|||
|
|
this.page = 1
|
|||
|
|
this.hasMore = true
|
|||
|
|
this.loadOrders()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
refreshData() {
|
|||
|
|
this.page = 1
|
|||
|
|
this.hasMore = true
|
|||
|
|
this.loadOrders()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
loadMore() {
|
|||
|
|
if (this.hasMore && !this.loading) {
|
|||
|
|
this.page++
|
|||
|
|
this.loadOrders()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getStatusText(status) {
|
|||
|
|
const map = {
|
|||
|
|
pending: '待支付',
|
|||
|
|
paid: '已支付',
|
|||
|
|
cancelled: '已取消'
|
|||
|
|
}
|
|||
|
|
return map[status] || status
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatDateTime(dateTime) {
|
|||
|
|
if (!dateTime) return '-'
|
|||
|
|
const date = new Date(dateTime)
|
|||
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
viewOrderDetail(order) {
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: `/pages/billing/order-detail?id=${order.id}`
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async cancelOrder(order) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '确定要取消该订单吗?',
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
try {
|
|||
|
|
const result = await billingApi.cancelOrder(order.id)
|
|||
|
|
if (result.code === 200) {
|
|||
|
|
uni.showToast({ title: '已取消', icon: 'success' })
|
|||
|
|
this.refreshData()
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({ title: result.message || '取消失败', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('取消订单失败:', error)
|
|||
|
|
uni.showToast({ title: '取消失败', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async payOrder(order) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '支付确认',
|
|||
|
|
content: `确认支付订单 #${order.orderNo},金额 ¥${order.actualAmount}?`,
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
uni.showLoading({ title: '支付中...' })
|
|||
|
|
try {
|
|||
|
|
const result = await billingApi.payOrder(order.id)
|
|||
|
|
uni.hideLoading()
|
|||
|
|
if (result.code === 200) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '支付成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
this.refreshData()
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({ title: result.message || '支付失败', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
console.error('支付失败:', error)
|
|||
|
|
uni.showToast({ title: '支付失败', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
goBack() {
|
|||
|
|
uni.navigateBack()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
goToPlanSelect() {
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: '/pages/billing/plan-select'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.order-list-page {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
background: #F8FAFC;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 导航栏 */
|
|||
|
|
.custom-nav {
|
|||
|
|
background: #FFFFFF;
|
|||
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20rpx 32rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-btn {
|
|||
|
|
width: 72rpx;
|
|||
|
|
height: 72rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-title {
|
|||
|
|
font-size: 34rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1E293B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 筛选栏 */
|
|||
|
|
.filter-bar {
|
|||
|
|
display: flex;
|
|||
|
|
background: #FFFFFF;
|
|||
|
|
padding: 20rpx 32rpx;
|
|||
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-item {
|
|||
|
|
padding: 16rpx 32rpx;
|
|||
|
|
background: #F8FAFC;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #64748B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-item.active {
|
|||
|
|
background: #667eea;
|
|||
|
|
color: #FFFFFF;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 页面内容 */
|
|||
|
|
.page-content {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 24rpx 32rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 订单列表 */
|
|||
|
|
.order-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-card {
|
|||
|
|
background: #FFFFFF;
|
|||
|
|
border-radius: 24rpx;
|
|||
|
|
padding: 28rpx;
|
|||
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
padding-bottom: 20rpx;
|
|||
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-info {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-no {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1E293B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-time {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #94A3B8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-status {
|
|||
|
|
padding: 8rpx 20rpx;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-status.pending {
|
|||
|
|
background: #FEF3C7;
|
|||
|
|
color: #D97706;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-status.paid {
|
|||
|
|
background: #D1FAE5;
|
|||
|
|
color: #059669;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-status.cancelled {
|
|||
|
|
background: #F3F4F6;
|
|||
|
|
color: #6B7280;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-body {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-info {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-name {
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1E293B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-duration {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #64748B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.price-info {
|
|||
|
|
text-align: right;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.original-price {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #94A3B8;
|
|||
|
|
text-decoration: line-through;
|
|||
|
|
margin-bottom: 4rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.actual-price {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #F56C6C;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.order-footer {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
padding-top: 20rpx;
|
|||
|
|
border-top: 2rpx solid #F1F5F9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn {
|
|||
|
|
padding: 16rpx 32rpx;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn.cancel {
|
|||
|
|
background: #F3F4F6;
|
|||
|
|
color: #64748B;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn.pay {
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
color: #FFFFFF;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 加载更多 */
|
|||
|
|
.load-more {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 32rpx;
|
|||
|
|
color: #64748B;
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 空状态 */
|
|||
|
|
.empty-state {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 120rpx 32rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-title {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1E293B;
|
|||
|
|
margin-top: 32rpx;
|
|||
|
|
margin-bottom: 16rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-desc {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #64748B;
|
|||
|
|
margin-bottom: 48rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-action {
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
color: #FFFFFF;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
padding: 28rpx 64rpx;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
</style>
|