feat(checkin): 实现作业详情接口对接及页面重构
- 在 checkin.js 中扩展作业详情接口返回字段 - 重构 JoinCheckInPage.vue 页面,移除 mock 数据并实现接口数据绑定 - 新增格式化函数处理周期、频次和提交类型显示 - 优化确认加入作业的交互逻辑
Showing
2 changed files
with
104 additions
and
20 deletions
| 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> | ... | ... |
-
Please register or login to post a comment