This commit is contained in:
imeepos
2025-10-21 10:21:45 +08:00
parent 7e3f94bae3
commit 3a99ff96d5
28 changed files with 6981 additions and 176 deletions

View File

@@ -0,0 +1,132 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { useThemeColor } from '@/hooks/use-theme-color';
import { IconSymbol } from '@/components/ui/icon-symbol';
interface BalanceCardProps {
balance: number;
onRecharge: () => void;
onHistory?: () => void;
}
export function BalanceCard({ balance, onRecharge, onHistory }: BalanceCardProps) {
const tintColor = useThemeColor({}, 'tint');
const cardColor = useThemeColor({}, 'card');
const borderColor = useThemeColor({}, 'cardBorder');
return (
<ThemedView style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<IconSymbol name="dollarsign.circle" size={24} color={tintColor} />
<ThemedText style={styles.title}></ThemedText>
</View>
{onHistory && (
<TouchableOpacity onPress={onHistory} style={styles.historyButton}>
<ThemedText style={[styles.historyText, { color: tintColor }]}>
</ThemedText>
</TouchableOpacity>
)}
</View>
<View style={styles.balanceContainer}>
<ThemedText style={styles.currency}>¥</ThemedText>
<ThemedText style={styles.balance}>{balance.toFixed(2)}</ThemedText>
</View>
<TouchableOpacity
style={[styles.rechargeButton, { backgroundColor: tintColor }]}
onPress={onRecharge}
activeOpacity={0.8}
>
<IconSymbol name="plus.circle.fill" size={20} color="white" />
<ThemedText style={styles.rechargeText}></ThemedText>
</TouchableOpacity>
<View style={styles.tips}>
<IconSymbol name="info.circle" size={14} color="#999" />
<ThemedText style={styles.tipsText}>
</ThemedText>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
title: {
fontSize: 16,
fontWeight: '600',
},
historyButton: {
paddingHorizontal: 12,
paddingVertical: 6,
},
historyText: {
fontSize: 14,
fontWeight: '500',
},
balanceContainer: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'center',
marginVertical: 24,
},
currency: {
fontSize: 24,
fontWeight: '600',
marginBottom: 4,
},
balance: {
fontSize: 48,
fontWeight: 'bold',
lineHeight: 48,
},
rechargeButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 24,
gap: 8,
alignSelf: 'center',
},
rechargeText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
tips: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
},
tipsText: {
fontSize: 12,
color: '#666',
flex: 1,
},
});

View File

