refactor: 优化代码和添加类型检查脚本

- 统一 OWNER_ID 导入,从 lib/auth 导入而非环境变量
- 添加 type-checks 脚本用于 TypeScript 类型检查
- 对接 my 页面后端接口,使用 useTemplateGenerations
- 添加 MySkeleton 骨架屏组件
- 添加下拉刷新功能
- 添加空状态提示文案

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2026-01-16 12:34:27 +08:00
parent 755a374b67
commit 4270973652
8 changed files with 205 additions and 168 deletions

View File

@@ -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,
},
})

View 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,
},
})

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 }

View File

@@ -8,7 +8,8 @@
"language": "Language",
"languageSwitch": "Language: 中文",
"languageSwitchEn": "Language: English",
"logout": "Logout"
"logout": "Logout",
"noWorks": "No works yet"
},
"changePassword": {
"title": "Change Password",

View File

@@ -8,7 +8,8 @@
"language": "语言",
"languageSwitch": "语言: 中文",
"languageSwitchEn": "Language: English",
"logout": "退出登录"
"logout": "退出登录",
"noWorks": "暂无作品"
},
"changePassword": {
"title": "修改密码",

View File

@@ -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",