feat: 添加库位管理功能,创建库位列表和批量操作界面,支持库位状态查询和更新

This commit is contained in:
xudan 2025-07-25 16:18:50 +08:00
parent e18d0314e6
commit 4ee7e9af54
8 changed files with 1342 additions and 19 deletions

16
gemini.md Normal file
View File

@ -0,0 +1,16 @@
# Gemini Project: MyReactNativeApp
This is a React Native application built with TypeScript.
## Project Structure
- `src/`: Contains the main source code for the application.
- `components/`: Reusable React components.
- `context/`: React context for state management.
- `hooks/`: Custom React hooks.
- `navigation/`: Navigation logic using React Navigation.
- `screens/`: Application screens.
- `services/`: Services for API calls and other business logic.
- `types/`: TypeScript type definitions.
- `android/`: Android specific code.
- `ios/`: iOS specific code.

View File

@ -8,4 +8,6 @@ import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);

85
package-lock.json generated
View File

@ -19,6 +19,7 @@
"axios": "^1.11.0",
"react": "19.1.0",
"react-native": "0.80.1",
"react-native-elements": "^3.4.3",
"react-native-gesture-handler": "^2.27.1",
"react-native-get-random-values": "^1.11.0",
"react-native-safe-area-context": "^5.5.2",
@ -9203,6 +9204,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -10297,6 +10305,15 @@
"node": ">=8"
}
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@ -10938,6 +10955,74 @@
}
}
},
"node_modules/react-native-elements": {
"version": "3.4.3",
"resolved": "https://registry.npmmirror.com/react-native-elements/-/react-native-elements-3.4.3.tgz",
"integrity": "sha512-VtZc25EecPZyUBER85zFK9ZbY6kkUdcm1ZwJ9hdoGSCr1R/GFgxor4jngOcSYeMvQ+qimd5No44OVJW3rSJECA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@types/react-native-vector-icons": "^6.4.6",
"color": "^3.1.2",
"deepmerge": "^4.2.2",
"hoist-non-react-statics": "^3.3.2",
"lodash.isequal": "^4.5.0",
"opencollective-postinstall": "^2.0.3",
"react-native-ratings": "8.0.4",
"react-native-size-matters": "^0.3.1"
},
"peerDependencies": {
"react-native-safe-area-context": ">= 3.0.0",
"react-native-vector-icons": ">7.0.0"
}
},
"node_modules/react-native-elements/node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/react-native-elements/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/react-native-elements/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/react-native-elements/node_modules/react-native-ratings": {
"version": "8.0.4",
"resolved": "https://registry.npmmirror.com/react-native-ratings/-/react-native-ratings-8.0.4.tgz",
"integrity": "sha512-Xczu5lskIIRD6BEdz9A0jDRpEck/SFxRqiglkXi0u67yAtI1/pcJC76P4MukCbT8K4BPVl+42w83YqXBoBRl7A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-elements/node_modules/react-native-size-matters": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz",
"integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==",
"license": "MIT",
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.27.1",
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.27.1.tgz",

View File

@ -21,6 +21,7 @@
"axios": "^1.11.0",
"react": "19.1.0",
"react-native": "0.80.1",
"react-native-elements": "^3.4.3",
"react-native-gesture-handler": "^2.27.1",
"react-native-get-random-values": "^1.11.0",
"react-native-safe-area-context": "^5.5.2",

View File

@ -7,6 +7,7 @@ import TaskEditScreen from '../screens/TaskEditScreen';
import RunScreen from '../screens/RunScreen';
import EditScreen from '../screens/EditScreen';
import SettingsScreen from '../screens/SettingsScreen';
import LocationScreen from '../screens/LocationScreen';
const HomeStack = createStackNavigator();
const Tab = createBottomTabNavigator();
@ -74,7 +75,7 @@ export default function AppNavigator() {
options={{ headerShown: false }}
/>
<Tab.Screen name="任务列表" component={RunScreen} />
<Tab.Screen name="编辑" component={EditScreen} />
<Tab.Screen name="库位" component={LocationScreen} />
<Tab.Screen name="设置" component={SettingsScreen} />
</Tab.Navigator>
);

View File

@ -1,18 +0,0 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function EditScreen() {
return (
<View style={styles.screenContainer}>
<Text>!</Text>
</View>
);
}
const styles = StyleSheet.create({
screenContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@ -0,0 +1,219 @@
import React, { useEffect, useState, useCallback } from 'react';
import { StyleSheet, View, FlatList, Text, ActivityIndicator, Alert } from 'react-native';
import { SearchBar, CheckBox, Button, useTheme, Overlay, ListItem } from '@rneui/themed';
import api from '../services/api';
interface Location {
id: string;
layer_name: string;
is_occupied: boolean;
is_locked: boolean;
is_empty_tray: boolean;
is_disabled: boolean;
}
const LocationScreen = () => {
const { theme } = useTheme();
const [locations, setLocations] = useState<Location[]>([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [isOverlayVisible, setOverlayVisible] = useState(false);
const fetchData = useCallback(async (query = '') => {
setLoading(true);
try {
const response = await api.get('/api/vwed-operate-point/list', {
params: { layer_name: query },
});
setLocations(response.storage_locations);
} catch (error) {
console.error(error);
Alert.alert('错误', '加载库位列表失败。');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleSearch = () => {
fetchData(search);
};
const toggleSelection = (id: string) => {
setSelectedLocations(prev =>
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
);
};
const handleBatchUpdate = async (action: string) => {
if (selectedLocations.length === 0) {
Alert.alert('未选择', '请选择要操作的库位。');
return;
}
setOverlayVisible(false);
try {
const response = await api.put('/api/vwed-operate-point/batch-status', {
layer_names: selectedLocations,
action: action,
});
const { success_count, failed_count, results } = response;
let message = `批量操作完成:\n成功 ${success_count} 个, 失败 ${failed_count} 个。\n\n`;
if (failed_count > 0) {
message += '失败详情:\n';
results.forEach((res: any) => {
if (!res.success) {
message += `${res.layer_name}: ${res.message}\n`;
}
});
}
Alert.alert('操作结果', message);
fetchData(search);
setSelectedLocations([]);
} catch (error) {
console.error(error);
Alert.alert('错误', `批量操作失败: ${error.message}`);
}
};
const actions = [
{ title: '禁用', action: 'disable' },
{ title: '启用', action: 'enable' },
{ title: '占用', action: 'occupy' },
{ title: '释放', action: 'release' },
{ title: '锁定', action: 'lock' },
{ title: '解锁', action: 'unlock' },
];
const renderItem = ({ item }: { item: Location }) => (
<View style={[styles.itemContainer, { backgroundColor: theme.colors.background }]}>
<CheckBox
checked={selectedLocations.includes(item.id)}
onPress={() => toggleSelection(item.id)}
containerStyle={{ backgroundColor: 'transparent' }}
/>
<Text style={[styles.itemText, { color: theme.colors.black }]}>{item.layer_name}</Text>
<Text style={[styles.itemText, { color: item.is_occupied ? 'red' : 'green' }]}>{item.is_occupied ? '是' : '否'}</Text>
<Text style={[styles.itemText, { color: item.is_locked ? 'red' : 'green' }]}>{item.is_locked ? '是' : '否'}</Text>
<Text style={[styles.itemText, { color: item.is_empty_tray ? 'orange' : 'green' }]}>{item.is_empty_tray ? '是' : '否'}</Text>
<Text style={[styles.itemText, { color: item.is_disabled ? 'red' : 'green' }]}>{item.is_disabled ? '是' : '否'}</Text>
</View>
);
if (loading) {
return (
<View style={styles.loader}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: theme.colors.grey5 }]}>
<View style={styles.searchContainer}>
<SearchBar
placeholder="搜索库位..."
onChangeText={setSearch}
value={search}
containerStyle={styles.searchBarContainer}
inputContainerStyle={{ backgroundColor: theme.colors.grey4 }}
/>
<Button title="搜索" onPress={handleSearch} buttonStyle={styles.searchButton} />
</View>
<Button
title="批量操作"
onPress={() => setOverlayVisible(true)}
containerStyle={styles.batchButtonContainer}
buttonStyle={{ backgroundColor: theme.colors.primary }}
/>
<Overlay isVisible={isOverlayVisible} onBackdropPress={() => setOverlayVisible(false)}>
<View>
{actions.map((item, index) => (
<ListItem key={index} onPress={() => handleBatchUpdate(item.action)} bottomDivider>
<ListItem.Content>
<ListItem.Title>{item.title}</ListItem.Title>
</ListItem.Content>
</ListItem>
))}
</View>
</Overlay>
<View style={[styles.headerContainer, { backgroundColor: theme.colors.grey4 }]}>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text>
</View>
<FlatList
data={locations}
renderItem={renderItem}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
loader: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
},
searchBarContainer: {
flex: 1,
backgroundColor: 'transparent',
borderTopWidth: 0,
borderBottomWidth: 0,
},
searchButton: {
marginRight: 10,
},
batchButtonContainer: {
margin: 10,
},
itemContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 10,
},
itemText: {
flex: 1,
textAlign: 'center',
fontSize: 14,
},
headerContainer: {
flexDirection: 'row',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
headerText: {
flex: 1,
textAlign: 'center',
fontWeight: 'bold',
fontSize: 16,
},
separator: {
height: 1,
backgroundColor: '#e0e0e0',
},
});
export default LocationScreen;

1017
库位管理接口文档.md Normal file

File diff suppressed because it is too large Load Diff