智慧教务系统
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.
 
 
 
 
 
 

1292 lines
33 KiB

<template>
<view class="course-schedule">
<!-- Course Info -->
<view class="course-info">
<text class="course-title">课程安排详情</text>
<view class="course-detail">
<text class="course-item">日期{{ schedule_info.course_date }}</text>
<text class="course-item">课程名称{{ schedule_info.course_name }}</text>
<text class="course-item">课程时间{{ schedule_info.time_slot }}</text>
<text class="course-item">主教练{{ schedule_info.coach_name || '待安排' }}</text>
<text class="course-item">场地信息{{ schedule_info.venue_name }}</text>
</view>
</view>
<!-- Formal Students Section -->
<view class="section">
<text class="section-title">正式学员</text>
<view class="cards-grid">
<!-- Student Card with Data -->
<view
v-for="(stu, idx) in formalStudents"
:key="idx"
class="student-card filled"
@tap="viewStudent(stu)"
>
<view v-if="stu.renewal_status" class="renewal-badge">待续费</view>
<view class="avatar">{{ stu.name && stu.name.charAt(0) }}</view>
<view class="student-info">
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄:{{ stu.age || '未知' }}岁</view>
<view class="course-status">课程状态:{{ stu.courseStatus }}</view>
<view class="course-status">上课情况:{{ stu.course_progress.used }}/{{ stu.course_progress.total }}节</view>
<view class="expiry-date" v-if="stu.student_course_info">到期时间:{{ stu.student_course_info.end_date || '未设置' }}</view>
</view>
</view>
<!-- Empty Slots -->
<view
v-for="n in formalEmptySeats"
:key="n"
class="student-card empty"
:data-type="'formal'"
:data-index="n"
@tap="openStudentModal"
>
<view class="add-icon">
<text class="plus-icon">+</text>
</view>
<view class="add-text">
<view class="slot-title">空位</view>
<view class="slot-subtitle">点击添加学员</view>
</view>
</view>
</view>
</view>
<!-- Waiting List Section -->
<view class="section">
<text class="section-title waiting-title">等待位</text>
<view class="cards-grid">
<!-- Waiting Students -->
<view
v-for="(stu, idx) in waitingStudents"
:key="idx"
class="student-card waiting filled"
@tap="viewStudent(stu)"
>
<view class="avatar waiting-avatar">{{ stu.name && stu.name.charAt(0) }}</view>
<view class="student-info">
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄:{{ stu.age || '未知' }}岁</view>
<view class="course-status">课程状态:{{ stu.courseStatus }}</view>
<view class="course-progress">上课情况:{{ stu.course_progress.used }}/{{ stu.course_progress.total }}节 ({{ stu.course_progress.percentage }}%)</view>
<view class="expiry-date">到期时间:{{ stu.expiryDate || '未设置' }}</view>
</view>
</view>
<!-- Waiting Empty Slots -->
<view
v-for="n in waitingEmptySeats"
:key="n"
class="student-card waiting"
:data-type="'waiting'"
:data-index="n"
@tap="openStudentModal"
>
<view class="add-icon waiting-icon">
<text class="plus-icon">+</text>
</view>
<view class="add-text">
<view class="slot-title">等待位</view>
<view class="slot-subtitle">点击添加学员</view>
</view>
</view>
</view>
</view>
<!-- Bottom Popup Modal -->
<view v-if="showModal" class="modal-overlay" @tap="closeModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text>添加学员</text>
</view>
<view class="modal-body">
<!-- Customer Selection - 只在没有预设学生时显示 -->
<view v-if="!presetStudent || (!presetStudent.name && !presetStudent.phone)" class="form-section">
<text class="form-label">客户选择</text>
<input
v-model="searchQuery"
placeholder="请输入手机号或姓名"
class="search-input"
@input="searchStudents"
/>
<!-- Search Results -->
<view v-if="searchResults.length > 0" class="search-results">
<view
v-for="student in searchResults"
:key="student.id"
:class="['student-item', { selected: selectedStudent && selectedStudent.id === student.id }]"
@tap="selectStudent(student)"
>
<view class="student-avatar">{{ student.name ? student.name.charAt(0) : '?' }}</view>
<view class="student-details">
<view class="student-name">
{{ student.name }}
<text v-if="student.is_formal_student" class="formal-badge">正式学员</text>
</view>
<view class="student-phone">{{ student.phone }}</view>
</view>
<view v-if="selectedStudent && selectedStudent.id === student.id" class="check-icon">
<text>✓</text>
</view>
</view>
</view>
</view>
<!-- 预设学生信息显示 -->
<view v-if="presetStudent && presetStudent.name && presetStudent.phone" class="form-section">
<text class="form-label">选中学员</text>
<view class="preset-student">
<view class="student-avatar">{{ (presetStudent && presetStudent.name) ? presetStudent.name.charAt(0) : '?' }}</view>
<view class="student-details">
<view class="student-name">{{ (presetStudent && presetStudent.name) || '未知学员' }}</view>
<view class="student-phone">{{ (presetStudent && presetStudent.phone) || '未知手机号' }}</view>
</view>
</view>
</view>
<!-- Course Arrangement -->
<view class="form-section">
<text class="form-label">课程安排</text>
<view class="radio-group">
<label class="radio-item">
<radio
value="1"
:checked="courseArrangement === '1'"
@tap="courseArrangement = '1'"
/>
<text class="radio-text">临时课</text>
</label>
<label class="radio-item">
<radio
value="2"
:checked="courseArrangement === '2'"
@tap="courseArrangement = '2'"
/>
<text class="radio-text">固定课</text>
</label>
</view>
</view>
<!-- Remarks -->
<view class="form-section">
<text class="form-label">备注</text>
<textarea
v-model="remarks"
placeholder="请输入备注信息"
class="remarks-textarea"
></textarea>
</view>
</view>
<!-- Modal Footer -->
<view class="modal-footer">
<view class="btn btn-cancel" @tap="closeModal">取消</view>
<view class="btn btn-confirm" @tap="confirmSelection">确定</view>
</view>
</view>
</view>
<!-- 请假原因弹窗 -->
<fui-modal ref="leaveReasonModal" :buttons="[]" width="600" title="请假申请">
<view class="leave-form">
<view class="leave-label">请假原因</view>
<view class="leave-input">
<fui-textarea v-model="leaveReason" placeholder="请输入请假原因" :isCounter="true" :maxlength="200"
minHeight="200" :isAutoHeight="true"></fui-textarea>
</view>
<view class="leave-buttons">
<fui-button background="#434544" color="#fff" borderColor="#666" btnSize="medium"
@tap="$refs.leaveReasonModal.close()">取消</fui-button>
<fui-button background="#29d3b4" color="#fff" btnSize="medium"
@tap="submitLeaveRequest">提交</fui-button>
</view>
</view>
</fui-modal>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
course_id: '',
schedule_info: {}, // 课程安排基本信息
date: '',
students: [], // 所有学员数据
formalStudents: [], // 正式学员
waitingStudents: [], // 等待位学员
resource_id: '',
student_id: '', // 预设学生ID
leaveReason: '', // 请假原因
currentStudent: null, // 当前操作的学生
formalEmptySeats: [], // 正式学员空位数组
waitingEmptySeats: [1, 2], // 等待位固定2个空位
// 新增弹窗相关数据
showModal: false,
searchQuery: '',
courseArrangement: '1', // 1=临时课, 2=固定课
remarks: '',
selectedStudent: null,
currentSlot: null,
searchResults: [],
presetStudent: null // 预设学生信息
};
},
onLoad(query) {
console.log('onLoad 参数:', query);
// 支持两种参数名:id 和 schedule_id
this.course_id = query.id || query.schedule_id || '';
this.resource_id = query.resource_id || '';
this.student_id = query.student_id || '';
console.log('初始化参数 - course_id:', this.course_id, 'resource_id:', this.resource_id, 'student_id:', this.student_id);
// 智能路由逻辑:如果有预设的resource_id和student_id,直接获取学生信息
if (this.resource_id && this.student_id) {
this.loadPresetStudent();
}
// 确保有课程ID才调用API
if (this.course_id) {
this.getScheduleDetail();
} else {
console.error('缺少课程ID参数');
uni.showToast({
title: '缺少课程ID参数',
icon: 'none'
});
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载预设学生信息
async loadPresetStudent() {
try {
// 调用API获取客户资源详细信息
const res = await apiRoute.getCustomerResourcesInfo({ resource_sharing_id: this.resource_id });
if (res.code === 1 && res.data) {
// 直接从客户资源详情获取基础信息,然后调用优化后的接口获取课程状态
const customerRes = await apiRoute.getCustomerResourcesAll({
phone_number: res.data.phone_number
});
let isFormalStudent = false;
let courseInfo = [];
if (customerRes.code === 1 && customerRes.data && customerRes.data.length > 0) {
const customer = customerRes.data.find(c => c.member_id === res.data.member_id);
if (customer) {
isFormalStudent = customer.is_formal_student || false;
courseInfo = customer.course_info || [];
}
}
this.presetStudent = {
id: res.data.id,
name: res.data.name,
phone: res.data.phone_number,
age: res.data.age,
member_id: res.data.member_id,
resource_id: res.data.id,
is_formal_student: isFormalStudent,
course_info: courseInfo
};
this.selectedStudent = this.presetStudent;
console.log('加载预设学生信息:', this.presetStudent);
}
} catch (error) {
console.error('加载预设学生信息失败:', error);
}
},
// 打开学生选择弹窗
openStudentModal(event) {
// 从 data 属性获取参数
const type = event.currentTarget.dataset.type;
const index = event.currentTarget.dataset.index;
this.showModal = true;
this.currentSlot = { type, index };
this.resetForm();
// 如果有预设学生,直接选中
if (this.presetStudent) {
this.selectedStudent = this.presetStudent;
}
},
// 关闭弹窗
closeModal() {
this.showModal = false;
this.resetForm();
},
// 重置表单
resetForm() {
if (!this.presetStudent) {
this.searchQuery = '';
this.searchResults = [];
this.selectedStudent = null;
}
this.courseArrangement = '1';
this.remarks = '';
},
// 搜索学生 - 智能检索
async searchStudents() {
if (!this.searchQuery.trim()) {
this.searchResults = [];
return;
}
// 智能判断输入类型:如果全是数字则判断为手机号,否则为姓名
const isPhoneNumber = /^\d+$/.test(this.searchQuery.trim());
try {
uni.showLoading({
title: '搜索中...'
});
// 调用后端接口进行检索
const params = {};
if (isPhoneNumber) {
params.phone_number = this.searchQuery.trim();
} else {
params.name = this.searchQuery.trim();
}
const res = await apiRoute.getCustomerResourcesAll(params);
uni.hideLoading();
if (res.code === 1 && Array.isArray(res.data)) {
// 后端已经返回了正式学员状态,直接使用
this.searchResults = res.data.map(student => ({
id: student.id,
name: student.name,
phone: student.phone_number,
age: student.age,
member_id: student.member_id,
resource_id: student.id,
is_formal_student: student.is_formal_student || false,
course_info: student.course_info || []
}));
} else {
this.searchResults = [];
}
} catch (error) {
uni.hideLoading();
console.error('搜索学员失败:', error);
this.searchResults = [];
}
},
// 选择学生
selectStudent(student) {
this.selectedStudent = student;
},
// 确认选择
async confirmSelection() {
if (!this.selectedStudent) {
uni.showToast({
title: '请选择学员',
icon: 'none'
});
return;
}
// 如果选择的是固定课,需要验证是否为正式学员
if (this.courseArrangement === '2') {
if (!this.selectedStudent.is_formal_student) {
uni.showToast({
title: '只有正式学员才能选择固定课',
icon: 'none'
});
return;
}
}
// 构建添加学员的数据
const data = {
'resources_id': this.selectedStudent.resource_id || this.resource_id,
'person_type': 'customer_resource',
'schedule_id': this.course_id,
'course_date': this.schedule_info.course_date,
'time_slot': this.schedule_info.time_slot,
'position': this.currentSlot.index,
'schedule_type': this.currentSlot.type === 'waiting' ? 2 : 1,
'course_type': this.currentSlot.type === 'waiting' ? 3 : parseInt(this.courseArrangement),
'remark': this.remarks
};
try {
uni.showLoading({
title: '添加中...'
});
let res = await apiRoute.addSchedule(data);
uni.hideLoading();
if (res.code == 1) {
uni.showToast({
title: '添加成功',
icon: 'success'
});
this.closeModal();
// 刷新数据
await this.getScheduleDetail();
} else {
uni.showToast({
title: res.msg || '添加失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('添加学员失败:', error);
// 尝试获取具体的错误信息
let errorMsg = '添加失败';
if (error && error.data && error.data.msg) {
errorMsg = error.data.msg;
} else if (error && error.message) {
errorMsg = error.message;
} else if (typeof error === 'string') {
errorMsg = error;
}
uni.showToast({
title: errorMsg,
icon: 'none'
});
}
},
// 获取课程安排类型文本
getScheduleTypeText(type) {
const typeMap = {
1: '固定课',
2: '临时课'
};
return typeMap[type] || '未知';
},
viewStudent(stu) {
console.log(stu, this.schedule_info);
// 显示操作选项弹窗
uni.showActionSheet({
title: `学员: ${stu.name}`,
itemList: stu.person_type === 'customer_resource' ? ['取消课程'] : ['申请请假'],
success: (res) => {
if (res.tapIndex === 0) {
// 判断学员类型
if (stu.person_type === 'customer_resource') {
// 如果是客户资源,弹出确认取消课程的弹窗
uni.showModal({
title: '取消课程',
content: `是否取消学员 ${stu.name} 的课程?`,
success: async (res) => {
if (res.confirm) {
// 用户点击确定,调用schedule_del接口
try {
uni.showLoading({
title: '处理中...'
});
const params = {
resources_id: stu.resources_id,
id: this.schedule_info.id
};
const result = await apiRoute.schedule_del(params);
uni.hideLoading();
if (result.code === 1) {
uni.showToast({
title: '取消课程成功',
icon: 'success'
});
// 刷新数据
await this.getScheduleDetail();
} else {
uni.showToast({
title: result.msg || '取消课程失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
});
console.error('取消课程失败:', error);
}
}
}
});
} else if (stu.person_type === 'student') {
// 如果是学生,弹出请假原因输入框
this.$refs.leaveReasonModal.open();
this.currentStudent = stu; // 保存当前操作的学生信息
}
}
}
});
},
getStatusText(status) {
const statusMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusMap[status] || status;
},
// 获取课程类型文本
getCourseTypeText(courseType) {
const courseTypeMap = {
1: '正式课',
2: '体验课',
3: '补课',
4: '试听课'
};
return courseTypeMap[courseType] || '未知';
},
// 分类学员数据
categorizeStudents() {
// 根据schedule_type分类学员
// schedule_type: 1=正式位, 2=等待位
this.formalStudents = this.students.filter(stu => stu.schedule_type === 1 || stu.schedule_type === null);
this.waitingStudents = this.students.filter(stu => stu.schedule_type === 2);
// 确保学员名称存在,如果不存在则返回未知学员
this.formalStudents.forEach(stu => {
if (!stu.name) stu.name = '未知学员';
});
this.waitingStudents.forEach(stu => {
if (!stu.name) stu.name = '未知学员';
});
console.log('学员分类完成:', {
formal: this.formalStudents.length,
waiting: this.waitingStudents.length
});
},
async scheduleList() {
try {
console.log('开始获取学员列表, schedule_id:', this.course_id);
let res = await apiRoute.scheduleList({
'schedule_id': this.course_id
})
console.log('学员列表响应:', res);
if (res.code === 1 && Array.isArray(res.data)) {
// 确保学员列表按照位置排序
this.students = res.data.sort((a, b) => {
// 如果有position字段,按position排序
if (a.position !== undefined && b.position !== undefined) {
return parseInt(a.position) - parseInt(b.position);
}
// 否则按照ID排序
return a.id - b.id;
});
// 分类学员
this.categorizeStudents();
} else {
this.students = [];
this.formalStudents = [];
this.waitingStudents = [];
}
// 更新可用容量
this.updateAvailableCapacity();
return this.students;
} catch (error) {
console.error('获取学员列表失败:', error);
this.students = [];
this.formalStudents = [];
this.waitingStudents = [];
return [];
}
},
async getScheduleDetail() {
try {
console.log('开始获取课程安排详情, schedule_id:', this.course_id);
let res = await apiRoute.getScheduleDetail({
'schedule_id': this.course_id
})
console.log('课程安排详情响应:', res);
if (res.code === 1 && res.data) {
this.schedule_info = res.data.schedule_info;
this.formalStudents = res.data.formal_students || [];
this.waitingStudents = res.data.waiting_students || [];
// 计算可用容量
this.updateAvailableCapacity();
} else {
uni.showToast({
title: res.msg || '获取课程安排详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取课程安排详情失败:', error);
uni.showToast({
title: '获取课程安排详情失败',
icon: 'none'
});
}
},
// 更新可用容量
updateAvailableCapacity() {
// 确保schedule_info已加载
if (!this.schedule_info) return;
console.log('更新可用容量 - 课程最大容量:', this.schedule_info.max_students, '正式学员数量:', this.formalStudents.length);
// 计算正式位已占用的位置数
const occupiedFormalSeats = this.formalStudents.length;
// 获取课程最大容量(正式位)
const maxStudents = parseInt(this.schedule_info.max_students) || 0;
let remainingFormalSeats = 0;
if (maxStudents === 0) {
// 如果max_students为0,则没有学员限制,可以一直新增学员
// 这里设置一个合理的默认显示数量,比如显示最多6个空位
remainingFormalSeats = Math.max(1, 6 - occupiedFormalSeats);
} else {
// 有限制的情况下,计算剩余空位
remainingFormalSeats = Math.max(0, maxStudents - occupiedFormalSeats);
}
console.log('计算后的正式位剩余容量:', remainingFormalSeats);
// 初始化正式位空位数组
this.formalEmptySeats = [];
for (let i = 1; i <= remainingFormalSeats; i++) {
this.formalEmptySeats.push(i);
}
// 等待位永远都能添加学员,这里固定显示2个等待位空位(减去已有的等待位学员)
const remainingWaitingSeats = Math.max(0, 2 - this.waitingStudents.length);
this.waitingEmptySeats = [];
for (let i = 1; i <= remainingWaitingSeats; i++) {
this.waitingEmptySeats.push(i);
}
console.log('等待位剩余空位:', remainingWaitingSeats);
},
async addStudent(e, index, type = 'formal') {
console.log('添加学员到位置:', index, '类型:', type);
const data = {
'resources_id': this.resource_id,
'person_type': 'customer_resource',
'schedule_id': this.course_id,
'course_date': this.schedule_info.course_date,
'time_slot': this.schedule_info.time_slot,
'position': index, // 位置信息
'schedule_type': type === 'waiting' ? 2 : 1, // 1=正式位, 2=等待位
'course_type': type === 'waiting' ? 3 : 1 // 3=等待位, 1=正式课
};
try {
uni.showLoading({
title: '添加中...'
});
let res = await apiRoute.addSchedule(data)
uni.hideLoading();
if (res.code == 1) {
uni.showToast({
title: '添加成功',
icon: 'success'
});
// 刷新数据并更新可用容量
await this.getScheduleDetail();
} else {
uni.showToast({
title: res.msg || '添加失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('添加学员失败:', error);
// 尝试获取具体的错误信息
let errorMsg = '添加失败';
if (error && error.data && error.data.msg) {
errorMsg = error.data.msg;
} else if (error && error.message) {
errorMsg = error.message;
} else if (typeof error === 'string') {
errorMsg = error;
}
uni.showToast({
title: errorMsg,
icon: 'none'
});
}
},
// 提交请假申请
submitLeaveRequest() {
if (!this.leaveReason.trim()) {
uni.showToast({
title: '请填写请假原因',
icon: 'none'
});
return;
}
// 调用请假接口
uni.showLoading({
title: '提交中...'
});
// 构建请假参数
const params = {
resources_id: this.currentStudent.resources_id,
id: this.schedule_info.id,
remark: this.leaveReason
};
// 调用请假接口
apiRoute.schedule_del(params)
.then(res => {
uni.hideLoading();
if (res.code === 1) {
uni.showToast({
title: '请假申请提交成功',
icon: 'success'
});
this.$refs.leaveReasonModal.close();
this.leaveReason = ''; // 清空请假原因
// 刷新数据并更新可用容量
this.getScheduleDetail().then(() => {
// 确保数据刷新后更新可用容量
this.updateAvailableCapacity();
});
} else {
uni.showToast({
title: res.msg || '请假申请提交失败',
icon: 'none'
});
}
})
.catch(err => {
uni.hideLoading();
uni.showToast({
title: '请假申请提交失败,请重试',
icon: 'none'
});
});
}
}
}
</script>
<style lang="less" scoped>
.course-schedule {
background: #232323;
min-height: 100vh;
padding-bottom: 30rpx;
}
.header {
display: flex;
align-items: center;
padding: 40rpx 30rpx 20rpx 30rpx;
position: relative;
.back-btn {
position: absolute;
left: 30rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
color: #fff;
font-size: 48rpx;
font-weight: bold;
}
}
.title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
text-align: center;
flex: 1;
}
}
.course-info {
font-size: 28rpx;
background: #434544;
border-radius: 16rpx;
padding: 30rpx 20rpx;
.course-title {
color: #fff;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 16rpx;
display: block;
}
.course-detail {
.course-item {
color: #29d3b4;
font-size: 26rpx;
margin-bottom: 8rpx;
display: block;
}
}
}
.section {
margin: 30rpx;
.section-title {
color: #ffd86b;
font-size: 28rpx;
margin-bottom: 30rpx;
display: block;
&.waiting-title {
color: #8a7fff !important;
}
}
}
.cards-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.student-card {
background: #333;
border-radius: 12rpx;
padding: 24rpx;
width: calc(50% - 10rpx);
box-sizing: border-box;
display: flex;
align-items: flex-start;
position: relative;
min-height: 200rpx;
.renewal-badge {
position: absolute;
top: 8rpx;
right: 8rpx;
background: #ff6b6b;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
}
.avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #29d3b4;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 18rpx;
flex-shrink: 0;
&.waiting-avatar {
background: #8a7fff;
}
}
.student-info {
flex: 1;
overflow: hidden;
.student-name {
color: #fff;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.student-age,
.course-status,
.course-arrangement,
.remaining-hours,
.expiry-date {
color: #bdbdbd;
font-size: 22rpx;
margin-bottom: 4rpx;
line-height: 1.3;
}
}
.add-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #ffd86b;
color: #232323;
display: flex;
align-items: center;
justify-content: center;
margin-right: 18rpx;
flex-shrink: 0;
.plus-icon {
font-size: 36rpx;
font-weight: bold;
}
&.waiting-icon {
background: #8a7fff;
color: #1a1a2a;
}
}
.add-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.slot-title {
color: #ffd86b;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 4rpx;
}
.slot-subtitle {
color: #ffd86b;
font-size: 22rpx;
}
}
&.empty {
border: 2rpx dashed #ffd86b;
background: #232323;
}
&.waiting {
background: #2a2a3a;
border: 1rpx solid #8a7fff;
&.empty {
border: 2rpx dashed #8a7fff;
background: #1a1a2a;
.add-text {
.slot-title,
.slot-subtitle {
color: #8a7fff;
}
}
}
&.filled {
.student-info {
.student-name {
color: #8a7fff;
}
}
}
}
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
}
.modal-content {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
width: 100%;
max-height: 80vh;
overflow: hidden;
}
.modal-header {
padding: 40rpx;
border-bottom: 1rpx solid #eee;
text-align: center;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-body {
padding: 40rpx;
max-height: 60vh;
overflow-y: auto;
}
.form-section {
margin-bottom: 40rpx;
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
display: block;
}
}
.search-tabs {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
.tab-btn {
flex: 1;
padding: 20rpx;
text-align: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #666;
&.active {
background: #29d3b4;
color: #fff;
}
}
}
.search-input {
width: 100%;
padding: 24rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
height: 90rpx;
}
.search-results {
margin-top: 20rpx;
max-height: 400rpx;
overflow-y: auto;
}
.student-item {
display: flex;
align-items: center;
padding: 20rpx;
border: 1rpx solid #eee;
border-radius: 8rpx;
margin-bottom: 16rpx;
&.selected {
border-color: #29d3b4;
background: rgba(41, 211, 180, 0.1);
}
.student-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #29d3b4;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
margin-right: 20rpx;
}
.student-details {
flex: 1;
.student-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
display: flex;
align-items: center;
gap: 16rpx;
.formal-badge {
background: #29d3b4;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
white-space: nowrap;
}
}
.student-phone {
font-size: 24rpx;
color: #666;
}
}
.check-icon {
color: #29d3b4;
font-size: 32rpx;
}
}
.preset-student {
display: flex;
align-items: center;
padding: 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
.student-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #29d3b4;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
margin-right: 20rpx;
}
.student-details {
flex: 1;
.student-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.student-phone {
font-size: 24rpx;
color: #666;
}
}
}
.radio-group {
display: flex;
gap: 40rpx;
.radio-item {
display: flex;
align-items: center;
.radio-text {
margin-left: 16rpx;
font-size: 28rpx;
color: #333;
}
}
}
.remarks-textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
resize: none;
}
.modal-footer {
padding: 40rpx;
border-top: 1rpx solid #eee;
display: flex;
gap: 20rpx;
.btn {
flex: 1;
padding: 24rpx;
text-align: center;
border-radius: 8rpx;
font-size: 28rpx;
&.btn-cancel {
background: #f5f5f5;
color: #666;
}
&.btn-confirm {
background: #29d3b4;
color: #fff;
}
}
}
/* 请假弹窗样式 */
.leave-form {
padding: 20rpx;
.leave-label {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
}
.leave-input {
margin-bottom: 30rpx;
}
.leave-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
.fui-button {
flex: 1;
}
}
}
</style>