import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/blocs/setting/setting_bloc.dart'; import 'package:ainoval/models/novel_setting_item.dart'; import 'package:ainoval/models/setting_group.dart'; import 'package:ainoval/models/setting_type.dart'; // 导入设定类型枚举 import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/screens/editor/widgets/novel_setting_detail.dart'; import 'package:ainoval/screens/editor/widgets/floating_setting_dialogs.dart'; // import 'package:ainoval/screens/editor/widgets/menu_builder.dart'; // import 'package:ainoval/screens/editor/widgets/dropdown_manager.dart'; import 'package:ainoval/screens/editor/widgets/custom_dropdown.dart'; import 'package:ainoval/widgets/common/app_search_field.dart'; // 导入统一搜索组件 import 'package:ainoval/utils/web_theme.dart'; // 导入全局主题 // import 'dart:async'; /// 小说设定侧边栏组件 /// /// 用于管理小说设定条目和设定组,以树状列表方式展示 class NovelSettingSidebar extends StatefulWidget { final String novelId; const NovelSettingSidebar({ Key? key, required this.novelId, }) : super(key: key); @override State createState() => _NovelSettingSidebarState(); } class _NovelSettingSidebarState extends State with AutomaticKeepAliveClientMixin { final TextEditingController _searchController = TextEditingController(); // 展开的设定组ID集合 final Set _expandedGroupIds = {}; // 分组模式:'type' = 按设定分类分组,'group' = 按设定组分组 String _groupingMode = 'type'; // 默认使用设定分类分组 // 展开的设定类型集合(用于按类型分组时) final Set _expandedTypeIds = {}; @override bool get wantKeepAlive => true; // 🚀 保持页面存活状态 @override void initState() { super.initState(); // 🚀 优化:简化初始化逻辑,直接检查数据状态 final settingState = context.read().state; AppLogger.i('NovelSettingSidebar', '📊 初始化设定侧边栏 - 小说ID: ${widget.novelId}'); AppLogger.i('NovelSettingSidebar', ' 组状态: ${settingState.groupsStatus}, 组数量: ${settingState.groups.length}'); AppLogger.i('NovelSettingSidebar', ' 条目状态: ${settingState.itemsStatus}, 条目数量: ${settingState.items.length}'); // 🚀 优化:更积极的加载策略,即使状态为loading也可以确保数据最新 if (settingState.groupsStatus == SettingStatus.initial || settingState.groupsStatus == SettingStatus.failure || settingState.groups.isEmpty) { AppLogger.i('NovelSettingSidebar', '🚀 立即加载设定组'); context.read().add(LoadSettingGroups(widget.novelId)); } if (settingState.itemsStatus == SettingStatus.initial || settingState.itemsStatus == SettingStatus.failure || settingState.items.isEmpty) { AppLogger.i('NovelSettingSidebar', '🚀 立即加载设定条目用于引用检测'); context.read().add(LoadSettingItems(novelId: widget.novelId)); } // 🚀 新增:如果数据已经存在,立即通知场景编辑器可以开始引用检测 if (settingState.itemsStatus == SettingStatus.success && settingState.items.isNotEmpty) { AppLogger.i('NovelSettingSidebar', '✅ 设定数据已就绪,条目数量: ${settingState.items.length}'); } } @override void dispose() { _searchController.dispose(); super.dispose(); } // 切换分组模式 void _toggleGroupingMode(String mode) { setState(() { _groupingMode = mode; }); AppLogger.i('NovelSettingSidebar', '切换分组模式: $mode'); } // 切换设定类型展开/折叠状态 void _toggleTypeExpansion(String typeValue) { setState(() { if (_expandedTypeIds.contains(typeValue)) { _expandedTypeIds.remove(typeValue); AppLogger.i('NovelSettingSidebar', '折叠设定类型: $typeValue'); } else { _expandedTypeIds.add(typeValue); AppLogger.i('NovelSettingSidebar', '展开设定类型: $typeValue'); } }); } // 切换设定组展开/折叠状态 void _toggleGroupExpansion(String groupId) { final settingState = context.read().state; final group = settingState.groups.firstWhere( (g) => g.id == groupId, orElse: () => SettingGroup(name: '未知设定组'), ); setState(() { if (_expandedGroupIds.contains(groupId)) { _expandedGroupIds.remove(groupId); AppLogger.i('NovelSettingSidebar', '折叠设定组: ${group.name}'); } else { _expandedGroupIds.add(groupId); AppLogger.i('NovelSettingSidebar', '展开设定组: ${group.name}, 组内条目ID数量: ${group.itemIds?.length ?? 0}, 实际条目数量: ${settingState.items.length}'); // 检查是否有任何组内条目未加载 final missingItems = []; if (group.itemIds != null) { for (final itemId in group.itemIds!) { if (!settingState.items.any((item) => item.id == itemId)) { missingItems.add(itemId); } } } // 如果有未加载的条目,重新加载所有条目 if (missingItems.isNotEmpty) { AppLogger.i('NovelSettingSidebar', '发现未加载的条目: $missingItems, 重新加载所有条目'); context.read().add(LoadSettingItems( novelId: widget.novelId, )); } } }); } // 创建新设定组 void _createSettingGroup() { final settingBloc = context.read(); FloatingSettingDialogs.showSettingGroup( context: context, novelId: widget.novelId, onSave: (group) { settingBloc.add(CreateSettingGroup( novelId: widget.novelId, group: group, )); }, ); } // 编辑设定组 void _editSettingGroup(String groupId) { final settingBloc = context.read(); final group = settingBloc.state.groups.firstWhere( (g) => g.id == groupId, orElse: () => SettingGroup(name: '未知设定组'), ); FloatingSettingDialogs.showSettingGroup( context: context, novelId: widget.novelId, group: group, onSave: (updatedGroup) { settingBloc.add(UpdateSettingGroup( novelId: widget.novelId, groupId: groupId, group: updatedGroup, )); }, ); } // 删除设定组 void _deleteSettingGroup(String groupId) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: const Text('确定要删除这个设定组吗?组内的设定条目将不会被删除。'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); context.read().add(DeleteSettingGroup( novelId: widget.novelId, groupId: groupId, )); }, style: ElevatedButton.styleFrom( backgroundColor: WebTheme.error, foregroundColor: WebTheme.white, ), child: const Text('删除'), ), ], ), ); } // 创建新设定条目 void _createSettingItem({String? groupId}) { // 如果没有指定groupId,则尝试使用第一个可用的设定组 String? defaultGroupId = groupId; if (defaultGroupId == null) { final settingState = context.read().state; if (settingState.groups.isNotEmpty) { defaultGroupId = settingState.groups.first.id; } } FloatingNovelSettingDetail.show( context: context, novelId: widget.novelId, groupId: defaultGroupId, isEditing: true, onSave: _saveSettingItem, onCancel: () { // 取消回调 }, ); } // 编辑设定条目 // void _editSettingItem(String itemId, {String? groupId}) { // FloatingNovelSettingDetail.show( // context: context, // itemId: itemId, // novelId: widget.novelId, // groupId: groupId, // isEditing: true, // onSave: _saveSettingItem, // onCancel: () { // // 取消回调 // }, // ); // } // 查看设定条目 void _viewSettingItem(String itemId, {String? groupId}) { FloatingNovelSettingDetail.show( context: context, itemId: itemId, novelId: widget.novelId, groupId: groupId, isEditing: false, onSave: _saveSettingItem, onCancel: () { // 取消回调 }, ); } // 删除设定条目 // void _deleteSettingItem(String itemId) { // showDialog( // context: context, // builder: (context) => AlertDialog( // title: const Text('确认删除'), // content: const Text('确定要删除这个设定条目吗?此操作不可撤销。'), // actions: [ // TextButton( // onPressed: () => Navigator.of(context).pop(), // child: const Text('取消'), // ), // ElevatedButton( // onPressed: () { // Navigator.of(context).pop(); // context.read().add(DeleteSettingItem( // novelId: widget.novelId, // itemId: itemId, // )); // }, // style: ElevatedButton.styleFrom( // backgroundColor: WebTheme.error, // foregroundColor: WebTheme.white, // ), // child: const Text('删除'), // ), // ], // ), // ); // } // 保存设定条目 void _saveSettingItem(NovelSettingItem item, String? groupId) { AppLogger.i('NovelSettingSidebar', '保存设定条目: ${item.name}, ID=${item.id}, 传入组ID=${groupId}'); if (item.id == null) { // 创建新条目 final settingBloc = context.read(); if (groupId != null) { // 使用传入的组ID创建并添加到组中 settingBloc.add(CreateSettingItemAndAddToGroup( novelId: widget.novelId, item: item, groupId: groupId, )); AppLogger.i('NovelSettingSidebar', '使用组ID创建并添加到组: $groupId'); } else { // 无组ID时直接创建条目 settingBloc.add(CreateSettingItem( novelId: widget.novelId, item: item, )); AppLogger.i('NovelSettingSidebar', '无组ID创建'); } } else { // 更新现有条目 final settingBloc = context.read(); final state = settingBloc.state; settingBloc.add(UpdateSettingItem( novelId: widget.novelId, itemId: item.id!, item: item, )); // 处理组变更:对比旧组与新组,执行移除/添加 final String? oldGroupId = _findGroupIdByItemId(item.id!, state); if (oldGroupId != groupId) { AppLogger.i('NovelSettingSidebar', '检测到组变更: old=$oldGroupId -> new=$groupId'); if (oldGroupId != null) { settingBloc.add(RemoveItemFromGroup( novelId: widget.novelId, groupId: oldGroupId, itemId: item.id!, )); AppLogger.i('NovelSettingSidebar', '已从旧组移除: $oldGroupId'); } if (groupId != null) { settingBloc.add(AddItemToGroup( novelId: widget.novelId, groupId: groupId, itemId: item.id!, )); AppLogger.i('NovelSettingSidebar', '已添加到新组: $groupId'); } } else { AppLogger.i('NovelSettingSidebar', '组未变更,跳过组更新'); } } } // 激活或取消激活设定组 void _toggleGroupActive(String groupId, bool currentIsActive) { context.read().add(SetGroupActiveContext( novelId: widget.novelId, groupId: groupId, isActive: !currentIsActive, )); } // 搜索设定条目 void _searchItems(String searchTerm) { if (searchTerm.isEmpty) { // 如果搜索词为空,加载所有条目 context.read().add(LoadSettingItems( novelId: widget.novelId, )); } else { // 搜索条目 context.read().add(LoadSettingItems( novelId: widget.novelId, name: searchTerm, )); } } // 根据设定条目ID查找所属的设定组ID String? _findGroupIdByItemId(String itemId, SettingState state) { for (final group in state.groups) { if (group.itemIds != null && group.itemIds!.contains(itemId)) { return group.id; } } return null; } @override Widget build(BuildContext context) { super.build(context); // 🚀 必须调用父类的build方法 return Material( color: WebTheme.getSurfaceColor(context), child: Container( color: WebTheme.getSurfaceColor(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 分组切换按钮 _buildGroupingToggle(context), // 搜索和操作栏 _buildSearchBar(context), // 内容区域 Expanded( child: BlocBuilder( buildWhen: (previous, current) { // 仅当与列表相关的数据发生变化时才重建,避免无关状态变更导致的重建 final itemsChanged = !identical(previous.items, current.items); final groupsChanged = !identical(previous.groups, current.groups); final selectedGroupChanged = previous.selectedGroupId != current.selectedGroupId; return itemsChanged || groupsChanged || selectedGroupChanged; }, builder: (context, state) { // 🚀 新增:设定数据加载状态日志 AppLogger.i('NovelSettingSidebar', '🔄 构建设定侧边栏'); AppLogger.d('NovelSettingSidebar', '📊 设定条目数量: ${state.items.length}'); AppLogger.d('NovelSettingSidebar', '📁 设定组数量: ${state.groups.length}'); // 🔧 修复:数量异常提醒 if (state.items.length > 100) { AppLogger.w('NovelSettingSidebar', '⚠️ 设定数量异常多: ${state.items.length}个,请检查是否为历史恢复导致'); } if (state.items.isNotEmpty) { AppLogger.d('NovelSettingSidebar', '📋 设定条目列表:'); for (int i = 0; i < state.items.length && i < 10; i++) { final item = state.items[i]; AppLogger.d('NovelSettingSidebar', ' [$i] ${item.name} (ID: ${item.id})'); } if (state.items.length > 10) { AppLogger.d('NovelSettingSidebar', ' ... 还有 ${state.items.length - 10} 个设定条目'); } } if (state.groupsStatus == SettingStatus.loading && state.groups.isEmpty) { return _buildLoadingState(context); } if (state.groupsStatus == SettingStatus.failure) { return _buildErrorState(context, state.error); } if (state.groups.isEmpty && state.items.isEmpty) { return _buildEmptyState(context); } return _buildSettingList(context, state); }, ), ), ], ), ), ); } // 构建分组切换按钮 Widget _buildGroupingToggle(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), border: Border( bottom: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: Row( children: [ // 按设定分类分组按钮 Expanded( child: GestureDetector( onTap: () => _toggleGroupingMode('type'), child: Container( height: 28, decoration: BoxDecoration( color: _groupingMode == 'type' ? WebTheme.getPrimaryColor(context) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all( color: _groupingMode == 'type' ? WebTheme.getPrimaryColor(context) : WebTheme.getSecondaryBorderColor(context), width: 1, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.category, size: 14, color: _groupingMode == 'type' ? WebTheme.white : WebTheme.getSecondaryTextColor(context), ), const SizedBox(width: 4), Text( '按分类', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _groupingMode == 'type' ? WebTheme.white : WebTheme.getSecondaryTextColor(context), ), ), ], ), ), ), ), const SizedBox(width: 8), // 按设定组分组按钮 Expanded( child: GestureDetector( onTap: () => _toggleGroupingMode('group'), child: Container( height: 28, decoration: BoxDecoration( color: _groupingMode == 'group' ? WebTheme.getPrimaryColor(context) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all( color: _groupingMode == 'group' ? WebTheme.getPrimaryColor(context) : WebTheme.getSecondaryBorderColor(context), width: 1, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.folder, size: 14, color: _groupingMode == 'group' ? WebTheme.white : WebTheme.getSecondaryTextColor(context), ), const SizedBox(width: 4), Text( '按组别', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _groupingMode == 'group' ? WebTheme.white : WebTheme.getSecondaryTextColor(context), ), ), ], ), ), ), ), ], ), ); } // 构建搜索和操作栏 Widget _buildSearchBar(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), decoration: BoxDecoration( color: WebTheme.getBackgroundColor(context), border: Border( bottom: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: Row( children: [ // 搜索框 Expanded( child: AppSearchField( controller: _searchController, hintText: '搜索设定...', height: 34, fillColor: WebTheme.getBackgroundColor(context), onChanged: (value) { if (value.isEmpty) { _searchItems(''); } }, onSubmitted: _searchItems, onClear: () { _searchController.clear(); _searchItems(''); }, ), ), const SizedBox(width: 4), // 🔧 新增:设定数量指示器 BlocBuilder( buildWhen: (previous, current) => previous.items.length != current.items.length, builder: (context, settingState) { if (settingState.items.isNotEmpty) { return Container( height: 34, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( color: settingState.items.length > 50 ? Colors.orange.withOpacity(0.1) : WebTheme.isDarkMode(context) ? WebTheme.darkGrey100.withOpacity(0.3) : WebTheme.grey100, borderRadius: BorderRadius.circular(6), border: settingState.items.length > 50 ? Border.all(color: Colors.orange.withOpacity(0.3), width: 1) : Border.all(color: WebTheme.getSecondaryBorderColor(context), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.settings_outlined, size: 14, color: settingState.items.length > 50 ? Colors.orange.shade700 : WebTheme.getSecondaryTextColor(context), ), const SizedBox(width: 4), Text( '${settingState.items.length}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: settingState.items.length > 50 ? Colors.orange.shade700 : WebTheme.getSecondaryTextColor(context), ), ), ], ), ); } return const SizedBox.shrink(); }, ), // 新建条目按钮 SizedBox( height: 34, child: OutlinedButton.icon( onPressed: () => _createSettingItem(), icon: const Icon(Icons.add, size: 14), label: const Text('新建条目'), style: OutlinedButton.styleFrom( foregroundColor: WebTheme.getTextColor(context), backgroundColor: WebTheme.getBackgroundColor(context), side: BorderSide( color: WebTheme.getTextColor(context), width: 1.0, ), padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 0, ), textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), ), ), const SizedBox(width: 4), // 新建组按钮 SizedBox( height: 34, child: OutlinedButton.icon( onPressed: _createSettingGroup, icon: const Icon(Icons.create_new_folder_outlined, size: 14), label: const Text('新建组'), style: OutlinedButton.styleFrom( foregroundColor: WebTheme.getSecondaryTextColor(context), backgroundColor: WebTheme.getBackgroundColor(context), side: BorderSide( color: WebTheme.getSecondaryTextColor(context), width: 1.0, ), padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 0, ), textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), ), ), const SizedBox(width: 2), // 设置按钮 IconButton( onPressed: () { // TODO: 实现设定设置功能 }, icon: Icon( Icons.settings_outlined, size: 16, color: WebTheme.getSecondaryTextColor(context), ), tooltip: '设定设置', splashRadius: 16, padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 28, minHeight: 28, ), ), ], ), ); } // 构建加载状态 Widget _buildLoadingState(BuildContext context) { return const Center( child: CircularProgressIndicator(), ); } // 构建错误状态 Widget _buildErrorState(BuildContext context, String? error) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, color: WebTheme.error, size: 48, ), const SizedBox(height: 16), Text( '加载设定数据失败', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), if (error != null) Padding( padding: const EdgeInsets.all(8.0), child: Text( error, style: TextStyle( color: WebTheme.getSecondaryTextColor(context), ), textAlign: TextAlign.center, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: () { context.read().add(LoadSettingGroups(widget.novelId)); }, child: const Text('重试'), ), ], ), ); } // 构建空状态 Widget _buildEmptyState(BuildContext context) { return Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '设定库为空', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 8), Text( '设定库存储您小说世界的信息,包括角色、地点、物品及更多设定内容。', style: TextStyle( color: WebTheme.getSecondaryTextColor(context), fontSize: 14, height: 1.5, ), ), const SizedBox(height: 12), InkWell( onTap: _createSettingGroup, child: Text( '→ 点击创建第一个设定组', style: TextStyle( color: WebTheme.getTextColor(context), fontSize: 14, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 8), InkWell( onTap: () => _createSettingItem(), child: Text( '→ 点击创建第一个设定条目', style: TextStyle( color: WebTheme.getTextColor(context), fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), ); } // 构建设定列表(树状结构) Widget _buildSettingList(BuildContext context, SettingState state) { final isSearching = _searchController.text.isNotEmpty; return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ // 搜索结果 if (isSearching && state.items.isNotEmpty) ..._buildSearchResultItems(context, state.items), // 如果正在搜索且没有结果 if (isSearching && state.items.isEmpty) Padding( padding: const EdgeInsets.all(16.0), child: Text( '没有找到匹配"${_searchController.text}"的设定条目', style: TextStyle( fontSize: 14, color: WebTheme.getSecondaryTextColor(context), fontStyle: FontStyle.italic, ), ), ), // 不在搜索时根据分组模式显示内容 if (!isSearching) ..._buildGroupedContent(context, state), ], ); } // 构建分组内容 List _buildGroupedContent(BuildContext context, SettingState state) { if (_groupingMode == 'type') { // 按设定分类分组 return _buildTypeGroupedItems(context, state.items); } else { // 按设定组分组 return state.groups.map((group) => _buildSettingGroupItem(context, group, state.items)).toList(); } } // 构建按设定类型分组的列表 List _buildTypeGroupedItems(BuildContext context, List allItems) { // 按类型分组设定条目 final Map> typeGroups = {}; for (final item in allItems) { final type = item.type ?? 'OTHER'; if (!typeGroups.containsKey(type)) { typeGroups[type] = []; } typeGroups[type]!.add(item); } // 按类型显示名称排序 final sortedTypes = typeGroups.keys.toList() ..sort((a, b) { final typeA = SettingType.fromValue(a); final typeB = SettingType.fromValue(b); return typeA.displayName.compareTo(typeB.displayName); }); return sortedTypes.map((typeValue) { final typeEnum = SettingType.fromValue(typeValue); final items = typeGroups[typeValue]!; // 按名称排序条目 items.sort((a, b) => a.name.compareTo(b.name)); return _buildSettingTypeItem(context, typeEnum, items); }).toList(); } // 构建设定类型项目 Widget _buildSettingTypeItem(BuildContext context, SettingType type, List items) { final isExpanded = _expandedTypeIds.contains(type.value); return Container( decoration: BoxDecoration( border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: Column( children: [ // 设定类型标题行 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey50, border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), bottom: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: InkWell( onTap: () => _toggleTypeExpansion(type.value), child: Row( children: [ // 类型图标 (items.isNotEmpty && items.first.imageUrl != null && items.first.imageUrl!.isNotEmpty) ? ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( items.first.imageUrl!, width: 24, height: 24, fit: BoxFit.cover, errorBuilder: (ctx, err, st) => Icon( _getTypeIconData(type), size: 24, color: WebTheme.getSecondaryTextColor(context), ), loadingBuilder: (ctx, child, loading) { if (loading == null) return child; return Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ); }, ), ) : Icon( _getTypeIconData(type), size: 24, color: WebTheme.getSecondaryTextColor(context), ), const SizedBox(width: 8), // 设定类型名称 Expanded( child: Text( type.displayName, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), ), // 右侧控制区域 Row( mainAxisSize: MainAxisSize.min, children: [ // 条目数量 Text( '${items.length} entries', style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), ), const SizedBox(width: 4), // 创建该类型设定按钮 GestureDetector( onTap: () => _createSettingItemWithType(type), child: Container( width: 24, height: 24, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: Colors.transparent, width: 1, ), ), child: Icon( Icons.add, size: 14, color: WebTheme.getSecondaryTextColor(context), ), ), ), const SizedBox(width: 4), // 展开/折叠图标 Icon( isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, size: 16, color: WebTheme.getSecondaryTextColor(context), ), ], ), ], ), ), ), // 如果展开,显示该类型的设定条目 if (isExpanded) ..._buildTypeSettingItems(context, items), ], ), ); } // 构建类型分组下的设定条目列表 List _buildTypeSettingItems(BuildContext context, List items) { if (items.isEmpty) { return [ Container( padding: const EdgeInsets.all(16), child: Text( '该类型下暂无设定条目', style: TextStyle( fontSize: 13, color: WebTheme.getSecondaryTextColor(context), fontStyle: FontStyle.italic, ), ), ), ]; } return items.map((item) => _buildSettingItemTile(context, item, null)).toList(); } // 创建指定类型的设定条目 void _createSettingItemWithType(SettingType type) { FloatingNovelSettingDetail.show( context: context, novelId: widget.novelId, isEditing: true, prefilledType: type.value, // 预设指定的类型 onSave: _saveSettingItem, onCancel: () { // 取消操作的回调 }, ); } // 构建搜索结果的设定条目列表 List _buildSearchResultItems(BuildContext context, List items) { return [ // 搜索结果标题 Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), child: Text( '搜索结果', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: WebTheme.getSecondaryTextColor(context), ), ), ), // 搜索结果列表 - 查找每个条目所属的组ID ...items.map((item) { final state = context.read().state; final groupId = item.id != null ? _findGroupIdByItemId(item.id!, state) : null; return _buildSettingItemTile(context, item, groupId); }), ]; } // 构建设定组项目 Widget _buildSettingGroupItem(BuildContext context, SettingGroup group, List allItems) { final isExpanded = _expandedGroupIds.contains(group.id); // 调试信息 if (isExpanded && group.id != null) { AppLogger.i('NovelSettingSidebar', '展开组 ${group.name}(${group.id}) - 组内条目IDs: ${group.itemIds}, 所有条目数量: ${allItems.length}'); } // 筛选属于该组的条目 final List groupItems = []; if (group.itemIds != null && group.itemIds!.isNotEmpty) { for (final itemId in group.itemIds!) { final item = allItems.firstWhere( (item) => item.id == itemId, orElse: () => NovelSettingItem( id: itemId, name: "加载中...", content: "" ), ); groupItems.add(item); } // 按名称排序 groupItems.sort((a, b) => a.name.compareTo(b.name)); // 调试信息 if (isExpanded) { AppLogger.i('NovelSettingSidebar', '筛选后组内条目数量: ${groupItems.length}'); } } return Container( decoration: BoxDecoration( border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: Column( children: [ // 设定组标题行 - 重新设计样式 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey50, border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), bottom: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: InkWell( onTap: () { if (group.id != null) { _toggleGroupExpansion(group.id!); } }, child: Row( children: [ // 设定组名称 Expanded( child: Text( group.name, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), ), // 右侧控制区域 Row( mainAxisSize: MainAxisSize.min, children: [ // 条目数量 Text( '${groupItems.length} entries', style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), ), const SizedBox(width: 4), // 添加按钮 if (group.id != null) GestureDetector( onTap: () => _createSettingItem(groupId: group.id), child: Container( width: 24, height: 24, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: Colors.transparent, width: 1, ), ), child: Icon( Icons.add, size: 14, color: WebTheme.getSecondaryTextColor(context), ), ), ), const SizedBox(width: 4), // 展开/折叠图标 Icon( isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, size: 16, color: WebTheme.getSecondaryTextColor(context), ), // 设定组菜单按钮 if (group.id != null) _buildGroupMenuButton(context, group), ], ), ], ), ), ), // 如果展开,显示该组的设定条目 if (isExpanded && group.id != null) ..._buildSettingItems(context, groupItems, group.id!), ], ), ); } // 构建设定条目列表 List _buildSettingItems(BuildContext context, List items, String groupId) { if (items.isEmpty) { return [ Container( padding: const EdgeInsets.all(16), child: Text( '该设定组下暂无条目', style: TextStyle( fontSize: 13, color: WebTheme.getSecondaryTextColor(context), fontStyle: FontStyle.italic, ), ), ), ]; } return items.map((item) => _buildSettingItemTile(context, item, groupId)).toList(); } // 构建设定条目项 - 重新设计为更简洁的样式 Widget _buildSettingItemTile(BuildContext context, NovelSettingItem item, String? groupId) { return Container( decoration: BoxDecoration( border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey100, width: 1.0, ), ), ), child: InkWell( onTap: () { if (item.id != null) { _viewSettingItem(item.id!, groupId: groupId); } }, child: Container( padding: const EdgeInsets.all(8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // 设定类型图标 Container( width: 48, height: 48, decoration: BoxDecoration( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey100, borderRadius: BorderRadius.circular(12), border: Border.all( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.white, width: 2, ), ), child: (item.imageUrl != null && item.imageUrl!.isNotEmpty) ? ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( item.imageUrl!, width: 24, height: 24, fit: BoxFit.cover, errorBuilder: (ctx, err, st) => Icon( _getTypeIconData(SettingType.fromValue(item.type ?? 'OTHER')), size: 24, color: WebTheme.getSecondaryTextColor(context), ), loadingBuilder: (ctx, child, loading) { if (loading == null) return child; return Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ); }, ), ) : Icon( _getTypeIconData(SettingType.fromValue(item.type ?? 'OTHER')), size: 24, color: WebTheme.getSecondaryTextColor(context), ), ), const SizedBox(width: 12), // 内容区域 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Expanded( child: Text( item.name.isNotEmpty ? item.name : 'Unnamed Entry', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: item.name.isNotEmpty ? WebTheme.getTextColor(context) : WebTheme.getSecondaryTextColor(context), fontStyle: item.name.isNotEmpty ? FontStyle.normal : FontStyle.italic, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ), ], ), // 描述内容 if (item.description != null && item.description!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Text( item.description!, style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), // 标签行(放在最后) if (item.tags != null && item.tags!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8), child: Wrap( spacing: 4, runSpacing: 4, children: item.tags!.map((tag) => Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, borderRadius: BorderRadius.circular(4), ), child: Text( tag, style: TextStyle( fontSize: 10, color: WebTheme.getTextColor(context), ), ), )).toList(), ), ), ], ), ), ], ), ), ), ); } // 获取类型图标 IconData _getTypeIconData(SettingType type) { switch (type) { case SettingType.character: return Icons.person; case SettingType.location: return Icons.place; case SettingType.item: return Icons.inventory_2; case SettingType.lore: return Icons.public; case SettingType.event: return Icons.event; case SettingType.concept: return Icons.auto_awesome; case SettingType.faction: return Icons.groups; case SettingType.creature: return Icons.pets; case SettingType.magicSystem: return Icons.auto_fix_high; case SettingType.technology: return Icons.science; case SettingType.culture: return Icons.emoji_people; case SettingType.history: return Icons.history; case SettingType.organization: return Icons.apartment; case SettingType.worldview: return Icons.public; case SettingType.pleasurePoint: return Icons.whatshot; case SettingType.anticipationHook: return Icons.bolt; case SettingType.theme: return Icons.category; case SettingType.tone: return Icons.tonality; case SettingType.style: return Icons.brush; case SettingType.trope: return Icons.theater_comedy; case SettingType.plotDevice: return Icons.schema; case SettingType.powerSystem: return Icons.flash_on; case SettingType.timeline: return Icons.timeline; case SettingType.religion: return Icons.account_balance; case SettingType.politics: return Icons.gavel; case SettingType.economy: return Icons.attach_money; case SettingType.geography: return Icons.map; default: return Icons.article; } } // 构建设定组菜单按钮 Widget _buildGroupMenuButton(BuildContext context, SettingGroup group) { if (group.id == null) return const SizedBox.shrink(); return CustomDropdown( width: 200, align: 'right', trigger: Icon( Icons.more_vert, size: 16, color: WebTheme.getSecondaryTextColor(context), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownItem( icon: Icons.edit, label: '编辑设定组', onTap: () async { _editSettingGroup(group.id!); }, ), DropdownItem( icon: group.isActiveContext == true ? Icons.star : Icons.star_border, label: group.isActiveContext == true ? '取消活跃状态' : '设为活跃上下文', onTap: () async { _toggleGroupActive(group.id!, group.isActiveContext ?? false); }, ), DropdownItem( icon: Icons.add_circle_outline, label: '添加设定条目到此组', onTap: () async { _createSettingItem(groupId: group.id); }, ), const DropdownDivider(), DropdownItem( icon: Icons.delete_outline, label: '删除设定组', isDangerous: true, onTap: () async { _deleteSettingGroup(group.id!); }, ), ], ), ); } // 构建设定条目菜单按钮 // Widget _buildItemMenuButton(BuildContext context, NovelSettingItem item, String? groupId) { return const SizedBox.shrink(); } // 根据设定条目类型构建对应图标 // Widget _buildTypeIcon(String type) { return const SizedBox.shrink(); } // 根据设定条目类型获取对应颜色 // Color _getTypeColor(SettingType type) { // switch (type) { // case SettingType.character: // return WebTheme.getPrimaryColor(context); // case SettingType.location: // return WebTheme.getSecondaryColor(context); // case SettingType.item: // return WebTheme.getTextColor(context); // case SettingType.lore: // return WebTheme.getSecondaryTextColor(context); // case SettingType.event: // return WebTheme.error; // case SettingType.concept: // return WebTheme.getOnSurfaceColor(context); // case SettingType.faction: // return WebTheme.getTextColor(context); // case SettingType.creature: // return WebTheme.getSecondaryTextColor(context); // case SettingType.magicSystem: // return WebTheme.getPrimaryColor(context); // case SettingType.technology: // return WebTheme.getSecondaryTextColor(context); // case SettingType.culture: // return Colors.deepOrange; // case SettingType.history: // return Colors.brown; // case SettingType.organization: // return Colors.indigo; // case SettingType.worldview: // return Colors.purple; // case SettingType.pleasurePoint: // return Colors.redAccent; // case SettingType.anticipationHook: // return Colors.teal; // case SettingType.theme: // return Colors.blueGrey; // case SettingType.tone: // return Colors.amber; // case SettingType.style: // return Colors.cyan; // case SettingType.trope: // return Colors.pink; // case SettingType.plotDevice: // return Colors.green; // case SettingType.powerSystem: // return Colors.orange; // case SettingType.timeline: // return Colors.blue; // case SettingType.religion: // return Colors.deepPurple; // case SettingType.politics: // return Colors.red; // case SettingType.economy: // return Colors.lightGreen; // case SettingType.geography: // return Colors.lightBlue; // default: // return WebTheme.getSecondaryTextColor(context); // } // } }