Commit 551858c395b7e3051d5125d92b27c102031735cb

Authored by xp.Huang
2 parents 90011028 29d4d358

Merge branch 'feat/video-component-support-rtsp-protocol' into 'main_dev'

feat: 实现视频组件rtsp协议播放

See merge request yunteng/thingskit-front!649
... ... @@ -9,15 +9,21 @@
9 9 "COAP",
10 10 "echarts",
11 11 "edrx",
12   - "EFENTO",
  12 + "EFENTO",
  13 + "fingerprintjs",
  14 + "flvjs",
  15 + "flvjs",
13 16 "inited",
  17 + "liveui",
14 18 "MQTT",
15 19 "notif",
16 20 "PROTOBUF",
  21 + "rtsp",
17 22 "SCADA",
18 23 "SNMP",
19 24 "unref",
20 25 "vben",
  26 + "videojs",
21 27 "VITE",
22 28 "vnode",
23 29 "vueuse",
... ...
... ... @@ -35,6 +35,7 @@
35 35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts"
36 36 },
37 37 "dependencies": {
  38 + "@fingerprintjs/fingerprintjs": "^3.4.1",
38 39 "@iconify/iconify": "^2.0.3",
39 40 "@logicflow/core": "^0.6.9",
40 41 "@logicflow/extension": "^0.6.9",
... ... @@ -49,6 +50,7 @@
49 50 "cropperjs": "^1.5.12",
50 51 "crypto-js": "^4.1.1",
51 52 "echarts": "^5.1.2",
  53 + "flv.js": "^1.6.2",
52 54 "hls.js": "^1.0.10",
53 55 "intro.js": "^4.1.0",
54 56 "jsoneditor": "^9.7.2",
... ... @@ -65,6 +67,7 @@
65 67 "tinymce": "^5.8.2",
66 68 "vditor": "^3.8.6",
67 69 "video.js": "^7.20.3",
  70 + "videojs-flvjs-es6": "^1.0.1",
68 71 "vue": "3.2.31",
69 72 "vue-i18n": "9.1.7",
70 73 "vue-json-pretty": "^2.0.4",
... ...
... ... @@ -109,3 +109,13 @@ export const getStreamingPlayUrl = (entityId: string) => {
109 109 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`,
110 110 });
111 111 };
  112 +
  113 +export const getFlvPlayUrl = (url: string, browserId: string) => {
  114 + return `/api/yt/rtsp/openFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`;
  115 +};
  116 +
  117 +export const closeFlvPlay = (url: string, browserId: string) => {
  118 + return defHttp.get({
  119 + url: `/rtsp/closeFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`,
  120 + });
  121 +};
... ...
... ... @@ -4,15 +4,19 @@
4 4 import 'video.js/dist/video-js.css';
5 5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
6 6 import { useDesign } from '/@/hooks/web/useDesign';
7   -
  7 + import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
  8 + import { isShareMode } from '/@/views/sys/share/hook';
  9 + import 'videojs-flvjs-es6';
8 10 const { prefixCls } = useDesign('basic-video-play');
9 11
10 12 const props = defineProps<{
11 13 options?: VideoJsPlayerOptions;
  14 + withToken?: boolean;
12 15 }>();
13 16
14 17 const emit = defineEmits<{
15 18 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
  19 + (event: 'onUnmounted'): void;
16 20 }>();
17 21
18 22 const videoPlayEl = ref<HTMLVideoElement>();
... ... @@ -20,13 +24,34 @@
20 24 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();
21 25
22 26 const getOptions = computed(() => {
23   - const { options } = props;
24   - const defaultOptions: VideoJsPlayerOptions = {
  27 + const { options, withToken } = props;
  28 + const defaultOptions: VideoJsPlayerOptions & Recordable = {
25 29 language: 'zh',
26 30 muted: true,
27 31 liveui: true,
28 32 controls: true,
  33 + techOrder: ['html5', 'flvjs'],
  34 + flvjs: {
  35 + mediaDataSource: {
  36 + isLive: true,
  37 + cors: true,
  38 + hasAudio: false,
  39 + withCredentials: false,
  40 + autoCleanupSourceBuffer: true,
  41 + autoCleanupMaxBackwardDuration: 60,
  42 + },
  43 + config: {
  44 + headers: {
  45 + ...(withToken
  46 + ? {
  47 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,
  48 + }
  49 + : {}),
  50 + },
  51 + },
  52 + },
29 53 };
  54 +
30 55 return { ...defaultOptions, ...options };
31 56 });
32 57
... ... @@ -50,6 +75,7 @@
50 75 onUnmounted(() => {
51 76 unref(videoPlayInstance)?.dispose();
52 77 videoPlayInstance.value = null;
  78 + emit('onUnmounted');
53 79 });
54 80 </script>
55 81
... ... @@ -58,7 +84,9 @@
58 84 <video
59 85 ref="videoPlayEl"
60 86 class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full"
61   - ></video>
  87 + muted
  88 + >
  89 + </video>
62 90 </div>
63 91 </template>
64 92
... ...
... ... @@ -2,17 +2,26 @@ export enum VideoPlayerType {
2 2 m3u8 = 'application/x-mpegURL',
3 3 mp4 = 'video/mp4',
4 4 webm = 'video/webm',
  5 + flv = 'video/x-flv',
5 6 }
6 7
  8 +export const isRtspProtocol = (url: string) => {
  9 + const reg = /^rtsp:\/\//g;
  10 + return reg.test(url);
  11 +};
  12 +
7 13 export const getVideoTypeByUrl = (url: string) => {
8   - const splitExtReg = /(?:.*)(?<=\.)/;
9   - const type = url.replace(splitExtReg, '');
10   - /**
11   - * https://vcsplay.scjtonline.cn:8200/live/HD_1569b634-4789-11eb-ab67-3cd2e55e0b20.m3u8?auth_key=1681179278-0-0-5c54a376f2ca32d05c4a152ee96336e9
12   - * 如果是这种格式的m3u8,则截取的是这一部分.m3u8?auth_key=1681179278-0-0-5c54a376f2ca32d05c4a152ee96336e9
13   - */
14   - if (type.startsWith('m3u8')) return VideoPlayerType.m3u8;
15   - if (type.startsWith('mp4')) return VideoPlayerType.mp4;
16   - if (type.startsWith('webm')) return VideoPlayerType.webm;
17   - return VideoPlayerType.webm;
  14 + try {
  15 + const { protocol, pathname } = new URL(url);
  16 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv;
  17 +
  18 + const reg = /[^.]\w*$/;
  19 + const mathValue = pathname.match(reg) || [];
  20 + const ext = (mathValue[0] as keyof typeof VideoPlayerType) || 'webm';
  21 + const type = VideoPlayerType[ext];
  22 + return type ? type : VideoPlayerType.webm;
  23 + } catch (error) {
  24 + console.error(error);
  25 + return VideoPlayerType.webm;
  26 + }
18 27 };
... ...
  1 +import { load } from '@fingerprintjs/fingerprintjs';
  2 +export const useFingerprint = () => {
  3 + const getResult = async () => {
  4 + const fp = await load();
  5 + const result = await fp.get();
  6 + return result;
  7 + };
  8 +
  9 + return { getResult };
  10 +};
... ...
... ... @@ -11,58 +11,89 @@
11 11 @cancel="handleCancel"
12 12 >
13 13 <div
14   - class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52 video-container"
  14 + class="flex items-center justify-center bg-dark-900 w-full h-full min-h-96 video-container"
15 15 >
16   - <BasicVideoPlay v-if="showVideo" :options="options" />
  16 + <BasicVideoPlay
  17 + v-if="showVideo"
  18 + :options="(options as any)"
  19 + :withToken="withToken"
  20 + @on-unmounted="handleCloseFlvPlayUrl"
  21 + />
17 22 </div>
18 23 </BasicModal>
19 24 </div>
20 25 </template>
21 26 <script setup lang="ts">
22   - import { ref, reactive } from 'vue';
  27 + import { ref, reactive, unref } from 'vue';
23 28 import { BasicModal, useModalInner } from '/@/components/Modal';
24 29 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
25 30 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
26 31 import { AccessMode } from './config.data';
27   - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
  32 + import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager';
  33 + import { isRtspProtocol } from '/@/components/Video/src/utils';
28 34 import { VideoJsPlayerOptions } from 'video.js';
  35 + import { useFingerprint } from '/@/utils/useFingerprint';
  36 + import { GetResult } from '@fingerprintjs/fingerprintjs';
29 37
30 38 const heightNum = ref(800);
31 39 const showVideo = ref(false);
  40 +
  41 + const playUrl = ref('');
  42 +
  43 + const withToken = ref(false);
  44 +
  45 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  46 +
32 47 const options = reactive<VideoJsPlayerOptions>({
33 48 width: '100%' as unknown as number,
34   - height: '100%' as unknown as number,
  49 + height: 384 as unknown as number,
35 50 autoplay: true,
36 51 });
37 52
38   - const setSources = (url: string) => {
  53 + const setSources = (url: string, fingerprintResult: GetResult) => {
  54 + const flag = isRtspProtocol(url);
39 55 options.sources = [
40 56 {
41   - src: url,
  57 + src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url,
42 58 type: getVideoTypeByUrl(url),
43 59 },
44 60 ];
45 61 };
46 62
  63 + const { getResult } = useFingerprint();
47 64 const [register] = useModalInner(
48 65 async (data: { record: CameraModel | StreamingManageRecord }) => {
49 66 const { record } = data;
  67 + const result = await getResult();
  68 + fingerprintResult.value = result;
50 69 if (record.accessMode === AccessMode.ManuallyEnter) {
51 70 if ((record as CameraModel).videoUrl) {
52   - setSources((record as CameraModel).videoUrl);
  71 + if (isRtspProtocol((record as CameraModel).videoUrl)) {
  72 + playUrl.value = (record as CameraModel).videoUrl;
  73 + closeFlvPlay(unref(playUrl), result.visitorId);
  74 + withToken.value = true;
  75 + }
  76 + setSources((record as CameraModel).videoUrl, result);
53 77 }
54 78 } else {
55 79 try {
56 80 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
57   - setSources(url);
  81 + setSources(url, result);
58 82 } catch (error) {}
59 83 }
60 84 showVideo.value = true;
61 85 }
62 86 );
63 87
  88 + const handleCloseFlvPlayUrl = () => {
  89 + if (isRtspProtocol(unref(playUrl))) {
  90 + closeFlvPlay(unref(playUrl)!, unref(fingerprintResult)!.visitorId!);
  91 + }
  92 + };
  93 +
64 94 const handleCancel = () => {
65 95 showVideo.value = false;
  96 + withToken.value = false;
66 97 };
67 98 </script>
68 99
... ...
1 1 <script setup lang="ts">
2 2 import { PageWrapper } from '/@/components/Page';
3 3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
4   - import { onMounted, reactive, ref, unref, watch } from 'vue';
  4 + import { onMounted, reactive, Ref, ref, unref, watch } from 'vue';
5 5 import { Spin, Button, Pagination, Space, List } from 'ant-design-vue';
6   - import { cameraPage } from '/@/api/camera/cameraManager';
  6 + import { cameraPage, closeFlvPlay, getFlvPlayUrl } from '/@/api/camera/cameraManager';
7 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8 8 import { useFullscreen } from '@vueuse/core';
9 9 import CameraDrawer from './CameraDrawer.vue';
... ... @@ -16,11 +16,16 @@
16 16 import { VideoJsPlayerOptions } from 'video.js';
17 17 import { getBoundingClientRect } from '/@/utils/domUtils';
18 18 import { Authority } from '/@/components/Authority';
  19 + import { isRtspProtocol } from '/@/components/Video/src/utils';
  20 + import { useFingerprint } from '/@/utils/useFingerprint';
  21 + import { GetResult } from '@fingerprintjs/fingerprintjs';
19 22
20 23 type CameraRecordItem = CameraRecord & {
21 24 canPlay?: boolean;
22 25 isTransform?: boolean;
  26 + withToken?: boolean;
23 27 videoPlayerOptions?: VideoJsPlayerOptions;
  28 + playSourceUrl?: string;
24 29 };
25 30
26 31 const basicVideoPlayOptions: VideoJsPlayerOptions = {
... ... @@ -43,6 +48,8 @@
43 48 total: 0,
44 49 });
45 50
  51 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  52 +
46 53 // 树形选择器
47 54 const handleSelect = (orgId: string) => {
48 55 organizationId.value = orgId;
... ... @@ -60,12 +67,14 @@
60 67 });
61 68 pagination.total = total;
62 69
  70 + const result = await getResult();
  71 + fingerprintResult.value = result;
63 72 for (const item of items) {
64 73 (item as CameraRecordItem).isTransform = false;
65 74 (item as CameraRecordItem).videoPlayerOptions = {
66 75 ...basicVideoPlayOptions,
67 76 };
68   - beforeVideoPlay(item);
  77 + beforeVideoPlay(item, result);
69 78 }
70 79 if (items.length < pagination.pageSize) {
71 80 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({
... ... @@ -81,15 +90,25 @@
81 90 }
82 91 };
83 92
84   - const beforeVideoPlay = async (record: CameraRecordItem) => {
  93 + const { getResult } = useFingerprint();
  94 + const beforeVideoPlay = async (record: CameraRecordItem, fingerprintResult: GetResult) => {
85 95 if (record.accessMode === AccessMode.ManuallyEnter) {
86 96 if (record.videoUrl) {
  97 + const isFlvPlay = isRtspProtocol(record.videoUrl);
  98 + const type = getVideoTypeByUrl(record.videoUrl);
  99 + record.playSourceUrl = record.videoUrl;
  100 + if (isFlvPlay) {
  101 + // handleFlvPlayerUnload(record, fingerprintResult!.visitorId);
  102 + record.playSourceUrl = getFlvPlayUrl(record.videoUrl, fingerprintResult.visitorId);
  103 + record.withToken = true;
  104 + }
  105 +
87 106 (record as CameraRecordItem).videoPlayerOptions = {
88 107 ...basicVideoPlayOptions,
89 108 sources: [
90 109 {
91   - src: record.videoUrl,
92   - type: getVideoTypeByUrl(record.videoUrl),
  110 + src: record.playSourceUrl,
  111 + type,
93 112 },
94 113 ],
95 114 };
... ... @@ -104,6 +123,7 @@
104 123 const oldRecord = unref(cameraList).at(index)!;
105 124 unref(cameraList)[index] = {
106 125 ...oldRecord,
  126 +
107 127 videoPlayerOptions: {
108 128 ...basicVideoPlayOptions,
109 129 sources: [
... ... @@ -112,7 +132,7 @@
112 132 type: getVideoTypeByUrl(url),
113 133 },
114 134 ],
115   - },
  135 + } as any,
116 136 isTransform: true,
117 137 };
118 138 }
... ... @@ -134,7 +154,7 @@
134 154 getCameraList();
135 155 };
136 156
137   - const { enter, isFullscreen } = useFullscreen(videoContainer);
  157 + const { enter, isFullscreen } = useFullscreen(videoContainer as Ref<HTMLDivElement>);
138 158
139 159 const handleFullScreen = () => {
140 160 enter();
... ... @@ -160,6 +180,12 @@
160 180 });
161 181 };
162 182
  183 + const handleCloseFlvPlayUrl = async (record: CameraRecordItem) => {
  184 + if (isRtspProtocol(record.videoUrl)) {
  185 + closeFlvPlay(record.videoUrl, unref(fingerprintResult)!.visitorId!);
  186 + }
  187 + };
  188 +
163 189 onMounted(() => {
164 190 getCameraList();
165 191 });
... ... @@ -244,7 +270,7 @@
244 270 :loading="loading"
245 271 :data-source="cameraList"
246 272 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list"
247   - :grid="gridLayout"
  273 + :grid="(gridLayout as any)"
248 274 :style="{ '--height': `${100 / pagination.colNumber}%` }"
249 275 >
250 276 <template #renderItem="{ item }">
... ... @@ -263,7 +289,12 @@
263 289 v-show="!item.isTransform"
264 290 :spinning="!item.isTransform"
265 291 />
266   - <BasicVideoPlay v-if="item.isTransform" :options="item.videoPlayerOptions" />
  292 + <BasicVideoPlay
  293 + v-if="item.isTransform"
  294 + :options="item.videoPlayerOptions"
  295 + :with-token="item.withToken"
  296 + @on-unmounted="handleCloseFlvPlayUrl(item)"
  297 + />
267 298 <div
268 299 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center"
269 300 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
... ... @@ -316,6 +347,10 @@
316 347 .split-mode-list:deep(.ant-row) {
317 348 width: 100%;
318 349 height: 100%;
  350 +
  351 + > div {
  352 + height: var(--height);
  353 + }
319 354 }
320 355
321 356 .split-mode-list:deep(.ant-list-item) {
... ...
  1 +import { FormSchema } from '/@/components/Form';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
  3 +import { h, ref, unref } from 'vue';
  4 +import { isExistDataManagerNameApi } from '/@/api/datamanager/dataManagerApi';
  5 +import { getDeviceProfile } from '/@/api/alarm/position';
  6 +import { BasicColumn, BasicTableProps } from '/@/components/Table';
  7 +import { devicePage } from '/@/api/device/deviceManager';
  8 +import { Tag } from 'ant-design-vue';
  9 +import { DeviceRecord } from '/@/api/device/model/deviceModel';
  10 +import { FETCH_SETTING } from '/@/components/Table/src/const';
  11 +import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
  12 +import { useMessage } from '/@/hooks/web/useMessage';
  13 +
  14 +const typeValue = ref('');
  15 +export enum CredentialsEnum {
  16 + IS_ANONYMOUS = 'anonymous',
  17 + IS_BASIC = 'basic',
  18 + IS_PEM = 'pem',
  19 +}
  20 +export const isBasic = (type: string) => {
  21 + return type === CredentialsEnum.IS_BASIC;
  22 +};
  23 +export const isPem = (type: string) => {
  24 + return type === CredentialsEnum.IS_PEM;
  25 +};
  26 +
  27 +export enum DataSourceType {
  28 + ALL = 'ALL',
  29 + PRODUCT = 'PRODUCTS',
  30 + DEVICE = 'DEVICES',
  31 +}
  32 +
  33 +export enum BasicInfoFormField {
  34 + DATA_SOURCE_TYPE = 'datasourceType',
  35 + DATA_SOURCE_PRODUCT = 'datasourceProduct',
  36 + DATA_SOURCE_DEVICE = 'datasourceDevice',
  37 + CONVERT_CONFIG_ID = 'convertConfigId',
  38 +}
  39 +
  40 +export enum DeviceStatusEnum {
  41 + OFFLINE = 'OFFLINE',
  42 + ONLINE = 'ONLINE',
  43 + INACTIVE = 'INACTIVE',
  44 +}
  45 +
  46 +export enum DeviceStatusNameEnum {
  47 + OFFLINE = '离线',
  48 + ONLINE = '在线',
  49 + INACTIVE = '待激活',
  50 +}
  51 +
  52 +export enum DeviceTypeEnum {
  53 + SENSOR = 'SENSOR',
  54 + DIRECT_CONNECTION = 'DIRECT_CONNECTION',
  55 + GATEWAY = 'GATEWAY',
  56 +}
  57 +
  58 +export enum DeviceTypeNameEnum {
  59 + SENSOR = '网关子设备',
  60 + DIRECT_CONNECTION = '直连设备',
  61 + GATEWAY = '网关设备',
  62 +}
  63 +
  64 +const handleGroupDevice = (options: DeviceRecord[]) => {
  65 + const map = new Map<string, string[]>();
  66 + options.forEach((item) => {
  67 + if (map.has(item.profileId)) {
  68 + const deviceList = map.get(item.profileId)!;
  69 + deviceList.push(item.tbDeviceId);
  70 + } else {
  71 + map.set(item.profileId, [item.tbDeviceId]);
  72 + }
  73 + });
  74 + const value = Array.from(map.entries()).map(([product, devices]) => ({ product, devices }));
  75 +
  76 + return value;
  77 +};
  78 +
  79 +const deviceTableFormSchema: FormSchema[] = [
  80 + {
  81 + field: 'name',
  82 + label: '设备名称',
  83 + component: 'Input',
  84 + colProps: { span: 9 },
  85 + componentProps: {
  86 + placeholder: '请输入设备名称',
  87 + },
  88 + },
  89 + {
  90 + field: 'deviceType',
  91 + label: '设备类型',
  92 + component: 'ApiSelect',
  93 + colProps: { span: 9 },
  94 + componentProps: {
  95 + placeholder: '请选择设备类型',
  96 + api: findDictItemByCode,
  97 + params: {
  98 + dictCode: 'device_type',
  99 + },
  100 + labelField: 'itemText',
  101 + valueField: 'itemValue',
  102 + },
  103 + },
  104 +];
  105 +const { clipboardRef, isSuccessRef } = useCopyToClipboard();
  106 +const { createMessage } = useMessage();
  107 +const deviceTableColumn: BasicColumn[] = [
  108 + {
  109 + title: '状态',
  110 + dataIndex: 'deviceState',
  111 + customRender: ({ text }) => {
  112 + return h(
  113 + Tag,
  114 + {
  115 + color:
  116 + text === DeviceStatusEnum.INACTIVE
  117 + ? 'warning'
  118 + : text === DeviceStatusEnum.OFFLINE
  119 + ? 'error'
  120 + : 'success',
  121 + },
  122 + () => DeviceStatusNameEnum[text]
  123 + );
  124 + },
  125 + },
  126 + {
  127 + title: '别名/设备名称',
  128 + dataIndex: 'name',
  129 + customRender: ({ record }) => {
  130 + return h('div', [
  131 + h(
  132 + 'div',
  133 + {
  134 + class: 'cursor-pointer',
  135 + onClick: () => {
  136 + clipboardRef.value = record.name;
  137 + if (unref(isSuccessRef)) createMessage.success('复制成功~');
  138 + },
  139 + },
  140 + [
  141 + record.alias && h('div', { class: 'truncate' }, record.alias),
  142 + h('div', { class: 'text-blue-400 truncate' }, record.name),
  143 + ]
  144 + ),
  145 + ]);
  146 + },
  147 + },
  148 + {
  149 + title: '设备类型',
  150 + dataIndex: 'deviceType',
  151 + customRender: ({ text }) => {
  152 + return h(Tag, { color: 'success' }, () => DeviceTypeNameEnum[text]);
  153 + },
  154 + },
  155 + {
  156 + title: '所属产品',
  157 + dataIndex: 'deviceProfile.name',
  158 + },
  159 + {
  160 + title: '所属组织',
  161 + dataIndex: 'organizationDTO.name',
  162 + },
  163 +];
  164 +
  165 +const TransferTableProps: BasicTableProps = {
  166 + formConfig: {
  167 + layout: 'inline',
  168 + labelWidth: 80,
  169 + schemas: deviceTableFormSchema,
  170 + actionColOptions: { span: 6 },
  171 + },
  172 + size: 'small',
  173 + maxHeight: 240,
  174 + useSearchForm: true,
  175 + columns: deviceTableColumn,
  176 + showIndexColumn: false,
  177 + fetchSetting: FETCH_SETTING,
  178 +} as BasicTableProps;
  179 +
  180 +export const modeForm = (submitFn?: Function): FormSchema[] => {
  181 + return [
  182 + {
  183 + field: BasicInfoFormField.CONVERT_CONFIG_ID,
  184 + label: '',
  185 + component: 'Input',
  186 + show: false,
  187 + },
  188 + {
  189 + field: BasicInfoFormField.DATA_SOURCE_TYPE,
  190 + label: '数据源',
  191 + component: 'RadioGroup',
  192 + defaultValue: DataSourceType.ALL,
  193 + componentProps: {
  194 + options: [
  195 + { label: '全部', value: DataSourceType.ALL },
  196 + { label: '产品', value: DataSourceType.PRODUCT },
  197 + { label: '设备', value: DataSourceType.DEVICE },
  198 + ],
  199 + },
  200 + },
  201 + {
  202 + field: BasicInfoFormField.DATA_SOURCE_PRODUCT,
  203 + label: '数据源产品',
  204 + component: 'TransferModal',
  205 + ifShow: ({ model }) => {
  206 + return model[BasicInfoFormField.DATA_SOURCE_TYPE] !== DataSourceType.ALL;
  207 + },
  208 + valueField: 'value',
  209 + changeEvent: 'update:value',
  210 + componentProps: ({ formActionType }) => {
  211 + const { setFieldsValue } = formActionType;
  212 + return {
  213 + api: getDeviceProfile,
  214 + labelField: 'name',
  215 + valueField: 'tbProfileId',
  216 + transferProps: {
  217 + listStyle: { height: '400px' },
  218 + showSearch: true,
  219 + filterOption: (inputValue: string, option: Recordable) => {
  220 + const upperCaseInputValue = inputValue.toUpperCase();
  221 + const upperCaseOptionValue = option.name.toUpperCase();
  222 + return upperCaseOptionValue.includes(upperCaseInputValue);
  223 + },
  224 + },
  225 + onChange: () => {
  226 + setFieldsValue({ [BasicInfoFormField.DATA_SOURCE_DEVICE]: [] });
  227 + },
  228 + };
  229 + },
  230 + },
  231 + {
  232 + field: BasicInfoFormField.DATA_SOURCE_DEVICE,
  233 + label: '数据源设备',
  234 + component: 'TransferTableModal',
  235 + ifShow: ({ model }) => {
  236 + return model[BasicInfoFormField.DATA_SOURCE_TYPE] === DataSourceType.DEVICE;
  237 + },
  238 + valueField: 'value',
  239 + changeEvent: 'update:value',
  240 + componentProps: ({ formActionType }) => {
  241 + const { getFieldsValue } = formActionType;
  242 + const values = getFieldsValue();
  243 + const convertConfigId = Reflect.get(values, BasicInfoFormField.CONVERT_CONFIG_ID);
  244 + const devices = Reflect.get(values, BasicInfoFormField.DATA_SOURCE_DEVICE);
  245 +
  246 + return {
  247 + labelField: 'name',
  248 + valueField: 'tbDeviceId',
  249 + primaryKey: 'tbDeviceId',
  250 + pendingTableProps: {
  251 + ...TransferTableProps,
  252 + api: devicePage,
  253 + beforeFetch: (params) => {
  254 + const values = getFieldsValue();
  255 + const deviceProfileIds = Reflect.get(values, BasicInfoFormField.DATA_SOURCE_PRODUCT);
  256 + const convertConfigId = Reflect.get(values, BasicInfoFormField.CONVERT_CONFIG_ID);
  257 + if (convertConfigId) {
  258 + Object.assign(params, { convertConfigId, selected: false });
  259 + }
  260 + return { ...params, deviceProfileIds };
  261 + },
  262 + } as BasicTableProps,
  263 + selectedTableProps: {
  264 + ...TransferTableProps,
  265 + // api
  266 + api: !!(convertConfigId && devices) ? devicePage : undefined,
  267 + beforeFetch: (params) => {
  268 + const values = getFieldsValue();
  269 + const deviceProfileIds = Reflect.get(values, BasicInfoFormField.DATA_SOURCE_PRODUCT);
  270 + const convertConfigId = Reflect.get(values, BasicInfoFormField.CONVERT_CONFIG_ID);
  271 + if (convertConfigId) {
  272 + Object.assign(params, { convertConfigId, selected: true });
  273 + }
  274 + return { ...params, deviceProfileIds };
  275 + },
  276 + } as BasicTableProps,
  277 + initSelectedOptions: async ({ setSelectedTotal }) => {
  278 + const values = getFieldsValue();
  279 + const convertConfigId = Reflect.get(values, BasicInfoFormField.CONVERT_CONFIG_ID);
  280 + const deviceProfileIds = Reflect.get(values, BasicInfoFormField.DATA_SOURCE_PRODUCT);
  281 + const devices = Reflect.get(values, BasicInfoFormField.DATA_SOURCE_DEVICE);
  282 + if (convertConfigId && devices) {
  283 + const { items, total } = await devicePage({
  284 + page: 1,
  285 + pageSize: 10,
  286 + convertConfigId: values[BasicInfoFormField.CONVERT_CONFIG_ID],
  287 + deviceProfileIds,
  288 + selected: true,
  289 + });
  290 + setSelectedTotal(total);
  291 + return items;
  292 + }
  293 + return [];
  294 + },
  295 + onSelectedAfter: async () => {
  296 + submitFn && (await submitFn(false));
  297 + },
  298 + onRemoveAfter: async ({ reloadSelected }) => {
  299 + submitFn && (await submitFn(false));
  300 + reloadSelected();
  301 + },
  302 + transformValue: (_selectedRowKeys: string[], selectedRows: DeviceRecord[]) => {
  303 + return handleGroupDevice(selectedRows);
  304 + },
  305 + };
  306 + },
  307 + },
  308 + {
  309 + field: 'type',
  310 + label: '转换方式',
  311 + component: 'ApiSelect',
  312 + required: true,
  313 + colProps: {
  314 + span: 24,
  315 + },
  316 + componentProps({}) {
  317 + return {
  318 + api: findDictItemByCode,
  319 + params: {
  320 + dictCode: 'convert_data_to',
  321 + },
  322 + labelField: 'itemText',
  323 + valueField: 'itemValue',
  324 + onChange(value) {
  325 + typeValue.value = value;
  326 + },
  327 + };
  328 + },
  329 + },
  330 + {
  331 + field: 'remark',
  332 + label: '描述',
  333 + colProps: { span: 24 },
  334 + component: 'Input',
  335 + componentProps: {
  336 + maxLength: 255,
  337 + placeholder: '请输入描述',
  338 + },
  339 + },
  340 + ];
  341 +};
  342 +
  343 +export const modeKafkaForm: FormSchema[] = [
  344 + {
  345 + field: 'name',
  346 + label: '名称',
  347 + colProps: { span: 12 },
  348 + required: true,
  349 + component: 'Input',
  350 + componentProps: {
  351 + maxLength: 255,
  352 + placeholder: '请输入名称',
  353 + },
  354 + dynamicRules: () => {
  355 + return [
  356 + {
  357 + required: true,
  358 + validator(_, value) {
  359 + return new Promise((resolve, reject) => {
  360 + if (value == '') {
  361 + reject('请输入名称');
  362 + } else {
  363 + resolve();
  364 + }
  365 + });
  366 + },
  367 + },
  368 + ];
  369 + },
  370 + },
  371 + {
  372 + field: 'topicPattern',
  373 + label: '消息主题',
  374 + colProps: { span: 12 },
  375 + required: true,
  376 + component: 'Input',
  377 + defaultValue: 'my-topic',
  378 + componentProps: {
  379 + maxLength: 255,
  380 + placeholder: '请输入消息主题',
  381 + },
  382 + },
  383 + {
  384 + field: 'bootstrapServers',
  385 + label: '服务器',
  386 + colProps: { span: 12 },
  387 + component: 'Input',
  388 + defaultValue: 'localhost:9092',
  389 + required: true,
  390 + componentProps: {
  391 + maxLength: 255,
  392 + placeholder: 'localhost:9092',
  393 + },
  394 + },
  395 + {
  396 + field: 'retries',
  397 + label: '重连次数',
  398 + colProps: { span: 12 },
  399 + component: 'InputNumber',
  400 + defaultValue: 0,
  401 + componentProps: {
  402 + maxLength: 255,
  403 + },
  404 + },
  405 + {
  406 + field: 'batchSize',
  407 + label: '生产者并发',
  408 + colProps: { span: 12 },
  409 + component: 'InputNumber',
  410 + defaultValue: 16384,
  411 + componentProps: {
  412 + maxLength: 255,
  413 + },
  414 + },
  415 + {
  416 + field: 'linger',
  417 + label: '缓存时间',
  418 + colProps: { span: 12 },
  419 + component: 'InputNumber',
  420 + defaultValue: 0,
  421 + componentProps: {
  422 + maxLength: 255,
  423 + },
  424 + },
  425 + {
  426 + field: 'bufferMemory',
  427 + label: '最大缓存',
  428 + colProps: { span: 12 },
  429 + component: 'InputNumber',
  430 + defaultValue: 33554432,
  431 + componentProps: {
  432 + maxLength: 255,
  433 + },
  434 + },
  435 + {
  436 + field: 'acks',
  437 + component: 'Select',
  438 + label: '响应码',
  439 + colProps: { span: 12 },
  440 + defaultValue: '-1',
  441 + componentProps: {
  442 + placeholder: '请选择响应码',
  443 + options: [
  444 + { label: 'all', value: 'all' },
  445 + { label: '-1', value: '-1' },
  446 + { label: '0', value: '0' },
  447 + { label: '1', value: '1' },
  448 + ],
  449 + },
  450 + },
  451 + {
  452 + field: 'keySerializer',
  453 + label: '键序列化',
  454 + colProps: { span: 24 },
  455 + required: true,
  456 + component: 'Input',
  457 + defaultValue: 'org.apache.kafka.common.serialization.StringSerializer',
  458 + componentProps: {
  459 + maxLength: 255,
  460 + placeholder: 'org.apache.kafka.common.serialization.StringSerializer',
  461 + },
  462 + },
  463 + {
  464 + field: 'valueSerializer',
  465 + label: '值序列化',
  466 + colProps: { span: 24 },
  467 + required: true,
  468 + component: 'Input',
  469 + defaultValue: 'org.apache.kafka.common.serialization.StringSerializer',
  470 + componentProps: {
  471 + maxLength: 255,
  472 + placeholder: 'org.apache.kafka.common.serialization.StringSerializer',
  473 + },
  474 + },
  475 + {
  476 + field: 'otherProperties',
  477 + label: '其他属性',
  478 + colProps: { span: 24 },
  479 + component: 'JAddInput',
  480 + subLabel: '不可重复',
  481 + },
  482 + {
  483 + field: 'addMetadataKeyValuesAsKafkaHeaders',
  484 + label: '是否启用',
  485 + colProps: { span: 12 },
  486 + component: 'Checkbox',
  487 + renderComponentContent: '将消息的元数据以键值对的方式添加到Kafka消息头中',
  488 + },
  489 + {
  490 + field: 'kafkaHeadersCharset',
  491 + component: 'Select',
  492 + label: '字符集',
  493 + required: true,
  494 + colProps: { span: 12 },
  495 + defaultValue: 'UTF-8',
  496 + componentProps: {
  497 + placeholder: '请选择字符集编码',
  498 + options: [
  499 + { label: 'US-ASCII', value: 'US' },
  500 + { label: 'ISO-8859-1', value: 'ISO-8859-1' },
  501 + { label: 'UTF-8', value: 'UTF-8' },
  502 + { label: 'UTF-16BE', value: 'UTF-16BE' },
  503 + { label: 'UTF-16LE', value: 'UTF-16LE' },
  504 + { label: 'UTF-16', value: 'UTF-16' },
  505 + ],
  506 + },
  507 + ifShow: ({ values }) => {
  508 + return !!values.addMetadataKeyValuesAsKafkaHeaders;
  509 + },
  510 + },
  511 + {
  512 + field: 'description',
  513 + label: '说明',
  514 + colProps: { span: 24 },
  515 + component: 'InputTextArea',
  516 + componentProps: {
  517 + maxLength: 255,
  518 + rows: 4,
  519 + placeholder: '请输入说明',
  520 + },
  521 + },
  522 +];
  523 +
  524 +export const modeMqttForm: FormSchema[] = [
  525 + {
  526 + field: 'name',
  527 + label: '名称',
  528 + colProps: { span: 12 },
  529 + component: 'Input',
  530 + componentProps: {
  531 + maxLength: 255,
  532 + placeholder: '请输入名称',
  533 + },
  534 + },
  535 + {
  536 + field: 'topicPattern',
  537 + label: '主题模式',
  538 + colProps: { span: 12 },
  539 + required: true,
  540 + component: 'Input',
  541 + defaultValue: 'my-topic',
  542 + componentProps: {
  543 + maxLength: 255,
  544 + placeholder: '请输入Topic pattern',
  545 + },
  546 + },
  547 + {
  548 + field: 'host',
  549 + label: '主机',
  550 + colProps: { span: 12 },
  551 + component: 'Input',
  552 + componentProps: {
  553 + maxLength: 255,
  554 + placeholder: '请输入Host',
  555 + },
  556 + },
  557 + {
  558 + field: 'port',
  559 + label: '端口',
  560 + colProps: { span: 12 },
  561 + component: 'InputNumber',
  562 + defaultValue: 1883,
  563 + required: true,
  564 + componentProps: {
  565 + maxLength: 255,
  566 + placeholder: '请输入Port',
  567 + },
  568 + },
  569 + {
  570 + field: 'connectTimeoutSec',
  571 + label: '连接超时(秒)',
  572 + colProps: { span: 12 },
  573 + component: 'InputNumber',
  574 + defaultValue: 10,
  575 + required: true,
  576 + componentProps: {
  577 + maxLength: 255,
  578 + placeholder: '请输入Connection timeout (sec)',
  579 + },
  580 + },
  581 + {
  582 + field: 'clientId',
  583 + label: '客户端ID',
  584 + colProps: { span: 12 },
  585 + component: 'Input',
  586 + componentProps: ({ formActionType }) => {
  587 + const { updateSchema } = formActionType;
  588 + return {
  589 + onChange(e) {
  590 + if (!e.data) {
  591 + updateSchema({
  592 + field: 'appendClientIdSuffix',
  593 + show: false,
  594 + });
  595 + } else {
  596 + updateSchema({
  597 + field: 'appendClientIdSuffix',
  598 + show: true,
  599 + });
  600 + }
  601 + },
  602 + maxLength: 255,
  603 + placeholder: '请输入Client ID',
  604 + };
  605 + },
  606 + },
  607 + {
  608 + field: 'appendClientIdSuffix',
  609 + label: '',
  610 + colProps: { span: 12 },
  611 + defaultValue: false,
  612 + component: 'Checkbox',
  613 + renderComponentContent: '将服务ID作为后缀添加到客户端ID',
  614 + show: false,
  615 + },
  616 + {
  617 + field: 'cleanSession',
  618 + label: '是否启用',
  619 + colProps: { span: 12 },
  620 + defaultValue: true,
  621 + component: 'Checkbox',
  622 + renderComponentContent: '清除会话',
  623 + },
  624 + {
  625 + field: 'ssl',
  626 + label: '是否启用',
  627 + colProps: { span: 12 },
  628 + defaultValue: false,
  629 + component: 'Checkbox',
  630 + renderComponentContent: '启用SSL',
  631 + },
  632 + {
  633 + field: 'type',
  634 + component: 'Select',
  635 + label: '凭据类型',
  636 + colProps: { span: 12 },
  637 + defaultValue: 'anonymous',
  638 + componentProps: {
  639 + placeholder: '请选择Credentials',
  640 + options: [
  641 + { label: 'Anonymous', value: 'anonymous' },
  642 + { label: 'Basic', value: 'basic' },
  643 + { label: 'PEM', value: 'pem' },
  644 + ],
  645 + },
  646 + },
  647 + {
  648 + field: 'username',
  649 + label: '用户名',
  650 + colProps: { span: 12 },
  651 + component: 'Input',
  652 + required: true,
  653 + componentProps: {
  654 + maxLength: 255,
  655 + placeholder: '请输入用户名',
  656 + },
  657 + ifShow: ({ values }) => isBasic(Reflect.get(values, 'type')),
  658 + },
  659 + {
  660 + field: 'password',
  661 + label: '密码',
  662 + colProps: { span: 12 },
  663 + component: 'InputPassword',
  664 + componentProps: {
  665 + maxLength: 255,
  666 + placeholder: '请输入密码',
  667 + },
  668 + ifShow: ({ values }) => isBasic(Reflect.get(values, 'type')),
  669 + },
  670 + {
  671 + field: '4',
  672 + label: '',
  673 + colProps: { span: 24 },
  674 + component: 'Input',
  675 + slot: 'uploadAdd1',
  676 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  677 + },
  678 + {
  679 + field: '11',
  680 + label: '',
  681 + colProps: { span: 24 },
  682 + component: 'Input',
  683 + slot: 'showImg1',
  684 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  685 + },
  686 + {
  687 + field: '5',
  688 + label: '',
  689 + colProps: { span: 24 },
  690 + component: 'Input',
  691 + slot: 'uploadAdd2',
  692 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  693 + },
  694 + {
  695 + field: '1111',
  696 + label: '',
  697 + colProps: { span: 24 },
  698 + component: 'Input',
  699 + slot: 'showImg2',
  700 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  701 + },
  702 + {
  703 + field: '6',
  704 + label: '',
  705 + colProps: { span: 24 },
  706 + component: 'Input',
  707 + slot: 'uploadAdd3',
  708 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  709 + },
  710 + {
  711 + field: '111111',
  712 + label: '',
  713 + colProps: { span: 24 },
  714 + component: 'Input',
  715 + slot: 'showImg3',
  716 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  717 + },
  718 + {
  719 + field: 'password',
  720 + label: '密码',
  721 + colProps: { span: 12 },
  722 + component: 'InputPassword',
  723 + componentProps: {
  724 + maxLength: 255,
  725 + placeholder: '请输入密码',
  726 + },
  727 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  728 + },
  729 + {
  730 + field: 'description',
  731 + label: '说明',
  732 + colProps: { span: 24 },
  733 + component: 'InputTextArea',
  734 + componentProps: {
  735 + maxLength: 255,
  736 + rows: 4,
  737 + placeholder: '请输入说明',
  738 + },
  739 + },
  740 +];
  741 +
  742 +export const modeRabbitMqForm: FormSchema[] = [
  743 + {
  744 + field: 'name',
  745 + label: '名称',
  746 + colProps: { span: 12 },
  747 + required: true,
  748 + component: 'Input',
  749 + componentProps: {
  750 + maxLength: 255,
  751 + placeholder: '请输入名称',
  752 + },
  753 + dynamicRules: () => {
  754 + return [
  755 + {
  756 + required: true,
  757 + validator(_, value) {
  758 + return new Promise((resolve, reject) => {
  759 + if (value == '') {
  760 + reject('请输入名称');
  761 + } else {
  762 + resolve();
  763 + }
  764 + });
  765 + },
  766 + },
  767 + ];
  768 + },
  769 + },
  770 + {
  771 + field: 'exchangeNamePattern',
  772 + label: '交换名称模式',
  773 + colProps: { span: 12 },
  774 + component: 'Input',
  775 + componentProps: {
  776 + maxLength: 255,
  777 + placeholder: '请输入模式',
  778 + },
  779 + },
  780 + {
  781 + field: 'routingKeyPattern',
  782 + label: '路由密钥模式',
  783 + colProps: { span: 12 },
  784 + component: 'Input',
  785 + componentProps: {
  786 + maxLength: 255,
  787 + placeholder: '请输入模式',
  788 + },
  789 + },
  790 + {
  791 + field: 'messageProperties',
  792 + component: 'Select',
  793 + label: '消息属性',
  794 + colProps: { span: 12 },
  795 + componentProps: {
  796 + placeholder: '请选择消息属性',
  797 + options: [
  798 + { label: 'BASIC', value: 'BASIC' },
  799 + { label: 'TEXT_PLAIN', value: 'TEXT_PLAIN' },
  800 + { label: 'MINIMAL_BASIC', value: 'MINIMAL_BASIC' },
  801 + { label: 'MINIMAL_PERSISTENT_BASIC', value: 'MINIMAL_PERSISTENT_BASIC' },
  802 + { label: 'PERSISTENT_BASIC', value: 'PERSISTENT_BASIC' },
  803 + { label: 'PERSISTENT_TEXT_PLAIN', value: 'PERSISTENT_TEXT_PLAIN' },
  804 + ],
  805 + },
  806 + },
  807 + {
  808 + field: 'host',
  809 + label: '主机',
  810 + colProps: { span: 12 },
  811 + component: 'Input',
  812 + required: true,
  813 + defaultValue: 'localhost',
  814 + componentProps: {
  815 + maxLength: 255,
  816 + placeholder: 'localhost',
  817 + },
  818 + },
  819 + {
  820 + field: 'port',
  821 + label: '端口',
  822 + colProps: { span: 12 },
  823 + component: 'InputNumber',
  824 + defaultValue: 5672,
  825 + required: true,
  826 + componentProps: {
  827 + maxLength: 255,
  828 + placeholder: '请输入Port',
  829 + },
  830 + },
  831 + {
  832 + field: 'virtualHost',
  833 + label: '虚拟端口(以/开头)',
  834 + colProps: { span: 12 },
  835 + component: 'Input',
  836 + defaultValue: '/',
  837 + componentProps: {
  838 + maxLength: 255,
  839 + placeholder: '/',
  840 + },
  841 + },
  842 + {
  843 + field: 'username',
  844 + label: '用户名',
  845 + colProps: { span: 12 },
  846 + component: 'Input',
  847 + defaultValue: 'guest',
  848 + componentProps: {
  849 + maxLength: 255,
  850 + placeholder: '请输入用户名',
  851 + },
  852 + },
  853 + {
  854 + field: 'password',
  855 + label: '密码',
  856 + colProps: { span: 12 },
  857 + component: 'InputPassword',
  858 + defaultValue: 'guest',
  859 + componentProps: {
  860 + maxLength: 255,
  861 + placeholder: '请输入密码',
  862 + },
  863 + },
  864 + {
  865 + field: 'automaticRecoveryEnabled',
  866 + label: '是否启用',
  867 + colProps: { span: 12 },
  868 + component: 'Checkbox',
  869 + renderComponentContent: '自动恢复',
  870 + },
  871 + {
  872 + field: 'connectionTimeout',
  873 + label: '连接超时(毫秒)',
  874 + colProps: { span: 12 },
  875 + component: 'InputNumber',
  876 + defaultValue: 60000,
  877 + componentProps: {
  878 + maxLength: 255,
  879 + placeholder: '请输入Connection timeout (ms)',
  880 + },
  881 + },
  882 + {
  883 + field: 'handshakeTimeout',
  884 + label: '握手超时(毫秒)',
  885 + colProps: { span: 12 },
  886 + component: 'InputNumber',
  887 + defaultValue: 10000,
  888 + componentProps: {
  889 + maxLength: 255,
  890 + placeholder: '请输入Handshake timeout (ms)',
  891 + },
  892 + },
  893 + {
  894 + field: 'clientProperties',
  895 + label: '客户端属性',
  896 + colProps: { span: 24 },
  897 + component: 'JAddInput',
  898 + subLabel: '不可重复',
  899 + },
  900 + {
  901 + field: 'description',
  902 + label: '说明',
  903 + colProps: { span: 24 },
  904 + component: 'InputTextArea',
  905 + componentProps: {
  906 + maxLength: 255,
  907 + rows: 4,
  908 + placeholder: '请输入说明',
  909 + },
  910 + },
  911 +];
  912 +
  913 +export const modeApiForm: FormSchema[] = [
  914 + {
  915 + field: 'name',
  916 + label: '名称',
  917 + colProps: { span: 12 },
  918 + required: true,
  919 + component: 'Input',
  920 + componentProps: {
  921 + maxLength: 255,
  922 + placeholder: '请输入名称',
  923 + },
  924 + dynamicRules: ({ values }) => {
  925 + return [
  926 + {
  927 + required: true,
  928 + validator(_, value) {
  929 + return new Promise((resolve, reject) => {
  930 + if (value == '') {
  931 + reject('请输入名称');
  932 + } else {
  933 + if (values.name) {
  934 + isExistDataManagerNameApi({
  935 + name: value,
  936 + type:
  937 + typeValue.value == ''
  938 + ? 'org.thingsboard.rule.engine.rest.TbRestApiCallNode'
  939 + : typeValue.value,
  940 + }).then((data) => {
  941 + if (data == true) {
  942 + // createMessage.error('名称已存在');
  943 + resolve();
  944 + } else {
  945 + resolve();
  946 + }
  947 + });
  948 + } else {
  949 + resolve();
  950 + }
  951 + }
  952 + });
  953 + },
  954 + },
  955 + ];
  956 + },
  957 + },
  958 + {
  959 + field: 'restEndpointUrlPattern',
  960 + label: '端点URL模式',
  961 + colProps: { span: 12 },
  962 + required: true,
  963 + defaultValue: 'http://localhost/api',
  964 + component: 'Input',
  965 + componentProps: {
  966 + maxLength: 255,
  967 + placeholder: '请输入Endpoint URL pattern',
  968 + },
  969 + },
  970 + {
  971 + field: 'requestMethod',
  972 + component: 'Select',
  973 + label: '请求方式',
  974 + colProps: { span: 12 },
  975 + defaultValue: 'POST',
  976 + componentProps: {
  977 + placeholder: '请选择Request method',
  978 + options: [
  979 + { label: 'GET', value: 'GET' },
  980 + { label: 'POST', value: 'POST' },
  981 + { label: 'PUT', value: 'PUT' },
  982 + { label: 'DELETE', value: 'DELETE' },
  983 + ],
  984 + },
  985 + },
  986 + {
  987 + field: 'enableProxy',
  988 + label: '是否启用',
  989 + colProps: { span: 12 },
  990 + component: 'Checkbox',
  991 + renderComponentContent: '启用代理',
  992 + },
  993 +
  994 + {
  995 + field: 'proxyHost',
  996 + label: '代理主机',
  997 + colProps: { span: 12 },
  998 + required: true,
  999 + component: 'Input',
  1000 + componentProps: {
  1001 + maxLength: 255,
  1002 + placeholder: 'http或者https开头',
  1003 + },
  1004 + ifShow: ({ values }) => {
  1005 + return !!values.enableProxy;
  1006 + },
  1007 + },
  1008 + {
  1009 + field: 'proxyPort',
  1010 + label: '代理端口',
  1011 + colProps: { span: 12 },
  1012 + required: true,
  1013 + component: 'InputNumber',
  1014 + defaultValue: 0,
  1015 + componentProps: {
  1016 + maxLength: 255,
  1017 + placeholder: 'http或者https开头',
  1018 + },
  1019 + ifShow: ({ values }) => {
  1020 + return !!values.enableProxy;
  1021 + },
  1022 + },
  1023 + {
  1024 + field: 'proxyUser',
  1025 + label: '代理用户',
  1026 + colProps: { span: 12 },
  1027 + required: true,
  1028 + component: 'Input',
  1029 + componentProps: {
  1030 + maxLength: 255,
  1031 + placeholder: '请输入代理用户',
  1032 + },
  1033 + ifShow: ({ values }) => {
  1034 + return !!values.enableProxy;
  1035 + },
  1036 + },
  1037 + {
  1038 + field: 'proxyPassword',
  1039 + label: '代理密码',
  1040 + colProps: { span: 12 },
  1041 + required: true,
  1042 + component: 'InputPassword',
  1043 + componentProps: {
  1044 + maxLength: 255,
  1045 + placeholder: '请输入代理密码',
  1046 + },
  1047 + ifShow: ({ values }) => {
  1048 + return !!values.enableProxy;
  1049 + },
  1050 + },
  1051 +
  1052 + {
  1053 + field: 'useSystemProxyProperties',
  1054 + label: '是否启用',
  1055 + colProps: { span: 12 },
  1056 + component: 'Checkbox',
  1057 + renderComponentContent: '使用系统代理属性',
  1058 + },
  1059 + {
  1060 + field: 'maxParallelRequestsCount',
  1061 + label: '最大并行请求数',
  1062 + colProps: { span: 12 },
  1063 + required: true,
  1064 + component: 'InputNumber',
  1065 + defaultValue: 0,
  1066 + componentProps: {
  1067 + maxLength: 255,
  1068 + },
  1069 + ifShow: ({ values }) => {
  1070 + return !!values.useSystemProxyProperties;
  1071 + },
  1072 + },
  1073 + {
  1074 + field: 'ignoreRequestBody',
  1075 + label: '是否启用',
  1076 + colProps: { span: 12 },
  1077 + component: 'Checkbox',
  1078 + renderComponentContent: '无请求正文',
  1079 + },
  1080 + {
  1081 + field: 'readTimeoutMs',
  1082 + label: '读取超时(毫秒)',
  1083 + colProps: { span: 12 },
  1084 + required: true,
  1085 + component: 'InputNumber',
  1086 + defaultValue: 0,
  1087 + componentProps: {
  1088 + maxLength: 255,
  1089 + },
  1090 + ifShow: ({ values }) => {
  1091 + return !values.useSystemProxyProperties;
  1092 + },
  1093 + },
  1094 + {
  1095 + field: 'maxParallelRequestsCount',
  1096 + label: '最大并行请求数',
  1097 + colProps: { span: 12 },
  1098 + required: true,
  1099 + component: 'InputNumber',
  1100 + defaultValue: 0,
  1101 + componentProps: {
  1102 + maxLength: 255,
  1103 + },
  1104 + ifShow: ({ values }) => {
  1105 + return !values.useSystemProxyProperties;
  1106 + },
  1107 + },
  1108 + {
  1109 + field: 'headers',
  1110 + label: 'Headers',
  1111 + colProps: { span: 24 },
  1112 + defaultValue: { 'Content-Type': 'application/json' },
  1113 + component: 'JAddInput',
  1114 + subLabel: '不可重复',
  1115 + },
  1116 +
  1117 + {
  1118 + field: 'useRedisQueueForMsgPersistence',
  1119 + label: '是否启用',
  1120 + colProps: { span: 12 },
  1121 + component: 'Checkbox',
  1122 + renderComponentContent: '使用redis队列进行消息持久性',
  1123 + },
  1124 + {
  1125 + field: 'trimQueue',
  1126 + label: '是否启用',
  1127 + colProps: { span: 12 },
  1128 + component: 'Checkbox',
  1129 + renderComponentContent: '修剪redis队列',
  1130 + ifShow: ({ values }) => {
  1131 + return !!values.useRedisQueueForMsgPersistence;
  1132 + },
  1133 + },
  1134 + {
  1135 + field: 'maxQueueSize',
  1136 + label: 'Redis队列最大数',
  1137 + colProps: { span: 12 },
  1138 + required: true,
  1139 + component: 'InputNumber',
  1140 + defaultValue: 0,
  1141 + componentProps: {
  1142 + maxLength: 255,
  1143 + },
  1144 + ifShow: ({ values }) => {
  1145 + return !!values.useRedisQueueForMsgPersistence;
  1146 + },
  1147 + },
  1148 +
  1149 + {
  1150 + field: 'type',
  1151 + component: 'Select',
  1152 + label: '凭据类型',
  1153 + colProps: { span: 12 },
  1154 + defaultValue: 'anonymous',
  1155 + componentProps: {
  1156 + placeholder: '请选择凭据类型',
  1157 + options: [
  1158 + { label: 'Anonymous', value: 'anonymous' },
  1159 + { label: 'Basic', value: 'basic' },
  1160 + { label: 'PEM', value: 'pem' },
  1161 + ],
  1162 + },
  1163 + },
  1164 + {
  1165 + field: 'username',
  1166 + label: '用户名',
  1167 + colProps: { span: 12 },
  1168 + component: 'Input',
  1169 + required: true,
  1170 + componentProps: {
  1171 + maxLength: 255,
  1172 + placeholder: '请输入用户名',
  1173 + },
  1174 + ifShow: ({ values }) => isBasic(Reflect.get(values, 'type')),
  1175 + },
  1176 + {
  1177 + field: 'password',
  1178 + label: '密码',
  1179 + colProps: { span: 12 },
  1180 + component: 'InputPassword',
  1181 + required: true,
  1182 + componentProps: {
  1183 + maxLength: 255,
  1184 + placeholder: '请输入密码',
  1185 + },
  1186 + ifShow: ({ values }) => isBasic(Reflect.get(values, 'type')),
  1187 + },
  1188 + {
  1189 + field: '1',
  1190 + label: '',
  1191 + colProps: { span: 24 },
  1192 + component: 'Input',
  1193 + slot: 'uploadAdd1',
  1194 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1195 + },
  1196 + {
  1197 + field: '11',
  1198 + label: '',
  1199 + colProps: { span: 24 },
  1200 + component: 'Input',
  1201 + slot: 'showImg1',
  1202 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1203 + },
  1204 + {
  1205 + field: '1',
  1206 + label: '',
  1207 + colProps: { span: 24 },
  1208 + component: 'Input',
  1209 + slot: 'uploadAdd2',
  1210 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1211 + },
  1212 + {
  1213 + field: '1111',
  1214 + label: '',
  1215 + colProps: { span: 24 },
  1216 + component: 'Input',
  1217 + slot: 'showImg2',
  1218 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1219 + },
  1220 + {
  1221 + field: '1',
  1222 + label: '',
  1223 + colProps: { span: 24 },
  1224 + component: 'Input',
  1225 + slot: 'uploadAdd3',
  1226 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1227 + },
  1228 + {
  1229 + field: '111111',
  1230 + label: '',
  1231 + colProps: { span: 24 },
  1232 + component: 'Input',
  1233 + slot: 'showImg3',
  1234 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1235 + },
  1236 + {
  1237 + field: 'password',
  1238 + label: '密码',
  1239 + colProps: { span: 12 },
  1240 + component: 'InputPassword',
  1241 + componentProps: {
  1242 + maxLength: 255,
  1243 + placeholder: '请输入密码',
  1244 + },
  1245 + ifShow: ({ values }) => isPem(Reflect.get(values, 'type')),
  1246 + },
  1247 +
  1248 + {
  1249 + field: 'description',
  1250 + label: '说明',
  1251 + colProps: { span: 24 },
  1252 + component: 'InputTextArea',
  1253 + componentProps: {
  1254 + maxLength: 255,
  1255 + rows: 4,
  1256 + placeholder: '请输入说明',
  1257 + },
  1258 + },
  1259 +];
... ...
  1 +<template>
  2 + <div class="transfer-config-mode">
  3 + <BasicForm :showSubmitButton="false" @register="register">
  4 + <template #uploadAdd1="{ field }">
  5 + <span style="display: none">{{ field }}</span>
  6 + <a-upload-dragger
  7 + v-model:fileList="fileList1"
  8 + name="file"
  9 + :key="1"
  10 + :multiple="false"
  11 + @change="handleChange('T', $event)"
  12 + :before-upload="() => false"
  13 + >
  14 + <p class="ant-upload-drag-icon">
  15 + <InboxOutlined />
  16 + </p>
  17 + <p class="ant-upload-text">点击或将文件拖拽到这里上传</p>
  18 + <p class="ant-upload-hint">
  19 + 支持扩展名:.jpeg .png .jpg ...
  20 + <br />
  21 + 文件大小:最大支持5M
  22 + </p>
  23 + </a-upload-dragger>
  24 + </template>
  25 + <template #showImg1="{ field }">
  26 + <span style="display: none">{{ field }}</span>
  27 + <img
  28 + v-if="showImg1"
  29 + :src="showImg1Pic"
  30 + alt="avatar"
  31 + style="width: 6.25rem; height: 6.25rem"
  32 + />
  33 + </template>
  34 + <div style="margin-top: 50px"></div>
  35 + <template #uploadAdd2="{ field }">
  36 + <span style="display: none">{{ field }}</span>
  37 + <a-upload-dragger
  38 + v-model:fileList="fileList2"
  39 + name="file"
  40 + :key="2"
  41 + :multiple="false"
  42 + @change="handleChange('F', $event)"
  43 + :before-upload="() => false"
  44 + >
  45 + <p class="ant-upload-drag-icon">
  46 + <InboxOutlined />
  47 + </p>
  48 + <p class="ant-upload-text">点击或将文件拖拽到这里上传</p>
  49 + <p class="ant-upload-hint">
  50 + 支持扩展名:.jpeg .png .jpg ...
  51 + <br />
  52 + 文件大小:最大支持5M
  53 + </p>
  54 + </a-upload-dragger>
  55 + </template>
  56 + <template #showImg2="{ field }">
  57 + <span style="display: none">{{ field }}</span>
  58 + <img
  59 + v-if="showImg2"
  60 + :src="showImg2Pic"
  61 + alt="avatar"
  62 + style="width: 6.25rem; height: 6.25rem"
  63 + />
  64 + </template>
  65 + <div style="margin-top: 50px"></div>
  66 + <template #uploadAdd3="{ field }">
  67 + <span style="display: none">{{ field }}</span>
  68 + <a-upload-dragger
  69 + v-model:fileList="fileList3"
  70 + name="file"
  71 + :key="3"
  72 + :multiple="false"
  73 + @change="handleChange('C', $event)"
  74 + :before-upload="() => false"
  75 + >
  76 + <p class="ant-upload-drag-icon">
  77 + <InboxOutlined />
  78 + </p>
  79 + <p class="ant-upload-text">点击或将文件拖拽到这里上传</p>
  80 + <p class="ant-upload-hint">
  81 + 支持扩展名:.jpeg .png .jpg ...
  82 + <br />
  83 + 文件大小:最大支持5M
  84 + </p>
  85 + </a-upload-dragger>
  86 + </template>
  87 + <template #showImg3="{ field }">
  88 + <span style="display: none">{{ field }}</span>
  89 + <img
  90 + v-if="showImg3"
  91 + :src="showImg3Pic"
  92 + alt="avatar"
  93 + style="width: 6.25rem; height: 6.25rem"
  94 + />
  95 + </template>
  96 + </BasicForm>
  97 + </div>
  98 +</template>
  99 +<script lang="ts">
  100 + import { defineComponent, ref, reactive, nextTick } from 'vue';
  101 + import { BasicForm, useForm } from '/@/components/Form';
  102 + import { CredentialsEnum, modeMqttForm } from '../config';
  103 + import { InboxOutlined } from '@ant-design/icons-vue';
  104 + import { Alert, Divider, Descriptions, Upload } from 'ant-design-vue';
  105 + import { uploadApi } from '/@/api/personal/index';
  106 + import { useMessage } from '/@/hooks/web/useMessage';
  107 +
  108 + export default defineComponent({
  109 + components: {
  110 + BasicForm,
  111 + [Alert.name]: Alert,
  112 + [Divider.name]: Divider,
  113 + [Descriptions.name]: Descriptions,
  114 + [Descriptions.Item.name]: Descriptions.Item,
  115 + InboxOutlined,
  116 + [Upload.Dragger.name]: Upload.Dragger,
  117 + },
  118 + emits: ['next', 'prev', 'register'],
  119 + setup(_, { emit }) {
  120 + const showImg1 = ref(false);
  121 + const showImg1Pic = ref('');
  122 + const showImg2 = ref(false);
  123 + const showImg2Pic = ref('');
  124 + const showImg3 = ref(false);
  125 + const showImg3Pic = ref('');
  126 + const { createMessage } = useMessage();
  127 + let caCertFileName = ref('');
  128 + let privateKeyFileName = ref('');
  129 + let certFileName = ref('');
  130 + let fileList1: any = ref<[]>([]);
  131 + let fileList2: any = ref<[]>([]);
  132 + let fileList3: any = ref<[]>([]);
  133 + const credentialsV: any = reactive({
  134 + credentials: {
  135 + type: '',
  136 + },
  137 + });
  138 + const sonValues: any = reactive({
  139 + configuration: {},
  140 + });
  141 + const [register, { validate, setFieldsValue, resetFields: defineClearFunc }] = useForm({
  142 + labelWidth: 120,
  143 + schemas: modeMqttForm,
  144 + actionColOptions: {
  145 + span: 14,
  146 + },
  147 + resetButtonOptions: {
  148 + text: '上一步',
  149 + },
  150 + resetFunc: customResetFunc,
  151 + submitFunc: customSubmitFunc,
  152 + });
  153 +
  154 + /**
  155 + * 上传图片
  156 + */
  157 + const handleChange = async (e, { file }) => {
  158 + if (file.status === 'removed') {
  159 + if (e == 'T') {
  160 + fileList1.value = [];
  161 + showImg1.value = false;
  162 + showImg1Pic.value = '';
  163 + caCertFileName.value = '';
  164 + } else if (e == 'F') {
  165 + fileList2.value = [];
  166 + showImg2.value = false;
  167 + showImg2Pic.value = '';
  168 + certFileName.value = '';
  169 + } else {
  170 + fileList3.value = [];
  171 + showImg3.value = false;
  172 + showImg3Pic.value = '';
  173 + privateKeyFileName.value = '';
  174 + }
  175 + } else {
  176 + const isLt5M = file.size / 1024 / 1024 < 5;
  177 + if (!isLt5M) {
  178 + createMessage.error('图片大小不能超过5MB!');
  179 + } else {
  180 + e == 'T'
  181 + ? (fileList1.value = [file])
  182 + : e == 'F'
  183 + ? (fileList2.value = [file])
  184 + : (fileList3.value = [file]);
  185 + const formData = new FormData();
  186 + formData.append('file', file);
  187 + const response = await uploadApi(formData);
  188 + if (response.fileStaticUri) {
  189 + if (e == 'T') {
  190 + caCertFileName.value = response.fileStaticUri;
  191 + const iscaCertFileNamePic = caCertFileName.value.split('.').pop();
  192 + if (
  193 + iscaCertFileNamePic == 'jpg' ||
  194 + iscaCertFileNamePic == 'png' ||
  195 + iscaCertFileNamePic == 'jpeg' ||
  196 + iscaCertFileNamePic == 'gif'
  197 + ) {
  198 + showImg1.value = true;
  199 + showImg1Pic.value = response.fileStaticUri;
  200 + } else {
  201 + showImg1.value = false;
  202 + }
  203 + } else if (e == 'F') {
  204 + certFileName.value = response.fileStaticUri;
  205 + const iscertFileNamePic = certFileName.value.split('.').pop();
  206 + if (
  207 + iscertFileNamePic == 'jpg' ||
  208 + iscertFileNamePic == 'png' ||
  209 + iscertFileNamePic == 'jpeg' ||
  210 + iscertFileNamePic == 'gif'
  211 + ) {
  212 + showImg2.value = true;
  213 + showImg2Pic.value = response.fileStaticUri;
  214 + } else {
  215 + showImg2.value = false;
  216 + }
  217 + } else {
  218 + privateKeyFileName.value = response.fileStaticUri;
  219 + const isprivateKeyFileNamePic = privateKeyFileName.value.split('.').pop();
  220 + if (
  221 + isprivateKeyFileNamePic == 'jpg' ||
  222 + isprivateKeyFileNamePic == 'png' ||
  223 + isprivateKeyFileNamePic == 'jpeg' ||
  224 + isprivateKeyFileNamePic == 'gif'
  225 + ) {
  226 + showImg3.value = true;
  227 + showImg3Pic.value = response.fileStaticUri;
  228 + } else {
  229 + showImg3.value = false;
  230 + }
  231 + }
  232 + }
  233 + }
  234 + }
  235 + };
  236 + const setStepTwoFieldsValueFunc = (v, v1, v2) => {
  237 + setFieldsValue(v);
  238 + setFieldsValue({
  239 + name: v1,
  240 + description: v2,
  241 + });
  242 + setFieldsValue({
  243 + password: v.credentials?.password,
  244 + username: v.credentials?.username,
  245 + type: v.credentials?.type,
  246 + });
  247 + fileList1.value = [
  248 + {
  249 + name: v.credentials?.caCertFileName.slice(39),
  250 + uid: '1',
  251 + },
  252 + ];
  253 + fileList2.value = [
  254 + {
  255 + name: v.credentials?.certFileName.slice(39),
  256 + uid: '2',
  257 + },
  258 + ];
  259 + fileList3.value = [
  260 + {
  261 + name: v.credentials?.privateKeyFileName.slice(39),
  262 + uid: '3',
  263 + },
  264 + ];
  265 + caCertFileName.value = v.credentials?.caCertFileName;
  266 + certFileName.value = v.credentials?.certFileName;
  267 + privateKeyFileName.value = v.credentials?.privateKeyFileName;
  268 + const iscaCertFileNamePic = v.credentials?.caCertFileName.split('.').pop();
  269 + const iscertFileNamePic = v.credentials?.certFileName.split('.').pop();
  270 + const isprivateKeyFileNamePic = v.credentials?.privateKeyFileName.split('.').pop();
  271 + if (
  272 + iscaCertFileNamePic == 'jpg' ||
  273 + iscaCertFileNamePic == 'png' ||
  274 + iscaCertFileNamePic == 'jpeg' ||
  275 + iscaCertFileNamePic == 'gif'
  276 + ) {
  277 + showImg1.value = true;
  278 + showImg1Pic.value = v.credentials?.caCertFileName;
  279 + } else {
  280 + showImg1.value = false;
  281 + }
  282 + if (
  283 + iscertFileNamePic == 'jpg' ||
  284 + iscertFileNamePic == 'png' ||
  285 + iscertFileNamePic == 'jpeg' ||
  286 + iscertFileNamePic == 'gif'
  287 + ) {
  288 + showImg2.value = true;
  289 + showImg2Pic.value = v.credentials?.certFileName;
  290 + } else {
  291 + showImg2.value = false;
  292 + }
  293 + if (
  294 + isprivateKeyFileNamePic == 'jpg' ||
  295 + isprivateKeyFileNamePic == 'png' ||
  296 + isprivateKeyFileNamePic == 'jpeg' ||
  297 + isprivateKeyFileNamePic == 'gif'
  298 + ) {
  299 + showImg3.value = true;
  300 + showImg3Pic.value = v.credentials?.privateKeyFileName;
  301 + } else {
  302 + showImg3.value = false;
  303 + }
  304 + };
  305 + const customClearStepTwoValueFunc = async () => {
  306 + nextTick(() => {
  307 + defineClearFunc();
  308 + fileList1.value = [];
  309 + fileList2.value = [];
  310 + fileList3.value = [];
  311 + caCertFileName.value = '';
  312 + privateKeyFileName.value = '';
  313 + certFileName.value = '';
  314 + showImg1.value = false;
  315 + showImg1Pic.value = '';
  316 + showImg2.value = false;
  317 + showImg2Pic.value = '';
  318 + showImg3.value = false;
  319 + showImg3Pic.value = '';
  320 + });
  321 + };
  322 + async function customResetFunc() {
  323 + emit('prev');
  324 + }
  325 + async function customSubmitFunc() {
  326 + try {
  327 + const values = await validate();
  328 + emit('next', values);
  329 + } catch (error) {
  330 + } finally {
  331 + }
  332 + }
  333 + const getSonValueFunc = async () => {
  334 + sonValues.configuration = await validate();
  335 + credentialsV.credentials.type = sonValues.configuration.type;
  336 + if (credentialsV.credentials.type == CredentialsEnum.IS_BASIC) {
  337 + credentialsV.credentials.username = sonValues.configuration.username;
  338 + credentialsV.credentials.password = sonValues.configuration.password;
  339 + sonValues.configuration.username = undefined;
  340 + sonValues.configuration.password = undefined;
  341 + } else if (credentialsV.credentials.type == CredentialsEnum.IS_PEM) {
  342 + credentialsV.credentials.caCertFileName = caCertFileName.value;
  343 + credentialsV.credentials.certFileName = certFileName.value;
  344 + credentialsV.credentials.privateKeyFileName = privateKeyFileName.value;
  345 + }
  346 + if (!sonValues.configuration.clientId) {
  347 + sonValues.configuration.clientId = null;
  348 + }
  349 + Object.assign(sonValues.configuration, credentialsV);
  350 + return sonValues;
  351 + };
  352 + return {
  353 + getSonValueFunc,
  354 + register,
  355 + setStepTwoFieldsValueFunc,
  356 + customClearStepTwoValueFunc,
  357 + fileList1,
  358 + fileList2,
  359 + fileList3,
  360 + handleChange,
  361 + caCertFileName,
  362 + privateKeyFileName,
  363 + certFileName,
  364 + showImg1,
  365 + showImg1Pic,
  366 + showImg2,
  367 + showImg2Pic,
  368 + showImg3,
  369 + showImg3Pic,
  370 + };
  371 + },
  372 + });
  373 +</script>
  374 +
  375 +<style lang="less" scoped>
  376 + :deep(.ant-col-24) {
  377 + margin-bottom: 20px !important;
  378 + }
  379 +
  380 + :deep(.ant-btn-default) {
  381 + color: white;
  382 + background: #377dff;
  383 + }
  384 +</style>
... ...