Files
device-voice-h5/pages/order-list/order-list.vue
luo b95daedba6
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m1s
fix: icons和布局
2026-07-29 17:29:55 +08:00

596 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="container">
<view class="filter-tabs">
<view
v-for="(item, index) in filterOptions"
:key="index"
class="tab-item"
:class="{ active: filterIndex === index }"
@tap="onFilterChange(index)"
>
{{ item.label }}
<view v-if="filterIndex === index" class="tab-line"></view>
</view>
</view>
<scroll-view scroll-y class="scroll-container" @scrolltolower="loadMore">
<view v-if="orderList.length === 0 && !loading" class="empty-state">
<image class="empty-icon" src="/static/order.png" mode="aspectFit" alt="我的订单"></image>
<view class="empty-title">暂无订单</view>
<view class="empty-desc">当前账号下暂无订单信息</view>
</view>
<view v-else class="order-list">
<view class="order-card" v-for="item in orderList" :key="item.order_id" @tap="openOrderDetail(item)">
<view class="card-header">
<view class="header-left">
<view class="header-row">
<text class="field-tag">订单号</text>
<text class="order-no">{{ item.order_no }}</text>
</view>
</view>
<view class="status-badge" :class="getStatusClass(item.payment_status)">
{{ item.payment_status_name }}
</view>
</view>
<view class="card-body">
<view class="info-grid">
<view class="info-item">
<view class="info-label">套餐名称</view>
<view class="info-value">{{ getPackageNamesText(item) }}</view>
</view>
<view class="info-item">
<view class="info-label">资产标识</view>
<view class="info-value">{{ item.asset_identifier || '-' }}</view>
</view>
<view class="info-item">
<view class="info-label">订单金额</view>
<view class="info-value amount">¥{{ formatMoney(item.total_amount) }}</view>
</view>
<view class="info-item">
<view class="info-label">下单时间</view>
<view class="info-value">{{ item.created_at }}</view>
</view>
</view>
<view class="card-footer">
<button v-if="item.payment_status === 1" class="btn-pay" :disabled="orderPayingId !== null"
@tap.stop="showOrderPaymentMethods(item)">
{{ isOrderPaying(item) ? '处理中...' : '立即支付' }}
</button>
<up-button class="btn-renew" type="primary" size="small"
:disabled="orderPayingId !== null" @tap.stop="startOrderRenewal(item)">立即续费</up-button>
</view>
</view>
</view>
<view class="load-more" v-if="orderList.length > 0">
<text v-if="loading">加载中...</text>
<text v-else-if="noMore">没有更多了</text>
<text v-else>上拉加载更多</text>
</view>
</view>
</scroll-view>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { assetApi, orderApi } from '@/api/index.js';
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
import { useUserStore } from '@/store/index.js';
import {
consumePendingPaymentRefresh,
handlePaymentError,
isValidAlipayPaymentLink,
isValidWechatPayConfig,
openAlipayPayment,
PAYMENT_REFRESH_TARGETS,
showPaymentToast,
wechatH5Pay
} from '@/utils/payment.js';
import { formatMoney } from '@/utils/display.js';
import { getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
const userStore = useUserStore();
const orderList = reactive([]);
const loading = ref(false);
const noMore = ref(false);
const page = ref(1);
const orderPayingId = ref(null);
const renewalPopupRef = ref(null);
const pageSize = 10;
const filterIndex = ref(0);
const allowedPaymentMethods = ref([]);
let assetTypePromise = null;
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
const filterOptions = [
{ label: '全部', value: null },
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已取消', value: 3 },
{ label: '已退款', value: 4 }
];
const getStatusClass = (status) => {
const classMap = {
1: 'tag-warning',
2: 'tag-success',
3: 'tag-secondary',
4: 'tag-info'
};
return classMap[status] || '';
};
const loadAssetType = async () => {
if (!userStore.state.identifier) return;
if (assetTypePromise) return assetTypePromise;
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
.then((data) => {
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
})
.catch((error) => {
console.error('加载资产类型失败', error);
})
.finally(() => {
assetTypePromise = null;
});
return assetTypePromise;
};
const resetOrderListAndLoad = () => {
page.value = 1;
noMore.value = false;
loadOrderList();
};
const scheduleOrderStatusRefresh = () => {
uni.showToast({
title: '正在同步支付状态',
icon: 'none'
});
resetOrderListAndLoad();
setTimeout(() => {
resetOrderListAndLoad();
}, 1500);
setTimeout(() => {
resetOrderListAndLoad();
}, 3000);
};
const onFilterChange = (index) => {
filterIndex.value = index;
resetOrderListAndLoad();
};
const loadOrderList = async (append = false) => {
if (loading.value || (append && noMore.value)) return;
loading.value = true;
const paymentStatus = filterOptions[filterIndex.value].value;
try {
const data = await orderApi.getList(
userStore.state.identifier,
page.value,
pageSize,
paymentStatus
);
const newData = (data.items || []).map((item) => ({
...item,
asset_identifier: item.asset_identifier || item.virtual_no || item.VirtualNo || item.imei || '',
payment_status_name: item.payment_status === 1
? '待支付'
: item.payment_status === 2
? '已支付'
: item.payment_status === 3
? '已取消'
: item.payment_status === 4
? '已退款'
: '未知',
created_at: item.created_at ? item.created_at.split('T').join(' ').slice(0, 19) : ''
}));
if (append) {
orderList.push(...newData);
} else {
orderList.splice(0, orderList.length, ...newData);
}
if (newData.length < pageSize) {
noMore.value = true;
} else {
page.value += 1;
}
} catch (error) {
console.error('加载订单列表失败', error);
}
loading.value = false;
};
const loadMore = () => {
if (!noMore.value) {
loadOrderList(true);
}
};
const isOrderPaying = (order) => orderPayingId.value === order.order_id;
const getPackageNamesText = (order) => {
const packageNames = Array.isArray(order?.package_names)
? order.package_names.filter(Boolean)
: [];
return packageNames.join('、') || '-';
};
const openOrderDetail = (order) => {
if (!order?.order_id || orderPayingId.value !== null) return;
uni.navigateTo({ url: `/pages/order-detail/order-detail?id=${order.order_id}` });
};
const startOrderRenewal = (order) => {
const packageIds = Array.isArray(order?.package_ids) ? order.package_ids.filter(Boolean) : [];
if (!packageIds.length) {
uni.showToast({ title: '当前订单暂无可续费套餐', icon: 'none' });
return;
}
const packageNames = Array.isArray(order.package_names) ? order.package_names : [];
renewalPopupRef.value?.open(packageIds, packageNames);
};
const showOrderPaymentMethods = async (order) => {
if (orderPayingId.value !== null || !order?.order_id) return;
await loadAssetType();
const options = paymentMethodOptions.value;
if (!options.length) {
uni.showToast({ title: '暂无可用支付方式', icon: 'none' });
return;
}
uni.showActionSheet({
itemList: options.map((item) => item.label),
success: ({ tapIndex }) => {
const selectedMethod = options[tapIndex]?.value;
if (selectedMethod) {
handleOrderPayment(order, selectedMethod);
}
}
});
};
const handleOrderPayment = async (order, paymentMethod) => {
if (orderPayingId.value !== null || !order?.order_id) return;
orderPayingId.value = order.order_id;
uni.showLoading({
title: '处理中...',
mask: true
});
try {
const payData = await orderApi.pay(order.order_id, paymentMethod);
uni.hideLoading();
if (paymentMethod === 'wallet') {
await confirmOrderStatus(order);
setTimeout(() => {
resetOrderListAndLoad();
}, 1500);
return;
}
if (paymentMethod === 'wechat') {
if (!isValidWechatPayConfig(payData?.pay_config)) {
uni.showToast({
title: '支付参数获取失败',
icon: 'none'
});
return;
}
try {
await wechatH5Pay(payData.pay_config);
await confirmOrderStatus(order);
setTimeout(() => {
resetOrderListAndLoad();
}, 1500);
} catch (error) {
handlePaymentError(error);
}
return;
}
if (!isValidAlipayPaymentLink(payData?.payment_link)) {
uni.showToast({
title: '支付链接获取失败',
icon: 'none'
});
return;
}
await openAlipayPayment(payData.payment_link, PAYMENT_REFRESH_TARGETS.ORDER_LIST);
} catch (error) {
uni.hideLoading();
console.error('订单支付失败', error);
uni.showToast({
title: error.msg || error.message || '支付失败,请稍后重试',
icon: 'none'
});
} finally {
orderPayingId.value = null;
}
};
const confirmOrderStatus = async (order) => {
try {
const detail = await orderApi.getDetail(order.order_id);
const status = detail?.payment_status ?? detail?.order?.payment_status;
showPaymentToast(status === 2, status === 2 ? '支付成功' : '支付结果确认中');
} catch (error) {
console.error('确认订单支付状态失败', error);
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
}
};
onShow(() => {
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
return;
}
scheduleOrderStatusRefresh();
});
onMounted(() => {
loadAssetType();
loadOrderList();
});
</script>
<style lang="scss" scoped>
.container {
position: fixed;
top: var(--window-top);
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
padding-top: 0;
}
.filter-tabs {
display: flex;
background: #fff;
border-radius: 20rpx;
z-index: 100;
flex-shrink: 0;
margin-top: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
.tab-item {
position: relative;
flex: 1;
text-align: center;
font-size: 28rpx;
color: #999;
padding: 28rpx 0;
transition: all 0.2s;
&.active {
color: #333;
font-weight: 600;
font-size: 30rpx;
}
.tab-line {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3rpx;
}
}
}
.scroll-container {
flex: 1;
height: 100%;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 40rpx;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #999;
text-align: center;
}
}
.order-card {
background: #fff;
border-radius: 20rpx;
margin-bottom: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 28rpx 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.header-left {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.header-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.field-tag {
font-size: 20rpx;
color: #999;
background: #f5f6f7;
padding: 2rpx 10rpx;
border-radius: 4rpx;
}
.order-no {
font-size: 26rpx;
font-weight: 600;
color: #333;
font-family: monospace;
}
}
.card-body {
padding: 24rpx 28rpx;
.info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16rpx;
.info-item {
display: flex;
align-items: baseline;
gap: 8rpx;
.info-label {
font-size: 22rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #333;
flex: 1;
min-width: 0;
word-break: break-all;
&.amount {
color: #ff6b6b;
font-weight: 700;
font-size: 30rpx;
}
}
}
}
.card-footer {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 16rpx;
.btn-pay {
background: #1890ff;
color: #fff;
border: none;
border-radius: 20rpx;
height: 64rpx;
padding: 0 28rpx;
font-size: 26rpx;
font-weight: 500;
width: auto;
min-width: 168rpx;
line-height: 64rpx;
&[disabled] {
opacity: 0.7;
}
&::after {
border: none;
}
}
.btn-renew {
flex: 1;
width: 100%;
min-width: 0;
}
}
}
}
.status-badge {
padding: 8rpx 20rpx;
border-radius: 30rpx;
font-size: 22rpx;
font-weight: 500;
&.tag-warning {
background: #fff7e6;
color: #fa8c16;
}
&.tag-success {
background: #e6f7ff;
color: #1890ff;
}
&.tag-secondary {
background: #f5f5f5;
color: #999;
}
&.tag-info {
background: #f0f0f0;
color: #666;
}
}
.load-more {
text-align: center;
padding: 32rpx;
color: #999;
font-size: 24rpx;
}
</style>