expo-popcore-app/app/(tabs)/my.tsx

441 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } 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'
const { width: screenWidth } = Dimensions.get('window')
const GALLERY_GAP = 2
const GALLERY_HORIZONTAL_PADDING = 0
// 计算每个卡片的宽度:屏幕宽度 - 左右padding - 2个间距然后除以3使用 Math.floor 确保整数像素
const GALLERY_ITEM_SIZE = Math.floor(
(screenWidth - GALLERY_HORIZONTAL_PADDING * 2 - GALLERY_GAP * 2) / 3
)
// status状态有running pending completed
const works = [
{ id: 1, status: 'running' as const, count: 1 },
{ id: 2, status: 'completed' as const, count: 1 },
{ id: 3, status: 'completed' as const, count: 1 },
{ id: 4, status: 'completed' as const, count: 2 },
{ id: 5, status: 'pending' as const, count: 1 },
{ id: 6, status: 'pending' as const, count: 1 },
{ id: 7, status: 'pending' as const, count: 1 },
{ id: 8, status: 'pending' as const, count: 1 },
{ id: 9, status: 'completed' as const, count: 1 },
{ id: 10, status: 'completed' as const, count: 1 },
{ id: 11, status: 'completed' as const, count: 1 },
{ id: 12, status: 'completed' as const, count: 1 },
{ id: 13, status: 'completed' as const, count: 1 },
{ id: 14, status: 'completed' as const, count: 1 },
{ id: 15, status: 'completed' as const, count: 1 },
{ id: 16, status: 'completed' as const, count: 1 },
{ id: 17, status: 'completed' as const, count: 1 },
]
export default function My() {
const router = useRouter()
const { t, i18n } = useTranslation()
const [editDrawerVisible, setEditDrawerVisible] = useState(false)
const [profileName, setProfileName] = useState('乔乔乔乔')
// 处理设置菜单选择
const handleSettingsSelect = (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)
}
}
// 设置菜单选项
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' },
]
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={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 12345678</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>
{/* 作品九宫格 */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.galleryGrid}>
{works.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
// 每行的前两个item有右边距第三个没有
index % 3 !== 2 && styles.galleryItemMarginRight,
// 所有item都有下边距最后一行也会有但影响不大
styles.galleryItemMarginBottom,
]}
onPress={() => {
// 只有已完成的作品才能点击进入详情页
if (item.status === 'completed') {
router.push({
pathname: '/generationRecord' as any,
params: { id: item.id.toString() },
})
}
}}
disabled={item.status !== 'completed'}
>
<Image
source={require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
{/* 生成中遮罩 */}
{item.status != 'completed' && (
<View style={styles.generatingOverlay} />
)}
{/* 右上角作品数量角标 */}
{item.status === 'completed'&&<View style={styles.counterBadge}>
<Text style={styles.counterText}>
{item.count}
</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>
))}
</View>
</ScrollView>
{/* 编辑资料抽屉 */}
<EditProfileDrawer
visible={editDrawerVisible}
onClose={() => setEditDrawerVisible(false)}
initialName={profileName}
onSave={(name) => setProfileName(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',
},
header: {
backgroundColor: '#000000',
paddingTop: 8,
paddingBottom: 12,
},
statusBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingBottom: 8,
},
time: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
statusIcons: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
signalBars: {
width: 18,
height: 12,
backgroundColor: '#FFFFFF',
borderRadius: 2,
},
wifiIcon: {
width: 16,
height: 12,
backgroundColor: '#FFFFFF',
borderRadius: 2,
},
batteryIcon: {
width: 24,
height: 12,
backgroundColor: '#FFFFFF',
borderRadius: 2,
},
titleBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
appTitle: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
pointsContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
pointsText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
searchButton: {
padding: 4,
},
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, //左右各留 8 像素的内边距
paddingVertical: 6, //上下各留 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',
},
sectionMoreIcon: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: '#FFFFFF33',
},
galleryGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 10,
},
galleryItem: {
width: GALLERY_ITEM_SIZE,
// 使用等比例 1:1保证容器永远是正方形
aspectRatio: 1,
overflow: 'hidden',
backgroundColor: '#1C1E22',
position: 'relative',
},
galleryItemMarginRight: {
marginRight: GALLERY_GAP,
},
galleryItemMarginBottom: {
marginBottom: GALLERY_GAP,
},
galleryImage: {
width: '100%',
// 高度由 aspectRatio 决定,避免拉伸
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',
},
})