Commit bddcb179d4ce25bcb948fc110d535d7d90a9957d

Authored by xp.Huang
2 parents 2d4e8f73 14204ba0

Merge branch 'main_dev' into 'main'

Main dev

See merge request yunteng/thingskit-front!1291
Showing 33 changed files with 648 additions and 809 deletions
@@ -72,14 +72,16 @@ @@ -72,14 +72,16 @@
72 "sortablejs": "^1.14.0", 72 "sortablejs": "^1.14.0",
73 "tinymce": "^5.8.2", 73 "tinymce": "^5.8.2",
74 "vditor": "^3.8.6", 74 "vditor": "^3.8.6",
75 - "video.js": "^7.20.3",  
76 - "videojs-flvjs-es6": "^1.0.1",  
77 "vue": "3.3.4", 75 "vue": "3.3.4",
78 "vue-i18n": "9.1.7", 76 "vue-i18n": "9.1.7",
79 "vue-json-pretty": "^2.0.4", 77 "vue-json-pretty": "^2.0.4",
80 "vue-router": "^4.0.11", 78 "vue-router": "^4.0.11",
81 "vue-types": "^4.0.3", 79 "vue-types": "^4.0.3",
82 "vue3-grid-layout": "^1.0.0", 80 "vue3-grid-layout": "^1.0.0",
  81 + "xgplayer": "^3.0.14",
  82 + "xgplayer-flv": "^3.0.14",
  83 + "xgplayer-hls": "^3.0.14",
  84 + "xgplayer-mp4": "^3.0.14",
83 "xlsx": "^0.17.0" 85 "xlsx": "^0.17.0"
84 }, 86 },
85 "devDependencies": { 87 "devDependencies": {
@@ -101,7 +103,6 @@ @@ -101,7 +103,6 @@
101 "@types/qrcode": "^1.4.1", 103 "@types/qrcode": "^1.4.1",
102 "@types/qs": "^6.9.7", 104 "@types/qs": "^6.9.7",
103 "@types/sortablejs": "^1.10.7", 105 "@types/sortablejs": "^1.10.7",
104 - "@types/video.js": "^7.3.49",  
105 "@typescript-eslint/eslint-plugin": "^4.29.1", 106 "@typescript-eslint/eslint-plugin": "^4.29.1",
106 "@typescript-eslint/parser": "^4.29.1", 107 "@typescript-eslint/parser": "^4.29.1",
107 "@vitejs/plugin-legacy": "^1.5.1", 108 "@vitejs/plugin-legacy": "^1.5.1",
@@ -109,7 +109,7 @@ export const deleteStreamingMediaRecord = (params: StreamingMediaDeleteParam) => @@ -109,7 +109,7 @@ export const deleteStreamingMediaRecord = (params: StreamingMediaDeleteParam) =>
109 * @returns 109 * @returns
110 */ 110 */
111 export const getStreamingPlayUrl = (entityId: string) => { 111 export const getStreamingPlayUrl = (entityId: string) => {
112 - return defHttp.get( 112 + return defHttp.get<{ data: { url: string } }>(
113 { 113 {
114 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`, 114 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`,
115 }, 115 },
@@ -35,6 +35,7 @@ export interface CameraModel { @@ -35,6 +35,7 @@ export interface CameraModel {
35 deviceId: string; 35 deviceId: string;
36 deviceName: string; 36 deviceName: string;
37 }; 37 };
  38 + playProtocol?: number;
38 } 39 }
39 40
40 export interface StreamingManageRecord { 41 export interface StreamingManageRecord {
@@ -106,4 +107,14 @@ export interface CameraRecord { @@ -106,4 +107,14 @@ export interface CameraRecord {
106 channelNo: string; 107 channelNo: string;
107 deviceId: string; 108 deviceId: string;
108 }; 109 };
  110 + videoPlatformDTO?: VideoPlatformDTO;
  111 +}
  112 +
  113 +interface VideoPlatformDTO {
  114 + enabled: boolean;
  115 + type: number;
  116 + host: string;
  117 + appKey: string;
  118 + appSecret: string;
  119 + ssl: number;
109 } 120 }
1 -import { RuleChainPaginationItemType } from './model/type'; 1 +import { ClearEventsParam, RuleChainPaginationItemType } from './model/type';
2 import { TBPaginationResult } from '/#/axios'; 2 import { TBPaginationResult } from '/#/axios';
3 import { defHttp } from '/@/utils/http/axios'; 3 import { defHttp } from '/@/utils/http/axios';
4 import { RuleChainDetail, RuleChainType } from '/@/views/rule/designer/types/ruleNode'; 4 import { RuleChainDetail, RuleChainType } from '/@/views/rule/designer/types/ruleNode';
@@ -75,3 +75,26 @@ export const getRuleNodeEventList = ( @@ -75,3 +75,26 @@ export const getRuleNodeEventList = (
75 { joinPrefix: false } 75 { joinPrefix: false }
76 ); 76 );
77 }; 77 };
  78 +
  79 +export const doClearEvents = ({
  80 + tenantId,
  81 + startTime,
  82 + endTime,
  83 + eventType,
  84 + ruleId,
  85 +}: ClearEventsParam) => {
  86 + return defHttp.post(
  87 + {
  88 + url: `${Api.GET_RULE_NODE_EVENTS}/${ruleId}/clear`,
  89 + params: {
  90 + startTime,
  91 + endTime,
  92 + tenantId,
  93 + },
  94 + data: {
  95 + eventType,
  96 + },
  97 + },
  98 + { joinPrefix: false }
  99 + );
  100 +};
  1 +import { EventTypeEnum } from '/@/views/rule/designer/src/components/UpdateNodeDrawer/EventSelect/config';
  2 +
1 export interface RuleChainPaginationItemType { 3 export interface RuleChainPaginationItemType {
2 id: Id; 4 id: Id;
3 createdTime: number; 5 createdTime: number;
@@ -19,3 +21,11 @@ export interface Id { @@ -19,3 +21,11 @@ export interface Id {
19 export interface AdditionalInfo { 21 export interface AdditionalInfo {
20 description: string; 22 description: string;
21 } 23 }
  24 +
  25 +export interface ClearEventsParam {
  26 + ruleId: string;
  27 + tenantId: string;
  28 + startTime: string;
  29 + endTime: string;
  30 + eventType: EventTypeEnum;
  31 +}
1 <template> 1 <template>
2 - <a-tree-select v-bind="getAttrs" @change="handleChange"> 2 + <a-tree-select
  3 + v-bind="getAttrs"
  4 + @change="handleChange"
  5 + :showSearch="true"
  6 + :filterTreeNode="filterTreeNode"
  7 + >
3 <template #[item]="data" v-for="item in Object.keys($slots)"> 8 <template #[item]="data" v-for="item in Object.keys($slots)">
4 <slot :name="item" v-bind="data || {}"></slot> 9 <slot :name="item" v-bind="data || {}"></slot>
5 </template> 10 </template>
@@ -60,6 +65,18 @@ @@ -60,6 +65,18 @@
60 props.immediate && fetch(); 65 props.immediate && fetch();
61 }); 66 });
62 67
  68 + function filterTreeNode(value, options) {
  69 + const { props } = options || {};
  70 +
  71 + if (!value) {
  72 + return true;
  73 + }
  74 + let { name } = props || {};
  75 + value = value.toLowerCase();
  76 + name = name.toLowerCase();
  77 + return name.includes(value);
  78 + }
  79 +
63 async function fetch() { 80 async function fetch() {
64 const { api } = props; 81 const { api } = props;
65 if (!api || !isFunction(api)) return; 82 if (!api || !isFunction(api)) return;
@@ -80,7 +97,7 @@ @@ -80,7 +97,7 @@
80 isFirstLoaded.value = true; 97 isFirstLoaded.value = true;
81 emit('options-change', treeData.value); 98 emit('options-change', treeData.value);
82 } 99 }
83 - return { getAttrs, loading, handleChange }; 100 + return { getAttrs, loading, handleChange, filterTreeNode };
84 }, 101 },
85 }); 102 });
86 </script> 103 </script>
1 -import { withInstall } from '/@/utils/index';  
2 -import VideoPlay from './src/VideoPlay.vue'; 1 +export { default as XGPlayer } from './src/XGPlayer.vue';
3 2
4 export { getVideoTypeByUrl } from './src/utils'; 3 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, toRaw, unref } from 'vue';  
6 - import { useDesign } from '/@/hooks/web/useDesign';  
7 - import { getJwtToken, getShareJwtToken } from '/@/utils/auth';  
8 - import { isShareMode } from '/@/views/sys/share/hook';  
9 - import 'videojs-flvjs-es6';  
10 - const { prefixCls } = useDesign('basic-video-play');  
11 -  
12 - const props = withDefaults(  
13 - defineProps<{  
14 - options?: VideoJsPlayerOptions;  
15 - withToken?: boolean;  
16 - immediateInitOnMounted?: boolean;  
17 - }>(),  
18 - {  
19 - immediateInitOnMounted: true,  
20 - }  
21 - );  
22 -  
23 - const emit = defineEmits<{  
24 - (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;  
25 - (event: 'onUnmounted'): void;  
26 - }>();  
27 -  
28 - const videoPlayEl = ref<HTMLVideoElement>();  
29 -  
30 - const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();  
31 -  
32 - const getOptions = computed(() => {  
33 - const { options, withToken } = props;  
34 -  
35 - const defaultOptions: VideoJsPlayerOptions & Recordable = {  
36 - language: 'zh',  
37 - muted: true,  
38 - liveui: true,  
39 - controls: true,  
40 - techOrder: ['html5', 'flvjs'],  
41 - flvjs: {  
42 - mediaDataSource: {  
43 - isLive: true,  
44 - cors: true,  
45 - hasAudio: false,  
46 - withCredentials: false,  
47 - },  
48 - config: {  
49 - headers: {  
50 - ...(withToken  
51 - ? {  
52 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,  
53 - }  
54 - : {}),  
55 - },  
56 - autoCleanupSourceBuffer: true,  
57 - },  
58 - },  
59 - };  
60 - return videoJs.mergeOptions(defaultOptions, options);  
61 - });  
62 -  
63 - const getWidthHeight = computed(() => {  
64 - let { width = 300, height = 150 } = unref(getOptions);  
65 - width = isNumber(width) ? (`${width}px` as unknown as number) : width;  
66 - height = isNumber(height) ? (`${height}px` as unknown as number) : height;  
67 - return { width, height } as CSSProperties;  
68 - });  
69 -  
70 - const init = () => {  
71 - if (unref(videoPlayInstance)) unref(videoPlayInstance)?.dispose();  
72 - videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => {  
73 - emit('ready', unref(videoPlayInstance));  
74 - });  
75 - //fix 修复videojs解决直播延时的问题  
76 - videoPlayInstance.value?.on('timeupdate', function () {  
77 - // 计算表最新推流的时间和现在播放器播放推流的时间  
78 - let differTime =  
79 - (videoPlayInstance.value as any as VideoJsPlayer)?.buffered()?.end(0) -  
80 - (videoPlayInstance.value as any as VideoJsPlayer)?.currentTime();  
81 - // 差值小于1.5s时根据1倍速进行播放  
82 - if (differTime < 1.5) {  
83 - videoPlayInstance.value?.playbackRate(1);  
84 - }  
85 - // 差值大于1.5s小于10s根据1.2倍速进行播放  
86 - if (differTime < 10 && differTime > 1.5) {  
87 - videoPlayInstance.value?.playbackRate(1.2);  
88 - }  
89 - // 差值大于10s时进行重新加载直播流  
90 - if (differTime > 10) {  
91 - init();  
92 - }  
93 - });  
94 - //  
95 - };  
96 -  
97 - const customInit = (getOptionsFn: (optios: VideoJsPlayerOptions) => VideoJsPlayerOptions) => {  
98 - return (videoPlayInstance.value = videoJs(  
99 - unref(videoPlayEl)!,  
100 - getOptionsFn(toRaw(unref(getOptions))),  
101 - () => {  
102 - emit('ready', unref(videoPlayInstance));  
103 - }  
104 - ));  
105 - };  
106 -  
107 - onMounted(() => {  
108 - props.immediateInitOnMounted && init();  
109 - });  
110 -  
111 - onUnmounted(() => {  
112 - unref(videoPlayInstance)?.dispose();  
113 - videoPlayInstance.value = null;  
114 - emit('onUnmounted');  
115 - });  
116 -  
117 - defineExpose({  
118 - customInit,  
119 - reloadPlayer: init,  
120 - getInstance: () => unref(videoPlayInstance),  
121 - });  
122 -</script>  
123 -  
124 -<template>  
125 - <div :class="prefixCls" class="!w-full h-full" :style="getWidthHeight" style="min-height: 100%">  
126 - <video  
127 - ref="videoPlayEl"  
128 - class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full"  
129 - muted  
130 - >  
131 - </video>  
132 - </div>  
133 -</template>  
134 -  
135 -<style lang="less">  
136 - @prefix-cls: ~'@{namespace}-basic-video-play';  
137 -  
138 - .@{prefix-cls} {  
139 - .vjs-error-display {  
140 - .vjs-modal-dialog-content::after {  
141 - content: '无法加载视频,原因可能是服务器或网络故障,也可能是格式不支持.';  
142 - }  
143 - }  
144 - }  
145 -</style>  
  1 +<script setup lang="ts">
  2 + import Player, { Events, IError } from 'xgplayer';
  3 + import { FlvPlugin } from 'xgplayer-flv';
  4 + import Mp4Plugin from 'xgplayer-mp4';
  5 + import { HlsPlugin } from 'xgplayer-hls';
  6 + import { onMounted, shallowRef, computed, unref, toRaw, onUnmounted, ref, watch } from 'vue';
  7 + import PresetPlayer from 'xgplayer';
  8 + import { IPlayerOptions } from 'xgplayer/es/player';
  9 + import 'xgplayer/dist/index.min.css';
  10 + import { isShareMode } from '/@/views/sys/share/hook';
  11 + import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
  12 + import { XGPlayerProps } from '/@/components/Video/src/types';
  13 + import { StreamType } from './types';
  14 +
  15 + const props = withDefaults(defineProps<XGPlayerProps>(), {
  16 + streamType: 'auto',
  17 + autoPlay: true,
  18 + config: () => ({}),
  19 + });
  20 +
  21 + const emits = defineEmits<{
  22 + (eventName: 'ready', player: PresetPlayer): void;
  23 + (eventName: 'play', player: PresetPlayer): void;
  24 + (eventName: 'pause', player: PresetPlayer): void;
  25 + (eventName: 'ended', player: PresetPlayer): void;
  26 + (eventName: 'onUnmounted', player: PresetPlayer): void;
  27 + }>();
  28 +
  29 + function getStreamTypeByUrl(url = ''): StreamType | undefined {
  30 + url = url || '';
  31 + if (url.endsWith('.m3u8')) return 'hls';
  32 + else if (url.endsWith('.mp4')) return 'mp4';
  33 + else if (url.endsWith('.flv')) {
  34 + return 'flv';
  35 + } else return;
  36 + }
  37 +
  38 + const getPluginByStreamType = (): IPlayerOptions => {
  39 + let { url, withToken } = props;
  40 + let { streamType } = props;
  41 + streamType = streamType === 'auto' ? getStreamTypeByUrl(url)! : streamType;
  42 +
  43 + const liveConfig = {
  44 + targetLatency: 10,
  45 + maxLatency: 20,
  46 + disconnectTime: 0,
  47 + fetchOptions: withToken
  48 + ? {
  49 + headers: {
  50 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,
  51 + },
  52 + }
  53 + : {},
  54 + };
  55 + const config: IPlayerOptions = {
  56 + flv: liveConfig,
  57 + hls: liveConfig,
  58 + };
  59 + switch (streamType) {
  60 + case 'hls':
  61 + config.plugins = [HlsPlugin];
  62 + break;
  63 + case 'mp4':
  64 + config.plugins = [Mp4Plugin];
  65 + break;
  66 + case 'flv':
  67 + config.plugins = [FlvPlugin];
  68 + break;
  69 + }
  70 + return config;
  71 + };
  72 +
  73 + const videoElRef = shallowRef<Nullable<HTMLDivElement>>();
  74 +
  75 + const playerRef = shallowRef<Nullable<PresetPlayer>>();
  76 +
  77 + const propsRef = ref<XGPlayerProps>({});
  78 +
  79 + const getPlayerConfig = computed<IPlayerOptions>(() => {
  80 + const { url, autoPlay, config } = props;
  81 +
  82 + const basicConfig: IPlayerOptions = {
  83 + ...config,
  84 + ...propsRef,
  85 + url,
  86 + lang: 'zh',
  87 + isLive: true,
  88 + autoplay: autoPlay,
  89 + autoplayMuted: autoPlay,
  90 + ...getPluginByStreamType(),
  91 + };
  92 + return basicConfig;
  93 + });
  94 +
  95 + function onDecodeError() {
  96 + console.warn('player happend decode error');
  97 + // playerRef.value?.destroy?.();
  98 + // initializePlayer();
  99 + playerRef.value?.switchURL(props.url!);
  100 + }
  101 +
  102 + function initializePlayer() {
  103 + if (unref(playerRef)) {
  104 + playerRef.value?.destroy?.();
  105 + playerRef.value = null;
  106 + }
  107 +
  108 + const config = toRaw(unref(getPlayerConfig));
  109 +
  110 + if (!unref(videoElRef)) return;
  111 +
  112 + const player = (playerRef.value = new Player(Object.assign(config, { el: unref(videoElRef) })));
  113 +
  114 + player.on(Events.READY, () => {
  115 + emits('ready', player);
  116 + });
  117 +
  118 + player.setEventsMiddleware({
  119 + error: (event, callback) => {
  120 + const code = (
  121 + event as unknown as {
  122 + error: MediaError;
  123 + }
  124 + ).error.code;
  125 + if (code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
  126 + if (!props.url) {
  127 + return;
  128 + }
  129 + callback();
  130 + return;
  131 + }
  132 +
  133 + if (code === MediaError.MEDIA_ERR_DECODE) {
  134 + // 视频流可以播放 中途解码失败重载
  135 + if (playerRef.value?.isPlaying) {
  136 + onDecodeError();
  137 + }
  138 + return;
  139 + }
  140 +
  141 + callback();
  142 + },
  143 + });
  144 +
  145 + player.on(Events.PAUSE, () => {
  146 + emits('pause', player);
  147 + });
  148 +
  149 + player.on(Events.ENDED, () => {
  150 + emits('ended', player);
  151 + });
  152 +
  153 + player.on(Events.PLAY, () => {
  154 + emits('play', player);
  155 + });
  156 + }
  157 +
  158 + onMounted(() => {
  159 + initializePlayer();
  160 + });
  161 +
  162 + onUnmounted(() => {
  163 + emits('onUnmounted', unref(playerRef)!);
  164 + playerRef.value?.destroy?.();
  165 + });
  166 +
  167 + watch(
  168 + () => props.url,
  169 + () => {
  170 + initializePlayer();
  171 + }
  172 + );
  173 +
  174 + defineExpose({
  175 + getPlayerInstance: () => unref(playerRef),
  176 + });
  177 +</script>
  178 +
  179 +<template>
  180 + <div ref="videoElRef"></div>
  181 +</template>
  1 +import { IPlayerOptions } from 'xgplayer/es/player';
  2 +
  3 +export type StreamType = 'flv' | 'mp4' | 'hls' | 'auto';
  4 +export interface XGPlayerProps {
  5 + streamType?: StreamType;
  6 + autoPlay?: boolean;
  7 + url?: string;
  8 + withToken?: boolean;
  9 + config?: Omit<IPlayerOptions, 'url'>;
  10 +}
  1 +export const createOrganizationSearch = () => {
  2 + return {
  3 + showSearch: true,
  4 + filterTreeNode: (value, options) => {
  5 + const { props } = options || {};
  6 +
  7 + if (!value) {
  8 + return true;
  9 + }
  10 + let { name } = props || {};
  11 + value = value?.toLowerCase();
  12 + name = name?.toLowerCase();
  13 + return name.includes(value);
  14 + },
  15 + };
  16 +};
