541 lines
13 KiB
Vue
541 lines
13 KiB
Vue
<template>
|
|
<view class="my-package-page">
|
|
<!-- 套餐统计 -->
|
|
<view class="stats-card">
|
|
<view class="stat-item">
|
|
<text class="stat-value">{{ stats.totalHours }}</text>
|
|
<text class="stat-label">总课时(小时)</text>
|
|
</view>
|
|
<view class="stat-divider"></view>
|
|
<view class="stat-item">
|
|
<text class="stat-value">{{ stats.usedHours }}</text>
|
|
<text class="stat-label">已使用</text>
|
|
</view>
|
|
<view class="stat-divider"></view>
|
|
<view class="stat-item">
|
|
<text class="stat-value highlight">{{ stats.remainHours }}</text>
|
|
<text class="stat-label">剩余</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 顶部Tab -->
|
|
<view class="tabs">
|
|
<view
|
|
class="tab-item"
|
|
:class="{ active: currentTab === index }"
|
|
v-for="(tab, index) in tabs"
|
|
:key="index"
|
|
@click="switchTab(index)"
|
|
>
|
|
{{ tab.name }}
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 套餐列表 -->
|
|
<scroll-view scroll-y class="package-list">
|
|
<view v-if="packageList.length > 0">
|
|
<view class="package-card" v-for="(item, index) in packageList" :key="index">
|
|
<!-- 套餐头部 -->
|
|
<view class="package-header">
|
|
<view class="package-type" :class="item.typeClass">{{ item.typeName }}</view>
|
|
<text class="package-status" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</text>
|
|
</view>
|
|
|
|
<!-- 套餐名称 -->
|
|
<view class="package-name">{{ item.name }}</view>
|
|
|
|
<!-- 套餐信息 -->
|
|
<view class="package-info">
|
|
<view class="info-item">
|
|
<text class="info-label">购买时间</text>
|
|
<text class="info-value">{{ item.purchaseDate }}</text>
|
|
</view>
|
|
<view class="info-item">
|
|
<text class="info-label">有效期至</text>
|
|
<text class="info-value">{{ item.expireDate }}</text>
|
|
</view>
|
|
<view class="info-item">
|
|
<text class="info-label">服务对象</text>
|
|
<text class="info-value">{{ item.studentName }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 课时进度 -->
|
|
<view class="progress-section">
|
|
<view class="progress-header">
|
|
<text class="progress-label">课时进度</text>
|
|
<text class="progress-text">{{ item.usedHours }}/{{ item.totalHours }}小时</text>
|
|
</view>
|
|
<view class="progress-bar">
|
|
<view class="progress-fill" :style="{ width: (item.usedHours / item.totalHours * 100) + '%' }"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 操作按钮 -->
|
|
<view class="package-actions">
|
|
<button class="btn-detail" @click="goDetail(item)">查看详情</button>
|
|
<button class="btn-book" v-if="item.status === 1" @click="goBook(item)">预约服务</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 空状态 -->
|
|
<view class="empty-state" v-else>
|
|
<text class="empty-icon">📦</text>
|
|
<text class="empty-text">暂无套餐</text>
|
|
<button class="btn-buy" @click="goBuy">去购买</button>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { packageApi } from '@/api/index.js'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
currentTab: 0,
|
|
tabs: [
|
|
{ name: '全部', status: null },
|
|
{ name: '待开始', status: 0 },
|
|
{ name: '进行中', status: 1 },
|
|
{ name: '已完成', status: 2 }
|
|
],
|
|
stats: {
|
|
totalHours: 0,
|
|
usedHours: 0,
|
|
remainHours: 0
|
|
},
|
|
packageList: [],
|
|
loading: false
|
|
}
|
|
},
|
|
|
|
onLoad() {
|
|
console.log('[我的套餐] 页面加载')
|
|
this.loadData()
|
|
},
|
|
|
|
onPullDownRefresh() {
|
|
this.loadData().then(() => {
|
|
uni.stopPullDownRefresh()
|
|
})
|
|
},
|
|
|
|
methods: {
|
|
switchTab(index) {
|
|
this.currentTab = index
|
|
this.loadPackages()
|
|
},
|
|
|
|
async loadData() {
|
|
await this.loadStats()
|
|
await this.loadPackages()
|
|
},
|
|
|
|
async loadStats() {
|
|
try {
|
|
console.log('[我的套餐] 开始加载统计数据')
|
|
const res = await packageApi.getMyPackages()
|
|
console.log('[我的套餐] 统计数据响应:', res)
|
|
|
|
if (res && res.code === 200 && res.data) {
|
|
const packages = Array.isArray(res.data) ? res.data : (res.data.records || [])
|
|
|
|
// 计算统计数据
|
|
let totalHours = 0
|
|
let usedHours = 0
|
|
|
|
packages.forEach(pkg => {
|
|
if (pkg.totalHours) {
|
|
totalHours += Number(pkg.totalHours) || 0
|
|
}
|
|
if (pkg.usedHours) {
|
|
usedHours += Number(pkg.usedHours) || 0
|
|
}
|
|
})
|
|
|
|
this.stats = {
|
|
totalHours: totalHours,
|
|
usedHours: usedHours,
|
|
remainHours: totalHours - usedHours
|
|
}
|
|
|
|
console.log('[我的套餐] 统计数据:', this.stats)
|
|
}
|
|
} catch (error) {
|
|
console.error('[我的套餐] 加载统计数据失败:', error)
|
|
// 使用默认值
|
|
this.stats = {
|
|
totalHours: 0,
|
|
usedHours: 0,
|
|
remainHours: 0
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadPackages() {
|
|
if (this.loading) return
|
|
this.loading = true
|
|
|
|
try {
|
|
console.log('[我的套餐] 开始加载套餐列表')
|
|
const status = this.tabs[this.currentTab].status
|
|
const params = {
|
|
page: 1,
|
|
size: 100
|
|
}
|
|
if (status !== null) {
|
|
params.status = status
|
|
}
|
|
|
|
console.log('[我的套餐] 请求参数:', params)
|
|
const res = await packageApi.getMyPackages()
|
|
console.log('[我的套餐] 套餐列表响应:', res)
|
|
|
|
if (res && res.code === 200 && res.data) {
|
|
let packages = Array.isArray(res.data) ? res.data : (res.data.records || [])
|
|
|
|
// 如果有状态筛选,进行过滤
|
|
if (status !== null) {
|
|
packages = packages.filter(p => p.status === status)
|
|
}
|
|
|
|
// 格式化数据
|
|
this.packageList = packages.map(pkg => ({
|
|
id: pkg.id,
|
|
name: pkg.packageName || '未命名套餐',
|
|
typeName: this.getPackageTypeName(pkg.packageType),
|
|
typeClass: this.getPackageTypeClass(pkg.packageType),
|
|
purchaseDate: this.formatDate(pkg.startDate || pkg.createTime),
|
|
expireDate: this.formatDate(pkg.expireDate),
|
|
studentName: pkg.studentName || '未指定',
|
|
totalHours: Number(pkg.totalHours) || 0,
|
|
usedHours: Number(pkg.usedHours) || 0,
|
|
status: pkg.status || 0
|
|
}))
|
|
|
|
console.log('[我的套餐] 格式化后的套餐列表:', this.packageList)
|
|
} else {
|
|
console.log('[我的套餐] 响应数据格式异常')
|
|
this.packageList = []
|
|
}
|
|
} catch (error) {
|
|
console.error('[我的套餐] 加载套餐列表失败:', error)
|
|
uni.showToast({
|
|
title: '加载失败',
|
|
icon: 'none'
|
|
})
|
|
this.packageList = []
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
getPackageTypeName(type) {
|
|
const typeMap = {
|
|
'basic': '基准式陪伴',
|
|
'special': '专项陪伴',
|
|
'warm': '暖心式陪伴',
|
|
'trial': '体验课',
|
|
'growth': '成长规划',
|
|
'assessment': '测评服务'
|
|
}
|
|
return typeMap[type] || '套餐'
|
|
},
|
|
|
|
getPackageTypeClass(type) {
|
|
const classMap = {
|
|
'basic': 'type-basic',
|
|
'special': 'type-special',
|
|
'warm': 'type-warm',
|
|
'trial': 'type-trial',
|
|
'growth': 'type-growth',
|
|
'assessment': 'type-assessment'
|
|
}
|
|
return classMap[type] || 'type-basic'
|
|
},
|
|
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-'
|
|
const date = new Date(dateStr)
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
},
|
|
|
|
getStatusClass(status) {
|
|
const map = { 0: 'pending', 1: 'ongoing', 2: 'completed' }
|
|
return map[status] || ''
|
|
},
|
|
|
|
getStatusText(status) {
|
|
const map = { 0: '待开始', 1: '进行中', 2: '已完成' }
|
|
return map[status] || ''
|
|
},
|
|
|
|
goDetail(item) {
|
|
uni.navigateTo({
|
|
url: `/user-package/pages/package/detail?id=${item.id}`
|
|
})
|
|
},
|
|
|
|
goBook(item) {
|
|
// 快速预约是 tabBar 页面,不支持传参
|
|
// 先保存参数到本地存储
|
|
uni.setStorageSync('bookingParams', { packageId: item.id })
|
|
uni.switchTab({
|
|
url: `/pages/booking/quick-booking`
|
|
})
|
|
},
|
|
|
|
goBuy() {
|
|
uni.switchTab({
|
|
url: '/pages/service/list'
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.my-package-page {
|
|
min-height: 100vh;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.stats-card {
|
|
background: linear-gradient(135deg, #5fc9ba 0%, #7dd9ca 100%);
|
|
margin: 20rpx;
|
|
border-radius: 16rpx;
|
|
padding: 40rpx 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.stat-item {
|
|
flex: 1;
|
|
text-align: center;
|
|
|
|
.stat-value {
|
|
display: block;
|
|
font-size: 48rpx;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
margin-bottom: 8rpx;
|
|
|
|
&.highlight {
|
|
color: #ffeb3b;
|
|
}
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 24rpx;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
}
|
|
}
|
|
|
|
.stat-divider {
|
|
width: 1rpx;
|
|
height: 60rpx;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
background: #fff;
|
|
padding: 0 20rpx;
|
|
|
|
.tab-item {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 24rpx 0;
|
|
font-size: 28rpx;
|
|
color: #666;
|
|
position: relative;
|
|
|
|
&.active {
|
|
color: #5fc9ba;
|
|
font-weight: 500;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 40rpx;
|
|
height: 4rpx;
|
|
background: #5fc9ba;
|
|
border-radius: 2rpx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.package-list {
|
|
height: calc(100vh - 280rpx);
|
|
padding: 20rpx;
|
|
}
|
|
|
|
.package-card {
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
padding: 24rpx;
|
|
margin-bottom: 20rpx;
|
|
|
|
.package-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16rpx;
|
|
|
|
.package-type {
|
|
font-size: 22rpx;
|
|
padding: 4rpx 16rpx;
|
|
border-radius: 20rpx;
|
|
|
|
&.type-basic { background: #e8f5e9; color: #4caf50; }
|
|
&.type-special { background: #e3f2fd; color: #2196f3; }
|
|
&.type-warm { background: #fff3e0; color: #ff9800; }
|
|
&.type-trial { background: #fce4ec; color: #e91e63; }
|
|
}
|
|
|
|
.package-status {
|
|
font-size: 24rpx;
|
|
|
|
&.pending { color: #ff9800; }
|
|
&.ongoing { color: #4caf50; }
|
|
&.completed { color: #999; }
|
|
}
|
|
}
|
|
|
|
.package-name {
|
|
font-size: 32rpx;
|
|
font-weight: 500;
|
|
color: #333;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.package-info {
|
|
background: #f9f9f9;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
margin-bottom: 20rpx;
|
|
|
|
.info-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8rpx;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.info-label {
|
|
font-size: 26rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 26rpx;
|
|
color: #333;
|
|
}
|
|
}
|
|
}
|
|
|
|
.progress-section {
|
|
margin-bottom: 20rpx;
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 12rpx;
|
|
|
|
.progress-label {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.progress-text {
|
|
font-size: 26rpx;
|
|
color: #5fc9ba;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 12rpx;
|
|
background: #e0e0e0;
|
|
border-radius: 6rpx;
|
|
overflow: hidden;
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #5fc9ba, #7dd9ca);
|
|
border-radius: 6rpx;
|
|
transition: width 0.3s;
|
|
}
|
|
}
|
|
}
|
|
|
|
.package-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 20rpx;
|
|
padding-top: 16rpx;
|
|
border-top: 1rpx solid #f0f0f0;
|
|
|
|
button {
|
|
height: 64rpx;
|
|
line-height: 64rpx;
|
|
padding: 0 32rpx;
|
|
font-size: 26rpx;
|
|
border-radius: 32rpx;
|
|
margin: 0;
|
|
}
|
|
|
|
.btn-detail {
|
|
background: #fff;
|
|
color: #666;
|
|
border: 1rpx solid #ddd;
|
|
}
|
|
|
|
.btn-book {
|
|
background: #5fc9ba;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 100rpx 0;
|
|
|
|
.empty-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.btn-buy {
|
|
background: #5fc9ba;
|
|
color: #fff;
|
|
height: 72rpx;
|
|
line-height: 72rpx;
|
|
padding: 0 60rpx;
|
|
font-size: 28rpx;
|
|
border-radius: 36rpx;
|
|
}
|
|
}
|
|
</style>
|