- 添加菜单管理相关的 API 接口 - 实现菜单列表、新增菜单、编辑菜单、删除菜单等功能 - 添加菜单管理的国际化支持 - 新增菜单管理的样式文件main
parent
e1932a717b
commit
1890f9028d
@ -0,0 +1,34 @@ |
||||
.color-red { |
||||
color: red |
||||
} |
||||
|
||||
.text-center { |
||||
text-align: center |
||||
} |
||||
|
||||
.p-10 { |
||||
padding: 10px |
||||
} |
||||
|
||||
.construc-container table{ |
||||
display: table;; |
||||
} |
||||
|
||||
.search-bar-box{ |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
margin-bottom: 10px; |
||||
|
||||
.search-bar-left,.search-bar-right{ |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
} |
||||
.search-bar-left{ |
||||
width: 60%; |
||||
} |
||||
.search-bar-right{ |
||||
width: 40%; |
||||
} |
||||
} |
@ -0,0 +1,423 @@ |
||||
import { Space, Switch, Input, Table, Form, Button, Select, Popconfirm, App, Modal, InputNumber, TreeSelect } from 'antd'; |
||||
import { apiInterceptors, getMenuList, addMenu, updateMenu, deleteMenu, updateMenuStatus, getMenuDetail } from '@/client/api'; |
||||
import useUser from '@/hooks/use-user'; |
||||
import SystemConstructConstruct from '@/new-components/layout/SystemConstruct'; |
||||
import styles from './index.module.scss'; |
||||
import type { ColumnsType } from 'antd/es/table'; |
||||
import React, { useEffect, useState } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { Menu } from '@/types/prompt'; |
||||
import { useRequest } from 'ahooks'; |
||||
import { TFunction } from 'i18next'; |
||||
|
||||
|
||||
|
||||
const { Option } = Select; |
||||
|
||||
|
||||
const Prompt = () => { |
||||
const { message } = App.useApp(); |
||||
const { t } = useTranslation(); |
||||
const [menuList, setMenuList] = useState<Menu[]>([]); |
||||
const [searchForm] = Form.useForm(); |
||||
const [menuModalVisible, setMenuModalVisible] = useState(false); |
||||
const [menuModalType, setMenuModalType] = useState<'add' | 'edit'>('add'); |
||||
const [menuEditId, setMenuEditId] = useState<number | null>(null); |
||||
const [menuForm] = Form.useForm(); |
||||
|
||||
|
||||
useEffect(() => { |
||||
getMenus(); |
||||
}, []); |
||||
|
||||
const processMenuData = (data: Menu[]): Menu[] => { |
||||
return data.map(item => ({ |
||||
...item, |
||||
children: item.children?.length ? processMenuData(item.children) : undefined, |
||||
})); |
||||
}; |
||||
|
||||
const { run: getMenus, loading: loadingMenus } = useRequest( |
||||
async (params?: { name?: string; status?: string }) => { |
||||
const [_, data] = await apiInterceptors(getMenuList(params)); |
||||
return data; |
||||
}, |
||||
{ |
||||
manual: true, |
||||
onSuccess: (data: any) => { |
||||
if (Array.isArray(data)) { |
||||
const processedData = processMenuData(data); |
||||
setMenuList(processedData); |
||||
} |
||||
}, |
||||
} |
||||
); |
||||
|
||||
const handleAddMenu = () => { |
||||
setMenuModalType('add'); |
||||
setMenuModalVisible(true); |
||||
menuForm.resetFields(); |
||||
}; |
||||
|
||||
const handleEditMenu = async (record: Menu) => { |
||||
setMenuModalType('edit'); |
||||
setMenuEditId(record.id); |
||||
setMenuModalVisible(true); |
||||
|
||||
try { |
||||
const [err, data] = await apiInterceptors(getMenuDetail(record.id)); |
||||
|
||||
if (!err && data) { |
||||
menuForm.setFieldsValue({ |
||||
...data, |
||||
status: data.status === 0, |
||||
visible: data.visible === 1, |
||||
}); |
||||
} else { |
||||
message.error('获取菜单详情失败'); |
||||
} |
||||
} catch (e) { |
||||
message.error('获取菜单详情失败'); |
||||
} |
||||
}; |
||||
|
||||
const buildMenuTreeOptions = (list: Menu[]): any[] => { |
||||
// 先递归构建原始菜单树
|
||||
const menuOptions = list.map(item => ({ |
||||
title: item.name, |
||||
value: item.id, |
||||
children: item.children ? buildMenuTreeOptionsRecursive(item.children) : [], |
||||
})); |
||||
|
||||
// 最外层包裹一个“根目录”
|
||||
return [ |
||||
{ |
||||
title: '根目录', |
||||
value: 0, |
||||
children: menuOptions, |
||||
}, |
||||
]; |
||||
}; |
||||
|
||||
// 仅用于递归构建子节点,不添加“根目录”
|
||||
const buildMenuTreeOptionsRecursive = (list: Menu[]): any[] => { |
||||
return list.map(item => ({ |
||||
title: item.name, |
||||
value: item.id, |
||||
children: item.children ? buildMenuTreeOptionsRecursive(item.children) : [], |
||||
})); |
||||
}; |
||||
|
||||
const handleDeleteMenu = async (record: Menu) => { |
||||
if (record.children && record.children.length > 0) { |
||||
message.warning('该菜单包含子菜单,不能直接删除'); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
const [_, res] = await apiInterceptors(deleteMenu(record.id)); |
||||
if (res) { |
||||
message.success('删除成功'); |
||||
await getMenus(); // 刷新列表
|
||||
} else { |
||||
message.error(res?.err_msg || '删除失败'); |
||||
} |
||||
} catch (e) { |
||||
message.error('请求失败,请重试'); |
||||
} |
||||
}; |
||||
|
||||
const getMenuColumns = (t: TFunction): ColumnsType<Menu> => [ |
||||
{ |
||||
title: t('menu_nmae'), |
||||
dataIndex: 'name', |
||||
key: 'name', |
||||
width: '20%', |
||||
ellipsis: true, |
||||
}, |
||||
{ |
||||
title: t('component_path'), |
||||
dataIndex: 'path', |
||||
key: 'path', |
||||
width: '25%', |
||||
ellipsis: true, |
||||
}, |
||||
{ |
||||
title: t('component_name'), |
||||
dataIndex: 'component_name', |
||||
key: 'component_name', |
||||
width: '25%', |
||||
ellipsis: true, |
||||
}, |
||||
{ |
||||
title: t('rights_markup'), |
||||
dataIndex: 'permission', |
||||
key: 'permission', |
||||
width: '15%', |
||||
}, |
||||
{ |
||||
title: t('sort'), |
||||
dataIndex: 'sort', |
||||
key: 'sort', |
||||
width: '8%', |
||||
align: 'center', |
||||
}, |
||||
{ |
||||
title: t('Status'), |
||||
dataIndex: 'status', |
||||
key: 'status', |
||||
width: '10%', |
||||
render: (status: number, record: Menu) => ( |
||||
<Switch |
||||
checked={status === 0} |
||||
checkedChildren="启用" |
||||
unCheckedChildren="停用" |
||||
onChange={async (checked) => { |
||||
try { |
||||
const [_, res] = await apiInterceptors( |
||||
updateMenuStatus({ id: record.id, status: checked ? 0 : 1 }) |
||||
); |
||||
if (res) { |
||||
message.success('状态更新成功'); |
||||
// 刷新列表
|
||||
getMenus(); |
||||
} else { |
||||
message.error('状态更新失败'); |
||||
} |
||||
} catch (e) { |
||||
message.error('请求失败,请重试'); |
||||
} |
||||
}} |
||||
/> |
||||
), |
||||
}, |
||||
{ |
||||
title: t('Operation'), |
||||
dataIndex: 'operate', |
||||
key: 'operate', |
||||
width: '15%', |
||||
render: (_, record) => ( |
||||
<Space> |
||||
<Button type="link" size="small" onClick={() => handleEditMenu(record)}> |
||||
编辑 |
||||
</Button> |
||||
<Popconfirm title="确认删除吗?" onConfirm={async () => await handleDeleteMenu(record)}> |
||||
<Button type="link" size="small" danger> |
||||
删除 |
||||
</Button> |
||||
</Popconfirm> |
||||
</Space> |
||||
), |
||||
}, |
||||
]; |
||||
|
||||
|
||||
const handleSearch = async () => { |
||||
try { |
||||
const values = await searchForm.validateFields(); |
||||
// 这里可以发请求给后端带参数 /api/menu/tree?name=xxx&status=xxx
|
||||
await getMenus(values); |
||||
} catch (err) { |
||||
console.error('表单验证失败:', err); |
||||
} |
||||
}; |
||||
|
||||
const handleReset = async () => { |
||||
searchForm.resetFields(); // 清空表单
|
||||
await getMenus(); // 重新获取全部数据
|
||||
}; |
||||
return ( |
||||
<SystemConstructConstruct> |
||||
<div className={`px-6 py-2 ${styles['construc-container']} h-[90vh] overflow-y-auto`}> |
||||
|
||||
<Form |
||||
form={searchForm} |
||||
layout="inline" |
||||
onFinish={handleSearch} |
||||
className="mb-4" |
||||
> |
||||
<Form.Item label="菜单名称" name="name"> |
||||
<Input placeholder="请输入菜单名称" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="状态" name="status"> |
||||
<Select style={{ width: 120 }} placeholder="请选择状态"> |
||||
<Option value="">全部</Option> |
||||
<Option value="0">启用</Option> |
||||
<Option value="1">停用</Option> |
||||
</Select> |
||||
</Form.Item> |
||||
|
||||
<Form.Item> |
||||
<Space> |
||||
<Button type="primary" htmlType="submit"> |
||||
搜索 |
||||
</Button> |
||||
<Button onClick={handleReset}>重置</Button> |
||||
<Button type="primary" onClick={handleAddMenu}> |
||||
新增 |
||||
</Button> |
||||
</Space> |
||||
</Form.Item> |
||||
</Form> |
||||
<Modal |
||||
title={menuModalType === 'add' ? '新增菜单' : '编辑菜单'} |
||||
open={menuModalVisible} |
||||
onCancel={() => { |
||||
setMenuModalVisible(false); |
||||
menuForm.resetFields(); |
||||
}} |
||||
footer={null} |
||||
> |
||||
<Form |
||||
form={menuForm} |
||||
labelCol={{ span: 6 }} |
||||
initialValues={{ |
||||
visible: true // 确保新增时默认开启
|
||||
}} |
||||
wrapperCol={{ span: 16 }} |
||||
onFinish={async (values) => { |
||||
const params = { |
||||
...values, |
||||
status: 0, |
||||
visible: values.visible ? 1 : 0, |
||||
}; |
||||
try { |
||||
let apiCall; |
||||
if (menuModalType === 'edit' && menuEditId) { |
||||
apiCall = updateMenu({ ...params, id: menuEditId }); |
||||
} else { |
||||
apiCall = addMenu(params); |
||||
} |
||||
|
||||
const [_, res] = await apiInterceptors(apiCall); |
||||
if (res) { |
||||
message.success(menuModalType === 'edit' ? '编辑成功' : '新增成功'); |
||||
setMenuModalVisible(false); |
||||
menuForm.resetFields(); |
||||
await getMenus(); // 刷新列表
|
||||
} else { |
||||
message.error(res?.err_msg || '操作失败'); |
||||
} |
||||
} catch (e) { |
||||
message.error('请求失败,请重试'); |
||||
} |
||||
}} |
||||
> |
||||
<Form.Item label="菜单名称" name="name" rules={[{ required: true }]}> |
||||
<Input placeholder="请输入菜单名称" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="权限标识" name="permission"> |
||||
<Input placeholder="请输入权限标识" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="菜单类型" name="type" rules={[{ required: true }]}> |
||||
<Select placeholder="请选择菜单类型"> |
||||
<Option value={1}>目录</Option> |
||||
<Option value={2}>菜单</Option> |
||||
<Option value={3}>按钮</Option> |
||||
</Select> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="排序" name="sort" rules={[{ required: true }]}> |
||||
<InputNumber min={0} style={{ width: '100%' }} /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="父级菜单" name="parent_id" rules={[{ required: true }]}> |
||||
<TreeSelect |
||||
treeData={buildMenuTreeOptions(menuList)} |
||||
placeholder="请选择上级菜单" |
||||
treeDefaultExpandAll |
||||
/> |
||||
</Form.Item> |
||||
|
||||
|
||||
<Form.Item |
||||
label="路由地址" |
||||
name="path" |
||||
rules={[ |
||||
{ required: true, message: '请输入路由地址' }, |
||||
{ |
||||
validator(_, value) { |
||||
const trimmed = value?.trim(); |
||||
if (!trimmed) { |
||||
return Promise.reject(new Error('请输入路由地址')); |
||||
} |
||||
if (trimmed.startsWith('/')) { |
||||
return Promise.resolve(); |
||||
} |
||||
return Promise.reject(new Error('路由地址必须以 / 开头')); |
||||
}, |
||||
}, |
||||
]} |
||||
normalize={(value) => value?.trim()} // 自动去除前后空格
|
||||
> |
||||
<Input placeholder="请输入组件路径" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item |
||||
label="组件路径" |
||||
name="component" |
||||
rules={[ |
||||
{ required: true, message: '请输入组件路径' }, |
||||
{ |
||||
validator(_, value) { |
||||
const trimmed = value?.trim(); |
||||
if (!trimmed) { |
||||
return Promise.reject(new Error('请输入组件路径')); |
||||
} |
||||
if (trimmed.startsWith('/')) { |
||||
return Promise.resolve(); |
||||
} |
||||
return Promise.reject(new Error('组件路径必须以 / 开头')); |
||||
}, |
||||
}, |
||||
]} |
||||
normalize={(value) => value?.trim()} // 自动去除前后空格
|
||||
> |
||||
<Input placeholder="请输入组件路径" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item label="组件名称" name="component_name" rules={[{ required: true }]}> |
||||
<Input placeholder="请输入组件名称" /> |
||||
</Form.Item> |
||||
|
||||
{/* <Form.Item label="状态" name="status" valuePropName="checked"> |
||||
<Switch checkedChildren="启用" unCheckedChildren="停用" defaultChecked /> |
||||
</Form.Item> */} |
||||
|
||||
<Form.Item |
||||
label="是否可见" |
||||
name="visible" |
||||
valuePropName="checked" |
||||
getValueFromEvent={(e) => (e ? 1 : 0)} |
||||
initialValue={true} |
||||
> |
||||
<Switch checkedChildren="是" unCheckedChildren="否" /> |
||||
</Form.Item> |
||||
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}> |
||||
<Space> |
||||
<Button onClick={() => setMenuModalVisible(false)}>取消</Button> |
||||
<Button type="primary" htmlType="submit">提交</Button> |
||||
</Space> |
||||
</Form.Item> |
||||
</Form> |
||||
</Modal> |
||||
|
||||
<Table |
||||
columns={getMenuColumns(t)} |
||||
dataSource={menuList} |
||||
rowKey="id" |
||||
loading={loadingMenus} // ✅ 显示加载状态
|
||||
childrenColumnName="children" |
||||
defaultExpandAllRows |
||||
pagination={false} |
||||
/> |
||||
</div> |
||||
</SystemConstructConstruct> |
||||
); |
||||
}; |
||||
|
||||
export default Prompt; |
Loading…
Reference in new issue