hookehuyr

feat(teacher): 新增教师模块打卡管理和我的班级功能

- 添加打卡管理页面,包含日历视图、打卡统计和学生动态
- 新增我的班级页面,实现班级成员管理和数据统计展示
- 更新路由配置和组件类型声明
- 优化页面交互和样式细节
......@@ -36,6 +36,7 @@ declare module 'vue' {
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCircle: typeof import('vant/es')['Circle']
VanCol: typeof import('vant/es')['Col']
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanDatePicker: typeof import('vant/es')['DatePicker']
......@@ -61,9 +62,12 @@ declare module 'vue' {
VanRate: typeof import('vant/es')['Rate']
VanRow: typeof import('vant/es')['Row']
VanSearch: typeof import('vant/es')['Search']
VanSticky: typeof import('vant/es')['Sticky']
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VanUploader: typeof import('vant/es')['Uploader']
......
/*
* @Date: 2025-06-17 16:46:50
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-18 11:34:36
* @LastEditTime: 2025-06-19 13:23:04
* @FilePath: /mlaj/src/router/teacher.js
* @Description: 文件描述
*/
export default [
{
path: '/teacher/index',
path: '/teacher/checkin',
name: 'Teacher',
component: () => import('../views/teacher/testPage.vue'),
component: () => import('../views/teacher/checkinPage.vue'),
meta: {
title: '教师',
title: '打卡管理',
requiresAuth: true
},
},
......@@ -24,4 +24,13 @@ export default [
requiresAuth: true
},
},
{
path: '/teacher/myClass',
name: 'MyClass',
component: () => import('../views/teacher/myClassPage.vue'),
meta: {
title: '我的班级',
requiresAuth: true
},
},
]
......
......@@ -90,6 +90,15 @@
<FrostedGlass class="rounded-xl overflow-hidden mb-5">
<MenuItem
v-for="item in menuItems4"
:key="item.path"
v-bind="item"
@click="handleMenuClick(item.path)"
/>
</FrostedGlass>
<FrostedGlass class="rounded-xl overflow-hidden mb-5">
<MenuItem
v-for="item in menuItems3"
:key="item.path"
v-bind="item"
......@@ -295,6 +304,19 @@ const menuItems3 = ref([
},
]);
const menuItems4 = ref([
{
icon: "book",
title: "打卡管理",
path: "/teacher/checkin",
},
{
icon: "user",
title: "我的班级",
path: "/teacher/myClass",
},
]);
const handleCheckin = () => {
if (checkinData.value.length) {
showCheckInDialog.value = true;
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-17 20:08:40
* @FilePath: /mlaj/src/views/teacher/testPage.vue
* @LastEditTime: 2025-06-19 14:49:38
* @FilePath: /mlaj/src/views/teacher/checkinPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout :hasTitle="false">
<van-config-provider :theme-vars="themeVars">
<van-sticky>
<van-dropdown-menu active-color="#4caf50">
<van-dropdown-item v-model="value1" :options="option1" />
<van-dropdown-item v-model="value2" :options="option2" />
<van-dropdown-item v-model="value3" :options="option3" />
<van-dropdown-item v-model="value1" :options="option1" @change="handleChange1" />
<van-dropdown-item v-model="value2" :options="option2" @change="handleChange2" />
<van-dropdown-item v-model="value3" :options="option3" @change="handleChange3" />
</van-dropdown-menu>
</van-sticky>
<van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false" :style="{ height: calendarHeight }"
switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false"
......@@ -22,21 +24,21 @@
<div style="padding: 0 1rem;">
<van-row gutter="15">
<van-col span="12">
<van-button type="primary" block icon="add-square">主要按钮</van-button>
<van-button type="primary" block icon="photo" @click="handleAdd">安排打卡</van-button>
</van-col>
<van-col span="12">
<van-button type="primary" block icon="video">主要按钮</van-button>
<van-button type="primary" block icon="video">设置作业</van-button>
</van-col>
</van-row>
</div>
<div v-if="showProgress" class="text-wrapper">
<div class="text-header">目标进度</div>
<div class="text-header">打卡统计</div>
<div style="background-color: #FFF; margin-top: 1rem;">
<div class="grade-percentage-main">
<van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
<van-col span="12">
<span>作业目标</span>
<span>年级目标</span>
</van-col>
<van-col span="12" style="text-align: right;">
<span style="font-weight: bold;">{{ progress1 }}%</span>
......@@ -65,13 +67,8 @@
</div>
</div>
<van-tabs v-model:active="active" style="margin: 0 1rem;">
<van-tab title="标签 1"></van-tab>
<van-tab title="标签 2"></van-tab>
<van-tab title="标签 3"></van-tab>
</van-tabs>
<div v-if="active === 0" style="padding: 0 1rem; color: #4caf50;">
<div style="padding: 0 1rem; color: #4caf50;">
<div class="text-header">学生动态</div>
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
......@@ -92,12 +89,12 @@
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
<!-- <van-col span="3">
<div v-if="post.is_my" class="post-menu">
<van-icon name="edit" @click="editCheckin(post)" />
<van-icon name="delete-o" @click="delCheckin(post)" />
</div>
</van-col>
</van-col> -->
</van-row>
</div>
<div class="post-content">
......@@ -189,21 +186,33 @@ const value1 = ref(0);
const value2 = ref('a');
const value3 = ref('v');
const option1 = [
{ text: '全部商品', value: 0 },
{ text: '新款商品', value: 1 },
{ text: '活动商品', value: 2 },
{ text: '全部年级', value: 0 },
{ text: '一年级', value: 1 },
{ text: '二年级', value: 2 },
];
const option2 = [
{ text: '默认排序', value: 'a' },
{ text: '好评排序', value: 'b' },
{ text: '销量排序', value: 'c' },
{ text: '全部班级', value: 'a' },
{ text: '一班级', value: 'b' },
{ text: '二班级', value: 'c' },
];
const option3 = [
{ text: '默认排序', value: 'v' },
{ text: '好评排序', value: 'b' },
{ text: '销量排序', value: 'c' },
{ text: '全部课程', value: 'v' },
{ text: '一课程', value: 'b' },
{ text: '二课程', value: 'c' },
];
const handleChange1 = (val) => {
console.log('val', val);
}
const handleChange2 = (val) => {
console.log('val', val);
}
const handleChange3 = (val) => {
console.log('val', val);
}
const active = ref(0);
const myRefCalendar = ref(null);
......@@ -426,11 +435,12 @@ const stopAllVideos = () => {
});
};
const themeVars = {
const themeVars = reactive({
calendarSelectedDayBackground: '#4caf50',
calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
calendarInfoLineHeight: '0.3rem',
}
buttonNormalFontSize: '1rem',
})
const progress1 = ref(0);
// const progress2 = ref(76);
......@@ -467,11 +477,18 @@ const formatter = (day) => {
// 检查是否已签到
if (checkin_days.includes(formattedDate)) {
// 如果是当前选中的已签到日期,使用特殊样式
if (selectedDate.value === formattedDate) {
day.className = 'calendar-selected';
day.type = 'selected';
day.bottomInfo = '已签到';
} else {
day.className = 'calendar-checkin';
day.type = 'selected';
day.bottomInfo = '已签到';
}
}
}
// 选中今天的日期
if (dayjs(day.date).isSame(new Date(), 'day')) {
......@@ -483,14 +500,22 @@ const formatter = (day) => {
return day;
}
// 添加一个响应式变量来存储当前选中的日期
const selectedDate = ref('');
const onSelectDay = (day) => {
getTaskDetail(dayjs(day).format('YYYY-MM'));
// 更新当前选中的日期
const currentSelectedDate = dayjs(day).format('YYYY-MM-DD');
selectedDate.value = currentSelectedDate;
// 修改浏览器地址把当前的date加入地址栏, 页面不刷新
router.push({
path: route.path,
query: {
...route.query,
date: dayjs(day).format('YYYY-MM-DD')
date: currentSelectedDate
}
})
// 重置分页参数
......@@ -498,9 +523,10 @@ const onSelectDay = (day) => {
checkinDataList.value = []
finished.value = false
// 重新加载数据
onLoad(dayjs(day).format('YYYY-MM-DD'))
onLoad(currentSelectedDate)
}
const onClickSubtitle = (evt) => {
console.warn('点击了日期标题');
}
......@@ -685,6 +711,17 @@ const formatData = (data) => {
})
return formattedData;
}
const handleAdd = () => {
router.push({
path: '/teacher/form',
query: {
post_id: route.query.id,
type: 'image',
status: 'add',
}
})
}
</script>
<style lang="less">
......
<!--
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-19 16:46:06
* @FilePath: /mlaj/src/views/teacher/myClassPage.vue
* @Description: 我的班级页面
-->
<template>
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<!-- 用户信息卡片 -->
<div>
<div class="bg-white p-4">
<div class="flex items-center mb-4">
<van-image round width="3rem" height="3rem"
:src="userInfo.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" class="mr-3" />
<div class="flex-1">
<h2 class="text-lg font-bold text-gray-800">{{ userInfo.name }}</h2>
<div class="flex items-center mt-1">
<van-icon name="clock-o" size="20" color="#10b981" class="mr-1" />
<van-icon name="chat-o" size="20" color="#10b981" class="mr-1" />
<van-icon name="comment-circle-o" size="20" color="#10b981" />
</div>
</div>
</div>
</div>
</div>
<!-- 筛选器 -->
<div>
<van-sticky>
<van-dropdown-menu active-color="#10b981">
<van-dropdown-item v-model="gradeFilter" :options="gradeOptions" @change="handleGradeChange" />
<van-dropdown-item v-model="classFilter" :options="classOptions" @change="handleClassChange" />
<van-dropdown-item v-model="courseFilter" :options="courseOptions" @change="handleCourseChange" />
</van-dropdown-menu>
</van-sticky>
</div>
<!-- 统计数据 -->
<div>
<van-row>
<!-- 出勤率 -->
<van-col span="8">
<div class="bg-white p-4 text-center">
<div class="relative w-16 h-16 mx-auto mb-2">
<van-circle v-model:current-rate="attendanceRate" :rate="attendanceRate" :speed="100"
:text="attendanceRate + '%'" stroke-width="50" color="#10b981" size="64" />
</div>
<div class="text-sm text-gray-600">出勤率</div>
</div>
</van-col>
<!-- 任务完成 -->
<van-col span="8">
<div class="bg-white p-4 text-center">
<div class="relative w-16 h-16 mx-auto mb-2">
<van-circle v-model:current-rate="taskCompletionRate" :rate="taskCompletionRate" :speed="100"
:text="taskCompletionRate + '%'" stroke-width="50" color="#3b82f6" size="64" />
</div>
<div class="text-sm text-gray-600">任务完成</div>
</div>
</van-col>
<!-- 学习进度 -->
<van-col span="8">
<div class="bg-white p-4 text-center">
<div class="relative w-16 h-16 mx-auto mb-2">
<van-circle v-model:current-rate="learningProgress" :rate="learningProgress" :speed="100"
:text="learningProgress + '%'" stroke-width="50" color="#f59e0b" size="64" />
</div>
<div class="text-sm text-gray-600">学习进度</div>
</div>
</van-col>
</van-row>
</div>
<!-- 班级成员 -->
<div class="">
<div class="">
<!-- 标题和搜索 -->
<div class="p-4 border-b border-gray-100 bg-white">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-bold text-gray-800">班级成员 ({{ studentList.length }})</h3>
<div @click="showSortPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer">
<span>{{ sortFilter }}</span>
<van-icon name="arrow-down" size="14" class="ml-1" />
</div>
</div>
<van-search v-model="searchKeyword" placeholder="请搜索" @search="handleSearch" @input="handleSearch" />
</div>
<!-- 学生列表 -->
<div class="p-4">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="student in filteredStudentList" :key="student.id"
class="flex items-center justify-between py-3 border-gray-50 bg-white rounded-xl p-4 text-center shadow-sm mb-4"
@click="handleStudentClick(student)">
<div class="flex items-center flex-1">
<van-image round width="2.5rem" height="2.5rem"
:src="student.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" class="mr-3" />
<div class="flex-1">
<div class="flex items-center">
<span class="font-medium text-gray-800 mr-2">{{ student.name }}</span>
<van-icon v-if="student.gender === 'male'" name="friends-o" color="#3b82f6" size="14" />
<van-icon v-else name="like-o" color="#ec4899" size="14" />
</div>
<div class="text-sm text-gray-500" style="text-align: left;">{{ student.className }}</div>
</div>
</div>
<div class="text-right">
<div class="flex items-center text-sm text-gray-500 mb-1">
<van-icon name="phone-o" size="12" class="mr-1" />
<span>{{ formatPhone(student.phone) }}</span>
</div>
<div class="text-xs text-gray-400">{{ student.lastActiveTime }}</div>
</div>
<van-icon name="arrow" color="#d1d5db" size="16" class="ml-2" />
</div>
</van-list>
</div>
</div>
</div>
<!-- 排序弹窗 -->
<van-popup v-model:show="showSortPopup" position="bottom" round>
<div class="p-4">
<div class="text-center text-lg font-bold mb-4">选择排序方式</div>
<van-cell-group>
<van-cell
v-for="option in sortOptions"
:key="option.value"
:title="option.text"
clickable
@click="onSortSelect(option)"
:border="false"
:class="{ 'text-green-600': sortFilter === option.value }"
>
<template #right-icon>
<van-icon v-if="sortFilter === option.value" name="success" color="#10b981" />
</template>
</van-cell>
</van-cell-group>
<div class="mt-4">
<van-button block @click="showSortPopup = false">取消</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import AppLayout from '@/layouts/AppLayout.vue'
const router = useRouter()
// 用户信息
const userInfo = ref({
name: '我的班级',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
})
// 筛选器数据
const gradeFilter = ref('全部年级')
const classFilter = ref('全部班级')
const courseFilter = ref('全部课程')
const sortFilter = ref('按活跃度')
const searchKeyword = ref('')
const showSortPopup = ref(false)
// 筛选器选项
const gradeOptions = ref([
{ text: '全部年级', value: '全部年级' },
{ text: '高一年级', value: '高一年级' },
{ text: '高二年级', value: '高二年级' },
{ text: '高三年级', value: '高三年级' }
])
const classOptions = ref([
{ text: '全部班级', value: '全部班级' },
{ text: '高一(1)班', value: '高一(1)班' },
{ text: '高一(2)班', value: '高一(2)班' },
{ text: '高一(3)班', value: '高一(3)班' }
])
const courseOptions = ref([
{ text: '全部课程', value: '全部课程' },
{ text: '语文', value: '语文' },
{ text: '数学', value: '数学' },
{ text: '英语', value: '英语' }
])
const sortOptions = ref([
{ text: '按活跃度', value: '按活跃度' },
{ text: '按姓名', value: '按姓名' },
{ text: '按成绩', value: '按成绩' }
])
// 统计数据
const attendanceRate = ref(85)
const taskCompletionRate = ref(76)
const learningProgress = ref(92)
// 学生列表
const studentList = ref([
{
id: 1,
name: '张明',
gender: 'male',
className: '高一(3)班',
phone: '13812345678',
lastActiveTime: '5分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 2,
name: '李华',
gender: 'female',
className: '高一(3)班',
phone: '13987654321',
lastActiveTime: '10分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 3,
name: '王强',
gender: 'male',
className: '高一(2)班',
phone: '13512349876',
lastActiveTime: '15分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
{
id: 4,
name: '赵敏',
gender: 'female',
className: '高一(1)班',
phone: '13643214321',
lastActiveTime: '30分钟前活跃',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
}
])
// 列表加载状态
const loading = ref(false)
const finished = ref(false)
/**
* 过滤后的学生列表
*/
const filteredStudentList = computed(() => {
let filtered = studentList.value
// 按班级筛选
if (classFilter.value !== '全部班级') {
filtered = filtered.filter(student => student.className === classFilter.value)
}
// 按搜索关键词筛选
if (searchKeyword.value) {
filtered = filtered.filter(student =>
student.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
// 排序
if (sortFilter.value === '按姓名') {
filtered.sort((a, b) => a.name.localeCompare(b.name))
}
return filtered
})
/**
* 格式化手机号
* @param {string} phone - 手机号
* @returns {string} 格式化后的手机号
*/
const formatPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3')
}
/**
* 处理年级筛选变化
* @param {string} value - 选中的年级
*/
const handleGradeChange = (value) => {
console.log('年级筛选:', value)
// 这里可以根据年级筛选更新班级选项
}
/**
* 处理班级筛选变化
* @param {string} value - 选中的班级
*/
const handleClassChange = (value) => {
console.log('班级筛选:', value)
}
/**
* 处理课程筛选变化
* @param {string} value - 选中的课程
*/
const handleCourseChange = (value) => {
console.log('课程筛选:', value)
}
/**
* 处理排序变化
* @param {string} value - 选中的排序方式
*/
const handleSortChange = (value) => {
console.log('排序变化:', value)
}
/**
* 处理排序选择
* @param {Object} option - 选中的排序选项
*/
const onSortSelect = (option) => {
sortFilter.value = option.value
showSortPopup.value = false
handleSortChange(option.value)
}
/**
* 处理搜索
* @param {string} value - 搜索关键词
*/
const handleSearch = (value) => {
console.log('搜索:', value)
}
/**
* 处理学生点击
* @param {Object} student - 学生信息
*/
const handleStudentClick = (student) => {
console.log('点击学生:', student)
// 这里可以跳转到学生详情页
}
/**
* 加载更多数据
*/
const onLoad = () => {
setTimeout(() => {
loading.value = false
finished.value = true
}, 1000)
}
/**
* 组件挂载时初始化数据
*/
onMounted(() => {
// 这里可以调用API获取实际数据
console.log('我的班级页面已加载')
})
</script>
<style scoped>
/* 自定义样式 */
.van-dropdown-menu {
background-color: white;
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.van-circle {
font-size: 12px;
font-weight: bold;
}
.van-search {
background-color: #f9fafb;
border-radius: 0.5rem;
}
.van-list {
min-height: 200px;
}
</style>