新增后台管理系统(答辩用
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
<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">></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>
|
||||
Reference in New Issue
Block a user