index.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  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. // 杀后台方法
  539. async killBackgroundApp() {
  540. try {
  541. // 🧹 清理登录信息
  542. uni.removeStorageSync('userInfo')
  543. uni.removeStorageSync('JSESSIONID')
  544. // 🧹 清理通话相关缓存
  545. uni.removeStorageSync('currcall')
  546. uni.removeStorageSync('lastCallInfo')
  547. // 判断是否在真机环境
  548. if (typeof wmpf !== 'undefined') {
  549. await wmpf.Channel.invoke({
  550. command: 'killBackgroundApp',
  551. data: {
  552. sn: this.sn
  553. },
  554. success: (res) => {
  555. uni.showToast({
  556. title: '已退出',
  557. icon: 'success'
  558. });
  559. }
  560. });
  561. } else {
  562. // 开发环境也要清理缓存
  563. uni.showToast({
  564. title: '已退出(开发环境)',
  565. icon: 'success'
  566. });
  567. }
  568. } catch (error) {
  569. console.error('❌ 退出失败:', error);
  570. uni.showToast({
  571. title: '退出失败',
  572. icon: 'error'
  573. });
  574. }
  575. },
  576. // 重置无操作计时器
  577. resetInactivityTimer() {
  578. if (this.inactivityTimer) {
  579. clearTimeout(this.inactivityTimer);
  580. }
  581. this.inactivityTimer = setTimeout(() => {
  582. this.killBackgroundApp();
  583. }, this.inactivityTimeout);
  584. },
  585. // 监听用户活动的方法
  586. handleUserActivity() {
  587. this.resetInactivityTimer();
  588. },
  589. selectContact(contact) {
  590. this.currentContact = contact
  591. },
  592. async getContactList() {
  593. try {
  594. // 不需要传参数,device-request.js 会自动从 userInfo 获取 devId 和 sbmc
  595. const result = await deviceApi.selParentInfo()
  596. if (result && result.data && result.data.ssData) {
  597. const parent = result.data.ssData
  598. // 处理头像:如果有 yszwj 就用 getImageUrl 转换,否则用默认头像
  599. const defaultAvatar = 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132'
  600. const avatar = parent.yszwj ? getImageUrl(parent.yszwj) : defaultAvatar
  601. // 现在只有一个家长,直接赋值为数组(兼容现有页面结构)
  602. this.contacts = [{
  603. id: parent.ryid, // 人员ID
  604. username: parent.xm, // 姓名
  605. avatar: avatar, // 艺术照(头像)
  606. openid: parent.wbid, // 外部ID(微信openid)
  607. hasPoints: false // 是否有未读留言(默认false)
  608. }]
  609. console.log('✅ 获取联系人成功:', parent)
  610. }
  611. } catch (error) {
  612. console.error('❌ 获取联系人失败:', error)
  613. }
  614. },
  615. goToMessage(contactId) {
  616. uni.navigateTo({
  617. url: '/pages/parent/message?contactId=' + contactId + "&role=device"
  618. })
  619. },
  620. // 检查个人服务状态
  621. async checkServiceStatus(grfwxmm = 115) {
  622. try {
  623. const result = await deviceApi.grfw_chkGrfw(grfwxmm)
  624. console.log('检查服务状态返回:', result)
  625. if (result && result.data) {
  626. const data = result.data
  627. if (data.ssCode === 0 && data.ssData) {
  628. // 成功获取服务状态
  629. this.serviceStatus = data.ssData
  630. console.log('✅ 服务状态:', this.serviceStatus)
  631. } else if (data.ssCode > 0) {
  632. // 服务异常
  633. console.warn('⚠️ 服务状态异常:', data.ssMsg)
  634. this.serviceStatus = null
  635. }
  636. }
  637. } catch (error) {
  638. console.error('❌ 检查服务状态失败:', error)
  639. this.serviceStatus = null
  640. }
  641. },
  642. // 记录通话并刷新服务状态
  643. async recordCallAndRefresh(callOptions) {
  644. try {
  645. const duration = parseInt(callOptions.duration) || 0 // 秒
  646. const callType = callOptions.callType || 'voice'
  647. if (duration <= 0) {
  648. console.log('⚠️ 通话时长为0,不记录')
  649. await this.loadPageAfterCall()
  650. return
  651. }
  652. // 转换为分钟,不满1分钟按1分钟算(向上取整)
  653. const minutes = Math.ceil(duration / 60)
  654. console.log(`📊 通话时长: ${duration}秒 → ${minutes}分钟(向上取整)`)
  655. // 根据通话类型确定服务项目码
  656. const grfwxmm = callType === 'video' ? 115 : 111 // 115=视频电话, 111=音频电话
  657. // 调用接口记录通话
  658. console.log(`📝 记录通话: grfwxmm=${grfwxmm}, 时长=${minutes}分钟`)
  659. await deviceApi.grfw_endGrfw({
  660. grfwxmm: grfwxmm,
  661. sc: minutes, // 时长(分钟)
  662. ll: 0, // 流量
  663. ms: `通话${duration}秒` // 备注
  664. })
  665. console.log('✅ 通话记录成功')
  666. // 重新加载服务状态
  667. await this.checkServiceStatus(grfwxmm)
  668. // 清理通话记录标记
  669. const callInfo = uni.getStorageSync('lastCallInfo') || {}
  670. delete callInfo.needRecord
  671. uni.setStorageSync('lastCallInfo', callInfo)
  672. } catch (error) {
  673. console.error('❌ 记录通话失败:', error)
  674. uni.showToast({
  675. title: '记录通话失败',
  676. icon: 'none'
  677. })
  678. } finally {
  679. await this.loadPageAfterCall()
  680. }
  681. },
  682. // 通话结束后加载页面内容
  683. async loadPageAfterCall() {
  684. const userInfo = uni.getStorageSync('userInfo')
  685. if (userInfo) {
  686. if (userInfo.xm) {
  687. this.studentName = userInfo.xm
  688. }
  689. if (userInfo.yszwj) {
  690. this.studentAvatar = getImageUrl(userInfo.yszwj)
  691. }
  692. this.sn = userInfo.devId
  693. }
  694. await this.getContactList()
  695. this.registerVoipEvent()
  696. this.setVoipEndPagePath()
  697. this.resetInactivityTimer()
  698. const pages = getCurrentPages();
  699. const currentPage = pages[pages.length - 1];
  700. if (currentPage.$getAppWebview) {
  701. const webview = currentPage.$getAppWebview();
  702. webview.addEventListener('click', () => {
  703. this.handleUserActivity();
  704. });
  705. }
  706. },
  707. }
  708. }
  709. </script>
  710. <style scoped>
  711. .heng .split-layout {
  712. display: flex;
  713. height: 100vh;
  714. }
  715. .heng .left-panel {
  716. width: 50%;
  717. border-right: 2rpx solid #dcdcdc;
  718. background: #f5f5f5;
  719. overflow-y: auto;
  720. }
  721. .heng .right-panel {
  722. flex: 1;
  723. background: #fff;
  724. height: 100%;
  725. overflow-y: auto;
  726. }
  727. .heng .empty-message {
  728. height: 100%;
  729. display: flex;
  730. align-items: center;
  731. justify-content: center;
  732. color: #999;
  733. font-size: 32rpx;
  734. }
  735. .heng .header {
  736. display: flex;
  737. align-items: center;
  738. padding: 16rpx 20rpx 10rpx;
  739. /* height: 200rpx; */
  740. border-bottom: 1rpx solid #d2d2d2;
  741. background: #f5f5f5;
  742. box-sizing: border-box;
  743. justify-content: space-between;
  744. }
  745. .heng .avatar {
  746. width: 42rpx;
  747. height: 42rpx;
  748. border-radius: 50%;
  749. margin-right: 8rpx;
  750. }
  751. .heng .header-left {
  752. display: flex;
  753. align-items: center;
  754. }
  755. .heng .logout-btn image {
  756. width: 20rpx;
  757. height: 20rpx;
  758. }
  759. .heng .greeting {
  760. font-size: 14rpx;
  761. color: #333;
  762. }
  763. .heng .contact-list {
  764. margin-top: 20rpx;
  765. display: flex;
  766. flex-direction: column;
  767. justify-content: center;
  768. gap: 15rpx;
  769. padding: 0 12rpx;
  770. }
  771. .heng .contact-card {
  772. width: calc(100% - 20rpx);
  773. /* margin: 16rpx 20rpx; */
  774. padding: 8rpx;
  775. background-color: #fff;
  776. border-radius: 3rpx;
  777. display: flex;
  778. align-items: center;
  779. background: #f5f5f5;
  780. border: 1px solid #dddfe6;
  781. justify-content: space-between;
  782. position: relative;
  783. /* 添加相对定位 */
  784. }
  785. .heng .contact-card.active::after {
  786. content: '';
  787. position: absolute;
  788. right: -13rpx;
  789. top: 50%;
  790. transform: translateY(-50%);
  791. width: 0;
  792. height: 0;
  793. border-top: 8rpx solid transparent;
  794. border-bottom: 8rpx solid transparent;
  795. border-left: 8rpx solid #d2d2d2;
  796. }
  797. .heng .contact-info {
  798. display: flex;
  799. align-items: center;
  800. }
  801. .heng .contact-avatar {
  802. width: 52rpx;
  803. height: 52rpx;
  804. border: 1px solid #fff;
  805. margin-right: 8rpx;
  806. border-radius: 0rpx;
  807. }
  808. .heng .contact-name {
  809. font-size: 16rpx;
  810. color: #333;
  811. }
  812. .heng .action-buttons {
  813. display: flex;
  814. justify-content: space-around;
  815. gap: 12rpx;
  816. margin-right: 8rpx;
  817. }
  818. .heng .action-btn {
  819. display: flex;
  820. flex-direction: column;
  821. align-items: center;
  822. }
  823. .heng .action-btn-icon {
  824. width: 32rpx;
  825. height: 32rpx;
  826. margin-bottom: 2rpx;
  827. display: flex;
  828. align-items: center;
  829. justify-content: center;
  830. background: #ffffff;
  831. border: 1px solid #dddfe6;
  832. box-sizing: border-box;
  833. border-radius: 3rpx;
  834. }
  835. .heng .action-btn image {
  836. width: 60%;
  837. height: 60%;
  838. }
  839. .heng .action-btn text {
  840. font-size: 10rpx;
  841. color: #000000;
  842. }
  843. .heng .message-point {
  844. position: relative;
  845. }
  846. .heng .message-point::after {
  847. content: '';
  848. position: absolute;
  849. width: 8rpx;
  850. height: 8rpx;
  851. background-color: #eb6100;
  852. border-radius: 50%;
  853. top: -4rpx;
  854. right: -4rpx;
  855. }
  856. .shu .header {
  857. display: flex;
  858. align-items: center;
  859. padding: 40rpx 50rpx 25rpx;
  860. height: 200rpx;
  861. border-bottom: 2rpx solid #d2d2d2;
  862. background: #f5f5f5;
  863. box-sizing: border-box;
  864. justify-content: space-between;
  865. }
  866. .shu .avatar {
  867. width: 108rpx;
  868. height: 108rpx;
  869. border-radius: 50%;
  870. margin-right: 20rpx;
  871. }
  872. .shu .header-left {
  873. display: flex;
  874. align-items: center;
  875. }
  876. .shu .logout-btn image {
  877. width: 40rpx;
  878. height: 40rpx;
  879. }
  880. .shu .greeting {
  881. font-size: 38rpx;
  882. color: #333;
  883. }
  884. .shu .contact-list {
  885. margin-top: 30rpx;
  886. }
  887. .shu .contact-card {
  888. margin: 40rpx 50rpx;
  889. padding: 20rpx;
  890. background-color: #fff;
  891. border-radius: 8rpx;
  892. display: flex;
  893. align-items: center;
  894. background: #f5f5f5;
  895. border: 1px solid #dddfe6;
  896. justify-content: space-between;
  897. }
  898. .shu .contact-info {
  899. display: flex;
  900. align-items: center;
  901. }
  902. .shu .contact-avatar {
  903. width: 128rpx;
  904. height: 128rpx;
  905. border: 1px solid #fff;
  906. margin-right: 20rpx;
  907. border-radius: 0rpx;
  908. }
  909. .shu .contact-name {
  910. font-size: 32rpx;
  911. color: #333;
  912. }
  913. .shu .action-buttons {
  914. display: flex;
  915. justify-content: space-around;
  916. gap: 30rpx;
  917. /* margin-right: 20rpx; */
  918. }
  919. .shu .action-btn {
  920. display: flex;
  921. flex-direction: column;
  922. align-items: center;
  923. }
  924. .shu .action-btn-icon {
  925. width: 80rpx;
  926. height: 80rpx;
  927. margin-bottom: 5rpx;
  928. display: flex;
  929. align-items: center;
  930. justify-content: center;
  931. background: #ffffff;
  932. border: 1px solid #dddfe6;
  933. box-sizing: border-box;
  934. border-radius: 10rpx;
  935. }
  936. .shu .action-btn image {
  937. width: 60%;
  938. height: 60%;
  939. }
  940. .shu .action-btn text {
  941. font-size: 24rpx;
  942. color: #000000;
  943. }
  944. .shu .message-point {
  945. position: relative;
  946. }
  947. .shu .message-point::after {
  948. content: '';
  949. position: absolute;
  950. width: 18rpx;
  951. height: 18rpx;
  952. background-color: #eb6100;
  953. border-radius: 50%;
  954. top: -9rpx;
  955. right: -9rpx;
  956. }
  957. .debug-info {
  958. padding: 20rpx;
  959. background: #fff3cd;
  960. margin: 20rpx;
  961. border-radius: 8rpx;
  962. word-break: break-all;
  963. }
  964. .debug-info text {
  965. font-size: 24rpx;
  966. color: #856404;
  967. }
  968. /* 横屏服务状态样式 */
  969. .heng .service-status {
  970. margin: 12rpx;
  971. padding: 10rpx 12rpx;
  972. background: #fff;
  973. border-radius: 6rpx;
  974. border: 1px solid #dddfe6;
  975. }
  976. .heng .service-status-header {
  977. margin-bottom: 8rpx;
  978. }
  979. .heng .service-status-title {
  980. font-size: 12rpx;
  981. color: #666;
  982. font-weight: bold;
  983. }
  984. .heng .service-status-content {
  985. display: flex;
  986. flex-wrap: wrap;
  987. gap: 8rpx;
  988. }
  989. .heng .status-item {
  990. display: flex;
  991. align-items: center;
  992. gap: 4rpx;
  993. padding: 4rpx 8rpx;
  994. background: #f0f9ff;
  995. border-radius: 4rpx;
  996. }
  997. .heng .status-label {
  998. font-size: 10rpx;
  999. color: #666;
  1000. }
  1001. .heng .status-value {
  1002. font-size: 10rpx;
  1003. color: #07c160;
  1004. font-weight: bold;
  1005. }
  1006. /* 竖屏服务状态样式 */
  1007. .shu .service-status {
  1008. margin: 30rpx 50rpx;
  1009. padding: 24rpx;
  1010. background: #fff;
  1011. border-radius: 12rpx;
  1012. border: 1px solid #dddfe6;
  1013. }
  1014. .shu .service-status-header {
  1015. margin-bottom: 16rpx;
  1016. }
  1017. .shu .service-status-title {
  1018. font-size: 28rpx;
  1019. color: #666;
  1020. font-weight: bold;
  1021. }
  1022. .shu .service-status-content {
  1023. display: flex;
  1024. flex-wrap: wrap;
  1025. gap: 20rpx;
  1026. }
  1027. .shu .status-item {
  1028. display: flex;
  1029. align-items: center;
  1030. gap: 10rpx;
  1031. padding: 12rpx 20rpx;
  1032. background: #f0f9ff;
  1033. border-radius: 8rpx;
  1034. }
  1035. .shu .status-label {
  1036. font-size: 26rpx;
  1037. color: #666;
  1038. }
  1039. .shu .status-value {
  1040. font-size: 26rpx;
  1041. color: #07c160;
  1042. font-weight: bold;
  1043. }
  1044. </style>