2025-07-22 15:10:57 +08:00
|
|
|
|
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';
|
2025-07-21 15:10:39 +08:00
|
|
|
|
|
|
|
|
|
export default function SettingsScreen() {
|
2025-07-22 15:10:57 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-21 15:10:39 +08:00
|
|
|
|
return (
|
2025-07-22 15:10:57 +08:00
|
|
|
|
<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>
|
2025-07-21 15:10:39 +08:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
2025-07-22 15:10:57 +08:00
|
|
|
|
container: {
|
2025-07-21 15:10:39 +08:00
|
|
|
|
flex: 1,
|
2025-07-22 15:10:57 +08:00
|
|
|
|
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)',
|
2025-07-21 15:10:39 +08:00
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
2025-07-22 15:10:57 +08:00
|
|
|
|
zIndex: 1,
|
|
|
|
|
},
|
|
|
|
|
loadingText: {
|
|
|
|
|
marginTop: 10,
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
color: '#007AFF',
|
|
|
|
|
},
|
|
|
|
|
clearButton: {
|
|
|
|
|
backgroundColor: '#FF3B30',
|
2025-07-21 15:10:39 +08:00
|
|
|
|
},
|
|
|
|
|
});
|