任务三

This commit is contained in:
Yvan
2026-01-07 19:10:02 +08:00
parent 3c0f3b7165
commit be3b6641a6
50 changed files with 2916 additions and 1167 deletions
-1
View File
@@ -168,7 +168,6 @@ export default defineConfig({
mock: {
include: ["mock/**/*", "src/pages/**/_mock.ts"],
},
utoopack: {},
requestRecord: {},
exportStatic: {},
define: {
+3 -2
View File
@@ -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": "" },
}
},
};
+12
View File
@@ -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
View File
@@ -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;
}
+133
View File
@@ -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;
}
}
}
+249 -20
View File
@@ -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>
);
};
+157
View File
@@ -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;
+126
View File
@@ -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;
+204
View File
@@ -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;
}
}
+122 -57
View File
@@ -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>
);
};
+25 -7
View File
@@ -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;
+2
View File
@@ -9,3 +9,5 @@ export * from './dictionary';
export * from './dictionaryDetail';
export * from './operationRecord';
export * from './casbin';
export * from './params';
export * from './systemConfig';
+61
View File
@@ -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,
});
}
+12
View File
@@ -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',
});
}
+68
View File
@@ -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;
};
};