ops-console v1.0.2 (versionCode 8) 源码入库

This commit is contained in:
txh
2026-09-03 18:01:05 +08:00
commit 685fff27c8
170 changed files with 53049 additions and 0 deletions

View File

@@ -0,0 +1,511 @@
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<MenuPage> createState() => _MenuPageState();
}
class _MenuPageState extends ConsumerState<MenuPage> {
final SystemService _service = SystemService();
List<SysMenu> _items = [];
bool _loading = false;
String? _error;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _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<void> _refresh() async {
await _loadData();
}
/// 构建菜单树:parentId 为空或 "0" 的为根节点。
List<_MenuNode> _buildTree(List<SysMenu> menus) {
final nodeMap = <String, _MenuNode>{};
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<void> _showForm({SysMenu? menu}) async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
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<void> _deleteMenu(SysMenu menu) async {
final confirmed = await showDialog<bool>(
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<String>(
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<String>(
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<SysMenu> allMenus;
@override
State<_MenuForm> createState() => _MenuFormState();
}
class _MenuFormState extends State<_MenuForm> {
final _formKey = GlobalKey<FormState>();
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 = <String, dynamic>{
'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<String>(
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<int>(
title: const Text('目录'),
value: 0,
groupValue: _type,
onChanged: (v) => setState(() => _type = v ?? 0),
contentPadding: EdgeInsets.zero,
),
),
Expanded(
child: RadioListTile<int>(
title: const Text('菜单'),
value: 1,
groupValue: _type,
onChanged: (v) => setState(() => _type = v ?? 1),
contentPadding: EdgeInsets.zero,
),
),
Expanded(
child: RadioListTile<int>(
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('保存'),
),
],
),
],
),
),
),
);
}
}