refactor: 优化代码和添加类型检查脚本
- 统一 OWNER_ID 导入,从 lib/auth 导入而非环境变量 - 添加 type-checks 脚本用于 TypeScript 类型检查 - 对接 my 页面后端接口,使用 useTemplateGenerations - 添加 MySkeleton 骨架屏组件 - 添加下拉刷新功能 - 添加空状态提示文案 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Dimensions,
|
||||
Pressable,
|
||||
StatusBar as RNStatusBar,
|
||||
RefreshControl,
|
||||
} from 'react-native'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
@@ -18,41 +19,47 @@ 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'
|
||||
|
||||
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 },
|
||||
|
||||
]
|
||||
|
||||
// 获取作品封面图
|
||||
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 [profileName, setProfileName] = useState('乔乔乔乔')
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
// 使用 useTemplateGenerations hook 获取用户作品列表
|
||||
const {
|
||||
generations,
|
||||
loading,
|
||||
error,
|
||||
execute: loadGenerations,
|
||||
refetch,
|
||||
} = useTemplateGenerations()
|
||||
|
||||
// 初始化加载作品列表
|
||||
useEffect(() => {
|
||||
loadGenerations({ page: 1, limit: 50 })
|
||||
}, [])
|
||||
|
||||
// 处理下拉刷新
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
await refetch({ page: 1, limit: 50 })
|
||||
setRefreshing(false)
|
||||
}, [refetch])
|
||||
|
||||
// 处理设置菜单选择
|
||||
const handleSettingsSelect = async (value: string) => {
|
||||
@@ -172,70 +179,86 @@ export default function My() {
|
||||
</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"
|
||||
/>
|
||||
{loading && !refreshing ? (
|
||||
<MySkeleton />
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#FFFFFF"
|
||||
colors={['#FFFFFF']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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' && (
|
||||
<View style={styles.generatingOverlay} />
|
||||
)}
|
||||
|
||||
{/* 右上角作品数量角标 */}
|
||||
{item.status === 'completed'&&<View style={styles.counterBadge}>
|
||||
<Text style={styles.counterText}>
|
||||
{item.count}
|
||||
{/* 数量角标:已完成且有结果 */}
|
||||
{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>}
|
||||
|
||||
{/* "生成中"角标:覆盖在图片左下角 */}
|
||||
{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>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* 编辑资料抽屉 */}
|
||||
<EditProfileDrawer
|
||||
@@ -276,75 +299,6 @@ const styles = StyleSheet.create({
|
||||
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',
|
||||
@@ -384,8 +338,8 @@ const styles = StyleSheet.create({
|
||||
opacity: 0.7,
|
||||
},
|
||||
editButton: {
|
||||
paddingHorizontal: 8, //左右各留 8 像素的内边距
|
||||
paddingVertical: 6, //上下各留 6 像素的内边距
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#1C1E22',
|
||||
},
|
||||
@@ -412,12 +366,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
sectionMoreIcon: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#FFFFFF33',
|
||||
},
|
||||
galleryGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -425,7 +373,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
galleryItem: {
|
||||
width: GALLERY_ITEM_SIZE,
|
||||
// 使用等比例 1:1,保证容器永远是正方形
|
||||
aspectRatio: 1,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#1C1E22',
|
||||
@@ -439,7 +386,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
galleryImage: {
|
||||
width: '100%',
|
||||
// 高度由 aspectRatio 决定,避免拉伸
|
||||
height: undefined,
|
||||
aspectRatio: 1,
|
||||
},
|
||||
@@ -471,4 +417,14 @@ const styles = StyleSheet.create({
|
||||
fontSize: 9,
|
||||
fontWeight: '500',
|
||||
},
|
||||
emptyState: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyStateText: {
|
||||
color: '#FFFFFF80',
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
|
||||
78
components/skeleton/HomeSkeleton.tsx
Normal file
78
components/skeleton/HomeSkeleton.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Dimensions, View, StyleSheet } from 'react-native'
|
||||
import { Skeleton } from './skeleton'
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window')
|
||||
|
||||
const horizontalPadding = 8 * 2
|
||||
const cardGap = 5
|
||||
const cardWidth = (screenWidth - horizontalPadding - cardGap) / 2
|
||||
|
||||
export function HomeSkeleton() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 标签骨架屏 */}
|
||||
<View style={styles.tabsContainer}>
|
||||
{[1, 2, 3, 4].map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
width={60}
|
||||
height={20}
|
||||
borderRadius={4}
|
||||
style={styles.tabSkeleton}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 卡片骨架屏 */}
|
||||
<View style={styles.gridContainer}>
|
||||
{[1, 2, 3, 4, 5, 6].map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.cardSkeleton,
|
||||
index % 2 === 0 ? styles.cardLeft : styles.cardRight,
|
||||
{ width: cardWidth },
|
||||
]}
|
||||
>
|
||||
<Skeleton
|
||||
width="100%"
|
||||
height={cardWidth * 1.2}
|
||||
borderRadius={16}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#090A0B',
|
||||
},
|
||||
tabsContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
gap: 20,
|
||||
marginBottom: 18,
|
||||
},
|
||||
tabSkeleton: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
gridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: 8,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
cardSkeleton: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
cardLeft: {
|
||||
marginRight: 0,
|
||||
},
|
||||
cardRight: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
})
|
||||
@@ -4,9 +4,9 @@ import { useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { OWNER_ID } from '@/lib/auth'
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useActivates = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { OWNER_ID } from '@/lib/auth'
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useCategories = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
@@ -11,7 +11,6 @@ import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useTemplateGenerations = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -106,3 +105,4 @@ export const useTemplateGenerations = () => {
|
||||
}
|
||||
|
||||
export type { TemplateGeneration }
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"language": "Language",
|
||||
"languageSwitch": "Language: 中文",
|
||||
"languageSwitchEn": "Language: English",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"noWorks": "No works yet"
|
||||
},
|
||||
"changePassword": {
|
||||
"title": "Change Password",
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"language": "语言",
|
||||
"languageSwitch": "语言: 中文",
|
||||
"languageSwitchEn": "Language: English",
|
||||
"logout": "退出登录"
|
||||
"logout": "退出登录",
|
||||
"noWorks": "暂无作品"
|
||||
},
|
||||
"changePassword": {
|
||||
"title": "修改密码",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"prebuild:android": "expo prebuild --platform android",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"claude": "claude --dangerously-skip-permissions",
|
||||
"type-checks": "tsc --noEmit",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
|
||||
Reference in New Issue
Block a user