bjdm_bzrDm.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. <template>
  2. <!--
  3. 班主任点名页面
  4. 功能:支持班级考勤管理,包括学生状态切换、表格显示等
  5. -->
  6. <view class="page-container">
  7. <!-- 固定头部区域 -->
  8. <view class="fixed-header">
  9. <!-- 筛选条件区域 -->
  10. <view class="filter-area" :class="{ 'simple-mode': isSimpleMode }">
  11. <!-- 完整模式:显示表单 -->
  12. <view v-if="!isSimpleMode" class="full-form" style="position: relative;z-index: 1000;">
  13. <Form :rules="fieldConfigs" v-model="formData" ref="formRef">
  14. <up-table>
  15. <up-tr>
  16. <up-th>班级</up-th>
  17. <Td field="bjid">
  18. <SsSelect v-model="formData.bjid" :options="bjOption" :loading="bjLoading"
  19. placeholder="请选择班级" @change="onXcTypeChange" />
  20. </Td>
  21. </up-tr>
  22. <up-tr>
  23. <up-th>日期</up-th>
  24. <Td field="rq">
  25. <SsDatetimePicker v-model="formData.rq" mode="date" :max-date="maxDate"
  26. :z-index="99999" placeholder="请选择日期" @change="onDateChange" />
  27. <text v-if="weekdayText" class="weekday-text">{{ weekdayText }}</text>
  28. </Td>
  29. </up-tr>
  30. <up-tr>
  31. <up-th>节次</up-th>
  32. <Td field="jc">
  33. <view class="onoff-button-group">
  34. <SsOnoffButton v-model="formData.jc" name="jc" label="上午" value="1" />
  35. <SsOnoffButton v-model="formData.jc" name="jc" label="下午" value="2" />
  36. <SsOnoffButton v-model="formData.jc" name="jc" label="晚上" value="3" />
  37. </view>
  38. </Td>
  39. </up-tr>
  40. </up-table>
  41. </Form>
  42. </view>
  43. <!-- 简化模式:显示回显信息 -->
  44. <view v-else class="simple-display">
  45. <text class="display-text">{{ displayText }}</text>
  46. </view>
  47. </view>
  48. <!-- 考勤统计 -->
  49. <view class="stats-area">
  50. <view class="stat-item "><text class="stat-absent">缺勤:</text>{{ stats.absent }}人</view>
  51. <view class="stat-item "><text class="stat-sick">请假:</text>{{ stats.leave }}人</view>
  52. <view class="stat-item "><text class="stat-parent">家长请假:</text>{{ stats.parent }}人</view>
  53. </view>
  54. </view>
  55. <!-- 学生列表区域 -->
  56. <view class="student-list-area" :style="studentListStyle">
  57. <!-- 学生表格 -->
  58. <view class="student-table">
  59. <!-- 表头 -->
  60. <view class="table-header">
  61. <text class="col-number">序号</text>
  62. <text class="col-name">姓名</text>
  63. <text class="col-status">状态</text>
  64. </view>
  65. <!-- 学生行 -->
  66. <view v-for="(student, index) in studentList" :key="student.id" class="table-row"
  67. :class="getRowClass(student.kqlbm, student.rcid)" @click="handleStudentClick(student)">
  68. <text class="col-number">{{ index < 10 ? '0' + (index + 1) : index + 1 }}</text>
  69. <text class="col-name">{{ student.xm }}</text>
  70. <text class="col-status">{{ getStatusText(student.kqlbm, student.rcid) }}</text>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 底部按钮 -->
  75. <SsBottom :buttons="bottomButtons" @button-click="handleBottomAction" />
  76. <!-- 二次确认弹窗 -->
  77. <SsConfirm v-model:visible="showConfirm" title="点名情况" width="90%" height="42vh" :bottom-buttons="confirmButtons"
  78. @button-click="handleConfirmAction" @close="handleConfirmClose">
  79. <!-- 统计信息 -->
  80. <view class="stats-section">
  81. <view class="stat-item">
  82. <text class="stat-label">出勤:</text>
  83. <text class="stat-value present">{{ attendanceStats.present }}人</text>
  84. </view>
  85. <view class="stat-item">
  86. <text class="stat-label">缺勤:</text>
  87. <text class="stat-value absent">{{ attendanceStats.absent }}人</text>
  88. </view>
  89. <view class="stat-item">
  90. <text class="stat-label">事假:</text>
  91. <text class="stat-value leave">{{ attendanceStats.leave }}人</text>
  92. </view>
  93. <view class="stat-item">
  94. <text class="stat-label">病假:</text>
  95. <text class="stat-value sick">{{ attendanceStats.sick }}人</text>
  96. </view>
  97. <view class="stat-item">
  98. <text class="stat-label">家长请假:</text>
  99. <text class="stat-value parent-leave">{{ attendanceStats.parentLeave }}人</text>
  100. </view>
  101. </view>
  102. </SsConfirm>
  103. </view>
  104. </template>
  105. <script setup>
  106. /**
  107. * 班主任点名页面
  108. *
  109. * 主要功能:
  110. * 1. 筛选条件管理(班级、日期、节次)
  111. * 2. 学生表格显示和状态管理
  112. * 3. 三行变一行的下拉切换
  113. * 4. 学生考勤状态管理
  114. * 5. 考勤数据保存和提交
  115. */
  116. import { ref, computed, onMounted, onUnmounted } from 'vue'
  117. import Form from '@/components/Form/index.vue'
  118. import Td from '@/components/Td/index.vue'
  119. import SsDatetimePicker from '@/components/SsDatetimePicker/index.vue'
  120. import SsSelect from '@/components/SsSelect/index.vue'
  121. import SsOnoffButton from '@/components/SsOnoffButton/index.vue'
  122. import SsBottom from '@/components/SsBottom/index.vue'
  123. import SsConfirm from '@/components/SsConfirm/index.vue'
  124. import { commonApi } from '@/api/common'
  125. import { kqjlApi } from '@/api/kqjl'
  126. import { goBack } from '@/utils/navigation'
  127. // ==================== 数据定义 ====================
  128. /**
  129. * 表单数据
  130. * @property {string} bjid - 班级ID
  131. * @property {string} rq - 日期
  132. * @property {string} jc - 节次 (1-上午, 2-下午, 3-晚上)
  133. */
  134. const formData = ref({
  135. bjid: '',
  136. rq: '', // 将在初始化时设置为今天
  137. jc: '', // 将根据当前时间自动设置
  138. })
  139. // 日期相关数据
  140. const maxDate = ref(new Date()) // 最大日期为今天,禁止选择今天之后的
  141. const weekdayText = ref('') // 星期几的显示文本
  142. /**
  143. * 页面状态管理
  144. * @property {boolean} isSimpleMode - 是否为简化模式(三行变一行)
  145. * @property {Array} studentList - 学生列表数据
  146. * @property {boolean} refresherTriggered - 下拉刷新状态
  147. */
  148. const isSimpleMode = ref(false) // 是否为简化模式
  149. const studentList = ref([]) // 学生列表
  150. // 确认弹窗相关
  151. const showConfirm = ref(false) // 是否显示确认弹窗
  152. /**
  153. * 计算回显文本
  154. * 在简化模式下显示的筛选条件摘要
  155. * 格式:班级名称 + 日期 + 节次
  156. */
  157. const displayText = computed(() => {
  158. // const bjText = bjOption.value.find(item => item.v === formData.value.bjid)?.n || '未选择班级'
  159. const jcText = formData.value.jc === '1' ? '上午' : formData.value.jc === '2' ? '下午' : '晚上'
  160. return `${formData.value.rq} ${jcText}`
  161. })
  162. /**
  163. * 计算考勤统计
  164. */
  165. const stats = computed(() => {
  166. const absent = studentList.value.filter(s => s.kqlbm === 1).length // 缺勤
  167. const sick = studentList.value.filter(s => s.kqlbm === 41).length // 病假
  168. const personal = studentList.value.filter(s => s.kqlbm === 31).length // 事假
  169. const leave = sick + personal // 请假 = 病假 + 事假
  170. const parent = studentList.value.filter(s => s.rcid > 0).length // 家长请假
  171. return { absent, leave, parent }
  172. })
  173. /**
  174. * 计算学生列表区域的样式
  175. */
  176. const studentListStyle = computed(() => {
  177. return {
  178. marginTop: headerHeight.value + 'rpx'
  179. }
  180. })
  181. // ==================== 确认弹窗配置 ====================
  182. /**
  183. * 确认弹窗按钮配置
  184. */
  185. const confirmButtons = ref([
  186. { text: '取消', action: 'cancel', type: 'default' },
  187. { text: '确认提交', action: 'confirm', type: 'primary' }
  188. ])
  189. /**
  190. * 考勤统计数据
  191. */
  192. const attendanceStats = computed(() => {
  193. const stats = {
  194. present: 0,
  195. absent: 0,
  196. leave: 0,
  197. sick: 0,
  198. parentLeave: 0
  199. }
  200. studentList.value.forEach(student => {
  201. if (student.rcid > 0) {
  202. stats.parentLeave++ // 家长请假(优先判断)
  203. } else if (student.kqlbm === 81) {
  204. stats.present++ // 出勤
  205. } else if (student.kqlbm === 1) {
  206. stats.absent++ // 缺勤
  207. } else if (student.kqlbm === 31) {
  208. stats.leave++ // 事假
  209. } else if (student.kqlbm === 41) {
  210. stats.sick++ // 病假
  211. } else {
  212. stats.absent++ // 其他情况默认为缺勤
  213. }
  214. })
  215. console.log('考勤统计更新:', stats, '学生总数:', studentList.value.length)
  216. return stats
  217. })
  218. // ==================== 表单配置 ====================
  219. /**
  220. * 表单字段校验配置
  221. * 定义各个字段的校验规则
  222. */
  223. const fieldConfigs = {
  224. bjid: {
  225. rules: [{ required: true, message: '班级不能为空' }]
  226. },
  227. rq: {
  228. rules: [{ required: true, message: '日期不能为空' }]
  229. },
  230. jc: {
  231. rules: [{ required: true, message: '请选择节次' }]
  232. }
  233. }
  234. /**
  235. * 表单引用
  236. * 用于表单校验和数据提交
  237. */
  238. const formRef = ref(null)
  239. /**
  240. * 班级下拉框配置
  241. * 通过字典API获取班级列表数据
  242. */
  243. const bjOption = ref([])
  244. const bjLoading = ref(false) // 班级数据加载状态
  245. /**
  246. * 加载班级选项
  247. */
  248. const loadClassOptions = async () => {
  249. bjLoading.value = true
  250. try {
  251. const result = await commonApi.getDictOptionsByCbName('bj')
  252. console.log('班级字典数据:', result)
  253. // 现在后端返回正常的JSON对象格式
  254. if (result.data && result.data.result) {
  255. const resultData = result.data.result
  256. console.log('班级result数据:', resultData)
  257. // 转换为SsSelect需要的格式:{"40211":"A班","40212":"A2班"} -> [{n:"A班",v:"40211"}]
  258. bjOption.value = Object.keys(resultData).map(key => ({
  259. n: resultData[key], // 显示名称:A班、A2班
  260. v: key // 值:40211、40212
  261. }))
  262. // 默认选中第一个
  263. if (bjOption.value.length > 0 && !formData.value.bjid) {
  264. formData.value.bjid = bjOption.value[0].v
  265. console.log('默认选中班级:', bjOption.value[0])
  266. loadStudentData()
  267. }
  268. } else {
  269. console.error('数据格式不正确:', result.data)
  270. }
  271. } catch (error) {
  272. console.error('加载班级选项失败:', error)
  273. uni.showToast({
  274. title: '加载班级失败',
  275. icon: 'error'
  276. })
  277. } finally {
  278. bjLoading.value = false
  279. }
  280. }
  281. /**
  282. * 初始化表单默认值
  283. */
  284. const initFormDefaults = () => {
  285. // 设置默认日期为今天
  286. const today = new Date()
  287. const year = today.getFullYear()
  288. const month = String(today.getMonth() + 1).padStart(2, '0')
  289. const day = String(today.getDate()).padStart(2, '0')
  290. formData.value.rq = `${year}-${month}-${day}`
  291. // 更新星期几显示
  292. updateWeekdayText(formData.value.rq)
  293. // 根据当前时间设置默认节次 - 使用国际通用标准
  294. const currentTime = new Date()
  295. const currentHour = currentTime.getHours()
  296. let currentPeriod = ''
  297. let jcValue = '1'
  298. if (currentHour >= 0 && currentHour < 12) {
  299. // 0-12点:上午
  300. currentPeriod = '上午'
  301. jcValue = '1'
  302. } else if (currentHour >= 12 && currentHour < 18) {
  303. // 12-18点:下午
  304. currentPeriod = '下午'
  305. jcValue = '2'
  306. } else {
  307. // 18-24点:晚上
  308. currentPeriod = '晚上'
  309. jcValue = '3'
  310. }
  311. formData.value.jc = jcValue
  312. console.log('初始化表单默认值:', formData.value, '当前时间:', currentHour + '点', '时间段:', currentPeriod)
  313. }
  314. /**
  315. * 更新星期几显示文本
  316. */
  317. const updateWeekdayText = (dateStr) => {
  318. if (!dateStr) {
  319. weekdayText.value = ''
  320. return
  321. }
  322. try {
  323. const date = new Date(dateStr)
  324. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  325. weekdayText.value = weekdays[date.getDay()]
  326. } catch (error) {
  327. console.error('解析日期失败:', error)
  328. weekdayText.value = ''
  329. }
  330. }
  331. /**
  332. * 日期选择变化处理
  333. */
  334. const onDateChange = (value) => {
  335. console.log('日期选择变化:', value)
  336. updateWeekdayText(value)
  337. }
  338. // ==================== 事件处理方法 ====================
  339. /**
  340. * 班级选择变化处理
  341. * 当用户选择不同班级时触发,重新加载该班级的学生数据
  342. * @param {string} value - 选中的班级ID
  343. * @param {Object} option - 选中的班级选项对象
  344. */
  345. const onXcTypeChange = (value, option) => {
  346. console.log('班级选择变化:', value, option)
  347. console.log('当前formData.bj:', formData.value.bjid)
  348. // 重新加载学生数据
  349. loadStudentData()
  350. }
  351. /**
  352. * 加载学生数据
  353. * 根据当前选择的班级、日期、节次加载学生列表
  354. * TODO: 替换为真实的接口调用
  355. */
  356. const loadStudentData = async () => {
  357. try {
  358. const res = await kqjlApi.bjdm_initBzrDm({ bjid: formData.value.bjid })
  359. console.log('返回数据:', res.data.bjList)
  360. if (res.data.bjList && res.data.bjList.length > 0) {
  361. studentList.value = res.data.bjList[0]
  362. } else {
  363. console.log('没有数据:', res.data.msg)
  364. uni.showToast({
  365. title: res.data.msg,
  366. icon: 'none'
  367. })
  368. }
  369. } catch (error) {
  370. console.error('加载学生数据失败:', error)
  371. uni.showToast({
  372. title: '加载学生数据失败',
  373. icon: 'none'
  374. })
  375. }
  376. }
  377. /**
  378. * 处理学生点击
  379. * 切换学生的考勤状态
  380. * @param {Object} student - 被点击的学生对象
  381. */
  382. const handleStudentClick = (student) => {
  383. console.log('点击学生:', student)
  384. // 状态切换逻辑:出勤(0) → 缺勤(1) → 病假(2) → 事假(3) → 家长请假(4) → 出勤(0)
  385. let newStatus
  386. switch (student.kqlbm) {
  387. case 81: // 出勤 → 缺勤
  388. newStatus = 1
  389. break
  390. case 1: // 缺勤 → 病假
  391. newStatus = 41
  392. break
  393. case 41: // 病假 → 事假
  394. newStatus = 31
  395. break
  396. case 31: // 事假 → 出勤
  397. newStatus = 81
  398. break
  399. case 4: // 家长请假 → 不可点击
  400. newStatus = 4
  401. break;
  402. default: // 其他状态 → 出勤
  403. newStatus = 81
  404. }
  405. // 更新本地数据
  406. const targetStudent = studentList.value.find(s => s.ryid === student.ryid)
  407. if (targetStudent) {
  408. targetStudent.kqlbm = newStatus
  409. }
  410. }
  411. // 页面滚动位置跟踪
  412. const pageScrollTop = ref(0)
  413. const initialFilterHeight = ref(0) // 筛选区域的初始高度
  414. const headerHeight = ref(300) // 固定头部高度(rpx),初始值设大一点避免遮挡
  415. /**
  416. * 处理页面滚动事件
  417. * 监听整个页面的滚动位置
  418. */
  419. const handlePageScroll = (scrollTop) => {
  420. const previousScrollTop = pageScrollTop.value
  421. pageScrollTop.value = scrollTop
  422. console.log('页面滚动位置:', scrollTop, '初始高度:', initialFilterHeight.value)
  423. // 检测滚动方向和位置
  424. const isScrollingDown = scrollTop > previousScrollTop
  425. const isScrollingUp = scrollTop < previousScrollTop
  426. // 三行变一行:向下滚动超过筛选区域高度的30px时触发
  427. if (isScrollingDown &&
  428. scrollTop > (initialFilterHeight.value + 30) &&
  429. !isSimpleMode.value) {
  430. console.log('向下滚动超过阈值,切换到简化模式')
  431. isSimpleMode.value = true
  432. }
  433. // 一行变三行:向上滚动回到接近顶部时触发
  434. if (isScrollingUp &&
  435. scrollTop <= 10 &&
  436. isSimpleMode.value) {
  437. console.log('向上滚动回到顶部,切换到完整模式')
  438. isSimpleMode.value = false
  439. }
  440. }
  441. /**
  442. * 获取表格行的样式类
  443. * 根据学生状态返回对应的CSS类名
  444. * @param {number} status - 学生状态
  445. * @param {number} rcid - 日程id
  446. * @returns {string} CSS类名
  447. */
  448. const getRowClass = (status, rcid) => {
  449. if (rcid > 0) {
  450. return 'row-parent' // 家长请假
  451. } else {
  452. switch (status) {
  453. case 1: return 'row-absent' // 缺勤
  454. case 41: return 'row-sick' // 病假
  455. case 31: return 'row-personal' // 事假
  456. default: return 'row-normal' // 出勤
  457. }
  458. }
  459. }
  460. /**
  461. * 获取考勤状态文本
  462. * 将数字状态码转换为可读的文本描述
  463. * @param {number} status - 状态码
  464. * @param {number} rcid - 日程id
  465. * @returns {string} 状态文本
  466. */
  467. const getStatusText = (status, rcid) => {
  468. if (rcid > 0) {
  469. return '家长请假'
  470. } else {
  471. switch (status) {
  472. case 81: return '出勤'
  473. case 1: return '缺勤'
  474. case 41: return '病假'
  475. case 31: return '事假'
  476. // case 4: return '家长请假'
  477. default: return '出勤'
  478. }
  479. }
  480. }
  481. // ==================== 底部按钮配置 ====================
  482. /**
  483. * 底部按钮配置
  484. * 只在非点名模式下显示,提供取消和保存功能
  485. */
  486. const bottomButtons = [
  487. { text: '取消', action: 'back' },
  488. { text: '保存', action: 'save' }
  489. ]
  490. /**
  491. * 底部按钮事件处理
  492. * 处理取消和保存操作
  493. * @param {Object} data - 按钮数据
  494. * @param {string} data.action - 按钮动作类型
  495. */
  496. const handleBottomAction = (data) => {
  497. console.log('底部按钮操作:', data)
  498. switch (data.action) {
  499. case 'back':
  500. // 返回处理
  501. goBack()
  502. break
  503. case 'save':
  504. // 显示二次确认弹窗
  505. showConfirm.value = true
  506. break
  507. }
  508. }
  509. /**
  510. * 确认弹窗按钮点击处理
  511. */
  512. const handleConfirmAction = (data) => {
  513. console.log('确认弹窗按钮点击:', data)
  514. switch (data.action) {
  515. case 'cancel':
  516. showConfirm.value = false
  517. break
  518. case 'confirm':
  519. // 执行实际的保存操作
  520. showConfirm.value = false
  521. handleSave()
  522. break
  523. }
  524. }
  525. /**
  526. * 确认弹窗关闭处理
  527. */
  528. const handleConfirmClose = () => {
  529. console.log('确认弹窗关闭')
  530. showConfirm.value = false
  531. }
  532. const getKssj = () => {
  533. let kssj = ''
  534. if (formData.value.jc === '1') {
  535. kssj = formData.value.rq + ' 06:00:00'
  536. } else if (formData.value.jc === '2') {
  537. kssj = formData.value.rq + ' 13:40:00'
  538. } else if (formData.value.jc === '3') {
  539. kssj = formData.value.rq + ' 18:30:00'
  540. }
  541. return kssj
  542. }
  543. const getJssj = () => {
  544. let jssj = ''
  545. if (formData.value.jc === '1') {
  546. jssj = formData.value.rq + ' 12:00:00'
  547. } else if (formData.value.jc === '2') {
  548. jssj = formData.value.rq + ' 16:00:00'
  549. } else if (formData.value.jc === '3') {
  550. jssj = formData.value.rq + ' 21:00:00'
  551. }
  552. return jssj
  553. }
  554. const getKkrs = () => {
  555. let kkrs = 0
  556. studentList.value.forEach(student => {
  557. if (student.kqlbm === 1) {
  558. kkrs++
  559. }
  560. })
  561. return kkrs
  562. }
  563. const getQjrs = () => {
  564. let qjrs = 0
  565. studentList.value.forEach(student => {
  566. if (student.kqlbm === 41 || student.kqlbm === 31 || student.rcid > 0) {
  567. qjrs++
  568. }
  569. })
  570. return qjrs
  571. }
  572. /**
  573. * 保存学生考勤数据
  574. * 收集所有学生的考勤状态并提交到后端
  575. */
  576. const handleSave = async () => {
  577. console.log('保存学生考勤数据')
  578. try {
  579. const params = {
  580. bjid: formData.value.bjid,
  581. kkrs: getKkrs(), //缺勤人数
  582. qjrs: getQjrs(), //请假人数
  583. // rq: formData.value.rq + ' 00:00:00', //补齐时间
  584. jkssj: getKssj(),
  585. jjssj: getJssj(),
  586. ryList: JSON.stringify(studentList.value
  587. .filter(student => student.kqlbm !== 81)
  588. .map(student => ({
  589. ryid: student.ryid,
  590. kqlbm: student.kqlbm
  591. })))
  592. }
  593. console.log(JSON.stringify(params))
  594. const response = await kqjlApi.bjdm_saveBzrDm(params)
  595. console.log('保存考勤数据成功:', response)
  596. if (response.data.ssCode === 0) {
  597. uni.showToast({
  598. title: response.data.msg,
  599. icon: 'success'
  600. })
  601. goBack()
  602. }
  603. } catch (error) {
  604. console.error('保存考勤数据失败:', error)
  605. uni.showToast({
  606. title: '保存失败',
  607. icon: 'none'
  608. })
  609. }
  610. }
  611. // ==================== 页面初始化 ====================
  612. /**
  613. * 页面挂载时的初始化
  614. */
  615. onMounted(() => {
  616. // 初始化表单默认值
  617. initFormDefaults()
  618. // 加载班级选项
  619. loadClassOptions()
  620. // 加载学生数据
  621. // loadStudentData()
  622. // 多次尝试获取固定头部的高度,确保准确
  623. const measureHeader = () => {
  624. uni.createSelectorQuery().select('.fixed-header').boundingClientRect((rect) => {
  625. if (rect && rect.height > 0) {
  626. initialFilterHeight.value = rect.height
  627. const headerHeightRpx = rect.height * 2 // px转rpx
  628. headerHeight.value = headerHeightRpx
  629. console.log('固定头部高度:', rect.height, 'px =', headerHeightRpx, 'rpx')
  630. } else {
  631. // 如果还没获取到,再试一次
  632. setTimeout(measureHeader, 50)
  633. }
  634. }).exec()
  635. }
  636. // 立即尝试测量,然后延迟再测量一次确保准确
  637. measureHeader()
  638. setTimeout(measureHeader, 200)
  639. })
  640. /**
  641. * 页面卸载时清理
  642. */
  643. onUnmounted(() => {
  644. // 清理工作
  645. })
  646. // 在uni-app中,我们需要在页面级别定义onPageScroll
  647. // 这里先定义函数,然后在页面配置中使用
  648. // ==================== 接口预留 ====================
  649. /**
  650. * TODO: 需要实现的接口方法
  651. *
  652. * 1. getClassList() - 获取班级列表
  653. * 返回格式:[{ n: '班级名称', v: '班级ID' }]
  654. *
  655. * 2. getStudentList(params) - 获取学生列表
  656. * 参数:{ classId, date, period }
  657. * 返回格式:[{ id, name, code, avatar, row, col, status }]
  658. *
  659. * 3. updateStudentStatus(params) - 更新学生状态
  660. * 参数:{ studentId, status, classId, date, period }
  661. *
  662. * 4. submitAttendanceData(params) - 提交考勤数据
  663. * 参数:{ classId, date, period, attendanceList }
  664. *
  665. * 5. getAttendanceHistory(params) - 获取历史考勤记录
  666. * 参数:{ classId, startDate, endDate }
  667. */
  668. // 将handlePageScroll挂载到全局,供页面配置使用
  669. if (typeof globalThis !== 'undefined') {
  670. globalThis.pageScrollHandler = handlePageScroll
  671. }
  672. </script>
  673. <script>
  674. // 页面配置,监听页面滚动
  675. export default {
  676. onPageScroll(e) {
  677. // 调用全局的滚动处理函数
  678. if (typeof globalThis !== 'undefined' && globalThis.pageScrollHandler) {
  679. globalThis.pageScrollHandler(e.scrollTop)
  680. }
  681. }
  682. }
  683. </script>
  684. <style lang="scss" scoped>
  685. .page-container {
  686. display: flex;
  687. flex-direction: column;
  688. height: 100vh;
  689. background-color: #f2f3f4;
  690. }
  691. .fixed-header {
  692. position: fixed;
  693. top: 0;
  694. left: 0;
  695. right: 0;
  696. z-index: 100;
  697. background-color: #f2f3f4;
  698. }
  699. .filter-area {
  700. // background-color: #fff;
  701. transition: all 0.3s ease;
  702. border-bottom: 1rpx solid #eceded;
  703. // 完整模式
  704. .full-form {
  705. padding: 18rpx 0;
  706. }
  707. // 简化模式
  708. &.simple-mode {
  709. .simple-display {
  710. padding: 45rpx 0;
  711. background-color: #f2f3f4;
  712. border-bottom: 1rpx solid #eceded;
  713. .display-text {
  714. font-size: 32rpx;
  715. font-weight: bold;
  716. color: #333;
  717. text-align: center;
  718. display: block;
  719. }
  720. }
  721. }
  722. }
  723. .onoff-button-group {
  724. display: flex;
  725. flex-wrap: wrap;
  726. gap: 20rpx;
  727. align-items: flex-start;
  728. }
  729. .stats-area {
  730. background-color: #fff;
  731. padding: 20rpx;
  732. display: flex;
  733. align-items: center;
  734. gap: 20rpx;
  735. .stat-item {
  736. font-size: 24rpx;
  737. font-weight: 500;
  738. }
  739. .stat-absent {
  740. color: #ff0000;
  741. }
  742. .stat-sick {
  743. color: #ee9700;
  744. }
  745. .stat-parent {
  746. color: #00a0e9;
  747. }
  748. }
  749. .student-list-area {
  750. flex: 1;
  751. background-color: #ffffff;
  752. padding-bottom: 120rpx; // 给底部按钮留出空间
  753. }
  754. .student-table {
  755. width: calc(100% - 32rpx);
  756. margin: 0 auto;
  757. border: 1rpx solid #d2d2d2;
  758. .table-header {
  759. display: flex;
  760. background-color: #efefef;
  761. border-bottom: 1rpx solid #d2d2d2;
  762. padding: 20rpx;
  763. .col-number {
  764. width: 120rpx;
  765. text-align: center;
  766. font-size: 28rpx;
  767. font-weight: bold;
  768. color: #333;
  769. }
  770. .col-name {
  771. padding-left: 30rpx;
  772. flex: 1;
  773. font-size: 28rpx;
  774. font-weight: bold;
  775. color: #333;
  776. }
  777. .col-status {
  778. width: 200rpx;
  779. font-size: 28rpx;
  780. font-weight: bold;
  781. color: #333;
  782. text-align: left;
  783. }
  784. }
  785. .table-row {
  786. display: flex;
  787. padding: 20rpx;
  788. border-bottom: 1rpx solid #d2d2d2;
  789. transition: background-color 0.3s ease;
  790. &:last-child {
  791. border-bottom: none;
  792. }
  793. &:active {
  794. background-color: #f0f0f0;
  795. }
  796. .col-number {
  797. width: 120rpx;
  798. font-size: 28rpx;
  799. color: #333;
  800. text-align: center;
  801. }
  802. .col-name {
  803. padding-left: 30rpx;
  804. flex: 1;
  805. font-size: 28rpx;
  806. color: #333;
  807. }
  808. .col-status {
  809. width: 200rpx;
  810. font-size: 28rpx;
  811. text-align: left;
  812. color: #333;
  813. }
  814. // 状态行样式
  815. &.row-normal {
  816. background-color: #fff;
  817. }
  818. &.row-absent {
  819. background-color: #ff0000;
  820. color: #fff;
  821. .col-number,
  822. .col-name,
  823. .col-status {
  824. color: #fff;
  825. }
  826. }
  827. &.row-sick {
  828. background-color: #ee9700;
  829. color: #fff;
  830. .col-number,
  831. .col-name,
  832. .col-status {
  833. color: #fff;
  834. }
  835. }
  836. &.row-personal {
  837. background-color: #eb6100;
  838. color: #fff;
  839. .col-number,
  840. .col-name,
  841. .col-status {
  842. color: #fff;
  843. }
  844. }
  845. &.row-parent {
  846. background-color: #00a0e9;
  847. color: #fff;
  848. .col-number,
  849. .col-name,
  850. .col-status {
  851. color: #fff;
  852. }
  853. }
  854. }
  855. }
  856. // 星期几显示样式
  857. .weekday-text {
  858. font-size: 32rpx;
  859. color: #333;
  860. position: absolute;
  861. right: 100rpx;
  862. top: 50%;
  863. transform: translateY(-50%);
  864. padding-bottom: 6rpx;
  865. }
  866. // ==================== 确认弹窗样式 ====================
  867. // 统计信息样式
  868. .stats-section {
  869. padding: 40rpx;
  870. display: flex;
  871. flex-direction: column;
  872. gap: 20rpx;
  873. .stat-item {
  874. display: flex;
  875. align-items: center;
  876. justify-content: center;
  877. font-size: 34rpx;
  878. color: #333;
  879. .stat-label {
  880. text-align: right;
  881. flex: 1;
  882. margin-right: 20rpx;
  883. }
  884. .stat-value {
  885. flex: 1;
  886. }
  887. }
  888. }
  889. </style>