@@ -0,0 +1,319 @@
import React, { useState } from 'react';
import { StyleSheet, ScrollView, TouchableOpacity, RefreshControl, Alert, ActivityIndicator } from 'react-native';
import { router } from 'expo-router';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { useThemeColor } from '@/hooks/use-theme-color';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { GenerationRecord } from '@/lib/auth/types';
interface GenerationRecordsProps {
userId?: string;
}
const mockRecords: GenerationRecord[] = [
{
id: '1',
userId: 'user1',
type: 'image',
title: '夏日风景插画',
description: '生成了一张充满夏日气息的风景插画',
thumbnail: undefined,
mediaUrl: undefined,
status: 'completed',
cost: 0.5,
createdAt: new Date('2024-01-15T10:30:00'),
updatedAt: new Date('2024-01-15T10:35:00'),
},
{
id: '2',
userId: 'user1',
type: 'video',
title: '产品介绍视频',
description: '为新产品制作了一个介绍视频',
thumbnail: undefined,
mediaUrl: undefined,
status: 'pending',
cost: 2.0,
createdAt: new Date('2024-01-14T15:20:00'),
updatedAt: new Date('2024-01-14T15:20:00'),
},
];
export function GenerationRecords({ userId }: GenerationRecordsProps) {
const [records, setRecords] = useState<GenerationRecord[]>(mockRecords);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const tintColor = useThemeColor({}, 'tint');
const textColor = useThemeColor({}, 'text');
const getTypeIcon = (type: string) => {
switch (type) {
case 'image':
return 'photo';
case 'video':
return 'video';
case 'text':
return 'doc.text';
default:
return 'doc';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return '#34C759';
case 'pending':
return '#FF9500';
case 'failed':
return '#FF3B30';
default:
return '#999';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return '已完成';
case 'pending':
return '生成中';
case 'failed':
return '失败';
default:
return '未知';
}
};
const handleRefresh = async () => {
setRefreshing(true);
try {
// TODO: 实现刷新逻辑
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error('刷新失败:', error);
} finally {
setRefreshing(false);
}
};
const handleDelete = (recordId: string) => {
Alert.alert(
'删除记录',
'确定要删除这条生成记录吗?此操作无法撤销。',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => {
setRecords(prev => prev.filter(record => record.id !== recordId));
}
}
]
);
};
const handlePress = (record: GenerationRecord) => {
if (record.status === 'completed') {
// TODO: 跳转到详情页面
console.log('查看记录详情:', record.id);
}
};
if (records.length === 0 && !loading) {
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<IconSymbol name="clock" size={20} color={tintColor} />
<ThemedText style={styles.title}></ThemedText>
</ThemedView>
<ThemedView style={styles.emptyContainer}>
<IconSymbol name="doc.text" size={48} color="#ccc" />
<ThemedText style={styles.emptyText}></ThemedText>
<ThemedText style={styles.emptySubText}></ThemedText>
</ThemedView>
</ThemedView>
);
}
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<IconSymbol name="clock" size={20} color={tintColor} />
<ThemedText style={styles.title}></ThemedText>
<ThemedText style={styles.count}>({records.length})</ThemedText>
</ThemedView>
<ScrollView
style={styles.scrollView}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
{records.map((record) => (
<TouchableOpacity
key={record.id}
style={styles.recordItem}
onPress={() => handlePress(record)}
activeOpacity={0.7}
>
<ThemedView style={styles.recordLeft}>
<ThemedView style={styles.typeIcon}>
<IconSymbol
name={getTypeIcon(record.type)}
size={20}
color={tintColor}
/>
</ThemedView>
<ThemedView style={styles.recordContent}>
<ThemedText style={styles.recordTitle}>{record.title}</ThemedText>
{record.description && (
<ThemedText style={styles.recordDescription} numberOfLines={2}>
{record.description}
</ThemedText>
)}
<ThemedView style={styles.recordMeta}>
<ThemedText style={styles.recordTime}>
{new Date(record.createdAt).toLocaleDateString()}
</ThemedText>
<ThemedText style={styles.recordCost}>¥{record.cost}</ThemedText>
</ThemedView>
</ThemedView>
</ThemedView>
<ThemedView style={styles.recordRight}>
<ThemedView
style={[
styles.statusBadge,
{ backgroundColor: getStatusColor(record.status) + '20' }
]}
>
<ThemedText
style={[
styles.statusText,
{ color: getStatusColor(record.status) }
]}
>
{getStatusText(record.status)}
</ThemedText>
</ThemedView>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleDelete(record.id)}
>
<IconSymbol name="trash" size={16} color="#FF3B30" />
</TouchableOpacity>
</ThemedView>
</TouchableOpacity>
))}
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 16,
},
title: {
fontSize: 18,
fontWeight: '600',
},
count: {
fontSize: 14,
color: '#666',
},
scrollView: {
flex: 1,
},
recordItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
recordLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
gap: 12,
},
typeIcon: {
width: 40,
height: 40,
borderRadius: 8,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
},
recordContent: {
flex: 1,
},
recordTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
recordDescription: {
fontSize: 14,
color: '#666',
marginBottom: 4,
},
recordMeta: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
recordTime: {
fontSize: 12,
color: '#999',
},
recordCost: {
fontSize: 12,
color: '#666',
fontWeight: '500',
},
recordRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
fontWeight: '500',
},
deleteButton: {
padding: 8,
},
emptyContainer: {
alignItems: 'center',
paddingVertical: 40,
},
emptyText: {
fontSize: 16,
fontWeight: '600',
marginTop: 16,
marginBottom: 8,
},
emptySubText: {
fontSize: 14,
color: '#666',
},
});

View File

