吉安感知网项目-前端
罗广辉
2026-01-13 9f1828431a15fc96d8bc5ee230429030bdbbc469
Merge remote-tracking branch 'origin/master'
3 files modified
290 ■■■■ changed files
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue 250 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/voiceCall/index.vue 12 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue 28 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -45,13 +45,14 @@
  
      <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
    </div>
  </template>
  <script setup>
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
  /** ✅ 你的 WS 地址前缀(后面拼 userId) */
  const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
/** ✅ 你的 WS 地址前缀(后面拼 userId) */
const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
  // 解析URL参数
  const parseUrlParams = () => {
@@ -73,38 +74,38 @@
  const uid = ref('3')
  const peerUid = ref('2')
  const connected = ref(false)
  const state = ref('idle') // idle | calling | ringing | in-call
  const logText = ref('')
  const remoteAudio = ref(null)
    /**
     * 根据状态返回对应颜色
     */
    const getStateColor = (state) => {
        const colors = {
            'idle': '#666',
            'calling': '#2196F3',
            'ringing': '#FF9800',
            'in-call': '#4CAF50'
        }
        return colors[state] || '#666'
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
const logText = ref('')
const remoteAudio = ref(null)
/**
 * 根据状态返回对应颜色
 */
const getStateColor = (state) => {
    const colors = {
        'idle': '#666',
        'calling': '#2196F3',
        'ringing': '#FF9800',
        'in-call': '#4CAF50'
    }
  let ws = null
  let pc = null
  let localStream = null
  const pendingCandidates = []
  // 来电暂存
  let incomingFrom = null
  let acceptedByMe = false // 我是否已经点了接听
  let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
  // 计时
  let timer = null
  const seconds = ref(0)
  const durationText = computed(() => {
    return colors[state] || '#666'
}
let ws = null
let pc = null
let localStream = null
const pendingCandidates = []
// 来电暂存
let incomingFrom = null
let acceptedByMe = false // 我是否已经点了接听
let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
// 计时
let timer = null
const seconds = ref(0)
const durationText = computed(() => {
    const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
    const s = String(seconds.value % 60).padStart(2, '0')
    return `${m}:${s}`
@@ -162,122 +163,123 @@
  
  function createPC() {
    pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    })
    pc.onicecandidate = (e) => {
      if (e.candidate) {
        send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
      }
        if (e.candidate) {
            send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
        }
    }
    pc.ontrack = (e) => {
      log('✅ 收到远端音频流')
      const stream = e.streams[0]
      if (remoteAudio.value) {
        remoteAudio.value.srcObject = stream
        remoteAudio.value.play().catch(() => {})
      }
        log('✅ 收到远端音频流')
        const stream = e.streams[0]
        if (remoteAudio.value) {
            remoteAudio.value.srcObject = stream
            remoteAudio.value.play().catch(() => {
            })
        }
    }
    pc.onconnectionstatechange = () => {
      log('pc.connectionState =', pc.connectionState)
        log('pc.connectionState =', pc.connectionState)
    }
  }
  async function getLocalAudio() {
}
async function getLocalAudio() {
    if (localStream) return localStream
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
      throw new Error('getUserMedia not available')
        log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
        throw new Error('getUserMedia not available')
    }
    try {
      localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
      log('✅ 已获取本地麦克风')
      return localStream
        localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        log('✅ 已获取本地麦克风')
        return localStream
    } catch (e) {
      log('❌ 获取麦克风失败:', String(e))
      throw e
        log('❌ 获取麦克风失败:', String(e))
        throw e
    }
  }
  /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
  async function ensureLocalTracksAdded() {
}
/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
async function ensureLocalTracksAdded() {
    if (!pc) createPC()
    const stream = await getLocalAudio()
    const existingTrackIds = new Set(
      pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
        pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
    )
    stream.getTracks().forEach(track => {
      if (!existingTrackIds.has(track.id)) {
        pc.addTrack(track, stream)
        log('addTrack ok:', track.id)
      } else {
        // log('skip addTrack:', track.id)
      }
        if (!existingTrackIds.has(track.id)) {
            pc.addTrack(track, stream)
            log('addTrack ok:', track.id)
        } else {
            // log('skip addTrack:', track.id)
        }
    })
    return stream
  }
  async function flushCandidates() {
}
async function flushCandidates() {
    if (!pc || !pc.remoteDescription) return
    while (pendingCandidates.length) {
      const c = pendingCandidates.shift()
      try {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } catch (e) {
        log('addIceCandidate failed:', String(e))
      }
        const c = pendingCandidates.shift()
        try {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } catch (e) {
            log('addIceCandidate failed:', String(e))
        }
    }
  }
  /* ---------------- 信令处理 ---------------- */
  async function onSignal(msg) {
}
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
    if (!msg) return
    const t = (msg.type || '').toString()
    // ✅ 忽略后端回显给自己的消息(你的后端会回显 sender)
    if (msg.from && String(msg.from) === String(uid.value)) {
      return
        return
    }
    // system 消息:login/pong/busy 仅记录
    if (msg.from === 'system') {
      log('system msg:', msg)
      return
        log('system msg:', msg)
        return
    }
    if (!t) return
    if (t === 'call') {
      // 收到来电:弹接听按钮
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      acceptedByMe = false
      offeredByPeer = false
      state.value = 'ringing'
      log('📞 收到来电 call,来自', incomingFrom)
      return
        // 收到来电:弹接听按钮
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        acceptedByMe = false
        offeredByPeer = false
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        return
    }
    if (t === 'accept') {
      // 主叫收到 accept:开始 offer
      log('✅ 对方接听 accept,开始建立连接(offer)')
      await startOffer()
      return
        // 主叫收到 accept:开始 offer
        log('✅ 对方接听 accept,开始建立连接(offer)')
        await startOffer()
        return
    }
    if (t === 'busy') {
      log('❌ 对方忙线/拒绝')
      cleanup(false)
      return
        log('❌ 对方忙线/拒绝')
        cleanup(false)
        return
    }
  
    if (t === 'offer') {
@@ -473,12 +475,14 @@
    lastOfferSdp = null
  
    state.value = 'idle'
  }
  onMounted(() => {
    // 解析URL参数
    parseUrlParams()
  })
}
onMounted(() => {
    const receiveParameters = route.query.contact
    console.log('接收参数', receiveParameters)
    // 解析URL参数
    parseUrlParams()
})
  onBeforeUnmount(() => {
    cleanup(false)
uniapps/work-app/src/pages/voiceCall/index.vue
@@ -120,15 +120,11 @@
}
// 拨打电话方法
const makeCall = (contact) => {
  const contactName = contact.friendNickName || '未知联系人'
  console.log('拨打电话给', contactName)
  // 这里可以添加实际的拨打电话逻辑
  // uni.showToast({
  //   title: `正在拨打${contactName}的电话`,
  //   icon: 'none'
  // })
  console.log('拨打电话', contact)
  // 将contact对象转换为JSON字符串并URL编码
  const contactStr = encodeURIComponent(JSON.stringify(contact))
  uni.navigateTo({
    url: `/subPackages/voiceCallDetail/index`,
    url: `/subPackages/voiceCallDetail/index?contact=${contactStr}`,
  })
}
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
@@ -1,25 +1,29 @@
<!-- 登录页 -->
<template>
   <!-- <web-view :src="`${viewUrl}`" @message="viewMessage" :allow="allow"/> -->
   <WebViewPlus
     ref="sWebViewRef"
     :src="`${viewUrl}`"
     @webMessage="onPostMessage"
   />
  <WebViewPlus
    ref="sWebViewRef"
    :src="`${viewUrl}`"
    @webMessage="onPostMessage"
  />
</template>
<script setup>
import { useUserStore } from "@/store/index.js"
import { getWebViewUrl } from "@/utils/index.js";
import { onLoad } from "@dcloudio/uni-app";
const userStore = useUserStore()
const userParams = userStore?.userInfo ? JSON.stringify(userStore.userInfo) : '{}'
const sWebViewRef = ref(null);
const viewUrl = ref("");
onLoad((options) => {
      // const currentItem = options.currentItem;
      // viewUrl.value = getWebViewUrl("/mapWork", { currentItem: currentItem });
      //       viewUrl.value = getWebViewUrl("/voiceCallDetail");
          viewUrl.value = 'https://192.168.57.124:5175/drone-command/demo';
    });
    function onPostMessage(data) {}
  // 解析传递过来的contact参数
  const contact = options.contact ? JSON.parse(decodeURIComponent(options.contact)) : null;
  // 构建viewUrl,将contact参数拼接到URL中
  const contactParam = contact ? encodeURIComponent(JSON.stringify(contact)) : '';
  viewUrl.value = `https://192.168.1.157:5176/mobile-web-view/#/webViewWrapper/voiceCallDetail?params=${encodeURIComponent(userParams)}&contact=${contactParam}`;
  // viewUrl.value = 'https://192.168.57.124:5178/drone-command/demo';
});
function onPostMessage(data) {}
// #ifdef APP-PLUS
function requestAndroidMicPermission() {
  return new Promise((resolve) => {