index.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  1. <template>
  2. <view class="content" :class="screenMode">
  3. <view class="split-layout" v-if="screenMode === 'heng'">
  4. <!-- 左侧联系人列表 -->
  5. <view class="left-panel">
  6. <view class="header">
  7. <view class="header-left">
  8. <image class="avatar" :src="studentAvatar"></image>
  9. <text class="greeting">{{ studentName }},你好!</text>
  10. </view>
  11. <view class="logout-btn" @click="killBackgroundApp">
  12. <image src="/static/icon/logout.png"></image>
  13. </view>
  14. </view>
  15. <view class="contact-list">
  16. <view class="contact-card" v-for="contact in contacts" :key="contact.id"
  17. :class="{ 'active': currentContact?.id === contact.id }">
  18. <view class="contact-info">
  19. <image class="contact-avatar" :src="contact.avatar"></image>
  20. <text class="contact-name">{{ contact.username }}</text>
  21. </view>
  22. <view class="action-buttons">
  23. <view class="action-btn" @click.stop="startAudioCall(contact)">
  24. <view class="action-btn-icon">
  25. <image src="/static/icon/audio.png"></image>
  26. </view>
  27. <text>音频</text>
  28. </view>
  29. <view class="action-btn" @click.stop="startVideoCall(contact)">
  30. <view class="action-btn-icon">
  31. <image src="/static/icon/video.png"></image>
  32. </view>
  33. <text>视频</text>
  34. </view>
  35. <view class="action-btn" :class="{ 'message-point': contact.hasPoints }"
  36. @click="selectContact(contact)">
  37. <view class="action-btn-icon">
  38. <image src="/static/icon/message.png"></image>
  39. </view>
  40. <text>留言</text>
  41. </view>
  42. </view>
  43. </view>
  44. </view>
  45. <!-- 个人服务状态 -->
  46. <view class="service-status" v-if="serviceStatus">
  47. <view class="service-status-header">
  48. <text class="service-status-title">服务状态</text>
  49. </view>
  50. <view class="service-status-content">
  51. <view class="status-item" v-if="serviceStatus.sfmf === 1">
  52. <text class="status-label">免费服务</text>
  53. </view>
  54. <view class="status-item" v-if="serviceStatus.zdsc > 0">
  55. <text class="status-label">时长</text>
  56. <text class="status-value">{{ serviceStatus.ljsc || 0 }}/{{ serviceStatus.zdsc }}分钟</text>
  57. </view>
  58. <view class="status-item" v-if="serviceStatus.zdcs > 0">
  59. <text class="status-label">次数</text>
  60. <text class="status-value">{{ serviceStatus.ljcs || 0 }}/{{ serviceStatus.zdcs }}次</text>
  61. </view>
  62. <view class="status-item" v-if="serviceStatus.zdll > 0">
  63. <text class="status-label">流量</text>
  64. <text class="status-value">{{ serviceStatus.ljll || 0 }}/{{ serviceStatus.zdll }}MB</text>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 右侧消息面板 -->
  70. <view class="right-panel">
  71. <message-page v-if="currentContact" :contact-id="currentContact.id" role="device"></message-page>
  72. <!-- <view v-else class="empty-message">
  73. <text>请选择联系人开始聊天</text>
  74. </view> -->
  75. </view>
  76. </view>
  77. <view class="split-layout" v-else>
  78. <!-- 头部欢迎语 -->
  79. <view class="header">
  80. <view class="header-left">
  81. <image class="avatar" :src="studentAvatar"></image>
  82. <text class="greeting">{{ studentName }},你好!</text>
  83. </view>
  84. <view class="logout-btn" @click="killBackgroundApp">
  85. <image src="/static/icon/logout.png"></image>
  86. </view>
  87. </view>
  88. <!-- 调试信息 -->
  89. <!-- <view class="debug-info" @click="startCall">
  90. <text>Options1: {{ JSON.stringify(options) }}</text>
  91. </view> -->
  92. <!-- 联系人卡片列表 -->
  93. <view class="contact-list">
  94. <view class="contact-card" v-for="contact in contacts" :key="contact.id">
  95. <view class="contact-info">
  96. <image class="contact-avatar" :src="contact.avatar"></image>
  97. <text class="contact-name">{{ contact.username }}</text>
  98. </view>
  99. <view class="action-buttons">
  100. <view class="action-btn" @click="startAudioCall(contact)">
  101. <view class="action-btn-icon">
  102. <image src="/static/icon/audio.png"></image>
  103. </view>
  104. <text>音频</text>
  105. </view>
  106. <view class="action-btn" @click="startVideoCall(contact)">
  107. <view class="action-btn-icon">
  108. <image src="/static/icon/video.png"></image>
  109. </view>
  110. <text>视频</text>
  111. </view>
  112. <view class="action-btn" :class="{ 'message-point': contact.hasPoints }"
  113. @click="goToMessage(contact.id)">
  114. <view class="action-btn-icon">
  115. <image src="/static/icon/message.png"></image>
  116. </view>
  117. <text>留言</text>
  118. </view>
  119. </view>
  120. </view>
  121. </view>
  122. <!-- 个人服务状态 -->
  123. <view class="service-status" v-if="serviceStatus">
  124. <view class="service-status-header">
  125. <text class="service-status-title">服务状态</text>
  126. </view>
  127. <view class="service-status-content">
  128. <view class="status-item" v-if="serviceStatus.sfmf === 1">
  129. <text class="status-label">免费服务</text>
  130. </view>
  131. <view class="status-item" v-if="serviceStatus.zdsc > 0">
  132. <text class="status-label">时长</text>
  133. <text class="status-value">{{ serviceStatus.ljsc || 0 }}/{{ serviceStatus.zdsc }}分钟</text>
  134. </view>
  135. <view class="status-item" v-if="serviceStatus.zdcs > 0">
  136. <text class="status-label">次数</text>
  137. <text class="status-value">{{ serviceStatus.ljcs || 0 }}/{{ serviceStatus.zdcs }}次</text>
  138. </view>
  139. <view class="status-item" v-if="serviceStatus.zdll > 0">
  140. <text class="status-label">流量</text>
  141. <text class="status-value">{{ serviceStatus.ljll || 0 }}/{{ serviceStatus.zdll }}MB</text>
  142. </view>
  143. </view>
  144. </view>
  145. </view>
  146. <!-- 自定义Modal -->
  147. <custom-modal
  148. :visible="modalVisible"
  149. :title="modalTitle"
  150. :content="modalContent"
  151. :showCancel="modalShowCancel"
  152. :confirmText="modalConfirmText"
  153. @confirm="handleModalConfirm"
  154. @cancel="handleModalCancel"
  155. />
  156. </view>
  157. </template>
  158. <script>
  159. import messagePage from '../parent/message.vue'
  160. import customModal from '@/components/custom-modal.vue'
  161. import { deviceApi } from '@/api/device'
  162. import { getImageUrl } from '@/utils/util'
  163. const wmpfVoip = requirePlugin('wmpf-voip').default
  164. const isWmpf = (typeof wmpf !== 'undefined')
  165. // 指定接听方使用的小程序版本。formal/正式版(默认);trial/体验版;developer/开发版
  166. const miniprogramState = (() => {
  167. const accountInfo = wx.getAccountInfoSync();
  168. if (accountInfo && accountInfo.miniProgram) {
  169. const platform = { develop: 'developer', trial: 'trial', release: 'formal' }
  170. return platform[accountInfo.miniProgram.envVersion]
  171. }
  172. })()
  173. export default {
  174. components: {
  175. messagePage,
  176. customModal
  177. },
  178. data() {
  179. return {
  180. studentAvatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132',
  181. studentName: '默认学生',
  182. title: '家庭联系',
  183. screenMode: 'heng',
  184. currentContact: null,
  185. contacts: [],
  186. serviceStatus: null, // 个人服务状态
  187. _voipEndPathSet: false, // 记录是否已设置通话结束跳转页面
  188. studentId: '1',
  189. sn: 'A1000006B624470',
  190. inactivityTimer: null,
  191. inactivityTimeout: 180000, // 3分钟
  192. options: null,
  193. // 自定义Modal相关
  194. modalVisible: false,
  195. modalTitle: '提示',
  196. modalContent: '',
  197. modalShowCancel: false,
  198. modalConfirmText: '确定',
  199. modalResolve: null, // 用于Promise
  200. }
  201. },
  202. async onLoad(options) {
  203. // console.log('Device onLoad', options)
  204. this.options = options
  205. // 🔧 关闭可能存在的旧Modal
  206. this.modalVisible = false
  207. if (this.modalResolve) {
  208. this.modalResolve(false) // 取消旧的Promise
  209. this.modalResolve = null
  210. }
  211. uni.getSystemInfo({
  212. success: (res) => {
  213. this.screenMode = res.windowWidth > res.windowHeight ? 'heng' : 'shu'
  214. }
  215. })
  216. // 🎯 检查是否从通话结束返回
  217. if (options && options.fromCall === 'true') {
  218. console.log('📞 检测到通话结束返回,准备记录通话')
  219. await this.recordCallAndRefresh(options)
  220. return // 处理完通话记录后直接返回
  221. }
  222. // 🎯 检查是否已有登录信息(从通话结束页面返回的情况)
  223. const userInfo = uni.getStorageSync('userInfo')
  224. if (userInfo && userInfo.devId && (!options || !options.sn)) {
  225. // 已登录且没有传递新的 sn,说明是从其他页面返回
  226. console.log('✅ 检测到已登录,使用缓存数据')
  227. // 恢复学生信息
  228. if (userInfo.xm) {
  229. this.studentName = userInfo.xm
  230. }
  231. if (userInfo.yszwj) {
  232. this.studentAvatar = getImageUrl(userInfo.yszwj)
  233. }
  234. // 恢复 sn
  235. this.sn = userInfo.devId
  236. // 获取联系人列表
  237. await this.getContactList()
  238. // 检查个人服务状态
  239. await this.checkServiceStatus(115)
  240. // 注册通话事件
  241. this.registerVoipEvent()
  242. this.setVoipEndPagePath()
  243. // 初始化无操作检测
  244. this.resetInactivityTimer()
  245. // 添加全局点击事件监听
  246. const pages = getCurrentPages();
  247. const currentPage = pages[pages.length - 1];
  248. if (currentPage.$getAppWebview) {
  249. const webview = currentPage.$getAppWebview();
  250. webview.addEventListener('click', () => {
  251. this.handleUserActivity();
  252. });
  253. }
  254. return // 直接返回,不需要重新登录
  255. }
  256. // 🆕 新登录流程:从 options 获取 sn 和 cardNo
  257. if (options && options.sn) {
  258. this.sn = options.sn
  259. }
  260. const cardNo = options && options.cardNo ? options.cardNo : ''
  261. // 可选的 studentId 参数
  262. if (options && options.studentId) {
  263. this.studentId = options.studentId
  264. }
  265. // 第一步: 设备登录
  266. const loginSuccess = await this.deviceLogin(this.sn, cardNo)
  267. // 登录成功后才继续后续操作
  268. if (!loginSuccess) {
  269. return // 登录失败,直接返回
  270. }
  271. this.registerVoipEvent()
  272. // 初始化无操作检测
  273. this.resetInactivityTimer()
  274. // 添加全局点击事件监听
  275. const pages = getCurrentPages();
  276. const currentPage = pages[pages.length - 1];
  277. if (currentPage.$getAppWebview) {
  278. const webview = currentPage.$getAppWebview();
  279. webview.addEventListener('click', () => {
  280. this.handleUserActivity();
  281. });
  282. }
  283. },
  284. onUnload() {
  285. // 清理定时器
  286. if (this.inactivityTimer) {
  287. clearTimeout(this.inactivityTimer);
  288. }
  289. },
  290. methods: {
  291. // 显示自定义Modal
  292. showCustomModal(options = {}) {
  293. return new Promise((resolve) => {
  294. this.modalTitle = options.title || '提示'
  295. this.modalContent = options.content || ''
  296. this.modalShowCancel = options.showCancel || false
  297. this.modalConfirmText = options.confirmText || '确定'
  298. this.modalResolve = resolve
  299. this.modalVisible = true
  300. })
  301. },
  302. // 处理Modal确认
  303. handleModalConfirm() {
  304. this.modalVisible = false
  305. if (this.modalResolve) {
  306. this.modalResolve(true)
  307. this.modalResolve = null
  308. }
  309. },
  310. // 处理Modal取消
  311. handleModalCancel() {
  312. this.modalVisible = false
  313. if (this.modalResolve) {
  314. this.modalResolve(false)
  315. this.modalResolve = null
  316. }
  317. },
  318. // 设备登录
  319. async deviceLogin(sn, cardNo) {
  320. try {
  321. const result = await deviceApi.login(sn, cardNo)
  322. if (result && result.data) {
  323. const loginData = result.data
  324. // 🚫 检查登录失败的情况(废卡/登录过期等)
  325. if (loginData.msg) {
  326. // 有 msg 字段说明登录失败
  327. // 弹出自定义确认框
  328. const confirmed = await this.showCustomModal({
  329. title: '登录失败',
  330. content: '登录失败,卡无登记',
  331. showCancel: false,
  332. confirmText: '确定'
  333. })
  334. // 用户确认后,调用退出
  335. if (confirmed) {
  336. await this.killBackgroundApp()
  337. }
  338. return false
  339. }
  340. // 🎯 参考 H5 login.html 的 userData 构建方式(304-314行)
  341. // 后端返回的数据没有 userInfo 层级,需要手动构建
  342. const userData = {
  343. devId: loginData.devId,
  344. sbmc: loginData.sbmc,
  345. sessId: loginData.sessId,
  346. xm: loginData.xm,
  347. yhsbToken: loginData.yhsbToken,
  348. onlineToken: loginData.onlineToken,
  349. syList: loginData.sylist || loginData.syList,
  350. yhid: loginData.yhid,
  351. yhm: loginData.yhm
  352. }
  353. // 可选字段:如果存在则添加
  354. if (loginData.yszwj) {
  355. userData.yszwj = loginData.yszwj
  356. }
  357. if (loginData.zjzwj) {
  358. userData.zjzwj = loginData.zjzwj
  359. }
  360. // 保存完整的用户信息
  361. uni.setStorageSync('userInfo', userData)
  362. // 保存 JSESSIONID
  363. if (userData.sessId) {
  364. uni.setStorageSync('JSESSIONID', userData.sessId)
  365. } else {
  366. console.warn('⚠️ 登录数据中没有 sessId')
  367. }
  368. // 更新页面显示的学生信息
  369. if (userData.xm) {
  370. this.studentName = userData.xm
  371. }
  372. if (userData.yszwj){
  373. this.studentAvatar = getImageUrl(userData.yszwj)
  374. }
  375. // 如果登录返回了学生ID,更新当前学生ID
  376. if (loginData.studentId) {
  377. this.studentId = loginData.studentId
  378. }
  379. // ✅ 登录成功,获取联系人列表
  380. await this.getContactList()
  381. // 检查个人服务状态
  382. await this.checkServiceStatus(115)
  383. console.log('✅ 设备登录成功:', userData.xm)
  384. return true
  385. } else {
  386. throw new Error('登录返回数据格式错误')
  387. }
  388. } catch (error) {
  389. console.error('❌ 设备登录失败:', error)
  390. // 弹出自定义确认框
  391. await this.showCustomModal({
  392. title: '登录失败',
  393. content: '网络错误或服务异常',
  394. showCancel: false,
  395. confirmText: '确定'
  396. })
  397. // 用户确认后,调用退出
  398. await this.killBackgroundApp()
  399. return false
  400. }
  401. },
  402. // 通用通话方法,供音频和视频通话共用
  403. async startCall(contact, roomType) {
  404. // 显示loading弹窗
  405. uni.showLoading({
  406. title: '呼叫中...',
  407. mask: true // 添加遮罩,防止触摸穿透
  408. });
  409. // 记录当前联系人
  410. this.currentContact = contact;
  411. // 清除可能的上次通话记录,确保本次通话信息独立
  412. if (uni.getStorageSync('currcall')) {
  413. uni.removeStorageSync('currcall');
  414. }
  415. // 先存储基本联系人信息
  416. uni.setStorageSync('lastCallInfo', {
  417. name: contact.username || '未知联系人',
  418. avatar: contact.avatar || '/static/logo.png',
  419. duration: 0,
  420. time: new Date().toLocaleString(),
  421. type: roomType,
  422. });
  423. this.setVoipEndPagePath()
  424. try {
  425. const res = await wmpfVoip.initByCaller({
  426. roomType: roomType,
  427. caller: {
  428. id: this.sn,
  429. },
  430. listener: {
  431. id: contact.openid,
  432. name: contact.username
  433. },
  434. businessType: 1,
  435. miniprogramState: miniprogramState
  436. })
  437. console.log("res:",res)
  438. if (res.isSuccess) {
  439. uni.hideLoading();
  440. uni.redirectTo({
  441. url: wmpfVoip.CALL_PAGE_PATH
  442. })
  443. return true
  444. } else {
  445. uni.hideLoading();
  446. console.error("呼叫失败,res:",res)
  447. uni.showToast({
  448. title: '呼叫失败1',
  449. icon: 'error'
  450. })
  451. return false
  452. }
  453. } catch (error) {
  454. uni.hideLoading();
  455. console.error('通话错误:', error)
  456. uni.showToast({
  457. title: '发起通话失败',
  458. icon: 'none'
  459. })
  460. return false
  461. }
  462. },
  463. // 设置通话结束跳转地址
  464. setVoipEndPagePath(forceUpdate) {
  465. if (this._voipEndPathSet && !forceUpdate) return;
  466. if (wmpfVoip) {
  467. const callInfo = uni.getStorageSync('lastCallInfo') || {};
  468. // 构建参数:如果需要记录通话,传递参数给首页
  469. let options = '';
  470. if (callInfo.needRecord && callInfo.duration > 0) {
  471. options = `fromCall=true&duration=${callInfo.duration}&callType=${callInfo.type || 'voice'}`;
  472. console.log('🔗 设置通话结束跳转参数:', options);
  473. }
  474. wmpfVoip.setVoipEndPagePath({
  475. url: '/pages/device/index', // 通话结束直接返回首页
  476. key: 'Call', // 设置业务类型为通话
  477. options: options, // 传递通话数据
  478. routeType: 'redirectTo' // 使用redirectTo方式跳转
  479. });
  480. this._voipEndPathSet = true; // 记录已设置状态
  481. }
  482. },
  483. registerVoipEvent() {
  484. wmpfVoip.onVoipEvent((event) => {
  485. const eventName = event.eventName;
  486. // 开始通话时清除无操作定时器
  487. if (eventName === 'startVoip') {
  488. clearTimeout(this.inactivityTimer);
  489. }
  490. // 定义不同类型的结束事件
  491. const hangupEvent = ['hangUpVoip', 'endVoip']; // 正常挂断
  492. const cancelEvent = ['cancelVoip']; // 主动取消
  493. const timeoutEvent = ['timeout']; // 超时未接听
  494. const rejectEvent = ['rejectVoip']; // 拒绝接听
  495. const callInfo = uni.getStorageSync('lastCallInfo') || {};
  496. // 处理正常通话结束
  497. if (hangupEvent.includes(eventName)) {
  498. callInfo.duration = event.data?.keepTime || 0;
  499. callInfo.status = callInfo.duration > 0 ? '通话已结束' : '未接通';
  500. // 如果有通话时长,标记需要记录到后端
  501. if (callInfo.duration > 0) {
  502. callInfo.needRecord = true;
  503. console.log('✅ 通话结束,时长:', callInfo.duration, '秒,类型:', callInfo.type);
  504. }
  505. }
  506. // 处理取消通话
  507. else if (cancelEvent.includes(eventName)) {
  508. callInfo.duration = 0;
  509. callInfo.status = '已取消';
  510. }
  511. // 处理超时未接听
  512. else if (timeoutEvent.includes(eventName)) {
  513. callInfo.duration = 0;
  514. callInfo.status = '未接听';
  515. }
  516. // 处理拒绝接听
  517. else if (rejectEvent.includes(eventName)) {
  518. callInfo.duration = 0;
  519. callInfo.status = '已拒绝';
  520. }
  521. // 如果是任何一种结束事件,保存信息并更新跳转页面
  522. if ([...hangupEvent, ...cancelEvent, ...timeoutEvent, ...rejectEvent].includes(eventName)) {
  523. callInfo.endType = eventName;
  524. callInfo.endTime = Date.now();
  525. uni.hideLoading();
  526. uni.setStorageSync('lastCallInfo', callInfo);
  527. this.setVoipEndPagePath(true);
  528. this.resetInactivityTimer();
  529. }
  530. })
  531. },
  532. async startAudioCall(contact) {
  533. await this.startCall(contact, 'voice')
  534. },
  535. async startVideoCall(contact) {
  536. await this.startCall(contact, 'video')
  537. },
  538. // 杀后台方法 by xu 2025-12-29
  539. async killBackgroundApp() {
  540. try {
  541. // 🧹 清理登录信息
  542. uni.removeStorageSync('userInfo')
  543. uni.removeStorageSync('JSESSIONID')
  544. // 🧹 清理通话相关缓存
  545. uni.removeStorageSync('currcall')
  546. uni.removeStorageSync('lastCallInfo')
  547. // 提示退出成功 by xu 2025-12-29
  548. uni.showToast({
  549. title: '已退出',
  550. icon: 'success',
  551. duration: 1500
  552. });
  553. // 延迟跳转到通知页 by xu 2025-12-29
  554. setTimeout(() => {
  555. uni.redirectTo({
  556. url: '/pages/device/notice'
  557. });
  558. }, 1500);
  559. } catch (error) {
  560. console.error('❌ 退出失败:', error);
  561. uni.showToast({
  562. title: '退出失败',
  563. icon: 'error'
  564. });
  565. }
  566. },
  567. // 重置无操作计时器
  568. resetInactivityTimer() {
  569. if (this.inactivityTimer) {
  570. clearTimeout(this.inactivityTimer);
  571. }
  572. this.inactivityTimer = setTimeout(() => {
  573. this.killBackgroundApp();
  574. }, this.inactivityTimeout);
  575. },
  576. // 监听用户活动的方法
  577. handleUserActivity() {
  578. this.resetInactivityTimer();
  579. },
  580. selectContact(contact) {
  581. this.currentContact = contact
  582. },
  583. async getContactList() {
  584. try {
  585. // 不需要传参数,device-request.js 会自动从 userInfo 获取 devId 和 sbmc
  586. const result = await deviceApi.selParentInfo()
  587. if (result && result.data && result.data.ssData) {
  588. const parent = result.data.ssData
  589. // 处理头像:如果有 yszwj 就用 getImageUrl 转换,否则用默认头像
  590. const defaultAvatar = 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132'
  591. const avatar = parent.yszwj ? getImageUrl(parent.yszwj) : defaultAvatar
  592. // 现在只有一个家长,直接赋值为数组(兼容现有页面结构)
  593. this.contacts = [{
  594. id: parent.ryid, // 人员ID
  595. username: parent.xm, // 姓名
  596. avatar: avatar, // 艺术照(头像)
  597. openid: parent.wbid, // 外部ID(微信openid)
  598. hasPoints: false // 是否有未读留言(默认false)
  599. }]
  600. console.log('✅ 获取联系人成功:', parent)
  601. }
  602. } catch (error) {
  603. console.error('❌ 获取联系人失败:', error)
  604. }
  605. },
  606. goToMessage(contactId) {
  607. uni.navigateTo({
  608. url: '/pages/parent/message?contactId=' + contactId + "&role=device"
  609. })
  610. },
  611. // 检查个人服务状态
  612. async checkServiceStatus(grfwxmm = 115) {
  613. try {
  614. const result = await deviceApi.grfw_chkGrfw(grfwxmm)
  615. console.log('检查服务状态返回:', result)
  616. if (result && result.data) {
  617. const data = result.data
  618. if (data.ssCode === 0 && data.ssData) {
  619. // 成功获取服务状态
  620. this.serviceStatus = data.ssData
  621. console.log('✅ 服务状态:', this.serviceStatus)
  622. } else if (data.ssCode > 0) {
  623. // 服务异常
  624. console.warn('⚠️ 服务状态异常:', data.ssMsg)
  625. this.serviceStatus = null
  626. }
  627. }
  628. } catch (error) {
  629. console.error('❌ 检查服务状态失败:', error)
  630. this.serviceStatus = null
  631. }
  632. },
  633. // 记录通话并刷新服务状态
  634. async recordCallAndRefresh(callOptions) {
  635. try {
  636. const duration = parseInt(callOptions.duration) || 0 // 秒
  637. const callType = callOptions.callType || 'voice'
  638. if (duration <= 0) {
  639. console.log('⚠️ 通话时长为0,不记录')
  640. await this.loadPageAfterCall()
  641. return
  642. }
  643. // 转换为分钟,不满1分钟按1分钟算(向上取整)
  644. const minutes = Math.ceil(duration / 60)
  645. console.log(`📊 通话时长: ${duration}秒 → ${minutes}分钟(向上取整)`)
  646. // 根据通话类型确定服务项目码
  647. const grfwxmm = callType === 'video' ? 115 : 111 // 115=视频电话, 111=音频电话
  648. // 调用接口记录通话
  649. console.log(`📝 记录通话: grfwxmm=${grfwxmm}, 时长=${minutes}分钟`)
  650. await deviceApi.grfw_endGrfw({
  651. grfwxmm: grfwxmm,
  652. sc: minutes, // 时长(分钟)
  653. ll: 0, // 流量
  654. ms: `通话${duration}秒` // 备注
  655. })
  656. console.log('✅ 通话记录成功')
  657. // 重新加载服务状态
  658. await this.checkServiceStatus(grfwxmm)
  659. // 清理通话记录标记
  660. const callInfo = uni.getStorageSync('lastCallInfo') || {}
  661. delete callInfo.needRecord
  662. uni.setStorageSync('lastCallInfo', callInfo)
  663. } catch (error) {
  664. console.error('❌ 记录通话失败:', error)
  665. uni.showToast({
  666. title: '记录通话失败',
  667. icon: 'none'
  668. })
  669. } finally {
  670. await this.loadPageAfterCall()
  671. }
  672. },
  673. // 通话结束后加载页面内容
  674. async loadPageAfterCall() {
  675. const userInfo = uni.getStorageSync('userInfo')
  676. if (userInfo) {
  677. if (userInfo.xm) {
  678. this.studentName = userInfo.xm
  679. }
  680. if (userInfo.yszwj) {
  681. this.studentAvatar = getImageUrl(userInfo.yszwj)
  682. }
  683. this.sn = userInfo.devId
  684. }
  685. await this.getContactList()
  686. this.registerVoipEvent()
  687. this.setVoipEndPagePath()
  688. this.resetInactivityTimer()
  689. const pages = getCurrentPages();
  690. const currentPage = pages[pages.length - 1];
  691. if (currentPage.$getAppWebview) {
  692. const webview = currentPage.$getAppWebview();
  693. webview.addEventListener('click', () => {
  694. this.handleUserActivity();
  695. });
  696. }
  697. },
  698. }
  699. }
  700. </script>
  701. <style scoped>
  702. .heng .split-layout {
  703. display: flex;
  704. height: 100vh;
  705. }
  706. .heng .left-panel {
  707. width: 50%;
  708. border-right: 2rpx solid #dcdcdc;
  709. background: #f5f5f5;
  710. overflow-y: auto;
  711. }
  712. .heng .right-panel {
  713. flex: 1;
  714. background: #fff;
  715. height: 100%;
  716. overflow-y: auto;
  717. }
  718. .heng .empty-message {
  719. height: 100%;
  720. display: flex;
  721. align-items: center;
  722. justify-content: center;
  723. color: #999;
  724. font-size: 32rpx;
  725. }
  726. .heng .header {
  727. display: flex;
  728. align-items: center;
  729. padding: 16rpx 20rpx 10rpx;
  730. /* height: 200rpx; */
  731. border-bottom: 1rpx solid #d2d2d2;
  732. background: #f5f5f5;
  733. box-sizing: border-box;
  734. justify-content: space-between;
  735. }
  736. .heng .avatar {
  737. width: 42rpx;
  738. height: 42rpx;
  739. border-radius: 50%;
  740. margin-right: 8rpx;
  741. }
  742. .heng .header-left {
  743. display: flex;
  744. align-items: center;
  745. }
  746. .heng .logout-btn image {
  747. width: 20rpx;
  748. height: 20rpx;
  749. }
  750. .heng .greeting {
  751. font-size: 14rpx;
  752. color: #333;
  753. }
  754. .heng .contact-list {
  755. margin-top: 20rpx;
  756. display: flex;
  757. flex-direction: column;
  758. justify-content: center;
  759. gap: 15rpx;
  760. padding: 0 12rpx;
  761. }
  762. .heng .contact-card {
  763. width: calc(100% - 20rpx);
  764. /* margin: 16rpx 20rpx; */
  765. padding: 8rpx;
  766. background-color: #fff;
  767. border-radius: 3rpx;
  768. display: flex;
  769. align-items: center;
  770. background: #f5f5f5;
  771. border: 1px solid #dddfe6;
  772. justify-content: space-between;
  773. position: relative;
  774. /* 添加相对定位 */
  775. }
  776. .heng .contact-card.active::after {
  777. content: '';
  778. position: absolute;
  779. right: -13rpx;
  780. top: 50%;
  781. transform: translateY(-50%);
  782. width: 0;
  783. height: 0;
  784. border-top: 8rpx solid transparent;
  785. border-bottom: 8rpx solid transparent;
  786. border-left: 8rpx solid #d2d2d2;
  787. }
  788. .heng .contact-info {
  789. display: flex;
  790. align-items: center;
  791. }
  792. .heng .contact-avatar {
  793. width: 52rpx;
  794. height: 52rpx;
  795. border: 1px solid #fff;
  796. margin-right: 8rpx;
  797. border-radius: 0rpx;
  798. }
  799. .heng .contact-name {
  800. font-size: 16rpx;
  801. color: #333;
  802. }
  803. .heng .action-buttons {
  804. display: flex;
  805. justify-content: space-around;
  806. gap: 12rpx;
  807. margin-right: 8rpx;
  808. }
  809. .heng .action-btn {
  810. display: flex;
  811. flex-direction: column;
  812. align-items: center;
  813. }
  814. .heng .action-btn-icon {
  815. width: 32rpx;
  816. height: 32rpx;
  817. margin-bottom: 2rpx;
  818. display: flex;
  819. align-items: center;
  820. justify-content: center;
  821. background: #ffffff;
  822. border: 1px solid #dddfe6;
  823. box-sizing: border-box;
  824. border-radius: 3rpx;
  825. }
  826. .heng .action-btn image {
  827. width: 60%;
  828. height: 60%;
  829. }
  830. .heng .action-btn text {
  831. font-size: 10rpx;
  832. color: #000000;
  833. }
  834. .heng .message-point {
  835. position: relative;
  836. }
  837. .heng .message-point::after {
  838. content: '';
  839. position: absolute;
  840. width: 8rpx;
  841. height: 8rpx;
  842. background-color: #eb6100;
  843. border-radius: 50%;
  844. top: -4rpx;
  845. right: -4rpx;
  846. }
  847. .shu .header {
  848. display: flex;
  849. align-items: center;
  850. padding: 40rpx 50rpx 25rpx;
  851. height: 200rpx;
  852. border-bottom: 2rpx solid #d2d2d2;
  853. background: #f5f5f5;
  854. box-sizing: border-box;
  855. justify-content: space-between;
  856. }
  857. .shu .avatar {
  858. width: 108rpx;
  859. height: 108rpx;
  860. border-radius: 50%;
  861. margin-right: 20rpx;
  862. }
  863. .shu .header-left {
  864. display: flex;
  865. align-items: center;
  866. }
  867. .shu .logout-btn image {
  868. width: 40rpx;
  869. height: 40rpx;
  870. }
  871. .shu .greeting {
  872. font-size: 38rpx;
  873. color: #333;
  874. }
  875. .shu .contact-list {
  876. margin-top: 30rpx;
  877. }
  878. .shu .contact-card {
  879. margin: 40rpx 50rpx;
  880. padding: 20rpx;
  881. background-color: #fff;
  882. border-radius: 8rpx;
  883. display: flex;
  884. align-items: center;
  885. background: #f5f5f5;
  886. border: 1px solid #dddfe6;
  887. justify-content: space-between;
  888. }
  889. .shu .contact-info {
  890. display: flex;
  891. align-items: center;
  892. }
  893. .shu .contact-avatar {
  894. width: 128rpx;
  895. height: 128rpx;
  896. border: 1px solid #fff;
  897. margin-right: 20rpx;
  898. border-radius: 0rpx;
  899. }
  900. .shu .contact-name {
  901. font-size: 32rpx;
  902. color: #333;
  903. }
  904. .shu .action-buttons {
  905. display: flex;
  906. justify-content: space-around;
  907. gap: 30rpx;
  908. /* margin-right: 20rpx; */
  909. }
  910. .shu .action-btn {
  911. display: flex;
  912. flex-direction: column;
  913. align-items: center;
  914. }
  915. .shu .action-btn-icon {
  916. width: 80rpx;
  917. height: 80rpx;
  918. margin-bottom: 5rpx;
  919. display: flex;
  920. align-items: center;
  921. justify-content: center;
  922. background: #ffffff;
  923. border: 1px solid #dddfe6;
  924. box-sizing: border-box;
  925. border-radius: 10rpx;
  926. }
  927. .shu .action-btn image {
  928. width: 60%;
  929. height: 60%;
  930. }
  931. .shu .action-btn text {
  932. font-size: 24rpx;
  933. color: #000000;
  934. }
  935. .shu .message-point {
  936. position: relative;
  937. }
  938. .shu .message-point::after {
  939. content: '';
  940. position: absolute;
  941. width: 18rpx;
  942. height: 18rpx;
  943. background-color: #eb6100;
  944. border-radius: 50%;
  945. top: -9rpx;
  946. right: -9rpx;
  947. }
  948. .debug-info {
  949. padding: 20rpx;
  950. background: #fff3cd;
  951. margin: 20rpx;
  952. border-radius: 8rpx;
  953. word-break: break-all;
  954. }
  955. .debug-info text {
  956. font-size: 24rpx;
  957. color: #856404;
  958. }
  959. /* 横屏服务状态样式 */
  960. .heng .service-status {
  961. margin: 12rpx;
  962. padding: 10rpx 12rpx;
  963. background: #fff;
  964. border-radius: 6rpx;
  965. border: 1px solid #dddfe6;
  966. }
  967. .heng .service-status-header {
  968. margin-bottom: 8rpx;
  969. }
  970. .heng .service-status-title {
  971. font-size: 12rpx;
  972. color: #666;
  973. font-weight: bold;
  974. }
  975. .heng .service-status-content {
  976. display: flex;
  977. flex-wrap: wrap;
  978. gap: 8rpx;
  979. }
  980. .heng .status-item {
  981. display: flex;
  982. align-items: center;
  983. gap: 4rpx;
  984. padding: 4rpx 8rpx;
  985. background: #f0f9ff;
  986. border-radius: 4rpx;
  987. }
  988. .heng .status-label {
  989. font-size: 10rpx;
  990. color: #666;
  991. }
  992. .heng .status-value {
  993. font-size: 10rpx;
  994. color: #07c160;
  995. font-weight: bold;
  996. }
  997. /* 竖屏服务状态样式 */
  998. .shu .service-status {
  999. margin: 30rpx 50rpx;
  1000. padding: 24rpx;
  1001. background: #fff;
  1002. border-radius: 12rpx;
  1003. border: 1px solid #dddfe6;
  1004. }
  1005. .shu .service-status-header {
  1006. margin-bottom: 16rpx;
  1007. }
  1008. .shu .service-status-title {
  1009. font-size: 28rpx;
  1010. color: #666;
  1011. font-weight: bold;
  1012. }
  1013. .shu .service-status-content {
  1014. display: flex;
  1015. flex-wrap: wrap;
  1016. gap: 20rpx;
  1017. }
  1018. .shu .status-item {
  1019. display: flex;
  1020. align-items: center;
  1021. gap: 10rpx;
  1022. padding: 12rpx 20rpx;
  1023. background: #f0f9ff;
  1024. border-radius: 8rpx;
  1025. }
  1026. .shu .status-label {
  1027. font-size: 26rpx;
  1028. color: #666;
  1029. }
  1030. .shu .status-value {
  1031. font-size: 26rpx;
  1032. color: #07c160;
  1033. font-weight: bold;
  1034. }
  1035. </style>