Files
device-voice-h5/pages/order-list/order-list.vue
luo 8027916e7a
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 57s
fix: ui
2026-07-30 12:17:32 +08:00

730 lines
15 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="order-number" @tap.stop="copyOrderNo(item.order_no)">
<text class="order-label">订单号</text>
<text class="order-no">{{ item.order_no || '-' }}</text>
<view class="copy-icon">
<view class="copy-back"></view>
<view class="copy-front"></view>
</view>
</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 class="order-action-button detail-action-button"
@tap.stop="openOrderDetail(item)">查看详情</button>
<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 !== 1" 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>
</scroll-view>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { 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 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 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) => {
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;
};
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 openOrderDetail = (order) => {
if (!order?.order_id) 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 = (order) => {
if (!order?.order_id) return;
const packageNames = Array.isArray(order.package_names) ? order.package_names : [];
renewalPopupRef.value?.openOrderPayment(order.order_id, packageNames);
};
onShow(() => {
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
return;
}
scheduleOrderStatusRefresh();
});
onMounted(() => {
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;
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: #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;
}
/* Screenshot-aligned order list skin */
.container {
top: var(--window-top);
background: #f8f9fb;
padding-top: 0;
}
.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;
}
.scroll-container {
height: 0;
min-height: 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-label { flex-shrink: 0; }
.order-no {
max-width: 410rpx;
overflow: hidden;
color: #777b84;
font-size: 26rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-icon {
position: relative;
width: 30rpx;
height: 30rpx;
margin-left: 14rpx;
flex-shrink: 0;
}
.copy-back,
.copy-front {
position: absolute;
width: 15rpx;
height: 15rpx;
border: 3rpx solid #717782;
border-radius: 4rpx;
box-sizing: border-box;
}
.copy-back { top: 3rpx; left: 3rpx; }
.copy-front { right: 2rpx; bottom: 2rpx; background: #fff; }
.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: center;
justify-content: flex-start;
gap: 16rpx;
margin-top: 0;
padding: 0 0 28rpx;
border-top: 0;
flex-direction: row;
}
.order-card .card-body .card-footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-top: 0;
padding: 0 0 28rpx;
border-top: 0;
}
.created-time {
min-width: 0;
flex: 1;
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;
gap: 16rpx;
margin-left: auto;
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;
}
.detail-action-button {
border-color: #d9dde2;
background: #fff;
color: #17191c;
}
.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: #147de2; }
.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>