Commit c2819b0f692e6a20dcafbdf60bb7981fd2bf98c1

Authored by xp.Huang
2 parents 6d5dafbd 8414e9ef

Merge branch 'ww' into 'main'

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

See merge request huang/yun-teng-iot-front!383
1   -{
2   - "i18n-ally.localesPaths": [
3   - "src/locales",
4   - "src/locales/lang",
5   - "public/resource/tinymce/langs"
6   - ],
7   - "commentTranslate.targetLanguage": "en",
8   - "cSpell.words": [
9   - "Cmds",
10   - "unref"
11   - ]
12   -}
... ... @@ -63,6 +63,7 @@
63 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",
... ...
... ... @@ -344,7 +344,7 @@
344 344 }
345 345
346 346 &-form-container {
347   - padding: 16px 16px 16px 36px;
  347 + padding: 16px 16px 16px 16px;
348 348
349 349 .ant-form {
350 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 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>
... ...
... ... @@ -4,6 +4,7 @@ import { deviceProfile, getGATEWAYdevice } from '/@/api/device/deviceManager';
4 4
5 5 export enum TypeEnum {
6 6 IS_GATEWAY = 'GATEWAY',
  7 + SENSOR = 'SENSOR',
7 8 }
8 9 export const isGateWay = (type: string) => {
9 10 return type === TypeEnum.IS_GATEWAY;
... ...
... ... @@ -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>
... ...