前端重构

This commit is contained in:
Yvan 2026-01-08 14:02:38 +08:00
parent f4fcdaa322
commit cf16ab6448
14 changed files with 365 additions and 102 deletions

View File

@ -3,6 +3,7 @@
*
*/
import { AvatarDropdown, AvatarName, Footer } from '@/components';
import CommandMenu from '@/components/CommandMenu';
import { getUserInfo } from '@/services/kratos/user';
import { getToken, removeToken } from '@/utils/auth';
import {
@ -12,12 +13,14 @@ import {
SettingOutlined,
UserOutlined,
QuestionCircleOutlined,
SearchOutlined,
} from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { history } from '@umijs/max';
import { message, Tooltip } from 'antd';
import React, { useState } from 'react';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
@ -114,69 +117,69 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
}));
};
// 打开设置
const handleOpenSetting = () => {
const settingBtn = document.querySelector('.ant-pro-setting-drawer-handle');
if (settingBtn) {
(settingBtn as HTMLElement).click();
}
};
const iconStyle: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: '50%',
border: '1px solid #d9d9d9',
cursor: 'pointer',
};
return {
// 先展开 settings让后面的配置可以覆盖
...initialState?.settings,
// 右上角操作区
actionsRender: () => {
const isDark = initialState?.settings?.navTheme === 'realDark';
const isDarkTheme = initialState?.settings?.navTheme === 'realDark';
return [
// 帮助文档
<Tooltip title="帮助文档" key="doc">
<a
href="https://github.com/go-kratos/kratos"
href="https://go-kratos.dev/"
target="_blank"
rel="noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: '50%',
border: '1px solid #d9d9d9',
cursor: 'pointer',
color: 'inherit',
}}
style={{ ...iconStyle, color: 'inherit' }}
>
<QuestionCircleOutlined style={{ fontSize: 16 }} />
</a>
</Tooltip>,
// 搜索 (Ctrl+K)
<Tooltip title="搜索 (Ctrl+K)" key="search">
<span
onClick={() => window.dispatchEvent(new CustomEvent('openCommandMenu'))}
style={iconStyle}
>
<SearchOutlined style={{ fontSize: 16 }} />
</span>
</Tooltip>,
// 系统设置
<Tooltip title="系统设置" key="setting">
<span onClick={handleOpenSetting} style={iconStyle}>
<SettingOutlined style={{ fontSize: 16 }} />
</span>
</Tooltip>,
// 刷新
<Tooltip title="刷新" key="refresh">
<span
onClick={handleRefresh}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: '50%',
border: '1px solid #d9d9d9',
cursor: 'pointer',
}}
>
<span onClick={handleRefresh} style={iconStyle}>
<ReloadOutlined style={{ fontSize: 16 }} />
</span>
</Tooltip>,
// 切换主题
<Tooltip title="切换主题" key="theme">
<span
onClick={handleToggleTheme}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: '50%',
border: '1px solid #d9d9d9',
cursor: 'pointer',
}}
>
{isDark ? (
<span onClick={handleToggleTheme} style={iconStyle}>
{isDarkTheme ? (
<BulbFilled style={{ fontSize: 16, color: '#faad14' }} />
) : (
<BulbOutlined style={{ fontSize: 16 }} />
@ -188,7 +191,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
// 头像配置
avatarProps: {
src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg,
src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg || '/default-avatar.svg',
title: <AvatarName />,
icon: <UserOutlined />,
render: (_, avatarChildren) => <AvatarDropdown menu>{avatarChildren}</AvatarDropdown>,
@ -231,25 +234,47 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
return (
<>
{children}
{isDev && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
<CommandMenuWrapper />
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
</>
);
},
};
};
// CommandMenu包装组件
const CommandMenuWrapper: React.FC = () => {
const [open, setOpen] = useState(false);
React.useEffect(() => {
const handleOpen = () => setOpen(true);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
setOpen(true);
}
};
window.addEventListener('openCommandMenu', handleOpen);
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('openCommandMenu', handleOpen);
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
return <CommandMenu open={open} onClose={() => setOpen(false)} />;
};
/**
*
* @see https://umijs.org/docs/max/request#配置

View File

@ -0,0 +1,193 @@
/**
* KRA - Command Menu Component
* Ctrl+K快捷键
*/
import React, { useState, useEffect, useMemo } from 'react';
import { Modal, Input, List, Typography, Space, Tag } from 'antd';
import { SearchOutlined, ArrowRightOutlined, BulbOutlined, LogoutOutlined } from '@ant-design/icons';
import { history, useModel } from '@umijs/max';
import { createStyles } from 'antd-style';
const { Text } = Typography;
const useStyles = createStyles(({ token }) => ({
searchInput: {
fontSize: 16,
padding: '12px 16px',
},
categoryTitle: {
fontSize: 12,
fontWeight: 600,
color: token.colorTextSecondary,
marginTop: 8,
marginBottom: 4,
},
listItem: {
padding: '8px 12px',
cursor: 'pointer',
borderRadius: 4,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
},
}));
interface CommandMenuProps {
open: boolean;
onClose: () => void;
}
interface MenuItem {
label: string;
path?: string;
action?: () => void;
icon?: React.ReactNode;
}
interface MenuCategory {
label: string;
items: MenuItem[];
}
const CommandMenu: React.FC<CommandMenuProps> = ({ open, onClose }) => {
const { styles } = useStyles();
const [searchValue, setSearchValue] = useState('');
const { initialState, setInitialState } = useModel('@@initialState');
// 菜单项
const menuCategories: MenuCategory[] = useMemo(() => {
const categories: MenuCategory[] = [
{
label: '跳转',
items: [
{ label: '仪表盘', path: '/dashboard' },
{ label: '用户管理', path: '/admin/user' },
{ label: '角色管理', path: '/admin/authority' },
{ label: '菜单管理', path: '/admin/menu' },
{ label: 'API管理', path: '/admin/api' },
{ label: '操作日志', path: '/admin/operation' },
{ label: '字典管理', path: '/admin/dictionary' },
{ label: '参数配置', path: '/admin/params' },
{ label: '系统配置', path: '/systemTools/system' },
{ label: '个人信息', path: '/person' },
{ label: '关于', path: '/about' },
],
},
{
label: '操作',
items: [
{
label: '亮色主题',
icon: <BulbOutlined />,
action: () => {
setInitialState((s) => ({
...s,
settings: { ...s?.settings, navTheme: 'light' },
}));
},
},
{
label: '暗色主题',
icon: <BulbOutlined />,
action: () => {
setInitialState((s) => ({
...s,
settings: { ...s?.settings, navTheme: 'realDark' },
}));
},
},
{
label: '退出登录',
icon: <LogoutOutlined />,
action: () => {
history.push('/user/login');
},
},
],
},
];
return categories;
}, [setInitialState]);
// 过滤菜单
const filteredCategories = useMemo(() => {
if (!searchValue) return menuCategories;
return menuCategories
.map((category) => ({
...category,
items: category.items.filter((item) =>
item.label.toLowerCase().includes(searchValue.toLowerCase())
),
}))
.filter((category) => category.items.length > 0);
}, [menuCategories, searchValue]);
// 处理点击
const handleItemClick = (item: MenuItem) => {
if (item.path) {
history.push(item.path);
} else if (item.action) {
item.action();
}
onClose();
setSearchValue('');
};
// 监听Ctrl+K快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
width={500}
closable={false}
styles={{ body: { maxHeight: '50vh', overflow: 'auto', padding: '12px' } }}
>
<Input
placeholder="请输入你需要快捷到达的功能"
prefix={<SearchOutlined />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className={styles.searchInput}
autoFocus
allowClear
/>
{filteredCategories.map((category, index) => (
<div key={index}>
{category.items.length > 0 && (
<>
<div className={styles.categoryTitle}>{category.label}</div>
<List
size="small"
dataSource={category.items}
renderItem={(item) => (
<div
className={styles.listItem}
onClick={() => handleItemClick(item)}
>
<Space>
{item.icon || <ArrowRightOutlined />}
<Text>{item.label}</Text>
</Space>
</div>
)}
/>
</>
)}
</div>
))}
</Modal>
);
};
export default CommandMenu;

View File

@ -134,7 +134,7 @@ const AboutPage: React.FC = () => {
</Helmet>
{/* 头部信息 */}
<Card className={styles.headerCard} bordered={false}>
<Card className={styles.headerCard} variant="borderless">
<div className={styles.logoWrapper}>
<Logo size={80} />
</div>
@ -166,7 +166,7 @@ const AboutPage: React.FC = () => {
<Row gutter={[24, 24]}>
{/* 系统信息 */}
<Col xs={24} lg={12}>
<Card title="系统信息" className={styles.infoCard} bordered={false}>
<Card title="系统信息" className={styles.infoCard} variant="borderless">
<Descriptions column={1} labelStyle={{ width: '120px' }}>
<Descriptions.Item label="系统名称">Kratos Admin</Descriptions.Item>
<Descriptions.Item label="当前版本">
@ -200,7 +200,7 @@ const AboutPage: React.FC = () => {
{/* 技术栈 */}
<Col xs={24} lg={12}>
<Card title="技术栈" className={styles.infoCard} bordered={false}>
<Card title="技术栈" className={styles.infoCard} variant="borderless">
<Descriptions column={1} labelStyle={{ width: '120px' }}>
<Descriptions.Item label="后端语言">Go 1.21+</Descriptions.Item>
<Descriptions.Item label="微服务框架">Kratos v2</Descriptions.Item>
@ -215,7 +215,7 @@ const AboutPage: React.FC = () => {
{/* 核心特性 */}
<Col xs={24}>
<Card title="核心特性" className={styles.infoCard} bordered={false}>
<Card title="核心特性" className={styles.infoCard} variant="borderless">
<Row gutter={[24, 0]}>
{features.map((feature, index) => (
<Col xs={24} md={12} lg={8} key={index}>
@ -239,7 +239,7 @@ const AboutPage: React.FC = () => {
{/* 鸣谢 */}
<Col xs={24}>
<Card title="鸣谢" className={styles.infoCard} bordered={false}>
<Card title="鸣谢" className={styles.infoCard} variant="borderless">
<Paragraph>
Kratos Admin
</Paragraph>

View File

@ -31,7 +31,7 @@ const Notice: React.FC = () => {
<Card
title="公告"
className="kra-dashboard-card"
bordered={false}
variant="borderless"
extra={<a href="#"></a>}
>
<List

View File

@ -103,7 +103,7 @@ const PluginTable: React.FC = () => {
<Card
title="最新插件"
className="kra-dashboard-card"
bordered={false}
variant="borderless"
extra={
<Space>
<Button

View File

@ -38,7 +38,7 @@ const QuickLinks: React.FC = () => {
<Card
title="快捷功能"
className="kra-dashboard-card"
bordered={false}
variant="borderless"
>
<Row gutter={[12, 12]}>
{links.map((link, index) => (

View File

@ -227,22 +227,22 @@ const PersonPage: React.FC = () => {
<Row gutter={[16, 16]} style={{ padding: '24px 0' }}>
<Col xs={12} md={6}>
<div className={styles.statCard}>
<Statistic title="项目参与" value={138} valueStyle={{ color: '#1890ff' }} />
<Statistic title="项目参与" value={138} styles={{ content: { color: '#1890ff' } }} />
</div>
</Col>
<Col xs={12} md={6}>
<div className={styles.statCard}>
<Statistic title="代码提交" value={2300} suffix="+" valueStyle={{ color: '#52c41a' }} />
<Statistic title="代码提交" value={2300} suffix="+" styles={{ content: { color: '#52c41a' } }} />
</div>
</Col>
<Col xs={12} md={6}>
<div className={styles.statCard}>
<Statistic title="任务完成" value={95} suffix="%" valueStyle={{ color: '#722ed1' }} />
<Statistic title="任务完成" value={95} suffix="%" styles={{ content: { color: '#722ed1' } }} />
</div>
</Col>
<Col xs={12} md={6}>
<div className={styles.statCard}>
<Statistic title="获得勋章" value={12} valueStyle={{ color: '#faad14' }} />
<Statistic title="获得勋章" value={12} styles={{ content: { color: '#faad14' } }} />
</div>
</Col>
</Row>
@ -281,7 +281,7 @@ const PersonPage: React.FC = () => {
</Helmet>
{/* 顶部个人信息卡片 */}
<Card className={styles.profileCard} bordered={false} bodyStyle={{ padding: 0 }}>
<Card className={styles.profileCard} variant="borderless" styles={{ body: { padding: 0 } }}>
<div className={styles.coverBg} />
<div className={styles.profileInfo}>
<div className={styles.avatarWrapper}>
@ -329,7 +329,7 @@ const PersonPage: React.FC = () => {
<Row gutter={24}>
{/* 左侧信息栏 */}
<Col xs={24} lg={8}>
<Card className={styles.infoCard} bordered={false}>
<Card className={styles.infoCard} variant="borderless">
<div className={styles.infoTitle}>
<UserOutlined style={{ color: '#1890ff' }} />
</div>
@ -368,7 +368,7 @@ const PersonPage: React.FC = () => {
</div>
</Card>
<Card className={styles.infoCard} bordered={false}>
<Card className={styles.infoCard} variant="borderless">
<div className={styles.infoTitle}>
<TrophyOutlined style={{ color: '#faad14' }} />
</div>
@ -383,7 +383,7 @@ const PersonPage: React.FC = () => {
{/* 右侧内容区 */}
<Col xs={24} lg={16}>
<Card className={styles.infoCard} bordered={false}>
<Card className={styles.infoCard} variant="borderless">
<Tabs items={tabItems} />
</Card>
</Col>

View File

@ -46,9 +46,10 @@ const AuthorityPage: React.FC = () => {
const fetchTableData = async () => {
setLoading(true);
try {
const res = await getAuthorityList({ page: 1, pageSize: 1000 });
const res = await getAuthorityList();
if (res.code === 0) {
setTableData(res.data?.list || []);
// GVA接口直接返回树形数组不是分页格式
setTableData(res.data || []);
}
} catch (error) {
console.error('Failed to fetch authority list:', error);

View File

@ -84,9 +84,10 @@ const UserPage: React.FC = () => {
const fetchAuthorityOptions = async () => {
try {
const res = await getAuthorityList({ page: 1, pageSize: 1000 });
const res = await getAuthorityList();
if (res.code === 0) {
const options = buildAuthorityOptions(res.data?.list || []);
// GVA接口直接返回树形数组不是分页格式
const options = buildAuthorityOptions(res.data || []);
setAuthOptions(options);
}
} catch (error) {

View File

@ -4,42 +4,63 @@
import request from '@/utils/request';
export interface Authority {
authorityId: string;
authorityId: number | string;
authorityName: string;
parentId?: string;
parentId?: number | string;
defaultRouter?: string;
children?: Authority[];
dataAuthorityId?: Authority[];
}
/** 获取角色列表 */
export const getAuthorityList = (data: { page: number; pageSize: number }) => {
return request.post('/authority/getAuthorityList', data);
export const getAuthorityList = (data?: { page?: number; pageSize?: number }) => {
return request.post('/authority/getAuthorityList', data || {});
};
/** 删除角色 */
export const deleteAuthority = (data: { authorityId: string }) => {
return request.post('/authority/deleteAuthority', data);
export const deleteAuthority = (data: { authorityId: number | string }) => {
return request.post('/authority/deleteAuthority', { authorityId: Number(data.authorityId) });
};
/** 创建角色 */
export const createAuthority = (data: Authority) => {
return request.post('/authority/createAuthority', data);
return request.post('/authority/createAuthority', {
...data,
authorityId: Number(data.authorityId),
parentId: data.parentId ? Number(data.parentId) : 0,
});
};
/** 拷贝角色 */
export const copyAuthority = (data: { authority: Authority; oldAuthorityId: string }) => {
return request.post('/authority/copyAuthority', data);
export const copyAuthority = (data: { authority: Authority; oldAuthorityId: number | string }) => {
return request.post('/authority/copyAuthority', {
authority: {
...data.authority,
authorityId: Number(data.authority.authorityId),
parentId: data.authority.parentId ? Number(data.authority.parentId) : 0,
},
oldAuthorityId: Number(data.oldAuthorityId),
});
};
/** 设置角色资源权限 */
export const setDataAuthority = (data: { authorityId: string; dataAuthorityId: Authority[] }) => {
return request.post('/authority/setDataAuthority', data);
export const setDataAuthority = (data: { authorityId: number | string; dataAuthorityId: Authority[] }) => {
return request.post('/authority/setDataAuthority', {
authorityId: Number(data.authorityId),
dataAuthorityId: data.dataAuthorityId?.map(item => ({
...item,
authorityId: Number(item.authorityId),
})),
});
};
/** 修改角色 */
export const updateAuthority = (data: Authority) => {
return request.put('/authority/updateAuthority', data);
return request.put('/authority/updateAuthority', {
...data,
authorityId: Number(data.authorityId),
parentId: data.parentId ? Number(data.parentId) : 0,
});
};
export default {

View File

@ -5,13 +5,19 @@ import request from '@/utils/request';
import type { ApiResponse } from '@/types/api';
/** 获取角色按钮权限 */
export const getAuthorityBtn = (data: { authorityId: string; menuID: number }) => {
return request.post<ApiResponse<any>>('/authorityBtn/getAuthorityBtn', data);
export const getAuthorityBtn = (data: { authorityId: number | string; menuID: number }) => {
return request.post<ApiResponse<any>>('/authorityBtn/getAuthorityBtn', {
...data,
authorityId: Number(data.authorityId),
});
};
/** 设置角色按钮权限 */
export const setAuthorityBtn = (data: { authorityId: string; menuID: number; selected: string[] }) => {
return request.post<ApiResponse<any>>('/authorityBtn/setAuthorityBtn', data);
export const setAuthorityBtn = (data: { authorityId: number | string; menuID: number; selected: string[] }) => {
return request.post<ApiResponse<any>>('/authorityBtn/setAuthorityBtn', {
...data,
authorityId: Number(data.authorityId),
});
};
/** 检查是否可以移除按钮权限 */

View File

@ -10,13 +10,18 @@ export interface CasbinInfo {
}
/** 更新角色API权限 */
export const updateCasbin = (data: { authorityId: string; casbinInfos: CasbinInfo[] }) => {
return request.post<ApiResponse<any>>('/casbin/updateCasbin', data);
export const updateCasbin = (data: { authorityId: number | string; casbinInfos: CasbinInfo[] }) => {
return request.post<ApiResponse<any>>('/casbin/updateCasbin', {
...data,
authorityId: Number(data.authorityId),
});
};
/** 获取角色API权限列表 */
export const getPolicyPathByAuthorityId = (data: { authorityId: string }) => {
return request.post<ApiResponse<{ paths: CasbinInfo[] }>>('/casbin/getPolicyPathByAuthorityId', data);
export const getPolicyPathByAuthorityId = (data: { authorityId: number | string }) => {
return request.post<ApiResponse<{ paths: CasbinInfo[] }>>('/casbin/getPolicyPathByAuthorityId', {
authorityId: Number(data.authorityId),
});
};
export default {

View File

@ -44,13 +44,18 @@ export const getBaseMenuTree = () => {
};
/** 添加菜单权限关联 */
export const addMenuAuthority = (data: { menus: number[]; authorityId: string }) => {
return request.post('/menu/addMenuAuthority', data);
export const addMenuAuthority = (data: { menus: number[]; authorityId: number | string }) => {
return request.post('/menu/addMenuAuthority', {
...data,
authorityId: Number(data.authorityId),
});
};
/** 获取菜单权限关联 */
export const getMenuAuthority = (data: { authorityId: string }) => {
return request.post('/menu/getMenuAuthority', data);
export const getMenuAuthority = (data: { authorityId: number | string }) => {
return request.post('/menu/getMenuAuthority', {
authorityId: Number(data.authorityId),
});
};
/** 删除菜单 */

View File

@ -48,8 +48,11 @@ export const getUserList = (data: PageParams) => {
};
/** 设置用户权限 */
export const setUserAuthority = (data: { uuid: string; authorityId: string }) => {
return request.post<ApiResponse<any>>('/user/setUserAuthority', data);
export const setUserAuthority = (data: { uuid: string; authorityId: string | number }) => {
return request.post<ApiResponse<any>>('/user/setUserAuthority', {
...data,
authorityId: Number(data.authorityId),
});
};
/** 删除用户 */
@ -73,8 +76,11 @@ export const setSelfSetting = (data: any) => {
};
/** 设置用户多角色权限 */
export const setUserAuthorities = (data: { ID: number; authorityIds: string[] }) => {
return request.post<ApiResponse<any>>('/user/setUserAuthorities', data);
export const setUserAuthorities = (data: { ID: number; authorityIds: (number | string)[] }) => {
return request.post<ApiResponse<any>>('/user/setUserAuthorities', {
...data,
authorityIds: data.authorityIds.map(id => Number(id)),
});
};
/** 获取用户信息 */