Commit d9e4ec706d6970a67dd2d2a474f9ea65afbb6040

Authored by xp.Huang
2 parents 5d526eda 8518abd2

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

Feat/video component support rtsp protocol

See merge request yunteng/thingskit-front!651
@@ -7,15 +7,21 @@ @@ -7,15 +7,21 @@
7 "cSpell.words": [ 7 "cSpell.words": [
8 "COAP", 8 "COAP",
9 "edrx", 9 "edrx",
10 - "EFENTO", 10 + "EFENTO",
  11 + "fingerprintjs",
  12 + "flvjs",
  13 + "flvjs",
11 "inited", 14 "inited",
  15 + "liveui",
12 "MQTT", 16 "MQTT",
13 "notif", 17 "notif",
14 "PROTOBUF", 18 "PROTOBUF",
  19 + "rtsp",
15 "SCADA", 20 "SCADA",
16 "SNMP", 21 "SNMP",
17 "unref", 22 "unref",
18 "vben", 23 "vben",
  24 + "videojs",
19 "VITE", 25 "VITE",
20 "windicss" 26 "windicss"
21 ] 27 ]
@@ -35,6 +35,7 @@ @@ -35,6 +35,7 @@
35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts" 35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts"
36 }, 36 },
37 "dependencies": { 37 "dependencies": {
  38 + "@fingerprintjs/fingerprintjs": "^3.4.1",
38 "@iconify/iconify": "^2.0.3", 39 "@iconify/iconify": "^2.0.3",
39 "@logicflow/core": "^0.6.9", 40 "@logicflow/core": "^0.6.9",
40 "@logicflow/extension": "^0.6.9", 41 "@logicflow/extension": "^0.6.9",
@@ -49,6 +50,7 @@ @@ -49,6 +50,7 @@
49 "cropperjs": "^1.5.12", 50 "cropperjs": "^1.5.12",
50 "crypto-js": "^4.1.1", 51 "crypto-js": "^4.1.1",
51 "echarts": "^5.1.2", 52 "echarts": "^5.1.2",
  53 + "flv.js": "^1.6.2",
52 "hls.js": "^1.0.10", 54 "hls.js": "^1.0.10",
53 "intro.js": "^4.1.0", 55 "intro.js": "^4.1.0",
54 "jsoneditor": "^9.7.2", 56 "jsoneditor": "^9.7.2",
@@ -65,6 +67,7 @@ @@ -65,6 +67,7 @@
65 "tinymce": "^5.8.2", 67 "tinymce": "^5.8.2",
66 "vditor": "^3.8.6", 68 "vditor": "^3.8.6",
67 "video.js": "^7.20.3", 69 "video.js": "^7.20.3",
  70 + "videojs-flvjs-es6": "^1.0.1",
68 "vue": "3.2.31", 71 "vue": "3.2.31",
69 "vue-i18n": "9.1.7", 72 "vue-i18n": "9.1.7",
70 "vue-json-pretty": "^2.0.4", 73 "vue-json-pretty": "^2.0.4",
@@ -101,3 +101,13 @@ export const getStreamingPlayUrl = (entityId: string) => { @@ -101,3 +101,13 @@ export const getStreamingPlayUrl = (entityId: string) => {
101 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`, 101 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`,
102 }); 102 });
103 }; 103 };
  104 +
  105 +export const getFlvPlayUrl = (url: string, browserId: string) => {
  106 + return `/api/yt/rtsp/openFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`;
  107 +};
  108 +
  109 +export const closeFlvPlay = (url: string, browserId: string) => {
  110 + return defHttp.get({
  111 + url: `/rtsp/closeFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`,
  112 + });
  113 +};
