webapp/src/screens/SettingsScreen.tsx

468 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
TouchableOpacity,
Alert,
ActivityIndicator,
ScrollView,
} from 'react-native';
import { AppSettings } from '../types/config';
import {
getSettings,
saveSettings,
downloadConfig,
getConfig,
clearCachedConfig,
} from '../services/configService';
import { useTasks } from '../context/TasksContext';
export default function SettingsScreen() {
const { refreshConfig } = useTasks();
const [settings, setSettings] = useState<AppSettings>({
configFileName: '',
serverUrl: '',
});
const [loading, setLoading] = useState(false);
const [configStatus, setConfigStatus] = useState<string>('未加载');
// 组件加载时获取设置
useEffect(() => {
loadSettings();
checkConfigStatus();
}, []);
const loadSettings = async () => {
try {
const currentSettings = await getSettings();
setSettings(currentSettings);
} catch (error) {
Alert.alert('错误', '加载设置失败');
}
};
const checkConfigStatus = async () => {
const config = await getConfig();
if (config) {
setConfigStatus(
`已加载 (版本: ${config.version}, 任务数: ${
config.tasks?.length || 0
})`,
);
} else {
setConfigStatus('未加载');
}
};
const handleSaveSettings = async () => {
if (!settings.configFileName.trim()) {
Alert.alert('错误', '请输入配置文件名');
return;
}
if (!settings.serverUrl.trim()) {
Alert.alert('错误', '请输入服务器地址');
return;
}
try {
await saveSettings(settings);
Alert.alert('成功', '设置已保存');
} catch (error) {
Alert.alert('错误', '保存设置失败');
}
};
const handleRefreshLocalConfig = async () => {
setLoading(true);
try {
// 清除缓存并重新加载本地配置
await clearCachedConfig();
await refreshConfig();
await checkConfigStatus();
Alert.alert('成功', '本地配置已刷新!');
} catch (error) {
console.error('刷新本地配置失败:', error);
Alert.alert('错误', '刷新本地配置失败');
} finally {
setLoading(false);
}
};
const handleDownloadConfig = async () => {
if (!settings.configFileName.trim()) {
Alert.alert('错误', '请先输入配置文件名');
return;
}
if (!settings.serverUrl.trim()) {
Alert.alert('错误', '请先输入服务器地址');
return;
}
setLoading(true);
try {
// 先保存设置
await saveSettings(settings);
// 下载配置文件
const config = await downloadConfig(
settings.serverUrl,
settings.configFileName,
);
Alert.alert(
'成功',
`配置文件下载成功!\n版本: ${config.version}\n任务数量: ${
config.tasks?.length || 0
}`,
[
{
text: '确定',
onPress: async () => {
await refreshConfig();
checkConfigStatus();
},
},
],
);
} catch (error) {
Alert.alert(
'下载失败',
error instanceof Error ? error.message : '未知错误',
);
} finally {
setLoading(false);
}
};
const handleClearCache = async () => {
Alert.alert(
'确认清除',
'确定要清除缓存的配置文件吗?系统将重新加载本地 config.json 文件。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: async () => {
setLoading(true);
try {
await clearCachedConfig();
await refreshConfig();
checkConfigStatus();
Alert.alert('成功', '缓存已清除,已重新加载配置');
} catch (error) {
Alert.alert('错误', '清除缓存失败');
} finally {
setLoading(false);
}
},
},
],
);
};
const handleTestConnection = async () => {
if (!settings.serverUrl.trim()) {
Alert.alert('错误', '请先输入服务器地址');
return;
}
setLoading(true);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${settings.serverUrl}/health`, {
method: 'GET',
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.ok) {
Alert.alert('连接成功', '服务器连接正常');
} else {
Alert.alert('连接失败', `HTTP ${response.status}`);
}
} catch (error) {
Alert.alert('连接失败', '无法连接到服务器');
} finally {
setLoading(false);
}
};
return (
<ScrollView style={styles.container}>
<View style={styles.content}>
{loading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>...</Text>
</View>
)}
{/* 配置状态 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.statusText}>{configStatus}</Text>
</View>
{/* 配置文件设置 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<TextInput
style={styles.textInput}
value={settings.configFileName}
onChangeText={text =>
setSettings(prev => ({ ...prev, configFileName: text }))
}
placeholder="config"
placeholderTextColor="#999"
autoCapitalize="none"
/>
</View>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.downloadButton]}
onPress={handleDownloadConfig}
disabled={loading}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.refreshButton]}
onPress={handleRefreshLocalConfig}
disabled={loading}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.clearButton]}
onPress={handleClearCache}
disabled={loading}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
</View>
{/* 服务器设置 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<TextInput
style={styles.textInput}
value={settings.serverUrl}
onChangeText={text =>
setSettings(prev => ({ ...prev, serverUrl: text }))
}
placeholder="http://localhost:3000/api"
placeholderTextColor="#999"
autoCapitalize="none"
keyboardType="url"
/>
</View>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.testButton]}
onPress={handleTestConnection}
disabled={loading}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.saveButton]}
onPress={handleSaveSettings}
disabled={loading}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
</View>
{/* 说明信息 */}
<View style={styles.infoSection}>
<Text style={styles.infoTitle}>使</Text>
<Text style={styles.infoText}></Text>
<Text style={styles.infoText}>
1.
</Text>
<Text style={styles.infoText}>2. config.json </Text>
<Text style={styles.infoText}>3. </Text>
<Text style={styles.infoText}>
"下载配置文件"
</Text>
<Text style={styles.infoText}>
"清除缓存"
</Text>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
color: '#333',
},
section: {
backgroundColor: 'white',
borderRadius: 10,
padding: 20,
marginBottom: 20,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
color: '#333',
},
inputGroup: {
marginBottom: 15,
},
label: {
fontSize: 16,
marginBottom: 8,
color: '#333',
fontWeight: '500',
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fafafa',
},
extension: {
marginLeft: 8,
fontSize: 16,
color: '#666',
fontWeight: '500',
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
},
statusLabel: {
fontSize: 16,
color: '#333',
fontWeight: '500',
},
statusText: {
fontSize: 16,
color: '#007AFF',
marginLeft: 8,
},
button: {
borderRadius: 8,
padding: 12,
alignItems: 'center',
minHeight: 48,
justifyContent: 'center',
},
downloadButton: {
backgroundColor: '#007AFF',
flex: 1,
marginRight: 10,
},
refreshButton: {
backgroundColor: '#32D74B',
flex: 1,
},
testButton: {
backgroundColor: '#34C759',
flex: 1,
marginRight: 10,
},
saveButton: {
backgroundColor: '#FF9500',
flex: 1,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
buttonRow: {
flexDirection: 'row',
marginTop: 10,
},
infoSection: {
backgroundColor: 'white',
borderRadius: 10,
padding: 20,
marginBottom: 20,
},
infoTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
color: '#333',
},
infoText: {
fontSize: 14,
lineHeight: 20,
color: '#666',
marginBottom: 5,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#007AFF',
},
clearButton: {
backgroundColor: '#FF3B30',
},
});