Commit c2819b0f692e6a20dcafbdf60bb7981fd2bf98c1

Authored by xp.Huang
2 parents 6d5dafbd 8414e9ef

Merge branch 'ww' into 'main'

fix: BUG in teambition && usage video.js replace vue3-video-player

See merge request huang/yun-teng-iot-front!383
1 -{  
2 - "i18n-ally.localesPaths": [  
3 - "src/locales",  
4 - "src/locales/lang",  
5 - "public/resource/tinymce/langs"  
6 - ],  
7 - "commentTranslate.targetLanguage": "en",  
8 - "cSpell.words": [  
9 - "Cmds",  
10 - "unref"  
11 - ]  
12 -}  
@@ -63,6 +63,7 @@ @@ -63,6 +63,7 @@
63 "sortablejs": "^1.14.0", 63 "sortablejs": "^1.14.0",
64 "tinymce": "^5.8.2", 64 "tinymce": "^5.8.2",
65 "vditor": "^3.8.6", 65 "vditor": "^3.8.6",
  66 + "video.js": "^7.20.3",
66 "vue": "^3.2.31", 67 "vue": "^3.2.31",
67 "vue-i18n": "9.1.7", 68 "vue-i18n": "9.1.7",
68 "vue-json-pretty": "^2.0.4", 69 "vue-json-pretty": "^2.0.4",
@@ -90,6 +91,7 @@ @@ -90,6 +91,7 @@
90 "@types/qrcode": "^1.4.1", 91 "@types/qrcode": "^1.4.1",
91 "@types/qs": "^6.9.7", 92 "@types/qs": "^6.9.7",
92 "@types/sortablejs": "^1.10.7", 93 "@types/sortablejs": "^1.10.7",
  94 + "@types/video.js": "^7.3.49",
