Commit 139ee0766487ccfc7476083ab6d15e572c15e651

Authored by fengtao
2 parents 24d0462e 17655a6f

Merge branch 'main' into ft_local_dev

1 { 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 - ] 2 + "i18n-ally.localesPaths": ["src/locales", "src/locales/lang", "public/resource/tinymce/langs"]
12 } 3 }
1 -import { Plugin } from 'vite';  
2 -  
3 -export function dropConsoleInVue3VideoPlayPlugin(params?: { enabled: boolean }) {  
4 - const { enabled = true } = params || {};  
5 - return {  
6 - name: 'drop-console-in-vue3-video-play-plugin',  
7 - transform(src, id) {  
8 - if (enabled) {  
9 - if (id.includes('vue3-video-play')) {  
10 - return {  
11 - code: src.replaceAll('console.log', ''),  
12 - };  
13 - }  
14 - }  
15 - },  
16 - } as Plugin;  
17 -}  
@@ -16,7 +16,6 @@ import { configThemePlugin } from './theme'; @@ -16,7 +16,6 @@ import { configThemePlugin } from './theme';
16 import { configImageminPlugin } from './imagemin'; 16 import { configImageminPlugin } from './imagemin';
17 import { configSvgIconsPlugin } from './svgSprite'; 17 import { configSvgIconsPlugin } from './svgSprite';
18 import { configHmrPlugin } from './hmr'; 18 import { configHmrPlugin } from './hmr';
19 -import { dropConsoleInVue3VideoPlayPlugin } from './dropConsoleInVue3VideoPlay';  
20 19
21 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) { 20 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
22 const { 21 const {
@@ -64,8 +63,6 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) { @@ -64,8 +63,6 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
64 //vite-plugin-theme 63 //vite-plugin-theme
65 vitePlugins.push(configThemePlugin(isBuild)); 64 vitePlugins.push(configThemePlugin(isBuild));
66 65
67 - vitePlugins.push(dropConsoleInVue3VideoPlayPlugin());  
68 -  
69 // The following plugins only work in the production environment 66 // The following plugins only work in the production environment
70 if (isBuild) { 67 if (isBuild) {
71 //vite-plugin-imagemin 68 //vite-plugin-imagemin
@@ -63,13 +63,13 @@ @@ -63,13 +63,13 @@
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",
69 "vue-router": "^4.0.11", 70 "vue-router": "^4.0.11",
70 "vue-types": "^4.0.3", 71 "vue-types": "^4.0.3",
71 "vue3-grid-layout": "^1.0.0", 72 "vue3-grid-layout": "^1.0.0",
72 - "vue3-video-play": "^1.3.1-beta.4",  
73 "xlsx": "^0.17.0" 73 "xlsx": "^0.17.0"
74 }, 74 },
75 "devDependencies": { 75 "devDependencies": {
@@ -90,6 +90,7 @@ @@ -90,6 +90,7 @@
90 "@types/qrcode": "^1.4.1", 90 "@types/qrcode": "^1.4.1",
91 "@types/qs": "^6.9.7", 91 "@types/qs": "^6.9.7",
92 "@types/sortablejs": "^1.10.7", 92 "@types/sortablejs": "^1.10.7",
  93 + "@types/video.js": "^7.3.49",
93 "@typescript-eslint/eslint-plugin": "^4.29.1", 94 "@typescript-eslint/eslint-plugin": "^4.29.1",
94 "@typescript-eslint/parser": "^4.29.1", 95 "@typescript-eslint/parser": "^4.29.1",
95 "@vitejs/plugin-legacy": "^1.5.1", 96 "@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,27 @@ @@ -5,21 +5,27 @@
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';  
10 import { useFullscreen } from '@vueuse/core'; 8 import { useFullscreen } from '@vueuse/core';
11 import CameraDrawer from './CameraDrawer.vue'; 9 import CameraDrawer from './CameraDrawer.vue';
12 import { useDrawer } from '/@/components/Drawer'; 10 import { useDrawer } from '/@/components/Drawer';
13 - import { AccessMode, MediaType, PageMode } from './config.data'; 11 + import { AccessMode, PageMode } from './config.data';
14 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; 12 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
15 - import { isDef } from '/@/utils/is';  
16 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; 13 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
17 import { buildUUID } from '/@/utils/uuid'; 14 import { buildUUID } from '/@/utils/uuid';
  15 + import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
  16 + import { VideoJsPlayerOptions } from 'video.js';
  17 + import { getBoundingClientRect } from '/@/utils/domUtils';
18 18
19 type CameraRecordItem = CameraRecord & { 19 type CameraRecordItem = CameraRecord & {
20 canPlay?: boolean; 20 canPlay?: boolean;
21 - type?: string;  
22 isTransform?: boolean; 21 isTransform?: boolean;
  22 + videoPlayerOptions?: VideoJsPlayerOptions;
  23 + };
  24 +
  25 + const basicVideoPlayOptions: VideoJsPlayerOptions = {
  26 + width: '100%' as unknown as number,
  27 + height: '100%' as unknown as number,
  28 + autoplay: true,
23 }; 29 };
24 30
25 const emit = defineEmits(['switchMode']); 31 const emit = defineEmits(['switchMode']);
@@ -36,32 +42,6 @@ @@ -36,32 +42,6 @@
36 total: 0, 42 total: 0,
37 }); 43 });
38 44
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 // 树形选择器 45 // 树形选择器
66 const handleSelect = (orgId: string) => { 46 const handleSelect = (orgId: string) => {
67 organizationId.value = orgId; 47 organizationId.value = orgId;
@@ -80,7 +60,6 @@ @@ -80,7 +60,6 @@
80 pagination.total = total; 60 pagination.total = total;
81 61
82 for (const item of items) { 62 for (const item of items) {
83 - // await beforeVideoPlay(item);  
84 (item as CameraRecordItem).isTransform = false; 63 (item as CameraRecordItem).isTransform = false;
85 beforeVideoPlay(item); 64 beforeVideoPlay(item);
86 } 65 }
@@ -97,30 +76,39 @@ @@ -97,30 +76,39 @@
97 loading.value = false; 76 loading.value = false;
98 } 77 }
99 }; 78 };
100 - const getMediaType = (suffix: string) => {  
101 - return suffix === MediaType.M3U8 ? suffix : `video/${suffix}`;  
102 - };  
103 79
104 const beforeVideoPlay = async (record: CameraRecordItem) => { 80 const beforeVideoPlay = async (record: CameraRecordItem) => {
105 - let reg = /(?:.*)(?<=\.)/;  
106 if (record.accessMode === AccessMode.ManuallyEnter) { 81 if (record.accessMode === AccessMode.ManuallyEnter) {
107 if (record.videoUrl) { 82 if (record.videoUrl) {
108 - const type = record.videoUrl.replace(reg, '');  
109 - record.type = getMediaType(type); 83 + (record as CameraRecordItem).videoPlayerOptions = {
  84 + ...basicVideoPlayOptions,
  85 + sources: [
  86 + {
  87 + src: record.videoUrl,
  88 + type: getVideoTypeByUrl(record.videoUrl),
  89 + },
  90 + ],
  91 + };
110 record.isTransform = true; 92 record.isTransform = true;
111 } 93 }
112 } 94 }
113 if (record.accessMode === AccessMode.Streaming) { 95 if (record.accessMode === AccessMode.Streaming) {
114 try { 96 try {
115 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); 97 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); 98 const index = unref(cameraList).findIndex((item) => item.id === record.id);
118 if (~index) { 99 if (~index) {
119 const oldRecord = unref(cameraList).at(index)!; 100 const oldRecord = unref(cameraList).at(index)!;
120 unref(cameraList)[index] = { 101 unref(cameraList)[index] = {
121 ...oldRecord, 102 ...oldRecord,
122 - videoUrl: url,  
123 - type: getMediaType(type), 103 + videoPlayerOptions: {
  104 + ...basicVideoPlayOptions,
  105 + sources: [
  106 + {
  107 + src: url,
  108 + type: getVideoTypeByUrl(url),
  109 + },
  110 + ],
  111 + },
124 isTransform: true, 112 isTransform: true,
125 }; 113 };
126 } 114 }
@@ -160,20 +148,6 @@ @@ -160,20 +148,6 @@
160 } 148 }
161 }; 149 };
162 150
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(); 151 const [registerDrawer, { openDrawer }] = useDrawer();
178 152
179 const handleAddCamera = () => { 153 const handleAddCamera = () => {
@@ -185,13 +159,26 @@ @@ -185,13 +159,26 @@
185 onMounted(() => { 159 onMounted(() => {
186 getCameraList(); 160 getCameraList();
187 }); 161 });
  162 +
  163 + const listEl = ref();
  164 + onMounted(() => {
  165 + const clientHeight = document.documentElement.clientHeight;
  166 + const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect;
  167 + // list pading top 8 maring-top 8 extra slot 56
  168 + const listContainerMarginBottom = 16;
  169 + const listContainerHeight = clientHeight - rect.top - listContainerMarginBottom;
  170 + const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector(
  171 + '.ant-spin-container'
  172 + ) as HTMLElement;
  173 + listContainerEl && (listContainerEl.style.height = listContainerHeight + 'px');
  174 + });
188 </script> 175 </script>
189 176
190 <template> 177 <template>
191 <div> 178 <div>
192 <PageWrapper dense contentFullHeight contentClass="flex"> 179 <PageWrapper dense contentFullHeight contentClass="flex">
193 <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> 180 <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"> 181 + <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"> 182 <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"> 183 <div class="flex gap-4 cursor-pointer items-center">
197 <div 184 <div
@@ -247,6 +234,7 @@ @@ -247,6 +234,7 @@
247 </div> 234 </div>
248 <section ref="videoContainer" class="flex-auto"> 235 <section ref="videoContainer" class="flex-auto">
249 <List 236 <List
  237 + ref="listEl"
250 :loading="loading" 238 :loading="loading"
251 :data-source="cameraList" 239 :data-source="cameraList"
252 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list" 240 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list"
@@ -264,24 +252,12 @@ @@ -264,24 +252,12 @@
264 v-if="!item.placeholder" 252 v-if="!item.placeholder"
265 class="bg-black w-full h-full overflow-hidden relative video-container" 253 class="bg-black w-full h-full overflow-hidden relative video-container"
266 > 254 >
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" 255 + <Spin
  256 + class="!absolute top-1/2 left-1/2 transform -translate-1/2"
  257 + v-show="!item.isTransform"
  258 + :spinning="!item.isTransform"
278 /> 259 />
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> 260 + <BasicVideoPlay v-if="item.isTransform" :options="item.videoPlayerOptions" />
285 <div 261 <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" 262 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)" 263 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
@@ -313,24 +289,6 @@ @@ -313,24 +289,6 @@
313 height: 100%; 289 height: 100%;
314 } 290 }
315 291
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 { 292 .video-container {
335 .video-container-mask { 293 .video-container-mask {
336 opacity: 0; 294 opacity: 0;
@@ -344,8 +302,6 @@ @@ -344,8 +302,6 @@
344 } 302 }
345 303
346 .video-container-error-msk { 304 .video-container-error-msk {
347 - // opacity: 0;  
348 - // visibility: hidden;  
349 color: #000; 305 color: #000;
350 } 306 }
351 } 307 }
@@ -363,7 +319,6 @@ @@ -363,7 +319,6 @@
363 319
364 .split-mode-list:deep(.ant-col) { 320 .split-mode-list:deep(.ant-col) {
365 width: 100%; 321 width: 100%;
366 - // height: var(--height);  
367 height: 100%; 322 height: 100%;
368 } 323 }
369 </style> 324 </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;
@@ -688,9 +689,39 @@ export const CommandSchemas: FormSchema[] = [ @@ -688,9 +689,39 @@ export const CommandSchemas: FormSchema[] = [
688 }, 689 },
689 }, 690 },
690 { 691 {
  692 + field: 'valueType',
  693 + label: '命令类型',
  694 + component: 'RadioGroup',
  695 + defaultValue: 'json',
  696 + componentProps: () => {
  697 + return {
  698 + options: [
  699 + { label: 'JSON', value: 'json' },
  700 + { label: '字符串', value: 'string' },
  701 + ],
  702 + };
  703 + },
  704 + },
  705 + {
  706 + field: 'commandText',
  707 + label: '请输入命令内容',
  708 + ifShow: ({ model }) => {
  709 + return model['valueType'] === 'string';
  710 + },
  711 + component: 'InputTextArea',
  712 + componentProps: {
  713 + autosize: {
  714 + minRows: 6,
  715 + },
  716 + },
  717 + },
  718 + {
691 field: 'commandValue', 719 field: 'commandValue',
692 label: '请输入命令内容', 720 label: '请输入命令内容',
693 slot: 'commandSlot', 721 slot: 'commandSlot',
694 component: 'InputTextArea', 722 component: 'InputTextArea',
  723 + show: ({ model }) => {
  724 + return model['valueType'] === 'json';
  725 + },
695 }, 726 },
696 ]; 727 ];
@@ -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 });
@@ -30,6 +30,14 @@ @@ -30,6 +30,14 @@
30 import { QuestionCircleOutlined } from '@ant-design/icons-vue'; 30 import { QuestionCircleOutlined } from '@ant-design/icons-vue';
31 import { Tooltip } from 'ant-design-vue'; 31 import { Tooltip } from 'ant-design-vue';
32 32
  33 + interface CommandParams {
  34 + additionalInfo: Recordable;
  35 + cmdType: string;
  36 + method: string;
  37 + params: string | Recordable;
  38 + persistent: boolean;
  39 + }
  40 +
