feat(Theme): enhance theming and UI consistency across components
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 8m15s
Frontend CI / ota-android (push) Successful in 10m56s
Frontend CI / build-android-apk (push) Successful in 1h3m22s

- 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:
lafay
2026-03-25 05:16:54 +08:00
parent 90d834695f
commit 4ee3079b9f
86 changed files with 6777 additions and 5890 deletions

View File

@@ -2,13 +2,21 @@
* 应用中心:与首页顶栏、个人主页帖子卡片同一套圆角 / 阴影 / 主色体系
*/
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { View, StyleSheet, ScrollView, TouchableOpacity, StatusBar } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import {
spacing,
fontSizes,
borderRadius,
shadows,
useAppColors,
useResolvedColorScheme,
type AppColors,
} from '../../theme';
import * as hrefs from '../../navigation/hrefs';
import { Text, ResponsiveContainer } from '../../components/common';
import { useResponsive, useResponsiveSpacing, useBreakpointGTE } from '../../hooks/useResponsive';
@@ -31,20 +39,26 @@ const APP_ENTRIES: AppItem[] = [
},
];
/** 与个人主页 PostCard 外层 `postWrapper` 一致 */
const postCardShell = {
marginBottom: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 16,
overflow: 'hidden' as const,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
};
export const AppsScreen: React.FC = () => {
const colors = useAppColors();
const resolvedScheme = useResolvedColorScheme();
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
const styles = useMemo(() => createAppsStyles(colors), [colors]);
/** 与个人主页 PostCard 外层 `postWrapper` 一致 */
const postCardShell = useMemo(
() => ({
marginBottom: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 16,
overflow: 'hidden' as const,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
}),
[colors]
);
const router = useRouter();
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
@@ -100,7 +114,7 @@ export const AppsScreen: React.FC = () => {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle="dark-content" backgroundColor="#FAFAFA" />
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.default} />
{/* 与 MessageListScreen 顶栏同一结构 / 样式 */}
<View style={[styles.msgHeader, isWideScreen && styles.msgHeaderWide]}>
@@ -143,93 +157,96 @@ export const AppsScreen: React.FC = () => {
export default AppsScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
msgHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: '#FAFAFA',
...shadows.sm,
},
msgHeaderWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg,
},
msgHeaderLeft: {
width: 44,
},
msgHeaderCenter: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
msgHeaderTitle: {
fontSize: 19,
fontWeight: '700',
color: '#333',
},
msgHeaderTitleWide: {
fontSize: 22,
},
msgHeaderRight: {
flexDirection: 'row',
alignItems: 'center',
},
/** 与消息页「+」按钮同占位宽度,标题视觉居中 */
msgHeaderRightSpacer: {
width: 36,
height: 44,
},
pageSubtitle: {
marginBottom: spacing.md,
lineHeight: fontSizes.sm * 1.45,
},
/** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */
scroll: {
flex: 1,
backgroundColor: '#FAFAFA',
},
scrollContent: {
paddingTop: spacing.md,
flexGrow: 1,
backgroundColor: '#FAFAFA',
},
appCardInner: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.lg,
paddingHorizontal: spacing.lg,
},
/** 与首页发帖 FAB 同主色实心圆 */
appIconCircle: {
width: 52,
height: 52,
borderRadius: borderRadius.full,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
},
appCardText: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
appTitle: {
fontWeight: '700',
fontSize: fontSizes.md + 1,
},
appSubtitle: {
marginTop: 4,
},
footer: {
alignItems: 'center',
paddingTop: spacing.xl,
paddingBottom: spacing.md,
},
});
function createAppsStyles(colors: AppColors) {
const headerBg = colors.background.default;
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
msgHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: headerBg,
...shadows.sm,
},
msgHeaderWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg,
},
msgHeaderLeft: {
width: 44,
},
msgHeaderCenter: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
msgHeaderTitle: {
fontSize: 19,
fontWeight: '700',
color: colors.text.primary,
},
msgHeaderTitleWide: {
fontSize: 22,
},
msgHeaderRight: {
flexDirection: 'row',
alignItems: 'center',
},
/** 与消息页「+」按钮同占位宽度,标题视觉居中 */
msgHeaderRightSpacer: {
width: 36,
height: 44,
},
pageSubtitle: {
marginBottom: spacing.md,
lineHeight: fontSizes.sm * 1.45,
},
/** 与消息列表区同色,避免顶栏阴影落在灰底上像一条线 */
scroll: {
flex: 1,
backgroundColor: headerBg,
},
scrollContent: {
paddingTop: spacing.md,
flexGrow: 1,
backgroundColor: headerBg,
},
appCardInner: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.lg,
paddingHorizontal: spacing.lg,
},
/** 与首页发帖 FAB 同主色实心圆 */
appIconCircle: {
width: 52,
height: 52,
borderRadius: borderRadius.full,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
},
appCardText: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
appTitle: {
fontWeight: '700',
fontSize: fontSizes.md + 1,
},
appSubtitle: {
marginTop: 4,
},
footer: {
alignItems: 'center',
paddingTop: spacing.xl,
paddingBottom: spacing.md,
},
});
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
@@ -14,11 +14,117 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme';
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { authService, resolveAuthApiError } from '../../services/authService';
import { showPrompt } from '../../services/promptService';
function createForgotPasswordStyles(colors: AppColors) {
return StyleSheet.create({
container: { flex: 1 },
gradient: { flex: 1 },
keyboardView: { flex: 1 },
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: spacing.xl,
paddingVertical: spacing['2xl'],
},
formCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius['2xl'],
padding: spacing.xl,
...shadows.md,
},
title: {
fontSize: fontSizes['2xl'],
fontWeight: '700',
color: colors.text.primary,
textAlign: 'center',
marginBottom: spacing.xs,
},
subtitle: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
textAlign: 'center',
marginBottom: spacing.lg,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
borderWidth: 1.5,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
height: 52,
marginBottom: spacing.md,
},
inputIcon: {
marginRight: spacing.sm,
},
input: {
flex: 1,
fontSize: fontSizes.md,
color: colors.text.primary,
},
codeRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.md,
},
codeInput: {
flex: 1,
marginBottom: 0,
},
sendCodeButton: {
height: 52,
minWidth: 110,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.md,
},
sendCodeButtonDisabled: {
opacity: 0.6,
},
sendCodeButtonText: {
color: '#fff',
fontSize: fontSizes.sm,
fontWeight: '600',
},
submitButton: {
height: 50,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.sm,
},
submitButtonDisabled: {
opacity: 0.6,
},
submitButtonText: {
color: '#fff',
fontSize: fontSizes.lg,
fontWeight: '700',
},
backButton: {
alignItems: 'center',
marginTop: spacing.md,
},
backButtonText: {
color: colors.primary.main,
fontSize: fontSizes.md,
fontWeight: '500',
},
});
}
export const ForgotPasswordScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
const router = useRouter();
const [email, setEmail] = useState('');
const [verificationCode, setVerificationCode] = useState('');
@@ -199,106 +305,4 @@ export const ForgotPasswordScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
gradient: { flex: 1 },
keyboardView: { flex: 1 },
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: spacing.xl,
paddingVertical: spacing['2xl'],
},
formCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius['2xl'],
padding: spacing.xl,
...shadows.md,
},
title: {
fontSize: fontSizes['2xl'],
fontWeight: '700',
color: colors.text.primary,
textAlign: 'center',
marginBottom: spacing.xs,
},
subtitle: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
textAlign: 'center',
marginBottom: spacing.lg,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
borderWidth: 1.5,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
height: 52,
marginBottom: spacing.md,
},
inputIcon: {
marginRight: spacing.sm,
},
input: {
flex: 1,
fontSize: fontSizes.md,
color: colors.text.primary,
},
codeRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.md,
},
codeInput: {
flex: 1,
marginBottom: 0,
},
sendCodeButton: {
height: 52,
minWidth: 110,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.md,
},
sendCodeButtonDisabled: {
opacity: 0.6,
},
sendCodeButtonText: {
color: '#fff',
fontSize: fontSizes.sm,
fontWeight: '600',
},
submitButton: {
height: 50,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.sm,
},
submitButtonDisabled: {
opacity: 0.6,
},
submitButtonText: {
color: '#fff',
fontSize: fontSizes.lg,
fontWeight: '700',
},
backButton: {
alignItems: 'center',
marginTop: spacing.md,
},
backButtonText: {
color: colors.primary.main,
fontSize: fontSizes.md,
fontWeight: '500',
},
});
export default ForgotPasswordScreen;

View File

@@ -7,7 +7,7 @@
* - 屏幕宽度 >= 768px分栏布局左侧橙色右侧中性灰
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
View,
Text,
@@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme';
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores';
import * as hrefs from '../../navigation/hrefs';
import { useResponsive, useResponsiveValue } from '../../hooks';
@@ -35,6 +35,8 @@ import { showPrompt } from '../../services/promptService';
const SPLIT_BREAKPOINT = 768;
export const LoginScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createLoginStyles(colors), [colors]);
const router = useRouter();
const login = useAuthStore((state) => state.login);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
@@ -453,7 +455,8 @@ export const LoginScreen: React.FC = () => {
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
};
const styles = StyleSheet.create({
function createLoginStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
},
@@ -762,6 +765,7 @@ const styles = StyleSheet.create({
width: '100%',
// 移除这里的阴影,避免重复
},
});
});
}
export default LoginScreen;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
@@ -12,11 +12,148 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { qrcodeApi } from '../../services/authService';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { AppBackButton } from '../../components/common';
function createQrCodeConfirmStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
closeButton: {
padding: spacing.sm,
},
headerTitle: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
},
placeholder: {
width: 40,
},
content: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
},
infoCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.xl,
alignItems: 'center',
marginBottom: spacing.xl,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
infoText: {
fontSize: fontSizes.md,
color: colors.text.secondary,
marginBottom: spacing.lg,
},
userInfo: {
alignItems: 'center',
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: spacing.md,
},
avatarPlaceholder: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.background.disabled,
justifyContent: 'center',
alignItems: 'center',
marginBottom: spacing.md,
},
nickname: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
},
buttonContainer: {
gap: spacing.md,
},
button: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
borderRadius: borderRadius.md,
alignItems: 'center',
},
confirmButton: {
backgroundColor: colors.primary.main,
},
confirmButtonText: {
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},
cancelButton: {
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider,
},
cancelButtonText: {
color: colors.text.secondary,
fontSize: fontSizes.md,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: spacing.md,
fontSize: fontSizes.md,
color: colors.text.secondary,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
},
errorText: {
marginTop: spacing.md,
fontSize: fontSizes.md,
color: colors.text.secondary,
textAlign: 'center',
},
backButton: {
marginTop: spacing.xl,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
backgroundColor: colors.primary.main,
borderRadius: borderRadius.md,
},
backButtonText: {
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},
});
}
export const QRCodeConfirmScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createQrCodeConfirmStyles(colors), [colors]);
const { sessionId: sessionIdParam } = useLocalSearchParams<{ sessionId?: string }>();
const sessionId = sessionIdParam || '';
@@ -37,9 +174,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
} catch (err: any) {
const errorMsg = err.response?.data?.message || '扫码失败';
setError(errorMsg);
Alert.alert('扫码失败', errorMsg, [
{ text: '确定', onPress: () => router.back() }
]);
Alert.alert('扫码失败', errorMsg, [{ text: '确定', onPress: () => router.back() }]);
} finally {
setLoading(false);
}
@@ -49,9 +184,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
try {
setLoading(true);
await qrcodeApi.confirm(sessionId);
Alert.alert('登录成功', '网页端已登录', [
{ text: '确定', onPress: () => router.back() }
]);
Alert.alert('登录成功', '网页端已登录', [{ text: '确定', onPress: () => router.back() }]);
} catch (err: any) {
const errorMsg = err.response?.data?.message || '确认登录失败';
Alert.alert('登录失败', errorMsg);
@@ -63,7 +196,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
const handleCancel = async () => {
try {
await qrcodeApi.cancel(sessionId);
} catch (err) {
} catch {
// 忽略取消错误
}
router.back();
@@ -97,7 +230,12 @@ export const QRCodeConfirmScreen: React.FC = () => {
return (
<SafeAreaView style={styles.container}>
<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>
<View style={styles.placeholder} />
</View>
@@ -105,14 +243,14 @@ export const QRCodeConfirmScreen: React.FC = () => {
<View style={styles.content}>
<View style={styles.infoCard}>
<Text style={styles.infoText}></Text>
{userInfo && (
<View style={styles.userInfo}>
{userInfo.avatar ? (
<Image source={{ uri: userInfo.avatar }} style={styles.avatar} />
) : (
<View style={styles.avatarPlaceholder}>
<MaterialCommunityIcons name="account" size={40} color="#999" />
<MaterialCommunityIcons name="account" size={40} color={colors.text.disabled} />
</View>
)}
<Text style={styles.nickname}>{userInfo.nickname}</Text>
@@ -127,7 +265,7 @@ export const QRCodeConfirmScreen: React.FC = () => {
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
<ActivityIndicator color={colors.text.inverse} />
) : (
<Text style={styles.confirmButtonText}></Text>
)}
@@ -146,137 +284,4 @@ export const QRCodeConfirmScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
closeButton: {
padding: spacing.sm,
},
headerTitle: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: '#333',
},
placeholder: {
width: 40,
},
content: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
},
infoCard: {
backgroundColor: '#fff',
borderRadius: borderRadius.lg,
padding: spacing.xl,
alignItems: 'center',
marginBottom: spacing.xl,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
infoText: {
fontSize: fontSizes.md,
color: '#666',
marginBottom: spacing.lg,
},
userInfo: {
alignItems: 'center',
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: spacing.md,
},
avatarPlaceholder: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
marginBottom: spacing.md,
},
nickname: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: '#333',
},
buttonContainer: {
gap: spacing.md,
},
button: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
borderRadius: borderRadius.md,
alignItems: 'center',
},
confirmButton: {
backgroundColor: colors.primary.main,
},
confirmButtonText: {
color: '#fff',
fontSize: fontSizes.md,
fontWeight: '600',
},
cancelButton: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ddd',
},
cancelButtonText: {
color: '#666',
fontSize: fontSizes.md,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: spacing.md,
fontSize: fontSizes.md,
color: '#666',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
},
errorText: {
marginTop: spacing.md,
fontSize: fontSizes.md,
color: '#666',
textAlign: 'center',
},
backButton: {
marginTop: spacing.xl,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
backgroundColor: colors.primary.main,
borderRadius: borderRadius.md,
},
backButtonText: {
color: '#fff',
fontSize: fontSizes.md,
fontWeight: '600',
},
});
export default QRCodeConfirmScreen;
export default QRCodeConfirmScreen;

View File

@@ -7,7 +7,7 @@
* - 屏幕宽度 >= 768px分栏布局左侧橙色右侧中性灰
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
View,
Text,
@@ -25,7 +25,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme';
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { authService, resolveAuthApiError } from '../../services/authService';
import { useAuthStore } from '../../stores';
import * as hrefs from '../../navigation/hrefs';
@@ -36,6 +36,8 @@ import { showPrompt } from '../../services/promptService';
const SPLIT_BREAKPOINT = 768;
export const RegisterScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
const router = useRouter();
const register = useAuthStore((state) => state.register);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
@@ -739,7 +741,8 @@ export const RegisterScreen: React.FC = () => {
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
};
const styles = StyleSheet.create({
function createRegisterStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
},
@@ -1076,6 +1079,7 @@ const styles = StyleSheet.create({
marginBottom: spacing.xl,
textAlign: 'center',
},
});
});
}
export default RegisterScreen;

View File

@@ -7,7 +7,7 @@
* 投票编辑器在宽屏下优化布局
*/
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
View,
ScrollView,
@@ -25,7 +25,14 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams, useNavigation } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import {
spacing,
fontSizes,
borderRadius,
shadows,
useAppColors,
type AppColors,
} from '../../theme';
import { Text, ResponsiveContainer } from '../../components/common';
import { channelService, postService, showPrompt, voteService } from '../../services';
import { ApiError } from '../../services/api';
@@ -84,6 +91,8 @@ const getPublishErrorMessage = (error: unknown): string => {
};
export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
const colors = useAppColors();
const styles = useMemo(() => createCreatePostStyles(colors), [colors]);
const { onClose } = props;
const router = useRouter();
const navigation = useNavigation();
@@ -805,7 +814,8 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
);
};
const styles = StyleSheet.create({
function createCreatePostStyles(colors: AppColors) {
return StyleSheet.create({
flex: {
flex: 1,
},
@@ -1050,6 +1060,7 @@ const styles = StyleSheet.create({
loadingText: {
fontSize: fontSizes.md,
},
});
});
}
export default CreatePostScreen;

View File

@@ -24,7 +24,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useRouter, useFocusEffect } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
@@ -56,12 +56,125 @@ type ViewMode = 'list' | 'grid';
type PostType = 'follow' | 'hot' | 'latest';
type LatestCapsule = { id: string; name: string };
function createHomeStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
header: {
backgroundColor: colors.background.paper,
},
searchWrapper: {
paddingTop: spacing.lg,
paddingBottom: spacing.sm,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
homeTabBar: {
marginTop: spacing.xs,
marginBottom: spacing.sm,
},
viewToggleBtn: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
capsuleWrapper: {
paddingBottom: spacing.sm,
backgroundColor: colors.background.paper,
},
capsuleList: {
gap: spacing.xs,
paddingRight: spacing.lg,
},
capsuleItem: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: 999,
},
capsuleText: {
fontSize: 13,
fontWeight: '600',
color: colors.text.secondary,
},
capsuleTextActive: {
color: colors.primary.main,
},
listContent: {
flexGrow: 1,
},
listHeader: {
width: '100%',
},
contentContainer: {
flex: 1,
},
listItem: {},
waterfallScroll: {
flex: 1,
},
waterfallContainer: {
width: '100%',
flexGrow: 1,
},
waterfallColumnsRow: {
width: '100%',
flexDirection: 'row',
alignItems: 'flex-start',
},
waterfallColumn: {
flex: 1,
},
waterfallItem: {},
floatingButton: {
position: 'absolute',
right: 20,
bottom: 20,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
...shadows.lg,
},
floatingButtonDesktop: {
right: 40,
bottom: 40,
width: 64,
height: 64,
borderRadius: 32,
},
floatingButtonWide: {
right: 60,
bottom: 60,
width: 72,
height: 72,
borderRadius: 36,
},
loadingMoreFooter: {
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
},
});
}
export const HomeScreen: React.FC = () => {
const router = useRouter();
const insets = useSafeAreaInsets();
const { posts: storePosts } = useUserStore();
const currentUser = useCurrentUser();
const colors = useAppColors();
const resolvedScheme = useResolvedColorScheme();
const styles = useMemo(() => createHomeStyles(colors), [colors]);
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
// 使用响应式 hook
const {
width,
@@ -827,7 +940,7 @@ export const HomeScreen: React.FC = () => {
if (showSearch) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
<SearchScreen
onBack={() => setShowSearch(false)}
/>
@@ -838,8 +951,8 @@ export const HomeScreen: React.FC = () => {
// 正常首页内容
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
{/* 顶部Header */}
<View style={styles.header}>
{/* 搜索栏 */}
@@ -942,115 +1055,3 @@ export const HomeScreen: React.FC = () => {
</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',
},
});

View File

@@ -26,7 +26,13 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import {
spacing,
fontSizes,
borderRadius,
useAppColors,
type AppColors,
} from '../../theme';
import { Post, Comment, VoteResultDTO } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
@@ -39,6 +45,8 @@ import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../h
import * as hrefs from '../../navigation/hrefs';
export const PostDetailScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createPostDetailStyles(colors), [colors]);
const navigation = useNavigation();
const router = useRouter();
const insets = useSafeAreaInsets();
@@ -290,6 +298,9 @@ export const PostDetailScreen: React.FC = () => {
};
navigation.setOptions({
// 与页面容器 background.default 一致,并去掉原生 header 底部分隔阴影,避免「一条线」
headerStyle: { backgroundColor: colors.background.default },
headerShadowVisible: false,
headerBackVisible: false,
headerTitleAlign: 'left',
headerLeft: () => (
@@ -324,7 +335,7 @@ export const PostDetailScreen: React.FC = () => {
</View>
),
});
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation]);
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation, colors.background.default, styles]);
// 监听键盘事件
useEffect(() => {
@@ -1702,7 +1713,8 @@ export const PostDetailScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
function createPostDetailStyles(colors: AppColors) {
return StyleSheet.create({
flex: {
flex: 1,
},
@@ -2124,4 +2136,5 @@ const styles = StyleSheet.create({
textAlign: 'center',
paddingVertical: spacing.md,
},
});
});
}

View File

@@ -4,7 +4,7 @@
* 支持响应式布局,宽屏下显示更大的搜索结果区域
*/
import React, { useState, useCallback, useEffect } from 'react';
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import {
View,
FlatList,
@@ -16,7 +16,7 @@ import {
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Post, User } from '../../types';
import { useUserStore } from '../../stores';
import { postService, authService } from '../../services';
@@ -37,6 +37,8 @@ interface SearchScreenProps {
}
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
const colors = useAppColors();
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
const router = useRouter();
const insets = useSafeAreaInsets();
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore();
@@ -544,115 +546,117 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
searchHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}70`,
},
searchShell: {
flex: 1,
borderRadius: borderRadius.xl,
backgroundColor: `${colors.primary.main}08`,
paddingHorizontal: spacing.xs,
paddingVertical: spacing.xs,
// 移除阴影效果
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
cancelButton: {
backgroundColor: `${colors.primary.main}14`,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
},
cancelText: {
fontSize: fontSizes.md,
fontWeight: '600',
},
tabWrapper: {
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}50`,
},
suggestionsContainer: {
flex: 1,
},
section: {
marginTop: spacing.md,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
color: colors.text.primary,
},
tagContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
tag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
},
tagText: {
marginLeft: spacing.xs,
},
userCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
flexDirection: 'row',
alignItems: 'center',
},
userCardInfo: {
flex: 1,
marginLeft: spacing.md,
},
userCardName: {
fontWeight: '600',
color: colors.text.primary,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
},
userName: {
fontWeight: '600',
color: colors.text.primary,
},
followingBadge: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: `${colors.primary.main}14`,
alignItems: 'center',
justifyContent: 'center',
},
loadingMore: {
paddingVertical: spacing.md,
alignItems: 'center',
},
});
function createSearchScreenStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
searchHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}70`,
},
searchShell: {
flex: 1,
borderRadius: borderRadius.xl,
backgroundColor: `${colors.primary.main}08`,
paddingHorizontal: spacing.xs,
paddingVertical: spacing.xs,
// 移除阴影效果
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
cancelButton: {
backgroundColor: `${colors.primary.main}14`,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
},
cancelText: {
fontSize: fontSizes.md,
fontWeight: '600',
},
tabWrapper: {
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}50`,
},
suggestionsContainer: {
flex: 1,
},
section: {
marginTop: spacing.md,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
color: colors.text.primary,
},
tagContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
tag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
},
tagText: {
marginLeft: spacing.xs,
},
userCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
flexDirection: 'row',
alignItems: 'center',
},
userCardInfo: {
flex: 1,
marginLeft: spacing.md,
},
userCardName: {
fontWeight: '600',
color: colors.text.primary,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
},
userName: {
fontWeight: '600',
color: colors.text.primary,
},
followingBadge: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: `${colors.primary.main}14`,
alignItems: 'center',
justifyContent: 'center',
},
loadingMore: {
paddingVertical: spacing.md,
alignItems: 'center',
},
});
}

