Files
----/前端源码/uni-app/components/screens/ProfileOrdersScreen.vue

577 lines
14 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="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环境使用confirmuni-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>