feat(teacher): 新增教师模块路由、表单页面和作业管理页面
添加教师模块相关功能,包括: 1. 在路由配置中添加教师模块路由 2. 创建教师作业新增表单页面 3. 实现教师作业管理日历视图页面 4. 添加相关Vant组件支持
Showing
5 changed files
with
1513 additions
and
1 deletions
| ... | @@ -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 | ] | ... | ... |
src/router/teacher.js
0 → 100644
| 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 | +] |
src/views/teacher/formPage.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-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> |
src/views/teacher/testPage.vue
0 → 100644
| 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> |
-
Please register or login to post a comment