后台-个人设置
This commit is contained in:
@@ -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'),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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: [] },
|
||||
];
|
||||
|
||||
@@ -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}>
|
||||
|
||||
101
src/pages/Profile/components/LoginLog.tsx
Normal file
101
src/pages/Profile/components/LoginLog.tsx
Normal 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;
|
||||
95
src/pages/Profile/components/OperateLog.tsx
Normal file
95
src/pages/Profile/components/OperateLog.tsx
Normal 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;
|
||||
96
src/pages/Profile/components/ProfileEditForm.tsx
Normal file
96
src/pages/Profile/components/ProfileEditForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
69
src/pages/Profile/index.tsx
Normal file
69
src/pages/Profile/index.tsx
Normal 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;
|
||||
@@ -7,4 +7,10 @@ export const AdminLogServices = {
|
||||
getAdminMenuAjaxList: '/AdminMenu/getAdminMenuAjaxList',
|
||||
/** 权限 */
|
||||
getAdminFunctionAjaxList: '/AdminFunction/getAdminFunctionAjaxList',
|
||||
|
||||
/** AJAX */
|
||||
/** 登录日志 */
|
||||
getAdminLoginLogAjaxList: '/AdminLog/getAdminLoginLogAjaxList',
|
||||
/** 操作日志 */
|
||||
getAdminSysLogAjaxList: '/AdminLog/getAdminSysLogAjaxList',
|
||||
} as const;
|
||||
|
||||
@@ -10,6 +10,8 @@ export const AdminServices = {
|
||||
add: '/Admin/add',
|
||||
edit: '/Admin/edit',
|
||||
del: '/Admin/del',
|
||||
/** 个人设置 */
|
||||
profile: '/Admin/profile',
|
||||
/** 登录管理员权限列表 */
|
||||
auth: '/Admin/auth',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user