bjdm_bzrDmHomep copy.vue 36 KB

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