修改弹窗模块及配色

This commit is contained in:
李宇奇 2025-07-09 17:16:24 +08:00
parent b6c8db84a8
commit b80d368d5e
7 changed files with 995 additions and 58 deletions

12
App.tsx
View File

@ -5,15 +5,17 @@
* @format * @format
*/ */
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect, useRef} from 'react';
import Navigation from './src/navigation'; import Navigation from './src/navigation';
import {ThemeProvider} from './src/contexts/ThemeContext'; import {ThemeProvider} from './src/contexts/ThemeContext';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {View, Text, ActivityIndicator, StyleSheet} from 'react-native'; import {View, Text, ActivityIndicator, StyleSheet} from 'react-native';
import {BeautifulDialog, DialogUtils} from './src/utils/DialogUtils';
function App(): JSX.Element { function App(): JSX.Element {
const [fontLoaded, setFontLoaded] = useState(false); const [fontLoaded, setFontLoaded] = useState(false);
const [fontError, setFontError] = useState<string | null>(null); const [fontError, setFontError] = useState<string | null>(null);
const dialogRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
const loadFont = async () => { const loadFont = async () => {
@ -44,6 +46,13 @@ function App(): JSX.Element {
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, []); }, []);
// 设置弹窗引用
useEffect(() => {
if (dialogRef.current) {
DialogUtils.setDialogRef(dialogRef.current);
}
}, [fontLoaded]);
// 显示加载状态或错误信息 // 显示加载状态或错误信息
if (!fontLoaded || fontError) { if (!fontLoaded || fontError) {
return ( return (
@ -60,6 +69,7 @@ function App(): JSX.Element {
return ( return (
<ThemeProvider> <ThemeProvider>
<Navigation /> <Navigation />
<BeautifulDialog ref={dialogRef} />
</ThemeProvider> </ThemeProvider>
); );
} }

27
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pda_react_native_template", "name": "pda_react_native_template",
"version": "0.0.1", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pda_react_native_template", "name": "pda_react_native_template",
"version": "0.0.1", "version": "1.0.0",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
@ -20,6 +20,7 @@
"react-native": "0.75.5", "react-native": "0.75.5",
"react-native-chart-kit": "^6.12.0", "react-native-chart-kit": "^6.12.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-modal": "^14.0.0-rc.1",
"react-native-safe-area-context": "^5.5.1", "react-native-safe-area-context": "^5.5.1",
"react-native-screens": "^4.5.0", "react-native-screens": "^4.5.0",
"react-native-svg": "^15.12.0", "react-native-svg": "^15.12.0",
@ -13322,6 +13323,15 @@
} }
} }
}, },
"node_modules/react-native-animatable": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz",
"integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
}
},
"node_modules/react-native-chart-kit": { "node_modules/react-native-chart-kit": {
"version": "6.12.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz", "resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz",
@ -13368,6 +13378,19 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-modal": {
"version": "14.0.0-rc.1",
"resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
"integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==",
"license": "MIT",
"dependencies": {
"react-native-animatable": "1.4.0"
},
"peerDependencies": {
"react": "*",
"react-native": ">=0.70.0"
}
},
"node_modules/react-native-safe-area-context": { "node_modules/react-native-safe-area-context": {
"version": "5.5.1", "version": "5.5.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.1.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.1.tgz",

View File

@ -22,6 +22,7 @@
"react-native": "0.75.5", "react-native": "0.75.5",
"react-native-chart-kit": "^6.12.0", "react-native-chart-kit": "^6.12.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-modal": "^14.0.0-rc.1",
"react-native-safe-area-context": "^5.5.1", "react-native-safe-area-context": "^5.5.1",
"react-native-screens": "^4.5.0", "react-native-screens": "^4.5.0",
"react-native-svg": "^15.12.0", "react-native-svg": "^15.12.0",

View File

@ -5,7 +5,6 @@ import {
StyleSheet, StyleSheet,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
Alert,
SafeAreaView, SafeAreaView,
ActivityIndicator, ActivityIndicator,
} from 'react-native'; } from 'react-native';
@ -17,6 +16,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import Svg, {Path} from 'react-native-svg'; import Svg, {Path} from 'react-native-svg';
import axios from 'axios'; import axios from 'axios';
import {DialogUtils} from '../../utils';
interface StockInEmptyResponse { interface StockInEmptyResponse {
code: number; code: number;
@ -41,9 +41,7 @@ export const StockInEmpty: React.FC = () => {
// 空托入库方法 // 空托入库方法
const handleEmptyIn = async () => { const handleEmptyIn = async () => {
if (!vehicleNo.trim()) { if (!vehicleNo.trim()) {
Alert.alert('警告', '请先填写载具号', [ DialogUtils.showWarningMessage('警告', '请先填写载具号', '返回填写');
{text: '返回填写', style: 'cancel'},
]);
return; return;
} }
@ -55,29 +53,29 @@ export const StockInEmpty: React.FC = () => {
); );
if (response.status !== 200) { if (response.status !== 200) {
Alert.alert('警告', '服务器请求失败', [ DialogUtils.showWarningMessage('警告', '服务器请求失败', '我知道了');
{text: '我知道了', style: 'cancel'},
]);
return; return;
} }
const responseData = response.data; const responseData = response.data;
if (responseData.code === 200) { if (responseData.code === 200) {
Alert.alert('成功', '', [{text: '我知道了', style: 'default'}]); DialogUtils.showSuccessMessage('成功', '空载具入库成功', '我知道了');
setVehicleNo(''); setVehicleNo('');
} else { } else {
Alert.alert('警告', `服务器返回失败:${responseData.message}`, [ DialogUtils.showWarningMessage(
{text: '我知道了', style: 'cancel'}, '警告',
]); `服务器返回失败:${responseData.message}`,
'我知道了',
);
} }
} catch (error) { } catch (error) {
Alert.alert( DialogUtils.showErrorMessage(
'请求发生错误', '请求发生错误',
`请求服务器发生错误:${ `请求服务器发生错误:${
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
}`, }`,
[{text: '我知道了', style: 'cancel'}], '我知道了',
); );
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -6,7 +6,6 @@ import {
ScrollView, ScrollView,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
Alert,
ActivityIndicator, ActivityIndicator,
} from 'react-native'; } from 'react-native';
import {useNavigation} from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native';
@ -14,6 +13,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import {useTheme} from '../../contexts/ThemeContext'; import {useTheme} from '../../contexts/ThemeContext';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import Svg, {Path} from 'react-native-svg'; import Svg, {Path} from 'react-native-svg';
import {DialogUtils, MessageItem} from '../../utils';
interface PackageDataItem { interface PackageDataItem {
id: string; id: string;
@ -55,21 +55,24 @@ export const StockInManual: React.FC = () => {
if (code.length === 15) { if (code.length === 15) {
setLastProcessedVehicleCode(code); setLastProcessedVehicleCode(code);
Alert.alert('扫码成功', `绑定托盘: ${code}`); DialogUtils.showSuccessMessage('扫码成功', `绑定托盘: ${code}`);
} else { } else {
Alert.alert('绑定托盘失败', '无效的载具号长度'); DialogUtils.showErrorMessage('绑定托盘失败', '无效的载具号长度');
} }
}, [vehicleCode, lastProcessedVehicleCode]); }, [vehicleCode, lastProcessedVehicleCode]);
const resolveCode = useCallback(() => { const resolveCode = useCallback(() => {
if (!goodsCode) { if (!goodsCode) {
Alert.alert('警告', '条码文本框内无数据,请先扫描或者输入数据'); DialogUtils.showWarningMessage(
'警告',
'条码文本框内无数据,请先扫描或者输入数据',
);
return; return;
} }
const codeData = goodsCode.split(','); const codeData = goodsCode.split(',');
if (![6, 7, 8].includes(codeData.length)) { if (![6, 7, 8].includes(codeData.length)) {
Alert.alert('警告', '条码格式错误'); DialogUtils.showWarningMessage('警告', '条码格式错误');
return; return;
} }
@ -101,7 +104,7 @@ export const StockInManual: React.FC = () => {
setPackageData(prev => [...prev, newItem]); setPackageData(prev => [...prev, newItem]);
setPackageDataId(prev => prev + 1); setPackageDataId(prev => prev + 1);
} else { } else {
Alert.alert('警告', '该物料批次已存在,不能重复添加'); DialogUtils.showWarningMessage('警告', '该物料批次已存在,不能重复添加');
} }
setGoodsCode(''); setGoodsCode('');
@ -130,44 +133,38 @@ export const StockInManual: React.FC = () => {
}; };
const modifyNumber = (_id: string) => { const modifyNumber = (_id: string) => {
Alert.alert('修改数量', '请输入新的数量', [ DialogUtils.showConfirmMessage('修改数量', '请输入新的数量', {
{text: '取消', style: 'cancel'}, cancelLabel: '取消',
{ confirmLabel: '确定',
text: '确定', confirm: () => {
onPress: () => {
// TODO: 需要实现一个自定义的输入对话框 // TODO: 需要实现一个自定义的输入对话框
Alert.alert('提示', '此功能暂不可用,请稍后再试'); DialogUtils.showMessage('提示', '此功能暂不可用,请稍后再试');
}, },
}, });
]);
}; };
const showDetails = (item: PackageDataItem) => { const showDetails = (item: PackageDataItem) => {
const message = [ const message: MessageItem[] = [
{label: '序号:', msg: item.id}, {label: '序号', msg: item.id},
{label: '采购单号:', msg: item.segment1}, {label: '采购单号', msg: item.segment1},
{label: '物料号:', msg: item.itemId}, {label: '物料号', msg: item.itemId},
{label: '批次号:', msg: item.batch}, {label: '批次号', msg: item.batch},
{label: '数量:', msg: item.quantity}, {label: '数量', msg: item.quantity},
{label: '重量:', msg: item.weight}, {label: '重量', msg: item.weight},
{label: '生产日期:', msg: item.productData}, {label: '生产日期', msg: item.productData},
]; ];
Alert.alert( DialogUtils.showMessageList('数据详情', message, '我知道了');
'数据详情',
message.map(m => `${m.label} ${m.msg}`).join('\n'),
[{text: '我知道了'}],
);
}; };
const wheelComplete = async () => { const wheelComplete = async () => {
if (packageData.length === 0) { if (packageData.length === 0) {
Alert.alert('警告', '您的码盘数据为空'); DialogUtils.showWarningMessage('警告', '您的码盘数据为空');
return; return;
} }
if (!vehicleCode.trim()) { if (!vehicleCode.trim()) {
Alert.alert('警告', '请先扫描载具号'); DialogUtils.showWarningMessage('警告', '请先扫描载具号');
return; return;
} }
@ -175,16 +172,18 @@ export const StockInManual: React.FC = () => {
const factoryIndex = 2; // 默认二厂 const factoryIndex = 2; // 默认二厂
const statusIndex = 1; // 默认合格 const statusIndex = 1; // 默认合格
const confirm = await new Promise(resolve => const confirm = await new Promise<boolean>(resolve => {
Alert.alert( DialogUtils.showConfirmMessage(
'码盘完成', '码盘完成',
`载具:${vehicleCode} 码盘 ${packageData.length} 条数据,是否继续?`, `载具:${vehicleCode} 码盘 ${packageData.length} 条数据,是否继续?`,
[ {
{text: '取消', style: 'cancel', onPress: () => resolve(false)}, cancelLabel: '取消',
{text: '继续', onPress: () => resolve(true)}, confirmLabel: '继续',
], cancel: () => resolve(false),
), confirm: () => resolve(true),
},
); );
});
if (!confirm) return; if (!confirm) return;
@ -211,17 +210,24 @@ export const StockInManual: React.FC = () => {
setPackageDataId(0); setPackageDataId(0);
setVehicleCode(''); setVehicleCode('');
setPackageData([]); setPackageData([]);
Alert.alert('码盘成功', '', [{text: '我知道了'}]); DialogUtils.showSuccessMessage(
'码盘成功',
'码盘操作已完成',
'我知道了',
);
} else { } else {
Alert.alert('警告', `服务器返回失败:${data.message}`); DialogUtils.showWarningMessage(
'警告',
`服务器返回失败:${data.message}`,
);
} }
} catch (error) { } catch (error) {
Alert.alert( DialogUtils.showErrorMessage(
'请求发生错误', '请求发生错误',
`请求服务器发生错误:${ `请求服务器发生错误:${
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
}`, }`,
[{text: '我知道了', style: 'cancel'}], '我知道了',
); );
} finally { } finally {
setLoading(false); setLoading(false);

893
src/utils/DialogUtils.tsx Normal file
View File

@ -0,0 +1,893 @@
import React, {useState, useRef} from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Dimensions,
ScrollView,
Animated,
StatusBar,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import LinearGradient from 'react-native-linear-gradient';
import {theme} from '../constants/theme';
const {width, height} = Dimensions.get('window');
export interface MessageItem {
label: string;
msg: string;
}
export interface DialogConfig {
type: 'success' | 'warning' | 'error' | 'info' | 'confirm';
title: string;
message?: string;
messageList?: MessageItem[];
buttons?: DialogButton[];
onConfirm?: () => void;
onCancel?: () => void;
confirmText?: string;
cancelText?: string;
showIcon?: boolean;
}
interface DialogButton {
text: string;
onPress?: () => void;
style?: 'default' | 'cancel' | 'destructive';
}
// 美观的弹窗组件
interface BeautifulDialogProps {
ref?: any;
}
export const BeautifulDialog: React.FC<BeautifulDialogProps> = React.forwardRef(
(props, ref) => {
const [visible, setVisible] = useState(false);
const [config, setConfig] = useState<DialogConfig | null>(null);
const scaleAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(0)).current;
React.useImperativeHandle(ref, () => ({
show: (dialogConfig: DialogConfig) => {
setConfig(dialogConfig);
setVisible(true);
// 动画效果
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
]).start();
},
hide: () => {
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 0,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
]).start(() => {
setVisible(false);
setConfig(null);
});
},
}));
const handleClose = () => {
const dialogRef = (ref as any)?.current || ref;
if (dialogRef?.hide) {
dialogRef.hide();
}
};
const handleConfirm = () => {
config?.onConfirm?.();
handleClose();
};
const handleCancel = () => {
config?.onCancel?.();
handleClose();
};
const getIconConfig = () => {
switch (config?.type) {
case 'success':
return {
name: 'check-circle',
color: theme.colors.cyan,
bgColor: '#e6f7ff',
};
case 'warning':
return {
name: 'warning',
color: theme.colors.cyan,
bgColor: '#e6f7ff',
};
case 'error':
return {name: 'error', color: theme.colors.cyan, bgColor: '#e6f7ff'};
case 'confirm':
return {name: 'help', color: theme.colors.cyan, bgColor: '#e6f7ff'};
default:
return {name: 'info', color: theme.colors.cyan, bgColor: '#e6f7ff'};
}
};
const getGradientColors = () => {
switch (config?.type) {
case 'success':
return [theme.colors.cyan, theme.colors.aqua];
case 'warning':
return [theme.colors.cyan, theme.colors.aqua];
case 'error':
return [theme.colors.cyan, theme.colors.aqua];
case 'confirm':
return [theme.colors.cyan, theme.colors.aqua];
default:
return [theme.colors.cyan, theme.colors.aqua];
}
};
if (!visible || !config) return null;
const iconConfig = getIconConfig();
const gradientColors = getGradientColors();
return (
<Modal
visible={visible}
transparent
animationType="none"
statusBarTranslucent
onRequestClose={handleClose}>
<StatusBar
backgroundColor="rgba(0, 0, 0, 0.5)"
barStyle="light-content"
/>
<Animated.View style={[styles.overlay, {opacity: opacityAnim}]}>
<Animated.View
style={[styles.dialogContainer, {transform: [{scale: scaleAnim}]}]}>
{/* 头部图标区域 */}
<View
style={[
styles.iconContainer,
{backgroundColor: iconConfig.bgColor},
]}>
<View style={styles.iconWrapper}>
<Icon
name={iconConfig.name}
size={32}
color={iconConfig.color}
/>
</View>
</View>
{/* 内容区域 */}
<View style={styles.contentContainer}>
<Text style={styles.title}>{config.title}</Text>
{config.message && (
<Text style={styles.message}>{config.message}</Text>
)}
{config.messageList && (
<View style={styles.messageListContainer}>
{config.messageList.map((item, index) => (
<View key={index} style={styles.messageItem}>
<Text style={styles.messageLabel}>{item.label}</Text>
<Text style={styles.messageValue}>{item.msg}</Text>
</View>
))}
</View>
)}
</View>
{/* 按钮区域 */}
<View style={styles.buttonContainer}>
{config.type === 'confirm' ? (
<>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={handleCancel}>
<Text style={styles.cancelButtonText}>
{config.cancelText || '取消'}
</Text>
</TouchableOpacity>
<LinearGradient
colors={gradientColors}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[styles.button, styles.confirmButton]}>
<TouchableOpacity
style={styles.buttonTouchable}
onPress={handleConfirm}>
<Text style={styles.confirmButtonText}>
{config.confirmText || '确定'}
</Text>
</TouchableOpacity>
</LinearGradient>
</>
) : (
<LinearGradient
colors={gradientColors}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[styles.button, styles.singleButton]}>
<TouchableOpacity
style={styles.buttonTouchable}
onPress={handleConfirm}>
<Text style={styles.confirmButtonText}>
{config.confirmText || '确定'}
</Text>
</TouchableOpacity>
</LinearGradient>
)}
</View>
</Animated.View>
</Animated.View>
</Modal>
);
},
);
// 输入框弹窗组件
interface InputDialogProps {
visible: boolean;
title: string;
message?: string;
hintText?: string;
cancelLabel?: string;
confirmLabel?: string;
onCancel: () => void;
onConfirm: (value: string) => void;
}
export const InputDialog: React.FC<InputDialogProps> = ({
visible,
title,
message,
hintText,
cancelLabel = '取消',
confirmLabel = '确定',
onCancel,
onConfirm,
}) => {
const [inputValue, setInputValue] = useState('');
const scaleAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(0)).current;
React.useEffect(() => {
if (visible) {
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
]).start();
} else {
scaleAnim.setValue(0);
opacityAnim.setValue(0);
}
}, [visible, opacityAnim, scaleAnim]);
const handleConfirm = () => {
onConfirm(inputValue);
setInputValue('');
};
const handleCancel = () => {
onCancel();
setInputValue('');
};
if (!visible) return null;
return (
<Modal
visible={visible}
transparent
animationType="none"
statusBarTranslucent
onRequestClose={handleCancel}>
<StatusBar
backgroundColor="rgba(0, 0, 0, 0.5)"
barStyle="light-content"
/>
<Animated.View style={[styles.overlay, {opacity: opacityAnim}]}>
<Animated.View
style={[styles.dialogContainer, {transform: [{scale: scaleAnim}]}]}>
{/* 头部图标 */}
<View style={[styles.iconContainer, {backgroundColor: '#e6f7ff'}]}>
<View style={styles.iconWrapper}>
<Icon name="edit" size={32} color={theme.colors.cyan} />
</View>
</View>
{/* 内容区域 */}
<View style={styles.contentContainer}>
<Text style={styles.title}>{title}</Text>
{message && <Text style={styles.message}>{message}</Text>}
<View style={styles.inputContainer}>
<TextInput
style={styles.textInput}
placeholder={hintText}
placeholderTextColor="#999"
value={inputValue}
onChangeText={setInputValue}
maxLength={1000}
autoFocus
multiline={false}
/>
</View>
</View>
{/* 按钮区域 */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={handleCancel}>
<Text style={styles.cancelButtonText}>{cancelLabel}</Text>
</TouchableOpacity>
<LinearGradient
colors={[theme.colors.cyan, theme.colors.aqua]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[styles.button, styles.confirmButton]}>
<TouchableOpacity
style={styles.buttonTouchable}
onPress={handleConfirm}>
<Text style={styles.confirmButtonText}>{confirmLabel}</Text>
</TouchableOpacity>
</LinearGradient>
</View>
</Animated.View>
</Animated.View>
</Modal>
);
};
// 单选框弹窗组件
interface SingleSelectDialogProps {
visible: boolean;
title: string;
message?: string;
options: string[];
selectedIndex?: number;
submitText?: string;
onSubmit: (index: number, value: string) => void;
onCancel: () => void;
}
export const SingleSelectDialog: React.FC<SingleSelectDialogProps> = ({
visible,
title,
message,
options,
selectedIndex = 0,
submitText = '确定',
onSubmit,
onCancel,
}) => {
const [selected, setSelected] = useState(selectedIndex);
const scaleAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(0)).current;
React.useEffect(() => {
if (visible) {
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
]).start();
} else {
scaleAnim.setValue(0);
opacityAnim.setValue(0);
setSelected(selectedIndex);
}
}, [visible, selectedIndex, opacityAnim, scaleAnim]);
const handleSubmit = () => {
onSubmit(selected, options[selected]);
};
if (!visible) return null;
return (
<Modal
visible={visible}
transparent
animationType="none"
statusBarTranslucent
onRequestClose={onCancel}>
<StatusBar
backgroundColor="rgba(0, 0, 0, 0.5)"
barStyle="light-content"
/>
<Animated.View style={[styles.overlay, {opacity: opacityAnim}]}>
<Animated.View
style={[
styles.dialogContainer,
styles.selectDialogContainer,
{transform: [{scale: scaleAnim}]},
]}>
{/* 头部 */}
<View style={styles.selectHeader}>
<View style={[styles.iconContainer, {backgroundColor: '#e6f7ff'}]}>
<View style={styles.iconWrapper}>
<Icon name="list" size={28} color={theme.colors.cyan} />
</View>
</View>
<View style={styles.headerContent}>
<Text style={styles.title}>{title}</Text>
<TouchableOpacity onPress={onCancel} style={styles.closeButton}>
<Icon name="close" size={24} color="#666" />
</TouchableOpacity>
</View>
</View>
{message && (
<View style={styles.messageContainer}>
<Text style={styles.message}>{message}</Text>
</View>
)}
{/* 选项列表 */}
<ScrollView
style={styles.optionsContainer}
showsVerticalScrollIndicator={false}>
{options.map((option, index) => (
<TouchableOpacity
key={index}
style={[
styles.optionItem,
selected === index && styles.selectedOptionItem,
]}
onPress={() => setSelected(index)}>
<View style={styles.optionContent}>
<View
style={[
styles.radioButton,
selected === index && styles.radioButtonSelected,
]}>
{selected === index && (
<View style={styles.radioButtonInner} />
)}
</View>
<Text
style={[
styles.optionText,
selected === index && styles.selectedOptionText,
]}>
{option}
</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
{/* 提交按钮 */}
<View style={styles.selectButtonContainer}>
<LinearGradient
colors={[theme.colors.cyan, theme.colors.aqua]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={styles.submitButton}>
<TouchableOpacity
style={styles.buttonTouchable}
onPress={handleSubmit}>
<Text style={styles.confirmButtonText}>{submitText}</Text>
</TouchableOpacity>
</LinearGradient>
</View>
</Animated.View>
</Animated.View>
</Modal>
);
};
// 全新的 DialogUtils 类
export class DialogUtils {
private static dialogRef: any = null;
static setDialogRef(ref: any) {
DialogUtils.dialogRef = ref;
}
/**
*
*/
static showMessage(
title: string,
message: string,
btnLabel: string = '确定',
onPress?: () => void,
): void {
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'info',
title,
message,
confirmText: btnLabel,
onConfirm: onPress,
});
}
}
/**
* message label msg
*/
static showMessageList(
title: string,
message: MessageItem[],
btnLabel: string = '确定',
onPress?: () => void,
): void {
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'info',
title,
messageList: message,
confirmText: btnLabel,
onConfirm: onPress,
});
}
}
/**
*
*/
static showSuccessMessage(
title: string,
message: string,
btnLabel: string = '确定',
onPress?: () => void,
): void {
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'success',
title,
message,
confirmText: btnLabel,
onConfirm: onPress,
});
}
}
/**
*
*/
static showWarningMessage(
title: string,
message: string,
btnLabel: string = '确定',
onPress?: () => void,
): void {
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'warning',
title,
message,
confirmText: btnLabel,
onConfirm: onPress,
});
}
}
/**
*
*/
static showErrorMessage(
title: string,
message: string,
btnLabel: string = '确定',
onPress?: () => void,
): void {
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'error',
title,
message,
confirmText: btnLabel,
onConfirm: onPress,
});
}
}
/**
*
*/
static showConfirmMessage(
title: string,
message: string,
options: {
cancel?: () => void;
confirm?: () => void;
cancelLabel?: string;
confirmLabel?: string;
} = {},
): void {
const {
cancel,
confirm,
cancelLabel = '取消',
confirmLabel = '确定',
} = options;
if (DialogUtils.dialogRef) {
DialogUtils.dialogRef.show({
type: 'confirm',
title,
message,
cancelText: cancelLabel,
confirmText: confirmLabel,
onCancel: cancel,
onConfirm: confirm,
});
}
}
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
dialogContainer: {
backgroundColor: '#fff',
borderRadius: 16,
minWidth: width * 0.8,
maxWidth: width * 0.9,
maxHeight: height * 0.8,
shadowColor: '#000',
shadowOffset: {width: 0, height: 8},
shadowOpacity: 0.25,
shadowRadius: 16,
elevation: 8,
},
iconContainer: {
alignItems: 'center',
paddingTop: 24,
paddingBottom: 16,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
iconWrapper: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
contentContainer: {
paddingHorizontal: 24,
paddingBottom: 24,
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
textAlign: 'center',
marginBottom: 12,
},
message: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 24,
marginBottom: 8,
},
messageListContainer: {
backgroundColor: '#f8f9fa',
borderRadius: 8,
padding: 16,
marginTop: 12,
},
messageItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
messageLabel: {
fontSize: 14,
color: '#666',
fontWeight: '500',
minWidth: 80,
},
messageValue: {
fontSize: 14,
color: '#333',
flex: 1,
},
buttonContainer: {
flexDirection: 'row',
paddingHorizontal: 24,
paddingBottom: 24,
gap: 12,
},
button: {
flex: 1,
height: 48,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
buttonTouchable: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
cancelButton: {
backgroundColor: '#f5f5f5',
borderWidth: 1,
borderColor: '#d9d9d9',
},
confirmButton: {
shadowColor: theme.colors.cyan,
shadowOffset: {width: 0, height: 4},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
singleButton: {
shadowColor: theme.colors.cyan,
shadowOffset: {width: 0, height: 4},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
cancelButtonText: {
color: '#666',
fontSize: 16,
fontWeight: '600',
},
confirmButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
inputContainer: {
marginTop: 16,
},
textInput: {
borderWidth: 1,
borderColor: '#d9d9d9',
borderRadius: 8,
padding: 12,
fontSize: 16,
color: '#333',
backgroundColor: '#fafafa',
},
// 单选框相关样式
selectDialogContainer: {
maxHeight: height * 0.7,
},
selectHeader: {
paddingBottom: 8,
},
headerContent: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 24,
paddingTop: 8,
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
alignItems: 'center',
},
messageContainer: {
paddingHorizontal: 24,
paddingBottom: 12,
},
optionsContainer: {
maxHeight: height * 0.4,
paddingHorizontal: 24,
},
optionItem: {
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
selectedOptionItem: {
backgroundColor: '#e6f7ff',
borderRadius: 8,
borderBottomColor: 'transparent',
marginVertical: 2,
},
optionContent: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
},
radioButton: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 2,
borderColor: '#d9d9d9',
marginRight: 16,
justifyContent: 'center',
alignItems: 'center',
},
radioButtonSelected: {
borderColor: theme.colors.cyan,
},
radioButtonInner: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: theme.colors.cyan,
},
optionText: {
fontSize: 16,
color: '#333',
flex: 1,
},
selectedOptionText: {
color: theme.colors.cyan,
fontWeight: '600',
},
selectButtonContainer: {
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 24,
},
submitButton: {
height: 48,
borderRadius: 12,
shadowColor: theme.colors.cyan,
shadowOffset: {width: 0, height: 4},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
});

6
src/utils/index.ts Normal file
View File

@ -0,0 +1,6 @@
// 弹窗工具导出
export {DialogUtils, InputDialog, SingleSelectDialog} from './DialogUtils';
export type {MessageItem} from './DialogUtils';
// 存储工具导出
export * from './storage';