任务三

This commit is contained in:
Yvan 2026-01-07 15:42:48 +08:00
parent ada3084683
commit 81f6883635
24 changed files with 4755 additions and 1611 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,25 @@ import "google/protobuf/duration.proto";
message Bootstrap {
Server server = 1;
Data data = 2;
JWT jwt = 3;
Casbin casbin = 4;
Captcha captcha = 5;
Upload upload = 6;
JWT jwt = 2;
System system = 3;
Captcha captcha = 4;
Mysql mysql = 5;
Pgsql pgsql = 6;
Sqlite sqlite = 7;
Redis redis = 8;
Local local = 9;
Qiniu qiniu = 10;
Minio minio = 11;
AliyunOss aliyun_oss = 12;
TencentCos tencent_cos = 13;
AwsS3 aws_s3 = 14;
CloudflareR2 cloudflare_r2 = 15;
HuaweiObs huawei_obs = 16;
Email email = 17;
Excel excel = 18;
Cors cors = 19;
Zap zap = 20;
}
message Server {
@ -29,27 +43,6 @@ message Server {
GRPC grpc = 2;
}
message Data {
message Database {
string driver = 1;
string source = 2;
string table_prefix = 3;
int32 max_idle_conns = 4;
int32 max_open_conns = 5;
int32 max_lifetime = 6;
bool log_mode = 7;
}
message Redis {
string addr = 1;
string password = 2;
int32 db = 3;
bool use_cluster = 4;
repeated string cluster_addrs = 5;
}
Database database = 1;
Redis redis = 2;
}
message JWT {
string signing_key = 1;
string expires_time = 2;
@ -57,8 +50,16 @@ message JWT {
string issuer = 4;
}
message Casbin {
string model_path = 1;
message System {
string env = 1;
string db_type = 2;
string oss_type = 3;
bool use_redis = 4;
bool use_multipoint = 5;
int32 iplimit_count = 6;
int32 iplimit_time = 7;
string router_prefix = 8;
bool use_strict_auth = 9;
}
message Captcha {
@ -69,80 +70,157 @@ message Captcha {
int32 open_captcha_timeout = 5;
}
message Upload {
string type = 1;
string path = 2;
int64 max_size = 3;
message Local {
string store_path = 1;
}
message Aliyun {
string endpoint = 1;
string access_key_id = 2;
string access_key_secret = 3;
string bucket_name = 4;
string bucket_url = 5;
string base_path = 6;
}
message Tencent {
string bucket = 1;
string region = 2;
string secret_id = 3;
string secret_key = 4;
string base_url = 5;
string path_prefix = 6;
}
message Qiniu {
string zone = 1;
string bucket = 2;
string img_path = 3;
string access_key = 4;
string secret_key = 5;
bool use_https = 6;
bool use_cdn_domains = 7;
}
message Minio {
string id = 1;
string secret = 2;
string bucket = 3;
string endpoint = 4;
string base_path = 5;
bool use_ssl = 6;
}
message AwsS3 {
string bucket = 1;
string region = 2;
string endpoint = 3;
string secret_id = 4;
string secret_key = 5;
string base_url = 6;
string path_prefix = 7;
bool s3_force_path_style = 8;
bool disable_ssl = 9;
}
message HuaweiObs {
string path = 1;
string bucket = 2;
string endpoint = 3;
string access_key = 4;
string secret_key = 5;
}
message CloudflareR2 {
string bucket = 1;
string base_url = 2;
string path = 3;
string account_id = 4;
string access_key_id = 5;
string secret_access_key = 6;
}
Local local = 4;
Aliyun aliyun = 5;
Tencent tencent = 6;
Qiniu qiniu = 7;
Minio minio = 8;
AwsS3 aws_s3 = 9;
HuaweiObs huawei_obs = 10;
CloudflareR2 cloudflare_r2 = 11;
message Mysql {
string path = 1;
string port = 2;
string config = 3;
string db_name = 4;
string username = 5;
string password = 6;
int32 max_idle_conns = 7;
int32 max_open_conns = 8;
string log_mode = 9;
bool log_zap = 10;
}
message Pgsql {
string path = 1;
string port = 2;
string config = 3;
string db_name = 4;
string username = 5;
string password = 6;
int32 max_idle_conns = 7;
int32 max_open_conns = 8;
string log_mode = 9;
bool log_zap = 10;
}
message Sqlite {
string path = 1;
string db_name = 2;
int32 max_idle_conns = 3;
int32 max_open_conns = 4;
string log_mode = 5;
bool log_zap = 6;
}
message Redis {
bool use_cluster = 1;
string addr = 2;
string password = 3;
int32 db = 4;
repeated string cluster_addrs = 5;
}
message Local {
string path = 1;
string store_path = 2;
}
message Qiniu {
string zone = 1;
string bucket = 2;
string img_path = 3;
bool use_https = 4;
string access_key = 5;
string secret_key = 6;
bool use_cdn_domains = 7;
}
message Minio {
string endpoint = 1;
string access_key_id = 2;
string access_key_secret = 3;
string bucket_name = 4;
bool use_ssl = 5;
string base_path = 6;
string bucket_url = 7;
}
message AliyunOss {
string endpoint = 1;
string access_key_id = 2;
string access_key_secret = 3;
string bucket_name = 4;
string bucket_url = 5;
string base_path = 6;
}
message TencentCos {
string bucket = 1;
string region = 2;
string secret_id = 3;
string secret_key = 4;
string base_url = 5;
string path_prefix = 6;
}
message AwsS3 {
string bucket = 1;
string region = 2;
string endpoint = 3;
bool s3_force_path_style = 4;
bool disable_ssl = 5;
string secret_id = 6;
string secret_key = 7;
string base_url = 8;
string path_prefix = 9;
}
message CloudflareR2 {
string bucket = 1;
string base_url = 2;
string path = 3;
string account_id = 4;
string access_key_id = 5;
string secret_access_key = 6;
}
message HuaweiObs {
string path = 1;
string bucket = 2;
string endpoint = 3;
string access_key = 4;
string secret_key = 5;
}
message Email {
string to = 1;
int32 port = 2;
string from = 3;
string host = 4;
bool is_ssl = 5;
string secret = 6;
string nickname = 7;
}
message Excel {
string dir = 1;
}
message Cors {
string mode = 1;
repeated CorsWhitelist whitelist = 2;
}
message CorsWhitelist {
string allow_origin = 1;
string allow_headers = 2;
string allow_methods = 3;
string expose_headers = 4;
bool allow_credentials = 5;
}
message Zap {
string level = 1;
string format = 2;
string prefix = 3;
string director = 4;
bool show_line = 5;
string encode_level = 6;
string stacktrace_key = 7;
bool log_in_console = 8;
int32 retention_day = 9;
}

View File

@ -1,6 +1,7 @@
package data
import (
"fmt"
"kra/internal/conf"
"kra/internal/data/query"
@ -20,8 +21,10 @@ type Data struct {
}
// NewDB 创建数据库连接
func NewDB(c *conf.Data) (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{
func NewDB(c *conf.Mysql) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s",
c.Username, c.Password, c.Path, c.Port, c.DbName, c.Config)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {

View File

@ -1,47 +1,83 @@
/**
* @name umi
* @description path,component,routes,redirect,wrappers,name,icon
* @param path path 第一种是动态参数 :id *
* @param component location path React src/pages
* @param routes layout 使
* @param redirect
* @param wrappers
* @param name menu.ts menu.xxxx name login menu.ts menu.login
* @param icon https://ant.design/components/icon-cn 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes
* @description GVA
*/
export default [
{
path: "/user",
path: '/user',
layout: false,
routes: [
{
name: "login",
path: "/user/login",
component: "./user/login",
name: 'login',
path: '/user/login',
component: './user/login',
},
],
},
{
path: "/welcome",
name: "welcome",
icon: "smile",
component: "./Welcome",
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
component: './dashboard',
},
{
name: "admin",
icon: "crown",
path: "/admins",
access: "canAdmin",
component: "./admins",
path: '/system',
name: 'system',
icon: 'setting',
access: 'canAdmin',
routes: [
{
path: '/system/user',
name: 'user',
icon: 'user',
component: './system/user',
},
{
path: '/system/authority',
name: 'authority',
icon: 'team',
component: './system/authority',
},
{
path: '/system/menu',
name: 'menu',
icon: 'menu',
component: './system/menu',
},
{
path: '/system/api',
name: 'api',
icon: 'api',
component: './system/api',
},
{
path: '/system/dictionary',
name: 'dictionary',
icon: 'book',
component: './system/dictionary',
},
{
path: '/system/operation',
name: 'operation',
icon: 'fileSearch',
component: './system/operation',
},
],
},
{
path: "/",
redirect: "/welcome",
path: '/person',
name: 'person',
icon: 'idcard',
component: './person',
hideInMenu: true,
},
{
component: "404",
path: '/',
redirect: '/dashboard',
},
{
path: '*',
layout: false,
path: "./*",
component: './404',
},
];

View File

@ -1,20 +1,22 @@
import { history, useIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd';
import React from 'react';
/**
* 404
*/
import { Button, Result } from 'antd';
import { history } from '@umijs/max';
const NoFoundPage: React.FC = () => (
<Card variant="borderless">
const NotFoundPage: React.FC = () => {
return (
<Result
status="404"
title="404"
subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })}
subTitle="抱歉,您访问的页面不存在"
extra={
<Button type="primary" onClick={() => history.push('/')}>
{useIntl().formatMessage({ id: 'pages.404.buttonText' })}
</Button>
}
/>
</Card>
);
);
};
export default NoFoundPage;
export default NotFoundPage;

View File

@ -0,0 +1,41 @@
/**
*
*/
import { PageContainer } from '@ant-design/pro-components';
import { Card, Col, Row, Statistic } from 'antd';
import { UserOutlined, TeamOutlined, ApiOutlined, MenuOutlined } from '@ant-design/icons';
const Dashboard: React.FC = () => {
return (
<PageContainer>
<Row gutter={16}>
<Col span={6}>
<Card>
<Statistic title="用户总数" value={128} prefix={<UserOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="角色数量" value={8} prefix={<TeamOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="API数量" value={256} prefix={<ApiOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="菜单数量" value={32} prefix={<MenuOutlined />} />
</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>
);
};
export default Dashboard;

View File

@ -0,0 +1,338 @@
/**
*
* GVA person.vue
*/
import {
EditOutlined,
PhoneOutlined,
MailOutlined,
LockOutlined,
UserOutlined,
EnvironmentOutlined,
BankOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import {
Card,
Avatar,
Button,
Form,
Input,
Modal,
message,
Row,
Col,
Typography,
Tag,
Timeline,
Statistic,
Tabs,
Space,
} from 'antd';
import { useState, useEffect } from 'react';
import { useModel } from '@umijs/max';
import { setSelfInfo, changePassword } from '@/services/system/user';
const { Title, Text } = Typography;
const PersonPage: React.FC = () => {
const { initialState, setInitialState } = useModel('@@initialState');
const userInfo = initialState?.currentUser || {};
const [editNickName, setEditNickName] = useState(false);
const [nickName, setNickName] = useState('');
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [phoneModalVisible, setPhoneModalVisible] = useState(false);
const [emailModalVisible, setEmailModalVisible] = useState(false);
const [phoneCountdown, setPhoneCountdown] = useState(0);
const [emailCountdown, setEmailCountdown] = useState(0);
const [passwordForm] = Form.useForm();
const [phoneForm] = Form.useForm();
const [emailForm] = Form.useForm();
// 修改昵称
const handleEditNickName = () => {
setNickName(userInfo.nickName || '');
setEditNickName(true);
};
const handleSaveNickName = async () => {
const res = await setSelfInfo({ nickName });
if (res.code === 0) {
message.success('修改成功');
setInitialState((s) => ({
...s,
currentUser: { ...s?.currentUser, nickName },
}));
setEditNickName(false);
}
};
// 修改密码
const handleChangePassword = async (values: any) => {
if (values.newPassword !== values.confirmPassword) {
message.error('两次密码不一致');
return;
}
const res = await changePassword({
password: values.password,
newPassword: values.newPassword,
});
if (res.code === 0) {
message.success('修改密码成功');
setPasswordModalVisible(false);
passwordForm.resetFields();
}
};
// 获取手机验证码
const getPhoneCode = () => {
setPhoneCountdown(60);
const timer = setInterval(() => {
setPhoneCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
};
// 修改手机号
const handleChangePhone = async (values: any) => {
const res = await setSelfInfo({ phone: values.phone });
if (res.code === 0) {
message.success('修改成功');
setInitialState((s) => ({
...s,
currentUser: { ...s?.currentUser, phone: values.phone },
}));
setPhoneModalVisible(false);
phoneForm.resetFields();
}
};
// 获取邮箱验证码
const getEmailCode = () => {
setEmailCountdown(60);
const timer = setInterval(() => {
setEmailCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
};
// 修改邮箱
const handleChangeEmail = async (values: any) => {
const res = await setSelfInfo({ email: values.email });
if (res.code === 0) {
message.success('修改成功');
setInitialState((s) => ({
...s,
currentUser: { ...s?.currentUser, email: values.email },
}));
setEmailModalVisible(false);
emailForm.resetFields();
}
};
// 活动数据
const activities = [
{ timestamp: '2024-01-10', title: '完成项目里程碑', content: '成功完成第三季度主要项目开发任务', color: 'blue' },
{ timestamp: '2024-01-11', title: '代码审核完成', content: '完成核心模块代码审核,提出多项改进建议', color: 'green' },
{ timestamp: '2024-01-12', title: '技术分享会', content: '主持团队技术分享会,分享前端性能优化经验', color: 'orange' },
{ timestamp: '2024-01-13', title: '新功能上线', content: '成功上线用户反馈的新特性', color: 'red' },
];
return (
<PageContainer>
{/* 顶部个人信息卡片 */}
<Card style={{ marginBottom: 24 }}>
<div style={{ background: '#f0f5ff', height: 120, marginBottom: -60, borderRadius: 8 }} />
<Row gutter={24} align="middle" style={{ paddingLeft: 24 }}>
<Col>
<Avatar size={100} src={userInfo.headerImg} icon={<UserOutlined />} />
</Col>
<Col flex={1} style={{ paddingTop: 60 }}>
<Space align="center" style={{ marginBottom: 16 }}>
{!editNickName ? (
<>
<Title level={4} style={{ margin: 0 }}>{userInfo.nickName}</Title>
<Button type="text" icon={<EditOutlined />} onClick={handleEditNickName} />
</>
) : (
<>
<Input
value={nickName}
onChange={(e) => setNickName(e.target.value)}
style={{ width: 200 }}
/>
<Button type="primary" onClick={handleSaveNickName}></Button>
<Button onClick={() => setEditNickName(false)}></Button>
</>
)}
</Space>
<Space size={24}>
<Text type="secondary"><EnvironmentOutlined /> ··</Text>
<Text type="secondary"><BankOutlined /> </Text>
<Text type="secondary"><UserOutlined /> ·</Text>
</Space>
</Col>
</Row>
</Card>
<Row gutter={24}>
{/* 左侧信息栏 */}
<Col span={8}>
<Card title="基本信息" style={{ marginBottom: 24 }}>
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Row justify="space-between" align="middle">
<Space><PhoneOutlined style={{ color: '#1890ff' }} /><Text></Text><Text>{userInfo.phone || '未设置'}</Text></Space>
<Button type="link" onClick={() => setPhoneModalVisible(true)}></Button>
</Row>
<Row justify="space-between" align="middle">
<Space><MailOutlined style={{ color: '#52c41a' }} /><Text></Text><Text>{userInfo.email || '未设置'}</Text></Space>
<Button type="link" onClick={() => setEmailModalVisible(true)}></Button>
</Row>
<Row justify="space-between" align="middle">
<Space><LockOutlined style={{ color: '#722ed1' }} /><Text></Text><Text></Text></Space>
<Button type="link" onClick={() => setPasswordModalVisible(true)}></Button>
</Row>
</Space>
</Card>
<Card title="技能特长">
<Space wrap>
<Tag color="success">GoLang</Tag>
<Tag color="warning">JavaScript</Tag>
<Tag color="error">Vue</Tag>
<Tag color="default">Gorm</Tag>
<Button type="dashed" size="small">+ </Button>
</Space>
</Card>
</Col>
{/* 右侧内容区 */}
<Col span={16}>
<Card>
<Tabs
items={[
{
key: 'stats',
label: '数据统计',
children: (
<Row gutter={24} style={{ padding: '24px 0' }}>
<Col span={6}><Statistic title="项目参与" value={138} valueStyle={{ color: '#1890ff' }} /></Col>
<Col span={6}><Statistic title="代码提交" value={2300} suffix="次" valueStyle={{ color: '#52c41a' }} /></Col>
<Col span={6}><Statistic title="任务完成" value={95} suffix="%" valueStyle={{ color: '#722ed1' }} /></Col>
<Col span={6}><Statistic title="获得勋章" value={12} valueStyle={{ color: '#faad14' }} /></Col>
</Row>
),
},
{
key: 'activities',
label: '近期动态',
children: (
<Timeline style={{ padding: '24px 0' }} items={activities.map((item) => ({
color: item.color,
children: (
<>
<Text strong>{item.title}</Text>
<br />
<Text type="secondary">{item.content}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>{item.timestamp}</Text>
</>
),
}))} />
),
},
]}
/>
</Card>
</Col>
</Row>
{/* 修改密码弹窗 */}
<Modal
title="修改密码"
open={passwordModalVisible}
onCancel={() => { setPasswordModalVisible(false); passwordForm.resetFields(); }}
onOk={() => passwordForm.submit()}
>
<Form form={passwordForm} onFinish={handleChangePassword} layout="vertical">
<Form.Item name="password" label="原密码" rules={[{ required: true }, { min: 6, message: '最少6个字符' }]}>
<Input.Password />
</Form.Item>
<Form.Item name="newPassword" label="新密码" rules={[{ required: true }, { min: 6, message: '最少6个字符' }]}>
<Input.Password />
</Form.Item>
<Form.Item name="confirmPassword" label="确认密码" rules={[
{ required: true },
{ min: 6, message: '最少6个字符' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) return Promise.resolve();
return Promise.reject(new Error('两次密码不一致'));
},
}),
]}>
<Input.Password />
</Form.Item>
</Form>
</Modal>
{/* 修改手机号弹窗 */}
<Modal
title="修改手机号"
open={phoneModalVisible}
onCancel={() => { setPhoneModalVisible(false); phoneForm.resetFields(); }}
onOk={() => phoneForm.submit()}
>
<Form form={phoneForm} onFinish={handleChangePhone} layout="vertical">
<Form.Item name="phone" label="手机号" rules={[{ required: true }]}>
<Input prefix={<PhoneOutlined />} placeholder="请输入新的手机号码" />
</Form.Item>
<Form.Item name="code" label="验证码" rules={[{ required: true }]}>
<Space.Compact style={{ width: '100%' }}>
<Input placeholder="请输入验证码[模拟]" style={{ flex: 1 }} />
<Button type="primary" disabled={phoneCountdown > 0} onClick={getPhoneCode}>
{phoneCountdown > 0 ? `${phoneCountdown}s` : '获取验证码'}
</Button>
</Space.Compact>
</Form.Item>
</Form>
</Modal>
{/* 修改邮箱弹窗 */}
<Modal
title="修改邮箱"
open={emailModalVisible}
onCancel={() => { setEmailModalVisible(false); emailForm.resetFields(); }}
onOk={() => emailForm.submit()}
>
<Form form={emailForm} onFinish={handleChangeEmail} layout="vertical">
<Form.Item name="email" label="邮箱" rules={[{ required: true }, { type: 'email', message: '请输入有效的邮箱地址' }]}>
<Input prefix={<MailOutlined />} placeholder="请输入新的邮箱地址" />
</Form.Item>
<Form.Item name="code" label="验证码" rules={[{ required: true }]}>
<Space.Compact style={{ width: '100%' }}>
<Input placeholder="请输入验证码[模拟]" style={{ flex: 1 }} />
<Button type="primary" disabled={emailCountdown > 0} onClick={getEmailCode}>
{emailCountdown > 0 ? `${emailCountdown}s` : '获取验证码'}
</Button>
</Space.Compact>
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
};
export default PersonPage;

View File

@ -0,0 +1,147 @@
/**
* API管理页面
* GVA
*/
import { PlusOutlined, DeleteOutlined, EditOutlined, SyncOutlined } from '@ant-design/icons';
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Space, Tag } from 'antd';
import { useRef, useState, useEffect } from 'react';
import { getApiList, createApi, updateApi, deleteApi, syncApi, getApiGroups, freshCasbin } from '@/services/system/api';
const methodColors: Record<string, string> = {
GET: 'green',
POST: 'blue',
PUT: 'orange',
DELETE: 'red',
PATCH: 'purple',
};
const ApiPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.ApiItem>();
const [isEdit, setIsEdit] = useState(false);
const [apiGroups, setApiGroups] = useState<string[]>([]);
const fetchApiGroups = async () => {
const res = await getApiGroups();
if (res.code === 0 && res.data?.groups) {
setApiGroups(res.data.groups);
}
};
useEffect(() => {
fetchApiGroups();
}, []);
const handleDelete = async (ids: number[]) => {
const res = await deleteApi({ ids });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
}
};
const handleSync = async () => {
const res = await syncApi();
if (res.code === 0) {
message.success('同步成功');
actionRef.current?.reload();
}
};
const handleFreshCasbin = async () => {
const res = await freshCasbin();
if (res.code === 0) {
message.success('刷新成功');
}
};
const columns: ProColumns<API.ApiItem>[] = [
{ title: 'ID', dataIndex: 'ID', search: false },
{ title: 'API路径', dataIndex: 'path' },
{ title: 'API描述', dataIndex: 'description' },
{ title: 'API分组', dataIndex: 'apiGroup', valueType: 'select', valueEnum: Object.fromEntries(apiGroups.map((g) => [g, g])) },
{
title: '请求方法',
dataIndex: 'method',
valueType: 'select',
valueEnum: { GET: 'GET', POST: 'POST', PUT: 'PUT', DELETE: 'DELETE', PATCH: 'PATCH' },
render: (_, record) => <Tag color={methodColors[record.method || '']}>{record.method}</Tag>,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}></Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete([record.ID!])}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
<ProTable<API.ApiItem>
headerTitle="API列表"
actionRef={actionRef}
rowKey="ID"
columns={columns}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
setCurrentRow(undefined);
setIsEdit(false);
setModalVisible(true);
}}>API</Button>,
<Button key="sync" icon={<SyncOutlined />} onClick={handleSync}>API</Button>,
<Button key="fresh" onClick={handleFreshCasbin}></Button>,
]}
request={async (params) => {
const res = await getApiList({ page: params.current, pageSize: params.pageSize, ...params });
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
<ModalForm
title={isEdit ? '编辑API' : '新增API'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
onFinish={async (values) => {
let res;
if (isEdit) {
res = await updateApi({ ...currentRow, ...values });
} else {
res = await createApi(values);
}
if (res.code === 0) {
message.success(isEdit ? '编辑成功' : '创建成功');
setModalVisible(false);
actionRef.current?.reload();
return true;
}
return false;
}}
>
<ProFormText name="path" label="API路径" rules={[{ required: true }]} />
<ProFormText name="description" label="API描述" rules={[{ required: true }]} />
<ProFormSelect name="apiGroup" label="API分组" options={apiGroups.map((g) => ({ label: g, value: g }))} rules={[{ required: true }]} />
<ProFormSelect name="method" label="请求方法" options={['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].map((m) => ({ label: m, value: m }))} rules={[{ required: true }]} />
</ModalForm>
</PageContainer>
);
};
export default ApiPage;

View File

@ -0,0 +1,252 @@
/**
*
* GVA
*/
import { PlusOutlined, DeleteOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormDigit } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Space, Tree, Card, Row, Col } from 'antd';
import { useRef, useState, useEffect } from 'react';
import {
getAuthorityList,
createAuthority,
updateAuthority,
deleteAuthority,
copyAuthority,
} from '@/services/system/authority';
import { getBaseMenuTree, addMenuAuthority, getMenuAuthority } from '@/services/system/menu';
import { getAllApis } from '@/services/system/api';
import { updateCasbin, getPolicyPathByAuthorityId } from '@/services/system/casbin';
const AuthorityPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.Authority>();
const [isEdit, setIsEdit] = useState(false);
const [menuTree, setMenuTree] = useState<any[]>([]);
const [apiList, setApiList] = useState<API.ApiItem[]>([]);
const [checkedMenuKeys, setCheckedMenuKeys] = useState<number[]>([]);
const [checkedApiKeys, setCheckedApiKeys] = useState<string[]>([]);
const [selectedAuthority, setSelectedAuthority] = useState<API.Authority>();
// 获取菜单树
const fetchMenuTree = async () => {
const res = await getBaseMenuTree();
if (res.code === 0 && res.data?.menus) {
setMenuTree(buildTreeData(res.data.menus));
}
};
// 获取API列表
const fetchApis = async () => {
const res = await getAllApis({});
if (res.code === 0 && res.data?.apis) {
setApiList(res.data.apis);
}
};
// 构建树形数据
const buildTreeData = (data: API.Menu[]): any[] => {
return data.map((item) => ({
key: item.ID,
title: item.meta?.title || item.name,
children: item.children ? buildTreeData(item.children) : undefined,
}));
};
useEffect(() => {
fetchMenuTree();
fetchApis();
}, []);
// 选择角色时加载权限
const handleSelectAuthority = async (record: API.Authority) => {
setSelectedAuthority(record);
// 获取菜单权限
const menuRes = await getMenuAuthority({ authorityId: record.authorityId! });
if (menuRes.code === 0 && menuRes.data?.menus) {
const menuIds = extractMenuIds(menuRes.data.menus);
setCheckedMenuKeys(menuIds);
}
// 获取API权限
const apiRes = await getPolicyPathByAuthorityId({ authorityId: record.authorityId! });
if (apiRes.code === 0 && apiRes.data?.paths) {
const apiKeys = apiRes.data.paths.map((p) => `${p.path}:${p.method}`);
setCheckedApiKeys(apiKeys);
}
};
// 提取菜单ID
const extractMenuIds = (menus: API.Menu[]): number[] => {
const ids: number[] = [];
const extract = (list: API.Menu[]) => {
list.forEach((m) => {
if (m.ID) ids.push(m.ID);
if (m.children) extract(m.children);
});
};
extract(menus);
return ids;
};
// 保存菜单权限
const handleSaveMenuAuth = async () => {
if (!selectedAuthority) return;
const menus = checkedMenuKeys.map((id) => ({ ID: id }));
const res = await addMenuAuthority({
menus: menus as API.Menu[],
authorityId: selectedAuthority.authorityId!,
});
if (res.code === 0) {
message.success('菜单权限保存成功');
}
};
// 保存API权限
const handleSaveApiAuth = async () => {
if (!selectedAuthority) return;
const casbinInfos = checkedApiKeys.map((key) => {
const [path, method] = key.split(':');
return { path, method };
});
const res = await updateCasbin({
authorityId: selectedAuthority.authorityId!,
casbinInfos,
});
if (res.code === 0) {
message.success('API权限保存成功');
}
};
// 删除角色
const handleDelete = async (authorityId: number) => {
const res = await deleteAuthority({ authorityId });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
}
};
// 拷贝角色
const handleCopy = async (record: API.Authority) => {
const res = await copyAuthority({
authority: { ...record, authorityName: `${record.authorityName}_copy` },
oldAuthorityId: record.authorityId!,
});
if (res.code === 0) {
message.success('拷贝成功');
actionRef.current?.reload();
}
};
const columns: ProColumns<API.Authority>[] = [
{ title: '角色ID', dataIndex: 'authorityId' },
{ title: '角色名称', dataIndex: 'authorityName' },
{ title: '父角色ID', dataIndex: 'parentId' },
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}></Button>
<Button type="link" icon={<CopyOutlined />} onClick={() => handleCopy(record)}></Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.authorityId!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
<Button type="link" onClick={() => handleSelectAuthority(record)}></Button>
</Space>
),
},
];
return (
<PageContainer>
<Row gutter={16}>
<Col span={12}>
<ProTable<API.Authority>
headerTitle="角色列表"
actionRef={actionRef}
rowKey="authorityId"
columns={columns}
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
setCurrentRow(undefined);
setIsEdit(false);
setModalVisible(true);
}}></Button>,
]}
request={async (params) => {
const res = await getAuthorityList({ page: params.current, pageSize: params.pageSize });
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
</Col>
<Col span={12}>
{selectedAuthority && (
<Card title={`权限设置 - ${selectedAuthority.authorityName}`}>
<Card type="inner" title="菜单权限" extra={<Button onClick={handleSaveMenuAuth}></Button>}>
<Tree
checkable
treeData={menuTree}
checkedKeys={checkedMenuKeys}
onCheck={(keys) => setCheckedMenuKeys(keys as number[])}
/>
</Card>
<Card type="inner" title="API权限" style={{ marginTop: 16 }} extra={<Button onClick={handleSaveApiAuth}></Button>}>
<Tree
checkable
treeData={apiList.map((api) => ({
key: `${api.path}:${api.method}`,
title: `${api.description} [${api.method}] ${api.path}`,
}))}
checkedKeys={checkedApiKeys}
onCheck={(keys) => setCheckedApiKeys(keys as string[])}
height={300}
/>
</Card>
</Card>
)}
</Col>
</Row>
<ModalForm
title={isEdit ? '编辑角色' : '新增角色'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
onFinish={async (values) => {
let res;
if (isEdit) {
res = await updateAuthority({ ...currentRow, ...values });
} else {
res = await createAuthority(values);
}
if (res.code === 0) {
message.success(isEdit ? '编辑成功' : '创建成功');
setModalVisible(false);
actionRef.current?.reload();
return true;
}
return false;
}}
>
<ProFormDigit name="authorityId" label="角色ID" rules={[{ required: true }]} disabled={isEdit} />
<ProFormText name="authorityName" label="角色名称" rules={[{ required: true }]} />
<ProFormDigit name="parentId" label="父角色ID" initialValue={0} />
</ModalForm>
</PageContainer>
);
};
export default AuthorityPage;

View File

@ -0,0 +1,264 @@
/**
*
* GVA
*/
import { PlusOutlined, DeleteOutlined, EditOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons';
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSwitch } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Space, Card, Row, Col, Upload, Switch } from 'antd';
import { useRef, useState } from 'react';
import {
getSysDictionaryList,
createSysDictionary,
updateSysDictionary,
deleteSysDictionary,
exportSysDictionary,
importSysDictionary,
} from '@/services/system/dictionary';
import {
getSysDictionaryDetailList,
createSysDictionaryDetail,
updateSysDictionaryDetail,
deleteSysDictionaryDetail,
} from '@/services/system/dictionaryDetail';
const DictionaryPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const detailActionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.Dictionary>();
const [currentDetail, setCurrentDetail] = useState<API.DictionaryDetail>();
const [isEdit, setIsEdit] = useState(false);
const [isDetailEdit, setIsDetailEdit] = useState(false);
const [selectedDict, setSelectedDict] = useState<API.Dictionary>();
const handleDelete = async (id: number) => {
const res = await deleteSysDictionary({ ID: id });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
}
};
const handleDeleteDetail = async (id: number) => {
const res = await deleteSysDictionaryDetail({ ID: id });
if (res.code === 0) {
message.success('删除成功');
detailActionRef.current?.reload();
}
};
const handleExport = async (id: number) => {
const res = await exportSysDictionary({ ID: id });
if (res.code === 0 && res.data) {
const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dictionary_${id}.json`;
a.click();
message.success('导出成功');
}
};
const handleImport = async (file: File) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = JSON.parse(e.target?.result as string);
const res = await importSysDictionary(data);
if (res.code === 0) {
message.success('导入成功');
actionRef.current?.reload();
}
} catch {
message.error('文件格式错误');
}
};
reader.readAsText(file);
return false;
};
const dictColumns: ProColumns<API.Dictionary>[] = [
{ title: 'ID', dataIndex: 'ID', search: false },
{ title: '字典名称', dataIndex: 'name' },
{ title: '字典类型', dataIndex: 'type' },
{ title: '描述', dataIndex: 'desc', search: false },
{
title: '状态',
dataIndex: 'status',
search: false,
render: (_, record) => <Switch checked={record.status} disabled />,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Button type="link" onClick={() => setSelectedDict(record)}></Button>
<Button type="link" icon={<EditOutlined />} onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}></Button>
<Button type="link" icon={<DownloadOutlined />} onClick={() => handleExport(record.ID!)}></Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
];
const detailColumns: ProColumns<API.DictionaryDetail>[] = [
{ title: 'ID', dataIndex: 'ID' },
{ title: '标签', dataIndex: 'label' },
{ title: '值', dataIndex: 'value' },
{ title: '扩展', dataIndex: 'extend' },
{ title: '排序', dataIndex: 'sort' },
{
title: '状态',
dataIndex: 'status',
render: (_, record) => <Switch checked={record.status} disabled />,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => {
setCurrentDetail(record);
setIsDetailEdit(true);
setDetailModalVisible(true);
}}></Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDeleteDetail(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
<Row gutter={16}>
<Col span={12}>
<ProTable<API.Dictionary>
headerTitle="字典列表"
actionRef={actionRef}
rowKey="ID"
columns={dictColumns}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
setCurrentRow(undefined);
setIsEdit(false);
setModalVisible(true);
}}></Button>,
<Upload key="import" accept=".json" showUploadList={false} beforeUpload={handleImport}>
<Button icon={<UploadOutlined />}></Button>
</Upload>,
]}
request={async (params) => {
const res = await getSysDictionaryList({ page: params.current, pageSize: params.pageSize, ...params });
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
</Col>
<Col span={12}>
{selectedDict && (
<Card title={`字典详情 - ${selectedDict.name}`}>
<ProTable<API.DictionaryDetail>
actionRef={detailActionRef}
rowKey="ID"
columns={detailColumns}
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
setCurrentDetail({ sysDictionaryID: selectedDict.ID });
setIsDetailEdit(false);
setDetailModalVisible(true);
}}></Button>,
]}
request={async (params) => {
const res = await getSysDictionaryDetailList({
page: params.current,
pageSize: params.pageSize,
sysDictionaryID: selectedDict.ID,
});
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
</Card>
)}
</Col>
</Row>
<ModalForm
title={isEdit ? '编辑字典' : '新增字典'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
onFinish={async (values) => {
let res;
if (isEdit) {
res = await updateSysDictionary({ ...currentRow, ...values });
} else {
res = await createSysDictionary(values);
}
if (res.code === 0) {
message.success(isEdit ? '编辑成功' : '创建成功');
setModalVisible(false);
actionRef.current?.reload();
return true;
}
return false;
}}
>
<ProFormText name="name" label="字典名称" rules={[{ required: true }]} />
<ProFormText name="type" label="字典类型" rules={[{ required: true }]} />
<ProFormText name="desc" label="描述" />
<ProFormSwitch name="status" label="状态" initialValue={true} />
</ModalForm>
<ModalForm
title={isDetailEdit ? '编辑详情' : '新增详情'}
open={detailModalVisible}
onOpenChange={setDetailModalVisible}
initialValues={currentDetail}
onFinish={async (values) => {
let res;
if (isDetailEdit) {
res = await updateSysDictionaryDetail({ ...currentDetail, ...values });
} else {
res = await createSysDictionaryDetail({ ...currentDetail, ...values });
}
if (res.code === 0) {
message.success(isDetailEdit ? '编辑成功' : '创建成功');
setDetailModalVisible(false);
detailActionRef.current?.reload();
return true;
}
return false;
}}
>
<ProFormText name="label" label="标签" rules={[{ required: true }]} />
<ProFormText name="value" label="值" rules={[{ required: true }]} />
<ProFormText name="extend" label="扩展" />
<ProFormText name="sort" label="排序" initialValue={0} />
<ProFormSwitch name="status" label="状态" initialValue={true} />
</ModalForm>
</PageContainer>
);
};
export default DictionaryPage;

View File

@ -0,0 +1,126 @@
/**
*
* GVA
*/
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormDigit, ProFormSwitch, ProFormSelect } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Space } from 'antd';
import { useRef, useState } from 'react';
import { getMenuList, addBaseMenu, updateBaseMenu, deleteBaseMenu } from '@/services/system/menu';
const MenuPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.Menu>();
const [isEdit, setIsEdit] = useState(false);
const handleDelete = async (id: number) => {
const res = await deleteBaseMenu({ ID: id });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
}
};
const columns: ProColumns<API.Menu>[] = [
{ title: 'ID', dataIndex: 'ID', search: false },
{ title: '路由名称', dataIndex: 'name' },
{ title: '路由路径', dataIndex: 'path' },
{ title: '组件路径', dataIndex: 'component', search: false },
{ title: '排序', dataIndex: 'sort', search: false },
{ title: '标题', dataIndex: ['meta', 'title'] },
{ title: '图标', dataIndex: ['meta', 'icon'], search: false },
{
title: '隐藏',
dataIndex: 'hidden',
search: false,
render: (_, record) => (record.hidden ? '是' : '否'),
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Button type="link" icon={<PlusOutlined />} onClick={() => {
setCurrentRow({ parentId: record.ID });
setIsEdit(false);
setModalVisible(true);
}}></Button>
<Button type="link" icon={<EditOutlined />} onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}></Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
<ProTable<API.Menu>
headerTitle="菜单列表"
actionRef={actionRef}
rowKey="ID"
columns={columns}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => {
setCurrentRow({ parentId: 0 });
setIsEdit(false);
setModalVisible(true);
}}></Button>,
]}
request={async (params) => {
const res = await getMenuList({ page: params.current, pageSize: params.pageSize });
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
<ModalForm
title={isEdit ? '编辑菜单' : '新增菜单'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
onFinish={async (values) => {
const data = {
...values,
meta: { title: values.title, icon: values.icon, keepAlive: values.keepAlive },
};
let res;
if (isEdit) {
res = await updateBaseMenu({ ...currentRow, ...data });
} else {
res = await addBaseMenu(data);
}
if (res.code === 0) {
message.success(isEdit ? '编辑成功' : '创建成功');
setModalVisible(false);
actionRef.current?.reload();
return true;
}
return false;
}}
>
<ProFormDigit name="parentId" label="父菜单ID" />
<ProFormText name="path" label="路由路径" rules={[{ required: true }]} />
<ProFormText name="name" label="路由名称" rules={[{ required: true }]} />
<ProFormText name="component" label="组件路径" rules={[{ required: true }]} />
<ProFormText name="title" label="菜单标题" rules={[{ required: true }]} />
<ProFormText name="icon" label="菜单图标" />
<ProFormDigit name="sort" label="排序" initialValue={0} />
<ProFormSwitch name="hidden" label="是否隐藏" />
<ProFormSwitch name="keepAlive" label="缓存页面" />
</ModalForm>
</PageContainer>
);
};
export default MenuPage;

View File

@ -0,0 +1,198 @@
/**
*
* GVA sysOperationRecord.vue
*/
import { DeleteOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Space, Tag, Popover, Typography } from 'antd';
import { useRef, useState } from 'react';
import {
getSysOperationRecordList,
deleteSysOperationRecord,
deleteSysOperationRecordByIds,
} from '@/services/system/operationRecord';
import dayjs from 'dayjs';
const { Text } = Typography;
const OperationRecordPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
// 格式化JSON显示
const formatBody = (value: string) => {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch {
return value;
}
};
// 删除单条记录
const handleDelete = async (id: number) => {
const res = await deleteSysOperationRecord({ ID: id });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
}
};
// 批量删除
const handleBatchDelete = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请选择要删除的记录');
return;
}
const res = await deleteSysOperationRecordByIds({ ids: selectedRowKeys });
if (res.code === 0) {
message.success('删除成功');
setSelectedRowKeys([]);
actionRef.current?.reload();
}
};
const columns: ProColumns<API.OperationRecord>[] = [
{
title: '操作人',
dataIndex: ['user', 'userName'],
width: 140,
search: false,
render: (_, record) => (
<span>{record.user?.userName}({record.user?.nickName})</span>
),
},
{
title: '日期',
dataIndex: 'CreatedAt',
width: 180,
search: false,
render: (_, record) => dayjs(record.CreatedAt).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '状态码',
dataIndex: 'status',
width: 100,
render: (_, record) => <Tag color="success">{record.status}</Tag>,
},
{
title: '请求IP',
dataIndex: 'ip',
width: 120,
search: false,
},
{
title: '请求方法',
dataIndex: 'method',
width: 100,
},
{
title: '请求路径',
dataIndex: 'path',
width: 240,
ellipsis: true,
},
{
title: '请求',
dataIndex: 'body',
width: 80,
search: false,
render: (_, record) => (
record.body ? (
<Popover
content={
<div style={{ maxHeight: 400, maxWidth: 400, overflow: 'auto', background: '#112435', padding: 12, borderRadius: 4 }}>
<pre style={{ color: '#f08047', margin: 0, fontSize: 12 }}>{formatBody(record.body)}</pre>
</div>
}
trigger="click"
>
<Button type="link" size="small"></Button>
</Popover>
) : <Text type="secondary"></Text>
),
},
{
title: '响应',
dataIndex: 'resp',
width: 80,
search: false,
render: (_, record) => (
record.resp ? (
<Popover
content={
<div style={{ maxHeight: 400, maxWidth: 400, overflow: 'auto', background: '#112435', padding: 12, borderRadius: 4 }}>
<pre style={{ color: '#f08047', margin: 0, fontSize: 12 }}>{formatBody(record.resp)}</pre>
</div>
}
trigger="click"
>
<Button type="link" size="small"></Button>
</Popover>
) : <Text type="secondary"></Text>
),
},
{
title: '操作',
valueType: 'option',
width: 100,
render: (_, record) => (
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
),
},
];
return (
<PageContainer>
<ProTable<API.OperationRecord>
headerTitle="操作记录"
actionRef={actionRef}
rowKey="ID"
columns={columns}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as number[]),
}}
toolBarRender={() => [
<Popconfirm
key="batchDelete"
title="确定删除选中的记录?"
onConfirm={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
>
<Button
danger
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0}
>
</Button>
</Popconfirm>,
]}
request={async (params) => {
const res = await getSysOperationRecordList({
page: params.current,
pageSize: params.pageSize,
method: params.method,
path: params.path,
status: params.status ? Number(params.status) : undefined,
});
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
pagination={{
defaultPageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '30', '50', '100'],
}}
/>
</PageContainer>
);
};
export default OperationRecordPage;

View File

@ -0,0 +1,285 @@
/**
*
* GVA
*/
import { PlusOutlined, DeleteOutlined, EditOutlined, KeyOutlined } from '@ant-design/icons';
import { PageContainer, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { Button, message, Popconfirm, Switch, Avatar, Space, Modal, Input, Cascader } from 'antd';
import { useRef, useState, useEffect } from 'react';
import {
getUserList,
register,
deleteUser,
setUserInfo,
setUserAuthorities,
resetPassword,
} from '@/services/system/user';
import { getAuthorityList } from '@/services/system/authority';
const UserPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [resetPwdVisible, setResetPwdVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<API.UserInfo>();
const [isEdit, setIsEdit] = useState(false);
const [authOptions, setAuthOptions] = useState<any[]>([]);
const [newPassword, setNewPassword] = useState('');
// 获取角色选项
const fetchAuthorities = async () => {
const res = await getAuthorityList({ page: 1, pageSize: 100 });
if (res.code === 0 && res.data?.list) {
const options = buildAuthOptions(res.data.list);
setAuthOptions(options);
}
};
// 构建角色级联选项
const buildAuthOptions = (data: API.Authority[]): any[] => {
return data.map((item) => ({
value: item.authorityId,
label: item.authorityName,
children: item.children ? buildAuthOptions(item.children) : undefined,
}));
};
useEffect(() => {
fetchAuthorities();
}, []);
// 删除用户
const handleDelete = async (id: number) => {
const res = await deleteUser({ id });
if (res.code === 0) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error(res.msg || '删除失败');
}
};
// 切换启用状态
const handleSwitchEnable = async (record: API.UserInfo) => {
const res = await setUserInfo({
...record,
enable: record.enable === 1 ? 2 : 1,
});
if (res.code === 0) {
message.success(record.enable === 1 ? '禁用成功' : '启用成功');
actionRef.current?.reload();
}
};
// 修改角色
const handleChangeAuthority = async (record: API.UserInfo, authorityIds: number[]) => {
const res = await setUserAuthorities({
uuid: record.uuid!,
authorityIds,
});
if (res.code === 0) {
message.success('角色设置成功');
actionRef.current?.reload();
}
};
// 生成随机密码
const generatePassword = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let pwd = '';
for (let i = 0; i < 12; i++) {
pwd += chars.charAt(Math.floor(Math.random() * chars.length));
}
setNewPassword(pwd);
navigator.clipboard.writeText(pwd).then(() => {
message.success('密码已复制到剪贴板');
});
};
// 重置密码
const handleResetPassword = async () => {
if (!newPassword) {
message.warning('请输入或生成密码');
return;
}
const res = await resetPassword({ id: currentRow!.ID! });
if (res.code === 0) {
message.success('密码重置成功');
setResetPwdVisible(false);
setNewPassword('');
}
};
const columns: ProColumns<API.UserInfo>[] = [
{
title: '头像',
dataIndex: 'headerImg',
search: false,
render: (_, record) => <Avatar src={record.headerImg} />,
},
{ title: 'ID', dataIndex: 'ID', search: false },
{ title: '用户名', dataIndex: 'userName' },
{ title: '昵称', dataIndex: 'nickName' },
{ title: '手机号', dataIndex: 'phone' },
{ title: '邮箱', dataIndex: 'email' },
{
title: '用户角色',
dataIndex: 'authorityIds',
search: false,
render: (_, record) => (
<Cascader
options={authOptions}
value={record.authorities?.map((a) => a.authorityId)}
onChange={(value) => handleChangeAuthority(record, value as number[])}
multiple
maxTagCount="responsive"
style={{ width: 200 }}
/>
),
},
{
title: '启用',
dataIndex: 'enable',
search: false,
render: (_, record) => (
<Switch
checked={record.enable === 1}
onChange={() => handleSwitchEnable(record)}
/>
),
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<Space>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.ID!)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => {
setCurrentRow(record);
setIsEdit(true);
setModalVisible(true);
}}
>
</Button>
<Button
type="link"
icon={<KeyOutlined />}
onClick={() => {
setCurrentRow(record);
setResetPwdVisible(true);
}}
>
</Button>
</Space>
),
},
];
return (
<PageContainer>
<ProTable<API.UserInfo>
headerTitle="用户列表"
actionRef={actionRef}
rowKey="ID"
columns={columns}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setCurrentRow(undefined);
setIsEdit(false);
setModalVisible(true);
}}
>
</Button>,
]}
request={async (params) => {
const res = await getUserList({
page: params.current,
pageSize: params.pageSize,
...params,
});
return {
data: res.data?.list || [],
total: res.data?.total || 0,
success: res.code === 0,
};
}}
/>
<ModalForm
title={isEdit ? '编辑用户' : '新增用户'}
open={modalVisible}
onOpenChange={setModalVisible}
initialValues={currentRow}
onFinish={async (values) => {
let res;
if (isEdit) {
res = await setUserInfo({ ...currentRow, ...values });
} else {
res = await register(values);
}
if (res.code === 0) {
message.success(isEdit ? '编辑成功' : '创建成功');
setModalVisible(false);
actionRef.current?.reload();
return true;
}
message.error(res.msg || '操作失败');
return false;
}}
>
{!isEdit && (
<>
<ProFormText name="userName" label="用户名" rules={[{ required: true }, { min: 5 }]} />
<ProFormText.Password name="password" label="密码" rules={[{ required: true }, { min: 6 }]} />
</>
)}
<ProFormText name="nickName" label="昵称" rules={[{ required: true }]} />
<ProFormText name="phone" label="手机号" />
<ProFormText name="email" label="邮箱" />
<ProFormSelect
name="authorityIds"
label="用户角色"
mode="multiple"
options={authOptions}
rules={[{ required: true }]}
/>
</ModalForm>
<Modal
title="重置密码"
open={resetPwdVisible}
onCancel={() => {
setResetPwdVisible(false);
setNewPassword('');
}}
onOk={handleResetPassword}
>
<p>: {currentRow?.userName}</p>
<Space>
<Input.Password
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码"
/>
<Button onClick={generatePassword}></Button>
</Space>
</Modal>
</PageContainer>
);
};
export default UserPage;

View File

@ -1,386 +1,127 @@
import { Footer } from "@/components";
import { getFakeCaptcha } from "@/services/ant-design-pro/login";
import { createAdminService } from "@/services/index";
import { LoginRequest } from "@/services/kratos/admin/v1/index";
import {
AlipayCircleOutlined,
LockOutlined,
MobileOutlined,
TaobaoCircleOutlined,
UserOutlined,
WeiboCircleOutlined,
} from "@ant-design/icons";
import {
LoginForm,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from "@ant-design/pro-components";
import {
FormattedMessage,
Helmet,
SelectLang,
useIntl,
useModel,
} from "@umijs/max";
import { Alert, App, Tabs } from "antd";
import { createStyles } from "antd-style";
import React, { useState } from "react";
import { flushSync } from "react-dom";
import Settings from "../../../../config/defaultSettings";
const adminService = createAdminService();
const useStyles = createStyles(({ token }) => {
return {
action: {
marginLeft: "8px",
color: "rgba(0, 0, 0, 0.2)",
fontSize: "24px",
verticalAlign: "middle",
cursor: "pointer",
transition: "color 0.3s",
"&:hover": {
color: token.colorPrimaryActive,
},
},
lang: {
width: 42,
height: 42,
lineHeight: "42px",
position: "fixed",
right: 16,
borderRadius: token.borderRadius,
":hover": {
backgroundColor: token.colorBgTextHover,
},
},
container: {
display: "flex",
flexDirection: "column",
height: "100vh",
overflow: "auto",
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: "100% 100%",
},
};
});
const ActionIcons = () => {
const { styles } = useStyles();
return (
<>
<AlipayCircleOutlined
key="AlipayCircleOutlined"
className={styles.action}
/>
<TaobaoCircleOutlined
key="TaobaoCircleOutlined"
className={styles.action}
/>
<WeiboCircleOutlined
key="WeiboCircleOutlined"
className={styles.action}
/>
</>
);
};
const Lang = () => {
const { styles } = useStyles();
return (
<div className={styles.lang} data-lang>
{SelectLang && <SelectLang />}
</div>
);
};
const LoginMessage: React.FC<{
content: string;
}> = ({ content }) => {
return (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
};
/**
*
* GVA
*/
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 { useState, useEffect } from 'react';
import { login, captcha } from '@/services/system/user';
const Login: React.FC = () => {
const [userLoginState, setUserLoginState] = useState<API.LoginResult>({});
const [type, setType] = useState<string>("account");
const { initialState, setInitialState } = useModel("@@initialState");
const { styles } = useStyles();
const { message } = App.useApp();
const intl = useIntl();
const { setInitialState } = useModel('@@initialState');
const [captchaInfo, setCaptchaInfo] = useState<{
captchaId: string;
picPath: string;
openCaptcha: boolean;
}>({ captchaId: '', picPath: '', openCaptcha: false });
const [loading, setLoading] = useState(false);
const handleSubmit = async (req: LoginRequest) => {
// 获取验证码
const getCaptcha = async () => {
try {
const userInfo = await adminService.Login(req);
const defaultLoginSuccessMessage = intl.formatMessage({
id: "pages.login.success",
defaultMessage: "登录成功!",
});
message.success(defaultLoginSuccessMessage);
// set user state
flushSync(() => {
setInitialState((state) => ({
...state,
currentUser: userInfo,
}));
});
const urlParams = new URL(window.location.href).searchParams;
window.location.href = urlParams.get("redirect") || "/";
console.log(userInfo);
const res = await captcha();
if (res.code === 0 && res.data) {
setCaptchaInfo({
captchaId: res.data.captchaId,
picPath: res.data.picPath,
openCaptcha: res.data.openCaptcha,
});
}
} catch (error) {
const defaultLoginFailureMessage = intl.formatMessage({
id: "pages.login.failure",
defaultMessage: "登录失败,请重试!",
});
console.log(error);
message.error(defaultLoginFailureMessage);
console.error('获取验证码失败:', error);
}
};
const { status, type: loginType } = userLoginState;
useEffect(() => {
getCaptcha();
}, []);
// 提交登录
const handleSubmit = async (values: API.LoginParams) => {
setLoading(true);
try {
const res = await login({
...values,
captchaId: captchaInfo.captchaId,
});
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 {
message.error(res.msg || '登录失败');
getCaptcha();
}
} catch (error) {
message.error('登录失败,请重试');
getCaptcha();
}
setLoading(false);
};
return (
<div className={styles.container}>
<Helmet>
<title>
{intl.formatMessage({
id: "menu.login",
defaultMessage: "登录页",
})}
{Settings.title && ` - ${Settings.title}`}
</title>
</Helmet>
<Lang />
<div
style={{
flex: "1",
padding: "32px 0",
}}
>
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}>
<Spin spinning={loading}>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: "75vw",
}}
logo={<img alt="logo" src="/logo.svg" />}
title="Ant Design"
subTitle={intl.formatMessage({
id: "pages.layouts.userLayout.title",
})}
initialValues={{
autoLogin: true,
}}
actions={[
<FormattedMessage
key="loginWith"
id="pages.login.loginWith"
defaultMessage="其他登录方式"
/>,
<ActionIcons key="icons" />,
]}
onFinish={async (values) => {
await handleSubmit(values as LoginRequest);
}}
title="Kratos Admin"
subTitle="基于 Kratos 的后台管理系统"
onFinish={handleSubmit}
initialValues={{ username: 'admin' }}
>
<Tabs
activeKey={type}
onChange={setType}
centered
items={[
{
key: "account",
label: intl.formatMessage({
id: "pages.login.accountLogin.tab",
defaultMessage: "账户密码登录",
}),
},
{
key: "mobile",
label: intl.formatMessage({
id: "pages.login.phoneLogin.tab",
defaultMessage: "手机号登录",
}),
},
<ProFormText
name="username"
fieldProps={{ size: 'large', prefix: <UserOutlined /> }}
placeholder="请输入用户名"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 5, message: '用户名至少5个字符' },
]}
/>
{status === "error" && loginType === "account" && (
<LoginMessage
content={intl.formatMessage({
id: "pages.login.accountLogin.errorMessage",
defaultMessage: "账户或密码错误(admin/ant.design)",
})}
/>
)}
{type === "account" && (
<>
<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="username"
fieldProps={{
size: "large",
prefix: <UserOutlined />,
}}
placeholder={intl.formatMessage({
id: "pages.login.username.placeholder",
defaultMessage: "用户名: admin or user",
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.username.required"
defaultMessage="请输入用户名!"
/>
),
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: "large",
prefix: <LockOutlined />,
}}
placeholder={intl.formatMessage({
id: "pages.login.password.placeholder",
defaultMessage: "密码: ant.design",
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.password.required"
defaultMessage="请输入密码!"
/>
),
},
]}
/>
</>
)}
{status === "error" && loginType === "mobile" && (
<LoginMessage content="验证码错误" />
)}
{type === "mobile" && (
<>
<ProFormText
fieldProps={{
size: "large",
prefix: <MobileOutlined />,
}}
name="mobile"
placeholder={intl.formatMessage({
id: "pages.login.phoneNumber.placeholder",
defaultMessage: "手机号",
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.phoneNumber.required"
defaultMessage="请输入手机号!"
/>
),
},
{
pattern: /^1\d{10}$/,
message: (
<FormattedMessage
id="pages.login.phoneNumber.invalid"
defaultMessage="手机号格式错误!"
/>
),
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: "large",
prefix: <LockOutlined />,
}}
captchaProps={{
size: "large",
}}
placeholder={intl.formatMessage({
id: "pages.login.captcha.placeholder",
defaultMessage: "请输入验证码",
})}
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${intl.formatMessage({
id: "pages.getCaptchaSecondText",
defaultMessage: "获取验证码",
})}`;
}
return intl.formatMessage({
id: "pages.login.phoneLogin.getVerificationCode",
defaultMessage: "获取验证码",
});
}}
name="captcha"
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.captcha.required"
defaultMessage="请输入验证码!"
/>
),
},
]}
onGetCaptcha={async (phone) => {
const result = await getFakeCaptcha({
phone,
});
if (!result) {
return;
}
message.success("获取验证码成功验证码为1234");
}}
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>
)}
<div
style={{
marginBottom: 24,
}}
>
<ProFormCheckbox noStyle name="autoLogin">
<FormattedMessage
id="pages.login.rememberMe"
defaultMessage="自动登录"
/>
</ProFormCheckbox>
<a
style={{
float: "right",
}}
>
<FormattedMessage
id="pages.login.forgotPassword"
defaultMessage="忘记密码"
/>
</a>
</div>
</LoginForm>
</div>
<Footer />
</Spin>
</div>
);
};

