From e4439e5d0a2cba9983f013bd044fe4e22c7b7077 Mon Sep 17 00:00:00 2001
From: chenyao <1219716595@qq.com>
Date: Wed, 11 Feb 2026 16:35:06 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 applications/task-work-order/src/utils/auth.js                      |    5 
 applications/drone-command/src/assets/images/active-menu-item.png   |    0 
 applications/drone-command/src/page/index/top/index.vue             |  340 +++++++++++++++++++++++++------------
 uniapps/work-app/src/api/voiceCall/index.js                         |    2 
 applications/drone-command/src/utils/auth.js                        |    4 
 applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue |   45 +++-
 applications/drone-command/src/assets/images/menu-item.png          |    0 
 uniapps/work-app/src/subPackages/voiceCallDetail/index.vue          |   83 ++++++--
 uniapps/work-app/src/pages/voiceCall/index.vue                      |    2 
 9 files changed, 326 insertions(+), 155 deletions(-)

diff --git a/applications/drone-command/src/assets/images/active-menu-item.png b/applications/drone-command/src/assets/images/active-menu-item.png
new file mode 100644
index 0000000..39fd486
--- /dev/null
+++ b/applications/drone-command/src/assets/images/active-menu-item.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/menu-item.png b/applications/drone-command/src/assets/images/menu-item.png
new file mode 100644
index 0000000..3b24a46
--- /dev/null
+++ b/applications/drone-command/src/assets/images/menu-item.png
Binary files differ
diff --git a/applications/drone-command/src/page/index/top/index.vue b/applications/drone-command/src/page/index/top/index.vue
index e7f7b93..d090627 100644
--- a/applications/drone-command/src/page/index/top/index.vue
+++ b/applications/drone-command/src/page/index/top/index.vue
@@ -1,38 +1,49 @@
 <template>
-  <div class="avue-top">
-    <div class="top-bar__title">
-      <img :src="logoUrl" alt="">
-      <span>低空飞行监管子系统</span>
-    </div>
+  <header class="header-container">
+    <div class="content-wrap">
+      <div class="logo-title-wrap">
+        <img :src="logoUrl" alt="Logo"></img>
+        <p class="title">低空飞行监管子系统</p>
+      </div>
 
-    <div class="top-bar__right">
-      <div class="top-user">
-        <div class="icon-box">
-         <img class="gateway" @click="jumpMH" src="@/assets/images/mh.png" alt="进入门户" title="进入门户">
-        </div>
+      <div class="header-right">
+        <nav class="nav-menu">
+          <div v-for="(item, index) in topMenus" :key="index" class="nav-item"
+            :class="{ active: item.active }" @click="handleMenuClick(item)">
+            <span>{{ item.label }}</span>
+          </div>
+        </nav>
 
-        <el-dropdown popper-class="command-custom-dropdown">
-          <span class="el-dropdown-link">
-            <img class="top-bar__img" :src="userInfo.avatar" alt="" />
-          </span>
+        <div class="icon-group">
+          <div class="top-user">
+            <div class="icon-box">
+              <img class="gateway" @click="jumpMH" src="@/assets/images/mh.png" alt="进入门户" title="进入门户">
+            </div>
 
-          <template #dropdown>
-            <el-dropdown-menu>
-              <!--              <el-dropdown-item>
+            <el-dropdown popper-class="command-custom-dropdown">
+              <span class="el-dropdown-link">
+                <img class="top-bar__img" :src="userInfo.avatar" alt="" />
+              </span>
+
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <!--              <el-dropdown-item>
                 <router-link to="/">{{ $t('navbar.dashboard') }}</router-link>
               </el-dropdown-item>-->
-              <!-- <el-dropdown-item>
+                  <!-- <el-dropdown-item>
                 <router-link to="/info/index">{{ $t('navbar.userinfo') }}</router-link>
               </el-dropdown-item> -->
-              <el-dropdown-item @click="logout" divided>{{ $t('navbar.logOut') }}
-              </el-dropdown-item>
-            </el-dropdown-menu>
-          </template>
-        </el-dropdown>
-        <top-setting></top-setting>
+                  <el-dropdown-item @click="logout" divided>{{ $t('navbar.logOut') }}
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+            <top-setting></top-setting>
+          </div>
+        </div>
       </div>
     </div>
