Explorar el Código

feat:一次大更新

apple hace 1 día
padre
commit
8403225a98

+ 12 - 12
api/kqjl.js

@@ -1,7 +1,7 @@
 // 考勤记录api
 import request from "@/utils/request";
 
-export const kqjlApi = {
+export const kqjlApi = {
     bjdm_cx: (data) => {       
         return request.post( `/service?ssServ=bjdm_cx&management=1&isReady=1`,
         data,
@@ -52,14 +52,14 @@ export const kqjlApi = {
 
       );  
     },
-    mp_njdmHomep_refreshLoad:(data) => {
-      return request.post( `/service?ssServ=mp_njdmHomep_refresh`,
-        data,
-        {
-          loading: false,
-          formData: true,
-        }
-      );  
-    },
-
-}
+    mp_njdmHomep_refreshLoad:(data) => {
+      return request.post( `/service?ssServ=mp_njdmHomep_refresh`,
+        data,
+        {
+          loading: false,
+          formData: true,
+        }
+      );  
+    },
+
+}

+ 45 - 0
api/xcdm.js

@@ -0,0 +1,45 @@
+// 校车点名 api
+import request from "@/utils/request";
+
+export const xcdmApi = {
+  mp_xcdmHomep_load: (data) => {
+    return request.post(`/service?ssServ=mp_xcdmHomep_load`,
+      data,
+      {
+        loading: true,
+        formData: true,
+      }
+    );
+  },
+  // 切换校车成员状态
+  mp_xcdmHomep_swState: (data) => {
+    return request.post(`/service?ssServ=mp_xcdmHomep_swState`,
+      data,
+      {
+        loading: false,
+        formData: true,
+      }
+    );
+  },
+  // 提交
+  mp_xcdmHomep_subm: (data) => {
+    return request.post(`/service?ssServ=mp_xcdmHomep_subm`,
+      data,
+      {
+        loading: true,
+        formData: true,
+      }
+    );
+  },
+  // 保存校车电话记录
+  mp_xcdmHomep_saveXcdhjl: (data) => {
+    return request.post(`/service?ssServ=mp_xcdmHomep_saveXcdhjl`,
+      data,
+      {
+        loading: false,
+        formData: true,
+      }
+    );
+  },
+}
+

+ 12 - 6
components/SsConfirm/index.vue

@@ -1,17 +1,17 @@
 <template>
-  <view v-if="visible" class="ss-confirm">
+  <view v-if="visible" class="ss-confirm" catchtouchmove="true" @touchmove.stop.prevent="noop">
     <!-- 遮罩层 - 模糊背景 -->
-    <view class="confirm-mask" @click="handleMaskClick"></view>
+    <view class="confirm-mask" catchtouchmove="true" @touchmove.stop.prevent="noop" @click="handleMaskClick"></view>
 
     <!-- 弹窗内容 -->
-    <view class="confirm-content" :style="contentStyle">
+    <view class="confirm-content" :style="contentStyle" catchtouchmove="true" @touchmove.stop.prevent="noop">
       <!-- 头部信息 - 可选 -->
-      <view v-if="showHeader" class="confirm-header">
+      <view v-if="headerVisible" class="confirm-header">
         <slot name="header">
           <view class="header-title">{{ title }}</view>
         </slot>
       </view>
-      <view class="header-line" v-if="showHeader"></view>
+      <view class="header-line" v-if="headerVisible"></view>
 
       <!-- 主要内容区域 - 通过slot传入 -->
       <view class="confirm-body">
@@ -40,7 +40,7 @@ const props = defineProps({
   // 弹窗标题
   title: {
     type: String,
-    default: '确认'
+    default: ''
   },
   // 是否显示头部
   showHeader: {
@@ -80,6 +80,12 @@ const props = defineProps({
 // 事件定义
 const emit = defineEmits(['update:visible', 'close', 'button-click', 'mask-click'])
 
+const noop = () => {}
+
+const headerVisible = computed(() => {
+  return props.showHeader && !!String(props.title || '').trim()
+})
+
 // 计算弹窗样式
 const contentStyle = computed(() => {
   const style = {}

+ 21 - 2
pages.json

@@ -59,6 +59,24 @@
 				"navigationBarTitleText": "班主任点名"
 			}
 		},
+		{
+			"path": "pages/xcdm/index",
+			"style": {
+				"navigationBarTitleText": "校车点名"
+			}
+		},
+		{
+			"path": "pages/xcdm/exception",
+			"style": {
+				"navigationBarTitleText": "异常登记"
+			}
+		},
+		{
+			"path": "pages/xcdm/parent",
+			"style": {
+				"navigationBarTitleText": "校车位置"
+			}
+		},
 		{
 			"path": "pages/common/list",
 			"style": {
@@ -140,9 +158,10 @@
 			}
 		},
 		{
-			"path": "pages/device/callend",
+			"path": "pages/device/notice",
 			"style": {
-				"navigationBarTitleText": "通话结束"
+				"navigationStyle": "custom",
+				"disableScroll": true
 			}
 		}
 		

+ 0 - 1248
pages/bjdm/bjdm_bzrDmHomep copy.vue

@@ -1,1248 +0,0 @@
-<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>
-                                <up-th>班级</up-th>
-                                <Td field="bjid">
-                                    <view 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">
-                <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-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="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>
-
-        <!-- 学生列表区域 -->
-        <scroll-view v-else class="student-list-area" :style="studentListStyle" scroll-y @scroll="handleStudentListScroll">
-            <!-- 学生表格 -->
-            <view class="student-table">
-                <!-- 表头 -->
-                <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 < 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, onMounted, onUnmounted, 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 { commonApi } from '@/api/common'
-import { kqjlApi } from '@/api/kqjl'
-import { goBack } from '@/utils/navigation'
-import { getDictTranslation } from '@/utils/fieldFormatter'
-import Icon from '@/components/icon/index.vue';
-
-// ==================== 数据定义 ====================
-
-/**
- * 表单数据
- * @property {string} bjid - 班级ID
- * @property {string} rq - 日期
- * @property {string} jc - 节次 (1-上午, 2-下午, 3-晚上)
- */
-const formData = ref({
-    bjid: '',
-    rq: '', // 将在初始化时设置为今天
-    jc: '', // 将根据当前时间自动设置
-})
-
-// 日期相关数据
-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 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 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 = {
-    bjid: {
-        rules: [{ required: true, message: '班级不能为空' }]
-    },
-    rq: {
-        rules: [{ required: true, message: '日期不能为空' }]
-    },
-    jc: {
-        rules: [{ required: true, message: '请选择节次' }]
-    }
-}
-/**
- * 表单引用
- * 用于表单校验和数据提交
- */
-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 loadClassOptions = async () => {
-    bjLoading.value = true
-    try {
-        const result = await commonApi.getDictOptionsByCbName('bj')
-        //  console.log('班级字典数据:', result)
-
-        // 现在后端返回正常的JSON对象格式
-        if (result.data && result.data.result) {
-            const resultData = result.data.result
-            //  console.log('班级result数据:', resultData)
-
-            // 转换为SsSelect需要的格式:{"40211":"A班","40212":"A2班"} -> [{n:"A班",v:"40211"}]
-            bjOption.value = Object.keys(resultData).map(key => ({
-                n: resultData[key], // 显示名称:A班、A2班
-                v: key // 值:40211、40212
-            }))
-
-            // 默认选中第一个
-            if (bjOption.value.length > 0 && !formData.value.bjid) {
-                formData.value.bjid = bjOption.value[0].v
-                //  console.log('默认选中班级:', bjOption.value[0])
-            }
-            // 解析班级名称(只读显示)
-            resolveBjName()
-            // 加载学生数据
-            loadStudentData()
-        } else {
-            console.error('数据格式不正确:', result.data)
-        }
-    } catch (error) {
-        console.error('加载班级选项失败:', error)
-        uni.showToast({
-            title: '加载班级失败',
-            icon: 'error'
-        })
-    } finally {
-        bjLoading.value = false
-    }
-}
-
-/**
- * 初始化表单默认值
- */
-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 currentPeriod = ''
-    let jcValue = '1'
-
-    if (currentHour >= 0 && currentHour < 12) {
-        // 0-12点:上午
-        currentPeriod = '上午'
-        jcValue = '1'
-    } else if (currentHour >= 12 && currentHour < 18) {
-        // 12-18点:下午
-        currentPeriod = '下午'
-        jcValue = '2'
-    } else {
-        // 18-24点:晚上
-        currentPeriod = '晚上'
-        jcValue = '3'
-    }
-
-    formData.value.jc = jcValue
-    //  console.log('初始化表单默认值:', formData.value, '当前时间:', currentHour + '点', '时间段:', currentPeriod)
-}
-
-/**
- * 更新星期几显示文本
- */
-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, option) => {
-
-    // 重新加载学生数据
-    loadStudentData()
-}
-
-/**
- * 加载学生数据
- * 根据当前选择的班级、日期、节次加载学生列表
- * TODO: 替换为真实的接口调用
- */
-const loadStudentData = async () => {
-    try {
-        
-        const res = await kqjlApi.mp_bzrdmHomep_load({ bjid: formData.value.bjid })
-        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
-        }
-        // 如果接口返回了 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} 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 getKssj = () => {
-    let kssj = ''
-    if (formData.value.jc === '1') {
-        kssj = formData.value.rq + ' 06:00:00'
-    } else if (formData.value.jc === '2') {
-        kssj = formData.value.rq + ' 13:40:00'
-    } else if (formData.value.jc === '3') {
-        kssj = formData.value.rq + ' 18:30:00'
-    }
-    return kssj
-}
-
-const getJssj = () => {
-    let jssj = ''
-    if (formData.value.jc === '1') {
-        jssj = formData.value.rq + ' 12:00:00'
-    } else if (formData.value.jc === '2') {
-        jssj = formData.value.rq + ' 16:00:00'
-    } else if (formData.value.jc === '3') {
-        jssj = formData.value.rq + ' 21:00:00'
-    }
-    return jssj
-}
-
-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: getKssj(),
-            jjssj: getJssj(),
-            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()
-        }
-
-
-
-    } 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;
-}
-
-.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>

+ 0 - 35
pages/bjdm/bjdm_bzrDmHomep.vue

@@ -977,42 +977,7 @@ const handleSave = async () => {
     }
 }
 
-// ==================== 页面初始化 ====================
 
-/**
- * 页面挂载时的初始化
- */
-// 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 调用) ==============