1 -<template>  
2 - <div>  
3 - <BasicModal  
4 - v-bind="$attrs"  
5 - width="55rem"  
6 - destroyOnClose  
7 - :height="heightNum"  
8 - @register="register"  
9 - title="视频预览"  
10 - :showOkBtn="false"  
11 - @cancel="handleCancel"  
12 - >  
13 - <div  
14 - class="flex items-center justify-center bg-dark-900 w-full h-full min-h-96 video-container"  
15 - >  
16 - <BasicVideoPlay  
17 - v-if="showVideo"  
18 - :options="(options as any)"  
19 - :withToken="withToken"  
20 - @on-unmounted="handleCloseFlvPlayUrl"  
21 - />  
22 - </div>  
23 - </BasicModal>  
24 - </div>  
25 -</template>  
26 -<script setup lang="ts">  
27 - import { ref, reactive, unref } from 'vue';  
28 - import { BasicModal, useModalInner } from '/@/components/Modal';  
29 - import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';  
30 - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';  
31 - import { AccessMode } from './config.data';  
32 - import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager';  
33 - import { isRtspProtocol } from '/@/components/Video/src/utils';  
34 - import { VideoJsPlayerOptions } from 'video.js';  
35 - import { useFingerprint } from '/@/utils/useFingerprint';  
36 - import { GetResult } from '@fingerprintjs/fingerprintjs';  
37 -  
38 - const heightNum = ref(800);  
39 - const showVideo = ref(false);  
40 -  
41 - const playUrl = ref('');  
42 -  
43 - const withToken = ref(false);  
44 -  
45 - const videoId = ref<string>();  
46 -  
47 - const fingerprintResult = ref<Nullable<GetResult>>(null);  
48 -  
49 - const options = reactive<VideoJsPlayerOptions>({  
50 - width: '100%' as unknown as number,  
51 - height: 384 as unknown as number,  
52 - autoplay: true,  
53 - });  
54 -  
55 - const setSources = (url: string, fingerprintResult: GetResult) => {  
56 - const flag = isRtspProtocol(url);  
57 - options.sources = [  
58 - {  
59 - src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url,  
60 - type: getVideoTypeByUrl(url),  
61 - },  
62 - ];  
63 - };  
64 -  
65 - const { getResult } = useFingerprint();  
66 - const [register] = useModalInner(  
67 - async (data: { record: CameraModel | StreamingManageRecord }) => {  
68 - const { record } = data;  
69 - videoId.value = record.id || '';  
70 - const result = await getResult();  
71 - fingerprintResult.value = result;  
72 - if (record.accessMode === AccessMode.ManuallyEnter) {  
73 - if ((record as CameraModel).videoUrl) {  
74 - if (isRtspProtocol((record as CameraModel).videoUrl)) {  
75 - playUrl.value = (record as CameraModel).videoUrl;  
76 - closeFlvPlay(unref(playUrl), result.visitorId);  
77 - withToken.value = true;  
78 - }  
79 - setSources((record as CameraModel).videoUrl, result);  
80 - }  
81 - } else {  
82 - try {  
83 - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);  
84 - setSources(url, result);  
85 - } catch (error) {}  
86 - }  
87 - showVideo.value = true;  
88 - }  
89 - );  
90 -  
91 - const handleCloseFlvPlayUrl = () => {  
92 - if (isRtspProtocol(unref(playUrl))) {  
93 - closeFlvPlay(unref(playUrl)!, unref(fingerprintResult)!.visitorId!);  
94 - }  
95 - };  
96 -  
97 - const handleCancel = () => {  
98 - showVideo.value = false;  
99 - withToken.value = false;  
100 - };  
101 -</script>  
102 -  
103 -<style lang="less" scoped>  
104 - .video-container:deep(.vben-basic-video-play) {  
105 - min-height: 13rem;  
106 - }  
107 -  
108 - .video-container:deep(.video-js) {  
109 - min-height: 13rem;  
110 - }  
111 -</style>  
@@ -80,8 +80,7 @@ @@ -80,8 +80,7 @@
80 </BasicTable> 80 </BasicTable>
81 </PageWrapper> 81 </PageWrapper>
82 <CameraDrawer @register="registerDrawer" @success="handleSuccess" /> 82 <CameraDrawer @register="registerDrawer" @success="handleSuccess" />
83 - <VideoPreviewModal @register="registerModal" />  
84 - <VideoModal @register="registerModal1" /> 83 + <VideoModal @register="registerModal" />
85 <GBTDrawer @register="registerGBTDrawer" @success="handleGBTSuccess" /> 84 <GBTDrawer @register="registerGBTDrawer" @success="handleGBTSuccess" />
86 </div> 85 </div>
87 </template> 86 </template>
@@ -103,14 +102,16 @@ @@ -103,14 +102,16 @@
103 CameraPermission, 102 CameraPermission,
104 VideoPlatformEnum, 103 VideoPlatformEnum,
105 accessModeConfig, 104 accessModeConfig,
  105 + getPlayUrl,
106 } from './config.data'; 106 } from './config.data';
107 - import VideoPreviewModal from './DialogPreviewVideo.vue';  
108 import { useModal } from '/@/components/Modal'; 107 import { useModal } from '/@/components/Modal';
109 import { Authority } from '/@/components/Authority'; 108 import { Authority } from '/@/components/Authority';
110 import { useBatchDelete } from '/@/hooks/web/useBatchDelete'; 109 import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
111 import { Popconfirm } from 'ant-design-vue'; 110 import { Popconfirm } from 'ant-design-vue';
112 import { Tag } from 'ant-design-vue'; 111 import { Tag } from 'ant-design-vue';
113 import VideoModal from '/@/views/device/list/cpns/tabs/VideoChannel/videoModal.vue'; 112 import VideoModal from '/@/views/device/list/cpns/tabs/VideoChannel/videoModal.vue';
  113 + import { VideoCancelModalParamsType } from '/@/views/device/list/cpns/tabs/VideoChannel/config';
  114 + import { CameraRecord } from '/@/api/camera/model/cameraModel';
