rentease-app/pages/billing/billing-center.vue

844 lines
21 KiB
Vue
Raw Normal View History

2026-04-20 06:23:11 +00:00
<template>
<view class="billing-center-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" @click="refreshData">
<uni-icons type="refresh" size="20" color="#64748B"></uni-icons>
</view>
</view>
</view>
<scroll-view scroll-y class="page-content" @scrolltolower="loadMore">
<!-- 计费状态卡片 -->
<view class="status-card" :class="billingStatusClass">
<view class="status-header">
<view class="status-icon">
<uni-icons :type="statusIcon" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="status-info">
<text class="status-title">{{statusTitle}}</text>
<text class="status-desc">{{statusDesc}}</text>
</view>
</view>
<view class="status-progress" v-if="daysLeft > 0">
<view class="progress-bar">
<view class="progress-fill" :style="{width: progressPercent + '%', background: progressColor}"></view>
</view>
<text class="days-left">剩余 {{daysLeft}} </text>
</view>
<view class="status-action" v-if="needRenewal">
<button class="renew-btn" @click="goToPlanSelect">立即续费</button>
</view>
</view>
<!-- 当前套餐 -->
<view class="section-card" v-if="billingInfo.plan">
<view class="card-header">
<view class="header-left">
<uni-icons type="vip-filled" size="20" color="#667eea"></uni-icons>
<text class="header-title">当前套餐</text>
</view>
<view class="plan-tag" :class="planTagType">{{billingInfoStatusText}}</view>
</view>
<view class="plan-content">
<view class="plan-main">
<text class="plan-name">{{billingInfo.plan.name}}</text>
<view class="plan-price">
<text class="price">¥{{billingInfo.plan.monthlyPrice}}</text>
<text class="unit">/</text>
</view>
</view>
<text class="plan-desc">{{billingInfo.plan.description}}</text>
<view class="plan-resources">
<view class="resource-item">
<uni-icons type="home-filled" size="16" color="#667eea"></uni-icons>
<text>{{billingInfo.plan.maxApartments}} 栋公寓</text>
</view>
<view class="resource-item">
<uni-icons type="shop-filled" size="16" color="#667eea"></uni-icons>
<text>{{billingInfo.plan.maxRooms}} 个房间</text>
</view>
<view class="resource-item">
<uni-icons type="person-filled" size="16" color="#667eea"></uni-icons>
<text>{{billingInfo.plan.maxUsers}} 个用户</text>
</view>
</view>
</view>
</view>
<!-- 资源使用 -->
<view class="section-card">
<view class="card-header">
<view class="header-left">
<uni-icons type="chart-filled" size="20" color="#667eea"></uni-icons>
<text class="header-title">资源使用</text>
</view>
<text class="refresh-text" @click="refreshUsage">刷新</text>
</view>
<view class="usage-list" v-if="usageStats">
<view class="usage-item">
<view class="usage-label">
<uni-icons type="home-filled" size="16" color="#409EFF"></uni-icons>
<text>公寓</text>
</view>
<view class="usage-bar">
<view class="progress-bg">
<view class="progress-fill" :style="{width: apartmentPercent + '%', background: getProgressColor(apartmentPercent)}"></view>
</view>
</view>
<view class="usage-value">
<text>{{usageStats.usage.apartments}}/{{usageStats.limits.apartments}}</text>
</view>
</view>
<view class="usage-item">
<view class="usage-label">
<uni-icons type="shop-filled" size="16" color="#67C23A"></uni-icons>
<text>房间</text>
</view>
<view class="usage-bar">
<view class="progress-bg">
<view class="progress-fill" :style="{width: roomPercent + '%', background: getProgressColor(roomPercent)}"></view>
</view>
</view>
<view class="usage-value">
<text>{{usageStats.usage.rooms}}/{{usageStats.limits.rooms}}</text>
</view>
</view>
<view class="usage-item">
<view class="usage-label">
<uni-icons type="person-filled" size="16" color="#E6A23C"></uni-icons>
<text>用户</text>
</view>
<view class="usage-bar">
<view class="progress-bg">
<view class="progress-fill" :style="{width: userPercent + '%', background: getProgressColor(userPercent)}"></view>
</view>
</view>
<view class="usage-value">
<text>{{usageStats.usage.users}}/{{usageStats.limits.users}}</text>
</view>
</view>
</view>
<view class="empty-tip" v-else>
<text>暂无使用数据</text>
</view>
</view>
<!-- 快捷入口 -->
<view class="quick-menu">
<view class="menu-item" @click="goToPlanSelect">
<view class="menu-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<uni-icons type="plus-filled" size="24" color="#FFFFFF"></uni-icons>
</view>
<text class="menu-text">购买套餐</text>
</view>
<view class="menu-item" @click="goToOrderList">
<view class="menu-icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
<uni-icons type="list-filled" size="24" color="#FFFFFF"></uni-icons>
</view>
<text class="menu-text">我的订单</text>
</view>
<view class="menu-item" @click="goToPaymentRecord">
<view class="menu-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<uni-icons type="wallet-filled" size="24" color="#FFFFFF"></uni-icons>
</view>
<text class="menu-text">支付记录</text>
</view>
</view>
<!-- 最近订单 -->
<view class="section-card" v-if="recentOrders.length > 0">
<view class="card-header">
<view class="header-left">
<uni-icons type="list-filled" size="20" color="#667eea"></uni-icons>
<text class="header-title">最近订单</text>
</view>
<text class="more-text" @click="goToOrderList">查看全部</text>
</view>
<view class="order-list">
<view v-for="(order, index) in recentOrders" :key="index" class="order-item" @click="viewOrderDetail(order)">
<view class="order-info">
<text class="order-no">{{order.orderNo}}</text>
<text class="order-plan">{{order.planName}}</text>
</view>
<view class="order-meta">
<text class="order-amount">¥{{order.actualAmount}}</text>
<view class="order-status" :class="order.status">{{getOrderStatusText(order.status)}}</view>
</view>
</view>
</view>
</view>
<view class="safe-area-bottom" style="height: 40rpx;"></view>
</scroll-view>
</view>
</template>
<script>
import { billingApi } from '@/api/index.js'
export default {
data() {
return {
billingInfo: {},
usageStats: null,
recentOrders: [],
isLoading: false
}
},
computed: {
billingStatusClass() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active' || status === 'paid_active') {
return 'status-normal'
} else if (status === 'trial_expired' || status === 'paid_expired') {
return 'status-expired'
}
return 'status-warning'
},
statusIcon() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active') return 'gift-filled'
if (status === 'paid_active') return 'checkmarkempty'
if (status === 'trial_expired' || status === 'paid_expired') return 'info-filled'
return 'info-filled'
},
statusTitle() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active') return '试用期中'
if (status === 'paid_active') return '付费使用中'
if (status === 'trial_expired') return '试用期已过期'
if (status === 'paid_expired') return '付费期已过期'
return '账户状态异常'
},
statusDesc() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active') {
return `试用期将于 ${this.formatDate(this.billingInfo.trialEndDate)} 结束`
}
if (status === 'paid_active') {
return `有效期至 ${this.formatDate(this.billingInfo.paidEndDate)}`
}
if (status === 'trial_expired' || status === 'paid_expired') {
return '您的账户已过期,请尽快续费以恢复使用'
}
return '请联系客服处理'
},
needRenewal() {
const status = this.billingInfo.billingStatus
return status === 'trial_expired' || status === 'paid_expired'
},
daysLeft() {
if (!this.billingInfo) return 0
const endDate = this.billingInfo.trialEndDate || this.billingInfo.paidEndDate
if (!endDate) return 0
const end = new Date(endDate)
const now = new Date()
const diff = Math.ceil((end - now) / (1000 * 60 * 60 * 24))
return Math.max(0, diff)
},
progressPercent() {
const total = 30
return Math.min(100, Math.max(0, (this.daysLeft / total) * 100))
},
progressColor() {
if (this.daysLeft > 7) return '#67C23A'
if (this.daysLeft > 3) return '#E6A23C'
return '#F56C6C'
},
planTagType() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active') return 'success'
if (status === 'paid_active') return 'primary'
if (status === 'trial_expired' || status === 'paid_expired') return 'danger'
return 'info'
},
billingInfoStatusText() {
const status = this.billingInfo.billingStatus
if (status === 'trial_active') return '试用期'
if (status === 'paid_active') return '付费期'
if (status === 'trial_expired') return '试用期已过期'
if (status === 'paid_expired') return '付费期已过期'
return '未知'
},
apartmentPercent() {
if (!this.usageStats) return 0
const { usage, limits } = this.usageStats
if (limits.apartments === 0) return 0
return Math.min(100, Math.round((usage.apartments / limits.apartments) * 100))
},
roomPercent() {
if (!this.usageStats) return 0
const { usage, limits } = this.usageStats
if (limits.rooms === 0) return 0
return Math.min(100, Math.round((usage.rooms / limits.rooms) * 100))
},
userPercent() {
if (!this.usageStats) return 0
const { usage, limits } = this.usageStats
if (limits.users === 0) return 0
return Math.min(100, Math.round((usage.users / limits.users) * 100))
}
},
onLoad() {
this.loadData()
},
onShow() {
this.loadData()
},
onPullDownRefresh() {
this.loadData().finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
async loadData() {
if (this.isLoading) return
this.isLoading = true
try {
await Promise.all([
this.loadBillingInfo(),
this.loadUsageStats(),
this.loadRecentOrders()
])
} catch (error) {
console.error('加载数据失败:', error)
} finally {
this.isLoading = false
}
},
async loadBillingInfo() {
try {
const res = await billingApi.getInfo()
if (res.code === 200) {
const { tenant, plan, usage } = res.data
this.billingInfo = {
...tenant,
plan,
usage
}
}
} catch (error) {
console.error('加载计费信息失败:', error)
}
},
async loadUsageStats() {
try {
const res = await billingApi.getUsage()
if (res.code === 200) {
this.usageStats = res.data
}
} catch (error) {
console.error('加载使用统计失败:', error)
}
},
async loadRecentOrders() {
try {
const res = await billingApi.getOrders({ page: 1, pageSize: 5 })
if (res.code === 200) {
this.recentOrders = res.data.list || []
}
} catch (error) {
console.error('加载订单失败:', error)
}
},
refreshData() {
this.loadData()
uni.showToast({ title: '已刷新', icon: 'success' })
},
async refreshUsage() {
await this.loadUsageStats()
uni.showToast({ title: '已刷新', icon: 'success' })
},
getProgressColor(percent) {
if (percent < 60) return '#67C23A'
if (percent < 80) return '#E6A23C'
return '#F56C6C'
},
getOrderStatusText(status) {
const map = {
pending: '待支付',
paid: '已支付',
cancelled: '已取消'
}
return map[status] || status
},
formatDate(date) {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
},
goBack() {
uni.navigateBack()
},
goToPlanSelect() {
uni.navigateTo({
url: '/pages/billing/plan-select'
})
},
goToOrderList() {
uni.navigateTo({
url: '/pages/billing/order-list'
})
},
goToPaymentRecord() {
uni.navigateTo({
url: '/pages/billing/payment-record'
})
},
viewOrderDetail(order) {
uni.navigateTo({
url: `/pages/billing/order-detail?id=${order.id}`
})
},
loadMore() {
// 加载更多订单
}
}
}
</script>
<style scoped>
.billing-center-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;
}
/* 状态卡片 */
.status-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
border-left: 8rpx solid transparent;
}
.status-card.status-normal {
border-left-color: #67C23A;
}
.status-card.status-expired {
border-left-color: #F56C6C;
}
.status-card.status-warning {
border-left-color: #E6A23C;
}
.status-header {
display: flex;
align-items: center;
gap: 24rpx;
}
.status-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-normal .status-icon {
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
}
.status-expired .status-icon {
background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%);
}
.status-warning .status-icon {
background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
}
.status-info {
flex: 1;
}
.status-title {
display: block;
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
margin-bottom: 8rpx;
}
.status-desc {
display: block;
font-size: 26rpx;
color: #64748B;
}
.status-progress {
margin-top: 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.progress-bar {
flex: 1;
height: 12rpx;
background: #F1F5F9;
border-radius: 6rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 6rpx;
transition: width 0.3s ease;
}
.days-left {
font-size: 24rpx;
color: #64748B;
white-space: nowrap;
}
.status-action {
margin-top: 24rpx;
}
.renew-btn {
width: 100%;
background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
/* 区块卡片 */
.section-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F1F5F9;
}
.header-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.header-title {
font-size: 30rpx;
font-weight: 700;
color: #1E293B;
}
.plan-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 500;
}
.plan-tag.success {
background: #D1FAE5;
color: #059669;
}
.plan-tag.primary {
background: #DBEAFE;
color: #2563EB;
}
.plan-tag.danger {
background: #FEE2E2;
color: #DC2626;
}
.plan-tag.info {
background: #F3F4F6;
color: #6B7280;
}
.refresh-text, .more-text {
font-size: 26rpx;
color: #409EFF;
}
/* 套餐内容 */
.plan-content {
text-align: center;
}
.plan-main {
margin-bottom: 16rpx;
}
.plan-name {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #1E293B;
margin-bottom: 12rpx;
}
.plan-price {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8rpx;
}
.plan-price .price {
font-size: 48rpx;
font-weight: 700;
color: #F56C6C;
}
.plan-price .unit {
font-size: 26rpx;
color: #94A3B8;
}
.plan-desc {
display: block;
font-size: 26rpx;
color: #64748B;
margin-bottom: 24rpx;
}
.plan-resources {
display: flex;
justify-content: center;
gap: 32rpx;
flex-wrap: wrap;
}
.resource-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 24rpx;
background: #F8FAFC;
border-radius: 12rpx;
font-size: 26rpx;
color: #64748B;
}
/* 资源使用 */
.usage-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.usage-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.usage-label {
width: 100rpx;
display: flex;
align-items: center;
gap: 8rpx;
font-size: 26rpx;
color: #64748B;
flex-shrink: 0;
}
.usage-bar {
flex: 1;
}
.progress-bg {
height: 12rpx;
background: #F1F5F9;
border-radius: 6rpx;
overflow: hidden;
}
.usage-value {
width: 100rpx;
text-align: right;
font-size: 24rpx;
color: #64748B;
flex-shrink: 0;
}
/* 快捷菜单 */
.quick-menu {
display: flex;
justify-content: space-around;
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.menu-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.menu-icon {
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.menu-text {
font-size: 26rpx;
color: #64748B;
}
/* 订单列表 */
.order-list {
display: flex;
flex-direction: column;
}
.order-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 2rpx solid #F1F5F9;
}
.order-item:last-child {
border-bottom: none;
}
.order-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.order-no {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
}
.order-plan {
font-size: 24rpx;
color: #64748B;
}
.order-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.order-amount {
font-size: 30rpx;
font-weight: 700;
color: #F56C6C;
}
.order-status {
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.order-status.pending {
background: #FEF3C7;
color: #D97706;
}
.order-status.paid {
background: #D1FAE5;
color: #059669;
}
.order-status.cancelled {
background: #F3F4F6;
color: #6B7280;
}
/* 空提示 */
.empty-tip {
text-align: center;
padding: 48rpx 0;
color: #94A3B8;
font-size: 28rpx;
}
</style>