knowledge_tree.dart 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import '../services/api_service.dart';
  4. import '../providers/app_provider.dart';
  5. import '../models/knowledge.dart';
  6. /// 知识点树 Widget — 左侧导航
  7. class KnowledgeTreeWidget extends ConsumerStatefulWidget {
  8. final void Function(String? chapterFilter, String? displayName) onSelectChapter;
  9. const KnowledgeTreeWidget({super.key, required this.onSelectChapter});
  10. @override
  11. ConsumerState<KnowledgeTreeWidget> createState() => _KnowledgeTreeWidgetState();
  12. }
  13. class _KnowledgeTreeWidgetState extends ConsumerState<KnowledgeTreeWidget> {
  14. List<KnowledgeNode> _tree = [];
  15. String? _selectedCode;
  16. bool _isLoading = true;
  17. @override
  18. void initState() {
  19. super.initState();
  20. _load();
  21. }
  22. Future<void> _load() async {
  23. final api = ref.read(apiServiceProvider);
  24. try {
  25. final data = await api.knowledgeTree();
  26. setState(() {
  27. _tree = (data['tree'] as List?)
  28. ?.map((j) => KnowledgeNode.fromJson(j))
  29. .toList() ??
  30. [];
  31. _isLoading = false;
  32. });
  33. } catch (_) {
  34. setState(() => _isLoading = false);
  35. }
  36. }
  37. @override
  38. Widget build(BuildContext context) {
  39. return Column(
  40. children: [
  41. // Search
  42. Padding(
  43. padding: const EdgeInsets.all(8),
  44. child: TextField(
  45. decoration: InputDecoration(
  46. hintText: '搜索知识点...',
  47. prefixIcon: const Icon(Icons.search, size: 20),
  48. contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
  49. border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
  50. isDense: true,
  51. ),
  52. onChanged: (v) {
  53. // TODO: filter tree locally
  54. },
  55. ),
  56. ),
  57. // Tree
  58. Expanded(
  59. child: _isLoading
  60. ? const Center(child: CircularProgressIndicator())
  61. : ListView(
  62. children: [
  63. ListTile(
  64. title: const Text('全部题目'),
  65. selected: _selectedCode == null,
  66. onTap: () {
  67. setState(() => _selectedCode = null);
  68. widget.onSelectChapter(null, null);
  69. },
  70. ),
  71. ..._tree.map((node) => _buildTreeNode(node, 0)),
  72. ],
  73. ),
  74. ),
  75. ],
  76. );
  77. }
  78. Widget _buildTreeNode(KnowledgeNode node, int depth) {
  79. // Build chapter filter string: for top-level nodes, use "XX-name" format
  80. final chapterFilter = depth == 0 ? '${node.code}-${node.name}' : node.name;
  81. if (node.children.isEmpty) {
  82. return ListTile(
  83. contentPadding: EdgeInsets.only(left: 16.0 + depth * 16.0),
  84. dense: true,
  85. title: Text(node.name, style: const TextStyle(fontSize: 13)),
  86. selected: _selectedCode == node.code,
  87. onTap: () {
  88. setState(() => _selectedCode = node.code);
  89. widget.onSelectChapter(chapterFilter, node.name);
  90. },
  91. );
  92. }
  93. return ExpansionTile(
  94. tilePadding: EdgeInsets.only(left: depth * 16.0),
  95. title: Text(
  96. node.name,
  97. style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
  98. ),
  99. initiallyExpanded: depth < 2,
  100. onExpansionChanged: (expanded) {
  101. if (expanded) {
  102. setState(() => _selectedCode = node.code);
  103. widget.onSelectChapter(chapterFilter, node.name);
  104. }
  105. },
  106. children: node.children.map((c) => _buildTreeNode(c, depth + 1)).toList(),
  107. );
  108. }
  109. }