-  </div>
+  </header>
 </template>
 
 <script>
@@ -69,6 +80,35 @@
   data () {
     return {
       logoUrl: logo,
+      activeMenu: 'drone-control',
+      topMenus: [
+        {
+          key: 'twin-supervision',
+          label: '孪生监管',
+          path: '/flight-supervision/#/brain',
+        },
+        {
+          key: 'airspace-collaboration',
+          label: '空域协同',
+          path: '/flight-supervision/#/space',
+        },
+        {
+          key: 'air-traffic',
+          label: '空中交通',
+          path: '/flight-supervision/#/activity',
+        },
+        {
+          key: 'information-service',
+          label: '信息服务',
+          path: '/flight-supervision/#/infoService',
+        },
+        {
+          key: 'drone-control',
+          label: '无人机管控',
+          path: '',
+          active: true,
+        },
+      ],
     }
   },
   filters: {},
@@ -93,115 +133,189 @@
         cancelButtonClass: 'command-message-box-cancel',
       }).then(() => {
         this.$store.dispatch('LogOut').then(() => {
-					const {VITE_APP_PARENT_SYSTEM,VITE_APP_ENV} = import.meta.env
-					const isDev = VITE_APP_ENV === 'development'
-					isDev
-						? this.$router.push({ path: '/login' })
-						: window.location.replace(`${VITE_APP_PARENT_SYSTEM}/#/login`)
+          const { VITE_APP_PARENT_SYSTEM, VITE_APP_ENV } = import.meta.env
+          const isDev = VITE_APP_ENV === 'development'
+          isDev
+            ? this.$router.push({ path: '/login' })
+            : window.location.replace(`${VITE_APP_PARENT_SYSTEM}/#/login`)
         })
       })
     },
     jumpMH () {
-
+      window.open(`${window.location.origin}/droneWeb/#/gateway`, '_blank')
+    },
+    handleMenuClick (item) {
+      this.activeMenu = item.key
+      if (!item.path) return
+      window.open(`${window.location.origin}${item.path}`, '_blank')
     },
   },
 }
 </script>
 
 <style lang="scss" scoped>
