429 lines
15 KiB
TypeScript
429 lines
15 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
ScrollView,
|
||
Dimensions,
|
||
Pressable,
|
||
StatusBar as RNStatusBar,
|
||
} from 'react-native'
|
||
import { StatusBar } from 'expo-status-bar'
|
||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||
import { Image } from 'expo-image'
|
||
import { useRouter } from 'expo-router'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { PointsIcon, SearchIcon, SettingsIcon } from '@/components/icon'
|
||
import EditProfileDrawer from '@/components/drawer/EditProfileDrawer'
|
||
import Dropdown from '@/components/ui/dropdown'
|
||
import Toast from '@/components/ui/Toast'
|
||
import { signOut } from '@/lib/auth'
|
||
import { useTemplateGenerations, type TemplateGeneration } from '@/hooks'
|
||
import { MySkeleton } from '@/components/skeleton/MySkeleton'
|
||
import { useSession } from '@/lib/auth'
|
||
|
||
const { width: screenWidth } = Dimensions.get('window')
|
||
const GALLERY_GAP = 2
|
||
const GALLERY_HORIZONTAL_PADDING = 0
|
||
const GALLERY_ITEM_SIZE = Math.floor(
|
||
(screenWidth - GALLERY_HORIZONTAL_PADDING * 2 - GALLERY_GAP * 2) / 3
|
||
)
|
||
|
||
// 获取作品封面图
|
||
const getCoverUrl = (item: TemplateGeneration) =>
|
||
item.resultUrl?.[0] || item.template?.coverImageUrl
|
||
|
||
export default function My() {
|
||
const router = useRouter()
|
||
const { t, i18n } = useTranslation()
|
||
const [editDrawerVisible, setEditDrawerVisible] = useState(false)
|
||
|
||
// 获取当前登录用户信息
|
||
const { data: session } = useSession()
|
||
const userName = session?.user?.name || session?.user?.username || '用户'
|
||
const [profileName, setProfileName] = useState(userName)
|
||
|
||
// 当 session 变化时更新用户名
|
||
useEffect(() => {
|
||
if (session?.user?.name || session?.user?.username) {
|
||
setProfileName(session?.user?.name || session?.user?.username || '用户')
|
||
}
|
||
}, [session])
|
||
|
||
// 使用 useTemplateGenerations hook 获取用户作品列表
|
||
const {
|
||
generations,
|
||
loading,
|
||
error,
|
||
execute: loadGenerations,
|
||
refetch,
|
||
} = useTemplateGenerations()
|
||
|
||
// 初始化加载作品列表
|
||
useEffect(() => {
|
||
loadGenerations({ page: 1, limit: 50 })
|
||
}, [])
|
||
|
||
// 处理设置菜单选择
|
||
const handleSettingsSelect = async (value: string) => {
|
||
if (value === 'changePassword') {
|
||
router.push('/changePassword' as any)
|
||
} else if (value === 'language') {
|
||
// 切换语言
|
||
const newLang = i18n.language === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||
i18n.changeLanguage(newLang)
|
||
} else if (value === 'logout') {
|
||
// 退出登录
|
||
console.log('🚪 点击退出登录')
|
||
const confirmText = i18n.language === 'zh-CN' ? '确定' : 'OK'
|
||
const cancelText = i18n.language === 'zh-CN' ? '取消' : 'Cancel'
|
||
const message = i18n.language === 'zh-CN' ? '确定要退出登录吗?' : 'Are you sure you want to logout?'
|
||
|
||
Toast.showActionSheet({
|
||
itemList: [message, confirmText, cancelText]
|
||
}).then(async (index) => {
|
||
// index 1 是确定按钮
|
||
if (index === 1) {
|
||
console.log('🚪 开始执行退出登录')
|
||
try {
|
||
Toast.showLoading({ title: i18n.language === 'zh-CN' ? '退出中...' : 'Logging out...' })
|
||
// 调用 better-auth 的 signOut 方法
|
||
await signOut()
|
||
Toast.hideLoading()
|
||
console.log('✅ 退出登录成功,跳转到登录页')
|
||
Toast.show(i18n.language === 'zh-CN' ? '退出登录成功' : 'Logged out successfully')
|
||
// 跳转到登录页面(注意:路由是 /auth 不是 /login)
|
||
router.replace('/auth')
|
||
} catch (error) {
|
||
Toast.hideLoading()
|
||
console.error('❌ 退出登录失败:', error)
|
||
Toast.show(i18n.language === 'zh-CN' ? '退出登录失败,请稍后重试' : 'Failed to logout, please try again later')
|
||
}
|
||
}
|
||
}).catch(() => {
|
||
console.log('❌ 取消退出登录')
|
||
})
|
||
}
|
||
}
|
||
|
||
// 设置菜单选项
|
||
const getLanguageLabel = () => {
|
||
if (i18n.language === 'zh-CN') {
|
||
return t('my.languageSwitch')
|
||
} else {
|
||
return t('my.languageSwitchEn')
|
||
}
|
||
}
|
||
|
||
const settingsOptions = [
|
||
{ label: t('my.changePassword'), value: 'changePassword' },
|
||
{ label: getLanguageLabel(), value: 'language' },
|
||
{ label: t('my.logout'), value: 'logout' },
|
||
]
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top']}>
|
||
<StatusBar style="light" />
|
||
<RNStatusBar barStyle="light-content" />
|
||
|
||
{/* 顶部积分与设置 */}
|
||
<View style={styles.topBar}>
|
||
<Pressable
|
||
style={styles.pointsPill}
|
||
onPress={() => router.push('/membership' as any)}
|
||
>
|
||
<PointsIcon />
|
||
<Text style={styles.pointsPillText}>60</Text>
|
||
</Pressable>
|
||
<Dropdown
|
||
options={settingsOptions}
|
||
onSelect={(value) => handleSettingsSelect(value)}
|
||
renderTrigger={(selectedOption, isOpen, toggle) => (
|
||
<Pressable onPress={toggle}>
|
||
<SettingsIcon />
|
||
</Pressable>
|
||
)}
|
||
dropdownStyle={{
|
||
minWidth: 160,
|
||
right: 10,
|
||
backgroundColor: '#2A2A2A80',
|
||
}}
|
||
/>
|
||
</View>
|
||
|
||
{/* 个人信息区 */}
|
||
<View style={styles.profileSection}>
|
||
<Image
|
||
source={session?.user?.image ? { uri: session.user.image } : require('@/assets/images/icon.png')}
|
||
style={styles.avatar}
|
||
contentFit="cover"
|
||
/>
|
||
<View style={styles.profileInfo}>
|
||
<Text style={styles.profileName}>{profileName}</Text>
|
||
<Text style={styles.profileSubTitle}>ID {session?.user?.id?.slice(0, 8) || '--------'}</Text>
|
||
</View>
|
||
<Pressable
|
||
style={styles.editButton}
|
||
onPress={() => setEditDrawerVisible(true)}
|
||
>
|
||
<Text style={styles.editButtonText}>{t('my.editProfile')}</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* "生成作品" 标题行 */}
|
||
<View style={styles.sectionHeader}>
|
||
<Text style={styles.sectionTitle}>{t('my.generatedWorks')}</Text>
|
||
<Pressable
|
||
style={styles.sectionMoreButton}
|
||
onPress={() => router.push('/worksList' as any)}
|
||
>
|
||
<SearchIcon />
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* 作品九宫格 */}
|
||
{loading ? (
|
||
<MySkeleton />
|
||
) : (
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={styles.scrollContent}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<View style={styles.galleryGrid}>
|
||
{generations.map((item, index) => (
|
||
<Pressable
|
||
key={item.id}
|
||
style={[
|
||
styles.galleryItem,
|
||
index % 3 !== 2 && styles.galleryItemMarginRight,
|
||
styles.galleryItemMarginBottom,
|
||
]}
|
||
onPress={() => {
|
||
if (item.status === 'completed') {
|
||
router.push({
|
||
pathname: '/generationRecord' as any,
|
||
params: { id: item.id },
|
||
})
|
||
}
|
||
}}
|
||
disabled={item.status !== 'completed'}
|
||
>
|
||
<Image
|
||
source={getCoverUrl(item) ? { uri: getCoverUrl(item) } : require('@/assets/images/membership.png')}
|
||
style={styles.galleryImage}
|
||
contentFit="cover"
|
||
/>
|
||
|
||
{/* 遮罩:非完成状态 */}
|
||
{item.status !== 'completed' && (
|
||
<View style={styles.generatingOverlay} />
|
||
)}
|
||
|
||
{/* 数量角标:已完成且有结果 */}
|
||
{item.status === 'completed' && (item.resultUrl?.length || 0) > 0 && (
|
||
<View style={styles.counterBadge}>
|
||
<Text style={styles.counterText}>
|
||
{item.resultUrl?.length || 1}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* 状态角标 */}
|
||
{item.status === 'running' && (
|
||
<View style={styles.generatingBadge}>
|
||
<Text style={styles.generatingBadgeText}>{t('my.generating')}</Text>
|
||
</View>
|
||
)}
|
||
{item.status === 'pending' && (
|
||
<View style={styles.generatingBadge}>
|
||
<Text style={styles.generatingBadgeText}>{t('my.queuing')}</Text>
|
||
</View>
|
||
)}
|
||
</Pressable>
|
||
))}
|
||
|
||
{/* 空状态提示 */}
|
||
{!loading && generations.length === 0 && (
|
||
<View style={styles.emptyState}>
|
||
<Text style={styles.emptyStateText}>
|
||
{t('my.noWorks')}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
)}
|
||
|
||
{/* 编辑资料抽屉 */}
|
||
<EditProfileDrawer
|
||
visible={editDrawerVisible}
|
||
onClose={() => setEditDrawerVisible(false)}
|
||
initialName={profileName}
|
||
initialAvatar={session?.user?.image}
|
||
onSave={(data) => {
|
||
setProfileName(data.name)
|
||
}}
|
||
/>
|
||
</SafeAreaView>
|
||
)
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
topBar: {
|
||
paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
|
||
paddingTop: 19,
|
||
paddingRight: 16,
|
||
flexDirection: 'row',
|
||
justifyContent: 'flex-end',
|
||
backgroundColor: '#090A0B',
|
||
gap: 12,
|
||
},
|
||
pointsPill: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 1,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 100,
|
||
backgroundColor: '#1C1E22',
|
||
},
|
||
pointsPillText: {
|
||
color: '#FFCF00',
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
scrollContent: {
|
||
backgroundColor: '#090A0B',
|
||
paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
|
||
},
|
||
profileSection: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingLeft: 16,
|
||
paddingRight: 16,
|
||
marginTop: 20,
|
||
marginBottom: 32,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
avatar: {
|
||
width: 64,
|
||
height: 64,
|
||
borderRadius: 32,
|
||
overflow: 'hidden',
|
||
marginRight: 16,
|
||
},
|
||
profileInfo: {
|
||
flex: 1,
|
||
},
|
||
profileName: {
|
||
color: '#F5F5F5',
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
marginBottom: 4,
|
||
},
|
||
profileSubTitle: {
|
||
color: '#FFFFFF',
|
||
fontSize: 12,
|
||
opacity: 0.7,
|
||
},
|
||
editButton: {
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 6,
|
||
borderRadius: 6,
|
||
backgroundColor: '#1C1E22',
|
||
},
|
||
editButtonText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 12,
|
||
},
|
||
sectionHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 12,
|
||
marginHorizontal: 12,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
sectionTitle: {
|
||
color: '#FFFFFF',
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
},
|
||
sectionMoreButton: {
|
||
width: 20,
|
||
height: 20,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
galleryGrid: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
marginBottom: 10,
|
||
},
|
||
galleryItem: {
|
||
width: GALLERY_ITEM_SIZE,
|
||
aspectRatio: 1,
|
||
overflow: 'hidden',
|
||
backgroundColor: '#1C1E22',
|
||
position: 'relative',
|
||
},
|
||
galleryItemMarginRight: {
|
||
marginRight: GALLERY_GAP,
|
||
},
|
||
galleryItemMarginBottom: {
|
||
marginBottom: GALLERY_GAP,
|
||
},
|
||
galleryImage: {
|
||
width: '100%',
|
||
height: undefined,
|
||
aspectRatio: 1,
|
||
},
|
||
generatingOverlay: {
|
||
...StyleSheet.absoluteFillObject,
|
||
backgroundColor: '#00000080',
|
||
},
|
||
counterBadge: {
|
||
position: 'absolute',
|
||
right: 8,
|
||
bottom: 8,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 1,
|
||
borderRadius: 6,
|
||
backgroundColor: '#16181B1A',
|
||
},
|
||
counterText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 10,
|
||
fontWeight: '600',
|
||
},
|
||
generatingBadge: {
|
||
position: 'absolute',
|
||
left: 8,
|
||
bottom: 8,
|
||
},
|
||
generatingBadgeText: {
|
||
color: '#F5F5F5',
|
||
fontSize: 9,
|
||
fontWeight: '500',
|
||
},
|
||
emptyState: {
|
||
width: '100%',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 60,
|
||
},
|
||
emptyStateText: {
|
||
color: '#FFFFFF80',
|
||
fontSize: 14,
|
||
},
|
||
})
|