upload project source code
This commit is contained in:
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal file
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal file
@@ -0,0 +1,576 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user