feat(teacher): 新增教师模块打卡管理和我的班级功能
- 添加打卡管理页面,包含日历视图、打卡统计和学生动态 - 新增我的班级页面,实现班级成员管理和数据统计展示 - 更新路由配置和组件类型声明 - 优化页面交互和样式细节
Showing
5 changed files
with
496 additions
and
40 deletions
| ... | @@ -36,6 +36,7 @@ declare module 'vue' { | ... | @@ -36,6 +36,7 @@ declare module 'vue' { |
| 36 | VanCell: typeof import('vant/es')['Cell'] | 36 | VanCell: typeof import('vant/es')['Cell'] |
| 37 | VanCellGroup: typeof import('vant/es')['CellGroup'] | 37 | VanCellGroup: typeof import('vant/es')['CellGroup'] |
| 38 | VanCheckbox: typeof import('vant/es')['Checkbox'] | 38 | VanCheckbox: typeof import('vant/es')['Checkbox'] |
| 39 | + VanCircle: typeof import('vant/es')['Circle'] | ||
| 39 | VanCol: typeof import('vant/es')['Col'] | 40 | VanCol: typeof import('vant/es')['Col'] |
| 40 | VanConfigProvider: typeof import('vant/es')['ConfigProvider'] | 41 | VanConfigProvider: typeof import('vant/es')['ConfigProvider'] |
| 41 | VanDatePicker: typeof import('vant/es')['DatePicker'] | 42 | VanDatePicker: typeof import('vant/es')['DatePicker'] |
| ... | @@ -61,9 +62,12 @@ declare module 'vue' { | ... | @@ -61,9 +62,12 @@ declare module 'vue' { |
| 61 | VanRate: typeof import('vant/es')['Rate'] | 62 | VanRate: typeof import('vant/es')['Rate'] |
| 62 | VanRow: typeof import('vant/es')['Row'] | 63 | VanRow: typeof import('vant/es')['Row'] |
| 63 | VanSearch: typeof import('vant/es')['Search'] | 64 | VanSearch: typeof import('vant/es')['Search'] |
| 65 | + VanSticky: typeof import('vant/es')['Sticky'] | ||
| 64 | VanSwipe: typeof import('vant/es')['Swipe'] | 66 | VanSwipe: typeof import('vant/es')['Swipe'] |
| 65 | VanSwipeItem: typeof import('vant/es')['SwipeItem'] | 67 | VanSwipeItem: typeof import('vant/es')['SwipeItem'] |
| 66 | VanTab: typeof import('vant/es')['Tab'] | 68 | VanTab: typeof import('vant/es')['Tab'] |
| 69 | + VanTabbar: typeof import('vant/es')['Tabbar'] | ||
| 70 | + VanTabbarItem: typeof import('vant/es')['TabbarItem'] | ||
| 67 | VanTabs: typeof import('vant/es')['Tabs'] | 71 | VanTabs: typeof import('vant/es')['Tabs'] |
| 68 | VanTag: typeof import('vant/es')['Tag'] | 72 | VanTag: typeof import('vant/es')['Tag'] |
| 69 | VanUploader: typeof import('vant/es')['Uploader'] | 73 | VanUploader: typeof import('vant/es')['Uploader'] | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-17 16:46:50 | 2 | * @Date: 2025-06-17 16:46:50 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-06-18 11:34:36 | 4 | + * @LastEditTime: 2025-06-19 13:23:04 |
| 5 | * @FilePath: /mlaj/src/router/teacher.js | 5 | * @FilePath: /mlaj/src/router/teacher.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| 8 | export default [ | 8 | export default [ |
| 9 | { | 9 | { |
| 10 | - path: '/teacher/index', | 10 | + path: '/teacher/checkin', |
| 11 | name: 'Teacher', | 11 | name: 'Teacher', |
| 12 | - component: () => import('../views/teacher/testPage.vue'), | 12 | + component: () => import('../views/teacher/checkinPage.vue'), |
| 13 | meta: { | 13 | meta: { |
| 14 | - title: '教师', | 14 | + title: '打卡管理', |
| 15 | requiresAuth: true | 15 | requiresAuth: true |
| 16 | }, | 16 | }, |
| 17 | }, | 17 | }, |
| ... | @@ -24,4 +24,13 @@ export default [ | ... | @@ -24,4 +24,13 @@ export default [ |
| 24 | requiresAuth: true | 24 | requiresAuth: true |
| 25 | }, | 25 | }, |
| 26 | }, | 26 | }, |
| 27 | + { | ||
| 28 | + path: '/teacher/myClass', | ||
| 29 | + name: 'MyClass', | ||
| 30 | + component: () => import('../views/teacher/myClassPage.vue'), | ||
| 31 | + meta: { | ||
| 32 | + title: '我的班级', | ||
| 33 | + requiresAuth: true | ||
| 34 | + }, | ||
| 35 | + }, | ||
| 27 | ] | 36 | ] | ... | ... |
| ... | @@ -90,6 +90,15 @@ | ... | @@ -90,6 +90,15 @@ |
| 90 | 90 | ||
| 91 | <FrostedGlass class="rounded-xl overflow-hidden mb-5"> | 91 | <FrostedGlass class="rounded-xl overflow-hidden mb-5"> |
| 92 | <MenuItem | 92 | <MenuItem |
| 93 | + v-for="item in menuItems4" | ||
| 94 | + :key="item.path" | ||
| 95 | + v-bind="item" | ||
| 96 | + @click="handleMenuClick(item.path)" | ||
| 97 | + /> | ||
| 98 | + </FrostedGlass> | ||
| 99 | + | ||
| 100 | + <FrostedGlass class="rounded-xl overflow-hidden mb-5"> | ||
| 101 | + <MenuItem | ||
| 93 | v-for="item in menuItems3" | 102 | v-for="item in menuItems3" |
| 94 | :key="item.path" | 103 | :key="item.path" |
| 95 | v-bind="item" | 104 | v-bind="item" |
| ... | @@ -295,6 +304,19 @@ const menuItems3 = ref([ | ... | @@ -295,6 +304,19 @@ const menuItems3 = ref([ |
| 295 | }, | 304 | }, |
| 296 | ]); | 305 | ]); |
| 297 | 306 | ||
| 307 | +const menuItems4 = ref([ | ||
| 308 | + { | ||
| 309 | + icon: "book", | ||
| 310 | + title: "打卡管理", | ||
| 311 | + path: "/teacher/checkin", | ||
| 312 | + }, | ||
| 313 | + { | ||
| 314 | + icon: "user", | ||
| 315 | + title: "我的班级", | ||
| 316 | + path: "/teacher/myClass", | ||
| 317 | + }, | ||
| 318 | +]); | ||
| 319 | + | ||
| 298 | const handleCheckin = () => { | 320 | const handleCheckin = () => { |
| 299 | if (checkinData.value.length) { | 321 | if (checkinData.value.length) { |
| 300 | showCheckInDialog.value = true; | 322 | showCheckInDialog.value = true; | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-05-29 15:34:17 | 2 | * @Date: 2025-05-29 15:34:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-06-17 20:08:40 | 4 | + * @LastEditTime: 2025-06-19 14:49:38 |
| 5 | - * @FilePath: /mlaj/src/views/teacher/testPage.vue | 5 | + * @FilePath: /mlaj/src/views/teacher/checkinPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <AppLayout :hasTitle="false"> | 9 | <AppLayout :hasTitle="false"> |
| 10 | <van-config-provider :theme-vars="themeVars"> | 10 | <van-config-provider :theme-vars="themeVars"> |
| 11 | - <van-dropdown-menu active-color="#4caf50"> | 11 | + <van-sticky> |
| 12 | - <van-dropdown-item v-model="value1" :options="option1" /> | 12 | + <van-dropdown-menu active-color="#4caf50"> |
| 13 | - <van-dropdown-item v-model="value2" :options="option2" /> | 13 | + <van-dropdown-item v-model="value1" :options="option1" @change="handleChange1" /> |
| 14 | - <van-dropdown-item v-model="value3" :options="option3" /> | 14 | + <van-dropdown-item v-model="value2" :options="option2" @change="handleChange2" /> |
| 15 | - </van-dropdown-menu> | 15 | + <van-dropdown-item v-model="value3" :options="option3" @change="handleChange3" /> |
| 16 | + </van-dropdown-menu> | ||
| 17 | + </van-sticky> | ||
| 16 | 18 | ||
| 17 | <van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false" :style="{ height: calendarHeight }" | 19 | <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" | 20 | switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false" |
| ... | @@ -22,21 +24,21 @@ | ... | @@ -22,21 +24,21 @@ |
| 22 | <div style="padding: 0 1rem;"> | 24 | <div style="padding: 0 1rem;"> |
| 23 | <van-row gutter="15"> | 25 | <van-row gutter="15"> |
| 24 | <van-col span="12"> | 26 | <van-col span="12"> |
| 25 | - <van-button type="primary" block icon="add-square">主要按钮</van-button> | 27 | + <van-button type="primary" block icon="photo" @click="handleAdd">安排打卡</van-button> |
| 26 | </van-col> | 28 | </van-col> |
| 27 | <van-col span="12"> | 29 | <van-col span="12"> |
| 28 | - <van-button type="primary" block icon="video">主要按钮</van-button> | 30 | + <van-button type="primary" block icon="video">设置作业</van-button> |
| 29 | </van-col> | 31 | </van-col> |
| 30 | </van-row> | 32 | </van-row> |
| 31 | </div> | 33 | </div> |
| 32 | 34 | ||
| 33 | <div v-if="showProgress" class="text-wrapper"> | 35 | <div v-if="showProgress" class="text-wrapper"> |
| 34 | - <div class="text-header">目标进度</div> | 36 | + <div class="text-header">打卡统计</div> |
| 35 | <div style="background-color: #FFF; margin-top: 1rem;"> | 37 | <div style="background-color: #FFF; margin-top: 1rem;"> |
| 36 | <div class="grade-percentage-main"> | 38 | <div class="grade-percentage-main"> |
| 37 | <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;"> | 39 | <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;"> |
| 38 | <van-col span="12"> | 40 | <van-col span="12"> |
| 39 | - <span>作业目标</span> | 41 | + <span>年级目标</span> |
| 40 | </van-col> | 42 | </van-col> |
| 41 | <van-col span="12" style="text-align: right;"> | 43 | <van-col span="12" style="text-align: right;"> |
| 42 | <span style="font-weight: bold;">{{ progress1 }}%</span> | 44 | <span style="font-weight: bold;">{{ progress1 }}%</span> |
| ... | @@ -65,13 +67,8 @@ | ... | @@ -65,13 +67,8 @@ |
| 65 | </div> | 67 | </div> |
| 66 | </div> | 68 | </div> |
| 67 | 69 | ||
| 68 | - <van-tabs v-model:active="active" style="margin: 0 1rem;"> | 70 | + <div style="padding: 0 1rem; color: #4caf50;"> |
| 69 | - <van-tab title="标签 1"></van-tab> | 71 | + <div class="text-header">学生动态</div> |
| 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 | 72 | <van-list |
| 76 | v-if="checkinDataList.length" | 73 | v-if="checkinDataList.length" |
| 77 | v-model:loading="loading" | 74 | v-model:loading="loading" |
| ... | @@ -92,12 +89,12 @@ | ... | @@ -92,12 +89,12 @@ |
| 92 | <div class="post-time">{{ post.user.time }}</div> | 89 | <div class="post-time">{{ post.user.time }}</div> |
| 93 | </div> | 90 | </div> |
| 94 | </van-col> | 91 | </van-col> |
| 95 | - <van-col span="3"> | 92 | + <!-- <van-col span="3"> |
| 96 | <div v-if="post.is_my" class="post-menu"> | 93 | <div v-if="post.is_my" class="post-menu"> |
| 97 | <van-icon name="edit" @click="editCheckin(post)" /> | 94 | <van-icon name="edit" @click="editCheckin(post)" /> |
| 98 | <van-icon name="delete-o" @click="delCheckin(post)" /> | 95 | <van-icon name="delete-o" @click="delCheckin(post)" /> |
| 99 | </div> | 96 | </div> |
| 100 | - </van-col> | 97 | + </van-col> --> |
| 101 | </van-row> | 98 | </van-row> |
| 102 | </div> | 99 | </div> |
| 103 | <div class="post-content"> | 100 | <div class="post-content"> |
| ... | @@ -189,21 +186,33 @@ const value1 = ref(0); | ... | @@ -189,21 +186,33 @@ const value1 = ref(0); |
| 189 | const value2 = ref('a'); | 186 | const value2 = ref('a'); |
| 190 | const value3 = ref('v'); | 187 | const value3 = ref('v'); |
| 191 | const option1 = [ | 188 | const option1 = [ |
| 192 | - { text: '全部商品', value: 0 }, | 189 | + { text: '全部年级', value: 0 }, |
| 193 | - { text: '新款商品', value: 1 }, | 190 | + { text: '一年级', value: 1 }, |
| 194 | - { text: '活动商品', value: 2 }, | 191 | + { text: '二年级', value: 2 }, |
| 195 | ]; | 192 | ]; |
| 196 | const option2 = [ | 193 | const option2 = [ |
| 197 | - { text: '默认排序', value: 'a' }, | 194 | + { text: '全部班级', value: 'a' }, |
| 198 | - { text: '好评排序', value: 'b' }, | 195 | + { text: '一班级', value: 'b' }, |
| 199 | - { text: '销量排序', value: 'c' }, | 196 | + { text: '二班级', value: 'c' }, |
| 200 | ]; | 197 | ]; |
| 201 | const option3 = [ | 198 | const option3 = [ |
| 202 | - { text: '默认排序', value: 'v' }, | 199 | + { text: '全部课程', value: 'v' }, |
| 203 | - { text: '好评排序', value: 'b' }, | 200 | + { text: '一课程', value: 'b' }, |
| 204 | - { text: '销量排序', value: 'c' }, | 201 | + { text: '二课程', value: 'c' }, |
| 205 | ]; | 202 | ]; |
| 206 | 203 | ||
| 204 | +const handleChange1 = (val) => { | ||
| 205 | + console.log('val', val); | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +const handleChange2 = (val) => { | ||
| 209 | + console.log('val', val); | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +const handleChange3 = (val) => { | ||
| 213 | + console.log('val', val); | ||
| 214 | +} | ||
| 215 | + | ||
| 207 | const active = ref(0); | 216 | const active = ref(0); |
| 208 | 217 | ||
| 209 | const myRefCalendar = ref(null); | 218 | const myRefCalendar = ref(null); |
| ... | @@ -426,11 +435,12 @@ const stopAllVideos = () => { | ... | @@ -426,11 +435,12 @@ const stopAllVideos = () => { |
| 426 | }); | 435 | }); |
| 427 | }; | 436 | }; |
| 428 | 437 | ||
| 429 | -const themeVars = { | 438 | +const themeVars = reactive({ |
| 430 | calendarSelectedDayBackground: '#4caf50', | 439 | calendarSelectedDayBackground: '#4caf50', |
| 431 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', | 440 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', |
| 432 | calendarInfoLineHeight: '0.3rem', | 441 | calendarInfoLineHeight: '0.3rem', |
| 433 | -} | 442 | + buttonNormalFontSize: '1rem', |
| 443 | +}) | ||
| 434 | 444 | ||
| 435 | const progress1 = ref(0); | 445 | const progress1 = ref(0); |
| 436 | // const progress2 = ref(76); | 446 | // const progress2 = ref(76); |
| ... | @@ -467,9 +477,16 @@ const formatter = (day) => { | ... | @@ -467,9 +477,16 @@ const formatter = (day) => { |
| 467 | 477 | ||
| 468 | // 检查是否已签到 | 478 | // 检查是否已签到 |
| 469 | if (checkin_days.includes(formattedDate)) { | 479 | if (checkin_days.includes(formattedDate)) { |
| 470 | - day.className = 'calendar-checkin'; | 480 | + // 如果是当前选中的已签到日期,使用特殊样式 |
| 471 | - day.type = 'selected'; | 481 | + if (selectedDate.value === formattedDate) { |
| 472 | - day.bottomInfo = '已签到'; | 482 | + day.className = 'calendar-selected'; |
| 483 | + day.type = 'selected'; | ||
| 484 | + day.bottomInfo = '已签到'; | ||
| 485 | + } else { | ||
| 486 | + day.className = 'calendar-checkin'; | ||
| 487 | + day.type = 'selected'; | ||
| 488 | + day.bottomInfo = '已签到'; | ||
| 489 | + } | ||
| 473 | } | 490 | } |
| 474 | } | 491 | } |
| 475 | 492 | ||
| ... | @@ -483,14 +500,22 @@ const formatter = (day) => { | ... | @@ -483,14 +500,22 @@ const formatter = (day) => { |
| 483 | return day; | 500 | return day; |
| 484 | } | 501 | } |
| 485 | 502 | ||
| 503 | +// 添加一个响应式变量来存储当前选中的日期 | ||
| 504 | +const selectedDate = ref(''); | ||
| 505 | + | ||
| 486 | const onSelectDay = (day) => { | 506 | const onSelectDay = (day) => { |
| 487 | getTaskDetail(dayjs(day).format('YYYY-MM')); | 507 | getTaskDetail(dayjs(day).format('YYYY-MM')); |
| 508 | + | ||
| 509 | + // 更新当前选中的日期 | ||
| 510 | + const currentSelectedDate = dayjs(day).format('YYYY-MM-DD'); | ||
| 511 | + selectedDate.value = currentSelectedDate; | ||
| 512 | + | ||
| 488 | // 修改浏览器地址把当前的date加入地址栏, 页面不刷新 | 513 | // 修改浏览器地址把当前的date加入地址栏, 页面不刷新 |
| 489 | router.push({ | 514 | router.push({ |
| 490 | path: route.path, | 515 | path: route.path, |
| 491 | query: { | 516 | query: { |
| 492 | ...route.query, | 517 | ...route.query, |
| 493 | - date: dayjs(day).format('YYYY-MM-DD') | 518 | + date: currentSelectedDate |
| 494 | } | 519 | } |
| 495 | }) | 520 | }) |
| 496 | // 重置分页参数 | 521 | // 重置分页参数 |
| ... | @@ -498,9 +523,10 @@ const onSelectDay = (day) => { | ... | @@ -498,9 +523,10 @@ const onSelectDay = (day) => { |
| 498 | checkinDataList.value = [] | 523 | checkinDataList.value = [] |
| 499 | finished.value = false | 524 | finished.value = false |
| 500 | // 重新加载数据 | 525 | // 重新加载数据 |
| 501 | - onLoad(dayjs(day).format('YYYY-MM-DD')) | 526 | + onLoad(currentSelectedDate) |
| 502 | } | 527 | } |
| 503 | 528 | ||
| 529 | + | ||
| 504 | const onClickSubtitle = (evt) => { | 530 | const onClickSubtitle = (evt) => { |
| 505 | console.warn('点击了日期标题'); | 531 | console.warn('点击了日期标题'); |
| 506 | } | 532 | } |
| ... | @@ -685,6 +711,17 @@ const formatData = (data) => { | ... | @@ -685,6 +711,17 @@ const formatData = (data) => { |
| 685 | }) | 711 | }) |
| 686 | return formattedData; | 712 | return formattedData; |
| 687 | } | 713 | } |
| 714 | + | ||
| 715 | +const handleAdd = () => { | ||
| 716 | + router.push({ | ||
| 717 | + path: '/teacher/form', | ||
| 718 | + query: { | ||
| 719 | + post_id: route.query.id, | ||
| 720 | + type: 'image', | ||
| 721 | + status: 'add', | ||
| 722 | + } | ||
| 723 | + }) | ||
| 724 | +} | ||
| 688 | </script> | 725 | </script> |
| 689 | 726 | ||
| 690 | <style lang="less"> | 727 | <style lang="less"> | ... | ... |
src/views/teacher/myClassPage.vue
0 → 100644
| 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-19 16:46:06 | ||
| 6 | + * @FilePath: /mlaj/src/views/teacher/myClassPage.vue | ||
| 7 | + * @Description: 我的班级页面 | ||
| 8 | +--> | ||
| 9 | +<template> | ||
| 10 | + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"> | ||
| 11 | + <!-- 用户信息卡片 --> | ||
| 12 | + <div> | ||
| 13 | + <div class="bg-white p-4"> | ||
| 14 | + <div class="flex items-center mb-4"> | ||
| 15 | + <van-image round width="3rem" height="3rem" | ||
| 16 | + :src="userInfo.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" class="mr-3" /> | ||
| 17 | + <div class="flex-1"> | ||
| 18 | + <h2 class="text-lg font-bold text-gray-800">{{ userInfo.name }}</h2> | ||
| 19 | + <div class="flex items-center mt-1"> | ||
| 20 | + <van-icon name="clock-o" size="20" color="#10b981" class="mr-1" /> | ||
| 21 | + <van-icon name="chat-o" size="20" color="#10b981" class="mr-1" /> | ||
| 22 | + <van-icon name="comment-circle-o" size="20" color="#10b981" /> | ||
| 23 | + </div> | ||
| 24 | + </div> | ||
| 25 | + </div> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + | ||
| 29 | + <!-- 筛选器 --> | ||
| 30 | + <div> | ||
| 31 | + <van-sticky> | ||
| 32 | + <van-dropdown-menu active-color="#10b981"> | ||
| 33 | + <van-dropdown-item v-model="gradeFilter" :options="gradeOptions" @change="handleGradeChange" /> | ||
| 34 | + <van-dropdown-item v-model="classFilter" :options="classOptions" @change="handleClassChange" /> | ||
| 35 | + <van-dropdown-item v-model="courseFilter" :options="courseOptions" @change="handleCourseChange" /> | ||
| 36 | + </van-dropdown-menu> | ||
| 37 | + </van-sticky> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | + <!-- 统计数据 --> | ||
| 41 | + <div> | ||
| 42 | + <van-row> | ||
| 43 | + <!-- 出勤率 --> | ||
| 44 | + <van-col span="8"> | ||
| 45 | + <div class="bg-white p-4 text-center"> | ||
| 46 | + <div class="relative w-16 h-16 mx-auto mb-2"> | ||
| 47 | + <van-circle v-model:current-rate="attendanceRate" :rate="attendanceRate" :speed="100" | ||
| 48 | + :text="attendanceRate + '%'" stroke-width="50" color="#10b981" size="64" /> | ||
| 49 | + </div> | ||
| 50 | + <div class="text-sm text-gray-600">出勤率</div> | ||
| 51 | + </div> | ||
| 52 | + </van-col> | ||
| 53 | + <!-- 任务完成 --> | ||
| 54 | + <van-col span="8"> | ||
| 55 | + <div class="bg-white p-4 text-center"> | ||
| 56 | + <div class="relative w-16 h-16 mx-auto mb-2"> | ||
| 57 | + <van-circle v-model:current-rate="taskCompletionRate" :rate="taskCompletionRate" :speed="100" | ||
| 58 | + :text="taskCompletionRate + '%'" stroke-width="50" color="#3b82f6" size="64" /> | ||
| 59 | + </div> | ||
| 60 | + <div class="text-sm text-gray-600">任务完成</div> | ||
| 61 | + </div> | ||
| 62 | + </van-col> | ||
| 63 | + <!-- 学习进度 --> | ||
| 64 | + <van-col span="8"> | ||
| 65 | + <div class="bg-white p-4 text-center"> | ||
| 66 | + <div class="relative w-16 h-16 mx-auto mb-2"> | ||
| 67 | + <van-circle v-model:current-rate="learningProgress" :rate="learningProgress" :speed="100" | ||
| 68 | + :text="learningProgress + '%'" stroke-width="50" color="#f59e0b" size="64" /> | ||
| 69 | + </div> | ||
| 70 | + <div class="text-sm text-gray-600">学习进度</div> | ||
| 71 | + </div> | ||
| 72 | + </van-col> | ||
| 73 | + </van-row> | ||
| 74 | + </div> | ||
| 75 | + | ||
| 76 | + <!-- 班级成员 --> | ||
| 77 | + <div class=""> | ||
| 78 | + <div class=""> | ||
| 79 | + <!-- 标题和搜索 --> | ||
| 80 | + <div class="p-4 border-b border-gray-100 bg-white"> | ||
| 81 | + <div class="flex items-center justify-between mb-3"> | ||
| 82 | + <h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentList.length }})</h3> | ||
| 83 | + <div @click="showSortPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer"> | ||
| 84 | + <span>{{ sortFilter }}</span> | ||
| 85 | + <van-icon name="arrow-down" size="14" class="ml-1" /> | ||
| 86 | + </div> | ||
| 87 | + </div> | ||
| 88 | + <van-search v-model="searchKeyword" placeholder="请搜索" @search="handleSearch" @input="handleSearch" /> | ||
| 89 | + </div> | ||
| 90 | + | ||
| 91 | + <!-- 学生列表 --> | ||
| 92 | + <div class="p-4"> | ||
| 93 | + <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> | ||
| 94 | + <div v-for="student in filteredStudentList" :key="student.id" | ||
| 95 | + class="flex items-center justify-between py-3 border-gray-50 bg-white rounded-xl p-4 text-center shadow-sm mb-4" | ||
| 96 | + @click="handleStudentClick(student)"> | ||
| 97 | + <div class="flex items-center flex-1"> | ||
| 98 | + <van-image round width="2.5rem" height="2.5rem" | ||
| 99 | + :src="student.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" class="mr-3" /> | ||
| 100 | + <div class="flex-1"> | ||
| 101 | + <div class="flex items-center"> | ||
| 102 | + <span class="font-medium text-gray-800 mr-2">{{ student.name }}</span> | ||
| 103 | + <van-icon v-if="student.gender === 'male'" name="friends-o" color="#3b82f6" size="14" /> | ||
| 104 | + <van-icon v-else name="like-o" color="#ec4899" size="14" /> | ||
| 105 | + </div> | ||
| 106 | + <div class="text-sm text-gray-500" style="text-align: left;">{{ student.className }}</div> | ||
| 107 | + </div> | ||
| 108 | + </div> | ||
| 109 | + <div class="text-right"> | ||
| 110 | + <div class="flex items-center text-sm text-gray-500 mb-1"> | ||
| 111 | + <van-icon name="phone-o" size="12" class="mr-1" /> | ||
| 112 | + <span>{{ formatPhone(student.phone) }}</span> | ||
| 113 | + </div> | ||
| 114 | + <div class="text-xs text-gray-400">{{ student.lastActiveTime }}</div> | ||
| 115 | + </div> | ||
| 116 | + <van-icon name="arrow" color="#d1d5db" size="16" class="ml-2" /> | ||
| 117 | + </div> | ||
| 118 | + </van-list> | ||
| 119 | + </div> | ||
| 120 | + </div> | ||
| 121 | + </div> | ||
| 122 | + | ||
| 123 | + <!-- 排序弹窗 --> | ||
| 124 | + <van-popup v-model:show="showSortPopup" position="bottom" round> | ||
| 125 | + <div class="p-4"> | ||
| 126 | + <div class="text-center text-lg font-bold mb-4">选择排序方式</div> | ||
| 127 | + <van-cell-group> | ||
| 128 | + <van-cell | ||
| 129 | + v-for="option in sortOptions" | ||
| 130 | + :key="option.value" | ||
| 131 | + :title="option.text" | ||
| 132 | + clickable | ||
| 133 | + @click="onSortSelect(option)" | ||
| 134 | + :border="false" | ||
| 135 | + :class="{ 'text-green-600': sortFilter === option.value }" | ||
| 136 | + > | ||
| 137 | + <template #right-icon> | ||
| 138 | + <van-icon v-if="sortFilter === option.value" name="success" color="#10b981" /> | ||
| 139 | + </template> | ||
| 140 | + </van-cell> | ||
| 141 | + </van-cell-group> | ||
| 142 | + <div class="mt-4"> | ||
| 143 | + <van-button block @click="showSortPopup = false">取消</van-button> | ||
| 144 | + </div> | ||
| 145 | + </div> | ||
| 146 | + </van-popup> | ||
| 147 | + </div> | ||
| 148 | +</template> | ||
| 149 | + | ||
| 150 | +<script setup> | ||
| 151 | +import { ref, computed, onMounted } from 'vue' | ||
| 152 | +import { useRouter } from 'vue-router' | ||
| 153 | +import AppLayout from '@/layouts/AppLayout.vue' | ||
| 154 | + | ||
| 155 | +const router = useRouter() | ||
| 156 | + | ||
| 157 | +// 用户信息 | ||
| 158 | +const userInfo = ref({ | ||
| 159 | + name: '我的班级', | ||
| 160 | + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 161 | +}) | ||
| 162 | + | ||
| 163 | +// 筛选器数据 | ||
| 164 | +const gradeFilter = ref('全部年级') | ||
| 165 | +const classFilter = ref('全部班级') | ||
| 166 | +const courseFilter = ref('全部课程') | ||
| 167 | +const sortFilter = ref('按活跃度') | ||
| 168 | +const searchKeyword = ref('') | ||
| 169 | +const showSortPopup = ref(false) | ||
| 170 | + | ||
| 171 | +// 筛选器选项 | ||
| 172 | +const gradeOptions = ref([ | ||
| 173 | + { text: '全部年级', value: '全部年级' }, | ||
| 174 | + { text: '高一年级', value: '高一年级' }, | ||
| 175 | + { text: '高二年级', value: '高二年级' }, | ||
| 176 | + { text: '高三年级', value: '高三年级' } | ||
| 177 | +]) | ||
| 178 | + | ||
| 179 | +const classOptions = ref([ | ||
| 180 | + { text: '全部班级', value: '全部班级' }, | ||
| 181 | + { text: '高一(1)班', value: '高一(1)班' }, | ||
| 182 | + { text: '高一(2)班', value: '高一(2)班' }, | ||
| 183 | + { text: '高一(3)班', value: '高一(3)班' } | ||
| 184 | +]) | ||
| 185 | + | ||
| 186 | +const courseOptions = ref([ | ||
| 187 | + { text: '全部课程', value: '全部课程' }, | ||
| 188 | + { text: '语文', value: '语文' }, | ||
| 189 | + { text: '数学', value: '数学' }, | ||
| 190 | + { text: '英语', value: '英语' } | ||
| 191 | +]) | ||
| 192 | + | ||
| 193 | +const sortOptions = ref([ | ||
| 194 | + { text: '按活跃度', value: '按活跃度' }, | ||
| 195 | + { text: '按姓名', value: '按姓名' }, | ||
| 196 | + { text: '按成绩', value: '按成绩' } | ||
| 197 | +]) | ||
| 198 | + | ||
| 199 | +// 统计数据 | ||
| 200 | +const attendanceRate = ref(85) | ||
| 201 | +const taskCompletionRate = ref(76) | ||
| 202 | +const learningProgress = ref(92) | ||
| 203 | + | ||
| 204 | +// 学生列表 | ||
| 205 | +const studentList = ref([ | ||
| 206 | + { | ||
| 207 | + id: 1, | ||
| 208 | + name: '张明', | ||
| 209 | + gender: 'male', | ||
| 210 | + className: '高一(3)班', | ||
| 211 | + phone: '13812345678', | ||
| 212 | + lastActiveTime: '5分钟前活跃', | ||
| 213 | + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 214 | + }, | ||
| 215 | + { | ||
| 216 | + id: 2, | ||
| 217 | + name: '李华', | ||
| 218 | + gender: 'female', | ||
| 219 | + className: '高一(3)班', | ||
| 220 | + phone: '13987654321', | ||
| 221 | + lastActiveTime: '10分钟前活跃', | ||
| 222 | + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 223 | + }, | ||
| 224 | + { | ||
| 225 | + id: 3, | ||
| 226 | + name: '王强', | ||
| 227 | + gender: 'male', | ||
| 228 | + className: '高一(2)班', | ||
| 229 | + phone: '13512349876', | ||
| 230 | + lastActiveTime: '15分钟前活跃', | ||
| 231 | + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 232 | + }, | ||
| 233 | + { | ||
| 234 | + id: 4, | ||
| 235 | + name: '赵敏', | ||
| 236 | + gender: 'female', | ||
| 237 | + className: '高一(1)班', | ||
| 238 | + phone: '13643214321', | ||
| 239 | + lastActiveTime: '30分钟前活跃', | ||
| 240 | + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 241 | + } | ||
| 242 | +]) | ||
| 243 | + | ||
| 244 | +// 列表加载状态 | ||
| 245 | +const loading = ref(false) | ||
| 246 | +const finished = ref(false) | ||
| 247 | + | ||
| 248 | +/** | ||
| 249 | + * 过滤后的学生列表 | ||
| 250 | + */ | ||
| 251 | +const filteredStudentList = computed(() => { | ||
| 252 | + let filtered = studentList.value | ||
| 253 | + | ||
| 254 | + // 按班级筛选 | ||
| 255 | + if (classFilter.value !== '全部班级') { | ||
| 256 | + filtered = filtered.filter(student => student.className === classFilter.value) | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | + // 按搜索关键词筛选 | ||
| 260 | + if (searchKeyword.value) { | ||
| 261 | + filtered = filtered.filter(student => | ||
| 262 | + student.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) | ||
| 263 | + ) | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + // 排序 | ||
| 267 | + if (sortFilter.value === '按姓名') { | ||
| 268 | + filtered.sort((a, b) => a.name.localeCompare(b.name)) | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + return filtered | ||
| 272 | +}) | ||
| 273 | + | ||
| 274 | +/** | ||
| 275 | + * 格式化手机号 | ||
| 276 | + * @param {string} phone - 手机号 | ||
| 277 | + * @returns {string} 格式化后的手机号 | ||
| 278 | + */ | ||
| 279 | +const formatPhone = (phone) => { | ||
| 280 | + if (!phone) return '' | ||
| 281 | + return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3') | ||
| 282 | +} | ||
| 283 | + | ||
| 284 | +/** | ||
| 285 | + * 处理年级筛选变化 | ||
| 286 | + * @param {string} value - 选中的年级 | ||
| 287 | + */ | ||
| 288 | +const handleGradeChange = (value) => { | ||
| 289 | + console.log('年级筛选:', value) | ||
| 290 | + // 这里可以根据年级筛选更新班级选项 | ||
| 291 | +} | ||
| 292 | + | ||
| 293 | +/** | ||
| 294 | + * 处理班级筛选变化 | ||
| 295 | + * @param {string} value - 选中的班级 | ||
| 296 | + */ | ||
| 297 | +const handleClassChange = (value) => { | ||
| 298 | + console.log('班级筛选:', value) | ||
| 299 | +} | ||
| 300 | + | ||
| 301 | +/** | ||
| 302 | + * 处理课程筛选变化 | ||
| 303 | + * @param {string} value - 选中的课程 | ||
| 304 | + */ | ||
| 305 | +const handleCourseChange = (value) => { | ||
| 306 | + console.log('课程筛选:', value) | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +/** | ||
| 310 | + * 处理排序变化 | ||
| 311 | + * @param {string} value - 选中的排序方式 | ||
| 312 | + */ | ||
| 313 | +const handleSortChange = (value) => { | ||
| 314 | + console.log('排序变化:', value) | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +/** | ||
| 318 | + * 处理排序选择 | ||
| 319 | + * @param {Object} option - 选中的排序选项 | ||
| 320 | + */ | ||
| 321 | +const onSortSelect = (option) => { | ||
| 322 | + sortFilter.value = option.value | ||
| 323 | + showSortPopup.value = false | ||
| 324 | + handleSortChange(option.value) | ||
| 325 | +} | ||
| 326 | + | ||
| 327 | +/** | ||
| 328 | + * 处理搜索 | ||
| 329 | + * @param {string} value - 搜索关键词 | ||
| 330 | + */ | ||
| 331 | +const handleSearch = (value) => { | ||
| 332 | + console.log('搜索:', value) | ||
| 333 | +} | ||
| 334 | + | ||
| 335 | +/** | ||
| 336 | + * 处理学生点击 | ||
| 337 | + * @param {Object} student - 学生信息 | ||
| 338 | + */ | ||
| 339 | +const handleStudentClick = (student) => { | ||
| 340 | + console.log('点击学生:', student) | ||
| 341 | + // 这里可以跳转到学生详情页 | ||
| 342 | +} | ||
| 343 | + | ||
| 344 | +/** | ||
| 345 | + * 加载更多数据 | ||
| 346 | + */ | ||
| 347 | +const onLoad = () => { | ||
| 348 | + setTimeout(() => { | ||
| 349 | + loading.value = false | ||
| 350 | + finished.value = true | ||
| 351 | + }, 1000) | ||
| 352 | +} | ||
| 353 | + | ||
| 354 | +/** | ||
| 355 | + * 组件挂载时初始化数据 | ||
| 356 | + */ | ||
| 357 | +onMounted(() => { | ||
| 358 | + // 这里可以调用API获取实际数据 | ||
| 359 | + console.log('我的班级页面已加载') | ||
| 360 | +}) | ||
| 361 | +</script> | ||
| 362 | + | ||
| 363 | +<style scoped> | ||
| 364 | +/* 自定义样式 */ | ||
| 365 | +.van-dropdown-menu { | ||
| 366 | + background-color: white; | ||
| 367 | + border-radius: 0.75rem; | ||
| 368 | + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); | ||
| 369 | +} | ||
| 370 | + | ||
| 371 | +.van-circle { | ||
| 372 | + font-size: 12px; | ||
| 373 | + font-weight: bold; | ||
| 374 | +} | ||
| 375 | + | ||
| 376 | +.van-search { | ||
| 377 | + background-color: #f9fafb; | ||
| 378 | + border-radius: 0.5rem; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.van-list { | ||
| 382 | + min-height: 200px; | ||
| 383 | +} | ||
| 384 | +</style> |
-
Please register or login to post a comment