/** * 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([]); const [total, setTotal] = useState(0); const [current, setCurrent] = useState(1); const [pageSize, setPageSize] = useState(10); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [showAllQuery, setShowAllQuery] = useState(false); // Detail drawer const [detailVisible, setDetailVisible] = useState(false); const [detailData, setDetailData] = useState(null); // Export drawer const [exportVisible, setExportVisible] = useState(false); const [exportLoading, setExportLoading] = useState(false); const [menuTreeData, setMenuTreeData] = useState([]); const [apiTreeData, setApiTreeData] = useState([]); const [dictTreeData, setDictTreeData] = useState([]); const [checkedMenuKeys, setCheckedMenuKeys] = useState([]); const [checkedApiKeys, setCheckedApiKeys] = useState([]); const [checkedDictKeys, setCheckedDictKeys] = useState([]); 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(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 = {}; 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: (
{item.description} {item.path}
), data: item, })), })); }; // Build dictionary tree data const buildDictTree = (dicts: any[]): DataNode[] => { return dicts.map((dict) => ({ key: dict.ID, title: (
{dict.name} {dict.type}
), children: dict.sysDictionaryDetails?.map((detail: any) => ({ key: `detail_${detail.ID}`, title: (
{detail.label} 值: {detail.value}
), })), })); }; // 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 = [ { 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) => ( handleDelete(record)} okText="确定" cancelText="取消" > ), }, ]; return ( {/* Search Form */}
创建日期 } > {showAllQuery && ( <> {/* Additional query fields can be added here */} )}
{/* Table */}
`共 ${t} 条`, onChange: (page, size) => { setCurrent(page); setPageSize(size); }, }} scroll={{ x: 800 }} /> {/* Detail Drawer */} setDetailVisible(false)} width={500} destroyOnClose > {detailData && ( {detailData.versionName} {detailData.versionCode} {detailData.description} )} {/* Export Drawer */} } >
{/* Menu Tree */}
选择菜单
setMenuFilterText(e.target.value)} allowClear />
setCheckedMenuKeys(keys as React.Key[])} treeData={menuTreeData} filterTreeNode={(node) => filterTreeNode(menuFilterText, node)} />
{/* API Tree */}
选择API
setApiFilterText(e.target.value)} allowClear />
setCheckedApiKeys(keys as React.Key[])} treeData={apiTreeData} filterTreeNode={(node) => filterTreeNode(apiFilterText, node)} />
{/* Dictionary Tree */}
选择字典
setDictFilterText(e.target.value)} allowClear />
setCheckedDictKeys(keys as React.Key[])} treeData={dictTreeData} filterTreeNode={(node) => filterTreeNode(dictFilterText, node)} />
{/* Import Drawer */} } >

将JSON文件拖到此处,或点击上传

只能上传JSON文件

handleJsonContentChange(e.target.value)} rows={10} placeholder="请粘贴版本JSON" /> {importPreviewData && (
菜单 ({countMenus(importPreviewData.menus)}项)
API ({importPreviewData.apis?.length || 0}项)
字典 ({importPreviewData.dictionaries?.length || 0}项)
)}
); }; export default VersionPage;