后台-个人设置

This commit is contained in:
2026-01-26 16:59:52 +08:00
parent c84d4f74a6
commit 535fb734ea
11 changed files with 425 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
import { DownOutlined } from '@ant-design/icons';
import { DownOutlined, SettingOutlined } from '@ant-design/icons';
import { Button, Dropdown, Popconfirm, Space } from 'antd';
import { navigate } from '@/router/routerUtils';
import { useUserStore } from '@/store/UserStore';
import { GapBox } from '../GapBox';
@@ -16,7 +17,7 @@ export const HeaderUserInfo: React.FC = () => {
menu={{
items: [
{
key: 'user-info',
key: 'admin-info',
disabled: true, // 只展示,不可操作
label: (
<Space size={8} style={{ color: 'rgba(0,0,0,0.88)' }}>
@@ -26,6 +27,15 @@ export const HeaderUserInfo: React.FC = () => {
</Space>
),
},
{
type: 'divider',
},
{
key: 'profile',
icon: <SettingOutlined />,
label: '个人信息',
onClick: () => navigate('/profile'),
},
],
}}
>

View File

@@ -1,13 +1,11 @@
import { Checkbox } from 'antd';
import { useEffect, useState } from 'react';
import { useCompanyStore } from '@/store/CompanyStore';
import { useUserStore } from '@/store/UserStore';
const TabNavSaveCheckBoxPlugin = () => {
const [value, setValue] = useState(false);
const user = useUserStore().user;
const company = useCompanyStore().company;
const storageKey = `u${user.user_id}_c${company.company_id}_tabNavSave`;
const storageKey = `u${user.admin_id}_tabNavSave`;
useEffect(() => {
setValue(localStorage.getItem(storageKey) == '1');
}, []);
@@ -19,7 +17,8 @@ const TabNavSaveCheckBoxPlugin = () => {
console.log(e);
setValue(e.target.checked);
localStorage.setItem(storageKey, `${e.target.checked ? 1 : 0}`);
}}>
}}
>
</Checkbox>
);

View File

