feat: 首页更新
This commit is contained in:
239
pages/HomeScreen/components/NoticeBar/index.tsx
Normal file
239
pages/HomeScreen/components/NoticeBar/index.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 公告栏组件 - 增强版
|
||||
*
|
||||
* 基于 xinyong-web 的 NoticeBar 组件重建
|
||||
* 功能包括:
|
||||
* - 顶部通知栏(自动轮播)
|
||||
* - 消息和客服按钮
|
||||
* - 公告详情弹窗(支持文本、图片、链接)
|
||||
* - 主题适配(light/dark/orange)
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
Modal,
|
||||
Linking,
|
||||
Image as RNImage,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
// import { Button } from 'react-native-paper';
|
||||
import { useThemeColors, useColorScheme } from '@/theme';
|
||||
// import { mockNotices } from '@/services/mockHomeService';
|
||||
// import type { Notice } from '@/types/home';
|
||||
import useMsgStore, { useUnreadMessageTotal } from '@/stores/msgStore';
|
||||
import { filter, map } from 'lodash-es';
|
||||
import { styles } from './styles';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
interface NoticeBarProps {
|
||||
onNoticePress?: (notice: Record<string, any>) => void;
|
||||
onMessagePress?: () => void;
|
||||
onServicePress?: () => void;
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公告栏组件 - 增强版
|
||||
*/
|
||||
export default function NoticeBar({
|
||||
onNoticePress,
|
||||
onMessagePress,
|
||||
onServicePress,
|
||||
unreadCount = 0,
|
||||
}: NoticeBarProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const s = styles[colorScheme];
|
||||
const themeColors = useThemeColors();
|
||||
const [currentNotice, setCurrentNotice] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
const animatedValue = useRef(new Animated.Value(1)).current;
|
||||
const { notices } = useMsgStore();
|
||||
const unreadMessageTotal = useUnreadMessageTotal();
|
||||
|
||||
const noticeList = useMemo(() => {
|
||||
return map(
|
||||
filter(notices, (item) => item.message_id == 13 && !!item.content),
|
||||
(notice) => {
|
||||
return {
|
||||
...notice,
|
||||
content: notice.content.replace(
|
||||
/<(\/?)(p|span|div|br|strong|em|b|i|u|s|ul|li|ol|h1|h2|h3|h4|h5|h6|a|img|table|tr|td|th|tbody|thead|tfoot|style|script)[^>]*>/gi,
|
||||
''
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [notices]);
|
||||
|
||||
// 自动切换公告
|
||||
useEffect(() => {
|
||||
if (noticeList.length === 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCurrentNotice((prev) => (prev + 1) % noticeList.length);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [noticeList.length]);
|
||||
|
||||
// 处理关闭公告
|
||||
const handleClose = useCallback(() => {
|
||||
Animated.timing(animatedValue, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setVisible(false);
|
||||
});
|
||||
}, [animatedValue]);
|
||||
|
||||
// 处理公告点击 - 打开详情弹窗
|
||||
const handleNoticePress = useCallback(() => {
|
||||
if (noticeList.length > 0) {
|
||||
setShowDetailModal(true);
|
||||
onNoticePress?.(noticeList[currentNotice]);
|
||||
}
|
||||
}, [noticeList, currentNotice, onNoticePress]);
|
||||
|
||||
// 处理链接点击
|
||||
const handleLinkPress = useCallback((url: string) => {
|
||||
Linking.openURL(url).catch((err) => {
|
||||
console.error('打开链接失败:', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!visible || noticeList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentNoticeData = noticeList[currentNotice] as Record<string, any>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 顶部通知栏 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
s.container,
|
||||
{
|
||||
opacity: animatedValue,
|
||||
transform: [
|
||||
{
|
||||
scaleY: animatedValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name="volume-medium-outline"
|
||||
size={24}
|
||||
color={themeColors.tint}
|
||||
style={s.volumeIcon}
|
||||
/>
|
||||
<TouchableOpacity style={{ flex: 1 }} onPress={handleNoticePress} activeOpacity={0.7}>
|
||||
<Text style={s.content} numberOfLines={1}>
|
||||
{currentNoticeData.content}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 右侧按钮组 */}
|
||||
<View style={s.rightButtonsContainer}>
|
||||
<TouchableOpacity style={s.actionButton} onPress={onMessagePress} activeOpacity={0.7}>
|
||||
<Ionicons name="volume-medium-outline" size={18} color={themeColors.tint} />
|
||||
{/*{unreadCount > 0 && (*/}
|
||||
{/* <Text style={[s.actionButtonText, { marginLeft: 4 }]}>{unreadCount}</Text>*/}
|
||||
{/*)}*/}
|
||||
<Text style={[s.actionButtonText, { marginLeft: 4 }]}>消息</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={s.actionButton} onPress={onServicePress} activeOpacity={0.7}>
|
||||
<Ionicons name="headset" size={18} color={themeColors.tint} />
|
||||
<Text style={s.actionButtonText}>客服</Text>
|
||||
</TouchableOpacity>
|
||||
{/*<TouchableOpacity style={s.closeButton} onPress={handleClose}>*/}
|
||||
{/* <Text style={s.closeText}>✕</Text>*/}
|
||||
{/*</TouchableOpacity>*/}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* 公告详情弹窗 */}
|
||||
<Modal
|
||||
visible={showDetailModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowDetailModal(false)}
|
||||
>
|
||||
<View style={s.modalOverlay}>
|
||||
<View style={s.modalContent}>
|
||||
{/* 弹窗头部 */}
|
||||
<View style={s.modalHeader}>
|
||||
<Text style={s.modalTitle}>公告详情</Text>
|
||||
<TouchableOpacity
|
||||
style={s.modalCloseButton}
|
||||
onPress={() => setShowDetailModal(false)}
|
||||
>
|
||||
<Text style={s.modalCloseText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 弹窗内容 */}
|
||||
<ScrollView style={s.modalBody}>
|
||||
{currentNoticeData.title && (
|
||||
<Text style={s.noticeTitle}>{currentNoticeData.title}</Text>
|
||||
)}
|
||||
{currentNoticeData.create_time && (
|
||||
<Text style={s.noticeDate}>
|
||||
{currentNoticeData.formatDate || currentNoticeData.create_time}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 根据内容类型渲染不同的内容 */}
|
||||
{currentNoticeData.content_type === 1 && (
|
||||
<Text style={s.noticeTextContent}>{currentNoticeData.content}</Text>
|
||||
)}
|
||||
{currentNoticeData.content_type === 2 && currentNoticeData.content && (
|
||||
<View style={s.noticeImageContainer}>
|
||||
{imageLoading && (
|
||||
<ActivityIndicator
|
||||
size="large"
|
||||
color={themeColors.primary}
|
||||
style={s.imageLoader}
|
||||
/>
|
||||
)}
|
||||
<RNImage
|
||||
source={{ uri: currentNoticeData.content }}
|
||||
style={s.noticeImage}
|
||||
resizeMode="contain"
|
||||
onLoadStart={() => {
|
||||
setImageLoading(true);
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
setImageLoading(false);
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('图片加载失败:', error);
|
||||
setImageLoading(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{currentNoticeData.content_type === 3 && (
|
||||
<TouchableOpacity onPress={() => handleLinkPress(currentNoticeData.content)}>
|
||||
<Text style={s.noticeLink}>{currentNoticeData.content}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
156
pages/HomeScreen/components/NoticeBar/styles.ts
Normal file
156
pages/HomeScreen/components/NoticeBar/styles.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Dimensions } from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
export const styles = createThemeStyles((colors) => ({
|
||||
// 顶部通知栏
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginHorizontal: 0,
|
||||
marginBottom: 0,
|
||||
borderRadius: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
volumeIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginRight: 8,
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
closeButton: {
|
||||
marginLeft: 8,
|
||||
padding: 4,
|
||||
},
|
||||
closeText: {
|
||||
fontSize: 16,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
// 右侧按钮组
|
||||
rightButtonsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 8,
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 6,
|
||||
// paddingVertical: 6,
|
||||
marginLeft: 8,
|
||||
borderRadius: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
height: 20,
|
||||
// backgroundColor: colors.primary,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 10,
|
||||
color: colors.text,
|
||||
fontWeight: '500',
|
||||
marginLeft: 4,
|
||||
},
|
||||
// 弹窗样式
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
width: width * 0.9,
|
||||
maxHeight: height * 0.8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: colors.text,
|
||||
},
|
||||
modalCloseButton: {
|
||||
padding: 8,
|
||||
},
|
||||
modalCloseText: {
|
||||
fontSize: 20,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
modalBody: {
|
||||
padding: 16,
|
||||
},
|
||||
noticeTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
},
|
||||
noticeDate: {
|
||||
fontSize: 12,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 12,
|
||||
},
|
||||
noticeTextContent: {
|
||||
fontSize: 13,
|
||||
color: colors.text,
|
||||
lineHeight: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
noticeImageContainer: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
noticeImage: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
borderRadius: 8,
|
||||
},
|
||||
imageLoader: {
|
||||
position: 'absolute',
|
||||
},
|
||||
noticeLink: {
|
||||
fontSize: 13,
|
||||
color: colors.primary,
|
||||
textDecorationLine: 'underline',
|
||||
marginBottom: 12,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user