You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
107 lines
2.7 KiB
107 lines
2.7 KiB
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,
|
|
},
|
|
});
|
|
|