前端重构
This commit is contained in:
parent
b244c7f044
commit
d76655a30a
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "ant-design-pro",
|
||||
"version": "6.0.0",
|
||||
"name": "kratos-admin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "An out-of-box UI solution for enterprise applications",
|
||||
"repository": "git@github.com:ant-design/ant-design-pro.git",
|
||||
"description": "KRA - Kratos Admin Management System",
|
||||
"repository": "",
|
||||
"scripts": {
|
||||
"analyze": "cross-env ANALYZE=1 max build",
|
||||
"build": "max build",
|
||||
|
|
@ -35,12 +35,24 @@
|
|||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@ant-design/pro-components": "^2.8.9",
|
||||
"@ant-design/charts": "^2.2.1",
|
||||
"antd": "^6.0.0",
|
||||
"antd-style": "^3.7.0",
|
||||
"axios": "^1.8.2",
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"qs": "^6.13.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"screenfull": "^6.0.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/pro-cli": "^3.3.0",
|
||||
|
|
@ -50,9 +62,13 @@
|
|||
"@testing-library/react": "^16.3.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qs": "^6.9.17",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/spark-md5": "^3.0.4",
|
||||
"@umijs/lint": "^4.6.1",
|
||||
"@umijs/max": "^4.6.2",
|
||||
"cross-env": "^10.0.0",
|
||||
|
|
|
|||
|
|
@ -1,42 +1,11 @@
|
|||
/**
|
||||
* 权限配置 - 参考GVA的权限验证逻辑
|
||||
* @see https://umijs.org/docs/max/access#access
|
||||
*/
|
||||
|
||||
* */
|
||||
export default function access(
|
||||
initialState: { currentUser?: API.UserInfo } | undefined,
|
||||
initialState: { currentUser?: API.CurrentUser } | undefined,
|
||||
) {
|
||||
const { currentUser } = initialState ?? {};
|
||||
|
||||
// 获取当前用户的角色ID
|
||||
const authorityId = currentUser?.authorityId || currentUser?.authority?.authorityId;
|
||||
|
||||
// 超级管理员角色ID(与GVA保持一致,888为超级管理员)
|
||||
const isSuperAdmin = authorityId === 888;
|
||||
|
||||
// 普通管理员
|
||||
const isAdmin = !!currentUser && !!authorityId;
|
||||
|
||||
return {
|
||||
// 是否为超级管理员
|
||||
canSuperAdmin: isSuperAdmin,
|
||||
// 是否为管理员(已登录且有角色)
|
||||
canAdmin: isAdmin,
|
||||
// 是否已登录
|
||||
isLogin: !!currentUser,
|
||||
// 按钮权限检查函数 - 参考GVA的btnAuth
|
||||
canAccess: (btnKey: string) => {
|
||||
// 超级管理员拥有所有权限
|
||||
if (isSuperAdmin) return true;
|
||||
// 检查当前路由的按钮权限
|
||||
const btns = (currentUser as any)?.btns || {};
|
||||
return !!btns[btnKey];
|
||||
},
|
||||
canAdmin: currentUser && currentUser.access === 'admin',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮权限Hook - 参考GVA的useBtnAuth
|
||||
* 在组件中使用: const { canAccess } = useAccess();
|
||||
* 然后: canAccess('add') 检查是否有添加权限
|
||||
*/
|
||||
|
|
|
|||
147
web/src/app.tsx
147
web/src/app.tsx
|
|
@ -2,9 +2,11 @@ import {
|
|||
AvatarDropdown,
|
||||
AvatarName,
|
||||
Footer,
|
||||
Question,
|
||||
SelectLang,
|
||||
} from "@/components";
|
||||
import { getUserInfo } from "@/services/system/user";
|
||||
import { createAdminService } from "@/services/index";
|
||||
import { Admin } from "@/services/kratos/admin/v1/index";
|
||||
import { LinkOutlined } from "@ant-design/icons";
|
||||
import type { Settings as LayoutSettings } from "@ant-design/pro-components";
|
||||
import { SettingDrawer } from "@ant-design/pro-components";
|
||||
|
|
@ -17,42 +19,32 @@ const isDev = process.env.NODE_ENV === "development";
|
|||
const isDevOrTest = isDev || process.env.CI;
|
||||
const loginPath = "/user/login";
|
||||
|
||||
/**
|
||||
* 获取用户信息 - 参考GVA的GetUserInfo
|
||||
*/
|
||||
const fetchUserInfo = async (): Promise<API.UserInfo | undefined> => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const res = await getUserInfo();
|
||||
// 与GVA一致,code === 0 表示成功
|
||||
if (res.code === 0 && res.data?.userInfo) {
|
||||
// 同步更新localStorage中的userInfo - 与GVA一致
|
||||
localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo));
|
||||
return res.data.userInfo;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
// 错误已在requestErrorConfig中处理,这里不需要额外处理
|
||||
// 401错误会弹出Modal,用户点击确定后跳转登录页
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const adminService = createAdminService();
|
||||
|
||||
/**
|
||||
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
*/
|
||||
* */
|
||||
export async function getInitialState(): Promise<{
|
||||
settings?: Partial<LayoutSettings> & { showWatermark?: boolean };
|
||||
currentUser?: API.UserInfo;
|
||||
settings?: Partial<LayoutSettings>;
|
||||
currentUser?: API.CurrentUser;
|
||||
loading?: boolean;
|
||||
fetchUserInfo?: () => Promise<API.UserInfo | undefined>;
|
||||
fetchUserInfo?: () => Promise<Admin | undefined>;
|
||||
}> {
|
||||
// 如果不是登录页面,获取用户信息 - 与GVA的permission.js逻辑一致
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
return await adminService.Current({});
|
||||
} catch (error) {
|
||||
history.push(loginPath);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
// 如果不是登录页面,执行
|
||||
const { location } = history;
|
||||
if (![loginPath, "/user/register", "/user/register-result"].includes(location.pathname)) {
|
||||
if (
|
||||
![loginPath, "/user/register", "/user/register-result"].includes(
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
const currentUser = await fetchUserInfo();
|
||||
return {
|
||||
fetchUserInfo,
|
||||
|
|
@ -65,3 +57,98 @@ export async function getInitialState(): Promise<{
|
|||
settings: defaultSettings as Partial<LayoutSettings>,
|
||||
};
|
||||
}
|
||||
|
||||
// ProLayout 支持的api https://procomponents.ant.design/components/layout
|
||||
export const layout: RunTimeLayoutConfig = ({
|
||||
initialState,
|
||||
setInitialState,
|
||||
}) => {
|
||||
return {
|
||||
actionsRender: () => [
|
||||
<Question key="doc" />,
|
||||
<SelectLang key="SelectLang" />,
|
||||
],
|
||||
avatarProps: {
|
||||
src: initialState?.currentUser?.avatar,
|
||||
title: <AvatarName />,
|
||||
render: (_, avatarChildren) => (
|
||||
<AvatarDropdown>{avatarChildren}</AvatarDropdown>
|
||||
),
|
||||
},
|
||||
waterMarkProps: {
|
||||
content: initialState?.currentUser?.name,
|
||||
},
|
||||
footerRender: () => <Footer />,
|
||||
onPageChange: () => {
|
||||
const { location } = history;
|
||||
// 如果没有登录,重定向到 login
|
||||
if (!initialState?.currentUser && location.pathname !== loginPath) {
|
||||
history.push(loginPath);
|
||||
}
|
||||
},
|
||||
bgLayoutImgList: [
|
||||
{
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr",
|
||||
left: 85,
|
||||
bottom: 100,
|
||||
height: "303px",
|
||||
},
|
||||
{
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr",
|
||||
bottom: -68,
|
||||
right: -45,
|
||||
height: "303px",
|
||||
},
|
||||
{
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "331px",
|
||||
},
|
||||
],
|
||||
links: isDevOrTest
|
||||
? [
|
||||
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
|
||||
<LinkOutlined />
|
||||
<span>OpenAPI 文档</span>
|
||||
</Link>,
|
||||
]
|
||||
: [],
|
||||
menuHeaderRender: undefined,
|
||||
// 自定义 403 页面
|
||||
// unAccessible: <div>unAccessible</div>,
|
||||
// 增加一个 loading 的状态
|
||||
childrenRender: (children) => {
|
||||
// if (initialState?.loading) return <PageLoading />;
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{isDevOrTest && (
|
||||
<SettingDrawer
|
||||
disableUrlParams
|
||||
enableDarkTheme
|
||||
settings={initialState?.settings}
|
||||
onSettingChange={(settings) => {
|
||||
setInitialState((preInitialState) => ({
|
||||
...preInitialState,
|
||||
settings,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
...initialState?.settings,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @name request 配置,可以配置错误处理
|
||||
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
|
||||
* @doc https://umijs.org/docs/max/request#配置
|
||||
*/
|
||||
export const request: RequestConfig = {
|
||||
baseURL: isDev ? "" : "https://proapi.azurewebsites.net",
|
||||
...errorConfig,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,31 +3,29 @@ import { DefaultFooter } from '@ant-design/pro-components';
|
|||
import React from 'react';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<DefaultFooter
|
||||
style={{
|
||||
background: 'none',
|
||||
}}
|
||||
copyright={`${currentYear} KRA Admin - Kratos React Admin`}
|
||||
copyright="Powered by Ant Desgin"
|
||||
links={[
|
||||
{
|
||||
key: 'KRA Admin',
|
||||
title: 'KRA Admin',
|
||||
href: 'https://github.com/your-org/kra',
|
||||
key: 'Ant Design Pro',
|
||||
title: 'Ant Design Pro',
|
||||
href: 'https://pro.ant.design',
|
||||
blankTarget: true,
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
title: <GithubOutlined />,
|
||||
href: 'https://github.com/your-org/kra',
|
||||
href: 'https://github.com/ant-design/ant-design-pro',
|
||||
blankTarget: true,
|
||||
},
|
||||
{
|
||||
key: 'Kratos',
|
||||
title: 'Powered by Kratos',
|
||||
href: 'https://go-kratos.dev',
|
||||
key: 'Ant Design',
|
||||
title: 'Ant Design',
|
||||
href: 'https://ant.design',
|
||||
blankTarget: true,
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface LogoProps {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const Logo: React.FC<LogoProps> = ({ collapsed }) => {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
}}>
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="KRA Admin"
|
||||
style={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
}}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span style={{
|
||||
marginLeft: 8,
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#3b82f6',
|
||||
}}>
|
||||
KRA Admin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import { createAdminService } from "@/services/index";
|
||||
import {
|
||||
LogoutOutlined,
|
||||
SwapOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { history, useModel } from "@umijs/max";
|
||||
import type { MenuProps } from "antd";
|
||||
import { message, Spin } from "antd";
|
||||
import { Spin } from "antd";
|
||||
import { createStyles } from "antd-style";
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import HeaderDropdown from "../HeaderDropdown";
|
||||
import { setUserAuthority } from "@/services/system/user";
|
||||
|
||||
const adminService = createAdminService();
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
|
|
@ -20,7 +22,7 @@ export type GlobalHeaderRightProps = {
|
|||
export const AvatarName = () => {
|
||||
const { initialState } = useModel("@@initialState");
|
||||
const { currentUser } = initialState || {};
|
||||
return <span className="anticon">{currentUser?.nickName || currentUser?.userName}</span>;
|
||||
return <span className="anticon">{currentUser?.name}</span>;
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
|
|
@ -42,21 +44,22 @@ const useStyles = createStyles(({ token }) => {
|
|||
});
|
||||
|
||||
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
||||
menu,
|
||||
children,
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
const { initialState, setInitialState } = useModel("@@initialState");
|
||||
|
||||
// 退出登录
|
||||
/**
|
||||
* 退出登录,并且将当前的 url 保存
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
await adminService.Logout({});
|
||||
const { search, pathname } = window.location;
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
const searchParams = new URLSearchParams({
|
||||
redirect: pathname + search,
|
||||
});
|
||||
/** 此方法会跳转到 redirect 参数所在的位置 */
|
||||
const redirect = urlParams.get("redirect");
|
||||
// Note: There may be security issues, please note
|
||||
if (window.location.pathname !== "/user/login" && !redirect) {
|
||||
history.replace({
|
||||
pathname: "/user/login",
|
||||
|
|
@ -64,21 +67,9 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
|||
});
|
||||
}
|
||||
};
|
||||
const { styles } = useStyles();
|
||||
|
||||
// 切换角色
|
||||
const changeUserAuth = async (authorityId: number) => {
|
||||
try {
|
||||
const res = await setUserAuthority({ authorityId });
|
||||
if (res.code === 0) {
|
||||
message.success('角色切换成功');
|
||||
window.sessionStorage.setItem('needCloseAll', 'true');
|
||||
window.sessionStorage.setItem('needToHome', 'true');
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('角色切换失败');
|
||||
}
|
||||
};
|
||||
const { initialState, setInitialState } = useModel("@@initialState");
|
||||
|
||||
const onMenuClick: MenuProps["onClick"] = (event) => {
|
||||
const { key } = event;
|
||||
|
|
@ -89,21 +80,18 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
|||
loginOut();
|
||||
return;
|
||||
}
|
||||
if (key === "center") {
|
||||
history.push('/person');
|
||||
return;
|
||||
}
|
||||
// 处理角色切换
|
||||
if (key.startsWith('authority_')) {
|
||||
const authorityId = parseInt(key.replace('authority_', ''), 10);
|
||||
changeUserAuth(authorityId);
|
||||
return;
|
||||
}
|
||||
history.push(`/account/${key}`);
|
||||
};
|
||||
|
||||
const loading = (
|
||||
<span className={styles.action}>
|
||||
<Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
|
||||
<Spin
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
@ -113,61 +101,34 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
|||
|
||||
const { currentUser } = initialState;
|
||||
|
||||
if (!currentUser || (!currentUser.nickName && !currentUser.userName)) {
|
||||
if (!currentUser || !currentUser.name) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
// 构建菜单项 - 参考GVA的角色切换功能
|
||||
const menuItems = useMemo(() => {
|
||||
const items: MenuProps['items'] = [];
|
||||
|
||||
// 当前角色显示
|
||||
if (currentUser.authority?.authorityName) {
|
||||
items.push({
|
||||
key: 'currentRole',
|
||||
label: <span style={{ fontWeight: 'bold' }}>当前角色:{currentUser.authority.authorityName}</span>,
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 可切换的其他角色
|
||||
if (currentUser.authorities && currentUser.authorities.length > 1) {
|
||||
const otherAuthorities = currentUser.authorities.filter(
|
||||
(auth: any) => auth.authorityId !== currentUser.authorityId
|
||||
);
|
||||
|
||||
if (otherAuthorities.length > 0) {
|
||||
items.push({ type: 'divider' });
|
||||
otherAuthorities.forEach((auth: any) => {
|
||||
items.push({
|
||||
key: `authority_${auth.authorityId}`,
|
||||
icon: <SwapOutlined />,
|
||||
label: `切换为:${auth.authorityName}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({ type: 'divider' });
|
||||
|
||||
// 个人信息
|
||||
items.push({
|
||||
key: 'center',
|
||||
icon: <UserOutlined />,
|
||||
label: '个人信息',
|
||||
});
|
||||
|
||||
items.push({ type: 'divider' });
|
||||
|
||||
// 退出登录
|
||||
items.push({
|
||||
key: 'logout',
|
||||
const menuItems = [
|
||||
...(menu
|
||||
? [
|
||||
{
|
||||
key: "center",
|
||||
icon: <UserOutlined />,
|
||||
label: "个人中心",
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
icon: <SettingOutlined />,
|
||||
label: "个人设置",
|
||||
},
|
||||
{
|
||||
type: "divider" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "logout",
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [currentUser]);
|
||||
label: "退出登录",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<HeaderDropdown
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
* 布局组件
|
||||
*/
|
||||
import Footer from './Footer';
|
||||
import Logo from './Logo';
|
||||
import { Question, SelectLang } from './RightContent';
|
||||
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
|
||||
|
||||
export { AvatarDropdown, AvatarName, Footer, Logo, Question, SelectLang };
|
||||
export { AvatarDropdown, AvatarName, Footer, Question, SelectLang };
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
/**
|
||||
* 404 页面
|
||||
*/
|
||||
import { Button, Result } from 'antd';
|
||||
import { history } from '@umijs/max';
|
||||
import { history, useIntl } from '@umijs/max';
|
||||
import { Button, Card, Result } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const NotFoundPage: React.FC = () => {
|
||||
return (
|
||||
const NoFoundPage: React.FC = () => (
|
||||
<Card variant="borderless">
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="抱歉,您访问的页面不存在"
|
||||
subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => history.push('/')}>
|
||||
返回首页
|
||||
{useIntl().formatMessage({ id: 'pages.404.buttonText' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
export default NoFoundPage;
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
.dashboard {
|
||||
:global {
|
||||
.ant-page-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.statContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.statInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.statTrend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.statChart {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.quickCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 280px;
|
||||
|
||||
.shortcutItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f5ff;
|
||||
|
||||
.shortcutIcon {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcutIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.shortcutTitle {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.noticeCard,
|
||||
.docCard,
|
||||
.sysCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.sysCard {
|
||||
.sysItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.statCard {
|
||||
.statChart {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quickCard {
|
||||
.shortcutItem {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.shortcutIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shortcutTitle {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
/**
|
||||
* 仪表盘页面
|
||||
* 参考 GVA 的 Dashboard 布局
|
||||
*/
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Col, Row, Statistic, Tag, List, Typography, Progress, Space, Tooltip } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
ApiOutlined,
|
||||
MenuOutlined,
|
||||
RiseOutlined,
|
||||
SettingOutlined,
|
||||
DatabaseOutlined,
|
||||
LinkOutlined,
|
||||
SafetyOutlined,
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 统计卡片数据
|
||||
const statsData = [
|
||||
{ title: '访问人数', value: 268500, icon: <UserOutlined />, trend: '+80%', color: '#1890ff', percent: 80 },
|
||||
{ title: '新增客户', value: 12580, icon: <TeamOutlined />, trend: '+25%', color: '#52c41a', percent: 65 },
|
||||
{ title: '解决数量', value: 8846, icon: <ApiOutlined />, trend: '+15%', color: '#722ed1', percent: 45 },
|
||||
];
|
||||
|
||||
// 快捷功能
|
||||
const shortcuts = [
|
||||
{ icon: <MenuOutlined />, title: '菜单管理', path: '/system/menu' },
|
||||
{ icon: <ApiOutlined />, title: 'API管理', path: '/system/api' },
|
||||
{ icon: <TeamOutlined />, title: '角色管理', path: '/system/role' },
|
||||
{ icon: <UserOutlined />, title: '用户管理', path: '/system/user' },
|
||||
{ icon: <DatabaseOutlined />, title: '字典管理', path: '/system/dict' },
|
||||
{ icon: <SettingOutlined />, title: '系统配置', path: '/system/config' },
|
||||
];
|
||||
|
||||
// 公告数据
|
||||
const notices = [
|
||||
{ type: 'processing', typeTitle: '公告', title: 'Kratos Admin v1.0 正式发布,欢迎使用!' },
|
||||
{ type: 'success', typeTitle: '通知', title: '系统已完成安全升级,请放心使用。' },
|
||||
{ type: 'warning', typeTitle: '警告', title: '请定期修改密码,确保账户安全。' },
|
||||
{ type: 'error', typeTitle: '重要', title: '数据库将于本周日凌晨进行维护。' },
|
||||
{ type: 'default', typeTitle: '信息', title: '感谢您对 Kratos Admin 的支持!' },
|
||||
];
|
||||
|
||||
// 内容数据
|
||||
const contentData = [
|
||||
{ name: '用户', value: 128, color: '#1890ff' },
|
||||
{ name: '角色', value: 8, color: '#52c41a' },
|
||||
{ name: 'API', value: 256, color: '#722ed1' },
|
||||
{ name: '菜单', value: 32, color: '#faad14' },
|
||||
{ name: '字典', value: 15, color: '#13c2c2' },
|
||||
{ name: '部门', value: 12, color: '#eb2f96' },
|
||||
];
|
||||
|
||||
// 文档链接
|
||||
const docLinks = [
|
||||
{ title: 'Kratos 官方文档', url: 'https://go-kratos.dev/' },
|
||||
{ title: 'Ant Design Pro', url: 'https://pro.ant.design/' },
|
||||
{ title: 'React 官方文档', url: 'https://react.dev/' },
|
||||
{ title: 'Go 语言文档', url: 'https://go.dev/doc/' },
|
||||
];
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<PageContainer
|
||||
header={{ title: '' }}
|
||||
className={styles.dashboard}
|
||||
>
|
||||
{/* 统计卡片行 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{statsData.map((item, index) => (
|
||||
<Col xs={24} sm={12} lg={8} key={index}>
|
||||
<Card className={styles.statCard} bordered={false}>
|
||||
<div className={styles.statContent}>
|
||||
<div className={styles.statInfo}>
|
||||
<Text type="secondary">{item.title}</Text>
|
||||
<div className={styles.statValue}>
|
||||
<Statistic value={item.value} valueStyle={{ fontSize: 28, fontWeight: 600 }} />
|
||||
</div>
|
||||
<div className={styles.statTrend}>
|
||||
<RiseOutlined style={{ color: '#52c41a' }} />
|
||||
<Text style={{ color: '#52c41a', marginLeft: 4 }}>{item.trend}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.statChart}>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={item.percent}
|
||||
size={80}
|
||||
strokeColor={item.color}
|
||||
format={() => <span style={{ fontSize: 14 }}>{item.percent}%</span>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
{/* 左侧内容数据 */}
|
||||
<Col xs={24} lg={18}>
|
||||
<Card title="内容数据" bordered={false} className={styles.chartCard}>
|
||||
<Row gutter={[16, 24]}>
|
||||
{contentData.map((item, index) => (
|
||||
<Col xs={12} sm={8} md={4} key={index}>
|
||||
<div className={styles.dataItem}>
|
||||
<div className={styles.dataValue} style={{ color: item.color }}>
|
||||
{item.value}
|
||||
</div>
|
||||
<div className={styles.dataName}>{item.name}</div>
|
||||
<Progress
|
||||
percent={Math.min((item.value / 300) * 100, 100)}
|
||||
showInfo={false}
|
||||
strokeColor={item.color}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<div className={styles.progressSection}>
|
||||
<div className={styles.progressTitle}>系统资源使用情况</div>
|
||||
<Row gutter={[32, 16]} style={{ marginTop: 16 }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.progressItem}>
|
||||
<div className={styles.progressLabel}>
|
||||
<CloudServerOutlined /> CPU 使用率
|
||||
</div>
|
||||
<Progress percent={45} strokeColor="#1890ff" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.progressItem}>
|
||||
<div className={styles.progressLabel}>
|
||||
<DatabaseOutlined /> 内存使用率
|
||||
</div>
|
||||
<Progress percent={68} strokeColor="#52c41a" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.progressItem}>
|
||||
<div className={styles.progressLabel}>
|
||||
<SafetyOutlined /> 磁盘使用率
|
||||
</div>
|
||||
<Progress percent={32} strokeColor="#722ed1" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.progressItem}>
|
||||
<div className={styles.progressLabel}>
|
||||
<ApiOutlined /> API 调用量
|
||||
</div>
|
||||
<Progress percent={85} strokeColor="#faad14" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧快捷功能 */}
|
||||
<Col xs={24} lg={6}>
|
||||
<Card title="快捷功能" bordered={false} className={styles.quickCard}>
|
||||
<Row gutter={[8, 16]}>
|
||||
{shortcuts.map((item, index) => (
|
||||
<Col span={8} key={index}>
|
||||
<div
|
||||
className={styles.shortcutItem}
|
||||
onClick={() => history.push(item.path)}
|
||||
>
|
||||
<div className={styles.shortcutIcon}>{item.icon}</div>
|
||||
<Text className={styles.shortcutTitle}>{item.title}</Text>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
{/* 公告 */}
|
||||
<Col xs={24} md={12} lg={8}>
|
||||
<Card
|
||||
title="公告"
|
||||
bordered={false}
|
||||
extra={<a>查看更多</a>}
|
||||
className={styles.noticeCard}
|
||||
>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={notices}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ padding: '8px 0', border: 'none' }}>
|
||||
<Space>
|
||||
<Tag color={item.type}>{item.typeTitle}</Tag>
|
||||
<Tooltip title={item.title}>
|
||||
<Text ellipsis style={{ maxWidth: 200 }}>{item.title}</Text>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 文档链接 */}
|
||||
<Col xs={24} md={12} lg={8}>
|
||||
<Card
|
||||
title="文档"
|
||||
bordered={false}
|
||||
extra={<a>查看更多</a>}
|
||||
className={styles.docCard}
|
||||
>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={docLinks}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ padding: '8px 0', border: 'none' }}>
|
||||
<a href={item.url} target="_blank" rel="noopener noreferrer">
|
||||
<Space>
|
||||
<LinkOutlined />
|
||||
<Text>{item.title}</Text>
|
||||
</Space>
|
||||
</a>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="系统信息" bordered={false} className={styles.sysCard}>
|
||||
<div className={styles.sysItem}>
|
||||
<Text type="secondary">系统名称</Text>
|
||||
<Text strong>Kratos Admin</Text>
|
||||
</div>
|
||||
<div className={styles.sysItem}>
|
||||
<Text type="secondary">后端框架</Text>
|
||||
<Text strong>Go Kratos</Text>
|
||||
</div>
|
||||
<div className={styles.sysItem}>
|
||||
<Text type="secondary">前端框架</Text>
|
||||
<Text strong>React + Ant Design Pro</Text>
|
||||
</div>
|
||||
<div className={styles.sysItem}>
|
||||
<Text type="secondary">版本号</Text>
|
||||
<Text strong>v1.0.0</Text>
|
||||
</div>
|
||||
<div className={styles.sysItem}>
|
||||
<Text type="secondary">服务器状态</Text>
|
||||
<Tag color="success">运行中</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
/**
|
||||
* 个人中心页面
|
||||
* 与 GVA person.vue 保持一致的功能
|
||||
*/
|
||||
import {
|
||||
EditOutlined,
|
||||
PhoneOutlined,
|
||||
MailOutlined,
|
||||
LockOutlined,
|
||||
UserOutlined,
|
||||
EnvironmentOutlined,
|
||||
BankOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import {
|
||||
Card,
|
||||
Avatar,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
message,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Tag,
|
||||
Timeline,
|
||||
Statistic,
|
||||
Tabs,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { setSelfInfo, changePassword } from '@/services/system/user';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const PersonPage: React.FC = () => {
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
const userInfo = initialState?.currentUser || {};
|
||||
|
||||
const [editNickName, setEditNickName] = useState(false);
|
||||
const [nickName, setNickName] = useState('');
|
||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
||||
const [phoneModalVisible, setPhoneModalVisible] = useState(false);
|
||||
const [emailModalVisible, setEmailModalVisible] = useState(false);
|
||||
const [phoneCountdown, setPhoneCountdown] = useState(0);
|
||||
const [emailCountdown, setEmailCountdown] = useState(0);
|
||||
|
||||
const [passwordForm] = Form.useForm();
|
||||
const [phoneForm] = Form.useForm();
|
||||
const [emailForm] = Form.useForm();
|
||||
|
||||
// 修改昵称
|
||||
const handleEditNickName = () => {
|
||||
setNickName(userInfo.nickName || '');
|
||||
setEditNickName(true);
|
||||
};
|
||||
|
||||
const handleSaveNickName = async () => {
|
||||
const res = await setSelfInfo({ nickName });
|
||||
if (res.code === 0) {
|
||||
message.success('修改成功');
|
||||
setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: { ...s?.currentUser, nickName },
|
||||
}));
|
||||
setEditNickName(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改密码
|
||||
const handleChangePassword = async (values: any) => {
|
||||
if (values.newPassword !== values.confirmPassword) {
|
||||
message.error('两次密码不一致');
|
||||
return;
|
||||
}
|
||||
const res = await changePassword({
|
||||
password: values.password,
|
||||
newPassword: values.newPassword,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('修改密码成功');
|
||||
setPasswordModalVisible(false);
|
||||
passwordForm.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取手机验证码
|
||||
const getPhoneCode = () => {
|
||||
setPhoneCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
setPhoneCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 修改手机号
|
||||
const handleChangePhone = async (values: any) => {
|
||||
const res = await setSelfInfo({ phone: values.phone });
|
||||
if (res.code === 0) {
|
||||
message.success('修改成功');
|
||||
setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: { ...s?.currentUser, phone: values.phone },
|
||||
}));
|
||||
setPhoneModalVisible(false);
|
||||
phoneForm.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取邮箱验证码
|
||||
const getEmailCode = () => {
|
||||
setEmailCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
setEmailCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 修改邮箱
|
||||
const handleChangeEmail = async (values: any) => {
|
||||
const res = await setSelfInfo({ email: values.email });
|
||||
if (res.code === 0) {
|
||||
message.success('修改成功');
|
||||
setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: { ...s?.currentUser, email: values.email },
|
||||
}));
|
||||
setEmailModalVisible(false);
|
||||
emailForm.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 活动数据
|
||||
const activities = [
|
||||
{ timestamp: '2024-01-10', title: '完成项目里程碑', content: '成功完成第三季度主要项目开发任务', color: 'blue' },
|
||||
{ timestamp: '2024-01-11', title: '代码审核完成', content: '完成核心模块代码审核,提出多项改进建议', color: 'green' },
|
||||
{ timestamp: '2024-01-12', title: '技术分享会', content: '主持团队技术分享会,分享前端性能优化经验', color: 'orange' },
|
||||
{ timestamp: '2024-01-13', title: '新功能上线', content: '成功上线用户反馈的新特性', color: 'red' },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{/* 顶部个人信息卡片 */}
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<div style={{ background: '#f0f5ff', height: 120, marginBottom: -60, borderRadius: 8 }} />
|
||||
<Row gutter={24} align="middle" style={{ paddingLeft: 24 }}>
|
||||
<Col>
|
||||
<Avatar size={100} src={userInfo.headerImg} icon={<UserOutlined />} />
|
||||
</Col>
|
||||
<Col flex={1} style={{ paddingTop: 60 }}>
|
||||
<Space align="center" style={{ marginBottom: 16 }}>
|
||||
{!editNickName ? (
|
||||
<>
|
||||
<Title level={4} style={{ margin: 0 }}>{userInfo.nickName}</Title>
|
||||
<Button type="text" icon={<EditOutlined />} onClick={handleEditNickName} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
value={nickName}
|
||||
onChange={(e) => setNickName(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSaveNickName}>确认</Button>
|
||||
<Button onClick={() => setEditNickName(false)}>取消</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<Space size={24}>
|
||||
<Text type="secondary"><EnvironmentOutlined /> 中国·北京市·朝阳区</Text>
|
||||
<Text type="secondary"><BankOutlined /> 北京翻转极光科技有限公司</Text>
|
||||
<Text type="secondary"><UserOutlined /> 技术部·前端事业群</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Row gutter={24}>
|
||||
{/* 左侧信息栏 */}
|
||||
<Col span={8}>
|
||||
<Card title="基本信息" style={{ marginBottom: 24 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Space><PhoneOutlined style={{ color: '#1890ff' }} /><Text>手机号码:</Text><Text>{userInfo.phone || '未设置'}</Text></Space>
|
||||
<Button type="link" onClick={() => setPhoneModalVisible(true)}>修改</Button>
|
||||
</Row>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Space><MailOutlined style={{ color: '#52c41a' }} /><Text>邮箱地址:</Text><Text>{userInfo.email || '未设置'}</Text></Space>
|
||||
<Button type="link" onClick={() => setEmailModalVisible(true)}>修改</Button>
|
||||
</Row>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Space><LockOutlined style={{ color: '#722ed1' }} /><Text>账号密码:</Text><Text>已设置</Text></Space>
|
||||
<Button type="link" onClick={() => setPasswordModalVisible(true)}>修改</Button>
|
||||
</Row>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="技能特长">
|
||||
<Space wrap>
|
||||
<Tag color="success">GoLang</Tag>
|
||||
<Tag color="warning">JavaScript</Tag>
|
||||
<Tag color="error">Vue</Tag>
|
||||
<Tag color="default">Gorm</Tag>
|
||||
<Button type="dashed" size="small">+ 添加技能</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<Col span={16}>
|
||||
<Card>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'stats',
|
||||
label: '数据统计',
|
||||
children: (
|
||||
<Row gutter={24} style={{ padding: '24px 0' }}>
|
||||
<Col span={6}><Statistic title="项目参与" value={138} valueStyle={{ color: '#1890ff' }} /></Col>
|
||||
<Col span={6}><Statistic title="代码提交" value={2300} suffix="次" valueStyle={{ color: '#52c41a' }} /></Col>
|
||||
<Col span={6}><Statistic title="任务完成" value={95} suffix="%" valueStyle={{ color: '#722ed1' }} /></Col>
|
||||
<Col span={6}><Statistic title="获得勋章" value={12} valueStyle={{ color: '#faad14' }} /></Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'activities',
|
||||
label: '近期动态',
|
||||
children: (
|
||||
<Timeline style={{ padding: '24px 0' }} items={activities.map((item) => ({
|
||||
color: item.color,
|
||||
children: (
|
||||
<>
|
||||
<Text strong>{item.title}</Text>
|
||||
<br />
|
||||
<Text type="secondary">{item.content}</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item.timestamp}</Text>
|
||||
</>
|
||||
),
|
||||
}))} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 修改密码弹窗 */}
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={passwordModalVisible}
|
||||
onCancel={() => { setPasswordModalVisible(false); passwordForm.resetFields(); }}
|
||||
onOk={() => passwordForm.submit()}
|
||||
>
|
||||
<Form form={passwordForm} onFinish={handleChangePassword} layout="vertical">
|
||||
<Form.Item name="password" label="原密码" rules={[{ required: true }, { min: 6, message: '最少6个字符' }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item name="newPassword" label="新密码" rules={[{ required: true }, { min: 6, message: '最少6个字符' }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item name="confirmPassword" label="确认密码" rules={[
|
||||
{ required: true },
|
||||
{ min: 6, message: '最少6个字符' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) return Promise.resolve();
|
||||
return Promise.reject(new Error('两次密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 修改手机号弹窗 */}
|
||||
<Modal
|
||||
title="修改手机号"
|
||||
open={phoneModalVisible}
|
||||
onCancel={() => { setPhoneModalVisible(false); phoneForm.resetFields(); }}
|
||||
onOk={() => phoneForm.submit()}
|
||||
>
|
||||
<Form form={phoneForm} onFinish={handleChangePhone} layout="vertical">
|
||||
<Form.Item name="phone" label="手机号" rules={[{ required: true }]}>
|
||||
<Input prefix={<PhoneOutlined />} placeholder="请输入新的手机号码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="验证码" rules={[{ required: true }]}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder="请输入验证码[模拟]" style={{ flex: 1 }} />
|
||||
<Button type="primary" disabled={phoneCountdown > 0} onClick={getPhoneCode}>
|
||||
{phoneCountdown > 0 ? `${phoneCountdown}s` : '获取验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 修改邮箱弹窗 */}
|
||||
<Modal
|
||||
title="修改邮箱"
|
||||
open={emailModalVisible}
|
||||
onCancel={() => { setEmailModalVisible(false); emailForm.resetFields(); }}
|
||||
onOk={() => emailForm.submit()}
|
||||
>
|
||||
<Form form={emailForm} onFinish={handleChangeEmail} layout="vertical">
|
||||
<Form.Item name="email" label="邮箱" rules={[{ required: true }, { type: 'email', message: '请输入有效的邮箱地址' }]}>
|
||||
<Input prefix={<MailOutlined />} placeholder="请输入新的邮箱地址" />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="验证码" rules={[{ required: true }]}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder="请输入验证码[模拟]" style={{ flex: 1 }} />
|
||||
<Button type="primary" disabled={emailCountdown > 0} onClick={getEmailCode}>
|
||||
{emailCountdown > 0 ? `${emailCountdown}s` : '获取验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonPage;
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
/**
|
||||
* API管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space, Tag } from 'antd';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { getApiList, createApi, updateApi, deleteApi, syncApi, getApiGroups, freshCasbin } from '@/services/system/api';
|
||||
|
||||
const methodColors: Record<string, string> = {
|
||||
GET: 'green',
|
||||
POST: 'blue',
|
||||
PUT: 'orange',
|
||||
DELETE: 'red',
|
||||
PATCH: 'purple',
|
||||
};
|
||||
|
||||
const ApiPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.ApiItem>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [apiGroups, setApiGroups] = useState<string[]>([]);
|
||||
|
||||
const fetchApiGroups = async () => {
|
||||
const res = await getApiGroups();
|
||||
if (res.code === 0 && res.data?.groups) {
|
||||
setApiGroups(res.data.groups);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchApiGroups();
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
const res = await deleteApi({ ids });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
const res = await syncApi();
|
||||
if (res.code === 0) {
|
||||
message.success('同步成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFreshCasbin = async () => {
|
||||
const res = await freshCasbin();
|
||||
if (res.code === 0) {
|
||||
message.success('刷新成功');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.ApiItem>[] = [
|
||||
{ title: 'ID', dataIndex: 'ID', search: false },
|
||||
{ title: 'API路径', dataIndex: 'path' },
|
||||
{ title: 'API描述', dataIndex: 'description' },
|
||||
{ title: 'API分组', dataIndex: 'apiGroup', valueType: 'select', valueEnum: Object.fromEntries(apiGroups.map((g) => [g, g])) },
|
||||
{
|
||||
title: '请求方法',
|
||||
dataIndex: 'method',
|
||||
valueType: 'select',
|
||||
valueEnum: { GET: 'GET', POST: 'POST', PUT: 'PUT', DELETE: 'DELETE', PATCH: 'PATCH' },
|
||||
render: (_, record) => <Tag color={methodColors[record.method || '']}>{record.method}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}>编辑</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete([record.ID!])}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<API.ApiItem>
|
||||
headerTitle="API列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={columns}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}>新增API</Button>,
|
||||
<Button key="sync" icon={<SyncOutlined />} onClick={handleSync}>同步API</Button>,
|
||||
<Button key="fresh" onClick={handleFreshCasbin}>刷新缓存</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getApiList({ page: params.current, pageSize: params.pageSize, ...params });
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑API' : '新增API'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await updateApi({ ...currentRow, ...values });
|
||||
} else {
|
||||
res = await createApi(values);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="path" label="API路径" rules={[{ required: true }]} />
|
||||
<ProFormText name="description" label="API描述" rules={[{ required: true }]} />
|
||||
<ProFormSelect name="apiGroup" label="API分组" options={apiGroups.map((g) => ({ label: g, value: g }))} rules={[{ required: true }]} />
|
||||
<ProFormSelect name="method" label="请求方法" options={['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].map((m) => ({ label: m, value: m }))} rules={[{ required: true }]} />
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiPage;
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* 角色管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormDigit } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space, Tree, Card, Row, Col } from 'antd';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import {
|
||||
getAuthorityList,
|
||||
createAuthority,
|
||||
updateAuthority,
|
||||
deleteAuthority,
|
||||
copyAuthority,
|
||||
} from '@/services/system/authority';
|
||||
import { getBaseMenuTree, addMenuAuthority, getMenuAuthority } from '@/services/system/menu';
|
||||
import { getAllApis } from '@/services/system/api';
|
||||
import { updateCasbin, getPolicyPathByAuthorityId } from '@/services/system/casbin';
|
||||
|
||||
const AuthorityPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.Authority>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [menuTree, setMenuTree] = useState<any[]>([]);
|
||||
const [apiList, setApiList] = useState<API.ApiItem[]>([]);
|
||||
const [checkedMenuKeys, setCheckedMenuKeys] = useState<number[]>([]);
|
||||
const [checkedApiKeys, setCheckedApiKeys] = useState<string[]>([]);
|
||||
const [selectedAuthority, setSelectedAuthority] = useState<API.Authority>();
|
||||
|
||||
// 获取菜单树
|
||||
const fetchMenuTree = async () => {
|
||||
const res = await getBaseMenuTree();
|
||||
if (res.code === 0 && res.data?.menus) {
|
||||
setMenuTree(buildTreeData(res.data.menus));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取API列表
|
||||
const fetchApis = async () => {
|
||||
const res = await getAllApis({});
|
||||
if (res.code === 0 && res.data?.apis) {
|
||||
setApiList(res.data.apis);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建树形数据
|
||||
const buildTreeData = (data: API.Menu[]): any[] => {
|
||||
return data.map((item) => ({
|
||||
key: item.ID,
|
||||
title: item.meta?.title || item.name,
|
||||
children: item.children ? buildTreeData(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenuTree();
|
||||
fetchApis();
|
||||
}, []);
|
||||
|
||||
// 选择角色时加载权限
|
||||
const handleSelectAuthority = async (record: API.Authority) => {
|
||||
setSelectedAuthority(record);
|
||||
// 获取菜单权限
|
||||
const menuRes = await getMenuAuthority({ authorityId: record.authorityId! });
|
||||
if (menuRes.code === 0 && menuRes.data?.menus) {
|
||||
const menuIds = extractMenuIds(menuRes.data.menus);
|
||||
setCheckedMenuKeys(menuIds);
|
||||
}
|
||||
// 获取API权限
|
||||
const apiRes = await getPolicyPathByAuthorityId({ authorityId: record.authorityId! });
|
||||
if (apiRes.code === 0 && apiRes.data?.paths) {
|
||||
const apiKeys = apiRes.data.paths.map((p) => `${p.path}:${p.method}`);
|
||||
setCheckedApiKeys(apiKeys);
|
||||
}
|
||||
};
|
||||
|
||||
// 提取菜单ID
|
||||
const extractMenuIds = (menus: API.Menu[]): number[] => {
|
||||
const ids: number[] = [];
|
||||
const extract = (list: API.Menu[]) => {
|
||||
list.forEach((m) => {
|
||||
if (m.ID) ids.push(m.ID);
|
||||
if (m.children) extract(m.children);
|
||||
});
|
||||
};
|
||||
extract(menus);
|
||||
return ids;
|
||||
};
|
||||
|
||||
|
||||
// 保存菜单权限
|
||||
const handleSaveMenuAuth = async () => {
|
||||
if (!selectedAuthority) return;
|
||||
const menus = checkedMenuKeys.map((id) => ({ ID: id }));
|
||||
const res = await addMenuAuthority({
|
||||
menus: menus as API.Menu[],
|
||||
authorityId: selectedAuthority.authorityId!,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('菜单权限保存成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存API权限
|
||||
const handleSaveApiAuth = async () => {
|
||||
if (!selectedAuthority) return;
|
||||
const casbinInfos = checkedApiKeys.map((key) => {
|
||||
const [path, method] = key.split(':');
|
||||
return { path, method };
|
||||
});
|
||||
const res = await updateCasbin({
|
||||
authorityId: selectedAuthority.authorityId!,
|
||||
casbinInfos,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('API权限保存成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除角色
|
||||
const handleDelete = async (authorityId: number) => {
|
||||
const res = await deleteAuthority({ authorityId });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 拷贝角色
|
||||
const handleCopy = async (record: API.Authority) => {
|
||||
const res = await copyAuthority({
|
||||
authority: { ...record, authorityName: `${record.authorityName}_copy` },
|
||||
oldAuthorityId: record.authorityId!,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('拷贝成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.Authority>[] = [
|
||||
{ title: '角色ID', dataIndex: 'authorityId' },
|
||||
{ title: '角色名称', dataIndex: 'authorityName' },
|
||||
{ title: '父角色ID', dataIndex: 'parentId' },
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}>编辑</Button>
|
||||
<Button type="link" icon={<CopyOutlined />} onClick={() => handleCopy(record)}>拷贝</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.authorityId!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Button type="link" onClick={() => handleSelectAuthority(record)}>设置权限</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<ProTable<API.Authority>
|
||||
headerTitle="角色列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="authorityId"
|
||||
columns={columns}
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}>新增角色</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getAuthorityList({ page: params.current, pageSize: params.pageSize });
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{selectedAuthority && (
|
||||
<Card title={`权限设置 - ${selectedAuthority.authorityName}`}>
|
||||
<Card type="inner" title="菜单权限" extra={<Button onClick={handleSaveMenuAuth}>保存</Button>}>
|
||||
<Tree
|
||||
checkable
|
||||
treeData={menuTree}
|
||||
checkedKeys={checkedMenuKeys}
|
||||
onCheck={(keys) => setCheckedMenuKeys(keys as number[])}
|
||||
/>
|
||||
</Card>
|
||||
<Card type="inner" title="API权限" style={{ marginTop: 16 }} extra={<Button onClick={handleSaveApiAuth}>保存</Button>}>
|
||||
<Tree
|
||||
checkable
|
||||
treeData={apiList.map((api) => ({
|
||||
key: `${api.path}:${api.method}`,
|
||||
title: `${api.description} [${api.method}] ${api.path}`,
|
||||
}))}
|
||||
checkedKeys={checkedApiKeys}
|
||||
onCheck={(keys) => setCheckedApiKeys(keys as string[])}
|
||||
height={300}
|
||||
/>
|
||||
</Card>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑角色' : '新增角色'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await updateAuthority({ ...currentRow, ...values });
|
||||
} else {
|
||||
res = await createAuthority(values);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormDigit name="authorityId" label="角色ID" rules={[{ required: true }]} disabled={isEdit} />
|
||||
<ProFormText name="authorityName" label="角色名称" rules={[{ required: true }]} />
|
||||
<ProFormDigit name="parentId" label="父角色ID" initialValue={0} />
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthorityPage;
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
/**
|
||||
* 字典管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSwitch } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space, Card, Row, Col, Upload, Switch } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
getSysDictionaryList,
|
||||
createSysDictionary,
|
||||
updateSysDictionary,
|
||||
deleteSysDictionary,
|
||||
exportSysDictionary,
|
||||
importSysDictionary,
|
||||
} from '@/services/system/dictionary';
|
||||
import {
|
||||
getSysDictionaryDetailList,
|
||||
createSysDictionaryDetail,
|
||||
updateSysDictionaryDetail,
|
||||
deleteSysDictionaryDetail,
|
||||
} from '@/services/system/dictionaryDetail';
|
||||
|
||||
const DictionaryPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const detailActionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.Dictionary>();
|
||||
const [currentDetail, setCurrentDetail] = useState<API.DictionaryDetail>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [isDetailEdit, setIsDetailEdit] = useState(false);
|
||||
const [selectedDict, setSelectedDict] = useState<API.Dictionary>();
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteSysDictionary({ ID: id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteDetail = async (id: number) => {
|
||||
const res = await deleteSysDictionaryDetail({ ID: id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
detailActionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async (id: number) => {
|
||||
const res = await exportSysDictionary({ ID: id });
|
||||
if (res.code === 0 && res.data) {
|
||||
const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `dictionary_${id}.json`;
|
||||
a.click();
|
||||
message.success('导出成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
const res = await importSysDictionary(data);
|
||||
if (res.code === 0) {
|
||||
message.success('导入成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch {
|
||||
message.error('文件格式错误');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
const dictColumns: ProColumns<API.Dictionary>[] = [
|
||||
{ title: 'ID', dataIndex: 'ID', search: false },
|
||||
{ title: '字典名称', dataIndex: 'name' },
|
||||
{ title: '字典类型', dataIndex: 'type' },
|
||||
{ title: '描述', dataIndex: 'desc', search: false },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
search: false,
|
||||
render: (_, record) => <Switch checked={record.status} disabled />,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => setSelectedDict(record)}>详情</Button>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}>编辑</Button>
|
||||
<Button type="link" icon={<DownloadOutlined />} onClick={() => handleExport(record.ID!)}>导出</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const detailColumns: ProColumns<API.DictionaryDetail>[] = [
|
||||
{ title: 'ID', dataIndex: 'ID' },
|
||||
{ title: '标签', dataIndex: 'label' },
|
||||
{ title: '值', dataIndex: 'value' },
|
||||
{ title: '扩展', dataIndex: 'extend' },
|
||||
{ title: '排序', dataIndex: 'sort' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (_, record) => <Switch checked={record.status} disabled />,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => {
|
||||
setCurrentDetail(record);
|
||||
setIsDetailEdit(true);
|
||||
setDetailModalVisible(true);
|
||||
}}>编辑</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDeleteDetail(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<ProTable<API.Dictionary>
|
||||
headerTitle="字典列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={dictColumns}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}>新增字典</Button>,
|
||||
<Upload key="import" accept=".json" showUploadList={false} beforeUpload={handleImport}>
|
||||
<Button icon={<UploadOutlined />}>导入</Button>
|
||||
</Upload>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getSysDictionaryList({ page: params.current, pageSize: params.pageSize, ...params });
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{selectedDict && (
|
||||
<Card title={`字典详情 - ${selectedDict.name}`}>
|
||||
<ProTable<API.DictionaryDetail>
|
||||
actionRef={detailActionRef}
|
||||
rowKey="ID"
|
||||
columns={detailColumns}
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentDetail({ sysDictionaryID: selectedDict.ID });
|
||||
setIsDetailEdit(false);
|
||||
setDetailModalVisible(true);
|
||||
}}>新增详情</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getSysDictionaryDetailList({
|
||||
page: params.current,
|
||||
pageSize: params.pageSize,
|
||||
sysDictionaryID: selectedDict.ID,
|
||||
});
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑字典' : '新增字典'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await updateSysDictionary({ ...currentRow, ...values });
|
||||
} else {
|
||||
res = await createSysDictionary(values);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="name" label="字典名称" rules={[{ required: true }]} />
|
||||
<ProFormText name="type" label="字典类型" rules={[{ required: true }]} />
|
||||
<ProFormText name="desc" label="描述" />
|
||||
<ProFormSwitch name="status" label="状态" initialValue={true} />
|
||||
</ModalForm>
|
||||
|
||||
<ModalForm
|
||||
title={isDetailEdit ? '编辑详情' : '新增详情'}
|
||||
open={detailModalVisible}
|
||||
onOpenChange={setDetailModalVisible}
|
||||
initialValues={currentDetail}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isDetailEdit) {
|
||||
res = await updateSysDictionaryDetail({ ...currentDetail, ...values });
|
||||
} else {
|
||||
res = await createSysDictionaryDetail({ ...currentDetail, ...values });
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isDetailEdit ? '编辑成功' : '创建成功');
|
||||
setDetailModalVisible(false);
|
||||
detailActionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="label" label="标签" rules={[{ required: true }]} />
|
||||
<ProFormText name="value" label="值" rules={[{ required: true }]} />
|
||||
<ProFormText name="extend" label="扩展" />
|
||||
<ProFormText name="sort" label="排序" initialValue={0} />
|
||||
<ProFormSwitch name="status" label="状态" initialValue={true} />
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictionaryPage;
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/**
|
||||
* 菜单管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormDigit, ProFormSwitch, ProFormSelect } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { getMenuList, addBaseMenu, updateBaseMenu, deleteBaseMenu } from '@/services/system/menu';
|
||||
|
||||
const MenuPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.Menu>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteBaseMenu({ ID: id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.Menu>[] = [
|
||||
{ title: 'ID', dataIndex: 'ID', search: false },
|
||||
{ title: '路由名称', dataIndex: 'name' },
|
||||
{ title: '路由路径', dataIndex: 'path' },
|
||||
{ title: '组件路径', dataIndex: 'component', search: false },
|
||||
{ title: '排序', dataIndex: 'sort', search: false },
|
||||
{ title: '标题', dataIndex: ['meta', 'title'] },
|
||||
{ title: '图标', dataIndex: ['meta', 'icon'], search: false },
|
||||
{
|
||||
title: '隐藏',
|
||||
dataIndex: 'hidden',
|
||||
search: false,
|
||||
render: (_, record) => (record.hidden ? '是' : '否'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentRow({ parentId: record.ID });
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}>添加子菜单</Button>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}>编辑</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<API.Menu>
|
||||
headerTitle="菜单列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={columns}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setCurrentRow({ parentId: 0 });
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}>新增菜单</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getMenuList({ page: params.current, pageSize: params.pageSize });
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑菜单' : '新增菜单'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
onFinish={async (values) => {
|
||||
const data = {
|
||||
...values,
|
||||
meta: { title: values.title, icon: values.icon, keepAlive: values.keepAlive },
|
||||
};
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await updateBaseMenu({ ...currentRow, ...data });
|
||||
} else {
|
||||
res = await addBaseMenu(data);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormDigit name="parentId" label="父菜单ID" />
|
||||
<ProFormText name="path" label="路由路径" rules={[{ required: true }]} />
|
||||
<ProFormText name="name" label="路由名称" rules={[{ required: true }]} />
|
||||
<ProFormText name="component" label="组件路径" rules={[{ required: true }]} />
|
||||
<ProFormText name="title" label="菜单标题" rules={[{ required: true }]} />
|
||||
<ProFormText name="icon" label="菜单图标" />
|
||||
<ProFormDigit name="sort" label="排序" initialValue={0} />
|
||||
<ProFormSwitch name="hidden" label="是否隐藏" />
|
||||
<ProFormSwitch name="keepAlive" label="缓存页面" />
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuPage;
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
/**
|
||||
* 操作记录页面
|
||||
* 与 GVA sysOperationRecord.vue 保持一致的功能
|
||||
*/
|
||||
import { DeleteOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space, Tag, Popover, Typography } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
getSysOperationRecordList,
|
||||
deleteSysOperationRecord,
|
||||
deleteSysOperationRecordByIds,
|
||||
} from '@/services/system/operationRecord';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const OperationRecordPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
|
||||
// 格式化JSON显示
|
||||
const formatBody = (value: string) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除单条记录
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteSysOperationRecord({ ID: id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请选择要删除的记录');
|
||||
return;
|
||||
}
|
||||
const res = await deleteSysOperationRecordByIds({ ids: selectedRowKeys });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.OperationRecord>[] = [
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: ['user', 'userName'],
|
||||
width: 140,
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
<span>{record.user?.userName}({record.user?.nickName})</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'CreatedAt',
|
||||
width: 180,
|
||||
search: false,
|
||||
render: (_, record) => dayjs(record.CreatedAt).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '状态码',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (_, record) => <Tag color="success">{record.status}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '请求IP',
|
||||
dataIndex: 'ip',
|
||||
width: 120,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
dataIndex: 'method',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '请求路径',
|
||||
dataIndex: 'path',
|
||||
width: 240,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '请求',
|
||||
dataIndex: 'body',
|
||||
width: 80,
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
record.body ? (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ maxHeight: 400, maxWidth: 400, overflow: 'auto', background: '#112435', padding: 12, borderRadius: 4 }}>
|
||||
<pre style={{ color: '#f08047', margin: 0, fontSize: 12 }}>{formatBody(record.body)}</pre>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
>
|
||||
<Button type="link" size="small">查看</Button>
|
||||
</Popover>
|
||||
) : <Text type="secondary">无</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '响应',
|
||||
dataIndex: 'resp',
|
||||
width: 80,
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
record.resp ? (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ maxHeight: 400, maxWidth: 400, overflow: 'auto', background: '#112435', padding: 12, borderRadius: 4 }}>
|
||||
<pre style={{ color: '#f08047', margin: 0, fontSize: 12 }}>{formatBody(record.resp)}</pre>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
>
|
||||
<Button type="link" size="small">查看</Button>
|
||||
</Popover>
|
||||
) : <Text type="secondary">无</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<API.OperationRecord>
|
||||
headerTitle="操作记录"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={columns}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as number[]),
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Popconfirm
|
||||
key="batchDelete"
|
||||
title="确定删除选中的记录?"
|
||||
onConfirm={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getSysOperationRecordList({
|
||||
page: params.current,
|
||||
pageSize: params.pageSize,
|
||||
method: params.method,
|
||||
path: params.path,
|
||||
status: params.status ? Number(params.status) : undefined,
|
||||
});
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '30', '50', '100'],
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperationRecordPage;
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
/**
|
||||
* 系统参数管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Space } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
getSysParamsList,
|
||||
createSysParams,
|
||||
updateSysParams,
|
||||
deleteSysParams,
|
||||
deleteSysParamsByIds,
|
||||
} from '@/services/system/params';
|
||||
|
||||
const ParamsPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.SysParams>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteSysParams({ ID: id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请选择要删除的记录');
|
||||
return;
|
||||
}
|
||||
const res = await deleteSysParamsByIds({ 'IDs[]': selectedRowKeys });
|
||||
if (res.code === 0) {
|
||||
message.success('批量删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.SysParams>[] = [
|
||||
{ title: 'ID', dataIndex: 'ID', search: false, width: 80 },
|
||||
{ title: '参数名', dataIndex: 'name', width: 150 },
|
||||
{ title: '参数键', dataIndex: 'key', width: 150 },
|
||||
{ title: '参数值', dataIndex: 'value', ellipsis: true, search: false },
|
||||
{ title: '描述', dataIndex: 'desc', ellipsis: true, search: false },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', search: false, width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<API.SysParams>
|
||||
headerTitle="参数列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={columns}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as number[]),
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
新增参数
|
||||
</Button>,
|
||||
<Popconfirm key="batch" title="确定批量删除?" onConfirm={handleBatchDelete}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getSysParamsList({
|
||||
page: params.current,
|
||||
pageSize: params.pageSize,
|
||||
name: params.name,
|
||||
key: params.key,
|
||||
});
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑参数' : '新增参数'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
modalProps={{ destroyOnClose: true }}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await updateSysParams({ ...currentRow, ...values });
|
||||
} else {
|
||||
res = await createSysParams(values);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="name" label="参数名" rules={[{ required: true, message: '请输入参数名' }]} />
|
||||
<ProFormText name="key" label="参数键" rules={[{ required: true, message: '请输入参数键' }]} />
|
||||
<ProFormTextArea name="value" label="参数值" rules={[{ required: true, message: '请输入参数值' }]} />
|
||||
<ProFormTextArea name="desc" label="描述" />
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamsPage;
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/**
|
||||
* 系统状态页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Row, Col, Progress, Statistic, Spin, Button } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getServerInfo } from '@/services/system/systemConfig';
|
||||
|
||||
const StatePage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [serverInfo, setServerInfo] = useState<API.ServerInfo>();
|
||||
|
||||
const fetchServerInfo = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getServerInfo();
|
||||
if (res.code === 0 && res.data?.server) {
|
||||
setServerInfo(res.data.server);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServerInfo();
|
||||
}, []);
|
||||
|
||||
const getCpuAvg = () => {
|
||||
if (!serverInfo?.cpu?.cpus?.length) return 0;
|
||||
const sum = serverInfo.cpu.cpus.reduce((a, b) => a + b, 0);
|
||||
return Math.round(sum / serverInfo.cpu.cpus.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchServerInfo} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Card title="操作系统信息">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Statistic title="操作系统" value={serverInfo?.os?.goos || '-'} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="CPU核心数" value={serverInfo?.os?.numCpu || 0} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="Go版本" value={serverInfo?.os?.goVersion || '-'} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="Goroutine数" value={serverInfo?.os?.numGoroutine || 0} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card title="CPU使用率">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={getCpuAvg()}
|
||||
format={(percent) => `${percent}%`}
|
||||
status={getCpuAvg() > 80 ? 'exception' : 'normal'}
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Statistic title="CPU核心数" value={serverInfo?.cpu?.cores || 0} />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card title="内存使用">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.ram?.usedPercent || 0}
|
||||
format={(percent) => `${percent}%`}
|
||||
status={(serverInfo?.ram?.usedPercent || 0) > 80 ? 'exception' : 'normal'}
|
||||
/>
|
||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||
<Col span={12}>
|
||||
<Statistic title="已用内存" value={serverInfo?.ram?.usedMb || 0} suffix="MB" />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="总内存" value={serverInfo?.ram?.totalMb || 0} suffix="MB" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card title="磁盘使用">
|
||||
{serverInfo?.disk?.map((d, i) => (
|
||||
<div key={i} style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>{d.mountPoint}</div>
|
||||
<Progress
|
||||
percent={d.usedPercent}
|
||||
status={d.usedPercent > 80 ? 'exception' : 'normal'}
|
||||
/>
|
||||
<Row gutter={16} style={{ marginTop: 8 }}>
|
||||
<Col span={12}>
|
||||
<Statistic title="已用" value={d.usedGb} suffix="GB" valueStyle={{ fontSize: 14 }} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="总计" value={d.totalGb} suffix="GB" valueStyle={{ fontSize: 14 }} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Spin>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatePage;
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
/**
|
||||
* 用户管理页面
|
||||
* 与 GVA 保持一致的功能
|
||||
*/
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, KeyOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { Button, message, Popconfirm, Switch, Avatar, Space, Modal, Input, Cascader } from 'antd';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import {
|
||||
getUserList,
|
||||
register,
|
||||
deleteUser,
|
||||
setUserInfo,
|
||||
setUserAuthorities,
|
||||
resetPassword,
|
||||
} from '@/services/system/user';
|
||||
import { getAuthorityList } from '@/services/system/authority';
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [resetPwdVisible, setResetPwdVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.UserInfo>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [authOptions, setAuthOptions] = useState<any[]>([]);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
|
||||
// 获取角色选项
|
||||
const fetchAuthorities = async () => {
|
||||
const res = await getAuthorityList({ page: 1, pageSize: 100 });
|
||||
if (res.code === 0 && res.data?.list) {
|
||||
const options = buildAuthOptions(res.data.list);
|
||||
setAuthOptions(options);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建角色级联选项
|
||||
const buildAuthOptions = (data: API.Authority[]): any[] => {
|
||||
return data.map((item) => ({
|
||||
value: item.authorityId,
|
||||
label: item.authorityName,
|
||||
children: item.children ? buildAuthOptions(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAuthorities();
|
||||
}, []);
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteUser({ id });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 切换启用状态
|
||||
const handleSwitchEnable = async (record: API.UserInfo) => {
|
||||
const res = await setUserInfo({
|
||||
...record,
|
||||
enable: record.enable === 1 ? 2 : 1,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success(record.enable === 1 ? '禁用成功' : '启用成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 修改角色
|
||||
const handleChangeAuthority = async (record: API.UserInfo, authorityIds: number[]) => {
|
||||
const res = await setUserAuthorities({
|
||||
uuid: record.uuid!,
|
||||
authorityIds,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('角色设置成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 生成随机密码
|
||||
const generatePassword = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let pwd = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
pwd += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setNewPassword(pwd);
|
||||
navigator.clipboard.writeText(pwd).then(() => {
|
||||
message.success('密码已复制到剪贴板');
|
||||
});
|
||||
};
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = async () => {
|
||||
if (!newPassword) {
|
||||
message.warning('请输入或生成密码');
|
||||
return;
|
||||
}
|
||||
const res = await resetPassword({ id: currentRow!.ID! });
|
||||
if (res.code === 0) {
|
||||
message.success('密码重置成功');
|
||||
setResetPwdVisible(false);
|
||||
setNewPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.UserInfo>[] = [
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'headerImg',
|
||||
search: false,
|
||||
render: (_, record) => <Avatar src={record.headerImg} />,
|
||||
},
|
||||
{ title: 'ID', dataIndex: 'ID', search: false },
|
||||
{ title: '用户名', dataIndex: 'userName' },
|
||||
{ title: '昵称', dataIndex: 'nickName' },
|
||||
{ title: '手机号', dataIndex: 'phone' },
|
||||
{ title: '邮箱', dataIndex: 'email' },
|
||||
{
|
||||
title: '用户角色',
|
||||
dataIndex: 'authorityIds',
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
<Cascader
|
||||
options={authOptions}
|
||||
value={record.authorities?.map((a) => a.authorityId)}
|
||||
onChange={(value) => handleChangeAuthority(record, value as number[])}
|
||||
multiple
|
||||
maxTagCount="responsive"
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '启用',
|
||||
dataIndex: 'enable',
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
<Switch
|
||||
checked={record.enable === 1}
|
||||
onChange={() => handleSwitchEnable(record)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setIsEdit(true);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<KeyOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setResetPwdVisible(true);
|
||||
}}
|
||||
>
|
||||
重置密码
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<API.UserInfo>
|
||||
headerTitle="用户列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="ID"
|
||||
columns={columns}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setIsEdit(false);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
新增用户
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getUserList({
|
||||
page: params.current,
|
||||
pageSize: params.pageSize,
|
||||
...params,
|
||||
});
|
||||
return {
|
||||
data: res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
success: res.code === 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModalForm
|
||||
title={isEdit ? '编辑用户' : '新增用户'}
|
||||
open={modalVisible}
|
||||
onOpenChange={setModalVisible}
|
||||
initialValues={currentRow}
|
||||
onFinish={async (values) => {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await setUserInfo({ ...currentRow, ...values });
|
||||
} else {
|
||||
res = await register(values);
|
||||
}
|
||||
if (res.code === 0) {
|
||||
message.success(isEdit ? '编辑成功' : '创建成功');
|
||||
setModalVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
message.error(res.msg || '操作失败');
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{!isEdit && (
|
||||
<>
|
||||
<ProFormText name="userName" label="用户名" rules={[{ required: true }, { min: 5 }]} />
|
||||
<ProFormText.Password name="password" label="密码" rules={[{ required: true }, { min: 6 }]} />
|
||||
</>
|
||||
)}
|
||||
<ProFormText name="nickName" label="昵称" rules={[{ required: true }]} />
|
||||
<ProFormText name="phone" label="手机号" />
|
||||
<ProFormText name="email" label="邮箱" />
|
||||
<ProFormSelect
|
||||
name="authorityIds"
|
||||
label="用户角色"
|
||||
mode="multiple"
|
||||
options={authOptions}
|
||||
rules={[{ required: true }]}
|
||||
/>
|
||||
</ModalForm>
|
||||
|
||||
<Modal
|
||||
title="重置密码"
|
||||
open={resetPwdVisible}
|
||||
onCancel={() => {
|
||||
setResetPwdVisible(false);
|
||||
setNewPassword('');
|
||||
}}
|
||||
onOk={handleResetPassword}
|
||||
>
|
||||
<p>用户: {currentRow?.userName}</p>
|
||||
<Space>
|
||||
<Input.Password
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
<Button onClick={generatePassword}>生成随机密码</Button>
|
||||
</Space>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPage;
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #194bfb;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.oblique {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
top: -15%;
|
||||
width: 60%;
|
||||
height: 130%;
|
||||
background: #fff;
|
||||
transform: rotate(-12deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 380px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #194bfb;
|
||||
}
|
||||
}
|
||||
|
||||
.inputIcon {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.captchaRow {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captchaInput {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.captchaImg {
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
background: #c3d4f2;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.loginBtn {
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: #194bfb;
|
||||
border-color: #194bfb;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(25, 75, 251, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: #1240d9;
|
||||
border-color: #1240d9;
|
||||
}
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: #194bfb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bannerContent {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
padding: 40px;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.featureItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oblique {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,192 +1,386 @@
|
|||
/**
|
||||
* 登录页面
|
||||
* 参考 GVA 的 UI 风格
|
||||
*/
|
||||
import { LockOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
|
||||
import { history, useModel } from '@umijs/max';
|
||||
import { message, Input, Button, Form, Spin } from 'antd';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { login, captcha } from '@/services/system/user';
|
||||
import styles from './index.less';
|
||||
import { Footer } from "@/components";
|
||||
import { getFakeCaptcha } from "@/services/ant-design-pro/login";
|
||||
import { createAdminService } from "@/services/index";
|
||||
import { LoginRequest } from "@/services/kratos/admin/v1/index";
|
||||
import {
|
||||
AlipayCircleOutlined,
|
||||
LockOutlined,
|
||||
MobileOutlined,
|
||||
TaobaoCircleOutlined,
|
||||
UserOutlined,
|
||||
WeiboCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
LoginForm,
|
||||
ProFormCaptcha,
|
||||
ProFormCheckbox,
|
||||
ProFormText,
|
||||
} from "@ant-design/pro-components";
|
||||
import {
|
||||
FormattedMessage,
|
||||
Helmet,
|
||||
SelectLang,
|
||||
useIntl,
|
||||
useModel,
|
||||
} from "@umijs/max";
|
||||
import { Alert, App, Tabs } from "antd";
|
||||
import { createStyles } from "antd-style";
|
||||
import React, { useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import Settings from "../../../../config/defaultSettings";
|
||||
|
||||
const adminService = createAdminService();
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
action: {
|
||||
marginLeft: "8px",
|
||||
color: "rgba(0, 0, 0, 0.2)",
|
||||
fontSize: "24px",
|
||||
verticalAlign: "middle",
|
||||
cursor: "pointer",
|
||||
transition: "color 0.3s",
|
||||
"&:hover": {
|
||||
color: token.colorPrimaryActive,
|
||||
},
|
||||
},
|
||||
lang: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
lineHeight: "42px",
|
||||
position: "fixed",
|
||||
right: 16,
|
||||
borderRadius: token.borderRadius,
|
||||
":hover": {
|
||||
backgroundColor: token.colorBgTextHover,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
overflow: "auto",
|
||||
backgroundImage:
|
||||
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
|
||||
backgroundSize: "100% 100%",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ActionIcons = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlipayCircleOutlined
|
||||
key="AlipayCircleOutlined"
|
||||
className={styles.action}
|
||||
/>
|
||||
<TaobaoCircleOutlined
|
||||
key="TaobaoCircleOutlined"
|
||||
className={styles.action}
|
||||
/>
|
||||
<WeiboCircleOutlined
|
||||
key="WeiboCircleOutlined"
|
||||
className={styles.action}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Lang = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.lang} data-lang>
|
||||
{SelectLang && <SelectLang />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginMessage: React.FC<{
|
||||
content: string;
|
||||
}> = ({ content }) => {
|
||||
return (
|
||||
<Alert
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
}}
|
||||
message={content}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { setInitialState } = useModel('@@initialState');
|
||||
const [form] = Form.useForm();
|
||||
const [captchaInfo, setCaptchaInfo] = useState<{
|
||||
captchaId: string;
|
||||
picPath: string;
|
||||
openCaptcha: boolean;
|
||||
captchaLength: number;
|
||||
}>({ captchaId: '', picPath: '', openCaptcha: false, captchaLength: 6 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userLoginState, setUserLoginState] = useState<API.LoginResult>({});
|
||||
const [type, setType] = useState<string>("account");
|
||||
const { initialState, setInitialState } = useModel("@@initialState");
|
||||
const { styles } = useStyles();
|
||||
const { message } = App.useApp();
|
||||
const intl = useIntl();
|
||||
|
||||
// 获取验证码
|
||||
const getCaptcha = async () => {
|
||||
const handleSubmit = async (req: LoginRequest) => {
|
||||
try {
|
||||
const res = await captcha();
|
||||
if (res.code === 0 && res.data) {
|
||||
setCaptchaInfo({
|
||||
captchaId: res.data.captchaId,
|
||||
picPath: res.data.picPath,
|
||||
openCaptcha: res.data.openCaptcha,
|
||||
captchaLength: res.data.captchaLength || 6,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCaptcha();
|
||||
}, []);
|
||||
|
||||
// 提交登录
|
||||
const handleSubmit = async (values: API.LoginParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await login({
|
||||
...values,
|
||||
captchaId: captchaInfo.captchaId,
|
||||
const userInfo = await adminService.Login(req);
|
||||
const defaultLoginSuccessMessage = intl.formatMessage({
|
||||
id: "pages.login.success",
|
||||
defaultMessage: "登录成功!",
|
||||
});
|
||||
if (res.code === 0 && res.data) {
|
||||
message.success('登录成功');
|
||||
localStorage.setItem('token', res.data.token);
|
||||
localStorage.setItem('userInfo', JSON.stringify(res.data.user));
|
||||
await setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: res.data?.user,
|
||||
message.success(defaultLoginSuccessMessage);
|
||||
// set user state
|
||||
flushSync(() => {
|
||||
setInitialState((state) => ({
|
||||
...state,
|
||||
currentUser: userInfo,
|
||||
}));
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
history.push(urlParams.get('redirect') || '/');
|
||||
} else {
|
||||
message.error(res.msg || '登录失败');
|
||||
getCaptcha();
|
||||
}
|
||||
});
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
window.location.href = urlParams.get("redirect") || "/";
|
||||
console.log(userInfo);
|
||||
} catch (error) {
|
||||
message.error('登录失败,请重试');
|
||||
getCaptcha();
|
||||
const defaultLoginFailureMessage = intl.formatMessage({
|
||||
id: "pages.login.failure",
|
||||
defaultMessage: "登录失败,请重试!",
|
||||
});
|
||||
console.log(error);
|
||||
message.error(defaultLoginFailureMessage);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const { status, type: loginType } = userLoginState;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{/* 左侧登录表单区域 */}
|
||||
<div className={styles.leftSection}>
|
||||
{/* 斜切装饰块 */}
|
||||
<div className={styles.oblique} />
|
||||
|
||||
<div className={styles.formWrapper}>
|
||||
<Spin spinning={loading}>
|
||||
{/* Logo 和标题 */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logo}>
|
||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="#194bfb">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
|
||||
stroke="#194bfb" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className={styles.title}>Kratos Admin</h1>
|
||||
<p className={styles.subtitle}>基于 Go Kratos 和 React 的后台管理系统</p>
|
||||
</div>
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage({
|
||||
id: "menu.login",
|
||||
defaultMessage: "登录页",
|
||||
})}
|
||||
{Settings.title && ` - ${Settings.title}`}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Lang />
|
||||
<div
|
||||
style={{
|
||||
flex: "1",
|
||||
padding: "32px 0",
|
||||
}}
|
||||
>
|
||||
<LoginForm
|
||||
contentStyle={{
|
||||
minWidth: 280,
|
||||
maxWidth: "75vw",
|
||||
}}
|
||||
logo={<img alt="logo" src="/logo.svg" />}
|
||||
title="Ant Design"
|
||||
subTitle={intl.formatMessage({
|
||||
id: "pages.layouts.userLayout.title",
|
||||
})}
|
||||
initialValues={{
|
||||
autoLogin: true,
|
||||
}}
|
||||
actions={[
|
||||
<FormattedMessage
|
||||
key="loginWith"
|
||||
id="pages.login.loginWith"
|
||||
defaultMessage="其他登录方式"
|
||||
/>,
|
||||
<ActionIcons key="icons" />,
|
||||
]}
|
||||
onFinish={async (values) => {
|
||||
await handleSubmit(values as LoginRequest);
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={type}
|
||||
onChange={setType}
|
||||
centered
|
||||
items={[
|
||||
{
|
||||
key: "account",
|
||||
label: intl.formatMessage({
|
||||
id: "pages.login.accountLogin.tab",
|
||||
defaultMessage: "账户密码登录",
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "mobile",
|
||||
label: intl.formatMessage({
|
||||
id: "pages.login.phoneLogin.tab",
|
||||
defaultMessage: "手机号登录",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 登录表单 */}
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{ username: 'admin' }}
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 5, message: '用户名至少5个字符' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className={styles.inputIcon} />}
|
||||
placeholder="请输入用户名"
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className={styles.inputIcon} />}
|
||||
placeholder="请输入密码"
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{captchaInfo.openCaptcha && (
|
||||
<Form.Item
|
||||
name="captcha"
|
||||
rules={[
|
||||
{ required: true, message: '请输入验证码' },
|
||||
{ pattern: /^\d+$/, message: '验证码须为数字' },
|
||||
]}
|
||||
>
|
||||
<div className={styles.captchaRow}>
|
||||
<Input
|
||||
prefix={<SafetyCertificateOutlined className={styles.inputIcon} />}
|
||||
placeholder="请输入验证码"
|
||||
className={styles.captchaInput}
|
||||
{status === "error" && loginType === "account" && (
|
||||
<LoginMessage
|
||||
content={intl.formatMessage({
|
||||
id: "pages.login.accountLogin.errorMessage",
|
||||
defaultMessage: "账户或密码错误(admin/ant.design)",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{type === "account" && (
|
||||
<>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <UserOutlined />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: "pages.login.username.placeholder",
|
||||
defaultMessage: "用户名: admin or user",
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.username.required"
|
||||
defaultMessage="请输入用户名!"
|
||||
/>
|
||||
<div className={styles.captchaImg} onClick={getCaptcha}>
|
||||
{captchaInfo.picPath && (
|
||||
<img src={captchaInfo.picPath} alt="验证码" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <LockOutlined />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: "pages.login.password.placeholder",
|
||||
defaultMessage: "密码: ant.design",
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.password.required"
|
||||
defaultMessage="请输入密码!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block className={styles.loginBtn}>
|
||||
登 录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
{status === "error" && loginType === "mobile" && (
|
||||
<LoginMessage content="验证码错误" />
|
||||
)}
|
||||
{type === "mobile" && (
|
||||
<>
|
||||
<ProFormText
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <MobileOutlined />,
|
||||
}}
|
||||
name="mobile"
|
||||
placeholder={intl.formatMessage({
|
||||
id: "pages.login.phoneNumber.placeholder",
|
||||
defaultMessage: "手机号",
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.phoneNumber.required"
|
||||
defaultMessage="请输入手机号!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
pattern: /^1\d{10}$/,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.phoneNumber.invalid"
|
||||
defaultMessage="手机号格式错误!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormCaptcha
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <LockOutlined />,
|
||||
}}
|
||||
captchaProps={{
|
||||
size: "large",
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: "pages.login.captcha.placeholder",
|
||||
defaultMessage: "请输入验证码",
|
||||
})}
|
||||
captchaTextRender={(timing, count) => {
|
||||
if (timing) {
|
||||
return `${count} ${intl.formatMessage({
|
||||
id: "pages.getCaptchaSecondText",
|
||||
defaultMessage: "获取验证码",
|
||||
})}`;
|
||||
}
|
||||
return intl.formatMessage({
|
||||
id: "pages.login.phoneLogin.getVerificationCode",
|
||||
defaultMessage: "获取验证码",
|
||||
});
|
||||
}}
|
||||
name="captcha"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.captcha.required"
|
||||
defaultMessage="请输入验证码!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
onGetCaptcha={async (phone) => {
|
||||
const result = await getFakeCaptcha({
|
||||
phone,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
message.success("获取验证码成功!验证码为:1234");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<ProFormCheckbox noStyle name="autoLogin">
|
||||
<FormattedMessage
|
||||
id="pages.login.rememberMe"
|
||||
defaultMessage="自动登录"
|
||||
/>
|
||||
</ProFormCheckbox>
|
||||
<a
|
||||
style={{
|
||||
float: "right",
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="pages.login.forgotPassword"
|
||||
defaultMessage="忘记密码"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧图片区域 */}
|
||||
<div className={styles.rightSection}>
|
||||
<div className={styles.bannerContent}>
|
||||
<h2>欢迎使用 Kratos Admin</h2>
|
||||
<p>高效、安全、易用的企业级后台管理系统</p>
|
||||
<div className={styles.features}>
|
||||
<div className={styles.featureItem}>
|
||||
<span className={styles.featureIcon}>🚀</span>
|
||||
<span>高性能</span>
|
||||
</div>
|
||||
<div className={styles.featureItem}>
|
||||
<span className={styles.featureIcon}>🔒</span>
|
||||
<span>安全可靠</span>
|
||||
</div>
|
||||
<div className={styles.featureItem}>
|
||||
<span className={styles.featureIcon}>📦</span>
|
||||
<span>开箱即用</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className={styles.footer}>
|
||||
<span>Kratos Admin © 2024 基于 Go Kratos 框架</span>
|
||||
</LoginForm>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,165 +1,54 @@
|
|||
/**
|
||||
* 请求配置 - 完全参考GVA的request.js实现
|
||||
* 包含请求拦截器、响应拦截器、错误处理
|
||||
*/
|
||||
import type { RequestOptions } from "@@/plugin-request/request";
|
||||
import type { RequestOptions } from "@@/plugin-request/request";
|
||||
import type { RequestConfig } from "@umijs/max";
|
||||
import { message, Modal } from "antd";
|
||||
import { history } from "@umijs/max";
|
||||
import { message as toast } from "antd";
|
||||
|
||||
const loginPath = "/user/login";
|
||||
|
||||
// 获取错误消息 - 与GVA的getErrorMessage一致
|
||||
function getErrorMessage(error: any): string {
|
||||
return error.response?.data?.msg || error.response?.statusText || '请求失败';
|
||||
// Define the structure of the expected response.
|
||||
interface ResponseStructure {
|
||||
code: number;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
metadata?: Map<string, number>;
|
||||
}
|
||||
|
||||
// 清除存储并跳转登录 - 与GVA的ClearStorage一致
|
||||
function clearStorageAndRedirect() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
localStorage.removeItem('originSetting');
|
||||
sessionStorage.clear();
|
||||
history.push({ pathname: loginPath });
|
||||
}
|
||||
|
||||
// 预设错误信息 - 与GVA的presetErrors一致
|
||||
const presetErrors: Record<string | number, { title: string; tips: string }> = {
|
||||
500: {
|
||||
title: '服务器发生内部错误',
|
||||
tips: '此类错误内容常见于后台panic,请先查看后台日志',
|
||||
},
|
||||
404: {
|
||||
title: '资源未找到',
|
||||
tips: '此类错误多为接口未注册或请求路径与api路径不符',
|
||||
},
|
||||
401: {
|
||||
title: '身份认证失败',
|
||||
tips: '您的身份认证已过期或无效,请重新登录',
|
||||
},
|
||||
network: {
|
||||
title: '网络错误',
|
||||
tips: '无法连接到服务器,请检查您的网络连接',
|
||||
},
|
||||
};
|
||||
|
||||
// 显示错误弹窗 - 与GVA的ErrorPreview组件逻辑一致
|
||||
function showErrorModal(code: string | number, errorMessage: string, onConfirm?: () => void) {
|
||||
const preset = presetErrors[code] || { title: '请求错误', tips: '请检查控制台获取更多信息' };
|
||||
|
||||
Modal.error({
|
||||
title: preset.title,
|
||||
content: (
|
||||
<div>
|
||||
<p style={{ marginBottom: 8 }}>{errorMessage}</p>
|
||||
<p style={{ color: '#666', fontSize: 12 }}>{preset.tips}</p>
|
||||
</div>
|
||||
),
|
||||
okText: '确定',
|
||||
onOk: () => {
|
||||
onConfirm?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 请求配置
|
||||
// Request configuration with error handling and interceptors.
|
||||
export const errorConfig: RequestConfig = {
|
||||
timeout: 99999, // 超时时间 - 与GVA保持一致
|
||||
|
||||
errorConfig: {
|
||||
// 错误抛出器 - 与GVA一致,code !== 0 时抛出错误
|
||||
errorThrower: (res) => {
|
||||
const { code, msg, message: errorMsg } = res as any;
|
||||
if (code !== 0) {
|
||||
const error: any = new Error(msg || errorMsg || '请求失败');
|
||||
error.info = { code, message: msg || errorMsg };
|
||||
const { code, reason, message } = res as unknown as ResponseStructure;
|
||||
if (code != 200) {
|
||||
const error: any = new Error(message);
|
||||
error.info = { reason, message };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 错误处理器 - 与GVA的response error拦截器一致
|
||||
errorHandler: (error: any, opts: any) => {
|
||||
if (opts?.skipErrorHandler) throw error;
|
||||
|
||||
// 网络错误 - 与GVA一致
|
||||
if (!error.response) {
|
||||
showErrorModal('network', getErrorMessage(error));
|
||||
return;
|
||||
if (error.response?.data) {
|
||||
const errorInfo: ResponseStructure | undefined = error.response?.data;
|
||||
if (errorInfo) {
|
||||
const { message, reason } = errorInfo;
|
||||
toast.error(reason + ": " + message);
|
||||
}
|
||||
} else if (error.response) {
|
||||
toast.error(`Response status: ${error.response.status}`);
|
||||
} else if (error.request) {
|
||||
toast.error("None response! Please retry.");
|
||||
} else {
|
||||
toast.error("Request error, please retry.");
|
||||
}
|
||||
|
||||
const status = error.response?.status;
|
||||
const errorMsg = getErrorMessage(error);
|
||||
|
||||
// 401 身份认证失败 - 与GVA一致,点击确定后清除存储并跳转
|
||||
if (status === 401) {
|
||||
showErrorModal(401, errorMsg, () => {
|
||||
clearStorageAndRedirect();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他HTTP错误 - 与GVA一致
|
||||
showErrorModal(status, errorMsg);
|
||||
},
|
||||
},
|
||||
|
||||
// 请求拦截器 - 与GVA的request拦截器一致
|
||||
requestInterceptors: [
|
||||
(config: RequestOptions) => {
|
||||
const token = localStorage.getItem('token') || '';
|
||||
let userId = '';
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
const user = JSON.parse(userInfo);
|
||||
userId = String(user.ID || user.id || '');
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse error
|
||||
}
|
||||
|
||||
// 设置请求头 - 与GVA完全一致
|
||||
config.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-token': token,
|
||||
'x-user-id': userId,
|
||||
...config.headers,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
],
|
||||
|
||||
// 响应拦截器 - 与GVA的response拦截器一致
|
||||
responseInterceptors: [
|
||||
(response: any) => {
|
||||
// 处理新token - 与GVA一致
|
||||
const newToken = response.headers?.['new-token'];
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
(response) => {
|
||||
if (response.status != 200) {
|
||||
toast.error(`Request error: ${response.status}`);
|
||||
}
|
||||
|
||||
const { data } = response;
|
||||
|
||||
// 如果没有code字段,直接返回 - 与GVA一致
|
||||
if (typeof data?.code === 'undefined') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// code === 0 或 headers.success === 'true' 表示成功 - 与GVA完全一致
|
||||
if (data.code === 0 || response.headers?.success === 'true') {
|
||||
// 处理header中的msg - 与GVA一致
|
||||
if (response.headers?.msg) {
|
||||
data.msg = decodeURI(response.headers.msg);
|
||||
}
|
||||
// 返回 response.data 而不是整个 response - 与GVA一致
|
||||
return { ...response, data };
|
||||
}
|
||||
|
||||
// 业务错误,显示错误消息 - 与GVA一致
|
||||
message.error(data.msg || decodeURI(response.headers?.msg || '') || '请求失败');
|
||||
return { ...response, data };
|
||||
return response;
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
/**
|
||||
* API管理 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 分页获取API列表 POST /api/getApiList */
|
||||
export async function getApiList(data: API.PageParams & { path?: string; description?: string; apiGroup?: string; method?: string }) {
|
||||
return request<API.ApiListResult>('/api/api/getApiList', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建API POST /api/createApi */
|
||||
export async function createApi(data: API.ApiItem) {
|
||||
return request<API.BaseResult>('/api/api/createApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据ID获取API POST /api/getApiById */
|
||||
export async function getApiById(data: { id: number }) {
|
||||
return request<API.ApiResult>('/api/api/getApiById', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新API POST /api/updateApi */
|
||||
export async function updateApi(data: API.ApiItem) {
|
||||
return request<API.BaseResult>('/api/api/updateApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置角色API权限 POST /api/setAuthApi */
|
||||
export async function setAuthApi(data: { authorityId: number; apis: API.ApiItem[] }) {
|
||||
return request<API.BaseResult>('/api/api/setAuthApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取所有API POST /api/getAllApis */
|
||||
export async function getAllApis(data?: { authorityId?: number }) {
|
||||
return request<API.AllApisResult>('/api/api/getAllApis', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除API POST /api/deleteApi */
|
||||
export async function deleteApi(data: { ids: number[] }) {
|
||||
return request<API.BaseResult>('/api/api/deleteApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除API DELETE /api/deleteApisByIds */
|
||||
export async function deleteApisByIds(data: { ids: number[] }) {
|
||||
return request<API.BaseResult>('/api/api/deleteApisByIds', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新Casbin缓存 GET /api/freshCasbin */
|
||||
export async function freshCasbin() {
|
||||
return request<API.BaseResult>('/api/api/freshCasbin', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 同步API GET /api/syncApi */
|
||||
export async function syncApi() {
|
||||
return request<API.BaseResult>('/api/api/syncApi', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取API分组 GET /api/getApiGroups */
|
||||
export async function getApiGroups() {
|
||||
return request<API.ApiGroupsResult>('/api/api/getApiGroups', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 忽略API POST /api/ignoreApi */
|
||||
export async function ignoreApi(data: { path: string; method: string }) {
|
||||
return request<API.BaseResult>('/api/api/ignoreApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 确认同步API POST /api/enterSyncApi */
|
||||
export async function enterSyncApi(data: { newApis: API.ApiItem[]; deleteApis: API.ApiItem[] }) {
|
||||
return request<API.BaseResult>('/api/api/enterSyncApi', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* 角色管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 获取角色列表 POST /authority/getAuthorityList */
|
||||
export async function getAuthorityList(data: API.PageParams) {
|
||||
return request<API.AuthorityListResult>('/api/authority/getAuthorityList', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除角色 POST /authority/deleteAuthority */
|
||||
export async function deleteAuthority(data: { authorityId: number }) {
|
||||
return request<API.BaseResult>('/api/authority/deleteAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建角色 POST /authority/createAuthority */
|
||||
export async function createAuthority(data: API.Authority) {
|
||||
return request<API.AuthorityResult>('/api/authority/createAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 拷贝角色 POST /authority/copyAuthority */
|
||||
export async function copyAuthority(data: { authority: API.Authority; oldAuthorityId: number }) {
|
||||
return request<API.AuthorityResult>('/api/authority/copyAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置角色资源权限 POST /authority/setDataAuthority */
|
||||
export async function setDataAuthority(data: { authorityId: number; dataAuthorityId: number[] }) {
|
||||
return request<API.BaseResult>('/api/authority/setDataAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改角色 PUT /authority/updateAuthority */
|
||||
export async function updateAuthority(data: API.Authority) {
|
||||
return request<API.BaseResult>('/api/authority/updateAuthority', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* Casbin权限管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 更新角色API权限 POST /casbin/updateCasbin */
|
||||
export async function updateCasbin(data: { authorityId: number; casbinInfos: API.CasbinInfo[] }) {
|
||||
return request<API.BaseResult>('/api/casbin/updateCasbin', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取权限列表 POST /casbin/getPolicyPathByAuthorityId */
|
||||
export async function getPolicyPathByAuthorityId(data: { authorityId: number }) {
|
||||
return request<API.CasbinPolicyResult>('/api/casbin/getPolicyPathByAuthorityId', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* 字典管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 创建字典 POST /sysDictionary/createSysDictionary */
|
||||
export async function createSysDictionary(data: API.Dictionary) {
|
||||
return request<API.BaseResult>('/api/sysDictionary/createSysDictionary', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除字典 DELETE /sysDictionary/deleteSysDictionary */
|
||||
export async function deleteSysDictionary(data: { ID: number }) {
|
||||
return request<API.BaseResult>('/api/sysDictionary/deleteSysDictionary', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新字典 PUT /sysDictionary/updateSysDictionary */
|
||||
export async function updateSysDictionary(data: API.Dictionary) {
|
||||
return request<API.BaseResult>('/api/sysDictionary/updateSysDictionary', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据ID查询字典 GET /sysDictionary/findSysDictionary */
|
||||
export async function findSysDictionary(params: { ID: number }) {
|
||||
return request<API.DictionaryResult>('/api/sysDictionary/findSysDictionary', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 分页获取字典列表 GET /sysDictionary/getSysDictionaryList */
|
||||
export async function getSysDictionaryList(params: API.PageParams & { name?: string; type?: string; status?: boolean }) {
|
||||
return request<API.DictionaryListResult>('/api/sysDictionary/getSysDictionaryList', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 导出字典JSON GET /sysDictionary/exportSysDictionary */
|
||||
export async function exportSysDictionary(params: { ID: number }) {
|
||||
return request<API.DictionaryExportResult>('/api/sysDictionary/exportSysDictionary', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 导入字典JSON POST /sysDictionary/importSysDictionary */
|
||||
export async function importSysDictionary(data: API.Dictionary) {
|
||||
return request<API.BaseResult>('/api/sysDictionary/importSysDictionary', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/**
|
||||
* 字典详情管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 创建字典详情 POST /sysDictionaryDetail/createSysDictionaryDetail */
|
||||
export async function createSysDictionaryDetail(data: API.DictionaryDetail) {
|
||||
return request<API.BaseResult>('/api/sysDictionaryDetail/createSysDictionaryDetail', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除字典详情 DELETE /sysDictionaryDetail/deleteSysDictionaryDetail */
|
||||
export async function deleteSysDictionaryDetail(data: { ID: number }) {
|
||||
return request<API.BaseResult>('/api/sysDictionaryDetail/deleteSysDictionaryDetail', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新字典详情 PUT /sysDictionaryDetail/updateSysDictionaryDetail */
|
||||
export async function updateSysDictionaryDetail(data: API.DictionaryDetail) {
|
||||
return request<API.BaseResult>('/api/sysDictionaryDetail/updateSysDictionaryDetail', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据ID查询字典详情 GET /sysDictionaryDetail/findSysDictionaryDetail */
|
||||
export async function findSysDictionaryDetail(params: { ID: number }) {
|
||||
return request<API.DictionaryDetailResult>('/api/sysDictionaryDetail/findSysDictionaryDetail', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 分页获取字典详情列表 GET /sysDictionaryDetail/getSysDictionaryDetailList */
|
||||
export async function getSysDictionaryDetailList(params: API.PageParams & { sysDictionaryID?: number }) {
|
||||
return request<API.DictionaryDetailListResult>('/api/sysDictionaryDetail/getSysDictionaryDetailList', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取层级字典详情树形结构(根据字典ID) GET /sysDictionaryDetail/getDictionaryTreeList */
|
||||
export async function getDictionaryTreeList(params: { sysDictionaryID: number }) {
|
||||
return request<API.DictionaryTreeResult>('/api/sysDictionaryDetail/getDictionaryTreeList', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取层级字典详情树形结构(根据字典类型) GET /sysDictionaryDetail/getDictionaryTreeListByType */
|
||||
export async function getDictionaryTreeListByType(params: { dictType: string }) {
|
||||
return request<API.DictionaryTreeResult>('/api/sysDictionaryDetail/getDictionaryTreeListByType', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据父级ID获取字典详情 GET /sysDictionaryDetail/getDictionaryDetailsByParent */
|
||||
export async function getDictionaryDetailsByParent(params: { parentID: number; includeChildren?: boolean }) {
|
||||
return request<API.DictionaryDetailListResult>('/api/sysDictionaryDetail/getDictionaryDetailsByParent', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取字典详情的完整路径 GET /sysDictionaryDetail/getDictionaryPath */
|
||||
export async function getDictionaryPath(params: { ID: number }) {
|
||||
return request<API.DictionaryPathResult>('/api/sysDictionaryDetail/getDictionaryPath', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* 系统管理 API 服务导出
|
||||
*/
|
||||
export * from './user';
|
||||
export * from './authority';
|
||||
export * from './menu';
|
||||
export * from './api';
|
||||
export * from './dictionary';
|
||||
export * from './dictionaryDetail';
|
||||
export * from './operationRecord';
|
||||
export * from './casbin';
|
||||
export * from './params';
|
||||
export * from './systemConfig';
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/**
|
||||
* 菜单管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 获取动态路由 POST /menu/getMenu */
|
||||
export async function asyncMenu() {
|
||||
return request<API.MenuResult>('/api/menu/getMenu', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取菜单列表 POST /menu/getMenuList */
|
||||
export async function getMenuList(data: API.PageParams) {
|
||||
return request<API.MenuListResult>('/api/menu/getMenuList', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增菜单 POST /menu/addBaseMenu */
|
||||
export async function addBaseMenu(data: API.Menu) {
|
||||
return request<API.BaseResult>('/api/menu/addBaseMenu', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取基础路由列表 POST /menu/getBaseMenuTree */
|
||||
export async function getBaseMenuTree() {
|
||||
return request<API.MenuTreeResult>('/api/menu/getBaseMenuTree', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/** 添加角色菜单关联 POST /menu/addMenuAuthority */
|
||||
export async function addMenuAuthority(data: { menus: API.Menu[]; authorityId: number }) {
|
||||
return request<API.BaseResult>('/api/menu/addMenuAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取角色菜单关联 POST /menu/getMenuAuthority */
|
||||
export async function getMenuAuthority(data: { authorityId: number }) {
|
||||
return request<API.MenuAuthorityResult>('/api/menu/getMenuAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除菜单 POST /menu/deleteBaseMenu */
|
||||
export async function deleteBaseMenu(data: { ID: number }) {
|
||||
return request<API.BaseResult>('/api/menu/deleteBaseMenu', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改菜单 POST /menu/updateBaseMenu */
|
||||
export async function updateBaseMenu(data: API.Menu) {
|
||||
return request<API.BaseResult>('/api/menu/updateBaseMenu', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据ID获取菜单 POST /menu/getBaseMenuById */
|
||||
export async function getBaseMenuById(data: { id: number }) {
|
||||
return request<API.MenuResult>('/api/menu/getBaseMenuById', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* 操作记录管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 删除操作记录 DELETE /sysOperationRecord/deleteSysOperationRecord */
|
||||
export async function deleteSysOperationRecord(data: { ID: number }) {
|
||||
return request<API.BaseResult>('/api/sysOperationRecord/deleteSysOperationRecord', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除操作记录 DELETE /sysOperationRecord/deleteSysOperationRecordByIds */
|
||||
export async function deleteSysOperationRecordByIds(data: { ids: number[] }) {
|
||||
return request<API.BaseResult>('/api/sysOperationRecord/deleteSysOperationRecordByIds', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 分页获取操作记录列表 GET /sysOperationRecord/getSysOperationRecordList */
|
||||
export async function getSysOperationRecordList(params: API.PageParams & {
|
||||
path?: string;
|
||||
method?: string;
|
||||
status?: number;
|
||||
ip?: string;
|
||||
}) {
|
||||
return request<API.OperationRecordListResult>('/api/sysOperationRecord/getSysOperationRecordList', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* 系统参数管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 创建参数 POST /sysParams/createSysParams */
|
||||
export async function createSysParams(data: API.SysParams) {
|
||||
return request<API.BaseResult>('/api/sysParams/createSysParams', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除参数 DELETE /sysParams/deleteSysParams */
|
||||
export async function deleteSysParams(params: { ID: number }) {
|
||||
return request<API.BaseResult>('/api/sysParams/deleteSysParams', {
|
||||
method: 'DELETE',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除参数 DELETE /sysParams/deleteSysParamsByIds */
|
||||
export async function deleteSysParamsByIds(params: { 'IDs[]': number[] }) {
|
||||
return request<API.BaseResult>('/api/sysParams/deleteSysParamsByIds', {
|
||||
method: 'DELETE',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新参数 PUT /sysParams/updateSysParams */
|
||||
export async function updateSysParams(data: API.SysParams) {
|
||||
return request<API.BaseResult>('/api/sysParams/updateSysParams', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据ID查询参数 GET /sysParams/findSysParams */
|
||||
export async function findSysParams(params: { ID: number }) {
|
||||
return request<API.SysParamsResult>('/api/sysParams/findSysParams', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 分页获取参数列表 GET /sysParams/getSysParamsList */
|
||||
export async function getSysParamsList(params: API.PageParams & { name?: string; key?: string }) {
|
||||
return request<API.SysParamsListResult>('/api/sysParams/getSysParamsList', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据key获取参数 GET /sysParams/getSysParam */
|
||||
export async function getSysParam(params: { key: string }) {
|
||||
return request<API.SysParamsResult>('/api/sysParams/getSysParam', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* 系统配置 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 获取服务器信息 POST /system/getServerInfo */
|
||||
export async function getServerInfo() {
|
||||
return request<API.ServerInfoResult>('/api/system/getServerInfo', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
|
@ -1,468 +0,0 @@
|
|||
/**
|
||||
* 系统管理 API 类型定义
|
||||
* 与 GVA 保持一致
|
||||
*/
|
||||
declare namespace API {
|
||||
// 基础响应
|
||||
type BaseResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
// 分页参数
|
||||
type PageParams = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
// 登录参数
|
||||
type LoginParams = {
|
||||
username: string;
|
||||
password: string;
|
||||
captcha?: string;
|
||||
captchaId?: string;
|
||||
};
|
||||
|
||||
// 登录结果
|
||||
type LoginResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
user: UserInfo;
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 验证码结果
|
||||
type CaptchaResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
captchaId: string;
|
||||
picPath: string;
|
||||
captchaLength: number;
|
||||
openCaptcha: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// 注册参数
|
||||
type RegisterParams = {
|
||||
username: string;
|
||||
password: string;
|
||||
nickName?: string;
|
||||
headerImg?: string;
|
||||
authorityId?: number;
|
||||
authorityIds?: number[];
|
||||
};
|
||||
|
||||
// 修改密码参数
|
||||
type ChangePasswordParams = {
|
||||
password: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
// 用户信息
|
||||
type UserInfo = {
|
||||
ID?: number;
|
||||
uuid?: string;
|
||||
userName?: string;
|
||||
nickName?: string;
|
||||
sideMode?: string;
|
||||
headerImg?: string;
|
||||
baseColor?: string;
|
||||
activeColor?: string;
|
||||
authorityId?: number;
|
||||
authority?: Authority;
|
||||
authorities?: Authority[];
|
||||
phone?: string;
|
||||
email?: string;
|
||||
enable?: number;
|
||||
};
|
||||
|
||||
// 用户信息结果
|
||||
type UserInfoResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
userInfo: UserInfo;
|
||||
};
|
||||
};
|
||||
|
||||
// 用户列表结果
|
||||
type UserListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: UserInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// 角色
|
||||
type Authority = {
|
||||
authorityId?: number;
|
||||
authorityName?: string;
|
||||
parentId?: number;
|
||||
dataAuthorityId?: Authority[];
|
||||
children?: Authority[];
|
||||
menus?: Menu[];
|
||||
defaultRouter?: string;
|
||||
};
|
||||
|
||||
// 角色结果
|
||||
type AuthorityResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
authority: Authority;
|
||||
};
|
||||
};
|
||||
|
||||
// 角色列表结果
|
||||
type AuthorityListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: Authority[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 菜单
|
||||
type Menu = {
|
||||
ID?: number;
|
||||
parentId?: number;
|
||||
path?: string;
|
||||
name?: string;
|
||||
hidden?: boolean;
|
||||
component?: string;
|
||||
sort?: number;
|
||||
meta?: MenuMeta;
|
||||
children?: Menu[];
|
||||
parameters?: MenuParameter[];
|
||||
menuBtn?: MenuButton[];
|
||||
};
|
||||
|
||||
// 菜单元信息
|
||||
type MenuMeta = {
|
||||
activeName?: string;
|
||||
keepAlive?: boolean;
|
||||
defaultMenu?: boolean;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
closeTab?: boolean;
|
||||
};
|
||||
|
||||
// 菜单参数
|
||||
type MenuParameter = {
|
||||
ID?: number;
|
||||
type?: string;
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
// 菜单按钮
|
||||
type MenuButton = {
|
||||
ID?: number;
|
||||
name?: string;
|
||||
desc?: string;
|
||||
};
|
||||
|
||||
// 菜单结果
|
||||
type MenuResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
menu?: Menu;
|
||||
menus?: Menu[];
|
||||
};
|
||||
};
|
||||
|
||||
// 菜单列表结果
|
||||
type MenuListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: Menu[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 菜单树结果
|
||||
type MenuTreeResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
menus: Menu[];
|
||||
};
|
||||
};
|
||||
|
||||
// 菜单权限结果
|
||||
type MenuAuthorityResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
menus: Menu[];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// API项
|
||||
type ApiItem = {
|
||||
ID?: number;
|
||||
path?: string;
|
||||
description?: string;
|
||||
apiGroup?: string;
|
||||
method?: string;
|
||||
};
|
||||
|
||||
// API结果
|
||||
type ApiResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
api: ApiItem;
|
||||
};
|
||||
};
|
||||
|
||||
// API列表结果
|
||||
type ApiListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: ApiItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 所有API结果
|
||||
type AllApisResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
apis: ApiItem[];
|
||||
};
|
||||
};
|
||||
|
||||
// API分组结果
|
||||
type ApiGroupsResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
groups: string[];
|
||||
};
|
||||
};
|
||||
|
||||
// 字典
|
||||
type Dictionary = {
|
||||
ID?: number;
|
||||
name?: string;
|
||||
type?: string;
|
||||
status?: boolean;
|
||||
desc?: string;
|
||||
sysDictionaryDetails?: DictionaryDetail[];
|
||||
};
|
||||
|
||||
// 字典结果
|
||||
type DictionaryResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
sysDictionary: Dictionary;
|
||||
};
|
||||
};
|
||||
|
||||
// 字典列表结果
|
||||
type DictionaryListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: Dictionary[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 字典导出结果
|
||||
type DictionaryExportResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: Dictionary;
|
||||
};
|
||||
|
||||
// 字典详情
|
||||
type DictionaryDetail = {
|
||||
ID?: number;
|
||||
sysDictionaryID?: number;
|
||||
parentID?: number;
|
||||
label?: string;
|
||||
value?: string;
|
||||
extend?: string;
|
||||
status?: boolean;
|
||||
sort?: number;
|
||||
children?: DictionaryDetail[];
|
||||
};
|
||||
|
||||
// 字典详情结果
|
||||
type DictionaryDetailResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
sysDictionaryDetail: DictionaryDetail;
|
||||
};
|
||||
};
|
||||
|
||||
// 字典详情列表结果
|
||||
type DictionaryDetailListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: DictionaryDetail[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 字典树结果
|
||||
type DictionaryTreeResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: DictionaryDetail[];
|
||||
};
|
||||
};
|
||||
|
||||
// 字典路径结果
|
||||
type DictionaryPathResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
path: DictionaryDetail[];
|
||||
};
|
||||
};
|
||||
|
||||
// 操作记录
|
||||
type OperationRecord = {
|
||||
ID?: number;
|
||||
ip?: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
status?: number;
|
||||
latency?: string;
|
||||
agent?: string;
|
||||
error_message?: string;
|
||||
body?: string;
|
||||
resp?: string;
|
||||
user_id?: number;
|
||||
user?: UserInfo;
|
||||
CreatedAt?: string;
|
||||
};
|
||||
|
||||
// 操作记录列表结果
|
||||
type OperationRecordListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: OperationRecord[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// Casbin信息
|
||||
type CasbinInfo = {
|
||||
path: string;
|
||||
method: string;
|
||||
};
|
||||
|
||||
// Casbin策略结果
|
||||
type CasbinPolicyResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
paths: CasbinInfo[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 系统参数
|
||||
type SysParams = {
|
||||
ID?: number;
|
||||
name?: string;
|
||||
key?: string;
|
||||
value?: string;
|
||||
desc?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
// 系统参数结果
|
||||
type SysParamsResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: SysParams;
|
||||
};
|
||||
|
||||
// 系统参数列表结果
|
||||
type SysParamsListResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
list: SysParams[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 服务器信息
|
||||
type ServerInfo = {
|
||||
os: {
|
||||
goos: string;
|
||||
numCpu: number;
|
||||
compiler: string;
|
||||
goVersion: string;
|
||||
numGoroutine: number;
|
||||
};
|
||||
cpu: {
|
||||
cpus: number[];
|
||||
cores: number;
|
||||
};
|
||||
ram: {
|
||||
usedMb: number;
|
||||
totalMb: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
disk: {
|
||||
mountPoint: string;
|
||||
usedMb: number;
|
||||
usedGb: number;
|
||||
totalMb: number;
|
||||
totalGb: number;
|
||||
usedPercent: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
// 服务器信息结果
|
||||
type ServerInfoResult = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
server: ServerInfo;
|
||||
};
|
||||
};
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
/**
|
||||
* 用户管理 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 用户登录 POST /base/login */
|
||||
export async function login(data: API.LoginParams) {
|
||||
return request<API.LoginResult>('/api/base/login', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取验证码 POST /base/captcha */
|
||||
export async function captcha() {
|
||||
return request<API.CaptchaResult>('/api/base/captcha', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/** 用户注册 POST /user/admin_register */
|
||||
export async function register(data: API.RegisterParams) {
|
||||
return request<API.BaseResult>('/api/user/admin_register', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改密码 POST /user/changePassword */
|
||||
export async function changePassword(data: API.ChangePasswordParams) {
|
||||
return request<API.BaseResult>('/api/user/changePassword', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 分页获取用户列表 POST /user/getUserList */
|
||||
export async function getUserList(data: API.PageParams) {
|
||||
return request<API.UserListResult>('/api/user/getUserList', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置用户权限 POST /user/setUserAuthority */
|
||||
export async function setUserAuthority(data: { authorityId: number }) {
|
||||
return request<API.BaseResult>('/api/user/setUserAuthority', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除用户 DELETE /user/deleteUser */
|
||||
export async function deleteUser(data: { id: number }) {
|
||||
return request<API.BaseResult>('/api/user/deleteUser', {
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** 设置用户信息 PUT /user/setUserInfo */
|
||||
export async function setUserInfo(data: API.UserInfo) {
|
||||
return request<API.BaseResult>('/api/user/setUserInfo', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置自身信息 PUT /user/setSelfInfo */
|
||||
export async function setSelfInfo(data: API.UserInfo) {
|
||||
return request<API.BaseResult>('/api/user/setSelfInfo', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置自身界面配置 PUT /user/setSelfSetting */
|
||||
export async function setSelfSetting(data: Record<string, any>) {
|
||||
return request<API.BaseResult>('/api/user/setSelfSetting', {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置用户多角色权限 POST /user/setUserAuthorities */
|
||||
export async function setUserAuthorities(data: { uuid: string; authorityIds: number[] }) {
|
||||
return request<API.BaseResult>('/api/user/setUserAuthorities', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取用户信息 GET /user/getUserInfo */
|
||||
export async function getUserInfo() {
|
||||
return request<API.UserInfoResult>('/api/user/getUserInfo', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置密码 POST /user/resetPassword */
|
||||
export async function resetPassword(data: { id: number }) {
|
||||
return request<API.BaseResult>('/api/user/resetPassword', {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
/**
|
||||
* 格式化工具函数 - 参考GVA的format.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* 设置主题色 - 参考GVA的setBodyPrimaryColor
|
||||
* @param color 主题色
|
||||
* @param mode 模式 'light' | 'dark'
|
||||
*/
|
||||
export function setBodyPrimaryColor(color: string, mode: 'light' | 'dark' = 'light'): void {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary-color', color);
|
||||
|
||||
// 设置不同透明度的主题色变体
|
||||
root.style.setProperty('--primary-color-hover', adjustColor(color, mode === 'dark' ? 20 : -10));
|
||||
root.style.setProperty('--primary-color-active', adjustColor(color, mode === 'dark' ? 30 : -20));
|
||||
root.style.setProperty('--primary-color-light', adjustColor(color, mode === 'dark' ? -30 : 40));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整颜色亮度
|
||||
* @param color 十六进制颜色
|
||||
* @param amount 调整量(正数变亮,负数变暗)
|
||||
*/
|
||||
function adjustColor(color: string, amount: number): string {
|
||||
const hex = color.replace('#', '');
|
||||
const num = parseInt(hex, 16);
|
||||
|
||||
let r = (num >> 16) + amount;
|
||||
let g = ((num >> 8) & 0x00FF) + amount;
|
||||
let b = (num & 0x0000FF) + amount;
|
||||
|
||||
r = Math.max(0, Math.min(255, r));
|
||||
g = Math.max(0, Math.min(255, g));
|
||||
b = Math.max(0, Math.min(255, b));
|
||||
|
||||
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param date 日期
|
||||
* @param format 格式
|
||||
*/
|
||||
export function formatDate(date: Date | string | number, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
const d = new Date(date);
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param bytes 字节数
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/**
|
||||
* 工具函数集合 - 参考GVA的utils
|
||||
*/
|
||||
|
||||
export * from './page';
|
||||
export * from './storage';
|
||||
export * from './format';
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* 页面工具函数 - 参考GVA的page.js
|
||||
*/
|
||||
|
||||
const APP_NAME = 'KRA Admin';
|
||||
|
||||
/**
|
||||
* 获取页面标题
|
||||
* @param pageTitle 页面标题
|
||||
* @returns 完整的页面标题
|
||||
*/
|
||||
export function getPageTitle(pageTitle?: string): string {
|
||||
if (pageTitle) {
|
||||
return `${pageTitle} - ${APP_NAME}`;
|
||||
}
|
||||
return APP_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面标题
|
||||
* @param title 页面标题
|
||||
*/
|
||||
export function setPageTitle(title?: string): void {
|
||||
document.title = getPageTitle(title);
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* 存储工具函数 - 参考GVA的用户存储逻辑
|
||||
*/
|
||||
|
||||
const TOKEN_KEY = 'token';
|
||||
const USER_INFO_KEY = 'userInfo';
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
*/
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Token
|
||||
*/
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Token
|
||||
*/
|
||||
export function removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo(): API.UserInfo | null {
|
||||
const userInfo = localStorage.getItem(USER_INFO_KEY);
|
||||
if (userInfo) {
|
||||
try {
|
||||
return JSON.parse(userInfo);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
export function setUserInfo(userInfo: API.UserInfo): void {
|
||||
localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户信息
|
||||
*/
|
||||
export function removeUserInfo(): void {
|
||||
localStorage.removeItem(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有存储 - 参考GVA的ClearStorage
|
||||
*/
|
||||
export function clearStorage(): void {
|
||||
removeToken();
|
||||
removeUserInfo();
|
||||
sessionStorage.clear();
|
||||
localStorage.removeItem('originSetting');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要跳转到首页
|
||||
*/
|
||||
export function checkNeedToHome(): boolean {
|
||||
const need = sessionStorage.getItem('needToHome') === 'true';
|
||||
if (need) {
|
||||
sessionStorage.removeItem('needToHome');
|
||||
}
|
||||
return need;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置需要跳转到首页
|
||||
*/
|
||||
export function setNeedToHome(): void {
|
||||
sessionStorage.setItem('needToHome', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要关闭所有标签
|
||||
*/
|
||||
export function checkNeedCloseAll(): boolean {
|
||||
const need = sessionStorage.getItem('needCloseAll') === 'true';
|
||||
if (need) {
|
||||
sessionStorage.removeItem('needCloseAll');
|
||||
}
|
||||
return need;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置需要关闭所有标签
|
||||
*/
|
||||
export function setNeedCloseAll(): void {
|
||||
sessionStorage.setItem('needCloseAll', 'true');
|
||||
}
|
||||
Loading…
Reference in New Issue