+ 0 - 237
pages/device/callend.vue

@@ -1,237 +0,0 @@
-<template>
-    <view class="call-end-container">
-        <!-- 有通话信息:显示详细信息 -->
-        <view class="call-info-card" v-if="hasCallInfo">
-            <image class="avatar" :src="contactAvatar" mode="aspectFill"></image>
-            <view class="call-info">
-                <text class="title">{{ status }}</text>
-                <text class="contact-name">{{ contactName }}</text>
-                <view class="duration-info">
-                    <view class="duration-info" v-if="status === '通话已结束' && duration > 0">
-                        <text>通话时长: {{ formatDuration(duration) }}</text>
-                    </view>
-                </view>
-            </view>
-        </view>
-
-        <!-- 没有通话信息:显示简单提示 -->
-        <view class="simple-info" v-else>
-            <text class="simple-title">通话结束</text>
-        </view>
-
-        <!-- 根据是否有通话信息显示不同按钮 -->
-        <button class="return-btn" v-if="hasCallInfo" @click="goToHomePage">返回首页</button>
-        <button class="close-btn" v-else @click="killBackgroundApp">关闭</button>
-    </view>
-</template>
-
-<script>
-export default {
-    data() {
-        return {
-            hasCallInfo: false, // 是否有通话信息
-            contactName: '未知联系人',
-            contactAvatar: '/static/logo.png',
-            duration: 0, // 通话时长(秒)
-            status: '通话已结束',
-            callType: 'voice' // 'voice' 或 'video'
-        }
-    },
-    onLoad(options) {
-        // 从路由参数中获取通话信息
-        if (options && options.name) {
-            console.log('callend options', options)
-            this.hasCallInfo = true
-            this.contactName = decodeURIComponent(options.name) || '未知联系人';
-            this.contactAvatar = decodeURIComponent(options.avatar) || '/static/logo.png';
-            this.duration = parseInt(options.duration || 0);
-            this.status = decodeURIComponent(options.status || '通话已结束');
-        } else {
-            console.log('callend 无参数,显示简单模式')
-            this.hasCallInfo = false
-        }
-        console.log('callend 模式:', this.hasCallInfo ? '详细信息' : '简单模式')
-    },
-    methods: {
-        // 格式化通话时长
-        formatDuration(seconds) {
-            if (!seconds || seconds <= 0) return '00:00'
-
-            const minutes = Math.floor(seconds / 60)
-            const remainingSeconds = seconds % 60
-
-            const formattedMinutes = String(minutes).padStart(2, '0')
-            const formattedSeconds = String(remainingSeconds).padStart(2, '0')
-
-            return `${formattedMinutes}:${formattedSeconds}`
-        },
-
-        // 返回首页
-        goToHomePage() {
-            uni.reLaunch({
-                url: '/pages/device/index'
-            })
-        },
-
-        // 退出小程序
-        async killBackgroundApp() {
-            try {
-                // 🧹 清理登录信息
-                uni.removeStorageSync('userInfo')
-                uni.removeStorageSync('JSESSIONID')
-
-                // 🧹 清理通话相关缓存
-                uni.removeStorageSync('currcall')
-                uni.removeStorageSync('lastCallInfo')
-
-                // 判断是否在真机环境
-                if (typeof wmpf !== 'undefined') {
-                    await wmpf.Channel.invoke({
-                        command: 'killBackgroundApp',
-                        data: {},
-                        success: (res) => {
-                            uni.showToast({
-                                title: '已退出',
-                                icon: 'success'
-                            });
-                        }
-                    });
-                } else {
-                    // 开发环境也要清理缓存
-                    uni.showToast({
-                        title: '已退出(开发环境)',
-                        icon: 'success'
-                    });
-                }
-            } catch (error) {
-                console.error('❌ 退出失败:', error);
-                uni.showToast({
-                    title: '退出失败',
-                    icon: 'error'
-                });
-            }
-        }
-    }
-}
-</script>
-
-<style>
-.call-end-container {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    padding: 40rpx;
-    min-height: 100vh;
-    background-color: #f5f5f5;
-}
-
-.call-info-card {
-    width: 90%;
-    background-color: #fff;
-    border-radius: 16rpx;
-    padding: 40rpx;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
-    margin-bottom: 60rpx;
-}
-
-.avatar {
-    width: 160rpx;
-    height: 160rpx;
-    border-radius: 50%;
-    margin-bottom: 30rpx;
-    border: 4rpx solid #f0f0f0;
-}
-
-.call-info {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    width: 100%;
-}
-
-.title {
-    font-size: 36rpx;
-    font-weight: bold;
-    color: #333;
-    margin-bottom: 20rpx;
-}
-
-.contact-name {
-    font-size: 48rpx;
-    color: #000;
-    margin-bottom: 30rpx;
-    font-weight: 500;
-}
-
-.duration-info {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 32rpx;
-    color: #666;
-    margin-top: 20rpx;
-}
-
-.icon {
-    width: 32rpx;
-    height: 32rpx;
-    margin-right: 10rpx;
-}
-
-.simple-info {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    padding: 80rpx 40rpx;
-}
-
-.simple-title {
-    font-size: 48rpx;
-    font-weight: bold;
-    color: #333;
-}
-
-.return-btn {
-    background-color: #07c160;
-    color: #fff;
-    font-size: 36rpx;
-    padding: 20rpx 0;
-    width: 90%;
-    border-radius: 12rpx;
-    margin-top: 60rpx;
-}
-
-.close-btn {
-    background-color: #ff4d4f;
-    color: #fff;
-    font-size: 36rpx;
-    padding: 20rpx 0;
-    width: 90%;
-    border-radius: 12rpx;
-    margin-top: 60rpx;
-}
-
-/* 横屏和竖屏模式的响应式样式 */
-@media screen and (orientation: landscape) {
-    .call-info-card {
-        flex-direction: row;
-        align-items: center;
-        text-align: left;
-        padding: 30rpx 40rpx;
-    }
-
-    .avatar {
-        margin-right: 40rpx;
-        margin-bottom: 0;
-    }
-
-    .call-info {
-        align-items: flex-start;
-    }
-}
-</style>

+ 0 - 451
pages/device/device.js

