Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
188
src/components/common/AppDialogHost.tsx
Normal file
188
src/components/common/AppDialogHost.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Modal, Pressable, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import type { AlertButton } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
||||
import { bindDialogListener, DialogPayload } from '../../services/dialogService';
|
||||
import { borderRadius, colors, shadows, spacing } from '../../theme';
|
||||
|
||||
const AppDialogHost: React.FC = () => {
|
||||
const [dialog, setDialog] = useState<DialogPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unbind = bindDialogListener((payload) => {
|
||||
setDialog(payload);
|
||||
});
|
||||
return unbind;
|
||||
}, []);
|
||||
|
||||
const actions = useMemo<AlertButton[]>(() => {
|
||||
if (!dialog?.actions?.length) return [{ text: '确定' }];
|
||||
return dialog.actions.slice(0, 3);
|
||||
}, [dialog]);
|
||||
|
||||
const onClose = () => {
|
||||
const cancelAction = actions.find((action) => action.style === 'cancel');
|
||||
if (cancelAction?.onPress) {
|
||||
cancelAction.onPress();
|
||||
}
|
||||
setDialog(null);
|
||||
};
|
||||
|
||||
const onActionPress = (action: AlertButton) => {
|
||||
action.onPress?.();
|
||||
setDialog(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={!!dialog}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
statusBarTranslucent
|
||||
>
|
||||
<Pressable
|
||||
style={styles.backdrop}
|
||||
onPress={() => {
|
||||
if (dialog?.options?.cancelable ?? true) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Pressable style={styles.container}>
|
||||
<View style={styles.iconHeader}>
|
||||
<LinearGradient
|
||||
colors={['#FF6B35', '#FF8F66']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.iconBadge}
|
||||
>
|
||||
<MaterialCommunityIcons name="carrot" size={20} color="#FFFFFF" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<Text style={styles.title}>{dialog?.title || '提示'}</Text>
|
||||
{!!dialog?.message && <Text style={styles.message}>{dialog.message}</Text>}
|
||||
|
||||
<View style={styles.actions}>
|
||||
{actions.map((action, index) => {
|
||||
const isDestructive = action.style === 'destructive';
|
||||
const isCancel = action.style === 'cancel';
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`${action.text || 'action'}-${index}`}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
isCancel && styles.cancelButton,
|
||||
!isCancel && !isDestructive && styles.primaryButton,
|
||||
isDestructive && styles.destructiveButton,
|
||||
]}
|
||||
onPress={() => onActionPress(action)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
!isCancel && !isDestructive && styles.primaryText,
|
||||
isCancel && styles.cancelText,
|
||||
isDestructive && styles.destructiveText,
|
||||
]}
|
||||
>
|
||||
{action.text || '确定'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.36)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
container: {
|
||||
width: '100%',
|
||||
maxWidth: 380,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius['2xl'],
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.lg,
|
||||
...shadows.lg,
|
||||
},
|
||||
iconHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
iconBadge: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: borderRadius.full,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: colors.text.primary,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
marginTop: spacing.md,
|
||||
color: colors.text.secondary,
|
||||
fontSize: 14,
|
||||
lineHeight: 21,
|
||||
textAlign: 'center',
|
||||
},
|
||||
actions: {
|
||||
marginTop: spacing.xl,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
actionButton: {
|
||||
height: 46,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: `${colors.primary.main}28`,
|
||||
backgroundColor: '#FFFFFF',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
destructiveButton: {
|
||||
backgroundColor: '#FEECEC',
|
||||
borderColor: '#FCD4D1',
|
||||
},
|
||||
actionText: {
|
||||
color: colors.text.primary,
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
},
|
||||
primaryText: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
cancelText: {
|
||||
color: colors.text.secondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
destructiveText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
});
|
||||
|
||||
export default AppDialogHost;
|
||||
Reference in New Issue
Block a user