@@ -4,15 +4,19 @@ @@ -4,15 +4,19 @@
4 import 'video.js/dist/video-js.css'; 4 import 'video.js/dist/video-js.css';
5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue'; 5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
6 import { useDesign } from '/@/hooks/web/useDesign'; 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 const { prefixCls } = useDesign('basic-video-play'); 10 const { prefixCls } = useDesign('basic-video-play');
9 11
10 const props = defineProps<{ 12 const props = defineProps<{
11 options?: VideoJsPlayerOptions; 13 options?: VideoJsPlayerOptions;
  14 + withToken?: boolean;
12 }>(); 15 }>();
13 16
14 const emit = defineEmits<{ 17 const emit = defineEmits<{
15 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void; 18 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
  19 + (event: 'onUnmounted'): void;
16 }>(); 20 }>();
17 21
18 const videoPlayEl = ref<HTMLVideoElement>(); 22 const videoPlayEl = ref<HTMLVideoElement>();
@@ -20,13 +24,34 @@ @@ -20,13 +24,34 @@
20 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>(); 24 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();
21 25
22 const getOptions = computed(() => { 26 const getOptions = computed(() => {
23 - const { options } = props;  
24 - const defaultOptions: VideoJsPlayerOptions = { 27 + const { options, withToken } = props;
  28 + const defaultOptions: VideoJsPlayerOptions & Recordable = {
25 language: 'zh', 29 language: 'zh',
26 muted: true, 30 muted: true,
27 liveui: true, 31 liveui: true,
28 controls: true, 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 return { ...defaultOptions, ...options }; 55 return { ...defaultOptions, ...options };
31 }); 56 });
32 57
@@ -50,6 +75,7 @@ @@ -50,6 +75,7 @@
50 onUnmounted(() => { 75 onUnmounted(() => {
51 unref(videoPlayInstance)?.dispose(); 76 unref(videoPlayInstance)?.dispose();
52 videoPlayInstance.value = null; 77 videoPlayInstance.value = null;
  78 + emit('onUnmounted');
53 }); 79 });
54 </script> 80 </script>
55 81
@@ -58,7 +84,9 @@ @@ -58,7 +84,9 @@
58 <video 84 <video
59 ref="videoPlayEl" 85 ref="videoPlayEl"
60 class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full" 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 </div> 90 </div>
63 </template> 91 </template>
64 92
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 - 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;  
18 -}; 1 +export enum VideoPlayerType {
  2 + m3u8 = 'application/x-mpegURL',
  3 + mp4 = 'video/mp4',
  4 + webm = 'video/webm',
  5 + flv = 'video/x-flv',
  6 +}
  7 +
  8 +export const isRtspProtocol = (url: string) => {
  9 + const reg = /^rtsp:\/\//g;
  10 + return reg.test(url);
  11 +};
  12 +
  13 +export const getVideoTypeByUrl = (url: string) => {
  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 + }
  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,58 +11,89 @@
11 @cancel="handleCancel" 11 @cancel="handleCancel"
12 > 12 >
13 <div 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 </div> 22 </div>
18 </BasicModal> 23 </BasicModal>
19 </div> 24 </div>
20 </template> 25 </template>
21 <script setup lang="ts"> 26 <script setup lang="ts">
22 - import { ref, reactive } from 'vue'; 27 + import { ref, reactive, unref } from 'vue';
23 import { BasicModal, useModalInner } from '/@/components/Modal'; 28 import { BasicModal, useModalInner } from '/@/components/Modal';
24 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; 29 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
25 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; 30 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
26 import { AccessMode } from './config.data'; 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 import { VideoJsPlayerOptions } from 'video.js'; 34 import { VideoJsPlayerOptions } from 'video.js';
  35 + import { useFingerprint } from '/@/utils/useFingerprint';
  36 + import { GetResult } from '@fingerprintjs/fingerprintjs';
29 37
30 const heightNum = ref(800); 38 const heightNum = ref(800);
31 const showVideo = ref(false); 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 const options = reactive<VideoJsPlayerOptions>({ 47 const options = reactive<VideoJsPlayerOptions>({
33 width: '100%' as unknown as number, 48 width: '100%' as unknown as number,
34 - height: '100%' as unknown as number, 49 + height: 384 as unknown as number,
35 autoplay: true, 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 options.sources = [ 55 options.sources = [
40 { 56 {
41 - src: url, 57 + src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url,
42 type: getVideoTypeByUrl(url), 58 type: getVideoTypeByUrl(url),
43 }, 59 },
44 ]; 60 ];
45 }; 61 };
46 62
  63 + const { getResult } = useFingerprint();
47 const [register] = useModalInner( 64 const [register] = useModalInner(
48 async (data: { record: CameraModel | StreamingManageRecord }) => { 65 async (data: { record: CameraModel | StreamingManageRecord }) => {
49 const { record } = data; 66 const { record } = data;
  67 + const result = await getResult();
  68 + fingerprintResult.value = result;
50 if (record.accessMode === AccessMode.ManuallyEnter) { 69 if (record.accessMode === AccessMode.ManuallyEnter) {
51 if ((record as CameraModel).videoUrl) { 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 } else { 78 } else {
55 try { 79 try {
56 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); 80 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
57 - setSources(url); 81 + setSources(url, result);
58 } catch (error) {} 82 } catch (error) {}
59 } 83 }
60 showVideo.value = true; 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 const handleCancel = () => { 94 const handleCancel = () => {
65 showVideo.value = false; 95 showVideo.value = false;
  96 + withToken.value = false;
66 }; 97 };
67 </script> 98 </script>
68 99
1 <script setup lang="ts"> 1 <script setup lang="ts">
2 import { PageWrapper } from '/@/components/Page'; 2 import { PageWrapper } from '/@/components/Page';
3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue'; 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 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, closeFlvPlay, getFlvPlayUrl } from '/@/api/camera/cameraManager';
7 import { CameraRecord } from '/@/api/camera/model/cameraModel'; 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8 import { useFullscreen } from '@vueuse/core'; 8 import { useFullscreen } from '@vueuse/core';
9 import CameraDrawer from './CameraDrawer.vue'; 9 import CameraDrawer from './CameraDrawer.vue';
@@ -16,11 +16,16 @@ @@ -16,11 +16,16 @@
16 import { VideoJsPlayerOptions } from 'video.js'; 16 import { VideoJsPlayerOptions } from 'video.js';
17 import { getBoundingClientRect } from '/@/utils/domUtils'; 17 import { getBoundingClientRect } from '/@/utils/domUtils';
18 import { Authority } from '/@/components/Authority'; 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 type CameraRecordItem = CameraRecord & { 23 type CameraRecordItem = CameraRecord & {
21 canPlay?: boolean; 24 canPlay?: boolean;
22 isTransform?: boolean; 25 isTransform?: boolean;
  26 + withToken?: boolean;
23 videoPlayerOptions?: VideoJsPlayerOptions; 27 videoPlayerOptions?: VideoJsPlayerOptions;
  28 + playSourceUrl?: string;
24 }; 29 };
25 30
26 const basicVideoPlayOptions: VideoJsPlayerOptions = { 31 const basicVideoPlayOptions: VideoJsPlayerOptions = {
@@ -43,6 +48,8 @@ @@ -43,6 +48,8 @@
43 total: 0, 48 total: 0,
44 }); 49 });
45 50
  51 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  52 +
46 // 树形选择器 53 // 树形选择器
47 const handleSelect = (orgId: string) => { 54 const handleSelect = (orgId: string) => {
48 organizationId.value = orgId; 55 organizationId.value = orgId;
@@ -60,12 +67,14 @@ @@ -60,12 +67,14 @@
60 }); 67 });
61 pagination.total = total; 68 pagination.total = total;
62 69
  70 + const result = await getResult();
  71 + fingerprintResult.value = result;
63 for (const item of items) { 72 for (const item of items) {
64 (item as CameraRecordItem).isTransform = false; 73 (item as CameraRecordItem).isTransform = false;
65 (item as CameraRecordItem).videoPlayerOptions = { 74 (item as CameraRecordItem).videoPlayerOptions = {
66 ...basicVideoPlayOptions, 75 ...basicVideoPlayOptions,
67 }; 76 };
68 - beforeVideoPlay(item); 77 + beforeVideoPlay(item, result);
69 } 78 }
70 if (items.length < pagination.pageSize) { 79 if (items.length < pagination.pageSize) {
71 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({ 80 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({
@@ -81,15 +90,25 @@ @@ -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 if (record.accessMode === AccessMode.ManuallyEnter) { 95 if (record.accessMode === AccessMode.ManuallyEnter) {
86 if (record.videoUrl) { 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 (record as CameraRecordItem).videoPlayerOptions = { 106 (record as CameraRecordItem).videoPlayerOptions = {
88 ...basicVideoPlayOptions, 107 ...basicVideoPlayOptions,
89 sources: [ 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,6 +123,7 @@
104 const oldRecord = unref(cameraList).at(index)!; 123 const oldRecord = unref(cameraList).at(index)!;
105 unref(cameraList)[index] = { 124 unref(cameraList)[index] = {
106 ...oldRecord, 125 ...oldRecord,
  126 +
107 videoPlayerOptions: { 127 videoPlayerOptions: {
108 ...basicVideoPlayOptions, 128 ...basicVideoPlayOptions,
109 sources: [ 129 sources: [
@@ -112,7 +132,7 @@ @@ -112,7 +132,7 @@
112 type: getVideoTypeByUrl(url), 132 type: getVideoTypeByUrl(url),
113 }, 133 },
114 ], 134 ],
115 - }, 135 + } as any,
116 isTransform: true, 136 isTransform: true,
117 }; 137 };
118 } 138 }
@@ -134,7 +154,7 @@ @@ -134,7 +154,7 @@
134 getCameraList(); 154 getCameraList();
135 }; 155 };
136 156
137 - const { enter, isFullscreen } = useFullscreen(videoContainer); 157 + const { enter, isFullscreen } = useFullscreen(videoContainer as Ref<HTMLDivElement>);
138 158
139 const handleFullScreen = () => { 159 const handleFullScreen = () => {
140 enter(); 160 enter();
@@ -160,6 +180,12 @@ @@ -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 onMounted(() => { 189 onMounted(() => {
164 getCameraList(); 190 getCameraList();
165 }); 191 });
@@ -244,7 +270,7 @@ @@ -244,7 +270,7 @@
244 :loading="loading" 270 :loading="loading"
245 :data-source="cameraList" 271 :data-source="cameraList"
246 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list" 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 :style="{ '--height': `${100 / pagination.colNumber}%` }" 274 :style="{ '--height': `${100 / pagination.colNumber}%` }"
249 > 275 >
250 <template #renderItem="{ item }"> 276 <template #renderItem="{ item }">
@@ -263,7 +289,12 @@ @@ -263,7 +289,12 @@
263 v-show="!item.isTransform" 289 v-show="!item.isTransform"
264 :spinning="!item.isTransform" 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 <div 298 <div
268 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center" 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 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" 300 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
@@ -316,6 +347,10 @@ @@ -316,6 +347,10 @@
316 .split-mode-list:deep(.ant-row) { 347 .split-mode-list:deep(.ant-row) {
317 width: 100%; 348 width: 100%;
318 height: 100%; 349 height: 100%;
  350 +
  351 + > div {
  352 + height: var(--height);
  353 + }
319 } 354 }
320 355
321 .split-mode-list:deep(.ant-list-item) { 356 .split-mode-list:deep(.ant-list-item) {