index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <view class="main-container">
  3. <!-- 主内容区域 - 使用 swiper 实现滑动切换 -->
  4. <swiper class="main-swiper" :current="currentIndex" @change="onSwiperChange" :circular="false" :duration="300">
  5. <!-- 动态渲染页面 -->
  6. <swiper-item v-for="(page, index) in pages" :key="index">
  7. <view class="page-container">
  8. <BjdmStatisticsPage v-if="page.key === 'mp_njdmHomep' && page.activated"
  9. v-show="currentIndex === index" :ref="el => setPageRef(el, index)" />
  10. <XcdmPage v-else-if="page.key === 'xcDm' && page.activated" v-show="currentIndex === index"
  11. :ref="el => setPageRef(el, index)" />
  12. <BzrdmPage v-else-if="page.key === 'bzrDm' && page.activated" v-show="currentIndex === index"
  13. :ref="el => setPageRef(el, index)" />
  14. <TodoPage v-else-if="page.key === 'todo' && page.activated" v-show="currentIndex === index"
  15. :ref="el => setPageRef(el, index)" />
  16. <MyPage v-else-if="page.key === 'my' && page.activated" v-show="currentIndex === index"
  17. :ref="el => setPageRef(el, index)" />
  18. </view>
  19. </swiper-item>
  20. </swiper>
  21. </view>
  22. </template>
  23. <script setup>
  24. import { ref, nextTick } from 'vue'
  25. import { onShow, onHide, onLoad, onUnload } from '@dcloudio/uni-app'
  26. import Icon from '@/components/icon/index.vue'
  27. // 引入各个页面组件
  28. import MyPage from '@/pages/my/index.vue'
  29. import TodoPage from '@/pages/todo/todo_list.vue'
  30. import BjdmStatisticsPage from '@/pages/statistics/bjdm_statistics.vue'
  31. import BzrdmPage from '@/pages/bjdm/bjdm_bzrDmHomep.vue'
  32. import XcdmPage from '@/pages/xcdm/index.vue'
  33. // 当前选中的页面索引
  34. const currentIndex = ref(0)
  35. // 上一个页面索引
  36. const prevIndex = ref(0)
  37. // 页面组件引用数组
  38. const pageRefs = ref([])
  39. // 页面配置(根据登录态与 syList 动态构建)
  40. const pages = ref([])
  41. // WebSocket 连接地址
  42. const SOCKET_URL = 'wss://m.hfdcschool.com/btcSkt?ssToken=1'
  43. // WebSocket 连接实例
  44. let socketTask = null
  45. /**
  46. * 初始化 WebSocket 连接
  47. */
  48. function initWebSocket() {
  49. if (socketTask) return
  50. socketTask = uni.connectSocket({
  51. url: SOCKET_URL,
  52. fail: (err) => {
  53. console.error('[Main] WebSocket connectSocket 调用失败:', err)
  54. },
  55. complete: () => { }
  56. })
  57. socketTask.onOpen(() => {
  58. console.log('[Main] WebSocket 连接成功:', SOCKET_URL)
  59. // 连接成功后立刻发送 ping
  60. socketTask.send({
  61. data: 'ping',
  62. success: () => {
  63. console.log('[Main] WebSocket ping 发送成功')
  64. },
  65. fail: (err) => {
  66. console.error('[Main] WebSocket ping 发送失败:', err)
  67. }
  68. })
  69. })
  70. socketTask.onError((err) => {
  71. console.error('[Main] WebSocket 连接失败:', err)
  72. })
  73. socketTask.onMessage((res) => {
  74. console.log('[Main] WebSocket 收到消息:', res?.data)
  75. })
  76. socketTask.onClose((res) => {
  77. console.log('[Main] WebSocket 连接关闭:', res)
  78. socketTask = null
  79. })
  80. }
  81. /**
  82. * 关闭 WebSocket 连接
  83. */
  84. function closeWebSocket() {
  85. if (!socketTask) return
  86. socketTask.close({
  87. complete: () => {
  88. socketTask = null
  89. }
  90. })
  91. }
  92. /**
  93. * 设置页面组件引用
  94. */
  95. const setPageRef = (el, index) => {
  96. if (el) {
  97. pageRefs.value[index] = el
  98. }
  99. }
  100. /**
  101. * 触发子组件的生命周期
  102. */
  103. const triggerPageLifecycle = (pageIndex, lifecycle) => {
  104. const pageRef = pageRefs.value[pageIndex]
  105. if (pageRef && typeof pageRef[lifecycle] === 'function') {
  106. // console.log(`[Main] 触发页面 ${pages.value[pageIndex]?.title} 的 ${lifecycle}`)
  107. pageRef[lifecycle]()
  108. } else {
  109. try {
  110. console.warn('[Main] 生命周期调用失败: 未找到方法', {
  111. pageIndex,
  112. lifecycle,
  113. hasRef: !!pageRef,
  114. refKeys: pageRef ? Object.keys(pageRef) : []
  115. })
  116. } catch (e) { }
  117. }
  118. }
  119. // 在小程序环境中,组件渲染与 ref 赋值可能比 nextTick 更晚
  120. // 增加一个轻量重试机制,确保拿到子组件并调用其暴露的生命周期
  121. function ensureRefReady(pageIndex, lifecycle, tries = 0) {
  122. const pageRef = pageRefs.value[pageIndex]
  123. if (pageRef && typeof pageRef[lifecycle] === 'function') {
  124. // console.log(`[Main] ensureRefReady 命中 -> 调用 ${pages.value[pageIndex]?.title}.${lifecycle}`)
  125. pageRef[lifecycle]()
  126. return
  127. }
  128. if (tries >= 8) {
  129. console.warn('[Main] ensureRefReady 超过重试次数,放弃调用', {
  130. pageIndex,
  131. lifecycle,
  132. hasRef: !!pageRef,
  133. refKeys: pageRef ? Object.keys(pageRef) : []
  134. })
  135. return
  136. }
  137. setTimeout(() => ensureRefReady(pageIndex, lifecycle, tries + 1), 30)
  138. }
  139. /**
  140. * swiper 切换事件
  141. */
  142. const onSwiperChange = (e) => {
  143. const newIndex = e.detail.current
  144. const oldIndex = currentIndex.value
  145. // 触发前一个页面的 onHide
  146. if (oldIndex !== newIndex) {
  147. console.log(`[Main] 触发页面 ${oldIndex} 的 onHide`)
  148. triggerPageLifecycle(oldIndex, 'onHide')
  149. }
  150. // 更新当前页面索引
  151. prevIndex.value = oldIndex
  152. currentIndex.value = newIndex
  153. // 懒加载激活
  154. activateByIndex(currentIndex.value)
  155. // 触发新页面的 onShow(可能需要等待 ref 就绪)
  156. nextTick(() => {
  157. console.log(`[Main] 触发页面 ${newIndex} 的 onShow`)
  158. ensureRefReady(currentIndex.value, 'onShow')
  159. })
  160. console.log('切换到页面:', pages.value[currentIndex.value].title)
  161. }
  162. /**
  163. * 点击指示器切换页面
  164. */
  165. function switchToPage(index) {
  166. if (currentIndex.value !== index) {
  167. const oldIndex = currentIndex.value
  168. // 触发前一个页面的 onHide
  169. // console.log(`[Main] switchToPage 触发页面 ${oldIndex} 的 onHide`)
  170. triggerPageLifecycle(oldIndex, 'onHide')
  171. // 更新页面索引
  172. prevIndex.value = oldIndex
  173. currentIndex.value = index
  174. // 懒加载激活
  175. activateByIndex(currentIndex.value)
  176. // 触发新页面的 onShow(可能需要等待 ref 就绪)
  177. nextTick(() => {
  178. // console.log(`[Main] switchToPage 触发页面 ${index} 的 onShow`)
  179. ensureRefReady(currentIndex.value, 'onShow')
  180. })
  181. }
  182. }
  183. /**
  184. * 激活页面
  185. */
  186. function activateByIndex(pageIndex) {
  187. const page = pages.value[pageIndex]
  188. if (!page) return
  189. if (!page.activated) {
  190. // 标记为已激活,触发首次挂载
  191. page.activated = true
  192. // 首次激活后,等组件挂载完成再触发子组件 onLoad(自定义)
  193. nextTick(() => {
  194. ensureRefReady(pageIndex, 'onLoad')
  195. })
  196. }
  197. }
  198. /**
  199. * 是否已登录
  200. */
  201. function isLoggedIn() {
  202. try {
  203. const u = uni.getStorageSync('userInfo')
  204. if (!u) return false
  205. const info = typeof u === 'string' ? JSON.parse(u) : u
  206. return !!info?.yhsbToken
  207. } catch (e) {
  208. return false
  209. }
  210. }
  211. /**
  212. * 获取 syList(数组)
  213. */
  214. function getSyList() {
  215. try {
  216. const u = uni.getStorageSync('userInfo')
  217. const info = typeof u === 'string' ? JSON.parse(u) : u
  218. const raw = info?.syList || info?.sylist || '[]'
  219. // return ['statistics','bzrDmHomep']
  220. if (Array.isArray(raw)) return raw
  221. return JSON.parse(raw)
  222. } catch (e) {
  223. return []
  224. }
  225. }
  226. /**
  227. * 按登录态构建一级页
  228. * - 未登录:仅“我的”
  229. * - 已登录:按 syList 渲染可见首页
  230. */
  231. function buildPagesFromAuth() {
  232. const list = []
  233. const loggedIn = isLoggedIn()
  234. const sy = loggedIn ? getSyList() : []
  235. // 能力 → 页面配置映射(按需扩展)
  236. const capabilityMap = {
  237. mp_xcdmHomep: {
  238. key: 'xcDm',
  239. title: '校车点名',
  240. icon: 'icon-dianming',
  241. path: 'pages/xcdm/index',
  242. component: XcdmPage,
  243. },
  244. mp_njdmHomep: {
  245. key: 'mp_njdmHomep',
  246. title: '点名统计',
  247. icon: 'icon-tongji',
  248. path: 'pages/statistics/bjdm_statistics',
  249. component: BjdmStatisticsPage,
  250. },
  251. mp_bzrdmHomep: {
  252. key: 'bzrDm',
  253. title: '班主任点名',
  254. icon: 'icon-dianming',
  255. path: 'pages/bjdm/bjdm_bzrDm',
  256. component: BzrdmPage,
  257. },
  258. todo: {
  259. key: 'todo',
  260. title: '待办',
  261. icon: 'icon-daiban',
  262. path: 'pages/todo/todo_list',
  263. component: TodoPage,
  264. },
  265. }
  266. if (loggedIn) {
  267. const added = new Set()
  268. // 仅当 syList 含 mp_xcdmHomep 时才展示“校车点名”
  269. if (sy.includes('mp_xcdmHomep')) {
  270. list.push({ ...capabilityMap.mp_xcdmHomep, activated: false })
  271. added.add(capabilityMap.mp_xcdmHomep.key)
  272. }
  273. sy.forEach(cap => {
  274. const conf = capabilityMap[cap]
  275. if (conf && !added.has(conf.key)) {
  276. list.push({ ...conf, activated: false })
  277. added.add(conf.key)
  278. }
  279. })
  280. }
  281. // 末尾永远追加“我的”
  282. list.push({
  283. key: 'my',
  284. title: '我的',
  285. icon: 'icon-wode',
  286. path: 'pages/my/index',
  287. component: MyPage,
  288. activated: false,
  289. })
  290. pages.value = list
  291. console.log('✅ 获取权限列表成功:', list)
  292. }
  293. // 主容器的生命周期
  294. onLoad((options) => {
  295. // 页面进入时建立 WebSocket 连接
  296. initWebSocket()
  297. // 测试写死
  298. // options.sn = 'A100006B6256E6'
  299. // options.cardNo = 'E00401532101245F'
  300. if(options.cardNo == 'E004015316BE6182'){
  301. options.cardNo = 'E004015316BE61821'
  302. }
  303. // if (true) {
  304. if (typeof wmpf !== 'undefined') {
  305. console.log('WMPF环境')
  306. uni.reLaunch({
  307. url: `/pages/device/index?sn=${options.sn}&cardNo=${options.cardNo}`
  308. })
  309. } else {
  310. console.log('非WMPF环境')
  311. // console.log('主容器页面加载', options)
  312. // 先按登录态构建一级页
  313. buildPagesFromAuth()
  314. // 临时:主首页写死默认打开“校车点名”
  315. const xcdmIndex = pages.value.findIndex(p => p.key === 'xcDm')
  316. currentIndex.value = xcdmIndex >= 0 ? xcdmIndex : 0
  317. // 激活当前页(首次时会触发 onLoad)
  318. activateByIndex(currentIndex.value)
  319. // 监听登录事件,登录后重建一级页
  320. uni.$on('login', () => {
  321. const currentKey = pages.value[currentIndex.value]?.key
  322. buildPagesFromAuth()
  323. // 临时:登录后默认打开“校车点名”
  324. const idx = pages.value.findIndex(p => p.key === 'xcDm')
  325. currentIndex.value = idx >= 0 ? idx : 0
  326. activateByIndex(currentIndex.value)
  327. nextTick(() => {
  328. ensureRefReady(currentIndex.value, 'onShow')
  329. })
  330. })
  331. }
  332. })
  333. onShow(() => {
  334. // console.log('主容器页面显示')
  335. // 触发当前页面的 onShow(可能需要等待 ref 就绪)
  336. nextTick(() => {
  337. ensureRefReady(currentIndex.value, 'onShow')
  338. })
  339. })
  340. onHide(() => {
  341. // console.log('主容器页面隐藏')
  342. // 触发当前页面的 onHide
  343. triggerPageLifecycle(currentIndex.value, 'onHide')
  344. })
  345. onUnload(() => {
  346. // console.log('主容器页面卸载')
  347. // 触发所有页面的 onUnload
  348. pages.value.forEach((_, index) => {
  349. triggerPageLifecycle(index, 'onUnload')
  350. })
  351. // 页面卸载时关闭 WebSocket 连接
  352. closeWebSocket()
  353. })
  354. </script>
  355. <style lang="scss" scoped>
  356. .main-container {
  357. width: 100%;
  358. height: 100vh;
  359. display: flex;
  360. flex-direction: column;
  361. }
  362. .top-indicator {
  363. height: 100rpx;
  364. background: #ffffff;
  365. border-bottom: 1rpx solid #e6e6e6;
  366. display: flex;
  367. align-items: center;
  368. justify-content: space-around;
  369. padding: 0 20rpx;
  370. box-sizing: border-box;
  371. flex-shrink: 0;
  372. }
  373. .indicator-item {
  374. display: flex;
  375. flex-direction: column;
  376. align-items: center;
  377. justify-content: center;
  378. flex: 1;
  379. height: 100%;
  380. gap: 8rpx;
  381. text {
  382. font-size: 24rpx;
  383. transition: color 0.3s;
  384. }
  385. &.active {
  386. text {
  387. font-weight: bold;
  388. }
  389. }
  390. &:active {
  391. background-color: rgba(83, 156, 248, 0.1);
  392. }
  393. }
  394. .main-swiper {
  395. flex: 1;
  396. width: 100%;
  397. height: calc(100vh - 100rpx);
  398. }
  399. .page-container {
  400. width: 100%;
  401. height: 100%;
  402. overflow: hidden;
  403. }
  404. </style>