feat: integrate real message data with SDK in message page

Replace mock data with useMessages hook and useMessageActions for real-time message management. Add support for tab-based filtering (all/notice/other), pull-to-refresh, infinite scroll pagination, and mark-as-read functionality. Integrate LoadingState, ErrorState, and PaginationLoader components for better UX.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
imeepos 2026-01-21 14:58:32 +08:00
parent 6c17d720ca
commit 4868ff8660
2 changed files with 226 additions and 109 deletions

View File

@ -6,87 +6,198 @@ import {
ScrollView,
StatusBar as RNStatusBar,
Pressable,
RefreshControl,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { useMessages, type Message } from '@/hooks/use-messages'
import { useMessageActions } from '@/hooks/use-message-actions'
import LoadingState from '@/components/LoadingState'
import ErrorState from '@/components/ErrorState'
import PaginationLoader from '@/components/PaginationLoader'
// 消息卡片数据
interface MessageCard {
id: number
title: string
subtitle: string
body: string
time: string
type: 'notice' | 'other'
isNew?: boolean // 是否为新消息
const getMessageTypeByTab = (tab: 'all' | 'notice' | 'other') => {
if (tab === 'all') return undefined
if (tab === 'notice') return ['SYSTEM', 'ACTIVITY']
return ['BILLING', 'MARKETING']
}
const messageCards: MessageCard[] = [
{
id: 1,
title: '恭喜你获得双12新用户专享福利',
subtitle: '打开推送的内容,限时优惠,多种玩法,快来体验~',
body: '图片占位。',
time: '2023-12-29 18:32:21',
type: 'notice',
isNew: true, // 新消息
},
{
id: 2,
title: '新功能上线AI 智能生成',
subtitle: '全新 AI 功能已上线,快来体验吧!',
body: '图片占位。',
time: '2023-12-28 15:20:10',
type: 'notice',
isNew: true, // 新消息
},
{
id: 3,
title: '系统维护通知',
subtitle: '系统将于今晚进行维护升级',
body: '图片占位。',
time: '2023-12-27 10:15:30',
type: 'other',
isNew: false, // 非新消息
},
{
id: 4,
title: '限时活动:分享有礼',
subtitle: '分享你的作品,赢取丰厚奖励',
body: '图片占位。',
time: '2023-12-26 14:05:22',
type: 'notice',
isNew: false, // 非新消息
},
{
id: 5,
title: '版本更新提醒',
subtitle: '新版本已发布,建议及时更新',
body: '图片占位。',
time: '2023-12-25 09:30:45',
type: 'other',
isNew: false, // 非新消息
},
]
const formatTime = (date: Date) => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export default function MessageScreen() {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<'all' | 'notice' | 'other'>('all')
const { markRead } = useMessageActions()
// 根据选中的标签过滤卡片
const filteredCards = messageCards.filter((card) => {
if (activeTab === 'all') return true
return card.type === activeTab
const messageType = getMessageTypeByTab(activeTab)
const { messages, loading, loadingMore, error, refetch, loadMore, hasMore, execute } = useMessages({
limit: 20,
})
useEffect(() => {
execute({ type: messageType as any })
}, [activeTab])
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 50
if (isCloseToBottom && hasMore && !loadingMore && !loading) {
loadMore()
}
}
const handleMessagePress = async (message: Message) => {
if (!message.isRead) {
await markRead(message.id)
refetch()
}
}
if (loading && messages.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.all')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.all')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('notice')}>
{activeTab === 'notice' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.notice')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.notice')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('other')}>
{activeTab === 'other' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.other')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.other')}</Text>
)}
</Pressable>
</View>
<LoadingState color="#FFFFFF" />
</SafeAreaView>
)
}
if (error && messages.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.all')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.all')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('notice')}>
{activeTab === 'notice' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.notice')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.notice')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('other')}>
{activeTab === 'other' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.other')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.other')}</Text>
)}
</Pressable>
</View>
<ErrorState message={error.message} onRetry={refetch} />
</SafeAreaView>
)
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
{/* 固定在顶部的标签选择器 */}
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
@ -156,45 +267,57 @@ export default function MessageScreen() {
</Pressable>
</View>
{/* 可滚动的消息卡片列表 */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={400}
refreshControl={
<RefreshControl
refreshing={loading && messages.length > 0}
onRefresh={refetch}
tintColor="#FFFFFF"
/>
}
>
{/* 消息卡片列表 */}
{filteredCards.length > 0 ? (
filteredCards.map((card) => (
<View key={card.id} style={styles.cardContainer}>
{/* 新消息绿色指示点 */}
{card.isNew && (
{messages.length > 0 ? (
<>
{messages.map((message) => (
<Pressable
key={message.id}
onPress={() => handleMessagePress(message)}
>
<View style={styles.cardContainer}>
{!message.isRead && (
<View style={styles.newMessageDotContainer}>
<View style={styles.newMessageDot} />
</View>
)}
<Text style={styles.cardTitle}>
{card.title}
{message.title}
</Text>
<Text style={styles.cardSubtitle} numberOfLines={2}>
{card.subtitle}
{message.content}
</Text>
<View style={styles.cardBody}>
<Text style={styles.cardBodyText}>
{/* TODO:这里是图片 */}
{card.body}
{message.data || ''}
</Text>
</View>
<Text style={styles.cardTime}>
{card.time}
{formatTime(message.createdAt)}
</Text>
</View>
))
</Pressable>
))}
{loadingMore && <PaginationLoader color="#FFFFFF" />}
</>
) : (
<View style={styles.emptyContainer}>
{/* <NoNewsIcon /> */}
💭
<Text style={styles.emptyIcon}>💭</Text>
<Text style={styles.emptyText}>{t('message.noMessages')}</Text>
</View>
)}
@ -216,12 +339,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
paddingTop: 12,
},
appMiniIcon: {
width: 28,
height: 18,
borderRadius: 4,
backgroundColor: '#FF66AA',
},
segment: {
flexDirection: 'row',
paddingHorizontal: 8,
@ -230,7 +347,6 @@ const styles = StyleSheet.create({
backgroundColor: '#090A0B',
gap: 16,
},
segmentText: {
fontSize: 14,
color: '#FFFFFF',
@ -252,20 +368,13 @@ const styles = StyleSheet.create({
segmentTextActiveText: {
zIndex: 1,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#00FF66',
marginRight: 4,
},
cardContainer: {
backgroundColor: '#16181B',
borderRadius: 16,
padding: 16,
marginBottom: 12,
marginHorizontal: 4,
position: 'relative', // 用于定位新消息指示点
position: 'relative',
},
newMessageDotContainer: {
position: 'absolute',
@ -283,7 +392,7 @@ const styles = StyleSheet.create({
width: 6,
height: 6,
borderRadius: 4,
backgroundColor: '#00FF66', // 绿色
backgroundColor: '#00FF66',
zIndex: 1,
},
cardTitle: {
@ -300,8 +409,9 @@ const styles = StyleSheet.create({
cardBody: {
backgroundColor: '#26292E',
borderRadius: 12,
height:100,
height: 100,
marginBottom: 12,
padding: 8,
},
cardBodyText: {
color: '#FFFFFF',
@ -318,6 +428,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingTop: 200,
},
emptyIcon: {
fontSize: 48,
},
emptyText: {
color: '#8A8A8A',
fontSize: 12,

View File

@ -9,10 +9,14 @@ import { handleError } from './use-error'
interface ListMessagesParams {
page?: number
limit?: number
type?: ('SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING')[]
isRead?: boolean
}
const DEFAULT_PARAMS = {
limit: 20,
orderBy: 'createdAt',
order: 'desc',
}
export const useMessages = (initialParams?: ListMessagesParams) => {