feat: 增加库位屏幕的下拉刷新功能,优化API响应处理,改进界面样式和用户体验

This commit is contained in:
xudan 2025-07-25 16:47:01 +08:00
parent 4ee7e9af54
commit c03df200ab
6 changed files with 282 additions and 66 deletions

View File

@ -9,7 +9,9 @@
"apiEndpoints": { "apiEndpoints": {
"getTasks": "/api/vwed-task/list", "getTasks": "/api/vwed-task/list",
"getTaskDetail": "/api/vwed-task/{taskId}", "getTaskDetail": "/api/vwed-task/{taskId}",
"runTask": "/api/vwed-task-edit/run" "runTask": "/api/vwed-task-edit/run",
"getLocationList": "/api/vwed-operate-point/list",
"batchUpdateLocation": "/api/vwed-operate-point/batch-status"
}, },
"taskApiEndpoints": { "taskApiEndpoints": {
"getTaskList": "/task", "getTaskList": "/task",

View File

@ -1,7 +1,16 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { StyleSheet, View, FlatList, Text, ActivityIndicator, Alert } from 'react-native'; import {
import { SearchBar, CheckBox, Button, useTheme, Overlay, ListItem } from '@rneui/themed'; StyleSheet,
View,
FlatList,
Text,
ActivityIndicator,
Alert,
RefreshControl,
} from 'react-native';
import { SearchBar, CheckBox, Button, Overlay, ListItem } from '@rneui/themed';
import api from '../services/api'; import api from '../services/api';
import { getConfig } from '../services/configService';
interface Location { interface Location {
id: string; id: string;
@ -13,20 +22,35 @@ interface Location {
} }
const LocationScreen = () => { const LocationScreen = () => {
const { theme } = useTheme();
const [locations, setLocations] = useState<Location[]>([]); const [locations, setLocations] = useState<Location[]>([]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedLocations, setSelectedLocations] = useState<string[]>([]); const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [isOverlayVisible, setOverlayVisible] = useState(false); const [isOverlayVisible, setOverlayVisible] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const fetchData = useCallback(async (query = '') => { const fetchData = useCallback(async (query = '') => {
setLoading(true); setLoading(true);
try { try {
const response = await api.get('/api/vwed-operate-point/list', { const config = await getConfig();
const endpoint =
(config?.apiEndpoints as any)?.getLocationList ||
'/api/vwed-operate-point/list';
const response = await api.get(endpoint, {
params: { layer_name: query }, params: { layer_name: query },
}); });
setLocations(response.storage_locations); console.log('API Response:', response); // 调试日志
// API 拦截器已经处理了响应,直接使用返回的数据
const data = response as any;
if (data && data.storage_locations) {
setLocations(data.storage_locations);
} else if (Array.isArray(data)) {
setLocations(data);
} else {
console.warn('Unexpected response format:', data);
setLocations([]);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Alert.alert('错误', '加载库位列表失败。'); Alert.alert('错误', '加载库位列表失败。');
@ -43,9 +67,20 @@ const LocationScreen = () => {
fetchData(search); fetchData(search);
}; };
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
await fetchData(search);
} catch (error) {
console.error('刷新失败:', error);
} finally {
setIsRefreshing(false);
}
}, [fetchData, search]);
const toggleSelection = (id: string) => { const toggleSelection = (id: string) => {
setSelectedLocations(prev => setSelectedLocations(prev =>
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id] prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id],
); );
}; };
@ -56,12 +91,19 @@ const LocationScreen = () => {
} }
setOverlayVisible(false); setOverlayVisible(false);
try { try {
const response = await api.put('/api/vwed-operate-point/batch-status', { const config = await getConfig();
const endpoint =
(config?.apiEndpoints as any)?.batchUpdateLocation ||
'/api/vwed-operate-point/batch-status';
const response = await api.put(endpoint, {
layer_names: selectedLocations, layer_names: selectedLocations,
action: action, action: action,
}); });
const { success_count, failed_count, results } = response; console.log('Batch Update Response:', response); // 调试日志
// API 拦截器已经处理了响应,直接使用返回的数据
const { success_count, failed_count, results } = response as any;
let message = `批量操作完成:\n成功 ${success_count} 个, 失败 ${failed_count} 个。\n\n`; let message = `批量操作完成:\n成功 ${success_count} 个, 失败 ${failed_count} 个。\n\n`;
if (failed_count > 0) { if (failed_count > 0) {
message += '失败详情:\n'; message += '失败详情:\n';
@ -75,7 +117,7 @@ const LocationScreen = () => {
fetchData(search); fetchData(search);
setSelectedLocations([]); setSelectedLocations([]);
} catch (error) { } catch (error: any) {
console.error(error); console.error(error);
Alert.alert('错误', `批量操作失败: ${error.message}`); Alert.alert('错误', `批量操作失败: ${error.message}`);
} }
@ -91,72 +133,137 @@ const LocationScreen = () => {
]; ];
const renderItem = ({ item }: { item: Location }) => ( const renderItem = ({ item }: { item: Location }) => (
<View style={[styles.itemContainer, { backgroundColor: theme.colors.background }]}> <View style={styles.itemContainer}>
<CheckBox <CheckBox
checked={selectedLocations.includes(item.id)} checked={selectedLocations.includes(item.id)}
onPress={() => toggleSelection(item.id)} onPress={() => toggleSelection(item.id)}
containerStyle={{ backgroundColor: 'transparent' }} containerStyle={styles.checkboxContainer}
checkedColor="#4CAF50"
uncheckedColor="#666"
/> />
<Text style={[styles.itemText, { color: theme.colors.black }]}>{item.layer_name}</Text> <Text style={styles.itemText}>{item.layer_name}</Text>
<Text style={[styles.itemText, { color: item.is_occupied ? 'red' : 'green' }]}>{item.is_occupied ? '是' : '否'}</Text> <Text
<Text style={[styles.itemText, { color: item.is_locked ? 'red' : 'green' }]}>{item.is_locked ? '是' : '否'}</Text> style={[
<Text style={[styles.itemText, { color: item.is_empty_tray ? 'orange' : 'green' }]}>{item.is_empty_tray ? '是' : '否'}</Text> styles.itemText,
<Text style={[styles.itemText, { color: item.is_disabled ? 'red' : 'green' }]}>{item.is_disabled ? '是' : '否'}</Text> { color: item.is_occupied ? '#F44336' : '#4CAF50' },
]}
>
{item.is_occupied ? '是' : '否'}
</Text>
<Text
style={[
styles.itemText,
{ color: item.is_locked ? '#F44336' : '#4CAF50' },
]}
>
{item.is_locked ? '是' : '否'}
</Text>
<Text
style={[
styles.itemText,
{ color: item.is_empty_tray ? '#FF9800' : '#4CAF50' },
]}
>
{item.is_empty_tray ? '是' : '否'}
</Text>
<Text
style={[
styles.itemText,
{ color: item.is_disabled ? '#F44336' : '#4CAF50' },
]}
>
{item.is_disabled ? '是' : '否'}
</Text>
</View> </View>
); );
if (loading) { if (loading) {
return ( return (
<View style={styles.loader}> <View style={styles.loader}>
<ActivityIndicator size="large" color={theme.colors.primary} /> <ActivityIndicator size="large" color="#4CAF50" />
<Text style={styles.loadingText}>...</Text>
</View> </View>
); );
} }
return ( return (
<View style={[styles.container, { backgroundColor: theme.colors.grey5 }]}> <View style={styles.container}>
<View style={styles.searchContainer}> <View style={styles.searchContainer}>
<SearchBar <SearchBar
placeholder="搜索库位..." placeholder="搜索库位..."
onChangeText={setSearch} onChangeText={setSearch}
value={search} value={search}
containerStyle={styles.searchBarContainer} containerStyle={styles.searchBarContainer}
inputContainerStyle={{ backgroundColor: theme.colors.grey4 }} inputContainerStyle={styles.searchInputContainer}
/> inputStyle={styles.searchInput}
<Button title="搜索" onPress={handleSearch} buttonStyle={styles.searchButton} /> placeholderTextColor="#999"
</View> searchIcon={{ color: '#999' }}
clearIcon={{ color: '#999' }}
/>
<Button
title="搜索"
onPress={handleSearch}
buttonStyle={styles.searchButton}
titleStyle={styles.searchButtonText}
/>
</View>
<Button <Button
title="批量操作" title="批量操作"
onPress={() => setOverlayVisible(true)} onPress={() => setOverlayVisible(true)}
containerStyle={styles.batchButtonContainer} containerStyle={styles.batchButtonContainer}
buttonStyle={{ backgroundColor: theme.colors.primary }} buttonStyle={styles.batchButton}
titleStyle={styles.batchButtonText}
/> />
<Overlay isVisible={isOverlayVisible} onBackdropPress={() => setOverlayVisible(false)}> <Overlay
<View> isVisible={isOverlayVisible}
onBackdropPress={() => setOverlayVisible(false)}
overlayStyle={styles.overlay}
>
<View style={styles.overlayContainer}>
<Text style={styles.overlayTitle}></Text>
{actions.map((item, index) => ( {actions.map((item, index) => (
<ListItem key={index} onPress={() => handleBatchUpdate(item.action)} bottomDivider> <ListItem
key={index}
onPress={() => handleBatchUpdate(item.action)}
bottomDivider
containerStyle={styles.listItemContainer}
>
<ListItem.Content> <ListItem.Content>
<ListItem.Title>{item.title}</ListItem.Title> <ListItem.Title style={styles.listItemTitle}>
{item.title}
</ListItem.Title>
</ListItem.Content> </ListItem.Content>
</ListItem> </ListItem>
))} ))}
</View> </View>
</Overlay> </Overlay>
<View style={[styles.headerContainer, { backgroundColor: theme.colors.grey4 }]}> <View style={styles.headerContainer}>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text> <Text style={styles.headerText}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text> <Text style={styles.headerText}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text> <Text style={styles.headerText}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text> <Text style={styles.headerText}></Text>
<Text style={[styles.headerText, { color: theme.colors.black }]}></Text> <Text style={styles.headerText}></Text>
</View> </View>
<FlatList <FlatList
data={locations} data={locations}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item.id} keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />} ItemSeparatorComponent={() => <View style={styles.separator} />}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={['#4CAF50']}
tintColor="#4CAF50"
title="下拉刷新"
titleColor="#999"
/>
}
showsVerticalScrollIndicator={false}
/> />
</View> </View>
); );
@ -165,54 +272,131 @@ const LocationScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#1a1a1a',
}, },
loader: { loader: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#1a1a1a',
},
loadingText: {
color: '#999',
marginTop: 10,
fontSize: 16,
}, },
searchContainer: { searchContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#2a2a2a',
paddingHorizontal: 10,
paddingVertical: 5,
}, },
searchBarContainer: { searchBarContainer: {
flex: 1, flex: 1,
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderTopWidth: 0, borderTopWidth: 0,
borderBottomWidth: 0, borderBottomWidth: 0,
paddingHorizontal: 0,
},
searchInputContainer: {
backgroundColor: '#333',
borderRadius: 8,
},
searchInput: {
color: '#fff',
fontSize: 16,
}, },
searchButton: { searchButton: {
marginRight: 10, backgroundColor: '#4CAF50',
borderRadius: 8,
paddingHorizontal: 20,
paddingVertical: 12,
marginLeft: 10,
},
searchButtonText: {
color: '#fff',
fontWeight: 'bold',
}, },
batchButtonContainer: { batchButtonContainer: {
margin: 10, margin: 15,
},
batchButton: {
backgroundColor: '#2196F3',
borderRadius: 8,
paddingVertical: 12,
},
batchButtonText: {
color: '#fff',
fontWeight: 'bold',
fontSize: 16,
},
overlay: {
backgroundColor: '#2a2a2a',
borderRadius: 12,
padding: 0,
},
overlayContainer: {
minWidth: 250,
paddingVertical: 10,
},
overlayTitle: {
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
paddingVertical: 15,
borderBottomWidth: 1,
borderBottomColor: '#444',
},
listItemContainer: {
backgroundColor: 'transparent',
paddingVertical: 15,
paddingHorizontal: 20,
borderBottomColor: '#444',
},
listItemTitle: {
fontSize: 16,
textAlign: 'center',
color: '#fff',
}, },
itemContainer: { itemContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 8, paddingVertical: 12,
paddingHorizontal: 10, paddingHorizontal: 10,
backgroundColor: '#2a2a2a',
},
checkboxContainer: {
backgroundColor: 'transparent',
borderWidth: 0,
padding: 0,
marginLeft: 0,
marginRight: 5,
}, },
itemText: { itemText: {
flex: 1, flex: 1,
textAlign: 'center', textAlign: 'center',
fontSize: 14, fontSize: 14,
color: '#fff',
}, },
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
padding: 12, padding: 12,
backgroundColor: '#333',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#ccc', borderBottomColor: '#444',
}, },
headerText: { headerText: {
flex: 1, flex: 1,
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 16, fontSize: 16,
color: '#fff',
}, },
separator: { separator: {
height: 1, height: 1,
backgroundColor: '#e0e0e0', backgroundColor: '#333',
}, },
}); });

View File

@ -24,6 +24,7 @@ export default function SettingsScreen() {
const [settings, setSettings] = useState<AppSettings>({ const [settings, setSettings] = useState<AppSettings>({
configFileName: '', configFileName: '',
serverUrl: '', serverUrl: '',
taskServerUrl: '',
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [configStatus, setConfigStatus] = useState<string>('未加载'); const [configStatus, setConfigStatus] = useState<string>('未加载');
@ -251,14 +252,29 @@ export default function SettingsScreen() {
<Text style={styles.sectionTitle}></Text> <Text style={styles.sectionTitle}></Text>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}></Text> <Text style={styles.label}></Text>
<TextInput <TextInput
style={styles.textInput} style={styles.textInput}
value={settings.serverUrl} value={settings.serverUrl}
onChangeText={text => onChangeText={text =>
setSettings(prev => ({ ...prev, serverUrl: text })) setSettings(prev => ({ ...prev, serverUrl: text }))
} }
placeholder="http://localhost:3000/api" placeholder="http://192.168.189.206:8000"
placeholderTextColor="#999"
autoCapitalize="none"
keyboardType="url"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<TextInput
style={styles.textInput}
value={settings.taskServerUrl}
onChangeText={text =>
setSettings(prev => ({ ...prev, taskServerUrl: text }))
}
placeholder="http://192.168.189.206:8080/jeecg-boot"
placeholderTextColor="#999" placeholderTextColor="#999"
autoCapitalize="none" autoCapitalize="none"
keyboardType="url" keyboardType="url"
@ -308,7 +324,7 @@ export default function SettingsScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f5f5f5', backgroundColor: '#1a1a1a',
}, },
content: { content: {
padding: 20, padding: 20,
@ -318,10 +334,10 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 30, marginBottom: 30,
color: '#333', color: '#fff',
}, },
section: { section: {
backgroundColor: 'white', backgroundColor: '#2a2a2a',
borderRadius: 10, borderRadius: 10,
padding: 20, padding: 20,
marginBottom: 20, marginBottom: 20,
@ -330,7 +346,7 @@ const styles = StyleSheet.create({
width: 0, width: 0,
height: 2, height: 2,
}, },
shadowOpacity: 0.1, shadowOpacity: 0.3,
shadowRadius: 3.84, shadowRadius: 3.84,
elevation: 5, elevation: 5,
}, },
@ -338,7 +354,7 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 15, marginBottom: 15,
color: '#333', color: '#fff',
}, },
inputGroup: { inputGroup: {
marginBottom: 15, marginBottom: 15,
@ -346,7 +362,7 @@ const styles = StyleSheet.create({
label: { label: {
fontSize: 16, fontSize: 16,
marginBottom: 8, marginBottom: 8,
color: '#333', color: '#fff',
fontWeight: '500', fontWeight: '500',
}, },
inputRow: { inputRow: {
@ -356,16 +372,17 @@ const styles = StyleSheet.create({
textInput: { textInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#ddd', borderColor: '#444',
borderRadius: 8, borderRadius: 8,
padding: 12, padding: 12,
fontSize: 16, fontSize: 16,
backgroundColor: '#fafafa', backgroundColor: '#333',
color: '#fff',
}, },
extension: { extension: {
marginLeft: 8, marginLeft: 8,
fontSize: 16, fontSize: 16,
color: '#666', color: '#999',
fontWeight: '500', fontWeight: '500',
}, },
statusRow: { statusRow: {
@ -375,12 +392,12 @@ const styles = StyleSheet.create({
}, },
statusLabel: { statusLabel: {
fontSize: 16, fontSize: 16,
color: '#333', color: '#fff',
fontWeight: '500', fontWeight: '500',
}, },
statusText: { statusText: {
fontSize: 16, fontSize: 16,
color: '#007AFF', color: '#4CAF50',
marginLeft: 8, marginLeft: 8,
}, },
button: { button: {
@ -418,7 +435,7 @@ const styles = StyleSheet.create({
marginTop: 10, marginTop: 10,
}, },
infoSection: { infoSection: {
backgroundColor: 'white', backgroundColor: '#2a2a2a',
borderRadius: 10, borderRadius: 10,
padding: 20, padding: 20,
marginBottom: 20, marginBottom: 20,
@ -427,12 +444,12 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 15, marginBottom: 15,
color: '#333', color: '#fff',
}, },
infoText: { infoText: {
fontSize: 14, fontSize: 14,
lineHeight: 20, lineHeight: 20,
color: '#666', color: '#999',
marginBottom: 5, marginBottom: 5,
}, },
loadingOverlay: { loadingOverlay: {
@ -441,7 +458,7 @@ const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.8)', backgroundColor: 'rgba(26, 26, 26, 0.8)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 1, zIndex: 1,
@ -449,7 +466,7 @@ const styles = StyleSheet.create({
loadingText: { loadingText: {
marginTop: 10, marginTop: 10,
fontSize: 16, fontSize: 16,
color: '#007AFF', color: '#4CAF50',
}, },
clearButton: { clearButton: {
backgroundColor: '#FF3B30', backgroundColor: '#FF3B30',

View File

@ -11,7 +11,18 @@ api.interceptors.request.use(
async config => { async config => {
const appConfig = await getConfig(); const appConfig = await getConfig();
const settings = await getSettings(); const settings = await getSettings();
const serverUrl = settings.serverUrl || appConfig?.serverUrl;
// 优先使用设置中的服务器地址,然后是配置文件中的地址
let serverUrl = '';
// 根据请求的URL判断使用哪个服务器地址
if (config.url?.includes('/task')) {
// 任务管理相关的API使用taskServerUrl
serverUrl = settings.taskServerUrl || appConfig?.taskServerUrl || '';
} else {
// 其他API使用普通的serverUrl库位管理等
serverUrl = settings.serverUrl || appConfig?.serverUrl || '';
}
if (!serverUrl) { if (!serverUrl) {
const error = new Error('服务器地址未配置'); const error = new Error('服务器地址未配置');

View File

@ -8,6 +8,7 @@ const CONFIG_CACHE_KEY = 'cached_config';
const DEFAULT_SETTINGS: AppSettings = { const DEFAULT_SETTINGS: AppSettings = {
configFileName: 'config.json', configFileName: 'config.json',
serverUrl: '', serverUrl: '',
taskServerUrl: '',
}; };
// 获取设置 // 获取设置

View File

@ -28,4 +28,5 @@ export interface AppConfig {
export interface AppSettings { export interface AppSettings {
configFileName: string; configFileName: string;
serverUrl: string; serverUrl: string;
taskServerUrl?: string; // 任务管理服务器地址
} }