Browse Source

监视墙

caoyang 6 months ago
parent
commit
d0510bfce7

+ 53 - 48
src/api/safe/camera/index.ts

@@ -1,48 +1,53 @@
-import request from '@/config/axios'
-
-// 安防前端设备 VO
-export interface CameraVO {
-  id: number // 设备ID
-  name: string // 名称
-  code: string // 编号
-  sort: number // 排序
-  type: string // 类型
-  serverId: number // 后端设备
-  aiMode: string // AI模型
-  warn: string // 报警状态
-  warnApi: string // 报警接口
-  desc: string // 描述
-}
-
-// 安防前端设备 API
-export const CameraApi = {
-  // 查询安防前端设备分页
-  getCameraPage: async (params: any) => {
-    return await request.get({ url: `/safe/camera/page`, params })
-  },
-
-  // 查询安防前端设备详情
-  getCamera: async (id: number) => {
-    return await request.get({ url: `/safe/camera/get?id=` + id })
-  },
-
-  // 新增安防前端设备
-  createCamera: async (data: CameraVO) => {
-    return await request.post({ url: `/safe/camera/create`, data })
-  },
-
-  // 修改安防前端设备
-  updateCamera: async (data: CameraVO) => {
-    return await request.put({ url: `/safe/camera/update`, data })
-  },
-
-  // 删除安防前端设备
-  deleteCamera: async (id: number) => {
-    return await request.delete({ url: `/safe/camera/delete?id=` + id })
-  },
-
-  // 导出安防前端设备 Excel
-  exportCamera: async (params) => {
-    return await request.download({ url: `/safe/camera/export-excel`, params })
-  },
-}
+import request from '@/config/axios'
+
+// 安防前端设备 VO
+export interface CameraVO {
+  id: number // 设备ID
+  name: string // 名称
+  code: string // 编号
+  sort: number // 排序
+  type: string // 类型
+  serverId: number // 后端设备
+  aiMode: string // AI模型
+  warn: string // 报警状态
+  warnApi: string // 报警接口
+  desc: string // 描述
+}
+
+// 安防前端设备 API
+export const CameraApi = {
+  // 查询安防前端设备分页
+  getCameraPage: async (params: any) => {
+    return await request.get({ url: `/safe/camera/page`, params })
+  },
+
+  // 查询安防前端设备详情
+  getCamera: async (id: number) => {
+    return await request.get({ url: `/safe/camera/get?id=` + id })
+  },
+
+  // 新增安防前端设备
+  createCamera: async (data: CameraVO) => {
+    return await request.post({ url: `/safe/camera/create`, data })
+  },
+
+  // 修改安防前端设备
+  updateCamera: async (data: CameraVO) => {
+    return await request.put({ url: `/safe/camera/update`, data })
+  },
+
+  // 删除安防前端设备
+  deleteCamera: async (id: number) => {
+    return await request.delete({ url: `/safe/camera/delete?id=` + id })
+  },
+
+  // 导出安防前端设备 Excel
+  exportCamera: async (params) => {
+    return await request.download({ url: `/safe/camera/export-excel`, params })
+  },
+
+  // 根据后端设备id查询前端设备
+  getCameras: async (id: number) => {
+    return await request.get({ url: `/safe/camera/cameras?serverId=` + id })
+  },
+}

+ 5 - 0
src/api/safe/server/index.ts

@@ -53,4 +53,9 @@ export const ServerApi = {
   exportServer: async (params) => {
     return await request.download({ url: `/safe/server/export-excel`, params })
   },
+
+  // 根据机构id查询后端设备
+  getServers: async (id: number) => {
+    return await request.get({ url: `/safe/server/servers?orgId=` + id })
+  },
 }

+ 10 - 0
src/utils/srsUrl.ts

@@ -0,0 +1,10 @@
+// SRS地址转换工具 by  caoyang
+
+export const getWebrtcUrl = (ip: String, domain: String,code: String) => {
+  return 'http://' + ip + ':1985/rtc/v1/whep/?app=' + domain + '&stream=' + code
+}
+
+export const getWebrtcAiUrl = (ip: String, domain: String,code: String) => {
+  return getWebrtcUrl(ip,domain,code) + '-ai'
+}
+

+ 3 - 2
src/views/safe/camera/CameraView.vue

@@ -17,6 +17,7 @@
 <script setup lang="ts">
 import { CameraApi } from '@/api/safe/camera'
 import { ServerApi } from '@/api/safe/server'