View File

@ -0,0 +1,106 @@
/**
* API管理
* GVA
*/
import { request } from '@umijs/max';
/** 分页获取API列表 POST /api/getApiList */
export async function getApiList(data: API.PageParams & { path?: string; description?: string; apiGroup?: string; method?: string }) {
return request<API.ApiListResult>('/api/api/getApiList', {
method: 'POST',
data,
});
}
/** 创建API POST /api/createApi */
export async function createApi(data: API.ApiItem) {
return request<API.BaseResult>('/api/api/createApi', {
method: 'POST',
data,
});
}
/** 根据ID获取API POST /api/getApiById */
export async function getApiById(data: { id: number }) {
return request<API.ApiResult>('/api/api/getApiById', {
method: 'POST',
data,
});
}
/** 更新API POST /api/updateApi */
export async function updateApi(data: API.ApiItem) {
return request<API.BaseResult>('/api/api/updateApi', {
method: 'POST',
data,
});
}
/** 设置角色API权限 POST /api/setAuthApi */
export async function setAuthApi(data: { authorityId: number; apis: API.ApiItem[] }) {
return request<API.BaseResult>('/api/api/setAuthApi', {
method: 'POST',
data,
});
}
/** 获取所有API POST /api/getAllApis */
export async function getAllApis(data?: { authorityId?: number }) {
return request<API.AllApisResult>('/api/api/getAllApis', {
method: 'POST',
data,
});
}
/** 删除API POST /api/deleteApi */
export async function deleteApi(data: { ids: number[] }) {
return request<API.BaseResult>('/api/api/deleteApi', {
method: 'POST',
data,
});
}
/** 批量删除API DELETE /api/deleteApisByIds */
export async function deleteApisByIds(data: { ids: number[] }) {
return request<API.BaseResult>('/api/api/deleteApisByIds', {
method: 'DELETE',
data,
});
}
/** 刷新Casbin缓存 GET /api/freshCasbin */
export async function freshCasbin() {
return request<API.BaseResult>('/api/api/freshCasbin', {
method: 'GET',
});
}
/** 同步API GET /api/syncApi */
export async function syncApi() {
return request<API.BaseResult>('/api/api/syncApi', {
method: 'GET',
});
}
/** 获取API分组 GET /api/getApiGroups */
export async function getApiGroups() {
return request<API.ApiGroupsResult>('/api/api/getApiGroups', {
method: 'GET',
});
}
/** 忽略API POST /api/ignoreApi */
export async function ignoreApi(data: { path: string; method: string }) {
return request<API.BaseResult>('/api/api/ignoreApi', {
method: 'POST',
data,
});
}
/** 确认同步API POST /api/enterSyncApi */
export async function enterSyncApi(data: { newApis: API.ApiItem[]; deleteApis: API.ApiItem[] }) {
return request<API.BaseResult>('/api/api/enterSyncApi', {
method: 'POST',
data,
});
}

