613 lines
25 KiB
TypeScript
613 lines
25 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Switch,
|
|
Button,
|
|
Row,
|
|
Col,
|
|
Card as AntdCard,
|
|
Tag,
|
|
Table,
|
|
message,
|
|
Typography,
|
|
Space,
|
|
TimePicker,
|
|
Divider,
|
|
} from 'antd';
|
|
import {
|
|
CloudOutlined,
|
|
TeamOutlined,
|
|
BellOutlined,
|
|
DatabaseOutlined,
|
|
CalendarOutlined,
|
|
CheckCircleOutlined,
|
|
DownloadOutlined,
|
|
FileTextOutlined,
|
|
DeleteOutlined,
|
|
RestOutlined,
|
|
SettingOutlined,
|
|
UserOutlined,
|
|
NotificationOutlined,
|
|
ApiOutlined,
|
|
SafetyCertificateOutlined,
|
|
ToolOutlined,
|
|
RightOutlined,
|
|
EyeOutlined,
|
|
} from '@ant-design/icons';
|
|
import {
|
|
Smartphone,
|
|
Mail,
|
|
Send,
|
|
MessageSquare,
|
|
ShieldCheck,
|
|
Users,
|
|
LineChart,
|
|
Wrench,
|
|
Cpu,
|
|
Camera,
|
|
Wind,
|
|
Box,
|
|
Activity,
|
|
Zap,
|
|
RefreshCw,
|
|
Shield,
|
|
Key,
|
|
Lock,
|
|
} from 'lucide-react';
|
|
import dayjs from 'dayjs';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { getRoleList } from '@/api/role';
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
const BasicSettings = () => {
|
|
const { t } = useTranslation();
|
|
const [form] = Form.useForm();
|
|
const [roles, setRoles] = useState<any[]>([]);
|
|
|
|
// 响应式字体大小
|
|
const fontNormal = 'clamp(12px, 0.75vw, 13px)';
|
|
const fontSmall = 'clamp(11px, 0.7vw, 12px)';
|
|
const fontTitle = 'clamp(14px, 0.85vw, 16px)';
|
|
const fontLarge = 'clamp(18px, 1.1vw, 22px)';
|
|
|
|
const handleSave = () => {
|
|
message.success(t('systemSetting.fixed2SettingsSaveFixed2'));
|
|
};
|
|
|
|
// 顶部统计卡片数据
|
|
const topCards = [
|
|
{
|
|
title: t('systemSetting.fixed2Current'),
|
|
value: 'v2.8.1',
|
|
tag: '最新版',
|
|
tagColor: 'blue',
|
|
icon: <Cpu size={24} color="#1677ff" />,
|
|
bg: 'linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%)'
|
|
},
|
|
{
|
|
title: t('systemSetting.onlineUsers'),
|
|
value: '12',
|
|
trend: '↑ 2',
|
|
trendColor: '#52c41a',
|
|
icon: <Users size={24} color="#52c41a" />,
|
|
bg: 'linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%)'
|
|
},
|
|
{
|
|
title: t('systemSetting.fixed2AlertFixed2Fixed22'),
|
|
value: '3 / 4',
|
|
trend: t('roles.normal'),
|
|
trendColor: '#52c41a',
|
|
icon: <BellOutlined style={{ fontSize: 24, color: '#ff4d4f' }} />,
|
|
bg: 'linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%)'
|
|
},
|
|
{
|
|
title: t('systemSetting.statusFixed2'),
|
|
value: t('roles.normal'),
|
|
trend: '●',
|
|
trendColor: '#52c41a',
|
|
icon: <DatabaseOutlined style={{ fontSize: 24, color: '#13c2c2' }} />,
|
|
bg: 'linear-gradient(135deg, #e6fffb 0%, #b5f5ec 100%)'
|
|
},
|
|
{
|
|
title: t('systemSetting.s282'),
|
|
value: '2025-05-20',
|
|
icon: <CalendarOutlined style={{ fontSize: 24, color: '#722ed1' }} />,
|
|
bg: 'linear-gradient(135deg, #f9f0ff 0%, #efdbff 100%)'
|
|
},
|
|
];
|
|
|
|
// 角色权限概览
|
|
const roleOverview = [
|
|
//平台管理员
|
|
{
|
|
title: t('roles.superAdmin'),
|
|
users: 2,
|
|
perms: 28,
|
|
tags: ['全部权限', '系统管理'],
|
|
color: '#1677ff',
|
|
bg: '#e6f4ff',
|
|
icon: <ShieldCheck size={20} color="#1677ff" />
|
|
},
|
|
//普通用户
|
|
{
|
|
title: t('systemSetting.fixed2OM'),
|
|
users: 5,
|
|
perms: 18,
|
|
tags: ["处理工单","处理任务"],
|
|
color: '#52c41a',
|
|
bg: '#f6ffed',
|
|
icon: <Wrench size={20} color="#52c41a" />
|
|
},
|
|
//机构管理员
|
|
{
|
|
title: t('systemSetting.fixed2Inspection2'),
|
|
users: 8,
|
|
perms: 12,
|
|
tags: ['管理机构下所有场站', '告警处理'],
|
|
color: '#fa8c16',
|
|
bg: '#fff7e6',
|
|
icon: <Users size={20} color="#fa8c16" />
|
|
},
|
|
//场站管理员
|
|
{
|
|
title: t('systemSetting.s322'),
|
|
users: 3,
|
|
perms: 15,
|
|
tags: ['管理自己场站', '报表查看','告警处理'],
|
|
color: '#722ed1',
|
|
bg: '#f9f0ff',
|
|
icon: <LineChart size={20} color="#722ed1" />
|
|
},
|
|
];
|
|
const matchRoleStyle = (roleName: string) => {
|
|
const found = roleOverview.find(item => item.title === roleName);
|
|
if (found) return found;
|
|
// 兜底默认样式
|
|
return {
|
|
color: '#8c8c8c',
|
|
bg: '#f5f5f5',
|
|
icon: null,
|
|
tags: []
|
|
};
|
|
};
|
|
|
|
// 设备状态
|
|
const deviceStatuses = [
|
|
{ name: t('home.inverter'), count: '5/5', status: t('roles.normal') },
|
|
{ name: t('home.combinerBox'), count: '20/20', status: t('roles.normal') },
|
|
{ name: t('home.WeatherStation'), count: '1/1', status: t('roles.normal') },
|
|
{ name: t('devices.camera'), count: '12/12', status: t('roles.normal') },
|
|
{ name: t('constants.uav'), count: '2/2', status: t('roles.normal') },
|
|
{ name: t('devices.robot'), count: '5/5', status: t('roles.normal') },
|
|
{ name: t('systemSetting.api'), count: t('roles.normal'), status: t('roles.normal') },
|
|
{ name: 'Modbus', count: t('roles.normal'), status: t('roles.normal') },
|
|
];
|
|
const getRoleData = () => {
|
|
const params = { pageNum: 1, pageSize: 9999 };
|
|
getRoleList(params).then(res => {
|
|
if (res.code === 200) {
|
|
let sortedRoles = (res.data || []).sort((a, b) => (a.roleSort || 999) - (b.roleSort || 999));
|
|
setRoles(sortedRoles);
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
getRoleData();
|
|
}, [])
|
|
|
|
|
|
return (
|
|
<div className="normal-spacing" style={{ padding: '8px 0px', background: '#f5f7fa', minHeight: '100vh' }}>
|
|
<div style={{ padding: '0px 0px 8px' }}>
|
|
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('systemSetting.basicSettings')}</Typography.Title>
|
|
</div>
|
|
|
|
|
|
{/* 顶部统计卡片 */}
|
|
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
|
|
{topCards.map((item, idx) => (
|
|
<Col key={idx} style={{ minWidth: '168px', flex: '1 0 auto', maxWidth: "250px" }}>
|
|
<AntdCard bordered={false} bodyStyle={{ padding: '12px 16px' }} style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
{/*<div style={{ width: 40, height: 40, borderRadius: 10, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
{React.cloneElement(item.icon as React.ReactElement, { size: 20, style: item.icon.props.style ? { ...item.icon.props.style, fontSize: 20 } : { fontSize: 20 } })}
|
|
</div>*/}
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ color: '#8c8c8c', fontSize: fontSmall, marginBottom: 2 }}>{item.title}</div>
|
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
|
<span style={{ fontSize: 'clamp(16px, 1vw, 20px)', fontWeight: 700, color: '#262626' }}>{item.value}</span>
|
|
{item.tag && <Tag color={item.tagColor} style={{ fontSize: 9, margin: 0, borderRadius: 4, padding: '0 4px' }}>{item.tag}</Tag>}
|
|
{item.trend && <span style={{ color: item.trendColor, fontSize: fontSmall, fontWeight: 600 }}>{item.trend}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AntdCard>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
|
|
<Row gutter={[8, 8]}>
|
|
{/* 左侧 */}
|
|
<Col xs={24} xl={17}>
|
|
{/* 基础配置 */}
|
|
<AntdCard
|
|
title={<Space><SettingOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.basicSettings')}</b></Space>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '16px' }}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Row gutter={16}>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.systemName')} name="systemName" style={{ marginBottom: 12 }}>
|
|
<Input size="small" defaultValue={t('login.systemName')} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.fixed2Default2')} name="defaultSite" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue={t('layout.defaultStationName')}>
|
|
<Select.Option value="1MW">{t('layout.defaultStationName')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.defaultLanguage')} name="defaultLang" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue="zh">
|
|
<Select.Option value="zh">{t('users.chinese')}</Select.Option>
|
|
<Select.Option value="en">English</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.timezone')} name="timezone" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue="UTC+08:00">
|
|
<Select.Option value="8">UTC+08:00</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.default')} name="defaultPage" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue="dashboard">
|
|
<Select.Option value="dashboard">{t('router.overview')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.refreshFixed2FrequencyFixed2')} name="refreshRate" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue="5min">
|
|
<Select.Option value="5min">{t('systemSetting.5')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Form.Item label={t('systemSetting.s232')} name="mapService" style={{ marginBottom: 12 }}>
|
|
<Select size="small" defaultValue="gaode">
|
|
<Select.Option value="gaode">{t('systemSetting.s24')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col xs={24} md={16}>
|
|
<div style={{ display: 'flex', gap: 'clamp(8px, 1vw, 20px)', marginTop: 4, flexWrap: 'wrap' }}>
|
|
<Space>
|
|
<span style={{ fontSize: fontSmall }}>{t('systemSetting.s34')}</span>
|
|
<Switch size="small" defaultChecked />
|
|
</Space>
|
|
<Space>
|
|
<span style={{ fontSize: fontSmall }}>{t('systemSetting.fixed2Ai')}</span>
|
|
<Switch size="small" defaultChecked />
|
|
</Space>
|
|
<Space>
|
|
<span style={{ fontSize: fontSmall }}>{t('systemSetting.s52')}</span>
|
|
<Switch size="small" defaultChecked />
|
|
</Space>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
<div style={{ marginTop: 12 }}>
|
|
<Button type="primary" size="small" onClick={handleSave} style={{ width: 100, borderRadius: 6 }}>{t('systemSetting.fixed2SaveFixed2SettingsFixed2')}</Button>
|
|
<Button size="small" style={{ marginLeft: 8, borderRadius: 6 }}>{t('systemSetting.fixed2ResumeFixed2DefaultFixed2')}</Button>
|
|
</div>
|
|
</Form>
|
|
</AntdCard>
|
|
|
|
{/* 通知与告警策略 */}
|
|
<AntdCard
|
|
title={<Space><BellOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.alert2')}</b></Space>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '16px' }}
|
|
>
|
|
<Row gutter={[12, 12]} style={{ marginBottom: 8 }}>
|
|
{[
|
|
{ label: t('alerts.sms'), icon: <Smartphone size={16} />, color: '#52c41a', status: `${t('alerts.enabled')}` },
|
|
{ label: t('alerts.email'), icon: <Mail size={16} />, color: '#1677ff', status: `${t('alerts.enabled')}` },
|
|
{ label: 'Telegram', icon: <Send size={16} />, color: '#13c2c2', status: `${t('alerts.enabled')}` },
|
|
{ label: t('report.systemNotification'), icon: <BellOutlined style={{ fontSize: 16 }} />, color: '#fa8c16', status: `${t('alerts.enabled')}` },
|
|
].map((item, idx) => (
|
|
<Col xs={24} sm={12} lg={6} key={idx}>
|
|
<div style={{ display: 'flex', alignItems: 'center', padding: '10px 12px', background: '#f8fafc', borderRadius: 8, border: '1px solid #f1f5f9' }}>
|
|
<div style={{ width: 32, height: 32, borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 1px 4px rgba(0,0,0,0.04)', marginRight: 10 }}>
|
|
{/*{React.cloneElement(item.icon as React.ReactElement, { style: { color: item.color } })}*/}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: 10, color: '#8c8c8c' }}>{item.label}</div>
|
|
<div style={{ fontSize: 10, color: '#52c41a' }}>{item.status}</div>
|
|
</div>
|
|
<Switch size="small" defaultChecked />
|
|
</div>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
<Row gutter={[24, 12]}>
|
|
<Col xs={24} sm={12} md={8}><Space size={8}><span style={{ fontSize: fontSmall }}>{t('systemSetting.fixed2CriticalAlert')}</span><Switch size="small" defaultChecked /></Space></Col>
|
|
<Col xs={24} sm={12} md={8}><Space size={8}><span style={{ fontSize: fontSmall }}>{t('systemSetting.fixed2GenerationFixed2Descend15')}</span><Switch size="small" defaultChecked /></Space></Col>
|
|
<Col xs={24} sm={12} md={8}><Space size={8}><span style={{ fontSize: fontSmall }}>{t('systemSetting.fixed2Communicating')}</span><Switch size="small" defaultChecked /></Space></Col>
|
|
<Col xs={24} sm={12} md={8}><Space size={8}><span style={{ fontSize: fontSmall }}>{t('systemSetting.everydayPush')}</span><Switch size="small" defaultChecked /></Space></Col>
|
|
<Col xs={24} md={16}>
|
|
<Space size="middle" style={{ flexWrap: 'wrap' }}>
|
|
<span style={{ fontSize: fontSmall }}>{t('systemSetting.Nighttime')}</span>
|
|
<TimePicker.RangePicker size="small" defaultValue={[dayjs('23:00', 'HH:mm'), dayjs('07:00', 'HH:mm')]} format="HH:mm" style={{ width: 140 }} />
|
|
<Switch size="small" />
|
|
</Space>
|
|
</Col>
|
|
</Row>
|
|
</AntdCard>
|
|
|
|
{/* 用户与权限预览 */}
|
|
<AntdCard
|
|
title={<Space><Users size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2RolePermissionFixed2OverviewFixed2')}</b></Space>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)', marginBottom: 0 }}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<Table
|
|
size="small"
|
|
scroll={{ x: 'max-content' }}
|
|
rowStyle={{ height: '32px', padding: 0 }}
|
|
empty={false}
|
|
|
|
|
|
pagination={{ size: 'small', total: 4, pageSize: 4, showTotal: (total) => `共 ${total} 条` }}
|
|
columns={[
|
|
{ title: t('systemSetting.s36'), dataIndex: 'name', key: 'name', fixed: 'left', width: 80 },
|
|
{
|
|
title: t('users.role'),
|
|
dataIndex: 'role',
|
|
key: 'role',
|
|
width: 100,
|
|
render: (role) => {
|
|
const config: any = {
|
|
'超级管理员': { color: '#1677ff', bg: '#e6f4ff' },
|
|
'运维工程师': { color: '#52c41a', bg: '#f6ffed' },
|
|
'巡检主管': { color: '#fa8c16', bg: '#fff7e6' },
|
|
'数据分析师': { color: '#722ed1', bg: '#f9f0ff' },
|
|
};
|
|
const s = config[role] || { color: '#8c8c8c', bg: '#f5f5f5' };
|
|
return <Tag bordered={false} style={{ color: s.color, background: s.bg, fontSize: 10, margin: 0 }}>{role}</Tag>;
|
|
}
|
|
},
|
|
{ title: t('users.phone'), dataIndex: 'phone', key: 'phone', width: 110 },
|
|
{ title: t('users.email'), dataIndex: 'email', key: 'email', ellipsis: true },
|
|
{
|
|
title: t('roles.status'),
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 70,
|
|
render: (status) => (
|
|
<Space size={4}>
|
|
<div style={{ width: 6, height: 6, borderRadius: '50%', background: status === t('devices.online') ? '#52c41a' : '#bfbfbf' }} />
|
|
<span style={{ fontSize: 10 }}>{status}</span>
|
|
</Space>
|
|
)
|
|
},
|
|
{
|
|
title: t('roles.actions'), key: 'action', width: 60, render: () => <Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
style={{ padding: 0, fontSize: 11 }}
|
|
>{t('common.view')}</Button>, fixed: 'right'
|
|
},
|
|
]}
|
|
dataSource={[
|
|
{ name: '张运维', role: '超级管理员', phone: '138****0001', email: 'zhangyw@pvsmart.com', status: t('devices.online') },
|
|
{ name: '李工程师', role: '运维工程师', phone: '139****0002', email: 'li.gongcheng@pvsmart.com', status: t('devices.online') },
|
|
{ name: '王班长', role: '巡检主管', phone: '137****0003', email: 'wang.banzhang@pvsmart.com', status: t('devices.online') },
|
|
{ name: '赵分析师', role: '数据分析师', phone: '136****0004', email: 'zhao.fx@pvsmart.com', status: t('devices.online') },
|
|
]}
|
|
/>
|
|
</AntdCard>
|
|
</Col>
|
|
|
|
{/* 右侧 */}
|
|
<Col xs={24} xl={7}>
|
|
{/* 角色权限概览 */}
|
|
<AntdCard
|
|
title={<Space><Users size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2RolePermissionFixed2OverviewFixed2')}</b></Space>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '10px' }}
|
|
>
|
|
<Row gutter={[8, 8]}>
|
|
{roles.map((role, idx) => {
|
|
const styleConf = matchRoleStyle(role.roleName);
|
|
return (
|
|
<Col xs={24} sm={12} key={idx}>
|
|
<div style={{ background: styleConf.bg, borderRadius: 8, padding: 8, height: '100%' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, alignItems: 'center' }}>
|
|
{styleConf.icon}
|
|
</div>
|
|
<div style={{ fontWeight: 600, fontSize: 11, color: styleConf.color, marginBottom: 2 }}>
|
|
{role.roleName}
|
|
</div>
|
|
<div style={{ fontSize: 9, color: '#8c8c8c', marginBottom: 6 }}>
|
|
用户 {role.users} | 菜单权限 {role.menuIds?.length ?? 0}
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
{styleConf.tags.map((tag, tidx) => (
|
|
<span
|
|
key={tidx}
|
|
style={{
|
|
fontSize: 8,
|
|
padding: '0 4px',
|
|
background: '#fff',
|
|
color: styleConf.color,
|
|
borderRadius: 2,
|
|
border: `1px solid ${styleConf.color}20`
|
|
}}
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Col>
|
|
);
|
|
})}
|
|
</Row>
|
|
</AntdCard>
|
|
|
|
{/* 设备接入状态 */}
|
|
<AntdCard
|
|
title={<Space><ApiOutlined style={{ color: '#1677ff' }} /><b>{t('systemSetting.fixed2DeviceAccessFixed2StatusFixed2')}</b></Space>}
|
|
extra={<Button type="link" size="small" style={{ fontSize: 11, padding: 0 }}>{t('common.detail')}<RightOutlined style={{ fontSize: 9 }} /></Button>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<div style={{ flex: '0 0 90px' }}>
|
|
{deviceStatuses.map((item, idx) => (
|
|
<div key={idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
|
<Space size={4}>
|
|
<div style={{ width: 5, height: 5, borderRadius: '50%', background: '#52c41a' }} />
|
|
<span style={{ fontSize: 10, color: '#595959' }}>{item.name}</span>
|
|
</Space>
|
|
<span style={{ fontSize: 10, fontWeight: 500 }}>{item.count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div style={{ flex: 1, position: 'relative', height: 130, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(22, 119, 255, 0.02)', borderRadius: 10 }}>
|
|
<svg width="100%" height="100%" viewBox="0 0 200 160" style={{ maxWidth: 180 }}>
|
|
<defs>
|
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
</filter>
|
|
</defs>
|
|
<g stroke="#1677ff30" strokeWidth="0.8" strokeDasharray="3 2">
|
|
<line x1="100" y1="80" x2="60" y2="40" />
|
|
<line x1="100" y1="80" x2="140" y2="40" />
|
|
<line x1="100" y1="80" x2="160" y2="80" />
|
|
<line x1="100" y1="80" x2="140" y2="120" />
|
|
<line x1="100" y1="80" x2="60" y2="120" />
|
|
<line x1="100" y1="80" x2="40" y2="80" />
|
|
</g>
|
|
<circle cx="100" cy="80" r="14" fill="#1677ff" filter="url(#glow)" />
|
|
<rect x="93" y="73" width="14" height="14" rx="2" fill="none" stroke="#fff" strokeWidth="1.2" />
|
|
<path d="M96 76h8M96 80h8M96 84h8" stroke="#fff" strokeWidth="0.8" />
|
|
{[
|
|
{ x: 60, y: 40, icon: <Cpu size={10} />, label: t('home.inverter') },
|
|
{ x: 140, y: 40, icon: <Box size={10} />, label: t('home.combinerBox') },
|
|
{ x: 160, y: 80, icon: <Wind size={10} />, label: t('video.WeatherStation') },
|
|
{ x: 140, y: 120, icon: <Camera size={10} />, label: t('devices.camera') },
|
|
{ x: 60, y: 120, icon: <Activity size={10} />, label: t('devices.robot') },
|
|
{ x: 40, y: 80, icon: <Zap size={10} />, label: t('systemSetting.s38') },
|
|
].map((node, i) => (
|
|
<g key={i}>
|
|
<circle cx={node.x} cy={node.y} r="10" fill="#fff" stroke="#e8e8e8" strokeWidth="0.8" />
|
|
<foreignObject x={node.x - 5} y={node.y - 5} width="10" height="10">
|
|
<div style={{ color: '#1677ff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{node.icon}</div>
|
|
</foreignObject>
|
|
<text x={node.x} y={node.y + 16} textAnchor="middle" style={{ fontSize: 7, fill: '#8c8c8c' }}>{node.label}</text>
|
|
</g>
|
|
))}
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</AntdCard>
|
|
|
|
{/* 数据与安全 */}
|
|
<AntdCard
|
|
title={<Space><Shield size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.dataSafety')}</b></Space>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '10px 16px' }}>
|
|
{[
|
|
{ label: t('systemSetting.s39'), value: '3 年', icon: <DatabaseOutlined style={{ color: '#1677ff' }} /> },
|
|
{ label: t('systemSetting.fixed2Login2'), value: '已开启', icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />, color: '#52c41a' },
|
|
{ label: t('systemSetting.s412'), value: '02:00', icon: <RefreshCw size={12} color="#1677ff" /> },
|
|
{ label: t('systemSetting.api'), value: t('roles.normal'), icon: <Key size={12} color="#52c41a" />, color: '#52c41a' },
|
|
{ label: t('systemSetting.s43'), value: '2025-05-24 02:00', icon: <CalendarOutlined style={{ color: '#1677ff' }} /> },
|
|
{ label: t('systemSetting.ssl'), value: '有效期至 2026-12-31', icon: <SafetyCertificateOutlined style={{ color: '#fa8c16' }} /> },
|
|
{ label: t('systemSetting.s45'), value: '已开启', icon: <Lock size={12} color="#52c41a" />, color: '#52c41a' },
|
|
].map((item, idx) => (
|
|
<div key={idx} style={{ gridColumn: item.isFull ? '1 / -1' : 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<div style={{ width: 20, height: 20, borderRadius: 5, background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
{React.isValidElement(item.icon) ? React.cloneElement(item.icon as React.ReactElement, { size: 12, style: { ...item.icon.props.style, fontSize: 12 } }) : item.icon}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: 9, color: '#8c8c8c' }}>{item.label}</div>
|
|
<div style={{ fontSize: 10, fontWeight: 500, color: item.color || '#262626' }}>{item.value}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AntdCard>
|
|
|
|
{/* 系统维护 */}
|
|
<AntdCard
|
|
title={<Space><Wrench size={16} style={{ color: '#1677ff' }} /><b>{t('systemSetting.systemMaintenance')}</b></Space>}
|
|
extra={<Button type="link" size="small" danger style={{ fontSize: 10, padding: 0 }}>{t('systemSetting.s192')}</Button>}
|
|
bordered={false}
|
|
style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<Row gutter={[8, 8]}>
|
|
{[
|
|
{ label: t('systemSetting.s46'), icon: <DeleteOutlined />, color: '#1677ff' },
|
|
{ label: t('systemSetting.s47'), icon: <RefreshCw size={16} />, color: '#1677ff' },
|
|
{ label: t('systemSetting.downloadFixed2'), icon: <DownloadOutlined />, color: '#1677ff' },
|
|
{ label: t('systemSetting.fixed2LogDesc6Fixed2ExportFixed2'), icon: <FileTextOutlined />, color: '#1677ff' },
|
|
].map((btn, idx) => (
|
|
<Col xs={12} sm={6} key={idx}>
|
|
<Button
|
|
size="small"
|
|
style={{
|
|
width: '100%',
|
|
height: 52,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: 8,
|
|
gap: 2,
|
|
border: '1px solid #f0f0f0',
|
|
background: '#fff',
|
|
padding: '2px'
|
|
}}
|
|
>
|
|
<div style={{ color: btn.color, fontSize: 16 }}>
|
|
{React.isValidElement(btn.icon) ? React.cloneElement(btn.icon as React.ReactElement, { size: 16, style: { fontSize: 16 } }) : btn.icon}
|
|
</div>
|
|
<div style={{ fontSize: 10, color: '#595959' }}>{btn.label}</div>
|
|
</Button>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</AntdCard>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BasicSettings;
|
|
|