Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
7daa9df6a6
|
@ -0,0 +1,13 @@
|
||||||
|
import server from '@/utils/request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 列表
|
||||||
|
list: (data: any) => server.post(`/media/device/_query/`, data),
|
||||||
|
// 详情
|
||||||
|
detail: (id: string): any => server.get(`/media/device/${id}`),
|
||||||
|
// 新增
|
||||||
|
save: (data: any) => server.post(`/media/device/${data.channel}`, data),
|
||||||
|
// 修改
|
||||||
|
update: (data: any) => server.put(`/media/device/${data.channel}/${data.id}`, data),
|
||||||
|
del: (id: string) => server.remove(`/media/device/${id}`),
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
/**
|
||||||
|
* 获取今日及当月告警数量
|
||||||
|
*/
|
||||||
|
export const dashboard = (data:Record<string,any[]>)=> server.post('/dashboard/_multi',data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const getAlarm = (params:Record<string,any[]>) => server.get('/alarm/record/_query',params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取告警数量
|
||||||
|
*/
|
||||||
|
export const getAlarmConfigCount = (data:Record<string,any>) => server.post('/alarm/config/_count',data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取报警等级
|
||||||
|
*/
|
||||||
|
export const getAlarmLevel = () => server.get('/alarm/config/default/level');
|
|
@ -45,7 +45,10 @@ const iconKeys = [
|
||||||
'InfoCircleOutlined',
|
'InfoCircleOutlined',
|
||||||
'SearchOutlined',
|
'SearchOutlined',
|
||||||
'EllipsisOutlined',
|
'EllipsisOutlined',
|
||||||
'ClockCircleOutlined'
|
'ClockCircleOutlined',
|
||||||
|
'PartitionOutlined',
|
||||||
|
'ShareAltOutlined',
|
||||||
|
'playCircleOutlined',
|
||||||
]
|
]
|
||||||
|
|
||||||
const Icon = (props: {type: string}) => {
|
const Icon = (props: {type: string}) => {
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<Charts :options="TodayDevOptions"></Charts> </TopCard
|
<Charts :options="TodayDevOptions"></Charts> </TopCard
|
||||||
></a-col>
|
></a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :span="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<div class="message-card">
|
<div class="message-card">
|
||||||
<Guide title="设备消息">
|
<Guide title="设备消息">
|
||||||
|
@ -452,6 +452,7 @@ const getEcharts = (data: any) => {
|
||||||
_time = '1M';
|
_time = '1M';
|
||||||
format = 'yyyy年-M月';
|
format = 'yyyy年-M月';
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard([
|
dashboard([
|
||||||
{
|
{
|
||||||
dashboard: 'device',
|
dashboard: 'device',
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-container">save</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -0,0 +1,334 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<Search
|
||||||
|
:columns="columns"
|
||||||
|
target="notice-config"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
<JTable
|
||||||
|
ref="listRef"
|
||||||
|
:columns="columns"
|
||||||
|
:request="DeviceApi.list"
|
||||||
|
:defaultParams="{
|
||||||
|
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||||
|
}"
|
||||||
|
:params="params"
|
||||||
|
>
|
||||||
|
<template #headerTitle>
|
||||||
|
<a-button type="primary" @click="handleAdd"> 新增 </a-button>
|
||||||
|
</template>
|
||||||
|
<template #card="slotProps">
|
||||||
|
<CardBox
|
||||||
|
:value="slotProps"
|
||||||
|
:actions="getActions(slotProps, 'card')"
|
||||||
|
v-bind="slotProps"
|
||||||
|
:showStatus="true"
|
||||||
|
:status="
|
||||||
|
slotProps.state.value === 'online' ? 'success' : 'error'
|
||||||
|
"
|
||||||
|
:statusText="slotProps.state.text"
|
||||||
|
:statusNames="{ success: 'success', error: 'error' }"
|
||||||
|
>
|
||||||
|
<template #img>
|
||||||
|
<slot name="img">
|
||||||
|
<img :src="getImage('/device-media.png')" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<h3 class="card-item-content-title">
|
||||||
|
{{ slotProps.name }}
|
||||||
|
</h3>
|
||||||
|
<a-row>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">厂商</div>
|
||||||
|
<div>{{ slotProps.manufacturer }}</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
通道数量
|
||||||
|
</div>
|
||||||
|
<div>{{ slotProps.channelNumber }}</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">型号</div>
|
||||||
|
<div>{{ slotProps.model }}</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
接入方式
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ providerType[slotProps.provider] }}
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
<template #actions="item">
|
||||||
|
<a-tooltip
|
||||||
|
v-bind="item.tooltip"
|
||||||
|
:title="item.disabled && item.tooltip.title"
|
||||||
|
>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="item.popConfirm"
|
||||||
|
v-bind="item.popConfirm"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
>
|
||||||
|
<a-button :disabled="item.disabled">
|
||||||
|
<AIcon type="DeleteOutlined" />
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
<template v-else>
|
||||||
|
<a-button
|
||||||
|
:disabled="item.disabled"
|
||||||
|
@click="item.onClick"
|
||||||
|
>
|
||||||
|
<AIcon :type="item.icon" />
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</CardBox>
|
||||||
|
</template>
|
||||||
|
<template #action="slotProps">
|
||||||
|
<a-space :size="16">
|
||||||
|
<a-tooltip
|
||||||
|
v-for="i in getActions(slotProps, 'table')"
|
||||||
|
:key="i.key"
|
||||||
|
v-bind="i.tooltip"
|
||||||
|
>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="i.popConfirm"
|
||||||
|
v-bind="i.popConfirm"
|
||||||
|
:disabled="i.disabled"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
:disabled="i.disabled"
|
||||||
|
style="padding: 0"
|
||||||
|
type="link"
|
||||||
|
><AIcon :type="i.icon"
|
||||||
|
/></a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-button
|
||||||
|
style="padding: 0"
|
||||||
|
type="link"
|
||||||
|
v-else
|
||||||
|
@click="i.onClick && i.onClick(slotProps)"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
:disabled="i.disabled"
|
||||||
|
style="padding: 0"
|
||||||
|
type="link"
|
||||||
|
><AIcon :type="i.icon"
|
||||||
|
/></a-button>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</JTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DeviceApi from '@/api/media/device';
|
||||||
|
import type { ActionsType } from '@/components/Table/index.vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const providerType = {
|
||||||
|
'gb28181-2016': 'GB/T28181',
|
||||||
|
'fixed-media': '固定地址',
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const listRef = ref<Record<string, any>>({});
|
||||||
|
const params = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '接入方式',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '固定地址', value: 'fixed-media' },
|
||||||
|
{ label: 'GB/T28181', value: 'gb28181-2016' },
|
||||||
|
],
|
||||||
|
handleValue: (v: any) => {
|
||||||
|
return '123';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通道数量',
|
||||||
|
dataIndex: 'channelNumber',
|
||||||
|
key: 'channelNumber',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '厂商',
|
||||||
|
dataIndex: 'manufacturer',
|
||||||
|
key: 'manufacturer',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品名称',
|
||||||
|
dataIndex: 'productId',
|
||||||
|
key: 'productId',
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '固定地址', value: 'fixed-media' },
|
||||||
|
{ label: 'GB/T28181', value: 'gb28181-2016' },
|
||||||
|
],
|
||||||
|
handleValue: (v: any) => {
|
||||||
|
return '123';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'state',
|
||||||
|
key: 'state',
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '禁用', value: 'notActive' },
|
||||||
|
{ label: '离线', value: 'offline' },
|
||||||
|
{ label: '在线', value: 'online' },
|
||||||
|
],
|
||||||
|
handleValue: (v: any) => {
|
||||||
|
return '123';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 250,
|
||||||
|
scopedSlots: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
const handleSearch = (e: any) => {
|
||||||
|
// console.log('handleSearch:', e);
|
||||||
|
params.value = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*/
|
||||||
|
const handleAdd = () => {
|
||||||
|
router.push(`/media/device/Save`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActions = (
|
||||||
|
data: Partial<Record<string, any>>,
|
||||||
|
type: 'card' | 'table',
|
||||||
|
): ActionsType[] => {
|
||||||
|
if (!data) return [];
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
text: '编辑',
|
||||||
|
tooltip: {
|
||||||
|
title: '编辑',
|
||||||
|
},
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
router.push(`/media/device/Save?id=${data.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
text: '查看通道',
|
||||||
|
tooltip: {
|
||||||
|
title: '查看通道',
|
||||||
|
},
|
||||||
|
icon: 'PartitionOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
router.push(
|
||||||
|
`/media/device/Channel?id=${data.id}&type=${data.provider}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'debug',
|
||||||
|
text: '更新通道',
|
||||||
|
tooltip: {
|
||||||
|
title:
|
||||||
|
data.provider === 'fixed-media'
|
||||||
|
? '固定地址无法更新通道'
|
||||||
|
: data.state.value === 'offline'
|
||||||
|
? '设备已离线'
|
||||||
|
: data.state.value === 'notActive'
|
||||||
|
? '设备已禁用'
|
||||||
|
: '',
|
||||||
|
},
|
||||||
|
disabled:
|
||||||
|
data.state.value === 'offline' ||
|
||||||
|
data.state.value === 'notActive' ||
|
||||||
|
data.provider === 'fixed-media',
|
||||||
|
icon: 'SyncOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
// updateChannel()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
text: '删除',
|
||||||
|
tooltip: {
|
||||||
|
title: '在线设备无法删除',
|
||||||
|
},
|
||||||
|
disabled: data.state.value === 'online',
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除?',
|
||||||
|
onConfirm: async () => {
|
||||||
|
const resp = await DeviceApi.del(data.id);
|
||||||
|
if (resp.status === 200) {
|
||||||
|
message.success('操作成功!');
|
||||||
|
listRef.value?.reload();
|
||||||
|
} else {
|
||||||
|
message.error('操作失败!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: 'DeleteOutlined',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.page-container {
|
||||||
|
background: #f0f2f5;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
type BaseItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
type State = {
|
||||||
|
value: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeviceItem = {
|
||||||
|
photoUrl?: string;
|
||||||
|
channelNumber: number;
|
||||||
|
createTime: number;
|
||||||
|
firmware: string;
|
||||||
|
gatewayId: string;
|
||||||
|
host: string;
|
||||||
|
manufacturer: string;
|
||||||
|
model: string;
|
||||||
|
port: number;
|
||||||
|
provider: string;
|
||||||
|
state: State;
|
||||||
|
streamMode: string;
|
||||||
|
transport: string;
|
||||||
|
} & BaseItem;
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div class="chart" ref="chart"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const { proxy } = <any>getCurrentInstance();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 图表数据
|
||||||
|
options:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>{}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制图表
|
||||||
|
*/
|
||||||
|
const createChart = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const myChart = echarts.init(proxy.$refs.chart);
|
||||||
|
myChart.setOption(props.options);
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
myChart.resize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.options,
|
||||||
|
() => createChart(),
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="home-title">
|
||||||
|
<div v-if="title">{{ title }}</div>
|
||||||
|
<div v-else class="title">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="extra-text">
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="home-title-english">{{ english }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="Guide">
|
||||||
|
interface guideProps {
|
||||||
|
title?: string;
|
||||||
|
english?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<guideProps>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.home-title {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-left: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: @primary-color;
|
||||||
|
border: 1px solid #b4c0da;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.title{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-title-english {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<div class="new-alarm">
|
||||||
|
<div class="title">最新警告</div>
|
||||||
|
<div v-if="alarmList.length" class="new-alarm-items">
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in alarmList.slice(0, 3)" :key="item">
|
||||||
|
<div class="new-alarm-item">
|
||||||
|
<div class="new-alarm-item-time">
|
||||||
|
<img
|
||||||
|
:src="getImage('/alarm/bashboard.png')"
|
||||||
|
alt=""
|
||||||
|
/>{{
|
||||||
|
moment(item.alarmTime).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="new-alarm-item-content">
|
||||||
|
<a-tooltip
|
||||||
|
:title="item.alarmName"
|
||||||
|
placement="topLeft"
|
||||||
|
>
|
||||||
|
<a>{{ item.alarmName }}</a>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="new-alarm-item-state">
|
||||||
|
<a-badge
|
||||||
|
:status="
|
||||||
|
item.state?.value === 'warning'
|
||||||
|
? 'error'
|
||||||
|
: 'default'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</a-badge>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
item.state?.value === 'warning'
|
||||||
|
? 'error'
|
||||||
|
: 'default'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ item.state?.text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'new-alarm-item-level',
|
||||||
|
`level-${item.level}`,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ item.levelName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-body">
|
||||||
|
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE"></a-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Empty } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import moment from 'moment';
|
||||||
|
const props = defineProps({
|
||||||
|
alarmList: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped lang="less">
|
||||||
|
.new-alarm {
|
||||||
|
background-color: white;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e0e4e8;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.new-alarm-items {
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.new-alarm-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 18px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
.new-alarm-item-time {
|
||||||
|
width: 180px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.new-alarm-item-content {
|
||||||
|
width: ~'calc(100% - 360px)';
|
||||||
|
}
|
||||||
|
.new-alarm-item-state {
|
||||||
|
width: 90px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
.error {
|
||||||
|
color: @error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.new-alarm-item-level {
|
||||||
|
width: 52px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&.level-1 {
|
||||||
|
background-color: #e50012;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-2 {
|
||||||
|
background-color: #ff9457;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-3 {
|
||||||
|
background-color: #fabd47;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-4 {
|
||||||
|
background-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-5 {
|
||||||
|
background-color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-body {
|
||||||
|
height: 142px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-radio-group
|
||||||
|
v-if="quickBtn"
|
||||||
|
default-value="today"
|
||||||
|
button-style="solid"
|
||||||
|
v-model:value="radioValue"
|
||||||
|
@change="(e) => handleBtnChange(e.target.value)"
|
||||||
|
>
|
||||||
|
<a-radio-button
|
||||||
|
v-for="item in quickBtnList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-range-picker
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="margin-left: 12px"
|
||||||
|
@change="rangeChange"
|
||||||
|
v-model:value="rangeVal"
|
||||||
|
:allowClear="false"
|
||||||
|
>
|
||||||
|
</a-range-picker>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import moment from 'moment';
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
interface BtnOptions {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmitProps {
|
||||||
|
(e: 'change', data: Record<string, any>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<EmitProps>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 显示快捷按钮
|
||||||
|
quickBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 快捷按钮列表
|
||||||
|
quickBtnList: {
|
||||||
|
type: Array as PropType<BtnOptions[]>,
|
||||||
|
default: [
|
||||||
|
{ label: '今日', value: 'today' },
|
||||||
|
{ label: '近一周', value: 'week' },
|
||||||
|
{ label: '近一月', value: 'month' },
|
||||||
|
{ label: '近一年', value: 'year' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'today',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const radioValue = ref(props.type || 'week' || undefined);
|
||||||
|
const rangeVal = ref<[string, string]>();
|
||||||
|
|
||||||
|
const rangeChange = (val: any) => {
|
||||||
|
radioValue.value = undefined;
|
||||||
|
emit('change', {
|
||||||
|
start: moment(val[0]).valueOf(),
|
||||||
|
end: moment(val[1]).valueOf(),
|
||||||
|
type: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeByType = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'hour':
|
||||||
|
return moment().subtract(1, 'hours').valueOf();
|
||||||
|
case 'week':
|
||||||
|
return moment().subtract(6, 'days').valueOf();
|
||||||
|
case 'month':
|
||||||
|
return moment().subtract(29, 'days').valueOf();
|
||||||
|
case 'year':
|
||||||
|
return moment().subtract(365, 'days').valueOf();
|
||||||
|
default:
|
||||||
|
return moment().startOf('day').valueOf();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBtnChange = (val: string) => {
|
||||||
|
radioValue.value = val;
|
||||||
|
let endTime = moment(new Date()).valueOf();
|
||||||
|
let startTime = getTimeByType(val);
|
||||||
|
if (val === 'yesterday') {
|
||||||
|
startTime = moment().subtract(1, 'days').startOf('day').valueOf();
|
||||||
|
endTime = moment().subtract(1, 'days').endOf('day').valueOf();
|
||||||
|
}
|
||||||
|
rangeVal.value = [
|
||||||
|
moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
];
|
||||||
|
emit('change', {
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
type: val,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
handleBtnChange(radioValue.value);
|
||||||
|
watch(
|
||||||
|
() => radioValue.value,
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<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" v-if="img">
|
||||||
|
<img :src="img" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="content-right-echart" v-else>
|
||||||
|
<slot></slot>
|
||||||
|
</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 '@/views/device/DashBoard/typings'
|
||||||
|
|
||||||
|
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;
|
||||||
|
.top-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
.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: 123px;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: .7;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-right-echart{
|
||||||
|
height: 123px;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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,547 @@
|
||||||
|
<template>
|
||||||
|
<page-container>
|
||||||
|
<div class="DashBoardBox">
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="6">
|
||||||
|
<TopCard
|
||||||
|
title="今日告警"
|
||||||
|
:value="state.today"
|
||||||
|
:footer="currentMonAlarm"
|
||||||
|
>
|
||||||
|
<Charts :options="state.fifteenOptions"></Charts>
|
||||||
|
</TopCard>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<TopCard
|
||||||
|
title="告警配置"
|
||||||
|
:value="state.config"
|
||||||
|
:footer="alarmState"
|
||||||
|
:img="getImage('/device/device-number.png')"
|
||||||
|
></TopCard>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<NewAlarm :alarm-list="state.alarmList"></NewAlarm>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<div class="alarm-card">
|
||||||
|
<Guide>
|
||||||
|
<template #title>
|
||||||
|
<span style="margin-right: 24px">告警统计</span>
|
||||||
|
<a-select
|
||||||
|
style="width: 40%"
|
||||||
|
v-model:value="queryCodition.targetType"
|
||||||
|
:options="
|
||||||
|
isNoCommunity ? selectOpt1 : selectOpt2
|
||||||
|
"
|
||||||
|
@change="selectChange"
|
||||||
|
></a-select>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<TimeSelect
|
||||||
|
key="flow-static"
|
||||||
|
:type="'week'"
|
||||||
|
:quickBtnList="quickBtnList"
|
||||||
|
@change="initQueryTime"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Guide>
|
||||||
|
<div class="alarmBox">
|
||||||
|
<div class="alarmStatistics-chart">
|
||||||
|
<Charts
|
||||||
|
:options="alarmStatisticsOption"
|
||||||
|
></Charts>
|
||||||
|
</div>
|
||||||
|
<div class="alarmRank">
|
||||||
|
<h4>告警排名</h4>
|
||||||
|
<ul v-if="state.ranking.length" class="rankingList">
|
||||||
|
<li v-for="(item,i) in state.ranking" :key="item.targetId">
|
||||||
|
<img :src="getImage(`/rule-engine/dashboard/ranking/${i+1}.png`)" alt="">
|
||||||
|
<span class="rankingItemTitle" :title="item.targetName">{{item.targetName}}</span>
|
||||||
|
<span class="rankingItemValue">{{item.count}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="empty-body">
|
||||||
|
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE"></a-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</page-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Empty } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import Charts from './components/Charts.vue';
|
||||||
|
import TopCard from './components/TopCard.vue';
|
||||||
|
import NewAlarm from './components/NewAlarm.vue';
|
||||||
|
import TimeSelect from './components/TimeSelect.vue';
|
||||||
|
import Guide from './components/Guide.vue';
|
||||||
|
import encodeQuery from '@/utils/encodeQuery';
|
||||||
|
import type { SelectTypes } from 'ant-design-vue/es/select';
|
||||||
|
import type { Footer } from '@/views/rule-engine/DashBoard/typings';
|
||||||
|
import { isNoCommunity } from '@/utils/utils';
|
||||||
|
import {
|
||||||
|
dashboard,
|
||||||
|
getAlarm,
|
||||||
|
getAlarmConfigCount,
|
||||||
|
getAlarmLevel,
|
||||||
|
} from '@/api/rule-engine/dashboard';
|
||||||
|
import moment from 'moment';
|
||||||
|
let currentMonAlarm = ref<Footer[]>([
|
||||||
|
{
|
||||||
|
title: '当月告警',
|
||||||
|
value: 0,
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let alarmState = ref<Footer[]>([
|
||||||
|
{
|
||||||
|
title: '正常',
|
||||||
|
value: 0,
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '禁用',
|
||||||
|
value: 0,
|
||||||
|
status: 'error',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const selectOpt1 = ref<Object[]>([
|
||||||
|
{ label: '设备', value: 'device' },
|
||||||
|
{ label: '产品', value: 'product' },
|
||||||
|
{ label: '组织', value: 'org' },
|
||||||
|
{ label: '其它', value: 'other' },
|
||||||
|
]);
|
||||||
|
const selectOpt2 = ref<SelectTypes['options']>([
|
||||||
|
{ label: '设备', value: 'device' },
|
||||||
|
{ label: '产品', value: 'product' },
|
||||||
|
{ label: '其它', value: 'other' },
|
||||||
|
]);
|
||||||
|
let queryCodition = reactive({
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
targetType: 'device',
|
||||||
|
});
|
||||||
|
let alarmStatisticsOption = ref<any>({});
|
||||||
|
const quickBtnList = [
|
||||||
|
{ label: '昨日', value: 'yesterday' },
|
||||||
|
{ label: '近一周', value: 'week' },
|
||||||
|
{ label: '近一月', value: 'month' },
|
||||||
|
{ label: '近一年', value: 'year' },
|
||||||
|
];
|
||||||
|
type DashboardItem = {
|
||||||
|
group: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
};
|
||||||
|
let state = reactive<{
|
||||||
|
today: number;
|
||||||
|
thisMonth: number;
|
||||||
|
config: number;
|
||||||
|
enabledConfig: number;
|
||||||
|
disabledConfig: number;
|
||||||
|
alarmList: any[];
|
||||||
|
ranking: { targetId: string; targetName: string; count: number }[];
|
||||||
|
fifteenOptions: any;
|
||||||
|
}>({
|
||||||
|
today: 0,
|
||||||
|
thisMonth: 0,
|
||||||
|
config: 0,
|
||||||
|
enabledConfig: 0,
|
||||||
|
disabledConfig: 0,
|
||||||
|
alarmList: [],
|
||||||
|
ranking: [],
|
||||||
|
fifteenOptions: {},
|
||||||
|
});
|
||||||
|
// 今日告警
|
||||||
|
const today = {
|
||||||
|
dashboard: 'alarm',
|
||||||
|
object: 'record',
|
||||||
|
measurement: 'trend',
|
||||||
|
dimension: 'agg',
|
||||||
|
group: 'today',
|
||||||
|
params: {
|
||||||
|
time: '1d',
|
||||||
|
// targetType: 'device',
|
||||||
|
format: 'HH:mm:ss',
|
||||||
|
from: moment(new Date(new Date().setHours(0, 0, 0, 0))).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
),
|
||||||
|
to: 'now',
|
||||||
|
// limit: 24,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// 当月告警
|
||||||
|
const thisMonth = {
|
||||||
|
dashboard: 'alarm',
|
||||||
|
object: 'record',
|
||||||
|
measurement: 'trend',
|
||||||
|
dimension: 'agg',
|
||||||
|
group: 'thisMonth',
|
||||||
|
params: {
|
||||||
|
time: '1M',
|
||||||
|
// targetType: 'device',
|
||||||
|
format: 'yyyy-MM',
|
||||||
|
limit: 1,
|
||||||
|
from: 'now-1M',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fifteen = {
|
||||||
|
dashboard: 'alarm',
|
||||||
|
object: 'record',
|
||||||
|
measurement: 'trend',
|
||||||
|
dimension: 'agg',
|
||||||
|
group: '15day',
|
||||||
|
params: {
|
||||||
|
time: '1d',
|
||||||
|
format: 'yyyy-MM-dd',
|
||||||
|
// targetType: 'product',
|
||||||
|
from: 'now-15d',
|
||||||
|
to: 'now',
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const getDashBoard = () => {
|
||||||
|
dashboard([today, thisMonth, fifteen]).then((res) => {
|
||||||
|
if (res.status == 200) {
|
||||||
|
const _data = res.result as DashboardItem[];
|
||||||
|
state.today = _data.find(
|
||||||
|
(item) => item.group === 'today',
|
||||||
|
)?.data.value;
|
||||||
|
state.thisMonth = _data.find(
|
||||||
|
(item) => item.group === 'thisMonth',
|
||||||
|
)?.data.value;
|
||||||
|
currentMonAlarm.value[0].value = state.thisMonth;
|
||||||
|
const fifteenData = _data
|
||||||
|
.filter((item) => item.group === '15day')
|
||||||
|
.map((item) => item.data)
|
||||||
|
.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
state.fifteenOptions = {
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: fifteenData.map((item) => item.timeString),
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: '2%',
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '告警数',
|
||||||
|
data: fifteenData.map((item) => item.value),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#2F54EB',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
getDashBoard();
|
||||||
|
const getAlarmConfig = async () => {
|
||||||
|
const countRes = await getAlarmConfigCount({});
|
||||||
|
const enabeldRes = await getAlarmConfigCount({
|
||||||
|
terms: [
|
||||||
|
{
|
||||||
|
column: 'state',
|
||||||
|
value: 'enabled',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const disableRes = await getAlarmConfigCount({
|
||||||
|
terms: [
|
||||||
|
{
|
||||||
|
column: 'state',
|
||||||
|
value: 'disabled',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (countRes.status == 200) {
|
||||||
|
state.config = countRes.result;
|
||||||
|
}
|
||||||
|
if (enabeldRes.status == 200) {
|
||||||
|
state.enabledConfig = enabeldRes.result;
|
||||||
|
alarmState.value[0].value = state.enabledConfig;
|
||||||
|
}
|
||||||
|
if (disableRes.status == 200) {
|
||||||
|
state.disabledConfig = disableRes.result;
|
||||||
|
alarmState.value[1].value = state.disabledConfig;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getAlarmConfig();
|
||||||
|
const getCurrentAlarm = async () => {
|
||||||
|
const alarmLevel: any = await getAlarmLevel();
|
||||||
|
const sorts = { alarmTime: 'desc' };
|
||||||
|
const currentAlarm: any = await getAlarm(encodeQuery({ sorts }));
|
||||||
|
if (currentAlarm.status === 200) {
|
||||||
|
if (alarmLevel.status === 200) {
|
||||||
|
const levels = alarmLevel.result.levels;
|
||||||
|
state.alarmList = currentAlarm.result?.data
|
||||||
|
.filter((i: any) => i?.state?.value === 'warning')
|
||||||
|
.map((item: { level: any }) => ({
|
||||||
|
...item,
|
||||||
|
levelName: levels.find((l: any) => l.level === item.level)
|
||||||
|
?.title,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
state.alarmList = currentAlarm.result?.data.filter(
|
||||||
|
(item: any) => item?.state?.value === 'warning',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getCurrentAlarm();
|
||||||
|
//初始化查询条件
|
||||||
|
const initQueryTime = (data: any) => {
|
||||||
|
queryCodition.startTime = data.start;
|
||||||
|
queryCodition.endTime = data.end;
|
||||||
|
console.log(queryCodition);
|
||||||
|
selectChange();
|
||||||
|
};
|
||||||
|
const selectChange = () => {
|
||||||
|
let time = '1h';
|
||||||
|
let format = 'HH';
|
||||||
|
let limit = 12;
|
||||||
|
const dt = queryCodition.endTime - queryCodition.startTime;
|
||||||
|
const hour = 60 * 60 * 1000;
|
||||||
|
const day = hour * 24;
|
||||||
|
const month = day * 30;
|
||||||
|
const year = 365 * day;
|
||||||
|
if (dt <= day) {
|
||||||
|
limit = Math.abs(Math.ceil(dt / hour));
|
||||||
|
} else if (dt > day && dt < year) {
|
||||||
|
limit = Math.abs(Math.ceil(dt / day)) + 1;
|
||||||
|
time = '1d';
|
||||||
|
format = 'M月dd日';
|
||||||
|
} else if (dt >= year) {
|
||||||
|
limit = Math.abs(Math.floor(dt / month));
|
||||||
|
time = '1M';
|
||||||
|
format = 'yyyy年-M月';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 告警趋势
|
||||||
|
const chartData = {
|
||||||
|
dashboard: 'alarm',
|
||||||
|
object: 'record',
|
||||||
|
measurement: 'trend',
|
||||||
|
dimension: 'agg',
|
||||||
|
group: 'alarmTrend',
|
||||||
|
params: {
|
||||||
|
targetType: queryCodition.targetType, // product、device、org、other
|
||||||
|
format: format,
|
||||||
|
time: time,
|
||||||
|
// from: 'now-1y', // now-1d、now-1w、now-1M、now-1y
|
||||||
|
// to: 'now',
|
||||||
|
limit: limit, // 12
|
||||||
|
// time: params.time.type === 'today' ? '1h' : '1d',
|
||||||
|
from: moment(queryCodition.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
to: moment(queryCodition.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
// limit: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 告警排名
|
||||||
|
const order = {
|
||||||
|
dashboard: 'alarm',
|
||||||
|
object: 'record',
|
||||||
|
measurement: 'rank',
|
||||||
|
dimension: 'agg',
|
||||||
|
group: 'alarmRank',
|
||||||
|
params: {
|
||||||
|
// time: '1h',
|
||||||
|
time: time,
|
||||||
|
targetType: queryCodition.targetType,
|
||||||
|
from: moment(queryCodition.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
to: moment(queryCodition.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
limit: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let tip = '其它';
|
||||||
|
if (queryCodition.targetType === 'device') {
|
||||||
|
tip = '设备';
|
||||||
|
} else if (queryCodition.targetType === 'product') {
|
||||||
|
tip = '产品';
|
||||||
|
} else if (queryCodition.targetType === 'org') {
|
||||||
|
tip = '组织';
|
||||||
|
}
|
||||||
|
// 网络请求
|
||||||
|
dashboard([chartData, order]).then((res) => {
|
||||||
|
if (res.status == 200) {
|
||||||
|
const xData: string[] = [];
|
||||||
|
const sData: number[] = [];
|
||||||
|
res.result
|
||||||
|
.filter((item: any) => item.group === 'alarmTrend')
|
||||||
|
.forEach((item: any) => {
|
||||||
|
xData.push(item.data.timeString);
|
||||||
|
sData.push(item.data.value);
|
||||||
|
});
|
||||||
|
alarmStatisticsOption.value = {
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: xData.reverse(),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
// axisPointer: {
|
||||||
|
// type: 'shadow',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: '2%',
|
||||||
|
bottom: '5%',
|
||||||
|
left: '24px',
|
||||||
|
right: '48px',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: tip,
|
||||||
|
data: sData.reverse(),
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
color: '#685DEB',
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: '#685DEB', // 100% 处的颜色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: '#FFFFFF', // 0% 处的颜色
|
||||||
|
},
|
||||||
|
],
|
||||||
|
global: false, // 缺省为 false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
state.ranking = res.result
|
||||||
|
?.filter((item: any) => item.group === 'alarmRank')
|
||||||
|
.map((d: { data: { value: any } }) => d.data?.value)
|
||||||
|
.sort(
|
||||||
|
(a: { count: number }, b: { count: number }) =>
|
||||||
|
b.count - a.count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="less">
|
||||||
|
.alarm-card {
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
padding: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.alarmBox {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
.alarmStatistics-chart {
|
||||||
|
width: 70%;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.alarmRank {
|
||||||
|
position: relative;
|
||||||
|
width: 30%;
|
||||||
|
padding-left: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rankingList {
|
||||||
|
margin: 25px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
zoom: 1;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
display: table;
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
clear: both;
|
||||||
|
height: 0;
|
||||||
|
font-size: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
//color: red;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rankingItemNumber {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 1.5px;
|
||||||
|
margin-right: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #edf0f3;
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #314659;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rankingItemTitle {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-body {
|
||||||
|
height: 490px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type Footer = {
|
||||||
|
title: string;
|
||||||
|
value: number | string;
|
||||||
|
status?: "default" | "error" | "success" | "warning" | "processing"
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<page-container>
|
<page-container>
|
||||||
<a-card>
|
<div>
|
||||||
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
|
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
|
||||||
<JTable
|
<JTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
:title="title"
|
:title="title"
|
||||||
@success="refresh"
|
@success="refresh"
|
||||||
/>
|
/>
|
||||||
</a-card>
|
</div>
|
||||||
</page-container>
|
</page-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -209,8 +209,8 @@ const columns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '说明',
|
title: '说明',
|
||||||
dataIndex: 'describe',
|
dataIndex: 'description',
|
||||||
key: 'describe',
|
key: 'description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|
|
@ -82,8 +82,8 @@ export default defineConfig(({ mode}) => {
|
||||||
// target: 'http://192.168.33.22:8800',
|
// target: 'http://192.168.33.22:8800',
|
||||||
// target: 'http://192.168.32.244:8881',
|
// target: 'http://192.168.32.244:8881',
|
||||||
// target: 'http://47.112.135.104:5096', // opcua
|
// target: 'http://47.112.135.104:5096', // opcua
|
||||||
// target: 'http://120.77.179.54:8844', // 120测试
|
target: 'http://120.77.179.54:8844', // 120测试
|
||||||
target: 'http://47.108.63.174:8845', // 测试
|
// target: 'http://47.108.63.174:8845', // 测试
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue