fix: merge
This commit is contained in:
commit
787df89acd
|
@ -0,0 +1,27 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
export const query = (data: any) =>
|
||||
server.post(`/data-collect/channel/_query`, data);
|
||||
|
||||
export const remove = (id: string) =>
|
||||
server.remove(`/data-collect/channel/${id}`);
|
||||
|
||||
export const save = (data: any) => server.post(`/data-collect/channel`, data);
|
||||
|
||||
export const update = (id: string, data: any) =>
|
||||
server.put(`/data-collect/channel/${id}`, data);
|
||||
|
||||
export const getProviders = () => server.get(`/gateway/device/providers`);
|
||||
|
||||
export const queryOptionsList = (type: strimg) =>
|
||||
server.get(`/data-collect/opc/${type}`);
|
||||
|
||||
export const validateField = (data: any) =>
|
||||
server.post(`/data-collect/opc/endpoint/_validate`, data, null, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
},
|
||||
});
|
||||
|
||||
export const queryCertificateList = () =>
|
||||
server.get(`/network/certificate/_query/no-paging?paging=false`, {});
|
|
@ -0,0 +1,6 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
export const queryCount = (type: string, data: any) =>
|
||||
server.post(`/data-collect/${type}/_count`, data);
|
||||
|
||||
export const dashboard = (data: any) => server.post(`/dashboard/_multi`, data);
|
|
@ -35,8 +35,12 @@ export default {
|
|||
// 更改国标ID
|
||||
updateGbChannelId: (id: string, data: any): any => server.put(`/media/gb28181-cascade/binding/${id}`, data),
|
||||
// 查询通道分页列表
|
||||
queryChannelList: (data: any): any => server.post(`media/channel/_query`, data),
|
||||
queryChannelList: (data: any): any => server.post(`/media/channel/_query`, data),
|
||||
// 推送
|
||||
publish: (id: string, params: any) => server.get(`/media/gb28181-cascade/${id}/bindings/publish`, params)
|
||||
publish: (id: string, params: any) => server.get(`/media/gb28181-cascade/${id}/bindings/publish`, params),
|
||||
|
||||
// 分屏展示接口
|
||||
// 设备树
|
||||
getMediaTree: (data?: any) => server.post<any>(`/media/device/_query/no-paging`, data),
|
||||
|
||||
}
|
|
@ -70,6 +70,10 @@ const iconKeys = [
|
|||
'CaretDownOutlined',
|
||||
'MinusOutlined',
|
||||
'AudioOutlined',
|
||||
'BellOutlined',
|
||||
'UserOutlined',
|
||||
'LogoutOutlined',
|
||||
'ReadIconOutlined'
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<div style="height: 48px; display: flex; display: flex">
|
||||
<AIcon type="BellOutlined" @click.prevent />
|
||||
<div class="notice-container">
|
||||
<a-dropdown :trigger="['click']" @visible-change="visibleChange">
|
||||
<div class="icon-content">
|
||||
<AIcon
|
||||
type="BellOutlined"
|
||||
@click.prevent
|
||||
style="font-size: 16px"
|
||||
/>
|
||||
<span class="unread" v-show="total > 0">{{ total }}</span>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<div class="content">
|
||||
<NoticeInfo />
|
||||
<div>
|
||||
<NoticeInfo :data="list" @on-action="getList" />
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
@ -16,16 +21,79 @@
|
|||
<script setup lang="ts">
|
||||
import { getList_api } from '@/api/account/notificationRecord';
|
||||
import NoticeInfo from './NoticeInfo.vue';
|
||||
import { getWebSocket } from '@/utils/websocket';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { changeStatus_api } from '@/api/account/notificationRecord';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
const updateCount = computed(()=>useUserInfo().$state.alarmUpdateCount);
|
||||
|
||||
const total = ref(0);
|
||||
const list = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const subscribeNotice = () => {
|
||||
getWebSocket('notification', '/notifications', {})
|
||||
?.pipe()
|
||||
.subscribe((resp: any) => {
|
||||
total.value += 1;
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
description:
|
||||
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
|
||||
onClick: () => {
|
||||
changeStatus_api('_read', [resp.id]);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
'terms[0].column': 'state',
|
||||
'terms[0].value': 'unread',
|
||||
'sorts[0].name': 'notifyTime',
|
||||
'sorts[0].order': 'desc',
|
||||
};
|
||||
getList_api(params).then((resp) => {});
|
||||
getList_api(params)
|
||||
.then((resp: any) => {
|
||||
list.value = resp.result.data;
|
||||
total.value = resp.result.total;
|
||||
})
|
||||
.finally(() => (loading.value = false));
|
||||
};
|
||||
subscribeNotice();
|
||||
getList();
|
||||
watch(updateCount, () => getList());
|
||||
const visibleChange = (bool: boolean) => {
|
||||
bool && getList();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.notice-container {
|
||||
.icon-content {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.unread {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -12px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background: #ff4d4f;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,12 +1,51 @@
|
|||
<template>
|
||||
<div class="notice-info-container">
|
||||
<a-tabs :activeKey="'default'">
|
||||
<a-tab-pane key="default" tab="未读消息"> 111 </a-tab-pane>
|
||||
<a-tab-pane key="default" tab="未读消息">
|
||||
<div class="no-data" v-if="props.data.length === 0">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg" alt="" />
|
||||
</div>
|
||||
|
||||
<div v-else class="content">
|
||||
<ul class="list">
|
||||
<li
|
||||
class="list-item"
|
||||
v-for="item in props.data"
|
||||
@click="read(item.id)"
|
||||
>
|
||||
<h5>{{ item.topicName }}</h5>
|
||||
<p>{{ item.message }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btns">
|
||||
<span @click="read()">当前标记为已读</span>
|
||||
<span @click="jumpPage('account/NotificationRecord')"
|
||||
>查看更多</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { changeStatus_api } from '@/api/account/notificationRecord';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
|
||||
const emits = defineEmits(['onAction']);
|
||||
const props = defineProps<{
|
||||
data: any[];
|
||||
}>();
|
||||
const { jumpPage } = useMenuStore();
|
||||
|
||||
const read = (id?: string) => {
|
||||
const ids = id ? [id] : props.data.map((item) => item.id);
|
||||
changeStatus_api('_read', ids).then((resp:any) => {
|
||||
if (resp.status === 200) emits('onAction');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.notice-info-container {
|
||||
|
@ -20,5 +59,64 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
width: 100%;
|
||||
padding: 73px 0 88px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.list {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
&::-webkit-scrollbar {
|
||||
//隐藏或取消滚动条
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
padding: 12px 24px;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
h5 {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: #f0f5ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
span {
|
||||
display: block;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="jumpPage('account/center')" style="width: 160px;">
|
||||
<AIcon type="UserOutlined" />
|
||||
<a-menu-item @click="push('/account/center')" style="width: 160px;">
|
||||
<AIcon type="UserOutlined" style="margin-right: 8px;" />
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="logOut">
|
||||
<AIcon type="LogoutOutlined" />
|
||||
<AIcon type="LogoutOutlined" style="margin-right: 8px;" />
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
|
@ -27,17 +27,17 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { loginout_api } from '@/api/login';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
const {push} = useRouter();
|
||||
|
||||
const userInfo = useUserInfo().$state.userInfos as any;
|
||||
|
||||
const { jumpPage } = useMenuStore();
|
||||
|
||||
const logOut = () => {
|
||||
loginout_api().then(() => {
|
||||
localStorage.clear();
|
||||
jumpPage('user/login');
|
||||
push('/user/login');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<svg width="58" height="50" viewBox="0 0 58 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29 49.5794C45.0163 49.5794 58 46.8079 58 43.3891C58 39.9702 45.0163 37.1987 29 37.1987C12.9837 37.1987 0 39.9702 0 43.3891C0 46.8079 12.9837 49.5794 29 49.5794Z" fill="#FAFAFA"/>
|
||||
<path d="M33.2639 9.99902L9.93594 16.3635C9.47351 16.4896 9.43643 17.1312 9.88123 17.3098L26.5406 23.999L49.1273 18.1035C49.5839 17.9843 49.6364 17.3571 49.206 17.1637L33.2639 9.99902Z" fill="#FAFAFA" stroke="#E0E0E0"/>
|
||||
<path d="M24.2975 4.69484C25.7779 5.25264 26.8448 8.08321 25.7779 9.45134L22.4067 10.9999C21.6665 10.2491 20.7123 8.2462 21.0824 6.52395C21.2746 5.62954 21.4884 5.19123 22.1983 4.7653C22.8554 4.37107 23.7889 4.5032 24.2975 4.69484Z" fill="white" stroke="#E0E0E0"/>
|
||||
<path d="M35.2162 3.78468C33.7358 4.34248 32.6689 7.17306 33.7358 8.54119L37.107 10.0897C37.8472 9.33897 38.8014 7.33604 38.4313 5.61379C38.2391 4.71939 38.0253 4.28107 37.3153 3.85514C36.6583 3.46091 35.7248 3.59304 35.2162 3.78468Z" fill="white" stroke="#E0E0E0"/>
|
||||
<mask id="path-5-inside-1_990_11491" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.5449 17.5308C42.5432 17.4907 42.5411 17.4504 42.5387 17.4099C42.3461 14.2156 40.8848 9.80379 37.5252 7.09156C35.6005 5.42908 33.0395 4.3785 29.6986 4.65434C26.3456 4.78127 23.9278 6.13333 22.2162 8.01686C19.2086 11.1131 18.2885 15.6671 18.4811 18.8607C18.4835 18.9011 18.4863 18.9414 18.4894 18.9814C18.515 19.5938 18.6444 20.1629 18.8655 20.688C20.262 24.3125 25.1708 25.5541 30.934 25.2065C30.9736 25.2042 31.0131 25.2017 31.0526 25.1991C36.7632 24.8346 41.4308 23.0119 42.3762 19.2717C42.533 18.7234 42.5931 18.1424 42.5449 17.5308Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.5449 17.5308C42.5432 17.4907 42.5411 17.4504 42.5387 17.4099C42.3461 14.2156 40.8848 9.80379 37.5252 7.09156C35.6005 5.42908 33.0395 4.3785 29.6986 4.65434C26.3456 4.78127 23.9278 6.13333 22.2162 8.01686C19.2086 11.1131 18.2885 15.6671 18.4811 18.8607C18.4835 18.9011 18.4863 18.9414 18.4894 18.9814C18.515 19.5938 18.6444 20.1629 18.8655 20.688C20.262 24.3125 25.1708 25.5541 30.934 25.2065C30.9736 25.2042 31.0131 25.2017 31.0526 25.1991C36.7632 24.8346 41.4308 23.0119 42.3762 19.2717C42.533 18.7234 42.5931 18.1424 42.5449 17.5308Z" fill="white"/>
|
||||
<path d="M42.5387 17.4099L43.5369 17.3497L43.5369 17.3497L42.5387 17.4099ZM42.5449 17.5308L41.5459 17.5738L41.5466 17.5916L41.548 17.6094L42.5449 17.5308ZM37.5252 7.09156L36.8715 7.84832L36.8841 7.8592L36.897 7.86964L37.5252 7.09156ZM29.6986 4.65434L29.7364 5.65362L29.7587 5.65278L29.7809 5.65095L29.6986 4.65434ZM22.2162 8.01686L22.9335 8.71363L22.9451 8.7017L22.9563 8.68938L22.2162 8.01686ZM18.4811 18.8607L17.4829 18.9209L18.4811 18.8607ZM18.4894 18.9814L19.4885 18.9396L19.4878 18.9217L19.4864 18.904L18.4894 18.9814ZM18.8655 20.688L19.7987 20.3284L19.7931 20.3141L19.7871 20.2999L18.8655 20.688ZM31.0526 25.1991L30.9889 24.2011L30.9878 24.2012L31.0526 25.1991ZM42.3762 19.2717L41.4148 18.9968L41.4105 19.0117L41.4067 19.0266L42.3762 19.2717ZM41.5405 17.4701C41.5426 17.5049 41.5444 17.5394 41.5459 17.5738L43.544 17.4879C43.5421 17.442 43.5397 17.3959 43.5369 17.3497L41.5405 17.4701ZM36.897 7.86964C39.9727 10.3527 41.359 14.4604 41.5405 17.4701L43.5369 17.3497C43.3331 13.9708 41.7968 9.2549 38.1533 6.31347L36.897 7.86964ZM38.1788 6.33479C36.0609 4.50532 33.2375 3.35875 29.6163 3.65773L29.7809 5.65095C32.8416 5.39825 35.1402 6.35285 36.8715 7.84832L38.1788 6.33479ZM22.9563 8.68938C24.496 6.99503 26.6647 5.76991 29.7364 5.65362L29.6608 3.65505C26.0266 3.79264 23.3597 5.27163 21.4761 7.34434L22.9563 8.68938ZM21.4989 7.3201C18.2372 10.6779 17.2792 15.5428 17.4829 18.9209L19.4793 18.8005C19.2978 15.7915 20.18 11.5483 22.9335 8.71363L21.4989 7.3201ZM17.4829 18.9209C17.4857 18.967 17.4888 19.013 17.4924 19.0588L19.4864 18.904C19.4837 18.8697 19.4814 18.8352 19.4793 18.8005L17.4829 18.9209ZM19.7871 20.2999C19.6131 19.8867 19.5093 19.435 19.4885 18.9396L17.4903 19.0232C17.5208 19.7525 17.6756 20.439 17.9439 21.0761L19.7871 20.2999ZM17.9324 21.0475C18.7723 23.2274 20.6451 24.6048 22.941 25.3838C25.2296 26.1603 28.0393 26.3829 30.9942 26.2047L30.8738 24.2084C28.0655 24.3777 25.5393 24.1534 23.5835 23.4898C21.6351 22.8288 20.3553 21.7731 19.7987 20.3284L17.9324 21.0475ZM30.9942 26.2047C31.0353 26.2023 31.0763 26.1997 31.1174 26.197L30.9878 24.2012C30.9499 24.2037 30.9119 24.2061 30.8738 24.2084L30.9942 26.2047ZM41.4067 19.0266C41.0298 20.5176 39.8986 21.7128 38.0652 22.6014C36.2245 23.4935 33.7708 24.0236 30.9889 24.2011L31.1163 26.1971C34.045 26.0101 36.7804 25.4466 38.9375 24.4012C41.1019 23.3522 42.7772 21.766 43.3457 19.5167L41.4067 19.0266ZM43.3377 19.5465C43.5279 18.8813 43.5992 18.1806 43.5419 17.4523L41.548 17.6094C41.587 18.1042 41.5381 18.5654 41.4148 18.9968L43.3377 19.5465Z" fill="#E0E0E0" mask="url(#path-5-inside-1_990_11491)"/>
|
||||
<ellipse cx="21.5982" cy="19.5977" rx="2.27602" ry="1.30058" transform="rotate(-3.45082 21.5982 19.5977)" fill="#F4F4F4"/>
|
||||
<ellipse cx="2.27602" cy="1.30058" rx="2.27602" ry="1.30058" transform="matrix(-0.974459 0.224564 0.224564 0.974459 43.8887 15.7603)" fill="#F4F4F4"/>
|
||||
<circle cx="26.0809" cy="15.0666" r="1.84249" transform="rotate(-3.45082 26.0809 15.0666)" fill="#E0E0E0"/>
|
||||
<circle cx="26.6121" cy="14.6075" r="0.650291" transform="rotate(-3.45082 26.6121 14.6075)" fill="white"/>
|
||||
<circle cx="25.4848" cy="15.971" r="0.325146" transform="rotate(-3.45082 25.4848 15.971)" fill="white"/>
|
||||
<circle cx="37.7645" cy="14.362" r="1.84249" transform="rotate(-3.45082 37.7645 14.362)" fill="#E0E0E0"/>
|
||||
<circle cx="38.2937" cy="13.9029" r="0.650291" transform="rotate(-3.45082 38.2937 13.9029)" fill="white"/>
|
||||
<circle cx="37.1683" cy="15.2669" r="0.325146" transform="rotate(-3.45082 37.1683 15.2669)" fill="white"/>
|
||||
<ellipse cx="32.3087" cy="16.6698" rx="1.51282" ry="1.23036" transform="rotate(-3.45082 32.3087 16.6698)" fill="#E0E0E0"/>
|
||||
<mask id="mask0_990_11491" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="28" y="19" width="8" height="4">
|
||||
<path d="M28.8401 21.0038C29.1929 20.5749 29.549 20.2953 30.0249 20.0923C30.5162 19.8826 31.1577 19.7447 32.1 19.6404C33.021 19.5386 33.5501 19.5974 33.9102 19.7125C34.2573 19.8235 34.4861 19.9957 34.8185 20.246C34.8296 20.2544 34.8408 20.2628 34.8521 20.2713C35.02 20.3977 35.1717 20.6012 35.2384 20.787C35.2708 20.8773 35.274 20.938 35.2703 20.9682C35.2681 20.9866 35.2652 20.9882 35.2637 20.989C35.2635 20.9891 35.2633 20.9892 35.2632 20.9893C34.8844 21.3249 34.6341 21.5424 34.2159 21.7233C33.7851 21.9096 33.148 22.067 32.0018 22.1938C30.8548 22.3207 30.2398 22.3016 29.8387 22.2257C29.513 22.164 29.319 22.0665 29.0343 21.9233C28.9772 21.8947 28.9166 21.8642 28.8504 21.8317C28.7416 21.7784 28.657 21.6792 28.6311 21.5668C28.609 21.4712 28.6107 21.2826 28.8401 21.0038Z" fill="#FD7477" stroke="#666666"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_990_11491)">
|
||||
<path d="M31.2438 19.4706L31.0537 18.6566C31.0373 18.5863 31.1139 18.545 31.1837 18.5631C31.3003 18.5935 31.4791 18.5924 31.6924 18.4731C32.0018 18.3001 32.1775 17.9936 32.2267 17.862L32.3581 20.041L32.0351 20.0605C31.6628 20.083 31.3286 19.8338 31.2438 19.4706Z" fill="white" stroke="#E0E0E0" stroke-width="0.5"/>
|
||||
<path d="M33.4522 19.3375L33.543 18.5065C33.5509 18.4348 33.4699 18.403 33.4028 18.4294C33.2906 18.4735 33.113 18.4939 32.887 18.4012C32.559 18.2666 32.3477 17.9835 32.2831 17.8587L32.4145 20.0378L32.7374 20.0183C33.1097 19.9958 33.4116 19.7083 33.4522 19.3375Z" fill="white" stroke="#E0E0E0" stroke-width="0.5"/>
|
||||
</g>
|
||||
<path d="M31.421 18.7266C30.2561 19.2905 29.7431 20.4957 29.7177 20.5566C29.7081 20.5796 29.7032 20.6044 29.7031 20.6293C29.7031 20.6543 29.708 20.679 29.7175 20.7021C29.727 20.7252 29.741 20.7462 29.7587 20.7639C29.7763 20.7815 29.7972 20.7956 29.8203 20.8052C29.8434 20.8147 29.8681 20.8197 29.893 20.8197C29.918 20.8198 29.9428 20.8149 29.9658 20.8053C29.9889 20.7958 30.0099 20.7818 30.0276 20.7642C30.0453 20.7466 30.0593 20.7256 30.0689 20.7026C30.0756 20.6866 30.6789 19.416 32.19 19.1705C33.6941 18.926 34.9386 20.104 34.9503 20.119C34.9812 20.1584 35.0264 20.184 35.076 20.1903C35.1257 20.1966 35.1758 20.183 35.2155 20.1526C35.2553 20.1222 35.2814 20.0773 35.2882 20.0277C35.2951 19.9781 35.2821 19.9278 35.2522 19.8878C35.0034 19.5814 34.7147 19.3099 34.3937 19.0805C33.6826 18.5696 32.926 18.3613 32.2053 18.4785C31.9332 18.5227 31.669 18.6063 31.421 18.7266Z" fill="#E0E0E0" stroke="#E0E0E0" stroke-width="0.1"/>
|
||||
<line x1="23.7287" y1="18.1659" x2="23.5991" y2="18.5272" stroke="#E0E0E0" stroke-width="0.7" stroke-linecap="round"/>
|
||||
<line x1="24.9494" y1="18.31" x2="24.893" y2="18.4672" stroke="#E0E0E0" stroke-width="0.7" stroke-linecap="round"/>
|
||||
<line x1="0.35" y1="-0.35" x2="0.733819" y2="-0.35" transform="matrix(0.44853 0.893768 0.893768 -0.44853 40.4297 15.9731)" stroke="#E0E0E0" stroke-width="0.7" stroke-linecap="round"/>
|
||||
<line x1="0.35" y1="-0.35" x2="0.517055" y2="-0.35" transform="matrix(0.448552 0.893757 0.893757 -0.448552 39.2363 16.2622)" stroke="#E0E0E0" stroke-width="0.7" stroke-linecap="round"/>
|
||||
<path d="M9 17.5027C9 17.1455 9.36388 16.9035 9.69336 17.0416L25 23.4605V46.2127L9 38.7176V17.5027Z" fill="#FBFBFB" stroke="#E0E0E0"/>
|
||||
<path d="M8.74631 16.66L24.8561 23.8265L22.245 30.3166L6.15202 22.5625L8.74631 16.66Z" fill="white" stroke="#E0E0E0"/>
|
||||
<path d="M50 18.2859C50 17.9597 49.6926 17.7208 49.3765 17.8014L25.5 23.8878V45.8708C25.5 46.2033 25.8185 46.4432 26.1381 46.3513L50 39.4934V18.2859Z" fill="#F4F4F4" stroke="#E0E0E0"/>
|
||||
<path d="M50.0635 18.0495C49.9823 17.7978 49.719 17.6529 49.4628 17.719L26.1262 23.7393L28.0893 30.3834L51.865 23.6308L50.0635 18.0495Z" fill="white" stroke="#E0E0E0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,466 @@
|
|||
<!-- 分屏组件 -->
|
||||
<template>
|
||||
<div class="live-player-warp">
|
||||
<div class="live-player-content">
|
||||
<!-- 工具栏 -->
|
||||
<div class="player-screen-tool" v-if="showScreen">
|
||||
<a-radio-group
|
||||
v-model:value="screen"
|
||||
button-style="solid"
|
||||
@change="handleScreenChange"
|
||||
>
|
||||
<a-radio-button :value="1">单屏</a-radio-button>
|
||||
<a-radio-button :value="4">四分屏</a-radio-button>
|
||||
<a-radio-button :value="9">九分屏</a-radio-button>
|
||||
<a-radio-button :value="0">全屏</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div class="screen-tool-save">
|
||||
<a-tooltip title="可保存分屏配置记录">
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
<a-popover
|
||||
v-model:visible="visible"
|
||||
trigger="click"
|
||||
title="分屏名称"
|
||||
>
|
||||
<template #content>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入名称',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最多可输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-textarea v-model:value="formData.name" />
|
||||
</a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="saveHistory"
|
||||
:loading="loading"
|
||||
style="width: 100%; margin-top: 16px"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
</a-form>
|
||||
</template>
|
||||
<a-dropdown-button
|
||||
type="primary"
|
||||
@click="visible = true"
|
||||
>
|
||||
保存
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-empty
|
||||
v-if="!historyList.length"
|
||||
description="暂无数据"
|
||||
/>
|
||||
<a-menu-item
|
||||
v-for="(item, index) in historyList"
|
||||
:key="`his${index}`"
|
||||
@click="handleHistory(item)"
|
||||
>
|
||||
<a-space>
|
||||
<span>{{ item.name }}</span>
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="(e: any) => {
|
||||
e?.stopPropagation();
|
||||
deleteHistory(item.key);
|
||||
}
|
||||
"
|
||||
>
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
@click="
|
||||
(e:any) =>
|
||||
e?.stopPropagation()
|
||||
"
|
||||
/>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 播放器 -->
|
||||
<div class="player-body">
|
||||
<div
|
||||
ref="fullscreenRef"
|
||||
class="player-screen"
|
||||
:class="`screen-${screen}`"
|
||||
>
|
||||
<template v-for="(item, index) in players" :key="item.key">
|
||||
<div
|
||||
class="player-screen-item"
|
||||
:class="{
|
||||
active:
|
||||
showScreen &&
|
||||
playerActive === index &&
|
||||
!isFullscreen,
|
||||
'full-screen': isFullscreen,
|
||||
}"
|
||||
:style="{ display: item.show ? 'block' : 'none' }"
|
||||
@click="playerActive = index"
|
||||
>
|
||||
<div
|
||||
class="media-btn-refresh"
|
||||
:style="{
|
||||
display: item.url ? 'block' : 'none',
|
||||
}"
|
||||
@click="handleRefresh($event, item, index)"
|
||||
>
|
||||
刷新
|
||||
</div>
|
||||
<LivePlayer
|
||||
:src="item.url"
|
||||
:width="screenWidth"
|
||||
:height="screenHeight"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 控制器 -->
|
||||
</div>
|
||||
<MediaTool @onMouseDown="handleMouseDown" @onMouseUp="handleMouseUp" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import {
|
||||
deleteSearchHistory,
|
||||
getSearchHistory,
|
||||
saveSearchHistory,
|
||||
} from '@/api/comm';
|
||||
import { message } from 'ant-design-vue';
|
||||
import LivePlayer from '@/components/Player/index.vue';
|
||||
import MediaTool from '@/components/Player/mediaTool.vue';
|
||||
|
||||
type Player = {
|
||||
id?: string;
|
||||
url?: string;
|
||||
channelId?: string;
|
||||
key: string;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
interface ScreenProps {
|
||||
url?: string;
|
||||
id?: string;
|
||||
channelId: string;
|
||||
className?: string;
|
||||
historyHandle?: (deviceId: string, channelId: string) => string;
|
||||
/**
|
||||
*
|
||||
* @param id 当前选中播发视频ID
|
||||
* @param type 当前操作动作
|
||||
*/
|
||||
onMouseDown?: (deviceId: string, channelId: string, type: string) => void;
|
||||
/**
|
||||
*
|
||||
* @param id 当前选中播发视频ID
|
||||
* @param type 当前操作动作
|
||||
*/
|
||||
onMouseUp?: (deviceId: string, channelId: string, type: string) => void;
|
||||
showScreen?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<ScreenProps>();
|
||||
|
||||
const DEFAULT_SAVE_CODE = 'screen-save';
|
||||
|
||||
// 分屏数量 1/4/9/0
|
||||
const screen = ref(1);
|
||||
// 视频窗口
|
||||
const players = ref<Player[]>([]);
|
||||
// 当前选中的窗口
|
||||
const playerActive = ref(0);
|
||||
// 单个播放窗口宽高
|
||||
const screenWidth = ref('');
|
||||
const screenHeight = ref('');
|
||||
// 历史记录
|
||||
const historyList = ref<any[]>([]);
|
||||
// 展示保存浮窗
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
// 保存表单
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
name: '',
|
||||
});
|
||||
|
||||
// 全屏元素
|
||||
const fullscreenRef = ref(null);
|
||||
const { isFullscreen, enter, exit, toggle } = useFullscreen(
|
||||
fullscreenRef.value,
|
||||
);
|
||||
|
||||
/**
|
||||
* 刷新视频
|
||||
* @param id
|
||||
* @param channelId
|
||||
* @param url
|
||||
* @param index
|
||||
*/
|
||||
const reloadPlayer = (
|
||||
id: string,
|
||||
channelId: string,
|
||||
url: string,
|
||||
index: number,
|
||||
) => {
|
||||
const olPlayers = [...players.value];
|
||||
olPlayers[index] = {
|
||||
id: '',
|
||||
channelId: '',
|
||||
url: '',
|
||||
key: olPlayers[index].key,
|
||||
show: true,
|
||||
};
|
||||
const newPlayer = {
|
||||
id,
|
||||
url,
|
||||
channelId,
|
||||
key: olPlayers[index].key,
|
||||
show: true,
|
||||
};
|
||||
players.value = [...olPlayers];
|
||||
setTimeout(() => {
|
||||
olPlayers[index] = newPlayer;
|
||||
players.value = [...olPlayers];
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 视频链接变化, 更新播放内容
|
||||
* @param id
|
||||
* @param channelId
|
||||
* @param url
|
||||
*/
|
||||
const replaceVideo = (id: string, channelId: string, url: string) => {
|
||||
const olPlayers = [...players.value];
|
||||
const newPlayer = {
|
||||
id,
|
||||
url,
|
||||
channelId,
|
||||
key: olPlayers[playerActive.value].key,
|
||||
show: true,
|
||||
};
|
||||
|
||||
if (olPlayers[playerActive.value].url === url) {
|
||||
// 刷新视频
|
||||
reloadPlayer(id, channelId, url, playerActive.value);
|
||||
} else {
|
||||
olPlayers[playerActive.value] = newPlayer;
|
||||
players.value = olPlayers;
|
||||
}
|
||||
if (playerActive.value === screen.value - 1) {
|
||||
// 当前位置为分屏最后一位
|
||||
playerActive.value = 0;
|
||||
} else {
|
||||
playerActive.value += 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击分屏历史记录
|
||||
* @param item
|
||||
*/
|
||||
const handleHistory = (item: any) => {
|
||||
if (props.historyHandle) {
|
||||
const log = JSON.parse(item.content || '{}');
|
||||
screen.value = log.screen;
|
||||
const oldPlayers = [...players.value];
|
||||
|
||||
players.value = oldPlayers.map((oldPlayer, index) => {
|
||||
oldPlayer.show = false;
|
||||
if (index < log.screen) {
|
||||
const { deviceId, channelId } = log.players[index];
|
||||
return {
|
||||
...oldPlayer,
|
||||
id: deviceId,
|
||||
channelId: deviceId,
|
||||
url: deviceId
|
||||
? props.historyHandle!(deviceId, channelId)
|
||||
: '',
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
return oldPlayer;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取历史分屏
|
||||
*/
|
||||
const getHistory = async () => {
|
||||
const res = await getSearchHistory(DEFAULT_SAVE_CODE);
|
||||
if (res.success) {
|
||||
historyList.value = res.result;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除历史分屏
|
||||
* @param id
|
||||
*/
|
||||
const deleteHistory = async (id: string) => {
|
||||
const res = await deleteSearchHistory(DEFAULT_SAVE_CODE, id);
|
||||
if (res.success) {
|
||||
getHistory();
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存分屏
|
||||
*/
|
||||
const saveHistory = async () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
const param = {
|
||||
name: formData.value.name,
|
||||
content: JSON.stringify({
|
||||
screen: screen.value,
|
||||
players: players.value.map((item: any) => ({
|
||||
deviceId: item.id,
|
||||
channelId: item.channelId,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
loading.value = true;
|
||||
const res = await saveSearchHistory(param, DEFAULT_SAVE_CODE);
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
visible.value = false;
|
||||
getHistory();
|
||||
message.success('保存成功');
|
||||
formRef.value.resetFields();
|
||||
} else {
|
||||
message.error('保存失败');
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
const mediaInit = () => {
|
||||
const newArr = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
newArr.push({
|
||||
id: '',
|
||||
channelId: '',
|
||||
url: '',
|
||||
key: 'time_' + new Date().getTime() + i,
|
||||
show: i === 0,
|
||||
});
|
||||
}
|
||||
players.value = newArr;
|
||||
};
|
||||
|
||||
/**
|
||||
* 改变分屏数量
|
||||
* @param e
|
||||
*/
|
||||
const handleScreenChange = (e: any) => {
|
||||
if (e.target.value) {
|
||||
screenChange(e.target.value);
|
||||
} else {
|
||||
// 全屏操作
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
const screenChange = (index: number) => {
|
||||
players.value = players.value.map((m: any, i: number) => ({
|
||||
id: '',
|
||||
channelId: '',
|
||||
url: '',
|
||||
updateTime: 0,
|
||||
key: m.key,
|
||||
show: i < index,
|
||||
}));
|
||||
playerActive.value = 0;
|
||||
screen.value = index;
|
||||
|
||||
// if (screen.value === 4) {
|
||||
// screenWidth.value = '350px';
|
||||
// screenHeight.value = '2000px';
|
||||
// }
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新
|
||||
* @param e
|
||||
* @param item
|
||||
* @param index
|
||||
*/
|
||||
const handleRefresh = (e: any, item: any, index: number) => {
|
||||
e.stopPropagation();
|
||||
if (item.url) {
|
||||
reloadPlayer(item.id!, item.channelId!, item.url!, index);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击控制按钮
|
||||
* @param type 控制类型
|
||||
*/
|
||||
const handleMouseDown = (type: string) => {
|
||||
const { id, channelId } = players.value[playerActive.value];
|
||||
if (id && channelId && props.onMouseDown) {
|
||||
props.onMouseDown(id, channelId, type);
|
||||
}
|
||||
};
|
||||
const handleMouseUp = (type: string) => {
|
||||
const { id, channelId } = players.value[playerActive.value];
|
||||
if (id && channelId && props.onMouseUp) {
|
||||
props.onMouseUp(id, channelId, type);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.url,
|
||||
(val) => {
|
||||
if (val && props.id) {
|
||||
replaceVideo(props.id, props.channelId, val);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.showScreen !== false) {
|
||||
getHistory();
|
||||
}
|
||||
mediaInit();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
replaceVideo,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
</style>
|
|
@ -0,0 +1,82 @@
|
|||
.live-player-warp {
|
||||
display: flex;
|
||||
|
||||
.live-player-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.player-screen-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
height: auto;
|
||||
padding: 4px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.player-body {
|
||||
flex: 1;
|
||||
|
||||
.player-screen {
|
||||
position: relative;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.screen-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&.screen-4 {
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
&.screen-9 {
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&.screen-4,
|
||||
&.screen-9 {
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid red;
|
||||
}
|
||||
|
||||
.full-screen {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.player-screen-item {
|
||||
position: relative;
|
||||
|
||||
.media-btn-refresh {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 2;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ import vue3videoPlay from 'vue3-video-play';
|
|||
const props = defineProps({
|
||||
src: { type: String, default: '' },
|
||||
type: { type: String, default: 'mp4' },
|
||||
width: { type: String, default: '500px' },
|
||||
height: { type: String, default: '280px' },
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -21,8 +23,6 @@ watch(
|
|||
|
||||
const options = reactive({
|
||||
...props,
|
||||
width: '500px', //播放器高度
|
||||
height: '280px', //播放器高度
|
||||
color: '#409eff', //主题色
|
||||
title: '', //视频名称
|
||||
// src: props.src,
|
||||
|
|
|
@ -1,5 +1,49 @@
|
|||
export const LoginPath = '/login'
|
||||
|
||||
export const AccountMenu = {
|
||||
path: '/account',
|
||||
component: () => import('@/components/Layout/BasicLayoutPage.vue'),
|
||||
redirect: '/account/center',
|
||||
name: 'account',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
icon: '',
|
||||
hideInMenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/account/center',
|
||||
name: 'account/center',
|
||||
meta: {
|
||||
title: '基本设置',
|
||||
icon: '',
|
||||
hideInMenu: false
|
||||
},
|
||||
component: () => import('@/views/account/Center/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account/NotificationSubscription',
|
||||
name: 'account/NotificationSubscription',
|
||||
meta: {
|
||||
title: '通知订阅',
|
||||
icon: '',
|
||||
hideInMenu: false
|
||||
},
|
||||
component: () => import('@/views/account/NotificationSubscription/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account/NotificationRecord',
|
||||
name: 'account/NotificationRecord',
|
||||
meta: {
|
||||
title: '通知记录',
|
||||
icon: '',
|
||||
hideInMenu: false
|
||||
},
|
||||
component: () => import('@/views/account/NotificationRecord/index.vue')
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default [
|
||||
{ path: '/*', redirect: '/'},
|
||||
// start: 测试用, 可删除
|
||||
|
@ -27,18 +71,6 @@ export default [
|
|||
path: '/system/Api',
|
||||
component: () => import('@/views/system/Platforms/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account/center',
|
||||
component: () => import('@/views/account/Center/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account/NotificationSubscription',
|
||||
component: () => import('@/views/account/NotificationSubscription/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account/NotificationRecord',
|
||||
component: () => import('@/views/account/NotificationRecord/index.vue')
|
||||
},
|
||||
// end: 测试用, 可删除
|
||||
|
||||
// 初始化
|
||||
|
|
|
@ -4,8 +4,8 @@ import { filterAsnycRouter, MenuItem } from '@/utils/menu'
|
|||
import { isArray } from 'lodash-es'
|
||||
import { usePermissionStore } from './permission'
|
||||
import router from '@/router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { onlyMessage } from '@/utils/comm'
|
||||
import { AccountMenu } from '@/router/menu'
|
||||
|
||||
const defaultOwnParams = [
|
||||
{
|
||||
|
@ -115,8 +115,11 @@ export const useMenuStore = defineStore({
|
|||
hideInMenu: true
|
||||
}
|
||||
})
|
||||
menusData.push(AccountMenu)
|
||||
silderMenus.push(AccountMenu)
|
||||
this.siderMenus = silderMenus
|
||||
console.log('menusData', menusData)
|
||||
console.log('silderMenus', silderMenus)
|
||||
res(menusData)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -20,7 +20,9 @@ export const useUserInfo = defineStore('userInfo', {
|
|||
token: '',
|
||||
user: {},
|
||||
},
|
||||
alarmUpdateCount: 0
|
||||
}),
|
||||
|
||||
actions: {
|
||||
login(userInfo: any) {
|
||||
const username = userInfo.userName.trim();
|
||||
|
@ -49,6 +51,9 @@ export const useUserInfo = defineStore('userInfo', {
|
|||
}
|
||||
}).catch(() => rej())
|
||||
})
|
||||
},
|
||||
updateAlarm(){
|
||||
this.alarmUpdateCount += 1
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,12 +24,17 @@ export const request = axios.create({
|
|||
* @param {String} url
|
||||
* @param {Object} [data]
|
||||
* @param {String} responseType 如果接口是需要导出文件流,那么responseType = 'blob'
|
||||
* @param {Object|String} [ext] 扩展参数,如果是配置headers,ext对象内包含headers对象,如下
|
||||
* {
|
||||
headers: {'Content-Type': 'text/plain;charset=UTF-8'},
|
||||
}
|
||||
* @returns {AxiosInstance}
|
||||
*/
|
||||
export const post = function<T>(url: string, data = {}, params = {}) {
|
||||
params = typeof params === 'string' ? { responseType: params } : params
|
||||
export const post = function<T>(url: string, data = {}, params = {}, ext={}) {
|
||||
ext = typeof ext === 'string' ? { responseType: ext } : ext
|
||||
return request<any, AxiosResponseRewrite<T>>({
|
||||
...params,
|
||||
...ext,
|
||||
params,
|
||||
method: 'POST',
|
||||
url,
|
||||
data
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
<template lang="">
|
||||
<a-modal
|
||||
:title="data.id ? '编辑' : '新增'"
|
||||
:visible="true"
|
||||
width="700px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
:rules="FormValidate"
|
||||
ref="formRef"
|
||||
>
|
||||
<a-form-item label="通道名称" name="name">
|
||||
<a-input
|
||||
placeholder="请输入通道名称"
|
||||
v-model:value="formData.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="通讯协议" name="provider">
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.provider"
|
||||
:options="providersList"
|
||||
placeholder="请选择通讯协议"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
:disabled="!!id"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'MODBUS_TCP'"
|
||||
:name="['configuration', 'host']"
|
||||
:rules="FormValidate.host"
|
||||
>
|
||||
<div class="form-label">
|
||||
Modbus主机IP
|
||||
<span class="form-label-required">*</span>
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<p>支持ipv4、ipv6、域名</p>
|
||||
</template>
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-input
|
||||
placeholder="请输入Modbus主机IP"
|
||||
v-model:value="formData.configuration.host"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'MODBUS_TCP'"
|
||||
label="端口"
|
||||
:name="['configuration', 'port']"
|
||||
:rules="FormValidate.port"
|
||||
>
|
||||
<a-input-number
|
||||
style="width: 100%"
|
||||
placeholder="请输入端口"
|
||||
v-model:value="formData.configuration.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'OPC_UA'"
|
||||
label="端点url"
|
||||
:name="['configuration', 'endpoint']"
|
||||
:rules="FormValidate.endpoint"
|
||||
>
|
||||
<a-input
|
||||
placeholder="请输入端点url"
|
||||
v-model:value="formData.configuration.endpoint"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'OPC_UA'"
|
||||
label="安全策略"
|
||||
:name="['configuration.securityPolicy']"
|
||||
:rules="FormValidate.securityPolicy"
|
||||
>
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.configuration.securityPolicy"
|
||||
:options="Options['security-policies']"
|
||||
placeholder="请选择安全策略"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'OPC_UA'"
|
||||
label="安全模式"
|
||||
:name="['configuration.securityMode']"
|
||||
:rules="FormValidate.securityMode"
|
||||
>
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.configuration.securityMode"
|
||||
:options="Options['security-modes']"
|
||||
placeholder="请选择安全模式"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="
|
||||
formData.configuration.securityMode === 'SignAndEncrypt' ||
|
||||
formData.configuration.securityMode === 'Sign'
|
||||
"
|
||||
label="证书"
|
||||
:name="['configuration.certificate']"
|
||||
:rules="FormValidate.certificate"
|
||||
>
|
||||
<a-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.configuration.certificate"
|
||||
:options="certificateList"
|
||||
placeholder="请选择证书"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.provider === 'OPC_UA'"
|
||||
label="权限认证"
|
||||
:name="['configuration.authType']"
|
||||
:rules="FormValidate.authType"
|
||||
>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:checkStyle="true"
|
||||
:options="Options['auth-types']"
|
||||
v-model="formData.configuration.authType"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.configuration.authType === 'username'"
|
||||
label="用户名"
|
||||
:name="['configuration.username']"
|
||||
:rules="FormValidate.username"
|
||||
>
|
||||
<a-input
|
||||
placeholder="请输入用户名"
|
||||
v-model:value="formData.configuration.username"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="formData.configuration.authType === 'username'"
|
||||
label="密码"
|
||||
:name="['configuration.password']"
|
||||
:rules="FormValidate.password"
|
||||
>
|
||||
<a-input-password
|
||||
placeholder="请输入密码"
|
||||
v-model:value="formData.configuration.password"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="description">
|
||||
<a-textarea
|
||||
placeholder="请输入说明"
|
||||
v-model:value="formData.description"
|
||||
:maxlength="200"
|
||||
:rows="3"
|
||||
showCount
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button key="back" @click="handleCancel">取消</a-button>
|
||||
<PermissionButton
|
||||
key="submit"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="handleOk"
|
||||
style="margin-left: 8px"
|
||||
:hasPermission="`DataCollect/Channel:${id ? 'update' : 'add'}`"
|
||||
>
|
||||
确认
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
save,
|
||||
update,
|
||||
queryOptionsList,
|
||||
queryCertificateList,
|
||||
getProviders,
|
||||
} from '@/api/data-collect/channel';
|
||||
import { FormValidate, FormState } from '../data';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import type { FormDataType } from '../type.d';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const loading = ref(false);
|
||||
const id = props.data.id;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const certificateList = ref([]);
|
||||
const providersList = ref([]);
|
||||
const Options = ref({
|
||||
'auth-types': [],
|
||||
'security-modes': [],
|
||||
'security-policies': [],
|
||||
});
|
||||
|
||||
const formData = ref<FormDataType>(FormState);
|
||||
|
||||
const handleOk = async () => {
|
||||
const params = await formRef.value?.validate();
|
||||
loading.value = true;
|
||||
const response = !id
|
||||
? await save(params)
|
||||
: await update(id, { ...props.data, ...params });
|
||||
if (response.status === 200) {
|
||||
emit('change', true);
|
||||
}
|
||||
loading.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const getOptionsList = async () => {
|
||||
for (let key in Options.value) {
|
||||
const res = await queryOptionsList(key);
|
||||
Options.value[key] = res.result.map((item) => ({
|
||||
label: item?.text || item,
|
||||
value: item?.value || item,
|
||||
}));
|
||||
}
|
||||
};
|
||||
const getCertificateList = async () => {
|
||||
const res = await queryCertificateList();
|
||||
certificateList.value = res.result.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
};
|
||||
|
||||
const getProvidersList = async () => {
|
||||
const resp = await getProviders();
|
||||
if (resp.status === 200) {
|
||||
const list = [
|
||||
{ label: 'OPC UA', value: 'OPC_UA' },
|
||||
{ label: 'Modbus TCP', value: 'MODBUS_TCP' },
|
||||
];
|
||||
const arr = resp.result
|
||||
.filter(
|
||||
(item: any) => item.id === 'modbus-tcp' || item.id === 'opc-ua',
|
||||
)
|
||||
.map((it: any) => (it?.id === 'opc-ua' ? 'OPC_UA' : 'MODBUS_TCP'));
|
||||
const providers = list.filter((item: any) => arr.includes(item.value));
|
||||
providersList.value = providers;
|
||||
if (arr.includes('OPC_UA')) {
|
||||
getOptionsList();
|
||||
}
|
||||
}
|
||||
};
|
||||
getProvidersList();
|
||||
getCertificateList();
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (value.id) formData.value = value;
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form {
|
||||
.form-radio-button {
|
||||
width: 148px;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.form-label {
|
||||
height: 30px;
|
||||
padding-bottom: 8px;
|
||||
.form-label-required {
|
||||
color: red;
|
||||
margin: 0 4px 0 -2px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,141 @@
|
|||
import { validateField } from '@/api/data-collect/channel';
|
||||
import { FormDataType } from './type.d';
|
||||
|
||||
export const FormState: FormDataType = {
|
||||
name: '',
|
||||
provider: undefined,
|
||||
configuration: {
|
||||
host: '',
|
||||
port: '502',
|
||||
endpoint: '',
|
||||
securityPolicy: undefined,
|
||||
securityMode: undefined,
|
||||
certificate: undefined,
|
||||
authType: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
description: '',
|
||||
};
|
||||
|
||||
export const StatusColorEnum = {
|
||||
running: 'success',
|
||||
disabled: 'error',
|
||||
partialError: 'processing',
|
||||
failed: 'warning',
|
||||
stopped: 'default',
|
||||
};
|
||||
export const updateStatus = {
|
||||
disabled: {
|
||||
state: 'enabled',
|
||||
runningState: 'running',
|
||||
},
|
||||
enabled: {
|
||||
state: 'disabled',
|
||||
runningState: 'stopped',
|
||||
},
|
||||
};
|
||||
|
||||
export const TiTlePermissionButtonStyle = {
|
||||
padding: 0,
|
||||
color: ' #1890ff !important',
|
||||
'font-weight': 700,
|
||||
'font-size': '16px',
|
||||
overflow: 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'white-space': 'nowrap',
|
||||
width: 'calc(100%-100px)',
|
||||
// width: '60%',
|
||||
};
|
||||
|
||||
export const regOnlyNumber = new RegExp(/^\d+$/);
|
||||
|
||||
export const regIP = new RegExp(
|
||||
/^([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||
);
|
||||
export const regIPv6 = new RegExp(
|
||||
/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
|
||||
);
|
||||
export const regDomain = new RegExp(
|
||||
/([0-9a-z-]{2,}\.[0-9a-z-]{2,3}\.[0-9a-z-]{2,3}|[0-9a-z-]{2,}\.[0-9a-z-]{2,3})$/i,
|
||||
);
|
||||
export const checkEndpoint = (_rule: Rule, value: string): Promise<any> =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
if (value) {
|
||||
const res = await validateField(value);
|
||||
return res.result.passed ? resolve('') : reject(res.result.reason);
|
||||
}
|
||||
});
|
||||
export const FormValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
provider: [{ required: true, message: '请选择通讯协议' }],
|
||||
host: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Modbus主机IP',
|
||||
},
|
||||
{
|
||||
pattern: regIP || regIPv6 || regDomain,
|
||||
message: '请输入正确格式的Modbus主机IP地址',
|
||||
},
|
||||
],
|
||||
port: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入端口',
|
||||
},
|
||||
{
|
||||
pattern: regOnlyNumber,
|
||||
message: '请输入1-65535之间的正整数',
|
||||
},
|
||||
],
|
||||
|
||||
endpoint: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入端点url',
|
||||
},
|
||||
{
|
||||
validator: checkEndpoint,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
|
||||
securityPolicy: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择安全策略',
|
||||
},
|
||||
],
|
||||
securityMode: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择安全模式',
|
||||
},
|
||||
],
|
||||
certificate: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择证书',
|
||||
},
|
||||
],
|
||||
authType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择权限认证',
|
||||
},
|
||||
],
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
};
|
|
@ -0,0 +1,341 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div>
|
||||
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||
|
||||
<j-pro-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
model="CARD"
|
||||
:gridColumn="3"
|
||||
:request="query"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
@click="handlAdd"
|
||||
hasPermission="DataCollect/Channel:add"
|
||||
>
|
||||
<template #icon><AIcon type="PlusOutlined" /></template>
|
||||
新增通道
|
||||
</PermissionButton>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:showStatus="true"
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:status="getState(slotProps).value"
|
||||
:statusText="getState(slotProps).text"
|
||||
:statusNames="StatusColorEnum"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/network.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="card-item-content">
|
||||
<PermissionButton
|
||||
type="link"
|
||||
@click="handlEye(slotProps.id)"
|
||||
hasPermission="DataCollect/Collector:view"
|
||||
:style="TiTlePermissionButtonStyle"
|
||||
>
|
||||
{{ slotProps.name }}
|
||||
</PermissionButton>
|
||||
|
||||
<a-row class="card-item-content-box">
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
协议
|
||||
</div>
|
||||
<div class="card-item-content-text">
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.provider
|
||||
}}</template>
|
||||
{{ slotProps.provider }}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
地址
|
||||
</div>
|
||||
<div class="card-item-content-text">
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.configuration
|
||||
.host ||
|
||||
slotProps.configuration
|
||||
.endpoint
|
||||
}}</template>
|
||||
<span class="details-text">{{
|
||||
slotProps.configuration
|
||||
.host ||
|
||||
slotProps.configuration
|
||||
.endpoint
|
||||
}}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<PermissionButton
|
||||
:disabled="item.disabled"
|
||||
:popConfirm="item.popConfirm"
|
||||
:tooltip="{
|
||||
...item.tooltip,
|
||||
}"
|
||||
@click="item.onClick"
|
||||
:hasPermission="
|
||||
'DataCollect/Channel:' + item.key
|
||||
"
|
||||
>
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item?.text }}</span>
|
||||
</template>
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</j-pro-table>
|
||||
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="TypePage">
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { query, remove, update } from '@/api/data-collect/channel';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
TiTlePermissionButtonStyle,
|
||||
StatusColorEnum,
|
||||
updateStatus,
|
||||
} from './data';
|
||||
import { useMenuStore } from 'store/menu';
|
||||
import Save from './Save/index.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const params = ref<Record<string, any>>({});
|
||||
const options = ref([]);
|
||||
const visible = ref(false);
|
||||
const current = ref({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '通道名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '通讯协议',
|
||||
dataIndex: 'provider',
|
||||
key: 'provider',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'OPC_UA', value: 'OPC_UA' },
|
||||
{ label: 'MODBUS_TCP', value: 'MODBUS_TCP' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
ellipsis: true,
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: 'enabled' },
|
||||
{ label: '禁用', value: 'disabled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '运行状态',
|
||||
dataIndex: 'runningState',
|
||||
key: 'runningState',
|
||||
ellipsis: true,
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '运行中', value: 'running' },
|
||||
{ label: '部分错误', value: 'partialError' },
|
||||
{ label: '错误', value: 'failed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
const state = data.state.value;
|
||||
const stateText = state === 'enabled' ? '禁用' : '启用';
|
||||
const actions = [
|
||||
{
|
||||
key: 'update',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
handlEdit(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
text: stateText,
|
||||
tooltip: {
|
||||
title: stateText,
|
||||
},
|
||||
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
|
||||
popConfirm: {
|
||||
title: `确认${stateText}?`,
|
||||
onConfirm: async () => {
|
||||
const res = await update(data.id, updateStatus[state]);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value?.reload();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
disabled: state === 'enabled',
|
||||
tooltip: {
|
||||
title:
|
||||
state === 'enabled' ? '请先禁用该组件,再删除。' : '删除',
|
||||
},
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
onConfirm: async () => {
|
||||
const res = await remove(data.id);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlEdit = (data: object) => {
|
||||
current.value = _.cloneDeep(data);
|
||||
visible.value = true;
|
||||
};
|
||||
const handlEye = (id: string) => {
|
||||
console.log(id);
|
||||
};
|
||||
const saveChange = (value: object) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
const getState = (record: Partial<Record<string, any>>) => {
|
||||
if (record) {
|
||||
if (record?.state?.value === 'enabled') {
|
||||
return { ...record?.runningState };
|
||||
} else {
|
||||
return {
|
||||
text: '禁用',
|
||||
value: 'disabled',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.card-item-content {
|
||||
min-height: 100px;
|
||||
|
||||
.card-item-content-title-a {
|
||||
// color: #000 !important;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
.card-item-content-box {
|
||||
min-height: 50px;
|
||||
}
|
||||
.card-item-content-text {
|
||||
margin-top: 10px;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 12px;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
}
|
||||
.details-text {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,19 @@
|
|||
export interface ConfigurationType {
|
||||
port: string | undefined;
|
||||
host: string | undefined;;
|
||||
username: string;
|
||||
password: string;
|
||||
endpoint: string,
|
||||
securityPolicy: string | undefined,
|
||||
securityMode: string | undefined,
|
||||
certificate: string | undefined,
|
||||
authType: string | undefined,
|
||||
|
||||
}
|
||||
|
||||
export interface FormDataType {
|
||||
name: string;
|
||||
provider: string | undefined,
|
||||
configuration: ConfigurationType;
|
||||
description?: string;
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="dash-board">
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<h3 style="width: 100px">点位数据量</h3>
|
||||
</div>
|
||||
<div class="right">
|
||||
<a-radio-group
|
||||
default-value="a"
|
||||
button-style="solid"
|
||||
style="margin-right: 10px"
|
||||
v-model:value="data.time.type"
|
||||
>
|
||||
<a-radio-button value="hour">
|
||||
最近1小时
|
||||
</a-radio-button>
|
||||
<a-radio-button value="today"> 今日 </a-radio-button>
|
||||
<a-radio-button value="week"> 近一周 </a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-range-picker
|
||||
:allowClear="false"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
v-model="data.time"
|
||||
@change="pickerTimeChange"
|
||||
>
|
||||
<template #suffixIcon
|
||||
><AIcon type="CalendarOutlined"
|
||||
/></template>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ref="chartRef" style="width: 100%; height: 350px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dashboard } from '@/api/data-collect/dashboard';
|
||||
import { getTimeByType, pointParams, pointOptionsSeries } from '../tool.ts';
|
||||
import * as echarts from 'echarts';
|
||||
import { Dayjs } from 'dayjs';
|
||||
|
||||
const chartRef = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
const data = ref({
|
||||
time: {
|
||||
type: 'hour',
|
||||
end: 0,
|
||||
start: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const pickerTimeChange = (
|
||||
value: [Dayjs, Dayjs],
|
||||
dateString: [string, string],
|
||||
) => {
|
||||
data.value.time.start = Date.parse(dateString[0]);
|
||||
data.value.time.end = Date.parse(dateString[1]);
|
||||
data.value.time.type = undefined;
|
||||
};
|
||||
|
||||
const getEcharts = async (val) => {
|
||||
loading.value = true;
|
||||
const resp = await dashboard(pointParams(val));
|
||||
if (resp.success) {
|
||||
const x = resp.result
|
||||
.map((item: any) => item.data.timeString)
|
||||
.reverse();
|
||||
const y = resp.result.map((item: any) => item.data.value).reverse();
|
||||
handleOptions(x, y);
|
||||
}
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleOptions = (x = [], y = []) => {
|
||||
const chart = chartRef.value;
|
||||
if (chart) {
|
||||
const myChart = echarts.init(chart);
|
||||
const options = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: x,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
grid: {
|
||||
left: '80px',
|
||||
right: '50px',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
color: ['#979AFF'],
|
||||
series: [
|
||||
{
|
||||
name: '消息量',
|
||||
data: y,
|
||||
...pointOptionsSeries,
|
||||
},
|
||||
],
|
||||
};
|
||||
myChart.setOption(options);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => data.value.time.type,
|
||||
(value) => {
|
||||
data.value.time.end = Date.parse(new Date());
|
||||
data.value.time.start = Date.parse(getTimeByType(value));
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
watch(
|
||||
() => data.value,
|
||||
(value) => {
|
||||
const { time } = value;
|
||||
if (time.type || (time.end && time.start)) {
|
||||
getEcharts(value);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dash-board {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
|
||||
border-radius: 2px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.left h3 {
|
||||
width: 200px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.empty {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div class="top-card">
|
||||
<div class="top-card-content">
|
||||
<div class="content-left">
|
||||
<div class="content-left-title">
|
||||
<span>{{ title }}</span>
|
||||
<a-tooltip placement="top" v-if="tooltip">
|
||||
<template #title>
|
||||
<span>{{ tooltip }}</span>
|
||||
</template>
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="content-left-value">{{ value }}</div>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-card-footer">
|
||||
<template v-for="(item, index) in footer" :key="index">
|
||||
<span v-if="!item.status">{{ item.title }}</span>
|
||||
<a-badge v-else :text="item.title" :status="item.status" />
|
||||
<div class="footer-item-value">{{ item.value }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import type { Footer } from '../typings.d';
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
tooltip: { type: String, default: '' },
|
||||
img: { type: String, default: '' },
|
||||
footer: { type: Array as PropType<Footer[]>, default: '' },
|
||||
value: { type: Number, default: 0 },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.top-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// height: 200px;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e4e8;
|
||||
border-radius: 2px;
|
||||
max-height: 215px;
|
||||
.top-card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
.content-left {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
&-title {
|
||||
color: rgba(0, 0, 0, 0.64);
|
||||
}
|
||||
&-value {
|
||||
padding: 12px 0;
|
||||
color: #323130;
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
.content-right {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 123px;
|
||||
max-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.top-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
.footer-item-value {
|
||||
color: #323130;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,38 @@
|
|||
.media-dash-board {
|
||||
.top-card-items {
|
||||
margin-bottom: 12px;
|
||||
height: 100px;
|
||||
.top-card-item {
|
||||
width: 25%;
|
||||
padding: 6px 24px;
|
||||
border: 1px solid #e3e3e3;
|
||||
|
||||
.top-card-top {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
|
||||
.top-card-top-left {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.top-card-top-right {
|
||||
.top-card-total {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-card-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-dash-board-body {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8" v-for="item in statusData" :key="item[0].type">
|
||||
<TopCard
|
||||
:title="item[0].label"
|
||||
:img="
|
||||
getImage(`/DataCollect/dashboard/${item[0].type}.png`)
|
||||
"
|
||||
:footer="item"
|
||||
:value="item[0].total"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<Card />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TopCard from './components/TopCard.vue';
|
||||
import Card from './components/Card.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { queryCount } from '@/api/data-collect/dashboard';
|
||||
import { defaultParams, statusData } from './tool';
|
||||
|
||||
const getNumberData = () => {
|
||||
statusData.value.forEach(async (item) => {
|
||||
const res = await queryCount(item[0].type, {});
|
||||
const resp = await queryCount(item[0].type, defaultParams);
|
||||
item[0].total = res.result || 0;
|
||||
item[0].value = resp.result || 0;
|
||||
});
|
||||
};
|
||||
getNumberData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,155 @@
|
|||
import moment from 'moment';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const getParams = (dt: any) => {
|
||||
switch (dt.type) {
|
||||
case 'today':
|
||||
return {
|
||||
limit: 24,
|
||||
interval: '1h',
|
||||
format: 'HH:mm',
|
||||
};
|
||||
case 'week':
|
||||
return {
|
||||
limit: 7,
|
||||
interval: '1d',
|
||||
format: 'MM-dd',
|
||||
};
|
||||
case 'hour':
|
||||
return {
|
||||
limit: 60,
|
||||
interval: '1m',
|
||||
format: 'HH:mm',
|
||||
};
|
||||
default:
|
||||
const time = dt.end - dt.start;
|
||||
const hour = 60 * 60 * 1000;
|
||||
const days = hour * 24;
|
||||
const year = days * 365;
|
||||
if (time <= hour) {
|
||||
return {
|
||||
limit: Math.abs(Math.ceil(time / (60 * 60))),
|
||||
interval: '1m',
|
||||
format: 'HH:mm',
|
||||
};
|
||||
} else if (time > hour && time <= days) {
|
||||
return {
|
||||
limit: Math.abs(Math.ceil(time / hour)),
|
||||
interval: '1h',
|
||||
format: 'HH:mm',
|
||||
};
|
||||
} else if (time >= year) {
|
||||
return {
|
||||
limit: Math.abs(Math.ceil(time / days / 31)) + 1,
|
||||
interval: '1M',
|
||||
format: 'yyyy年-M月',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
limit: Math.abs(Math.ceil(time / days)) + 1,
|
||||
interval: '1d',
|
||||
format: 'MM-dd',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getTimeByType = (type) => {
|
||||
switch (type) {
|
||||
case 'hour':
|
||||
return moment().subtract(1, 'hours');
|
||||
case 'week':
|
||||
return moment().subtract(6, 'days');
|
||||
case 'month':
|
||||
return moment().subtract(29, 'days');
|
||||
case 'year':
|
||||
return moment().subtract(365, 'days');
|
||||
default:
|
||||
return moment().startOf('day');
|
||||
}
|
||||
};
|
||||
|
||||
export const pointParams = (data) => [
|
||||
{
|
||||
dashboard: 'collector',
|
||||
object: 'pointData',
|
||||
measurement: 'quantity',
|
||||
dimension: 'agg',
|
||||
params: {
|
||||
limit: getParams(data.time).limit,
|
||||
from: data.time.start,
|
||||
to: data.time.end,
|
||||
interval: getParams(data.time).interval,
|
||||
format: getParams(data.time).format,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const pointOptionsSeries = {
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: '#60DFC7',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#60DFC7', // 100% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#FFFFFF', // 0% 处的颜色
|
||||
},
|
||||
],
|
||||
global: false, // 缺省为 false
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultParams = {
|
||||
terms: [
|
||||
{
|
||||
column: 'runningState',
|
||||
termType: 'not',
|
||||
value: 'running',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const statusData = ref([
|
||||
[
|
||||
{
|
||||
type: 'channel',
|
||||
title: '异常通道',
|
||||
status: 'error',
|
||||
label: '通道数量',
|
||||
value: 0,
|
||||
total: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'collector',
|
||||
title: '异常采集器',
|
||||
status: 'error',
|
||||
label: '采集器数量',
|
||||
value: 0,
|
||||
total: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'point',
|
||||
title: '异常点位',
|
||||
status: 'error',
|
||||
label: '采集点位',
|
||||
value: 0,
|
||||
total: 0,
|
||||
},
|
||||
],
|
||||
]);
|
|
@ -0,0 +1,18 @@
|
|||
export type Agg = {
|
||||
duration: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type AggPlaying = {
|
||||
playerTotal: number;
|
||||
playingTotal: number;
|
||||
};
|
||||
|
||||
export type Footer = {
|
||||
title: string;
|
||||
value: number | string;
|
||||
total: number | string;
|
||||
status?: 'default' | 'error' | 'success' | 'warning' | 'processing' | '';
|
||||
type: string;
|
||||
label: string;
|
||||
};
|
|
@ -54,18 +54,6 @@
|
|||
}"
|
||||
>
|
||||
<AIcon type="ReadIconOutlined" />
|
||||
<!-- <svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 18H6L2 22V2C2 2 2.9 2 4 2H20C21.1 2 22 2 22 2V11H20V4H4V16H12V18ZM23 14.34L21.59 12.93L17.35 17.17L15.23 15.05L13.82 16.46L17.34 20L23 14.34Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg> -->
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
type="link"
|
||||
|
@ -80,7 +68,11 @@
|
|||
</template>
|
||||
</j-pro-table>
|
||||
|
||||
<ViewDialog v-if="viewVisible" v-model:visible="viewVisible" :data="viewItem" />
|
||||
<ViewDialog
|
||||
v-if="viewVisible"
|
||||
v-model:visible="viewVisible"
|
||||
:data="viewItem"
|
||||
/>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
|
@ -97,7 +89,10 @@ import { optionItem } from '@/views/rule-engine/Scene/typings';
|
|||
import { dictItemType } from '@/views/system/DataSource/typing';
|
||||
import moment from 'moment';
|
||||
import { message } from 'ant-design-vue';
|
||||
import NoticeCp from '@/components/Layout/components/Notice.vue';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
const { updateAlarm } = useUserInfo();
|
||||
const columns = [
|
||||
{
|
||||
title: '类型',
|
||||
|
@ -181,6 +176,7 @@ const table = {
|
|||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
table.refresh();
|
||||
updateAlarm();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -316,7 +316,7 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
|||
|
||||
const getProvidersList = async () => {
|
||||
const res = await getProviders();
|
||||
providersList = res.result;
|
||||
providersList.value = res.result;
|
||||
};
|
||||
getProvidersList();
|
||||
|
||||
|
@ -337,7 +337,7 @@ const handlEye = (id: string) => {
|
|||
const getDescription = (slotProps: Record<string, any>) =>
|
||||
slotProps.description
|
||||
? slotProps.description
|
||||
: providersList?.find(
|
||||
: providersList.value?.find(
|
||||
(item: Record<string, any>) => item.id === slotProps.provider,
|
||||
)?.description;
|
||||
|
||||
|
|
|
@ -33,8 +33,7 @@
|
|||
>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
<a-empty v-if="empty" class="empty" />
|
||||
<div v-else ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
@ -54,7 +53,6 @@ import {
|
|||
|
||||
const chartRef = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
const empty = ref(false);
|
||||
const data = ref({
|
||||
type: 'hour',
|
||||
time: [null, null],
|
||||
|
@ -108,7 +106,6 @@ const handleCpuOptions = (optionsData, xAxis) => {
|
|||
if (chart) {
|
||||
const myChart = echarts.init(chart);
|
||||
const dataKeys = Object.keys(optionsData);
|
||||
|
||||
const options = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
|
@ -143,7 +140,6 @@ const handleCpuOptions = (optionsData, xAxis) => {
|
|||
: typeDataLine,
|
||||
};
|
||||
myChart.setOption(options);
|
||||
xAxis.length === 0 && (empty.value = true);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
|
@ -190,7 +186,4 @@ watch(
|
|||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -33,8 +33,7 @@
|
|||
>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
<a-empty v-if="empty" class="empty" />
|
||||
<div v-else ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
@ -53,7 +52,6 @@ import {
|
|||
} from './tool.ts';
|
||||
|
||||
const chartRef = ref<Record<string, any>>({});
|
||||
const empty = ref(false);
|
||||
const loading = ref(false);
|
||||
const data = ref({
|
||||
type: 'hour',
|
||||
|
@ -147,7 +145,6 @@ const handleJVMOptions = (optionsData, xAxis) => {
|
|||
: typeDataLine,
|
||||
};
|
||||
myChart.setOption(options);
|
||||
xAxis.length === 0 && (empty.value = true);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
|
@ -194,7 +191,4 @@ watch(
|
|||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,18 +37,13 @@
|
|||
@change="pickerTimeChange"
|
||||
>
|
||||
<template #suffixIcon
|
||||
><a-icon type="calendar"
|
||||
><AIcon type="CalendarOutlined"
|
||||
/></template>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a-empty v-if="empty" class="empty" />
|
||||
<div
|
||||
v-else
|
||||
ref="chartRef"
|
||||
style="width: 100%; height: 350px"
|
||||
></div>
|
||||
<div ref="chartRef" style="width: 100%; height: 350px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
@ -61,13 +56,11 @@ import {
|
|||
typeDataLine,
|
||||
areaStyle,
|
||||
networkParams,
|
||||
arrayReverse,
|
||||
} from './tool.ts';
|
||||
import moment from 'moment';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const chartRef = ref<Record<string, any>>({});
|
||||
const empty = ref(false);
|
||||
const loading = ref(false);
|
||||
const data = ref({
|
||||
type: 'bytesRead',
|
||||
|
@ -134,7 +127,6 @@ const setOptions = (data, key) => ({
|
|||
|
||||
const handleNetworkOptions = (optionsData, xAxis) => {
|
||||
const chart = chartRef.value;
|
||||
|
||||
if (chart) {
|
||||
const myChart = echarts.init(chart);
|
||||
const dataKeys = Object.keys(optionsData);
|
||||
|
@ -148,7 +140,7 @@ const handleNetworkOptions = (optionsData, xAxis) => {
|
|||
type: 'value',
|
||||
},
|
||||
grid: {
|
||||
left: '80px',
|
||||
left: '100px',
|
||||
right: '50px',
|
||||
},
|
||||
tooltip: {
|
||||
|
@ -161,7 +153,6 @@ const handleNetworkOptions = (optionsData, xAxis) => {
|
|||
: typeDataLine,
|
||||
};
|
||||
myChart.setOption(options);
|
||||
// xAxis.length === 0 && (empty.value = true);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
|
@ -215,7 +206,4 @@ watch(
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.empty {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
@padding: 20px;
|
||||
|
||||
.split-screen {
|
||||
display: flex;
|
||||
|
||||
.left-content {
|
||||
width: 300px;
|
||||
padding-right: @padding;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
|
||||
.online {
|
||||
color: rgba(82, 196, 26, 1);
|
||||
.anticon {
|
||||
color: rgba(82, 196, 26, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
.anticon {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.left-search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding-left: @padding;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-basis: 30px;
|
||||
justify-content: center;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.live-player {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.live-player-content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
.player-screen {
|
||||
display: grid;
|
||||
|
||||
&.screen-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&.screen-4 {
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
&.screen-9 {
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&.screen-4,
|
||||
&.screen-9 {
|
||||
grid-gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player-tools {
|
||||
flex-basis: 280px;
|
||||
padding: 50px 12px 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen {
|
||||
@media (min-width: 1300px) {
|
||||
.split-screen {
|
||||
.left-content {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<a-card class="splitScreen">
|
||||
<div class="split-screen">
|
||||
<LeftTree @onSelect="mediaStart" />
|
||||
<div class="right-content">
|
||||
<ScreenPlayer
|
||||
ref="player"
|
||||
:id="deviceId"
|
||||
:channelId="channelId"
|
||||
:onMouseUp="(id, cId) => channelApi.ptzStop(id, cId)"
|
||||
:onMouseDown="
|
||||
(id, cId, type) => channelApi.ptzTool(id, cId, type)
|
||||
"
|
||||
:historyHandle="(dId, cId) => getMediaUrl(dId, cId)"
|
||||
showScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LeftTree from './tree.vue';
|
||||
import channelApi from '@/api/media/channel';
|
||||
import ScreenPlayer from '@/components/Player/ScreenPlayer.vue';
|
||||
|
||||
const deviceId = ref('');
|
||||
const channelId = ref('');
|
||||
const player = ref();
|
||||
|
||||
/**
|
||||
* 获取视频链接
|
||||
* @param dId
|
||||
* @param cId
|
||||
*/
|
||||
const getMediaUrl = (dId: string, cId: string): string => {
|
||||
return channelApi.ptzStart(dId, cId, 'mp4');
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击左侧摄像头, 播放对应视频
|
||||
* @param e
|
||||
*/
|
||||
const mediaStart = (e: { cId: string; dId: string }) => {
|
||||
channelId.value = e.cId;
|
||||
deviceId.value = e.dId;
|
||||
player.value?.replaceVideo(e.dId, e.cId, getMediaUrl(e.dId, e.cId));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
</style>
|
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<div class="left-content">
|
||||
<a-tree
|
||||
:height="700"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:show-icon="true"
|
||||
:tree-data="treeData"
|
||||
:loadData="onLoadData"
|
||||
:fieldNames="{ title: 'name', key: 'id' }"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #icon="{ id, selected }">
|
||||
<AIcon
|
||||
type="VideoCameraOutlined"
|
||||
v-if="!treeData.find((f: any) => f.id === id)"
|
||||
/>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import cascadeApi from '@/api/media/cascade';
|
||||
|
||||
type Emits = {
|
||||
(e: 'onSelect', data: { dId: string; cId: string }): void;
|
||||
};
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
interface DataNode {
|
||||
name: string;
|
||||
id: string;
|
||||
isLeaf?: boolean;
|
||||
channelNumber?: number;
|
||||
icon?: any;
|
||||
status: {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
children?: DataNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击节点
|
||||
* @param _
|
||||
* @param param1
|
||||
*/
|
||||
const onSelect = (_: any, { node }: any) => {
|
||||
emit('onSelect', { dId: node.deviceId, cId: node.channelId });
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为子节点
|
||||
* @param node
|
||||
*/
|
||||
const isLeaf = (node: any): boolean => {
|
||||
if (node.channelNumber) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
*/
|
||||
const treeData = ref<any[]>([]);
|
||||
const getDeviceList = async () => {
|
||||
const res = await cascadeApi.getMediaTree({ paging: false });
|
||||
if (res.success) {
|
||||
treeData.value = res.result.map((m: any) => {
|
||||
const extra: any = {};
|
||||
extra.isLeaf = isLeaf(m);
|
||||
return {
|
||||
...m,
|
||||
...extra,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
getDeviceList();
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param list
|
||||
* @param key
|
||||
* @param children
|
||||
*/
|
||||
const updateTreeData = (
|
||||
list: DataNode[],
|
||||
key: any,
|
||||
children: DataNode[],
|
||||
): DataNode[] => {
|
||||
return list.map((node) => {
|
||||
if (node.id === key) {
|
||||
return {
|
||||
...node,
|
||||
children: node.children
|
||||
? [...node.children, ...children]
|
||||
: children,
|
||||
};
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateTreeData(node.children, key, children),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取子节点
|
||||
* @param key
|
||||
* @param params
|
||||
*/
|
||||
const getChildren = (key: any, params: any): Promise<any> => {
|
||||
return new Promise(async (resolve) => {
|
||||
const res = await cascadeApi.queryChannelList(params);
|
||||
if (res.status === 200) {
|
||||
const { total, pageIndex, pageSize } = res.result;
|
||||
treeData.value = updateTreeData(
|
||||
treeData.value,
|
||||
key,
|
||||
res.result.data.map((item: DataNode) => ({
|
||||
...item,
|
||||
class: item.status.value,
|
||||
isLeaf: isLeaf(item),
|
||||
})),
|
||||
);
|
||||
|
||||
if (total > (pageIndex + 1) * pageSize) {
|
||||
setTimeout(() => {
|
||||
getChildren(key, {
|
||||
...params,
|
||||
pageIndex: params.pageIndex + 1,
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
resolve(res.result);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 异步加载子节点数据
|
||||
* @param param0
|
||||
*/
|
||||
const onLoadData = ({ key, children }: any): Promise<void> => {
|
||||
return new Promise(async (resolve) => {
|
||||
if (children) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
await getChildren(key, {
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
terms: [
|
||||
{
|
||||
column: 'deviceId',
|
||||
value: key,
|
||||
},
|
||||
],
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
</style>
|
|
@ -3903,8 +3903,8 @@ jetlinks-store@^0.0.3:
|
|||
|
||||
jetlinks-ui-components@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.3.tgz#8307018ed5bcdd42f3bb4e29376e04f3e8a425db"
|
||||
integrity sha512-jH/BtAu21zVzMopHCXEn1jwbGHh/1XHP68ekiOGH+jWowLbb2pkX0kHFyF6ydhYa7cspvJ04atWlEugXvXZYvg==
|
||||
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.3.tgz#303ca83cf6096721e49e72d1a3a73b054b0aa7fa"
|
||||
integrity sha512-Jm7tP/CtnK2GIRSPTjd/UOw8emZ3C7/i9af8m+XCM8wi/J1SZh4cZGc487vR1DPxyWZfJjG87Zdy45DZ5EMw2w==
|
||||
dependencies:
|
||||
"@vueuse/core" "^9.12.0"
|
||||
ant-design-vue "^3.2.15"
|
||||
|
|
Loading…
Reference in New Issue