diff --git a/lib/features/main_container/domain/tab_config.dart b/lib/features/main_container/domain/tab_config.dart new file mode 100644 index 00000000..e9cf9688 --- /dev/null +++ b/lib/features/main_container/domain/tab_config.dart @@ -0,0 +1,119 @@ +import 'package:equatable/equatable.dart'; + +class TabConfigItem extends Equatable { + final String id; + final String name; + final String nameEn; + final String icon; + final bool isEnabled; + final int order; + + const TabConfigItem({ + required this.id, + required this.name, + required this.nameEn, + required this.icon, + required this.isEnabled, + required this.order, + }); + + TabConfigItem copyWith({ + String? id, + String? name, + String? nameEn, + String? icon, + bool? isEnabled, + int? order, + }) { + return TabConfigItem( + id: id ?? this.id, + name: name ?? this.name, + nameEn: nameEn ?? this.nameEn, + icon: icon ?? this.icon, + isEnabled: isEnabled ?? this.isEnabled, + order: order ?? this.order, + ); + } + + @override + List get props => [id, name, nameEn, icon, isEnabled, order]; +} + +class TabConfig extends Equatable { + final List items; + + const TabConfig({required this.items}); + + List get enabledItems { + final enabled = items.where((item) => item.isEnabled).toList(); + enabled.sort((a, b) => a.order.compareTo(b.order)); + return enabled; + } + + TabConfig copyWith({List? items}) { + return TabConfig(items: items ?? this.items); + } + + TabConfig toggleItem(String itemId) { + final currentItem = items.firstWhere((item) => item.id == itemId); + + // 如果当前项是启用的,检查是否是最后一个启用的 + if (currentItem.isEnabled) { + final enabledCount = items.where((item) => item.isEnabled).length; + if (enabledCount <= 1) { + // 至少保留一个 Tab,不允许关闭 + return this; + } + } + + final updatedItems = items.map((item) { + if (item.id == itemId) { + return item.copyWith(isEnabled: !item.isEnabled); + } + return item; + }).toList(); + return copyWith(items: updatedItems); + } + + static TabConfig defaultConfig() { + return const TabConfig( + items: [ + TabConfigItem( + id: 'home', + name: '状态', + nameEn: 'Status', + icon: 'grid_view_rounded', + isEnabled: true, + order: 1, + ), + TabConfigItem( + id: 'ai', + name: 'AI', + nameEn: 'AI', + icon: 'auto_awesome_rounded', + isEnabled: true, + order: 2, + ), + TabConfigItem( + id: 'warning', + name: '告警中心', + nameEn: 'Alerts', + icon: 'warning_amber_rounded', + isEnabled: true, + order: 3, + ), + TabConfigItem( + id: 'my', + name: '我的', + nameEn: 'Profile', + icon: 'person_rounded', + isEnabled: true, + order: 4, + ), + ], + ); + } + + @override + List get props => [items]; +} diff --git a/lib/features/main_container/presentation/bloc/tab_config_cubit.dart b/lib/features/main_container/presentation/bloc/tab_config_cubit.dart new file mode 100644 index 00000000..dff65db0 --- /dev/null +++ b/lib/features/main_container/presentation/bloc/tab_config_cubit.dart @@ -0,0 +1,51 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart'; + +class TabConfigState { + final TabConfig config; + final int selectedIndex; + + const TabConfigState({ + required this.config, + this.selectedIndex = 0, + }); + + TabConfigState copyWith({ + TabConfig? config, + int? selectedIndex, + }) { + return TabConfigState( + config: config ?? this.config, + selectedIndex: selectedIndex ?? this.selectedIndex, + ); + } +} + +class TabConfigCubit extends Cubit { + TabConfigCubit() : super(TabConfigState(config: TabConfig.defaultConfig())); + + /// 切换 Tab 的启用状态 + void toggleTab(String tabId) { + final newConfig = state.config.toggleItem(tabId); + + // 如果当前选中的 Tab 被禁用,自动切换到第一个启用的 Tab + int newIndex = state.selectedIndex; + if (newIndex >= newConfig.enabledItems.length) { + newIndex = 0; + } + + emit(state.copyWith(config: newConfig, selectedIndex: newIndex)); + } + + /// 切换选中的 Tab + void selectTab(int index) { + if (index >= 0 && index < state.config.enabledItems.length) { + emit(state.copyWith(selectedIndex: index)); + } + } + + /// 重置为默认配置 + void resetToDefault() { + emit(TabConfigState(config: TabConfig.defaultConfig())); + } +} diff --git a/lib/features/main_container/presentation/cubit/tab_config_cubit.dart b/lib/features/main_container/presentation/cubit/tab_config_cubit.dart new file mode 100644 index 00000000..4de78e0b --- /dev/null +++ b/lib/features/main_container/presentation/cubit/tab_config_cubit.dart @@ -0,0 +1,87 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import '../../domain/tab_config.dart'; + +abstract class TabConfigState {} + +class TabConfigInitial extends TabConfigState {} + +class TabConfigLoaded extends TabConfigState { + final TabConfig config; + + TabConfigLoaded(this.config); +} + +class TabConfigCubit extends Cubit { + final SharedPreferences _prefs; + static const String _configKey = 'tab_config'; + + TabConfigCubit(this._prefs) : super(TabConfigInitial()) { + _loadConfig(); + } + + void _loadConfig() { + try { + final configJson = _prefs.getString(_configKey); + if (configJson != null) { + final List decoded = jsonDecode(configJson); + final items = decoded.map((e) { + final map = Map.from(e); + return TabConfigItem( + id: map['id'] as String, + name: map['name'] as String, + nameEn: map['nameEn'] as String, + icon: map['icon'] as String, + isEnabled: map['isEnabled'] as bool, + order: map['order'] as int, + ); + }).toList(); + emit(TabConfigLoaded(TabConfig(items: items))); + } else { + final defaultConfig = TabConfig.defaultConfig(); + _saveConfig(defaultConfig); + emit(TabConfigLoaded(defaultConfig)); + } + } catch (e) { + final defaultConfig = TabConfig.defaultConfig(); + emit(TabConfigLoaded(defaultConfig)); + } + } + + void _saveConfig(TabConfig config) { + try { + final configList = config.items.map((item) => { + 'id': item.id, + 'name': item.name, + 'nameEn': item.nameEn, + 'icon': item.icon, + 'isEnabled': item.isEnabled, + 'order': item.order, + }).toList(); + _prefs.setString(_configKey, jsonEncode(configList)); + } catch (e) { + print('保存 Tab 配置失败: $e'); + } + } + + void toggleTab(String itemId) { + if (state is TabConfigLoaded) { + final currentState = state as TabConfigLoaded; + final newConfig = currentState.config.toggleItem(itemId); + + // 只有配置真正改变时才保存和 emit + if (newConfig != currentState.config) { + _saveConfig(newConfig); + emit(TabConfigLoaded(newConfig)); + } + } + } + + TabConfig? getConfig() { + if (state is TabConfigLoaded) { + return (state as TabConfigLoaded).config; + } + return null; + } +} diff --git a/lib/features/main_container/presentation/pages/custom_main_container.dart b/lib/features/main_container/presentation/pages/custom_main_container.dart new file mode 100644 index 00000000..87483510 --- /dev/null +++ b/lib/features/main_container/presentation/pages/custom_main_container.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart'; +import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart'; +import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart'; + +import '../../../waring_center/presentation/pages/warning_center_page.dart'; + +class CustomMainContainer extends StatefulWidget { + const CustomMainContainer({super.key}); + + @override + State createState() => _CustomMainContainerState(); +} + +class _CustomMainContainerState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; // 保持页面状态 + + @override + Widget build(BuildContext context) { + super.build(context); + + debugPrint('🔍 [CustomMainContainer] build 被调用'); + + return BlocBuilder( + builder: (context, state) { + final enabledTabs = state.config.enabledItems; + final currentIndex = state.selectedIndex; + + debugPrint('🔍 [CustomMainContainer] enabledTabs=${enabledTabs.length}, currentIndex=$currentIndex'); + debugPrint('🔍 [CustomMainContainer] enabledTabs IDs=${enabledTabs.map((e) => e.id).toList()}'); + + if (enabledTabs.isEmpty) { + return const Scaffold( + body: Center(child: Text('请至少启用一个Tab')), + ); + } + + return Scaffold( + body: IndexedStack( + index: currentIndex, + children: _buildPages(enabledTabs), + ), + bottomNavigationBar: const CustomBottomNavBar(), + ); + }, + ); + } + + List _buildPages(List enabledTabs) { + return enabledTabs.map((tab) { + switch (tab.id) { + case 'home': + return const HomePage(); + case 'ai': + return const AiPage(); + case 'warning': + return const WarningCenterPage(); + case 'my': + return const MyPage(); + default: + return const HomePage(); + } + }).toList(); + } +} diff --git a/lib/features/main_container/presentation/pages/tab_settings_page.dart b/lib/features/main_container/presentation/pages/tab_settings_page.dart new file mode 100644 index 00000000..1c6c820a --- /dev/null +++ b/lib/features/main_container/presentation/pages/tab_settings_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart'; +import 'package:cc_ui_kit/cc_ui_kit.dart'; + +class TabSettingsPage extends StatelessWidget { + const TabSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Tab 配置'), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + ), + body: BlocBuilder( + builder: (context, state) { + final tabs = state.config.items; + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: tabs.length, + itemBuilder: (context, index) { + final tab = tabs[index]; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon(_getIconData(tab.icon)), + title: Text(tab.name), + subtitle: Text(tab.nameEn), + trailing: Switch( + value: tab.isEnabled, + onChanged: (value) { + context.read().toggleTab(tab.id); + }, + ), + ), + ); + }, + ); + }, + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16), + child: CCPrimaryButton( + text: '重置为默认', + onPressed: () { + context.read().resetToDefault(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已重置为默认配置')), + ); + }, + ), + ), + ); + } + + IconData _getIconData(String iconName) { + switch (iconName) { + case 'grid_view_rounded': + return Icons.grid_view_rounded; + case 'auto_awesome_rounded': + return Icons.auto_awesome_rounded; + case 'warning_amber_rounded': + return Icons.warning_amber_rounded; + case 'person_rounded': + return Icons.person_rounded; + default: + return Icons.home_rounded; + } + } +} diff --git a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart new file mode 100644 index 00000000..118f58b5 --- /dev/null +++ b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart'; +import 'package:cc_ui_kit/cc_ui_kit.dart'; + +class CustomBottomNavBar extends StatelessWidget { + const CustomBottomNavBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final enabledTabs = state.config.enabledItems; + + if (enabledTabs.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(enabledTabs.length, (index) { + final tab = enabledTabs[index]; + final isSelected = index == state.selectedIndex; + + return _buildNavItem( + context, + icon: _getIconData(tab.icon), + label: tab.name, + isSelected: isSelected, + onTap: () => context.read().selectTab(index), + ); + }), + ), + ), + ); + }, + ); + } + + Widget _buildNavItem( + BuildContext context, { + required IconData icon, + required String label, + required bool isSelected, + required VoidCallback onTap, + }) { + return Expanded( + child: InkWell( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected ? const Color(0xFF2196F3) : Colors.grey, + size: 24, + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isSelected ? const Color(0xFF2196F3) : Colors.grey, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } + + IconData _getIconData(String iconName) { + switch (iconName) { + case 'grid_view_rounded': + return Icons.grid_view_rounded; + case 'auto_awesome_rounded': + return Icons.auto_awesome_rounded; + case 'warning_amber_rounded': + return Icons.warning_amber_rounded; + case 'person_rounded': + return Icons.person_rounded; + default: + return Icons.home_rounded; + } + } +} diff --git a/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart b/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart new file mode 100644 index 00000000..3b12347e --- /dev/null +++ b/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart @@ -0,0 +1,192 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'warning_center_event.dart'; +import 'warning_center_state.dart'; + +class WarningCenterBloc extends Bloc { + WarningCenterBloc() : super(const WarningCenterInitial()) { + on(_onLoadData); + on(_onFilterByLevel); + on(_onFilterByTime); + on(_onSearch); + } + + Future _onLoadData( + WarningCenterLoadData event, + Emitter emit, + ) async { + emit(const WarningCenterLoading()); + + try { + await Future.delayed(const Duration(milliseconds: 500)); + + final stats = const WarningStats( + totalCount: 156, + todayCount: 156, + severeCount: 8, + processingCount: 15, + closedCount: 133, + avgResponseTime: 2.3, + changePercent: -12, + ); + + final warnings = [ + const WarningItem( + id: '1', + level: '严重', + deviceName: '逆变器12', + deviceType: '组串式', + content: '逆变器停机', + status: '处理中', + time: '2025-05-25 09:18:32', + duration: '2小时18分', + responsiblePerson: '张运维', + ), + const WarningItem( + id: '2', + level: '重要', + deviceName: '逆变器13', + deviceType: '组串式', + content: '逆变器停机', + status: '处理中', + time: '2025-05-25 09:18:32', + duration: '2小时18分', + responsiblePerson: '张运维', + ), + const WarningItem( + id: '3', + level: '严重', + deviceName: '逆变器05', + deviceType: '组串式', + content: '逆变器停机', + status: '处理中', + time: '2025-05-25 09:18:32', + duration: '2小时18分', + responsiblePerson: '张运维', + ), + const WarningItem( + id: '4', + level: '一般', + deviceName: '逆变器08', + deviceType: '组串式', + content: '温度过高告警', + status: '待处理', + time: '2025-05-25 08:30:15', + duration: '3小时05分', + responsiblePerson: '李工程师', + ), + const WarningItem( + id: '5', + level: '提示', + deviceName: '逆变器15', + deviceType: '组串式', + content: '效率低于阈值', + status: '已关闭', + time: '2025-05-24 16:45:20', + duration: '1天5小时', + responsiblePerson: '王技术员', + ), + const WarningItem( + id: '6', + level: '严重', + deviceName: '汇流箱03', + deviceType: '直流汇流', + content: '通信中断', + status: '处理中', + time: '2025-05-25 07:20:10', + duration: '4小时15分', + responsiblePerson: '张运维', + ), + const WarningItem( + id: '7', + level: '重要', + deviceName: '逆变器20', + deviceType: '组串式', + content: '输出电压异常', + status: '待处理', + time: '2025-05-25 10:05:30', + duration: '1小时30分', + responsiblePerson: '赵工程师', + ), + const WarningItem( + id: '8', + level: '一般', + deviceName: '变压器02', + deviceType: '升压变压器', + content: '负载率超过80%', + status: '已关闭', + time: '2025-05-24 14:20:45', + duration: '1天10小时', + responsiblePerson: '李工程师', + ), + ]; + + emit(WarningCenterLoaded( + stats: stats, + warnings: warnings, + )); + } catch (e) { + emit(WarningCenterError('加载数据失败:$e')); + } + } + + Future _onFilterByLevel( + WarningCenterFilterByLevel event, + Emitter emit, + ) async { + if (state is WarningCenterLoaded) { + final currentState = state as WarningCenterLoaded; + + List filteredWarnings; + if (event.level == '全部') { + filteredWarnings = currentState.warnings; + } else { + filteredWarnings = currentState.warnings + .where((w) => w.level == event.level) + .toList(); + } + + emit(currentState.copyWith( + selectedLevel: event.level, + warnings: filteredWarnings, + )); + } + } + + Future _onFilterByTime( + WarningCenterFilterByTime event, + Emitter emit, + ) async { + if (state is WarningCenterLoaded) { + final currentState = state as WarningCenterLoaded; + emit(currentState.copyWith( + selectedTimeRange: event.timeRange, + )); + } + } + + Future _onSearch( + WarningCenterSearch event, + Emitter emit, + ) async { + if (state is WarningCenterLoaded) { + final currentState = state as WarningCenterLoaded; + + List filteredWarnings; + if (event.keyword.isEmpty) { + filteredWarnings = currentState.warnings; + } else { + filteredWarnings = currentState.warnings + .where((w) => + w.deviceName.contains(event.keyword) || + w.content.contains(event.keyword) || + w.deviceType.contains(event.keyword)) + .toList(); + } + + emit(currentState.copyWith( + searchKeyword: event.keyword, + warnings: filteredWarnings, + )); + } + } +} diff --git a/lib/features/waring_center/presentation/bloc/warning_center_event.dart b/lib/features/waring_center/presentation/bloc/warning_center_event.dart new file mode 100644 index 00000000..54aedea4 --- /dev/null +++ b/lib/features/waring_center/presentation/bloc/warning_center_event.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +abstract class WarningCenterEvent extends Equatable { + const WarningCenterEvent(); + + @override + List get props => []; +} + +class WarningCenterLoadData extends WarningCenterEvent { + const WarningCenterLoadData(); +} + +class WarningCenterFilterByLevel extends WarningCenterEvent { + final String level; + + const WarningCenterFilterByLevel(this.level); + + @override + List get props => [level]; +} + +class WarningCenterFilterByTime extends WarningCenterEvent { + final String timeRange; + + const WarningCenterFilterByTime(this.timeRange); + + @override + List get props => [timeRange]; +} + +class WarningCenterSearch extends WarningCenterEvent { + final String keyword; + + const WarningCenterSearch(this.keyword); + + @override + List get props => [keyword]; +} diff --git a/lib/features/waring_center/presentation/bloc/warning_center_state.dart b/lib/features/waring_center/presentation/bloc/warning_center_state.dart new file mode 100644 index 00000000..02a344bd --- /dev/null +++ b/lib/features/waring_center/presentation/bloc/warning_center_state.dart @@ -0,0 +1,134 @@ +import 'package:equatable/equatable.dart'; + +class WarningStats extends Equatable { + final int totalCount; + final int todayCount; + final int severeCount; + final int processingCount; + final int closedCount; + final double avgResponseTime; + final double changePercent; + + const WarningStats({ + required this.totalCount, + required this.todayCount, + required this.severeCount, + required this.processingCount, + required this.closedCount, + required this.avgResponseTime, + required this.changePercent, + }); + + @override + List get props => [ + totalCount, + todayCount, + severeCount, + processingCount, + closedCount, + avgResponseTime, + changePercent, + ]; +} + +class WarningItem extends Equatable { + final String id; + final String level; + final String deviceName; + final String deviceType; + final String content; + final String status; + final String time; + final String duration; + final String responsiblePerson; + + const WarningItem({ + required this.id, + required this.level, + required this.deviceName, + required this.deviceType, + required this.content, + required this.status, + required this.time, + required this.duration, + required this.responsiblePerson, + }); + + @override + List get props => [ + id, + level, + deviceName, + deviceType, + content, + status, + time, + duration, + responsiblePerson, + ]; +} + +abstract class WarningCenterState extends Equatable { + const WarningCenterState(); + + @override + List get props => []; +} + +class WarningCenterInitial extends WarningCenterState { + const WarningCenterInitial(); +} + +class WarningCenterLoading extends WarningCenterState { + const WarningCenterLoading(); +} + +class WarningCenterLoaded extends WarningCenterState { + final WarningStats stats; + final List warnings; + final String selectedLevel; + final String selectedTimeRange; + final String searchKeyword; + + const WarningCenterLoaded({ + required this.stats, + required this.warnings, + this.selectedLevel = '全部', + this.selectedTimeRange = '近7天', + this.searchKeyword = '', + }); + + @override + List get props => [ + stats, + warnings, + selectedLevel, + selectedTimeRange, + searchKeyword, + ]; + + WarningCenterLoaded copyWith({ + WarningStats? stats, + List? warnings, + String? selectedLevel, + String? selectedTimeRange, + String? searchKeyword, + }) { + return WarningCenterLoaded( + stats: stats ?? this.stats, + warnings: warnings ?? this.warnings, + selectedLevel: selectedLevel ?? this.selectedLevel, + selectedTimeRange: selectedTimeRange ?? this.selectedTimeRange, + searchKeyword: searchKeyword ?? this.searchKeyword, + ); + } +} + +class WarningCenterError extends WarningCenterState { + final String message; + + const WarningCenterError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/features/waring_center/presentation/pages/warning_center_page.dart b/lib/features/waring_center/presentation/pages/warning_center_page.dart new file mode 100644 index 00000000..6dd01e3f --- /dev/null +++ b/lib/features/waring_center/presentation/pages/warning_center_page.dart @@ -0,0 +1,638 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; +import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_bloc.dart'; +import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_event.dart'; +import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_state.dart'; + +class WarningCenterPage extends StatefulWidget { + const WarningCenterPage({super.key}); + + @override + State createState() => _WarningCenterPageState(); +} + +class _WarningCenterPageState extends State { + late WarningCenterBloc _bloc; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _bloc = WarningCenterBloc(); + _bloc.add(const WarningCenterLoadData()); + } + + @override + void dispose() { + _searchController.dispose(); + _bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _bloc, + child: Scaffold( + backgroundColor: const Color(0xFF0A2E6B), + body: BlocBuilder( + builder: (context, state) { + if (state is WarningCenterLoading) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (state is WarningCenterError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.white, size: 48), + const SizedBox(height: 16), + Text( + state.message, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + } + + if (state is WarningCenterLoaded) { + return _buildContent(context, state); + } + + return const SizedBox(); + }, + ), + ), + ); + } + + Widget _buildContent(BuildContext context, WarningCenterLoaded state) { + return CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 80, + floating: false, + pinned: true, + backgroundColor: const Color(0xFF0A2E6B), + title: Row( + children: [ + Text( + AppLocalizations.of(context).translate('warning_center.title'), + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF4A7AFF), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '1', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ], + ), + actions: [ + Stack( + children: [ + IconButton( + icon: const Icon(Icons.notifications_none, color: Colors.white, size: 28), + onPressed: () {}, + ), + Positioned( + right: 8, + top: 8, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: const Text( + '8', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + const SizedBox(width: 8), + ], + ), + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: _buildTodayStatsCard(context, state), + ), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildStatsCard( + context, + AppLocalizations.of(context).translate('warning_center.severe_alerts'), + state.stats.severeCount.toString(), + const Color(0xFFE74C3C), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatsCard( + context, + AppLocalizations.of(context).translate('warning_center.processing'), + state.stats.processingCount.toString(), + const Color(0xFFF39C12), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildStatsCard( + context, + AppLocalizations.of(context).translate('warning_center.closed'), + state.stats.closedCount.toString(), + const Color(0xFF27AE60), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatsCard( + context, + AppLocalizations.of(context).translate('warning_center.avg_response_time'), + '${state.stats.avgResponseTime}h', + const Color(0xFF3498DB), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.all')), + _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.severe')), + _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.important')), + _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.general')), + _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.hint')), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context).translate('warning_center.device_type'), + style: TextStyle( + fontSize: 13, + color: state.selectedLevel == AppLocalizations.of(context).translate('warning_center.all') + ? const Color(0xFF4A7AFF) + : Colors.grey.shade700, + ), + ), + const SizedBox(width: 4), + Icon(Icons.keyboard_arrow_down, size: 16, color: Colors.grey.shade600), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.last_7_days')), + const SizedBox(width: 8), + _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.specific_date')), + const SizedBox(width: 8), + _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.all')), + const SizedBox(width: 8), + Expanded( + child: Container( + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: AppLocalizations.of(context).translate('warning_center.search_placeholder'), + hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade400), + border: InputBorder.none, + isDense: true, + ), + style: const TextStyle(fontSize: 13), + onChanged: (value) { + _bloc.add(WarningCenterSearch(value)); + }, + ), + ), + Icon(Icons.search, size: 18, color: Colors.grey.shade400), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final warning = state.warnings[index]; + return _buildWarningCard(context, warning); + }, + childCount: state.warnings.length, + ), + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 100), + ), + ], + ); + } + + Widget _buildTodayStatsCard(BuildContext context, WarningCenterLoaded state) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF4A7AFF), Color(0xFF2E5CDB)], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + AppLocalizations.of(context).translate('warning_center.today_total'), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Color(0xFFE74C3C), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_downward, color: Colors.white, size: 12), + ), + ], + ), + const SizedBox(height: 8), + Text( + state.stats.todayCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.arrow_downward, color: Colors.white, size: 14), + const SizedBox(width: 4), + Text( + '${state.stats.changePercent.abs()}% ${AppLocalizations.of(context).translate('warning_center.vs_yesterday')}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildStatsCard(BuildContext context, String title, String value, Color color) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + ), + ), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _buildFilterChip(BuildContext context, WarningCenterLoaded state, String label) { + final isSelected = state.selectedLevel == label; + return GestureDetector( + onTap: () { + _bloc.add(WarningCenterFilterByLevel(label)); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF4A7AFF) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? const Color(0xFF4A7AFF) : Colors.grey.shade300, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + color: isSelected ? Colors.white : Colors.grey.shade700, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ); + } + + Widget _buildTimeFilterChip(BuildContext context, WarningCenterLoaded state, String label) { + final isSelected = state.selectedTimeRange == label; + return GestureDetector( + onTap: () { + _bloc.add(WarningCenterFilterByTime(label)); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF4A7AFF) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? const Color(0xFF4A7AFF) : Colors.grey.shade300, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + color: isSelected ? Colors.white : Colors.grey.shade700, + ), + ), + ), + ); + } + + Widget _buildWarningCard(BuildContext context, WarningItem warning) { + Color levelColor; + switch (warning.level) { + case '严重': + levelColor = const Color(0xFFE74C3C); + break; + case '重要': + levelColor = const Color(0xFFF39C12); + break; + case '一般': + levelColor = const Color(0xFF27AE60); + break; + default: + levelColor = const Color(0xFF3498DB); + } + + Color statusColor; + switch (warning.status) { + case '处理中': + statusColor = const Color(0xFF4A7AFF); + break; + case '待处理': + statusColor = const Color(0xFFF39C12); + break; + case '已关闭': + statusColor = const Color(0xFF27AE60); + break; + default: + statusColor = Colors.grey; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: levelColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + warning.level, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + '${warning.deviceName}(${warning.deviceType})', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1A1A), + ), + ), + ), + Text( + warning.duration, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: statusColor), + ), + child: Text( + warning.status, + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${AppLocalizations.of(context).translate('warning_center.alert_content')}:${warning.content}', + style: const TextStyle( + fontSize: 14, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 6), + Text( + '${AppLocalizations.of(context).translate('warning_center.alert_time')}:${warning.time}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + AppLocalizations.of(context).translate('warning_center.pending_status'), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + warning.status, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 4), + Text( + AppLocalizations.of(context).translate('warning_center.responsible_person'), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + warning.responsiblePerson, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1A1A), + ), + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/waring_center/presentation/routes/warning_center_routes.dart b/lib/features/waring_center/presentation/routes/warning_center_routes.dart new file mode 100644 index 00000000..5e3414cc --- /dev/null +++ b/lib/features/waring_center/presentation/routes/warning_center_routes.dart @@ -0,0 +1,12 @@ +import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/core/router/route_paths.dart'; +import 'package:maibu_satabot_v2/features/waring_center/presentation/pages/warning_center_page.dart'; + +class WarningCenterRoutes { + static List get routes => [ + GoRoute( + path: RoutePaths.warningCenter, + builder: (context, state) => const WarningCenterPage(), + ), + ]; +}