wx-shop/pages/order/index.vue
2025-12-04 23:14:35 +08:00

772 lines
17 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="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>