+import { getWebrtcUrl, getWebrtcAiUrl } from '@/utils/srsUrl'
 import { onUnmounted} from 'vue'
 
 defineOptions({ name: 'CameraView' })
@@ -41,8 +42,8 @@ const open = async (id: number) => {
       if(camera && camera.serverId){
         const server = await ServerApi.getServer(camera.serverId);
         dialogTitle.value = server.name + ' - ' + camera.name + ' 监视预览'
-        webrtcUrl.value = 'http://' + server.ip + ':1985/rtc/v1/whep/?app=' + server.domain + '&stream=' + camera.code
-        webrtcAiUrl.value = webrtcUrl.value + '-ai'
+        webrtcUrl.value = getWebrtcUrl(server.ip,server.domain,camera.code)
+        webrtcAiUrl.value = getWebrtcAiUrl(server.ip,server.domain,camera.code)
         play('res')
       }
     } finally {

+ 2 - 1
src/views/safe/server/ServerForm.vue

@@ -33,7 +33,7 @@
               :props="defaultProps"
               check-strictly
               node-key="id"
-              placeholder="请选择属机构"
+              placeholder="请选择属机构"
             />
       </el-form-item>
       <el-form-item label="接入协议" prop="protocol">
@@ -106,6 +106,7 @@ const formData = ref({
 const formRules = reactive({
   name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
   protocol: [{ required: true, message: '接入协议不能为空', trigger: 'change' }],
+  orgId: [{ required: true, message: '所属机构不能为空', trigger: 'change' }],
   ip: [{ required: true, message: 'SRS地址不能为空', trigger: 'blur' }],
   port: [{ required: true, message: 'SRS端口不能为空', trigger: 'blur' }],
   domain: [{ required: true, message: 'SRS服务域不能为空', trigger: 'blur' }],

+ 63 - 0
src/views/safe/wall/OrgTree.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="head-container">
+    <el-input v-model="deptName" class="mb-20px" clearable placeholder="请输入机构名称">
+      <template #prefix>
+        <Icon icon="ep:search" />
+      </template>
+    </el-input>
+  </div>
+  <div class="head-container">
+    <el-tree
+      ref="treeRef"
+      :data="deptList"
+      :expand-on-click-node="false"
+      :filter-node-method="filterNode"
+      :props="defaultProps"
+      default-expand-all
+      highlight-current
+      node-key="id"
+      @node-click="handleNodeClick"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ElTree } from 'element-plus'
+import * as DeptApi from '@/api/system/dept'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+defineOptions({ name: 'OrgTree' })
+
+const deptName = ref('')
+const deptList = ref<Tree[]>([]) // 树形结构
+const treeRef = ref<InstanceType<typeof ElTree>>()
+
+/** 获得部门树 */
+const getTree = async () => {
+  const res = await DeptApi.getSimpleDeptList()
+  deptList.value = []
+  deptList.value.push(...handleTree(res))
+}
+
+/** 基于名字过滤 */
+const filterNode = (name: string, data: Tree) => {
+  if (!name) return true
+  return data.name.includes(name)
+}
+
+/** 处理部门被点击 */
+const handleNodeClick = async (row: { [key: string]: any }) => {
+  emits('node-click', row)
+}
+const emits = defineEmits(['node-click'])
+
+/** 监听deptName */
+watch(deptName, (val) => {
+  treeRef.value!.filter(val)
+})
+
+/** 初始化 */
+onMounted(async () => {
+  await getTree()
+})
+</script>

+ 109 - 0
src/views/safe/wall/Player.vue

@@ -0,0 +1,109 @@
+<template>
+  <el-card>
+    <template #header>{{title + '(' + memo + ')'}}</template>
+    <video
+      ref="videoPlayer"
+      controls="true" autoplay="true"
+    ></video>
+    <template #footer>
+      <el-button type="primary" @click="play('res')"><Icon icon="fa-solid:video" /> 原画面</el-button>
+      <el-button type="success" @click="play('ai')"><Icon icon="fa-solid:video" /> AI画面</el-button>
+    </template>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { getWebrtcUrl, getWebrtcAiUrl } from '@/utils/srsUrl'
+import { onUnmounted,defineProps,onMounted } from 'vue'
+
+defineOptions({ name: 'Player' })
+const props = defineProps({
+  ip: String,
+  domain: String,
+  code: String,
+  name: String,
+});
+const videoPlayer = ref(null)
+let sdk = null
+let title = ref('')
+let memo = ref('')
+let playUrl = ref('')
+let webrtcUrl = ref('')
+let webrtcAiUrl = ref('')
+
+/** 打开组件 */
+const open = async (ip: String, domain: String,code: String,name: String) => {
+    try {
+      webrtcUrl.value = getWebrtcUrl(ip,domain,code)
+      webrtcAiUrl.value = getWebrtcAiUrl(ip,domain,code)
+      title.value = name
+      play('res')
+    } finally {
+    }
+}
+
+const play = async (type: String) => {
+  try {
+    if(type === 'res'){
+      playUrl.value = webrtcUrl.value
+      memo.value = '原画面'
+    }else if(type === 'ai'){
+      playUrl.value = webrtcAiUrl.value
+      memo.value = 'AI画面'
+    }
+    console.log('播放地址:' + playUrl.value)
+    if (playUrl.value) {
+      if (sdk) {
+        sdk.close()
+        sdk = null
+      }
+      sdk = new SrsRtcWhipWhepAsync()
+      sdk.play(playUrl.value)
+        .then(function (session) {
+          videoPlayer.value.srcObject = sdk.stream
+        })
+        .catch(function (reason) {
+          console.error(reason)
+        })
+    } else {
+      ElMessage.error(
+        playUrl.value + '视频源无法播放'
+      )
+    }
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+const close = async () => {
+  if (sdk) {
+    sdk.close()
+  }
+  sdk = null
+  playUrl = ref('')
+  memo = ref('')
+  title = ref('')
+}
+onMounted(() => {
+  console.log('Player props:', props.ip, props.domain, props.code, props.name);
+  try {
+      webrtcUrl.value = getWebrtcUrl(props.ip,props.domain,props.code)
+      webrtcAiUrl.value = getWebrtcAiUrl(props.ip,props.domain,props.code)
+      title.value = props.name
+      play('res')
+    } finally {
+    }
+});
+
+onUnmounted(() => {
+  close
+})
+
+
+</script>
+
+<style scoped>
+video {
+  width: 100%;
+  height: auto;
+}
+</style>

+ 101 - 0
src/views/safe/wall/index.vue

@@ -0,0 +1,101 @@
+<template>
+   <el-row :gutter="20">
+    <!-- 左侧部门树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1">
+        <OrgTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="20" :xs="24">
+      <ContentWrap>
+        <el-tabs
+          type="card" v-loading="loading" v-model="firstTab" @tab-click="handleServerClick">
+            <el-tab-pane
+              v-for="server in serverList"
+              :key="server.id"
+              :label="server.name"
+              :name="server.id"
+            >
+              <el-row>
+                <el-col :span="6" v-for="camera in cameraList" :key="camera">
+                  <Player  
+                    :ip="server.ip"
+                    :domain="server.domain"
+                    :code="camera.code"
+                    :name="camera.name"/> 
+                </el-col>
+              </el-row>
+            </el-tab-pane>
+        </el-tabs>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup lang="ts">
+import { ServerApi } from '@/api/safe/server'
+import { CameraApi } from '@/api/safe/camera'
+import Player from './Player.vue'
+import OrgTree from './OrgTree.vue'
+
+/** 安防前端设备 列表 */
+defineOptions({ name: 'SafeWall' })
+
+const loading = ref(true) // 列表的加载中
+let serverList = ref([])//后端设备列表
+let cameraList = ref([])//前端设备列表
+let firstTab = ref('')//标签第一个显示
+
+/** 处理部门(机构)被点击 */
+const handleServerClick = async (tab, event) => {
+  cameraList = ref([])
+  const serverId = tab.props.name;
+  console.log(serverId);
+  await getCameraList(serverId)
+}
+
+/** 处理后端设备被点击 */
+const handleDeptNodeClick = async (row) => {
+  serverList = ref([])
+  cameraList = ref([])
+  firstTab = ref('')
+  await getServerList(row.id)
+}
+
+/** 后端设备列表 */
+const getServerList = async (orgId) => {
+  loading.value = true
+  let active = ''
+  try {
+    serverList.value = await ServerApi.getServers(orgId)
+    if(serverList){
+      serverList.value.forEach(element => {
+        active = element.name
+        return
+      });
+    }
+  } finally {
+    firstTab.value = active
+    loading.value = false
+    console.log('aaaaa:',firstTab)
+  }
+}
+
+/** 前端设备列表 */
+const getCameraList = async (serverId) => {
+  loading.value = true
+  try {
+    if(serverId){
+      cameraList.value = await CameraApi.getCameras(serverId)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+
+/** 初始化 **/
+onMounted(async () => {
+  loading.value = false
+})
+</script>