智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

715 lines
14 KiB

<template>
<view class="sign-in-container">
<view class="content">
<!-- 课程信息 -->
<view class="course-info-card" v-if="scheduleInfo">
<view class="course-title">{{ scheduleInfo.course_name }}</view>
<view class="course-time">{{ scheduleInfo.course_date }} {{ scheduleInfo.time_slot }}</view>
<view class="course-detail">
<view class="detail-item">
<text class="detail-label">授课教练:</text>
<text class="detail-value">{{ scheduleInfo.coach_name }}</text>
</view>
<view class="detail-item">
<text class="detail-label">上课场地:</text>
<text class="detail-value">{{ scheduleInfo.venue_name }}</text>
</view>
<view class="detail-item">
<text class="detail-label">学员人数:</text>
<text class="detail-value">{{ studentList.length }}人</text>
</view>
</view>
</view>
<!-- 学员列表 -->
<view class="student-section">
<view class="section-header">
<view class="section-title">学员点名</view>
<!-- <view class="action-buttons">-->
<!-- <view class="view-btn primary" @click="checkAllStudents">全部签到</view>-->
<!-- <view class="view-btn danger" @click="uncheckAllStudents">全部取消</view>-->
<!-- </view>-->
</view>
<view class="empty-list" v-if="studentList.length === 0">
<image :src="$util.img('/static/icon-img/empty.png')" mode="aspectFit" class="empty-img"></image>
<text class="empty-text">暂无学员数据</text>
</view>
<view class="student-list" v-else>
<view class="student-item" v-for="(student, index) in studentList" :key="index"
@click="toggleStudentStatus(index)">
<view class="student-avatar">
<image :src="student.avatar || $util.img('/static/icon-img/avatar.png')" mode="aspectFill"></image>
<view :class="['status-badge',student.statusClass]"></view>
</view>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
<text class="student-phone">{{ student.phone_number || '无联系电话' }}</text>
</view>
<view class="status-container">
<view class="current-status" @click.stop="toggleStudentStatus(index)"
:class="'status-' + student.status">
{{ student.status_text }}
</view>
</view>
</view>
</view>
</view>
<!-- 点名备注 -->
<view class="remark-section">
<view class="section-title">点名备注</view>
<fui-textarea v-model="signInRemark" placeholder="请输入点名备注(可选)" maxlength="200"></fui-textarea>
</view>
<!-- 课堂照片 -->
<view class="photo-section">
<view class="section-title">课堂照片</view>
<view class="photo-upload">
<view v-if="!classPhoto" class="photo-placeholder" @click="chooseImage">
<view class="placeholder-icon">📷</view>
<view class="placeholder-text">点击添加课堂照片</view>
</view>
<view v-else class="photo-preview">
<image :src="classPhoto" mode="aspectFill" class="preview-image" @click="previewImage"></image>
<view class="photo-actions">
<view class="action-btn" @click="chooseImage">重新选择</view>
<view class="action-btn delete" @click="deleteImage">删除</view>
</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-btn">
<fui-button type="primary" @click="submitSignIn" :loading="submitting">提交点名</fui-button>
</view>
</view>
</view>
</template>
<script>
import api from '@/api/apiRoute.js';
import util from '@/common/util.js';
export default {
data() {
return {
// 课程ID
scheduleId: null,
// 课程信息
scheduleInfo: null,
// 学员列表
studentListRaw: [],
// 点名备注
signInRemark: '',
// 课堂照片
classPhoto: '',
classPhotoRemoteUrl: '', // 服务器返回的资源地址
// 提交状态
submitting: false
};
},
computed: {
statusClass() {
const statusMap = {
'pending': 'status-pending',
'upcoming': 'status-upcoming',
'ongoing': 'status-ongoing',
'completed': 'status-completed'
};
return statusMap[this.scheduleInfo.status] || '';
},
studentList() {
const statusMap = {
0: 'status-absent',
1: 'status-present',
2: 'status-leave'
};
return this.studentListRaw.map(student => ({
...student,
statusClass: statusMap[student.status] || 'status-absent',
status_text: this.getStatusText(student.status)
}));
}
},
onLoad(options) {
if (options.id) {
this.scheduleId = options.id;
this.loadScheduleInfo();
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
},
methods: {
// 加载课程安排信息
async loadScheduleInfo() {
uni.showLoading({
title: '加载中...'
});
try {
const res = await api.getCourseScheduleInfo({
schedule_id: this.scheduleId
});
if (res.code === 1) {
this.scheduleInfo = res.data;
// 处理学员列表
if (this.scheduleInfo.students && this.scheduleInfo.students.length > 0) {
this.studentListRaw = [...this.scheduleInfo.students];
}
} else {
uni.showToast({
title: res.msg || '获取课程安排信息失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取课程安排信息失败:', error);
uni.showToast({
title: '获取课程安排信息失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
// 获取学员状态样式
getStatusClass(status) {
const statusMap = {
0: 'status-absent',
1: 'status-present',
2: 'status-leave'
};
return statusMap[status] || 'status-absent';
},
// 获取状态文本
getStatusText(status) {
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusTextMap[status] || '未知状态';
},
// 切换学员状态
toggleStudentStatus(index) {
const student = this.studentList[index];
// 状态循环:未到 -> 已到 -> 请假 -> 未到
let newStatus = 0;
if (student.status === 0) {
newStatus = 1;
} else if (student.status === 1) {
newStatus = 2;
} else {
newStatus = 0;
}
this.setStudentStatus(index, newStatus);
},
// 设置学员状态
setStudentStatus(index, status) {
if (index >= 0 && index < this.studentList.length) {
this.studentList[index].status = status;
// 更新状态文本
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
this.studentList[index].status_text = statusTextMap[status];
}
},
// 全部签到
checkAllStudents() {
this.studentList.forEach((student, index) => {
this.setStudentStatus(index, 1);
});
},
// 全部取消
uncheckAllStudents() {
this.studentList.forEach((student, index) => {
this.setStudentStatus(index, 0);
});
},
// 选择图片
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
this.classPhoto = tempFilePath;
// 显示上传进度
uni.showLoading({
title: '上传中...'
});
// 使用uploadFile方法上传到服务器
util.uploadFile(
tempFilePath,
(fileData) => {
// 上传成功,保存服务器返回的URL
this.classPhotoRemoteUrl = fileData.url;
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success'
});
console.log('图片上传成功:', fileData);
},
(error) => {
// 上传失败,清空本地图片
this.classPhoto = '';
this.classPhotoRemoteUrl = '';
uni.hideLoading();
console.error('图片上传失败:', error);
}
);
},
fail: (error) => {
console.error('选择图片失败:', error);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
// 预览图片
previewImage() {
uni.previewImage({
urls: [this.classPhoto],
current: this.classPhoto
});
},
// 删除图片
deleteImage() {
uni.showModal({
title: '确认删除',
content: '确定要删除这张课堂照片吗?',
success: (res) => {
if (res.confirm) {
this.classPhoto = '';
this.classPhotoRemoteUrl = '';
}
}
});
},
// 提交点名
async submitSignIn() {
// 如果有图片但还未上传完成,提示等待
if (this.classPhoto && !this.classPhotoRemoteUrl) {
uni.showToast({
title: '图片上传中,请稍候',
icon: 'none'
});
return;
}
// 准备提交数据
const studentData = this.studentList.map(student => ({
student_id: student.student_id,
resource_id: student.resource_id,
status: student.status
}));
const submitData = {
schedule_id: this.scheduleId,
students: studentData,
remark: this.signInRemark,
class_photo: this.classPhotoRemoteUrl || '' // 使用服务器返回的资源地址
};
this.submitting = true;
try {
// 使用API进行点名
const res = await api.submitScheduleSignIn(submitData);
if (res.code === 1) {
uni.showToast({
title: '点名成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: res.msg || '点名失败',
icon: 'none'
});
}
} catch (error) {
console.error('点名失败:', error);
uni.showToast({
title: '点名失败,请重试',
icon: 'none'
});
} finally {
this.submitting = false;
}
}
}
};
</script>
<style lang="scss" scoped>
.sign-in-container {
min-height: 100vh;
background-color: #18181c;
}
.content {
padding: 30rpx;
}
.course-info-card {
background-color: #23232a;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 30rpx;
}
.course-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
margin-bottom: 10rpx;
}
.course-time {
font-size: 26rpx;
color: #29d3b4;
margin-bottom: 20rpx;
}
.course-detail {
background-color: #2a2a2a;
border-radius: 8rpx;
padding: 16rpx;
}
.detail-item {
display: flex;
margin-bottom: 10rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
}
.detail-label {
color: #999;
width: 140rpx;
}
.detail-value {
color: #fff;
flex: 1;
}
.student-section {
background-color: #23232a;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #fff;
}
.action-buttons {
display: flex;
gap: 16rpx;
}
.view-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
border-radius: 6rpx;
text-align: center;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
&.primary {
background-color: #29d3b4;
&:active {
background-color: #23b89d;
}
}
&.danger {
background-color: #ff3b30;
&:active {
background-color: #d63126;
}
}
}
.student-list {
max-height: 600rpx;
overflow-y: auto;
}
.student-item {
display: flex;
align-items: center;
background-color: #2a2a2a;
border-radius: 8rpx;
padding: 16rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.student-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
overflow: hidden;
position: relative;
margin-right: 20rpx;
image {
width: 100%;
height: 100%;
}
}
.status-badge {
position: absolute;
bottom: 0;
right: 0;
width: 24rpx;
height: 24rpx;
border-radius: 12rpx;
background-color: #999;
border: 2rpx solid #fff;
}
.status-absent {
background-color: #ff3b30;
}
.status-present {
background-color: #34c759;
}
.status-leave {
background-color: #ff9500;
}
.student-info {
flex: 1;
display: flex;
flex-direction: column;
}
.student-name {
font-size: 28rpx;
color: #fff;
margin-bottom: 6rpx;
}
.student-phone {
font-size: 24rpx;
color: #999;
}
.status-container {
margin-left: 16rpx;
}
.current-status {
padding: 12rpx 20rpx;
font-size: 24rpx;
border-radius: 30rpx;
text-align: center;
min-width: 80rpx;
cursor: pointer;
transition: all 0.3s ease;
&.status-0 {
background-color: #3a3a3a;
color: #fff;
}
&.status-1 {
background-color: #34c759;
color: #fff;
}
&.status-2 {
background-color: #ff9500;
color: #fff;
}
&:active {
transform: scale(0.95);
}
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-img {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
.remark-section {
background-color: #23232a;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 40rpx;
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
}
.photo-section {
background-color: #23232a;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 40rpx;
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
}
.photo-upload {
width: 100%;
}
.photo-placeholder {
width: 100%;
height: 200rpx;
background-color: #2a2a2a;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2rpx dashed #666;
.placeholder-icon {
font-size: 60rpx;
margin-bottom: 10rpx;
}
.placeholder-text {
font-size: 24rpx;
color: #999;
}
}
.photo-preview {
width: 100%;
.preview-image {
width: 100%;
height: 300rpx;
border-radius: 12rpx;
}
.photo-actions {
display: flex;
gap: 20rpx;
margin-top: 16rpx;
.action-btn {
flex: 1;
height: 60rpx;
background-color: #29d3b4;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #fff;
&.delete {
background-color: #ff3b30;
}
}
}
}
.submit-btn {
margin-top: 40rpx;
padding-bottom: 40rpx;
}
</style>