hookehuyr

feat(checkin): 实现作业详情接口对接及页面重构

- 在 checkin.js 中扩展作业详情接口返回字段
- 重构 JoinCheckInPage.vue 页面,移除 mock 数据并实现接口数据绑定
- 新增格式化函数处理周期、频次和提交类型显示
- 优化确认加入作业的交互逻辑
1 /* 1 /*
2 * @Date: 2025-06-06 09:26:16 2 * @Date: 2025-06-06 09:26:16
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-10 10:37:44 4 + * @LastEditTime: 2025-11-17 14:33:59
5 * @FilePath: /mlaj/src/api/checkin.js 5 * @FilePath: /mlaj/src/api/checkin.js
6 * @Description: 签到模块相关接口 6 * @Description: 签到模块相关接口
7 */ 7 */
...@@ -35,7 +35,7 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param ...@@ -35,7 +35,7 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param
35 * @description: 获取作业详情 35 * @description: 获取作业详情
36 * @param: i 作业id 36 * @param: i 作业id
37 * @param: month 月份 37 * @param: month 月份
38 - * @returns data: { id 作业id, title 作业名称, frequency 交作业的频次, begin_date 开始时间, end_date 结束时间, task_type 任务类型 [checkin=签到 | file=上传附件], is_gray 作业是否应该置灰, my_checkin_dates 我在日历中打过卡的日期, target_number 打卡的目标数量, checkin_number 已经打卡的数量, checkin_avatars 最后打卡的10个人的头像 } 38 + * @returns data: { id 作业id, title 作业名称, note 作业描述, frequency 交作业的频次, cycle 交作业的周期 {0=本周期 | 30=每月 | 7=每周 | 1=每日}, attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频], begin_date 开始时间, end_date 结束时间, task_type 任务类型 [checkin=签到 | file=上传附件], is_gray 作业是否应该置灰, my_checkin_dates 我在日历中打过卡的日期, target_number 打卡的目标数量, checkin_number 已经打卡的数量, checkin_avatars 最后打卡的10个人的头像 }
39 */ 39 */
40 export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params)) 40 export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params))
41 41
......
1 <!-- 1 <!--
2 * @Date: 2025-11-17 13:42:00 2 * @Date: 2025-11-17 13:42:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-17 14:22:00 4 + * @LastEditTime: 2025-11-17 15:07:52
5 * @FilePath: /mlaj/src/views/checkin/JoinCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/JoinCheckInPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
8 <template> 8 <template>
9 <AppLayout :hasTitle="false"> 9 <AppLayout :hasTitle="false">
10 - <div class="join-check-in-page"> 10 + <div class="JoinCheckInPage">
11 <!-- 头部课程信息 --> 11 <!-- 头部课程信息 -->
12 <div class="joinHeader bg-white rounded-lg shadow px-4 py-3"> 12 <div class="joinHeader bg-white rounded-lg shadow px-4 py-3">
13 <div class="flex flex-col"> 13 <div class="flex flex-col">
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 </div> 35 </div>
36 36
37 - <!-- 作业信息 --> 37 + <!-- 作业信息(接口) -->
38 <div class="infoCard bg-white rounded-lg shadow mt-4"> 38 <div class="infoCard bg-white rounded-lg shadow mt-4">
39 <div class="cardHeader px-4 py-3 border-b border-gray-100"> 39 <div class="cardHeader px-4 py-3 border-b border-gray-100">
40 <div class="cardTitle text-sm font-semibold text-gray-800">作业信息</div> 40 <div class="cardTitle text-sm font-semibold text-gray-800">作业信息</div>
...@@ -42,34 +42,38 @@ ...@@ -42,34 +42,38 @@
42 <div class="cardBody px-4 py-3 space-y-2"> 42 <div class="cardBody px-4 py-3 space-y-2">
43 <div class="infoItem flex justify-between text-sm"> 43 <div class="infoItem flex justify-between text-sm">
44 <div class="label text-gray-500">作业名称</div> 44 <div class="label text-gray-500">作业名称</div>
45 - <div class="value text-gray-800">{{ mock_task_info.task_title }}</div> 45 + <div class="value text-gray-800">{{ task_detail.title || '-' }}</div>
46 </div> 46 </div>
47 <div class="infoItem flex justify-between text-sm"> 47 <div class="infoItem flex justify-between text-sm">
48 - <div class="label text-gray-500">所属班级</div> 48 + <div class="label text-gray-500">作业周期</div>
49 - <div class="value text-gray-800">{{ mock_task_info.grade_name }} · {{ mock_task_info.class_name }}</div> 49 + <div class="value text-gray-800">{{ format_cycle(task_detail.cycle) }}</div>
50 </div> 50 </div>
51 <div class="infoItem flex justify-between text-sm"> 51 <div class="infoItem flex justify-between text-sm">
52 - <div class="label text-gray-500">目标次数</div> 52 + <div class="label text-gray-500">交作业的频次</div>
53 - <div class="value text-gray-800">{{ mock_task_info.target_number }} 次</div> 53 + <div class="value text-gray-800">{{ format_frequency(task_detail.frequency) }}</div>
54 </div> 54 </div>
55 <div class="infoItem flex justify-between text-sm"> 55 <div class="infoItem flex justify-between text-sm">
56 - <div class="label text-gray-500">起止时间</div> 56 + <div class="label text-gray-500">提交类型</div>
57 - <div class="value text-gray-800">{{ mock_task_info.period_desc }}</div> 57 + <div class="value text-gray-800">{{ format_attachment_types(task_detail.attachment_type) }}</div>
58 </div> 58 </div>
59 <div class="infoItem flex justify-between text-sm"> 59 <div class="infoItem flex justify-between text-sm">
60 - <div class="label text-gray-500">报名截止</div> 60 + <div class="label text-gray-500">开始时间</div>
61 - <div class="value text-gray-800">{{ mock_task_info.join_deadline_desc }}</div> 61 + <div class="value text-gray-800">{{ task_detail.begin_date || '-' }}</div>
62 + </div>
63 + <div class="infoItem flex justify-between text-sm">
64 + <div class="label text-gray-500">结束时间</div>
65 + <div class="value text-gray-800">{{ task_detail.end_date || '-' }}</div>
62 </div> 66 </div>
63 </div> 67 </div>
64 </div> 68 </div>
65 69
66 - <!-- 作业说明 --> 70 + <!-- 作业说明(接口) -->
67 <div class="infoCard bg-white rounded-lg shadow mt-4"> 71 <div class="infoCard bg-white rounded-lg shadow mt-4">
68 <div class="cardHeader px-4 py-3 border-b border-gray-100"> 72 <div class="cardHeader px-4 py-3 border-b border-gray-100">
69 <div class="cardTitle text-sm font-semibold text-gray-800">作业说明</div> 73 <div class="cardTitle text-sm font-semibold text-gray-800">作业说明</div>
70 </div> 74 </div>
71 <div class="cardBody px-4 py-3"> 75 <div class="cardBody px-4 py-3">
72 - <div class="text-sm text-gray-700 leading-6 whitespace-pre-line">{{ mock_task_info.task_desc }}</div> 76 + <div class="text-sm text-gray-700 leading-6" v-html="task_detail.note || '暂无作业说明'"></div>
73 </div> 77 </div>
74 </div> 78 </div>
75 79
...@@ -89,6 +93,7 @@ import { useRoute, useRouter } from 'vue-router' ...@@ -89,6 +93,7 @@ import { useRoute, useRouter } from 'vue-router'
89 import { useTitle } from "@vueuse/core"; 93 import { useTitle } from "@vueuse/core";
90 import { showConfirmDialog } from 'vant' 94 import { showConfirmDialog } from 'vant'
91 import AppLayout from '@/components/layout/AppLayout.vue' 95 import AppLayout from '@/components/layout/AppLayout.vue'
96 +import { getTaskDetailAPI } from "@/api/checkin"
92 97
93 const $route = useRoute(); 98 const $route = useRoute();
94 const $router = useRouter(); 99 const $router = useRouter();
...@@ -122,18 +127,92 @@ const load_mock_data = () => { ...@@ -122,18 +127,92 @@ const load_mock_data = () => {
122 // 预留:后续可根据 $route.query.id 拉取真实数据并映射 127 // 预留:后续可根据 $route.query.id 拉取真实数据并映射
123 } 128 }
124 129
130 +// 作业详情数据
131 +const task_detail = ref({})
132 +
133 +/**
134 + * 获取作业详情(接口)
135 + * @returns {Promise<void>}
136 + * 注释:使用 target_checkin_id 作为作业ID,调用作业详情接口并填充页面字段。
137 + */
138 +const load_task_detail = async () => {
139 + try {
140 + const { code, data } = await getTaskDetailAPI({ i: target_checkin_id.value })
141 + if (code) {
142 + task_detail.value = data || {}
143 + }
144 + } catch (error) {
145 + // 接口异常时保持页面可用
146 + task_detail.value = {}
147 + }
148 +}
149 +
150 +/**
151 + * 格式化作业周期文案
152 + * @param {number|string} cycle 周期数值编码
153 + * @returns {string} 中文周期说明
154 + * 注释:0=本周期,30=每月,7=每周,1=每日。
155 + */
156 +const format_cycle = (cycle) => {
157 + const map = {
158 + 0: '本周期',
159 + 30: '每月',
160 + 7: '每周',
161 + 1: '每日'
162 + }
163 + const key = Number(cycle)
164 + return map[key] || '-'
165 +}
166 +
167 +/**
168 + * 格式化频次文案
169 + * @param {number|string} freq 频次
170 + * @returns {string} 频次说明
171 + * 注释:显示为“X 次”。
172 + */
173 +const format_frequency = (freq) => {
174 + if (freq === undefined || freq === null || freq === '') return '-'
175 + const num = Number(freq)
176 + return isNaN(num) ? '-' : `${num} 次`
177 +}
178 +
179 +/**
180 + * 格式化提交类型
181 + * @param {Array|Object|string} types 提交类型配置
182 + * @returns {string} 类型列表中文
183 + * 注释:支持数组或对象,统一转中文并以“、”连接。
184 + */
185 +const format_attachment_types = (types) => {
186 + const type_map = {
187 + text: '文本',
188 + image: '图片',
189 + audio: '音频',
190 + video: '视频'
191 + }
192 + if (!types) return '文本、图片、音频、视频'
193 + if (Array.isArray(types)) {
194 + return types.map(k => type_map[k] || k).join('、') || '-'
195 + }
196 + if (typeof types === 'object') {
197 + return Object.keys(types).map(k => type_map[k] || types[k] || k).join('、') || '-'
198 + }
199 + return type_map[types] || String(types)
200 +}
201 +
125 /** 202 /**
126 * 点击加入作业的处理 203 * 点击加入作业的处理
127 * @returns {Promise<void>} 204 * @returns {Promise<void>}
128 * 注释:弹出确认框,确认后跳转到打卡页。 205 * 注释:弹出确认框,确认后跳转到打卡页。
129 */ 206 */
130 -const on_join_click = async () => { 207 +const on_join_click = () => {
131 try { 208 try {
132 - await showConfirmDialog({ 209 + showConfirmDialog({
133 title: '确认加入', 210 title: '确认加入',
134 message: '是否确认加入该作业?加入后可前往打卡页进行每日打卡。', 211 message: '是否确认加入该作业?加入后可前往打卡页进行每日打卡。',
135 confirmButtonColor: '#4caf50', 212 confirmButtonColor: '#4caf50',
136 }) 213 })
214 + .then(() => {
215 + // on confirm
137 // 确认后跳转到打卡页 216 // 确认后跳转到打卡页
138 $router.push({ 217 $router.push({
139 path: '/checkin/index', 218 path: '/checkin/index',
...@@ -141,6 +220,10 @@ const on_join_click = async () => { ...@@ -141,6 +220,10 @@ const on_join_click = async () => {
141 id: target_checkin_id.value, 220 id: target_checkin_id.value,
142 } 221 }
143 }) 222 })
223 + })
224 + .catch(() => {
225 + // on cancel
226 + });
144 } catch (err) { 227 } catch (err) {
145 // 用户取消 228 // 用户取消
146 } 229 }
...@@ -148,11 +231,12 @@ const on_join_click = async () => { ...@@ -148,11 +231,12 @@ const on_join_click = async () => {
148 231
149 onMounted(() => { 232 onMounted(() => {
150 load_mock_data() 233 load_mock_data()
234 + load_task_detail()
151 }) 235 })
152 </script> 236 </script>
153 237
154 <style lang="less" scoped> 238 <style lang="less" scoped>
155 -.join-check-in-page { 239 +.JoinCheckInPage {
156 min-height: 100vh; 240 min-height: 100vh;
157 background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); 241 background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff);
158 padding: 1rem; 242 padding: 1rem;
...@@ -188,7 +272,7 @@ onMounted(() => { ...@@ -188,7 +272,7 @@ onMounted(() => {
188 } 272 }
189 273
190 .joinAction { 274 .joinAction {
191 - box-shadow: 0 -6px 12px rgba(0,0,0,0.06); 275 + // box-shadow: 0 -6px 12px rgba(0,0,0,0.06);
192 } 276 }
193 } 277 }
194 </style> 278 </style>
......