Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
wangshuaiswim 2023-02-16 16:23:07 +08:00
commit 7daa9df6a6
16 changed files with 1440 additions and 8 deletions

13
src/api/media/device.ts Normal file
View File

@ -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}`),
}

View File

@ -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');

View File

@ -45,7 +45,10 @@ const iconKeys = [
'InfoCircleOutlined',
'SearchOutlined',
'EllipsisOutlined',
'ClockCircleOutlined'
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'playCircleOutlined',
]
const Icon = (props: {type: string}) => {

View File

@ -40,7 +40,7 @@
<Charts :options="TodayDevOptions"></Charts> </TopCard
></a-col>
</a-row>
<a-row :span="24">
<a-row :gutter="24">
<a-col :span="24">
<div class="message-card">
<Guide title="设备消息">
@ -452,6 +452,7 @@ const getEcharts = (data: any) => {
_time = '1M';
format = 'yyyy年-M月';
}
dashboard([
{
dashboard: 'device',

View File

@ -0,0 +1,7 @@
<template>
<div class="page-container">save</div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -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>

24
src/views/media/Device/typings.d.ts vendored Normal file
View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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, // productdeviceorgother
format: format,
time: time,
// from: 'now-1y', // now-1dnow-1wnow-1Mnow-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>

View File

@ -0,0 +1,5 @@
export type Footer = {
title: string;
value: number | string;
status?: "default" | "error" | "success" | "warning" | "processing"
}

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<a-card>
<div>
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
<JTable
:columns="columns"
@ -147,7 +147,7 @@
:title="title"
@success="refresh"
/>
</a-card>
</div>
</page-container>
</template>
@ -209,8 +209,8 @@ const columns = [
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
dataIndex: 'description',
key: 'description',
},
{
title: '操作',

View File

@ -82,8 +82,8 @@ export default defineConfig(({ mode}) => {
// target: 'http://192.168.33.22:8800',
// target: 'http://192.168.32.244:8881',
// target: 'http://47.112.135.104:5096', // opcua
// target: 'http://120.77.179.54:8844', // 120测试
target: 'http://47.108.63.174:8845', // 测试
target: 'http://120.77.179.54:8844', // 120测试
// target: 'http://47.108.63.174:8845', // 测试
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}