底部tab的灵活可控
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user