93 "@typescript-eslint/eslint-plugin": "^4.29.1", 95 "@typescript-eslint/eslint-plugin": "^4.29.1",
94 "@typescript-eslint/parser": "^4.29.1", 96 "@typescript-eslint/parser": "^4.29.1",
95 "@vitejs/plugin-legacy": "^1.5.1", 97 "@vitejs/plugin-legacy": "^1.5.1",
@@ -344,7 +344,7 @@ @@ -344,7 +344,7 @@
344 } 344 }
345 345
346 &-form-container { 346 &-form-container {
347 - padding: 16px 16px 16px 36px; 347 + padding: 16px 16px 16px 16px;
348 348
349 .ant-form { 349 .ant-form {
350 padding: 12px 10px 6px 10px; 350 padding: 12px 10px 6px 10px;
  1 +import { withInstall } from '/@/utils/index';
  2 +import VideoPlay from './src/VideoPlay.vue';
  3 +
  4 +export { getVideoTypeByUrl } from './src/utils';
  5 +
  6 +export const BasicVideoPlay = withInstall(VideoPlay);
  1 +<script lang="ts" setup>
  2 + import { isNumber } from 'lodash';
  3 + import videoJs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js';
  4 + import 'video.js/dist/video-js.css';
  5 + import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
  6 + import { useDesign } from '/@/hooks/web/useDesign';
  7 +
  8 + const { prefixCls } = useDesign('basic-video-play');
  9 +
  10 + const props = defineProps<{
  11 + options?: VideoJsPlayerOptions;
  12 + }>();
  13 +
  14 + const emit = defineEmits<{
  15 + (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
  16 + }>();
  17 +
  18 + const videoPlayEl = ref<HTMLVideoElement>();
  19 +
  20 + const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();
  21 +
  22 + const getOptions = computed(() => {
  23 + const { options } = props;
  24 + const defaultOptions: VideoJsPlayerOptions = {
  25 + language: 'zh',
  26 + muted: true,
  27 + liveui: true,
  28 + controls: true,
  29 + };
  30 + return { ...defaultOptions, ...options };
  31 + });
  32 +
  33 + const getWidthHeight = computed(() => {
  34 + let { width = 300, height = 150 } = unref(getOptions);
  35 + width = isNumber(width) ? (`${width}px` as unknown as number) : width;
  36 + height = isNumber(height) ? (`${height}px` as unknown as number) : height;
  37 + return { width, height } as CSSProperties;
  38 + });
  39 +
  40 + const init = () => {
  41 + videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => {
  42 + emit('ready', unref(videoPlayInstance));
  43 + });
  44 + };
  45 +
  46 + onMounted(() => {
  47 + init();
  48 + });
  49 +
  50 + onUnmounted(() => {
  51 + unref(videoPlayInstance)?.dispose();
  52 + videoPlayInstance.value = null;
  53 + });
  54 +</script>
  55 +
  56 +<template>
  57 + <div :class="prefixCls" class="w-full h-full" :style="getWidthHeight">
  58 + <video
  59 + ref="videoPlayEl"
  60 + class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full"
  61 + ></video>
  62 + </div>
  63 +</template>
  64 +
  65 +<style lang="less">
  66 + @prefix-cls: ~'@{namespace}-basic-video-play';
  67 +
  68 + .@{prefix-cls} {
  69 + .vjs-error-display {
  70 + .vjs-modal-dialog-content::after {
  71 + content: '无法加载视频,原因可能是服务器或网络故障,也可能是格式不支持.';
  72 + }
  73 + }
  74 + }
  75 +</style>
  1 +export enum VideoPlayerType {
  2 + m3u8 = 'application/x-mpegURL',
  3 + mp4 = 'video/mp4',
  4 + webm = 'video/webm',
  5 +}
  6 +
  7 +export const getVideoTypeByUrl = (url: string) => {
  8 + const splitExtReg = /(?:.*)(?<=\.)/;
  9 +
  10 + const type = url.replace(splitExtReg, '');
  11 +
  12 + if (VideoPlayerType[type]) return VideoPlayerType[type];
  13 +
  14 + return VideoPlayerType.mp4;
  15 +};
@@ -383,7 +383,7 @@ export const MediaTypeValidate: Rule[] = [ @@ -383,7 +383,7 @@ export const MediaTypeValidate: Rule[] = [
383 required: true, 383 required: true,
384 validator: (_, value: string) => { 384 validator: (_, value: string) => {
385 const reg = /(?:.*)(?<=\.)/; 385 const reg = /(?:.*)(?<=\.)/;
386 - const type = value.replace(reg, ''); 386 + const type = (value || '').replace(reg, '');
387 if (type !== MediaType.M3U8) { 387 if (type !== MediaType.M3U8) {
388 return Promise.reject('视频流只支持m3u8格式'); 388 return Promise.reject('视频流只支持m3u8格式');
389 } 389 }
@@ -3,117 +3,63 @@ @@ -3,117 +3,63 @@
3 <BasicModal 3 <BasicModal
4 v-bind="$attrs" 4 v-bind="$attrs"
5 width="55rem" 5 width="55rem"
  6 + destroyOnClose
6 :height="heightNum" 7 :height="heightNum"
7 @register="register" 8 @register="register"
8 title="视频预览" 9 title="视频预览"
9 - @cancel="handleCancel"  
10 :showOkBtn="false" 10 :showOkBtn="false"
  11 + @cancel="handleCancel"
11 > 12 >
12 - <div class="video-sty">  
13 - <div>  
14 - <videoPlay  
15 - v-if="showVideo"  
16 - ref="video"  
17 - style="display: inline-block; width: 100%"  
18 - v-bind="options"  
19 - />  
20 - </div> 13 + <div class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52">
  14 + <BasicVideoPlay v-if="showVideo" :options="options" />
21 </div> 15 </div>
22 - <!-- <div  
23 - class="bg-black h-80 text-light-50 w-full h-full flex justify-center items-center"  
24 - v-if="!showVideo"  
25 - >  
26 - 视频播放出错啦!  
27 - </div> -->  
28 </BasicModal> 16 </BasicModal>
29 </div> 17 </div>
30 </template> 18 </template>
31 <script setup lang="ts"> 19 <script setup lang="ts">
32 - import { ref, nextTick, reactive } from 'vue'; 20 + import { ref, reactive } from 'vue';
33 import { BasicModal, useModalInner } from '/@/components/Modal'; 21 import { BasicModal, useModalInner } from '/@/components/Modal';
34 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; 22 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
35 - import { videoPlay } from 'vue3-video-play'; // 引入组件  
36 - import 'vue3-video-play/dist/style.css'; // 引入css  
37 - import { AccessMode, MediaType } from './config.data'; 23 + import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
  24 + import { AccessMode } from './config.data';
38 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; 25 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
  26 + import { VideoJsPlayerOptions } from 'video.js';
39 27
40 const heightNum = ref(800); 28 const heightNum = ref(800);
41 const showVideo = ref(false); 29 const showVideo = ref(false);
42 - const options = reactive({  
43 - width: '800px',  
44 - height: '450px',  
45 - color: '#409eff',  
46 - muted: false, //静音  
47 - webFullScreen: false,  
48 - autoPlay: true, //自动播放  
49 - currentTime: 0,  
50 - loop: false, //循环播放  
51 - mirror: false, //镜像画面  
52 - ligthOff: false, //关灯模式  
53 - volume: 0.3, //默认音量大小  
54 - control: true, //是否显示控制器  
55 - title: '', //视频名称  
56 - type: 'm3u8',  
57 - src: '', //视频源  
58 - controlBtns: [  
59 - 'audioTrack',  
60 - 'quality',  
61 - 'speedRate',  
62 - 'volume',  
63 - 'setting',  
64 - 'pip',  
65 - 'pageFullScreen',  
66 - 'fullScreen',  
67 - ], 30 + const options = reactive<VideoJsPlayerOptions>({
  31 + width: '100%' as unknown as number,
  32 + height: '100%' as unknown as number,
  33 + autoplay: true,
68 }); 34 });
69 - const video: any = ref(null);  
70 35
71 - nextTick(() => {  
72 - console.log(video.value);  
73 - });  
74 -  
75 - const getMediaType = (suffix: string) => {  
76 - return suffix === MediaType.M3U8 ? suffix : `video/${suffix}`; 36 + const setSources = (url: string) => {
  37 + options.sources = [
  38 + {
  39 + src: url,
  40 + type: getVideoTypeByUrl(url),
  41 + },
  42 + ];
77 }; 43 };
78 44
79 const [register] = useModalInner( 45 const [register] = useModalInner(
80 async (data: { record: CameraModel | StreamingManageRecord }) => { 46 async (data: { record: CameraModel | StreamingManageRecord }) => {
81 - let reg = /(?:.*)(?<=\.)/;  
82 const { record } = data; 47 const { record } = data;
83 if (record.accessMode === AccessMode.ManuallyEnter) { 48 if (record.accessMode === AccessMode.ManuallyEnter) {
84 if ((record as CameraModel).videoUrl) { 49 if ((record as CameraModel).videoUrl) {
85 - const type = (record as CameraModel).videoUrl.replace(reg, '');  
86 - showVideo.value = true;  
87 - options.type = getMediaType(type);  
88 - options.src = (record as CameraModel).videoUrl;  
89 - options.autoPlay = true; 50 + setSources((record as CameraModel).videoUrl);
90 } 51 }
91 } else { 52 } else {
92 try { 53 try {
93 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); 54 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
94 - showVideo.value = true;  
95 - options.src = url;  
96 - const type = (url as CameraModel).videoUrl.replace(reg, '');  
97 - options.type = getMediaType(type);  
98 - } catch (error) {  
99 - } finally {  
100 - showVideo.value = true;  
101 - } 55 + setSources(url);
  56 + } catch (error) {}
102 } 57 }
  58 + showVideo.value = true;
103 } 59 }
104 ); 60 );
105 61
106 const handleCancel = () => { 62 const handleCancel = () => {
107 - //关闭暂停播放视频  
108 - options.src = '';  
109 - video.value.pause(); 63 + showVideo.value = false;
110 }; 64 };
111 </script> 65 </script>
112 -<style>  
113 - .video-sty {  
114 - width: 100%;  
115 - display: flex;  
116 - align-items: center;  
117 - justify-content: center;  
118 - }  
119 -</style>  
@@ -5,21 +5,28 @@ @@ -5,21 +5,28 @@
5 import { Spin, Button, Pagination, Space, List } from 'ant-design-vue'; 5 import { Spin, Button, Pagination, Space, List } from 'ant-design-vue';
6 import { cameraPage } from '/@/api/camera/cameraManager'; 6 import { cameraPage } from '/@/api/camera/cameraManager';
7 import { CameraRecord } from '/@/api/camera/model/cameraModel'; 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8 - import { videoPlay as VideoPlay } from 'vue3-video-play';  
9 import 'vue3-video-play/dist/style.css'; 8 import 'vue3-video-play/dist/style.css';
10 import { useFullscreen } from '@vueuse/core'; 9 import { useFullscreen } from '@vueuse/core';
11 import CameraDrawer from './CameraDrawer.vue'; 10 import CameraDrawer from './CameraDrawer.vue';
12 import { useDrawer } from '/@/components/Drawer'; 11 import { useDrawer } from '/@/components/Drawer';
13 - import { AccessMode, MediaType, PageMode } from './config.data'; 12 + import { AccessMode, PageMode } from './config.data';
14 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; 13 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
15 - import { isDef } from '/@/utils/is';  
16 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; 14 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
17 import { buildUUID } from '/@/utils/uuid'; 15 import { buildUUID } from '/@/utils/uuid';
  16 + import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
  17 + import { VideoJsPlayerOptions } from 'video.js';
  18 + import { getBoundingClientRect } from '/@/utils/domUtils';
18 19
19 type CameraRecordItem = CameraRecord & { 20 type CameraRecordItem = CameraRecord & {
20 canPlay?: boolean; 21 canPlay?: boolean;
21 - type?: string;  
22 isTransform?: boolean; 22 isTransform?: boolean;
  23 + videoPlayerOptions?: VideoJsPlayerOptions;
  24 + };
  25 +
  26 + const basicVideoPlayOptions: VideoJsPlayerOptions = {
  27 + width: '100%' as unknown as number,
  28 + height: '100%' as unknown as number,
  29 + autoplay: true,
23 }; 30 };
24 31
25 const emit = defineEmits(['switchMode']); 32 const emit = defineEmits(['switchMode']);
@@ -36,32 +43,6 @@ @@ -36,32 +43,6 @@
36 total: 0, 43 total: 0,
37 }); 44 });
38 45
39 - const options = reactive({  
40 - width: '200px',  
41 - height: '200px',  
42 - color: '#409eff',  
43 - muted: true, //静音  
44 - webFullScreen: false,  
45 - autoPlay: true, //自动播放  
46 - currentTime: 0,  
47 - loop: false, //循环播放  
48 - mirror: false, //镜像画面  
49 - ligthOff: false, //关灯模式  
50 - volume: 0.3, //默认音量大小  
51 - control: true, //是否显示控制器  
52 - type: 'm3u8',  
53 - controlBtns: [  
54 - 'audioTrack',  
55 - 'quality',  
56 - 'speedRate',  
57 - 'volume',  
58 - 'setting',  
59 - 'pip',  
60 - 'pageFullScreen',  
61 - 'fullScreen',  
62 - ],  
63 - });  
64 -  
65 // 树形选择器 46 // 树形选择器
66 const handleSelect = (orgId: string) => { 47 const handleSelect = (orgId: string) => {
67 organizationId.value = orgId; 48 organizationId.value = orgId;
@@ -80,7 +61,6 @@ @@ -80,7 +61,6 @@
80 pagination.total = total; 61 pagination.total = total;
81 62
82 for (const item of items) { 63 for (const item of items) {
83 - // await beforeVideoPlay(item);  
84 (item as CameraRecordItem).isTransform = false; 64 (item as CameraRecordItem).isTransform = false;
85 beforeVideoPlay(item); 65 beforeVideoPlay(item);
86 } 66 }
@@ -97,30 +77,39 @@ @@ -97,30 +77,39 @@
97 loading.value = false; 77 loading.value = false;
98 } 78 }
99 }; 79 };
100 - const getMediaType = (suffix: string) => {  
101 - return suffix === MediaType.M3U8 ? suffix : `video/${suffix}`;  
102 - };  
103 80
104 const beforeVideoPlay = async (record: CameraRecordItem) => { 81 const beforeVideoPlay = async (record: CameraRecordItem) => {
105 - let reg = /(?:.*)(?<=\.)/;  
106 if (record.accessMode === AccessMode.ManuallyEnter) { 82 if (record.accessMode === AccessMode.ManuallyEnter) {
107 if (record.videoUrl) { 83 if (record.videoUrl) {
108 - const type = record.videoUrl.replace(reg, '');  
109 - record.type = getMediaType(type); 84 + (record as CameraRecordItem).videoPlayerOptions = {
  85 + ...basicVideoPlayOptions,
  86 + sources: [
  87 + {
  88 + src: record.videoUrl,
  89 + type: getVideoTypeByUrl(record.videoUrl),
  90 + },
  91 + ],
  92 + };
110 record.isTransform = true; 93 record.isTransform = true;
111 } 94 }
112 } 95 }
113 if (record.accessMode === AccessMode.Streaming) { 96 if (record.accessMode === AccessMode.Streaming) {
114 try { 97 try {
115 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); 98 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
116 - const type = url.replace(reg, '');  
117 const index = unref(cameraList).findIndex((item) => item.id === record.id); 99 const index = unref(cameraList).findIndex((item) => item.id === record.id);
118 if (~index) { 100 if (~index) {
119 const oldRecord = unref(cameraList).at(index)!; 101 const oldRecord = unref(cameraList).at(index)!;
120 unref(cameraList)[index] = { 102 unref(cameraList)[index] = {
121 ...oldRecord, 103 ...oldRecord,
122 - videoUrl: url,  
123 - type: getMediaType(type), 104 + videoPlayerOptions: {
  105 + ...basicVideoPlayOptions,
  106 + sources: [
  107 + {
  108 + src: url,
  109 + type: getVideoTypeByUrl(url),
  110 + },
  111 + ],
  112 + },
124 isTransform: true, 113 isTransform: true,
125 }; 114 };
126 } 115 }
@@ -160,20 +149,6 @@ @@ -160,20 +149,6 @@
160 } 149 }
161 }; 150 };
162 151
163 - const handleLoadStart = (record: CameraRecordItem) => {  
164 - const index = unref(cameraList).findIndex((item) => item.id === record.id);  
165 - setTimeout(() => {  
166 - ~index &&  
167 - !unref(cameraList).at(index)!.canPlay &&  
168 - (unref(cameraList).at(index)!.canPlay = false);  
169 - }, 30000);  
170 - };  
171 -  
172 - const handleLoadData = (record: CameraRecordItem) => {  
173 - const index = unref(cameraList).findIndex((item) => item.id === record.id);  
174 - ~index && (unref(cameraList).at(index)!.canPlay = true);  
175 - };  
176 -  
177 const [registerDrawer, { openDrawer }] = useDrawer(); 152 const [registerDrawer, { openDrawer }] = useDrawer();
178 153
179 const handleAddCamera = () => { 154 const handleAddCamera = () => {
@@ -185,13 +160,26 @@ @@ -185,13 +160,26 @@
185 onMounted(() => { 160 onMounted(() => {
186 getCameraList(); 161 getCameraList();
187 }); 162 });
  163 +
  164 + const listEl = ref();
  165 + onMounted(() => {
  166 + const clientHeight = document.documentElement.clientHeight;
  167 + const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect;
  168 + // list pading top 8 maring-top 8 extra slot 56
  169 + const listContainerMarginBottom = 16;
  170 + const listContainerHeight = clientHeight - rect.top - listContainerMarginBottom;
  171 + const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector(
  172 + '.ant-spin-container'
  173 + ) as HTMLElement;
  174 + listContainerEl && (listContainerEl.style.height = listContainerHeight + 'px');
  175 + });
188 </script> 176 </script>
189 177
190 <template> 178 <template>
191 <div> 179 <div>
192 <PageWrapper dense contentFullHeight contentClass="flex"> 180 <PageWrapper dense contentFullHeight contentClass="flex">
193 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> 181 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
194 - <section class="p-4 pl-9 split-screen-mode flex flex-col flex-auto w-3/4 xl:w-4/5"> 182 + <section class="p-4 split-screen-mode flex flex-col flex-auto w-3/4 xl:w-4/5">
195 <div class="p-3 bg-light-50 flex justify-between mb-4 dark:bg-dark-900"> 183 <div class="p-3 bg-light-50 flex justify-between mb-4 dark:bg-dark-900">
196 <div class="flex gap-4 cursor-pointer items-center"> 184 <div class="flex gap-4 cursor-pointer items-center">
197 <div 185 <div
@@ -247,6 +235,7 @@ @@ -247,6 +235,7 @@
247 </div> 235 </div>
248 <section ref="videoContainer" class="flex-auto"> 236 <section ref="videoContainer" class="flex-auto">
249 <List 237 <List
  238 + ref="listEl"
250 :loading="loading" 239 :loading="loading"
251 :data-source="cameraList" 240 :data-source="cameraList"
252 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list" 241 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list"
@@ -264,24 +253,12 @@ @@ -264,24 +253,12 @@
264 v-if="!item.placeholder" 253 v-if="!item.placeholder"
265 class="bg-black w-full h-full overflow-hidden relative video-container" 254 class="bg-black w-full h-full overflow-hidden relative video-container"
266 > 255 >
267 - <Spin v-show="!item.isTransform" :spinning="!item.isTransform">  
268 - <div class="bg-black text-light-50"> </div>  
269 - </Spin>  
270 - <VideoPlay  
271 - v-show="item.isTransform"  
272 - @loadstart="handleLoadStart(item)"  
273 - @loadeddata="handleLoadData(item)"  
274 - v-bind="options"  
275 - :src="item.videoUrl"  
276 - :title="item.name"  
277 - :type="item.type" 256 + <Spin
  257 + class="!absolute top-1/2 left-1/2 transform -translate-1/2"
  258 + v-show="!item.isTransform"
  259 + :spinning="!item.isTransform"
278 /> 260 />
279 - <div  
280 - v-if="item.isTransform && isDef(item.canPlay) && !item.canPlay"  
281 - class="video-container-error-msk absolute top-0 left-0 text-lg w-full h-full text-light-50 flex justify-center items-center z-50 bg-black"  
282 - >  
283 - 视频加载出错了!  
284 - </div> 261 + <BasicVideoPlay v-if="item.isTransform" :options="item.videoPlayerOptions" />
285 <div 262 <div
286 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center" 263 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center"
287 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" 264 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
@@ -313,24 +290,6 @@ @@ -313,24 +290,6 @@
313 height: 100%; 290 height: 100%;
314 } 291 }
315 292
316 - .split-screen-mode:deep(.d-player-wrap) {  
317 - width: 100%;  
318 - height: 100%;  
319 - }  
320 -  
321 - .split-screen-mode:deep(.ant-tabs-tab-active) {  
322 - border-bottom: 1px solid #eee;  
323 - }  
324 -  
325 - .split-screen-mode:deep(video) {  
326 - position: absolute;  
327 - height: calc(100%) !important;  
328 - }  
329 -  
330 - .split-screen-mode:deep(.d-player-control) {  
331 - z-index: 99;  
332 - }  
333 -  
334 .video-container { 293 .video-container {
335 .video-container-mask { 294 .video-container-mask {
336 opacity: 0; 295 opacity: 0;
@@ -344,8 +303,6 @@ @@ -344,8 +303,6 @@
344 } 303 }
345 304
346 .video-container-error-msk { 305 .video-container-error-msk {
347 - // opacity: 0;  
348 - // visibility: hidden;  
349 color: #000; 306 color: #000;
350 } 307 }
351 } 308 }
@@ -363,7 +320,6 @@ @@ -363,7 +320,6 @@
363 320
364 .split-mode-list:deep(.ant-col) { 321 .split-mode-list:deep(.ant-col) {
365 width: 100%; 322 width: 100%;
366 - // height: var(--height);  
367 height: 100%; 323 height: 100%;
368 } 324 }
369 </style> 325 </style>
@@ -173,8 +173,8 @@ export const formSchema: QFormSchema[] = [ @@ -173,8 +173,8 @@ export const formSchema: QFormSchema[] = [
173 { 173 {
174 field: 'videoUrl', 174 field: 'videoUrl',
175 label: '视频流', 175 label: '视频流',
176 - required: true,  
177 component: 'Input', 176 component: 'Input',
  177 + required: true,
178 ifShow({ values }) { 178 ifShow({ values }) {
179 return values.accessMode === AccessMode.ManuallyEnter; 179 return values.accessMode === AccessMode.ManuallyEnter;
180 }, 180 },
@@ -182,7 +182,7 @@ export const formSchema: QFormSchema[] = [ @@ -182,7 +182,7 @@ export const formSchema: QFormSchema[] = [
182 placeholder: '请输入视频流', 182 placeholder: '请输入视频流',
183 maxLength: 255, 183 maxLength: 255,
184 }, 184 },
185 - rules: [...CameraVideoUrl, ...MediaTypeValidate, { required: true, message: '视频流是必填项' }], 185 + rules: [{ required: true, message: '视频流是必填项' }, ...CameraVideoUrl, ...MediaTypeValidate],
186 }, 186 },
187 187
188 { 188 {
1 <template> 1 <template>
2 - <div class="organization-tree flex relative">  
3 - <div class="cursor-pointer flex py-4 fold-icon" :class="foldFlag ? 'absolute' : ''"> 2 + <div class="organization-tree flex relative items-center py-4" :class="foldFlag ? '' : 'pl-4'">
  3 + <div
  4 + class="cursor-pointer flex py-4 fold-icon absolute rounded svg:fill-gray-400 hover:bg-gray-200"
  5 + :class="foldFlag ? '' : '-right-4'"
  6 + >
4 <div @click="handleFold"> 7 <div @click="handleFold">
5 - <CaretRightOutlined :class="[foldFlag ? '' : 'rotate-180']" class="text-xl transform" /> 8 + <CaretRightOutlined
  9 + :class="[foldFlag ? '' : 'rotate-180']"
  10 + class="transform fill-gray-100"
  11 + />
6 </div> 12 </div>
7 </div> 13 </div>
8 - <div  
9 - :style="{ width: foldFlag ? '0px' : '100%' }"  
10 - :class="[foldFlag ? '' : 'my-4 ml-2']"  
11 - class="bg-white mr-0 overflow-hidden"  
12 - > 14 + <div :style="{ width: foldFlag ? '0px' : '100%' }" class="bg-white mr-0 overflow-hidden h-full">
13 <BasicTree 15 <BasicTree
14 title="组织列表" 16 title="组织列表"
15 toolbar 17 toolbar
@@ -161,7 +161,7 @@ @@ -161,7 +161,7 @@
161 <template> 161 <template>
162 <PageWrapper dense contentFullHeight contentClass="flex"> 162 <PageWrapper dense contentFullHeight contentClass="flex">
163 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> 163 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
164 - <section class="flex-auto pl-9 p-4 configuration-list"> 164 + <section class="flex-auto p-4 configuration-list">
165 <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4"> 165 <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4">
166 <BasicForm @register="registerForm" /> 166 <BasicForm @register="registerForm" />
167 </div> 167 </div>
@@ -4,6 +4,7 @@ import { deviceProfile, getGATEWAYdevice } from '/@/api/device/deviceManager'; @@ -4,6 +4,7 @@ import { deviceProfile, getGATEWAYdevice } from '/@/api/device/deviceManager';
4 4
5 export enum TypeEnum { 5 export enum TypeEnum {
6 IS_GATEWAY = 'GATEWAY', 6 IS_GATEWAY = 'GATEWAY',
  7 + SENSOR = 'SENSOR',
7 } 8 }
8 export const isGateWay = (type: string) => { 9 export const isGateWay = (type: string) => {
9 return type === TypeEnum.IS_GATEWAY; 10 return type === TypeEnum.IS_GATEWAY;
@@ -5,46 +5,60 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; @@ -5,46 +5,60 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
5 import { getCustomerList } from '/@/api/device/deviceManager'; 5 import { getCustomerList } from '/@/api/device/deviceManager';
6 import { DescItem } from '/@/components/Description/index'; 6 import { DescItem } from '/@/components/Description/index';
7 import moment from 'moment'; 7 import moment from 'moment';
  8 +import { h } from 'vue';
  9 +import { Button } from 'ant-design-vue';
  10 +import { TypeEnum } from './data';
  11 +
8 // 设备详情的描述 12 // 设备详情的描述
9 -export const descSchema: DescItem[] = [  
10 - {  
11 - field: 'createTime',  
12 - label: '创建时间',  
13 - },  
14 - {  
15 - field: 'name',  
16 - label: '设备名称',  
17 - },  
18 - {  
19 - field: 'label',  
20 - label: '设备标签',  
21 - },  
22 - {  
23 - field: 'deviceProfile.name',  
24 - label: '产品',  
25 - },  
26 - {  
27 - field: 'gatewayName',  
28 - label: '所属网关',  
29 - show: (data) => !!data.gatewayName,  
30 - },  
31 - {  
32 - field: 'deviceType',  
33 - label: '设备类型',  
34 - render: (text) => {  
35 - return text === DeviceTypeEnum.GATEWAY  
36 - ? '网关设备'  
37 - : text == DeviceTypeEnum.DIRECT_CONNECTION  
38 - ? '直连设备'  
39 - : '网关子设备'; 13 +export const descSchema = (emit: EmitType): DescItem[] => {
  14 + return [
  15 + {
  16 + field: 'createTime',
  17 + label: '创建时间',
40 }, 18 },
41 - },  
42 - {  
43 - field: 'description',  
44 - label: '描述',  
45 - span: 2,  
46 - },  
47 -]; 19 + {
  20 + field: 'name',
  21 + label: '设备名称',
  22 + },
  23 + {
  24 + field: 'label',
  25 + label: '设备标签',
  26 + },
  27 + {
  28 + field: 'deviceProfile.name',
  29 + label: '产品',
  30 + render(val, data) {
  31 + if (TypeEnum.SENSOR !== data.deviceType) return val;
  32 + return h(
  33 + Button,
  34 + { type: 'link', style: { padding: 0 }, onClick: () => emit('open-gateway-device', data) },
  35 + { default: () => val }
  36 + );
  37 + },
  38 + },
  39 + {
  40 + field: 'gatewayName',
  41 + label: '所属网关',
  42 + show: (data) => !!data.gatewayName,
  43 + },
  44 + {
  45 + field: 'deviceType',
  46 + label: '设备类型',
  47 + render: (text) => {
  48 + return text === DeviceTypeEnum.GATEWAY
  49 + ? '网关设备'
  50 + : text == DeviceTypeEnum.DIRECT_CONNECTION
  51 + ? '直连设备'
  52 + : '网关子设备';
  53 + },
  54 + },
  55 + {
  56 + field: 'description',
  57 + label: '描述',
  58 + // span: 2,
  59 + },
  60 + ];
  61 +};
48 62
49 // 实时数据表格 63 // 实时数据表格
50 export const realTimeDataColumns: BasicColumn[] = [ 64 export const realTimeDataColumns: BasicColumn[] = [
@@ -10,7 +10,11 @@ @@ -10,7 +10,11 @@
10 > 10 >
11 <Tabs v-model:activeKey="activeKey" :size="size"> 11 <Tabs v-model:activeKey="activeKey" :size="size">
12 <TabPane key="1" tab="详情"> 12 <TabPane key="1" tab="详情">
13 - <Detail ref="deviceDetailRef" :deviceDetail="deviceDetail" /> 13 + <Detail
  14 + ref="deviceDetailRef"
  15 + :deviceDetail="deviceDetail"
  16 + @open-gateway-device="handleOpenGatewayDevice"
  17 + />
14 </TabPane> 18 </TabPane>
15 <TabPane key="2" tab="实时数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'"> 19 <TabPane key="2" tab="实时数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'">
16 <RealTimeData :deviceDetail="deviceDetail" /> 20 <RealTimeData :deviceDetail="deviceDetail" />
@@ -67,7 +71,7 @@ @@ -67,7 +71,7 @@
67 TBoxDetail, 71 TBoxDetail,
68 HistoryData, 72 HistoryData,
69 }, 73 },
70 - emits: ['reload', 'register', 'openTbDeviceDetail'], 74 + emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'],
71 setup(_props, { emit }) { 75 setup(_props, { emit }) {
72 const activeKey = ref('1'); 76 const activeKey = ref('1');
73 const size = ref('small'); 77 const size = ref('small');
@@ -92,6 +96,10 @@ @@ -92,6 +96,10 @@
92 const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => { 96 const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => {
93 emit('openTbDeviceDetail', data); 97 emit('openTbDeviceDetail', data);
94 }; 98 };
  99 +
  100 + const handleOpenGatewayDevice = (data: { gatewayId: string; tbDeviceId: string }) => {
  101 + emit('openGatewayDeviceDetail', { id: data.gatewayId });
  102 + };
95 return { 103 return {
96 size, 104 size,
97 activeKey, 105 activeKey,
@@ -101,6 +109,7 @@ @@ -101,6 +109,7 @@
101 deviceDetailRef, 109 deviceDetailRef,
102 tbDeviceId, 110 tbDeviceId,
103 handleOpenTbDeviceDetail, 111 handleOpenTbDeviceDetail,
  112 + handleOpenGatewayDevice,
104 }; 113 };
105 }, 114 },
106 }); 115 });
@@ -92,10 +92,11 @@ @@ -92,10 +92,11 @@
92 required: true, 92 required: true,
93 }, 93 },
94 }, 94 },
95 - setup(props) { 95 + emits: ['open-gateway-device'],
  96 + setup(props, { emit }) {
96 const [register] = useDescription({ 97 const [register] = useDescription({
97 layout: 'vertical', 98 layout: 'vertical',
98 - schema: descSchema, 99 + schema: descSchema(emit),
99 column: 2, 100 column: 2,
100 }); 101 });
101 102
@@ -152,9 +152,12 @@ @@ -152,9 +152,12 @@
152 <DeviceDetailDrawer 152 <DeviceDetailDrawer
153 @register="registerDetailDrawer" 153 @register="registerDetailDrawer"
154 @open-tb-device-detail="handleOpenTbDeviceDetail" 154 @open-tb-device-detail="handleOpenTbDeviceDetail"
  155 + @open-gateway-device-detail="handleOpenGatewayDetail"
155 /> 156 />
156 <DeviceDetailDrawer @register="registerTbDetailDrawer" /> 157 <DeviceDetailDrawer @register="registerTbDetailDrawer" />
157 158
  159 + <DeviceDetailDrawer @register="registerGatewayDetailDrawer" />
  160 +
158 <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" /> 161 <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" />
159 <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> 162 <CustomerModal @register="registerCustomerModal" @reload="handleReload" />
160 </PageWrapper> 163 </PageWrapper>
@@ -218,6 +221,7 @@ @@ -218,6 +221,7 @@
218 const [registerCustomerModal, { openModal: openCustomerModal }] = useModal(); 221 const [registerCustomerModal, { openModal: openCustomerModal }] = useModal();
219 const [registerDetailDrawer, { openDrawer }] = useDrawer(); 222 const [registerDetailDrawer, { openDrawer }] = useDrawer();
220 const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer(); 223 const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer();
  224 + const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer();
221 225
222 const [registerTable, { reload, setSelectedRowKeys, setProps }] = useTable({ 226 const [registerTable, { reload, setSelectedRowKeys, setProps }] = useTable({
223 title: '设备列表', 227 title: '设备列表',
@@ -329,6 +333,10 @@ @@ -329,6 +333,10 @@
329 openTbDeviceDrawer(true, data); 333 openTbDeviceDrawer(true, data);
330 }; 334 };
331 335
  336 + const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => {
  337 + openGatewayDetailDrawer(true, data);
  338 + };
  339 +
332 return { 340 return {
333 registerTable, 341 registerTable,
334 handleCreate, 342 handleCreate,
@@ -354,6 +362,8 @@ @@ -354,6 +362,8 @@
354 handleReload, 362 handleReload,
355 registerTbDetailDrawer, 363 registerTbDetailDrawer,
356 handleOpenTbDeviceDetail, 364 handleOpenTbDeviceDetail,
  365 + handleOpenGatewayDetail,
  366 + registerGatewayDetailDrawer,
357 }; 367 };
358 }, 368 },
359 }); 369 });
1 <template> 1 <template>
2 <BasicDrawer v-bind="$attrs" title="产品详情" @register="register" width="50%"> 2 <BasicDrawer v-bind="$attrs" title="产品详情" @register="register" width="50%">
3 - <Tabs :animated="true" v-model:activeKey="activeKey">  
4 - <TabPane forceRender key="1" tab="产品"> 3 + <Tabs :animated="true" v-model:activeKey="activeKey" @change="handlePanelChange">
  4 + <TabPane forceRender key="product" tab="产品">
