| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700 |
- <template>
- <view class="subscription-page">
- <view v-if="servicePackages.length > 0" class="card-list">
- <view
- v-for="pkg in servicePackages"
- :key="pkg.grfwbid"
- class="service-card"
- :class="{ expanded: isExpanded(pkg) }"
- @click="toggleExpand(pkg)"
- >
- <view class="card-title">{{ pkg.mc }}</view>
- <view class="card-price-row">
- <view class="card-price">¥ {{ getPackageTotalPrice(pkg) }}</view>
- <button class="card-action" @click.stop="toggleExpand(pkg)">
- {{ isExpanded(pkg) ? '收起' : '订阅' }}
- </button>
- </view>
- <template v-if="!isExpanded(pkg)">
- <view class="card-preview">
- <rich-text :nodes="getPackageIntroHtml(pkg)"></rich-text>
- </view>
- </template>
- <template v-else>
- <view class="expanded-body" @click.stop>
- <view class="expanded-intro">
- <rich-text :nodes="getRichNodes(getPackageIntroHtml(pkg))"></rich-text>
- </view>
- <view class="detail-section-title">详情</view>
- <view v-for="(detail, idx) in getPackageDetailList(pkg)" :key="idx" class="detail-item">
- <view class="detail-item-title">{{ idx + 1 }}. {{ detail.title }}</view>
- <view class="detail-item-rt">
- <rich-text :nodes="getRichNodes(detail.html)"></rich-text>
- </view>
- </view>
- </view>
- <view class="expanded-footer" @click.stop>
- <view class="bottom-terms">
- <view class="checkbox" :class="{ checked: termsAccepted }" @click="toggleTermsAccepted">
- <text v-if="termsAccepted" class="checkmark">✓</text>
- </view>
-
- <text class="terms-text" @click="openTermsDrawer">《服务条款》</text>
- </view>
- <button
- class="bottom-btn"
- :disabled="isPaymentInProgress || !termsAccepted || !finalAmount || finalAmount <= 0"
- @click="handlePay"
- >
- 订阅
- </button>
- </view>
- </template>
- </view>
- </view>
- <view v-else class="empty-state">
- <text>暂无可用服务包</text>
- </view>
- <u-popup :show="termsDrawerVisible" mode="bottom" round="16" @close="closeTermsDrawer">
- <view class="terms-drawer">
- <view class="drawer-head">
- <view class="drawer-title">服务条款</view>
- </view>
- <scroll-view class="drawer-body" scroll-y>
- <view class="drawer-text">
- <view class="drawer-p">1. 订阅服务为虚拟服务,一经开通即刻生效。</view>
- <view class="drawer-p">2. 订阅权益以页面展示及系统实际开通为准。</view>
- <view class="drawer-p">3. 若出现网络/系统异常导致开通失败,请联系客服处理。</view>
- <view class="drawer-p">4. 更多条款内容以平台最终版本为准。</view>
- </view>
- </scroll-view>
- <view class="drawer-actions">
- <button class="drawer-agree" @click="agreeTerms">我已阅读并同意</button>
- </view>
- </view>
- </u-popup>
- </view>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { grfwApi } from '@/api/grfw'
- // 兼容不同字段命名的支付参数
- const normalizePayParams = (res) => {
- const data = res?.data.ssData || res || {}
- const payload = data.prepay || data.payInfo || data
- if (!payload) {
- return {}
- }
- const rawPackage =
- payload.package ||
- payload.packageVal ||
- (payload.prepayId ? `prepay_id=${payload.prepayId}` : undefined)
- const normalizedPackage =
- typeof rawPackage === 'string' && rawPackage && !rawPackage.startsWith('prepay_id=')
- ? `prepay_id=${rawPackage}`
- : rawPackage
- return {
- appId: payload.appId,
- timeStamp: payload.timeStamp || payload.timestamp,
- nonceStr: payload.nonceStr || payload.noncestr,
- package: normalizedPackage,
- signType: payload.signType || payload.sign_type || 'RSA',
- paySign: payload.paySign || payload.sign,
- orderId: data.outTradeNo
- }
- }
- // 个人服务包数据
- const servicePackages = ref([]) // 服务包列表
- const selectedPackage = ref(null) // 选中的服务包
- const isPaymentInProgress = ref(false) // 支付进行中标志
- const expandedPackageId = ref('')
- const termsAccepted = ref(false)
- const termsDrawerVisible = ref(false)
- const buildIntroHtml = (rawText) => {
- if (!rawText || typeof rawText !== 'string') return ''
- const lines = rawText
- .split(/\r?\n/)
- .map((line) => line.trim())
- .filter(Boolean)
- if (!lines.length) return ''
- return lines
- .map((line) => `<p>${line}</p>`)
- .join('')
- }
- const normalizeServicePackages = (ssData) => {
- if (!Array.isArray(ssData)) return []
- // ssData[0] 按需求跳过,从 ssData[1] 开始读取服务包
- return ssData
- .slice(1)
- .filter((pkg) => pkg && pkg.grfwbid)
- .map((pkg) => {
- const detailItems = Array.isArray(pkg.grfwbmxList)
- ? [...pkg.grfwbmxList].sort((a, b) => (a?.xh || 0) - (b?.xh || 0))
- : []
- return {
- ...pkg,
- jg: parseFloat(pkg.jg) || 0,
- num: Number(pkg.num) || 0,
- introHtml: buildIntroHtml(pkg.ms),
- detailList: detailItems.map((item) => ({
- title: item?.mc || '',
- html: `<p>${item?.ms || getServiceDesc(item)}</p>`
- }))
- }
- })
- }
- // 最终支付金额(使用服务包价格,周包需要乘以周数)
- const finalAmount = computed(() => {
- if (selectedPackage.value) {
- const pkg = selectedPackage.value
- // 如果是周包(num > 0),总价 = 单价 × 周数
- if (pkg.num > 0) {
- return parseFloat(pkg.jg * pkg.num) || 0
- }
- return parseFloat(pkg.jg) || 0
- }
- return 0
- })
- // 选择服务包
- const selectPackage = (pkg) => {
- selectedPackage.value = pkg
- console.log('选中服务包:', pkg)
- }
- const getPackageTotalPrice = (pkg) => {
- if (!pkg) return 0
- if (pkg.num > 0) {
- return parseFloat(pkg.jg * pkg.num) || 0
- }
- return parseFloat(pkg.jg) || 0
- }
- const formatServiceLine = (item) => {
- if (!item) return ''
- if (item.sc > 0) return `${item.mc}:${item.sc}分钟`
- if (item.cs > 0) return `${item.mc}:${item.cs}次`
- if (item.ll > 0) return `${item.mc}:${item.ll}MB`
- return `${item.mc}`
- }
- const getPackageIntroHtml = (pkg) => {
- if (!pkg) return ''
- if (pkg.introHtml) return pkg.introHtml
- const list = pkg?.grfwbmxList || []
- const lines = list.map(formatServiceLine).filter(Boolean)
- return lines.map((line) => `<p>• ${line}</p>`).join('')
- }
- const getPackageDetailList = (pkg) => {
- if (!pkg) return []
- if (Array.isArray(pkg.detailList) && pkg.detailList.length > 0) {
- return pkg.detailList
- }
- const list = pkg?.grfwbmxList || []
- return list.map((item) => ({
- title: item?.mc || '',
- html: `<p>${getServiceDesc(item)}</p>`
- }))
- }
- const getRichNodes = (html) => {
- if (!html) return ''
- // 通过内联样式控制 rich-text 的基础排版(不同端对外部样式支持不一致)
- return `<div style="font-size: 36rpx; line-height: 36rpx; color: #000;">${html}</div>`
- }
- const getServiceDesc = (item) => {
- if (!item) return ''
- if (item.sfmf == 1) {
- return '该服务为免费项,订阅后可直接使用。'
- }
- if (item.sc > 0) {
- return `包含可用时长:${item.sc} 分钟。`
- }
- if (item.cs > 0) {
- return `包含可用次数:${item.cs} 次。`
- }
- if (item.ll > 0) {
- return `包含可用流量:${item.ll} MB。`
- }
- return '订阅后可使用该服务。'
- }
- const openDetail = (pkg) => {
- selectPackage(pkg)
- termsAccepted.value = false
- expandedPackageId.value = String(pkg?.grfwbid || '')
- }
- const collapseDetail = () => {
- expandedPackageId.value = ''
- termsAccepted.value = false
- }
- const isExpanded = (pkg) => String(pkg?.grfwbid || '') === expandedPackageId.value
- const toggleExpand = (pkg) => {
- const pkgId = String(pkg?.grfwbid || '')
- if (!pkgId) return
- if (expandedPackageId.value === pkgId) {
- collapseDetail()
- return
- }
- openDetail(pkg)
- }
- const openTermsDrawer = () => {
- termsDrawerVisible.value = true
- }
- const closeTermsDrawer = () => {
- termsDrawerVisible.value = false
- }
- const agreeTerms = () => {
- termsAccepted.value = true
- termsDrawerVisible.value = false
- }
- const toggleTermsAccepted = () => {
- termsAccepted.value = !termsAccepted.value
- }
- // 发起支付
- const handlePay = async () => {
- if (isPaymentInProgress.value) {
- console.log('支付进行中,请勿重复点击')
- return
- }
- // 检查是否选择了服务包
- if (!selectedPackage.value) {
- uni.showToast({
- title: '请选择服务包',
- icon: 'none'
- })
- return
- }
- const amount = finalAmount.value
- if (!amount || amount <= 0) {
- uni.showToast({
- title: '支付金额异常',
- icon: 'none'
- })
- return
- }
- try {
- isPaymentInProgress.value = true
- // 1. 创建订单,获取支付参数
- console.log('创建个人服务包订单,包ID:', selectedPackage.value.grfwbid)
- console.log('支付金额:', amount)
- const payParamsRaw = await grfwApi.grfw_prepayGrfwb({
- grfwbid: selectedPackage.value.grfwbid
- })
- console.log('创建订单接口原始返回:', payParamsRaw.data.ssData)
- const payParams = normalizePayParams(payParamsRaw)
- if (!payParams.timeStamp || !payParams.nonceStr || !payParams.package || !payParams.paySign) {
- throw new Error('支付参数不完整')
- }
- console.log('支付参数:', payParams)
- // 2. 调用微信支付
- const paymentResult = await uni.requestPayment({
- provider: 'wxpay',
- timeStamp: String(payParams.timeStamp),
- nonceStr: payParams.nonceStr,
- package: payParams.package,
- signType: payParams.signType || 'RSA',
- paySign: payParams.paySign,
- })
- console.log('支付结果:', paymentResult)
- // 3. 支付成功 - 跳转到支付结果页面
- const resultOrderId = payParams.orderId
- if (resultOrderId) {
- // 跳转到支付结果页面,进行订单状态查询,传递 grfwbid 用于确认服务
- uni.redirectTo({
- url: `/pages/payment/result?orderId=${resultOrderId}&grfwbid=${selectedPackage.value.grfwbid}`
- })
- } else {
- // 如果没有订单ID,直接提示成功
- uni.showToast({
- title: '支付成功',
- icon: 'success',
- duration: 2000
- })
- setTimeout(() => {
- uni.navigateBack()
- }, 2000)
- }
- } catch (error) {
- console.error('支付失败:', error)
- // 处理不同的错误情况
- if (error.errMsg) {
- // 微信支付相关错误
- if (error.errMsg.includes('cancel')) {
- uni.showToast({
- title: '已取消支付',
- icon: 'none'
- })
- } else if (error.errMsg.includes('fail')) {
- uni.showToast({
- title: '支付失败,请重试',
- icon: 'none'
- })
- }
- } else {
- // 其他错误(比如创建订单失败)
- uni.showToast({
- title: error.message || '操作失败',
- icon: 'none'
- })
- }
- } finally {
- isPaymentInProgress.value = false
- }
- }
- // 加载个人服务包列表
- const loadServicePackages = async () => {
- try {
- const res = await grfwApi.grfw_initGrfwbBuy({})
- console.log(res)
- const packages = normalizeServicePackages(res?.data?.ssData)
- if (!packages.length) {
- throw new Error('暂无可用服务包')
- }
- servicePackages.value = packages
- selectedPackage.value = packages[0] || null
- termsAccepted.value = false
- expandedPackageId.value = ''
- } catch (error) {
- console.error('加载服务包失败:', error)
- servicePackages.value = []
- selectedPackage.value = null
- uni.showToast({
- title: error?.message || '加载服务包失败',
- icon: 'none'
- })
- }
- }
- // 页面加载
- onLoad((options) => {
- console.log('充值页面加载,参数:', options)
- // 加载服务包列表
- loadServicePackages()
- })
- // 测试跳转到支付结果页
- const goToTestResult = () => {
- // 使用固定的订单号测试
- const testOrderId = 'pmsgrfwb7m05clpYYgwHG8qlroZB1'
- const testGrfwbid = selectedPackage.value?.grfwbid || ''
- uni.redirectTo({
- url: `/pages/payment/result?orderId=${testOrderId}&grfwbid=${testGrfwbid}`
- })
- }
- </script>
- <style lang="scss" scoped>
- .subscription-page {
- min-height: 100vh;
- background: #fff;
- padding: 24rpx;
- box-sizing: border-box;
- }
- .card-list {
- display: flex;
- flex-direction: column;
- gap: 30rpx;
- }
- .service-card {
- background: #fafafb;
- border-radius: 6rpx;
- padding: 40rpx 50rpx 50rpx;
- box-shadow: 0 7rpx 10rpx rgba(0, 0, 0, 0.2);
- border: 2rpx solid #cbcbcb;
- }
- .service-card.expanded {
- }
- .card-price-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 18rpx;
- margin-top: 10rpx;
- }
- .card-title {
- font-size: 48rpx;
- // font-weight: 700;
- color: #000;
- line-height: 48rpx;
- }
- .card-price {
- font-size: 48rpx;
- // font-weight: 700;
- color: #000;
- }
- .card-action {
- height: 50rpx;
- line-height: 50rpx;
- width: 110rpx;
- border-radius: 6rpx;
- background: #3a3e51;
- color: #fff;
- font-size: 26rpx;
- border: none;
- margin: 0;
- flex-shrink: 0;
- }
- .card-action::after {
- border: none;
- }
- .card-preview {
- margin-top: 18rpx;
- display: -webkit-box;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .expanded-intro {
- // padding-top: 10rpx;
- }
- .card-preview :deep(p),
- .expanded-intro :deep(p) {
- margin: 0;
- padding: 0;
- font-size: 26rpx;
- line-height: 36rpx;
- color: #000;
- }
- .detail-item-rt {
- margin-top: 8rpx;
- }
- .detail-item-rt :deep(p),
- .detail-item-rt :deep(div),
- .detail-item-rt :deep(span),
- .detail-item-rt :deep(li) {
- margin: 0;
- padding: 0;
- font-size: 26rpx;
- line-height: 36rpx;
- color: #000;
- }
- .detail-item-rt :deep(b),
- .detail-item-rt :deep(strong) {
- font-weight: 700;
- color: #111;
- }
- .empty-state {
- text-align: center;
- padding: 80rpx 0;
- color: #999;
- font-size: 28rpx;
- }
- .detail-section-title {
- margin-top: 40rpx;
- margin-bottom: 12rpx;
- font-size: 48rpx;
- color: #000;
- line-height: 48rpx;
- }
- .detail-item {
- padding: 14rpx 0;
- }
- .detail-item-title {
- font-size: 36rpx;
- font-weight: bold;
- color: #000;
- }
- .detail-item-desc {
- margin-top: 8rpx;
- font-size: 26rpx;
- line-height: 36rpx;
- color: #7b8597;
- }
- .expanded-body {
- margin-top: 18rpx;
- }
- .terms-text {
- font-size: 36rpx;
- color: #0030ab;
- }
- .expanded-footer {
- padding-top: 16rpx;
- }
- .bottom-terms {
- display: flex;
- align-items: center;
- gap: 10rpx;
- padding: 0 6rpx 14rpx;
- }
- .checkbox {
- width: 34rpx;
- height: 34rpx;
- border-radius: 6rpx;
- border: 2rpx solid #b8b8b8;
- background: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- }
- .checkbox.checked {
- border-color: #3a3e51;
- background: #3a3e51;
- }
- .checkmark {
- color: #fff;
- font-size: 24rpx;
- line-height: 24rpx;
- font-weight: 700;
- }
- .bottom-btn {
- width: 100%;
- height: 70rpx;
- line-height: 70rpx;
- border-radius: 6rpx;
- background: #3a3e51;
- color: #fff;
- font-size: 40rpx;
- // font-weight: 700;
- border: none;
- }
- .bottom-btn[disabled] {
- background: #3a3e51;
- color: #fff;
- }
- .bottom-btn::after {
- border: none;
- }
- .terms-drawer {
- background: #fff;
- padding: 22rpx 24rpx 0;
- max-height: 80vh;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- }
- .drawer-head {
- display: flex;
- align-items: center;
- justify-content: center;
- padding-bottom: 16rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .drawer-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #111;
- }
- .drawer-body {
- flex: 1;
- min-height: 0;
- padding: 16rpx 0;
- }
- .drawer-text {
- padding-right: 10rpx;
- }
- .drawer-p {
- font-size: 26rpx;
- line-height: 40rpx;
- color: #444;
- margin-bottom: 12rpx;
- }
- .drawer-actions {
- margin-top: auto;
- padding-top: 14rpx;
- }
- .drawer-agree {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- border-radius: 6rpx;
- background: #3a3e51;
- color: #fff;
- font-size: 30rpx;
- font-weight: 700;
- border: none;
- }
- .drawer-agree::after {
- border: none;
- }
- </style>
|