@@ -3,6 +3,7 @@ import { Menu } from 'antd';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Colors, DefaultERPName, headerHeight } from '@/configs/config';
import { routes } from '@/configs/routes';
import { getHash } from '@/router/routerUtils';
import { useCompanyStore } from '@/store/CompanyStore';
import { useRefreshStore } from '@/store/RefreshStore';
@@ -27,6 +28,29 @@ const TabNavPlugin: React.FC = () => {
const modeRef = useRef('');
const company = useCompanyStore().company;
function findRouteTitle(pathname: string, routes: any[], parentPath = ''): string | undefined {
for (const route of routes) {
const routePath = route.path || '';
//const fullPath = routePath.startsWith('/') ? routePath : (parentPath + '/' + routePath).replace(/\/+/g, '/');
const fullPath = routePath.startsWith('/') ? routePath : `${parentPath}/${routePath}`.replace(/\/+/g, '/');
// 如果 pathname 完全匹配当前路由,返回 title
if (route.title && pathname === fullPath) return route.title;
// 如果有子路由,递归查找
if (route.children?.length) {
const title = findRouteTitle(pathname, route.children, fullPath);
if (title) return title;
// 如果 pathname === 父路由 path 且子路由存在 title返回第一个子路由的 title
if (pathname === fullPath && route.children[0]?.title) {
return route.children[0].title;
}
}
}
return undefined;
}
const callback = () => {
const span = document.querySelector('.urlList-active');
if (urlListRef.current && span && modeRef.current != 'del') {
@@ -102,6 +126,13 @@ const TabNavPlugin: React.FC = () => {
const pathname = getHash();
setPathname(pathname);
const routeTitle = findRouteTitle(pathname, routes);
// 决定最终 title
const finalTitle =
routeTitle || (document.title ? `${document.title}`.replace(` - ${DefaultERPName}`, '') : pathname);
document.title = `${finalTitle} - ${DefaultERPName}`;
let flag = true;
if ((document.title == '404' || pathname == '/temp' || pathname == '/') && !isPhone) {
return;
@@ -117,7 +148,7 @@ const TabNavPlugin: React.FC = () => {
if (flag) {
urlListRef.current.push({
url: pathname,
title: `${document.title}`.replace(` - ${DefaultERPName}`, ''),
title: finalTitle,
search: '',
});
setUrlList([...urlListRef.current]);

View File

@@ -59,5 +59,13 @@ export const routes: IRouteItem[] = [
//
],
},
{
path: '/profile',
Layout: AppLayout,
children: [
{ path: '/index', Component: lazy(() => import('@/pages/Profile')), title: '个人信息' },
//
],
},
{ path: '*', Layout: ErrorPage, children: [] },
];

View File

@@ -17,7 +17,7 @@ const Login = () => {
},
});
document.title = '登录';
document.title = `登录 - ${DefaultERPName}`;
const login = () => {
request(stringify(userInfo));
@@ -48,7 +48,6 @@ const Login = () => {
>
<div style={{ marginBottom: 24, fontSize: 20, display: 'flex', justifyContent: 'center' }}>
{DefaultERPName}
{document.title}
</div>
<FormPlugin>
<FormItemPlugin allCol={24}>

View File

@@ -0,0 +1,101 @@
import { stringify } from 'qs';
import { useEffect, useState } from 'react';
import { FooterPagination } from '@/components/PaginationPlugin';
import type { ColumnsTypeUltra } from '@/components/TableColumnsFilterPlugin';
import { TablePlugin } from '@/components/TablePlugin';
import { statusObj } from '@/configs/adminLogConfig';
import type { IAjaxDataBase, IParamsBase } from '@/interfaces/common';
import { AdminLogServices } from '@/services/AdminLogServices';
import { tableFixedByPhone, toArray } from '@/utils/common';
import { useRequest } from '@/utils/useRequest';
interface IAjaxData extends IAjaxDataBase {
data: any[];
}
type IParams = IParamsBase & {
login_dateL?: string;
login_dateU?: string;
admin_id?: number;
};
/** 登录日志页面 */
const AdminLoginLogForm: React.FC = () => {
const [params] = useState<IParams>({
curr_page: 1,
page_count: 20,
admin_id: Number(localStorage.getItem('admin_id')) || undefined,
});
const [ajaxData, setAjaxData] = useState<IAjaxData>({ count: 0, data: [] });
const { loading: listLoading, request: listRequest } = useRequest(AdminLogServices.getAdminLoginLogAjaxList, {
onSuccessCodeZero: (res) => {
setAjaxData({
count: res.count || 0,
data: toArray(res.data),
});
},
});
const columns: ColumnsTypeUltra<any> = [
{
title: '#',
width: 50,
fixed: tableFixedByPhone('left'),
align: 'center',
render(_value, _record, index) {
return index + 1;
},
},
{ title: '登录账号', dataIndex: 'username', width: 80 },
{ title: '登录IP', dataIndex: 'ip', width: 100 },
{ title: '登录地址', dataIndex: 'ip_location', width: 100 },
{ title: '平台', dataIndex: 'os', width: 80 },
{ title: '浏览器', dataIndex: 'browser', width: 80 },
{ title: '登录时间', dataIndex: 'login_date', width: 120 },
{ title: '状态', dataIndex: 'status', width: 60, render: (value) => statusObj[value] },
{
title: '备注',
dataIndex: 'remark',
width: 120,
},
];
function page(curr: number) {
if (!listLoading) {
params.curr_page = curr;
listRequest(stringify(params));
}
}
useEffect(() => {
page(1);
}, []);
return (
<>
<TablePlugin
loading={listLoading}
onChange={(_p, _f, sort: any) => {
page(1);
}}
dataSource={ajaxData.data}
columns={columns}
scroll={{ x: true }}
rowKey={'login_id'}
/>
<FooterPagination
current={params.curr_page}
pageSize={params.page_count}
total={ajaxData.count}
onChange={(curr, pageSize) => {
params.page_count = pageSize;
page(curr);
}}
/>
</>
);
};
/** 登录日志页面 */
export default AdminLoginLogForm;

View File

@@ -0,0 +1,95 @@
import { stringify } from 'qs';
import { useEffect, useState } from 'react';
import { FooterPagination } from '@/components/PaginationPlugin';
import type { ColumnsTypeUltra } from '@/components/TableColumnsFilterPlugin';
import { TablePlugin } from '@/components/TablePlugin';
import { statusObj } from '@/configs/adminLogConfig';
import type { IAjaxDataBase, IParamsBase } from '@/interfaces/common';
import { AdminLogServices } from '@/services/AdminLogServices';
import { tableFixedByPhone, toArray } from '@/utils/common';
import { useRequest } from '@/utils/useRequest';
interface IAjaxData extends IAjaxDataBase {
data: any[];
}
type IParams = IParamsBase & {
create_dateL?: string;
create_dateU?: string;
admin_id?: number;
};
/** 操作日志页面 */
const AdminSysLogForm: React.FC = () => {
const [params] = useState<IParams>({
curr_page: 1,
page_count: 20,
admin_id: Number(localStorage.getItem('admin_id')) || undefined,
});
const [ajaxData, setAjaxData] = useState<IAjaxData>({ count: 0, data: [] });
const { loading: listLoading, request: listRequest } = useRequest(AdminLogServices.getAdminSysLogAjaxList, {
onSuccessCodeZero: (res) => {
setAjaxData({
count: res.count || 0,
data: toArray(res.data),
});
},
});
const columns: ColumnsTypeUltra<any> = [
{
title: '#',
width: 20,
fixed: tableFixedByPhone('left'),
align: 'center',
render(_value, _record, index) {
return index + 1;
},
},
{ title: '操作账号', dataIndex: 'username', width: 50 },
{ title: '操作IP', dataIndex: 'ip', width: 100 },
{ title: '菜单', dataIndex: 'menu_ch_name', width: 80 },
{ title: '权限', dataIndex: 'function_ch_name', width: 60 },
{ title: '操作时间', dataIndex: 'create_date', width: 120 },
{ title: '状态', dataIndex: 'status', width: 60, render: (value) => statusObj[value] },
];
function page(curr: number) {
if (!listLoading) {
params.curr_page = curr;
listRequest(stringify(params));
}
}
useEffect(() => {
page(1);
}, []);
return (
<>
<TablePlugin
loading={listLoading}
onChange={(_p, _f, sort: any) => {
page(1);
}}
dataSource={ajaxData.data}
columns={columns}
scroll={{ x: true }}
rowKey={'sys_id'}
/>
<FooterPagination
current={params.curr_page}
pageSize={params.page_count}
total={ajaxData.count}
onChange={(curr, pageSize) => {
params.page_count = pageSize;
page(curr);
}}
/>
</>
);
};
/** 操作日志页面 */
export default AdminSysLogForm;

View File

@@ -0,0 +1,96 @@
import { Button, Form, Input, notification } from 'antd';
import { stringify } from 'qs';
import type React from 'react';
import { useEffect } from 'react';
import { AdminServices } from '@/services/AdminServices';
import { useUserStore } from '@/store/UserStore';
import type { IRef } from '@/utils/type';
import { useRequest } from '@/utils/useRequest';
interface IProps extends IRef {
onCallback?: () => void;
}
export type IProfileEditFormType = {
show: (data?: any) => void;
};
export const ProfileEditForm: React.FC<IProps> = (props) => {
const [form] = Form.useForm();
const userInfo = useUserStore().user;
// 请求成功回调
const success = (res: any) => {
if (res.err_code === 0) {
notification.success({ message: '保存成功' });
props.onCallback?.();
}
};
const { loading: editLoading, request: editRequest } = useRequest(AdminServices.profile, { onSuccess: success });
const save = async () => {
try {
const values = await form.validateFields();
editRequest(stringify(values));
} catch (error) {
console.log('表单验证未通过', error);
}
};
useEffect(() => {
form.setFieldsValue({
username: userInfo.username,
mobile: userInfo.mobile,
email: userInfo.email,
nickname: userInfo.nickname,
});
}, [userInfo]);
return (
<Form form={form} labelCol={{ style: { width: window?.dfConfig?.language === 'zh-cn' ? 80 : 120 } }}>
<Form.Item label='用户名' name='username'>
<Input disabled />
</Form.Item>
<Form.Item
label='昵称'
name='nickname'
required
rules={[
{ required: true, message: '请输入昵称' },
{ min: 2, message: '最少2字符' },
{ max: 20, message: '最多20字符' },
]}
>
<Input allowClear placeholder='请填写昵称' maxLength={20} showCount />
</Form.Item>
<Form.Item
label='手机号'
name='mobile'
required
rules={[
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
]}
>
<Input allowClear placeholder='请填写手机号' maxLength={11} showCount />
</Form.Item>
<Form.Item label='邮箱' name='email' rules={[{ type: 'email', message: '邮箱格式不正确' }]}>
<Input allowClear placeholder='请填写邮箱' />
</Form.Item>
<Form.Item label='密码' name='password' rules={[{ min: 6, max: 18, message: '密码需6~18位' }]}>
<Input.Password allowClear placeholder='不修改请留空' />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button type='primary' size='large' onClick={save} loading={editLoading}>
</Button>
</Form.Item>
</Form>
);
};

View File

@@ -0,0 +1,69 @@
import { Card, Col, Row, Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
import LoginLog from './components/LoginLog';
import OperateLog from './components/OperateLog';
import { type IProfileEditFormType, ProfileEditForm } from './components/ProfileEditForm';
/** 管理员个人信息页面 */
const ProfileForm: React.FC = () => {
const ProfileEditFormRef = useRef<IProfileEditFormType>(null);
const [activeTab, setActiveTab] = useState<string>('login');
const getTabStorageKey = () => {
const admin_id = localStorage.getItem('admin_id') || 'guest';
return `log_list_active_tab_${admin_id}`;
};
const onTabChange = (key: string) => {
setActiveTab(key);
};
/** 进入页面时恢复上一次 tab */
useEffect(() => {
const lastTab = localStorage.getItem(getTabStorageKey());
if (lastTab) {
setActiveTab(lastTab);
}
}, []);
return (
<Row gutter={16}>
{/* 左:个人信息编辑 */}
<Col span={8}>
<Card title='个人信息'>
<ProfileEditForm ref={ProfileEditFormRef} />
</Card>
</Col>
{/* 右:日志 */}
<Col span={16}>
<Card title='日志记录'>
<Tabs
type='line'
activeKey={activeTab}
onChange={onTabChange}
items={[
{
key: 'login',
label: '登录日志',
children: <LoginLog />,
},
{
key: 'operate',
label: '操作日志',
children: <OperateLog />,
},
]}
/>
</Card>
</Col>
</Row>
);
};
const Profile = () => (
<PageContainerPlugin breadcrumb={['个人中心', '个人信息']}>
<ProfileForm />
</PageContainerPlugin>
);
export default Profile;

View File

@@ -7,4 +7,10 @@ export const AdminLogServices = {
getAdminMenuAjaxList: '/AdminMenu/getAdminMenuAjaxList',
/** 权限 */
getAdminFunctionAjaxList: '/AdminFunction/getAdminFunctionAjaxList',
/** AJAX */
/** 登录日志 */
getAdminLoginLogAjaxList: '/AdminLog/getAdminLoginLogAjaxList',
/** 操作日志 */
getAdminSysLogAjaxList: '/AdminLog/getAdminSysLogAjaxList',
} as const;

View File

@@ -10,6 +10,8 @@ export const AdminServices = {
add: '/Admin/add',
edit: '/Admin/edit',
del: '/Admin/del',
/** 个人设置 */
profile: '/Admin/profile',
/** 登录管理员权限列表 */
auth: '/Admin/auth',
} as const;