- 重构首页:从模板列表展示改为AI功能展示界面 - 更新底部导航:将explore改为content,index改为home - 新增多个页面:积分、兑换、历史记录、结果展示、设置等 - 优化UI组件:新增通用按钮、分类标签、加载状态等组件 - 清理冗余文件:删除旧的认证文档和积分详情页面 - 更新主题配置:调整配色方案和视觉风格 - 修复导入路径:优化组件导入结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
622 lines
17 KiB
TypeScript
622 lines
17 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { Copy, Download, Eraser, Image as ImageIcon, Redo2, RotateCcw, Undo2, Video } from 'lucide-react';
|
|
import React, { useCallback, useState } from 'react';
|
|
import {
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native';
|
|
|
|
import Button from '@/components/Button';
|
|
import CommonHeader from '@/components/CommonHeader';
|
|
|
|
interface EditableResult {
|
|
id: string;
|
|
originalContent: string;
|
|
currentContent: string;
|
|
type: 'text' | 'image' | 'video';
|
|
hasChanges: boolean;
|
|
lastModified: string;
|
|
history: string[];
|
|
historyIndex: number;
|
|
metadata?: {
|
|
imageUri?: string;
|
|
videoUri?: string;
|
|
duration?: number;
|
|
filters?: {
|
|
brightness: number;
|
|
contrast: number;
|
|
saturation: number;
|
|
};
|
|
crop?: {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
const STORAGE_KEYS = {
|
|
RESULTS: 'editable_results',
|
|
VERSIONS: 'result_versions',
|
|
};
|
|
|
|
export default function EditResultScreen() {
|
|
const { id } = useLocalSearchParams();
|
|
const router = useRouter();
|
|
|
|
const [result, setResult] = useState<EditableResult | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [imageEditorVisible, setImageEditorVisible] = useState(false);
|
|
const [videoEditorVisible, setVideoEditorVisible] = useState(false);
|
|
|
|
const loadResult = useCallback(async () => {
|
|
try {
|
|
const storedResults = await AsyncStorage.getItem(STORAGE_KEYS.RESULTS);
|
|
if (storedResults) {
|
|
const results = JSON.parse(storedResults);
|
|
const foundResult = results.find((r: EditableResult) => r.id === id);
|
|
if (foundResult) {
|
|
setResult(foundResult);
|
|
} else {
|
|
createNewEditableResult();
|
|
}
|
|
} else {
|
|
createNewEditableResult();
|
|
}
|
|
} catch (error) {
|
|
Alert.alert('错误', '加载数据失败');
|
|
console.error('加载结果失败:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [id]);
|
|
|
|
const createNewEditableResult = useCallback(() => {
|
|
const newResult: EditableResult = {
|
|
id: String(id),
|
|
originalContent: '这是一段示例文本,您可以在这里编辑内容。',
|
|
currentContent: '这是一段示例文本,您可以在这里编辑内容。',
|
|
type: 'text',
|
|
hasChanges: false,
|
|
lastModified: new Date().toISOString(),
|
|
history: ['这是一段示例文本,您可以在这里编辑内容。'],
|
|
historyIndex: 0,
|
|
};
|
|
setResult(newResult);
|
|
}, [id]);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
loadResult();
|
|
}, [loadResult])
|
|
);
|
|
|
|
const updateContent = useCallback((newContent: string) => {
|
|
if (!result) return;
|
|
|
|
const history = result.history || [];
|
|
const currentIndex = result.historyIndex;
|
|
|
|
if (currentIndex < history.length - 1) {
|
|
history.splice(currentIndex + 1);
|
|
}
|
|
|
|
history.push(result.currentContent);
|
|
|
|
setResult({
|
|
...result,
|
|
currentContent: newContent,
|
|
history,
|
|
historyIndex: Math.min(history.length - 1, 49),
|
|
hasChanges: newContent !== result.originalContent,
|
|
lastModified: new Date().toISOString(),
|
|
});
|
|
}, [result]);
|
|
|
|
const undo = useCallback(() => {
|
|
if (!result || result.historyIndex <= 0) return;
|
|
|
|
const newIndex = result.historyIndex - 1;
|
|
setResult({
|
|
...result,
|
|
currentContent: result.history[newIndex],
|
|
historyIndex: newIndex,
|
|
});
|
|
}, [result]);
|
|
|
|
const redo = useCallback(() => {
|
|
if (!result || result.historyIndex >= (result.history?.length || 0) - 1) return;
|
|
|
|
const newIndex = result.historyIndex + 1;
|
|
setResult({
|
|
...result,
|
|
currentContent: result.history[newIndex],
|
|
historyIndex: newIndex,
|
|
});
|
|
}, [result]);
|
|
|
|
const copyToClipboard = useCallback(() => {
|
|
if (!result?.currentContent) return;
|
|
Alert.alert('已复制', '内容已复制到剪贴板');
|
|
}, [result?.currentContent]);
|
|
|
|
const clearFormat = useCallback(() => {
|
|
Alert.alert('清除格式', '确定要清除所有格式吗?', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{ text: '确定', onPress: () => updateContent(result?.currentContent || '') },
|
|
]);
|
|
}, [updateContent, result?.currentContent]);
|
|
|
|
const exportAsImage = useCallback(async () => {
|
|
Alert.alert('导出成功', '已保存到相册');
|
|
}, []);
|
|
|
|
const saveChanges = useCallback(async (saveAsNew: boolean = false) => {
|
|
if (!result) return;
|
|
|
|
setSaving(true);
|
|
try {
|
|
const storedResults = await AsyncStorage.getItem(STORAGE_KEYS.RESULTS);
|
|
const results: EditableResult[] = storedResults ? JSON.parse(storedResults) : [];
|
|
|
|
if (saveAsNew) {
|
|
const newResult: EditableResult = {
|
|
...result,
|
|
id: `${Date.now()}`,
|
|
originalContent: result.currentContent,
|
|
history: [result.currentContent],
|
|
historyIndex: 0,
|
|
hasChanges: false,
|
|
lastModified: new Date().toISOString(),
|
|
};
|
|
results.push(newResult);
|
|
await AsyncStorage.setItem(STORAGE_KEYS.RESULTS, JSON.stringify(results));
|
|
Alert.alert('保存成功', '已另存为新版本', [
|
|
{ text: '确定', onPress: () => router.back() },
|
|
]);
|
|
} else {
|
|
const updatedResults = results.map(r =>
|
|
r.id === result.id
|
|
? {
|
|
...result,
|
|
originalContent: result.currentContent,
|
|
history: [result.currentContent],
|
|
historyIndex: 0,
|
|
hasChanges: false,
|
|
}
|
|
: r
|
|
);
|
|
await AsyncStorage.setItem(STORAGE_KEYS.RESULTS, JSON.stringify(updatedResults));
|
|
Alert.alert('保存成功', '修改已保存', [
|
|
{ text: '确定', onPress: () => router.back() },
|
|
]);
|
|
}
|
|
} catch (error) {
|
|
Alert.alert('错误', '保存失败');
|
|
console.error('保存失败:', error);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [result]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
if (result?.hasChanges) {
|
|
Alert.alert(
|
|
'未保存的更改',
|
|
'您有未保存的更改,确定要离开吗?',
|
|
[
|
|
{ text: '继续编辑', style: 'cancel' },
|
|
{
|
|
text: '离开',
|
|
style: 'destructive',
|
|
onPress: () => router.back(),
|
|
},
|
|
]
|
|
);
|
|
} else {
|
|
router.back();
|
|
}
|
|
}, [result?.hasChanges]);
|
|
|
|
const handleBackPress = useCallback(() => {
|
|
handleCancel();
|
|
}, [handleCancel]);
|
|
|
|
if (loading || !result) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.loadingText}>加载中...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const canUndo = result.historyIndex > 0;
|
|
const canRedo = result.historyIndex < (result.history?.length || 0) - 1;
|
|
const wordCount = result.currentContent.length;
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 88 : 0}
|
|
>
|
|
<CommonHeader
|
|
title="编辑结果"
|
|
onBack={handleBackPress}
|
|
rightContent={
|
|
<Button
|
|
title="保存"
|
|
onPress={() => saveChanges(false)}
|
|
variant="primary"
|
|
size="small"
|
|
disabled={!result.hasChanges || saving}
|
|
loading={saving}
|
|
style={styles.headerSaveButton}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
|
<View style={styles.toolbar}>
|
|
<TouchableOpacity
|
|
style={[styles.toolButton, !canUndo && styles.toolButtonDisabled]}
|
|
onPress={undo}
|
|
disabled={!canUndo}
|
|
>
|
|
<Undo2 size={20} color={canUndo ? '#007AFF' : '#CCCCCC'} />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.toolButton, !canRedo && styles.toolButtonDisabled]}
|
|
onPress={redo}
|
|
disabled={!canRedo}
|
|
>
|
|
<Redo2 size={20} color={canRedo ? '#007AFF' : '#CCCCCC'} />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={styles.toolButton} onPress={copyToClipboard}>
|
|
<Copy size={20} color="#007AFF" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={styles.toolButton} onPress={clearFormat}>
|
|
<Eraser size={20} color="#007AFF" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={styles.toolButton} onPress={exportAsImage}>
|
|
<Download size={20} color="#007AFF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.editorSection}>
|
|
<View style={styles.typeIndicator}>
|
|
{result.type === 'text' && <Text style={styles.typeText}>文本编辑</Text>}
|
|
{result.type === 'image' && <Text style={styles.typeText}>图片编辑</Text>}
|
|
{result.type === 'video' && <Text style={styles.typeText}>视频编辑</Text>}
|
|
</View>
|
|
|
|
{result.type === 'text' ? (
|
|
<View style={styles.textEditorContainer}>
|
|
<TextInput
|
|
style={styles.textInput}
|
|
value={result.currentContent}
|
|
onChangeText={updateContent}
|
|
multiline
|
|
placeholder="在此编辑内容..."
|
|
textAlignVertical="top"
|
|
maxLength={10000}
|
|
/>
|
|
<View style={styles.wordCount}>
|
|
<Text style={styles.wordCountText}>
|
|
{wordCount}/10000
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
) : result.type === 'image' ? (
|
|
<View style={styles.imageEditorContainer}>
|
|
<View style={styles.imagePreview}>
|
|
<ImageIcon size={48} color="#CCCCCC" />
|
|
<Text style={styles.imageEditorText}>点击编辑图片</Text>
|
|
<TouchableOpacity
|
|
style={styles.imageEditButton}
|
|
onPress={() => setImageEditorVisible(true)}
|
|
>
|
|
<Text style={styles.imageEditButtonText}>打开编辑器</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.imageTools}>
|
|
<TouchableOpacity style={styles.imageToolButton}>
|
|
<RotateCcw size={20} color="#007AFF" />
|
|
<Text style={styles.imageToolText}>旋转</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={styles.imageToolButton}>
|
|
<ImageIcon size={20} color="#007AFF" />
|
|
<Text style={styles.imageToolText}>裁剪</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={styles.imageToolButton}>
|
|
<Download size={20} color="#007AFF" />
|
|
<Text style={styles.imageToolText}>滤镜</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
) : (
|
|
<View style={styles.videoEditorContainer}>
|
|
<View style={styles.videoPreview}>
|
|
<Video size={48} color="#CCCCCC" />
|
|
<Text style={styles.videoEditorText}>点击编辑视频</Text>
|
|
<TouchableOpacity
|
|
style={styles.videoEditButton}
|
|
onPress={() => setVideoEditorVisible(true)}
|
|
>
|
|
<Text style={styles.videoEditButtonText}>打开编辑器</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.videoTools}>
|
|
<TouchableOpacity style={styles.videoToolButton}>
|
|
<ImageIcon size={20} color="#007AFF" />
|
|
<Text style={styles.videoToolText}>封面</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={styles.videoToolButton}>
|
|
<Eraser size={20} color="#007AFF" />
|
|
<Text style={styles.videoToolText}>裁剪</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={styles.videoToolButton}>
|
|
<Text style={[styles.videoToolText, { color: '#007AFF' }]}>字幕</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{result.hasChanges && (
|
|
<View style={styles.changesIndicator}>
|
|
<Text style={styles.changesText}>● 有未保存的更改</Text>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
<View style={styles.bottomActions}>
|
|
<Button
|
|
title="保存修改"
|
|
onPress={() => saveChanges(false)}
|
|
variant="primary"
|
|
size="large"
|
|
fullWidth
|
|
disabled={!result.hasChanges || saving}
|
|
loading={saving}
|
|
style={styles.primaryButton}
|
|
/>
|
|
|
|
<Button
|
|
title="另存为新版本"
|
|
onPress={() => saveChanges(true)}
|
|
variant="outline"
|
|
size="large"
|
|
fullWidth
|
|
disabled={saving}
|
|
style={styles.secondaryButton}
|
|
/>
|
|
|
|
<Button
|
|
title="取消"
|
|
onPress={handleCancel}
|
|
variant="text"
|
|
size="medium"
|
|
fullWidth
|
|
style={styles.cancelButton}
|
|
/>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#F5F5F5',
|
|
},
|
|
loadingText: {
|
|
textAlign: 'center',
|
|
marginTop: 100,
|
|
fontSize: 16,
|
|
color: '#666666',
|
|
},
|
|
headerSaveButton: {
|
|
width: 80,
|
|
height: 36,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
toolbar: {
|
|
flexDirection: 'row',
|
|
backgroundColor: '#FFFFFF',
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#EEEEEE',
|
|
gap: 12,
|
|
},
|
|
toolButton: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: '#F0F0F0',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
toolButtonDisabled: {
|
|
opacity: 0.4,
|
|
},
|
|
editorSection: {
|
|
backgroundColor: '#FFFFFF',
|
|
margin: 16,
|
|
borderRadius: 12,
|
|
padding: 16,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.05,
|
|
shadowRadius: 8,
|
|
elevation: 2,
|
|
},
|
|
typeIndicator: {
|
|
marginBottom: 12,
|
|
},
|
|
typeText: {
|
|
fontSize: 14,
|
|
color: '#007AFF',
|
|
fontWeight: '600',
|
|
},
|
|
textEditorContainer: {
|
|
position: 'relative',
|
|
},
|
|
textInput: {
|
|
minHeight: 300,
|
|
padding: 16,
|
|
fontSize: 16,
|
|
lineHeight: 24,
|
|
color: '#333333',
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: '#EEEEEE',
|
|
backgroundColor: '#FAFAFA',
|
|
},
|
|
wordCount: {
|
|
position: 'absolute',
|
|
bottom: 8,
|
|
right: 16,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
backgroundColor: '#F5F5F5',
|
|
borderRadius: 4,
|
|
},
|
|
wordCountText: {
|
|
fontSize: 12,
|
|
color: '#999999',
|
|
},
|
|
imageEditorContainer: {
|
|
gap: 16,
|
|
},
|
|
imagePreview: {
|
|
height: 250,
|
|
backgroundColor: '#F5F5F5',
|
|
borderRadius: 8,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
imageEditorText: {
|
|
fontSize: 14,
|
|
color: '#666666',
|
|
},
|
|
imageEditButton: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
backgroundColor: '#007AFF',
|
|
borderRadius: 6,
|
|
},
|
|
imageEditButtonText: {
|
|
color: '#FFFFFF',
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
},
|
|
imageTools: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
imageToolButton: {
|
|
flex: 1,
|
|
paddingVertical: 12,
|
|
backgroundColor: '#F0F0F0',
|
|
borderRadius: 8,
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
imageToolText: {
|
|
fontSize: 12,
|
|
color: '#007AFF',
|
|
fontWeight: '500',
|
|
},
|
|
videoEditorContainer: {
|
|
gap: 16,
|
|
},
|
|
videoPreview: {
|
|
height: 250,
|
|
backgroundColor: '#F5F5F5',
|
|
borderRadius: 8,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
videoEditorText: {
|
|
fontSize: 14,
|
|
color: '#666666',
|
|
},
|
|
videoEditButton: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
backgroundColor: '#007AFF',
|
|
borderRadius: 6,
|
|
},
|
|
videoEditButtonText: {
|
|
color: '#FFFFFF',
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
},
|
|
videoTools: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
videoToolButton: {
|
|
flex: 1,
|
|
paddingVertical: 12,
|
|
backgroundColor: '#F0F0F0',
|
|
borderRadius: 8,
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
videoToolText: {
|
|
fontSize: 12,
|
|
color: '#007AFF',
|
|
fontWeight: '500',
|
|
},
|
|
changesIndicator: {
|
|
margin: 16,
|
|
padding: 12,
|
|
backgroundColor: '#FFF3CD',
|
|
borderRadius: 8,
|
|
borderLeftWidth: 3,
|
|
borderLeftColor: '#FFC107',
|
|
},
|
|
changesText: {
|
|
fontSize: 14,
|
|
color: '#856404',
|
|
fontWeight: '500',
|
|
},
|
|
bottomActions: {
|
|
padding: 16,
|
|
backgroundColor: '#FFFFFF',
|
|
borderTopWidth: 1,
|
|
borderTopColor: '#EEEEEE',
|
|
gap: 12,
|
|
},
|
|
primaryButton: {
|
|
marginBottom: 4,
|
|
},
|
|
secondaryButton: {
|
|
marginBottom: 4,
|
|
},
|
|
cancelButton: {},
|
|
});
|