feat(users): add reset password functionality for users and implement forgot password page

This commit is contained in:
Tran Anh Tuan
2026-02-03 17:33:47 +07:00
parent 9bc15192ec
commit dca363275e
35 changed files with 1592 additions and 321 deletions

View File

@@ -1,7 +1,15 @@
import IconFont from '@/components/IconFont';
import { StatisticCard } from '@ant-design/pro-components';
import { Flex, GlobalToken, Grid, theme } from 'antd';
import {
Divider,
Flex,
GlobalToken,
Grid,
theme,
Tooltip,
Typography,
} from 'antd';
const { Text } = Typography;
type BinarySensorsProps = {
nodeConfigs: MasterModel.NodeConfig[];
};
@@ -19,6 +27,7 @@ export const getBinaryEntities = (
interface IconTypeResult {
iconType: string;
color: string;
name?: string;
}
const getIconTypeByEntity = (
@@ -27,60 +36,103 @@ const getIconTypeByEntity = (
): IconTypeResult => {
if (!entity.config || entity.config.length === 0) {
return {
iconType: 'icon-device-setting',
iconType: 'icon-not-found',
color: token.colorPrimary,
};
}
switch (entity.config[0].subType) {
case 'smoke':
return {
iconType: 'icon-fire',
iconType: 'icon-smoke1',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
name: entity.value === 0 ? 'Bình thường' : 'Phát hiện',
};
case 'heat':
return {
iconType: 'icon-fire',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
name: entity.value === 0 ? 'Bình thường' : 'Phát hiện',
};
case 'motion':
return {
iconType: 'icon-motion',
color: entity.value === 0 ? token.colorTextBase : token.colorInfoActive,
name: entity.value === 0 ? 'Không' : 'Phát hiện',
};
case 'flood':
return {
iconType: 'icon-water-ingress',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
name: entity.value === 0 ? 'Không' : 'Phát hiện',
};
case 'door':
return {
iconType: entity.value === 0 ? 'icon-door' : 'icon-door-open',
iconType: entity.value === 0 ? 'icon-door' : 'icon-door-open1',
color: entity.value === 0 ? token.colorText : token.colorWarning,
name: entity.value === 0 ? 'Đóng' : 'Mở',
};
case 'button':
return {
iconType: 'icon-alarm-button',
color: entity.value === 0 ? token.colorText : token.colorSuccess,
name: entity.value === 0 ? 'Tắt' : 'Bật',
};
default:
return {
iconType: 'icon-door',
iconType: 'icon-not-found',
color: token.colorPrimary,
};
}
};
const StatisticCardItem = (entity: MasterModel.Entity, token: GlobalToken) => {
const { iconType, color } = getIconTypeByEntity(entity, token);
const { iconType, color, name } = getIconTypeByEntity(entity, token);
return (
<StatisticCard
bordered={false}
key={entity.entityId}
style={{
borderRadius: 8,
background: token.colorBgContainer,
border: `1px solid ${token.colorBorder}`,
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.boxShadow = `0 4px 12px ${token.colorPrimary}20`;
el.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.boxShadow = 'none';
el.style.transform = 'translateY(0)';
}}
statistic={{
title: entity.name,
icon: (
<IconFont type={iconType} style={{ color: color, fontSize: 24 }} />
<Tooltip title={entity.name}>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 56,
marginBottom: 8,
}}
>
<IconFont type={iconType} style={{ color, fontSize: 32 }} />
</div>
</Tooltip>
),
value: entity.active === 1 ? 'Active' : 'Inactive',
title: (
<Text
ellipsis={{ tooltip: entity.name }}
style={{ fontSize: 13, fontWeight: 500 }}
>
{entity.name}
</Text>
),
value: name,
valueStyle: { fontSize: 12, color, fontWeight: 600, marginTop: 8 },
}}
/>
);
@@ -93,11 +145,20 @@ const BinarySensors = ({ nodeConfigs }: BinarySensorsProps) => {
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 wrap="wrap" gap="middle">
<Divider orientation="left">Cảm biến</Divider>
{binarySensors.map((entity) => (
<div
key={entity.entityId}
style={{
width: 'fit-content',
}}
>
{StatisticCardItem(entity, token)}
</div>
))}
</Flex>
);
};

View File

@@ -5,7 +5,7 @@ import { apiQueryNodeConfigMessage } from '@/services/master/MessageController';
import { apiGetThingDetail } from '@/services/master/ThingController';
import { PageContainer, ProCard } from '@ant-design/pro-components';
import { history, useIntl, useModel, useParams } from '@umijs/max';
import { Grid } from 'antd';
import { Divider, Flex, Grid } from 'antd';
import { useEffect, useState } from 'react';
import BinarySensors from './components/BinarySensors';
import ThingTitle from './components/ThingTitle';
@@ -95,17 +95,23 @@ const DetailDevicePage = () => {
>
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
<ProCard
bodyStyle={{
paddingInline: 12,
paddingBlock: 8,
}}
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 colSpan={{ xs: 24, sm: 24, md: 24, lg: 18, xl: 18 }}>
<Flex wrap gap="small">
<BinarySensors nodeConfigs={nodeConfigs} />
<Divider orientation="left">Trạng thái</Divider>
</Flex>
</ProCard>
</ProCard>
</PageContainer>