个人中心页面

This commit is contained in:
mmc
2026-05-13 11:12:17 +08:00
parent d349903ed6
commit 38ceb37926
6 changed files with 568 additions and 84 deletions

View File

@@ -45,6 +45,48 @@ export function getUserByRole(params) {
});
}
// 查询个人信息
export function getUserProfile() {
return request({
url: '/system/user/profile',
method: 'get'
});
}
// 修改个人信息
export function updateUserProfile(data) {
return request({
url: '/system/user/profile',
method: 'put',
data
});
}
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
};
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
params: data
});
}
// 用户头像上传
export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import {
Layout, Menu, Button, Space, ConfigProvider, theme, Select
Layout, Menu, Button, Space, ConfigProvider, theme, Select, Avatar
} from 'antd';
import zhCN from 'antd/locale/zh_CN';
import {
@@ -16,6 +16,7 @@ import { logout } from '../store/userSlice';
import { setCurrentStation, clearStation } from '../store/stationSlice'; // 👈 引入
import { getMenuList } from '../api/mune';
import { getStationList } from '../api/stationManage';
import envConfig from '../../env';
const { Header, Sider, Content } = Layout;
@@ -156,7 +157,19 @@ export default function AppLayout() {
<Space size="middle">
<span style={{ fontSize: 13 }}>{time}</span>
<span>{userInfo?.username}</span>
<Space
style={{ cursor: 'pointer', padding: '0 4px' }}
onClick={() => navigate('/profile')}
className="hover:opacity-80"
>
{userInfo?.avatar && (
<Avatar
size="small"
src={userInfo.avatar.startsWith('http') ? userInfo.avatar : `${envConfig.baseURL}${userInfo.avatar}`}
/>
)}
<span style={{ fontSize: 14 }}>{userInfo?.nickName || userInfo?.username}</span>
</Space>
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout}>
退出登录
</Button>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import {
Card, Row, Col, Select, DatePicker, Input, Button, Space, Table, Tag,
Typography, message, Pagination, Spin, Drawer, Descriptions, Switch
Typography, message, Pagination, Spin, Drawer, Descriptions, Switch, Form
} from 'antd';
import {
BellOutlined, ClockCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined,
@@ -9,6 +9,7 @@ import {
} from '@ant-design/icons';
import { Line, Pie, Bar } from '@ant-design/charts';
import dayjs from 'dayjs';
import { exportTable } from "../../lib/utils.js"
// 接口引入
import {
@@ -21,6 +22,7 @@ import {
const { RangePicker } = DatePicker;
const { Text } = Typography;
const { Option } = Select;
// ====================== 你提供的枚举 ======================
const alarmTypeMap = {
@@ -58,6 +60,7 @@ const levelText = Object.keys(alarmLevelConfig).reduce((map, key) => {
}, {});
export default function AlarmCenterPage() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
@@ -122,6 +125,11 @@ export default function AlarmCenterPage() {
const res = await getAlarmDetail(id);
if (res.code === 0 || res.code === 200) {
setCurrentAlarm(res.data);
// 表单赋值
form.setFieldsValue({
handleStatus: res.data.handleStatus,
handleRemark: '已处理'
});
setDrawerVisible(true);
}
} catch (err) {
@@ -132,9 +140,13 @@ export default function AlarmCenterPage() {
};
// ====================== 处理告警 ======================
const doHandle = async () => {
const doHandle = async (values) => {
try {
await handleAlarm({ id: currentAlarm.id, handleStatus: 3, handleRemark: '已处理' });
await handleAlarm({
id: currentAlarm.id,
handleStatus: values.handleStatus,
handleRemark: values.handleRemark || '已处理'
});
message.success('处理成功');
setDrawerVisible(false);
fetchList();
@@ -146,6 +158,7 @@ export default function AlarmCenterPage() {
// ====================== 导出 ======================
const doExport = () => {
//exportTable(columns, alarmList, "告警记录")
exportAlarm(queryParams).then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' });
const url = URL.createObjectURL(blob);
@@ -505,93 +518,110 @@ export default function AlarmCenterPage() {
>
<Spin spinning={detailLoading}>
{currentAlarm && (
<>
{/* 告警头部 */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
<div style={{ width: 40, height: 40, background: '#FFF1F0', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', marginRight: 12 }}>
<ExclamationCircleOutlined style={{ fontSize: 20, color: '#FF4D4F' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 700 }}>{currentAlarm.alarmTitle}</span>
<Tag color={levelColor[currentAlarm.alarmLevel]} style={{ borderRadius: 2, border: 'none', color: '#fff' }}>
{levelText[currentAlarm.alarmLevel]}
</Tag>
<Form form={form} onFinish={doHandle} layout="vertical">
<>
{/* 告警头部 */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
<div style={{ width: 40, height: 40, background: '#FFF1F0', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', marginRight: 12 }}>
<ExclamationCircleOutlined style={{ fontSize: 20, color: '#FF4D4F' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 700 }}>{currentAlarm.alarmTitle}</span>
<Tag color={levelColor[currentAlarm.alarmLevel]} style={{ borderRadius: 2, border: 'none', color: '#fff' }}>
{levelText[currentAlarm.alarmLevel]}
</Tag>
</div>
<div style={{ fontSize: 12, color: '#86909C' }}>告警编号:{currentAlarm.alarmNo}</div>
</div>
<div style={{ fontSize: 12, color: '#86909C' }}>告警编号:{currentAlarm.alarmNo}</div>
</div>
</div>
{/* 告警基础信息 */}
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="来源设备">{currentAlarm.deviceName || '-'}</Descriptions.Item>
<Descriptions.Item label="告警类型">{alarmTypeMap[currentAlarm.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label="发生时间">{dayjs(currentAlarm.alarmTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
<Descriptions.Item label="处理状态">
{(() => {
let color = 'default';
const status = currentAlarm.handleStatus;
// 1:待处理 2:处理中 3:已关闭 4:已忽略
switch (status) {
case 1: color = 'orange'; break;
case 2: color = 'blue'; break;
case 3: color = 'green'; break;
case 4: color = 'default'; break;
default: break;
}
return <Tag color={color}>{handleStatusMap[status] || '-'}</Tag>;
})()}
</Descriptions.Item>
<Descriptions.Item label="负责人">{currentAlarm.handleUserName || '-'}</Descriptions.Item>
</Descriptions>
{/* 告警基础信息 */}
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="来源设备">{currentAlarm.deviceName || '-'}</Descriptions.Item>
<Descriptions.Item label="告警类型">{alarmTypeMap[currentAlarm.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label="发生时间">{dayjs(currentAlarm.alarmTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
<Descriptions.Item label="处理状态">
{(() => {
let color = 'default';
const status = currentAlarm.handleStatus;
// 1:待处理 2:处理中 3:已关闭 4:已忽略
switch (status) {
case 1: color = 'orange'; break;
case 2: color = 'blue'; break;
case 3: color = 'green'; break;
case 4: color = 'default'; break;
default: break;
}
return <Tag color={color}>{handleStatusMap[status] || '-'}</Tag>;
})()}
</Descriptions.Item>
<Descriptions.Item label="负责人">{currentAlarm.handleUserName || '-'}</Descriptions.Item>
</Descriptions>
{/* 告警描述 */}
<Card size="small" title="告警描述" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.alarmContent || '-'}</div>
</Card>
{/* 告警描述 */}
<Card size="small" title="告警描述" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.alarmContent || '-'}</div>
</Card>
{/* 根因分析 */}
<Card size="small" title="根因分析建议" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.rootCause || '-'}</div>
</Card>
{/* 根因分析 */}
<Card size="small" title="根因分析建议" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.rootCause || '-'}</div>
</Card>
{/* 推荐处理措施 */}
<Card size="small" title="推荐处理措施" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.handleSuggestion || '-'}</div>
</Card>
{/* 推荐处理措施 */}
<Card size="small" title="推荐处理措施" style={{ marginBottom: 16 }}>
<div style={{ color: '#333' }}>{currentAlarm.handleSuggestion || '-'}</div>
</Card>
{/* 通知与订阅 */}
<Card size="small" title="通知与订阅" style={{ marginBottom: 16 }}>
<Row gutter={[8, 8]}>
<Col span={8}>
<Space>
<svg t="1778634297838" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9839" width="16" height="16"><path d="M761.984 1009.28H253.696C138.368 1009.28 44.8 915.712 44.8 800.384V292.096C44.8 176.768 138.368 83.2 253.696 83.2h508.288c115.328 0 208.896 93.568 208.896 208.896v508.288c0 115.328-93.568 208.896-208.896 208.896z" fill="#00E560" p-id="9840"></path><path d="M507.776 265.856c-72.576 0-139.776 20.352-193.536 54.528-54.272 34.432-94.592 83.2-113.408 139.264-7.552 22.528-11.648 46.464-11.648 71.04 0 92.16 56.448 172.928 142.208 220.544 7.68 4.224 11.648 13.44 8.32 21.632-6.4 15.616-14.592 30.592-24.32 44.672-3.072 4.352 0.768 10.368 6.016 8.96 29.44-7.808 56.96-19.072 81.92-33.28 9.088-5.248 19.456-6.912 29.696-4.864 23.936 4.736 49.024 7.296 74.88 7.296 176 0 318.592-118.656 318.592-264.832 0-146.304-142.72-264.96-318.72-264.96z" fill="#FFFFFF" p-id="9841"></path></svg>
<span>短信</span>
<Switch checked={currentAlarm.notifySms === 1} disabled />
</Space>
</Col>
<Col span={8}>
<Space>
<svg t="1778634173765" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8764" width="16" height="16"><path d="M512.003 79C272.855 79 79 272.855 79 512.003 79 751.145 272.855 945 512.003 945 751.145 945 945 751.145 945 512.003 945 272.855 751.145 79 512.003 79z m200.075 375.014c-0.867 3.764-3.117 9.347-6.234 16.012h0.087l-0.347 0.648c-18.183 38.86-65.631 115.108-65.631 115.108l-0.215-0.52-13.856 24.147h66.8L565.063 779l29.002-115.368h-52.598l18.27-76.29c-14.76 3.55-32.253 8.436-52.945 15.1 0 0-27.967 16.36-80.607-31.5 0 0-35.501-31.29-14.891-39.078 8.744-3.33 42.466-7.573 69.004-11.122 35.93-4.845 57.965-7.441 57.965-7.441s-110.607 1.643-136.841-2.468c-26.237-4.11-59.525-47.905-66.626-86.377 0 0-10.953-21.117 23.595-11.122 34.547 10 177.535 38.95 177.535 38.95s-185.933-56.992-198.36-70.929c-12.381-13.846-36.406-75.902-33.289-113.981 0 0 1.343-9.521 11.127-6.926 0 0 137.49 62.75 231.475 97.152 94.028 34.403 175.76 51.885 165.2 96.414z" fill="#3AA2EB" p-id="8765"></path></svg>
{/*<NotificationOutlined />*/}
<span>钉钉</span>
<Switch checked={currentAlarm.notifyTelegram === 1} disabled />
</Space>
</Col>
<Col span={8}>
<Space>
<svg t="1778634149974" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7716" width="16" height="16"><path d="M512 512m-448 0a448 448 0 1 0 896 0 448 448 0 1 0-896 0Z" fill="#608BE9" p-id="7717"></path><path d="M192 302m32 0l576 0q32 0 32 32l0 356q0 32-32 32l-576 0q-32 0-32-32l0-356q0-32 32-32Z" fill="#EAEDF5" p-id="7718"></path><path d="M224 722h576c17.673 0 32-14.327 32-32v-58C660.96 493.333 554.294 424 512 424c-42.294 0-148.96 69.333-320 208v58c0 17.673 14.327 32 32 32z" fill="#CCDAF7" p-id="7719"></path><path d="M224 302h576c17.673 0 32 14.327 32 32v58C651.35 517.333 544.683 580 512 580c-32.683 0-139.35-62.667-320-188v-58c0-17.673 14.327-32 32-32z" fill="#FFFFFF" p-id="7720"></path></svg>
{/*<NotificationOutlined />*/}
<span>邮件</span>
<Switch checked={currentAlarm.notifyEmail === 1} disabled />
</Space>
</Col>
</Row>
</Card>
{/* 处理操作 */}
<Card size="small" title="处理操作" style={{ marginBottom: 16 }}>
<Form.Item label="处理状态" name="handleStatus" rules={[{ required: true, message: '请选择处理状态' }]}>
<Select>
<Option value={1}>待处理</Option>
<Option value={2}>处理中</Option>
<Option value={3}>已关闭</Option>
<Option value={4}>已忽略</Option>
</Select>
</Form.Item>
<Form.Item label="处理备注" name="handleRemark">
<Input.TextArea rows={2} placeholder="请输入处理备注" />
</Form.Item>
</Card>
{/* 底部按钮 */}
<Button type="primary" block onClick={doHandle}>标记为已解决</Button>
</>
{/* 通知与订阅 */}
<Card size="small" title="通知与订阅" style={{ marginBottom: 16 }}>
<Row gutter={[8, 8]}>
<Col span={8}>
<Space>
<svg t="1778634297838" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9839" width="16" height="16"><path d="M761.984 1009.28H253.696C138.368 1009.28 44.8 915.712 44.8 800.384V292.096C44.8 176.768 138.368 83.2 253.696 83.2h508.288c115.328 0 208.896 93.568 208.896 208.896v508.288c0 115.328-93.568 208.896-208.896 208.896z" fill="#00E560" p-id="9840"></path><path d="M507.776 265.856c-72.576 0-139.776 20.352-193.536 54.528-54.272 34.432-94.592 83.2-113.408 139.264-7.552 22.528-11.648 46.464-11.648 71.04 0 92.16 56.448 172.928 142.208 220.544 7.68 4.224 11.648 13.44 8.32 21.632-6.4 15.616-14.592 30.592-24.32 44.672-3.072 4.352 0.768 10.368 6.016 8.96 29.44-7.808 56.96-19.072 81.92-33.28 9.088-5.248 19.456-6.912 29.696-4.864 23.936 4.736 49.024 7.296 74.88 7.296 176 0 318.592-118.656 318.592-264.832 0-146.304-142.72-264.96-318.72-264.96z" fill="#FFFFFF" p-id="9841"></path></svg>
<span>短信</span>
<Switch checked={currentAlarm.notifySms === 1} />
</Space>
</Col>
<Col span={8}>
<Space>
<svg t="1778634173765" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8764" width="16" height="16"><path d="M512.003 79C272.855 79 79 272.855 79 512.003 79 751.145 272.855 945 512.003 945 751.145 945 945 751.145 945 512.003 945 272.855 751.145 79 512.003 79z m200.075 375.014c-0.867 3.764-3.117 9.347-6.234 16.012h0.087l-0.347 0.648c-18.183 38.86-65.631 115.108-65.631 115.108l-0.215-0.52-13.856 24.147h66.8L565.063 779l29.002-115.368h-52.598l18.27-76.29c-14.76 3.55-32.253 8.436-52.945 15.1 0 0-27.967 16.36-80.607-31.5 0 0-35.501-31.29-14.891-39.078 8.744-3.33 42.466-7.573 69.004-11.122 35.93-4.845 57.965-7.441 57.965-7.441s-110.607 1.643-136.841-2.468c-26.237-4.11-59.525-47.905-66.626-86.377 0 0-10.953-21.117 23.595-11.122 34.547 10 177.535 38.95 177.535 38.95s-185.933-56.992-198.36-70.929c-12.381-13.846-36.406-75.902-33.289-113.981 0 0 1.343-9.521 11.127-6.926 0 0 137.49 62.75 231.475 97.152 94.028 34.403 175.76 51.885 165.2 96.414z" fill="#3AA2EB" p-id="8765"></path></svg>
{/*<NotificationOutlined />*/}
<span>钉钉</span>
<Switch checked={currentAlarm.notifyTelegram === 1} />
</Space>
</Col>
<Col span={8}>
<Space>
<svg t="1778634149974" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7716" width="16" height="16"><path d="M512 512m-448 0a448 448 0 1 0 896 0 448 448 0 1 0-896 0Z" fill="#608BE9" p-id="7717"></path><path d="M192 302m32 0l576 0q32 0 32 32l0 356q0 32-32 32l-576 0q-32 0-32-32l0-356q0-32 32-32Z" fill="#EAEDF5" p-id="7718"></path><path d="M224 722h576c17.673 0 32-14.327 32-32v-58C660.96 493.333 554.294 424 512 424c-42.294 0-148.96 69.333-320 208v58c0 17.673 14.327 32 32 32z" fill="#CCDAF7" p-id="7719"></path><path d="M224 302h576c17.673 0 32 14.327 32 32v58C651.35 517.333 544.683 580 512 580c-32.683 0-139.35-62.667-320-188v-58c0-17.673 14.327-32 32-32z" fill="#FFFFFF" p-id="7720"></path></svg>
{/*<NotificationOutlined />*/}
<span>邮件</span>
<Switch checked={currentAlarm.notifyEmail === 1} />
</Space>
</Col>
</Row>
</Card>
{/* 底部按钮 */}
<Button type="primary" block htmlType="submit">确认处理</Button>
</>
</Form>
)}
</Spin>
</Drawer>

View File

@@ -130,3 +130,40 @@ export function getWeatherType(weatherText = "") {
// 兜底
return "cloudy";
}
// ==============================================
// 通用导出表格为 CSV / Excel(通用版,不写死任何字段)
// ==============================================
export const exportTable = (columns, dataSource, fileName = "导出数据") => {
// 1. 从 columns 自动提取表头(过滤掉不显示的列)
const headers = columns
.filter(col => col.title && col.dataIndex) // 只取有标题+有字段的列
.map(col => col.title);
// 2. 自动提取数据(根据 dataIndex 取值)
const data = dataSource.map(item => {
return columns
.filter(col => col.title && col.dataIndex)
.map(col => {
let val = item[col.dataIndex];
// 处理 undefined / null
if (val === undefined || val === null) return "";
return String(val).replace(/"/g, '""'); // 转义CSV引号
});
});
// 3. 拼接CSV内容
const csvContent = [
headers.join(','),
...data.map(row => `"${row.join('","')}"`)
].join('\n');
// 4. 下载文件(解决中文乱码)
const blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${fileName}_${dayjs().format("YYYY-MM-DD")}.csv`;
a.click();
URL.revokeObjectURL(url);
};

359
src/pages/Profile.tsx Normal file
View File

@@ -0,0 +1,359 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Button,
Upload,
message,
Tabs,
Avatar,
Row,
Col,
Divider,
Typography,
Space,
} from 'antd';
import {
UserOutlined,
LockOutlined,
UploadOutlined,
PhoneOutlined,
MailOutlined,
SafetyOutlined,
EditOutlined,
} from '@ant-design/icons';
import { useSelector } from 'react-redux';
import { RootState, useAppDispatch } from '../store';
import { setUserInfo } from '../store/userSlice';
import { getUserProfile, updateUserProfile, updateUserPwd, uploadAvatar } from '../api/user';
import envConfig from '../../env';
const { TabPane } = Tabs;
const { Title } = Typography;
const Profile: React.FC = () => {
const [loading, setLoading] = useState(false);
const [profileData, setProfileData] = useState<any>(null);
const { userInfo } = useSelector((state: RootState) => state.user);
const dispatch = useAppDispatch();
const [form] = Form.useForm();
const [pwdForm] = Form.useForm();
const fetchProfile = async () => {
try {
const res: any = await getUserProfile();
if (res.code === 200) {
setProfileData(res.data);
form.setFieldsValue(res.data);
}
} catch (error) {
message.error('获取个人信息失败');
}
};
useEffect(() => {
fetchProfile();
}, []);
const handleUpdateProfile = async (values: any) => {
setLoading(true);
try {
const res: any = await updateUserProfile(values);
if (res.code === 200) {
message.success('修改成功');
const newUserInfo = { ...userInfo, ...values };
dispatch(setUserInfo(newUserInfo));
fetchProfile();
} else {
message.error(res.msg || '修改失败');
}
} catch (error) {
message.error('请求失败');
} finally {
setLoading(false);
}
};
const handleUpdatePwd = async (values: any) => {
if (values.newPassword !== values.confirmPassword) {
message.error('两次输入的密码不一致');
return;
}
setLoading(true);
try {
const res: any = await updateUserPwd(values.oldPassword, values.newPassword);
if (res.code === 200) {
message.success('密码修改成功');
pwdForm.resetFields();
} else {
message.error(res.msg || '修改失败');
}
} catch (error) {
message.error('请求失败');
} finally {
setLoading(false);
}
};
const handleUpload = async (options: any) => {
const { file } = options;
const formData = new FormData();
formData.append('avatarfile', file);
try {
const res: any = await uploadAvatar(formData);
if (res.code === 200) {
message.success('头像上传成功');
const newUserInfo = { ...userInfo, avatar: res.imgUrl };
dispatch(setUserInfo(newUserInfo));
fetchProfile();
} else {
message.error(res.msg || '上传失败');
}
} catch (error) {
message.error('上传失败');
}
};
const avatarUrl = profileData?.avatar
? profileData.avatar.startsWith('http')
? profileData.avatar
: `${envConfig.baseURL}${profileData.avatar}`
: null;
return (
<div style={{ padding: '32px 40px', background: '#f5f7fa', minHeight: '100vh' }}>
<Title level={4} style={{ marginBottom: 24, fontWeight: 600 }}>
个人中心
</Title>
<Row gutter={24}>
{/* 左侧信息卡片 */}
<Col xs={24} sm={24} md={8} lg={7} xl={6}>
<Card
bordered={false}
style={{
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
textAlign: 'center',
overflow: 'hidden',
}}
>
<div style={{ padding: '20px 0' }}>
<Avatar
size={120}
src={avatarUrl}
icon={<UserOutlined style={{ fontSize: 50 }} />}
style={{
border: '6px solid #fff',
boxShadow: '0 4px 16px rgba(0,0,0,0.08)',
}}
/>
<div style={{ marginTop: 20 }}>
<Upload customRequest={handleUpload} showUploadList={false} accept="image/*">
<Button
icon={<UploadOutlined />}
style={{
borderRadius: 6,
borderColor: '#d9d9d9',
}}
>
更换头像
</Button>
</Upload>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
<div style={{ textAlign: 'left', padding: '8px 12px' }}>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<UserOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>用户名称:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.userName}</strong>
</div>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<PhoneOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>手机号码:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.phonenumber || '未设置'}</strong>
</div>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<MailOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>用户邮箱:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.email || '未设置'}</strong>
</div>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<SafetyOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>所属角色:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>
{profileData?.roles?.map((r: any) => r.roleName).join(', ') || '未设置'}
</strong>
</div>
</Space>
</div>
</Card>
</Col>
{/* 右侧表单卡片 */}
<Col xs={24} sm={24} md={16} lg={17} xl={18}>
<Card
bordered={false}
style={{
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
overflow: 'hidden',
}}
>
<Tabs
defaultActiveKey="1"
type="card"
style={{
marginTop: -8,
}}
>
{/* 基本资料 */}
<TabPane tab="基本资料" key="1">
<div style={{ maxWidth: 500, margin: '0 auto', padding: '12px 0' }}>
<Form
form={form}
layout="vertical"
onFinish={handleUpdateProfile}
initialValues={profileData}
size="large"
>
<Form.Item
label="用户昵称"
name="nickName"
rules={[{ required: true, message: '请输入用户昵称' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入用户昵称"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="手机号码"
name="phonenumber"
rules={[{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="请输入手机号码"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[{ type: 'email', message: '请输入正确的邮箱格式' }]}
>
<Input
prefix={<MailOutlined />}
placeholder="请输入邮箱"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item style={{ marginTop: 28 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
icon={<EditOutlined />}
style={{
borderRadius: 6,
height: 42,
width: 160,
}}
>
保存修改
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
{/* 修改密码 */}
<TabPane tab="修改密码" key="2">
<div style={{ maxWidth: 500, margin: '0 auto', padding: '12px 0' }}>
<Form
form={pwdForm}
layout="vertical"
onFinish={handleUpdatePwd}
size="large"
>
<Form.Item
label="旧密码"
name="oldPassword"
rules={[{ required: true, message: '请输入旧密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入旧密码"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="新密码"
name="newPassword"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度不能小于6位' },
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入新密码"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="确认密码"
name="confirmPassword"
rules={[{ required: true, message: '请确认新密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入新密码"
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item style={{ marginTop: 28 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
icon={<EditOutlined />}
style={{
borderRadius: 6,
height: 42,
width: 160,
}}
>
确认修改
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
</Tabs>
</Card>
</Col>
</Row>
</div>
);
};
export default Profile;

View File

@@ -24,6 +24,7 @@ const Organization = lazy(() => import('../components/SystemSetting/Organization
const StationManage = lazy(() => import('../components/SystemSetting/StationManage'));
const SerialNumGenerate = lazy(() => import('../pages/SerialNumGenerate'));
const SystemSetting = lazy(() => import('../pages/SystemSetting'));
const Profile = lazy(() => import('../pages/Profile'));
function PageFallback() {
return (
@@ -57,6 +58,7 @@ export const routeMeta: Record<string, {
'/organization': { title: '组织管理', roles: ['admin'], requiresAuth: true },
'/station': { title: '场站管理', roles: ['admin'], requiresAuth: true },
'/RealtimeMonitor': { title: '实时监控', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/profile': { title: '个人中心', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
};
const routes: RouteObject[] = [
@@ -102,6 +104,7 @@ const routes: RouteObject[] = [
{ path: 'organization', element: <Organization /> },
{ path: 'station', element: <StationManage /> },
{ path: 'system', element: <SystemSetting /> },
{ path: 'profile', element: <Profile /> },
{
path: '*',
element: (