feat(teacher): 新增教师端作业主页功能
- 添加作业主页路由及页面组件 - 实现作业信息展示、统计数据和日历功能 - 添加学生完成情况展示组件 - 更新任务管理页跳转逻辑 - 添加相关SVG图标和组件类型声明
Showing
7 changed files
with
526 additions
and
2 deletions
| ... | @@ -7,3 +7,8 @@ https://oa-dev.onwall.cn/f/mlaj | ... | @@ -7,3 +7,8 @@ https://oa-dev.onwall.cn/f/mlaj |
| 7 | - 教师端新增作业管理页面:路径 `/teacher/tasks`,标题“作业管理”。 | 7 | - 教师端新增作业管理页面:路径 `/teacher/tasks`,标题“作业管理”。 |
| 8 | - 列表展示:作业名称、开始时间、截止时间。 | 8 | - 列表展示:作业名称、开始时间、截止时间。 |
| 9 | - 当前数据来源为Mock,后续可替换为真实接口数据。 | 9 | - 当前数据来源为Mock,后续可替换为真实接口数据。 |
| 10 | + - 教师端新增作业主页:路径 `/teacher/tasks/:id`,标题“作业主页”。 | ||
| 11 | + - 头部:作业名称、介绍文案、细项信息(周期、频次、时间段、附件类型)。 | ||
| 12 | + - 统计:出勤率与任务完成率(参考 `myClassPage.vue` 统计样式,数据Mock)。 | ||
| 13 | + - 日历:使用 `van-calendar` 单选模式,选择日期后展示当日学生完成情况。 | ||
| 14 | + - 学生完成情况:参考图片2样式,勾选代表已完成,未勾选代表未完成(数据Mock)。 | ... | ... |
src/assets/check_corner.svg
0 → 100644
| 1 | +<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <defs> | ||
| 3 | + <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"> | ||
| 4 | + <stop offset="0%" stop-color="#10b981"/> | ||
| 5 | + <stop offset="100%" stop-color="#16a34a"/> | ||
| 6 | + </linearGradient> | ||
| 7 | + </defs> | ||
| 8 | + <!-- 右上角圆弧形底色(四分之一圆),更贴近示例图 --> | ||
| 9 | + <path d="M36 0 A36 36 0 0 1 0 36 L36 36 Z" fill="url(#g)" /> | ||
| 10 | + <!-- 白色对勾 --> | ||
| 11 | + <path d="M22 8 L28 14 L16 26" fill="none" stroke="#ffffff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" /> | ||
| 12 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -32,6 +32,7 @@ declare module 'vue' { | ... | @@ -32,6 +32,7 @@ declare module 'vue' { |
| 32 | RouterView: typeof import('vue-router')['RouterView'] | 32 | RouterView: typeof import('vue-router')['RouterView'] |
| 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| 34 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 34 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 35 | + TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] | ||
| 35 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 36 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] |
| 36 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] | 37 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] |
| 37 | UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default'] | 38 | UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default'] | ... | ... |
src/components/ui/TaskCalendar.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-11-19 21:20:00 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-11-19 21:22:29 | ||
| 5 | + * @FilePath: /mlaj/src/components/ui/TaskCalendar.vue | ||
| 6 | + * @Description: 自定义轻量日历组件(单月视图,7列栅格,点击选择日期;样式参考示例图) | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="TaskCalendar bg-white rounded-lg shadow px-4 py-4 relative"> | ||
| 10 | + <!-- 顶部:左右切月 + 月份标题 --> | ||
| 11 | + <div class="header flex items-center justify-between mb-3"> | ||
| 12 | + <van-icon name="arrow-left" class="text-gray-500" @click="go_prev_month" /> | ||
| 13 | + <div class="monthTitle text-xl font-bold text-gray-900 cursor-pointer" @click="open_month_picker">{{ month_title }}</div> | ||
| 14 | + <van-icon name="arrow" class="text-gray-500 rotate-180" @click="go_next_month" /> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <!-- 年月选择弹窗(使用 Vant DatePicker) --> | ||
| 18 | + <van-popup v-model:show="show_date_picker" position="bottom"> | ||
| 19 | + <van-picker-group | ||
| 20 | + title="选择年月" | ||
| 21 | + :tabs="['选择年月']" | ||
| 22 | + @confirm="on_confirm_year_month" | ||
| 23 | + @cancel="on_cancel_year_month" | ||
| 24 | + > | ||
| 25 | + <van-date-picker | ||
| 26 | + v-model="year_month_value" | ||
| 27 | + :min-date="min_date" | ||
| 28 | + :max-date="max_date" | ||
| 29 | + :columns-type="columns_type" | ||
| 30 | + /> | ||
| 31 | + </van-picker-group> | ||
| 32 | + </van-popup> | ||
| 33 | + | ||
| 34 | + <!-- 星期标题 --> | ||
| 35 | + <div class="weekRow grid grid-cols-7 gap-3 mb-2 text-center text-gray-500 text-sm"> | ||
| 36 | + <div v-for="w in weeks" :key="w">{{ w }}</div> | ||
| 37 | + </div> | ||
| 38 | + | ||
| 39 | + <!-- 日期网格:圆形按钮,选中高亮;无日期不显示圆但保留格子位置 --> | ||
| 40 | + <div class="daysGrid grid grid-cols-7 gap-3"> | ||
| 41 | + <div v-for="day in grid_days" :key="day.key" | ||
| 42 | + class="dayItem flex items-center justify-center rounded-full" :class="[ | ||
| 43 | + (!day.date || day.type !== 'current') ? 'invisible' : 'bg-green-100 text-gray-700', | ||
| 44 | + is_selected(day) ? 'bg-green-500 text-white' : '' | ||
| 45 | + ]" @click="on_click_day(day)"> | ||
| 46 | + <span v-if="day.date">{{ day.date.getDate() }}</span> | ||
| 47 | + </div> | ||
| 48 | + </div> | ||
| 49 | + </div> | ||
| 50 | + | ||
| 51 | +</template> | ||
| 52 | + | ||
| 53 | +<script setup> | ||
| 54 | +import { ref, computed, watch } from 'vue' | ||
| 55 | + | ||
| 56 | +/** | ||
| 57 | + * 组件对外暴露的 v-model 值:选中日期(YYYY-MM-DD) | ||
| 58 | + */ | ||
| 59 | +const props = defineProps({ | ||
| 60 | + modelValue: { type: String, default: '' } | ||
| 61 | +}) | ||
| 62 | +const emit = defineEmits(['update:modelValue', 'select']) | ||
| 63 | + | ||
| 64 | +// | ||
| 65 | +// 状态:当前面板年月与选中日期 | ||
| 66 | +// | ||
| 67 | +const selected_date = ref(props.modelValue || format_date(new Date())) | ||
| 68 | +const panel_year = ref(Number(selected_date.value.slice(0, 4))) | ||
| 69 | +const panel_month = ref(Number(selected_date.value.slice(5, 7))) | ||
| 70 | + | ||
| 71 | +watch(() => props.modelValue, (val) => { | ||
| 72 | + if (val) selected_date.value = val | ||
| 73 | +}) | ||
| 74 | + | ||
| 75 | +/** | ||
| 76 | + * 月份标题文案 | ||
| 77 | + * @returns {string} | ||
| 78 | + */ | ||
| 79 | +const month_title = computed(() => `${panel_year.value}年${panel_month.value}月`) | ||
| 80 | + | ||
| 81 | +/** | ||
| 82 | + * 星期标题 | ||
| 83 | + */ | ||
| 84 | +const weeks = ref(['日', '一', '二', '三', '四', '五', '六']) | ||
| 85 | + | ||
| 86 | +/** | ||
| 87 | + * 生成日历网格(含上月/下月占位) | ||
| 88 | + * @returns {Array<{key:string,type:string,date:Date|null}>} | ||
| 89 | + */ | ||
| 90 | +const grid_days = computed(() => { | ||
| 91 | + const first = new Date(panel_year.value, panel_month.value - 1, 1) | ||
| 92 | + const first_weekday = first.getDay() // 0..6 | ||
| 93 | + const days_in_month = new Date(panel_year.value, panel_month.value, 0).getDate() | ||
| 94 | + const days = [] | ||
| 95 | + // 上月占位 | ||
| 96 | + for (let i = 0; i < first_weekday; i++) { | ||
| 97 | + days.push({ key: `p-${i}`, type: 'prev', date: null }) | ||
| 98 | + } | ||
| 99 | + // 当月日期 | ||
| 100 | + for (let d = 1; d <= days_in_month; d++) { | ||
| 101 | + const dt = new Date(panel_year.value, panel_month.value - 1, d) | ||
| 102 | + days.push({ key: `c-${d}`, type: 'current', date: dt }) | ||
| 103 | + } | ||
| 104 | + // 末尾占位:补至整周 | ||
| 105 | + const tail = (7 - (days.length % 7)) % 7 | ||
| 106 | + for (let j = 0; j < tail; j++) { | ||
| 107 | + days.push({ key: `n-${j}`, type: 'next', date: null }) | ||
| 108 | + } | ||
| 109 | + return days | ||
| 110 | +}) | ||
| 111 | + | ||
| 112 | +/** | ||
| 113 | + * 判断是否选中该日期 | ||
| 114 | + * @param {{type:string,date:Date|null}} day | ||
| 115 | + * @returns {boolean} | ||
| 116 | + */ | ||
| 117 | +function is_selected(day) { | ||
| 118 | + if (!day.date) return false | ||
| 119 | + return format_date(day.date) === selected_date.value | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +/** | ||
| 123 | + * 点击某日期:更新选中,并向外派发事件 | ||
| 124 | + * @param {{type:string,date:Date|null}} day | ||
| 125 | + * @returns {void} | ||
| 126 | + */ | ||
| 127 | +function on_click_day(day) { | ||
| 128 | + if (!day.date) return | ||
| 129 | + const val = format_date(day.date) | ||
| 130 | + selected_date.value = val | ||
| 131 | + emit('update:modelValue', val) | ||
| 132 | + emit('select', val) | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +/** | ||
| 136 | + * 切换至上/下月 | ||
| 137 | + */ | ||
| 138 | +function go_prev_month() { | ||
| 139 | + const d = new Date(panel_year.value, panel_month.value - 2, 1) | ||
| 140 | + panel_year.value = d.getFullYear() | ||
| 141 | + panel_month.value = d.getMonth() + 1 | ||
| 142 | +} | ||
| 143 | +function go_next_month() { | ||
| 144 | + const d = new Date(panel_year.value, panel_month.value, 1) | ||
| 145 | + panel_year.value = d.getFullYear() | ||
| 146 | + panel_month.value = d.getMonth() + 1 | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +/** | ||
| 150 | + * 日期格式化为 YYYY-MM-DD | ||
| 151 | + * @param {Date} d - 日期对象 | ||
| 152 | + * @returns {string} | ||
| 153 | + */ | ||
| 154 | +function format_date(d) { | ||
| 155 | + const y = d.getFullYear() | ||
| 156 | + const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 157 | + const day = String(d.getDate()).padStart(2, '0') | ||
| 158 | + return `${y}-${m}-${day}` | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +/** | ||
| 162 | + * 年月选择弹窗相关状态 | ||
| 163 | + */ | ||
| 164 | +const show_date_picker = ref(false) | ||
| 165 | +// DatePicker 的 v-model 值(仅年/月),与 Vant 用法保持一致 | ||
| 166 | +const year_month_value = ref([String(panel_year.value), String(panel_month.value).padStart(2, '0')]) | ||
| 167 | +/** | ||
| 168 | + * DatePicker 列类型(仅年份与月份) | ||
| 169 | + * @type {string[]} | ||
| 170 | + */ | ||
| 171 | +const columns_type = ['year', 'month'] | ||
| 172 | +// 选择范围(可按需调整) | ||
| 173 | +const min_date = new Date(2020, 0, 1) | ||
| 174 | +const max_date = new Date(2035, 11, 31) | ||
| 175 | + | ||
| 176 | +/** | ||
| 177 | + * 打开年月选择弹窗 | ||
| 178 | + * @returns {void} | ||
| 179 | + */ | ||
| 180 | +function open_month_picker() { | ||
| 181 | + // 同步当前面板年月到选择器 | ||
| 182 | + year_month_value.value = [String(panel_year.value), String(panel_month.value).padStart(2, '0')] | ||
| 183 | + show_date_picker.value = true | ||
| 184 | +} | ||
| 185 | + | ||
| 186 | +/** | ||
| 187 | + * 取消选择年月 | ||
| 188 | + * @returns {void} | ||
| 189 | + */ | ||
| 190 | +function on_cancel_year_month() { | ||
| 191 | + show_date_picker.value = false | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +/** | ||
| 195 | + * 确认选择年月:更新面板年月并关闭弹窗 | ||
| 196 | + * @returns {void} | ||
| 197 | + */ | ||
| 198 | +function on_confirm_year_month() { | ||
| 199 | + const [y, m] = year_month_value.value | ||
| 200 | + panel_year.value = Number(y) | ||
| 201 | + panel_month.value = Number(m) | ||
| 202 | + show_date_picker.value = false | ||
| 203 | +} | ||
| 204 | +</script> | ||
| 205 | + | ||
| 206 | +<style lang="less" scoped> | ||
| 207 | +.TaskCalendar { | ||
| 208 | + // 防止内部内容溢出容器尺寸 | ||
| 209 | + width: 100%; | ||
| 210 | + max-width: 100%; | ||
| 211 | + overflow: hidden; | ||
| 212 | + .header { | ||
| 213 | + .monthTitle { | ||
| 214 | + line-height: 1.3; | ||
| 215 | + } | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + .weekRow { | ||
| 219 | + // 兜底栅格:确保7列布局,避免类未编译导致竖排 | ||
| 220 | + display: grid; | ||
| 221 | + grid-template-columns: repeat(7, 1fr); | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + .daysGrid { | ||
| 225 | + // 兜底栅格:确保7列布局,避免类未编译导致竖排 | ||
| 226 | + display: grid; | ||
| 227 | + grid-template-columns: repeat(7, 1fr); | ||
| 228 | + | ||
| 229 | + .dayItem { | ||
| 230 | + // 自适应圆点尺寸:不超过容器列宽与2.5rem,保持正圆 | ||
| 231 | + width: 100%; | ||
| 232 | + max-width: 2.5rem; | ||
| 233 | + aspect-ratio: 1 / 1; | ||
| 234 | + border-radius: 9999px; | ||
| 235 | + justify-self: center; | ||
| 236 | + transition: all 0.2s ease-in-out; | ||
| 237 | + } | ||
| 238 | + } | ||
| 239 | +} | ||
| 240 | +</style> |
| ... | @@ -43,6 +43,15 @@ export default [ | ... | @@ -43,6 +43,15 @@ export default [ |
| 43 | }, | 43 | }, |
| 44 | }, | 44 | }, |
| 45 | { | 45 | { |
| 46 | + path: '/teacher/tasks/:id', | ||
| 47 | + name: 'TeacherTaskHome', | ||
| 48 | + component: () => import('../views/teacher/taskHomePage.vue'), | ||
| 49 | + meta: { | ||
| 50 | + title: '作业主页', | ||
| 51 | + requiresAuth: true | ||
| 52 | + }, | ||
| 53 | + }, | ||
| 54 | + { | ||
| 46 | path: '/teacher/student/:id', | 55 | path: '/teacher/student/:id', |
| 47 | name: 'Student', | 56 | name: 'Student', |
| 48 | component: () => import('../views/teacher/studentPage.vue'), | 57 | component: () => import('../views/teacher/studentPage.vue'), | ... | ... |
src/views/teacher/taskHomePage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-11-19 21:00:00 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-11-19 21:43:15 | ||
| 5 | + * @FilePath: /mlaj/src/views/teacher/taskHomePage.vue | ||
| 6 | + * @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock) | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="TaskHomePage"> | ||
| 10 | + <!-- 头部卡片:名称、介绍、细项(参考图片1结构) --> | ||
| 11 | + <div class="headCard bg-white rounded-lg shadow px-4 py-4"> | ||
| 12 | + <div class="title text-2xl font-bold text-gray-900 mb-2">{{ task_title }}</div> | ||
| 13 | + <div class="intro text-base text-gray-700 leading-relaxed mb-3">{{ task_intro }}</div> | ||
| 14 | + <!-- 三图展示(可选),使用CDN示例地址并加压缩参数 --> | ||
| 15 | + <!-- <div class="images grid grid-cols-3 gap-3 mb-3"> | ||
| 16 | + <img v-for="(img, idx) in task_images" :key="idx" :src="img" | ||
| 17 | + class="rounded-md object-cover w-full h-24" /> | ||
| 18 | + </div> --> | ||
| 19 | + <div class="details text-sm text-gray-600"> | ||
| 20 | + <div class="detailItem">周期:{{ task_details.cycle }}</div> | ||
| 21 | + <div class="detailItem">频次:{{ task_details.frequency }}</div> | ||
| 22 | + <div class="detailItem">时间段:{{ task_details.time_range }}</div> | ||
| 23 | + <div class="detailItem">附件类型:{{ task_details.attachment_type }}</div> | ||
| 24 | + </div> | ||
| 25 | + </div> | ||
| 26 | + | ||
| 27 | + <!-- 统计数据(参考 myClassPage.vue 出勤率/任务完成) --> | ||
| 28 | + <div class="statsCard bg-white rounded-lg shadow px-4 py-4 mt-4"> | ||
| 29 | + <van-row gutter="16"> | ||
| 30 | + <!-- 出勤率 --> | ||
| 31 | + <van-col span="12"> | ||
| 32 | + <div class="text-center"> | ||
| 33 | + <div class="relative w-16 h-16 mx-auto mb-2"> | ||
| 34 | + <van-circle v-model:current-rate="checkin_count" :rate="checkin_count" :text="checkin_text" | ||
| 35 | + stroke-width="70" color="#10b981" size="64" layer-color="#eee" /> | ||
| 36 | + </div> | ||
| 37 | + <div class="text-sm text-gray-600">出勤率</div> | ||
| 38 | + </div> | ||
| 39 | + </van-col> | ||
| 40 | + <!-- 任务完成 --> | ||
| 41 | + <van-col span="12"> | ||
| 42 | + <div class="text-center"> | ||
| 43 | + <div class="relative w-16 h-16 mx-auto mb-2"> | ||
| 44 | + <van-circle v-model:current-rate="upload_count" :rate="upload_count" :text="upload_text" | ||
| 45 | + stroke-width="70" color="#3b82f6" size="64" layer-color="#eee" /> | ||
| 46 | + </div> | ||
| 47 | + <div class="text-sm text-gray-600">任务完成</div> | ||
| 48 | + </div> | ||
| 49 | + </van-col> | ||
| 50 | + </van-row> | ||
| 51 | + </div> | ||
| 52 | + | ||
| 53 | + <!-- 日历:选择日期后展示该日期的学生完成情况 --> | ||
| 54 | + <div class="calendarCard bg-white rounded-lg shadow px-4 py-4 mt-4"> | ||
| 55 | + <div class="text-base font-semibold text-gray-800 mb-2">选择日期查看完成情况</div> | ||
| 56 | + <TaskCalendar v-model="selected_date" @select="on_date_select" /> | ||
| 57 | + </div> | ||
| 58 | + | ||
| 59 | + <!-- 学生完成情况(参考图片2样式) --> | ||
| 60 | + <div class="studentsCard bg-white rounded-lg shadow px-4 py-4 mt-4"> | ||
| 61 | + <div class="flex items-center justify-between mb-3"> | ||
| 62 | + <div class="text-base font-semibold text-gray-800">完成情况({{ completed_count }}/{{ students.length }}) | ||
| 63 | + </div> | ||
| 64 | + <div class="text-xs text-gray-500">当前日期:{{ current_date_text }}</div> | ||
| 65 | + </div> | ||
| 66 | + <div class="grid grid-cols-5 gap-3 StudentsGrid"> | ||
| 67 | + <div v-for="(stu, idx) in students_status" :key="stu.id" | ||
| 68 | + class="studentItem relative rounded-md h-16 flex flex-col items-center justify-center text-center border overflow-hidden" | ||
| 69 | + :class="stu.completed ? 'bg-white border-green-500 text-green-600' : 'bg-gray-100 border-gray-300 text-gray-500'"> | ||
| 70 | + <div class="text-sm font-semibold">{{ idx + 1 }}</div> | ||
| 71 | + <div class="text-sm mt-1">{{ stu.name }}</div> | ||
| 72 | + <img v-if="stu.completed" :src="checkCorner" alt="checked" class="cornerIcon" /> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | + </div> | ||
| 76 | + </div> | ||
| 77 | +</template> | ||
| 78 | + | ||
| 79 | +<script setup> | ||
| 80 | +import { ref, computed } from 'vue' | ||
| 81 | +import { useRoute } from 'vue-router' | ||
| 82 | +import { useTitle } from '@vueuse/core' | ||
| 83 | +import TaskCalendar from '@/components/ui/TaskCalendar.vue' | ||
| 84 | +import checkCorner from '@/assets/check_corner.svg' | ||
| 85 | + | ||
| 86 | +const $route = useRoute() | ||
| 87 | +useTitle('作业主页') | ||
| 88 | + | ||
| 89 | +// | ||
| 90 | +// Mock:作业基础信息 | ||
| 91 | +// | ||
| 92 | +const task_id = $route.params.id || '0' | ||
| 93 | +const task_title = ref('组长每日共修打卡内容') | ||
| 94 | +const task_intro = ref('组长与副组长以上每日语音打卡内容:《义工宣言》《万事如意祈请文》《吉祥圆满感恩文》') | ||
| 95 | +const task_images = ref([ | ||
| 96 | + 'https://cdn.ipadbiz.cn/mlaj/demo-task-1.png?imageMogr2/thumbnail/200x/strip/quality/70', | ||
| 97 | + 'https://cdn.ipadbiz.cn/mlaj/demo-task-2.png?imageMogr2/thumbnail/200x/strip/quality/70', | ||
| 98 | + 'https://cdn.ipadbiz.cn/mlaj/demo-task-3.png?imageMogr2/thumbnail/200x/strip/quality/70' | ||
| 99 | +]) | ||
| 100 | +const task_details = ref({ | ||
| 101 | + cycle: '每天', | ||
| 102 | + frequency: '每日一次', | ||
| 103 | + time_range: '00:00 ~ 23:59', | ||
| 104 | + attachment_type: '语音/文本' | ||
| 105 | +}) | ||
| 106 | + | ||
| 107 | +// | ||
| 108 | +// Mock:统计数据 | ||
| 109 | +// | ||
| 110 | +const checkin_count = ref(56) | ||
| 111 | +const upload_count = ref(62) | ||
| 112 | +const checkin_text = computed(() => `${checkin_count.value}%`) | ||
| 113 | +const upload_text = computed(() => `${upload_count.value}%`) | ||
| 114 | + | ||
| 115 | +// | ||
| 116 | +// Mock:学生与完成记录 | ||
| 117 | +// 注:每个学生给出若干已完成的日期字符串(YYYY-MM-DD) | ||
| 118 | +// | ||
| 119 | +const today = new Date() | ||
| 120 | +const selected_date = ref(format_date(today)) | ||
| 121 | +const students = ref([ | ||
| 122 | + { id: '1', name: '王菲', completed_dates: ['2025-11-18', selected_date.value] }, | ||
| 123 | + { id: '2', name: '朱明献', completed_dates: ['2025-11-18'] }, | ||
| 124 | + { id: '3', name: '陈小云', completed_dates: [] }, | ||
| 125 | + { id: '4', name: '冯新虎', completed_dates: [selected_date.value] }, | ||
| 126 | + { id: '5', name: '罗睿', completed_dates: ['2025-11-17', '2025-11-18'] }, | ||
| 127 | + { id: '6', name: '吴绍婷', completed_dates: [] }, | ||
| 128 | + { id: '7', name: '焦淑敏', completed_dates: [selected_date.value] }, | ||
| 129 | + { id: '8', name: '李言斐', completed_dates: [] }, | ||
| 130 | + { id: '9', name: '陈正统', completed_dates: [] }, | ||
| 131 | + { id: '10', name: '杨子娟', completed_dates: [] }, | ||
| 132 | + { id: '11', name: '方萍', completed_dates: [selected_date.value] }, | ||
| 133 | + { id: '12', name: '冯静', completed_dates: [selected_date.value] }, | ||
| 134 | + { id: '13', name: '尤瑞', completed_dates: [] }, | ||
| 135 | + { id: '14', name: '鲁镇伟', completed_dates: [] }, | ||
| 136 | + { id: '15', name: '黄润', completed_dates: [selected_date.value] }, | ||
| 137 | + { id: '16', name: '王亚琼', completed_dates: [selected_date.value] }, | ||
| 138 | + { id: '17', name: '高晓云', completed_dates: [selected_date.value] }, | ||
| 139 | + { id: '18', name: '张朗', completed_dates: [] }, | ||
| 140 | + { id: '19', name: '姚娟', completed_dates: [selected_date.value] }, | ||
| 141 | + { id: '20', name: '李凯', completed_dates: [selected_date.value] }, | ||
| 142 | + { id: '21', name: '李鑫', completed_dates: [] }, | ||
| 143 | + { id: '22', name: '礼忠斌', completed_dates: [] }, | ||
| 144 | + { id: '23', name: '谭小梅', completed_dates: [selected_date.value] }, | ||
| 145 | + { id: '24', name: '赵红梅', completed_dates: [] } | ||
| 146 | +]) | ||
| 147 | + | ||
| 148 | +/** | ||
| 149 | + * 将日期对象格式化为 YYYY-MM-DD | ||
| 150 | + * @param {Date} d - 日期对象 | ||
| 151 | + * @returns {string} 格式化后的日期字符串 | ||
| 152 | + * 注释:补零并返回标准格式,便于与完成记录匹配。 | ||
| 153 | + */ | ||
| 154 | +function format_date(d) { | ||
| 155 | + const y = d.getFullYear() | ||
| 156 | + const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 157 | + const day = String(d.getDate()).padStart(2, '0') | ||
| 158 | + return `${y}-${m}-${day}` | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +/** | ||
| 162 | + * 处理日历选择事件 | ||
| 163 | + * @param {any} date - 选中的日期对象 | ||
| 164 | + * @returns {void} | ||
| 165 | + * 注释:更新选中日期,并联动学生完成情况。 | ||
| 166 | + */ | ||
| 167 | +function on_date_select(val) { | ||
| 168 | + // 自定义日历组件返回 YYYY-MM-DD 字符串 | ||
| 169 | + selected_date.value = val | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +/** | ||
| 173 | + * 计算某日期下学生完成情况列表 | ||
| 174 | + * @returns {{id:string,name:string,completed:boolean}[]} 学生状态列表 | ||
| 175 | + * 注释:根据 selected_date 在每个学生的 completed_dates 中判断是否完成。 | ||
| 176 | + */ | ||
| 177 | +const students_status = computed(() => { | ||
| 178 | + return students.value.map(stu => ({ | ||
| 179 | + id: stu.id, | ||
| 180 | + name: stu.name, | ||
| 181 | + completed: stu.completed_dates.includes(selected_date.value) | ||
| 182 | + })) | ||
| 183 | +}) | ||
| 184 | + | ||
| 185 | +/** | ||
| 186 | + * 完成人数统计文案 | ||
| 187 | + * @returns {number} 完成人数 | ||
| 188 | + */ | ||
| 189 | +const completed_count = computed(() => students_status.value.filter(s => s.completed).length) | ||
| 190 | + | ||
| 191 | +/** | ||
| 192 | + * 当前日期的展示文本 | ||
| 193 | + * @returns {string} 文本 | ||
| 194 | + */ | ||
| 195 | +const current_date_text = computed(() => selected_date.value) | ||
| 196 | +</script> | ||
| 197 | + | ||
| 198 | +<style lang="less" scoped> | ||
| 199 | +.TaskHomePage { | ||
| 200 | + min-height: 100vh; | ||
| 201 | + background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); | ||
| 202 | + padding: 1rem; | ||
| 203 | + padding-bottom: 6rem; | ||
| 204 | + | ||
| 205 | + .headCard { | ||
| 206 | + .title { | ||
| 207 | + line-height: 1.3; | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + .intro { | ||
| 211 | + color: #374151; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + .details { | ||
| 215 | + .detailItem { | ||
| 216 | + margin-bottom: 0.25rem; | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + .studentsCard { | ||
| 222 | + // 兜底:强制学生列表为5列栅格,避免一行仅1个的问题 | ||
| 223 | + .StudentsGrid { | ||
| 224 | + display: grid; | ||
| 225 | + grid-template-columns: repeat(5, 1fr); | ||
| 226 | + gap: 0.75rem; // 等同于 tailwind 的 gap-3 | ||
| 227 | + } | ||
| 228 | + .grid>div { | ||
| 229 | + transition: all 0.2s ease-in-out; | ||
| 230 | + } | ||
| 231 | + .studentItem { | ||
| 232 | + min-height: 4rem; | ||
| 233 | + // 卡片圆角裁切,配合右上角图片效果 | ||
| 234 | + overflow: hidden; | ||
| 235 | + } | ||
| 236 | + .cornerIcon { | ||
| 237 | + // 右上角图片样式(对勾角标),贴边显示 | ||
| 238 | + position: absolute; | ||
| 239 | + top: -1px; | ||
| 240 | + right: -1px; | ||
| 241 | + width: 28px; | ||
| 242 | + height: 28px; | ||
| 243 | + } | ||
| 244 | + } | ||
| 245 | +} | ||
| 246 | +</style> |
| ... | @@ -29,7 +29,7 @@ | ... | @@ -29,7 +29,7 @@ |
| 29 | </div> | 29 | </div> |
| 30 | <!-- 右侧按钮:占用较小空间,右对齐 --> | 30 | <!-- 右侧按钮:占用较小空间,右对齐 --> |
| 31 | <div class="right flex items-center justify-end w-20 ml-3"> | 31 | <div class="right flex items-center justify-end w-20 ml-3"> |
| 32 | - <van-button type="primary" size="small" round class="w-full">查看</van-button> | 32 | + <van-button type="primary" size="small" round class="w-full" @click="go_task_home(task.id)">查看</van-button> |
| 33 | </div> | 33 | </div> |
| 34 | </div> | 34 | </div> |
| 35 | </div> | 35 | </div> |
| ... | @@ -45,10 +45,11 @@ | ... | @@ -45,10 +45,11 @@ |
| 45 | 45 | ||
| 46 | <script setup> | 46 | <script setup> |
| 47 | import { ref } from 'vue' | 47 | import { ref } from 'vue' |
| 48 | -import { useRoute } from 'vue-router' | 48 | +import { useRoute, useRouter } from 'vue-router' |
| 49 | import { useTitle } from '@vueuse/core' | 49 | import { useTitle } from '@vueuse/core' |
| 50 | 50 | ||
| 51 | const $route = useRoute() | 51 | const $route = useRoute() |
| 52 | +const $router = useRouter() | ||
| 52 | useTitle($route.meta.title) | 53 | useTitle($route.meta.title) |
| 53 | 54 | ||
| 54 | // | 55 | // |
| ... | @@ -72,6 +73,16 @@ const format_date_range = (begin_date, end_date) => { | ... | @@ -72,6 +73,16 @@ const format_date_range = (begin_date, end_date) => { |
| 72 | const end = end_date || '-' | 73 | const end = end_date || '-' |
| 73 | return `${start} 至 ${end}` | 74 | return `${start} 至 ${end}` |
| 74 | } | 75 | } |
| 76 | + | ||
| 77 | +/** | ||
| 78 | + * 跳转到作业主页 | ||
| 79 | + * @param {string} id - 作业ID | ||
| 80 | + * @returns {void} | ||
| 81 | + * 注释:点击列表项右侧“查看”按钮,导航到教师端作业主页。 | ||
| 82 | + */ | ||
| 83 | +const go_task_home = (id) => { | ||
| 84 | + $router.push({ name: 'TeacherTaskHome', params: { id } }) | ||
| 85 | +} | ||
| 75 | </script> | 86 | </script> |
| 76 | 87 | ||
| 77 | <style lang="less" scoped> | 88 | <style lang="less" scoped> | ... | ... |
-
Please register or login to post a comment