任务三
This commit is contained in:
@@ -168,7 +168,6 @@ export default defineConfig({
|
||||
mock: {
|
||||
include: ["mock/**/*", "src/pages/**/_mock.ts"],
|
||||
},
|
||||
utoopack: {},
|
||||
requestRecord: {},
|
||||
exportStatic: {},
|
||||
define: {
|
||||
|
||||
+3
-2
@@ -15,9 +15,10 @@ export default {
|
||||
* @doc https://github.com/chimurai/http-proxy-middleware
|
||||
*/
|
||||
dev: {
|
||||
"/v1/": {
|
||||
"/api/": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
pathRewrite: { "^/api": "" },
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,6 +62,18 @@ export default [
|
||||
icon: 'fileSearch',
|
||||
component: './system/operation',
|
||||
},
|
||||
{
|
||||
path: '/system/params',
|
||||
name: 'params',
|
||||
icon: 'control',
|
||||
component: './system/params',
|
||||
},
|
||||
{
|
||||
path: '/system/state',
|
||||
name: 'state',
|
||||
icon: 'monitor',
|
||||
component: './system/state',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+12
-7
@@ -5,8 +5,7 @@ import {
|
||||
Question,
|
||||
SelectLang,
|
||||
} from "@/components";
|
||||
import { createAdminService } from "@/services/index";
|
||||
import { Admin } from "@/services/kratos/admin/v1/index";
|
||||
import { getUserInfo } from "@/services/system/user";
|
||||
import { LinkOutlined } from "@ant-design/icons";
|
||||
import type { Settings as LayoutSettings } from "@ant-design/pro-components";
|
||||
import { SettingDrawer } from "@ant-design/pro-components";
|
||||
@@ -19,20 +18,26 @@ const isDev = process.env.NODE_ENV === "development";
|
||||
const isDevOrTest = isDev || process.env.CI;
|
||||
const loginPath = "/user/login";
|
||||
|
||||
const adminService = createAdminService();
|
||||
|
||||
/**
|
||||
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
* */
|
||||
export async function getInitialState(): Promise<{
|
||||
settings?: Partial<LayoutSettings>;
|
||||
currentUser?: API.CurrentUser;
|
||||
currentUser?: API.UserInfo;
|
||||
loading?: boolean;
|
||||
fetchUserInfo?: () => Promise<Admin | undefined>;
|
||||
fetchUserInfo?: () => Promise<API.UserInfo | undefined>;
|
||||
}> {
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
return await adminService.Current({});
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const res = await getUserInfo();
|
||||
if (res.code === 0 && res.data?.userInfo) {
|
||||
return res.data.userInfo;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
history.push(loginPath);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createAdminService } from "@/services/index";
|
||||
import {
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
@@ -12,8 +11,6 @@ import React from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import HeaderDropdown from "../HeaderDropdown";
|
||||
|
||||
const adminService = createAdminService();
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
children?: React.ReactNode;
|
||||
@@ -22,7 +19,7 @@ export type GlobalHeaderRightProps = {
|
||||
export const AvatarName = () => {
|
||||
const { initialState } = useModel("@@initialState");
|
||||
const { currentUser } = initialState || {};
|
||||
return <span className="anticon">{currentUser?.name}</span>;
|
||||
return <span className="anticon">{currentUser?.nickName || currentUser?.userName}</span>;
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
@@ -51,7 +48,9 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
||||
* 退出登录,并且将当前的 url 保存
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
await adminService.Logout({});
|
||||
// 清除本地存储的token和用户信息
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
const { search, pathname } = window.location;
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
const searchParams = new URLSearchParams({
|
||||
@@ -80,6 +79,10 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
||||
loginOut();
|
||||
return;
|
||||
}
|
||||
if (key === "center") {
|
||||
history.push('/person');
|
||||
return;
|
||||
}
|
||||
history.push(`/account/${key}`);
|
||||
};
|
||||
|
||||
@@ -101,7 +104,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
|
||||
|
||||
const { currentUser } = initialState;
|
||||
|
||||
if (!currentUser || !currentUser.name) {
|
||||
if (!currentUser || (!currentUser.nickName && !currentUser.userName)) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
.dashboard {
|
||||
:global {
|
||||
.ant-page-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.statContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.statInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.statTrend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.statChart {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.quickCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 280px;
|
||||
|
||||
.shortcutItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f5ff;
|
||||
|
||||
.shortcutIcon {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcutIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.shortcutTitle {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.noticeCard,
|
||||
.docCard,
|
||||
.sysCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.sysCard {
|
||||
.sysItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.statCard {
|
||||
.statChart {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quickCard {
|
||||
.shortcutItem {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.shortcutIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shortcutTitle {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,268 @@
|
||||
/**
|
||||
* 仪表盘页面
|
||||
* 参考 GVA 的 Dashboard 布局
|
||||
*/
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Col, Row, Statistic } from 'antd';
|
||||
import { UserOutlined, TeamOutlined, ApiOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
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>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="用户总数" value={128} prefix={<UserOutlined />} />
|
||||
<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 span={6}>
|
||||
<Card>
|
||||
<Statistic title="角色数量" value={8} prefix={<TeamOutlined />} />
|
||||
|
||||
{/* 右侧快捷功能 */}
|
||||
<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>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="API数量" value={256} prefix={<ApiOutlined />} />
|
||||
</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 span={6}>
|
||||
<Card>
|
||||
<Statistic title="菜单数量" value={32} prefix={<MenuOutlined />} />
|
||||
|
||||
{/* 文档链接 */}
|
||||
<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>
|
||||
<Card style={{ marginTop: 16 }} title="欢迎使用 Kratos Admin">
|
||||
<p>这是一个基于 Kratos 框架的后台管理系统,前端使用 React + Ant Design Pro。</p>
|
||||
<p>功能与 GVA (gin-vue-admin) 保持一致。</p>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 系统参数管理页面
|
||||
* 与 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;
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 系统状态页面
|
||||
* 与 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;
|
||||
@@ -0,0 +1,204 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #194bfb;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.oblique {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
top: -15%;
|
||||
width: 60%;
|
||||
height: 130%;
|
||||
background: #fff;
|
||||
transform: rotate(-12deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 380px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #194bfb;
|
||||
}
|
||||
}
|
||||
|
||||
.inputIcon {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.captchaRow {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captchaInput {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.captchaImg {
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
background: #c3d4f2;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.loginBtn {
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: #194bfb;
|
||||
border-color: #194bfb;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(25, 75, 251, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: #1240d9;
|
||||
border-color: #1240d9;
|
||||
}
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: #194bfb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bannerContent {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
padding: 40px;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.featureItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oblique {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
/**
|
||||
* 登录页面
|
||||
* 与 GVA 保持一致的功能
|
||||
* 参考 GVA 的 UI 风格
|
||||
*/
|
||||
import { LockOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
|
||||
import { LoginForm, ProFormText } from '@ant-design/pro-components';
|
||||
import { history, useModel } from '@umijs/max';
|
||||
import { message, Image, Spin } from 'antd';
|
||||
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';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { setInitialState } = useModel('@@initialState');
|
||||
const [form] = Form.useForm();
|
||||
const [captchaInfo, setCaptchaInfo] = useState<{
|
||||
captchaId: string;
|
||||
picPath: string;
|
||||
openCaptcha: boolean;
|
||||
}>({ captchaId: '', picPath: '', openCaptcha: false });
|
||||
captchaLength: number;
|
||||
}>({ captchaId: '', picPath: '', openCaptcha: false, captchaLength: 6 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取验证码
|
||||
@@ -27,6 +29,7 @@ const Login: React.FC = () => {
|
||||
captchaId: res.data.captchaId,
|
||||
picPath: res.data.picPath,
|
||||
openCaptcha: res.data.openCaptcha,
|
||||
captchaLength: res.data.captchaLength || 6,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -48,15 +51,12 @@ const Login: React.FC = () => {
|
||||
});
|
||||
if (res.code === 0 && res.data) {
|
||||
message.success('登录成功');
|
||||
// 存储token
|
||||
localStorage.setItem('token', res.data.token);
|
||||
localStorage.setItem('userInfo', JSON.stringify(res.data.user));
|
||||
// 更新全局状态
|
||||
await setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: res.data?.user,
|
||||
}));
|
||||
// 跳转到首页
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
history.push(urlParams.get('redirect') || '/');
|
||||
} else {
|
||||
@@ -71,57 +71,122 @@ const Login: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}}>
|
||||
<Spin spinning={loading}>
|
||||
<LoginForm
|
||||
title="Kratos Admin"
|
||||
subTitle="基于 Kratos 的后台管理系统"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{ username: 'admin' }}
|
||||
>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{ size: 'large', prefix: <UserOutlined /> }}
|
||||
placeholder="请输入用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 5, message: '用户名至少5个字符' },
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{ size: 'large', prefix: <LockOutlined /> }}
|
||||
placeholder="请输入密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
/>
|
||||
{captchaInfo.openCaptcha && (
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
|
||||
<ProFormText
|
||||
name="captcha"
|
||||
fieldProps={{ size: 'large', prefix: <SafetyCertificateOutlined /> }}
|
||||
placeholder="请输入验证码"
|
||||
rules={[{ required: true, message: '请输入验证码' }]}
|
||||
/>
|
||||
<Image
|
||||
src={captchaInfo.picPath}
|
||||
alt="验证码"
|
||||
preview={false}
|
||||
style={{ height: 40, cursor: 'pointer' }}
|
||||
onClick={getCaptcha}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* 登录表单 */}
|
||||
<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}
|
||||
/>
|
||||
<div className={styles.captchaImg} onClick={getCaptcha}>
|
||||
{captchaInfo.picPath && (
|
||||
<img src={captchaInfo.picPath} alt="验证码" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block className={styles.loginBtn}>
|
||||
登 录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</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>
|
||||
)}
|
||||
</LoginForm>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className={styles.footer}>
|
||||
<span>Kratos Admin © 2024 基于 Go Kratos 框架</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
import type { RequestOptions } from "@@/plugin-request/request";
|
||||
import type { RequestConfig } from "@umijs/max";
|
||||
import { message as toast } from "antd";
|
||||
import { history } from "@umijs/max";
|
||||
|
||||
// Define the structure of the expected response.
|
||||
interface ResponseStructure {
|
||||
code: number;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
msg?: string;
|
||||
metadata?: Map<string, number>;
|
||||
}
|
||||
|
||||
const loginPath = "/user/login";
|
||||
|
||||
// Request configuration with error handling and interceptors.
|
||||
export const errorConfig: RequestConfig = {
|
||||
errorConfig: {
|
||||
errorThrower: (res) => {
|
||||
const { code, reason, message } = res as unknown as ResponseStructure;
|
||||
if (code != 200) {
|
||||
const error: any = new Error(message);
|
||||
error.info = { reason, message };
|
||||
const { code, reason, message, msg } = res as unknown as ResponseStructure;
|
||||
if (code !== 0 && code !== 200) {
|
||||
const error: any = new Error(message || msg);
|
||||
error.info = { reason, message: message || msg };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
errorHandler: (error: any, opts: any) => {
|
||||
if (opts?.skipErrorHandler) throw error;
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
history.push(loginPath);
|
||||
return;
|
||||
}
|
||||
if (error.response?.data) {
|
||||
const errorInfo: ResponseStructure | undefined = error.response?.data;
|
||||
if (errorInfo) {
|
||||
const { message, reason } = errorInfo;
|
||||
toast.error(reason + ": " + message);
|
||||
const { message, msg, reason } = errorInfo;
|
||||
toast.error((reason ? reason + ": " : "") + (message || msg || "请求失败"));
|
||||
}
|
||||
} else if (error.response) {
|
||||
toast.error(`Response status: ${error.response.status}`);
|
||||
@@ -40,12 +50,20 @@ export const errorConfig: RequestConfig = {
|
||||
},
|
||||
requestInterceptors: [
|
||||
(config: RequestOptions) => {
|
||||
// 添加token到请求头
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
'x-token': token,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
],
|
||||
responseInterceptors: [
|
||||
(response) => {
|
||||
if (response.status != 200) {
|
||||
if (response.status !== 200) {
|
||||
toast.error(`Request error: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -9,3 +9,5 @@ export * from './dictionary';
|
||||
export * from './dictionaryDetail';
|
||||
export * from './operationRecord';
|
||||
export * from './casbin';
|
||||
export * from './params';
|
||||
export * from './systemConfig';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 系统参数管理 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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 系统配置 API 服务
|
||||
* 与 GVA 保持一致的接口定义
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 获取服务器信息 POST /system/getServerInfo */
|
||||
export async function getServerInfo() {
|
||||
return request<API.ServerInfoResult>('/api/system/getServerInfo', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
Vendored
+68
@@ -398,3 +398,71 @@ declare namespace API {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 系统参数
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user