✨ feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面 - 更新底部导航:将explore改为content,index改为home - 新增多个页面:积分、兑换、历史记录、结果展示、设置等 - 优化UI组件:新增通用按钮、分类标签、加载状态等组件 - 清理冗余文件:删除旧的认证文档和积分详情页面 - 更新主题配置:调整配色方案和视觉风格 - 修复导入路径:优化组件导入结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
621
app/edit/[id].tsx
Normal file
621
app/edit/[id].tsx
Normal file
@@ -0,0 +1,621 @@
|
||||
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: {},
|
||||
});
|
||||
Reference in New Issue
Block a user