View File

@ -0,0 +1,53 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 获取角色列表 POST /authority/getAuthorityList */
export async function getAuthorityList(data: API.PageParams) {
return request<API.AuthorityListResult>('/api/authority/getAuthorityList', {
method: 'POST',
data,
});
}
/** 删除角色 POST /authority/deleteAuthority */
export async function deleteAuthority(data: { authorityId: number }) {
return request<API.BaseResult>('/api/authority/deleteAuthority', {
method: 'POST',
data,
});
}
/** 创建角色 POST /authority/createAuthority */
export async function createAuthority(data: API.Authority) {
return request<API.AuthorityResult>('/api/authority/createAuthority', {
method: 'POST',
data,
});
}
/** 拷贝角色 POST /authority/copyAuthority */
export async function copyAuthority(data: { authority: API.Authority; oldAuthorityId: number }) {
return request<API.AuthorityResult>('/api/authority/copyAuthority', {
method: 'POST',
data,
});
}
/** 设置角色资源权限 POST /authority/setDataAuthority */
export async function setDataAuthority(data: { authorityId: number; dataAuthorityId: number[] }) {
return request<API.BaseResult>('/api/authority/setDataAuthority', {
method: 'POST',
data,
});
}
/** 修改角色 PUT /authority/updateAuthority */
export async function updateAuthority(data: API.Authority) {
return request<API.BaseResult>('/api/authority/updateAuthority', {
method: 'PUT',
data,
});
}