View File

@@ -26,18 +26,20 @@ import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useNavigation, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
import { colors } from '../../theme';
import { useAppColors } from '../../theme';
import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores';
import { useBreakpointGTE } from '../../hooks/useResponsive';
import {
useChatScreen,
chatScreenStyles as baseStyles,
useChatScreenStyles,
EmojiPanel,
MorePanel,
MentionPanel,
@@ -53,10 +55,11 @@ export const ChatScreen: React.FC = () => {
const navigation = useNavigation();
const router = useRouter();
const insets = useSafeAreaInsets();
const colors = useAppColors();
const styles = useChatScreenStyles();
// 响应式布局
const isWideScreen = useBreakpointGTE('lg');
const styles = baseStyles;
// 监听屏幕宽度变化当变为大屏幕时自动跳转到web端首页
useEffect(() => {
@@ -79,6 +82,8 @@ export const ChatScreen: React.FC = () => {
beforeHeight: 0,
});
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const showJumpToLatestRef = useRef(false);
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
const containerStyle = useMemo(() => ([
styles.container,
@@ -200,6 +205,7 @@ export const ChatScreen: React.FC = () => {
loadMoreHistory,
handleMessageListContentSizeChange,
handleReachLatestEdge,
jumpToLatestMessages,
setBrowsingHistory,
} = useChatScreen();
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
@@ -210,6 +216,18 @@ export const ChatScreen: React.FC = () => {
}
}, [loadingMore, showEdgeLoadingIndicator]);
useEffect(() => {
if (loading) {
showJumpToLatestRef.current = false;
setShowJumpToLatest(false);
}
}, [loading]);
useEffect(() => {
showJumpToLatestRef.current = false;
setShowJumpToLatest(false);
}, [conversationId]);
useEffect(() => {
return () => {
if (preloadCooldownTimerRef.current) {
@@ -314,6 +332,14 @@ export const ChatScreen: React.FC = () => {
viewportHeight: layoutMeasurement.height,
};
// invertedoffset 越大离最新消息端越远,与 setBrowsingHistory(120) 阈值一致
const shouldShowJump =
contentOffset.y > 120 && !loading && messages.length > 0;
if (showJumpToLatestRef.current !== shouldShowJump) {
showJumpToLatestRef.current = shouldShowJump;
setShowJumpToLatest(shouldShowJump);
}
// 离开最新消息端后进入“浏览历史”模式,屏蔽自动跟随到底
if (contentOffset.y > 120) {
setBrowsingHistory(true);
@@ -360,7 +386,17 @@ export const ChatScreen: React.FC = () => {
}, 350);
});
}
}, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge, showEdgeLoadingIndicator, setBrowsingHistory]);
}, [
scrollPositionRef,
hasMoreHistory,
loadingMore,
loading,
messages.length,
loadMoreHistory,
handleReachLatestEdge,
showEdgeLoadingIndicator,
setBrowsingHistory,
]);
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
handleMessageListContentSizeChange(contentWidth, contentHeight);
@@ -474,6 +510,18 @@ export const ChatScreen: React.FC = () => {
</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 File

@@ -4,7 +4,7 @@
* 支持响应式布局
*/
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import {
View,
StyleSheet,
@@ -19,7 +19,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { groupService } from '../../services/groupService';
import { uploadService } from '../../services/uploadService';
import { Avatar, Text, Button, Loading } from '../../components/common';
@@ -27,6 +27,8 @@ import { User } from '../../types';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
const CreateGroupScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createCreateGroupStyles(colors), [colors]);
const router = useRouter();
// 表单状态
@@ -310,171 +312,173 @@ const CreateGroupScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
// 头部区域样式
headerSection: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.xl,
},
avatarContainer: {
marginRight: spacing.lg,
},
avatarWrapper: {
position: 'relative',
},
avatarImage: {
width: 80,
height: 80,
borderRadius: 40,
},
avatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: 40,
},
avatarBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.primary.main,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
nameInputContainer: {
flex: 1,
paddingTop: spacing.sm,
},
inputLabel: {
marginBottom: spacing.sm,
fontWeight: '600',
},
nameInput: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.lg,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider,
},
charCount: {
textAlign: 'right',
marginTop: spacing.xs,
},
// 区域样式
section: {
marginBottom: spacing.xl,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
marginBottom: spacing.sm,
},
// 文本域样式
textAreaContainer: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
padding: spacing.md,
},
textArea: {
minHeight: 100,
fontSize: fontSizes.md,
color: colors.text.primary,
lineHeight: 22,
},
textAreaCharCount: {
textAlign: 'right',
marginTop: spacing.sm,
},
// 已选成员样式
selectedMembersList: {
paddingVertical: spacing.sm,
},
selectedMemberItem: {
alignItems: 'center',
marginRight: spacing.lg,
width: 64,
},
removeMemberButton: {
position: 'absolute',
top: -4,
right: 4,
},
removeIconContainer: {
backgroundColor: colors.text.secondary,
borderRadius: 10,
width: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
selectedMemberName: {
marginTop: spacing.xs,
textAlign: 'center',
width: '100%',
},
// 邀请按钮样式
inviteButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.xl,
...shadows.sm,
},
inviteIconContainer: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
inviteTextContainer: {
flex: 1,
},
inviteTitle: {
fontWeight: '600',
marginBottom: 2,
},
// 底部按钮样式
footer: {
padding: spacing.lg,
paddingBottom: spacing.xl,
backgroundColor: colors.background.paper,
borderTopWidth: 1,
borderTopColor: colors.divider,
},
});
function createCreateGroupStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
// 头部区域样式
headerSection: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.xl,
},
avatarContainer: {
marginRight: spacing.lg,
},
avatarWrapper: {
position: 'relative',
},
avatarImage: {
width: 80,
height: 80,
borderRadius: 40,
},
avatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: 40,
},
avatarBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.primary.main,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
nameInputContainer: {
flex: 1,
paddingTop: spacing.sm,
},
inputLabel: {
marginBottom: spacing.sm,
fontWeight: '600',
},
nameInput: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.lg,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider,
},
charCount: {
textAlign: 'right',
marginTop: spacing.xs,
},
// 区域样式
section: {
marginBottom: spacing.xl,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
marginBottom: spacing.sm,
},
// 文本域样式
textAreaContainer: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
padding: spacing.md,
},
textArea: {
minHeight: 100,
fontSize: fontSizes.md,
color: colors.text.primary,
lineHeight: 22,
},
textAreaCharCount: {
textAlign: 'right',
marginTop: spacing.sm,
},
// 已选成员样式
selectedMembersList: {
paddingVertical: spacing.sm,
},
selectedMemberItem: {
alignItems: 'center',
marginRight: spacing.lg,
width: 64,
},
removeMemberButton: {
position: 'absolute',
top: -4,
right: 4,
},
removeIconContainer: {
backgroundColor: colors.text.secondary,
borderRadius: 10,
width: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
selectedMemberName: {
marginTop: spacing.xs,
textAlign: 'center',
width: '100%',
},
// 邀请按钮样式
inviteButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.xl,
...shadows.sm,
},
inviteIconContainer: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
inviteTextContainer: {
flex: 1,
},
inviteTitle: {
fontWeight: '600',
marginBottom: 2,
},
// 底部按钮样式
footer: {
padding: spacing.lg,
paddingBottom: spacing.xl,
backgroundColor: colors.background.paper,
borderTopWidth: 1,
borderTopColor: colors.divider,
},
});
}
export default CreateGroupScreen;

View File

@@ -25,7 +25,14 @@ import { useFocusEffect } from '@react-navigation/native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import {
spacing,
fontSizes,
borderRadius,
shadows,
useAppColors,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { groupService } from '../../services/groupService';
import { uploadService } from '../../services/uploadService';
@@ -56,6 +63,8 @@ const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: s
];
const GroupInfoScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createGroupInfoStyles(colors), [colors]);
const router = useRouter();
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
groupId?: string | string[];
@@ -1071,7 +1080,8 @@ const GroupInfoScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
function createGroupInfoStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
@@ -1594,6 +1604,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
});
});
}
export default GroupInfoScreen;

View File

@@ -3,7 +3,7 @@ import { View, StyleSheet, ActivityIndicator, Alert, ScrollView, TouchableOpacit
import { useRouter, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { routePayloadCache } from '../../stores/routePayloadCache';
import { groupService } from '../../services/groupService';
@@ -12,6 +12,8 @@ import { GroupMemberResponse } from '../../types/dto';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
const GroupInviteDetailScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createGroupInviteDetailStyles(colors), [colors]);
const router = useRouter();
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
@@ -174,84 +176,86 @@ const GroupInviteDetailScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
cardIconContainer: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: colors.info.light + '30',
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
flex: 1,
},
memberCount: {
marginRight: spacing.xs,
},
loadingWrap: {
paddingVertical: spacing.md,
alignItems: 'center',
},
memberPreview: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
},
memberAvatar: {
marginLeft: -8,
marginBottom: spacing.sm,
},
memberAvatarFirst: {
marginLeft: 0,
},
ownerBadge: {
position: 'absolute',
bottom: -2,
right: -2,
backgroundColor: colors.warning.main,
borderRadius: 8,
paddingHorizontal: 4,
paddingVertical: 1,
},
ownerBadgeText: {
fontSize: 10,
fontWeight: '700',
},
});
function createGroupInviteDetailStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
cardIconContainer: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: colors.info.light + '30',
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
flex: 1,
},
memberCount: {
marginRight: spacing.xs,
},
loadingWrap: {
paddingVertical: spacing.md,
alignItems: 'center',
},
memberPreview: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
},
memberAvatar: {
marginLeft: -8,
marginBottom: spacing.sm,
},
memberAvatarFirst: {
marginLeft: 0,
},
ownerBadge: {
position: 'absolute',
bottom: -2,
right: -2,
backgroundColor: colors.warning.main,
borderRadius: 8,
paddingHorizontal: 4,
paddingVertical: 1,
},
ownerBadgeText: {
fontSize: 10,
fontWeight: '700',
},
});
}
export default GroupInviteDetailScreen;

View File

@@ -21,7 +21,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores';
import { groupService } from '../../services/groupService';
import {
@@ -55,6 +55,8 @@ interface MemberGroup {
}
const GroupMembersScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createGroupMembersStyles(colors), [colors]);
const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>();
const groupId = firstRouteParam(groupIdParam) ?? '';
const { currentUser } = useAuthStore();
@@ -646,122 +648,124 @@ const GroupMembersScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
section: {
marginBottom: spacing.md,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.default,
},
memberItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
memberInfo: {
flex: 1,
marginLeft: spacing.md,
},
memberNameRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2,
},
memberName: {
fontWeight: '500',
},
roleBadge: {
paddingHorizontal: spacing.xs,
paddingVertical: 2,
borderRadius: borderRadius.sm,
marginLeft: spacing.sm,
},
mutedBadge: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
// 模态框样式
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius.lg,
borderTopRightRadius: borderRadius.lg,
padding: spacing.lg,
maxHeight: '80%',
},
modalHeader: {
alignItems: 'center',
marginBottom: spacing.md,
},
modalTitle: {
fontWeight: '700',
marginTop: spacing.sm,
marginBottom: spacing.xs,
},
actionItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
actionText: {
marginLeft: spacing.md,
},
inputLabel: {
marginBottom: spacing.xs,
},
input: {
backgroundColor: colors.background.default,
borderRadius: borderRadius.md,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.md,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider,
marginBottom: spacing.md,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: spacing.md,
},
modalButton: {
flex: 1,
marginHorizontal: spacing.xs,
},
// 分页加载样式
loadingFooter: {
paddingVertical: spacing.md,
alignItems: 'center',
},
loadMoreBtn: {
paddingVertical: spacing.md,
alignItems: 'center',
},
noMoreText: {
textAlign: 'center',
paddingVertical: spacing.md,
},
});
function createGroupMembersStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
section: {
marginBottom: spacing.md,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.default,
},
memberItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
memberInfo: {
flex: 1,
marginLeft: spacing.md,
},
memberNameRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2,
},
memberName: {
fontWeight: '500',
},
roleBadge: {
paddingHorizontal: spacing.xs,
paddingVertical: 2,
borderRadius: borderRadius.sm,
marginLeft: spacing.sm,
},
mutedBadge: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
// 模态框样式
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius.lg,
borderTopRightRadius: borderRadius.lg,
padding: spacing.lg,
maxHeight: '80%',
},
modalHeader: {
alignItems: 'center',
marginBottom: spacing.md,
},
modalTitle: {
fontWeight: '700',
marginTop: spacing.sm,
marginBottom: spacing.xs,
},
actionItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
actionText: {
marginLeft: spacing.md,
},
inputLabel: {
marginBottom: spacing.xs,
},
input: {
backgroundColor: colors.background.default,
borderRadius: borderRadius.md,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.md,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider,
marginBottom: spacing.md,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: spacing.md,
},
modalButton: {
flex: 1,
marginHorizontal: spacing.xs,
},
// 分页加载样式
loadingFooter: {
paddingVertical: spacing.md,
alignItems: 'center',
},
loadMoreBtn: {
paddingVertical: spacing.md,
alignItems: 'center',
},
noMoreText: {
textAlign: 'center',
paddingVertical: spacing.md,
},
});
}
export default GroupMembersScreen;

View File

@@ -4,7 +4,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { Avatar, Text } from '../../components/common';
import * as hrefs from '../../navigation/hrefs';
import { routePayloadCache } from '../../stores/routePayloadCache';
@@ -14,6 +14,8 @@ import { userManager } from '../../stores/userManager';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
const GroupRequestDetailScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createGroupRequestDetailStyles(colors), [colors]);
const router = useRouter();
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
@@ -174,52 +176,54 @@ const GroupRequestDetailScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.md,
...shadows.sm,
},
sectionTitle: {
marginBottom: spacing.sm,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
meta: {
marginLeft: spacing.md,
flex: 1,
},
name: {
marginBottom: 2,
},
subDesc: {
marginTop: spacing.sm,
},
});
function createGroupRequestDetailStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.md,
...shadows.sm,
},
sectionTitle: {
marginBottom: spacing.sm,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
meta: {
marginLeft: spacing.md,
flex: 1,
},
name: {
marginBottom: 2,
},
subDesc: {
marginTop: spacing.sm,
},
});
}
export default GroupRequestDetailScreen;

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
@@ -13,7 +13,7 @@ import {
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius } from '../../theme';
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
import Avatar from '../../components/common/Avatar';
import Text from '../../components/common/Text';
import { groupService } from '../../services/groupService';
@@ -22,7 +22,160 @@ import { GroupResponse, JoinType } from '../../types/dto';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { EmptyState } from '../../components/common';
function createJoinGroupStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
padding: spacing.lg,
},
heroCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.lg,
},
heroIconWrap: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.primary.light + '26',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.md,
},
heroTitle: {
marginBottom: spacing.xs,
},
tip: {
lineHeight: 20,
},
formCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
flex: 1,
},
label: {
marginBottom: spacing.xs,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: colors.divider,
borderRadius: borderRadius.md,
backgroundColor: colors.background.paper,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
color: colors.text.primary,
},
searchRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
searchBtn: {
width: 46,
height: 46,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
},
searchResultSection: {
marginBottom: spacing.lg,
},
sectionTitle: {
marginBottom: spacing.sm,
fontWeight: '600',
},
listSection: {
flex: 1,
},
groupCard: {
borderWidth: 1,
borderColor: colors.divider,
borderRadius: borderRadius.md,
padding: spacing.md,
backgroundColor: colors.background.default,
marginBottom: spacing.md,
},
groupHeader: {
flexDirection: 'row',
alignItems: 'center',
},
groupMeta: {
marginLeft: spacing.md,
flex: 1,
},
groupName: {
marginBottom: spacing.xs,
fontWeight: '600',
},
groupDesc: {
marginTop: spacing.sm,
lineHeight: 18,
},
groupInfoRow: {
marginTop: spacing.sm,
marginBottom: spacing.xs,
flexDirection: 'row',
justifyContent: 'space-between',
},
groupNoRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.md,
},
copyBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.sm,
backgroundColor: colors.primary.light + '22',
},
copyBtnText: {
marginLeft: 4,
},
submitBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: borderRadius.md,
backgroundColor: colors.primary.main,
minHeight: 46,
},
submitBtnDisabled: {
opacity: 0.5,
},
submitText: {
marginLeft: spacing.xs,
},
emptyText: {
marginTop: spacing.sm,
textAlign: 'center',
},
loadingFooter: {
paddingVertical: spacing.md,
alignItems: 'center',
},
loadMoreBtn: {
paddingVertical: spacing.md,
alignItems: 'center',
},
noMoreText: {
textAlign: 'center',
paddingVertical: spacing.md,
},
});
}
const JoinGroupScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createJoinGroupStyles(colors), [colors]);
const router = useRouter();
const [keyword, setKeyword] = useState('');
const [searching, setSearching] = useState(false);
@@ -295,153 +448,4 @@ const JoinGroupScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
padding: spacing.lg,
},
heroCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.lg,
},
heroIconWrap: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.primary.light + '26',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.md,
},
heroTitle: {
marginBottom: spacing.xs,
},
tip: {
lineHeight: 20,
},
formCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
flex: 1,
},
label: {
marginBottom: spacing.xs,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: colors.divider,
borderRadius: borderRadius.md,
backgroundColor: colors.background.paper,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
color: colors.text.primary,
},
searchRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
searchBtn: {
width: 46,
height: 46,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
},
searchResultSection: {
marginBottom: spacing.lg,
},
sectionTitle: {
marginBottom: spacing.sm,
fontWeight: '600',
},
listSection: {
flex: 1,
},
groupCard: {
borderWidth: 1,
borderColor: colors.divider,
borderRadius: borderRadius.md,
padding: spacing.md,
backgroundColor: colors.background.default,
marginBottom: spacing.md,
},
groupHeader: {
flexDirection: 'row',
alignItems: 'center',
},
groupMeta: {
marginLeft: spacing.md,
flex: 1,
},
groupName: {
marginBottom: spacing.xs,
fontWeight: '600',
},
groupDesc: {
marginTop: spacing.sm,
lineHeight: 18,
},
groupInfoRow: {
marginTop: spacing.sm,
marginBottom: spacing.xs,
flexDirection: 'row',
justifyContent: 'space-between',
},
groupNoRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.md,
},
copyBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.sm,
backgroundColor: colors.primary.light + '22',
},
copyBtnText: {
marginLeft: 4,
},
submitBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: borderRadius.md,
backgroundColor: colors.primary.main,
minHeight: 46,
},
submitBtnDisabled: {
opacity: 0.5,
},
submitText: {
marginLeft: spacing.xs,
},
emptyText: {
marginTop: spacing.sm,
textAlign: 'center',
},
loadingFooter: {
paddingVertical: spacing.md,
alignItems: 'center',
},
loadMoreBtn: {
paddingVertical: spacing.md,
alignItems: 'center',
},
noMoreText: {
textAlign: 'center',
paddingVertical: spacing.md,
},
});
export default JoinGroupScreen;

