Showing
11 changed files
with
246 additions
and
76 deletions
@@ -45,5 +45,5 @@ VITE_CONTENT_SECURITY_POLICY = false | @@ -45,5 +45,5 @@ VITE_CONTENT_SECURITY_POLICY = false | ||
45 | # Alarm Notify Polling Interval Time | 45 | # Alarm Notify Polling Interval Time |
46 | VITE_ALARM_NOTIFY_POLLING_INTERVAL_TIME = 5000 | 46 | VITE_ALARM_NOTIFY_POLLING_INTERVAL_TIME = 5000 |
47 | 47 | ||
48 | -# Alarm Notify Auto Close Time | ||
49 | -VITE_ALARM_NOTIFY_DURATION = 5000 | 48 | +# Alarm Notify Auto Close Time Unit is Second |
49 | +VITE_ALARM_NOTIFY_DURATION = 5 |
@@ -46,5 +46,5 @@ VITE_CONTENT_SECURITY_POLICY = false | @@ -46,5 +46,5 @@ VITE_CONTENT_SECURITY_POLICY = false | ||
46 | # Alarm Notify Polling Interval Time | 46 | # Alarm Notify Polling Interval Time |
47 | VITE_ALARM_NOTIFY_POLLING_INTERVAL_TIME = 60000 | 47 | VITE_ALARM_NOTIFY_POLLING_INTERVAL_TIME = 60000 |
48 | 48 | ||
49 | -# Alarm Notify Auto Close Time | ||
50 | -VITE_ALARM_NOTIFY_DURATION = 5000 | 49 | +# Alarm Notify Auto Close Time Unit is Second |
50 | +VITE_ALARM_NOTIFY_DURATION = 5 |
@@ -8,7 +8,6 @@ | @@ -8,7 +8,6 @@ | ||
8 | 8 | ||
9 | <script lang="ts" setup> | 9 | <script lang="ts" setup> |
10 | import { ConfigProvider } from 'ant-design-vue'; | 10 | import { ConfigProvider } from 'ant-design-vue'; |
11 | - import { useAlarmNotify } from './views/alarm/log/hook/useAlarmNotify'; | ||
12 | import { AppProvider } from '/@/components/Application'; | 11 | import { AppProvider } from '/@/components/Application'; |
13 | import { useTitle } from '/@/hooks/web/useTitle'; | 12 | import { useTitle } from '/@/hooks/web/useTitle'; |
14 | import { useLocale } from '/@/locales/useLocale'; | 13 | import { useLocale } from '/@/locales/useLocale'; |
@@ -16,5 +15,4 @@ | @@ -16,5 +15,4 @@ | ||
16 | const { getAntdLocale } = useLocale(); | 15 | const { getAntdLocale } = useLocale(); |
17 | 16 | ||
18 | useTitle(); | 17 | useTitle(); |
19 | - useAlarmNotify(); | ||
20 | </script> | 18 | </script> |
@@ -31,7 +31,6 @@ | @@ -31,7 +31,6 @@ | ||
31 | import { CreateContextOptions } from '/@/components/ContextMenu'; | 31 | import { CreateContextOptions } from '/@/components/ContextMenu'; |
32 | 32 | ||
33 | import { CheckEvent } from './typing'; | 33 | import { CheckEvent } from './typing'; |
34 | - import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; | ||
35 | 34 | ||
36 | interface State { | 35 | interface State { |
37 | expandedKeys: Keys; | 36 | expandedKeys: Keys; |
@@ -52,13 +51,13 @@ | @@ -52,13 +51,13 @@ | ||
52 | 'update:searchValue', | 51 | 'update:searchValue', |
53 | ], | 52 | ], |
54 | setup(props, { attrs, slots, emit, expose }) { | 53 | setup(props, { attrs, slots, emit, expose }) { |
55 | - /** | ||
56 | - * 作者自带Tree组件 | ||
57 | - * 新增显示隐藏 | ||
58 | - * ft | ||
59 | - */ | ||
60 | - //ft | ||
61 | - const isFlod = ref(false); | 54 | + // /** |
55 | + // * 作者自带Tree组件 | ||
56 | + // * 新增显示隐藏 | ||
57 | + // * ft | ||
58 | + // */ | ||
59 | + // //ft | ||
60 | + // const isFlod = ref(false); | ||
62 | //ft | 61 | //ft |
63 | const state = reactive<State>({ | 62 | const state = reactive<State>({ |
64 | checkStrictly: props.checkStrictly, | 63 | checkStrictly: props.checkStrictly, |
@@ -234,9 +233,9 @@ | @@ -234,9 +233,9 @@ | ||
234 | } | 233 | } |
235 | 234 | ||
236 | //ft | 235 | //ft |
237 | - function handleFlodOrUnFoldFunc(v) { | ||
238 | - isFlod.value = v; | ||
239 | - } | 236 | + // function handleFlodOrUnFoldFunc(v) { |
237 | + // isFlod.value = v; | ||
238 | + // } | ||
240 | //ft | 239 | //ft |
241 | function handleClickNode(key: string, children: TreeItem[]) { | 240 | function handleClickNode(key: string, children: TreeItem[]) { |
242 | if (!props.clickRowToExpand || !children || children.length === 0) return; | 241 | if (!props.clickRowToExpand || !children || children.length === 0) return; |
@@ -419,7 +418,7 @@ | @@ -419,7 +418,7 @@ | ||
419 | const showTitle = title || toolbar || search || slots.headerTitle; | 418 | const showTitle = title || toolbar || search || slots.headerTitle; |
420 | const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' }; | 419 | const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' }; |
421 | return ( | 420 | return ( |
422 | - <div style={isFlod.value ? '' : 'width:0vw'} class={[prefixCls, 'h-full', attrs.class]}> | 421 | + <div class={[prefixCls, 'h-full', attrs.class]}> |
423 | {showTitle && ( | 422 | {showTitle && ( |
424 | <TreeHeader | 423 | <TreeHeader |
425 | style={'position:relative'} | 424 | style={'position:relative'} |
@@ -448,23 +447,6 @@ | @@ -448,23 +447,6 @@ | ||
448 | </ScrollContainer> | 447 | </ScrollContainer> |
449 | 448 | ||
450 | <Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" /> | 449 | <Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" /> |
451 | - <span | ||
452 | - v-show={unref(isFlod)} | ||
453 | - onClick={() => handleFlodOrUnFoldFunc(false)} | ||
454 | - class={['is-flod', unref(isFlod) ? 'fold-right' : 'fold-left']} | ||
455 | - > | ||
456 | - <DoubleLeftOutlined /> | ||
457 | - </span> | ||
458 | - <span | ||
459 | - v-show={!unref(isFlod) && unref(treeDataRef).length != 0} | ||
460 | - onClick={() => handleFlodOrUnFoldFunc(true)} | ||
461 | - class={[ | ||
462 | - 'is-unflod', | ||
463 | - !unref(isFlod) && unref(treeDataRef).length != 0 ? 'fold-left' : 'fold-right', | ||
464 | - ]} | ||
465 | - > | ||
466 | - <DoubleRightOutlined /> | ||
467 | - </span> | ||
468 | </div> | 450 | </div> |
469 | ); | 451 | ); |
470 | }; | 452 | }; |
@@ -481,6 +463,7 @@ | @@ -481,6 +463,7 @@ | ||
481 | top: 0.85rem; | 463 | top: 0.85rem; |
482 | left: 1.1vw; | 464 | left: 1.1vw; |
483 | } | 465 | } |
466 | + | ||
484 | .fold-right { | 467 | .fold-right { |
485 | z-index: 1; | 468 | z-index: 1; |
486 | cursor: pointer; | 469 | cursor: pointer; |
@@ -29,6 +29,7 @@ | @@ -29,6 +29,7 @@ | ||
29 | import { useLockPage } from '/@/hooks/web/useLockPage'; | 29 | import { useLockPage } from '/@/hooks/web/useLockPage'; |
30 | 30 | ||
31 | import { useAppInject } from '/@/hooks/web/useAppInject'; | 31 | import { useAppInject } from '/@/hooks/web/useAppInject'; |
32 | + import { useAlarmNotify } from '/@/views/alarm/log/hook/useAlarmNotify'; | ||
32 | 33 | ||
33 | export default defineComponent({ | 34 | export default defineComponent({ |
34 | name: 'DefaultLayout', | 35 | name: 'DefaultLayout', |
@@ -57,7 +58,7 @@ | @@ -57,7 +58,7 @@ | ||
57 | } | 58 | } |
58 | return cls; | 59 | return cls; |
59 | }); | 60 | }); |
60 | - | 61 | + useAlarmNotify(); |
61 | return { | 62 | return { |
62 | getShowFullHeaderRef, | 63 | getShowFullHeaderRef, |
63 | getShowSidebar, | 64 | getShowSidebar, |
@@ -4,6 +4,9 @@ import { notification, Button, Tag } from 'ant-design-vue'; | @@ -4,6 +4,9 @@ import { notification, Button, Tag } from 'ant-design-vue'; | ||
4 | import { h, onMounted, onUnmounted } from 'vue'; | 4 | import { h, onMounted, onUnmounted } from 'vue'; |
5 | import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; | 5 | import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; |
6 | import { alarmLevel } from '/@/views/device/list/config/detail.config'; | 6 | import { alarmLevel } from '/@/views/device/list/config/detail.config'; |
7 | +import { RoleEnum } from '/@/enums/roleEnum'; | ||
8 | +import { usePermission } from '/@/hooks/web/usePermission'; | ||
9 | +import { useUserStore } from '/@/store/modules/user'; | ||
7 | 10 | ||
8 | interface UseAlarmNotifyParams { | 11 | interface UseAlarmNotifyParams { |
9 | alarmNotifyStatus?: AlarmStatus; | 12 | alarmNotifyStatus?: AlarmStatus; |
@@ -44,7 +47,7 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) { | @@ -44,7 +47,7 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) { | ||
44 | 47 | ||
45 | notification.open({ | 48 | notification.open({ |
46 | message: '设备告警', | 49 | message: '设备告警', |
47 | - duration, | 50 | + duration: Number(duration), |
48 | key, | 51 | key, |
49 | description: h('div', {}, [ | 52 | description: h('div', {}, [ |
50 | h('div', { style: { marginRight: '5px' } }, [ | 53 | h('div', { style: { marginRight: '5px' } }, [ |
@@ -88,8 +91,22 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) { | @@ -88,8 +91,22 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) { | ||
88 | }, interval); | 91 | }, interval); |
89 | }; | 92 | }; |
90 | 93 | ||
94 | + const { hasPermission } = usePermission(); | ||
95 | + | ||
91 | onMounted(() => { | 96 | onMounted(() => { |
92 | - polling(); | 97 | + const alarmPermissionKey = 'api:alarm:global:notify'; |
98 | + const hasPermissionRole = [RoleEnum.CUSTOMER_USER, RoleEnum.TENANT_ADMIN]; | ||
99 | + const userInfo = useUserStore().getUserInfo; | ||
100 | + const userRoles = userInfo.roles || []; | ||
101 | + | ||
102 | + const getPermissionFlag = () => { | ||
103 | + for (const item of userRoles) { | ||
104 | + const flag = hasPermissionRole.find((each) => item === each); | ||
105 | + if (flag) return true; | ||
106 | + } | ||
107 | + return false; | ||
108 | + }; | ||
109 | + if (hasPermission(alarmPermissionKey) && getPermissionFlag()) polling(); | ||
93 | }); | 110 | }); |
94 | 111 | ||
95 | onUnmounted(() => { | 112 | onUnmounted(() => { |
@@ -19,6 +19,12 @@ | @@ -19,6 +19,12 @@ | ||
19 | /> | 19 | /> |
20 | </div> | 20 | </div> |
21 | </div> | 21 | </div> |
22 | + <!-- <div | ||
23 | + class="bg-black h-80 text-light-50 w-full h-full flex justify-center items-center" | ||
24 | + v-if="!showVideo" | ||
25 | + > | ||
26 | + 视频播放出错啦! | ||
27 | + </div> --> | ||
22 | </BasicModal> | 28 | </BasicModal> |
23 | </div> | 29 | </div> |
24 | </template> | 30 | </template> |
@@ -28,14 +34,9 @@ | @@ -28,14 +34,9 @@ | ||
28 | import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; | 34 | import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; |
29 | import { videoPlay } from 'vue3-video-play'; // 引入组件 | 35 | import { videoPlay } from 'vue3-video-play'; // 引入组件 |
30 | import 'vue3-video-play/dist/style.css'; // 引入css | 36 | import 'vue3-video-play/dist/style.css'; // 引入css |
31 | - import { AccessMode } from './config.data'; | 37 | + import { AccessMode, MediaType } from './config.data'; |
32 | import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | 38 | import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; |
33 | 39 | ||
34 | - enum MediaType { | ||
35 | - MP4 = 'mp4', | ||
36 | - M3U8 = 'm3u8', | ||
37 | - } | ||
38 | - | ||
39 | const heightNum = ref(800); | 40 | const heightNum = ref(800); |
40 | const showVideo = ref(false); | 41 | const showVideo = ref(false); |
41 | const options = reactive({ | 42 | const options = reactive({ |
@@ -94,7 +95,10 @@ | @@ -94,7 +95,10 @@ | ||
94 | options.src = url; | 95 | options.src = url; |
95 | const type = (url as CameraModel).videoUrl.replace(reg, ''); | 96 | const type = (url as CameraModel).videoUrl.replace(reg, ''); |
96 | options.type = getMediaType(type); | 97 | options.type = getMediaType(type); |
97 | - } catch (error) {} | 98 | + } catch (error) { |
99 | + } finally { | ||
100 | + showVideo.value = true; | ||
101 | + } | ||
98 | } | 102 | } |
99 | } | 103 | } |
100 | ); | 104 | ); |
@@ -2,33 +2,42 @@ | @@ -2,33 +2,42 @@ | ||
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 { computed, onMounted, reactive, ref, unref, watch } from 'vue'; | 4 | import { computed, onMounted, reactive, ref, unref, watch } from 'vue'; |
5 | - import { Tabs, Row, Col, Spin, Button } from 'ant-design-vue'; | 5 | + import { Tabs, Row, Col, Spin, Button, Pagination, Empty } from 'ant-design-vue'; |
6 | import { cameraPage } from '/@/api/camera/cameraManager'; | 6 | import { cameraPage } from '/@/api/camera/cameraManager'; |
7 | import { CameraRecord } from '/@/api/camera/model/cameraModel'; | 7 | import { CameraRecord } from '/@/api/camera/model/cameraModel'; |
8 | - import { videoPlay as VideoPlay } from 'vue3-video-play'; // 引入组件 | ||
9 | - import 'vue3-video-play/dist/style.css'; // 引入css | 8 | + import { videoPlay as VideoPlay } from 'vue3-video-play'; |
9 | + import 'vue3-video-play/dist/style.css'; | ||
10 | import { useFullscreen } from '@vueuse/core'; | 10 | import { useFullscreen } from '@vueuse/core'; |
11 | import CameraDrawer from './CameraDrawer.vue'; | 11 | import CameraDrawer from './CameraDrawer.vue'; |
12 | import { useDrawer } from '/@/components/Drawer'; | 12 | import { useDrawer } from '/@/components/Drawer'; |
13 | - import { PageMode } from './config.data'; | 13 | + import { AccessMode, MediaType, PageMode } from './config.data'; |
14 | import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; | 14 | import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; |
15 | + import { isDef } from '/@/utils/is'; | ||
16 | + import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | ||
17 | + | ||
18 | + type CameraRecordItem = CameraRecord & { | ||
19 | + canPlay?: boolean; | ||
20 | + type?: string; | ||
21 | + isTransform?: boolean; | ||
22 | + }; | ||
15 | 23 | ||
16 | const emit = defineEmits(['switchMode']); | 24 | const emit = defineEmits(['switchMode']); |
17 | const organizationIdTreeRef = ref(null); | 25 | const organizationIdTreeRef = ref(null); |
18 | const videoContainer = ref<Nullable<HTMLDivElement>>(null); | 26 | const videoContainer = ref<Nullable<HTMLDivElement>>(null); |
19 | const activeKey = ref(PageMode.SPLIT_SCREEN_MODE); | 27 | const activeKey = ref(PageMode.SPLIT_SCREEN_MODE); |
20 | - const cameraList = ref<CameraRecord[]>([]); | 28 | + const cameraList = ref<CameraRecordItem[]>([]); |
21 | const organizationId = ref<Nullable<string>>(null); | 29 | const organizationId = ref<Nullable<string>>(null); |
22 | const loading = ref(false); | 30 | const loading = ref(false); |
23 | const pagination = reactive({ | 31 | const pagination = reactive({ |
24 | page: 1, | 32 | page: 1, |
25 | pageSize: 4, | 33 | pageSize: 4, |
26 | colNumber: 2, | 34 | colNumber: 2, |
35 | + total: 0, | ||
27 | }); | 36 | }); |
28 | 37 | ||
29 | const options = reactive({ | 38 | const options = reactive({ |
30 | - width: '800px', | ||
31 | - height: '450px', | 39 | + width: '200px', |
40 | + height: '200px', | ||
32 | color: '#409eff', | 41 | color: '#409eff', |
33 | muted: false, //静音 | 42 | muted: false, //静音 |
34 | webFullScreen: false, | 43 | webFullScreen: false, |
@@ -39,9 +48,7 @@ | @@ -39,9 +48,7 @@ | ||
39 | ligthOff: false, //关灯模式 | 48 | ligthOff: false, //关灯模式 |
40 | volume: 0.3, //默认音量大小 | 49 | volume: 0.3, //默认音量大小 |
41 | control: true, //是否显示控制器 | 50 | control: true, //是否显示控制器 |
42 | - title: '', //视频名称 | ||
43 | type: 'm3u8', | 51 | type: 'm3u8', |
44 | - src: '', //视频源 | ||
45 | controlBtns: [ | 52 | controlBtns: [ |
46 | 'audioTrack', | 53 | 'audioTrack', |
47 | 'quality', | 54 | 'quality', |
@@ -68,21 +75,63 @@ | @@ -68,21 +75,63 @@ | ||
68 | const getCameraList = async () => { | 75 | const getCameraList = async () => { |
69 | try { | 76 | try { |
70 | loading.value = true; | 77 | loading.value = true; |
71 | - const { items } = await cameraPage({ | ||
72 | - page: 1, | 78 | + const { items, total } = await cameraPage({ |
79 | + page: pagination.page, | ||
73 | pageSize: pagination.pageSize, | 80 | pageSize: pagination.pageSize, |
74 | organizationId: unref(organizationId)!, | 81 | organizationId: unref(organizationId)!, |
75 | }); | 82 | }); |
83 | + pagination.total = total; | ||
84 | + | ||
85 | + for (const item of items) { | ||
86 | + // await beforeVideoPlay(item); | ||
87 | + (item as CameraRecordItem).isTransform = false; | ||
88 | + beforeVideoPlay(item); | ||
89 | + } | ||
76 | cameraList.value = items; | 90 | cameraList.value = items; |
77 | } catch (error) { | 91 | } catch (error) { |
78 | } finally { | 92 | } finally { |
79 | loading.value = false; | 93 | loading.value = false; |
80 | } | 94 | } |
81 | }; | 95 | }; |
96 | + const getMediaType = (suffix: string) => { | ||
97 | + return suffix === MediaType.M3U8 ? suffix : `video/${suffix}`; | ||
98 | + }; | ||
99 | + | ||
100 | + const beforeVideoPlay = async (record: CameraRecordItem) => { | ||
101 | + let reg = /(?:.*)(?<=\.)/; | ||
102 | + if (record.accessMode === AccessMode.ManuallyEnter) { | ||
103 | + if (record.videoUrl) { | ||
104 | + const type = record.videoUrl.replace(reg, ''); | ||
105 | + record.type = getMediaType(type); | ||
106 | + record.isTransform = true; | ||
107 | + } | ||
108 | + } | ||
109 | + if (record.accessMode === AccessMode.Streaming) { | ||
110 | + try { | ||
111 | + const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); | ||
112 | + const type = url.replace(reg, ''); | ||
113 | + const index = unref(cameraList).findIndex((item) => item.id === record.id); | ||
114 | + if (~index) { | ||
115 | + const oldRecord = unref(cameraList).at(index)!; | ||
116 | + unref(cameraList)[index] = { | ||
117 | + ...oldRecord, | ||
118 | + videoUrl: url, | ||
119 | + type: getMediaType(type), | ||
120 | + isTransform: true, | ||
121 | + }; | ||
122 | + } | ||
123 | + } catch (error) { | ||
124 | + } finally { | ||
125 | + const index = unref(cameraList).findIndex((item) => item.id === record.id); | ||
126 | + if (~index) unref(cameraList)[index].isTransform = true; | ||
127 | + } | ||
128 | + } | ||
129 | + }; | ||
82 | 130 | ||
83 | const handleSwitchLayoutWay = (pageSize: number, layout: number) => { | 131 | const handleSwitchLayoutWay = (pageSize: number, layout: number) => { |
84 | pagination.colNumber = layout; | 132 | pagination.colNumber = layout; |
85 | pagination.pageSize = pageSize; | 133 | pagination.pageSize = pageSize; |
134 | + pagination.page = 1; | ||
86 | getCameraList(); | 135 | getCameraList(); |
87 | }; | 136 | }; |
88 | 137 | ||
@@ -104,6 +153,20 @@ | @@ -104,6 +153,20 @@ | ||
104 | } | 153 | } |
105 | }; | 154 | }; |
106 | 155 | ||
156 | + const handleLoadStart = (record: CameraRecordItem) => { | ||
157 | + const index = unref(cameraList).findIndex((item) => item.id === record.id); | ||
158 | + setTimeout(() => { | ||
159 | + ~index && | ||
160 | + !unref(cameraList).at(index)!.canPlay && | ||
161 | + (unref(cameraList).at(index)!.canPlay = false); | ||
162 | + }, 30000); | ||
163 | + }; | ||
164 | + | ||
165 | + const handleLoadData = (record: CameraRecordItem) => { | ||
166 | + const index = unref(cameraList).findIndex((item) => item.id === record.id); | ||
167 | + ~index && (unref(cameraList).at(index)!.canPlay = true); | ||
168 | + }; | ||
169 | + | ||
107 | const [registerDrawer, { openDrawer }] = useDrawer(); | 170 | const [registerDrawer, { openDrawer }] = useDrawer(); |
108 | 171 | ||
109 | const handleAddCamera = () => { | 172 | const handleAddCamera = () => { |
@@ -145,6 +208,18 @@ | @@ -145,6 +208,18 @@ | ||
145 | > | 208 | > |
146 | <SvgIcon class="text-2xl" prefix="iconfont" name="grid-nine" /> | 209 | <SvgIcon class="text-2xl" prefix="iconfont" name="grid-nine" /> |
147 | </div> | 210 | </div> |
211 | + <div> | ||
212 | + <Pagination | ||
213 | + v-model:current="pagination.page" | ||
214 | + :total="pagination.total" | ||
215 | + :page-size="pagination.pageSize" | ||
216 | + :show-size-changer="false" | ||
217 | + @change="getCameraList" | ||
218 | + :simple="true" | ||
219 | + :show-quick-jumper="true" | ||
220 | + :show-total="(total) => `共 ${total} 条`" | ||
221 | + /> | ||
222 | + </div> | ||
148 | </div> | 223 | </div> |
149 | <div class="flex items-center gap-4"> | 224 | <div class="flex items-center gap-4"> |
150 | <div class="flex"> | 225 | <div class="flex"> |
@@ -159,6 +234,10 @@ | @@ -159,6 +234,10 @@ | ||
159 | </div> | 234 | </div> |
160 | <section ref="videoContainer" class="bg-light-50 flex-auto"> | 235 | <section ref="videoContainer" class="bg-light-50 flex-auto"> |
161 | <Spin :spinning="loading" class="h-full"> | 236 | <Spin :spinning="loading" class="h-full"> |
237 | + <Empty | ||
238 | + class="h-full flex flex-col justify-center items-center" | ||
239 | + v-if="!cameraList.length" | ||
240 | + /> | ||
162 | <Row :gutter="16" class="h-full mx-0"> | 241 | <Row :gutter="16" class="h-full mx-0"> |
163 | <Col | 242 | <Col |
164 | v-for="item in cameraList" | 243 | v-for="item in cameraList" |
@@ -168,8 +247,31 @@ | @@ -168,8 +247,31 @@ | ||
168 | :span="getColLayout" | 247 | :span="getColLayout" |
169 | > | 248 | > |
170 | <div class="box-border w-full h-full p-3"> | 249 | <div class="box-border w-full h-full p-3"> |
171 | - <div class="bg-yellow-50 w-full h-full overflow-hidden"> | ||
172 | - <VideoPlay v-bind="options" :src="item.videoUrl" /> | 250 | + <div class="bg-black w-full h-full overflow-hidden relative video-container"> |
251 | + <Spin v-show="!item.isTransform" :spinning="!item.isTransform"> | ||
252 | + <div class="bg-black text-light-50"> </div> | ||
253 | + </Spin> | ||
254 | + <VideoPlay | ||
255 | + v-show="item.isTransform" | ||
256 | + @loadstart="handleLoadStart(item)" | ||
257 | + @loadeddata="handleLoadData(item)" | ||
258 | + v-bind="options" | ||
259 | + :src="item.videoUrl" | ||
260 | + :title="item.name" | ||
261 | + :type="item.type" | ||
262 | + /> | ||
263 | + <div | ||
264 | + v-if="item.isTransform && isDef(item.canPlay) && !item.canPlay" | ||
265 | + class="video-container-error-msk absolute top-0 left-0 text-lg w-full h-full text-light-50 flex justify-center items-center z-50 bg-black" | ||
266 | + > | ||
267 | + 视频加载出错了! | ||
268 | + </div> | ||
269 | + <div | ||
270 | + class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center" | ||
271 | + style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" | ||
272 | + > | ||
273 | + <span>{{ item.name }}</span> | ||
274 | + </div> | ||
173 | </div> | 275 | </div> |
174 | </div> | 276 | </div> |
175 | </Col> | 277 | </Col> |
@@ -203,4 +305,33 @@ | @@ -203,4 +305,33 @@ | ||
203 | .split-screen-mode:deep(.ant-tabs-tab-active) { | 305 | .split-screen-mode:deep(.ant-tabs-tab-active) { |
204 | border-bottom: 1px solid #eee; | 306 | border-bottom: 1px solid #eee; |
205 | } | 307 | } |
308 | + | ||
309 | + .split-screen-mode:deep(video) { | ||
310 | + position: absolute; | ||
311 | + height: calc(100%) !important; | ||
312 | + } | ||
313 | + | ||
314 | + .split-screen-mode:deep(.d-player-control) { | ||
315 | + z-index: 99; | ||
316 | + } | ||
317 | + | ||
318 | + .video-container { | ||
319 | + .video-container-mask { | ||
320 | + opacity: 0; | ||
321 | + transition: opacity 0.5; | ||
322 | + pointer-events: none; | ||
323 | + } | ||
324 | + | ||
325 | + &:hover { | ||
326 | + .video-container-mask { | ||
327 | + opacity: 1; | ||
328 | + } | ||
329 | + | ||
330 | + .video-container-error-msk { | ||
331 | + // opacity: 0; | ||
332 | + // visibility: hidden; | ||
333 | + color: #000; | ||
334 | + } | ||
335 | + } | ||
336 | + } | ||
206 | </style> | 337 | </style> |
@@ -29,6 +29,11 @@ export enum PageMode { | @@ -29,6 +29,11 @@ export enum PageMode { | ||
29 | FULL_SCREEN_MODE = 'fullScreenMode', | 29 | FULL_SCREEN_MODE = 'fullScreenMode', |
30 | } | 30 | } |
31 | 31 | ||
32 | +export enum MediaType { | ||
33 | + MP4 = 'mp4', | ||
34 | + M3U8 = 'm3u8', | ||
35 | +} | ||
36 | + | ||
32 | // 表格列数据 | 37 | // 表格列数据 |
33 | export const columns: BasicColumn[] = [ | 38 | export const columns: BasicColumn[] = [ |
34 | { | 39 | { |
@@ -58,7 +63,7 @@ export const columns: BasicColumn[] = [ | @@ -58,7 +63,7 @@ export const columns: BasicColumn[] = [ | ||
58 | width: 160, | 63 | width: 160, |
59 | }, | 64 | }, |
60 | { | 65 | { |
61 | - title: '接受方式', | 66 | + title: '获取方式', |
62 | dataIndex: 'accessMode', | 67 | dataIndex: 'accessMode', |
63 | width: 100, | 68 | width: 100, |
64 | slots: { customRender: 'accessMode' }, | 69 | slots: { customRender: 'accessMode' }, |
1 | <template> | 1 | <template> |
2 | - <div style="position: absolute"> </div> | ||
3 | - <div class="bg-white m-4 mr-0 overflow-hidden"> | ||
4 | - <BasicTree | ||
5 | - title="组织列表" | ||
6 | - toolbar | ||
7 | - search | ||
8 | - :clickRowToExpand="false" | ||
9 | - :treeData="treeData" | ||
10 | - :expandedKeys="treeExpandData" | ||
11 | - :replaceFields="{ key: 'id', title: 'name' }" | ||
12 | - :selectedKeys="selectedKeys" | ||
13 | - @select="handleSelect" | ||
14 | - v-bind="$attrs" | ||
15 | - /> | 2 | + <div class="organization-tree flex relative"> |
3 | + <div class="cursor-pointer flex py-4 fold-icon" :class="foldFlag ? 'absolute' : ''"> | ||
4 | + <div @click="handleFold"> | ||
5 | + <DoubleRightOutlined :class="[foldFlag ? '' : 'rotate-180']" class="text-xl transform" /> | ||
6 | + </div> | ||
7 | + </div> | ||
8 | + <div | ||
9 | + :style="{ width: foldFlag ? '0px' : '100%' }" | ||
10 | + :class="[foldFlag ? '' : 'my-4 ml-2']" | ||
11 | + class="bg-white mr-0 overflow-hidden" | ||
12 | + > | ||
13 | + <BasicTree | ||
14 | + title="组织列表" | ||
15 | + toolbar | ||
16 | + search | ||
17 | + :clickRowToExpand="false" | ||
18 | + :treeData="treeData" | ||
19 | + :expandedKeys="treeExpandData" | ||
20 | + :replaceFields="{ key: 'id', title: 'name' }" | ||
21 | + :selectedKeys="selectedKeys" | ||
22 | + @select="handleSelect" | ||
23 | + v-bind="$attrs" | ||
24 | + /> | ||
25 | + </div> | ||
16 | </div> | 26 | </div> |
17 | </template> | 27 | </template> |
18 | <script lang="ts" setup name="OrganizationIdTree"> | 28 | <script lang="ts" setup name="OrganizationIdTree"> |
19 | - import { onMounted, ref } from 'vue'; | 29 | + import { onMounted, ref, unref } from 'vue'; |
20 | import { BasicTree, TreeItem } from '/@/components/Tree'; | 30 | import { BasicTree, TreeItem } from '/@/components/Tree'; |
21 | import { getOrganizationList } from '/@/api/system/system'; | 31 | import { getOrganizationList } from '/@/api/system/system'; |
32 | + import { DoubleRightOutlined } from '@ant-design/icons-vue'; | ||
22 | 33 | ||
23 | const emit = defineEmits(['select']); | 34 | const emit = defineEmits(['select']); |
24 | const treeData = ref<TreeItem[]>([]); | 35 | const treeData = ref<TreeItem[]>([]); |
@@ -37,6 +48,12 @@ | @@ -37,6 +48,12 @@ | ||
37 | function resetOrganization() { | 48 | function resetOrganization() { |
38 | selectedKeys.value = []; | 49 | selectedKeys.value = []; |
39 | } | 50 | } |
51 | + | ||
52 | + const foldFlag = ref(true); | ||
53 | + const handleFold = () => { | ||
54 | + foldFlag.value = !unref(foldFlag); | ||
55 | + }; | ||
56 | + | ||
40 | onMounted(async () => { | 57 | onMounted(async () => { |
41 | treeData.value = (await getOrganizationList()) as unknown as TreeItem[]; | 58 | treeData.value = (await getOrganizationList()) as unknown as TreeItem[]; |
42 | const getAllIds = findForAllId(treeData.value as any, []); | 59 | const getAllIds = findForAllId(treeData.value as any, []); |
@@ -47,3 +64,17 @@ | @@ -47,3 +64,17 @@ | ||
47 | resetOrganization, | 64 | resetOrganization, |
48 | }); | 65 | }); |
49 | </script> | 66 | </script> |
67 | + | ||
68 | +<style scoped lang="less"> | ||
69 | + .organization-tree { | ||
70 | + .expand { | ||
71 | + opacity: 0; | ||
72 | + } | ||
73 | + | ||
74 | + &:hover { | ||
75 | + .expand { | ||
76 | + opacity: 1; | ||
77 | + } | ||
78 | + } | ||
79 | + } | ||
80 | +</style> |
@@ -35,7 +35,7 @@ export const formSchema: FormSchema[] = [ | @@ -35,7 +35,7 @@ export const formSchema: FormSchema[] = [ | ||
35 | }, | 35 | }, |
36 | { | 36 | { |
37 | field: 'viewType', | 37 | field: 'viewType', |
38 | - label: '名称', | 38 | + label: '公开性', |
39 | component: 'RadioGroup', | 39 | component: 'RadioGroup', |
40 | defaultValue: ViewType.PRIVATE_VIEW, | 40 | defaultValue: ViewType.PRIVATE_VIEW, |
41 | helpMessage: [ | 41 | helpMessage: [ |
@@ -45,7 +45,7 @@ export const formSchema: FormSchema[] = [ | @@ -45,7 +45,7 @@ export const formSchema: FormSchema[] = [ | ||
45 | placeholder: '请选择公开性', | 45 | placeholder: '请选择公开性', |
46 | options: [ | 46 | options: [ |
47 | { label: '私有看板', value: ViewType.PRIVATE_VIEW }, | 47 | { label: '私有看板', value: ViewType.PRIVATE_VIEW }, |
48 | - { label: '公开看板', value: ViewType.PUBLIC_VIEW }, | 48 | + { label: '公开看板', value: ViewType.PUBLIC_VIEW, disabled: true }, |
49 | ], | 49 | ], |
50 | }, | 50 | }, |
51 | }, | 51 | }, |