refactor(database): migrate to new modular database layer and unify data access
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m44s
Frontend CI / ota-android (push) Successful in 12m51s
Frontend CI / build-android-apk (push) Successful in 1h1m26s

- Remove legacy database.ts, LocalDataSource.ts, and MessageRepository.ts
- Create new src/database/ module with messageRepository, userCacheRepository, conversationRepository, and groupCacheRepository
- Update all consumers to import from @/database instead of services/database
- Add web platform blur handling for modal components to fix focus issues
- Flatten SystemMessageItem and NotificationsScreen styles for consistent design
- Add draggable slider in ChatSettingsScreen and dynamic font size support
- Introduce 9 new chat color themes
- Add profile screens for about, terms, and privacy policy with navigation routes
- Add policy links to login and registration screens
- Fix post share URL format from /posts/ to /post/
This commit is contained in:
lafay
2026-04-04 08:01:45 +08:00
parent 189b977fac
commit 82c2970a85
76 changed files with 3382 additions and 2000 deletions

View File

@@ -155,6 +155,13 @@ const QRCodeScannerWeb: React.FC<QRCodeScannerProps> = ({ visible, onClose }) =>
const themeColors = useAppColors();
const styles = useMemo(() => createQrScannerStyles(themeColors), [themeColors]);
useEffect(() => {
if (visible && Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
}, [visible]);
return (
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
<View style={styles.container}>

View File

@@ -4,7 +4,7 @@
* 提供友好的用户界面和流畅的交互体验
*/
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
Modal,
View,
@@ -62,6 +62,13 @@ const ReportDialog: React.FC<ReportDialogProps> = ({
const targetConfig = TARGET_CONFIG[targetType];
useEffect(() => {
if (visible && Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
}, [visible]);
// 判断补充说明是否必填(选择"其他原因"时必填)
const isDescriptionRequired = selectedReason === 'other';
const descriptionPlaceholder = isDescriptionRequired

View File

@@ -1,7 +1,7 @@
/**
* SystemMessageItem 系统消息项组件 - 美化版
* SystemMessageItem 系统消息项组件 - 扁平化风格
* 根据系统消息类型显示不同图标和内容
* 采用卡片式设计符合胡萝卜BBS整体风格
* 与登录、注册、设置页面风格保持一致
*/
import React, { useMemo } from 'react';
@@ -9,7 +9,7 @@ import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { SystemMessageResponse, SystemMessageType } from '../../types/dto';
import Text from '../common/Text';
import Avatar from '../common/Avatar';
@@ -124,17 +124,13 @@ function createSystemMessageStyles(colors: AppColors) {
container: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
marginHorizontal: spacing.md,
marginBottom: spacing.sm,
...shadows.sm,
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: colors.divider || '#E5E5EA',
},
unreadContainer: {
backgroundColor: colors.primary.light + '08',
borderLeftWidth: 3,
borderLeftColor: colors.primary.main,
backgroundColor: '#FFF8F6',
},
unreadIndicator: {
position: 'absolute',
@@ -143,19 +139,19 @@ function createSystemMessageStyles(colors: AppColors) {
marginTop: -4,
width: 8,
height: 8,
borderRadius: borderRadius.full,
backgroundColor: colors.primary.main,
borderRadius: 4,
backgroundColor: '#FF6B35',
},
iconContainer: {
marginRight: spacing.md,
marginRight: 14,
},
avatarWrapper: {
position: 'relative',
},
iconWrapper: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
width: 44,
height: 44,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
@@ -163,13 +159,13 @@ function createSystemMessageStyles(colors: AppColors) {
position: 'absolute',
bottom: -2,
right: -2,
width: 20,
height: 20,
borderRadius: borderRadius.full,
width: 18,
height: 18,
borderRadius: 9,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
borderColor: '#fff',
},
content: {
flex: 1,
@@ -180,17 +176,17 @@ function createSystemMessageStyles(colors: AppColors) {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.xs,
marginBottom: 4,
},
titleLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
marginRight: spacing.sm,
marginRight: 8,
},
title: {
fontWeight: '500',
fontSize: fontSizes.md,
fontSize: 15,
color: colors.text.primary,
},
unreadTitle: {
@@ -200,10 +196,10 @@ function createSystemMessageStyles(colors: AppColors) {
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.xs,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: borderRadius.sm,
marginLeft: spacing.xs,
borderRadius: 4,
marginLeft: 6,
},
statusText: {
fontSize: 10,
@@ -211,55 +207,59 @@ function createSystemMessageStyles(colors: AppColors) {
marginLeft: 2,
},
time: {
fontSize: fontSizes.sm,
fontSize: 13,
flexShrink: 0,
color: colors.text.hint,
},
messageContent: {
lineHeight: 20,
fontSize: fontSizes.sm,
fontSize: 14,
color: colors.text.secondary,
},
extraInfo: {
flexDirection: 'row',
alignItems: 'center',
marginTop: spacing.xs,
backgroundColor: colors.primary.light + '12',
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.md,
marginTop: 8,
backgroundColor: '#F5F5F7',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 8,
alignSelf: 'flex-start',
},
extraText: {
marginLeft: spacing.xs,
marginLeft: 6,
flex: 1,
fontWeight: '500',
fontSize: 13,
},
actionsRow: {
flexDirection: 'row',
marginTop: spacing.sm,
gap: spacing.sm,
marginTop: 12,
gap: 10,
},
actionBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.xs,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.md,
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 10,
borderWidth: 1,
gap: spacing.xs,
gap: 4,
},
rejectBtn: {
borderColor: `${colors.error.main}55`,
backgroundColor: `${colors.error.main}18`,
borderColor: colors.error.main + '40',
backgroundColor: colors.error.light + '20',
},
approveBtn: {
borderColor: `${colors.success.main}55`,
backgroundColor: `${colors.success.main}18`,
borderColor: colors.success.main + '40',
backgroundColor: colors.success.light + '20',
},
actionBtnText: {
fontWeight: '500',
fontSize: 13,
},
arrowContainer: {
marginLeft: spacing.sm,
marginLeft: 8,
justifyContent: 'center',
alignItems: 'center',
height: 20,
@@ -318,6 +318,13 @@ const getSystemMessageTitle = (message: SystemMessageResponse): string => {
}
};
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
message,
onPress,

View File

@@ -9,6 +9,7 @@ import {
Modal,
StatusBar,
Dimensions,
Platform,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore';
@@ -25,6 +26,10 @@ const IncomingCallModal: React.FC = () => {
useEffect(() => {
if (incomingCall) {
if (Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {

View File

@@ -15,6 +15,7 @@ import {
Modal,
Pressable,
Dimensions,
Platform,
} from 'react-native';
import {
useResponsive,
@@ -159,6 +160,10 @@ export function AdaptiveLayout({
// 抽屉动画
useEffect(() => {
if (isDrawerOpen) {
if (Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 1,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Modal, Pressable, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Modal, Pressable, StyleSheet, Text, TouchableOpacity, View, Platform } from 'react-native';
import type { AlertButton } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
@@ -93,6 +93,12 @@ function createDialogHostStyles(colors: AppColors) {
});
}
const blurActiveElementOnWeb = () => {
if (Platform.OS !== 'web') return;
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
};
const AppDialogHost: React.FC = () => {
const [dialog, setDialog] = useState<DialogPayload | null>(null);
const colors = useAppColors();
@@ -105,6 +111,12 @@ const AppDialogHost: React.FC = () => {
return unbind;
}, []);
useEffect(() => {
if (dialog) {
blurActiveElementOnWeb();
}
}, [dialog]);
const actions = useMemo<AlertButton[]>(() => {
if (!dialog?.actions?.length) return [{ text: '确定' }];
return dialog.actions;

View File

@@ -14,6 +14,7 @@ import {
Text,
StatusBar,
Alert,
Platform,
} from 'react-native';
import { Image as ExpoImage } from 'expo-image';
import {
@@ -126,6 +127,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 打开/关闭时重置状态
useEffect(() => {
if (visible) {
if (Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
setCurrentIndex(initialIndex);
setShowControls(true);
setError(false);

View File

@@ -3,7 +3,7 @@
* 全屏模态播放视频,基于 expo-video
*/
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import {
Modal,
View,
@@ -12,6 +12,7 @@ import {
TouchableOpacity,
Text,
StatusBar,
Platform,
} from 'react-native';
import { VideoView, useVideoPlayer } from 'expo-video';
import { MaterialCommunityIcons } from '@expo/vector-icons';
@@ -79,6 +80,13 @@ export const VideoPlayerModal: React.FC<VideoPlayerModalProps> = ({
url,
onClose,
}) => {
useEffect(() => {
if (visible && Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
}
}, [visible]);
return (
<Modal
visible={visible}