Files
frontend/src/components/common/AppDialogHost.tsx

193 lines
5.4 KiB
TypeScript
Raw Normal View History

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, shadows, spacing, useAppColors, type AppColors } from '../../theme';
function createDialogHostStyles(colors: AppColors) {
return 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: colors.background.paper,
justifyContent: 'center',
alignItems: 'center',
},
primaryButton: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
cancelButton: {
backgroundColor: colors.background.paper,
borderColor: colors.divider,
},
destructiveButton: {
backgroundColor: `${colors.error.main}18`,
borderColor: `${colors.error.main}40`,
},
actionText: {
color: colors.text.primary,
fontSize: 15,
fontWeight: '700',
},
primaryText: {
color: colors.text.inverse,
},
cancelText: {
color: colors.text.secondary,
fontWeight: '600',
},
destructiveText: {
color: colors.error.main,
},
});
}
const AppDialogHost: React.FC = () => {
const [dialog, setDialog] = useState<DialogPayload | null>(null);
const colors = useAppColors();
const styles = useMemo(() => createDialogHostStyles(colors), [colors]);
useEffect(() => {
const unbind = bindDialogListener((payload) => {
setDialog(payload);
});
return unbind;
}, []);
const actions = useMemo<AlertButton[]>(() => {
if (!dialog?.actions?.length) return [{ text: '确定' }];
return dialog.actions;
}, [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>
);
};
export default AppDialogHost;