upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View 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环境使用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>