|
@@ -0,0 +1,829 @@
|
|
|
|
|
+import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
|
|
|
|
+import { menuApi, MenuVO, MenuType, MenuStatus } from '../api/Menu';
|
|
|
|
|
+import { handleTree, TreeNode } from '../utils/tree';
|
|
|
|
|
+import { toast } from 'sonner';
|
|
|
|
|
+import {
|
|
|
|
|
+ Maximize2, HelpCircle, ChevronDown, Check,
|
|
|
|
|
+ FileText, Folder, Menu, Settings, User, Users,
|
|
|
|
|
+ Lock, Unlock, Search, Edit, Trash2, Plus, Minus,
|
|
|
|
|
+ CheckCircle, X, AlertTriangle, Info, Bell, MessageSquare,
|
|
|
|
|
+ Phone, MapPin, Calendar, Clock, Star, Share2,
|
|
|
|
|
+ Download, Upload, RefreshCw, Eye, EyeOff, Home,
|
|
|
|
|
+ ShoppingBag, Package, Database, PieChart, BarChart3,
|
|
|
|
|
+ Grid3x3, List, Link2, Key, Wrench, Monitor, Server
|
|
|
|
|
+} from 'lucide-react';
|
|
|
|
|
+import { Input } from './ui/input';
|
|
|
|
|
+import { Label } from './ui/label';
|
|
|
|
|
+import { Switch } from './ui/switch';
|
|
|
|
|
+import { Popover, PopoverTrigger, PopoverContent } from './ui/popover';
|
|
|
|
|
+import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
|
|
|
|
|
+
|
|
|
|
|
+interface MenuNode extends Omit<MenuVO, 'id'>, Omit<TreeNode, 'id'> {
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ children?: MenuNode[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface MenuFormProps {
|
|
|
|
|
+ onSuccess?: () => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface MenuFormRef {
|
|
|
|
|
+ open: (type: string, id?: number, parentId?: number) => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) => {
|
|
|
|
|
+ const [dialogVisible, setDialogVisible] = useState(false);
|
|
|
|
|
+ const [dialogTitle, setDialogTitle] = useState('');
|
|
|
|
|
+ const [formType, setFormType] = useState<'create' | 'update'>('create');
|
|
|
|
|
+ const [formLoading, setFormLoading] = useState(false);
|
|
|
|
|
+ const [menuTree, setMenuTree] = useState<MenuNode[]>([]);
|
|
|
|
|
+ const [popoverOpen, setPopoverOpen] = useState(false);
|
|
|
|
|
+ const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
|
|
|
|
+ const [formData, setFormData] = useState<MenuVO>({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ permission: '',
|
|
|
|
|
+ type: MenuType.DIR,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ parentId: 0,
|
|
|
|
|
+ path: '',
|
|
|
|
|
+ icon: '',
|
|
|
|
|
+ component: '',
|
|
|
|
|
+ componentName: '',
|
|
|
|
|
+ status: 0, // 0 是开启,1 是关闭
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ keepAlive: true,
|
|
|
|
|
+ alwaysShow: true,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 暴露方法给父组件
|
|
|
|
|
+ useImperativeHandle(ref, () => ({
|
|
|
|
|
+ open: (type: string, id?: number, parentId?: number) => {
|
|
|
|
|
+ setDialogTitle(type === 'create' ? '新增菜单' : '修改菜单');
|
|
|
|
|
+ setFormType(type as 'create' | 'update');
|
|
|
|
|
+ resetForm();
|
|
|
|
|
+
|
|
|
|
|
+ if (parentId) {
|
|
|
|
|
+ setFormData(prev => ({ ...prev, parentId }));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (id) {
|
|
|
|
|
+ loadMenuData(id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setFormLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取菜单树
|
|
|
|
|
+ getTree();
|
|
|
|
|
+
|
|
|
|
|
+ // 设置 dialogVisible 为 true
|
|
|
|
|
+ setDialogVisible(true);
|
|
|
|
|
+ },
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 组件挂载时获取菜单树
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ getTree();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取菜单树
|
|
|
|
|
+ const getTree = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await menuApi.getSimpleMenusList();
|
|
|
|
|
+ const data = (res as any)?.data || res || [];
|
|
|
|
|
+ const treeData = handleTree<MenuNode>(data);
|
|
|
|
|
+ const rootMenu: MenuNode = {
|
|
|
|
|
+ id: 0,
|
|
|
|
|
+ name: '主类目',
|
|
|
|
|
+ children: treeData,
|
|
|
|
|
+ type: MenuType.DIR,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ parentId: 0,
|
|
|
|
|
+ path: '',
|
|
|
|
|
+ icon: '',
|
|
|
|
|
+ permission: '',
|
|
|
|
|
+ status: 0, // 0 是开启,1 是关闭
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ keepAlive: false,
|
|
|
|
|
+ };
|
|
|
|
|
+ setMenuTree([rootMenu]);
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ toast.error(error.message || '获取菜单树失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 加载菜单数据
|
|
|
|
|
+ const loadMenuData = async (id: number) => {
|
|
|
|
|
+ setFormLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await menuApi.getMenu(id);
|
|
|
|
|
+ const data = (res as any)?.data || res;
|
|
|
|
|
+ // 确保 status 是数字类型,用于正确回显
|
|
|
|
|
+ setFormData({
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? 0) // 0 是开启,1 是关闭
|
|
|
|
|
+ } as MenuVO);
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ toast.error(error.message || '获取菜单详情失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setFormLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 重置表单
|
|
|
|
|
+ const resetForm = () => {
|
|
|
|
|
+ setFormData({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ permission: '',
|
|
|
|
|
+ type: MenuType.DIR,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ parentId: 0,
|
|
|
|
|
+ path: '',
|
|
|
|
|
+ icon: '',
|
|
|
|
|
+ component: '',
|
|
|
|
|
+ componentName: '',
|
|
|
|
|
+ status: 0, // 0 是开启,1 是关闭
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ keepAlive: true,
|
|
|
|
|
+ alwaysShow: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否是外部链接
|
|
|
|
|
+ const isExternal = (path: string) => {
|
|
|
|
|
+ return /^(https?:|mailto:|tel:)/.test(path);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 提交表单
|
|
|
|
|
+ const submitForm = async () => {
|
|
|
|
|
+ // 验证必填字段
|
|
|
|
|
+ if (!formData.name) {
|
|
|
|
|
+ toast.error('菜单名称不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.type) {
|
|
|
|
|
+ toast.error('菜单类型不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (formData.sort === undefined || formData.sort === null) {
|
|
|
|
|
+ toast.error('菜单顺序不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (formData.type !== MenuType.BUTTON && !formData.path) {
|
|
|
|
|
+ toast.error('路由地址不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (formData.status === undefined || formData.status === null) {
|
|
|
|
|
+ toast.error('菜单状态不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证路径格式
|
|
|
|
|
+ if (formData.type === MenuType.DIR || formData.type === MenuType.MENU) {
|
|
|
|
|
+ if (!isExternal(formData.path)) {
|
|
|
|
|
+ if (formData.parentId === 0 && formData.path.charAt(0) !== '/') {
|
|
|
|
|
+ toast.error('路径必须以 / 开头');
|
|
|
|
|
+ return;
|
|
|
|
|
+ } else if (formData.parentId !== 0 && formData.path.charAt(0) === '/') {
|
|
|
|
|
+ toast.error('路径不能以 / 开头');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setFormLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (formType === 'create') {
|
|
|
|
|
+ await menuApi.createMenu(formData);
|
|
|
|
|
+ toast.success('创建成功');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await menuApi.updateMenu(formData);
|
|
|
|
|
+ toast.success('更新成功');
|
|
|
|
|
+ }
|
|
|
|
|
+ setDialogVisible(false);
|
|
|
|
|
+ onSuccess?.();
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ toast.error(error.message || '操作失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setFormLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前选中菜单的名称
|
|
|
|
|
+ const getSelectedMenuName = (): string => {
|
|
|
|
|
+ if (!formData.parentId || formData.parentId === 0) return '主类目';
|
|
|
|
|
+
|
|
|
|
|
+ const findMenuName = (nodes: MenuNode[], targetId: number): string | null => {
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ if (node.id === targetId) return node.name;
|
|
|
|
|
+ if (node.children) {
|
|
|
|
|
+ const found = findMenuName(node.children, targetId);
|
|
|
|
|
+ if (found) return found;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return findMenuName(menuTree, formData.parentId) || '主类目';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // ElementPlus 图标列表(显示 React 图标,保存 ElementPlus 格式)
|
|
|
|
|
+ const elementPlusIcons = [
|
|
|
|
|
+ { name: 'ep:document', label: '文档', icon: FileText },
|
|
|
|
|
+ { name: 'ep:folder', label: '文件夹', icon: Folder },
|
|
|
|
|
+ { name: 'ep:menu', label: '菜单', icon: Menu },
|
|
|
|
|
+ { name: 'ep:setting', label: '设置', icon: Settings },
|
|
|
|
|
+ { name: 'ep:user', label: '用户', icon: User },
|
|
|
|
|
+ { name: 'ep:users', label: '用户组', icon: Users },
|
|
|
|
|
+ { name: 'ep:lock', label: '锁定', icon: Lock },
|
|
|
|
|
+ { name: 'ep:unlock', label: '解锁', icon: Unlock },
|
|
|
|
|
+ { name: 'ep:search', label: '搜索', icon: Search },
|
|
|
|
|
+ { name: 'ep:edit', label: '编辑', icon: Edit },
|
|
|
|
|
+ { name: 'ep:delete', label: '删除', icon: Trash2 },
|
|
|
|
|
+ { name: 'ep:plus', label: '添加', icon: Plus },
|
|
|
|
|
+ { name: 'ep:minus', label: '减少', icon: Minus },
|
|
|
|
|
+ { name: 'ep:check', label: '确认', icon: CheckCircle },
|
|
|
|
|
+ { name: 'ep:close', label: '关闭', icon: X },
|
|
|
|
|
+ { name: 'ep:warning', label: '警告', icon: AlertTriangle },
|
|
|
|
|
+ { name: 'ep:info', label: '信息', icon: Info },
|
|
|
|
|
+ { name: 'ep:success', label: '成功', icon: CheckCircle },
|
|
|
|
|
+ { name: 'ep:error', label: '错误', icon: X },
|
|
|
|
|
+ { name: 'ep:bell', label: '通知', icon: Bell },
|
|
|
|
|
+ { name: 'ep:message', label: '消息', icon: MessageSquare },
|
|
|
|
|
+ { name: 'ep:phone', label: '电话', icon: Phone },
|
|
|
|
|
+ { name: 'ep:location', label: '位置', icon: MapPin },
|
|
|
|
|
+ { name: 'ep:calendar', label: '日历', icon: Calendar },
|
|
|
|
|
+ { name: 'ep:clock', label: '时钟', icon: Clock },
|
|
|
|
|
+ { name: 'ep:star', label: '星标', icon: Star },
|
|
|
|
|
+ { name: 'ep:share', label: '分享', icon: Share2 },
|
|
|
|
|
+ { name: 'ep:download', label: '下载', icon: Download },
|
|
|
|
|
+ { name: 'ep:upload', label: '上传', icon: Upload },
|
|
|
|
|
+ { name: 'ep:refresh', label: '刷新', icon: RefreshCw },
|
|
|
|
|
+ { name: 'ep:view', label: '查看', icon: Eye },
|
|
|
|
|
+ { name: 'ep:hide', label: '隐藏', icon: EyeOff },
|
|
|
|
|
+ { name: 'ep:home', label: '首页', icon: Home },
|
|
|
|
|
+ { name: 'ep:shop', label: '商店', icon: ShoppingBag },
|
|
|
|
|
+ { name: 'ep:goods', label: '商品', icon: Package },
|
|
|
|
|
+ { name: 'ep:data', label: '数据', icon: Database },
|
|
|
|
|
+ { name: 'ep:pie-chart', label: '饼图', icon: PieChart },
|
|
|
|
|
+ { name: 'ep:bar-chart', label: '柱状图', icon: BarChart3 },
|
|
|
|
|
+ { name: 'ep:grid', label: '网格', icon: Grid3x3 },
|
|
|
|
|
+ { name: 'ep:list', label: '列表', icon: List },
|
|
|
|
|
+ { name: 'ep:connection', label: '连接', icon: Link2 },
|
|
|
|
|
+ { name: 'ep:link', label: '链接', icon: Link2 },
|
|
|
|
|
+ { name: 'ep:key', label: '钥匙', icon: Key },
|
|
|
|
|
+ { name: 'ep:tools', label: '工具', icon: Wrench },
|
|
|
|
|
+ { name: 'ep:monitor', label: '监控', icon: Monitor },
|
|
|
|
|
+ { name: 'ep:server', label: '服务器', icon: Server },
|
|
|
|
|
+ { name: 'ep:database', label: '数据库', icon: Database },
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 递归渲染菜单树选项(自定义组件)
|
|
|
|
|
+ const renderMenuTreeOptions = (nodes: MenuNode[], level: number = 0, onSelect: (id: number) => void): React.ReactNode => {
|
|
|
|
|
+ return nodes.map(node => {
|
|
|
|
|
+ if (node.id === undefined || node.id === null) return null;
|
|
|
|
|
+ const idValue = node.id;
|
|
|
|
|
+ if (idValue === undefined) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const indentWidth = level * 20;
|
|
|
|
|
+ const hasChildren = node.children && node.children.length > 0;
|
|
|
|
|
+ const isSelected = formData.parentId === idValue;
|
|
|
|
|
+ const isDisabled = node.id === formData.id;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <React.Fragment key={node.id}>
|
|
|
|
|
+ <div
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ if (!isDisabled) {
|
|
|
|
|
+ onSelect(idValue);
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ className={`
|
|
|
|
|
+ flex items-center justify-between px-3 py-2 text-sm cursor-pointer
|
|
|
|
|
+ transition-colors select-none
|
|
|
|
|
+ ${isDisabled
|
|
|
|
|
+ ? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
+ : 'hover:bg-blue-50 active:bg-blue-100'
|
|
|
|
|
+ }
|
|
|
|
|
+ ${isSelected
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-600 font-semibold'
|
|
|
|
|
+ : 'text-gray-700'
|
|
|
|
|
+ }
|
|
|
|
|
+ `}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ paddingLeft: `${12 + indentWidth}px`,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 文本区域:左侧层级线 + 菜单名 */}
|
|
|
|
|
+ <div className="flex items-center gap-1.5 flex-1 min-w-0">
|
|
|
|
|
+ {level > 0 && (
|
|
|
|
|
+ <span className="flex-shrink-0 text-gray-400 text-xs font-mono">│</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <span className="flex-1 min-w-0 truncate">{node.name}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* 自定义选中标记 */}
|
|
|
|
|
+ {isSelected && !isDisabled && (
|
|
|
|
|
+ <Check className="w-4 h-4 text-blue-600 ml-2 flex-shrink-0" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {hasChildren && node.children && renderMenuTreeOptions(node.children, level + 1, onSelect)}
|
|
|
|
|
+ </React.Fragment>
|
|
|
|
|
+ );
|
|
|
|
|
+ }).filter(Boolean);
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!dialogVisible) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999] animate-in fade-in duration-200 p-4">
|
|
|
|
|
+ <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl animate-in zoom-in duration-200 relative z-[10000] max-h-[85vh] flex flex-col overflow-hidden">
|
|
|
|
|
+ {/* 弹窗标题 */}
|
|
|
|
|
+ <div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
|
|
|
|
|
+ <h3 className="text-base text-gray-900">{dialogTitle}</h3>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
|
|
|
|
+ <Maximize2 className="w-4 h-4 text-gray-600" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setDialogVisible(false)}
|
|
|
|
|
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 弹窗内容 */}
|
|
|
|
|
+ <div className="px-5 py-3 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
|
|
|
|
|
+ {formLoading ? (
|
|
|
|
|
+ <div className="py-6 text-center text-gray-500">加载中...</div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-x-32 gap-y-2.5">
|
|
|
|
|
+ {/* 上级菜单 - 占满整行(优化版) */}
|
|
|
|
|
+<div className="col-span-2 grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
|
|
+ <label className="text-sm text-gray-700 whitespace-nowrap">上级菜单</label>
|
|
|
|
|
+ {/* 外层包裹确保下拉容器定位正确 */}
|
|
|
|
|
+ <div className="relative w-full">
|
|
|
|
|
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
|
|
|
|
+ <PopoverTrigger asChild>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ disabled={!menuTree || menuTree.length === 0}
|
|
|
|
|
+ className="w-full h-[36px] bg-white border border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100
|
|
|
|
|
+ rounded-md transition-all outline-none px-3 text-left flex items-center justify-between
|
|
|
|
|
+ disabled:opacity-50 disabled:cursor-not-allowed hover:border-gray-300"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className={formData.parentId === 0 ? 'text-gray-500' : 'text-gray-900'}>
|
|
|
|
|
+ {getSelectedMenuName()}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <ChevronDown className={`w-4 h-4 text-gray-400 transition-transform ${popoverOpen ? 'rotate-180' : ''}`} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </PopoverTrigger>
|
|
|
|
|
+ <PopoverContent
|
|
|
|
|
+ className="p-0 border-gray-200 shadow-md bg-white rounded-md"
|
|
|
|
|
+ align="start"
|
|
|
|
|
+ side="bottom"
|
|
|
|
|
+ sideOffset={4}
|
|
|
|
|
+ style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="max-h-[200px] overflow-y-auto"
|
|
|
|
|
+ style={{ maxHeight: '200px' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 空数据兜底 */}
|
|
|
|
|
+ {!menuTree || menuTree.length === 0 ? (
|
|
|
|
|
+ <div className="px-3 py-2 text-sm text-gray-400">暂无可选菜单</div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ renderMenuTreeOptions(menuTree, 0, (id: number) => {
|
|
|
|
|
+ setFormData(prev => ({ ...prev, parentId: id }));
|
|
|
|
|
+ setPopoverOpen(false);
|
|
|
|
|
+ })
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </PopoverContent>
|
|
|
|
|
+ </Popover>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 菜单名称 */}
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700">
|
|
|
|
|
+ <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
+ 菜单名称
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.name}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
|
|
|
+ placeholder="请输入菜单名称"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 菜单类型 */}
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3">
|
|
|
|
|
+ <label className="text-sm text-gray-700">
|
|
|
|
|
+ <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
+ 菜单类型
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex gap-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, type: MenuType.DIR }))}
|
|
|
|
|
+ className={`flex-1 px-3 py-1.5 text-xs border border-r-0 rounded-l-lg transition-all ${
|
|
|
|
|
+ formData.type === MenuType.DIR
|
|
|
|
|
+ ? 'bg-blue-500 text-white border-blue-500 z-10'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 目录
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, type: MenuType.MENU }))}
|
|
|
|
|
+ className={`flex-1 px-3 py-1.5 text-xs border transition-all ${
|
|
|
|
|
+ formData.type === MenuType.MENU
|
|
|
|
|
+ ? 'bg-blue-500 text-white border-blue-500 z-10'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 菜单
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, type: MenuType.BUTTON }))}
|
|
|
|
|
+ className={`flex-1 px-3 py-1.5 text-xs border border-l-0 rounded-r-lg transition-all ${
|
|
|
|
|
+ formData.type === MenuType.BUTTON
|
|
|
|
|
+ ? 'bg-blue-500 text-white border-blue-500 z-10'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 按钮
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 菜单图标 */}
|
|
|
|
|
+ {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700">菜单图标</label>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.icon}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, icon: e.target.value }))}
|
|
|
|
|
+ placeholder="请选择或输入图标名称(如:ep:document)"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100 flex-1"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Popover open={iconPickerOpen} onOpenChange={setIconPickerOpen}>
|
|
|
|
|
+ <PopoverTrigger asChild>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ className="px-3 h-[36px] bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm whitespace-nowrap"
|
|
|
|
|
+ >
|
|
|
|
|
+ 选择图标
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </PopoverTrigger>
|
|
|
|
|
+ <PopoverContent
|
|
|
|
|
+ className="w-[400px] p-3 border-gray-200 shadow-md bg-white rounded-md max-h-[85vh]"
|
|
|
|
|
+ align="end"
|
|
|
|
|
+ side="bottom"
|
|
|
|
|
+ sideOffset={4}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex flex-col">
|
|
|
|
|
+ <h4 className="text-sm font-semibold text-gray-700 mb-2">选择图标</h4>
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="grid gap-1.5 overflow-y-auto"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ gridTemplateColumns: 'repeat(8, 1fr)',
|
|
|
|
|
+ maxHeight: '300px',
|
|
|
|
|
+ height: '300px'
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {elementPlusIcons.map((iconItem) => {
|
|
|
|
|
+ const IconComponent = iconItem.icon;
|
|
|
|
|
+ const isSelected = formData.icon === iconItem.name;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={iconItem.name}
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setFormData(prev => ({ ...prev, icon: iconItem.name }));
|
|
|
|
|
+ setIconPickerOpen(false);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className={`
|
|
|
|
|
+ flex items-center justify-center p-1.5 rounded cursor-pointer
|
|
|
|
|
+ transition-all border
|
|
|
|
|
+ ${isSelected
|
|
|
|
|
+ ? 'bg-blue-50 border-blue-500 shadow-sm'
|
|
|
|
|
+ : 'bg-white border-gray-200 hover:bg-gray-50 hover:border-blue-300'
|
|
|
|
|
+ }
|
|
|
|
|
+ `}
|
|
|
|
|
+ style={{ aspectRatio: '1' }}
|
|
|
|
|
+ title={`${iconItem.name} - ${iconItem.label}`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <IconComponent
|
|
|
|
|
+ className={`
|
|
|
|
|
+ w-3.5 h-3.5
|
|
|
|
|
+ ${isSelected ? 'text-blue-600' : 'text-gray-600'}
|
|
|
|
|
+ `}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </PopoverContent>
|
|
|
|
|
+ </Popover>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 路由地址 */}
|
|
|
|
|
+ {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3">
|
|
|
|
|
+ <label className="text-sm text-gray-700 flex items-center gap-1">
|
|
|
|
|
+ <span className="text-red-500">*</span>
|
|
|
|
|
+ 路由地址
|
|
|
|
|
+ <HelpCircle className="w-3.5 h-3.5 text-gray-400" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.path}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, path: e.target.value }))}
|
|
|
|
|
+ placeholder="请输入路由地址"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 组件地址 */}
|
|
|
|
|
+ {formData.type === MenuType.MENU && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700">组件地址</label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.component}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, component: e.target.value }))}
|
|
|
|
|
+ placeholder="例如:system/user/index"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 组件名字 */}
|
|
|
|
|
+ {formData.type === MenuType.MENU && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3">
|
|
|
|
|
+ <label className="text-sm text-gray-700">组件名字</label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.componentName || ''}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, componentName: e.target.value }))}
|
|
|
|
|
+ placeholder="例如:SystemUser"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 权限标识 */}
|
|
|
|
|
+ {formData.type !== MenuType.DIR && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700 flex items-center gap-1">
|
|
|
|
|
+ 权限标识
|
|
|
|
|
+ <HelpCircle className="w-3.5 h-3.5 text-gray-400" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ value={formData.permission}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, permission: e.target.value }))}
|
|
|
|
|
+ placeholder="请输入权限标识"
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 显示排序 */}
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700">
|
|
|
|
|
+ <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
+ 显示排序
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ value={formData.sort}
|
|
|
|
|
+ onChange={(e) => setFormData(prev => ({ ...prev, sort: Number(e.target.value) || 0 }))}
|
|
|
|
|
+ placeholder="请输入排序号"
|
|
|
|
|
+ min={0}
|
|
|
|
|
+ className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 菜单状态 */}
|
|
|
|
|
+ <div className="col-span-4 grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700">
|
|
|
|
|
+ <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
+ 菜单状态
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex gap-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, status: 0 }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border rounded-l-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ Number(formData.status) === 0
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ Number(formData.status) === 0 ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {Number(formData.status) === 0 && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 开启
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, status: 1 }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border border-l-0 rounded-r-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ Number(formData.status) === 1
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ Number(formData.status) === 1 ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {Number(formData.status) === 1 && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 关闭
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 显示状态 */}
|
|
|
|
|
+ {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700 flex items-center gap-1">
|
|
|
|
|
+ 显示状态
|
|
|
|
|
+ <HelpCircle className="w-3.5 h-3.5 text-gray-400" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex gap-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, visible: true }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border rounded-l-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ formData.visible === true
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ formData.visible === true ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {formData.visible === true && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 显示
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, visible: false }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border border-l-0 rounded-r-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ formData.visible === false
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ formData.visible === false ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {formData.visible === false && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 隐藏
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 总是显示 */}
|
|
|
|
|
+ {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
+ <div className="grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700 flex items-center gap-1">
|
|
|
|
|
+ 总是显示
|
|
|
|
|
+ <HelpCircle className="w-3.5 h-3.5 text-gray-400" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex gap-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, alwaysShow: true }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border rounded-l-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ (formData.alwaysShow ?? true) === true
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ (formData.alwaysShow ?? true) === true ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {(formData.alwaysShow ?? true) === true && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 总是
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, alwaysShow: false }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border border-l-0 rounded-r-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ (formData.alwaysShow ?? true) === false
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ (formData.alwaysShow ?? true) === false ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {(formData.alwaysShow ?? true) === false && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 不是
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 缓存状态 */}
|
|
|
|
|
+ {formData.type === MenuType.MENU && (
|
|
|
|
|
+ <div className="col-span-4 grid grid-cols-[100px_1fr] items-center gap-3" style={{ marginRight: '30px' }}>
|
|
|
|
|
+ <label className="text-sm text-gray-700 flex items-center gap-1">
|
|
|
|
|
+ 缓存状态
|
|
|
|
|
+ <HelpCircle className="w-3.5 h-3.5 text-gray-400" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex gap-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, keepAlive: true }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border rounded-l-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ (formData.keepAlive ?? true) === true
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ (formData.keepAlive ?? true) === true ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {(formData.keepAlive ?? true) === true && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 缓存
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setFormData(prev => ({ ...prev, keepAlive: false }))}
|
|
|
|
|
+ className={`flex-1 px-4 py-1.5 text-xs border border-l-0 rounded-r-lg transition-all flex items-center justify-center gap-1.5 ${
|
|
|
|
|
+ (formData.keepAlive ?? true) === false
|
|
|
|
|
+ ? 'bg-blue-50 text-blue-700 border-blue-500'
|
|
|
|
|
+ : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className={`w-3 h-3 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
+ (formData.keepAlive ?? true) === false ? 'border-blue-500 bg-blue-500' : 'border-gray-300 bg-white'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {(formData.keepAlive ?? true) === false && (
|
|
|
|
|
+ <div className="w-1.5 h-1.5 rounded-full bg-white"></div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 不缓存
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 弹窗底部 */}
|
|
|
|
|
+ <div className="px-5 py-3 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-2 rounded-b-xl shrink-0">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setDialogVisible(false)}
|
|
|
|
|
+ className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={submitForm}
|
|
|
|
|
+ disabled={formLoading}
|
|
|
|
|
+ className="px-4 py-2 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg hover:shadow-lg hover:shadow-blue-400/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
+ >
|
|
|
|
|
+ {formLoading ? '提交中...' : '确定'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+MenuForm.displayName = 'MenuForm';
|
|
|
|
|
+
|
|
|
|
|
+export default MenuForm;
|
|
|
|
|
+
|