fix: improve VideoSocialButton styling

- Use vertical layout with icon above count (TikTok style)
- Show correct liked/favorited state with filled icons
- Add count formatting (1.5k, 2.8w)
- Improve button background and positioning
- Update tests for new component structure
This commit is contained in:
imeepos 2026-01-28 20:09:59 +08:00
parent e6416ee604
commit 20459ffd1d
2 changed files with 141 additions and 58 deletions

View File

@ -90,6 +90,20 @@ describe('VideoSocialButton Component', () => {
) )
expect(getByText('50')).toBeTruthy() expect(getByText('50')).toBeTruthy()
}) })
it('应该在已点赞状态显示实心心形图标', () => {
const { getByTestId } = render(
<VideoSocialButton templateId="test-1" liked={true} testID="video-social-liked" />
)
expect(getByTestId('video-social-liked-like-button')).toBeTruthy()
})
it('应该在已收藏状态显示实心星形图标', () => {
const { getByTestId } = render(
<VideoSocialButton templateId="test-1" favorited={true} testID="video-social-favorited" />
)
expect(getByTestId('video-social-favorited-favorite-button')).toBeTruthy()
})
}) })
describe('交互', () => { describe('交互', () => {
@ -113,6 +127,26 @@ describe('VideoSocialButton Component', () => {
expect(onFavorite).toHaveBeenCalledTimes(1) expect(onFavorite).toHaveBeenCalledTimes(1)
}) })
it('已点赞状态点击应该调用 onUnlike', () => {
const onUnlike = jest.fn()
const { getByTestId } = render(
<VideoSocialButton templateId="test-1" liked={true} onUnlike={onUnlike} testID="video-social-on-unlike" />
)
const likeButton = getByTestId('video-social-on-unlike-like-button')
fireEvent.press(likeButton)
expect(onUnlike).toHaveBeenCalledTimes(1)
})
it('已收藏状态点击应该调用 onUnfavorite', () => {
const onUnfavorite = jest.fn()
const { getByTestId } = render(
<VideoSocialButton templateId="test-1" favorited={true} onUnfavorite={onUnfavorite} testID="video-social-on-unfavorite" />
)
const favoriteButton = getByTestId('video-social-on-unfavorite-favorite-button')
fireEvent.press(favoriteButton)
expect(onUnfavorite).toHaveBeenCalledTimes(1)
})
it('loading 状态下应该禁用按钮', () => { it('loading 状态下应该禁用按钮', () => {
const onLike = jest.fn() const onLike = jest.fn()
const onFavorite = jest.fn() const onFavorite = jest.fn()
@ -136,6 +170,46 @@ describe('VideoSocialButton Component', () => {
}) })
}) })
describe('数量格式化', () => {
it('应该显示零数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={0} favoriteCount={0} />
)
expect(getByText('0')).toBeTruthy()
})
it('应该格式化千位数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={1500} favoriteCount={2800} />
)
expect(getByText('1.5k')).toBeTruthy()
expect(getByText('2.8k')).toBeTruthy()
})
it('应该格式化万位数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={15000} favoriteCount={28000} />
)
expect(getByText('1.5w')).toBeTruthy()
expect(getByText('2.8w')).toBeTruthy()
})
it('应该处理大数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={9999} favoriteCount={8888} />
)
expect(getByText('9999')).toBeTruthy()
expect(getByText('8888')).toBeTruthy()
})
it('应该处理 undefined 数量', () => {
const { getAllByText } = render(
<VideoSocialButton templateId="test-1" />
)
expect(getAllByText('0').length).toBe(2)
})
})
describe('样式', () => { describe('样式', () => {
it('应该使用垂直布局', () => { it('应该使用垂直布局', () => {
const { getByTestId } = render( const { getByTestId } = render(
@ -155,21 +229,6 @@ describe('VideoSocialButton Component', () => {
}) })
describe('边界情况', () => { describe('边界情况', () => {
it('应该处理零数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={0} favoriteCount={0} />
)
expect(getByText('0')).toBeTruthy()
})
it('应该处理大数量', () => {
const { getByText } = render(
<VideoSocialButton templateId="test-1" likeCount={9999} favoriteCount={8888} />
)
expect(getByText('9999')).toBeTruthy()
expect(getByText('8888')).toBeTruthy()
})
it('应该处理缺失的回调函数', () => { it('应该处理缺失的回调函数', () => {
const { getByTestId } = render( const { getByTestId } = render(
<VideoSocialButton templateId="test-1" testID="video-social-no-callback" /> <VideoSocialButton templateId="test-1" testID="video-social-no-callback" />

View File

@ -1,7 +1,6 @@
import React, { memo, useCallback, useMemo } from 'react' import React, { memo, useCallback } from 'react'
import { View, StyleSheet } from 'react-native' import { View, StyleSheet, Text, Pressable } from 'react-native'
import { LikeButton, LikeButtonProps } from './LikeButton' import { Ionicons } from '@expo/vector-icons'
import { FavoriteButton, FavoriteButtonProps } from './FavoriteButton'
export interface VideoSocialButtonProps { export interface VideoSocialButtonProps {
templateId: string templateId: string
@ -52,41 +51,53 @@ const VideoSocialButtonComponent: React.FC<VideoSocialButtonProps> = ({
} }
}, [loading, favorited, onFavorite, onUnfavorite]) }, [loading, favorited, onFavorite, onUnfavorite])
// LikeButton 的 props // 格式化数量显示
const likeButtonProps: LikeButtonProps = useMemo( const formatCount = (count?: number): string => {
() => ({ if (count === undefined || count === null) return '0'
liked, if (count >= 10000) {
loading, return `${(count / 10000).toFixed(1)}w`
count: likeCount, }
onPress: handleLikePress, if (count >= 1000) {
testID: testID ? `${testID}-like-button` : undefined, return `${(count / 1000).toFixed(1)}k`
}), }
[liked, loading, likeCount, handleLikePress, testID] return count.toString()
) }
// FavoriteButton 的 props
const favoriteButtonProps: FavoriteButtonProps = useMemo(
() => ({
favorited,
loading,
count: favoriteCount,
onPress: handleFavoritePress,
testID: testID ? `${testID}-favorite-button` : undefined,
}),
[favorited, loading, favoriteCount, handleFavoritePress, testID]
)
return ( return (
<View style={styles.container} testID={testID}> <View style={styles.container} testID={testID}>
{/* 点赞按钮 */} {/* 点赞按钮 */}
<View style={styles.button}> <Pressable
<LikeButton {...likeButtonProps} /> style={styles.buttonWrapper}
</View> onPress={handleLikePress}
disabled={loading}
testID={testID ? `${testID}-like-button` : undefined}
>
<View style={[styles.iconContainer, liked && styles.iconContainerActive]}>
<Ionicons
name={liked ? 'heart' : 'heart-outline'}
size={28}
color={liked ? '#FF3B30' : '#FFFFFF'}
/>
</View>
<Text style={styles.count}>{formatCount(likeCount)}</Text>
</Pressable>
{/* 收藏按钮 */} {/* 收藏按钮 */}
<View style={styles.button}> <Pressable
<FavoriteButton {...favoriteButtonProps} /> style={styles.buttonWrapper}
</View> onPress={handleFavoritePress}
disabled={loading}
testID={testID ? `${testID}-favorite-button` : undefined}
>
<View style={[styles.iconContainer, favorited && styles.iconContainerActive]}>
<Ionicons
name={favorited ? 'star' : 'star-outline'}
size={28}
color={favorited ? '#FFD700' : '#FFFFFF'}
/>
</View>
<Text style={styles.count}>{formatCount(favoriteCount)}</Text>
</Pressable>
</View> </View>
) )
} }
@ -96,20 +107,33 @@ export const VideoSocialButton = memo(VideoSocialButtonComponent)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
position: 'absolute', position: 'absolute',
right: 13, right: 12,
bottom: 100, bottom: 180,
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
gap: 16, gap: 20,
}, },
button: { buttonWrapper: {
width: 48, alignItems: 'center',
height: 48, gap: 4,
borderRadius: 24, },
backgroundColor: 'rgba(25, 27, 31, 0.8)', iconContainer: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: 1, },
borderColor: 'rgba(47, 49, 52, 1)', iconContainerActive: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
},
count: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
}, },
}) })