hookehuyr

feat(teacher): 新增教师模块路由、表单页面和作业管理页面

添加教师模块相关功能,包括:
1. 在路由配置中添加教师模块路由
2. 创建教师作业新增表单页面
3. 实现教师作业管理日历视图页面
4. 添加相关Vant组件支持
...@@ -30,15 +30,20 @@ declare module 'vue' { ...@@ -30,15 +30,20 @@ declare module 'vue' {
30 UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] 30 UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default']
31 UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default'] 31 UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default']
32 VanActionSheet: typeof import('vant/es')['ActionSheet'] 32 VanActionSheet: typeof import('vant/es')['ActionSheet']
33 + VanBackTop: typeof import('vant/es')['BackTop']
33 VanButton: typeof import('vant/es')['Button'] 34 VanButton: typeof import('vant/es')['Button']
34 VanCalendar: typeof import('vant/es')['Calendar'] 35 VanCalendar: typeof import('vant/es')['Calendar']
36 + VanCell: typeof import('vant/es')['Cell']
35 VanCellGroup: typeof import('vant/es')['CellGroup'] 37 VanCellGroup: typeof import('vant/es')['CellGroup']
36 VanCheckbox: typeof import('vant/es')['Checkbox'] 38 VanCheckbox: typeof import('vant/es')['Checkbox']
37 VanCol: typeof import('vant/es')['Col'] 39 VanCol: typeof import('vant/es')['Col']
38 VanConfigProvider: typeof import('vant/es')['ConfigProvider'] 40 VanConfigProvider: typeof import('vant/es')['ConfigProvider']
39 VanDatePicker: typeof import('vant/es')['DatePicker'] 41 VanDatePicker: typeof import('vant/es')['DatePicker']
42 + VanDatetimePicker: typeof import('vant/es')['DatetimePicker']
40 VanDialog: typeof import('vant/es')['Dialog'] 43 VanDialog: typeof import('vant/es')['Dialog']
41 VanDivider: typeof import('vant/es')['Divider'] 44 VanDivider: typeof import('vant/es')['Divider']
45 + VanDropdownItem: typeof import('vant/es')['DropdownItem']
46 + VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
42 VanEmpty: typeof import('vant/es')['Empty'] 47 VanEmpty: typeof import('vant/es')['Empty']
43 VanField: typeof import('vant/es')['Field'] 48 VanField: typeof import('vant/es')['Field']
44 VanForm: typeof import('vant/es')['Form'] 49 VanForm: typeof import('vant/es')['Form']
...@@ -55,6 +60,7 @@ declare module 'vue' { ...@@ -55,6 +60,7 @@ declare module 'vue' {
55 VanProgress: typeof import('vant/es')['Progress'] 60 VanProgress: typeof import('vant/es')['Progress']
56 VanRate: typeof import('vant/es')['Rate'] 61 VanRate: typeof import('vant/es')['Rate']
57 VanRow: typeof import('vant/es')['Row'] 62 VanRow: typeof import('vant/es')['Row']
63 + VanSearch: typeof import('vant/es')['Search']
58 VanSwipe: typeof import('vant/es')['Swipe'] 64 VanSwipe: typeof import('vant/es')['Swipe']
59 VanSwipeItem: typeof import('vant/es')['SwipeItem'] 65 VanSwipeItem: typeof import('vant/es')['SwipeItem']
60 VanTab: typeof import('vant/es')['Tab'] 66 VanTab: typeof import('vant/es')['Tab']
......
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-06-13 11:24:11 4 + * @LastEditTime: 2025-06-17 16:47:13
5 * @FilePath: /mlaj/src/router/routes.js 5 * @FilePath: /mlaj/src/router/routes.js
6 * @Description: 路由地址映射配置 6 * @Description: 路由地址映射配置
7 */ 7 */
8 import checkinRoutes from './checkin' 8 import checkinRoutes from './checkin'
9 +import teacherRoutes from './teacher'
9 10
10 export const routes = [ 11 export const routes = [
11 { 12 {
...@@ -226,4 +227,5 @@ export const routes = [ ...@@ -226,4 +227,5 @@ export const routes = [
226 } 227 }
227 }, 228 },
228 ...checkinRoutes, 229 ...checkinRoutes,
230 + ...teacherRoutes,
229 ] 231 ]
......
1 +/*
2 + * @Date: 2025-06-17 16:46:50
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-06-18 11:34:36
5 + * @FilePath: /mlaj/src/router/teacher.js
6 + * @Description: 文件描述
7 + */
8 +export default [
9 + {
10 + path: '/teacher/index',
11 + name: 'Teacher',
12 + component: () => import('../views/teacher/testPage.vue'),
13 + meta: {
14 + title: '教师',
15 + requiresAuth: true
16 + },
17 + },
18 + {
19 + path: '/teacher/form',
20 + name: 'TeacherForm',
21 + component: () => import('../views/teacher/formPage.vue'),
22 + meta: {
23 + title: '教师新增作业',
24 + requiresAuth: true
25 + },
26 + },
27 +]
1 +<!--
2 + * @Author: hookehuyr hookehuyr@gmail.com
3 + * @Date: 2025-01-20 10:00:00
4 + * @LastEditors: hookehuyr hookehuyr@gmail.com
5 + * @LastEditTime: 2025-06-18 11:43:03
6 + * @FilePath: /mlaj/src/views/teacher/formPage.vue
7 + * @Description: 教师作业新增表单页面
8 +-->
9 +<template>
10 + <AppLayout title="新增作业">
11 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
12 + <div class="px-4 py-6">
13 + <FrostedGlass class="rounded-xl overflow-hidden">
14 + <div class="py-4">
15 + <van-form @submit="handleSubmit">
16 + <van-cell-group inset>
17 + <!-- 作业名称 -->
18 + <van-field
19 + v-model="formData.homework_name"
20 + name="homework_name"
21 + label="作业名称"
22 + placeholder="请输入作业名称"
23 + required
24 + :border="false"
25 + :rules="[{ required: true, message: '请输入作业名称' }]"
26 + />
27 +
28 + <!-- 类型 -->
29 + <van-field
30 + v-model="formData.type"
31 + is-link
32 + readonly
33 + name="type"
34 + label="类型"
35 + placeholder="请选择类型"
36 + :border="false"
37 + @click="showTypePicker = true"
38 + />
39 +
40 + <!-- 频次 -->
41 + <van-field
42 + v-model="formData.frequency"
43 + is-link
44 + readonly
45 + name="frequency"
46 + label="频次"
47 + placeholder="请选择频次"
48 + :border="false"
49 + @click="showFrequencyPicker = true"
50 + />
51 +
52 + <!-- 目标总数 -->
53 + <van-field
54 + v-model="formData.target_count"
55 + type="number"
56 + name="target_count"
57 + label="目标总数"
58 + :border="false"
59 + placeholder="请输入目标数量"
60 + />
61 +
62 + <!-- 开始时间 -->
63 + <van-field
64 + v-model="startTimeDisplay"
65 + is-link
66 + readonly
67 + name="start_time"
68 + label="开始时间"
69 + placeholder="请选择开始时间"
70 + :border="false"
71 + @click="showStartTimePicker = true"
72 + />
73 +
74 + <!-- 结束时间 -->
75 + <van-field
76 + v-model="endTimeDisplay"
77 + is-link
78 + readonly
79 + name="end_time"
80 + label="结束时间"
81 + placeholder="请选择结束时间"
82 + :border="false"
83 + @click="showEndTimePicker = true"
84 + />
85 +
86 + <!-- 课程 -->
87 + <van-field
88 + v-model="formData.course"
89 + is-link
90 + readonly
91 + name="course"
92 + label="课程"
93 + placeholder="请选择课程"
94 + :border="false"
95 + @click="showCoursePicker = true"
96 + />
97 +
98 + <!-- 活动 -->
99 + <van-field
100 + v-model="formData.activity"
101 + is-link
102 + readonly
103 + name="activity"
104 + label="活动"
105 + placeholder="请选择活动"
106 + :border="false"
107 + @click="showActivityPicker = true"
108 + />
109 +
110 + <!-- 年级 -->
111 + <van-field
112 + v-model="formData.grade"
113 + is-link
114 + readonly
115 + name="grade"
116 + label="年级"
117 + placeholder="请选择年级"
118 + :border="false"
119 + @click="showGradePicker = true"
120 + />
121 +
122 + <!-- 班级 -->
123 + <van-field
124 + v-model="formData.class_name"
125 + is-link
126 + readonly
127 + name="class_name"
128 + label="班级"
129 + placeholder="请选择班级"
130 + :border="false"
131 + @click="showClassPicker = true"
132 + />
133 +
134 + <!-- 小组 -->
135 + <van-field
136 + v-model="formData.group_name"
137 + is-link
138 + readonly
139 + name="group_name"
140 + label="小组"
141 + placeholder="请选择小组"
142 + :border="false"
143 + @click="showGroupPicker = true"
144 + />
145 + </van-cell-group>
146 +
147 + <!-- 提交按钮 -->
148 + <div style="margin: 16px;">
149 + <van-button
150 + native-type="submit"
151 + type="primary"
152 + block
153 + round
154 + :loading="loading"
155 + class="bg-green-500 hover:bg-green-600 transition-colors"
156 + >
157 + 确认并保存
158 + </van-button>
159 + </div>
160 + </van-form>
161 + </div>
162 + </FrostedGlass>
163 + </div>
164 + </div>
165 +
166 + <!-- 类型选择器 -->
167 + <van-popup v-model:show="showTypePicker" position="bottom">
168 + <van-picker
169 + :columns="typeOptions"
170 + @confirm="onTypeConfirm"
171 + @cancel="showTypePicker = false"
172 + />
173 + </van-popup>
174 +
175 + <!-- 频次选择器 -->
176 + <van-popup v-model:show="showFrequencyPicker" position="bottom">
177 + <van-picker
178 + :columns="frequencyOptions"
179 + @confirm="onFrequencyConfirm"
180 + @cancel="showFrequencyPicker = false"
181 + />
182 + </van-popup>
183 +
184 + <!-- 开始时间选择器 -->
185 + <van-popup v-model:show="showStartTimePicker" position="bottom">
186 + <van-picker-group
187 + title="选择开始时间"
188 + @confirm="onStartTimeConfirm"
189 + @cancel="showStartTimePicker = false"
190 + >
191 + <van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
192 + </van-picker-group>
193 + </van-popup>
194 +
195 + <!-- 结束时间选择器 -->
196 + <van-popup v-model:show="showEndTimePicker" position="bottom">
197 + <van-picker-group
198 + title="选择结束时间"
199 + @confirm="onEndTimeConfirm"
200 + @cancel="showEndTimePicker = false"
201 + >
202 + <van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
203 + </van-picker-group>
204 + </van-popup>
205 +
206 + <!-- 课程选择器 -->
207 + <van-popup v-model:show="showCoursePicker" position="bottom">
208 + <div class="p-4">
209 + <van-search
210 + v-model="courseSearchValue"
211 + placeholder="搜索课程"
212 + @search="searchCourse"
213 + />
214 + <van-list>
215 + <van-cell
216 + v-for="course in filteredCourses"
217 + :key="course.id"
218 + :title="course.name"
219 + is-link
220 + :border="false"
221 + @click="onCourseSelect(course)"
222 + />
223 + </van-list>
224 + </div>
225 + </van-popup>
226 +
227 + <!-- 活动选择器 -->
228 + <van-popup v-model:show="showActivityPicker" position="bottom">
229 + <div class="p-4">
230 + <van-search
231 + v-model="activitySearchValue"
232 + placeholder="搜索活动"
233 + @search="searchActivity"
234 + />
235 + <van-list>
236 + <van-cell
237 + v-for="activity in filteredActivities"
238 + :key="activity.id"
239 + :title="activity.name"
240 + is-link
241 + :border="false"
242 + @click="onActivitySelect(activity)"
243 + />
244 + </van-list>
245 + </div>
246 + </van-popup>
247 +
248 + <!-- 年级选择器 -->
249 + <van-popup v-model:show="showGradePicker" position="bottom">
250 + <div class="p-4">
251 + <van-search
252 + v-model="gradeSearchValue"
253 + placeholder="搜索年级"
254 + @search="searchGrade"
255 + />
256 + <van-list>
257 + <van-cell
258 + v-for="grade in filteredGrades"
259 + :key="grade.id"
260 + :title="grade.name"
261 + is-link
262 + :border="false"
263 + @click="onGradeSelect(grade)"
264 + />
265 + </van-list>
266 + </div>
267 + </van-popup>
268 +
269 + <!-- 班级选择器 -->
270 + <van-popup v-model:show="showClassPicker" position="bottom">
271 + <div class="p-4">
272 + <van-search
273 + v-model="classSearchValue"
274 + placeholder="搜索班级"
275 + @search="searchClass"
276 + />
277 + <van-list>
278 + <van-cell
279 + v-for="classItem in filteredClasses"
280 + :key="classItem.id"
281 + :title="classItem.name"
282 + is-link
283 + :border="false"
284 + @click="onClassSelect(classItem)"
285 + />
286 + </van-list>
287 + </div>
288 + </van-popup>
289 +
290 + <!-- 小组选择器 -->
291 + <van-popup v-model:show="showGroupPicker" position="bottom">
292 + <div class="p-4">
293 + <van-search
294 + v-model="groupSearchValue"
295 + placeholder="搜索小组"
296 + @search="searchGroup"
297 + />
298 + <van-list>
299 + <van-cell
300 + v-for="group in filteredGroups"
301 + :key="group.id"
302 + :title="group.name"
303 + is-link
304 + :border="false"
305 + @click="onGroupSelect(group)"
306 + />
307 + </van-list>
308 + </div>
309 + </van-popup>
310 + </AppLayout>
311 +</template>
312 +
313 +<script setup>
314 +import { ref, computed, onMounted } from 'vue';
315 +import { useRouter } from 'vue-router';
316 +import { showToast, DatePicker, Popup } from 'vant';
317 +import AppLayout from '@/components/layout/AppLayout.vue';
318 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
319 +import { useTitle } from '@vueuse/core';
320 +
321 +const $route = useRoute();
322 +const $router = useRouter();
323 +useTitle($route.meta.title || '新增作业');
324 +
325 +// 表单数据
326 +const formData = ref({
327 + homework_name: '',
328 + type: '',
329 + frequency: '',
330 + target_count: '',
331 + start_time: new Date(),
332 + end_time: new Date(),
333 + course: '',
334 + activity: '',
335 + grade: '',
336 + class_name: '',
337 + group_name: ''
338 +});
339 +
340 +// 加载状态
341 +const loading = ref(false);
342 +
343 +// 弹窗显示状态
344 +const showTypePicker = ref(false);
345 +const showFrequencyPicker = ref(false);
346 +const showStartTimePicker = ref(false);
347 +const showEndTimePicker = ref(false);
348 +const showCoursePicker = ref(false);
349 +const showActivityPicker = ref(false);
350 +const showGradePicker = ref(false);
351 +const showClassPicker = ref(false);
352 +const showGroupPicker = ref(false);
353 +
354 +// 日期选择器相关
355 +const startDate = ref(new Date());
356 +const endDate = ref(new Date());
357 +const minDate = new Date(2020, 0, 1);
358 +const maxDate = new Date(2030, 11, 31);
359 +
360 +// 选项数据
361 +const typeOptions = ref([
362 + { text: '签到', value: 'checkin' },
363 + { text: '作业', value: 'homework' },
364 + { text: '考试', value: 'exam' },
365 + { text: '活动', value: 'activity' }
366 +]);
367 +
368 +const frequencyOptions = ref([
369 + { text: '一次', value: 'once' },
370 + { text: '每日', value: 'daily' },
371 + { text: '每周', value: 'weekly' },
372 + { text: '每月', value: 'monthly' }
373 +]);
374 +
375 +// 搜索值
376 +const courseSearchValue = ref('');
377 +const activitySearchValue = ref('');
378 +const gradeSearchValue = ref('');
379 +const classSearchValue = ref('');
380 +const groupSearchValue = ref('');
381 +
382 +// 数据列表
383 +const courses = ref([
384 + { id: 1, name: '数学课程' },
385 + { id: 2, name: '语文课程' },
386 + { id: 3, name: '英语课程' },
387 + { id: 4, name: '物理课程' }
388 +]);
389 +
390 +const activities = ref([
391 + { id: 1, name: '春游活动' },
392 + { id: 2, name: '运动会' },
393 + { id: 3, name: '文艺汇演' },
394 + { id: 4, name: '科技节' }
395 +]);
396 +
397 +const grades = ref([
398 + { id: 1, name: '一年级' },
399 + { id: 2, name: '二年级' },
400 + { id: 3, name: '三年级' },
401 + { id: 4, name: '四年级' }
402 +]);
403 +
404 +const classes = ref([
405 + { id: 1, name: '一班' },
406 + { id: 2, name: '二班' },
407 + { id: 3, name: '三班' },
408 + { id: 4, name: '四班' }
409 +]);
410 +
411 +const groups = ref([
412 + { id: 1, name: '第一小组' },
413 + { id: 2, name: '第二小组' },
414 + { id: 3, name: '第三小组' },
415 + { id: 4, name: '第四小组' }
416 +]);
417 +
418 +// 计算属性 - 时间显示格式
419 +const startTimeDisplay = computed(() => {
420 + if (!formData.value.start_time) return '';
421 + return formatDateTime(formData.value.start_time);
422 +});
423 +
424 +const endTimeDisplay = computed(() => {
425 + if (!formData.value.end_time) return '';
426 + return formatDateTime(formData.value.end_time);
427 +});
428 +
429 +// 过滤后的数据
430 +const filteredCourses = computed(() => {
431 + if (!courseSearchValue.value) return courses.value;
432 + return courses.value.filter(course =>
433 + course.name.toLowerCase().includes(courseSearchValue.value.toLowerCase())
434 + );
435 +});
436 +
437 +const filteredActivities = computed(() => {
438 + if (!activitySearchValue.value) return activities.value;
439 + return activities.value.filter(activity =>
440 + activity.name.toLowerCase().includes(activitySearchValue.value.toLowerCase())
441 + );
442 +});
443 +
444 +const filteredGrades = computed(() => {
445 + if (!gradeSearchValue.value) return grades.value;
446 + return grades.value.filter(grade =>
447 + grade.name.toLowerCase().includes(gradeSearchValue.value.toLowerCase())
448 + );
449 +});
450 +
451 +const filteredClasses = computed(() => {
452 + if (!classSearchValue.value) return classes.value;
453 + return classes.value.filter(classItem =>
454 + classItem.name.toLowerCase().includes(classSearchValue.value.toLowerCase())
455 + );
456 +});
457 +
458 +const filteredGroups = computed(() => {
459 + if (!groupSearchValue.value) return groups.value;
460 + return groups.value.filter(group =>
461 + group.name.toLowerCase().includes(groupSearchValue.value.toLowerCase())
462 + );
463 +});
464 +
465 +/**
466 + * 格式化日期时间
467 + * @param {Date} date - 日期对象
468 + * @returns {string} 格式化后的日期时间字符串
469 + */
470 +const formatDateTime = (date) => {
471 + if (!date) return '';
472 + const d = new Date(date);
473 + const year = d.getFullYear();
474 + const month = String(d.getMonth() + 1).padStart(2, '0');
475 + const day = String(d.getDate()).padStart(2, '0');
476 + const hours = String(d.getHours()).padStart(2, '0');
477 + const minutes = String(d.getMinutes()).padStart(2, '0');
478 + return `${year}-${month}-${day} ${hours}:${minutes}`;
479 +};
480 +
481 +/**
482 + * 类型选择确认
483 + * @param {Object} option - 选中的选项
484 + */
485 +const onTypeConfirm = (option) => {
486 + formData.value.type = option.selectedOptions[0].text;
487 + showTypePicker.value = false;
488 +};
489 +
490 +/**
491 + * 频次选择确认
492 + * @param {Object} option - 选中的选项
493 + */
494 +const onFrequencyConfirm = (option) => {
495 + formData.value.frequency = option.selectedOptions[0].text;
496 + showFrequencyPicker.value = false;
497 +};
498 +
499 +/**
500 + * 开始时间确认
501 + */
502 +const onStartTimeConfirm = () => {
503 + formData.value.start_time = startDate.value;
504 + showStartTimePicker.value = false;
505 +};
506 +
507 +/**
508 + * 结束时间确认
509 + */
510 +const onEndTimeConfirm = () => {
511 + formData.value.end_time = endDate.value;
512 + showEndTimePicker.value = false;
513 +};
514 +
515 +/**
516 + * 课程选择
517 + * @param {Object} course - 选中的课程
518 + */
519 +const onCourseSelect = (course) => {
520 + formData.value.course = course.name;
521 + showCoursePicker.value = false;
522 + courseSearchValue.value = '';
523 +};
524 +
525 +/**
526 + * 活动选择
527 + * @param {Object} activity - 选中的活动
528 + */
529 +const onActivitySelect = (activity) => {
530 + formData.value.activity = activity.name;
531 + showActivityPicker.value = false;
532 + activitySearchValue.value = '';
533 +};
534 +
535 +/**
536 + * 年级选择
537 + * @param {Object} grade - 选中的年级
538 + */
539 +const onGradeSelect = (grade) => {
540 + formData.value.grade = grade.name;
541 + showGradePicker.value = false;
542 + gradeSearchValue.value = '';
543 +};
544 +
545 +/**
546 + * 班级选择
547 + * @param {Object} classItem - 选中的班级
548 + */
549 +const onClassSelect = (classItem) => {
550 + formData.value.class_name = classItem.name;
551 + showClassPicker.value = false;
552 + classSearchValue.value = '';
553 +};
554 +
555 +/**
556 + * 小组选择
557 + * @param {Object} group - 选中的小组
558 + */
559 +const onGroupSelect = (group) => {
560 + formData.value.group_name = group.name;
561 + showGroupPicker.value = false;
562 + groupSearchValue.value = '';
563 +};
564 +
565 +/**
566 + * 搜索课程
567 + * @param {string} value - 搜索值
568 + */
569 +const searchCourse = (value) => {
570 + courseSearchValue.value = value;
571 +};
572 +
573 +/**
574 + * 搜索活动
575 + * @param {string} value - 搜索值
576 + */
577 +const searchActivity = (value) => {
578 + activitySearchValue.value = value;
579 +};
580 +
581 +/**
582 + * 搜索年级
583 + * @param {string} value - 搜索值
584 + */
585 +const searchGrade = (value) => {
586 + gradeSearchValue.value = value;
587 +};
588 +
589 +/**
590 + * 搜索班级
591 + * @param {string} value - 搜索值
592 + */
593 +const searchClass = (value) => {
594 + classSearchValue.value = value;
595 +};
596 +
597 +/**
598 + * 搜索小组
599 + * @param {string} value - 搜索值
600 + */
601 +const searchGroup = (value) => {
602 + groupSearchValue.value = value;
603 +};
604 +
605 +/**
606 + * 表单提交处理
607 + * @param {Object} values - 表单值
608 + */
609 +const handleSubmit = async (values) => {
610 + try {
611 + loading.value = true;
612 +
613 + // 验证必填项
614 + if (!formData.value.homework_name) {
615 + showToast('请输入作业名称');
616 + return;
617 + }
618 +
619 + // 这里可以调用API提交数据
620 + console.log('提交的表单数据:', formData.value);
621 +
622 + // 模拟API调用
623 + await new Promise(resolve => setTimeout(resolve, 1000));
624 +
625 + showToast('保存成功');
626 +
627 + // 返回上一页或跳转到列表页
628 + $router.back();
629 +
630 + } catch (error) {
631 + console.error('提交失败:', error);
632 + showToast('保存失败,请重试');
633 + } finally {
634 + loading.value = false;
635 + }
636 +};
637 +
638 +/**
639 + * 组件挂载时初始化数据
640 + */
641 +onMounted(() => {
642 + // 这里可以调用API获取课程、活动、年级、班级、小组等数据
643 + console.log('页面初始化');
644 +});
645 +</script>
646 +
647 +<style scoped>
648 +/* 自定义样式 */
649 +:deep(.van-field__label) {
650 + color: #333;
651 + font-weight: 500;
652 +}
653 +
654 +:deep(.van-field--required .van-field__label::before) {
655 + color: #ee0a24;
656 +}
657 +
658 +:deep(.van-button--primary) {
659 + background: linear-gradient(135deg, #10b981 0%, #059669 100%);
660 + border: none;
661 +}
662 +
663 +:deep(.van-popup) {
664 + border-radius: 16px 16px 0 0;
665 +}
666 +
667 +:deep(.van-picker__toolbar) {
668 + border-radius: 16px 16px 0 0;
669 +}
670 +</style>
1 +<!--
2 + * @Date: 2025-05-29 15:34:17
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-06-17 20:08:40
5 + * @FilePath: /mlaj/src/views/teacher/testPage.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <AppLayout :hasTitle="false">
10 + <van-config-provider :theme-vars="themeVars">
11 + <van-dropdown-menu active-color="#4caf50">
12 + <van-dropdown-item v-model="value1" :options="option1" />
13 + <van-dropdown-item v-model="value2" :options="option2" />
14 + <van-dropdown-item v-model="value3" :options="option3" />
15 + </van-dropdown-menu>
16 +
17 + <van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false" :style="{ height: calendarHeight }"
18 + switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false"
19 + @select="onSelectDay"
20 + @click-subtitle="onClickSubtitle">
21 + </van-calendar>
22 + <div style="padding: 0 1rem;">
23 + <van-row gutter="15">
24 + <van-col span="12">
25 + <van-button type="primary" block icon="add-square">主要按钮</van-button>
26 + </van-col>
27 + <van-col span="12">
28 + <van-button type="primary" block icon="video">主要按钮</van-button>
29 + </van-col>
30 + </van-row>
31 + </div>
32 +
33 + <div v-if="showProgress" class="text-wrapper">
34 + <div class="text-header">目标进度</div>
35 + <div style="background-color: #FFF; margin-top: 1rem;">
36 + <div class="grade-percentage-main">
37 + <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
38 + <van-col span="12">
39 + <span>作业目标</span>
40 + </van-col>
41 + <van-col span="12" style="text-align: right;">
42 + <span style="font-weight: bold;">{{ progress1 }}%</span>
43 + </van-col>
44 + </van-row>
45 + <div style="overflow: hidden;">
46 + <van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
47 + </div>
48 + </div>
49 + <!-- <div class="class-percentage-main">
50 + <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
51 + <van-col span="12">
52 + <span>班级目标</span>
53 + </van-col>
54 + <van-col span="12" style="text-align: right;">
55 + <span style="font-weight: bold;">{{ progress2 }}%</span>
56 + </van-col>
57 + </van-row>
58 + <van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
59 + </div> -->
60 + <div style="padding: 0.75rem 1rem;">
61 + <van-image round width="2.8rem" height="2.8rem" :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="contain"
62 + v-for="(item, index) in teamAvatars" :key="index"
63 + :style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
64 + </div>
65 + </div>
66 + </div>
67 +
68 + <van-tabs v-model:active="active" style="margin: 0 1rem;">
69 + <van-tab title="标签 1"></van-tab>
70 + <van-tab title="标签 2"></van-tab>
71 + <van-tab title="标签 3"></van-tab>
72 + </van-tabs>
73 +
74 + <div v-if="active === 0" style="padding: 0 1rem; color: #4caf50;">
75 + <van-list
76 + v-if="checkinDataList.length"
77 + v-model:loading="loading"
78 + :finished="finished"
79 + finished-text="没有更多了"
80 + @load="onLoad"
81 + class="space-y-4"
82 + >
83 + <div class="post-card" v-for="post in checkinDataList" :key="post.id">
84 + <div class="post-header">
85 + <van-row>
86 + <van-col span="4">
87 + <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
88 + </van-col>
89 + <van-col span="17">
90 + <div class="user-info">
91 + <div class="username">{{ post.user.name }}</div>
92 + <div class="post-time">{{ post.user.time }}</div>
93 + </div>
94 + </van-col>
95 + <van-col span="3">
96 + <div v-if="post.is_my" class="post-menu">
97 + <van-icon name="edit" @click="editCheckin(post)" />
98 + <van-icon name="delete-o" @click="delCheckin(post)" />
99 + </div>
100 + </van-col>
101 + </van-row>
102 + </div>
103 + <div class="post-content">
104 + <div class="post-text">{{ post.content }}</div>
105 + <div class="post-media">
106 + <div v-if="post.images.length" class="post-images">
107 + <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
108 + @click="openImagePreview(index, post)" />
109 + </div>
110 + <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
111 + <div v-for="(v, idx) in post.videoList" :key="idx">
112 + <!-- 视频封面和播放按钮 -->
113 + <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
114 + <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
115 + :alt="v.content" class="w-full h-full object-cover" />
116 + <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
117 + @click="startPlay(v)">
118 + <div
119 + class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
120 + <van-icon name="play-circle-o" class="text-white" size="40" />
121 + </div>
122 + </div>
123 + </div>
124 + <!-- 视频播放器 -->
125 + <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
126 + :ref="el => {
127 + if(el) {
128 + // 确保不重复添加
129 + if (!videoPlayers?.includes(el)) {
130 + videoPlayers?.push(el);
131 + }
132 + }
133 + }"
134 + @onPlay="handleVideoPlay(player, post)"
135 + @onPause="handleVideoPause(post)" />
136 + </div>
137 + <AudioPlayer
138 + v-if="post.audio.length"
139 + :songs="post.audio"
140 + class="post-audio"
141 + :id="post.id"
142 + :ref="el => {
143 + if(el) {
144 + // 确保不重复添加
145 + if (!audioPlayers?.includes(el)) {
146 + audioPlayers?.push(el);
147 + }
148 + }
149 + }"
150 + @play="(player) => handleAudioPlay(player, post)"
151 + />
152 + </div>
153 + </div>
154 + <div class="post-footer">
155 + <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
156 + <span class="like-count">{{ post.likes }}</span>
157 + </div>
158 + </div>
159 + </van-list>
160 + <van-empty v-else description="暂无数据" />
161 + </div>
162 +
163 + <van-back-top right="5vw" bottom="10vh" />
164 + <div style="height: 5rem;"></div>
165 + </van-config-provider>
166 +
167 + <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog>
168 + </AppLayout>
169 +</template>
170 +
171 +<script setup>
172 +import { ref, onBeforeUnmount, onMounted, computed } from 'vue'
173 +import { useRoute, useRouter } from 'vue-router'
174 +import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant';
175 +import AppLayout from "@/components/layout/AppLayout.vue";
176 +import FrostedGlass from "@/components/ui/FrostedGlass.vue";
177 +import VideoPlayer from "@/components/ui/VideoPlayer.vue";
178 +import AudioPlayer from "@/components/ui/AudioPlayer.vue";
179 +import { useTitle } from '@vueuse/core';
180 +import dayjs from 'dayjs';
181 +
182 +import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
183 +
184 +const route = useRoute()
185 +const router = useRouter()
186 +useTitle(route.meta.title);
187 +
188 +const value1 = ref(0);
189 +const value2 = ref('a');
190 +const value3 = ref('v');
191 +const option1 = [
192 + { text: '全部商品', value: 0 },
193 + { text: '新款商品', value: 1 },
194 + { text: '活动商品', value: 2 },
195 +];
196 +const option2 = [
197 + { text: '默认排序', value: 'a' },
198 + { text: '好评排序', value: 'b' },
199 + { text: '销量排序', value: 'c' },
200 +];
201 +const option3 = [
202 + { text: '默认排序', value: 'v' },
203 + { text: '好评排序', value: 'b' },
204 + { text: '销量排序', value: 'c' },
205 +];
206 +
207 +const active = ref(0);
208 +
209 +const myRefCalendar = ref(null);
210 +
211 +// 窗口尺寸相关的响应式数据
212 +const windowHeight = ref(window.innerHeight);
213 +const windowWidth = ref(window.innerWidth);
214 +
215 +/**
216 + * 动态计算日历高度
217 + * 根据屏幕尺寸和设备类型自适应调整
218 + */
219 +const calendarHeight = computed(() => {
220 + // 获取可视窗口高度
221 + const viewportHeight = windowHeight.value;
222 +
223 + // 预留给其他内容的空间(头部、底部等)
224 + const reservedSpace = 200; // 可根据实际需要调整
225 +
226 + // 计算可用高度
227 + const availableHeight = viewportHeight - reservedSpace;
228 +
229 + // 设置最小和最大高度限制
230 + const minHeight = 300; // 最小高度
231 + const maxHeight = 500; // 最大高度
232 +
233 + // 根据屏幕宽度调整高度比例
234 + let heightRatio = 0.6; // 默认占可用高度的55%
235 +
236 + if (windowWidth.value < 375) {
237 + // 小屏手机
238 + heightRatio = 0.45;
239 + } else if (windowWidth.value < 414) {
240 + // 中等屏幕手机
241 + heightRatio = 0.42;
242 + } else if (windowWidth.value >= 768) {
243 + // 平板或更大屏幕
244 + heightRatio = 0.35;
245 + }
246 +
247 + const calculatedHeight = Math.floor(availableHeight * heightRatio);
248 +
249 + // 确保高度在合理范围内
250 + const finalHeight = Math.max(minHeight, Math.min(maxHeight, calculatedHeight));
251 +
252 + return `${finalHeight}px`;
253 +});
254 +
255 +/**
256 + * 监听窗口尺寸变化
257 + */
258 +const handleResize = () => {
259 + windowHeight.value = window.innerHeight;
260 + windowWidth.value = window.innerWidth;
261 +};
262 +
263 +// 组件挂载时添加事件监听
264 +onMounted(() => {
265 + window.addEventListener('resize', handleResize);
266 + // 监听屏幕方向变化(移动端)
267 + window.addEventListener('orientationchange', () => {
268 + // 延迟更新,等待方向变化完成
269 + setTimeout(handleResize, 100);
270 + });
271 +});
272 +
273 +// 存储所有视频播放器的引用
274 +const videoPlayers = ref([]);
275 +
276 +// 存储所有音频播放器的引用
277 +const audioPlayers = ref([]);
278 +
279 +// 组件卸载前清理播放器引用和事件监听器
280 +onBeforeUnmount(() => {
281 + // 停止所有视频和音频播放
282 + if (videoPlayers.value) {
283 + videoPlayers.value.forEach(player => {
284 + if (player && typeof player?.pause === 'function') {
285 + player?.pause();
286 + }
287 + });
288 + }
289 +
290 + stopAllAudio();
291 +
292 + // 清空引用数组
293 + if (videoPlayers.value) videoPlayers.value = [];
294 + if (audioPlayers.value) audioPlayers.value = [];
295 +
296 + // 清理事件监听器
297 + window.removeEventListener('resize', handleResize);
298 + window.removeEventListener('orientationchange', handleResize);
299 +});
300 +
301 +
302 +/**
303 + * 开始播放指定帖子的视频
304 + * @param {Object} post - 要播放视频的帖子对象
305 + */
306 +const startPlay = (post) => {
307 + // 确保checkinDataList.value是一个数组
308 + if (checkinDataList.value) {
309 + // 先暂停所有其他视频
310 + checkinDataList.value.forEach(p => {
311 + p.videoList.forEach(v => {
312 + if (v.id !== post.id) {
313 + v.isPlaying = false;
314 + }
315 + });
316 + });
317 + }
318 +
319 + // 设置当前视频为播放状态
320 + post.isPlaying = true;
321 +};
322 +
323 +/**
324 + * 处理视频播放事件
325 + * @param {Object} player - 视频播放器实例
326 + * @param {Object} post - 包含视频的帖子对象
327 + */
328 +const handleVideoPlay = (player, post) => {
329 + stopAllAudio();
330 +};
331 +
332 +/**
333 + * 处理视频暂停事件
334 + * @param {Object} post - 包含视频的帖子对象
335 + */
336 +const handleVideoPause = (post) => {
337 + // 视频暂停时不改变isPlaying状态,保持播放器可见
338 + // 这样用户可以继续从暂停处播放
339 +};
340 +
341 +/**
342 + * 停止除当前播放器外的所有其他视频
343 + * @param {Object} currentPlayer - 当前播放的视频播放器实例
344 + * @param {Object} currentPost - 当前播放的帖子对象
345 + */
346 +const stopOtherVideos = (currentPlayer, currentPost) => {
347 + // 确保videoPlayers.value是一个数组
348 + if (videoPlayers.value) {
349 + // 暂停其他视频播放器
350 + videoPlayers.value.forEach(player => {
351 + if (player !== currentPlayer && player.pause) {
352 + player.pause();
353 + }
354 + });
355 + }
356 +
357 + // 更新其他帖子的播放状态
358 + checkinDataList.value.forEach(p => {
359 + p.videoList.forEach(v => {
360 + if (v.id !== currentPost.id) {
361 + v.isPlaying = false;
362 + }
363 + });
364 + });
365 +};
366 +
367 +/**
368 + * 处理音频播放事件
369 + * @param {Object} player - 音频播放器实例
370 + * @param {Object} post - 包含音频的帖子对象
371 + */
372 +const handleAudioPlay = (player, post) => {
373 + // 停止其他音频播放
374 + stopOtherAudio(player, post);
375 +};
376 +
377 +const stopOtherAudio = (currentPlayer, currentPost) => {
378 + // 确保audioPlayers.value是一个数组
379 + if (audioPlayers.value) {
380 + // 暂停其他音频播放器
381 + audioPlayers.value.forEach(player => {
382 + if (player.id!== currentPost.id && player.pause) {
383 + player.pause();
384 + }
385 + });
386 + }
387 + // 更新其他帖子的播放状态
388 + checkinDataList.value.forEach(post => {
389 + if (post.id!== currentPost.id) {
390 + post.isPlaying = false;
391 + }
392 + });
393 + // 停止所有视频播放
394 + stopAllVideos();
395 +}
396 +
397 +const stopAllAudio = () => {
398 + // 确保audioPlayers.value是一个数组
399 + if (!audioPlayers.value) return;
400 + audioPlayers.value?.forEach(player => {
401 + // 使用组件暴露的pause方法
402 + if (typeof player.pause === 'function') {
403 + player.pause();
404 + }
405 + });
406 + // 更新所有帖子的播放状态
407 + checkinDataList.value.forEach(post => {
408 + if (post.audio.length) {
409 + post.isPlaying = false;
410 + }
411 + });
412 +}
413 +
414 +/**
415 + * 停止所有视频播放
416 + */
417 +const stopAllVideos = () => {
418 + // 确保videoPlayers.value是一个数组
419 + if (!videoPlayers.value) return;
420 +
421 + // 更新所有帖子的播放状态
422 + checkinDataList.value.forEach(p => {
423 + p.videoList.forEach(v => {
424 + v.isPlaying = false;
425 + });
426 + });
427 +};
428 +
429 +const themeVars = {
430 + calendarSelectedDayBackground: '#4caf50',
431 + calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
432 + calendarInfoLineHeight: '0.3rem',
433 +}
434 +
435 +const progress1 = ref(0);
436 +// const progress2 = ref(76);
437 +
438 +const teamAvatars = ref([])
439 +
440 +// 图片预览相关
441 +const showImagePreview = ref(false);
442 +const startPosition = ref(0);
443 +const currentPost = ref(null);
444 +
445 +// 打开图片预览
446 +const openImagePreview = (index, post) => {
447 + currentPost.value = post;
448 + startPosition.value = index;
449 + showImagePreview.value = true;
450 +}
451 +
452 +// 图片切换事件处理
453 +const onChange = (index) => {
454 + startPosition.value = index;
455 +}
456 +const formatter = (day) => {
457 + const year = day.date.getFullYear();
458 + const month = day.date.getMonth() + 1;
459 + const date = day.date.getDate();
460 +
461 + let checkin_days = myCheckinDates.value;
462 +
463 + // 检查当前日期是否在签到日期列表中
464 + if (checkin_days && checkin_days.length > 0) {
465 + // 格式化当前日期为YYYY-MM-DD格式,与checkin_days中的格式匹配
466 + const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`;
467 +
468 + // 检查是否已签到
469 + if (checkin_days.includes(formattedDate)) {
470 + day.className = 'calendar-checkin';
471 + day.type = 'selected';
472 + day.bottomInfo = '已签到';
473 + }
474 + }
475 +
476 + // 选中今天的日期
477 + if (dayjs(day.date).isSame(new Date(), 'day')) {
478 + day.className = 'calendar-today';
479 + day.type = 'selected';
480 + day.bottomInfo = '今日';
481 + }
482 +
483 + return day;
484 +}
485 +
486 +const onSelectDay = (day) => {
487 + getTaskDetail(dayjs(day).format('YYYY-MM'));
488 + // 修改浏览器地址把当前的date加入地址栏, 页面不刷新
489 + router.push({
490 + path: route.path,
491 + query: {
492 + ...route.query,
493 + date: dayjs(day).format('YYYY-MM-DD')
494 + }
495 + })
496 + // 重置分页参数
497 + page.value = 0
498 + checkinDataList.value = []
499 + finished.value = false
500 + // 重新加载数据
501 + onLoad(dayjs(day).format('YYYY-MM-DD'))
502 +}
503 +
504 +const onClickSubtitle = (evt) => {
505 + console.warn('点击了日期标题');
506 +}
507 +
508 +const handLike = async (post) => {
509 + if (!post.is_liked) {
510 + const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
511 + if (code) {
512 + showSuccessToast('点赞成功')
513 + post.likes++;
514 + post.is_liked = true;
515 + }
516 + } else {
517 + const { code, data } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id, })
518 + if (code) {
519 + showSuccessToast('取消点赞成功')
520 + post.likes--;
521 + post.is_liked = false;
522 + }
523 + }
524 +}
525 +
526 +const editCheckin = (post) => {
527 + if (post.file_type === 'image') {
528 + router.push({
529 + path: '/checkin/image',
530 + query: {
531 + post_id: post.id,
532 + type: 'image',
533 + status: 'edit',
534 + }
535 + })
536 + } else if (post.file_type === 'video') {
537 + router.push({
538 + path: '/checkin/video',
539 + query: {
540 + post_id: post.id,
541 + type: 'video',
542 + status: 'edit',
543 + }
544 + })
545 + } else if (post.file_type === 'audio') {
546 + router.push({
547 + path: '/checkin/audio',
548 + query: {
549 + post_id: post.id,
550 + type: 'audio',
551 + status: 'edit',
552 + }
553 + })
554 + }
555 +}
556 +
557 +const delCheckin = (post) => {
558 + showConfirmDialog({
559 + title: '温馨提示',
560 + message: '您是否确定要删除该动态?',
561 + confirmButtonColor: '#4caf50',
562 + })
563 + .then(async () => {
564 + // 调用接口
565 + const { code, data } = await delUploadTaskInfoAPI({ i: post.id });
566 + if (code) {
567 + // 删除成功后,刷新页面
568 + showSuccessToast('删除成功');
569 + // router.go(0);
570 + // 删除post_id相应的数据
571 + checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id);
572 + } else {
573 + showErrorToast('删除失败');
574 + }
575 + })
576 + .catch(() => {
577 + // on cancel
578 + });
579 +}
580 +
581 +const taskDetail = ref({});
582 +const myCheckinDates = ref([]);
583 +const checkinDataList = ref([]);
584 +const showProgress = ref(true);
585 +
586 +const getTaskDetail = async (month) => {
587 + const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
588 + if (code) {
589 + taskDetail.value = data;
590 + progress1.value = ((data.checkin_number/data.target_number)*100).toFixed(1); // 计算进度条百分比
591 + showProgress.value = !isNaN(progress1.value); // 如果是NaN,就不显示进度条
592 + teamAvatars.value = data.checkin_avatars;
593 + // 获取当前用户的打卡日期
594 + myCheckinDates.value = data.my_checkin_dates;
595 + // 把['2025-06-06'] 转化为 [6] 只取日期去掉0
596 + // myCheckinDates.value = myCheckinDates.value.map(date => {
597 + // return dayjs(date).date();
598 + // })
599 + }
600 +}
601 +
602 +const loading = ref(false)
603 +const finished = ref(false)
604 +const limit = ref(3)
605 +const page = ref(0)
606 +
607 +const onLoad = async (date) => {
608 + const nextPage = page.value;
609 + const current_date = date || route.query.date || dayjs().format('YYYY-MM-DD');
610 + //
611 + const res = await getUploadTaskListAPI({
612 + limit: limit.value,
613 + page: nextPage,
614 + task_id: route.query.id,
615 + date: current_date
616 + });
617 + if (res.code) {
618 + // 整理数据结构
619 + checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)];
620 + finished.value = res.data.checkin_list.length < limit.value;
621 + page.value = nextPage + 1;
622 + }
623 + loading.value = false;
624 +};
625 +
626 +onMounted(async () => {
627 + const current_date = route.query.date;
628 + if (current_date) {
629 + getTaskDetail(dayjs(current_date).format('YYYY-MM'));
630 + myRefCalendar.value?.reset(new Date(current_date));
631 + onLoad(current_date);
632 + } else {
633 + getTaskDetail(dayjs().format('YYYY-MM'));
634 + onLoad(dayjs().format('YYYY-MM-DD'));
635 + }
636 +})
637 +
638 +const formatData = (data) => {
639 + let formattedData = [];
640 + formattedData = data?.checkin_list.map((item, index) => {
641 + let images = [];
642 + let audio = [];
643 + let videoList = [];
644 + if (item.file_type === 'image') {
645 + images = item.files.map(file => {
646 + return file.value;
647 + });
648 + } else if (item.file_type === 'video') {
649 + videoList = item.files.map(file => {
650 + return {
651 + id: file.meta_id,
652 + video: file.value,
653 + videoCover: file.cover,
654 + isPlaying: false,
655 + }
656 + })
657 + } else if (item.file_type === 'audio') {
658 + audio = item.files.map(file => {
659 + return {
660 + title: file.name ? file.name : '打卡音频',
661 + artist: file.artist ? file.artist : '',
662 + url: file.value,
663 + cover: file.cover ? file.cover : '',
664 + }
665 + })
666 + }
667 + return {
668 + id: item.id,
669 + task_id: item.task_id,
670 + user: {
671 + name: item.username,
672 + avatar: item.avatar,
673 + time: item.created_time_desc,
674 + },
675 + content: item.note,
676 + images,
677 + videoList,
678 + audio,
679 + isPlaying: false,
680 + likes: item.like_count,
681 + is_liked: item.is_like,
682 + is_my: item.is_my,
683 + file_type: item.file_type,
684 + }
685 + })
686 + return formattedData;
687 +}
688 +</script>
689 +
690 +<style lang="less">
691 +.van-back-top {
692 + background-color: #4caf50;
693 +}
694 +.calendar-checkin {
695 + .van-calendar__selected-day {
696 + background: #a2d8a3 !important;
697 + }
698 +}
699 +
700 +.calendar-today {
701 + .van-calendar__selected-day {
702 + background: #FAAB0C !important;
703 + }
704 +}
705 +
706 +.text-wrapper {
707 + padding: 1rem;
708 + color: #4caf50;
709 +
710 + .text-header {
711 + font-size: 1.15rem;
712 + }
713 +
714 + .grade-percentage-main {
715 + padding: 0.75rem 1rem;
716 + }
717 +
718 + .class-percentage-main {
719 + padding: 0.75rem 1rem;
720 + }
721 +
722 + .upload-wrapper {
723 + display: flex;
724 + margin: 1rem 0;
725 + gap: 1rem;
726 + .upload-boxer {
727 + text-align: center;
728 + border: 1px solid #a2d8a3;
729 + border-radius: 5px;
730 + padding: 1rem 0;
731 + flex: 1;
732 + background-color: #fff;
733 + }
734 + }
735 +}
736 +
737 +.post-card {
738 + margin: 1rem 0;
739 + padding: 1rem;
740 + background-color: #FFF;
741 + border-radius: 5px;
742 +
743 + .post-header {
744 + margin-bottom: 1rem;
745 + }
746 +
747 + .user-info {
748 + margin-left: 0.5rem;
749 +
750 + .username {
751 + font-weight: 500;
752 + }
753 +
754 + .post-time {
755 + color: gray;
756 + font-size: 0.8rem;
757 + }
758 + }
759 +
760 + .post-menu {
761 + display: flex;
762 + justify-content: space-between;
763 + align-items: center;
764 + margin-bottom: 1rem;
765 + }
766 +
767 + .post-content {
768 + .post-text {
769 + color: #666;
770 + margin-bottom: 1rem;
771 + }
772 +
773 + .post-media {
774 + .post-images {
775 + display: flex;
776 + flex-wrap: wrap;
777 + gap: 0.5rem;
778 + }
779 +
780 + .post-video {
781 + margin: 1rem 0;
782 + width: 100%;
783 + border-radius: 8px;
784 + overflow: hidden;
785 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
786 + }
787 +
788 + .post-audio {
789 + margin: 1rem 0;
790 + }
791 + }
792 + }
793 +
794 + .post-footer {
795 + margin-top: 1rem;
796 + color: #666;
797 +
798 + .like-icon {
799 + margin-right: 0.25rem;
800 + }
801 +
802 + .like-count {
803 + font-size: 0.9rem;
804 + }
805 + }
806 +}
807 +</style>