2026-03-09 21:29:03 +08:00
|
|
|
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';
|
2026-03-25 05:16:54 +08:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
const AppDialogHost: React.FC = () => {
|
|
|
|
|
const [dialog, setDialog] = useState<DialogPayload | null>(null);
|
2026-03-25 05:16:54 +08:00
|
|
|
const colors = useAppColors();
|
|
|
|
|
const styles = useMemo(() => createDialogHostStyles(colors), [colors]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const unbind = bindDialogListener((payload) => {
|
|
|
|
|
setDialog(payload);
|
|
|
|
|
});
|
|
|
|
|
return unbind;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const actions = useMemo<AlertButton[]>(() => {
|
|
|
|
|
if (!dialog?.actions?.length) return [{ text: '确定' }];
|
2026-03-25 05:16:54 +08:00
|
|
|
return dialog.actions;
|
2026-03-09 21:29:03 +08:00
|
|
|
}, [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;
|