Files
digital-pilates/store/tabBarConfigSlice.ts
richarjiang be0dd750eb feat(vip): 限制底部栏自定义功能为VIP专享
非VIP用户尝试配置底部栏时将显示会员购买弹窗,
只有VIP会员才能自由开启或关闭导航标签。
包含会员权益说明的国际化支持和存储结构重构。
2025-12-01 16:56:54 +08:00

230 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './index';
// Tab 配置接口
export interface TabConfig {
id: string; // tab 标识符
icon: string; // SF Symbol 图标名
titleKey: string; // i18n 翻译 key
enabled: boolean; // 是否启用
canBeDisabled: boolean; // 是否可以被禁用(系统级配置,不持久化)
order: number; // 显示顺序
}
// 用户可持久化的配置(只包含用户可控制的属性)
interface UserTabConfig {
id: string;
enabled: boolean;
order: number;
}
// State 接口
interface TabBarConfigState {
configs: TabConfig[];
isInitialized: boolean;
}
// 默认配置
export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
{
id: 'statistics',
icon: 'chart.pie.fill',
titleKey: 'statistics.tabs.health',
enabled: true,
canBeDisabled: false,
order: 1,
},
{
id: 'medications',
icon: 'pills.fill',
titleKey: 'statistics.tabs.medications',
enabled: true,
canBeDisabled: true, // 用药管理可以被关闭
order: 2,
},
{
id: 'fasting',
icon: 'timer',
titleKey: 'statistics.tabs.fasting',
enabled: true,
canBeDisabled: true, // 断食可以被关闭
order: 3,
},
{
id: 'challenges',
icon: 'trophy.fill',
titleKey: 'statistics.tabs.challenges',
enabled: true,
canBeDisabled: true, // 挑战可以被关闭
order: 4,
},
{
id: 'personal',
icon: 'person.fill',
titleKey: 'statistics.tabs.personal',
enabled: true,
canBeDisabled: false,
order: 5,
},
];
// AsyncStorage key
const STORAGE_KEY = 'tab_bar_config';
// 初始状态
const initialState: TabBarConfigState = {
configs: DEFAULT_TAB_CONFIGS,
isInitialized: false,
};
const tabBarConfigSlice = createSlice({
name: 'tabBarConfig',
initialState,
reducers: {
// 设置配置(用于从 AsyncStorage 恢复)
setConfigs: (state, action: PayloadAction<TabConfig[]>) => {
state.configs = action.payload;
state.isInitialized = true;
},
// 切换 tab 启用状态
toggleTabEnabled: (state, action: PayloadAction<string>) => {
const tabId = action.payload;
const config = state.configs.find(c => c.id === tabId);
if (config && config.canBeDisabled) {
config.enabled = !config.enabled;
// 自动持久化到 AsyncStorage
saveConfigsToStorage(state.configs);
}
},
// 更新 tab 顺序(拖拽后)
reorderTabs: (state, action: PayloadAction<TabConfig[]>) => {
// 更新顺序,同时保持其他属性不变
const newConfigs = action.payload.map((config, index) => ({
...config,
order: index + 1,
}));
state.configs = newConfigs;
// 自动持久化到 AsyncStorage
saveConfigsToStorage(newConfigs);
},
// 恢复默认配置
resetToDefault: (state) => {
state.configs = DEFAULT_TAB_CONFIGS;
// 持久化到 AsyncStorage
saveConfigsToStorage(DEFAULT_TAB_CONFIGS);
},
// 标记已初始化
markInitialized: (state) => {
state.isInitialized = true;
},
},
});
// 持久化配置到 AsyncStorage只保存用户可控制的属性
const saveConfigsToStorage = async (configs: TabConfig[]) => {
try {
// 只保存用户可控制的属性enabled 和 order
const userConfigs: UserTabConfig[] = configs.map(config => ({
id: config.id,
enabled: config.enabled,
order: config.order,
}));
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(userConfigs));
logger.info('底部栏配置已保存');
} catch (error) {
logger.error('保存底部栏配置失败:', error);
}
};
// 从 AsyncStorage 加载配置
export const loadTabBarConfigs = () => async (dispatch: any) => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const userConfigs = JSON.parse(stored) as UserTabConfig[];
// 验证配置有效性
if (Array.isArray(userConfigs) && userConfigs.length > 0) {
// 合并用户配置和默认配置
const mergedConfigs = mergeWithDefaults(userConfigs);
dispatch(setConfigs(mergedConfigs));
logger.info('底部栏配置已加载');
return;
}
}
// 如果没有存储或无效,使用默认配置
dispatch(setConfigs(DEFAULT_TAB_CONFIGS));
dispatch(markInitialized());
} catch (error) {
logger.error('加载底部栏配置失败:', error);
// 出错时使用默认配置
dispatch(setConfigs(DEFAULT_TAB_CONFIGS));
dispatch(markInitialized());
}
};
// 合并用户配置和默认配置
const mergeWithDefaults = (userConfigs: UserTabConfig[]): TabConfig[] => {
const merged: TabConfig[] = [];
// 遍历默认配置,将用户的 enabled 和 order 合并进来
DEFAULT_TAB_CONFIGS.forEach(defaultConfig => {
const userConfig = userConfigs.find(c => c.id === defaultConfig.id);
if (userConfig) {
// 合并系统配置icon, titleKey, canBeDisabled从默认配置读取
// 用户配置enabled, order从用户配置读取
merged.push({
...defaultConfig,
enabled: userConfig.enabled,
order: userConfig.order,
});
} else {
// 新增的 tab使用默认配置
merged.push({
...defaultConfig,
order: merged.length + 1,
});
}
});
// 按 order 排序
return merged.sort((a, b) => a.order - b.order);
};
// Actions
export const {
setConfigs,
toggleTabEnabled,
reorderTabs,
resetToDefault,
markInitialized,
} = tabBarConfigSlice.actions;
// Selectors
export const selectTabBarConfigs = (state: RootState) => state.tabBarConfig.configs;
// ✅ 使用 createSelector 进行记忆化,避免不必要的重渲染
export const selectEnabledTabs = createSelector(
[selectTabBarConfigs],
(configs) => configs
.filter(config => config.enabled)
.sort((a, b) => a.order - b.order)
);
export const selectIsInitialized = (state: RootState) => state.tabBarConfig.isInitialized;
// 按 id 获取配置
export const selectTabConfigById = (tabId: string) => (state: RootState) =>
state.tabBarConfig.configs.find(config => config.id === tabId);
export default tabBarConfigSlice.reducer;