前端重构
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user