33 export default defineComponent({ 41 export default defineComponent({
34 components: { BasicForm, Button, QuestionCircleOutlined, Tooltip }, 42 components: { BasicForm, Button, QuestionCircleOutlined, Tooltip },
35 props: { 43 props: {
@@ -41,7 +49,7 @@ @@ -41,7 +49,7 @@
41 emits: ['register'], 49 emits: ['register'],
42 setup(props) { 50 setup(props) {
43 const { createMessage } = useMessage(); 51 const { createMessage } = useMessage();
44 - const jsonData: any = ref({}); 52 + const jsonData = ref<CommandParams>({} as unknown as CommandParams);
45 const disable = ref(false); 53 const disable = ref(false);
46 const [registerForm, { getFieldsValue, validate, resetFields }] = useForm({ 54 const [registerForm, { getFieldsValue, validate, resetFields }] = useForm({
47 labelWidth: 100, 55 labelWidth: 100,
@@ -82,13 +90,17 @@ @@ -82,13 +90,17 @@
82 if (!valid) return; 90 if (!valid) return;
83 // 收集表单数据 91 // 收集表单数据
84 const field = getFieldsValue(); 92 const field = getFieldsValue();
85 - const getJson = unref(jsonInstance).get(); 93 + if (field.valueType === 'json') {
  94 + const getJson = unref(jsonInstance).get();
  95 + jsonData.value.params = getJson;
  96 + } else {
  97 + jsonData.value.params = field.commandText;
  98 + }
86 jsonData.value.persistent = true; 99 jsonData.value.persistent = true;
87 jsonData.value.additionalInfo = { 100 jsonData.value.additionalInfo = {
88 cmdType: 'API', 101 cmdType: 'API',
89 }; 102 };
90 jsonData.value.method = 'methodThingskit'; 103 jsonData.value.method = 'methodThingskit';
91 - jsonData.value.params = { ...getJson };  
92 commandIssuanceApi(field.commandType, props.deviceDetail.tbDeviceId, jsonData.value) 104 commandIssuanceApi(field.commandType, props.deviceDetail.tbDeviceId, jsonData.value)
93 .then((res) => { 105 .then((res) => {
94 if (!res) return; 106 if (!res) return;
@@ -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>
@@ -4,12 +4,12 @@ @@ -4,12 +4,12 @@
4 ref="formRef" 4 ref="formRef"
5 :model="scriptForm" 5 :model="scriptForm"
6 name="basic" 6 name="basic"
7 - :label-col="{ span: 3 }"  
8 - :wrapper-col="{ span: 17 }" 7 + :label-col="{ span: 4 }"
  8 + :wrapper-col="{ span: 16 }"
9 autocomplete="off" 9 autocomplete="off"
10 > 10 >
11 <a-form-item 11 <a-form-item
12 - :label="ifAdd ? '名称' : '输入参数'" 12 + :label="ifAdd ? '名称' : '输入参数(params)'"
13 :name="ifAdd ? 'name' : 'params'" 13 :name="ifAdd ? 'name' : 'params'"
14 :rules="[{ required: true, message: ifAdd ? '请输入脚本名称' : '请输入参数' }]" 14 :rules="[{ required: true, message: ifAdd ? '请输入脚本名称' : '请输入参数' }]"
15 > 15 >
@@ -42,7 +42,10 @@ @@ -42,7 +42,10 @@
42 copy 42 copy
43 </Button> 43 </Button>
44 </a-form-item> 44 </a-form-item>
45 - <a-form-item :label="ifAdd ? '备注' : '输出参数'" :name="ifAdd ? 'description' : 'output'"> 45 + <a-form-item
  46 + :label="ifAdd ? '备注' : '输出参数(output)'"
  47 + :name="ifAdd ? 'description' : 'output'"
  48 + >
46 <a-textarea 49 <a-textarea
47 :rows="3" 50 :rows="3"
48 v-if="ifAdd" 51 v-if="ifAdd"
@@ -98,7 +98,7 @@ @@ -98,7 +98,7 @@
98 }, 10); 98 }, 10);
99 } else { 99 } else {
100 if (res) { 100 if (res) {
101 - converScriptRef.value?.setScriptOutputData(res?.output); 101 + converScriptRef.value?.setScriptOutputData(res?.output || res?.error);
102 } 102 }
103 } 103 }
104 emits('success', res); 104 emits('success', res);