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:
@@ -6,87 +6,198 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
StatusBar as RNStatusBar,
|
StatusBar as RNStatusBar,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
RefreshControl,
|
||||||
|
NativeScrollEvent,
|
||||||
|
NativeSyntheticEvent,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { useTranslation } from 'react-i18next'
|
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'
|
||||||
|
|
||||||
// 消息卡片数据
|
const getMessageTypeByTab = (tab: 'all' | 'notice' | 'other') => {
|
||||||
interface MessageCard {
|
if (tab === 'all') return undefined
|
||||||
id: number
|
if (tab === 'notice') return ['SYSTEM', 'ACTIVITY']
|
||||||
title: string
|
return ['BILLING', 'MARKETING']
|
||||||
subtitle: string
|
|
||||||
body: string
|
|
||||||
time: string
|
|
||||||
type: 'notice' | 'other'
|
|
||||||
isNew?: boolean // 是否为新消息
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageCards: MessageCard[] = [
|
const formatTime = (date: Date) => {
|
||||||
{
|
const d = new Date(date)
|
||||||
id: 1,
|
const year = d.getFullYear()
|
||||||
title: '恭喜你,获得双12新用户专享福利',
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
subtitle: '打开推送的内容,限时优惠,多种玩法,快来体验~',
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
body: '图片占位。',
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
time: '2023-12-29 18:32:21',
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
type: 'notice',
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
isNew: true, // 新消息
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
},
|
}
|
||||||
{
|
|
||||||
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, // 非新消息
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function MessageScreen() {
|
export default function MessageScreen() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [activeTab, setActiveTab] = useState<'all' | 'notice' | 'other'>('all')
|
const [activeTab, setActiveTab] = useState<'all' | 'notice' | 'other'>('all')
|
||||||
|
const { markRead } = useMessageActions()
|
||||||
|
|
||||||
// 根据选中的标签过滤卡片
|
const messageType = getMessageTypeByTab(activeTab)
|
||||||
const filteredCards = messageCards.filter((card) => {
|
const { messages, loading, loadingMore, error, refetch, loadMore, hasMore, execute } = useMessages({
|
||||||
if (activeTab === 'all') return true
|
limit: 20,
|
||||||
return card.type === activeTab
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<RNStatusBar barStyle="light-content" />
|
<RNStatusBar barStyle="light-content" />
|
||||||
|
|
||||||
{/* 固定在顶部的标签选择器 */}
|
|
||||||
<View style={styles.segment}>
|
<View style={styles.segment}>
|
||||||
<Pressable onPress={() => setActiveTab('all')}>
|
<Pressable onPress={() => setActiveTab('all')}>
|
||||||
{activeTab === 'all' ? (
|
{activeTab === 'all' ? (
|
||||||
@@ -156,45 +267,57 @@ export default function MessageScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 可滚动的消息卡片列表 */}
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
scrollEventThrottle={400}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={loading && messages.length > 0}
|
||||||
|
onRefresh={refetch}
|
||||||
|
tintColor="#FFFFFF"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* 消息卡片列表 */}
|
{messages.length > 0 ? (
|
||||||
{filteredCards.length > 0 ? (
|
<>
|
||||||
filteredCards.map((card) => (
|
{messages.map((message) => (
|
||||||
<View key={card.id} style={styles.cardContainer}>
|
<Pressable
|
||||||
{/* 新消息绿色指示点 */}
|
key={message.id}
|
||||||
{card.isNew && (
|
onPress={() => handleMessagePress(message)}
|
||||||
|
>
|
||||||
|
<View style={styles.cardContainer}>
|
||||||
|
{!message.isRead && (
|
||||||
<View style={styles.newMessageDotContainer}>
|
<View style={styles.newMessageDotContainer}>
|
||||||
<View style={styles.newMessageDot} />
|
<View style={styles.newMessageDot} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<Text style={styles.cardTitle}>
|
<Text style={styles.cardTitle}>
|
||||||
{card.title}
|
{message.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.cardSubtitle} numberOfLines={2}>
|
<Text style={styles.cardSubtitle} numberOfLines={2}>
|
||||||
{card.subtitle}
|
{message.content}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.cardBody}>
|
<View style={styles.cardBody}>
|
||||||
<Text style={styles.cardBodyText}>
|
<Text style={styles.cardBodyText}>
|
||||||
{/* TODO:这里是图片 */}
|
{message.data || ''}
|
||||||
{card.body}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.cardTime}>
|
<Text style={styles.cardTime}>
|
||||||
{card.time}
|
{formatTime(message.createdAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
))
|
</Pressable>
|
||||||
|
))}
|
||||||
|
{loadingMore && <PaginationLoader color="#FFFFFF" />}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
{/* <NoNewsIcon /> */}
|
<Text style={styles.emptyIcon}>💭</Text>
|
||||||
💭
|
|
||||||
<Text style={styles.emptyText}>{t('message.noMessages')}</Text>
|
<Text style={styles.emptyText}>{t('message.noMessages')}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -216,12 +339,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
},
|
},
|
||||||
appMiniIcon: {
|
|
||||||
width: 28,
|
|
||||||
height: 18,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: '#FF66AA',
|
|
||||||
},
|
|
||||||
segment: {
|
segment: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
@@ -230,7 +347,6 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#090A0B',
|
backgroundColor: '#090A0B',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
},
|
},
|
||||||
|
|
||||||
segmentText: {
|
segmentText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
@@ -252,20 +368,13 @@ const styles = StyleSheet.create({
|
|||||||
segmentTextActiveText: {
|
segmentTextActiveText: {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
statusDot: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: '#00FF66',
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
cardContainer: {
|
cardContainer: {
|
||||||
backgroundColor: '#16181B',
|
backgroundColor: '#16181B',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
marginHorizontal: 4,
|
marginHorizontal: 4,
|
||||||
position: 'relative', // 用于定位新消息指示点
|
position: 'relative',
|
||||||
},
|
},
|
||||||
newMessageDotContainer: {
|
newMessageDotContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -283,7 +392,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 6,
|
width: 6,
|
||||||
height: 6,
|
height: 6,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: '#00FF66', // 绿色
|
backgroundColor: '#00FF66',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
@@ -302,6 +411,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
height: 100,
|
height: 100,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
|
padding: 8,
|
||||||
},
|
},
|
||||||
cardBodyText: {
|
cardBodyText: {
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
@@ -318,6 +428,9 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingTop: 200,
|
paddingTop: 200,
|
||||||
},
|
},
|
||||||
|
emptyIcon: {
|
||||||
|
fontSize: 48,
|
||||||
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
color: '#8A8A8A',
|
color: '#8A8A8A',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import { handleError } from './use-error'
|
|||||||
interface ListMessagesParams {
|
interface ListMessagesParams {
|
||||||
page?: number
|
page?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
|
type?: ('SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING')[]
|
||||||
|
isRead?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PARAMS = {
|
const DEFAULT_PARAMS = {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
|
orderBy: 'createdAt',
|
||||||
|
order: 'desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMessages = (initialParams?: ListMessagesParams) => {
|
export const useMessages = (initialParams?: ListMessagesParams) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user