5 <div class="relative"> 5 <div class="relative">
6 <DeviceConfigurationStep :ifShowBtn="false" ref="DevConStRef" /> 6 <DeviceConfigurationStep :ifShowBtn="false" ref="DevConStRef" />
7 <div class="absolute w-full h-full top-0 cursor-not-allowed"></div> 7 <div class="absolute w-full h-full top-0 cursor-not-allowed"></div>
8 </div> 8 </div>
9 </TabPane> 9 </TabPane>
10 - <TabPane forceRender key="2" tab="传输配置"> 10 + <TabPane forceRender key="transport" tab="传输配置">
11 <div class="relative"> 11 <div class="relative">
12 <TransportConfigurationStep :ifShowBtn="false" ref="TransConStRef" /> 12 <TransportConfigurationStep :ifShowBtn="false" ref="TransConStRef" />
13 <div class="absolute w-full h-full top-0 cursor-not-allowed"></div> 13 <div class="absolute w-full h-full top-0 cursor-not-allowed"></div>
14 </div> 14 </div>
15 </TabPane> 15 </TabPane>
16 - <TabPane forceRender key="3" tab="物模型管理"> 16 + <TabPane forceRender key="modelOfMatter" tab="物模型管理">
17 <PhysicalModelManagementStep /> 17 <PhysicalModelManagementStep />
18 </TabPane> 18 </TabPane>
19 </Tabs> 19 </Tabs>
@@ -30,7 +30,11 @@ @@ -30,7 +30,11 @@
30 30
31 defineEmits(['register']); 31 defineEmits(['register']);
32 32
33 - const activeKey = ref('1'); 33 + type ActiveKey = 'product' | 'transport' | 'modelOfMatter';
  34 +
  35 + const activeKey = ref<ActiveKey>('product');
  36 +
  37 + const record = ref<Recordable>({});
