feat(Theme): enhance theming and UI consistency across components
- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability. - Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage. - Introduced SystemChrome component to manage system UI background color based on theme. - Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure. - Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system. - Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
This commit is contained in:
2
app.json
2
app.json
@@ -5,7 +5,7 @@
|
|||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "automatic",
|
||||||
"scheme": "carrotbbs",
|
"scheme": "carrotbbs",
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash-icon.png",
|
"image": "./assets/splash-icon.png",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Tabs, usePathname } from 'expo-router';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
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 { BREAKPOINTS } from '../../../src/hooks/useResponsive';
|
||||||
import { useHomeTabBarVisibilityStore, useTotalUnreadCount } from '../../../src/stores';
|
import { useHomeTabBarVisibilityStore, useTotalUnreadCount } from '../../../src/stores';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ const TAB_BAR_FLOATING_MARGIN = 12;
|
|||||||
const TAB_BAR_MARGIN = 20;
|
const TAB_BAR_MARGIN = 20;
|
||||||
|
|
||||||
export default function TabsLayout() {
|
export default function TabsLayout() {
|
||||||
|
const colors = useAppColors();
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -50,7 +51,7 @@ export default function TabsLayout() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return visibleStyle;
|
return visibleStyle;
|
||||||
}, [hideTabBar, insets.bottom, isHomeStackRoute, scrollHideTabBar]);
|
}, [hideTabBar, insets.bottom, isHomeStackRoute, scrollHideTabBar, colors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
|
||||||
import { AppBackButton } from '../../../../src/components/common';
|
import { AppBackButton } from '../../../../src/components/common';
|
||||||
import { colors } from '../../../../src/theme';
|
import { useAppColors } from '../../../../src/theme';
|
||||||
|
|
||||||
const headerOptions = {
|
|
||||||
headerStyle: { backgroundColor: colors.background.paper },
|
|
||||||
headerTintColor: colors.text.primary,
|
|
||||||
headerTitleStyle: { fontWeight: '600' as const },
|
|
||||||
headerShadowVisible: false,
|
|
||||||
headerBackTitle: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProfileStackLayout() {
|
export default function ProfileStackLayout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useAppColors();
|
||||||
|
|
||||||
|
const headerOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
headerStyle: { backgroundColor: colors.background.paper },
|
||||||
|
headerTintColor: colors.text.primary,
|
||||||
|
headerTitleStyle: { fontWeight: '600' as const },
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerBackTitle: '',
|
||||||
|
}),
|
||||||
|
[colors]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
131
app/_layout.tsx
131
app/_layout.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { AppState, AppStateStatus, Platform, View, ActivityIndicator, StyleSheet } from 'react-native';
|
import { AppState, AppStateStatus, Platform, View, ActivityIndicator } from 'react-native';
|
||||||
import { Stack, useRouter } from 'expo-router';
|
import { Stack, useRouter } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
@@ -7,15 +7,20 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|||||||
import { PaperProvider } from 'react-native-paper';
|
import { PaperProvider } from 'react-native-paper';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import * as SystemUI from 'expo-system-ui';
|
||||||
|
|
||||||
import { registerNotificationPresentationHandler } from '../src/services/notificationPreferences';
|
import { registerNotificationPresentationHandler } from '../src/services/notificationPreferences';
|
||||||
import { paperTheme } from '../src/theme';
|
import {
|
||||||
|
ThemeBootstrap,
|
||||||
|
useAppColors,
|
||||||
|
usePaperThemeFromStore,
|
||||||
|
useResolvedColorScheme,
|
||||||
|
} from '../src/theme';
|
||||||
import { AppBackButton } from '../src/components/common';
|
import { AppBackButton } from '../src/components/common';
|
||||||
import AppPromptBar from '../src/components/common/AppPromptBar';
|
import AppPromptBar from '../src/components/common/AppPromptBar';
|
||||||
import AppDialogHost from '../src/components/common/AppDialogHost';
|
import AppDialogHost from '../src/components/common/AppDialogHost';
|
||||||
import { installAlertOverride } from '../src/services/alertOverride';
|
import { installAlertOverride } from '../src/services/alertOverride';
|
||||||
import { useAuthStore } from '../src/stores';
|
import { useAuthStore } from '../src/stores';
|
||||||
import { colors } from '../src/theme';
|
|
||||||
|
|
||||||
registerNotificationPresentationHandler();
|
registerNotificationPresentationHandler();
|
||||||
|
|
||||||
@@ -47,9 +52,24 @@ if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SystemChrome() {
|
||||||
|
const colors = useAppColors();
|
||||||
|
useEffect(() => {
|
||||||
|
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 }) {
|
function SessionGate({ children }: { children: React.ReactNode }) {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
||||||
|
const colors = useAppColors();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCurrentUser().finally(() => setReady(true));
|
fetchCurrentUser().finally(() => setReady(true));
|
||||||
@@ -57,7 +77,14 @@ function SessionGate({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return (
|
return (
|
||||||
<View style={gateStyles.loading}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -110,59 +137,69 @@ function NotificationBootstrap() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
function ThemedStack() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
|
||||||
|
<SystemChrome />
|
||||||
|
<SessionGate>
|
||||||
|
<NotificationBootstrap />
|
||||||
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
|
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="post/[postId]"
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: '',
|
||||||
|
// 与 PostDetailScreen 的 SafeAreaView(background.default)同色,避免出现 header/内容之间的色差线
|
||||||
|
headerStyle: { backgroundColor: colors.background.default },
|
||||||
|
headerTintColor: colors.text.primary,
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerBackVisible: false,
|
||||||
|
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="user/[userId]"
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: '用户主页',
|
||||||
|
headerStyle: { backgroundColor: colors.background.paper },
|
||||||
|
headerTintColor: colors.text.primary,
|
||||||
|
headerBackVisible: false,
|
||||||
|
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SessionGate>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemedProviders({ children }: { children: React.ReactNode }) {
|
||||||
|
const theme = usePaperThemeFromStore();
|
||||||
|
return <PaperProvider theme={theme}>{children}</PaperProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<PaperProvider theme={paperTheme}>
|
<ThemedProviders>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StatusBar style="light" />
|
<ThemeBootstrap />
|
||||||
<SessionGate>
|
<ThemedStack />
|
||||||
<NotificationBootstrap />
|
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen
|
|
||||||
name="post/[postId]"
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: '',
|
|
||||||
headerStyle: { backgroundColor: colors.background.paper },
|
|
||||||
headerTintColor: colors.text.primary,
|
|
||||||
headerBackVisible: false,
|
|
||||||
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="user/[userId]"
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: '用户主页',
|
|
||||||
headerStyle: { backgroundColor: colors.background.paper },
|
|
||||||
headerTintColor: colors.text.primary,
|
|
||||||
headerBackVisible: false,
|
|
||||||
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</SessionGate>
|
|
||||||
<AppPromptBar />
|
<AppPromptBar />
|
||||||
<AppDialogHost />
|
<AppDialogHost />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</PaperProvider>
|
</ThemedProviders>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gateStyles = StyleSheet.create({
|
|
||||||
loading: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.background.default,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -11,7 +11,7 @@ import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context'
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { usePathname, useRouter } from 'expo-router';
|
import { usePathname, useRouter } from 'expo-router';
|
||||||
|
|
||||||
import { colors, shadows } from '../theme';
|
import { useAppColors, shadows, type AppColors } from '../theme';
|
||||||
import { useTotalUnreadCount } from '../stores';
|
import { useTotalUnreadCount } from '../stores';
|
||||||
import { AppRouteStack } from './AppRouteStack';
|
import { AppRouteStack } from './AppRouteStack';
|
||||||
|
|
||||||
@@ -36,7 +36,114 @@ function pathToTab(pathname: string): TabName {
|
|||||||
return 'HomeTab';
|
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() {
|
export function AppDesktopShell() {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createDesktopShellStyles(colors), [colors]);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -134,106 +241,3 @@ export function AppDesktopShell() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
* 支持嵌套回复显示、楼层号、身份标识、删除评论、图片显示
|
* 支持嵌套回复显示、楼层号、身份标识、删除评论、图片显示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
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 { Comment, CommentImage } from '../../types';
|
||||||
import Text from '../common/Text';
|
import Text from '../common/Text';
|
||||||
import Avatar from '../common/Avatar';
|
import Avatar from '../common/Avatar';
|
||||||
@@ -31,6 +31,174 @@ interface CommentItemProps {
|
|||||||
currentUserId?: string; // 当前用户ID,用于判断子评论作者
|
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<CommentItemProps> = ({
|
const CommentItem: React.FC<CommentItemProps> = ({
|
||||||
comment,
|
comment,
|
||||||
onUserPress,
|
onUserPress,
|
||||||
@@ -47,6 +215,8 @@ const CommentItem: React.FC<CommentItemProps> = ({
|
|||||||
onImagePress,
|
onImagePress,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createCommentItemStyles(colors), [colors]);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (dateString: string): string => {
|
const formatTime = (dateString: string): string => {
|
||||||
@@ -471,170 +641,4 @@ const CommentItem: React.FC<CommentItemProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default CommentItem;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { PostCardProps, PostCardAction } from './types';
|
|||||||
import { ImageGridItem, SmartImage } from '../../common';
|
import { ImageGridItem, SmartImage } from '../../common';
|
||||||
import Text from '../../common/Text';
|
import Text from '../../common/Text';
|
||||||
import Avatar from '../../common/Avatar';
|
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 { getPreviewImageUrl } from '../../../utils/imageHelper';
|
||||||
import PostImages from './components/PostImages';
|
import PostImages from './components/PostImages';
|
||||||
import { useResponsive } from '../../../hooks/useResponsive';
|
import { useResponsive } from '../../../hooks/useResponsive';
|
||||||
@@ -113,6 +113,270 @@ function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolea
|
|||||||
return true;
|
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 主组件
|
* PostCard 主组件
|
||||||
* 仅支持新 API
|
* 仅支持新 API
|
||||||
@@ -129,6 +393,8 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
|||||||
style,
|
style,
|
||||||
} = normalizedProps;
|
} = normalizedProps;
|
||||||
|
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createPostCardStyles(colors), [colors]);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
@@ -444,266 +710,4 @@ PostCardInner.displayName = 'PostCard';
|
|||||||
|
|
||||||
const PostCard = memo(PostCardInner, arePostCardPropsEqual);
|
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;
|
export default PostCard;
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { View, StyleSheet } from 'react-native';
|
||||||
import { colors, spacing, borderRadius } from '../../../../theme';
|
import { spacing, borderRadius } from '../../../../theme';
|
||||||
import { useResponsive } from '../../../../hooks/useResponsive';
|
import { useResponsive } from '../../../../hooks/useResponsive';
|
||||||
import { ImageGrid, ImageGridItem } from '../../../common';
|
import { ImageGrid, ImageGridItem } from '../../../common';
|
||||||
import { PostImagesProps } from '../types';
|
import { PostImagesProps } from '../types';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -12,7 +12,7 @@ import { CameraView, useCameraPermissions } from 'expo-camera';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
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');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -21,14 +21,130 @@ interface QRCodeScannerProps {
|
|||||||
onClose: () => void;
|
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<QRCodeScannerProps> = ({ visible, onClose }) => {
|
export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const themeColors = useAppColors();
|
||||||
|
const styles = useMemo(() => createQrScannerStyles(themeColors), [themeColors]);
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
const [scanned, setScanned] = useState(false);
|
const [scanned, setScanned] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// 重置扫描状态,确保每次打开都能重新扫描
|
|
||||||
setScanned(false);
|
setScanned(false);
|
||||||
if (!permission?.granted) {
|
if (!permission?.granted) {
|
||||||
requestPermission();
|
requestPermission();
|
||||||
@@ -39,16 +155,11 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
|
|||||||
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
|
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
|
||||||
if (scanned) return;
|
if (scanned) return;
|
||||||
setScanned(true);
|
setScanned(true);
|
||||||
|
|
||||||
// 先关闭扫描界面,类似微信的做法
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// 解析二维码内容
|
|
||||||
// 格式: carrotbbs://qrcode/login?session_id=xxx
|
|
||||||
if (data.startsWith('carrotbbs://qrcode/login')) {
|
if (data.startsWith('carrotbbs://qrcode/login')) {
|
||||||
const sessionId = extractSessionId(data);
|
const sessionId = extractSessionId(data);
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
// 跳转到确认页面
|
|
||||||
router.push(hrefs.hrefQrLoginConfirm(sessionId));
|
router.push(hrefs.hrefQrLoginConfirm(sessionId));
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('无效的二维码', '无法识别该二维码');
|
Alert.alert('无效的二维码', '无法识别该二维码');
|
||||||
@@ -79,7 +190,7 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
|
|||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.permissionContainer}>
|
<View style={styles.permissionContainer}>
|
||||||
<MaterialCommunityIcons name="camera-off" size={64} color="#999" />
|
<MaterialCommunityIcons name="camera-off" size={64} color="rgba(255,255,255,0.5)" />
|
||||||
<Text style={styles.permissionText}>需要相机权限才能扫码</Text>
|
<Text style={styles.permissionText}>需要相机权限才能扫码</Text>
|
||||||
<TouchableOpacity style={styles.permissionButton} onPress={requestPermission}>
|
<TouchableOpacity style={styles.permissionButton} onPress={requestPermission}>
|
||||||
<Text style={styles.permissionButtonText}>授予权限</Text>
|
<Text style={styles.permissionButtonText}>授予权限</Text>
|
||||||
@@ -132,117 +243,4 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export default QRCodeScanner;
|
||||||
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;
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
* 用于搜索内容
|
* 用于搜索内容
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -18,6 +18,63 @@ interface SearchBarProps {
|
|||||||
autoFocus?: boolean;
|
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<SearchBarProps> = ({
|
const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
value,
|
value,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
@@ -27,6 +84,8 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createSearchBarStyles(colors), [colors]);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
@@ -59,7 +118,6 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
// 确保光标可见
|
|
||||||
cursorColor={colors.primary.main}
|
cursorColor={colors.primary.main}
|
||||||
selectionColor={`${colors.primary.main}40`}
|
selectionColor={`${colors.primary.main}40`}
|
||||||
/>
|
/>
|
||||||
@@ -69,73 +127,11 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
style={styles.clearButton}
|
style={styles.clearButton}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons name="close" size={14} color={colors.text.secondary} />
|
||||||
name="close"
|
|
||||||
size={14}
|
|
||||||
color={colors.text.secondary}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default SearchBar;
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
* 采用卡片式设计,符合胡萝卜BBS整体风格
|
* 采用卡片式设计,符合胡萝卜BBS整体风格
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { View, TouchableOpacity, StyleSheet, Animated } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
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 { SystemMessageResponse, SystemMessageType } from '../../types/dto';
|
||||||
import Text from '../common/Text';
|
import Text from '../common/Text';
|
||||||
import Avatar from '../common/Avatar';
|
import Avatar from '../common/Avatar';
|
||||||
@@ -23,9 +23,9 @@ interface SystemMessageItemProps {
|
|||||||
index?: number; // 用于交错动画
|
index?: number; // 用于交错动画
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统消息类型到图标和颜色的映射
|
|
||||||
const getSystemMessageIcon = (
|
const getSystemMessageIcon = (
|
||||||
systemType: SystemMessageType
|
systemType: SystemMessageType,
|
||||||
|
colors: AppColors
|
||||||
): { icon: keyof typeof MaterialCommunityIcons.glyphMap; color: string; bgColor: string; gradient: string[] } => {
|
): { icon: keyof typeof MaterialCommunityIcons.glyphMap; color: string; bgColor: string; gradient: string[] } => {
|
||||||
switch (systemType) {
|
switch (systemType) {
|
||||||
case 'like_post':
|
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 getSystemMessageTitle = (message: SystemMessageResponse): string => {
|
||||||
const { system_type, extra_data } = message;
|
const { system_type, extra_data } = message;
|
||||||
@@ -177,6 +326,9 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
requestActionLoading = false,
|
requestActionLoading = false,
|
||||||
index = 0,
|
index = 0,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createSystemMessageStyles(colors), [colors]);
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (dateString: string): string => {
|
const formatTime = (dateString: string): string => {
|
||||||
try {
|
try {
|
||||||
@@ -189,7 +341,7 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { icon, color, bgColor } = getSystemMessageIcon(message.system_type);
|
const { icon, color, bgColor } = getSystemMessageIcon(message.system_type, colors);
|
||||||
const title = getSystemMessageTitle(message);
|
const title = getSystemMessageTitle(message);
|
||||||
const { extra_data } = message;
|
const { extra_data } = message;
|
||||||
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
|
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
|
||||||
@@ -212,17 +364,17 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
const getStatusBadge = () => {
|
const getStatusBadge = () => {
|
||||||
if (requestStatus === 'accepted') {
|
if (requestStatus === 'accepted') {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.statusBadge, { backgroundColor: '#E8F5E9' }]}>
|
<View style={[styles.statusBadge, { backgroundColor: `${colors.success.main}22` }]}>
|
||||||
<MaterialCommunityIcons name="check" size={10} color="#4CAF50" />
|
<MaterialCommunityIcons name="check" size={10} color={colors.success.main} />
|
||||||
<Text variant="caption" color="#4CAF50" style={styles.statusText}>已同意</Text>
|
<Text variant="caption" color={colors.success.main} style={styles.statusText}>已同意</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (requestStatus === 'rejected') {
|
if (requestStatus === 'rejected') {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.statusBadge, { backgroundColor: '#FFEBEE' }]}>
|
<View style={[styles.statusBadge, { backgroundColor: `${colors.error.main}22` }]}>
|
||||||
<MaterialCommunityIcons name="close" size={10} color="#F44336" />
|
<MaterialCommunityIcons name="close" size={10} color={colors.error.main} />
|
||||||
<Text variant="caption" color="#F44336" style={styles.statusText}>已拒绝</Text>
|
<Text variant="caption" color={colors.error.main} style={styles.statusText}>已拒绝</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -328,16 +480,16 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
onPress={() => onRequestAction?.(false)}
|
onPress={() => onRequestAction?.(false)}
|
||||||
disabled={requestActionLoading}
|
disabled={requestActionLoading}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="close" size={14} color="#F44336" />
|
<MaterialCommunityIcons name="close" size={14} color={colors.error.main} />
|
||||||
<Text variant="caption" color="#F44336" style={styles.actionBtnText}>拒绝</Text>
|
<Text variant="caption" color={colors.error.main} style={styles.actionBtnText}>拒绝</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionBtn, styles.approveBtn]}
|
style={[styles.actionBtn, styles.approveBtn]}
|
||||||
onPress={() => onRequestAction?.(true)}
|
onPress={() => onRequestAction?.(true)}
|
||||||
disabled={requestActionLoading}
|
disabled={requestActionLoading}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="check" size={14} color="#4CAF50" />
|
<MaterialCommunityIcons name="check" size={14} color={colors.success.main} />
|
||||||
<Text variant="caption" color="#4CAF50" style={styles.actionBtnText}>同意</Text>
|
<Text variant="caption" color={colors.success.main} style={styles.actionBtnText}>同意</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -353,151 +505,4 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default SystemMessageItem;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
* 新增胶囊式、分段式等现代设计风格
|
* 新增胶囊式、分段式等现代设计风格
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, useMemo } from 'react';
|
||||||
import { View, TouchableOpacity, StyleSheet, ScrollView, Animated, ViewStyle, StyleProp } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet, ScrollView, ViewStyle, StyleProp } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
import Text from '../common/Text';
|
||||||
|
|
||||||
type TabBarVariant = 'default' | 'pill' | 'segmented' | 'modern';
|
type TabBarVariant = 'default' | 'pill' | 'segmented' | 'modern';
|
||||||
@@ -20,10 +20,168 @@ interface TabBarProps {
|
|||||||
rightContent?: ReactNode;
|
rightContent?: ReactNode;
|
||||||
variant?: TabBarVariant;
|
variant?: TabBarVariant;
|
||||||
icons?: string[];
|
icons?: string[];
|
||||||
/** 合并到最外层容器,用于按页面微调外边距等 */
|
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<TabBarProps> = ({
|
const TabBar: React.FC<TabBarProps> = ({
|
||||||
tabs,
|
tabs,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
@@ -34,6 +192,9 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
icons,
|
icons,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createTabBarStyles(colors), [colors]);
|
||||||
|
|
||||||
const renderTabs = () => {
|
const renderTabs = () => {
|
||||||
return tabs.map((tab, index) => {
|
return tabs.map((tab, index) => {
|
||||||
const isActive = index === activeIndex;
|
const isActive = index === activeIndex;
|
||||||
@@ -112,11 +273,7 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
style={styles.segmentedTabIcon}
|
style={styles.segmentedTabIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text variant="body" color={isActive ? colors.primary.main : colors.text.secondary} style={styles.segmentedTabText}>
|
||||||
variant="body"
|
|
||||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
|
||||||
style={styles.segmentedTabText}
|
|
||||||
>
|
|
||||||
{tab}
|
{tab}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -124,7 +281,6 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// default variant
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={index}
|
key={index}
|
||||||
@@ -161,11 +317,7 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
if (scrollable) {
|
if (scrollable) {
|
||||||
return (
|
return (
|
||||||
<View style={[getContainerStyle(), style]}>
|
<View style={[getContainerStyle(), style]}>
|
||||||
<ScrollView
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollableContainer}>
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.scrollableContainer}
|
|
||||||
>
|
|
||||||
{renderTabs()}
|
{renderTabs()}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{rightContent && <View style={styles.rightContent}>{rightContent}</View>}
|
{rightContent && <View style={styles.rightContent}>{rightContent}</View>}
|
||||||
@@ -181,170 +333,4 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default TabBar;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* 在宽屏下显示更大的头像和封面
|
* 在宽屏下显示更大的头像和封面
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Image,
|
Image,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
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 { User } from '../../types';
|
||||||
import Text from '../common/Text';
|
import Text from '../common/Text';
|
||||||
import Button from '../common/Button';
|
import Button from '../common/Button';
|
||||||
@@ -49,7 +49,8 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
|||||||
onFollowersPress,
|
onFollowersPress,
|
||||||
onAvatarPress,
|
onAvatarPress,
|
||||||
}) => {
|
}) => {
|
||||||
// 响应式布局
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createUserProfileHeaderStyles(colors), [colors]);
|
||||||
const { isWideScreen, isDesktop, width } = useResponsive();
|
const { isWideScreen, isDesktop, width } = useResponsive();
|
||||||
|
|
||||||
// 格式化数字
|
// 格式化数字
|
||||||
@@ -317,7 +318,8 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createUserProfileHeaderStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
},
|
},
|
||||||
@@ -533,7 +535,8 @@ const styles = StyleSheet.create({
|
|||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
padding: spacing.sm,
|
padding: spacing.sm,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 React.memo 避免不必要的重新渲染
|
// 使用 React.memo 避免不必要的重新渲染
|
||||||
const MemoizedUserProfileHeader = React.memo(UserProfileHeader);
|
const MemoizedUserProfileHeader = React.memo(UserProfileHeader);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 风格与现代整体UI保持一致
|
* 风格与现代整体UI保持一致
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Animated,
|
Animated,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { VoteOptionDTO } from '../../types';
|
||||||
import Text from '../common/Text';
|
import Text from '../common/Text';
|
||||||
|
|
||||||
@@ -28,6 +28,146 @@ interface VoteCardProps {
|
|||||||
compact?: boolean;
|
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<VoteCardProps> = ({
|
const VoteCard: React.FC<VoteCardProps> = ({
|
||||||
options,
|
options,
|
||||||
totalVotes,
|
totalVotes,
|
||||||
@@ -38,7 +178,8 @@ const VoteCard: React.FC<VoteCardProps> = ({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 动画值
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createVoteCardStyles(colors), [colors]);
|
||||||
const progressAnim = React.useRef(new Animated.Value(0)).current;
|
const progressAnim = React.useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -151,7 +292,7 @@ const VoteCard: React.FC<VoteCardProps> = ({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [hasVoted, votedOptionId, calculatePercentage, handleVote, isLoading, progressAnim, compact]);
|
}, [hasVoted, votedOptionId, calculatePercentage, handleVote, isLoading, progressAnim, compact, colors, styles]);
|
||||||
|
|
||||||
// 排序后的选项(已投票的排在前面)
|
// 排序后的选项(已投票的排在前面)
|
||||||
const sortedOptions = React.useMemo(() => {
|
const sortedOptions = React.useMemo(() => {
|
||||||
@@ -229,142 +370,4 @@ const VoteCard: React.FC<VoteCardProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default VoteCard;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 用于创建帖子时编辑投票选项
|
* 用于创建帖子时编辑投票选项
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
import Text from '../common/Text';
|
||||||
|
|
||||||
interface VoteEditorProps {
|
interface VoteEditorProps {
|
||||||
@@ -24,6 +24,86 @@ interface VoteEditorProps {
|
|||||||
maxLength?: number;
|
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<VoteEditorProps> = ({
|
const VoteEditor: React.FC<VoteEditorProps> = ({
|
||||||
options,
|
options,
|
||||||
onAddOption,
|
onAddOption,
|
||||||
@@ -33,6 +113,8 @@ const VoteEditor: React.FC<VoteEditorProps> = ({
|
|||||||
minOptions = 2,
|
minOptions = 2,
|
||||||
maxLength = 50,
|
maxLength = 50,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createVoteEditorStyles(colors), [colors]);
|
||||||
const validOptionsCount = options.filter(opt => opt.trim() !== '').length;
|
const validOptionsCount = options.filter(opt => opt.trim() !== '').length;
|
||||||
const canAddOption = options.length < maxOptions;
|
const canAddOption = options.length < maxOptions;
|
||||||
const canRemoveOption = options.length > minOptions;
|
const canRemoveOption = options.length > minOptions;
|
||||||
@@ -122,82 +204,4 @@ const VoteEditor: React.FC<VoteEditorProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default VoteEditor;
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
* 用于帖子列表中显示投票预览,类似微博风格
|
* 用于帖子列表中显示投票预览,类似微博风格
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
import Text from '../common/Text';
|
||||||
|
|
||||||
interface VotePreviewProps {
|
interface VotePreviewProps {
|
||||||
@@ -19,11 +19,51 @@ interface VotePreviewProps {
|
|||||||
onPress?: () => void;
|
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<VotePreviewProps> = ({
|
const VotePreview: React.FC<VotePreviewProps> = ({
|
||||||
totalVotes = 0,
|
totalVotes = 0,
|
||||||
optionsCount = 0,
|
optionsCount = 0,
|
||||||
onPress,
|
onPress,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createVotePreviewStyles(colors), [colors]);
|
||||||
|
|
||||||
// 格式化票数
|
// 格式化票数
|
||||||
const formatVoteCount = (count: number): string => {
|
const formatVoteCount = (count: number): string => {
|
||||||
if (count >= 10000) {
|
if (count >= 10000) {
|
||||||
@@ -71,39 +111,4 @@ const VotePreview: React.FC<VotePreviewProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default VotePreview;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { TouchableOpacity, StyleProp, ViewStyle, StyleSheet } from 'react-native';
|
import { TouchableOpacity, StyleProp, ViewStyle, StyleSheet } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
|
||||||
import { borderRadius, colors, spacing } from '../../theme';
|
import { borderRadius, spacing, useAppColors } from '../../theme';
|
||||||
|
|
||||||
type AppBackButtonIcon = 'arrow-left' | 'close';
|
type AppBackButtonIcon = 'arrow-left' | 'close';
|
||||||
|
|
||||||
@@ -20,11 +20,15 @@ const AppBackButton: React.FC<AppBackButtonProps> = ({
|
|||||||
onPress,
|
onPress,
|
||||||
icon = 'arrow-left',
|
icon = 'arrow-left',
|
||||||
style,
|
style,
|
||||||
iconColor = colors.text.primary,
|
iconColor: iconColorProp,
|
||||||
backgroundColor = colors.background.paper,
|
backgroundColor: backgroundColorProp,
|
||||||
hitSlop = 8,
|
hitSlop = 8,
|
||||||
testID,
|
testID,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const iconColor = iconColorProp ?? colors.text.primary;
|
||||||
|
const backgroundColor = backgroundColorProp ?? colors.background.paper;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
|||||||
@@ -5,10 +5,98 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
||||||
import { bindDialogListener, DialogPayload } from '../../services/dialogService';
|
import { bindDialogListener, DialogPayload } from '../../services/dialogService';
|
||||||
import { borderRadius, colors, shadows, spacing } from '../../theme';
|
import { borderRadius, shadows, spacing, useAppColors, type AppColors } from '../../theme';
|
||||||
|
|
||||||
|
function createDialogHostStyles(colors: AppColors) {
|
||||||
|
return 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: colors.background.paper,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: colors.primary.main,
|
||||||
|
borderColor: colors.primary.main,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
borderColor: colors.divider,
|
||||||
|
},
|
||||||
|
destructiveButton: {
|
||||||
|
backgroundColor: `${colors.error.main}18`,
|
||||||
|
borderColor: `${colors.error.main}40`,
|
||||||
|
},
|
||||||
|
actionText: {
|
||||||
|
color: colors.text.primary,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
primaryText: {
|
||||||
|
color: colors.text.inverse,
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
color: colors.text.secondary,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
destructiveText: {
|
||||||
|
color: colors.error.main,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const AppDialogHost: React.FC = () => {
|
const AppDialogHost: React.FC = () => {
|
||||||
const [dialog, setDialog] = useState<DialogPayload | null>(null);
|
const [dialog, setDialog] = useState<DialogPayload | null>(null);
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createDialogHostStyles(colors), [colors]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unbind = bindDialogListener((payload) => {
|
const unbind = bindDialogListener((payload) => {
|
||||||
@@ -19,7 +107,7 @@ const AppDialogHost: React.FC = () => {
|
|||||||
|
|
||||||
const actions = useMemo<AlertButton[]>(() => {
|
const actions = useMemo<AlertButton[]>(() => {
|
||||||
if (!dialog?.actions?.length) return [{ text: '确定' }];
|
if (!dialog?.actions?.length) return [{ text: '确定' }];
|
||||||
return dialog.actions.slice(0, 3);
|
return dialog.actions;
|
||||||
}, [dialog]);
|
}, [dialog]);
|
||||||
|
|
||||||
const onClose = () => {
|
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;
|
export default AppDialogHost;
|
||||||
|
|||||||
@@ -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 { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
|
||||||
import { bindPromptListener, PromptPayload, PromptType } from '../../services/promptService';
|
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 {
|
interface PromptState extends PromptPayload {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -12,29 +12,96 @@ interface PromptState extends PromptPayload {
|
|||||||
|
|
||||||
const DEFAULT_DURATION = 2200;
|
const DEFAULT_DURATION = 2200;
|
||||||
|
|
||||||
const styleMap: Record<PromptType, { backgroundColor: string; icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'] }> = {
|
function createPromptBarStyles(colors: AppColors) {
|
||||||
info: { backgroundColor: '#FFFFFF', icon: 'information-outline' },
|
return StyleSheet.create({
|
||||||
success: { backgroundColor: '#FFFFFF', icon: 'check-circle-outline' },
|
wrapper: {
|
||||||
warning: { backgroundColor: '#FFFFFF', icon: 'alert-outline' },
|
position: 'absolute',
|
||||||
error: { backgroundColor: '#FFFFFF', icon: 'alert-circle-outline' },
|
left: spacing.md,
|
||||||
};
|
right: spacing.md,
|
||||||
|
zIndex: 9999,
|
||||||
const iconColorMap: Record<PromptType, string> = {
|
},
|
||||||
info: colors.primary.main,
|
card: {
|
||||||
success: colors.success.main,
|
borderRadius: borderRadius.xl,
|
||||||
warning: colors.warning.dark,
|
paddingVertical: spacing.md,
|
||||||
error: colors.error.main,
|
paddingHorizontal: spacing.md,
|
||||||
};
|
borderWidth: 1,
|
||||||
|
borderColor: `${colors.primary.main}22`,
|
||||||
const accentColorMap: Record<PromptType, string> = {
|
flexDirection: 'row',
|
||||||
info: colors.primary.main,
|
alignItems: 'center',
|
||||||
success: colors.success.main,
|
overflow: 'hidden',
|
||||||
warning: colors.warning.main,
|
...shadows.lg,
|
||||||
error: colors.error.main,
|
},
|
||||||
};
|
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 AppPromptBar: React.FC = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createPromptBarStyles(colors), [colors]);
|
||||||
|
|
||||||
|
const styleMap = useMemo<
|
||||||
|
Record<PromptType, { backgroundColor: string; icon: React.ComponentProps<typeof MaterialCommunityIcons>['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<PromptState | null>(null);
|
const [prompt, setPrompt] = useState<PromptState | null>(null);
|
||||||
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const animation = useRef(new Animated.Value(0)).current;
|
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;
|
export default AppPromptBar;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 使用 expo-image 实现内存+磁盘双级缓存,头像秒加载
|
* 使用 expo-image 实现内存+磁盘双级缓存,头像秒加载
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { colors, borderRadius } from '../../theme';
|
import { borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||||
import Text from './Text';
|
import Text from './Text';
|
||||||
|
|
||||||
type AvatarSource = string | { uri: string } | number | null;
|
type AvatarSource = string | { uri: string } | number | null;
|
||||||
@@ -27,29 +27,50 @@ interface AvatarProps {
|
|||||||
style?: ViewStyle;
|
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<AvatarProps> = ({
|
const Avatar: React.FC<AvatarProps> = ({
|
||||||
source,
|
source,
|
||||||
size = 40,
|
size = 40,
|
||||||
name,
|
name,
|
||||||
onPress,
|
onPress,
|
||||||
showBadge = false,
|
showBadge = false,
|
||||||
badgeColor = colors.success.main,
|
badgeColor,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
// 获取首字母
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createAvatarStyles(colors), [colors]);
|
||||||
|
const resolvedBadgeColor = badgeColor ?? colors.success.main;
|
||||||
|
|
||||||
const getInitial = (): string => {
|
const getInitial = (): string => {
|
||||||
if (!name) return '?';
|
if (!name) return '?';
|
||||||
const firstChar = name.charAt(0).toUpperCase();
|
const firstChar = name.charAt(0).toUpperCase();
|
||||||
// 中文字符
|
|
||||||
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
|
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
|
||||||
return firstChar;
|
return firstChar;
|
||||||
}
|
}
|
||||||
return firstChar;
|
return firstChar;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染头像内容
|
|
||||||
const renderAvatarContent = () => {
|
const renderAvatarContent = () => {
|
||||||
// 如果有图片源
|
|
||||||
if (source) {
|
if (source) {
|
||||||
const imageSource =
|
const imageSource =
|
||||||
typeof source === 'string' ? { uri: source } : source;
|
typeof source === 'string' ? { uri: source } : source;
|
||||||
@@ -72,7 +93,6 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示首字母
|
|
||||||
const fontSize = size / 2;
|
const fontSize = size / 2;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -100,7 +120,7 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
styles.badge,
|
styles.badge,
|
||||||
{
|
{
|
||||||
backgroundColor: badgeColor,
|
backgroundColor: resolvedBadgeColor,
|
||||||
width: size * 0.3,
|
width: size * 0.3,
|
||||||
height: size * 0.3,
|
height: size * 0.3,
|
||||||
borderRadius: (size * 0.3) / 2,
|
borderRadius: (size * 0.3) / 2,
|
||||||
@@ -124,23 +144,4 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
return avatarContainer;
|
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;
|
export default Avatar;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 支持多种变体、尺寸、加载状态和图标
|
* 支持多种变体、尺寸、加载状态和图标
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
import Text from './Text';
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'danger';
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'danger';
|
||||||
@@ -27,12 +27,86 @@ interface ButtonProps {
|
|||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
icon?: string; // MaterialCommunityIcons name
|
icon?: string;
|
||||||
iconPosition?: IconPosition;
|
iconPosition?: IconPosition;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
style?: ViewStyle;
|
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<ButtonProps> = ({
|
const Button: React.FC<ButtonProps> = ({
|
||||||
title,
|
title,
|
||||||
onPress,
|
onPress,
|
||||||
@@ -45,11 +119,12 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
// 获取按钮样式
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createButtonStyles(colors), [colors]);
|
||||||
|
|
||||||
const getButtonStyle = (): ViewStyle[] => {
|
const getButtonStyle = (): ViewStyle[] => {
|
||||||
const baseStyle: ViewStyle[] = [styles.base, styles[`size_${size}`]];
|
const baseStyle: ViewStyle[] = [styles.base, styles[`size_${size}`]];
|
||||||
|
|
||||||
// 变体样式
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
baseStyle.push(styles.primary);
|
baseStyle.push(styles.primary);
|
||||||
@@ -68,12 +143,10 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用状态
|
|
||||||
if (disabled || loading) {
|
if (disabled || loading) {
|
||||||
baseStyle.push(styles.disabled);
|
baseStyle.push(styles.disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全宽度
|
|
||||||
if (fullWidth) {
|
if (fullWidth) {
|
||||||
baseStyle.push(styles.fullWidth);
|
baseStyle.push(styles.fullWidth);
|
||||||
}
|
}
|
||||||
@@ -81,7 +154,6 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
return baseStyle;
|
return baseStyle;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取文本样式
|
|
||||||
const getTextStyle = (): TextStyle => {
|
const getTextStyle = (): TextStyle => {
|
||||||
const baseStyle: TextStyle = {
|
const baseStyle: TextStyle = {
|
||||||
...styles.textBase,
|
...styles.textBase,
|
||||||
@@ -109,7 +181,6 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
return baseStyle;
|
return baseStyle;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取图标大小
|
|
||||||
const getIconSize = (): number => {
|
const getIconSize = (): number => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'sm':
|
case 'sm':
|
||||||
@@ -121,7 +192,6 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取图标颜色
|
|
||||||
const getIconColor = (): string => {
|
const getIconColor = (): string => {
|
||||||
if (disabled || loading) {
|
if (disabled || loading) {
|
||||||
return colors.text.disabled;
|
return colors.text.disabled;
|
||||||
@@ -150,9 +220,9 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
size="small"
|
size="small"
|
||||||
color={variant === 'outline' || variant === 'text'
|
color={
|
||||||
? colors.primary.main
|
variant === 'outline' || variant === 'text' ? colors.primary.main : colors.text.inverse
|
||||||
: colors.text.inverse}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
@@ -179,82 +249,4 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default Button;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* 白色背景、圆角、阴影,支持点击
|
* 白色背景、圆角、阴影,支持点击
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { View, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
|
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';
|
type ShadowSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -13,10 +13,20 @@ interface CardProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
shadow?: ShadowSize;
|
shadow?: ShadowSize | 'none';
|
||||||
style?: ViewStyle;
|
style?: ViewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCardStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const Card: React.FC<CardProps> = ({
|
const Card: React.FC<CardProps> = ({
|
||||||
children,
|
children,
|
||||||
onPress,
|
onPress,
|
||||||
@@ -24,13 +34,11 @@ const Card: React.FC<CardProps> = ({
|
|||||||
shadow = 'none',
|
shadow = 'none',
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createCardStyles(colors), [colors]);
|
||||||
const shadowStyle = shadow !== 'none' ? shadows[shadow as keyof typeof shadows] : undefined;
|
const shadowStyle = shadow !== 'none' ? shadows[shadow as keyof typeof shadows] : undefined;
|
||||||
|
|
||||||
const cardStyle = [
|
const cardStyle = [styles.card, { padding }, shadowStyle].filter(Boolean);
|
||||||
styles.card,
|
|
||||||
{ padding },
|
|
||||||
shadowStyle,
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (onPress) {
|
if (onPress) {
|
||||||
return (
|
return (
|
||||||
@@ -47,12 +55,4 @@ const Card: React.FC<CardProps> = ({
|
|||||||
return <View style={[...cardStyle, style]}>{children}</View>;
|
return <View style={[...cardStyle, style]}>{children}</View>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
backgroundColor: colors.background.paper,
|
|
||||||
borderRadius: borderRadius.lg,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet, ViewStyle } from 'react-native';
|
import { View, StyleSheet, ViewStyle } from 'react-native';
|
||||||
import { colors, spacing } from '../../theme';
|
import { spacing, useAppColors } from '../../theme';
|
||||||
|
|
||||||
interface DividerProps {
|
interface DividerProps {
|
||||||
margin?: number;
|
margin?: number;
|
||||||
@@ -13,16 +13,14 @@ interface DividerProps {
|
|||||||
style?: ViewStyle;
|
style?: ViewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Divider: React.FC<DividerProps> = ({
|
const Divider: React.FC<DividerProps> = ({ margin = spacing.lg, color, style }) => {
|
||||||
margin = spacing.lg,
|
const colors = useAppColors();
|
||||||
color = colors.divider,
|
const lineColor = color ?? colors.divider;
|
||||||
style,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.divider,
|
styles.divider,
|
||||||
{ marginVertical: margin, backgroundColor: color },
|
{ marginVertical: margin, backgroundColor: lineColor },
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
* 显示空数据时的占位界面,采用现代插图风格设计
|
* 显示空数据时的占位界面,采用现代插图风格设计
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { View, StyleSheet, ViewStyle, Dimensions } from 'react-native';
|
import { View, StyleSheet, ViewStyle } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 Text from './Text';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
|
||||||
@@ -20,6 +20,109 @@ interface EmptyStateProps {
|
|||||||
variant?: 'default' | 'modern' | 'compact';
|
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<EmptyStateProps> = ({
|
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
icon = 'folder-open-outline',
|
icon = 'folder-open-outline',
|
||||||
title,
|
title,
|
||||||
@@ -29,7 +132,9 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
style,
|
style,
|
||||||
variant = 'modern',
|
variant = 'modern',
|
||||||
}) => {
|
}) => {
|
||||||
// 现代风格空状态
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createEmptyStateStyles(colors), [colors]);
|
||||||
|
|
||||||
if (variant === 'modern') {
|
if (variant === 'modern') {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.modernContainer, style]}>
|
<View style={[styles.modernContainer, style]}>
|
||||||
@@ -45,19 +150,11 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
<View style={styles.decorativeCircle1} />
|
<View style={styles.decorativeCircle1} />
|
||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text variant="h3" color={colors.text.primary} style={styles.modernTitle}>
|
||||||
variant="h3"
|
|
||||||
color={colors.text.primary}
|
|
||||||
style={styles.modernTitle}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text
|
<Text variant="body" color={colors.text.secondary} style={styles.modernDescription}>
|
||||||
variant="body"
|
|
||||||
color={colors.text.secondary}
|
|
||||||
style={styles.modernDescription}
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -74,7 +171,6 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 紧凑风格
|
|
||||||
if (variant === 'compact') {
|
if (variant === 'compact') {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.compactContainer, style]}>
|
<View style={[styles.compactContainer, style]}>
|
||||||
@@ -84,18 +180,13 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
color={colors.text.disabled}
|
color={colors.text.disabled}
|
||||||
style={styles.compactIcon}
|
style={styles.compactIcon}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text variant="body" color={colors.text.secondary} style={styles.compactTitle}>
|
||||||
variant="body"
|
|
||||||
color={colors.text.secondary}
|
|
||||||
style={styles.compactTitle}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认风格
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, style]}>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
@@ -104,19 +195,11 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
color={colors.text.disabled}
|
color={colors.text.disabled}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text variant="h3" color={colors.text.secondary} style={styles.title}>
|
||||||
variant="h3"
|
|
||||||
color={colors.text.secondary}
|
|
||||||
style={styles.title}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text
|
<Text variant="body" color={colors.text.secondary} style={styles.description}>
|
||||||
variant="body"
|
|
||||||
color={colors.text.secondary}
|
|
||||||
style={styles.description}
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -133,110 +216,4 @@ const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default EmptyState;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as MediaLibrary from 'expo-media-library';
|
import * as MediaLibrary from 'expo-media-library';
|
||||||
import { File, Paths } from 'expo-file-system';
|
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');
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 支持预览图优化
|
* 支持预览图优化
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
import React, { useMemo, useCallback, useState, useEffect, createContext, useContext } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SmartImage } from './SmartImage';
|
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';
|
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
|
||||||
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
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<typeof createImageGridStyles>;
|
||||||
|
const ImageGridStylesContext = createContext<ImageGridStyles | null>(null);
|
||||||
|
|
||||||
|
function useImageGridStyles(): ImageGridStyles {
|
||||||
|
const ctx = useContext(ImageGridStylesContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useImageGridStyles must be used within ImageGrid or CompactImageGrid');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 单张图片子组件 ───────────────────────────────────────────────────────────
|
// ─── 单张图片子组件 ───────────────────────────────────────────────────────────
|
||||||
// 独立成组件,方便用 useState 管理异步加载到的实际尺寸
|
// 独立成组件,方便用 useState 管理异步加载到的实际尺寸
|
||||||
|
|
||||||
@@ -123,6 +206,7 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||||||
usePreview,
|
usePreview,
|
||||||
displayMode,
|
displayMode,
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useImageGridStyles();
|
||||||
const [aspectRatio, setAspectRatio] = useState<number | null>(null);
|
const [aspectRatio, setAspectRatio] = useState<number | null>(null);
|
||||||
const mainUri = image.uri || image.url || '';
|
const mainUri = image.uri || image.url || '';
|
||||||
const thumbnailUri =
|
const thumbnailUri =
|
||||||
@@ -245,7 +329,8 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
displayMode = 'list',
|
displayMode = 'list',
|
||||||
usePreview = true,
|
usePreview = true,
|
||||||
}) => {
|
}) => {
|
||||||
// 通过 onLayout 拿到容器实际宽度
|
const colors = useAppColors();
|
||||||
|
const gridStyles = useMemo(() => createImageGridStyles(colors), [colors]);
|
||||||
const [containerWidth, setContainerWidth] = useState(0);
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
// 过滤有效图片 - 支持 uri 或 url 字段
|
// 过滤有效图片 - 支持 uri 或 url 字段
|
||||||
@@ -303,7 +388,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
// 渲染横向双图
|
// 渲染横向双图
|
||||||
const renderHorizontal = () => {
|
const renderHorizontal = () => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.horizontalContainer, { gap }]}>
|
<View style={[gridStyles.horizontalContainer, { gap }]}>
|
||||||
{displayImages.map((image, index) => {
|
{displayImages.map((image, index) => {
|
||||||
const previewUrl = usePreview && displayMode
|
const previewUrl = usePreview && displayMode
|
||||||
? getPreviewImageUrl(image as any, displayMode)
|
? getPreviewImageUrl(image as any, displayMode)
|
||||||
@@ -314,7 +399,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
key={image.id || index}
|
key={image.id || index}
|
||||||
onPress={() => handleImagePress(index)}
|
onPress={() => handleImagePress(index)}
|
||||||
style={[
|
style={[
|
||||||
styles.horizontalItem,
|
gridStyles.horizontalItem,
|
||||||
{
|
{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
@@ -324,7 +409,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri: previewUrl }}
|
source={{ uri: previewUrl }}
|
||||||
style={styles.fullSize}
|
style={gridStyles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
/>
|
/>
|
||||||
@@ -338,7 +423,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
// 渲染网格布局
|
// 渲染网格布局
|
||||||
const renderGrid = () => {
|
const renderGrid = () => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.gridContainer, { gap }]}>
|
<View style={[gridStyles.gridContainer, { gap }]}>
|
||||||
{displayImages.map((image, index) => {
|
{displayImages.map((image, index) => {
|
||||||
const isLastVisible = index === displayImages.length - 1;
|
const isLastVisible = index === displayImages.length - 1;
|
||||||
const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay;
|
const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay;
|
||||||
@@ -352,8 +437,8 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
key={image.id || index}
|
key={image.id || index}
|
||||||
onPress={() => handleImagePress(index)}
|
onPress={() => handleImagePress(index)}
|
||||||
style={[
|
style={[
|
||||||
styles.gridItem,
|
gridStyles.gridItem,
|
||||||
gridColumns === 3 ? styles.gridItem3 : styles.gridItem2,
|
gridColumns === 3 ? gridStyles.gridItem3 : gridStyles.gridItem2,
|
||||||
{
|
{
|
||||||
borderRadius: borderRadiusValue,
|
borderRadius: borderRadiusValue,
|
||||||
},
|
},
|
||||||
@@ -361,13 +446,13 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri: previewUrl }}
|
source={{ uri: previewUrl }}
|
||||||
style={styles.fullSize}
|
style={gridStyles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
/>
|
/>
|
||||||
{showOverlay && (
|
{showOverlay && (
|
||||||
<View style={styles.moreOverlay}>
|
<View style={gridStyles.moreOverlay}>
|
||||||
<Text style={styles.moreText}>+{remainingCount}</Text>
|
<Text style={gridStyles.moreText}>+{remainingCount}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -397,7 +482,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
|
|
||||||
const renderColumn = (columnImages: ImageGridItem[], columnIndex: number) => {
|
const renderColumn = (columnImages: ImageGridItem[], columnIndex: number) => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.masonryColumn, { gap }]}>
|
<View style={[gridStyles.masonryColumn, { gap }]}>
|
||||||
{columnImages.map((image, index) => {
|
{columnImages.map((image, index) => {
|
||||||
const actualIndex = columnIndex + index * 2;
|
const actualIndex = columnIndex + index * 2;
|
||||||
const aspectRatio = image.width && image.height
|
const aspectRatio = image.width && image.height
|
||||||
@@ -414,7 +499,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
key={image.id || actualIndex}
|
key={image.id || actualIndex}
|
||||||
onPress={() => handleImagePress(actualIndex)}
|
onPress={() => handleImagePress(actualIndex)}
|
||||||
style={[
|
style={[
|
||||||
styles.masonryItem,
|
gridStyles.masonryItem,
|
||||||
{
|
{
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
height: Math.max(height, itemWidth * 0.7),
|
height: Math.max(height, itemWidth * 0.7),
|
||||||
@@ -424,7 +509,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri: previewUrl }}
|
source={{ uri: previewUrl }}
|
||||||
style={styles.fullSize}
|
style={gridStyles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
/>
|
/>
|
||||||
@@ -436,7 +521,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.masonryContainer, { gap }]}>
|
<View style={[gridStyles.masonryContainer, { gap }]}>
|
||||||
{renderColumn(leftColumn, 0)}
|
{renderColumn(leftColumn, 0)}
|
||||||
{renderColumn(rightColumn, 1)}
|
{renderColumn(rightColumn, 1)}
|
||||||
</View>
|
</View>
|
||||||
@@ -464,13 +549,15 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<ImageGridStylesContext.Provider value={gridStyles}>
|
||||||
style={[styles.container, style]}
|
<View
|
||||||
testID={testID}
|
style={[gridStyles.container, style]}
|
||||||
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}
|
testID={testID}
|
||||||
>
|
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}
|
||||||
{renderContent()}
|
>
|
||||||
</View>
|
{renderContent()}
|
||||||
|
</View>
|
||||||
|
</ImageGridStylesContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -490,6 +577,8 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
borderRadius: borderRadiusValue = borderRadius.sm,
|
borderRadius: borderRadiusValue = borderRadius.sm,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const gridStyles = useMemo(() => createImageGridStyles(colors), [colors]);
|
||||||
const containerWidth = maxWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING - 36 - spacing.sm; // 36是头像宽度
|
const containerWidth = maxWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING - 36 - spacing.sm; // 36是头像宽度
|
||||||
|
|
||||||
const renderCompactGrid = () => {
|
const renderCompactGrid = () => {
|
||||||
@@ -506,7 +595,7 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => props.onImagePress?.(images, 0)}
|
onPress={() => props.onImagePress?.(images, 0)}
|
||||||
style={[
|
style={[
|
||||||
styles.compactItem,
|
gridStyles.compactItem,
|
||||||
{
|
{
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@@ -516,7 +605,7 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
||||||
style={styles.fullSize}
|
style={gridStyles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
/>
|
/>
|
||||||
@@ -529,13 +618,13 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
const { itemSize } = calculateGridDimensions(count, containerWidth, gap, columns);
|
const { itemSize } = calculateGridDimensions(count, containerWidth, gap, columns);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.compactGrid, { gap }]}>
|
<View style={[gridStyles.compactGrid, { gap }]}>
|
||||||
{images.slice(0, 6).map((image, index) => (
|
{images.slice(0, 6).map((image, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={image.id || index}
|
key={image.id || index}
|
||||||
onPress={() => props.onImagePress?.(images, index)}
|
onPress={() => props.onImagePress?.(images, index)}
|
||||||
style={[
|
style={[
|
||||||
styles.compactItem,
|
gridStyles.compactItem,
|
||||||
{
|
{
|
||||||
width: itemSize,
|
width: itemSize,
|
||||||
height: itemSize,
|
height: itemSize,
|
||||||
@@ -545,13 +634,13 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
||||||
style={styles.fullSize}
|
style={gridStyles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
/>
|
/>
|
||||||
{index === 5 && images.length > 6 && (
|
{index === 5 && images.length > 6 && (
|
||||||
<View style={styles.moreOverlay}>
|
<View style={gridStyles.moreOverlay}>
|
||||||
<Text style={styles.moreText}>+{images.length - 6}</Text>
|
<Text style={gridStyles.moreText}>+{images.length - 6}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -560,86 +649,11 @@ export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <View style={styles.compactContainer}>{renderCompactGrid()}</View>;
|
return (
|
||||||
|
<ImageGridStylesContext.Provider value={gridStyles}>
|
||||||
|
<View style={gridStyles.compactContainer}>{renderCompactGrid()}</View>
|
||||||
|
</ImageGridStylesContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default ImageGrid;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 支持标签、错误提示、图标、多行输入等
|
* 支持标签、错误提示、图标、多行输入等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
import Text from './Text';
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
@@ -36,6 +36,46 @@ interface InputProps {
|
|||||||
autoCorrect?: boolean;
|
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<InputProps> = ({
|
const Input: React.FC<InputProps> = ({
|
||||||
value,
|
value,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
@@ -55,6 +95,8 @@ const Input: React.FC<InputProps> = ({
|
|||||||
keyboardType = 'default',
|
keyboardType = 'default',
|
||||||
autoCorrect = true,
|
autoCorrect = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createInputStyles(colors), [colors]);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const getBorderColor = () => {
|
const getBorderColor = () => {
|
||||||
@@ -87,11 +129,7 @@ const Input: React.FC<InputProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[
|
style={[styles.input, multiline && styles.multilineInput, inputStyle]}
|
||||||
styles.input,
|
|
||||||
multiline && styles.multilineInput,
|
|
||||||
inputStyle,
|
|
||||||
]}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -105,15 +143,11 @@ const Input: React.FC<InputProps> = ({
|
|||||||
autoCapitalize={autoCapitalize}
|
autoCapitalize={autoCapitalize}
|
||||||
keyboardType={keyboardType}
|
keyboardType={keyboardType}
|
||||||
autoCorrect={autoCorrect}
|
autoCorrect={autoCorrect}
|
||||||
// 确保光标可见
|
|
||||||
cursorColor={colors.primary.main}
|
cursorColor={colors.primary.main}
|
||||||
selectionColor={`${colors.primary.main}40`}
|
selectionColor={`${colors.primary.main}40`}
|
||||||
/>
|
/>
|
||||||
{rightIcon && (
|
{rightIcon && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={onRightIconPress} disabled={!onRightIconPress}>
|
||||||
onPress={onRightIconPress}
|
|
||||||
disabled={!onRightIconPress}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={rightIcon as any}
|
name={rightIcon as any}
|
||||||
size={20}
|
size={20}
|
||||||
@@ -132,43 +166,4 @@ const Input: React.FC<InputProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default Input;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* 支持不同尺寸、全屏模式
|
* 支持不同尺寸、全屏模式
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { View, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native';
|
import { View, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native';
|
||||||
import { colors } from '../../theme';
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
|
||||||
type LoadingSize = 'sm' | 'md' | 'lg';
|
type LoadingSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -16,12 +16,32 @@ interface LoadingProps {
|
|||||||
style?: ViewStyle;
|
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<LoadingProps> = ({
|
const Loading: React.FC<LoadingProps> = ({
|
||||||
size = 'md',
|
size = 'md',
|
||||||
color = colors.primary.main,
|
color,
|
||||||
fullScreen = false,
|
fullScreen = false,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createLoadingStyles(colors), [colors]);
|
||||||
|
const indicatorColor = color ?? colors.primary.main;
|
||||||
|
|
||||||
const getSize = (): 'small' | 'large' | undefined => {
|
const getSize = (): 'small' | 'large' | undefined => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'sm':
|
case 'sm':
|
||||||
@@ -36,30 +56,16 @@ const Loading: React.FC<LoadingProps> = ({
|
|||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.fullScreen, style]}>
|
<View style={[styles.fullScreen, style]}>
|
||||||
<ActivityIndicator size={getSize()} color={color} />
|
<ActivityIndicator size={getSize()} color={indicatorColor} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, style]}>
|
||||||
<ActivityIndicator size={getSize()} color={color} />
|
<ActivityIndicator size={getSize()} color={indicatorColor} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default Loading;
|
||||||
|
|||||||
137
src/components/common/SearchBar.tsx
Normal file
137
src/components/common/SearchBar.tsx
Normal file
@@ -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<SearchBarProps> = ({
|
||||||
|
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 (
|
||||||
|
<View style={[styles.container, isFocused && styles.containerFocused]}>
|
||||||
|
<View style={[styles.searchIconWrap, isFocused && styles.searchIconWrapFocused]}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="magnify"
|
||||||
|
size={18}
|
||||||
|
color={isFocused ? colors.primary.main : colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={colors.text.hint}
|
||||||
|
returnKeyType="search"
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
cursorColor={colors.primary.main}
|
||||||
|
selectionColor={`${colors.primary.main}40`}
|
||||||
|
/>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => onChangeText('')}
|
||||||
|
style={styles.clearButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="close" size={14} color={colors.text.secondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBar;
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* 基于 expo-image 封装,原生支持 GIF/WebP 动图
|
* 基于 expo-image 封装,原生支持 GIF/WebP 动图
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -17,7 +17,36 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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';
|
export type ImageLoadState = 'loading' | 'success' | 'error';
|
||||||
@@ -91,6 +120,8 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
|||||||
lazyRootMargin = '160px',
|
lazyRootMargin = '160px',
|
||||||
cachePolicy = 'memory-disk',
|
cachePolicy = 'memory-disk',
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createSmartImageStyles(colors), [colors]);
|
||||||
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
|
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
|
||||||
const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web');
|
const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web');
|
||||||
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
|
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
|
||||||
@@ -244,7 +275,7 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
|||||||
<View
|
<View
|
||||||
ref={Platform.OS === 'web' ? lazyTargetRef : undefined}
|
ref={Platform.OS === 'web' ? lazyTargetRef : undefined}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
style={[containerStyle, style as ViewStyle, { backgroundColor: '#F5F5F5' }]}
|
style={[containerStyle, style as ViewStyle, { backgroundColor: colors.background.default }]}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -328,36 +359,15 @@ export const VariantImage: React.FC<ImageVariantProps> = ({
|
|||||||
<SmartImage
|
<SmartImage
|
||||||
{...props}
|
{...props}
|
||||||
style={finalStyle}
|
style={finalStyle}
|
||||||
imageStyle={[styles.variantImage, imageStyle]}
|
imageStyle={[variantStyles.variantImage, imageStyle]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const variantStyles = StyleSheet.create({
|
||||||
image: {
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
variantImage: {
|
variantImage: {
|
||||||
flex: 1,
|
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;
|
export default SmartImage;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text as RNText, TextProps, StyleSheet, TextStyle, ViewStyle } from 'react-native';
|
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';
|
type TextVariant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'label';
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ const Text: React.FC<CustomTextProps> = ({
|
|||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
const textStyle = [
|
const textStyle = [
|
||||||
styles.base,
|
styles.base,
|
||||||
variantStyles[variant],
|
variantStyles[variant],
|
||||||
|
|||||||
@@ -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 { View, StyleSheet, ScrollView, TouchableOpacity, StatusBar } from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 * as hrefs from '../../navigation/hrefs';
|
||||||
import { Text, ResponsiveContainer } from '../../components/common';
|
import { Text, ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive, useResponsiveSpacing, useBreakpointGTE } from '../../hooks/useResponsive';
|
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 = () => {
|
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 router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
@@ -100,7 +114,7 @@ export const AppsScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#FAFAFA" />
|
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.default} />
|
||||||
|
|
||||||
{/* 与 MessageListScreen 顶栏同一结构 / 样式 */}
|
{/* 与 MessageListScreen 顶栏同一结构 / 样式 */}
|
||||||
<View style={[styles.msgHeader, isWideScreen && styles.msgHeaderWide]}>
|
<View style={[styles.msgHeader, isWideScreen && styles.msgHeaderWide]}>
|
||||||
@@ -143,93 +157,96 @@ export const AppsScreen: React.FC = () => {
|
|||||||
|
|
||||||
export default AppsScreen;
|
export default AppsScreen;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createAppsStyles(colors: AppColors) {
|
||||||
container: {
|
const headerBg = colors.background.default;
|
||||||
flex: 1,
|
return StyleSheet.create({
|
||||||
backgroundColor: colors.background.default,
|
container: {
|
||||||
},
|
flex: 1,
|
||||||
msgHeader: {
|
backgroundColor: colors.background.default,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
msgHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.md,
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
justifyContent: 'space-between',
|
||||||
backgroundColor: '#FAFAFA',
|
paddingHorizontal: spacing.md,
|
||||||
...shadows.sm,
|
paddingVertical: spacing.md,
|
||||||
},
|
backgroundColor: headerBg,
|
||||||
msgHeaderWide: {
|
...shadows.sm,
|
||||||
paddingHorizontal: spacing.lg,
|
},
|
||||||
paddingVertical: spacing.lg,
|
msgHeaderWide: {
|
||||||
},
|
paddingHorizontal: spacing.lg,
|
||||||
msgHeaderLeft: {
|
paddingVertical: spacing.lg,
|
||||||
width: 44,
|
},
|
||||||
},
|
msgHeaderLeft: {
|
||||||
msgHeaderCenter: {
|
width: 44,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
msgHeaderCenter: {
|
||||||
gap: spacing.xs,
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
msgHeaderTitle: {
|
gap: spacing.xs,
|
||||||
fontSize: 19,
|
},
|
||||||
fontWeight: '700',
|
msgHeaderTitle: {
|
||||||
color: '#333',
|
fontSize: 19,
|
||||||
},
|
fontWeight: '700',
|
||||||
msgHeaderTitleWide: {
|
color: colors.text.primary,
|
||||||
fontSize: 22,
|
},
|
||||||
},
|
msgHeaderTitleWide: {
|
||||||
msgHeaderRight: {
|
fontSize: 22,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
msgHeaderRight: {
|
||||||
},
|
flexDirection: 'row',
|
||||||
/** 与消息页「+」按钮同占位宽度,标题视觉居中 */
|
alignItems: 'center',
|
||||||
msgHeaderRightSpacer: {
|
},
|
||||||
width: 36,
|
/** 与消息页「+」按钮同占位宽度,标题视觉居中 */
|
||||||
height: 44,
|
msgHeaderRightSpacer: {
|
||||||
},
|
width: 36,
|
||||||
pageSubtitle: {
|
height: 44,
|
||||||
marginBottom: spacing.md,
|
},
|
||||||
lineHeight: fontSizes.sm * 1.45,
|
pageSubtitle: {
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
/** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */
|
lineHeight: fontSizes.sm * 1.45,
|
||||||
scroll: {
|
},
|
||||||
flex: 1,
|
/** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */
|
||||||
backgroundColor: '#FAFAFA',
|
scroll: {
|
||||||
},
|
flex: 1,
|
||||||
scrollContent: {
|
backgroundColor: headerBg,
|
||||||
paddingTop: spacing.md,
|
},
|
||||||
flexGrow: 1,
|
scrollContent: {
|
||||||
backgroundColor: '#FAFAFA',
|
paddingTop: spacing.md,
|
||||||
},
|
flexGrow: 1,
|
||||||
appCardInner: {
|
backgroundColor: headerBg,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
appCardInner: {
|
||||||
paddingVertical: spacing.lg,
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.lg,
|
alignItems: 'center',
|
||||||
},
|
paddingVertical: spacing.lg,
|
||||||
/** 与首页发帖 FAB 同主色实心圆 */
|
paddingHorizontal: spacing.lg,
|
||||||
appIconCircle: {
|
},
|
||||||
width: 52,
|
/** 与首页发帖 FAB 同主色实心圆 */
|
||||||
height: 52,
|
appIconCircle: {
|
||||||
borderRadius: borderRadius.full,
|
width: 52,
|
||||||
backgroundColor: colors.primary.main,
|
height: 52,
|
||||||
alignItems: 'center',
|
borderRadius: borderRadius.full,
|
||||||
justifyContent: 'center',
|
backgroundColor: colors.primary.main,
|
||||||
},
|
alignItems: 'center',
|
||||||
appCardText: {
|
justifyContent: 'center',
|
||||||
flex: 1,
|
},
|
||||||
marginLeft: spacing.md,
|
appCardText: {
|
||||||
marginRight: spacing.sm,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
appTitle: {
|
marginRight: spacing.sm,
|
||||||
fontWeight: '700',
|
},
|
||||||
fontSize: fontSizes.md + 1,
|
appTitle: {
|
||||||
},
|
fontWeight: '700',
|
||||||
appSubtitle: {
|
fontSize: fontSizes.md + 1,
|
||||||
marginTop: 4,
|
},
|
||||||
},
|
appSubtitle: {
|
||||||
footer: {
|
marginTop: 4,
|
||||||
alignItems: 'center',
|
},
|
||||||
paddingTop: spacing.xl,
|
footer: {
|
||||||
paddingBottom: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
paddingTop: spacing.xl,
|
||||||
});
|
paddingBottom: spacing.md,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -14,11 +14,117 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
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 { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { showPrompt } from '../../services/promptService';
|
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 = () => {
|
export const ForgotPasswordScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [verificationCode, setVerificationCode] = 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;
|
export default ForgotPasswordScreen;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰
|
* - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
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 { useAuthStore } from '../../stores';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { useResponsive, useResponsiveValue } from '../../hooks';
|
import { useResponsive, useResponsiveValue } from '../../hooks';
|
||||||
@@ -35,6 +35,8 @@ import { showPrompt } from '../../services/promptService';
|
|||||||
const SPLIT_BREAKPOINT = 768;
|
const SPLIT_BREAKPOINT = 768;
|
||||||
|
|
||||||
export const LoginScreen: React.FC = () => {
|
export const LoginScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createLoginStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const login = useAuthStore((state) => state.login);
|
const login = useAuthStore((state) => state.login);
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
@@ -453,7 +455,8 @@ export const LoginScreen: React.FC = () => {
|
|||||||
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
|
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createLoginStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -762,6 +765,7 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
// 移除这里的阴影,避免重复
|
// 移除这里的阴影,避免重复
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default LoginScreen;
|
export default LoginScreen;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -12,11 +12,148 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { qrcodeApi } from '../../services/authService';
|
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';
|
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 = () => {
|
export const QRCodeConfirmScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createQrCodeConfirmStyles(colors), [colors]);
|
||||||
const { sessionId: sessionIdParam } = useLocalSearchParams<{ sessionId?: string }>();
|
const { sessionId: sessionIdParam } = useLocalSearchParams<{ sessionId?: string }>();
|
||||||
const sessionId = sessionIdParam || '';
|
const sessionId = sessionIdParam || '';
|
||||||
|
|
||||||
@@ -37,9 +174,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMsg = err.response?.data?.message || '扫码失败';
|
const errorMsg = err.response?.data?.message || '扫码失败';
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
Alert.alert('扫码失败', errorMsg, [
|
Alert.alert('扫码失败', errorMsg, [{ text: '确定', onPress: () => router.back() }]);
|
||||||
{ text: '确定', onPress: () => router.back() }
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -49,9 +184,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await qrcodeApi.confirm(sessionId);
|
await qrcodeApi.confirm(sessionId);
|
||||||
Alert.alert('登录成功', '网页端已登录', [
|
Alert.alert('登录成功', '网页端已登录', [{ text: '确定', onPress: () => router.back() }]);
|
||||||
{ text: '确定', onPress: () => router.back() }
|
|
||||||
]);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMsg = err.response?.data?.message || '确认登录失败';
|
const errorMsg = err.response?.data?.message || '确认登录失败';
|
||||||
Alert.alert('登录失败', errorMsg);
|
Alert.alert('登录失败', errorMsg);
|
||||||
@@ -63,7 +196,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
try {
|
try {
|
||||||
await qrcodeApi.cancel(sessionId);
|
await qrcodeApi.cancel(sessionId);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// 忽略取消错误
|
// 忽略取消错误
|
||||||
}
|
}
|
||||||
router.back();
|
router.back();
|
||||||
@@ -97,7 +230,12 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<AppBackButton onPress={handleCancel} icon="close" style={styles.closeButton} iconColor="#666" />
|
<AppBackButton
|
||||||
|
onPress={handleCancel}
|
||||||
|
icon="close"
|
||||||
|
style={styles.closeButton}
|
||||||
|
iconColor={colors.text.secondary}
|
||||||
|
/>
|
||||||
<Text style={styles.headerTitle}>确认登录</Text>
|
<Text style={styles.headerTitle}>确认登录</Text>
|
||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
@@ -105,14 +243,14 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.infoCard}>
|
<View style={styles.infoCard}>
|
||||||
<Text style={styles.infoText}>您正在登录网页端</Text>
|
<Text style={styles.infoText}>您正在登录网页端</Text>
|
||||||
|
|
||||||
{userInfo && (
|
{userInfo && (
|
||||||
<View style={styles.userInfo}>
|
<View style={styles.userInfo}>
|
||||||
{userInfo.avatar ? (
|
{userInfo.avatar ? (
|
||||||
<Image source={{ uri: userInfo.avatar }} style={styles.avatar} />
|
<Image source={{ uri: userInfo.avatar }} style={styles.avatar} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.avatarPlaceholder}>
|
<View style={styles.avatarPlaceholder}>
|
||||||
<MaterialCommunityIcons name="account" size={40} color="#999" />
|
<MaterialCommunityIcons name="account" size={40} color={colors.text.disabled} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<Text style={styles.nickname}>{userInfo.nickname}</Text>
|
<Text style={styles.nickname}>{userInfo.nickname}</Text>
|
||||||
@@ -127,7 +265,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator color="#fff" />
|
<ActivityIndicator color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.confirmButtonText}>确认登录</Text>
|
<Text style={styles.confirmButtonText}>确认登录</Text>
|
||||||
)}
|
)}
|
||||||
@@ -146,137 +284,4 @@ export const QRCodeConfirmScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export default QRCodeConfirmScreen;
|
||||||
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;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰
|
* - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
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 { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
@@ -36,6 +36,8 @@ import { showPrompt } from '../../services/promptService';
|
|||||||
const SPLIT_BREAKPOINT = 768;
|
const SPLIT_BREAKPOINT = 768;
|
||||||
|
|
||||||
export const RegisterScreen: React.FC = () => {
|
export const RegisterScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const register = useAuthStore((state) => state.register);
|
const register = useAuthStore((state) => state.register);
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
@@ -739,7 +741,8 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
|
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createRegisterStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -1076,6 +1079,7 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default RegisterScreen;
|
export default RegisterScreen;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* 投票编辑器在宽屏下优化布局
|
* 投票编辑器在宽屏下优化布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -25,7 +25,14 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter, useLocalSearchParams, useNavigation } from 'expo-router';
|
import { useRouter, useLocalSearchParams, useNavigation } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
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 { Text, ResponsiveContainer } from '../../components/common';
|
||||||
import { channelService, postService, showPrompt, voteService } from '../../services';
|
import { channelService, postService, showPrompt, voteService } from '../../services';
|
||||||
import { ApiError } from '../../services/api';
|
import { ApiError } from '../../services/api';
|
||||||
@@ -84,6 +91,8 @@ const getPublishErrorMessage = (error: unknown): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createCreatePostStyles(colors), [colors]);
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
@@ -805,7 +814,8 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createCreatePostStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
flex: {
|
flex: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -1050,6 +1060,7 @@ const styles = StyleSheet.create({
|
|||||||
loadingText: {
|
loadingText: {
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default CreatePostScreen;
|
export default CreatePostScreen;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import { useRouter, useFocusEffect } from 'expo-router';
|
import { useRouter, useFocusEffect } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
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 { Post } from '../../types';
|
||||||
import { useUserStore, useHomeTabBarVisibilityStore } from '../../stores';
|
import { useUserStore, useHomeTabBarVisibilityStore } from '../../stores';
|
||||||
import { useCurrentUser } from '../../stores/authStore';
|
import { useCurrentUser } from '../../stores/authStore';
|
||||||
@@ -56,12 +56,125 @@ type ViewMode = 'list' | 'grid';
|
|||||||
type PostType = 'follow' | 'hot' | 'latest';
|
type PostType = 'follow' | 'hot' | 'latest';
|
||||||
type LatestCapsule = { id: string; name: string };
|
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 = () => {
|
export const HomeScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { posts: storePosts } = useUserStore();
|
const { posts: storePosts } = useUserStore();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const resolvedScheme = useResolvedColorScheme();
|
||||||
|
const styles = useMemo(() => createHomeStyles(colors), [colors]);
|
||||||
|
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
|
||||||
|
|
||||||
// 使用响应式 hook
|
// 使用响应式 hook
|
||||||
const {
|
const {
|
||||||
width,
|
width,
|
||||||
@@ -827,7 +940,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
|
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
|
||||||
<SearchScreen
|
<SearchScreen
|
||||||
onBack={() => setShowSearch(false)}
|
onBack={() => setShowSearch(false)}
|
||||||
/>
|
/>
|
||||||
@@ -838,8 +951,8 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 正常首页内容
|
// 正常首页内容
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
|
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
|
||||||
|
|
||||||
{/* 顶部Header */}
|
{/* 顶部Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
@@ -942,115 +1055,3 @@ export const HomeScreen: React.FC = () => {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
|
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
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 { Post, Comment, VoteResultDTO } from '../../types';
|
||||||
import { useUserStore } from '../../stores';
|
import { useUserStore } from '../../stores';
|
||||||
import { useCurrentUser } from '../../stores/authStore';
|
import { useCurrentUser } from '../../stores/authStore';
|
||||||
@@ -39,6 +45,8 @@ import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../h
|
|||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
export const PostDetailScreen: React.FC = () => {
|
export const PostDetailScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createPostDetailStyles(colors), [colors]);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -290,6 +298,9 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
|
// 与页面容器 background.default 一致,并去掉原生 header 底部分隔阴影,避免「一条线」
|
||||||
|
headerStyle: { backgroundColor: colors.background.default },
|
||||||
|
headerShadowVisible: false,
|
||||||
headerBackVisible: false,
|
headerBackVisible: false,
|
||||||
headerTitleAlign: 'left',
|
headerTitleAlign: 'left',
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
@@ -324,7 +335,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation]);
|
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation, colors.background.default, styles]);
|
||||||
|
|
||||||
// 监听键盘事件
|
// 监听键盘事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1702,7 +1713,8 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createPostDetailStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
flex: {
|
flex: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -2124,4 +2136,5 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 支持响应式布局,宽屏下显示更大的搜索结果区域
|
* 支持响应式布局,宽屏下显示更大的搜索结果区域
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { Post, User } from '../../types';
|
||||||
import { useUserStore } from '../../stores';
|
import { useUserStore } from '../../stores';
|
||||||
import { postService, authService } from '../../services';
|
import { postService, authService } from '../../services';
|
||||||
@@ -37,6 +37,8 @@ interface SearchScreenProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore();
|
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore();
|
||||||
@@ -544,115 +546,117 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createSearchScreenStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
searchHeader: {
|
},
|
||||||
flexDirection: 'row',
|
searchHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background.paper,
|
alignItems: 'center',
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomColor: `${colors.divider}70`,
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: `${colors.divider}70`,
|
||||||
searchShell: {
|
},
|
||||||
flex: 1,
|
searchShell: {
|
||||||
borderRadius: borderRadius.xl,
|
flex: 1,
|
||||||
backgroundColor: `${colors.primary.main}08`,
|
borderRadius: borderRadius.xl,
|
||||||
paddingHorizontal: spacing.xs,
|
backgroundColor: `${colors.primary.main}08`,
|
||||||
paddingVertical: spacing.xs,
|
paddingHorizontal: spacing.xs,
|
||||||
// 移除阴影效果
|
paddingVertical: spacing.xs,
|
||||||
shadowColor: 'transparent',
|
// 移除阴影效果
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowColor: 'transparent',
|
||||||
shadowOpacity: 0,
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowRadius: 0,
|
shadowOpacity: 0,
|
||||||
elevation: 0,
|
shadowRadius: 0,
|
||||||
},
|
elevation: 0,
|
||||||
cancelButton: {
|
},
|
||||||
backgroundColor: `${colors.primary.main}14`,
|
cancelButton: {
|
||||||
borderRadius: borderRadius.full,
|
backgroundColor: `${colors.primary.main}14`,
|
||||||
paddingHorizontal: spacing.md,
|
borderRadius: borderRadius.full,
|
||||||
paddingVertical: spacing.xs,
|
paddingHorizontal: spacing.md,
|
||||||
},
|
paddingVertical: spacing.xs,
|
||||||
cancelText: {
|
},
|
||||||
fontSize: fontSizes.md,
|
cancelText: {
|
||||||
fontWeight: '600',
|
fontSize: fontSizes.md,
|
||||||
},
|
fontWeight: '600',
|
||||||
tabWrapper: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
tabWrapper: {
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomColor: `${colors.divider}50`,
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: `${colors.divider}50`,
|
||||||
suggestionsContainer: {
|
},
|
||||||
flex: 1,
|
suggestionsContainer: {
|
||||||
},
|
flex: 1,
|
||||||
section: {
|
},
|
||||||
marginTop: spacing.md,
|
section: {
|
||||||
},
|
marginTop: spacing.md,
|
||||||
sectionHeader: {
|
},
|
||||||
flexDirection: 'row',
|
sectionHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: spacing.sm,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
sectionTitle: {
|
},
|
||||||
fontWeight: '600',
|
sectionTitle: {
|
||||||
color: colors.text.primary,
|
fontWeight: '600',
|
||||||
},
|
color: colors.text.primary,
|
||||||
tagContainer: {
|
},
|
||||||
flexDirection: 'row',
|
tagContainer: {
|
||||||
flexWrap: 'wrap',
|
flexDirection: 'row',
|
||||||
},
|
flexWrap: 'wrap',
|
||||||
tag: {
|
},
|
||||||
flexDirection: 'row',
|
tag: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background.paper,
|
alignItems: 'center',
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
borderWidth: 1,
|
borderRadius: borderRadius.lg,
|
||||||
borderColor: colors.divider,
|
borderWidth: 1,
|
||||||
},
|
borderColor: colors.divider,
|
||||||
tagText: {
|
},
|
||||||
marginLeft: spacing.xs,
|
tagText: {
|
||||||
},
|
marginLeft: spacing.xs,
|
||||||
userCard: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
userCard: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
flexDirection: 'row',
|
borderRadius: borderRadius.lg,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
userCardInfo: {
|
},
|
||||||
flex: 1,
|
userCardInfo: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
userCardName: {
|
},
|
||||||
fontWeight: '600',
|
userCardName: {
|
||||||
color: colors.text.primary,
|
fontWeight: '600',
|
||||||
},
|
color: colors.text.primary,
|
||||||
userItem: {
|
},
|
||||||
flexDirection: 'row',
|
userItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background.paper,
|
alignItems: 'center',
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
borderRadius: borderRadius.lg,
|
||||||
userInfo: {
|
},
|
||||||
flex: 1,
|
userInfo: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
userName: {
|
},
|
||||||
fontWeight: '600',
|
userName: {
|
||||||
color: colors.text.primary,
|
fontWeight: '600',
|
||||||
},
|
color: colors.text.primary,
|
||||||
followingBadge: {
|
},
|
||||||
width: 20,
|
followingBadge: {
|
||||||
height: 20,
|
width: 20,
|
||||||
borderRadius: 10,
|
height: 20,
|
||||||
backgroundColor: `${colors.primary.main}14`,
|
borderRadius: 10,
|
||||||
alignItems: 'center',
|
backgroundColor: `${colors.primary.main}14`,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
loadingMore: {
|
},
|
||||||
paddingVertical: spacing.md,
|
loadingMore: {
|
||||||
alignItems: 'center',
|
paddingVertical: spacing.md,
|
||||||
},
|
alignItems: 'center',
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,18 +26,20 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRouter } from 'expo-router';
|
import { useNavigation, useRouter } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||||
import { colors } from '../../theme';
|
import { useAppColors } from '../../theme';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { messageManager } from '../../stores';
|
import { messageManager } from '../../stores';
|
||||||
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
||||||
import {
|
import {
|
||||||
useChatScreen,
|
useChatScreen,
|
||||||
chatScreenStyles as baseStyles,
|
useChatScreenStyles,
|
||||||
EmojiPanel,
|
EmojiPanel,
|
||||||
MorePanel,
|
MorePanel,
|
||||||
MentionPanel,
|
MentionPanel,
|
||||||
@@ -53,10 +55,11 @@ export const ChatScreen: React.FC = () => {
|
|||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useChatScreenStyles();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
const isWideScreen = useBreakpointGTE('lg');
|
const isWideScreen = useBreakpointGTE('lg');
|
||||||
const styles = baseStyles;
|
|
||||||
|
|
||||||
// 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页
|
// 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,6 +82,8 @@ export const ChatScreen: React.FC = () => {
|
|||||||
beforeHeight: 0,
|
beforeHeight: 0,
|
||||||
});
|
});
|
||||||
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const showJumpToLatestRef = useRef(false);
|
||||||
|
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
|
||||||
|
|
||||||
const containerStyle = useMemo(() => ([
|
const containerStyle = useMemo(() => ([
|
||||||
styles.container,
|
styles.container,
|
||||||
@@ -200,6 +205,7 @@ export const ChatScreen: React.FC = () => {
|
|||||||
loadMoreHistory,
|
loadMoreHistory,
|
||||||
handleMessageListContentSizeChange,
|
handleMessageListContentSizeChange,
|
||||||
handleReachLatestEdge,
|
handleReachLatestEdge,
|
||||||
|
jumpToLatestMessages,
|
||||||
setBrowsingHistory,
|
setBrowsingHistory,
|
||||||
} = useChatScreen();
|
} = useChatScreen();
|
||||||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||||
@@ -210,6 +216,18 @@ export const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [loadingMore, showEdgeLoadingIndicator]);
|
}, [loadingMore, showEdgeLoadingIndicator]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
showJumpToLatestRef.current = false;
|
||||||
|
setShowJumpToLatest(false);
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showJumpToLatestRef.current = false;
|
||||||
|
setShowJumpToLatest(false);
|
||||||
|
}, [conversationId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (preloadCooldownTimerRef.current) {
|
if (preloadCooldownTimerRef.current) {
|
||||||
@@ -314,6 +332,14 @@ export const ChatScreen: React.FC = () => {
|
|||||||
viewportHeight: layoutMeasurement.height,
|
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) {
|
if (contentOffset.y > 120) {
|
||||||
setBrowsingHistory(true);
|
setBrowsingHistory(true);
|
||||||
@@ -360,7 +386,17 @@ export const ChatScreen: React.FC = () => {
|
|||||||
}, 350);
|
}, 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) => {
|
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||||||
handleMessageListContentSizeChange(contentWidth, contentHeight);
|
handleMessageListContentSizeChange(contentWidth, contentHeight);
|
||||||
@@ -474,6 +510,18 @@ export const ChatScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{showJumpToLatest && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.jumpToLatestFab}
|
||||||
|
onPress={jumpToLatestMessages}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="回到最新消息"
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="chevron-down" size={20} color={colors.primary.main} />
|
||||||
|
<Text style={styles.jumpToLatestFabText}>最新消息</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 底部区域:输入框 + 面板 */}
|
{/* 底部区域:输入框 + 面板 */}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 支持响应式布局
|
* 支持响应式布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -19,7 +19,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
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 { groupService } from '../../services/groupService';
|
||||||
import { uploadService } from '../../services/uploadService';
|
import { uploadService } from '../../services/uploadService';
|
||||||
import { Avatar, Text, Button, Loading } from '../../components/common';
|
import { Avatar, Text, Button, Loading } from '../../components/common';
|
||||||
@@ -27,6 +27,8 @@ import { User } from '../../types';
|
|||||||
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
|
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
|
||||||
|
|
||||||
const CreateGroupScreen: React.FC = () => {
|
const CreateGroupScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createCreateGroupStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 表单状态
|
// 表单状态
|
||||||
@@ -310,171 +312,173 @@ const CreateGroupScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createCreateGroupStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
scrollView: {
|
},
|
||||||
flex: 1,
|
scrollView: {
|
||||||
},
|
flex: 1,
|
||||||
scrollContent: {
|
},
|
||||||
padding: spacing.lg,
|
scrollContent: {
|
||||||
paddingBottom: spacing.xl,
|
padding: spacing.lg,
|
||||||
},
|
paddingBottom: spacing.xl,
|
||||||
// 头部区域样式
|
},
|
||||||
headerSection: {
|
// 头部区域样式
|
||||||
flexDirection: 'row',
|
headerSection: {
|
||||||
alignItems: 'flex-start',
|
flexDirection: 'row',
|
||||||
marginBottom: spacing.xl,
|
alignItems: 'flex-start',
|
||||||
},
|
marginBottom: spacing.xl,
|
||||||
avatarContainer: {
|
},
|
||||||
marginRight: spacing.lg,
|
avatarContainer: {
|
||||||
},
|
marginRight: spacing.lg,
|
||||||
avatarWrapper: {
|
},
|
||||||
position: 'relative',
|
avatarWrapper: {
|
||||||
},
|
position: 'relative',
|
||||||
avatarImage: {
|
},
|
||||||
width: 80,
|
avatarImage: {
|
||||||
height: 80,
|
width: 80,
|
||||||
borderRadius: 40,
|
height: 80,
|
||||||
},
|
borderRadius: 40,
|
||||||
avatarPlaceholder: {
|
},
|
||||||
justifyContent: 'center',
|
avatarPlaceholder: {
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: colors.background.default,
|
alignItems: 'center',
|
||||||
borderRadius: 40,
|
backgroundColor: colors.background.default,
|
||||||
},
|
borderRadius: 40,
|
||||||
avatarBadge: {
|
},
|
||||||
position: 'absolute',
|
avatarBadge: {
|
||||||
bottom: 0,
|
position: 'absolute',
|
||||||
right: 0,
|
bottom: 0,
|
||||||
backgroundColor: colors.primary.main,
|
right: 0,
|
||||||
width: 28,
|
backgroundColor: colors.primary.main,
|
||||||
height: 28,
|
width: 28,
|
||||||
borderRadius: 14,
|
height: 28,
|
||||||
justifyContent: 'center',
|
borderRadius: 14,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
borderWidth: 2,
|
alignItems: 'center',
|
||||||
borderColor: colors.background.paper,
|
borderWidth: 2,
|
||||||
},
|
borderColor: colors.background.paper,
|
||||||
nameInputContainer: {
|
},
|
||||||
flex: 1,
|
nameInputContainer: {
|
||||||
paddingTop: spacing.sm,
|
flex: 1,
|
||||||
},
|
paddingTop: spacing.sm,
|
||||||
inputLabel: {
|
},
|
||||||
marginBottom: spacing.sm,
|
inputLabel: {
|
||||||
fontWeight: '600',
|
marginBottom: spacing.sm,
|
||||||
},
|
fontWeight: '600',
|
||||||
nameInput: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
nameInput: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
paddingHorizontal: spacing.md,
|
borderRadius: borderRadius.lg,
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
fontSize: fontSizes.lg,
|
paddingVertical: spacing.md,
|
||||||
color: colors.text.primary,
|
fontSize: fontSizes.lg,
|
||||||
borderWidth: 1,
|
color: colors.text.primary,
|
||||||
borderColor: colors.divider,
|
borderWidth: 1,
|
||||||
},
|
borderColor: colors.divider,
|
||||||
charCount: {
|
},
|
||||||
textAlign: 'right',
|
charCount: {
|
||||||
marginTop: spacing.xs,
|
textAlign: 'right',
|
||||||
},
|
marginTop: spacing.xs,
|
||||||
// 区域样式
|
},
|
||||||
section: {
|
// 区域样式
|
||||||
marginBottom: spacing.xl,
|
section: {
|
||||||
},
|
marginBottom: spacing.xl,
|
||||||
sectionHeader: {
|
},
|
||||||
flexDirection: 'row',
|
sectionHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: spacing.sm,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
sectionTitle: {
|
},
|
||||||
fontWeight: '600',
|
sectionTitle: {
|
||||||
marginBottom: spacing.sm,
|
fontWeight: '600',
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
// 文本域样式
|
},
|
||||||
textAreaContainer: {
|
// 文本域样式
|
||||||
backgroundColor: colors.background.paper,
|
textAreaContainer: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
borderWidth: 1,
|
borderRadius: borderRadius.lg,
|
||||||
borderColor: colors.divider,
|
borderWidth: 1,
|
||||||
padding: spacing.md,
|
borderColor: colors.divider,
|
||||||
},
|
padding: spacing.md,
|
||||||
textArea: {
|
},
|
||||||
minHeight: 100,
|
textArea: {
|
||||||
fontSize: fontSizes.md,
|
minHeight: 100,
|
||||||
color: colors.text.primary,
|
fontSize: fontSizes.md,
|
||||||
lineHeight: 22,
|
color: colors.text.primary,
|
||||||
},
|
lineHeight: 22,
|
||||||
textAreaCharCount: {
|
},
|
||||||
textAlign: 'right',
|
textAreaCharCount: {
|
||||||
marginTop: spacing.sm,
|
textAlign: 'right',
|
||||||
},
|
marginTop: spacing.sm,
|
||||||
// 已选成员样式
|
},
|
||||||
selectedMembersList: {
|
// 已选成员样式
|
||||||
paddingVertical: spacing.sm,
|
selectedMembersList: {
|
||||||
},
|
paddingVertical: spacing.sm,
|
||||||
selectedMemberItem: {
|
},
|
||||||
alignItems: 'center',
|
selectedMemberItem: {
|
||||||
marginRight: spacing.lg,
|
alignItems: 'center',
|
||||||
width: 64,
|
marginRight: spacing.lg,
|
||||||
},
|
width: 64,
|
||||||
removeMemberButton: {
|
},
|
||||||
position: 'absolute',
|
removeMemberButton: {
|
||||||
top: -4,
|
position: 'absolute',
|
||||||
right: 4,
|
top: -4,
|
||||||
},
|
right: 4,
|
||||||
removeIconContainer: {
|
},
|
||||||
backgroundColor: colors.text.secondary,
|
removeIconContainer: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.text.secondary,
|
||||||
width: 20,
|
borderRadius: 10,
|
||||||
height: 20,
|
width: 20,
|
||||||
justifyContent: 'center',
|
height: 20,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
borderWidth: 2,
|
alignItems: 'center',
|
||||||
borderColor: colors.background.paper,
|
borderWidth: 2,
|
||||||
},
|
borderColor: colors.background.paper,
|
||||||
selectedMemberName: {
|
},
|
||||||
marginTop: spacing.xs,
|
selectedMemberName: {
|
||||||
textAlign: 'center',
|
marginTop: spacing.xs,
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
},
|
width: '100%',
|
||||||
// 邀请按钮样式
|
},
|
||||||
inviteButton: {
|
// 邀请按钮样式
|
||||||
flexDirection: 'row',
|
inviteButton: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background.paper,
|
alignItems: 'center',
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
padding: spacing.md,
|
borderRadius: borderRadius.lg,
|
||||||
marginBottom: spacing.xl,
|
padding: spacing.md,
|
||||||
...shadows.sm,
|
marginBottom: spacing.xl,
|
||||||
},
|
...shadows.sm,
|
||||||
inviteIconContainer: {
|
},
|
||||||
width: 48,
|
inviteIconContainer: {
|
||||||
height: 48,
|
width: 48,
|
||||||
borderRadius: borderRadius.lg,
|
height: 48,
|
||||||
backgroundColor: colors.primary.light + '20',
|
borderRadius: borderRadius.lg,
|
||||||
justifyContent: 'center',
|
backgroundColor: colors.primary.light + '20',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginRight: spacing.md,
|
||||||
inviteTextContainer: {
|
},
|
||||||
flex: 1,
|
inviteTextContainer: {
|
||||||
},
|
flex: 1,
|
||||||
inviteTitle: {
|
},
|
||||||
fontWeight: '600',
|
inviteTitle: {
|
||||||
marginBottom: 2,
|
fontWeight: '600',
|
||||||
},
|
marginBottom: 2,
|
||||||
// 底部按钮样式
|
},
|
||||||
footer: {
|
// 底部按钮样式
|
||||||
padding: spacing.lg,
|
footer: {
|
||||||
paddingBottom: spacing.xl,
|
padding: spacing.lg,
|
||||||
backgroundColor: colors.background.paper,
|
paddingBottom: spacing.xl,
|
||||||
borderTopWidth: 1,
|
backgroundColor: colors.background.paper,
|
||||||
borderTopColor: colors.divider,
|
borderTopWidth: 1,
|
||||||
},
|
borderTopColor: colors.divider,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default CreateGroupScreen;
|
export default CreateGroupScreen;
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ import { useFocusEffect } from '@react-navigation/native';
|
|||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
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 { useAuthStore } from '../../stores';
|
||||||
import { groupService } from '../../services/groupService';
|
import { groupService } from '../../services/groupService';
|
||||||
import { uploadService } from '../../services/uploadService';
|
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 GroupInfoScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupInfoStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
|
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
|
||||||
groupId?: string | string[];
|
groupId?: string | string[];
|
||||||
@@ -1071,7 +1080,8 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createGroupInfoStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
@@ -1594,6 +1604,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: colors.primary.main,
|
backgroundColor: colors.primary.main,
|
||||||
borderColor: colors.primary.main,
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default GroupInfoScreen;
|
export default GroupInfoScreen;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { View, StyleSheet, ActivityIndicator, Alert, ScrollView, TouchableOpacit
|
|||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { Avatar, Text } from '../../components/common';
|
||||||
import { routePayloadCache } from '../../stores/routePayloadCache';
|
import { routePayloadCache } from '../../stores/routePayloadCache';
|
||||||
import { groupService } from '../../services/groupService';
|
import { groupService } from '../../services/groupService';
|
||||||
@@ -12,6 +12,8 @@ import { GroupMemberResponse } from '../../types/dto';
|
|||||||
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
||||||
|
|
||||||
const GroupInviteDetailScreen: React.FC = () => {
|
const GroupInviteDetailScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupInviteDetailStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
||||||
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
||||||
@@ -174,84 +176,86 @@ const GroupInviteDetailScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createGroupInviteDetailStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
emptyWrap: {
|
},
|
||||||
flex: 1,
|
emptyWrap: {
|
||||||
padding: spacing.lg,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
padding: spacing.lg,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
gap: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
gap: spacing.md,
|
||||||
backBtn: {
|
},
|
||||||
padding: spacing.sm,
|
backBtn: {
|
||||||
},
|
padding: spacing.sm,
|
||||||
scrollView: {
|
},
|
||||||
flex: 1,
|
scrollView: {
|
||||||
},
|
flex: 1,
|
||||||
scrollContent: {
|
},
|
||||||
padding: spacing.lg,
|
scrollContent: {
|
||||||
paddingBottom: spacing.xl,
|
padding: spacing.lg,
|
||||||
},
|
paddingBottom: spacing.xl,
|
||||||
card: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
card: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
padding: spacing.lg,
|
borderRadius: borderRadius.lg,
|
||||||
...shadows.sm,
|
padding: spacing.lg,
|
||||||
},
|
...shadows.sm,
|
||||||
cardHeader: {
|
},
|
||||||
flexDirection: 'row',
|
cardHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginBottom: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
cardIconContainer: {
|
},
|
||||||
width: 30,
|
cardIconContainer: {
|
||||||
height: 30,
|
width: 30,
|
||||||
borderRadius: 15,
|
height: 30,
|
||||||
backgroundColor: colors.info.light + '30',
|
borderRadius: 15,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.info.light + '30',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginRight: spacing.sm,
|
justifyContent: 'center',
|
||||||
},
|
marginRight: spacing.sm,
|
||||||
cardTitle: {
|
},
|
||||||
fontWeight: '600',
|
cardTitle: {
|
||||||
flex: 1,
|
fontWeight: '600',
|
||||||
},
|
flex: 1,
|
||||||
memberCount: {
|
},
|
||||||
marginRight: spacing.xs,
|
memberCount: {
|
||||||
},
|
marginRight: spacing.xs,
|
||||||
loadingWrap: {
|
},
|
||||||
paddingVertical: spacing.md,
|
loadingWrap: {
|
||||||
alignItems: 'center',
|
paddingVertical: spacing.md,
|
||||||
},
|
alignItems: 'center',
|
||||||
memberPreview: {
|
},
|
||||||
flexDirection: 'row',
|
memberPreview: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
alignItems: 'center',
|
||||||
},
|
flexWrap: 'wrap',
|
||||||
memberAvatar: {
|
},
|
||||||
marginLeft: -8,
|
memberAvatar: {
|
||||||
marginBottom: spacing.sm,
|
marginLeft: -8,
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
memberAvatarFirst: {
|
},
|
||||||
marginLeft: 0,
|
memberAvatarFirst: {
|
||||||
},
|
marginLeft: 0,
|
||||||
ownerBadge: {
|
},
|
||||||
position: 'absolute',
|
ownerBadge: {
|
||||||
bottom: -2,
|
position: 'absolute',
|
||||||
right: -2,
|
bottom: -2,
|
||||||
backgroundColor: colors.warning.main,
|
right: -2,
|
||||||
borderRadius: 8,
|
backgroundColor: colors.warning.main,
|
||||||
paddingHorizontal: 4,
|
borderRadius: 8,
|
||||||
paddingVertical: 1,
|
paddingHorizontal: 4,
|
||||||
},
|
paddingVertical: 1,
|
||||||
ownerBadgeText: {
|
},
|
||||||
fontSize: 10,
|
ownerBadgeText: {
|
||||||
fontWeight: '700',
|
fontSize: 10,
|
||||||
},
|
fontWeight: '700',
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default GroupInviteDetailScreen;
|
export default GroupInviteDetailScreen;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
import { useLocalSearchParams } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { useAuthStore } from '../../stores';
|
||||||
import { groupService } from '../../services/groupService';
|
import { groupService } from '../../services/groupService';
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +55,8 @@ interface MemberGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GroupMembersScreen: React.FC = () => {
|
const GroupMembersScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupMembersStyles(colors), [colors]);
|
||||||
const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>();
|
const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>();
|
||||||
const groupId = firstRouteParam(groupIdParam) ?? '';
|
const groupId = firstRouteParam(groupIdParam) ?? '';
|
||||||
const { currentUser } = useAuthStore();
|
const { currentUser } = useAuthStore();
|
||||||
@@ -646,122 +648,124 @@ const GroupMembersScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createGroupMembersStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
section: {
|
},
|
||||||
marginBottom: spacing.md,
|
section: {
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
sectionHeader: {
|
},
|
||||||
flexDirection: 'row',
|
sectionHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: colors.background.default,
|
paddingVertical: spacing.sm,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
memberItem: {
|
},
|
||||||
flexDirection: 'row',
|
memberItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.md,
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
paddingVertical: spacing.md,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.divider,
|
||||||
memberInfo: {
|
},
|
||||||
flex: 1,
|
memberInfo: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
memberNameRow: {
|
},
|
||||||
flexDirection: 'row',
|
memberNameRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginBottom: 2,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: 2,
|
||||||
memberName: {
|
},
|
||||||
fontWeight: '500',
|
memberName: {
|
||||||
},
|
fontWeight: '500',
|
||||||
roleBadge: {
|
},
|
||||||
paddingHorizontal: spacing.xs,
|
roleBadge: {
|
||||||
paddingVertical: 2,
|
paddingHorizontal: spacing.xs,
|
||||||
borderRadius: borderRadius.sm,
|
paddingVertical: 2,
|
||||||
marginLeft: spacing.sm,
|
borderRadius: borderRadius.sm,
|
||||||
},
|
marginLeft: spacing.sm,
|
||||||
mutedBadge: {
|
},
|
||||||
flexDirection: 'row',
|
mutedBadge: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginTop: 2,
|
alignItems: 'center',
|
||||||
},
|
marginTop: 2,
|
||||||
// 模态框样式
|
},
|
||||||
modalOverlay: {
|
// 模态框样式
|
||||||
flex: 1,
|
modalOverlay: {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
flex: 1,
|
||||||
justifyContent: 'flex-end',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
},
|
justifyContent: 'flex-end',
|
||||||
modalContent: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
modalContent: {
|
||||||
borderTopLeftRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
borderTopRightRadius: borderRadius.lg,
|
borderTopLeftRadius: borderRadius.lg,
|
||||||
padding: spacing.lg,
|
borderTopRightRadius: borderRadius.lg,
|
||||||
maxHeight: '80%',
|
padding: spacing.lg,
|
||||||
},
|
maxHeight: '80%',
|
||||||
modalHeader: {
|
},
|
||||||
alignItems: 'center',
|
modalHeader: {
|
||||||
marginBottom: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
modalTitle: {
|
},
|
||||||
fontWeight: '700',
|
modalTitle: {
|
||||||
marginTop: spacing.sm,
|
fontWeight: '700',
|
||||||
marginBottom: spacing.xs,
|
marginTop: spacing.sm,
|
||||||
},
|
marginBottom: spacing.xs,
|
||||||
actionItem: {
|
},
|
||||||
flexDirection: 'row',
|
actionItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.md,
|
alignItems: 'center',
|
||||||
borderBottomWidth: 1,
|
paddingVertical: spacing.md,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.divider,
|
||||||
actionText: {
|
},
|
||||||
marginLeft: spacing.md,
|
actionText: {
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
inputLabel: {
|
},
|
||||||
marginBottom: spacing.xs,
|
inputLabel: {
|
||||||
},
|
marginBottom: spacing.xs,
|
||||||
input: {
|
},
|
||||||
backgroundColor: colors.background.default,
|
input: {
|
||||||
borderRadius: borderRadius.md,
|
backgroundColor: colors.background.default,
|
||||||
paddingHorizontal: spacing.md,
|
borderRadius: borderRadius.md,
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
fontSize: fontSizes.md,
|
paddingVertical: spacing.md,
|
||||||
color: colors.text.primary,
|
fontSize: fontSizes.md,
|
||||||
borderWidth: 1,
|
color: colors.text.primary,
|
||||||
borderColor: colors.divider,
|
borderWidth: 1,
|
||||||
marginBottom: spacing.md,
|
borderColor: colors.divider,
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
modalButtons: {
|
},
|
||||||
flexDirection: 'row',
|
modalButtons: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
marginTop: spacing.md,
|
justifyContent: 'space-between',
|
||||||
},
|
marginTop: spacing.md,
|
||||||
modalButton: {
|
},
|
||||||
flex: 1,
|
modalButton: {
|
||||||
marginHorizontal: spacing.xs,
|
flex: 1,
|
||||||
},
|
marginHorizontal: spacing.xs,
|
||||||
// 分页加载样式
|
},
|
||||||
loadingFooter: {
|
// 分页加载样式
|
||||||
paddingVertical: spacing.md,
|
loadingFooter: {
|
||||||
alignItems: 'center',
|
paddingVertical: spacing.md,
|
||||||
},
|
alignItems: 'center',
|
||||||
loadMoreBtn: {
|
},
|
||||||
paddingVertical: spacing.md,
|
loadMoreBtn: {
|
||||||
alignItems: 'center',
|
paddingVertical: spacing.md,
|
||||||
},
|
alignItems: 'center',
|
||||||
noMoreText: {
|
},
|
||||||
textAlign: 'center',
|
noMoreText: {
|
||||||
paddingVertical: spacing.md,
|
textAlign: 'center',
|
||||||
},
|
paddingVertical: spacing.md,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default GroupMembersScreen;
|
export default GroupMembersScreen;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
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 { Avatar, Text } from '../../components/common';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { routePayloadCache } from '../../stores/routePayloadCache';
|
import { routePayloadCache } from '../../stores/routePayloadCache';
|
||||||
@@ -14,6 +14,8 @@ import { userManager } from '../../stores/userManager';
|
|||||||
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
||||||
|
|
||||||
const GroupRequestDetailScreen: React.FC = () => {
|
const GroupRequestDetailScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupRequestDetailStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
||||||
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
||||||
@@ -174,52 +176,54 @@ const GroupRequestDetailScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createGroupRequestDetailStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
emptyWrap: {
|
},
|
||||||
flex: 1,
|
emptyWrap: {
|
||||||
padding: spacing.lg,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
padding: spacing.lg,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
gap: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
gap: spacing.md,
|
||||||
backBtn: {
|
},
|
||||||
padding: spacing.sm,
|
backBtn: {
|
||||||
},
|
padding: spacing.sm,
|
||||||
scrollView: {
|
},
|
||||||
flex: 1,
|
scrollView: {
|
||||||
},
|
flex: 1,
|
||||||
scrollContent: {
|
},
|
||||||
padding: spacing.lg,
|
scrollContent: {
|
||||||
paddingBottom: spacing.xl,
|
padding: spacing.lg,
|
||||||
},
|
paddingBottom: spacing.xl,
|
||||||
card: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
card: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
padding: spacing.lg,
|
borderRadius: borderRadius.lg,
|
||||||
marginBottom: spacing.md,
|
padding: spacing.lg,
|
||||||
...shadows.sm,
|
marginBottom: spacing.md,
|
||||||
},
|
...shadows.sm,
|
||||||
sectionTitle: {
|
},
|
||||||
marginBottom: spacing.sm,
|
sectionTitle: {
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
row: {
|
},
|
||||||
flexDirection: 'row',
|
row: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
meta: {
|
},
|
||||||
marginLeft: spacing.md,
|
meta: {
|
||||||
flex: 1,
|
marginLeft: spacing.md,
|
||||||
},
|
flex: 1,
|
||||||
name: {
|
},
|
||||||
marginBottom: 2,
|
name: {
|
||||||
},
|
marginBottom: 2,
|
||||||
subDesc: {
|
},
|
||||||
marginTop: spacing.sm,
|
subDesc: {
|
||||||
},
|
marginTop: spacing.sm,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default GroupRequestDetailScreen;
|
export default GroupRequestDetailScreen;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 Avatar from '../../components/common/Avatar';
|
||||||
import Text from '../../components/common/Text';
|
import Text from '../../components/common/Text';
|
||||||
import { groupService } from '../../services/groupService';
|
import { groupService } from '../../services/groupService';
|
||||||
@@ -22,7 +22,160 @@ import { GroupResponse, JoinType } from '../../types/dto';
|
|||||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||||
import { EmptyState } from '../../components/common';
|
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 JoinGroupScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createJoinGroupStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [searching, setSearching] = useState(false);
|
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;
|
export default JoinGroupScreen;
|
||||||
|
|||||||
@@ -30,7 +30,14 @@ import { useRouter } from 'expo-router';
|
|||||||
import { useIsFocused } from '@react-navigation/native';
|
import { useIsFocused } from '@react-navigation/native';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto';
|
||||||
import { authService } from '../../services';
|
import { authService } from '../../services';
|
||||||
import { useUserStore, useAuthStore } from '../../stores';
|
import { useUserStore, useAuthStore } from '../../stores';
|
||||||
@@ -73,6 +80,8 @@ interface SearchResultItem {
|
|||||||
* 支持响应式双栏布局
|
* 支持响应式双栏布局
|
||||||
*/
|
*/
|
||||||
export const MessageListScreen: React.FC = () => {
|
export const MessageListScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createMessageListStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -565,7 +574,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -614,7 +623,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{!user.is_following && (
|
{!user.is_following && (
|
||||||
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
@@ -661,11 +670,11 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
const renderSearchMode = () => (
|
const renderSearchMode = () => (
|
||||||
<View style={styles.searchModeContainer}>
|
<View style={styles.searchModeContainer}>
|
||||||
<View style={styles.searchInputContainer}>
|
<View style={styles.searchInputContainer}>
|
||||||
<AppBackButton onPress={handleCloseSearch} iconColor="#666" />
|
<AppBackButton onPress={handleCloseSearch} iconColor={colors.text.secondary} />
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.searchInput}
|
style={styles.searchInput}
|
||||||
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
|
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={colors.text.hint}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChangeText={setSearchText}
|
onChangeText={setSearchText}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -676,7 +685,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
{searchText.length > 0 && (
|
{searchText.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setSearchText('')}>
|
<TouchableOpacity onPress={() => setSearchText('')}>
|
||||||
<MaterialCommunityIcons name="close-circle" size={18} color="#999" />
|
<MaterialCommunityIcons name="close-circle" size={18} color={colors.text.hint} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -733,7 +742,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
|
|
||||||
<View style={styles.headerRightContainer}>
|
<View style={styles.headerRightContainer}>
|
||||||
<TouchableOpacity style={styles.headerRightBtn} onPress={handleOpenActionMenu}>
|
<TouchableOpacity style={styles.headerRightBtn} onPress={handleOpenActionMenu}>
|
||||||
<MaterialCommunityIcons name="plus" size={24} color="#666" />
|
<MaterialCommunityIcons name="plus" size={24} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -744,7 +753,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.searchBox}>
|
<View style={styles.searchBox}>
|
||||||
<MaterialCommunityIcons name="magnify" size={18} color="#999" />
|
<MaterialCommunityIcons name="magnify" size={18} color={colors.text.hint} />
|
||||||
<Text style={styles.searchPlaceholder}>搜索</Text>
|
<Text style={styles.searchPlaceholder}>搜索</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -793,19 +802,19 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
<Pressable style={styles.actionMenuBackdrop} onPress={() => setActionMenuVisible(false)}>
|
<Pressable style={styles.actionMenuBackdrop} onPress={() => setActionMenuVisible(false)}>
|
||||||
<View style={styles.actionMenu}>
|
<View style={styles.actionMenu}>
|
||||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('scanQRCode')}>
|
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('scanQRCode')}>
|
||||||
<MaterialCommunityIcons name="qrcode-scan" size={21} color="#444" />
|
<MaterialCommunityIcons name="qrcode-scan" size={21} color={colors.text.primary} />
|
||||||
<Text style={styles.actionMenuText}>扫码登录</Text>
|
<Text style={styles.actionMenuText}>扫码登录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('join')}>
|
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('join')}>
|
||||||
<MaterialCommunityIcons name="account-plus-outline" size={21} color="#444" />
|
<MaterialCommunityIcons name="account-plus-outline" size={21} color={colors.text.primary} />
|
||||||
<Text style={styles.actionMenuText}>加入群聊</Text>
|
<Text style={styles.actionMenuText}>加入群聊</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('create')}>
|
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('create')}>
|
||||||
<MaterialCommunityIcons name="account-multiple-plus" size={21} color="#444" />
|
<MaterialCommunityIcons name="account-multiple-plus" size={21} color={colors.text.primary} />
|
||||||
<Text style={styles.actionMenuText}>创建群聊</Text>
|
<Text style={styles.actionMenuText}>创建群聊</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('readAll')} disabled={isMarking}>
|
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('readAll')} disabled={isMarking}>
|
||||||
<MaterialCommunityIcons name="message-check-outline" size={21} color="#444" />
|
<MaterialCommunityIcons name="message-check-outline" size={21} color={colors.text.primary} />
|
||||||
<Text style={styles.actionMenuText}>{isMarking ? '处理中...' : '全部已读'}</Text>
|
<Text style={styles.actionMenuText}>{isMarking ? '处理中...' : '全部已读'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -838,7 +847,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
// 默认占位符
|
// 默认占位符
|
||||||
<View style={styles.chatPlaceholder}>
|
<View style={styles.chatPlaceholder}>
|
||||||
<View style={styles.chatPlaceholderContent}>
|
<View style={styles.chatPlaceholderContent}>
|
||||||
<MaterialCommunityIcons name="message-text-outline" size={64} color="#DDD" />
|
<MaterialCommunityIcons name="message-text-outline" size={64} color={colors.chat.iconMuted} />
|
||||||
<Text style={styles.chatPlaceholderText}>选择一个会话开始聊天</Text>
|
<Text style={styles.chatPlaceholderText}>选择一个会话开始聊天</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -870,288 +879,290 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createMessageListStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: '#FAFAFA',
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
// 桌面端双栏布局
|
},
|
||||||
desktopContainer: {
|
// 桌面端双栏布局
|
||||||
flex: 1,
|
desktopContainer: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
},
|
flexDirection: 'row',
|
||||||
sidebar: {
|
},
|
||||||
backgroundColor: '#FAFAFA',
|
sidebar: {
|
||||||
borderRightWidth: 1,
|
backgroundColor: colors.background.default,
|
||||||
borderRightColor: '#E8E8E8',
|
borderRightWidth: 1,
|
||||||
},
|
borderRightColor: colors.chat.border,
|
||||||
chatArea: {
|
},
|
||||||
flex: 1,
|
chatArea: {
|
||||||
backgroundColor: '#F5F7FA',
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
chatPlaceholder: {
|
},
|
||||||
flex: 1,
|
chatPlaceholder: {
|
||||||
backgroundColor: '#F5F7FA',
|
flex: 1,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
chatPlaceholderContent: {
|
},
|
||||||
alignItems: 'center',
|
chatPlaceholderContent: {
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
chatPlaceholderText: {
|
},
|
||||||
marginTop: spacing.md,
|
chatPlaceholderText: {
|
||||||
fontSize: 16,
|
marginTop: spacing.md,
|
||||||
color: '#999',
|
fontSize: 16,
|
||||||
},
|
color: colors.text.hint,
|
||||||
header: {
|
},
|
||||||
flexDirection: 'row',
|
header: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: '#FAFAFA',
|
paddingVertical: spacing.md,
|
||||||
...shadows.sm,
|
backgroundColor: colors.background.default,
|
||||||
},
|
...shadows.sm,
|
||||||
headerWide: {
|
},
|
||||||
paddingHorizontal: spacing.lg,
|
headerWide: {
|
||||||
paddingVertical: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
paddingVertical: spacing.lg,
|
||||||
headerTitleWide: {
|
},
|
||||||
fontSize: 22,
|
headerTitleWide: {
|
||||||
},
|
fontSize: 22,
|
||||||
searchContainerWide: {
|
},
|
||||||
paddingHorizontal: spacing.lg,
|
searchContainerWide: {
|
||||||
},
|
paddingHorizontal: spacing.lg,
|
||||||
listContentWide: {
|
},
|
||||||
paddingHorizontal: spacing.lg,
|
listContentWide: {
|
||||||
},
|
paddingHorizontal: spacing.lg,
|
||||||
headerLeft: {
|
},
|
||||||
width: 44,
|
headerLeft: {
|
||||||
},
|
width: 44,
|
||||||
headerCenter: {
|
},
|
||||||
flexDirection: 'row',
|
headerCenter: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
gap: spacing.xs,
|
alignItems: 'center',
|
||||||
},
|
gap: spacing.xs,
|
||||||
headerTitle: {
|
},
|
||||||
fontSize: 19,
|
headerTitle: {
|
||||||
fontWeight: '700',
|
fontSize: 19,
|
||||||
color: '#333',
|
fontWeight: '700',
|
||||||
},
|
color: colors.text.primary,
|
||||||
totalBadge: {
|
},
|
||||||
minWidth: 18,
|
totalBadge: {
|
||||||
height: 18,
|
minWidth: 18,
|
||||||
borderRadius: 9,
|
height: 18,
|
||||||
backgroundColor: '#FF4757',
|
borderRadius: 9,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.error.main,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 5,
|
justifyContent: 'center',
|
||||||
},
|
paddingHorizontal: 5,
|
||||||
totalBadgeText: {
|
},
|
||||||
color: '#FFF',
|
totalBadgeText: {
|
||||||
fontSize: 11,
|
color: colors.primary.contrast,
|
||||||
fontWeight: '600',
|
fontSize: 11,
|
||||||
},
|
fontWeight: '600',
|
||||||
headerRight: {
|
},
|
||||||
width: 44,
|
headerRight: {
|
||||||
height: 44,
|
width: 44,
|
||||||
alignItems: 'center',
|
height: 44,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
headerRightContainer: {
|
},
|
||||||
flexDirection: 'row',
|
headerRightContainer: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
headerRightBtn: {
|
},
|
||||||
width: 36,
|
headerRightBtn: {
|
||||||
height: 44,
|
width: 36,
|
||||||
alignItems: 'center',
|
height: 44,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
actionMenuBackdrop: {
|
},
|
||||||
flex: 1,
|
actionMenuBackdrop: {
|
||||||
backgroundColor: 'rgba(0,0,0,0.08)',
|
flex: 1,
|
||||||
},
|
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||||
actionMenu: {
|
},
|
||||||
position: 'absolute',
|
actionMenu: {
|
||||||
top: 88,
|
position: 'absolute',
|
||||||
right: spacing.md,
|
top: 88,
|
||||||
width: 188,
|
right: spacing.md,
|
||||||
backgroundColor: '#FFF',
|
width: 188,
|
||||||
borderRadius: 12,
|
backgroundColor: colors.background.paper,
|
||||||
paddingVertical: spacing.sm,
|
borderRadius: 12,
|
||||||
...shadows.md,
|
paddingVertical: spacing.sm,
|
||||||
},
|
...shadows.md,
|
||||||
actionMenuItem: {
|
},
|
||||||
flexDirection: 'row',
|
actionMenuItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.md,
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
},
|
paddingVertical: spacing.md,
|
||||||
actionMenuText: {
|
},
|
||||||
marginLeft: spacing.sm,
|
actionMenuText: {
|
||||||
fontSize: 16,
|
marginLeft: spacing.sm,
|
||||||
color: '#333',
|
fontSize: 16,
|
||||||
},
|
color: colors.text.primary,
|
||||||
searchContainer: {
|
},
|
||||||
paddingHorizontal: spacing.md,
|
searchContainer: {
|
||||||
paddingBottom: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: '#FAFAFA',
|
paddingBottom: spacing.sm,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
searchBox: {
|
},
|
||||||
flexDirection: 'row',
|
searchBox: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: '#F0F0F0',
|
alignItems: 'center',
|
||||||
borderRadius: 10,
|
backgroundColor: colors.chat.surfaceInput,
|
||||||
paddingHorizontal: spacing.sm,
|
borderRadius: 10,
|
||||||
paddingVertical: 10,
|
paddingHorizontal: spacing.sm,
|
||||||
},
|
paddingVertical: 10,
|
||||||
searchPlaceholder: {
|
},
|
||||||
fontSize: 14,
|
searchPlaceholder: {
|
||||||
color: '#999',
|
fontSize: 14,
|
||||||
marginLeft: spacing.xs,
|
color: colors.text.hint,
|
||||||
},
|
marginLeft: spacing.xs,
|
||||||
listContent: {
|
},
|
||||||
flexGrow: 1,
|
listContent: {
|
||||||
backgroundColor: '#FAFAFA',
|
flexGrow: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
loadingContainer: {
|
},
|
||||||
flex: 1,
|
loadingContainer: {
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.xl * 2,
|
alignItems: 'center',
|
||||||
},
|
paddingVertical: spacing.xl * 2,
|
||||||
loadingMoreContainer: {
|
},
|
||||||
flexDirection: 'row',
|
loadingMoreContainer: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
justifyContent: 'center',
|
||||||
},
|
paddingVertical: spacing.md,
|
||||||
loadingMoreText: {
|
},
|
||||||
marginLeft: spacing.sm,
|
loadingMoreText: {
|
||||||
fontSize: 14,
|
marginLeft: spacing.sm,
|
||||||
color: '#999',
|
fontSize: 14,
|
||||||
},
|
color: colors.text.hint,
|
||||||
searchModeContainer: {
|
},
|
||||||
flex: 1,
|
searchModeContainer: {
|
||||||
backgroundColor: '#FAFAFA',
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
searchInputContainer: {
|
},
|
||||||
flexDirection: 'row',
|
searchInputContainer: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: '#F0F0F0',
|
alignItems: 'center',
|
||||||
borderRadius: 10,
|
backgroundColor: colors.chat.surfaceInput,
|
||||||
paddingHorizontal: spacing.md,
|
borderRadius: 10,
|
||||||
marginHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
marginTop: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
height: 40,
|
marginTop: spacing.md,
|
||||||
},
|
height: 40,
|
||||||
searchInput: {
|
},
|
||||||
flex: 1,
|
searchInput: {
|
||||||
fontSize: 15,
|
flex: 1,
|
||||||
color: '#333',
|
fontSize: 15,
|
||||||
marginLeft: spacing.md,
|
color: colors.text.primary,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
searchTabs: {
|
},
|
||||||
flexDirection: 'row',
|
searchTabs: {
|
||||||
paddingHorizontal: spacing.md,
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
gap: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
},
|
gap: spacing.sm,
|
||||||
searchTab: {
|
},
|
||||||
flex: 1,
|
searchTab: {
|
||||||
paddingVertical: spacing.sm,
|
flex: 1,
|
||||||
alignItems: 'center',
|
paddingVertical: spacing.sm,
|
||||||
backgroundColor: '#F0F0F0',
|
alignItems: 'center',
|
||||||
borderRadius: 8,
|
backgroundColor: colors.chat.surfaceInput,
|
||||||
},
|
borderRadius: 8,
|
||||||
searchTabActive: {
|
},
|
||||||
backgroundColor: colors.primary.main,
|
searchTabActive: {
|
||||||
},
|
backgroundColor: colors.primary.main,
|
||||||
searchTabText: {
|
},
|
||||||
fontSize: 14,
|
searchTabText: {
|
||||||
color: '#666',
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
color: colors.text.secondary,
|
||||||
},
|
fontWeight: '500',
|
||||||
searchTabTextActive: {
|
},
|
||||||
color: '#FFF',
|
searchTabTextActive: {
|
||||||
},
|
color: colors.primary.contrast,
|
||||||
searchResultsContent: {
|
},
|
||||||
flexGrow: 1,
|
searchResultsContent: {
|
||||||
paddingHorizontal: spacing.md,
|
flexGrow: 1,
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
searchResultItem: {
|
},
|
||||||
flexDirection: 'row',
|
searchResultItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.md,
|
alignItems: 'center',
|
||||||
backgroundColor: '#FFF',
|
paddingVertical: spacing.md,
|
||||||
borderRadius: 10,
|
backgroundColor: colors.background.paper,
|
||||||
marginBottom: spacing.sm,
|
borderRadius: 10,
|
||||||
paddingHorizontal: spacing.md,
|
marginBottom: spacing.sm,
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
searchResultContent: {
|
},
|
||||||
flex: 1,
|
searchResultContent: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
searchResultHeader: {
|
},
|
||||||
flexDirection: 'row',
|
searchResultHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 2,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: 2,
|
||||||
searchResultName: {
|
},
|
||||||
fontSize: 15,
|
searchResultName: {
|
||||||
fontWeight: '600',
|
fontSize: 15,
|
||||||
color: '#333',
|
fontWeight: '600',
|
||||||
},
|
color: colors.text.primary,
|
||||||
searchResultTime: {
|
},
|
||||||
fontSize: 12,
|
searchResultTime: {
|
||||||
color: '#999',
|
fontSize: 12,
|
||||||
},
|
color: colors.text.hint,
|
||||||
searchResultMessage: {
|
},
|
||||||
fontSize: 13,
|
searchResultMessage: {
|
||||||
color: '#888',
|
fontSize: 13,
|
||||||
},
|
color: colors.text.secondary,
|
||||||
searchingText: {
|
},
|
||||||
marginTop: spacing.sm,
|
searchingText: {
|
||||||
fontSize: 14,
|
marginTop: spacing.sm,
|
||||||
color: '#999',
|
fontSize: 14,
|
||||||
},
|
color: colors.text.hint,
|
||||||
followingBadge: {
|
},
|
||||||
width: 24,
|
followingBadge: {
|
||||||
height: 24,
|
width: 24,
|
||||||
borderRadius: 12,
|
height: 24,
|
||||||
backgroundColor: colors.primary.light + '30',
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.primary.light + '30',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
highlightText: {
|
},
|
||||||
color: colors.primary.main,
|
highlightText: {
|
||||||
fontWeight: '700',
|
color: colors.primary.main,
|
||||||
backgroundColor: colors.primary.light + '30',
|
fontWeight: '700',
|
||||||
},
|
backgroundColor: colors.primary.light + '30',
|
||||||
matchedMessagesContainer: {
|
},
|
||||||
marginTop: spacing.sm,
|
matchedMessagesContainer: {
|
||||||
},
|
marginTop: spacing.sm,
|
||||||
matchedMessageItem: {
|
},
|
||||||
flexDirection: 'row',
|
matchedMessageItem: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.xs,
|
alignItems: 'flex-start',
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
paddingVertical: spacing.xs,
|
||||||
borderBottomColor: '#F0F0F0',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.divider,
|
||||||
matchedMessageText: {
|
},
|
||||||
flex: 1,
|
matchedMessageText: {
|
||||||
fontSize: 13,
|
flex: 1,
|
||||||
color: '#555',
|
fontSize: 13,
|
||||||
lineHeight: 18,
|
color: colors.text.secondary,
|
||||||
},
|
lineHeight: 18,
|
||||||
matchedMessageTime: {
|
},
|
||||||
fontSize: 11,
|
matchedMessageTime: {
|
||||||
color: '#AAA',
|
fontSize: 11,
|
||||||
marginLeft: spacing.sm,
|
color: colors.text.hint,
|
||||||
minWidth: 45,
|
marginLeft: spacing.sm,
|
||||||
},
|
minWidth: 45,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import {
|
|||||||
Dimensions,
|
Dimensions,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
} from 'react-native';
|
} 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 { useIsFocused } from '@react-navigation/native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { SystemMessageResponse } from '../../types/dto';
|
||||||
import { messageService } from '../../services/messageService';
|
import { messageService } from '../../services/messageService';
|
||||||
import { commentService } from '../../services/commentService';
|
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']);
|
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 }) => {
|
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -554,143 +556,145 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createNotificationsStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
// Header 样式 - 美化版
|
},
|
||||||
header: {
|
// Header 样式 - 美化版
|
||||||
flexDirection: 'row',
|
header: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.lg,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
backgroundColor: colors.background.paper,
|
paddingVertical: spacing.md,
|
||||||
...shadows.sm,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
...shadows.sm,
|
||||||
headerWide: {
|
},
|
||||||
paddingHorizontal: spacing.xl,
|
headerWide: {
|
||||||
paddingVertical: spacing.lg,
|
paddingHorizontal: spacing.xl,
|
||||||
},
|
paddingVertical: spacing.lg,
|
||||||
headerTitleContainer: {
|
},
|
||||||
flexDirection: 'row',
|
headerTitleContainer: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
flex: 1,
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
},
|
justifyContent: 'center',
|
||||||
headerTitle: {
|
},
|
||||||
fontSize: 18,
|
headerTitle: {
|
||||||
fontWeight: '600',
|
fontSize: 18,
|
||||||
color: colors.text.primary,
|
fontWeight: '600',
|
||||||
},
|
color: colors.text.primary,
|
||||||
headerTitleWide: {
|
},
|
||||||
fontSize: 20,
|
headerTitleWide: {
|
||||||
},
|
fontSize: 20,
|
||||||
unreadBadge: {
|
},
|
||||||
backgroundColor: colors.primary.main,
|
unreadBadge: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.primary.main,
|
||||||
paddingHorizontal: 6,
|
borderRadius: 10,
|
||||||
paddingVertical: 2,
|
paddingHorizontal: 6,
|
||||||
marginLeft: spacing.sm,
|
paddingVertical: 2,
|
||||||
minWidth: 20,
|
marginLeft: spacing.sm,
|
||||||
alignItems: 'center',
|
minWidth: 20,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
unreadBadgeText: {
|
},
|
||||||
color: colors.text.inverse,
|
unreadBadgeText: {
|
||||||
fontSize: 11,
|
color: colors.text.inverse,
|
||||||
fontWeight: '600',
|
fontSize: 11,
|
||||||
},
|
fontWeight: '600',
|
||||||
backButton: {
|
},
|
||||||
width: 40,
|
backButton: {
|
||||||
height: 40,
|
width: 40,
|
||||||
justifyContent: 'center',
|
height: 40,
|
||||||
alignItems: 'flex-start',
|
justifyContent: 'center',
|
||||||
},
|
alignItems: 'flex-start',
|
||||||
backButtonPlaceholder: {
|
},
|
||||||
width: 40,
|
backButtonPlaceholder: {
|
||||||
},
|
width: 40,
|
||||||
// 分类筛选 - 美化版(使用分段式设计)
|
},
|
||||||
filterContainer: {
|
// 分类筛选 - 美化版(使用分段式设计)
|
||||||
flexDirection: 'row',
|
filterContainer: {
|
||||||
paddingHorizontal: spacing.md,
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
paddingVertical: spacing.sm,
|
||||||
},
|
backgroundColor: colors.background.paper,
|
||||||
filterContainerWideWeb: {
|
},
|
||||||
paddingHorizontal: spacing.xl,
|
filterContainerWideWeb: {
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.xl,
|
||||||
justifyContent: 'center',
|
paddingVertical: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
justifyContent: 'center',
|
||||||
},
|
backgroundColor: colors.background.paper,
|
||||||
filterTag: {
|
},
|
||||||
flexDirection: 'row',
|
filterTag: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.md,
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
marginRight: spacing.xs,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: borderRadius.lg,
|
marginRight: spacing.xs,
|
||||||
backgroundColor: colors.background.default,
|
borderRadius: borderRadius.lg,
|
||||||
borderWidth: 1,
|
backgroundColor: colors.background.default,
|
||||||
borderColor: 'transparent',
|
borderWidth: 1,
|
||||||
},
|
borderColor: 'transparent',
|
||||||
filterTagWide: {
|
},
|
||||||
paddingHorizontal: spacing.lg,
|
filterTagWide: {
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
marginRight: spacing.sm,
|
paddingVertical: spacing.md,
|
||||||
},
|
marginRight: spacing.sm,
|
||||||
filterTagActive: {
|
},
|
||||||
backgroundColor: colors.primary.main,
|
filterTagActive: {
|
||||||
borderColor: colors.primary.main,
|
backgroundColor: colors.primary.main,
|
||||||
},
|
borderColor: colors.primary.main,
|
||||||
filterTagTextActive: {
|
},
|
||||||
fontWeight: '600',
|
filterTagTextActive: {
|
||||||
},
|
fontWeight: '600',
|
||||||
filterCount: {
|
},
|
||||||
marginLeft: spacing.xs,
|
filterCount: {
|
||||||
backgroundColor: colors.primary.light + '40',
|
marginLeft: spacing.xs,
|
||||||
paddingHorizontal: 6,
|
backgroundColor: colors.primary.light + '40',
|
||||||
paddingVertical: 1,
|
paddingHorizontal: 6,
|
||||||
borderRadius: 8,
|
paddingVertical: 1,
|
||||||
minWidth: 18,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
minWidth: 18,
|
||||||
},
|
alignItems: 'center',
|
||||||
filterCountActive: {
|
},
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
filterCountActive: {
|
||||||
},
|
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||||
listContent: {
|
},
|
||||||
flexGrow: 1,
|
listContent: {
|
||||||
paddingTop: spacing.md,
|
flexGrow: 1,
|
||||||
paddingBottom: spacing.lg,
|
paddingTop: spacing.md,
|
||||||
},
|
paddingBottom: spacing.lg,
|
||||||
listContentWide: {
|
},
|
||||||
paddingHorizontal: spacing.xl,
|
listContentWide: {
|
||||||
},
|
paddingHorizontal: spacing.xl,
|
||||||
loadingContainer: {
|
},
|
||||||
flex: 1,
|
loadingContainer: {
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.xl * 2,
|
alignItems: 'center',
|
||||||
},
|
paddingVertical: spacing.xl * 2,
|
||||||
loadingMore: {
|
},
|
||||||
flexDirection: 'row',
|
loadingMore: {
|
||||||
justifyContent: 'center',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.lg,
|
alignItems: 'center',
|
||||||
},
|
paddingVertical: spacing.lg,
|
||||||
loadingMoreText: {
|
},
|
||||||
marginLeft: spacing.sm,
|
loadingMoreText: {
|
||||||
},
|
marginLeft: spacing.sm,
|
||||||
emptyContainer: {
|
},
|
||||||
flex: 1,
|
emptyContainer: {
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
paddingTop: spacing['3xl'],
|
alignItems: 'center',
|
||||||
},
|
paddingTop: spacing['3xl'],
|
||||||
emptyContainerWeb: {
|
},
|
||||||
minHeight: 500,
|
emptyContainerWeb: {
|
||||||
justifyContent: 'center',
|
minHeight: 500,
|
||||||
paddingTop: 100,
|
justifyContent: 'center',
|
||||||
},
|
paddingTop: 100,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 参考QQ和微信的实现,提供聊天管理功能
|
* 参考QQ和微信的实现,提供聊天管理功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { useAuthStore } from '../../stores';
|
||||||
import { authService } from '../../services/authService';
|
import { authService } from '../../services/authService';
|
||||||
import { messageService } from '../../services/messageService';
|
import { messageService } from '../../services/messageService';
|
||||||
@@ -33,6 +33,8 @@ import * as hrefs from '../../navigation/hrefs';
|
|||||||
import { AppBackButton } from '../../components/common';
|
import { AppBackButton } from '../../components/common';
|
||||||
|
|
||||||
const PrivateChatInfoScreen: React.FC = () => {
|
const PrivateChatInfoScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createPrivateChatInfoStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const raw = useLocalSearchParams<{
|
const raw = useLocalSearchParams<{
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
@@ -277,7 +279,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
trackColor={{ false: '#E0E0E0', true: colors.primary.light }}
|
trackColor={{ false: '#E0E0E0', true: colors.primary.light }}
|
||||||
thumbColor={value ? colors.primary.main : '#F5F5F5'}
|
thumbColor={value ? colors.primary.main : colors.background.default}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -420,101 +422,103 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createPrivateChatInfoStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
pageHeader: {
|
},
|
||||||
flexDirection: 'row',
|
pageHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
paddingVertical: spacing.sm,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.divider,
|
||||||
pageTitle: {
|
},
|
||||||
fontWeight: '600',
|
pageTitle: {
|
||||||
},
|
fontWeight: '600',
|
||||||
pageHeaderPlaceholder: {
|
},
|
||||||
width: 40,
|
pageHeaderPlaceholder: {
|
||||||
height: 40,
|
width: 40,
|
||||||
},
|
height: 40,
|
||||||
scrollView: {
|
},
|
||||||
flex: 1,
|
scrollView: {
|
||||||
},
|
flex: 1,
|
||||||
scrollContent: {
|
},
|
||||||
paddingBottom: spacing.xl,
|
scrollContent: {
|
||||||
},
|
paddingBottom: spacing.xl,
|
||||||
headerCard: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
headerCard: {
|
||||||
marginHorizontal: spacing.md,
|
backgroundColor: colors.background.paper,
|
||||||
marginTop: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
marginTop: spacing.md,
|
||||||
padding: spacing.lg,
|
borderRadius: borderRadius.lg,
|
||||||
...shadows.sm,
|
padding: spacing.lg,
|
||||||
},
|
...shadows.sm,
|
||||||
userHeader: {
|
},
|
||||||
flexDirection: 'row',
|
userHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
userInfo: {
|
},
|
||||||
flex: 1,
|
userInfo: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
userName: {
|
},
|
||||||
fontWeight: '600',
|
userName: {
|
||||||
marginBottom: spacing.xs,
|
fontWeight: '600',
|
||||||
},
|
marginBottom: spacing.xs,
|
||||||
section: {
|
},
|
||||||
marginTop: spacing.lg,
|
section: {
|
||||||
paddingHorizontal: spacing.md,
|
marginTop: spacing.lg,
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
sectionTitle: {
|
},
|
||||||
marginBottom: spacing.sm,
|
sectionTitle: {
|
||||||
marginLeft: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
fontWeight: '500',
|
marginLeft: spacing.sm,
|
||||||
},
|
fontWeight: '500',
|
||||||
card: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
card: {
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
...shadows.sm,
|
borderRadius: borderRadius.lg,
|
||||||
overflow: 'hidden',
|
...shadows.sm,
|
||||||
},
|
overflow: 'hidden',
|
||||||
settingItem: {
|
},
|
||||||
flexDirection: 'row',
|
settingItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.sm,
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingVertical: spacing.sm,
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
settingIconContainer: {
|
},
|
||||||
width: 36,
|
settingIconContainer: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: borderRadius.md,
|
height: 36,
|
||||||
backgroundColor: colors.primary.light + '20',
|
borderRadius: borderRadius.md,
|
||||||
justifyContent: 'center',
|
backgroundColor: colors.primary.light + '20',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginRight: spacing.md,
|
||||||
settingIconDanger: {
|
},
|
||||||
backgroundColor: colors.error.light + '20',
|
settingIconDanger: {
|
||||||
},
|
backgroundColor: colors.error.light + '20',
|
||||||
settingTitle: {
|
},
|
||||||
flex: 1,
|
settingTitle: {
|
||||||
},
|
flex: 1,
|
||||||
dangerText: {
|
},
|
||||||
color: colors.error.main,
|
dangerText: {
|
||||||
},
|
color: colors.error.main,
|
||||||
divider: {
|
},
|
||||||
marginLeft: spacing.xl + 36,
|
divider: {
|
||||||
width: 'auto',
|
marginLeft: spacing.xl + 36,
|
||||||
marginVertical: spacing.xs,
|
width: 'auto',
|
||||||
},
|
marginVertical: spacing.xs,
|
||||||
deleteButton: {
|
},
|
||||||
marginTop: spacing.sm,
|
deleteButton: {
|
||||||
},
|
marginTop: spacing.sm,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default PrivateChatInfoScreen;
|
export default PrivateChatInfoScreen;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
|
|||||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Avatar, Text, AppBackButton } from '../../../../components/common';
|
import { Avatar, Text, AppBackButton } from '../../../../components/common';
|
||||||
import { colors, spacing } from '../../../../theme';
|
import { useAppColors, spacing } from '../../../../theme';
|
||||||
import { chatScreenStyles as baseStyles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||||
import { ChatHeaderProps } from './types';
|
import { ChatHeaderProps } from './types';
|
||||||
|
|
||||||
@@ -24,6 +24,9 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|||||||
onGroupInfoPress,
|
onGroupInfoPress,
|
||||||
isWideScreen: propIsWideScreen,
|
isWideScreen: propIsWideScreen,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const baseStyles = useChatScreenStyles();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
// 使用 768px 作为大屏幕和小屏幕的分界线
|
// 使用 768px 作为大屏幕和小屏幕的分界线
|
||||||
const isWideScreenHook = useBreakpointGTE('lg');
|
const isWideScreenHook = useBreakpointGTE('lg');
|
||||||
@@ -57,7 +60,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|||||||
fontSize: isWideScreen ? 13 : 12,
|
fontSize: isWideScreen ? 13 : 12,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [isWideScreen]);
|
}, [isWideScreen, baseStyles]);
|
||||||
|
|
||||||
// 获取显示标题
|
// 获取显示标题
|
||||||
const getDisplayTitle = () => {
|
const getDisplayTitle = () => {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
|
|||||||
import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native';
|
import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Text } from '../../../../components/common';
|
import { Text } from '../../../../components/common';
|
||||||
import { colors } from '../../../../theme';
|
import { useAppColors } from '../../../../theme';
|
||||||
import { chatScreenStyles as baseStyles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||||
import { ChatInputProps, GroupMessage, SenderInfo } from './types';
|
import { ChatInputProps, GroupMessage, SenderInfo } from './types';
|
||||||
import { extractTextFromSegments } from '../../../../types/dto';
|
import { extractTextFromSegments } from '../../../../types/dto';
|
||||||
@@ -39,14 +39,15 @@ export const ChatInput: React.FC<ChatInputProps & {
|
|||||||
otherUser,
|
otherUser,
|
||||||
getSenderInfo,
|
getSenderInfo,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useChatScreenStyles();
|
||||||
const isDisabled = isGroupChat && isMuted;
|
const isDisabled = isGroupChat && isMuted;
|
||||||
|
|
||||||
// 获取当前用户ID
|
// 获取当前用户ID
|
||||||
const currentUserId = currentUser?.id || '';
|
const currentUserId = currentUser?.id || '';
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
const isWideScreen = useBreakpointGTE('lg');
|
const isWideScreen = useBreakpointGTE('lg');
|
||||||
const styles = baseStyles;
|
|
||||||
|
|
||||||
const inputContainerStyle = useMemo(() => ([
|
const inputContainerStyle = useMemo(() => ([
|
||||||
styles.inputContainer,
|
styles.inputContainer,
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { Image as ExpoImage } from 'expo-image';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { Text } from '../../../../components/common';
|
import { Text } from '../../../../components/common';
|
||||||
import { chatScreenStyles as baseStyles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||||
import { EMOJIS } from './constants';
|
import { EMOJIS } from './constants';
|
||||||
import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '../../../../services/stickerService';
|
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');
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -41,6 +41,8 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
onInsertSticker,
|
onInsertSticker,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const baseStyles = useChatScreenStyles();
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('emoji');
|
const [activeTab, setActiveTab] = useState<TabType>('emoji');
|
||||||
const [stickers, setStickers] = useState<CustomSticker[]>([]);
|
const [stickers, setStickers] = useState<CustomSticker[]>([]);
|
||||||
const [loadingStickers, setLoadingStickers] = useState(false);
|
const [loadingStickers, setLoadingStickers] = useState(false);
|
||||||
@@ -82,7 +84,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
lineHeight: emojiSize.size + 6,
|
lineHeight: emojiSize.size + 6,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [emojiSize, emojiColumns]);
|
}, [emojiSize, emojiColumns, baseStyles]);
|
||||||
|
|
||||||
// 加载自定义表情
|
// 加载自定义表情
|
||||||
const loadStickers = useCallback(async () => {
|
const loadStickers = useCallback(async () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 实现手机端群信息界面的核心功能
|
* 实现手机端群信息界面的核心功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Avatar, Text } from '../../../../components/common';
|
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 { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto';
|
||||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||||
import { groupManager } from '../../../../stores/groupManager';
|
import { groupManager } from '../../../../stores/groupManager';
|
||||||
@@ -53,6 +53,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
onInviteMembers,
|
onInviteMembers,
|
||||||
onViewAllMembers,
|
onViewAllMembers,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]);
|
||||||
const isWideScreen = useBreakpointGTE('lg');
|
const isWideScreen = useBreakpointGTE('lg');
|
||||||
const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current;
|
const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current;
|
||||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||||
@@ -363,7 +365,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createGroupInfoPanelStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
@@ -378,7 +381,7 @@ const styles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: PANEL_WIDTH,
|
width: PANEL_WIDTH,
|
||||||
backgroundColor: '#FFF',
|
backgroundColor: colors.background.paper,
|
||||||
zIndex: 101,
|
zIndex: 101,
|
||||||
...shadows.lg,
|
...shadows.lg,
|
||||||
},
|
},
|
||||||
@@ -389,12 +392,12 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#E8E8E8',
|
borderBottomColor: colors.divider,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: fontSizes.xl,
|
fontSize: fontSizes.xl,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
padding: spacing.xs,
|
padding: spacing.xs,
|
||||||
@@ -409,12 +412,12 @@ const styles = StyleSheet.create({
|
|||||||
groupName: {
|
groupName: {
|
||||||
fontSize: fontSizes.xl,
|
fontSize: fontSizes.xl,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
marginTop: spacing.md,
|
marginTop: spacing.md,
|
||||||
},
|
},
|
||||||
memberCount: {
|
memberCount: {
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
color: '#999',
|
color: colors.text.secondary,
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
},
|
},
|
||||||
joinType: {
|
joinType: {
|
||||||
@@ -442,7 +445,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
divider: {
|
divider: {
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: '#E8E8E8',
|
backgroundColor: colors.divider,
|
||||||
marginHorizontal: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
@@ -456,11 +459,11 @@ const styles = StyleSheet.create({
|
|||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
marginLeft: spacing.xs,
|
marginLeft: spacing.xs,
|
||||||
},
|
},
|
||||||
noticeBox: {
|
noticeBox: {
|
||||||
backgroundColor: '#FFF9E6',
|
backgroundColor: colors.chat.tipBg,
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeftWidth: 3,
|
borderLeftWidth: 3,
|
||||||
@@ -468,23 +471,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
noticeText: {
|
noticeText: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: '#666',
|
color: colors.text.secondary,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
noticeTime: {
|
noticeTime: {
|
||||||
fontSize: fontSizes.xs,
|
fontSize: fontSizes.xs,
|
||||||
color: '#999',
|
color: colors.text.hint,
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
},
|
},
|
||||||
descriptionBox: {
|
descriptionBox: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: '#666',
|
color: colors.text.secondary,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
memberList: {
|
memberList: {
|
||||||
@@ -518,7 +521,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
memberName: {
|
memberName: {
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
ownerBadge: {
|
ownerBadge: {
|
||||||
@@ -541,7 +544,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
moreMembers: {
|
moreMembers: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: '#999',
|
color: colors.text.secondary,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
},
|
},
|
||||||
@@ -550,15 +553,15 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#F0F0F0',
|
borderBottomColor: colors.chat.borderHairline,
|
||||||
},
|
},
|
||||||
infoLabel: {
|
infoLabel: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: '#999',
|
color: colors.text.secondary,
|
||||||
},
|
},
|
||||||
infoValue: {
|
infoValue: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
actionSection: {
|
actionSection: {
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
@@ -588,6 +591,7 @@ const styles = StyleSheet.create({
|
|||||||
leaveButtonText: {
|
leaveButtonText: {
|
||||||
color: colors.error.main,
|
color: colors.error.main,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default GroupInfoPanel;
|
export default GroupInfoPanel;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { chatScreenStyles as styles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { LongPressMenuProps } from './types';
|
import { LongPressMenuProps } from './types';
|
||||||
import { RECALL_TIME_LIMIT } from './constants';
|
import { RECALL_TIME_LIMIT } from './constants';
|
||||||
import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto';
|
import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto';
|
||||||
@@ -34,6 +34,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onAddSticker,
|
onAddSticker,
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useChatScreenStyles();
|
||||||
const scaleAnimation = useRef(new Animated.Value(0)).current;
|
const scaleAnimation = useRef(new Animated.Value(0)).current;
|
||||||
const opacityAnimation = useRef(new Animated.Value(0)).current;
|
const opacityAnimation = useRef(new Animated.Value(0)).current;
|
||||||
const blurActiveElementOnWeb = () => {
|
const blurActiveElementOnWeb = () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React from 'react';
|
|||||||
import { View, ScrollView, TouchableOpacity } from 'react-native';
|
import { View, ScrollView, TouchableOpacity } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Avatar, Text } from '../../../../components/common';
|
import { Avatar, Text } from '../../../../components/common';
|
||||||
import { chatScreenStyles as styles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { MentionPanelProps } from './types';
|
import { MentionPanelProps } from './types';
|
||||||
|
|
||||||
export const MentionPanel: React.FC<MentionPanelProps> = ({
|
export const MentionPanel: React.FC<MentionPanelProps> = ({
|
||||||
@@ -18,6 +18,7 @@ export const MentionPanel: React.FC<MentionPanelProps> = ({
|
|||||||
onMentionAll,
|
onMentionAll,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useChatScreenStyles();
|
||||||
// 过滤成员列表
|
// 过滤成员列表
|
||||||
const filteredMembers = members.filter(member => {
|
const filteredMembers = members.filter(member => {
|
||||||
if (member.user_id === currentUserId) return false; // 排除自己
|
if (member.user_id === currentUserId) return false; // 排除自己
|
||||||
|
|||||||
@@ -15,20 +15,12 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Avatar, Text, ImageGridItem } from '../../../../components/common';
|
import { Avatar, Text, ImageGridItem } from '../../../../components/common';
|
||||||
import { chatScreenStyles as baseStyles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||||
import { MessageBubbleProps, SenderInfo, MenuPosition, GroupMessage } from './types';
|
import { MessageBubbleProps, SenderInfo, MenuPosition, GroupMessage } from './types';
|
||||||
import { MessageSegmentsRenderer } from './SegmentRenderer';
|
import { MessageSegmentsRenderer } from './SegmentRenderer';
|
||||||
import { MessageSegment, ImageSegmentData, extractTextFromSegments } from '../../../../types/dto';
|
import { MessageSegment, ImageSegmentData, extractTextFromSegments } from '../../../../types/dto';
|
||||||
import { SwipeableMessageBubble } from './SwipeableMessageBubble';
|
import { SwipeableMessageBubble } from './SwipeableMessageBubble';
|
||||||
import {
|
|
||||||
getBubbleBorderRadius,
|
|
||||||
getMessageGroupPosition,
|
|
||||||
shouldShowSenderInfo,
|
|
||||||
bubbleColors,
|
|
||||||
bubbleStyles,
|
|
||||||
} from './bubbleStyles';
|
|
||||||
|
|
||||||
// 获取屏幕宽度
|
// 获取屏幕宽度
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -61,7 +53,8 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const bubbleRef = useRef<View>(null);
|
const bubbleRef = useRef<View>(null);
|
||||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||||
|
const baseStyles = useChatScreenStyles();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
const { width } = useResponsive();
|
const { width } = useResponsive();
|
||||||
const isWideScreen = useBreakpointGTE('lg');
|
const isWideScreen = useBreakpointGTE('lg');
|
||||||
@@ -89,7 +82,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|||||||
height: isWideScreen ? 280 : 220,
|
height: isWideScreen ? 280 : 220,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [maxContentWidth, isWideScreen]);
|
}, [maxContentWidth, isWideScreen, baseStyles]);
|
||||||
|
|
||||||
// 系统通知消息特殊处理
|
// 系统通知消息特殊处理
|
||||||
// 支持两种判断方式:
|
// 支持两种判断方式:
|
||||||
@@ -236,26 +229,46 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderBubbleShell = useCallback(
|
||||||
|
(
|
||||||
|
child: React.ReactNode,
|
||||||
|
innerExtra: (object | undefined | null | false)[] = []
|
||||||
|
) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.messageBubbleOuter,
|
||||||
|
isMe ? styles.myBubbleOuter : styles.theirBubbleOuter,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.messageBubbleInner,
|
||||||
|
isMe ? styles.myBubbleInner : styles.theirBubbleInner,
|
||||||
|
...innerExtra.filter(Boolean),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
[isMe, styles]
|
||||||
|
);
|
||||||
|
|
||||||
// 渲染消息内容
|
// 渲染消息内容
|
||||||
const renderMessageContent = () => {
|
const renderMessageContent = () => {
|
||||||
// 撤回消息
|
// 撤回消息
|
||||||
if (isRecalled) {
|
if (isRecalled) {
|
||||||
return (
|
return renderBubbleShell(
|
||||||
<View style={[
|
<Text
|
||||||
styles.messageBubble,
|
style={[
|
||||||
isMe ? styles.myBubble : styles.theirBubble,
|
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||||
styles.recalledBubble,
|
styles.recalledText,
|
||||||
]}>
|
]}
|
||||||
<Text
|
selectable={false}
|
||||||
style={[
|
>
|
||||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
消息已撤回
|
||||||
styles.recalledText,
|
</Text>,
|
||||||
]}
|
[styles.recalledBubble]
|
||||||
selectable={false}
|
|
||||||
>
|
|
||||||
消息已撤回
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,71 +295,60 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|||||||
|
|
||||||
// 如果没有 segments,显示空内容或错误提示
|
// 如果没有 segments,显示空内容或错误提示
|
||||||
if (!hasSegments) {
|
if (!hasSegments) {
|
||||||
return (
|
return renderBubbleShell(
|
||||||
<View style={[
|
<Text
|
||||||
styles.messageBubble,
|
style={[
|
||||||
isMe ? styles.myBubble : styles.theirBubble,
|
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||||
]}>
|
{ opacity: 0.5 },
|
||||||
<Text
|
]}
|
||||||
style={[
|
selectable={false}
|
||||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
>
|
||||||
{ opacity: 0.5 },
|
[消息格式错误]
|
||||||
]}
|
</Text>
|
||||||
selectable={false}
|
|
||||||
>
|
|
||||||
[消息格式错误]
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有回复,用于调整气泡样式
|
// 检查是否有回复,用于调整气泡样式
|
||||||
const hasReply = segments.some(s => s.type === 'reply');
|
const hasReply = segments.some(s => s.type === 'reply');
|
||||||
|
|
||||||
return (
|
return renderBubbleShell(
|
||||||
<View style={[
|
<MessageSegmentsRenderer
|
||||||
styles.messageBubble,
|
segments={segments}
|
||||||
isMe ? styles.myBubble : styles.theirBubble,
|
isMe={isMe}
|
||||||
hasReply && segmentStyles.replyBubble,
|
currentUserId={currentUserId}
|
||||||
isPureImageMessage && segmentStyles.pureImageBubble,
|
memberMap={memberMap}
|
||||||
]}>
|
replyMessage={getReplyMessage()}
|
||||||
<MessageSegmentsRenderer
|
getSenderInfo={getSenderInfo}
|
||||||
segments={segments}
|
onAtPress={() => undefined}
|
||||||
isMe={isMe}
|
onReplyPress={onReplyPress}
|
||||||
currentUserId={currentUserId}
|
onImagePress={(url) => {
|
||||||
memberMap={memberMap}
|
// 查找点击的图片索引
|
||||||
replyMessage={getReplyMessage()}
|
const clickIndex = imageSegments.findIndex(
|
||||||
getSenderInfo={getSenderInfo}
|
img => img.url === url || img.thumbnail_url === url
|
||||||
onAtPress={() => undefined}
|
);
|
||||||
onReplyPress={onReplyPress}
|
if (clickIndex !== -1 && onImagePress) {
|
||||||
onImagePress={(url) => {
|
onImagePress(imageSegments, clickIndex);
|
||||||
// 查找点击的图片索引
|
}
|
||||||
const clickIndex = imageSegments.findIndex(
|
}}
|
||||||
img => img.url === url || img.thumbnail_url === url
|
onImageLongPress={(position?: { x: number; y: number }) => {
|
||||||
);
|
// 图片长按触发和消息气泡一样的菜单
|
||||||
if (clickIndex !== -1 && onImagePress) {
|
// 如果有位置信息,直接使用;否则使用气泡位置
|
||||||
onImagePress(imageSegments, clickIndex);
|
if (position) {
|
||||||
}
|
onLongPress(message, {
|
||||||
}}
|
x: 0,
|
||||||
onImageLongPress={(position?: { x: number; y: number }) => {
|
y: 0,
|
||||||
// 图片长按触发和消息气泡一样的菜单
|
width: 0,
|
||||||
// 如果有位置信息,直接使用;否则使用气泡位置
|
height: 0,
|
||||||
if (position) {
|
pressX: position.x,
|
||||||
onLongPress(message, {
|
pressY: position.y,
|
||||||
x: 0,
|
});
|
||||||
y: 0,
|
} else {
|
||||||
width: 0,
|
handleLongPress();
|
||||||
height: 0,
|
}
|
||||||
pressX: position.x,
|
}}
|
||||||
pressY: position.y,
|
onLinkPress={() => undefined}
|
||||||
});
|
/>,
|
||||||
} else {
|
[hasReply && segmentStyles.replyBubble, isPureImageMessage && segmentStyles.pureImageBubble]
|
||||||
handleLongPress();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onLinkPress={() => undefined}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React from 'react';
|
|||||||
import { View, TouchableOpacity } from 'react-native';
|
import { View, TouchableOpacity } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Text } from '../../../../components/common';
|
import { Text } from '../../../../components/common';
|
||||||
import { chatScreenStyles as styles } from './styles';
|
import { useChatScreenStyles } from './styles';
|
||||||
import { MORE_ACTIONS } from './constants';
|
import { MORE_ACTIONS } from './constants';
|
||||||
import { MorePanelProps } from './types';
|
import { MorePanelProps } from './types';
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export const MorePanel: React.FC<MorePanelProps> = ({
|
|||||||
onAction,
|
onAction,
|
||||||
disabledActionIds = [],
|
disabledActionIds = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useChatScreenStyles();
|
||||||
const disabledSet = new Set(disabledActionIds);
|
const disabledSet = new Set(disabledActionIds);
|
||||||
return (
|
return (
|
||||||
<View style={styles.panelContainer}>
|
<View style={styles.panelContainer}>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
UserDTO,
|
UserDTO,
|
||||||
extractTextFromSegments,
|
extractTextFromSegments,
|
||||||
} from '../../../../types/dto';
|
} from '../../../../types/dto';
|
||||||
import { colors, spacing } from '../../../../theme';
|
import { spacing, useAppColors, type AppColors } from '../../../../theme';
|
||||||
import { SenderInfo } from './types';
|
import { SenderInfo } from './types';
|
||||||
|
|
||||||
const IMAGE_MAX_WIDTH = 200;
|
const IMAGE_MAX_WIDTH = 200;
|
||||||
@@ -142,12 +142,11 @@ export const renderSegment = (
|
|||||||
/**
|
/**
|
||||||
* 渲染文本 Segment
|
* 渲染文本 Segment
|
||||||
*/
|
*/
|
||||||
const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => {
|
const TextSegmentBody: React.FC<{ data: TextSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||||
// 兼容 content 和 text 两种字段
|
const styles = useSegmentStyles();
|
||||||
const text = data.content ?? data.text ?? '';
|
const text = data.content ?? data.text ?? '';
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={`text-${text.substring(0, 10)}`}
|
|
||||||
style={[styles.textContent, isMe ? styles.textMe : styles.textOther]}
|
style={[styles.textContent, isMe ? styles.textMe : styles.textOther]}
|
||||||
selectable={false}
|
selectable={false}
|
||||||
>
|
>
|
||||||
@@ -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 <TextSegmentBody key={key} data={data} isMe={isMe} />;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染图片 Segment
|
* 渲染图片 Segment
|
||||||
*/
|
*/
|
||||||
@@ -166,6 +170,7 @@ const ImageSegment: React.FC<{
|
|||||||
onImagePress?: (url: string) => void;
|
onImagePress?: (url: string) => void;
|
||||||
onImageLongPress?: (position?: { x: number; y: number }) => void;
|
onImageLongPress?: (position?: { x: number; y: number }) => void;
|
||||||
}> = ({ data, isMe, onImagePress, onImageLongPress }) => {
|
}> = ({ data, isMe, onImagePress, onImageLongPress }) => {
|
||||||
|
const styles = useSegmentStyles();
|
||||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||||
const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
|
const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
|
||||||
const [loadError, setLoadError] = useState(false);
|
const [loadError, setLoadError] = useState(false);
|
||||||
@@ -362,16 +367,13 @@ const renderImageSegment = (
|
|||||||
/**
|
/**
|
||||||
* 渲染 @ Segment
|
* 渲染 @ Segment
|
||||||
*/
|
*/
|
||||||
const renderAtSegment = (
|
const AtSegmentBody: React.FC<{
|
||||||
data: AtSegmentData,
|
data: AtSegmentData;
|
||||||
props: Omit<SegmentRendererProps, 'segment'>
|
props: Omit<SegmentRendererProps, 'segment'>;
|
||||||
): React.ReactNode => {
|
}> = ({ data, props }) => {
|
||||||
|
const styles = useSegmentStyles();
|
||||||
const { currentUserId, memberMap, onAtPress, isMe } = props;
|
const { currentUserId, memberMap, onAtPress, isMe } = props;
|
||||||
|
|
||||||
// 判断是否@当前用户
|
|
||||||
const isAtMe = data.user_id === currentUserId || data.user_id === 'all';
|
const isAtMe = data.user_id === currentUserId || data.user_id === 'all';
|
||||||
|
|
||||||
// 优先从 memberMap 中查找昵称,兜底使用 data.nickname(兼容旧消息),再兜底显示"用户"
|
|
||||||
let displayName: string;
|
let displayName: string;
|
||||||
if (data.user_id === 'all') {
|
if (data.user_id === 'all') {
|
||||||
displayName = '所有人';
|
displayName = '所有人';
|
||||||
@@ -379,10 +381,8 @@ const renderAtSegment = (
|
|||||||
const member = memberMap?.get(data.user_id);
|
const member = memberMap?.get(data.user_id);
|
||||||
displayName = member?.nickname || data.nickname || '用户';
|
displayName = member?.nickname || data.nickname || '用户';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={`at-${data.user_id}`}
|
|
||||||
style={[
|
style={[
|
||||||
styles.atText,
|
styles.atText,
|
||||||
isMe ? styles.atTextMe : styles.atTextOther,
|
isMe ? styles.atTextMe : styles.atTextOther,
|
||||||
@@ -395,15 +395,19 @@ const renderAtSegment = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderAtSegment = (
|
||||||
|
data: AtSegmentData,
|
||||||
|
props: Omit<SegmentRendererProps, 'segment'>
|
||||||
|
): React.ReactNode => <AtSegmentBody key={`at-${data.user_id}`} data={data} props={props} />;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染表情 Segment
|
* 渲染表情 Segment
|
||||||
*/
|
*/
|
||||||
const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNode => {
|
const FaceSegmentBody: React.FC<{ data: FaceSegmentData }> = ({ data }) => {
|
||||||
// 如果有自定义表情URL,显示图片
|
const styles = useSegmentStyles();
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
return (
|
return (
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
key={`face-${data.id}`}
|
|
||||||
source={{ uri: data.url }}
|
source={{ uri: data.url }}
|
||||||
style={styles.faceImage}
|
style={styles.faceImage}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
@@ -411,29 +415,32 @@ const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNod
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则显示表情名称(后续可以映射到本地表情)
|
|
||||||
return (
|
return (
|
||||||
<Text key={`face-${data.id}`} style={styles.faceText}>
|
<Text style={styles.faceText}>
|
||||||
[{data.name || `表情${data.id}`}]
|
[{data.name || `表情${data.id}`}]
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFaceSegment = (data: FaceSegmentData, _isMe: boolean): React.ReactNode => (
|
||||||
|
<FaceSegmentBody key={`face-${data.id}`} data={data} />
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染语音 Segment
|
* 渲染语音 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 (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`voice-${data.url}`}
|
|
||||||
style={[styles.voiceContainer, isMe ? styles.voiceMe : styles.voiceOther]}
|
style={[styles.voiceContainer, isMe ? styles.voiceMe : styles.voiceOther]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="microphone"
|
name="microphone"
|
||||||
size={20}
|
size={20}
|
||||||
color={isMe ? '#FFFFFF' : '#666666'}
|
color={isMe ? themeColors.primary.contrast : themeColors.chat.textTertiary}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.voiceDuration, isMe ? styles.textMe : styles.textOther]}>
|
<Text style={[styles.voiceDuration, isMe ? styles.textMe : styles.textOther]}>
|
||||||
{data.duration ? `${data.duration}"` : '语音'}
|
{data.duration ? `${data.duration}"` : '语音'}
|
||||||
@@ -442,10 +449,16 @@ const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactN
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactNode => (
|
||||||
|
<VoiceSegmentBody key={`voice-${data.url}`} data={data} isMe={isMe} />
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视频 Segment 组件 - 支持点击播放
|
* 视频 Segment 组件 - 支持点击播放
|
||||||
*/
|
*/
|
||||||
const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||||
|
const styles = useSegmentStyles();
|
||||||
|
const themeColors = useAppColors();
|
||||||
const [playerVisible, setPlayerVisible] = useState(false);
|
const [playerVisible, setPlayerVisible] = useState(false);
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
@@ -469,11 +482,11 @@ const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ dat
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.videoPlaceholder}>
|
<View style={styles.videoPlaceholder}>
|
||||||
<MaterialCommunityIcons name="video" size={32} color="#FFFFFF" />
|
<MaterialCommunityIcons name="video" size={32} color={themeColors.primary.contrast} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.videoPlayIcon}>
|
<View style={styles.videoPlayIcon}>
|
||||||
<MaterialCommunityIcons name="play-circle" size={40} color="#FFFFFF" />
|
<MaterialCommunityIcons name="play-circle" size={40} color={themeColors.primary.contrast} />
|
||||||
</View>
|
</View>
|
||||||
{data.duration && (
|
{data.duration && (
|
||||||
<Text style={styles.videoDuration}>{formatDuration(data.duration)}</Text>
|
<Text style={styles.videoDuration}>{formatDuration(data.duration)}</Text>
|
||||||
@@ -496,12 +509,12 @@ const renderVideoSegment = (data: VideoSegmentData, isMe: boolean): React.ReactN
|
|||||||
/**
|
/**
|
||||||
* 渲染文件 Segment
|
* 渲染文件 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) : '';
|
const fileSize = data.size ? formatFileSize(data.size) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`file-${data.url}`}
|
|
||||||
style={[styles.fileContainer, isMe ? styles.fileMe : styles.fileOther]}
|
style={[styles.fileContainer, isMe ? styles.fileMe : styles.fileOther]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@@ -509,33 +522,36 @@ const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNod
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="file-document"
|
name="file-document"
|
||||||
size={28}
|
size={28}
|
||||||
color={isMe ? '#FFFFFF' : colors.primary.main}
|
color={isMe ? themeColors.primary.contrast : themeColors.primary.main}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.fileInfo}>
|
<View style={styles.fileInfo}>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.fileName, isMe ? styles.textMe : styles.textOther]}
|
style={[styles.fileName, isMe ? styles.textMe : styles.textOther]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
{fileSize && (
|
{fileSize ? <Text style={styles.fileSize}>{fileSize}</Text> : null}
|
||||||
<Text style={styles.fileSize}>{fileSize}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => (
|
||||||
|
<FileSegmentBody key={`file-${data.url}`} data={data} isMe={isMe} />
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染链接 Segment
|
* 渲染链接 Segment
|
||||||
*/
|
*/
|
||||||
const renderLinkSegment = (
|
const LinkSegmentBody: React.FC<{
|
||||||
data: LinkSegmentData,
|
data: LinkSegmentData;
|
||||||
props: Omit<SegmentRendererProps, 'segment'>
|
props: Omit<SegmentRendererProps, 'segment'>;
|
||||||
): React.ReactNode => {
|
}> = ({ data, props }) => {
|
||||||
|
const styles = useSegmentStyles();
|
||||||
const { onLinkPress, isMe } = props;
|
const { onLinkPress, isMe } = props;
|
||||||
|
|
||||||
const handlePress = async () => {
|
const handlePress = async () => {
|
||||||
if (onLinkPress) {
|
if (onLinkPress) {
|
||||||
onLinkPress(data.url);
|
onLinkPress(data.url);
|
||||||
@@ -552,26 +568,25 @@ const renderLinkSegment = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`link-${data.url}`}
|
|
||||||
style={[styles.linkContainer, isMe ? styles.linkMe : styles.linkOther]}
|
style={[styles.linkContainer, isMe ? styles.linkMe : styles.linkOther]}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{data.image && (
|
{data.image ? (
|
||||||
<ExpoImage source={{ uri: data.image }} style={styles.linkImage} contentFit="cover" cachePolicy="memory" />
|
<ExpoImage source={{ uri: data.image }} style={styles.linkImage} contentFit="cover" cachePolicy="memory" />
|
||||||
)}
|
) : null}
|
||||||
<View style={styles.linkContent}>
|
<View style={styles.linkContent}>
|
||||||
<Text style={styles.linkTitle} numberOfLines={2}>
|
<Text style={styles.linkTitle} numberOfLines={2}>
|
||||||
{data.title || data.url}
|
{data.title || data.url}
|
||||||
</Text>
|
</Text>
|
||||||
{data.description && (
|
{data.description ? (
|
||||||
<Text style={styles.linkDescription} numberOfLines={2}>
|
<Text style={styles.linkDescription} numberOfLines={2}>
|
||||||
{data.description}
|
{data.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
) : null}
|
||||||
<Text style={styles.linkUrl} numberOfLines={1}>
|
<Text style={styles.linkUrl} numberOfLines={1}>
|
||||||
{(() => {
|
{(() => {
|
||||||
try {
|
try {
|
||||||
@@ -586,6 +601,11 @@ const renderLinkSegment = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderLinkSegment = (
|
||||||
|
data: LinkSegmentData,
|
||||||
|
props: Omit<SegmentRendererProps, 'segment'>
|
||||||
|
): React.ReactNode => <LinkSegmentBody key={`link-${data.url}`} data={data} props={props} />;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回复预览组件(用于消息气泡内显示回复引用)
|
* 回复预览组件(用于消息气泡内显示回复引用)
|
||||||
*/
|
*/
|
||||||
@@ -596,6 +616,9 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
|||||||
onPress,
|
onPress,
|
||||||
getSenderInfo,
|
getSenderInfo,
|
||||||
}) => {
|
}) => {
|
||||||
|
const themeColors = useAppColors();
|
||||||
|
const styles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||||
|
|
||||||
if (!replyMessage) {
|
if (!replyMessage) {
|
||||||
// 如果没有引用消息详情,只显示引用ID
|
// 如果没有引用消息详情,只显示引用ID
|
||||||
return (
|
return (
|
||||||
@@ -605,9 +628,11 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.replyLine} />
|
<View style={styles.replyLine} />
|
||||||
<Text style={styles.replyText} numberOfLines={2}>
|
<View style={styles.replyContent}>
|
||||||
引用消息
|
<Text style={styles.replyText} numberOfLines={2}>
|
||||||
</Text>
|
引用消息
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -658,6 +683,7 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
<View style={styles.replyLine} />
|
||||||
<View style={styles.replyContent}>
|
<View style={styles.replyContent}>
|
||||||
<Text style={styles.replySender} numberOfLines={1}>
|
<Text style={styles.replySender} numberOfLines={1}>
|
||||||
{senderName}:
|
{senderName}:
|
||||||
@@ -702,7 +728,9 @@ export const MessageSegmentsRenderer: React.FC<{
|
|||||||
onLinkPress,
|
onLinkPress,
|
||||||
getSenderInfo,
|
getSenderInfo,
|
||||||
}) => {
|
}) => {
|
||||||
// 找出 reply segment(如果有)
|
const themeColors = useAppColors();
|
||||||
|
const segStyles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||||
|
|
||||||
const replySegment = segments.find(s => s.type === 'reply');
|
const replySegment = segments.find(s => s.type === 'reply');
|
||||||
const otherSegments = segments.filter(s => s.type !== 'reply');
|
const otherSegments = segments.filter(s => s.type !== 'reply');
|
||||||
const chunks = partitionMessageSegments(otherSegments);
|
const chunks = partitionMessageSegments(otherSegments);
|
||||||
@@ -717,58 +745,58 @@ export const MessageSegmentsRenderer: React.FC<{
|
|||||||
onImageLongPress,
|
onImageLongPress,
|
||||||
onLinkPress,
|
onLinkPress,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.segmentsContainer}>
|
<SegmentStylesContext.Provider value={segStyles}>
|
||||||
{/* 回复预览 */}
|
<View style={segStyles.segmentsContainer}>
|
||||||
{replySegment && (
|
{replySegment ? (
|
||||||
<ReplyPreviewSegment
|
<ReplyPreviewSegment
|
||||||
replyData={(replySegment.data as ReplySegmentData)}
|
replyData={replySegment.data as ReplySegmentData}
|
||||||
replyMessage={replyMessage}
|
replyMessage={replyMessage}
|
||||||
isMe={isMe}
|
isMe={isMe}
|
||||||
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
|
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
|
||||||
getSenderInfo={getSenderInfo}
|
getSenderInfo={getSenderInfo}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{/* 其他 Segment:行内文案与块级图片/媒体分行展示,多图纵向排列 */}
|
<View style={segStyles.segmentsColumn}>
|
||||||
<View style={styles.segmentsColumn}>
|
{chunks.map((chunk, i) => {
|
||||||
{chunks.map((chunk, i) => {
|
if (chunk.kind === 'inline') {
|
||||||
if (chunk.kind === 'inline') {
|
return (
|
||||||
|
<View key={`inl-${i}`} style={segStyles.inlineChunk}>
|
||||||
|
{chunk.parts.map((segment, j) => (
|
||||||
|
<React.Fragment
|
||||||
|
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || j}`}
|
||||||
|
>
|
||||||
|
{renderSegment(segment, renderProps)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (chunk.kind === 'images') {
|
||||||
|
return (
|
||||||
|
<View key={`imgs-${i}`} style={segStyles.imagesChunk}>
|
||||||
|
{chunk.parts.map((segment, j) => (
|
||||||
|
<View
|
||||||
|
key={`img-${(segment.data as ImageSegmentData)?.url || j}`}
|
||||||
|
style={j < chunk.parts.length - 1 ? segStyles.imageStackItem : segStyles.imageStackItemLast}
|
||||||
|
>
|
||||||
|
{renderSegment(segment, renderProps)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<View key={`inl-${i}`} style={styles.inlineChunk}>
|
<View key={`blk-${i}`} style={segStyles.blockChunk}>
|
||||||
{chunk.parts.map((segment, j) => (
|
{renderSegment(chunk.segment, renderProps)}
|
||||||
<React.Fragment
|
|
||||||
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || j}`}
|
|
||||||
>
|
|
||||||
{renderSegment(segment, renderProps)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
if (chunk.kind === 'images') {
|
</View>
|
||||||
return (
|
|
||||||
<View key={`imgs-${i}`} style={styles.imagesChunk}>
|
|
||||||
{chunk.parts.map((segment, j) => (
|
|
||||||
<View
|
|
||||||
key={`img-${(segment.data as ImageSegmentData)?.url || j}`}
|
|
||||||
style={j < chunk.parts.length - 1 ? styles.imageStackItem : styles.imageStackItemLast}
|
|
||||||
>
|
|
||||||
{renderSegment(segment, renderProps)}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View key={`blk-${i}`} style={styles.blockChunk}>
|
|
||||||
{renderSegment(chunk.segment, renderProps)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</SegmentStylesContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -787,7 +815,8 @@ const formatDuration = (seconds: number): string => {
|
|||||||
return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `0:${secs.toString().padStart(2, '0')}`;
|
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: {
|
segmentsContainer: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -826,10 +855,10 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
textMe: {
|
textMe: {
|
||||||
color: '#1A1A1A', // 统一使用深色字体
|
color: colors.chat.textPrimary,
|
||||||
},
|
},
|
||||||
textOther: {
|
textOther: {
|
||||||
color: '#1A1A1A', // 统一使用深色字体
|
color: colors.chat.textPrimary,
|
||||||
},
|
},
|
||||||
|
|
||||||
// @提及 - Telegram风格:蓝色高亮
|
// @提及 - Telegram风格:蓝色高亮
|
||||||
@@ -845,7 +874,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
atTextOther: {
|
atTextOther: {
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
@@ -860,7 +889,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@@ -892,7 +921,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: spacing.sm + 2,
|
paddingVertical: spacing.sm + 2,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
@@ -916,7 +945,7 @@ const styles = StyleSheet.create({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
width: 220,
|
width: 220,
|
||||||
height: 165,
|
height: 165,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@@ -953,7 +982,7 @@ const styles = StyleSheet.create({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
right: 10,
|
right: 10,
|
||||||
color: '#FFFFFF',
|
color: colors.primary.contrast,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
@@ -971,7 +1000,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
maxWidth: 300,
|
maxWidth: 300,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
@@ -981,7 +1010,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||||
},
|
},
|
||||||
fileOther: {
|
fileOther: {
|
||||||
backgroundColor: '#F8F9FA',
|
backgroundColor: colors.chat.surfaceRaised,
|
||||||
},
|
},
|
||||||
fileIcon: {
|
fileIcon: {
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
@@ -998,11 +1027,11 @@ const styles = StyleSheet.create({
|
|||||||
fileName: {
|
fileName: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
},
|
},
|
||||||
fileSize: {
|
fileSize: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
@@ -1012,8 +1041,8 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maxWidth: 280,
|
maxWidth: 280,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@@ -1023,7 +1052,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||||
},
|
},
|
||||||
linkOther: {
|
linkOther: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
},
|
},
|
||||||
linkImage: {
|
linkImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1035,12 +1064,12 @@ const styles = StyleSheet.create({
|
|||||||
linkTitle: {
|
linkTitle: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
linkDescription: {
|
linkDescription: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#666666',
|
color: colors.chat.textTertiary,
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
@@ -1051,16 +1080,17 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 回复预览 - Telegram风格:简洁设计
|
// 回复预览 - Telegram风格:左侧色条 + 简洁背景
|
||||||
replyPreview: {
|
replyPreview: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
paddingHorizontal: spacing.sm + 2,
|
paddingHorizontal: spacing.sm + 2,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
// 移除阴影
|
gap: 8,
|
||||||
shadowColor: 'transparent',
|
shadowColor: 'transparent',
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowOpacity: 0,
|
shadowOpacity: 0,
|
||||||
@@ -1074,28 +1104,45 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(0, 0, 0, 0.03)',
|
backgroundColor: 'rgba(0, 0, 0, 0.03)',
|
||||||
},
|
},
|
||||||
replyLine: {
|
replyLine: {
|
||||||
width: 0,
|
width: 3,
|
||||||
marginRight: 0,
|
alignSelf: 'stretch',
|
||||||
|
minHeight: 28,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: colors.chat.link,
|
||||||
},
|
},
|
||||||
replyContent: {
|
replyContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
replySender: {
|
replySender: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
marginRight: 6,
|
marginRight: 6,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
replyText: {
|
replyText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#666666',
|
color: colors.chat.textTertiary,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type SegmentStyles = ReturnType<typeof createSegmentStyles>;
|
||||||
|
|
||||||
|
const SegmentStylesContext = React.createContext<SegmentStyles | null>(null);
|
||||||
|
|
||||||
|
function useSegmentStyles(): SegmentStyles {
|
||||||
|
const ctx = React.useContext(SegmentStylesContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useSegmentStyles 必须在 MessageSegmentsRenderer 内使用');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
export default MessageSegmentsRenderer;
|
export default MessageSegmentsRenderer;
|
||||||
@@ -8,7 +8,7 @@ import { View, StyleSheet, Animated } from 'react-native';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { colors } from '../../../../theme';
|
import { useAppColors } from '../../../../theme';
|
||||||
|
|
||||||
// 滑动阈值
|
// 滑动阈值
|
||||||
const SWIPE_THRESHOLD = 30;
|
const SWIPE_THRESHOLD = 30;
|
||||||
@@ -27,6 +27,7 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
|||||||
onReply,
|
onReply,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
const translateX = useRef(new Animated.Value(0)).current;
|
const translateX = useRef(new Animated.Value(0)).current;
|
||||||
const hasTriggeredRef = useRef(false);
|
const hasTriggeredRef = useRef(false);
|
||||||
|
|
||||||
|
|||||||
@@ -4,52 +4,40 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
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;
|
export const BUBBLE_RADIUS = 16;
|
||||||
|
|
||||||
// 气泡颜色方案(参考 Element X 的语义化颜色)
|
export function getBubbleColors(colors: AppColors) {
|
||||||
export const bubbleColors = {
|
return {
|
||||||
// 自己发送的消息
|
outgoing: {
|
||||||
outgoing: {
|
background: colors.chat.bubbleOutgoing,
|
||||||
background: '#E8E8E8', // Element X 风格:灰色系
|
text: colors.chat.textPrimary,
|
||||||
text: '#1A1A1A',
|
},
|
||||||
},
|
incoming: {
|
||||||
// 收到的消息
|
background: colors.chat.bubbleIncoming,
|
||||||
incoming: {
|
text: colors.chat.textPrimary,
|
||||||
background: '#FFFFFF',
|
},
|
||||||
text: '#1A1A1A',
|
replyHighlight: {
|
||||||
},
|
background: colors.chat.replyTint,
|
||||||
// 被回复的消息高亮
|
borderLeft: colors.chat.replyBorder,
|
||||||
replyHighlight: {
|
},
|
||||||
background: 'rgba(74, 136, 199, 0.1)',
|
};
|
||||||
borderLeft: '#4A88C7',
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 消息在组中的位置类型
|
|
||||||
export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last';
|
export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last';
|
||||||
|
|
||||||
/**
|
export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => {
|
||||||
* 根据消息在组中的位置获取圆角样式
|
|
||||||
* 参考 Element X 的动态圆角逻辑
|
|
||||||
*/
|
|
||||||
export const getBubbleBorderRadius = (
|
|
||||||
isMe: boolean,
|
|
||||||
position: MessageGroupPosition
|
|
||||||
): ViewStyle => {
|
|
||||||
const radius = BUBBLE_RADIUS;
|
const radius = BUBBLE_RADIUS;
|
||||||
|
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
// 自己发送的消息
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'single':
|
case 'single':
|
||||||
return {
|
return {
|
||||||
borderTopLeftRadius: radius,
|
borderTopLeftRadius: radius,
|
||||||
borderTopRightRadius: radius,
|
borderTopRightRadius: radius,
|
||||||
borderBottomLeftRadius: radius,
|
borderBottomLeftRadius: radius,
|
||||||
borderBottomRightRadius: 4, // 尖角在右下
|
borderBottomRightRadius: 4,
|
||||||
};
|
};
|
||||||
case 'first':
|
case 'first':
|
||||||
return {
|
return {
|
||||||
@@ -74,11 +62,10 @@ export const getBubbleBorderRadius = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 收到的消息
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'single':
|
case 'single':
|
||||||
return {
|
return {
|
||||||
borderTopLeftRadius: 4, // 尖角在左上
|
borderTopLeftRadius: 4,
|
||||||
borderTopRightRadius: radius,
|
borderTopRightRadius: radius,
|
||||||
borderBottomLeftRadius: radius,
|
borderBottomLeftRadius: radius,
|
||||||
borderBottomRightRadius: radius,
|
borderBottomRightRadius: radius,
|
||||||
@@ -108,9 +95,6 @@ export const getBubbleBorderRadius = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断消息在组中的位置
|
|
||||||
*/
|
|
||||||
export const getMessageGroupPosition = (
|
export const getMessageGroupPosition = (
|
||||||
index: number,
|
index: number,
|
||||||
messages: Array<{ sender_id: string }>,
|
messages: Array<{ sender_id: string }>,
|
||||||
@@ -118,13 +102,13 @@ export const getMessageGroupPosition = (
|
|||||||
): MessageGroupPosition => {
|
): MessageGroupPosition => {
|
||||||
const currentMessage = messages[index];
|
const currentMessage = messages[index];
|
||||||
if (!currentMessage) return 'single';
|
if (!currentMessage) return 'single';
|
||||||
|
|
||||||
const prevMessage = index > 0 ? messages[index - 1] : null;
|
const prevMessage = index > 0 ? messages[index - 1] : null;
|
||||||
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
|
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
|
||||||
|
|
||||||
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
|
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
|
||||||
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
|
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
|
||||||
|
|
||||||
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
|
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
|
||||||
return 'single';
|
return 'single';
|
||||||
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
|
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
|
||||||
@@ -136,135 +120,114 @@ export const getMessageGroupPosition = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否显示发送者信息(只在组的第一条消息显示)
|
|
||||||
*/
|
|
||||||
export const shouldShowSenderInfo = (
|
export const shouldShowSenderInfo = (
|
||||||
index: number,
|
index: number,
|
||||||
messages: Array<{ sender_id: string }>,
|
messages: Array<{ sender_id: string }>
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (index === 0) return true;
|
if (index === 0) return true;
|
||||||
|
|
||||||
const currentMessage = messages[index];
|
const currentMessage = messages[index];
|
||||||
const prevMessage = messages[index - 1];
|
const prevMessage = messages[index - 1];
|
||||||
|
|
||||||
return prevMessage.sender_id !== currentMessage.sender_id;
|
return prevMessage.sender_id !== currentMessage.sender_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle => {
|
||||||
* 获取气泡基础样式
|
const bc = getBubbleColors(colors);
|
||||||
*/
|
return {
|
||||||
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: {
|
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm + 4,
|
paddingVertical: spacing.sm + 4,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
},
|
maxWidth: '75%',
|
||||||
|
backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background,
|
||||||
// 自己发送的消息
|
shadowColor: colors.chat.shadow,
|
||||||
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',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.06,
|
shadowOpacity: 0.06,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
};
|
||||||
|
};
|
||||||
// 长按反馈阴影
|
|
||||||
longPressShadow: {
|
export const getBubbleTextStyle = (isMe: boolean, colors: AppColors): TextStyle => {
|
||||||
shadowColor: '#000',
|
const bc = getBubbleColors(colors);
|
||||||
shadowOffset: { width: 0, height: 3 },
|
return {
|
||||||
shadowOpacity: 0.12,
|
color: isMe ? bc.outgoing.text : bc.incoming.text,
|
||||||
shadowRadius: 8,
|
fontSize: 16,
|
||||||
elevation: 6,
|
lineHeight: 23,
|
||||||
},
|
letterSpacing: 0.2,
|
||||||
|
fontWeight: '400',
|
||||||
// 被回复消息的高亮边框
|
};
|
||||||
replyHighlight: {
|
};
|
||||||
borderLeftWidth: 3,
|
|
||||||
borderLeftColor: colors.primary.main,
|
export function createBubbleStyles(colors: AppColors) {
|
||||||
backgroundColor: bubbleColors.replyHighlight.background,
|
const bc = getBubbleColors(colors);
|
||||||
},
|
return StyleSheet.create({
|
||||||
|
bubble: {
|
||||||
// 已撤回消息
|
paddingHorizontal: spacing.md,
|
||||||
recalled: {
|
paddingVertical: spacing.sm + 4,
|
||||||
backgroundColor: '#F5F7FA',
|
minWidth: 60,
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: '#E8E8E8',
|
outgoing: {
|
||||||
borderStyle: 'dashed',
|
backgroundColor: bc.outgoing.background,
|
||||||
borderRadius: 12,
|
},
|
||||||
},
|
incoming: {
|
||||||
|
backgroundColor: bc.incoming.background,
|
||||||
// 系统通知
|
},
|
||||||
systemNotice: {
|
text: {
|
||||||
alignItems: 'center',
|
fontSize: 16,
|
||||||
marginVertical: spacing.sm,
|
lineHeight: 23,
|
||||||
},
|
letterSpacing: 0.2,
|
||||||
systemNoticeText: {
|
fontWeight: '400',
|
||||||
fontSize: 12,
|
},
|
||||||
color: '#8E8E93',
|
shadow: {
|
||||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
shadowColor: colors.chat.shadow,
|
||||||
paddingHorizontal: spacing.md,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
paddingVertical: spacing.xs,
|
shadowOpacity: 0.06,
|
||||||
borderRadius: 12,
|
shadowRadius: 2,
|
||||||
overflow: 'hidden',
|
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 {
|
export default {
|
||||||
BUBBLE_RADIUS,
|
BUBBLE_RADIUS,
|
||||||
bubbleColors,
|
getBubbleColors,
|
||||||
getBubbleBorderRadius,
|
getBubbleBorderRadius,
|
||||||
getMessageGroupPosition,
|
getMessageGroupPosition,
|
||||||
shouldShowSenderInfo,
|
shouldShowSenderInfo,
|
||||||
getBubbleBaseStyle,
|
getBubbleBaseStyle,
|
||||||
getBubbleTextStyle,
|
getBubbleTextStyle,
|
||||||
bubbleStyles,
|
createBubbleStyles,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export * from './types';
|
|||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
|
||||||
// 样式
|
// 样式
|
||||||
export { chatScreenStyles } from './styles';
|
export { createChatScreenStyles, useChatScreenStyles } from './styles';
|
||||||
|
|
||||||
// 组件
|
// 组件
|
||||||
export { EmojiPanel } from './EmojiPanel';
|
export { EmojiPanel } from './EmojiPanel';
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
* 支持响应式布局
|
* 支持响应式布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { StyleSheet, Dimensions } from 'react-native';
|
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');
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -18,19 +19,20 @@ const BREAKPOINTS = {
|
|||||||
const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop;
|
const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop;
|
||||||
const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet;
|
const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet;
|
||||||
|
|
||||||
export const chatScreenStyles = StyleSheet.create({
|
export function createChatScreenStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
// 容器
|
// 容器
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#F0F2F5',
|
backgroundColor: colors.chat.screen,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 头部
|
// 头部
|
||||||
headerContainer: {
|
headerContainer: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: 'rgba(255, 107, 53, 0.12)',
|
borderBottomColor: 'rgba(255, 107, 53, 0.12)',
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 3 },
|
shadowOffset: { width: 0, height: 3 },
|
||||||
shadowOpacity: 0.06,
|
shadowOpacity: 0.06,
|
||||||
shadowRadius: 10,
|
shadowRadius: 10,
|
||||||
@@ -42,7 +44,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm + 2,
|
paddingVertical: spacing.sm + 2,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
width: 38,
|
width: 38,
|
||||||
@@ -112,36 +114,61 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: 19,
|
borderRadius: 19,
|
||||||
backgroundColor: '#F7F8FA',
|
backgroundColor: colors.chat.surfaceRaised,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ECEFF3',
|
borderColor: colors.chat.borderLight,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 消息列表
|
// 消息列表
|
||||||
messageListContainer: {
|
messageListContainer: {
|
||||||
flex: 1,
|
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: {
|
listContent: {
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingTop: spacing.md,
|
paddingTop: spacing.md,
|
||||||
paddingBottom: spacing.xl,
|
paddingBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 时间分隔 - QQ风格:更明显的时间标签
|
// 时间分隔 - 轻量胶囊(弱化字重,接近 Telegram 日期条)
|
||||||
timeContainer: {
|
timeContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginVertical: spacing.lg,
|
marginVertical: spacing.lg,
|
||||||
},
|
},
|
||||||
timeText: {
|
timeText: {
|
||||||
color: '#8E8E93',
|
color: 'rgba(142, 142, 147, 0.95)',
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
backgroundColor: 'rgba(142, 142, 147, 0.1)',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.sm + 4,
|
||||||
paddingVertical: 6,
|
paddingVertical: 5,
|
||||||
borderRadius: 14,
|
borderRadius: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 系统通知
|
// 系统通知
|
||||||
@@ -152,7 +179,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
systemNoticeText: {
|
systemNoticeText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
@@ -177,7 +204,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
// 头像
|
// 头像
|
||||||
avatarWrapper: {
|
avatarWrapper: {
|
||||||
marginHorizontal: 2,
|
marginHorizontal: 2,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
@@ -185,7 +212,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
groupAvatarWrapper: {
|
groupAvatarWrapper: {
|
||||||
marginHorizontal: 2,
|
marginHorizontal: 2,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
@@ -200,53 +227,59 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
senderName: {
|
senderName: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 消息气泡 - Telegram风格:浅色背景,统一字体颜色
|
// 消息气泡 - 外层极轻阴影(纯文字气泡更接近扁平 IM)
|
||||||
messageBubble: {
|
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,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm + 4,
|
paddingVertical: spacing.sm + 4,
|
||||||
borderRadius: 16,
|
|
||||||
// 自适应内容宽度
|
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
},
|
},
|
||||||
myBubble: {
|
myBubbleInner: {
|
||||||
backgroundColor: '#DFF2FF', // Telegram风格的浅蓝色
|
backgroundColor: colors.chat.replyTint,
|
||||||
borderBottomRightRadius: 4, // 右下角尖,箭头在右下
|
borderBottomRightRadius: 4,
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
},
|
||||||
theirBubble: {
|
theirBubbleInner: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderTopLeftRadius: 4, // 左上角尖,箭头在左上
|
borderTopLeftRadius: 4,
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
},
|
||||||
recalledBubble: {
|
recalledBubble: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#E8E8E8',
|
borderColor: colors.chat.border,
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
},
|
},
|
||||||
myBubbleText: {
|
myBubbleText: {
|
||||||
color: '#1A1A1A', // 统一使用深色字体
|
color: colors.chat.textPrimary, // 主文案
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
lineHeight: 23,
|
lineHeight: 23,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
theirBubbleText: {
|
theirBubbleText: {
|
||||||
color: '#1A1A1A', // 统一使用深色字体
|
color: colors.chat.textPrimary, // 主文案
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
lineHeight: 23,
|
lineHeight: 23,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
@@ -255,29 +288,27 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
// 选中状态
|
// 选中状态
|
||||||
selectedBubble: {
|
selectedBubble: {
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#7FB6E6',
|
borderColor: colors.chat.replyBorder,
|
||||||
},
|
},
|
||||||
|
// 仅对齐右侧,不再套一层浅色底,避免与气泡叠色产生「边缘多出一块蓝」
|
||||||
myMessageContentPanel: {
|
myMessageContentPanel: {
|
||||||
backgroundColor: '#DFF2FF',
|
alignItems: 'flex-end',
|
||||||
borderRadius: 14,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
},
|
},
|
||||||
mySelectedBubble: {
|
mySelectedBubble: {
|
||||||
backgroundColor: '#CFEAFF',
|
backgroundColor: colors.chat.replyTintActive,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#7FB6E6',
|
borderColor: colors.chat.replyBorder,
|
||||||
},
|
},
|
||||||
theirSelectedBubble: {
|
theirSelectedBubble: {
|
||||||
backgroundColor: '#EEF5FC',
|
backgroundColor: colors.chat.menuHighlight,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#7FB6E6',
|
borderColor: colors.chat.replyBorder,
|
||||||
},
|
},
|
||||||
selectedText: {
|
selectedText: {
|
||||||
// 文本选中时的样式
|
// 文本选中时的样式
|
||||||
},
|
},
|
||||||
recalledText: {
|
recalledText: {
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -285,7 +316,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
imageBubble: {
|
imageBubble: {
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 3 },
|
shadowOffset: { width: 0, height: 3 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 6,
|
shadowRadius: 6,
|
||||||
@@ -314,27 +345,28 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
readStatusText: {
|
readStatusText: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
readStatusTextRead: {
|
readStatusTextRead: {
|
||||||
color: '#34C759',
|
color: colors.chat.success,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 输入框区域
|
// 输入框区域 - 与列表背景同色底栏,顶部细分隔
|
||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: colors.chat.screen,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderTopColor: 'rgba(0, 0, 0, 0.06)',
|
||||||
},
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: 'transparent',
|
||||||
borderWidth: 1,
|
borderWidth: 0,
|
||||||
borderColor: colors.divider,
|
borderRadius: 0,
|
||||||
borderRadius: 24,
|
marginHorizontal: 10,
|
||||||
marginHorizontal: 14,
|
marginBottom: 10,
|
||||||
marginBottom: 12,
|
marginTop: 2,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
// 移除明显的阴影效果,改用更微妙的边框
|
|
||||||
shadowColor: 'transparent',
|
shadowColor: 'transparent',
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowOpacity: 0,
|
shadowOpacity: 0,
|
||||||
@@ -344,7 +376,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
inputInner: {
|
inputInner: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F7F8FA',
|
backgroundColor: colors.chat.surfaceRaised,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingHorizontal: spacing.xs,
|
paddingHorizontal: spacing.xs,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
@@ -357,7 +389,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
},
|
},
|
||||||
inputInnerMuted: {
|
inputInnerMuted: {
|
||||||
backgroundColor: '#F0F0F0',
|
backgroundColor: colors.chat.borderHairline,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -366,7 +398,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#FFF5F5',
|
backgroundColor: colors.chat.warningBg,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
@@ -375,7 +407,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
mutedBannerText: {
|
mutedBannerText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#FF3B30',
|
color: colors.chat.danger,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -421,14 +453,14 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
maxHeight: 100,
|
maxHeight: 100,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
inputMuted: {
|
inputMuted: {
|
||||||
color: '#CCC',
|
color: colors.chat.iconMuted,
|
||||||
},
|
},
|
||||||
inputTransparent: {
|
inputTransparent: {
|
||||||
color: 'transparent',
|
color: 'transparent',
|
||||||
@@ -444,7 +476,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
inputHighlightText: {
|
inputHighlightText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
inputHighlightMention: {
|
inputHighlightMention: {
|
||||||
@@ -462,10 +494,10 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
|
|
||||||
// 面板
|
// 面板
|
||||||
panelWrapper: {
|
panelWrapper: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#E8E8E8',
|
borderTopColor: colors.chat.border,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: -2 },
|
shadowOffset: { width: 0, height: -2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
@@ -473,11 +505,11 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
// 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充
|
// 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充
|
||||||
emojiPanelWrapper: {
|
emojiPanelWrapper: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
},
|
},
|
||||||
panelContainer: {
|
panelContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表情面板
|
// 表情面板
|
||||||
@@ -505,16 +537,16 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#E8E8E8',
|
borderTopColor: colors.chat.border,
|
||||||
},
|
},
|
||||||
panelCloseButton: {
|
panelCloseButton: {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
@@ -533,8 +565,8 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#E8E8E8',
|
borderTopColor: colors.chat.border,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
},
|
},
|
||||||
panelTab: {
|
panelTab: {
|
||||||
width: 44,
|
width: 44,
|
||||||
@@ -545,7 +577,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
marginRight: spacing.xs,
|
marginRight: spacing.xs,
|
||||||
},
|
},
|
||||||
panelTabActive: {
|
panelTabActive: {
|
||||||
backgroundColor: '#DFF2FF',
|
backgroundColor: colors.chat.replyTint,
|
||||||
},
|
},
|
||||||
panelTabEmoji: {
|
panelTabEmoji: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
@@ -553,7 +585,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
panelTabDivider: {
|
panelTabDivider: {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 24,
|
height: 24,
|
||||||
backgroundColor: '#E8E8E8',
|
backgroundColor: colors.chat.bubbleOutgoing,
|
||||||
marginHorizontal: spacing.sm,
|
marginHorizontal: spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -571,13 +603,13 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
stickerEmptyText: {
|
stickerEmptyText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: colors.chat.textTertiary,
|
||||||
marginTop: spacing.md,
|
marginTop: spacing.md,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
stickerEmptySubText: {
|
stickerEmptySubText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#999',
|
color: colors.chat.textMuted,
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
},
|
},
|
||||||
stickerGrid: {
|
stickerGrid: {
|
||||||
@@ -593,7 +625,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
margin: 4,
|
margin: 4,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: '#F5F5F5',
|
backgroundColor: colors.chat.surfaceInput,
|
||||||
},
|
},
|
||||||
stickerImage: {
|
stickerImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -618,8 +650,8 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@@ -627,7 +659,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
moreItemText: {
|
moreItemText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#666',
|
color: colors.chat.textTertiary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -636,7 +668,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 12,
|
left: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
shadowColor: colors.primary.main,
|
shadowColor: colors.primary.main,
|
||||||
shadowOffset: { width: 0, height: -3 },
|
shadowOffset: { width: 0, height: -3 },
|
||||||
@@ -651,7 +683,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
// @成员选择面板
|
// @成员选择面板
|
||||||
mentionPanelContainer: {
|
mentionPanelContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
},
|
},
|
||||||
mentionPanelDragHandle: {
|
mentionPanelDragHandle: {
|
||||||
@@ -663,7 +695,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 3,
|
height: 3,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
backgroundColor: '#E0E0E0',
|
backgroundColor: colors.chat.sheetGrip,
|
||||||
},
|
},
|
||||||
mentionPanelHeader: {
|
mentionPanelHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -673,12 +705,12 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#F0F0F0',
|
borderBottomColor: colors.chat.borderHairline,
|
||||||
},
|
},
|
||||||
mentionPanelTitle: {
|
mentionPanelTitle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#AAAAAA',
|
color: colors.chat.textPlaceholder,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
@@ -686,7 +718,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
width: 26,
|
width: 26,
|
||||||
height: 26,
|
height: 26,
|
||||||
borderRadius: 13,
|
borderRadius: 13,
|
||||||
backgroundColor: '#F5F5F5',
|
backgroundColor: colors.chat.surfaceInput,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -728,7 +760,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
mentionItemName: {
|
mentionItemName: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
},
|
},
|
||||||
mentionItemNameAll: {
|
mentionItemNameAll: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
@@ -737,7 +769,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
mentionItemSub: {
|
mentionItemSub: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#BBBBBB',
|
color: colors.chat.iconMuted,
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
},
|
},
|
||||||
mentionItemRole: {
|
mentionItemRole: {
|
||||||
@@ -752,7 +784,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
mentionEmptyText: {
|
mentionEmptyText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#C8C8C8',
|
color: colors.chat.iconSoft,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 长按菜单 - 底部横条形式(类似QQ)
|
// 长按菜单 - 底部横条形式(类似QQ)
|
||||||
@@ -763,10 +795,10 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
// 旧版菜单样式(保留兼容)
|
// 旧版菜单样式(保留兼容)
|
||||||
menuContainer: {
|
menuContainer: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 12,
|
shadowRadius: 12,
|
||||||
@@ -781,21 +813,21 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
menuItemBorder: {
|
menuItemBorder: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#F0F0F0',
|
borderBottomColor: colors.chat.borderHairline,
|
||||||
},
|
},
|
||||||
menuItemText: {
|
menuItemText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
// 底部菜单容器
|
// 底部菜单容器
|
||||||
bottomMenuContainer: {
|
bottomMenuContainer: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
paddingTop: spacing.md,
|
paddingTop: spacing.md,
|
||||||
paddingBottom: spacing.xl,
|
paddingBottom: spacing.xl,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: -4 },
|
shadowOffset: { width: 0, height: -4 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
@@ -803,7 +835,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
// 消息预览区域
|
// 消息预览区域
|
||||||
messagePreviewContainer: {
|
messagePreviewContainer: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
marginHorizontal: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
@@ -813,7 +845,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
messagePreviewText: {
|
messagePreviewText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: colors.chat.textTertiary,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
// 操作按钮横条
|
// 操作按钮横条
|
||||||
@@ -824,7 +856,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#F0F0F0',
|
borderBottomColor: colors.chat.borderHairline,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
menuActionItem: {
|
menuActionItem: {
|
||||||
@@ -839,19 +871,19 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
backgroundColor: '#F0F7FF',
|
backgroundColor: colors.chat.overlayQuote,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
menuActionLabel: {
|
menuActionLabel: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
// 取消按钮
|
// 取消按钮
|
||||||
menuCancelButton: {
|
menuCancelButton: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
marginHorizontal: spacing.md,
|
marginHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
@@ -859,7 +891,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
menuCancelText: {
|
menuCancelText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#333',
|
color: colors.text.primary,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -874,7 +906,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 2,
|
paddingHorizontal: 2,
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@@ -901,20 +933,21 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
qqMenuItemLabel: {
|
qqMenuItemLabel: {
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: '#FFFFFF',
|
color: colors.primary.contrast,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 回复预览
|
// 回复预览(输入区)- 与气泡内引用同色条
|
||||||
replyPreviewContainer: {
|
replyPreviewContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: 'rgba(74, 136, 199, 0.08)',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
borderLeftWidth: 3,
|
borderLeftWidth: 3,
|
||||||
borderLeftColor: colors.primary.main,
|
borderLeftColor: colors.chat.link,
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
replyPreviewContent: {
|
replyPreviewContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -932,7 +965,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
replyPreviewMessage: {
|
replyPreviewMessage: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
replyPreviewClose: {
|
replyPreviewClose: {
|
||||||
@@ -948,11 +981,11 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
overlayContent: {
|
overlayContent: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
padding: spacing.xl,
|
padding: spacing.xl,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
shadowColor: '#000',
|
shadowColor: colors.chat.shadow,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 12,
|
shadowRadius: 12,
|
||||||
@@ -961,13 +994,13 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
overlayText: {
|
overlayText: {
|
||||||
marginTop: spacing.md,
|
marginTop: spacing.md,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#666',
|
color: colors.chat.textTertiary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表情包添加按钮(管理界面第一位)
|
// 表情包添加按钮(管理界面第一位)
|
||||||
stickerAddButton: {
|
stickerAddButton: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: colors.primary.main,
|
borderColor: colors.primary.main,
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
@@ -986,9 +1019,9 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
|
|
||||||
// 表情包管理按钮(表情面板第一位)
|
// 表情包管理按钮(表情面板第一位)
|
||||||
stickerManageButton: {
|
stickerManageButton: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#E8E8E8',
|
borderColor: colors.chat.border,
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -1000,7 +1033,7 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
stickerManageText: {
|
stickerManageText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#666',
|
color: colors.chat.textTertiary,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
@@ -1018,20 +1051,20 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 5,
|
height: 5,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor: '#E0E0E0',
|
backgroundColor: colors.chat.sheetGrip,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
manageHeaderDivider: {
|
manageHeaderDivider: {
|
||||||
height: StyleSheet.hairlineWidth,
|
height: StyleSheet.hairlineWidth,
|
||||||
backgroundColor: '#F0F0F0',
|
backgroundColor: colors.chat.borderHairline,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表情包选择状态
|
// 表情包选择状态
|
||||||
stickerItemSelected: {
|
stickerItemSelected: {
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#7FB6E6',
|
borderColor: colors.chat.replyBorder,
|
||||||
},
|
},
|
||||||
stickerCheckOverlay: {
|
stickerCheckOverlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
@@ -1045,19 +1078,19 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
height: 22,
|
height: 22,
|
||||||
borderRadius: 11,
|
borderRadius: 11,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#FFF',
|
borderColor: colors.chat.card,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
stickerCheckBoxSelected: {
|
stickerCheckBoxSelected: {
|
||||||
backgroundColor: '#7FB6E6',
|
backgroundColor: colors.chat.replyBorder,
|
||||||
borderColor: '#7FB6E6',
|
borderColor: colors.chat.replyBorder,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表情管理模态框
|
// 表情管理模态框
|
||||||
manageModalContainer: {
|
manageModalContainer: {
|
||||||
backgroundColor: '#F5F7FA',
|
backgroundColor: colors.chat.surfaceMuted,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -1068,9 +1101,9 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#E8E8E8',
|
borderBottomColor: colors.chat.border,
|
||||||
},
|
},
|
||||||
manageHeaderButton: {
|
manageHeaderButton: {
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
@@ -1079,28 +1112,28 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
manageModalTitle: {
|
manageModalTitle: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#1A1A1A',
|
color: colors.chat.textPrimary,
|
||||||
},
|
},
|
||||||
manageDoneText: {
|
manageDoneText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
manageSelectText: {
|
manageSelectText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
manageInfoBar: {
|
manageInfoBar: {
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#E8E8E8',
|
borderBottomColor: colors.chat.border,
|
||||||
},
|
},
|
||||||
manageInfoText: {
|
manageInfoText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#8E8E93',
|
color: colors.chat.textSecondary,
|
||||||
},
|
},
|
||||||
manageStickerGrid: {
|
manageStickerGrid: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -1115,9 +1148,9 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: colors.chat.card,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#E8E8E8',
|
borderTopColor: colors.chat.border,
|
||||||
paddingBottom: 34, // 安全区域
|
paddingBottom: 34, // 安全区域
|
||||||
},
|
},
|
||||||
manageSelectAllButton: {
|
manageSelectAllButton: {
|
||||||
@@ -1126,24 +1159,24 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
manageSelectAllText: {
|
manageSelectAllText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#4A88C7',
|
color: colors.chat.link,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
manageDeleteButton: {
|
manageDeleteButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#FF3B30',
|
backgroundColor: colors.chat.danger,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingVertical: spacing.sm + 2,
|
paddingVertical: spacing.sm + 2,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
gap: 6,
|
gap: 6,
|
||||||
},
|
},
|
||||||
manageDeleteButtonDisabled: {
|
manageDeleteButtonDisabled: {
|
||||||
backgroundColor: '#CCC',
|
backgroundColor: colors.chat.iconMuted,
|
||||||
},
|
},
|
||||||
manageDeleteText: {
|
manageDeleteText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#FFFFFF',
|
color: colors.primary.contrast,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
manageTipBar: {
|
manageTipBar: {
|
||||||
@@ -1152,12 +1185,12 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
backgroundColor: '#FFF9E6',
|
backgroundColor: colors.chat.tipBg,
|
||||||
gap: 6,
|
gap: 6,
|
||||||
},
|
},
|
||||||
manageTipText: {
|
manageTipText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#999',
|
color: colors.chat.textMuted,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 管理界面空状态
|
// 管理界面空状态
|
||||||
@@ -1186,5 +1219,9 @@ export const chatScreenStyles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default chatScreenStyles;
|
export function useChatScreenStyles() {
|
||||||
|
const colors = useAppColors();
|
||||||
|
return useMemo(() => createChatScreenStyles(colors), [colors]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -546,6 +546,13 @@ export const useChatScreen = () => {
|
|||||||
isBrowsingHistoryRef.current = false;
|
isBrowsingHistoryRef.current = false;
|
||||||
}, [isHistoryLoadingLocked]);
|
}, [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) => {
|
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||||
isBrowsingHistoryRef.current = browsing;
|
isBrowsingHistoryRef.current = browsing;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -1396,6 +1403,7 @@ export const useChatScreen = () => {
|
|||||||
handleClearConversation,
|
handleClearConversation,
|
||||||
handleMessageListContentSizeChange,
|
handleMessageListContentSizeChange,
|
||||||
handleReachLatestEdge,
|
handleReachLatestEdge,
|
||||||
|
jumpToLatestMessages,
|
||||||
setBrowsingHistory,
|
setBrowsingHistory,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
* onPress 引用不参与比较,请配合父组件 ref + 按 id 查最新会话使用。
|
* 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 { Animated, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { colors, spacing, shadows } from '../../../theme';
|
import { spacing, shadows, useAppColors, type AppColors } from '../../../theme';
|
||||||
import {
|
import {
|
||||||
ConversationResponse,
|
ConversationResponse,
|
||||||
extractTextFromSegments,
|
extractTextFromSegments,
|
||||||
@@ -150,6 +150,124 @@ function areConversationRowEqual(
|
|||||||
return true;
|
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<ConversationListRowProps> = ({
|
const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||||
item,
|
item,
|
||||||
scale,
|
scale,
|
||||||
@@ -158,6 +276,8 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
formatTime,
|
formatTime,
|
||||||
systemChannelId,
|
systemChannelId,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createConversationRowStyles(colors), [colors]);
|
||||||
const isSystemChannel = item.id === systemChannelId;
|
const isSystemChannel = item.id === systemChannelId;
|
||||||
const isGroupChat = !!(item.type === 'group' && item.group);
|
const isGroupChat = !!(item.type === 'group' && item.group);
|
||||||
|
|
||||||
@@ -196,7 +316,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
{isSystemChannel ? (
|
{isSystemChannel ? (
|
||||||
<View style={styles.systemAvatar}>
|
<View style={styles.systemAvatar}>
|
||||||
<MaterialCommunityIcons name="bell-ring" size={24} color="#FFF" />
|
<MaterialCommunityIcons name="bell-ring" size={24} color={colors.primary.contrast} />
|
||||||
</View>
|
</View>
|
||||||
) : isGroupChat ? (
|
) : isGroupChat ? (
|
||||||
<View style={styles.groupAvatar}>
|
<View style={styles.groupAvatar}>
|
||||||
@@ -204,7 +324,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.groupAvatarPlaceholder}>
|
<View style={styles.groupAvatarPlaceholder}>
|
||||||
<MaterialCommunityIcons name="account-group" size={28} color="#FFF" />
|
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -271,119 +391,3 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
ConversationListRowInner.displayName = 'ConversationListRow';
|
ConversationListRowInner.displayName = 'ConversationListRow';
|
||||||
|
|
||||||
export const ConversationListRow = memo(ConversationListRowInner, areConversationRowEqual);
|
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 嵌入式聊天组件 - 用于桌面端双栏布局右侧
|
* 嵌入式聊天组件 - 用于桌面端双栏布局右侧
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
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 { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto';
|
||||||
import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common';
|
import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common';
|
||||||
import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores';
|
import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores';
|
||||||
@@ -34,12 +34,210 @@ import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
|
|||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth } = Dimensions.get('window');
|
||||||
const PANEL_WIDTH = 360;
|
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 {
|
interface EmbeddedChatProps {
|
||||||
conversation: ConversationResponse;
|
conversation: ConversationResponse;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack }) => {
|
export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack }) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createEmbeddedChatStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentUser = useAuthStore(state => state.currentUser);
|
const currentUser = useAuthStore(state => state.currentUser);
|
||||||
|
|
||||||
@@ -411,7 +609,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{/* 大屏幕(>= 768px)时隐藏返回按钮 */}
|
{/* 大屏幕(>= 768px)时隐藏返回按钮 */}
|
||||||
{!isWideScreen ? (
|
{!isWideScreen ? (
|
||||||
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor="#666" />
|
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor={colors.text.secondary} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.headerButton} />
|
<View style={styles.headerButton} />
|
||||||
)}
|
)}
|
||||||
@@ -426,11 +624,11 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
{/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */}
|
{/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */}
|
||||||
{isWideScreen && isGroupChat ? (
|
{isWideScreen && isGroupChat ? (
|
||||||
<TouchableOpacity onPress={toggleGroupInfoPanel} style={styles.headerButton}>
|
<TouchableOpacity onPress={toggleGroupInfoPanel} style={styles.headerButton}>
|
||||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color="#666" />
|
<MaterialCommunityIcons name="dots-horizontal" size={22} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity onPress={handleNavigateToFullChat} style={styles.headerButton}>
|
<TouchableOpacity onPress={handleNavigateToFullChat} style={styles.headerButton}>
|
||||||
<MaterialCommunityIcons name="arrow-expand" size={22} color="#666" />
|
<MaterialCommunityIcons name="arrow-expand" size={22} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -443,7 +641,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
</View>
|
</View>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<MaterialCommunityIcons name="message-text-outline" size={48} color="#DDD" />
|
<MaterialCommunityIcons name="message-text-outline" size={48} color={colors.text.disabled} />
|
||||||
<Text style={styles.emptyText}>暂无消息</Text>
|
<Text style={styles.emptyText}>暂无消息</Text>
|
||||||
<Text style={styles.emptySubtext}>发送第一条消息开始聊天</Text>
|
<Text style={styles.emptySubtext}>发送第一条消息开始聊天</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -472,7 +670,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
<View style={styles.inputArea}>
|
<View style={styles.inputArea}>
|
||||||
<View style={styles.inputRow}>
|
<View style={styles.inputRow}>
|
||||||
<TouchableOpacity style={styles.iconButton}>
|
<TouchableOpacity style={styles.iconButton}>
|
||||||
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
|
<MaterialCommunityIcons name="plus-circle-outline" size={28} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
@@ -480,7 +678,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={styles.textInput}
|
style={styles.textInput}
|
||||||
placeholder="发送消息..."
|
placeholder="发送消息..."
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={colors.text.hint}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChangeText={setInputText}
|
onChangeText={setInputText}
|
||||||
multiline={false}
|
multiline={false}
|
||||||
@@ -494,7 +692,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.iconButton}>
|
<TouchableOpacity style={styles.iconButton}>
|
||||||
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
|
<MaterialCommunityIcons name="emoticon-outline" size={28} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -503,9 +701,9 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
disabled={!inputText.trim() || sending}
|
disabled={!inputText.trim() || sending}
|
||||||
>
|
>
|
||||||
{sending ? (
|
{sending ? (
|
||||||
<ActivityIndicator size="small" color="#FFF" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<MaterialCommunityIcons name="send" size={20} color="#FFF" />
|
<MaterialCommunityIcons name="send" size={20} color={colors.text.inverse} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -604,190 +802,3 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
|
||||||
import { Avatar, Text } from '../../../components/common';
|
import { Avatar, Text } from '../../../components/common';
|
||||||
import { colors, spacing, borderRadius, shadows } from '../../../theme';
|
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../../theme';
|
||||||
|
|
||||||
interface GroupInfoSummaryCardProps {
|
interface GroupInfoSummaryCardProps {
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
@@ -14,6 +14,79 @@ interface GroupInfoSummaryCardProps {
|
|||||||
memberCountText?: string;
|
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<GroupInfoSummaryCardProps> = ({
|
export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
|
||||||
groupName,
|
groupName,
|
||||||
groupAvatar,
|
groupAvatar,
|
||||||
@@ -21,12 +94,16 @@ export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
|
|||||||
groupDescription,
|
groupDescription,
|
||||||
memberCountText,
|
memberCountText,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
|
||||||
return (
|
return (
|
||||||
<View style={styles.headerCard}>
|
<View style={styles.headerCard}>
|
||||||
<View style={styles.groupHeader}>
|
<View style={styles.groupHeader}>
|
||||||
<Avatar source={groupAvatar || ''} size={88} name={groupName} />
|
<Avatar source={groupAvatar || ''} size={88} name={groupName} />
|
||||||
<View style={styles.groupInfo}>
|
<View style={styles.groupInfo}>
|
||||||
<Text variant="h3" style={styles.groupName} numberOfLines={1}>{groupName || '群聊'}</Text>
|
<Text variant="h3" style={styles.groupName} numberOfLines={1}>
|
||||||
|
{groupName || '群聊'}
|
||||||
|
</Text>
|
||||||
<View style={styles.groupMeta}>
|
<View style={styles.groupMeta}>
|
||||||
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} />
|
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} />
|
||||||
<Text variant="caption" color={colors.text.secondary}>
|
<Text variant="caption" color={colors.text.secondary}>
|
||||||
@@ -63,93 +140,34 @@ export const DecisionFooter: React.FC<DecisionFooterProps> = ({
|
|||||||
onApprove,
|
onApprove,
|
||||||
processedText = '该请求已处理',
|
processedText = '该请求已处理',
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
if (canAction) {
|
if (canAction) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
||||||
<TouchableOpacity style={[styles.btn, styles.reject]} onPress={onReject} disabled={submitting}>
|
<TouchableOpacity style={[styles.btn, styles.reject]} onPress={onReject} disabled={submitting}>
|
||||||
<Text variant="body" color={colors.error.main}>拒绝</Text>
|
<Text variant="body" color={colors.error.main}>
|
||||||
|
拒绝
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.btn, styles.approve]} onPress={onApprove} disabled={submitting}>
|
<TouchableOpacity style={[styles.btn, styles.approve]} onPress={onApprove} disabled={submitting}>
|
||||||
{submitting ? <ActivityIndicator color="#fff" /> : <Text variant="body" color="#fff">同意</Text>}
|
{submitting ? (
|
||||||
|
<ActivityIndicator color={colors.text.inverse} />
|
||||||
|
) : (
|
||||||
|
<Text variant="body" color={colors.text.inverse}>
|
||||||
|
同意
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={[styles.statusBox, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
<View style={[styles.statusBox, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
||||||
<Text variant="caption" color={colors.text.secondary}>{processedText}</Text>
|
<Text variant="caption" color={colors.text.secondary}>
|
||||||
|
{processedText}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { authService } from '../../../services/authService';
|
||||||
import { useAuthStore } from '../../../stores';
|
import { useAuthStore } from '../../../stores';
|
||||||
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
|
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
|
||||||
@@ -41,6 +41,8 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createMutualFollowSelectorStyles(colors), [colors]);
|
||||||
const { currentUser } = useAuthStore();
|
const { currentUser } = useAuthStore();
|
||||||
|
|
||||||
const [friendList, setFriendList] = useState<User[]>([]);
|
const [friendList, setFriendList] = useState<User[]>([]);
|
||||||
@@ -209,93 +211,96 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createMutualFollowSelectorStyles(colors: AppColors) {
|
||||||
modalOverlay: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
modalOverlay: {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
flex: 1,
|
||||||
justifyContent: 'flex-end',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
},
|
justifyContent: 'flex-end',
|
||||||
modalContent: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
modalContent: {
|
||||||
borderTopLeftRadius: borderRadius['2xl'],
|
backgroundColor: colors.background.paper,
|
||||||
borderTopRightRadius: borderRadius['2xl'],
|
borderTopLeftRadius: borderRadius['2xl'],
|
||||||
height: '80%',
|
borderTopRightRadius: borderRadius['2xl'],
|
||||||
paddingHorizontal: spacing.lg,
|
height: '80%',
|
||||||
paddingTop: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.xl,
|
paddingTop: spacing.lg,
|
||||||
},
|
paddingBottom: spacing.xl,
|
||||||
modalHeader: {
|
},
|
||||||
flexDirection: 'row',
|
modalHeader: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: spacing.lg,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.lg,
|
||||||
modalHeaderButton: {
|
},
|
||||||
paddingVertical: spacing.sm,
|
modalHeaderButton: {
|
||||||
minWidth: 60,
|
paddingVertical: spacing.sm,
|
||||||
},
|
minWidth: 60,
|
||||||
modalTitle: {
|
},
|
||||||
fontWeight: '700',
|
modalTitle: {
|
||||||
fontSize: fontSizes.xl,
|
fontWeight: '700',
|
||||||
},
|
fontSize: fontSizes.xl,
|
||||||
confirmButton: {
|
color: colors.text.primary,
|
||||||
fontWeight: '600',
|
},
|
||||||
textAlign: 'right',
|
confirmButton: {
|
||||||
},
|
fontWeight: '600',
|
||||||
tipContainer: {
|
textAlign: 'right',
|
||||||
flexDirection: 'row',
|
},
|
||||||
backgroundColor: colors.background.default,
|
tipContainer: {
|
||||||
borderRadius: borderRadius.lg,
|
flexDirection: 'row',
|
||||||
padding: spacing.xs,
|
backgroundColor: colors.background.default,
|
||||||
marginBottom: spacing.md,
|
borderRadius: borderRadius.lg,
|
||||||
},
|
padding: spacing.xs,
|
||||||
tipText: {
|
marginBottom: spacing.md,
|
||||||
fontWeight: '600',
|
},
|
||||||
},
|
tipText: {
|
||||||
selectedBadge: {
|
fontWeight: '600',
|
||||||
backgroundColor: colors.primary.light + '20',
|
},
|
||||||
paddingHorizontal: spacing.md,
|
selectedBadge: {
|
||||||
paddingVertical: spacing.xs,
|
backgroundColor: colors.primary.light + '20',
|
||||||
borderRadius: borderRadius.sm,
|
paddingHorizontal: spacing.md,
|
||||||
alignSelf: 'flex-start',
|
paddingVertical: spacing.xs,
|
||||||
marginBottom: spacing.md,
|
borderRadius: borderRadius.sm,
|
||||||
},
|
alignSelf: 'flex-start',
|
||||||
friendListContent: {
|
marginBottom: spacing.md,
|
||||||
paddingBottom: spacing.xl,
|
},
|
||||||
},
|
friendListContent: {
|
||||||
friendItem: {
|
paddingBottom: spacing.xl,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
friendItem: {
|
||||||
paddingVertical: spacing.md,
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.sm,
|
alignItems: 'center',
|
||||||
borderRadius: borderRadius.lg,
|
paddingVertical: spacing.md,
|
||||||
marginBottom: spacing.xs,
|
paddingHorizontal: spacing.sm,
|
||||||
},
|
borderRadius: borderRadius.lg,
|
||||||
friendItemSelected: {
|
marginBottom: spacing.xs,
|
||||||
backgroundColor: colors.primary.light + '15',
|
},
|
||||||
},
|
friendItemSelected: {
|
||||||
friendInfo: {
|
backgroundColor: colors.primary.light + '15',
|
||||||
flex: 1,
|
},
|
||||||
marginLeft: spacing.md,
|
friendInfo: {
|
||||||
marginRight: spacing.sm,
|
flex: 1,
|
||||||
},
|
marginLeft: spacing.md,
|
||||||
nickname: {
|
marginRight: spacing.sm,
|
||||||
fontWeight: '500',
|
},
|
||||||
marginBottom: 2,
|
nickname: {
|
||||||
},
|
fontWeight: '500',
|
||||||
checkbox: {
|
marginBottom: 2,
|
||||||
width: 26,
|
},
|
||||||
height: 26,
|
checkbox: {
|
||||||
borderRadius: 13,
|
width: 26,
|
||||||
borderWidth: 2,
|
height: 26,
|
||||||
borderColor: colors.divider,
|
borderRadius: 13,
|
||||||
justifyContent: 'center',
|
borderWidth: 2,
|
||||||
alignItems: 'center',
|
borderColor: colors.divider,
|
||||||
},
|
justifyContent: 'center',
|
||||||
checkboxSelected: {
|
alignItems: 'center',
|
||||||
backgroundColor: colors.primary.main,
|
},
|
||||||
borderColor: colors.primary.main,
|
checkboxSelected: {
|
||||||
},
|
backgroundColor: colors.primary.main,
|
||||||
});
|
borderColor: colors.primary.main,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default MutualFollowSelectorModal;
|
export default MutualFollowSelectorModal;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { Text, ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { authService, resolveAuthApiError } from '../../services/authService';
|
import { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
@@ -17,6 +17,8 @@ import { showPrompt } from '../../services/promptService';
|
|||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
|
|
||||||
export const AccountSecurityScreen: React.FC = () => {
|
export const AccountSecurityScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]);
|
||||||
const { isWideScreen, isMobile } = useResponsive();
|
const { isWideScreen, isMobile } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const currentUser = useAuthStore((state) => state.currentUser);
|
const currentUser = useAuthStore((state) => state.currentUser);
|
||||||
@@ -232,7 +234,7 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
disabled={sendingCode || countdown > 0}
|
disabled={sendingCode || countdown > 0}
|
||||||
>
|
>
|
||||||
{sendingCode ? (
|
{sendingCode ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||||||
)}
|
)}
|
||||||
@@ -244,7 +246,11 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
onPress={handleVerifyEmail}
|
onPress={handleVerifyEmail}
|
||||||
disabled={verifyingEmail}
|
disabled={verifyingEmail}
|
||||||
>
|
>
|
||||||
{verifyingEmail ? <ActivityIndicator size="small" color="#fff" /> : <Text style={styles.primaryButtonText}>验证邮箱</Text>}
|
{verifyingEmail ? (
|
||||||
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -298,7 +304,7 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
||||||
>
|
>
|
||||||
{sendingChangePwdCode ? (
|
{sendingChangePwdCode ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.sendCodeButtonText}>
|
<Text style={styles.sendCodeButtonText}>
|
||||||
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
||||||
@@ -323,7 +329,11 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
onPress={handleChangePassword}
|
onPress={handleChangePassword}
|
||||||
disabled={updatingPassword}
|
disabled={updatingPassword}
|
||||||
>
|
>
|
||||||
{updatingPassword ? <ActivityIndicator size="small" color="#fff" /> : <Text style={styles.primaryButtonText}>更新密码</Text>}
|
{updatingPassword ? (
|
||||||
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.primaryButtonText}>更新密码</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -343,109 +353,117 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createAccountSecurityStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
scrollContent: {
|
},
|
||||||
paddingVertical: spacing.md,
|
scrollContent: {
|
||||||
},
|
paddingVertical: spacing.md,
|
||||||
section: {
|
},
|
||||||
marginBottom: spacing.lg,
|
section: {
|
||||||
},
|
marginBottom: spacing.lg,
|
||||||
sectionHeader: {
|
},
|
||||||
flexDirection: 'row',
|
sectionHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.lg,
|
alignItems: 'center',
|
||||||
marginBottom: spacing.sm,
|
paddingHorizontal: spacing.lg,
|
||||||
gap: spacing.xs,
|
marginBottom: spacing.sm,
|
||||||
},
|
gap: spacing.xs,
|
||||||
sectionTitle: {
|
},
|
||||||
fontWeight: '600',
|
sectionTitle: {
|
||||||
},
|
fontWeight: '600',
|
||||||
card: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
card: {
|
||||||
marginHorizontal: spacing.lg,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: borderRadius.lg,
|
marginHorizontal: spacing.lg,
|
||||||
padding: spacing.md,
|
borderRadius: borderRadius.lg,
|
||||||
},
|
padding: spacing.md,
|
||||||
statusRow: {
|
},
|
||||||
flexDirection: 'row',
|
statusRow: {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
statusBadge: {
|
},
|
||||||
paddingHorizontal: spacing.sm,
|
statusBadge: {
|
||||||
paddingVertical: 4,
|
paddingHorizontal: spacing.sm,
|
||||||
borderRadius: borderRadius.sm,
|
paddingVertical: 4,
|
||||||
},
|
borderRadius: borderRadius.sm,
|
||||||
statusVerified: {
|
},
|
||||||
backgroundColor: '#E8F5E9',
|
statusVerified: {
|
||||||
},
|
backgroundColor: colors.success.light + '40',
|
||||||
statusUnverified: {
|
},
|
||||||
backgroundColor: '#FFF3E0',
|
statusUnverified: {
|
||||||
},
|
backgroundColor: colors.warning.light + '40',
|
||||||
inputWrapper: {
|
},
|
||||||
flexDirection: 'row',
|
statusVerifiedText: {
|
||||||
alignItems: 'center',
|
color: colors.success.dark,
|
||||||
backgroundColor: colors.background.default,
|
},
|
||||||
borderRadius: borderRadius.lg,
|
statusUnverifiedText: {
|
||||||
borderWidth: 1,
|
color: colors.warning.dark,
|
||||||
borderColor: colors.divider,
|
},
|
||||||
paddingHorizontal: spacing.md,
|
inputWrapper: {
|
||||||
height: 48,
|
flexDirection: 'row',
|
||||||
marginBottom: spacing.sm,
|
alignItems: 'center',
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
inputIcon: {
|
borderRadius: borderRadius.lg,
|
||||||
marginRight: spacing.sm,
|
borderWidth: 1,
|
||||||
},
|
borderColor: colors.divider,
|
||||||
input: {
|
paddingHorizontal: spacing.md,
|
||||||
flex: 1,
|
height: 48,
|
||||||
color: colors.text.primary,
|
marginBottom: spacing.sm,
|
||||||
fontSize: fontSizes.md,
|
},
|
||||||
},
|
inputIcon: {
|
||||||
codeRow: {
|
marginRight: spacing.sm,
|
||||||
flexDirection: 'row',
|
},
|
||||||
alignItems: 'center',
|
input: {
|
||||||
gap: spacing.sm,
|
flex: 1,
|
||||||
marginBottom: spacing.sm,
|
color: colors.text.primary,
|
||||||
},
|
fontSize: fontSizes.md,
|
||||||
codeInput: {
|
},
|
||||||
flex: 1,
|
codeRow: {
|
||||||
marginBottom: 0,
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
sendCodeButton: {
|
gap: spacing.sm,
|
||||||
height: 48,
|
marginBottom: spacing.sm,
|
||||||
minWidth: 110,
|
},
|
||||||
borderRadius: borderRadius.lg,
|
codeInput: {
|
||||||
backgroundColor: colors.primary.main,
|
flex: 1,
|
||||||
alignItems: 'center',
|
marginBottom: 0,
|
||||||
justifyContent: 'center',
|
},
|
||||||
paddingHorizontal: spacing.sm,
|
sendCodeButton: {
|
||||||
},
|
height: 48,
|
||||||
sendCodeButtonText: {
|
minWidth: 110,
|
||||||
color: '#fff',
|
borderRadius: borderRadius.lg,
|
||||||
fontSize: fontSizes.sm,
|
backgroundColor: colors.primary.main,
|
||||||
fontWeight: '600',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
primaryButton: {
|
paddingHorizontal: spacing.sm,
|
||||||
height: 48,
|
},
|
||||||
borderRadius: borderRadius.lg,
|
sendCodeButtonText: {
|
||||||
backgroundColor: colors.primary.main,
|
color: colors.text.inverse,
|
||||||
alignItems: 'center',
|
fontSize: fontSizes.sm,
|
||||||
justifyContent: 'center',
|
fontWeight: '600',
|
||||||
marginTop: spacing.xs,
|
},
|
||||||
},
|
primaryButton: {
|
||||||
primaryButtonText: {
|
height: 48,
|
||||||
color: '#fff',
|
borderRadius: borderRadius.lg,
|
||||||
fontSize: fontSizes.md,
|
backgroundColor: colors.primary.main,
|
||||||
fontWeight: '700',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
buttonDisabled: {
|
marginTop: spacing.xs,
|
||||||
opacity: 0.6,
|
},
|
||||||
},
|
primaryButtonText: {
|
||||||
});
|
color: colors.text.inverse,
|
||||||
|
fontSize: fontSizes.md,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default AccountSecurityScreen;
|
export default AccountSecurityScreen;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
@@ -12,12 +12,52 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
|
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
|
||||||
import { authService } from '../../services';
|
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 { User } from '../../types';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
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 = () => {
|
export const BlockedUsersScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createBlockedUsersStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
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;
|
export default BlockedUsersScreen;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 表单在宽屏下居中显示
|
* 表单在宽屏下居中显示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -22,12 +22,274 @@ import { useNavigation } from '@react-navigation/native';
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
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 { useAuthStore } from '../../stores';
|
||||||
import { Avatar, Button, Text, ResponsiveContainer } from '../../components/common';
|
import { Avatar, Button, Text, ResponsiveContainer } from '../../components/common';
|
||||||
import { authService, uploadService } from '../../services';
|
import { authService, uploadService } from '../../services';
|
||||||
import { useResponsive } from '../../hooks';
|
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<typeof createEditProfileStyles>;
|
||||||
|
|
||||||
|
|
||||||
// 表单输入项组件
|
// 表单输入项组件
|
||||||
interface FormFieldProps {
|
interface FormFieldProps {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -41,6 +303,8 @@ interface FormFieldProps {
|
|||||||
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
||||||
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url';
|
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url';
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
sheet: EditProfileSheet;
|
||||||
|
colors: AppColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormField: React.FC<FormFieldProps> = ({
|
const FormField: React.FC<FormFieldProps> = ({
|
||||||
@@ -55,19 +319,21 @@ const FormField: React.FC<FormFieldProps> = ({
|
|||||||
autoCapitalize = 'sentences',
|
autoCapitalize = 'sentences',
|
||||||
keyboardType = 'default',
|
keyboardType = 'default',
|
||||||
editable = true,
|
editable = true,
|
||||||
|
sheet,
|
||||||
|
colors,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.formField}>
|
<View style={sheet.formField}>
|
||||||
<View style={styles.fieldIconContainer}>
|
<View style={sheet.fieldIconContainer}>
|
||||||
<MaterialCommunityIcons name={icon as any} size={22} color={colors.primary.main} />
|
<MaterialCommunityIcons name={icon as any} size={22} color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.fieldContent}>
|
<View style={sheet.fieldContent}>
|
||||||
<Text variant="caption" style={styles.fieldLabel}>{label}</Text>
|
<Text variant="caption" style={sheet.fieldLabel}>{label}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[
|
style={[
|
||||||
styles.fieldInput,
|
sheet.fieldInput,
|
||||||
multiline && styles.textArea,
|
multiline && sheet.textArea,
|
||||||
!editable && styles.disabledInput
|
!editable && sheet.disabledInput
|
||||||
]}
|
]}
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
@@ -86,6 +352,8 @@ const FormField: React.FC<FormFieldProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EditProfileScreen: React.FC = () => {
|
export const EditProfileScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { currentUser, updateUser } = useAuthStore();
|
const { currentUser, updateUser } = useAuthStore();
|
||||||
const { isWideScreen, isMobile, width } = useResponsive();
|
const { isWideScreen, isMobile, width } = useResponsive();
|
||||||
@@ -401,6 +669,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
onChangeText={setNickname}
|
onChangeText={setNickname}
|
||||||
placeholder="请输入昵称"
|
placeholder="请输入昵称"
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
@@ -414,6 +684,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
@@ -425,6 +697,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
onChangeText={setLocation}
|
onChangeText={setLocation}
|
||||||
placeholder="你所在的城市"
|
placeholder="你所在的城市"
|
||||||
maxLength={30}
|
maxLength={30}
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
@@ -438,6 +712,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -455,6 +731,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
placeholder="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
keyboardType="phone-pad"
|
keyboardType="phone-pad"
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
@@ -468,6 +746,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
|
sheet={styles}
|
||||||
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -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;
|
export default EditProfileScreen;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 在宽屏下使用网格布局
|
* 在宽屏下使用网格布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
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 { User } from '../../types';
|
||||||
import { useAuthStore, useUserStore } from '../../stores';
|
import { useAuthStore, useUserStore } from '../../stores';
|
||||||
import { authService } from '../../services';
|
import { authService } from '../../services';
|
||||||
@@ -26,6 +26,8 @@ import * as hrefs from '../../navigation/hrefs';
|
|||||||
import { useResponsive, useColumnCount } from '../../hooks';
|
import { useResponsive, useColumnCount } from '../../hooks';
|
||||||
|
|
||||||
const FollowListScreen: React.FC = () => {
|
const FollowListScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createFollowListStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
|
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
|
||||||
const type = typeParam === 'followers' ? 'followers' : 'following';
|
const type = typeParam === 'followers' ? 'followers' : 'following';
|
||||||
@@ -356,136 +358,138 @@ const FollowListScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createFollowListStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
// 列表布局样式
|
},
|
||||||
listContent: {
|
// 列表布局样式
|
||||||
flexGrow: 1,
|
listContent: {
|
||||||
paddingHorizontal: spacing.md,
|
flexGrow: 1,
|
||||||
paddingTop: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingBottom: spacing.lg,
|
paddingTop: spacing.md,
|
||||||
},
|
paddingBottom: spacing.lg,
|
||||||
headerCard: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
headerCard: {
|
||||||
borderRadius: borderRadius.xl,
|
backgroundColor: colors.background.paper,
|
||||||
paddingHorizontal: spacing.lg,
|
borderRadius: borderRadius.xl,
|
||||||
paddingVertical: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
marginBottom: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderWidth: 1,
|
marginBottom: spacing.md,
|
||||||
borderColor: colors.divider + '4A',
|
borderWidth: 1,
|
||||||
...shadows.sm,
|
borderColor: colors.divider + '4A',
|
||||||
},
|
...shadows.sm,
|
||||||
headerAccent: {
|
},
|
||||||
width: 28,
|
headerAccent: {
|
||||||
height: 3,
|
width: 28,
|
||||||
borderRadius: 999,
|
height: 3,
|
||||||
backgroundColor: colors.primary.main + 'A6',
|
borderRadius: 999,
|
||||||
marginBottom: spacing.sm,
|
backgroundColor: colors.primary.main + 'A6',
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
headerMainRow: {
|
},
|
||||||
flexDirection: 'row',
|
headerMainRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'space-between',
|
||||||
headerTitle: {
|
},
|
||||||
fontWeight: '700',
|
headerTitle: {
|
||||||
},
|
fontWeight: '700',
|
||||||
headerCountPill: {
|
},
|
||||||
paddingHorizontal: spacing.sm,
|
headerCountPill: {
|
||||||
paddingVertical: 4,
|
paddingHorizontal: spacing.sm,
|
||||||
borderRadius: borderRadius.full,
|
paddingVertical: 4,
|
||||||
backgroundColor: colors.background.default,
|
borderRadius: borderRadius.full,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
headerSubtitle: {
|
},
|
||||||
marginTop: spacing.xs,
|
headerSubtitle: {
|
||||||
},
|
marginTop: spacing.xs,
|
||||||
userItem: {
|
},
|
||||||
flexDirection: 'row',
|
userItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background.paper,
|
alignItems: 'center',
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
padding: spacing.md,
|
borderRadius: borderRadius.lg,
|
||||||
marginBottom: spacing.sm,
|
padding: spacing.md,
|
||||||
borderWidth: 1,
|
marginBottom: spacing.sm,
|
||||||
borderColor: colors.divider + '45',
|
borderWidth: 1,
|
||||||
...shadows.sm,
|
borderColor: colors.divider + '45',
|
||||||
},
|
...shadows.sm,
|
||||||
userInfo: {
|
},
|
||||||
flex: 1,
|
userInfo: {
|
||||||
marginLeft: spacing.md,
|
flex: 1,
|
||||||
marginRight: spacing.sm,
|
marginLeft: spacing.md,
|
||||||
},
|
marginRight: spacing.sm,
|
||||||
nickname: {
|
},
|
||||||
fontWeight: '600',
|
nickname: {
|
||||||
marginBottom: 2,
|
fontWeight: '600',
|
||||||
},
|
marginBottom: 2,
|
||||||
bio: {
|
},
|
||||||
marginTop: 2,
|
bio: {
|
||||||
},
|
marginTop: 2,
|
||||||
followButton: {
|
},
|
||||||
minWidth: 82,
|
followButton: {
|
||||||
},
|
minWidth: 82,
|
||||||
// 网格布局样式
|
},
|
||||||
gridContent: {
|
// 网格布局样式
|
||||||
flexGrow: 1,
|
gridContent: {
|
||||||
padding: spacing.lg,
|
flexGrow: 1,
|
||||||
},
|
padding: spacing.lg,
|
||||||
emptyGridContent: {
|
},
|
||||||
flex: 1,
|
emptyGridContent: {
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
},
|
alignItems: 'center',
|
||||||
gridRow: {
|
},
|
||||||
justifyContent: 'flex-start',
|
gridRow: {
|
||||||
gap: spacing.md,
|
justifyContent: 'flex-start',
|
||||||
marginBottom: spacing.md,
|
gap: spacing.md,
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
userCard: {
|
},
|
||||||
flex: 1,
|
userCard: {
|
||||||
backgroundColor: colors.background.paper,
|
flex: 1,
|
||||||
borderRadius: borderRadius.xl,
|
backgroundColor: colors.background.paper,
|
||||||
padding: spacing.md,
|
borderRadius: borderRadius.xl,
|
||||||
alignItems: 'center',
|
padding: spacing.md,
|
||||||
borderWidth: 1,
|
alignItems: 'center',
|
||||||
borderColor: colors.divider + '45',
|
borderWidth: 1,
|
||||||
...shadows.sm,
|
borderColor: colors.divider + '45',
|
||||||
},
|
...shadows.sm,
|
||||||
userCardHeader: {
|
},
|
||||||
marginBottom: spacing.sm,
|
userCardHeader: {
|
||||||
},
|
marginBottom: spacing.sm,
|
||||||
userCardContent: {
|
},
|
||||||
alignItems: 'center',
|
userCardContent: {
|
||||||
width: '100%',
|
alignItems: 'center',
|
||||||
},
|
width: '100%',
|
||||||
userCardNickname: {
|
},
|
||||||
fontWeight: '600',
|
userCardNickname: {
|
||||||
marginBottom: 2,
|
fontWeight: '600',
|
||||||
textAlign: 'center',
|
marginBottom: 2,
|
||||||
},
|
textAlign: 'center',
|
||||||
userCardBio: {
|
},
|
||||||
marginTop: spacing.xs,
|
userCardBio: {
|
||||||
textAlign: 'center',
|
marginTop: spacing.xs,
|
||||||
},
|
textAlign: 'center',
|
||||||
userCardFooter: {
|
},
|
||||||
marginTop: spacing.md,
|
userCardFooter: {
|
||||||
width: '100%',
|
marginTop: spacing.md,
|
||||||
},
|
width: '100%',
|
||||||
userCardFollowButton: {
|
},
|
||||||
width: '100%',
|
userCardFollowButton: {
|
||||||
},
|
width: '100%',
|
||||||
footerLoading: {
|
},
|
||||||
flexDirection: 'row',
|
footerLoading: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
justifyContent: 'center',
|
||||||
gap: spacing.xs,
|
paddingVertical: spacing.md,
|
||||||
},
|
gap: spacing.xs,
|
||||||
footerLoadingText: {
|
},
|
||||||
marginLeft: spacing.xs,
|
footerLoadingText: {
|
||||||
},
|
marginLeft: spacing.xs,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default FollowListScreen;
|
export default FollowListScreen;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 在宽屏下居中显示
|
* 在宽屏下居中显示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { Text, ResponsiveContainer } from '../../components/common';
|
||||||
import {
|
import {
|
||||||
loadNotificationPreferences,
|
loadNotificationPreferences,
|
||||||
@@ -33,6 +33,8 @@ interface NotificationSettingItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationSettingsScreen: React.FC = () => {
|
export const NotificationSettingsScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]);
|
||||||
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
|
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
|
||||||
const [pushEnabled, setPushEnabled] = useState(true);
|
const [pushEnabled, setPushEnabled] = useState(true);
|
||||||
const [soundEnabled, setSoundEnabled] = useState(true);
|
const [soundEnabled, setSoundEnabled] = useState(true);
|
||||||
@@ -183,72 +185,74 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function createNotificationSettingsStyles(colors: AppColors) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: colors.background.default,
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.background.default,
|
||||||
scrollContent: {
|
},
|
||||||
paddingVertical: spacing.md,
|
scrollContent: {
|
||||||
},
|
paddingVertical: spacing.md,
|
||||||
section: {
|
},
|
||||||
marginBottom: spacing.lg,
|
section: {
|
||||||
},
|
marginBottom: spacing.lg,
|
||||||
sectionHeader: {
|
},
|
||||||
flexDirection: 'row',
|
sectionHeader: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: spacing.lg,
|
alignItems: 'center',
|
||||||
marginBottom: spacing.sm,
|
paddingHorizontal: spacing.lg,
|
||||||
gap: spacing.xs,
|
marginBottom: spacing.sm,
|
||||||
},
|
gap: spacing.xs,
|
||||||
sectionTitle: {
|
},
|
||||||
fontWeight: '600',
|
sectionTitle: {
|
||||||
},
|
fontWeight: '600',
|
||||||
card: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
card: {
|
||||||
marginHorizontal: spacing.lg,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: borderRadius.lg,
|
marginHorizontal: spacing.lg,
|
||||||
overflow: 'hidden',
|
borderRadius: borderRadius.lg,
|
||||||
},
|
overflow: 'hidden',
|
||||||
settingItem: {
|
},
|
||||||
flexDirection: 'row',
|
settingItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingVertical: spacing.md,
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
iconContainer: {
|
},
|
||||||
width: 36,
|
iconContainer: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: borderRadius.md,
|
height: 36,
|
||||||
backgroundColor: colors.primary.light + '20',
|
borderRadius: borderRadius.md,
|
||||||
justifyContent: 'center',
|
backgroundColor: colors.primary.light + '20',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: spacing.md,
|
alignItems: 'center',
|
||||||
},
|
marginRight: spacing.md,
|
||||||
settingContent: {
|
},
|
||||||
flex: 1,
|
settingContent: {
|
||||||
},
|
flex: 1,
|
||||||
subtitle: {
|
},
|
||||||
marginTop: 2,
|
subtitle: {
|
||||||
},
|
marginTop: 2,
|
||||||
divider: {
|
},
|
||||||
height: 1,
|
divider: {
|
||||||
backgroundColor: colors.divider,
|
height: 1,
|
||||||
marginLeft: 36 + spacing.md + spacing.md,
|
backgroundColor: colors.divider,
|
||||||
},
|
marginLeft: 36 + spacing.md + spacing.md,
|
||||||
tipContainer: {
|
},
|
||||||
flexDirection: 'row',
|
tipContainer: {
|
||||||
alignItems: 'flex-start',
|
flexDirection: 'row',
|
||||||
marginHorizontal: spacing.lg,
|
alignItems: 'flex-start',
|
||||||
padding: spacing.md,
|
marginHorizontal: spacing.lg,
|
||||||
backgroundColor: colors.background.paper,
|
padding: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
backgroundColor: colors.background.paper,
|
||||||
gap: spacing.sm,
|
borderRadius: borderRadius.lg,
|
||||||
},
|
gap: spacing.sm,
|
||||||
tipText: {
|
},
|
||||||
flex: 1,
|
tipText: {
|
||||||
lineHeight: 20,
|
flex: 1,
|
||||||
},
|
lineHeight: 20,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default NotificationSettingsScreen;
|
export default NotificationSettingsScreen;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 在宽屏下居中显示,最大宽度限制
|
* 在宽屏下居中显示,最大宽度限制
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -16,7 +16,15 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import Constants from 'expo-constants';
|
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 { useAuthStore } from '../../stores';
|
||||||
import { Text, ResponsiveContainer } from '../../components/common';
|
import { Text, ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
@@ -34,11 +42,9 @@ interface SettingsItem {
|
|||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取应用版本号
|
|
||||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
||||||
|
|
||||||
// 设置分组配置
|
const SETTINGS_GROUPS_BASE = [
|
||||||
const SETTINGS_GROUPS = [
|
|
||||||
{
|
{
|
||||||
title: '账号与安全',
|
title: '账号与安全',
|
||||||
icon: 'shield-check-outline',
|
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 = () => {
|
export const SettingsScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { logout } = useAuthStore();
|
const { logout } = useAuthStore();
|
||||||
const { isWideScreen } = useResponsive();
|
const { isWideScreen } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
|
const colors = useAppColors();
|
||||||
// 底部间距,避免被 TabBar 遮挡
|
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
|
||||||
|
const themePreference = useThemePreference();
|
||||||
|
const setThemePreference = useSetThemePreference();
|
||||||
|
|
||||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
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) => {
|
const handleItemPress = (key: string) => {
|
||||||
switch (key) {
|
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':
|
case 'edit_profile':
|
||||||
router.push(hrefs.hrefProfileEdit());
|
router.push(hrefs.hrefProfileEdit());
|
||||||
break;
|
break;
|
||||||
@@ -103,7 +251,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await logout();
|
await logout();
|
||||||
router.replace(hrefs.hrefAuthLogin());
|
router.replace(hrefs.hrefAuthLogin());
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -113,7 +261,6 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染单个设置项
|
|
||||||
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
|
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -122,14 +269,11 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
index === 0 && styles.settingItemFirst,
|
index === 0 && styles.settingItemFirst,
|
||||||
index === total - 1 && styles.settingItemLast,
|
index === total - 1 && styles.settingItemLast,
|
||||||
]}
|
]}
|
||||||
onPress={() => item.onPress ? item.onPress() : handleItemPress(item.key)}
|
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.settingItemLeft}>
|
<View style={styles.settingItemLeft}>
|
||||||
<View style={[
|
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
|
||||||
styles.iconContainer,
|
|
||||||
item.danger && styles.dangerIconContainer
|
|
||||||
]}>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={item.icon as any}
|
name={item.icon as any}
|
||||||
size={20}
|
size={20}
|
||||||
@@ -137,10 +281,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.settingContent}>
|
<View style={styles.settingContent}>
|
||||||
<Text
|
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary}>
|
||||||
variant="body"
|
|
||||||
color={item.danger ? colors.error.main : colors.text.primary}
|
|
||||||
>
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
{item.subtitle && (
|
{item.subtitle && (
|
||||||
@@ -156,8 +297,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 渲染分组
|
const renderGroup = (group: (typeof settingsGroups)[0]) => (
|
||||||
const renderGroup = (group: typeof SETTINGS_GROUPS[0], groupIndex: number) => (
|
|
||||||
<View key={group.title} style={styles.groupContainer}>
|
<View key={group.title} style={styles.groupContainer}>
|
||||||
<View style={styles.groupHeader}>
|
<View style={styles.groupHeader}>
|
||||||
<MaterialCommunityIcons name={group.icon as any} size={16} color={colors.primary.main} />
|
<MaterialCommunityIcons name={group.icon as any} size={16} color={colors.primary.main} />
|
||||||
@@ -166,14 +306,11 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
{group.items.map((item, index) =>
|
{group.items.map((item, index) => renderSettingItem(item, index, group.items.length))}
|
||||||
renderSettingItem(item, index, group.items.length)
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 退出登录按钮
|
|
||||||
const renderLogoutButton = () => (
|
const renderLogoutButton = () => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.logoutButton}
|
style={styles.logoutButton}
|
||||||
@@ -187,13 +324,11 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 渲染内容
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<>
|
<>
|
||||||
{SETTINGS_GROUPS.map((group, index) => renderGroup(group, index))}
|
{settingsGroups.map((group) => renderGroup(group))}
|
||||||
{renderLogoutButton()}
|
{renderLogoutButton()}
|
||||||
|
|
||||||
{/* 底部版权信息 */}
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text variant="caption" color={colors.text.hint}>
|
<Text variant="caption" color={colors.text.hint}>
|
||||||
萝卜社区 v{APP_VERSION}
|
萝卜社区 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;
|
export default SettingsScreen;
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { colors } from '../../theme';
|
import { useAppColors } from '../../theme';
|
||||||
import { Post } from '../../types';
|
import { Post } from '../../types';
|
||||||
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
|
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
|
||||||
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, sharedStyles } from './useUserProfile';
|
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
|
||||||
|
|
||||||
interface UserProfileScreenProps {
|
interface UserProfileScreenProps {
|
||||||
mode: ProfileMode;
|
mode: ProfileMode;
|
||||||
@@ -25,6 +25,8 @@ interface UserProfileScreenProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => {
|
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
|
||||||
const { isDesktop, isTablet } = useResponsive();
|
const { isDesktop, isTablet } = useResponsive();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ export { BlockedUsersScreen } from './BlockedUsersScreen';
|
|||||||
export { AccountSecurityScreen } from './AccountSecurityScreen';
|
export { AccountSecurityScreen } from './AccountSecurityScreen';
|
||||||
|
|
||||||
// 导出 Hook 供需要自定义的场景使用
|
// 导出 Hook 供需要自定义的场景使用
|
||||||
export { useUserProfile, sharedStyles, TABS, TAB_ICONS } from './useUserProfile';
|
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
|
||||||
export type { ProfileMode, UseUserProfileOptions, UseUserProfileReturn } from './useUserProfile';
|
export type { ProfileMode, UseUserProfileOptions, UseUserProfileReturn } from './useUserProfile';
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
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 { Post, User } from '../../types';
|
||||||
import { useUserStore } from '../../stores';
|
import { useUserStore } from '../../stores';
|
||||||
import { useCurrentUser } from '../../stores/authStore';
|
import { useCurrentUser } from '../../stores/authStore';
|
||||||
@@ -443,62 +444,61 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 共享的样式
|
export function createSharedProfileStyles(colors: AppColors) {
|
||||||
import { StyleSheet } from 'react-native';
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
export const sharedStyles = StyleSheet.create({
|
flex: 1,
|
||||||
container: {
|
backgroundColor: colors.background.default,
|
||||||
flex: 1,
|
},
|
||||||
backgroundColor: colors.background.default,
|
scrollContent: {
|
||||||
},
|
flexGrow: 1,
|
||||||
scrollContent: {
|
},
|
||||||
flexGrow: 1,
|
desktopContainer: {
|
||||||
},
|
flex: 1,
|
||||||
desktopContainer: {
|
flexDirection: 'row',
|
||||||
flex: 1,
|
gap: spacing.lg,
|
||||||
flexDirection: 'row',
|
padding: spacing.lg,
|
||||||
gap: spacing.lg,
|
},
|
||||||
padding: spacing.lg,
|
desktopSidebar: {
|
||||||
},
|
width: 380,
|
||||||
desktopSidebar: {
|
flexShrink: 0,
|
||||||
width: 380,
|
},
|
||||||
flexShrink: 0,
|
desktopContent: {
|
||||||
},
|
flex: 1,
|
||||||
desktopContent: {
|
minWidth: 0,
|
||||||
flex: 1,
|
},
|
||||||
minWidth: 0,
|
desktopScrollContent: {
|
||||||
},
|
flexGrow: 1,
|
||||||
desktopScrollContent: {
|
},
|
||||||
flexGrow: 1,
|
tabBarContainer: {
|
||||||
},
|
marginTop: spacing.xs,
|
||||||
tabBarContainer: {
|
marginBottom: 2,
|
||||||
marginTop: spacing.xs,
|
},
|
||||||
marginBottom: 2,
|
contentContainer: {
|
||||||
},
|
flex: 1,
|
||||||
contentContainer: {
|
minHeight: 350,
|
||||||
flex: 1,
|
paddingTop: spacing.xs,
|
||||||
minHeight: 350,
|
},
|
||||||
paddingTop: spacing.xs,
|
postsContainer: {
|
||||||
},
|
paddingHorizontal: spacing.md,
|
||||||
postsContainer: {
|
paddingTop: spacing.sm,
|
||||||
paddingHorizontal: spacing.md,
|
},
|
||||||
paddingTop: spacing.sm,
|
postWrapper: {
|
||||||
},
|
marginBottom: spacing.md,
|
||||||
postWrapper: {
|
backgroundColor: colors.background.paper,
|
||||||
marginBottom: spacing.md,
|
borderRadius: 16,
|
||||||
backgroundColor: colors.background.paper,
|
overflow: 'hidden',
|
||||||
borderRadius: 16,
|
shadowColor: colors.chat.shadow,
|
||||||
overflow: 'hidden',
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowColor: '#000',
|
shadowOpacity: 0.06,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 8,
|
||||||
shadowOpacity: 0.06,
|
elevation: 2,
|
||||||
shadowRadius: 8,
|
},
|
||||||
elevation: 2,
|
lastPost: {
|
||||||
},
|
marginBottom: spacing['2xl'],
|
||||||
lastPost: {
|
},
|
||||||
marginBottom: spacing['2xl'],
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Tab 配置
|
// Tab 配置
|
||||||
export const TABS = ['帖子', '收藏'];
|
export const TABS = ['帖子', '收藏'];
|
||||||
|
|||||||
@@ -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 { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Pressable } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { routePayloadCache } from '../../stores/routePayloadCache';
|
||||||
import { scheduleService } from '../../services/scheduleService';
|
import { scheduleService } from '../../services/scheduleService';
|
||||||
|
|
||||||
@@ -32,15 +32,146 @@ const formatWeekRanges = (weeks: number[]) => {
|
|||||||
return ranges.join(', ');
|
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 CourseDetailScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createCourseDetailStyles(colors), [colors]);
|
||||||
const { courseId } = useLocalSearchParams<{ courseId?: string }>();
|
const { courseId } = useLocalSearchParams<{ courseId?: string }>();
|
||||||
const cached = courseId ? routePayloadCache.getCourseDetail(courseId) : undefined;
|
const cached = courseId ? routePayloadCache.getCourseDetail(courseId) : undefined;
|
||||||
const course = cached?.course;
|
const course = cached?.course;
|
||||||
const relatedCourses = cached?.relatedCourses ?? [];
|
const relatedCourses = cached?.relatedCourses ?? [];
|
||||||
|
|
||||||
const timeLines =
|
const timeLines = relatedCourses.length > 0 ? relatedCourses : course ? [course] : [];
|
||||||
relatedCourses.length > 0 ? relatedCourses : course ? [course] : [];
|
|
||||||
|
|
||||||
const handleDeleteCourse = () => {
|
const handleDeleteCourse = () => {
|
||||||
if (!course) return;
|
if (!course) return;
|
||||||
@@ -98,7 +229,9 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.label}>课程名称</Text>
|
<Text style={styles.label}>课程名称</Text>
|
||||||
<Text style={styles.value} numberOfLines={2}>{course.name}</Text>
|
<Text style={styles.value} numberOfLines={2}>
|
||||||
|
{course.name}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.row, styles.rowLast]}>
|
<View style={[styles.row, styles.rowLast]}>
|
||||||
@@ -110,9 +243,7 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
|
第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
|
||||||
{getMergedSectionIndex(item.startSection)}节
|
{getMergedSectionIndex(item.startSection)}节
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.timeItemSub}>
|
<Text style={styles.timeItemSub}>任课教师:{item.teacher || '未填写'}</Text>
|
||||||
任课教师:{item.teacher || '未填写'}
|
|
||||||
</Text>
|
|
||||||
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
|
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -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;
|
export default CourseDetailScreen;
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { useRouter } from 'expo-router';
|
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 { useResponsive } from '../../hooks/useResponsive';
|
||||||
import {
|
import {
|
||||||
Course,
|
Course,
|
||||||
@@ -148,6 +155,8 @@ const getWeekDates = (weekOffset: number = 0) => {
|
|||||||
const INITIAL_WEEK = getCurrentWeekNumber();
|
const INITIAL_WEEK = getCurrentWeekNumber();
|
||||||
|
|
||||||
export const ScheduleScreen: React.FC = () => {
|
export const ScheduleScreen: React.FC = () => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = useMemo(() => createScheduleStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 使用响应式 hook 检测屏幕尺寸
|
// 使用响应式 hook 检测屏幕尺寸
|
||||||
const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive();
|
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: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
@@ -1483,6 +1493,7 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default ScheduleScreen;
|
export default ScheduleScreen;
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export const installAlertOverride = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButtons.length <= 3) {
|
// 自定义对话框支持多按钮(含「主题模式」等 4 项);Web 上原生 Alert 常不可用
|
||||||
|
const MAX_CUSTOM_DIALOG_ACTIONS = 8;
|
||||||
|
if (actionButtons.length <= MAX_CUSTOM_DIALOG_ACTIONS) {
|
||||||
showDialog({
|
showDialog({
|
||||||
title: nextTitle,
|
title: nextTitle,
|
||||||
message: nextMessage,
|
message: nextMessage,
|
||||||
@@ -49,7 +51,6 @@ export const installAlertOverride = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 极少数复杂按钮场景,保底回退原生 Alert
|
|
||||||
nativeAlert(nextTitle, nextMessage, buttons, options);
|
nativeAlert(nextTitle, nextMessage, buttons, options);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ export { userManager } from './userManager';
|
|||||||
export {
|
export {
|
||||||
useHomeTabBarVisibilityStore,
|
useHomeTabBarVisibilityStore,
|
||||||
} from './homeTabBarVisibilityStore';
|
} from './homeTabBarVisibilityStore';
|
||||||
|
export {
|
||||||
|
useAppColors,
|
||||||
|
useThemePreference,
|
||||||
|
useResolvedColorScheme,
|
||||||
|
useSetThemePreference,
|
||||||
|
usePaperThemeFromStore,
|
||||||
|
ThemeBootstrap,
|
||||||
|
useThemeStore,
|
||||||
|
type ThemePreference,
|
||||||
|
type ResolvedScheme,
|
||||||
|
} from './themeStore';
|
||||||
export {
|
export {
|
||||||
useConversations as useMessageManagerConversations,
|
useConversations as useMessageManagerConversations,
|
||||||
useMessages,
|
useMessages,
|
||||||
|
|||||||
133
src/stores/themeStore.ts
Normal file
133
src/stores/themeStore.ts
Normal file
@@ -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<void>;
|
||||||
|
setSystemScheme: (s: ResolvedScheme) => void;
|
||||||
|
hydrate: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialSystem: ResolvedScheme = 'light';
|
||||||
|
|
||||||
|
export const useThemeStore = create<ThemeState>((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);
|
||||||
|
}
|
||||||
@@ -1,183 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* 胡萝卜BBS主题系统
|
* 胡萝卜BBS 主题:明暗色板、Paper 主题、设计 token
|
||||||
* 基于胡萝卜橙主色调的设计系统
|
* 新代码请优先使用 useAppColors();静态 colors 为浅色快照,与深色模式不同步。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ==================== 颜色系统 ====================
|
export { lightColors, darkColors, type AppColors } from './palettes';
|
||||||
export const colors = {
|
export { buildPaperTheme } from './paperTheme';
|
||||||
primary: {
|
export * from './tokens';
|
||||||
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 {
|
||||||
export const fontSizes = {
|
useAppColors,
|
||||||
xs: 10,
|
useThemePreference,
|
||||||
sm: 12,
|
useResolvedColorScheme,
|
||||||
md: 14,
|
useSetThemePreference,
|
||||||
lg: 16,
|
usePaperThemeFromStore,
|
||||||
xl: 18,
|
ThemeBootstrap,
|
||||||
'2xl': 20,
|
useThemeStore,
|
||||||
'3xl': 24,
|
type ThemePreference,
|
||||||
'4xl': 30,
|
type ResolvedScheme,
|
||||||
};
|
} from '../stores/themeStore';
|
||||||
|
|
||||||
// ==================== 间距系统(4px基准)====================
|
import { lightColors } from './palettes';
|
||||||
export const spacing = {
|
import { buildPaperTheme } from './paperTheme';
|
||||||
xs: 4,
|
|
||||||
sm: 8,
|
|
||||||
md: 12,
|
|
||||||
lg: 16,
|
|
||||||
xl: 20,
|
|
||||||
'2xl': 24,
|
|
||||||
'3xl': 32,
|
|
||||||
'4xl': 40,
|
|
||||||
'5xl': 48,
|
|
||||||
'6xl': 56,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 圆角系统 ====================
|
/** 浅色静态快照;随主题切换请使用 useAppColors() */
|
||||||
export const borderRadius = {
|
export const colors = lightColors;
|
||||||
sm: 4,
|
|
||||||
md: 8,
|
|
||||||
lg: 12,
|
|
||||||
xl: 16,
|
|
||||||
'2xl': 24,
|
|
||||||
full: 9999,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 阴影系统 ====================
|
export const paperTheme = buildPaperTheme(lightColors, false);
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
185
src/theme/palettes.ts
Normal file
185
src/theme/palettes.ts
Normal file
@@ -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;
|
||||||
54
src/theme/paperTheme.ts
Normal file
54
src/theme/paperTheme.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
60
src/theme/tokens.ts
Normal file
60
src/theme/tokens.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user