644 lines
16 KiB
Vue
644 lines
16 KiB
Vue
<template>
|
|
<view class="page">
|
|
<u-navbar title="购买套餐" :border-bottom="false" :background="bgc" back-icon-color="#111827" title-color="#111827" title-size="34" height="44"></u-navbar>
|
|
|
|
<scroll-view scroll-y class="page-scroll">
|
|
<view class="ability-card">
|
|
<view class="ability-card-header">
|
|
<view class="ability-card-top">
|
|
<image class="ability-icon" :src="abilityIcon" mode="aspectFill" v-if="abilityIcon"></image>
|
|
<view class="ability-icon ability-icon--placeholder" v-else>
|
|
<text>{{ (abilityName || '能').slice(0, 1) }}</text>
|
|
</view>
|
|
<view class="ability-info">
|
|
<text class="ability-name">{{ abilityName || '--' }}</text>
|
|
<text class="ability-area" v-if="areaName">{{ areaName }}</text>
|
|
</view>
|
|
</view>
|
|
<view hover-class="app-tap-hover" class="order-record-link" @tap="goOrderList">
|
|
<u-icon name="list" size="28" color="#4297F3"></u-icon>
|
|
<text>购买记录</text>
|
|
</view>
|
|
</view>
|
|
<view class="ability-card-stats">
|
|
<view class="stat-box">
|
|
<text class="stat-num">{{ formatCount(remainingCount) }}</text>
|
|
<text class="stat-label">剩余/次</text>
|
|
</view>
|
|
<view class="stat-box">
|
|
<text class="stat-num">{{ formatCount(usedCount) }}</text>
|
|
<text class="stat-label">已用/次</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="section">
|
|
<view class="section-title">
|
|
<view class="section-bar"></view>
|
|
<text>选择套餐</text>
|
|
</view>
|
|
<view class="suit-loading" v-if="suitLoading">加载中...</view>
|
|
<view class="suit-empty" v-else-if="!suitList.length">暂无可用套餐</view>
|
|
<view class="suit-grid" v-else>
|
|
<view
|
|
hover-class="app-tap-hover"
|
|
class="suit-card"
|
|
v-for="(item, index) in suitList"
|
|
:key="item.id || index"
|
|
:class="{ 'suit-card--active': suitIndex === index }"
|
|
@click="selectSuit(index)"
|
|
>
|
|
<view class="suit-check" v-if="suitIndex === index">
|
|
<text class="suit-check-mark">✓</text>
|
|
</view>
|
|
<text class="suit-name">{{ suitName(item) }}</text>
|
|
<view class="suit-price">
|
|
<text class="suit-price-symbol">¥</text>
|
|
<text class="suit-price-num">{{ formatPrice(item) }}</text>
|
|
</view>
|
|
<text class="suit-count">{{ formatCount(suitTimes(item)) }} 次</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="section">
|
|
<view class="section-title">
|
|
<view class="section-bar"></view>
|
|
<text>支付方式</text>
|
|
</view>
|
|
<view class="pay-loading" v-if="channelLoading">加载支付方式...</view>
|
|
<view class="pay-empty" v-else-if="!paymentList.length">暂无可用支付方式</view>
|
|
<radio-group v-else class="pay-list" @change="onChannelChange">
|
|
<label
|
|
class="pay-item"
|
|
v-for="item in paymentList"
|
|
:key="item.id"
|
|
>
|
|
<view class="pay-item-left">
|
|
<image class="pay-icon" :src="item.picture" mode="aspectFit" v-if="item.picture"></image>
|
|
<view class="pay-icon pay-icon--placeholder" v-else>
|
|
<text>付</text>
|
|
</view>
|
|
<text class="pay-name">{{ item.name || '支付' }}</text>
|
|
</view>
|
|
<radio
|
|
:value="String(item.id)"
|
|
:checked="String(channelId) === String(item.id)"
|
|
color="#4297F3"
|
|
/>
|
|
</label>
|
|
</radio-group>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<view class="footer-placeholder"></view>
|
|
<view class="footer-bar">
|
|
<view class="footer-price">
|
|
<text class="footer-label">应付</text>
|
|
<text class="footer-amount">¥{{ selectedPrice }}</text>
|
|
</view>
|
|
<view
|
|
hover-class="app-tap-hover"
|
|
class="footer-btn"
|
|
:class="{ 'footer-btn--disabled': !canPay }"
|
|
@click="handlePay"
|
|
>
|
|
<text>立即支付</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
data() {
|
|
return {
|
|
bgc: { backgroundColor: '#fff' },
|
|
areaId: '',
|
|
areaName: '',
|
|
areaAbilityId: '',
|
|
abilityName: '',
|
|
abilityIcon: '',
|
|
remainingCount: 0,
|
|
usedCount: 0,
|
|
suitLoading: false,
|
|
suitList: [],
|
|
suitIndex: 0,
|
|
channelLoading: false,
|
|
paymentList: [],
|
|
channelId: '',
|
|
paying: false,
|
|
abilityId:''
|
|
}
|
|
},
|
|
computed: {
|
|
selectedSuit() {
|
|
return this.suitList[this.suitIndex] || null
|
|
},
|
|
selectedPrice() {
|
|
return this.formatPrice(this.selectedSuit || {})
|
|
},
|
|
canPay() {
|
|
return !!(this.selectedSuit && this.selectedSuit.id && this.channelId && !this.paying)
|
|
}
|
|
},
|
|
onLoad(e) {
|
|
this.areaId = e.areaId || ''
|
|
this.abilityId = e.abilityId ? decodeURIComponent(e.abilityId) : ''
|
|
this.areaName = e.areaName ? decodeURIComponent(e.areaName) : ''
|
|
this.areaAbilityId = e.areaAbilityId || ''
|
|
this.abilityName = e.abilityName ? decodeURIComponent(e.abilityName) : ''
|
|
this.abilityIcon = e.abilityIcon ? decodeURIComponent(e.abilityIcon) : ''
|
|
this.remainingCount = e.remainingCount != null ? e.remainingCount : 0
|
|
this.usedCount = e.usedCount != null ? e.usedCount : 0
|
|
this.fetchSuitList()
|
|
this.fetchPaymentChannels()
|
|
},
|
|
methods: {
|
|
goOrderList() {
|
|
if (!this.abilityId) {
|
|
uni.showToast({ title: '缺少能力ID', icon: 'none' })
|
|
return
|
|
}
|
|
const q = [
|
|
`abilityId=${this.abilityId}`,
|
|
`abilityName=${encodeURIComponent(this.abilityName || '')}`
|
|
].join('&')
|
|
uni.navigateTo({
|
|
url: `/page_shanghu/guanli/area_ability_order_list?${q}`
|
|
})
|
|
},
|
|
formatCount(val) {
|
|
const n = Number(val)
|
|
return Number.isFinite(n) ? n : 0
|
|
},
|
|
formatPrice(item) {
|
|
const n = Number(item && (item.price != null ? item.price : item.amount))
|
|
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
|
},
|
|
suitName(item) {
|
|
return (item && (item.name || item.suitName)) || '标准套餐'
|
|
},
|
|
suitTimes(item) {
|
|
if (!item) return 0
|
|
if (item.suitCount != null) return item.suitCount
|
|
if (item.count != null) return item.count
|
|
if (item.totalCount != null) return item.totalCount
|
|
return item.times
|
|
},
|
|
normalizeRows(res) {
|
|
if (res.code != 200) return []
|
|
return res.rows != null ? res.rows : (Array.isArray(res.data) ? res.data : [])
|
|
},
|
|
fetchSuitList() {
|
|
if (!this.areaId) return
|
|
this.suitLoading = true
|
|
const params = {
|
|
operationAreaId: this.areaId,
|
|
areaId: this.areaId,
|
|
abilityId:this.abilityId
|
|
}
|
|
if (this.areaAbilityId) {
|
|
params.areaAbilityId = this.areaAbilityId
|
|
}
|
|
this.$u
|
|
.get('/bst/abilitySuit/list', params)
|
|
.then((res) => {
|
|
this.suitList = this.normalizeRows(res)
|
|
if (res.code != 200) {
|
|
uni.showToast({ title: res.msg || '套餐加载失败', icon: 'none' })
|
|
}
|
|
if (this.suitList.length && this.suitIndex >= this.suitList.length) {
|
|
this.suitIndex = 0
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.suitList = []
|
|
uni.showToast({ title: '套餐加载失败', icon: 'none' })
|
|
})
|
|
.finally(() => {
|
|
this.suitLoading = false
|
|
})
|
|
},
|
|
fetchPaymentChannels() {
|
|
if (!this.areaId) return
|
|
const appId = this.$store.state.appid || 1
|
|
this.channelLoading = true
|
|
this.$u
|
|
.get('/app/channel/list', {
|
|
appId,
|
|
areaId: this.areaId,
|
|
bstType: 3,
|
|
appType: 1
|
|
})
|
|
.then((res) => {
|
|
if (res.code == 200 && Array.isArray(res.data)) {
|
|
this.paymentList = res.data
|
|
if (res.data.length) {
|
|
this.channelId = String(res.data[0].id)
|
|
}
|
|
} else {
|
|
this.paymentList = []
|
|
uni.showToast({ title: res.msg || '支付方式加载失败', icon: 'none' })
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.paymentList = []
|
|
uni.showToast({ title: '支付方式加载失败', icon: 'none' })
|
|
})
|
|
.finally(() => {
|
|
this.channelLoading = false
|
|
})
|
|
},
|
|
onChannelChange(e) {
|
|
if (e && e.detail && e.detail.value != null) {
|
|
this.channelId = String(e.detail.value)
|
|
}
|
|
},
|
|
selectSuit(index) {
|
|
this.suitIndex = index
|
|
},
|
|
getPayChannelId() {
|
|
return this.channelId
|
|
},
|
|
goTradeRecord() {
|
|
uni.showToast({ title: '交易记录开发中', icon: 'none' })
|
|
},
|
|
handlePay() {
|
|
if (!this.canPay) {
|
|
if (!this.selectedSuit || this.selectedSuit.id == null) {
|
|
uni.showToast({ title: '请选择套餐', icon: 'none' })
|
|
} else if (!this.getPayChannelId()) {
|
|
uni.showToast({ title: '支付通道未准备好', icon: 'none' })
|
|
this.fetchPaymentChannels()
|
|
} else if (!this.areaAbilityId) {
|
|
uni.showToast({ title: '缺少拓展能力信息', icon: 'none' })
|
|
}
|
|
return
|
|
}
|
|
this.paying = true
|
|
uni.showLoading({ title: '提交中...', mask: true })
|
|
const channelId = this.getPayChannelId()
|
|
const data = {
|
|
areaAbilityId: Number(this.areaAbilityId) || this.areaAbilityId,
|
|
suit: { ...this.selectedSuit },
|
|
channelId: Number(channelId) || channelId
|
|
}
|
|
this.$u
|
|
.post('/bst/abilityOrder', data)
|
|
.then((res) => {
|
|
if (res.code != 200) {
|
|
uni.showToast({ title: res.msg || '下单失败', icon: 'none' })
|
|
return
|
|
}
|
|
if (res.data && res.data.needPay === true && res.data.payParams) {
|
|
this.requestWxPay(res)
|
|
} else {
|
|
this.onPaySuccess()
|
|
}
|
|
})
|
|
.catch(() => {
|
|
uni.showToast({ title: '下单失败', icon: 'none' })
|
|
})
|
|
.finally(() => {
|
|
this.paying = false
|
|
uni.hideLoading()
|
|
})
|
|
},
|
|
requestWxPay(res) {
|
|
const that = this
|
|
uni.requestPayment({
|
|
provider: 'wxpay',
|
|
timeStamp: res.data.payParams.timeStamp,
|
|
nonceStr: res.data.payParams.nonceStr,
|
|
package: res.data.payParams.packageVal,
|
|
signType: res.data.payParams.signType,
|
|
paySign: res.data.payParams.paySign,
|
|
success() {
|
|
if (res.data.pay && res.data.pay.no) {
|
|
that.$u.put(`/app/pay/refreshPayResult?no=${res.data.pay.no}`).finally(() => {
|
|
that.onPaySuccess()
|
|
})
|
|
} else {
|
|
that.onPaySuccess()
|
|
}
|
|
},
|
|
fail() {
|
|
uni.showToast({ title: '已取消支付', icon: 'none' })
|
|
}
|
|
})
|
|
},
|
|
onPaySuccess() {
|
|
uni.showToast({ title: '购买成功', icon: 'success' })
|
|
const pages = getCurrentPages()
|
|
const prev = pages[pages.length - 2]
|
|
if (prev && prev.$vm) {
|
|
prev.$vm._needRefresh = true
|
|
}
|
|
setTimeout(() => uni.navigateBack(), 600)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
page {
|
|
background: #f6f8fa;
|
|
}
|
|
.page {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.page-scroll {
|
|
flex: 1;
|
|
height: calc(100vh - 44px - 120rpx);
|
|
padding: 24rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
.ability-card {
|
|
background: #fff;
|
|
border-radius: 24rpx;
|
|
padding: 28rpx;
|
|
box-shadow: 0 8rpx 24rpx rgba(44, 138, 240, 0.06);
|
|
}
|
|
.ability-card-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16rpx;
|
|
}
|
|
.ability-card-top {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
}
|
|
.ability-icon {
|
|
width: 88rpx;
|
|
height: 88rpx;
|
|
border-radius: 16rpx;
|
|
flex-shrink: 0;
|
|
background: #f3f4f6;
|
|
}
|
|
.ability-icon--placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, #86efac 0%, #22c55e 100%);
|
|
text {
|
|
font-size: 38rpx;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
}
|
|
.ability-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0 16rpx;
|
|
}
|
|
.ability-name {
|
|
display: block;
|
|
font-size: 34rpx;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
}
|
|
.ability-area {
|
|
display: block;
|
|
margin-top: 8rpx;
|
|
font-size: 26rpx;
|
|
color: #6b7280;
|
|
}
|
|
.order-record-link {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
font-size: 22rpx;
|
|
color: #4297F3;
|
|
padding-top: 4rpx;
|
|
text {
|
|
margin-top: 4rpx;
|
|
}
|
|
}
|
|
.ability-card-stats {
|
|
margin-top: 20rpx;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 16rpx;
|
|
}
|
|
.stat-box {
|
|
min-width: 120rpx;
|
|
padding: 12rpx 20rpx;
|
|
background: #f8fafc;
|
|
border-radius: 12rpx;
|
|
text-align: center;
|
|
border: 1rpx solid #eef2f7;
|
|
}
|
|
.stat-num {
|
|
display: block;
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
line-height: 1.2;
|
|
}
|
|
.stat-label {
|
|
display: block;
|
|
margin-top: 4rpx;
|
|
font-size: 22rpx;
|
|
color: #9ca3af;
|
|
}
|
|
.section {
|
|
margin-top: 28rpx;
|
|
}
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 20rpx;
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
}
|
|
.section-bar {
|
|
width: 8rpx;
|
|
height: 32rpx;
|
|
background: #4297F3;
|
|
border-radius: 4rpx;
|
|
margin-right: 12rpx;
|
|
}
|
|
.suit-loading,
|
|
.suit-empty {
|
|
padding: 60rpx 0;
|
|
text-align: center;
|
|
font-size: 28rpx;
|
|
color: #9ca3af;
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
}
|
|
.suit-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20rpx;
|
|
}
|
|
.suit-card {
|
|
position: relative;
|
|
width: calc(50% - 10rpx);
|
|
box-sizing: border-box;
|
|
background: #fff;
|
|
border: 2rpx solid #e5e7eb;
|
|
border-radius: 20rpx;
|
|
padding: 28rpx 20rpx 24rpx;
|
|
overflow: hidden;
|
|
}
|
|
.suit-card--active {
|
|
border-color: #4297F3;
|
|
border-width: 3rpx;
|
|
background: linear-gradient(135deg, #f0f7ff 0%, #fff 100%);
|
|
box-shadow: 0 6rpx 20rpx rgba(66, 151, 243, 0.2);
|
|
}
|
|
.suit-check {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 52rpx;
|
|
height: 52rpx;
|
|
background: linear-gradient(135deg, #4297F3 0%, #60a5fa 100%);
|
|
clip-path: polygon(100% 0, 100% 100%, 0 100%);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: flex-end;
|
|
padding: 0 6rpx 6rpx 0;
|
|
}
|
|
.suit-check-mark {
|
|
font-size: 24rpx;
|
|
color: #fff;
|
|
font-weight: 700;
|
|
}
|
|
.suit-name {
|
|
display: block;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
text-align: center;
|
|
}
|
|
.suit-price {
|
|
margin-top: 16rpx;
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: center;
|
|
color: #f17f37;
|
|
}
|
|
.suit-price-symbol {
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
}
|
|
.suit-price-num {
|
|
font-size: 48rpx;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
.suit-count {
|
|
display: block;
|
|
margin-top: 12rpx;
|
|
text-align: center;
|
|
font-size: 26rpx;
|
|
color: #6b7280;
|
|
}
|
|
.pay-loading,
|
|
.pay-empty {
|
|
padding: 40rpx 0;
|
|
text-align: center;
|
|
font-size: 28rpx;
|
|
color: #9ca3af;
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
}
|
|
.pay-list {
|
|
display: block;
|
|
}
|
|
.pay-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
padding: 24rpx 28rpx;
|
|
margin-bottom: 16rpx;
|
|
border: 1rpx solid #eef2f7;
|
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
.pay-item-left {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.pay-icon {
|
|
width: 64rpx;
|
|
height: 64rpx;
|
|
border-radius: 12rpx;
|
|
flex-shrink: 0;
|
|
background: #f3f4f6;
|
|
}
|
|
.pay-icon--placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #07c160;
|
|
text {
|
|
font-size: 28rpx;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
}
|
|
.pay-name {
|
|
margin-left: 20rpx;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
}
|
|
.footer-placeholder {
|
|
height: 140rpx;
|
|
}
|
|
.footer-bar {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
background: #fff;
|
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
|
z-index: 100;
|
|
}
|
|
.footer-price {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.footer-label {
|
|
font-size: 24rpx;
|
|
color: #9ca3af;
|
|
margin-right: 8rpx;
|
|
}
|
|
.footer-amount {
|
|
font-size: 40rpx;
|
|
font-weight: 700;
|
|
color: #f17f37;
|
|
}
|
|
.footer-btn {
|
|
width: 280rpx;
|
|
height: 88rpx;
|
|
line-height: 88rpx;
|
|
text-align: center;
|
|
background: linear-gradient(135deg, #4297F3 0%, #2b76e5 100%);
|
|
border-radius: 44rpx;
|
|
color: #fff;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
}
|
|
.footer-btn--disabled {
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|