diff --git a/src/components/Header/HeaderUserInfo.tsx b/src/components/Header/HeaderUserInfo.tsx index b0ead18..6ffcd7f 100644 --- a/src/components/Header/HeaderUserInfo.tsx +++ b/src/components/Header/HeaderUserInfo.tsx @@ -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: ( @@ -26,6 +27,15 @@ export const HeaderUserInfo: React.FC = () => { ), }, + { + type: 'divider', + }, + { + key: 'profile', + icon: , + label: '个人信息', + onClick: () => navigate('/profile'), + }, ], }} > diff --git a/src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx b/src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx index 0341f11..86a4e18 100644 --- a/src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx +++ b/src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx @@ -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}`); - }}> + }} + > 保留标签 ); diff --git a/src/components/TabNavPlugin/index.tsx b/src/components/TabNavPlugin/index.tsx index e7c6d86..2bef747 100644 --- a/src/components/TabNavPlugin/index.tsx +++ b/src/components/TabNavPlugin/index.tsx @@ -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]); diff --git a/src/configs/routes.ts b/src/configs/routes.ts index 026ca19..a04c4b3 100644 --- a/src/configs/routes.ts +++ b/src/configs/routes.ts @@ -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: [] }, ]; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 8d58320..543ccb1 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -17,7 +17,7 @@ const Login = () => { }, }); - document.title = '登录'; + document.title = `登录 - ${DefaultERPName}`; const login = () => { request(stringify(userInfo)); @@ -48,7 +48,6 @@ const Login = () => { >
{DefaultERPName} - {document.title}
diff --git a/src/pages/Profile/components/LoginLog.tsx b/src/pages/Profile/components/LoginLog.tsx new file mode 100644 index 0000000..b63dbc7 --- /dev/null +++ b/src/pages/Profile/components/LoginLog.tsx @@ -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({ + curr_page: 1, + page_count: 20, + admin_id: Number(localStorage.getItem('admin_id')) || undefined, + }); + const [ajaxData, setAjaxData] = useState({ 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 = [ + { + 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 ( + <> + { + page(1); + }} + dataSource={ajaxData.data} + columns={columns} + scroll={{ x: true }} + rowKey={'login_id'} + /> + + { + params.page_count = pageSize; + page(curr); + }} + /> + + ); +}; + +/** 登录日志页面 */ +export default AdminLoginLogForm; diff --git a/src/pages/Profile/components/OperateLog.tsx b/src/pages/Profile/components/OperateLog.tsx new file mode 100644 index 0000000..03c4ec0 --- /dev/null +++ b/src/pages/Profile/components/OperateLog.tsx @@ -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({ + curr_page: 1, + page_count: 20, + admin_id: Number(localStorage.getItem('admin_id')) || undefined, + }); + const [ajaxData, setAjaxData] = useState({ 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 = [ + { + 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 ( + <> + { + page(1); + }} + dataSource={ajaxData.data} + columns={columns} + scroll={{ x: true }} + rowKey={'sys_id'} + /> + + { + params.page_count = pageSize; + page(curr); + }} + /> + + ); +}; + +/** 操作日志页面 */ +export default AdminSysLogForm; diff --git a/src/pages/Profile/components/ProfileEditForm.tsx b/src/pages/Profile/components/ProfileEditForm.tsx new file mode 100644 index 0000000..5a4fbc6 --- /dev/null +++ b/src/pages/Profile/components/ProfileEditForm.tsx @@ -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 = (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 ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx new file mode 100644 index 0000000..04374ea --- /dev/null +++ b/src/pages/Profile/index.tsx @@ -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(null); + const [activeTab, setActiveTab] = useState('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 ( + + {/* 左:个人信息编辑 */} + + + + + + + {/* 右:日志 */} + + + , + }, + { + key: 'operate', + label: '操作日志', + children: , + }, + ]} + /> + + + + ); +}; + +const Profile = () => ( + + + +); +export default Profile; diff --git a/src/services/AdminLogServices.ts b/src/services/AdminLogServices.ts index 4e0541a..813476e 100644 --- a/src/services/AdminLogServices.ts +++ b/src/services/AdminLogServices.ts @@ -7,4 +7,10 @@ export const AdminLogServices = { getAdminMenuAjaxList: '/AdminMenu/getAdminMenuAjaxList', /** 权限 */ getAdminFunctionAjaxList: '/AdminFunction/getAdminFunctionAjaxList', + + /** AJAX */ + /** 登录日志 */ + getAdminLoginLogAjaxList: '/AdminLog/getAdminLoginLogAjaxList', + /** 操作日志 */ + getAdminSysLogAjaxList: '/AdminLog/getAdminSysLogAjaxList', } as const; diff --git a/src/services/AdminServices.ts b/src/services/AdminServices.ts index a695544..4ed1ef6 100644 --- a/src/services/AdminServices.ts +++ b/src/services/AdminServices.ts @@ -10,6 +10,8 @@ export const AdminServices = { add: '/Admin/add', edit: '/Admin/edit', del: '/Admin/del', + /** 个人设置 */ + profile: '/Admin/profile', /** 登录管理员权限列表 */ auth: '/Admin/auth', } as const;