kqjl_bzrDm.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <template>
  2. <!--
  3. 班主任点名页面
  4. 功能:支持班级考勤管理,包括学生状态切换、座位布局显示等
  5. 模式:
  6. - overview: 初始完整筛选条件模式
  7. - overview-simple: 双击退出后的简化模式
  8. - attendance: 点名模式
  9. -->
  10. <view class="page-container"
  11. @touchstart="handlePageTouchStart"
  12. @touchmove="handlePageTouchMove"
  13. @touchend="handlePageTouchEnd">
  14. <!-- 筛选条件区域 -->
  15. <view class="filter-area" :class="{ 'simple-mode': attendanceMode !== 'overview' }">
  16. <!-- 完整模式:显示表单 -->
  17. <Form v-if="attendanceMode === 'overview'" :rules="fieldConfigs" v-model="formData" ref="formRef">
  18. <up-table>
  19. <up-tr>
  20. <up-th>班级</up-th>
  21. <Td field="bj">
  22. <SsSelect
  23. v-model="formData.bj"
  24. :options="bjOption"
  25. :loading="bjLoading"
  26. placeholder="请选择班级"
  27. @change="onXcTypeChange"
  28. />
  29. </Td>
  30. </up-tr>
  31. <up-tr>
  32. <up-th>日期</up-th>
  33. <Td field="rq">
  34. <SsInput v-model="formData.rq" placeholder="请输入日期" />
  35. </Td>
  36. </up-tr>
  37. <up-tr>
  38. <up-th>节次</up-th>
  39. <Td field="jc">
  40. <view class="onoff-button-group">
  41. <SsOnoffButton
  42. v-model="formData.jc"
  43. name="jc"
  44. label="上午"
  45. value="1"
  46. />
  47. <SsOnoffButton
  48. v-model="formData.jc"
  49. name="jc"
  50. label="下午"
  51. value="2"
  52. />
  53. <SsOnoffButton
  54. v-model="formData.jc"
  55. name="jc"
  56. label="晚上"
  57. value="3"
  58. />
  59. </view>
  60. </Td>
  61. </up-tr>
  62. </up-table>
  63. </Form>
  64. <!-- 简化模式:显示回显信息 -->
  65. <view v-else class="simple-display">
  66. <text class="display-text">{{ displayText }}</text>
  67. </view>
  68. </view>
  69. <!-- 点名区域 -->
  70. <view class="attendance-area">
  71. <SeatLayout
  72. :students="studentList"
  73. :layout="seatLayout"
  74. :mode="attendanceMode"
  75. @modeChange="handleModeChange"
  76. @statusChange="handleStatusChange"
  77. @studentClick="handleStudentClick"
  78. />
  79. </view>
  80. <!-- 下拉提示 -->
  81. <view v-if="attendanceMode === 'overview-simple'" class="pull-indicator" :class="{ 'show': showPullIndicator }">
  82. <text class="pull-text">下拉恢复筛选条件</text>
  83. </view>
  84. <!-- 底部按钮 - 只在非点名模式下显示 -->
  85. <SsBottom
  86. v-if="attendanceMode !== 'attendance'"
  87. :buttons="bottomButtons"
  88. @button-click="handleBottomAction"
  89. />
  90. </view>
  91. </template>
  92. <script setup>
  93. /**
  94. * 班主任点名页面
  95. *
  96. * 主要功能:
  97. * 1. 筛选条件管理(班级、日期、节次)
  98. * 2. 学生座位布局显示和缩放
  99. * 3. 点名模式切换(全览 ↔ 点名)
  100. * 4. 学生考勤状态管理
  101. * 5. 下拉手势恢复筛选条件
  102. * 6. 考勤数据保存和提交
  103. */
  104. import { ref, computed } from 'vue'
  105. import Form from '@/components/Form/index.vue'
  106. import Td from '@/components/Td/index.vue'
  107. import SsInput from '@/components/SsInput/index.vue'
  108. import SsSelect from '@/components/SsSelect/index.vue'
  109. import SsOnoffButton from '@/components/SsOnoffButton/index.vue'
  110. import SeatLayout from './components/SeatLayout.vue'
  111. import SsBottom from '@/components/SsBottom/index.vue'
  112. // ==================== 数据定义 ====================
  113. /**
  114. * 表单数据
  115. * @property {string} bj - 班级ID
  116. * @property {string} rq - 日期
  117. * @property {string} jc - 节次 (1-上午, 2-下午, 3-晚上)
  118. */
  119. const formData = ref({
  120. bj: '1',
  121. rq: '2025年6月31日 星期五',
  122. jc: '1', // 默认选中上午
  123. })
  124. /**
  125. * 点名相关状态管理
  126. * @property {string} attendanceMode - 当前模式
  127. * - 'overview': 初始完整筛选条件模式
  128. * - 'overview-simple': 双击退出后的简化模式
  129. * - 'attendance': 点名模式
  130. * @property {Array} studentList - 学生列表数据
  131. * @property {Object} seatLayout - 座位布局配置
  132. */
  133. const attendanceMode = ref('overview')
  134. const studentList = ref([]) // 学生列表
  135. const seatLayout = ref({ rows: 5, cols: 10 }) // 座位布局:5行10列
  136. /**
  137. * 下拉手势相关状态
  138. * 用于在 overview-simple 模式下下拉恢复完整筛选条件
  139. */
  140. const showPullIndicator = ref(false) // 是否显示下拉提示
  141. const pullStartY = ref(0) // 下拉起始Y坐标
  142. const pullDistance = ref(0) // 下拉距离
  143. /**
  144. * 计算回显文本
  145. * 在简化模式下显示的筛选条件摘要
  146. * 格式:班级名称 + 日期 + 节次
  147. */
  148. const displayText = computed(() => {
  149. const bjText = bjOption.value.find(item => item.v === formData.value.bj)?.n || '未选择班级'
  150. const jcText = formData.value.jc === '1' ? '上午' : formData.value.jc === '2' ? '下午' : '晚上'
  151. return `${bjText} ${formData.value.rq} ${jcText}`
  152. })
  153. // ==================== 表单配置 ====================
  154. /**
  155. * 表单字段校验配置
  156. * 定义各个字段的校验规则
  157. */
  158. const fieldConfigs = {
  159. bj: {
  160. rules: [{ required: true, message: '班级不能为空' }]
  161. },
  162. rq: {
  163. rules: [{ required: true, message: '日期不能为空' }]
  164. },
  165. jc: {
  166. rules: [{ required: true, message: '请选择节次' }]
  167. }
  168. }
  169. /**
  170. * 表单引用
  171. * 用于表单校验和数据提交
  172. */
  173. const formRef = ref(null)
  174. /**
  175. * 班级下拉框配置
  176. * TODO: 从接口获取班级列表数据
  177. */
  178. const bjOption = ref([
  179. { n: '一年级一班', v: '1' },
  180. { n: '一年级二班', v: '2' }
  181. // TODO: 调用 getClassList() 接口获取真实班级数据
  182. ])
  183. const bjLoading = ref(false) // 班级数据加载状态
  184. // ==================== 事件处理方法 ====================
  185. /**
  186. * 班级选择变化处理
  187. * 当用户选择不同班级时触发,重新加载该班级的学生数据
  188. * @param {string} value - 选中的班级ID
  189. * @param {Object} option - 选中的班级选项对象
  190. */
  191. const onXcTypeChange = (value, option) => {
  192. console.log('班级选择变化:', value, option)
  193. console.log('当前formData.bj:', formData.value.bj)
  194. // 重新加载学生数据
  195. loadStudentData()
  196. // TODO: 可以在这里添加其他班级变化后的逻辑
  197. // 例如:清空之前的考勤记录、重置座位布局等
  198. }
  199. /**
  200. * 加载学生数据
  201. * 根据当前选择的班级、日期、节次加载学生列表和座位布局
  202. * TODO: 替换为真实的接口调用
  203. */
  204. const loadStudentData = async () => {
  205. try {
  206. // TODO: 调用真实接口获取学生数据
  207. // const response = await getStudentList({
  208. // classId: formData.value.bj,
  209. // date: formData.value.rq,
  210. // period: formData.value.jc
  211. // })
  212. // 模拟学生数据 - 生成5行10列的座位布局
  213. const mockStudents = []
  214. for (let row = 1; row <= 5; row++) {
  215. for (let col = 1; col <= 10; col++) {
  216. const studentId = `${row}${col.toString().padStart(2, '0')}`
  217. mockStudents.push({
  218. id: studentId,
  219. name: `学生${studentId}`,
  220. code: studentId,
  221. avatar: '/static/logo.png', // 默认头像
  222. row: row, // 座位行号
  223. col: col, // 座位列号
  224. status: 81 // 考勤状态:81-出勤, 1-旷课, 11-迟到, 21-早退, 0-请假
  225. })
  226. }
  227. }
  228. studentList.value = mockStudents
  229. seatLayout.value = { rows: 5, cols: 10 }
  230. console.log('学生数据加载完成:', studentList.value.length)
  231. } catch (error) {
  232. console.error('加载学生数据失败:', error)
  233. uni.showToast({
  234. title: '加载学生数据失败',
  235. icon: 'none'
  236. })
  237. }
  238. }
  239. /**
  240. * 处理模式切换
  241. * 管理三种模式之间的切换逻辑
  242. * @param {string} newMode - 新的模式 ('overview' | 'attendance')
  243. */
  244. const handleModeChange = (newMode) => {
  245. console.log('模式切换:', attendanceMode.value, '->', newMode)
  246. if (newMode === 'overview') {
  247. // 从点名模式退出时,进入简化显示模式
  248. // 用户需要下拉手势才能恢复完整筛选条件
  249. attendanceMode.value = 'overview-simple'
  250. } else {
  251. // 进入点名模式
  252. attendanceMode.value = newMode
  253. }
  254. }
  255. /**
  256. * 处理学生状态变化
  257. * 当用户点击学生卡片切换考勤状态时触发
  258. * @param {Object} statusData - 状态变化数据
  259. * @param {string} statusData.studentId - 学生ID
  260. * @param {number} statusData.oldStatus - 原状态
  261. * @param {number} statusData.newStatus - 新状态
  262. */
  263. const handleStatusChange = (statusData) => {
  264. console.log('学生状态变化:', statusData)
  265. // 更新本地数据
  266. const student = studentList.value.find(s => s.id === statusData.studentId)
  267. if (student) {
  268. student.status = statusData.newStatus
  269. }
  270. // TODO: 调用接口同步状态到后端
  271. // await updateStudentStatus({
  272. // studentId: statusData.studentId,
  273. // status: statusData.newStatus,
  274. // classId: formData.value.bj,
  275. // date: formData.value.rq,
  276. // period: formData.value.jc
  277. // })
  278. }
  279. /**
  280. * 处理学生卡片点击
  281. * 记录用户点击的学生信息,可用于统计分析
  282. * @param {Object} student - 被点击的学生对象
  283. */
  284. const handleStudentClick = (student) => {
  285. console.log('点击学生:', student)
  286. // TODO: 可以在这里添加点击统计、学生详情查看等功能
  287. }
  288. // ==================== 手势处理 ====================
  289. /**
  290. * 页面级下拉手势开始处理
  291. * 只在 overview-simple 模式下响应,用于恢复完整筛选条件
  292. * @param {TouchEvent} e - 触摸事件对象
  293. */
  294. const handlePageTouchStart = (e) => {
  295. // 只在简化模式下响应下拉手势
  296. if (attendanceMode.value !== 'overview-simple') return
  297. // 安全检查,防止 touches 为空导致错误
  298. if (!e.touches || !e.touches[0]) return
  299. pullStartY.value = e.touches[0].clientY
  300. pullDistance.value = 0
  301. }
  302. /**
  303. * 页面级下拉手势移动处理
  304. * 计算下拉距离并显示视觉反馈
  305. * @param {TouchEvent} e - 触摸事件对象
  306. */
  307. const handlePageTouchMove = (e) => {
  308. if (attendanceMode.value !== 'overview-simple') return
  309. if (!e.touches || !e.touches[0]) return
  310. const currentY = e.touches[0].clientY
  311. pullDistance.value = currentY - pullStartY.value
  312. // 下拉超过50rpx时显示提示
  313. if (pullDistance.value > 50) {
  314. showPullIndicator.value = true
  315. } else {
  316. showPullIndicator.value = false
  317. }
  318. }
  319. /**
  320. * 页面级下拉手势结束处理
  321. * 根据下拉距离决定是否恢复完整筛选条件
  322. */
  323. const handlePageTouchEnd = () => {
  324. if (attendanceMode.value !== 'overview-simple') return
  325. // 下拉超过100rpx时恢复完整筛选条件
  326. if (pullDistance.value > 100) {
  327. attendanceMode.value = 'overview'
  328. }
  329. // 重置手势状态
  330. showPullIndicator.value = false
  331. pullDistance.value = 0
  332. }
  333. // ==================== 底部按钮配置 ====================
  334. /**
  335. * 底部按钮配置
  336. * 只在非点名模式下显示,提供取消和保存功能
  337. */
  338. const bottomButtons = [
  339. { text: '取消', action: 'back' },
  340. { text: '保存', action: 'save' }
  341. ]
  342. /**
  343. * 底部按钮事件处理
  344. * 处理取消和保存操作
  345. * @param {Object} data - 按钮数据
  346. * @param {string} data.action - 按钮动作类型
  347. */
  348. const handleBottomAction = (data) => {
  349. console.log('底部按钮操作:', data)
  350. switch(data.action) {
  351. case 'cancel':
  352. // 取消操作,可以重置数据或其他逻辑
  353. console.log('取消操作')
  354. break
  355. case 'back':
  356. // 返回上一页
  357. uni.navigateBack()
  358. break
  359. case 'save':
  360. // 保存考勤数据
  361. handleSave()
  362. break
  363. }
  364. }
  365. /**
  366. * 保存学生考勤数据
  367. * 收集所有学生的考勤状态并提交到后端
  368. * TODO: 替换为真实的接口调用
  369. */
  370. const handleSave = async () => {
  371. console.log('保存学生考勤数据')
  372. try {
  373. // 收集所有学生的考勤数据
  374. const attendanceData = studentList.value.map(student => ({
  375. id: student.id,
  376. name: student.name,
  377. code: student.code,
  378. row: student.row,
  379. col: student.col,
  380. status: student.status,
  381. statusText: getStatusText(student.status)
  382. }))
  383. console.log('所有学生考勤状态:', attendanceData)
  384. // TODO: 调用接口提交考勤数据
  385. // const response = await submitAttendanceData({
  386. // classId: formData.value.bj,
  387. // date: formData.value.rq,
  388. // period: formData.value.jc,
  389. // attendanceList: attendanceData
  390. // })
  391. uni.showToast({
  392. title: '保存成功',
  393. icon: 'success'
  394. })
  395. // TODO: 保存成功后可以执行其他操作
  396. // 例如:返回上一页、清空数据、刷新列表等
  397. } catch (error) {
  398. console.error('保存考勤数据失败:', error)
  399. uni.showToast({
  400. title: '保存失败',
  401. icon: 'none'
  402. })
  403. }
  404. }
  405. /**
  406. * 获取考勤状态文本
  407. * 将数字状态码转换为可读的文本描述
  408. * @param {number} status - 状态码
  409. * @returns {string} 状态文本
  410. */
  411. const getStatusText = (status) => {
  412. switch(status) {
  413. case 81: return '出勤'
  414. case 1: return '旷课'
  415. case 11: return '迟到'
  416. case 21: return '早退'
  417. case 0: return '请假'
  418. default: return '未知'
  419. }
  420. }
  421. // ==================== 页面初始化 ====================
  422. /**
  423. * 页面加载时初始化数据
  424. * 自动加载默认班级的学生数据
  425. */
  426. loadStudentData()
  427. // ==================== 接口预留 ====================
  428. /**
  429. * TODO: 需要实现的接口方法
  430. *
  431. * 1. getClassList() - 获取班级列表
  432. * 返回格式:[{ n: '班级名称', v: '班级ID' }]
  433. *
  434. * 2. getStudentList(params) - 获取学生列表
  435. * 参数:{ classId, date, period }
  436. * 返回格式:[{ id, name, code, avatar, row, col, status }]
  437. *
  438. * 3. updateStudentStatus(params) - 更新学生状态
  439. * 参数:{ studentId, status, classId, date, period }
  440. *
  441. * 4. submitAttendanceData(params) - 提交考勤数据
  442. * 参数:{ classId, date, period, attendanceList }
  443. *
  444. * 5. getAttendanceHistory(params) - 获取历史考勤记录
  445. * 参数:{ classId, startDate, endDate }
  446. */
  447. </script>
  448. <style lang="scss" scoped>
  449. .filter-area {
  450. transition: all 0.3s ease;
  451. background-color: #fff;
  452. // 完整模式(默认)
  453. &:not(.simple-mode) {
  454. // 显示所有筛选条件
  455. opacity: 1;
  456. }
  457. // 简化模式(点名状态下)
  458. &.simple-mode {
  459. .simple-display {
  460. padding: 30rpx 40rpx;
  461. background-color: #fff;
  462. border-bottom: 2rpx solid #E5E5E5;
  463. position: relative;
  464. .display-text {
  465. font-size: 32rpx;
  466. font-weight: bold;
  467. color: #333;
  468. text-align: center;
  469. display: block;
  470. }
  471. .pull-indicator {
  472. position: absolute;
  473. top: 100%;
  474. left: 50%;
  475. transform: translateX(-50%);
  476. background-color: rgba(0, 0, 0, 0.7);
  477. color: white;
  478. padding: 10rpx 30rpx;
  479. border-radius: 20rpx;
  480. font-size: 24rpx;
  481. opacity: 0;
  482. transition: opacity 0.3s ease;
  483. z-index: 100;
  484. &.show {
  485. opacity: 1;
  486. }
  487. .pull-text {
  488. white-space: nowrap;
  489. }
  490. }
  491. }
  492. }
  493. }
  494. .onoff-button-group {
  495. display: flex;
  496. flex-wrap: wrap;
  497. gap: 20rpx;
  498. align-items: flex-start;
  499. }
  500. .attendance-area {
  501. flex: 1;
  502. min-height: 80vh; // 确保有足够的高度显示内容
  503. overflow: hidden;
  504. background-color: #f5f5f5;
  505. padding: 20rpx;
  506. }
  507. </style>