832 lines
24 KiB
TypeScript
832 lines
24 KiB
TypeScript
/**
|
||
* 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;
|