View File

@ -0,0 +1,21 @@
/**
* Casbin权限管理 API
* GVA
*/
import { request } from '@umijs/max';
/** 更新角色API权限 POST /casbin/updateCasbin */
export async function updateCasbin(data: { authorityId: number; casbinInfos: API.CasbinInfo[] }) {
return request<API.BaseResult>('/api/casbin/updateCasbin', {
method: 'POST',
data,
});
}
/** 获取权限列表 POST /casbin/getPolicyPathByAuthorityId */
export async function getPolicyPathByAuthorityId(data: { authorityId: number }) {
return request<API.CasbinPolicyResult>('/api/casbin/getPolicyPathByAuthorityId', {
method: 'POST',
data,
});
}

View File

@ -0,0 +1,61 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 创建字典 POST /sysDictionary/createSysDictionary */
export async function createSysDictionary(data: API.Dictionary) {
return request<API.BaseResult>('/api/sysDictionary/createSysDictionary', {
method: 'POST',
data,
});
}
/** 删除字典 DELETE /sysDictionary/deleteSysDictionary */
export async function deleteSysDictionary(data: { ID: number }) {
return request<API.BaseResult>('/api/sysDictionary/deleteSysDictionary', {
method: 'DELETE',
data,
});
}
/** 更新字典 PUT /sysDictionary/updateSysDictionary */
export async function updateSysDictionary(data: API.Dictionary) {
return request<API.BaseResult>('/api/sysDictionary/updateSysDictionary', {
method: 'PUT',
data,
});
}
/** 根据ID查询字典 GET /sysDictionary/findSysDictionary */
export async function findSysDictionary(params: { ID: number }) {
return request<API.DictionaryResult>('/api/sysDictionary/findSysDictionary', {
method: 'GET',
params,
});
}
/** 分页获取字典列表 GET /sysDictionary/getSysDictionaryList */
export async function getSysDictionaryList(params: API.PageParams & { name?: string; type?: string; status?: boolean }) {
return request<API.DictionaryListResult>('/api/sysDictionary/getSysDictionaryList', {
method: 'GET',
params,
});
}
/** 导出字典JSON GET /sysDictionary/exportSysDictionary */
export async function exportSysDictionary(params: { ID: number }) {
return request<API.DictionaryExportResult>('/api/sysDictionary/exportSysDictionary', {
method: 'GET',
params,
});
}
/** 导入字典JSON POST /sysDictionary/importSysDictionary */
export async function importSysDictionary(data: API.Dictionary) {
return request<API.BaseResult>('/api/sysDictionary/importSysDictionary', {
method: 'POST',
data,
});
}

