hookehuyr

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

添加批量选择章节功能,支持多选章节并为每个章节设置开始和结束时间
优化章节选择器UI,增加时间设置区域
添加时间验证逻辑,确保开始时间不晚于结束时间
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2025-01-20 10:00:00 3 * @Date: 2025-01-20 10:00:00
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2025-06-23 09:49:16 5 + * @LastEditTime: 2025-06-23 11:38:17
6 * @FilePath: /mlaj/src/views/teacher/formPage.vue 6 * @FilePath: /mlaj/src/views/teacher/formPage.vue
7 * @Description: 教师作业新增表单页面 7 * @Description: 教师作业新增表单页面
8 --> 8 -->
...@@ -122,11 +122,13 @@ ...@@ -122,11 +122,13 @@
122 <van-icon name="arrow" class="arrow-icon" @click="showCoursePicker = true" /> 122 <van-icon name="arrow" class="arrow-icon" @click="showCoursePicker = true" />
123 </div> 123 </div>
124 </div> 124 </div>
125 - <div class="select-row"> 125 + <div class="select-row" v-if="formData.course">
126 <div class="select-item"> 126 <div class="select-item">
127 <van-icon name="bookmark-o" class="select-icon" /> 127 <van-icon name="bookmark-o" class="select-icon" />
128 <span class="select-label">课程章节:</span> 128 <span class="select-label">课程章节:</span>
129 - <span class="select-value" @click="showChapterPicker = true">{{ formData.chapter || '请选择章节' }}</span> 129 + <span class="select-value" @click="showChapterPicker = true">
130 + {{ selectedChaptersDisplay || '请选择章节' }}
131 + </span>
130 <van-icon name="arrow" class="arrow-icon" @click="showChapterPicker = true" /> 132 <van-icon name="arrow" class="arrow-icon" @click="showChapterPicker = true" />
131 </div> 133 </div>
132 </div> 134 </div>
...@@ -202,14 +204,68 @@ ...@@ -202,14 +204,68 @@
202 </van-popup> 204 </van-popup>
203 205
204 <!-- 课程章节选择器 --> 206 <!-- 课程章节选择器 -->
205 - <van-popup v-model:show="showChapterPicker" position="bottom"> 207 + <van-popup v-model:show="showChapterPicker" position="bottom" class="chapter-picker-popup">
206 - <div class="p-4"> 208 + <div class="chapter-picker-container">
209 + <div class="picker-header">
210 + <h3>批量设置课程章节</h3>
207 <van-search v-model="chapterSearchValue" placeholder="搜索章节" @search="searchChapter" /> 211 <van-search v-model="chapterSearchValue" placeholder="搜索章节" @search="searchChapter" />
208 - <van-list>
209 - <van-cell v-for="chapter in filteredChapters" :key="chapter.id" :title="chapter.name" is-link :border="false"
210 - @click="onChapterSelect(chapter)" />
211 - </van-list>
212 </div> 212 </div>
213 +
214 + <div class="chapter-list">
215 + <div v-for="chapter in filteredAvailableChapters" :key="chapter.id" class="chapter-item">
216 + <van-checkbox
217 + v-model="chapter.selected"
218 + @change="onChapterToggle(chapter)"
219 + class="chapter-checkbox"
220 + >
221 + {{ chapter.name }}
222 + </van-checkbox>
223 +
224 + <!-- 时间设置区域 -->
225 + <div v-if="chapter.selected" class="time-settings">
226 + <div class="time-row">
227 + <span class="time-label">开始时间:</span>
228 + <van-field
229 + v-model="chapter.startTime"
230 + readonly
231 + placeholder="请选择开始时间"
232 + @click="openTimePicker(chapter, 'start')"
233 + class="time-field"
234 + />
235 + </div>
236 + <div class="time-row">
237 + <span class="time-label">结束时间:</span>
238 + <van-field
239 + v-model="chapter.endTime"
240 + readonly
241 + placeholder="请选择结束时间"
242 + @click="openTimePicker(chapter, 'end')"
243 + class="time-field"
244 + />
245 + </div>
246 + </div>
247 + </div>
248 + </div>
249 +
250 + <div class="picker-footer">
251 + <van-button @click="cancelChapterSelection" class="cancel-btn">取消</van-button>
252 + <van-button type="primary" @click="confirmChapterSelection" class="confirm-btn">
253 + 确认选择 ({{ selectedChaptersCount }}个章节)
254 + </van-button>
255 + </div>
256 + </div>
257 + </van-popup>
258 +
259 + <!-- 时间选择器 -->
260 + <van-popup v-model:show="showChapterTimePicker" position="bottom">
261 + <van-date-picker
262 + v-model="currentChapterTime"
263 + title="选择时间"
264 + :min-date="minDate"
265 + :max-date="maxDate"
266 + @confirm="onChapterTimeConfirm"
267 + @cancel="showChapterTimePicker = false"
268 + />
213 </van-popup> 269 </van-popup>
214 270
215 <!-- 活动选择器 --> 271 <!-- 活动选择器 -->
...@@ -299,6 +355,7 @@ const showStartTimePicker = ref(false); ...@@ -299,6 +355,7 @@ const showStartTimePicker = ref(false);
299 const showEndTimePicker = ref(false); 355 const showEndTimePicker = ref(false);
300 const showCoursePicker = ref(false); 356 const showCoursePicker = ref(false);
301 const showChapterPicker = ref(false); 357 const showChapterPicker = ref(false);
358 +const showChapterTimePicker = ref(false);
302 const showActivityPicker = ref(false); 359 const showActivityPicker = ref(false);
303 const showGradePicker = ref(false); 360 const showGradePicker = ref(false);
304 const showClassPicker = ref(false); 361 const showClassPicker = ref(false);
...@@ -341,25 +398,44 @@ const courses = ref([ ...@@ -341,25 +398,44 @@ const courses = ref([
341 { id: 4, name: '物理课程' } 398 { id: 4, name: '物理课程' }
342 ]); 399 ]);
343 400
344 -// 章节数据 - 独立的章节列表,不与课程关联 401 +// 章节数据 - 根据课程动态变化
345 -const chapters = ref([ 402 +const chapters = ref([]);
346 - { id: 1, name: '第一章 数与代数' }, 403 +
347 - { id: 2, name: '第二章 几何图形' }, 404 +// 各课程对应的章节数据
348 - { id: 3, name: '第三章 统计与概率' }, 405 +const courseChapters = {
349 - { id: 4, name: '第四章 函数与方程' }, 406 + '数学课程': [
350 - { id: 5, name: '第一单元 现代文阅读' }, 407 + { id: 1, name: '第一章 数与代数', selected: false, startTime: '', endTime: '' },
351 - { id: 6, name: '第二单元 古诗文阅读' }, 408 + { id: 2, name: '第二章 几何图形', selected: false, startTime: '', endTime: '' },
352 - { id: 7, name: '第三单元 写作训练' }, 409 + { id: 3, name: '第三章 统计与概率', selected: false, startTime: '', endTime: '' },
353 - { id: 8, name: '第四单元 口语交际' }, 410 + { id: 4, name: '第四章 函数与方程', selected: false, startTime: '', endTime: '' }
354 - { id: 9, name: 'Unit 1 Hello World' }, 411 + ],
355 - { id: 10, name: 'Unit 2 My Family' }, 412 + '语文课程': [
356 - { id: 11, name: 'Unit 3 School Life' }, 413 + { id: 5, name: '第一单元 现代文阅读', selected: false, startTime: '', endTime: '' },
357 - { id: 12, name: 'Unit 4 Hobbies' }, 414 + { id: 6, name: '第二单元 古诗文阅读', selected: false, startTime: '', endTime: '' },
358 - { id: 13, name: '第一章 力学基础' }, 415 + { id: 7, name: '第三单元 写作训练', selected: false, startTime: '', endTime: '' },
359 - { id: 14, name: '第二章 热学原理' }, 416 + { id: 8, name: '第四单元 口语交际', selected: false, startTime: '', endTime: '' }
360 - { id: 15, name: '第三章 电磁学' }, 417 + ],
361 - { id: 16, name: '第四章 光学现象' } 418 + '英语课程': [
362 -]); 419 + { id: 9, name: 'Unit 1 Hello World', selected: false, startTime: '', endTime: '' },
420 + { id: 10, name: 'Unit 2 My Family', selected: false, startTime: '', endTime: '' },
421 + { id: 11, name: 'Unit 3 School Life', selected: false, startTime: '', endTime: '' },
422 + { id: 12, name: 'Unit 4 Hobbies', selected: false, startTime: '', endTime: '' }
423 + ],
424 + '物理课程': [
425 + { id: 13, name: '第一章 力学基础', selected: false, startTime: '', endTime: '' },
426 + { id: 14, name: '第二章 热学原理', selected: false, startTime: '', endTime: '' },
427 + { id: 15, name: '第三章 电磁学', selected: false, startTime: '', endTime: '' },
428 + { id: 16, name: '第四章 光学现象', selected: false, startTime: '', endTime: '' }
429 + ]
430 +};
431 +
432 +// 已选择的章节列表
433 +const selectedChapters = ref([]);
434 +
435 +// 当前时间选择相关
436 +const currentChapterTime = ref(['2024', '01', '01']);
437 +const currentChapter = ref(null);
438 +const currentTimeType = ref(''); // 'start' or 'end'
363 439
364 const activities = ref([ 440 const activities = ref([
365 { id: 1, name: '春游活动' }, 441 { id: 1, name: '春游活动' },
...@@ -415,6 +491,27 @@ const filteredChapters = computed(() => { ...@@ -415,6 +491,27 @@ const filteredChapters = computed(() => {
415 ); 491 );
416 }); 492 });
417 493
494 +// 可选择的章节列表(根据当前课程)
495 +const filteredAvailableChapters = computed(() => {
496 + if (!chapterSearchValue.value) return chapters.value;
497 + return chapters.value.filter(chapter =>
498 + chapter.name.toLowerCase().includes(chapterSearchValue.value.toLowerCase())
499 + );
500 +});
501 +
502 +// 已选择章节的显示文本
503 +const selectedChaptersDisplay = computed(() => {
504 + const count = selectedChapters.value.length;
505 + if (count === 0) return '';
506 + if (count === 1) return selectedChapters.value[0].name;
507 + return `已选择 ${count} 个章节`;
508 +});
509 +
510 +// 已选择章节数量
511 +const selectedChaptersCount = computed(() => {
512 + return chapters.value.filter(chapter => chapter.selected).length;
513 +});
514 +
418 const filteredActivities = computed(() => { 515 const filteredActivities = computed(() => {
419 if (!activitySearchValue.value) return activities.value; 516 if (!activitySearchValue.value) return activities.value;
420 return activities.value.filter(activity => 517 return activities.value.filter(activity =>
...@@ -499,18 +596,119 @@ const onEndTimeConfirm = () => { ...@@ -499,18 +596,119 @@ const onEndTimeConfirm = () => {
499 */ 596 */
500 const onCourseSelect = (course) => { 597 const onCourseSelect = (course) => {
501 formData.value.course = course.name; 598 formData.value.course = course.name;
599 + // 清空之前选择的章节
600 + selectedChapters.value = [];
601 + // 根据选择的课程更新章节数据
602 + chapters.value = courseChapters[course.name] ? JSON.parse(JSON.stringify(courseChapters[course.name])) : [];
502 showCoursePicker.value = false; 603 showCoursePicker.value = false;
503 courseSearchValue.value = ''; 604 courseSearchValue.value = '';
504 }; 605 };
505 606
506 /** 607 /**
507 - * 章节选择 608 + * 章节切换选择状态
508 - * @param {Object} chapter - 选中的章节 609 + * @param {Object} chapter - 章节对象
610 + */
611 +const onChapterToggle = (chapter) => {
612 + // 切换选择状态的逻辑已经由 v-model 处理
613 + console.log('章节选择状态变更:', chapter.name, chapter.selected);
614 +};
615 +
616 +/**
617 + * 打开时间选择器
618 + * @param {Object} chapter - 章节对象
619 + * @param {string} type - 时间类型 'start' 或 'end'
620 + */
621 +const openTimePicker = (chapter, type) => {
622 + currentChapter.value = chapter;
623 + currentTimeType.value = type;
624 +
625 + // 设置当前时间
626 + const timeValue = type === 'start' ? chapter.startTime : chapter.endTime;
627 + if (timeValue) {
628 + const date = new Date(timeValue);
629 + currentChapterTime.value = [
630 + date.getFullYear().toString(),
631 + (date.getMonth() + 1).toString().padStart(2, '0'),
632 + date.getDate().toString().padStart(2, '0')
633 + ];
634 + } else {
635 + currentChapterTime.value = ['2024', '01', '01'];
636 + }
637 +
638 + showChapterTimePicker.value = true;
639 +};
640 +
641 +/**
642 + * 确认章节时间选择
643 + */
644 +const onChapterTimeConfirm = () => {
645 + const [year, month, day] = currentChapterTime.value;
646 + const dateStr = `${year}-${month}-${day}`;
647 +
648 + if (currentChapter.value && currentTimeType.value) {
649 + // 时间验证逻辑
650 + if (currentTimeType.value === 'start') {
651 + // 设置开始时间时,检查是否晚于结束时间
652 + if (currentChapter.value.endTime && dateStr > currentChapter.value.endTime) {
653 + showToast('开始时间不能晚于结束时间');
654 + return;
655 + }
656 + currentChapter.value.startTime = dateStr;
657 + } else {
658 + // 设置结束时间时,检查是否早于开始时间
659 + if (currentChapter.value.startTime && dateStr < currentChapter.value.startTime) {
660 + showToast('结束时间不能早于开始时间');
661 + return;
662 + }
663 + currentChapter.value.endTime = dateStr;
664 + }
665 + }
666 +
667 + showChapterTimePicker.value = false;
668 + currentChapter.value = null;
669 + currentTimeType.value = '';
670 +};
671 +
672 +/**
673 + * 取消章节选择
674 + */
675 +const cancelChapterSelection = () => {
676 + // 重置所有章节的选择状态
677 + chapters.value.forEach(chapter => {
678 + chapter.selected = false;
679 + chapter.startTime = '';
680 + chapter.endTime = '';
681 + });
682 + showChapterPicker.value = false;
683 + chapterSearchValue.value = '';
684 +};
685 +
686 +/**
687 + * 确认章节选择
509 */ 688 */
510 -const onChapterSelect = (chapter) => { 689 +const confirmChapterSelection = () => {
511 - formData.value.chapter = chapter.name; 690 + // 更新已选择的章节列表
691 + selectedChapters.value = chapters.value.filter(chapter => chapter.selected);
692 +
693 + // 验证已选择章节的时间设置
694 + const invalidChapters = selectedChapters.value.filter(chapter => !chapter.startTime || !chapter.endTime);
695 + if (invalidChapters.length > 0) {
696 + showToast('请为所有选中的章节设置开始和结束时间');
697 + return;
698 + }
699 +
700 + // 验证时间逻辑:结束时间不能早于开始时间
701 + const timeInvalidChapters = selectedChapters.value.filter(chapter =>
702 + chapter.startTime && chapter.endTime && chapter.endTime < chapter.startTime
703 + );
704 + if (timeInvalidChapters.length > 0) {
705 + showToast('存在结束时间早于开始时间的章节,请重新设置');
706 + return;
707 + }
708 +
512 showChapterPicker.value = false; 709 showChapterPicker.value = false;
513 chapterSearchValue.value = ''; 710 chapterSearchValue.value = '';
711 + showToast(`已选择 ${selectedChapters.value.length} 个章节`);
514 }; 712 };
515 713
516 /** 714 /**
...@@ -833,6 +1031,111 @@ onMounted(() => { ...@@ -833,6 +1031,111 @@ onMounted(() => {
833 border-radius: 16px 16px 0 0; 1031 border-radius: 16px 16px 0 0;
834 } 1032 }
835 1033
1034 +/* 章节选择器样式 */
1035 +/* .chapter-picker-popup {
1036 + :deep(.van-popup) {
1037 + max-height: 80vh;
1038 + }
1039 +} */
1040 +
1041 +.chapter-picker-container {
1042 + display: flex;
1043 + flex-direction: column;
1044 + height: 80vh;
1045 + background: white;
1046 +}
1047 +
1048 +.picker-header {
1049 + padding: 16px;
1050 + border-bottom: 1px solid #eee;
1051 +}
1052 +
1053 +.picker-header h3 {
1054 + margin: 0 0 12px 0;
1055 + font-size: 18px;
1056 + font-weight: 600;
1057 + color: #333;
1058 + text-align: center;
1059 +}
1060 +
1061 +.chapter-list {
1062 + flex: 1;
1063 + overflow-y: auto;
1064 + padding: 16px;
1065 +}
1066 +
1067 +.chapter-item {
1068 + margin-bottom: 16px;
1069 + padding: 12px;
1070 + background: #f8f9fa;
1071 + border-radius: 8px;
1072 + border: 1px solid #e9ecef;
1073 +}
1074 +
1075 +.chapter-checkbox {
1076 + margin-bottom: 8px;
1077 +}
1078 +
1079 +:deep(.chapter-checkbox .van-checkbox__label) {
1080 + font-size: 16px;
1081 + font-weight: 500;
1082 + color: #333;
1083 +}
1084 +
1085 +.time-settings {
1086 + margin-top: 12px;
1087 + padding-top: 12px;
1088 + border-top: 1px solid #dee2e6;
1089 +}
1090 +
1091 +.time-settings .time-row {
1092 + display: flex;
1093 + align-items: center;
1094 + margin-bottom: 8px;
1095 +}
1096 +
1097 +.time-settings .time-label {
1098 + font-size: 14px;
1099 + color: #666;
1100 + margin-right: 12px;
1101 + min-width: 80px;
1102 +}
1103 +
1104 +.time-field {
1105 + flex: 1;
1106 + background: white;
1107 + border-radius: 6px;
1108 +}
1109 +
1110 +:deep(.time-field .van-field__control) {
1111 + padding: 8px 12px;
1112 + font-size: 14px;
1113 +}
1114 +
1115 +.picker-footer {
1116 + display: flex;
1117 + gap: 12px;
1118 + padding: 16px;
1119 + border-top: 1px solid #eee;
1120 + background: #f8f9fa;
1121 +}
1122 +
1123 +.cancel-btn {
1124 + flex: 1;
1125 + height: 44px;
1126 + border: 1px solid #ddd;
1127 + background: white;
1128 + color: #666;
1129 +}
1130 +
1131 +.confirm-btn {
1132 + flex: 2;
1133 + height: 44px;
1134 + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
1135 + border: none;
1136 + font-weight: 600;
1137 +}
1138 +
836 /* 响应式设计 */ 1139 /* 响应式设计 */
837 @media (max-width: 480px) { 1140 @media (max-width: 480px) {
838 .setting-row { 1141 .setting-row {
...@@ -844,5 +1147,19 @@ onMounted(() => { ...@@ -844,5 +1147,19 @@ onMounted(() => {
844 .select-item { 1147 .select-item {
845 padding: 10px 12px; 1148 padding: 10px 12px;
846 } 1149 }
1150 +
1151 + .chapter-picker-container {
1152 + height: 85vh;
1153 + }
1154 +
1155 + .time-settings .time-row {
1156 + flex-direction: column;
1157 + align-items: flex-start;
1158 + gap: 4px;
1159 + }
1160 +
1161 + .time-settings .time-label {
1162 + min-width: auto;
1163 + }
847 } 1164 }
848 </style> 1165 </style>
......