| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- <template>
- <view class="call-center">
- <view v-if="contacts.length" class="contact-list">
- <view class="contact-card" v-for="contact in contacts" :key="contact.id">
-
- <view class="contact-info">
- <image class="contact-avatar" :src="contact.avatar" mode="aspectFill" />
- <view class="contact-meta">
- <text class="contact-name">{{ contact.username }}</text>
- <text class="contact-desc">{{ contact.className }}</text>
- </view>
- </view>
- <view class="action-buttons">
- <view class="action-btn" @click="startVoiceCall(contact)">
- <view class="action-btn-icon">
- <image src="/static/icon/audio.png" mode="aspectFit" />
- </view>
- <text>音频</text>
- </view>
- <view class="action-btn" @click="startVideoCall(contact)">
- <view class="action-btn-icon">
- <image src="/static/icon/video.png" mode="aspectFit" />
- </view>
- <text>视频</text>
- </view>
- <view class="action-btn" :class="{ 'message-point': contact.hasPoints }" @click="goToMessage(contact)">
- <view class="action-btn-icon">
- <image src="/static/icon/message.png" mode="aspectFit" />
- </view>
- <text>留言</text>
- </view>
- </view>
- </view>
- </view>
- <view v-else class="empty-tip">
- <text>暂未关联可呼叫的设备</text>
- </view>
- </view>
- </template>
- <script>
- import { getImageUrl } from '@/utils/util'
- const CALL_END_STORAGE_KEY = 'parentLastCallInfo'
- const CALL_END_PAGE_URL = '/pages/parent/call-center' // 通话结束返回通话中心页
- let wmpfVoip = null
- try {
- if (typeof requirePlugin === 'function') {
- wmpfVoip = requirePlugin('wmpf-voip').default
- }
- } catch (error) {
- console.warn('wmpf-voip 插件暂不可用', error)
- }
- const miniprogramState = (() => {
- if (typeof wx === 'undefined' || typeof wx.getAccountInfoSync !== 'function') {
- return 'formal'
- }
- const accountInfo = wx.getAccountInfoSync()
- if (accountInfo && accountInfo.miniProgram) {
- const platform = { develop: 'developer', trial: 'trial', release: 'formal' }
- return platform[accountInfo.miniProgram.envVersion]
- }
- return 'formal'
- })()
- const DEFAULT_AVATAR = '/static/logo.png'
- const DEFAULT_CLASS_NAME = '所属班级'
- export default {
- data() {
- return {
- parentInfo: {
- xm: '张小同',
- openid: 'oKFvD4pG_iKBJR0wfjiDicm7fjaA'
- // openid: 'oKFvD4jogQe9DTDRNgFjW5ybobEU'
- },
- contacts: [
- {
- id: 'stu-001',
- username: '张小小同',
- className: '三年级二班',
- avatar: '/static/images/yishuzhao_nan.svg',
- deviceSn: 'A100006B623E86',
- pushToken: 'BgAADLbeQ1s4jDjapuGe4HKnrI-I-5D57-TLABuqXrB0saDv-fuzNlT-32rTadDMrLrpNLxcUMe1y6FlESz7-EVGC4kjHaK_P7WZQUfEhHzxP55ndPPrYOSzvq6aez115InzFIOM1PZhdNS5U6pItMBid7zqVX4',
- }
- ],
- currentContact: null,
- _voipEndPathSet: false,
- _voipEventRegistered: false,
- defaultAvatar: DEFAULT_AVATAR,
- defaultClassName: DEFAULT_CLASS_NAME
- }
- },
- onLoad() {
- this.initParentInfo()
- this.registerVoipEvent()
- },
- methods: {
- initParentInfo() {
- let info = uni.getStorageSync('userInfo')
- if (typeof info === 'string') {
- try {
- info = JSON.parse(info)
- } catch (error) {
- info = null
- }
- }
- if (!info) {
- this.loadContactList()
- return
- }
- this.parentInfo = {
- ...this.parentInfo,
- ...info
- }
- this.loadContactList()
- },
- loadContactList() {
- // 统一处理联系人来源:仅格式化 data 中已有的联系人,保持展示/拨号结构一致
- const formatContact = (item = {}, index = 0) => ({
- id: item.id || `contact-${index}`,
- username: item.username || item.name || '学生',
- avatar: this.formatAvatar(item.avatar || item.yszwj),
- className: item.className || item.bjmc || item.bmmc || item.bjmcName || this.defaultClassName,
- deviceSn: item.deviceSn || item.sn || item.devId || '',
- pushToken: item.pushToken || item.voipToken || '',
- deviceName: item.deviceName || item.username || item.name || '家庭设备',
- hasPoints: !!item.hasPoints,
- })
- this.contacts = (this.contacts || []).map(formatContact)
- },
- formatAvatar(value) {
- if (!value) return this.defaultAvatar
- if (typeof value === 'string' && (value.startsWith('http') || value.startsWith('https') || value.startsWith('data:'))) {
- return value
- }
- return getImageUrl(value)
- },
- async startVoiceCall(contact) {
- await this.startCall(contact, 'voice')
- },
- async startVideoCall(contact) {
- await this.startCall(contact, 'video')
- },
- async startCall(contact, roomType) {
- if (!wmpfVoip) {
- uni.showToast({ title: '通话能力暂不可用', icon: 'none' })
- return
- }
- const callerId = this.parentInfo?.openid
-
- if (!callerId) {
- uni.showToast({ title: '缺少家长身份标识', icon: 'none' })
- return
- }
- if (!contact?.deviceSn) {
- uni.showToast({ title: '未找到可呼叫的设备编号', icon: 'none' })
- return
- }
- if (!contact?.pushToken) {
- uni.showToast({ title: '设备未上报通话凭证', icon: 'none' })
- return
- }
- uni.showLoading({ title: '呼叫中...', mask: true })
- this.currentContact = contact
- const callInfo = {
- name: contact.username || '家庭设备',
- avatar: contact.avatar || '/static/logo.png',
- duration: 0,
- time: new Date().toLocaleString(),
- type: roomType,
- status: '呼叫中'
- }
- uni.setStorageSync(CALL_END_STORAGE_KEY, callInfo)
- this.setVoipEndPagePath()
- console.log(callerId)
- console.log(this.parentInfo?.xm)
- console.log(contact.deviceSn)
- console.log(contact.username)
- console.log(contact.pushToken)
- console.log(miniprogramState)
- try {
- const res = await wmpfVoip.initByCaller({
- roomType,
- caller: {
- id: callerId,
- name: this.parentInfo?.xm || '家长'
- },
- listener: {
- id: contact.deviceSn,
- name: contact.username
- },
- businessType: 2,
- voipToken: contact.pushToken,
- miniprogramState: "formal"
- })
- if (res.isSuccess) {
- uni.hideLoading()
- uni.redirectTo({ url: wmpfVoip.CALL_PAGE_PATH })
- return
- }
- uni.hideLoading()
- console.error('呼叫失败', res)
- uni.showToast({ title: '呼叫失败', icon: 'error' })
- } catch (error) {
- uni.hideLoading()
- console.error('通话异常:', error)
- uni.showToast({ title: '发起通话失败', icon: 'none' })
- }
- },
- setVoipEndPagePath(forceUpdate = false) {
- if (this._voipEndPathSet && !forceUpdate) return
- if (!wmpfVoip) return
- const callInfo = uni.getStorageSync(CALL_END_STORAGE_KEY) || {}
- const options = callInfo.name ?
- `name=${encodeURIComponent(callInfo.name)}&avatar=${encodeURIComponent(callInfo.avatar)}&duration=${callInfo.duration || 0}&type=${callInfo.type || 'voice'}&status=${encodeURIComponent(callInfo.status || '通话已结束')}` : ''
- wmpfVoip.setVoipEndPagePath({
- url: CALL_END_PAGE_URL,
- key: 'Call',
- options,
- routeType: 'redirectTo'
- })
- this._voipEndPathSet = true
- },
- registerVoipEvent() {
- if (this._voipEventRegistered || !wmpfVoip) return
- wmpfVoip.onVoipEvent((event) => {
- const eventName = event.eventName
- const hangupEvent = ['hangUpVoip', 'endVoip']
- const cancelEvent = ['cancelVoip']
- const timeoutEvent = ['timeout']
- const rejectEvent = ['rejectVoip']
- const callInfo = uni.getStorageSync(CALL_END_STORAGE_KEY) || {}
- if (hangupEvent.includes(eventName)) {
- callInfo.duration = event.data?.keepTime || 0
- callInfo.status = event.data?.keepTime > 0 ? '通话已结束' : '未接通'
- } else if (cancelEvent.includes(eventName)) {
- callInfo.duration = 0
- callInfo.status = '已取消'
- } else if (timeoutEvent.includes(eventName)) {
- callInfo.duration = 0
- callInfo.status = '未接听'
- } else if (rejectEvent.includes(eventName)) {
- callInfo.duration = 0
- callInfo.status = '已拒绝'
- }
- if ([...hangupEvent, ...cancelEvent, ...timeoutEvent, ...rejectEvent].includes(eventName)) {
- callInfo.endType = eventName
- callInfo.endTime = Date.now()
- uni.hideLoading()
- uni.setStorageSync(CALL_END_STORAGE_KEY, callInfo)
- this.setVoipEndPagePath(true)
- }
- })
- this._voipEventRegistered = true
- },
- goToMessage(contact) {
- const params = {
- role: 'parent'
- }
- if (contact?.studentId) {
- params.studentId = contact.studentId
- }
- if (contact?.id) {
- params.contactId = contact.id
- }
- const query = Object.keys(params)
- .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
- .join('&')
- uni.navigateTo({
- url: `/pages/parent/message?${query}`
- })
- }
- }
- }
- </script>
- <style scoped>
- .call-center {
- min-height: 100vh;
- padding: 32rpx;
- background: #f5f5f5;
- box-sizing: border-box;
- }
- .contact-list {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- }
- .contact-card {
- background: #fff;
- border-radius: 12rpx;
- border: 2rpx solid #e4e7ed;
- padding: 24rpx;
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.05);
- }
- .contact-info {
- display: flex;
- align-items: center;
- }
- .contact-avatar {
- width: 96rpx;
- height: 96rpx;
- border-radius: 8rpx;
- margin-right: 16rpx;
- background: #f0f0f0;
- }
- .contact-meta {
- display: flex;
- flex-direction: column;
- gap: 8rpx;
- }
- .contact-name {
- font-size: 30rpx;
- color: #333;
- font-weight: 500;
- }
- .contact-desc {
- font-size: 26rpx;
- color: #8c8c8c;
- }
- .empty-tip {
- margin-top: 60rpx;
- text-align: center;
- color: #888;
- font-size: 28rpx;
- }
- .action-buttons {
- display: flex;
- gap: 18rpx;
- }
- .action-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6rpx;
- }
- .action-btn-icon {
- width: 56rpx;
- height: 56rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #ffffff;
- border: 2rpx solid #e0e0e0;
- border-radius: 10rpx;
- }
- .action-btn-icon image {
- width: 32rpx;
- height: 32rpx;
- }
- .action-btn text {
- font-size: 24rpx;
- color: #333;
- }
- .message-point {
- position: relative;
- }
- .message-point::after {
- content: '';
- position: absolute;
- width: 12rpx;
- height: 12rpx;
- background-color: #eb6100;
- border-radius: 50%;
- top: -4rpx;
- right: -4rpx;
- }
- </style>
|