Files
gym-manage/gym-manage-coach/pages/course/list.vue
T

378 lines
8.2 KiB
Vue

<template>
<view class="course-list-page">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
v-model="keyword"
type="text"
placeholder="搜索课程名称..."
@confirm="handleSearch"
/>
<text class="search-btn" @tap="handleSearch">搜索</text>
</view>
<!-- 状态筛选 -->
<scroll-view scroll-x class="filter-bar">
<view
v-for="tab in filterTabs"
:key="tab.key"
class="filter-tab"
:class="{ active: activeFilter === tab.key }"
@tap="changeFilter(tab.key)"
>
{{ tab.label }}
</view>
</scroll-view>
<!-- 时间图例 -->
<view class="time-legend">
<view class="legend-item">
<view class="legend-dot theory"></view>
<text class="legend-text">理论时间</text>
</view>
<view class="legend-item">
<view class="legend-dot actual"></view>
<text class="legend-text">实际时间</text>
</view>
</view>
<!-- 骨架屏 -->
<view v-if="loading">
<view v-for="i in 5" :key="'sk-'+i" class="sk-row skeleton-pulse" style="flex-direction:column;align-items:stretch;gap:10px">
<view class="flex-between">
<view class="skeleton sk-line" style="width:50%;height:18px"></view>
<view class="skeleton sk-line" style="width:48px;height:20px;border-radius:20px"></view>
</view>
<view class="skeleton sk-line sk-line-long" style="height:13px"></view>
<view class="skeleton sk-line sk-line-medium" style="height:12px"></view>
</view>
</view>
<!-- 真实列表 -->
<scroll-view v-else scroll-y class="course-scroll" @scrolltolower="loadMore">
<view v-if="filteredCourses.length === 0" class="empty-state">
<text>暂无相关团课</text>
</view>
<view v-else class="course-list">
<view
v-for="(course, index) in filteredCourses"
:key="course.id"
class="course-item list-item-enter"
:style="{ animationDelay: (index * 0.06) + 's' }"
@tap="goDetail(course.id)"
>
<view class="item-left">
<view class="item-title-row">
<text class="item-title">{{ course.courseName }}</text>
<view :class="['tag', 'tag-sm', getCourseStatusClass(course.status, course.deletedAt)]">
{{ getCourseStatusLabel(course.status, course.deletedAt) }}
</view>
</view>
<view class="item-info">
<text class="info-text">时间</text>
<text
:class="['info-value', hasActualTime(course) ? 'time-badge-actual' : 'time-badge-theory']"
>
{{ formatDate(course.actualStartTime || course.startTime) }}
{{ formatTimeShort(course.actualStartTime || course.startTime) }}
{{ formatTimeShort(course.actualEndTime || course.endTime) }}
</text>
</view>
<view class="item-info">
<text class="info-text">地点</text>
<text class="info-value">{{ course.location || '未设置' }}</text>
</view>
<view class="item-info">
<text class="info-text">人数</text>
<text class="info-value">{{ course.currentMembers || 0 }}/{{ course.maxMembers }}</text>
</view>
</view>
<view class="item-right">
<text class="arrow">&gt;</text>
</view>
</view>
</view>
<view class="safe-area-bottom" style="height:60px"></view>
</scroll-view>
</view>
</template>
<script>
import { useAuthStore } from '@/store/auth.js'
import { getCoachCourses } from '@/api/course.js'
import {
formatDate, formatTime, formatTimeShort,
getCourseStatusLabel, getCourseStatusClass,
hasActualTime
} from '@/utils/index.js'
export default {
data() {
return {
allCourses: [],
loading: true,
keyword: '',
activeFilter: 'all',
filterTabs: [
{ key: 'all', label: '全部' },
{ key: '0', label: '正常' },
{ key: '3', label: '进行中' },
{ key: '2', label: '已结束' },
{ key: '1', label: '已取消' },
{ key: '4', label: '超时' }
]
}
},
computed: {
filteredCourses() {
let list = this.allCourses
if (this.activeFilter !== 'all') {
list = list.filter(
c => (c.deletedAt == null || c.deletedAt === '') && Number(c.status) === Number(this.activeFilter)
)
}
if (this.keyword.trim()) {
const kw = this.keyword.trim().toLowerCase()
list = list.filter(
c =>
c.courseName?.toLowerCase().includes(kw) ||
String(c.id).includes(kw) ||
(c.location && c.location.toLowerCase().includes(kw))
)
}
return list
}
},
onShow() {
this.loadCourses()
},
methods: {
formatDate,
formatTimeStr: formatTime,
formatTimeShort,
getCourseStatusLabel,
getCourseStatusClass,
hasActualTime,
async loadCourses() {
const authStore = useAuthStore()
if (!authStore.state.isLoggedIn) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
this.loading = true
try {
const courses = await getCoachCourses()
this.allCourses = Array.isArray(courses) ? courses : []
} catch (e) {
console.error('加载团课失败:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
this.allCourses = []
} finally {
this.loading = false
}
},
handleSearch() {},
changeFilter(key) { this.activeFilter = key },
goDetail(id) { uni.navigateTo({ url: `/pages/course/detail?id=${id}` }) },
loadMore() {}
}
}
</script>
<style scoped>
.course-list-page {
min-height: 100vh;
background: #f6f8fc;
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #e9edf4;
}
.search-input {
flex: 1;
height: 38px;
background: #f1f5f9;
border-radius: 10px;
padding: 0 14px;
font-size: 14px;
border: 1px solid #e2e8f0;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #4f7cff;
}
.search-btn {
font-size: 14px;
color: #4f7cff;
font-weight: 500;
padding: 8px 4px;
}
.filter-bar {
white-space: nowrap;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #e9edf4;
}
.filter-tab {
display: inline-block;
padding: 6px 16px;
margin-right: 8px;
border-radius: 20px;
font-size: 13px;
color: #64748b;
background: #f1f5f9;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.filter-tab:active { transform: scale(0.95); }
.filter-tab.active {
background: #eef3ff;
color: #4f7cff;
font-weight: 500;
box-shadow: 0 2px 8px rgba(79, 124, 255, 0.1);
}
.course-scroll {
height: calc(100vh - 120px);
}
.course-list {
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.course-item {
background: #ffffff;
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.course-item:active {
background: #f8f9ff;
transform: scale(0.98);
}
.item-left { flex: 1; }
.item-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.item-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.tag-sm {
font-size: 11px;
padding: 2px 8px;
}
.item-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.info-text {
font-size: 12px;
color: #94a3b8;
width: 36px;
}
.info-value {
font-size: 13px;
color: #475569;
}
.item-right { margin-left: 8px; }
.arrow { font-size: 16px; color: #c0c8d4; }
/* ---- 时间样式 ---- */
.time-badge-theory {
background: #f1f5f9;
padding: 2px 8px;
border-radius: 6px;
}
.time-badge-actual {
background: #fef3c7;
padding: 2px 8px;
border-radius: 6px;
color: #92400e;
}
/* ---- 时间图例 ---- */
.time-legend {
display: flex;
justify-content: flex-end;
gap: 16px;
padding: 4px 16px 0;
background: #ffffff;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 3px;
}
.legend-dot.theory {
background: #f1f5f9;
border: 1px solid #d1d5db;
}
.legend-dot.actual {
background: #fef3c7;
border: 1px solid #f59e0b;
}
.legend-text {
font-size: 11px;
color: #94a3b8;
}
</style>