772 lines
17 KiB
Vue
772 lines
17 KiB
Vue
<template>
|
||
<view class="order-list-page">
|
||
<!-- 状态筛选标签 -->
|
||
<view class="status-tabs">
|
||
<view
|
||
class="status-tab"
|
||
v-for="tab in statusTabs"
|
||
:key="tab.value"
|
||
:class="{ active: currentStatus === tab.value }"
|
||
@click="switchStatus(tab.value)"
|
||
>
|
||
{{ tab.label }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 订单列表 -->
|
||
<scroll-view
|
||
scroll-y
|
||
class="order-scroll"
|
||
@scrolltolower="loadMore"
|
||
:refresher-enabled="true"
|
||
:refresher-triggered="refreshing"
|
||
@refresherrefresh="refresh"
|
||
>
|
||
<view class="order-list">
|
||
<view
|
||
class="order-item"
|
||
v-for="order in orderList"
|
||
:key="order.order_id"
|
||
@click="goToDetail(order)"
|
||
>
|
||
<!-- 订单头部 -->
|
||
<view class="order-header">
|
||
<view class="order-info">
|
||
<text class="order-no">订单号:{{ order.order_id }}</text>
|
||
<text class="order-date">{{ order.created_at }}</text>
|
||
</view>
|
||
<view class="order-status-tag" :class="getStatusClass(order.status)">
|
||
{{ getStatusText(order.status) }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品列表 -->
|
||
<view class="order-goods">
|
||
<view
|
||
class="goods-item"
|
||
v-for="(item, index) in (order.items || order.product_list || [order])"
|
||
:key="index"
|
||
>
|
||
<view class="goods-image">
|
||
<image
|
||
:src="item.product_image || item.main_image_url || item.product_main_image_url || item.image"
|
||
mode="aspectFill"
|
||
@error="handleImageError"
|
||
></image>
|
||
</view>
|
||
<view class="goods-info">
|
||
<view class="goods-name">{{ item.product_name || item.name || order.product_name }}</view>
|
||
<view class="goods-spec" v-if="item.sku_name || item.spec">
|
||
{{ item.sku_name || item.spec }}
|
||
</view>
|
||
<view class="goods-price-row">
|
||
<view class="goods-price">
|
||
<text class="price-symbol">¥</text>
|
||
<text class="price-value">{{ formatPrice(item.price || order.price || item.sku_price) }}</text>
|
||
</view>
|
||
<view class="goods-quantity">x{{ item.quantity || order.quantity || 1 }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 订单底部 -->
|
||
<view class="order-footer">
|
||
<view class="order-total">
|
||
<text class="total-label">共{{ getTotalQuantity(order) }}件商品 合计:</text>
|
||
<text class="total-price">¥{{ formatPrice(order.final_price || order.payable_amount || order.total_price || order.amount || order.total_amount) }}</text>
|
||
</view>
|
||
<view class="order-actions">
|
||
<view
|
||
class="action-btn"
|
||
v-for="action in getOrderActions(order)"
|
||
:key="action.label"
|
||
:class="action.class"
|
||
@click.stop="handleAction(action, order)"
|
||
>
|
||
{{ action.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-if="!loading && orderList.length === 0" class="empty-state">
|
||
<text class="empty-icon">📦</text>
|
||
<text class="empty-text">暂无订单</text>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="loading && orderList.length > 0" class="loading-more">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 没有更多 -->
|
||
<view v-if="!hasMore && orderList.length > 0" class="no-more">
|
||
<text class="no-more-text">没有更多订单了</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '@/api/request.js';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
currentStatus: 'all', // 当前选中的状态
|
||
statusTabs: [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '待支付', value: 1 },
|
||
// { label: '支付失败', value: 2 },
|
||
{ label: '待发货', value: 3 },
|
||
{ label: '待收货', value: 4 },
|
||
{ label: '已完成', value: 5 },
|
||
// { label: '已取消', value: 6 },
|
||
// { label: '已退款', value: 7 }
|
||
],
|
||
orderList: [],
|
||
loading: false,
|
||
refreshing: false,
|
||
page: 1,
|
||
pageSize: 10,
|
||
hasMore: true,
|
||
statusConfig: {
|
||
1: { label: '待支付', class: 'pending' },
|
||
2: { label: '支付失败', class: 'cancelled' },
|
||
3: { label: '待发货', class: 'warning' },
|
||
4: { label: '待收货', class: 'warning' },
|
||
5: { label: '已完成', class: 'success' },
|
||
6: { label: '已取消', class: 'cancelled' },
|
||
7: { label: '已退款', class: 'cancelled' }
|
||
}
|
||
};
|
||
},
|
||
onLoad(options) {
|
||
// 如果从其他页面传入状态参数,切换到对应状态
|
||
if (options.status) {
|
||
const statusValue = Number(options.status);
|
||
this.currentStatus = Number.isNaN(statusValue) ? options.status : statusValue;
|
||
}
|
||
this.loadOrderList();
|
||
},
|
||
onShow() {
|
||
// 页面显示时刷新列表(从订单详情返回时)
|
||
this.refresh();
|
||
},
|
||
methods: {
|
||
// 格式化价格(分转元)
|
||
formatPrice(value) {
|
||
if (!value && value !== 0) {
|
||
return '0.00';
|
||
}
|
||
// 如果已经是元为单位,直接返回
|
||
// if (value < 1000) {
|
||
// return parseFloat(value).toFixed(2);
|
||
// }
|
||
// 分转换为元
|
||
value = value / 100;
|
||
return value.toFixed(2);
|
||
},
|
||
|
||
// 格式化日期
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const date = new Date(dateStr);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
},
|
||
|
||
// 兼容数字与旧字符串状态
|
||
normalizeStatus(status) {
|
||
if (status === null || status === undefined) return null;
|
||
const numericStatus = Number(status);
|
||
if (!Number.isNaN(numericStatus)) {
|
||
return numericStatus;
|
||
}
|
||
const legacyMap = {
|
||
'pending': 1,
|
||
'paid': 3,
|
||
'shipped': 4,
|
||
'completed': 5,
|
||
'cancelled': 6,
|
||
'refunded': 7,
|
||
'failed': 2,
|
||
'pay_failed': 2
|
||
};
|
||
return legacyMap[status] !== undefined ? legacyMap[status] : status;
|
||
},
|
||
|
||
// 获取状态文本
|
||
getStatusText(status) {
|
||
const normalized = this.normalizeStatus(status);
|
||
if (typeof normalized === 'number' && this.statusConfig[normalized]) {
|
||
return this.statusConfig[normalized].label;
|
||
}
|
||
const legacyTextMap = {
|
||
'pending': '待付款',
|
||
'paid': '待发货',
|
||
'shipped': '待收货',
|
||
'completed': '已完成',
|
||
'cancelled': '已取消',
|
||
'refunded': '已退款'
|
||
};
|
||
return legacyTextMap[normalized] || legacyTextMap[status] || status || '未知';
|
||
},
|
||
|
||
// 获取状态样式类
|
||
getStatusClass(status) {
|
||
const normalized = this.normalizeStatus(status);
|
||
if (typeof normalized === 'number' && this.statusConfig[normalized]) {
|
||
return this.statusConfig[normalized].class;
|
||
}
|
||
const classMap = {
|
||
'pending': 'pending',
|
||
'paid': 'warning',
|
||
'shipped': 'warning',
|
||
'completed': 'success',
|
||
'cancelled': 'cancelled',
|
||
'refunded': 'cancelled'
|
||
};
|
||
return classMap[normalized] || classMap[status] || '';
|
||
},
|
||
|
||
// 获取订单操作按钮
|
||
getOrderActions(order) {
|
||
const status = order.status;
|
||
const actions = [];
|
||
const normalized = this.normalizeStatus(status);
|
||
|
||
if (typeof normalized === 'number') {
|
||
switch (normalized) {
|
||
case 1: // 待支付
|
||
case 2: // 支付失败
|
||
actions.push({ label: '取消订单', action: 'cancel', class: 'cancel-btn' });
|
||
actions.push({ label: '立即付款', action: 'pay', class: 'primary-btn' });
|
||
break;
|
||
case 3: // 待发货
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
break;
|
||
case 4: // 待收货
|
||
actions.push({ label: '查看物流', action: 'logistics', class: 'default-btn' });
|
||
actions.push({ label: '确认收货', action: 'confirm', class: 'primary-btn' });
|
||
break;
|
||
case 5: // 已完成
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
actions.push({ label: '再次购买', action: 'rebuy', class: 'primary-btn' });
|
||
break;
|
||
default:
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
break;
|
||
}
|
||
return actions;
|
||
}
|
||
|
||
// 兼容旧字符串状态
|
||
if (status === 'pending') {
|
||
actions.push({ label: '取消订单', action: 'cancel', class: 'cancel-btn' });
|
||
actions.push({ label: '立即付款', action: 'pay', class: 'primary-btn' });
|
||
} else if (status === 'paid') {
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
} else if (status === 'shipped') {
|
||
actions.push({ label: '查看物流', action: 'logistics', class: 'default-btn' });
|
||
actions.push({ label: '确认收货', action: 'confirm', class: 'primary-btn' });
|
||
} else if (status === 'completed') {
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
actions.push({ label: '再次购买', action: 'rebuy', class: 'primary-btn' });
|
||
} else {
|
||
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||
}
|
||
|
||
return actions;
|
||
},
|
||
|
||
// 获取订单商品总数量
|
||
getTotalQuantity(order) {
|
||
const items = order.items || order.product_list;
|
||
if (items && Array.isArray(items)) {
|
||
return items.reduce((sum, item) => sum + (parseInt(item.quantity) || 1), 0);
|
||
}
|
||
return parseInt(order.quantity) || 1;
|
||
},
|
||
|
||
// 切换状态
|
||
switchStatus(status) {
|
||
if (this.currentStatus === status) return;
|
||
this.currentStatus = status;
|
||
this.page = 1;
|
||
this.hasMore = true;
|
||
this.orderList = [];
|
||
this.loadOrderList();
|
||
},
|
||
|
||
// 加载订单列表
|
||
async loadOrderList() {
|
||
if (this.loading || !this.hasMore) return;
|
||
|
||
this.loading = true;
|
||
try {
|
||
const params = {
|
||
page: this.page,
|
||
page_size: this.pageSize
|
||
};
|
||
|
||
// 如果不是全部,添加状态筛选
|
||
if (this.currentStatus !== 'all') {
|
||
params.status = this.currentStatus;
|
||
}
|
||
|
||
const response = await request('xcx/orders', 'GET', params);
|
||
|
||
// 处理返回数据
|
||
let orders = [];
|
||
if (Array.isArray(response)) {
|
||
orders = response;
|
||
} else if (response.list && Array.isArray(response.list)) {
|
||
orders = response.list;
|
||
} else if (response.data && Array.isArray(response.data)) {
|
||
orders = response.data;
|
||
}
|
||
|
||
if (orders.length < this.pageSize) {
|
||
this.hasMore = false;
|
||
}
|
||
|
||
if (this.page === 1) {
|
||
this.orderList = orders;
|
||
} else {
|
||
this.orderList = [...this.orderList, ...orders];
|
||
}
|
||
|
||
this.page++;
|
||
} catch (error) {
|
||
console.error('加载订单列表失败:', error);
|
||
// 如果是第一页且没有数据,显示空状态
|
||
if (this.page === 1) {
|
||
this.orderList = [];
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
this.refreshing = false;
|
||
}
|
||
},
|
||
|
||
// 刷新列表
|
||
refresh() {
|
||
this.refreshing = true;
|
||
this.page = 1;
|
||
this.hasMore = true;
|
||
this.orderList = [];
|
||
this.loadOrderList();
|
||
},
|
||
|
||
// 加载更多
|
||
loadMore() {
|
||
if (!this.loading && this.hasMore) {
|
||
this.loadOrderList();
|
||
}
|
||
},
|
||
|
||
// 处理订单操作
|
||
handleAction(action, order) {
|
||
switch (action.action) {
|
||
case 'pay':
|
||
this.payOrder(order);
|
||
break;
|
||
case 'cancel':
|
||
this.cancelOrder(order);
|
||
break;
|
||
case 'confirm':
|
||
this.confirmOrder(order);
|
||
break;
|
||
case 'logistics':
|
||
this.viewLogistics(order);
|
||
break;
|
||
case 'rebuy':
|
||
this.rebuyOrder(order);
|
||
break;
|
||
case 'detail':
|
||
default:
|
||
this.goToDetail(order);
|
||
break;
|
||
}
|
||
},
|
||
|
||
// 跳转到订单详情
|
||
goToDetail(order) {
|
||
const orderId = order.id || order.order_id;
|
||
if (orderId) {
|
||
uni.navigateTo({
|
||
url: `/pages/order/detail?id=${orderId}`
|
||
});
|
||
}
|
||
},
|
||
|
||
// 支付订单
|
||
async payOrder(order) {
|
||
uni.showToast({
|
||
title: '支付功能开发中',
|
||
icon: 'none'
|
||
});
|
||
// TODO: 实现支付逻辑
|
||
},
|
||
|
||
// 取消订单
|
||
async cancelOrder(order) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要取消这个订单吗?',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
await request(`xcx/order/${order.id || order.order_id}/cancel`, 'POST');
|
||
uni.showToast({
|
||
title: '订单已取消',
|
||
icon: 'success'
|
||
});
|
||
this.refresh();
|
||
} catch (error) {
|
||
console.error('取消订单失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '取消订单失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 确认收货
|
||
async confirmOrder(order) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定已收到商品吗?',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
await request(`xcx/order/${order.id || order.order_id}/confirm`, 'POST');
|
||
uni.showToast({
|
||
title: '确认收货成功',
|
||
icon: 'success'
|
||
});
|
||
this.refresh();
|
||
} catch (error) {
|
||
console.error('确认收货失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '确认收货失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 查看物流
|
||
viewLogistics(order) {
|
||
uni.showToast({
|
||
title: '物流功能开发中',
|
||
icon: 'none'
|
||
});
|
||
// TODO: 实现物流查看逻辑
|
||
},
|
||
|
||
// 再次购买
|
||
rebuyOrder(order) {
|
||
uni.showToast({
|
||
title: '再次购买功能开发中',
|
||
icon: 'none'
|
||
});
|
||
// TODO: 实现再次购买逻辑
|
||
},
|
||
|
||
// 图片加载错误处理
|
||
handleImageError(e) {
|
||
console.log('图片加载失败', e);
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.order-list-page {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.status-tabs {
|
||
background-color: #fff;
|
||
display: flex;
|
||
padding: 20rpx 24rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.status-tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 16rpx 0;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
position: relative;
|
||
}
|
||
|
||
.status-tab.active {
|
||
color: #9810fa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status-tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60rpx;
|
||
height: 4rpx;
|
||
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||
border-radius: 2rpx;
|
||
}
|
||
|
||
.order-scroll {
|
||
flex: 1;
|
||
height: calc(100vh - 100rpx);
|
||
}
|
||
|
||
.order-list {
|
||
padding: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.order-item {
|
||
background-color: #fff;
|
||
border-radius: 24rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.order-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
padding-bottom: 24rpx;
|
||
border-bottom: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.order-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.order-no {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-date {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.order-status-tag {
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-status-tag.pending {
|
||
background-color: #fff3e0;
|
||
color: #f57c00;
|
||
}
|
||
|
||
.order-status-tag.warning {
|
||
background-color: #e1bee7;
|
||
color: #7b1fa2;
|
||
}
|
||
|
||
.order-status-tag.success {
|
||
background-color: #c8e6c9;
|
||
color: #388e3c;
|
||
}
|
||
|
||
.order-status-tag.cancelled {
|
||
background-color: #ffcdd2;
|
||
color: #c62828;
|
||
}
|
||
|
||
.order-goods {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.goods-item {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.goods-image {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
background-color: #f5f5f5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.goods-image image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.goods-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.goods-name {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.goods-spec {
|
||
margin-top: 8rpx;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.goods-price-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.goods-price {
|
||
color: #e7000b;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.price-symbol {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.price-value {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.goods-quantity {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.order-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 24rpx;
|
||
border-top: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.order-total {
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.total-label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.total-price {
|
||
font-size: 32rpx;
|
||
color: #e7000b;
|
||
font-weight: bold;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.order-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 12rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.default-btn {
|
||
border: 2rpx solid #ddd;
|
||
color: #666;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||
color: #fff;
|
||
}
|
||
|
||
.cancel-btn {
|
||
border: 2rpx solid #ddd;
|
||
color: #999;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 120rpx 0;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 120rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.loading-more,
|
||
.no-more {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
}
|
||
|
||
.loading-text,
|
||
.no-more-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
</style>
|