hookehuyr

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

- 新增打卡详情页面,包含作业描述和打卡类型选择功能
- 将打卡类型选择从首页移动到详情页
- 在首页添加"我要打卡"按钮跳转到详情页
/*
* @Date: 2025-03-21 13:28:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-30 16:34:59
* @LastEditTime: 2025-09-30 16:50:44
* @FilePath: /mlaj/src/router/checkin.js
* @Description: 文件描述
*/
......@@ -47,7 +47,16 @@ export default [
name: 'IndexCheckIn',
component: () => import('@/views/checkin/IndexCheckInPage.vue'),
meta: {
title: '',
title: '打卡页',
requiresAuth: true
}
},
{
path: '/checkin/detail',
name: 'CheckinDetail',
component: () => import('@/views/checkin/CheckinDetailPage.vue'),
meta: {
title: '打卡详情',
requiresAuth: true
}
},
......
<template>
<div class="checkin-detail-page">
<!-- 页面内容 -->
<div class="page-content">
<!-- 作业描述 -->
<div class="section-wrapper">
<div class="section-title">作业描述</div>
<div class="section-content">
<div v-if="taskDetail.description" class="description-text">
{{ taskDetail.description }}
</div>
<div v-else class="no-description">
暂无作业描述
</div>
</div>
</div>
<!-- 打卡类型选择 -->
<div v-if="!taskDetail.is_finish" class="section-wrapper">
<div class="section-title">选择打卡类型</div>
<div class="section-content">
<div class="checkin-types">
<div
v-for="option in attachmentTypeOptions"
:key="option.key"
@click="handleCheckinTypeClick(option.key)"
class="checkin-type-item"
>
<van-icon
:name="getIconName(option.key)"
size="2rem"
color="#4caf50"
/>
<span class="type-text">{{ option.value }}</span>
</div>
</div>
</div>
</div>
<!-- 已完成提示 -->
<div v-else class="section-wrapper">
<div class="finished-notice">
<van-icon name="success" size="3rem" color="#4caf50" />
<div class="finished-text">作业已完成</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getTaskDetailAPI } from "@/api/checkin";
import { getTeacherFindSettingsAPI } from '@/api/teacher'
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs'
const route = useRoute()
const router = useRouter()
useTitle('打卡详情');
// 任务详情数据
const taskDetail = ref({});
// 作品类型选项
const attachmentTypeOptions = ref([]);
/**
* 返回上一页
*/
const onClickLeft = () => {
router.back()
}
/**
* 根据打卡类型获取对应的图标名称
* @param {string} type - 打卡类型
* @returns {string} 图标名称
*/
const getIconName = (type) => {
const iconMap = {
'text': 'edit',
'image': 'photo',
'video': 'video',
'audio': 'music'
};
return iconMap[type] || 'edit';
};
/**
* 处理打卡类型点击事件
* @param {string} type - 打卡类型
*/
const handleCheckinTypeClick = (type) => {
switch (type) {
case 'text':
goToCheckinTextPage();
break;
case 'image':
goToCheckinImagePage();
break;
case 'video':
goToCheckinVideoPage();
break;
case 'audio':
goToCheckinAudioPage();
break;
default:
console.warn('未知的打卡类型:', type);
}
};
/**
* 跳转到文本打卡页面
*/
const goToCheckinTextPage = () => {
router.push({
path: '/checkin/text',
query: {
id: route.query.id,
type: 'text'
}
})
}
/**
* 跳转到图片打卡页面
*/
const goToCheckinImagePage = () => {
router.push({
path: '/checkin/image',
query: {
id: route.query.id,
type: 'image'
}
})
}
/**
* 跳转到视频打卡页面
*/
const goToCheckinVideoPage = () => {
router.push({
path: '/checkin/video',
query: {
id: route.query.id,
type: 'video',
}
})
}
/**
* 跳转到音频打卡页面
*/
const goToCheckinAudioPage = () => {
router.push({
path: '/checkin/audio',
query: {
id: route.query.id,
type: 'audio',
}
})
}
/**
* 获取任务详情
*/
const getTaskDetail = async (month) => {
const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
if (code) {
taskDetail.value = data;
}
}
/**
* 页面挂载时的初始化逻辑
*/
onMounted(async () => {
// 获取任务详情
getTaskDetail(dayjs().format('YYYY-MM'));
// 获取作品类型数据
try {
const { code, data } = await getTeacherFindSettingsAPI();
if (code && data.task_attachment_type) {
attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
key,
value
}));
}
} catch (error) {
console.error('获取作品类型数据失败:', error);
}
})
</script>
<style lang="less" scoped>
.checkin-detail-page {
min-height: 100vh;
background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff);
}
.page-content {
padding: 1rem;
}
.section-wrapper {
background-color: #fff;
border-radius: 12px;
margin-bottom: 1rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: #4caf50;
padding: 1rem 1rem 0.5rem;
border-bottom: 1px solid #f0f0f0;
}
.section-content {
padding: 1rem;
}
.description-text {
color: #666;
line-height: 1.6;
font-size: 0.95rem;
}
.no-description {
color: #999;
font-style: italic;
text-align: center;
padding: 2rem 0;
}
.class-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-text {
font-size: 0.85rem;
color: #666;
}
.checkin-types {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.checkin-type-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem 1rem;
border: 2px solid #e8f5e8;
border-radius: 12px;
background-color: #fafffe;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #4caf50;
background-color: #f0fdf4;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.15);
}
&:active {
transform: translateY(0);
}
}
.type-text {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #4caf50;
font-weight: 500;
}
.finished-notice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
}
.finished-text {
margin-top: 1rem;
font-size: 1.1rem;
color: #4caf50;
font-weight: 600;
}
</style>
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-30 16:46:38
* @LastEditTime: 2025-09-30 17:02:29
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......@@ -22,11 +22,6 @@
<!-- 可滚动的内容区域 -->
<div class="scrollable-content">
<!-- TODO: 作业描述暂时没有字段数据 -->
<div class="text-wrapper">
<div class="text-header">作业描述</div>
</div>
<div v-if="showProgress" class="text-wrapper">
<div class="text-header">目标进度</div>
<div style="background-color: #FFF; margin-top: 1rem;">
......@@ -62,22 +57,18 @@
</div>
</div>
<!-- 我要打卡按钮 -->
<div v-if="!taskDetail.is_finish" class="text-wrapper" style="padding-bottom: 0;">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
<div
v-for="option in attachmentTypeOptions"
:key="option.key"
@click="handleCheckinTypeClick(option.key)"
class="upload-button"
>
<van-icon
:name="getIconName(option.key)"
size="1.2rem"
/>
<span class="button-text">{{ option.value }}</span>
</div>
</div>
<van-button
type="primary"
block
round
@click="goToCheckinDetailPage"
class="checkin-action-button"
>
<van-icon name="edit" size="1.2rem" />
<span style="margin-left: 0.5rem;">我要打卡</span>
</van-button>
</div>
<div class="text-wrapper">
......@@ -545,6 +536,18 @@ const handleCheckinTypeClick = (type) => {
}
};
/**
* 跳转到打卡详情页面
*/
const goToCheckinDetailPage = () => {
router.push({
path: '/checkin/detail',
query: {
id: route.query.id
}
})
}
const goToCheckinTextPage = () => {
router.push({
path: '/checkin/text',
......