diff --git a/app/(tabs)/video.test.tsx b/app/(tabs)/video.test.tsx
index 0bb72a4..f5d3861 100644
--- a/app/(tabs)/video.test.tsx
+++ b/app/(tabs)/video.test.tsx
@@ -46,6 +46,106 @@ jest.mock('@/components/LoadingState', () => 'LoadingState')
jest.mock('@/components/ErrorState', () => 'ErrorState')
jest.mock('@/components/PaginationLoader', () => 'PaginationLoader')
+// Mock VideoSocialButton
+jest.mock('@/components/blocks/ui/VideoSocialButton', () => ({
+ VideoSocialButton: ({ testID, liked, favorited, likeCount, favoriteCount }: any) => {
+ const React = require('react')
+ const { View, Text } = require('react-native')
+ return React.createElement(
+ View,
+ { testID },
+ liked !== undefined && React.createElement(Text, { testID: `${testID}-like-button` }, liked ? 'liked' : 'not liked'),
+ favorited !== undefined && React.createElement(Text, { testID: `${testID}-favorite-button` }, favorited ? 'favorited' : 'not favorited'),
+ likeCount !== undefined && React.createElement(Text, {}, likeCount.toString()),
+ favoriteCount !== undefined && React.createElement(Text, {}, favoriteCount.toString())
+ )
+ },
+}))
+
+// Mock LikeButton and FavoriteButton
+jest.mock('@/components/blocks/ui/LikeButton', () => ({
+ LikeButton: ({ testID, count }: any) => {
+ const React = require('react')
+ const { Text } = require('react-native')
+ return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'LikeButton')
+ },
+}))
+
+jest.mock('@/components/blocks/ui/FavoriteButton', () => ({
+ FavoriteButton: ({ testID, count }: any) => {
+ const React = require('react')
+ const { Text } = require('react-native')
+ return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'FavoriteButton')
+ },
+}))
+
+// Mock hooks
+jest.mock('@/hooks/use-template-like', () => ({
+ useTemplateLike: jest.fn(() => ({
+ liked: false,
+ loading: false,
+ like: jest.fn(),
+ unlike: jest.fn(),
+ checkLiked: jest.fn(),
+ })),
+}))
+
+jest.mock('@/hooks/use-template-favorite', () => ({
+ useTemplateFavorite: jest.fn(() => ({
+ favorited: false,
+ loading: false,
+ favorite: jest.fn(),
+ unfavorite: jest.fn(),
+ checkFavorited: jest.fn(),
+ })),
+}))
+
+jest.mock('@/stores/templateSocialStore', () => {
+ const actualStore = jest.requireActual('@/stores/templateSocialStore')
+ return {
+ ...actualStore,
+ useTemplateSocialStore: jest.fn((selector) => {
+ const state = {
+ likedMap: {},
+ favoritedMap: {},
+ likeCountMap: {},
+ favoriteCountMap: {},
+ setLikedStates: jest.fn(),
+ setFavoritedStates: jest.fn(),
+ setLikeCountStates: jest.fn(),
+ setFavoriteCountStates: jest.fn(),
+ setLiked: jest.fn(),
+ setFavorited: jest.fn(),
+ setLikeCount: jest.fn(),
+ setFavoriteCount: jest.fn(),
+ incrementLikeCount: jest.fn(),
+ decrementLikeCount: jest.fn(),
+ getLiked: jest.fn(),
+ getFavorited: jest.fn(),
+ getLikeCount: jest.fn(),
+ getFavoriteCount: jest.fn(),
+ isLiked: jest.fn(() => false),
+ isFavorited: jest.fn(() => false),
+ clear: jest.fn(),
+ }
+ if (typeof selector === 'function') {
+ return selector(state)
+ }
+ return state
+ }),
+ templateSocialStore: {
+ useLiked: jest.fn(() => false),
+ useFavorited: jest.fn(() => false),
+ useLikeCount: jest.fn(() => undefined),
+ useFavoriteCount: jest.fn(() => undefined),
+ },
+ useTemplateLiked: jest.fn(() => false),
+ useTemplateFavorited: jest.fn(() => false),
+ useTemplateLikeCount: jest.fn(() => undefined),
+ useTemplateFavoriteCount: jest.fn(() => undefined),
+ }
+})
+
// Mock react-native RefreshControl (directly from react-native)
jest.mock('react-native', () =>
Object.assign({}, jest.requireActual('react-native'), {
@@ -194,6 +294,61 @@ describe('Video Screen', () => {
})
})
+ describe('VideoItem 社交按钮集成', () => {
+ const mockItem = createMockTemplateDetail() as TemplateDetail
+ const mockVideoHeight = 600
+
+ it('应该渲染社交按钮', () => {
+ const { getByTestId } = render(
+
+ )
+
+ expect(getByTestId('video-social-button')).toBeTruthy()
+ })
+
+ it('应该显示点赞数量', () => {
+ const itemWithLikes = createMockTemplateDetail({
+ likeCount: 150,
+ }) as TemplateDetail
+
+ const { getByText } = render(
+
+ )
+
+ expect(getByText('150')).toBeTruthy()
+ })
+
+ it('应该显示收藏数量', () => {
+ const itemWithFavorites = createMockTemplateDetail({
+ favoriteCount: 80,
+ }) as TemplateDetail
+
+ const { getByText } = render(
+
+ )
+
+ expect(getByText('80')).toBeTruthy()
+ })
+
+ it('应该有点赞交互功能', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const likeButton = getByTestId('video-social-button-like-button')
+ expect(likeButton).toBeTruthy()
+ })
+
+ it('应该有收藏交互功能', () => {
+ const { getByTestId } = render(
+
+ )
+
+ const favoriteButton = getByTestId('video-social-button-favorite-button')
+ expect(favoriteButton).toBeTruthy()
+ })
+ })
+
describe('VideoScreen Component', () => {
it('should show loading state when templates are loading', () => {
mockUseTemplates.mockReturnValue({
diff --git a/app/(tabs)/video.tsx b/app/(tabs)/video.tsx
index b2e6ef3..3965920 100644
--- a/app/(tabs)/video.tsx
+++ b/app/(tabs)/video.tsx
@@ -17,6 +17,16 @@ import { useTranslation } from 'react-i18next'
import { SameStyleIcon, WhiteStarIcon } from '@/components/icon'
import { useRouter } from 'expo-router'
import { useTemplates, type TemplateDetail } from '@/hooks'
+import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton'
+import { useTemplateLike } from '@/hooks/use-template-like'
+import { useTemplateFavorite } from '@/hooks/use-template-favorite'
+import {
+ useTemplateSocialStore,
+ useTemplateLiked,
+ useTemplateFavorited,
+ useTemplateLikeCount,
+ useTemplateFavoriteCount,
+} from '@/stores/templateSocialStore'
import LoadingState from '@/components/LoadingState'
import ErrorState from '@/components/ErrorState'
import PaginationLoader from '@/components/PaginationLoader'
@@ -67,6 +77,16 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi
const router = useRouter()
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
+ // 从 store 获取状态
+ const liked = useTemplateLiked(item.id) ?? false
+ const favorited = useTemplateFavorited(item.id) ?? false
+ const likeCount = useTemplateLikeCount(item.id) ?? item.likeCount
+ const favoriteCount = useTemplateFavoriteCount(item.id) ?? item.favoriteCount
+
+ // 使用 hooks 获取操作函数
+ const { like: onLike, unlike: onUnlike, loading: likeLoading } = useTemplateLike(item.id)
+ const { favorite: onFavorite, unfavorite: onUnfavorite, loading: favoriteLoading } = useTemplateFavorite(item.id)
+
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
const { source } = event
if (source?.width && source?.height) {
@@ -109,6 +129,20 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi
{t('video.makeSame')}
+ {/* 社交按钮 */}
+
)
})
diff --git a/stores/templateSocialStore.ts b/stores/templateSocialStore.ts
index 1751a82..abc87f4 100644
--- a/stores/templateSocialStore.ts
+++ b/stores/templateSocialStore.ts
@@ -11,6 +11,9 @@ interface TemplateSocialState {
// 点赞数量缓存: templateId -> number
likeCountMap: Record
+ // 收藏数量缓存: templateId -> number
+ favoriteCountMap: Record
+
// 批量更新点赞状态
setLikedStates: (states: Record) => void
@@ -20,6 +23,9 @@ interface TemplateSocialState {
// 批量更新点赞数量
setLikeCountStates: (states: Record) => void
+ // 批量更新收藏数量
+ setFavoriteCountStates: (states: Record) => void
+
// 更新单个点赞状态
setLiked: (templateId: string, liked: boolean) => void
@@ -29,6 +35,9 @@ interface TemplateSocialState {
// 更新单个点赞数量
setLikeCount: (templateId: string, count: number) => void
+ // 更新单个收藏数量
+ setFavoriteCount: (templateId: string, count: number) => void
+
// 增加点赞数量(乐观更新)
incrementLikeCount: (templateId: string) => void
@@ -44,6 +53,9 @@ interface TemplateSocialState {
// 获取点赞数量(如果不存在返回 undefined)
getLikeCount: (templateId: string) => number | undefined
+ // 获取收藏数量(如果不存在返回 undefined)
+ getFavoriteCount: (templateId: string) => number | undefined
+
// 获取点赞状态(如果不存在返回 false)
isLiked: (templateId: string) => boolean
@@ -61,6 +73,7 @@ export const useTemplateSocialStore = create()(
likedMap: {},
favoritedMap: {},
likeCountMap: {},
+ favoriteCountMap: {},
// Actions
setLikedStates: (states) => {
@@ -81,6 +94,12 @@ export const useTemplateSocialStore = create()(
}))
},
+ setFavoriteCountStates: (states) => {
+ set((state) => ({
+ favoriteCountMap: { ...state.favoriteCountMap, ...states },
+ }))
+ },
+
setLiked: (templateId, liked) => {
set((state) => ({
likedMap: { ...state.likedMap, [templateId]: liked },
@@ -99,6 +118,12 @@ export const useTemplateSocialStore = create()(
}))
},
+ setFavoriteCount: (templateId, count) => {
+ set((state) => ({
+ favoriteCountMap: { ...state.favoriteCountMap, [templateId]: count },
+ }))
+ },
+
incrementLikeCount: (templateId) => {
set((state) => ({
likeCountMap: {
@@ -132,6 +157,10 @@ export const useTemplateSocialStore = create()(
return get().likeCountMap[templateId]
},
+ getFavoriteCount: (templateId) => {
+ return get().favoriteCountMap[templateId]
+ },
+
isLiked: (templateId) => {
return get().likedMap[templateId] || false
},
@@ -145,6 +174,7 @@ export const useTemplateSocialStore = create()(
likedMap: {},
favoritedMap: {},
likeCountMap: {},
+ favoriteCountMap: {},
})
},
}),
@@ -171,3 +201,12 @@ export function useTemplateLikeCount(templateId: string | undefined) {
templateId ? state.likeCountMap[templateId] : undefined
)
}
+
+export function useTemplateFavoriteCount(templateId: string | undefined) {
+ return useTemplateSocialStore((state) =>
+ templateId ? state.favoriteCountMap[templateId] : undefined
+ )
+}
+
+// 为了方便使用,导出 templateSocialStore 别名
+export const templateSocialStore = useTemplateSocialStore