hookehuyr

feat(教师表单): 实现批量选择课程章节并设置时间功能

添加批量选择章节功能,支持多选章节并为每个章节设置开始和结束时间
优化章节选择器UI,增加时间设置区域
添加时间验证逻辑,确保开始时间不晚于结束时间
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-23 09:49:16
* @LastEditTime: 2025-06-23 11:38:17
* @FilePath: /mlaj/src/views/teacher/formPage.vue
* @Description: 教师作业新增表单页面
-->
......@@ -122,11 +122,13 @@
<van-icon name="arrow" class="arrow-icon" @click="showCoursePicker = true" />
</div>
</div>
<div class="select-row">
<div class="select-row" v-if="formData.course">
<div class="select-item">
<van-icon name="bookmark-o" class="select-icon" />
<span class="select-label">课程章节:</span>
<span class="select-value" @click="showChapterPicker = true">{{ formData.chapter || '请选择章节' }}</span>
<span class="select-value" @click="showChapterPicker = true">
{{ selectedChaptersDisplay || '请选择章节' }}
</span>
<van-icon name="arrow" class="arrow-icon" @click="showChapterPicker = true" />
</div>
</div>
......@@ -202,14 +204,68 @@
</van-popup>
<!-- 课程章节选择器 -->
<van-popup v-model:show="showChapterPicker" position="bottom">
<div class="p-4">
<van-popup v-model:show="showChapterPicker" position="bottom" class="chapter-picker-popup">
<div class="chapter-picker-container">
<div class="picker-header">
<h3>批量设置课程章节</h3>
<van-search v-model="chapterSearchValue" placeholder="搜索章节" @search="searchChapter" />
<van-list>
<van-cell v-for="chapter in filteredChapters" :key="chapter.id" :title="chapter.name" is-link :border="false"
@click="onChapterSelect(chapter)" />
</van-list>
</div>
<div class="chapter-list">
<div v-for="chapter in filteredAvailableChapters" :key="chapter.id" class="chapter-item">
<van-checkbox
v-model="chapter.selected"
@change="onChapterToggle(chapter)"
class="chapter-checkbox"
>
{{ chapter.name }}
</van-checkbox>
<!-- 时间设置区域 -->
<div v-if="chapter.selected" class="time-settings">
<div class="time-row">
<span class="time-label">开始时间:</span>
<van-field
v-model="chapter.startTime"
readonly
placeholder="请选择开始时间"
@click="openTimePicker(chapter, 'start')"
class="time-field"
/>
</div>
<div class="time-row">
<span class="time-label">结束时间:</span>
<van-field
v-model="chapter.endTime"
readonly
placeholder="请选择结束时间"
@click="openTimePicker(chapter, 'end')"
class="time-field"
/>
</div>
</div>
</div>
</div>
<div class="picker-footer">
<van-button @click="cancelChapterSelection" class="cancel-btn">取消</van-button>
<van-button type="primary" @click="confirmChapterSelection" class="confirm-btn">
确认选择 ({{ selectedChaptersCount }}个章节)
</van-button>
</div>
</div>
</van-popup>
<!-- 时间选择器 -->
<van-popup v-model:show="showChapterTimePicker" position="bottom">
<van-date-picker
v-model="currentChapterTime"
title="选择时间"
:min-date="minDate"
:max-date="maxDate"
@confirm="onChapterTimeConfirm"
@cancel="showChapterTimePicker = false"
/>
</van-popup>
<!-- 活动选择器 -->
......@@ -299,6 +355,7 @@ const showStartTimePicker = ref(false);
const showEndTimePicker = ref(false);
const showCoursePicker = ref(false);
const showChapterPicker = ref(false);
const showChapterTimePicker = ref(false);
const showActivityPicker = ref(false);
const showGradePicker = ref(false);
const showClassPicker = ref(false);
......@@ -341,25 +398,44 @@ const courses = ref([
{ id: 4, name: '物理课程' }
]);
// 章节数据 - 独立的章节列表,不与课程关联
const chapters = ref([
{ id: 1, name: '第一章 数与代数' },
{ id: 2, name: '第二章 几何图形' },
{ id: 3, name: '第三章 统计与概率' },
{ id: 4, name: '第四章 函数与方程' },
{ id: 5, name: '第一单元 现代文阅读' },
{ id: 6, name: '第二单元 古诗文阅读' },
{ id: 7, name: '第三单元 写作训练' },
{ id: 8, name: '第四单元 口语交际' },
{ id: 9, name: 'Unit 1 Hello World' },
{ id: 10, name: 'Unit 2 My Family' },
{ id: 11, name: 'Unit 3 School Life' },
{ id: 12, name: 'Unit 4 Hobbies' },
{ id: 13, name: '第一章 力学基础' },
{ id: 14, name: '第二章 热学原理' },
{ id: 15, name: '第三章 电磁学' },
{ id: 16, name: '第四章 光学现象' }
]);
// 章节数据 - 根据课程动态变化
const chapters = ref([]);
// 各课程对应的章节数据
const courseChapters = {
'数学课程': [
{ id: 1, name: '第一章 数与代数', selected: false, startTime: '', endTime: '' },
{ id: 2, name: '第二章 几何图形', selected: false, startTime: '', endTime: '' },
{ id: 3, name: '第三章 统计与概率', selected: false, startTime: '', endTime: '' },
{ id: 4, name: '第四章 函数与方程', selected: false, startTime: '', endTime: '' }
],
'语文课程': [
{ id: 5, name: '第一单元 现代文阅读', selected: false, startTime: '', endTime: '' },
{ id: 6, name: '第二单元 古诗文阅读', selected: false, startTime: '', endTime: '' },
{ id: 7, name: '第三单元 写作训练', selected: false, startTime: '', endTime: '' },
{ id: 8, name: '第四单元 口语交际', selected: false, startTime: '', endTime: '' }
],
'英语课程': [
{ id: 9, name: 'Unit 1 Hello World', selected: false, startTime: '', endTime: '' },
{ id: 10, name: 'Unit 2 My Family', selected: false, startTime: '', endTime: '' },
{ id: 11, name: 'Unit 3 School Life', selected: false, startTime: '', endTime: '' },
{ id: 12, name: 'Unit 4 Hobbies', selected: false, startTime: '', endTime: '' }
],
'物理课程': [
{ id: 13, name: '第一章 力学基础', selected: false, startTime: '', endTime: '' },
{ id: 14, name: '第二章 热学原理', selected: false, startTime: '', endTime: '' },
{ id: 15, name: '第三章 电磁学', selected: false, startTime: '', endTime: '' },
{ id: 16, name: '第四章 光学现象', selected: false, startTime: '', endTime: '' }
]
};
// 已选择的章节列表
const selectedChapters = ref([]);
// 当前时间选择相关
const currentChapterTime = ref(['2024', '01', '01']);
const currentChapter = ref(null);
const currentTimeType = ref(''); // 'start' or 'end'
const activities = ref([
{ id: 1, name: '春游活动' },
......@@ -415,6 +491,27 @@ const filteredChapters = computed(() => {
);
});
// 可选择的章节列表(根据当前课程)
const filteredAvailableChapters = computed(() => {
if (!chapterSearchValue.value) return chapters.value;
return chapters.value.filter(chapter =>
chapter.name.toLowerCase().includes(chapterSearchValue.value.toLowerCase())
);
});
// 已选择章节的显示文本
const selectedChaptersDisplay = computed(() => {
const count = selectedChapters.value.length;
if (count === 0) return '';
if (count === 1) return selectedChapters.value[0].name;
return `已选择 ${count} 个章节`;
});
// 已选择章节数量
const selectedChaptersCount = computed(() => {
return chapters.value.filter(chapter => chapter.selected).length;
});
const filteredActivities = computed(() => {
if (!activitySearchValue.value) return activities.value;
return activities.value.filter(activity =>
......@@ -499,18 +596,119 @@ const onEndTimeConfirm = () => {
*/
const onCourseSelect = (course) => {
formData.value.course = course.name;
// 清空之前选择的章节
selectedChapters.value = [];
// 根据选择的课程更新章节数据
chapters.value = courseChapters[course.name] ? JSON.parse(JSON.stringify(courseChapters[course.name])) : [];
showCoursePicker.value = false;
courseSearchValue.value = '';
};
/**
* 章节选择
* @param {Object} chapter - 选中的章节
* 章节切换选择状态
* @param {Object} chapter - 章节对象
*/
const onChapterToggle = (chapter) => {
// 切换选择状态的逻辑已经由 v-model 处理
console.log('章节选择状态变更:', chapter.name, chapter.selected);
};
/**
* 打开时间选择器
* @param {Object} chapter - 章节对象
* @param {string} type - 时间类型 'start' 或 'end'
*/
const openTimePicker = (chapter, type) => {
currentChapter.value = chapter;
currentTimeType.value = type;
// 设置当前时间
const timeValue = type === 'start' ? chapter.startTime : chapter.endTime;
if (timeValue) {
const date = new Date(timeValue);
currentChapterTime.value = [
date.getFullYear().toString(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0')
];
} else {
currentChapterTime.value = ['2024', '01', '01'];
}
showChapterTimePicker.value = true;
};
/**
* 确认章节时间选择
*/
const onChapterTimeConfirm = () => {
const [year, month, day] = currentChapterTime.value;
const dateStr = `${year}-${month}-${day}`;
if (currentChapter.value && currentTimeType.value) {
// 时间验证逻辑
if (currentTimeType.value === 'start') {
// 设置开始时间时,检查是否晚于结束时间
if (currentChapter.value.endTime && dateStr > currentChapter.value.endTime) {
showToast('开始时间不能晚于结束时间');
return;
}
currentChapter.value.startTime = dateStr;
} else {
// 设置结束时间时,检查是否早于开始时间
if (currentChapter.value.startTime && dateStr < currentChapter.value.startTime) {
showToast('结束时间不能早于开始时间');
return;
}
currentChapter.value.endTime = dateStr;
}
}
showChapterTimePicker.value = false;
currentChapter.value = null;
currentTimeType.value = '';
};
/**
* 取消章节选择
*/
const cancelChapterSelection = () => {
// 重置所有章节的选择状态
chapters.value.forEach(chapter => {
chapter.selected = false;
chapter.startTime = '';
chapter.endTime = '';
});
showChapterPicker.value = false;
chapterSearchValue.value = '';
};
/**
* 确认章节选择
*/
const onChapterSelect = (chapter) => {
formData.value.chapter = chapter.name;
const confirmChapterSelection = () => {
// 更新已选择的章节列表
selectedChapters.value = chapters.value.filter(chapter => chapter.selected);
// 验证已选择章节的时间设置
const invalidChapters = selectedChapters.value.filter(chapter => !chapter.startTime || !chapter.endTime);
if (invalidChapters.length > 0) {
showToast('请为所有选中的章节设置开始和结束时间');
return;
}
// 验证时间逻辑:结束时间不能早于开始时间
const timeInvalidChapters = selectedChapters.value.filter(chapter =>
chapter.startTime && chapter.endTime && chapter.endTime < chapter.startTime
);
if (timeInvalidChapters.length > 0) {
showToast('存在结束时间早于开始时间的章节,请重新设置');
return;
}
showChapterPicker.value = false;
chapterSearchValue.value = '';
showToast(`已选择 ${selectedChapters.value.length} 个章节`);
};
/**
......@@ -833,6 +1031,111 @@ onMounted(() => {
border-radius: 16px 16px 0 0;
}
/* 章节选择器样式 */
/* .chapter-picker-popup {
:deep(.van-popup) {
max-height: 80vh;
}
} */
.chapter-picker-container {
display: flex;
flex-direction: column;
height: 80vh;
background: white;
}
.picker-header {
padding: 16px;
border-bottom: 1px solid #eee;
}
.picker-header h3 {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
}
.chapter-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.chapter-item {
margin-bottom: 16px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.chapter-checkbox {
margin-bottom: 8px;
}
:deep(.chapter-checkbox .van-checkbox__label) {
font-size: 16px;
font-weight: 500;
color: #333;
}
.time-settings {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #dee2e6;
}
.time-settings .time-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.time-settings .time-label {
font-size: 14px;
color: #666;
margin-right: 12px;
min-width: 80px;
}
.time-field {
flex: 1;
background: white;
border-radius: 6px;
}
:deep(.time-field .van-field__control) {
padding: 8px 12px;
font-size: 14px;
}
.picker-footer {
display: flex;
gap: 12px;
padding: 16px;
border-top: 1px solid #eee;
background: #f8f9fa;
}
.cancel-btn {
flex: 1;
height: 44px;
border: 1px solid #ddd;
background: white;
color: #666;
}
.confirm-btn {
flex: 2;
height: 44px;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
border: none;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 480px) {
.setting-row {
......@@ -844,5 +1147,19 @@ onMounted(() => {
.select-item {
padding: 10px 12px;
}
.chapter-picker-container {
height: 85vh;
}
.time-settings .time-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.time-settings .time-label {
min-width: auto;
}
}
</style>
......