View File

@@ -30,7 +30,14 @@ import { useRouter } from 'expo-router';
import { useIsFocused } from '@react-navigation/native';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme';
import {
spacing,
fontSizes,
shadows,
borderRadius,
useAppColors,
type AppColors,
} from '../../theme';
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto';
import { authService } from '../../services';
import { useUserStore, useAuthStore } from '../../stores';
@@ -73,6 +80,8 @@ interface SearchResultItem {
* 支持响应式双栏布局
*/
export const MessageListScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createMessageListStyles(colors), [colors]);
const router = useRouter();
const isFocused = useIsFocused();
const insets = useSafeAreaInsets();
@@ -565,7 +574,7 @@ export const MessageListScreen: React.FC = () => {
))}
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
</TouchableOpacity>
);
}
@@ -614,7 +623,7 @@ export const MessageListScreen: React.FC = () => {
</View>
)}
{!user.is_following && (
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
)}
</TouchableOpacity>
);
@@ -661,11 +670,11 @@ export const MessageListScreen: React.FC = () => {
const renderSearchMode = () => (
<View style={styles.searchModeContainer}>
<View style={styles.searchInputContainer}>
<AppBackButton onPress={handleCloseSearch} iconColor="#666" />
<AppBackButton onPress={handleCloseSearch} iconColor={colors.text.secondary} />
<TextInput
style={styles.searchInput}
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
placeholderTextColor="#999"
placeholderTextColor={colors.text.hint}
value={searchText}
onChangeText={setSearchText}
autoFocus
@@ -676,7 +685,7 @@ export const MessageListScreen: React.FC = () => {
/>
{searchText.length > 0 && (
<TouchableOpacity onPress={() => setSearchText('')}>
<MaterialCommunityIcons name="close-circle" size={18} color="#999" />
<MaterialCommunityIcons name="close-circle" size={18} color={colors.text.hint} />
</TouchableOpacity>
)}
</View>
@@ -733,7 +742,7 @@ export const MessageListScreen: React.FC = () => {
<View style={styles.headerRightContainer}>
<TouchableOpacity style={styles.headerRightBtn} onPress={handleOpenActionMenu}>
<MaterialCommunityIcons name="plus" size={24} color="#666" />
<MaterialCommunityIcons name="plus" size={24} color={colors.text.secondary} />
</TouchableOpacity>
</View>
</View>
@@ -744,7 +753,7 @@ export const MessageListScreen: React.FC = () => {
activeOpacity={0.7}
>
<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>
</View>
</TouchableOpacity>
@@ -793,19 +802,19 @@ export const MessageListScreen: React.FC = () => {
<Pressable style={styles.actionMenuBackdrop} onPress={() => setActionMenuVisible(false)}>
<View style={styles.actionMenu}>
<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>
</TouchableOpacity>
<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>
</TouchableOpacity>
<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>
</TouchableOpacity>
<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>
</TouchableOpacity>
</View>
@@ -838,7 +847,7 @@ export const MessageListScreen: React.FC = () => {
// 默认占位符
<View style={styles.chatPlaceholder}>
<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>
</View>
</View>
@@ -870,288 +879,290 @@ export const MessageListScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FAFAFA',
},
// 桌面端双栏布局
desktopContainer: {
flex: 1,
flexDirection: 'row',
},
sidebar: {
backgroundColor: '#FAFAFA',
borderRightWidth: 1,
borderRightColor: '#E8E8E8',
},
chatArea: {
flex: 1,
backgroundColor: '#F5F7FA',
},
chatPlaceholder: {
flex: 1,
backgroundColor: '#F5F7FA',
alignItems: 'center',
justifyContent: 'center',
},
chatPlaceholderContent: {
alignItems: 'center',
justifyContent: 'center',
},
chatPlaceholderText: {
marginTop: spacing.md,
fontSize: 16,
color: '#999',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: '#FAFAFA',
...shadows.sm,
},
headerWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg,
},
headerTitleWide: {
fontSize: 22,
},
searchContainerWide: {
paddingHorizontal: spacing.lg,
},
listContentWide: {
paddingHorizontal: spacing.lg,
},
headerLeft: {
width: 44,
},
headerCenter: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
headerTitle: {
fontSize: 19,
fontWeight: '700',
color: '#333',
},
totalBadge: {
minWidth: 18,
height: 18,
borderRadius: 9,
backgroundColor: '#FF4757',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 5,
},
totalBadgeText: {
color: '#FFF',
fontSize: 11,
fontWeight: '600',
},
headerRight: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
headerRightContainer: {
flexDirection: 'row',
alignItems: 'center',
},
headerRightBtn: {
width: 36,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
actionMenuBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.08)',
},
actionMenu: {
position: 'absolute',
top: 88,
right: spacing.md,
width: 188,
backgroundColor: '#FFF',
borderRadius: 12,
paddingVertical: spacing.sm,
...shadows.md,
},
actionMenuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
},
actionMenuText: {
marginLeft: spacing.sm,
fontSize: 16,
color: '#333',
},
searchContainer: {
paddingHorizontal: spacing.md,
paddingBottom: spacing.sm,
backgroundColor: '#FAFAFA',
},
searchBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F0F0F0',
borderRadius: 10,
paddingHorizontal: spacing.sm,
paddingVertical: 10,
},
searchPlaceholder: {
fontSize: 14,
color: '#999',
marginLeft: spacing.xs,
},
listContent: {
flexGrow: 1,
backgroundColor: '#FAFAFA',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.xl * 2,
},
loadingMoreContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
},
loadingMoreText: {
marginLeft: spacing.sm,
fontSize: 14,
color: '#999',
},
searchModeContainer: {
flex: 1,
backgroundColor: '#FAFAFA',
},
searchInputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F0F0F0',
borderRadius: 10,
paddingHorizontal: spacing.md,
marginHorizontal: spacing.md,
marginTop: spacing.md,
height: 40,
},
searchInput: {
flex: 1,
fontSize: 15,
color: '#333',
marginLeft: spacing.md,
},
searchTabs: {
flexDirection: 'row',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
gap: spacing.sm,
},
searchTab: {
flex: 1,
paddingVertical: spacing.sm,
alignItems: 'center',
backgroundColor: '#F0F0F0',
borderRadius: 8,
},
searchTabActive: {
backgroundColor: colors.primary.main,
},
searchTabText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
searchTabTextActive: {
color: '#FFF',
},
searchResultsContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
},
searchResultItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
backgroundColor: '#FFF',
borderRadius: 10,
marginBottom: spacing.sm,
paddingHorizontal: spacing.md,
},
searchResultContent: {
flex: 1,
marginLeft: spacing.md,
},
searchResultHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 2,
},
searchResultName: {
fontSize: 15,
fontWeight: '600',
color: '#333',
},
searchResultTime: {
fontSize: 12,
color: '#999',
},
searchResultMessage: {
fontSize: 13,
color: '#888',
},
searchingText: {
marginTop: spacing.sm,
fontSize: 14,
color: '#999',
},
followingBadge: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.primary.light + '30',
alignItems: 'center',
justifyContent: 'center',
},
highlightText: {
color: colors.primary.main,
fontWeight: '700',
backgroundColor: colors.primary.light + '30',
},
matchedMessagesContainer: {
marginTop: spacing.sm,
},
matchedMessageItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingVertical: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#F0F0F0',
},
matchedMessageText: {
flex: 1,
fontSize: 13,
color: '#555',
lineHeight: 18,
},
matchedMessageTime: {
fontSize: 11,
color: '#AAA',
marginLeft: spacing.sm,
minWidth: 45,
},
});
function createMessageListStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// 桌面端双栏布局
desktopContainer: {
flex: 1,
flexDirection: 'row',
},
sidebar: {
backgroundColor: colors.background.default,
borderRightWidth: 1,
borderRightColor: colors.chat.border,
},
chatArea: {
flex: 1,
backgroundColor: colors.chat.surfaceMuted,
},
chatPlaceholder: {
flex: 1,
backgroundColor: colors.chat.surfaceMuted,
alignItems: 'center',
justifyContent: 'center',
},
chatPlaceholderContent: {
alignItems: 'center',
justifyContent: 'center',
},
chatPlaceholderText: {
marginTop: spacing.md,
fontSize: 16,
color: colors.text.hint,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: colors.background.default,
...shadows.sm,
},
headerWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg,
},
headerTitleWide: {
fontSize: 22,
},
searchContainerWide: {
paddingHorizontal: spacing.lg,
},
listContentWide: {
paddingHorizontal: spacing.lg,
},
headerLeft: {
width: 44,
},
headerCenter: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
headerTitle: {
fontSize: 19,
fontWeight: '700',
color: colors.text.primary,
},
totalBadge: {
minWidth: 18,
height: 18,
borderRadius: 9,
backgroundColor: colors.error.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 5,
},
totalBadgeText: {
color: colors.primary.contrast,
fontSize: 11,
fontWeight: '600',
},
headerRight: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
headerRightContainer: {
flexDirection: 'row',
alignItems: 'center',
},
headerRightBtn: {
width: 36,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
actionMenuBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.08)',
},
actionMenu: {
position: 'absolute',
top: 88,
right: spacing.md,
width: 188,
backgroundColor: colors.background.paper,
borderRadius: 12,
paddingVertical: spacing.sm,
...shadows.md,
},
actionMenuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
},
actionMenuText: {
marginLeft: spacing.sm,
fontSize: 16,
color: colors.text.primary,
},
searchContainer: {
paddingHorizontal: spacing.md,
paddingBottom: spacing.sm,
backgroundColor: colors.background.default,
},
searchBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 10,
paddingHorizontal: spacing.sm,
paddingVertical: 10,
},
searchPlaceholder: {
fontSize: 14,
color: colors.text.hint,
marginLeft: spacing.xs,
},
listContent: {
flexGrow: 1,
backgroundColor: colors.background.default,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.xl * 2,
},
loadingMoreContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
},
loadingMoreText: {
marginLeft: spacing.sm,
fontSize: 14,
color: colors.text.hint,
},
searchModeContainer: {
flex: 1,
backgroundColor: colors.background.default,
},
searchInputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 10,
paddingHorizontal: spacing.md,
marginHorizontal: spacing.md,
marginTop: spacing.md,
height: 40,
},
searchInput: {
flex: 1,
fontSize: 15,
color: colors.text.primary,
marginLeft: spacing.md,
},
searchTabs: {
flexDirection: 'row',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
gap: spacing.sm,
},
searchTab: {
flex: 1,
paddingVertical: spacing.sm,
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 8,
},
searchTabActive: {
backgroundColor: colors.primary.main,
},
searchTabText: {
fontSize: 14,
color: colors.text.secondary,
fontWeight: '500',
},
searchTabTextActive: {
color: colors.primary.contrast,
},
searchResultsContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
},
searchResultItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 10,
marginBottom: spacing.sm,
paddingHorizontal: spacing.md,
},
searchResultContent: {
flex: 1,
marginLeft: spacing.md,
},
searchResultHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 2,
},
searchResultName: {
fontSize: 15,
fontWeight: '600',
color: colors.text.primary,
},
searchResultTime: {
fontSize: 12,
color: colors.text.hint,
},
searchResultMessage: {
fontSize: 13,
color: colors.text.secondary,
},
searchingText: {
marginTop: spacing.sm,
fontSize: 14,
color: colors.text.hint,
},
followingBadge: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.primary.light + '30',
alignItems: 'center',
justifyContent: 'center',
},
highlightText: {
color: colors.primary.main,
fontWeight: '700',
backgroundColor: colors.primary.light + '30',
},
matchedMessagesContainer: {
marginTop: spacing.sm,
},
matchedMessageItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingVertical: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
matchedMessageText: {
flex: 1,
fontSize: 13,
color: colors.text.secondary,
lineHeight: 18,
},
matchedMessageTime: {
fontSize: 11,
color: colors.text.hint,
marginLeft: spacing.sm,
minWidth: 45,
},
});
}

View File

@@ -16,11 +16,11 @@ import {
Dimensions,
BackHandler,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useIsFocused } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { SystemMessageResponse } from '../../types/dto';
import { messageService } from '../../services/messageService';
import { commentService } from '../../services/commentService';
@@ -45,6 +45,8 @@ const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'f
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
const colors = useAppColors();
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
const isFocused = useIsFocused();
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
const router = useRouter();
@@ -554,143 +556,145 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// Header 样式 - 美化版
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
...shadows.sm,
},
headerWide: {
paddingHorizontal: spacing.xl,
paddingVertical: spacing.lg,
},
headerTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text.primary,
},
headerTitleWide: {
fontSize: 20,
},
unreadBadge: {
backgroundColor: colors.primary.main,
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: spacing.sm,
minWidth: 20,
alignItems: 'center',
justifyContent: 'center',
},
unreadBadgeText: {
color: colors.text.inverse,
fontSize: 11,
fontWeight: '600',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'flex-start',
},
backButtonPlaceholder: {
width: 40,
},
// 分类筛选 - 美化版(使用分段式设计)
filterContainer: {
flexDirection: 'row',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
},
filterContainerWideWeb: {
paddingHorizontal: spacing.xl,
paddingVertical: spacing.md,
justifyContent: 'center',
backgroundColor: colors.background.paper,
},
filterTag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
marginRight: spacing.xs,
borderRadius: borderRadius.lg,
backgroundColor: colors.background.default,
borderWidth: 1,
borderColor: 'transparent',
},
filterTagWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
marginRight: spacing.sm,
},
filterTagActive: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
filterTagTextActive: {
fontWeight: '600',
},
filterCount: {
marginLeft: spacing.xs,
backgroundColor: colors.primary.light + '40',
paddingHorizontal: 6,
paddingVertical: 1,
borderRadius: 8,
minWidth: 18,
alignItems: 'center',
},
filterCountActive: {
backgroundColor: 'rgba(255, 255, 255, 0.25)',
},
listContent: {
flexGrow: 1,
paddingTop: spacing.md,
paddingBottom: spacing.lg,
},
listContentWide: {
paddingHorizontal: spacing.xl,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.xl * 2,
},
loadingMore: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.lg,
},
loadingMoreText: {
marginLeft: spacing.sm,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: spacing['3xl'],
},
emptyContainerWeb: {
minHeight: 500,
justifyContent: 'center',
paddingTop: 100,
},
});
function createNotificationsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// Header 样式 - 美化版
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
...shadows.sm,
},
headerWide: {
paddingHorizontal: spacing.xl,
paddingVertical: spacing.lg,
},
headerTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text.primary,
},
headerTitleWide: {
fontSize: 20,
},
unreadBadge: {
backgroundColor: colors.primary.main,
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: spacing.sm,
minWidth: 20,
alignItems: 'center',
justifyContent: 'center',
},
unreadBadgeText: {
color: colors.text.inverse,
fontSize: 11,
fontWeight: '600',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'flex-start',
},
backButtonPlaceholder: {
width: 40,
},
// 分类筛选 - 美化版(使用分段式设计)
filterContainer: {
flexDirection: 'row',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
},
filterContainerWideWeb: {
paddingHorizontal: spacing.xl,
paddingVertical: spacing.md,
justifyContent: 'center',
backgroundColor: colors.background.paper,
},
filterTag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
marginRight: spacing.xs,
borderRadius: borderRadius.lg,
backgroundColor: colors.background.default,
borderWidth: 1,
borderColor: 'transparent',
},
filterTagWide: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
marginRight: spacing.sm,
},
filterTagActive: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
filterTagTextActive: {
fontWeight: '600',
},
filterCount: {
marginLeft: spacing.xs,
backgroundColor: colors.primary.light + '40',
paddingHorizontal: 6,
paddingVertical: 1,
borderRadius: 8,
minWidth: 18,
alignItems: 'center',
},
filterCountActive: {
backgroundColor: 'rgba(255, 255, 255, 0.25)',
},
listContent: {
flexGrow: 1,
paddingTop: spacing.md,
paddingBottom: spacing.lg,
},
listContentWide: {
paddingHorizontal: spacing.xl,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.xl * 2,
},
loadingMore: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: spacing.lg,
},
loadingMoreText: {
marginLeft: spacing.sm,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: spacing['3xl'],
},
emptyContainerWeb: {
minHeight: 500,
justifyContent: 'center',
paddingTop: 100,
},
});
}

View File

@@ -4,7 +4,7 @@
* 参考QQ和微信的实现提供聊天管理功能
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
@@ -16,7 +16,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores';
import { authService } from '../../services/authService';
import { messageService } from '../../services/messageService';
@@ -33,6 +33,8 @@ import * as hrefs from '../../navigation/hrefs';
import { AppBackButton } from '../../components/common';
const PrivateChatInfoScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createPrivateChatInfoStyles(colors), [colors]);
const router = useRouter();
const raw = useLocalSearchParams<{
conversationId?: string;
@@ -277,7 +279,7 @@ const PrivateChatInfoScreen: React.FC = () => {
value={value}
onValueChange={onValueChange}
trackColor={{ false: '#E0E0E0', true: colors.primary.light }}
thumbColor={value ? colors.primary.main : '#F5F5F5'}
thumbColor={value ? colors.primary.main : colors.background.default}
/>
</View>
);
@@ -420,101 +422,103 @@ const PrivateChatInfoScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
pageTitle: {
fontWeight: '600',
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: spacing.xl,
},
headerCard: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.md,
marginTop: spacing.md,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
userHeader: {
flexDirection: 'row',
alignItems: 'center',
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
},
userName: {
fontWeight: '600',
marginBottom: spacing.xs,
},
section: {
marginTop: spacing.lg,
paddingHorizontal: spacing.md,
},
sectionTitle: {
marginBottom: spacing.sm,
marginLeft: spacing.sm,
fontWeight: '500',
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
...shadows.sm,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
},
settingIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '20',
},
settingTitle: {
flex: 1,
},
dangerText: {
color: colors.error.main,
},
divider: {
marginLeft: spacing.xl + 36,
width: 'auto',
marginVertical: spacing.xs,
},
deleteButton: {
marginTop: spacing.sm,
},
});
function createPrivateChatInfoStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
pageTitle: {
fontWeight: '600',
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: spacing.xl,
},
headerCard: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.md,
marginTop: spacing.md,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
userHeader: {
flexDirection: 'row',
alignItems: 'center',
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
},
userName: {
fontWeight: '600',
marginBottom: spacing.xs,
},
section: {
marginTop: spacing.lg,
paddingHorizontal: spacing.md,
},
sectionTitle: {
marginBottom: spacing.sm,
marginLeft: spacing.sm,
fontWeight: '500',
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
...shadows.sm,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
},
settingIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '20',
},
settingTitle: {
flex: 1,
},
dangerText: {
color: colors.error.main,
},
divider: {
marginLeft: spacing.xl + 36,
width: 'auto',
marginVertical: spacing.xs,
},
deleteButton: {
marginTop: spacing.sm,
},
});
}
export default PrivateChatInfoScreen;

