Files
CloudHome-Ops/lib/screens/store/org_tree_page.dart

555 lines
16 KiB
Dart
Raw Normal View History

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<OrgTreePage> createState() => _OrgTreePageState();
}
class _OrgTreePageState extends ConsumerState<OrgTreePage> {
final OrgService _orgService = OrgService();
List<OrgNode> _tree = [];
bool _loading = false;
String? _error;
OrgNode? _selectedNode;
/// 受限账号(带 orgId 的渠道/代理商等)仅有只读查看权限,隐藏新增/删除等写操作入口。
bool get _canEdit => !SessionOrgScope.instance.enabled;
@override
void initState() {
super.initState();
_loadTree();
}
Future<void> _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<void> _deleteNode(OrgNode node) async {
final confirmed = await showDialog<bool>(
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<String>(
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<FormState>();
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<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
try {
final data = <String, dynamic>{
'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<int>(
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;
}