knowledge_tree.dart 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import '../providers/app_provider.dart';
  4. import '../models/knowledge.dart';
  5. class KnowledgeTreeWidget extends ConsumerStatefulWidget {
  6. final Function(String? code, String? name) onSelectNode;
  7. const KnowledgeTreeWidget({super.key, required this.onSelectNode});
  8. @override
  9. ConsumerState<KnowledgeTreeWidget> createState() => _KnowledgeTreeWidgetState();
  10. }
  11. class _KnowledgeTreeWidgetState extends ConsumerState<KnowledgeTreeWidget> {
  12. List<KnowledgeNode> _tree = [];
  13. String? _selectedCode;
  14. bool _isLoading = true;
  15. @override
  16. void initState() {
  17. super.initState();
  18. _load();
  19. }
  20. Future<void> _load() async {
  21. final api = ref.read(apiServiceProvider);
  22. try {
  23. final data = await api.knowledgeTree();
  24. setState(() {
  25. _tree = (data['tree'] as List?)
  26. ?.map((j) => KnowledgeNode.fromJson(j))
  27. .toList() ?? [];
  28. _isLoading = false;
  29. });
  30. } catch (_) {
  31. setState(() => _isLoading = false);
  32. }
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. return Column(
  37. children: [
  38. Expanded(
  39. child: _isLoading
  40. ? const Center(child: CircularProgressIndicator())
  41. : ListView(
  42. children: [
  43. ListTile(
  44. leading: const Icon(Icons.select_all, size: 18),
  45. title: const Text('全部题目'),
  46. selected: _selectedCode == null,
  47. onTap: () {
  48. setState(() => _selectedCode = null);
  49. widget.onSelectNode(null, null);
  50. },
  51. ),
  52. ..._tree.map((node) => _buildNode(node, 0)),
  53. ],
  54. ),
  55. ),
  56. ],
  57. );
  58. }
  59. Widget _buildNode(KnowledgeNode node, int depth) {
  60. if (node.children.isEmpty) {
  61. // Leaf node: click to filter
  62. return ListTile(
  63. contentPadding: EdgeInsets.only(left: 16.0 + depth * 16.0),
  64. dense: true,
  65. leading: const Icon(Icons.circle, size: 6),
  66. title: Text(node.name, style: const TextStyle(fontSize: 13)),
  67. selected: _selectedCode == node.code,
  68. onTap: () {
  69. setState(() => _selectedCode = node.code);
  70. widget.onSelectNode(node.code, node.name);
  71. },
  72. );
  73. }
  74. // Branch node: expand to browse, click filter icon to filter
  75. return ExpansionTile(
  76. tilePadding: EdgeInsets.only(left: depth * 16.0),
  77. leading: GestureDetector(
  78. onTap: () {
  79. setState(() => _selectedCode = node.code);
  80. widget.onSelectNode(node.code, node.name);
  81. },
  82. child: Icon(
  83. Icons.filter_alt_outlined,
  84. size: 16,
  85. color: _selectedCode == node.code
  86. ? Theme.of(context).colorScheme.primary
  87. : Colors.grey,
  88. ),
  89. ),
  90. title: Text(node.name,
  91. style: TextStyle(
  92. fontSize: 13,
  93. fontWeight: FontWeight.w500,
  94. color: _selectedCode == node.code
  95. ? Theme.of(context).colorScheme.primary
  96. : null,
  97. )),
  98. initiallyExpanded: false,
  99. children: node.children.map((c) => _buildNode(c, depth + 1)).toList(),
  100. );
  101. }
  102. }