Commit c2819b0f692e6a20dcafbdf60bb7981fd2bf98c1
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
Showing
18 changed files
with
279 additions
and
247 deletions
.vscode/settings.json
deleted
100644 → 0
... | ... | @@ -63,6 +63,7 @@ |
63 | 63 | "sortablejs": "^1.14.0", |
64 | 64 | "tinymce": "^5.8.2", |
65 | 65 | "vditor": "^3.8.6", |
66 | + "video.js": "^7.20.3", | |
66 | 67 | "vue": "^3.2.31", |
67 | 68 | "vue-i18n": "9.1.7", |
68 | 69 | "vue-json-pretty": "^2.0.4", |
... | ... | @@ -90,6 +91,7 @@ |
90 | 91 | "@types/qrcode": "^1.4.1", |
91 | 92 | "@types/qs": "^6.9.7", |
92 | 93 | "@types/sortablejs": "^1.10.7", |
94 | + "@types/video.js": "^7.3.49", | |
93 | 95 | "@typescript-eslint/eslint-plugin": "^4.29.1", |
94 | 96 | "@typescript-eslint/parser": "^4.29.1", |
95 | 97 | "@vitejs/plugin-legacy": "^1.5.1", | ... | ... |
src/components/Video/index.ts
0 → 100644
src/components/Video/src/VideoPlay.vue
0 → 100644
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> | ... | ... |
src/components/Video/src/utils.ts
0 → 100644
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 | 383 | required: true, |
384 | 384 | validator: (_, value: string) => { |
385 | 385 | const reg = /(?:.*)(?<=\.)/; |
386 | - const type = value.replace(reg, ''); | |
386 | + const type = (value || '').replace(reg, ''); | |
387 | 387 | if (type !== MediaType.M3U8) { |
388 | 388 | return Promise.reject('视频流只支持m3u8格式'); |
389 | 389 | } | ... | ... |
... | ... | @@ -3,117 +3,63 @@ |
3 | 3 | <BasicModal |
4 | 4 | v-bind="$attrs" |
5 | 5 | width="55rem" |
6 | + destroyOnClose | |
6 | 7 | :height="heightNum" |
7 | 8 | @register="register" |
8 | 9 | title="视频预览" |
9 | - @cancel="handleCancel" | |
10 | 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 | 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 | 16 | </BasicModal> |
29 | 17 | </div> |
30 | 18 | </template> |
31 | 19 | <script setup lang="ts"> |
32 | - import { ref, nextTick, reactive } from 'vue'; | |
20 | + import { ref, reactive } from 'vue'; | |
33 | 21 | import { BasicModal, useModalInner } from '/@/components/Modal'; |
34 | 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 | 25 | import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; |
26 | + import { VideoJsPlayerOptions } from 'video.js'; | |
39 | 27 | |
40 | 28 | const heightNum = ref(800); |
41 | 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 | 45 | const [register] = useModalInner( |
80 | 46 | async (data: { record: CameraModel | StreamingManageRecord }) => { |
81 | - let reg = /(?:.*)(?<=\.)/; | |
82 | 47 | const { record } = data; |
83 | 48 | if (record.accessMode === AccessMode.ManuallyEnter) { |
84 | 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 | 52 | } else { |
92 | 53 | try { |
93 | 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 | 62 | const handleCancel = () => { |
107 | - //关闭暂停播放视频 | |
108 | - options.src = ''; | |
109 | - video.value.pause(); | |
63 | + showVideo.value = false; | |
110 | 64 | }; |
111 | 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 | 5 | import { Spin, Button, Pagination, Space, List } from 'ant-design-vue'; |
6 | 6 | import { cameraPage } from '/@/api/camera/cameraManager'; |
7 | 7 | import { CameraRecord } from '/@/api/camera/model/cameraModel'; |
8 | - import { videoPlay as VideoPlay } from 'vue3-video-play'; | |
9 | 8 | import 'vue3-video-play/dist/style.css'; |
10 | 9 | import { useFullscreen } from '@vueuse/core'; |
11 | 10 | import CameraDrawer from './CameraDrawer.vue'; |
12 | 11 | import { useDrawer } from '/@/components/Drawer'; |
13 | - import { AccessMode, MediaType, PageMode } from './config.data'; | |
12 | + import { AccessMode, PageMode } from './config.data'; | |
14 | 13 | import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; |
15 | - import { isDef } from '/@/utils/is'; | |
16 | 14 | import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; |
17 | 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 | 20 | type CameraRecordItem = CameraRecord & { |
20 | 21 | canPlay?: boolean; |
21 | - type?: string; | |
22 | 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 | 32 | const emit = defineEmits(['switchMode']); |
... | ... | @@ -36,32 +43,6 @@ |
36 | 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 | 47 | const handleSelect = (orgId: string) => { |
67 | 48 | organizationId.value = orgId; |
... | ... | @@ -80,7 +61,6 @@ |
80 | 61 | pagination.total = total; |
81 | 62 | |
82 | 63 | for (const item of items) { |
83 | - // await beforeVideoPlay(item); | |
84 | 64 | (item as CameraRecordItem).isTransform = false; |
85 | 65 | beforeVideoPlay(item); |
86 | 66 | } |
... | ... | @@ -97,30 +77,39 @@ |
97 | 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 | 81 | const beforeVideoPlay = async (record: CameraRecordItem) => { |
105 | - let reg = /(?:.*)(?<=\.)/; | |
106 | 82 | if (record.accessMode === AccessMode.ManuallyEnter) { |
107 | 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 | 93 | record.isTransform = true; |
111 | 94 | } |
112 | 95 | } |
113 | 96 | if (record.accessMode === AccessMode.Streaming) { |
114 | 97 | try { |
115 | 98 | const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); |
116 | - const type = url.replace(reg, ''); | |
117 | 99 | const index = unref(cameraList).findIndex((item) => item.id === record.id); |
118 | 100 | if (~index) { |
119 | 101 | const oldRecord = unref(cameraList).at(index)!; |
120 | 102 | unref(cameraList)[index] = { |
121 | 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 | 113 | isTransform: true, |
125 | 114 | }; |
126 | 115 | } |
... | ... | @@ -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 | 152 | const [registerDrawer, { openDrawer }] = useDrawer(); |
178 | 153 | |
179 | 154 | const handleAddCamera = () => { |
... | ... | @@ -185,13 +160,26 @@ |
185 | 160 | onMounted(() => { |
186 | 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 | 176 | </script> |
189 | 177 | |
190 | 178 | <template> |
191 | 179 | <div> |
192 | 180 | <PageWrapper dense contentFullHeight contentClass="flex"> |
193 | 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 | 183 | <div class="p-3 bg-light-50 flex justify-between mb-4 dark:bg-dark-900"> |
196 | 184 | <div class="flex gap-4 cursor-pointer items-center"> |
197 | 185 | <div |
... | ... | @@ -247,6 +235,7 @@ |
247 | 235 | </div> |
248 | 236 | <section ref="videoContainer" class="flex-auto"> |
249 | 237 | <List |
238 | + ref="listEl" | |
250 | 239 | :loading="loading" |
251 | 240 | :data-source="cameraList" |
252 | 241 | class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list" |
... | ... | @@ -264,24 +253,12 @@ |
264 | 253 | v-if="!item.placeholder" |
265 | 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 | 262 | <div |
286 | 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 | 264 | style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" |
... | ... | @@ -313,24 +290,6 @@ |
313 | 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 | 293 | .video-container { |
335 | 294 | .video-container-mask { |
336 | 295 | opacity: 0; |
... | ... | @@ -344,8 +303,6 @@ |
344 | 303 | } |
345 | 304 | |
346 | 305 | .video-container-error-msk { |
347 | - // opacity: 0; | |
348 | - // visibility: hidden; | |
349 | 306 | color: #000; |
350 | 307 | } |
351 | 308 | } |
... | ... | @@ -363,7 +320,6 @@ |
363 | 320 | |
364 | 321 | .split-mode-list:deep(.ant-col) { |
365 | 322 | width: 100%; |
366 | - // height: var(--height); | |
367 | 323 | height: 100%; |
368 | 324 | } |
369 | 325 | </style> | ... | ... |
... | ... | @@ -173,8 +173,8 @@ export const formSchema: QFormSchema[] = [ |
173 | 173 | { |
174 | 174 | field: 'videoUrl', |
175 | 175 | label: '视频流', |
176 | - required: true, | |
177 | 176 | component: 'Input', |
177 | + required: true, | |
178 | 178 | ifShow({ values }) { |
179 | 179 | return values.accessMode === AccessMode.ManuallyEnter; |
180 | 180 | }, |
... | ... | @@ -182,7 +182,7 @@ export const formSchema: QFormSchema[] = [ |
182 | 182 | placeholder: '请输入视频流', |
183 | 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 | 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 | 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 | 12 | </div> |
7 | 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 | 15 | <BasicTree |
14 | 16 | title="组织列表" |
15 | 17 | toolbar | ... | ... |
... | ... | @@ -161,7 +161,7 @@ |
161 | 161 | <template> |
162 | 162 | <PageWrapper dense contentFullHeight contentClass="flex"> |
163 | 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 | 165 | <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4"> |
166 | 166 | <BasicForm @register="registerForm" /> |
167 | 167 | </div> | ... | ... |
... | ... | @@ -5,46 +5,60 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
5 | 5 | import { getCustomerList } from '/@/api/device/deviceManager'; |
6 | 6 | import { DescItem } from '/@/components/Description/index'; |
7 | 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 | 64 | export const realTimeDataColumns: BasicColumn[] = [ | ... | ... |
... | ... | @@ -10,7 +10,11 @@ |
10 | 10 | > |
11 | 11 | <Tabs v-model:activeKey="activeKey" :size="size"> |
12 | 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 | 18 | </TabPane> |
15 | 19 | <TabPane key="2" tab="实时数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'"> |
16 | 20 | <RealTimeData :deviceDetail="deviceDetail" /> |
... | ... | @@ -67,7 +71,7 @@ |
67 | 71 | TBoxDetail, |
68 | 72 | HistoryData, |
69 | 73 | }, |
70 | - emits: ['reload', 'register', 'openTbDeviceDetail'], | |
74 | + emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], | |
71 | 75 | setup(_props, { emit }) { |
72 | 76 | const activeKey = ref('1'); |
73 | 77 | const size = ref('small'); |
... | ... | @@ -92,6 +96,10 @@ |
92 | 96 | const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => { |
93 | 97 | emit('openTbDeviceDetail', data); |
94 | 98 | }; |
99 | + | |
100 | + const handleOpenGatewayDevice = (data: { gatewayId: string; tbDeviceId: string }) => { | |
101 | + emit('openGatewayDeviceDetail', { id: data.gatewayId }); | |
102 | + }; | |
95 | 103 | return { |
96 | 104 | size, |
97 | 105 | activeKey, |
... | ... | @@ -101,6 +109,7 @@ |
101 | 109 | deviceDetailRef, |
102 | 110 | tbDeviceId, |
103 | 111 | handleOpenTbDeviceDetail, |
112 | + handleOpenGatewayDevice, | |
104 | 113 | }; |
105 | 114 | }, |
106 | 115 | }); | ... | ... |
... | ... | @@ -92,10 +92,11 @@ |
92 | 92 | required: true, |
93 | 93 | }, |
94 | 94 | }, |
95 | - setup(props) { | |
95 | + emits: ['open-gateway-device'], | |
96 | + setup(props, { emit }) { | |
96 | 97 | const [register] = useDescription({ |
97 | 98 | layout: 'vertical', |
98 | - schema: descSchema, | |
99 | + schema: descSchema(emit), | |
99 | 100 | column: 2, |
100 | 101 | }); |
101 | 102 | ... | ... |
... | ... | @@ -152,9 +152,12 @@ |
152 | 152 | <DeviceDetailDrawer |
153 | 153 | @register="registerDetailDrawer" |
154 | 154 | @open-tb-device-detail="handleOpenTbDeviceDetail" |
155 | + @open-gateway-device-detail="handleOpenGatewayDetail" | |
155 | 156 | /> |
156 | 157 | <DeviceDetailDrawer @register="registerTbDetailDrawer" /> |
157 | 158 | |
159 | + <DeviceDetailDrawer @register="registerGatewayDetailDrawer" /> | |
160 | + | |
158 | 161 | <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" /> |
159 | 162 | <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> |
160 | 163 | </PageWrapper> |
... | ... | @@ -218,6 +221,7 @@ |
218 | 221 | const [registerCustomerModal, { openModal: openCustomerModal }] = useModal(); |
219 | 222 | const [registerDetailDrawer, { openDrawer }] = useDrawer(); |
220 | 223 | const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer(); |
224 | + const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer(); | |
221 | 225 | |
222 | 226 | const [registerTable, { reload, setSelectedRowKeys, setProps }] = useTable({ |
223 | 227 | title: '设备列表', |
... | ... | @@ -329,6 +333,10 @@ |
329 | 333 | openTbDeviceDrawer(true, data); |
330 | 334 | }; |
331 | 335 | |
336 | + const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => { | |
337 | + openGatewayDetailDrawer(true, data); | |
338 | + }; | |
339 | + | |
332 | 340 | return { |
333 | 341 | registerTable, |
334 | 342 | handleCreate, |
... | ... | @@ -354,6 +362,8 @@ |
354 | 362 | handleReload, |
355 | 363 | registerTbDetailDrawer, |
356 | 364 | handleOpenTbDeviceDetail, |
365 | + handleOpenGatewayDetail, | |
366 | + registerGatewayDetailDrawer, | |
357 | 367 | }; |
358 | 368 | }, |
359 | 369 | }); | ... | ... |
1 | 1 | <template> |
2 | 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 | 5 | <div class="relative"> |
6 | 6 | <DeviceConfigurationStep :ifShowBtn="false" ref="DevConStRef" /> |
7 | 7 | <div class="absolute w-full h-full top-0 cursor-not-allowed"></div> |
8 | 8 | </div> |
9 | 9 | </TabPane> |
10 | - <TabPane forceRender key="2" tab="传输配置"> | |
10 | + <TabPane forceRender key="transport" tab="传输配置"> | |
11 | 11 | <div class="relative"> |
12 | 12 | <TransportConfigurationStep :ifShowBtn="false" ref="TransConStRef" /> |
13 | 13 | <div class="absolute w-full h-full top-0 cursor-not-allowed"></div> |
14 | 14 | </div> |
15 | 15 | </TabPane> |
16 | - <TabPane forceRender key="3" tab="物模型管理"> | |
16 | + <TabPane forceRender key="modelOfMatter" tab="物模型管理"> | |
17 | 17 | <PhysicalModelManagementStep /> |
18 | 18 | </TabPane> |
19 | 19 | </Tabs> |
... | ... | @@ -30,7 +30,11 @@ |
30 | 30 | |
31 | 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 | 39 | const DevConStRef = ref<InstanceType<typeof DeviceConfigurationStep>>(); |
36 | 40 | const TransConStRef = ref<InstanceType<typeof TransportConfigurationStep>>(); |
... | ... | @@ -44,11 +48,14 @@ |
44 | 48 | }; |
45 | 49 | |
46 | 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 | 59 | </script> |
53 | 60 | |
54 | 61 | <style lang="less" scope></style> | ... | ... |