| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- <template>
- <div id="LivePlayerBox">
- <div
- v-for="(item, index) in streamList"
- :key="index"
- :class="['live-video-item', { 'is-selected': selectedIndex === index }]"
- @click.stop="handleSelect(item, index)"
- @dblclick.capture.stop.prevent="handleDblclick($event, item, index)"
- >
- <component :is="item.zj" :src="item.src" :autoplay="item.autoplay" class="liveVideo" />
- </div>
- </div>
- <!-- 双击放大:弹窗预览(青蓝监控风格,等比展示不失真) -->
- <Teleport to="body">
- <div v-if="previewVisible" class="camera-preview-mask" @click.self="closePreview">
- <div
- class="camera-preview-panel"
- :style="{ transform: `translate(${previewOffset.x}px, ${previewOffset.y}px)` }"
- >
- <div class="camera-preview-header" @mousedown="onPreviewDragStart">
- <span class="camera-preview-title">{{ previewTitle }}</span>
- <div class="camera-preview-close" title="关闭" @click="closePreview" @mousedown.stop>×</div>
- </div>
- <div class="camera-preview-body">
- <div class="camera-preview-stage">
- <component
- v-if="previewItem"
- :is="previewItem.zj"
- :src="previewItem.src"
- :autoplay="true"
- :stretch="false"
- class="camera-preview-player"
- />
- </div>
- </div>
- </div>
- </div>
- </Teleport>
- </template>
- <script setup lang="ts">
- import { reactive, ref, onMounted, onBeforeUnmount } from 'vue'
- import HlsPlayer from '@/components/vent/camera/HlsPlayer.vue'
- import LivePlayerWrapper from '@/components/vent/camera/LivePlayerWrapper.vue'
- interface PreviewStreamItem {
- zj: any
- src: string
- autoplay: boolean
- name?: string
- meta?: any
- }
- const props = defineProps<{
- srcList: any[]
- /** 自动播放起始下标,配合 autoPlayCount 使用,默认 0 */
- autoPlayStart?: number
- /** 自动播放数量:未传则全部自动播放 */
- autoPlayCount?: number
- /** 摄像头选中回调 */
- onSelect?: (camera: any, index: number) => void
- }>()
- const streamList = ref<PreviewStreamItem[]>([])
- /** 当前选中的摄像头下标 */
- const selectedIndex = ref(-1)
- /** 弹窗预览 */
- const previewVisible = ref(false)
- const previewTitle = ref('摄像头预览')
- const previewItem = ref<PreviewStreamItem | null>(null)
- /** 弹窗拖动偏移 */
- const previewOffset = reactive({ x: 0, y: 0 })
- let dragging = false
- let dragStartX = 0
- let dragStartY = 0
- let dragOriginX = 0
- let dragOriginY = 0
- function handleSelect(item: PreviewStreamItem, index: number) {
- selectedIndex.value = index
- props.onSelect?.(item.meta || { addr: item.src, name: item.name }, index)
- }
- function exitBrowserFullscreen() {
- if (document.fullscreenElement && document.exitFullscreen) {
- document.exitFullscreen().catch(() => {})
- }
- }
- function onPreviewDragMove(e: MouseEvent) {
- if (!dragging) return
- previewOffset.x = dragOriginX + (e.clientX - dragStartX)
- previewOffset.y = dragOriginY + (e.clientY - dragStartY)
- }
- function onPreviewDragEnd() {
- if (!dragging) return
- dragging = false
- window.removeEventListener('mousemove', onPreviewDragMove)
- window.removeEventListener('mouseup', onPreviewDragEnd)
- }
- /** 按住标题栏拖动弹窗 */
- function onPreviewDragStart(e: MouseEvent) {
- if (e.button !== 0) return
- dragging = true
- dragStartX = e.clientX
- dragStartY = e.clientY
- dragOriginX = previewOffset.x
- dragOriginY = previewOffset.y
- window.addEventListener('mousemove', onPreviewDragMove)
- window.addEventListener('mouseup', onPreviewDragEnd)
- }
- function closePreview() {
- onPreviewDragEnd()
- previewVisible.value = false
- previewItem.value = null
- previewOffset.x = 0
- previewOffset.y = 0
- }
- /** 双击:弹窗内等比放大预览,不进入浏览器全屏、不撑破网格 */
- function handleDblclick(_e: MouseEvent, item: PreviewStreamItem, index: number) {
- handleSelect(item, index)
- exitBrowserFullscreen()
- if (previewVisible.value && selectedIndex.value === index && previewItem.value?.src === item.src) {
- closePreview()
- return
- }
- previewTitle.value = item.name || item.meta?.name || '摄像头预览'
- previewItem.value = {
- zj: item.zj,
- src: item.src,
- autoplay: true,
- name: item.name,
- meta: item.meta,
- }
- previewOffset.x = 0
- previewOffset.y = 0
- previewVisible.value = true
- }
- onMounted(() => {
- const componentList: PreviewStreamItem[] = []
- const start = props.autoPlayStart ?? 0
- for (let i = 0; i < props.srcList.length; i++) {
- const stream = props.srcList[i]
- if (stream.addr.includes('0.0.0.0')) {
- stream.addr = stream.addr.replace('0.0.0.0', window.location.hostname)
- }
- const autoplay =
- props.autoPlayCount == null ? true : i >= start && i < start + props.autoPlayCount
- const meta = {
- name: stream.name,
- addr: stream.addr,
- cameraRate: stream.cameraRate,
- devicekind: stream.devicekind,
- id: stream.id,
- deviceid: stream.deviceid || stream.devId || stream.deviceId || stream.deviceID || '',
- ip: stream.ip || '',
- }
- if (stream.addr.toLowerCase().includes('.m3u8') || stream.addr.toLowerCase().startsWith('rtsp://')) {
- componentList.push({ zj: HlsPlayer, src: stream.addr, autoplay, name: stream.name, meta })
- } else if (stream.addr.startsWith('rtmp://')) {
- componentList.push({ zj: LivePlayerWrapper, src: stream.addr, autoplay, name: stream.name, meta })
- } else {
- componentList.push({ zj: LivePlayerWrapper, src: stream.addr, autoplay, name: stream.name, meta })
- }
- }
- streamList.value = componentList
- })
- onBeforeUnmount(() => {
- closePreview()
- streamList.value = []
- selectedIndex.value = -1
- })
- </script>
- <style lang="less" scoped>
- .camera-preview-mask {
- position: fixed;
- inset: 0;
- z-index: 3000;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(9, 23, 44, 0.82);
- backdrop-filter: blur(4px);
- }
- .camera-preview-panel {
- /* 略缩小弹窗,仍按 16:9 等比约束,避免画面被强行拉伸 */
- width: min(72vw, calc(68vh * 16 / 9), 960px);
- max-height: 78vh;
- display: flex;
- flex-direction: column;
- border: 1px solid rgba(0, 210, 255, 0.75);
- box-shadow:
- 0 0 0 2px rgba(8, 40, 70, 0.95),
- 0 0 22px rgba(0, 200, 255, 0.3),
- inset 0 0 0 1px rgba(0, 180, 255, 0.28);
- background: linear-gradient(180deg, rgba(6, 28, 48, 0.98) 0%, rgba(4, 16, 32, 0.98) 100%);
- overflow: hidden;
- will-change: transform;
- }
- .camera-preview-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 36px;
- padding: 0 14px;
- flex-shrink: 0;
- color: #fff;
- background: linear-gradient(to right, #134c77, transparent);
- border-bottom: 1px solid rgba(41, 171, 241, 0.45);
- cursor: move;
- user-select: none;
- }
- .camera-preview-title {
- color: #e8f4ff;
- font-size: 14px;
- font-weight: 600;
- letter-spacing: 0.5px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .camera-preview-close {
- width: 28px;
- height: 24px;
- line-height: 20px;
- text-align: center;
- cursor: pointer;
- color: #01fefc;
- border: 1px solid #01fefc;
- border-radius: 2px;
- flex-shrink: 0;
- transition: background-color 0.2s, box-shadow 0.2s;
- user-select: none;
- &:hover {
- background-color: rgba(36, 132, 188, 0.55);
- box-shadow: 0 0 6px rgba(1, 254, 252, 0.45);
- }
- }
- .camera-preview-body {
- flex: 1;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 14px;
- box-sizing: border-box;
- background: #0a1a2e;
- }
- .camera-preview-stage {
- width: 100%;
- aspect-ratio: 16 / 9;
- max-height: calc(78vh - 64px);
- background: #000;
- border: 1px solid rgba(0, 183, 255, 0.4);
- overflow: hidden;
- position: relative;
- }
- .camera-preview-player {
- width: 100% !important;
- height: 100% !important;
- display: block;
- }
- :deep(.camera-preview-stage video),
- :deep(.camera-preview-stage canvas),
- :deep(.camera-preview-stage .liveVideo),
- :deep(.camera-preview-stage .video-js),
- :deep(.camera-preview-stage .video-wrapper),
- :deep(.camera-preview-stage .player-wrapper) {
- width: 100% !important;
- height: 100% !important;
- max-width: 100%;
- max-height: 100%;
- object-fit: contain !important;
- background: #000;
- }
- </style>
|