peixue-dev/peidu/uniapp/user-package/pages/user/my-package.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>