feat(master/device-detail && alarm): Enhance device detail page with alarm list and binary sensors integration, update iconfont URLs, and improve alarm confirmation handling

This commit is contained in:
Tran Anh Tuan
2026-01-27 20:56:54 +07:00
parent ed5751002b
commit ea07d0c99e
16 changed files with 497 additions and 104 deletions

View File

@@ -0,0 +1,105 @@
import IconFont from '@/components/IconFont';
import { StatisticCard } from '@ant-design/pro-components';
import { Flex, GlobalToken, Grid, theme } from 'antd';
type BinarySensorsProps = {
nodeConfigs: MasterModel.NodeConfig[];
};
export const getBinaryEntities = (
nodeConfigs: MasterModel.NodeConfig[] = [],
): MasterModel.Entity[] =>
nodeConfigs.flatMap((nodeConfig) =>
nodeConfig.type === 'din'
? nodeConfig.entities.filter(
(entity) => entity.type === 'bin' && entity.active === 1,
)
: [],
);
interface IconTypeResult {
iconType: string;
color: string;
}
const getIconTypeByEntity = (
entity: MasterModel.Entity,
token: GlobalToken,
): IconTypeResult => {
if (!entity.config || entity.config.length === 0) {
return {
iconType: 'icon-device-setting',
color: token.colorPrimary,
};
}
switch (entity.config[0].subType) {
case 'smoke':
return {
iconType: 'icon-fire',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'heat':
return {
iconType: 'icon-fire',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'motion':
return {
iconType: 'icon-motion',
color: entity.value === 0 ? token.colorTextBase : token.colorInfoActive,
};
case 'flood':
return {
iconType: 'icon-water-ingress',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'door':
return {
iconType: entity.value === 0 ? 'icon-door' : 'icon-door-open',
color: entity.value === 0 ? token.colorText : token.colorWarning,
};
case 'button':
return {
iconType: 'icon-alarm-button',
color: entity.value === 0 ? token.colorText : token.colorSuccess,
};
default:
return {
iconType: 'icon-door',
color: token.colorPrimary,
};
}
};
const StatisticCardItem = (entity: MasterModel.Entity, token: GlobalToken) => {
const { iconType, color } = getIconTypeByEntity(entity, token);
return (
<StatisticCard
key={entity.entityId}
statistic={{
title: entity.name,
icon: (
<IconFont type={iconType} style={{ color: color, fontSize: 24 }} />
),
value: entity.active === 1 ? 'Active' : 'Inactive',
}}
/>
);
};
const BinarySensors = ({ nodeConfigs }: BinarySensorsProps) => {
const binarySensors = getBinaryEntities(nodeConfigs);
console.log('BinarySensor: ', binarySensors);
const { token } = theme.useToken();
const { useBreakpoint } = Grid;
const screens = useBreakpoint();
return (
<Flex wrap="wrap">
<StatisticCard.Group direction={screens.sm ? 'row' : 'column'}>
{binarySensors.map((entity) => StatisticCardItem(entity, token))}
</StatisticCard.Group>
</Flex>
);
};
export default BinarySensors;

View File

@@ -1,10 +1,13 @@
import DeviceAlarmList from '@/components/shared/DeviceAlarmList';
import TooltipIconFontButton from '@/components/shared/TooltipIconFontButton';
import { ROUTER_HOME } from '@/constants/routes';
import { apiQueryMessage } from '@/services/master/MessageController';
import { apiQueryNodeConfigMessage } from '@/services/master/MessageController';
import { apiGetThingDetail } from '@/services/master/ThingController';
import { PageContainer } from '@ant-design/pro-components';
import { history, useModel, useParams } from '@umijs/max';
import { PageContainer, ProCard } from '@ant-design/pro-components';
import { history, useIntl, useModel, useParams } from '@umijs/max';
import { Grid } from 'antd';
import { useEffect, useState } from 'react';
import BinarySensors from './components/BinarySensors';
import ThingTitle from './components/ThingTitle';
const DetailDevicePage = () => {
@@ -12,6 +15,10 @@ const DetailDevicePage = () => {
const [thing, setThing] = useState<MasterModel.Thing | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { initialState } = useModel('@@initialState');
const { useBreakpoint } = Grid;
const screens = useBreakpoint();
const intl = useIntl();
const [nodeConfigs, setNodeConfigs] = useState<MasterModel.NodeConfig[]>([]);
const getThingDetail = async () => {
setIsLoading(true);
try {
@@ -25,7 +32,7 @@ const DetailDevicePage = () => {
};
const getDeviceConfig = async () => {
try {
const resp = await apiQueryMessage(
const resp = await apiQueryNodeConfigMessage(
thing?.metadata?.data_channel_id || '',
initialState?.currentUserProfile?.metadata?.frontend_thing_key || '',
{
@@ -34,7 +41,11 @@ const DetailDevicePage = () => {
subtopic: `config.${thing?.metadata?.type}.node`,
},
);
console.log('Device Config:', resp.messages![0].string_value_parsed);
if (resp.messages && resp.messages.length > 0) {
console.log('Node Configs: ', resp.messages[0].string_value_parsed);
setNodeConfigs(resp.messages[0].string_value_parsed ?? []);
}
} catch (error) {}
};
useEffect(() => {
@@ -82,7 +93,21 @@ const DetailDevicePage = () => {
/>,
]}
>
Thing ID: {thingId}
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
<ProCard
title={intl.formatMessage({
id: 'master.thing.detail.alarmList.title',
})}
colSpan={{ xs: 24, sm: 24, md: 24, lg: 6, xl: 6 }}
bodyStyle={{ paddingInline: 0, paddingBlock: 0 }}
bordered
>
<DeviceAlarmList key="thing-alarms-key" thingId={thingId || ''} />
</ProCard>
<ProCard>
<BinarySensors nodeConfigs={nodeConfigs} />
</ProCard>
</ProCard>
</PageContainer>
);
};