View File

@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text, AppBackButton } from '../../../../components/common';
import { colors, spacing } from '../../../../theme';
import { chatScreenStyles as baseStyles } from './styles';
import { useAppColors, spacing } from '../../../../theme';
import { useChatScreenStyles } from './styles';
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
import { ChatHeaderProps } from './types';
@@ -24,6 +24,9 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
onGroupInfoPress,
isWideScreen: propIsWideScreen,
}) => {
const colors = useAppColors();
const baseStyles = useChatScreenStyles();
// 响应式布局
// 使用 768px 作为大屏幕和小屏幕的分界线
const isWideScreenHook = useBreakpointGTE('lg');
@@ -57,7 +60,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
fontSize: isWideScreen ? 13 : 12,
},
});
}, [isWideScreen]);
}, [isWideScreen, baseStyles]);
// 获取显示标题
const getDisplayTitle = () => {

View File

@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Text } from '../../../../components/common';
import { colors } from '../../../../theme';
import { chatScreenStyles as baseStyles } from './styles';
import { useAppColors } from '../../../../theme';
import { useChatScreenStyles } from './styles';
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
import { ChatInputProps, GroupMessage, SenderInfo } from './types';
import { extractTextFromSegments } from '../../../../types/dto';
@@ -39,14 +39,15 @@ export const ChatInput: React.FC<ChatInputProps & {
otherUser,
getSenderInfo,
}) => {
const colors = useAppColors();
const styles = useChatScreenStyles();
const isDisabled = isGroupChat && isMuted;
// 获取当前用户ID
const currentUserId = currentUser?.id || '';
// 响应式布局
const isWideScreen = useBreakpointGTE('lg');
const styles = baseStyles;
const inputContainerStyle = useMemo(() => ([
styles.inputContainer,

View File

@@ -10,11 +10,11 @@ import { Image as ExpoImage } from 'expo-image';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { Text } from '../../../../components/common';
import { chatScreenStyles as baseStyles } from './styles';
import { useChatScreenStyles } from './styles';
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
import { EMOJIS } from './constants';
import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '../../../../services/stickerService';
import { colors, spacing } from '../../../../theme';
import { useAppColors, spacing } from '../../../../theme';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
@@ -41,6 +41,8 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
onInsertSticker,
onClose,
}) => {
const colors = useAppColors();
const baseStyles = useChatScreenStyles();
const [activeTab, setActiveTab] = useState<TabType>('emoji');
const [stickers, setStickers] = useState<CustomSticker[]>([]);
const [loadingStickers, setLoadingStickers] = useState(false);
@@ -82,7 +84,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
lineHeight: emojiSize.size + 6,
},
});
}, [emojiSize, emojiColumns]);
}, [emojiSize, emojiColumns, baseStyles]);
// 加载自定义表情
const loadStickers = useCallback(async () => {

View File

@@ -5,7 +5,7 @@
* 实现手机端群信息界面的核心功能
*/
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
@@ -17,7 +17,7 @@ import {
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text } from '../../../../components/common';
import { colors, spacing, fontSizes, shadows } from '../../../../theme';
import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme';
import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto';
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
import { groupManager } from '../../../../stores/groupManager';
@@ -53,6 +53,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
onInviteMembers,
onViewAllMembers,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]);
const isWideScreen = useBreakpointGTE('lg');
const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current;
const fadeAnim = React.useRef(new Animated.Value(0)).current;
@@ -363,7 +365,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
);
};
const styles = StyleSheet.create({
function createGroupInfoPanelStyles(colors: AppColors) {
return StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
@@ -378,7 +381,7 @@ const styles = StyleSheet.create({
top: 0,
bottom: 0,
width: PANEL_WIDTH,
backgroundColor: '#FFF',
backgroundColor: colors.background.paper,
zIndex: 101,
...shadows.lg,
},
@@ -389,12 +392,12 @@ const styles = StyleSheet.create({
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: '#E8E8E8',
borderBottomColor: colors.divider,
},
headerTitle: {
fontSize: fontSizes.xl,
fontWeight: '600',
color: '#333',
color: colors.text.primary,
},
closeButton: {
padding: spacing.xs,
@@ -409,12 +412,12 @@ const styles = StyleSheet.create({
groupName: {
fontSize: fontSizes.xl,
fontWeight: '600',
color: '#333',
color: colors.text.primary,
marginTop: spacing.md,
},
memberCount: {
fontSize: fontSizes.md,
color: '#999',
color: colors.text.secondary,
marginTop: spacing.xs,
},
joinType: {
@@ -442,7 +445,7 @@ const styles = StyleSheet.create({
},
divider: {
height: 1,
backgroundColor: '#E8E8E8',
backgroundColor: colors.divider,
marginHorizontal: spacing.md,
},
section: {
@@ -456,11 +459,11 @@ const styles = StyleSheet.create({
sectionTitle: {
fontSize: fontSizes.md,
fontWeight: '600',
color: '#333',
color: colors.text.primary,
marginLeft: spacing.xs,
},
noticeBox: {
backgroundColor: '#FFF9E6',
backgroundColor: colors.chat.tipBg,
padding: spacing.md,
borderRadius: 8,
borderLeftWidth: 3,
@@ -468,23 +471,23 @@ const styles = StyleSheet.create({
},
noticeText: {
fontSize: fontSizes.sm,
color: '#666',
color: colors.text.secondary,
lineHeight: 20,
},
noticeTime: {
fontSize: fontSizes.xs,
color: '#999',
color: colors.text.hint,
marginTop: spacing.xs,
textAlign: 'right',
},
descriptionBox: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
padding: spacing.md,
borderRadius: 8,
},
description: {
fontSize: fontSizes.sm,
color: '#666',
color: colors.text.secondary,
lineHeight: 20,
},
memberList: {
@@ -518,7 +521,7 @@ const styles = StyleSheet.create({
},
memberName: {
fontSize: fontSizes.md,
color: '#333',
color: colors.text.primary,
flex: 1,
},
ownerBadge: {
@@ -541,7 +544,7 @@ const styles = StyleSheet.create({
},
moreMembers: {
fontSize: fontSizes.sm,
color: '#999',
color: colors.text.secondary,
textAlign: 'center',
paddingVertical: spacing.sm,
},
@@ -550,15 +553,15 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
borderBottomColor: colors.chat.borderHairline,
},
infoLabel: {
fontSize: fontSizes.sm,
color: '#999',
color: colors.text.secondary,
},
infoValue: {
fontSize: fontSizes.sm,
color: '#333',
color: colors.text.primary,
},
actionSection: {
padding: spacing.md,
@@ -588,6 +591,7 @@ const styles = StyleSheet.create({
leaveButtonText: {
color: colors.error.main,
},
});
});
}
export default GroupInfoPanel;

View File

@@ -15,7 +15,7 @@ import {
Platform,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { chatScreenStyles as styles } from './styles';
import { useChatScreenStyles } from './styles';
import { LongPressMenuProps } from './types';
import { RECALL_TIME_LIMIT } from './constants';
import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto';
@@ -34,6 +34,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
onDelete,
onAddSticker,
}) => {
const styles = useChatScreenStyles();
const scaleAnimation = useRef(new Animated.Value(0)).current;
const opacityAnimation = useRef(new Animated.Value(0)).current;
const blurActiveElementOnWeb = () => {

View File

@@ -6,7 +6,7 @@ import React from 'react';
import { View, ScrollView, TouchableOpacity } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text } from '../../../../components/common';
import { chatScreenStyles as styles } from './styles';
import { useChatScreenStyles } from './styles';
import { MentionPanelProps } from './types';
export const MentionPanel: React.FC<MentionPanelProps> = ({
@@ -18,6 +18,7 @@ export const MentionPanel: React.FC<MentionPanelProps> = ({
onMentionAll,
onClose,
}) => {
const styles = useChatScreenStyles();
// 过滤成员列表
const filteredMembers = members.filter(member => {
if (member.user_id === currentUserId) return false; // 排除自己

View File

@@ -15,20 +15,12 @@ import {
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text, ImageGridItem } from '../../../../components/common';
import { chatScreenStyles as baseStyles } from './styles';
import { useChatScreenStyles } from './styles';
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
import { MessageBubbleProps, SenderInfo, MenuPosition, GroupMessage } from './types';
import { MessageSegmentsRenderer } from './SegmentRenderer';
import { MessageSegment, ImageSegmentData, extractTextFromSegments } from '../../../../types/dto';
import { SwipeableMessageBubble } from './SwipeableMessageBubble';
import {
getBubbleBorderRadius,
getMessageGroupPosition,
shouldShowSenderInfo,
bubbleColors,
bubbleStyles,
} from './bubbleStyles';
// 获取屏幕宽度
const { width: SCREEN_WIDTH } = Dimensions.get('window');
@@ -61,7 +53,8 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
}) => {
const bubbleRef = useRef<View>(null);
const pressPositionRef = useRef({ x: 0, y: 0 });
const baseStyles = useChatScreenStyles();
// 响应式布局
const { width } = useResponsive();
const isWideScreen = useBreakpointGTE('lg');
@@ -89,7 +82,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
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 = () => {
// 撤回消息
if (isRecalled) {
return (
<View style={[
styles.messageBubble,
isMe ? styles.myBubble : styles.theirBubble,
styles.recalledBubble,
]}>
<Text
style={[
isMe ? styles.myBubbleText : styles.theirBubbleText,
styles.recalledText,
]}
selectable={false}
>
</Text>
</View>
return renderBubbleShell(
<Text
style={[
isMe ? styles.myBubbleText : styles.theirBubbleText,
styles.recalledText,
]}
selectable={false}
>
</Text>,
[styles.recalledBubble]
);
}
@@ -282,71 +295,60 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
// 如果没有 segments显示空内容或错误提示
if (!hasSegments) {
return (
<View style={[
styles.messageBubble,
isMe ? styles.myBubble : styles.theirBubble,
]}>
<Text
style={[
isMe ? styles.myBubbleText : styles.theirBubbleText,
{ opacity: 0.5 },
]}
selectable={false}
>
[]
</Text>
</View>
return renderBubbleShell(
<Text
style={[
isMe ? styles.myBubbleText : styles.theirBubbleText,
{ opacity: 0.5 },
]}
selectable={false}
>
[]
</Text>
);
}
// 检查是否有回复,用于调整气泡样式
const hasReply = segments.some(s => s.type === 'reply');
return (
<View style={[
styles.messageBubble,
isMe ? styles.myBubble : styles.theirBubble,
hasReply && segmentStyles.replyBubble,
isPureImageMessage && segmentStyles.pureImageBubble,
]}>
<MessageSegmentsRenderer
segments={segments}
isMe={isMe}
currentUserId={currentUserId}
memberMap={memberMap}
replyMessage={getReplyMessage()}
getSenderInfo={getSenderInfo}
onAtPress={() => undefined}
onReplyPress={onReplyPress}
onImagePress={(url) => {
// 查找点击的图片索引
const clickIndex = imageSegments.findIndex(
img => img.url === url || img.thumbnail_url === url
);
if (clickIndex !== -1 && onImagePress) {
onImagePress(imageSegments, clickIndex);
}
}}
onImageLongPress={(position?: { x: number; y: number }) => {
// 图片长按触发和消息气泡一样的菜单
// 如果有位置信息,直接使用;否则使用气泡位置
if (position) {
onLongPress(message, {
x: 0,
y: 0,
width: 0,
height: 0,
pressX: position.x,
pressY: position.y,
});
} else {
handleLongPress();
}
}}
onLinkPress={() => undefined}
/>
</View>
return renderBubbleShell(
<MessageSegmentsRenderer
segments={segments}
isMe={isMe}
currentUserId={currentUserId}
memberMap={memberMap}
replyMessage={getReplyMessage()}
getSenderInfo={getSenderInfo}
onAtPress={() => undefined}
onReplyPress={onReplyPress}
onImagePress={(url) => {
// 查找点击的图片索引
const clickIndex = imageSegments.findIndex(
img => img.url === url || img.thumbnail_url === url
);
if (clickIndex !== -1 && onImagePress) {
onImagePress(imageSegments, clickIndex);
}
}}
onImageLongPress={(position?: { x: number; y: number }) => {
// 图片长按触发和消息气泡一样的菜单
// 如果有位置信息,直接使用;否则使用气泡位置
if (position) {
onLongPress(message, {
x: 0,
y: 0,
width: 0,
height: 0,
pressX: position.x,
pressY: position.y,
});
} else {
handleLongPress();
}
}}
onLinkPress={() => undefined}
/>,
[hasReply && segmentStyles.replyBubble, isPureImageMessage && segmentStyles.pureImageBubble]
);
};

View File

@@ -6,7 +6,7 @@ import React from 'react';
import { View, TouchableOpacity } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Text } from '../../../../components/common';
import { chatScreenStyles as styles } from './styles';
import { useChatScreenStyles } from './styles';
import { MORE_ACTIONS } from './constants';
import { MorePanelProps } from './types';
@@ -14,6 +14,7 @@ export const MorePanel: React.FC<MorePanelProps> = ({
onAction,
disabledActionIds = [],
}) => {
const styles = useChatScreenStyles();
const disabledSet = new Set(disabledActionIds);
return (
<View style={styles.panelContainer}>

View File

@@ -31,7 +31,7 @@ import {
UserDTO,
extractTextFromSegments,
} from '../../../../types/dto';
import { colors, spacing } from '../../../../theme';
import { spacing, useAppColors, type AppColors } from '../../../../theme';
import { SenderInfo } from './types';
const IMAGE_MAX_WIDTH = 200;
@@ -142,12 +142,11 @@ export const renderSegment = (
/**
* 渲染文本 Segment
*/
const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => {
// 兼容 content 和 text 两种字段
const TextSegmentBody: React.FC<{ data: TextSegmentData; isMe: boolean }> = ({ data, isMe }) => {
const styles = useSegmentStyles();
const text = data.content ?? data.text ?? '';
return (
<Text
key={`text-${text.substring(0, 10)}`}
style={[styles.textContent, isMe ? styles.textMe : styles.textOther]}
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
*/
@@ -166,6 +170,7 @@ const ImageSegment: React.FC<{
onImagePress?: (url: string) => void;
onImageLongPress?: (position?: { x: number; y: number }) => void;
}> = ({ data, isMe, onImagePress, onImageLongPress }) => {
const styles = useSegmentStyles();
const pressPositionRef = useRef({ x: 0, y: 0 });
const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
const [loadError, setLoadError] = useState(false);
@@ -362,16 +367,13 @@ const renderImageSegment = (
/**
* 渲染 @ Segment
*/
const renderAtSegment = (
data: AtSegmentData,
props: Omit<SegmentRendererProps, 'segment'>
): React.ReactNode => {
const AtSegmentBody: React.FC<{
data: AtSegmentData;
props: Omit<SegmentRendererProps, 'segment'>;
}> = ({ data, props }) => {
const styles = useSegmentStyles();
const { currentUserId, memberMap, onAtPress, isMe } = props;
// 判断是否@当前用户
const isAtMe = data.user_id === currentUserId || data.user_id === 'all';
// 优先从 memberMap 中查找昵称,兜底使用 data.nickname兼容旧消息再兜底显示"用户"
let displayName: string;
if (data.user_id === 'all') {
displayName = '所有人';
@@ -379,10 +381,8 @@ const renderAtSegment = (
const member = memberMap?.get(data.user_id);
displayName = member?.nickname || data.nickname || '用户';
}
return (
<Text
key={`at-${data.user_id}`}
style={[
styles.atText,
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
*/
const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNode => {
// 如果有自定义表情URL显示图片
const FaceSegmentBody: React.FC<{ data: FaceSegmentData }> = ({ data }) => {
const styles = useSegmentStyles();
if (data.url) {
return (
<ExpoImage
key={`face-${data.id}`}
source={{ uri: data.url }}
style={styles.faceImage}
contentFit="cover"
@@ -411,29 +415,32 @@ const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNod
/>
);
}
// 否则显示表情名称(后续可以映射到本地表情)
return (
<Text key={`face-${data.id}`} style={styles.faceText}>
<Text style={styles.faceText}>
[{data.name || `表情${data.id}`}]
</Text>
);
};
const renderFaceSegment = (data: FaceSegmentData, _isMe: boolean): React.ReactNode => (
<FaceSegmentBody key={`face-${data.id}`} data={data} />
);
/**
* 渲染语音 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 (
<TouchableOpacity
key={`voice-${data.url}`}
style={[styles.voiceContainer, isMe ? styles.voiceMe : styles.voiceOther]}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name="microphone"
size={20}
color={isMe ? '#FFFFFF' : '#666666'}
color={isMe ? themeColors.primary.contrast : themeColors.chat.textTertiary}
/>
<Text style={[styles.voiceDuration, isMe ? styles.textMe : styles.textOther]}>
{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 组件 - 支持点击播放
*/
const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ data, isMe }) => {
const styles = useSegmentStyles();
const themeColors = useAppColors();
const [playerVisible, setPlayerVisible] = useState(false);
const handlePress = useCallback(() => {
@@ -469,11 +482,11 @@ const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ dat
/>
) : (
<View style={styles.videoPlaceholder}>
<MaterialCommunityIcons name="video" size={32} color="#FFFFFF" />
<MaterialCommunityIcons name="video" size={32} color={themeColors.primary.contrast} />
</View>
)}
<View style={styles.videoPlayIcon}>
<MaterialCommunityIcons name="play-circle" size={40} color="#FFFFFF" />
<MaterialCommunityIcons name="play-circle" size={40} color={themeColors.primary.contrast} />
</View>
{data.duration && (
<Text style={styles.videoDuration}>{formatDuration(data.duration)}</Text>
@@ -496,12 +509,12 @@ const renderVideoSegment = (data: VideoSegmentData, isMe: boolean): React.ReactN
/**
* 渲染文件 Segment
*/
const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => {
const FileSegmentBody: React.FC<{ data: FileSegmentData; isMe: boolean }> = ({ data, isMe }) => {
const styles = useSegmentStyles();
const themeColors = useAppColors();
const fileSize = data.size ? formatFileSize(data.size) : '';
return (
<TouchableOpacity
key={`file-${data.url}`}
style={[styles.fileContainer, isMe ? styles.fileMe : styles.fileOther]}
activeOpacity={0.7}
>
@@ -509,33 +522,36 @@ const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNod
<MaterialCommunityIcons
name="file-document"
size={28}
color={isMe ? '#FFFFFF' : colors.primary.main}
color={isMe ? themeColors.primary.contrast : themeColors.primary.main}
/>
</View>
<View style={styles.fileInfo}>
<Text
<Text
style={[styles.fileName, isMe ? styles.textMe : styles.textOther]}
numberOfLines={1}
>
{data.name}
</Text>
{fileSize && (
<Text style={styles.fileSize}>{fileSize}</Text>
)}
{fileSize ? <Text style={styles.fileSize}>{fileSize}</Text> : null}
</View>
</TouchableOpacity>
);
};
const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => (
<FileSegmentBody key={`file-${data.url}`} data={data} isMe={isMe} />
);
/**
* 渲染链接 Segment
*/
const renderLinkSegment = (
data: LinkSegmentData,
props: Omit<SegmentRendererProps, 'segment'>
): React.ReactNode => {
const LinkSegmentBody: React.FC<{
data: LinkSegmentData;
props: Omit<SegmentRendererProps, 'segment'>;
}> = ({ data, props }) => {
const styles = useSegmentStyles();
const { onLinkPress, isMe } = props;
const handlePress = async () => {
if (onLinkPress) {
onLinkPress(data.url);
@@ -552,26 +568,25 @@ const renderLinkSegment = (
}
}
};
return (
<TouchableOpacity
key={`link-${data.url}`}
style={[styles.linkContainer, isMe ? styles.linkMe : styles.linkOther]}
onPress={handlePress}
activeOpacity={0.7}
>
{data.image && (
{data.image ? (
<ExpoImage source={{ uri: data.image }} style={styles.linkImage} contentFit="cover" cachePolicy="memory" />
)}
) : null}
<View style={styles.linkContent}>
<Text style={styles.linkTitle} numberOfLines={2}>
{data.title || data.url}
</Text>
{data.description && (
{data.description ? (
<Text style={styles.linkDescription} numberOfLines={2}>
{data.description}
</Text>
)}
) : null}
<Text style={styles.linkUrl} numberOfLines={1}>
{(() => {
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,
getSenderInfo,
}) => {
const themeColors = useAppColors();
const styles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
if (!replyMessage) {
// 如果没有引用消息详情只显示引用ID
return (
@@ -605,9 +628,11 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
activeOpacity={0.7}
>
<View style={styles.replyLine} />
<Text style={styles.replyText} numberOfLines={2}>
</Text>
<View style={styles.replyContent}>
<Text style={styles.replyText} numberOfLines={2}>
</Text>
</View>
</TouchableOpacity>
);
}
@@ -658,6 +683,7 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
onPress={onPress}
activeOpacity={0.7}
>
<View style={styles.replyLine} />
<View style={styles.replyContent}>
<Text style={styles.replySender} numberOfLines={1}>
{senderName}:
@@ -702,7 +728,9 @@ export const MessageSegmentsRenderer: React.FC<{
onLinkPress,
getSenderInfo,
}) => {
// 找出 reply segment如果有
const themeColors = useAppColors();
const segStyles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
const replySegment = segments.find(s => s.type === 'reply');
const otherSegments = segments.filter(s => s.type !== 'reply');
const chunks = partitionMessageSegments(otherSegments);
@@ -717,58 +745,58 @@ export const MessageSegmentsRenderer: React.FC<{
onImageLongPress,
onLinkPress,
};
return (
<View style={styles.segmentsContainer}>
{/* 回复预览 */}
{replySegment && (
<ReplyPreviewSegment
replyData={(replySegment.data as ReplySegmentData)}
replyMessage={replyMessage}
isMe={isMe}
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
getSenderInfo={getSenderInfo}
/>
)}
{/* 其他 Segment行内文案与块级图片/媒体分行展示,多图纵向排列 */}
<View style={styles.segmentsColumn}>
{chunks.map((chunk, i) => {
if (chunk.kind === 'inline') {
<SegmentStylesContext.Provider value={segStyles}>
<View style={segStyles.segmentsContainer}>
{replySegment ? (
<ReplyPreviewSegment
replyData={replySegment.data as ReplySegmentData}
replyMessage={replyMessage}
isMe={isMe}
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
getSenderInfo={getSenderInfo}
/>
) : null}
<View style={segStyles.segmentsColumn}>
{chunks.map((chunk, i) => {
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 (
<View key={`inl-${i}`} style={styles.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 key={`blk-${i}`} style={segStyles.blockChunk}>
{renderSegment(chunk.segment, renderProps)}
</View>
);
}
if (chunk.kind === 'images') {
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')}`;
};
const styles = StyleSheet.create({
function createSegmentStyles(colors: AppColors) {
return StyleSheet.create({
// 容器
segmentsContainer: {
flexDirection: 'column',
@@ -826,10 +855,10 @@ const styles = StyleSheet.create({
fontWeight: '400',
},
textMe: {
color: '#1A1A1A', // 统一使用深色字体
color: colors.chat.textPrimary,
},
textOther: {
color: '#1A1A1A', // 统一使用深色字体
color: colors.chat.textPrimary,
},
// @提及 - Telegram风格蓝色高亮
@@ -845,7 +874,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
atTextOther: {
color: '#4A88C7',
color: colors.chat.link,
backgroundColor: 'rgba(74, 136, 199, 0.12)',
borderRadius: 4,
},
@@ -860,7 +889,7 @@ const styles = StyleSheet.create({
borderRadius: 12,
overflow: 'hidden',
marginTop: 4,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
@@ -892,7 +921,7 @@ const styles = StyleSheet.create({
paddingVertical: spacing.sm + 2,
borderRadius: 20,
minWidth: 100,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
@@ -916,7 +945,7 @@ const styles = StyleSheet.create({
overflow: 'hidden',
width: 220,
height: 165,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
@@ -953,7 +982,7 @@ const styles = StyleSheet.create({
position: 'absolute',
bottom: 10,
right: 10,
color: '#FFFFFF',
color: colors.primary.contrast,
fontSize: 13,
fontWeight: '600',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
@@ -971,7 +1000,7 @@ const styles = StyleSheet.create({
borderRadius: 16,
minWidth: 220,
maxWidth: 300,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
@@ -981,7 +1010,7 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255, 255, 255, 0.25)',
},
fileOther: {
backgroundColor: '#F8F9FA',
backgroundColor: colors.chat.surfaceRaised,
},
fileIcon: {
marginRight: spacing.md,
@@ -998,11 +1027,11 @@ const styles = StyleSheet.create({
fileName: {
fontSize: 15,
fontWeight: '600',
color: '#1A1A1A',
color: colors.chat.textPrimary,
},
fileSize: {
fontSize: 13,
color: '#8E8E93',
color: colors.chat.textSecondary,
marginTop: 4,
fontWeight: '500',
},
@@ -1012,8 +1041,8 @@ const styles = StyleSheet.create({
borderRadius: 16,
overflow: 'hidden',
maxWidth: 280,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
backgroundColor: colors.chat.card,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
@@ -1023,7 +1052,7 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255, 255, 255, 0.95)',
},
linkOther: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
},
linkImage: {
width: '100%',
@@ -1035,12 +1064,12 @@ const styles = StyleSheet.create({
linkTitle: {
fontSize: 15,
fontWeight: '600',
color: '#1A1A1A',
color: colors.chat.textPrimary,
lineHeight: 20,
},
linkDescription: {
fontSize: 13,
color: '#666666',
color: colors.chat.textTertiary,
marginTop: 6,
lineHeight: 18,
},
@@ -1051,16 +1080,17 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
// 回复预览 - Telegram风格简洁设计
// 回复预览 - Telegram风格左侧色条 + 简洁背景
replyPreview: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 0,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.sm + 2,
borderRadius: 6,
width: '100%',
minWidth: 200,
// 移除阴影
gap: 8,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
@@ -1074,28 +1104,45 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(0, 0, 0, 0.03)',
},
replyLine: {
width: 0,
marginRight: 0,
width: 3,
alignSelf: 'stretch',
minHeight: 28,
borderRadius: 2,
backgroundColor: colors.chat.link,
},
replyContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'nowrap',
minWidth: 0,
},
replySender: {
fontSize: 13,
fontWeight: '600',
color: '#4A88C7',
color: colors.chat.link,
marginRight: 6,
flexShrink: 0,
},
replyText: {
fontSize: 13,
color: '#666666',
color: colors.chat.textTertiary,
flex: 1,
flexShrink: 1,
},
});
});
}
type SegmentStyles = ReturnType<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;

View File

@@ -8,7 +8,7 @@ import { View, StyleSheet, Animated } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import { colors } from '../../../../theme';
import { useAppColors } from '../../../../theme';
// 滑动阈值
const SWIPE_THRESHOLD = 30;
@@ -27,6 +27,7 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
onReply,
enabled = true,
}) => {
const colors = useAppColors();
const translateX = useRef(new Animated.Value(0)).current;
const hasTriggeredRef = useRef(false);

View File

@@ -4,52 +4,40 @@
*/
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
import { colors, spacing } from '../../../../theme';
import { spacing, type AppColors } from '../../../../theme';
// 气泡圆角大小(参考 Element X: 12pt
export const BUBBLE_RADIUS = 16;
// 气泡颜色方案(参考 Element X 的语义化颜色)
export const bubbleColors = {
// 自己发送的消息
outgoing: {
background: '#E8E8E8', // Element X 风格:灰色系
text: '#1A1A1A',
},
// 收到的消息
incoming: {
background: '#FFFFFF',
text: '#1A1A1A',
},
// 被回复的消息高亮
replyHighlight: {
background: 'rgba(74, 136, 199, 0.1)',
borderLeft: '#4A88C7',
},
};
export function getBubbleColors(colors: AppColors) {
return {
outgoing: {
background: colors.chat.bubbleOutgoing,
text: colors.chat.textPrimary,
},
incoming: {
background: colors.chat.bubbleIncoming,
text: colors.chat.textPrimary,
},
replyHighlight: {
background: colors.chat.replyTint,
borderLeft: colors.chat.replyBorder,
},
};
}
// 消息在组中的位置类型
export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last';
/**
* 根据消息在组中的位置获取圆角样式
* 参考 Element X 的动态圆角逻辑
*/
export const getBubbleBorderRadius = (
isMe: boolean,
position: MessageGroupPosition
): ViewStyle => {
export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => {
const radius = BUBBLE_RADIUS;
if (isMe) {
// 自己发送的消息
switch (position) {
case 'single':
return {
borderTopLeftRadius: radius,
borderTopRightRadius: radius,
borderBottomLeftRadius: radius,
borderBottomRightRadius: 4, // 尖角在右下
borderBottomRightRadius: 4,
};
case 'first':
return {
@@ -74,11 +62,10 @@ export const getBubbleBorderRadius = (
};
}
} else {
// 收到的消息
switch (position) {
case 'single':
return {
borderTopLeftRadius: 4, // 尖角在左上
borderTopLeftRadius: 4,
borderTopRightRadius: radius,
borderBottomLeftRadius: radius,
borderBottomRightRadius: radius,
@@ -108,9 +95,6 @@ export const getBubbleBorderRadius = (
}
};
/**
* 判断消息在组中的位置
*/
export const getMessageGroupPosition = (
index: number,
messages: Array<{ sender_id: string }>,
@@ -118,13 +102,13 @@ export const getMessageGroupPosition = (
): MessageGroupPosition => {
const currentMessage = messages[index];
if (!currentMessage) return 'single';
const prevMessage = index > 0 ? messages[index - 1] : null;
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
return 'single';
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
@@ -136,135 +120,114 @@ export const getMessageGroupPosition = (
}
};
/**
* 判断是否显示发送者信息(只在组的第一条消息显示)
*/
export const shouldShowSenderInfo = (
index: number,
messages: Array<{ sender_id: string }>,
messages: Array<{ sender_id: string }>
): boolean => {
if (index === 0) return true;
const currentMessage = messages[index];
const prevMessage = messages[index - 1];
return prevMessage.sender_id !== currentMessage.sender_id;
};
/**
* 获取气泡基础样式
*/
export const getBubbleBaseStyle = (isMe: boolean): ViewStyle => ({
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 4,
minWidth: 60,
maxWidth: '75%',
backgroundColor: isMe ? bubbleColors.outgoing.background : bubbleColors.incoming.background,
// Element X 风格的微妙阴影
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 2,
elevation: 2,
});
/**
* 获取文本样式
*/
export const getBubbleTextStyle = (isMe: boolean): TextStyle => ({
color: isMe ? bubbleColors.outgoing.text : bubbleColors.incoming.text,
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
fontWeight: '400',
});
/**
* 消息气泡样式集合
*/
export const bubbleStyles = StyleSheet.create({
// 基础气泡
bubble: {
export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle => {
const bc = getBubbleColors(colors);
return {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 4,
minWidth: 60,
},
// 自己发送的消息
outgoing: {
backgroundColor: bubbleColors.outgoing.background,
},
// 收到的消息
incoming: {
backgroundColor: bubbleColors.incoming.background,
},
// 文本样式
text: {
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
fontWeight: '400',
},
// 阴影样式Element X 风格)
shadow: {
shadowColor: '#000',
maxWidth: '75%',
backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 2,
elevation: 2,
},
// 长按反馈阴影
longPressShadow: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 6,
},
// 被回复消息的高亮边框
replyHighlight: {
borderLeftWidth: 3,
borderLeftColor: colors.primary.main,
backgroundColor: bubbleColors.replyHighlight.background,
},
// 已撤回消息
recalled: {
backgroundColor: '#F5F7FA',
borderWidth: 1,
borderColor: '#E8E8E8',
borderStyle: 'dashed',
borderRadius: 12,
},
// 系统通知
systemNotice: {
alignItems: 'center',
marginVertical: spacing.sm,
},
systemNoticeText: {
fontSize: 12,
color: '#8E8E93',
backgroundColor: 'rgba(142, 142, 147, 0.12)',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: 12,
overflow: 'hidden',
},
});
};
};
export const getBubbleTextStyle = (isMe: boolean, colors: AppColors): TextStyle => {
const bc = getBubbleColors(colors);
return {
color: isMe ? bc.outgoing.text : bc.incoming.text,
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
fontWeight: '400',
};
};
export function createBubbleStyles(colors: AppColors) {
const bc = getBubbleColors(colors);
return StyleSheet.create({
bubble: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 4,
minWidth: 60,
},
outgoing: {
backgroundColor: bc.outgoing.background,
},
incoming: {
backgroundColor: bc.incoming.background,
},
text: {
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
fontWeight: '400',
},
shadow: {
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 2,
elevation: 2,
},
longPressShadow: {
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 6,
},
replyHighlight: {
borderLeftWidth: 3,
borderLeftColor: colors.primary.main,
backgroundColor: bc.replyHighlight.background,
},
recalled: {
backgroundColor: colors.chat.surfaceMuted,
borderWidth: 1,
borderColor: colors.chat.border,
borderStyle: 'dashed',
borderRadius: 12,
},
systemNotice: {
alignItems: 'center',
marginVertical: spacing.sm,
},
systemNoticeText: {
fontSize: 12,
color: colors.chat.textSecondary,
backgroundColor: colors.chat.surfaceMuted,
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: 12,
overflow: 'hidden',
},
});
}
export default {
BUBBLE_RADIUS,
bubbleColors,
getBubbleColors,
getBubbleBorderRadius,
getMessageGroupPosition,
shouldShowSenderInfo,
getBubbleBaseStyle,
getBubbleTextStyle,
bubbleStyles,
};
createBubbleStyles,
};

View File

@@ -9,7 +9,7 @@ export * from './types';
export * from './constants';
// 样式
export { chatScreenStyles } from './styles';
export { createChatScreenStyles, useChatScreenStyles } from './styles';
// 组件
export { EmojiPanel } from './EmojiPanel';

View File

@@ -3,8 +3,9 @@
* 支持响应式布局
*/
import { useMemo } from 'react';
import { StyleSheet, Dimensions } from 'react-native';
import { colors, spacing, shadows } from '../../../../theme';
import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
@@ -18,19 +19,20 @@ const BREAKPOINTS = {
const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop;
const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet;
export const chatScreenStyles = StyleSheet.create({
export function createChatScreenStyles(colors: AppColors) {
return StyleSheet.create({
// 容器
container: {
flex: 1,
backgroundColor: '#F0F2F5',
backgroundColor: colors.chat.screen,
},
// 头部
headerContainer: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 107, 53, 0.12)',
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.06,
shadowRadius: 10,
@@ -42,7 +44,7 @@ export const chatScreenStyles = StyleSheet.create({
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 2,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
},
backButton: {
width: 38,
@@ -112,36 +114,61 @@ export const chatScreenStyles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
borderRadius: 19,
backgroundColor: '#F7F8FA',
backgroundColor: colors.chat.surfaceRaised,
borderWidth: 1,
borderColor: '#ECEFF3',
borderColor: colors.chat.borderLight,
},
// 消息列表
messageListContainer: {
flex: 1,
},
// 回到底部Telegram 式浮动条)
jumpToLatestFab: {
position: 'absolute',
right: 14,
bottom: 10,
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.96)',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(0, 0, 0, 0.08)',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12,
shadowRadius: 6,
elevation: 4,
},
jumpToLatestFabText: {
fontSize: 13,
fontWeight: '600',
color: colors.primary.main,
},
listContent: {
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
paddingBottom: spacing.xl,
},
// 时间分隔 - QQ风格更明显的时间标签
// 时间分隔 - 轻量胶囊(弱化字重,接近 Telegram 日期条)
timeContainer: {
alignItems: 'center',
marginVertical: spacing.lg,
},
timeText: {
color: '#8E8E93',
fontSize: 12,
backgroundColor: 'rgba(142, 142, 147, 0.12)',
paddingHorizontal: spacing.md,
paddingVertical: 6,
borderRadius: 14,
fontWeight: '600',
color: 'rgba(142, 142, 147, 0.95)',
fontSize: 11,
backgroundColor: 'rgba(142, 142, 147, 0.1)',
paddingHorizontal: spacing.sm + 4,
paddingVertical: 5,
borderRadius: 12,
fontWeight: '500',
overflow: 'hidden',
letterSpacing: 0.3,
letterSpacing: 0.2,
},
// 系统通知
@@ -152,7 +179,7 @@ export const chatScreenStyles = StyleSheet.create({
},
systemNoticeText: {
fontSize: 12,
color: '#8E8E93',
color: colors.chat.textSecondary,
backgroundColor: 'rgba(142, 142, 147, 0.12)',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
@@ -177,7 +204,7 @@ export const chatScreenStyles = StyleSheet.create({
// 头像
avatarWrapper: {
marginHorizontal: 2,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
@@ -185,7 +212,7 @@ export const chatScreenStyles = StyleSheet.create({
},
groupAvatarWrapper: {
marginHorizontal: 2,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
@@ -200,53 +227,59 @@ export const chatScreenStyles = StyleSheet.create({
},
senderName: {
fontSize: 13,
color: '#4A88C7',
color: colors.chat.link,
marginBottom: 4,
marginLeft: 4,
fontWeight: '600',
},
// 消息气泡 - Telegram风格浅色背景统一字体颜色
messageBubble: {
// 消息气泡 - 外层极轻阴影(纯文字气泡更接近扁平 IM
messageBubbleOuter: {
borderRadius: 16,
minWidth: 60,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 0.5 },
shadowOpacity: 0.04,
shadowRadius: 1,
elevation: 1,
},
myBubbleOuter: {
borderBottomRightRadius: 4,
},
theirBubbleOuter: {
borderTopLeftRadius: 4,
},
// 内层:背景 + 裁剪,防止回复条/@ 高亮在圆角外露出颜色
messageBubbleInner: {
borderRadius: 16,
overflow: 'hidden',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 4,
borderRadius: 16,
// 自适应内容宽度
minWidth: 60,
},
myBubble: {
backgroundColor: '#DFF2FF', // Telegram风格的浅蓝色
borderBottomRightRadius: 4, // 右下角尖,箭头在右下
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
elevation: 2,
myBubbleInner: {
backgroundColor: colors.chat.replyTint,
borderBottomRightRadius: 4,
},
theirBubble: {
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 4, // 左上角尖,箭头在左上
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
elevation: 2,
theirBubbleInner: {
backgroundColor: colors.chat.card,
borderTopLeftRadius: 4,
},
recalledBubble: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
borderWidth: 1,
borderColor: '#E8E8E8',
borderColor: colors.chat.border,
borderStyle: 'dashed',
},
myBubbleText: {
color: '#1A1A1A', // 统一使用深色字体
color: colors.chat.textPrimary, // 主文案
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
fontWeight: '400',
},
theirBubbleText: {
color: '#1A1A1A', // 统一使用深色字体
color: colors.chat.textPrimary, // 主文案
fontSize: 16,
lineHeight: 23,
letterSpacing: 0.2,
@@ -255,29 +288,27 @@ export const chatScreenStyles = StyleSheet.create({
// 选中状态
selectedBubble: {
borderWidth: 2,
borderColor: '#7FB6E6',
borderColor: colors.chat.replyBorder,
},
// 仅对齐右侧,不再套一层浅色底,避免与气泡叠色产生「边缘多出一块蓝」
myMessageContentPanel: {
backgroundColor: '#DFF2FF',
borderRadius: 14,
paddingHorizontal: 4,
paddingBottom: 4,
alignItems: 'flex-end',
},
mySelectedBubble: {
backgroundColor: '#CFEAFF',
backgroundColor: colors.chat.replyTintActive,
borderWidth: 2,
borderColor: '#7FB6E6',
borderColor: colors.chat.replyBorder,
},
theirSelectedBubble: {
backgroundColor: '#EEF5FC',
backgroundColor: colors.chat.menuHighlight,
borderWidth: 2,
borderColor: '#7FB6E6',
borderColor: colors.chat.replyBorder,
},
selectedText: {
// 文本选中时的样式
},
recalledText: {
color: '#8E8E93',
color: colors.chat.textSecondary,
fontStyle: 'italic',
},
@@ -285,7 +316,7 @@ export const chatScreenStyles = StyleSheet.create({
imageBubble: {
borderRadius: 18,
overflow: 'hidden',
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.15,
shadowRadius: 6,
@@ -314,27 +345,28 @@ export const chatScreenStyles = StyleSheet.create({
},
readStatusText: {
fontSize: 10,
color: '#8E8E93',
color: colors.chat.textSecondary,
fontWeight: '500',
},
readStatusTextRead: {
color: '#34C759',
color: colors.chat.success,
},
// 输入框区域
// 输入框区域 - 与列表背景同色底栏,顶部细分隔
inputWrapper: {
backgroundColor: 'transparent',
backgroundColor: colors.chat.screen,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, 0.06)',
},
inputContainer: {
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider,
borderRadius: 24,
marginHorizontal: 14,
marginBottom: 12,
backgroundColor: 'transparent',
borderWidth: 0,
borderRadius: 0,
marginHorizontal: 10,
marginBottom: 10,
marginTop: 2,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
// 移除明显的阴影效果,改用更微妙的边框
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
@@ -344,7 +376,7 @@ export const chatScreenStyles = StyleSheet.create({
inputInner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F7F8FA',
backgroundColor: colors.chat.surfaceRaised,
borderRadius: 20,
paddingHorizontal: spacing.xs,
paddingVertical: 4,
@@ -357,7 +389,7 @@ export const chatScreenStyles = StyleSheet.create({
elevation: 0,
},
inputInnerMuted: {
backgroundColor: '#F0F0F0',
backgroundColor: colors.chat.borderHairline,
opacity: 0.7,
},
@@ -366,7 +398,7 @@ export const chatScreenStyles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFF5F5',
backgroundColor: colors.chat.warningBg,
paddingVertical: spacing.xs,
paddingHorizontal: spacing.md,
marginBottom: spacing.xs,
@@ -375,7 +407,7 @@ export const chatScreenStyles = StyleSheet.create({
},
mutedBannerText: {
fontSize: 13,
color: '#FF3B30',
color: colors.chat.danger,
fontWeight: '500',
},
@@ -421,14 +453,14 @@ export const chatScreenStyles = StyleSheet.create({
},
input: {
fontSize: 16,
color: '#1A1A1A',
color: colors.chat.textPrimary,
paddingTop: 8,
paddingBottom: 8,
maxHeight: 100,
lineHeight: 20,
},
inputMuted: {
color: '#CCC',
color: colors.chat.iconMuted,
},
inputTransparent: {
color: 'transparent',
@@ -444,7 +476,7 @@ export const chatScreenStyles = StyleSheet.create({
},
inputHighlightText: {
fontSize: 16,
color: '#1A1A1A',
color: colors.chat.textPrimary,
lineHeight: 20,
},
inputHighlightMention: {
@@ -462,10 +494,10 @@ export const chatScreenStyles = StyleSheet.create({
// 面板
panelWrapper: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
shadowColor: '#000',
borderTopColor: colors.chat.border,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
@@ -473,11 +505,11 @@ export const chatScreenStyles = StyleSheet.create({
},
// 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充
emojiPanelWrapper: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
},
panelContainer: {
flex: 1,
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
},
// 表情面板
@@ -505,16 +537,16 @@ export const chatScreenStyles = StyleSheet.create({
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
borderTopColor: colors.chat.border,
},
panelCloseButton: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderRadius: 8,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
@@ -533,8 +565,8 @@ export const chatScreenStyles = StyleSheet.create({
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
backgroundColor: '#FFFFFF',
borderTopColor: colors.chat.border,
backgroundColor: colors.chat.card,
},
panelTab: {
width: 44,
@@ -545,7 +577,7 @@ export const chatScreenStyles = StyleSheet.create({
marginRight: spacing.xs,
},
panelTabActive: {
backgroundColor: '#DFF2FF',
backgroundColor: colors.chat.replyTint,
},
panelTabEmoji: {
fontSize: 24,
@@ -553,7 +585,7 @@ export const chatScreenStyles = StyleSheet.create({
panelTabDivider: {
width: 1,
height: 24,
backgroundColor: '#E8E8E8',
backgroundColor: colors.chat.bubbleOutgoing,
marginHorizontal: spacing.sm,
},
@@ -571,13 +603,13 @@ export const chatScreenStyles = StyleSheet.create({
},
stickerEmptyText: {
fontSize: 16,
color: '#666',
color: colors.chat.textTertiary,
marginTop: spacing.md,
fontWeight: '500',
},
stickerEmptySubText: {
fontSize: 13,
color: '#999',
color: colors.chat.textMuted,
marginTop: spacing.xs,
},
stickerGrid: {
@@ -593,7 +625,7 @@ export const chatScreenStyles = StyleSheet.create({
margin: 4,
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#F5F5F5',
backgroundColor: colors.chat.surfaceInput,
},
stickerImage: {
width: '100%',
@@ -618,8 +650,8 @@ export const chatScreenStyles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.sm,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
backgroundColor: colors.chat.card,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
@@ -627,7 +659,7 @@ export const chatScreenStyles = StyleSheet.create({
},
moreItemText: {
fontSize: 13,
color: '#666',
color: colors.chat.textTertiary,
fontWeight: '500',
},
@@ -636,7 +668,7 @@ export const chatScreenStyles = StyleSheet.create({
position: 'absolute',
left: 12,
right: 12,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderRadius: 18,
shadowColor: colors.primary.main,
shadowOffset: { width: 0, height: -3 },
@@ -651,7 +683,7 @@ export const chatScreenStyles = StyleSheet.create({
// @成员选择面板
mentionPanelContainer: {
flex: 1,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderRadius: 18,
},
mentionPanelDragHandle: {
@@ -663,7 +695,7 @@ export const chatScreenStyles = StyleSheet.create({
width: 32,
height: 3,
borderRadius: 2,
backgroundColor: '#E0E0E0',
backgroundColor: colors.chat.sheetGrip,
},
mentionPanelHeader: {
flexDirection: 'row',
@@ -673,12 +705,12 @@ export const chatScreenStyles = StyleSheet.create({
paddingTop: 4,
paddingBottom: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#F0F0F0',
borderBottomColor: colors.chat.borderHairline,
},
mentionPanelTitle: {
fontSize: 12,
fontWeight: '600',
color: '#AAAAAA',
color: colors.chat.textPlaceholder,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
@@ -686,7 +718,7 @@ export const chatScreenStyles = StyleSheet.create({
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: '#F5F5F5',
backgroundColor: colors.chat.surfaceInput,
alignItems: 'center',
justifyContent: 'center',
},
@@ -728,7 +760,7 @@ export const chatScreenStyles = StyleSheet.create({
mentionItemName: {
fontSize: 15,
fontWeight: '500',
color: '#1A1A1A',
color: colors.chat.textPrimary,
},
mentionItemNameAll: {
fontSize: 15,
@@ -737,7 +769,7 @@ export const chatScreenStyles = StyleSheet.create({
},
mentionItemSub: {
fontSize: 12,
color: '#BBBBBB',
color: colors.chat.iconMuted,
marginTop: 1,
},
mentionItemRole: {
@@ -752,7 +784,7 @@ export const chatScreenStyles = StyleSheet.create({
},
mentionEmptyText: {
fontSize: 14,
color: '#C8C8C8',
color: colors.chat.iconSoft,
},
// 长按菜单 - 底部横条形式类似QQ
@@ -763,10 +795,10 @@ export const chatScreenStyles = StyleSheet.create({
},
// 旧版菜单样式(保留兼容)
menuContainer: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderRadius: 12,
minWidth: 160,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 12,
@@ -781,21 +813,21 @@ export const chatScreenStyles = StyleSheet.create({
},
menuItemBorder: {
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
borderBottomColor: colors.chat.borderHairline,
},
menuItemText: {
fontSize: 15,
color: '#333',
color: colors.text.primary,
fontWeight: '500',
},
// 底部菜单容器
bottomMenuContainer: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: spacing.md,
paddingBottom: spacing.xl,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 8,
@@ -803,7 +835,7 @@ export const chatScreenStyles = StyleSheet.create({
},
// 消息预览区域
messagePreviewContainer: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
marginHorizontal: spacing.md,
marginBottom: spacing.md,
paddingHorizontal: spacing.md,
@@ -813,7 +845,7 @@ export const chatScreenStyles = StyleSheet.create({
},
messagePreviewText: {
fontSize: 14,
color: '#666',
color: colors.chat.textTertiary,
lineHeight: 20,
},
// 操作按钮横条
@@ -824,7 +856,7 @@ export const chatScreenStyles = StyleSheet.create({
paddingHorizontal: spacing.sm,
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
borderBottomColor: colors.chat.borderHairline,
marginBottom: spacing.md,
},
menuActionItem: {
@@ -839,19 +871,19 @@ export const chatScreenStyles = StyleSheet.create({
width: 56,
height: 56,
borderRadius: 16,
backgroundColor: '#F0F7FF',
backgroundColor: colors.chat.overlayQuote,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 8,
},
menuActionLabel: {
fontSize: 13,
color: '#333',
color: colors.text.primary,
fontWeight: '500',
},
// 取消按钮
menuCancelButton: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
marginHorizontal: spacing.md,
paddingVertical: spacing.md,
borderRadius: 12,
@@ -859,7 +891,7 @@ export const chatScreenStyles = StyleSheet.create({
},
menuCancelText: {
fontSize: 16,
color: '#333',
color: colors.text.primary,
fontWeight: '600',
},
@@ -874,7 +906,7 @@ export const chatScreenStyles = StyleSheet.create({
borderRadius: 6,
paddingVertical: 6,
paddingHorizontal: 2,
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
@@ -901,20 +933,21 @@ export const chatScreenStyles = StyleSheet.create({
},
qqMenuItemLabel: {
fontSize: 11,
color: '#FFFFFF',
color: colors.primary.contrast,
fontWeight: '500',
},
// 回复预览
// 回复预览(输入区)- 与气泡内引用同色条
replyPreviewContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F7FA',
backgroundColor: 'rgba(74, 136, 199, 0.08)',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderLeftWidth: 3,
borderLeftColor: colors.primary.main,
borderLeftColor: colors.chat.link,
marginBottom: spacing.xs,
borderRadius: 8,
},
replyPreviewContent: {
flex: 1,
@@ -932,7 +965,7 @@ export const chatScreenStyles = StyleSheet.create({
},
replyPreviewMessage: {
fontSize: 12,
color: '#8E8E93',
color: colors.chat.textSecondary,
marginTop: 2,
},
replyPreviewClose: {
@@ -948,11 +981,11 @@ export const chatScreenStyles = StyleSheet.create({
zIndex: 1000,
},
overlayContent: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
padding: spacing.xl,
borderRadius: 16,
alignItems: 'center',
shadowColor: '#000',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 12,
@@ -961,13 +994,13 @@ export const chatScreenStyles = StyleSheet.create({
overlayText: {
marginTop: spacing.md,
fontSize: 15,
color: '#666',
color: colors.chat.textTertiary,
fontWeight: '500',
},
// 表情包添加按钮(管理界面第一位)
stickerAddButton: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderWidth: 1.5,
borderColor: colors.primary.main,
borderStyle: 'dashed',
@@ -986,9 +1019,9 @@ export const chatScreenStyles = StyleSheet.create({
// 表情包管理按钮(表情面板第一位)
stickerManageButton: {
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderWidth: 1,
borderColor: '#E8E8E8',
borderColor: colors.chat.border,
borderStyle: 'dashed',
alignItems: 'center',
justifyContent: 'center',
@@ -1000,7 +1033,7 @@ export const chatScreenStyles = StyleSheet.create({
},
stickerManageText: {
fontSize: 12,
color: '#666',
color: colors.chat.textTertiary,
marginTop: 4,
fontWeight: '500',
},
@@ -1018,20 +1051,20 @@ export const chatScreenStyles = StyleSheet.create({
width: 36,
height: 5,
borderRadius: 3,
backgroundColor: '#E0E0E0',
backgroundColor: colors.chat.sheetGrip,
alignSelf: 'center',
marginTop: 8,
marginBottom: 4,
},
manageHeaderDivider: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#F0F0F0',
backgroundColor: colors.chat.borderHairline,
},
// 表情包选择状态
stickerItemSelected: {
borderWidth: 2,
borderColor: '#7FB6E6',
borderColor: colors.chat.replyBorder,
},
stickerCheckOverlay: {
...StyleSheet.absoluteFillObject,
@@ -1045,19 +1078,19 @@ export const chatScreenStyles = StyleSheet.create({
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: '#FFF',
borderColor: colors.chat.card,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
alignItems: 'center',
justifyContent: 'center',
},
stickerCheckBoxSelected: {
backgroundColor: '#7FB6E6',
borderColor: '#7FB6E6',
backgroundColor: colors.chat.replyBorder,
borderColor: colors.chat.replyBorder,
},
// 表情管理模态框
manageModalContainer: {
backgroundColor: '#F5F7FA',
backgroundColor: colors.chat.surfaceMuted,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
overflow: 'hidden',
@@ -1068,9 +1101,9 @@ export const chatScreenStyles = StyleSheet.create({
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderBottomWidth: 1,
borderBottomColor: '#E8E8E8',
borderBottomColor: colors.chat.border,
},
manageHeaderButton: {
minWidth: 60,
@@ -1079,28 +1112,28 @@ export const chatScreenStyles = StyleSheet.create({
manageModalTitle: {
fontSize: 17,
fontWeight: '600',
color: '#1A1A1A',
color: colors.chat.textPrimary,
},
manageDoneText: {
fontSize: 16,
color: '#4A88C7',
color: colors.chat.link,
fontWeight: '500',
},
manageSelectText: {
fontSize: 16,
color: '#4A88C7',
color: colors.chat.link,
fontWeight: '500',
},
manageInfoBar: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderBottomWidth: 1,
borderBottomColor: '#E8E8E8',
borderBottomColor: colors.chat.border,
},
manageInfoText: {
fontSize: 13,
color: '#8E8E93',
color: colors.chat.textSecondary,
},
manageStickerGrid: {
flexDirection: 'row',
@@ -1115,9 +1148,9 @@ export const chatScreenStyles = StyleSheet.create({
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: '#FFFFFF',
backgroundColor: colors.chat.card,
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
borderTopColor: colors.chat.border,
paddingBottom: 34, // 安全区域
},
manageSelectAllButton: {
@@ -1126,24 +1159,24 @@ export const chatScreenStyles = StyleSheet.create({
},
manageSelectAllText: {
fontSize: 15,
color: '#4A88C7',
color: colors.chat.link,
fontWeight: '500',
},
manageDeleteButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FF3B30',
backgroundColor: colors.chat.danger,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm + 2,
borderRadius: 20,
gap: 6,
},
manageDeleteButtonDisabled: {
backgroundColor: '#CCC',
backgroundColor: colors.chat.iconMuted,
},
manageDeleteText: {
fontSize: 15,
color: '#FFFFFF',
color: colors.primary.contrast,
fontWeight: '600',
},
manageTipBar: {
@@ -1152,12 +1185,12 @@ export const chatScreenStyles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: '#FFF9E6',
backgroundColor: colors.chat.tipBg,
gap: 6,
},
manageTipText: {
fontSize: 12,
color: '#999',
color: colors.chat.textMuted,
},
// 管理界面空状态
@@ -1186,5 +1219,9 @@ export const chatScreenStyles = StyleSheet.create({
textAlign: 'center',
},
});
}
export default chatScreenStyles;
export function useChatScreenStyles() {
const colors = useAppColors();
return useMemo(() => createChatScreenStyles(colors), [colors]);
}

View File

@@ -546,6 +546,13 @@ export const useChatScreen = () => {
isBrowsingHistoryRef.current = false;
}, [isHistoryLoadingLocked]);
/** 用户点击「回到底部」解除浏览历史锁并滚到最新消息端inverted 下 offset=0 */
const jumpToLatestMessages = useCallback(() => {
suppressAutoFollowRef.current = false;
isBrowsingHistoryRef.current = false;
scrollToLatest(true, true, 'jump-latest-button');
}, [scrollToLatest]);
const setBrowsingHistory = useCallback((browsing: boolean) => {
isBrowsingHistoryRef.current = browsing;
}, []);
@@ -1396,6 +1403,7 @@ export const useChatScreen = () => {
handleClearConversation,
handleMessageListContentSizeChange,
handleReachLatestEdge,
jumpToLatestMessages,
setBrowsingHistory,
};
};

View File

@@ -3,10 +3,10 @@
* onPress 引用不参与比较,请配合父组件 ref + 按 id 查最新会话使用。
*/
import React, { memo, useEffect, useRef, useState } from 'react';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, shadows } from '../../../theme';
import { spacing, shadows, useAppColors, type AppColors } from '../../../theme';
import {
ConversationResponse,
extractTextFromSegments,
@@ -150,6 +150,124 @@ function areConversationRowEqual(
return true;
}
function createConversationRowStyles(colors: AppColors) {
return StyleSheet.create({
conversationItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: 14,
backgroundColor: colors.background.paper,
marginHorizontal: spacing.md,
marginTop: spacing.sm,
borderRadius: 12,
...shadows.sm,
},
conversationItemSelected: {
backgroundColor: colors.primary.light + '20',
borderColor: colors.primary.main,
borderWidth: 1,
},
avatarContainer: {
position: 'relative',
},
systemAvatar: {
width: 50,
height: 50,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primary.main,
},
conversationContent: {
flex: 1,
marginLeft: spacing.md,
},
conversationHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
nameRow: {
flexDirection: 'row',
alignItems: 'center',
},
officialBadge: {
backgroundColor: colors.primary.main,
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
marginRight: spacing.xs,
},
officialBadgeText: {
color: colors.primary.contrast,
fontSize: 10,
fontWeight: '600',
},
userName: {
fontWeight: '600',
color: colors.text.primary,
fontSize: 16,
},
groupIcon: {
marginRight: 4,
},
memberCount: {
fontSize: 12,
color: colors.text.secondary,
marginLeft: 2,
},
pinnedIcon: {
marginLeft: 6,
},
groupAvatar: {
width: 50,
height: 50,
},
groupAvatarPlaceholder: {
width: 50,
height: 50,
borderRadius: 12,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
},
timeText: {
color: colors.text.secondary,
fontSize: 12,
},
messageRow: {
flexDirection: 'row',
alignItems: 'center',
},
messageText: {
flex: 1,
color: colors.text.secondary,
fontSize: 14,
},
unreadMessageText: {
color: colors.text.primary,
fontWeight: '500',
},
unreadBadge: {
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
paddingHorizontal: 6,
},
unreadBadgeText: {
color: colors.primary.contrast,
fontSize: 12,
fontWeight: '600',
},
});
}
const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
item,
scale,
@@ -158,6 +276,8 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
formatTime,
systemChannelId,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createConversationRowStyles(colors), [colors]);
const isSystemChannel = item.id === systemChannelId;
const isGroupChat = !!(item.type === 'group' && item.group);
@@ -196,7 +316,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
<View style={styles.avatarContainer}>
{isSystemChannel ? (
<View style={styles.systemAvatar}>
<MaterialCommunityIcons name="bell-ring" size={24} color="#FFF" />
<MaterialCommunityIcons name="bell-ring" size={24} color={colors.primary.contrast} />
</View>
) : isGroupChat ? (
<View style={styles.groupAvatar}>
@@ -204,7 +324,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
<Avatar source={displayAvatar} size={50} name={displayName} />
) : (
<View style={styles.groupAvatarPlaceholder}>
<MaterialCommunityIcons name="account-group" size={28} color="#FFF" />
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
</View>
)}
</View>
@@ -271,119 +391,3 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
ConversationListRowInner.displayName = 'ConversationListRow';
export const ConversationListRow = memo(ConversationListRowInner, areConversationRowEqual);
const styles = StyleSheet.create({
conversationItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: 14,
backgroundColor: '#FFF',
marginHorizontal: spacing.md,
marginTop: spacing.sm,
borderRadius: 12,
...shadows.sm,
},
conversationItemSelected: {
backgroundColor: colors.primary.light + '20',
borderColor: colors.primary.main,
borderWidth: 1,
},
avatarContainer: {
position: 'relative',
},
systemAvatar: {
width: 50,
height: 50,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primary.main,
},
conversationContent: {
flex: 1,
marginLeft: spacing.md,
},
conversationHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
nameRow: {
flexDirection: 'row',
alignItems: 'center',
},
officialBadge: {
backgroundColor: colors.primary.main,
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
marginRight: spacing.xs,
},
officialBadgeText: {
color: '#FFF',
fontSize: 10,
fontWeight: '600',
},
userName: {
fontWeight: '600',
color: '#333',
fontSize: 16,
},
groupIcon: {
marginRight: 4,
},
memberCount: {
fontSize: 12,
color: '#999',
marginLeft: 2,
},
pinnedIcon: {
marginLeft: 6,
},
groupAvatar: {
width: 50,
height: 50,
},
groupAvatarPlaceholder: {
width: 50,
height: 50,
borderRadius: 12,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
},
timeText: {
color: '#999',
fontSize: 12,
},
messageRow: {
flexDirection: 'row',
alignItems: 'center',
},
messageText: {
flex: 1,
color: '#888',
fontSize: 14,
},
unreadMessageText: {
color: '#333',
fontWeight: '500',
},
unreadBadge: {
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
paddingHorizontal: 6,
},
unreadBadgeText: {
color: '#FFF',
fontSize: 12,
fontWeight: '600',
},
});

View File

@@ -3,7 +3,7 @@
* 嵌入式聊天组件 - 用于桌面端双栏布局右侧
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import {
View,
FlatList,
@@ -19,7 +19,7 @@ import {
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Image as ExpoImage } from 'expo-image';
import { colors, spacing, fontSizes, shadows } from '../../../theme';
import { spacing, fontSizes, shadows, useAppColors, type AppColors } from '../../../theme';
import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto';
import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common';
import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores';
@@ -34,12 +34,210 @@ import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
const { width: screenWidth } = Dimensions.get('window');
const PANEL_WIDTH = 360;
function createEmbeddedChatStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.chat.screen,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
height: 56,
},
headerButton: {
padding: spacing.xs,
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
headerTitle: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
marginLeft: spacing.sm,
maxWidth: 200,
},
messageList: {
flex: 1,
backgroundColor: colors.chat.screen,
},
centerContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: spacing.xl,
},
emptyText: {
marginTop: spacing.md,
fontSize: 16,
color: colors.text.secondary,
},
emptySubtext: {
marginTop: spacing.xs,
fontSize: 14,
color: colors.text.disabled,
},
listContent: {
padding: spacing.md,
paddingBottom: spacing.xl,
},
messageRow: {
flexDirection: 'row',
alignItems: 'flex-end',
marginBottom: spacing.md,
maxWidth: screenWidth * 0.7,
},
messageRowLeft: {
alignSelf: 'flex-start',
},
messageRowRight: {
alignSelf: 'flex-end',
justifyContent: 'flex-end',
},
messageBubble: {
maxWidth: screenWidth * 0.5,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: 18,
marginHorizontal: spacing.sm,
},
messageBubbleMe: {
backgroundColor: colors.chat.replyTint,
borderBottomRightRadius: 4,
},
messageBubbleOther: {
backgroundColor: colors.chat.bubbleIncoming,
borderBottomLeftRadius: 4,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 0.5 },
shadowOpacity: 0.04,
shadowRadius: 1,
elevation: 1,
},
senderName: {
fontSize: 12,
color: colors.text.secondary,
marginBottom: 2,
},
messageText: {
fontSize: 15,
lineHeight: 20,
},
messageTextMe: {
color: colors.chat.textPrimary,
},
messageTextOther: {
color: colors.chat.textPrimary,
},
inputArea: {
backgroundColor: colors.chat.screen,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.divider,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
},
iconButton: {
padding: spacing.xs,
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
inputContainer: {
flex: 1,
backgroundColor: colors.chat.surfaceInput,
borderRadius: 20,
paddingHorizontal: spacing.md,
minHeight: 40,
justifyContent: 'center',
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
textInput: {
fontSize: 15,
color: colors.text.primary,
padding: 0,
maxHeight: 100,
},
sendButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.xs,
},
sendButtonDisabled: {
backgroundColor: colors.background.disabled,
},
messageTextRecalled: {
fontStyle: 'italic',
color: colors.text.secondary,
},
imageGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 4,
},
messageImage: {
borderRadius: 8,
marginRight: 4,
marginBottom: 4,
},
imagePlaceholder: {
width: 120,
height: 120,
borderRadius: 8,
backgroundColor: colors.background.disabled,
justifyContent: 'center',
alignItems: 'center',
marginRight: 4,
marginBottom: 4,
},
imagePlaceholderText: {
color: colors.text.secondary,
fontSize: 12,
marginTop: 4,
},
});
}
interface EmbeddedChatProps {
conversation: ConversationResponse;
onBack: () => void;
}
export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack }) => {
const colors = useAppColors();
const styles = useMemo(() => createEmbeddedChatStyles(colors), [colors]);
const router = useRouter();
const currentUser = useAuthStore(state => state.currentUser);
@@ -411,7 +609,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
<View style={styles.header}>
{/* 大屏幕(>= 768px时隐藏返回按钮 */}
{!isWideScreen ? (
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor="#666" />
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor={colors.text.secondary} />
) : (
<View style={styles.headerButton} />
)}
@@ -426,11 +624,11 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
{/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */}
{isWideScreen && isGroupChat ? (
<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 onPress={handleNavigateToFullChat} style={styles.headerButton}>
<MaterialCommunityIcons name="arrow-expand" size={22} color="#666" />
<MaterialCommunityIcons name="arrow-expand" size={22} color={colors.text.secondary} />
</TouchableOpacity>
)}
</View>
@@ -443,7 +641,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
</View>
) : messages.length === 0 ? (
<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.emptySubtext}></Text>
</View>
@@ -472,7 +670,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
<View style={styles.inputArea}>
<View style={styles.inputRow}>
<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>
<View style={styles.inputContainer}>
@@ -480,7 +678,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
ref={inputRef}
style={styles.textInput}
placeholder="发送消息..."
placeholderTextColor="#999"
placeholderTextColor={colors.text.hint}
value={inputText}
onChangeText={setInputText}
multiline={false}
@@ -494,7 +692,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
</View>
<TouchableOpacity style={styles.iconButton}>
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
<MaterialCommunityIcons name="emoticon-outline" size={28} color={colors.text.secondary} />
</TouchableOpacity>
<TouchableOpacity
@@ -503,9 +701,9 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
disabled={!inputText.trim() || 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>
</View>
@@ -604,190 +802,3 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
</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,
},
});

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text } from '../../../components/common';
import { colors, spacing, borderRadius, shadows } from '../../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../../theme';
interface GroupInfoSummaryCardProps {
groupName?: string;
@@ -14,6 +14,79 @@ interface GroupInfoSummaryCardProps {
memberCountText?: string;
}
function createGroupRequestSharedStyles(colors: AppColors) {
return StyleSheet.create({
headerCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.md,
...shadows.sm,
},
groupHeader: {
flexDirection: 'row',
alignItems: 'center',
},
groupInfo: {
marginLeft: spacing.lg,
flex: 1,
},
groupName: {
marginBottom: spacing.xs,
fontWeight: '700',
},
groupMeta: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
groupNoText: {
marginTop: spacing.xs,
},
descriptionContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginTop: spacing.md,
},
groupDesc: {
marginLeft: spacing.sm,
flex: 1,
lineHeight: 20,
},
footer: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.sm,
flexDirection: 'row',
backgroundColor: colors.background.default,
},
btn: {
flex: 1,
height: 42,
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
},
reject: {
marginRight: spacing.sm,
backgroundColor: colors.error.light + '25',
borderWidth: 1,
borderColor: colors.error.light,
},
approve: {
marginLeft: spacing.sm,
backgroundColor: colors.primary.main,
},
statusBox: {
alignItems: 'center',
paddingTop: spacing.sm,
backgroundColor: colors.background.default,
},
});
}
export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
groupName,
groupAvatar,
@@ -21,12 +94,16 @@ export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
groupDescription,
memberCountText,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
return (
<View style={styles.headerCard}>
<View style={styles.groupHeader}>
<Avatar source={groupAvatar || ''} size={88} name={groupName} />
<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}>
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary}>
@@ -63,93 +140,34 @@ export const DecisionFooter: React.FC<DecisionFooterProps> = ({
onApprove,
processedText = '该请求已处理',
}) => {
const colors = useAppColors();
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
const insets = useSafeAreaInsets();
if (canAction) {
return (
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
<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 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>
</View>
);
}
return (
<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>
);
};
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,
},
});

View File

@@ -12,7 +12,7 @@ import {
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../../theme';
import { authService } from '../../../services/authService';
import { useAuthStore } from '../../../stores';
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
@@ -41,6 +41,8 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
onClose,
onConfirm,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createMutualFollowSelectorStyles(colors), [colors]);
const { currentUser } = useAuthStore();
const [friendList, setFriendList] = useState<User[]>([]);
@@ -209,93 +211,96 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius['2xl'],
borderTopRightRadius: borderRadius['2xl'],
height: '80%',
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.xl,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.lg,
},
modalHeaderButton: {
paddingVertical: spacing.sm,
minWidth: 60,
},
modalTitle: {
fontWeight: '700',
fontSize: fontSizes.xl,
},
confirmButton: {
fontWeight: '600',
textAlign: 'right',
},
tipContainer: {
flexDirection: 'row',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.xs,
marginBottom: spacing.md,
},
tipText: {
fontWeight: '600',
},
selectedBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginBottom: spacing.md,
},
friendListContent: {
paddingBottom: spacing.xl,
},
friendItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.xs,
},
friendItemSelected: {
backgroundColor: colors.primary.light + '15',
},
friendInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '500',
marginBottom: 2,
},
checkbox: {
width: 26,
height: 26,
borderRadius: 13,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
},
checkboxSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
});
function createMutualFollowSelectorStyles(colors: AppColors) {
return StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius['2xl'],
borderTopRightRadius: borderRadius['2xl'],
height: '80%',
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.xl,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.lg,
},
modalHeaderButton: {
paddingVertical: spacing.sm,
minWidth: 60,
},
modalTitle: {
fontWeight: '700',
fontSize: fontSizes.xl,
color: colors.text.primary,
},
confirmButton: {
fontWeight: '600',
textAlign: 'right',
},
tipContainer: {
flexDirection: 'row',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.xs,
marginBottom: spacing.md,
},
tipText: {
fontWeight: '600',
},
selectedBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginBottom: spacing.md,
},
friendListContent: {
paddingBottom: spacing.xl,
},
friendItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.xs,
},
friendItemSelected: {
backgroundColor: colors.primary.light + '15',
},
friendInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '500',
marginBottom: 2,
},
checkbox: {
width: 26,
height: 26,
borderRadius: 13,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
},
checkboxSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
});
}
export default MutualFollowSelectorModal;

View File

@@ -9,7 +9,7 @@ import {
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Text, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { authService, resolveAuthApiError } from '../../services/authService';
@@ -17,6 +17,8 @@ import { showPrompt } from '../../services/promptService';
import { useAuthStore } from '../../stores';
export const AccountSecurityScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]);
const { isWideScreen, isMobile } = useResponsive();
const insets = useSafeAreaInsets();
const currentUser = useAuthStore((state) => state.currentUser);
@@ -232,7 +234,7 @@ export const AccountSecurityScreen: React.FC = () => {
disabled={sendingCode || countdown > 0}
>
{sendingCode ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
)}
@@ -244,7 +246,11 @@ export const AccountSecurityScreen: React.FC = () => {
onPress={handleVerifyEmail}
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>
</View>
</View>
@@ -298,7 +304,7 @@ export const AccountSecurityScreen: React.FC = () => {
disabled={sendingChangePwdCode || changePwdCountdown > 0}
>
{sendingChangePwdCode ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.sendCodeButtonText}>
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
@@ -323,7 +329,11 @@ export const AccountSecurityScreen: React.FC = () => {
onPress={handleChangePassword}
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>
</View>
</View>
@@ -343,109 +353,117 @@ export const AccountSecurityScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
sectionTitle: {
fontWeight: '600',
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
padding: spacing.md,
},
statusRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
},
statusBadge: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.sm,
},
statusVerified: {
backgroundColor: '#E8F5E9',
},
statusUnverified: {
backgroundColor: '#FFF3E0',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
height: 48,
marginBottom: spacing.sm,
},
inputIcon: {
marginRight: spacing.sm,
},
input: {
flex: 1,
color: colors.text.primary,
fontSize: fontSizes.md,
},
codeRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
codeInput: {
flex: 1,
marginBottom: 0,
},
sendCodeButton: {
height: 48,
minWidth: 110,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.sm,
},
sendCodeButtonText: {
color: '#fff',
fontSize: fontSizes.sm,
fontWeight: '600',
},
primaryButton: {
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.xs,
},
primaryButtonText: {
color: '#fff',
fontSize: fontSizes.md,
fontWeight: '700',
},
buttonDisabled: {
opacity: 0.6,
},
});
function createAccountSecurityStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
sectionTitle: {
fontWeight: '600',
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
padding: spacing.md,
},
statusRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
},
statusBadge: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.sm,
},
statusVerified: {
backgroundColor: colors.success.light + '40',
},
statusUnverified: {
backgroundColor: colors.warning.light + '40',
},
statusVerifiedText: {
color: colors.success.dark,
},
statusUnverifiedText: {
color: colors.warning.dark,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
height: 48,
marginBottom: spacing.sm,
},
inputIcon: {
marginRight: spacing.sm,
},
input: {
flex: 1,
color: colors.text.primary,
fontSize: fontSizes.md,
},
codeRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
codeInput: {
flex: 1,
marginBottom: 0,
},
sendCodeButton: {
height: 48,
minWidth: 110,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.sm,
},
sendCodeButtonText: {
color: colors.text.inverse,
fontSize: fontSizes.sm,
fontWeight: '600',
},
primaryButton: {
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.xs,
},
primaryButtonText: {
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '700',
},
buttonDisabled: {
opacity: 0.6,
},
});
}
export default AccountSecurityScreen;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
FlatList,
@@ -12,12 +12,52 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useRouter } from 'expo-router';
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
import { authService } from '../../services';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { User } from '../../types';
import { useResponsive } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
function createBlockedUsersStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
loadingWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
listContent: {
padding: spacing.md,
gap: spacing.sm,
},
emptyContent: {
flexGrow: 1,
justifyContent: 'center',
},
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
...shadows.sm,
},
content: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
},
});
}
export const BlockedUsersScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createBlockedUsersStyles(colors), [colors]);
const router = useRouter();
const { isMobile } = useResponsive();
const insets = useSafeAreaInsets();
@@ -141,40 +181,4 @@ export const BlockedUsersScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
loadingWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
listContent: {
padding: spacing.md,
gap: spacing.sm,
},
emptyContent: {
flexGrow: 1,
justifyContent: 'center',
},
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
...shadows.sm,
},
content: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
},
});
export default BlockedUsersScreen;

View File

@@ -5,7 +5,7 @@
* 表单在宽屏下居中显示
*/
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import {
View,
ScrollView,
@@ -22,12 +22,274 @@ import { useNavigation } from '@react-navigation/native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import {
spacing,
fontSizes,
borderRadius,
shadows,
useAppColors,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { Avatar, Button, Text, ResponsiveContainer } from '../../components/common';
import { authService, uploadService } from '../../services';
import { useResponsive } from '../../hooks';
function createEditProfileStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
keyboardView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
},
// ===== 预览区域 - 与 UserProfileHeader 完全一致 =====
previewContainer: {
marginBottom: spacing.lg,
},
coverContainer: {
position: 'relative',
overflow: 'hidden',
borderRadius: borderRadius.lg,
},
coverTouchable: {
width: '100%',
height: '100%',
},
coverImage: {
width: '100%',
height: '100%',
},
gradient: {
width: '100%',
height: '100%',
},
coverUploadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
coverEditOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
},
coverEditText: {
color: colors.text.inverse,
fontSize: fontSizes.sm,
marginTop: spacing.xs,
fontWeight: '500',
},
waveDecoration: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 40,
},
wave: {
width: '100%',
height: '100%',
backgroundColor: colors.background.paper,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
},
// 用户信息卡片
profileCard: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.md,
marginTop: -50,
borderRadius: borderRadius.xl,
padding: spacing.lg,
...shadows.md,
},
avatarWrapper: {
alignItems: 'center',
marginTop: -60,
marginBottom: spacing.md,
},
avatarContainer: {
position: 'relative',
padding: 4,
backgroundColor: colors.background.paper,
borderRadius: 50,
},
avatarUploadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 60,
justifyContent: 'center',
alignItems: 'center',
},
editAvatarButton: {
position: 'absolute',
bottom: 0,
right: 0,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.primary.main,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
userInfo: {
alignItems: 'center',
marginBottom: spacing.md,
},
nickname: {
marginBottom: spacing.xs,
fontWeight: '700',
},
username: {
marginBottom: spacing.sm,
},
bio: {
textAlign: 'center',
marginTop: spacing.sm,
lineHeight: 20,
},
bioPlaceholder: {
textAlign: 'center',
marginTop: spacing.sm,
fontStyle: 'italic',
},
metaInfo: {
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
marginBottom: spacing.md,
gap: spacing.sm,
},
metaTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.md,
},
metaTagText: {
marginLeft: spacing.xs,
fontSize: fontSizes.xs,
},
editHint: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.md,
paddingVertical: spacing.sm,
gap: spacing.xs,
},
editHintText: {
fontSize: fontSizes.xs,
},
// ===== 表单区域 =====
formCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.lg,
shadowColor: colors.text.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
sectionTitle: {
marginBottom: spacing.lg,
},
formField: {
flexDirection: 'row',
alignItems: 'flex-start',
},
fieldIconContainer: {
width: 40,
height: 40,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
marginTop: spacing.xs,
},
fieldContent: {
flex: 1,
},
fieldLabel: {
marginBottom: spacing.xs,
fontWeight: '500',
},
fieldInput: {
fontSize: fontSizes.md,
color: colors.text.primary,
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
disabledInput: {
color: colors.text.disabled,
},
divider: {
height: 1,
backgroundColor: colors.divider,
marginVertical: spacing.md,
marginLeft: 56,
},
// 保存按钮
buttonContainer: {
marginTop: spacing.sm,
},
saveButton: {
backgroundColor: colors.primary.main,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
shadowColor: colors.primary.main,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonContent: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
buttonIcon: {
marginRight: spacing.sm,
},
saveButtonText: {
color: colors.text.inverse,
},
});
}
type EditProfileSheet = ReturnType<typeof createEditProfileStyles>;
// 表单输入项组件
interface FormFieldProps {
icon: string;
@@ -41,6 +303,8 @@ interface FormFieldProps {
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url';
editable?: boolean;
sheet: EditProfileSheet;
colors: AppColors;
}
const FormField: React.FC<FormFieldProps> = ({
@@ -55,19 +319,21 @@ const FormField: React.FC<FormFieldProps> = ({
autoCapitalize = 'sentences',
keyboardType = 'default',
editable = true,
sheet,
colors,
}) => {
return (
<View style={styles.formField}>
<View style={styles.fieldIconContainer}>
<View style={sheet.formField}>
<View style={sheet.fieldIconContainer}>
<MaterialCommunityIcons name={icon as any} size={22} color={colors.primary.main} />
</View>
<View style={styles.fieldContent}>
<Text variant="caption" style={styles.fieldLabel}>{label}</Text>
<View style={sheet.fieldContent}>
<Text variant="caption" style={sheet.fieldLabel}>{label}</Text>
<TextInput
style={[
styles.fieldInput,
multiline && styles.textArea,
!editable && styles.disabledInput
sheet.fieldInput,
multiline && sheet.textArea,
!editable && sheet.disabledInput
]}
value={value}
onChangeText={onChangeText}
@@ -86,6 +352,8 @@ const FormField: React.FC<FormFieldProps> = ({
};
export const EditProfileScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
const navigation = useNavigation();
const { currentUser, updateUser } = useAuthStore();
const { isWideScreen, isMobile, width } = useResponsive();
@@ -401,6 +669,8 @@ export const EditProfileScreen: React.FC = () => {
onChangeText={setNickname}
placeholder="请输入昵称"
maxLength={20}
sheet={styles}
colors={colors}
/>
<View style={styles.divider} />
@@ -414,6 +684,8 @@ export const EditProfileScreen: React.FC = () => {
maxLength={100}
multiline
numberOfLines={3}
sheet={styles}
colors={colors}
/>
<View style={styles.divider} />
@@ -425,6 +697,8 @@ export const EditProfileScreen: React.FC = () => {
onChangeText={setLocation}
placeholder="你所在的城市"
maxLength={30}
sheet={styles}
colors={colors}
/>
<View style={styles.divider} />
@@ -438,6 +712,8 @@ export const EditProfileScreen: React.FC = () => {
maxLength={100}
autoCapitalize="none"
keyboardType="url"
sheet={styles}
colors={colors}
/>
</View>
@@ -455,6 +731,8 @@ export const EditProfileScreen: React.FC = () => {
placeholder="请输入手机号"
maxLength={11}
keyboardType="phone-pad"
sheet={styles}
colors={colors}
/>
<View style={styles.divider} />
@@ -468,6 +746,8 @@ export const EditProfileScreen: React.FC = () => {
maxLength={100}
autoCapitalize="none"
keyboardType="email-address"
sheet={styles}
colors={colors}
/>
</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;

View File

@@ -5,7 +5,7 @@
* 在宽屏下使用网格布局
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
View,
FlatList,
@@ -17,7 +17,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { User } from '../../types';
import { useAuthStore, useUserStore } from '../../stores';
import { authService } from '../../services';
@@ -26,6 +26,8 @@ import * as hrefs from '../../navigation/hrefs';
import { useResponsive, useColumnCount } from '../../hooks';
const FollowListScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createFollowListStyles(colors), [colors]);
const router = useRouter();
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
const type = typeParam === 'followers' ? 'followers' : 'following';
@@ -356,136 +358,138 @@ const FollowListScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// 列表布局样式
listContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
paddingBottom: spacing.lg,
},
headerCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.divider + '4A',
...shadows.sm,
},
headerAccent: {
width: 28,
height: 3,
borderRadius: 999,
backgroundColor: colors.primary.main + 'A6',
marginBottom: spacing.sm,
},
headerMainRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
headerTitle: {
fontWeight: '700',
},
headerCountPill: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.full,
backgroundColor: colors.background.default,
},
headerSubtitle: {
marginTop: spacing.xs,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.sm,
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
marginBottom: 2,
},
bio: {
marginTop: 2,
},
followButton: {
minWidth: 82,
},
// 网格布局样式
gridContent: {
flexGrow: 1,
padding: spacing.lg,
},
emptyGridContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
gridRow: {
justifyContent: 'flex-start',
gap: spacing.md,
marginBottom: spacing.md,
},
userCard: {
flex: 1,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
padding: spacing.md,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userCardHeader: {
marginBottom: spacing.sm,
},
userCardContent: {
alignItems: 'center',
width: '100%',
},
userCardNickname: {
fontWeight: '600',
marginBottom: 2,
textAlign: 'center',
},
userCardBio: {
marginTop: spacing.xs,
textAlign: 'center',
},
userCardFooter: {
marginTop: spacing.md,
width: '100%',
},
userCardFollowButton: {
width: '100%',
},
footerLoading: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.xs,
},
footerLoadingText: {
marginLeft: spacing.xs,
},
});
function createFollowListStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// 列表布局样式
listContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
paddingBottom: spacing.lg,
},
headerCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.divider + '4A',
...shadows.sm,
},
headerAccent: {
width: 28,
height: 3,
borderRadius: 999,
backgroundColor: colors.primary.main + 'A6',
marginBottom: spacing.sm,
},
headerMainRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
headerTitle: {
fontWeight: '700',
},
headerCountPill: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.full,
backgroundColor: colors.background.default,
},
headerSubtitle: {
marginTop: spacing.xs,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.sm,
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
marginBottom: 2,
},
bio: {
marginTop: 2,
},
followButton: {
minWidth: 82,
},
// 网格布局样式
gridContent: {
flexGrow: 1,
padding: spacing.lg,
},
emptyGridContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
gridRow: {
justifyContent: 'flex-start',
gap: spacing.md,
marginBottom: spacing.md,
},
userCard: {
flex: 1,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
padding: spacing.md,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userCardHeader: {
marginBottom: spacing.sm,
},
userCardContent: {
alignItems: 'center',
width: '100%',
},
userCardNickname: {
fontWeight: '600',
marginBottom: 2,
textAlign: 'center',
},
userCardBio: {
marginTop: spacing.xs,
textAlign: 'center',
},
userCardFooter: {
marginTop: spacing.md,
width: '100%',
},
userCardFollowButton: {
width: '100%',
},
footerLoading: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.xs,
},
footerLoadingText: {
marginLeft: spacing.xs,
},
});
}
export default FollowListScreen;

View File

@@ -4,7 +4,7 @@
* 在宽屏下居中显示
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
View,
StyleSheet,
@@ -13,7 +13,7 @@ import {
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Text, ResponsiveContainer } from '../../components/common';
import {
loadNotificationPreferences,
@@ -33,6 +33,8 @@ interface NotificationSettingItem {
}
export const NotificationSettingsScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]);
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
const [pushEnabled, setPushEnabled] = useState(true);
const [soundEnabled, setSoundEnabled] = useState(true);
@@ -183,72 +185,74 @@ export const NotificationSettingsScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
sectionTitle: {
fontWeight: '600',
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingContent: {
flex: 1,
},
subtitle: {
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: colors.divider,
marginLeft: 36 + spacing.md + spacing.md,
},
tipContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
marginHorizontal: spacing.lg,
padding: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
gap: spacing.sm,
},
tipText: {
flex: 1,
lineHeight: 20,
},
});
function createNotificationSettingsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
sectionTitle: {
fontWeight: '600',
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingContent: {
flex: 1,
},
subtitle: {
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: colors.divider,
marginLeft: 36 + spacing.md + spacing.md,
},
tipContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
marginHorizontal: spacing.lg,
padding: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
gap: spacing.sm,
},
tipText: {
flex: 1,
lineHeight: 20,
},
});
}
export default NotificationSettingsScreen;

View File

@@ -4,7 +4,7 @@
* 在宽屏下居中显示,最大宽度限制
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
View,
StyleSheet,
@@ -16,7 +16,15 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import {
useAppColors,
spacing,
fontSizes,
borderRadius,
useThemePreference,
useSetThemePreference,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { Text, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
@@ -34,11 +42,9 @@ interface SettingsItem {
subtitle?: string;
}
// 获取应用版本号
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
// 设置分组配置
const SETTINGS_GROUPS = [
const SETTINGS_GROUPS_BASE = [
{
title: '账号与安全',
icon: 'shield-check-outline',
@@ -66,19 +72,161 @@ const SETTINGS_GROUPS = [
},
];
function createSettingsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
groupContainer: {
marginBottom: spacing.lg,
},
groupHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
groupTitle: {
fontWeight: '600',
fontSize: fontSizes.sm,
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
settingItemFirst: {
borderTopLeftRadius: borderRadius.lg,
borderTopRightRadius: borderRadius.lg,
},
settingItemLast: {
borderBottomLeftRadius: borderRadius.lg,
borderBottomRightRadius: borderRadius.lg,
borderBottomWidth: 0,
},
settingItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
dangerIconContainer: {
backgroundColor: colors.error.light + '20',
},
settingContent: {
flex: 1,
},
subtitle: {
marginTop: 2,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
marginTop: spacing.sm,
marginBottom: spacing.lg,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
gap: spacing.sm,
},
logoutText: {
fontWeight: '500',
},
footer: {
alignItems: 'center',
marginTop: spacing.xl,
paddingBottom: spacing.xl,
},
copyright: {
marginTop: spacing.xs,
},
});
}
export const SettingsScreen: React.FC = () => {
const router = useRouter();
const { logout } = useAuthStore();
const { isWideScreen } = useResponsive();
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
// 底部间距,避免被 TabBar 遮挡
const colors = useAppColors();
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
const themePreference = useThemePreference();
const setThemePreference = useSetThemePreference();
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
// 处理设置项点击
const settingsGroups = useMemo(() => {
const themeSubtitle =
themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色';
return [
{
title: '显示与外观',
icon: 'palette-outline',
items: [
{
key: 'theme',
title: '主题模式',
icon: 'theme-light-dark',
showArrow: true,
subtitle: themeSubtitle,
},
],
},
...SETTINGS_GROUPS_BASE,
];
}, [themePreference]);
const handleItemPress = (key: string) => {
switch (key) {
case 'theme':
Alert.alert('主题模式', '选择应用界面明暗', [
{ text: '取消', style: 'cancel' },
{
text: '跟随系统',
onPress: () => {
void setThemePreference('system');
},
},
{
text: '浅色',
onPress: () => {
void setThemePreference('light');
},
},
{
text: '深色',
onPress: () => {
void setThemePreference('dark');
},
},
]);
break;
case 'edit_profile':
router.push(hrefs.hrefProfileEdit());
break;
@@ -103,7 +251,7 @@ export const SettingsScreen: React.FC = () => {
onPress: async () => {
await logout();
router.replace(hrefs.hrefAuthLogin());
}
},
},
]
);
@@ -113,7 +261,6 @@ export const SettingsScreen: React.FC = () => {
}
};
// 渲染单个设置项
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
<TouchableOpacity
key={item.key}
@@ -122,14 +269,11 @@ export const SettingsScreen: React.FC = () => {
index === 0 && styles.settingItemFirst,
index === total - 1 && styles.settingItemLast,
]}
onPress={() => item.onPress ? item.onPress() : handleItemPress(item.key)}
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
activeOpacity={0.7}
>
<View style={styles.settingItemLeft}>
<View style={[
styles.iconContainer,
item.danger && styles.dangerIconContainer
]}>
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
<MaterialCommunityIcons
name={item.icon as any}
size={20}
@@ -137,10 +281,7 @@ export const SettingsScreen: React.FC = () => {
/>
</View>
<View style={styles.settingContent}>
<Text
variant="body"
color={item.danger ? colors.error.main : colors.text.primary}
>
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary}>
{item.title}
</Text>
{item.subtitle && (
@@ -156,8 +297,7 @@ export const SettingsScreen: React.FC = () => {
</TouchableOpacity>
);
// 渲染分组
const renderGroup = (group: typeof SETTINGS_GROUPS[0], groupIndex: number) => (
const renderGroup = (group: (typeof settingsGroups)[0]) => (
<View key={group.title} style={styles.groupContainer}>
<View style={styles.groupHeader}>
<MaterialCommunityIcons name={group.icon as any} size={16} color={colors.primary.main} />
@@ -166,14 +306,11 @@ export const SettingsScreen: React.FC = () => {
</Text>
</View>
<View style={styles.card}>
{group.items.map((item, index) =>
renderSettingItem(item, index, group.items.length)
)}
{group.items.map((item, index) => renderSettingItem(item, index, group.items.length))}
</View>
</View>
);
// 退出登录按钮
const renderLogoutButton = () => (
<TouchableOpacity
style={styles.logoutButton}
@@ -187,13 +324,11 @@ export const SettingsScreen: React.FC = () => {
</TouchableOpacity>
);
// 渲染内容
const renderContent = () => (
<>
{SETTINGS_GROUPS.map((group, index) => renderGroup(group, index))}
{settingsGroups.map((group) => renderGroup(group))}
{renderLogoutButton()}
{/* 底部版权信息 */}
<View style={styles.footer}>
<Text variant="caption" color={colors.text.hint}>
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;

View File

@@ -12,12 +12,12 @@ import {
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../../theme';
import { useAppColors } from '../../theme';
import { Post } from '../../types';
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, sharedStyles } from './useUserProfile';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
interface UserProfileScreenProps {
mode: ProfileMode;
@@ -25,6 +25,8 @@ interface UserProfileScreenProps {
}
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => {
const colors = useAppColors();
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
const { isDesktop, isTablet } = useResponsive();
const {

View File

@@ -16,5 +16,5 @@ export { BlockedUsersScreen } from './BlockedUsersScreen';
export { AccountSecurityScreen } from './AccountSecurityScreen';
// 导出 Hook 供需要自定义的场景使用
export { useUserProfile, sharedStyles, TABS, TAB_ICONS } from './useUserProfile';
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
export type { ProfileMode, UseUserProfileOptions, UseUserProfileReturn } from './useUserProfile';

View File

@@ -4,9 +4,10 @@
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import { StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors, spacing } from '../../theme';
import { spacing, type AppColors } from '../../theme';
import { Post, User } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
@@ -443,62 +444,61 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
};
};
// 共享的样式
import { StyleSheet } from 'react-native';
export const sharedStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
flexGrow: 1,
},
desktopContainer: {
flex: 1,
flexDirection: 'row',
gap: spacing.lg,
padding: spacing.lg,
},
desktopSidebar: {
width: 380,
flexShrink: 0,
},
desktopContent: {
flex: 1,
minWidth: 0,
},
desktopScrollContent: {
flexGrow: 1,
},
tabBarContainer: {
marginTop: spacing.xs,
marginBottom: 2,
},
contentContainer: {
flex: 1,
minHeight: 350,
paddingTop: spacing.xs,
},
postsContainer: {
paddingHorizontal: spacing.md,
paddingTop: spacing.sm,
},
postWrapper: {
marginBottom: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
lastPost: {
marginBottom: spacing['2xl'],
},
});
export function createSharedProfileStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
flexGrow: 1,
},
desktopContainer: {
flex: 1,
flexDirection: 'row',
gap: spacing.lg,
padding: spacing.lg,
},
desktopSidebar: {
width: 380,
flexShrink: 0,
},
desktopContent: {
flex: 1,
minWidth: 0,
},
desktopScrollContent: {
flexGrow: 1,
},
tabBarContainer: {
marginTop: spacing.xs,
marginBottom: 2,
},
contentContainer: {
flex: 1,
minHeight: 350,
paddingTop: spacing.xs,
},
postsContainer: {
paddingHorizontal: spacing.md,
paddingTop: spacing.sm,
},
postWrapper: {
marginBottom: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 16,
overflow: 'hidden',
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
lastPost: {
marginBottom: spacing['2xl'],
},
});
}
// Tab 配置
export const TABS = ['帖子', '收藏'];

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Pressable } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, fontSizes, shadows } from '../../theme';
import { spacing, borderRadius, fontSizes, shadows, useAppColors, type AppColors } from '../../theme';
import { routePayloadCache } from '../../stores/routePayloadCache';
import { scheduleService } from '../../services/scheduleService';
@@ -32,15 +32,146 @@ const formatWeekRanges = (weeks: number[]) => {
return ranges.join(', ');
};
function createCourseDetailStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
},
mask: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.32)',
justifyContent: 'center',
paddingHorizontal: spacing.lg,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
paddingVertical: spacing.lg,
paddingHorizontal: spacing.lg,
maxHeight: '82%',
...shadows.lg,
},
contentScroll: {
maxHeight: '100%',
},
contentScrollContainer: {
paddingBottom: spacing.xs,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.lg,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}AA`,
paddingBottom: spacing.md,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
},
iconBadge: {
width: 30,
height: 30,
borderRadius: borderRadius.full,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: `${colors.primary.main}12`,
marginRight: spacing.sm,
},
title: {
fontSize: fontSizes.xl,
fontWeight: '700',
color: colors.text.primary,
},
closeButton: {
width: 28,
height: 28,
alignItems: 'center',
justifyContent: 'center',
borderRadius: borderRadius.full,
backgroundColor: colors.background.default,
},
row: {
marginBottom: spacing.lg,
flexDirection: 'row',
alignItems: 'flex-start',
},
rowLast: {
marginBottom: 0,
},
label: {
width: 76,
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.text.secondary,
lineHeight: 22,
marginTop: 1,
},
value: {
flex: 1,
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
lineHeight: 24,
},
timeList: {
flex: 1,
gap: spacing.sm,
},
timeItemCard: {
borderWidth: 1,
borderColor: `${colors.primary.main}20`,
borderRadius: borderRadius.md,
backgroundColor: `${colors.primary.main}08`,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
},
timeItemMain: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.text.primary,
lineHeight: 20,
},
timeItemSub: {
marginTop: spacing.xs,
fontSize: fontSizes.sm,
color: colors.text.secondary,
lineHeight: 18,
},
actionRow: {
marginTop: spacing.lg,
alignItems: 'flex-end',
},
deleteButton: {
height: 38,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.full,
backgroundColor: `${colors.error.main}10`,
borderWidth: 1,
borderColor: `${colors.error.main}25`,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
deleteButtonText: {
fontSize: fontSizes.sm,
fontWeight: '700',
color: colors.error.main,
},
});
}
const CourseDetailScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createCourseDetailStyles(colors), [colors]);
const { courseId } = useLocalSearchParams<{ courseId?: string }>();
const cached = courseId ? routePayloadCache.getCourseDetail(courseId) : undefined;
const course = cached?.course;
const relatedCourses = cached?.relatedCourses ?? [];
const timeLines =
relatedCourses.length > 0 ? relatedCourses : course ? [course] : [];
const timeLines = relatedCourses.length > 0 ? relatedCourses : course ? [course] : [];
const handleDeleteCourse = () => {
if (!course) return;
@@ -98,7 +229,9 @@ const CourseDetailScreen: React.FC = () => {
>
<View style={styles.row}>
<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 style={[styles.row, styles.rowLast]}>
@@ -110,9 +243,7 @@ const CourseDetailScreen: React.FC = () => {
{formatWeekRanges(item.weeks)} · {WEEKDAY_NAMES[item.dayOfWeek]}
{getMergedSectionIndex(item.startSection)}
</Text>
<Text style={styles.timeItemSub}>
{item.teacher || '未填写'}
</Text>
<Text style={styles.timeItemSub}>{item.teacher || '未填写'}</Text>
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
</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;

View File

@@ -23,7 +23,14 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme';
import {
fontSizes,
spacing,
borderRadius,
shadows,
useAppColors,
type AppColors,
} from '../../theme';
import { useResponsive } from '../../hooks/useResponsive';
import {
Course,
@@ -148,6 +155,8 @@ const getWeekDates = (weekOffset: number = 0) => {
const INITIAL_WEEK = getCurrentWeekNumber();
export const ScheduleScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createScheduleStyles(colors), [colors]);
const router = useRouter();
// 使用响应式 hook 检测屏幕尺寸
const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive();
@@ -1063,7 +1072,8 @@ export const ScheduleScreen: React.FC = () => {
);
};
const styles = StyleSheet.create({
function createScheduleStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
@@ -1483,6 +1493,7 @@ const styles = StyleSheet.create({
color: colors.text.secondary,
fontWeight: '500',
},
});
});
}
export default ScheduleScreen;