View File

@ -0,0 +1,77 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 创建字典详情 POST /sysDictionaryDetail/createSysDictionaryDetail */
export async function createSysDictionaryDetail(data: API.DictionaryDetail) {
return request<API.BaseResult>('/api/sysDictionaryDetail/createSysDictionaryDetail', {
method: 'POST',
data,
});
}
/** 删除字典详情 DELETE /sysDictionaryDetail/deleteSysDictionaryDetail */
export async function deleteSysDictionaryDetail(data: { ID: number }) {
return request<API.BaseResult>('/api/sysDictionaryDetail/deleteSysDictionaryDetail', {
method: 'DELETE',
data,
});
}
/** 更新字典详情 PUT /sysDictionaryDetail/updateSysDictionaryDetail */
export async function updateSysDictionaryDetail(data: API.DictionaryDetail) {
return request<API.BaseResult>('/api/sysDictionaryDetail/updateSysDictionaryDetail', {
method: 'PUT',
data,
});
}
/** 根据ID查询字典详情 GET /sysDictionaryDetail/findSysDictionaryDetail */
export async function findSysDictionaryDetail(params: { ID: number }) {
return request<API.DictionaryDetailResult>('/api/sysDictionaryDetail/findSysDictionaryDetail', {
method: 'GET',
params,
});
}
/** 分页获取字典详情列表 GET /sysDictionaryDetail/getSysDictionaryDetailList */
export async function getSysDictionaryDetailList(params: API.PageParams & { sysDictionaryID?: number }) {
return request<API.DictionaryDetailListResult>('/api/sysDictionaryDetail/getSysDictionaryDetailList', {
method: 'GET',
params,
});
}
/** 获取层级字典详情树形结构根据字典ID GET /sysDictionaryDetail/getDictionaryTreeList */
export async function getDictionaryTreeList(params: { sysDictionaryID: number }) {
return request<API.DictionaryTreeResult>('/api/sysDictionaryDetail/getDictionaryTreeList', {
method: 'GET',
params,
});
}
/** 获取层级字典详情树形结构(根据字典类型) GET /sysDictionaryDetail/getDictionaryTreeListByType */
export async function getDictionaryTreeListByType(params: { dictType: string }) {
return request<API.DictionaryTreeResult>('/api/sysDictionaryDetail/getDictionaryTreeListByType', {
method: 'GET',
params,
});
}
/** 根据父级ID获取字典详情 GET /sysDictionaryDetail/getDictionaryDetailsByParent */
export async function getDictionaryDetailsByParent(params: { parentID: number; includeChildren?: boolean }) {
return request<API.DictionaryDetailListResult>('/api/sysDictionaryDetail/getDictionaryDetailsByParent', {
method: 'GET',
params,
});
}
/** 获取字典详情的完整路径 GET /sysDictionaryDetail/getDictionaryPath */
export async function getDictionaryPath(params: { ID: number }) {
return request<API.DictionaryPathResult>('/api/sysDictionaryDetail/getDictionaryPath', {
method: 'GET',
params,
});
}

