底部tab的灵活可控

This commit is contained in:
2026-05-14 15:08:19 +08:00
parent a4ac503abd
commit 03f797e717
11 changed files with 1515 additions and 0 deletions

View File

@@ -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<Object?> get props => [id, name, nameEn, icon, isEnabled, order];
}
class TabConfig extends Equatable {
final List<TabConfigItem> items;
const TabConfig({required this.items});
List<TabConfigItem> get enabledItems {
final enabled = items.where((item) => item.isEnabled).toList();
enabled.sort((a, b) => a.order.compareTo(b.order));
return enabled;
}
TabConfig copyWith({List<TabConfigItem>? 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<Object?> get props => [items];
}

View File

@@ -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<TabConfigState> {
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()));
}
}

View File

@@ -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<TabConfigState> {
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<dynamic> decoded = jsonDecode(configJson);
final items = decoded.map((e) {
final map = Map<String, dynamic>.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;
}
}

View File

@@ -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<CustomMainContainer> createState() => _CustomMainContainerState();
}
class _CustomMainContainerState extends State<CustomMainContainer> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // 保持页面状态
@override
Widget build(BuildContext context) {
super.build(context);
debugPrint('🔍 [CustomMainContainer] build 被调用');
return BlocBuilder<TabConfigCubit, TabConfigState>(
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<Widget> _buildPages(List<dynamic> 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();
}
}

View File

@@ -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<TabConfigCubit, TabConfigState>(
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<TabConfigCubit>().toggleTab(tab.id);
},
),
),
);
},
);
},
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: CCPrimaryButton(
text: '重置为默认',
onPressed: () {
context.read<TabConfigCubit>().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;
}
}
}

View File

@@ -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<TabConfigCubit, TabConfigState>(
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<TabConfigCubit>().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;
}
}
}

View File

@@ -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<WarningCenterEvent, WarningCenterState> {
WarningCenterBloc() : super(const WarningCenterInitial()) {
on<WarningCenterLoadData>(_onLoadData);
on<WarningCenterFilterByLevel>(_onFilterByLevel);
on<WarningCenterFilterByTime>(_onFilterByTime);
on<WarningCenterSearch>(_onSearch);
}
Future<void> _onLoadData(
WarningCenterLoadData event,
Emitter<WarningCenterState> 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<void> _onFilterByLevel(
WarningCenterFilterByLevel event,
Emitter<WarningCenterState> emit,
) async {
if (state is WarningCenterLoaded) {
final currentState = state as WarningCenterLoaded;
List<WarningItem> 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<void> _onFilterByTime(
WarningCenterFilterByTime event,
Emitter<WarningCenterState> emit,
) async {
if (state is WarningCenterLoaded) {
final currentState = state as WarningCenterLoaded;
emit(currentState.copyWith(
selectedTimeRange: event.timeRange,
));
}
}
Future<void> _onSearch(
WarningCenterSearch event,
Emitter<WarningCenterState> emit,
) async {
if (state is WarningCenterLoaded) {
final currentState = state as WarningCenterLoaded;
List<WarningItem> 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,
));
}
}
}

View File

@@ -0,0 +1,39 @@
import 'package:equatable/equatable.dart';
abstract class WarningCenterEvent extends Equatable {
const WarningCenterEvent();
@override
List<Object?> get props => [];
}
class WarningCenterLoadData extends WarningCenterEvent {
const WarningCenterLoadData();
}
class WarningCenterFilterByLevel extends WarningCenterEvent {
final String level;
const WarningCenterFilterByLevel(this.level);
@override
List<Object?> get props => [level];
}
class WarningCenterFilterByTime extends WarningCenterEvent {
final String timeRange;
const WarningCenterFilterByTime(this.timeRange);
@override
List<Object?> get props => [timeRange];
}
class WarningCenterSearch extends WarningCenterEvent {
final String keyword;
const WarningCenterSearch(this.keyword);
@override
List<Object?> get props => [keyword];
}

View File

@@ -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<Object?> 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<Object?> get props => [
id,
level,
deviceName,
deviceType,
content,
status,
time,
duration,
responsiblePerson,
];
}
abstract class WarningCenterState extends Equatable {
const WarningCenterState();
@override
List<Object?> get props => [];
}
class WarningCenterInitial extends WarningCenterState {
const WarningCenterInitial();
}
class WarningCenterLoading extends WarningCenterState {
const WarningCenterLoading();
}
class WarningCenterLoaded extends WarningCenterState {
final WarningStats stats;
final List<WarningItem> 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<Object?> get props => [
stats,
warnings,
selectedLevel,
selectedTimeRange,
searchKeyword,
];
WarningCenterLoaded copyWith({
WarningStats? stats,
List<WarningItem>? 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<Object?> get props => [message];
}

View File

@@ -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<WarningCenterPage> createState() => _WarningCenterPageState();
}
class _WarningCenterPageState extends State<WarningCenterPage> {
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<WarningCenterBloc, WarningCenterState>(
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),
),
),
],
),
],
),
],
),
);
}
}

View File

@@ -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<RouteBase> get routes => [
GoRoute(
path: RoutePaths.warningCenter,
builder: (context, state) => const WarningCenterPage(),
),
];
}