16 changed files with 1687 additions and 405 deletions
@ -0,0 +1,216 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\api\controller\apiController; |
|||
|
|||
use app\Request; |
|||
use app\service\api\apiService\CourseScheduleService; |
|||
use core\base\BaseApiService; |
|||
|
|||
/** |
|||
* 课程安排相关接口 |
|||
* Class CourseSchedule |
|||
* @package app\api\controller\apiController |
|||
*/ |
|||
class CourseSchedule extends BaseApiService |
|||
{ |
|||
/** |
|||
* 获取课程安排列表 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getScheduleList(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
return success((new CourseScheduleService())->getScheduleList($data)); |
|||
} |
|||
|
|||
/** |
|||
* 获取课程安排详情 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getScheduleInfo(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["id", 0] |
|||
]); |
|||
$result = (new CourseScheduleService())->getScheduleInfo($data['id']); |
|||
if (isset($result['code']) && $result['code'] === 0) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success('SUCCESS', $result); |
|||
} |
|||
|
|||
/** |
|||
* 创建课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function createSchedule(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
$result = (new CourseScheduleService())->createSchedule($data); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '创建成功', $result['data'] ?? []); |
|||
} |
|||
|
|||
/** |
|||
* 批量创建课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function batchCreateSchedule(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
$result = (new CourseScheduleService())->batchCreateSchedule($data); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '批量创建成功', $result['data'] ?? []); |
|||
} |
|||
|
|||
/** |
|||
* 更新课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function updateSchedule(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
$result = (new CourseScheduleService())->updateSchedule($data); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '更新成功', $result['data'] ?? []); |
|||
} |
|||
|
|||
/** |
|||
* 删除课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function deleteSchedule(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["id", 0] |
|||
]); |
|||
$result = (new CourseScheduleService())->deleteSchedule($data['id']); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '删除成功'); |
|||
} |
|||
|
|||
/** |
|||
* 获取场地列表 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getVenueList(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
return success((new CourseScheduleService())->getVenueList($data)); |
|||
} |
|||
|
|||
/** |
|||
* 获取场地可用时间 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getVenueAvailableTime(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["venue_id", 0], |
|||
["date", ""] |
|||
]); |
|||
return success((new CourseScheduleService())->getVenueAvailableTime($data)); |
|||
} |
|||
|
|||
/** |
|||
* 检查教练时间冲突 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function checkCoachConflict(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["coach_id", 0], |
|||
["date", ""], |
|||
["time_slot", ""], |
|||
["schedule_id", 0] // 排除当前正在编辑的课程安排 |
|||
]); |
|||
return success((new CourseScheduleService())->checkCoachConflict($data)); |
|||
} |
|||
|
|||
/** |
|||
* 获取课程安排统计 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getScheduleStatistics(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
return success((new CourseScheduleService())->getScheduleStatistics($data)); |
|||
} |
|||
|
|||
/** |
|||
* 学员加入课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function joinSchedule(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["schedule_id", 0], |
|||
["student_id", 0], |
|||
["course_type", 0], // 0-正常, 1-加课, 2-补课, 3-等待位 |
|||
["resources_id", 0] |
|||
]); |
|||
$result = (new CourseScheduleService())->joinSchedule($data); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '添加成功', $result['data'] ?? []); |
|||
} |
|||
|
|||
/** |
|||
* 学员退出课程安排 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function leaveSchedule(Request $request) |
|||
{ |
|||
$data = $this->request->params([ |
|||
["schedule_id", 0], |
|||
["student_id", 0], |
|||
["remark", ""] |
|||
]); |
|||
$result = (new CourseScheduleService())->leaveSchedule($data); |
|||
if (!$result['code']) { |
|||
return fail($result['msg']); |
|||
} |
|||
return success($result['msg'] ?? '操作成功'); |
|||
} |
|||
|
|||
/** |
|||
* 获取筛选选项 |
|||
* @param Request $request |
|||
* @return \think\Response |
|||
*/ |
|||
public function getFilterOptions(Request $request) |
|||
{ |
|||
$data = $request->all(); |
|||
return success((new CourseScheduleService())->getFilterOptions($data)); |
|||
} |
|||
} |
|||
@ -0,0 +1,517 @@ |
|||
<template> |
|||
<view class="schedule-detail" v-if="visible"> |
|||
<!-- <view class="popup-wrapper">--> |
|||
<!-- <view class="popup-header">--> |
|||
<!-- <text class="popup-title">课次详情</text>--> |
|||
<!-- <view class="close-btn" @click="closePopup">--> |
|||
<!-- <text class="close-icon">×</text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="popup-content" v-if="loading">--> |
|||
<!-- <view class="loading">--> |
|||
<!-- <fui-loading></fui-loading>--> |
|||
<!-- <text class="loading-text">加载中...</text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="popup-content" v-else-if="error">--> |
|||
<!-- <view class="error-message">--> |
|||
<!-- <text>{{ errorMessage }}</text>--> |
|||
<!-- <view class="retry-btn" @click="fetchScheduleDetail">--> |
|||
<!-- <text>重试</text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="popup-content" v-else>--> |
|||
<!-- <view class="course-title">--> |
|||
<!-- <text>{{ scheduleDetail.title || '暂无课程名称' }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="course-time">--> |
|||
<!-- <text>{{ scheduleDetail.course_date || '' }} {{ scheduleDetail.time_slot || '' }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="schedule-info">--> |
|||
<!-- <view class="info-item">--> |
|||
<!-- <text class="info-label">授课教师:</text>--> |
|||
<!-- <text class="info-value">{{ scheduleDetail.coach?.name || '未设置' }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="info-item">--> |
|||
<!-- <text class="info-label">教室:</text>--> |
|||
<!-- <text class="info-value">{{ scheduleDetail.venue?.venue_name || '未设置' }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="info-item">--> |
|||
<!-- <text class="info-label">当前人数:</text>--> |
|||
<!-- <text class="info-value">{{ studentCount }}/{{ scheduleDetail.venue?.capacity || 0 }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="info-item">--> |
|||
<!-- <text class="info-label">课程内容:</text>--> |
|||
<!-- <text class="info-value">{{ scheduleDetail.content || '未设置上课内容' }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="info-item" v-if="scheduleDetail.remark">--> |
|||
<!-- <text class="info-label">备注:</text>--> |
|||
<!-- <text class="info-value">{{ scheduleDetail.remark }}</text>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <!– 学员列表 –>--> |
|||
<!-- <view class="student-list-section">--> |
|||
<!-- <view class="section-title">--> |
|||
<!-- <text>学员列表</text>--> |
|||
<!-- <text class="status-tag" :class="statusClass">{{ statusText }}</text>--> |
|||
<!-- </view>--> |
|||
<!-- <view class="student-list" v-if="scheduleDetail.student_courses && scheduleDetail.student_courses.length > 0">--> |
|||
<!-- <view class="student-item" v-for="(student, index) in scheduleDetail.student_courses" :key="index">--> |
|||
<!-- <view class="student-avatar">--> |
|||
<!-- <image :src="$util.img(student.avatar)" mode="aspectFill"></image>--> |
|||
<!-- </view>--> |
|||
<!-- <view class="student-info">--> |
|||
<!-- <text class="student-name">{{ student.name }}</text>--> |
|||
<!-- <text class="student-status" :class="{'signed': student.status === 'signed'}">--> |
|||
<!-- {{ student.status === 'signed' ? '已签到' : '未签到' }}--> |
|||
<!-- </text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- <view class="empty-list" v-else>--> |
|||
<!-- <text>暂无学员参与此课程</text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- <view class="popup-footer">--> |
|||
<!-- <view class="action-btn adjust-btn" @click="handleAdjustClass">--> |
|||
<!-- <text>调课</text>--> |
|||
<!-- </view>--> |
|||
<!-- <view class="action-btn sign-btn" @click="handleSignIn">--> |
|||
<!-- <text>点名</text>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/api/apiRoute.js'; |
|||
|
|||
export default { |
|||
name: 'ScheduleDetail', |
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
scheduleId: { |
|||
type: [String, Number], |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
loading: false, |
|||
error: false, |
|||
errorMessage: '加载失败,请重试', |
|||
scheduleDetail: {}, |
|||
studentCount: 0 |
|||
} |
|||
}, |
|||
computed: { |
|||
// 课程状态文本和样式 |
|||
statusText() { |
|||
if (!this.scheduleDetail.student_courses || !this.scheduleDetail.student_courses[0]) { |
|||
return '未开始'; |
|||
} |
|||
|
|||
const now = new Date(); |
|||
const startDate = this.scheduleDetail.student_courses[0].start_date ? |
|||
new Date(this.scheduleDetail.student_courses[0].start_date) : null; |
|||
const endDate = this.scheduleDetail.student_courses[0].end_date ? |
|||
new Date(this.scheduleDetail.student_courses[0].end_date) : null; |
|||
|
|||
if (startDate && endDate) { |
|||
if (now >= startDate && now <= endDate) { |
|||
return '上课中'; |
|||
} else if (now > endDate) { |
|||
return '已结束'; |
|||
} else if (now < startDate) { |
|||
return '未开始'; |
|||
} |
|||
} |
|||
|
|||
return '未开始'; |
|||
}, |
|||
statusClass() { |
|||
switch (this.statusText) { |
|||
case '上课中': |
|||
return 'status-in-progress'; |
|||
case '已结束': |
|||
return 'status-ended'; |
|||
case '未开始': |
|||
default: |
|||
return 'status-not-started'; |
|||
} |
|||
} |
|||
}, |
|||
watch: { |
|||
// 监听课程ID变化,重新获取数据 |
|||
scheduleId: { |
|||
immediate: true, |
|||
handler(newVal) { |
|||
if (newVal && this.visible) { |
|||
this.fetchScheduleDetail(); |
|||
} |
|||
} |
|||
}, |
|||
// 监听弹窗可见性变化 |
|||
visible(newVal) { |
|||
if (newVal && this.scheduleId) { |
|||
this.fetchScheduleDetail(); |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
// 获取课程详情 |
|||
async fetchScheduleDetail() { |
|||
if (!this.scheduleId) { |
|||
this.error = true; |
|||
this.errorMessage = '课程ID不能为空'; |
|||
return; |
|||
} |
|||
|
|||
this.loading = true; |
|||
this.error = false; |
|||
|
|||
try { |
|||
// 使用新接口获取课程安排详情 |
|||
const res = await apiRoute.getCourseScheduleInfo({ |
|||
id: this.scheduleId |
|||
}); |
|||
|
|||
if (res.code === 1 && res.data) { |
|||
this.scheduleDetail = res.data; |
|||
// 计算学生数量 |
|||
this.studentCount = this.scheduleDetail.student_courses ? |
|||
this.scheduleDetail.student_courses.length : 0; |
|||
} else { |
|||
// 如果新接口不可用,尝试使用旧接口 |
|||
const fallbackRes = await apiRoute.courseInfo({ |
|||
id: this.scheduleId |
|||
}); |
|||
|
|||
if (fallbackRes.code === 1 && fallbackRes.data) { |
|||
this.scheduleDetail = fallbackRes.data; |
|||
this.studentCount = this.scheduleDetail.student_courses ? |
|||
this.scheduleDetail.student_courses.length : 0; |
|||
} else { |
|||
throw new Error(res.msg || fallbackRes.msg || '获取课程详情失败'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('获取课程详情失败:', error); |
|||
this.error = true; |
|||
this.errorMessage = error.message || '获取课程详情失败,请重试'; |
|||
} finally { |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
|
|||
// 关闭弹窗 |
|||
closePopup() { |
|||
this.$emit('update:visible', false); |
|||
}, |
|||
|
|||
// 点名功能 |
|||
handleSignIn() { |
|||
// 如果课程已结束,显示提示 |
|||
if (this.statusText === '已结束') { |
|||
uni.showToast({ |
|||
title: '课程已结束,无法点名', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 如果没有学生,显示提示 |
|||
if (!this.scheduleDetail.student_courses || this.scheduleDetail.student_courses.length === 0) { |
|||
uni.showToast({ |
|||
title: '暂无学员,无法点名', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 触发点名事件,由父组件处理 |
|||
this.$emit('sign-in', { |
|||
scheduleId: this.scheduleId, |
|||
scheduleDetail: this.scheduleDetail |
|||
}); |
|||
}, |
|||
|
|||
// 调课功能 |
|||
handleAdjustClass() { |
|||
// 如果课程已结束,显示提示 |
|||
if (this.statusText === '已结束') { |
|||
uni.showToast({ |
|||
title: '课程已结束,无法调课', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 触发调课事件,由父组件处理 |
|||
this.$emit('adjust-class', { |
|||
scheduleId: this.scheduleId, |
|||
scheduleDetail: this.scheduleDetail |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.schedule-detail { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
z-index: 999; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.popup-wrapper { |
|||
width: 90%; |
|||
max-height: 80vh; |
|||
background-color: #434544; |
|||
border-radius: 16rpx; |
|||
overflow: hidden; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.popup-header { |
|||
padding: 30rpx; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
border-bottom: 1px solid #555; |
|||
} |
|||
|
|||
.popup-title { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
} |
|||
|
|||
.close-btn { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.close-icon { |
|||
font-size: 40rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.popup-content { |
|||
flex: 1; |
|||
padding: 30rpx; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.loading, .error-message { |
|||
height: 300rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.loading-text { |
|||
margin-top: 20rpx; |
|||
font-size: 28rpx; |
|||
color: #ccc; |
|||
} |
|||
|
|||
.error-message { |
|||
color: #ff6b6b; |
|||
font-size: 28rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
.retry-btn { |
|||
margin-top: 30rpx; |
|||
padding: 12rpx 30rpx; |
|||
background-color: #29d3b4; |
|||
border-radius: 8rpx; |
|||
color: #fff; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.course-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.course-time { |
|||
font-size: 28rpx; |
|||
color: #FAD24E; |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.schedule-info { |
|||
background-color: #333; |
|||
border-radius: 12rpx; |
|||
padding: 24rpx; |
|||
} |
|||
|
|||
.info-item { |
|||
display: flex; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.info-label { |
|||
width: 160rpx; |
|||
font-size: 26rpx; |
|||
color: #ccc; |
|||
} |
|||
|
|||
.info-value { |
|||
flex: 1; |
|||
font-size: 26rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.student-list-section { |
|||
margin-top: 30rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding-bottom: 16rpx; |
|||
border-bottom: 1px solid #555; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title text { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.status-tag { |
|||
font-size: 24rpx; |
|||
padding: 4rpx 16rpx; |
|||
border-radius: 30rpx; |
|||
} |
|||
|
|||
.status-in-progress { |
|||
background-color: #FAD24E; |
|||
color: #333; |
|||
} |
|||
|
|||
.status-ended { |
|||
background-color: #e2e2e2; |
|||
color: #333; |
|||
} |
|||
|
|||
.status-not-started { |
|||
background-color: #1cd188; |
|||
color: #fff; |
|||
} |
|||
|
|||
.student-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.student-item { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.student-avatar { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
background-color: #555; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.student-avatar image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.student-info { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.student-name { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
margin-bottom: 6rpx; |
|||
} |
|||
|
|||
.student-status { |
|||
font-size: 24rpx; |
|||
color: #ff6b6b; |
|||
} |
|||
|
|||
.student-status.signed { |
|||
color: #1cd188; |
|||
} |
|||
|
|||
.empty-list { |
|||
padding: 40rpx 0; |
|||
text-align: center; |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.popup-footer { |
|||
display: flex; |
|||
padding: 30rpx; |
|||
gap: 20rpx; |
|||
border-top: 1px solid #555; |
|||
} |
|||
|
|||
.action-btn { |
|||
flex: 1; |
|||
height: 80rpx; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.adjust-btn { |
|||
background-color: #555; |
|||
color: #fff; |
|||
} |
|||
|
|||
.sign-btn { |
|||
background-color: #29d3b4; |
|||
color: #fff; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,481 @@ |
|||
<template> |
|||
<view class="schedule-detail-container"> |
|||
<!-- 页面加载状态 --> |
|||
<view class="loading-container" v-if="loading"> |
|||
<fui-loading></fui-loading> |
|||
<text class="loading-text">加载中...</text> |
|||
</view> |
|||
|
|||
<!-- 错误状态显示 --> |
|||
<view class="error-container" v-else-if="error"> |
|||
<text class="error-text">{{ errorMessage }}</text> |
|||
<view class="retry-btn" @click="fetchScheduleDetail"> |
|||
<text>重试</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="schedule-content" v-else> |
|||
<!-- 课程基本信息 --> |
|||
<view class="schedule-header"> |
|||
<view class="course-title"> |
|||
<text>{{ scheduleDetail.title || '暂无课程名称' }}</text> |
|||
<text class="status-tag" :class="statusClass">{{ statusText }}</text> |
|||
</view> |
|||
<view class="course-time">{{ scheduleDetail.course_date || '' }} {{ scheduleDetail.time_slot || '' }}</view> |
|||
</view> |
|||
|
|||
<!-- 课程详细信息 --> |
|||
<view class="info-card"> |
|||
<view class="info-item"> |
|||
<text class="info-label">授课教师:</text> |
|||
<text class="info-value">{{ scheduleDetail.coach?.name || '未设置' }}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">教室:</text> |
|||
<text class="info-value">{{ scheduleDetail.venue?.venue_name || '未设置' }}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">当前人数:</text> |
|||
<text class="info-value">{{ studentCount }}/{{ scheduleDetail.venue?.capacity || 0 }}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">课程内容:</text> |
|||
<text class="info-value">{{ scheduleDetail.content || '未设置上课内容' }}</text> |
|||
</view> |
|||
<view class="info-item" v-if="scheduleDetail.remark"> |
|||
<text class="info-label">备注:</text> |
|||
<text class="info-value">{{ scheduleDetail.remark }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 学员列表 --> |
|||
<view class="student-list-section"> |
|||
<view class="section-title"> |
|||
<text>学员列表</text> |
|||
</view> |
|||
<view class="student-list" v-if="scheduleDetail.student_courses && scheduleDetail.student_courses.length > 0"> |
|||
<view class="student-item" v-for="(student, index) in scheduleDetail.student_courses" :key="index"> |
|||
<view class="student-avatar"> |
|||
<image :src="$util.img(student.avatar)" mode="aspectFill"></image> |
|||
</view> |
|||
<view class="student-info"> |
|||
<text class="student-name">{{ student.name }}</text> |
|||
<text class="student-status" :class="{'signed': student.status === 'signed'}"> |
|||
{{ student.status === 'signed' ? '已签到' : '未签到' }} |
|||
</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="empty-list" v-else> |
|||
<text>暂无学员参与此课程</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部按钮区域 --> |
|||
<view class="footer-actions"> |
|||
<view class="action-btn adjust-btn" @click="handleAdjustClass"> |
|||
<text>调课</text> |
|||
</view> |
|||
<view class="action-btn sign-btn" @click="handleSignIn"> |
|||
<text>点名</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 引入课程详情组件 --> |
|||
<schedule-detail |
|||
:visible="showDetailPopup" |
|||
:scheduleId="scheduleId" |
|||
@update:visible="showDetailPopup = $event" |
|||
@sign-in="onSignIn" |
|||
@adjust-class="onAdjustClass" |
|||
></schedule-detail> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/api/apiRoute.js' |
|||
import ScheduleDetail from '@/components/schedule/ScheduleDetail.vue' |
|||
|
|||
export default { |
|||
components: { |
|||
ScheduleDetail |
|||
}, |
|||
data() { |
|||
return { |
|||
scheduleId: '', // 课程安排ID |
|||
loading: false, |
|||
error: false, |
|||
errorMessage: '加载失败,请重试', |
|||
scheduleDetail: {}, |
|||
studentCount: 0, |
|||
showDetailPopup: false |
|||
} |
|||
}, |
|||
computed: { |
|||
// 课程状态文本和样式 |
|||
statusText() { |
|||
if (!this.scheduleDetail.student_courses || !this.scheduleDetail.student_courses[0]) { |
|||
return '未开始'; |
|||
} |
|||
|
|||
const now = new Date(); |
|||
const startDate = this.scheduleDetail.student_courses[0].start_date ? |
|||
new Date(this.scheduleDetail.student_courses[0].start_date) : null; |
|||
const endDate = this.scheduleDetail.student_courses[0].end_date ? |
|||
new Date(this.scheduleDetail.student_courses[0].end_date) : null; |
|||
|
|||
if (startDate && endDate) { |
|||
if (now >= startDate && now <= endDate) { |
|||
return '上课中'; |
|||
} else if (now > endDate) { |
|||
return '已结束'; |
|||
} else if (now < startDate) { |
|||
return '未开始'; |
|||
} |
|||
} |
|||
|
|||
return '未开始'; |
|||
}, |
|||
statusClass() { |
|||
switch (this.statusText) { |
|||
case '上课中': |
|||
return 'status-in-progress'; |
|||
case '已结束': |
|||
return 'status-ended'; |
|||
case '未开始': |
|||
default: |
|||
return 'status-not-started'; |
|||
} |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
if (options.id) { |
|||
this.scheduleId = options.id; |
|||
this.fetchScheduleDetail(); |
|||
} else { |
|||
this.error = true; |
|||
this.errorMessage = '未找到课程安排ID'; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 获取课程详情 |
|||
async fetchScheduleDetail() { |
|||
if (!this.scheduleId) { |
|||
this.error = true; |
|||
this.errorMessage = '课程ID不能为空'; |
|||
return; |
|||
} |
|||
|
|||
this.loading = true; |
|||
this.error = false; |
|||
|
|||
try { |
|||
// 使用新接口获取课程安排详情 |
|||
const res = await apiRoute.getCourseScheduleInfo({ |
|||
id: this.scheduleId |
|||
}); |
|||
|
|||
if (res.code === 1 && res.data) { |
|||
this.scheduleDetail = res.data; |
|||
// 计算学生数量 |
|||
this.studentCount = this.scheduleDetail.student_courses ? |
|||
this.scheduleDetail.student_courses.length : 0; |
|||
} else { |
|||
// 如果新接口不可用,尝试使用旧接口 |
|||
const fallbackRes = await apiRoute.courseInfo({ |
|||
id: this.scheduleId |
|||
}); |
|||
|
|||
if (fallbackRes.code === 1 && fallbackRes.data) { |
|||
this.scheduleDetail = fallbackRes.data; |
|||
this.studentCount = this.scheduleDetail.student_courses ? |
|||
this.scheduleDetail.student_courses.length : 0; |
|||
} else { |
|||
throw new Error(res.msg || fallbackRes.msg || '获取课程详情失败'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('获取课程详情失败:', error); |
|||
this.error = true; |
|||
this.errorMessage = error.message || '获取课程详情失败,请重试'; |
|||
} finally { |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
|
|||
// 点名功能 |
|||
handleSignIn() { |
|||
// 如果课程已结束,显示提示 |
|||
if (this.statusText === '已结束') { |
|||
uni.showToast({ |
|||
title: '课程已结束,无法点名', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 如果没有学生,显示提示 |
|||
if (!this.scheduleDetail.student_courses || this.scheduleDetail.student_courses.length === 0) { |
|||
uni.showToast({ |
|||
title: '暂无学员,无法点名', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 显示点名界面或跳转至点名页面 |
|||
uni.navigateTo({ |
|||
url: `/pages/coach/schedule/sign_in?id=${this.scheduleId}` |
|||
}); |
|||
}, |
|||
|
|||
// 调课功能 |
|||
handleAdjustClass() { |
|||
// 如果课程已结束,显示提示 |
|||
if (this.statusText === '已结束') { |
|||
uni.showToast({ |
|||
title: '课程已结束,无法调课', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 跳转至调课页面 |
|||
uni.navigateTo({ |
|||
url: `/pages/coach/schedule/adjust_course?id=${this.scheduleId}` |
|||
}); |
|||
}, |
|||
|
|||
// 从弹窗组件中点名处理 |
|||
onSignIn(data) { |
|||
console.log('处理点名:', data); |
|||
uni.navigateTo({ |
|||
url: `/pages/coach/schedule/sign_in?id=${data.scheduleId}` |
|||
}); |
|||
}, |
|||
|
|||
// 从弹窗组件中调课处理 |
|||
onAdjustClass(data) { |
|||
console.log('处理调课:', data); |
|||
uni.navigateTo({ |
|||
url: `/pages/coach/schedule/adjust_course?id=${data.scheduleId}` |
|||
}); |
|||
}, |
|||
|
|||
// 显示弹窗 |
|||
showPopup() { |
|||
this.showDetailPopup = true; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.schedule-detail-container { |
|||
background-color: #292929; |
|||
min-height: 100vh; |
|||
padding-bottom: 140rpx; // 为底部按钮留出空间 |
|||
} |
|||
|
|||
.loading-container, .error-container { |
|||
height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.loading-text, .error-text { |
|||
margin-top: 30rpx; |
|||
font-size: 28rpx; |
|||
color: #ccc; |
|||
} |
|||
|
|||
.retry-btn { |
|||
margin-top: 30rpx; |
|||
padding: 12rpx 30rpx; |
|||
background-color: #29d3b4; |
|||
border-radius: 8rpx; |
|||
color: #fff; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.schedule-content { |
|||
padding: 30rpx; |
|||
} |
|||
|
|||
.schedule-header { |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.course-title { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.status-tag { |
|||
font-size: 24rpx; |
|||
padding: 4rpx 16rpx; |
|||
border-radius: 30rpx; |
|||
} |
|||
|
|||
.status-in-progress { |
|||
background-color: #FAD24E; |
|||
color: #333; |
|||
} |
|||
|
|||
.status-ended { |
|||
background-color: #e2e2e2; |
|||
color: #333; |
|||
} |
|||
|
|||
.status-not-started { |
|||
background-color: #1cd188; |
|||
color: #fff; |
|||
} |
|||
|
|||
.course-time { |
|||
font-size: 28rpx; |
|||
color: #FAD24E; |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.info-card { |
|||
background-color: #434544; |
|||
border-radius: 16rpx; |
|||
padding: 24rpx; |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.info-item { |
|||
display: flex; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.info-label { |
|||
width: 160rpx; |
|||
font-size: 26rpx; |
|||
color: #ccc; |
|||
} |
|||
|
|||
.info-value { |
|||
flex: 1; |
|||
font-size: 26rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.student-list-section { |
|||
background-color: #434544; |
|||
border-radius: 16rpx; |
|||
padding: 24rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding-bottom: 16rpx; |
|||
border-bottom: 1px solid #555; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title text { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.student-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.student-item { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.student-avatar { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
background-color: #555; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.student-avatar image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.student-info { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.student-name { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
} |
|||
|
|||
.student-status { |
|||
font-size: 24rpx; |
|||
color: #ff6b6b; |
|||
background: rgba(255, 107, 107, 0.1); |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
.student-status.signed { |
|||
color: #1cd188; |
|||
background: rgba(28, 209, 136, 0.1); |
|||
} |
|||
|
|||
.empty-list { |
|||
padding: 40rpx 0; |
|||
text-align: center; |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.footer-actions { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
display: flex; |
|||
padding: 30rpx; |
|||
gap: 20rpx; |
|||
background-color: #292929; |
|||
border-top: 1px solid #434544; |
|||
} |
|||
|
|||
.action-btn { |
|||
flex: 1; |
|||
height: 80rpx; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.adjust-btn { |
|||
background-color: #555; |
|||
color: #fff; |
|||
} |
|||
|
|||
.sign-btn { |
|||
background-color: #29d3b4; |
|||
color: #fff; |
|||
} |
|||
</style> |
|||
@ -1,263 +0,0 @@ |
|||
<template> |
|||
<view class="container"> |
|||
<view class="header"> |
|||
<text class="title">字典功能测试</text> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">1. 测试单个字典获取</view> |
|||
<button class="test-btn" @click="testSingleDict">测试获取单个字典</button> |
|||
<view class="result" v-if="singleResult"> |
|||
<text class="result-title">结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(singleResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">2. 测试批量字典获取</view> |
|||
<button class="test-btn" @click="testBatchDict">测试批量获取字典</button> |
|||
<view class="result" v-if="batchResult"> |
|||
<text class="result-title">结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(batchResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">3. 测试字典缓存</view> |
|||
<button class="test-btn" @click="testCache">测试缓存机制</button> |
|||
<view class="result" v-if="cacheResult"> |
|||
<text class="result-title">缓存测试结果:</text> |
|||
<text class="result-content">{{ cacheResult }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">4. 测试静默请求</view> |
|||
<button class="test-btn" @click="testQuietRequest">测试静默请求</button> |
|||
<view class="result" v-if="quietResult"> |
|||
<text class="result-title">静默请求结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(quietResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">5. 清除缓存</view> |
|||
<button class="test-btn clear" @click="clearAllCache">清除所有缓存</button> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import dictUtil from '@/common/dictUtil.js' |
|||
import axiosQuiet from '@/common/axiosQuiet.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
singleResult: null, |
|||
batchResult: null, |
|||
cacheResult: null, |
|||
quietResult: null |
|||
} |
|||
}, |
|||
methods: { |
|||
// 测试单个字典获取 |
|||
async testSingleDict() { |
|||
try { |
|||
console.log('开始测试单个字典获取') |
|||
const result = await dictUtil.getDict('source') |
|||
this.singleResult = result |
|||
console.log('单个字典获取结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '单个字典测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('单个字典获取失败:', error) |
|||
this.singleResult = { error: error.message || '获取失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '单个字典测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试批量字典获取 |
|||
async testBatchDict() { |
|||
try { |
|||
console.log('开始测试批量字典获取') |
|||
const keys = ['source', 'SourceChannel', 'customer_purchasing_power'] |
|||
const result = await dictUtil.getBatchDict(keys) |
|||
this.batchResult = result |
|||
console.log('批量字典获取结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '批量字典测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('批量字典获取失败:', error) |
|||
this.batchResult = { error: error.message || '获取失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '批量字典测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试缓存机制 |
|||
async testCache() { |
|||
try { |
|||
console.log('开始测试缓存机制') |
|||
|
|||
// 清除缓存 |
|||
dictUtil.clearCache(['source']) |
|||
|
|||
// 第一次获取(应该从接口获取) |
|||
const start1 = Date.now() |
|||
await dictUtil.getDict('source', true) |
|||
const time1 = Date.now() - start1 |
|||
|
|||
// 第二次获取(应该从缓存获取) |
|||
const start2 = Date.now() |
|||
await dictUtil.getDict('source', true) |
|||
const time2 = Date.now() - start2 |
|||
|
|||
this.cacheResult = `第一次获取: ${time1}ms, 第二次获取: ${time2}ms, 缓存提升: ${Math.round((time1 - time2) / time1 * 100)}%` |
|||
|
|||
uni.showToast({ |
|||
title: '缓存测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('缓存测试失败:', error) |
|||
this.cacheResult = '缓存测试失败: ' + (error.message || '未知错误') |
|||
|
|||
uni.showToast({ |
|||
title: '缓存测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试静默请求 |
|||
async testQuietRequest() { |
|||
try { |
|||
console.log('开始测试静默请求') |
|||
const result = await axiosQuiet.get('/dict/batch', { |
|||
keys: 'source,SourceChannel' |
|||
}) |
|||
this.quietResult = result |
|||
console.log('静默请求结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '静默请求测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('静默请求失败:', error) |
|||
this.quietResult = { error: error.message || error.msg || '请求失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '静默请求测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 清除所有缓存 |
|||
clearAllCache() { |
|||
dictUtil.clearCache() |
|||
this.singleResult = null |
|||
this.batchResult = null |
|||
this.cacheResult = null |
|||
this.quietResult = null |
|||
|
|||
uni.showToast({ |
|||
title: '缓存已清除', |
|||
icon: 'success' |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.container { |
|||
padding: 20rpx; |
|||
background: #f5f5f5; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.header { |
|||
background: #fff; |
|||
padding: 30rpx; |
|||
border-radius: 12rpx; |
|||
margin-bottom: 20rpx; |
|||
text-align: center; |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.test-section { |
|||
background: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.test-btn { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
border: none; |
|||
border-radius: 8rpx; |
|||
padding: 16rpx 32rpx; |
|||
font-size: 26rpx; |
|||
margin-bottom: 20rpx; |
|||
|
|||
&.clear { |
|||
background: #ff6b6b; |
|||
} |
|||
|
|||
&:active { |
|||
opacity: 0.8; |
|||
} |
|||
} |
|||
|
|||
.result { |
|||
background: #f8f9fa; |
|||
border-radius: 8rpx; |
|||
padding: 20rpx; |
|||
border-left: 4rpx solid #29d3b4; |
|||
|
|||
.result-title { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.result-content { |
|||
font-size: 22rpx; |
|||
color: #333; |
|||
word-break: break-all; |
|||
white-space: pre-wrap; |
|||
font-family: monospace; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue