视频播放调试完成

This commit is contained in:
BianLzhaoMin 2025-06-24 18:12:02 +08:00
parent a40836ea9b
commit 8cc362bc82
17 changed files with 1575 additions and 126 deletions

183
package-lock.json generated
View File

@ -9,11 +9,13 @@
"version": "0.0.1",
"dependencies": {
"@kjgl77/datav-vue3": "^1.7.4",
"@vueuse/core": "^13.4.0",
"axios": "1.7.7",
"codess": "^1.1.5",
"copy-to-clipboard": "3.3.3",
"cropperjs": "1.6.2",
"echarts": "5.5.1",
"flv.js": "^1.6.2",
"glob": "11.0.0",
"mime": "4.0.4",
"mitt": "3.0.1",
@ -31,6 +33,7 @@
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vicons/ionicons5": "^0.13.0",
"@vitejs/plugin-vue": "5.1.2",
"naive-ui": "^2.42.0",
"vite": "5.4.1"
@ -627,6 +630,74 @@
"@vueuse/core": "^10.11.1"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://repo.huaweicloud.com/repository/npm/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT"
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://repo.huaweicloud.com/repository/npm/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -914,9 +985,16 @@
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://repo.huaweicloud.com/repository/npm/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"version": "0.0.21",
"resolved": "https://repo.huaweicloud.com/repository/npm/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vicons/ionicons5": {
"version": "0.13.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
"integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
@ -1029,90 +1107,41 @@
"integrity": "sha512-L2MCDD8l7yC62Te5UUyPVpmexhL9ipVnYRw9CsWfm/BGRL5FwDX4a25bcJ/OJSD3+Hx+k/a8LDKcG2AFdJV3BA=="
},
"node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"version": "13.4.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/core/-/core-13.4.0.tgz",
"integrity": "sha512-OnK7zW3bTq/QclEk17+vDFN3tuAm8ONb9zQUIHrYQkkFesu3WeGUx/3YzpEp+ly53IfDAT9rsYXgGW6piNZC5w==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.4.0",
"@vueuse/shared": "13.4.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"version": "13.4.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/metadata/-/metadata-13.4.0.tgz",
"integrity": "sha512-CPDQ/IgOeWbqItg1c/pS+Ulum63MNbpJ4eecjFJqgD/JUCJ822zLfpw6M9HzSvL6wbzMieOtIAW/H8deQASKHg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"version": "13.4.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/shared/-/shared-13.4.0.tgz",
"integrity": "sha512-+AxuKbw8R1gYy5T21V5yhadeNM7rJqb4cPaRI9DdGnnNl3uqXh+unvQ3uCaA2DjYLbNr1+l7ht/B4qEsRegX6A==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://repo.huaweicloud.com/repository/npm/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"vue": "^3.5.0"
}
},
"node_modules/acorn": {
@ -1869,6 +1898,12 @@
"node": ">= 0.4"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://repo.huaweicloud.com/repository/npm/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
@ -2014,6 +2049,16 @@
"node": ">=8"
}
},
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -4464,6 +4509,12 @@
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="
},
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://repo.huaweicloud.com/repository/npm/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@ -12,11 +12,13 @@
},
"dependencies": {
"@kjgl77/datav-vue3": "^1.7.4",
"@vueuse/core": "^13.4.0",
"axios": "1.7.7",
"codess": "^1.1.5",
"copy-to-clipboard": "3.3.3",
"cropperjs": "1.6.2",
"echarts": "5.5.1",
"flv.js": "^1.6.2",
"glob": "11.0.0",
"mime": "4.0.4",
"mitt": "3.0.1",
@ -34,6 +36,7 @@
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vicons/ionicons5": "^0.13.0",
"@vitejs/plugin-vue": "5.1.2",
"naive-ui": "^2.42.0",
"vite": "5.4.1"

BIN
src/assets/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,73 @@
<template>
<!-- 模态框通用样式 -->
<n-card
size="huge"
role="dialog"
aria-modal="true"
:bordered="false"
class="modal-container"
:style="{ width: width, height: height }"
>
<n-flex justify="space-between" style="height: 40px">
<n-gradient-text
style="font-size: 20px; font-weight: 600; letter-spacing: 2px"
gradient="linear-gradient(90deg, #CDF7FD 0%, #DAFAFE 20%, #CDF7FD 40%, #DAFAFE 60%, #CDF7FD 80%, #DAFAFE 100%)"
>
{{ modalTitle }}
</n-gradient-text>
<img
class="close-icon"
src="@/assets/home-imgs/close.png"
@click="onHandleCloseModal"
/>
</n-flex>
<div class="modal-content">
<slot></slot>
</div>
</n-card>
</template>
<script setup>
const props = defineProps({
width: {
type: String,
default: '80%',
},
height: {
type: String,
default: '',
},
modalTitle: {
type: String,
default: '',
},
})
const emits = defineEmits(['onHandleCloseModal'])
//
const onHandleCloseModal = () => {
emits('onHandleCloseModal')
}
</script>
<style lang="scss" scoped>
.modal-container {
display: flex;
flex-direction: column;
background: url('@/assets/home-imgs/modal-bg.png') no-repeat center center;
background-size: 100% 100%;
}
.close-icon {
width: 20px;
height: 20px;
cursor: pointer;
}
.modal-content {
flex: 1;
height: calc(100% - 40px);
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<!-- 视频播放器 Flv格式视频 -->
<video
:id="videoId"
autoplay
controls
@timeupdate="progress($event)"
style="width: 100%; height: 100%; border-radius: 12px"
/>
</template>
<script setup>
import flv from 'flv.js'
import { ref, watch, nextTick } from 'vue'
let flvPlayerList = []
let replayCount = 0
let errorMsg = ref('')
const playUrl = ref('')
const dfp = defineProps({
cameraNode: {
type: Object,
default: () => {},
},
steamType: {
type: String,
default: 'HTTP-FLV',
},
videoId: {
type: String,
default: '',
},
})
const playvideo = () => {
errorMsg.value = ''
if (flv.isSupported()) {
stopvideo()
nextTick(() => {
let videoElement = document.getElementById(dfp.videoId)
console.log(videoElement, 'videoElement')
videoElement.contorls = true
let flvPlayer = flv.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: playUrl.value,
})
flvPlayer.attachMediaElement(videoElement)
flvPlayer.load()
setTimeout(function () {
flvPlayer.play()
}, 100)
flvPlayer.on('error', function (error) {
errorMsg.value = error
if (replayCount > 0) {
return
}
replayCount++
setTimeout(function () {
console.log('flvjs replayCount', replayCount)
playvideo()
}, 100)
})
flvPlayer.on('scriptdata_arrived', function (e) {
errorMsg.value = ''
flvPlayerList.push(flvPlayer)
})
})
} else {
ElMessage({
message: '当前浏览器不支持flvjs播放',
type: 'error',
duration: 2000,
})
}
}
const stopvideo = async () => {
if (flvPlayerList.length) {
flvPlayerList.forEach((item) => {
if (item) {
item.unload()
item.detachMediaElement()
item.destroy()
}
})
flvPlayerList = []
}
}
const progress = (e) => {
let self = this
var bf = e.srcElement.buffered
var currentTime = e.srcElement.currentTime
if (bf.length > 0) {
let end = bf.end(0)
if (end - currentTime > 10) {
e.srcElement.currentTime = end - 0.1
}
}
}
watch(
() => dfp.cameraNode,
() => {
errorMsg.value = ''
console.log(dfp.cameraNode, 'dfp.cameraNode')
if (dfp.cameraNode.steamURL) {
playUrl.value =
dfp.cameraNode.steamURL +
'&puid=' +
dfp.cameraNode.puid +
'&idx=' +
dfp.cameraNode.idx +
'&stream=' +
dfp.cameraNode.stream
// console.log(dfp.cameraNode.steamURL, 'playUrl.value')
console.log(playUrl.value, 'playUrl.value')
playvideo()
} else {
stopvideo()
}
},
{ deep: true },
)
</script>
<style scoped>
#video {
width: 100%;
height: 100%;
object-fit: fill;
background-color: black;
}
</style>

View File

@ -11,7 +11,7 @@ const timeout = 13000 //api请求超时时间
export const service = axios.create({
//可创建多个 axios实例
baseURL: baseApiURL, //设置公共的请求前缀
baseURL: '', //设置公共的请求前缀
timeout: timeout, //超时终止请求
})
@ -38,10 +38,11 @@ service.interceptors.response.use(
msg: '请求发生错误',
})
}
const status = data.status
const status = data?.status || response.status
switch (status) {
case 200:
return data
// return data
return Promise.resolve(response)
case 401: //表示需要重新登录
if (!modelShow) {
modelShow = true

View File

@ -1,15 +1,15 @@
/** 用户全局数据 */
import { defineStore } from 'pinia';
import allStorage from '@/action/storageManage';
import { defineStore } from 'pinia'
import allStorage from '@/action/storageManage'
export const userDataStore = defineStore('userDataStore', {
state: () => {
const userStorage = allStorage.userStorage();
const userStorage = allStorage.userStorage()
/** 校验数据 */
let userInfo = userStorage.value;
let userInfo = userStorage.value
if (typeof userInfo !== 'object') {
userInfo = {};
userInfo = {}
}
return {
userInfo: userInfo || {}, //当前登录用户的基础数据
@ -18,30 +18,30 @@ export const userDataStore = defineStore('userDataStore', {
userMenuList: [], //用于展示的菜单列表,结构树形化
tagsMap: {}, // 页面标签MAPlayoutName为键名
iframeList: [], //iframe 数组iframe也属于标签跟标签挂钩
};
}
},
getters: {},
actions: {
setUserInfo(value) {
this.userInfo = value || {};
this.userInfo = value || {}
/** 存入缓存 */
const userStorage = allStorage.userStorage();
userStorage.value = value;
const userStorage = allStorage.userStorage()
userStorage.value = value
},
setUserMenuConfigNameMap(value) {
this.userMenuConfigNameMap = value || {};
this.userMenuConfigNameMap = value || {}
},
setUserMenuConfigPathMap(value) {
this.userMenuConfigPathMap = value || {};
this.userMenuConfigPathMap = value || {}
},
setUserMenuList(value) {
this.userMenuList = value || [];
this.userMenuList = value || []
},
setTagsMap(value) {
this.tagsMap = value || {};
this.tagsMap = value || {}
},
setIframeList(value) {
this.iframeList = value || [];
this.iframeList = value || []
},
},
});
})

19
src/utils/initLogin.js Normal file
View File

@ -0,0 +1,19 @@
import { service } from '@/http/request'
// 初始化登录
export const initLoginApi = (data) => {
return service.post('/third-party/login', data)
}
// 获取设备的URL地址
export const getDeviceUrlApi = (data) => {
return service.post('/third-party/stream/live', data)
}
// 调整设备的摄像机视角
export const changeDeviceCameraApi = (data) => {
return service.post('/third-party/ptz/start/turn', data)
}
// 停止设备的摄像机视角
export const stopDeviceCameraApi = (data) => {
return service.post('/third-party/ptz/stop/turn', data)
}

View File

@ -1,16 +1,33 @@
<template>
<!-- 中一 ---- 自巡检视角 -->
<div class="center-one child-container">
<div class="view-title">自巡检视角</div>
<div class="view-title" v-if="!fullScreenVisible">自巡检视角</div>
<div class="video-container">
<video controls style="width: 100%; height: 100%; border-radius: 12px" autoplay>
<!-- <video controls style="width: 100%; height: 100%; border-radius: 12px" autoplay>
<source src="@/assets/video/测试.mp4" type="video/mp4" autoplay />
</video>
</video> -->
<FlvPlayer :cameraNode="cameraNode_2" :videoId="props.videoId" />
</div>
</div>
</template>
<script setup></script>
<script setup>
import FlvPlayer from '@/components/FlvPlayer/index.vue'
const props = defineProps({
cameraNode_2: {
type: Object,
default: () => {},
},
videoId: {
type: String,
default: 'video-5',
},
fullScreenVisible: {
type: Boolean,
default: false,
},
})
</script>
<style lang="scss" scoped>
.center-one {

View File

@ -45,25 +45,29 @@
class="arrow-top hand-direction"
src="@/assets/home-imgs/control-2-arrow.png"
alt=""
@click="handleChangeCamera('TurnUp')"
/>
<img
class="arrow-right hand-direction"
src="@/assets/home-imgs/control-2-arrow.png"
alt=""
@click="handleChangeCamera('TurnRight')"
/>
<img
class="arrow-bottom hand-direction"
src="@/assets/home-imgs/control-2-arrow.png"
alt=""
@click="handleChangeCamera('TurnDown')"
/>
<img
class="arrow-left hand-direction"
src="@/assets/home-imgs/control-2-arrow.png"
alt=""
@click="handleChangeCamera('TurnLeft')"
/>
<!-- 中间的按钮 -->
<div class="row-3-item-1-center">
<div class="row-3-item-1-center" @click="handleStopCamera">
<img
class="center-icon"
src="@/assets/home-imgs/control-2-stop.png"
@ -84,8 +88,13 @@
style="margin-bottom: 2px"
src="@/assets/home-imgs/control-2-add.png"
alt=""
@click="handleChangeCamera('ZoomIn')"
/>
<img
src="@/assets/home-imgs/control-2-reduce.png"
alt=""
@click="handleChangeCamera('ZoomOut')"
/>
<img src="@/assets/home-imgs/control-2-reduce.png" alt="" />
</div>
</div>
</n-grid-item>
@ -245,6 +254,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { changeDeviceCameraApi, stopDeviceCameraApi } from '@/utils/initLogin'
const upDownHeight = ref(30)
const container = ref(null)
const draggable = ref(null)
@ -333,6 +344,28 @@ const handleChange4 = (e) => {
const formatTooltip = (value) => {
return `${value}%`
}
const handleChangeCamera = async (direction) => {
// console.log(direction)
const res = await changeDeviceCameraApi({
token: localStorage.getItem('token'),
puid: '201115200268437643',
idx: 1,
motion: direction,
})
console.log(res, '调整位置---')
}
const handleStopCamera = async () => {
const res = await stopDeviceCameraApi({
token: localStorage.getItem('token'),
puid: '201115200268437643',
idx: 1,
})
console.log(res, '停止位置---')
}
</script>
<style lang="scss" scoped>
.control-deck {

View File

@ -2,6 +2,12 @@
<!-- 左侧盒子1 -->
<div class="left-one child-container">
<n-flex justify="space-between" v-if="isShowAllBtns">
<div class="btns" @click="onHandlePresetSetting" v-if="!fullScreenVisible">
预置位配置
</div>
<div class="btns" @click="onHandleInspectionTask" v-if="!fullScreenVisible">
巡视任务
</div>
<div class="btns" @click="onHandleOperationPanel" v-if="!fullScreenVisible">
操作面板
</div>
@ -21,15 +27,43 @@
</n-flex>
<div class="video-container" :style="{ marginTop: isShowAllBtns ? '24px' : '0' }">
<video controls style="width: 100%; height: 100%; border-radius: 12px" autoplay>
<source src="@/assets/video/测试.mp4" type="video/mp4" autoplay />
</video>
<FlvPlayer :cameraNode="cameraNode" :videoId="props.videoId" />
</div>
</div>
<!-- 预置位配置 -->
<n-modal v-model:show="presetSettingVisible">
<PresetSetting
v-if="showModal === 1"
@onHandleCloseModal="onHandleCloseModal"
:presetSettingVisible="presetSettingVisible"
/>
<InspectionTask
v-if="showModal === 2"
@onHandleCloseModal="onHandleCloseModal"
@onHandleAddInspectionTask="onHandleAddInspectionTask"
/>
</n-modal>
<!-- 新增巡视任务 -->
<n-modal v-model:show="addInspectionTaskVisible">
<AddOrEditForm
v-if="addInspectionTaskVisible"
@onHandleCloseModalInner="onHandleCloseModalInner"
/>
</n-modal>
</template>
<script setup>
import { ref } from 'vue'
import PresetSetting from './modal-content/preset-setting.vue'
import InspectionTask from './modal-content/inspection-task.vue'
import AddOrEditForm from './modal-content/add-or-edit-form.vue'
import FlvPlayer from '@/components/FlvPlayer/index.vue'
const presetSettingVisible = ref(false)
const addInspectionTaskVisible = ref(false)
const showModal = ref(1)
const props = defineProps({
fullScreenVisible: {
@ -40,8 +74,49 @@ const props = defineProps({
type: Boolean,
default: true,
},
cameraNode: {
type: Object,
default: () => {},
},
videoId: {
type: String,
default: 'video-1',
},
})
const emits = defineEmits(['onHandleOperationPanel', 'onHandleFullScreen'])
const emits = defineEmits([
'onHandleOperationPanel',
'onHandleFullScreen',
'onHandleFullScreenToggle',
'onHandleChangeView',
])
//
const onHandlePresetSetting = () => {
showModal.value = 1
presetSettingVisible.value = true
}
//
const onHandleCloseModal = () => {
showModal.value = null
presetSettingVisible.value = false
}
//
const onHandleInspectionTask = () => {
showModal.value = 2
presetSettingVisible.value = true
}
//
const onHandleAddInspectionTask = () => {
addInspectionTaskVisible.value = true
}
//
const onHandleCloseModalInner = () => {
addInspectionTaskVisible.value = false
}
//
const onHandleOperationPanel = () => {
@ -59,7 +134,9 @@ const onHandleBackFullScreen = () => {
}
//
const onHandleChangeView = () => {}
const onHandleChangeView = () => {
emits('onHandleChangeView')
}
</script>
<style lang="scss" scoped>
@ -69,12 +146,14 @@ const onHandleChangeView = () => {}
height: 100%; /* 确保容器有高度 */
.btns {
width: 160px;
height: 50px;
// width: 90px;
padding: 0 10px;
height: 36px;
background: url('@/assets/home-imgs/button-bg.png') no-repeat center center;
background-size: 100% 100%;
line-height: 50px;
line-height: 36px;
text-align: center;
font-size: 12px;
cursor: pointer;
}

View File

@ -0,0 +1,194 @@
<template>
<!-- 新增或编辑弹框 -->
<DialogModal
:width="`60%`"
:height="`100vh`"
:modalTitle="addOrEditTitle"
@onHandleCloseModal="onHandleCloseModalInner"
>
<n-form ref="addOrEditFormRef" size="small" label-placement="left" style="margin-top: 10px">
<n-grid x-gap="24" :cols="24">
<n-gi :span="6">
<n-form-item label="设备名称">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="底盘名称">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="任务地图">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="巡检次数">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="24">
<n-gi :span="6">
<n-form-item label="智能事件">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="任务名称">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="任务周期">
<n-input
v-model:value="deviceName"
placeholder="任务名称"
clearable
style="width: 240px"
/>
</n-form-item>
</n-gi>
<n-gi :span="6">
<n-form-item label="开始时间">
<n-button type="info" @click="onHandleOpenTimePicker">
<template #icon>
<NIcon>
<AddCircleSharp />
</NIcon>
</template>
</n-button>
</n-form-item>
</n-gi>
</n-grid>
<n-grid
x-gap="24"
:cols="24"
v-for="(item, index) in timeSelectList"
:key="item.week"
style="margin-bottom: 10px"
>
<n-gi :span="6">
<n-time-picker
clearable
@confirm="handleConfirm"
default-formatted-value="00:00:00"
/>
</n-gi>
<n-gi :span="6">
<n-select v-model:value="item.week" :options="options" />
</n-gi>
<n-gi :span="6" v-if="index > 0">
<n-button quaternary @click="onHandleDeleteTimeSelect(index)">
<template #icon>
<NIcon color="#F90202">
<TrashSharp />
</NIcon>
</template>
</n-button>
</n-gi>
</n-grid>
</n-form>
</DialogModal>
</template>
<script setup>
import DialogModal from '@/components/DialogModal/index.vue'
import { ref } from 'vue'
import { NIcon, useMessage } from 'naive-ui'
import { AddCircleSharp, TrashSharp } from '@vicons/ionicons5'
const message = useMessage()
const addOrEditTitle = ref('新增巡视任务') //
const value = ref('1')
const deviceName = ref('')
const timeSelectList = ref([{ times: '00:00:00', week: '' }])
const options = ref([
{
label: '星期一',
value: '1',
},
{
label: '星期二',
value: '2',
},
{
label: '星期三',
value: '3',
},
{
label: '星期四',
value: '4',
},
{
label: '星期五',
value: '5',
},
{
label: '星期六',
value: '6',
},
{
label: '星期日 ',
value: '7',
},
])
const emits = defineEmits(['onHandleCloseModalInner'])
//
const onHandleCloseModalInner = () => {
emits('onHandleCloseModalInner')
}
//
const onHandleOpenTimePicker = () => {
console.log('新增时间选择器')
if (timeSelectList.value.length < 7) {
timeSelectList.value.push({ times: '00:00:00', week: '' })
} else {
//
message.warning('最多添加7个开始时间', { duration: 1000 })
}
}
//
const onHandleDeleteTimeSelect = (index) => {
timeSelectList.value.splice(index, 1)
}
const handleConfirm = (value) => {
console.log(value)
}
</script>
<style scoped></style>

View File

@ -0,0 +1,242 @@
<template>
<!-- 巡视任务 -->
<DialogModal
@onHandleCloseModal="onHandleCloseModal"
:modalTitle="modalTitle"
:height="`90vh`"
:width="`90%`"
>
<!-- 巡视任务-->
<div class="inspection-task-container" ref="inspectionTaskContainer">
<n-form
inline
ref="formRef"
size="small"
label-placement="left"
style="margin-top: 10px"
>
<n-form-item>
<n-input placeholder="任务名称" clearable style="width: 240px" />
</n-form-item>
<n-button type="info"> 查询</n-button>
<n-button color="#6E90A9" style="margin-left: 8px" @click="onHandleAdd">
新增
</n-button>
</n-form>
<n-data-table :columns="columns" :data="data" />
<div style="margin-top: 10px; display: flex; justify-content: flex-end">
<n-pagination
:page-count="100"
show-size-picker
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
/>
</div>
</div>
</DialogModal>
</template>
<script setup>
import DialogModal from '@/components/DialogModal/index.vue'
import { AddCircleSharp } from '@vicons/ionicons5'
import { NIcon } from 'naive-ui'
import { NButton, NSwitch } from 'naive-ui'
import { ref } from 'vue'
const emits = defineEmits(['onHandleCloseModal', 'onHandleAddInspectionTask'])
const modalTitle = ref('巡视任务') //
const addOrEditVisible = ref(false) //
const addOrEditTitle = ref('新增巡视任务') //
const page = ref(1)
const pageSize = ref(10)
const btnList = [
{
label: '编辑',
type: 'info',
btnType: 1,
},
{
label: '立即执行',
type: 'info',
btnType: 2,
},
{
label: '下发',
type: 'info',
btnType: 3,
},
{
label: '删除',
type: 'info',
btnType: 4,
},
]
const columns = ref([
{
title: '任务ID',
key: 'age',
align: 'center',
},
{
title: '任务名',
key: 'tags',
align: 'center',
},
{
title: '设备名称',
key: 'age',
align: 'center',
},
{
title: '地盘名称',
key: 'age',
align: 'center',
},
{
title: '任务频次',
key: 'age',
align: 'center',
},
{
title: '执行时间',
key: 'age',
align: 'center',
},
{
title: '任务状态',
key: 'age',
align: 'center',
},
{
title: '启用',
key: 'age',
align: 'center',
render(row) {
return h(NSwitch, {
value: row.isEnable,
onChange: (value) => onHandleSwitch(row, value),
})
},
},
{
title: '操作',
key: 'age',
align: 'center',
width: 260,
render(row) {
const buttonS = btnList.map((btn) => {
return h(
NButton,
{
size: 'small',
onClick: () => onHandleBtn(row, btn.btnType),
type: btn.type,
style: {
marginRight: '4px',
},
},
{ default: () => btn.label },
)
})
return buttonS
},
},
])
const data = ref([
{
age: '2025-06-01 10:00:00',
tags: ['nice'],
isEnable: true,
},
{
age: '2025-06-01 10:00:00',
tags: ['nice'],
isEnable: false,
},
{
age: '2025-06-01 10:00:00',
tags: ['nice'],
isEnable: false,
},
])
//
const onHandleAdd = () => {
console.log('新增')
// addOrEditVisible.value = true
// addOrEditTitle.value = ''
emits('onHandleAddInspectionTask')
}
//
const onHandleBtn = (row, type) => {
console.log(row, type)
// 1. 2. 3. 4.
switch (type) {
case 1:
onHandleEditTable(row)
break
case 2:
onHandleImmediateExecution(row)
break
case 3:
onHandleSend(row)
break
case 4:
onHandleDelete(row)
break
}
}
//
const onHandleEditTable = (row) => {
console.log(row)
addOrEditVisible.value = true
addOrEditTitle.value = '编辑巡视任务'
}
//
const onHandleImmediateExecution = (row) => {
console.log(row)
}
//
const onHandleSend = (row) => {
console.log(row)
}
//
const onHandleDelete = (row) => {
console.log(row)
}
//
const onHandleCloseModal = () => {
emits('onHandleCloseModal')
}
//
const onHandleCloseModalInner = () => {
addOrEditVisible.value = false
}
//
const onHandleSwitch = (row, value) => {
console.log(row, value)
row.isEnable = value
}
</script>
<style lang="scss" scoped>
.plane-map-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<!-- 预置位配置 -->
<DialogModal @onHandleCloseModal="onHandleCloseModal" :modalTitle="modalTitle" :height="`90vh`">
<!-- 平面图操作区域 -->
<div class="plane-map-container" ref="planeMapContainer">
<svg
ref="svgMap"
class="floor-plan"
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
preserveAspectRatio="xMidYMid meet"
@click="handleMapClick"
>
<!-- 平面图背景 -->
<image
v-if="imgLoaded"
:href="demoImg"
:width="viewBox.width"
:height="viewBox.height"
preserveAspectRatio="xMidYMid slice"
style="object-fit: cover"
/>
<!-- 动态渲染标记点 -->
<circle
v-for="(point, index) in points"
:key="index"
:cx="point.x"
:cy="point.y"
r="8"
fill="red"
@click.stop="selectPoint(index)"
@mousedown="startDrag(index, $event)"
/>
<!-- 动态渲染连线 -->
<line
v-for="(line, index) in lines"
:key="'line-' + index"
:x1="line.start.x"
:y1="line.start.y"
:x2="line.end.x"
:y2="line.end.y"
stroke="blue"
stroke-width="2"
/>
</svg>
<div class="map-control-container">
<n-button type="info" @click="handleZoomIn">放大</n-button>
<n-button type="info" @click="handleZoomOut">缩小</n-button>
</div>
</div>
</DialogModal>
</template>
<script setup>
import DialogModal from '@/components/DialogModal/index.vue'
import { refThrottled, useMouseInElement } from '@vueuse/core'
import { ref, onMounted, nextTick, watch } from 'vue'
// import { CashOutline as CashIcon } from '@vicons/ionicons5'
import { NIcon } from 'naive-ui'
import demoImg from '@/assets/demo.png'
const emits = defineEmits(['onHandleCloseModal'])
const modalTitle = ref('预置位配置')
const svgMap = refThrottled(null)
const points = ref([])
const lines = ref([])
const imgLoaded = ref(false)
const imgNaturalSize = ref({ width: 0, height: 0 })
const props = defineProps({
presetSettingVisible: {
type: Boolean,
default: false,
},
})
// viewBox
const viewBox = ref({
x: 0,
y: 0,
width: 0,
height: 0,
})
//
const containerSize = ref({ width: 0, height: 0 })
const planeMapContainer = ref(null)
//
const onHandleCloseModal = () => {
emits('onHandleCloseModal')
}
//
const startDrag = (index, e) => {
e.preventDefault()
const { x: startX, y: startY } = points.value[index]
const onMove = (moveEvent) => {
const rect = svgMap.value.getBoundingClientRect()
points.value[index] = {
x: moveEvent.clientX - rect.left,
y: moveEvent.clientY - rect.top,
}
}
const onUp = () => {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
}
// 线
const connectPoints = () => {
if (points.value.length >= 2) {
lines.value.push({
start: points.value[0],
end: points.value[1],
})
}
}
onMounted(() => {
//
nextTick(() => {
const resizeObserver = new ResizeObserver((entries) => {
if (entries[0]) {
containerSize.value = {
width: entries[0].contentRect.width,
height: entries[0].contentRect.height,
}
}
})
if (planeMapContainer.value) {
resizeObserver.observe(planeMapContainer.value)
}
})
})
//
const handleMapClick = (e) => {
if (!svgMap.value) return
const pt = svgMap.value.createSVGPoint()
pt.x = e.clientX
pt.y = e.clientY
const svgPt = pt.matrixTransform(svgMap.value.getScreenCTM().inverse())
points.value.push({
x: svgPt.x,
y: svgPt.y,
})
}
watch(
() => props.presetSettingVisible,
(newVal) => {
if (newVal) {
nextTick(() => {
loadImage()
})
}
},
{
immediate: true,
},
)
const initMapSize = () => {
if (!planeMapContainer.value) return
containerSize.value = {
width: planeMapContainer.value.clientWidth,
height: planeMapContainer.value.clientHeight,
}
// viewBox
viewBox.value.width = containerSize.value.width
viewBox.value.height = containerSize.value.height
}
//
const loadImage = () => {
const img = new Image()
img.onload = () => {
imgNaturalSize.value = {
width: img.naturalWidth,
height: img.naturalHeight,
}
imgLoaded.value = true
adjustViewBox()
}
img.onerror = () => {
console.error('图片加载失败,请检查路径:', demoImg)
}
img.src = demoImg // 使
}
// viewBox
const adjustViewBox = () => {
if (!planeMapContainer.value || !imgNaturalSize.value.width) return
const container = planeMapContainer.value
const containerRatio = container.clientWidth / container.clientHeight
const imgRatio = imgNaturalSize.value.width / imgNaturalSize.value.height
if (containerRatio > imgRatio) {
//
viewBox.value = {
x: (imgNaturalSize.value.width - imgNaturalSize.value.height * containerRatio) / 2,
y: 0,
width: imgNaturalSize.value.height * containerRatio,
height: imgNaturalSize.value.height,
}
} else {
//
viewBox.value = {
x: 0,
y: (imgNaturalSize.value.height - imgNaturalSize.value.width / containerRatio) / 2,
width: imgNaturalSize.value.width,
height: imgNaturalSize.value.width / containerRatio,
}
}
}
const handleZoomIn = () => {
console.log('放大')
viewBox.value.width = viewBox.value.width * 1.1
viewBox.value.height = viewBox.value.height * 1.1
}
const handleZoomOut = () => {
console.log('缩小')
viewBox.value.width = viewBox.value.width * 0.9
viewBox.value.height = viewBox.value.height * 0.9
}
</script>
<style lang="scss" scoped>
.plane-map-container {
width: 100%;
height: 100%;
position: relative;
.map-control-container {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<!-- 预置位配置 -->
<DialogModal @onHandleCloseModal="onHandleCloseModal" :modalTitle="modalTitle" :height="`90vh`">
<!-- 平面图操作区域 -->
<div class="plane-map-container" ref="planeMapContainer">
<svg
ref="svgMap"
class="floor-plan"
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
preserveAspectRatio="xMidYMid meet"
@click="handleMapClick"
>
<!-- 平面图背景 -->
<image
v-if="imgLoaded"
:href="demoImg"
:width="viewBox.width"
:height="viewBox.height"
preserveAspectRatio="xMidYMid slice"
style="object-fit: cover"
/>
<!-- 动态渲染标记点 -->
<circle
v-for="(point, index) in points"
:key="index"
:cx="point.x"
:cy="point.y"
r="8"
fill="red"
@click.stop="selectPoint(index)"
@mousedown="startDrag(index, $event)"
/>
<!-- 动态渲染连线 -->
<line
v-for="(line, index) in lines"
:key="'line-' + index"
:x1="line.start.x"
:y1="line.start.y"
:x2="line.end.x"
:y2="line.end.y"
stroke="blue"
stroke-width="2"
/>
</svg>
<div class="map-control-container">
<n-button type="info" @click="handleZoomIn">放大</n-button>
<n-button type="info" @click="handleZoomOut">缩小</n-button>
</div>
</div>
</DialogModal>
</template>
<script setup>
import DialogModal from '@/components/DialogModal/index.vue'
import { refThrottled, useMouseInElement } from '@vueuse/core'
import { ref, onMounted, nextTick, watch } from 'vue'
// import { CashOutline as CashIcon } from '@vicons/ionicons5'
import { NIcon } from 'naive-ui'
import demoImg from '@/assets/demo.png'
const emits = defineEmits(['onHandleCloseModal'])
const modalTitle = ref('预置位配置')
const svgMap = refThrottled(null)
const points = ref([])
const lines = ref([])
const imgLoaded = ref(false)
const imgNaturalSize = ref({ width: 0, height: 0 })
const props = defineProps({
presetSettingVisible: {
type: Boolean,
default: false,
},
})
// viewBox
const viewBox = ref({
x: 0,
y: 0,
width: 0,
height: 0,
})
//
const containerSize = ref({ width: 0, height: 0 })
const planeMapContainer = ref(null)
//
const onHandleCloseModal = () => {
emits('onHandleCloseModal')
}
//
const startDrag = (index, e) => {
e.preventDefault()
const { x: startX, y: startY } = points.value[index]
const onMove = (moveEvent) => {
const rect = svgMap.value.getBoundingClientRect()
points.value[index] = {
x: moveEvent.clientX - rect.left,
y: moveEvent.clientY - rect.top,
}
}
const onUp = () => {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
}
// 线
const connectPoints = () => {
if (points.value.length >= 2) {
lines.value.push({
start: points.value[0],
end: points.value[1],
})
}
}
onMounted(() => {
//
nextTick(() => {
const resizeObserver = new ResizeObserver((entries) => {
if (entries[0]) {
containerSize.value = {
width: entries[0].contentRect.width,
height: entries[0].contentRect.height,
}
}
})
if (planeMapContainer.value) {
resizeObserver.observe(planeMapContainer.value)
}
})
})
//
const handleMapClick = (e) => {
if (!svgMap.value) return
const pt = svgMap.value.createSVGPoint()
pt.x = e.clientX
pt.y = e.clientY
const svgPt = pt.matrixTransform(svgMap.value.getScreenCTM().inverse())
points.value.push({
x: svgPt.x,
y: svgPt.y,
})
}
watch(
() => props.presetSettingVisible,
(newVal) => {
if (newVal) {
nextTick(() => {
loadImage()
})
}
},
{
immediate: true,
},
)
const initMapSize = () => {
if (!planeMapContainer.value) return
containerSize.value = {
width: planeMapContainer.value.clientWidth,
height: planeMapContainer.value.clientHeight,
}
// viewBox
viewBox.value.width = containerSize.value.width
viewBox.value.height = containerSize.value.height
}
//
const loadImage = () => {
const img = new Image()
img.onload = () => {
imgNaturalSize.value = {
width: img.naturalWidth,
height: img.naturalHeight,
}
imgLoaded.value = true
adjustViewBox()
}
img.onerror = () => {
console.error('图片加载失败,请检查路径:', demoImg)
}
img.src = demoImg // 使
}
// viewBox
const adjustViewBox = () => {
if (!planeMapContainer.value || !imgNaturalSize.value.width) return
const container = planeMapContainer.value
const containerRatio = container.clientWidth / container.clientHeight
const imgRatio = imgNaturalSize.value.width / imgNaturalSize.value.height
if (containerRatio > imgRatio) {
//
viewBox.value = {
x: (imgNaturalSize.value.width - imgNaturalSize.value.height * containerRatio) / 2,
y: 0,
width: imgNaturalSize.value.height * containerRatio,
height: imgNaturalSize.value.height,
}
} else {
//
viewBox.value = {
x: 0,
y: (imgNaturalSize.value.height - imgNaturalSize.value.width / containerRatio) / 2,
width: imgNaturalSize.value.width,
height: imgNaturalSize.value.width / containerRatio,
}
}
}
const handleZoomIn = () => {
console.log('放大')
viewBox.value.width = viewBox.value.width * 1.1
viewBox.value.height = viewBox.value.height * 1.1
}
const handleZoomOut = () => {
console.log('缩小')
viewBox.value.width = viewBox.value.width * 0.9
viewBox.value.height = viewBox.value.height * 0.9
}
</script>
<style lang="scss" scoped>
.plane-map-container {
width: 100%;
height: 100%;
position: relative;
.map-control-container {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@ -39,7 +39,7 @@
>
<template #error>
<n-icon :size="100" color="lightGrey">
<ImageOutlineIcon />
<!-- <ImageOutlineIcon /> -->
</n-icon>
</template>
</n-image>

View File

@ -4,54 +4,56 @@
<!-- 机器人首页 -->
<!-- 使用grid布局 -->
<!-- 非全屏模式 -->
<div class="robot-container" v-if="!fullScreenVisible">
<div class="robot-container">
<!-- 左一 -->
<div class="robot-1">
<div
:style="{
gridColumn: fullScreenVisible ? '1 / 9' : '1 / 4',
gridRow: fullScreenVisible ? '1 / 13' : '1 / 7',
}"
>
<LeftOne
:videoId="'video-1'"
:cameraNode="cameraNode_1"
:fullScreenVisible="fullScreenVisible"
@onHandleChangeView="onHandleChangeView"
@onHandleOperationPanel="onHandleOperationPanel"
@onHandleFullScreenToggle="onHandleFullScreenToggle"
@onHandleFullScreenToggle="onHandleFullScreenToggleL"
/>
</div>
<!-- 左二 -->
<div class="robot-2">
<div class="robot-2" v-if="!fullScreenVisible">
<LeftTwo />
</div>
<!-- 中一 -->
<div class="robot-3">
<CenterOne />
<div
:style="{
gridColumn: fullScreenVisible ? '9 / 13' : '4 / 10',
gridRow: fullScreenVisible ? '1 / 6' : '1 / 8',
}"
>
<CenterOne
:videoId="'video-2'"
:cameraNode_2="cameraNode_2"
:fullScreenVisible="fullScreenVisible"
/>
</div>
<!-- 中二 -->
<div class="robot-4">
<div class="robot-4" v-if="!fullScreenVisible">
<CenterTwo />
</div>
<!-- 右一 -->
<div class="robot-5">
<div class="robot-5" v-if="!fullScreenVisible">
<RightOne />
</div>
<!-- 右二 -->
<div class="robot-6">
<div class="robot-6" v-if="!fullScreenVisible">
<RightTwo />
</div>
</div>
<!-- 全屏模式 -->
<div class="full-screen-container" v-if="fullScreenVisible">
<div class="full-screen-left1">
<LeftOne
:fullScreenVisible="fullScreenVisible"
@onHandleFullScreenToggle="onHandleFullScreenToggle"
/>
</div>
<div class="full-screen-right1">
<LeftOne
:isShowAllBtns="false"
:fullScreenVisible="fullScreenVisible"
@onHandleFullScreenToggle="onHandleFullScreenToggle"
/>
</div>
<div class="full-screen-right2">
<div class="robot-7" v-if="fullScreenVisible">
<ControlDeck />
</div>
</div>
@ -69,6 +71,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { initLoginApi, getDeviceUrlApi } from '@/utils/initLogin'
import { useScale } from '@/hooks/useScale' // Hook
import LeftOne from './components/left-one.vue'
import LeftTwo from './components/left-two.vue'
@ -84,6 +87,25 @@ const fullScreenVisible = ref(false) // 全屏状态
// 使 useScale Hook setup()
const { baseWidth, baseHeight, scale } = useScale(appRef)
const cameraNode_1 = ref({
puid: '201115200268437643',
idx: 0,
stream: 0,
name: '',
operateType: 1,
steamCode: '',
steamURL: '',
})
const cameraNode_2 = ref({
puid: '201115200268437643',
idx: 1,
stream: 0,
name: '',
operateType: 1,
steamCode: '',
steamURL: '',
})
const operationPanelVisible = ref(false) //
//
@ -91,9 +113,63 @@ const onHandleOperationPanel = (visible) => {
operationPanelVisible.value = visible
}
//
const onHandleFullScreenToggle = (visible) => {
const onHandleFullScreenToggleL = (visible) => {
fullScreenVisible.value = visible
}
//
const onHandleChangeView = () => {
const tempIdx = cameraNode_1.value.idx
cameraNode_1.value.idx = cameraNode_2.value.idx
cameraNode_2.value.idx = tempIdx
const temp = cameraNode_1.value.steamURL
cameraNode_1.value.steamURL = cameraNode_2.value.steamURL
cameraNode_2.value.steamURL = temp
}
const loginParams = ref({
// - IP
address: '60.168.132.97',
port: '49988',
// -
user: 'admin',
// -
password: 'Aa123456',
// - ID
epid: 'YDJFXK',
// -
bfix: 0,
})
//
const initLoginData = async () => {
initLoginApi(loginParams.value).then((res) => {
localStorage.setItem('token', res?.data?.token)
getDeviceData()
})
}
//
const getDeviceData = async () => {
const { data: res } = await getDeviceUrlApi({
token: localStorage.getItem('token'),
puid: '201115200268437643',
idx: 0,
stream: 0,
type: 'HTTP-FLV',
})
const { data: res2 } = await getDeviceUrlApi({
token: localStorage.getItem('token'),
puid: '201115200268437643',
idx: 1,
stream: 0,
type: 'HTTP-FLV',
})
cameraNode_1.value.steamURL = res?.streamUrl
cameraNode_2.value.steamURL = res2?.streamUrl
}
initLoginData()
</script>
<style lang="scss">
@ -138,6 +214,11 @@ const onHandleFullScreenToggle = (visible) => {
grid-row: 8 / 13;
}
.robot-7 {
grid-column: 9 / 13;
grid-row: 6 / 13;
}
.full-screen-left1 {
grid-column: 1 / 9;
grid-row: 1 / 13;