Files
bw-expo-app/app/edit/[id].tsx
imeepos b9399bf4cf feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面
- 更新底部导航:将explore改为content,index改为home
- 新增多个页面:积分、兑换、历史记录、结果展示、设置等
- 优化UI组件:新增通用按钮、分类标签、加载状态等组件
- 清理冗余文件:删除旧的认证文档和积分详情页面
- 更新主题配置:调整配色方案和视觉风格
- 修复导入路径:优化组件导入结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 17:53:56 +08:00

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: {},
});