hookehuyr

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

- 在 checkin.js 中扩展作业详情接口返回字段
- 重构 JoinCheckInPage.vue 页面,移除 mock 数据并实现接口数据绑定
- 新增格式化函数处理周期、频次和提交类型显示
- 优化确认加入作业的交互逻辑
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-10 10:37:44
* @LastEditTime: 2025-11-17 14:33:59
* @FilePath: /mlaj/src/api/checkin.js
* @Description: 签到模块相关接口
*/
......@@ -35,7 +35,7 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param
* @description: 获取作业详情
* @param: i 作业id
* @param: month 月份
* @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个人的头像 }
* @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个人的头像 }
*/
export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params))
......
<!--
* @Date: 2025-11-17 13:42:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-17 14:22:00
* @LastEditTime: 2025-11-17 15:07:52
* @FilePath: /mlaj/src/views/checkin/JoinCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout :hasTitle="false">
<div class="join-check-in-page">
<div class="JoinCheckInPage">
<!-- 头部课程信息 -->
<div class="joinHeader bg-white rounded-lg shadow px-4 py-3">
<div class="flex flex-col">
......@@ -34,7 +34,7 @@
</div>
</div>
<!-- 作业信息 -->
<!-- 作业信息(接口) -->
<div class="infoCard bg-white rounded-lg shadow mt-4">
<div class="cardHeader px-4 py-3 border-b border-gray-100">
<div class="cardTitle text-sm font-semibold text-gray-800">作业信息</div>
......@@ -42,34 +42,38 @@
<div class="cardBody px-4 py-3 space-y-2">
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">作业名称</div>
<div class="value text-gray-800">{{ mock_task_info.task_title }}</div>
<div class="value text-gray-800">{{ task_detail.title || '-' }}</div>
</div>
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">所属班级</div>
<div class="value text-gray-800">{{ mock_task_info.grade_name }} · {{ mock_task_info.class_name }}</div>
<div class="label text-gray-500">作业周期</div>
<div class="value text-gray-800">{{ format_cycle(task_detail.cycle) }}</div>
</div>
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">目标次数</div>
<div class="value text-gray-800">{{ mock_task_info.target_number }} 次</div>
<div class="label text-gray-500">交作业的频次</div>
<div class="value text-gray-800">{{ format_frequency(task_detail.frequency) }}</div>
</div>
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">起止时间</div>
<div class="value text-gray-800">{{ mock_task_info.period_desc }}</div>
<div class="label text-gray-500">提交类型</div>
<div class="value text-gray-800">{{ format_attachment_types(task_detail.attachment_type) }}</div>
</div>
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">报名截止</div>
<div class="value text-gray-800">{{ mock_task_info.join_deadline_desc }}</div>
<div class="label text-gray-500">开始时间</div>
<div class="value text-gray-800">{{ task_detail.begin_date || '-' }}</div>
</div>
<div class="infoItem flex justify-between text-sm">
<div class="label text-gray-500">结束时间</div>
<div class="value text-gray-800">{{ task_detail.end_date || '-' }}</div>
</div>
</div>
</div>
<!-- 作业说明 -->
<!-- 作业说明(接口) -->
<div class="infoCard bg-white rounded-lg shadow mt-4">
<div class="cardHeader px-4 py-3 border-b border-gray-100">
<div class="cardTitle text-sm font-semibold text-gray-800">作业说明</div>
</div>
<div class="cardBody px-4 py-3">
<div class="text-sm text-gray-700 leading-6 whitespace-pre-line">{{ mock_task_info.task_desc }}</div>
<div class="text-sm text-gray-700 leading-6" v-html="task_detail.note || '暂无作业说明'"></div>
</div>
</div>
......@@ -89,6 +93,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useTitle } from "@vueuse/core";
import { showConfirmDialog } from 'vant'
import AppLayout from '@/components/layout/AppLayout.vue'
import { getTaskDetailAPI } from "@/api/checkin"
const $route = useRoute();
const $router = useRouter();
......@@ -122,18 +127,92 @@ const load_mock_data = () => {
// 预留:后续可根据 $route.query.id 拉取真实数据并映射
}
// 作业详情数据
const task_detail = ref({})
/**
* 获取作业详情(接口)
* @returns {Promise<void>}
* 注释:使用 target_checkin_id 作为作业ID,调用作业详情接口并填充页面字段。
*/
const load_task_detail = async () => {
try {
const { code, data } = await getTaskDetailAPI({ i: target_checkin_id.value })
if (code) {
task_detail.value = data || {}
}
} catch (error) {
// 接口异常时保持页面可用
task_detail.value = {}
}
}
/**
* 格式化作业周期文案
* @param {number|string} cycle 周期数值编码
* @returns {string} 中文周期说明
* 注释:0=本周期,30=每月,7=每周,1=每日。
*/
const format_cycle = (cycle) => {
const map = {
0: '本周期',
30: '每月',
7: '每周',
1: '每日'
}
const key = Number(cycle)
return map[key] || '-'
}
/**
* 格式化频次文案
* @param {number|string} freq 频次
* @returns {string} 频次说明
* 注释:显示为“X 次”。
*/
const format_frequency = (freq) => {
if (freq === undefined || freq === null || freq === '') return '-'
const num = Number(freq)
return isNaN(num) ? '-' : `${num} 次`
}
/**
* 格式化提交类型
* @param {Array|Object|string} types 提交类型配置
* @returns {string} 类型列表中文
* 注释:支持数组或对象,统一转中文并以“、”连接。
*/
const format_attachment_types = (types) => {
const type_map = {
text: '文本',
image: '图片',
audio: '音频',
video: '视频'
}
if (!types) return '文本、图片、音频、视频'
if (Array.isArray(types)) {
return types.map(k => type_map[k] || k).join('、') || '-'
}
if (typeof types === 'object') {
return Object.keys(types).map(k => type_map[k] || types[k] || k).join('、') || '-'
}
return type_map[types] || String(types)
}
/**
* 点击加入作业的处理
* @returns {Promise<void>}
* 注释:弹出确认框,确认后跳转到打卡页。
*/
const on_join_click = async () => {
const on_join_click = () => {
try {
await showConfirmDialog({
showConfirmDialog({
title: '确认加入',
message: '是否确认加入该作业?加入后可前往打卡页进行每日打卡。',
confirmButtonColor: '#4caf50',
})
.then(() => {
// on confirm
// 确认后跳转到打卡页
$router.push({
path: '/checkin/index',
......@@ -141,6 +220,10 @@ const on_join_click = async () => {
id: target_checkin_id.value,
}
})
})
.catch(() => {
// on cancel
});
} catch (err) {
// 用户取消
}
......@@ -148,11 +231,12 @@ const on_join_click = async () => {
onMounted(() => {
load_mock_data()
load_task_detail()
})
</script>
<style lang="less" scoped>
.join-check-in-page {
.JoinCheckInPage {
min-height: 100vh;
background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff);
padding: 1rem;
......@@ -188,7 +272,7 @@ onMounted(() => {
}
.joinAction {
box-shadow: 0 -6px 12px rgba(0,0,0,0.06);
// box-shadow: 0 -6px 12px rgba(0,0,0,0.06);
}
}
</style>
......