View File

@ -0,0 +1,11 @@
/**
* API
*/
export * from './user';
export * from './authority';
export * from './menu';
export * from './api';
export * from './dictionary';
export * from './dictionaryDetail';
export * from './operationRecord';
export * from './casbin';

View File

@ -0,0 +1,75 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 获取动态路由 POST /menu/getMenu */
export async function asyncMenu() {
return request<API.MenuResult>('/api/menu/getMenu', {
method: 'POST',
});
}
/** 获取菜单列表 POST /menu/getMenuList */
export async function getMenuList(data: API.PageParams) {
return request<API.MenuListResult>('/api/menu/getMenuList', {
method: 'POST',
data,
});
}
/** 新增菜单 POST /menu/addBaseMenu */
export async function addBaseMenu(data: API.Menu) {
return request<API.BaseResult>('/api/menu/addBaseMenu', {
method: 'POST',
data,
});
}
/** 获取基础路由列表 POST /menu/getBaseMenuTree */
export async function getBaseMenuTree() {
return request<API.MenuTreeResult>('/api/menu/getBaseMenuTree', {
method: 'POST',
});
}
/** 添加角色菜单关联 POST /menu/addMenuAuthority */
export async function addMenuAuthority(data: { menus: API.Menu[]; authorityId: number }) {
return request<API.BaseResult>('/api/menu/addMenuAuthority', {
method: 'POST',
data,
});
}
/** 获取角色菜单关联 POST /menu/getMenuAuthority */
export async function getMenuAuthority(data: { authorityId: number }) {
return request<API.MenuAuthorityResult>('/api/menu/getMenuAuthority', {
method: 'POST',
data,
});
}
/** 删除菜单 POST /menu/deleteBaseMenu */
export async function deleteBaseMenu(data: { ID: number }) {
return request<API.BaseResult>('/api/menu/deleteBaseMenu', {
method: 'POST',
data,
});
}
/** 修改菜单 POST /menu/updateBaseMenu */
export async function updateBaseMenu(data: API.Menu) {
return request<API.BaseResult>('/api/menu/updateBaseMenu', {
method: 'POST',
data,
});
}
/** 根据ID获取菜单 POST /menu/getBaseMenuById */
export async function getBaseMenuById(data: { id: number }) {
return request<API.MenuResult>('/api/menu/getBaseMenuById', {
method: 'POST',
data,
});
}

