个人中心页面
This commit is contained in:
@@ -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'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
359
src/pages/Profile.tsx
Normal 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;
|
||||
@@ -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: (
|
||||
|
||||
Reference in New Issue
Block a user