bjdm_bzrDmHomep.vue 41 KB

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