@@ -1,451 +0,0 @@
-const log = require("./log");
-const util = require('./util');
-const crypt = require('./WXBizMsgCrypt');
-const wmpfVoip = requirePlugin('wmpf-voip').default
- 
-// 判断当前是否为设备端拉起小程序
-const isWmpf = (typeof wmpf !== 'undefined');
- 
-// 指定接听方使用的小程序版本。formal/正式版(默认);trial/体验版;developer/开发版
-const miniprogramState = (() => {
-	const accountInfo = wx.getAccountInfoSync();
-	if(accountInfo && accountInfo.miniProgram){
-		const platform = { develop: 'developer', trial: 'trial', release: 'formal' }
-		return platform[accountInfo.miniProgram.envVersion]
-	}
-})()
-console.log('miniprogramState='+miniprogramState)
- 
-// 「设备接入」从微信公众平台获取的 model_id
-const modelId = 'DSAF58AS5F3SA2FD2SA33DS55';
- 
-// 设备名称,用于授权时显示给用户
-const deviceName = '好宝宝';
- 
-// 通话监听
-const onVoipEvent = () => {
-	wmpfVoip.onVoipEvent((event) => {
-		const eventName = event.eventName;
-		// 定义挂断事件
-		const filterEvent = ['hangUpVoip', 'cancelVoip', 'timeout', 'rejectVoip'];
-		// 挂断动作向设备端发送消息通知
-		if(eventName == 'endVoip' || filterEvent.indexOf(eventName) > -1){
-			// 每次通话只能推送一次挂断信息
-			if(event.groupId && wx.getStorageSync('currcall') != event.groupId){
-				sendMsgDevice('EndVoip');
-				wx.setStorageSync('currcall', event.groupId)
-				log.info('通知设备关闭小程序')
-			}
-		}
-		// 通话记录参数
-		const callData = {
-			groupId: event.groupId,
-			params: {
-				eventName: eventName, data: event.data
-			},
-		}
-		// 挂断上报通话记录
-		filterEvent.indexOf(eventName) > -1 && updateCallRecord(callData)
-		// 为避免上面几种情况执行异常,在结束通话时再次上报,延迟执行是为了避免并发
-		if(eventName == 'endVoip'){
-			setTimeout(() => {
-				updateCallRecord(callData);
-			}, 1500)
-		}
-		// 非通话中,打印调试
-		(eventName != 'calling') && log.info(`onVoipEvent`, event);
-		// 设备打微信,微信端插件通话页面 onShow
-		if(eventName == 'callPageOnShow' && !isWmpf){
-			setUIConfig(1)
-		}
-	})
-}
- 
-// 设置通话结束跳转地址
-const setVoipEndPagePath = (recordId) => {
-	if(!isWmpf){
-		wmpfVoip.setVoipEndPagePath({
-			url: '/pages/call/index',
-			key: 'Call',
-		})
-	}
-}
- 
-/**
- * 小程序传递消息给设备
- * @param {*} command 
- */
-const sendMsgDevice = (command) => {
-	if(isWmpf){
-		wmpf.Channel.invoke({
-			command: command,
-			success: function(res) {
-				wx.setStorageSync(command+'_invoke', 1)
-				setTimeout(() => {
-					wx.removeStorageSync(command+'_invoke');
-				}, 1000)
-				log.info('wmpf.Channel.invoke success:', res.data)
-			},
-			fail: function(res){
-				log.error('wmpf.Channel.invoke fail:', res);
-			}
-		})
-	}
-}
- 
-/**
- * 微信打设备
- * @param {拨打方用户 openId} openid 
- * @param {拨打对方用户信息 name:拨打方名字,仅显示用;roomType:voice音频房间,video音视频房间;voipToken:从设备获取的 pushToken} contact 
- */
-const wechatCallDevice = (contact) => {
-	wx.showLoading({title: '呼叫中',mask: true}), setTimeout(() => {wx.hideLoading()}, 3000)
-	const initByCaller = async (config, data) => {
-		log.info('====>通话请求参数', data, config)
-		setUIConfig(2);
-		const { groupId, isSuccess, errCode, errMsg } = await wmpfVoip.initByCaller(data)
-		wx.hideLoading();
-		if (isSuccess) {
-			requestCallVoip(groupId, data.roomType, 0, config);
-			const callPagePlugin = 'plugin-private://wxf830863afde621eb/pages/call-page-plugin/call-page-plugin'
-			wx.redirectTo({
-				url: `${callPagePlugin}?isCaller=1&roomType=${data.roomType}&groupId=${groupId}`,
-			})
-		} else {
-			log.error('拨打请求失败:errCode='+errCode+';errMsg='+errMsg, config, data)
-			if(errCode == 14){
-				return wx.showToast({title: '手机微信拨打硬件设备模式,voipToken 错误',icon: 'error'})
-			}
-			wx.showToast({title: '拨打失败',icon: 'error'})
-		}
-	}
-	const init = () => {
-		checkDeviceAuth(contact.deviceId, ()=>{
-			getCallConfigParams(contact.id, contact.relId, (config) => {
-				initByCaller(config, {
-					caller: {
-						id: config.openid, name: config.name
-					},
-					listener: {
-						id: config.deviceId
-					},
-					roomType: contact.roomtype,
-					voipToken: config.pushToken,
-					businessType: 2,
-					miniprogramState: miniprogramState,
-				})
-			})
-		})
-	}
-	// 通话结束延长1秒才能再次拉起通话,0.2秒检查一次
-	const checkCallEnd = () => {
-		if(wx.getStorageSync('EndVoip_invoke')){
-			setTimeout(() => {
-				checkCallEnd();
-			}, 200)
-		}else{
-			init()
-		}
-	}
-	checkCallEnd();
-}
- 
-/**
- * 设备拨打微信
- */
-const deviceCallWechat = () => {
-	const { query } = wmpfVoip.getPluginEnterOptions()
-	log.info('====>设备拨打微信传参:', query)
-	setUIConfig(query.isCaller === '1' ? 3 : 4);
-	if(query.isPreLaunch == 'false' && query.isCaller === '1' && query.contactId){
-		var contactId = query.contactId;
-		var relId = query.deviceId;
-		if(query.isRecord == '1'){
-			contactId = -2;
-			relId = query.contactId;
-		}
-		wx.setStorageSync('token', query.token);
-		getCallConfigParams(contactId, relId, (config) => {
-			wmpfVoip.initByCaller({
-				caller: {
-					id: query.deviceId
-				},
-				listener: {
-					id: config.openid,name: config.name
-				},
-				roomType: query.roomType,
-				voipToken: query.voipToken,
-				businessType: 1,
-				miniprogramState: miniprogramState,
-			}).then((res)=>{
-				log.info('===========>设备打微信initByCaller success', res);
-				res.isSuccess && requestCallVoip(res.groupId, query.roomType, 1, config);
-				if(!res.isSuccess){
-					if(res.errCode == 9){
-						wx.showToast({title: '未授权设备无法使用通话功能',icon: 'error'})
-					}
-					if(res.errCode == 13){
-						// 传递参数给设备处理
-						sendMsgDevice('VoipTokenErr13');
-					}
-					sendMsgDevice('errorEnd');
-				}
-			}).catch((e) => {
-				sendMsgDevice('errorEnd');
-				log.error('==========>设备打微信initByCaller fail', e);
-			})
-		}, () => {
-			sendMsgDevice('errorEnd');
-		})
-	}
-}
- 
-/**
- * 获取通话配置参数
- * @param {通讯录id} contactId 
- */
-const getCallConfigParams = (contactId, relId, callback, errback) => {
-	util.rqt({
-		url: '/eeop/wechatcall/getCallConfig',
-		data: {
-			id: contactId,appid: crypt.appid,relId: relId
-		},
-		callBack(res){
-			const encryptOpenid = res.data.encryptOpenid;
-			if(!encryptOpenid){
-				errback && errback();
-				log.error('获取配置参数openid失败', res.data);
-				return;
-			}
-			res.data.openid = crypt.decrypt(encryptOpenid.TimeStamp, encryptOpenid.Nonce, encryptOpenid.Encrypt, encryptOpenid.MsgSignature);
-			callback && callback(res.data);
-		},
-		errCallBack(res){
-			errback && errback();
-			log.error('请求后台配置出现异常', res);
-		}
-	})
-}
- 
-/**
- * 拨打电话
- */
-const requestCallVoip = (groupId, roomType, callWay, config) => {
-	updateCallRecord({
-		groupId: groupId,
-		userId: config.userId,
-		deviceId: config.deviceId,
-		callType: (roomType == 'video' ? 1 : 0),
-		callWay: callWay,
-		params: {
-			eventName: 'startVoip'
-		}
-	})
-}
- 
-const updateCallRecord = (data) => {
-	util.rqt({
-		url: '/eeop/wechatcall/updateCallRecord',
-		method: 'POST',
-		data: data,
-		callBack(res){
-			log.info('上报通话返回结果', res, data);
-		},
-		errCallBack(res){
-			log.error('上报通话数据返回异常', res, data)
-		}
-	})
-}
- 
-/**
- * 添加联系人
- * @param {*} options 
- * @param {*} callback 
- */
-const addContact = (options, callback) => {
-	if(!options.deviceId || !options.code){
-		return wx.showToast({title: '扫描失败',icon: 'none'})
-	}
-	checkDeviceAuth(options.deviceId, () => {
-		util.rqt({
-			url: '/eeop/wechatcall/bind',
-			data: options,
-			callBack(res){
-				open('添加成功', true);
-			},
-			errCallBack(res){
-				if(res.code == 3010){
-					return open('二维码已过期', false);
-				}
-				open(res.msg, false);
-			}
-		})
-		const open = (content, status) => {
-			wx.showToast({
-				title: content, icon: 'none',
-				success(res){
-					setTimeout(() => {
-						callback && callback(status);
-					}, 1500)
-				}
-			})
-		}
-	})
-}
- 
-/**
- * 更新token
- */
-const updatePushToken = () => {
-	// 根据设备deviceId更新token信息到后台服务器
-	const updatePushToken = (deviceId, pushToken) => {
-		util.rqt({
-			url: '/eeop/wechatcall/pushToken',
-			data: {
-				deviceId: deviceId,pushToken: pushToken
-			},
-			callBack(result){
-				log.info('=====>设备端拉起小程序时向后台请求更新token返回结果callBack:deviceId='+deviceId+';pushToken='+pushToken, result);
-			},
-			errCallBack(result){
-				log.info('=====>设备端拉起小程序时向后台请求更新token返回结果errCallBack:deviceId='+deviceId+';pushToken='+pushToken, result);
-			}
-		});
-	}
-	// 获取pushToken
-	const getWmpfPushToken = (deviceId) => {
-		log.info('=====>获取pushToken', deviceId);
-		wmpf.getWmpfPushToken({
-			success(res){
-				log.info('=====>wmpf.getWmpfPushToken success', res);
-				const pushToken = res.token;
-				if(pushToken && pushToken != 'undefined'){
-					updatePushToken(deviceId, pushToken);
-				}
-			},
-			fail(res){
-				log.error('=====>wmpf.getWmpfPushToken fail', res);
-			}
-		})
-	}
-	// 获取设备deviceId
-	const { query } = wmpfVoip.getPluginEnterOptions()
-	log.info('设备拉起小程序获取参数:', query);
-	query.token && wx.setStorageSync('token', query.token);
-	query.deviceId && getWmpfPushToken(query.deviceId);
-}
- 
-/**
- * 检测是否授权
- * @param {用户信息} contact 
- */
-const checkDeviceAuth = (deviceId, callback) => {
-	const requestDeviceVoIP = (snTicket) => {
-		wx.hideLoading();
-		wx.requestDeviceVoIP({
-			sn: deviceId, // 向用户发起通话的设备 sn(需要与设备注册时一致)
-			snTicket: snTicket, // 获取的 snTicket
-			modelId: modelId, // 「设备接入」从微信公众平台获取的 model_id
-			deviceName: deviceName, // 设备名称,用于授权时显示给用户
-			success(res) {
-				callback && callback();
-				log.info(`requestDeviceVoIP success:`, res)
-			},
-			fail(err) {
-				if(err.errCode == 10021){
-					wx.showModal({
-						title: '是否要打开设置页面',
-						content: '需要获取您的音视频通话授权信息,请到小程序的设置中打开授权',
-						success(res) {
-							res.confirm && wx.openSetting()
-						}
-					})
-				}
-				log.error(`requestDeviceVoIP fail:`, err)
-			},
-		})
-	}
-	const getSnTicket = () => {
-		util.rqt({
-			url: '/eeop/wechatcall/setting/getSnTicket',
-			data: {
-				deviceId: deviceId
-			},
-			callBack(res){
-				requestDeviceVoIP(res.data.snTicket)
-			},
-			errCallBack(res){
-				log.error(`getSnTicket fail:`, err)
-			}
-		})
-	}
-	util.checkWechatVersion('2.30.3') && wx.getDeviceVoIPList({
-		success(res) {
-			log.info('当前用户授权的设备:', res.list)
-			const isAuth = res.list.some(element => {
-				return element.sn == deviceId && element.status == 1;
-			})
-			if(isAuth){
-				callback && callback()
-			}else{
-				getSnTicket();
-			}
-		}
-	})
-}
- 
-/**
- * 自定义UI相关配置
- * @param {拨打场景: 1设备打微信,微信端接听,微信端设置;2微信打设备,微信端设置;3设备打微信,设备端设置;4微信打设备,设备端接听,设备端设置}  scene
- */
-const setUIConfig = (scene) => {
-	return;
-	// 显示设备端
-	var UI_1 = {
-		cameraRotation: 0,  // caller的视频画面旋转角度,有效值为 0, 90, 180, 270。默认 0
-		aspectRatio: 3/4, // 纵横比,caller的视频画面会进行适配比例,有效值 数字。默认 4/3
-		horMirror: false, // 横向镜像,boolean 值,默认 false
-		vertMirror: false, // 竖直镜像,同上
-		enableToggleCamera: false, // 是否支持切换摄像头,false 则不显示「摄像头开关」按钮。默认false 【该配置项在wmpf无效,wmpf默认开摄像头,且不显示开关按钮】
-	}
-	// 显示微信端
-	var UI_2 = {
-		cameraRotation: 0,
-		aspectRatio: 4/3,
-		horMirror: false,
-		vertMirror: false,
-		enableToggleCamera: false,
-	}
-	if(scene == 1){
-		wmpfVoip.setUIConfig({
-			btnText: null,
-			callerUI: UI_1,
-			listenerUI: UI_2
-		})
-	}
-	if(scene == 2){
-		wmpfVoip.setUIConfig({
-			btnText: null,
-			callerUI: UI_1,
-			listenerUI: UI_2
-		})
-	}
-	if(scene == 3){
-		wmpfVoip.setUIConfig({
-			btnText: null,
-			callerUI: UI_1,
-			listenerUI: UI_2
-		})
-	}
-	if(scene == 4){
-		wmpfVoip.setUIConfig({
-			btnText: null,
-			callerUI: UI_2,
-			listenerUI: UI_1
-		})
-	}
-}
- 
-module.exports = {
-	isWmpf, updatePushToken, wechatCallDevice, deviceCallWechat, checkDeviceAuth, onVoipEvent, setVoipEndPagePath, addContact
-}

