| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- <template>
- <!--
- 班主任点名页面
- 功能:支持班级考勤管理,包括学生状态切换、座位布局显示等
- 模式:
- - overview: 初始完整筛选条件模式
- - overview-simple: 双击退出后的简化模式
- - attendance: 点名模式
- -->
- <view class="page-container"
- @touchstart="handlePageTouchStart"
- @touchmove="handlePageTouchMove"
- @touchend="handlePageTouchEnd">
- <!-- 筛选条件区域 -->
- <view class="filter-area" :class="{ 'simple-mode': attendanceMode !== 'overview' }">
- <!-- 完整模式:显示表单 -->
- <Form v-if="attendanceMode === 'overview'" :rules="fieldConfigs" v-model="formData" ref="formRef">
- <up-table>
- <up-tr>
- <up-th>班级</up-th>
- <Td field="bj">
- <SsSelect
- v-model="formData.bj"
- :options="bjOption"
- :loading="bjLoading"
- placeholder="请选择班级"
- @change="onXcTypeChange"
- />
- </Td>
- </up-tr>
- <up-tr>
- <up-th>日期</up-th>
- <Td field="rq">
- <SsInput v-model="formData.rq" placeholder="请输入日期" />
- </Td>
- </up-tr>
- <up-tr>
- <up-th>节次</up-th>
- <Td field="jc">
- <view class="onoff-button-group">
- <SsOnoffButton
- v-model="formData.jc"
- name="jc"
- label="上午"
- value="1"
- />
- <SsOnoffButton
- v-model="formData.jc"
- name="jc"
- label="下午"
- value="2"
- />
- <SsOnoffButton
- v-model="formData.jc"
- name="jc"
- label="晚上"
- value="3"
- />
- </view>
- </Td>
- </up-tr>
- </up-table>
- </Form>
- <!-- 简化模式:显示回显信息 -->
- <view v-else class="simple-display">
- <text class="display-text">{{ displayText }}</text>
- </view>
- </view>
- <!-- 点名区域 -->
- <view class="attendance-area">
- <SeatLayout
- :students="studentList"
- :layout="seatLayout"
- :mode="attendanceMode"
- @modeChange="handleModeChange"
- @statusChange="handleStatusChange"
- @studentClick="handleStudentClick"
- />
- </view>
- <!-- 下拉提示 -->
- <view v-if="attendanceMode === 'overview-simple'" class="pull-indicator" :class="{ 'show': showPullIndicator }">
- <text class="pull-text">下拉恢复筛选条件</text>
- </view>
- <!-- 底部按钮 - 只在非点名模式下显示 -->
- <SsBottom
- v-if="attendanceMode !== 'attendance'"
- :buttons="bottomButtons"
- @button-click="handleBottomAction"
- />
- </view>
- </template>
- <script setup>
- /**
- * 班主任点名页面
- *
- * 主要功能:
- * 1. 筛选条件管理(班级、日期、节次)
- * 2. 学生座位布局显示和缩放
- * 3. 点名模式切换(全览 ↔ 点名)
- * 4. 学生考勤状态管理
- * 5. 下拉手势恢复筛选条件
- * 6. 考勤数据保存和提交
- */
- import { ref, computed } from 'vue'
- import Form from '@/components/Form/index.vue'
- import Td from '@/components/Td/index.vue'
- import SsInput from '@/components/SsInput/index.vue'
- import SsSelect from '@/components/SsSelect/index.vue'
- import SsOnoffButton from '@/components/SsOnoffButton/index.vue'
- import SeatLayout from './components/SeatLayout.vue'
- import SsBottom from '@/components/SsBottom/index.vue'
- // ==================== 数据定义 ====================
- /**
- * 表单数据
- * @property {string} bj - 班级ID
- * @property {string} rq - 日期
- * @property {string} jc - 节次 (1-上午, 2-下午, 3-晚上)
- */
- const formData = ref({
- bj: '1',
- rq: '2025年6月31日 星期五',
- jc: '1', // 默认选中上午
- })
- /**
- * 点名相关状态管理
- * @property {string} attendanceMode - 当前模式
- * - 'overview': 初始完整筛选条件模式
- * - 'overview-simple': 双击退出后的简化模式
- * - 'attendance': 点名模式
- * @property {Array} studentList - 学生列表数据
- * @property {Object} seatLayout - 座位布局配置
- */
- const attendanceMode = ref('overview')
- const studentList = ref([]) // 学生列表
- const seatLayout = ref({ rows: 5, cols: 10 }) // 座位布局:5行10列
- /**
- * 下拉手势相关状态
- * 用于在 overview-simple 模式下下拉恢复完整筛选条件
- */
- const showPullIndicator = ref(false) // 是否显示下拉提示
- const pullStartY = ref(0) // 下拉起始Y坐标
- const pullDistance = ref(0) // 下拉距离
- /**
- * 计算回显文本
- * 在简化模式下显示的筛选条件摘要
- * 格式:班级名称 + 日期 + 节次
- */
- const displayText = computed(() => {
- const bjText = bjOption.value.find(item => item.v === formData.value.bj)?.n || '未选择班级'
- const jcText = formData.value.jc === '1' ? '上午' : formData.value.jc === '2' ? '下午' : '晚上'
- return `${bjText} ${formData.value.rq} ${jcText}`
- })
- // ==================== 表单配置 ====================
- /**
- * 表单字段校验配置
- * 定义各个字段的校验规则
- */
- const fieldConfigs = {
- bj: {
- rules: [{ required: true, message: '班级不能为空' }]
- },
- rq: {
- rules: [{ required: true, message: '日期不能为空' }]
- },
- jc: {
- rules: [{ required: true, message: '请选择节次' }]
- }
- }
- /**
- * 表单引用
- * 用于表单校验和数据提交
- */
- const formRef = ref(null)
- /**
- * 班级下拉框配置
- * TODO: 从接口获取班级列表数据
- */
- const bjOption = ref([
- { n: '一年级一班', v: '1' },
- { n: '一年级二班', v: '2' }
- // TODO: 调用 getClassList() 接口获取真实班级数据
- ])
- const bjLoading = ref(false) // 班级数据加载状态
- // ==================== 事件处理方法 ====================
- /**
- * 班级选择变化处理
- * 当用户选择不同班级时触发,重新加载该班级的学生数据
- * @param {string} value - 选中的班级ID
- * @param {Object} option - 选中的班级选项对象
- */
- const onXcTypeChange = (value, option) => {
- console.log('班级选择变化:', value, option)
- console.log('当前formData.bj:', formData.value.bj)
- // 重新加载学生数据
- loadStudentData()
- // TODO: 可以在这里添加其他班级变化后的逻辑
- // 例如:清空之前的考勤记录、重置座位布局等
- }
- /**
- * 加载学生数据
- * 根据当前选择的班级、日期、节次加载学生列表和座位布局
- * TODO: 替换为真实的接口调用
- */
- const loadStudentData = async () => {
- try {
- // TODO: 调用真实接口获取学生数据
- // const response = await getStudentList({
- // classId: formData.value.bj,
- // date: formData.value.rq,
- // period: formData.value.jc
- // })
- // 模拟学生数据 - 生成5行10列的座位布局
- const mockStudents = []
- for (let row = 1; row <= 5; row++) {
- for (let col = 1; col <= 10; col++) {
- const studentId = `${row}${col.toString().padStart(2, '0')}`
- mockStudents.push({
- id: studentId,
- name: `学生${studentId}`,
- code: studentId,
- avatar: '/static/logo.png', // 默认头像
- row: row, // 座位行号
- col: col, // 座位列号
- status: 81 // 考勤状态:81-出勤, 1-旷课, 11-迟到, 21-早退, 0-请假
- })
- }
- }
- studentList.value = mockStudents
- seatLayout.value = { rows: 5, cols: 10 }
- console.log('学生数据加载完成:', studentList.value.length)
- } catch (error) {
- console.error('加载学生数据失败:', error)
- uni.showToast({
- title: '加载学生数据失败',
- icon: 'none'
- })
- }
- }
- /**
- * 处理模式切换
- * 管理三种模式之间的切换逻辑
- * @param {string} newMode - 新的模式 ('overview' | 'attendance')
- */
- const handleModeChange = (newMode) => {
- console.log('模式切换:', attendanceMode.value, '->', newMode)
- if (newMode === 'overview') {
- // 从点名模式退出时,进入简化显示模式
- // 用户需要下拉手势才能恢复完整筛选条件
- attendanceMode.value = 'overview-simple'
- } else {
- // 进入点名模式
- attendanceMode.value = newMode
- }
- }
- /**
- * 处理学生状态变化
- * 当用户点击学生卡片切换考勤状态时触发
- * @param {Object} statusData - 状态变化数据
- * @param {string} statusData.studentId - 学生ID
- * @param {number} statusData.oldStatus - 原状态
- * @param {number} statusData.newStatus - 新状态
- */
- const handleStatusChange = (statusData) => {
- console.log('学生状态变化:', statusData)
- // 更新本地数据
- const student = studentList.value.find(s => s.id === statusData.studentId)
- if (student) {
- student.status = statusData.newStatus
- }
- // TODO: 调用接口同步状态到后端
- // await updateStudentStatus({
- // studentId: statusData.studentId,
- // status: statusData.newStatus,
- // classId: formData.value.bj,
- // date: formData.value.rq,
- // period: formData.value.jc
- // })
- }
- /**
- * 处理学生卡片点击
- * 记录用户点击的学生信息,可用于统计分析
- * @param {Object} student - 被点击的学生对象
- */
- const handleStudentClick = (student) => {
- console.log('点击学生:', student)
- // TODO: 可以在这里添加点击统计、学生详情查看等功能
- }
- // ==================== 手势处理 ====================
- /**
- * 页面级下拉手势开始处理
- * 只在 overview-simple 模式下响应,用于恢复完整筛选条件
- * @param {TouchEvent} e - 触摸事件对象
- */
- const handlePageTouchStart = (e) => {
- // 只在简化模式下响应下拉手势
- if (attendanceMode.value !== 'overview-simple') return
- // 安全检查,防止 touches 为空导致错误
- if (!e.touches || !e.touches[0]) return
- pullStartY.value = e.touches[0].clientY
- pullDistance.value = 0
- }
- /**
- * 页面级下拉手势移动处理
- * 计算下拉距离并显示视觉反馈
- * @param {TouchEvent} e - 触摸事件对象
- */
- const handlePageTouchMove = (e) => {
- if (attendanceMode.value !== 'overview-simple') return
- if (!e.touches || !e.touches[0]) return
- const currentY = e.touches[0].clientY
- pullDistance.value = currentY - pullStartY.value
- // 下拉超过50rpx时显示提示
- if (pullDistance.value > 50) {
- showPullIndicator.value = true
- } else {
- showPullIndicator.value = false
- }
- }
- /**
- * 页面级下拉手势结束处理
- * 根据下拉距离决定是否恢复完整筛选条件
- */
- const handlePageTouchEnd = () => {
- if (attendanceMode.value !== 'overview-simple') return
- // 下拉超过100rpx时恢复完整筛选条件
- if (pullDistance.value > 100) {
- attendanceMode.value = 'overview'
- }
- // 重置手势状态
- showPullIndicator.value = false
- pullDistance.value = 0
- }
- // ==================== 底部按钮配置 ====================
- /**
- * 底部按钮配置
- * 只在非点名模式下显示,提供取消和保存功能
- */
- const bottomButtons = [
- { text: '取消', action: 'back' },
- { text: '保存', action: 'save' }
- ]
- /**
- * 底部按钮事件处理
- * 处理取消和保存操作
- * @param {Object} data - 按钮数据
- * @param {string} data.action - 按钮动作类型
- */
- const handleBottomAction = (data) => {
- console.log('底部按钮操作:', data)
- switch(data.action) {
- case 'cancel':
- // 取消操作,可以重置数据或其他逻辑
- console.log('取消操作')
- break
- case 'back':
- // 返回上一页
- uni.navigateBack()
- break
- case 'save':
- // 保存考勤数据
- handleSave()
- break
- }
- }
- /**
- * 保存学生考勤数据
- * 收集所有学生的考勤状态并提交到后端
- * TODO: 替换为真实的接口调用
- */
- const handleSave = async () => {
- console.log('保存学生考勤数据')
- try {
- // 收集所有学生的考勤数据
- const attendanceData = studentList.value.map(student => ({
- id: student.id,
- name: student.name,
- code: student.code,
- row: student.row,
- col: student.col,
- status: student.status,
- statusText: getStatusText(student.status)
- }))
- console.log('所有学生考勤状态:', attendanceData)
- // TODO: 调用接口提交考勤数据
- // const response = await submitAttendanceData({
- // classId: formData.value.bj,
- // date: formData.value.rq,
- // period: formData.value.jc,
- // attendanceList: attendanceData
- // })
- uni.showToast({
- title: '保存成功',
- icon: 'success'
- })
- // TODO: 保存成功后可以执行其他操作
- // 例如:返回上一页、清空数据、刷新列表等
- } catch (error) {
- console.error('保存考勤数据失败:', error)
- uni.showToast({
- title: '保存失败',
- icon: 'none'
- })
- }
- }
- /**
- * 获取考勤状态文本
- * 将数字状态码转换为可读的文本描述
- * @param {number} status - 状态码
- * @returns {string} 状态文本
- */
- const getStatusText = (status) => {
- switch(status) {
- case 81: return '出勤'
- case 1: return '旷课'
- case 11: return '迟到'
- case 21: return '早退'
- case 0: return '请假'
- default: return '未知'
- }
- }
- // ==================== 页面初始化 ====================
- /**
- * 页面加载时初始化数据
- * 自动加载默认班级的学生数据
- */
- loadStudentData()
- // ==================== 接口预留 ====================
- /**
- * TODO: 需要实现的接口方法
- *
- * 1. getClassList() - 获取班级列表
- * 返回格式:[{ n: '班级名称', v: '班级ID' }]
- *
- * 2. getStudentList(params) - 获取学生列表
- * 参数:{ classId, date, period }
- * 返回格式:[{ id, name, code, avatar, row, col, status }]
- *
- * 3. updateStudentStatus(params) - 更新学生状态
- * 参数:{ studentId, status, classId, date, period }
- *
- * 4. submitAttendanceData(params) - 提交考勤数据
- * 参数:{ classId, date, period, attendanceList }
- *
- * 5. getAttendanceHistory(params) - 获取历史考勤记录
- * 参数:{ classId, startDate, endDate }
- */
- </script>
- <style lang="scss" scoped>
- .filter-area {
- transition: all 0.3s ease;
- background-color: #fff;
- // 完整模式(默认)
- &:not(.simple-mode) {
- // 显示所有筛选条件
- opacity: 1;
- }
- // 简化模式(点名状态下)
- &.simple-mode {
- .simple-display {
- padding: 30rpx 40rpx;
- background-color: #fff;
- border-bottom: 2rpx solid #E5E5E5;
- position: relative;
- .display-text {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- text-align: center;
- display: block;
- }
- .pull-indicator {
- position: absolute;
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 10rpx 30rpx;
- border-radius: 20rpx;
- font-size: 24rpx;
- opacity: 0;
- transition: opacity 0.3s ease;
- z-index: 100;
- &.show {
- opacity: 1;
- }
- .pull-text {
- white-space: nowrap;
- }
- }
- }
- }
- }
- .onoff-button-group {
- display: flex;
- flex-wrap: wrap;
- gap: 20rpx;
- align-items: flex-start;
- }
- .attendance-area {
- flex: 1;
- min-height: 80vh; // 确保有足够的高度显示内容
- overflow: hidden;
- background-color: #f5f5f5;
- padding: 20rpx;
- }
- </style>
|