Compare commits

..

5 Commits

Author SHA1 Message Date
484b9c332b Merge branch 'master' of http://112.4.208.194:3000/liyuqi/pda_react_native_template 2025-07-16 08:11:33 +08:00
b80d368d5e 修改弹窗模块及配色 2025-07-09 17:16:24 +08:00
b6c8db84a8 优化项目结构 2025-07-09 16:21:15 +08:00
e937988771 优化项目结构 2025-07-09 14:47:04 +08:00
1705ace101 修改配色 2025-07-09 10:35:33 +08:00
20 changed files with 1336 additions and 525 deletions

42
App.tsx
View File

@ -1,25 +1,3 @@
// /**
// * Sample React Native App
// * https://github.com/facebook/react-native
// *
// * @format
// */
//
// import React from 'react';
// import Navigation from './src/navigation';
// import {ThemeProvider} from './src/contexts/ThemeContext';
//
// function App(): JSX.Element {
// return (
// <ThemeProvider>
// <Navigation />
// </ThemeProvider>
// );
// }
//
// export default App;
/**
* Sample React Native App
* https://github.com/facebook/react-native
@ -27,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 {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 () => {
@ -66,14 +46,19 @@ function App(): JSX.Element {
return () => clearTimeout(timeout);
}, []);
// 设置弹窗引用
useEffect(() => {
if (dialogRef.current) {
DialogUtils.setDialogRef(dialogRef.current);
}
}, [fontLoaded]);
// 显示加载状态或错误信息
if (!fontLoaded || fontError) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
<Text style={styles.statusText}>
{fontError || 'Loading icons...'}
</Text>
<Text style={styles.statusText}>{fontError || 'Loading icons...'}</Text>
<Text style={styles.debugText}>
Using MaterialIcons font family: {MaterialIcons.getFontFamily()}
</Text>
@ -84,6 +69,7 @@ function App(): JSX.Element {
return (
<ThemeProvider>
<Navigation />
<BeautifulDialog ref={dialogRef} />
</ThemeProvider>
);
}
@ -108,4 +94,4 @@ const styles = StyleSheet.create({
},
});
export default App;
export default App;

View File

@ -1,6 +1,6 @@
{
"name": "pda_react_native_template",
"version": "0.0.1",
"version": "1.0.0",
"private": true,
"scripts": {
"android": "react-native run-android",
@ -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",

View File

@ -1,26 +0,0 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from 'react';
import {StatusBar} from 'react-native';
import Navigation from './navigation';
import {ThemeProvider} from './contexts/ThemeContext';
import {theme} from './constants/theme';
const App = () => {
return (
<ThemeProvider theme={theme}>
<StatusBar
backgroundColor={theme.colors.primary}
barStyle="light-content"
/>
<Navigation />
</ThemeProvider>
);
};
export default App;

View File

@ -2,4 +2,4 @@ export const ENV = {
API_URL: __DEV__ ? 'http://dev-api.example.com' : 'https://api.example.com',
APP_NAME: 'PdaRnTemplate',
VERSION: '1.0.0',
};
};

View File

@ -1,45 +1,31 @@
// 将Flutter的颜色值转换为React Native可用的格式
// Flutter: 0xff05dcef -> React Native: #05dcef
export const theme = {
colors: {
// 基础颜色
primary: '#4158D0', // 主色调
secondary: '#C850C0', // 次要色调
tertiary: '#FFCC70', // 第三色调
background: '#ffffff', // 背景色
text: '#333333', // 文本颜色
textLight: '#666666', // 次要文本颜色
border: '#dddddd', // 边框颜色
error: '#ff3b30', // 错误颜色
success: '#4cd964', // 成功颜色
warning: '#ff9500', // 警告颜色
cyan: '#00FFFF', // 青色背景
aqua: '#05DCEF', // 青色主色调 (0xff05dcef)
background: '#ffffff', // 背景色
backgroundGray: '#F5F5F5', // 灰色背景
text: '#333333', // 文本颜色
textLight: '#666666', // 次要文本颜色
border: '#dddddd', // 边框颜色
error: '#ff3b30', // 错误颜色
cyan: '#00FFFF', // 青色背景
lightGreen: '#7FFFAA', // 浅绿色
// 渐变色配置
gradients: {
primary: ['#4158D0', '#C850C0', '#FFCC70'], // 主要渐变(蓝紫金)
contrast: ['#00F5A0', '#00D9F5'], // 对比渐变(绿青)
card: ['#9795F0', '#E3C3F1'], // 卡片渐变(柔和紫色)
login: ['#4158D0', '#C850C0', '#FFCC70'], // 登录页面专用渐变
loginWave: ['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.2)'], // 登录页面波浪渐变
header: ['#4158D0', '#C850C0'], // 头部渐变
button: ['#4158D0', '#C850C0'], // 按钮渐变
// 波浪背景渐变
wave: {
start: 'rgba(255, 255, 255, 0.3)',
middle: 'rgba(255, 255, 255, 0.2)',
end: 'rgba(255, 255, 255, 0.1)'
}
primary: ['#05DCEF', '#7DE2F5', '#B8F2FF'], // 主要渐变(青蓝白,小清新)
contrast: ['#00F5A0', '#00D9F5'], // 对比渐变(绿青)
header: ['#05DCEF', '#7DE2F5'], // 头部渐变
button: ['#05DCEF', '#7DE2F5'], // 按钮渐变
},
// 图表颜色
chart: {
blue: '#0077B6', // 深蓝
orange: '#FB8500', // 明亮的橙
cyan: '#00FFFF', // 青色
lightGreen: '#7FFFAA', // 浅绿色
},
},
// 字体大小
fontSize: {
small: 12,
@ -50,7 +36,7 @@ export const theme = {
xxlarge: 24,
title: 32,
},
// 间距
spacing: {
xs: 4,
@ -59,7 +45,7 @@ export const theme = {
large: 24,
xl: 32,
},
// 圆角
borderRadius: {
small: 4,
@ -68,7 +54,7 @@ export const theme = {
xl: 16,
circle: 999,
},
// 阴影
shadow: {
small: {
@ -87,7 +73,7 @@ export const theme = {
width: 0,
height: 4,
},
shadowOpacity: 0.30,
shadowOpacity: 0.3,
shadowRadius: 4.65,
elevation: 4,
},
@ -102,12 +88,6 @@ export const theme = {
elevation: 6,
},
},
// 波浪效果配置
wave: {
height: 60,
opacity: 1,
},
};
// 类型定义
@ -116,4 +96,4 @@ export type Theme = typeof theme;
// 导出类型
declare module '@react-navigation/native' {
export type ExtendedTheme = Theme;
}
}

View File

@ -18,4 +18,4 @@ export const useTheme = () => {
throw new Error('useTheme must be used within a ThemeProvider');
}
return theme;
};
};

View File

@ -1,23 +0,0 @@
class ScannerService {
async init(): Promise<void> {
// 初始化扫描器
console.log('Scanner initialized');
}
async startScan(): Promise<string> {
// 开始扫描
return new Promise((resolve) => {
// 模拟扫描结果
setTimeout(() => {
resolve('Scanned barcode result');
}, 1000);
});
}
async stopScan(): Promise<void> {
// 停止扫描
console.log('Scanner stopped');
}
}
export const scannerService = new ScannerService();

View File

@ -1,38 +0,0 @@
import {useState, useCallback} from 'react';
import {scannerService} from '../device/scanner';
export const useScanner = () => {
const [isScanning, setIsScanning] = useState(false);
const [result, setResult] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
const startScan = useCallback(async () => {
try {
setIsScanning(true);
setError(null);
const scanResult = await scannerService.startScan();
setResult(scanResult);
} catch (err) {
setError(err as Error);
} finally {
setIsScanning(false);
}
}, []);
const stopScan = useCallback(async () => {
try {
await scannerService.stopScan();
setIsScanning(false);
} catch (err) {
setError(err as Error);
}
}, []);
return {
isScanning,
result,
error,
startScan,
stopScan,
};
};

View File

@ -4,7 +4,7 @@ import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {LoginScreen} from '../screens/auth/LoginScreen';
import {HomeScreen} from '../screens/home/HomeScreen';
import {StockInEmpty} from '../screens/stockIn/StockInEmpty';
import StockInWheelManual from '../screens/stockIn/StockInWheelManual';
import {StockInManual} from '../screens/stockIn/StockInManual';
import {RootStackParamList} from './types';
import {Platform} from 'react-native';
import {screensEnabled, enableScreens} from 'react-native-screens';
@ -50,8 +50,8 @@ const Navigation = () => {
}}
/>
<Stack.Screen
name="StockInWheelManual"
component={StockInWheelManual}
name="StockInManual"
component={StockInManual}
options={{
animation: 'none',
}}

View File

@ -4,8 +4,8 @@ export type RootStackParamList = {
Login: undefined;
Home: undefined;
StockInEmpty: undefined;
StockInWheelEBS: undefined;
StockInWheelManual: undefined;
StockInEBS: undefined;
StockInManual: undefined;
Pick: undefined;
StockCheck: undefined;
// 在这里添加其他页面的路由参数定义
@ -18,4 +18,4 @@ declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
}

View File

@ -28,7 +28,7 @@ export const LoginScreen: React.FC = () => {
const theme = useTheme();
const handleLogin = useCallback(() => {
if (!isFocused) return;
if (!isFocused) {return;}
InteractionManager.runAfterInteractions(() => {
if (isFocused) {
@ -41,9 +41,9 @@ export const LoginScreen: React.FC = () => {
<Screen style={styles.screen}>
{/* 背景区域 */}
<View style={styles.backgroundContainer}>
<WaveBackground
height={height * 0.6}
gradientColors={theme.colors.gradients.primary}
<WaveBackground
height={height * 0.6}
gradientColors={theme.colors.gradients.primary}
/>
</View>
@ -51,7 +51,7 @@ export const LoginScreen: React.FC = () => {
<View style={styles.contentContainer}>
{/* 标题区域 */}
<View style={styles.titleContainer}>
<Text style={styles.title}></Text>
<Text style={styles.title}></Text>
<Text style={styles.subtitle}></Text>
</View>
@ -60,8 +60,8 @@ export const LoginScreen: React.FC = () => {
{/* Logo */}
<View style={styles.logoWrapper}>
<View style={[styles.logoCircle, theme.shadow.medium]}>
<Text style={[styles.logoText, {color: theme.colors.primary}]}>
JW
<Text style={[styles.logoText, {color: theme.colors.aqua}]}>
BK
</Text>
</View>
</View>
@ -83,7 +83,7 @@ export const LoginScreen: React.FC = () => {
{/* 底部版权信息 */}
<View style={styles.footer}>
<Text style={[styles.footerText, {color: theme.colors.textLight}]}>
© 2024
© 2025
</Text>
</View>
</View>
@ -183,4 +183,4 @@ const styles = StyleSheet.create({
footerText: {
fontSize: 12,
},
});
});

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, {useState, useEffect} from 'react';
import {
View,
Text,
@ -8,16 +8,16 @@ import {
ScrollView,
Dimensions,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/types';
import { PieChart } from 'react-native-chart-kit';
import {useNavigation} from '@react-navigation/native';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {RootStackParamList} from '../../navigation/types';
import {PieChart} from 'react-native-chart-kit';
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 Svg, { Path } from 'react-native-svg';
import Svg, {Path} from 'react-native-svg';
const { width } = Dimensions.get('window');
const {width} = Dimensions.get('window');
type HomeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
@ -48,22 +48,22 @@ export const HomeScreen: React.FC = () => {
}, []);
const menuItems: MenuItem[] = [
{ title: '空载具入库', icon: 'local-shipping', route: 'StockInEmpty' },
{ title: '手动码盘入库', icon: 'inventory', route: 'StockInWheelManual' },
{title: '空载具入库', icon: 'local-shipping', route: 'StockInEmpty'},
{title: '手动码盘入库', icon: 'inventory', route: 'StockInManual'},
];
const stockData = [
{
name: '空闲',
population: 40,
color: theme.colors.chart.blue,
color: theme.colors.chart.cyan,
legendFontColor: theme.colors.textLight,
legendFontSize: theme.fontSize.regular,
},
{
name: '占用',
population: 60,
color: theme.colors.chart.orange,
color: theme.colors.chart.lightGreen,
legendFontColor: theme.colors.textLight,
legendFontSize: theme.fontSize.regular,
},
@ -77,23 +77,30 @@ export const HomeScreen: React.FC = () => {
if (!fontLoaded) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.colors.background }]}>
<Text style={{ color: theme.colors.text }}>...</Text>
<View
style={[
styles.loadingContainer,
{backgroundColor: theme.colors.backgroundGray},
]}>
<Text style={{color: theme.colors.text}}>...</Text>
</View>
);
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.colors.background }]}>
<SafeAreaView
style={[
styles.container,
{backgroundColor: theme.colors.backgroundGray},
]}>
{/* 头部区域 */}
<View style={styles.headerSection}>
{/* 渐变背景 */}
<LinearGradient
colors={theme.colors.gradients.primary}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}>
{/* 半透明波浪效果 */}
<View style={styles.waveOverlay}>
<Svg height="100%" width={width} style={styles.waveSvg}>
@ -131,17 +138,25 @@ export const HomeScreen: React.FC = () => {
style={styles.menuButton}>
<Icon name="menu" size={24} color={theme.colors.background} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: theme.colors.background }]}>
WMS移动终端
<Text style={[styles.headerTitle, {color: theme.colors.background}]}>
WMS移动终端
</Text>
<TouchableOpacity style={styles.iconButton}>
<Icon name="notifications" size={24} color={theme.colors.background} />
<Icon
name="notifications"
size={24}
color={theme.colors.background}
/>
</TouchableOpacity>
</View>
</View>
{/* 主体内容 */}
<View style={[styles.mainContent, { backgroundColor: '#FFFFFF' }]}>
<View
style={[
styles.mainContent,
{backgroundColor: theme.colors.backgroundGray},
]}>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* 快捷操作区 */}
<View style={styles.quickActions}>
@ -157,9 +172,17 @@ export const HomeScreen: React.FC = () => {
end={{x: 1, y: 0}}
/>
<View style={styles.iconContainer}>
<Icon name={item.icon} size={32} color={theme.colors.background} />
<Icon
name={item.icon}
size={32}
color={theme.colors.background}
/>
</View>
<Text style={[styles.actionTitle, {color: theme.colors.background}]}>
<Text
style={[
styles.actionTitle,
{color: theme.colors.background},
]}>
{item.title}
</Text>
</TouchableOpacity>
@ -190,29 +213,39 @@ export const HomeScreen: React.FC = () => {
{/* 侧边菜单 */}
{isDrawerOpen && (
<>
<View style={[styles.drawer, { backgroundColor: theme.colors.background }]}>
<View
style={[styles.drawer, {backgroundColor: theme.colors.background}]}>
<LinearGradient
colors={theme.colors.gradients.primary}
style={styles.drawerHeader}>
<Text style={[styles.drawerTitle, { color: theme.colors.background }]}>
<Text
style={[styles.drawerTitle, {color: theme.colors.background}]}>
</Text>
<Text style={[styles.drawerSubtitle, { color: theme.colors.background }]}>
<Text
style={[
styles.drawerSubtitle,
{color: theme.colors.background},
]}>
使WMS移动终端
</Text>
</LinearGradient>
{menuItems.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.drawerItem, { borderBottomColor: theme.colors.border }]}
style={[
styles.drawerItem,
{borderBottomColor: theme.colors.border},
]}
onPress={() => {
setIsDrawerOpen(false);
navigation.navigate(item.route);
}}>
<View style={styles.drawerIconContainer}>
<Icon name={item.icon} size={28} color={theme.colors.primary} />
<Icon name={item.icon} size={28} color={theme.colors.aqua} />
</View>
<Text style={[styles.drawerItemText, { color: theme.colors.text }]}>
<Text
style={[styles.drawerItemText, {color: theme.colors.text}]}>
{item.title}
</Text>
</TouchableOpacity>

View File

@ -5,11 +5,9 @@ import {
StyleSheet,
TextInput,
TouchableOpacity,
Alert,
SafeAreaView,
ActivityIndicator,
} from 'react-native';
import {httpService} from '../../services/http';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {useNavigation} from '@react-navigation/native';
import {RootStackParamList} from '../../navigation/types';
@ -17,11 +15,16 @@ import {useTheme} from '../../contexts/ThemeContext';
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;
message: string;
data: string;
data: {
code: number;
message: string;
};
}
type StockInEmptyScreenNavigationProp = NativeStackNavigationProp<
@ -38,55 +41,53 @@ export const StockInEmpty: React.FC = () => {
// 空托入库方法
const handleEmptyIn = async () => {
if (!vehicleNo.trim()) {
Alert.alert('警告', '请先填写载具号', [
{text: '返回填写', style: 'cancel'},
]);
DialogUtils.showWarningMessage('警告', '请先填写载具号', '返回填写');
return;
}
try {
setLoading(true);
const response = await httpService.post<StockInEmptyResponse>(
const response = await axios.post<StockInEmptyResponse>(
'/api/vehicle/empty-in',
{vehicleNo: vehicleNo.trim()}
{vehicleNo: vehicleNo.trim()},
);
if (response.code !== 200) {
Alert.alert('警告', '服务器请求失败', [
{text: '我知道了', style: 'cancel'},
]);
if (response.status !== 200) {
DialogUtils.showWarningMessage('警告', '服务器请求失败', '我知道了');
return;
}
// 确保response.data是字符串类型
const responseData = typeof response.data === 'string'
? JSON.parse(response.data)
: response.data;
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('请求发生错误', `请求服务器发生错误:${error}`, [
{text: '我知道了', style: 'cancel'},
]);
DialogUtils.showErrorMessage(
'请求发生错误',
`请求服务器发生错误:${
error instanceof Error ? error.message : String(error)
}`,
'我知道了',
);
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={[styles.container, {backgroundColor: theme.colors.background}]}>
<SafeAreaView
style={[styles.container, {backgroundColor: theme.colors.background}]}>
{/* 头部导航栏 */}
<LinearGradient
colors={[theme.colors.primary, theme.colors.secondary, theme.colors.tertiary]}
colors={theme.colors.gradients.primary}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={styles.header}>
@ -95,7 +96,9 @@ export const StockInEmpty: React.FC = () => {
style={styles.backButton}>
<Icon name="arrow-back" size={24} color={theme.colors.background} />
</TouchableOpacity>
<Text style={[styles.headerTitle, {color: theme.colors.background}]}></Text>
<Text style={[styles.headerTitle, {color: theme.colors.background}]}>
</Text>
<View style={styles.headerRight} />
</LinearGradient>
@ -108,23 +111,25 @@ export const StockInEmpty: React.FC = () => {
style={styles.waveSvg}
preserveAspectRatio="none">
<Path
fill={`${theme.colors.tertiary}40`}
fill={`${theme.colors.aqua}30`}
d="M0,96L48,112C96,128,192,160,288,186.7C384,213,480,235,576,213.3C672,192,768,128,864,128C960,128,1056,192,1152,213.3C1248,235,1344,213,1392,202.7L1440,192L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"
/>
</Svg>
</View>
{/* 主体内容 */}
<View style={styles.content}>
<LinearGradient
colors={theme.colors.gradients.contrast}
style={StyleSheet.absoluteFill}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
/>
<View
style={[
styles.content,
{backgroundColor: theme.colors.backgroundGray},
]}>
{/* 说明文字 */}
<View style={[styles.infoSection, {backgroundColor: `${theme.colors.primary}15`}]}>
<Icon name="info-outline" size={24} color={theme.colors.primary} />
<View
style={[
styles.infoSection,
{backgroundColor: `${theme.colors.aqua}15`},
]}>
<Icon name="info-outline" size={24} color={theme.colors.aqua} />
<Text style={[styles.infoText, {color: theme.colors.text}]}>
</Text>
@ -135,11 +140,20 @@ export const StockInEmpty: React.FC = () => {
<Text style={[styles.inputLabel, {color: theme.colors.text}]}>
<Text style={styles.required}>*</Text>
</Text>
<View style={[styles.inputWrapper, {
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
}]}>
<Icon name="qr-code-scanner" size={24} color={theme.colors.primary} style={styles.inputIcon} />
<View
style={[
styles.inputWrapper,
{
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
},
]}>
<Icon
name="qr-code-scanner"
size={24}
color={theme.colors.aqua}
style={styles.inputIcon}
/>
<TextInput
style={[styles.input, {color: theme.colors.text}]}
value={vehicleNo}
@ -160,7 +174,7 @@ export const StockInEmpty: React.FC = () => {
{/* 提交按钮 */}
<LinearGradient
colors={[theme.colors.primary, theme.colors.secondary]}
colors={theme.colors.gradients.button}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[styles.submitButton, loading && styles.submitButtonDisabled]}>
@ -172,8 +186,17 @@ export const StockInEmpty: React.FC = () => {
<ActivityIndicator color={theme.colors.background} />
) : (
<>
<Icon name="save" size={24} color={theme.colors.background} style={styles.submitIcon} />
<Text style={[styles.submitButtonText, {color: theme.colors.background}]}>
<Icon
name="save"
size={24}
color={theme.colors.background}
style={styles.submitIcon}
/>
<Text
style={[
styles.submitButtonText,
{color: theme.colors.background},
]}>
</Text>
</>

View File

@ -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;
@ -34,7 +34,7 @@ interface PackageDataItem {
lineLocationId: string;
}
const StockInWheelManual: React.FC = () => {
export const StockInManual: React.FC = () => {
const theme = useTheme();
const navigation = useNavigation();
const [vehicleCode, setVehicleCode] = useState('');
@ -42,9 +42,6 @@ const StockInWheelManual: React.FC = () => {
const [packageData, setPackageData] = useState<PackageDataItem[]>([]);
const [packageDataId, setPackageDataId] = useState(0);
const [lastProcessedVehicleCode, setLastProcessedVehicleCode] = useState('');
const [areaID, setAreaID] = useState('有卤');
const [status, setStatus] = useState('合格');
const [factory, setFactory] = useState('二厂');
const [loading, setLoading] = useState(false);
const vehicleInputRef = useRef<TextInput>(null);
@ -52,25 +49,30 @@ const StockInWheelManual: React.FC = () => {
const resolveVehicle = useCallback(() => {
const code = vehicleCode.trim();
if (code.length === 0 || code === lastProcessedVehicleCode) {return;}
if (code.length === 0 || code === lastProcessedVehicleCode) {
return;
}
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;
}
@ -102,7 +104,7 @@ const StockInWheelManual: React.FC = () => {
setPackageData(prev => [...prev, newItem]);
setPackageDataId(prev => prev + 1);
} else {
Alert.alert('警告', '该物料批次已存在,不能重复添加');
DialogUtils.showWarningMessage('警告', '该物料批次已存在,不能重复添加');
}
setGoodsCode('');
@ -131,71 +133,57 @@ const StockInWheelManual: React.FC = () => {
};
const modifyNumber = (_id: string) => {
Alert.alert(
'修改数量',
'请输入新的数量',
[
{text: '取消', style: 'cancel'},
{
text: '确定',
onPress: () => {
// TODO: 需要实现一个自定义的输入对话框
Alert.alert('提示', '此功能暂不可用,请稍后再试');
},
},
],
);
DialogUtils.showConfirmMessage('修改数量', '请输入新的数量', {
cancelLabel: '取消',
confirmLabel: '确定',
confirm: () => {
// TODO: 需要实现一个自定义的输入对话框
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;
}
const areaIDIndex = areaID === '有卤' ? 1 : 2;
const factoryIndex = factory === '二厂' ? 2 : 3;
const statusIndex = {
合格: 1,
不合格: 2,
封存: 3,
待检: 4,
进口物料: 5,
}[status] || 1;
const areaIDIndex = 1; // 默认有卤
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;
@ -222,28 +210,40 @@ const StockInWheelManual: React.FC = () => {
setPackageDataId(0);
setVehicleCode('');
setPackageData([]);
Alert.alert('码盘成功', '', [{text: '我知道了'}]);
DialogUtils.showSuccessMessage(
'码盘成功',
'码盘操作已完成',
'我知道了',
);
} else {
Alert.alert('警告', `服务器返回失败:${data.message}`);
DialogUtils.showWarningMessage(
'警告',
`服务器返回失败:${data.message}`,
);
}
} catch (error) {
Alert.alert('请求发生错误', `请求服务器发生错误:${error instanceof Error ? error.message : String(error)}`, [
{text: '我知道了', style: 'cancel'},
]);
DialogUtils.showErrorMessage(
'请求发生错误',
`请求服务器发生错误:${
error instanceof Error ? error.message : String(error)
}`,
'我知道了',
);
} finally {
setLoading(false);
}
};
return (
<View style={[styles.container, {backgroundColor: theme.colors.background}]}>
<View
style={[styles.container, {backgroundColor: theme.colors.background}]}>
{/* 头部导航栏 */}
<LinearGradient
colors={[theme.colors.primary, theme.colors.secondary, theme.colors.tertiary]}
colors={theme.colors.gradients.primary}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={styles.header}>
<TouchableOpacity
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}>
<Icon name="arrow-back" size={24} color={theme.colors.background} />
@ -263,23 +263,25 @@ const StockInWheelManual: React.FC = () => {
style={styles.waveSvg}
preserveAspectRatio="none">
<Path
fill={`${theme.colors.tertiary}40`}
fill={`${theme.colors.aqua}30`}
d="M0,96L48,112C96,128,192,160,288,186.7C384,213,480,235,576,213.3C672,192,768,128,864,128C960,128,1056,192,1152,213.3C1248,235,1344,213,1392,202.7L1440,192L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"
/>
</Svg>
</View>
<View style={styles.mainContent}>
<LinearGradient
colors={theme.colors.gradients.contrast}
style={StyleSheet.absoluteFill}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
/>
<View
style={[
styles.mainContent,
{backgroundColor: theme.colors.backgroundGray},
]}>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* 信息提示区 */}
<View style={[styles.infoSection, {backgroundColor: `${theme.colors.primary}15`}]}>
<Icon name="info-outline" size={24} color={theme.colors.primary} />
<View
style={[
styles.infoSection,
{backgroundColor: `${theme.colors.aqua}15`},
]}>
<Icon name="info-outline" size={24} color={theme.colors.aqua} />
<Text style={[styles.infoText, {color: theme.colors.text}]}>
</Text>
@ -287,12 +289,23 @@ const StockInWheelManual: React.FC = () => {
{/* 载具号输入区 */}
<View style={styles.inputContainer}>
<Text style={[styles.label, {color: theme.colors.text}]}></Text>
<View style={[styles.inputWrapper, {
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
}]}>
<Icon name="qr-code-scanner" size={24} color={theme.colors.primary} style={styles.inputIcon} />
<Text style={[styles.label, {color: theme.colors.text}]}>
</Text>
<View
style={[
styles.inputWrapper,
{
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
},
]}>
<Icon
name="qr-code-scanner"
size={24}
color={theme.colors.aqua}
style={styles.inputIcon}
/>
<TextInput
ref={vehicleInputRef}
style={[styles.input, {color: theme.colors.text}]}
@ -305,7 +318,11 @@ const StockInWheelManual: React.FC = () => {
<TouchableOpacity
onPress={() => setVehicleCode('')}
style={styles.clearButton}>
<Icon name="cancel" size={20} color={theme.colors.textLight} />
<Icon
name="cancel"
size={20}
color={theme.colors.textLight}
/>
</TouchableOpacity>
)}
</View>
@ -313,12 +330,23 @@ const StockInWheelManual: React.FC = () => {
{/* 条码输入区 */}
<View style={styles.inputContainer}>
<Text style={[styles.label, {color: theme.colors.text}]}></Text>
<View style={[styles.inputWrapper, {
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
}]}>
<Icon name="qr-code-2" size={24} color={theme.colors.primary} style={styles.inputIcon} />
<Text style={[styles.label, {color: theme.colors.text}]}>
</Text>
<View
style={[
styles.inputWrapper,
{
borderColor: theme.colors.border,
backgroundColor: `${theme.colors.background}CC`,
},
]}>
<Icon
name="qr-code-2"
size={24}
color={theme.colors.aqua}
style={styles.inputIcon}
/>
<TextInput
ref={goodsInputRef}
style={[styles.input, {color: theme.colors.text}]}
@ -331,155 +359,55 @@ const StockInWheelManual: React.FC = () => {
<TouchableOpacity
onPress={() => setGoodsCode('')}
style={styles.clearButton}>
<Icon name="cancel" size={20} color={theme.colors.textLight} />
<Icon
name="cancel"
size={20}
color={theme.colors.textLight}
/>
</TouchableOpacity>
)}
</View>
</View>
{/* 选项区域 */}
<View style={styles.optionsContainer}>
{/* 区域选择 */}
<View style={styles.optionGroup}>
<Text style={[styles.optionLabel, {color: theme.colors.text}]}></Text>
<View style={styles.radioGroup}>
<TouchableOpacity
style={[
styles.radioButton,
{borderColor: theme.colors.border},
areaID === '有卤' && styles.radioButtonActive,
areaID === '有卤' && {backgroundColor: theme.colors.primary},
]}
onPress={() => setAreaID('有卤')}>
<Text
style={[
styles.radioText,
{color: theme.colors.text},
areaID === '有卤' && {color: theme.colors.background},
]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.radioButton,
{borderColor: theme.colors.border},
areaID === '无卤' && styles.radioButtonActive,
areaID === '无卤' && {backgroundColor: theme.colors.primary},
]}
onPress={() => setAreaID('无卤')}>
<Text
style={[
styles.radioText,
{color: theme.colors.text},
areaID === '无卤' && {color: theme.colors.background},
]}>
</Text>
</TouchableOpacity>
</View>
</View>
{/* 工厂选择 */}
<View style={styles.optionGroup}>
<Text style={[styles.optionLabel, {color: theme.colors.text}]}></Text>
<View style={styles.radioGroup}>
<TouchableOpacity
style={[
styles.radioButton,
{borderColor: theme.colors.border},
factory === '二厂' && styles.radioButtonActive,
factory === '二厂' && {backgroundColor: theme.colors.primary},
]}
onPress={() => setFactory('二厂')}>
<Text
style={[
styles.radioText,
{color: theme.colors.text},
factory === '二厂' && {color: theme.colors.background},
]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.radioButton,
{borderColor: theme.colors.border},
factory === '三厂' && styles.radioButtonActive,
factory === '三厂' && {backgroundColor: theme.colors.primary},
]}
onPress={() => setFactory('三厂')}>
<Text
style={[
styles.radioText,
{color: theme.colors.text},
factory === '三厂' && {color: theme.colors.background},
]}>
</Text>
</TouchableOpacity>
</View>
</View>
{/* 状态选择 - 改为与区域、工厂相同的样式 */}
<View style={styles.optionGroup}>
<Text style={[styles.optionLabel, {color: theme.colors.text}]}></Text>
<View style={styles.radioGroup}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.radioGroup}>
{['合格', '不合格', '封存', '待检', '进口物料'].map((item) => (
<TouchableOpacity
key={item}
style={[
styles.radioButton,
{borderColor: theme.colors.border},
status === item && styles.radioButtonActive,
status === item && {backgroundColor: theme.colors.primary},
]}
onPress={() => setStatus(item)}>
<Text
style={[
styles.radioText,
{color: theme.colors.text},
status === item && {color: theme.colors.background},
]}>
{item}
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
</View>
</View>
{/* 操作按钮区 */}
<View style={styles.buttonGroup}>
<LinearGradient
colors={[theme.colors.primary, theme.colors.secondary]}
colors={theme.colors.gradients.button}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={styles.button}>
<TouchableOpacity
style={styles.buttonContent}
onPress={resolveCode}>
<Icon name="add-circle-outline" size={24} color={theme.colors.background} style={styles.buttonIcon} />
<Text style={[styles.buttonText, {color: theme.colors.background}]}>
<Icon
name="add-circle-outline"
size={24}
color={theme.colors.background}
style={styles.buttonIcon}
/>
<Text
style={[styles.buttonText, {color: theme.colors.background}]}>
</Text>
</TouchableOpacity>
</LinearGradient>
<LinearGradient
colors={[theme.colors.success, theme.colors.secondary]}
colors={theme.colors.gradients.button}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={styles.button}>
<TouchableOpacity
style={styles.buttonContent}
onPress={wheelComplete}>
<Icon name="check-circle-outline" size={24} color={theme.colors.background} style={styles.buttonIcon} />
<Text style={[styles.buttonText, {color: theme.colors.background}]}>
<Icon
name="check-circle-outline"
size={24}
color={theme.colors.background}
style={styles.buttonIcon}
/>
<Text
style={[styles.buttonText, {color: theme.colors.background}]}>
</Text>
</TouchableOpacity>
@ -489,42 +417,65 @@ const StockInWheelManual: React.FC = () => {
{/* 物料列表 */}
<View style={styles.tableContainer}>
<View style={styles.tableHeader}>
<Icon name="list-alt" size={24} color={theme.colors.primary} />
<Icon name="list-alt" size={24} color={theme.colors.aqua} />
<Text style={[styles.tableTitle, {color: theme.colors.text}]}>
({packageData.length})
</Text>
</View>
{packageData.map((item, index) => (
<View
key={item.id}
style={[styles.card]}>
<LinearGradient
colors={theme.colors.gradients.contrast}
style={[StyleSheet.absoluteFill, {borderRadius: 12}]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
{packageData.map(item => (
<View key={item.id} style={[styles.card]}>
<View
style={[
StyleSheet.absoluteFill,
{
borderRadius: 12,
backgroundColor: theme.colors.background,
},
]}
/>
{/* 卡片头部 */}
<View style={styles.cardHeader}>
<View style={styles.cardHeaderLeft}>
<Text style={[styles.cardHeaderText, {color: theme.colors.background}]}>
<Text
style={[
styles.cardHeaderText,
{color: theme.colors.text},
]}>
{item.id}
</Text>
<Text style={[styles.cardHeaderText, {color: theme.colors.background}]}>
<Text
style={[
styles.cardHeaderText,
{color: theme.colors.text},
]}>
{item.segment1}
</Text>
</View>
<View style={styles.cardActions}>
<TouchableOpacity
style={[styles.cardActionButton, {backgroundColor: `${theme.colors.primary}15`}]}
style={[
styles.cardActionButton,
{backgroundColor: `${theme.colors.aqua}15`},
]}
onPress={() => showDetails(item)}>
<Icon name="info-outline" size={20} color={theme.colors.primary} />
<Icon
name="info-outline"
size={20}
color={theme.colors.aqua}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.cardActionButton, {backgroundColor: `${theme.colors.error}15`}]}
style={[
styles.cardActionButton,
{backgroundColor: `${theme.colors.error}15`},
]}
onPress={() => deleteItem(item.id)}>
<Icon name="delete-outline" size={20} color={theme.colors.error} />
<Icon
name="delete-outline"
size={20}
color={theme.colors.error}
/>
</TouchableOpacity>
</View>
</View>
@ -533,37 +484,83 @@ const StockInWheelManual: React.FC = () => {
<View style={styles.cardContent}>
<View style={styles.cardRow}>
<View style={styles.cardField}>
<Text style={[styles.cardLabel, {color: theme.colors.background}]}></Text>
<Text style={[styles.cardValue, {color: theme.colors.background}]}>{item.itemId}</Text>
<Text
style={[
styles.cardLabel,
{color: theme.colors.textLight},
]}>
</Text>
<Text
style={[styles.cardValue, {color: theme.colors.text}]}>
{item.itemId}
</Text>
</View>
<View style={styles.cardField}>
<Text style={[styles.cardLabel, {color: theme.colors.background}]}></Text>
<Text style={[styles.cardValue, {color: theme.colors.background}]}>{item.batch}</Text>
<Text
style={[
styles.cardLabel,
{color: theme.colors.textLight},
]}>
</Text>
<Text
style={[styles.cardValue, {color: theme.colors.text}]}>
{item.batch}
</Text>
</View>
</View>
<View style={styles.cardRow}>
<View style={styles.cardField}>
<Text style={[styles.cardLabel, {color: theme.colors.background}]}></Text>
<Text
style={[
styles.cardLabel,
{color: theme.colors.textLight},
]}>
</Text>
<TouchableOpacity
style={styles.editableValue}
onPress={() => modifyNumber(item.id)}>
<Text style={[styles.cardValue, {color: theme.colors.background}]}>
<Text
style={[
styles.cardValue,
{color: theme.colors.text},
]}>
{item.quantity}
</Text>
<Icon name="edit" size={16} color={theme.colors.background} />
<Icon name="edit" size={16} color={theme.colors.text} />
</TouchableOpacity>
</View>
<View style={styles.cardField}>
<Text style={[styles.cardLabel, {color: theme.colors.background}]}></Text>
<Text style={[styles.cardValue, {color: theme.colors.background}]}>{item.weight}</Text>
<Text
style={[
styles.cardLabel,
{color: theme.colors.textLight},
]}>
</Text>
<Text
style={[styles.cardValue, {color: theme.colors.text}]}>
{item.weight}
</Text>
</View>
</View>
<View style={styles.cardRow}>
<View style={styles.cardField}>
<Text style={[styles.cardLabel, {color: theme.colors.background}]}></Text>
<Text style={[styles.cardValue, {color: theme.colors.background}]}>{item.productData}</Text>
<Text
style={[
styles.cardLabel,
{color: theme.colors.textLight},
]}>
</Text>
<Text
style={[styles.cardValue, {color: theme.colors.text}]}>
{item.productData}
</Text>
</View>
</View>
</View>
@ -574,9 +571,17 @@ const StockInWheelManual: React.FC = () => {
</View>
{loading && (
<View style={[styles.loadingOverlay, {backgroundColor: 'rgba(0, 0, 0, 0.7)'}]}>
<View style={[styles.loadingCard, {backgroundColor: theme.colors.background}]}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<View
style={[
styles.loadingOverlay,
{backgroundColor: 'rgba(0, 0, 0, 0.7)'},
]}>
<View
style={[
styles.loadingCard,
{backgroundColor: theme.colors.background},
]}>
<ActivityIndicator size="large" color={theme.colors.aqua} />
<Text style={[styles.loadingText, {color: theme.colors.text}]}>
...
</Text>
@ -675,36 +680,7 @@ const styles = StyleSheet.create({
clearButton: {
padding: 4,
},
optionsContainer: {
marginBottom: 20,
},
optionGroup: {
marginBottom: 16,
},
optionLabel: {
fontSize: 16,
marginBottom: 8,
fontWeight: '500',
},
radioGroup: {
flexDirection: 'row',
gap: 12,
},
radioButton: {
borderWidth: 1,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 16,
minWidth: 80,
alignItems: 'center',
},
radioButtonActive: {
borderColor: 'transparent',
},
radioText: {
fontSize: 14,
fontWeight: '500',
},
buttonGroup: {
flexDirection: 'row',
gap: 12,
@ -821,4 +797,4 @@ const styles = StyleSheet.create({
},
});
export default StockInWheelManual;
export default StockInManual;

View File

@ -18,4 +18,4 @@ export interface DeviceInfo {
model: string;
osVersion: string;
serialNumber: string;
}
}

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';

View File

@ -35,4 +35,4 @@ export const storage = {
console.error('Error clearing data', error);
}
},
};
};