feat(ui): 添加底部标签栏自定义配置功能和药物堆叠展示
- 新增底部标签栏配置页面,支持切换标签显示/隐藏和恢复默认设置 - 实现已服用药物的堆叠卡片展示,优化药物列表视觉层次 - 集成Redux状态管理底部标签栏配置,支持本地持久化 - 优化个人中心页面背景渐变效果,移除装饰性圆圈元素 - 更新启动页和应用图标为新的品牌视觉 - 药物详情页AI分析加载动画替换为Lottie动画 - 调整药物卡片圆角半径提升视觉一致性 - 新增多语言支持(中英文)用于标签栏配置界面 主要改进: 1. 用户可以自定义底部导航栏显示内容 2. 已完成的药物以堆叠形式展示,节省空间 3. 配置数据通过AsyncStorage持久化保存 4. 支持默认配置恢复功能
This commit is contained in:
@@ -12,7 +12,9 @@ import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { selectEnabledTabs } from '@/store/tabBarConfigSlice';
|
||||
|
||||
// Tab configuration
|
||||
type TabConfig = {
|
||||
@@ -34,6 +36,9 @@ export default function TabLayout() {
|
||||
const colorTokens = Colors[theme];
|
||||
const pathname = usePathname();
|
||||
const glassEffectAvailable = isLiquidGlassAvailable();
|
||||
|
||||
// 获取已启用的标签配置(按自定义顺序)
|
||||
const enabledTabs = useAppSelector(selectEnabledTabs);
|
||||
|
||||
// Helper function to determine if a tab is selected
|
||||
const isTabSelected = (routeName: string): boolean => {
|
||||
@@ -174,42 +179,45 @@ export default function TabLayout() {
|
||||
tabBarShowLabel: false,
|
||||
});
|
||||
|
||||
// 根据配置渲染标签页
|
||||
if (glassEffectAvailable) {
|
||||
return <NativeTabs>
|
||||
<NativeTabs.Trigger name="statistics">
|
||||
<Label>{t('statistics.tabs.health')}</Label>
|
||||
<Icon sf="chart.pie.fill" drawable="custom_android_drawable" />
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="medications">
|
||||
<Icon sf="pills.fill" drawable="custom_android_drawable" />
|
||||
<Label>{t('statistics.tabs.medications')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="fasting">
|
||||
<Icon sf="timer" drawable="custom_android_drawable" />
|
||||
<Label>{t('statistics.tabs.fasting')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="challenges">
|
||||
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
||||
<Label>{t('statistics.tabs.challenges')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="personal">
|
||||
<Icon sf="person.fill" drawable="custom_settings_drawable" />
|
||||
<Label>{t('statistics.tabs.personal')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
</NativeTabs>
|
||||
return (
|
||||
<NativeTabs>
|
||||
{enabledTabs.map((tab) => {
|
||||
const tabConfig = TAB_CONFIGS[tab.id];
|
||||
if (!tabConfig) return null;
|
||||
|
||||
return (
|
||||
<NativeTabs.Trigger key={tab.id} name={tab.id}>
|
||||
<Icon sf={tabConfig.icon as any} drawable="custom_android_drawable" />
|
||||
<Label>{t(tabConfig.titleKey)}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
);
|
||||
})}
|
||||
</NativeTabs>
|
||||
);
|
||||
}
|
||||
|
||||
// 确定初始路由(第一个启用的标签)
|
||||
const initialRouteName = enabledTabs.length > 0 ? enabledTabs[0].id : 'statistics';
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
initialRouteName="statistics"
|
||||
initialRouteName={initialRouteName}
|
||||
screenOptions={({ route }) => getScreenOptions(route.name)}
|
||||
>
|
||||
|
||||
<Tabs.Screen name="statistics" options={{ title: t('statistics.tabs.health') }} />
|
||||
<Tabs.Screen name="medications" options={{ title: t('statistics.tabs.medications') }} />
|
||||
<Tabs.Screen name="fasting" options={{ title: t('statistics.tabs.fasting') }} />
|
||||
<Tabs.Screen name="challenges" options={{ title: t('statistics.tabs.challenges') }} />
|
||||
<Tabs.Screen name="personal" options={{ title: t('statistics.tabs.personal') }} />
|
||||
{enabledTabs.map((tab) => {
|
||||
const tabConfig = TAB_CONFIGS[tab.id];
|
||||
if (!tabConfig) return null;
|
||||
|
||||
return (
|
||||
<Tabs.Screen
|
||||
key={tab.id}
|
||||
name={tab.id}
|
||||
options={{ title: t(tabConfig.titleKey) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||
import { TakenMedicationsStack } from '@/components/medication/TakenMedicationsStack';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { MedicalDisclaimerSheet } from '@/components/ui/MedicalDisclaimerSheet';
|
||||
@@ -189,6 +190,16 @@ export default function MedicationsScreen() {
|
||||
return medicationsWithImages.filter((item: any) => item.status === activeFilter);
|
||||
}, [activeFilter, medicationsWithImages]);
|
||||
|
||||
const activeMedications = useMemo(() => {
|
||||
if (activeFilter !== 'all') return filteredMedications;
|
||||
return filteredMedications.filter((item: any) => item.status !== 'taken' && item.status !== 'skipped');
|
||||
}, [activeFilter, filteredMedications]);
|
||||
|
||||
const completedMedications = useMemo(() => {
|
||||
if (activeFilter !== 'all') return [];
|
||||
return filteredMedications.filter((item: any) => item.status === 'taken' || item.status === 'skipped');
|
||||
}, [activeFilter, filteredMedications]);
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const taken = medicationsWithImages.filter((item: any) => item.status === 'taken').length;
|
||||
// "未服用"计数包含 missed(已错过)和 upcoming(待服用)
|
||||
@@ -354,7 +365,8 @@ export default function MedicationsScreen() {
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.cardsWrapper}>
|
||||
{filteredMedications.map((item: any) => (
|
||||
{/* 渲染未服用的药物 */}
|
||||
{activeMedications.map((item: any) => (
|
||||
<MedicationCard
|
||||
key={item.id}
|
||||
medication={item}
|
||||
@@ -364,6 +376,17 @@ export default function MedicationsScreen() {
|
||||
onCelebrate={handleMedicationTakenCelebration}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 渲染已完成(服用/跳过)的药物堆叠 */}
|
||||
{completedMedications.length > 0 && (
|
||||
<TakenMedicationsStack
|
||||
medications={completedMedications}
|
||||
colors={colors}
|
||||
selectedDate={selectedDate}
|
||||
onOpenDetails={(item) => handleOpenMedicationDetails(item.medicationId)}
|
||||
onCelebrate={handleMedicationTakenCelebration}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ActivityHeatMap from '@/components/ActivityHeatMap';
|
||||
import { BadgeShowcaseModal } from '@/components/badges/BadgeShowcaseModal';
|
||||
import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree';
|
||||
import { palette } from '@/constants/Colors';
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||||
@@ -586,6 +587,16 @@ export default function PersonalScreen() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('personal.sections.customization'),
|
||||
items: [
|
||||
{
|
||||
icon: 'albums-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||
title: t('personal.menu.tabBarConfig'),
|
||||
onPress: () => router.push(ROUTES.TAB_BAR_CONFIG),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 开发者section(需要连续点击三次用户名激活)
|
||||
...(showDeveloperSection ? [{
|
||||
title: t('personal.sections.developer'),
|
||||
@@ -698,16 +709,12 @@ export default function PersonalScreen() {
|
||||
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||
colors={[palette.purple[100], '#F5F5F5']}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0.3, y: 0.4 }}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* 装饰性圆圈 */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={{
|
||||
@@ -759,33 +766,14 @@ export default function PersonalScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
right: 20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
height: '60%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
|
||||
@@ -386,7 +386,7 @@ export default function ExploreScreen() {
|
||||
<View style={styles.headerContent}>
|
||||
{/* 左边logo */}
|
||||
<Image
|
||||
source={require('@/assets/icon.icon/Assets/icon-1756312748268.png')}
|
||||
source={require('@/assets/machine.png')}
|
||||
style={styles.logoImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { STORAGE_KEYS, setUnauthorizedHandler } from '@/services/api';
|
||||
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
|
||||
import { fetchChallenges } from '@/store/challengesSlice';
|
||||
import { loadTabBarConfigs } from '@/store/tabBarConfigSlice';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -120,6 +121,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
// 初始化底部栏配置
|
||||
useEffect(() => {
|
||||
dispatch(loadTabBarConfigs());
|
||||
}, [dispatch]);
|
||||
|
||||
// ==================== 基础服务初始化(不需要权限,总是执行)====================
|
||||
React.useEffect(() => {
|
||||
const initializeBasicServices = async () => {
|
||||
@@ -515,6 +521,7 @@ export default function RootLayout() {
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen name="badges/index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="settings/tab-bar-config" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -1299,8 +1300,13 @@ export default function MedicationDetailScreen() {
|
||||
</View>
|
||||
|
||||
{aiAnalysisLoading && (
|
||||
<View style={styles.aiLoadingRow}>
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
<View style={styles.aiLoadingContainer}>
|
||||
<LottieView
|
||||
source={require('@/assets/lottie/loading-blue.json')}
|
||||
autoPlay
|
||||
loop
|
||||
style={styles.aiLoadingAnimation}
|
||||
/>
|
||||
<Text style={[styles.aiLoadingText, { color: colors.textSecondary }]}>
|
||||
{t('medications.detail.aiAnalysis.analyzing')}
|
||||
</Text>
|
||||
@@ -2276,13 +2282,19 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
aiLoadingRow: {
|
||||
flexDirection: 'row',
|
||||
aiLoadingContainer: {
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 24,
|
||||
gap: 12,
|
||||
},
|
||||
aiLoadingAnimation: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
},
|
||||
aiLoadingText: {
|
||||
fontSize: 13,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
aiHeroRow: {
|
||||
flexDirection: 'row',
|
||||
|
||||
266
app/settings/tab-bar-config.tsx
Normal file
266
app/settings/tab-bar-config.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import {
|
||||
resetToDefault,
|
||||
selectTabBarConfigs,
|
||||
toggleTabEnabled,
|
||||
type TabConfig,
|
||||
} from '@/store/tabBarConfigSlice';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { palette } from '@/constants/Colors';
|
||||
|
||||
export default function TabBarConfigScreen() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const safeAreaTop = useSafeAreaTop(60);
|
||||
const configs = useAppSelector(selectTabBarConfigs);
|
||||
const isGlassAvailable = isLiquidGlassAvailable();
|
||||
|
||||
// 处理开关切换
|
||||
const handleToggle = useCallback(
|
||||
(tabId: string) => {
|
||||
dispatch(toggleTabEnabled(tabId));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// 恢复默认设置
|
||||
const handleReset = useCallback(() => {
|
||||
Alert.alert(
|
||||
t('personal.tabBarConfig.resetConfirm.title'),
|
||||
t('personal.tabBarConfig.resetConfirm.message'),
|
||||
[
|
||||
{
|
||||
text: t('personal.tabBarConfig.resetConfirm.cancel'),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: t('personal.tabBarConfig.resetConfirm.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
dispatch(resetToDefault());
|
||||
Alert.alert('', t('personal.tabBarConfig.resetSuccess'));
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [dispatch, t]);
|
||||
|
||||
// 渲染单个 Tab 行
|
||||
const renderTabRow = useCallback(
|
||||
(item: TabConfig, index: number, total: number) => {
|
||||
return (
|
||||
<View key={item.id}>
|
||||
<View style={styles.tabItem}>
|
||||
{/* Tab 图标和名称 */}
|
||||
<View style={styles.tabInfo}>
|
||||
<View style={styles.iconContainer}>
|
||||
<IconSymbol name={item.icon as any} size={24} color="#9370DB" />
|
||||
</View>
|
||||
<View style={styles.tabTextContainer}>
|
||||
<Text style={styles.tabTitle}>{t(item.titleKey)}</Text>
|
||||
{!item.canBeDisabled && (
|
||||
<Text style={styles.tabSubtitle}>
|
||||
{t('personal.tabBarConfig.cannotDisable')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 开关 */}
|
||||
<Switch
|
||||
value={item.enabled}
|
||||
onValueChange={() => handleToggle(item.id)}
|
||||
disabled={!item.canBeDisabled}
|
||||
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||||
thumbColor="#FFFFFF"
|
||||
style={styles.switch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 分割线 - 最后一项不显示 */}
|
||||
{index < total - 1 && (
|
||||
<View style={styles.separatorContainer}>
|
||||
<View style={styles.separator} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[handleToggle, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[palette.purple[100], '#F5F5F5']}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0.3, y: 0.4 }}
|
||||
style={styles.gradientBackground}
|
||||
/>
|
||||
|
||||
{/* 顶部导航栏 */}
|
||||
<HeaderBar
|
||||
title={t('personal.tabBarConfig.title')}
|
||||
onBack={() => router.back()}
|
||||
right={
|
||||
<TouchableOpacity onPress={handleReset} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||
<Text style={styles.headerRightButton}>
|
||||
{t('personal.tabBarConfig.resetButton')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingTop: safeAreaTop }]} // 增加顶部间距,因为 HeaderBar 现在是 absolute 的
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* 说明区域 */}
|
||||
<View style={styles.headerSection}>
|
||||
<Text style={styles.subtitle}>{t('personal.tabBarConfig.subtitle')}</Text>
|
||||
<View style={styles.descriptionCard}>
|
||||
<View style={styles.hintRow}>
|
||||
<Ionicons name="information-circle-outline" size={20} color="#9370DB" />
|
||||
<Text style={styles.descriptionText}>
|
||||
{t('personal.tabBarConfig.description')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Tab 列表 - 聚合在一个卡片中 */}
|
||||
<View style={styles.sectionContainer}>
|
||||
{configs.map((item, index) => renderTabRow(item, index, configs.length))}
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: '60%', // 渐变覆盖上半部分即可
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
headerSection: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#6C757D',
|
||||
marginBottom: 12,
|
||||
},
|
||||
descriptionCard: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(147, 112, 219, 0.1)',
|
||||
},
|
||||
hintRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
descriptionText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
color: '#2C3E50',
|
||||
lineHeight: 18,
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
marginBottom: 20,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
tabItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
separatorContainer: {
|
||||
paddingLeft: 68, // 40(icon) + 12(gap) + 16(padding)
|
||||
paddingRight: 16,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: '#F0F0F0',
|
||||
},
|
||||
tabInfo: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
tabTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
tabTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#2C3E50',
|
||||
marginBottom: 2,
|
||||
},
|
||||
tabSubtitle: {
|
||||
fontSize: 12,
|
||||
color: '#9370DB',
|
||||
},
|
||||
switch: {
|
||||
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
|
||||
},
|
||||
headerRightButton: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#9370DB', // 使用主色调
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user