rentease-app/pages/billing/plan-select.vue

703 lines
17 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="plan-select-page">
<!-- 自定义导航栏 -->
<view class="custom-nav safe-area-top">
<view class="nav-content">
<view class="nav-btn" @click="goBack">
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
</view>
<text class="nav-title">选择套餐</text>
<view class="nav-btn"></view>
</view>
</view>
<scroll-view scroll-y class="page-content">
<!-- 套餐列表 -->
<view class="plan-list">
<view
v-for="(plan, index) in plans"
:key="plan.id"
class="plan-card"
:class="{ active: selectedPlanId === plan.id, recommended: plan.isRecommended }"
@click="selectPlan(plan)"
>
<view v-if="plan.isRecommended" class="recommend-badge">推荐</view>
<view class="plan-header">
<view class="plan-info">
<text class="plan-name">{{plan.name}}</text>
<text class="plan-desc">{{plan.description}}</text>
</view>
<view class="plan-price">
<text class="price">¥{{plan.monthlyPrice}}</text>
<text class="unit">/月</text>
</view>
</view>
<view class="plan-features">
<view class="feature-item">
<uni-icons type="checkmarkempty" size="16" color="#67C23A"></uni-icons>
<text>最多 {{plan.maxApartments}} 栋公寓</text>
</view>
<view class="feature-item">
<uni-icons type="checkmarkempty" size="16" color="#67C23A"></uni-icons>
<text>最多 {{plan.maxRooms}} 个房间</text>
</view>
<view class="feature-item">
<uni-icons type="checkmarkempty" size="16" color="#67C23A"></uni-icons>
<text>最多 {{plan.maxUsers}} 个用户</text>
</view>
</view>
<view class="select-indicator" v-if="selectedPlanId === plan.id">
<uni-icons type="checkmarkempty" size="20" color="#FFFFFF"></uni-icons>
</view>
</view>
</view>
<!-- 购买时长 -->
<view class="section-card" v-if="selectedPlanId">
<view class="section-title">购买时长</view>
<view class="period-list">
<view
v-for="period in periods"
:key="period.value"
class="period-item"
:class="{ active: selectedMonths === period.value, discount: period.discount }"
@click="selectPeriod(period.value)"
>
<text class="period-label">{{period.label}}</text>
<text v-if="period.discount" class="discount-tag">省{{period.discount}}%</text>
</view>
</view>
</view>
<!-- 费用明细 -->
<view class="section-card price-detail" v-if="selectedPlanId">
<view class="section-title">费用明细</view>
<view class="price-list">
<view class="price-row">
<text class="row-label">套餐单价</text>
<text class="row-value">¥{{selectedPlan?.monthlyPrice || 0}}/月</text>
</view>
<view class="price-row">
<text class="row-label">购买时长</text>
<text class="row-value">{{selectedMonths}} 个月</text>
</view>
<view class="price-row">
<text class="row-label">基础费用</text>
<text class="row-value">¥{{baseAmount}}</text>
</view>
<view class="price-row" v-if="discountAmount > 0">
<text class="row-label">优惠金额</text>
<text class="row-value discount">-¥{{discountAmount}}</text>
</view>
<view class="price-row total">
<text class="row-label">合计</text>
<text class="row-value total-price">¥{{totalAmount}}</text>
</view>
</view>
</view>
<!-- 支付方式说明 -->
<view class="section-card" v-if="paymentSettings">
<view class="section-title">支付方式</view>
<view class="payment-methods">
<view class="payment-item" v-if="paymentSettings.alipayAccount">
<view class="payment-icon alipay">
<text>支</text>
</view>
<view class="payment-info">
<text class="payment-name">支付宝</text>
<text class="payment-account">{{paymentSettings.alipayAccount}}</text>
</view>
<view class="copy-btn" @click="copyText(paymentSettings.alipayAccount)">复制</view>
</view>
<view class="payment-item" v-if="paymentSettings.wechatId">
<view class="payment-icon wechat">
<text>微</text>
</view>
<view class="payment-info">
<text class="payment-name">微信支付</text>
<text class="payment-account">{{paymentSettings.wechatId}}</text>
</view>
<view class="copy-btn" @click="copyText(paymentSettings.wechatId)">复制</view>
</view>
<view class="payment-item" v-if="paymentSettings.bankAccount">
<view class="payment-icon bank">
<text>银</text>
</view>
<view class="payment-info">
<text class="payment-name">银行转账</text>
<text class="payment-account">{{paymentSettings.bankName}} {{paymentSettings.bankAccount}}</text>
</view>
</view>
</view>
<view class="payment-notice">
<text class="notice-title">注意事项:</text>
<text class="notice-item">1. 转账时请备注您的账号信息</text>
<text class="notice-item">2. 转账完成后,请联系客服确认</text>
<text class="notice-item" v-if="paymentSettings.servicePhone">3. 客服电话:{{paymentSettings.servicePhone}}</text>
</view>
</view>
<view class="safe-area-bottom" style="height: 120rpx;"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar safe-area-bottom" v-if="selectedPlanId">
<view class="price-summary">
<text class="summary-label">合计:</text>
<text class="summary-price">¥{{totalAmount}}</text>
</view>
<button class="submit-btn" :loading="submitting" @click="createOrder">创建订单</button>
</view>
</view>
</template>
<script>
import { billingApi } from '@/api/index.js'
export default {
data() {
return {
plans: [],
selectedPlanId: null,
selectedMonths: 1,
periods: [
{ label: '1个月', value: 1, discount: 0 },
{ label: '3个月', value: 3, discount: 5 },
{ label: '6个月', value: 6, discount: 10 },
{ label: '12个月', value: 12, discount: 15 }
],
paymentSettings: null,
submitting: false,
baseAmount: 0,
discountAmount: 0,
totalAmount: 0
}
},
computed: {
selectedPlan() {
return this.plans.find(p => p.id === this.selectedPlanId)
}
},
watch: {
selectedPlanId() {
this.calculatePrice()
},
selectedMonths() {
this.calculatePrice()
}
},
onLoad() {
this.loadPlans()
this.loadPaymentSettings()
},
methods: {
async loadPlans() {
try {
const res = await billingApi.getPlans()
if (res.code === 200) {
// 过滤掉免费版monthlyPrice <= 0和未启用的套餐
this.plans = (res.data || []).filter(p => p.status === 'active' && p.monthlyPrice > 0)
// 如果有推荐套餐,默认选中
const recommended = this.plans.find(p => p.isRecommended)
if (recommended) {
this.selectedPlanId = recommended.id
} else if (this.plans.length > 0) {
this.selectedPlanId = this.plans[0].id
}
}
} catch (error) {
console.error('加载套餐失败:', error)
uni.showToast({ title: '加载套餐失败', icon: 'none' })
}
},
async loadPaymentSettings() {
try {
const res = await billingApi.getPaymentSettings()
if (res.code === 200) {
this.paymentSettings = res.data
}
} catch (error) {
console.error('加载支付设置失败:', error)
}
},
selectPlan(plan) {
this.selectedPlanId = plan.id
},
selectPeriod(months) {
this.selectedMonths = months
},
calculatePrice() {
if (!this.selectedPlan) {
this.baseAmount = 0
this.discountAmount = 0
this.totalAmount = 0
return
}
const monthlyPrice = this.selectedPlan.monthlyPrice
const baseAmount = monthlyPrice * this.selectedMonths
// 计算折扣
const period = this.periods.find(p => p.value === this.selectedMonths)
const discountRate = period ? period.discount / 100 : 0
const discountAmount = Math.round(baseAmount * discountRate)
this.baseAmount = baseAmount
this.discountAmount = discountAmount
this.totalAmount = baseAmount - discountAmount
},
async createOrder() {
if (!this.selectedPlanId) {
uni.showToast({ title: '请选择套餐', icon: 'none' })
return
}
this.submitting = true
try {
const res = await billingApi.createOrder({
planId: this.selectedPlanId,
months: this.selectedMonths
})
if (res.code === 200) {
uni.showToast({
title: '订单创建成功',
icon: 'success'
})
// 跳转到订单详情页
setTimeout(() => {
uni.redirectTo({
url: `/pages/billing/order-detail?id=${res.data.order.id}`
})
}, 1500)
} else {
uni.showToast({
title: res.message || '创建订单失败',
icon: 'none'
})
}
} catch (error) {
console.error('创建订单失败:', error)
uni.showToast({
title: '创建订单失败',
icon: 'none'
})
} finally {
this.submitting = false
}
},
copyText(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
},
goBack() {
uni.navigateBack()
}
}
}
</script>
<style scoped>
.plan-select-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
flex-direction: column;
}
/* 导航栏 */
.custom-nav {
background: #FFFFFF;
border-bottom: 2rpx solid #F1F5F9;
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
}
.nav-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
}
/* 页面内容 */
.page-content {
flex: 1;
padding: 24rpx 32rpx;
}
/* 套餐卡片 */
.plan-list {
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 24rpx;
}
.plan-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
border: 4rpx solid transparent;
position: relative;
}
.plan-card.active {
border-color: #667eea;
}
.plan-card.recommended {
background: linear-gradient(135deg, #667eea08 0%, #764ba208 100%);
}
.recommend-badge {
position: absolute;
top: -4rpx;
right: 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #FFFFFF;
font-size: 22rpx;
font-weight: 600;
padding: 8rpx 24rpx;
border-radius: 0 0 12rpx 12rpx;
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24rpx;
}
.plan-info {
flex: 1;
}
.plan-name {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #1E293B;
margin-bottom: 8rpx;
}
.plan-desc {
display: block;
font-size: 24rpx;
color: #64748B;
}
.plan-price {
text-align: right;
}
.plan-price .price {
font-size: 48rpx;
font-weight: 700;
color: #F56C6C;
}
.plan-price .unit {
font-size: 24rpx;
color: #94A3B8;
}
.plan-features {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.feature-item {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 26rpx;
color: #64748B;
}
.select-indicator {
position: absolute;
bottom: 32rpx;
right: 32rpx;
width: 48rpx;
height: 48rpx;
background: #667eea;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* 区块卡片 */
.section-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #1E293B;
margin-bottom: 24rpx;
}
/* 时长选择 */
.period-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.period-item {
padding: 20rpx 32rpx;
background: #F8FAFC;
border-radius: 12rpx;
border: 2rpx solid transparent;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
min-width: 140rpx;
}
.period-item.active {
background: #667eea;
border-color: #667eea;
}
.period-item.discount {
border-color: #F56C6C;
}
.period-item.active.discount {
border-color: #667eea;
}
.period-label {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.period-item.active .period-label {
color: #FFFFFF;
}
.discount-tag {
font-size: 20rpx;
color: #F56C6C;
background: #FEE2E2;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.period-item.active .discount-tag {
color: #FFFFFF;
background: rgba(255, 255, 255, 0.3);
}
/* 费用明细 */
.price-detail .price-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-row.total {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 2rpx solid #F1F5F9;
}
.row-label {
font-size: 28rpx;
color: #64748B;
}
.row-value {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.row-value.discount {
color: #67C23A;
}
.row-value.total-price {
font-size: 40rpx;
color: #F56C6C;
font-weight: 700;
}
/* 支付方式 */
.payment-methods {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.payment-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx;
background: #F8FAFC;
border-radius: 12rpx;
}
.payment-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.payment-icon.alipay {
background: #1677FF;
}
.payment-icon.wechat {
background: #07C160;
}
.payment-icon.bank {
background: #FF6B6B;
}
.payment-icon text {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 700;
}
.payment-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.payment-name {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.payment-account {
font-size: 24rpx;
color: #64748B;
}
.copy-btn {
padding: 12rpx 24rpx;
background: #667eea;
color: #FFFFFF;
font-size: 24rpx;
border-radius: 8rpx;
}
.payment-notice {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 2rpx solid #F1F5F9;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.notice-title {
font-size: 26rpx;
color: #1E293B;
font-weight: 600;
}
.notice-item {
font-size: 24rpx;
color: #64748B;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
border-top: 2rpx solid #F1F5F9;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
}
.price-summary {
display: flex;
align-items: baseline;
gap: 8rpx;
}
.summary-label {
font-size: 28rpx;
color: #64748B;
}
.summary-price {
font-size: 40rpx;
color: #F56C6C;
font-weight: 700;
}
.submit-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx 48rpx;
border-radius: 16rpx;
border: none;
}
</style>