吉安感知网项目-前端
张含笑
2026-01-15 a6842ea879721063affc0eb51f75a02b59c7c1f4
feat:语音通话模板
3 files modified
1 files added
524 ■■■■ changed files
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue 82 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/hooks/useGlobalWS.js 201 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/login/index.vue 53 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/websocket.js 188 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,17 +1,17 @@
<template>
    <div class="call-container" :class="{'incoming-call': state === 'ringing'} ">
      <div class="content-wrapper">
<!--        <div class="input-group">-->
<!--            <div class="input-item">-->
<!--                <span class="input-label">我的 userId:</span>-->
<!--                <input v-model="uid" class="input-field" />-->
<!--            </div>-->
<!--          -->
<!--            <div class="input-item">-->
<!--                <span class="input-label">对方 userId:</span>-->
<!--                <input v-model="peerUid" class="input-field" />-->
<!--            </div>-->
<!--        </div>-->
            <!--        <div class="input-group">
                    <div class="input-item">
                        <span class="input-label">我的 userId:</span>
                        <input v-model="uid" class="input-field" />
                    </div>
                    <div class="input-item">
                        <span class="input-label">对方 userId:</span>
                        <input v-model="peerUid" class="input-field" />
                    </div>
                </div>-->
  
        <div class="button-group" v-if="false">
            <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
@@ -27,7 +27,7 @@
            <strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong>
        </div>
        <div class="button-group">
            <button v-if="false" @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button>
                <button  @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button>
          
            <button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button>
            <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button>
@@ -38,16 +38,16 @@
      </div>
  
<!--      <div class="tip-message">-->
<!--        <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>-->
<!--      </div>-->
        <!--    <div class="tip-message">
            <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
            </div>-->
  
<!--      <div class="log-container">-->
<!--        <div class="log-box">-->
<!--            <div class="log-title">日志信息:</div>-->
<!--            <pre class="log-content">{{ logText }}</pre>-->
<!--        </div>-->
<!--      </div>-->
        <!--    <div class="log-container">
            <div class="log-box">
                <div class="log-title">日志信息:</div>
                <pre class="log-content">{{ logText }}</pre>
            </div>
            </div>-->
  
      <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
    </div>
@@ -75,6 +75,16 @@
        log('❌ 解析URL参数失败:', String(e))
      }
    }
}
// 从URL参数或本地存储获取用户ID
const getUserIdFromStorage = () => {
    const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId')
    if (userId) {
        log('🔗 从本地存储获取userId:', userId)
        return userId
    }
    return '3' // 默认值
  }
  const uid = ref('3')
@@ -318,6 +328,8 @@
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
console.log('1cesss',msg);
    if (!msg) return
    const t = (msg.type || '').toString()
