修改弹窗模块及配色
This commit is contained in:
parent
b6c8db84a8
commit
b80d368d5e
12
App.tsx
12
App.tsx
|
|
@ -5,15 +5,17 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
import Navigation from './src/navigation';
|
||||
import {ThemeProvider} from './src/contexts/ThemeContext';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import {View, Text, ActivityIndicator, StyleSheet} from 'react-native';
|
||||
import {BeautifulDialog, DialogUtils} from './src/utils/DialogUtils';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [fontLoaded, setFontLoaded] = useState(false);
|
||||
const [fontError, setFontError] = useState<string | null>(null);
|
||||
const dialogRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadFont = async () => {
|
||||
|
|
@ -44,6 +46,13 @@ function App(): JSX.Element {
|
|||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
// 设置弹窗引用
|
||||
useEffect(() => {
|
||||
if (dialogRef.current) {
|
||||
DialogUtils.setDialogRef(dialogRef.current);
|
||||
}
|
||||
}, [fontLoaded]);
|
||||
|
||||
// 显示加载状态或错误信息
|
||||
if (!fontLoaded || fontError) {
|
||||
return (
|
||||
|
|
@ -60,6 +69,7 @@ function App(): JSX.Element {
|
|||
return (
|
||||
<ThemeProvider>
|
||||
<Navigation />
|
||||
<BeautifulDialog ref={dialogRef} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
27
package-lock.json
generated
27
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pda_react_native_template",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pda_react_native_template",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"react-native": "0.75.5",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"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-screens": "^4.5.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": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz",
|
||||
|
|
@ -13368,6 +13378,19 @@
|
|||
"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": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.1.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"react-native": "0.75.5",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"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-screens": "^4.5.0",
|
||||
"react-native-svg": "^15.12.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
SafeAreaView,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
|
|
@ -17,6 +16,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
|
|||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
import axios from 'axios';
|
||||
import {DialogUtils} from '../../utils';
|
||||
|
||||
interface StockInEmptyResponse {
|
||||
code: number;
|
||||
|
|
@ -41,9 +41,7 @@ export const StockInEmpty: React.FC = () => {
|
|||
// 空托入库方法
|
||||
const handleEmptyIn = async () => {
|
||||
if (!vehicleNo.trim()) {
|
||||
Alert.alert('警告', '请先填写载具号', [
|
||||
{text: '返回填写', style: 'cancel'},
|
||||
]);
|
||||
DialogUtils.showWarningMessage('警告', '请先填写载具号', '返回填写');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -55,29 +53,29 @@ export const StockInEmpty: React.FC = () => {
|
|||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
Alert.alert('警告', '服务器请求失败', [
|
||||
{text: '我知道了', style: 'cancel'},
|
||||
]);
|
||||
DialogUtils.showWarningMessage('警告', '服务器请求失败', '我知道了');
|
||||
return;
|
||||
}
|
||||
|
||||
const responseData = response.data;
|
||||
|
||||
if (responseData.code === 200) {
|
||||
Alert.alert('成功', '', [{text: '我知道了', style: 'default'}]);
|
||||
DialogUtils.showSuccessMessage('成功', '空载具入库成功', '我知道了');
|
||||
setVehicleNo('');
|
||||
} else {
|
||||
Alert.alert('警告', `服务器返回失败:${responseData.message}`, [
|
||||
{text: '我知道了', style: 'cancel'},
|
||||
]);
|
||||
DialogUtils.showWarningMessage(
|
||||
'警告',
|
||||
`服务器返回失败:${responseData.message}`,
|
||||
'我知道了',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
DialogUtils.showErrorMessage(
|
||||
'请求发生错误',
|
||||
`请求服务器发生错误:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
[{text: '我知道了', style: 'cancel'}],
|
||||
'我知道了',
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-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 LinearGradient from 'react-native-linear-gradient';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
import {DialogUtils, MessageItem} from '../../utils';
|
||||
|
||||
interface PackageDataItem {
|
||||
id: string;
|
||||
|
|
@ -55,21 +55,24 @@ export const StockInManual: React.FC = () => {
|
|||
|
||||
if (code.length === 15) {
|
||||
setLastProcessedVehicleCode(code);
|
||||
Alert.alert('扫码成功', `绑定托盘: ${code}`);
|
||||
DialogUtils.showSuccessMessage('扫码成功', `绑定托盘: ${code}`);
|
||||
} else {
|
||||
Alert.alert('绑定托盘失败', '无效的载具号长度');
|
||||
DialogUtils.showErrorMessage('绑定托盘失败', '无效的载具号长度');
|
||||
}
|
||||
}, [vehicleCode, lastProcessedVehicleCode]);
|
||||
|
||||
const resolveCode = useCallback(() => {
|
||||
if (!goodsCode) {
|
||||
Alert.alert('警告', '条码文本框内无数据,请先扫描或者输入数据');
|
||||
DialogUtils.showWarningMessage(
|
||||
'警告',
|
||||
'条码文本框内无数据,请先扫描或者输入数据',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const codeData = goodsCode.split(',');
|
||||
if (![6, 7, 8].includes(codeData.length)) {
|
||||
Alert.alert('警告', '条码格式错误');
|
||||
DialogUtils.showWarningMessage('警告', '条码格式错误');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +104,7 @@ export const StockInManual: React.FC = () => {
|
|||
setPackageData(prev => [...prev, newItem]);
|
||||
setPackageDataId(prev => prev + 1);
|
||||
} else {
|
||||
Alert.alert('警告', '该物料批次已存在,不能重复添加');
|
||||
DialogUtils.showWarningMessage('警告', '该物料批次已存在,不能重复添加');
|
||||
}
|
||||
|
||||
setGoodsCode('');
|
||||
|
|
@ -130,44 +133,38 @@ export const StockInManual: React.FC = () => {
|
|||
};
|
||||
|
||||
const modifyNumber = (_id: string) => {
|
||||
Alert.alert('修改数量', '请输入新的数量', [
|
||||
{text: '取消', style: 'cancel'},
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => {
|
||||
DialogUtils.showConfirmMessage('修改数量', '请输入新的数量', {
|
||||
cancelLabel: '取消',
|
||||
confirmLabel: '确定',
|
||||
confirm: () => {
|
||||
// TODO: 需要实现一个自定义的输入对话框
|
||||
Alert.alert('提示', '此功能暂不可用,请稍后再试');
|
||||
DialogUtils.showMessage('提示', '此功能暂不可用,请稍后再试');
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const showDetails = (item: PackageDataItem) => {
|
||||
const message = [
|
||||
{label: '序号:', msg: item.id},
|
||||
{label: '采购单号:', msg: item.segment1},
|
||||
{label: '物料号:', msg: item.itemId},
|
||||
{label: '批次号:', msg: item.batch},
|
||||
{label: '数量:', msg: item.quantity},
|
||||
{label: '重量:', msg: item.weight},
|
||||
{label: '生产日期:', msg: item.productData},
|
||||
const message: MessageItem[] = [
|
||||
{label: '序号', msg: item.id},
|
||||
{label: '采购单号', msg: item.segment1},
|
||||
{label: '物料号', msg: item.itemId},
|
||||
{label: '批次号', msg: item.batch},
|
||||
{label: '数量', msg: item.quantity},
|
||||
{label: '重量', msg: item.weight},
|
||||
{label: '生产日期', msg: item.productData},
|
||||
];
|
||||
|
||||
Alert.alert(
|
||||
'数据详情',
|
||||
message.map(m => `${m.label} ${m.msg}`).join('\n'),
|
||||
[{text: '我知道了'}],
|
||||
);
|
||||
DialogUtils.showMessageList('数据详情', message, '我知道了');
|
||||
};
|
||||
|
||||
const wheelComplete = async () => {
|
||||
if (packageData.length === 0) {
|
||||
Alert.alert('警告', '您的码盘数据为空');
|
||||
DialogUtils.showWarningMessage('警告', '您的码盘数据为空');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vehicleCode.trim()) {
|
||||
Alert.alert('警告', '请先扫描载具号');
|
||||
DialogUtils.showWarningMessage('警告', '请先扫描载具号');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -175,16 +172,18 @@ export const StockInManual: React.FC = () => {
|
|||
const factoryIndex = 2; // 默认二厂
|
||||
const statusIndex = 1; // 默认合格
|
||||
|
||||
const confirm = await new Promise(resolve =>
|
||||
Alert.alert(
|
||||
const confirm = await new Promise<boolean>(resolve => {
|
||||
DialogUtils.showConfirmMessage(
|
||||
'码盘完成',
|
||||
`载具:${vehicleCode} 码盘 ${packageData.length} 条数据,是否继续?`,
|
||||
[
|
||||
{text: '取消', style: 'cancel', onPress: () => resolve(false)},
|
||||
{text: '继续', onPress: () => resolve(true)},
|
||||
],
|
||||
),
|
||||
{
|
||||
cancelLabel: '取消',
|
||||
confirmLabel: '继续',
|
||||
cancel: () => resolve(false),
|
||||
confirm: () => resolve(true),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!confirm) return;
|
||||
|
||||
|
|
@ -211,17 +210,24 @@ export const StockInManual: React.FC = () => {
|
|||
setPackageDataId(0);
|
||||
setVehicleCode('');
|
||||
setPackageData([]);
|
||||
Alert.alert('码盘成功', '', [{text: '我知道了'}]);
|
||||
DialogUtils.showSuccessMessage(
|
||||
'码盘成功',
|
||||
'码盘操作已完成',
|
||||
'我知道了',
|
||||
);
|
||||
} else {
|
||||
Alert.alert('警告', `服务器返回失败:${data.message}`);
|
||||
DialogUtils.showWarningMessage(
|
||||
'警告',
|
||||
`服务器返回失败:${data.message}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
DialogUtils.showErrorMessage(
|
||||
'请求发生错误',
|
||||
`请求服务器发生错误:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
[{text: '我知道了', style: 'cancel'}],
|
||||
'我知道了',
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
893
src/utils/DialogUtils.tsx
Normal file
893
src/utils/DialogUtils.tsx
Normal 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
6
src/utils/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// 弹窗工具导出
|
||||
export {DialogUtils, InputDialog, SingleSelectDialog} from './DialogUtils';
|
||||
export type {MessageItem} from './DialogUtils';
|
||||
|
||||
// 存储工具导出
|
||||
export * from './storage';
|
||||
Loading…
Reference in New Issue
Block a user