<script setup>
import {
onLoad,
onShow,
onUnload
} from '@dcloudio/uni-app'
import {
callMsg,
devNote,
endAtelephoneCall
} from '../request/license.js'
import {
ref,
watch,
computed,
inject,
onMounted,
nextTick
} from 'vue'
import {
authorizationSn,
inquireAuthorizationSn,
checkAndRequestPermission
} from '../utils/wxVoip.js'
import {
useHooks
} from '../hooks/hooks.js'
// 创建基础菜单项
const baseMenuItems = ref([
{
title: '定位',
pagePath: '/pages/location/index',
icon: '/static/tabBar/icon-tabbar-no-location.png',
selectedIcon: '/static/tabBar/icon-tabbar-location.png',
isTab: true
},
{
title: '应用',
pagePath: '/pages/home/index',
icon: '/static/tabBar/icon-tabbar-no-index.png',
selectedIcon: '/static/tabBar/icon-tabbar-index.png',
isTab: true
},
{
title: '健康',
pagePath: '/pages/health/index',
icon: '/static/tabBar/icon-tabbar-no-healthy.png',
selectedIcon: '/static/tabBar/icon-tabbar-healthy.png',
isTab: true
},
{
title: '学校', // 新增学校菜单
pagePath: '/pages/school/index',
icon: '/static/tabBar/icon-tabbar-no-index.png',
selectedIcon: '/static/tabBar/icon-tabbar-index.png',
isTab: true
},
{
title: '我的',
pagePath: '/pages/my/index',
icon: '/static/tabBar/icon-tabbar-no-my.png',
selectedIcon: '/static/tabBar/icon-tabbar-my.png',
isTab: true
}
])
const currentIndex = ref(1)
const popup = ref('')
const userImg = inject('userImg')
const list = ref([])
const videocall = inject('videocall')
const pageData = ref({})
const plugin = ref(null)
let isInCall = false
let voipEventOff = null
const showToastFunc = ref(null)
const isTag = ref(null)
const roomNum = ref(null)
const data = ref({})
const showLoader = ref(false)
const props = defineProps({
textIndex: {
type: Number,
default: 0
},
status: {
type: Boolean,
default: false
},
userRole: { // 新增用户身份属性
type: String,
default: 'parent' // 默认为家长身份
}
})
// 初始化菜单列表
const initializeMenuList = (isTag) => {
const menuItems = [...baseMenuItems.value]
// 只有在非教师角色且是微信环境下才添加视频通话按钮
// #ifdef MP-WEIXIN
if (isTag && props.userRole !== 'teacher') {
menuItems.splice(2, 0, {
title: '',
pagePath: '',
icon: '/static/item/shipintonghua.png',
selectedIcon: '/static/item/shipintonghua.png',
isCenter: true
})
}
// #endif
list.value = menuItems
// 确保初始化后保持正确的选中状态
if (!props.status) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage) {
const currentPath = '/' + currentPage.route
const tabIndex = menuItems.findIndex(item => item.pagePath === currentPath)
if (tabIndex !== -1) {
currentIndex.value = tabIndex
}
}
}
}
// 根据 status 过滤菜单项
const filteredList = computed(() => {
if (!list.value.length) {
return filterMenuByRole(baseMenuItems.value)
}
return filterMenuByRole(list.value)
})
const filterMenuByRole = (menuList) => {
if(props.userRole === 'teacher') {
// 教师只显示学校和我的
return menuList.filter(item =>
item.title === '学校' ||
item.title === '我的'
)
}
// 家长显示除学校外的所有菜单
return menuList.filter(item => item.title !== '学校')
}
// 监听路由变化
watch(() => {
const pages = getCurrentPages();
return pages[pages.length - 1] && pages[pages.length - 1].route;
}, (newRoute) => {
if (newRoute) {
const currentPath = '/' + newRoute;
const tabIndex = list.value.findIndex(item => item.pagePath === currentPath);
if (tabIndex !== -1) {
currentIndex.value = tabIndex;
emit('tabChange', {
index: tabIndex,
type: 'tab'
});
}
}
}, {
immediate: true
});
// 监听 props.textIndex 的变化
watch(() => props.textIndex, (newValue) => {
if (newValue !== undefined && newValue !== null) {
currentIndex.value = newValue;
}
}, {
immediate: true
});
// 初始化时设置默认选中项
watch(() => props.status, (newValue) => {
if (newValue) {
const applicationIndex = list.value.findIndex(item => item.title === '应用')
if (applicationIndex !== -1) {
currentIndex.value = applicationIndex
}
}
}, {
immediate: true
})
watch(() => videocall.value, (newVal) => {
if (newVal !== null && props.userRole !== 'teacher') {
initializeMenuList(newVal)
} else {
initializeMenuList(false) // 教师角色强制传false
}
}, {
immediate: true
})
const isActive = (filteredIndex, item) => {
if (props.status) {
// 在特殊状态下,根据title判断
return item.title === list.value[currentIndex.value]?.title
}
// 在正常状态下,需要找到实际的索引
const actualIndex = list.value.findIndex(i => i.title === item.title)
return actualIndex === currentIndex.value
}
const emit = defineEmits(['tabChange'])
// 页面导航处理函数
function handleNavigation(item) {
if (item.isTab) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = '/' + currentPage.route
if (currentPath === item.pagePath) {
return
}
const existingPageIndex = pages.findIndex(page => '/' + page.route === item.pagePath)
if (existingPageIndex !== -1) {
const delta = pages.length - 1 - existingPageIndex
uni.navigateBack({
delta: delta
})
} else {
if (pages.length >= 9) {
uni.redirectTo({
url: item.pagePath
})
} else {
uni.navigateTo({
url: item.pagePath
})
}
}
} else {
const pages = getCurrentPages()
if (pages.length >= 9) {
uni.redirectTo({
url: item.pagePath
})
} else {
uni.navigateTo({
url: item.pagePath
})
}
}
}
//获取用户是否授权
async function getDeviceVoIP(e) {
const res = await inquireAuthorizationSn()
const imei = getApp().globalData.imei
let status = null
if (res.list.length !== 0) {
const y = res.list.find(item => item.sn === imei)
if (!y) {
status = 0
} else {
status = y.status
}
} else {
status = 0
}
// 已授权 or 未授权
showLoader.value = false
if (status === 1) {
getCallMsg(e)
} else {
try {
const noteRes = await devNote()
let data = noteRes.data.data
const authRes = await authorizationSn(data)
getCallMsg(e)
} catch (error) {
if (error.errCode === 10021) {
checkAndRequestPermission()
}
}
}
}
// 通话类型
function btnType(e) {
popup.value.close();
showLoader.value = true
getDeviceVoIP(e)
}
// 结束通话
function endCall() {
console.log('开始结束通话流程, roomId:', roomNum.value);
// 立即重置状态,防止重复调用
const currentRoomId = roomNum.value;
isInCall = false;
roomNum.value = '';
// 取消事件监听
if (voipEventOff) {
try {
voipEventOff();
} catch (error) {
console.error('清理事件监听器失败:', error);
}
voipEventOff = null;
}
// 如果有房间ID,调用结束通话API
if (currentRoomId) {
endAtelephoneCall({
roomId: currentRoomId
}).then(res => {
console.log('结束通话API调用成功:', res);
}).catch(error => {
console.error('结束通话API调用失败:', error);
// API调用失败不影响本地状态重置
});
}
// 重置全局状态
if (getApp().globalData) {
getApp().globalData.isMiniappCalling = false;
}
// 隐藏加载状态
showLoader.value = false;
}
// 开始通话
async function getCallMsg(e) {
try {
console.log('开始发起通话, 类型:', e === 0 ? '语音' : '视频');
// 检查是否在通话中
if (isInCall) {
console.log('已在通话中,拒绝重复发起');
uni.showToast({
title: '已在通话中',
icon: 'none'
});
showLoader.value = false;
return;
}
// 确保状态清理
if (voipEventOff) {
try {
voipEventOff();
} catch (error) {
console.error('清理旧事件监听器失败:', error);
}
voipEventOff = null;
}
// 准备通话消息
const msg = {
callType: e === 0 ? 'SWV' : 'SW'
};
// 获取通话所需参数
const response = await callMsg(msg);
if(!response || response.data.code !== 0) {
console.error('获取通话参数失败:', response);
uni.showToast({
title: '通话参数异常',
icon: 'none'
});
showLoader.value = false;
return;
}
const number = response.data.data;
const {
channelName,
robotUID,
deviceUID,
token,
license,
appId,
openid,
deviceId,
modelId,
roomId,
screenHeight,
screenWidth
} = number;
// 验证必要参数
if (!channelName || !robotUID || !deviceUID || !token || !license || !appId || !openid || !deviceId || !modelId || !roomId) {
console.error('通话参数不完整:', number);
uni.showToast({
title: '通话参数不完整',
icon: 'none'
});
showLoader.value = false;
return;
}
roomNum.value = roomId;
console.log('通话参数获取成功, roomId:', roomId);
// 准备 Agora 配置
const agoraConfig = {
agoraVoIP: {
trigger: 'MiniApp',
version: 'v2',
transmission: {
agora: {
channelName,
robotUID,
deviceUID,
token,
license,
appId,
encryptionMode: 'NONE',
encryptionKey: '',
encryptionKdfSalt: '',
source: {
videoCodec: 'JPEG',
videoWidth: screenWidth,
videoHeight: screenHeight,
videoFrameRate: 10
},
sink: {
audioCodec: 'G722',
videoTranscode: true,
videoCodec: 'JPEG',
videoWidth: screenWidth,
videoHeight: screenHeight,
videoFrameRate: 10
}
},
wechat: {
openId: openid,
appId: 'wxa1ac988b47d44c63', // wxa1ac988b47d44c63 太阳树wx0ad0b6dda50c3cde
deviceId: deviceId,
modelId: modelId,
landscape: false,
payload: '',
subscribeVideoLength: (screenHeight - screenWidth) > 0 ? screenHeight : screenWidth,
roomType: e === 0 ? 'voice' : 'video',
listenerName: "m",
versionType: 2, //版本环境 2 体验 0 正式 1开发
query: "a=b",
callerCameraStatus: 0,
listenerCameraStatus: 0,
encodeVideoRotation: 1,
source: {
videoCodec: "H264",
videoWidth: screenWidth,
videoHeight: screenHeight,
videoFrameRate: 10,
// 240 284
},
sink: {
videoTranscode: true,
videoCodec: "H264",
// audiocodec:'G722',//string PCMU、PCMA、G722、AAC
videoWidth: screenWidth,
videoHeight: screenHeight,
// 240 320
videoFrameRate: 10
}
}
}
}
};
// 准备呼叫选项
const options = {
roomType: e === 0 ? 'voice' : 'video',
sn: deviceId,
modelId,
payload: JSON.stringify(agoraConfig),
isCloud: true,
encodeVideoRotation: 1,
};
// .replace(/"/g, '\\"')
// 清除旧的事件监听
if (voipEventOff) {
voipEventOff();
voipEventOff = null;
}
// 在使用插件前进行健壮性校验
if (!plugin.value) {
uni.showToast({
title: '通话组件未初始化',
icon: 'none'
});
return;
}
const requiredMethods = ['onVoipEvent','getPluginEnterOptions','getPluginOnloadOptions','setUIConfig','callDevice'];
const missing = requiredMethods.find(m => typeof plugin.value[m] !== 'function');
if (missing) {
uni.showToast({
title: '通话能力缺失:' + missing,
icon: 'none'
});
return;
}
// 设置新的事件监听
voipEventOff = plugin.value.onVoipEvent((event) => {
console.log('收到voip事件:', event.eventName, event);
switch (event.eventName) {
case 'endVoip':
case 'finishVoip':
case 'cancelVoip':
case 'hangUpVoip':
console.log('通话结束事件触发:', event.eventName);
endCall();
break;
case 'acceptVoip':
console.log('通话被接受');
break;
case 'rejectVoip':
console.log('通话被拒绝');
endCall();
break;
case 'errorVoip':
console.error('voip错误事件:', event);
endCall();
break;
default:
console.log('未处理的voip事件:', event.eventName);
break;
}
});
const query = plugin.value.getPluginEnterOptions()
const queryB = plugin.value.getPluginOnloadOptions()
// 小程序主动呼叫设备时,通知App.vue设置正确的配置
if (!queryB || Object.keys(queryB).length === 0) {
// 通知App.vue这是小程序主动呼叫
getApp().globalData.isMiniappCalling = true
// 立即设置配置,确保在通话发起前配置正确
getApp().setVoipUIConfig()
} else {
getApp().globalData.isMiniappCalling = false
getApp().setVoipUIConfig()
}
// 发起通话
try {
console.log('开始调用callDevice, options:', options);
const callResult = await plugin.value.callDevice(options);
console.log('callDevice调用结果:', callResult);
if (callResult) {
isInCall = true;
console.log('通话发起成功,准备跳转页面');
// 跳转到通话页面
const callPage = plugin.value.CALL_PAGE_PATH;
if (!callPage) {
console.error('通话页面未配置');
endCall();
uni.showToast({ title: '通话页面未配置', icon: 'none' });
return;
}
const pages = getCurrentPages();
const nav = pages.length >= 9 ? uni.redirectTo : uni.navigateTo;
nav({
url: callPage,
success: () => {
console.log('成功跳转到通话页面');
showLoader.value = false;
},
fail: (error) => {
console.error('跳转通话页面失败:', error);
endCall();
uni.showToast({ title: '打开通话页面失败', icon: 'none' });
}
});
} else {
throw new Error('呼叫失败:未收到有效响应');
}
} catch (callError) {
console.error('callDevice调用失败:', callError);
endCall();
uni.showToast({
title: '呼叫失败',
icon: 'error'
});
}
} catch (error) {
console.error('发起通话过程中发生错误:', error);
endCall();
uni.showToast({
title: '发起通话失败',
icon: 'error'
});
} finally {
showLoader.value = false;
}
}
// 强制结束通话
function forceEndCall() {
console.log('强制结束通话');
if (plugin.value && typeof plugin.value.forceHangUpVoip === 'function') {
try {
plugin.value.forceHangUpVoip();
} catch (error) {
console.error('强制结束通话失败:', error);
}
}
// 确保状态被重置
endCall();
}
function itemBtn(filteredIndex, item) {
// 视频通话按钮的处理
if (!item.pagePath) {
if (!showToastFunc.value(uni.getStorageSync('isOnline'))) return;
popup.value.open();
return;
}
// 找到在完整列表中的实际索引
const actualIndex = list.value.findIndex(i => i.title === item.title);
if (item.isCenter) {
emit('tabChange', {
index: actualIndex,
type: 'center'
});
return;
}
// 避免重复点击当前项
if (currentIndex.value === actualIndex) return;
// 更新当前索引
currentIndex.value = actualIndex;
emit('tabChange', {
index: actualIndex,
type: 'tab'
});
handleNavigation(item);
}
// UI配置已统一在App.vue中处理,这里不再需要定义
// 初始化插件
function initPlugin() {
if (!plugin.value) {
plugin.value = getApp().globalData.plugin
}
// UI配置统一在App.vue中处理,这里只初始化插件
}
// 重置所有voip相关状态
function resetVoipState() {
console.log('重置voip状态');
isInCall = false;
roomNum.value = '';
showLoader.value = false;
if (voipEventOff) {
try {
voipEventOff();
} catch (error) {
console.error('重置时清理事件监听器失败:', error);
}
voipEventOff = null;
}
if (getApp().globalData) {
getApp().globalData.isMiniappCalling = false;
}
}
// 更新当前标签页索引
function updateCurrentIndex() {
if (!props.status) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = '/' + currentPage.route
const tabIndex = list.value.findIndex(item => item.pagePath === currentPath)
if (tabIndex !== -1) {
currentIndex.value = tabIndex
}
}
}
// 3. 修改 watch 相关逻辑,确保正确的导航状态
watch(() => props.status, (newValue) => {
if (newValue) {
const applicationIndex = list.value.findIndex(item => item.title === '应用');
if (applicationIndex !== -1) {
currentIndex.value = applicationIndex;
emit('tabChange', {
index: applicationIndex,
type: 'tab',
title: '应用'
});
}
}
}, {
immediate: true
});
// 监听路由变化,确保选中状态正确
watch(() => {
const pages = getCurrentPages()
return pages[pages.length - 1] && pages[pages.length - 1].route
}, (newRoute) => {
if (!props.status && newRoute) {
const currentPath = '/' + newRoute
const tabIndex = list.value.findIndex(item => item.pagePath === currentPath)
if (tabIndex !== -1) {
currentIndex.value = tabIndex
}
}
}, {
immediate: true
})
watch(() => props.status, (newValue) => {
if (newValue) {
const applicationIndex = list.value.findIndex(item => item.title === '应用')
if (applicationIndex !== -1) {
currentIndex.value = applicationIndex
// 只发送切换事件,不跳转页面
emit('tabChange', {
index: applicationIndex,
type: 'tab',
title: '应用'
})
}
}
}, {
immediate: true
})
onUnload(() => {
// #ifdef MP-WEIXIN
console.log('组件卸载,清理voip相关状态');
if (voipEventOff) {
try {
voipEventOff();
} catch (error) {
console.error('卸载时清理事件监听器失败:', error);
}
voipEventOff = null;
}
isInCall = false;
roomNum.value = '';
showLoader.value = false;
// #endif
})
onMounted(() => {
initializeMenuList(videocall.value)
})
onLoad(() => {
// 初始化其他功能
showToastFunc.value = useHooks().showToastFunc
// 设置初始索引
if (!props.status && props.textIndex !== undefined) {
currentIndex.value = props.textIndex
} else {
currentIndex.value = 1 // 确保默认是1
}
})
onShow(() => {
// 初始化插件
// #ifdef MP-WEIXIN
initPlugin()
// #endif
// UI配置统一在App.vue中处理
// 更新当前标签页索引
updateCurrentIndex()
})
let tabRefs = [] // 普通数组
const sliderLeft = ref(0)
const sliderWidth = ref(0)
// 计算滑块样式
const sliderStyle = computed(() => {
return {
left: sliderLeft.value + 'px',
width: sliderWidth.value + 'px',
transition: 'left 0.3s cubic-bezier(.4,0,.2,1),width 0.3s cubic-bezier(.4,0,.2,1)'
}
})
function updateSlider() {
nextTick(() => {
const tabEls = tabRefs
const activeIdx = filteredList.value.findIndex((item, idx) => isActive(idx, item))
if (tabEls && tabEls[activeIdx]) {
const el = tabEls[activeIdx]
if (el && el.offsetLeft !== undefined) {
sliderLeft.value = el.offsetLeft
sliderWidth.value = el.offsetWidth
}
}
})
}
watch(filteredList, () => {
tabRefs = []
updateSlider()
}, { immediate: true })
watch(() => currentIndex.value, updateSlider)
onMounted(() => {
tabRefs = []
updateSlider()
})
</script>
