hookehuyr

feat(打卡): 添加打卡详情页并优化打卡入口

- 新增打卡详情页面,包含作业描述和打卡类型选择功能
- 将打卡类型选择从首页移动到详情页
- 在首页添加"我要打卡"按钮跳转到详情页
1 /* 1 /*
2 * @Date: 2025-03-21 13:28:30 2 * @Date: 2025-03-21 13:28:30
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-30 16:34:59 4 + * @LastEditTime: 2025-09-30 16:50:44
5 * @FilePath: /mlaj/src/router/checkin.js 5 * @FilePath: /mlaj/src/router/checkin.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -47,7 +47,16 @@ export default [ ...@@ -47,7 +47,16 @@ export default [
47 name: 'IndexCheckIn', 47 name: 'IndexCheckIn',
48 component: () => import('@/views/checkin/IndexCheckInPage.vue'), 48 component: () => import('@/views/checkin/IndexCheckInPage.vue'),
49 meta: { 49 meta: {
50 - title: '', 50 + title: '打卡页',
51 + requiresAuth: true
52 + }
53 + },
54 + {
55 + path: '/checkin/detail',
56 + name: 'CheckinDetail',
57 + component: () => import('@/views/checkin/CheckinDetailPage.vue'),
58 + meta: {
59 + title: '打卡详情',
51 requiresAuth: true 60 requiresAuth: true
52 } 61 }
53 }, 62 },
......
1 +<template>
2 + <div class="checkin-detail-page">
3 + <!-- 页面内容 -->
4 + <div class="page-content">
5 + <!-- 作业描述 -->
6 + <div class="section-wrapper">
7 + <div class="section-title">作业描述</div>
8 + <div class="section-content">
9 + <div v-if="taskDetail.description" class="description-text">
10 + {{ taskDetail.description }}
11 + </div>
12 + <div v-else class="no-description">
13 + 暂无作业描述
14 + </div>
15 + </div>
16 + </div>
17 +
18 + <!-- 打卡类型选择 -->
19 + <div v-if="!taskDetail.is_finish" class="section-wrapper">
20 + <div class="section-title">选择打卡类型</div>
21 + <div class="section-content">
22 + <div class="checkin-types">
23 + <div
24 + v-for="option in attachmentTypeOptions"
25 + :key="option.key"
26 + @click="handleCheckinTypeClick(option.key)"
27 + class="checkin-type-item"
28 + >
29 + <van-icon
30 + :name="getIconName(option.key)"
31 + size="2rem"
32 + color="#4caf50"
33 + />
34 + <span class="type-text">{{ option.value }}</span>
35 + </div>
36 + </div>
37 + </div>
38 + </div>
39 +
40 + <!-- 已完成提示 -->
41 + <div v-else class="section-wrapper">
42 + <div class="finished-notice">
43 + <van-icon name="success" size="3rem" color="#4caf50" />
44 + <div class="finished-text">作业已完成</div>
45 + </div>
46 + </div>
47 + </div>
48 + </div>
49 +</template>
50 +
51 +<script setup>
52 +import { ref, computed, onMounted } from 'vue'
53 +import { useRoute, useRouter } from 'vue-router'
54 +import { getTaskDetailAPI } from "@/api/checkin";
55 +import { getTeacherFindSettingsAPI } from '@/api/teacher'
56 +import { useTitle } from '@vueuse/core';
57 +import dayjs from 'dayjs'
58 +
59 +const route = useRoute()
60 +const router = useRouter()
61 +useTitle('打卡详情');
62 +
63 +// 任务详情数据
64 +const taskDetail = ref({});
65 +
66 +// 作品类型选项
67 +const attachmentTypeOptions = ref([]);
68 +
69 +/**
70 + * 返回上一页
71 + */
72 +const onClickLeft = () => {
73 + router.back()
74 +}
75 +
76 +/**
77 + * 根据打卡类型获取对应的图标名称
78 + * @param {string} type - 打卡类型
79 + * @returns {string} 图标名称
80 + */
81 +const getIconName = (type) => {
82 + const iconMap = {
83 + 'text': 'edit',
84 + 'image': 'photo',
85 + 'video': 'video',
86 + 'audio': 'music'
87 + };
88 + return iconMap[type] || 'edit';
89 +};
90 +
91 +/**
92 + * 处理打卡类型点击事件
93 + * @param {string} type - 打卡类型
94 + */
95 +const handleCheckinTypeClick = (type) => {
96 + switch (type) {
97 + case 'text':
98 + goToCheckinTextPage();
99 + break;
100 + case 'image':
101 + goToCheckinImagePage();
102 + break;
103 + case 'video':
104 + goToCheckinVideoPage();
105 + break;
106 + case 'audio':
107 + goToCheckinAudioPage();
108 + break;
109 + default:
110 + console.warn('未知的打卡类型:', type);
111 + }
112 +};
113 +
114 +/**
115 + * 跳转到文本打卡页面
116 + */
117 +const goToCheckinTextPage = () => {
118 + router.push({
119 + path: '/checkin/text',
120 + query: {
121 + id: route.query.id,
122 + type: 'text'
123 + }
124 + })
125 +}
126 +
127 +/**
128 + * 跳转到图片打卡页面
129 + */
130 +const goToCheckinImagePage = () => {
131 + router.push({
132 + path: '/checkin/image',
133 + query: {
134 + id: route.query.id,
135 + type: 'image'
136 + }
137 + })
138 +}
139 +
140 +/**
141 + * 跳转到视频打卡页面
142 + */
143 +const goToCheckinVideoPage = () => {
144 + router.push({
145 + path: '/checkin/video',
146 + query: {
147 + id: route.query.id,
148 + type: 'video',
149 + }
150 + })
151 +}
152 +
153 +/**
154 + * 跳转到音频打卡页面
155 + */
156 +const goToCheckinAudioPage = () => {
157 + router.push({
158 + path: '/checkin/audio',
159 + query: {
160 + id: route.query.id,
161 + type: 'audio',
162 + }
163 + })
164 +}
165 +
166 +/**
167 + * 获取任务详情
168 + */
169 +const getTaskDetail = async (month) => {
170 + const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
171 + if (code) {
172 + taskDetail.value = data;
173 + }
174 +}
175 +
176 +/**
177 + * 页面挂载时的初始化逻辑
178 + */
179 +onMounted(async () => {
180 + // 获取任务详情
181 + getTaskDetail(dayjs().format('YYYY-MM'));
182 +
183 + // 获取作品类型数据
184 + try {
185 + const { code, data } = await getTeacherFindSettingsAPI();
186 + if (code && data.task_attachment_type) {
187 + attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
188 + key,
189 + value
190 + }));
191 + }
192 + } catch (error) {
193 + console.error('获取作品类型数据失败:', error);
194 + }
195 +})
196 +</script>
197 +
198 +<style lang="less" scoped>
199 +.checkin-detail-page {
200 + min-height: 100vh;
201 + background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff);
202 +}
203 +
204 +.page-content {
205 + padding: 1rem;
206 +}
207 +
208 +.section-wrapper {
209 + background-color: #fff;
210 + border-radius: 12px;
211 + margin-bottom: 1rem;
212 + overflow: hidden;
213 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
214 +}
215 +
216 +.section-title {
217 + font-size: 1.1rem;
218 + font-weight: 600;
219 + color: #4caf50;
220 + padding: 1rem 1rem 0.5rem;
221 + border-bottom: 1px solid #f0f0f0;
222 +}
223 +
224 +.section-content {
225 + padding: 1rem;
226 +}
227 +
228 +.description-text {
229 + color: #666;
230 + line-height: 1.6;
231 + font-size: 0.95rem;
232 +}
233 +
234 +.no-description {
235 + color: #999;
236 + font-style: italic;
237 + text-align: center;
238 + padding: 2rem 0;
239 +}
240 +
241 +.class-status {
242 + display: flex;
243 + align-items: center;
244 + gap: 0.5rem;
245 +}
246 +
247 +.status-text {
248 + font-size: 0.85rem;
249 + color: #666;
250 +}
251 +
252 +.checkin-types {
253 + display: grid;
254 + grid-template-columns: repeat(2, 1fr);
255 + gap: 1rem;
256 +}
257 +
258 +.checkin-type-item {
259 + display: flex;
260 + flex-direction: column;
261 + align-items: center;
262 + justify-content: center;
263 + padding: 1.5rem 1rem;
264 + border: 2px solid #e8f5e8;
265 + border-radius: 12px;
266 + background-color: #fafffe;
267 + cursor: pointer;
268 + transition: all 0.3s ease;
269 +
270 + &:hover {
271 + border-color: #4caf50;
272 + background-color: #f0fdf4;
273 + transform: translateY(-2px);
274 + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.15);
275 + }
276 +
277 + &:active {
278 + transform: translateY(0);
279 + }
280 +}
281 +
282 +.type-text {
283 + margin-top: 0.5rem;
284 + font-size: 0.9rem;
285 + color: #4caf50;
286 + font-weight: 500;
287 +}
288 +
289 +.finished-notice {
290 + display: flex;
291 + flex-direction: column;
292 + align-items: center;
293 + justify-content: center;
294 + padding: 3rem 1rem;
295 + text-align: center;
296 +}
297 +
298 +.finished-text {
299 + margin-top: 1rem;
300 + font-size: 1.1rem;
301 + color: #4caf50;
302 + font-weight: 600;
303 +}
304 +</style>
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-09-30 16:46:38 4 + * @LastEditTime: 2025-09-30 17:02:29
5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -22,11 +22,6 @@ ...@@ -22,11 +22,6 @@
22 22
23 <!-- 可滚动的内容区域 --> 23 <!-- 可滚动的内容区域 -->
24 <div class="scrollable-content"> 24 <div class="scrollable-content">
25 - <!-- TODO: 作业描述暂时没有字段数据 -->
26 - <div class="text-wrapper">
27 - <div class="text-header">作业描述</div>
28 - </div>
29 -
30 <div v-if="showProgress" class="text-wrapper"> 25 <div v-if="showProgress" class="text-wrapper">
31 <div class="text-header">目标进度</div> 26 <div class="text-header">目标进度</div>
32 <div style="background-color: #FFF; margin-top: 1rem;"> 27 <div style="background-color: #FFF; margin-top: 1rem;">
...@@ -62,22 +57,18 @@ ...@@ -62,22 +57,18 @@
62 </div> 57 </div>
63 </div> 58 </div>
64 59
60 + <!-- 我要打卡按钮 -->
65 <div v-if="!taskDetail.is_finish" class="text-wrapper" style="padding-bottom: 0;"> 61 <div v-if="!taskDetail.is_finish" class="text-wrapper" style="padding-bottom: 0;">
66 - <div class="text-header">打卡类型</div> 62 + <van-button
67 - <div class="upload-wrapper"> 63 + type="primary"
68 - <div 64 + block
69 - v-for="option in attachmentTypeOptions" 65 + round
70 - :key="option.key" 66 + @click="goToCheckinDetailPage"
71 - @click="handleCheckinTypeClick(option.key)" 67 + class="checkin-action-button"
72 - class="upload-button"
73 > 68 >
74 - <van-icon 69 + <van-icon name="edit" size="1.2rem" />
75 - :name="getIconName(option.key)" 70 + <span style="margin-left: 0.5rem;">我要打卡</span>
76 - size="1.2rem" 71 + </van-button>
77 - />
78 - <span class="button-text">{{ option.value }}</span>
79 - </div>
80 - </div>
81 </div> 72 </div>
82 73
83 <div class="text-wrapper"> 74 <div class="text-wrapper">
...@@ -545,6 +536,18 @@ const handleCheckinTypeClick = (type) => { ...@@ -545,6 +536,18 @@ const handleCheckinTypeClick = (type) => {
545 } 536 }
546 }; 537 };
547 538
539 +/**
540 + * 跳转到打卡详情页面
541 + */
542 +const goToCheckinDetailPage = () => {
543 + router.push({
544 + path: '/checkin/detail',
545 + query: {
546 + id: route.query.id
547 + }
548 + })
549 +}
550 +
548 const goToCheckinTextPage = () => { 551 const goToCheckinTextPage = () => {
549 router.push({ 552 router.push({
550 path: '/checkin/text', 553 path: '/checkin/text',
......