-.avue-top {
-  display: flex;
-  justify-content: space-between;
-  height: 100%;
+.header-container {
+  position: relative;
+  height: 110px;
   background: url('@/assets/images/topContainer/top-bg.png') center / 100% 100% no-repeat !important;
+  background-repeat: no-repeat;
+  background-size: cover;
   pointer-events: none;
-}
+  z-index: 2;
 
-.top-bar__left {
-  flex: 0 0 auto;
-}
-
-.top-bar__title {
-  margin-top: 16px;
-  margin-left: 30px;
-  flex: 1;
-  display: flex;
-  align-items: center;
-  height: pxToVh(48);
-
-  img {
-    margin-right: 17px;
-    width: 50px;
-    height: 50px;
+  .bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    pointer-events: none;
   }
 
-  span {
-    width: 353px;
-    height: 43px;
-    font-family: PangMen;
-    font-weight: 400;
-    font-size: 36px;
-    color: #FFFFFF;
-    letter-spacing: 3px;
-    text-shadow: 3px 3px 0px rgba(0, 13, 42, 0.27);
-    text-align: left;
-    font-style: normal;
-    text-transform: none;
-  }
-}
-
-.top-bar__right {
-  margin-right: 28px;
-  padding-top: 16px;
-  flex: 0 0 auto;
-  display: flex !important;
-  align-items: flex-start;
-  height: 100%;
-  pointer-events: auto;
-  box-sizing: border-box;
-
-  .icon-box {
-    margin-top: 6px;
-    margin-right: 30px;
+  .content-wrap {
+    position: absolute;
+    inset: 0;
     display: flex;
     align-items: center;
-    gap: 0 20px;
+    z-index: 1;
+    padding: 0 40px;
 
-    .gateway {
-      width: 21px;
-      height: 18px;
+    .logo-title-wrap {
+      display: flex;
+      align-items: center;
+      transform: translateY(-10px);
+
+      img {
+        width: 70px;
+        height: 63px;
+        vertical-align: middle;
+      }
+
+      .title {
+        margin-left: 16px;
+        margin-top: 0;
+        margin-bottom: 0;
+
+        width: 353px;
+        height: 43px;
+        font-family: PangMen;
+        font-weight: 400;
+        font-size: 36px;
+        color: #FFFFFF;
+        letter-spacing: 3px;
+        text-shadow: 3px 3px 0px rgba(0, 13, 42, 0.27);
+        text-align: left;
+        font-style: normal;
+        text-transform: none;
+      }
     }
 
-    >* {
-      cursor: pointer;
+    .header-right {
+      display: flex;
+      align-items: center;
+      margin-left: auto;
+      margin-top: -25px;
+      pointer-events: auto;
+
+      .nav-menu {
+        display: flex;
+        margin-right: 30px;
+        gap: 0px;
+
+        .nav-item {
+          width: 136px;
+          height: 34px;
+          box-sizing: border-box;
+          text-align: center;
+          color: #afafe0;
+          text-decoration: none;
+          font-size: 20px;
+          transition: color 0.3s;
+          padding: 0;
+          background: url('@/assets/images/menu-item.png') no-repeat center / 136px 34px;
+          cursor: pointer;
+          margin: 0 10px;
+
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding-bottom: 10px;
+          line-height: 1;
+
+          font-family: YouSheBiaoTiHei;
+
+          &:hover {
+            color: #fff;
+          }
+
+          &.active {
+            color: #fff;
+            background: url('@/assets/images/active-menu-item.png') no-repeat center / 136px 34px;
+
+            span {
+              background: linear-gradient(180deg,
+                  #fff 22.11%,
+                  #ffffff 86.69%);
+              background-size: contain;
+              background-clip: text;
+              -webkit-text-fill-color: transparent;
+              text-shadow: 0px 0px 13px 0px #5da6ef73;
+            }
+          }
+        }
+      }
+
+      .icon-group {
+        display: flex;
+        align-items: center;
+        margin-right: 20px;
+        gap: 15px;
+
+
+        .icon-box {
+          margin-top: 6px;
+          margin-right: 30px;
+          display: flex;
+          align-items: center;
+          gap: 0 20px;
+
+          .gateway {
+            width: 21px;
+            height: 18px;
+          }
+
+          >* {
+            cursor: pointer;
+          }
+        }
+      }
+
+      .top-bar__item {
+        margin-right: 15px;
+        display: inline-block !important;
+      }
+
+      .top-user {
+        display: flex;
+        align-items: center;
+      }
+
+      .top-bar__img {
+        margin-top: 5px;
+        width: 20px;
+        height: 20px;
+        border: 2px solid #383874;
+        border-radius: 50%;
+      }
+
+      .el-dropdown-link {
+        cursor: pointer;
+        color: #606266;
+      }
+
+      .el-dropdown-link:hover {
+        color: #409EFF;
+      }
     }
   }
-}
-
-.top-bar__item {
-  margin-right: 15px;
-  display: inline-block !important;
-}
-
-.top-user {
-  display: flex;
-  align-items: center;
-}
-
-.top-bar__img {
-  margin-top: 5px;
-  width: 20px;
-  height: 20px;
-  border: 2px solid #383874;
-  border-radius: 50%;
-}
-
-.el-dropdown-link {
-  cursor: pointer;
-  color: #606266;
-}
-
-.el-dropdown-link:hover {
-  color: #409EFF;
 }
 </style>
diff --git a/applications/drone-command/src/utils/auth.js b/applications/drone-command/src/utils/auth.js
index 4171132..e82ec6a 100644
--- a/applications/drone-command/src/utils/auth.js
+++ b/applications/drone-command/src/utils/auth.js
@@ -10,8 +10,8 @@
  */
 import Cookies from 'js-cookie';
 
-const TokenKey = 'saber3-access-token';
-const RefreshTokenKey = 'saber3-refresh-token';
+const TokenKey = 'command-access-token';
+const RefreshTokenKey = 'command-refresh-token';
 const SessionId = 'JSESSIONID';
 const UserId = 'b-user-id';
 
diff --git a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
index 6a2134c..ec529b9 100644
--- a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
+++ b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -41,8 +41,8 @@
 import defaultAvatar from '@/appDataSource/appwork/defaultAvatar.svg'
 const route = useRoute()
 /** ✅ 你的 WS 地址前缀(后面拼 userId) */
-const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
-
+// const WS_BASE = 'ws://218.202.104.82:38201/ws/chat?userId='
+const WS_BASE = 'ws://218.202.104.82:38201/ws/chat'
 // 解析URL参数
 const parseUrlParams = () => {
 	// 1. 优先从route.query.voiceparams获取参数
@@ -105,11 +105,11 @@
 	if (userId) {
 		return userId
 	}
-	return '8' // 默认值
+	return '3' // 默认值
 }
 
-const uid = ref('8')
-const peerUid = ref('7')
+const uid = ref('3')
+const peerUid = ref('2')
 // 联系人名称
 const contactName = ref('张三')
 
@@ -121,6 +121,8 @@
 const callEnded = ref(false)
 const endMessage = ref('对方已挂断')
 const endTimer = ref(null)
+// token 存储
+const accessToken = ref('')
 
 let ws = null
 let pc = null
@@ -227,7 +229,17 @@
 	}
 
 	try {
-		localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
+		localStream = await navigator.mediaDevices.getUserMedia({ 
+			audio: {
+				echoCancellation: true,
+				noiseSuppression: true,
+				autoGainControl: true,
+				// 移动端特定配置
+				channelCount: 1,
+				sampleRate: 16000,
+				sampleSize: 16
+			}
+	  	})
 		log('✅ 已获取本地麦克风')
 		return localStream
 	} catch (e) {
@@ -378,8 +390,10 @@
 
 	if (ws) ws.close()
 
-	ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
-
+	// 使用从参数中获取的token或默认token
+	const token = accessToken.value
+	// ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
+	ws = new WebSocket(WS_BASE, token)
 	ws.onopen = () => {
 		connected.value = true
 		log('WS 已连接 userId=', uid.value)
@@ -558,8 +572,19 @@
 
 onMounted(() => {
 	console.log('收拾收拾',route.query.voiceparams);
-	console.log('接收参数', JSON.parse(decodeURIComponent(receiveParameters.value)))
-
+	console.log('接收参数', receiveParameters)
+	if (route.query && route.query.params) {
+		try {
+			const params = JSON.parse(decodeURIComponent(route.query.params));
+			// 获取token
+			if (params.access_token) {
+				accessToken.value = params.access_token;
+				log('🔗 从params获取access_token:', accessToken.value);
+			}	
+		} catch (e) {
+			log('❌ 解析params参数失败:', String(e));
+		}
+	}
 	// 解析并使用contact参数
 	if (receiveParameters.value) {
 		try {
diff --git a/applications/task-work-order/src/utils/auth.js b/applications/task-work-order/src/utils/auth.js
index 4e41090..22a03a6 100644
--- a/applications/task-work-order/src/utils/auth.js
+++ b/applications/task-work-order/src/utils/auth.js
@@ -1,8 +1,9 @@
 import Cookies from 'js-cookie';
 
-const TokenKey = 'saber3-access-token';
-const RefreshTokenKey = 'saber3-refresh-token';
+const TokenKey = 'work-access-token';
+const RefreshTokenKey = 'work-refresh-token';
 const SessionId = 'JSESSIONID';
+
 const UserId = 'b-user-id';
 
 export function getToken() {
diff --git a/uniapps/work-app/src/api/voiceCall/index.js b/uniapps/work-app/src/api/voiceCall/index.js
index 448689d..2b7a26e 100644
--- a/uniapps/work-app/src/api/voiceCall/index.js
+++ b/uniapps/work-app/src/api/voiceCall/index.js
@@ -2,7 +2,7 @@
 
 export const getPhoneBookListApi = (data) => {
   return request({
-    url: '/webservice/jaUserContact/pageInfo',
+    url: '/system/user/listCommunication',
     method: 'post',
     data,
   })
diff --git a/uniapps/work-app/src/pages/voiceCall/index.vue b/uniapps/work-app/src/pages/voiceCall/index.vue
index 0a6b987..96f466d 100644
--- a/uniapps/work-app/src/pages/voiceCall/index.vue
+++ b/uniapps/work-app/src/pages/voiceCall/index.vue
@@ -20,7 +20,7 @@
 
           <!-- 联系人信息 -->
           <div class="contactInfo">
-            <div class="contactName">{{ contact.friendNickName  || '未知联系人' }}</div>
+            <div class="contactName">{{ contact.nickName || '未知联系人' }}</div>
             <div class="contactDept">{{ contact.deptName || '未知部门' }}</div>
           </div>
 
diff --git a/uniapps/work-app/src/subPackages/voiceCallDetail/index.vue b/uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
index 32fde49..86c915e 100644
--- a/uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
+++ b/uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
@@ -15,7 +15,62 @@
 const userParams = userStore?.userInfo ? JSON.stringify(userStore.userInfo) : '{}'
 const sWebViewRef = ref(null);
 const viewUrl = ref("");
-onLoad((options) => {
+
+// #ifdef APP-PLUS
+/**
+ * 请求 Android 麦克风权限
+ * @returns {Promise<boolean>} 权限是否授予
+ */
+async function requestAndroidMicPermission() {
+  try {
+    // 使用 uni 的权限 API(需要 HBuilderX 3.4.0+)
+    if (uni.requestPermissions) {
+      const result = await uni.requestPermissions({
+        permissions: ['android.permission.RECORD_AUDIO']
+      })
+      
+      console.log('权限请求结果:', result)
+      
+      if (result[0].granted) {
+        console.log('✅ 麦克风权限已授予')
+        return true
+      } else {
+        console.warn('⚠️ 麦克风权限被拒绝')
+        
+        // 可以提示用户
+        uni.showModal({
+          title: '权限提示',
+          content: '语音通话需要麦克风权限,请前往设置开启',
+          showCancel: false,
+          success: () => {
+            // 跳转到应用设置
+            plus.runtime.openURL(plus.runtime.getProperty('package') + '://settings')
+          }
+        })
+        return false
+      }
+    }
+    
+    // 降级方案:使用原生方法
+    return await requestAndroidMicPermissionNative()
+    
+  } catch (error) {
+    console.error('❌ 请求麦克风权限失败:', error)
+    return false
+  }
+}
+// #endif
+
+onLoad(async (options) => {
+  // #ifdef APP-PLUS
+  // 在加载 WebView 前先请求麦克风权限
+  console.log('📞 语音通话页面加载,请求麦克风权限')
+  const hasPermission = await requestAndroidMicPermission()
+  if (!hasPermission) {
+    console.warn('⚠️ 未获得麦克风权限,通话功能可能无法使用')
+  }
+  // #endif
+
   // 解析传递过来的contact参数
   const contact = options.contact ? JSON.parse(decodeURIComponent(options.contact)) : null;
   // 构建viewUrl,将contact参数拼接到URL中
@@ -23,32 +78,8 @@
   // viewUrl.value = `https://192.168.1.157:5176/mobile-web-view/#/webViewWrapper/voiceCallDetail?params=${encodeURIComponent(userParams)}&contact=${contactParam}`;
   viewUrl.value = getWebViewUrl("/voiceCallDetail", { contact: options.contact ,voiceparams: options.voiceparams});
 });
+
 function onPostMessage(data) {}
-// #ifdef APP-PLUS
-function requestAndroidMicPermission() {
-  return new Promise((resolve) => {
-    const main = plus.android.runtimeMainActivity()
-    const Build = plus.android.importClass('android.os.Build')
-    if (Build.VERSION.SDK_INT < 23) return resolve(true)
-
-    const Manifest = plus.android.importClass('android.Manifest')
-    const ActivityCompat = plus.android.importClass('androidx.core.app.ActivityCompat')
-
-    const permission = Manifest.permission.RECORD_AUDIO
-    ActivityCompat.requestPermissions(main, [permission], 1001)
-
-    // 简化:延迟检查一次(更严谨可写原生回调插件)
-    setTimeout(() => {
-      const PackageManager = plus.android.importClass('android.content.pm.PackageManager')
-      const granted = ActivityCompat.checkSelfPermission(main, permission) === PackageManager.PERMISSION_GRANTED
-      resolve(granted)
-    }, 800)
-  })
-}
-
-
-requestAndroidMicPermission()
-// #endif
 
 </script>
 

--
Gitblit v1.9.3