Browse Source
- src/components/StateView 提供 LoadingState/ErrorState(可重试)/EmptyState,颜色走主题 token - privacy 页复用 StateView 去除重复 loading/error 样板 - favorites/history 占位页接入 EmptyState 统一空态样式master
4 changed files with 124 additions and 92 deletions
@ -1,36 +1,14 @@ |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { StyleSheet, Text, View } from 'react-native'; |
|||
import { colors } from '../src/theme'; |
|||
import { EmptyState } from '../src/components/StateView'; |
|||
|
|||
/** |
|||
* 我的收藏页(占位) |
|||
*/ |
|||
export default function FavoritesScreen() { |
|||
return ( |
|||
<View style={styles.container}> |
|||
<Ionicons name="heart-outline" size={48} color={colors.textSecondary} /> |
|||
<Text style={styles.title}>我的收藏</Text> |
|||
<Text style={styles.subtitle}>收藏页面开发中,敬请期待</Text> |
|||
</View> |
|||
<EmptyState |
|||
icon="heart-outline" |
|||
title="我的收藏" |
|||
subtitle="收藏页面开发中,敬请期待" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
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, |
|||
}, |
|||
}); |
|||
@ -1,36 +1,14 @@ |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { StyleSheet, Text, View } from 'react-native'; |
|||
import { colors } from '../src/theme'; |
|||
import { EmptyState } from '../src/components/StateView'; |
|||
|
|||
/** |
|||
* 浏览历史页(占位) |
|||
*/ |
|||
export default function HistoryScreen() { |
|||
return ( |
|||
<View style={styles.container}> |
|||
<Ionicons name="time-outline" size={48} color={colors.textSecondary} /> |
|||
<Text style={styles.title}>浏览历史</Text> |
|||
<Text style={styles.subtitle}>浏览历史页面开发中,敬请期待</Text> |
|||
</View> |
|||
<EmptyState |
|||
icon="time-outline" |
|||
title="浏览历史" |
|||
subtitle="浏览历史页面开发中,敬请期待" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
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, |
|||
}, |
|||
}); |
|||
@ -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…
Reference in new issue