+ 13 - 21
pages/device/index.vue

@@ -602,7 +602,7 @@ export default {
 			await this.startCall(contact, 'video')
 		},
 
-		// 杀后台方法
+		// 杀后台方法 by xu 2025-12-29
 		async killBackgroundApp() {
 			try {
 				// 🧹 清理登录信息
@@ -613,27 +613,19 @@ export default {
 				uni.removeStorageSync('currcall')
 				uni.removeStorageSync('lastCallInfo')
 
-				// 判断是否在真机环境
-				if (typeof wmpf !== 'undefined') {
-					await wmpf.Channel.invoke({
-						command: 'killBackgroundApp',
-						data: {
-							sn: this.sn
-						},
-						success: (res) => {
-							uni.showToast({
-								title: '已退出',
-								icon: 'success'
-							});
-						}
-					});
-				} else {
-					// 开发环境也要清理缓存
-					uni.showToast({
-						title: '已退出(开发环境)',
-						icon: 'success'
+				// 提示退出成功 by xu 2025-12-29
+				uni.showToast({
+					title: '已退出',
+					icon: 'success',
+					duration: 1500
+				});
+
+				// 延迟跳转到通知页 by xu 2025-12-29
+				setTimeout(() => {
+					uni.redirectTo({
+						url: '/pages/device/notice'
 					});
-				}
+				}, 1500);
 			} catch (error) {
 				console.error('❌ 退出失败:', error);
 				uni.showToast({

+ 374 - 0
pages/device/notice.vue

@@ -0,0 +1,374 @@
+<template>
+	<view class="notice-page" :class="{ 'bg-gray': hasNotify }">
+		<!-- 默认展示背景图 -->
+		<image v-if="!hasNotify" class="bg" src="/static/images/deviceunlogin.png" mode="aspectFit" />
+
+		<!-- Mock:后续接 WebSocket 有消息时展示 -->
+		<!-- 动态布局优化 by xu 2025-12-29 -->
+		<view v-else class="notify-container">
+			<view class="notify-card"
+				:class="{ 'has-message': item.hasMessage }"
+				v-for="(item, index) in layoutInfo?.positions"
+				:key="item.id + '_' + index"
+				:style="{
+					left: item.left + 'px',
+					top: item.top + 'px',
+					width: item.width + 'px',
+					height: item.height + 'px',
+					gap: item.gap + 'px'
+				}">
+				<image class="avatar" :src="item.avatar" mode="aspectFill"
+					:style="{ width: item.avatarSize + 'px', height: item.avatarSize + 'px' }" />
+				<view class="name" :style="{ fontSize: item.fontSize + 'px' }">{{ item.name }}</view>
+			</view>
+		</view>
+
+		<view class="footer">
+			<view class="footer-text">
+				<view class="triangles">
+					<view class="triangle" />
+					<view class="triangle" />
+					<view class="triangle" />
+				</view>
+				请刷卡登录查看留言
+			</view>
+		</view>
+
+		<!-- 测试按钮 by xu 2025-12-29 -->
+		<view class="test-buttons">
+			<view class="test-btn" @click="removeUser">-</view>
+			<view class="test-count">{{ notifyUsers.length }}</view>
+			<view class="test-btn" @click="addUser">+</view>
+			<view class="test-btn reset-btn" @click="resetUsers">重置</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+
+const notifyUsers = ref([
+	{
+		id: '1',
+		name: '张三',
+		avatar: '/static/logo.png',
+		hasMessage: true,
+	},
+	{
+		id: '1',
+		name: '张三',
+		avatar: '/static/logo.png',
+		hasMessage: true,
+	},
+	{
+		id: '1',
+		name: '张三',
+		avatar: '/static/logo.png',
+		hasMessage: true,
+	},
+	
+])
+
+const hasNotify = computed(() => notifyUsers.value.length > 0)
+
+// 屏幕尺寸(px)
+// 原始需求:竖屏 800×1280,屏幕比例 5:8 (宽:高) = 1.6
+const SCREEN_WIDTH = 800
+const SCREEN_HEIGHT = 1280
+const FOOTER_HEIGHT = 50 * (SCREEN_WIDTH / 750) // 50rpx 转 px by xu 2025-12-29
+const HORIZONTAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 左右各10rpx by xu 2025-12-29
+const VERTICAL_PADDING = 10 * (SCREEN_WIDTH / 750) * 2 // 上下各10rpx by xu 2025-12-29
+const AVAILABLE_WIDTH = SCREEN_WIDTH - HORIZONTAL_PADDING // 减去水平边距
+const AVAILABLE_HEIGHT = (SCREEN_HEIGHT - FOOTER_HEIGHT) - VERTICAL_PADDING // 容器高度减去垂直边距 by xu 2025-12-29
+const SCREEN_RATIO = AVAILABLE_HEIGHT / AVAILABLE_WIDTH // 调整后的屏幕比例
+console.log(`[初始化] 屏幕:${SCREEN_WIDTH}×${SCREEN_HEIGHT}, 可用:${AVAILABLE_WIDTH.toFixed(2)}×${AVAILABLE_HEIGHT.toFixed(2)}, 比例:${SCREEN_RATIO.toFixed(3)}`)
+
+// 计算最优行列数 by xu 2025-12-29
+// 原则:优先保证x轴贴满,y轴可以有空隙
+function calculateOptimalGrid(n) {
+	if (n === 1) return { rows: 1, cols: 1 }
+
+	const CARD_RATIO = 49 / 40 // 卡片宽高比
+
+	let bestRows = 1, bestCols = n
+	let bestScore = Infinity
+
+	// 遍历可能的行数
+	for (let rows = 1; rows <= n; rows++) {
+		const cols = Math.ceil(n / rows)
+		const capacity = rows * cols
+		const waste = capacity - n
+		const wasteRatio = waste / n
+
+		// 计算这个组合下的卡片尺寸
+		const maxWidth = AVAILABLE_WIDTH / cols
+		const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO
+		const isWidthLimited = maxWidth < maxHeight // x轴是否贴满
+
+		const gridRatio = rows / cols
+		const ratioDiff = Math.abs(gridRatio - SCREEN_RATIO)
+
+		// 综合评分:
+		// 1. 优先选择x轴贴满的组合(isWidthLimited=true)
+		// 2. 其次考虑浪费比例
+		// 3. 最后考虑屏幕比例匹配
+		const widthPenalty = isWidthLimited ? 0 : 2.0 // x轴不贴满时重罚
+		const score = widthPenalty + wasteRatio * 0.5 + ratioDiff * 0.3
+
+		console.log(`  ${rows}×${cols}: maxW=${maxWidth.toFixed(1)}, maxH=${maxHeight.toFixed(1)}, ${isWidthLimited ? 'x轴贴满' : 'y轴贴满'}, 评分=${score.toFixed(3)}`)
+
+		if (score < bestScore) {
+			bestScore = score
+			bestRows = rows
+			bestCols = cols
+		}
+	}
+
+	console.log(`[行列选择] ${n}人 -> ${bestRows}×${bestCols} (容量:${bestRows * bestCols}, 浪费:${bestRows * bestCols - n}, 评分:${bestScore.toFixed(3)})`)
+	return { rows: bestRows, cols: bestCols }
+}
+
+// 计算布局信息 by xu 2025-12-29
+const layoutInfo = computed(() => {
+	const n = notifyUsers.value.length
+	if (n === 0) return null
+
+	const { rows, cols } = calculateOptimalGrid(n)
+	console.log(`[布局] 人数:${n}, 行列:${rows}×${cols}`)
+
+	// 卡片宽高比 by xu 2025-12-29
+	const CARD_RATIO = 49 / 40 // 宽:高 = 40:49
+
+	// 计算卡片最大尺寸(考虑宽高比)
+	const maxWidth = AVAILABLE_WIDTH / cols // 使用可用宽度 by xu 2025-12-29
+	const maxHeight = AVAILABLE_HEIGHT / rows / CARD_RATIO // 使用可用高度除以比例 by xu 2025-12-29
+	let cardWidth = Math.min(maxWidth, maxHeight)
+	console.log(`[尺寸] maxWidth:${maxWidth.toFixed(2)}, maxHeight:${maxHeight.toFixed(2)}, cardWidth:${cardWidth.toFixed(2)}`)
+
+	// 单张卡片时限制最大宽度 by xu 2025-12-29
+	if (n === 1) {
+		const maxSingleCardWidth = 600 // 单卡最大宽度500px
+		cardWidth = Math.min(cardWidth, maxSingleCardWidth)
+	}
+
+	const cardHeight = cardWidth * CARD_RATIO
+
+	// 计算整体布局的实际宽高
+	const totalWidth = cols * cardWidth
+	const totalHeight = rows * cardHeight
+
+	// 计算起始偏移(居中)
+	const offsetX = (AVAILABLE_WIDTH - totalWidth) / 2 + HORIZONTAL_PADDING / 2 // 使用可用宽度+左边距 by xu 2025-12-29
+	const offsetY = VERTICAL_PADDING / 2 + (AVAILABLE_HEIGHT - totalHeight) / 2 // 上边距+居中偏移 by xu 2025-12-29
+	console.log(`[偏移] offsetX:${offsetX.toFixed(2)}, offsetY:${offsetY.toFixed(2)}`)
+	console.log(`[总尺寸] totalWidth:${totalWidth.toFixed(2)}, totalHeight:${totalHeight.toFixed(2)}`)
+	console.log(`[可用空间] AVAILABLE_WIDTH:${AVAILABLE_WIDTH.toFixed(2)}, AVAILABLE_HEIGHT:${AVAILABLE_HEIGHT.toFixed(2)}`)
+	console.log(`[边距] HORIZONTAL_PADDING:${HORIZONTAL_PADDING.toFixed(2)}, VERTICAL_PADDING:${VERTICAL_PADDING.toFixed(2)}`)
+
+	// 动态计算字体大小、间距和头像尺寸 by xu 2025-12-29
+	// 基准:4×4时卡片宽度约194px,字体30rpx(32px)
+	const baseCardWidth = AVAILABLE_WIDTH / 4 // 4列时的卡片宽度
+	const baseFontSize = 30 * (SCREEN_WIDTH / 750) // 30rpx转px
+	const fontSize = Math.max(12, cardWidth / baseCardWidth * baseFontSize) // 按比例缩放,最小12px
+	const gap = Math.max(6, cardWidth * 0.06) // 间距为卡片宽度的6%,最小6px
+	const avatarSize = cardWidth * 0.55 // 头像尺寸为卡片宽度的55%,保持正方形
+
+	// 计算每个用户的位置
+	const positions = notifyUsers.value.map((user, index) => {
+		const row = Math.floor(index / cols)
+		const col = index % cols
+
+		// 计算当前行的元素数量 by xu 2025-12-29
+		const itemsInCurrentRow = Math.min(cols, n - row * cols)
+		// 如果当前行元素少于列数,计算居中偏移
+		const rowCenterOffset = itemsInCurrentRow < cols
+			? (cols - itemsInCurrentRow) * cardWidth / 2
+			: 0
+
+		return {
+			...user,
+			left: offsetX + col * cardWidth + rowCenterOffset,
+			top: offsetY + row * cardHeight,
+			width: cardWidth,
+			height: cardHeight,
+			fontSize,
+			gap,
+			avatarSize
+		}
+	})
+
+	return { positions, cardWidth, cardHeight, fontSize, gap, avatarSize }
+})
+
+// 测试功能:增加用户 by xu 2025-12-29
+function addUser() {
+	notifyUsers.value.push({
+		id: String(notifyUsers.value.length + 1),
+		name: '张三',
+		avatar: '/static/logo.png',
+		hasMessage: true,
+	})
+}
+
+// 测试功能:减少用户 by xu 2025-12-29
+function removeUser() {
+	if (notifyUsers.value.length > 0) {
+		notifyUsers.value.pop()
+	}
+}
+
+// 测试功能:重置用户 by xu 2025-12-29
+function resetUsers() {
+	notifyUsers.value = [
+		{
+			id: '1',
+			name: '张三',
+			avatar: '/static/logo.png',
+			hasMessage: true,
+		},
+		{
+			id: '2',
+			name: '张三',
+			avatar: '/static/logo.png',
+			hasMessage: true,
+		},
+		{
+			id: '3',
+			name: '张三',
+			avatar: '/static/logo.png',
+			hasMessage: true,
+		},
+	]
+}
+</script>
+
+<style>
+.notice-page {
+	width: 100vw;
+	height: 100vh;
+	background: #000;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	overflow: hidden;
+	padding-bottom: 50rpx;
+	box-sizing: border-box;
+}
+
+.bg-gray {
+	background: #b2b2b2;
+}
+
+.bg {
+	width: 100%;
+	height: 100%;
+}
+
+.notify-container {
+	width: 100%;
+	height: 100%;
+	position: relative;
+}
+
+.notify-card {
+	position: absolute;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	border: 1px solid #888;
+}
+
+.avatar {
+	border-radius: 50%;
+	border: 2px solid #fff;
+	box-sizing: border-box;
+	background: #f2f2f2;
+}
+
+.name {
+	color: #000;
+	font-weight: 600;
+	text-align: center;
+	word-break: break-all;
+}
+
+.footer {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	width: 100%;
+	height: 50rpx;
+	background: #b2b2b2;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.footer-text {
+	color: #000;
+	font-size: 26rpx;
+	line-height: 1;
+	display: inline-flex;
+	align-items: center;
+}
+
+.triangles {
+	display: inline-flex;
+	align-items: center;
+	gap: 6rpx;
+	margin-right: 12rpx;
+}
+
+.triangle {
+	width: 0;
+	height: 0;
+	border-top: 10rpx solid transparent;
+	border-bottom: 10rpx solid transparent;
+	border-left: 14rpx solid #000;
+}
+
+.test-buttons {
+	position: fixed;
+	right: 20rpx;
+	bottom: 100rpx;
+	display: flex;
+	align-items: center;
+	gap: 20rpx;
+	background: rgba(0, 0, 0, 0.7);
+	padding: 20rpx;
+	border-radius: 10rpx;
+	z-index: 9999;
+}
+
+.test-btn {
+	width: 60rpx;
+	height: 60rpx;
+	background: #fff;
+	color: #000;
+	font-size: 40rpx;
+	font-weight: bold;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 50%;
+	cursor: pointer;
+}
+
+.test-count {
+	color: #fff;
+	font-size: 32rpx;
+	font-weight: bold;
+	min-width: 40rpx;
+	text-align: center;
+}
+
+.reset-btn {
+	width: auto;
+	padding: 0 20rpx;
+	font-size: 24rpx;
+	border-radius: 30rpx;
+}
+</style>

+ 24 - 12
pages/main/index.vue

@@ -7,6 +7,8 @@
                 <view class="page-container">
                     <BjdmStatisticsPage v-if="page.key === 'mp_njdmHomep' && page.activated"
                         v-show="currentIndex === index" :ref="el => setPageRef(el, index)" />
+                    <XcdmPage v-else-if="page.key === 'xcDm' && page.activated" v-show="currentIndex === index"
+                        :ref="el => setPageRef(el, index)" />
                     <BzrdmPage v-else-if="page.key === 'bzrDm' && page.activated" v-show="currentIndex === index"
                         :ref="el => setPageRef(el, index)" />
                     <TodoPage v-else-if="page.key === 'todo' && page.activated" v-show="currentIndex === index"
@@ -29,6 +31,7 @@ import MyPage from '@/pages/my/index.vue'
 import TodoPage from '@/pages/todo/todo_list.vue'
 import BjdmStatisticsPage from '@/pages/statistics/bjdm_statistics.vue'
 import BzrdmPage from '@/pages/bjdm/bjdm_bzrDmHomep.vue'
+import XcdmPage from '@/pages/xcdm/index.vue'
 
 // 当前选中的页面索引
 const currentIndex = ref(0)
@@ -197,7 +200,7 @@ function getSyList() {
 /**
  * 按登录态构建一级页
  * - 未登录:仅“我的”
- * - 已登录且 syList 含 bzrDmHomep:加入“班主任点名”在“我的”前
+ * - 已登录:按 syList 渲染可见首页
  */
 function buildPagesFromAuth() {
     const list = []
@@ -206,6 +209,13 @@ function buildPagesFromAuth() {
 
     // 能力 → 页面配置映射(按需扩展)
     const capabilityMap = {
+        mp_xcdmHomep: {
+            key: 'xcDm',
+            title: '校车点名',
+            icon: 'icon-dianming',
+            path: 'pages/xcdm/index',
+            component: XcdmPage,
+        },
         mp_njdmHomep: {
             key: 'mp_njdmHomep',
             title: '点名统计',
@@ -231,6 +241,11 @@ function buildPagesFromAuth() {
 
     if (loggedIn) {
         const added = new Set()
+        // 仅当 syList 含 mp_xcdmHomep 时才展示“校车点名”
+        if (sy.includes('mp_xcdmHomep')) {
+            list.push({ ...capabilityMap.mp_xcdmHomep, activated: false })
+            added.add(capabilityMap.mp_xcdmHomep.key)
+        }
         sy.forEach(cap => {
             const conf = capabilityMap[cap]
             if (conf && !added.has(conf.key)) {
@@ -264,7 +279,7 @@ onLoad((options) => {
     }
     // if (true) {
 
-    if (typeof wmpf !== 'undefined') {
+    if (typeof wmpf !== 'undefined') { 
         console.log('WMPF环境')
 
         uni.reLaunch({
@@ -275,13 +290,9 @@ onLoad((options) => {
         //  console.log('主容器页面加载', options)
         // 先按登录态构建一级页
         buildPagesFromAuth()
-        // 默认打开第一个页面;若提供 ?tab=<key> 则按 tab 定位,找不到则回退 0
-        currentIndex.value = 0
-        const tabKey = options?.tab
-        if (tabKey) {
-            const foundIndex = pages.value.findIndex(p => p.key === tabKey)
-            currentIndex.value = foundIndex >= 0 ? foundIndex : 0
-        }
+        // 临时:主首页写死默认打开“校车点名”
+        const xcdmIndex = pages.value.findIndex(p => p.key === 'xcDm')
+        currentIndex.value = xcdmIndex >= 0 ? xcdmIndex : 0
         // 激活当前页(首次时会触发 onLoad)
         activateByIndex(currentIndex.value)
 
@@ -289,8 +300,9 @@ onLoad((options) => {
         uni.$on('login', () => {
             const currentKey = pages.value[currentIndex.value]?.key
             buildPagesFromAuth()
-            // 登录后默认定位到第一个;若需要可由 ?tab 决定
-            currentIndex.value = 0
+            // 临时:登录后默认打开“校车点名”
+            const idx = pages.value.findIndex(p => p.key === 'xcDm')
+            currentIndex.value = idx >= 0 ? idx : 0
             activateByIndex(currentIndex.value)
             nextTick(() => {
                 ensureRefReady(currentIndex.value, 'onShow')
@@ -378,4 +390,4 @@ onUnload(() => {
     height: 100%;
     overflow: hidden;
 }
-</style>
+</style>

+ 20 - 4
pages/my/index.vue

@@ -32,13 +32,21 @@
 					<text class="function-name">充值</text>
 				</view>
 
+				<!-- 校车位置(家长) -->
+				<!-- <view class="function-item" @click="goToBusLocation">
+					<view class="function-icon">
+						<Icon name="icon-cheliangyuyue" size="60" color="#e59f40" />
+					</view>
+					<text class="function-name">校车位置</text>
+				</view> -->
+
 				<!-- 呼叫留言 -->
-				<view class="function-item" @click="goToCallCenter">
+				<!-- <view class="function-item" @click="goToCallCenter">
 					<view class="function-icon">
 						<Icon name="icon-dianhua" size="60" color="#ff9f43" />
 					</view>
 					<text class="function-name">呼叫留言</text>
-				</view>
+				</view> -->
 
 				<!-- 留言功能 -->
 				<!-- <view class="function-item" @click="goToMessage">
@@ -79,12 +87,12 @@
 					</view>
 					<text class="function-name">点名</text>
 				</view> -->
-				<view class="function-item" @click="goToXunChaWebview">
+				<!-- <view class="function-item" @click="goToXunChaWebview">
 					<view class="function-icon">
 						<Icon name="icon-xuncha" size="60" color="#475fab" />
 					</view>
 					<text class="function-name">校长巡查</text>
-				</view>
+				</view> -->
 				<!-- <view class="function-item" @click="goXuncha">
 					<view class="function-icon">
 						<Icon name="icon-xuncha" size="60" color="#475fab" />
@@ -247,6 +255,14 @@ const goToRecharge = () => {
 	goTo('/pages/payment/recharge')
 }
 
+const goToBusLocation = () => {
+	if (!checkLogin()) {
+		pleaseLogin()
+		return
+	}
+	goTo('/pages/xcdm/parent')
+}
+
 const goToCallCenter = () => {
 	if (!checkLogin()) {
 		pleaseLogin()

+ 64 - 61
pages/payment/recharge.vue

@@ -6,13 +6,13 @@
 
 			<!-- 服务包列表 -->
 			<view v-if="servicePackages.length > 0" class="package-list">
-				<view
-					v-for="pkg in servicePackages"
-					:key="pkg.grfwbid"
-					class="package-item"
-					:class="{ active: selectedPackage && selectedPackage.grfwbid === pkg.grfwbid }"
-					@click="selectPackage(pkg)"
-				>
+				<view
+					v-for="pkg in servicePackages"
+					:key="pkg.grfwbid"
+					class="package-item"
+					:class="{ active: selectedPackage && String(selectedPackage.grfwbid) === String(pkg.grfwbid) }"
+					@click="selectPackage(pkg)"
+				>
 					<view class="package-header">
 						<view class="package-name">{{ pkg.mc }}</view>
 						<view class="package-info">
@@ -27,15 +27,15 @@
 							v-for="(item, index) in pkg.grfwbmxList"
 							:key="index"
 							class="service-item"
-						>
-							<text class="service-name">{{ item.mc }}</text>
-							<view class="service-details">
-								<text v-if="item.sfmf === 1" class="service-tag free">免费</text>
-								<text v-if="item.sc > 0" class="service-tag">{{ item.sc }}分钟</text>
-								<text v-if="item.cs > 0" class="service-tag">{{ item.cs }}次</text>
-								<text v-if="item.ll > 0" class="service-tag">{{ item.ll }}MB</text>
-							</view>
-						</view>
+						>
+							<text class="service-name">{{ item.mc }}</text>
+							<view class="service-details">
+								<text v-if="item.sfmf == 1" class="service-tag free">免费</text>
+								<text v-if="item.sc > 0" class="service-tag">{{ item.sc }}分钟</text>
+								<text v-if="item.cs > 0" class="service-tag">{{ item.cs }}次</text>
+								<text v-if="item.ll > 0" class="service-tag">{{ item.ll }}MB</text>
+							</view>
+						</view>
 					</view>
 				</view>
 			</view>
@@ -83,29 +83,39 @@
 	</view>
 </template>
 
-<script setup>
-import { ref, computed } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
-import { grfwApi } from '@/api/grfw'
-
-
-// 兼容不同字段命名的支付参数
-const normalizePayParams = (res) => {
-	const data = res?.data.ssData || res || {}
-	const payload = data.prepay || data.payInfo || data
-	if (!payload) {
-		return {}
-	}
-	return {
-		appId: payload.appId,
-		timeStamp: payload.timeStamp || payload.timestamp,
-		nonceStr: payload.nonceStr || payload.noncestr,
-		package: payload.package || payload.packageVal || payload.prepayId,
-		signType: payload.signType || payload.sign_type || 'RSA',
-		paySign: payload.paySign || payload.sign,
-		orderId: data.outTradeNo
-	}
-}
+<script setup>
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { grfwApi } from '@/api/grfw'
+
+
+// 兼容不同字段命名的支付参数
+const normalizePayParams = (res) => {
+	const data = res?.data.ssData || res || {}
+	const payload = data.prepay || data.payInfo || data
+	if (!payload) {
+		return {}
+	}
+
+	const rawPackage =
+		payload.package ||
+		payload.packageVal ||
+		(payload.prepayId ? `prepay_id=${payload.prepayId}` : undefined)
+	const normalizedPackage =
+		typeof rawPackage === 'string' && rawPackage && !rawPackage.startsWith('prepay_id=')
+			? `prepay_id=${rawPackage}`
+			: rawPackage
+
+	return {
+		appId: payload.appId,
+		timeStamp: payload.timeStamp || payload.timestamp,
+		nonceStr: payload.nonceStr || payload.noncestr,
+		package: normalizedPackage,
+		signType: payload.signType || payload.sign_type || 'RSA',
+		paySign: payload.paySign || payload.sign,
+		orderId: data.outTradeNo
+	}
+}
 
 // 个人服务包数据
 const servicePackages = ref([])  // 服务包列表
@@ -237,18 +247,13 @@ const handlePay = async () => {
 	}
 }
 
-// 加载个人服务包列表
-const loadServicePackages = async () => {
-	try {
-		uni.showLoading({
-			title: '加载中...',
-			mask: true
-		})
-
-		const res = await grfwApi.grfw_initGrfwbBuy({})
-		console.log('获取服务包列表返回:', res)
-		console.log('res.data:', res.data)
-		console.log('res.data.ssData:', res.data?.ssData)
+// 加载个人服务包列表
+const loadServicePackages = async () => {
+	try {
+		const res = await grfwApi.grfw_initGrfwbBuy({})
+		console.log('获取服务包列表返回:', res)
+		console.log('res.data:', res.data)
+		console.log('res.data.ssData:', res.data?.ssData)
 
 		// 数据在 res.data.ssData 里
 		if (res && res.data && res.data.ssData && Array.isArray(res.data.ssData)) {
@@ -270,16 +275,14 @@ const loadServicePackages = async () => {
 		} else {
 			console.log('res.data.ssData 不存在或不是数组')
 		}
-	} catch (error) {
-		console.error('加载服务包失败:', error)
-		uni.showToast({
-			title: '加载服务包失败',
-			icon: 'none'
-		})
-	} finally {
-		uni.hideLoading()
-	}
-}
+	} catch (error) {
+		console.error('加载服务包失败:', error)
+		uni.showToast({
+			title: '加载服务包失败',
+			icon: 'none'
+		})
+	}
+}
 
 // 页面加载
 onLoad((options) => {

+ 134 - 0
pages/xcdm/exception.vue

@@ -0,0 +1,134 @@
+<template>
+	<view class="page">
+		<view class="header">
+			<text class="header-title">异常登记{{ xm ? `(${xm})` : '' }}</text>
+		</view>
+
+		<view class="body">
+			<textarea class="textarea" v-model="remark" />
+		</view>
+
+		<SsBottom :buttons="bottomButtons" @button-click="handleBottomAction" />
+	</view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import SsBottom from '@/components/SsBottom/index.vue'
+import { xcdmApi } from '@/api/xcdm'
+
+const ryid = ref('')
+const xm = ref('')
+const site = ref('')
+const xcdmlbm = ref('1')
+const xcid = ref('')
+const jskssj = ref('')
+const jsjssj = ref('')
+const remark = ref('')
+
+onLoad((options) => {
+	const safeDecode = (v) => {
+		try {
+			return decodeURIComponent(v || '')
+		} catch (e) {
+			return v || ''
+		}
+	}
+	ryid.value = safeDecode(options?.zdryid || options?.ryid)
+	xm.value = safeDecode(options?.xm)
+	site.value = safeDecode(options?.site)
+	xcdmlbm.value = safeDecode(options?.xcdmlbm || options?.mode || '1')
+	xcid.value = safeDecode(options?.xcid)
+	jskssj.value = safeDecode(options?.jskssj)
+	jsjssj.value = safeDecode(options?.jsjssj)
+})
+
+const bottomButtons = [
+	{ text: '取消', action: 'cancel' },
+	{ text: '保存并提交', action: 'submit' }
+]
+
+const handleBottomAction = ({ action }) => {
+	if (action === 'cancel') {
+		uni.navigateBack()
+		return
+	}
+	if (action === 'submit') {
+		handleSubmit()
+	}
+}
+
+const handleSubmit = async () => {
+	try {
+		const to = Number(xcdmlbm.value) === 51 ? 61 : 11
+		const payload = {
+			xcid: xcid.value,
+			xcdmlbm: Number(xcdmlbm.value) || 1,
+			jskssj: jskssj.value,
+			jsjssj: jsjssj.value,
+			zdryid: ryid.value,
+			xcjslbm: to,
+			sm: remark.value
+		}
+		const res = await xcdmApi.mp_xcdmHomep_swState(payload)
+		console.log('mp_xcdmHomep_swState (exception):', payload, res)
+		uni.showToast({ title: '提交成功', icon: 'success' })
+		setTimeout(() => {
+			uni.navigateBack()
+		}, 200)
+	} catch (e) {
+		console.warn('mp_xcdmHomep_swState (exception) failed:', e)
+		uni.showToast({ title: '提交失败', icon: 'none' })
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	background: #f2f3f4;
+	box-sizing: border-box;
+	position: relative;
+}
+
+.header {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	height: 104rpx; // 约 52px
+	background: #f2f3f4;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	z-index: 10;
+	border-bottom: 1rpx solid #e8e8e8;
+}
+
+.header-title {
+	font-size: 32rpx;
+	color: #666;
+	line-height: 1;
+}
+
+.body {
+	position: absolute;
+	top: 104rpx; // header
+	left: 0;
+	right: 0;
+	bottom: 100rpx; // bottom buttons
+	background: #fff;
+}
+
+.textarea {
+	width: 100%;
+	height: 100%;
+	font-size: 30rpx;
+	box-sizing: border-box;
+	background: #fff;
+	border: none;
+	border-radius: 0;
+	padding: 24rpx;
+}
+</style>

+ 1665 - 0
pages/xcdm/index.vue

@@ -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>

+ 420 - 0
pages/xcdm/parent.vue

@@ -0,0 +1,420 @@
+<template>
+	<view class="page">
+		<view class="header">
+			<text class="car-no">{{ carNo }}</text>
+		</view>
+		<view class="header-divider"></view>
+
+		<scroll-view class="timeline-scroll" scroll-y :scroll-into-view="scrollIntoViewStopId"
+			:scroll-with-animation="true">
+			<view class="timeline">
+				<view v-for="item in stopList" :key="item.key" class="stop-item" :style="{
+					'--dot-size': item.dotSize + 'rpx',
+					'--line-top': item.lineTop + 'rpx',
+					'--line-bottom': item.lineBottom + 'rpx',
+					'--line-top-color': item.lineTopColor,
+					'--line-bottom-color': item.lineBottomColor
+				}" :id="'stop_' + item.key">
+					<view class="left">
+						<view class="line top" :class="{ hidden: item.isFirst }"></view>
+						<view class="dot" :class="dotClass(item)">
+							<image v-if="item.stage === 'start'" class="dot-icon dot-icon-start" src="/static/images/strat.svg"
+								mode="aspectFit" />
+							<image v-else-if="item.stage === 'end'" class="dot-icon dot-icon-end" src="/static/images/end.svg"
+								mode="aspectFit" />
+						</view>
+						<view class="line bottom" :class="{ hidden: item.isLast }"></view>
+					</view>
+
+					<view class="right">
+						<view class="name-row">
+							<view class="stop-name" :class="nameClass(item)">{{ item.name }}</view>
+						</view>
+						<view v-if="item.tip" class="tip-row">
+							<image class="bus-icon" src="/static/images/bus.svg" mode="aspectFit" />
+							<text class="tip-text" :class="tipClass(item)">{{ item.tip }}</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</scroll-view>
+
+		<!-- 调试:切换车辆站点 -->
+		<view class="debug-fab">
+			<view class="fab-btn" @click="goPrevStop">上一站</view>
+			<view class="fab-btn fab-primary" @click="goNextAction">{{ nextActionText }}</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { computed, ref, nextTick, watch, onMounted } from 'vue'
+
+const carNo = ref('粤A88888')
+const busIndex = ref(0) // mock:车辆当前所在站点索引
+const busState = ref('at') // at | enroute(用于模拟“即将到达/到达”两步)
+const childStopKey = ref('s4') // mock:家长孩子所在站点(用蓝色标记)
+const scrollIntoViewStopId = ref('')
+
+const stops = ref([
+	{ key: 's0', name: '学校' },
+	{ key: 's1', name: '莲花山头' },
+	{ key: 's2', name: '北塘四环公交站' },
+	{ key: 's3', name: '莲花山(山尾)' },
+	{ key: 's4', name: '碧桂园天玺山' },
+	{ key: 's5', name: '锦城花园' },
+	{ key: 's6', name: '华大路口' },
+	{ key: 's7', name: '海丰中专1站' },
+	{ key: 's8', name: '海丰中专2站' },
+	{ key: 's9', name: '结束点' },
+])
+
+const startKey = computed(() => stops.value[0]?.key || '')
+const endKey = computed(() => stops.value[stops.value.length - 1]?.key || '')
+const isBusAtStart = computed(() => busIndex.value === 0 && busState.value === 'at')
+const isBusAtEnd = computed(() => busIndex.value === Math.max(0, stops.value.length - 1) && busState.value === 'at')
+
+const clampBusIndex = (next) => Math.min(Math.max(next, 0), Math.max(0, stops.value.length - 1))
+const goPrevStop = () => {
+	busIndex.value = clampBusIndex(busIndex.value - 1)
+	busState.value = 'at'
+}
+
+const nextActionText = computed(() => {
+	if (busIndex.value >= Math.max(0, stops.value.length - 1)) return '已到终点'
+	return busState.value === 'at' ? '即将到下一站' : '到下一站'
+})
+
+const goNextAction = () => {
+	const last = Math.max(0, stops.value.length - 1)
+	if (busIndex.value >= last) return
+	if (busState.value === 'at') {
+		busState.value = 'enroute'
+		return
+	}
+	busIndex.value = clampBusIndex(busIndex.value + 1)
+	busState.value = 'at'
+}
+
+const activeStopKey = computed(() => {
+	const last = Math.max(0, stops.value.length - 1)
+	if (!stops.value.length) return ''
+	if (busState.value === 'enroute' && busIndex.value < last) return stops.value[busIndex.value + 1]?.key || ''
+	return stops.value[busIndex.value]?.key || ''
+})
+
+const scrollToActiveStop = () => {
+	const key = activeStopKey.value
+	if (!key) return
+	scrollIntoViewStopId.value = ''
+	nextTick(() => {
+		scrollIntoViewStopId.value = `stop_${key}`
+	})
+}
+
+watch([busIndex, busState], () => {
+	scrollToActiveStop()
+}, { immediate: true })
+
+// 页面初次渲染时,scroll-view 可能还未完成布局,做一次轻量重试确保能自动定位到当前/即将站点
+onMounted(() => {
+	const tryScroll = (tries = 0) => {
+		scrollToActiveStop()
+		if (tries >= 6) return
+		setTimeout(() => tryScroll(tries + 1), 60)
+	}
+	tryScroll(0)
+})
+
+const stopList = computed(() => {
+	const lastIndex = Math.max(0, stops.value.length - 1)
+	// 规则:不会同时存在“已到达”和“即将到达”
+	// - at:仅“已到达”(当前站)
+	// - enroute:仅“即将到达”(下一站)
+	const arrivedIndex = busState.value === 'at' ? busIndex.value : -1
+	const arrivingIndex =
+		busState.value === 'enroute' && busIndex.value < lastIndex ? busIndex.value + 1 : -1
+
+	return stops.value.map((s, idx) => ({
+		...s,
+		isFirst: idx === 0,
+		isLast: idx === stops.value.length - 1,
+		isChildStop: s.key === childStopKey.value,
+		stage:
+			idx === 0
+				? 'start'
+				: idx === lastIndex
+					? 'end'
+					: idx < busIndex.value
+						? 'past'
+						: busState.value === 'enroute' && idx === busIndex.value
+							? 'past'
+							: idx === arrivedIndex
+								? 'arrived'
+								: idx === arrivingIndex
+									? 'arriving'
+									: 'future',
+		tip:
+			idx === arrivingIndex && arrivingIndex !== -1
+				? '即将到达(距离340米)'
+				: idx === arrivedIndex && arrivedIndex > 0 && arrivedIndex < lastIndex
+					? '已到达'
+					: '',
+		dotSize:
+			idx === 0 || idx === lastIndex || idx === arrivedIndex || idx === arrivingIndex
+				? 56
+				: 36,
+		lineTop: idx === 0 ? 0 : 8,
+		lineBottom: idx === lastIndex ? 0 : 52,
+		lineTopColor: arrivingIndex === idx ? '#e59f40' : '#c3c7cb',
+		lineBottomColor: busState.value === 'enroute' && idx === busIndex.value ? '#e59f40' : '#c3c7cb',
+	}))
+})
+
+const dotClass = (item) => {
+	if (item.stage === 'start') return isBusAtStart.value ? 'dot-start dot-active' : 'dot-start'
+	if (item.stage === 'end') {
+		if (isBusAtEnd.value) return 'dot-end dot-active'
+		// 即将到达结束点:给结束点加橙色边框
+		if (busState.value === 'enroute' && busIndex.value + 1 === Math.max(0, stops.value.length - 1)) return 'dot-end dot-end-arriving'
+		return 'dot-end'
+	}
+	if (item.stage === 'arriving') return 'dot-arriving'
+	if (item.stage === 'arrived') return 'dot-arrived'
+	// 家长孩子所在站点:默认蓝底灰框(不覆盖“即将到达/已到达/始发/结束”)
+	if (item.isChildStop) return 'dot-child'
+	if (item.stage === 'past') return 'dot-past'
+	return 'dot-future'
+}
+
+const nameClass = (item) => {
+	if (item.stage === 'arriving' || item.stage === 'arrived') return 'name-strong'
+	return 'name-muted'
+}
+
+const tipClass = (item) => {
+	if (!item.tip) return ''
+	return 'tip-current'
+}
+
+</script>
+
+<style lang="scss" scoped>
+.page {
+	height: 100vh;
+	background: #f2f3f4;
+	padding: 0 28rpx 24rpx;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+}
+
+.header {
+	height: 100rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.car-no {
+	font-size: 40rpx; // 20px
+	color: #666;
+	font-weight: 600;
+}
+
+.header-divider {
+	width: 99%;
+	height: 2rpx; // 约 2px
+	background: #e6e6e6;
+	margin: 0 auto;
+}
+
+.timeline-scroll {
+	flex: 1;
+	height: 0;
+}
+
+.timeline {
+	padding-top: 28rpx;
+	width: 90%;
+	margin: 0 auto;
+	padding-bottom: 120rpx; // 给悬浮按钮留空间
+}
+
+.stop-item {
+	display: flex;
+	align-items: flex-start;
+	height: calc(var(--line-top) + var(--dot-size) + var(--line-bottom));
+	overflow: visible;
+}
+
+.left {
+	width: 72rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	flex-shrink: 0;
+}
+
+.line {
+	width: 4rpx;
+	flex: none;
+}
+
+.line.top {
+	height: var(--line-top);
+	background: var(--line-top-color);
+}
+
+.line.bottom {
+	height: var(--line-bottom);
+	background: var(--line-bottom-color);
+}
+
+.line.hidden {
+	background: transparent;
+}
+
+.dot {
+	width: var(--dot-size);
+	height: var(--dot-size);
+	border-radius: 9999rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.dot-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.dot-icon-start {
+	width: 30rpx;
+	height: 25rpx;
+}
+
+.dot-icon-end {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.dot-past {
+	background: #c3c7cb;
+}
+
+.dot-future {
+	background: #ffffff;
+	border: 2rpx solid #c3c7cb;
+}
+
+.dot-start {
+	background: #c3c7cb;
+}
+
+.dot-end {
+	background: #c3c7cb;
+}
+
+.dot-end-arriving {
+	border: 4rpx solid #e59f40;
+}
+
+.dot-active {
+	background: #e59f40;
+}
+
+.dot-arriving {
+	background: #ffffff;
+	border: 4rpx solid #e59f40;
+}
+
+.dot-arrived {
+	background: #e59f40;
+}
+
+.dot-child {
+	background: #357cdf;
+	border: 2rpx solid #c3c7cb;
+}
+
+.right {
+	flex: 1;
+	padding: 0;
+	padding-top: var(--line-top);
+}
+
+.name-row {
+	height: var(--dot-size);
+	display: flex;
+	align-items: center;
+}
+
+.stop-name {
+	font-size: 40rpx;
+	line-height: 1.1;
+}
+
+.name-muted {
+	color: #999;
+	font-weight: 400;
+}
+
+.name-strong {
+	color: #000;
+	font-weight: 700;
+	font-size: 48rpx;
+}
+
+.tip-row {
+	display: flex;
+	align-items: center;
+	gap: 10rpx;
+	margin-top: 6rpx;
+}
+
+.bus-icon {
+	width: 80rpx;
+    height: 40rpx;
+}
+
+.tip-text {
+	font-size: 32rpx; 
+	color: #e59f40;
+	line-height: 1.2;
+}
+
+.debug-fab {
+	position: fixed;
+	right: 24rpx;
+	bottom: 48rpx;
+	display: flex;
+	flex-direction: column;
+	gap: 16rpx;
+	z-index: 999;
+}
+
+.fab-btn {
+	width: 200rpx;
+	height: 72rpx;
+	border-radius: 12rpx;
+	background: #ffffff;
+	border: 2rpx solid #c3c7cb;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28rpx;
+	color: #333;
+	box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.08);
+}
+
+.fab-primary {
+	border-color: #e59f40;
+	color: #e59f40;
+}
+
+.fab-btn:active {
+	opacity: 0.9;
+}
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/images/bus.svg


BIN
static/images/deviceunlogin.png


+ 1 - 0
static/images/end.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769667154938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6609" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M875.054545 148.945455C781.963636 55.854545 651.636364 0 512 0S242.036364 55.854545 148.945455 148.945455 0 367.709091 0 512c0 139.636364 55.854545 269.963636 148.945455 363.054545S372.363636 1024 512 1024c144.290909 0 269.963636-55.854545 363.054545-148.945455S1024 656.290909 1024 512c0-144.290909-55.854545-269.963636-148.945455-363.054545z m-65.163636 660.945454c-37.236364 37.236364-83.781818 69.818182-134.981818 88.436364-51.2 23.272727-107.054545 32.581818-162.909091 32.581818-55.854545 0-111.709091-9.309091-162.909091-32.581818-51.2-23.272727-93.090909-51.2-134.981818-88.436364-37.236364-37.236364-69.818182-83.781818-88.436364-134.981818-23.272727-51.2-32.581818-107.054545-32.581818-162.909091 0-55.854545 9.309091-111.709091 32.581818-162.909091 23.272727-51.2 51.2-93.090909 88.436364-134.981818 37.236364-37.236364 83.781818-69.818182 134.981818-88.436364C400.290909 102.4 456.145455 93.090909 512 93.090909c55.854545 0 111.709091 9.309091 162.909091 32.581818 51.2 23.272727 93.090909 51.2 134.981818 88.436364 37.236364 37.236364 69.818182 83.781818 88.436364 130.327273 23.272727 55.854545 32.581818 111.709091 32.581818 167.563636 0 55.854545-9.309091 111.709091-32.581818 162.909091-18.618182 51.2-51.2 97.745455-88.436364 134.981818z" fill="#ffffff" p-id="6610"></path><path d="M279.272727 698.181818c0 23.272727 23.272727 46.545455 46.545455 46.545455h372.363636c23.272727 0 46.545455-23.272727 46.545455-46.545455V325.818182c0-23.272727-23.272727-46.545455-46.545455-46.545455H325.818182c-23.272727 0-46.545455 23.272727-46.545455 46.545455v372.363636z" fill="#ffffff" p-id="6611"></path></svg>

+ 1 - 0
static/images/strat.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769667148828" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6464" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M238.778182 23.272727l651.636363 395.636364c46.545455 23.272727 69.818182 69.818182 69.818182 93.090909s-23.272727 69.818182-69.818182 93.090909l-651.636363 395.636364c-25.6 14.336-45.242182 23.272727-69.818182 23.272727-46.545455 0-93.090909-46.545455-93.090909-93.090909V93.090909c0-46.545455 21.504-93.090909 93.090909-93.090909 24.576 0 44.218182 8.378182 69.818182 23.272727zM791.272727 512L209.454545 861.090909v-698.181818L791.272727 512z" fill="#ffffff" p-id="6465"></path></svg>

BIN
static/images/warning.gif


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/api/kqjl.js.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map


+ 1 - 1
unpackage/dist/dev/.sourcemap/mp-weixin/common/assets.js.map

@@ -1 +1 @@
-{"version":3,"file":"assets.js","sources":["static/logo.png","static/icon/tingtong.png","static/icon/guangbo.png","static/icon/yinbo.png","static/icon/emjoy.png","static/icon/audio.png","static/icon/video.png","static/icon/message.png","static/icon/logout.png"],"sourcesContent":["export default \"__VITE_ASSET__0d1a51c4__\"","export default \"__VITE_ASSET__74cc0994__\"","export default \"__VITE_ASSET__026192e1__\"","export default \"__VITE_ASSET__25a6ca73__\"","export default \"__VITE_ASSET__b63618f6__\"","export default \"__VITE_ASSET__a2f0ba11__\"","export default \"__VITE_ASSET__9510a760__\"","export default \"__VITE_ASSET__088b05e9__\"","export default \"__VITE_ASSET__a8811044__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;;;;;;;;;;"}
+{"version":3,"file":"assets.js","sources":["static/images/warning.gif","static/images/strat.svg","static/images/end.svg","static/images/bus.svg","static/logo.png","static/icon/tingtong.png","static/icon/guangbo.png","static/icon/yinbo.png","static/icon/emjoy.png","static/icon/audio.png","static/icon/video.png","static/icon/message.png","static/icon/logout.png","static/images/deviceunlogin.png"],"sourcesContent":["export default \"__VITE_ASSET__dba43bae__\"","export default \"__VITE_ASSET__393b1428__\"","export default \"__VITE_ASSET__7a909323__\"","export default \"__VITE_ASSET__257f5630__\"","export default \"__VITE_ASSET__0d1a51c4__\"","export default \"__VITE_ASSET__74cc0994__\"","export default \"__VITE_ASSET__026192e1__\"","export default \"__VITE_ASSET__25a6ca73__\"","export default \"__VITE_ASSET__b63618f6__\"","export default \"__VITE_ASSET__a2f0ba11__\"","export default \"__VITE_ASSET__9510a760__\"","export default \"__VITE_ASSET__088b05e9__\"","export default \"__VITE_ASSET__a8811044__\"","export default \"__VITE_ASSET__104704b5__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;;;;;;;;;;;;;;;"}

+ 4 - 1
unpackage/dist/dev/mp-weixin/app.js

@@ -11,6 +11,9 @@ if (!Math) {
   "./pages/xuncha/mp_excelRcXcdjl_edit.js";
   "./pages/bjdm/bjdm_bzrDm.js";
   "./pages/bjdm/bjdm_bzrDm_list.js";
+  "./pages/xcdm/index.js";
+  "./pages/xcdm/exception.js";
+  "./pages/xcdm/parent.js";
   "./pages/common/list.js";
   "./pages/xfjl/index.js";
   "./pages/payment/recharge.js";
@@ -24,7 +27,7 @@ if (!Math) {
   "./pages/common/webview.js";
   "./pages/common/h5-controller.js";
   "./pages/device/index.js";
-  "./pages/device/callend.js";
+  "./pages/device/notice.js";
 }
 const _sfc_main = {
   onLaunch: function() {

+ 4 - 1
unpackage/dist/dev/mp-weixin/app.json

@@ -8,6 +8,9 @@
     "pages/xuncha/mp_excelRcXcdjl_edit",
     "pages/bjdm/bjdm_bzrDm",
     "pages/bjdm/bjdm_bzrDm_list",
+    "pages/xcdm/index",
+    "pages/xcdm/exception",
+    "pages/xcdm/parent",
     "pages/common/list",
     "pages/xfjl/index",
     "pages/payment/recharge",
@@ -21,7 +24,7 @@
     "pages/common/webview",
     "pages/common/h5-controller",
     "pages/device/index",
-    "pages/device/callend"
+    "pages/device/notice"
   ],
   "window": {
     "navigationBarTextStyle": "black",

+ 17 - 7
unpackage/dist/dev/mp-weixin/common/assets.js

@@ -1,5 +1,9 @@
 "use strict";
-const _imports_0$1 = "/static/logo.png";
+const _imports_0$4 = "/static/images/warning.gif";
+const _imports_0$3 = "/static/images/strat.svg";
+const _imports_1$2 = "/static/images/end.svg";
+const _imports_2$2 = "/static/images/bus.svg";
+const _imports_0$2 = "/static/logo.png";
 const _imports_1$1 = "/static/icon/tingtong.png";
 const _imports_2$1 = "/static/icon/guangbo.png";
 const _imports_3$1 = "/static/icon/yinbo.png";
@@ -7,13 +11,19 @@ const _imports_4 = "/static/icon/emjoy.png";
 const _imports_1 = "/static/icon/audio.png";
 const _imports_2 = "/static/icon/video.png";
 const _imports_3 = "/static/icon/message.png";
-const _imports_0 = "/static/icon/logout.png";
-exports._imports_0 = _imports_0;
+const _imports_0$1 = "/static/icon/logout.png";
+const _imports_0 = "/static/images/deviceunlogin.png";
+exports._imports_0 = _imports_0$3;
 exports._imports_0$1 = _imports_0$1;
-exports._imports_1 = _imports_1;
-exports._imports_1$1 = _imports_1$1;
-exports._imports_2 = _imports_2;
-exports._imports_2$1 = _imports_2$1;
+exports._imports_0$2 = _imports_0;
+exports._imports_0$3 = _imports_0$4;
+exports._imports_0$4 = _imports_0$2;
+exports._imports_1 = _imports_1$2;
+exports._imports_1$1 = _imports_1;
+exports._imports_1$2 = _imports_1$1;
+exports._imports_2 = _imports_2$2;
+exports._imports_2$1 = _imports_2;
+exports._imports_2$2 = _imports_2$1;
 exports._imports_3 = _imports_3;
 exports._imports_3$1 = _imports_3$1;
 exports._imports_4 = _imports_4;

+ 2 - 2
unpackage/dist/dev/mp-weixin/common/vendor.js

@@ -7189,9 +7189,9 @@ function isConsoleWritable() {
   return isWritable;
 }
 function initRuntimeSocketService() {
-  const hosts = "127.0.0.1,172.20.10.2,28.0.0.1";
+  const hosts = "127.0.0.1,192.168.3.198";
   const port = "8090";
-  const id = "mp-weixin_yrAD2Z";
+  const id = "mp-weixin_OAFe8r";
   const lazy = typeof swan !== "undefined";
   let restoreError = lazy ? () => {
   } : initOnError();

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/my/index.wxml


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio