diff --git a/app/templateDetail.tsx b/app/templateDetail.tsx
index 0b8f219..a1235de 100644
--- a/app/templateDetail.tsx
+++ b/app/templateDetail.tsx
@@ -1,66 +1,79 @@
+import { useEffect } from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
StatusBar as RNStatusBar,
+ ActivityIndicator,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
-import { useRouter } from 'expo-router'
+import { useRouter, useLocalSearchParams } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { LeftArrowIcon } from '@/components/icon'
import SearchResultsGrid from '@/components/SearchResultsGrid'
+import { useTemplateDetail, useTemplateGenerations, type TemplateGeneration } from '@/hooks'
-// 模板详情数据 - 根据实际需求可以改为从 API 获取
-const templateDetailData = [
- {
- id: 1,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/android-icon-background.png'),
- height: 214,
- },
- {
- id: 2,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/android-icon-background.png'),
- height: 236,
- },
- {
- id: 3,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/favicon.png'),
- height: 214,
- },
- {
- id: 4,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/icon.png'),
- height: 200,
- },
- {
- id: 5,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/android-icon-background.png'),
- height: 220,
- },
- {
- id: 6,
- title: '猫咪圣诞写真',
- image: require('@/assets/images/android-icon-background.png'),
- height: 214,
- },
-]
+const CARD_HEIGHTS = [214, 236, 200, 220, 210, 225]
export default function TemplateDetailScreen() {
const { t } = useTranslation()
const router = useRouter()
+ const params = useLocalSearchParams()
+ const templateId = typeof params.id === 'string' ? params.id : undefined
+
+ const { data: templateDetail, loading: templateLoading, error: templateError, execute: loadTemplateDetail } = useTemplateDetail()
+ const { generations, loading: generationsLoading, execute: loadGenerations } = useTemplateGenerations()
+
+ useEffect(() => {
+ if (templateId) {
+ loadTemplateDetail({ id: templateId })
+ loadGenerations({ templateId, page: 1, limit: 20 })
+ }
+ }, [templateId, loadTemplateDetail, loadGenerations])
+
+ // 直接使用 TemplateGeneration 数据,只添加必要的 height 字段
+ const displayData = generations.map((generation, index) => ({
+ ...generation,
+ height: CARD_HEIGHTS[index % CARD_HEIGHTS.length],
+ title: generation.template?.title || generation.template?.titleEn || '',
+ image: generation.resultUrl?.[0] || generation.originalUrl || '',
+ }))
+
+ if (templateLoading && !templateDetail) {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+ if (templateError && !templateDetail) {
+ return (
+
+
+
+
+ 加载失败,请返回重试
+ router.back()}>
+ 返回
+
+
+
+ )
+ }
+
return (
-
+
{/* 顶部导航栏 */}
- {t('templateDetail.title')}
+ {templateDetail?.title || templateDetail?.titleEn || t('templateDetail.title')}
{t('templateDetail.subtitle')}
- {/* 图片网格 */}
-
+ {/* 加载更多指示器 */}
+ {generationsLoading && generations.length > 0 && (
+
+
+ 加载中...
+
+ )}
+
+
)
}
@@ -92,6 +112,12 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#090A0B',
},
+ centerContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 12,
+ },
header: {
flexDirection: 'row',
alignItems: 'center',
@@ -117,5 +143,32 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '400',
},
+ errorText: {
+ color: '#FFFFFF',
+ fontSize: 14,
+ textAlign: 'center',
+ },
+ retryButton: {
+ backgroundColor: '#FF6699',
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 8,
+ },
+ retryButtonText: {
+ color: '#FFFFFF',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ loadingMoreContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ paddingVertical: 16,
+ },
+ loadingMoreText: {
+ color: '#ABABAB',
+ fontSize: 12,
+ },
})
diff --git a/components/SearchResultsGrid.tsx b/components/SearchResultsGrid.tsx
index 487f6f3..6df04d7 100644
--- a/components/SearchResultsGrid.tsx
+++ b/components/SearchResultsGrid.tsx
@@ -12,17 +12,14 @@ import { useRouter } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { WhiteStarIcon } from '@/components/icon'
+import type { TemplateGeneration } from '@/hooks'
const { width: screenWidth } = Dimensions.get('window')
-interface SearchResultItem {
- id: number
- title: string
- image: any
+interface SearchResultItem extends TemplateGeneration {
height: number
- videoUrl?: any
- thumbnailUrl?: any
- duration?: string
+ title: string
+ image: string
}
interface SearchResultsGridProps {
@@ -33,27 +30,24 @@ export default function SearchResultsGrid({ results }: SearchResultsGridProps) {
const { t } = useTranslation()
const router = useRouter()
const [gridWidth, setGridWidth] = useState(screenWidth)
-
- const horizontalPadding = 8 * 2 // gridContainer 的左右 padding
- const cardGap = 5 // 两个卡片之间的间距
+
+ const horizontalPadding = 8 * 2
+ const cardGap = 5
const cardWidth = (gridWidth - horizontalPadding - cardGap) / 2
const handleSameStylePress = (item: SearchResultItem) => {
- // 构建模板数据,传递给 generateVideo 页面
const templateData = {
- id: item.id,
- videoUrl: item.videoUrl || item.image, // 如果没有 videoUrl,使用 image
- thumbnailUrl: item.thumbnailUrl || item.image, // 如果没有 thumbnailUrl,使用 image
- title: item.title,
- duration: item.duration || '00:00', // 默认时长
+ id: item.template?.id,
+ videoUrl: item.resultUrl?.[0] || item.originalUrl,
+ thumbnailUrl: item.template?.coverImageUrl,
+ title: item.template?.title || item.template?.titleEn || '',
}
router.push({
pathname: '/generateVideo' as any,
- params: {
- template: JSON.stringify(templateData),
- },
+ params: { template: JSON.stringify(templateData) },
} as any)
}
+
if (results.length === 0) {
return (
@@ -61,6 +55,7 @@ export default function SearchResultsGrid({ results }: SearchResultsGridProps) {
)
}
+
return (
-
+
{item.title}
@@ -116,9 +104,6 @@ const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
- scrollContent: {
- // paddingBottom: 100,
- },
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -142,18 +127,23 @@ const styles = StyleSheet.create({
borderRadius: 12,
overflow: 'hidden',
marginBottom: 8,
- position: 'relative',
},
cardImage: {
width: '100%',
height: '100%',
},
+ cardTitle: {
+ color: '#FFFFFF',
+ fontSize: 12,
+ fontWeight: '500',
+ paddingHorizontal: 8,
+ },
sameStyleButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
backgroundColor: '#262A31',
- height:32,
+ height: 32,
marginHorizontal: 8,
marginTop: 10,
marginBottom: 8,
@@ -166,12 +156,6 @@ const styles = StyleSheet.create({
fontWeight: '500',
lineHeight: 17,
},
- cardTitle: {
- color: '#FFFFFF',
- fontSize: 12,
- fontWeight: '500',
- paddingHorizontal: 8,
- },
emptyContainer: {
flex: 1,
backgroundColor: '#090A0B',
diff --git a/hooks/index.ts b/hooks/index.ts
new file mode 100644
index 0000000..116628f
--- /dev/null
+++ b/hooks/index.ts
@@ -0,0 +1,6 @@
+export { useActivates } from './use-activates'
+export { useCategories } from './use-categories'
+export { useTemplateActions } from './use-template-actions'
+export { useTemplates, type TemplateDetail } from './use-templates'
+export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './use-template-detail'
+export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations'
diff --git a/hooks/use-activates.ts b/hooks/use-activates.ts
index 6b18a3e..6b07705 100644
--- a/hooks/use-activates.ts
+++ b/hooks/use-activates.ts
@@ -1,17 +1,38 @@
-import { OWNER_ID } from "@/lib/auth"
-import { ApiError } from "@/lib/types"
import { root } from '@repo/core'
-import { ActivityController, ListActivitiesResult } from "@repo/sdk"
-import { useState } from "react"
+import { ActivityController, type ListActivitiesInput, type ListActivitiesResult } from '@repo/sdk'
+import { useState } from 'react'
+
+import { type ApiError } from '@/lib/types'
+
+import { handleError } from './use-error'
+
+const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
+
export const useActivates = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [data, setData] = useState()
- const load = async () => {
+
+ const load = async (params?: ListActivitiesInput) => {
try {
setLoading(true)
- const c = root.get(ActivityController)
- const data = await c.list({ page: 1, limit: 10, isActive: true, orderBy: 'sortOrder', order: 'desc', ownerId: OWNER_ID })
+ const activity = root.get(ActivityController)
+ const { data, error } = await handleError(
+ async () =>
+ await activity.list({
+ page: 1,
+ limit: 10,
+ isActive: true,
+ orderBy: 'sortOrder',
+ order: 'desc',
+ ownerId: OWNER_ID,
+ ...params,
+ }),
+ )
+ if (error) {
+ setError(error)
+ return
+ }
setData(data)
} catch (e) {
setError(e as ApiError)
@@ -24,6 +45,6 @@ export const useActivates = () => {
load,
loading,
error,
- data
+ data,
}
-}
\ No newline at end of file
+}
diff --git a/hooks/use-categories.ts b/hooks/use-categories.ts
new file mode 100644
index 0000000..6bd2f41
--- /dev/null
+++ b/hooks/use-categories.ts
@@ -0,0 +1,52 @@
+import { root } from '@repo/core'
+import { CategoryController, type ListCategoriesInput, type ListCategoriesResult } from '@repo/sdk'
+import { useState } from 'react'
+
+import { type ApiError } from '@/lib/types'
+
+import { handleError } from './use-error'
+
+const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
+
+export const useCategories = () => {
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [data, setData] = useState()
+
+ const load = async (params?: ListCategoriesInput) => {
+ try {
+ setLoading(true)
+ const category = root.get(CategoryController)
+ const { data, error } = await handleError(
+ async () =>
+ await category.list({
+ page: 1,
+ limit: 1000,
+ isActive: true,
+ orderBy: 'sortOrder',
+ order: 'desc',
+ withChildren: true,
+ ownerId: OWNER_ID,
+ ...params,
+ }),
+ )
+ if (error) {
+ setError(error)
+ return
+ }
+ setData(data)
+ } catch (e) {
+ setError(e as ApiError)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return {
+ load,
+ loading,
+ error,
+ data,
+ }
+}
+
diff --git a/hooks/use-error.ts b/hooks/use-error.ts
new file mode 100644
index 0000000..3c244c6
--- /dev/null
+++ b/hooks/use-error.ts
@@ -0,0 +1,10 @@
+import { type ApiError } from '@/lib/types'
+
+export async function handleError(cb: () => Promise) {
+ try {
+ const data = await cb()
+ return { data, error: null }
+ } catch (e) {
+ return { data: null, error: e as ApiError }
+ }
+}
diff --git a/hooks/use-template-detail.ts b/hooks/use-template-detail.ts
new file mode 100644
index 0000000..784ead2
--- /dev/null
+++ b/hooks/use-template-detail.ts
@@ -0,0 +1,52 @@
+import { root } from '@repo/core'
+import { TemplateController, type TemplateDetail } from '@repo/sdk'
+import { useCallback, useState } from 'react'
+
+import { type ApiError } from '@/lib/types'
+
+import { handleError } from './use-error'
+
+interface UseTemplateDetailParams {
+ id: string
+}
+
+export const useTemplateDetail = () => {
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [data, setData] = useState()
+
+ const execute = useCallback(async (params: UseTemplateDetailParams) => {
+ setLoading(true)
+ setError(null)
+
+ const template = root.get(TemplateController)
+ const { data, error } = await handleError(async () => await template.get(params.id))
+
+ if (error) {
+ setError(error)
+ setLoading(false)
+ return { data: undefined, error }
+ }
+
+ setData(data)
+ setLoading(false)
+ return { data, error: null }
+ }, [])
+
+ const refetch = useCallback(
+ (params: UseTemplateDetailParams) => {
+ return execute(params)
+ },
+ [execute],
+ )
+
+ return {
+ data,
+ loading,
+ error,
+ execute,
+ refetch,
+ }
+}
+
+export type { TemplateDetail }
diff --git a/hooks/use-template-generations.ts b/hooks/use-template-generations.ts
new file mode 100644
index 0000000..727725f
--- /dev/null
+++ b/hooks/use-template-generations.ts
@@ -0,0 +1,108 @@
+import { root } from '@repo/core'
+import {
+ TemplateGenerationController,
+ type ListTemplateGenerationsInput,
+ type ListTemplateGenerationsResult,
+ type TemplateGeneration,
+} from '@repo/sdk'
+import { useCallback, useRef, useState } from 'react'
+
+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)
+ const [loadingMore, setLoadingMore] = useState(false)
+ const [error, setError] = useState(null)
+ const [data, setData] = useState()
+ const currentPageRef = useRef(1)
+ const hasMoreRef = useRef(true)
+
+ const execute = useCallback(async (params?: ListTemplateGenerationsInput) => {
+ setLoading(true)
+ setError(null)
+ currentPageRef.current = params?.page || 1
+
+ const templateGeneration = root.get(TemplateGenerationController)
+ const { data, error } = await handleError(
+ async () =>
+ await templateGeneration.list({
+ page: params?.page || 1,
+ limit: params?.limit || 20,
+ ...params,
+ }),
+ )
+ if (error) {
+ setError(error)
+ setLoading(false)
+ return { data: undefined, error }
+ }
+
+ const generations = data?.data || []
+ hasMoreRef.current = generations.length >= (params?.limit || 20)
+ setData(data)
+ setLoading(false)
+ return { data, error: null }
+ }, [])
+
+ const loadMore = useCallback(
+ async (params?: Omit) => {
+ if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
+
+ setLoadingMore(true)
+ const nextPage = currentPageRef.current + 1
+
+ const templateGeneration = root.get(TemplateGenerationController)
+ const { data: newData, error } = await handleError(
+ async () =>
+ await templateGeneration.list({
+ page: nextPage,
+ limit: params?.limit || 20,
+ ...params,
+ }),
+ )
+
+ if (error) {
+ setLoadingMore(false)
+ return { data: undefined, error }
+ }
+
+ const newGenerations = newData?.data || []
+ hasMoreRef.current = newGenerations.length >= (params?.limit || 20)
+ currentPageRef.current = nextPage
+
+ setData((prev) => ({
+ ...newData,
+ data: [...(prev?.data || []), ...newGenerations],
+ }))
+ setLoadingMore(false)
+ return { data: newData, error: null }
+ },
+ [loading, loadingMore],
+ )
+
+ const refetch = useCallback(
+ (params?: ListTemplateGenerationsInput) => {
+ hasMoreRef.current = true
+ return execute(params)
+ },
+ [execute],
+ )
+
+ return {
+ data,
+ generations: data?.data || [],
+ loading,
+ loadingMore,
+ error,
+ execute,
+ refetch,
+ loadMore,
+ hasMore: hasMoreRef.current,
+ }
+}
+
+export type { TemplateGeneration }
diff --git a/hooks/use-templates.ts b/hooks/use-templates.ts
new file mode 100644
index 0000000..2619621
--- /dev/null
+++ b/hooks/use-templates.ts
@@ -0,0 +1,109 @@
+import { root } from '@repo/core'
+import { type ListTemplatesInput, type ListTemplatesResult, TemplateController, type TemplateDetail } from '@repo/sdk'
+import { useCallback, useRef, useState } from 'react'
+
+import { type ApiError } from '@/lib/types'
+
+import { handleError } from './use-error'
+
+const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
+
+type ListTemplatesParams = Omit
+
+const DEFAULT_PARAMS = {
+ limit: 20,
+ sortBy: 'createdAt' as const,
+ sortOrder: 'desc' as const,
+}
+
+export const useTemplates = (initialParams?: ListTemplatesParams) => {
+ const [loading, setLoading] = useState(false)
+ const [loadingMore, setLoadingMore] = useState(false)
+ const [error, setError] = useState(null)
+ const [data, setData] = useState()
+ const currentPageRef = useRef(1)
+ const hasMoreRef = useRef(true)
+
+ const execute = useCallback(async (params?: ListTemplatesParams) => {
+ setLoading(true)
+ setError(null)
+ currentPageRef.current = params?.page || 1
+
+ const template = root.get(TemplateController)
+ const { data, error } = await handleError(
+ async () =>
+ await template.list({
+ ...DEFAULT_PARAMS,
+ ...initialParams,
+ ...params,
+ ownerId: OWNER_ID,
+ }),
+ )
+
+ if (error) {
+ setError(error)
+ setLoading(false)
+ return { data: undefined, error }
+ }
+
+ const templates = data?.templates || []
+ hasMoreRef.current = templates.length >= (params?.limit || DEFAULT_PARAMS.limit)
+ setData(data)
+ setLoading(false)
+ return { data, error: null }
+ }, [initialParams])
+
+ const loadMore = useCallback(async () => {
+ if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
+
+ setLoadingMore(true)
+ const nextPage = currentPageRef.current + 1
+
+ const template = root.get(TemplateController)
+ const { data: newData, error } = await handleError(
+ async () =>
+ await template.list({
+ ...DEFAULT_PARAMS,
+ ...initialParams,
+ page: nextPage,
+ ownerId: OWNER_ID,
+ }),
+ )
+
+ if (error) {
+ setLoadingMore(false)
+ return { data: undefined, error }
+ }
+
+ const newTemplates = newData?.templates || []
+ hasMoreRef.current = newTemplates.length >= DEFAULT_PARAMS.limit
+ currentPageRef.current = nextPage
+
+ setData((prev) => ({
+ ...newData,
+ templates: [...(prev?.templates || []), ...newTemplates],
+ }))
+ setLoadingMore(false)
+ return { data: newData, error: null }
+ }, [loading, loadingMore, initialParams])
+
+ const refetch = useCallback(() => {
+ hasMoreRef.current = true
+ return execute()
+ }, [execute])
+
+ return {
+ data,
+ templates: data?.templates || [],
+ loading,
+ loadingMore,
+ error,
+ execute,
+ refetch,
+ loadMore,
+ hasMore: hasMoreRef.current,
+ }
+}
+
+export type { TemplateDetail }
+