fix: ui
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m9s

This commit is contained in:
luo
2026-07-30 15:41:11 +08:00
parent d0f032aa1c
commit 5f8ff768e6
4 changed files with 332 additions and 55 deletions

View File

@@ -334,7 +334,27 @@
}
};
defineExpose({ open, openOrderPayment });
const payOrderDirectly = async (orderId, method = 'wallet') => {
if (!orderId || submitting.value) return;
submitting.value = true;
uni.showLoading({ title: '准备支付...', mask: true });
try {
await orderApi.pay(orderId, method);
uni.hideLoading();
if (method === 'wallet') {
await confirmOrderStatus(orderId);
}
} catch (error) {
uni.hideLoading();
console.error('直接支付订单失败', error);
uni.showToast({ title: error.msg || error.message || '支付失败,请稍后重试', icon: 'none' });
} finally {
submitting.value = false;
}
};
defineExpose({ open, openOrderPayment, payOrderDirectly });
</script>
<style scoped lang="scss">

View File

@@ -55,10 +55,6 @@
<view class="info-label">充值金额</view>
<view class="info-value text-danger">+¥{{ formatMoney(item.amount) }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">支付方式</view>
<view class="info-value">{{ formatPaymentMethod(item.payment_method) }}</view>
</view>
<view v-if="hasAutoPurchaseStatus(item)" class="info-row flex-row-sb">
<view class="info-label">自动购包</view>
<view class="info-value">{{ getAutoPurchaseStatusText(item.auto_purchase_status) }}</view>
@@ -173,6 +169,53 @@
<button class="btn-apple btn-primary" :disabled="rechargeSubmitting" @tap="confirmRecharge">
{{ rechargeSubmitting ? '处理中...' : '确认充值' }}
</button>
</view>
</view>
</up-popup>
<up-popup :show="showRechargeOrderPaymentModal" mode="center" @close="closeRechargeOrderPaymentModal">
<view class="recharge-popup recharge-order-payment-popup">
<view class="popup-header">
<view class="popup-title">选择支付方式</view>
<view class="popup-close" @tap="closeRechargeOrderPaymentModal">×</view>
</view>
<view class="package-summary">
<view class="summary-label">充值订单</view>
<view class="summary-name">{{ selectedRechargeOrder?.recharge_order_no || selectedRechargeOrder?.recharge_no || '充值订单' }}</view>
<view class="summary-price">¥{{ formatMoney(selectedRechargeOrder?.amount) }}</view>
<view class="summary-details">
<view class="detail-item">
<text class="detail-label">充值金额</text>
<text class="detail-value">¥{{ formatMoney(selectedRechargeOrder?.amount) }}</text>
</view>
</view>
</view>
<view class="payment-methods">
<view
v-for="method in paymentMethodOptions"
:key="method.value"
class="method-item"
:class="{ active: rechargePaymentMethod === method.value }"
@tap="selectRechargePaymentMethod(method.value)"
>
<view class="method-left">
<image v-if="method.value === 'alipay'" class="method-icon" src="/static/支付宝支付.png" mode="aspectFit"></image>
<image v-else-if="method.value === 'wechat'" class="method-icon" src="/static/微信支付.png" mode="aspectFit"></image>
<image v-else class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
<text class="method-name">{{ method.label }}</text>
</view>
<view class="method-radio" :class="{ checked: rechargePaymentMethod === method.value }"></view>
</view>
<view v-if="paymentMethodOptions.length === 0" class="method-empty">暂无可用支付方式</view>
</view>
<view class="popup-footer">
<button class="btn-apple btn-secondary" @tap="closeRechargeOrderPaymentModal">取消</button>
<button class="btn-apple btn-primary" :disabled="rechargeOrderSubmittingKey !== null" @tap="confirmRechargeOrderPayment">
{{ rechargeOrderSubmittingKey !== null ? '处理中...' : '确认支付' }}
</button>
</view>
</view>
</up-popup>
@@ -221,6 +264,8 @@
const transactionPageSize = 5;
const showRechargeModal = ref(false);
const showRechargeOrderPaymentModal = ref(false);
const selectedRechargeOrder = ref(null);
const selectedAmount = ref(null);
const isCustomAmount = ref(false);
const customAmount = ref('');
@@ -251,15 +296,6 @@
return parseFloat(amount).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const formatPaymentMethod = (method) => {
const labelMap = {
wechat: '微信支付',
alipay: '支付宝支付',
wallet: '钱包支付'
};
return labelMap[method] || method || '-';
};
const getRechargeStatusClass = (status) => {
const classMap = {
0: 'tag-warning',
@@ -377,6 +413,12 @@
rechargePaymentMethod.value = method;
};
const closeRechargeOrderPaymentModal = () => {
if (rechargeOrderSubmittingKey.value !== null) return;
showRechargeOrderPaymentModal.value = false;
selectedRechargeOrder.value = null;
};
const loadWalletDetail = async () => {
try {
const data = await walletApi.getDetail(userStore.state.identifier);
@@ -642,18 +684,27 @@
applyRechargeCheck(await walletApi.rechargeCheck(userStore.state.identifier));
} catch (error) {
console.error('加载充值支付方式失败', error);
uni.showToast({ title: error.msg || error.message || '加载支付方式失败', icon: 'none' });
return;
}
const options = paymentMethodOptions.value;
if (!paymentMethodOptions.value.length) {
uni.showToast({ title: '暂无可用支付方式', icon: 'none' });
return;
}
rechargePaymentMethod.value = getDefaultPaymentMethod(rechargeAllowedPaymentMethods.value);
selectedRechargeOrder.value = rechargeOrder;
showRechargeOrderPaymentModal.value = true;
};
uni.showActionSheet({
itemList: options.map((item) => item.label),
success: ({ tapIndex }) => {
const selectedMethod = options[tapIndex]?.value;
if (selectedMethod) {
handleRechargePayment(rechargeOrder, selectedMethod);
}
}
});
const confirmRechargeOrderPayment = async () => {
if (!selectedRechargeOrder.value || rechargeOrderSubmittingKey.value !== null) return;
const rechargeOrder = selectedRechargeOrder.value;
const paymentMethod = rechargePaymentMethod.value;
showRechargeOrderPaymentModal.value = false;
await handleRechargePayment(rechargeOrder, paymentMethod);
if (rechargeOrderSubmittingKey.value === null) {
selectedRechargeOrder.value = null;
}
};
const handleRechargePayment = async (rechargeOrder, paymentMethod) => {
@@ -1149,4 +1200,64 @@
}
}
}
.recharge-order-payment-popup {
.package-summary {
padding: 40rpx 30rpx 30rpx;
background: linear-gradient(135deg, rgba(85, 171, 92, 0.05) 0%, rgba(85, 171, 92, 0.02) 100%);
border-bottom: 1rpx solid var(--border-light);
text-align: center;
.summary-label {
font-size: 22rpx;
color: var(--text-tertiary);
}
.summary-name {
margin: 20rpx 0 0;
font-size: 32rpx;
font-weight: 700;
line-height: 1.4;
color: var(--text-primary);
}
.summary-price {
margin: 20rpx 0 24rpx;
font-size: 56rpx;
font-weight: 700;
letter-spacing: -1rpx;
color: var(--primary);
}
.summary-details {
display: flex;
align-items: center;
justify-content: center;
padding-top: 20rpx;
border-top: 1rpx solid rgba(85, 171, 92, 0.1);
.detail-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.detail-label {
font-size: 22rpx;
color: var(--text-tertiary);
}
.detail-value {
font-size: 26rpx;
font-weight: 600;
color: var(--text-primary);
}
}
}
}
.payment-methods {
padding: 30rpx;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<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"
@@ -9,11 +10,18 @@
@tap="onFilterChange(index)"
>
{{ item.label }}
<view v-if="filterIndex === index" class="tab-line"></view>
</view>
</view>
<scroll-view scroll-y class="scroll-container" :style="scrollViewStyle" @scrolltolower="loadMore">
<swiper
class="order-swiper"
:current="filterIndex"
:circular="false"
:acceleration="true"
@change="onFilterSwiperChange"
>
<swiper-item v-for="(item, index) in filterOptions" :key="item.value ?? 'all'" class="order-swiper-item">
<scroll-view v-if="filterIndex === index" scroll-y class="scroll-container" :style="scrollViewStyle" @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>
@@ -66,7 +74,9 @@
<text v-else>上拉加载更多</text>
</view>
</view>
</scroll-view>
</scroll-view>
</swiper-item>
</swiper>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
@@ -106,6 +116,9 @@
const scrollViewStyle = computed(() => scrollViewHeight.value > 0
? { height: `${scrollViewHeight.value}px`, flex: 'none' }
: {});
const filterIndicatorStyle = computed(() => ({
left: `calc(${filterIndex.value * 20}% + 8rpx)`
}));
const updateScrollViewHeight = () => {
const windowInfo = typeof uni.getWindowInfo === 'function'
@@ -154,10 +167,18 @@
};
const onFilterChange = (index) => {
if (filterIndex.value === index) return;
filterIndex.value = index;
resetOrderListAndLoad();
};
const onFilterSwiperChange = (event) => {
const nextIndex = Number(event?.detail?.current);
if (Number.isInteger(nextIndex) && nextIndex >= 0 && nextIndex < filterOptions.length) {
onFilterChange(nextIndex);
}
};
const loadOrderList = async (append = false) => {
if (loading.value || (append && noMore.value)) return;
@@ -238,6 +259,10 @@
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);
};
@@ -532,6 +557,59 @@
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-swiper,
.order-swiper-item {
width: 100%;
min-height: 0;
}
.order-swiper {
flex: 1;
height: 0;
}
.order-swiper-item {
height: 100%;
}
.scroll-container {
flex: 1 1 0;
height: auto;

View File

@@ -1,6 +1,7 @@
<template>
<view class="container">
<view class="package-tabs">
<view class="package-tab-indicator" :style="{ left: packageTypeIndex === 0 ? '8rpx' : '50%' }"></view>
<view
v-for="tab in packageTypeTabs"
:key="tab.value"
@@ -12,34 +13,46 @@
</view>
</view>
<view v-if="packageList.length === 0 && !loading" class="empty-state">
<image class="empty-icon" src="/static/shop.png" mode="aspectFit" alt="套餐订购"></image>
<view class="empty-title">暂无{{ activePackageType === 'formal' ? '正式套餐' : '加油包' }}</view>
<view class="empty-desc">当前资产暂无适用的套餐</view>
</view>
<swiper
class="package-swiper"
:current="packageTypeIndex"
:circular="false"
:acceleration="true"
@change="onPackageSwiperChange"
>
<swiper-item v-for="tab in packageTypeTabs" :key="tab.value" class="package-swiper-item">
<scroll-view v-if="activePackageType === tab.value" scroll-y class="package-scroll">
<view v-if="packageList.length === 0 && !loading" class="empty-state">
<image class="empty-icon" src="/static/shop.png" mode="aspectFit" alt="套餐订购"></image>
<view class="empty-title">暂无{{ activePackageType === 'formal' ? '正式套餐' : '加油包' }}</view>
<view class="empty-desc">当前资产暂无适用的套餐</view>
</view>
<view class="package-list">
<view class="package-card" v-for="item in packageList" :key="item.package_id">
<view class="package-info">
<view class="package-name">{{ item.package_name }}</view>
<view class="package-meta">
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/有效期.png" mode="aspectFit" />
<text>{{ formatValidity(item.validity_days) }}</text>
</view>
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/流量.png" mode="aspectFit" />
<text>{{ formatData(item.data_allowance, item.data_unit) }}</text>
<view v-else class="package-list">
<view class="package-card" v-for="item in packageList" :key="item.package_id">
<view class="package-info">
<view class="package-name">{{ item.package_name }}</view>
<view class="package-meta">
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/有效期.png" mode="aspectFit" />
<text>{{ formatValidity(item.validity_days) }}</text>
</view>
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/流量.png" mode="aspectFit" />
<text>{{ formatData(item.data_allowance, item.data_unit) }}</text>
</view>
</view>
<view class="package-description">高速流量 · 全国通用 · 不限速</view>
</view>
<view class="package-side">
<view class="package-price"><text class="price-symbol">¥</text>{{ formatPackagePrice(item.retail_price) }}</view>
<button class="btn-apple btn-primary package-buy-button" @tap="buyPackage(item)">立即订购</button>
</view>
</view>
</view>
<view class="package-description">高速流量 · 全国通用 · 不限速</view>
</view>
<view class="package-side">
<view class="package-price"><text class="price-symbol">¥</text>{{ formatPackagePrice(item.retail_price) }}</view>
<button class="btn-apple btn-primary package-buy-button" @tap="buyPackage(item)">立即订购</button>
</view>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
<up-popup :show="showModal" mode="center" @close="showModal = false">
<view class="payment-popup">
@@ -121,6 +134,7 @@
const renewalPackageIds = ref([]);
const renewalPackageNames = ref([]);
const activePackageType = ref('formal');
const packageTypeIndex = ref(0);
const packageTypeTabs = [
{ label: '正式套餐', value: 'formal' },
{ label: '加油包', value: 'addon' }
@@ -217,11 +231,20 @@
};
const switchPackageType = (packageType) => {
const nextIndex = packageTypeTabs.findIndex((item) => item.value === packageType);
if (nextIndex < 0) return;
packageTypeIndex.value = nextIndex;
if (packageType === activePackageType.value) return;
activePackageType.value = packageType;
loadPackages(packageType);
};
const onPackageSwiperChange = (event) => {
const nextIndex = Number(event?.detail?.current);
const nextTab = packageTypeTabs[nextIndex];
if (nextTab) switchPackageType(nextTab.value);
};
const loadWalletBalance = async () => {
try {
const data = await walletApi.getDetail(userStore.state.identifier);
@@ -683,6 +706,8 @@
/* Screenshot-aligned package list skin */
.container {
display: flex;
flex-direction: column;
gap: 18rpx;
padding: 20rpx 24rpx 48rpx;
min-height: 100vh;
@@ -712,12 +737,55 @@
}
.container .package-tab.active {
background: #fff;
box-shadow: 0 4rpx 14rpx rgba(31, 35, 41, 0.06);
color: var(--primary);
background: transparent;
box-shadow: none;
color: #fff;
font-weight: 600;
}
.container .package-tabs {
position: relative;
padding: 8rpx;
background: #fff;
border-radius: 16rpx;
}
.container .package-tab {
position: relative;
z-index: 1;
padding: 20rpx 0;
color: var(--text-tertiary);
transition: color 0.3s;
}
.package-tab-indicator {
position: absolute;
top: 8rpx;
width: calc(50% - 16rpx);
height: calc(100% - 16rpx);
background: var(--primary);
border-radius: 12rpx;
transition: left 0.3s ease;
z-index: 0;
}
.package-swiper,
.package-swiper-item,
.package-scroll {
width: 100%;
min-height: 0;
}
.package-swiper {
flex: 1;
height: 0;
}
.package-swiper-item,
.package-scroll {
height: 100%;
}
.package-list {
display: flex;
flex-direction: column;