View File

@ -0,0 +1,34 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 删除操作记录 DELETE /sysOperationRecord/deleteSysOperationRecord */
export async function deleteSysOperationRecord(data: { ID: number }) {
return request<API.BaseResult>('/api/sysOperationRecord/deleteSysOperationRecord', {
method: 'DELETE',
data,
});
}
/** 批量删除操作记录 DELETE /sysOperationRecord/deleteSysOperationRecordByIds */
export async function deleteSysOperationRecordByIds(data: { ids: number[] }) {
return request<API.BaseResult>('/api/sysOperationRecord/deleteSysOperationRecordByIds', {
method: 'DELETE',
data,
});
}
/** 分页获取操作记录列表 GET /sysOperationRecord/getSysOperationRecordList */
export async function getSysOperationRecordList(params: API.PageParams & {
path?: string;
method?: string;
status?: number;
ip?: string;
}) {
return request<API.OperationRecordListResult>('/api/sysOperationRecord/getSysOperationRecordList', {
method: 'GET',
params,
});
}

400
web/src/services/system/typings.d.ts vendored Normal file
View File

@ -0,0 +1,400 @@
/**
* API
* GVA
*/
declare namespace API {
// 基础响应
type BaseResult = {
code: number;
msg: string;
data?: any;
};
// 分页参数
type PageParams = {
page?: number;
pageSize?: number;
};
// 登录参数
type LoginParams = {
username: string;
password: string;
captcha?: string;
captchaId?: string;
};
// 登录结果
type LoginResult = {
code: number;
msg: string;
data?: {
user: UserInfo;
token: string;
expiresAt: number;
};
};
// 验证码结果
type CaptchaResult = {
code: number;
msg: string;
data?: {
captchaId: string;
picPath: string;
captchaLength: number;
openCaptcha: boolean;
};
};
// 注册参数
type RegisterParams = {
username: string;
password: string;
nickName?: string;
headerImg?: string;
authorityId?: number;
authorityIds?: number[];
};
// 修改密码参数
type ChangePasswordParams = {
password: string;
newPassword: string;
};
// 用户信息
type UserInfo = {
ID?: number;
uuid?: string;
userName?: string;
nickName?: string;
sideMode?: string;
headerImg?: string;
baseColor?: string;
activeColor?: string;
authorityId?: number;
authority?: Authority;
authorities?: Authority[];
phone?: string;
email?: string;
enable?: number;
};
// 用户信息结果
type UserInfoResult = {
code: number;
msg: string;
data?: {
userInfo: UserInfo;
};
};
// 用户列表结果
type UserListResult = {
code: number;
msg: string;
data?: {
list: UserInfo[];
total: number;
page: number;
pageSize: number;
};
};
// 角色
type Authority = {
authorityId?: number;
authorityName?: string;
parentId?: number;
dataAuthorityId?: Authority[];
children?: Authority[];
menus?: Menu[];
defaultRouter?: string;
};
// 角色结果
type AuthorityResult = {
code: number;
msg: string;
data?: {
authority: Authority;
};
};
// 角色列表结果
type AuthorityListResult = {
code: number;
msg: string;
data?: {
list: Authority[];
total: number;
page: number;
pageSize: number;
};
};
// 菜单
type Menu = {
ID?: number;
parentId?: number;
path?: string;
name?: string;
hidden?: boolean;
component?: string;
sort?: number;
meta?: MenuMeta;
children?: Menu[];
parameters?: MenuParameter[];
menuBtn?: MenuButton[];
};
// 菜单元信息
type MenuMeta = {
activeName?: string;
keepAlive?: boolean;
defaultMenu?: boolean;
title?: string;
icon?: string;
closeTab?: boolean;
};
// 菜单参数
type MenuParameter = {
ID?: number;
type?: string;
key?: string;
value?: string;
};
// 菜单按钮
type MenuButton = {
ID?: number;
name?: string;
desc?: string;
};
// 菜单结果
type MenuResult = {
code: number;
msg: string;
data?: {
menu?: Menu;
menus?: Menu[];
};
};
// 菜单列表结果
type MenuListResult = {
code: number;
msg: string;
data?: {
list: Menu[];
total: number;
page: number;
pageSize: number;
};
};
// 菜单树结果
type MenuTreeResult = {
code: number;
msg: string;
data?: {
menus: Menu[];
};
};
// 菜单权限结果
type MenuAuthorityResult = {
code: number;
msg: string;
data?: {
menus: Menu[];
};
};
// API项
type ApiItem = {
ID?: number;
path?: string;
description?: string;
apiGroup?: string;
method?: string;
};
// API结果
type ApiResult = {
code: number;
msg: string;
data?: {
api: ApiItem;
};
};
// API列表结果
type ApiListResult = {
code: number;
msg: string;
data?: {
list: ApiItem[];
total: number;
page: number;
pageSize: number;
};
};
// 所有API结果
type AllApisResult = {
code: number;
msg: string;
data?: {
apis: ApiItem[];
};
};
// API分组结果
type ApiGroupsResult = {
code: number;
msg: string;
data?: {
groups: string[];
};
};
// 字典
type Dictionary = {
ID?: number;
name?: string;
type?: string;
status?: boolean;
desc?: string;
sysDictionaryDetails?: DictionaryDetail[];
};
// 字典结果
type DictionaryResult = {
code: number;
msg: string;
data?: {
sysDictionary: Dictionary;
};
};
// 字典列表结果
type DictionaryListResult = {
code: number;
msg: string;
data?: {
list: Dictionary[];
total: number;
page: number;
pageSize: number;
};
};
// 字典导出结果
type DictionaryExportResult = {
code: number;
msg: string;
data?: Dictionary;
};
// 字典详情
type DictionaryDetail = {
ID?: number;
sysDictionaryID?: number;
parentID?: number;
label?: string;
value?: string;
extend?: string;
status?: boolean;
sort?: number;
children?: DictionaryDetail[];
};
// 字典详情结果
type DictionaryDetailResult = {
code: number;
msg: string;
data?: {
sysDictionaryDetail: DictionaryDetail;
};
};
// 字典详情列表结果
type DictionaryDetailListResult = {
code: number;
msg: string;
data?: {
list: DictionaryDetail[];
total: number;
page: number;
pageSize: number;
};
};
// 字典树结果
type DictionaryTreeResult = {
code: number;
msg: string;
data?: {
list: DictionaryDetail[];
};
};
// 字典路径结果
type DictionaryPathResult = {
code: number;
msg: string;
data?: {
path: DictionaryDetail[];
};
};
// 操作记录
type OperationRecord = {
ID?: number;
ip?: string;
method?: string;
path?: string;
status?: number;
latency?: string;
agent?: string;
error_message?: string;
body?: string;
resp?: string;
user_id?: number;
user?: UserInfo;
CreatedAt?: string;
};
// 操作记录列表结果
type OperationRecordListResult = {
code: number;
msg: string;
data?: {
list: OperationRecord[];
total: number;
page: number;
pageSize: number;
};
};
// Casbin信息
type CasbinInfo = {
path: string;
method: string;
};
// Casbin策略结果
type CasbinPolicyResult = {
code: number;
msg: string;
data?: {
paths: CasbinInfo[];
};
};
}

