feat(recharge): add points recharge agreement flow

- add new API request for fetching points top up agreement content
- update recharge footer with agreement checkbox and protocol link
- add agreement popup modal to display full agreement text
- add payment pre-validation to enforce agreement acceptance before payment
- handle loading and error states for agreement retrieval
- adjust recharge footer layout to accommodate new UI elements
This commit is contained in:
duanshuwen
2026-07-16 20:34:01 +08:00
parent 67b1b78f1f
commit 99151bca58
4 changed files with 177 additions and 15 deletions

View File

@@ -1,21 +1,31 @@
<template>
<view class="recharge-footer">
<text class="recharge-gain">
支付金额 <text class="recharge-gain-value">¥{{ formattedPrice }}</text>
</text>
<view class="recharge-agreement" :class="{ 'is-disabled': !agreementReady }">
<CheckBox :model-value="agreed" @update:model-value="handleAgreementChange">
<text class="recharge-agreement-text">我已阅读并同意</text>
<text class="recharge-agreement-link" @tap.stop="handleViewAgreement">积分充值协议</text>
</CheckBox>
</view>
<view
class="recharge-pay-button"
:class="{ 'is-disabled': loading || price <= 0 }"
@tap="emit('pay')"
>
{{ loading ? "支付中..." : "立即支付" }}
<view class="recharge-payment-row">
<text class="recharge-gain">
支付金额 <text class="recharge-gain-value">¥{{ formattedPrice }}</text>
</text>
<view
class="recharge-pay-button"
:class="{ 'is-disabled': payDisabled }"
@tap="handlePay"
>
{{ loading ? "支付中..." : "立即支付" }}
</view>
</view>
</view>
</template>
<script setup>
import { computed } from "vue";
import CheckBox from "@/components/CheckBox/index.vue";
const props = defineProps({
price: {
@@ -26,11 +36,40 @@ const props = defineProps({
type: Boolean,
default: false,
},
agreed: {
type: Boolean,
default: false,
},
agreementReady: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["pay"]);
const emit = defineEmits(["pay", "update:agreed", "view-agreement"]);
const formattedPrice = computed(() => Number(props.price).toLocaleString("zh-CN"));
const payDisabled = computed(() => {
return props.loading || props.price <= 0 || !props.agreed || !props.agreementReady;
});
const handleAgreementChange = (value) => {
if (!props.agreementReady) {
emit("view-agreement");
return;
}
emit("update:agreed", value);
};
const handleViewAgreement = () => {
emit("view-agreement");
};
const handlePay = () => {
if (props.loading || props.price <= 0) return;
emit("pay");
};
</script>
<style scoped lang="scss">

View File

@@ -1,10 +1,9 @@
.recharge-footer {
position: sticky;
bottom: 12px;
display: grid;
grid-template-columns: 1fr 142px;
align-items: center;
gap: 10px;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
padding: 12px;
border-radius: 20px;
@@ -13,6 +12,38 @@
box-sizing: border-box;
}
.recharge-agreement {
display: flex;
align-items: center;
box-sizing: border-box;
}
.recharge-agreement.is-disabled {
opacity: 0.56;
}
.recharge-agreement-text,
.recharge-agreement-link {
font-size: 14px;
line-height: 24px;
}
.recharge-agreement-text {
color: #667386;
}
.recharge-agreement-link {
color: $theme-color-600;
font-weight: 700;
}
.recharge-payment-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 142px;
align-items: center;
gap: 10px;
}
.recharge-gain {
min-width: 0;
overflow: hidden;

View File

@@ -32,9 +32,20 @@
<RechargeFooter
:price="payPrice"
:loading="paying"
:agreed="agreementAccepted"
:agreement-ready="agreementReady"
@update:agreed="agreementAccepted = $event"
@view-agreement="handleViewAgreement"
@pay="handlePay"
/>
</view>
<AgreePopup
:visible="agreementVisible"
title="积分充值协议"
:agreement="agreementContent"
@close="agreementVisible = false"
/>
</view>
</template>
@@ -43,7 +54,12 @@ import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import { getRechargeOptions, rechargeCredit } from "@/request/api/AigcApi.js";
import {
getPointsTopUpAgreement,
getRechargeOptions,
rechargeCredit,
} from "@/request/api/AigcApi.js";
import AgreePopup from "@/pages/login/components/AgreePopup/index.vue";
import BalanceCard from "./components/BalanceCard/index.vue";
import CustomAmountCard from "./components/CustomAmountCard/index.vue";
import RechargeFooter from "./components/RechargeFooter/index.vue";
@@ -55,6 +71,12 @@ const selectedRechargeOption = ref(null);
const customAmount = ref("");
const rechargeMode = ref("package");
const paying = ref(false);
const agreementAccepted = ref(false);
const agreementContent = ref("");
const agreementLoading = ref(false);
const agreementVisible = ref(false);
const agreementReady = computed(() => Boolean(agreementContent.value.trim()));
const getRechargeOptionKey = (option) => {
if (!option) return "";
@@ -97,8 +119,41 @@ const fetchRechargeOptions = async () => {
}
};
const fetchPointsTopUpAgreement = async (showError = false) => {
if (agreementLoading.value) return false;
agreementLoading.value = true;
try {
const res = await getPointsTopUpAgreement();
const content = typeof res?.data === "string" ? res.data.trim() : "";
if (res?.code === 0 && content) {
agreementContent.value = content;
return true;
}
agreementContent.value = "";
agreementAccepted.value = false;
if (showError) {
showToast(res?.msg || "协议加载失败,请重试");
}
} catch (error) {
console.error("获取积分充值协议失败", error);
agreementContent.value = "";
agreementAccepted.value = false;
if (showError) {
showToast("协议加载失败,请重试");
}
} finally {
agreementLoading.value = false;
}
return false;
};
onLoad(() => {
agreementAccepted.value = false;
fetchRechargeOptions();
fetchPointsTopUpAgreement();
});
onShow(() => {
@@ -129,6 +184,23 @@ const handleSelectCustomAmount = () => {
selectedRechargeOption.value = null;
};
const handleViewAgreement = async () => {
if (agreementReady.value) {
agreementVisible.value = true;
return;
}
if (agreementLoading.value) {
showToast("协议加载中,请稍候");
return;
}
const loaded = await fetchPointsTopUpAgreement(true);
if (loaded) {
agreementVisible.value = true;
}
};
const requestWechatPay = (payData) => {
const { nonceStr, packageVal, paySign, signType, timeStamp } = payData || {};
@@ -174,6 +246,21 @@ const handlePay = async () => {
return;
}
if (agreementLoading.value) {
showToast("协议加载中,请稍候");
return;
}
if (!agreementReady.value) {
showToast("协议加载失败,请重试");
return;
}
if (!agreementAccepted.value) {
showToast("请先阅读并同意积分充值协议");
return;
}
paying.value = true;
uni.showLoading({ title: "正在发起支付..." });

View File

@@ -36,6 +36,10 @@ function getCreditLedgerPage(args) {
return request.post("/hotelBiz/credit/ledger/page", args);
}
function getPointsTopUpAgreement() {
return request.get("/hotelBiz/mainScene/pointsTopUpAgreement", {});
}
function createAigcGeneratorTaskShare(args) {
return request.post("/hotelBiz/aigcGeneratorTask/share/create", args);
}
@@ -54,6 +58,7 @@ export {
getRechargeOptions,
rechargeCredit,
getCreditLedgerPage,
getPointsTopUpAgreement,
createAigcGeneratorTaskShare,
getAigcGeneratorTaskShareDetail,
};