diff --git a/app.json b/app.json index 2511aa4..0037e51 100644 --- a/app.json +++ b/app.json @@ -5,7 +5,7 @@ "version": "1.0.11", "orientation": "default", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "automatic", "scheme": "carrotbbs", "splash": { "image": "./assets/splash-icon.png", diff --git a/app/(app)/(tabs)/_layout.tsx b/app/(app)/(tabs)/_layout.tsx index 0150ea8..f555b9b 100644 --- a/app/(app)/(tabs)/_layout.tsx +++ b/app/(app)/(tabs)/_layout.tsx @@ -4,7 +4,7 @@ import { Tabs, usePathname } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { colors, shadows } from '../../../src/theme'; +import { useAppColors, shadows } from '../../../src/theme'; import { BREAKPOINTS } from '../../../src/hooks/useResponsive'; import { useHomeTabBarVisibilityStore, useTotalUnreadCount } from '../../../src/stores'; @@ -13,6 +13,7 @@ const TAB_BAR_FLOATING_MARGIN = 12; const TAB_BAR_MARGIN = 20; export default function TabsLayout() { + const colors = useAppColors(); const { width } = useWindowDimensions(); const insets = useSafeAreaInsets(); const pathname = usePathname(); @@ -50,7 +51,7 @@ export default function TabsLayout() { }; } return visibleStyle; - }, [hideTabBar, insets.bottom, isHomeStackRoute, scrollHideTabBar]); + }, [hideTabBar, insets.bottom, isHomeStackRoute, scrollHideTabBar, colors]); return ( ({ + headerStyle: { backgroundColor: colors.background.paper }, + headerTintColor: colors.text.primary, + headerTitleStyle: { fontWeight: '600' as const }, + headerShadowVisible: false, + headerBackTitle: '', + }), + [colors] + ); return ( { + void SystemUI.setBackgroundColorAsync(colors.background.default); + if (Platform.OS === 'web' && typeof document !== 'undefined') { + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) { + meta.setAttribute('content', colors.background.default); + } + } + }, [colors.background.default]); + return null; +} + function SessionGate({ children }: { children: React.ReactNode }) { const [ready, setReady] = useState(false); const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser); + const colors = useAppColors(); useEffect(() => { fetchCurrentUser().finally(() => setReady(true)); @@ -57,7 +77,14 @@ function SessionGate({ children }: { children: React.ReactNode }) { if (!ready) { return ( - + ); @@ -110,59 +137,69 @@ function NotificationBootstrap() { return null; } -export default function RootLayout() { +function ThemedStack() { const router = useRouter(); + const colors = useAppColors(); + const resolved = useResolvedColorScheme(); + return ( + <> + + + + + + + + + router.back()} />, + }} + /> + router.back()} />, + }} + /> + + + + ); +} + +function ThemedProviders({ children }: { children: React.ReactNode }) { + const theme = usePaperThemeFromStore(); + return {children}; +} + +export default function RootLayout() { return ( - + - - - - - - - - router.back()} />, - }} - /> - router.back()} />, - }} - /> - - + + - + ); } - -const gateStyles = StyleSheet.create({ - loading: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: colors.background.default, - }, -}); diff --git a/src/app-navigation/AppDesktopShell.tsx b/src/app-navigation/AppDesktopShell.tsx index e9e5d9e..0d79850 100644 --- a/src/app-navigation/AppDesktopShell.tsx +++ b/src/app-navigation/AppDesktopShell.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { View, StyleSheet, @@ -11,7 +11,7 @@ import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context' import { MaterialCommunityIcons } from '@expo/vector-icons'; import { usePathname, useRouter } from 'expo-router'; -import { colors, shadows } from '../theme'; +import { useAppColors, shadows, type AppColors } from '../theme'; import { useTotalUnreadCount } from '../stores'; import { AppRouteStack } from './AppRouteStack'; @@ -36,7 +36,114 @@ function pathToTab(pathname: string): TabName { return 'HomeTab'; } +function createDesktopShellStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', + backgroundColor: colors.background.default, + }, + sidebar: { + backgroundColor: colors.background.paper, + borderRightWidth: 1, + borderRightColor: colors.divider, + flexDirection: 'column', + ...shadows.md, + }, + sidebarHeader: { + paddingHorizontal: 16, + paddingVertical: 20, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + }, + logoText: { + fontSize: 18, + fontWeight: '700', + color: colors.primary.main, + marginLeft: 8, + }, + sidebarContent: { + flex: 1, + paddingTop: 8, + }, + sidebarItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + marginHorizontal: 8, + marginVertical: 4, + borderRadius: 12, + }, + sidebarItemActive: { + backgroundColor: `${colors.primary.main}15`, + }, + sidebarItemCollapsed: { + justifyContent: 'center', + paddingHorizontal: 0, + }, + sidebarIconContainer: { + position: 'relative', + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 12, + }, + sidebarLabel: { + fontSize: 15, + fontWeight: '500', + color: colors.text.secondary, + marginLeft: 12, + flex: 1, + }, + sidebarLabelActive: { + color: colors.primary.main, + fontWeight: '600', + }, + collapseButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + paddingHorizontal: 16, + paddingVertical: 12, + borderTopWidth: 1, + borderTopColor: colors.divider, + }, + collapseButtonCollapsed: { + justifyContent: 'center', + paddingHorizontal: 0, + }, + badge: { + position: 'absolute', + top: 2, + right: 2, + backgroundColor: colors.error.main, + borderRadius: 10, + minWidth: 18, + height: 18, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 4, + }, + badgeText: { + color: colors.primary.contrast, + fontSize: 10, + fontWeight: '600', + }, + mainContent: { + flex: 1, + backgroundColor: colors.background.default, + }, + }); +} + export function AppDesktopShell() { + const colors = useAppColors(); + const styles = useMemo(() => createDesktopShellStyles(colors), [colors]); const insets = useSafeAreaInsets(); const router = useRouter(); const pathname = usePathname(); @@ -134,106 +241,3 @@ export function AppDesktopShell() { ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'row', - backgroundColor: colors.background.default, - }, - sidebar: { - backgroundColor: colors.background.paper, - borderRightWidth: 1, - borderRightColor: colors.divider, - flexDirection: 'column', - ...shadows.md, - }, - sidebarHeader: { - paddingHorizontal: 16, - paddingVertical: 20, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - }, - logoText: { - fontSize: 18, - fontWeight: '700', - color: colors.primary.main, - marginLeft: 8, - }, - sidebarContent: { - flex: 1, - paddingTop: 8, - }, - sidebarItem: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 12, - marginHorizontal: 8, - marginVertical: 4, - borderRadius: 12, - }, - sidebarItemActive: { - backgroundColor: `${colors.primary.main}15`, - }, - sidebarItemCollapsed: { - justifyContent: 'center', - paddingHorizontal: 0, - }, - sidebarIconContainer: { - position: 'relative', - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - borderRadius: 12, - }, - sidebarLabel: { - fontSize: 15, - fontWeight: '500', - color: colors.text.secondary, - marginLeft: 12, - flex: 1, - }, - sidebarLabelActive: { - color: colors.primary.main, - fontWeight: '600', - }, - collapseButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-end', - paddingHorizontal: 16, - paddingVertical: 12, - borderTopWidth: 1, - borderTopColor: colors.divider, - }, - collapseButtonCollapsed: { - justifyContent: 'center', - paddingHorizontal: 0, - }, - badge: { - position: 'absolute', - top: 2, - right: 2, - backgroundColor: colors.error.main, - borderRadius: 10, - minWidth: 18, - height: 18, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 4, - }, - badgeText: { - color: colors.primary.contrast, - fontSize: 10, - fontWeight: '600', - }, - mainContent: { - flex: 1, - backgroundColor: colors.background.default, - }, -}); diff --git a/src/components/business/CommentItem.tsx b/src/components/business/CommentItem.tsx index 555605c..7e53802 100644 --- a/src/components/business/CommentItem.tsx +++ b/src/components/business/CommentItem.tsx @@ -3,12 +3,12 @@ * 支持嵌套回复显示、楼层号、身份标识、删除评论、图片显示 */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; -import { colors, spacing, borderRadius, fontSizes } from '../../theme'; +import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { Comment, CommentImage } from '../../types'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; @@ -31,6 +31,174 @@ interface CommentItemProps { currentUserId?: string; // 当前用户ID,用于判断子评论作者 } +function createCommentItemStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + paddingTop: spacing.md, + paddingBottom: spacing.md, + paddingHorizontal: spacing.md, + backgroundColor: colors.background.paper, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.divider, + }, + content: { + flex: 1, + marginLeft: spacing.sm, + minWidth: 0, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.xs, + gap: spacing.xs, + }, + userInfo: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + flex: 1, + minWidth: 0, + }, + metaDot: { + fontSize: fontSizes.xs, + color: colors.text.hint, + marginHorizontal: 2, + }, + username: { + fontWeight: '600', + fontSize: fontSizes.sm, + color: colors.text.primary, + marginRight: spacing.xs, + }, + badge: { + paddingHorizontal: 4, + paddingVertical: 1, + borderRadius: 2, + marginRight: spacing.xs, + }, + smallBadge: { + paddingHorizontal: 2, + paddingVertical: 0, + }, + authorBadge: { + backgroundColor: colors.primary.main, + }, + adminBadge: { + backgroundColor: colors.error.main, + }, + badgeText: { + color: colors.text.inverse, + fontSize: fontSizes.xs, + fontWeight: '600', + }, + smallBadgeText: { + color: colors.text.inverse, + fontSize: 9, + fontWeight: '600', + }, + timeText: { + fontSize: fontSizes.xs, + flexShrink: 0, + }, + floorPlain: { + fontSize: fontSizes.xs, + flexShrink: 0, + }, + replyReference: { + marginBottom: spacing.xs, + }, + commentContent: { + marginBottom: spacing.xs, + }, + text: { + lineHeight: 22, + fontSize: fontSizes.md, + color: colors.text.primary, + }, + actions: { + flexDirection: 'row', + alignItems: 'center', + marginTop: spacing.xs, + flexWrap: 'wrap', + }, + actionButton: { + flexDirection: 'row', + alignItems: 'center', + marginRight: spacing.lg, + paddingVertical: 4, + paddingRight: spacing.xs, + }, + actionText: { + marginLeft: 4, + fontSize: fontSizes.xs, + }, + subRepliesContainer: { + marginTop: spacing.sm, + marginLeft: 0, + paddingLeft: spacing.sm, + paddingVertical: spacing.xs, + borderLeftWidth: 2, + borderLeftColor: colors.divider, + backgroundColor: colors.background.default, + }, + subReplyItem: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: spacing.md, + }, + subReplyItemLast: { + marginBottom: 0, + }, + subReplyBody: { + fontSize: fontSizes.sm, + lineHeight: 20, + marginTop: 2, + }, + subReplyContent: { + flex: 1, + marginLeft: spacing.xs, + }, + subReplyHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 2, + }, + subReplyAuthor: { + fontWeight: '600', + color: colors.text.primary, + marginRight: spacing.xs, + }, + moreRepliesButton: { + marginTop: spacing.xs, + paddingVertical: spacing.xs, + }, + replyToText: { + fontSize: fontSizes.xs, + }, + replyToName: { + fontSize: fontSizes.xs, + fontWeight: '500', + }, + subReplyActions: { + flexDirection: 'row', + alignItems: 'center', + marginTop: spacing.xs, + }, + subActionButton: { + flexDirection: 'row', + alignItems: 'center', + marginRight: spacing.sm, + paddingVertical: 2, + }, + subActionText: { + marginLeft: 2, + fontSize: fontSizes.xs, + }, + }); +} + const CommentItem: React.FC = ({ comment, onUserPress, @@ -47,6 +215,8 @@ const CommentItem: React.FC = ({ onImagePress, currentUserId, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createCommentItemStyles(colors), [colors]); const [isDeleting, setIsDeleting] = useState(false); // 格式化时间 const formatTime = (dateString: string): string => { @@ -471,170 +641,4 @@ const CommentItem: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - paddingTop: spacing.md, - paddingBottom: spacing.md, - paddingHorizontal: spacing.md, - backgroundColor: colors.background.paper, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.divider, - }, - content: { - flex: 1, - marginLeft: spacing.sm, - minWidth: 0, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.xs, - gap: spacing.xs, - }, - userInfo: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - flex: 1, - minWidth: 0, - }, - metaDot: { - fontSize: fontSizes.xs, - color: colors.text.hint, - marginHorizontal: 2, - }, - username: { - fontWeight: '600', - fontSize: fontSizes.sm, - color: colors.text.primary, - marginRight: spacing.xs, - }, - badge: { - paddingHorizontal: 4, - paddingVertical: 1, - borderRadius: 2, - marginRight: spacing.xs, - }, - smallBadge: { - paddingHorizontal: 2, - paddingVertical: 0, - }, - authorBadge: { - backgroundColor: colors.primary.main, - }, - adminBadge: { - backgroundColor: colors.error.main, - }, - badgeText: { - color: colors.text.inverse, - fontSize: fontSizes.xs, - fontWeight: '600', - }, - smallBadgeText: { - color: colors.text.inverse, - fontSize: 9, - fontWeight: '600', - }, - timeText: { - fontSize: fontSizes.xs, - flexShrink: 0, - }, - floorPlain: { - fontSize: fontSizes.xs, - flexShrink: 0, - }, - replyReference: { - marginBottom: spacing.xs, - }, - commentContent: { - marginBottom: spacing.xs, - }, - text: { - lineHeight: 22, - fontSize: fontSizes.md, - color: colors.text.primary, - }, - actions: { - flexDirection: 'row', - alignItems: 'center', - marginTop: spacing.xs, - flexWrap: 'wrap', - }, - actionButton: { - flexDirection: 'row', - alignItems: 'center', - marginRight: spacing.lg, - paddingVertical: 4, - paddingRight: spacing.xs, - }, - actionText: { - marginLeft: 4, - fontSize: fontSizes.xs, - }, - subRepliesContainer: { - marginTop: spacing.sm, - marginLeft: 0, - paddingLeft: spacing.sm, - paddingVertical: spacing.xs, - borderLeftWidth: 2, - borderLeftColor: colors.divider, - backgroundColor: colors.background.default, - }, - subReplyItem: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: spacing.md, - }, - subReplyItemLast: { - marginBottom: 0, - }, - subReplyBody: { - fontSize: fontSizes.sm, - lineHeight: 20, - marginTop: 2, - }, - subReplyContent: { - flex: 1, - marginLeft: spacing.xs, - }, - subReplyHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 2, - }, - subReplyAuthor: { - fontWeight: '600', - color: colors.text.primary, - marginRight: spacing.xs, - }, - moreRepliesButton: { - marginTop: spacing.xs, - paddingVertical: spacing.xs, - }, - replyToText: { - fontSize: fontSizes.xs, - }, - replyToName: { - fontSize: fontSizes.xs, - fontWeight: '500', - }, - subReplyActions: { - flexDirection: 'row', - alignItems: 'center', - marginTop: spacing.xs, - }, - subActionButton: { - flexDirection: 'row', - alignItems: 'center', - marginRight: spacing.sm, - paddingVertical: 2, - }, - subActionText: { - marginLeft: 2, - fontSize: fontSizes.xs, - }, -}); - export default CommentItem; diff --git a/src/components/business/PostCard/PostCard.tsx b/src/components/business/PostCard/PostCard.tsx index 6507764..e4ecaf1 100644 --- a/src/components/business/PostCard/PostCard.tsx +++ b/src/components/business/PostCard/PostCard.tsx @@ -19,7 +19,7 @@ import { PostCardProps, PostCardAction } from './types'; import { ImageGridItem, SmartImage } from '../../common'; import Text from '../../common/Text'; import Avatar from '../../common/Avatar'; -import { colors, spacing, borderRadius, fontSizes } from '../../../theme'; +import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../../theme'; import { getPreviewImageUrl } from '../../../utils/imageHelper'; import PostImages from './components/PostImages'; import { useResponsive } from '../../../hooks/useResponsive'; @@ -113,6 +113,270 @@ function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolea return true; } +function createPostCardStyles(colors: AppColors) { + return StyleSheet.create({ + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.divider, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.sm, + }, + authorWrap: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + minWidth: 0, + }, + authorMeta: { + marginLeft: spacing.sm, + flex: 1, + minWidth: 0, + }, + authorName: { + color: colors.text.primary, + fontWeight: '600', + fontSize: fontSizes.md, + }, + timeText: { + color: colors.text.hint, + fontSize: fontSizes.xs, + marginTop: 2, + }, + metaRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginTop: 2, + flexWrap: 'wrap', + }, + channelTag: { + flexDirection: 'row', + alignItems: 'center', + maxWidth: 140, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: borderRadius.sm, + backgroundColor: `${colors.primary.main}12`, + gap: 4, + }, + channelTagText: { + fontSize: fontSizes.xs, + color: colors.primary.main, + fontWeight: '600', + flexShrink: 1, + }, + pinnedTag: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: `${colors.warning.light}22`, + borderRadius: borderRadius.sm, + paddingHorizontal: 4, + paddingVertical: 1, + gap: 2, + }, + pinnedText: { + color: colors.warning.main, + fontSize: fontSizes.xs, + }, + badgeText: { + color: colors.primary.main, + fontSize: fontSizes.xs, + fontWeight: '600', + }, + deleteButton: { + padding: spacing.xs, + marginLeft: spacing.sm, + }, + title: { + color: colors.text.primary, + fontSize: fontSizes.lg, + fontWeight: '600', + marginBottom: spacing.xs, + }, + content: { + color: colors.text.secondary, + fontSize: fontSizes.md, + marginBottom: spacing.xs, + lineHeight: fontSizes.md * 1.45, + }, + expandBtn: { + marginBottom: spacing.sm, + }, + expandText: { + color: colors.primary.main, + fontSize: fontSizes.sm, + }, + topCommentBox: { + backgroundColor: colors.background.default, + borderRadius: borderRadius.sm, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + marginBottom: spacing.sm, + flexDirection: 'row', + alignItems: 'center', + }, + topCommentAuthor: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + fontWeight: '600', + marginRight: 4, + }, + topCommentText: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + flex: 1, + }, + gridCover: { + width: '100%', + aspectRatio: 0.78, + backgroundColor: colors.background.default, + }, + actions: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + actionsLeading: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + minWidth: 0, + marginRight: spacing.sm, + gap: spacing.sm, + }, + channelTagAfterViews: { + maxWidth: 130, + flexShrink: 1, + }, + viewsWrap: { + flexDirection: 'row', + alignItems: 'center', + flexShrink: 0, + }, + viewsText: { + color: colors.text.hint, + fontSize: fontSizes.sm, + marginLeft: 4, + }, + actionButtons: { + flexDirection: 'row', + alignItems: 'center', + }, + actionBtn: { + flexDirection: 'row', + alignItems: 'center', + marginLeft: spacing.md, + }, + actionText: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + marginLeft: 4, + }, + activeActionText: { + color: colors.error.main, + }, + activeActionTextMerged: { + color: colors.error.main, + fontSize: fontSizes.sm, + marginLeft: 4, + }, + gridRootCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + overflow: 'hidden', + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.divider, + }, + gridNoImagePreview: { + backgroundColor: colors.background.default, + margin: 6, + borderRadius: borderRadius.md, + minHeight: 120, + padding: 8, + }, + gridNoImageText: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + lineHeight: 20, + }, + gridVoteTag: { + position: 'absolute', + top: 8, + right: 8, + backgroundColor: `${colors.primary.main}CC`, + borderRadius: borderRadius.full, + paddingHorizontal: 8, + paddingVertical: 4, + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + gridVoteTagText: { + color: colors.primary.contrast, + fontSize: 10, + fontWeight: '600', + }, + gridTitleMain: { + color: colors.text.primary, + fontSize: fontSizes.md, + lineHeight: 20, + paddingHorizontal: 8, + paddingTop: 8, + minHeight: 44, + }, + gridChannelRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingHorizontal: 8, + paddingTop: 4, + paddingBottom: 2, + }, + gridChannelText: { + fontSize: fontSizes.xs, + color: colors.text.secondary, + fontWeight: '600', + flex: 1, + minWidth: 0, + }, + gridFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 8, + paddingVertical: 10, + }, + gridUserArea: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + minWidth: 0, + }, + gridUsername: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + marginLeft: 6, + flex: 1, + }, + gridLikeArea: { + flexDirection: 'row', + alignItems: 'center', + }, + gridLikeCount: { + color: colors.text.secondary, + fontSize: fontSizes.sm, + marginLeft: 4, + }, + }); +} + /** * PostCard 主组件 * 仅支持新 API @@ -129,6 +393,8 @@ const PostCardInner: React.FC = (normalizedProps) => { style, } = normalizedProps; + const colors = useAppColors(); + const styles = useMemo(() => createPostCardStyles(colors), [colors]); const [isExpanded, setIsExpanded] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -444,266 +710,4 @@ PostCardInner.displayName = 'PostCard'; const PostCard = memo(PostCardInner, arePostCardPropsEqual); -const styles = StyleSheet.create({ - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.md, - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.divider, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: spacing.sm, - }, - authorWrap: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - minWidth: 0, - }, - authorMeta: { - marginLeft: spacing.sm, - flex: 1, - minWidth: 0, - }, - authorName: { - color: colors.text.primary, - fontWeight: '600', - fontSize: fontSizes.md, - }, - timeText: { - color: colors.text.hint, - fontSize: fontSizes.xs, - marginTop: 2, - }, - metaRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - marginTop: 2, - flexWrap: 'wrap', - }, - channelTag: { - flexDirection: 'row', - alignItems: 'center', - maxWidth: 140, - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: borderRadius.sm, - backgroundColor: `${colors.primary.main}12`, - gap: 4, - }, - channelTagText: { - fontSize: fontSizes.xs, - color: colors.primary.main, - fontWeight: '600', - flexShrink: 1, - }, - pinnedTag: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: `${colors.warning.light}22`, - borderRadius: borderRadius.sm, - paddingHorizontal: 4, - paddingVertical: 1, - gap: 2, - }, - pinnedText: { - color: colors.warning.main, - fontSize: fontSizes.xs, - }, - badgeText: { - color: colors.primary.main, - fontSize: fontSizes.xs, - fontWeight: '600', - }, - deleteButton: { - padding: spacing.xs, - marginLeft: spacing.sm, - }, - title: { - color: colors.text.primary, - fontSize: fontSizes.lg, - fontWeight: '600', - marginBottom: spacing.xs, - }, - content: { - color: colors.text.secondary, - fontSize: fontSizes.md, - marginBottom: spacing.xs, - lineHeight: fontSizes.md * 1.45, - }, - expandBtn: { - marginBottom: spacing.sm, - }, - expandText: { - color: colors.primary.main, - fontSize: fontSizes.sm, - }, - topCommentBox: { - backgroundColor: colors.background.default, - borderRadius: borderRadius.sm, - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - marginBottom: spacing.sm, - flexDirection: 'row', - alignItems: 'center', - }, - topCommentAuthor: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - fontWeight: '600', - marginRight: 4, - }, - topCommentText: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - flex: 1, - }, - gridCover: { - width: '100%', - aspectRatio: 0.78, - backgroundColor: colors.background.default, - }, - actions: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - actionsLeading: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - minWidth: 0, - marginRight: spacing.sm, - gap: spacing.sm, - }, - channelTagAfterViews: { - maxWidth: 130, - flexShrink: 1, - }, - viewsWrap: { - flexDirection: 'row', - alignItems: 'center', - flexShrink: 0, - }, - viewsText: { - color: colors.text.hint, - fontSize: fontSizes.sm, - marginLeft: 4, - }, - actionButtons: { - flexDirection: 'row', - alignItems: 'center', - }, - actionBtn: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: spacing.md, - }, - actionText: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - marginLeft: 4, - }, - activeActionText: { - color: colors.error.main, - }, - activeActionTextMerged: { - color: colors.error.main, - fontSize: fontSizes.sm, - marginLeft: 4, - }, - gridRootCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - overflow: 'hidden', - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.divider, - }, - gridNoImagePreview: { - backgroundColor: colors.background.default, - margin: 6, - borderRadius: borderRadius.md, - minHeight: 120, - padding: 8, - }, - gridNoImageText: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - lineHeight: 20, - }, - gridVoteTag: { - position: 'absolute', - top: 8, - right: 8, - backgroundColor: `${colors.primary.main}CC`, - borderRadius: borderRadius.full, - paddingHorizontal: 8, - paddingVertical: 4, - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - gridVoteTagText: { - color: colors.primary.contrast, - fontSize: 10, - fontWeight: '600', - }, - gridTitleMain: { - color: colors.text.primary, - fontSize: fontSizes.md, - lineHeight: 20, - paddingHorizontal: 8, - paddingTop: 8, - minHeight: 44, - }, - gridChannelRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - paddingHorizontal: 8, - paddingTop: 4, - paddingBottom: 2, - }, - gridChannelText: { - fontSize: fontSizes.xs, - color: colors.text.secondary, - fontWeight: '600', - flex: 1, - minWidth: 0, - }, - gridFooter: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 8, - paddingVertical: 10, - }, - gridUserArea: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - minWidth: 0, - }, - gridUsername: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - marginLeft: 6, - flex: 1, - }, - gridLikeArea: { - flexDirection: 'row', - alignItems: 'center', - }, - gridLikeCount: { - color: colors.text.secondary, - fontSize: fontSizes.sm, - marginLeft: 4, - }, -}); - export default PostCard; \ No newline at end of file diff --git a/src/components/business/PostCard/components/PostImages.tsx b/src/components/business/PostCard/components/PostImages.tsx index 8a4ee81..c28dbeb 100644 --- a/src/components/business/PostCard/components/PostImages.tsx +++ b/src/components/business/PostCard/components/PostImages.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { View, StyleSheet } from 'react-native'; -import { colors, spacing, borderRadius } from '../../../../theme'; +import { spacing, borderRadius } from '../../../../theme'; import { useResponsive } from '../../../../hooks/useResponsive'; import { ImageGrid, ImageGridItem } from '../../../common'; import { PostImagesProps } from '../types'; diff --git a/src/components/business/QRCodeScanner.tsx b/src/components/business/QRCodeScanner.tsx index a83f370..401d683 100644 --- a/src/components/business/QRCodeScanner.tsx +++ b/src/components/business/QRCodeScanner.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { View, Text, @@ -12,7 +12,7 @@ import { CameraView, useCameraPermissions } from 'expo-camera'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as hrefs from '../../navigation/hrefs'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; const { width, height } = Dimensions.get('window'); @@ -21,14 +21,130 @@ interface QRCodeScannerProps { onClose: () => void; } +function createQrScannerStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + camera: { + flex: 1, + }, + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingTop: spacing.xl, + paddingBottom: spacing.md, + }, + closeButton: { + padding: spacing.sm, + }, + headerTitle: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: '#fff', + }, + placeholder: { + width: 40, + }, + scanArea: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + scanFrame: { + width: width * 0.7, + height: width * 0.7, + position: 'relative', + }, + corner: { + position: 'absolute', + width: 20, + height: 20, + borderColor: colors.primary.main, + borderWidth: 3, + }, + topLeft: { + top: 0, + left: 0, + borderRightWidth: 0, + borderBottomWidth: 0, + }, + topRight: { + top: 0, + right: 0, + borderLeftWidth: 0, + borderBottomWidth: 0, + }, + bottomLeft: { + bottom: 0, + left: 0, + borderRightWidth: 0, + borderTopWidth: 0, + }, + bottomRight: { + bottom: 0, + right: 0, + borderLeftWidth: 0, + borderTopWidth: 0, + }, + scanText: { + marginTop: spacing.lg, + fontSize: fontSizes.md, + color: '#fff', + textAlign: 'center', + }, + footer: { + padding: spacing.xl, + alignItems: 'center', + }, + flashButton: { + padding: spacing.md, + backgroundColor: 'rgba(255,255,255,0.2)', + borderRadius: borderRadius.full, + }, + permissionContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + permissionText: { + marginTop: spacing.lg, + fontSize: fontSizes.md, + color: 'rgba(255,255,255,0.72)', + textAlign: 'center', + }, + permissionButton: { + marginTop: spacing.xl, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + backgroundColor: colors.primary.main, + borderRadius: borderRadius.md, + }, + permissionButtonText: { + color: '#fff', + fontSize: fontSizes.md, + fontWeight: '600', + }, + }); +} + export const QRCodeScanner: React.FC = ({ visible, onClose }) => { const router = useRouter(); + const themeColors = useAppColors(); + const styles = useMemo(() => createQrScannerStyles(themeColors), [themeColors]); const [permission, requestPermission] = useCameraPermissions(); const [scanned, setScanned] = useState(false); useEffect(() => { if (visible) { - // 重置扫描状态,确保每次打开都能重新扫描 setScanned(false); if (!permission?.granted) { requestPermission(); @@ -39,16 +155,11 @@ export const QRCodeScanner: React.FC = ({ visible, onClose } const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => { if (scanned) return; setScanned(true); - - // 先关闭扫描界面,类似微信的做法 onClose(); - // 解析二维码内容 - // 格式: carrotbbs://qrcode/login?session_id=xxx if (data.startsWith('carrotbbs://qrcode/login')) { const sessionId = extractSessionId(data); if (sessionId) { - // 跳转到确认页面 router.push(hrefs.hrefQrLoginConfirm(sessionId)); } else { Alert.alert('无效的二维码', '无法识别该二维码'); @@ -79,7 +190,7 @@ export const QRCodeScanner: React.FC = ({ visible, onClose } - + 需要相机权限才能扫码 授予权限 @@ -132,117 +243,4 @@ export const QRCodeScanner: React.FC = ({ visible, onClose } ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - camera: { - flex: 1, - }, - overlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.5)', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingTop: spacing.xl, - paddingBottom: spacing.md, - }, - closeButton: { - padding: spacing.sm, - }, - headerTitle: { - fontSize: fontSizes.lg, - fontWeight: '600', - color: '#fff', - }, - placeholder: { - width: 40, - }, - scanArea: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - scanFrame: { - width: width * 0.7, - height: width * 0.7, - position: 'relative', - }, - corner: { - position: 'absolute', - width: 20, - height: 20, - borderColor: colors.primary.main, - borderWidth: 3, - }, - topLeft: { - top: 0, - left: 0, - borderRightWidth: 0, - borderBottomWidth: 0, - }, - topRight: { - top: 0, - right: 0, - borderLeftWidth: 0, - borderBottomWidth: 0, - }, - bottomLeft: { - bottom: 0, - left: 0, - borderRightWidth: 0, - borderTopWidth: 0, - }, - bottomRight: { - bottom: 0, - right: 0, - borderLeftWidth: 0, - borderTopWidth: 0, - }, - scanText: { - marginTop: spacing.lg, - fontSize: fontSizes.md, - color: '#fff', - textAlign: 'center', - }, - footer: { - padding: spacing.xl, - alignItems: 'center', - }, - flashButton: { - padding: spacing.md, - backgroundColor: 'rgba(255,255,255,0.2)', - borderRadius: borderRadius.full, - }, - permissionContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: spacing.xl, - }, - permissionText: { - marginTop: spacing.lg, - fontSize: fontSizes.md, - color: '#666', - textAlign: 'center', - }, - permissionButton: { - marginTop: spacing.xl, - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - backgroundColor: colors.primary.main, - borderRadius: borderRadius.md, - }, - permissionButtonText: { - color: '#fff', - fontSize: fontSizes.md, - fontWeight: '600', - }, -}); - -export default QRCodeScanner; \ No newline at end of file +export default QRCodeScanner; diff --git a/src/components/business/SearchBar.tsx b/src/components/business/SearchBar.tsx index c1bf4b4..0d66df7 100644 --- a/src/components/business/SearchBar.tsx +++ b/src/components/business/SearchBar.tsx @@ -3,10 +3,10 @@ * 用于搜索内容 */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; interface SearchBarProps { value: string; @@ -18,6 +18,63 @@ interface SearchBarProps { autoFocus?: boolean; } +function createSearchBarStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.full, + paddingHorizontal: spacing.xs, + height: 46, + borderWidth: 1, + borderColor: colors.divider, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + containerFocused: { + borderColor: colors.primary.main, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + searchIconWrap: { + width: 30, + height: 30, + marginLeft: spacing.xs, + marginRight: spacing.xs, + borderRadius: borderRadius.full, + backgroundColor: `${colors.text.secondary}12`, + alignItems: 'center', + justifyContent: 'center', + }, + searchIconWrapFocused: { + backgroundColor: `${colors.primary.main}1A`, + }, + input: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + paddingVertical: spacing.sm + 1, + paddingHorizontal: spacing.xs, + }, + clearButton: { + width: 24, + height: 24, + marginHorizontal: spacing.xs, + borderRadius: borderRadius.full, + backgroundColor: `${colors.text.secondary}14`, + alignItems: 'center', + justifyContent: 'center', + }, + }); +} + const SearchBar: React.FC = ({ value, onChangeText, @@ -27,6 +84,8 @@ const SearchBar: React.FC = ({ onBlur, autoFocus = false, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createSearchBarStyles(colors), [colors]); const [isFocused, setIsFocused] = useState(false); const handleFocus = () => { @@ -59,7 +118,6 @@ const SearchBar: React.FC = ({ onFocus={handleFocus} onBlur={handleBlur} autoFocus={autoFocus} - // 确保光标可见 cursorColor={colors.primary.main} selectionColor={`${colors.primary.main}40`} /> @@ -69,73 +127,11 @@ const SearchBar: React.FC = ({ style={styles.clearButton} activeOpacity={0.7} > - + )} ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.full, - paddingHorizontal: spacing.xs, - height: 46, - borderWidth: 1, - borderColor: '#E7E7E7', - // 减少常态阴影,避免看起来像"黑框" - shadowColor: 'transparent', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0, - shadowRadius: 0, - elevation: 0, - }, - containerFocused: { - // 焦点时使用更柔和的边框颜色 - borderColor: colors.primary.main, - // 不添加额外的阴影,保持简洁 - shadowColor: 'transparent', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0, - shadowRadius: 0, - elevation: 0, - }, - searchIconWrap: { - width: 30, - height: 30, - marginLeft: spacing.xs, - marginRight: spacing.xs, - borderRadius: borderRadius.full, - backgroundColor: `${colors.text.secondary}12`, - alignItems: 'center', - justifyContent: 'center', - }, - searchIconWrapFocused: { - backgroundColor: `${colors.primary.main}1A`, - }, - input: { - flex: 1, - fontSize: fontSizes.md, - color: colors.text.primary, - paddingVertical: spacing.sm + 1, - paddingHorizontal: spacing.xs, - }, - clearButton: { - width: 24, - height: 24, - marginHorizontal: spacing.xs, - borderRadius: borderRadius.full, - backgroundColor: `${colors.text.secondary}14`, - alignItems: 'center', - justifyContent: 'center', - }, -}); - export default SearchBar; diff --git a/src/components/business/SystemMessageItem.tsx b/src/components/business/SystemMessageItem.tsx index b10b238..45ccf14 100644 --- a/src/components/business/SystemMessageItem.tsx +++ b/src/components/business/SystemMessageItem.tsx @@ -4,12 +4,12 @@ * 采用卡片式设计,符合胡萝卜BBS整体风格 */ -import React from 'react'; -import { View, TouchableOpacity, StyleSheet, Animated } from 'react-native'; +import React, { useMemo } from 'react'; +import { View, TouchableOpacity, StyleSheet } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; -import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; +import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { SystemMessageResponse, SystemMessageType } from '../../types/dto'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; @@ -23,9 +23,9 @@ interface SystemMessageItemProps { index?: number; // 用于交错动画 } -// 系统消息类型到图标和颜色的映射 const getSystemMessageIcon = ( - systemType: SystemMessageType + systemType: SystemMessageType, + colors: AppColors ): { icon: keyof typeof MaterialCommunityIcons.glyphMap; color: string; bgColor: string; gradient: string[] } => { switch (systemType) { case 'like_post': @@ -119,6 +119,155 @@ const getSystemMessageIcon = ( } }; +function createSystemMessageStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-start', + padding: spacing.md, + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + marginHorizontal: spacing.md, + marginBottom: spacing.sm, + ...shadows.sm, + }, + unreadContainer: { + backgroundColor: colors.primary.light + '08', + borderLeftWidth: 3, + borderLeftColor: colors.primary.main, + }, + unreadIndicator: { + position: 'absolute', + left: 6, + top: '50%', + marginTop: -4, + width: 8, + height: 8, + borderRadius: borderRadius.full, + backgroundColor: colors.primary.main, + }, + iconContainer: { + marginRight: spacing.md, + }, + avatarWrapper: { + position: 'relative', + }, + iconWrapper: { + width: 48, + height: 48, + borderRadius: borderRadius.lg, + justifyContent: 'center', + alignItems: 'center', + }, + typeIconBadge: { + position: 'absolute', + bottom: -2, + right: -2, + width: 20, + height: 20, + borderRadius: borderRadius.full, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: colors.background.paper, + }, + content: { + flex: 1, + justifyContent: 'center', + minWidth: 0, + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.xs, + }, + titleLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + marginRight: spacing.sm, + }, + title: { + fontWeight: '500', + fontSize: fontSizes.md, + color: colors.text.primary, + }, + unreadTitle: { + fontWeight: '600', + color: colors.text.primary, + }, + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.xs, + paddingVertical: 2, + borderRadius: borderRadius.sm, + marginLeft: spacing.xs, + }, + statusText: { + fontSize: 10, + fontWeight: '500', + marginLeft: 2, + }, + time: { + fontSize: fontSizes.sm, + flexShrink: 0, + }, + messageContent: { + lineHeight: 20, + fontSize: fontSizes.sm, + }, + extraInfo: { + flexDirection: 'row', + alignItems: 'center', + marginTop: spacing.xs, + backgroundColor: colors.primary.light + '12', + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.md, + alignSelf: 'flex-start', + }, + extraText: { + marginLeft: spacing.xs, + flex: 1, + fontWeight: '500', + }, + actionsRow: { + flexDirection: 'row', + marginTop: spacing.sm, + gap: spacing.sm, + }, + actionBtn: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, + borderRadius: borderRadius.md, + borderWidth: 1, + gap: spacing.xs, + }, + rejectBtn: { + borderColor: `${colors.error.main}55`, + backgroundColor: `${colors.error.main}18`, + }, + approveBtn: { + borderColor: `${colors.success.main}55`, + backgroundColor: `${colors.success.main}18`, + }, + actionBtnText: { + fontWeight: '500', + }, + arrowContainer: { + marginLeft: spacing.sm, + justifyContent: 'center', + alignItems: 'center', + height: 20, + width: 20, + }, + }); +} + // 获取系统消息标题 const getSystemMessageTitle = (message: SystemMessageResponse): string => { const { system_type, extra_data } = message; @@ -177,6 +326,9 @@ const SystemMessageItem: React.FC = ({ requestActionLoading = false, index = 0, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createSystemMessageStyles(colors), [colors]); + // 格式化时间 const formatTime = (dateString: string): string => { try { @@ -189,7 +341,7 @@ const SystemMessageItem: React.FC = ({ } }; - const { icon, color, bgColor } = getSystemMessageIcon(message.system_type); + const { icon, color, bgColor } = getSystemMessageIcon(message.system_type, colors); const title = getSystemMessageTitle(message); const { extra_data } = message; const operatorName = extra_data?.actor_name || extra_data?.operator_name; @@ -212,17 +364,17 @@ const SystemMessageItem: React.FC = ({ const getStatusBadge = () => { if (requestStatus === 'accepted') { return ( - - - 已同意 + + + 已同意 ); } if (requestStatus === 'rejected') { return ( - - - 已拒绝 + + + 已拒绝 ); } @@ -328,16 +480,16 @@ const SystemMessageItem: React.FC = ({ onPress={() => onRequestAction?.(false)} disabled={requestActionLoading} > - - 拒绝 + + 拒绝 onRequestAction?.(true)} disabled={requestActionLoading} > - - 同意 + + 同意 )} @@ -353,151 +505,4 @@ const SystemMessageItem: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'flex-start', - padding: spacing.md, - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - marginHorizontal: spacing.md, - marginBottom: spacing.sm, - ...shadows.sm, - }, - unreadContainer: { - backgroundColor: colors.primary.light + '08', - borderLeftWidth: 3, - borderLeftColor: colors.primary.main, - }, - unreadIndicator: { - position: 'absolute', - left: 6, - top: '50%', - marginTop: -4, - width: 8, - height: 8, - borderRadius: borderRadius.full, - backgroundColor: colors.primary.main, - }, - iconContainer: { - marginRight: spacing.md, - }, - avatarWrapper: { - position: 'relative', - }, - iconWrapper: { - width: 48, - height: 48, - borderRadius: borderRadius.lg, - justifyContent: 'center', - alignItems: 'center', - }, - typeIconBadge: { - position: 'absolute', - bottom: -2, - right: -2, - width: 20, - height: 20, - borderRadius: borderRadius.full, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 2, - borderColor: colors.background.paper, - }, - content: { - flex: 1, - justifyContent: 'center', - minWidth: 0, - }, - titleRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.xs, - }, - titleLeft: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - marginRight: spacing.sm, - }, - title: { - fontWeight: '500', - fontSize: fontSizes.md, - color: colors.text.primary, - }, - unreadTitle: { - fontWeight: '600', - color: colors.text.primary, - }, - statusBadge: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.xs, - paddingVertical: 2, - borderRadius: borderRadius.sm, - marginLeft: spacing.xs, - }, - statusText: { - fontSize: 10, - fontWeight: '500', - marginLeft: 2, - }, - time: { - fontSize: fontSizes.sm, - flexShrink: 0, - }, - messageContent: { - lineHeight: 20, - fontSize: fontSizes.sm, - }, - extraInfo: { - flexDirection: 'row', - alignItems: 'center', - marginTop: spacing.xs, - backgroundColor: colors.primary.light + '12', - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - borderRadius: borderRadius.md, - alignSelf: 'flex-start', - }, - extraText: { - marginLeft: spacing.xs, - flex: 1, - fontWeight: '500', - }, - actionsRow: { - flexDirection: 'row', - marginTop: spacing.sm, - gap: spacing.sm, - }, - actionBtn: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.xs, - paddingHorizontal: spacing.md, - borderRadius: borderRadius.md, - borderWidth: 1, - gap: spacing.xs, - }, - rejectBtn: { - borderColor: '#FFCDD2', - backgroundColor: '#FFEBEE', - }, - approveBtn: { - borderColor: '#C8E6C9', - backgroundColor: '#E8F5E9', - }, - actionBtnText: { - fontWeight: '500', - }, - arrowContainer: { - marginLeft: spacing.sm, - justifyContent: 'center', - alignItems: 'center', - height: 20, - width: 20, - }, -}); - export default SystemMessageItem; diff --git a/src/components/business/TabBar.tsx b/src/components/business/TabBar.tsx index 8b29117..ee3ce6d 100644 --- a/src/components/business/TabBar.tsx +++ b/src/components/business/TabBar.tsx @@ -4,10 +4,10 @@ * 新增胶囊式、分段式等现代设计风格 */ -import React, { ReactNode } from 'react'; -import { View, TouchableOpacity, StyleSheet, ScrollView, Animated, ViewStyle, StyleProp } from 'react-native'; +import React, { ReactNode, useMemo } from 'react'; +import { View, TouchableOpacity, StyleSheet, ScrollView, ViewStyle, StyleProp } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import Text from '../common/Text'; type TabBarVariant = 'default' | 'pill' | 'segmented' | 'modern'; @@ -20,10 +20,168 @@ interface TabBarProps { rightContent?: ReactNode; variant?: TabBarVariant; icons?: string[]; - /** 合并到最外层容器,用于按页面微调外边距等 */ style?: StyleProp; } +function createTabBarStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + alignItems: 'center', + paddingRight: spacing.xs, + }, + scrollableContainer: { + flexDirection: 'row', + paddingHorizontal: spacing.md, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + flex: 1, + }, + tab: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + position: 'relative', + }, + activeTab: {}, + tabText: { + fontWeight: '500', + }, + activeTabText: { + fontWeight: '600', + }, + activeIndicator: { + position: 'absolute', + bottom: 0, + left: '25%', + right: '25%', + height: 3, + backgroundColor: colors.primary.main, + borderTopLeftRadius: borderRadius.sm, + borderTopRightRadius: borderRadius.sm, + }, + rightContent: { + paddingLeft: spacing.sm, + }, + pillContainer: { + flexDirection: 'row', + backgroundColor: colors.background.default, + padding: spacing.sm, + gap: spacing.sm, + }, + pillTab: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + borderRadius: borderRadius.lg, + backgroundColor: colors.background.paper, + }, + pillTabActive: { + backgroundColor: colors.primary.main, + }, + pillTabText: { + fontWeight: '600', + }, + segmentedContainer: { + flexDirection: 'row', + backgroundColor: colors.background.default, + padding: spacing.xs, + marginHorizontal: spacing.md, + marginVertical: spacing.sm, + borderRadius: borderRadius.lg, + }, + segmentedTab: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.sm, + backgroundColor: 'transparent', + }, + segmentedTabActive: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.md, + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + segmentedTabFirst: { + borderTopLeftRadius: borderRadius.md, + borderBottomLeftRadius: borderRadius.md, + }, + segmentedTabLast: { + borderTopRightRadius: borderRadius.md, + borderBottomRightRadius: borderRadius.md, + }, + segmentedTabText: { + fontWeight: '600', + }, + segmentedTabContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + segmentedTabIcon: { + marginRight: spacing.xs, + }, + modernContainer: { + flexDirection: 'row', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.xl, + marginHorizontal: spacing.lg, + marginVertical: spacing.md, + padding: spacing.xs, + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 3, + }, + modernTab: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.sm, + borderRadius: borderRadius.lg, + position: 'relative', + }, + modernTabActive: { + backgroundColor: colors.primary.main + '15', + }, + modernTabContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + modernTabIcon: { + marginRight: spacing.xs, + }, + modernTabText: { + fontWeight: '500', + fontSize: fontSizes.md, + }, + modernTabTextActive: { + fontWeight: '700', + }, + modernTabIndicator: { + position: 'absolute', + bottom: 4, + width: 20, + height: 3, + backgroundColor: colors.primary.main, + borderRadius: borderRadius.full, + }, + }); +} + const TabBar: React.FC = ({ tabs, activeIndex, @@ -34,6 +192,9 @@ const TabBar: React.FC = ({ icons, style, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createTabBarStyles(colors), [colors]); + const renderTabs = () => { return tabs.map((tab, index) => { const isActive = index === activeIndex; @@ -112,11 +273,7 @@ const TabBar: React.FC = ({ style={styles.segmentedTabIcon} /> )} - + {tab} @@ -124,7 +281,6 @@ const TabBar: React.FC = ({ ); } - // default variant return ( = ({ if (scrollable) { return ( - + {renderTabs()} {rightContent && {rightContent}} @@ -181,170 +333,4 @@ const TabBar: React.FC = ({ ); }; -const styles = StyleSheet.create({ - // Default variant - container: { - flexDirection: 'row', - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - alignItems: 'center', - paddingRight: spacing.xs, - }, - scrollableContainer: { - flexDirection: 'row', - paddingHorizontal: spacing.md, - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - flex: 1, - }, - tab: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.md, - position: 'relative', - }, - activeTab: { - // 激活状态样式 - }, - tabText: { - fontWeight: '500', - }, - activeTabText: { - fontWeight: '600', - }, - activeIndicator: { - position: 'absolute', - bottom: 0, - left: '25%', - right: '25%', - height: 3, - backgroundColor: colors.primary.main, - borderTopLeftRadius: borderRadius.sm, - borderTopRightRadius: borderRadius.sm, - }, - rightContent: { - paddingLeft: spacing.sm, - }, - - // Pill variant - pillContainer: { - flexDirection: 'row', - backgroundColor: colors.background.default, - padding: spacing.sm, - gap: spacing.sm, - }, - pillTab: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - borderRadius: borderRadius.lg, - backgroundColor: colors.background.paper, - }, - pillTabActive: { - backgroundColor: colors.primary.main, - }, - pillTabText: { - fontWeight: '600', - }, - - // Segmented variant - segmentedContainer: { - flexDirection: 'row', - backgroundColor: colors.background.default, - padding: spacing.xs, - marginHorizontal: spacing.md, - marginVertical: spacing.sm, - borderRadius: borderRadius.lg, - }, - segmentedTab: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.sm, - backgroundColor: 'transparent', - }, - segmentedTabActive: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.md, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - }, - segmentedTabFirst: { - borderTopLeftRadius: borderRadius.md, - borderBottomLeftRadius: borderRadius.md, - }, - segmentedTabLast: { - borderTopRightRadius: borderRadius.md, - borderBottomRightRadius: borderRadius.md, - }, - segmentedTabText: { - fontWeight: '600', - }, - segmentedTabContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - segmentedTabIcon: { - marginRight: spacing.xs, - }, - - // Modern variant - 现代化标签栏 - modernContainer: { - flexDirection: 'row', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - marginHorizontal: spacing.lg, - marginVertical: spacing.md, - padding: spacing.xs, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.08, - shadowRadius: 8, - elevation: 3, - }, - modernTab: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.sm, - borderRadius: borderRadius.lg, - position: 'relative', - }, - modernTabActive: { - backgroundColor: colors.primary.main + '15', // 10% opacity - }, - modernTabContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - modernTabIcon: { - marginRight: spacing.xs, - }, - modernTabText: { - fontWeight: '500', - fontSize: fontSizes.md, - }, - modernTabTextActive: { - fontWeight: '700', - }, - modernTabIndicator: { - position: 'absolute', - bottom: 4, - width: 20, - height: 3, - backgroundColor: colors.primary.main, - borderRadius: borderRadius.full, - }, -}); - export default TabBar; diff --git a/src/components/business/UserProfileHeader.tsx b/src/components/business/UserProfileHeader.tsx index 80d4e20..e685e70 100644 --- a/src/components/business/UserProfileHeader.tsx +++ b/src/components/business/UserProfileHeader.tsx @@ -6,7 +6,7 @@ * 在宽屏下显示更大的头像和封面 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, Image, @@ -15,7 +15,7 @@ import { } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { User } from '../../types'; import Text from '../common/Text'; import Button from '../common/Button'; @@ -49,7 +49,8 @@ const UserProfileHeader: React.FC = ({ onFollowersPress, onAvatarPress, }) => { - // 响应式布局 + const colors = useAppColors(); + const styles = useMemo(() => createUserProfileHeaderStyles(colors), [colors]); const { isWideScreen, isDesktop, width } = useResponsive(); // 格式化数字 @@ -317,7 +318,8 @@ const UserProfileHeader: React.FC = ({ ); }; -const styles = StyleSheet.create({ +function createUserProfileHeaderStyles(colors: AppColors) { + return StyleSheet.create({ container: { backgroundColor: colors.background.default, }, @@ -533,7 +535,8 @@ const styles = StyleSheet.create({ alignSelf: 'center', padding: spacing.sm, }, -}); + }); +} // 使用 React.memo 避免不必要的重新渲染 const MemoizedUserProfileHeader = React.memo(UserProfileHeader); diff --git a/src/components/business/VoteCard.tsx b/src/components/business/VoteCard.tsx index ec555ef..9705da1 100644 --- a/src/components/business/VoteCard.tsx +++ b/src/components/business/VoteCard.tsx @@ -4,7 +4,7 @@ * 风格与现代整体UI保持一致 */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { View, TouchableOpacity, @@ -12,7 +12,7 @@ import { Animated, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { VoteOptionDTO } from '../../types'; import Text from '../common/Text'; @@ -28,6 +28,146 @@ interface VoteCardProps { compact?: boolean; } +function createVoteCardStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + marginVertical: spacing.sm, + borderWidth: 1, + borderColor: colors.divider, + }, + containerCompact: { + padding: spacing.sm, + marginVertical: spacing.xs, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: spacing.sm, + }, + headerIcon: { + width: 24, + height: 24, + borderRadius: borderRadius.sm, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.sm, + }, + headerTitle: { + fontWeight: '600', + color: colors.text.primary, + }, + optionsList: { + gap: spacing.sm, + }, + optionContainer: { + position: 'relative', + borderRadius: borderRadius.md, + overflow: 'hidden', + }, + progressBar: { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + borderRadius: borderRadius.md, + }, + optionButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + borderRadius: borderRadius.md, + backgroundColor: colors.background.default, + minHeight: 44, + borderWidth: 1, + borderColor: 'transparent', + }, + optionButtonVoted: { + borderColor: colors.primary.main, + backgroundColor: 'transparent', + }, + optionIndicator: { + width: 18, + height: 18, + borderRadius: borderRadius.full, + borderWidth: 2, + borderColor: colors.divider, + marginRight: spacing.sm, + justifyContent: 'center', + alignItems: 'center', + }, + optionIndicatorVoted: { + backgroundColor: colors.primary.main, + borderColor: colors.primary.main, + }, + optionText: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + lineHeight: 20, + }, + optionTextCompact: { + fontSize: fontSizes.sm, + lineHeight: 18, + }, + resultContainer: { + flexDirection: 'row', + alignItems: 'center', + marginLeft: spacing.sm, + minWidth: 40, + justifyContent: 'flex-end', + }, + percentage: { + color: colors.text.secondary, + fontWeight: '500', + }, + percentageVoted: { + color: colors.primary.main, + fontWeight: '700', + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: spacing.md, + paddingTop: spacing.sm, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.divider, + }, + footerLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + footerText: { + fontSize: fontSizes.sm, + }, + unvoteButton: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + borderRadius: borderRadius.sm, + backgroundColor: colors.background.default, + }, + unvoteText: { + fontSize: fontSizes.sm, + }, + loadingOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: colors.background.paper + 'CC', + justifyContent: 'center', + alignItems: 'center', + borderRadius: borderRadius.lg, + }, + }); +} + const VoteCard: React.FC = ({ options, totalVotes, @@ -38,7 +178,8 @@ const VoteCard: React.FC = ({ isLoading = false, compact = false, }) => { - // 动画值 + const colors = useAppColors(); + const styles = useMemo(() => createVoteCardStyles(colors), [colors]); const progressAnim = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { @@ -151,7 +292,7 @@ const VoteCard: React.FC = ({ ); - }, [hasVoted, votedOptionId, calculatePercentage, handleVote, isLoading, progressAnim, compact]); + }, [hasVoted, votedOptionId, calculatePercentage, handleVote, isLoading, progressAnim, compact, colors, styles]); // 排序后的选项(已投票的排在前面) const sortedOptions = React.useMemo(() => { @@ -229,142 +370,4 @@ const VoteCard: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.md, - marginVertical: spacing.sm, - borderWidth: 1, - borderColor: colors.divider, - }, - containerCompact: { - padding: spacing.sm, - marginVertical: spacing.xs, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: spacing.sm, - }, - headerIcon: { - width: 24, - height: 24, - borderRadius: borderRadius.sm, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.sm, - }, - headerTitle: { - fontWeight: '600', - color: colors.text.primary, - }, - optionsList: { - gap: spacing.sm, - }, - optionContainer: { - position: 'relative', - borderRadius: borderRadius.md, - overflow: 'hidden', - }, - progressBar: { - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - borderRadius: borderRadius.md, - }, - optionButton: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - borderRadius: borderRadius.md, - backgroundColor: colors.background.default, - minHeight: 44, - borderWidth: 1, - borderColor: 'transparent', - }, - optionButtonVoted: { - borderColor: colors.primary.main, - backgroundColor: 'transparent', - }, - optionIndicator: { - width: 18, - height: 18, - borderRadius: borderRadius.full, - borderWidth: 2, - borderColor: colors.divider, - marginRight: spacing.sm, - justifyContent: 'center', - alignItems: 'center', - }, - optionIndicatorVoted: { - backgroundColor: colors.primary.main, - borderColor: colors.primary.main, - }, - optionText: { - flex: 1, - fontSize: fontSizes.md, - color: colors.text.primary, - lineHeight: 20, - }, - optionTextCompact: { - fontSize: fontSizes.sm, - lineHeight: 18, - }, - resultContainer: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: spacing.sm, - minWidth: 40, - justifyContent: 'flex-end', - }, - percentage: { - color: colors.text.secondary, - fontWeight: '500', - }, - percentageVoted: { - color: colors.primary.main, - fontWeight: '700', - }, - footer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginTop: spacing.md, - paddingTop: spacing.sm, - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: colors.divider, - }, - footerLeft: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - }, - footerText: { - fontSize: fontSizes.sm, - }, - unvoteButton: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - paddingVertical: spacing.xs, - paddingHorizontal: spacing.sm, - borderRadius: borderRadius.sm, - backgroundColor: colors.background.default, - }, - unvoteText: { - fontSize: fontSizes.sm, - }, - loadingOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: colors.background.paper + 'CC', - justifyContent: 'center', - alignItems: 'center', - borderRadius: borderRadius.lg, - }, -}); - export default VoteCard; diff --git a/src/components/business/VoteEditor.tsx b/src/components/business/VoteEditor.tsx index 19fde40..952a19c 100644 --- a/src/components/business/VoteEditor.tsx +++ b/src/components/business/VoteEditor.tsx @@ -3,7 +3,7 @@ * 用于创建帖子时编辑投票选项 */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { View, TouchableOpacity, @@ -11,7 +11,7 @@ import { TextInput, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import Text from '../common/Text'; interface VoteEditorProps { @@ -24,6 +24,86 @@ interface VoteEditorProps { maxLength?: number; } +function createVoteEditorStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + backgroundColor: colors.background.default, + borderRadius: borderRadius.lg, + marginHorizontal: spacing.lg, + marginTop: spacing.md, + marginBottom: spacing.md, + padding: spacing.md, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.md, + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, + headerTitle: { + fontWeight: '600', + color: colors.text.primary, + }, + optionsContainer: { + gap: spacing.sm, + }, + optionRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, + optionIndex: { + width: 20, + height: 20, + borderRadius: borderRadius.full, + backgroundColor: colors.background.disabled, + justifyContent: 'center', + alignItems: 'center', + }, + optionInput: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + backgroundColor: colors.background.paper, + borderRadius: borderRadius.md, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + borderWidth: 1, + borderColor: colors.divider, + height: 44, + }, + removeButton: { + padding: spacing.xs, + }, + removeButtonPlaceholder: { + width: 28, + }, + addOptionButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: spacing.sm, + paddingVertical: spacing.sm, + marginTop: spacing.xs, + }, + addOptionText: { + fontWeight: '500', + }, + hintContainer: { + marginTop: spacing.md, + paddingTop: spacing.sm, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.divider, + alignItems: 'center', + }, + }); +} + const VoteEditor: React.FC = ({ options, onAddOption, @@ -33,6 +113,8 @@ const VoteEditor: React.FC = ({ minOptions = 2, maxLength = 50, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createVoteEditorStyles(colors), [colors]); const validOptionsCount = options.filter(opt => opt.trim() !== '').length; const canAddOption = options.length < maxOptions; const canRemoveOption = options.length > minOptions; @@ -122,82 +204,4 @@ const VoteEditor: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - backgroundColor: colors.background.default, - borderRadius: borderRadius.lg, - marginHorizontal: spacing.lg, - marginTop: spacing.md, - marginBottom: spacing.md, - padding: spacing.md, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: spacing.md, - }, - headerLeft: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - }, - headerTitle: { - fontWeight: '600', - color: colors.text.primary, - }, - optionsContainer: { - gap: spacing.sm, - }, - optionRow: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - }, - optionIndex: { - width: 20, - height: 20, - borderRadius: borderRadius.full, - backgroundColor: colors.background.disabled, - justifyContent: 'center', - alignItems: 'center', - }, - optionInput: { - flex: 1, - fontSize: fontSizes.md, - color: colors.text.primary, - backgroundColor: colors.background.paper, - borderRadius: borderRadius.md, - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - borderWidth: 1, - borderColor: colors.divider, - height: 44, - }, - removeButton: { - padding: spacing.xs, - }, - removeButtonPlaceholder: { - width: 28, - }, - addOptionButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: spacing.sm, - paddingVertical: spacing.sm, - marginTop: spacing.xs, - }, - addOptionText: { - fontWeight: '500', - }, - hintContainer: { - marginTop: spacing.md, - paddingTop: spacing.sm, - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: colors.divider, - alignItems: 'center', - }, -}); - export default VoteEditor; diff --git a/src/components/business/VotePreview.tsx b/src/components/business/VotePreview.tsx index a430e95..1cbfad5 100644 --- a/src/components/business/VotePreview.tsx +++ b/src/components/business/VotePreview.tsx @@ -3,14 +3,14 @@ * 用于帖子列表中显示投票预览,类似微博风格 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, TouchableOpacity, StyleSheet, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import Text from '../common/Text'; interface VotePreviewProps { @@ -19,11 +19,51 @@ interface VotePreviewProps { onPress?: () => void; } +function createVotePreviewStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primary.light + '08', + borderRadius: borderRadius.md, + padding: spacing.md, + marginTop: spacing.sm, + borderWidth: 1, + borderColor: colors.primary.light + '30', + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.sm, + }, + content: { + flex: 1, + }, + title: { + fontSize: fontSizes.md, + fontWeight: '600', + color: colors.text.primary, + marginBottom: 2, + }, + subtitle: { + fontSize: fontSizes.sm, + color: colors.text.secondary, + }, + }); +} + const VotePreview: React.FC = ({ totalVotes = 0, optionsCount = 0, onPress, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createVotePreviewStyles(colors), [colors]); + // 格式化票数 const formatVoteCount = (count: number): string => { if (count >= 10000) { @@ -71,39 +111,4 @@ const VotePreview: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.primary.light + '08', - borderRadius: borderRadius.md, - padding: spacing.md, - marginTop: spacing.sm, - borderWidth: 1, - borderColor: colors.primary.light + '30', - }, - iconContainer: { - width: 36, - height: 36, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.sm, - }, - content: { - flex: 1, - }, - title: { - fontSize: fontSizes.md, - fontWeight: '600', - color: colors.text.primary, - marginBottom: 2, - }, - subtitle: { - fontSize: fontSizes.sm, - color: colors.text.secondary, - }, -}); - export default VotePreview; diff --git a/src/components/common/AppBackButton.tsx b/src/components/common/AppBackButton.tsx index 2e40bc7..687d936 100644 --- a/src/components/common/AppBackButton.tsx +++ b/src/components/common/AppBackButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { TouchableOpacity, StyleProp, ViewStyle, StyleSheet } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { borderRadius, colors, spacing } from '../../theme'; +import { borderRadius, spacing, useAppColors } from '../../theme'; type AppBackButtonIcon = 'arrow-left' | 'close'; @@ -20,11 +20,15 @@ const AppBackButton: React.FC = ({ onPress, icon = 'arrow-left', style, - iconColor = colors.text.primary, - backgroundColor = colors.background.paper, + iconColor: iconColorProp, + backgroundColor: backgroundColorProp, hitSlop = 8, testID, }) => { + const colors = useAppColors(); + const iconColor = iconColorProp ?? colors.text.primary; + const backgroundColor = backgroundColorProp ?? colors.background.paper; + return ( { const [dialog, setDialog] = useState(null); + const colors = useAppColors(); + const styles = useMemo(() => createDialogHostStyles(colors), [colors]); useEffect(() => { const unbind = bindDialogListener((payload) => { @@ -19,7 +107,7 @@ const AppDialogHost: React.FC = () => { const actions = useMemo(() => { if (!dialog?.actions?.length) return [{ text: '确定' }]; - return dialog.actions.slice(0, 3); + return dialog.actions; }, [dialog]); const onClose = () => { @@ -101,88 +189,4 @@ const AppDialogHost: React.FC = () => { ); }; -const styles = StyleSheet.create({ - backdrop: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.36)', - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: spacing.xl, - }, - container: { - width: '100%', - maxWidth: 380, - backgroundColor: colors.background.paper, - borderRadius: borderRadius['2xl'], - paddingHorizontal: spacing.xl, - paddingTop: spacing.lg, - paddingBottom: spacing.lg, - ...shadows.lg, - }, - iconHeader: { - alignItems: 'center', - marginBottom: spacing.md, - }, - iconBadge: { - width: 42, - height: 42, - borderRadius: borderRadius.full, - justifyContent: 'center', - alignItems: 'center', - }, - title: { - color: colors.text.primary, - fontSize: 18, - fontWeight: '700', - textAlign: 'center', - }, - message: { - marginTop: spacing.md, - color: colors.text.secondary, - fontSize: 14, - lineHeight: 21, - textAlign: 'center', - }, - actions: { - marginTop: spacing.xl, - gap: spacing.sm, - }, - actionButton: { - height: 46, - borderRadius: borderRadius.lg, - borderWidth: 1, - borderColor: `${colors.primary.main}28`, - backgroundColor: '#FFFFFF', - justifyContent: 'center', - alignItems: 'center', - }, - primaryButton: { - backgroundColor: colors.primary.main, - borderColor: colors.primary.main, - }, - cancelButton: { - backgroundColor: colors.background.paper, - borderColor: colors.divider, - }, - destructiveButton: { - backgroundColor: '#FEECEC', - borderColor: '#FCD4D1', - }, - actionText: { - color: colors.text.primary, - fontSize: 15, - fontWeight: '700', - }, - primaryText: { - color: '#FFFFFF', - }, - cancelText: { - color: colors.text.secondary, - fontWeight: '600', - }, - destructiveText: { - color: colors.error.main, - }, -}); - export default AppDialogHost; diff --git a/src/components/common/AppPromptBar.tsx b/src/components/common/AppPromptBar.tsx index d0e8e84..66a94d0 100644 --- a/src/components/common/AppPromptBar.tsx +++ b/src/components/common/AppPromptBar.tsx @@ -1,10 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { bindPromptListener, PromptPayload, PromptType } from '../../services/promptService'; -import { borderRadius, colors, shadows, spacing } from '../../theme'; +import { borderRadius, shadows, spacing, useAppColors, type AppColors } from '../../theme'; interface PromptState extends PromptPayload { id: number; @@ -12,29 +12,96 @@ interface PromptState extends PromptPayload { const DEFAULT_DURATION = 2200; -const styleMap: Record['name'] }> = { - info: { backgroundColor: '#FFFFFF', icon: 'information-outline' }, - success: { backgroundColor: '#FFFFFF', icon: 'check-circle-outline' }, - warning: { backgroundColor: '#FFFFFF', icon: 'alert-outline' }, - error: { backgroundColor: '#FFFFFF', icon: 'alert-circle-outline' }, -}; - -const iconColorMap: Record = { - info: colors.primary.main, - success: colors.success.main, - warning: colors.warning.dark, - error: colors.error.main, -}; - -const accentColorMap: Record = { - info: colors.primary.main, - success: colors.success.main, - warning: colors.warning.main, - error: colors.error.main, -}; +function createPromptBarStyles(colors: AppColors) { + return StyleSheet.create({ + wrapper: { + position: 'absolute', + left: spacing.md, + right: spacing.md, + zIndex: 9999, + }, + card: { + borderRadius: borderRadius.xl, + paddingVertical: spacing.md, + paddingHorizontal: spacing.md, + borderWidth: 1, + borderColor: `${colors.primary.main}22`, + flexDirection: 'row', + alignItems: 'center', + overflow: 'hidden', + ...shadows.lg, + }, + accentBar: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 4, + }, + iconWrap: { + width: 32, + height: 32, + borderRadius: borderRadius.full, + backgroundColor: `${colors.primary.main}12`, + alignItems: 'center', + justifyContent: 'center', + marginRight: spacing.md, + }, + textWrap: { + flex: 1, + paddingRight: spacing.sm, + }, + title: { + color: colors.text.primary, + fontWeight: '700', + fontSize: 14, + marginBottom: 2, + }, + message: { + color: colors.text.primary, + fontSize: 13, + lineHeight: 18, + }, + }); +} const AppPromptBar: React.FC = () => { const insets = useSafeAreaInsets(); + const colors = useAppColors(); + const styles = useMemo(() => createPromptBarStyles(colors), [colors]); + + const styleMap = useMemo< + Record['name'] }> + >( + () => ({ + info: { backgroundColor: colors.background.paper, icon: 'information-outline' }, + success: { backgroundColor: colors.background.paper, icon: 'check-circle-outline' }, + warning: { backgroundColor: colors.background.paper, icon: 'alert-outline' }, + error: { backgroundColor: colors.background.paper, icon: 'alert-circle-outline' }, + }), + [colors.background.paper], + ); + + const iconColorMap = useMemo( + () => ({ + info: colors.primary.main, + success: colors.success.main, + warning: colors.warning.dark, + error: colors.error.main, + }), + [colors.primary.main, colors.success.main, colors.warning.dark, colors.error.main], + ); + + const accentColorMap = useMemo( + () => ({ + info: colors.primary.main, + success: colors.success.main, + warning: colors.warning.main, + error: colors.error.main, + }), + [colors.primary.main, colors.success.main, colors.warning.main, colors.error.main], + ); + const [prompt, setPrompt] = useState(null); const hideTimerRef = useRef | null>(null); const animation = useRef(new Animated.Value(0)).current; @@ -131,55 +198,4 @@ const AppPromptBar: React.FC = () => { ); }; -const styles = StyleSheet.create({ - wrapper: { - position: 'absolute', - left: spacing.md, - right: spacing.md, - zIndex: 9999, - }, - card: { - borderRadius: borderRadius.xl, - paddingVertical: spacing.md, - paddingHorizontal: spacing.md, - borderWidth: 1, - borderColor: `${colors.primary.main}22`, - flexDirection: 'row', - alignItems: 'center', - overflow: 'hidden', - ...shadows.lg, - }, - accentBar: { - position: 'absolute', - left: 0, - top: 0, - bottom: 0, - width: 4, - }, - iconWrap: { - width: 32, - height: 32, - borderRadius: borderRadius.full, - backgroundColor: `${colors.primary.main}12`, - alignItems: 'center', - justifyContent: 'center', - marginRight: spacing.md, - }, - textWrap: { - flex: 1, - paddingRight: spacing.sm, - }, - title: { - color: colors.text.primary, - fontWeight: '700', - fontSize: 14, - marginBottom: 2, - }, - message: { - color: colors.text.primary, - fontSize: 13, - lineHeight: 18, - }, -}); - export default AppPromptBar; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 51090b8..808e88e 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -4,7 +4,7 @@ * 使用 expo-image 实现内存+磁盘双级缓存,头像秒加载 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, TouchableOpacity, @@ -12,7 +12,7 @@ import { ViewStyle, } from 'react-native'; import { Image as ExpoImage } from 'expo-image'; -import { colors, borderRadius } from '../../theme'; +import { borderRadius, useAppColors, type AppColors } from '../../theme'; import Text from './Text'; type AvatarSource = string | { uri: string } | number | null; @@ -27,29 +27,50 @@ interface AvatarProps { style?: ViewStyle; } +function createAvatarStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + position: 'relative', + }, + image: { + backgroundColor: colors.background.disabled, + }, + placeholder: { + backgroundColor: colors.primary.main, + justifyContent: 'center', + alignItems: 'center', + }, + badge: { + position: 'absolute', + borderWidth: 2, + borderColor: colors.background.paper, + }, + }); +} + const Avatar: React.FC = ({ source, size = 40, name, onPress, showBadge = false, - badgeColor = colors.success.main, + badgeColor, style, }) => { - // 获取首字母 + const colors = useAppColors(); + const styles = useMemo(() => createAvatarStyles(colors), [colors]); + const resolvedBadgeColor = badgeColor ?? colors.success.main; + const getInitial = (): string => { if (!name) return '?'; const firstChar = name.charAt(0).toUpperCase(); - // 中文字符 if (/[\u4e00-\u9fa5]/.test(firstChar)) { return firstChar; } return firstChar; }; - // 渲染头像内容 const renderAvatarContent = () => { - // 如果有图片源 if (source) { const imageSource = typeof source === 'string' ? { uri: source } : source; @@ -72,7 +93,6 @@ const Avatar: React.FC = ({ ); } - // 显示首字母 const fontSize = size / 2; return ( = ({ style={[ styles.badge, { - backgroundColor: badgeColor, + backgroundColor: resolvedBadgeColor, width: size * 0.3, height: size * 0.3, borderRadius: (size * 0.3) / 2, @@ -124,23 +144,4 @@ const Avatar: React.FC = ({ return avatarContainer; }; -const styles = StyleSheet.create({ - container: { - position: 'relative', - }, - image: { - backgroundColor: colors.background.disabled, - }, - placeholder: { - backgroundColor: colors.primary.main, - justifyContent: 'center', - alignItems: 'center', - }, - badge: { - position: 'absolute', - borderWidth: 2, - borderColor: colors.background.paper, - }, -}); - export default Avatar; diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index 68bc883..ffab0d6 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -3,7 +3,7 @@ * 支持多种变体、尺寸、加载状态和图标 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { TouchableOpacity, ActivityIndicator, @@ -13,7 +13,7 @@ import { TextStyle, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, borderRadius, spacing, fontSizes, shadows } from '../../theme'; +import { borderRadius, spacing, fontSizes, useAppColors, type AppColors } from '../../theme'; import Text from './Text'; type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'danger'; @@ -27,12 +27,86 @@ interface ButtonProps { size?: ButtonSize; disabled?: boolean; loading?: boolean; - icon?: string; // MaterialCommunityIcons name + icon?: string; iconPosition?: IconPosition; fullWidth?: boolean; style?: ViewStyle; } +function createButtonStyles(colors: AppColors) { + return StyleSheet.create({ + base: { + justifyContent: 'center', + alignItems: 'center', + borderRadius: borderRadius.md, + }, + content: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + size_sm: { + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + minHeight: 32, + }, + size_md: { + paddingVertical: spacing.md, + paddingHorizontal: spacing.lg, + minHeight: 40, + }, + size_lg: { + paddingVertical: spacing.lg, + paddingHorizontal: spacing.xl, + minHeight: 48, + }, + primary: { + backgroundColor: colors.primary.main, + }, + secondary: { + backgroundColor: colors.secondary.main, + }, + outline: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: colors.primary.main, + }, + text: { + backgroundColor: 'transparent', + shadowColor: 'transparent', + elevation: 0, + }, + danger: { + backgroundColor: colors.error.main, + }, + disabled: { + backgroundColor: colors.background.disabled, + borderColor: colors.background.disabled, + }, + fullWidth: { + width: '100%', + }, + textBase: { + fontWeight: '600', + }, + textSize_sm: { + fontSize: fontSizes.sm, + }, + textSize_md: { + fontSize: fontSizes.md, + }, + textSize_lg: { + fontSize: fontSizes.lg, + }, + iconLeft: { + marginRight: spacing.sm, + }, + iconRight: { + marginLeft: spacing.sm, + }, + }); +} + const Button: React.FC = ({ title, onPress, @@ -45,11 +119,12 @@ const Button: React.FC = ({ fullWidth = false, style, }) => { - // 获取按钮样式 + const colors = useAppColors(); + const styles = useMemo(() => createButtonStyles(colors), [colors]); + const getButtonStyle = (): ViewStyle[] => { const baseStyle: ViewStyle[] = [styles.base, styles[`size_${size}`]]; - // 变体样式 switch (variant) { case 'primary': baseStyle.push(styles.primary); @@ -68,12 +143,10 @@ const Button: React.FC = ({ break; } - // 禁用状态 if (disabled || loading) { baseStyle.push(styles.disabled); } - // 全宽度 if (fullWidth) { baseStyle.push(styles.fullWidth); } @@ -81,7 +154,6 @@ const Button: React.FC = ({ return baseStyle; }; - // 获取文本样式 const getTextStyle = (): TextStyle => { const baseStyle: TextStyle = { ...styles.textBase, @@ -109,7 +181,6 @@ const Button: React.FC = ({ return baseStyle; }; - // 获取图标大小 const getIconSize = (): number => { switch (size) { case 'sm': @@ -121,7 +192,6 @@ const Button: React.FC = ({ } }; - // 获取图标颜色 const getIconColor = (): string => { if (disabled || loading) { return colors.text.disabled; @@ -150,9 +220,9 @@ const Button: React.FC = ({ {loading ? ( ) : ( @@ -179,82 +249,4 @@ const Button: React.FC = ({ ); }; -const styles = StyleSheet.create({ - base: { - justifyContent: 'center', - alignItems: 'center', - borderRadius: borderRadius.md, - }, - content: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - // 尺寸样式 - size_sm: { - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - minHeight: 32, - }, - size_md: { - paddingVertical: spacing.md, - paddingHorizontal: spacing.lg, - minHeight: 40, - }, - size_lg: { - paddingVertical: spacing.lg, - paddingHorizontal: spacing.xl, - minHeight: 48, - }, - // 变体样式 - primary: { - backgroundColor: colors.primary.main, - }, - secondary: { - backgroundColor: colors.secondary.main, - }, - outline: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: colors.primary.main, - }, - text: { - backgroundColor: 'transparent', - shadowColor: 'transparent', - elevation: 0, - }, - danger: { - backgroundColor: colors.error.main, - }, - // 禁用状态 - disabled: { - backgroundColor: colors.background.disabled, - borderColor: colors.background.disabled, - }, - // 全宽度 - fullWidth: { - width: '100%', - }, - // 文本样式 - textBase: { - fontWeight: '600', - }, - textSize_sm: { - fontSize: fontSizes.sm, - }, - textSize_md: { - fontSize: fontSizes.md, - }, - textSize_lg: { - fontSize: fontSizes.lg, - }, - // 图标样式 - iconLeft: { - marginRight: spacing.sm, - }, - iconRight: { - marginLeft: spacing.sm, - }, -}); - export default Button; diff --git a/src/components/common/Card.tsx b/src/components/common/Card.tsx index d3f1ca9..42f9573 100644 --- a/src/components/common/Card.tsx +++ b/src/components/common/Card.tsx @@ -3,9 +3,9 @@ * 白色背景、圆角、阴影,支持点击 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'; -import { colors, borderRadius, spacing, shadows } from '../../theme'; +import { borderRadius, spacing, shadows, useAppColors, type AppColors } from '../../theme'; type ShadowSize = 'sm' | 'md' | 'lg'; @@ -13,10 +13,20 @@ interface CardProps { children: React.ReactNode; onPress?: () => void; padding?: number; - shadow?: ShadowSize; + shadow?: ShadowSize | 'none'; style?: ViewStyle; } +function createCardStyles(colors: AppColors) { + return StyleSheet.create({ + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + overflow: 'hidden', + }, + }); +} + const Card: React.FC = ({ children, onPress, @@ -24,13 +34,11 @@ const Card: React.FC = ({ shadow = 'none', style, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createCardStyles(colors), [colors]); const shadowStyle = shadow !== 'none' ? shadows[shadow as keyof typeof shadows] : undefined; - - const cardStyle = [ - styles.card, - { padding }, - shadowStyle, - ].filter(Boolean); + + const cardStyle = [styles.card, { padding }, shadowStyle].filter(Boolean); if (onPress) { return ( @@ -47,12 +55,4 @@ const Card: React.FC = ({ return {children}; }; -const styles = StyleSheet.create({ - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - overflow: 'hidden', - }, -}); - export default Card; diff --git a/src/components/common/Divider.tsx b/src/components/common/Divider.tsx index ee2ffbe..f60cdd1 100644 --- a/src/components/common/Divider.tsx +++ b/src/components/common/Divider.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { View, StyleSheet, ViewStyle } from 'react-native'; -import { colors, spacing } from '../../theme'; +import { spacing, useAppColors } from '../../theme'; interface DividerProps { margin?: number; @@ -13,16 +13,14 @@ interface DividerProps { style?: ViewStyle; } -const Divider: React.FC = ({ - margin = spacing.lg, - color = colors.divider, - style, -}) => { +const Divider: React.FC = ({ margin = spacing.lg, color, style }) => { + const colors = useAppColors(); + const lineColor = color ?? colors.divider; return ( diff --git a/src/components/common/EmptyState.tsx b/src/components/common/EmptyState.tsx index c1e0dfb..3112d94 100644 --- a/src/components/common/EmptyState.tsx +++ b/src/components/common/EmptyState.tsx @@ -3,10 +3,10 @@ * 显示空数据时的占位界面,采用现代插图风格设计 */ -import React from 'react'; -import { View, StyleSheet, ViewStyle, Dimensions } from 'react-native'; +import React, { useMemo } from 'react'; +import { View, StyleSheet, ViewStyle } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import Text from './Text'; import Button from './Button'; @@ -20,6 +20,109 @@ interface EmptyStateProps { variant?: 'default' | 'modern' | 'compact'; } +function createEmptyStateStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: spacing.xl, + }, + icon: { + marginBottom: spacing.lg, + }, + title: { + textAlign: 'center', + marginBottom: spacing.sm, + }, + description: { + textAlign: 'center', + marginBottom: spacing.lg, + }, + button: { + minWidth: 120, + }, + modernContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: spacing.xl, + minHeight: 280, + }, + illustrationContainer: { + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + marginBottom: spacing.xl, + width: 120, + height: 120, + }, + iconBackground: { + width: 100, + height: 100, + borderRadius: borderRadius['2xl'], + backgroundColor: colors.primary.main + '15', + alignItems: 'center', + justifyContent: 'center', + shadowColor: colors.primary.main, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 4, + }, + modernIcon: { + opacity: 0.9, + }, + decorativeCircle1: { + position: 'absolute', + width: 24, + height: 24, + borderRadius: borderRadius.full, + backgroundColor: colors.primary.light + '30', + top: 5, + right: 10, + }, + decorativeCircle2: { + position: 'absolute', + width: 16, + height: 16, + borderRadius: borderRadius.full, + backgroundColor: colors.primary.main + '20', + bottom: 15, + left: 5, + }, + modernTitle: { + textAlign: 'center', + marginBottom: spacing.sm, + fontWeight: '700', + fontSize: fontSizes.xl, + }, + modernDescription: { + textAlign: 'center', + marginBottom: spacing.lg, + fontSize: fontSizes.md, + lineHeight: 22, + maxWidth: 280, + }, + modernButton: { + minWidth: 140, + borderRadius: borderRadius.lg, + }, + compactContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: spacing.lg, + }, + compactIcon: { + marginRight: spacing.sm, + }, + compactTitle: { + fontSize: fontSizes.md, + }, + }); +} + const EmptyState: React.FC = ({ icon = 'folder-open-outline', title, @@ -29,7 +132,9 @@ const EmptyState: React.FC = ({ style, variant = 'modern', }) => { - // 现代风格空状态 + const colors = useAppColors(); + const styles = useMemo(() => createEmptyStateStyles(colors), [colors]); + if (variant === 'modern') { return ( @@ -45,19 +150,11 @@ const EmptyState: React.FC = ({ - + {title} {description && ( - + {description} )} @@ -74,7 +171,6 @@ const EmptyState: React.FC = ({ ); } - // 紧凑风格 if (variant === 'compact') { return ( @@ -84,18 +180,13 @@ const EmptyState: React.FC = ({ color={colors.text.disabled} style={styles.compactIcon} /> - + {title} ); } - // 默认风格 return ( = ({ color={colors.text.disabled} style={styles.icon} /> - + {title} {description && ( - + {description} )} @@ -133,110 +216,4 @@ const EmptyState: React.FC = ({ ); }; -const styles = StyleSheet.create({ - // 默认风格 - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: spacing.xl, - }, - icon: { - marginBottom: spacing.lg, - }, - title: { - textAlign: 'center', - marginBottom: spacing.sm, - }, - description: { - textAlign: 'center', - marginBottom: spacing.lg, - }, - button: { - minWidth: 120, - }, - - // 现代风格 - modernContainer: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: spacing.xl, - minHeight: 280, - }, - illustrationContainer: { - position: 'relative', - alignItems: 'center', - justifyContent: 'center', - marginBottom: spacing.xl, - width: 120, - height: 120, - }, - iconBackground: { - width: 100, - height: 100, - borderRadius: borderRadius['2xl'], - backgroundColor: colors.primary.main + '15', - alignItems: 'center', - justifyContent: 'center', - shadowColor: colors.primary.main, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.2, - shadowRadius: 12, - elevation: 4, - }, - modernIcon: { - opacity: 0.9, - }, - decorativeCircle1: { - position: 'absolute', - width: 24, - height: 24, - borderRadius: borderRadius.full, - backgroundColor: colors.primary.light + '30', - top: 5, - right: 10, - }, - decorativeCircle2: { - position: 'absolute', - width: 16, - height: 16, - borderRadius: borderRadius.full, - backgroundColor: colors.primary.main + '20', - bottom: 15, - left: 5, - }, - modernTitle: { - textAlign: 'center', - marginBottom: spacing.sm, - fontWeight: '700', - fontSize: fontSizes.xl, - }, - modernDescription: { - textAlign: 'center', - marginBottom: spacing.lg, - fontSize: fontSizes.md, - lineHeight: 22, - maxWidth: 280, - }, - modernButton: { - minWidth: 140, - borderRadius: borderRadius.lg, - }, - - // 紧凑风格 - compactContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: spacing.lg, - }, - compactIcon: { - marginRight: spacing.sm, - }, - compactTitle: { - fontSize: fontSizes.md, - }, -}); - export default EmptyState; diff --git a/src/components/common/ImageGallery.tsx b/src/components/common/ImageGallery.tsx index cccd7ca..a4a14ee 100644 --- a/src/components/common/ImageGallery.tsx +++ b/src/components/common/ImageGallery.tsx @@ -31,7 +31,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as MediaLibrary from 'expo-media-library'; import { File, Paths } from 'expo-file-system'; -import { colors, spacing, borderRadius, fontSizes } from '../../theme'; +import { spacing, borderRadius, fontSizes } from '../../theme'; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); diff --git a/src/components/common/ImageGrid.tsx b/src/components/common/ImageGrid.tsx index a96fb9b..f163e8b 100644 --- a/src/components/common/ImageGrid.tsx +++ b/src/components/common/ImageGrid.tsx @@ -5,7 +5,7 @@ * 支持预览图优化 */ -import React, { useMemo, useCallback, useState, useEffect } from 'react'; +import React, { useMemo, useCallback, useState, useEffect, createContext, useContext } from 'react'; import { View, StyleSheet, @@ -16,7 +16,7 @@ import { Image, } from 'react-native'; import { SmartImage } from './SmartImage'; -import { colors, spacing, borderRadius } from '../../theme'; +import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -101,6 +101,89 @@ const calculateGridDimensions = ( }; }; +function createImageGridStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + marginTop: spacing.sm, + }, + fullSize: { + flex: 1, + width: '100%', + height: '100%', + }, + singleContainer: { + overflow: 'hidden', + backgroundColor: colors.background.disabled, + }, + horizontalContainer: { + flexDirection: 'row', + }, + horizontalItem: { + overflow: 'hidden', + backgroundColor: colors.background.disabled, + }, + gridContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + gridItem: { + overflow: 'hidden', + backgroundColor: colors.background.disabled, + aspectRatio: 1, + }, + gridItem2: { + width: '48%', + }, + gridItem3: { + width: '31%', + }, + masonryContainer: { + flexDirection: 'row', + }, + masonryColumn: { + flex: 1, + }, + masonryItem: { + overflow: 'hidden', + backgroundColor: colors.background.disabled, + }, + moreOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + justifyContent: 'center', + alignItems: 'center', + borderRadius: borderRadius.md, + }, + moreText: { + color: colors.text.inverse, + fontSize: fontSizes.xl, + fontWeight: '500', + }, + compactContainer: { + marginTop: spacing.xs, + }, + compactGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + compactItem: { + overflow: 'hidden', + backgroundColor: colors.background.disabled, + }, + }); +} + +type ImageGridStyles = ReturnType; +const ImageGridStylesContext = createContext(null); + +function useImageGridStyles(): ImageGridStyles { + const ctx = useContext(ImageGridStylesContext); + if (!ctx) { + throw new Error('useImageGridStyles must be used within ImageGrid or CompactImageGrid'); + } + return ctx; +} + // ─── 单张图片子组件 ─────────────────────────────────────────────────────────── // 独立成组件,方便用 useState 管理异步加载到的实际尺寸 @@ -123,6 +206,7 @@ const SingleImageItem: React.FC = ({ usePreview, displayMode, }) => { + const styles = useImageGridStyles(); const [aspectRatio, setAspectRatio] = useState(null); const mainUri = image.uri || image.url || ''; const thumbnailUri = @@ -245,7 +329,8 @@ export const ImageGrid: React.FC = ({ displayMode = 'list', usePreview = true, }) => { - // 通过 onLayout 拿到容器实际宽度 + const colors = useAppColors(); + const gridStyles = useMemo(() => createImageGridStyles(colors), [colors]); const [containerWidth, setContainerWidth] = useState(0); // 过滤有效图片 - 支持 uri 或 url 字段 @@ -303,7 +388,7 @@ export const ImageGrid: React.FC = ({ // 渲染横向双图 const renderHorizontal = () => { return ( - + {displayImages.map((image, index) => { const previewUrl = usePreview && displayMode ? getPreviewImageUrl(image as any, displayMode) @@ -314,7 +399,7 @@ export const ImageGrid: React.FC = ({ key={image.id || index} onPress={() => handleImagePress(index)} style={[ - styles.horizontalItem, + gridStyles.horizontalItem, { flex: 1, aspectRatio: 1, @@ -324,7 +409,7 @@ export const ImageGrid: React.FC = ({ > @@ -338,7 +423,7 @@ export const ImageGrid: React.FC = ({ // 渲染网格布局 const renderGrid = () => { return ( - + {displayImages.map((image, index) => { const isLastVisible = index === displayImages.length - 1; const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay; @@ -352,8 +437,8 @@ export const ImageGrid: React.FC = ({ key={image.id || index} onPress={() => handleImagePress(index)} style={[ - styles.gridItem, - gridColumns === 3 ? styles.gridItem3 : styles.gridItem2, + gridStyles.gridItem, + gridColumns === 3 ? gridStyles.gridItem3 : gridStyles.gridItem2, { borderRadius: borderRadiusValue, }, @@ -361,13 +446,13 @@ export const ImageGrid: React.FC = ({ > {showOverlay && ( - - +{remainingCount} + + +{remainingCount} )} @@ -397,7 +482,7 @@ export const ImageGrid: React.FC = ({ const renderColumn = (columnImages: ImageGridItem[], columnIndex: number) => { return ( - + {columnImages.map((image, index) => { const actualIndex = columnIndex + index * 2; const aspectRatio = image.width && image.height @@ -414,7 +499,7 @@ export const ImageGrid: React.FC = ({ key={image.id || actualIndex} onPress={() => handleImagePress(actualIndex)} style={[ - styles.masonryItem, + gridStyles.masonryItem, { width: itemWidth, height: Math.max(height, itemWidth * 0.7), @@ -424,7 +509,7 @@ export const ImageGrid: React.FC = ({ > @@ -436,7 +521,7 @@ export const ImageGrid: React.FC = ({ }; return ( - + {renderColumn(leftColumn, 0)} {renderColumn(rightColumn, 1)} @@ -464,13 +549,15 @@ export const ImageGrid: React.FC = ({ } return ( - setContainerWidth(e.nativeEvent.layout.width)} - > - {renderContent()} - + + setContainerWidth(e.nativeEvent.layout.width)} + > + {renderContent()} + + ); }; @@ -490,6 +577,8 @@ export const CompactImageGrid: React.FC = ({ borderRadius: borderRadiusValue = borderRadius.sm, ...props }) => { + const colors = useAppColors(); + const gridStyles = useMemo(() => createImageGridStyles(colors), [colors]); const containerWidth = maxWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING - 36 - spacing.sm; // 36是头像宽度 const renderCompactGrid = () => { @@ -506,7 +595,7 @@ export const CompactImageGrid: React.FC = ({ props.onImagePress?.(images, 0)} style={[ - styles.compactItem, + gridStyles.compactItem, { width: size, height: size, @@ -516,7 +605,7 @@ export const CompactImageGrid: React.FC = ({ > @@ -529,13 +618,13 @@ export const CompactImageGrid: React.FC = ({ const { itemSize } = calculateGridDimensions(count, containerWidth, gap, columns); return ( - + {images.slice(0, 6).map((image, index) => ( props.onImagePress?.(images, index)} style={[ - styles.compactItem, + gridStyles.compactItem, { width: itemSize, height: itemSize, @@ -545,13 +634,13 @@ export const CompactImageGrid: React.FC = ({ > {index === 5 && images.length > 6 && ( - - +{images.length - 6} + + +{images.length - 6} )} @@ -560,86 +649,11 @@ export const CompactImageGrid: React.FC = ({ ); }; - return {renderCompactGrid()}; + return ( + + {renderCompactGrid()} + + ); }; -const styles = StyleSheet.create({ - container: { - marginTop: spacing.sm, - }, - fullSize: { - flex: 1, - width: '100%', - height: '100%', - }, - // 单图样式 - singleContainer: { - overflow: 'hidden', - backgroundColor: colors.background.disabled, - }, - // 横向布局样式 - horizontalContainer: { - flexDirection: 'row', - }, - horizontalItem: { - overflow: 'hidden', - backgroundColor: colors.background.disabled, - }, - // 网格布局样式 - gridContainer: { - flexDirection: 'row', - flexWrap: 'wrap', - }, - gridItem: { - overflow: 'hidden', - backgroundColor: colors.background.disabled, - aspectRatio: 1, - }, - gridItem2: { - width: '48%', // 2列布局,每列约48%宽度,留有余量避免换行 - }, - gridItem3: { - width: '31%', // 3列布局,每列约31%宽度,留有余量避免换行 - }, - // 瀑布流样式 - masonryContainer: { - flexDirection: 'row', - }, - masonryColumn: { - flex: 1, - }, - masonryItem: { - overflow: 'hidden', - backgroundColor: colors.background.disabled, - }, - // 更多遮罩 - 类似微博的灰色蒙版 - moreOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.4)', - justifyContent: 'center', - alignItems: 'center', - borderRadius: borderRadius.md, - }, - moreText: { - color: colors.text.inverse, - fontSize: fontSizes.xl, - fontWeight: '500', - }, - // 紧凑模式样式 - compactContainer: { - marginTop: spacing.xs, - }, - compactGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - }, - compactItem: { - overflow: 'hidden', - backgroundColor: colors.background.disabled, - }, -}); - -// 导入字体大小 -import { fontSizes } from '../../theme'; - export default ImageGrid; diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx index a4f958c..9d8229e 100644 --- a/src/components/common/Input.tsx +++ b/src/components/common/Input.tsx @@ -3,7 +3,7 @@ * 支持标签、错误提示、图标、多行输入等 */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { View, TextInput, @@ -13,7 +13,7 @@ import { TextStyle, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, borderRadius, spacing, fontSizes } from '../../theme'; +import { borderRadius, spacing, fontSizes, useAppColors, type AppColors } from '../../theme'; import Text from './Text'; interface InputProps { @@ -36,6 +36,46 @@ interface InputProps { autoCorrect?: boolean; } +function createInputStyles(colors: AppColors) { + return StyleSheet.create({ + wrapper: { + width: '100%', + }, + label: { + marginBottom: spacing.xs, + }, + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderWidth: 1, + borderRadius: borderRadius.md, + paddingHorizontal: spacing.md, + borderColor: colors.divider, + }, + input: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + paddingVertical: spacing.md, + minHeight: 44, + }, + multilineInput: { + textAlignVertical: 'top', + minHeight: 100, + }, + leftIcon: { + marginRight: spacing.sm, + }, + rightIcon: { + marginLeft: spacing.sm, + }, + error: { + marginTop: spacing.xs, + }, + }); +} + const Input: React.FC = ({ value, onChangeText, @@ -55,6 +95,8 @@ const Input: React.FC = ({ keyboardType = 'default', autoCorrect = true, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createInputStyles(colors), [colors]); const [isFocused, setIsFocused] = useState(false); const getBorderColor = () => { @@ -87,11 +129,7 @@ const Input: React.FC = ({ /> )} = ({ autoCapitalize={autoCapitalize} keyboardType={keyboardType} autoCorrect={autoCorrect} - // 确保光标可见 cursorColor={colors.primary.main} selectionColor={`${colors.primary.main}40`} /> {rightIcon && ( - + = ({ ); }; -const styles = StyleSheet.create({ - wrapper: { - width: '100%', - }, - label: { - marginBottom: spacing.xs, - }, - container: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderWidth: 1, - borderRadius: borderRadius.md, - paddingHorizontal: spacing.md, - // 减少非焦点状态的边框明显度 - borderColor: colors.divider, - }, - input: { - flex: 1, - fontSize: fontSizes.md, - color: colors.text.primary, - paddingVertical: spacing.md, - minHeight: 44, - }, - multilineInput: { - textAlignVertical: 'top', - minHeight: 100, - }, - leftIcon: { - marginRight: spacing.sm, - }, - rightIcon: { - marginLeft: spacing.sm, - }, - error: { - marginTop: spacing.xs, - }, -}); - export default Input; diff --git a/src/components/common/Loading.tsx b/src/components/common/Loading.tsx index 36a627c..f0caeb5 100644 --- a/src/components/common/Loading.tsx +++ b/src/components/common/Loading.tsx @@ -3,9 +3,9 @@ * 支持不同尺寸、全屏模式 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native'; -import { colors } from '../../theme'; +import { useAppColors, type AppColors } from '../../theme'; type LoadingSize = 'sm' | 'md' | 'lg'; @@ -16,12 +16,32 @@ interface LoadingProps { style?: ViewStyle; } +function createLoadingStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + padding: 20, + alignItems: 'center', + justifyContent: 'center', + }, + fullScreen: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.background.default, + }, + }); +} + const Loading: React.FC = ({ size = 'md', - color = colors.primary.main, + color, fullScreen = false, style, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createLoadingStyles(colors), [colors]); + const indicatorColor = color ?? colors.primary.main; + const getSize = (): 'small' | 'large' | undefined => { switch (size) { case 'sm': @@ -36,30 +56,16 @@ const Loading: React.FC = ({ if (fullScreen) { return ( - + ); } return ( - + ); }; -const styles = StyleSheet.create({ - container: { - padding: 20, - alignItems: 'center', - justifyContent: 'center', - }, - fullScreen: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.background.default, - }, -}); - export default Loading; diff --git a/src/components/common/SearchBar.tsx b/src/components/common/SearchBar.tsx new file mode 100644 index 0000000..0d66df7 --- /dev/null +++ b/src/components/common/SearchBar.tsx @@ -0,0 +1,137 @@ +/** + * SearchBar 搜索栏组件 + * 用于搜索内容 + */ + +import React, { useMemo, useState } from 'react'; +import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; + +interface SearchBarProps { + value: string; + onChangeText: (text: string) => void; + onSubmit: () => void; + placeholder?: string; + onFocus?: () => void; + onBlur?: () => void; + autoFocus?: boolean; +} + +function createSearchBarStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.full, + paddingHorizontal: spacing.xs, + height: 46, + borderWidth: 1, + borderColor: colors.divider, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + containerFocused: { + borderColor: colors.primary.main, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + searchIconWrap: { + width: 30, + height: 30, + marginLeft: spacing.xs, + marginRight: spacing.xs, + borderRadius: borderRadius.full, + backgroundColor: `${colors.text.secondary}12`, + alignItems: 'center', + justifyContent: 'center', + }, + searchIconWrapFocused: { + backgroundColor: `${colors.primary.main}1A`, + }, + input: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + paddingVertical: spacing.sm + 1, + paddingHorizontal: spacing.xs, + }, + clearButton: { + width: 24, + height: 24, + marginHorizontal: spacing.xs, + borderRadius: borderRadius.full, + backgroundColor: `${colors.text.secondary}14`, + alignItems: 'center', + justifyContent: 'center', + }, + }); +} + +const SearchBar: React.FC = ({ + value, + onChangeText, + onSubmit, + placeholder = '搜索...', + onFocus, + onBlur, + autoFocus = false, +}) => { + const colors = useAppColors(); + const styles = useMemo(() => createSearchBarStyles(colors), [colors]); + const [isFocused, setIsFocused] = useState(false); + + const handleFocus = () => { + setIsFocused(true); + onFocus?.(); + }; + + const handleBlur = () => { + setIsFocused(false); + onBlur?.(); + }; + + return ( + + + + + + {value.length > 0 && ( + onChangeText('')} + style={styles.clearButton} + activeOpacity={0.7} + > + + + )} + + ); +}; + +export default SearchBar; diff --git a/src/components/common/SmartImage.tsx b/src/components/common/SmartImage.tsx index c2a089a..ed315f8 100644 --- a/src/components/common/SmartImage.tsx +++ b/src/components/common/SmartImage.tsx @@ -4,7 +4,7 @@ * 基于 expo-image 封装,原生支持 GIF/WebP 动图 */ -import React, { useState, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { View, StyleSheet, @@ -17,7 +17,36 @@ import { } from 'react-native'; import { Image as ExpoImage } from 'expo-image'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, borderRadius } from '../../theme'; +import { borderRadius, useAppColors, type AppColors } from '../../theme'; + +function createSmartImageStyles(colors: AppColors) { + return StyleSheet.create({ + image: { + flex: 1, + width: '100%', + height: '100%', + }, + variantImage: { + flex: 1, + }, + overlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background.disabled, + }, + loadingContainer: { + padding: 8, + borderRadius: borderRadius.md, + backgroundColor: `${colors.background.paper}E6`, + }, + errorContainer: { + padding: 12, + borderRadius: borderRadius.md, + backgroundColor: `${colors.text.primary}0D`, + }, + }); +} // 图片加载状态 export type ImageLoadState = 'loading' | 'success' | 'error'; @@ -91,6 +120,8 @@ export const SmartImage: React.FC = ({ lazyRootMargin = '160px', cachePolicy = 'memory-disk', }) => { + const colors = useAppColors(); + const styles = useMemo(() => createSmartImageStyles(colors), [colors]); const [loadState, setLoadState] = useState('loading'); const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web'); const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false); @@ -244,7 +275,7 @@ export const SmartImage: React.FC = ({ ); @@ -328,36 +359,15 @@ export const VariantImage: React.FC = ({ ); }; -const styles = StyleSheet.create({ - image: { - flex: 1, - width: '100%', - height: '100%', - }, +const variantStyles = StyleSheet.create({ variantImage: { flex: 1, }, - overlay: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: colors.background.disabled, - }, - loadingContainer: { - padding: 8, - borderRadius: borderRadius.md, - backgroundColor: 'rgba(255, 255, 255, 0.9)', - }, - errorContainer: { - padding: 12, - borderRadius: borderRadius.md, - backgroundColor: 'rgba(0, 0, 0, 0.05)', - }, }); export default SmartImage; diff --git a/src/components/common/Text.tsx b/src/components/common/Text.tsx index 81591cc..8870a08 100644 --- a/src/components/common/Text.tsx +++ b/src/components/common/Text.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { Text as RNText, TextProps, StyleSheet, TextStyle, ViewStyle } from 'react-native'; -import { colors, fontSizes } from '../../theme'; +import { useAppColors, fontSizes } from '../../theme'; type TextVariant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'label'; @@ -60,6 +60,7 @@ const Text: React.FC = ({ style, ...props }) => { + const colors = useAppColors(); const textStyle = [ styles.base, variantStyles[variant], diff --git a/src/screens/apps/AppsScreen.tsx b/src/screens/apps/AppsScreen.tsx index 88dad4b..4c4da95 100644 --- a/src/screens/apps/AppsScreen.tsx +++ b/src/screens/apps/AppsScreen.tsx @@ -2,13 +2,21 @@ * 应用中心:与首页顶栏、个人主页帖子卡片同一套圆角 / 阴影 / 主色体系 */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { View, StyleSheet, ScrollView, TouchableOpacity, StatusBar } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { + spacing, + fontSizes, + borderRadius, + shadows, + useAppColors, + useResolvedColorScheme, + type AppColors, +} from '../../theme'; import * as hrefs from '../../navigation/hrefs'; import { Text, ResponsiveContainer } from '../../components/common'; import { useResponsive, useResponsiveSpacing, useBreakpointGTE } from '../../hooks/useResponsive'; @@ -31,20 +39,26 @@ const APP_ENTRIES: AppItem[] = [ }, ]; -/** 与个人主页 PostCard 外层 `postWrapper` 一致 */ -const postCardShell = { - marginBottom: spacing.md, - backgroundColor: colors.background.paper, - borderRadius: 16, - overflow: 'hidden' as const, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.06, - shadowRadius: 8, - elevation: 2, -}; - export const AppsScreen: React.FC = () => { + const colors = useAppColors(); + const resolvedScheme = useResolvedColorScheme(); + const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content'; + const styles = useMemo(() => createAppsStyles(colors), [colors]); + /** 与个人主页 PostCard 外层 `postWrapper` 一致 */ + const postCardShell = useMemo( + () => ({ + marginBottom: spacing.md, + backgroundColor: colors.background.paper, + borderRadius: 16, + overflow: 'hidden' as const, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, + }), + [colors] + ); const router = useRouter(); const insets = useSafeAreaInsets(); const { isMobile } = useResponsive(); @@ -100,7 +114,7 @@ export const AppsScreen: React.FC = () => { return ( - + {/* 与 MessageListScreen 顶栏同一结构 / 样式 */} @@ -143,93 +157,96 @@ export const AppsScreen: React.FC = () => { export default AppsScreen; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - msgHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - backgroundColor: '#FAFAFA', - ...shadows.sm, - }, - msgHeaderWide: { - paddingHorizontal: spacing.lg, - paddingVertical: spacing.lg, - }, - msgHeaderLeft: { - width: 44, - }, - msgHeaderCenter: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - }, - msgHeaderTitle: { - fontSize: 19, - fontWeight: '700', - color: '#333', - }, - msgHeaderTitleWide: { - fontSize: 22, - }, - msgHeaderRight: { - flexDirection: 'row', - alignItems: 'center', - }, - /** 与消息页「+」按钮同占位宽度,标题视觉居中 */ - msgHeaderRightSpacer: { - width: 36, - height: 44, - }, - pageSubtitle: { - marginBottom: spacing.md, - lineHeight: fontSizes.sm * 1.45, - }, - /** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */ - scroll: { - flex: 1, - backgroundColor: '#FAFAFA', - }, - scrollContent: { - paddingTop: spacing.md, - flexGrow: 1, - backgroundColor: '#FAFAFA', - }, - appCardInner: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.lg, - paddingHorizontal: spacing.lg, - }, - /** 与首页发帖 FAB 同主色实心圆 */ - appIconCircle: { - width: 52, - height: 52, - borderRadius: borderRadius.full, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - }, - appCardText: { - flex: 1, - marginLeft: spacing.md, - marginRight: spacing.sm, - }, - appTitle: { - fontWeight: '700', - fontSize: fontSizes.md + 1, - }, - appSubtitle: { - marginTop: 4, - }, - footer: { - alignItems: 'center', - paddingTop: spacing.xl, - paddingBottom: spacing.md, - }, -}); +function createAppsStyles(colors: AppColors) { + const headerBg = colors.background.default; + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + msgHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + backgroundColor: headerBg, + ...shadows.sm, + }, + msgHeaderWide: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.lg, + }, + msgHeaderLeft: { + width: 44, + }, + msgHeaderCenter: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + msgHeaderTitle: { + fontSize: 19, + fontWeight: '700', + color: colors.text.primary, + }, + msgHeaderTitleWide: { + fontSize: 22, + }, + msgHeaderRight: { + flexDirection: 'row', + alignItems: 'center', + }, + /** 与消息页「+」按钮同占位宽度,标题视觉居中 */ + msgHeaderRightSpacer: { + width: 36, + height: 44, + }, + pageSubtitle: { + marginBottom: spacing.md, + lineHeight: fontSizes.sm * 1.45, + }, + /** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */ + scroll: { + flex: 1, + backgroundColor: headerBg, + }, + scrollContent: { + paddingTop: spacing.md, + flexGrow: 1, + backgroundColor: headerBg, + }, + appCardInner: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.lg, + paddingHorizontal: spacing.lg, + }, + /** 与首页发帖 FAB 同主色实心圆 */ + appIconCircle: { + width: 52, + height: 52, + borderRadius: borderRadius.full, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + }, + appCardText: { + flex: 1, + marginLeft: spacing.md, + marginRight: spacing.sm, + }, + appTitle: { + fontWeight: '700', + fontSize: fontSizes.md + 1, + }, + appSubtitle: { + marginTop: 4, + }, + footer: { + alignItems: 'center', + paddingTop: spacing.xl, + paddingBottom: spacing.md, + }, + }); +} diff --git a/src/screens/auth/ForgotPasswordScreen.tsx b/src/screens/auth/ForgotPasswordScreen.tsx index e870cc1..fe092de 100644 --- a/src/screens/auth/ForgotPasswordScreen.tsx +++ b/src/screens/auth/ForgotPasswordScreen.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { View, Text, @@ -14,11 +14,117 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; +import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { showPrompt } from '../../services/promptService'; +function createForgotPasswordStyles(colors: AppColors) { + return StyleSheet.create({ + container: { flex: 1 }, + gradient: { flex: 1 }, + keyboardView: { flex: 1 }, + scrollContent: { + flexGrow: 1, + justifyContent: 'center', + paddingHorizontal: spacing.xl, + paddingVertical: spacing['2xl'], + }, + formCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius['2xl'], + padding: spacing.xl, + ...shadows.md, + }, + title: { + fontSize: fontSizes['2xl'], + fontWeight: '700', + color: colors.text.primary, + textAlign: 'center', + marginBottom: spacing.xs, + }, + subtitle: { + fontSize: fontSizes.sm, + color: colors.text.secondary, + textAlign: 'center', + marginBottom: spacing.lg, + }, + inputWrapper: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.default, + borderRadius: borderRadius.lg, + borderWidth: 1.5, + borderColor: colors.divider, + paddingHorizontal: spacing.md, + height: 52, + marginBottom: spacing.md, + }, + inputIcon: { + marginRight: spacing.sm, + }, + input: { + flex: 1, + fontSize: fontSizes.md, + color: colors.text.primary, + }, + codeRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + marginBottom: spacing.md, + }, + codeInput: { + flex: 1, + marginBottom: 0, + }, + sendCodeButton: { + height: 52, + minWidth: 110, + borderRadius: borderRadius.lg, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: spacing.md, + }, + sendCodeButtonDisabled: { + opacity: 0.6, + }, + sendCodeButtonText: { + color: '#fff', + fontSize: fontSizes.sm, + fontWeight: '600', + }, + submitButton: { + height: 50, + borderRadius: borderRadius.lg, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + marginTop: spacing.sm, + }, + submitButtonDisabled: { + opacity: 0.6, + }, + submitButtonText: { + color: '#fff', + fontSize: fontSizes.lg, + fontWeight: '700', + }, + backButton: { + alignItems: 'center', + marginTop: spacing.md, + }, + backButtonText: { + color: colors.primary.main, + fontSize: fontSizes.md, + fontWeight: '500', + }, + }); +} + export const ForgotPasswordScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]); const router = useRouter(); const [email, setEmail] = useState(''); const [verificationCode, setVerificationCode] = useState(''); @@ -199,106 +305,4 @@ export const ForgotPasswordScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { flex: 1 }, - gradient: { flex: 1 }, - keyboardView: { flex: 1 }, - scrollContent: { - flexGrow: 1, - justifyContent: 'center', - paddingHorizontal: spacing.xl, - paddingVertical: spacing['2xl'], - }, - formCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius['2xl'], - padding: spacing.xl, - ...shadows.md, - }, - title: { - fontSize: fontSizes['2xl'], - fontWeight: '700', - color: colors.text.primary, - textAlign: 'center', - marginBottom: spacing.xs, - }, - subtitle: { - fontSize: fontSizes.sm, - color: colors.text.secondary, - textAlign: 'center', - marginBottom: spacing.lg, - }, - inputWrapper: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.default, - borderRadius: borderRadius.lg, - borderWidth: 1.5, - borderColor: colors.divider, - paddingHorizontal: spacing.md, - height: 52, - marginBottom: spacing.md, - }, - inputIcon: { - marginRight: spacing.sm, - }, - input: { - flex: 1, - fontSize: fontSizes.md, - color: colors.text.primary, - }, - codeRow: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - marginBottom: spacing.md, - }, - codeInput: { - flex: 1, - marginBottom: 0, - }, - sendCodeButton: { - height: 52, - minWidth: 110, - borderRadius: borderRadius.lg, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: spacing.md, - }, - sendCodeButtonDisabled: { - opacity: 0.6, - }, - sendCodeButtonText: { - color: '#fff', - fontSize: fontSizes.sm, - fontWeight: '600', - }, - submitButton: { - height: 50, - borderRadius: borderRadius.lg, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - marginTop: spacing.sm, - }, - submitButtonDisabled: { - opacity: 0.6, - }, - submitButtonText: { - color: '#fff', - fontSize: fontSizes.lg, - fontWeight: '700', - }, - backButton: { - alignItems: 'center', - marginTop: spacing.md, - }, - backButtonText: { - color: colors.primary.main, - fontSize: fontSizes.md, - fontWeight: '500', - }, -}); - export default ForgotPasswordScreen; diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index 9f3c3b0..9443b65 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -7,7 +7,7 @@ * - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰 */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, @@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; +import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import * as hrefs from '../../navigation/hrefs'; import { useResponsive, useResponsiveValue } from '../../hooks'; @@ -35,6 +35,8 @@ import { showPrompt } from '../../services/promptService'; const SPLIT_BREAKPOINT = 768; export const LoginScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createLoginStyles(colors), [colors]); const router = useRouter(); const login = useAuthStore((state) => state.login); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); @@ -453,7 +455,8 @@ export const LoginScreen: React.FC = () => { return isSplitLayout ? renderSplitLayout() : renderSingleLayout(); }; -const styles = StyleSheet.create({ +function createLoginStyles(colors: AppColors) { + return StyleSheet.create({ container: { flex: 1, }, @@ -762,6 +765,7 @@ const styles = StyleSheet.create({ width: '100%', // 移除这里的阴影,避免重复 }, -}); + }); +} export default LoginScreen; \ No newline at end of file diff --git a/src/screens/auth/QRCodeConfirmScreen.tsx b/src/screens/auth/QRCodeConfirmScreen.tsx index 6c25b09..dfe9e94 100644 --- a/src/screens/auth/QRCodeConfirmScreen.tsx +++ b/src/screens/auth/QRCodeConfirmScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { View, Text, @@ -12,11 +12,148 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { qrcodeApi } from '../../services/authService'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { AppBackButton } from '../../components/common'; +function createQrCodeConfirmStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + closeButton: { + padding: spacing.sm, + }, + headerTitle: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: colors.text.primary, + }, + placeholder: { + width: 40, + }, + content: { + flex: 1, + padding: spacing.lg, + justifyContent: 'center', + }, + infoCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.xl, + alignItems: 'center', + marginBottom: spacing.xl, + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + infoText: { + fontSize: fontSizes.md, + color: colors.text.secondary, + marginBottom: spacing.lg, + }, + userInfo: { + alignItems: 'center', + }, + avatar: { + width: 80, + height: 80, + borderRadius: 40, + marginBottom: spacing.md, + }, + avatarPlaceholder: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.background.disabled, + justifyContent: 'center', + alignItems: 'center', + marginBottom: spacing.md, + }, + nickname: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: colors.text.primary, + }, + buttonContainer: { + gap: spacing.md, + }, + button: { + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + borderRadius: borderRadius.md, + alignItems: 'center', + }, + confirmButton: { + backgroundColor: colors.primary.main, + }, + confirmButtonText: { + color: colors.text.inverse, + fontSize: fontSizes.md, + fontWeight: '600', + }, + cancelButton: { + backgroundColor: colors.background.paper, + borderWidth: 1, + borderColor: colors.divider, + }, + cancelButtonText: { + color: colors.text.secondary, + fontSize: fontSizes.md, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: spacing.md, + fontSize: fontSizes.md, + color: colors.text.secondary, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + errorText: { + marginTop: spacing.md, + fontSize: fontSizes.md, + color: colors.text.secondary, + textAlign: 'center', + }, + backButton: { + marginTop: spacing.xl, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + backgroundColor: colors.primary.main, + borderRadius: borderRadius.md, + }, + backButtonText: { + color: colors.text.inverse, + fontSize: fontSizes.md, + fontWeight: '600', + }, + }); +} + export const QRCodeConfirmScreen: React.FC = () => { const router = useRouter(); + const colors = useAppColors(); + const styles = useMemo(() => createQrCodeConfirmStyles(colors), [colors]); const { sessionId: sessionIdParam } = useLocalSearchParams<{ sessionId?: string }>(); const sessionId = sessionIdParam || ''; @@ -37,9 +174,7 @@ export const QRCodeConfirmScreen: React.FC = () => { } catch (err: any) { const errorMsg = err.response?.data?.message || '扫码失败'; setError(errorMsg); - Alert.alert('扫码失败', errorMsg, [ - { text: '确定', onPress: () => router.back() } - ]); + Alert.alert('扫码失败', errorMsg, [{ text: '确定', onPress: () => router.back() }]); } finally { setLoading(false); } @@ -49,9 +184,7 @@ export const QRCodeConfirmScreen: React.FC = () => { try { setLoading(true); await qrcodeApi.confirm(sessionId); - Alert.alert('登录成功', '网页端已登录', [ - { text: '确定', onPress: () => router.back() } - ]); + Alert.alert('登录成功', '网页端已登录', [{ text: '确定', onPress: () => router.back() }]); } catch (err: any) { const errorMsg = err.response?.data?.message || '确认登录失败'; Alert.alert('登录失败', errorMsg); @@ -63,7 +196,7 @@ export const QRCodeConfirmScreen: React.FC = () => { const handleCancel = async () => { try { await qrcodeApi.cancel(sessionId); - } catch (err) { + } catch { // 忽略取消错误 } router.back(); @@ -97,7 +230,12 @@ export const QRCodeConfirmScreen: React.FC = () => { return ( - + 确认登录 @@ -105,14 +243,14 @@ export const QRCodeConfirmScreen: React.FC = () => { 您正在登录网页端 - + {userInfo && ( {userInfo.avatar ? ( ) : ( - + )} {userInfo.nickname} @@ -127,7 +265,7 @@ export const QRCodeConfirmScreen: React.FC = () => { disabled={loading} > {loading ? ( - + ) : ( 确认登录 )} @@ -146,137 +284,4 @@ export const QRCodeConfirmScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - closeButton: { - padding: spacing.sm, - }, - headerTitle: { - fontSize: fontSizes.lg, - fontWeight: '600', - color: '#333', - }, - placeholder: { - width: 40, - }, - content: { - flex: 1, - padding: spacing.lg, - justifyContent: 'center', - }, - infoCard: { - backgroundColor: '#fff', - borderRadius: borderRadius.lg, - padding: spacing.xl, - alignItems: 'center', - marginBottom: spacing.xl, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - infoText: { - fontSize: fontSizes.md, - color: '#666', - marginBottom: spacing.lg, - }, - userInfo: { - alignItems: 'center', - }, - avatar: { - width: 80, - height: 80, - borderRadius: 40, - marginBottom: spacing.md, - }, - avatarPlaceholder: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#f0f0f0', - justifyContent: 'center', - alignItems: 'center', - marginBottom: spacing.md, - }, - nickname: { - fontSize: fontSizes.lg, - fontWeight: '600', - color: '#333', - }, - buttonContainer: { - gap: spacing.md, - }, - button: { - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - borderRadius: borderRadius.md, - alignItems: 'center', - }, - confirmButton: { - backgroundColor: colors.primary.main, - }, - confirmButtonText: { - color: '#fff', - fontSize: fontSizes.md, - fontWeight: '600', - }, - cancelButton: { - backgroundColor: '#fff', - borderWidth: 1, - borderColor: '#ddd', - }, - cancelButtonText: { - color: '#666', - fontSize: fontSizes.md, - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: spacing.md, - fontSize: fontSizes.md, - color: '#666', - }, - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: spacing.xl, - }, - errorText: { - marginTop: spacing.md, - fontSize: fontSizes.md, - color: '#666', - textAlign: 'center', - }, - backButton: { - marginTop: spacing.xl, - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - backgroundColor: colors.primary.main, - borderRadius: borderRadius.md, - }, - backButtonText: { - color: '#fff', - fontSize: fontSizes.md, - fontWeight: '600', - }, -}); - -export default QRCodeConfirmScreen; \ No newline at end of file +export default QRCodeConfirmScreen; diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index fc5ff3d..19c0b90 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -7,7 +7,7 @@ * - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰 */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, @@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; +import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { useAuthStore } from '../../stores'; import * as hrefs from '../../navigation/hrefs'; @@ -36,6 +36,8 @@ import { showPrompt } from '../../services/promptService'; const SPLIT_BREAKPOINT = 768; export const RegisterScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createRegisterStyles(colors), [colors]); const router = useRouter(); const register = useAuthStore((state) => state.register); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); @@ -739,7 +741,8 @@ export const RegisterScreen: React.FC = () => { return isSplitLayout ? renderSplitLayout() : renderSingleLayout(); }; -const styles = StyleSheet.create({ +function createRegisterStyles(colors: AppColors) { + return StyleSheet.create({ container: { flex: 1, }, @@ -1076,6 +1079,7 @@ const styles = StyleSheet.create({ marginBottom: spacing.xl, textAlign: 'center', }, -}); + }); +} export default RegisterScreen; diff --git a/src/screens/create/CreatePostScreen.tsx b/src/screens/create/CreatePostScreen.tsx index 906c5d8..b8ae737 100644 --- a/src/screens/create/CreatePostScreen.tsx +++ b/src/screens/create/CreatePostScreen.tsx @@ -7,7 +7,7 @@ * 投票编辑器在宽屏下优化布局 */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { View, ScrollView, @@ -25,7 +25,14 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams, useNavigation } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { + spacing, + fontSizes, + borderRadius, + shadows, + useAppColors, + type AppColors, +} from '../../theme'; import { Text, ResponsiveContainer } from '../../components/common'; import { channelService, postService, showPrompt, voteService } from '../../services'; import { ApiError } from '../../services/api'; @@ -84,6 +91,8 @@ const getPublishErrorMessage = (error: unknown): string => { }; export const CreatePostScreen: React.FC = (props) => { + const colors = useAppColors(); + const styles = useMemo(() => createCreatePostStyles(colors), [colors]); const { onClose } = props; const router = useRouter(); const navigation = useNavigation(); @@ -805,7 +814,8 @@ export const CreatePostScreen: React.FC = (props) => { ); }; -const styles = StyleSheet.create({ +function createCreatePostStyles(colors: AppColors) { + return StyleSheet.create({ flex: { flex: 1, }, @@ -1050,6 +1060,7 @@ const styles = StyleSheet.create({ loadingText: { fontSize: fontSizes.md, }, -}); + }); +} export default CreatePostScreen; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 0687598..05e76a7 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -24,7 +24,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useRouter, useFocusEffect } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme'; import { Post } from '../../types'; import { useUserStore, useHomeTabBarVisibilityStore } from '../../stores'; import { useCurrentUser } from '../../stores/authStore'; @@ -56,12 +56,125 @@ type ViewMode = 'list' | 'grid'; type PostType = 'follow' | 'hot' | 'latest'; type LatestCapsule = { id: string; name: string }; +function createHomeStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + header: { + backgroundColor: colors.background.paper, + }, + searchWrapper: { + paddingTop: spacing.lg, + paddingBottom: spacing.sm, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + homeTabBar: { + marginTop: spacing.xs, + marginBottom: spacing.sm, + }, + viewToggleBtn: { + width: 44, + height: 44, + alignItems: 'center', + justifyContent: 'center', + }, + capsuleWrapper: { + paddingBottom: spacing.sm, + backgroundColor: colors.background.paper, + }, + capsuleList: { + gap: spacing.xs, + paddingRight: spacing.lg, + }, + capsuleItem: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: 999, + }, + capsuleText: { + fontSize: 13, + fontWeight: '600', + color: colors.text.secondary, + }, + capsuleTextActive: { + color: colors.primary.main, + }, + listContent: { + flexGrow: 1, + }, + listHeader: { + width: '100%', + }, + contentContainer: { + flex: 1, + }, + listItem: {}, + waterfallScroll: { + flex: 1, + }, + waterfallContainer: { + width: '100%', + flexGrow: 1, + }, + waterfallColumnsRow: { + width: '100%', + flexDirection: 'row', + alignItems: 'flex-start', + }, + waterfallColumn: { + flex: 1, + }, + waterfallItem: {}, + floatingButton: { + position: 'absolute', + right: 20, + bottom: 20, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + ...shadows.lg, + }, + floatingButtonDesktop: { + right: 40, + bottom: 40, + width: 64, + height: 64, + borderRadius: 32, + }, + floatingButtonWide: { + right: 60, + bottom: 60, + width: 72, + height: 72, + borderRadius: 36, + }, + loadingMoreFooter: { + paddingVertical: 20, + alignItems: 'center', + justifyContent: 'center', + }, + }); +} + export const HomeScreen: React.FC = () => { const router = useRouter(); const insets = useSafeAreaInsets(); const { posts: storePosts } = useUserStore(); const currentUser = useCurrentUser(); - + const colors = useAppColors(); + const resolvedScheme = useResolvedColorScheme(); + const styles = useMemo(() => createHomeStyles(colors), [colors]); + const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content'; + // 使用响应式 hook const { width, @@ -827,7 +940,7 @@ export const HomeScreen: React.FC = () => { if (showSearch) { return ( - + setShowSearch(false)} /> @@ -838,8 +951,8 @@ export const HomeScreen: React.FC = () => { // 正常首页内容 return ( - - + + {/* 顶部Header */} {/* 搜索栏 */} @@ -942,115 +1055,3 @@ export const HomeScreen: React.FC = () => { ); }; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - header: { - backgroundColor: colors.background.paper, - }, - searchWrapper: { - paddingTop: spacing.lg, - paddingBottom: spacing.xs, - // 移除阴影效果 - shadowColor: 'transparent', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0, - shadowRadius: 0, - elevation: 0, - }, - homeTabBar: { - marginTop: 0, - marginBottom: spacing.sm, - }, - viewToggleBtn: { - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - capsuleWrapper: { - paddingBottom: spacing.sm, - backgroundColor: colors.background.paper, - }, - capsuleList: { - gap: spacing.xs, - paddingRight: spacing.lg, - }, - capsuleItem: { - paddingHorizontal: spacing.md, - paddingVertical: spacing.xs, - borderRadius: 999, - }, - capsuleText: { - fontSize: 13, - fontWeight: '600', - color: colors.text.secondary, - }, - capsuleTextActive: { - color: colors.primary.main, - }, - listContent: { - flexGrow: 1, - }, - listHeader: { - width: '100%', - }, - contentContainer: { - flex: 1, - }, - listItem: { - // 动态设置 marginBottom - }, - waterfallScroll: { - flex: 1, - }, - waterfallContainer: { - width: '100%', - flexGrow: 1, - }, - waterfallColumnsRow: { - width: '100%', - flexDirection: 'row', - alignItems: 'flex-start', - }, - waterfallColumn: { - flex: 1, - }, - waterfallItem: { - // 动态设置 marginBottom - }, - floatingButton: { - position: 'absolute', - right: 20, - bottom: 20, - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - ...shadows.lg, - }, - floatingButtonDesktop: { - right: 40, - bottom: 40, - width: 64, - height: 64, - borderRadius: 32, - }, - floatingButtonWide: { - right: 60, - bottom: 60, - width: 72, - height: 72, - borderRadius: 36, - }, - loadingMoreFooter: { - paddingVertical: 20, - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 2db3174..630058c 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -26,7 +26,13 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { + spacing, + fontSizes, + borderRadius, + useAppColors, + type AppColors, +} from '../../theme'; import { Post, Comment, VoteResultDTO } from '../../types'; import { useUserStore } from '../../stores'; import { useCurrentUser } from '../../stores/authStore'; @@ -39,6 +45,8 @@ import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../h import * as hrefs from '../../navigation/hrefs'; export const PostDetailScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createPostDetailStyles(colors), [colors]); const navigation = useNavigation(); const router = useRouter(); const insets = useSafeAreaInsets(); @@ -290,6 +298,9 @@ export const PostDetailScreen: React.FC = () => { }; navigation.setOptions({ + // 与页面容器 background.default 一致,并去掉原生 header 底部分隔阴影,避免「一条线」 + headerStyle: { backgroundColor: colors.background.default }, + headerShadowVisible: false, headerBackVisible: false, headerTitleAlign: 'left', headerLeft: () => ( @@ -324,7 +335,7 @@ export const PostDetailScreen: React.FC = () => { ), }); - }, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation]); + }, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation, colors.background.default, styles]); // 监听键盘事件 useEffect(() => { @@ -1702,7 +1713,8 @@ export const PostDetailScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ +function createPostDetailStyles(colors: AppColors) { + return StyleSheet.create({ flex: { flex: 1, }, @@ -2124,4 +2136,5 @@ const styles = StyleSheet.create({ textAlign: 'center', paddingVertical: spacing.md, }, -}); + }); +} diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index 793720b..1ad58d0 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -4,7 +4,7 @@ * 支持响应式布局,宽屏下显示更大的搜索结果区域 */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { View, FlatList, @@ -16,7 +16,7 @@ import { import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { Post, User } from '../../types'; import { useUserStore } from '../../stores'; import { postService, authService } from '../../services'; @@ -37,6 +37,8 @@ interface SearchScreenProps { } export const SearchScreen: React.FC = ({ onBack }) => { + const colors = useAppColors(); + const styles = useMemo(() => createSearchScreenStyles(colors), [colors]); const router = useRouter(); const insets = useSafeAreaInsets(); const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore(); @@ -544,115 +546,117 @@ export const SearchScreen: React.FC = ({ onBack }) => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - searchHeader: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: `${colors.divider}70`, - }, - searchShell: { - flex: 1, - borderRadius: borderRadius.xl, - backgroundColor: `${colors.primary.main}08`, - paddingHorizontal: spacing.xs, - paddingVertical: spacing.xs, - // 移除阴影效果 - shadowColor: 'transparent', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0, - shadowRadius: 0, - elevation: 0, - }, - cancelButton: { - backgroundColor: `${colors.primary.main}14`, - borderRadius: borderRadius.full, - paddingHorizontal: spacing.md, - paddingVertical: spacing.xs, - }, - cancelText: { - fontSize: fontSizes.md, - fontWeight: '600', - }, - tabWrapper: { - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: `${colors.divider}50`, - }, - suggestionsContainer: { - flex: 1, - }, - section: { - marginTop: spacing.md, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.sm, - }, - sectionTitle: { - fontWeight: '600', - color: colors.text.primary, - }, - tagContainer: { - flexDirection: 'row', - flexWrap: 'wrap', - }, - tag: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - borderWidth: 1, - borderColor: colors.divider, - }, - tagText: { - marginLeft: spacing.xs, - }, - userCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - flexDirection: 'row', - alignItems: 'center', - }, - userCardInfo: { - flex: 1, - marginLeft: spacing.md, - }, - userCardName: { - fontWeight: '600', - color: colors.text.primary, - }, - userItem: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - }, - userInfo: { - flex: 1, - marginLeft: spacing.md, - }, - userName: { - fontWeight: '600', - color: colors.text.primary, - }, - followingBadge: { - width: 20, - height: 20, - borderRadius: 10, - backgroundColor: `${colors.primary.main}14`, - alignItems: 'center', - justifyContent: 'center', - }, - loadingMore: { - paddingVertical: spacing.md, - alignItems: 'center', - }, -}); +function createSearchScreenStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + searchHeader: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: `${colors.divider}70`, + }, + searchShell: { + flex: 1, + borderRadius: borderRadius.xl, + backgroundColor: `${colors.primary.main}08`, + paddingHorizontal: spacing.xs, + paddingVertical: spacing.xs, + // 移除阴影效果 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + cancelButton: { + backgroundColor: `${colors.primary.main}14`, + borderRadius: borderRadius.full, + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + }, + cancelText: { + fontSize: fontSizes.md, + fontWeight: '600', + }, + tabWrapper: { + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: `${colors.divider}50`, + }, + suggestionsContainer: { + flex: 1, + }, + section: { + marginTop: spacing.md, + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.sm, + }, + sectionTitle: { + fontWeight: '600', + color: colors.text.primary, + }, + tagContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + tag: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.divider, + }, + tagText: { + marginLeft: spacing.xs, + }, + userCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + flexDirection: 'row', + alignItems: 'center', + }, + userCardInfo: { + flex: 1, + marginLeft: spacing.md, + }, + userCardName: { + fontWeight: '600', + color: colors.text.primary, + }, + userItem: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + }, + userInfo: { + flex: 1, + marginLeft: spacing.md, + }, + userName: { + fontWeight: '600', + color: colors.text.primary, + }, + followingBadge: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: `${colors.primary.main}14`, + alignItems: 'center', + justifyContent: 'center', + }, + loadingMore: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + }); +} diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index 6c9f4ea..b4f56fd 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -26,18 +26,20 @@ import { ActivityIndicator, KeyboardAvoidingView, Platform, + TouchableOpacity, } from 'react-native'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useNavigation, useRouter } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { Text, ImageGallery, ImageGridItem } from '../../components/common'; -import { colors } from '../../theme'; +import { useAppColors } from '../../theme'; import * as hrefs from '../../navigation/hrefs'; import { messageManager } from '../../stores'; import { useBreakpointGTE } from '../../hooks/useResponsive'; import { useChatScreen, - chatScreenStyles as baseStyles, + useChatScreenStyles, EmojiPanel, MorePanel, MentionPanel, @@ -53,10 +55,11 @@ export const ChatScreen: React.FC = () => { const navigation = useNavigation(); const router = useRouter(); const insets = useSafeAreaInsets(); - + const colors = useAppColors(); + const styles = useChatScreenStyles(); + // 响应式布局 const isWideScreen = useBreakpointGTE('lg'); - const styles = baseStyles; // 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页 useEffect(() => { @@ -79,6 +82,8 @@ export const ChatScreen: React.FC = () => { beforeHeight: 0, }); const preloadCooldownTimerRef = useRef | null>(null); + const showJumpToLatestRef = useRef(false); + const [showJumpToLatest, setShowJumpToLatest] = useState(false); const containerStyle = useMemo(() => ([ styles.container, @@ -200,6 +205,7 @@ export const ChatScreen: React.FC = () => { loadMoreHistory, handleMessageListContentSizeChange, handleReachLatestEdge, + jumpToLatestMessages, setBrowsingHistory, } = useChatScreen(); const displayMessages = useMemo(() => [...messages].reverse(), [messages]); @@ -210,6 +216,18 @@ export const ChatScreen: React.FC = () => { } }, [loadingMore, showEdgeLoadingIndicator]); + useEffect(() => { + if (loading) { + showJumpToLatestRef.current = false; + setShowJumpToLatest(false); + } + }, [loading]); + + useEffect(() => { + showJumpToLatestRef.current = false; + setShowJumpToLatest(false); + }, [conversationId]); + useEffect(() => { return () => { if (preloadCooldownTimerRef.current) { @@ -314,6 +332,14 @@ export const ChatScreen: React.FC = () => { viewportHeight: layoutMeasurement.height, }; + // inverted:offset 越大离最新消息端越远,与 setBrowsingHistory(120) 阈值一致 + const shouldShowJump = + contentOffset.y > 120 && !loading && messages.length > 0; + if (showJumpToLatestRef.current !== shouldShowJump) { + showJumpToLatestRef.current = shouldShowJump; + setShowJumpToLatest(shouldShowJump); + } + // 离开最新消息端后进入“浏览历史”模式,屏蔽自动跟随到底 if (contentOffset.y > 120) { setBrowsingHistory(true); @@ -360,7 +386,17 @@ export const ChatScreen: React.FC = () => { }, 350); }); } - }, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge, showEdgeLoadingIndicator, setBrowsingHistory]); + }, [ + scrollPositionRef, + hasMoreHistory, + loadingMore, + loading, + messages.length, + loadMoreHistory, + handleReachLatestEdge, + showEdgeLoadingIndicator, + setBrowsingHistory, + ]); const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => { handleMessageListContentSizeChange(contentWidth, contentHeight); @@ -474,6 +510,18 @@ export const ChatScreen: React.FC = () => { )} + {showJumpToLatest && ( + + + 最新消息 + + )} {/* 底部区域:输入框 + 面板 */} diff --git a/src/screens/message/CreateGroupScreen.tsx b/src/screens/message/CreateGroupScreen.tsx index 5185947..c755ed3 100644 --- a/src/screens/message/CreateGroupScreen.tsx +++ b/src/screens/message/CreateGroupScreen.tsx @@ -4,7 +4,7 @@ * 支持响应式布局 */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { View, StyleSheet, @@ -19,7 +19,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { groupService } from '../../services/groupService'; import { uploadService } from '../../services/uploadService'; import { Avatar, Text, Button, Loading } from '../../components/common'; @@ -27,6 +27,8 @@ import { User } from '../../types'; import MutualFollowSelectorModal from './components/MutualFollowSelectorModal'; const CreateGroupScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createCreateGroupStyles(colors), [colors]); const router = useRouter(); // 表单状态 @@ -310,171 +312,173 @@ const CreateGroupScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: spacing.lg, - paddingBottom: spacing.xl, - }, - // 头部区域样式 - headerSection: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: spacing.xl, - }, - avatarContainer: { - marginRight: spacing.lg, - }, - avatarWrapper: { - position: 'relative', - }, - avatarImage: { - width: 80, - height: 80, - borderRadius: 40, - }, - avatarPlaceholder: { - justifyContent: 'center', - alignItems: 'center', - backgroundColor: colors.background.default, - borderRadius: 40, - }, - avatarBadge: { - position: 'absolute', - bottom: 0, - right: 0, - backgroundColor: colors.primary.main, - width: 28, - height: 28, - borderRadius: 14, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 2, - borderColor: colors.background.paper, - }, - nameInputContainer: { - flex: 1, - paddingTop: spacing.sm, - }, - inputLabel: { - marginBottom: spacing.sm, - fontWeight: '600', - }, - nameInput: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - fontSize: fontSizes.lg, - color: colors.text.primary, - borderWidth: 1, - borderColor: colors.divider, - }, - charCount: { - textAlign: 'right', - marginTop: spacing.xs, - }, - // 区域样式 - section: { - marginBottom: spacing.xl, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.sm, - }, - sectionTitle: { - fontWeight: '600', - marginBottom: spacing.sm, - }, - // 文本域样式 - textAreaContainer: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - borderWidth: 1, - borderColor: colors.divider, - padding: spacing.md, - }, - textArea: { - minHeight: 100, - fontSize: fontSizes.md, - color: colors.text.primary, - lineHeight: 22, - }, - textAreaCharCount: { - textAlign: 'right', - marginTop: spacing.sm, - }, - // 已选成员样式 - selectedMembersList: { - paddingVertical: spacing.sm, - }, - selectedMemberItem: { - alignItems: 'center', - marginRight: spacing.lg, - width: 64, - }, - removeMemberButton: { - position: 'absolute', - top: -4, - right: 4, - }, - removeIconContainer: { - backgroundColor: colors.text.secondary, - borderRadius: 10, - width: 20, - height: 20, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 2, - borderColor: colors.background.paper, - }, - selectedMemberName: { - marginTop: spacing.xs, - textAlign: 'center', - width: '100%', - }, - // 邀请按钮样式 - inviteButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.md, - marginBottom: spacing.xl, - ...shadows.sm, - }, - inviteIconContainer: { - width: 48, - height: 48, - borderRadius: borderRadius.lg, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - }, - inviteTextContainer: { - flex: 1, - }, - inviteTitle: { - fontWeight: '600', - marginBottom: 2, - }, - // 底部按钮样式 - footer: { - padding: spacing.lg, - paddingBottom: spacing.xl, - backgroundColor: colors.background.paper, - borderTopWidth: 1, - borderTopColor: colors.divider, - }, -}); +function createCreateGroupStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + padding: spacing.lg, + paddingBottom: spacing.xl, + }, + // 头部区域样式 + headerSection: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: spacing.xl, + }, + avatarContainer: { + marginRight: spacing.lg, + }, + avatarWrapper: { + position: 'relative', + }, + avatarImage: { + width: 80, + height: 80, + borderRadius: 40, + }, + avatarPlaceholder: { + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background.default, + borderRadius: 40, + }, + avatarBadge: { + position: 'absolute', + bottom: 0, + right: 0, + backgroundColor: colors.primary.main, + width: 28, + height: 28, + borderRadius: 14, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: colors.background.paper, + }, + nameInputContainer: { + flex: 1, + paddingTop: spacing.sm, + }, + inputLabel: { + marginBottom: spacing.sm, + fontWeight: '600', + }, + nameInput: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + fontSize: fontSizes.lg, + color: colors.text.primary, + borderWidth: 1, + borderColor: colors.divider, + }, + charCount: { + textAlign: 'right', + marginTop: spacing.xs, + }, + // 区域样式 + section: { + marginBottom: spacing.xl, + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.sm, + }, + sectionTitle: { + fontWeight: '600', + marginBottom: spacing.sm, + }, + // 文本域样式 + textAreaContainer: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.divider, + padding: spacing.md, + }, + textArea: { + minHeight: 100, + fontSize: fontSizes.md, + color: colors.text.primary, + lineHeight: 22, + }, + textAreaCharCount: { + textAlign: 'right', + marginTop: spacing.sm, + }, + // 已选成员样式 + selectedMembersList: { + paddingVertical: spacing.sm, + }, + selectedMemberItem: { + alignItems: 'center', + marginRight: spacing.lg, + width: 64, + }, + removeMemberButton: { + position: 'absolute', + top: -4, + right: 4, + }, + removeIconContainer: { + backgroundColor: colors.text.secondary, + borderRadius: 10, + width: 20, + height: 20, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: colors.background.paper, + }, + selectedMemberName: { + marginTop: spacing.xs, + textAlign: 'center', + width: '100%', + }, + // 邀请按钮样式 + inviteButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + marginBottom: spacing.xl, + ...shadows.sm, + }, + inviteIconContainer: { + width: 48, + height: 48, + borderRadius: borderRadius.lg, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.md, + }, + inviteTextContainer: { + flex: 1, + }, + inviteTitle: { + fontWeight: '600', + marginBottom: 2, + }, + // 底部按钮样式 + footer: { + padding: spacing.lg, + paddingBottom: spacing.xl, + backgroundColor: colors.background.paper, + borderTopWidth: 1, + borderTopColor: colors.divider, + }, + }); +} export default CreateGroupScreen; diff --git a/src/screens/message/GroupInfoScreen.tsx b/src/screens/message/GroupInfoScreen.tsx index 99af5a7..206fff5 100644 --- a/src/screens/message/GroupInfoScreen.tsx +++ b/src/screens/message/GroupInfoScreen.tsx @@ -25,7 +25,14 @@ import { useFocusEffect } from '@react-navigation/native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { + spacing, + fontSizes, + borderRadius, + shadows, + useAppColors, + type AppColors, +} from '../../theme'; import { useAuthStore } from '../../stores'; import { groupService } from '../../services/groupService'; import { uploadService } from '../../services/uploadService'; @@ -56,6 +63,8 @@ const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: s ]; const GroupInfoScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupInfoStyles(colors), [colors]); const router = useRouter(); const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{ groupId?: string | string[]; @@ -1071,7 +1080,8 @@ const GroupInfoScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ +function createGroupInfoStyles(colors: AppColors) { + return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, @@ -1594,6 +1604,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.primary.main, borderColor: colors.primary.main, }, -}); + }); +} export default GroupInfoScreen; diff --git a/src/screens/message/GroupInviteDetailScreen.tsx b/src/screens/message/GroupInviteDetailScreen.tsx index 0110de4..c0fffbd 100644 --- a/src/screens/message/GroupInviteDetailScreen.tsx +++ b/src/screens/message/GroupInviteDetailScreen.tsx @@ -3,7 +3,7 @@ import { View, StyleSheet, ActivityIndicator, Alert, ScrollView, TouchableOpacit import { useRouter, useLocalSearchParams } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { Avatar, Text } from '../../components/common'; import { routePayloadCache } from '../../stores/routePayloadCache'; import { groupService } from '../../services/groupService'; @@ -12,6 +12,8 @@ import { GroupMemberResponse } from '../../types/dto'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; const GroupInviteDetailScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupInviteDetailStyles(colors), [colors]); const router = useRouter(); const { messageId } = useLocalSearchParams<{ messageId?: string }>(); const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined; @@ -174,84 +176,86 @@ const GroupInviteDetailScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - emptyWrap: { - flex: 1, - padding: spacing.lg, - justifyContent: 'center', - alignItems: 'center', - gap: spacing.md, - }, - backBtn: { - padding: spacing.sm, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: spacing.lg, - paddingBottom: spacing.xl, - }, - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - ...shadows.sm, - }, - cardHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: spacing.md, - }, - cardIconContainer: { - width: 30, - height: 30, - borderRadius: 15, - backgroundColor: colors.info.light + '30', - alignItems: 'center', - justifyContent: 'center', - marginRight: spacing.sm, - }, - cardTitle: { - fontWeight: '600', - flex: 1, - }, - memberCount: { - marginRight: spacing.xs, - }, - loadingWrap: { - paddingVertical: spacing.md, - alignItems: 'center', - }, - memberPreview: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - }, - memberAvatar: { - marginLeft: -8, - marginBottom: spacing.sm, - }, - memberAvatarFirst: { - marginLeft: 0, - }, - ownerBadge: { - position: 'absolute', - bottom: -2, - right: -2, - backgroundColor: colors.warning.main, - borderRadius: 8, - paddingHorizontal: 4, - paddingVertical: 1, - }, - ownerBadgeText: { - fontSize: 10, - fontWeight: '700', - }, -}); +function createGroupInviteDetailStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + emptyWrap: { + flex: 1, + padding: spacing.lg, + justifyContent: 'center', + alignItems: 'center', + gap: spacing.md, + }, + backBtn: { + padding: spacing.sm, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + padding: spacing.lg, + paddingBottom: spacing.xl, + }, + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + ...shadows.sm, + }, + cardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: spacing.md, + }, + cardIconContainer: { + width: 30, + height: 30, + borderRadius: 15, + backgroundColor: colors.info.light + '30', + alignItems: 'center', + justifyContent: 'center', + marginRight: spacing.sm, + }, + cardTitle: { + fontWeight: '600', + flex: 1, + }, + memberCount: { + marginRight: spacing.xs, + }, + loadingWrap: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + memberPreview: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, + memberAvatar: { + marginLeft: -8, + marginBottom: spacing.sm, + }, + memberAvatarFirst: { + marginLeft: 0, + }, + ownerBadge: { + position: 'absolute', + bottom: -2, + right: -2, + backgroundColor: colors.warning.main, + borderRadius: 8, + paddingHorizontal: 4, + paddingVertical: 1, + }, + ownerBadgeText: { + fontSize: 10, + fontWeight: '700', + }, + }); +} export default GroupInviteDetailScreen; diff --git a/src/screens/message/GroupMembersScreen.tsx b/src/screens/message/GroupMembersScreen.tsx index b862f07..a2a9624 100644 --- a/src/screens/message/GroupMembersScreen.tsx +++ b/src/screens/message/GroupMembersScreen.tsx @@ -21,7 +21,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import { groupService } from '../../services/groupService'; import { @@ -55,6 +55,8 @@ interface MemberGroup { } const GroupMembersScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupMembersStyles(colors), [colors]); const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>(); const groupId = firstRouteParam(groupIdParam) ?? ''; const { currentUser } = useAuthStore(); @@ -646,122 +648,124 @@ const GroupMembersScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - section: { - marginBottom: spacing.md, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - backgroundColor: colors.background.default, - }, - memberItem: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - memberInfo: { - flex: 1, - marginLeft: spacing.md, - }, - memberNameRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 2, - }, - memberName: { - fontWeight: '500', - }, - roleBadge: { - paddingHorizontal: spacing.xs, - paddingVertical: 2, - borderRadius: borderRadius.sm, - marginLeft: spacing.sm, - }, - mutedBadge: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 2, - }, - // 模态框样式 - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'flex-end', - }, - modalContent: { - backgroundColor: colors.background.paper, - borderTopLeftRadius: borderRadius.lg, - borderTopRightRadius: borderRadius.lg, - padding: spacing.lg, - maxHeight: '80%', - }, - modalHeader: { - alignItems: 'center', - marginBottom: spacing.md, - }, - modalTitle: { - fontWeight: '700', - marginTop: spacing.sm, - marginBottom: spacing.xs, - }, - actionItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.md, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - actionText: { - marginLeft: spacing.md, - }, - inputLabel: { - marginBottom: spacing.xs, - }, - input: { - backgroundColor: colors.background.default, - borderRadius: borderRadius.md, - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - fontSize: fontSizes.md, - color: colors.text.primary, - borderWidth: 1, - borderColor: colors.divider, - marginBottom: spacing.md, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: spacing.md, - }, - modalButton: { - flex: 1, - marginHorizontal: spacing.xs, - }, - // 分页加载样式 - loadingFooter: { - paddingVertical: spacing.md, - alignItems: 'center', - }, - loadMoreBtn: { - paddingVertical: spacing.md, - alignItems: 'center', - }, - noMoreText: { - textAlign: 'center', - paddingVertical: spacing.md, - }, -}); +function createGroupMembersStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + section: { + marginBottom: spacing.md, + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: colors.background.default, + }, + memberItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + memberInfo: { + flex: 1, + marginLeft: spacing.md, + }, + memberNameRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 2, + }, + memberName: { + fontWeight: '500', + }, + roleBadge: { + paddingHorizontal: spacing.xs, + paddingVertical: 2, + borderRadius: borderRadius.sm, + marginLeft: spacing.sm, + }, + mutedBadge: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 2, + }, + // 模态框样式 + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: colors.background.paper, + borderTopLeftRadius: borderRadius.lg, + borderTopRightRadius: borderRadius.lg, + padding: spacing.lg, + maxHeight: '80%', + }, + modalHeader: { + alignItems: 'center', + marginBottom: spacing.md, + }, + modalTitle: { + fontWeight: '700', + marginTop: spacing.sm, + marginBottom: spacing.xs, + }, + actionItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + actionText: { + marginLeft: spacing.md, + }, + inputLabel: { + marginBottom: spacing.xs, + }, + input: { + backgroundColor: colors.background.default, + borderRadius: borderRadius.md, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + fontSize: fontSizes.md, + color: colors.text.primary, + borderWidth: 1, + borderColor: colors.divider, + marginBottom: spacing.md, + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: spacing.md, + }, + modalButton: { + flex: 1, + marginHorizontal: spacing.xs, + }, + // 分页加载样式 + loadingFooter: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + loadMoreBtn: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + noMoreText: { + textAlign: 'center', + paddingVertical: spacing.md, + }, + }); +} export default GroupMembersScreen; diff --git a/src/screens/message/GroupRequestDetailScreen.tsx b/src/screens/message/GroupRequestDetailScreen.tsx index 6e00171..5764b49 100644 --- a/src/screens/message/GroupRequestDetailScreen.tsx +++ b/src/screens/message/GroupRequestDetailScreen.tsx @@ -4,7 +4,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { Avatar, Text } from '../../components/common'; import * as hrefs from '../../navigation/hrefs'; import { routePayloadCache } from '../../stores/routePayloadCache'; @@ -14,6 +14,8 @@ import { userManager } from '../../stores/userManager'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; const GroupRequestDetailScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupRequestDetailStyles(colors), [colors]); const router = useRouter(); const { messageId } = useLocalSearchParams<{ messageId?: string }>(); const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined; @@ -174,52 +176,54 @@ const GroupRequestDetailScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - emptyWrap: { - flex: 1, - padding: spacing.lg, - justifyContent: 'center', - alignItems: 'center', - gap: spacing.md, - }, - backBtn: { - padding: spacing.sm, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: spacing.lg, - paddingBottom: spacing.xl, - }, - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - marginBottom: spacing.md, - ...shadows.sm, - }, - sectionTitle: { - marginBottom: spacing.sm, - }, - row: { - flexDirection: 'row', - alignItems: 'center', - }, - meta: { - marginLeft: spacing.md, - flex: 1, - }, - name: { - marginBottom: 2, - }, - subDesc: { - marginTop: spacing.sm, - }, -}); +function createGroupRequestDetailStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + emptyWrap: { + flex: 1, + padding: spacing.lg, + justifyContent: 'center', + alignItems: 'center', + gap: spacing.md, + }, + backBtn: { + padding: spacing.sm, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + padding: spacing.lg, + paddingBottom: spacing.xl, + }, + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + marginBottom: spacing.md, + ...shadows.sm, + }, + sectionTitle: { + marginBottom: spacing.sm, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + }, + meta: { + marginLeft: spacing.md, + flex: 1, + }, + name: { + marginBottom: 2, + }, + subDesc: { + marginTop: spacing.sm, + }, + }); +} export default GroupRequestDetailScreen; diff --git a/src/screens/message/JoinGroupScreen.tsx b/src/screens/message/JoinGroupScreen.tsx index c143cec..be4d3b1 100644 --- a/src/screens/message/JoinGroupScreen.tsx +++ b/src/screens/message/JoinGroupScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { View, StyleSheet, @@ -13,7 +13,7 @@ import { import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, borderRadius } from '../../theme'; +import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme'; import Avatar from '../../components/common/Avatar'; import Text from '../../components/common/Text'; import { groupService } from '../../services/groupService'; @@ -22,7 +22,160 @@ import { GroupResponse, JoinType } from '../../types/dto'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import { EmptyState } from '../../components/common'; +function createJoinGroupStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + padding: spacing.lg, + }, + heroCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + marginBottom: spacing.lg, + }, + heroIconWrap: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: colors.primary.light + '26', + alignItems: 'center', + justifyContent: 'center', + marginBottom: spacing.md, + }, + heroTitle: { + marginBottom: spacing.xs, + }, + tip: { + lineHeight: 20, + }, + formCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + flex: 1, + }, + label: { + marginBottom: spacing.xs, + }, + input: { + flex: 1, + borderWidth: 1, + borderColor: colors.divider, + borderRadius: borderRadius.md, + backgroundColor: colors.background.paper, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + color: colors.text.primary, + }, + searchRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: spacing.md, + }, + searchBtn: { + width: 46, + height: 46, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + marginLeft: spacing.sm, + }, + searchResultSection: { + marginBottom: spacing.lg, + }, + sectionTitle: { + marginBottom: spacing.sm, + fontWeight: '600', + }, + listSection: { + flex: 1, + }, + groupCard: { + borderWidth: 1, + borderColor: colors.divider, + borderRadius: borderRadius.md, + padding: spacing.md, + backgroundColor: colors.background.default, + marginBottom: spacing.md, + }, + groupHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + groupMeta: { + marginLeft: spacing.md, + flex: 1, + }, + groupName: { + marginBottom: spacing.xs, + fontWeight: '600', + }, + groupDesc: { + marginTop: spacing.sm, + lineHeight: 18, + }, + groupInfoRow: { + marginTop: spacing.sm, + marginBottom: spacing.xs, + flexDirection: 'row', + justifyContent: 'space-between', + }, + groupNoRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.md, + }, + copyBtn: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.sm, + paddingVertical: 4, + borderRadius: borderRadius.sm, + backgroundColor: colors.primary.light + '22', + }, + copyBtnText: { + marginLeft: 4, + }, + submitBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: borderRadius.md, + backgroundColor: colors.primary.main, + minHeight: 46, + }, + submitBtnDisabled: { + opacity: 0.5, + }, + submitText: { + marginLeft: spacing.xs, + }, + emptyText: { + marginTop: spacing.sm, + textAlign: 'center', + }, + loadingFooter: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + loadMoreBtn: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + noMoreText: { + textAlign: 'center', + paddingVertical: spacing.md, + }, + }); +} + const JoinGroupScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createJoinGroupStyles(colors), [colors]); const router = useRouter(); const [keyword, setKeyword] = useState(''); const [searching, setSearching] = useState(false); @@ -295,153 +448,4 @@ const JoinGroupScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - padding: spacing.lg, - }, - heroCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - marginBottom: spacing.lg, - }, - heroIconWrap: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: colors.primary.light + '26', - alignItems: 'center', - justifyContent: 'center', - marginBottom: spacing.md, - }, - heroTitle: { - marginBottom: spacing.xs, - }, - tip: { - lineHeight: 20, - }, - formCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - flex: 1, - }, - label: { - marginBottom: spacing.xs, - }, - input: { - flex: 1, - borderWidth: 1, - borderColor: colors.divider, - borderRadius: borderRadius.md, - backgroundColor: colors.background.paper, - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - color: colors.text.primary, - }, - searchRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: spacing.md, - }, - searchBtn: { - width: 46, - height: 46, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - marginLeft: spacing.sm, - }, - searchResultSection: { - marginBottom: spacing.lg, - }, - sectionTitle: { - marginBottom: spacing.sm, - fontWeight: '600', - }, - listSection: { - flex: 1, - }, - groupCard: { - borderWidth: 1, - borderColor: colors.divider, - borderRadius: borderRadius.md, - padding: spacing.md, - backgroundColor: colors.background.default, - marginBottom: spacing.md, - }, - groupHeader: { - flexDirection: 'row', - alignItems: 'center', - }, - groupMeta: { - marginLeft: spacing.md, - flex: 1, - }, - groupName: { - marginBottom: spacing.xs, - fontWeight: '600', - }, - groupDesc: { - marginTop: spacing.sm, - lineHeight: 18, - }, - groupInfoRow: { - marginTop: spacing.sm, - marginBottom: spacing.xs, - flexDirection: 'row', - justifyContent: 'space-between', - }, - groupNoRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: spacing.md, - }, - copyBtn: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.sm, - paddingVertical: 4, - borderRadius: borderRadius.sm, - backgroundColor: colors.primary.light + '22', - }, - copyBtnText: { - marginLeft: 4, - }, - submitBtn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: borderRadius.md, - backgroundColor: colors.primary.main, - minHeight: 46, - }, - submitBtnDisabled: { - opacity: 0.5, - }, - submitText: { - marginLeft: spacing.xs, - }, - emptyText: { - marginTop: spacing.sm, - textAlign: 'center', - }, - loadingFooter: { - paddingVertical: spacing.md, - alignItems: 'center', - }, - loadMoreBtn: { - paddingVertical: spacing.md, - alignItems: 'center', - }, - noMoreText: { - textAlign: 'center', - paddingVertical: spacing.md, - }, -}); - export default JoinGroupScreen; diff --git a/src/screens/message/MessageListScreen.tsx b/src/screens/message/MessageListScreen.tsx index 8644d93..958942f 100644 --- a/src/screens/message/MessageListScreen.tsx +++ b/src/screens/message/MessageListScreen.tsx @@ -30,7 +30,14 @@ import { useRouter } from 'expo-router'; import { useIsFocused } from '@react-navigation/native'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme'; +import { + spacing, + fontSizes, + shadows, + borderRadius, + useAppColors, + type AppColors, +} from '../../theme'; import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto'; import { authService } from '../../services'; import { useUserStore, useAuthStore } from '../../stores'; @@ -73,6 +80,8 @@ interface SearchResultItem { * 支持响应式双栏布局 */ export const MessageListScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createMessageListStyles(colors), [colors]); const router = useRouter(); const isFocused = useIsFocused(); const insets = useSafeAreaInsets(); @@ -565,7 +574,7 @@ export const MessageListScreen: React.FC = () => { ))} - + ); } @@ -614,7 +623,7 @@ export const MessageListScreen: React.FC = () => { )} {!user.is_following && ( - + )} ); @@ -661,11 +670,11 @@ export const MessageListScreen: React.FC = () => { const renderSearchMode = () => ( - + { /> {searchText.length > 0 && ( setSearchText('')}> - + )} @@ -733,7 +742,7 @@ export const MessageListScreen: React.FC = () => { - + @@ -744,7 +753,7 @@ export const MessageListScreen: React.FC = () => { activeOpacity={0.7} > - + 搜索 @@ -793,19 +802,19 @@ export const MessageListScreen: React.FC = () => { setActionMenuVisible(false)}> runMenuAction('scanQRCode')}> - + 扫码登录 runMenuAction('join')}> - + 加入群聊 runMenuAction('create')}> - + 创建群聊 runMenuAction('readAll')} disabled={isMarking}> - + {isMarking ? '处理中...' : '全部已读'} @@ -838,7 +847,7 @@ export const MessageListScreen: React.FC = () => { // 默认占位符 - + 选择一个会话开始聊天 @@ -870,288 +879,290 @@ export const MessageListScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#FAFAFA', - }, - // 桌面端双栏布局 - desktopContainer: { - flex: 1, - flexDirection: 'row', - }, - sidebar: { - backgroundColor: '#FAFAFA', - borderRightWidth: 1, - borderRightColor: '#E8E8E8', - }, - chatArea: { - flex: 1, - backgroundColor: '#F5F7FA', - }, - chatPlaceholder: { - flex: 1, - backgroundColor: '#F5F7FA', - alignItems: 'center', - justifyContent: 'center', - }, - chatPlaceholderContent: { - alignItems: 'center', - justifyContent: 'center', - }, - chatPlaceholderText: { - marginTop: spacing.md, - fontSize: 16, - color: '#999', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - backgroundColor: '#FAFAFA', - ...shadows.sm, - }, - headerWide: { - paddingHorizontal: spacing.lg, - paddingVertical: spacing.lg, - }, - headerTitleWide: { - fontSize: 22, - }, - searchContainerWide: { - paddingHorizontal: spacing.lg, - }, - listContentWide: { - paddingHorizontal: spacing.lg, - }, - headerLeft: { - width: 44, - }, - headerCenter: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - }, - headerTitle: { - fontSize: 19, - fontWeight: '700', - color: '#333', - }, - totalBadge: { - minWidth: 18, - height: 18, - borderRadius: 9, - backgroundColor: '#FF4757', - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 5, - }, - totalBadgeText: { - color: '#FFF', - fontSize: 11, - fontWeight: '600', - }, - headerRight: { - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - headerRightContainer: { - flexDirection: 'row', - alignItems: 'center', - }, - headerRightBtn: { - width: 36, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - actionMenuBackdrop: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.08)', - }, - actionMenu: { - position: 'absolute', - top: 88, - right: spacing.md, - width: 188, - backgroundColor: '#FFF', - borderRadius: 12, - paddingVertical: spacing.sm, - ...shadows.md, - }, - actionMenuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - }, - actionMenuText: { - marginLeft: spacing.sm, - fontSize: 16, - color: '#333', - }, - searchContainer: { - paddingHorizontal: spacing.md, - paddingBottom: spacing.sm, - backgroundColor: '#FAFAFA', - }, - searchBox: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#F0F0F0', - borderRadius: 10, - paddingHorizontal: spacing.sm, - paddingVertical: 10, - }, - searchPlaceholder: { - fontSize: 14, - color: '#999', - marginLeft: spacing.xs, - }, - listContent: { - flexGrow: 1, - backgroundColor: '#FAFAFA', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - paddingVertical: spacing.xl * 2, - }, - loadingMoreContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.md, - }, - loadingMoreText: { - marginLeft: spacing.sm, - fontSize: 14, - color: '#999', - }, - searchModeContainer: { - flex: 1, - backgroundColor: '#FAFAFA', - }, - searchInputContainer: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#F0F0F0', - borderRadius: 10, - paddingHorizontal: spacing.md, - marginHorizontal: spacing.md, - marginTop: spacing.md, - height: 40, - }, - searchInput: { - flex: 1, - fontSize: 15, - color: '#333', - marginLeft: spacing.md, - }, - searchTabs: { - flexDirection: 'row', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - gap: spacing.sm, - }, - searchTab: { - flex: 1, - paddingVertical: spacing.sm, - alignItems: 'center', - backgroundColor: '#F0F0F0', - borderRadius: 8, - }, - searchTabActive: { - backgroundColor: colors.primary.main, - }, - searchTabText: { - fontSize: 14, - color: '#666', - fontWeight: '500', - }, - searchTabTextActive: { - color: '#FFF', - }, - searchResultsContent: { - flexGrow: 1, - paddingHorizontal: spacing.md, - }, - searchResultItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.md, - backgroundColor: '#FFF', - borderRadius: 10, - marginBottom: spacing.sm, - paddingHorizontal: spacing.md, - }, - searchResultContent: { - flex: 1, - marginLeft: spacing.md, - }, - searchResultHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 2, - }, - searchResultName: { - fontSize: 15, - fontWeight: '600', - color: '#333', - }, - searchResultTime: { - fontSize: 12, - color: '#999', - }, - searchResultMessage: { - fontSize: 13, - color: '#888', - }, - searchingText: { - marginTop: spacing.sm, - fontSize: 14, - color: '#999', - }, - followingBadge: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: colors.primary.light + '30', - alignItems: 'center', - justifyContent: 'center', - }, - highlightText: { - color: colors.primary.main, - fontWeight: '700', - backgroundColor: colors.primary.light + '30', - }, - matchedMessagesContainer: { - marginTop: spacing.sm, - }, - matchedMessageItem: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - paddingVertical: spacing.xs, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#F0F0F0', - }, - matchedMessageText: { - flex: 1, - fontSize: 13, - color: '#555', - lineHeight: 18, - }, - matchedMessageTime: { - fontSize: 11, - color: '#AAA', - marginLeft: spacing.sm, - minWidth: 45, - }, -}); +function createMessageListStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + // 桌面端双栏布局 + desktopContainer: { + flex: 1, + flexDirection: 'row', + }, + sidebar: { + backgroundColor: colors.background.default, + borderRightWidth: 1, + borderRightColor: colors.chat.border, + }, + chatArea: { + flex: 1, + backgroundColor: colors.chat.surfaceMuted, + }, + chatPlaceholder: { + flex: 1, + backgroundColor: colors.chat.surfaceMuted, + alignItems: 'center', + justifyContent: 'center', + }, + chatPlaceholderContent: { + alignItems: 'center', + justifyContent: 'center', + }, + chatPlaceholderText: { + marginTop: spacing.md, + fontSize: 16, + color: colors.text.hint, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + backgroundColor: colors.background.default, + ...shadows.sm, + }, + headerWide: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.lg, + }, + headerTitleWide: { + fontSize: 22, + }, + searchContainerWide: { + paddingHorizontal: spacing.lg, + }, + listContentWide: { + paddingHorizontal: spacing.lg, + }, + headerLeft: { + width: 44, + }, + headerCenter: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + headerTitle: { + fontSize: 19, + fontWeight: '700', + color: colors.text.primary, + }, + totalBadge: { + minWidth: 18, + height: 18, + borderRadius: 9, + backgroundColor: colors.error.main, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 5, + }, + totalBadgeText: { + color: colors.primary.contrast, + fontSize: 11, + fontWeight: '600', + }, + headerRight: { + width: 44, + height: 44, + alignItems: 'center', + justifyContent: 'center', + }, + headerRightContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + headerRightBtn: { + width: 36, + height: 44, + alignItems: 'center', + justifyContent: 'center', + }, + actionMenuBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.08)', + }, + actionMenu: { + position: 'absolute', + top: 88, + right: spacing.md, + width: 188, + backgroundColor: colors.background.paper, + borderRadius: 12, + paddingVertical: spacing.sm, + ...shadows.md, + }, + actionMenuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + }, + actionMenuText: { + marginLeft: spacing.sm, + fontSize: 16, + color: colors.text.primary, + }, + searchContainer: { + paddingHorizontal: spacing.md, + paddingBottom: spacing.sm, + backgroundColor: colors.background.default, + }, + searchBox: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.chat.surfaceInput, + borderRadius: 10, + paddingHorizontal: spacing.sm, + paddingVertical: 10, + }, + searchPlaceholder: { + fontSize: 14, + color: colors.text.hint, + marginLeft: spacing.xs, + }, + listContent: { + flexGrow: 1, + backgroundColor: colors.background.default, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: spacing.xl * 2, + }, + loadingMoreContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + }, + loadingMoreText: { + marginLeft: spacing.sm, + fontSize: 14, + color: colors.text.hint, + }, + searchModeContainer: { + flex: 1, + backgroundColor: colors.background.default, + }, + searchInputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.chat.surfaceInput, + borderRadius: 10, + paddingHorizontal: spacing.md, + marginHorizontal: spacing.md, + marginTop: spacing.md, + height: 40, + }, + searchInput: { + flex: 1, + fontSize: 15, + color: colors.text.primary, + marginLeft: spacing.md, + }, + searchTabs: { + flexDirection: 'row', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + gap: spacing.sm, + }, + searchTab: { + flex: 1, + paddingVertical: spacing.sm, + alignItems: 'center', + backgroundColor: colors.chat.surfaceInput, + borderRadius: 8, + }, + searchTabActive: { + backgroundColor: colors.primary.main, + }, + searchTabText: { + fontSize: 14, + color: colors.text.secondary, + fontWeight: '500', + }, + searchTabTextActive: { + color: colors.primary.contrast, + }, + searchResultsContent: { + flexGrow: 1, + paddingHorizontal: spacing.md, + }, + searchResultItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + backgroundColor: colors.background.paper, + borderRadius: 10, + marginBottom: spacing.sm, + paddingHorizontal: spacing.md, + }, + searchResultContent: { + flex: 1, + marginLeft: spacing.md, + }, + searchResultHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 2, + }, + searchResultName: { + fontSize: 15, + fontWeight: '600', + color: colors.text.primary, + }, + searchResultTime: { + fontSize: 12, + color: colors.text.hint, + }, + searchResultMessage: { + fontSize: 13, + color: colors.text.secondary, + }, + searchingText: { + marginTop: spacing.sm, + fontSize: 14, + color: colors.text.hint, + }, + followingBadge: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: colors.primary.light + '30', + alignItems: 'center', + justifyContent: 'center', + }, + highlightText: { + color: colors.primary.main, + fontWeight: '700', + backgroundColor: colors.primary.light + '30', + }, + matchedMessagesContainer: { + marginTop: spacing.sm, + }, + matchedMessageItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + paddingVertical: spacing.xs, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.divider, + }, + matchedMessageText: { + flex: 1, + fontSize: 13, + color: colors.text.secondary, + lineHeight: 18, + }, + matchedMessageTime: { + fontSize: 11, + color: colors.text.hint, + marginLeft: spacing.sm, + minWidth: 45, + }, + }); +} diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index 5431a98..5c69eed 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -16,11 +16,11 @@ import { Dimensions, BackHandler, } from 'react-native'; -import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { SystemMessageResponse } from '../../types/dto'; import { messageService } from '../../services/messageService'; import { commentService } from '../../services/commentService'; @@ -45,6 +45,8 @@ const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'f const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']); export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => { + const colors = useAppColors(); + const styles = useMemo(() => createNotificationsStyles(colors), [colors]); const isFocused = useIsFocused(); const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); const router = useRouter(); @@ -554,143 +556,145 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - // Header 样式 - 美化版 - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.lg, - paddingVertical: spacing.md, - backgroundColor: colors.background.paper, - ...shadows.sm, - }, - headerWide: { - paddingHorizontal: spacing.xl, - paddingVertical: spacing.lg, - }, - headerTitleContainer: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - headerTitle: { - fontSize: 18, - fontWeight: '600', - color: colors.text.primary, - }, - headerTitleWide: { - fontSize: 20, - }, - unreadBadge: { - backgroundColor: colors.primary.main, - borderRadius: 10, - paddingHorizontal: 6, - paddingVertical: 2, - marginLeft: spacing.sm, - minWidth: 20, - alignItems: 'center', - justifyContent: 'center', - }, - unreadBadgeText: { - color: colors.text.inverse, - fontSize: 11, - fontWeight: '600', - }, - backButton: { - width: 40, - height: 40, - justifyContent: 'center', - alignItems: 'flex-start', - }, - backButtonPlaceholder: { - width: 40, - }, - // 分类筛选 - 美化版(使用分段式设计) - filterContainer: { - flexDirection: 'row', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - backgroundColor: colors.background.paper, - }, - filterContainerWideWeb: { - paddingHorizontal: spacing.xl, - paddingVertical: spacing.md, - justifyContent: 'center', - backgroundColor: colors.background.paper, - }, - filterTag: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - marginRight: spacing.xs, - borderRadius: borderRadius.lg, - backgroundColor: colors.background.default, - borderWidth: 1, - borderColor: 'transparent', - }, - filterTagWide: { - paddingHorizontal: spacing.lg, - paddingVertical: spacing.md, - marginRight: spacing.sm, - }, - filterTagActive: { - backgroundColor: colors.primary.main, - borderColor: colors.primary.main, - }, - filterTagTextActive: { - fontWeight: '600', - }, - filterCount: { - marginLeft: spacing.xs, - backgroundColor: colors.primary.light + '40', - paddingHorizontal: 6, - paddingVertical: 1, - borderRadius: 8, - minWidth: 18, - alignItems: 'center', - }, - filterCountActive: { - backgroundColor: 'rgba(255, 255, 255, 0.25)', - }, - listContent: { - flexGrow: 1, - paddingTop: spacing.md, - paddingBottom: spacing.lg, - }, - listContentWide: { - paddingHorizontal: spacing.xl, - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - paddingVertical: spacing.xl * 2, - }, - loadingMore: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - paddingVertical: spacing.lg, - }, - loadingMoreText: { - marginLeft: spacing.sm, - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - paddingTop: spacing['3xl'], - }, - emptyContainerWeb: { - minHeight: 500, - justifyContent: 'center', - paddingTop: 100, - }, -}); +function createNotificationsStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + // Header 样式 - 美化版 + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + backgroundColor: colors.background.paper, + ...shadows.sm, + }, + headerWide: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.lg, + }, + headerTitleContainer: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + headerTitle: { + fontSize: 18, + fontWeight: '600', + color: colors.text.primary, + }, + headerTitleWide: { + fontSize: 20, + }, + unreadBadge: { + backgroundColor: colors.primary.main, + borderRadius: 10, + paddingHorizontal: 6, + paddingVertical: 2, + marginLeft: spacing.sm, + minWidth: 20, + alignItems: 'center', + justifyContent: 'center', + }, + unreadBadgeText: { + color: colors.text.inverse, + fontSize: 11, + fontWeight: '600', + }, + backButton: { + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'flex-start', + }, + backButtonPlaceholder: { + width: 40, + }, + // 分类筛选 - 美化版(使用分段式设计) + filterContainer: { + flexDirection: 'row', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: colors.background.paper, + }, + filterContainerWideWeb: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.md, + justifyContent: 'center', + backgroundColor: colors.background.paper, + }, + filterTag: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + marginRight: spacing.xs, + borderRadius: borderRadius.lg, + backgroundColor: colors.background.default, + borderWidth: 1, + borderColor: 'transparent', + }, + filterTagWide: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + marginRight: spacing.sm, + }, + filterTagActive: { + backgroundColor: colors.primary.main, + borderColor: colors.primary.main, + }, + filterTagTextActive: { + fontWeight: '600', + }, + filterCount: { + marginLeft: spacing.xs, + backgroundColor: colors.primary.light + '40', + paddingHorizontal: 6, + paddingVertical: 1, + borderRadius: 8, + minWidth: 18, + alignItems: 'center', + }, + filterCountActive: { + backgroundColor: 'rgba(255, 255, 255, 0.25)', + }, + listContent: { + flexGrow: 1, + paddingTop: spacing.md, + paddingBottom: spacing.lg, + }, + listContentWide: { + paddingHorizontal: spacing.xl, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: spacing.xl * 2, + }, + loadingMore: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingVertical: spacing.lg, + }, + loadingMoreText: { + marginLeft: spacing.sm, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingTop: spacing['3xl'], + }, + emptyContainerWeb: { + minHeight: 500, + justifyContent: 'center', + paddingTop: 100, + }, + }); +} diff --git a/src/screens/message/PrivateChatInfoScreen.tsx b/src/screens/message/PrivateChatInfoScreen.tsx index b57aae7..2b8ace7 100644 --- a/src/screens/message/PrivateChatInfoScreen.tsx +++ b/src/screens/message/PrivateChatInfoScreen.tsx @@ -4,7 +4,7 @@ * 参考QQ和微信的实现,提供聊天管理功能 */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, StyleSheet, @@ -16,7 +16,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import { authService } from '../../services/authService'; import { messageService } from '../../services/messageService'; @@ -33,6 +33,8 @@ import * as hrefs from '../../navigation/hrefs'; import { AppBackButton } from '../../components/common'; const PrivateChatInfoScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createPrivateChatInfoStyles(colors), [colors]); const router = useRouter(); const raw = useLocalSearchParams<{ conversationId?: string; @@ -277,7 +279,7 @@ const PrivateChatInfoScreen: React.FC = () => { value={value} onValueChange={onValueChange} trackColor={{ false: '#E0E0E0', true: colors.primary.light }} - thumbColor={value ? colors.primary.main : '#F5F5F5'} + thumbColor={value ? colors.primary.main : colors.background.default} /> ); @@ -420,101 +422,103 @@ const PrivateChatInfoScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - pageHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - pageTitle: { - fontWeight: '600', - }, - pageHeaderPlaceholder: { - width: 40, - height: 40, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingBottom: spacing.xl, - }, - headerCard: { - backgroundColor: colors.background.paper, - marginHorizontal: spacing.md, - marginTop: spacing.md, - borderRadius: borderRadius.lg, - padding: spacing.lg, - ...shadows.sm, - }, - userHeader: { - flexDirection: 'row', - alignItems: 'center', - }, - userInfo: { - flex: 1, - marginLeft: spacing.md, - }, - userName: { - fontWeight: '600', - marginBottom: spacing.xs, - }, - section: { - marginTop: spacing.lg, - paddingHorizontal: spacing.md, - }, - sectionTitle: { - marginBottom: spacing.sm, - marginLeft: spacing.sm, - fontWeight: '500', - }, - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - ...shadows.sm, - overflow: 'hidden', - }, - settingItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - }, - settingIconContainer: { - width: 36, - height: 36, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - }, - settingIconDanger: { - backgroundColor: colors.error.light + '20', - }, - settingTitle: { - flex: 1, - }, - dangerText: { - color: colors.error.main, - }, - divider: { - marginLeft: spacing.xl + 36, - width: 'auto', - marginVertical: spacing.xs, - }, - deleteButton: { - marginTop: spacing.sm, - }, -}); +function createPrivateChatInfoStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + pageHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + pageTitle: { + fontWeight: '600', + }, + pageHeaderPlaceholder: { + width: 40, + height: 40, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingBottom: spacing.xl, + }, + headerCard: { + backgroundColor: colors.background.paper, + marginHorizontal: spacing.md, + marginTop: spacing.md, + borderRadius: borderRadius.lg, + padding: spacing.lg, + ...shadows.sm, + }, + userHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + userInfo: { + flex: 1, + marginLeft: spacing.md, + }, + userName: { + fontWeight: '600', + marginBottom: spacing.xs, + }, + section: { + marginTop: spacing.lg, + paddingHorizontal: spacing.md, + }, + sectionTitle: { + marginBottom: spacing.sm, + marginLeft: spacing.sm, + fontWeight: '500', + }, + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + ...shadows.sm, + overflow: 'hidden', + }, + settingItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + }, + settingIconContainer: { + width: 36, + height: 36, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.md, + }, + settingIconDanger: { + backgroundColor: colors.error.light + '20', + }, + settingTitle: { + flex: 1, + }, + dangerText: { + color: colors.error.main, + }, + divider: { + marginLeft: spacing.xl + 36, + width: 'auto', + marginVertical: spacing.xs, + }, + deleteButton: { + marginTop: spacing.sm, + }, + }); +} export default PrivateChatInfoScreen; diff --git a/src/screens/message/components/ChatScreen/ChatHeader.tsx b/src/screens/message/components/ChatScreen/ChatHeader.tsx index 71112ad..6945dac 100644 --- a/src/screens/message/components/ChatScreen/ChatHeader.tsx +++ b/src/screens/message/components/ChatScreen/ChatHeader.tsx @@ -7,8 +7,8 @@ import React, { useMemo } from 'react'; import { View, TouchableOpacity, StyleSheet } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text, AppBackButton } from '../../../../components/common'; -import { colors, spacing } from '../../../../theme'; -import { chatScreenStyles as baseStyles } from './styles'; +import { useAppColors, spacing } from '../../../../theme'; +import { useChatScreenStyles } from './styles'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { ChatHeaderProps } from './types'; @@ -24,6 +24,9 @@ export const ChatHeader: React.FC = ({ onGroupInfoPress, isWideScreen: propIsWideScreen, }) => { + const colors = useAppColors(); + const baseStyles = useChatScreenStyles(); + // 响应式布局 // 使用 768px 作为大屏幕和小屏幕的分界线 const isWideScreenHook = useBreakpointGTE('lg'); @@ -57,7 +60,7 @@ export const ChatHeader: React.FC = ({ fontSize: isWideScreen ? 13 : 12, }, }); - }, [isWideScreen]); + }, [isWideScreen, baseStyles]); // 获取显示标题 const getDisplayTitle = () => { diff --git a/src/screens/message/components/ChatScreen/ChatInput.tsx b/src/screens/message/components/ChatScreen/ChatInput.tsx index e6b313b..f48d46d 100644 --- a/src/screens/message/components/ChatScreen/ChatInput.tsx +++ b/src/screens/message/components/ChatScreen/ChatInput.tsx @@ -7,8 +7,8 @@ import React, { useMemo } from 'react'; import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Text } from '../../../../components/common'; -import { colors } from '../../../../theme'; -import { chatScreenStyles as baseStyles } from './styles'; +import { useAppColors } from '../../../../theme'; +import { useChatScreenStyles } from './styles'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { ChatInputProps, GroupMessage, SenderInfo } from './types'; import { extractTextFromSegments } from '../../../../types/dto'; @@ -39,14 +39,15 @@ export const ChatInput: React.FC { + const colors = useAppColors(); + const styles = useChatScreenStyles(); const isDisabled = isGroupChat && isMuted; - + // 获取当前用户ID const currentUserId = currentUser?.id || ''; - + // 响应式布局 const isWideScreen = useBreakpointGTE('lg'); - const styles = baseStyles; const inputContainerStyle = useMemo(() => ([ styles.inputContainer, diff --git a/src/screens/message/components/ChatScreen/EmojiPanel.tsx b/src/screens/message/components/ChatScreen/EmojiPanel.tsx index dbad4ad..9c0cfa8 100644 --- a/src/screens/message/components/ChatScreen/EmojiPanel.tsx +++ b/src/screens/message/components/ChatScreen/EmojiPanel.tsx @@ -10,11 +10,11 @@ import { Image as ExpoImage } from 'expo-image'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { Text } from '../../../../components/common'; -import { chatScreenStyles as baseStyles } from './styles'; +import { useChatScreenStyles } from './styles'; import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive'; import { EMOJIS } from './constants'; import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '../../../../services/stickerService'; -import { colors, spacing } from '../../../../theme'; +import { useAppColors, spacing } from '../../../../theme'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); @@ -41,6 +41,8 @@ export const EmojiPanel: React.FC = ({ onInsertSticker, onClose, }) => { + const colors = useAppColors(); + const baseStyles = useChatScreenStyles(); const [activeTab, setActiveTab] = useState('emoji'); const [stickers, setStickers] = useState([]); const [loadingStickers, setLoadingStickers] = useState(false); @@ -82,7 +84,7 @@ export const EmojiPanel: React.FC = ({ lineHeight: emojiSize.size + 6, }, }); - }, [emojiSize, emojiColumns]); + }, [emojiSize, emojiColumns, baseStyles]); // 加载自定义表情 const loadStickers = useCallback(async () => { diff --git a/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx index c89ff3b..aad0407 100644 --- a/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx +++ b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx @@ -5,7 +5,7 @@ * 实现手机端群信息界面的核心功能 */ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { View, StyleSheet, @@ -17,7 +17,7 @@ import { } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text } from '../../../../components/common'; -import { colors, spacing, fontSizes, shadows } from '../../../../theme'; +import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { groupManager } from '../../../../stores/groupManager'; @@ -53,6 +53,8 @@ export const GroupInfoPanel: React.FC = ({ onInviteMembers, onViewAllMembers, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]); const isWideScreen = useBreakpointGTE('lg'); const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current; const fadeAnim = React.useRef(new Animated.Value(0)).current; @@ -363,7 +365,8 @@ export const GroupInfoPanel: React.FC = ({ ); }; -const styles = StyleSheet.create({ +function createGroupInfoPanelStyles(colors: AppColors) { + return StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.3)', @@ -378,7 +381,7 @@ const styles = StyleSheet.create({ top: 0, bottom: 0, width: PANEL_WIDTH, - backgroundColor: '#FFF', + backgroundColor: colors.background.paper, zIndex: 101, ...shadows.lg, }, @@ -389,12 +392,12 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, paddingVertical: spacing.md, borderBottomWidth: 1, - borderBottomColor: '#E8E8E8', + borderBottomColor: colors.divider, }, headerTitle: { fontSize: fontSizes.xl, fontWeight: '600', - color: '#333', + color: colors.text.primary, }, closeButton: { padding: spacing.xs, @@ -409,12 +412,12 @@ const styles = StyleSheet.create({ groupName: { fontSize: fontSizes.xl, fontWeight: '600', - color: '#333', + color: colors.text.primary, marginTop: spacing.md, }, memberCount: { fontSize: fontSizes.md, - color: '#999', + color: colors.text.secondary, marginTop: spacing.xs, }, joinType: { @@ -442,7 +445,7 @@ const styles = StyleSheet.create({ }, divider: { height: 1, - backgroundColor: '#E8E8E8', + backgroundColor: colors.divider, marginHorizontal: spacing.md, }, section: { @@ -456,11 +459,11 @@ const styles = StyleSheet.create({ sectionTitle: { fontSize: fontSizes.md, fontWeight: '600', - color: '#333', + color: colors.text.primary, marginLeft: spacing.xs, }, noticeBox: { - backgroundColor: '#FFF9E6', + backgroundColor: colors.chat.tipBg, padding: spacing.md, borderRadius: 8, borderLeftWidth: 3, @@ -468,23 +471,23 @@ const styles = StyleSheet.create({ }, noticeText: { fontSize: fontSizes.sm, - color: '#666', + color: colors.text.secondary, lineHeight: 20, }, noticeTime: { fontSize: fontSizes.xs, - color: '#999', + color: colors.text.hint, marginTop: spacing.xs, textAlign: 'right', }, descriptionBox: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, padding: spacing.md, borderRadius: 8, }, description: { fontSize: fontSizes.sm, - color: '#666', + color: colors.text.secondary, lineHeight: 20, }, memberList: { @@ -518,7 +521,7 @@ const styles = StyleSheet.create({ }, memberName: { fontSize: fontSizes.md, - color: '#333', + color: colors.text.primary, flex: 1, }, ownerBadge: { @@ -541,7 +544,7 @@ const styles = StyleSheet.create({ }, moreMembers: { fontSize: fontSizes.sm, - color: '#999', + color: colors.text.secondary, textAlign: 'center', paddingVertical: spacing.sm, }, @@ -550,15 +553,15 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', paddingVertical: spacing.sm, borderBottomWidth: 1, - borderBottomColor: '#F0F0F0', + borderBottomColor: colors.chat.borderHairline, }, infoLabel: { fontSize: fontSizes.sm, - color: '#999', + color: colors.text.secondary, }, infoValue: { fontSize: fontSizes.sm, - color: '#333', + color: colors.text.primary, }, actionSection: { padding: spacing.md, @@ -588,6 +591,7 @@ const styles = StyleSheet.create({ leaveButtonText: { color: colors.error.main, }, -}); + }); +} export default GroupInfoPanel; diff --git a/src/screens/message/components/ChatScreen/LongPressMenu.tsx b/src/screens/message/components/ChatScreen/LongPressMenu.tsx index 1c6f901..58257b2 100644 --- a/src/screens/message/components/ChatScreen/LongPressMenu.tsx +++ b/src/screens/message/components/ChatScreen/LongPressMenu.tsx @@ -15,7 +15,7 @@ import { Platform, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { chatScreenStyles as styles } from './styles'; +import { useChatScreenStyles } from './styles'; import { LongPressMenuProps } from './types'; import { RECALL_TIME_LIMIT } from './constants'; import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto'; @@ -34,6 +34,7 @@ export const LongPressMenu: React.FC = ({ onDelete, onAddSticker, }) => { + const styles = useChatScreenStyles(); const scaleAnimation = useRef(new Animated.Value(0)).current; const opacityAnimation = useRef(new Animated.Value(0)).current; const blurActiveElementOnWeb = () => { diff --git a/src/screens/message/components/ChatScreen/MentionPanel.tsx b/src/screens/message/components/ChatScreen/MentionPanel.tsx index b1829af..18c1e4a 100644 --- a/src/screens/message/components/ChatScreen/MentionPanel.tsx +++ b/src/screens/message/components/ChatScreen/MentionPanel.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { View, ScrollView, TouchableOpacity } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text } from '../../../../components/common'; -import { chatScreenStyles as styles } from './styles'; +import { useChatScreenStyles } from './styles'; import { MentionPanelProps } from './types'; export const MentionPanel: React.FC = ({ @@ -18,6 +18,7 @@ export const MentionPanel: React.FC = ({ onMentionAll, onClose, }) => { + const styles = useChatScreenStyles(); // 过滤成员列表 const filteredMembers = members.filter(member => { if (member.user_id === currentUserId) return false; // 排除自己 diff --git a/src/screens/message/components/ChatScreen/MessageBubble.tsx b/src/screens/message/components/ChatScreen/MessageBubble.tsx index ffd5fe7..5625c1a 100644 --- a/src/screens/message/components/ChatScreen/MessageBubble.tsx +++ b/src/screens/message/components/ChatScreen/MessageBubble.tsx @@ -15,20 +15,12 @@ import { } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text, ImageGridItem } from '../../../../components/common'; -import { chatScreenStyles as baseStyles } from './styles'; +import { useChatScreenStyles } from './styles'; import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive'; import { MessageBubbleProps, SenderInfo, MenuPosition, GroupMessage } from './types'; import { MessageSegmentsRenderer } from './SegmentRenderer'; import { MessageSegment, ImageSegmentData, extractTextFromSegments } from '../../../../types/dto'; import { SwipeableMessageBubble } from './SwipeableMessageBubble'; -import { - getBubbleBorderRadius, - getMessageGroupPosition, - shouldShowSenderInfo, - bubbleColors, - bubbleStyles, -} from './bubbleStyles'; - // 获取屏幕宽度 const { width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -61,7 +53,8 @@ export const MessageBubble: React.FC = ({ }) => { const bubbleRef = useRef(null); const pressPositionRef = useRef({ x: 0, y: 0 }); - + const baseStyles = useChatScreenStyles(); + // 响应式布局 const { width } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); @@ -89,7 +82,7 @@ export const MessageBubble: React.FC = ({ height: isWideScreen ? 280 : 220, }, }); - }, [maxContentWidth, isWideScreen]); + }, [maxContentWidth, isWideScreen, baseStyles]); // 系统通知消息特殊处理 // 支持两种判断方式: @@ -236,26 +229,46 @@ export const MessageBubble: React.FC = ({ } }; + const renderBubbleShell = useCallback( + ( + child: React.ReactNode, + innerExtra: (object | undefined | null | false)[] = [] + ) => ( + + + {child} + + + ), + [isMe, styles] + ); + // 渲染消息内容 const renderMessageContent = () => { // 撤回消息 if (isRecalled) { - return ( - - - 消息已撤回 - - + return renderBubbleShell( + + 消息已撤回 + , + [styles.recalledBubble] ); } @@ -282,71 +295,60 @@ export const MessageBubble: React.FC = ({ // 如果没有 segments,显示空内容或错误提示 if (!hasSegments) { - return ( - - - [消息格式错误] - - + return renderBubbleShell( + + [消息格式错误] + ); } // 检查是否有回复,用于调整气泡样式 const hasReply = segments.some(s => s.type === 'reply'); - return ( - - undefined} - onReplyPress={onReplyPress} - onImagePress={(url) => { - // 查找点击的图片索引 - const clickIndex = imageSegments.findIndex( - img => img.url === url || img.thumbnail_url === url - ); - if (clickIndex !== -1 && onImagePress) { - onImagePress(imageSegments, clickIndex); - } - }} - onImageLongPress={(position?: { x: number; y: number }) => { - // 图片长按触发和消息气泡一样的菜单 - // 如果有位置信息,直接使用;否则使用气泡位置 - if (position) { - onLongPress(message, { - x: 0, - y: 0, - width: 0, - height: 0, - pressX: position.x, - pressY: position.y, - }); - } else { - handleLongPress(); - } - }} - onLinkPress={() => undefined} - /> - + return renderBubbleShell( + undefined} + onReplyPress={onReplyPress} + onImagePress={(url) => { + // 查找点击的图片索引 + const clickIndex = imageSegments.findIndex( + img => img.url === url || img.thumbnail_url === url + ); + if (clickIndex !== -1 && onImagePress) { + onImagePress(imageSegments, clickIndex); + } + }} + onImageLongPress={(position?: { x: number; y: number }) => { + // 图片长按触发和消息气泡一样的菜单 + // 如果有位置信息,直接使用;否则使用气泡位置 + if (position) { + onLongPress(message, { + x: 0, + y: 0, + width: 0, + height: 0, + pressX: position.x, + pressY: position.y, + }); + } else { + handleLongPress(); + } + }} + onLinkPress={() => undefined} + />, + [hasReply && segmentStyles.replyBubble, isPureImageMessage && segmentStyles.pureImageBubble] ); }; diff --git a/src/screens/message/components/ChatScreen/MorePanel.tsx b/src/screens/message/components/ChatScreen/MorePanel.tsx index 2dcbe04..af957a2 100644 --- a/src/screens/message/components/ChatScreen/MorePanel.tsx +++ b/src/screens/message/components/ChatScreen/MorePanel.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { View, TouchableOpacity } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Text } from '../../../../components/common'; -import { chatScreenStyles as styles } from './styles'; +import { useChatScreenStyles } from './styles'; import { MORE_ACTIONS } from './constants'; import { MorePanelProps } from './types'; @@ -14,6 +14,7 @@ export const MorePanel: React.FC = ({ onAction, disabledActionIds = [], }) => { + const styles = useChatScreenStyles(); const disabledSet = new Set(disabledActionIds); return ( diff --git a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx index b382fb6..a80b937 100644 --- a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx +++ b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx @@ -31,7 +31,7 @@ import { UserDTO, extractTextFromSegments, } from '../../../../types/dto'; -import { colors, spacing } from '../../../../theme'; +import { spacing, useAppColors, type AppColors } from '../../../../theme'; import { SenderInfo } from './types'; const IMAGE_MAX_WIDTH = 200; @@ -142,12 +142,11 @@ export const renderSegment = ( /** * 渲染文本 Segment */ -const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => { - // 兼容 content 和 text 两种字段 +const TextSegmentBody: React.FC<{ data: TextSegmentData; isMe: boolean }> = ({ data, isMe }) => { + const styles = useSegmentStyles(); const text = data.content ?? data.text ?? ''; return ( @@ -156,6 +155,11 @@ const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNod ); }; +const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => { + const key = `text-${(data.content ?? data.text ?? '').substring(0, 10)}`; + return ; +}; + /** * 渲染图片 Segment */ @@ -166,6 +170,7 @@ const ImageSegment: React.FC<{ onImagePress?: (url: string) => void; onImageLongPress?: (position?: { x: number; y: number }) => void; }> = ({ data, isMe, onImagePress, onImageLongPress }) => { + const styles = useSegmentStyles(); const pressPositionRef = useRef({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null); const [loadError, setLoadError] = useState(false); @@ -362,16 +367,13 @@ const renderImageSegment = ( /** * 渲染 @ Segment */ -const renderAtSegment = ( - data: AtSegmentData, - props: Omit -): React.ReactNode => { +const AtSegmentBody: React.FC<{ + data: AtSegmentData; + props: Omit; +}> = ({ data, props }) => { + const styles = useSegmentStyles(); const { currentUserId, memberMap, onAtPress, isMe } = props; - - // 判断是否@当前用户 const isAtMe = data.user_id === currentUserId || data.user_id === 'all'; - - // 优先从 memberMap 中查找昵称,兜底使用 data.nickname(兼容旧消息),再兜底显示"用户" let displayName: string; if (data.user_id === 'all') { displayName = '所有人'; @@ -379,10 +381,8 @@ const renderAtSegment = ( const member = memberMap?.get(data.user_id); displayName = member?.nickname || data.nickname || '用户'; } - return ( +): React.ReactNode => ; + /** * 渲染表情 Segment */ -const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNode => { - // 如果有自定义表情URL,显示图片 +const FaceSegmentBody: React.FC<{ data: FaceSegmentData }> = ({ data }) => { + const styles = useSegmentStyles(); if (data.url) { return ( ); } - - // 否则显示表情名称(后续可以映射到本地表情) return ( - + [{data.name || `表情${data.id}`}] ); }; +const renderFaceSegment = (data: FaceSegmentData, _isMe: boolean): React.ReactNode => ( + +); + /** * 渲染语音 Segment */ -const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactNode => { +const VoiceSegmentBody: React.FC<{ data: VoiceSegmentData; isMe: boolean }> = ({ data, isMe }) => { + const styles = useSegmentStyles(); + const themeColors = useAppColors(); return ( {data.duration ? `${data.duration}"` : '语音'} @@ -442,10 +449,16 @@ const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactN ); }; +const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactNode => ( + +); + /** * 视频 Segment 组件 - 支持点击播放 */ const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ data, isMe }) => { + const styles = useSegmentStyles(); + const themeColors = useAppColors(); const [playerVisible, setPlayerVisible] = useState(false); const handlePress = useCallback(() => { @@ -469,11 +482,11 @@ const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ dat /> ) : ( - + )} - + {data.duration && ( {formatDuration(data.duration)} @@ -496,12 +509,12 @@ const renderVideoSegment = (data: VideoSegmentData, isMe: boolean): React.ReactN /** * 渲染文件 Segment */ -const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => { +const FileSegmentBody: React.FC<{ data: FileSegmentData; isMe: boolean }> = ({ data, isMe }) => { + const styles = useSegmentStyles(); + const themeColors = useAppColors(); const fileSize = data.size ? formatFileSize(data.size) : ''; - return ( @@ -509,33 +522,36 @@ const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNod - {data.name} - {fileSize && ( - {fileSize} - )} + {fileSize ? {fileSize} : null} ); }; +const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => ( + +); + /** * 渲染链接 Segment */ -const renderLinkSegment = ( - data: LinkSegmentData, - props: Omit -): React.ReactNode => { +const LinkSegmentBody: React.FC<{ + data: LinkSegmentData; + props: Omit; +}> = ({ data, props }) => { + const styles = useSegmentStyles(); const { onLinkPress, isMe } = props; - + const handlePress = async () => { if (onLinkPress) { onLinkPress(data.url); @@ -552,26 +568,25 @@ const renderLinkSegment = ( } } }; - + return ( - {data.image && ( + {data.image ? ( - )} + ) : null} {data.title || data.url} - {data.description && ( + {data.description ? ( {data.description} - )} + ) : null} {(() => { try { @@ -586,6 +601,11 @@ const renderLinkSegment = ( ); }; +const renderLinkSegment = ( + data: LinkSegmentData, + props: Omit +): React.ReactNode => ; + /** * 回复预览组件(用于消息气泡内显示回复引用) */ @@ -596,6 +616,9 @@ export const ReplyPreviewSegment: React.FC = ({ onPress, getSenderInfo, }) => { + const themeColors = useAppColors(); + const styles = useMemo(() => createSegmentStyles(themeColors), [themeColors]); + if (!replyMessage) { // 如果没有引用消息详情,只显示引用ID return ( @@ -605,9 +628,11 @@ export const ReplyPreviewSegment: React.FC = ({ activeOpacity={0.7} > - - 引用消息 - + + + 引用消息 + + ); } @@ -658,6 +683,7 @@ export const ReplyPreviewSegment: React.FC = ({ onPress={onPress} activeOpacity={0.7} > + {senderName}: @@ -702,7 +728,9 @@ export const MessageSegmentsRenderer: React.FC<{ onLinkPress, getSenderInfo, }) => { - // 找出 reply segment(如果有) + const themeColors = useAppColors(); + const segStyles = useMemo(() => createSegmentStyles(themeColors), [themeColors]); + const replySegment = segments.find(s => s.type === 'reply'); const otherSegments = segments.filter(s => s.type !== 'reply'); const chunks = partitionMessageSegments(otherSegments); @@ -717,58 +745,58 @@ export const MessageSegmentsRenderer: React.FC<{ onImageLongPress, onLinkPress, }; - + return ( - - {/* 回复预览 */} - {replySegment && ( - onReplyPress?.((replySegment.data as ReplySegmentData).id)} - getSenderInfo={getSenderInfo} - /> - )} - - {/* 其他 Segment:行内文案与块级图片/媒体分行展示,多图纵向排列 */} - - {chunks.map((chunk, i) => { - if (chunk.kind === 'inline') { + + + {replySegment ? ( + onReplyPress?.((replySegment.data as ReplySegmentData).id)} + getSenderInfo={getSenderInfo} + /> + ) : null} + + + {chunks.map((chunk, i) => { + if (chunk.kind === 'inline') { + return ( + + {chunk.parts.map((segment, j) => ( + + {renderSegment(segment, renderProps)} + + ))} + + ); + } + if (chunk.kind === 'images') { + return ( + + {chunk.parts.map((segment, j) => ( + + {renderSegment(segment, renderProps)} + + ))} + + ); + } return ( - - {chunk.parts.map((segment, j) => ( - - {renderSegment(segment, renderProps)} - - ))} + + {renderSegment(chunk.segment, renderProps)} ); - } - if (chunk.kind === 'images') { - return ( - - {chunk.parts.map((segment, j) => ( - - {renderSegment(segment, renderProps)} - - ))} - - ); - } - return ( - - {renderSegment(chunk.segment, renderProps)} - - ); - })} + })} + - + ); }; @@ -787,7 +815,8 @@ const formatDuration = (seconds: number): string => { return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `0:${secs.toString().padStart(2, '0')}`; }; -const styles = StyleSheet.create({ +function createSegmentStyles(colors: AppColors) { + return StyleSheet.create({ // 容器 segmentsContainer: { flexDirection: 'column', @@ -826,10 +855,10 @@ const styles = StyleSheet.create({ fontWeight: '400', }, textMe: { - color: '#1A1A1A', // 统一使用深色字体 + color: colors.chat.textPrimary, }, textOther: { - color: '#1A1A1A', // 统一使用深色字体 + color: colors.chat.textPrimary, }, // @提及 - Telegram风格:蓝色高亮 @@ -845,7 +874,7 @@ const styles = StyleSheet.create({ borderRadius: 4, }, atTextOther: { - color: '#4A88C7', + color: colors.chat.link, backgroundColor: 'rgba(74, 136, 199, 0.12)', borderRadius: 4, }, @@ -860,7 +889,7 @@ const styles = StyleSheet.create({ borderRadius: 12, overflow: 'hidden', marginTop: 4, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, @@ -892,7 +921,7 @@ const styles = StyleSheet.create({ paddingVertical: spacing.sm + 2, borderRadius: 20, minWidth: 100, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, @@ -916,7 +945,7 @@ const styles = StyleSheet.create({ overflow: 'hidden', width: 220, height: 165, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, @@ -953,7 +982,7 @@ const styles = StyleSheet.create({ position: 'absolute', bottom: 10, right: 10, - color: '#FFFFFF', + color: colors.primary.contrast, fontSize: 13, fontWeight: '600', backgroundColor: 'rgba(0, 0, 0, 0.6)', @@ -971,7 +1000,7 @@ const styles = StyleSheet.create({ borderRadius: 16, minWidth: 220, maxWidth: 300, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2, @@ -981,7 +1010,7 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(255, 255, 255, 0.25)', }, fileOther: { - backgroundColor: '#F8F9FA', + backgroundColor: colors.chat.surfaceRaised, }, fileIcon: { marginRight: spacing.md, @@ -998,11 +1027,11 @@ const styles = StyleSheet.create({ fileName: { fontSize: 15, fontWeight: '600', - color: '#1A1A1A', + color: colors.chat.textPrimary, }, fileSize: { fontSize: 13, - color: '#8E8E93', + color: colors.chat.textSecondary, marginTop: 4, fontWeight: '500', }, @@ -1012,8 +1041,8 @@ const styles = StyleSheet.create({ borderRadius: 16, overflow: 'hidden', maxWidth: 280, - backgroundColor: '#FFFFFF', - shadowColor: '#000', + backgroundColor: colors.chat.card, + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, @@ -1023,7 +1052,7 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(255, 255, 255, 0.95)', }, linkOther: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, }, linkImage: { width: '100%', @@ -1035,12 +1064,12 @@ const styles = StyleSheet.create({ linkTitle: { fontSize: 15, fontWeight: '600', - color: '#1A1A1A', + color: colors.chat.textPrimary, lineHeight: 20, }, linkDescription: { fontSize: 13, - color: '#666666', + color: colors.chat.textTertiary, marginTop: 6, lineHeight: 18, }, @@ -1051,16 +1080,17 @@ const styles = StyleSheet.create({ fontWeight: '500', }, - // 回复预览 - Telegram风格:简洁设计 + // 回复预览 - Telegram风格:左侧色条 + 简洁背景 replyPreview: { flexDirection: 'row', + alignItems: 'center', marginBottom: 0, paddingVertical: spacing.sm, paddingHorizontal: spacing.sm + 2, borderRadius: 6, width: '100%', minWidth: 200, - // 移除阴影 + gap: 8, shadowColor: 'transparent', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0, @@ -1074,28 +1104,45 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(0, 0, 0, 0.03)', }, replyLine: { - width: 0, - marginRight: 0, + width: 3, + alignSelf: 'stretch', + minHeight: 28, + borderRadius: 2, + backgroundColor: colors.chat.link, }, replyContent: { flex: 1, flexDirection: 'row', alignItems: 'center', flexWrap: 'nowrap', + minWidth: 0, }, replySender: { fontSize: 13, fontWeight: '600', - color: '#4A88C7', + color: colors.chat.link, marginRight: 6, flexShrink: 0, }, replyText: { fontSize: 13, - color: '#666666', + color: colors.chat.textTertiary, flex: 1, flexShrink: 1, }, -}); + }); +} + +type SegmentStyles = ReturnType; + +const SegmentStylesContext = React.createContext(null); + +function useSegmentStyles(): SegmentStyles { + const ctx = React.useContext(SegmentStylesContext); + if (!ctx) { + throw new Error('useSegmentStyles 必须在 MessageSegmentsRenderer 内使用'); + } + return ctx; +} export default MessageSegmentsRenderer; \ No newline at end of file diff --git a/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx index 21dd0b6..6d47c97 100644 --- a/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx +++ b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx @@ -8,7 +8,7 @@ import { View, StyleSheet, Animated } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { PanGestureHandler, State } from 'react-native-gesture-handler'; import * as Haptics from 'expo-haptics'; -import { colors } from '../../../../theme'; +import { useAppColors } from '../../../../theme'; // 滑动阈值 const SWIPE_THRESHOLD = 30; @@ -27,6 +27,7 @@ export const SwipeableMessageBubble: React.FC = ({ onReply, enabled = true, }) => { + const colors = useAppColors(); const translateX = useRef(new Animated.Value(0)).current; const hasTriggeredRef = useRef(false); diff --git a/src/screens/message/components/ChatScreen/bubbleStyles.ts b/src/screens/message/components/ChatScreen/bubbleStyles.ts index a5a3667..9f47781 100644 --- a/src/screens/message/components/ChatScreen/bubbleStyles.ts +++ b/src/screens/message/components/ChatScreen/bubbleStyles.ts @@ -4,52 +4,40 @@ */ import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; -import { colors, spacing } from '../../../../theme'; +import { spacing, type AppColors } from '../../../../theme'; -// 气泡圆角大小(参考 Element X: 12pt) export const BUBBLE_RADIUS = 16; -// 气泡颜色方案(参考 Element X 的语义化颜色) -export const bubbleColors = { - // 自己发送的消息 - outgoing: { - background: '#E8E8E8', // Element X 风格:灰色系 - text: '#1A1A1A', - }, - // 收到的消息 - incoming: { - background: '#FFFFFF', - text: '#1A1A1A', - }, - // 被回复的消息高亮 - replyHighlight: { - background: 'rgba(74, 136, 199, 0.1)', - borderLeft: '#4A88C7', - }, -}; +export function getBubbleColors(colors: AppColors) { + return { + outgoing: { + background: colors.chat.bubbleOutgoing, + text: colors.chat.textPrimary, + }, + incoming: { + background: colors.chat.bubbleIncoming, + text: colors.chat.textPrimary, + }, + replyHighlight: { + background: colors.chat.replyTint, + borderLeft: colors.chat.replyBorder, + }, + }; +} -// 消息在组中的位置类型 export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last'; -/** - * 根据消息在组中的位置获取圆角样式 - * 参考 Element X 的动态圆角逻辑 - */ -export const getBubbleBorderRadius = ( - isMe: boolean, - position: MessageGroupPosition -): ViewStyle => { +export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => { const radius = BUBBLE_RADIUS; - + if (isMe) { - // 自己发送的消息 switch (position) { case 'single': return { borderTopLeftRadius: radius, borderTopRightRadius: radius, borderBottomLeftRadius: radius, - borderBottomRightRadius: 4, // 尖角在右下 + borderBottomRightRadius: 4, }; case 'first': return { @@ -74,11 +62,10 @@ export const getBubbleBorderRadius = ( }; } } else { - // 收到的消息 switch (position) { case 'single': return { - borderTopLeftRadius: 4, // 尖角在左上 + borderTopLeftRadius: 4, borderTopRightRadius: radius, borderBottomLeftRadius: radius, borderBottomRightRadius: radius, @@ -108,9 +95,6 @@ export const getBubbleBorderRadius = ( } }; -/** - * 判断消息在组中的位置 - */ export const getMessageGroupPosition = ( index: number, messages: Array<{ sender_id: string }>, @@ -118,13 +102,13 @@ export const getMessageGroupPosition = ( ): MessageGroupPosition => { const currentMessage = messages[index]; if (!currentMessage) return 'single'; - + const prevMessage = index > 0 ? messages[index - 1] : null; const nextMessage = index < messages.length - 1 ? messages[index + 1] : null; - + const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id; const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id; - + if (!isSameSenderAsPrev && !isSameSenderAsNext) { return 'single'; } else if (!isSameSenderAsPrev && isSameSenderAsNext) { @@ -136,135 +120,114 @@ export const getMessageGroupPosition = ( } }; -/** - * 判断是否显示发送者信息(只在组的第一条消息显示) - */ export const shouldShowSenderInfo = ( index: number, - messages: Array<{ sender_id: string }>, + messages: Array<{ sender_id: string }> ): boolean => { if (index === 0) return true; - + const currentMessage = messages[index]; const prevMessage = messages[index - 1]; - + return prevMessage.sender_id !== currentMessage.sender_id; }; -/** - * 获取气泡基础样式 - */ -export const getBubbleBaseStyle = (isMe: boolean): ViewStyle => ({ - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm + 4, - minWidth: 60, - maxWidth: '75%', - backgroundColor: isMe ? bubbleColors.outgoing.background : bubbleColors.incoming.background, - // Element X 风格的微妙阴影 - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.06, - shadowRadius: 2, - elevation: 2, -}); - -/** - * 获取文本样式 - */ -export const getBubbleTextStyle = (isMe: boolean): TextStyle => ({ - color: isMe ? bubbleColors.outgoing.text : bubbleColors.incoming.text, - fontSize: 16, - lineHeight: 23, - letterSpacing: 0.2, - fontWeight: '400', -}); - -/** - * 消息气泡样式集合 - */ -export const bubbleStyles = StyleSheet.create({ - // 基础气泡 - bubble: { +export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle => { + const bc = getBubbleColors(colors); + return { paddingHorizontal: spacing.md, paddingVertical: spacing.sm + 4, minWidth: 60, - }, - - // 自己发送的消息 - outgoing: { - backgroundColor: bubbleColors.outgoing.background, - }, - - // 收到的消息 - incoming: { - backgroundColor: bubbleColors.incoming.background, - }, - - // 文本样式 - text: { - fontSize: 16, - lineHeight: 23, - letterSpacing: 0.2, - fontWeight: '400', - }, - - // 阴影样式(Element X 风格) - shadow: { - shadowColor: '#000', + maxWidth: '75%', + backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background, + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 2, elevation: 2, - }, - - // 长按反馈阴影 - longPressShadow: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.12, - shadowRadius: 8, - elevation: 6, - }, - - // 被回复消息的高亮边框 - replyHighlight: { - borderLeftWidth: 3, - borderLeftColor: colors.primary.main, - backgroundColor: bubbleColors.replyHighlight.background, - }, - - // 已撤回消息 - recalled: { - backgroundColor: '#F5F7FA', - borderWidth: 1, - borderColor: '#E8E8E8', - borderStyle: 'dashed', - borderRadius: 12, - }, - - // 系统通知 - systemNotice: { - alignItems: 'center', - marginVertical: spacing.sm, - }, - systemNoticeText: { - fontSize: 12, - color: '#8E8E93', - backgroundColor: 'rgba(142, 142, 147, 0.12)', - paddingHorizontal: spacing.md, - paddingVertical: spacing.xs, - borderRadius: 12, - overflow: 'hidden', - }, -}); + }; +}; + +export const getBubbleTextStyle = (isMe: boolean, colors: AppColors): TextStyle => { + const bc = getBubbleColors(colors); + return { + color: isMe ? bc.outgoing.text : bc.incoming.text, + fontSize: 16, + lineHeight: 23, + letterSpacing: 0.2, + fontWeight: '400', + }; +}; + +export function createBubbleStyles(colors: AppColors) { + const bc = getBubbleColors(colors); + return StyleSheet.create({ + bubble: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm + 4, + minWidth: 60, + }, + outgoing: { + backgroundColor: bc.outgoing.background, + }, + incoming: { + backgroundColor: bc.incoming.background, + }, + text: { + fontSize: 16, + lineHeight: 23, + letterSpacing: 0.2, + fontWeight: '400', + }, + shadow: { + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.06, + shadowRadius: 2, + elevation: 2, + }, + longPressShadow: { + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.12, + shadowRadius: 8, + elevation: 6, + }, + replyHighlight: { + borderLeftWidth: 3, + borderLeftColor: colors.primary.main, + backgroundColor: bc.replyHighlight.background, + }, + recalled: { + backgroundColor: colors.chat.surfaceMuted, + borderWidth: 1, + borderColor: colors.chat.border, + borderStyle: 'dashed', + borderRadius: 12, + }, + systemNotice: { + alignItems: 'center', + marginVertical: spacing.sm, + }, + systemNoticeText: { + fontSize: 12, + color: colors.chat.textSecondary, + backgroundColor: colors.chat.surfaceMuted, + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: 12, + overflow: 'hidden', + }, + }); +} export default { BUBBLE_RADIUS, - bubbleColors, + getBubbleColors, getBubbleBorderRadius, getMessageGroupPosition, shouldShowSenderInfo, getBubbleBaseStyle, getBubbleTextStyle, - bubbleStyles, -}; \ No newline at end of file + createBubbleStyles, +}; diff --git a/src/screens/message/components/ChatScreen/index.ts b/src/screens/message/components/ChatScreen/index.ts index 10661fb..1ab1e27 100644 --- a/src/screens/message/components/ChatScreen/index.ts +++ b/src/screens/message/components/ChatScreen/index.ts @@ -9,7 +9,7 @@ export * from './types'; export * from './constants'; // 样式 -export { chatScreenStyles } from './styles'; +export { createChatScreenStyles, useChatScreenStyles } from './styles'; // 组件 export { EmojiPanel } from './EmojiPanel'; diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index b438fc9..a8264c7 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -3,8 +3,9 @@ * 支持响应式布局 */ +import { useMemo } from 'react'; import { StyleSheet, Dimensions } from 'react-native'; -import { colors, spacing, shadows } from '../../../../theme'; +import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -18,19 +19,20 @@ const BREAKPOINTS = { const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop; const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet; -export const chatScreenStyles = StyleSheet.create({ +export function createChatScreenStyles(colors: AppColors) { + return StyleSheet.create({ // 容器 container: { flex: 1, - backgroundColor: '#F0F2F5', + backgroundColor: colors.chat.screen, }, // 头部 headerContainer: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderBottomWidth: 1, borderBottomColor: 'rgba(255, 107, 53, 0.12)', - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.06, shadowRadius: 10, @@ -42,7 +44,7 @@ export const chatScreenStyles = StyleSheet.create({ justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.sm + 2, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, }, backButton: { width: 38, @@ -112,36 +114,61 @@ export const chatScreenStyles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderRadius: 19, - backgroundColor: '#F7F8FA', + backgroundColor: colors.chat.surfaceRaised, borderWidth: 1, - borderColor: '#ECEFF3', + borderColor: colors.chat.borderLight, }, // 消息列表 messageListContainer: { flex: 1, }, + // 回到底部(Telegram 式浮动条) + jumpToLatestFab: { + position: 'absolute', + right: 14, + bottom: 10, + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.96)', + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'rgba(0, 0, 0, 0.08)', + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.12, + shadowRadius: 6, + elevation: 4, + }, + jumpToLatestFabText: { + fontSize: 13, + fontWeight: '600', + color: colors.primary.main, + }, listContent: { paddingHorizontal: spacing.md, paddingTop: spacing.md, paddingBottom: spacing.xl, }, - // 时间分隔 - QQ风格:更明显的时间标签 + // 时间分隔 - 轻量胶囊(弱化字重,接近 Telegram 日期条) timeContainer: { alignItems: 'center', marginVertical: spacing.lg, }, timeText: { - color: '#8E8E93', - fontSize: 12, - backgroundColor: 'rgba(142, 142, 147, 0.12)', - paddingHorizontal: spacing.md, - paddingVertical: 6, - borderRadius: 14, - fontWeight: '600', + color: 'rgba(142, 142, 147, 0.95)', + fontSize: 11, + backgroundColor: 'rgba(142, 142, 147, 0.1)', + paddingHorizontal: spacing.sm + 4, + paddingVertical: 5, + borderRadius: 12, + fontWeight: '500', overflow: 'hidden', - letterSpacing: 0.3, + letterSpacing: 0.2, }, // 系统通知 @@ -152,7 +179,7 @@ export const chatScreenStyles = StyleSheet.create({ }, systemNoticeText: { fontSize: 12, - color: '#8E8E93', + color: colors.chat.textSecondary, backgroundColor: 'rgba(142, 142, 147, 0.12)', paddingHorizontal: spacing.md, paddingVertical: spacing.xs, @@ -177,7 +204,7 @@ export const chatScreenStyles = StyleSheet.create({ // 头像 avatarWrapper: { marginHorizontal: 2, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2, @@ -185,7 +212,7 @@ export const chatScreenStyles = StyleSheet.create({ }, groupAvatarWrapper: { marginHorizontal: 2, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2, @@ -200,53 +227,59 @@ export const chatScreenStyles = StyleSheet.create({ }, senderName: { fontSize: 13, - color: '#4A88C7', + color: colors.chat.link, marginBottom: 4, marginLeft: 4, fontWeight: '600', }, - // 消息气泡 - Telegram风格:浅色背景,统一字体颜色 - messageBubble: { + // 消息气泡 - 外层极轻阴影(纯文字气泡更接近扁平 IM) + messageBubbleOuter: { + borderRadius: 16, + minWidth: 60, + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 0.5 }, + shadowOpacity: 0.04, + shadowRadius: 1, + elevation: 1, + }, + myBubbleOuter: { + borderBottomRightRadius: 4, + }, + theirBubbleOuter: { + borderTopLeftRadius: 4, + }, + // 内层:背景 + 裁剪,防止回复条/@ 高亮在圆角外露出颜色 + messageBubbleInner: { + borderRadius: 16, + overflow: 'hidden', paddingHorizontal: spacing.md, paddingVertical: spacing.sm + 4, - borderRadius: 16, - // 自适应内容宽度 minWidth: 60, }, - myBubble: { - backgroundColor: '#DFF2FF', // Telegram风格的浅蓝色 - borderBottomRightRadius: 4, // 右下角尖,箭头在右下 - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.08, - shadowRadius: 2, - elevation: 2, + myBubbleInner: { + backgroundColor: colors.chat.replyTint, + borderBottomRightRadius: 4, }, - theirBubble: { - backgroundColor: '#FFFFFF', - borderTopLeftRadius: 4, // 左上角尖,箭头在左上 - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.08, - shadowRadius: 2, - elevation: 2, + theirBubbleInner: { + backgroundColor: colors.chat.card, + borderTopLeftRadius: 4, }, recalledBubble: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, borderWidth: 1, - borderColor: '#E8E8E8', + borderColor: colors.chat.border, borderStyle: 'dashed', }, myBubbleText: { - color: '#1A1A1A', // 统一使用深色字体 + color: colors.chat.textPrimary, // 主文案 fontSize: 16, lineHeight: 23, letterSpacing: 0.2, fontWeight: '400', }, theirBubbleText: { - color: '#1A1A1A', // 统一使用深色字体 + color: colors.chat.textPrimary, // 主文案 fontSize: 16, lineHeight: 23, letterSpacing: 0.2, @@ -255,29 +288,27 @@ export const chatScreenStyles = StyleSheet.create({ // 选中状态 selectedBubble: { borderWidth: 2, - borderColor: '#7FB6E6', + borderColor: colors.chat.replyBorder, }, + // 仅对齐右侧,不再套一层浅色底,避免与气泡叠色产生「边缘多出一块蓝」 myMessageContentPanel: { - backgroundColor: '#DFF2FF', - borderRadius: 14, - paddingHorizontal: 4, - paddingBottom: 4, + alignItems: 'flex-end', }, mySelectedBubble: { - backgroundColor: '#CFEAFF', + backgroundColor: colors.chat.replyTintActive, borderWidth: 2, - borderColor: '#7FB6E6', + borderColor: colors.chat.replyBorder, }, theirSelectedBubble: { - backgroundColor: '#EEF5FC', + backgroundColor: colors.chat.menuHighlight, borderWidth: 2, - borderColor: '#7FB6E6', + borderColor: colors.chat.replyBorder, }, selectedText: { // 文本选中时的样式 }, recalledText: { - color: '#8E8E93', + color: colors.chat.textSecondary, fontStyle: 'italic', }, @@ -285,7 +316,7 @@ export const chatScreenStyles = StyleSheet.create({ imageBubble: { borderRadius: 18, overflow: 'hidden', - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.15, shadowRadius: 6, @@ -314,27 +345,28 @@ export const chatScreenStyles = StyleSheet.create({ }, readStatusText: { fontSize: 10, - color: '#8E8E93', + color: colors.chat.textSecondary, fontWeight: '500', }, readStatusTextRead: { - color: '#34C759', + color: colors.chat.success, }, - // 输入框区域 + // 输入框区域 - 与列表背景同色底栏,顶部细分隔 inputWrapper: { - backgroundColor: 'transparent', + backgroundColor: colors.chat.screen, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: 'rgba(0, 0, 0, 0.06)', }, inputContainer: { - backgroundColor: colors.background.paper, - borderWidth: 1, - borderColor: colors.divider, - borderRadius: 24, - marginHorizontal: 14, - marginBottom: 12, + backgroundColor: 'transparent', + borderWidth: 0, + borderRadius: 0, + marginHorizontal: 10, + marginBottom: 10, + marginTop: 2, paddingHorizontal: spacing.sm, paddingVertical: spacing.sm, - // 移除明显的阴影效果,改用更微妙的边框 shadowColor: 'transparent', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0, @@ -344,7 +376,7 @@ export const chatScreenStyles = StyleSheet.create({ inputInner: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F7F8FA', + backgroundColor: colors.chat.surfaceRaised, borderRadius: 20, paddingHorizontal: spacing.xs, paddingVertical: 4, @@ -357,7 +389,7 @@ export const chatScreenStyles = StyleSheet.create({ elevation: 0, }, inputInnerMuted: { - backgroundColor: '#F0F0F0', + backgroundColor: colors.chat.borderHairline, opacity: 0.7, }, @@ -366,7 +398,7 @@ export const chatScreenStyles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - backgroundColor: '#FFF5F5', + backgroundColor: colors.chat.warningBg, paddingVertical: spacing.xs, paddingHorizontal: spacing.md, marginBottom: spacing.xs, @@ -375,7 +407,7 @@ export const chatScreenStyles = StyleSheet.create({ }, mutedBannerText: { fontSize: 13, - color: '#FF3B30', + color: colors.chat.danger, fontWeight: '500', }, @@ -421,14 +453,14 @@ export const chatScreenStyles = StyleSheet.create({ }, input: { fontSize: 16, - color: '#1A1A1A', + color: colors.chat.textPrimary, paddingTop: 8, paddingBottom: 8, maxHeight: 100, lineHeight: 20, }, inputMuted: { - color: '#CCC', + color: colors.chat.iconMuted, }, inputTransparent: { color: 'transparent', @@ -444,7 +476,7 @@ export const chatScreenStyles = StyleSheet.create({ }, inputHighlightText: { fontSize: 16, - color: '#1A1A1A', + color: colors.chat.textPrimary, lineHeight: 20, }, inputHighlightMention: { @@ -462,10 +494,10 @@ export const chatScreenStyles = StyleSheet.create({ // 面板 panelWrapper: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, borderTopWidth: 1, - borderTopColor: '#E8E8E8', - shadowColor: '#000', + borderTopColor: colors.chat.border, + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: -2 }, shadowOpacity: 0.1, shadowRadius: 8, @@ -473,11 +505,11 @@ export const chatScreenStyles = StyleSheet.create({ }, // 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充 emojiPanelWrapper: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, }, panelContainer: { flex: 1, - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, }, // 表情面板 @@ -505,16 +537,16 @@ export const chatScreenStyles = StyleSheet.create({ paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderTopWidth: 1, - borderTopColor: '#E8E8E8', + borderTopColor: colors.chat.border, }, panelCloseButton: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderRadius: 8, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, @@ -533,8 +565,8 @@ export const chatScreenStyles = StyleSheet.create({ paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderTopWidth: 1, - borderTopColor: '#E8E8E8', - backgroundColor: '#FFFFFF', + borderTopColor: colors.chat.border, + backgroundColor: colors.chat.card, }, panelTab: { width: 44, @@ -545,7 +577,7 @@ export const chatScreenStyles = StyleSheet.create({ marginRight: spacing.xs, }, panelTabActive: { - backgroundColor: '#DFF2FF', + backgroundColor: colors.chat.replyTint, }, panelTabEmoji: { fontSize: 24, @@ -553,7 +585,7 @@ export const chatScreenStyles = StyleSheet.create({ panelTabDivider: { width: 1, height: 24, - backgroundColor: '#E8E8E8', + backgroundColor: colors.chat.bubbleOutgoing, marginHorizontal: spacing.sm, }, @@ -571,13 +603,13 @@ export const chatScreenStyles = StyleSheet.create({ }, stickerEmptyText: { fontSize: 16, - color: '#666', + color: colors.chat.textTertiary, marginTop: spacing.md, fontWeight: '500', }, stickerEmptySubText: { fontSize: 13, - color: '#999', + color: colors.chat.textMuted, marginTop: spacing.xs, }, stickerGrid: { @@ -593,7 +625,7 @@ export const chatScreenStyles = StyleSheet.create({ margin: 4, borderRadius: 8, overflow: 'hidden', - backgroundColor: '#F5F5F5', + backgroundColor: colors.chat.surfaceInput, }, stickerImage: { width: '100%', @@ -618,8 +650,8 @@ export const chatScreenStyles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm, - backgroundColor: '#FFFFFF', - shadowColor: '#000', + backgroundColor: colors.chat.card, + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 4, @@ -627,7 +659,7 @@ export const chatScreenStyles = StyleSheet.create({ }, moreItemText: { fontSize: 13, - color: '#666', + color: colors.chat.textTertiary, fontWeight: '500', }, @@ -636,7 +668,7 @@ export const chatScreenStyles = StyleSheet.create({ position: 'absolute', left: 12, right: 12, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderRadius: 18, shadowColor: colors.primary.main, shadowOffset: { width: 0, height: -3 }, @@ -651,7 +683,7 @@ export const chatScreenStyles = StyleSheet.create({ // @成员选择面板 mentionPanelContainer: { flex: 1, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderRadius: 18, }, mentionPanelDragHandle: { @@ -663,7 +695,7 @@ export const chatScreenStyles = StyleSheet.create({ width: 32, height: 3, borderRadius: 2, - backgroundColor: '#E0E0E0', + backgroundColor: colors.chat.sheetGrip, }, mentionPanelHeader: { flexDirection: 'row', @@ -673,12 +705,12 @@ export const chatScreenStyles = StyleSheet.create({ paddingTop: 4, paddingBottom: 8, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#F0F0F0', + borderBottomColor: colors.chat.borderHairline, }, mentionPanelTitle: { fontSize: 12, fontWeight: '600', - color: '#AAAAAA', + color: colors.chat.textPlaceholder, letterSpacing: 0.5, textTransform: 'uppercase', }, @@ -686,7 +718,7 @@ export const chatScreenStyles = StyleSheet.create({ width: 26, height: 26, borderRadius: 13, - backgroundColor: '#F5F5F5', + backgroundColor: colors.chat.surfaceInput, alignItems: 'center', justifyContent: 'center', }, @@ -728,7 +760,7 @@ export const chatScreenStyles = StyleSheet.create({ mentionItemName: { fontSize: 15, fontWeight: '500', - color: '#1A1A1A', + color: colors.chat.textPrimary, }, mentionItemNameAll: { fontSize: 15, @@ -737,7 +769,7 @@ export const chatScreenStyles = StyleSheet.create({ }, mentionItemSub: { fontSize: 12, - color: '#BBBBBB', + color: colors.chat.iconMuted, marginTop: 1, }, mentionItemRole: { @@ -752,7 +784,7 @@ export const chatScreenStyles = StyleSheet.create({ }, mentionEmptyText: { fontSize: 14, - color: '#C8C8C8', + color: colors.chat.iconSoft, }, // 长按菜单 - 底部横条形式(类似QQ) @@ -763,10 +795,10 @@ export const chatScreenStyles = StyleSheet.create({ }, // 旧版菜单样式(保留兼容) menuContainer: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderRadius: 12, minWidth: 160, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12, @@ -781,21 +813,21 @@ export const chatScreenStyles = StyleSheet.create({ }, menuItemBorder: { borderBottomWidth: 1, - borderBottomColor: '#F0F0F0', + borderBottomColor: colors.chat.borderHairline, }, menuItemText: { fontSize: 15, - color: '#333', + color: colors.text.primary, fontWeight: '500', }, // 底部菜单容器 bottomMenuContainer: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingTop: spacing.md, paddingBottom: spacing.xl, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, shadowRadius: 8, @@ -803,7 +835,7 @@ export const chatScreenStyles = StyleSheet.create({ }, // 消息预览区域 messagePreviewContainer: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, marginHorizontal: spacing.md, marginBottom: spacing.md, paddingHorizontal: spacing.md, @@ -813,7 +845,7 @@ export const chatScreenStyles = StyleSheet.create({ }, messagePreviewText: { fontSize: 14, - color: '#666', + color: colors.chat.textTertiary, lineHeight: 20, }, // 操作按钮横条 @@ -824,7 +856,7 @@ export const chatScreenStyles = StyleSheet.create({ paddingHorizontal: spacing.sm, paddingVertical: spacing.md, borderBottomWidth: 1, - borderBottomColor: '#F0F0F0', + borderBottomColor: colors.chat.borderHairline, marginBottom: spacing.md, }, menuActionItem: { @@ -839,19 +871,19 @@ export const chatScreenStyles = StyleSheet.create({ width: 56, height: 56, borderRadius: 16, - backgroundColor: '#F0F7FF', + backgroundColor: colors.chat.overlayQuote, alignItems: 'center', justifyContent: 'center', marginBottom: 8, }, menuActionLabel: { fontSize: 13, - color: '#333', + color: colors.text.primary, fontWeight: '500', }, // 取消按钮 menuCancelButton: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, marginHorizontal: spacing.md, paddingVertical: spacing.md, borderRadius: 12, @@ -859,7 +891,7 @@ export const chatScreenStyles = StyleSheet.create({ }, menuCancelText: { fontSize: 16, - color: '#333', + color: colors.text.primary, fontWeight: '600', }, @@ -874,7 +906,7 @@ export const chatScreenStyles = StyleSheet.create({ borderRadius: 6, paddingVertical: 6, paddingHorizontal: 2, - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, @@ -901,20 +933,21 @@ export const chatScreenStyles = StyleSheet.create({ }, qqMenuItemLabel: { fontSize: 11, - color: '#FFFFFF', + color: colors.primary.contrast, fontWeight: '500', }, - // 回复预览 + // 回复预览(输入区)- 与气泡内引用同色条 replyPreviewContainer: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F7FA', + backgroundColor: 'rgba(74, 136, 199, 0.08)', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderLeftWidth: 3, - borderLeftColor: colors.primary.main, + borderLeftColor: colors.chat.link, marginBottom: spacing.xs, + borderRadius: 8, }, replyPreviewContent: { flex: 1, @@ -932,7 +965,7 @@ export const chatScreenStyles = StyleSheet.create({ }, replyPreviewMessage: { fontSize: 12, - color: '#8E8E93', + color: colors.chat.textSecondary, marginTop: 2, }, replyPreviewClose: { @@ -948,11 +981,11 @@ export const chatScreenStyles = StyleSheet.create({ zIndex: 1000, }, overlayContent: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, padding: spacing.xl, borderRadius: 16, alignItems: 'center', - shadowColor: '#000', + shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12, @@ -961,13 +994,13 @@ export const chatScreenStyles = StyleSheet.create({ overlayText: { marginTop: spacing.md, fontSize: 15, - color: '#666', + color: colors.chat.textTertiary, fontWeight: '500', }, // 表情包添加按钮(管理界面第一位) stickerAddButton: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderWidth: 1.5, borderColor: colors.primary.main, borderStyle: 'dashed', @@ -986,9 +1019,9 @@ export const chatScreenStyles = StyleSheet.create({ // 表情包管理按钮(表情面板第一位) stickerManageButton: { - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderWidth: 1, - borderColor: '#E8E8E8', + borderColor: colors.chat.border, borderStyle: 'dashed', alignItems: 'center', justifyContent: 'center', @@ -1000,7 +1033,7 @@ export const chatScreenStyles = StyleSheet.create({ }, stickerManageText: { fontSize: 12, - color: '#666', + color: colors.chat.textTertiary, marginTop: 4, fontWeight: '500', }, @@ -1018,20 +1051,20 @@ export const chatScreenStyles = StyleSheet.create({ width: 36, height: 5, borderRadius: 3, - backgroundColor: '#E0E0E0', + backgroundColor: colors.chat.sheetGrip, alignSelf: 'center', marginTop: 8, marginBottom: 4, }, manageHeaderDivider: { height: StyleSheet.hairlineWidth, - backgroundColor: '#F0F0F0', + backgroundColor: colors.chat.borderHairline, }, // 表情包选择状态 stickerItemSelected: { borderWidth: 2, - borderColor: '#7FB6E6', + borderColor: colors.chat.replyBorder, }, stickerCheckOverlay: { ...StyleSheet.absoluteFillObject, @@ -1045,19 +1078,19 @@ export const chatScreenStyles = StyleSheet.create({ height: 22, borderRadius: 11, borderWidth: 2, - borderColor: '#FFF', + borderColor: colors.chat.card, backgroundColor: 'rgba(255, 255, 255, 0.8)', alignItems: 'center', justifyContent: 'center', }, stickerCheckBoxSelected: { - backgroundColor: '#7FB6E6', - borderColor: '#7FB6E6', + backgroundColor: colors.chat.replyBorder, + borderColor: colors.chat.replyBorder, }, // 表情管理模态框 manageModalContainer: { - backgroundColor: '#F5F7FA', + backgroundColor: colors.chat.surfaceMuted, borderTopLeftRadius: 20, borderTopRightRadius: 20, overflow: 'hidden', @@ -1068,9 +1101,9 @@ export const chatScreenStyles = StyleSheet.create({ justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderBottomWidth: 1, - borderBottomColor: '#E8E8E8', + borderBottomColor: colors.chat.border, }, manageHeaderButton: { minWidth: 60, @@ -1079,28 +1112,28 @@ export const chatScreenStyles = StyleSheet.create({ manageModalTitle: { fontSize: 17, fontWeight: '600', - color: '#1A1A1A', + color: colors.chat.textPrimary, }, manageDoneText: { fontSize: 16, - color: '#4A88C7', + color: colors.chat.link, fontWeight: '500', }, manageSelectText: { fontSize: 16, - color: '#4A88C7', + color: colors.chat.link, fontWeight: '500', }, manageInfoBar: { paddingHorizontal: spacing.md, paddingVertical: spacing.sm, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderBottomWidth: 1, - borderBottomColor: '#E8E8E8', + borderBottomColor: colors.chat.border, }, manageInfoText: { fontSize: 13, - color: '#8E8E93', + color: colors.chat.textSecondary, }, manageStickerGrid: { flexDirection: 'row', @@ -1115,9 +1148,9 @@ export const chatScreenStyles = StyleSheet.create({ justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, - backgroundColor: '#FFFFFF', + backgroundColor: colors.chat.card, borderTopWidth: 1, - borderTopColor: '#E8E8E8', + borderTopColor: colors.chat.border, paddingBottom: 34, // 安全区域 }, manageSelectAllButton: { @@ -1126,24 +1159,24 @@ export const chatScreenStyles = StyleSheet.create({ }, manageSelectAllText: { fontSize: 15, - color: '#4A88C7', + color: colors.chat.link, fontWeight: '500', }, manageDeleteButton: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#FF3B30', + backgroundColor: colors.chat.danger, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm + 2, borderRadius: 20, gap: 6, }, manageDeleteButtonDisabled: { - backgroundColor: '#CCC', + backgroundColor: colors.chat.iconMuted, }, manageDeleteText: { fontSize: 15, - color: '#FFFFFF', + color: colors.primary.contrast, fontWeight: '600', }, manageTipBar: { @@ -1152,12 +1185,12 @@ export const chatScreenStyles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, - backgroundColor: '#FFF9E6', + backgroundColor: colors.chat.tipBg, gap: 6, }, manageTipText: { fontSize: 12, - color: '#999', + color: colors.chat.textMuted, }, // 管理界面空状态 @@ -1186,5 +1219,9 @@ export const chatScreenStyles = StyleSheet.create({ textAlign: 'center', }, }); +} -export default chatScreenStyles; +export function useChatScreenStyles() { + const colors = useAppColors(); + return useMemo(() => createChatScreenStyles(colors), [colors]); +} diff --git a/src/screens/message/components/ChatScreen/useChatScreen.ts b/src/screens/message/components/ChatScreen/useChatScreen.ts index 2208de9..225f410 100644 --- a/src/screens/message/components/ChatScreen/useChatScreen.ts +++ b/src/screens/message/components/ChatScreen/useChatScreen.ts @@ -546,6 +546,13 @@ export const useChatScreen = () => { isBrowsingHistoryRef.current = false; }, [isHistoryLoadingLocked]); + /** 用户点击「回到底部」:解除浏览历史锁并滚到最新消息端(inverted 下 offset=0) */ + const jumpToLatestMessages = useCallback(() => { + suppressAutoFollowRef.current = false; + isBrowsingHistoryRef.current = false; + scrollToLatest(true, true, 'jump-latest-button'); + }, [scrollToLatest]); + const setBrowsingHistory = useCallback((browsing: boolean) => { isBrowsingHistoryRef.current = browsing; }, []); @@ -1396,6 +1403,7 @@ export const useChatScreen = () => { handleClearConversation, handleMessageListContentSizeChange, handleReachLatestEdge, + jumpToLatestMessages, setBrowsingHistory, }; }; diff --git a/src/screens/message/components/ConversationListRow.tsx b/src/screens/message/components/ConversationListRow.tsx index 09ec234..cf1e9b3 100644 --- a/src/screens/message/components/ConversationListRow.tsx +++ b/src/screens/message/components/ConversationListRow.tsx @@ -3,10 +3,10 @@ * onPress 引用不参与比较,请配合父组件 ref + 按 id 查最新会话使用。 */ -import React, { memo, useEffect, useRef, useState } from 'react'; +import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, shadows } from '../../../theme'; +import { spacing, shadows, useAppColors, type AppColors } from '../../../theme'; import { ConversationResponse, extractTextFromSegments, @@ -150,6 +150,124 @@ function areConversationRowEqual( return true; } +function createConversationRowStyles(colors: AppColors) { + return StyleSheet.create({ + conversationItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: 14, + backgroundColor: colors.background.paper, + marginHorizontal: spacing.md, + marginTop: spacing.sm, + borderRadius: 12, + ...shadows.sm, + }, + conversationItemSelected: { + backgroundColor: colors.primary.light + '20', + borderColor: colors.primary.main, + borderWidth: 1, + }, + avatarContainer: { + position: 'relative', + }, + systemAvatar: { + width: 50, + height: 50, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.primary.main, + }, + conversationContent: { + flex: 1, + marginLeft: spacing.md, + }, + conversationHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + nameRow: { + flexDirection: 'row', + alignItems: 'center', + }, + officialBadge: { + backgroundColor: colors.primary.main, + borderRadius: 4, + paddingHorizontal: 6, + paddingVertical: 2, + marginRight: spacing.xs, + }, + officialBadgeText: { + color: colors.primary.contrast, + fontSize: 10, + fontWeight: '600', + }, + userName: { + fontWeight: '600', + color: colors.text.primary, + fontSize: 16, + }, + groupIcon: { + marginRight: 4, + }, + memberCount: { + fontSize: 12, + color: colors.text.secondary, + marginLeft: 2, + }, + pinnedIcon: { + marginLeft: 6, + }, + groupAvatar: { + width: 50, + height: 50, + }, + groupAvatarPlaceholder: { + width: 50, + height: 50, + borderRadius: 12, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + }, + timeText: { + color: colors.text.secondary, + fontSize: 12, + }, + messageRow: { + flexDirection: 'row', + alignItems: 'center', + }, + messageText: { + flex: 1, + color: colors.text.secondary, + fontSize: 14, + }, + unreadMessageText: { + color: colors.text.primary, + fontWeight: '500', + }, + unreadBadge: { + minWidth: 20, + height: 20, + borderRadius: 10, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + marginLeft: spacing.sm, + paddingHorizontal: 6, + }, + unreadBadgeText: { + color: colors.primary.contrast, + fontSize: 12, + fontWeight: '600', + }, + }); +} + const ConversationListRowInner: React.FC = ({ item, scale, @@ -158,6 +276,8 @@ const ConversationListRowInner: React.FC = ({ formatTime, systemChannelId, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createConversationRowStyles(colors), [colors]); const isSystemChannel = item.id === systemChannelId; const isGroupChat = !!(item.type === 'group' && item.group); @@ -196,7 +316,7 @@ const ConversationListRowInner: React.FC = ({ {isSystemChannel ? ( - + ) : isGroupChat ? ( @@ -204,7 +324,7 @@ const ConversationListRowInner: React.FC = ({ ) : ( - + )} @@ -271,119 +391,3 @@ const ConversationListRowInner: React.FC = ({ ConversationListRowInner.displayName = 'ConversationListRow'; export const ConversationListRow = memo(ConversationListRowInner, areConversationRowEqual); - -const styles = StyleSheet.create({ - conversationItem: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.md, - paddingVertical: 14, - backgroundColor: '#FFF', - marginHorizontal: spacing.md, - marginTop: spacing.sm, - borderRadius: 12, - ...shadows.sm, - }, - conversationItemSelected: { - backgroundColor: colors.primary.light + '20', - borderColor: colors.primary.main, - borderWidth: 1, - }, - avatarContainer: { - position: 'relative', - }, - systemAvatar: { - width: 50, - height: 50, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.primary.main, - }, - conversationContent: { - flex: 1, - marginLeft: spacing.md, - }, - conversationHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 4, - }, - nameRow: { - flexDirection: 'row', - alignItems: 'center', - }, - officialBadge: { - backgroundColor: colors.primary.main, - borderRadius: 4, - paddingHorizontal: 6, - paddingVertical: 2, - marginRight: spacing.xs, - }, - officialBadgeText: { - color: '#FFF', - fontSize: 10, - fontWeight: '600', - }, - userName: { - fontWeight: '600', - color: '#333', - fontSize: 16, - }, - groupIcon: { - marginRight: 4, - }, - memberCount: { - fontSize: 12, - color: '#999', - marginLeft: 2, - }, - pinnedIcon: { - marginLeft: 6, - }, - groupAvatar: { - width: 50, - height: 50, - }, - groupAvatarPlaceholder: { - width: 50, - height: 50, - borderRadius: 12, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - }, - timeText: { - color: '#999', - fontSize: 12, - }, - messageRow: { - flexDirection: 'row', - alignItems: 'center', - }, - messageText: { - flex: 1, - color: '#888', - fontSize: 14, - }, - unreadMessageText: { - color: '#333', - fontWeight: '500', - }, - unreadBadge: { - minWidth: 20, - height: 20, - borderRadius: 10, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - marginLeft: spacing.sm, - paddingHorizontal: 6, - }, - unreadBadgeText: { - color: '#FFF', - fontSize: 12, - fontWeight: '600', - }, -}); diff --git a/src/screens/message/components/EmbeddedChat.tsx b/src/screens/message/components/EmbeddedChat.tsx index 5357c57..0238ad2 100644 --- a/src/screens/message/components/EmbeddedChat.tsx +++ b/src/screens/message/components/EmbeddedChat.tsx @@ -3,7 +3,7 @@ * 嵌入式聊天组件 - 用于桌面端双栏布局右侧 */ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { View, FlatList, @@ -19,7 +19,7 @@ import { import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Image as ExpoImage } from 'expo-image'; -import { colors, spacing, fontSizes, shadows } from '../../../theme'; +import { spacing, fontSizes, shadows, useAppColors, type AppColors } from '../../../theme'; import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto'; import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common'; import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores'; @@ -34,12 +34,210 @@ import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen'; const { width: screenWidth } = Dimensions.get('window'); const PANEL_WIDTH = 360; +function createEmbeddedChatStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.chat.screen, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: colors.background.paper, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.divider, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + height: 56, + }, + headerButton: { + padding: spacing.xs, + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + headerCenter: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + headerTitle: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: colors.text.primary, + marginLeft: spacing.sm, + maxWidth: 200, + }, + messageList: { + flex: 1, + backgroundColor: colors.chat.screen, + }, + centerContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: spacing.xl, + }, + emptyText: { + marginTop: spacing.md, + fontSize: 16, + color: colors.text.secondary, + }, + emptySubtext: { + marginTop: spacing.xs, + fontSize: 14, + color: colors.text.disabled, + }, + listContent: { + padding: spacing.md, + paddingBottom: spacing.xl, + }, + messageRow: { + flexDirection: 'row', + alignItems: 'flex-end', + marginBottom: spacing.md, + maxWidth: screenWidth * 0.7, + }, + messageRowLeft: { + alignSelf: 'flex-start', + }, + messageRowRight: { + alignSelf: 'flex-end', + justifyContent: 'flex-end', + }, + messageBubble: { + maxWidth: screenWidth * 0.5, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + borderRadius: 18, + marginHorizontal: spacing.sm, + }, + messageBubbleMe: { + backgroundColor: colors.chat.replyTint, + borderBottomRightRadius: 4, + }, + messageBubbleOther: { + backgroundColor: colors.chat.bubbleIncoming, + borderBottomLeftRadius: 4, + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 0.5 }, + shadowOpacity: 0.04, + shadowRadius: 1, + elevation: 1, + }, + senderName: { + fontSize: 12, + color: colors.text.secondary, + marginBottom: 2, + }, + messageText: { + fontSize: 15, + lineHeight: 20, + }, + messageTextMe: { + color: colors.chat.textPrimary, + }, + messageTextOther: { + color: colors.chat.textPrimary, + }, + inputArea: { + backgroundColor: colors.chat.screen, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.divider, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm, + }, + inputRow: { + flexDirection: 'row', + alignItems: 'center', + }, + iconButton: { + padding: spacing.xs, + width: 44, + height: 44, + alignItems: 'center', + justifyContent: 'center', + }, + inputContainer: { + flex: 1, + backgroundColor: colors.chat.surfaceInput, + borderRadius: 20, + paddingHorizontal: spacing.md, + minHeight: 40, + justifyContent: 'center', + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + textInput: { + fontSize: 15, + color: colors.text.primary, + padding: 0, + maxHeight: 100, + }, + sendButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + marginLeft: spacing.xs, + }, + sendButtonDisabled: { + backgroundColor: colors.background.disabled, + }, + messageTextRecalled: { + fontStyle: 'italic', + color: colors.text.secondary, + }, + imageGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 4, + }, + messageImage: { + borderRadius: 8, + marginRight: 4, + marginBottom: 4, + }, + imagePlaceholder: { + width: 120, + height: 120, + borderRadius: 8, + backgroundColor: colors.background.disabled, + justifyContent: 'center', + alignItems: 'center', + marginRight: 4, + marginBottom: 4, + }, + imagePlaceholderText: { + color: colors.text.secondary, + fontSize: 12, + marginTop: 4, + }, + }); +} + interface EmbeddedChatProps { conversation: ConversationResponse; onBack: () => void; } export const EmbeddedChat: React.FC = ({ conversation, onBack }) => { + const colors = useAppColors(); + const styles = useMemo(() => createEmbeddedChatStyles(colors), [colors]); const router = useRouter(); const currentUser = useAuthStore(state => state.currentUser); @@ -411,7 +609,7 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack {/* 大屏幕(>= 768px)时隐藏返回按钮 */} {!isWideScreen ? ( - + ) : ( )} @@ -426,11 +624,11 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack {/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */} {isWideScreen && isGroupChat ? ( - + ) : ( - + )} @@ -443,7 +641,7 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack ) : messages.length === 0 ? ( - + 暂无消息 发送第一条消息开始聊天 @@ -472,7 +670,7 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack - + @@ -480,7 +678,7 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack ref={inputRef} style={styles.textInput} placeholder="发送消息..." - placeholderTextColor="#999" + placeholderTextColor={colors.text.hint} value={inputText} onChangeText={setInputText} multiline={false} @@ -494,7 +692,7 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack - + = ({ conversation, onBack disabled={!inputText.trim() || sending} > {sending ? ( - + ) : ( - + )} @@ -604,190 +802,3 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack ); }; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#F5F7FA', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - backgroundColor: '#FFF', - borderBottomWidth: 1, - borderBottomColor: '#E8E8E8', - ...shadows.sm, - height: 56, - }, - headerButton: { - padding: spacing.xs, - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - }, - headerCenter: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - headerTitle: { - fontSize: fontSizes.lg, - fontWeight: '600', - color: '#333', - marginLeft: spacing.sm, - maxWidth: 200, - }, - messageList: { - flex: 1, - backgroundColor: '#F5F7FA', - }, - centerContainer: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: spacing.xl, - }, - emptyText: { - marginTop: spacing.md, - fontSize: 16, - color: '#999', - }, - emptySubtext: { - marginTop: spacing.xs, - fontSize: 14, - color: '#BBB', - }, - listContent: { - padding: spacing.md, - paddingBottom: spacing.xl, - }, - messageRow: { - flexDirection: 'row', - alignItems: 'flex-end', - marginBottom: spacing.md, - maxWidth: screenWidth * 0.7, - }, - messageRowLeft: { - alignSelf: 'flex-start', - }, - messageRowRight: { - alignSelf: 'flex-end', - justifyContent: 'flex-end', - }, - messageBubble: { - maxWidth: screenWidth * 0.5, - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - borderRadius: 18, - marginHorizontal: spacing.sm, - }, - messageBubbleMe: { - backgroundColor: colors.primary.main, - borderBottomRightRadius: 4, - }, - messageBubbleOther: { - backgroundColor: '#FFF', - borderBottomLeftRadius: 4, - ...shadows.sm, - }, - senderName: { - fontSize: 12, - color: '#999', - marginBottom: 2, - }, - messageText: { - fontSize: 15, - lineHeight: 20, - }, - messageTextMe: { - color: '#FFF', - }, - messageTextOther: { - color: '#333', - }, - inputArea: { - backgroundColor: '#FFF', - borderTopWidth: 1, - borderTopColor: '#E8E8E8', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, - paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm, - }, - inputRow: { - flexDirection: 'row', - alignItems: 'center', - }, - iconButton: { - padding: spacing.xs, - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - inputContainer: { - flex: 1, - backgroundColor: '#F5F5F5', - borderRadius: 20, - paddingHorizontal: spacing.md, - minHeight: 40, - justifyContent: 'center', - // 移除阴影 - shadowColor: 'transparent', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0, - shadowRadius: 0, - elevation: 0, - }, - textInput: { - fontSize: 15, - color: '#333', - padding: 0, - maxHeight: 100, - }, - sendButton: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - marginLeft: spacing.xs, - }, - sendButtonDisabled: { - backgroundColor: '#E0E0E0', - }, - messageTextRecalled: { - fontStyle: 'italic', - color: '#999', - }, - imageGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - marginTop: 4, - }, - messageImage: { - borderRadius: 8, - marginRight: 4, - marginBottom: 4, - }, - imagePlaceholder: { - width: 120, - height: 120, - borderRadius: 8, - backgroundColor: 'rgba(0,0,0,0.1)', - justifyContent: 'center', - alignItems: 'center', - marginRight: 4, - marginBottom: 4, - }, - imagePlaceholderText: { - color: '#999', - fontSize: 12, - marginTop: 4, - }, -}); diff --git a/src/screens/message/components/GroupRequestShared.tsx b/src/screens/message/components/GroupRequestShared.tsx index 856286c..1512354 100644 --- a/src/screens/message/components/GroupRequestShared.tsx +++ b/src/screens/message/components/GroupRequestShared.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text } from '../../../components/common'; -import { colors, spacing, borderRadius, shadows } from '../../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../../theme'; interface GroupInfoSummaryCardProps { groupName?: string; @@ -14,6 +14,79 @@ interface GroupInfoSummaryCardProps { memberCountText?: string; } +function createGroupRequestSharedStyles(colors: AppColors) { + return StyleSheet.create({ + headerCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + marginBottom: spacing.md, + ...shadows.sm, + }, + groupHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + groupInfo: { + marginLeft: spacing.lg, + flex: 1, + }, + groupName: { + marginBottom: spacing.xs, + fontWeight: '700', + }, + groupMeta: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + groupNoText: { + marginTop: spacing.xs, + }, + descriptionContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + backgroundColor: colors.background.default, + borderRadius: borderRadius.lg, + padding: spacing.md, + marginTop: spacing.md, + }, + groupDesc: { + marginLeft: spacing.sm, + flex: 1, + lineHeight: 20, + }, + footer: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.sm, + flexDirection: 'row', + backgroundColor: colors.background.default, + }, + btn: { + flex: 1, + height: 42, + borderRadius: borderRadius.md, + alignItems: 'center', + justifyContent: 'center', + }, + reject: { + marginRight: spacing.sm, + backgroundColor: colors.error.light + '25', + borderWidth: 1, + borderColor: colors.error.light, + }, + approve: { + marginLeft: spacing.sm, + backgroundColor: colors.primary.main, + }, + statusBox: { + alignItems: 'center', + paddingTop: spacing.sm, + backgroundColor: colors.background.default, + }, + }); +} + export const GroupInfoSummaryCard: React.FC = ({ groupName, groupAvatar, @@ -21,12 +94,16 @@ export const GroupInfoSummaryCard: React.FC = ({ groupDescription, memberCountText, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]); return ( - {groupName || '群聊'} + + {groupName || '群聊'} + @@ -63,93 +140,34 @@ export const DecisionFooter: React.FC = ({ onApprove, processedText = '该请求已处理', }) => { + const colors = useAppColors(); + const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]); const insets = useSafeAreaInsets(); if (canAction) { return ( - 拒绝 + + 拒绝 + - {submitting ? : 同意} + {submitting ? ( + + ) : ( + + 同意 + + )} ); } return ( - {processedText} + + {processedText} + ); }; - -const styles = StyleSheet.create({ - headerCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - marginBottom: spacing.md, - ...shadows.sm, - }, - groupHeader: { - flexDirection: 'row', - alignItems: 'center', - }, - groupInfo: { - marginLeft: spacing.lg, - flex: 1, - }, - groupName: { - marginBottom: spacing.xs, - fontWeight: '700', - }, - groupMeta: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - }, - groupNoText: { - marginTop: spacing.xs, - }, - descriptionContainer: { - flexDirection: 'row', - alignItems: 'flex-start', - backgroundColor: colors.background.default, - borderRadius: borderRadius.lg, - padding: spacing.md, - marginTop: spacing.md, - }, - groupDesc: { - marginLeft: spacing.sm, - flex: 1, - lineHeight: 20, - }, - footer: { - paddingHorizontal: spacing.lg, - paddingTop: spacing.sm, - flexDirection: 'row', - backgroundColor: colors.background.default, - }, - btn: { - flex: 1, - height: 42, - borderRadius: borderRadius.md, - alignItems: 'center', - justifyContent: 'center', - }, - reject: { - marginRight: spacing.sm, - backgroundColor: colors.error.light + '25', - borderWidth: 1, - borderColor: colors.error.light, - }, - approve: { - marginLeft: spacing.sm, - backgroundColor: colors.primary.main, - }, - statusBox: { - alignItems: 'center', - paddingTop: spacing.sm, - backgroundColor: colors.background.default, - }, -}); diff --git a/src/screens/message/components/MutualFollowSelectorModal.tsx b/src/screens/message/components/MutualFollowSelectorModal.tsx index 9341041..2b04d75 100644 --- a/src/screens/message/components/MutualFollowSelectorModal.tsx +++ b/src/screens/message/components/MutualFollowSelectorModal.tsx @@ -12,7 +12,7 @@ import { } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../../theme'; import { authService } from '../../../services/authService'; import { useAuthStore } from '../../../stores'; import { Avatar, EmptyState, Loading, Text } from '../../../components/common'; @@ -41,6 +41,8 @@ const MutualFollowSelectorModal: React.FC = ({ onClose, onConfirm, }) => { + const colors = useAppColors(); + const styles = useMemo(() => createMutualFollowSelectorStyles(colors), [colors]); const { currentUser } = useAuthStore(); const [friendList, setFriendList] = useState([]); @@ -209,93 +211,96 @@ const MutualFollowSelectorModal: React.FC = ({ ); }; -const styles = StyleSheet.create({ - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'flex-end', - }, - modalContent: { - backgroundColor: colors.background.paper, - borderTopLeftRadius: borderRadius['2xl'], - borderTopRightRadius: borderRadius['2xl'], - height: '80%', - paddingHorizontal: spacing.lg, - paddingTop: spacing.lg, - paddingBottom: spacing.xl, - }, - modalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.lg, - }, - modalHeaderButton: { - paddingVertical: spacing.sm, - minWidth: 60, - }, - modalTitle: { - fontWeight: '700', - fontSize: fontSizes.xl, - }, - confirmButton: { - fontWeight: '600', - textAlign: 'right', - }, - tipContainer: { - flexDirection: 'row', - backgroundColor: colors.background.default, - borderRadius: borderRadius.lg, - padding: spacing.xs, - marginBottom: spacing.md, - }, - tipText: { - fontWeight: '600', - }, - selectedBadge: { - backgroundColor: colors.primary.light + '20', - paddingHorizontal: spacing.md, - paddingVertical: spacing.xs, - borderRadius: borderRadius.sm, - alignSelf: 'flex-start', - marginBottom: spacing.md, - }, - friendListContent: { - paddingBottom: spacing.xl, - }, - friendItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.md, - paddingHorizontal: spacing.sm, - borderRadius: borderRadius.lg, - marginBottom: spacing.xs, - }, - friendItemSelected: { - backgroundColor: colors.primary.light + '15', - }, - friendInfo: { - flex: 1, - marginLeft: spacing.md, - marginRight: spacing.sm, - }, - nickname: { - fontWeight: '500', - marginBottom: 2, - }, - checkbox: { - width: 26, - height: 26, - borderRadius: 13, - borderWidth: 2, - borderColor: colors.divider, - justifyContent: 'center', - alignItems: 'center', - }, - checkboxSelected: { - backgroundColor: colors.primary.main, - borderColor: colors.primary.main, - }, -}); +function createMutualFollowSelectorStyles(colors: AppColors) { + return StyleSheet.create({ + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: colors.background.paper, + borderTopLeftRadius: borderRadius['2xl'], + borderTopRightRadius: borderRadius['2xl'], + height: '80%', + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg, + paddingBottom: spacing.xl, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.lg, + }, + modalHeaderButton: { + paddingVertical: spacing.sm, + minWidth: 60, + }, + modalTitle: { + fontWeight: '700', + fontSize: fontSizes.xl, + color: colors.text.primary, + }, + confirmButton: { + fontWeight: '600', + textAlign: 'right', + }, + tipContainer: { + flexDirection: 'row', + backgroundColor: colors.background.default, + borderRadius: borderRadius.lg, + padding: spacing.xs, + marginBottom: spacing.md, + }, + tipText: { + fontWeight: '600', + }, + selectedBadge: { + backgroundColor: colors.primary.light + '20', + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: borderRadius.sm, + alignSelf: 'flex-start', + marginBottom: spacing.md, + }, + friendListContent: { + paddingBottom: spacing.xl, + }, + friendItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + paddingHorizontal: spacing.sm, + borderRadius: borderRadius.lg, + marginBottom: spacing.xs, + }, + friendItemSelected: { + backgroundColor: colors.primary.light + '15', + }, + friendInfo: { + flex: 1, + marginLeft: spacing.md, + marginRight: spacing.sm, + }, + nickname: { + fontWeight: '500', + marginBottom: 2, + }, + checkbox: { + width: 26, + height: 26, + borderRadius: 13, + borderWidth: 2, + borderColor: colors.divider, + justifyContent: 'center', + alignItems: 'center', + }, + checkboxSelected: { + backgroundColor: colors.primary.main, + borderColor: colors.primary.main, + }, + }); +} export default MutualFollowSelectorModal; diff --git a/src/screens/profile/AccountSecurityScreen.tsx b/src/screens/profile/AccountSecurityScreen.tsx index 1fb1e2f..ac85f26 100644 --- a/src/screens/profile/AccountSecurityScreen.tsx +++ b/src/screens/profile/AccountSecurityScreen.tsx @@ -9,7 +9,7 @@ import { } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { Text, ResponsiveContainer } from '../../components/common'; import { useResponsive } from '../../hooks'; import { authService, resolveAuthApiError } from '../../services/authService'; @@ -17,6 +17,8 @@ import { showPrompt } from '../../services/promptService'; import { useAuthStore } from '../../stores'; export const AccountSecurityScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]); const { isWideScreen, isMobile } = useResponsive(); const insets = useSafeAreaInsets(); const currentUser = useAuthStore((state) => state.currentUser); @@ -232,7 +234,7 @@ export const AccountSecurityScreen: React.FC = () => { disabled={sendingCode || countdown > 0} > {sendingCode ? ( - + ) : ( {countdown > 0 ? `${countdown}s` : '发送验证码'} )} @@ -244,7 +246,11 @@ export const AccountSecurityScreen: React.FC = () => { onPress={handleVerifyEmail} disabled={verifyingEmail} > - {verifyingEmail ? : 验证邮箱} + {verifyingEmail ? ( + + ) : ( + 验证邮箱 + )} @@ -298,7 +304,7 @@ export const AccountSecurityScreen: React.FC = () => { disabled={sendingChangePwdCode || changePwdCountdown > 0} > {sendingChangePwdCode ? ( - + ) : ( {changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'} @@ -323,7 +329,11 @@ export const AccountSecurityScreen: React.FC = () => { onPress={handleChangePassword} disabled={updatingPassword} > - {updatingPassword ? : 更新密码} + {updatingPassword ? ( + + ) : ( + 更新密码 + )} @@ -343,109 +353,117 @@ export const AccountSecurityScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - scrollContent: { - paddingVertical: spacing.md, - }, - section: { - marginBottom: spacing.lg, - }, - sectionHeader: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.lg, - marginBottom: spacing.sm, - gap: spacing.xs, - }, - sectionTitle: { - fontWeight: '600', - }, - card: { - backgroundColor: colors.background.paper, - marginHorizontal: spacing.lg, - borderRadius: borderRadius.lg, - padding: spacing.md, - }, - statusRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.md, - }, - statusBadge: { - paddingHorizontal: spacing.sm, - paddingVertical: 4, - borderRadius: borderRadius.sm, - }, - statusVerified: { - backgroundColor: '#E8F5E9', - }, - statusUnverified: { - backgroundColor: '#FFF3E0', - }, - inputWrapper: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.default, - borderRadius: borderRadius.lg, - borderWidth: 1, - borderColor: colors.divider, - paddingHorizontal: spacing.md, - height: 48, - marginBottom: spacing.sm, - }, - inputIcon: { - marginRight: spacing.sm, - }, - input: { - flex: 1, - color: colors.text.primary, - fontSize: fontSizes.md, - }, - codeRow: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - marginBottom: spacing.sm, - }, - codeInput: { - flex: 1, - marginBottom: 0, - }, - sendCodeButton: { - height: 48, - minWidth: 110, - borderRadius: borderRadius.lg, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: spacing.sm, - }, - sendCodeButtonText: { - color: '#fff', - fontSize: fontSizes.sm, - fontWeight: '600', - }, - primaryButton: { - height: 48, - borderRadius: borderRadius.lg, - backgroundColor: colors.primary.main, - alignItems: 'center', - justifyContent: 'center', - marginTop: spacing.xs, - }, - primaryButtonText: { - color: '#fff', - fontSize: fontSizes.md, - fontWeight: '700', - }, - buttonDisabled: { - opacity: 0.6, - }, -}); +function createAccountSecurityStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollContent: { + paddingVertical: spacing.md, + }, + section: { + marginBottom: spacing.lg, + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.lg, + marginBottom: spacing.sm, + gap: spacing.xs, + }, + sectionTitle: { + fontWeight: '600', + }, + card: { + backgroundColor: colors.background.paper, + marginHorizontal: spacing.lg, + borderRadius: borderRadius.lg, + padding: spacing.md, + }, + statusRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.md, + }, + statusBadge: { + paddingHorizontal: spacing.sm, + paddingVertical: 4, + borderRadius: borderRadius.sm, + }, + statusVerified: { + backgroundColor: colors.success.light + '40', + }, + statusUnverified: { + backgroundColor: colors.warning.light + '40', + }, + statusVerifiedText: { + color: colors.success.dark, + }, + statusUnverifiedText: { + color: colors.warning.dark, + }, + inputWrapper: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.default, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.divider, + paddingHorizontal: spacing.md, + height: 48, + marginBottom: spacing.sm, + }, + inputIcon: { + marginRight: spacing.sm, + }, + input: { + flex: 1, + color: colors.text.primary, + fontSize: fontSizes.md, + }, + codeRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + marginBottom: spacing.sm, + }, + codeInput: { + flex: 1, + marginBottom: 0, + }, + sendCodeButton: { + height: 48, + minWidth: 110, + borderRadius: borderRadius.lg, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: spacing.sm, + }, + sendCodeButtonText: { + color: colors.text.inverse, + fontSize: fontSizes.sm, + fontWeight: '600', + }, + primaryButton: { + height: 48, + borderRadius: borderRadius.lg, + backgroundColor: colors.primary.main, + alignItems: 'center', + justifyContent: 'center', + marginTop: spacing.xs, + }, + primaryButtonText: { + color: colors.text.inverse, + fontSize: fontSizes.md, + fontWeight: '700', + }, + buttonDisabled: { + opacity: 0.6, + }, + }); +} export default AccountSecurityScreen; diff --git a/src/screens/profile/BlockedUsersScreen.tsx b/src/screens/profile/BlockedUsersScreen.tsx index fd282b9..97ef4a5 100644 --- a/src/screens/profile/BlockedUsersScreen.tsx +++ b/src/screens/profile/BlockedUsersScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, FlatList, @@ -12,12 +12,52 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useRouter } from 'expo-router'; import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common'; import { authService } from '../../services'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { User } from '../../types'; import { useResponsive } from '../../hooks'; import * as hrefs from '../../navigation/hrefs'; +function createBlockedUsersStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + loadingWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + listContent: { + padding: spacing.md, + gap: spacing.sm, + }, + emptyContent: { + flexGrow: 1, + justifyContent: 'center', + }, + item: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + ...shadows.sm, + }, + content: { + flex: 1, + marginLeft: spacing.md, + marginRight: spacing.sm, + }, + nickname: { + fontWeight: '600', + }, + }); +} + export const BlockedUsersScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createBlockedUsersStyles(colors), [colors]); const router = useRouter(); const { isMobile } = useResponsive(); const insets = useSafeAreaInsets(); @@ -141,40 +181,4 @@ export const BlockedUsersScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - loadingWrap: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - listContent: { - padding: spacing.md, - gap: spacing.sm, - }, - emptyContent: { - flexGrow: 1, - justifyContent: 'center', - }, - item: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.md, - ...shadows.sm, - }, - content: { - flex: 1, - marginLeft: spacing.md, - marginRight: spacing.sm, - }, - nickname: { - fontWeight: '600', - }, -}); - export default BlockedUsersScreen; diff --git a/src/screens/profile/EditProfileScreen.tsx b/src/screens/profile/EditProfileScreen.tsx index 6a95d41..a57576a 100644 --- a/src/screens/profile/EditProfileScreen.tsx +++ b/src/screens/profile/EditProfileScreen.tsx @@ -5,7 +5,7 @@ * 表单在宽屏下居中显示 */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { View, ScrollView, @@ -22,12 +22,274 @@ import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { LinearGradient } from 'expo-linear-gradient'; -import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme'; +import { + spacing, + fontSizes, + borderRadius, + shadows, + useAppColors, + type AppColors, +} from '../../theme'; import { useAuthStore } from '../../stores'; import { Avatar, Button, Text, ResponsiveContainer } from '../../components/common'; import { authService, uploadService } from '../../services'; import { useResponsive } from '../../hooks'; +function createEditProfileStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + keyboardView: { + flex: 1, + }, + scrollContent: { + padding: spacing.lg, + }, + + // ===== 预览区域 - 与 UserProfileHeader 完全一致 ===== + previewContainer: { + marginBottom: spacing.lg, + }, + coverContainer: { + position: 'relative', + overflow: 'hidden', + borderRadius: borderRadius.lg, + }, + coverTouchable: { + width: '100%', + height: '100%', + }, + coverImage: { + width: '100%', + height: '100%', + }, + gradient: { + width: '100%', + height: '100%', + }, + coverUploadingOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + coverEditOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.3)', + justifyContent: 'center', + alignItems: 'center', + }, + coverEditText: { + color: colors.text.inverse, + fontSize: fontSizes.sm, + marginTop: spacing.xs, + fontWeight: '500', + }, + waveDecoration: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 40, + }, + wave: { + width: '100%', + height: '100%', + backgroundColor: colors.background.paper, + borderTopLeftRadius: 30, + borderTopRightRadius: 30, + }, + + // 用户信息卡片 + profileCard: { + backgroundColor: colors.background.paper, + marginHorizontal: spacing.md, + marginTop: -50, + borderRadius: borderRadius.xl, + padding: spacing.lg, + ...shadows.md, + }, + avatarWrapper: { + alignItems: 'center', + marginTop: -60, + marginBottom: spacing.md, + }, + avatarContainer: { + position: 'relative', + padding: 4, + backgroundColor: colors.background.paper, + borderRadius: 50, + }, + avatarUploadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: 60, + justifyContent: 'center', + alignItems: 'center', + }, + editAvatarButton: { + position: 'absolute', + bottom: 0, + right: 0, + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: colors.primary.main, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: colors.background.paper, + }, + userInfo: { + alignItems: 'center', + marginBottom: spacing.md, + }, + nickname: { + marginBottom: spacing.xs, + fontWeight: '700', + }, + username: { + marginBottom: spacing.sm, + }, + bio: { + textAlign: 'center', + marginTop: spacing.sm, + lineHeight: 20, + }, + bioPlaceholder: { + textAlign: 'center', + marginTop: spacing.sm, + fontStyle: 'italic', + }, + metaInfo: { + flexDirection: 'row', + justifyContent: 'center', + flexWrap: 'wrap', + marginBottom: spacing.md, + gap: spacing.sm, + }, + metaTag: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.default, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.md, + }, + metaTagText: { + marginLeft: spacing.xs, + fontSize: fontSizes.xs, + }, + editHint: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.background.default, + borderRadius: borderRadius.md, + paddingVertical: spacing.sm, + gap: spacing.xs, + }, + editHintText: { + fontSize: fontSizes.xs, + }, + + // ===== 表单区域 ===== + formCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.lg, + marginBottom: spacing.lg, + shadowColor: colors.text.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + sectionTitle: { + marginBottom: spacing.lg, + }, + formField: { + flexDirection: 'row', + alignItems: 'flex-start', + }, + fieldIconContainer: { + width: 40, + height: 40, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.md, + marginTop: spacing.xs, + }, + fieldContent: { + flex: 1, + }, + fieldLabel: { + marginBottom: spacing.xs, + fontWeight: '500', + }, + fieldInput: { + fontSize: fontSizes.md, + color: colors.text.primary, + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + textArea: { + height: 80, + textAlignVertical: 'top', + }, + disabledInput: { + color: colors.text.disabled, + }, + divider: { + height: 1, + backgroundColor: colors.divider, + marginVertical: spacing.md, + marginLeft: 56, + }, + + // 保存按钮 + buttonContainer: { + marginTop: spacing.sm, + }, + saveButton: { + backgroundColor: colors.primary.main, + borderRadius: borderRadius.lg, + paddingVertical: spacing.md, + shadowColor: colors.primary.main, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 4, + }, + saveButtonDisabled: { + opacity: 0.6, + }, + saveButtonContent: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + buttonIcon: { + marginRight: spacing.sm, + }, + saveButtonText: { + color: colors.text.inverse, + }, + }); +} + +type EditProfileSheet = ReturnType; + + // 表单输入项组件 interface FormFieldProps { icon: string; @@ -41,6 +303,8 @@ interface FormFieldProps { autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url'; editable?: boolean; + sheet: EditProfileSheet; + colors: AppColors; } const FormField: React.FC = ({ @@ -55,19 +319,21 @@ const FormField: React.FC = ({ autoCapitalize = 'sentences', keyboardType = 'default', editable = true, + sheet, + colors, }) => { return ( - - + + - - {label} + + {label} = ({ }; export const EditProfileScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createEditProfileStyles(colors), [colors]); const navigation = useNavigation(); const { currentUser, updateUser } = useAuthStore(); const { isWideScreen, isMobile, width } = useResponsive(); @@ -401,6 +669,8 @@ export const EditProfileScreen: React.FC = () => { onChangeText={setNickname} placeholder="请输入昵称" maxLength={20} + sheet={styles} + colors={colors} /> @@ -414,6 +684,8 @@ export const EditProfileScreen: React.FC = () => { maxLength={100} multiline numberOfLines={3} + sheet={styles} + colors={colors} /> @@ -425,6 +697,8 @@ export const EditProfileScreen: React.FC = () => { onChangeText={setLocation} placeholder="你所在的城市" maxLength={30} + sheet={styles} + colors={colors} /> @@ -438,6 +712,8 @@ export const EditProfileScreen: React.FC = () => { maxLength={100} autoCapitalize="none" keyboardType="url" + sheet={styles} + colors={colors} /> @@ -455,6 +731,8 @@ export const EditProfileScreen: React.FC = () => { placeholder="请输入手机号" maxLength={11} keyboardType="phone-pad" + sheet={styles} + colors={colors} /> @@ -468,6 +746,8 @@ export const EditProfileScreen: React.FC = () => { maxLength={100} autoCapitalize="none" keyboardType="email-address" + sheet={styles} + colors={colors} /> @@ -535,254 +815,5 @@ export const EditProfileScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - keyboardView: { - flex: 1, - }, - scrollContent: { - padding: spacing.lg, - }, - - // ===== 预览区域 - 与 UserProfileHeader 完全一致 ===== - previewContainer: { - marginBottom: spacing.lg, - }, - coverContainer: { - position: 'relative', - overflow: 'hidden', - borderRadius: borderRadius.lg, - }, - coverTouchable: { - width: '100%', - height: '100%', - }, - coverImage: { - width: '100%', - height: '100%', - }, - gradient: { - width: '100%', - height: '100%', - }, - coverUploadingOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - coverEditOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.3)', - justifyContent: 'center', - alignItems: 'center', - }, - coverEditText: { - color: colors.text.inverse, - fontSize: fontSizes.sm, - marginTop: spacing.xs, - fontWeight: '500', - }, - waveDecoration: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - height: 40, - }, - wave: { - width: '100%', - height: '100%', - backgroundColor: colors.background.paper, - borderTopLeftRadius: 30, - borderTopRightRadius: 30, - }, - - // 用户信息卡片 - profileCard: { - backgroundColor: colors.background.paper, - marginHorizontal: spacing.md, - marginTop: -50, - borderRadius: borderRadius.xl, - padding: spacing.lg, - ...shadows.md, - }, - avatarWrapper: { - alignItems: 'center', - marginTop: -60, - marginBottom: spacing.md, - }, - avatarContainer: { - position: 'relative', - padding: 4, - backgroundColor: colors.background.paper, - borderRadius: 50, - }, - avatarUploadingOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - borderRadius: 60, - justifyContent: 'center', - alignItems: 'center', - }, - editAvatarButton: { - position: 'absolute', - bottom: 0, - right: 0, - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: colors.primary.main, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 2, - borderColor: colors.background.paper, - }, - userInfo: { - alignItems: 'center', - marginBottom: spacing.md, - }, - nickname: { - marginBottom: spacing.xs, - fontWeight: '700', - }, - username: { - marginBottom: spacing.sm, - }, - bio: { - textAlign: 'center', - marginTop: spacing.sm, - lineHeight: 20, - }, - bioPlaceholder: { - textAlign: 'center', - marginTop: spacing.sm, - fontStyle: 'italic', - }, - metaInfo: { - flexDirection: 'row', - justifyContent: 'center', - flexWrap: 'wrap', - marginBottom: spacing.md, - gap: spacing.sm, - }, - metaTag: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.default, - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - borderRadius: borderRadius.md, - }, - metaTagText: { - marginLeft: spacing.xs, - fontSize: fontSizes.xs, - }, - editHint: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.background.default, - borderRadius: borderRadius.md, - paddingVertical: spacing.sm, - gap: spacing.xs, - }, - editHintText: { - fontSize: fontSizes.xs, - }, - - // ===== 表单区域 ===== - formCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.lg, - marginBottom: spacing.lg, - shadowColor: colors.text.primary, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - elevation: 2, - }, - sectionTitle: { - marginBottom: spacing.lg, - }, - formField: { - flexDirection: 'row', - alignItems: 'flex-start', - }, - fieldIconContainer: { - width: 40, - height: 40, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - marginTop: spacing.xs, - }, - fieldContent: { - flex: 1, - }, - fieldLabel: { - marginBottom: spacing.xs, - fontWeight: '500', - }, - fieldInput: { - fontSize: fontSizes.md, - color: colors.text.primary, - paddingVertical: spacing.sm, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - textArea: { - height: 80, - textAlignVertical: 'top', - }, - disabledInput: { - color: colors.text.disabled, - }, - divider: { - height: 1, - backgroundColor: colors.divider, - marginVertical: spacing.md, - marginLeft: 56, - }, - - // 保存按钮 - buttonContainer: { - marginTop: spacing.sm, - }, - saveButton: { - backgroundColor: colors.primary.main, - borderRadius: borderRadius.lg, - paddingVertical: spacing.md, - shadowColor: colors.primary.main, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, - }, - saveButtonDisabled: { - opacity: 0.6, - }, - saveButtonContent: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - }, - buttonIcon: { - marginRight: spacing.sm, - }, - saveButtonText: { - color: colors.text.inverse, - }, -}); export default EditProfileScreen; diff --git a/src/screens/profile/FollowListScreen.tsx b/src/screens/profile/FollowListScreen.tsx index f6802a0..17c2ce2 100644 --- a/src/screens/profile/FollowListScreen.tsx +++ b/src/screens/profile/FollowListScreen.tsx @@ -5,7 +5,7 @@ * 在宽屏下使用网格布局 */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, FlatList, @@ -17,7 +17,7 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; -import { colors, spacing, borderRadius, shadows } from '../../theme'; +import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { User } from '../../types'; import { useAuthStore, useUserStore } from '../../stores'; import { authService } from '../../services'; @@ -26,6 +26,8 @@ import * as hrefs from '../../navigation/hrefs'; import { useResponsive, useColumnCount } from '../../hooks'; const FollowListScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createFollowListStyles(colors), [colors]); const router = useRouter(); const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>(); const type = typeParam === 'followers' ? 'followers' : 'following'; @@ -356,136 +358,138 @@ const FollowListScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - // 列表布局样式 - listContent: { - flexGrow: 1, - paddingHorizontal: spacing.md, - paddingTop: spacing.md, - paddingBottom: spacing.lg, - }, - headerCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - paddingHorizontal: spacing.lg, - paddingVertical: spacing.md, - marginBottom: spacing.md, - borderWidth: 1, - borderColor: colors.divider + '4A', - ...shadows.sm, - }, - headerAccent: { - width: 28, - height: 3, - borderRadius: 999, - backgroundColor: colors.primary.main + 'A6', - marginBottom: spacing.sm, - }, - headerMainRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - headerTitle: { - fontWeight: '700', - }, - headerCountPill: { - paddingHorizontal: spacing.sm, - paddingVertical: 4, - borderRadius: borderRadius.full, - backgroundColor: colors.background.default, - }, - headerSubtitle: { - marginTop: spacing.xs, - }, - userItem: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - padding: spacing.md, - marginBottom: spacing.sm, - borderWidth: 1, - borderColor: colors.divider + '45', - ...shadows.sm, - }, - userInfo: { - flex: 1, - marginLeft: spacing.md, - marginRight: spacing.sm, - }, - nickname: { - fontWeight: '600', - marginBottom: 2, - }, - bio: { - marginTop: 2, - }, - followButton: { - minWidth: 82, - }, - // 网格布局样式 - gridContent: { - flexGrow: 1, - padding: spacing.lg, - }, - emptyGridContent: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - gridRow: { - justifyContent: 'flex-start', - gap: spacing.md, - marginBottom: spacing.md, - }, - userCard: { - flex: 1, - backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - padding: spacing.md, - alignItems: 'center', - borderWidth: 1, - borderColor: colors.divider + '45', - ...shadows.sm, - }, - userCardHeader: { - marginBottom: spacing.sm, - }, - userCardContent: { - alignItems: 'center', - width: '100%', - }, - userCardNickname: { - fontWeight: '600', - marginBottom: 2, - textAlign: 'center', - }, - userCardBio: { - marginTop: spacing.xs, - textAlign: 'center', - }, - userCardFooter: { - marginTop: spacing.md, - width: '100%', - }, - userCardFollowButton: { - width: '100%', - }, - footerLoading: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.md, - gap: spacing.xs, - }, - footerLoadingText: { - marginLeft: spacing.xs, - }, -}); +function createFollowListStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + // 列表布局样式 + listContent: { + flexGrow: 1, + paddingHorizontal: spacing.md, + paddingTop: spacing.md, + paddingBottom: spacing.lg, + }, + headerCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.xl, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + marginBottom: spacing.md, + borderWidth: 1, + borderColor: colors.divider + '4A', + ...shadows.sm, + }, + headerAccent: { + width: 28, + height: 3, + borderRadius: 999, + backgroundColor: colors.primary.main + 'A6', + marginBottom: spacing.sm, + }, + headerMainRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + headerTitle: { + fontWeight: '700', + }, + headerCountPill: { + paddingHorizontal: spacing.sm, + paddingVertical: 4, + borderRadius: borderRadius.full, + backgroundColor: colors.background.default, + }, + headerSubtitle: { + marginTop: spacing.xs, + }, + userItem: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + marginBottom: spacing.sm, + borderWidth: 1, + borderColor: colors.divider + '45', + ...shadows.sm, + }, + userInfo: { + flex: 1, + marginLeft: spacing.md, + marginRight: spacing.sm, + }, + nickname: { + fontWeight: '600', + marginBottom: 2, + }, + bio: { + marginTop: 2, + }, + followButton: { + minWidth: 82, + }, + // 网格布局样式 + gridContent: { + flexGrow: 1, + padding: spacing.lg, + }, + emptyGridContent: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + gridRow: { + justifyContent: 'flex-start', + gap: spacing.md, + marginBottom: spacing.md, + }, + userCard: { + flex: 1, + backgroundColor: colors.background.paper, + borderRadius: borderRadius.xl, + padding: spacing.md, + alignItems: 'center', + borderWidth: 1, + borderColor: colors.divider + '45', + ...shadows.sm, + }, + userCardHeader: { + marginBottom: spacing.sm, + }, + userCardContent: { + alignItems: 'center', + width: '100%', + }, + userCardNickname: { + fontWeight: '600', + marginBottom: 2, + textAlign: 'center', + }, + userCardBio: { + marginTop: spacing.xs, + textAlign: 'center', + }, + userCardFooter: { + marginTop: spacing.md, + width: '100%', + }, + userCardFollowButton: { + width: '100%', + }, + footerLoading: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + gap: spacing.xs, + }, + footerLoadingText: { + marginLeft: spacing.xs, + }, + }); +} export default FollowListScreen; diff --git a/src/screens/profile/NotificationSettingsScreen.tsx b/src/screens/profile/NotificationSettingsScreen.tsx index 6d6741d..5128679 100644 --- a/src/screens/profile/NotificationSettingsScreen.tsx +++ b/src/screens/profile/NotificationSettingsScreen.tsx @@ -4,7 +4,7 @@ * 在宽屏下居中显示 */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { View, StyleSheet, @@ -13,7 +13,7 @@ import { } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme'; import { Text, ResponsiveContainer } from '../../components/common'; import { loadNotificationPreferences, @@ -33,6 +33,8 @@ interface NotificationSettingItem { } export const NotificationSettingsScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]); const [vibrationEnabled, setVibrationEnabledState] = useState(true); const [pushEnabled, setPushEnabled] = useState(true); const [soundEnabled, setSoundEnabled] = useState(true); @@ -183,72 +185,74 @@ export const NotificationSettingsScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - scrollContent: { - paddingVertical: spacing.md, - }, - section: { - marginBottom: spacing.lg, - }, - sectionHeader: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.lg, - marginBottom: spacing.sm, - gap: spacing.xs, - }, - sectionTitle: { - fontWeight: '600', - }, - card: { - backgroundColor: colors.background.paper, - marginHorizontal: spacing.lg, - borderRadius: borderRadius.lg, - overflow: 'hidden', - }, - settingItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.md, - paddingHorizontal: spacing.md, - }, - iconContainer: { - width: 36, - height: 36, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - }, - settingContent: { - flex: 1, - }, - subtitle: { - marginTop: 2, - }, - divider: { - height: 1, - backgroundColor: colors.divider, - marginLeft: 36 + spacing.md + spacing.md, - }, - tipContainer: { - flexDirection: 'row', - alignItems: 'flex-start', - marginHorizontal: spacing.lg, - padding: spacing.md, - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - gap: spacing.sm, - }, - tipText: { - flex: 1, - lineHeight: 20, - }, -}); +function createNotificationSettingsStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollContent: { + paddingVertical: spacing.md, + }, + section: { + marginBottom: spacing.lg, + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.lg, + marginBottom: spacing.sm, + gap: spacing.xs, + }, + sectionTitle: { + fontWeight: '600', + }, + card: { + backgroundColor: colors.background.paper, + marginHorizontal: spacing.lg, + borderRadius: borderRadius.lg, + overflow: 'hidden', + }, + settingItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + paddingHorizontal: spacing.md, + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.md, + }, + settingContent: { + flex: 1, + }, + subtitle: { + marginTop: 2, + }, + divider: { + height: 1, + backgroundColor: colors.divider, + marginLeft: 36 + spacing.md + spacing.md, + }, + tipContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + marginHorizontal: spacing.lg, + padding: spacing.md, + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + gap: spacing.sm, + }, + tipText: { + flex: 1, + lineHeight: 20, + }, + }); +} export default NotificationSettingsScreen; diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index de13a6d..d8e3e92 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -4,7 +4,7 @@ * 在宽屏下居中显示,最大宽度限制 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, StyleSheet, @@ -16,7 +16,15 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import Constants from 'expo-constants'; -import { colors, spacing, fontSizes, borderRadius } from '../../theme'; +import { + useAppColors, + spacing, + fontSizes, + borderRadius, + useThemePreference, + useSetThemePreference, + type AppColors, +} from '../../theme'; import { useAuthStore } from '../../stores'; import { Text, ResponsiveContainer } from '../../components/common'; import { useResponsive } from '../../hooks'; @@ -34,11 +42,9 @@ interface SettingsItem { subtitle?: string; } -// 获取应用版本号 const APP_VERSION = Constants.expoConfig?.version || '1.0.0'; -// 设置分组配置 -const SETTINGS_GROUPS = [ +const SETTINGS_GROUPS_BASE = [ { title: '账号与安全', icon: 'shield-check-outline', @@ -66,19 +72,161 @@ const SETTINGS_GROUPS = [ }, ]; +function createSettingsStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollContent: { + paddingVertical: spacing.md, + }, + groupContainer: { + marginBottom: spacing.lg, + }, + groupHeader: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.lg, + marginBottom: spacing.sm, + gap: spacing.xs, + }, + groupTitle: { + fontWeight: '600', + fontSize: fontSizes.sm, + }, + card: { + backgroundColor: colors.background.paper, + marginHorizontal: spacing.lg, + borderRadius: borderRadius.lg, + overflow: 'hidden', + }, + settingItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: spacing.md, + paddingHorizontal: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + settingItemFirst: { + borderTopLeftRadius: borderRadius.lg, + borderTopRightRadius: borderRadius.lg, + }, + settingItemLast: { + borderBottomLeftRadius: borderRadius.lg, + borderBottomRightRadius: borderRadius.lg, + borderBottomWidth: 0, + }, + settingItemLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.md, + }, + dangerIconContainer: { + backgroundColor: colors.error.light + '20', + }, + settingContent: { + flex: 1, + }, + subtitle: { + marginTop: 2, + }, + logoutButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.background.paper, + marginHorizontal: spacing.lg, + marginTop: spacing.sm, + marginBottom: spacing.lg, + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + gap: spacing.sm, + }, + logoutText: { + fontWeight: '500', + }, + footer: { + alignItems: 'center', + marginTop: spacing.xl, + paddingBottom: spacing.xl, + }, + copyright: { + marginTop: spacing.xs, + }, + }); +} + export const SettingsScreen: React.FC = () => { const router = useRouter(); const { logout } = useAuthStore(); const { isWideScreen } = useResponsive(); const insets = useSafeAreaInsets(); const { isMobile } = useResponsive(); - - // 底部间距,避免被 TabBar 遮挡 + const colors = useAppColors(); + const styles = useMemo(() => createSettingsStyles(colors), [colors]); + const themePreference = useThemePreference(); + const setThemePreference = useSetThemePreference(); + const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md; - // 处理设置项点击 + const settingsGroups = useMemo(() => { + const themeSubtitle = + themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色'; + return [ + { + title: '显示与外观', + icon: 'palette-outline', + items: [ + { + key: 'theme', + title: '主题模式', + icon: 'theme-light-dark', + showArrow: true, + subtitle: themeSubtitle, + }, + ], + }, + ...SETTINGS_GROUPS_BASE, + ]; + }, [themePreference]); + const handleItemPress = (key: string) => { switch (key) { + case 'theme': + Alert.alert('主题模式', '选择应用界面明暗', [ + { text: '取消', style: 'cancel' }, + { + text: '跟随系统', + onPress: () => { + void setThemePreference('system'); + }, + }, + { + text: '浅色', + onPress: () => { + void setThemePreference('light'); + }, + }, + { + text: '深色', + onPress: () => { + void setThemePreference('dark'); + }, + }, + ]); + break; case 'edit_profile': router.push(hrefs.hrefProfileEdit()); break; @@ -103,7 +251,7 @@ export const SettingsScreen: React.FC = () => { onPress: async () => { await logout(); router.replace(hrefs.hrefAuthLogin()); - } + }, }, ] ); @@ -113,7 +261,6 @@ export const SettingsScreen: React.FC = () => { } }; - // 渲染单个设置项 const renderSettingItem = (item: SettingsItem, index: number, total: number) => ( { index === 0 && styles.settingItemFirst, index === total - 1 && styles.settingItemLast, ]} - onPress={() => item.onPress ? item.onPress() : handleItemPress(item.key)} + onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))} activeOpacity={0.7} > - + { /> - + {item.title} {item.subtitle && ( @@ -156,8 +297,7 @@ export const SettingsScreen: React.FC = () => { ); - // 渲染分组 - const renderGroup = (group: typeof SETTINGS_GROUPS[0], groupIndex: number) => ( + const renderGroup = (group: (typeof settingsGroups)[0]) => ( @@ -166,14 +306,11 @@ export const SettingsScreen: React.FC = () => { - {group.items.map((item, index) => - renderSettingItem(item, index, group.items.length) - )} + {group.items.map((item, index) => renderSettingItem(item, index, group.items.length))} ); - // 退出登录按钮 const renderLogoutButton = () => ( { ); - // 渲染内容 const renderContent = () => ( <> - {SETTINGS_GROUPS.map((group, index) => renderGroup(group, index))} + {settingsGroups.map((group) => renderGroup(group))} {renderLogoutButton()} - {/* 底部版权信息 */} 萝卜社区 v{APP_VERSION} @@ -222,98 +357,4 @@ export const SettingsScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - scrollContent: { - paddingVertical: spacing.md, - }, - groupContainer: { - marginBottom: spacing.lg, - }, - groupHeader: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: spacing.lg, - marginBottom: spacing.sm, - gap: spacing.xs, - }, - groupTitle: { - fontWeight: '600', - fontSize: fontSizes.sm, - }, - card: { - backgroundColor: colors.background.paper, - marginHorizontal: spacing.lg, - borderRadius: borderRadius.lg, - overflow: 'hidden', - }, - settingItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: spacing.md, - paddingHorizontal: spacing.md, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - settingItemFirst: { - borderTopLeftRadius: borderRadius.lg, - borderTopRightRadius: borderRadius.lg, - }, - settingItemLast: { - borderBottomLeftRadius: borderRadius.lg, - borderBottomRightRadius: borderRadius.lg, - borderBottomWidth: 0, - }, - settingItemLeft: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - iconContainer: { - width: 36, - height: 36, - borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - }, - dangerIconContainer: { - backgroundColor: colors.error.light + '20', - }, - settingContent: { - flex: 1, - }, - subtitle: { - marginTop: 2, - }, - logoutButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.background.paper, - marginHorizontal: spacing.lg, - marginTop: spacing.sm, - marginBottom: spacing.lg, - paddingVertical: spacing.md, - borderRadius: borderRadius.lg, - gap: spacing.sm, - }, - logoutText: { - fontWeight: '500', - }, - footer: { - alignItems: 'center', - marginTop: spacing.xl, - paddingBottom: spacing.xl, - }, - copyright: { - marginTop: spacing.xs, - }, -}); - export default SettingsScreen; diff --git a/src/screens/profile/UserProfileScreen.tsx b/src/screens/profile/UserProfileScreen.tsx index 29d4b71..f21aa89 100644 --- a/src/screens/profile/UserProfileScreen.tsx +++ b/src/screens/profile/UserProfileScreen.tsx @@ -12,12 +12,12 @@ import { ScrollView, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { colors } from '../../theme'; +import { useAppColors } from '../../theme'; import { Post } from '../../types'; import { PostCard, TabBar, UserProfileHeader } from '../../components/business'; import { Loading, EmptyState, ResponsiveContainer } from '../../components/common'; import { useResponsive } from '../../hooks'; -import { useUserProfile, ProfileMode, TABS, TAB_ICONS, sharedStyles } from './useUserProfile'; +import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile'; interface UserProfileScreenProps { mode: ProfileMode; @@ -25,6 +25,8 @@ interface UserProfileScreenProps { } export const UserProfileScreen: React.FC = ({ mode, userId }) => { + const colors = useAppColors(); + const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]); const { isDesktop, isTablet } = useResponsive(); const { diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts index a59ad38..5372dcd 100644 --- a/src/screens/profile/index.ts +++ b/src/screens/profile/index.ts @@ -16,5 +16,5 @@ export { BlockedUsersScreen } from './BlockedUsersScreen'; export { AccountSecurityScreen } from './AccountSecurityScreen'; // 导出 Hook 供需要自定义的场景使用 -export { useUserProfile, sharedStyles, TABS, TAB_ICONS } from './useUserProfile'; +export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile'; export type { ProfileMode, UseUserProfileOptions, UseUserProfileReturn } from './useUserProfile'; diff --git a/src/screens/profile/useUserProfile.ts b/src/screens/profile/useUserProfile.ts index 3f8e1ac..b078316 100644 --- a/src/screens/profile/useUserProfile.ts +++ b/src/screens/profile/useUserProfile.ts @@ -4,9 +4,10 @@ */ import { useState, useEffect, useCallback, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; import { useRouter } from 'expo-router'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { colors, spacing } from '../../theme'; +import { spacing, type AppColors } from '../../theme'; import { Post, User } from '../../types'; import { useUserStore } from '../../stores'; import { useCurrentUser } from '../../stores/authStore'; @@ -443,62 +444,61 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe }; }; -// 共享的样式 -import { StyleSheet } from 'react-native'; - -export const sharedStyles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - scrollContent: { - flexGrow: 1, - }, - desktopContainer: { - flex: 1, - flexDirection: 'row', - gap: spacing.lg, - padding: spacing.lg, - }, - desktopSidebar: { - width: 380, - flexShrink: 0, - }, - desktopContent: { - flex: 1, - minWidth: 0, - }, - desktopScrollContent: { - flexGrow: 1, - }, - tabBarContainer: { - marginTop: spacing.xs, - marginBottom: 2, - }, - contentContainer: { - flex: 1, - minHeight: 350, - paddingTop: spacing.xs, - }, - postsContainer: { - paddingHorizontal: spacing.md, - paddingTop: spacing.sm, - }, - postWrapper: { - marginBottom: spacing.md, - backgroundColor: colors.background.paper, - borderRadius: 16, - overflow: 'hidden', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.06, - shadowRadius: 8, - elevation: 2, - }, - lastPost: { - marginBottom: spacing['2xl'], - }, -}); +export function createSharedProfileStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollContent: { + flexGrow: 1, + }, + desktopContainer: { + flex: 1, + flexDirection: 'row', + gap: spacing.lg, + padding: spacing.lg, + }, + desktopSidebar: { + width: 380, + flexShrink: 0, + }, + desktopContent: { + flex: 1, + minWidth: 0, + }, + desktopScrollContent: { + flexGrow: 1, + }, + tabBarContainer: { + marginTop: spacing.xs, + marginBottom: 2, + }, + contentContainer: { + flex: 1, + minHeight: 350, + paddingTop: spacing.xs, + }, + postsContainer: { + paddingHorizontal: spacing.md, + paddingTop: spacing.sm, + }, + postWrapper: { + marginBottom: spacing.md, + backgroundColor: colors.background.paper, + borderRadius: 16, + overflow: 'hidden', + shadowColor: colors.chat.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, + }, + lastPost: { + marginBottom: spacing['2xl'], + }, + }); +} // Tab 配置 export const TABS = ['帖子', '收藏']; diff --git a/src/screens/schedule/CourseDetailScreen.tsx b/src/screens/schedule/CourseDetailScreen.tsx index 0afbdbe..27fcc09 100644 --- a/src/screens/schedule/CourseDetailScreen.tsx +++ b/src/screens/schedule/CourseDetailScreen.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Pressable } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { colors, spacing, borderRadius, fontSizes, shadows } from '../../theme'; +import { spacing, borderRadius, fontSizes, shadows, useAppColors, type AppColors } from '../../theme'; import { routePayloadCache } from '../../stores/routePayloadCache'; import { scheduleService } from '../../services/scheduleService'; @@ -32,15 +32,146 @@ const formatWeekRanges = (weeks: number[]) => { return ranges.join(', '); }; +function createCourseDetailStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + }, + mask: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.32)', + justifyContent: 'center', + paddingHorizontal: spacing.lg, + }, + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.xl, + paddingVertical: spacing.lg, + paddingHorizontal: spacing.lg, + maxHeight: '82%', + ...shadows.lg, + }, + contentScroll: { + maxHeight: '100%', + }, + contentScrollContainer: { + paddingBottom: spacing.xs, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.lg, + borderBottomWidth: 1, + borderBottomColor: `${colors.divider}AA`, + paddingBottom: spacing.md, + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + iconBadge: { + width: 30, + height: 30, + borderRadius: borderRadius.full, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: `${colors.primary.main}12`, + marginRight: spacing.sm, + }, + title: { + fontSize: fontSizes.xl, + fontWeight: '700', + color: colors.text.primary, + }, + closeButton: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center', + borderRadius: borderRadius.full, + backgroundColor: colors.background.default, + }, + row: { + marginBottom: spacing.lg, + flexDirection: 'row', + alignItems: 'flex-start', + }, + rowLast: { + marginBottom: 0, + }, + label: { + width: 76, + fontSize: fontSizes.md, + fontWeight: '600', + color: colors.text.secondary, + lineHeight: 22, + marginTop: 1, + }, + value: { + flex: 1, + fontSize: fontSizes.lg, + fontWeight: '600', + color: colors.text.primary, + lineHeight: 24, + }, + timeList: { + flex: 1, + gap: spacing.sm, + }, + timeItemCard: { + borderWidth: 1, + borderColor: `${colors.primary.main}20`, + borderRadius: borderRadius.md, + backgroundColor: `${colors.primary.main}08`, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.sm, + }, + timeItemMain: { + fontSize: fontSizes.md, + fontWeight: '600', + color: colors.text.primary, + lineHeight: 20, + }, + timeItemSub: { + marginTop: spacing.xs, + fontSize: fontSizes.sm, + color: colors.text.secondary, + lineHeight: 18, + }, + actionRow: { + marginTop: spacing.lg, + alignItems: 'flex-end', + }, + deleteButton: { + height: 38, + paddingHorizontal: spacing.md, + borderRadius: borderRadius.full, + backgroundColor: `${colors.error.main}10`, + borderWidth: 1, + borderColor: `${colors.error.main}25`, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + deleteButtonText: { + fontSize: fontSizes.sm, + fontWeight: '700', + color: colors.error.main, + }, + }); +} + const CourseDetailScreen: React.FC = () => { const router = useRouter(); + const colors = useAppColors(); + const styles = useMemo(() => createCourseDetailStyles(colors), [colors]); const { courseId } = useLocalSearchParams<{ courseId?: string }>(); const cached = courseId ? routePayloadCache.getCourseDetail(courseId) : undefined; const course = cached?.course; const relatedCourses = cached?.relatedCourses ?? []; - const timeLines = - relatedCourses.length > 0 ? relatedCourses : course ? [course] : []; + const timeLines = relatedCourses.length > 0 ? relatedCourses : course ? [course] : []; const handleDeleteCourse = () => { if (!course) return; @@ -98,7 +229,9 @@ const CourseDetailScreen: React.FC = () => { > 课程名称 - {course.name} + + {course.name} + @@ -110,9 +243,7 @@ const CourseDetailScreen: React.FC = () => { 第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第 {getMergedSectionIndex(item.startSection)}节 - - 任课教师:{item.teacher || '未填写'} - + 任课教师:{item.teacher || '未填写'} {item.location ? {item.location} : null} ))} @@ -132,132 +263,4 @@ const CourseDetailScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - mask: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.32)', - justifyContent: 'center', - paddingHorizontal: spacing.lg, - }, - card: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - paddingVertical: spacing.lg, - paddingHorizontal: spacing.lg, - maxHeight: '82%', - ...shadows.lg, - }, - contentScroll: { - maxHeight: '100%', - }, - contentScrollContainer: { - paddingBottom: spacing.xs, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.lg, - borderBottomWidth: 1, - borderBottomColor: `${colors.divider}AA`, - paddingBottom: spacing.md, - }, - headerLeft: { - flexDirection: 'row', - alignItems: 'center', - }, - iconBadge: { - width: 30, - height: 30, - borderRadius: borderRadius.full, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: `${colors.primary.main}12`, - marginRight: spacing.sm, - }, - title: { - fontSize: fontSizes.xl, - fontWeight: '700', - color: colors.text.primary, - }, - closeButton: { - width: 28, - height: 28, - alignItems: 'center', - justifyContent: 'center', - borderRadius: borderRadius.full, - backgroundColor: colors.background.default, - }, - row: { - marginBottom: spacing.lg, - flexDirection: 'row', - alignItems: 'flex-start', - }, - rowLast: { - marginBottom: 0, - }, - label: { - width: 76, - fontSize: fontSizes.md, - fontWeight: '600', - color: colors.text.secondary, - lineHeight: 22, - marginTop: 1, - }, - value: { - flex: 1, - fontSize: fontSizes.lg, - fontWeight: '600', - color: colors.text.primary, - lineHeight: 24, - }, - timeList: { - flex: 1, - gap: spacing.sm, - }, - timeItemCard: { - borderWidth: 1, - borderColor: `${colors.primary.main}20`, - borderRadius: borderRadius.md, - backgroundColor: `${colors.primary.main}08`, - paddingHorizontal: spacing.sm, - paddingVertical: spacing.sm, - }, - timeItemMain: { - fontSize: fontSizes.md, - fontWeight: '600', - color: colors.text.primary, - lineHeight: 20, - }, - timeItemSub: { - marginTop: spacing.xs, - fontSize: fontSizes.sm, - color: colors.text.secondary, - lineHeight: 18, - }, - actionRow: { - marginTop: spacing.lg, - alignItems: 'flex-end', - }, - deleteButton: { - height: 38, - paddingHorizontal: spacing.md, - borderRadius: borderRadius.full, - backgroundColor: `${colors.error.main}10`, - borderWidth: 1, - borderColor: `${colors.error.main}25`, - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs, - }, - deleteButtonText: { - fontSize: fontSizes.sm, - fontWeight: '700', - color: colors.error.main, - }, -}); - export default CourseDetailScreen; diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index ea71683..11f7e93 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -23,7 +23,14 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; -import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme'; +import { + fontSizes, + spacing, + borderRadius, + shadows, + useAppColors, + type AppColors, +} from '../../theme'; import { useResponsive } from '../../hooks/useResponsive'; import { Course, @@ -148,6 +155,8 @@ const getWeekDates = (weekOffset: number = 0) => { const INITIAL_WEEK = getCurrentWeekNumber(); export const ScheduleScreen: React.FC = () => { + const colors = useAppColors(); + const styles = useMemo(() => createScheduleStyles(colors), [colors]); const router = useRouter(); // 使用响应式 hook 检测屏幕尺寸 const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive(); @@ -1063,7 +1072,8 @@ export const ScheduleScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ +function createScheduleStyles(colors: AppColors) { + return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.paper, @@ -1483,6 +1493,7 @@ const styles = StyleSheet.create({ color: colors.text.secondary, fontWeight: '500', }, -}); + }); +} export default ScheduleScreen; diff --git a/src/services/alertOverride.ts b/src/services/alertOverride.ts index 955a0e6..643bafc 100644 --- a/src/services/alertOverride.ts +++ b/src/services/alertOverride.ts @@ -39,7 +39,9 @@ export const installAlertOverride = () => { return; } - if (actionButtons.length <= 3) { + // 自定义对话框支持多按钮(含「主题模式」等 4 项);Web 上原生 Alert 常不可用 + const MAX_CUSTOM_DIALOG_ACTIONS = 8; + if (actionButtons.length <= MAX_CUSTOM_DIALOG_ACTIONS) { showDialog({ title: nextTitle, message: nextMessage, @@ -49,7 +51,6 @@ export const installAlertOverride = () => { return; } - // 极少数复杂按钮场景,保底回退原生 Alert nativeAlert(nextTitle, nextMessage, buttons, options); }; }; diff --git a/src/stores/index.ts b/src/stores/index.ts index 2d82f9b..8779159 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -44,6 +44,17 @@ export { userManager } from './userManager'; export { useHomeTabBarVisibilityStore, } from './homeTabBarVisibilityStore'; +export { + useAppColors, + useThemePreference, + useResolvedColorScheme, + useSetThemePreference, + usePaperThemeFromStore, + ThemeBootstrap, + useThemeStore, + type ThemePreference, + type ResolvedScheme, +} from './themeStore'; export { useConversations as useMessageManagerConversations, useMessages, diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts new file mode 100644 index 0000000..f0126f3 --- /dev/null +++ b/src/stores/themeStore.ts @@ -0,0 +1,133 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useColorScheme } from 'react-native'; +import { create } from 'zustand'; +import { useEffect } from 'react'; +import { lightColors, darkColors, type AppColors } from '../theme/palettes'; +import { buildPaperTheme } from '../theme/paperTheme'; +import type { MD3Theme } from 'react-native-paper'; + +const STORAGE_KEY = 'app_theme_preference'; + +export type ThemePreference = 'light' | 'dark' | 'system'; + +export type ResolvedScheme = 'light' | 'dark'; + +function computeResolved(pref: ThemePreference, system: ResolvedScheme): ResolvedScheme { + return pref === 'system' ? system : pref; +} + +function buildState( + preference: ThemePreference, + systemScheme: ResolvedScheme +): { + resolvedScheme: ResolvedScheme; + colors: AppColors; + paperTheme: MD3Theme; +} { + const resolvedScheme = computeResolved(preference, systemScheme); + const isDark = resolvedScheme === 'dark'; + const colors = isDark ? darkColors : lightColors; + return { + resolvedScheme, + colors, + paperTheme: buildPaperTheme(colors, isDark), + }; +} + +type ThemeState = { + preference: ThemePreference; + systemScheme: ResolvedScheme; + hydrated: boolean; + resolvedScheme: ResolvedScheme; + colors: AppColors; + paperTheme: MD3Theme; + setPreference: (p: ThemePreference) => Promise; + setSystemScheme: (s: ResolvedScheme) => void; + hydrate: () => Promise; +}; + +const initialSystem: ResolvedScheme = 'light'; + +export const useThemeStore = create((set, get) => ({ + preference: 'system', + systemScheme: initialSystem, + hydrated: false, + ...buildState('system', initialSystem), + + setSystemScheme: (systemScheme) => { + const { preference, systemScheme: cur } = get(); + if (cur === systemScheme) return; + set({ + systemScheme, + ...buildState(preference, systemScheme), + }); + }, + + setPreference: async (preference) => { + try { + await AsyncStorage.setItem(STORAGE_KEY, preference); + } catch { + /* ignore */ + } + const { systemScheme } = get(); + set({ + preference, + ...buildState(preference, systemScheme), + }); + }, + + hydrate: async () => { + let preference: ThemePreference = 'system'; + try { + const raw = await AsyncStorage.getItem(STORAGE_KEY); + if (raw === 'light' || raw === 'dark' || raw === 'system') { + preference = raw; + } + } catch { + /* ignore */ + } + const { systemScheme } = get(); + set({ + preference, + hydrated: true, + ...buildState(preference, systemScheme), + }); + }, +})); + +/** 在根布局中同步系统配色并恢复持久化偏好 */ +export function ThemeBootstrap() { + const system = useColorScheme(); + const setSystemScheme = useThemeStore((s) => s.setSystemScheme); + const hydrate = useThemeStore((s) => s.hydrate); + + useEffect(() => { + void hydrate(); + }, [hydrate]); + + useEffect(() => { + setSystemScheme(system === 'dark' ? 'dark' : 'light'); + }, [system, setSystemScheme]); + + return null; +} + +export function useAppColors() { + return useThemeStore((s) => s.colors); +} + +export function useThemePreference() { + return useThemeStore((s) => s.preference); +} + +export function useResolvedColorScheme() { + return useThemeStore((s) => s.resolvedScheme); +} + +export function useSetThemePreference() { + return useThemeStore((s) => s.setPreference); +} + +export function usePaperThemeFromStore() { + return useThemeStore((s) => s.paperTheme); +} diff --git a/src/theme/index.ts b/src/theme/index.ts index 4a511c4..758eda3 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -1,183 +1,28 @@ /** - * 胡萝卜BBS主题系统 - * 基于胡萝卜橙主色调的设计系统 + * 胡萝卜BBS 主题:明暗色板、Paper 主题、设计 token + * 新代码请优先使用 useAppColors();静态 colors 为浅色快照,与深色模式不同步。 */ -// ==================== 颜色系统 ==================== -export const colors = { - primary: { - main: '#FF6B35', // 橙色(主色) - light: '#FFB733', - dark: '#CC8400', - contrast: '#FFFFFF', - }, - secondary: { - main: '#4CAF50', - light: '#80E27E', - dark: '#087F23', - }, - neutral: { - main: '#6B7280', // 中性灰(辅助色) - light: '#9CA3AF', - dark: '#374151', - bg: '#F3F4F6', // 浅灰背景 - bgDark: '#1F2937', // 深灰背景 - }, - accent: { - main: '#E57373', // 低饱和红(点缀色) - light: '#FFCDD2', - dark: '#C62828', - }, - background: { - default: '#F5F5F5', - paper: '#FFFFFF', - disabled: '#E0E0E0', - }, - text: { - primary: '#212121', - secondary: '#757575', - disabled: '#9E9E9E', - hint: '#BDBDBD', - inverse: '#FFFFFF', - }, - divider: '#E0E0E0', - error: { - main: '#F44336', - light: '#FF7961', - dark: '#D32F2F', - }, - success: { - main: '#4CAF50', - light: '#80E27E', - dark: '#087F23', - }, - warning: { - main: '#FF9800', - light: '#FFB74D', - dark: '#F57C00', - }, - info: { - main: '#2196F3', - light: '#64B5F6', - dark: '#1976D2', - }, -}; +export { lightColors, darkColors, type AppColors } from './palettes'; +export { buildPaperTheme } from './paperTheme'; +export * from './tokens'; -// ==================== 字体系统 ==================== -export const fontSizes = { - xs: 10, - sm: 12, - md: 14, - lg: 16, - xl: 18, - '2xl': 20, - '3xl': 24, - '4xl': 30, -}; +export { + useAppColors, + useThemePreference, + useResolvedColorScheme, + useSetThemePreference, + usePaperThemeFromStore, + ThemeBootstrap, + useThemeStore, + type ThemePreference, + type ResolvedScheme, +} from '../stores/themeStore'; -// ==================== 间距系统(4px基准)==================== -export const spacing = { - xs: 4, - sm: 8, - md: 12, - lg: 16, - xl: 20, - '2xl': 24, - '3xl': 32, - '4xl': 40, - '5xl': 48, - '6xl': 56, -}; +import { lightColors } from './palettes'; +import { buildPaperTheme } from './paperTheme'; -// ==================== 圆角系统 ==================== -export const borderRadius = { - sm: 4, - md: 8, - lg: 12, - xl: 16, - '2xl': 24, - full: 9999, -}; +/** 浅色静态快照;随主题切换请使用 useAppColors() */ +export const colors = lightColors; -// ==================== 阴影系统 ==================== -export const shadows = { - sm: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.18, - shadowRadius: 1.0, - elevation: 1 - }, - md: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.22, - shadowRadius: 2.22, - elevation: 3 - }, - lg: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 4.65, - elevation: 6 - }, -}; - -// ==================== 导出主题对象 ==================== -export const theme = { - colors, - fontSizes, - spacing, - borderRadius, - shadows -}; - -export type Theme = typeof theme; - -// ==================== 主题配置(用于React Native Paper)==================== -export const paperTheme = { - colors: { - primary: colors.primary.main, - primaryContainer: colors.primary.light, - secondary: colors.secondary.main, - secondaryContainer: colors.secondary.light, - tertiary: colors.info.main, - tertiaryContainer: colors.info.light, - surface: colors.background.paper, - surfaceVariant: colors.background.default, - surfaceDisabled: colors.background.disabled, - background: colors.background.default, - error: colors.error.main, - errorContainer: colors.error.light, - onPrimary: colors.primary.contrast, - onPrimaryContainer: colors.primary.dark, - onSecondary: colors.text.inverse, - onSecondaryContainer: colors.secondary.dark, - onTertiary: colors.text.inverse, - onTertiaryContainer: colors.info.dark, - onSurface: colors.text.primary, - onSurfaceVariant: colors.text.secondary, - onSurfaceDisabled: colors.text.disabled, - onError: colors.text.inverse, - onErrorContainer: colors.error.dark, - onBackground: colors.text.primary, - outline: colors.divider, - outlineVariant: colors.divider, - inverseSurface: colors.text.primary, - inverseOnSurface: colors.text.inverse, - inversePrimary: colors.primary.light, - shadow: '#000000', - scrim: '#000000', - backdrop: 'rgba(0, 0, 0, 0.5)', - elevation: { - level0: 'transparent', - level1: colors.background.paper, - level2: colors.background.paper, - level3: colors.background.paper, - level4: colors.background.paper, - level5: colors.background.paper, - }, - }, - roundness: borderRadius.md, -}; +export const paperTheme = buildPaperTheme(lightColors, false); diff --git a/src/theme/palettes.ts b/src/theme/palettes.ts new file mode 100644 index 0000000..e845f0b --- /dev/null +++ b/src/theme/palettes.ts @@ -0,0 +1,185 @@ +/** + * 浅色 / 深色色板与聊天场景语义色 + */ + +export const lightColors = { + primary: { + main: '#FF6B35', + light: '#FFB733', + dark: '#CC8400', + contrast: '#FFFFFF', + }, + secondary: { + main: '#4CAF50', + light: '#80E27E', + dark: '#087F23', + }, + neutral: { + main: '#6B7280', + light: '#9CA3AF', + dark: '#374151', + bg: '#F3F4F6', + bgDark: '#1F2937', + }, + accent: { + main: '#E57373', + light: '#FFCDD2', + dark: '#C62828', + }, + background: { + default: '#F5F5F5', + paper: '#FFFFFF', + disabled: '#E0E0E0', + }, + text: { + primary: '#212121', + secondary: '#757575', + disabled: '#9E9E9E', + hint: '#BDBDBD', + inverse: '#FFFFFF', + }, + divider: '#E0E0E0', + error: { + main: '#F44336', + light: '#FF7961', + dark: '#D32F2F', + }, + success: { + main: '#4CAF50', + light: '#80E27E', + dark: '#087F23', + }, + warning: { + main: '#FF9800', + light: '#FFB74D', + dark: '#F57C00', + }, + info: { + main: '#2196F3', + light: '#64B5F6', + dark: '#1976D2', + }, + chat: { + screen: '#F0F2F5', + card: '#FFFFFF', + surfaceRaised: '#F7F8FA', + surfaceMuted: '#F5F7FA', + surfaceInput: '#F5F5F5', + border: '#E8E8E8', + borderLight: '#ECEFF3', + borderHairline: '#F0F0F0', + textPrimary: '#1A1A1A', + textSecondary: '#8E8E93', + textTertiary: '#666666', + textPlaceholder: '#AAAAAA', + textMuted: '#999999', + link: '#4A88C7', + success: '#34C759', + danger: '#FF3B30', + bubbleOutgoing: '#E8E8E8', + bubbleIncoming: '#FFFFFF', + replyTint: '#DFF2FF', + replyTintActive: '#CFEAFF', + replyBorder: '#7FB6E6', + menuHighlight: '#EEF5FC', + iconMuted: '#BBBBBB', + iconSoft: '#C8C8C8', + sheetGrip: '#E0E0E0', + warningBg: '#FFF5F5', + tipBg: '#FFF9E6', + overlayQuote: '#F0F7FF', + shadow: '#000000', + }, +} as const; + +export type AppColors = typeof lightColors; + +export const darkColors = { + primary: { + main: '#FF8A50', + light: '#FFB733', + dark: '#E85D2A', + contrast: '#FFFFFF', + }, + secondary: { + main: '#66BB6A', + light: '#81C784', + dark: '#388E3C', + }, + neutral: { + main: '#9CA3AF', + light: '#D1D5DB', + dark: '#6B7280', + bg: '#374151', + bgDark: '#111827', + }, + accent: { + main: '#EF9A9A', + light: '#FFCDD2', + dark: '#E57373', + }, + background: { + default: '#121212', + paper: '#1E1E1E', + disabled: '#2C2C2C', + }, + text: { + primary: '#ECECEC', + secondary: '#A8A8A8', + disabled: '#6E6E6E', + hint: '#888888', + inverse: '#121212', + }, + divider: '#333333', + error: { + main: '#EF5350', + light: '#FF867C', + dark: '#C62828', + }, + success: { + main: '#66BB6A', + light: '#81C784', + dark: '#388E3C', + }, + warning: { + main: '#FFA726', + light: '#FFCC80', + dark: '#F57C00', + }, + info: { + main: '#42A5F5', + light: '#90CAF9', + dark: '#1976D2', + }, + chat: { + screen: '#0A0A0A', + card: '#1C1C1E', + surfaceRaised: '#2C2C2E', + surfaceMuted: '#252528', + surfaceInput: '#2A2A2C', + border: '#38383A', + borderLight: '#3A3A3C', + borderHairline: '#2C2C2E', + textPrimary: '#F2F2F7', + textSecondary: '#98989D', + textTertiary: '#AEAEB2', + textPlaceholder: '#636366', + textMuted: '#8E8E93', + link: '#5AC8FA', + success: '#32D74B', + danger: '#FF453A', + bubbleOutgoing: '#3A3A3C', + bubbleIncoming: '#2C2C2E', + replyTint: '#1A3A52', + replyTintActive: '#224060', + replyBorder: '#4A88C7', + menuHighlight: '#1C2A3A', + iconMuted: '#8E8E93', + iconSoft: '#636366', + sheetGrip: '#48484A', + warningBg: '#3A2222', + tipBg: '#3A3520', + overlayQuote: '#1A2838', + shadow: '#000000', + }, +} as unknown as AppColors; diff --git a/src/theme/paperTheme.ts b/src/theme/paperTheme.ts new file mode 100644 index 0000000..51eece9 --- /dev/null +++ b/src/theme/paperTheme.ts @@ -0,0 +1,54 @@ +import { MD3DarkTheme, MD3LightTheme, type MD3Theme } from 'react-native-paper'; +import { borderRadius } from './tokens'; +import type { AppColors } from './palettes'; + +export function buildPaperTheme(colors: AppColors, isDark: boolean): MD3Theme { + const base = isDark ? MD3DarkTheme : MD3LightTheme; + return { + ...base, + colors: { + ...base.colors, + primary: colors.primary.main, + primaryContainer: colors.primary.light, + secondary: colors.secondary.main, + secondaryContainer: colors.secondary.light, + tertiary: colors.info.main, + tertiaryContainer: colors.info.light, + surface: colors.background.paper, + surfaceVariant: colors.background.default, + surfaceDisabled: colors.background.disabled, + background: colors.background.default, + error: colors.error.main, + errorContainer: colors.error.light, + onPrimary: colors.primary.contrast, + onPrimaryContainer: colors.primary.dark, + onSecondary: colors.text.inverse, + onSecondaryContainer: colors.secondary.dark, + onTertiary: colors.text.inverse, + onTertiaryContainer: colors.info.dark, + onSurface: colors.text.primary, + onSurfaceVariant: colors.text.secondary, + onSurfaceDisabled: colors.text.disabled, + onError: colors.text.inverse, + onErrorContainer: colors.error.dark, + onBackground: colors.text.primary, + outline: colors.divider, + outlineVariant: colors.divider, + inverseSurface: colors.text.primary, + inverseOnSurface: colors.text.inverse, + inversePrimary: colors.primary.light, + shadow: '#000000', + scrim: '#000000', + backdrop: 'rgba(0, 0, 0, 0.5)', + elevation: { + level0: 'transparent', + level1: colors.background.paper, + level2: colors.background.paper, + level3: colors.background.paper, + level4: colors.background.paper, + level5: colors.background.paper, + }, + }, + roundness: borderRadius.md, + }; +} diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts new file mode 100644 index 0000000..2cbe911 --- /dev/null +++ b/src/theme/tokens.ts @@ -0,0 +1,60 @@ +/** + * 与明暗无关的设计 token + */ + +export const fontSizes = { + xs: 10, + sm: 12, + md: 14, + lg: 16, + xl: 18, + '2xl': 20, + '3xl': 24, + '4xl': 30, +}; + +export const spacing = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 20, + '2xl': 24, + '3xl': 32, + '4xl': 40, + '5xl': 48, + '6xl': 56, +}; + +export const borderRadius = { + sm: 4, + md: 8, + lg: 12, + xl: 16, + '2xl': 24, + full: 9999, +}; + +export const shadows = { + sm: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.18, + shadowRadius: 1.0, + elevation: 1, + }, + md: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.22, + shadowRadius: 2.22, + elevation: 3, + }, + lg: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 4.65, + elevation: 6, + }, +};