View File

@ -0,0 +1,108 @@
/**
* API
* GVA
*/
import { request } from '@umijs/max';
/** 用户登录 POST /base/login */
export async function login(data: API.LoginParams) {
return request<API.LoginResult>('/api/base/login', {
method: 'POST',
data,
});
}
/** 获取验证码 POST /base/captcha */
export async function captcha() {
return request<API.CaptchaResult>('/api/base/captcha', {
method: 'POST',
});
}
/** 用户注册 POST /user/admin_register */
export async function register(data: API.RegisterParams) {
return request<API.BaseResult>('/api/user/admin_register', {
method: 'POST',
data,
});
}
/** 修改密码 POST /user/changePassword */
export async function changePassword(data: API.ChangePasswordParams) {
return request<API.BaseResult>('/api/user/changePassword', {
method: 'POST',
data,
});
}
/** 分页获取用户列表 POST /user/getUserList */
export async function getUserList(data: API.PageParams) {
return request<API.UserListResult>('/api/user/getUserList', {
method: 'POST',
data,
});
}
/** 设置用户权限 POST /user/setUserAuthority */
export async function setUserAuthority(data: { uuid: string; authorityId: number }) {
return request<API.BaseResult>('/api/user/setUserAuthority', {
method: 'POST',
data,
});
}
/** 删除用户 DELETE /user/deleteUser */
export async function deleteUser(data: { id: number }) {
return request<API.BaseResult>('/api/user/deleteUser', {
method: 'DELETE',
data,
});
}
/** 设置用户信息 PUT /user/setUserInfo */
export async function setUserInfo(data: API.UserInfo) {
return request<API.BaseResult>('/api/user/setUserInfo', {
method: 'PUT',
data,
});
}
/** 设置自身信息 PUT /user/setSelfInfo */
export async function setSelfInfo(data: API.UserInfo) {
return request<API.BaseResult>('/api/user/setSelfInfo', {
method: 'PUT',
data,
});
}
/** 设置自身界面配置 PUT /user/setSelfSetting */
export async function setSelfSetting(data: Record<string, any>) {
return request<API.BaseResult>('/api/user/setSelfSetting', {
method: 'PUT',
data,
});
}
/** 设置用户多角色权限 POST /user/setUserAuthorities */
export async function setUserAuthorities(data: { uuid: string; authorityIds: number[] }) {
return request<API.BaseResult>('/api/user/setUserAuthorities', {
method: 'POST',
data,
});
}
/** 获取用户信息 GET /user/getUserInfo */
export async function getUserInfo() {
return request<API.UserInfoResult>('/api/user/getUserInfo', {
method: 'GET',
});
}
/** 重置密码 POST /user/resetPassword */
export async function resetPassword(data: { id: number }) {
return request<API.BaseResult>('/api/user/resetPassword', {
method: 'POST',
data,
});
}