前端重构

This commit is contained in:
Yvan
2026-01-08 11:20:29 +08:00
parent d76655a30a
commit f12f2e0f00
201 changed files with 27517 additions and 537 deletions
@@ -0,0 +1,78 @@
/**
* KRA - Auto Code Generator Page Styles
*/
.warningBar {
margin-bottom: 16px;
}
.sectionCard {
margin-bottom: 16px;
}
.aiSection {
position: relative;
}
.aiButtons {
position: absolute;
right: 8px;
bottom: 8px;
display: flex;
gap: 8px;
}
.toolbar {
margin-bottom: 16px;
}
.actionBar {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.expertSection {
padding: 16px;
}
.settingGroup {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
h4 {
margin-bottom: 16px;
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
}
.codePreview {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 4px;
overflow: auto;
max-height: calc(100vh - 200px);
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.5;
}
// Dark mode support
:global(.ant-layout-content[data-theme='dark']) {
.settingGroup {
border-color: #303030;
h4 {
color: rgba(255, 255, 255, 0.85);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
/**
* KRA - Auto Code Admin Page Styles
*/
.toolbar {
margin-bottom: 16px;
}
.aiWarning {
margin-left: 8px;
color: #ff4d4f;
font-size: 12px;
}
@@ -0,0 +1,514 @@
/**
* KRA - Auto Code Admin Page
* 自动化代码管理页面 - 查看历史记录、回滚、复用
*/
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Tag,
Modal,
Checkbox,
Drawer,
Form,
Input,
Select,
message,
Popconfirm,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
PlusOutlined,
DeleteOutlined,
RollbackOutlined,
CopyOutlined,
AppstoreAddOutlined,
RobotOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useNavigate } from '@umijs/max';
import {
getSysHistory,
rollback,
delSysHistory,
getMeta,
addFunc,
} from '@/services/kratos/autoCode';
import { formatDate } from '@/utils/date';
import styles from './index.less';
interface AutoCodeHistory {
ID: number;
CreatedAt: string;
structName: string;
description: string;
tableName: string;
flag: number;
package?: string;
packageName?: string;
abbreviation?: string;
}
interface RollbackForm {
deleteApi: boolean;
deleteMenu: boolean;
deleteTable: boolean;
}
interface AddFuncForm {
package: string;
structName: string;
packageName: string;
humpPackageName: string;
description: string;
abbreviation: string;
isAi: boolean;
prompt: string;
funcDesc: string;
funcName: string;
method: string;
isAuth: boolean;
router: string;
apiFunc?: string;
serverFunc?: string;
jsFunc?: string;
}
const AutoCodeAdminPage: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<AutoCodeHistory[]>([]);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
// Rollback dialog
const [rollbackVisible, setRollbackVisible] = useState(false);
const [rollbackRow, setRollbackRow] = useState<AutoCodeHistory | null>(null);
const [rollbackForm, setRollbackForm] = useState<RollbackForm>({
deleteApi: false,
deleteMenu: false,
deleteTable: false,
});
// Add function drawer
const [funcVisible, setFuncVisible] = useState(false);
const [funcLoading, setFuncLoading] = useState(false);
const [funcForm, setFuncForm] = useState<AddFuncForm>({
package: '',
structName: '',
packageName: '',
humpPackageName: '',
description: '',
abbreviation: '',
isAi: false,
prompt: '',
funcDesc: '',
funcName: '',
method: 'GET',
isAuth: true,
router: '',
});
// Fetch table data
const fetchData = async () => {
setLoading(true);
try {
const res = await getSysHistory({ page: current, pageSize });
if (res.code === 0) {
setDataSource(res.data?.list || []);
setTotal(res.data?.total || 0);
}
} catch (error) {
console.error('获取历史记录失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [current, pageSize]);
// Navigate to auto code page
const handleGoAutoCode = (row: AutoCodeHistory | null, mode?: number) => {
if (row) {
// Reuse or add field mode
navigate('/systemTools/autoCode', {
state: { id: row.ID, mode },
});
} else {
// New
navigate('/systemTools/autoCode');
}
};
// Open rollback dialog
const handleOpenRollback = (row: AutoCodeHistory) => {
setRollbackRow(row);
setRollbackForm({
deleteApi: false,
deleteMenu: false,
deleteTable: false,
});
setRollbackVisible(true);
};
// Execute rollback
const handleRollback = async () => {
if (!rollbackRow) return;
try {
const res = await rollback({
id: rollbackRow.ID,
...rollbackForm,
});
if (res.code === 0) {
message.success('回滚成功');
setRollbackVisible(false);
fetchData();
} else {
message.error(res.msg || '回滚失败');
}
} catch (error) {
message.error('回滚失败');
}
};
// Delete row
const handleDelete = async (row: AutoCodeHistory) => {
try {
const res = await delSysHistory({ id: row.ID });
if (res.code === 0) {
message.success('删除成功');
fetchData();
} else {
message.error(res.msg || '删除失败');
}
} catch (error) {
message.error('删除失败');
}
};
// Open add function drawer
const handleOpenAddFunc = async (row: AutoCodeHistory) => {
try {
const res = await getMeta({ id: row.ID });
if (res.code === 0) {
const meta = res.data;
setFuncForm({
package: meta.package || '',
structName: meta.structName || '',
packageName: meta.packageName || '',
humpPackageName: meta.humpPackageName || '',
description: meta.description || '',
abbreviation: meta.abbreviation || '',
isAi: false,
prompt: '',
funcDesc: '',
funcName: '',
method: 'GET',
isAuth: true,
router: '',
});
setFuncVisible(true);
}
} catch (error) {
message.error('获取元数据失败');
}
};
// Execute add function
const handleAddFunc = async () => {
if (!funcForm.funcName || !funcForm.router) {
message.warning('请填写方法名和路由');
return;
}
setFuncLoading(true);
try {
const res = await addFunc(funcForm);
if (res.code === 0) {
message.success('添加成功');
setFuncVisible(false);
} else {
message.error(res.msg || '添加失败');
}
} catch (error) {
message.error('添加失败');
} finally {
setFuncLoading(false);
}
};
const columns: ColumnsType<AutoCodeHistory> = [
{ title: 'ID', dataIndex: 'ID', width: 60 },
{
title: '日期',
dataIndex: 'CreatedAt',
width: 180,
render: (text) => formatDate(text),
},
{ title: '结构体名', dataIndex: 'structName', width: 150 },
{ title: '结构体描述', dataIndex: 'description', width: 150 },
{ title: '表名称', dataIndex: 'tableName', width: 150 },
{
title: '回滚标记',
dataIndex: 'flag',
width: 100,
render: (flag) =>
flag ? (
<Tag color="error">已回滚</Tag>
) : (
<Tag color="success">未回滚</Tag>
),
},
{
title: '操作',
key: 'action',
width: 320,
render: (_, record) => (
<Space wrap>
<Button
type="link"
size="small"
icon={<AppstoreAddOutlined />}
disabled={record.flag === 1}
onClick={() => handleOpenAddFunc(record)}
>
增加方法
</Button>
<Button
type="link"
size="small"
icon={<PlusOutlined />}
onClick={() => handleGoAutoCode(record, 1)}
>
增加字段
</Button>
<Button
type="link"
size="small"
icon={<RollbackOutlined />}
disabled={record.flag === 1}
onClick={() => handleOpenRollback(record)}
>
回滚
</Button>
<Button
type="link"
size="small"
icon={<CopyOutlined />}
onClick={() => handleGoAutoCode(record)}
>
复用
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
<Card>
<div className={styles.toolbar}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleGoAutoCode(null)}
>
新增
</Button>
</div>
<Table
rowKey="ID"
columns={columns}
dataSource={dataSource}
loading={loading}
pagination={{
current,
pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `共 ${t} 条`,
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}
/>
</Card>
{/* Rollback Dialog */}
<Modal
title="回滚确认"
open={rollbackVisible}
onCancel={() => setRollbackVisible(false)}
footer={[
<Button key="cancel" onClick={() => setRollbackVisible(false)}>
取消
</Button>,
<Popconfirm
key="confirm"
title="此操作将回滚生成文件和勾选项目,是否继续?"
onConfirm={handleRollback}
okText="确定"
cancelText="取消"
>
<Button type="primary">确定</Button>
</Popconfirm>,
]}
>
<Form layout="inline">
<Form.Item label="选项">
<Space>
<Checkbox
checked={rollbackForm.deleteApi}
onChange={(e) =>
setRollbackForm((prev) => ({ ...prev, deleteApi: e.target.checked }))
}
>
删除接口
</Checkbox>
<Checkbox
checked={rollbackForm.deleteMenu}
onChange={(e) =>
setRollbackForm((prev) => ({ ...prev, deleteMenu: e.target.checked }))
}
>
删除菜单
</Checkbox>
<Checkbox
checked={rollbackForm.deleteTable}
onChange={(e) =>
setRollbackForm((prev) => ({ ...prev, deleteTable: e.target.checked }))
}
>
删除表
</Checkbox>
</Space>
</Form.Item>
</Form>
</Modal>
{/* Add Function Drawer */}
<Drawer
title="增加方法"
open={funcVisible}
onClose={() => setFuncVisible(false)}
width={600}
extra={
<Space>
<Button onClick={() => setFuncVisible(false)}>取消</Button>
<Button type="primary" loading={funcLoading} onClick={handleAddFunc}>
生成
</Button>
</Space>
}
>
<Form layout="vertical">
<Form.Item label="包名">
<Input value={funcForm.package} disabled />
</Form.Item>
<Form.Item label="结构体名">
<Input value={funcForm.structName} disabled />
</Form.Item>
<Form.Item label="描述">
<Input value={funcForm.description} disabled />
</Form.Item>
<Form.Item label="是否AI填充">
<Checkbox
checked={funcForm.isAi}
onChange={(e) =>
setFuncForm((prev) => ({ ...prev, isAi: e.target.checked }))
}
/>
<span className={styles.aiWarning}>
当前AI帮写存在不稳定因素,生成代码后请注意手动调整部分内容
</span>
</Form.Item>
{funcForm.isAi && (
<Form.Item label="AI帮写">
<Input.TextArea
value={funcForm.prompt}
onChange={(e) =>
setFuncForm((prev) => ({ ...prev, prompt: e.target.value }))
}
rows={4}
placeholder="AI帮写功能,输入提示信息,自动生成代码"
/>
</Form.Item>
)}
<Form.Item label="方法介绍">
<Space.Compact style={{ width: '100%' }}>
<Input
value={funcForm.funcDesc}
onChange={(e) =>
setFuncForm((prev) => ({ ...prev, funcDesc: e.target.value }))
}
placeholder="请输入方法介绍"
/>
<Button type="primary" icon={<RobotOutlined />}>
补全
</Button>
</Space.Compact>
</Form.Item>
<Form.Item label="方法名" required>
<Input
value={funcForm.funcName}
onChange={(e) =>
setFuncForm((prev) => ({ ...prev, funcName: e.target.value }))
}
placeholder="请输入方法名"
/>
</Form.Item>
<Form.Item label="方法">
<Select
value={funcForm.method}
onChange={(val) => setFuncForm((prev) => ({ ...prev, method: val }))}
options={[
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
]}
/>
</Form.Item>
<Form.Item label="是否鉴权">
<Checkbox
checked={funcForm.isAuth}
onChange={(e) =>
setFuncForm((prev) => ({ ...prev, isAuth: e.target.checked }))
}
/>
</Form.Item>
<Form.Item label="路由path" required>
<Input
value={funcForm.router}
onChange={(e) =>
setFuncForm((prev) => ({
...prev,
router: e.target.value.replace(/\//g, ''),
}))
}
placeholder="路由path"
/>
</Form.Item>
</Form>
</Drawer>
</PageContainer>
);
};
export default AutoCodeAdminPage;
@@ -0,0 +1,9 @@
.searchCard {
margin-bottom: 16px;
}
.tableCard {
.toolbar {
margin-bottom: 16px;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
.form-create-container {
padding: 16px;
.form-create-alert {
margin-bottom: 16px;
}
.form-designer-wrapper {
min-height: calc(100vh - 350px);
border: 1px dashed #d9d9d9;
border-radius: 4px;
background: #fafafa;
}
.form-designer-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 350px);
.placeholder-content {
text-align: center;
color: #999;
h3 {
font-size: 18px;
color: #666;
margin-bottom: 16px;
}
p {
margin-bottom: 8px;
}
ul {
list-style: none;
padding: 0;
margin-top: 16px;
li {
padding: 4px 0;
color: #1890ff;
}
}
}
}
.form-json-preview {
margin-top: 16px;
padding: 16px;
background: #f5f5f5;
border-radius: 4px;
h4 {
margin-bottom: 8px;
color: #333;
}
pre {
margin: 0;
padding: 12px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
max-height: 300px;
overflow: auto;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
}
}
// 暗色主题适配
.ant-layout-content[data-theme='dark'] {
.form-create-container {
.form-designer-wrapper {
background: #1f1f1f;
border-color: #434343;
}
.form-designer-placeholder {
.placeholder-content {
color: #8c8c8c;
h3 {
color: #d9d9d9;
}
}
}
.form-json-preview {
background: #262626;
h4 {
color: #d9d9d9;
}
pre {
background: #1f1f1f;
border-color: #434343;
color: #d9d9d9;
}
}
}
}
@@ -0,0 +1,170 @@
import React, { useRef, useEffect, useState } from 'react';
import { Card, Alert, Button, Space, message, Modal, Input } from 'antd';
import { CopyOutlined, DownloadOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons';
import './index.less';
/**
* 表单设计器页面
*
* 注意:使用 React 版本的表单设计器或自定义实现
*/
const FormCreate: React.FC = () => {
const [formJson, setFormJson] = useState<string>('');
const [previewVisible, setPreviewVisible] = useState(false);
const [importVisible, setImportVisible] = useState(false);
const [importJson, setImportJson] = useState('');
// 默认表单配置
const defaultConfig = {
fieldReadonly: false,
};
// 复制JSON到剪贴板
const handleCopyJson = () => {
if (!formJson) {
message.warning('暂无表单配置');
return;
}
navigator.clipboard.writeText(formJson).then(() => {
message.success('已复制到剪贴板');
}).catch(() => {
message.error('复制失败');
});
};
// 导出JSON文件
const handleExportJson = () => {
if (!formJson) {
message.warning('暂无表单配置');
return;
}
const blob = new Blob([formJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `form-config-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
message.success('导出成功');
};
// 导入JSON
const handleImportJson = () => {
try {
JSON.parse(importJson);
setFormJson(importJson);
setImportVisible(false);
setImportJson('');
message.success('导入成功');
} catch (e) {
message.error('JSON格式错误');
}
};
// 清空表单
const handleClear = () => {
Modal.confirm({
title: '确认清空',
content: '确定要清空当前表单配置吗?',
onOk: () => {
setFormJson('');
message.success('已清空');
},
});
};
return (
<div className="form-create-container">
<Alert
message="表单设计器"
description="拖拽组件到画布中设计表单,支持导入导出JSON配置。此功能需要集成 form-create-designer React 版本。"
type="info"
showIcon
className="form-create-alert"
/>
<Card
title="表单设计器"
extra={
<Space>
<Button icon={<UploadOutlined />} onClick={() => setImportVisible(true)}>
导入JSON
</Button>
<Button icon={<CopyOutlined />} onClick={handleCopyJson}>
复制JSON
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExportJson}>
导出JSON
</Button>
<Button icon={<DeleteOutlined />} danger onClick={handleClear}>
清空
</Button>
</Space>
}
>
<div className="form-designer-wrapper">
{/*
使用 fc-designer 组件:
<fc-designer ref="designer" :config="config" height="calc(100vh - 160px)" />
React 版本需要使用 @form-create/designer 的 React 版本
或者使用其他 React 表单设计器如 formily、amis 等
*/}
<div className="form-designer-placeholder">
<div className="placeholder-content">
<h3>表单设计器区域</h3>
<p>此处需要集成 React 版本的表单设计器组件</p>
<p>推荐方案:</p>
<ul>
<li>@form-create/designer (React版)</li>
<li>@formily/antd-v5</li>
<li>amis-editor</li>
</ul>
</div>
</div>
</div>
{formJson && (
<div className="form-json-preview">
<h4>当前表单配置:</h4>
<pre>{formJson}</pre>
</div>
)}
</Card>
{/* 导入JSON弹窗 */}
<Modal
title="导入JSON配置"
open={importVisible}
onOk={handleImportJson}
onCancel={() => {
setImportVisible(false);
setImportJson('');
}}
width={600}
>
<Input.TextArea
rows={10}
value={importJson}
onChange={(e) => setImportJson(e.target.value)}
placeholder="请粘贴JSON配置"
/>
</Modal>
{/* 预览弹窗 */}
<Modal
title="表单预览"
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
footer={null}
width={800}
>
<div className="form-preview-content">
{/* 表单预览内容 */}
</div>
</Modal>
</div>
);
};
export default FormCreate;
@@ -0,0 +1,181 @@
.install-plugin-container {
padding: 16px;
.install-plugin-alert {
margin-bottom: 16px;
}
.upload-card {
.ant-upload-drag {
padding: 40px 20px;
background: #fafafa;
border: 2px dashed #d9d9d9;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
background: #e6f7ff;
}
}
.ant-upload-drag-icon {
margin-bottom: 16px;
.anticon {
font-size: 64px;
color: #1890ff;
}
}
.ant-upload-text {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.ant-upload-hint {
font-size: 14px;
color: #999;
}
}
.upload-tips {
margin-top: 24px;
padding: 16px;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 4px;
h4 {
margin-bottom: 12px;
color: #d48806;
font-size: 14px;
}
ul {
margin: 0;
padding-left: 20px;
color: #666;
li {
padding: 4px 0;
line-height: 1.6;
}
}
}
.install-result {
margin-top: 24px;
.result-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;
color: #333;
}
.result-list {
.result-item {
padding: 8px 12px;
margin-bottom: 8px;
background: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
&.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
.anticon {
color: #52c41a;
}
}
&.error {
background: #fff2f0;
border: 1px solid #ffccc7;
.anticon {
color: #ff4d4f;
}
}
.result-index {
font-weight: 500;
color: #666;
}
.result-msg {
flex: 1;
}
}
}
}
}
// 暗色主题适配
.ant-layout-content[data-theme='dark'] {
.install-plugin-container {
.upload-card {
.ant-upload-drag {
background: #1f1f1f;
border-color: #434343;
&:hover {
border-color: #177ddc;
background: #111b26;
}
}
.ant-upload-text {
color: #d9d9d9;
}
.ant-upload-hint {
color: #8c8c8c;
}
}
.upload-tips {
background: #2b2111;
border-color: #594214;
h4 {
color: #d89614;
}
ul {
color: #8c8c8c;
}
}
.install-result {
.result-title {
color: #d9d9d9;
}
.result-list {
.result-item {
background: #262626;
&.success {
background: #162312;
border-color: #274916;
}
&.error {
background: #2a1215;
border-color: #58181c;
}
.result-index {
color: #8c8c8c;
}
}
}
}
}
}
@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Card, Upload, Alert, message, Modal } from 'antd';
import { InboxOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { getToken } from '@/utils/auth';
import './index.less';
const { Dragger } = Upload;
interface InstallResult {
msg: string;
success?: boolean;
}
/**
* 插件安装页面
*/
const InstallPlugin: React.FC = () => {
const [installResults, setInstallResults] = useState<InstallResult[]>([]);
// 获取上传地址
const getUploadUrl = () => {
const baseUrl = process.env.NODE_ENV === 'development'
? '/api'
: (window as any).VITE_BASE_API || '/api';
return `${baseUrl}/autoCode/installPlugin`;
};
// 上传成功/失败处理
const handleUploadChange = (info: any) => {
const { status, response } = info.file;
if (status === 'done') {
if (response?.code === 0) {
// 安装成功
const results = response.data || [];
setInstallResults(results.map((item: any) => ({
msg: item.msg,
success: true,
})));
// 显示结果弹窗
if (results.length > 0) {
const msgContent = results.map((item: any, index: number) =>
`${index + 1}. ${item.msg}`
).join('\n');
Modal.success({
title: '插件安装结果',
content: (
<pre style={{ whiteSpace: 'pre-wrap', maxHeight: 400, overflow: 'auto' }}>
{msgContent}
</pre>
),
width: 500,
});
} else {
message.success('插件安装成功');
}
} else {
// 安装失败
message.error(response?.msg || '插件安装失败');
setInstallResults([{
msg: response?.msg || '插件安装失败',
success: false,
}]);
}
} else if (status === 'error') {
message.error('上传失败,请检查网络连接');
setInstallResults([{
msg: '上传失败,请检查网络连接',
success: false,
}]);
}
};
// 上传前校验
const beforeUpload = (file: File) => {
const isZip = file.type === 'application/zip' ||
file.type === 'application/x-zip-compressed' ||
file.name.endsWith('.zip');
if (!isZip) {
message.error('只能上传 ZIP 格式的插件包!');
return false;
}
const isLt100M = file.size / 1024 / 1024 < 100;
if (!isLt100M) {
message.error('插件包大小不能超过 100MB!');
return false;
}
return true;
};
return (
<div className="install-plugin-container">
<Alert
message="插件安装"
description="上传插件安装包(ZIP格式)进行安装。安装前请确保插件包是通过标准插件模板生成的。"
type="info"
showIcon
className="install-plugin-alert"
/>
<Card className="upload-card">
<Dragger
name="plug"
action={getUploadUrl()}
headers={{
'x-token': getToken() || '',
}}
showUploadList={false}
beforeUpload={beforeUpload}
onChange={handleUploadChange}
accept=".zip"
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">拖拽或点击上传</p>
<p className="ant-upload-hint">请把安装包的 ZIP 拖拽至此处上传</p>
</Dragger>
<div className="upload-tips">
<h4>安装说明:</h4>
<ul>
<li>只支持标准插件(通过插件模板生成的标准目录插件)</li>
<li>非标准插件请自行打包安装</li>
<li>插件包必须是 ZIP 格式</li>
<li>安装包大小限制为 100MB</li>
<li>安装过程中请勿刷新页面</li>
</ul>
</div>
</Card>
{installResults.length > 0 && (
<div className="install-result">
<div className="result-title">安装结果:</div>
<div className="result-list">
{installResults.map((result, index) => (
<div
key={index}
className={`result-item ${result.success ? 'success' : 'error'}`}
>
{result.success ? (
<CheckCircleOutlined />
) : (
<CloseCircleOutlined />
)}
<span className="result-index">{index + 1}.</span>
<span className="result-msg">{result.msg}</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default InstallPlugin;
@@ -0,0 +1,107 @@
.pub-plug-container {
padding: 16px;
.pub-plug-alert {
margin-bottom: 16px;
}
.plugin-name-input {
margin-bottom: 16px;
}
.transfer-card {
margin-bottom: 16px;
.transfer-warning {
margin-bottom: 16px;
}
.parent-menu-input {
margin-bottom: 16px;
}
.transfer-wrapper {
display: flex;
justify-content: center;
padding: 16px 0;
:global {
.ant-transfer {
.ant-transfer-list {
width: 400px;
height: 350px;
}
.ant-transfer-list-header {
background: #fafafa;
}
.ant-transfer-list-body {
.ant-transfer-list-content-item {
padding: 8px 12px;
.transfer-item-content {
display: flex;
flex-direction: column;
gap: 2px;
.item-title {
font-weight: 500;
color: #333;
}
.item-path {
font-size: 12px;
color: #999;
}
}
}
}
}
}
}
.transfer-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
.pub-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
// 暗色主题适配
.ant-layout-content[data-theme='dark'] {
.pub-plug-container {
.transfer-card {
.transfer-wrapper {
:global {
.ant-transfer {
.ant-transfer-list-header {
background: #1f1f1f;
}
.ant-transfer-list-body {
.ant-transfer-list-content-item {
.transfer-item-content {
.item-title {
color: #d9d9d9;
}
.item-path {
color: #8c8c8c;
}
}
}
}
}
}
}
}
}
}
+305
View File
@@ -0,0 +1,305 @@
import React, { useState, useEffect } from 'react';
import { Card, Input, Transfer, Button, Alert, message, Modal, Space } from 'antd';
import type { TransferProps } from 'antd';
import { pubPlug, initMenu, initAPI } from '@/services/kratos/autoCode';
import { getAllApis } from '@/services/kratos/api';
import { getMenuList } from '@/services/kratos/menu';
import './index.less';
interface MenuItem {
ID: number;
meta: {
title: string;
};
component: string;
children?: MenuItem[];
}
interface ApiItem {
ID: number;
description: string;
path: string;
}
/**
* 插件打包页面
*/
const PubPlug: React.FC = () => {
const [plugName, setPlugName] = useState('');
const [parentMenu, setParentMenu] = useState('');
// 菜单穿梭框
const [menuTargetKeys, setMenuTargetKeys] = useState<string[]>([]);
const [menuDataSource, setMenuDataSource] = useState<any[]>([]);
// API穿梭框
const [apiTargetKeys, setApiTargetKeys] = useState<string[]>([]);
const [apiDataSource, setApiDataSource] = useState<any[]>([]);
// 递归展开菜单到一级
const flattenMenus = (menus: MenuItem[]): MenuItem[] => {
const result: MenuItem[] = [];
menus.forEach((item) => {
if (item.children && item.children.length > 0) {
result.push(...flattenMenus(item.children));
} else {
result.push(item);
}
});
return result;
};
// 初始化数据
const initData = async () => {
try {
// 获取菜单列表
const menuRes = await getMenuList();
if (menuRes.code === 0 && menuRes.data) {
const flatMenus = flattenMenus(menuRes.data);
setMenuDataSource(flatMenus.map((item) => ({
key: String(item.ID),
ID: item.ID,
title: item.meta?.title || '',
component: item.component || '',
})));
}
// 获取API列表
const apiRes = await getAllApis();
if (apiRes.code === 0 && apiRes.data?.apis) {
setApiDataSource(apiRes.data.apis.map((item: ApiItem) => ({
key: String(item.ID),
ID: item.ID,
description: item.description || '',
path: item.path || '',
})));
}
} catch (error) {
console.error('初始化数据失败:', error);
}
};
useEffect(() => {
initData();
}, []);
// 菜单穿梭框筛选
const filterMenuOption: TransferProps['filterOption'] = (inputValue, option) => {
return (
option.title?.toLowerCase().includes(inputValue.toLowerCase()) ||
option.component?.toLowerCase().includes(inputValue.toLowerCase())
);
};
// API穿梭框筛选
const filterApiOption: TransferProps['filterOption'] = (inputValue, option) => {
return (
option.description?.toLowerCase().includes(inputValue.toLowerCase()) ||
option.path?.toLowerCase().includes(inputValue.toLowerCase())
);
};
// 定义安装菜单
const handleInitMenu = () => {
if (!parentMenu) {
message.error('请填写菜单组名');
return;
}
if (menuTargetKeys.length === 0) {
message.error('请至少选择一个菜单');
return;
}
if (!plugName) {
message.error('请填写插件名');
return;
}
Modal.confirm({
title: '生成初始菜单',
content: `点击后将会覆盖 server 下的 /plugin/${plugName}/initialize/menu. 是否继续?`,
okText: '生成',
cancelText: '取消',
onOk: async () => {
try {
const res = await initMenu({
plugName,
parentMenu,
menus: menuTargetKeys.map(Number),
});
if (res.code === 0) {
message.success('菜单初始化成功');
} else {
message.error(res.msg || '菜单初始化失败');
}
} catch (error) {
message.error('菜单初始化失败');
}
},
});
};
// 定义安装API
const handleInitAPI = () => {
if (apiTargetKeys.length === 0) {
message.error('请至少选择一个API');
return;
}
if (!plugName) {
message.error('请填写插件名');
return;
}
Modal.confirm({
title: '生成初始API',
content: `点击后将会覆盖 server 下的 /plugin/${plugName}/initialize/api. 是否继续?`,
okText: '生成',
cancelText: '取消',
onOk: async () => {
try {
const res = await initAPI({
plugName,
apis: apiTargetKeys.map(Number),
});
if (res.code === 0) {
message.success('API初始化成功');
} else {
message.error(res.msg || 'API初始化失败');
}
} catch (error) {
message.error('API初始化失败');
}
},
});
};
// 打包插件
const handlePubPlugin = () => {
if (!plugName) {
message.error('请填写插件名');
return;
}
Modal.confirm({
title: '打包',
content: `请检查 server 下的 /plugin/${plugName}/plugin.go 是否已放开需要的 initialize.Api(ctx) 和 initialize.Menu(ctx)?`,
okText: '打包',
cancelText: '取消',
onOk: async () => {
try {
const res = await pubPlug({ plugName });
if (res.code === 0) {
message.success(res.msg || '打包成功');
} else {
message.error(res.msg || '打包失败');
}
} catch (error) {
message.error('打包失败');
}
},
});
};
return (
<div className="pub-plug-container">
<Alert
message="目前只支持标准插件(通过插件模板生成的标准目录插件),非标准插件请自行打包"
type="warning"
showIcon
className="pub-plug-alert"
/>
<div className="plugin-name-input">
<Input
value={plugName}
onChange={(e) => setPlugName(e.target.value)}
placeholder="插件模板处填写的【插件名】"
size="large"
/>
</div>
{/* 菜单选择 */}
<Card title="菜单选择" className="transfer-card">
<Alert
message="穿梭框请只选择子级菜单即可"
type="info"
showIcon
className="transfer-warning"
/>
<Input
value={parentMenu}
onChange={(e) => setParentMenu(e.target.value)}
placeholder="请输入菜单组名,例:公告管理"
className="parent-menu-input"
/>
<div className="transfer-wrapper">
<Transfer
dataSource={menuDataSource}
targetKeys={menuTargetKeys}
onChange={(newTargetKeys) => setMenuTargetKeys(newTargetKeys as string[])}
filterOption={filterMenuOption}
showSearch
titles={['可选菜单', '使用菜单']}
operations={['选中', '移除']}
render={(item) => (
<div className="transfer-item-content">
<span className="item-title">{item.title}</span>
<span className="item-path">{item.component}</span>
</div>
)}
listStyle={{
width: 400,
height: 350,
}}
/>
</div>
<div className="transfer-actions">
<Button type="primary" onClick={handleInitMenu}>
定义安装菜单
</Button>
</div>
</Card>
{/* API选择 */}
<Card title="API选择" className="transfer-card">
<div className="transfer-wrapper">
<Transfer
dataSource={apiDataSource}
targetKeys={apiTargetKeys}
onChange={(newTargetKeys) => setApiTargetKeys(newTargetKeys as string[])}
filterOption={filterApiOption}
showSearch
titles={['可选API', '使用API']}
operations={['选中', '移除']}
render={(item) => (
<div className="transfer-item-content">
<span className="item-title">{item.description}</span>
<span className="item-path">{item.path}</span>
</div>
)}
listStyle={{
width: 400,
height: 350,
}}
/>
</div>
<div className="transfer-actions">
<Button type="primary" onClick={handleInitAPI}>
定义安装API
</Button>
</div>
</Card>
<div className="pub-actions">
<Button type="primary" size="large" onClick={handlePubPlugin}>
打包插件
</Button>
</div>
</div>
);
};
export default PubPlug;
@@ -0,0 +1,33 @@
/**
* KRA - System Error Log Page Styles
*/
.searchCard {
margin-bottom: 16px;
}
.toolbar {
margin-bottom: 16px;
}
.preContent {
white-space: pre-wrap;
word-break: break-word;
margin: 0;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.6;
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
}
// Dark mode support
:global(.ant-layout-content[data-theme='dark']) {
.preContent {
background: #1f1f1f;
color: rgba(255, 255, 255, 0.85);
}
}
@@ -0,0 +1,421 @@
/**
* KRA - System Error Log Page
* 系统错误日志页面 - 查看、删除错误日志
*/
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Form,
Input,
DatePicker,
Drawer,
Descriptions,
Tag,
message,
Tooltip,
Popconfirm,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
SearchOutlined,
ReloadOutlined,
DeleteOutlined,
InfoCircleOutlined,
QuestionCircleOutlined,
DownOutlined,
UpOutlined,
RobotOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import {
getSysErrorList,
getSysErrorById,
deleteSysError,
deleteSysErrorByIds,
} from '@/services/kratos/sysError';
import { formatDate } from '@/utils/date';
import styles from './index.less';
const { RangePicker } = DatePicker;
interface SysError {
ID: number;
CreatedAt: string;
form: string;
level: string;
status: string;
info: string;
solution?: string;
}
// Level tag mapping
const levelTagMap: Record<string, string> = {
fatal: 'error',
error: 'warning',
};
const levelLabelMap: Record<string, string> = {
fatal: '致命错误',
error: '一般错误',
};
// Status tag mapping
const statusTagMap: Record<string, string> = {
'未处理': 'default',
'处理中': 'processing',
'处理完成': 'success',
'处理失败': 'error',
};
const statusLabelMap: Record<string, string> = {
'未处理': '未处理',
'处理中': '处理中',
'处理完成': '处理完成',
'处理失败': '处理失败',
};
const SysErrorPage: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<SysError[]>([]);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [showAllQuery, setShowAllQuery] = useState(false);
// Detail drawer
const [detailVisible, setDetailVisible] = useState(false);
const [detailData, setDetailData] = useState<SysError | null>(null);
// Fetch table data
const fetchData = async () => {
setLoading(true);
try {
const values = form.getFieldsValue();
const params: any = {
page: current,
pageSize,
...values,
};
if (values.createdAtRange) {
params.startCreatedAt = values.createdAtRange[0]?.format('YYYY-MM-DD HH:mm:ss');
params.endCreatedAt = values.createdAtRange[1]?.format('YYYY-MM-DD HH:mm:ss');
delete params.createdAtRange;
}
const res = await getSysErrorList(params);
if (res.code === 0) {
setDataSource(res.data?.list || []);
setTotal(res.data?.total || 0);
}
} catch (error) {
console.error('获取错误日志列表失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [current, pageSize]);
// Search
const handleSearch = () => {
setCurrent(1);
fetchData();
};
// Reset
const handleReset = () => {
form.resetFields();
setCurrent(1);
fetchData();
};
// Delete single row
const handleDelete = async (record: SysError) => {
try {
const res = await deleteSysError({ ID: record.ID });
if (res.code === 0) {
message.success('删除成功');
fetchData();
}
} catch (error) {
message.error('删除失败');
}
};
// Batch delete
const handleBatchDelete = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请选择要删除的数据');
return;
}
try {
const res = await deleteSysErrorByIds({ ids: selectedRowKeys as number[] });
if (res.code === 0) {
message.success('删除成功');
setSelectedRowKeys([]);
fetchData();
}
} catch (error) {
message.error('删除失败');
}
};
// View details
const handleViewDetail = async (record: SysError) => {
try {
const res = await getSysErrorById({ ID: record.ID });
if (res.code === 0) {
setDetailData(res.data?.reSysError || res.data);
setDetailVisible(true);
}
} catch (error) {
message.error('获取详情失败');
}
};
// Get AI solution (placeholder - would need backend support)
const handleGetSolution = async (id: number) => {
message.info('AI方案功能需要后端支持');
};
const columns: ColumnsType<SysError> = [
{
title: '日期',
dataIndex: 'CreatedAt',
key: 'CreatedAt',
width: 180,
sorter: true,
render: (text) => formatDate(text),
},
{
title: '错误来源',
dataIndex: 'form',
key: 'form',
width: 120,
},
{
title: '错误等级',
dataIndex: 'level',
key: 'level',
width: 120,
render: (level) => (
<Tag color={levelTagMap[level] || 'default'}>
{levelLabelMap[level] || '一般错误'}
</Tag>
),
},
{
title: '处理状态',
dataIndex: 'status',
key: 'status',
width: 140,
render: (status) => (
<Tag color={statusTagMap[status] || 'default'}>
{statusLabelMap[status] || '未处理'}
</Tag>
),
},
{
title: '错误内容',
dataIndex: 'info',
key: 'info',
width: 240,
ellipsis: {
showTitle: false,
},
render: (text) => (
<Tooltip placement="topLeft" title={text}>
{text}
</Tooltip>
),
},
{
title: '解决方案',
dataIndex: 'solution',
key: 'solution',
width: 120,
ellipsis: {
showTitle: false,
},
render: (text) => (
<Tooltip placement="topLeft" title={text}>
{text || '-'}
</Tooltip>
),
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 280,
render: (_, record) => (
<Space>
{record.status !== '处理中' && (
<Button
type="link"
icon={<RobotOutlined />}
onClick={() => handleGetSolution(record.ID)}
>
方案
</Button>
)}
<Button
type="link"
icon={<InfoCircleOutlined />}
onClick={() => handleViewDetail(record)}
>
查看
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
{/* Search Form */}
<Card className={styles.searchCard}>
<Form form={form} layout="inline" onFinish={handleSearch}>
<Form.Item
name="createdAtRange"
label={
<span>
创建日期
<Tooltip title="搜索范围是开始日期(包含)至结束日期(不包含)">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
>
<RangePicker showTime style={{ width: 380 }} />
</Form.Item>
<Form.Item name="form" label="错误来源">
<Input placeholder="搜索条件" />
</Form.Item>
<Form.Item name="info" label="错误内容">
<Input placeholder="搜索条件" />
</Form.Item>
{showAllQuery && (
<>
{/* Additional query fields can be added here */}
</>
)}
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>
重置
</Button>
<Button
type="link"
icon={showAllQuery ? <UpOutlined /> : <DownOutlined />}
onClick={() => setShowAllQuery(!showAllQuery)}
>
{showAllQuery ? '收起' : '展开'}
</Button>
</Space>
</Form.Item>
</Form>
</Card>
{/* Table */}
<Card>
<div className={styles.toolbar}>
<Space>
<Popconfirm
title="确定要删除选中的数据吗?"
onConfirm={handleBatchDelete}
okText="确定"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
>
<Button
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0}
>
删除
</Button>
</Popconfirm>
</Space>
</div>
<Table
rowKey="ID"
columns={columns}
dataSource={dataSource}
loading={loading}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
pagination={{
current,
pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `共 ${t} 条`,
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}
scroll={{ x: 1200 }}
/>
</Card>
{/* Detail Drawer */}
<Drawer
title="查看"
open={detailVisible}
onClose={() => setDetailVisible(false)}
width={600}
destroyOnClose
>
{detailData && (
<Descriptions column={2} bordered layout="vertical">
<Descriptions.Item label="错误来源">
{detailData.form}
</Descriptions.Item>
<Descriptions.Item label="错误等级">
<Tag color={levelTagMap[detailData.level] || 'default'}>
{levelLabelMap[detailData.level] || '一般错误'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Tag color={statusTagMap[detailData.status] || 'default'}>
{statusLabelMap[detailData.status] || '未处理'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="错误内容" span={2}>
<pre className={styles.preContent}>{detailData.info}</pre>
</Descriptions.Item>
<Descriptions.Item label="解决方案" span={2}>
<pre className={styles.preContent}>{detailData.solution || '-'}</pre>
</Descriptions.Item>
</Descriptions>
)}
</Drawer>
</PageContainer>
);
};
export default SysErrorPage;
@@ -0,0 +1,33 @@
.systemConfig {
background-color: #fff;
padding: 24px;
border-radius: 8px;
:global(.dark) & {
background-color: #1f2937;
}
.dbTitle {
padding: 10px;
margin: 10px 0;
font-size: 16px;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 4px;
background-color: #fafafa;
:global(.dark) & {
background-color: #374151;
}
}
.buttonGroup {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
:global(.dark) & {
border-top-color: #374151;
}
}
}
+723
View File
@@ -0,0 +1,723 @@
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import {
Tabs,
Form,
Input,
InputNumber,
Select,
Switch,
Button,
Space,
message,
Modal,
Spin,
Tooltip,
} from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { getSystemConfig, setSystemConfig, reloadSystem } from '@/services/kratos/system';
import { emailTest } from '@/services/kratos/email';
import styles from './index.less';
const { Option } = Select;
interface MongoHost {
host: string;
port: string;
}
// 生成UUID
const generateUUID = (): string => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
}).replace(/-/g, '');
};
const SystemConfigPage: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('1');
const [config, setConfig] = useState<any>({});
const [mongoHosts, setMongoHosts] = useState<MongoHost[]>([{ host: '', port: '' }]);
// 加载配置
const loadConfig = async () => {
setLoading(true);
try {
const res = await getSystemConfig();
if (res.code === 0 && res.data?.config) {
const configData = res.data.config;
setConfig(configData);
form.setFieldsValue(configData);
if (configData.mongo?.hosts) {
setMongoHosts(configData.mongo.hosts);
}
}
} catch (error) {
message.error('加载配置失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadConfig();
}, []);
// 更新配置
const handleUpdate = async () => {
try {
const values = await form.validateFields();
// 合并mongo hosts
if (values.mongo) {
values.mongo.hosts = mongoHosts;
}
const res = await setSystemConfig({ config: values });
if (res.code === 0) {
message.success('配置文件设置成功');
loadConfig();
}
} catch (error) {
message.error('保存配置失败');
}
};
// 重载服务
const handleReload = () => {
Modal.confirm({
title: '警告',
content: '确定要重载服务?',
onOk: async () => {
try {
const res = await reloadSystem();
if (res.code === 0) {
message.success('操作成功');
}
} catch (error) {
message.error('重载服务失败');
}
},
});
};
// 测试邮件
const handleEmailTest = async () => {
try {
const res = await emailTest({ to: form.getFieldValue(['email', 'to']) });
if (res.code === 0) {
message.success('邮件发送成功');
} else {
message.error('邮件发送失败');
}
} catch (error) {
message.error('邮件发送失败');
}
};
// 生成JWT签名
const handleGenerateUUID = () => {
const uuid = generateUUID();
form.setFieldValue(['jwt', 'signing-key'], uuid);
};
// 添加Mongo节点
const addMongoNode = () => {
setMongoHosts([...mongoHosts, { host: '', port: '' }]);
};
// 删除Mongo节点
const removeMongoNode = (index: number) => {
if (mongoHosts.length > 1) {
const newHosts = mongoHosts.filter((_, i) => i !== index);
setMongoHosts(newHosts);
}
};
// 更新Mongo节点
const updateMongoHost = (index: number, field: 'host' | 'port', value: string) => {
const newHosts = [...mongoHosts];
newHosts[index][field] = value;
setMongoHosts(newHosts);
};
const useRedis = Form.useWatch(['system', 'use-redis'], form);
const useMongo = Form.useWatch(['system', 'use-mongo'], form);
const dbType = Form.useWatch(['system', 'db-type'], form);
const ossType = Form.useWatch(['system', 'oss-type'], form);
// 数据库配置表单项
const renderDbConfig = (type: string, prefix: string) => (
<>
<h3 className={styles.dbTitle}>{type}</h3>
<Form.Item label="用户名" name={[prefix, 'username']}>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item label="密码" name={[prefix, 'password']}>
<Input placeholder="请输入密码" />
</Form.Item>
<Form.Item label="地址" name={[prefix, 'path']}>
<Input placeholder="请输入地址" />
</Form.Item>
{(prefix === 'mssql' || prefix === 'sqlite') && (
<Form.Item label="端口" name={[prefix, 'port']}>
<Input placeholder="请输入端口" />
</Form.Item>
)}
<Form.Item label="数据库名称" name={[prefix, 'db-name']}>
<Input placeholder="请输入数据库名称" />
</Form.Item>
{prefix !== 'sqlite' && (
<>
<Form.Item label="前缀" name={[prefix, 'prefix']}>
<Input placeholder="默认为空" />
</Form.Item>
<Form.Item label="复数表" name={[prefix, 'singular']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="引擎" name={[prefix, 'engine']}>
<Input placeholder="默认为InnoDB" />
</Form.Item>
</>
)}
<Form.Item label="maxIdleConns" name={[prefix, 'max-idle-conns']}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="maxOpenConns" name={[prefix, 'max-open-conns']}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="写入日志" name={[prefix, 'log-zap']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="日志模式" name={[prefix, 'log-mode']}>
<Select>
<Option value="off">关闭</Option>
<Option value="fatal">致命</Option>
<Option value="error">错误</Option>
<Option value="warn">警告</Option>
<Option value="info">信息</Option>
<Option value="debug">调试</Option>
<Option value="trace">跟踪</Option>
</Select>
</Form.Item>
</>
);
const tabItems = [
{
key: '1',
label: '系统配置',
children: (
<>
<Form.Item label="端口值" name={['system', 'addr']}>
<InputNumber placeholder="请输入端口值" style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="数据库类型" name={['system', 'db-type']}>
<Select>
<Option value="mysql">mysql</Option>
<Option value="pgsql">pgsql</Option>
<Option value="mssql">mssql</Option>
<Option value="sqlite">sqlite</Option>
<Option value="oracle">oracle</Option>
</Select>
</Form.Item>
<Form.Item label="Oss类型" name={['system', 'oss-type']}>
<Select>
<Option value="local">本地</Option>
<Option value="qiniu">七牛</Option>
<Option value="tencent-cos">腾讯云COS</Option>
<Option value="aliyun-oss">阿里云OSS</Option>
<Option value="huawei-obs">华为云OBS</Option>
<Option value="cloudflare-r2">cloudflare R2</Option>
</Select>
</Form.Item>
<Form.Item label="多点登录拦截" name={['system', 'use-multipoint']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="开启redis" name={['system', 'use-redis']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="开启Mongo" name={['system', 'use-mongo']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="严格角色模式" name={['system', 'use-strict-auth']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="限流次数" name={['system', 'iplimit-count']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="限流时间" name={['system', 'iplimit-time']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="禁用自动迁移数据库表结构" name={['system', 'disable-auto-migrate']} valuePropName="checked">
<Switch />
</Form.Item>
<Tooltip title="请修改完成后,注意一并修改前端env环境下的VITE_BASE_PATH">
<Form.Item label="全局路由前缀" name={['system', 'router-prefix']}>
<Input placeholder="请输入全局路由前缀" />
</Form.Item>
</Tooltip>
</>
),
},
{
key: '2',
label: 'jwt签名',
children: (
<>
<Form.Item label="jwt签名" name={['jwt', 'signing-key']}>
<Input
placeholder="请输入jwt签名"
addonAfter={<Button type="link" size="small" onClick={handleGenerateUUID}>生成</Button>}
/>
</Form.Item>
<Form.Item label="有效期" name={['jwt', 'expires-time']}>
<Input placeholder="请输入有效期" />
</Form.Item>
<Form.Item label="缓冲期" name={['jwt', 'buffer-time']}>
<Input placeholder="请输入缓冲期" />
</Form.Item>
<Form.Item label="签发者" name={['jwt', 'issuer']}>
<Input placeholder="请输入签发者" />
</Form.Item>
</>
),
},
{
key: '3',
label: 'Zap日志配置',
children: (
<>
<Form.Item label="级别" name={['zap', 'level']}>
<Select>
<Option value="off">关闭</Option>
<Option value="fatal">致命</Option>
<Option value="error">错误</Option>
<Option value="warn">警告</Option>
<Option value="info">信息</Option>
<Option value="debug">调试</Option>
<Option value="trace">跟踪</Option>
</Select>
</Form.Item>
<Form.Item label="输出" name={['zap', 'format']}>
<Select>
<Option value="console">console</Option>
<Option value="json">json</Option>
</Select>
</Form.Item>
<Form.Item label="日志前缀" name={['zap', 'prefix']}>
<Input placeholder="请输入日志前缀" />
</Form.Item>
<Form.Item label="日志文件夹" name={['zap', 'director']}>
<Input placeholder="请输入日志文件夹" />
</Form.Item>
<Form.Item label="编码级" name={['zap', 'encode-level']}>
<Select style={{ width: '50%' }}>
<Option value="LowercaseLevelEncoder">LowercaseLevelEncoder</Option>
<Option value="LowercaseColorLevelEncoder">LowercaseColorLevelEncoder</Option>
<Option value="CapitalLevelEncoder">CapitalLevelEncoder</Option>
<Option value="CapitalColorLevelEncoder">CapitalColorLevelEncoder</Option>
</Select>
</Form.Item>
<Form.Item label="栈名" name={['zap', 'stacktrace-key']}>
<Input placeholder="请输入栈名" />
</Form.Item>
<Form.Item label="日志留存时间(默认以天为单位)" name={['zap', 'retention-day']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="显示行" name={['zap', 'show-line']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="输出控制台" name={['zap', 'log-in-console']} valuePropName="checked">
<Switch />
</Form.Item>
</>
),
},
];
// Redis配置
if (useRedis) {
tabItems.push({
key: '4',
label: 'Redis',
children: (
<>
<Form.Item label="库" name={['redis', 'db']}>
<InputNumber min={0} max={16} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="地址" name={['redis', 'addr']}>
<Input placeholder="请输入地址" />
</Form.Item>
<Form.Item label="密码" name={['redis', 'password']}>
<Input placeholder="请输入密码" />
</Form.Item>
</>
),
});
}
// 邮箱配置
tabItems.push({
key: '5',
label: '邮箱配置',
children: (
<>
<Form.Item label="接收者邮箱" name={['email', 'to']}>
<Input placeholder="可多个,以逗号分隔" />
</Form.Item>
<Form.Item label="端口" name={['email', 'port']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="发送者邮箱" name={['email', 'from']}>
<Input placeholder="请输入发送者邮箱" />
</Form.Item>
<Form.Item label="host" name={['email', 'host']}>
<Input placeholder="请输入host" />
</Form.Item>
<Form.Item label="是否为ssl" name={['email', 'is-ssl']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="是否LoginAuth认证" name={['email', 'is-loginauth']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="secret" name={['email', 'secret']}>
<Input placeholder="请输入secret" />
</Form.Item>
<Form.Item label="测试邮件">
<Button onClick={handleEmailTest}>测试邮件</Button>
</Form.Item>
</>
),
});
// 验证码配置
tabItems.push({
key: '7',
label: '验证码配置',
children: (
<>
<Form.Item label="字符长度" name={['captcha', 'key-long']}>
<InputNumber min={4} max={6} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="图片宽度" name={['captcha', 'img-width']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="图片高度" name={['captcha', 'img-height']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</>
),
});
// 数据库配置
tabItems.push({
key: '9',
label: '数据库配置',
children: (
<>
{dbType === 'mysql' && renderDbConfig('MySQL', 'mysql')}
{dbType === 'pgsql' && renderDbConfig('PostgreSQL', 'pgsql')}
{dbType === 'mssql' && renderDbConfig('MsSQL', 'mssql')}
{dbType === 'sqlite' && renderDbConfig('SQLite', 'sqlite')}
{dbType === 'oracle' && renderDbConfig('Oracle', 'oracle')}
</>
),
});
// OSS配置
tabItems.push({
key: '10',
label: 'oss配置',
children: (
<>
{ossType === 'local' && (
<>
<h3 className={styles.dbTitle}>本地配置</h3>
<Form.Item label="本地文件访问路径" name={['local', 'path']}>
<Input placeholder="请输入本地文件访问路径" />
</Form.Item>
<Form.Item label="本地文件存储路径" name={['local', 'store-path']}>
<Input placeholder="请输入本地文件存储路径" />
</Form.Item>
</>
)}
{ossType === 'qiniu' && (
<>
<h3 className={styles.dbTitle}>七牛上传配置</h3>
<Form.Item label="存储区域" name={['qiniu', 'zone']}>
<Input placeholder="请输入存储区域" />
</Form.Item>
<Form.Item label="空间名称" name={['qiniu', 'bucket']}>
<Input placeholder="请输入空间名称" />
</Form.Item>
<Form.Item label="CDN加速域名" name={['qiniu', 'img-path']}>
<Input placeholder="请输入CDN加速域名" />
</Form.Item>
<Form.Item label="是否使用https" name={['qiniu', 'use-https']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="accessKey" name={['qiniu', 'access-key']}>
<Input placeholder="请输入accessKey" />
</Form.Item>
<Form.Item label="secretKey" name={['qiniu', 'secret-key']}>
<Input placeholder="请输入secretKey" />
</Form.Item>
<Form.Item label="上传是否使用CDN上传加速" name={['qiniu', 'use-cdn-domains']} valuePropName="checked">
<Switch />
</Form.Item>
</>
)}
{ossType === 'tencent-cos' && (
<>
<h3 className={styles.dbTitle}>腾讯云COS上传配置</h3>
<Form.Item label="存储桶名称" name={['tencent-cos', 'bucket']}>
<Input placeholder="请输入存储桶名称" />
</Form.Item>
<Form.Item label="所属地域" name={['tencent-cos', 'region']}>
<Input placeholder="请输入所属地域" />
</Form.Item>
<Form.Item label="secretID" name={['tencent-cos', 'secret-id']}>
<Input placeholder="请输入secretID" />
</Form.Item>
<Form.Item label="secretKey" name={['tencent-cos', 'secret-key']}>
<Input placeholder="请输入secretKey" />
</Form.Item>
<Form.Item label="路径前缀" name={['tencent-cos', 'path-prefix']}>
<Input placeholder="请输入路径前缀" />
</Form.Item>
<Form.Item label="访问域名" name={['tencent-cos', 'base-url']}>
<Input placeholder="请输入访问域名" />
</Form.Item>
</>
)}
{ossType === 'aliyun-oss' && (
<>
<h3 className={styles.dbTitle}>阿里云OSS上传配置</h3>
<Form.Item label="区域" name={['aliyun-oss', 'endpoint']}>
<Input placeholder="请输入区域" />
</Form.Item>
<Form.Item label="accessKeyId" name={['aliyun-oss', 'access-key-id']}>
<Input placeholder="请输入accessKeyId" />
</Form.Item>
<Form.Item label="accessKeySecret" name={['aliyun-oss', 'access-key-secret']}>
<Input placeholder="请输入accessKeySecret" />
</Form.Item>
<Form.Item label="存储桶名称" name={['aliyun-oss', 'bucket-name']}>
<Input placeholder="请输入存储桶名称" />
</Form.Item>
<Form.Item label="访问域名" name={['aliyun-oss', 'bucket-url']}>
<Input placeholder="请输入访问域名" />
</Form.Item>
</>
)}
{ossType === 'huawei-obs' && (
<>
<h3 className={styles.dbTitle}>华为云OBS上传配置</h3>
<Form.Item label="路径" name={['hua-wei-obs', 'path']}>
<Input placeholder="请输入路径" />
</Form.Item>
<Form.Item label="存储桶名称" name={['hua-wei-obs', 'bucket']}>
<Input placeholder="请输入存储桶名称" />
</Form.Item>
<Form.Item label="区域" name={['hua-wei-obs', 'endpoint']}>
<Input placeholder="请输入区域" />
</Form.Item>
<Form.Item label="accessKey" name={['hua-wei-obs', 'access-key']}>
<Input placeholder="请输入accessKey" />
</Form.Item>
<Form.Item label="secretKey" name={['hua-wei-obs', 'secret-key']}>
<Input placeholder="请输入secretKey" />
</Form.Item>
</>
)}
{ossType === 'cloudflare-r2' && (
<>
<h3 className={styles.dbTitle}>Cloudflare R2上传配置</h3>
<Form.Item label="路径" name={['cloudflare-r2', 'path']}>
<Input placeholder="请输入路径" />
</Form.Item>
<Form.Item label="存储桶名称" name={['cloudflare-r2', 'bucket']}>
<Input placeholder="请输入存储桶名称" />
</Form.Item>
<Form.Item label="Base URL" name={['cloudflare-r2', 'base-url']}>
<Input placeholder="请输入Base URL" />
</Form.Item>
<Form.Item label="Account ID" name={['cloudflare-r2', 'account-id']}>
<Input placeholder="请输入Account ID" />
</Form.Item>
<Form.Item label="Access Key ID" name={['cloudflare-r2', 'access-key-id']}>
<Input placeholder="请输入Access Key ID" />
</Form.Item>
<Form.Item label="Secret Access Key" name={['cloudflare-r2', 'secret-access-key']}>
<Input placeholder="请输入Secret Access Key" />
</Form.Item>
</>
)}
</>
),
});
// Excel上传配置
tabItems.push({
key: '11',
label: 'Excel上传配置',
children: (
<Form.Item label="合成目标地址" name={['excel', 'dir']}>
<Input placeholder="请输入合成目标地址" />
</Form.Item>
),
});
// 自动化代码配置
tabItems.push({
key: '12',
label: '自动化代码配置',
children: (
<>
<Form.Item label="是否自动重启(linux)" name={['autocode', 'transfer-restart']} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="root(项目根路径)" name={['autocode', 'root']}>
<Input disabled />
</Form.Item>
<Form.Item label="Server(后端代码地址)" name={['autocode', 'server']}>
<Input placeholder="请输入后端代码地址" />
</Form.Item>
<Form.Item label="SApi(后端api文件夹地址)" name={['autocode', 'server-api']}>
<Input placeholder="请输入后端api文件夹地址" />
</Form.Item>
<Form.Item label="SInitialize(后端Initialize文件夹)" name={['autocode', 'server-initialize']}>
<Input placeholder="请输入后端Initialize文件夹" />
</Form.Item>
<Form.Item label="SModel(后端Model文件地址)" name={['autocode', 'server-model']}>
<Input placeholder="请输入后端Model文件地址" />
</Form.Item>
<Form.Item label="SRequest(后端Request文件夹地址)" name={['autocode', 'server-request']}>
<Input placeholder="请输入后端Request文件夹地址" />
</Form.Item>
<Form.Item label="SRouter(后端Router文件夹地址)" name={['autocode', 'server-router']}>
<Input placeholder="请输入后端Router文件夹地址" />
</Form.Item>
<Form.Item label="SService(后端Service文件夹地址)" name={['autocode', 'server-service']}>
<Input placeholder="请输入后端Service文件夹地址" />
</Form.Item>
<Form.Item label="Web(前端文件夹地址)" name={['autocode', 'web']}>
<Input placeholder="请输入前端文件夹地址" />
</Form.Item>
<Form.Item label="WApi(后端WApi文件夹地址)" name={['autocode', 'web-api']}>
<Input placeholder="请输入后端WApi文件夹地址" />
</Form.Item>
<Form.Item label="WForm(后端WForm文件夹地址)" name={['autocode', 'web-form']}>
<Input placeholder="请输入后端WForm文件夹地址" />
</Form.Item>
<Form.Item label="WTable(后端WTable文件夹地址)" name={['autocode', 'web-table']}>
<Input placeholder="请输入后端WTable文件夹地址" />
</Form.Item>
</>
),
});
// Mongo配置
if (useMongo) {
tabItems.push({
key: '14',
label: 'Mongo 数据库配置',
children: (
<>
<Form.Item label="collection name(表名,一般不写)" name={['mongo', 'coll']}>
<Input placeholder="请输入collection name" />
</Form.Item>
<Form.Item label="mongodb 选项" name={['mongo', 'options']}>
<Input placeholder="请输入mongodb 选项" />
</Form.Item>
<Form.Item label="database name(数据库名)" name={['mongo', 'database']}>
<Input placeholder="请输入数据库名" />
</Form.Item>
<Form.Item label="用户名" name={['mongo', 'username']}>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item label="密码" name={['mongo', 'password']}>
<Input placeholder="请输入密码" />
</Form.Item>
<Form.Item label="最小连接池" name={['mongo', 'min-pool-size']}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="最大连接池" name={['mongo', 'max-pool-size']}>
<InputNumber min={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="socket超时时间" name={['mongo', 'socket-timeout-ms']}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="连接超时时间" name={['mongo', 'connect-timeout-ms']}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="是否开启zap日志" name={['mongo', 'is-zap']} valuePropName="checked">
<Switch />
</Form.Item>
{mongoHosts.map((host, index) => (
<Form.Item key={index} label={`节点 ${index + 1}`}>
<Space>
<Input
placeholder="请输入地址"
value={host.host}
onChange={(e) => updateMongoHost(index, 'host', e.target.value)}
style={{ width: 200 }}
/>
<Input
placeholder="请输入端口"
value={host.port}
onChange={(e) => updateMongoHost(index, 'port', e.target.value)}
style={{ width: 120 }}
/>
{index > 0 && (
<Button
type="primary"
danger
size="small"
icon={<MinusOutlined />}
onClick={() => removeMongoNode(index)}
/>
)}
</Space>
</Form.Item>
))}
<Form.Item label=" " colon={false}>
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={addMongoNode} />
</Form.Item>
</>
),
});
}
return (
<PageContainer>
<Spin spinning={loading}>
<div className={styles.systemConfig}>
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 14 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</Form>
<div className={styles.buttonGroup}>
<Space>
<Button type="primary" onClick={handleUpdate}>
立即更新
</Button>
<Button type="primary" onClick={handleReload}>
重载服务
</Button>
</Space>
</div>
</div>
</Spin>
</PageContainer>
);
};
export default SystemConfigPage;
@@ -0,0 +1,162 @@
/**
* KRA - Version Management Page Styles
*/
.searchCard {
margin-bottom: 16px;
}
.toolbar {
margin-bottom: 16px;
}
// Tree container for export drawer
.treeContainer {
display: flex;
gap: 12px;
width: 100%;
}
.treeCard {
flex: 1;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
background: #fff;
display: flex;
flex-direction: column;
}
.treeHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #d9d9d9;
}
.treeTitle {
margin: 0;
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.treeFilter {
padding: 12px 16px;
border-bottom: 1px solid #d9d9d9;
background: #fafafa;
}
.treeBody {
flex: 1;
padding: 8px;
min-height: 300px;
max-height: 400px;
overflow-y: auto;
}
// API tree item
.apiTreeItem {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 4px;
}
.apiPath {
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin-left: 8px;
}
// Dictionary tree item
.dictTreeItem {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 4px;
}
.dictType {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin-left: 8px;
}
.dictValue {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin-left: 8px;
}
// Preview container for import drawer
.previewContainer {
display: flex;
gap: 12px;
width: 100%;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 16px;
background: #fafafa;
}
.previewCard {
flex: 1;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.previewHeader {
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #d9d9d9;
font-weight: 500;
}
.previewBody {
padding: 8px;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
}
// Dark mode support
:global(.ant-layout-content[data-theme='dark']) {
.treeCard,
.previewCard {
background: #1f1f1f;
border-color: #434343;
}
.treeHeader,
.treeFilter,
.previewHeader {
background: #141414;
border-color: #434343;
}
.treeTitle {
color: rgba(255, 255, 255, 0.85);
}
.apiPath,
.dictType,
.dictValue {
color: rgba(255, 255, 255, 0.45);
}
.previewContainer {
background: #141414;
border-color: #434343;
}
}
+831
View File
@@ -0,0 +1,831 @@
/**
* KRA - Version Management Page
* 版本管理页面 - 创建发版、导入版本、下载发版包
*/
import React, { useState, useRef, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Form,
Input,
DatePicker,
Modal,
Drawer,
Descriptions,
Tree,
Upload,
message,
Tooltip,
Popconfirm,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { DataNode } from 'antd/es/tree';
import {
SearchOutlined,
ReloadOutlined,
DeleteOutlined,
DownloadOutlined,
UploadOutlined,
InfoCircleOutlined,
QuestionCircleOutlined,
DownOutlined,
UpOutlined,
InboxOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import {
getSysVersionList,
deleteSysVersion,
deleteSysVersionByIds,
findSysVersion,
exportVersion,
importVersion,
downloadVersionJson,
} from '@/services/kratos/version';
import { getMenuList } from '@/services/kratos/menu';
import { getApiList } from '@/services/kratos/api';
import { getDictionaryList } from '@/services/kratos/dictionary';
import { formatDate } from '@/utils/date';
import styles from './index.less';
const { RangePicker } = DatePicker;
const { Dragger } = Upload;
interface SysVersion {
ID: number;
CreatedAt: string;
versionName: string;
versionCode: string;
description?: string;
}
interface ExportForm {
versionName: string;
versionCode: string;
description: string;
menuIds: number[];
apiIds: number[];
dictIds: number[];
}
const VersionPage: React.FC = () => {
const [form] = Form.useForm();
const [exportForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<SysVersion[]>([]);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [showAllQuery, setShowAllQuery] = useState(false);
// Detail drawer
const [detailVisible, setDetailVisible] = useState(false);
const [detailData, setDetailData] = useState<SysVersion | null>(null);
// Export drawer
const [exportVisible, setExportVisible] = useState(false);
const [exportLoading, setExportLoading] = useState(false);
const [menuTreeData, setMenuTreeData] = useState<DataNode[]>([]);
const [apiTreeData, setApiTreeData] = useState<DataNode[]>([]);
const [dictTreeData, setDictTreeData] = useState<DataNode[]>([]);
const [checkedMenuKeys, setCheckedMenuKeys] = useState<React.Key[]>([]);
const [checkedApiKeys, setCheckedApiKeys] = useState<React.Key[]>([]);
const [checkedDictKeys, setCheckedDictKeys] = useState<React.Key[]>([]);
const [menuFilterText, setMenuFilterText] = useState('');
const [apiFilterText, setApiFilterText] = useState('');
const [dictFilterText, setDictFilterText] = useState('');
// Import drawer
const [importVisible, setImportVisible] = useState(false);
const [importLoading, setImportLoading] = useState(false);
const [importJsonContent, setImportJsonContent] = useState('');
const [importPreviewData, setImportPreviewData] = useState<any>(null);
// Fetch table data
const fetchData = async () => {
setLoading(true);
try {
const values = form.getFieldsValue();
const params: any = {
page: current,
pageSize,
...values,
};
if (values.createdAtRange) {
params.startCreatedAt = values.createdAtRange[0]?.format('YYYY-MM-DD HH:mm:ss');
params.endCreatedAt = values.createdAtRange[1]?.format('YYYY-MM-DD HH:mm:ss');
delete params.createdAtRange;
}
const res = await getSysVersionList(params);
if (res.code === 0) {
setDataSource(res.data?.list || []);
setTotal(res.data?.total || 0);
}
} catch (error) {
console.error('获取版本列表失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [current, pageSize]);
// Search
const handleSearch = () => {
setCurrent(1);
fetchData();
};
// Reset
const handleReset = () => {
form.resetFields();
setCurrent(1);
fetchData();
};
// Delete single row
const handleDelete = async (record: SysVersion) => {
try {
const res = await deleteSysVersion({ ID: record.ID });
if (res.code === 0) {
message.success('删除成功');
fetchData();
}
} catch (error) {
message.error('删除失败');
}
};
// Batch delete
const handleBatchDelete = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请选择要删除的数据');
return;
}
try {
const res = await deleteSysVersionByIds({ ids: selectedRowKeys as number[] });
if (res.code === 0) {
message.success('删除成功');
setSelectedRowKeys([]);
fetchData();
}
} catch (error) {
message.error('删除失败');
}
};
// View details
const handleViewDetail = async (record: SysVersion) => {
try {
const res = await findSysVersion({ ID: record.ID });
if (res.code === 0) {
setDetailData(res.data);
setDetailVisible(true);
}
} catch (error) {
message.error('获取详情失败');
}
};
// Download version JSON
const handleDownload = async (record: SysVersion) => {
try {
const res = await downloadVersionJson({ ID: record.ID });
const blob = res instanceof Blob ? res : new Blob([res]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${record.versionName}_${record.versionCode}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('下载成功');
} catch (error) {
message.error('下载失败');
}
};
// Build menu tree data
const buildMenuTree = (menus: any[]): DataNode[] => {
return menus.map((menu) => ({
key: menu.ID,
title: menu.meta?.title || menu.title || menu.name,
children: menu.children ? buildMenuTree(menu.children) : undefined,
}));
};
// Build API tree data (grouped by apiGroup)
const buildApiTree = (apis: any[]): DataNode[] => {
const groups: Record<string, any[]> = {};
apis.forEach((api) => {
const group = api.apiGroup || '未分组';
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(api);
});
return Object.entries(groups).map(([group, items]) => ({
key: `group_${group}`,
title: `${group}组`,
children: items.map((item) => ({
key: `p:${item.path}m:${item.method}`,
title: (
<div className={styles.apiTreeItem}>
<span>{item.description}</span>
<Tooltip title={item.path}>
<span className={styles.apiPath}>{item.path}</span>
</Tooltip>
</div>
),
data: item,
})),
}));
};
// Build dictionary tree data
const buildDictTree = (dicts: any[]): DataNode[] => {
return dicts.map((dict) => ({
key: dict.ID,
title: (
<div className={styles.dictTreeItem}>
<span>{dict.name}</span>
<span className={styles.dictType}>{dict.type}</span>
</div>
),
children: dict.sysDictionaryDetails?.map((detail: any) => ({
key: `detail_${detail.ID}`,
title: (
<div className={styles.dictTreeItem}>
<span>{detail.label}</span>
<span className={styles.dictValue}>值: {detail.value}</span>
</div>
),
})),
}));
};
// Open export drawer
const handleOpenExport = async () => {
setExportVisible(true);
try {
// Fetch menu list
const menuRes = await getMenuList();
if (menuRes.code === 0) {
setMenuTreeData(buildMenuTree(menuRes.data?.list || menuRes.data || []));
}
// Fetch API list
const apiRes = await getApiList({ page: 1, pageSize: 9999 });
if (apiRes.code === 0) {
setApiTreeData(buildApiTree(apiRes.data?.list || []));
}
// Fetch dictionary list
const dictRes = await getDictionaryList({ page: 1, pageSize: 9999 });
if (dictRes.code === 0) {
setDictTreeData(buildDictTree(dictRes.data?.list || []));
}
} catch (error) {
message.error('获取数据失败');
}
};
// Close export drawer
const handleCloseExport = () => {
setExportVisible(false);
exportForm.resetFields();
setCheckedMenuKeys([]);
setCheckedApiKeys([]);
setCheckedDictKeys([]);
setMenuFilterText('');
setApiFilterText('');
setDictFilterText('');
};
// Handle export
const handleExport = async () => {
try {
const values = await exportForm.validateFields();
if (!values.versionName || !values.versionCode) {
message.warning('请填写版本名称和版本号');
return;
}
setExportLoading(true);
const menuIds = checkedMenuKeys.filter((k) => typeof k === 'number') as number[];
const apiIds = checkedApiKeys
.filter((k) => typeof k === 'string' && k.startsWith('p:'))
.map((k) => k);
const dictIds = checkedDictKeys.filter((k) => typeof k === 'number') as number[];
const res = await exportVersion({
...values,
menuIds,
apiIds,
dictIds,
});
if (res.code === 0) {
message.success('创建发版成功');
handleCloseExport();
fetchData();
} else {
message.error(res.msg || '创建发版失败');
}
} catch (error) {
message.error('创建发版失败');
} finally {
setExportLoading(false);
}
};
// Open import drawer
const handleOpenImport = () => {
setImportVisible(true);
};
// Close import drawer
const handleCloseImport = () => {
setImportVisible(false);
setImportJsonContent('');
setImportPreviewData(null);
};
// Handle JSON content change
const handleJsonContentChange = (content: string) => {
setImportJsonContent(content);
if (!content.trim()) {
setImportPreviewData(null);
return;
}
try {
const data = JSON.parse(content);
setImportPreviewData({
menus: data.menus || [],
apis: data.apis || [],
dictionaries: data.dictionaries || [],
});
} catch (error) {
setImportPreviewData(null);
}
};
// Handle file upload
const handleFileUpload = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
JSON.parse(content); // Validate JSON
handleJsonContentChange(content);
message.success('文件上传成功');
} catch (error) {
message.error('JSON文件格式错误');
}
};
reader.readAsText(file);
return false; // Prevent auto upload
};
// Handle import
const handleImport = async () => {
if (!importJsonContent.trim()) {
message.warning('请输入版本JSON');
return;
}
try {
JSON.parse(importJsonContent);
} catch (error) {
message.error('JSON格式错误,请检查输入内容');
return;
}
setImportLoading(true);
try {
const data = JSON.parse(importJsonContent);
const res = await importVersion(data);
if (res.code === 0) {
message.success('导入成功');
handleCloseImport();
fetchData();
} else {
message.error(res.msg || '导入失败');
}
} catch (error) {
message.error('导入失败');
} finally {
setImportLoading(false);
}
};
// Count total menus recursively
const countMenus = (menus: any[]): number => {
let count = 0;
menus.forEach((menu) => {
count += 1;
if (menu.children?.length) {
count += countMenus(menu.children);
}
});
return count;
};
// Filter tree nodes
const filterTreeNode = (searchValue: string, node: DataNode): boolean => {
const title = typeof node.title === 'string' ? node.title : '';
return title.toLowerCase().includes(searchValue.toLowerCase());
};
const columns: ColumnsType<SysVersion> = [
{
title: '日期',
dataIndex: 'CreatedAt',
key: 'CreatedAt',
width: 180,
sorter: true,
render: (text) => formatDate(text),
},
{
title: '版本名称',
dataIndex: 'versionName',
key: 'versionName',
width: 120,
},
{
title: '版本号',
dataIndex: 'versionCode',
key: 'versionCode',
width: 120,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 320,
render: (_, record) => (
<Space>
<Button
type="link"
icon={<InfoCircleOutlined />}
onClick={() => handleViewDetail(record)}
>
查看
</Button>
<Button
type="link"
icon={<DownloadOutlined />}
onClick={() => handleDownload(record)}
>
下载发版包
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer>
{/* Search Form */}
<Card className={styles.searchCard}>
<Form form={form} layout="inline" onFinish={handleSearch}>
<Form.Item
name="createdAtRange"
label={
<span>
创建日期
<Tooltip title="搜索范围是开始日期(包含)至结束日期(不包含)">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
>
<RangePicker showTime style={{ width: 380 }} />
</Form.Item>
<Form.Item name="versionName" label="版本名称">
<Input placeholder="搜索条件" />
</Form.Item>
<Form.Item name="versionCode" label="版本号">
<Input placeholder="搜索条件" />
</Form.Item>
{showAllQuery && (
<>
{/* Additional query fields can be added here */}
</>
)}
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>
重置
</Button>
<Button
type="link"
icon={showAllQuery ? <UpOutlined /> : <DownOutlined />}
onClick={() => setShowAllQuery(!showAllQuery)}
>
{showAllQuery ? '收起' : '展开'}
</Button>
</Space>
</Form.Item>
</Form>
</Card>
{/* Table */}
<Card>
<div className={styles.toolbar}>
<Space>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleOpenExport}
>
创建发版
</Button>
<Button icon={<UploadOutlined />} onClick={handleOpenImport}>
导入版本
</Button>
<Popconfirm
title="确定要删除选中的数据吗?"
onConfirm={handleBatchDelete}
okText="确定"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
>
<Button
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0}
>
删除
</Button>
</Popconfirm>
</Space>
</div>
<Table
rowKey="ID"
columns={columns}
dataSource={dataSource}
loading={loading}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
pagination={{
current,
pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `共 ${t} 条`,
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}
scroll={{ x: 800 }}
/>
</Card>
{/* Detail Drawer */}
<Drawer
title="查看"
open={detailVisible}
onClose={() => setDetailVisible(false)}
width={500}
destroyOnClose
>
{detailData && (
<Descriptions column={1} bordered>
<Descriptions.Item label="版本名称">
{detailData.versionName}
</Descriptions.Item>
<Descriptions.Item label="版本号">
{detailData.versionCode}
</Descriptions.Item>
<Descriptions.Item label="版本描述">
{detailData.description}
</Descriptions.Item>
</Descriptions>
)}
</Drawer>
{/* Export Drawer */}
<Drawer
title="创建发版"
open={exportVisible}
onClose={handleCloseExport}
width="80%"
destroyOnClose
extra={
<Space>
<Button onClick={handleCloseExport}>取消</Button>
<Button type="primary" loading={exportLoading} onClick={handleExport}>
创建发版
</Button>
</Space>
}
>
<Form form={exportForm} layout="vertical">
<Form.Item
name="versionName"
label="版本名称"
rules={[{ required: true, message: '请输入版本名称' }]}
>
<Input placeholder="请输入版本名称" />
</Form.Item>
<Form.Item
name="versionCode"
label="版本号"
rules={[{ required: true, message: '请输入版本号' }]}
>
<Input placeholder="请输入版本号" />
</Form.Item>
<Form.Item name="description" label="版本描述">
<Input.TextArea placeholder="请输入版本描述" rows={3} />
</Form.Item>
<Form.Item label="发版信息">
<div className={styles.treeContainer}>
{/* Menu Tree */}
<div className={styles.treeCard}>
<div className={styles.treeHeader}>
<span className={styles.treeTitle}>选择菜单</span>
</div>
<div className={styles.treeFilter}>
<Input
placeholder="输入关键字进行过滤"
value={menuFilterText}
onChange={(e) => setMenuFilterText(e.target.value)}
allowClear
/>
</div>
<div className={styles.treeBody}>
<Tree
checkable
defaultExpandAll
checkedKeys={checkedMenuKeys}
onCheck={(keys) => setCheckedMenuKeys(keys as React.Key[])}
treeData={menuTreeData}
filterTreeNode={(node) => filterTreeNode(menuFilterText, node)}
/>
</div>
</div>
{/* API Tree */}
<div className={styles.treeCard}>
<div className={styles.treeHeader}>
<span className={styles.treeTitle}>选择API</span>
</div>
<div className={styles.treeFilter}>
<Input
placeholder="输入关键字进行过滤"
value={apiFilterText}
onChange={(e) => setApiFilterText(e.target.value)}
allowClear
/>
</div>
<div className={styles.treeBody}>
<Tree
checkable
defaultExpandAll
checkedKeys={checkedApiKeys}
onCheck={(keys) => setCheckedApiKeys(keys as React.Key[])}
treeData={apiTreeData}
filterTreeNode={(node) => filterTreeNode(apiFilterText, node)}
/>
</div>
</div>
{/* Dictionary Tree */}
<div className={styles.treeCard}>
<div className={styles.treeHeader}>
<span className={styles.treeTitle}>选择字典</span>
</div>
<div className={styles.treeFilter}>
<Input
placeholder="输入关键字进行过滤"
value={dictFilterText}
onChange={(e) => setDictFilterText(e.target.value)}
allowClear
/>
</div>
<div className={styles.treeBody}>
<Tree
checkable
defaultExpandAll
checkedKeys={checkedDictKeys}
onCheck={(keys) => setCheckedDictKeys(keys as React.Key[])}
treeData={dictTreeData}
filterTreeNode={(node) => filterTreeNode(dictFilterText, node)}
/>
</div>
</div>
</div>
</Form.Item>
</Form>
</Drawer>
{/* Import Drawer */}
<Drawer
title="导入版本"
open={importVisible}
onClose={handleCloseImport}
width="80%"
destroyOnClose
extra={
<Space>
<Button onClick={handleCloseImport}>取消</Button>
<Button
type="primary"
loading={importLoading}
onClick={handleImport}
disabled={!importJsonContent.trim()}
>
导入
</Button>
</Space>
}
>
<Form layout="vertical">
<Form.Item label="上传文件">
<Dragger
accept=".json"
beforeUpload={handleFileUpload}
showUploadList={true}
maxCount={1}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">将JSON文件拖到此处,或点击上传</p>
<p className="ant-upload-hint">只能上传JSON文件</p>
</Dragger>
</Form.Item>
<Form.Item label="版本JSON">
<Input.TextArea
value={importJsonContent}
onChange={(e) => handleJsonContentChange(e.target.value)}
rows={10}
placeholder="请粘贴版本JSON"
/>
</Form.Item>
{importPreviewData && (
<Form.Item label="预览内容">
<div className={styles.previewContainer}>
<div className={styles.previewCard}>
<div className={styles.previewHeader}>
菜单 ({countMenus(importPreviewData.menus)}项)
</div>
<div className={styles.previewBody}>
<Tree
treeData={buildMenuTree(importPreviewData.menus)}
defaultExpandAll
/>
</div>
</div>
<div className={styles.previewCard}>
<div className={styles.previewHeader}>
API ({importPreviewData.apis?.length || 0}项)
</div>
<div className={styles.previewBody}>
<Tree
treeData={buildApiTree(importPreviewData.apis)}
defaultExpandAll
/>
</div>
</div>
<div className={styles.previewCard}>
<div className={styles.previewHeader}>
字典 ({importPreviewData.dictionaries?.length || 0}项)
</div>
<div className={styles.previewBody}>
<Tree
treeData={buildDictTree(importPreviewData.dictionaries)}
defaultExpandAll
/>
</div>
</div>
</div>
</Form.Item>
)}
</Form>
</Drawer>
</PageContainer>
);
};
export default VersionPage;