新增voice页面
This commit is contained in:
		
							parent
							
								
									09dc671216
								
							
						
					
					
						commit
						c96d550001
					
				
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							@ -13,6 +13,10 @@ p2p-explorer-web 是一个基于浏览器的 webtrc P2P 远程文件传输工具
 | 
				
			|||||||
- 🌐 无需安装,基于浏览器即可使用
 | 
					- 🌐 无需安装,基于浏览器即可使用
 | 
				
			||||||
- 💨 快速分享链接功能
 | 
					- 💨 快速分享链接功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 需要注意使用的服务
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					文件交互全部走 webrtc,使用 peerjs 的信令服务器(wss://0.peerjs.com/peerjs)交换 ice 候选(可信,没有本地数据上传),使用 peerjs 的打洞 0.peerjs.com:(可信,没有数据上传),当 udp 直连失败,会使用 peerjs 的中转服务(不可信,所有数据上传),后续会 docker 化,使用自己的 stun 与 turn 自定义部署。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 🚀 快速开始
 | 
					## 🚀 快速开始
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 安装
 | 
					### 安装
 | 
				
			||||||
@ -56,8 +60,13 @@ cp -r static dist/
 | 
				
			|||||||
## 待完成
 | 
					## 待完成
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- 本地发送到远程的 ui 展示
 | 
					- 本地发送到远程的 ui 展示
 | 
				
			||||||
- 屏幕预览
 | 
					- 屏幕预览 语音沟通 引用文件文字交流
 | 
				
			||||||
- docker 化,使用自己的 stun 与 turn 自定义部署
 | 
					- docker 化,使用自己的 stun 与 turn 自定义部署
 | 
				
			||||||
- 各种异常处理需要闭环
 | 
					- 各种异常处理需要闭环
 | 
				
			||||||
- 英文版本
 | 
					- 英文版本
 | 
				
			||||||
- 打包下载
 | 
					- 打包下载
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 感谢
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[PeerJS](https://peerjs.com/) 提供的 WebRTC 支持
 | 
				
			||||||
 | 
					[Ant Design Vue](https://2x.antdv.com/docs/vue/introduce-cn/) 提供的 UI 支持
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,8 @@
 | 
				
			|||||||
    "highlight.js": "^11.11.1",
 | 
					    "highlight.js": "^11.11.1",
 | 
				
			||||||
    "peerjs": "^1.5.4",
 | 
					    "peerjs": "^1.5.4",
 | 
				
			||||||
    "vue": "^3.3.0",
 | 
					    "vue": "^3.3.0",
 | 
				
			||||||
    "vue-i18n": "^9.1.9"
 | 
					    "vue-i18n": "^9.1.9",
 | 
				
			||||||
 | 
					    "vue-router": "^4.5.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@rushstack/eslint-patch": "^1.6.1",
 | 
					    "@rushstack/eslint-patch": "^1.6.1",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Index />
 | 
					  <RouterView />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { onMounted, onUnmounted } from "vue";
 | 
					import { onMounted, onUnmounted } from "vue";
 | 
				
			||||||
import Index from "./pages/file/index.vue";
 | 
					import { RouterView } from "vue-router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  console.log("App Launch");
 | 
					  console.log("App Launch");
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@ import { createApp } from 'vue'
 | 
				
			|||||||
import App from './App.vue'
 | 
					import App from './App.vue'
 | 
				
			||||||
import 'ant-design-vue/dist/reset.css';
 | 
					import 'ant-design-vue/dist/reset.css';
 | 
				
			||||||
import Antd from 'ant-design-vue';
 | 
					import Antd from 'ant-design-vue';
 | 
				
			||||||
 | 
					import router from './router/router';
 | 
				
			||||||
const app = createApp(App);
 | 
					const app = createApp(App);
 | 
				
			||||||
app.use(Antd);
 | 
					app.use(Antd);
 | 
				
			||||||
 | 
					app.use(router);
 | 
				
			||||||
app.mount('#app')
 | 
					app.mount('#app')
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										284
									
								
								src/pages/voice/webrtcVoice.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								src/pages/voice/webrtcVoice.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,284 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="webrtc-voice-container">
 | 
				
			||||||
 | 
					    <div class="call-status">
 | 
				
			||||||
 | 
					      <h2>{{ callStatus }}</h2>
 | 
				
			||||||
 | 
					      <div v-if="myPeerId" class="peer-id">
 | 
				
			||||||
 | 
					        我的ID: {{ myPeerId }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <button @click="copyPeerId">复制</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="delay-time">
 | 
				
			||||||
 | 
					      <h2>延迟时间: {{ delayTime + 'ms' }}</h2>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="connection-controls" v-if="!isInCall">
 | 
				
			||||||
 | 
					      <input type="text" v-model="remotePeerId" placeholder="输入对方的ID" :disabled="isInCall" />
 | 
				
			||||||
 | 
					      <button type="reset" class="call-btn start-call" @click="startCall" :disabled="!remotePeerId || isInCall">
 | 
				
			||||||
 | 
					        呼叫
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="call-controls" v-if="isInCall">
 | 
				
			||||||
 | 
					      <button class="call-btn end-call" @click="endCall">
 | 
				
			||||||
 | 
					        结束通话
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <button class="mute-btn" :class="{ muted: isMuted }" @click="toggleMute">
 | 
				
			||||||
 | 
					        {{ isMuted ? '取消静音' : '静音' }}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, onMounted, onUnmounted } from 'vue'
 | 
				
			||||||
 | 
					import Peer, { type MediaConnection } from 'peerjs'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const myPeerId = ref('')
 | 
				
			||||||
 | 
					const remotePeerId = ref('')
 | 
				
			||||||
 | 
					const isInCall = ref(false)
 | 
				
			||||||
 | 
					const isMuted = ref(false)
 | 
				
			||||||
 | 
					const callStatus = ref('准备连接...')
 | 
				
			||||||
 | 
					const localStream = ref<MediaStream | null>(null)
 | 
				
			||||||
 | 
					const peer = ref<Peer | null>(null)
 | 
				
			||||||
 | 
					const currentCall = ref<MediaConnection | null>(null)
 | 
				
			||||||
 | 
					const delayTime = ref(0)
 | 
				
			||||||
 | 
					let statsInterval: number | null = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initPeer = () => {
 | 
				
			||||||
 | 
					  peer.value = new Peer(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      config: {
 | 
				
			||||||
 | 
					        iceServers: [
 | 
				
			||||||
 | 
					          { urls: 'stun:8.134.35.244:3478' }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  peer.value.on('open', (id) => {
 | 
				
			||||||
 | 
					    myPeerId.value = id
 | 
				
			||||||
 | 
					    callStatus.value = '准备就绪'
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  peer.value.on('call', async (call) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const stream = await navigator.mediaDevices.getUserMedia({
 | 
				
			||||||
 | 
					        audio: true,
 | 
				
			||||||
 | 
					        video: false
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      localStream.value = stream
 | 
				
			||||||
 | 
					      call.answer(stream)
 | 
				
			||||||
 | 
					      handleCall(call)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      isInCall.value = true
 | 
				
			||||||
 | 
					      callStatus.value = '通话中'
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('获取音频设备失败:', error)
 | 
				
			||||||
 | 
					      callStatus.value = '无法访问麦克风'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  peer.value.on('error', (error) => {
 | 
				
			||||||
 | 
					    console.error('PeerJS错误:', error)
 | 
				
			||||||
 | 
					    callStatus.value = '连接错误'
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const copyPeerId = () => {
 | 
				
			||||||
 | 
					  navigator.clipboard.writeText(myPeerId.value)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const updateDelayTime = async () => {
 | 
				
			||||||
 | 
					  delayTime.value = 0
 | 
				
			||||||
 | 
					  if (currentCall.value && currentCall.value.peerConnection) {
 | 
				
			||||||
 | 
					    const stats = await currentCall.value.peerConnection.getStats()
 | 
				
			||||||
 | 
					    stats.forEach(report => {
 | 
				
			||||||
 | 
					      if (report.type === 'candidate-pair' && report.state === 'succeeded') {
 | 
				
			||||||
 | 
					        delayTime.value += report.currentRoundTripTime * 1000 //currentRoundTripTime是指往返时间
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (report.type === 'inbound-rtp' && report.kind === 'audio') {
 | 
				
			||||||
 | 
					        delayTime.value += Math.round(
 | 
				
			||||||
 | 
					          report.jitter * 1000 || 0 +                       // 抖动缓冲区变化
 | 
				
			||||||
 | 
					          report.playoutDelay || 0                                    // 播放延迟
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const handleCall = (call: MediaConnection) => {
 | 
				
			||||||
 | 
					  currentCall.value = call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  statsInterval = window.setInterval(updateDelayTime, 1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  call.on('stream', (remoteStream) => {
 | 
				
			||||||
 | 
					    const audio = new Audio()
 | 
				
			||||||
 | 
					    audio.srcObject = remoteStream
 | 
				
			||||||
 | 
					    audio.play()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  call.on('close', () => {
 | 
				
			||||||
 | 
					    endCall()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const startCall = async () => {
 | 
				
			||||||
 | 
					  if (!peer.value || !remotePeerId.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stream = await navigator.mediaDevices.getUserMedia({
 | 
				
			||||||
 | 
					      audio: true,
 | 
				
			||||||
 | 
					      video: false
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    localStream.value = stream
 | 
				
			||||||
 | 
					    const call = peer.value.call(remotePeerId.value, stream)
 | 
				
			||||||
 | 
					    handleCall(call)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isInCall.value = true
 | 
				
			||||||
 | 
					    callStatus.value = '通话中'
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error('获取音频设备失败:', error)
 | 
				
			||||||
 | 
					    callStatus.value = '无法访问麦克风'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toggleMute = () => {
 | 
				
			||||||
 | 
					  if (localStream.value) {
 | 
				
			||||||
 | 
					    const audioTrack = localStream.value.getAudioTracks()[0]
 | 
				
			||||||
 | 
					    isMuted.value = !isMuted.value
 | 
				
			||||||
 | 
					    audioTrack.enabled = !isMuted.value
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const endCall = () => {
 | 
				
			||||||
 | 
					  if (statsInterval) {
 | 
				
			||||||
 | 
					    clearInterval(statsInterval)
 | 
				
			||||||
 | 
					    statsInterval = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (currentCall.value) {
 | 
				
			||||||
 | 
					    currentCall.value.close()
 | 
				
			||||||
 | 
					    currentCall.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (localStream.value) {
 | 
				
			||||||
 | 
					    localStream.value.getTracks().forEach(track => track.stop())
 | 
				
			||||||
 | 
					    localStream.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isInCall.value = false
 | 
				
			||||||
 | 
					  isMuted.value = false
 | 
				
			||||||
 | 
					  callStatus.value = '准备就绪'
 | 
				
			||||||
 | 
					  delayTime.value = 0 // 重置延迟时间
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  initPeer()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
					  endCall()
 | 
				
			||||||
 | 
					  if (peer.value) {
 | 
				
			||||||
 | 
					    peer.value.destroy()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.webrtc-voice-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					  background-color: #f5f5f5;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.call-status {
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.call-status h2 {
 | 
				
			||||||
 | 
					  font-size: 24px;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.peer-id {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.connection-controls {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.connection-controls input {
 | 
				
			||||||
 | 
					  padding: 12px;
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 25px;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  width: 200px;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.connection-controls input:focus {
 | 
				
			||||||
 | 
					  border-color: #2196F3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.call-controls {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 20px;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button {
 | 
				
			||||||
 | 
					  padding: 12px 24px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 25px;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button:disabled {
 | 
				
			||||||
 | 
					  opacity: 0.5;
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.call-btn {
 | 
				
			||||||
 | 
					  min-width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.start-call {
 | 
				
			||||||
 | 
					  background-color: #4CAF50;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.end-call {
 | 
				
			||||||
 | 
					  background-color: #f44336;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mute-btn {
 | 
				
			||||||
 | 
					  background-color: #2196F3;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mute-btn.muted {
 | 
				
			||||||
 | 
					  background-color: #9E9E9E;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  transform: scale(1.05);
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/router/router.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/router/router.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { createRouter, createWebHashHistory } from "vue-router";
 | 
				
			||||||
 | 
					import Index from "../pages/file/index.vue";
 | 
				
			||||||
 | 
					import Voice from "../pages/voice/webrtcVoice.vue";
 | 
				
			||||||
 | 
					const router = createRouter({
 | 
				
			||||||
 | 
					    history: createWebHashHistory(),
 | 
				
			||||||
 | 
					    routes: [
 | 
				
			||||||
 | 
					        { path: "/", component: Index },
 | 
				
			||||||
 | 
					        { path: "/voice", component: Voice },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default router;
 | 
				
			||||||
@ -581,7 +581,7 @@
 | 
				
			|||||||
    "@vue/compiler-dom" "3.5.13"
 | 
					    "@vue/compiler-dom" "3.5.13"
 | 
				
			||||||
    "@vue/shared" "3.5.13"
 | 
					    "@vue/shared" "3.5.13"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@vue/devtools-api@^6.5.0":
 | 
					"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4":
 | 
				
			||||||
  version "6.6.4"
 | 
					  version "6.6.4"
 | 
				
			||||||
  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
 | 
					  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
 | 
				
			||||||
  integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
 | 
					  integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
 | 
				
			||||||
@ -2102,6 +2102,13 @@ vue-i18n@^9.1.9:
 | 
				
			|||||||
    "@intlify/shared" "9.14.2"
 | 
					    "@intlify/shared" "9.14.2"
 | 
				
			||||||
    "@vue/devtools-api" "^6.5.0"
 | 
					    "@vue/devtools-api" "^6.5.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vue-router@^4.5.0:
 | 
				
			||||||
 | 
					  version "4.5.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
 | 
				
			||||||
 | 
					  integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@vue/devtools-api" "^6.6.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vue-template-compiler@^2.7.14:
 | 
					vue-template-compiler@^2.7.14:
 | 
				
			||||||
  version "2.7.16"
 | 
					  version "2.7.16"
 | 
				
			||||||
  resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz#c81b2d47753264c77ac03b9966a46637482bb03b"
 | 
					  resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz#c81b2d47753264c77ac03b9966a46637482bb03b"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user