前端重构

This commit is contained in:
Yvan 2026-01-07 20:01:14 +08:00
parent b244c7f044
commit d76655a30a
38 changed files with 603 additions and 4328 deletions

View File

@ -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",

View File

@ -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')
*/

View File

@ -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,
};

View File

@ -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,
},
]}

View File

@ -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;

View File

@ -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

View File

@ -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 };

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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;
},
],
};

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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';

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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',
});
}

View File

@ -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;
};
};

View File

@ -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,
});
}

View File

@ -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];
}

View File

@ -1,7 +0,0 @@
/**
* - GVA的utils
*/
export * from './page';
export * from './storage';
export * from './format';

View File

@ -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);
}

View File

@ -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');
}