feat: migrate ice venue module
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<view class="courses-container">
|
||||
<!-- 极简原生筛选栏 -->
|
||||
<view class="native-filter-bar">
|
||||
<!-- 微信原生日期选择器 -->
|
||||
<picker
|
||||
mode="date"
|
||||
:value="selectedDate"
|
||||
@change="onDateChange"
|
||||
class="filter-picker-item"
|
||||
>
|
||||
<view class="picker-inner">
|
||||
<text class="picker-label">日期:</text>
|
||||
<text class="picker-value">{{ selectedDate ? formatDateDisplay(selectedDate) : '选择日期' }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view class="vertical-divider"></view>
|
||||
|
||||
<!-- 微信原生教练单列选择器 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="coachOptions"
|
||||
range-key="name"
|
||||
:value="selectedCoachIndex"
|
||||
@change="onCoachChange"
|
||||
class="filter-picker-item"
|
||||
>
|
||||
<view class="picker-inner">
|
||||
<text class="picker-label">教练:</text>
|
||||
<text class="picker-value">{{ selectedCoachName }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 课程列表(双栏高质感极简卡片) -->
|
||||
<view class="course-list">
|
||||
<StatePanel
|
||||
v-if="listState === 'error'"
|
||||
title="课程加载失败"
|
||||
:description="listErrorMessage"
|
||||
primary-text="重试"
|
||||
type="error"
|
||||
compact
|
||||
@primary="loadCourses"
|
||||
/>
|
||||
|
||||
<block v-else>
|
||||
<view
|
||||
v-for="course in courses"
|
||||
:key="course.id"
|
||||
class="course-card"
|
||||
@tap="onViewDetail(course.id)"
|
||||
>
|
||||
<image :src="course.cover" mode="aspectFill" class="course-cover" />
|
||||
|
||||
<view class="course-info">
|
||||
<view class="card-top-row">
|
||||
<text class="course-title">{{ course.title }}</text>
|
||||
<view class="badge success" v-if="course.is_booked">已预约</view>
|
||||
</view>
|
||||
|
||||
<view class="course-details">
|
||||
<text class="detail-item-text highlight-blue">时间:{{ formatTimeRange(course.start_time, course.end_time) }}</text>
|
||||
<view class="detail-bottom-meta">
|
||||
<text class="detail-item-text"><text class="label-dot">•</text>教练:{{ course.coach.name }}</text>
|
||||
<text class="detail-item-text"><text class="label-dot">•</text>人数:已约 {{ course.booked_count }}/{{ course.capacity }}人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="isLoadingMore" class="load-more-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="courses.length > 0 && !hasMore" class="load-more-tip">
|
||||
<text>— 已加载全部 —</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<StatePanel
|
||||
v-if="listState !== 'error' && !isLoading && courses.length === 0"
|
||||
title="暂无排课行程"
|
||||
description="当前筛选条件下暂时没有可查看的课程。"
|
||||
type="empty"
|
||||
compact
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { courseApi } from '@/modules/ice/api/course'
|
||||
import { errorMessage } from '@/modules/ice/api/session'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
|
||||
const selectedDate = ref('')
|
||||
const selectedCoachIndex = ref(0) // 索引 0 默认是 "全部教练"
|
||||
const isLoading = ref(false)
|
||||
const isLoadingMore = ref(false)
|
||||
const listState = ref('idle')
|
||||
const listErrorMessage = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const total = ref(0)
|
||||
|
||||
const hasMore = computed(() => courses.value.length < total.value)
|
||||
|
||||
const coaches = ref([])
|
||||
|
||||
// 原生 Picker 需要的可选数组
|
||||
const coachOptions = computed(() => {
|
||||
return [{ id: null, name: '全部教练' }, ...coaches.value]
|
||||
})
|
||||
|
||||
const selectedCoachId = computed(() => {
|
||||
return coachOptions.value[selectedCoachIndex.value]?.id || null
|
||||
})
|
||||
|
||||
const selectedCoachName = computed(() => {
|
||||
return coachOptions.value[selectedCoachIndex.value]?.name || '全部教练'
|
||||
})
|
||||
|
||||
const courses = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
const today = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
selectedDate.value = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`
|
||||
loadInitialData()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
page.value = 1
|
||||
loadCourses().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (hasMore.value && !isLoadingMore.value) {
|
||||
page.value++
|
||||
loadMoreCourses()
|
||||
}
|
||||
})
|
||||
|
||||
const loadInitialData = async () => {
|
||||
await Promise.all([loadCoaches(), loadCourses()])
|
||||
}
|
||||
|
||||
const loadCoaches = async () => {
|
||||
try {
|
||||
coaches.value = await courseApi.getCoaches()
|
||||
} catch (err) {
|
||||
coaches.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const loadCourses = async () => {
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await courseApi.getCourses({
|
||||
date: selectedDate.value,
|
||||
coachId: selectedCoachId.value,
|
||||
page: 1,
|
||||
pageSize
|
||||
})
|
||||
courses.value = data.items
|
||||
total.value = data.total
|
||||
listState.value = 'ready'
|
||||
listErrorMessage.value = ''
|
||||
} catch (err) {
|
||||
courses.value = []
|
||||
total.value = 0
|
||||
listState.value = 'error'
|
||||
listErrorMessage.value = errorMessage(err, '课程加载失败,请稍后重试。')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadMoreCourses = async () => {
|
||||
if (isLoadingMore.value) return
|
||||
isLoadingMore.value = true
|
||||
try {
|
||||
const data = await courseApi.getCourses({
|
||||
date: selectedDate.value,
|
||||
coachId: selectedCoachId.value,
|
||||
page: page.value,
|
||||
pageSize
|
||||
})
|
||||
courses.value = [...courses.value, ...data.items]
|
||||
total.value = data.total
|
||||
} catch (err) {
|
||||
page.value--
|
||||
uni.showToast({ title: errorMessage(err, '加载更多失败,请重试'), icon: 'none' })
|
||||
} finally {
|
||||
isLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onDateChange = (e) => {
|
||||
selectedDate.value = e.detail.value
|
||||
page.value = 1
|
||||
loadCourses()
|
||||
}
|
||||
|
||||
const onCoachChange = (e) => {
|
||||
selectedCoachIndex.value = Number(e.detail.value)
|
||||
page.value = 1
|
||||
loadCourses()
|
||||
}
|
||||
|
||||
const onViewDetail = (id) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ice/courses/detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
const formatDateDisplay = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const parts = dateStr.split('-')
|
||||
return `${parts[1]}月${parts[2]}日`
|
||||
}
|
||||
|
||||
const formatTimeRange = (startStr, endStr) => {
|
||||
const start = new Date(startStr)
|
||||
const end = new Date(endStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const mm = pad(start.getMonth() + 1)
|
||||
const dd = pad(start.getDate())
|
||||
const startHhMm = `${pad(start.getHours())}:${pad(start.getMinutes())}`
|
||||
const endHhMm = `${pad(end.getHours())}:${pad(end.getMinutes())}`
|
||||
return `${mm}-${dd} ${startHhMm} - ${endHhMm}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.courses-container {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-color-soft;
|
||||
padding-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
/* 极简原生筛选栏 */
|
||||
.native-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $bg-color-main;
|
||||
border-bottom: 1rpx solid $bg-color-hover;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: 90rpx;
|
||||
}
|
||||
|
||||
.filter-picker-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.picker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 90rpx;
|
||||
font-size: 26rpx;
|
||||
color: $text-color-regular;
|
||||
|
||||
.picker-label {
|
||||
color: $text-color-muted;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: $text-color-main;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 16rpx;
|
||||
color: $text-color-light;
|
||||
margin-left: 8rpx;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-divider {
|
||||
width: 1rpx;
|
||||
height: 40rpx;
|
||||
background-color: $bg-color-hover;
|
||||
}
|
||||
|
||||
/* 课程卡片 */
|
||||
.course-list {
|
||||
padding: $spacing-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
display: flex;
|
||||
background-color: $bg-color-main;
|
||||
border: 1rpx solid rgba(226, 232, 240, 0.8);
|
||||
border-radius: $border-radius-lg;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background-color: $bg-color-hover;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 180rpx;
|
||||
height: 150rpx;
|
||||
background-color: $bg-color-hover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-color-main;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.detail-item-text {
|
||||
font-size: 22rpx;
|
||||
color: $text-color-muted;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.highlight-blue {
|
||||
color: $color-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.label-dot {
|
||||
font-size: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-bottom-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xl;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 160rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: $spacing-md;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $text-color-main;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 22rpx;
|
||||
color: $text-color-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-tip {
|
||||
text-align: center;
|
||||
padding: $spacing-md 0;
|
||||
font-size: 22rpx;
|
||||
color: $text-color-light;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user