fix: 立即续费不跳转
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 59s

This commit is contained in:
luo
2026-07-28 16:02:40 +08:00
parent 64bccffe28
commit 34ca1c5eca
8 changed files with 431 additions and 12 deletions

View File

@@ -20,6 +20,11 @@
</swiper>
<view v-if="items.length > 1" class="popup-footer">
<view class="popup-swipe-hint" aria-label="左右滑动切换通知">
<text class="swipe-arrow"></text>
<text>左右滑动切换通知</text>
<text class="swipe-arrow"></text>
</view>
<view class="popup-dots">
<view v-for="(_, index) in items" :key="index" class="popup-dot" :class="{ active: index === current }"></view>
</view>
@@ -150,6 +155,22 @@
text-align: center;
}
.popup-swipe-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
margin-bottom: 16rpx;
color: var(--primary);
font-size: 24rpx;
font-weight: 500;
}
.swipe-arrow {
font-size: 34rpx;
line-height: 1;
}
.popup-dots {
display: flex;
justify-content: center;
@@ -166,7 +187,7 @@
.popup-dot.active { width: 28rpx; border-radius: 8rpx; background: var(--primary); }
.popup-counter {
margin-top: 16rpx;
margin-top: 12rpx;
color: var(--text-tertiary);
font-size: 22rpx;
}

View File

@@ -0,0 +1,374 @@
<template>
<up-popup :show="show" mode="center" @close="close">
<view class="renewal-popup">
<view class="popup-header">
<view class="popup-title">立即续费</view>
<view class="popup-close" @tap="close">×</view>
</view>
<view class="package-summary">
<view class="summary-label">续费套餐</view>
<view class="summary-name">{{ packageNamesText }}</view>
</view>
<view class="payment-methods">
<view
v-for="method in paymentMethodOptions"
:key="method.value"
class="method-item"
:class="{ active: paymentMethod === method.value }"
@tap="selectPaymentMethod(method.value)"
>
<view class="method-left">
<view v-if="method.value === 'alipay'" class="method-icon method-badge method-badge-alipay"></view>
<image v-else-if="method.value === 'wallet'" class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
<view v-else class="method-icon method-badge"></view>
<text class="method-name">{{ method.label }}</text>
</view>
<view class="method-radio" :class="{ checked: paymentMethod === 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="close">取消</button>
<button class="btn-apple btn-primary" :disabled="submitting" @tap="confirmPay">
{{ submitting ? '处理中...' : '确认续费' }}
</button>
</view>
</view>
</up-popup>
</template>
<script setup>
import { ref, computed } from 'vue';
import { assetApi, orderApi } from '@/api/index.js';
import {
handlePaymentError,
isValidAlipayPaymentLink,
isValidWechatPayConfig,
openAlipayPayment,
PAYMENT_REFRESH_TARGETS,
showPaymentToast,
wechatH5Pay
} from '@/utils/payment.js';
import { formatMoney } from '@/utils/display.js';
import { getDefaultPaymentMethod, getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
const props = defineProps({
identifier: { type: String, default: '' },
paymentRefreshTarget: { type: String, default: PAYMENT_REFRESH_TARGETS.ORDER_LIST }
});
const emit = defineEmits(['completed']);
const show = ref(false);
const submitting = ref(false);
const packageIds = ref([]);
const packageNames = ref([]);
const paymentMethod = ref('alipay');
const allowedPaymentMethods = ref([]);
const assetInfo = ref({});
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
const packageNamesText = computed(() => packageNames.value.filter(Boolean).join('、') || '当前套餐');
const hasPreparedPaymentData = (paymentData) => {
return isValidWechatPayConfig(paymentData?.pay_config) ||
isValidAlipayPaymentLink(paymentData?.payment_link);
};
const close = () => {
if (!submitting.value) show.value = false;
};
const loadAssetInfo = async () => {
assetInfo.value = await assetApi.getInfo(props.identifier);
allowedPaymentMethods.value = normalizePaymentMethods(assetInfo.value.allowed_payment_methods);
paymentMethod.value = getDefaultPaymentMethod(allowedPaymentMethods.value);
};
const open = async (ids, names = []) => {
const validIds = [...new Set((Array.isArray(ids) ? ids : [ids])
.map((value) => Number(value))
.filter((value) => Number.isInteger(value) && value > 0))];
if (!validIds.length) {
uni.showToast({ title: '当前暂无可续费套餐', icon: 'none' });
return;
}
if (!props.identifier) {
uni.showToast({ title: '未找到当前资产', icon: 'none' });
return;
}
packageIds.value = validIds;
packageNames.value = Array.isArray(names) ? names.filter(Boolean) : [];
try {
await loadAssetInfo();
} catch (error) {
console.error('加载续费支付方式失败', error);
uni.showToast({ title: error.msg || error.message || '加载支付方式失败', icon: 'none' });
return;
}
if (assetInfo.value.effective_realname_policy === 'before_order' &&
assetInfo.value.realname_required && Number(assetInfo.value.real_name_status) !== 1) {
uni.showModal({
title: '需要实名认证',
content: '当前资产下单前需要完成实名认证',
confirmText: '去认证',
cancelText: '取消',
success: ({ confirm }) => {
if (confirm) uni.navigateTo({ url: '/pages/auth/auth' });
}
});
return;
}
if (!paymentMethodOptions.value.length) {
uni.showToast({ title: '暂无可用支付方式', icon: 'none' });
return;
}
show.value = true;
};
const selectPaymentMethod = (method) => {
if (paymentMethodOptions.value.some((item) => item.value === method)) {
paymentMethod.value = method;
}
};
const confirmOrderStatus = async (orderId) => {
try {
const detail = await orderApi.getDetail(orderId);
const status = detail?.payment_status ?? detail?.order?.payment_status;
showPaymentToast(status === 2, status === 2 ? '支付成功' : '支付结果确认中');
if (status === 2) emit('completed');
} catch (error) {
console.error('确认续费订单状态失败', error);
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
}
};
const handlePreparedPayment = async (paymentData, orderId = null, isRecharge = false) => {
if (isValidWechatPayConfig(paymentData?.pay_config)) {
try {
await wechatH5Pay(paymentData.pay_config);
if (orderId) {
await confirmOrderStatus(orderId);
} else {
showPaymentToast(true, isRecharge ? '充值成功,套餐将自动购买' : '支付成功');
emit('completed');
}
} catch (error) {
handlePaymentError(error);
}
return;
}
if (isValidAlipayPaymentLink(paymentData?.payment_link)) {
await openAlipayPayment(paymentData.payment_link, props.paymentRefreshTarget);
return;
}
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
};
const handleRechargeOrder = async (orderResult) => {
const recharge = orderResult.recharge;
if (recharge.status === 2 || recharge.status === 3) {
uni.showToast({ title: `充值订单${recharge.status_name || '不可支付'},请重新下单`, icon: 'none' });
return;
}
if (recharge.status === 1 && !hasPreparedPaymentData(orderResult)) {
uni.showModal({
title: '提示',
content: '检测到您有待支付的充值订单,是否前往我的钱包查看?',
confirmText: '去查看',
cancelText: '取消',
success: ({ confirm }) => {
if (confirm) uni.navigateTo({ url: '/pages/my-wallet/my-wallet' });
}
});
return;
}
if (!hasPreparedPaymentData(orderResult)) {
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
const packageName = packageNamesText.value;
const rechargeAmount = orderResult.linked_package_info?.force_recharge_amount || recharge.amount;
uni.showModal({
title: '需要充值',
content: `续费${packageName}需要先充值¥${formatMoney(rechargeAmount)},充值成功后将自动购买套餐`,
confirmText: '去充值',
cancelText: '取消',
success: async ({ confirm }) => {
if (confirm) await handlePreparedPayment(orderResult, null, true);
}
});
};
const confirmPay = async () => {
if (submitting.value || !packageIds.value.length) return;
submitting.value = true;
uni.showLoading({ title: '创建订单...', mask: true });
try {
const orderResult = await orderApi.create(props.identifier, packageIds.value, paymentMethod.value);
if (orderResult?.order_type === 'recharge' && orderResult.recharge) {
uni.hideLoading();
show.value = false;
await handleRechargeOrder(orderResult);
return;
}
const orderId = orderResult?.order?.order_id;
if (!orderId) {
uni.hideLoading();
show.value = false;
uni.showToast({ title: '订单创建失败', icon: 'none' });
return;
}
uni.showLoading({ title: '准备支付...', mask: true });
const payResult = await orderApi.pay(orderId, paymentMethod.value);
uni.hideLoading();
show.value = false;
if (paymentMethod.value === 'wallet') {
await confirmOrderStatus(orderId);
return;
}
await handlePreparedPayment(payResult, orderId);
} catch (error) {
uni.hideLoading();
show.value = false;
console.error('创建续费订单或支付失败', error);
uni.showToast({ title: error.msg || error.message || '操作失败,请稍后重试', icon: 'none' });
} finally {
submitting.value = false;
}
};
defineExpose({ open });
</script>
<style scoped lang="scss">
.renewal-popup {
width: 620rpx;
max-width: calc(100vw - 80rpx);
padding: 32rpx;
box-sizing: border-box;
background: #fff;
border-radius: 24rpx;
}
.popup-header,
.popup-footer,
.method-left {
display: flex;
align-items: center;
}
.popup-header {
justify-content: space-between;
}
.popup-title {
color: var(--text-primary);
font-size: 34rpx;
font-weight: 600;
}
.popup-close {
width: 52rpx;
height: 52rpx;
color: var(--text-tertiary);
font-size: 44rpx;
line-height: 46rpx;
text-align: center;
}
.package-summary {
margin-top: 28rpx;
padding: 22rpx;
background: var(--bg-secondary);
border-radius: 14rpx;
}
.summary-label {
color: var(--text-tertiary);
font-size: 24rpx;
}
.summary-name {
margin-top: 8rpx;
color: var(--text-primary);
font-size: 30rpx;
font-weight: 600;
line-height: 1.5;
}
.payment-methods {
margin-top: 24rpx;
}
.method-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 0;
border-bottom: 1rpx solid var(--border-light);
&.active .method-name { color: var(--primary); }
}
.method-left { gap: 16rpx; }
.method-icon {
width: 44rpx;
height: 44rpx;
border-radius: 10rpx;
}
.method-badge {
display: flex;
align-items: center;
justify-content: center;
background: #07c160;
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
.method-badge-alipay { background: #1677ff; }
.method-name { color: var(--text-primary); font-size: 28rpx; }
.method-radio {
width: 32rpx;
height: 32rpx;
border: 2rpx solid var(--border-light);
border-radius: 50%;
&.checked {
border: 8rpx solid var(--primary);
}
}
.method-empty {
padding: 28rpx 0;
color: var(--text-tertiary);
font-size: 26rpx;
text-align: center;
}
.popup-footer {
gap: 20rpx;
margin-top: 28rpx;
button {
flex: 1;
margin: 0;
}
}
</style>

View File

@@ -53,3 +53,14 @@ When the user views or activates a notification, the client SHALL call `PUT /api
- **WHEN** marking a notification read fails
- **THEN** the client SHALL retain the unread state or refresh it from the backend
- **AND** the failure SHALL not prevent the user from viewing other asset or notification content
### Requirement: Multi-notification popup SHALL communicate horizontal navigation
When the homepage notification popup contains more than one notification, the client SHALL make the horizontal swipe interaction visible to the user and SHALL retain the current-position indicator.
#### Scenario: User opens multiple unread notifications
- **WHEN** the homepage popup contains two or more notifications
- **THEN** the popup SHALL display an explicit `左右滑动切换通知` hint
- **AND** the user SHALL be able to move between notifications by swiping horizontally
- **AND** the popup SHALL display the current notification position

View File

@@ -13,12 +13,12 @@ The H5/C client SHALL keep discontinued packages out of the ordinary package cat
- **WHEN** asset info returns a discontinued package through `current_package_id`
- **THEN** the client SHALL expose that ID only in the eligible renewal context
- **AND** the client SHALL allow the customer to continue through the existing package-order flow
- **AND** the client SHALL allow the customer to continue through the existing standard order/payment flow without navigating to the package catalog
#### Scenario: Existing customer renews from a historical order
- **WHEN** a historical order returns one or more valid `package_ids`
- **THEN** the client SHALL use those IDs for the renewal selection when the existing renewal flow provides that entry
- **THEN** the client SHALL use those IDs for the renewal selection when the order-list renewal entry is used
- **AND** the client SHALL preserve the historical order data unchanged
### Requirement: Renewal SHALL reuse the standard order creation contract
@@ -45,13 +45,13 @@ The client SHALL show an `立即续费` button in the homepage asset summary and
- **WHEN** the homepage asset summary is displayed
- **THEN** the client SHALL show an `立即续费` button
- **AND** tapping it SHALL start the standard renewal flow for the asset's current package ID when one is available
- **AND** tapping it SHALL directly start the standard order/payment flow for the asset's current package ID when one is available
#### Scenario: Customer renews from an order card
- **WHEN** an order is displayed in the order list
- **THEN** the client SHALL show an `立即续费` button for that order
- **AND** tapping it SHALL use the order's package IDs for the standard renewal flow
- **AND** tapping it SHALL directly start the standard order/payment flow for that order's package IDs without navigating to the package-order page
#### Scenario: Customer views another page

View File

@@ -19,7 +19,7 @@
- [x] 3.2 Keep discontinued packages out of the ordinary package catalog for new customers and agents
- [x] 3.3 Add the eligible existing-customer renewal path using `current_package_id` or historical `package_ids`
- [x] 3.4 Reuse `POST /api/c/v1/orders/create` for renewal and preserve selected identifier, package IDs, payment method, and existing payment result handling
- [x] 3.5 Show `立即续费` only on the homepage asset summary and every order-list card
- [x] 3.5 Show `立即续费` only on the homepage asset summary and every order-list card, with direct order/payment handling
## 4. Wallet recharge and payment completion

View File

@@ -99,6 +99,9 @@
<NotificationPopup :show="notificationPopupShow" :items="notificationItems"
:current="notificationPopupCurrent" @change="onNotificationChange" @close="closeNotificationPopup" />
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="home" @completed="loadAssetInfo" />
<!-- 日期选择器弹窗 -->
<up-datetime-picker v-if="!userInfo.isDevice" :show="showStartDatePicker" v-model="startDateTimestamp"
mode="date" @confirm="onStartDateConfirm" @cancel="showStartDatePicker=false"
@@ -128,6 +131,7 @@
import FunctionCard from '@/components/FunctionCard.vue';
import FloatingButton from '@/components/FloatingButton.vue';
import NotificationPopup from '@/components/NotificationPopup.vue';
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
import {
assetApi,
deviceApi,
@@ -136,6 +140,7 @@
import {
useUserStore
} from '@/store/index.js';
import { consumePendingPaymentRefresh, PAYMENT_REFRESH_TARGETS } from '@/utils/payment.js';
const userStore = useUserStore();
@@ -225,6 +230,7 @@
let notificationPopupShow = ref(false);
let notificationItems = ref([]);
let notificationPopupCurrent = ref(0);
let renewalPopupRef = ref(null);
const notificationReadPending = new Set();
const formatDate = (dateStr) => {
@@ -360,8 +366,7 @@
}
const packageName = deviceInfo.packageName === '-' ? '' : deviceInfo.packageName;
const query = `renewal_package_ids=${encodeURIComponent(String(packageId))}&renewal_package_names=${encodeURIComponent(packageName)}`;
uni.navigateTo({ url: `/pages/package-order/package-order?${query}` });
renewalPopupRef.value?.open([packageId], [packageName]);
};
const modifyWifi = () => {
@@ -861,6 +866,9 @@
});
onShow(() => {
if (consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME)) {
loadAssetInfo();
}
handleIndexEntry();
loadNotificationUnreadCount();
loadUnreadNotifications();

View File

@@ -68,7 +68,7 @@
{{ isOrderPaying(item) ? '处理中...' : '立即支付' }}
</button>
<button class="btn-pay btn-renew" :disabled="orderPayingId !== null"
@tap.stop="startHistoricalRenewal(item)">立即续费</button>
@tap.stop="startOrderRenewal(item)">立即续费</button>
</view>
</view>
</view>
@@ -80,6 +80,9 @@
</view>
</view>
</scroll-view>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
</view>
</template>
@@ -87,6 +90,7 @@
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,
@@ -108,6 +112,7 @@
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([]);
@@ -234,15 +239,14 @@
uni.navigateTo({ url: `/pages/order-detail/order-detail?id=${order.order_id}` });
};
const startHistoricalRenewal = (order) => {
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 : [];
const query = `renewal_package_ids=${encodeURIComponent(packageIds.join(','))}&renewal_package_names=${encodeURIComponent(packageNames.join('|'))}`;
uni.navigateTo({ url: `/pages/package-order/package-order?${query}` });
renewalPopupRef.value?.open(packageIds, packageNames);
};
const showOrderPaymentMethods = async (order) => {

View File

@@ -2,6 +2,7 @@ const ALIPAY_PENDING_REFRESH_KEY = 'pending_alipay_payment_refresh';
const ALIPAY_PAYMENT_DATA_KEY = 'pending_alipay_payment_data';
export const PAYMENT_REFRESH_TARGETS = {
HOME: 'home',
PACKAGE_ORDER: 'package-order',
ORDER_LIST: 'order-list',
MY_WALLET: 'my-wallet'