@@ -0,0 +1,165 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { router } from 'expo-router';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { useThemeColor } from '@/hooks/use-theme-color';
import { IconSymbol } from '@/components/ui/icon-symbol';
interface SettingsListProps {
onLogout: () => void;
}
interface SettingItem {
id: string;
title: string;
icon: string;
onPress?: () => void;
destructive?: boolean;
}
export function SettingsList({ onLogout }: SettingsListProps) {
const tintColor = useThemeColor({}, 'tint');
const textColor = useThemeColor({}, 'text');
const destructiveColor = '#FF3B30';
const settings: SettingItem[] = [
{
id: 'account',
title: '账户设置',
icon: 'person.circle',
onPress: () => router.push('/settings/account'),
},
{
id: 'notification',
title: '通知设置',
icon: 'bell',
onPress: () => router.push('/settings/notification'),
},
{
id: 'privacy',
title: '隐私设置',
icon: 'lock',
onPress: () => router.push('/settings/privacy'),
},
{
id: 'about',
title: '关于我们',
icon: 'info.circle',
onPress: () => router.push('/settings/about'),
},
{
id: 'feedback',
title: '意见反馈',
icon: 'exclamationmark.bubble',
onPress: () => router.push('/settings/feedback'),
},
{
id: 'logout',
title: '退出登录',
icon: 'arrow.right.square',
onPress: onLogout,
destructive: true,
},
];
const handleSettingPress = (item: SettingItem) => {
if (item.onPress) {
if (item.destructive) {
Alert.alert(
'退出登录',
'确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: item.onPress,
}
]
);
} else {
item.onPress();
}
}
};
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<IconSymbol name="gearshape" size={20} color={tintColor} />
<ThemedText style={styles.title}></ThemedText>
</ThemedView>
<ThemedView style={styles.settingsList}>
{settings.map((item) => (
<TouchableOpacity
key={item.id}
style={styles.settingItem}
onPress={() => handleSettingPress(item)}
activeOpacity={0.7}
>
<ThemedView style={styles.settingLeft}>
<IconSymbol
name={item.icon}
size={20}
color={item.destructive ? destructiveColor : tintColor}
/>
<ThemedText
style={[
styles.settingTitle,
{ color: item.destructive ? destructiveColor : textColor }
]}
>
{item.title}
</ThemedText>
</ThemedView>
<IconSymbol
name="chevron.right"
size={16}
color="#ccc"
/>
</TouchableOpacity>
))}
</ThemedView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 16,
},
title: {
fontSize: 18,
fontWeight: '600',
},
settingsList: {
borderRadius: 8,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 4,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
settingLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
settingTitle: {
fontSize: 16,
},
});

View File

@@ -0,0 +1,110 @@
import React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { useThemeColor } from '@/hooks/use-theme-color';
interface UserAvatarProps {
image?: string;
name: string;
size?: number;
showBorder?: boolean;
}
export function UserAvatar({
image,
name,
size = 40,
showBorder = true
}: UserAvatarProps) {
const borderColor = useThemeColor({}, 'cardBorder');
const placeholderColor = useThemeColor({}, 'imagePlaceholder');
const textColor = useThemeColor({}, 'text');
const avatarSize = size;
const borderRadius = avatarSize / 2;
const fontSize = avatarSize / 3;
const getInitials = (name: string) => {
return name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
.slice(0, 2);
};
return (
<View
style={[
styles.container,
{
width: avatarSize,
height: avatarSize,
borderRadius,
borderColor: showBorder ? borderColor : 'transparent',
}
]}
>
{image ? (
<Image
source={{ uri: image }}
style={[
styles.image,
{
width: avatarSize - 4,
height: avatarSize - 4,
borderRadius: (avatarSize - 4) / 2,
}
]}
resizeMode="cover"
/>
) : (
<ThemedView
style={[
styles.placeholder,
{
width: avatarSize - 4,
height: avatarSize - 4,
borderRadius: (avatarSize - 4) / 2,
backgroundColor: placeholderColor,
}
]}
>
<ThemedText
style={[
styles.initials,
{
fontSize,
color: textColor,
}
]}
>
{getInitials(name)}
</ThemedText>
</ThemedView>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
image: {
backgroundColor: 'transparent',
},
placeholder: {
justifyContent: 'center',
alignItems: 'center',
},
initials: {
fontWeight: '600',
textAlign: 'center',
},
});