底部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,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;
}
}
}