114 115
115 export default defineComponent({ 116 export default defineComponent({
116 components: { 117 components: {
@@ -119,7 +120,6 @@ @@ -119,7 +120,6 @@
119 BasicTable, 120 BasicTable,
120 TableAction, 121 TableAction,
121 CameraDrawer, 122 CameraDrawer,
122 - VideoPreviewModal,  
123 VideoModal, 123 VideoModal,
124 TableImg, 124 TableImg,
125 Authority, 125 Authority,
@@ -131,9 +131,8 @@ @@ -131,9 +131,8 @@
131 setup(_, { emit }) { 131 setup(_, { emit }) {
132 const searchInfo = reactive<Recordable>({}); 132 const searchInfo = reactive<Recordable>({});
133 const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo); 133 const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
134 - const [registerModal, { openModal }] = useModal(); //手动输入  
135 134
136 - const [registerModal1, { openModal: openModal1 }] = useModal(); //流媒体获取 135 + const [registerModal, { openModal }] = useModal(); //流媒体获取
137 // 表格hooks 136 // 表格hooks
138 const [registerTable, { reload, setProps, clearSelectedRowKeys }] = useTable({ 137 const [registerTable, { reload, setProps, clearSelectedRowKeys }] = useTable({
139 title: '视频列表', 138 title: '视频列表',
@@ -197,28 +196,25 @@ @@ -197,28 +196,25 @@
197 searchInfo.organizationId = organizationId; 196 searchInfo.organizationId = organizationId;
198 handleSuccess(); 197 handleSuccess();
199 }; 198 };
200 - const handleViewVideo = (record) => {  
201 - const { videoPlatformDTO } = record; 199 + const handleViewVideo = (record: CameraRecord) => {
  200 + const { videoPlatformDTO, params } = record;
202 const { type } = videoPlatformDTO || {}; 201 const { type } = videoPlatformDTO || {};
203 - const commonRecord = {  
204 - isUpdate: true,  
205 - record,  
206 - };  
207 - // 新增一个判断,GBT28181也弹出云台控制界面  
208 - // if (record.accessMode === AccessMode.Streaming && type === VideoPlatformEnum.ISC) {  
209 - // openModal1(true, commonRecord);  
210 - // } else if (record.accessMode === AccessMode.GBT28181) {  
211 - // openModal1(true, { ...commonRecord, ifShowGBT: true });  
212 - // } else openModal(true, commonRecord);  
213 202
214 - if (  
215 - record.accessMode === AccessMode.ManuallyEnter ||  
216 - (record.accessMode === AccessMode.Streaming && type === VideoPlatformEnum.FLUORITE)  
217 - ) {  
218 - openModal(true, commonRecord);  
219 - } else if (record.accessMode === AccessMode.GBT28181) {  
220 - openModal1(true, { ...commonRecord, ifShowGBT: true });  
221 - } else openModal1(true, commonRecord); 203 + openModal(true, {
  204 + record: {
  205 + id: record.id,
  206 + canControl:
  207 + [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode) &&
  208 + type !== VideoPlatformEnum.FLUORITE,
  209 + isGBT: record.accessMode === AccessMode.GBT28181,
  210 + channelId: params?.channelNo,
  211 + tbDeviceId: params?.deviceId,
  212 + getPlayUrl: async () => {
  213 + const result = await getPlayUrl(record);
  214 + return result;
  215 + },
  216 + } as VideoCancelModalParamsType,
  217 + });
222 }; 218 };
223 219
224 const handleSwitchMode = () => { 220 const handleSwitchMode = () => {
@@ -252,7 +248,6 @@ @@ -252,7 +248,6 @@
252 organizationIdTreeRef, 248 organizationIdTreeRef,
253 handleViewVideo, 249 handleViewVideo,
254 registerModal, 250 registerModal,
255 - registerModal1,  
256 AccessMode, 251 AccessMode,
257 handleSwitchMode, 252 handleSwitchMode,
258 CameraPermission, 253 CameraPermission,
@@ -3,24 +3,21 @@ @@ -3,24 +3,21 @@
3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue'; 3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
4 import { onMounted, reactive, Ref, 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, closeFlvPlay, getFlvPlayUrl } from '/@/api/camera/cameraManager'; 6 + import { cameraPage, closeFlvPlay } 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';
10 import GBTDrawer from './components/GBTDrawer.vue'; 10 import GBTDrawer from './components/GBTDrawer.vue';
11 import { useDrawer } from '/@/components/Drawer'; 11 import { useDrawer } from '/@/components/Drawer';
12 - import { AccessMode, CameraPermission, PageMode } from './config.data'; 12 + import { CameraPermission, getPlayUrl, PageMode } from './config.data';
13 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; 13 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
14 - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';  
15 import { buildUUID } from '/@/utils/uuid'; 14 import { buildUUID } from '/@/utils/uuid';
16 - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';  
17 - import { VideoJsPlayerOptions } from 'video.js';  
18 import { getBoundingClientRect } from '/@/utils/domUtils'; 15 import { getBoundingClientRect } from '/@/utils/domUtils';
19 import { Authority } from '/@/components/Authority'; 16 import { Authority } from '/@/components/Authority';
20 import { isRtspProtocol } from '/@/components/Video/src/utils'; 17 import { isRtspProtocol } from '/@/components/Video/src/utils';
21 import { useFingerprint } from '/@/utils/useFingerprint'; 18 import { useFingerprint } from '/@/utils/useFingerprint';
22 import { GetResult } from '@fingerprintjs/fingerprintjs'; 19 import { GetResult } from '@fingerprintjs/fingerprintjs';
23 - import { getVideoControlStart } from '/@/api/device/videoChannel'; 20 + import XGPlayer from '/@/components/Video/src/XGPlayer.vue';
24 21
25 const props = defineProps({ 22 const props = defineProps({
26 mode: { 23 mode: {
@@ -29,17 +26,12 @@ @@ -29,17 +26,12 @@
29 }, 26 },
30 }); 27 });
31 type CameraRecordItem = CameraRecord & { 28 type CameraRecordItem = CameraRecord & {
  29 + placeholder?: boolean;
32 canPlay?: boolean; 30 canPlay?: boolean;
33 isTransform?: boolean; 31 isTransform?: boolean;
34 withToken?: boolean; 32 withToken?: boolean;
35 - videoPlayerOptions?: VideoJsPlayerOptions;  
36 playSourceUrl?: string; 33 playSourceUrl?: string;
37 - };  
38 -  
39 - const basicVideoPlayOptions: VideoJsPlayerOptions = {  
40 - width: '100%' as unknown as number,  
41 - height: '100%' as unknown as number,  
42 - autoplay: true, 34 + streamType?: string;
43 }; 35 };
44 36
45 const emit = defineEmits(['switchMode']); 37 const emit = defineEmits(['switchMode']);
@@ -88,10 +80,7 @@ @@ -88,10 +80,7 @@
88 80
89 for (const item of unref(cameraList)) { 81 for (const item of unref(cameraList)) {
90 (item as CameraRecordItem).isTransform = false; 82 (item as CameraRecordItem).isTransform = false;
91 - (item as CameraRecordItem).videoPlayerOptions = {  
92 - ...basicVideoPlayOptions,  
93 - };  
94 - beforeVideoPlay(item as CameraRecordItem, result); 83 + beforeVideoPlay(item as CameraRecordItem);
95 } 84 }
96 } catch (error) { 85 } catch (error) {
97 } finally { 86 } finally {
@@ -100,75 +89,12 @@ @@ -100,75 +89,12 @@
100 }; 89 };
101 90
102 const { getResult } = useFingerprint(); 91 const { getResult } = useFingerprint();
103 - const beforeVideoPlay = async (record: CameraRecordItem, fingerprintResult: GetResult) => {  
104 - if (record.accessMode === AccessMode.ManuallyEnter) {  
105 - if (record.videoUrl) {  
106 - const isFlvPlay = isRtspProtocol(record.videoUrl);  
107 - const type = getVideoTypeByUrl(record.videoUrl);  
108 - record.playSourceUrl = record.videoUrl;  
109 - if (isFlvPlay) {  
110 - // handleFlvPlayerUnload(record, fingerprintResult!.visitorId);  
111 - record.playSourceUrl = getFlvPlayUrl(record.videoUrl, fingerprintResult.visitorId);  
112 - record.withToken = true;  
113 - }  
114 -  
115 - (record as CameraRecordItem).videoPlayerOptions = {  
116 - ...basicVideoPlayOptions,  
117 - sources: [  
118 - {  
119 - src: record.playSourceUrl,  
120 - type,  
121 - },  
122 - ],  
123 - };  
124 - record.isTransform = true;  
125 - }  
126 - }  
127 - if (record.accessMode === AccessMode.GBT28181) {  
128 - try {  
129 - const { params } = record;  
130 - if (params?.channelNo && params.deviceId) {  
131 - const result = await getVideoControlStart({  
132 - deviceId: params.deviceId,  
133 - channelId: params.channelNo,  
134 - });  
135 -  
136 - (record as CameraRecordItem).videoPlayerOptions = {  
137 - ...basicVideoPlayOptions,  
138 - sources: [{ src: result.data.flv, type: getVideoTypeByUrl(result.data.flv) }],  
139 - };  
140 - }  
141 - } finally {  
142 - record.isTransform = true;  
143 - }  
144 - }  
145 - if (record.accessMode === AccessMode.Streaming) {  
146 - try {  
147 - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);  
148 - const index = unref(cameraList).findIndex((item) => item.id === record.id);  
149 - if (~index) {  
150 - const oldRecord = unref(cameraList).at(index)!;  
151 - unref(cameraList)[index] = {  
152 - ...oldRecord,  
153 -  
154 - videoPlayerOptions: {  
155 - ...basicVideoPlayOptions,  
156 - sources: [  
157 - {  
158 - src: url,  
159 - type: getVideoTypeByUrl(url),  
160 - },  
161 - ],  
162 - } as any,  
163 - isTransform: true,  
164 - };  
165 - }  
166 - } catch (error) {  
167 - } finally {  
168 - const index = unref(cameraList).findIndex((item) => item.id === record.id);  
169 - if (~index) unref(cameraList)[index].isTransform = true;  
170 - }  
171 - } 92 + const beforeVideoPlay = async (record: CameraRecordItem) => {
  93 + if (!record.accessMode) return;
  94 + const { url, type } = await getPlayUrl(record);
  95 + record.playSourceUrl = url;
  96 + record.streamType = type;
  97 + record.isTransform = true;
172 }; 98 };
173 99
174 const gridLayout = ref({ gutter: 1, column: 2 }); 100 const gridLayout = ref({ gutter: 1, column: 2 });
@@ -312,11 +238,11 @@ @@ -312,11 +238,11 @@
312 ref="listEl" 238 ref="listEl"
313 :loading="loading" 239 :loading="loading"
314 :data-source="cameraList" 240 :data-source="cameraList"
315 - class="bg-light-50 w-full !h-full dark:bg-dark-900 split-mode-list" 241 + class="bg-light-50 w-full !h-full dark:bg-dark-900 split-mode-list full"
316 :grid="(gridLayout as any)" 242 :grid="(gridLayout as any)"
317 :style="{ '--height': `${100 / pagination.colNumber}%` }" 243 :style="{ '--height': `${100 / pagination.colNumber}%` }"
318 > 244 >
319 - <template #renderItem="{ item }"> 245 + <template #renderItem="{ item }: { item: CameraRecordItem }">
320 <List.Item> 246 <List.Item>
321 <div class="box-border w-full !h-full p-1px"> 247 <div class="box-border w-full !h-full p-1px">
322 <div 248 <div
@@ -332,17 +258,19 @@ @@ -332,17 +258,19 @@
332 v-show="!item.isTransform" 258 v-show="!item.isTransform"
333 :spinning="!item.isTransform" 259 :spinning="!item.isTransform"
334 /> 260 />
335 - <BasicVideoPlay 261 + <XGPlayer
336 v-if="item.isTransform" 262 v-if="item.isTransform"
337 - :options="item.videoPlayerOptions" 263 + :url="item.playSourceUrl"
  264 + :stream-type="item.streamType"
338 :with-token="item.withToken" 265 :with-token="item.withToken"
  266 + :config="{ width: '100%', height: '100%' }"
339 @on-unmounted="handleCloseFlvPlayUrl(item)" 267 @on-unmounted="handleCloseFlvPlayUrl(item)"
340 /> 268 />
341 <div 269 <div
342 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center" 270 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center"
343 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" 271 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
344 > 272 >
345 - <span>{{ item.name }}</span> 273 + <span>{{ item.name }}{{ pagination.colNumber }}</span>
346 </div> 274 </div>
347 </div> 275 </div>
348 </div> 276 </div>
@@ -366,8 +294,10 @@ @@ -366,8 +294,10 @@
366 height: 100%; 294 height: 100%;
367 } 295 }
368 296
369 - .split-screen-mode:deep(.ant-spin-container) {  
370 - height: 100% !important; 297 + .full {
  298 + :deep(.ant-spin-container) {
  299 + height: 100% !important;
  300 + }
371 } 301 }
372 302
373 .video-container { 303 .video-container {
@@ -6,7 +6,6 @@ import { h } from 'vue'; @@ -6,7 +6,6 @@ import { h } from 'vue';
6 import SnHelpMessage from './SnHelpMessage.vue'; 6 import SnHelpMessage from './SnHelpMessage.vue';
7 import SnHelpMessage1 from './SnHelpMessage1.vue'; 7 import SnHelpMessage1 from './SnHelpMessage1.vue';
8 import { OrgTreeSelect } from '../../common/OrgTreeSelect'; 8 import { OrgTreeSelect } from '../../common/OrgTreeSelect';
9 -import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';  
10 import { createImgPreview } from '/@/components/Preview'; 9 import { createImgPreview } from '/@/components/Preview';
11 import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; 10 import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter';
12 import { findDictItemByCode } from '/@/api/system/dict'; 11 import { findDictItemByCode } from '/@/api/system/dict';
@@ -15,7 +14,24 @@ import { DataSourceField } from '../../visual/packages/config/common.config'; @@ -15,7 +14,24 @@ import { DataSourceField } from '../../visual/packages/config/common.config';
15 import { getMeetTheConditionsDevice } from '/@/api/dataBoard'; 14 import { getMeetTheConditionsDevice } from '/@/api/dataBoard';
16 import { useMessage } from '/@/hooks/web/useMessage'; 15 import { useMessage } from '/@/hooks/web/useMessage';
17 import { TransportTypeEnum } from '../../device/profiles/components/TransportDescript/const'; 16 import { TransportTypeEnum } from '../../device/profiles/components/TransportDescript/const';
18 -import { getDeviceChannelList } from '/@/api/camera/cameraManager'; 17 +import {
  18 + getDeviceChannelList,
  19 + getFlvPlayUrl,
  20 + getStreamingPlayUrl,
  21 +} from '/@/api/camera/cameraManager';
  22 +import { CameraRecord } from '/@/api/camera/model/cameraModel';
  23 +import { isRtspProtocol } from '/@/components/Video/src/utils';
  24 +import { useFingerprint } from '/@/utils/useFingerprint';
  25 +import { getVideoControlStart } from '/@/api/device/videoChannel';
  26 +import { StreamType as PlayerStreamType } from '/@/components/Video/src/types';
  27 +
  28 +interface FileItem {
  29 + uid: string;
  30 + name: string;
  31 + status?: string;
  32 + response?: string;
  33 + url?: string;
  34 +}
19 35
20 useComponentRegister('OrgTreeSelect', OrgTreeSelect); 36 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
21 37
@@ -321,7 +337,7 @@ export const formSchema: QFormSchema[] = [ @@ -321,7 +337,7 @@ export const formSchema: QFormSchema[] = [
321 if (data) 337 if (data)
322 return data.map((item) => ({ 338 return data.map((item) => ({
323 ...item, 339 ...item,
324 - label: item.name, 340 + label: item.name || item.channelId,
325 value: item.channelId, 341 value: item.channelId,
326 })); 342 }));
327 } catch (error) {} 343 } catch (error) {}
@@ -604,3 +620,37 @@ export const formGBTSchema: QFormSchema[] = [ @@ -604,3 +620,37 @@ export const formGBTSchema: QFormSchema[] = [
604 values.allDevice === GBT28181DeviceSelectMethod.PARTIAL, 620 values.allDevice === GBT28181DeviceSelectMethod.PARTIAL,
605 }, 621 },
606 ]; 622 ];
  623 +
  624 +export async function getPlayUrl(
  625 + params: CameraRecord
  626 +): Promise<{ url: string; type: PlayerStreamType }> {
  627 + const { accessMode } = params;
  628 + if (accessMode === AccessMode.ManuallyEnter) {
  629 + const { videoUrl } = params;
  630 + if (params.videoUrl) {
  631 + const isRTSPPlay = isRtspProtocol(videoUrl);
  632 +
  633 + if (isRTSPPlay) {
  634 + const { getResult } = useFingerprint();
  635 + const fingerprint = await getResult();
  636 + return { url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), type: 'flv' };
  637 + } else {
  638 + return { url: videoUrl, type: 'auto' };
  639 + }
  640 + }
  641 + } else if (accessMode === AccessMode.GBT28181) {
  642 + const { deviceId, channelNo } = params?.params || {};
  643 + const result = await getVideoControlStart({ channelId: channelNo!, deviceId: deviceId! });
  644 + return { url: result.data.flv, type: 'flv' };
  645 + } else {
  646 + const { id, playProtocol } = params;
  647 + const result = await getStreamingPlayUrl(id);
  648 + const type: PlayerStreamType =
  649 + playProtocol === FluoriteMideaProtocolEnum.FLV
  650 + ? 'flv'
  651 + : playProtocol === FluoriteMideaProtocolEnum.HLS
  652 + ? 'hls'
  653 + : 'auto';
  654 + return { url: result.data.url, type };
  655 + }
  656 +}
@@ -165,6 +165,14 @@ @@ -165,6 +165,14 @@
165 values.icon = file.url || null; 165 values.icon = file.url || null;
166 } 166 }
167 values = { ...positionState, ...values }; 167 values = { ...positionState, ...values };
  168 +
  169 + Object.assign(values, {
  170 + code:
  171 + values?.tcpDeviceProtocol === TCPProtocolTypeEnum.CUSTOM
  172 + ? values?.code
  173 + : values?.addressCode,
  174 + });
  175 +
168 delete values.deviceAddress; 176 delete values.deviceAddress;
169 emit('next', values); 177 emit('next', values);
170 // 获取输入的数据 178 // 获取输入的数据
@@ -419,6 +427,7 @@ @@ -419,6 +427,7 @@
419 const file = (value.icon || []).at(0) || {}; 427 const file = (value.icon || []).at(0) || {};
420 value.icon = file.url || null; 428 value.icon = file.url || null;
421 } 429 }
  430 +
422 return { 431 return {
423 ...value, 432 ...value,
424 ...(value?.code || value?.addressCode 433 ...(value?.code || value?.addressCode
1 import { h } from 'vue'; 1 import { h } from 'vue';
2 import { BasicColumn, FormSchema } from '/@/components/Table'; 2 import { BasicColumn, FormSchema } from '/@/components/Table';
3 import { Tag } from 'ant-design-vue'; 3 import { Tag } from 'ant-design-vue';
4 -import { withInstall } from '/@/utils/index';  
5 -  
6 -import VideoPlay from './video.vue';  
7 -export const Video = withInstall(VideoPlay); 4 +export type VideoCancelModalParamsType = {
  5 + canControl?: boolean;
  6 + isGBT?: boolean;
  7 + tbDeviceId?: string;
  8 + channelId?: string;
  9 + id?: string;
  10 + playerProps?: Recordable;
  11 + getPlayUrl: () => Promise<Record<'url' | 'type', string>>;
  12 +};
8 13
9 //视频通道权限标识枚举 14 //视频通道权限标识枚举
10 export enum GBT28181_DEVICE_PERMISSION_ENUM { 15 export enum GBT28181_DEVICE_PERMISSION_ENUM {
@@ -50,7 +55,7 @@ export const configColumns: BasicColumn[] = [ @@ -50,7 +55,7 @@ export const configColumns: BasicColumn[] = [
50 return h( 55 return h(
51 Tag, 56 Tag,
52 { 57 {
53 - color: text === ChannelStatusEnum.ONLINE ? 'green' : 'blue', 58 + color: text === ChannelStatusEnum.ONLINE ? 'green' : 'red',
54 }, 59 },
55 () => (text === ChannelStatusEnum.ONLINE ? '在线' : '离线') 60 () => (text === ChannelStatusEnum.ONLINE ? '在线' : '离线')
56 ); 61 );
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 <a-button type="primary" @click="handleSyncPlay">通道同步</a-button> 8 <a-button type="primary" @click="handleSyncPlay">通道同步</a-button>
9 </Authority> 9 </Authority>
10 </template> 10 </template>
11 - <template #hasAudio="{ record }"> 11 + <template #hasAudio="{ record }: { record: VideoChannelItemType }">
12 <Switch 12 <Switch
13 :checked="record.status === 1" 13 :checked="record.status === 1"
14 :loading="record.pendingStatus" 14 :loading="record.pendingStatus"
@@ -25,6 +25,7 @@ @@ -25,6 +25,7 @@
25 auth: GBT28181_DEVICE_PERMISSION_ENUM.PLAY_SYNC, 25 auth: GBT28181_DEVICE_PERMISSION_ENUM.PLAY_SYNC,
26 icon: 'ant-design:play-circle-outlined', 26 icon: 'ant-design:play-circle-outlined',
27 onClick: handlePlay.bind(null, record), 27 onClick: handlePlay.bind(null, record),
  28 + ifShow: record?.status === 'ONLINE', //在线则显示播放按钮
28 }, 29 },
29 { 30 {
30 label: '停止', 31 label: '停止',
@@ -41,16 +42,22 @@ @@ -41,16 +42,22 @@
41 </template> 42 </template>
42 43
43 <script lang="ts" setup> 44 <script lang="ts" setup>
44 - import { configColumns, searchFormSchema, GBT28181_DEVICE_PERMISSION_ENUM } from './config'; 45 + import {
  46 + configColumns,
  47 + searchFormSchema,
  48 + GBT28181_DEVICE_PERMISSION_ENUM,
  49 + VideoCancelModalParamsType,
  50 + } from './config';
45 import { BasicTable, useTable, TableAction } from '/@/components/Table'; 51 import { BasicTable, useTable, TableAction } from '/@/components/Table';
46 import { Switch } from 'ant-design-vue'; 52 import { Switch } from 'ant-design-vue';
47 import { DeviceRecord } from '/@/api/device/model/deviceModel'; 53 import { DeviceRecord } from '/@/api/device/model/deviceModel';
48 import VideoModal from './videoModal.vue'; 54 import VideoModal from './videoModal.vue';
49 import { useModal } from '/@/components/Modal'; 55 import { useModal } from '/@/components/Modal';
50 import { useMessage } from '/@/hooks/web/useMessage'; 56 import { useMessage } from '/@/hooks/web/useMessage';
51 - import { getVideoChannelList } from '/@/api/device/videoChannel'; 57 + import { getVideoChannelList, getVideoControlStart } from '/@/api/device/videoChannel';
52 import { stopOnDemandVideoApiGet, syncVideoApiGet } from '/@/api/camera/cameraManager'; 58 import { stopOnDemandVideoApiGet, syncVideoApiGet } from '/@/api/camera/cameraManager';
53 import { Authority } from '/@/components/Authority'; 59 import { Authority } from '/@/components/Authority';
  60 + import { VideoChannelItemType } from '/@/api/device/model/videoChannelModel';
54 61
55 const props = defineProps<{ deviceDetail: DeviceRecord }>(); 62 const props = defineProps<{ deviceDetail: DeviceRecord }>();
56 63
@@ -91,10 +98,20 @@ @@ -91,10 +98,20 @@
91 } 98 }
92 }; 99 };
93 100
94 - const handlePlay = (record: Recordable) => { 101 + const handlePlay = (record: VideoChannelItemType) => {
  102 + const { channelId } = record;
95 openModal(true, { 103 openModal(true, {
96 - record,  
97 - ifShowGBT: true, 104 + record: {
  105 + canControl: true,
  106 + isGBT: true,
  107 + channelId,
  108 + tbDeviceId: props.deviceDetail.tbDeviceId,
  109 + getPlayUrl: async () => {
  110 + const { deviceId, channelId } = record;
  111 + const result = await getVideoControlStart({ deviceId, channelId });
  112 + return { type: 'flv', url: result.data.flv };
  113 + },
  114 + } as VideoCancelModalParamsType,
98 }); 115 });
99 }; 116 };
100 117
1 <script lang="ts" setup> 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 { Tooltip } from 'ant-design-vue'; 2 import { Tooltip } from 'ant-design-vue';
6 - import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';  
7 - import { useDesign } from '/@/hooks/web/useDesign';  
8 - import { getJwtToken, getShareJwtToken } from '/@/utils/auth';  
9 - import { isShareMode } from '/@/views/sys/share/hook';  
10 - import 'videojs-flvjs-es6'; 3 + import { ref, unref } from 'vue';
11 import { QuestionCircleOutlined } from '@ant-design/icons-vue'; 4 import { QuestionCircleOutlined } from '@ant-design/icons-vue';
12 import { 5 import {
13 CaretUpOutlined, 6 CaretUpOutlined,
@@ -19,106 +12,50 @@ @@ -19,106 +12,50 @@
19 ZoomOutOutlined, 12 ZoomOutOutlined,
20 } from '@ant-design/icons-vue'; 13 } from '@ant-design/icons-vue';
21 import { Button, Slider } from 'ant-design-vue'; 14 import { Button, Slider } from 'ant-design-vue';
22 - import { nextTick } from 'vue';  
23 import { controlling } from '/@/api/camera/cameraManager'; 15 import { controlling } from '/@/api/camera/cameraManager';
24 import { setVideoControl } from '/@/api/device/videoChannel'; 16 import { setVideoControl } from '/@/api/device/videoChannel';
25 -  
26 - const { prefixCls } = useDesign('basic-video-play');  
27 - interface IGbtOption {  
28 - tbDeviceId: string;  
29 - channelId: string;  
30 - } 17 + import XGPlayer from '/@/components/Video/src/XGPlayer.vue';
  18 + import PresetPlayer from 'xgplayer';
31 19
32 const props = defineProps<{ 20 const props = defineProps<{
33 - options?: VideoJsPlayerOptions;  
34 - withToken?: boolean;  
35 - isGBT?: boolean;  
36 - GBTOption: IGbtOption;  
37 - }>();  
38 -  
39 - const emit = defineEmits<{  
40 - (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;  
41 - (event: 'onUnmounted'): void;  
42 - }>();  
43 -  
44 - const videoPlayEl = ref<HTMLVideoElement>();  
45 -  
46 - const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();  
47 -  
48 - const getGBT = computed(() => {  
49 - const { isGBT } = props;  
50 - return isGBT;  
51 - });  
52 -  
53 - const getOptions = computed(() => {  
54 - const { options, withToken } = props;  
55 -  
56 - const defaultOptions: VideoJsPlayerOptions & Recordable = {  
57 - language: 'zh',  
58 - muted: true,  
59 - liveui: true,  
60 - controls: true,  
61 - techOrder: ['html5', 'flvjs'],  
62 - flvjs: {  
63 - mediaDataSource: {  
64 - isLive: true,  
65 - cors: true,  
66 - hasAudio: false,  
67 - withCredentials: false,  
68 - },  
69 - config: {  
70 - headers: {  
71 - ...(withToken  
72 - ? {  
73 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,  
74 - }  
75 - : {}),  
76 - },  
77 - autoCleanupSourceBuffer: true,  
78 - },  
79 - }, 21 + playUrl?: string;
  22 + options?: {
  23 + canControl?: boolean;
  24 + isGBT?: boolean;
  25 + tbDeviceId?: string;
  26 + channelId?: string;
  27 + id?: string;
  28 + playerProps?: Recordable;
80 }; 29 };
81 - return videoJs.mergeOptions(defaultOptions, options);  
82 - }); 30 + }>();
83 31
84 - const getWidthHeight = computed(() => {  
85 - let { width = 300, height = 150 } = unref(getOptions);  
86 - width = isNumber(width) ? (`${width}px` as unknown as number) : width;  
87 - height = isNumber(height) ? (`${height}px` as unknown as number) : height;  
88 - return { width, height } as CSSProperties;  
89 - }); 32 + const playerRef = ref<InstanceType<typeof XGPlayer>>();
90 33
91 // 控制云台控制速度 34 // 控制云台控制速度
92 const sliderValue = ref<number>(123); 35 const sliderValue = ref<number>(123);
93 36
94 - const init = () => {  
95 - if (unref(videoPlayInstance)) unref(videoPlayInstance)?.dispose();  
96 - videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => {  
97 - emit('ready', unref(videoPlayInstance));  
98 - });  
99 - }; 37 + const isPlay = ref(false);
100 38
101 //播放/暂停 39 //播放/暂停
102 const handleClick = () => { 40 const handleClick = () => {
103 - // unref(isPlay) ((true,停止),(false,开启))  
104 - if (unref(isPlay)) unref(videoPlayInstance)?.pause();  
105 - else unref(videoPlayInstance)?.play();  
106 - };  
107 -  
108 - const getId = () => {  
109 - const { options } = props || {};  
110 - const { sources }: any = options || {};  
111 - return sources?.[0]?.id; 41 + const playerInstance = unref(playerRef)?.getPlayerInstance();
  42 + if (playerInstance && playerInstance.paused) {
  43 + unref(playerRef)?.getPlayerInstance()?.play();
  44 + isPlay.value = true;
  45 + } else {
  46 + unref(playerRef)?.getPlayerInstance()?.pause();
  47 + isPlay.value = false;
  48 + }
112 }; 49 };
113 50
114 const handleControl = (action: number, direction: string) => { 51 const handleControl = (action: number, direction: string) => {
115 - const organizationId = getId();  
116 - controlling({ cameraIndexCode: organizationId, action, command: direction }); 52 + const { options } = props;
  53 + controlling({ cameraIndexCode: options?.id, action, command: direction });
117 }; 54 };
118 55
119 // 国标控制 56 // 国标控制
120 const handleGBTControl = (command: string, action?: number | string) => { 57 const handleGBTControl = (command: string, action?: number | string) => {
121 - const { tbDeviceId, channelId } = props.GBTOption; 58 + const { tbDeviceId, channelId } = props.options || {};
122 setVideoControl(tbDeviceId, channelId, { 59 setVideoControl(tbDeviceId, channelId, {
123 command, 60 command,
124 horizonSpeed: action, 61 horizonSpeed: action,
@@ -127,33 +64,9 @@ @@ -127,33 +64,9 @@
127 }); 64 });
128 }; 65 };
129 66
130 - const isPlay = ref<Boolean | null | undefined>(false);  
131 -  
132 - onMounted(async () => {  
133 - init();  
134 - await nextTick();  
135 - // isPlay.value = unref(videoPlayInstance)?.paused();  
136 - videoPlayInstance.value?.on('loadedmetadata', () => {});  
137 - videoPlayInstance.value?.on('waiting', () => {  
138 - isPlay.value = false;  
139 - });  
140 - videoPlayInstance.value?.on('play', () => {  
141 - isPlay.value = true;  
142 - });  
143 - videoPlayInstance.value?.on('playing', () => {  
144 - isPlay.value = true;  
145 - });  
146 - videoPlayInstance.value?.on('pause', () => {  
147 - isPlay.value = false;  
148 - });  
149 - videoPlayInstance.value?.on('ended', () => {  
150 - isPlay.value = false;  
151 - });  
152 - });  
153 -  
154 //长按开始 67 //长按开始
155 - const moveStart = (action) => {  
156 - if (unref(getGBT)) { 68 + const moveStart = (action: string) => {
  69 + if (unref(props.options?.isGBT)) {
157 handleGBTControl(action, unref(sliderValue)); 70 handleGBTControl(action, unref(sliderValue));
158 return; 71 return;
159 } 72 }
@@ -161,46 +74,40 @@ @@ -161,46 +74,40 @@
161 }; 74 };
162 75
163 // 长按结束 76 // 长按结束
164 - const moveStop = (action) => {  
165 - if (unref(getGBT)) { 77 + const moveStop = (action: string) => {
  78 + if (unref(props.options?.isGBT)) {
166 handleGBTControl('STOP', unref(sliderValue)); 79 handleGBTControl('STOP', unref(sliderValue));
167 return; 80 return;
168 } 81 }
169 handleControl(1, action); 82 handleControl(1, action);
170 }; 83 };
171 84
172 - //页面卸载  
173 - onUnmounted(() => {  
174 - unref(videoPlayInstance)?.dispose();  
175 - videoPlayInstance.value = null;  
176 - emit('onUnmounted');  
177 - });  
178 -  
179 - defineExpose({  
180 - reloadPlayer: init,  
181 - getInstance: () => unref(videoPlayInstance),  
182 - }); 85 + const handleVideoStatus = (flag: boolean) => {
  86 + isPlay.value = flag;
  87 + };
183 </script> 88 </script>
184 89
185 <template> 90 <template>
186 - <div  
187 - :class="prefixCls"  
188 - class="!w-full h-full flex"  
189 - :style="getWidthHeight"  
190 - style="min-height: 100%"  
191 - >  
192 - <video  
193 - ref="videoPlayEl"  
194 - class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-8/10 !h-full"  
195 - muted 91 + <div class="!w-full h-full flex" style="min-height: 100%">
  92 + <XGPlayer
  93 + ref="playerRef"
  94 + v-bind="options?.playerProps"
  95 + :url="playUrl"
  96 + auto-play
  97 + :config="{ fluid: true }"
  98 + @play="handleVideoStatus(true)"
  99 + @pause="handleVideoStatus(false)"
  100 + @ended="handleVideoStatus(false)"
  101 + />
  102 +
  103 + <div
  104 + v-if="options?.canControl"
  105 + class="!w-2/10 px-4 bg-white flex justify-center items-center flex-col"
196 > 106 >
197 - </video>  
198 -  
199 - <div class="!w-2/10 bg-white flex items-center flex-col">  
200 <Tooltip> 107 <Tooltip>
201 - <template #title  
202 - >长按按钮调用API,由于视频流媒体提供的流,有一定时延,请耐心等待。</template  
203 - > 108 + <template #title>
  109 + 长按按钮调用API,由于视频流媒体提供的流,有一定时延,请耐心等待。
  110 + </template>
204 <label class="validate-dot">云台控制</label> 111 <label class="validate-dot">云台控制</label>
205 <QuestionCircleOutlined :style="{ fontSize: '14px', marginLeft: '5px' }" /> 112 <QuestionCircleOutlined :style="{ fontSize: '14px', marginLeft: '5px' }" />
206 </Tooltip> 113 </Tooltip>
@@ -248,7 +155,7 @@ @@ -248,7 +155,7 @@
248 <Button class="circle" @click="handleClick" /> 155 <Button class="circle" @click="handleClick" />
249 </div> 156 </div>
250 </div> 157 </div>
251 - <div class="mt-5" v-if="getGBT"> 158 + <div class="mt-5" v-if="options.isGBT">
252 <Slider v-model:value="sliderValue" :min="0" :max="255" style="width: 130px" /> 159 <Slider v-model:value="sliderValue" :min="0" :max="255" style="width: 130px" />
253 </div> 160 </div>
254 <div class="flex justify-center mt-8"> 161 <div class="flex justify-center mt-8">
@@ -274,16 +181,6 @@ @@ -274,16 +181,6 @@
274 </template> 181 </template>
275 182
276 <style lang="less" scoped> 183 <style lang="less" scoped>
277 - @prefix-cls: ~'@{namespace}-basic-video-play';  
278 -  
279 - .@{prefix-cls} {  
280 - .vjs-error-display {  
281 - .vjs-modal-dialog-content::after {  
282 - content: '无法加载视频,原因可能是服务器或网络故障,也可能是格式不支持.';  
283 - }  
284 - }  
285 - }  
286 -  
287 .child { 184 .child {
288 position: absolute; 185 position: absolute;
289 width: 3rem; 186 width: 3rem;
1 <template> 1 <template>
2 - <div>  
3 - <BasicModal  
4 - v-bind="$attrs"  
5 - width="60rem"  
6 - destroyOnClose  
7 - :height="heightNum"  
8 - @register="register"  
9 - title="视频预览"  
10 - :showOkBtn="false"  
11 - @cancel="handleCancel"  
12 - >  
13 - <div  
14 - class="flex items-center justify-center w-full h-full min-h-96 video-container bg-dark-50"  
15 - >  
16 - <VideoPlayer  
17 - ref="videoInstance"  
18 - v-if="showVideo"  
19 - :options="(options as VideoJsPlayerOptions)"  
20 - :withToken="withToken"  
21 - :isGBT="isGBT"  
22 - :GBTOption="GBTOption"  
23 - />  
24 - </div>  
25 - </BasicModal>  
26 - </div> 2 + <BasicModal
  3 + v-bind="$attrs"
  4 + width="60rem"
  5 + destroyOnClose
  6 + :height="800"
  7 + @register="register"
  8 + title="视频预览"
  9 + :showOkBtn="false"
  10 + @cancel="handleCancel"
  11 + >
  12 + <div class="flex items-center justify-center w-full h-full min-h-96 video-container bg-dark-50">
  13 + <VideoPlayer :play-url="playUrl" :options="options" />
  14 + </div>
  15 + </BasicModal>
27 </template> 16 </template>
28 <script setup lang="ts"> 17 <script setup lang="ts">
29 - import { ref, reactive } from 'vue'; 18 + import { ref } from 'vue';
30 import { BasicModal, useModalInner } from '/@/components/Modal'; 19 import { BasicModal, useModalInner } from '/@/components/Modal';
31 - import { VideoJsPlayerOptions } from 'video.js';  
32 import VideoPlayer from './video.vue'; 20 import VideoPlayer from './video.vue';
33 - import { getVideoControlStart } from '/@/api/device/videoChannel';  
34 - import { VideoChannelItemType } from '/@/api/device/model/videoChannelModel';  
35 - import { getVideoTypeByUrl } from '/@/components/Video';  
36 - import { AccessMode } from '/@/views/camera/manage/config.data';  
37 - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';  
38 - 21 + import { VideoCancelModalParamsType } from '/@/views/device/list/cpns/tabs/VideoChannel/config';
39 const emit = defineEmits(['reloadTable', 'register']); 22 const emit = defineEmits(['reloadTable', 'register']);
40 23
41 - const heightNum = ref(800);  
42 - const showVideo = ref(false);  
43 -  
44 - const videoInstance = ref<InstanceType<typeof VideoPlayer>>();  
45 -  
46 - const videoId = ref<string>();  
47 -  
48 - const withToken = ref(false);  
49 -  
50 - const options = reactive<VideoJsPlayerOptions>({  
51 - width: '100%' as unknown as number,  
52 - height: 384 as unknown as number,  
53 - autoplay: true,  
54 - });  
55 - const GBTOption = ref({  
56 - tbDeviceId: '',  
57 - channelId: '',  
58 - });  
59 - const isGBT = ref<boolean>(false);  
60 - 24 + const playUrl = ref<Nullable<string>>();
  25 + const options = ref<Nullable<VideoCancelModalParamsType>>();
61 const [register, { setModalProps }] = useModalInner( 26 const [register, { setModalProps }] = useModalInner(
62 - async (data: { record: VideoChannelItemType }) => {  
63 - const { record, ifShowGBT = false } = data;  
64 - const { params } = record as Recordable;  
65 - videoId.value = record.id || '';  
66 - isGBT.value = ifShowGBT;  
67 - GBTOption.value.tbDeviceId = params?.deviceId ? params?.deviceId : record?.deviceId;  
68 - GBTOption.value.channelId = params?.channelNo ? params?.channelNo : record?.channelId; 27 + async (data: ModalParamsType<VideoCancelModalParamsType>) => {
  28 + const { record } = data;
69 try { 29 try {
  30 + playUrl.value = null;
  31 + options.value = null;
70 setModalProps({ loading: true, loadingTip: '视频加载中...' }); 32 setModalProps({ loading: true, loadingTip: '视频加载中...' });
71 - // getStreamingPlayUrl  
72 - let result: any = null;  
73 - if (record.accessMode === AccessMode.GBT28181 || ifShowGBT) {  
74 - result = await getVideoControlStart({  
75 - deviceId: params?.deviceId ? params?.deviceId : record?.deviceId,  
76 - channelId: params?.channelNo ? params?.channelNo : record?.channelId,  
77 - });  
78 - options.sources = [{ src: result.data.flv, type: getVideoTypeByUrl(result.data.flv) }];  
79 - } else {  
80 - result = await getStreamingPlayUrl(record.id);  
81 - options.sources = [  
82 - { src: result.data.url, type: getVideoTypeByUrl(result.data.url), id: record.id },  
83 - ];  
84 - }  
85 - showVideo.value = true; 33 + const { url, type } = await record.getPlayUrl();
  34 +
  35 + playUrl.value = url;
  36 + options.value = record;
  37 + options.value.playerProps = {
  38 + ...(options.value?.playerProps || {}),
  39 + streamType: type,
  40 + };
86 } catch (error) { 41 } catch (error) {
87 } finally { 42 } finally {
88 setModalProps({ loading: false }); 43 setModalProps({ loading: false });
@@ -91,18 +46,6 @@ @@ -91,18 +46,6 @@
91 ); 46 );
92 47
93 const handleCancel = () => { 48 const handleCancel = () => {
94 - showVideo.value = false;  
95 - withToken.value = false;  
96 emit('reloadTable'); 49 emit('reloadTable');
97 }; 50 };
98 </script> 51 </script>
99 -  
100 -<style lang="less" scoped>  
101 - .video-container:deep(.vben-basic-video-play) {  
102 - min-height: 13rem;  
103 - }  
104 -  
105 - .video-container:deep(.video-js) {  
106 - min-height: 13rem;  
107 - }  
108 -</style>  
@@ -140,6 +140,7 @@ @@ -140,6 +140,7 @@
140 EnumTableChartMode, 140 EnumTableChartMode,
141 ModeSwitchButton, 141 ModeSwitchButton,
142 } from '/@/components/Widget'; 142 } from '/@/components/Widget';
  143 + import { TransportTypeEnum } from '/@/enums/deviceEnum';
143 144
144 interface DeviceInfo { 145 interface DeviceInfo {
145 alarmStatus: 0 | 1; 146 alarmStatus: 0 | 1;
@@ -152,6 +153,7 @@ @@ -152,6 +153,7 @@
152 deviceType?: string; 153 deviceType?: string;
153 alias?: string; 154 alias?: string;
154 deviceProfileId: string; 155 deviceProfileId: string;
  156 + transportType?: string;
155 } 157 }
156 type MarkerList = DeviceInfo & { marker: any; label: any }; 158 type MarkerList = DeviceInfo & { marker: any; label: any };
157 159
@@ -377,7 +379,15 @@ @@ -377,7 +379,15 @@
377 width: 330, // 信息窗口宽度 379 width: 330, // 信息窗口宽度
378 height: 0, // 信息窗口高度 380 height: 0, // 信息窗口高度
379 }; 381 };
380 - const { name, alias, organizationDTO, deviceState, deviceProfile, deviceType } = record; 382 + const {
  383 + name,
  384 + alias,
  385 + organizationDTO,
  386 + deviceState,
  387 + deviceProfile,
  388 + deviceType,
  389 + transportType,
  390 + } = record;
381 const { address, longitude, latitude } = record.deviceInfo; 391 const { address, longitude, latitude } = record.deviceInfo;
382 392
383 // 创建信息窗口对象 393 // 创建信息窗口对象
@@ -411,7 +421,9 @@ @@ -411,7 +421,9 @@
411 <div style="display:flex;justify-content:end; margin-top:10px"> 421 <div style="display:flex;justify-content:end; margin-top:10px">
412 <button onclick="openDeviceInfoDrawer()" style="margin-right:10px;color:#fff;background-color:#409eff;padding:4px; border-radius:4px;">设备信息</button> 422 <button onclick="openDeviceInfoDrawer()" style="margin-right:10px;color:#fff;background-color:#409eff;padding:4px; border-radius:4px;">设备信息</button>
413 <button onclick="openHistoryModal()" style="display:${ 423 <button onclick="openHistoryModal()" style="display:${
414 - deviceType !== 'GATEWAY' ? 'block' : 'none' 424 + deviceType !== 'GATEWAY' && transportType !== TransportTypeEnum.GBT28181
  425 + ? 'block'
  426 + : 'none'
415 };color:#fff;background-color:#409eff;padding:4px; border-radius:4px;">${ 427 };color:#fff;background-color:#409eff;padding:4px; border-radius:4px;">${
416 deviceType !== 'GATEWAY' ? '历史数据' : '' 428 deviceType !== 'GATEWAY' ? '历史数据' : ''
417 }</button> 429 }</button>
@@ -443,10 +455,12 @@ @@ -443,10 +455,12 @@
443 455
444 // 设备信息 456 // 设备信息
445 const openDeviceInfoDrawer = async () => { 457 const openDeviceInfoDrawer = async () => {
446 - const { id, tbDeviceId } = globalRecord; 458 + const { id, tbDeviceId, transportType, deviceType } = globalRecord;
447 openDrawer(true, { 459 openDrawer(true, {
448 id, 460 id,
449 tbDeviceId, 461 tbDeviceId,
  462 + transportType,
  463 + deviceType,
450 }); 464 });
451 }; 465 };
452 466
@@ -144,7 +144,7 @@ export const list = [ @@ -144,7 +144,7 @@ export const list = [
144 }, 144 },
145 { 145 {
146 deviceType: '网关/直连设备', 146 deviceType: '网关/直连设备',
147 - function: '服务端响应共享属性客户端属性给设备端', 147 + function: '服务端响应共享属性客户端属性给设备端',
148 release: 'v1/devices/me/attributes/response/$request_id', 148 release: 'v1/devices/me/attributes/response/$request_id',
149 subscribe: 'v1/devices/me/attributes/response/+', 149 subscribe: 'v1/devices/me/attributes/response/+',
150 platform: '发布', 150 platform: '发布',
@@ -54,6 +54,7 @@ export const typeList = [ @@ -54,6 +54,7 @@ export const typeList = [
54 { label: '阿里', value: 'ALI_CLOUD' }, 54 { label: '阿里', value: 'ALI_CLOUD' },
55 { label: '钉钉', value: 'DING_TALK' }, 55 { label: '钉钉', value: 'DING_TALK' },
56 { label: '腾讯', value: 'TENCENT_CLOUD' }, 56 { label: '腾讯', value: 'TENCENT_CLOUD' },
  57 + { label: '阿里云语音', value: 'ALI_VOICE' },
57 ]; 58 ];
58 59
59 export const searchFormSchema: FormSchema[] = [ 60 export const searchFormSchema: FormSchema[] = [
@@ -4,6 +4,7 @@ import { Tinymce } from '/@/components/Tinymce/index'; @@ -4,6 +4,7 @@ import { Tinymce } from '/@/components/Tinymce/index';
4 import { getOrganizationList } from '/@/api/system/system'; 4 import { getOrganizationList } from '/@/api/system/system';
5 import { copyTransFun } from '/@/utils/fnUtils'; 5 import { copyTransFun } from '/@/utils/fnUtils';
6 import { Tag } from 'ant-design-vue'; 6 import { Tag } from 'ant-design-vue';
  7 +import { useComponentRegister } from '/@/components/Form';
7 8
8 export enum IsOrgEnum { 9 export enum IsOrgEnum {
9 IS_ORG_ENUM = 1, 10 IS_ORG_ENUM = 1,
@@ -12,6 +13,8 @@ export const isOrg = (type: string) => { @@ -12,6 +13,8 @@ export const isOrg = (type: string) => {
12 return type === IsOrgEnum.IS_ORG_ENUM; 13 return type === IsOrgEnum.IS_ORG_ENUM;
13 }; 14 };
14 15
  16 +useComponentRegister('Tinymce', Tinymce);
  17 +
15 export const columns: BasicColumn[] = [ 18 export const columns: BasicColumn[] = [
16 { 19 {
17 title: '类型', 20 title: '类型',
@@ -92,21 +95,13 @@ export const formSchema: FormSchema[] = [ @@ -92,21 +95,13 @@ export const formSchema: FormSchema[] = [
92 }, 95 },
93 { 96 {
94 field: 'content', 97 field: 'content',
95 - component: 'Input', 98 + component: 'Tinymce',
96 colProps: { span: 24 }, 99 colProps: { span: 24 },
97 label: '通知内容', 100 label: '通知内容',
98 required: true, 101 required: true,
99 componentProps: { 102 componentProps: {
100 maxLength: 255, 103 maxLength: 255,
101 }, 104 },
102 - render: ({ model, field }) => {  
103 - return h(Tinymce, {  
104 - value: model[field],  
105 - onChange: (value: string) => {  
106 - model[field] = value;  
107 - },  
108 - });  
109 - },  
110 }, 105 },
111 { 106 {
112 field: 'receiverType', 107 field: 'receiverType',
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 - import { useTable, BasicTable } from '/@/components/Table'; 2 + import { BasicTable, useTable } from '/@/components/Table';
3 import { getColumns } from './config'; 3 import { getColumns } from './config';
4 - import { getRuleNodeEventList } from '/@/api/ruleDesigner'; 4 + import { doClearEvents, getRuleNodeEventList } from '/@/api/ruleDesigner';
5 import { BasicNodeFormData, NodeData } from '../../../../types/node'; 5 import { BasicNodeFormData, NodeData } from '../../../../types/node';
6 import { computed, reactive, ref, unref, watch } from 'vue'; 6 import { computed, reactive, ref, unref, watch } from 'vue';
7 import { useUserStore } from '/@/store/modules/user'; 7 import { useUserStore } from '/@/store/modules/user';
8 import { EventSelect } from '../EventSelect'; 8 import { EventSelect } from '../EventSelect';
9 import { Icon } from '/@/components/Icon'; 9 import { Icon } from '/@/components/Icon';
10 - import { Tooltip } from 'ant-design-vue';  
11 - import { EventTypeEnum } from '../EventSelect/config'; 10 + import { Tooltip, Popconfirm } from 'ant-design-vue';
  11 + import { EventTypeEnum, EventTypeNameEnum } from '../EventSelect/config';
12 import { FilterForm } from '../FilterForm'; 12 import { FilterForm } from '../FilterForm';
13 import { useModal } from '/@/components/Modal'; 13 import { useModal } from '/@/components/Modal';
14 import { DataActionModeEnum } from '/@/enums/toolEnum'; 14 import { DataActionModeEnum } from '/@/enums/toolEnum';
@@ -31,6 +31,8 @@ @@ -31,6 +31,8 @@
31 data: {} as Recordable, 31 data: {} as Recordable,
32 }); 32 });
33 33
  34 + const dataLength = ref();
  35 +
34 const [register, { reload, setColumns, setPagination }] = useTable({ 36 const [register, { reload, setColumns, setPagination }] = useTable({
35 columns: getColumns(EventTypeEnum.DEBUG_RULE_NODE), 37 columns: getColumns(EventTypeEnum.DEBUG_RULE_NODE),
36 showIndexColumn: false, 38 showIndexColumn: false,
@@ -56,6 +58,8 @@ @@ -56,6 +58,8 @@
56 { ...searchParams.data, eventType: unref(eventType) } 58 { ...searchParams.data, eventType: unref(eventType) }
57 ); 59 );
58 60
  61 + dataLength.value = result.totalElements;
  62 +
59 return result; 63 return result;
60 }, 64 },
61 }); 65 });
@@ -100,6 +104,17 @@ @@ -100,6 +104,17 @@
100 } as ModalParamsType<string>); 104 } as ModalParamsType<string>);
101 }; 105 };
102 106
  107 + const handleClearStats = async () => {
  108 + const userInfo = useUserStore();
  109 + await doClearEvents({
  110 + ruleId: unref(getNodeId)!,
  111 + tenantId: userInfo.getUserInfo.tenantId!,
  112 + ...(searchParams.params as Record<'startTime' | 'endTime', string>),
  113 + eventType: unref(eventType),
  114 + });
  115 + reload();
  116 + };
  117 +
103 watch( 118 watch(
104 () => props.elementInfo?.id, 119 () => props.elementInfo?.id,
105 () => { 120 () => {
@@ -115,6 +130,20 @@ @@ -115,6 +130,20 @@
115 <EventSelect v-model:type="eventType" @change="handleEventTypeChange" /> 130 <EventSelect v-model:type="eventType" @change="handleEventTypeChange" />
116 </template> 131 </template>
117 <template #toolbar> 132 <template #toolbar>
  133 + <Popconfirm
  134 + @confirm="handleClearStats"
  135 + :title="`清除所有${EventTypeNameEnum[eventType]}`"
  136 + :content="`是否确认清除所有${EventTypeNameEnum[eventType]}`"
  137 + >
  138 + <Tooltip title="">
  139 + <Icon
  140 + v-if="dataLength"
  141 + icon="ant-design:delete-outlined"
  142 + class="cursor-pointer svg:text-2xl"
  143 + />
  144 + </Tooltip>
  145 + </Popconfirm>
  146 +
118 <Tooltip title="过滤器"> 147 <Tooltip title="过滤器">
119 <Icon 148 <Icon
120 icon="material-symbols:filter-list" 149 icon="material-symbols:filter-list"
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 import { BasicForm, FormActionType, ThingsModelForm } from '/@/components/Form'; 5 import { BasicForm, FormActionType, ThingsModelForm } from '/@/components/Form';
6 import { getFormSchemas, FormFieldsEnum } from './config'; 6 import { getFormSchemas, FormFieldsEnum } from './config';
7 import { FlipFlop } from '../FlipFlop'; 7 import { FlipFlop } from '../FlipFlop';
8 - import { ComponentPublicInstance, ref, unref } from 'vue'; 8 + import { ComponentPublicInstance, ref, unref, watch } from 'vue';
9 import { ExecutionActionListRefItemType } from './type'; 9 import { ExecutionActionListRefItemType } from './type';
10 import { useExecutionActionData } from './useExecutionActionData'; 10 import { useExecutionActionData } from './useExecutionActionData';
11 import { createNewExecutionActionItem } from '.'; 11 import { createNewExecutionActionItem } from '.';
@@ -63,6 +63,20 @@ @@ -63,6 +63,20 @@
63 hasAlarmNotify 63 hasAlarmNotify
64 ); 64 );
65 65
  66 + const { organizationId } = useSceneLinkageDrawerContext();
  67 +
  68 + /**
  69 + * @description on organization change
  70 + */
  71 + watch(organizationId, () => {
  72 + unref(executionActionListRef).forEach((flipFlopItem) =>
  73 + flipFlopItem.ref?.setFieldsValue({
  74 + [FormFieldsEnum.ENTITY_ID]: [],
  75 + [FormFieldsEnum.ALARM_PROFILED]: null,
  76 + })
  77 + );
  78 + });
  79 +
66 defineExpose({ 80 defineExpose({
67 getFieldsValue, 81 getFieldsValue,
68 setFieldsValue, 82 setFieldsValue,
@@ -97,8 +97,8 @@ @@ -97,8 +97,8 @@
97 97
98 onMounted(() => { 98 onMounted(() => {
99 const platform = getPlatFormInfo(); 99 const platform = getPlatFormInfo();
100 - defaultLogo.value = platform.logo || '';  
101 - show.value = !platform.background; 100 + defaultLogo.value = platform?.logo || '';
  101 + show.value = !platform?.background;
102 }); 102 });
103 </script> 103 </script>
104 <style lang="less"> 104 <style lang="less">
@@ -9,6 +9,9 @@ @@ -9,6 +9,9 @@
9 <div style="height: 50vh"> 9 <div style="height: 50vh">
10 <BasicForm @register="registerForm"> 10 <BasicForm @register="registerForm">
11 <template #organizationId="{ model, field }"> 11 <template #organizationId="{ model, field }">
  12 + <Button type="link" @click="handleOpenCreate" style="padding: 0; z-index: 9999"
  13 + >新增组织
  14 + </Button>
12 <BasicTree 15 <BasicTree
13 v-if="organizationTreeData.length" 16 v-if="organizationTreeData.length"
14 v-model:value="model[field]" 17 v-model:value="model[field]"
@@ -44,6 +47,8 @@ @@ -44,6 +47,8 @@
44 </a-select> 47 </a-select>
45 </template> 48 </template>
46 </BasicForm> 49 </BasicForm>
  50 +
  51 + <OrganizationDrawer @register="registerDrawer" @success="handleReload" />
47 </div> 52 </div>
48 </BasicModal> 53 </BasicModal>
49 <RoleDrawer @register="registerRoleDrawer" @success="handleSuccess" /> 54 <RoleDrawer @register="registerRoleDrawer" @success="handleSuccess" />
@@ -53,6 +58,7 @@ @@ -53,6 +58,7 @@
53 import { BasicModal, useModalInner } from '/@/components/Modal'; 58 import { BasicModal, useModalInner } from '/@/components/Modal';
54 import { BasicForm, useForm } from '/@/components/Form/index'; 59 import { BasicForm, useForm } from '/@/components/Form/index';
55 import { accountFormSchema } from './account.data'; 60 import { accountFormSchema } from './account.data';
  61 + import { Button } from 'ant-design-vue';
56 import { 62 import {
57 findCurrentUserRelation, 63 findCurrentUserRelation,
58 SaveOrUpdateUserInfo, 64 SaveOrUpdateUserInfo,
@@ -67,13 +73,16 @@ @@ -67,13 +73,16 @@
67 import { PlusOutlined } from '@ant-design/icons-vue'; 73 import { PlusOutlined } from '@ant-design/icons-vue';
68 import { useDrawer } from '/@/components/Drawer'; 74 import { useDrawer } from '/@/components/Drawer';
69 import RoleDrawer from '../role/RoleDrawer.vue'; 75 import RoleDrawer from '../role/RoleDrawer.vue';
  76 + import OrganizationDrawer from '/@/views/system/organization/OrganizationDrawer.vue';
70 77
71 export default defineComponent({ 78 export default defineComponent({
72 name: 'AccountModal', 79 name: 'AccountModal',
73 components: { 80 components: {
74 BasicModal, 81 BasicModal,
75 BasicForm, 82 BasicForm,
  83 + Button,
76 BasicTree, 84 BasicTree,
  85 + OrganizationDrawer,
77 PlusOutlined, 86 PlusOutlined,
78 RoleDrawer, 87 RoleDrawer,
79 VNodes: (_, { attrs }) => { 88 VNodes: (_, { attrs }) => {
@@ -273,6 +282,17 @@ @@ -273,6 +282,17 @@
273 ]; 282 ];
274 }; 283 };
275 284
  285 + const [registerDrawer, { openDrawer: addOpenDrawer }] = useDrawer();
  286 +
  287 + const handleOpenCreate = () => {
  288 + addOpenDrawer(true, { isUpdate: false });
  289 + };
  290 + const handleReload = async () => {
  291 + const groupListModel = await findCurrentUserGroups();
  292 + copyTransTreeFun(groupListModel);
  293 + organizationTreeData.value = groupListModel;
  294 + };
  295 +
276 return { 296 return {
277 registerModal, 297 registerModal,
278 registerForm, 298 registerForm,
@@ -291,6 +311,9 @@ @@ -291,6 +311,9 @@
291 handleCheckClick, 311 handleCheckClick,
292 handleUnSelectAll, 312 handleUnSelectAll,
293 handleStrictlyStatus, 313 handleStrictlyStatus,
  314 + handleOpenCreate,
  315 + registerDrawer,
  316 + handleReload,
294 }; 317 };
295 }, 318 },
296 }); 319 });
@@ -298,6 +321,8 @@ @@ -298,6 +321,8 @@
298 <style scoped lang="less"> 321 <style scoped lang="less">
299 :deep(.vben-basic-tree) { 322 :deep(.vben-basic-tree) {
300 width: 100% !important; 323 width: 100% !important;
  324 + margin-top: -28px !important;
  325 + padding: 0;
301 } 326 }
302 327
303 :deep(.is-unflod) { 328 :deep(.is-unflod) {
@@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
31 </template> 31 </template>
32 32
33 <script lang="ts"> 33 <script lang="ts">
34 - import { defineComponent, onMounted, ref, computed } from 'vue'; 34 + import { defineComponent, onMounted, ref, computed, watch } from 'vue';
35 import { Card } from 'ant-design-vue'; 35 import { Card } from 'ant-design-vue';
36 import { BasicForm, useForm } from '/@/components/Form/index'; 36 import { BasicForm, useForm } from '/@/components/Form/index';
37 import { schemas, provSchemas } from '../config/enterPriseInfo.config'; 37 import { schemas, provSchemas } from '../config/enterPriseInfo.config';
@@ -73,7 +73,10 @@ @@ -73,7 +73,10 @@
73 loading: false, 73 loading: false,
74 tip: '拼命加载中...', 74 tip: '拼命加载中...',
75 }); 75 });
76 - const [registerForm, { getFieldsValue, setFieldsValue, validate, clearValidate }] = useForm({ 76 + const [
  77 + registerForm,
  78 + { getFieldsValue, setFieldsValue, validate, clearValidate, validateFields },
  79 + ] = useForm({
77 labelWidth: 80, 80 labelWidth: 80,
78 schemas, 81 schemas,
79 showResetButton: false, 82 showResetButton: false,
@@ -101,6 +104,13 @@ @@ -101,6 +104,13 @@
101 const handleSetCodeImgUrl = (d) => { 104 const handleSetCodeImgUrl = (d) => {
102 qrcodePic.value = d; 105 qrcodePic.value = d;
103 }; 106 };
  107 + watch(
  108 + () => qrcodePic.value,
  109 + (newValue) => {
  110 + if (!newValue) validateFields(['qrcode']);
  111 + else clearValidate('qrcode');
  112 + }
  113 + );
104 // 更新 114 // 更新
105 const handleUpdateInfo = async () => { 115 const handleUpdateInfo = async () => {
106 try { 116 try {
1 import { BasicColumn, FormSchema } from '/@/components/Table'; 1 import { BasicColumn, FormSchema } from '/@/components/Table';
2 import moment from 'moment'; 2 import moment from 'moment';
3 -import { findDictItemByCode } from '/@/api/system/dict';  
4 import { format } from '../util'; 3 import { format } from '../util';
5 import { h } from 'vue'; 4 import { h } from 'vue';
6 import { Tag } from 'ant-design-vue'; 5 import { Tag } from 'ant-design-vue';
7 6
8 -//格式化资源类型和操作类型  
9 -let formatText;  
10 -let actionTypeText;  
11 -async function formatTextFunc() {  
12 - formatText = await findDictItemByCode({ dictCode: 'operate_log' });  
13 - actionTypeText = await findDictItemByCode({ dictCode: 'exception_log' });  
14 -}  
15 -formatTextFunc();  
16 // 表格数据 7 // 表格数据
17 export const columns: BasicColumn[] = [ 8 export const columns: BasicColumn[] = [
18 { 9 {
@@ -21,24 +12,6 @@ export const columns: BasicColumn[] = [ @@ -21,24 +12,6 @@ export const columns: BasicColumn[] = [
21 width: 120, 12 width: 120,
22 }, 13 },
23 { 14 {
24 - title: '客户名称',  
25 - dataIndex: 'customerName',  
26 - width: 120,  
27 - },  
28 - {  
29 - title: '资源类型',  
30 - dataIndex: 'entityType',  
31 - width: 180,  
32 - format: (_, record) => {  
33 - const text = formatText.find((f) => {  
34 - if (f.itemValue == record.entityType) {  
35 - return f.itemText;  
36 - }  
37 - });  
38 - return text?.itemText;  
39 - },  
40 - },  
41 - {  
42 title: '资源名称', 15 title: '资源名称',
43 dataIndex: 'entityName', 16 dataIndex: 'entityName',
44 width: 180, 17 width: 180,
@@ -49,19 +22,6 @@ export const columns: BasicColumn[] = [ @@ -49,19 +22,6 @@ export const columns: BasicColumn[] = [
49 width: 180, 22 width: 180,
50 }, 23 },
51 { 24 {
52 - title: '操作类型',  
53 - dataIndex: 'actionType',  
54 - width: 180,  
55 - format: (_, record) => {  
56 - const text = actionTypeText.find((f) => {  
57 - if (f.itemValue == record.actionType) {  
58 - return f.itemText;  
59 - }  
60 - });  
61 - return text?.itemText;  
62 - },  
63 - },  
64 - {  
65 title: '操作状态', 25 title: '操作状态',
66 dataIndex: 'actionStatus', 26 dataIndex: 'actionStatus',
67 width: 180, 27 width: 180,
@@ -116,22 +76,7 @@ export const formSchema: FormSchema[] = [ @@ -116,22 +76,7 @@ export const formSchema: FormSchema[] = [
116 disabled: true, 76 disabled: true,
117 }, 77 },
118 }, 78 },
119 - {  
120 - field: 'customerName',  
121 - label: '客户名称',  
122 - component: 'Input',  
123 - componentProps: {  
124 - disabled: true,  
125 - },  
126 - },  
127 - {  
128 - field: 'entityType',  
129 - label: '资源类型',  
130 - component: 'Input',  
131 - componentProps: {  
132 - disabled: true,  
133 - },  
134 - }, 79 +
135 { 80 {
136 field: 'entityName', 81 field: 'entityName',
137 label: '资源名称', 82 label: '资源名称',
@@ -150,14 +95,6 @@ export const formSchema: FormSchema[] = [ @@ -150,14 +95,6 @@ export const formSchema: FormSchema[] = [
150 }, 95 },
151 }, 96 },
152 { 97 {
153 - field: 'actionType',  
154 - label: '操作类型',  
155 - component: 'Input',  
156 - componentProps: {  
157 - disabled: true,  
158 - },  
159 - },  
160 - {  
161 field: 'actionStatus', 98 field: 'actionStatus',
162 label: '操作状态', 99 label: '操作状态',
163 component: 'Input', 100 component: 'Input',
1 import { BasicColumn } from '/@/components/Table'; 1 import { BasicColumn } from '/@/components/Table';
2 import { FormSchema } from '/@/components/Table'; 2 import { FormSchema } from '/@/components/Table';
3 import { useI18n } from '/@/hooks/web/useI18n'; 3 import { useI18n } from '/@/hooks/web/useI18n';
  4 +import { createOrganizationSearch } from '/@/utils/organizationSearch';
4 const { t } = useI18n(); 5 const { t } = useI18n();
5 6
6 export const columns: BasicColumn[] = [ 7 export const columns: BasicColumn[] = [
@@ -38,6 +39,7 @@ export const formSchema: FormSchema[] = [ @@ -38,6 +39,7 @@ export const formSchema: FormSchema[] = [
38 value: 'id', 39 value: 'id',
39 }, 40 },
40 maxTagCount: 10, 41 maxTagCount: 10,
  42 + ...createOrganizationSearch(),
41 getPopupContainer: () => document.body, 43 getPopupContainer: () => document.body,
42 }, 44 },
43 }, 45 },
@@ -13,6 +13,7 @@ export enum FormFieldEnum { @@ -13,6 +13,7 @@ export enum FormFieldEnum {
13 13
14 DEVICE_ID = 'deviceId', 14 DEVICE_ID = 'deviceId',
15 CHANNEL_ID = 'channelId', 15 CHANNEL_ID = 'channelId',
  16 + PLAY_PROTOCOL = 'playProtocol',
16 } 17 }
17 18
18 export interface DataSourceValueType { 19 export interface DataSourceValueType {
@@ -70,6 +71,7 @@ export const formSchemas: FormSchema[] = [ @@ -70,6 +71,7 @@ export const formSchemas: FormSchema[] = [
70 component: 'Input', 71 component: 'Input',
71 label: '地址', 72 label: '地址',
72 colProps: { span: 16 }, 73 colProps: { span: 16 },
  74 + required: true,
73 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.CUSTOM, 75 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.CUSTOM,
74 componentProps: { 76 componentProps: {
75 placeholder: '请输入自定义地址', 77 placeholder: '请输入自定义地址',
@@ -79,6 +81,7 @@ export const formSchemas: FormSchema[] = [ @@ -79,6 +81,7 @@ export const formSchemas: FormSchema[] = [
79 field: FormFieldEnum.ORGANIZATION_ID, 81 field: FormFieldEnum.ORGANIZATION_ID,
80 component: 'OrgTreeSelect', 82 component: 'OrgTreeSelect',
81 label: '组织', 83 label: '组织',
  84 + required: true,
82 colProps: { span: 8 }, 85 colProps: { span: 8 },
83 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE, 86 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE,
84 componentProps: ({ formActionType }) => { 87 componentProps: ({ formActionType }) => {
@@ -105,6 +108,7 @@ export const formSchemas: FormSchema[] = [ @@ -105,6 +108,7 @@ export const formSchemas: FormSchema[] = [
105 field: FormFieldEnum.VIDEO_ID, 108 field: FormFieldEnum.VIDEO_ID,
106 component: 'ApiSelect', 109 component: 'ApiSelect',
107 label: '地址', 110 label: '地址',
  111 + required: true,
108 colProps: { span: 8 }, 112 colProps: { span: 8 },
109 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE, 113 ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE,
110 componentProps: ({ formActionType, formModel }) => { 114 componentProps: ({ formActionType, formModel }) => {
@@ -129,6 +133,7 @@ export const formSchemas: FormSchema[] = [ @@ -129,6 +133,7 @@ export const formSchemas: FormSchema[] = [
129 accessMode === AccessMode.GBT28181 ? options.params?.channelNo : null, 133 accessMode === AccessMode.GBT28181 ? options.params?.channelNo : null,
130 [FormFieldEnum.DEVICE_ID]: 134 [FormFieldEnum.DEVICE_ID]:
131 accessMode === AccessMode.GBT28181 ? options.params?.deviceId : null, 135 accessMode === AccessMode.GBT28181 ? options.params?.deviceId : null,
  136 + [FormFieldEnum.PLAY_PROTOCOL]: options?.playProtocol,
132 }); 137 });
133 }, 138 },
134 }; 139 };
@@ -146,4 +151,10 @@ export const formSchemas: FormSchema[] = [ @@ -146,4 +151,10 @@ export const formSchemas: FormSchema[] = [
146 component: 'Input', 151 component: 'Input',
147 ifShow: false, 152 ifShow: false,
148 }, 153 },
  154 + {
  155 + field: FormFieldEnum.PLAY_PROTOCOL,
  156 + label: '播放协议',
  157 + component: 'InputNumber',
  158 + ifShow: false,
  159 + },
149 ]; 160 ];
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import { ComponentMode, ComponentPropsConfigType } from '/@/views/visual/packages/index.type'; 2 import { ComponentMode, ComponentPropsConfigType } from '/@/views/visual/packages/index.type';
3 import { option } from './config'; 3 import { option } from './config';
4 - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';  
5 - import { computed, onMounted, onUnmounted, toRaw } from 'vue';  
6 - import { VideoJsPlayerOptions } from 'video.js';  
7 - import { AccessMode } from '/@/views/camera/manage/config.data';  
8 - import { unref } from 'vue';  
9 - import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager'; 4 + import { XGPlayer } from '/@/components/Video';
  5 + import { onMounted, onUnmounted } from 'vue';
  6 + import { getPlayUrl } from '/@/views/camera/manage/config.data';
  7 + import { closeFlvPlay } from '/@/api/camera/cameraManager';
10 import { ref } from 'vue'; 8 import { ref } from 'vue';
11 import { Spin } from 'ant-design-vue'; 9 import { Spin } from 'ant-design-vue';
12 import { useFingerprint } from '/@/utils/useFingerprint'; 10 import { useFingerprint } from '/@/utils/useFingerprint';
13 - import { isRtspProtocol, VideoPlayerType } from '/@/components/Video/src/utils';  
14 - import { isShareMode } from '/@/views/sys/share/hook';  
15 - import { getJwtToken, getShareJwtToken } from '/@/utils/auth';  
16 - import { getVideoControlStart } from '/@/api/device/videoChannel'; 11 + import { isRtspProtocol } from '/@/components/Video/src/utils';
  12 + import { CameraRecord } from '/@/api/camera/model/cameraModel';
17 13
18 const props = defineProps<{ 14 const props = defineProps<{
19 config: ComponentPropsConfigType<typeof option>; 15 config: ComponentPropsConfigType<typeof option>;
@@ -21,24 +17,12 @@ @@ -21,24 +17,12 @@
21 17
22 const loading = ref(true); 18 const loading = ref(true);
23 19
24 - const basicVideoPlayEl = ref<Nullable<InstanceType<typeof BasicVideoPlay>>>(null);  
25 -  
26 - const withToken = ref(false);  
27 -  
28 - const playSource = ref<Record<'src' | 'type', string>>(  
29 - {} as unknown as Record<'src' | 'type', string>  
30 - ); 20 + const playUrl = ref<string>();
  21 + const playType = ref<string>();
31 22
32 const exampleVideoPlay = 23 const exampleVideoPlay =
33 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'; 24 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm';
34 25
35 - const getOptions = computed<VideoJsPlayerOptions>(() => {  
36 - return {  
37 - width: '100%' as unknown as number,  
38 - height: '100%' as unknown as number,  
39 - };  
40 - });  
41 -  
42 const { getResult } = useFingerprint(); 26 const { getResult } = useFingerprint();
43 27
44 const handleGetVideoPlayUrl = async () => { 28 const handleGetVideoPlayUrl = async () => {
@@ -46,45 +30,19 @@ @@ -46,45 +30,19 @@
46 const { config } = props; 30 const { config } = props;
47 const { option } = config; 31 const { option } = config;
48 const { videoConfig, uuid } = option || {}; 32 const { videoConfig, uuid } = option || {};
  33 + const { type, url } = await getPlayUrl({
  34 + id: videoConfig?.id,
  35 + accessMode: videoConfig?.accessMode,
  36 + playProtocol: videoConfig?.playProtocol,
  37 + videoUrl: videoConfig?.videoUrl,
  38 + params: {
  39 + deviceId: videoConfig?.deviceId,
  40 + channelNo: videoConfig?.channelId,
  41 + },
  42 + } as unknown as CameraRecord);
  43 + playType.value = type;
  44 + playUrl.value = url;
49 if (!uuid) return; 45 if (!uuid) return;
50 - const { url, id, accessMode, deviceId, channelId } = videoConfig || {};  
51 - let type = getVideoTypeByUrl(url!);  
52 - let playUrl = url;  
53 - if (accessMode === AccessMode.Streaming && id) {  
54 - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(id!);  
55 - playUrl = url;  
56 - playUrl && (type = getVideoTypeByUrl(playUrl!));  
57 - } else if (accessMode === AccessMode.GBT28181 && deviceId && channelId) {  
58 - const {  
59 - data: { flv },  
60 - } = await getVideoControlStart({ channelId, deviceId });  
61 -  
62 - playUrl = flv;  
63 - type = VideoPlayerType.flv;  
64 - }  
65 -  
66 - if (isRtspProtocol(url!)) {  
67 - const result = await getResult();  
68 - const { visitorId } = result;  
69 - playUrl = getFlvPlayUrl(playUrl!, visitorId);  
70 - withToken.value = true;  
71 - }  
72 -  
73 - playSource.value = {  
74 - src: playUrl!,  
75 - type,  
76 - };  
77 -  
78 - const instance = unref(basicVideoPlayEl)?.customInit((options) => {  
79 - if (unref(withToken)) {  
80 - (options as any).flvjs.config.headers = {  
81 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,  
82 - };  
83 - }  
84 - return { ...options, sources: [toRaw(unref(playSource))] } as VideoJsPlayerOptions;  
85 - });  
86 -  
87 - instance?.play();  
88 } finally { 46 } finally {
89 loading.value = false; 47 loading.value = false;
90 } 48 }
@@ -92,20 +50,8 @@ @@ -92,20 +50,8 @@
92 50
93 const handleSelectPreview = () => { 51 const handleSelectPreview = () => {
94 loading.value = false; 52 loading.value = false;
95 - const instance = unref(basicVideoPlayEl)?.customInit((options) => {  
96 - withToken.value = true;  
97 -  
98 - if (unref(withToken)) {  
99 - (options as any).flvjs.config.headers = {  
100 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,  
101 - };  
102 - }  
103 - return {  
104 - ...options,  
105 - sources: [{ type: getVideoTypeByUrl(exampleVideoPlay), src: exampleVideoPlay }],  
106 - } as VideoJsPlayerOptions;  
107 - });  
108 - instance?.play(); 53 + playUrl.value = exampleVideoPlay;
  54 + playType.value = 'auto';
109 }; 55 };
110 56
111 onMounted(() => { 57 onMounted(() => {
@@ -126,11 +72,10 @@ @@ -126,11 +72,10 @@
126 <template> 72 <template>
127 <main class="w-full h-full flex flex-col justify-center items-center p-2"> 73 <main class="w-full h-full flex flex-col justify-center items-center p-2">
128 <Spin :spinning="loading" wrapper-class-name="video-spin"> 74 <Spin :spinning="loading" wrapper-class-name="video-spin">
129 - <BasicVideoPlay  
130 - ref="basicVideoPlayEl"  
131 - :options="getOptions"  
132 - :with-token="withToken"  
133 - :immediateInitOnMounted="false" 75 + <XGPlayer
  76 + :url="playUrl"
  77 + :stream-type="playType"
  78 + :config="{ width: '100%', height: '100%' }"
134 /> 79 />
135 </Spin> 80 </Spin>
136 </main> 81 </main>