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,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", |
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,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> |