index.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  1. <template>
  2. <view>
  3. <swiper class="student-swiper" :class="students && students.length > 1 ? 'show-dots' : ''"
  4. :indicator-dots="students && students.length > 1" indicator-color="#ffffff" indicator-active-color="#666666"
  5. previous-margin="30rpx" next-margin="30rpx" v-if="students && students.length > 0" @change="onSwiperChange">
  6. <swiper-item v-for="student in students" :key="student.id" @click="onSwiperClick(student.id)">
  7. <view class="student-card">
  8. <view class="student-card-left">
  9. <image class="student-avatar" :src="student.avatar"></image>
  10. <view class="student-info">
  11. <text class="student-name">{{ student.name }}</text>
  12. <text class="student-class">{{ student.className }}</text>
  13. </view>
  14. </view>
  15. <view @click.stop="handleScan()" class="scan-btn" v-if="isLogin">
  16. <Icon name="icon-saoyisao2" size="72" color="#bfbfbf" />
  17. <text>扫一扫</text>
  18. </view>
  19. </view>
  20. </swiper-item>
  21. </swiper>
  22. <!-- 登录之后 -->
  23. <view class="container" v-if="isLogin">
  24. <view class="function-list">
  25. <!-- 校车位置(家长) -->
  26. <!-- <view class="function-item" @click="goToBusLocation">
  27. <view class="function-icon">
  28. <Icon name="icon-cheliangyuyue" size="60" color="#e59f40" />
  29. </view>
  30. <text class="function-name">校车位置</text>
  31. </view> -->
  32. <!-- 留言功能 -->
  33. <!-- <view class="function-item" @click="goToMessage">
  34. <view class="function-icon">
  35. <Icon name="icon-jinchu" size="60" color="#393f51" />
  36. <view class="badge" v-if="hasNewMessage"></view>
  37. </view>
  38. <text class="function-name">留言</text>
  39. </view> -->
  40. <!-- <view class="function-item" @click="goToInOut">
  41. <view class="function-icon">
  42. <Icon name="icon-duihuapaopao2" size="60" color="#393f51" />
  43. <view class="badge" v-if="hasNewRecord"></view>
  44. </view>
  45. <text class="function-name">进出</text>
  46. </view> -->
  47. <!-- <view class="function-item" @click="goToBzrDm">
  48. <view class="function-icon">
  49. <Icon name="icon-dianming" size="60" color="#8992ef"/>
  50. </view>
  51. <text class="function-name">班主任点名(小程序)</text>
  52. </view> -->
  53. <!-- <view class="function-item" @click="goToBzrDmWebview">
  54. <view class="function-icon">
  55. <Icon name="icon-dianming" size="60" color="#8992ef" />
  56. </view>
  57. <text class="function-name">班主任点名</text>
  58. </view> -->
  59. <view class="function-item" v-for="item in btnList" :key="item.id || item.xh || item.dest" @click="commonBtnClick(item)">
  60. <view class="function-icon">
  61. <image :src="getFullIconUrl(item.icon)" mode="aspectFill"></image>
  62. <view class="badge" v-if="item.hasNewDot"></view>
  63. </view>
  64. <text class="function-name">{{ item.desc }}</text>
  65. </view>
  66. <!-- <view class="function-item" @click="goToDmWebview">
  67. <view class="function-icon">
  68. <Icon name="icon-dianming" size="60" color="#8992ef"/>
  69. </view>
  70. <text class="function-name">点名</text>
  71. </view> -->
  72. <!-- <view class="function-item" @click="goToXunChaWebview">
  73. <view class="function-icon">
  74. <Icon name="icon-xuncha" size="60" color="#475fab" />
  75. </view>
  76. <text class="function-name">校长巡查</text>
  77. </view> -->
  78. <!-- <view class="function-item" @click="goXuncha">
  79. <view class="function-icon">
  80. <Icon name="icon-xuncha" size="60" color="#475fab" />
  81. </view>
  82. <text class="function-name">巡查</text>
  83. </view> -->
  84. <!-- <view class="function-item" @click="goToXfjl">
  85. <view class="function-icon">
  86. <Icon name="icon-xiaofeijilu" size="60" color="#c48c21" />
  87. </view>
  88. <text class="function-name">消费记录</text>
  89. </view> -->
  90. <!-- <view class="function-item" @click="goToTestList">
  91. <view class="function-icon">
  92. <Icon name="icon-dianming" size="60" color="#ff6b6b"/>
  93. </view>
  94. <text class="function-name">测试通用列表</text>
  95. </view> -->
  96. <!-- <view class="function-item" @click="goToClyyWebview">
  97. <view class="function-icon">
  98. <Icon name="icon-cheliangyuyue" size="60" color="#54a400" />
  99. </view>
  100. <text class="function-name">车辆预约</text>
  101. </view> -->
  102. <!-- 测试订阅消息跳转 -->
  103. <!-- <view class="function-item" @click="goToNoticeRedirect">
  104. <view class="function-icon">
  105. <Icon name="icon-xiaoxi" size="60" color="#ff6b6b" />
  106. </view>
  107. <text class="function-name">测试订阅跳转</text>
  108. </view> -->
  109. <!-- <view class="function-item" @click="goToLoginTest">
  110. <view class="function-icon">
  111. <Icon name="icon-denglu" size="60" color="#40ac6d" />
  112. </view>
  113. <text class="function-name">登录测试(H5)</text>
  114. </view>-->
  115. </view>
  116. </view>
  117. <!-- 未登录显示 -->
  118. <view class="unlogin" v-else>
  119. <image :src="unloginImageUrl" mode="aspectFit" />
  120. </view>
  121. <!-- 登录确认弹窗 -->
  122. <LoginConfirmModal
  123. ref="loginConfirmModalRef"
  124. v-model:visible="showLoginConfirm"
  125. :loginType="currentLoginType"
  126. @confirm="handleLoginConfirm"
  127. @cancel="handleLoginCancel"
  128. />
  129. </view>
  130. </template>
  131. <script setup>
  132. import { ref, computed } from 'vue'
  133. import { useUserStore } from '@/store/modules/user'
  134. import websocketService from '@/utils/websocket'
  135. import Icon from '@/components/icon/index.vue';
  136. import { onShow, onHide, onLoad, onUnload } from '@dcloudio/uni-app'
  137. import { goTo } from '@/utils/navigation'
  138. import { userApi } from '@/api/user';
  139. import { commonApi } from '@/api/common'
  140. import env from '@/config/env.js'
  141. import { getImageUrl } from '@/utils/util'
  142. // 组件导入
  143. import LoginPopup from '@/components/login/LoginPopup.vue'
  144. import LoginConfirmModal from '@/components/LoginConfirmModal/index.vue'
  145. const userStore = useUserStore();
  146. const hasNewMessage = ref(false);
  147. const currentStudentIndex = ref(0);
  148. const isLogin = ref(false);
  149. let userInfo = uni.getStorageSync('userInfo') || {};
  150. const messageListenersBound = ref(false)
  151. const studentIds = ref([]);
  152. const unloginImageUrl = "/static/images/unlogin.jpg";
  153. const students = ref([
  154. {
  155. id: 0,
  156. name: '请登录',
  157. className: '',
  158. avatar: '/static/logo.png'
  159. }
  160. ])
  161. // 登录确认弹窗相关
  162. const loginConfirmModalRef = ref(null)
  163. const showLoginConfirm = ref(false)
  164. const currentLoginType = ref({})
  165. const pendingLoginData = ref(null)
  166. const templateId = 'rVi7erjgdVRVBMgIhhHgPu9JMp3TM7Ug30x1ycAXY0w'
  167. // 登录类型配置映射
  168. const LOGIN_TYPE_MAP = {
  169. 1: { // PC端
  170. systemName: 'PC管理系统',
  171. title: '扫码登录',
  172. tip: '确认登录PC端管理系统吗?',
  173. icon: '/static/logo.png',
  174. },
  175. 2: { // 门系统
  176. systemName: '门禁系统',
  177. title: '扫码登录',
  178. tip: '确认登录门禁系统吗?',
  179. icon: '/static/logo.png',
  180. }
  181. // 未来可以继续扩展其他系统...
  182. }
  183. const btnList = ref([])
  184. const toNumber = (value) => {
  185. if (value === null || value === undefined || value === '') {
  186. return 0
  187. }
  188. const numberValue = Number(value)
  189. return Number.isNaN(numberValue) ? 0 : numberValue
  190. }
  191. const extractCountFromResponse = (payload) => {
  192. if (payload === null || payload === undefined) {
  193. return 0
  194. }
  195. if (typeof payload === 'number' || typeof payload === 'string') {
  196. return toNumber(payload)
  197. }
  198. if (Array.isArray(payload)) {
  199. for (const element of payload) {
  200. const count = extractCountFromResponse(element)
  201. if (count > 0) {
  202. return count
  203. }
  204. }
  205. return 0
  206. }
  207. if (typeof payload === 'object') {
  208. if (payload.ssCode !== undefined) {
  209. return payload.ssCode === 0 ? toNumber(payload.ssData) : 0
  210. }
  211. const preferredKeys = ['newMsgNum', 'msgNum', 'count', 'total', 'num', 'value']
  212. for (const key of preferredKeys) {
  213. if (payload[key] !== undefined) {
  214. const count = extractCountFromResponse(payload[key])
  215. if (count > 0) {
  216. return count
  217. }
  218. }
  219. }
  220. for (const key of Object.keys(payload)) {
  221. const count = extractCountFromResponse(payload[key])
  222. if (count > 0) {
  223. return count
  224. }
  225. }
  226. }
  227. return 0
  228. }
  229. const fetchBtnStateCount = async (stateService) => {
  230. if (!stateService) {
  231. return 0
  232. }
  233. try {
  234. const response = await userApi.callService(stateService)
  235. const count = extractCountFromResponse(response?.data)
  236. console.log(`[my] state接口 ${stateService} 返回数量:`, count, response?.data)
  237. return count
  238. } catch (error) {
  239. console.error(`[my] state接口 ${stateService} 调用失败:`, error)
  240. return 0
  241. }
  242. }
  243. const updateBtnBadgeStatus = async () => {
  244. if (!btnList.value.length) {
  245. return
  246. }
  247. const nextBtnList = await Promise.all(
  248. btnList.value.map(async (item) => {
  249. const stateService = (item.state || '').trim()
  250. if (!stateService) {
  251. return {
  252. ...item,
  253. hasNewDot: false
  254. }
  255. }
  256. const count = await fetchBtnStateCount(stateService)
  257. return {
  258. ...item,
  259. hasNewDot: count > 0
  260. }
  261. })
  262. )
  263. btnList.value = nextBtnList
  264. }
  265. const getFullIconUrl = (icon) => {
  266. return `${env.baseUrl}/skin/mp_easy/icon/${icon}.svg`
  267. }
  268. const commonBtnClick = (item) => {
  269. if (!checkLogin()) {
  270. pleaseLogin()
  271. return
  272. }
  273. if (item.dest === 'chat') {
  274. goToMessage()
  275. return
  276. }
  277. if (item.dest === 'buy') {
  278. goToRecharge()
  279. return
  280. }
  281. console.log(`准备进入 ${item.desc},先检查订阅消息`)
  282. // 先处理订阅,订阅完成后再跳转
  283. openPopup(() => {
  284. console.log('订阅处理完成,准备跳转')
  285. goTo('/pages/common/webview', {
  286. dest: item.dest || "mp_objList",
  287. title: item.desc,
  288. service: item.init
  289. })
  290. })
  291. }
  292. const goXuncha = () => {
  293. if (!checkLogin()) {
  294. pleaseLogin()
  295. return
  296. }
  297. }
  298. // 跳转到充值页面
  299. const goToRecharge = () => {
  300. if (!checkLogin()) {
  301. pleaseLogin()
  302. return
  303. }
  304. goTo('/pages/payment/recharge')
  305. }
  306. const goToBusLocation = () => {
  307. if (!checkLogin()) {
  308. pleaseLogin()
  309. return
  310. }
  311. goTo('/pages/xcdm/parent')
  312. }
  313. const goToCallCenter = () => {
  314. if (!checkLogin()) {
  315. pleaseLogin()
  316. return
  317. }
  318. goTo('/pages/parent/call-center')
  319. }
  320. const checkLogin = () => {
  321. if (typeof userInfo == 'string') {
  322. userInfo = JSON.parse(userInfo)
  323. }
  324. // console.log('userInfo', userInfo)
  325. if (!userInfo || !userInfo.yhsbToken) {
  326. return false
  327. }
  328. return true
  329. }
  330. const pleaseLogin = () => {
  331. uni.showToast({
  332. title: '请先登录',
  333. icon: 'none'
  334. })
  335. // 跳转到H5登录页面
  336. userStore.showH5Login()
  337. }
  338. // 扫码功能
  339. const handleScan = () => {
  340. console.log('handleScan')
  341. if (!checkLogin()) {
  342. pleaseLogin()
  343. return
  344. }
  345. // 调用微信小程序扫码API
  346. uni.scanCode({
  347. onlyFromCamera: false, // 允许从相册选择二维码图片
  348. scanType: ['qrCode', 'barCode'], // 支持二维码和条形码
  349. success: async (res) => {
  350. try {
  351. console.log('扫码结果:', res)
  352. const jsonStr = '{' + res.result.replace(/(\w+):/g, '"$1":') + '}';
  353. const obj = JSON.parse(jsonStr);
  354. console.log('扫码结果:', res, obj)
  355. // 获取登录类型配置
  356. const loginType = LOGIN_TYPE_MAP[obj.ss]
  357. if (!loginType) {
  358. uni.showToast({
  359. title: '不支持的登录类型',
  360. icon: 'none'
  361. })
  362. return
  363. }
  364. // 保存待处理的登录数据
  365. pendingLoginData.value = obj
  366. // 设置当前登录类型并显示确认弹窗
  367. currentLoginType.value = loginType
  368. showLoginConfirm.value = true
  369. } catch (error) {
  370. console.error('解析扫码结果失败:', error)
  371. uni.showToast({
  372. title: '二维码格式错误',
  373. icon: 'none'
  374. })
  375. }
  376. },
  377. fail: (err) => {
  378. console.error('扫码失败:', err)
  379. if (err.errMsg && err.errMsg.indexOf('cancel') === -1) {
  380. uni.showToast({
  381. title: '扫码失败',
  382. icon: 'none'
  383. })
  384. }
  385. }
  386. })
  387. }
  388. // 确认登录
  389. const handleLoginConfirm = async () => {
  390. if (!pendingLoginData.value) return
  391. const obj = pendingLoginData.value
  392. let resp = null
  393. try {
  394. uni.showLoading({
  395. title: '登录中...',
  396. mask: true
  397. })
  398. if (obj.ss == 1) { // PC端
  399. console.log('pc扫码登录')
  400. resp = await userApi.sacnLoginQrcode({ k: obj.k })
  401. } else if (obj.ss == 2) { // 门系统
  402. console.log('门系统扫码登录')
  403. resp = await userApi.sacnGateLoginQrcode({ k: obj.k })
  404. }
  405. console.log('扫码登录结果:', resp)
  406. uni.hideLoading()
  407. uni.showToast({
  408. title: '登录成功',
  409. icon: 'success'
  410. })
  411. } catch (error) {
  412. uni.hideLoading()
  413. console.error('登录失败:', error)
  414. uni.showToast({
  415. title: error.message || '登录失败',
  416. icon: 'none'
  417. })
  418. } finally {
  419. pendingLoginData.value = null
  420. }
  421. }
  422. // 取消登录
  423. const handleLoginCancel = () => {
  424. pendingLoginData.value = null
  425. console.log('用户取消登录')
  426. }
  427. const onSwiperChange = (e) => {
  428. currentStudentIndex.value = e.detail.current;
  429. }
  430. const onSwiperClick = (studentId) => {
  431. if (studentId == 0) {
  432. pleaseLogin()
  433. }else{
  434. goTo('/pages/my/changeInfo')
  435. }
  436. }
  437. const goToInOut = () => {
  438. if (checkLogin()) {
  439. // 获取当前学生ID
  440. const currentStudentId = students.value[currentStudentIndex.value].id;
  441. // 检查 studentIds 数组中是否存在当前学生ID
  442. const index = studentIds.value.indexOf(currentStudentId);
  443. if (index !== -1) {
  444. // 如果存在,才从数组中移除
  445. this.studentIds.splice(index, 1);
  446. }
  447. uni.navigateTo({
  448. url: '/pages/parent/in-out?role=parent&studentId=' + students.value[currentStudentIndex.value].id
  449. })
  450. } else {
  451. pleaseLogin()
  452. }
  453. }
  454. const goToMessage = () => {
  455. if (checkLogin()) {
  456. uni.navigateTo({
  457. url: '/pages/parent/message?role=parent'
  458. })
  459. } else {
  460. pleaseLogin()
  461. }
  462. }
  463. const goToBzrDm = () => {
  464. if (checkLogin()) {
  465. // 跳转到通用列表页面,传递班主任点名的配置
  466. const config = {
  467. ssServ: 'bjdm_cx',
  468. title: '班主任点名',
  469. baseParams: {}
  470. }
  471. const url = `/pages/bjdm/bjdm_bzrDm_list`
  472. goTo(url)
  473. } else {
  474. pleaseLogin()
  475. }
  476. }
  477. const goToTestList = () => {
  478. if (checkLogin()) {
  479. // 测试通用列表页面,可以传递不同的服务配置
  480. const config = {
  481. ssServ: 'bjdm_cx', // 可以改成其他服务测试
  482. title: '测试通用列表页面',
  483. baseParams: {
  484. // 可以添加额外的查询参数
  485. testParam: 'test'
  486. }
  487. }
  488. const url = `/pages/common/list?ssServ=${config.ssServ}&baseParams=${encodeURIComponent(JSON.stringify(config.baseParams))}`
  489. uni.navigateTo({
  490. url: url
  491. })
  492. } else {
  493. pleaseLogin()
  494. }
  495. }
  496. const goToXfjl = () => {
  497. if (checkLogin()) {
  498. uni.navigateTo({
  499. url: '/pages/xfjl/index'
  500. })
  501. } else {
  502. pleaseLogin()
  503. }
  504. }
  505. const goToClyy = () => {
  506. if (checkLogin()) {
  507. const yhid = uni.getStorageSync('userInfo').yhid
  508. // 测试通用列表页面,可以传递不同的服务配置
  509. // const url = `/pages/common/list?ssServ=${config.ssServ}&baseParams=${encodeURIComponent(JSON.stringify(config.baseParams))}`
  510. // goTo(url)
  511. goTo('/pages/clyy/clyy_inp')
  512. } else {
  513. pleaseLogin()
  514. }
  515. }
  516. const goToClyyWebview = () => {
  517. if (!checkLogin()) {
  518. pleaseLogin()
  519. return
  520. }
  521. console.log('准备进入车辆预约,先检查订阅消息')
  522. // 先处理订阅,订阅完成后再跳转
  523. openPopup(() => {
  524. console.log('订阅处理完成,准备跳转到车辆预约页面')
  525. goTo('/pages/common/webview', {
  526. dest: 'mp_clyy_inp',
  527. title: '车辆预约',
  528. })
  529. })
  530. }
  531. const goToXunChaWebview = () => {
  532. const testParams = {
  533. dest: 'mp_rcXcdjl_excelZxxzAdd', // 测试用的dest参数
  534. ssToken: '1b298017d5764bb39708ab3724739cbe', // 测试用的ssToken
  535. // 可以添加其他测试参数
  536. title: '校长巡查',
  537. }
  538. // 构建跳转URL
  539. const queryString = Object.keys(testParams)
  540. .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(testParams[key])}`)
  541. .join('&')
  542. // 跳转到redirect页面
  543. if (checkLogin()) {
  544. goTo(`/pages/common/webview?${queryString}`)
  545. // goTo('/pages/common/webview', {
  546. // dest: 'mp_excelRcXcdjl_edit',
  547. // ssToken: '39101b19b2c545b2a62c72177e033cc5',
  548. // })
  549. } else {
  550. pleaseLogin()
  551. }
  552. }
  553. const goToBzrDmWebview = () => {
  554. if (checkLogin()) {
  555. goTo('/pages/common/webview', {
  556. dest: 'mp_objList',
  557. title: '班主任点名(webview)',
  558. service: 'bjdm_cx'
  559. })
  560. // goTo('/pages/common/webview', {
  561. // dest: 'upload-image-demo',
  562. // })
  563. } else {
  564. pleaseLogin()
  565. }
  566. }
  567. const goToDmWebview = () => {
  568. goTo('/pages/common/webview', {
  569. dest: 'bjdm_bzrDm',
  570. title: '班主任点名(webview)',
  571. })
  572. }
  573. const goToLoginTest = () => {
  574. // console.log('🔐 跳转到H5登录测试页面')
  575. goTo('/pages/common/webview', {
  576. dest: 'login',
  577. title: '登录'
  578. })
  579. }
  580. // 测试订阅消息跳转
  581. const goToNoticeRedirect = () => {
  582. // 模拟订阅消息的参数,可以测试不同的dest和ssToken
  583. const testParams = {
  584. dest: 'mp_ccChk', // 测试用的dest参数
  585. ssToken: '5d8c4af16247457fbd9ea71f5982a733', // 测试用的ssToken
  586. // 可以添加其他测试参数
  587. testParam: 'test'
  588. }
  589. // 构建跳转URL
  590. const queryString = Object.keys(testParams)
  591. .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(testParams[key])}`)
  592. .join('&')
  593. // 跳转到redirect页面
  594. goTo(`/pages/common/webview?${queryString}`)
  595. }
  596. const hasNewRecord = computed(() => {
  597. // 获取当前选中的学生ID
  598. const currentStudentId = students[currentStudentIndex.value]?.id;
  599. // 如果当前没有选中学生,返回 false
  600. if (!currentStudentId) return false;
  601. // 检查当前学生ID是否在 studentIds 数组中
  602. return this.studentIds.includes(currentStudentId);
  603. })
  604. const openPopup = (callback) => {
  605. // console.log('openPopup 被调用')
  606. // 获取用户的当前设置,判断是否点击了"总是保持以上,不在询问"
  607. uni.getSetting({
  608. withSubscriptions: true, // 是否获取用户订阅消息的订阅状态,默认false不返回
  609. success(res) {
  610. // console.log('获取设置成功:', res)
  611. console.log('订阅设置:', res.subscriptionsSetting)
  612. if (res.subscriptionsSetting && res.subscriptionsSetting[templateId] === 'accept') {
  613. // console.log('用户已经同意订阅该模板消息')
  614. // // 已订阅,直接执行回调
  615. callback && callback()
  616. } else {
  617. console.log('用户未同意订阅或设置不存在,调起订阅消息弹窗')
  618. //因为没有选择总是保持,所以需要调起授权弹窗再次授权
  619. authorizationBtn(callback);
  620. }
  621. },
  622. fail(err) {
  623. console.error('获取设置失败:', err)
  624. // 获取设置失败,也尝试调起订阅弹窗
  625. authorizationBtn(callback);
  626. }
  627. })
  628. }
  629. const authorizationBtn = (callback) => {
  630. console.log('authorizationBtn')
  631. uni.requestSubscribeMessage({
  632. // tmplIds: ['5piwpV8roTajn7Mo2NrBpTjFTXwWEr002Ho-rw4HGQw'],
  633. tmplIds: [templateId],
  634. success(res) {
  635. console.log('订阅消息请求结果:', res)
  636. // const templateId = '5piwpV8roTajn7Mo2NrBpTjFTXwWEr002Ho-rw4HGQw'
  637. if (res[templateId] === 'accept') {
  638. console.log('用户同意订阅消息')
  639. uni.showToast({
  640. title: '订阅成功',
  641. icon: 'success'
  642. })
  643. } else {
  644. console.log('用户拒绝订阅消息')
  645. }
  646. // 无论用户是否同意,都执行回调
  647. callback && callback()
  648. },
  649. fail(err) {
  650. console.error('订阅消息请求失败:', err)
  651. // 失败也执行回调
  652. callback && callback()
  653. }
  654. })
  655. }
  656. const requestDeviceAuth = async () => {
  657. try {
  658. // 先检查是否已授权
  659. const voipList = await new Promise((resolve, reject) => {
  660. uni.getDeviceVoIPList({
  661. success: res => resolve(res.list),
  662. fail: err => reject(err)
  663. })
  664. })
  665. // console.log('voipList', voipList)
  666. // 如果已授权,直接返回
  667. if (voipList.some(device => device.status === 1)) {
  668. return true
  669. }
  670. // 获取设备票据(需要后端接口支持)
  671. // const ticketData = await userApi.getDeviceTicket()
  672. // 请求设备授权
  673. await new Promise((resolve, reject) => {
  674. uni.requestDeviceVoIP({
  675. isGroup: true,
  676. groupId: 'gUpNA_xJr-WkYBIeGofWSPQ',
  677. success: res => {
  678. // console.log('授权成功:', res)
  679. },
  680. fail: err => {
  681. console.error('授权失败:', err)
  682. if (err.errCode === 9) {
  683. uni.showModal({
  684. title: '提示',
  685. content: '请在设置中开启设备通话权限',
  686. success: () => {
  687. uni.openSetting()
  688. }
  689. })
  690. }
  691. reject(err)
  692. }
  693. })
  694. })
  695. return true
  696. } catch (error) {
  697. console.error('设备授权处理失败:', error)
  698. return false
  699. }
  700. }
  701. const getUserInfo = async () => {
  702. let userInfo = uni.getStorageSync('userInfo')
  703. if (typeof userInfo === 'string'){
  704. userInfo = JSON.parse(userInfo)
  705. }
  706. const userMoreInfo = await userApi.mp_infoHomep_load(userInfo.userId)
  707. console.log('userMoreInfo', userMoreInfo.data.ryInfo, typeof userMoreInfo.data.ryInfo)
  708. btnList.value = JSON.parse(userMoreInfo.data.btnList || '[]').sort((a, b) => (a.xh || 0) - (b.xh || 0)).map(item => ({
  709. ...item,
  710. hasNewDot: false
  711. })) // 更新按钮列表
  712. await updateBtnBadgeStatus()
  713. const rylbm = userMoreInfo.data.ryInfo?.rylbm;
  714. let huixian = '已登录';
  715. if (userMoreInfo.data.ryInfo?.bjid) { // 有班级id 优先班级
  716. const bjid = userMoreInfo.data.ryInfo?.bjid
  717. const bj = await commonApi.getDictByCbNameAndValue('bj', bjid)
  718. huixian = bj.data.result[bjid]
  719. }else if(userMoreInfo.data.ryInfo?.bmid){ // 教职工
  720. const bmid = userMoreInfo.data.ryInfo?.bmid
  721. const bm = await commonApi.getDictByCbNameAndValue('bm', bmid)
  722. huixian = bm.data.result[bmid]
  723. }else {
  724. huixian = '已登录'
  725. }
  726. // 如果有头像的话,则直接取头像
  727. if (userMoreInfo.data.ryInfo?.yszwj) {
  728. const avatar = getImageUrl(userMoreInfo.data.ryInfo.yszwj)
  729. students.value[0] = {
  730. id: 1,
  731. name: userInfo.xm,
  732. className: huixian,
  733. avatar: avatar
  734. }
  735. } else {
  736. // 如果性别码码为1,则取性别为男的头像 否则取性别为女
  737. if (userMoreInfo.data.ryInfo?.xbm == 1) {
  738. students.value[0] = {
  739. id: 1,
  740. name: userInfo.xm,
  741. className: huixian,
  742. avatar: '/static/images/yishuzhao_nan.svg'
  743. }
  744. } else {
  745. students.value[0] = {
  746. id: 1,
  747. name: userInfo.xm,
  748. className: huixian,
  749. avatar: '/static/images/yishuzhao_nv.svg'
  750. }
  751. }
  752. }
  753. let ryInfo = userMoreInfo.data.ryInfo
  754. console.log('ryInfo', ryInfo)
  755. uni.setStorageSync('userInfo', {
  756. ...userInfo,
  757. ...ryInfo,
  758. }) // 更新用户信息
  759. }
  760. const handleInit = (data) => {
  761. // console.log('handleInit', data)
  762. studentIds.value = data.studentIds || [];
  763. }
  764. const handleNewMessage = (data) => {
  765. // console.log('handleNewMessage', data)
  766. if (!studentIds.value.includes(data.studentId)) {
  767. studentIds.value.push(data.studentId);
  768. }
  769. }
  770. const handleNewRecord = (data) => {
  771. // console.log('handleNewRecord', data)
  772. }
  773. const handLogin = (data) => {
  774. isLogin.value = true
  775. requestDeviceAuth()
  776. getUserInfo()
  777. // websocketService.connect()
  778. if (!messageListenersBound.value) {
  779. uni.$on('init', handleInit)
  780. uni.$on('newMessage', handleNewMessage)
  781. uni.$on('newRecord', handleNewRecord)
  782. messageListenersBound.value = true
  783. }
  784. }
  785. const handleOnShow = () => {
  786. // console.log('首页显示')
  787. uni.$on('login', handLogin)
  788. if (checkLogin()) {
  789. // 如果已经登录,则执行登录后的操作
  790. handLogin()
  791. } else {
  792. isLogin.value = false
  793. }
  794. }
  795. const handleOnHide = () => {
  796. // console.log('首页隐藏')
  797. }
  798. const handleOnLoad = () => {
  799. // console.log('首页加载')
  800. // 监听登录事件(只绑定一次)
  801. uni.$on('login', handLogin)
  802. if (checkLogin()) {
  803. // 如果已经登录,则执行登录后的操作
  804. handLogin()
  805. } else {
  806. isLogin.value = false
  807. }
  808. }
  809. const handleOnUnload = () => {
  810. // console.log('首页卸载')
  811. if (messageListenersBound.value) {
  812. uni.$off('newMessage', handleNewMessage)
  813. uni.$off('newRecord', handleNewRecord)
  814. messageListenersBound.value = false
  815. }
  816. // websocketService.disconnect()
  817. }
  818. // 暴露生命周期方法供主容器调用(主容器会调用这些而非页面级 onLoad/onShow)
  819. defineExpose({
  820. onShow: handleOnShow,
  821. onHide: handleOnHide,
  822. onLoad: handleOnLoad,
  823. onUnload: handleOnUnload
  824. })
  825. </script>
  826. <style lang="scss">
  827. .wx-swiper-dots.wx-swiper-dots-horizontal {
  828. width: calc(100% - 90rpx);
  829. display: flex;
  830. justify-content: flex-end;
  831. display: none;
  832. display: flex;
  833. justify-content: flex-end;
  834. display: none;
  835. }
  836. .show-dots .wx-swiper-dots.wx-swiper-dots-horizontal {
  837. display: flex;
  838. }
  839. .wx-swiper-dot {
  840. border: 2rpx solid #666666 !important;
  841. background: #ffffff !important;
  842. }
  843. .wx-swiper-dot-active {
  844. background: #666666 !important;
  845. }
  846. .container {
  847. border-top: 2rpx solid #dcdcdc;
  848. padding: 30rpx;
  849. }
  850. .student-swiper {
  851. height: 250rpx;
  852. margin-top: 40rpx;
  853. // margin-bottom: 40rpx;
  854. }
  855. .student-card {
  856. box-sizing: border-box;
  857. display: flex;
  858. align-items: center;
  859. justify-content: space-between;
  860. padding: 30rpx;
  861. background: #FFFFFF;
  862. border-radius: 12rpx;
  863. box-shadow: 0rpx 6rpx 15rpx rgba(0, 0, 0, .3);
  864. margin: 10rpx 15rpx;
  865. }
  866. .student-card-left{
  867. display: flex;
  868. align-items: center;
  869. width: 70%;
  870. }
  871. .student-avatar {
  872. width: 124rpx;
  873. height: 124rpx;
  874. border-radius: 50%;
  875. margin-right: 20rpx;
  876. border: 2rpx solid #eee;
  877. flex-shrink: 0;
  878. }
  879. .student-info {
  880. height: 100rpx;
  881. display: flex;
  882. flex-direction: column;
  883. justify-content: flex-start;
  884. width: calc(100% - 124rpx);
  885. }
  886. .student-name {
  887. font-size: 36rpx;
  888. font-weight: bold;
  889. color: #333333;
  890. margin-bottom: 8rpx;
  891. white-space: nowrap;
  892. overflow: hidden;
  893. text-overflow: ellipsis;
  894. }
  895. .student-class {
  896. font-size: 30rpx;
  897. color: #666666;
  898. }
  899. .scan-btn{
  900. display: flex;
  901. justify-content: center;
  902. align-items: center;
  903. flex-direction: column;
  904. color:#999999;
  905. font-size: 32rpx;
  906. gap: 4rpx;
  907. border-left: 2rpx solid #dcdcdc;
  908. padding-left: 40rpx;
  909. padding-right: 10rpx;
  910. flex-shrink: 0;
  911. }
  912. .function-list {
  913. display: grid;
  914. grid-template-columns: repeat(4, 1fr);
  915. gap: 30rpx;
  916. // padding: 0 20rpx;
  917. }
  918. .function-item {
  919. display: flex;
  920. flex-direction: column;
  921. align-items: center;
  922. font-size: 28.87rpx;
  923. color: #333333;
  924. }
  925. .function-icon {
  926. position: relative;
  927. width: 100rpx;
  928. height: 100rpx;
  929. background: #ffffff;
  930. border: 2rpx solid #e9e9e9;
  931. box-shadow: 5rpx 5rpx 10rpx rgba(0, 0, 0, .3);
  932. border-radius: 10rpx;
  933. display: flex;
  934. align-items: center;
  935. justify-content: center;
  936. margin-bottom: 10rpx;
  937. }
  938. .function-icon image {
  939. width: 60rpx;
  940. height: 60rpx;
  941. }
  942. .badge {
  943. position: absolute;
  944. top: 1rpx;
  945. right: 1rpx;
  946. width: 20rpx;
  947. height: 20rpx;
  948. background: #eb6100;
  949. border-radius: 50%;
  950. }
  951. .function-name {
  952. font-size: 26rpx;
  953. color: #333333;
  954. }
  955. .login-btn {
  956. width: 100%;
  957. height: 88rpx;
  958. line-height: 88rpx;
  959. background: #666666;
  960. color: #fff;
  961. border-radius: 44rpx;
  962. font-size: 32rpx;
  963. border: none;
  964. }
  965. .login-btn::after {
  966. border: none;
  967. }
  968. .login-btn:active {
  969. opacity: 0.8;
  970. }
  971. .unlogin {
  972. width: 100%;
  973. height: calc(100vh - 226rpx);
  974. image {
  975. width: 100%;
  976. height: 100%;
  977. }
  978. }
  979. </style>