|
|
@@ -0,0 +1,1665 @@
|
|
|
+<template>
|
|
|
+ <!--
|
|
|
+ 校车点名页面(临时从班主任点名复制)
|
|
|
+ 功能:后续将改为校车点名首页与流程入口
|
|
|
+ -->
|
|
|
+ <view class="page-container">
|
|
|
+
|
|
|
+ <!-- 固定头部区域 不在考勤时间段、摘要模式时不显示 -->
|
|
|
+ <view class="fixed-header" v-if="!isNotInTime && !isSummaryMode">
|
|
|
+ <!-- 筛选条件区域 -->
|
|
|
+ <view class="filter-area">
|
|
|
+ <view class="full-form" style="position: relative;z-index: 1000;">
|
|
|
+ <Form :rules="fieldConfigs" v-model="formData" ref="formRef">
|
|
|
+ <up-table>
|
|
|
+ <up-tr>
|
|
|
+ <up-th>车牌号</up-th>
|
|
|
+ <Td field="carNo">
|
|
|
+ <view class="readonly-field">
|
|
|
+ <text class="readonly-text">{{ formData.carNo }}</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" />
|
|
|
+ </view>
|
|
|
+ </Td>
|
|
|
+ </up-tr>
|
|
|
+ </up-table>
|
|
|
+ </Form>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 站点进度(mock,固定7个点,第4个永远是中心点) -->
|
|
|
+ <view class="stop-progress" catchtouchmove="true" @touchstart.stop="handleStopTouchStart"
|
|
|
+ @touchmove.stop="handleStopTouchMove" @touchend.stop="handleStopTouchEnd"
|
|
|
+ @touchcancel.stop="handleStopTouchEnd">
|
|
|
+ <view class="stop-track">
|
|
|
+ <template v-for="slot in 7" :key="slot">
|
|
|
+ <view class="stop-node" :class="{ center: slot === 4 }"
|
|
|
+ @tap.stop="handleStopTap(slot - 1)">
|
|
|
+ <view class="stop-dot" :class="getStopDotClass(slot - 1)"></view>
|
|
|
+ </view>
|
|
|
+ <view v-if="slot < 7" class="stop-line" :class="getStopLineClass(slot - 1)"></view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+ <view class="stop-label-row">
|
|
|
+ <text class="stop-label-side stop-label-prev">{{ prevStopName }}</text>
|
|
|
+ <text class="stop-label-current">{{ currentStopName }}</text>
|
|
|
+ <text class="stop-label-side stop-label-next">{{ nextStopName }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 考勤统计(临时展示) -->
|
|
|
+ <view class="stats-area" v-if="!isSummaryMode && !isNotInTime">
|
|
|
+ <view class="stats-left">
|
|
|
+ <text class="stat-parent">家长请假:</text>
|
|
|
+ <text>{{ parentLeaveCount }}人</text>
|
|
|
+ </view>
|
|
|
+ <view class="stats-right">{{ rollcallProgressText }}</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-if="notInTimeMsg" class="isNotInTime-content-tip">{{ notInTimeMsg }}</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">{{ summaryJsrs }}人</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-label">请假:</text>
|
|
|
+ <text class="stat-value leave">{{ summaryQjrs }}人</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-label">未上车:</text>
|
|
|
+ <text class="stat-value absent">{{ summaryOffCount }}人</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 学生列表区域 -->
|
|
|
+ <view v-else class="student-table-wrapper" :style="studentTableWrapperStyle">
|
|
|
+ <view class="student-table">
|
|
|
+ <!-- 表头(固定,不随列表滚动) -->
|
|
|
+ <view class="table-header">
|
|
|
+ <text class="col-name">姓名</text>
|
|
|
+ <text class="col-class">班级</text>
|
|
|
+ <text class="col-status">状态</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 学生行(滚动区域) -->
|
|
|
+ <scroll-view class="student-list-area" scroll-y :scroll-into-view="scrollIntoViewId"
|
|
|
+ :scroll-with-animation="true" @scroll="handleStudentListScroll" @click="closeAllSwipeStates">
|
|
|
+ <view v-for="(student, index) in displayZdcyList" :key="student.zdryid" class="row-wrapper"
|
|
|
+ :id="'row_' + student.zdryid" :class="{ swiped: swipeStates[student.zdryid] }"
|
|
|
+ :style="{ '--actions-width': actionWidth(student) + 'rpx' }"
|
|
|
+ @touchstart="handleTouchStart($event, student.zdryid)"
|
|
|
+ @touchmove.stop="handleTouchMove($event, student.zdryid)"
|
|
|
+ @touchend="handleTouchEnd($event, student.zdryid)"
|
|
|
+ @touchcancel="handleTouchEnd($event, student.zdryid)">
|
|
|
+ <view class="table-row" :class="getRowClass(student)" @tap="handleStudentTap(student)">
|
|
|
+ <text class="col-name">{{ student.xm }}</text>
|
|
|
+ <text class="col-class">{{ student.bjid }}</text>
|
|
|
+ <text class="col-status">{{ getStatusText(student) }}</text>
|
|
|
+ <view class="drag-indicator" :style="getDragIndicatorStyle(student)"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="swipe-actions" :style="{ width: actionWidth(student) + 'rpx' }">
|
|
|
+ <view v-if="shouldShowException(student)" class="action-btn exception-btn"
|
|
|
+ @click.stop="handleException(student)">异常登记</view>
|
|
|
+ <view class="action-btn phone-btn" @click.stop="handleCallParent(student)">家长电话</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部按钮(保存) -->
|
|
|
+ <SsBottom :buttons="bottomButtons" @button-click="handleBottomAction" v-if="!isSummaryMode && !isNotInTime" />
|
|
|
+
|
|
|
+ <!-- 二次确认弹窗(取消“已上车”) -->
|
|
|
+ <SsConfirm v-model:visible="showConfirm" width="90%" height="34vh" :bottom-buttons="confirmButtons"
|
|
|
+ @button-click="handleConfirmAction" @close="handleConfirmClose">
|
|
|
+ <view class="confirm-message">
|
|
|
+ <image class="confirm-icon" src="/static/images/warning.gif" mode="aspectFit" />
|
|
|
+ <view class="confirm-text">{{ confirmTextLine1 }}</view>
|
|
|
+ <view class="confirm-text">{{ confirmTextLine2 }}</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 SsBottom from '@/components/SsBottom/index.vue'
|
|
|
+import SsConfirm from '@/components/SsConfirm/index.vue'
|
|
|
+import { xcdmApi } from '@/api/xcdm'
|
|
|
+import { formatDate as utilFormatDate } from '@/utils/date'
|
|
|
+
|
|
|
+import Icon from '@/components/icon/index.vue';
|
|
|
+
|
|
|
+// ==================== 数据定义 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 表单数据
|
|
|
+ * @property {string} rq - 日期
|
|
|
+ * @property {string} jc - 节次 (1-上午, 2-下午)
|
|
|
+ */
|
|
|
+const formData = ref({
|
|
|
+ carNo: '粤A88888',
|
|
|
+ rq: '', // 将在初始化时设置为今天
|
|
|
+ jc: '', // 将根据当前时间自动设置
|
|
|
+})
|
|
|
+
|
|
|
+// 校车点名类别码(接口返回 xcdmlbm):1=接,51=送
|
|
|
+const xcdmlbm = ref(1)
|
|
|
+// 当前站点序号(接口返回 curZdxh,1-based)
|
|
|
+const curZdxh = ref(1)
|
|
|
+// 接口返回:校车ID、接送时间段
|
|
|
+const xcid = ref('')
|
|
|
+const jskssj = ref('')
|
|
|
+const jsjssj = ref('')
|
|
|
+
|
|
|
+// 日期相关数据
|
|
|
+const maxDate = ref(new Date()) // 最大日期为今天,禁止选择今天之后的
|
|
|
+const weekdayText = ref('') // 星期几的显示文本
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面状态管理
|
|
|
+ * @property {Array} zdcyList - 成员列表数据
|
|
|
+ */
|
|
|
+const isSummaryMode = ref(false) // 是否为提交后的摘要模式
|
|
|
+const isNotInTime = ref(false)
|
|
|
+// 提交后摘要(接口返回 jsrs/qjrs/ycrs)
|
|
|
+const summaryJsrs = ref(0)
|
|
|
+const summaryQjrs = ref(0)
|
|
|
+const summaryYcrs = ref(0)
|
|
|
+const NotInTime = ref([
|
|
|
+ {
|
|
|
+ icon: 'icon-shangwu',
|
|
|
+ iconColor: '#1296db',
|
|
|
+ iconSize: 97,
|
|
|
+ title: '上午',
|
|
|
+ range:'7:40~12:00'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ icon: 'icon-xiawu',
|
|
|
+ iconColor: '#dd9900',
|
|
|
+ iconSize: 97,
|
|
|
+ title: '下午',
|
|
|
+ range:'13:40~16:00'
|
|
|
+ }
|
|
|
+])
|
|
|
+const notInTimeMsg = ref('')
|
|
|
+const zdcyList = ref([]) // 成员列表(接口返回 zdcyList)
|
|
|
+// 列表展示:按站点顺序排序(送时站点倒序)
|
|
|
+const displayZdcyList = computed(() => {
|
|
|
+ const list = Array.isArray(zdcyList.value) ? zdcyList.value : []
|
|
|
+ return [...list].sort((a, b) => {
|
|
|
+ const ao = getOrderIndexByZdxh(a?.zdxh)
|
|
|
+ const bo = getOrderIndexByZdxh(b?.zdxh)
|
|
|
+ if (ao !== bo) return ao - bo
|
|
|
+ return Number(a?.zdryid || 0) - Number(b?.zdryid || 0)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+const bottomButtons = [
|
|
|
+ { text: '保存并提交', action: 'save' }
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+// 确认弹窗相关
|
|
|
+const showConfirm = ref(false) // 是否显示确认弹窗
|
|
|
+
|
|
|
+/**
|
|
|
+ * 计算统计(校车点名)
|
|
|
+ */
|
|
|
+const stats = computed(() => {
|
|
|
+ const leave = zdcyList.value.filter(s => isLeave(s)).length
|
|
|
+ const wait = zdcyList.value.filter(s => isWait(s)).length
|
|
|
+ const on = zdcyList.value.filter(s => isSuccess(s)).length
|
|
|
+ const off = zdcyList.value.filter(s => isUnboard(s)).length
|
|
|
+ return { leave, wait, on, off }
|
|
|
+})
|
|
|
+
|
|
|
+const parentLeaveCount = computed(() => stats.value.leave)
|
|
|
+
|
|
|
+const totalCount = computed(() => zdcyList.value.length)
|
|
|
+
|
|
|
+const boardedCount = computed(() => stats.value.on)
|
|
|
+
|
|
|
+const rollcallProgressText = computed(() => {
|
|
|
+ const left = String(boardedCount.value).padStart(2, '0')
|
|
|
+ const right = String(totalCount.value).padStart(2, '0')
|
|
|
+ return `${left}/${right} 人`
|
|
|
+})
|
|
|
+
|
|
|
+const summaryOffCount = computed(() => (isSummaryMode.value ? summaryYcrs.value : stats.value.off))
|
|
|
+
|
|
|
+/**
|
|
|
+ * 计算学生列表区域的样式
|
|
|
+ */
|
|
|
+const studentTableWrapperStyle = computed(() => {
|
|
|
+ const headerPaddingAdjustRpx = 20 // 约 10px,微调顶部间距
|
|
|
+ return {
|
|
|
+ paddingTop: Math.max(0, headerHeight.value - headerPaddingAdjustRpx) + 'rpx'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 站点列表(接口返回 zdList:[{xh,mc,jc}])
|
|
|
+const zdList = ref([
|
|
|
+ { xh: 1, mc: '学校', jc: 'A' },
|
|
|
+ { xh: 2, mc: '莲花山头', jc: 'B' },
|
|
|
+ { xh: 3, mc: '北塘四环公交站', jc: 'C' },
|
|
|
+ { xh: 4, mc: '莲花山(山尾)', jc: 'D' },
|
|
|
+ { xh: 5, mc: '碧桂园天玺山', jc: 'E' },
|
|
|
+ { xh: 6, mc: '锦城花园', jc: 'F' },
|
|
|
+ { xh: 7, mc: '华大路口', jc: 'G' },
|
|
|
+ { xh: 8, mc: '海丰中专1站', jc: 'H' },
|
|
|
+ { xh: 9, mc: '海丰中专2站', jc: 'I' },
|
|
|
+ { xh: 10, mc: '结束点', jc: 'J' },
|
|
|
+])
|
|
|
+
|
|
|
+// 送(xcdmlbm=51)时,站点展示倒序(不改后端字段本身)
|
|
|
+const displayZdList = computed(() => {
|
|
|
+ const list = Array.isArray(zdList.value) ? zdList.value : []
|
|
|
+ return xcdmlbm.value === 51 ? [...list].reverse() : list
|
|
|
+})
|
|
|
+
|
|
|
+const getOrderIndexByZdxh = (zdxh) => {
|
|
|
+ const idx = displayZdList.value.findIndex(z => Number(z?.xh) === Number(zdxh))
|
|
|
+ return idx >= 0 ? idx : 0
|
|
|
+}
|
|
|
+
|
|
|
+const getStopIndexByZdxh = (zdxh) => getOrderIndexByZdxh(zdxh)
|
|
|
+
|
|
|
+// 实际当前站(由 curZdxh 决定),centerStopIndex 是站点条中间(第4个点)的那个
|
|
|
+const actualStopIndex = computed(() => getStopIndexByZdxh(curZdxh.value))
|
|
|
+const centerStopIndex = ref(0)
|
|
|
+
|
|
|
+const clampIndex = (idx, max) => Math.min(max, Math.max(0, idx))
|
|
|
+
|
|
|
+const slotStopIndices = computed(() => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ if (!stops.length) return Array.from({ length: 7 }).map(() => -1)
|
|
|
+ const center = clampIndex(centerStopIndex.value, stops.length - 1)
|
|
|
+ return Array.from({ length: 7 }).map((_, slotIdx) => center + (slotIdx - 3))
|
|
|
+})
|
|
|
+
|
|
|
+const getStopDotClass = (slotIdx) => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ const actual = clampIndex(actualStopIndex.value, Math.max(0, stops.length - 1))
|
|
|
+ const stopIndex = slotStopIndices.value[slotIdx]
|
|
|
+ const exists = stopIndex >= 0 && stopIndex < stops.length
|
|
|
+ const isCenterSlot = slotIdx === 3
|
|
|
+
|
|
|
+ if (!exists) return 'dot-empty'
|
|
|
+ if (isCenterSlot) {
|
|
|
+ if (stopIndex === actual) return 'dot-current'
|
|
|
+ if (stopIndex < actual) return 'dot-focus-past'
|
|
|
+ return 'dot-focus-future'
|
|
|
+ }
|
|
|
+ if (stopIndex === actual) return 'dot-actual'
|
|
|
+ if (stopIndex < actual) return 'dot-past'
|
|
|
+ return 'dot-future'
|
|
|
+}
|
|
|
+
|
|
|
+const currentStopName = computed(() => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ if (!stops.length) return ''
|
|
|
+ const center = clampIndex(centerStopIndex.value, stops.length - 1)
|
|
|
+ return stops[center]?.mc || ''
|
|
|
+})
|
|
|
+
|
|
|
+const prevStopName = computed(() => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ if (!stops.length) return ''
|
|
|
+ const center = clampIndex(centerStopIndex.value, stops.length - 1)
|
|
|
+ return stops[center - 1]?.mc || ''
|
|
|
+})
|
|
|
+
|
|
|
+const nextStopName = computed(() => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ if (!stops.length) return ''
|
|
|
+ const center = clampIndex(centerStopIndex.value, stops.length - 1)
|
|
|
+ return stops[center + 1]?.mc || ''
|
|
|
+})
|
|
|
+
|
|
|
+const getStopLineClass = (slotIdx) => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ const a = slotStopIndices.value[slotIdx]
|
|
|
+ const b = slotStopIndices.value[slotIdx + 1]
|
|
|
+ const aExists = a >= 0 && a < stops.length
|
|
|
+ const bExists = b >= 0 && b < stops.length
|
|
|
+ if (!aExists || !bExists) return 'line-empty'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+// 站点条左右滑动切换当前站
|
|
|
+const stopTouchStartX = ref(0)
|
|
|
+const stopTouchStartY = ref(0)
|
|
|
+const stopSwipeHandled = ref(false)
|
|
|
+
|
|
|
+const handleStopTouchStart = (event) => {
|
|
|
+ stopTouchStartX.value = event.touches[0].clientX
|
|
|
+ stopTouchStartY.value = event.touches[0].clientY
|
|
|
+ stopSwipeHandled.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleStopTouchMove = (event) => {
|
|
|
+ if (stopSwipeHandled.value) return
|
|
|
+ const deltaX = event.touches[0].clientX - stopTouchStartX.value
|
|
|
+ const deltaY = event.touches[0].clientY - stopTouchStartY.value
|
|
|
+ if (Math.abs(deltaX) <= Math.abs(deltaY) || Math.abs(deltaX) < 35) return
|
|
|
+
|
|
|
+ const maxIndex = Math.max(0, displayZdList.value.length - 1)
|
|
|
+ // 横向滑动时拦截,避免触发外层 swiper
|
|
|
+ try { event.preventDefault && event.preventDefault() } catch (e) { }
|
|
|
+ if (deltaX < 0) centerStopIndex.value = Math.min(maxIndex, centerStopIndex.value + 1)
|
|
|
+ else centerStopIndex.value = Math.max(0, centerStopIndex.value - 1)
|
|
|
+ stopSwipeHandled.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleStopTouchEnd = () => {
|
|
|
+ stopSwipeHandled.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleStopTap = (slotIdx) => {
|
|
|
+ const stops = displayZdList.value
|
|
|
+ const stopIndex = slotStopIndices.value[slotIdx]
|
|
|
+ const exists = stopIndex >= 0 && stopIndex < stops.length
|
|
|
+ if (!exists) return
|
|
|
+ centerStopIndex.value = stopIndex
|
|
|
+ console.log('[mock] centerStopIndex', centerStopIndex.value, 'curZdxh', curZdxh.value)
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 站点条 ↔ 学生列表联动 ====================
|
|
|
+const rowHeightPx = ref(0)
|
|
|
+const listViewportHeightPx = ref(0)
|
|
|
+const syncingFromListScroll = ref(false)
|
|
|
+const suppressListScrollSyncUntil = ref(0)
|
|
|
+
|
|
|
+const measureRowHeightPx = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(instance)
|
|
|
+ .select('.row-wrapper')
|
|
|
+ .boundingClientRect((rect) => {
|
|
|
+ if (rect?.height) rowHeightPx.value = rect.height
|
|
|
+ })
|
|
|
+ .exec()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const measureListViewportHeightPx = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(instance)
|
|
|
+ .select('.student-list-area')
|
|
|
+ .boundingClientRect((rect) => {
|
|
|
+ if (rect?.height) listViewportHeightPx.value = rect.height
|
|
|
+ })
|
|
|
+ .exec()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const scrollListToCenterStop = ({ animate = true } = {}) => {
|
|
|
+ const stopIndex = centerStopIndex.value
|
|
|
+ const zdxh = displayZdList.value[stopIndex]?.xh
|
|
|
+ const target = displayZdcyList.value.find(s => Number(s.zdxh) === Number(zdxh))
|
|
|
+ if (!target) return
|
|
|
+ suppressListScrollSyncUntil.value = Date.now() + 500
|
|
|
+ // scroll-into-view 无法动态控制动画开关,这里仅通过节流避免回流抖动
|
|
|
+ scrollIntoViewId.value = ''
|
|
|
+ nextTick(() => {
|
|
|
+ scrollIntoViewId.value = `row_${target.zdryid}`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+watch(centerStopIndex, () => {
|
|
|
+ if (syncingFromListScroll.value) return
|
|
|
+ scrollListToCenterStop({ animate: true })
|
|
|
+})
|
|
|
+
|
|
|
+const handleStudentListScroll = (e) => {
|
|
|
+ if (showConfirm.value) return
|
|
|
+ if (Date.now() < suppressListScrollSyncUntil.value) return
|
|
|
+ if (!rowHeightPx.value) return
|
|
|
+ const scrollTop = e?.detail?.scrollTop ?? 0
|
|
|
+ const scrollHeight = e?.detail?.scrollHeight ?? 0
|
|
|
+ const viewport = listViewportHeightPx.value || 0
|
|
|
+ const edgeThresholdPx = 6
|
|
|
+
|
|
|
+ // 顶部/底部兜底:解决“滚动到极限但 floor 计算不到最后一项”
|
|
|
+ if (scrollTop <= edgeThresholdPx) {
|
|
|
+ const first = displayZdcyList.value[0]
|
|
|
+ const nextCenter = first ? getStopIndexByZdxh(first.zdxh) : centerStopIndex.value
|
|
|
+ if (first && nextCenter !== centerStopIndex.value) {
|
|
|
+ syncingFromListScroll.value = true
|
|
|
+ centerStopIndex.value = nextCenter
|
|
|
+ nextTick(() => { syncingFromListScroll.value = false })
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (scrollHeight && viewport && scrollTop + viewport >= scrollHeight - edgeThresholdPx) {
|
|
|
+ const last = displayZdcyList.value[displayZdcyList.value.length - 1]
|
|
|
+ const nextCenter = last ? getStopIndexByZdxh(last.zdxh) : centerStopIndex.value
|
|
|
+ if (last && nextCenter !== centerStopIndex.value) {
|
|
|
+ syncingFromListScroll.value = true
|
|
|
+ centerStopIndex.value = nextCenter
|
|
|
+ nextTick(() => { syncingFromListScroll.value = false })
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const idx = Math.max(0, Math.min(displayZdcyList.value.length - 1, Math.floor(scrollTop / rowHeightPx.value)))
|
|
|
+ const student = displayZdcyList.value[idx]
|
|
|
+ if (!student) return
|
|
|
+ const nextCenter = getStopIndexByZdxh(student.zdxh)
|
|
|
+ if (nextCenter === centerStopIndex.value) return
|
|
|
+
|
|
|
+ syncingFromListScroll.value = true
|
|
|
+ centerStopIndex.value = nextCenter
|
|
|
+ nextTick(() => {
|
|
|
+ syncingFromListScroll.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// ==================== 确认弹窗配置 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 确认弹窗按钮配置
|
|
|
+ */
|
|
|
+const confirmButtons = ref([
|
|
|
+ { text: '取消', action: 'cancel', type: 'default' },
|
|
|
+ { text: '确认', action: 'confirm', type: 'primary' }
|
|
|
+])
|
|
|
+
|
|
|
+const confirmTextLine1 = ref('')
|
|
|
+const confirmTextLine2 = ref('')
|
|
|
+
|
|
|
+// ==================== 表单配置 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 表单字段校验配置
|
|
|
+ * 定义各个字段的校验规则
|
|
|
+ */
|
|
|
+const fieldConfigs = computed(() => {
|
|
|
+ const configs = {
|
|
|
+ rq: {
|
|
|
+ rules: [{ required: true, message: '日期不能为空' }]
|
|
|
+ },
|
|
|
+ jc: {
|
|
|
+ rules: [{ required: true, message: '请选择节次' }]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return configs
|
|
|
+})
|
|
|
+/**
|
|
|
+ * 表单引用
|
|
|
+ * 用于表单校验和数据提交
|
|
|
+ */
|
|
|
+const formRef = ref(null)
|
|
|
+
|
|
|
+/**
|
|
|
+ * 初始化表单默认值
|
|
|
+ */
|
|
|
+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) {
|
|
|
+ jcValue = '1'
|
|
|
+ } else {
|
|
|
+ jcValue = '2'
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 校车点名初始化(接口) ====================
|
|
|
+const loadXcdmInit = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ rq: formData.value.rq,
|
|
|
+ jc: formData.value.jc,
|
|
|
+ carNo: formData.value.carNo,
|
|
|
+ }
|
|
|
+ const res = await xcdmApi.mp_xcdmHomep_load(params)
|
|
|
+ console.log('mp_xcdmHomep_load:', res)
|
|
|
+
|
|
|
+ // 先只看返回结构,后续再接字段映射
|
|
|
+ if (res?.data?.xcdmlbm != null) {
|
|
|
+ xcdmlbm.value = Number(res.data.xcdmlbm) || 1
|
|
|
+ // 节次:接=上午(1),送=下午(2)
|
|
|
+ formData.value.jc = xcdmlbm.value === 51 ? '2' : '1'
|
|
|
+ }
|
|
|
+ // 车牌号回显:优先 xcList[0].mc,其次接口 carNo
|
|
|
+ const apiCarNo = res?.data?.xcList?.[0]?.mc || res?.data?.carNo
|
|
|
+ if (apiCarNo) formData.value.carNo = apiCarNo
|
|
|
+ if (res?.data?.xcid != null) xcid.value = String(res.data.xcid)
|
|
|
+ if (res?.data?.jskssj != null) jskssj.value = utilFormatDate(String(res.data.jskssj), 'yyyy-MM-dd HH:mm:ss')
|
|
|
+ if (res?.data?.jsjssj != null) jsjssj.value = utilFormatDate(String(res.data.jsjssj), 'yyyy-MM-dd HH:mm:ss')
|
|
|
+ if (res?.data?.curZdxh != null) curZdxh.value = Number(res.data.curZdxh) || 1
|
|
|
+ if (Array.isArray(res?.data?.zdList) && res.data.zdList.length) {
|
|
|
+ zdList.value = res.data.zdList
|
|
|
+ }
|
|
|
+ if (Array.isArray(res?.data?.zdcyList)) {
|
|
|
+ zdcyList.value = res.data.zdcyList
|
|
|
+ }
|
|
|
+ // 初始化时把中心点对齐到当前站
|
|
|
+ centerStopIndex.value = actualStopIndex.value
|
|
|
+ // 已点名摘要:只要返回 jsrs 就视为已点名(qjrs/ycrs 可能任何时候都会返回)
|
|
|
+ if (res?.data?.jsrs !== undefined && res?.data?.jsrs !== null) {
|
|
|
+ isSummaryMode.value = true
|
|
|
+ summaryJsrs.value = Number(res.data.jsrs || 0)
|
|
|
+ summaryQjrs.value = Number(res.data.qjrs || 0)
|
|
|
+ summaryYcrs.value = Number(res.data.ycrs || 0)
|
|
|
+ // 摘要模式不展示列表
|
|
|
+ zdcyList.value = []
|
|
|
+ } else {
|
|
|
+ isSummaryMode.value = false
|
|
|
+ summaryJsrs.value = 0
|
|
|
+ summaryQjrs.value = 0
|
|
|
+ summaryYcrs.value = 0
|
|
|
+ }
|
|
|
+ const msg = res?.data?.msg ? String(res.data.msg) : ''
|
|
|
+ notInTimeMsg.value = ''
|
|
|
+ if (msg.includes('不在校车接送时段') || msg.includes('不在点名时段')) {
|
|
|
+ isNotInTime.value = true
|
|
|
+ notInTimeMsg.value = msg
|
|
|
+ if (res?.data?.swsjd && NotInTime.value[0]) NotInTime.value[0].range = String(res.data.swsjd)
|
|
|
+ if (res?.data?.xwsjd && NotInTime.value[1]) NotInTime.value[1].range = String(res.data.xwsjd)
|
|
|
+ }
|
|
|
+ return res
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('mp_xcdmHomep_load failed:', e)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 事件处理方法 ====================
|
|
|
+
|
|
|
+// ==================== 状态码工具(上午接/下午送) ====================
|
|
|
+const getModeLabels = () => {
|
|
|
+ return xcdmlbm.value === 51
|
|
|
+ ? { base: '已上车', success: '已送达', unboard: '未上车' }
|
|
|
+ : { base: '待上车', success: '已上车', unboard: '未上车' }
|
|
|
+}
|
|
|
+
|
|
|
+const isLeave = (student) => Number(student?.rcid || 0) > 0
|
|
|
+const isUnboard = (student) => [11, 61].includes(Number(student?.xcjslbm))
|
|
|
+const isSuccess = (student) => [1, 51].includes(Number(student?.xcjslbm))
|
|
|
+const isWait = (student) => Number(student?.xcjslbm) === 0 && !isLeave(student)
|
|
|
+
|
|
|
+const getStatusText = (student) => {
|
|
|
+ if (isLeave(student)) return '请假'
|
|
|
+ const code = Number(student?.xcjslbm)
|
|
|
+ const labels = getModeLabels()
|
|
|
+ if (code === 0) return labels.base
|
|
|
+ if (code === 1 || code === 51) return labels.success
|
|
|
+ if (code === 11 || code === 61) return labels.unboard
|
|
|
+ return labels.base
|
|
|
+}
|
|
|
+
|
|
|
+const shouldShowException = (student) => {
|
|
|
+ if (!student) return false
|
|
|
+ if (isLeave(student)) return false
|
|
|
+ // 待上车/已上车时可异常登记;未上车不显示异常登记
|
|
|
+ return Number(student.xcjslbm) === 0 || isSuccess(student)
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 状态切换接口(mp_xcdmHomep_swState) ====================
|
|
|
+const callSwState = async ({ student, xcjslbm, sm }) => {
|
|
|
+ const payload = {
|
|
|
+ xcid: xcid.value,
|
|
|
+ xcdmlbm: xcdmlbm.value,
|
|
|
+ jskssj: jskssj.value,
|
|
|
+ jsjssj: jsjssj.value,
|
|
|
+ zdryid: student?.zdryid,
|
|
|
+ xcjslbm,
|
|
|
+ }
|
|
|
+ if (Number(xcjslbm) === 0 && student?.xcjsjlid != null) payload.xcjsjlid = student.xcjsjlid
|
|
|
+ if (sm) payload.sm = sm
|
|
|
+ const res = await xcdmApi.mp_xcdmHomep_swState(payload)
|
|
|
+ console.log('mp_xcdmHomep_swState:', payload, res)
|
|
|
+ return res
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 加载学生数据
|
|
|
+ * 当前无接口:使用前端 mock 数据
|
|
|
+ */
|
|
|
+const loadStudentData = async ({ syncScrollToCenter = false } = {}) => {
|
|
|
+ try {
|
|
|
+ isSummaryMode.value = false
|
|
|
+ isNotInTime.value = false
|
|
|
+
|
|
|
+ // 初始化接口(先打印返回,后续再把返回映射到页面字段)
|
|
|
+ await loadXcdmInit()
|
|
|
+ if (isNotInTime.value) return
|
|
|
+ if (isSummaryMode.value) return
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ measureHeaderHeight()
|
|
|
+ measureRowHeightPx()
|
|
|
+ measureListViewportHeightPx()
|
|
|
+ if (syncScrollToCenter) scrollListToCenterStop({ animate: true })
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载学生数据失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: '加载学生数据失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+/**
|
|
|
+ * 处理学生点击
|
|
|
+ * 切换学生的考勤状态
|
|
|
+ * @param {Object} student - 被点击的学生对象
|
|
|
+ */
|
|
|
+const handleStudentClick = async (student) => {
|
|
|
+ // console.log('点击学生:', student)
|
|
|
+
|
|
|
+ if (isLeave(student)) return
|
|
|
+
|
|
|
+ const code = Number(student?.xcjslbm)
|
|
|
+ const labels = getModeLabels()
|
|
|
+
|
|
|
+ // 已上车/已送达 -> 取消回到 0;未上车 -> 确认回到 0
|
|
|
+ if (isSuccess(student)) {
|
|
|
+ closeAllSwipeStates()
|
|
|
+ pendingStatusChange.value = { zdryid: student.zdryid, to: 0 }
|
|
|
+ confirmTextLine1.value = `“${labels.success}”消息已发送至家长`
|
|
|
+ confirmTextLine2.value = `是否取消“${labels.success}”?`
|
|
|
+ showConfirm.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (isUnboard(student)) {
|
|
|
+ closeAllSwipeStates()
|
|
|
+ pendingStatusChange.value = { zdryid: student.zdryid, to: 0 }
|
|
|
+ confirmTextLine1.value = `当前状态:${labels.unboard}`
|
|
|
+ confirmTextLine2.value = `是否改为“${labels.base}”?`
|
|
|
+ showConfirm.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 0 -> 1(上午接) / 51(下午送)
|
|
|
+ if (code === 0) {
|
|
|
+ const nextCode = xcdmlbm.value === 51 ? 51 : 1
|
|
|
+ const targetStudent = zdcyList.value.find(s => Number(s.zdryid) === Number(student.zdryid))
|
|
|
+ if (targetStudent) {
|
|
|
+ try {
|
|
|
+ await callSwState({ student: targetStudent, xcjslbm: nextCode })
|
|
|
+ targetStudent.xcjslbm = nextCode
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('mp_xcdmHomep_swState failed:', e)
|
|
|
+ uni.showToast({ title: '切换失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleBottomAction = ({ action }) => {
|
|
|
+ if (action === 'save') handleSave()
|
|
|
+}
|
|
|
+
|
|
|
+const handleSave = async () => {
|
|
|
+ try {
|
|
|
+ if (!xcid.value || !jskssj.value || !jsjssj.value) {
|
|
|
+ uni.showToast({ title: '缺少校车信息', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const qjrs = zdcyList.value.filter(s => isLeave(s)).length
|
|
|
+ const ycrs = zdcyList.value.filter(s => isUnboard(s)).length
|
|
|
+ const jsrs = Math.max(0, zdcyList.value.length - qjrs)
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ xcid: xcid.value,
|
|
|
+ jskssj: jskssj.value,
|
|
|
+ jsjssj: jsjssj.value,
|
|
|
+ jsrs,
|
|
|
+ qjrs,
|
|
|
+ ycrs,
|
|
|
+ }
|
|
|
+ const res = await xcdmApi.mp_xcdmHomep_subm(payload)
|
|
|
+ console.log('mp_xcdmHomep_subm:', payload, res)
|
|
|
+ uni.showToast({ title: '提交成功', icon: 'success' })
|
|
|
+ // 提交后重新拉取一次,刷新为“已点名”摘要
|
|
|
+ await loadStudentData({ syncScrollToCenter: false })
|
|
|
+ } catch (e) {
|
|
|
+ console.error('提交失败:', e)
|
|
|
+ uni.showToast({ title: '提交失败', icon: 'none' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 左滑状态管理(参考 todo_list.vue)
|
|
|
+const swipeStates = ref({})
|
|
|
+const touchStartX = ref(0)
|
|
|
+const touchStartY = ref(0)
|
|
|
+const isSwiping = ref(false)
|
|
|
+
|
|
|
+const actionWidth = (student) => {
|
|
|
+ // 待上车 / 已上车:异常登记+家长电话;未上车/请假:仅家长电话
|
|
|
+ return shouldShowException(student) ? 320 : 160
|
|
|
+}
|
|
|
+
|
|
|
+const closeAllSwipeStates = () => {
|
|
|
+ Object.keys(swipeStates.value).forEach(key => {
|
|
|
+ swipeStates.value[key] = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleTouchStart = (event, itemId) => {
|
|
|
+ if (showConfirm.value) return
|
|
|
+ touchStartX.value = event.touches[0].clientX
|
|
|
+ touchStartY.value = event.touches[0].clientY
|
|
|
+}
|
|
|
+
|
|
|
+const handleTouchMove = (event, itemId) => {
|
|
|
+ if (showConfirm.value) return
|
|
|
+ const deltaX = event.touches[0].clientX - touchStartX.value
|
|
|
+ const deltaY = event.touches[0].clientY - touchStartY.value
|
|
|
+
|
|
|
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 30) {
|
|
|
+ isSwiping.value = true
|
|
|
+ event.preventDefault()
|
|
|
+ if (deltaX < -50) {
|
|
|
+ closeAllSwipeStates()
|
|
|
+ swipeStates.value[itemId] = true
|
|
|
+ } else if (deltaX > 50) {
|
|
|
+ swipeStates.value[itemId] = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTouchEnd = () => {
|
|
|
+ if (showConfirm.value) return
|
|
|
+ // 延迟清理,避免 touchend 后立刻触发 tap
|
|
|
+ setTimeout(() => {
|
|
|
+ isSwiping.value = false
|
|
|
+ }, 50)
|
|
|
+}
|
|
|
+
|
|
|
+const handleStudentTap = (student) => {
|
|
|
+ if (isSwiping.value) return
|
|
|
+ handleStudentClick(student)
|
|
|
+}
|
|
|
+
|
|
|
+const pendingStatusChange = ref(null)
|
|
|
+
|
|
|
+const scrollIntoViewId = ref('')
|
|
|
+
|
|
|
+const handleCallParent = (student) => {
|
|
|
+ // 保存校车电话记录(不阻塞拨号)
|
|
|
+ try {
|
|
|
+ if (xcid.value && jskssj.value && jsjssj.value) {
|
|
|
+ xcdmApi.mp_xcdmHomep_saveXcdhjl({
|
|
|
+ xcid: xcid.value,
|
|
|
+ jskssj: jskssj.value,
|
|
|
+ jsjssj: jsjssj.value,
|
|
|
+ xcdmlbm: xcdmlbm.value,
|
|
|
+ zdryid: student.zdryid,
|
|
|
+ }).then((res) => {
|
|
|
+ console.log('mp_xcdmHomep_saveXcdhjl:', res)
|
|
|
+ }).catch((e) => {
|
|
|
+ console.warn('mp_xcdmHomep_saveXcdhjl failed:', e)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) { }
|
|
|
+ console.log('[mock] makePhoneCall', { zdryid: student.zdryid, phoneNumber: student.jzdh })
|
|
|
+ uni.makePhoneCall({ phoneNumber: student.jzdh })
|
|
|
+}
|
|
|
+
|
|
|
+const handleException = (student) => {
|
|
|
+ closeAllSwipeStates()
|
|
|
+ const mode = xcdmlbm.value
|
|
|
+ const site = zdList.value.find(z => Number(z.xh) === Number(student.zdxh))?.mc || ''
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/xcdm/exception?zdryid=${encodeURIComponent(student.zdryid)}&xm=${encodeURIComponent(student.xm)}&site=${encodeURIComponent(site)}&xcdmlbm=${encodeURIComponent(String(mode))}&xcid=${encodeURIComponent(String(xcid.value || ''))}&jskssj=${encodeURIComponent(String(jskssj.value || ''))}&jsjssj=${encodeURIComponent(String(jsjssj.value || ''))}`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取表格行的样式类
|
|
|
+ * @param {string} status - 状态
|
|
|
+ * @returns {string} CSS类名
|
|
|
+ */
|
|
|
+const getRowClass = (student) => {
|
|
|
+ const classes = []
|
|
|
+ const curOrder = getOrderIndexByZdxh(curZdxh.value)
|
|
|
+ const stuOrder = getOrderIndexByZdxh(student?.zdxh)
|
|
|
+ const stage = stuOrder < curOrder ? 'past' : stuOrder === curOrder ? 'current' : 'future'
|
|
|
+ if (stage === 'past') classes.push('row-site-past')
|
|
|
+ else if (stage === 'current') classes.push('row-site-current')
|
|
|
+ else classes.push('row-site-future')
|
|
|
+
|
|
|
+ if (isLeave(student)) {
|
|
|
+ classes.push('row-status-leave')
|
|
|
+ if (stage === 'current') classes.push('row-leave-current')
|
|
|
+ } else if (isUnboard(student)) {
|
|
|
+ classes.push('row-status-off')
|
|
|
+ } else if (isSuccess(student)) {
|
|
|
+ classes.push('row-status-on')
|
|
|
+ }
|
|
|
+
|
|
|
+ return classes.join(' ')
|
|
|
+}
|
|
|
+
|
|
|
+const getDragIndicatorStyle = (student) => {
|
|
|
+ let backgroundColor = '#357cdf'
|
|
|
+ if (isUnboard(student)) return { backgroundColor: '#ff0000' }
|
|
|
+ const curOrder = getOrderIndexByZdxh(curZdxh.value)
|
|
|
+ const stuOrder = getOrderIndexByZdxh(student?.zdxh)
|
|
|
+ const stage = stuOrder < curOrder ? 'past' : stuOrder === curOrder ? 'current' : 'future'
|
|
|
+ if (stage === 'past') backgroundColor = '#999999'
|
|
|
+ if (stage === 'future') backgroundColor = '#6b9ce0'
|
|
|
+ if (stage === 'current') backgroundColor = '#357cdf'
|
|
|
+ return { backgroundColor }
|
|
|
+}
|
|
|
+
|
|
|
+const headerHeight = ref(300) // 固定头部高度(rpx),初始值设大一点避免遮挡
|
|
|
+
|
|
|
+// 获取当前组件实例(用于 SelectorQuery)
|
|
|
+const instance = getCurrentInstance()
|
|
|
+
|
|
|
+/**
|
|
|
+ * 测量并更新头部高度
|
|
|
+ * @param {number} retryCount - 重试次数
|
|
|
+ */
|
|
|
+const measureHeaderHeight = (retryCount = 0) => {
|
|
|
+ nextTick(() => {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(instance) // 指定在当前组件实例中查询
|
|
|
+ .select('.fixed-header')
|
|
|
+ .boundingClientRect((rect) => {
|
|
|
+ if (rect && rect.height > 0) {
|
|
|
+ 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()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 确认弹窗按钮点击处理
|
|
|
+ */
|
|
|
+const handleConfirmAction = async (data) => {
|
|
|
+ // console.log('确认弹窗按钮点击:', data)
|
|
|
+
|
|
|
+ switch (data.action) {
|
|
|
+ case 'cancel':
|
|
|
+ showConfirm.value = false
|
|
|
+ pendingStatusChange.value = null
|
|
|
+ break
|
|
|
+ case 'confirm':
|
|
|
+ showConfirm.value = false
|
|
|
+ if (pendingStatusChange.value?.zdryid) {
|
|
|
+ const { zdryid, to } = pendingStatusChange.value
|
|
|
+ const targetStudent = zdcyList.value.find(s => Number(s.zdryid) === Number(zdryid))
|
|
|
+ if (targetStudent && !isLeave(targetStudent)) {
|
|
|
+ try {
|
|
|
+ await callSwState({ student: targetStudent, xcjslbm: Number(to) })
|
|
|
+ targetStudent.xcjslbm = Number(to)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('mp_xcdmHomep_swState failed:', e)
|
|
|
+ uni.showToast({ title: '切换失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pendingStatusChange.value = null
|
|
|
+ break
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 确认弹窗关闭处理
|
|
|
+ */
|
|
|
+const handleConfirmClose = () => {
|
|
|
+ // console.log('确认弹窗关闭')
|
|
|
+ showConfirm.value = false
|
|
|
+ pendingStatusChange.value = null
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 页面初始化 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面挂载时的初始化
|
|
|
+ */
|
|
|
+// 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)
|
|
|
+// // 如有需要,可在此初始化更多 mock 数据
|
|
|
+// })
|
|
|
+
|
|
|
+
|
|
|
+// ============== 暴露给主容器的生命周期(供 pages/main/index.vue 调用) ==============
|
|
|
+// 避免重复初始化的标记
|
|
|
+
|
|
|
+function onLoad() {
|
|
|
+ initFormDefaults()
|
|
|
+ loadStudentData({ syncScrollToCenter: true })
|
|
|
+}
|
|
|
+
|
|
|
+function onShow() {
|
|
|
+ // 每次切到该页时都刷新数据
|
|
|
+ // 头部高度会在 loadStudentData 完成后自动测量
|
|
|
+ initFormDefaults()
|
|
|
+ loadStudentData({ syncScrollToCenter: false })
|
|
|
+}
|
|
|
+
|
|
|
+function onHide() {
|
|
|
+ // 可按需做暂存处理
|
|
|
+}
|
|
|
+
|
|
|
+function onUnload() {
|
|
|
+ // 可按需做清理处理
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({ onLoad, onShow, onHide, onUnload })
|
|
|
+
|
|
|
+
|
|
|
+// 预留:后续如需全局滚动处理再接入
|
|
|
+
|
|
|
+</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 18rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.stop-progress {
|
|
|
+ width: 100%;
|
|
|
+ height: 120rpx; // 约 60px
|
|
|
+ background: #f2f3f4;
|
|
|
+ border-bottom: 1rpx solid #eceded;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 0 24rpx 36rpx;
|
|
|
+ overflow: visible;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-track {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-node {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 36rpx; // 18px
|
|
|
+ height: 36rpx; // 18px
|
|
|
+}
|
|
|
+
|
|
|
+.stop-node.center {
|
|
|
+ width: 56rpx; // 28px
|
|
|
+ height: 56rpx; // 28px
|
|
|
+}
|
|
|
+
|
|
|
+.stop-dot {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 9999rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-past {
|
|
|
+ background: #c3c7cb;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-future {
|
|
|
+ background: #fff;
|
|
|
+ border: 2rpx solid #c3c7cb; // 约 1px
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-actual {
|
|
|
+ background: #e59f40;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-current {
|
|
|
+ background: #e59f40;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-focus-future {
|
|
|
+ background: #fff;
|
|
|
+ border: 2rpx solid #e59f40;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-focus-past {
|
|
|
+ background: #c3c7cb;
|
|
|
+ border: 2rpx solid #e59f40;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-empty {
|
|
|
+ background: transparent;
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-line {
|
|
|
+ flex: 1;
|
|
|
+ height: 4rpx; // 约 2px
|
|
|
+ background: #c3c7cb;
|
|
|
+}
|
|
|
+
|
|
|
+.line-empty {
|
|
|
+ background: transparent;
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-label-row {
|
|
|
+ margin-top: 8rpx;
|
|
|
+ position: relative;
|
|
|
+ overflow: visible;
|
|
|
+ height: 52rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-label-current {
|
|
|
+ font-size: 44rpx; // 约 22px
|
|
|
+ color: #000;
|
|
|
+ font-weight: 700;
|
|
|
+ text-align: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: visible;
|
|
|
+ line-height: 1.1;
|
|
|
+ z-index: 2;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-label-side {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #999;
|
|
|
+ font-weight: 400;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ z-index: 1;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-label-prev {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-label-next {
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ .stat-parent {
|
|
|
+ color: #e59f40;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-left {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-right {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.student-list-area {
|
|
|
+ background-color: #ffffff;
|
|
|
+
|
|
|
+ flex: 1;
|
|
|
+ height: 0; // 配合 flex: 1,让 scroll-view 正确计算剩余空间
|
|
|
+}
|
|
|
+
|
|
|
+.student-table-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding-bottom: 120rpx; // 给底部按钮留出空间
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.student-table {
|
|
|
+ width: calc(100% - 32rpx);
|
|
|
+ margin: 0 auto;
|
|
|
+ border: 1rpx solid #d2d2d2;
|
|
|
+ background-color: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+
|
|
|
+ .row-wrapper {
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .row-wrapper:last-child .table-row {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .swipe-actions {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: stretch;
|
|
|
+ z-index: 1;
|
|
|
+ box-sizing: border-box;
|
|
|
+
|
|
|
+ .action-btn {
|
|
|
+ width: 160rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exception-btn {
|
|
|
+ background-color: #ff0000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .phone-btn {
|
|
|
+ background-color: #357cdf;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header {
|
|
|
+ display: flex;
|
|
|
+ background-color: #efefef;
|
|
|
+ border-bottom: 1rpx solid #d2d2d2;
|
|
|
+ padding: 20rpx;
|
|
|
+ align-items: center;
|
|
|
+ // margin-bottom: 10rpx;
|
|
|
+ .col-name {
|
|
|
+ width: 240rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .col-class {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .col-status {
|
|
|
+ width: 160rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row {
|
|
|
+ display: flex;
|
|
|
+ padding: 20rpx;
|
|
|
+ transition: background-color 0.3s ease;
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+ transition: transform 0.2s ease, background-color 0.3s ease;
|
|
|
+ border-bottom: 1rpx solid #d2d2d2;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ background-color: #f0f0f0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .col-name {
|
|
|
+ width: 240rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .col-class {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .col-status {
|
|
|
+ width: 160rpx;
|
|
|
+ text-align: left;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .drag-indicator {
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ top: 0;
|
|
|
+ bottom: 0;
|
|
|
+ width: 8rpx; // 约 4px
|
|
|
+ height: 100%;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态行样式
|
|
|
+ &.row-site-past {
|
|
|
+ background-color: #d4f8d4;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-site-current {
|
|
|
+ background-color: #e59f40;
|
|
|
+ font-weight: 700;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #000;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-site-future {
|
|
|
+ background-color: #fff;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-status-on {
|
|
|
+ background-color: #d4f8d4;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #666;
|
|
|
+ font-weight: 400;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-status-leave {
|
|
|
+ background-color: #c7e0ff;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #000;
|
|
|
+ font-weight: 400;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-leave-current {
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-status-off {
|
|
|
+ background-color: #eb6100;
|
|
|
+ .col-name,
|
|
|
+ .col-class,
|
|
|
+ .col-status {
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 400;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .row-wrapper.swiped .table-row {
|
|
|
+ transform: translateX(calc(-1 * var(--actions-width)));
|
|
|
+ }
|
|
|
+
|
|
|
+ .row-wrapper.swiped .drag-indicator {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 星期几显示样式
|
|
|
+.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;
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.confirm-message {
|
|
|
+ height: calc(100% - 100rpx);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.confirm-icon {
|
|
|
+ width: 120rpx;
|
|
|
+ height: 120rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.confirm-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 400;
|
|
|
+}
|
|
|
+
|
|
|
+// 只读班级名称样式
|
|
|
+.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-tip{
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+ font-weight: 400;
|
|
|
+ margin-top: -14rpx;
|
|
|
+ margin-bottom: 22rpx;
|
|
|
+ }
|
|
|
+ .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>
|