Browse Source

refactor: 新增共享 StateView 三态组件并接入数据/占位页

- src/components/StateView 提供 LoadingState/ErrorState(可重试)/EmptyState,颜色走主题 token
- privacy 页复用 StateView 去除重复 loading/error 样板
- favorites/history 占位页接入 EmptyState 统一空态样式
master
doublex001 2 months ago
parent
commit
7e849de548
  1. 34
      app/favorites.tsx
  2. 34
      app/history.tsx
  3. 41
      app/settings/privacy.tsx
  4. 107
      src/components/StateView.tsx

34
app/favorites.tsx

@ -1,36 +1,14 @@
import { Ionicons } from '@expo/vector-icons'; import { EmptyState } from '../src/components/StateView';
import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../src/theme';
/** /**
* 我的收藏页(占位) * 我的收藏页(占位)
*/ */
export default function FavoritesScreen() { export default function FavoritesScreen() {
return ( return (
<View style={styles.container}> <EmptyState
<Ionicons name="heart-outline" size={48} color={colors.textSecondary} /> icon="heart-outline"
<Text style={styles.title}>我的收藏</Text> title="我的收藏"
<Text style={styles.subtitle}>收藏页面开发中,敬请期待</Text> subtitle="收藏页面开发中,敬请期待"
</View> />
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.pageBg,
alignItems: 'center',
justifyContent: 'center',
},
title: {
marginTop: 12,
fontSize: 20,
fontWeight: '700',
color: colors.textPrimary,
},
subtitle: {
marginTop: 8,
fontSize: 13,
color: colors.textSecondary,
},
});

34
app/history.tsx

@ -1,36 +1,14 @@
import { Ionicons } from '@expo/vector-icons'; import { EmptyState } from '../src/components/StateView';
import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../src/theme';
/** /**
* 浏览历史页(占位) * 浏览历史页(占位)
*/ */
export default function HistoryScreen() { export default function HistoryScreen() {
return ( return (
<View style={styles.container}> <EmptyState
<Ionicons name="time-outline" size={48} color={colors.textSecondary} /> icon="time-outline"
<Text style={styles.title}>浏览历史</Text> title="浏览历史"
<Text style={styles.subtitle}>浏览历史页面开发中,敬请期待</Text> subtitle="浏览历史页面开发中,敬请期待"
</View> />
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.pageBg,
alignItems: 'center',
justifyContent: 'center',
},
title: {
marginTop: 12,
fontSize: 20,
fontWeight: '700',
color: colors.textPrimary,
},
subtitle: {
marginTop: 8,
fontSize: 13,
color: colors.textSecondary,
},
});

41
app/settings/privacy.tsx

@ -1,8 +1,9 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; import { Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { usePrivacyCenter } from '../../src/hooks'; import { usePrivacyCenter } from '../../src/hooks';
import { ErrorState, LoadingState } from '../../src/components/StateView';
import { colors } from '../../src/theme'; import { colors } from '../../src/theme';
import type { PrivacyLevelInfo, PrivacySettingItem, PrivacySection } from '../../src/types'; import type { PrivacyLevelInfo, PrivacySettingItem, PrivacySection } from '../../src/types';
@ -40,7 +41,7 @@ function PrivacyLevelCard({ level }: { level: PrivacyLevelInfo }) {
<Text style={styles.levelDesc}>{level.description}</Text> <Text style={styles.levelDesc}>{level.description}</Text>
</View> </View>
</View> </View>
<Pressable style={styles.levelBtn} onPress={() => console.log('进一步提升隐私')}> <Pressable style={styles.levelBtn} onPress={() => {}}>
<Text style={[styles.levelBtnText, { color: theme.text }]}>查看隐私报告</Text> <Text style={[styles.levelBtnText, { color: theme.text }]}>查看隐私报告</Text>
<Ionicons name="chevron-forward" size={14} color={theme.text} /> <Ionicons name="chevron-forward" size={14} color={theme.text} />
</Pressable> </Pressable>
@ -68,7 +69,6 @@ function SettingRow({
router.push(item.route); router.push(item.route);
return; return;
} }
console.log('[PrivacyCenter] 点击', item.id);
}; };
if (item.type === 'switch') { if (item.type === 'switch') {
@ -136,9 +136,7 @@ export default function PrivacyCenterScreen() {
return ( return (
<SafeAreaView style={styles.safeArea} edges={['top']}> <SafeAreaView style={styles.safeArea} edges={['top']}>
<Header onBack={() => router.back()} /> <Header onBack={() => router.back()} />
<View style={styles.stateBox}> <LoadingState />
<ActivityIndicator size="large" color={colors.primary} />
</View>
</SafeAreaView> </SafeAreaView>
); );
} }
@ -147,13 +145,7 @@ export default function PrivacyCenterScreen() {
return ( return (
<SafeAreaView style={styles.safeArea} edges={['top']}> <SafeAreaView style={styles.safeArea} edges={['top']}>
<Header onBack={() => router.back()} /> <Header onBack={() => router.back()} />
<View style={styles.stateBox}> <ErrorState message="加载失败,请重试" onRetry={refresh} />
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} />
<Text style={styles.stateText}>加载失败,请重试</Text>
<Pressable style={styles.retryBtn} onPress={refresh}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
</View>
</SafeAreaView> </SafeAreaView>
); );
} }
@ -214,29 +206,6 @@ const styles = StyleSheet.create({
padding: 12, padding: 12,
paddingBottom: 24, paddingBottom: 24,
}, },
stateBox: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
stateText: {
marginTop: 12,
fontSize: 14,
color: colors.textSecondary,
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
levelCard: { levelCard: {
borderRadius: 12, borderRadius: 12,
padding: 16, padding: 16,

107
src/components/StateView.tsx

@ -0,0 +1,107 @@
import { Ionicons } from '@expo/vector-icons';
import type { ReactNode } from 'react';
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../theme';
/* ===================== 通用状态视图 =====================
* 统一承载「加载中 / 加载失败 / 空数据」三态,供数据页复用,
* 避免各页面重复编写 ActivityIndicator + 重试按钮样板代码。
* 颜色全部走主题 token,遵守 AGENTS.md 配色约束。
*/
function StateBox({ children }: { children: ReactNode }) {
return <View style={styles.box}>{children}</View>;
}
/** 加载中 */
export function LoadingState({ text = '加载中…' }: { text?: string }) {
return (
<StateBox>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.text}>{text}</Text>
</StateBox>
);
}
/** 加载失败(可重试) */
export function ErrorState({
message = '加载失败,请重试',
onRetry,
}: {
message?: string;
onRetry?: () => void;
}) {
return (
<StateBox>
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} />
<Text style={styles.text}>{message}</Text>
{onRetry ? (
<Pressable style={styles.retryBtn} onPress={onRetry}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
) : null}
</StateBox>
);
}
/** 空数据 / 占位 */
export function EmptyState({
icon = 'document-text-outline',
title,
subtitle,
actionLabel,
onAction,
}: {
icon?: keyof typeof Ionicons.glyphMap;
title: string;
subtitle?: string;
actionLabel?: string;
onAction?: () => void;
}) {
return (
<StateBox>
<Ionicons name={icon} size={48} color={colors.textSecondary} />
<Text style={styles.title}>{title}</Text>
{subtitle ? <Text style={styles.text}>{subtitle}</Text> : null}
{actionLabel && onAction ? (
<Pressable style={styles.retryBtn} onPress={onAction}>
<Text style={styles.retryText}>{actionLabel}</Text>
</Pressable>
) : null}
</StateBox>
);
}
const styles = StyleSheet.create({
box: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
paddingHorizontal: 32,
},
text: {
marginTop: 4,
fontSize: 14,
color: colors.textSecondary,
textAlign: 'center',
},
title: {
marginTop: 12,
fontSize: 20,
fontWeight: '700',
color: colors.textPrimary,
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
});
Loading…
Cancel
Save