import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/org_node.dart'; import '../../services/org_service.dart'; import '../../utils/session_org_scope.dart'; /// 组织架构树页:递归树展示、颜色编码、新增/删除/详情。 class OrgTreePage extends ConsumerStatefulWidget { const OrgTreePage({super.key}); @override ConsumerState createState() => _OrgTreePageState(); } class _OrgTreePageState extends ConsumerState { final OrgService _orgService = OrgService(); List _tree = []; bool _loading = false; String? _error; OrgNode? _selectedNode; /// 受限账号(带 orgId 的渠道/代理商等)仅有只读查看权限,隐藏新增/删除等写操作入口。 bool get _canEdit => !SessionOrgScope.instance.enabled; @override void initState() { super.initState(); _loadTree(); } Future _loadTree() async { setState(() { _loading = true; _error = null; }); try { final tree = await _orgService.getOrgTree(); if (!mounted) return; setState(() { _tree = tree; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = '$e'; _loading = false; }); } } Future _deleteNode(OrgNode node) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('删除组织'), content: Text('确定要删除「${node.name}」吗?子节点也会一并删除。'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('删除'), ), ], ), ); if (confirmed != true) return; try { await _orgService.deleteOrg(node.id); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('组织已删除')), ); _loadTree(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('删除失败:$e')), ); } } void _showAddSheet({OrgNode? parent}) { showModalBottomSheet( useSafeArea: true, context: context, isScrollControlled: true, builder: (ctx) => _OrgFormSheet( orgService: _orgService, parentId: parent?.id, parentName: parent?.name, onSaved: () { Navigator.pop(ctx); _loadTree(); }, ), ); } void _showDetailSheet(OrgNode node) { showModalBottomSheet( useSafeArea: true, context: context, isScrollControlled: true, builder: (ctx) => _OrgDetailSheet( node: node, canEdit: _canEdit, onAddChild: () { Navigator.pop(ctx); _showAddSheet(parent: node); }, onDelete: () { Navigator.pop(ctx); _deleteNode(node); }, ), ); } @override Widget build(BuildContext context) { final canEdit = _canEdit; return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('组织架构')), body: _buildBody(), floatingActionButton: canEdit ? FloatingActionButton( onPressed: () => _showAddSheet(parent: _selectedNode), child: const Icon(Icons.add), ) : null, ); } Widget _buildBody() { final canEdit = _canEdit; 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: _loadTree, child: const Text('重试'), ), ], ), ); } if (_tree.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.account_tree_outlined, size: 64, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 16), const Text('暂无组织数据'), const SizedBox(height: 8), if (canEdit) const Text('点击 + 添加组织'), ], ), ); } return RefreshIndicator( onRefresh: _loadTree, child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), physics: const AlwaysScrollableScrollPhysics(), children: _tree.map((node) => _buildTreeNode(node, 0)).toList(), ), ); } Widget _buildTreeNode(OrgNode node, int depth) { final hasChildren = node.children.isNotEmpty; final typeInfo = _orgTypeInfo(node.type); final canEdit = _canEdit; if (hasChildren) { return ExpansionTile( key: ValueKey('org-${node.id}'), leading: _typeDot(typeInfo.color), title: _nodeTitle(node, typeInfo), subtitle: Text('${node.children.length} 个子节点', style: const TextStyle(fontSize: 12)), children: [ ...node.children.map((child) => _buildTreeNode(child, depth + 1)), if (canEdit) Padding( padding: const EdgeInsets.only(left: 56, top: 4, bottom: 4), child: Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: () => _showAddSheet(parent: node), icon: const Icon(Icons.add, size: 18), label: const Text('添加子节点'), ), ), ), ], ); } return ListTile( key: ValueKey('org-${node.id}'), leading: _typeDot(typeInfo.color), title: _nodeTitle(node, typeInfo), onLongPress: () => _selectNode(node), onTap: () => _showDetailSheet(node), trailing: PopupMenuButton( onSelected: (value) { if (value == 'add') { _showAddSheet(parent: node); } else if (value == 'detail') { _showDetailSheet(node); } else if (value == 'delete') { _deleteNode(node); } }, itemBuilder: (ctx) => [ if (canEdit) const PopupMenuItem(value: 'add', child: Text('添加子节点')), const PopupMenuItem(value: 'detail', child: Text('查看详情')), if (canEdit) const PopupMenuItem(value: 'delete', child: Text('删除')), ], ), ); } void _selectNode(OrgNode node) { setState(() => _selectedNode = node); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已选择「${node.name}」为父节点,点击 + 添加子组织')), ); } Widget _nodeTitle(OrgNode node, _TypeInfo info) { return Row( children: [ Expanded( child: Text( node.name, style: TextStyle(fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: info.color.withOpacity(0.15), borderRadius: BorderRadius.circular(999), border: Border.all(color: info.color.withOpacity(0.6)), ), child: Text( info.label, style: TextStyle( color: info.color, fontSize: 11, fontWeight: FontWeight.w600, ), ), ), ], ); } Widget _typeDot(Color color) { return Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ); } _TypeInfo _orgTypeInfo(int type) { switch (type) { case 0: return const _TypeInfo('平台', Colors.purple); case 1: return const _TypeInfo('渠道', Color(0xFF0B6397)); // 语义信息蓝 case 2: return const _TypeInfo('代理商', Colors.orange); case 3: return const _TypeInfo('门店', Colors.green); case 4: return const _TypeInfo('设备分组', Colors.grey); default: return const _TypeInfo('未知', Colors.grey); } } } class _TypeInfo { const _TypeInfo(this.label, this.color); final String label; final Color color; } /// 组织详情底部弹窗。 class _OrgDetailSheet extends StatelessWidget { const _OrgDetailSheet({ required this.node, required this.onAddChild, required this.onDelete, this.canEdit = true, }); final OrgNode node; final VoidCallback onAddChild; final VoidCallback onDelete; final bool canEdit; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( node.name, style: theme.textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.bold), ), const Divider(height: 24), _row(theme, '节点ID', node.id), _row(theme, '父级ID', node.parentId), _row(theme, '地址', node.address), _row(theme, '联系电话', node.contactPhone), _row(theme, '激活码', node.activationCode), const SizedBox(height: 20), if (canEdit) Row( children: [ Expanded( child: FilledButton.tonal( onPressed: onAddChild, child: const Text('添加子节点'), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton( onPressed: onDelete, style: OutlinedButton.styleFrom( foregroundColor: Colors.red, ), child: const Text('删除'), ), ), ], ), ], ), ), ); } Widget _row(ThemeData theme, String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 80, child: Text( label, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), ), Expanded( child: Text(value.isEmpty ? '—' : value), ), ], ), ); } } /// 新增组织表单。 class _OrgFormSheet extends StatefulWidget { const _OrgFormSheet({ required this.orgService, this.parentId, this.parentName, required this.onSaved, }); final OrgService orgService; final String? parentId; final String? parentName; final VoidCallback onSaved; @override State<_OrgFormSheet> createState() => _OrgFormSheetState(); } class _OrgFormSheetState extends State<_OrgFormSheet> { final _formKey = GlobalKey(); final _nameController = TextEditingController(); int _type = 1; bool _saving = false; static const _typeOptions = [ _TypeOption(0, '平台'), _TypeOption(1, '渠道'), _TypeOption(2, '代理商'), _TypeOption(3, '门店'), _TypeOption(4, '设备分组'), ]; @override void dispose() { _nameController.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { final data = { 'name': _nameController.text.trim(), 'type': _type, 'parentId': widget.parentId ?? '', }; await widget.orgService.saveOrUpdateOrg(data); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('组织已创建')), ); widget.onSaved(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存失败:$e')), ); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( padding: const EdgeInsets.all(20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '新增组织', style: Theme.of(context) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.bold), ), if (widget.parentName != null) ...[ const SizedBox(height: 8), Text( '父级组织:${widget.parentName}', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), ], const SizedBox(height: 20), TextFormField( controller: _nameController, decoration: const InputDecoration( labelText: '组织名称', border: OutlineInputBorder(), prefixIcon: Icon(Icons.label), ), validator: (v) => v == null || v.trim().isEmpty ? '请输入组织名称' : null, ), const SizedBox(height: 12), DropdownButtonFormField( value: _type, decoration: const InputDecoration( labelText: '组织类型', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category), ), items: _typeOptions .map((opt) => DropdownMenuItem( value: opt.type, child: Text(opt.label), )) .toList(), onChanged: (v) { if (v != null) setState(() => _type = v); }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('创建'), ), ), ], ), ), ), ); } } class _TypeOption { const _TypeOption(this.type, this.label); final int type; final String label; }