@@ -424,6 +436,9 @@
  /* ---------------- 业务操作 ---------------- */
  
  function connectWS() {
    // 确保获取最新的用户ID
    uid.value = getUserIdFromStorage()
    if (ws) ws.close()
  
    ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
@@ -587,6 +602,21 @@
onMounted(() => {
    const receiveParameters = route.query.contact
    console.log('接收参数', receiveParameters)
    // 解析并使用contact参数
    if (receiveParameters) {
        try {
            const contact = JSON.parse(decodeURIComponent(receiveParameters))
            if (contact.friendId) {
                peerUid.value = contact.friendId
                log('🔗 从contact参数获取peerUid:', contact.friendId)
                log('👤 联系人名称:', contact.friendNickName || '未知联系人')
            }
        } catch (e) {
            log('❌ 解析contact参数失败:', String(e))
        }
    }
    // 解析URL参数
    parseUrlParams()
    // 自动连接WebSocket
@@ -594,11 +624,11 @@
})
// 监听连接状态,连接成功后自动呼叫
watch(() => connected.value, (newVal) => {
    if (newVal && state.value === 'idle') {
        requestCall()
    }
})
// watch(() => connected.value, (newVal) => {
//     if (newVal && state.value === 'idle') {
//         requestCall()
//     }
// })
  onBeforeUnmount(() => {
    cleanup(false)
uniapps/work-app/src/hooks/useGlobalWS.js
@@ -1,21 +1,120 @@
import {useUserStore} from "@/store/index.js";
import {getEnvObj} from "@/utils/index.js";
// import {enterRoom} from "@/utils/voiceCallByTX/index.js";
import { ref, computed, watch } from "vue";
import useAppStore from "../store/modules/app/index.js";
let socketTask = null
import websocketService from "@/utils/websocket.js";
export function useGlobalWS() {
    const userStore = useUserStore();
  const appStore = useAppStore();
    const userId = computed(() => userStore?.userInfo?.user_id)
    const access_token = computed(() => userStore?.userInfo?.access_token)
    const {VITE_APP_WS_API_URL} = getEnvObj()
  // 设置默认用户ID为3
  const defaultUserId = '3';
  // WebSocket基础URL
  const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
    // 消息处理
    function messageHandler(payload) {
        switch (payload.biz_code) {
    console.log('🌐 全局WebSocket收到消息111111111111111111111:', payload)
    // 先尝试直接处理消息(适用于mobile-web-view的voiceCallDetail页面的消息格式)
    const t = (payload.type || '').toString()
    const bizCode = payload.biz_code || ''
    console.log('📋 消息分析:type=' + t + ', biz_code=' + bizCode)
    // 处理语音通话请求
    if (t === 'call') {
      console.log('📞 全局收到来电 call,来自', payload.from)
      // 构建来电参数
      const callParams = {
        peerUid: payload.from,
        from: payload.from,
        type: 'incoming'
      };
      // 转义参数以便在URL中传递
      const encodedParams = encodeURIComponent(JSON.stringify(callParams));
      // 跳转到mobile-web-view应用中的语音通话页面
      const voiceCallUrl = `http://localhost:9527/work-app/#/subPackages/voiceCallDetail/index?params=${encodedParams}`;
      console.log('🔗 准备导航到:', voiceCallUrl)
      try {
        // 优先使用uni-app的导航API(适用于uni-app环境)
        if (typeof uni !== 'undefined' && uni.navigateTo) {
          console.log('使用uni-app导航API跳转到语音通话页面');
          // 检查是否是跳转到应用内页面
          if (voiceCallUrl.includes('/subPackages/voiceCallDetail/')) {
            // 应用内页面跳转
            uni.navigateTo({
              url: voiceCallUrl.replace('http://localhost:9527/work-app/#', ''),
              success: () => {
                console.log('✅ 应用内导航成功!');
              },
              fail: (err) => {
                console.error('❌ 应用内导航失败:', err);
                // 如果应用内导航失败,尝试使用外部跳转
                fallbackToExternalNavigation();
              }
            });
          } else {
            // 外部页面跳转
            fallbackToExternalNavigation();
          }
        }
        // 否则尝试使用window.location跳转(适用于Web环境)
        else if (typeof window !== 'undefined' && window.location) {
          console.log('使用window.location跳转到语音通话页面');
          window.location.href = voiceCallUrl;
        } else {
          console.error('无法跳转到语音通话页面:当前环境不支持导航');
        }
      } catch (error) {
        console.error('跳转到语音通话页面失败:', error);
        // 尝试使用外部跳转作为最后的补救措施
        fallbackToExternalNavigation();
      }
      // 外部页面跳转的回调函数
      function fallbackToExternalNavigation() {
        console.log('尝试使用外部导航跳转到语音通话页面');
        try {
          // 使用uni-app的外部链接跳转API
          if (typeof uni !== 'undefined' && uni.navigateToWebview) {
            // 这是一个示例,具体API可能需要根据uni-app版本和配置调整
            uni.navigateToWebview({
              url: voiceCallUrl,
              success: () => {
                console.log('✅ 外部Webview导航成功!');
              },
              fail: (err) => {
                console.error('❌ 外部Webview导航失败:', err);
                // 如果还是失败,尝试使用浏览器打开
                if (typeof plus !== 'undefined' && plus.runtime) {
                  plus.runtime.openURL(voiceCallUrl);
                } else {
                  console.error('❌ 所有导航方式都失败了');
                }
              }
            });
          }
          // 或者使用plus.runtime.openURL(适用于App环境)
          else if (typeof plus !== 'undefined' && plus.runtime) {
            plus.runtime.openURL(voiceCallUrl);
            console.log('尝试使用plus.runtime.openURL打开外部链接');
          } else {
            console.error('无法跳转到外部页面:当前环境不支持外部导航');
          }
        } catch (fallbackError) {
          console.error('外部导航尝试失败:', fallbackError);
        }
      }
      return
    }
    // 然后处理biz_code格式的消息
    switch (bizCode) {
            case 'JOB_ISREFRESH':
        appStore.setJobUpdateKeyAdd()
                break
@@ -34,71 +133,35 @@
                // enterRoom(payload, userId.value)
                break
            default:
        // 记录未处理的消息
        console.log('未处理的WebSocket消息:', payload)
                break;
        }
    }
    // 关闭ws
    function closeWS() {
        socketTask?.close({
            success: () => {
                console.log('ws关闭连接');
            },
        })
    }
    // 初始化ws
  // 初始化WebSocket连接
    function initWS() {
        // 关闭,再连接ws
        closeWS()
        if (!access_token.value) return
        const url = VITE_APP_WS_API_URL
            + `?x-auth-token=${encodeURI(access_token?.value)}`
            + `&model_type=3&workspace-id=${userId.value}`
        // 创建连接
        socketTask = uni.connectSocket({
            url: url,
            success: () => {
                console.log('ws连接成功');
    try {
      // 使用默认用户ID初始化WebSocket连接
      websocketService.init(defaultUserId, WS_BASE);
      console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId);
      // 设置全局消息处理回调
      websocketService.setOnMessageCallback(messageHandler);
      console.log('✅ 全局WebSocket消息处理器已设置');
    } catch (error) {
      console.error('❌ 全局WebSocket初始化失败:', error);
    }
  }
  // 监听用户信息变化,初始化WebSocket
  watch(
    () => userStore.userInfo,
    (newUserInfo) => {
      console.log('👤 用户信息变化,尝试初始化WebSocket');
      initWS();
            },
            fail: (err) => {
                console.error('ws连接失败:', err);
            }
        });
        // 消息监听
        socketTask.onMessage((result) => {
            messageHandler(JSON.parse(result.data))
        })
    //==================================
    // 监听连接打开
    socketTask.onOpen((res) => {
      console.log('✅ WebSocket连接已建立')
      // reconnectAttempts = 0 // 连接成功后重置重连次数
      // 可以在这里发送心跳或订阅消息
      // startHeartbeat()
    })
    // 监听连接关闭
    socketTask.onClose((res) => {
      console.log(`WebSocket连接关闭,代码: ${res.code}, 原因: ${res.reason}`)
      // 根据不同的关闭代码处理
      if (res.code === 1000) { // 正常关闭
        console.log('连接正常关闭')
      } else if (res.code === 1006) { // 异常关闭
        console.log('连接异常关闭,尝试重连...')
      } else if (res.code === 1011) { // 服务器内部错误
        console.log('服务器内部错误(1011),延迟重连...')
      } else {
        console.log('其他原因关闭,尝试重连...')
      }
    })
    // 监听错误
    socketTask.onError((err) => {
      console.error('WebSocket发生错误:', err)
    })
    }
    // watch(access_token, initWS, {immediate: true})
    { immediate: true }
  )
}
uniapps/work-app/src/pages/login/index.vue
@@ -45,6 +45,7 @@
import md5 from "js-md5";
import { loginByUsername } from "@/api/user/index.js";
import { useUserStore } from "@/store/index.js";
import websocketService from "@/utils/websocket.js";
import { HOME_PATH, LOGIN_PATH, removeQueryString } from "@/router";
@@ -134,7 +135,59 @@
      userInfo.key,
      userInfo.code
    );
    console.log('登录成功,用户信息:', res.data.data);
    userStore.setUserInfo(res.data.data);
    // 初始化WebSocket连接
    try {
      const wsUrl = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
      // 设置默认用户ID为3
      const defaultUserId = '3';
      // 使用默认用户ID初始化WebSocket连接
      websocketService.init(defaultUserId, wsUrl);
      console.log('登录成功,WebSocket已初始化,默认用户ID:', defaultUserId);
      // 设置来电消息回调
      websocketService.setOnMessageCallback((message) => {
        if (message.type === 'call') {
          console.log('收到来电,准备跳转到语音通话页面');
          // 构建来电参数
          const callParams = {
            peerUid: message.from,
            from: message.from,
            type: 'incoming'
          };
          // 转义参数以便在URL中传递
          const encodedParams = encodeURIComponent(JSON.stringify(callParams));
          try {
            // 优先使用uni-app的导航API(适用于同应用内跳转)
            if (typeof uni !== 'undefined' && uni.navigateTo) {
              console.log('使用uni-app导航API跳转到语音通话页面');
              uni.navigateTo({
                url: `/subPackages/voiceCallDetail/index?params=${encodedParams}`
              });
            }
            // 否则使用window.location跳转(适用于Web环境)
            else if (typeof window !== 'undefined' && window.location) {
              console.log('使用window.location跳转到语音通话页面');
              // 注意:实际部署时需要使用正确的URL
              const voiceCallUrl = `http://localhost:8080/appPages/voiceCallDetail/index.html?params=${encodedParams}`;
              window.location.href = voiceCallUrl;
            } else {
              console.error('无法跳转到语音通话页面:当前环境不支持导航');
            }
          } catch (error) {
            console.error('跳转到语音通话页面失败:', error);
          }
        }
      });
    } catch (error) {
      console.error('WebSocket初始化失败:', error);
    }
    uni.reLaunch({
      url: "/pages/work/index",
    });
uniapps/work-app/src/utils/websocket.js
New file
@@ -0,0 +1,188 @@
// WebSocket服务管理
class WebSocketService {
  constructor() {
    this.socketTask = null
    this.connected = false
    this.url = ''
    this.userId = ''
    this.onMessageCallback = null
    this.onOpenCallback = null
    this.onCloseCallback = null
    this.onErrorCallback = null
    this.pingTimer = null
  }
  // 初始化WebSocket连接
  init(userId, url) {
    if (!url) {
      console.error('WebSocket初始化失败:缺少url')
      return
    }
    this.userId = userId || ''
    this.url = url + encodeURIComponent(this.userId)
    this.connect()
  }
  // 建立WebSocket连接
  connect() {
    try {
      // 关闭现有连接
      this.close()
      // 使用uni-app的WebSocket API
      this.socketTask = uni.connectSocket({
        url: this.url,
        success: () => {
          console.log('WebSocket连接已请求')
        },
        fail: (error) => {
          console.error('WebSocket连接请求失败:', error)
          if (this.onErrorCallback) {
            this.onErrorCallback(error)
          }
        }
      })
      // 监听连接打开
      this.socketTask.onOpen(() => {
        this.connected = true
        console.log('WebSocket已连接,userId:', this.userId)
        this.startPing()
        if (this.onOpenCallback) {
          this.onOpenCallback()
        }
      })
      // 监听连接关闭
      this.socketTask.onClose(() => {
        this.connected = false
        console.log('WebSocket已关闭')
        this.stopPing()
        if (this.onCloseCallback) {
          this.onCloseCallback()
        }
      })
      // 监听连接错误
      this.socketTask.onError((error) => {
        console.error('WebSocket错误:', error)
        this.connected = false
        this.stopPing()
        if (this.onErrorCallback) {
          this.onErrorCallback(error)
        }
      })
      // 监听消息
      this.socketTask.onMessage((res) => {
        try {
          const message = JSON.parse(res.data)
          console.log('收到WebSocket消息:', message)
          if (this.onMessageCallback) {
            this.onMessageCallback(message)
          }
        } catch (error) {
          console.error('解析WebSocket消息失败:', error)
        }
      })
    } catch (error) {
      console.error('WebSocket连接失败:', error)
      if (this.onErrorCallback) {
        this.onErrorCallback(error)
      }
    }
  }
  // 发送消息
  send(type, to, payload) {
    if (!type || !this.socketTask || !this.connected) {
      console.error('WebSocket发送失败:连接未建立')
      return
    }
    const message = {
      type,
      from: String(this.userId),
      to: String(to),
      payload: payload || null,
      timestamp: Date.now()
    }
    this.socketTask.send({
      data: JSON.stringify(message),
      success: () => {
        console.log('WebSocket消息发送成功:', message)
      },
      fail: (error) => {
        console.error('WebSocket消息发送失败:', error)
      }
    })
  }
  // 开始心跳检测
  startPing() {
    this.stopPing()
    // 每25秒发送一次心跳
    this.pingTimer = setInterval(() => {
      if (this.connected) {
        this.send('ping', 'system')
      }
    }, 25000)
  }
  // 停止心跳检测
  stopPing() {
    if (this.pingTimer) {
      clearInterval(this.pingTimer)
      this.pingTimer = null
    }
  }
  // 关闭连接
  close() {
    if (this.socketTask) {
      this.socketTask.close({
        code: 1000,
        reason: '正常关闭',
        success: () => {
          console.log('WebSocket已关闭')
        },
        fail: (error) => {
          console.error('WebSocket关闭失败:', error)
        }
      })
      this.socketTask = null
    }
    this.stopPing()
    this.connected = false
  }
  // 设置消息回调
  setOnMessageCallback(callback) {
    this.onMessageCallback = callback
  }
  // 设置连接打开回调
  setOnOpenCallback(callback) {
    this.onOpenCallback = callback
  }
  // 设置连接关闭回调
  setOnCloseCallback(callback) {
    this.onCloseCallback = callback
  }
  // 设置错误回调
  setOnErrorCallback(callback) {
    this.onErrorCallback = callback
  }
  // 获取连接状态
  getConnected() {
    return this.connected
  }
}
// 导出单例实例
export default new WebSocketService()