Files
device-voice-h5/pages/order-list/order-list.vue
luo d3d606b1a7
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m14s
fix: ui
2026-08-13 16:05:14 +08:00

759 lines
15 KiB
Vue
Raw Permalink 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 class="filter-tab-indicator" :style="filterIndicatorStyle"></view>
<view
v-for="(item, index) in filterOptions"
:key="index"
class="tab-item"
:class="{ active: filterIndex === index }"
@tap="onFilterChange(index)"
>
{{ item.label }}
</view>
</view>
<view v-if="!ordersLoaded || (loading && orderList.length === 0)" class="loading-state">
<up-loading-icon text="加载中" size="30"></up-loading-icon>
</view>
<view v-else-if="orderList.length === 0" 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">
<view class="card-header">
<view class="order-number" @tap.stop="copyOrderNo(item.order_no)">
<text class="order-no">{{ item.order_no || '-' }}</text>
<image class="copy-icon" src="/static/复制.png" mode="aspectFit"></image>
</view>
<view class="status-badge" :class="getStatusClass(item.payment_status)">
{{ item.payment_status_name }}
</view>
</view>
<view class="card-body">
<view class="package-row">
<view class="package-content">
<view class="package-name">{{ getPackageNamesText(item) }}</view>
<view class="package-desc">高速流量 · 全国通用 · 不限速</view>
</view>
<view class="order-amount">¥{{ formatMoney(item.total_amount) }}</view>
</view>
<view class="card-divider"></view>
<view class="card-footer">
<view class="created-time">下单时间{{ item.created_at || '-' }}</view>
<view class="order-actions">
<button v-if="item.payment_status === 1" class="order-action-button primary-action-button"
@tap.stop="showOrderPaymentMethods(item)">
立即支付
</button>
<button v-if="item.payment_status === 2" class="order-action-button primary-action-button"
@tap.stop="startOrderRenewal(item)">立即续费</button>
</view>
</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>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { onReachBottom, onShow } from '@dcloudio/uni-app';
import { orderApi } from '@/api/index.js';
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
import { useUserStore } from '@/store/index.js';
import {
consumePendingPaymentRefresh,
PAYMENT_REFRESH_TARGETS
} from '@/utils/payment.js';
import { formatMoney } from '@/utils/display.js';
const userStore = useUserStore();
const orderList = reactive([]);
const loading = ref(false);
const ordersLoaded = ref(false);
const noMore = ref(false);
const page = ref(1);
const renewalPopupRef = ref(null);
const pageSize = 10;
const filterIndex = ref(0);
const filterOptions = [
{ label: '全部', value: null },
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已取消', value: 3 },
{ label: '已退款', value: 4 }
];
const filterIndicatorStyle = computed(() => ({
left: `calc(${filterIndex.value * 20}% + 8rpx)`
}));
const getStatusClass = (status) => {
const classMap = {
1: 'tag-warning',
2: 'tag-success',
3: 'tag-secondary',
4: 'tag-info'
};
return classMap[status] || '';
};
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) => {
if (filterIndex.value === index) return;
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,
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;
ordersLoaded.value = true;
};
const loadMore = () => {
if (!noMore.value) {
loadOrderList(true);
}
};
const getPackageNamesText = (order) => {
const packageNames = Array.isArray(order?.package_names)
? order.package_names.filter(Boolean)
: [];
return packageNames.join('、') || '-';
};
const copyOrderNo = (orderNo) => {
if (!orderNo) return;
uni.setClipboardData({
data: String(orderNo),
success: () => uni.showToast({ title: '订单号已复制', icon: 'success' })
});
};
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 = (order) => {
if (!order?.order_id) return;
const packageNames = Array.isArray(order.package_names) ? order.package_names : [];
if (String(order.payment_method || '').toLowerCase() === 'wallet') {
renewalPopupRef.value?.payOrderDirectly(order.order_id, 'wallet');
return;
}
renewalPopupRef.value?.openOrderPayment(order.order_id, packageNames);
};
onShow(() => {
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
return;
}
scheduleOrderStatusRefresh();
});
onMounted(() => {
loadOrderList();
});
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
gap: 0;
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
min-height: 100vh;
padding: 0 0 48rpx;
}
.filter-tabs {
display: flex;
background: #fff;
border-radius: 20rpx;
z-index: 100;
flex-shrink: 0;
margin: 20rpx 24rpx 0;
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: var(--primary);
border-radius: 3rpx;
}
}
}
.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;
flex-direction: column;
gap: 16rpx;
.order-action-button {
width: 100%;
min-width: 0;
margin: 0;
}
.payment-action-buttons {
display: flex;
width: 100%;
gap: 16rpx;
.order-action-button {
flex: 1;
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: #eaf6ec;
color: var(--primary);
}
&.tag-secondary {
background: #f5f5f5;
color: #999;
}
&.tag-info {
background: #f0f0f0;
color: #666;
}
}
.load-more {
text-align: center;
padding: 32rpx;
color: #999;
font-size: 24rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 500rpx;
}
/* Screenshot-aligned order list skin */
.container {
position: fixed;
top: 88rpx;
right: 0;
bottom: 0;
left: 0;
background: #f8f9fb;
padding: 0;
gap: 0;
padding-top: 0;
height: auto;
min-height: 0;
overflow: hidden;
}
.filter-tabs {
display: flex;
align-items: stretch;
flex-shrink: 0;
margin-top: 0;
padding: 0 20rpx;
background: #fff;
border-radius: 0;
border-bottom: 1rpx solid #f0f1f3;
box-shadow: none;
overflow: hidden;
}
.filter-tabs .tab-item {
position: relative;
flex: 1;
min-width: 128rpx;
padding: 28rpx 12rpx 24rpx;
color: #777b84;
font-size: 28rpx;
text-align: center;
white-space: nowrap;
box-sizing: border-box;
}
.filter-tabs .tab-item.active {
color: var(--primary);
font-size: 28rpx;
font-weight: 600;
}
.filter-tabs .tab-line {
bottom: 0;
width: 42rpx;
height: 6rpx;
background: var(--primary);
border-radius: 8rpx;
}
.container .filter-tabs {
position: relative;
margin-top: 0;
padding: 8rpx;
background: #fff;
border: 0;
border-radius: 16rpx;
overflow: hidden;
}
.container .filter-tabs .tab-item {
position: relative;
z-index: 1;
min-width: 0;
padding: 20rpx 4rpx;
color: var(--text-tertiary);
font-size: 26rpx;
transition: color 0.3s;
}
.container .filter-tabs .tab-item.active {
background: transparent;
color: #fff;
font-size: 26rpx;
font-weight: 600;
}
.filter-tab-indicator {
position: absolute;
top: 8rpx;
width: calc(20% - 16rpx);
height: calc(100% - 16rpx);
background: var(--primary);
border-radius: 12rpx;
transition: left 0.3s ease;
z-index: 0;
}
.order-list {
padding: 20rpx 24rpx 48rpx;
box-sizing: border-box;
}
.order-card {
margin-bottom: 20rpx;
padding: 0 32rpx;
background: #fff;
border: 1rpx solid #f0f1f3;
border-radius: 18rpx;
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.04);
box-sizing: border-box;
}
.order-card .card-header {
align-items: center;
padding: 32rpx 0 24rpx;
border-bottom: 0;
}
.order-number {
display: flex;
align-items: center;
min-width: 0;
color: #777b84;
font-size: 26rpx;
}
.order-no {
max-width: 410rpx;
overflow: hidden;
color: #777b84;
font-size: 26rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-icon {
width: 30rpx;
height: 30rpx;
margin-left: 14rpx;
flex-shrink: 0;
}
.order-card .card-body {
padding: 0 0 28rpx;
}
.package-row {
display: flex;
align-items: center;
min-width: 0;
gap: 20rpx;
}
.package-content {
min-width: 0;
flex: 1;
}
.package-name {
color: #16181c;
font-size: 30rpx;
font-weight: 600;
line-height: 1.25;
word-break: break-all;
}
.package-desc {
margin-top: 14rpx;
color: #8b9099;
font-size: 23rpx;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.order-amount {
align-self: center;
color: #ff302f;
font-size: 30rpx;
font-weight: 600;
white-space: nowrap;
}
.card-divider {
height: 1rpx;
margin: 28rpx 0 20rpx;
background: #edf0f2;
}
.order-card .card-footer {
display: flex;
align-items: stretch;
justify-content: flex-start;
gap: 16rpx;
margin-top: 0;
padding: 0 0 28rpx;
border-top: 0;
flex-direction: column;
}
.order-card .card-body .card-footer {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: 16rpx;
margin-top: 0;
padding: 0 0 28rpx;
border-top: 0;
}
.created-time {
width: 100%;
min-width: 0;
flex: none;
color: #777b84;
font-size: 24rpx;
line-height: 1.3;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.order-actions {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 16rpx;
margin-left: 0;
align-self: flex-end;
flex-shrink: 0;
}
.order-card .order-action-button {
width: 180rpx;
height: 58rpx;
min-width: 0;
margin: 0;
padding: 0 16rpx;
border: 1rpx solid transparent;
border-radius: 12rpx;
font-size: 25rpx;
font-weight: 500;
line-height: 56rpx;
box-sizing: border-box;
white-space: nowrap;
-webkit-tap-highlight-color: transparent;
&::after { border: none; }
}
.order-card .card-body .card-footer .order-action-button {
width: 180rpx;
height: 58rpx;
min-width: 0;
margin: 0;
padding: 0 16rpx;
line-height: 56rpx;
}
.primary-action-button {
border-color: var(--primary);
background: var(--primary);
color: #fff;
}
.order-card .status-badge {
padding: 0 0 0 16rpx;
background: transparent;
border-radius: 0;
font-size: 25rpx;
font-weight: 500;
white-space: nowrap;
}
.order-card .status-badge.tag-warning { color: #f08a00; }
.order-card .status-badge.tag-success { color: var(--primary); }
.order-card .status-badge.tag-secondary { color: #7d828b; }
.order-card .status-badge.tag-info { color: #666d78; }
.load-more {
padding: 24rpx 32rpx 40rpx;
color: #9aa0aa;
font-size: 23rpx;
}
</style>