577 lines
14 KiB
Vue
577 lines
14 KiB
Vue
<template>
|
||
<view class="orders-screen">
|
||
<view class="orders-bg"></view>
|
||
<!-- 状态栏占位 -->
|
||
<view class="status-bar-placeholder"></view>
|
||
|
||
<!-- Header -->
|
||
<view class="orders-header">
|
||
<view class="orders-back-btn" @click="handleBack">
|
||
<text class="orders-back-icon">‹</text>
|
||
</view>
|
||
<text class="orders-title">我的订单</text>
|
||
<view class="orders-header-placeholder"></view>
|
||
</view>
|
||
|
||
<!-- Tabs -->
|
||
<view class="orders-tabs">
|
||
<view v-for="tab in tabs" :key="tab.value" class="orders-tab"
|
||
:class="{ 'orders-tab-active': currentTab === tab.value }" @click="switchTab(tab.value)">
|
||
<text class="orders-tab-text">{{ tab.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Content -->
|
||
<scroll-view scroll-y class="orders-content" refresher-enabled :refresher-triggered="refreshing"
|
||
@refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
|
||
<view class="orders-content-inner">
|
||
<!-- Loading -->
|
||
<view v-if="loading && orders.length === 0" class="orders-loading">
|
||
<text class="orders-loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- Empty -->
|
||
<view v-else-if="orders.length === 0" class="orders-empty">
|
||
<text class="orders-empty-icon">📦</text>
|
||
<text class="orders-empty-text">暂无订单</text>
|
||
</view>
|
||
|
||
<!-- List -->
|
||
<view v-else class="orders-list">
|
||
<view v-for="order in orders" :key="order.out_trade_no" class="order-item">
|
||
<!-- Header -->
|
||
<view class="order-header">
|
||
<text class="order-time">{{ order.paid_at || '待支付' }}</text>
|
||
<text class="order-status" :class="`status-${order.status}`">
|
||
{{ getStatusText(order.status) }}
|
||
</text>
|
||
</view>
|
||
|
||
<!-- Content -->
|
||
<view class="order-content">
|
||
<view class="order-info">
|
||
<text class="order-name">{{ order.description ||
|
||
getBusinessTypeName(order.business_type) }}</text>
|
||
<text class="order-no">订单号:{{ order.out_trade_no }}</text>
|
||
</view>
|
||
<view class="order-price">
|
||
<text class="order-price-symbol">¥</text>
|
||
<text class="order-price-amount">{{ order.total_amount }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Actions -->
|
||
<view class="order-actions">
|
||
<view v-if="order.status === 'pending'" class="order-action-btn order-action-cancel"
|
||
@click="handleCancelOrder(order.out_trade_no)">
|
||
<text class="order-action-text">取消订单</text>
|
||
</view>
|
||
<view v-if="order.status === 'pending'" class="order-action-btn order-action-pay"
|
||
@click="handlePayOrder(order)">
|
||
<text class="order-action-text">继续支付</text>
|
||
</view>
|
||
<view v-if="order.status === 'paid'" class="order-action-btn order-action-view"
|
||
@click="handleViewOrder(order)">
|
||
<text class="order-action-text">查看详情</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Load More -->
|
||
<view v-if="hasMore && !loading" class="orders-loadmore">
|
||
<text class="orders-loadmore-text">加载更多...</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from "vue";
|
||
import { closeOrder, wxPay } from "@/utils/payment";
|
||
import type { QueryOrderResponse } from "@/api/types";
|
||
import { paymentApi } from "@/api/payment";
|
||
|
||
declare const uni: any;
|
||
|
||
const emit = defineEmits<{
|
||
back: [];
|
||
showOrderDetail: [order: QueryOrderResponse];
|
||
}>();
|
||
|
||
const tabs = [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '待支付', value: 'pending' },
|
||
{ label: '已支付', value: 'paid' },
|
||
{ label: '已关闭', value: 'cancelled' },
|
||
];
|
||
|
||
const currentTab = ref('all');
|
||
const loading = ref(false);
|
||
const refreshing = ref(false);
|
||
const orders = ref<QueryOrderResponse[]>([]);
|
||
const page = ref(1);
|
||
const pageSize = 10;
|
||
const hasMore = ref(true);
|
||
|
||
const mapTradeStateToStatus = (tradeState: string): QueryOrderResponse['status'] => {
|
||
const state = (tradeState || '').toUpperCase();
|
||
if (state === 'SUCCESS') return 'paid';
|
||
if (state === 'NOTPAY') return 'pending';
|
||
if (state === 'CLOSED') return 'cancelled';
|
||
if (state === 'REFUND') return 'refunded';
|
||
return 'pending';
|
||
};
|
||
|
||
// 切换标签
|
||
const switchTab = (tab: string) => {
|
||
currentTab.value = tab;
|
||
page.value = 1;
|
||
orders.value = [];
|
||
hasMore.value = true;
|
||
loadOrders();
|
||
};
|
||
|
||
// 加载订单列表
|
||
const loadOrders = async () => {
|
||
if (loading.value) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
const res = await paymentApi.listOrders({
|
||
page_no: page.value,
|
||
page_size: pageSize,
|
||
});
|
||
|
||
const mapped: QueryOrderResponse[] = (res?.items || []).map((item: any) => ({
|
||
out_trade_no: item.out_trade_no,
|
||
transaction_id: item.transaction_id,
|
||
status: mapTradeStateToStatus(item.trade_state),
|
||
total_amount: item.total_amount,
|
||
paid_amount: item.total_amount,
|
||
paid_at: item.success_time,
|
||
business_type: item.business_type,
|
||
business_id: item.id,
|
||
description: item.description,
|
||
}));
|
||
|
||
const filtered = currentTab.value === 'all'
|
||
? mapped
|
||
: mapped.filter(o => o.status === currentTab.value);
|
||
|
||
if (page.value === 1) {
|
||
orders.value = filtered;
|
||
} else {
|
||
orders.value.push(...filtered);
|
||
}
|
||
|
||
hasMore.value = (res?.items || []).length >= pageSize;
|
||
} catch (error: any) {
|
||
console.error('加载订单失败:', error);
|
||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||
} finally {
|
||
loading.value = false;
|
||
refreshing.value = false;
|
||
}
|
||
};
|
||
|
||
// 下拉刷新
|
||
const onRefresh = () => {
|
||
refreshing.value = true;
|
||
page.value = 1;
|
||
orders.value = [];
|
||
hasMore.value = true;
|
||
loadOrders();
|
||
};
|
||
|
||
// 加载更多
|
||
const onLoadMore = () => {
|
||
if (!hasMore.value || loading.value) return;
|
||
page.value++;
|
||
loadOrders();
|
||
};
|
||
|
||
// 取消订单
|
||
const handleCancelOrder = (outTradeNo: string) => {
|
||
// Web环境使用confirm,uni-app环境使用showModal
|
||
if (typeof uni?.showModal === 'function') {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要取消该订单吗?',
|
||
success: async (res: any) => {
|
||
if (res.confirm) {
|
||
const success = await closeOrder(outTradeNo);
|
||
if (success) {
|
||
// 刷新列表
|
||
onRefresh();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
// Web环境使用原生confirm
|
||
const confirmed = confirm('确定要取消该订单吗?');
|
||
if (confirmed) {
|
||
(async () => {
|
||
const success = await closeOrder(outTradeNo);
|
||
if (success) {
|
||
// 刷新列表
|
||
onRefresh();
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
};
|
||
|
||
// 继续支付
|
||
const handlePayOrder = (order: QueryOrderResponse) => {
|
||
(async () => {
|
||
try {
|
||
const result = await wxPay({
|
||
description: order.description || getBusinessTypeName(order.business_type),
|
||
total_amount: order.total_amount,
|
||
business_type: order.business_type as any,
|
||
business_id: order.business_id,
|
||
pay_type: 'jsapi',
|
||
});
|
||
|
||
if (result.success) {
|
||
uni.showToast({ title: '支付成功', icon: 'success' });
|
||
onRefresh();
|
||
return;
|
||
}
|
||
|
||
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
|
||
}
|
||
})();
|
||
};
|
||
|
||
// 查看订单详情
|
||
const handleViewOrder = (order: QueryOrderResponse) => {
|
||
emit('showOrderDetail', order);
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status: string) => {
|
||
const statusMap: Record<string, string> = {
|
||
pending: '待支付',
|
||
paid: '已支付',
|
||
cancelled: '已关闭',
|
||
refunded: '已退款'
|
||
};
|
||
return statusMap[status] || status;
|
||
};
|
||
|
||
// 获取业务类型名称
|
||
const getBusinessTypeName = (type: string) => {
|
||
const typeMap: Record<string, string> = {
|
||
naming_report: '命名报告',
|
||
partner_apply: '推广合伙人',
|
||
test_report: '测名报告',
|
||
fortune_report: '财运报告',
|
||
test: '测试商品'
|
||
};
|
||
return typeMap[type] || type;
|
||
};
|
||
|
||
// 返回
|
||
const handleBack = () => {
|
||
emit('back');
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadOrders();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.orders-screen {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f0efe9;
|
||
position: relative;
|
||
}
|
||
|
||
.status-bar-placeholder {
|
||
height: var(--status-bar-height, 0);
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.orders-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
pointer-events: none;
|
||
opacity: 0.3;
|
||
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
}
|
||
|
||
/* Header */
|
||
.orders-header {
|
||
position: relative;
|
||
z-index: 10;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 32rpx;
|
||
border-bottom: 1rpx solid #dcd3c9;
|
||
background-color: rgba(253, 251, 247, 0.8);
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.orders-back-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
margin-left: -16rpx;
|
||
}
|
||
|
||
.orders-back-icon {
|
||
font-size: 48rpx;
|
||
color: #5a5a5a;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.orders-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #2c2c2c;
|
||
letter-spacing: 0.2em;
|
||
}
|
||
|
||
.orders-header-placeholder {
|
||
width: 64rpx;
|
||
}
|
||
|
||
/* Tabs */
|
||
.orders-tabs {
|
||
display: flex;
|
||
background-color: #fff;
|
||
border-bottom: 1rpx solid #e5e5e5;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.orders-tab {
|
||
flex: 1;
|
||
padding: 24rpx 0;
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
|
||
.orders-tab-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.orders-tab-active .orders-tab-text {
|
||
color: #8b2323;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.orders-tab-active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60rpx;
|
||
height: 4rpx;
|
||
background-color: #8b2323;
|
||
border-radius: 2rpx;
|
||
}
|
||
|
||
/* Content */
|
||
.orders-content {
|
||
flex: 1;
|
||
height: 0;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.orders-content-inner {
|
||
padding: 32rpx;
|
||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
/* Loading & Empty */
|
||
.orders-loading,
|
||
.orders-empty {
|
||
text-align: center;
|
||
padding: 120rpx 0;
|
||
}
|
||
|
||
.orders-loading-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.orders-empty-icon {
|
||
font-size: 120rpx;
|
||
display: block;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.orders-empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
/* Order List */
|
||
.orders-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.order-item {
|
||
background-color: #fffdf9;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #e5e5e5;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.order-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 24rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.order-time {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.order-status {
|
||
font-size: 24rpx;
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.status-paid {
|
||
background-color: #e8f5e9;
|
||
color: #4caf50;
|
||
}
|
||
|
||
.status-pending {
|
||
background-color: #fff3e0;
|
||
color: #ff9800;
|
||
}
|
||
|
||
.status-cancelled {
|
||
background-color: #f5f5f5;
|
||
color: #999;
|
||
}
|
||
|
||
.status-refunded {
|
||
background-color: #ffebee;
|
||
color: #f44336;
|
||
}
|
||
|
||
.order-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.order-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.order-name {
|
||
font-size: 28rpx;
|
||
color: #2c2c2c;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-no {
|
||
font-size: 20rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.order-price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.order-price-symbol {
|
||
font-size: 24rpx;
|
||
color: #8b2323;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.order-price-amount {
|
||
font-size: 36rpx;
|
||
color: #8b2323;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* Actions */
|
||
.order-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
padding: 0 24rpx 24rpx;
|
||
}
|
||
|
||
.order-action-btn {
|
||
padding: 12rpx 32rpx;
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid #e5e5e5;
|
||
}
|
||
|
||
.order-action-text {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.order-action-cancel {
|
||
background-color: #fff;
|
||
}
|
||
|
||
.order-action-pay {
|
||
background-color: #8b2323;
|
||
border-color: #8b2323;
|
||
}
|
||
|
||
.order-action-pay .order-action-text {
|
||
color: #fff;
|
||
}
|
||
|
||
.order-action-view {
|
||
background-color: #fff;
|
||
}
|
||
|
||
/* Load More */
|
||
.orders-loadmore {
|
||
text-align: center;
|
||
padding: 32rpx 0;
|
||
}
|
||
|
||
.orders-loadmore-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
</style>
|