34 38
35 const DevConStRef = ref<InstanceType<typeof DeviceConfigurationStep>>(); 39 const DevConStRef = ref<InstanceType<typeof DeviceConfigurationStep>>();
36 const TransConStRef = ref<InstanceType<typeof TransportConfigurationStep>>(); 40 const TransConStRef = ref<InstanceType<typeof TransportConfigurationStep>>();
@@ -44,11 +48,14 @@ @@ -44,11 +48,14 @@
44 }; 48 };
45 49
46 const [register, {}] = useDrawerInner(async (data: Recordable) => { 50 const [register, {}] = useDrawerInner(async (data: Recordable) => {
47 - activeKey.value = '1';  
48 - const res = await deviceConfigGetDetail(data.record.id);  
49 - setDeviceConfFormData(res);  
50 - setTransConfFormData(res); 51 + activeKey.value = 'product';
  52 + record.value = await deviceConfigGetDetail(data.record.id);
  53 + setDeviceConfFormData(unref(record));
51 }); 54 });
  55 +
  56 + const handlePanelChange = (activeKey: ActiveKey) => {
  57 + if (activeKey === 'transport') setTransConfFormData(unref(record));
  58 + };
52 </script> 59 </script>
53 60
54 <style lang="less" scope></style> 61 <style lang="less" scope></style>