import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/sys_menu.dart'; import '../../../services/api_client.dart'; import '../../../services/sys_service.dart'; /// 菜单管理页(树形展示)。 class MenuPage extends ConsumerStatefulWidget { const MenuPage({super.key}); @override ConsumerState createState() => _MenuPageState(); } class _MenuPageState extends ConsumerState { final SystemService _service = SystemService(); List _items = []; bool _loading = false; String? _error; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { _items = await _service.getMenuList(); } on ApiException catch (e) { _error = e.message; } catch (e) { _error = e.toString(); } finally { if (mounted) setState(() => _loading = false); } } Future _refresh() async { await _loadData(); } /// 构建菜单树:parentId 为空或 "0" 的为根节点。 List<_MenuNode> _buildTree(List menus) { final nodeMap = {}; for (final menu in menus) { nodeMap[menu.menuId] = _MenuNode(menu: menu); } final roots = <_MenuNode>[]; for (final menu in menus) { final node = nodeMap[menu.menuId]!; final parentId = menu.parentId.isEmpty || menu.parentId == '0' ? null : menu.parentId; if (parentId == null || !nodeMap.containsKey(parentId)) { roots.add(node); } else { nodeMap[parentId]!.children.add(node); } } // 排序 void sortNodes(List<_MenuNode> nodes) { nodes.sort((a, b) => a.menu.orderNum.compareTo(b.menu.orderNum)); for (final node in nodes) { sortNodes(node.children); } } sortNodes(roots); return roots; } Future _showForm({SysMenu? menu}) async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, builder: (context) => _MenuForm(menu: menu, allMenus: _items), ); if (result == null) return; try { await _service.saveMenu(result); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('保存成功')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存失败:$e')), ); } } } Future _deleteMenu(SysMenu menu) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: Text('确定要删除菜单「${menu.name}」吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text('删除'), ), ], ), ); if (confirmed != true) return; try { await _service.deleteMenu(menu.menuId); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('删除成功')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('删除失败:$e')), ); } } } String _typeLabel(int type) { switch (type) { case 0: return '目录'; case 1: return '菜单'; case 2: return '按钮'; default: return '未知'; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('菜单管理')), body: _buildBody(), floatingActionButton: FloatingActionButton( onPressed: () => _showForm(), child: const Icon(Icons.add), ), ); } Widget _buildBody() { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!), const SizedBox(height: 8), FilledButton.tonal( onPressed: _refresh, child: const Text('重试'), ), ], ), ); } if (_items.isEmpty) { return const Center(child: Text('暂无数据')); } final tree = _buildTree(_items); return RefreshIndicator( onRefresh: _refresh, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemCount: tree.length, itemBuilder: (context, index) { return _buildTreeNode(tree[index]); }, ), ); } Widget _buildTreeNode(_MenuNode node) { final menu = node.menu; final hasChildren = node.children.isNotEmpty; if (hasChildren) { return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ExpansionTile( leading: Icon(_iconData(menu.icon)), title: Text(menu.name), subtitle: Text( '${_typeLabel(menu.type)} | ${menu.url.isEmpty ? "无路由" : menu.url}', ), trailing: PopupMenuButton( onSelected: (value) { if (value == 'edit') { _showForm(menu: menu); } else if (value == 'delete') { _deleteMenu(menu); } }, itemBuilder: (context) => [ const PopupMenuItem(value: 'edit', child: Text('编辑')), const PopupMenuItem(value: 'delete', child: Text('删除')), ], ), children: node.children.map((child) => _buildTreeNode(child)).toList(), ), ); } return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ListTile( leading: Icon(_iconData(menu.icon)), title: Text(menu.name), subtitle: Text( '${_typeLabel(menu.type)} | ${menu.url.isEmpty ? "无路由" : menu.url}', ), trailing: PopupMenuButton( onSelected: (value) { if (value == 'edit') { _showForm(menu: menu); } else if (value == 'delete') { _deleteMenu(menu); } }, itemBuilder: (context) => [ const PopupMenuItem(value: 'edit', child: Text('编辑')), const PopupMenuItem(value: 'delete', child: Text('删除')), ], ), ), ); } IconData _iconData(String icon) { switch (icon) { case 'home': return Icons.home; case 'settings': return Icons.settings; case 'person': return Icons.person; case 'people': return Icons.people; case 'menu': return Icons.menu; case 'list': return Icons.list; case 'dashboard': return Icons.dashboard; case 'devices': return Icons.devices; case 'notifications': return Icons.notifications; default: return Icons.folder; } } } /// 菜单树节点辅助类。 class _MenuNode { _MenuNode({required this.menu}); final SysMenu menu; final List<_MenuNode> children = []; } /// 菜单表单。 class _MenuForm extends StatefulWidget { const _MenuForm({this.menu, this.allMenus = const []}); final SysMenu? menu; final List allMenus; @override State<_MenuForm> createState() => _MenuFormState(); } class _MenuFormState extends State<_MenuForm> { final _formKey = GlobalKey(); late TextEditingController _nameCtrl; late TextEditingController _urlCtrl; late TextEditingController _iconCtrl; late TextEditingController _orderNumCtrl; late TextEditingController _permsCtrl; late String _parentId; late int _type; late bool _show; @override void initState() { super.initState(); final m = widget.menu; _nameCtrl = TextEditingController(text: m?.name ?? ''); _urlCtrl = TextEditingController(text: m?.url ?? ''); _iconCtrl = TextEditingController(text: m?.icon ?? ''); _orderNumCtrl = TextEditingController(text: (m?.orderNum ?? 0).toString()); _permsCtrl = TextEditingController(text: m?.perms ?? ''); _parentId = m?.parentId ?? '0'; _type = m?.type ?? 0; _show = (m?.show ?? 0) == 0; } @override void dispose() { _nameCtrl.dispose(); _urlCtrl.dispose(); _iconCtrl.dispose(); _orderNumCtrl.dispose(); _permsCtrl.dispose(); super.dispose(); } void _submit() { if (!_formKey.currentState!.validate()) return; final data = { 'name': _nameCtrl.text.trim(), 'parentId': _parentId, 'type': _type, 'url': _urlCtrl.text.trim(), 'icon': _iconCtrl.text.trim(), 'orderNum': int.tryParse(_orderNumCtrl.text.trim()) ?? 0, 'perms': _permsCtrl.text.trim(), 'show': _show ? 0 : 1, }; if (widget.menu != null) { data['menuId'] = widget.menu!.menuId; } Navigator.pop(context, data); } @override Widget build(BuildContext context) { final isEdit = widget.menu != null; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( isEdit ? '编辑菜单' : '新增菜单', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), TextFormField( controller: _nameCtrl, decoration: const InputDecoration( labelText: '菜单名称', border: OutlineInputBorder(), ), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入菜单名称' : null, ), const SizedBox(height: 12), DropdownButtonFormField( value: _parentId, decoration: const InputDecoration( labelText: '父级菜单', border: OutlineInputBorder(), ), items: [ const DropdownMenuItem(value: '0', child: Text('一级菜单')), ...widget.allMenus.map((m) => DropdownMenuItem( value: m.menuId, child: Text(m.name), )), ], onChanged: (v) => setState(() => _parentId = v ?? '0'), ), const SizedBox(height: 12), Row( children: [ Expanded( child: RadioListTile( title: const Text('目录'), value: 0, groupValue: _type, onChanged: (v) => setState(() => _type = v ?? 0), contentPadding: EdgeInsets.zero, ), ), Expanded( child: RadioListTile( title: const Text('菜单'), value: 1, groupValue: _type, onChanged: (v) => setState(() => _type = v ?? 1), contentPadding: EdgeInsets.zero, ), ), Expanded( child: RadioListTile( title: const Text('按钮'), value: 2, groupValue: _type, onChanged: (v) => setState(() => _type = v ?? 2), contentPadding: EdgeInsets.zero, ), ), ], ), const SizedBox(height: 12), TextFormField( controller: _urlCtrl, decoration: const InputDecoration( labelText: '路由地址', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: _iconCtrl, decoration: const InputDecoration( labelText: '图标', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: _orderNumCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '排序号', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: _permsCtrl, decoration: const InputDecoration( labelText: '权限标识', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), SwitchListTile( title: const Text('是否显示'), value: _show, onChanged: (v) => setState(() => _show = v), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), const SizedBox(width: 8), FilledButton( onPressed: _submit, child: const Text('保存'), ), ], ), ], ), ), ), ); } }