Commit fc7bf0322625f7cfa0cf24e0aee24059bdee8ced

Authored by fengtao
2 parents 117ec55d 0950ebd2

Merge branch 'main' into ft_local_dev

... ... @@ -45,5 +45,5 @@ VITE_CONTENT_SECURITY_POLICY = false
45 45 # Alarm Notify Polling Interval Time
46 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 46 # Alarm Notify Polling Interval Time
47 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 8
9 9 <script lang="ts" setup>
10 10 import { ConfigProvider } from 'ant-design-vue';
11   - import { useAlarmNotify } from './views/alarm/log/hook/useAlarmNotify';
12 11 import { AppProvider } from '/@/components/Application';
13 12 import { useTitle } from '/@/hooks/web/useTitle';
14 13 import { useLocale } from '/@/locales/useLocale';
... ... @@ -16,5 +15,4 @@
16 15 const { getAntdLocale } = useLocale();
17 16
18 17 useTitle();
19   - useAlarmNotify();
20 18 </script>
... ...
... ... @@ -31,7 +31,6 @@
31 31 import { CreateContextOptions } from '/@/components/ContextMenu';
32 32
33 33 import { CheckEvent } from './typing';
34   - import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
35 34
36 35 interface State {
37 36 expandedKeys: Keys;
... ... @@ -52,13 +51,13 @@
52 51 'update:searchValue',
53 52 ],
54 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 61 //ft
63 62 const state = reactive<State>({
64 63 checkStrictly: props.checkStrictly,
... ... @@ -234,9 +233,9 @@
234 233 }
235 234
236 235 //ft
237   - function handleFlodOrUnFoldFunc(v) {
238   - isFlod.value = v;
239   - }
  236 + // function handleFlodOrUnFoldFunc(v) {
  237 + // isFlod.value = v;
  238 + // }
240 239 //ft
241 240 function handleClickNode(key: string, children: TreeItem[]) {
242 241 if (!props.clickRowToExpand || !children || children.length === 0) return;
... ... @@ -419,7 +418,7 @@
419 418 const showTitle = title || toolbar || search || slots.headerTitle;
420 419 const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
421 420 return (
422   - <div style={isFlod.value ? '' : 'width:0vw'} class={[prefixCls, 'h-full', attrs.class]}>
  421 + <div class={[prefixCls, 'h-full', attrs.class]}>
423 422 {showTitle && (
424 423 <TreeHeader
425 424 style={'position:relative'}
... ... @@ -448,23 +447,6 @@
448 447 </ScrollContainer>
449 448
450 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 450 </div>
469 451 );
470 452 };
... ... @@ -481,6 +463,7 @@
481 463 top: 0.85rem;
482 464 left: 1.1vw;
483 465 }
  466 +
484 467 .fold-right {
485 468 z-index: 1;
486 469 cursor: pointer;
... ...
... ... @@ -29,6 +29,7 @@
29 29 import { useLockPage } from '/@/hooks/web/useLockPage';
30 30
31 31 import { useAppInject } from '/@/hooks/web/useAppInject';
  32 + import { useAlarmNotify } from '/@/views/alarm/log/hook/useAlarmNotify';
32 33
33 34 export default defineComponent({
34 35 name: 'DefaultLayout',
... ... @@ -57,7 +58,7 @@
57 58 }
58 59 return cls;
59 60 });
60   -
  61 + useAlarmNotify();
61 62 return {
62 63 getShowFullHeaderRef,
63 64 getShowSidebar,
... ...
... ... @@ -4,6 +4,9 @@ import { notification, Button, Tag } from 'ant-design-vue';
4 4 import { h, onMounted, onUnmounted } from 'vue';
5 5 import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
6 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 11 interface UseAlarmNotifyParams {
9 12 alarmNotifyStatus?: AlarmStatus;
... ... @@ -44,7 +47,7 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) {
44 47
45 48 notification.open({
46 49 message: '设备告警',
47   - duration,
  50 + duration: Number(duration),
48 51 key,
49 52 description: h('div', {}, [
50 53 h('div', { style: { marginRight: '5px' } }, [
... ... @@ -88,8 +91,22 @@ export function useAlarmNotify(params: UseAlarmNotifyParams = {}) {
88 91 }, interval);
89 92 };
90 93
  94 + const { hasPermission } = usePermission();
  95 +
91 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 112 onUnmounted(() => {
... ...
... ... @@ -19,6 +19,12 @@
19 19 />
20 20 </div>
21 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 28 </BasicModal>
23 29 </div>
24 30 </template>
... ... @@ -28,14 +34,9 @@
28 34 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
29 35 import { videoPlay } from 'vue3-video-play'; // 引入组件
30 36 import 'vue3-video-play/dist/style.css'; // 引入css
31   - import { AccessMode } from './config.data';
  37 + import { AccessMode, MediaType } from './config.data';
32 38 import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
33 39
34   - enum MediaType {
35   - MP4 = 'mp4',
36   - M3U8 = 'm3u8',
37   - }
38   -
39 40 const heightNum = ref(800);
40 41 const showVideo = ref(false);
41 42 const options = reactive({
... ... @@ -94,7 +95,10 @@
94 95 options.src = url;
95 96 const type = (url as CameraModel).videoUrl.replace(reg, '');
96 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 2 import { PageWrapper } from '/@/components/Page';
3 3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
4 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 6 import { cameraPage } from '/@/api/camera/cameraManager';
7 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8   - import { videoPlay as VideoPlay } from 'vue3-video-play'; // 引入组件
9   - 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 10 import { useFullscreen } from '@vueuse/core';
11 11 import CameraDrawer from './CameraDrawer.vue';
12 12 import { useDrawer } from '/@/components/Drawer';
13   - import { PageMode } from './config.data';
  13 + import { AccessMode, MediaType, PageMode } from './config.data';
14 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 24 const emit = defineEmits(['switchMode']);
17 25 const organizationIdTreeRef = ref(null);
18 26 const videoContainer = ref<Nullable<HTMLDivElement>>(null);
19 27 const activeKey = ref(PageMode.SPLIT_SCREEN_MODE);
20   - const cameraList = ref<CameraRecord[]>([]);
  28 + const cameraList = ref<CameraRecordItem[]>([]);
21 29 const organizationId = ref<Nullable<string>>(null);
22 30 const loading = ref(false);
23 31 const pagination = reactive({
24 32 page: 1,
25 33 pageSize: 4,
26 34 colNumber: 2,
  35 + total: 0,
27 36 });
28 37
29 38 const options = reactive({
30   - width: '800px',
31   - height: '450px',
  39 + width: '200px',
  40 + height: '200px',
32 41 color: '#409eff',
33 42 muted: false, //静音
34 43 webFullScreen: false,
... ... @@ -39,9 +48,7 @@
39 48 ligthOff: false, //关灯模式
40 49 volume: 0.3, //默认音量大小
41 50 control: true, //是否显示控制器
42   - title: '', //视频名称
43 51 type: 'm3u8',
44   - src: '', //视频源
45 52 controlBtns: [
46 53 'audioTrack',
47 54 'quality',
... ... @@ -68,21 +75,63 @@
68 75 const getCameraList = async () => {
69 76 try {
70 77 loading.value = true;
71   - const { items } = await cameraPage({
72   - page: 1,
  78 + const { items, total } = await cameraPage({
  79 + page: pagination.page,
73 80 pageSize: pagination.pageSize,
74 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 90 cameraList.value = items;
77 91 } catch (error) {
78 92 } finally {
79 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 131 const handleSwitchLayoutWay = (pageSize: number, layout: number) => {
84 132 pagination.colNumber = layout;
85 133 pagination.pageSize = pageSize;
  134 + pagination.page = 1;
86 135 getCameraList();
87 136 };
88 137
... ... @@ -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 170 const [registerDrawer, { openDrawer }] = useDrawer();
108 171
109 172 const handleAddCamera = () => {
... ... @@ -145,6 +208,18 @@
145 208 >
146 209 <SvgIcon class="text-2xl" prefix="iconfont" name="grid-nine" />
147 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 223 </div>
149 224 <div class="flex items-center gap-4">
150 225 <div class="flex">
... ... @@ -159,6 +234,10 @@
159 234 </div>
160 235 <section ref="videoContainer" class="bg-light-50 flex-auto">
161 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 241 <Row :gutter="16" class="h-full mx-0">
163 242 <Col
164 243 v-for="item in cameraList"
... ... @@ -168,8 +247,31 @@
168 247 :span="getColLayout"
169 248 >
170 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 275 </div>
174 276 </div>
175 277 </Col>
... ... @@ -203,4 +305,33 @@
203 305 .split-screen-mode:deep(.ant-tabs-tab-active) {
204 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 337 </style>
... ...
... ... @@ -29,6 +29,11 @@ export enum PageMode {
29 29 FULL_SCREEN_MODE = 'fullScreenMode',
30 30 }
31 31
  32 +export enum MediaType {
  33 + MP4 = 'mp4',
  34 + M3U8 = 'm3u8',
  35 +}
  36 +
32 37 // 表格列数据
33 38 export const columns: BasicColumn[] = [
34 39 {
... ... @@ -58,7 +63,7 @@ export const columns: BasicColumn[] = [
58 63 width: 160,
59 64 },
60 65 {
61   - title: '接受方式',
  66 + title: '获取方式',
62 67 dataIndex: 'accessMode',
63 68 width: 100,
64 69 slots: { customRender: 'accessMode' },
... ...
1 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 26 </div>
17 27 </template>
18 28 <script lang="ts" setup name="OrganizationIdTree">
19   - import { onMounted, ref } from 'vue';
  29 + import { onMounted, ref, unref } from 'vue';
20 30 import { BasicTree, TreeItem } from '/@/components/Tree';
21 31 import { getOrganizationList } from '/@/api/system/system';
  32 + import { DoubleRightOutlined } from '@ant-design/icons-vue';
22 33
23 34 const emit = defineEmits(['select']);
24 35 const treeData = ref<TreeItem[]>([]);
... ... @@ -37,6 +48,12 @@
37 48 function resetOrganization() {
38 49 selectedKeys.value = [];
39 50 }
  51 +
  52 + const foldFlag = ref(true);
  53 + const handleFold = () => {
  54 + foldFlag.value = !unref(foldFlag);
  55 + };
  56 +
40 57 onMounted(async () => {
41 58 treeData.value = (await getOrganizationList()) as unknown as TreeItem[];
42 59 const getAllIds = findForAllId(treeData.value as any, []);
... ... @@ -47,3 +64,17 @@
47 64 resetOrganization,
48 65 });
49 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 35 },
36 36 {
37 37 field: 'viewType',
38   - label: '名称',
  38 + label: '公开性',
39 39 component: 'RadioGroup',
40 40 defaultValue: ViewType.PRIVATE_VIEW,
41 41 helpMessage: [
... ... @@ -45,7 +45,7 @@ export const formSchema: FormSchema[] = [
45 45 placeholder: '请选择公开性',
46 46 options: [
47 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 },
... ...