| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395 |
- <template>
- <!--
- 班主任点名页面
- 功能:支持班级考勤管理,包括学生状态切换、表格显示等
- -->
- <view class="page-container">
-
- <!-- 固定头部区域 不在考勤时间段、摘要模式时不显示 -->
- <view class="fixed-header" v-if="!isNotInTime && !isSummaryMode">
- <!-- 筛选条件区域 -->
- <view class="filter-area" :class="{ 'simple-mode': isSimpleMode }">
- <!-- 完整模式:显示表单 -->
- <view v-if="!isSimpleMode" class="full-form" style="position: relative;z-index: 1000;">
- <Form :rules="fieldConfigs" v-model="formData" ref="formRef">
- <up-table>
- <up-tr v-if="isEveningStudyMode && njOption.length > 1">
- <up-th>年级</up-th>
- <Td field="njm">
- <SsSelect v-model="formData.njm" :options="njOption"
- placeholder="请选择年级" @change="onNjChange" />
- </Td>
- </up-tr>
- <up-tr>
- <up-th>班级</up-th>
- <Td field="bjid">
- <!-- 晚自习模式:显示年级和班级选择 -->
- <SsSelect v-model="formData.bjid" :options="filteredBjOption"
- placeholder="请选择班级" @change="onXcTypeChange" v-if="isEveningStudyMode" />
- <!-- 正常模式:显示只读班级名称 -->
- <view v-else class="readonly-field">
- <text class="readonly-text">{{ bjName || formData.bjid }}</text>
- </view>
- </Td>
- </up-tr>
- <up-tr>
- <up-th>日期</up-th>
- <Td field="rq">
- <SsDatetimePicker v-model="formData.rq" mode="date" :max-date="maxDate"
- :z-index="99999" placeholder="请选择日期" @change="onDateChange" :disabled="true" />
- <text v-if="weekdayText" class="weekday-text">{{ weekdayText }}</text>
- </Td>
- </up-tr>
- <up-tr>
- <up-th>节次</up-th>
- <Td field="jc">
- <view class="onoff-button-group">
- <SsOnoffButton v-model="formData.jc" name="jc" label="上午" value="1" :disabled="true" />
- <SsOnoffButton v-model="formData.jc" name="jc" label="下午" value="2" :disabled="true" />
- <SsOnoffButton v-model="formData.jc" name="jc" label="晚上" value="3" :disabled="true" />
- </view>
- </Td>
- </up-tr>
- </up-table>
- </Form>
- </view>
- <!-- 简化模式:显示回显信息 -->
- <view v-else class="simple-display">
- <text class="display-text">{{ displayText }}</text>
- </view>
- </view>
- <!-- 考勤统计 -->
- <view class="stats-area" v-if="!isSummaryMode && !isNotInTime && !isClassSummaryMode">
- <view class="stat-item "><text class="stat-absent">缺勤:</text>{{ stats.absent }}人</view>
- <view class="stat-item "><text class="stat-sick">请假:</text>{{ stats.leave }}人</view>
- <view class="stat-item "><text class="stat-parent">家长请假:</text>{{ stats.parent }}人</view>
-
- </view>
- </view>
-
- <!-- 不在时间范围 -->
- <view v-else-if="isNotInTime" class="isNotInTime">
- <view class="isNotInTime-title">
- <Icon name="icon-naoling" size="97" color="#ff7e00" />
- 当前不在点名时段!
- </view>
- <view class="isNotInTime-content">
- <view class="isNotInTime-content-title">点名时段</view>
- <view v-for="item in NotInTime" class="isNotInTime-content-item">
- <view class="isNotInTime-content-item-icon">
- <Icon :name="item.icon" :size="item.iconSize" :color="item.iconColor" />
- </view>
- <view>
- <view>{{ item.title }}</view>
- <view>{{ item.range }}</view>
- </view>
- </view>
- </view>
- </view>
- <!-- 提交后摘要显示(仅显示缺勤/请假人数) -->
- <view v-if="isSummaryMode" class="summary-area">
- <view class="stats-section">
- <view class="stat-item">
- <text class="stat-label">点名情况:</text>
- <text class="stat-value absent">已点名</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">缺勤:</text>
- <text class="stat-value absent">{{ kkrs }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">请假:</text>
- <text class="stat-value leave">{{ qjrs }}人</text>
- </view>
- </view>
- </view>
- <!-- 班级选择模式下的摘要显示 -->
- <view v-else-if="isClassSummaryMode && isEveningStudyMode" class="summary-area">
- <view class="stats-section">
- <view class="stat-item">
- <text class="stat-label">点名情况:</text>
- <text class="stat-value absent">已点名</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">缺勤:</text>
- <text class="stat-value absent">{{ kkrs }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">请假:</text>
- <text class="stat-value leave">{{ qjrs }}人</text>
- </view>
- </view>
- </view>
- <!-- 学生列表区域 -->
- <scroll-view v-else class="student-list-area" :style="studentListStyle" scroll-y @scroll="handleStudentListScroll">
- <!-- 学生表格 -->
- <view class="student-table" v-if="!isNotInTime">
- <!-- 表头 -->
- <view class="table-header">
- <text class="col-number">序号</text>
- <text class="col-name">姓名</text>
- <text class="col-status">状态</text>
- </view>
- <!-- 学生行 -->
- <view v-for="(student, index) in studentList" :key="student.id" class="table-row"
- :class="getRowClass(student.kqlbm, student.rcid)" @click="handleStudentClick(student)">
- <text class="col-number">{{ (index+1) < 10 ? '0' + (index + 1) : index + 1 }}</text>
- <text class="col-name">{{ student.xm }}</text>
- <text class="col-status">{{ getStatusText(student.kqlbm, student.rcid) }}</text>
- </view>
- </view>
- </scroll-view>
- <!-- 底部按钮 -->
- <SsBottom :buttons="bottomButtons" @button-click="handleBottomAction" v-if="!isSummaryMode && !isNotInTime" />
- <!-- 二次确认弹窗 -->
- <SsConfirm v-model:visible="showConfirm" title="点名情况" width="90%" height="42vh" :bottom-buttons="confirmButtons"
- @button-click="handleConfirmAction" @close="handleConfirmClose">
- <!-- 统计信息 -->
- <view class="stats-section">
- <view class="stat-item">
- <text class="stat-label">出勤:</text>
- <text class="stat-value present">{{ attendanceStats.present }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">缺勤:</text>
- <text class="stat-value absent">{{ attendanceStats.absent }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">事假:</text>
- <text class="stat-value leave">{{ attendanceStats.leave }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">病假:</text>
- <text class="stat-value sick">{{ attendanceStats.sick }}人</text>
- </view>
- <view class="stat-item">
- <text class="stat-label">家长请假:</text>
- <text class="stat-value parent-leave">{{ attendanceStats.parentLeave }}人</text>
- </view>
- </view>
- </SsConfirm>
- </view>
- </template>
- <script setup>
- /**
- * 班主任点名页面
- *
- * 主要功能:
- * 1. 筛选条件管理(班级、日期、节次)
- * 2. 学生表格显示和状态管理
- * 3. 三行变一行的下拉切换
- * 4. 学生考勤状态管理
- * 5. 考勤数据保存和提交
- */
- import { ref, computed, nextTick, getCurrentInstance, defineExpose, watch } from 'vue'
- import Form from '@/components/Form/index.vue'
- import Td from '@/components/Td/index.vue'
- import SsDatetimePicker from '@/components/SsDatetimePicker/index.vue'
- import SsOnoffButton from '@/components/SsOnoffButton/index.vue'
- import SsSelect from '@/components/SsSelect/index.vue'
- import SsBottom from '@/components/SsBottom/index.vue'
- import SsConfirm from '@/components/SsConfirm/index.vue'
- import { commonApi } from '@/api/common'
- import { kqjlApi } from '@/api/kqjl'
- import { goBack } from '@/utils/navigation'
- import { getDictTranslation } from '@/utils/fieldFormatter'
- import { formatDate as utilFormatDate } from '@/utils/date'
- import Icon from '@/components/icon/index.vue';
- // ==================== 数据定义 ====================
- /**
- * 表单数据
- * @property {string} bjid - 班级ID
- * @property {string} rq - 日期
- * @property {string} jc - 节次 (1-上午, 2-下午, 3-晚上)
- */
- const formData = ref({
- bjid: '',
- njm: '', // 年级代码,晚自习模式使用
- rq: '', // 将在初始化时设置为今天
- jc: '', // 将根据当前时间自动设置
- jkssj: '',
- jjssj: ''
- })
- // 日期相关数据
- const maxDate = ref(new Date()) // 最大日期为今天,禁止选择今天之后的
- const weekdayText = ref('') // 星期几的显示文本
- /**
- * 页面状态管理
- * @property {boolean} isSimpleMode - 是否为简化模式(三行变一行)
- * @property {Array} studentList - 学生列表数据
- * @property {boolean} refresherTriggered - 下拉刷新状态
- */
- const isSimpleMode = ref(false) // 是否为简化模式
- const isSummaryMode = ref(false) // 是否为提交后的摘要模式
- const isNotInTime = ref(false)
- const isEveningStudyMode = ref(false) // 是否为班级选择模式
- const isClassSummaryMode = ref(false) // 班级选择模式下的摘要显示
- const NotInTime = ref([
- {
- icon: 'icon-shangwu',
- iconColor: '#1296db',
- iconSize: 97,
- title: '上午',
- range:'6:00~12:00'
- },
- {
- icon: 'icon-xiawu',
- iconColor: '#dd9900',
- iconSize: 97,
- title: '下午',
- range:'13:40~16:00'
- },
- {
- icon: 'icon-night',
- iconColor: '#333333',
- iconSize: 97,
- title: '晚上',
- range:'18:30~21:00'
- }
- ])
- const kkrs = ref(0) // 缺勤人数(来自接口摘要)
- const qjrs = ref(0) // 请假人数(来自接口摘要)
- const studentList = ref([]) // 学生列表
- // 晚自习模式相关数据
- const njOption = ref([]) // 年级选项
- const filteredBjOption = ref([]) // 根据年级过滤的班级选项
- const eveningStudyData = ref({
- bjList: [], // 班级列表
- njList: [] // 年级列表
- })
- // 确认弹窗相关
- const showConfirm = ref(false) // 是否显示确认弹窗
- /**
- * 计算回显文本
- * 在简化模式下显示的筛选条件摘要
- * 格式:班级名称 + 日期 + 节次
- */
- const displayText = computed(() => {
- // const bjText = bjOption.value.find(item => item.v === formData.value.bjid)?.n || '未选择班级'
- const jcText = formData.value.jc === '1' ? '上午' : formData.value.jc === '2' ? '下午' : '晚上'
- return `${formData.value.rq} ${jcText}`
- })
- /**
- * 计算考勤统计
- */
- const stats = computed(() => {
- const absent = studentList.value.filter(s => s.kqlbm === 1).length // 缺勤
- const sick = studentList.value.filter(s => s.kqlbm === 41).length // 病假
- const personal = studentList.value.filter(s => s.kqlbm === 31).length // 事假
- const leave = sick + personal // 请假 = 病假 + 事假
- const parent = studentList.value.filter(s => s.rcid > 0).length // 家长请假
- return { absent, leave, parent }
- })
- /**
- * 计算学生列表区域的样式
- */
- const studentListStyle = computed(() => {
- return {
- paddingTop: headerHeight.value + 'rpx'
- }
- })
- // ==================== 确认弹窗配置 ====================
- /**
- * 确认弹窗按钮配置
- */
- const confirmButtons = ref([
- { text: '取消', action: 'cancel', type: 'default' },
- { text: '确认提交', action: 'confirm', type: 'primary' }
- ])
- /**
- * 考勤统计数据
- */
- const attendanceStats = computed(() => {
- const stats = {
- present: 0,
- absent: 0,
- leave: 0,
- sick: 0,
- parentLeave: 0
- }
- studentList.value.forEach(student => {
- if (student.rcid > 0) {
- stats.parentLeave++ // 家长请假(优先判断)
- } else if (student.kqlbm === 81) {
- stats.present++ // 出勤
- } else if (student.kqlbm === 1) {
- stats.absent++ // 缺勤
- } else if (student.kqlbm === 31) {
- stats.leave++ // 事假
- } else if (student.kqlbm === 41) {
- stats.sick++ // 病假
- } else {
- stats.absent++ // 其他情况默认为缺勤
- }
- })
- // // console.log('考勤统计更新:', stats, '学生总数:', studentList.value.length)
- return stats
- })
- // ==================== 表单配置 ====================
- /**
- * 表单字段校验配置
- * 定义各个字段的校验规则
- */
- const fieldConfigs = computed(() => {
- const configs = {
- bjid: {
- rules: [{ required: true, message: '班级不能为空' }]
- },
- rq: {
- rules: [{ required: true, message: '日期不能为空' }]
- },
- jc: {
- rules: [{ required: true, message: '请选择节次' }]
- }
- }
- // 年级校验:只有在显示多年级选择时才必填
- if (njOption.value.length > 1) {
- configs.njm = {
- rules: [{ required: true, message: '年级不能为空' }]
- }
- }
- return configs
- })
- /**
- * 表单引用
- * 用于表单校验和数据提交
- */
- const formRef = ref(null)
- /**
- * 班级下拉框配置
- * 通过字典API获取班级列表数据
- */
- const bjOption = ref([])
- const bjLoading = ref(false) // 班级数据加载状态
- const bjName = ref('') // 班级名称(只读显示)
- const dictCache = new Map() // 简单的字典缓存
- const resolveBjName = async () => {
- console.log('resolveBjName111',formData.value.bjid)
- if (!formData.value.bjid) {
- bjName.value = ''
- return
- }
- const cacheKey = `bj_${formData.value.bjid}`
- await getDictTranslation('bj', formData.value.bjid, cacheKey, dictCache, () => {
- bjName.value = dictCache.get(cacheKey)
- })
- }
- /**
- * 初始化表单默认值
- */
- const initFormDefaults = () => {
- // 设置默认日期为今天
- const today = new Date()
- const year = today.getFullYear()
- const month = String(today.getMonth() + 1).padStart(2, '0')
- const day = String(today.getDate()).padStart(2, '0')
- formData.value.rq = `${year}-${month}-${day}`
- // 更新星期几显示
- updateWeekdayText(formData.value.rq)
- // 根据当前时间设置默认节次 - 使用国际通用标准
- const currentTime = new Date()
- const currentHour = currentTime.getHours()
- let jcValue = '1'
- if (currentHour >= 0 && currentHour < 12) {
- // 0-12点:上午
- jcValue = '1'
- } else if (currentHour >= 12 && currentHour < 18) {
- // 12-18点:下午
- jcValue = '2'
- } else {
- // 18-24点:晚上
- jcValue = '3'
- }
- formData.value.jc = jcValue
-
- }
- /**
- * 更新星期几显示文本
- */
- const updateWeekdayText = (dateStr) => {
- if (!dateStr) {
- weekdayText.value = ''
- return
- }
- try {
- const date = new Date(dateStr)
- const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
- weekdayText.value = weekdays[date.getDay()]
- } catch (error) {
- console.error('解析日期失败:', error)
- weekdayText.value = ''
- }
- }
- /**
- * 日期选择变化处理
- */
- const onDateChange = (value) => {
- // console.log('日期选择变化:', value)
- updateWeekdayText(value)
- }
- // ==================== 事件处理方法 ====================
- /**
- * 班级选择变化处理
- * 当用户选择不同班级时触发,重新加载该班级的学生数据
- * @param {string} value - 选中的班级ID
- * @param {Object} option - 选中的班级选项对象
- */
- const onXcTypeChange = (value) => {
- console.log('班级选择变化:', value)
- // 如果是晚自习模式且选择了班级,重新加载学生数据
- if (isEveningStudyMode.value && value) {
- console.log('晚自习模式选择班级,重新加载数据')
- loadStudentData()
- } else {
- // 重新加载学生数据
- loadStudentData()
- }
- }
- /**
- * 加载学生数据
- * 根据当前选择的班级、日期、节次加载学生列表
- * TODO: 替换为真实的接口调用
- */
- const loadStudentData = async () => {
- try {
- const res = await kqjlApi.mp_bzrdmHomep_load({ bjid: formData.value.bjid })
- formData.value.jkssj = res.data.jkssj
- formData.value.jjssj = res.data.jjssj
- formData.value.bjid = res.data.bjid
- resolveBjName()
- console.log('loadStudentData:', res)
- // 不在点名时段
- if(res.data.msg && res.data.msg.includes("不在点名时段")){
- isNotInTime.value = true
- NotInTime.value[0].range = res.data.swsjd;
- NotInTime.value[1].range = res.data.xwsjd;
- NotInTime.value[2].range = res.data.wssjd;
- return
- }
- // 开发阶段:所有模式都当作班级选择模式处理,支持年级班级选择
- if (!res.data.bjid) {
- console.log('进入班级选择模式')
- isEveningStudyMode.value = true
- isNotInTime.value = false
- isSummaryMode.value = false
- studentList.value = []
- if(res.data.msg && res.data.msg.includes("没有指定管辖的班级")){
- //如果没有bjid,没有njlist和bjlist,代表他只能晚自习点名,现在打开的是白天模式,所以要切换
- isEveningStudyMode.value = false
- isNotInTime.value = true
- return
- }
- processEveningStudyData(res.data)
- return
- }
- // 有bjid时,如果已经在班级选择模式,则加载选中的班级数据
- if (isEveningStudyMode.value) {
- console.log('班级选择模式:加载班级数据')
- isNotInTime.value = false
- isSummaryMode.value = false
- // 如果接口返回了 kkrs/qjrs,启用班级摘要模式
- if ((res.data.kkrs !== undefined && res.data.kkrs !== null) || (res.data.qjrs !== undefined && res.data.qjrs !== null)) {
- kkrs.value = Number(res.data.kkrs || 0)
- qjrs.value = Number(res.data.qjrs || 0)
- isClassSummaryMode.value = true
- studentList.value = []
- return
- }
- // 否则显示正常学生列表
- isClassSummaryMode.value = false
- if (res.data.bjList && res.data.bjList.length > 0) {
- studentList.value = res.data.bjList[0]
- // 数据加载完成后,测量头部高度
- nextTick(() => {
- measureHeaderHeight()
- })
- } else {
- uni.showToast({
- title: res.data.msg || '暂无数据',
- icon: 'none'
- })
- }
- return
- }
-
- // 如果接口返回了 kkrs/qjrs,启用摘要模式并隐藏列表
- if ((res.data.kkrs !== undefined && res.data.kkrs !== null) || (res.data.qjrs !== undefined && res.data.qjrs !== null)) {
- kkrs.value = Number(res.data.kkrs || 0)
- qjrs.value = Number(res.data.qjrs || 0)
- isSummaryMode.value = true
- studentList.value = []
- return
- }
- // 否则,显示正常列表模式
- isSummaryMode.value = false
- isNotInTime.value = false
- if (res.data.bjList && res.data.bjList.length > 0) {
- studentList.value = res.data.bjList[0]
- // 数据加载完成后,测量头部高度
- nextTick(() => {
- measureHeaderHeight()
- })
- } else {
- // console.log('没有数据:', res.data.msg)
- uni.showToast({
- title: res.data.msg || '暂无数据',
- icon: 'none'
- })
- }
- } catch (error) {
- console.error('加载学生数据失败:', error)
- uni.showToast({
- title: '加载学生数据失败',
- icon: 'none'
- })
- }
- }
- /**
- * 处理晚自习数据
- * 解析年级和班级数据,设置级联选择,并默认选中第一个年级第一个班级进行查询
- * @param {Object} data - 接口返回的晚自习数据
- */
- const processEveningStudyData = (data) => {
- console.log('处理晚自习数据:', data)
- // 保存原始数据
- eveningStudyData.value = {
- bjList: data.bjList || [],
- njList: data.njList || []
- }
- // 处理年级数据
- if (data.njList && data.njList.length > 0) {
- njOption.value = data.njList.map(nj => ({
- n: nj.mc,
- v: nj.njm
- }))
- // 自动选中第一个年级
- formData.value.njm = njOption.value[0].v
- filterBjByNj()
- // 自动选中第一个班级并查询
- setTimeout(() => {
- if (filteredBjOption.value.length > 0) {
- formData.value.bjid = filteredBjOption.value[0].v
- console.log('自动选中第一个班级:', formData.value.bjid)
- loadStudentData()
- }
- }, 100)
- } else {
- // 没有年级数据,直接处理班级数据并选中第一个
- filterBjByNj()
- setTimeout(() => {
- if (filteredBjOption.value.length > 0) {
- formData.value.bjid = filteredBjOption.value[0].v
- console.log('自动选中第一个班级:', formData.value.bjid)
- loadStudentData()
- }
- }, 100)
- }
- }
- /**
- * 根据年级过滤班级列表
- */
- const filterBjByNj = () => {
- console.log('根据年级过滤班级:', formData.value.njm)
- if (!formData.value.njm) {
- // 如果没有选择年级,显示所有班级
- filteredBjOption.value = eveningStudyData.value.bjList.map(bj => ({
- n: bj.mc,
- v: bj.bjid
- }))
- } else {
- // 根据选择的年级过滤班级
- filteredBjOption.value = eveningStudyData.value.bjList
- .filter(bj => bj.njm === formData.value.njm)
- .map(bj => ({
- n: bj.mc,
- v: bj.bjid
- }))
- }
- console.log('过滤后的班级:', filteredBjOption.value)
- // 清空已选择的班级
- formData.value.bjid = ''
- }
- /**
- * 年级选择变化处理
- * 当用户选择不同年级时,过滤对应的班级列表
- */
- const onNjChange = () => {
- console.log('年级选择变化:', formData.value.njm)
- filterBjByNj()
- }
- /**
- * 处理学生点击
- * 切换学生的考勤状态
- * @param {Object} student - 被点击的学生对象
- */
- const handleStudentClick = (student) => {
- // console.log('点击学生:', student)
- // 状态切换逻辑:出勤(0) → 缺勤(1) → 病假(2) → 事假(3) → 家长请假(4) → 出勤(0)
- let newStatus
- switch (student.kqlbm) {
- case 81: // 出勤 → 缺勤
- newStatus = 1
- break
- case 1: // 缺勤 → 病假
- newStatus = 41
- break
- case 41: // 病假 → 事假
- newStatus = 31
- break
- case 31: // 事假 → 出勤
- newStatus = 81
- break
- case 4: // 家长请假 → 不可点击
- newStatus = 4
- break;
- default: // 其他状态 → 出勤
- newStatus = 81
- }
- // 更新本地数据
- const targetStudent = studentList.value.find(s => s.ryid === student.ryid)
- if (targetStudent) {
- targetStudent.kqlbm = newStatus
- }
- }
- // 页面滚动位置跟踪
- const pageScrollTop = ref(0)
- const initialFilterHeight = ref(0) // 筛选区域的初始高度
- const headerHeight = ref(300) // 固定头部高度(rpx),初始值设大一点避免遮挡
- // 获取当前组件实例(用于 SelectorQuery)
- const instance = getCurrentInstance()
- /**
- * 处理页面滚动事件
- * 监听整个页面的滚动位置
- */
- const handlePageScroll = (scrollTop) => {
- const previousScrollTop = pageScrollTop.value
- pageScrollTop.value = scrollTop
- console.log('页面滚动位置:', scrollTop, '初始高度:', initialFilterHeight.value)
- // 检测滚动方向和位置
- const isScrollingDown = scrollTop > previousScrollTop
- const isScrollingUp = scrollTop < previousScrollTop
- // 三行变一行:向下滚动超过筛选区域高度的30px时触发
- if (isScrollingDown &&
- scrollTop > (initialFilterHeight.value + 30) &&
- !isSimpleMode.value) {
- console.log('向下滚动超过阈值,切换到简化模式')
- isSimpleMode.value = true
- }
- // 一行变三行:向上滚动回到筛选区域范围内时触发
- // 使用筛选区域高度的一半作为阈值,更容易触发恢复
- if (isScrollingUp &&
- scrollTop < (initialFilterHeight.value / 2) &&
- isSimpleMode.value) {
- console.log('向上滚动回到顶部,切换到完整模式')
- isSimpleMode.value = false
- }
- }
- /**
- * 处理学生列表区域的滚动事件
- * @param {Object} e - scroll-view 的滚动事件对象
- */
- const handleStudentListScroll = (e) => {
- handlePageScroll(e.detail.scrollTop)
- }
- /**
- * 测量并更新头部高度
- * @param {number} retryCount - 重试次数
- */
- const measureHeaderHeight = (retryCount = 0) => {
- nextTick(() => {
- uni.createSelectorQuery()
- .in(instance) // 指定在当前组件实例中查询
- .select('.fixed-header')
- .boundingClientRect((rect) => {
- if (rect && rect.height > 0) {
- initialFilterHeight.value = rect.height
- const headerHeightRpx = rect.height * 2 // px转rpx
- headerHeight.value = headerHeightRpx
- console.log('更新头部高度:', rect.height, 'px =', headerHeightRpx, 'rpx')
- } else if (retryCount < 5) {
- // 如果获取失败且重试次数未达到上限,延迟后重试
- console.log('头部高度获取失败,第', retryCount + 1, '次重试...', 'rect:', rect)
- setTimeout(() => {
- measureHeaderHeight(retryCount + 1)
- }, 100)
- }
- })
- .exec()
- })
- }
- // 监听 isSimpleMode 变化,重新测量头部高度
- watch(isSimpleMode, () => {
- // 立即测量新的头部高度,padding-top 的 CSS transition 会平滑过渡
- measureHeaderHeight()
- })
- /**
- * 获取表格行的样式类
- * 根据学生状态返回对应的CSS类名
- * @param {number} status - 学生状态
- * @param {number} rcid - 日程id
- * @returns {string} CSS类名
- */
- const getRowClass = (status, rcid) => {
- if (rcid > 0) {
- return 'row-parent' // 家长请假
- } else {
- switch (status) {
- case 1: return 'row-absent' // 缺勤
- case 41: return 'row-sick' // 病假
- case 31: return 'row-personal' // 事假
- default: return 'row-normal' // 出勤
- }
- }
- }
- /**
- * 获取考勤状态文本
- * 将数字状态码转换为可读的文本描述
- * @param {number} status - 状态码
- * @param {number} rcid - 日程id
- * @returns {string} 状态文本
- */
- const getStatusText = (status, rcid) => {
- if (rcid > 0) {
- return '家长请假'
- } else {
- switch (status) {
- case 81: return '出勤'
- case 1: return '缺勤'
- case 41: return '病假'
- case 31: return '事假'
- // case 4: return '家长请假'
- default: return '出勤'
- }
- }
- }
- // ==================== 底部按钮配置 ====================
- /**
- * 底部按钮配置
- * 只在非点名模式下显示,提供取消和保存功能
- */
- const bottomButtons = [
- // { text: '取消', action: 'back' },
- { text: '保存', action: 'save' }
- ]
- /**
- * 底部按钮事件处理
- * 处理取消和保存操作
- * @param {Object} data - 按钮数据
- * @param {string} data.action - 按钮动作类型
- */
- const handleBottomAction = (data) => {
- // console.log('底部按钮操作:', data)
- switch (data.action) {
- case 'back':
- // 返回处理
- goBack()
- break
- case 'save':
- // 显示二次确认弹窗
- showConfirm.value = true
- break
- }
- }
- /**
- * 确认弹窗按钮点击处理
- */
- const handleConfirmAction = (data) => {
- // console.log('确认弹窗按钮点击:', data)
- switch (data.action) {
- case 'cancel':
- showConfirm.value = false
- break
- case 'confirm':
- // 执行实际的保存操作
- showConfirm.value = false
- handleSave()
- break
- }
- }
- /**
- * 确认弹窗关闭处理
- */
- const handleConfirmClose = () => {
- // console.log('确认弹窗关闭')
- showConfirm.value = false
- }
- const getKkrs = () => {
- let kkrs = 0
- studentList.value.forEach(student => {
- if (student.kqlbm === 1) {
- kkrs++
- }
- })
- return kkrs
- }
- const getQjrs = () => {
- let qjrs = 0
- studentList.value.forEach(student => {
- if (student.kqlbm === 41 || student.kqlbm === 31 || student.rcid > 0) {
- qjrs++
- }
- })
- return qjrs
- }
- /**
- * 保存学生考勤数据
- * 收集所有学生的考勤状态并提交到后端
- */
- const handleSave = async () => {
- // console.log('保存学生考勤数据')
- try {
- const params = {
- bjid: formData.value.bjid,
- kkrs: getKkrs(), //缺勤人数
- qjrs: getQjrs(), //请假人数
- // rq: formData.value.rq + ' 00:00:00', //补齐时间
- jkssj: utilFormatDate(formData.value.jkssj, "yyyy-MM-dd HH:mm:ss"),
- jjssj: utilFormatDate(formData.value.jjssj, "yyyy-MM-dd HH:mm:ss"),
- ryList: JSON.stringify(studentList.value
- .filter(student => student.kqlbm !== 81)
- .map(student => ({
- ryid: student.ryid,
- kqlbm: student.kqlbm
- })))
- }
- console.log(JSON.stringify(params))
- const response = await kqjlApi.bjdm_saveBzrDm(params)
- console.log('保存考勤数据成功:', response)
- if (response.data.ssCode === 0) {
- // uni.showToast({
- // title: response.data.msg,
- // icon: 'success'
- // })
- // 提交成功后,重新调用加载接口,根据返回是否含 kkrs/qjrs 决定进入摘要模式
- await loadStudentData()
- // 保存后强制切换回完整模式,避免简化模式下卡住
- isSimpleMode.value = false
- }
- } catch (error) {
- console.error('保存考勤数据失败:', error)
- uni.showToast({
- title: '保存失败',
- icon: 'none'
- })
- }
- }
- // ==================== 页面初始化 ====================
- /**
- * 页面挂载时的初始化
- */
- // onMounted(() => {
- // // 初始化表单默认值
- // initFormDefaults()
- // // 加载班级选项
- // loadClassOptions()
- // // 加载学生数据
- // // loadStudentData()
- // // 多次尝试获取固定头部的高度,确保准确
- // const measureHeader = () => {
- // uni.createSelectorQuery().select('.fixed-header').boundingClientRect((rect) => {
- // if (rect && rect.height > 0) {
- // initialFilterHeight.value = rect.height
- // const headerHeightRpx = rect.height * 2 // px转rpx
- // headerHeight.value = headerHeightRpx
- // // console.log('固定头部高度:', rect.height, 'px =', headerHeightRpx, 'rpx')
- // } else {
- // // 如果还没获取到,再试一次
- // setTimeout(measureHeader, 50)
- // }
- // }).exec()
- // }
- // // 立即尝试测量,然后延迟再测量一次确保准确
- // measureHeader()
- // setTimeout(measureHeader, 200)
- // // 如已存在bjid,则解析一次名称
- // resolveBjName()
- // })
- // ============== 暴露给主容器的生命周期(供 pages/main/index.vue 调用) ==============
- // 避免重复初始化的标记
- function onLoad() {
- loadStudentData()
- initFormDefaults()
- }
- function onShow() {
- // 每次切到该页时都刷新数据
- // 头部高度会在 loadStudentData 完成后自动测量
- loadStudentData()
- initFormDefaults()
- }
- function onHide() {
- // 可按需做暂存处理
- }
- function onUnload() {
- // 可按需做清理处理
- }
- defineExpose({ onLoad, onShow, onHide, onUnload })
- // 将handlePageScroll挂载到全局,供页面配置使用
- if (typeof globalThis !== 'undefined') {
- globalThis.pageScrollHandler = handlePageScroll
- }
- </script>
- <style lang="scss" scoped>
- .page-container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background-color: #f2f3f4;
- position: relative;
- }
- .fixed-header {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 100;
- background-color: #f2f3f4;
- }
- .filter-area {
- // background-color: #fff;
- transition: all 0.3s ease;
- border-bottom: 1rpx solid #eceded;
- // 完整模式
- .full-form {
- padding: 18rpx 0;
- }
- // 简化模式
- &.simple-mode {
- .simple-display {
- padding: 45rpx 0;
- background-color: #f2f3f4;
- border-bottom: 1rpx solid #eceded;
- .display-text {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- text-align: center;
- display: block;
- }
- }
- }
- }
- .onoff-button-group {
- display: flex;
- flex-wrap: wrap;
- gap: 20rpx;
- align-items: flex-start;
- }
- .stats-area {
- background-color: #fff;
- padding: 24rpx;
- display: flex;
- align-items: center;
- gap: 20rpx;
- .stat-item {
- font-size: 28rpx;
- font-weight: 500;
- }
- .stat-absent {
- color: #ff0000;
- }
- .stat-sick {
- color: #ee9700;
- }
- .stat-parent {
- color: #00a0e9;
- }
- }
- .student-list-area {
- flex: 1;
- background-color: #ffffff;
- padding-bottom: 120rpx; // 给底部按钮留出空间
- height: 0; // 配合 flex: 1,让 scroll-view 正确计算剩余空间
- transition: padding-top 0.3s ease; // 与筛选区域同步过渡
- }
- .student-table {
- width: calc(100% - 32rpx);
- margin: 0 auto;
- border: 1rpx solid #d2d2d2;
-
- .table-header {
- display: flex;
- background-color: #efefef;
- border-bottom: 1rpx solid #d2d2d2;
- padding: 20rpx;
- align-items: center;
- // margin-bottom: 10rpx;
- .col-number {
- width: 120rpx;
- text-align: center;
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- }
- .col-name {
- padding-left: 30rpx;
- flex: 1;
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- }
- .col-status {
- width: 200rpx;
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- text-align: left;
- }
- }
- .table-row {
- display: flex;
- padding: 24rpx;
- border-top: 1rpx solid #d2d2d2;
- transition: background-color 0.3s ease;
-
- &:last-child {
- border-bottom: none;
- }
- &:active {
- background-color: #f0f0f0;
- }
- .col-number {
- width: 120rpx;
- font-size: 32rpx;
- color: #000;
- text-align: center;
- }
- .col-name {
- padding-left: 30rpx;
- flex: 1;
- font-size: 32rpx;
- color: #000;
- }
- .col-status {
- width: 200rpx;
- text-align: left;
- font-size: 32rpx;
- color: #000;
- }
- // 状态行样式
- &.row-normal {
- background-color: #fff;
- }
- &.row-absent {
- background-color: #ff0000;
- color: #fff;
- .col-number,
- .col-name,
- .col-status {
- color: #fff;
- }
- }
- &.row-sick {
- background-color: #ee9700;
- color: #fff;
- .col-number,
- .col-name,
- .col-status {
- color: #fff;
- }
- }
- &.row-personal {
- background-color: #eb6100;
- color: #fff;
- .col-number,
- .col-name,
- .col-status {
- color: #fff;
- }
- }
- &.row-parent {
- background-color: #00a0e9;
- color: #fff;
- .col-number,
- .col-name,
- .col-status {
- color: #fff;
- }
- }
- }
- }
- // 星期几显示样式
- .weekday-text {
- font-size: 32rpx;
- color: #333;
- position: absolute;
- right: 100rpx;
- top: 50%;
- transform: translateY(-50%);
- padding-bottom: 6rpx;
- }
- // ==================== 确认弹窗样式 ====================
- // 统计信息样式
- .stats-section {
- padding: 40rpx;
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- .stat-item {
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 34rpx;
- color: #333;
- .stat-label {
- text-align: right;
- flex: 1;
- margin-right: 20rpx;
- }
- .stat-value {
- flex: 1;
- }
- }
- }
- // 只读班级名称样式
- .readonly-field {
- width: 100%;
- min-height: 60rpx;
- display: flex;
- align-items: center;
- }
- .readonly-text {
- font-size: 32rpx;
- }
- // 晚自习模式相关样式
- .select-group {
- width: 100%;
- }
- .mt-10 {
- margin-top: 10rpx;
- }
- .summary-area{
- margin-top:40vh
- }
- .isNotInTime{
- width: calc(100% - 40rpx);
- height: calc(100% - 40rpx);
- background: #fff;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%,-50%);
- border-radius: 4rpx;
- border: 2rpx solid #ebecec;
- display: flex;
- flex-direction: column;
- padding: 20rpx;
- box-sizing: border-box;
- .isNotInTime-title{
- color: #333;
- font-weight: 500;
- font-size: 38rpx;
- text-align: center;
- flex: 3;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 30rpx;
- }
- .isNotInTime-content{
- flex: 7;
- border-radius: 4rpx;
- background: #f9fafb;
- padding: 0 30rpx;
- .isNotInTime-content-title{
- font-size: 36rpx;
- color: #333333;
- font-weight: 400;
- margin: 30rpx 0;
-
- }
- .isNotInTime-content-item{
- font-size: 36rpx;
- color: #333333;
- font-weight: 400;
- display: flex;
- align-items: center;
- justify-content: flex-start;
- background: #fff;
- padding:20rpx 30rpx;
- box-sizing: border-box;
- border-radius: 4rpx;
- margin-bottom: 30rpx;
- gap: 30rpx;
- .isNotInTime-content-item-icon{
- width: 120rpx;
- text-align: center;
- }
- }
- }
- }
- </style>
|