refactor(database): migrate to new modular database layer and unify data access
- 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:
@@ -237,6 +237,16 @@ export const LoginScreen: React.FC = () => {
|
||||
<Text style={styles.registerLink}>立即注册</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 协议提示 */}
|
||||
<View style={styles.policyFooter}>
|
||||
<Text style={styles.policyText}>
|
||||
登录即表示您同意
|
||||
<Text style={styles.policyLink} onPress={() => router.push(hrefs.hrefProfileTerms())}>《用户协议》</Text>
|
||||
和
|
||||
<Text style={styles.policyLink} onPress={() => router.push(hrefs.hrefProfilePrivacy())}>《隐私政策》</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
@@ -376,6 +386,18 @@ function createLoginStyles(colors: AppColors) {
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
},
|
||||
policyFooter: {
|
||||
alignItems: 'center',
|
||||
marginTop: 24,
|
||||
},
|
||||
policyText: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
policyLink: {
|
||||
color: THEME_COLORS.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { useAppColors, type AppColors } from '../../theme';
|
||||
import { RegisterStepProps } from './types';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
@@ -32,6 +34,7 @@ export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
||||
}) => {
|
||||
const colors = propColors || useAppColors();
|
||||
const styles = createStyles(colors);
|
||||
const router = useRouter();
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
@@ -185,28 +188,40 @@ export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
||||
})}
|
||||
|
||||
{/* 服务条款 */}
|
||||
<TouchableOpacity
|
||||
style={styles.termsContainer}
|
||||
onPress={() => {
|
||||
setAgreedToTerms(!agreedToTerms);
|
||||
if (errors.terms) {
|
||||
setErrors((prev) => ({ ...prev, terms: '' }));
|
||||
}
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||
size={22}
|
||||
color={agreedToTerms ? THEME_COLORS.primary : colors.text?.hint || '#999'}
|
||||
/>
|
||||
<View style={styles.termsContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setAgreedToTerms(!agreedToTerms);
|
||||
if (errors.terms) {
|
||||
setErrors((prev) => ({ ...prev, terms: '' }));
|
||||
}
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={styles.checkboxWrapper}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||
size={22}
|
||||
color={agreedToTerms ? THEME_COLORS.primary : colors.text?.hint || '#999'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.termsText}>
|
||||
我已阅读并同意
|
||||
<Text style={styles.termsLink}>《用户协议》</Text>
|
||||
<Text
|
||||
style={styles.termsLink}
|
||||
onPress={() => router.push(hrefs.hrefProfileTerms())}
|
||||
>
|
||||
《用户协议》
|
||||
</Text>
|
||||
和
|
||||
<Text style={styles.termsLink}>《隐私政策》</Text>
|
||||
<Text
|
||||
style={styles.termsLink}
|
||||
onPress={() => router.push(hrefs.hrefProfilePrivacy())}
|
||||
>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{errors.terms ? (
|
||||
<Text style={[styles.errorText, { marginTop: -12, marginBottom: 12 }]}>
|
||||
{errors.terms}
|
||||
@@ -295,10 +310,13 @@ function createStyles(colors: AppColors) {
|
||||
},
|
||||
termsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 20,
|
||||
marginTop: 4,
|
||||
},
|
||||
checkboxWrapper: {
|
||||
paddingTop: 2,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 14,
|
||||
color: colors.text?.secondary || '#666',
|
||||
|
||||
@@ -203,6 +203,13 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
// 发帖弹窗状态
|
||||
const [showCreatePost, setShowCreatePost] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCreatePost && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [showCreatePost]);
|
||||
|
||||
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
||||
const postIdsRef = useRef<Set<string>>(new Set());
|
||||
@@ -599,7 +606,7 @@ export const HomeScreen: React.FC = () => {
|
||||
} catch (shareError) {
|
||||
console.error('上报分享次数失败:', shareError);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||||
const postUrl = `https://browser.littlelan.cn/post/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
};
|
||||
|
||||
@@ -536,7 +536,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||||
const postUrl = `https://browser.littlelan.cn/post/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
}, [post?.id]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 支持响应式布局
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TextInput,
|
||||
FlatList,
|
||||
Image,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
@@ -45,6 +46,13 @@ const CreateGroupScreen: React.FC = () => {
|
||||
// 邀请成员模态框状态
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inviteModalVisible && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [inviteModalVisible]);
|
||||
|
||||
// 移除已选成员
|
||||
const removeSelectedMember = (userId: string) => {
|
||||
const newSelectedIds = new Set(selectedMemberIds);
|
||||
|
||||
@@ -118,6 +118,13 @@ const GroupInfoScreen: React.FC = () => {
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const [inviting, setInviting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ((editModalVisible || announcementModalVisible || transferModalVisible || joinTypeModalVisible || inviteModalVisible) && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [editModalVisible, announcementModalVisible, transferModalVisible, joinTypeModalVisible, inviteModalVisible]);
|
||||
|
||||
// 计算当前用户的角色
|
||||
const isOwner = currentMember?.role === 'owner';
|
||||
const isAdmin = currentMember?.role === 'admin' || isOwner;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Modal,
|
||||
TextInput,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
@@ -131,6 +132,13 @@ const GroupMembersScreen: React.FC = () => {
|
||||
const [nicknameModalVisible, setNicknameModalVisible] = useState(false);
|
||||
const [newNickname, setNewNickname] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if ((actionModalVisible || nicknameModalVisible) && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [actionModalVisible, nicknameModalVisible]);
|
||||
|
||||
// 计算当前用户的角色
|
||||
const isOwner = currentMember?.role === 'owner';
|
||||
const isAdmin = currentMember?.role === 'admin' || isOwner;
|
||||
|
||||
@@ -136,6 +136,13 @@ export const MessageListScreen: React.FC = () => {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [activeSearchTab, setActiveSearchTab] = useState<'chat' | 'user'>('chat');
|
||||
const [actionMenuVisible, setActionMenuVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionMenuVisible && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [actionMenuVisible]);
|
||||
const [scannerVisible, setScannerVisible] = useState(false);
|
||||
|
||||
// 系统通知显示状态 - 用于在移动端显示通知页面
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/**
|
||||
* 通知页 NotificationsScreen
|
||||
* 通知页 NotificationsScreen(扁平化风格)
|
||||
* 胡萝卜BBS - 系统消息列表
|
||||
* 【游标分页】使用 messageService.getSystemMessagesCursor
|
||||
* 支持响应式布局
|
||||
*
|
||||
* 设计风格:
|
||||
* - 纯白背景,扁平化设计
|
||||
* - 简洁的标题区域
|
||||
* - 分段式筛选标签
|
||||
* - 与登录、注册、设置页面风格一致
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
@@ -15,17 +21,19 @@ import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
BackHandler,
|
||||
Animated,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
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 { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||
import { SystemMessageResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { commentService } from '../../services/commentService';
|
||||
import { SystemMessageItem } from '../../components/business';
|
||||
import { Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
|
||||
import { Text, ResponsiveContainer } from '../../components/common';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useMessageManagerSystemUnreadCount } from '../../stores';
|
||||
@@ -41,6 +49,13 @@ const MESSAGE_TYPES = [
|
||||
{ key: 'announcement', title: '公告' },
|
||||
];
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
|
||||
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
|
||||
|
||||
@@ -51,6 +66,10 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
||||
const router = useRouter();
|
||||
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const slideAnim = useRef(new Animated.Value(20)).current;
|
||||
|
||||
// 修复竞态条件:使用本地状态管理窗口尺寸,避免 hydration 问题
|
||||
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
@@ -177,6 +196,22 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
}
|
||||
}, [isFocused]); // 只依赖 isFocused,函数使用 ref 稳定引用
|
||||
|
||||
// 启动入场动画
|
||||
useEffect(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 0,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
// 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式)
|
||||
useEffect(() => {
|
||||
if (!isFocused && onBack) {
|
||||
@@ -343,7 +378,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染头部(包含返回按钮)
|
||||
// 渲染头部(包含返回按钮)- 扁平化风格
|
||||
const renderHeader = () => {
|
||||
// 大屏模式(>= lg 断点)隐藏返回按钮
|
||||
const shouldShowBackButton = onBack && !isWideScreen;
|
||||
@@ -351,7 +386,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
return (
|
||||
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
|
||||
{shouldShowBackButton ? (
|
||||
<AppBackButton style={styles.backButton} onPress={onBack} />
|
||||
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.8}>
|
||||
<View style={styles.backIconButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={22} color={colors.text.primary} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.backButtonPlaceholder} />
|
||||
)}
|
||||
@@ -370,14 +409,14 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染空状态
|
||||
// 渲染空状态 - 扁平化风格
|
||||
const renderEmpty = () => (
|
||||
<View style={[styles.emptyContainer, isWideScreen && styles.emptyContainerWeb]}>
|
||||
<EmptyState
|
||||
title="暂无通知"
|
||||
description="关注一些用户来获取通知吧"
|
||||
icon="bell-outline"
|
||||
/>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<MaterialCommunityIcons name="bell-outline" size={64} color={colors.text.hint} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>暂无通知</Text>
|
||||
<Text style={styles.emptyDescription}>关注一些用户来获取通知吧</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -385,8 +424,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
if (!isHydrated) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -394,164 +434,175 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
{isWideScreen ? (
|
||||
<ResponsiveContainer maxWidth={containerMaxWidth}>
|
||||
{/* 头部 - 大屏模式下隐藏返回按钮 */}
|
||||
{renderHeader()}
|
||||
{/* 分类筛选 */}
|
||||
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
|
||||
{MESSAGE_TYPES.map(type => {
|
||||
const count = type.key === 'all'
|
||||
? displayMessages.length
|
||||
: displayMessages.filter((m) => {
|
||||
if (type.key === 'like_post') {
|
||||
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
if (type.key === 'group') {
|
||||
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
isActive && styles.filterTagActive,
|
||||
isWideScreen && styles.filterTagWide,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
transform: [{ translateY: slideAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{isWideScreen ? (
|
||||
<ResponsiveContainer maxWidth={containerMaxWidth}>
|
||||
{/* 头部 - 大屏模式下隐藏返回按钮 */}
|
||||
{renderHeader()}
|
||||
{/* 分类筛选 */}
|
||||
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
|
||||
{MESSAGE_TYPES.map(type => {
|
||||
const count = type.key === 'all'
|
||||
? displayMessages.length
|
||||
: displayMessages.filter((m) => {
|
||||
if (type.key === 'like_post') {
|
||||
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
if (type.key === 'group') {
|
||||
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
isActive && styles.filterTagActive,
|
||||
isWideScreen && styles.filterTagWide,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 消息列表 */}
|
||||
{isLoading && messages.length === 0 ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredMessages}
|
||||
renderItem={renderMessage}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
contentContainerStyle={[styles.listContent, isWideScreen && styles.listContentWide]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={renderEmpty}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.3}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<>
|
||||
{/* 头部 - 小屏模式下显示返回按钮 */}
|
||||
{renderHeader()}
|
||||
{/* 分类筛选 */}
|
||||
<View style={styles.filterContainer}>
|
||||
{MESSAGE_TYPES.map(type => {
|
||||
const count = type.key === 'all'
|
||||
? displayMessages.length
|
||||
: displayMessages.filter((m) => {
|
||||
if (type.key === 'like_post') {
|
||||
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
if (type.key === 'group') {
|
||||
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
isActive && styles.filterTagActive,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
|
||||
{/* 消息列表 */}
|
||||
{isLoading && messages.length === 0 ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredMessages}
|
||||
renderItem={renderMessage}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
contentContainerStyle={[styles.listContent, isWideScreen && styles.listContentWide]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={renderEmpty}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.3}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[THEME_COLORS.primary]}
|
||||
tintColor={THEME_COLORS.primary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<>
|
||||
{/* 头部 - 小屏模式下显示返回按钮 */}
|
||||
{renderHeader()}
|
||||
{/* 分类筛选 */}
|
||||
<View style={styles.filterContainer}>
|
||||
{MESSAGE_TYPES.map(type => {
|
||||
const count = type.key === 'all'
|
||||
? displayMessages.length
|
||||
: displayMessages.filter((m) => {
|
||||
if (type.key === 'like_post') {
|
||||
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
if (type.key === 'group') {
|
||||
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
isActive && styles.filterTagActive,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 消息列表 */}
|
||||
{isLoading && messages.length === 0 ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredMessages}
|
||||
renderItem={renderMessage}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={renderEmpty}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.3}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 消息列表 */}
|
||||
{isLoading && messages.length === 0 ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredMessages}
|
||||
renderItem={renderMessage}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={renderEmpty}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.3}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[THEME_COLORS.primary]}
|
||||
tintColor={THEME_COLORS.primary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@@ -560,21 +611,23 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
// Header 样式 - 美化版
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
// Header 样式 - 扁平化风格
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
...shadows.sm,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -591,7 +644,7 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
fontSize: 20,
|
||||
},
|
||||
unreadBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
@@ -601,7 +654,7 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.text.inverse,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
@@ -609,50 +662,59 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backIconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F5F5F7',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
// 分类筛选 - 美化版(使用分段式设计)
|
||||
// 分类筛选 - 扁平化风格(分段式设计)
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider || '#E5E5EA',
|
||||
},
|
||||
filterContainerWideWeb: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 16,
|
||||
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,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
marginRight: 8,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
filterTagWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
filterTagActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
borderColor: THEME_COLORS.primary,
|
||||
},
|
||||
filterTagTextActive: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
filterCount: {
|
||||
marginLeft: spacing.xs,
|
||||
backgroundColor: colors.primary.light + '40',
|
||||
marginLeft: 6,
|
||||
backgroundColor: THEME_COLORS.primaryLight + '40',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 8,
|
||||
@@ -664,37 +726,59 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
paddingVertical: 80,
|
||||
},
|
||||
loadingMore: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
loadingMoreText: {
|
||||
marginLeft: spacing.sm,
|
||||
marginLeft: 8,
|
||||
},
|
||||
// 空状态 - 扁平化风格
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing['3xl'],
|
||||
paddingTop: 80,
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
emptyContainerWeb: {
|
||||
minHeight: 500,
|
||||
justifyContent: 'center',
|
||||
paddingTop: 100,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: '#F5F5F7',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyDescription: {
|
||||
fontSize: 15,
|
||||
color: colors.text.secondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ import { useAuthStore } from '../../stores';
|
||||
import { authService } from '../../services/authService';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { ApiError } from '../../services/api';
|
||||
import {
|
||||
clearConversationMessages,
|
||||
getConversationCache,
|
||||
} from '../../services/database';
|
||||
import { messageRepository, conversationRepository } from '@/database';
|
||||
import { messageManager } from '../../stores/messageManager';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
|
||||
@@ -81,7 +78,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = await getConversationCache(conversationId);
|
||||
const cached = await conversationRepository.getCache(conversationId);
|
||||
if (cached) {
|
||||
setIsPinned(Boolean((cached as any).is_pinned));
|
||||
return;
|
||||
@@ -137,7 +134,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearConversationMessages(conversationId);
|
||||
await messageRepository.clearConversation(conversationId);
|
||||
Alert.alert('成功', '聊天记录已清空');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '清空聊天记录失败');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, ScrollView } from 'react-native';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, ScrollView, Platform } from 'react-native';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
@@ -18,6 +18,12 @@ import { useAppColors, spacing } from '../../../../theme';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
const blurActiveElementOnWeb = () => {
|
||||
if (Platform.OS !== 'web') return;
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
};
|
||||
|
||||
// 表情尺寸配置 - 固定宽度 40px,使用 flex 布局
|
||||
const EMOJI_SIZES = {
|
||||
mobile: { size: 24, itemWidth: 40, itemHeight: 40 },
|
||||
@@ -176,6 +182,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
|
||||
// 打开管理界面
|
||||
const handleOpenManage = () => {
|
||||
blurActiveElementOnWeb();
|
||||
setShowManageModal(true);
|
||||
setManageMode(false);
|
||||
setSelectedStickers(new Set());
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '../../../../types/dto';
|
||||
import { spacing, useAppColors, type AppColors } from '../../../../theme';
|
||||
import { SenderInfo } from './types';
|
||||
import { useChatSettingsStore } from '../../../../stores/chatSettingsStore';
|
||||
|
||||
const IMAGE_MAX_WIDTH = 200;
|
||||
const IMAGE_MAX_HEIGHT = 260;
|
||||
@@ -617,7 +618,8 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
||||
getSenderInfo,
|
||||
}) => {
|
||||
const themeColors = useAppColors();
|
||||
const styles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||
const fontSize = useChatSettingsStore((s) => s.fontSize);
|
||||
const styles = useMemo(() => createSegmentStyles(themeColors, fontSize), [themeColors, fontSize]);
|
||||
|
||||
if (!replyMessage) {
|
||||
// 如果没有引用消息详情,只显示引用ID
|
||||
@@ -729,7 +731,8 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
getSenderInfo,
|
||||
}) => {
|
||||
const themeColors = useAppColors();
|
||||
const segStyles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||
const fontSize = useChatSettingsStore((s) => s.fontSize);
|
||||
const segStyles = useMemo(() => createSegmentStyles(themeColors, fontSize), [themeColors, fontSize]);
|
||||
|
||||
const replySegment = segments.find(s => s.type === 'reply');
|
||||
const otherSegments = segments.filter(s => s.type !== 'reply');
|
||||
@@ -815,7 +818,7 @@ const formatDuration = (seconds: number): string => {
|
||||
return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `0:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
function createSegmentStyles(colors: AppColors) {
|
||||
function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
return StyleSheet.create({
|
||||
// 容器
|
||||
segmentsContainer: {
|
||||
@@ -847,10 +850,10 @@ function createSegmentStyles(colors: AppColors) {
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
// 文本 - 适配暗色模式
|
||||
// 文本 - 适配暗色模式,支持动态字号
|
||||
textContent: {
|
||||
fontSize: 17.5,
|
||||
lineHeight: 25,
|
||||
fontSize,
|
||||
lineHeight: Math.round(fontSize * 1.43),
|
||||
letterSpacing: 0.1,
|
||||
fontWeight: '400',
|
||||
},
|
||||
@@ -861,10 +864,10 @@ function createSegmentStyles(colors: AppColors) {
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
|
||||
// @提及 - 微信/QQ 风格:更精致的高亮
|
||||
// @提及 - 微信/QQ 风格:更精致的高亮,支持动态字号
|
||||
atText: {
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
fontSize,
|
||||
lineHeight: Math.round(fontSize * 1.4),
|
||||
fontWeight: '500',
|
||||
},
|
||||
atTextMe: {
|
||||
@@ -904,7 +907,7 @@ function createSegmentStyles(colors: AppColors) {
|
||||
height: 28,
|
||||
},
|
||||
faceText: {
|
||||
fontSize: 16,
|
||||
fontSize,
|
||||
color: '#666',
|
||||
},
|
||||
|
||||
@@ -930,7 +933,7 @@ function createSegmentStyles(colors: AppColors) {
|
||||
},
|
||||
voiceDuration: {
|
||||
marginLeft: spacing.sm,
|
||||
fontSize: 15,
|
||||
fontSize: fontSize - 1,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
@@ -1113,14 +1116,14 @@ function createSegmentStyles(colors: AppColors) {
|
||||
minWidth: 0,
|
||||
},
|
||||
replySender: {
|
||||
fontSize: 13,
|
||||
fontSize: fontSize - 3,
|
||||
fontWeight: '600',
|
||||
color: colors.chat.link,
|
||||
marginRight: 6,
|
||||
flexShrink: 0,
|
||||
},
|
||||
replyText: {
|
||||
fontSize: 13,
|
||||
fontSize: fontSize - 3,
|
||||
color: colors.chat.textTertiary,
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
|
||||
@@ -42,10 +42,7 @@ import {
|
||||
PendingChatAttachment,
|
||||
} from './types';
|
||||
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants';
|
||||
import {
|
||||
deleteMessage as deleteMessageFromDb,
|
||||
clearConversationMessages,
|
||||
} from '../../../../services/database';
|
||||
import { messageRepository } from '@/database';
|
||||
|
||||
export const useChatScreen = () => {
|
||||
const getSendErrorMessage = useCallback((error: unknown, fallback: string) => {
|
||||
@@ -1209,7 +1206,7 @@ export const useChatScreen = () => {
|
||||
const updatedMessages = messages.filter(m => m.id !== messageId);
|
||||
// 注意:这里我们依赖 MessageManager 的数据,所以需要通过其他方式刷新
|
||||
// 暂时只删除本地数据库
|
||||
await deleteMessageFromDb(messageId);
|
||||
await messageRepository.delete(messageId);
|
||||
} catch (error) {
|
||||
console.error('删除消息失败:', error);
|
||||
Alert.alert('删除失败', '无法删除消息');
|
||||
@@ -1224,7 +1221,7 @@ export const useChatScreen = () => {
|
||||
setLastSeq(0);
|
||||
setFirstSeq(0);
|
||||
setHasMoreHistory(true);
|
||||
await clearConversationMessages(conversationId);
|
||||
await messageRepository.clearConversation(conversationId);
|
||||
// 刷新消息列表
|
||||
await refreshMessages();
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
MessageSegment,
|
||||
} from '../../../types/dto';
|
||||
import { authService } from '../../../services';
|
||||
import { getUserCache } from '../../../services/database';
|
||||
import { userCacheRepository } from '@/database';
|
||||
import { Avatar, Text } from '../../../components/common';
|
||||
|
||||
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
||||
@@ -62,7 +62,7 @@ const AsyncMessagePreview: React.FC<{
|
||||
try {
|
||||
const asyncText = await extractTextFromSegmentsAsync(
|
||||
segments,
|
||||
getUserCache,
|
||||
userCacheRepository.get.bind(userCacheRepository),
|
||||
authService.getUserById.bind(authService)
|
||||
);
|
||||
if (isMountedRef.current && asyncText !== initialText) {
|
||||
|
||||
@@ -57,6 +57,10 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if (Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
if (!currentUserId) return;
|
||||
|
||||
const nextSelectedIds = new Set(stableInitialSelectedIds);
|
||||
|
||||
583
src/screens/profile/AboutScreen.tsx
Normal file
583
src/screens/profile/AboutScreen.tsx
Normal file
@@ -0,0 +1,583 @@
|
||||
/**
|
||||
* 关于我们页面 AboutScreen
|
||||
* 胡萝卜BBS - 应用信息、版本检查与更新
|
||||
* 扁平化设计风格,参考登录、注册、设置页面
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import Constants from 'expo-constants';
|
||||
import { useRouter } from 'expo-router';
|
||||
import {
|
||||
useAppColors,
|
||||
spacing,
|
||||
fontSizes,
|
||||
borderRadius,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { Text } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
import { checkForAPKUpdate, type VersionCheckResult } from '../../services/apkUpdateService';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
const CONTENT_MAX_WIDTH = 720;
|
||||
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
const APP_NAME = '萝卜社区';
|
||||
const APP_SLOGAN = '连接校园,分享生活';
|
||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
||||
const BUILD_NUMBER = Constants.expoConfig?.android?.versionCode || Constants.expoConfig?.ios?.buildNumber || '1';
|
||||
|
||||
function createAboutStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
content: {
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing['3xl'],
|
||||
paddingTop: spacing.xl,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 20,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
appName: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
appSlogan: {
|
||||
fontSize: 14,
|
||||
color: colors.text.secondary,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
versionText: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
|
||||
groupContainer: {
|
||||
marginBottom: spacing['2xl'],
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
groupHeader: {
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
groupTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.secondary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
settingItemLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
settingItemLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
},
|
||||
settingContent: {
|
||||
flex: 1,
|
||||
},
|
||||
subtitle: {
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
updateSection: {
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
marginBottom: spacing['2xl'],
|
||||
},
|
||||
updateCard: {
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg,
|
||||
},
|
||||
updateHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
updateIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.paper,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
updateTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
updateSubtitle: {
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
checkUpdateButton: {
|
||||
height: 48,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
checkUpdateText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
|
||||
updateStatusContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
updateStatusTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
marginTop: spacing.md,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
updateStatusDesc: {
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
newVersionContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
newVersionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
newVersionText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
newVersionBadge: {
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: borderRadius.sm,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
newVersionBadgeText: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
releaseDate: {
|
||||
fontSize: 12,
|
||||
color: colors.text.hint,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
releaseNotes: {
|
||||
fontSize: 14,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 20,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
updateActions: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
updateButton: {
|
||||
flex: 1,
|
||||
height: 40,
|
||||
borderRadius: borderRadius.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
},
|
||||
secondaryButton: {
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
primaryButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
|
||||
infoValue: {
|
||||
fontSize: 15,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.xl,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
copyright: {
|
||||
fontSize: 12,
|
||||
color: colors.text.hint,
|
||||
textAlign: 'center',
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
color: colors.text.hint,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateStatus = 'idle' | 'checking' | 'noUpdate' | 'hasUpdate' | 'error' | 'notSupported';
|
||||
|
||||
export const AboutScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createAboutStyles(colors), [colors]);
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const { isMobile } = useResponsive();
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
|
||||
const [versionInfo, setVersionInfo] = useState<VersionCheckResult | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||||
|
||||
const checkForUpdate = useCallback(async () => {
|
||||
setUpdateStatus('checking');
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
if (Platform.OS !== 'android') {
|
||||
setUpdateStatus('notSupported');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await checkForAPKUpdate(true);
|
||||
|
||||
if (!result) {
|
||||
setErrorMessage('无法获取版本信息,请检查网络连接');
|
||||
setUpdateStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
setVersionInfo(result);
|
||||
|
||||
if (result.hasUpdate) {
|
||||
setUpdateStatus('hasUpdate');
|
||||
} else {
|
||||
setUpdateStatus('noUpdate');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage('检查更新失败,请稍后重试');
|
||||
setUpdateStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleUpdate = useCallback(() => {
|
||||
if (!versionInfo?.downloadUrl) return;
|
||||
|
||||
Alert.alert(
|
||||
'更新应用',
|
||||
`即将下载版本 ${versionInfo.latestVersion},是否继续?`,
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '立即更新',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const canOpen = await Linking.canOpenURL(versionInfo.downloadUrl);
|
||||
if (canOpen) {
|
||||
await Linking.openURL(versionInfo.downloadUrl);
|
||||
} else {
|
||||
Alert.alert('错误', '无法打开下载链接');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '打开下载链接失败');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [versionInfo]);
|
||||
|
||||
const renderUpdateStatus = () => {
|
||||
switch (updateStatus) {
|
||||
case 'checking':
|
||||
return (
|
||||
<View style={styles.updateStatusContainer}>
|
||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
||||
<Text style={styles.updateStatusTitle}>正在检查更新...</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
case 'noUpdate':
|
||||
return (
|
||||
<View style={styles.updateStatusContainer}>
|
||||
<MaterialCommunityIcons name="check-circle" size={36} color="#4CAF50" />
|
||||
<Text style={styles.updateStatusTitle}>已是最新版本</Text>
|
||||
<Text style={styles.updateStatusDesc}>当前版本 {APP_VERSION} 已是最新版本</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.checkUpdateButton, { marginTop: spacing.lg, backgroundColor: colors.background.paper }]}
|
||||
onPress={() => setUpdateStatus('idle')}
|
||||
>
|
||||
<MaterialCommunityIcons name="refresh" size={18} color={colors.text.primary} />
|
||||
<Text style={[styles.checkUpdateText, { color: colors.text.primary }]}>重新检查</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
case 'hasUpdate':
|
||||
return (
|
||||
<View style={styles.newVersionContainer}>
|
||||
<View style={styles.newVersionHeader}>
|
||||
<Text style={styles.newVersionText}>发现新版本</Text>
|
||||
<View style={styles.newVersionBadge}>
|
||||
<Text style={styles.newVersionBadgeText}>New</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.releaseDate}>版本 {versionInfo?.latestVersion} · 当前版本 {versionInfo?.currentVersion}</Text>
|
||||
<Text style={styles.releaseNotes}>新版本已发布,建议立即更新以获得更好的体验。</Text>
|
||||
<View style={styles.updateActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.updateButton, styles.secondaryButton]}
|
||||
onPress={() => setUpdateStatus('idle')}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>稍后再说</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.updateButton, styles.primaryButton]}
|
||||
onPress={handleUpdate}
|
||||
>
|
||||
<MaterialCommunityIcons name="download" size={16} color="#fff" />
|
||||
<Text style={styles.primaryButtonText}>立即更新</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case 'error':
|
||||
return (
|
||||
<View style={styles.updateStatusContainer}>
|
||||
<MaterialCommunityIcons name="alert-circle" size={36} color={colors.error.main} />
|
||||
<Text style={styles.updateStatusTitle}>检查失败</Text>
|
||||
<Text style={styles.updateStatusDesc}>{errorMessage || '请检查网络连接后重试'}</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.checkUpdateButton, { marginTop: spacing.lg }]}
|
||||
onPress={checkForUpdate}
|
||||
>
|
||||
<MaterialCommunityIcons name="refresh" size={18} color="#fff" />
|
||||
<Text style={styles.checkUpdateText}>重新检查</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
case 'notSupported':
|
||||
return (
|
||||
<View style={styles.updateStatusContainer}>
|
||||
<MaterialCommunityIcons name="apple" size={36} color={colors.text.secondary} />
|
||||
<Text style={styles.updateStatusTitle}>iOS 平台</Text>
|
||||
<Text style={styles.updateStatusDesc}>iOS 用户请前往 App Store 检查更新</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.checkUpdateButton, { marginTop: spacing.lg, backgroundColor: colors.background.paper }]}
|
||||
onPress={() => setUpdateStatus('idle')}
|
||||
>
|
||||
<MaterialCommunityIcons name="refresh" size={18} color={colors.text.primary} />
|
||||
<Text style={[styles.checkUpdateText, { color: colors.text.primary }]}>重新检查</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<TouchableOpacity style={styles.checkUpdateButton} onPress={checkForUpdate}>
|
||||
<MaterialCommunityIcons name="update" size={18} color="#fff" />
|
||||
<Text style={styles.checkUpdateText}>检查新版本</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const legalItems = [
|
||||
{ key: 'terms', icon: 'file-document-outline', title: '用户协议', action: () => router.push(hrefs.hrefProfileTerms()) },
|
||||
{ key: 'privacy', icon: 'shield-outline', title: '隐私政策', action: () => router.push(hrefs.hrefProfilePrivacy()) },
|
||||
];
|
||||
|
||||
const appInfoItems = [
|
||||
{ key: 'name', icon: 'information-outline', title: '应用名称', value: APP_NAME },
|
||||
{ key: 'version', icon: 'tag-outline', title: '版本号', value: `v${APP_VERSION} (${BUILD_NUMBER})` },
|
||||
{ key: 'developer', icon: 'office-building-outline', title: '开发者', value: '青春之旅电子信息科技' },
|
||||
];
|
||||
|
||||
const renderSettingItem = (item: { key: string; icon: string; title: string; value?: string; action?: () => void }, index: number, total: number, showArrow: boolean = false) => (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.settingItem, index === total - 1 && styles.settingItemLast]}
|
||||
onPress={item.action}
|
||||
activeOpacity={0.7}
|
||||
disabled={!item.action}
|
||||
>
|
||||
<View style={styles.settingItemLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons name={item.icon as any} size={22} color={colors.text.secondary} />
|
||||
</View>
|
||||
<View style={styles.settingContent}>
|
||||
<Text variant="body" color={colors.text.primary}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.value && (
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
|
||||
{item.value}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{showArrow && (
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||
)}
|
||||
{!showArrow && item.value && (
|
||||
<Text style={styles.infoValue}>{item.value}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Text style={styles.logoText}>萝卜</Text>
|
||||
</View>
|
||||
<Text style={styles.appName}>{APP_NAME}</Text>
|
||||
<Text style={styles.appSlogan}>{APP_SLOGAN}</Text>
|
||||
<Text style={styles.versionText}>版本 {APP_VERSION}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.updateSection}>
|
||||
<View style={styles.updateCard}>
|
||||
<View style={styles.updateHeader}>
|
||||
<View style={styles.updateIconContainer}>
|
||||
<MaterialCommunityIcons name="cellphone-arrow-down" size={22} color={THEME_COLORS.primary} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.updateTitle}>版本更新</Text>
|
||||
<Text style={styles.updateSubtitle}>检查并获取最新版本</Text>
|
||||
</View>
|
||||
</View>
|
||||
{renderUpdateStatus()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.groupContainer}>
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
法律信息
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
{legalItems.map((item, index) => renderSettingItem(item, index, legalItems.length, true))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.groupContainer}>
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
应用信息
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
{appInfoItems.map((item, index) => renderSettingItem(item, index, appInfoItems.length, false))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.copyright}>© 2026 青春之旅电子信息科技(威海)有限公司</Text>
|
||||
<Text style={styles.companyName}>鲁ICP备XXXXXXXX号-1</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutScreen;
|
||||
@@ -15,6 +15,14 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 720;
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
import { authService, resolveAuthApiError } from '../../services/authService';
|
||||
import { showPrompt } from '../../services/promptService';
|
||||
import { useAuthStore } from '../../stores';
|
||||
@@ -186,167 +194,173 @@ export const AccountSecurityScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text variant="caption" style={styles.sectionTitle}>
|
||||
邮箱验证
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.statusRow}>
|
||||
<Text variant="body" color={colors.text.primary}>当前状态</Text>
|
||||
<View style={[styles.statusBadge, isEmailVerified ? styles.statusVerified : styles.statusUnverified]}>
|
||||
<Text variant="caption" color={isEmailVerified ? '#1B5E20' : '#B26A00'}>
|
||||
{emailStatusText}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="email-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="请输入邮箱"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.codeRow}>
|
||||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="验证码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={verificationCode}
|
||||
onChangeText={setVerificationCode}
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendCodeButton, (sendingCode || countdown > 0) && styles.buttonDisabled]}
|
||||
onPress={handleSendCode}
|
||||
disabled={sendingCode || countdown > 0}
|
||||
>
|
||||
{sendingCode ? (
|
||||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||
) : (
|
||||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, verifyingEmail && styles.buttonDisabled]}
|
||||
onPress={handleVerifyEmail}
|
||||
disabled={verifyingEmail}
|
||||
>
|
||||
{verifyingEmail ? (
|
||||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||
) : (
|
||||
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text variant="caption" style={styles.sectionTitle}>
|
||||
修改密码
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="当前密码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={oldPassword}
|
||||
onChangeText={setOldPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-plus-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="新密码(至少6位)"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={newPassword}
|
||||
onChangeText={setNewPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.codeRow}>
|
||||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="邮箱验证码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={changePasswordCode}
|
||||
onChangeText={setChangePasswordCode}
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendCodeButton, (sendingChangePwdCode || changePwdCountdown > 0) && styles.buttonDisabled]}
|
||||
onPress={handleSendChangePasswordCode}
|
||||
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
||||
>
|
||||
{sendingChangePwdCode ? (
|
||||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||
) : (
|
||||
<Text style={styles.sendCodeButtonText}>
|
||||
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-check-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="确认新密码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, updatingPassword && styles.buttonDisabled]}
|
||||
onPress={handleChangePassword}
|
||||
disabled={updatingPassword}
|
||||
>
|
||||
{updatingPassword ? (
|
||||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||
) : (
|
||||
<Text style={styles.primaryButtonText}>更新密码</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
|
||||
{content}
|
||||
<View style={styles.content}>
|
||||
{/* 邮箱验证分组 */}
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
邮箱验证
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
{/* 状态显示 */}
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>当前状态</Text>
|
||||
<View style={[styles.statusBadge, isEmailVerified ? styles.statusVerified : styles.statusUnverified]}>
|
||||
<Text style={[styles.statusText, isEmailVerified ? styles.statusVerifiedText : styles.statusUnverifiedText]}>
|
||||
{emailStatusText}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 邮箱输入框 */}
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="email-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="请输入邮箱"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 验证码行 */}
|
||||
<View style={styles.codeRow}>
|
||||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="验证码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={verificationCode}
|
||||
onChangeText={setVerificationCode}
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendCodeButton, (sendingCode || countdown > 0) && styles.buttonDisabled]}
|
||||
onPress={handleSendCode}
|
||||
disabled={sendingCode || countdown > 0}
|
||||
>
|
||||
{sendingCode ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 验证按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, verifyingEmail && styles.buttonDisabled]}
|
||||
onPress={handleVerifyEmail}
|
||||
disabled={verifyingEmail}
|
||||
>
|
||||
{verifyingEmail ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 修改密码分组 */}
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
修改密码
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
{/* 当前密码 */}
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="当前密码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={oldPassword}
|
||||
onChangeText={setOldPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 新密码 */}
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-plus-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="新密码(至少6位)"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={newPassword}
|
||||
onChangeText={setNewPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 确认新密码 */}
|
||||
<View style={styles.inputWrapper}>
|
||||
<MaterialCommunityIcons name="lock-check-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="确认新密码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 验证码行 */}
|
||||
<View style={styles.codeRow}>
|
||||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="邮箱验证码"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={changePasswordCode}
|
||||
onChangeText={setChangePasswordCode}
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendCodeButton, (sendingChangePwdCode || changePwdCountdown > 0) && styles.buttonDisabled]}
|
||||
onPress={handleSendChangePasswordCode}
|
||||
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
||||
>
|
||||
{sendingChangePwdCode ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.sendCodeButtonText}>
|
||||
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 更新密码按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, updatingPassword && styles.buttonDisabled]}
|
||||
onPress={handleChangePassword}
|
||||
disabled={updatingPassword}
|
||||
>
|
||||
{updatingPassword ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.primaryButtonText}>更新密码</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -356,37 +370,51 @@ function createAccountSecurityStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.paper,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
section: {
|
||||
marginBottom: spacing['2xl'],
|
||||
content: {
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
sectionHeader: {
|
||||
// 分组标题
|
||||
groupHeader: {
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
groupTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.secondary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
// 卡片样式 - 统一
|
||||
card: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 16,
|
||||
padding: 6,
|
||||
marginBottom: spacing['2xl'],
|
||||
marginHorizontal: spacing['2xl'],
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
// 状态显示
|
||||
statusRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
marginBottom: spacing.md,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
},
|
||||
statusLabel: {
|
||||
fontSize: 15,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: spacing.sm,
|
||||
@@ -394,24 +422,29 @@ function createAccountSecurityStyles(colors: AppColors) {
|
||||
borderRadius: borderRadius.sm,
|
||||
},
|
||||
statusVerified: {
|
||||
backgroundColor: colors.success.light + '40',
|
||||
backgroundColor: '#E8F5E9',
|
||||
},
|
||||
statusUnverified: {
|
||||
backgroundColor: colors.warning.light + '40',
|
||||
backgroundColor: '#FFF3E0',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
statusVerifiedText: {
|
||||
color: colors.success.dark,
|
||||
color: '#2E7D32',
|
||||
},
|
||||
statusUnverifiedText: {
|
||||
color: colors.warning.dark,
|
||||
color: '#E65100',
|
||||
},
|
||||
// 输入框样式 - 统一
|
||||
inputWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: spacing.md,
|
||||
height: 50,
|
||||
height: 56,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
inputIcon: {
|
||||
@@ -421,8 +454,9 @@ function createAccountSecurityStyles(colors: AppColors) {
|
||||
flex: 1,
|
||||
color: colors.text.primary,
|
||||
fontSize: fontSizes.md,
|
||||
height: 50,
|
||||
height: 56,
|
||||
},
|
||||
// 验证码行
|
||||
codeRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -433,30 +467,32 @@ function createAccountSecurityStyles(colors: AppColors) {
|
||||
flex: 1,
|
||||
marginBottom: 0,
|
||||
},
|
||||
// 发送验证码按钮
|
||||
sendCodeButton: {
|
||||
height: 50,
|
||||
height: 56,
|
||||
minWidth: 110,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.primary.main,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
sendCodeButtonText: {
|
||||
color: colors.text.inverse,
|
||||
color: '#fff',
|
||||
fontSize: fontSizes.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
// 主按钮样式 - 统一
|
||||
primaryButton: {
|
||||
height: 50,
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.primary.main,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: colors.text.inverse,
|
||||
color: '#fff',
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
* 胡萝卜BBS - 聊天个性化设置
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useCallback, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Dimensions,
|
||||
PanResponder,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import {
|
||||
useAppColors,
|
||||
spacing,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
useChatSettingsActions,
|
||||
CHAT_THEMES,
|
||||
} from '../../stores/chatSettingsStore';
|
||||
import { useCurrentUser } from '../../stores';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const CARD_MAX_WIDTH = 720;
|
||||
@@ -43,7 +41,7 @@ interface SliderProps {
|
||||
showValue?: boolean;
|
||||
}
|
||||
|
||||
// 自定义滑块组件
|
||||
// 自定义滑块组件(支持拖动)
|
||||
const Slider: React.FC<SliderProps> = ({
|
||||
value,
|
||||
min,
|
||||
@@ -55,34 +53,51 @@ const Slider: React.FC<SliderProps> = ({
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createSliderStyles(colors), [colors]);
|
||||
const trackWidthRef = useRef(0);
|
||||
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
const handlePress = useCallback((event: any) => {
|
||||
const { locationX } = event.nativeEvent;
|
||||
const sliderWidth = SCREEN_WIDTH - 64; // 考虑边距
|
||||
const newPercentage = Math.max(0, Math.min(100, (locationX / sliderWidth) * 100));
|
||||
const percentage = isNaN(value) ? 0 : ((value - min) / (max - min)) * 100;
|
||||
|
||||
const updateValue = useCallback((locationX: number) => {
|
||||
const width = trackWidthRef.current;
|
||||
if (width <= 0) return;
|
||||
|
||||
const newPercentage = Math.max(0, Math.min(100, (locationX / width) * 100));
|
||||
const newValue = Math.round(min + (newPercentage / 100) * (max - min));
|
||||
onValueChange(newValue);
|
||||
}, [min, max, onValueChange]);
|
||||
|
||||
const panResponder = useMemo(() => PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: (evt) => {
|
||||
updateValue(evt.nativeEvent.locationX);
|
||||
},
|
||||
onPanResponderMove: (evt) => {
|
||||
updateValue(evt.nativeEvent.locationX);
|
||||
},
|
||||
}), [updateValue]);
|
||||
|
||||
const handleLayout = useCallback((event: any) => {
|
||||
trackWidthRef.current = event.nativeEvent.layout.width;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.labelRow}>
|
||||
{leftLabel && <Text style={styles.label}>{leftLabel}</Text>}
|
||||
{showValue && <Text style={styles.value}>{value}</Text>}
|
||||
{showValue && <Text style={styles.value}>{isNaN(value) ? 0 : value}</Text>}
|
||||
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<View
|
||||
style={styles.trackContainer}
|
||||
onPress={handlePress}
|
||||
activeOpacity={1}
|
||||
onLayout={handleLayout}
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
<View style={styles.track}>
|
||||
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: colors.primary.main }]} />
|
||||
</View>
|
||||
<View style={[styles.thumb, { left: `${percentage}%`, backgroundColor: colors.primary.main }]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -168,29 +183,32 @@ function createStyles(colors: AppColors) {
|
||||
minHeight: 140,
|
||||
},
|
||||
previewMessage: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
maxWidth: '80%',
|
||||
alignSelf: 'flex-start',
|
||||
minWidth: 44,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
previewReply: {
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.sm,
|
||||
maxWidth: '80%',
|
||||
alignSelf: 'flex-end',
|
||||
minWidth: 44,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
previewText: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
previewTime: {
|
||||
fontSize: fontSizes.xs,
|
||||
color: colors.text.hint,
|
||||
marginTop: 2,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
// 主题选择样式
|
||||
themeList: {
|
||||
flexDirection: 'row',
|
||||
@@ -240,106 +258,56 @@ function createStyles(colors: AppColors) {
|
||||
marginTop: 4,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
// 设置项样式 - 扁平化
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
settingItemLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
settingItemLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
},
|
||||
settingTitle: {
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
settingSubtitle: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.secondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
// 滑块容器 - 扁平化
|
||||
sliderContainer: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
// 开关样式
|
||||
switch: {
|
||||
width: 50,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.divider,
|
||||
padding: 2,
|
||||
},
|
||||
switchActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
switchThumb: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const ChatSettingsScreen: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
|
||||
// 获取当前用户信息
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 从 store 获取状态
|
||||
const fontSize = useChatSettingsStore((s) => s.fontSize);
|
||||
const messageRadius = useChatSettingsStore((s) => s.messageRadius);
|
||||
const themeIndex = useChatSettingsStore((s) => s.themeIndex);
|
||||
const nightMode = useChatSettingsStore((s) => s.nightMode);
|
||||
|
||||
// 获取操作方法
|
||||
const { setFontSize, setMessageRadius, setTheme, toggleNightMode } = useChatSettingsActions();
|
||||
const { setFontSize, setMessageRadius, setTheme } = useChatSettingsActions();
|
||||
|
||||
const currentTheme = CHAT_THEMES[themeIndex];
|
||||
|
||||
// 用户显示名称
|
||||
const userName = currentUser?.nickname || currentUser?.username || '我';
|
||||
|
||||
// 渲染聊天预览
|
||||
const renderChatPreview = () => (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>预览</Text>
|
||||
<View style={[styles.previewContainer, { backgroundColor: currentTheme.secondary }]}>
|
||||
<View style={[styles.previewMessage, { borderRadius: messageRadius }]}>
|
||||
<Text style={[styles.previewText, { fontSize }]}>{userName}</Text>
|
||||
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
|
||||
<View style={[
|
||||
styles.previewMessage,
|
||||
{
|
||||
borderRadius: messageRadius,
|
||||
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
|
||||
}
|
||||
]}>
|
||||
<Text style={[styles.previewText, { fontSize }]}>
|
||||
早上好!👋
|
||||
</Text>
|
||||
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
|
||||
<Text style={[styles.previewText, { fontSize }]}>
|
||||
你知道现在几点吗?
|
||||
</Text>
|
||||
<Text style={styles.previewTime}>AM12:42</Text>
|
||||
</View>
|
||||
<View style={[styles.previewReply, { borderRadius: messageRadius, backgroundColor: currentTheme.bubble }]}>
|
||||
<View style={[
|
||||
styles.previewReply,
|
||||
{
|
||||
borderRadius: messageRadius,
|
||||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
|
||||
backgroundColor: currentTheme.bubble
|
||||
}
|
||||
]}>
|
||||
<Text style={[styles.previewText, { fontSize }]}>
|
||||
现在是威海的早晨😎
|
||||
</Text>
|
||||
<Text style={styles.previewTime}>AM12:57</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -410,87 +378,18 @@ export const ChatSettingsScreen: React.FC = () => {
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染其他设置项
|
||||
const renderOtherSettings = () => (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>其他</Text>
|
||||
<TouchableOpacity style={styles.settingItem}>
|
||||
<View style={styles.settingItemLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons name="image-outline" size={22} color={colors.text.secondary} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.settingTitle}>更改聊天壁纸</Text>
|
||||
</View>
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
|
||||
<View style={styles.settingItemLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons name="palette-outline" size={22} color={colors.text.secondary} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.settingTitle}>更改名称颜色</Text>
|
||||
<Text style={styles.settingSubtitle}>MiMQy</Text>
|
||||
</View>
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染夜间模式开关
|
||||
const renderNightMode = () => (
|
||||
<View style={styles.section}>
|
||||
<View style={[styles.settingItem, styles.settingItemLast]}>
|
||||
<View style={styles.settingItemLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons name="weather-night" size={22} color={colors.text.secondary} />
|
||||
</View>
|
||||
<Text style={styles.settingTitle}>切换到夜间模式</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.switch, nightMode && styles.switchActive]}
|
||||
onPress={() => toggleNightMode()}
|
||||
>
|
||||
<View style={[styles.switchThumb, { alignSelf: nightMode ? 'flex-end' : 'flex-start' }]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染浏览其他主题
|
||||
const renderBrowseThemes = () => (
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
|
||||
<View style={styles.settingItemLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons name="apps" size={22} color={colors.text.secondary} />
|
||||
</View>
|
||||
<Text style={styles.settingTitle}>浏览其他主题</Text>
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.scrollContent,
|
||||
{ paddingHorizontal: responsivePadding }
|
||||
{ paddingHorizontal: responsivePadding, paddingBottom: 80 }
|
||||
]}
|
||||
>
|
||||
{renderFontSizeSlider()}
|
||||
{renderChatPreview()}
|
||||
{renderThemeColors()}
|
||||
{renderRadiusSlider()}
|
||||
{renderOtherSettings()}
|
||||
{renderNightMode()}
|
||||
{renderBrowseThemes()}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
320
src/screens/profile/PrivacyPolicyScreen.tsx
Normal file
320
src/screens/profile/PrivacyPolicyScreen.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 隐私政策页面 PrivacyPolicyScreen
|
||||
* 胡萝卜BBS - 隐私政策
|
||||
* 扁平化设计风格
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import {
|
||||
useAppColors,
|
||||
spacing,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { Text } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 400;
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
};
|
||||
|
||||
// 政策最后更新日期
|
||||
const LAST_UPDATED = '2026年4月4日';
|
||||
|
||||
// 隐私政策内容
|
||||
const PRIVACY_SECTIONS = [
|
||||
{
|
||||
title: '引言',
|
||||
content: `萝卜社区(以下简称"我们"或"本应用")非常重视用户的隐私和个人信息保护。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。
|
||||
|
||||
本隐私政策旨在帮助您了解我们如何收集、使用、存储、共享和保护您的个人信息,以及您享有的相关权利。请您在使用我们的服务前,仔细阅读并理解本隐私政策的全部内容。
|
||||
|
||||
【敏感个人信息提示】请您注意,您发布的照片、视频可能包含生物识别信息,您的位置信息属于行踪轨迹信息,通讯录信息属于社交关系信息,以上均属于敏感个人信息。我们收集这些信息是为了实现校园社交的核心功能,请您谨慎考虑是否提供。拒绝提供敏感个人信息仅会影响相关功能的使用,不会影响您使用其他服务。`,
|
||||
},
|
||||
{
|
||||
title: '一、我们收集的信息',
|
||||
content: `1.1 您主动提供的信息
|
||||
当您注册、登录或使用我们的服务时,您可能需要提供以下信息:
|
||||
• 账号信息:手机号、邮箱地址、用户名、密码等
|
||||
• 个人资料:昵称、头像、性别、生日、学校、专业等
|
||||
• 联系信息:真实姓名、地址(用于特定功能)
|
||||
• 内容信息:您发布的内容、评论、私信等
|
||||
|
||||
1.2 我们在您使用服务时收集的信息
|
||||
• 设备信息:包括设备型号、操作系统版本、设备设置、唯一设备标识符(如IMEI、Android ID、IDFA、OAID、MAC地址等)、IP地址等
|
||||
• 日志信息:IP地址、访问时间、浏览记录、操作日志等
|
||||
• 位置信息:为了向您提供"校园圈"、"附近的人"等基于位置的服务,经您授权,我们会收集您的精确地理位置信息。该信息属于敏感个人信息,拒绝提供仅会影响相关功能,不影响其他服务的使用
|
||||
• 相机/相册权限:用于发布动态、更换头像。我们仅在您主动触发相关功能时申请权限,且不会在后台持续收集
|
||||
• 通讯录权限:经您授权后用于查找好友
|
||||
|
||||
1.3 从第三方获取的信息
|
||||
如您使用第三方账号登录功能(具体以应用实际提供的登录方式为准),我们会从第三方获取您授权共享的账号信息(如昵称、头像等)。`,
|
||||
},
|
||||
{
|
||||
title: '二、我们如何使用信息',
|
||||
content: `2.1 我们使用收集的信息用于以下目的:
|
||||
• 为您提供注册、登录及身份验证服务
|
||||
• 为您展示个性化的内容推荐
|
||||
• 实现社交功能,如好友推荐、内容分享、附近的人
|
||||
• 向您发送服务通知、安全提示
|
||||
• 改善和优化我们的产品和服务
|
||||
• 保护账号安全,防范欺诈和恶意行为
|
||||
• 遵守法律法规的要求
|
||||
|
||||
2.2 我们使用Cookie和同类技术
|
||||
我们使用Cookie和同类技术(如Web Beacon、Pixel标签、本地存储)来确保应用正常运转、记住您的偏好、分析您使用我们服务的情况。具体包括:
|
||||
• 用于记住您的登录状态
|
||||
• 了解您的使用偏好,提供个性化服务
|
||||
• 分析服务使用情况,优化产品体验
|
||||
• 用于广告归因和效果分析
|
||||
我们不会将Cookie用于本政策所述目的之外的任何用途。您可以通过设备设置管理Cookie,但这可能影响某些功能的使用。`,
|
||||
},
|
||||
{
|
||||
title: '三、信息的共享与披露',
|
||||
content: `3.1 我们不会将您的个人信息出售给任何第三方,但在以下情况下可能会共享或披露您的信息:
|
||||
|
||||
3.2 经您同意的情况
|
||||
在获得您的明确同意后,我们会与第三方共享您的个人信息。
|
||||
|
||||
3.3 与我们的关联公司共享
|
||||
为向您提供一致的服务体验,我们可能会与关联公司共享必要的个人信息。
|
||||
|
||||
3.4 与服务提供商共享
|
||||
我们可能会委托第三方服务提供商处理您的信息,如云服务提供商、支付服务提供商、数据分析服务商、推送服务商等。这些服务提供商仅能在我们授权的范围内使用您的信息,且我们要求其遵守严格的保密义务。
|
||||
|
||||
3.5 法律法规要求及无需征得同意的情形
|
||||
在以下情况下,我们可能会披露您的信息,且无需征得您的授权同意:
|
||||
(1)与国家安全、国防安全有关的;
|
||||
(2)与公共安全、公共卫生、重大公共利益有关的;
|
||||
(3)与犯罪侦查、起诉、审判和判决执行等有关的;
|
||||
(4)出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
|
||||
(5)所收集的个人信息是个人信息主体自行向社会公众公开的;
|
||||
(6)从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道;
|
||||
(7)根据个人信息主体要求签订和履行合同所必需的;
|
||||
(8)为配合学校、教育机构或相关主管部门履行法定管理职责(如学籍管理、校园安全、突发事件处理等)的;
|
||||
(9)法律法规规定的其他情形。
|
||||
|
||||
3.6 匿名化信息
|
||||
我们可能会对您的个人信息进行匿名化处理,使其无法识别特定个人,然后使用或共享这些匿名化信息。`,
|
||||
},
|
||||
{
|
||||
title: '四、信息的存储与保护',
|
||||
content: `4.1 信息存储
|
||||
• 原则上,我们在中华人民共和国境内收集和产生的个人信息,将存储在中华人民共和国境内
|
||||
• 如部分服务需要使用境外服务器(如跨境数据传输),我们会单独征得您的授权同意,并确保符合法律法规要求
|
||||
• 我们会采取合理的技术和管理措施保护您的信息安全
|
||||
• 我们只会在达成本隐私政策所述目的所需的期限内保留您的信息
|
||||
|
||||
4.2 安全措施
|
||||
• 使用加密技术保护数据传输安全
|
||||
• 实施访问控制,限制对个人信息的访问
|
||||
• 定期进行安全审计和风险评估
|
||||
• 对员工进行隐私保护培训
|
||||
|
||||
4.3 安全事件处理
|
||||
如发生个人信息泄露等安全事件,我们会:
|
||||
• 及时采取补救措施
|
||||
• 按照法律法规要求通知您和监管部门
|
||||
• 配合相关部门进行调查`,
|
||||
},
|
||||
{
|
||||
title: '五、您的权利',
|
||||
content: `5.1 访问和更正
|
||||
您有权访问和更正您的个人信息。您可以在应用内的"设置"-"编辑资料"中查看和修改大部分个人信息。
|
||||
|
||||
5.2 删除
|
||||
在以下情况下,您可以要求我们删除您的个人信息:
|
||||
• 我们处理信息的行为违反法律法规
|
||||
• 我们未经您同意收集、使用您的信息
|
||||
• 您不再使用我们的服务并要求删除
|
||||
• 我们违反与您的约定处理您的信息
|
||||
|
||||
5.3 撤回同意
|
||||
您可以随时撤回对我们收集、使用您个人信息的同意。但请注意,撤回同意可能会影响您使用某些服务。
|
||||
|
||||
5.4 注销账号
|
||||
您可以在"设置"-"账号安全"中申请注销账号。注销后,我们将停止为您提供服务,并删除或匿名化您的个人信息。但根据法律法规规定(如《网络安全法》要求的日志留存不少于6个月),我们需要保留部分必要信息以配合监管、处理历史纠纷或履行法定义务。
|
||||
|
||||
5.5 投诉与建议
|
||||
如果您对我们的隐私政策或信息处理有任何疑问或建议,请联系我们:system@qczlit.cn`,
|
||||
},
|
||||
{
|
||||
title: '六、未成年人保护',
|
||||
content: `6.1 我们非常重视对未成年人个人信息的保护。我们的服务主要面向大学生及成年用户。
|
||||
|
||||
6.2 如果您是未满18周岁的未成年人,在使用我们的服务前,请确保在监护人的陪同下阅读本隐私政策,并在获得监护人同意后再使用我们的服务。
|
||||
|
||||
6.3 如果您是未成年人的监护人,请监督您的被监护人使用本服务。
|
||||
|
||||
6.4 如果我们发现自己在未事先获得监护人同意的情况下收集了未成年人的个人信息,我们会尽快删除相关数据。`,
|
||||
},
|
||||
{
|
||||
title: '七、第三方SDK目录',
|
||||
content: `为了保障App的稳定运行或实现特定功能,我们可能接入第三方SDK。截至本隐私政策更新之日,我们主要使用以下服务:
|
||||
|
||||
• Expo推送服务:用于消息推送功能。可能收集设备标识符、设备信息。
|
||||
|
||||
如您后续接入其他第三方SDK(如微信登录、分享等功能),我们将在本章节更新相关说明,并告知您对应SDK收集的信息类型和用途。更新后的SDK目录将在应用内公布,请以最新版本为准。`,
|
||||
},
|
||||
{
|
||||
title: '八、隐私政策的更新',
|
||||
content: `8.1 我们可能会不时更新本隐私政策。更新后的隐私政策将在应用内公布,并标注更新日期。
|
||||
|
||||
8.2 对于重大变更,我们会在变更生效前通过应用内通知、弹窗等方式告知您。
|
||||
|
||||
8.3 如您不同意变更后的内容,应立即停止使用本服务。如您继续使用本服务,即视为您已阅读并同意受变更后的隐私政策约束。`,
|
||||
},
|
||||
{
|
||||
title: '九、联系我们',
|
||||
content: `如果您对本隐私政策有任何疑问、意见或建议,或者您希望行使您的权利,请通过以下方式与我们联系:
|
||||
|
||||
• 邮箱:system@qczlit.cn
|
||||
• 公司:青春之旅电子信息科技(威海)有限公司
|
||||
• 地址:山东省威海市环翠区哈尔滨工业大学(威海)
|
||||
|
||||
我们将在收到您的请求后的15个工作日内予以回复。对于复杂的请求,我们可能会延长回复期限,并及时告知您延长的原因和预计回复时间。`,
|
||||
},
|
||||
];
|
||||
|
||||
function createPrivacyStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 28,
|
||||
paddingTop: 24,
|
||||
},
|
||||
|
||||
// 内容区域
|
||||
content: {
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
// 更新日期
|
||||
lastUpdated: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
// 重点提示框
|
||||
highlightBox: {
|
||||
backgroundColor: '#FFF5F0',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: THEME_COLORS.primary,
|
||||
},
|
||||
highlightTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: THEME_COLORS.primary,
|
||||
marginBottom: 8,
|
||||
},
|
||||
highlightText: {
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
|
||||
// 隐私政策内容卡片
|
||||
privacyCard: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionLast: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: THEME_COLORS.primary,
|
||||
marginBottom: 12,
|
||||
},
|
||||
sectionContent: {
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
|
||||
// 底部版权
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
textAlign: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const PrivacyPolicyScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createPrivacyStyles(colors), [colors]);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isMobile } = useResponsive();
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
|
||||
|
||||
// 底部间距,避免被 TabBar 遮挡
|
||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{/* 更新日期 */}
|
||||
<Text style={styles.lastUpdated}>最后更新:{LAST_UPDATED}</Text>
|
||||
|
||||
{/* 重点提示 */}
|
||||
<View style={styles.highlightBox}>
|
||||
<Text style={styles.highlightTitle}>我们承诺保护您的隐私</Text>
|
||||
<Text style={styles.highlightText}>
|
||||
萝卜社区致力于保护用户的个人信息安全。我们仅收集提供服务所必需的信息,并采取严格的安全措施保护您的数据。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 隐私政策内容 */}
|
||||
<View style={styles.privacyCard}>
|
||||
{PRIVACY_SECTIONS.map((section, index) => (
|
||||
<View key={index} style={[styles.section, index === PRIVACY_SECTIONS.length - 1 && styles.sectionLast]}>
|
||||
<Text style={styles.sectionTitle}>{section.title}</Text>
|
||||
<Text style={styles.sectionContent}>{section.content}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部版权 */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>© 2026 青春之旅电子信息科技(威海)有限公司</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicyScreen;
|
||||
@@ -62,7 +62,6 @@ const SETTINGS_GROUPS_BASE = [
|
||||
icon: 'bell-outline',
|
||||
items: [
|
||||
{ key: 'notification_settings', title: '通知设置', icon: 'bell-cog-outline', showArrow: true, subtitle: '推送、震动、提示音' },
|
||||
{ key: 'language', title: '语言设置', icon: 'translate', showArrow: true, subtitle: '简体中文' },
|
||||
{ key: 'data_storage', title: '数据与存储', icon: 'database-outline', showArrow: true },
|
||||
],
|
||||
},
|
||||
@@ -72,8 +71,6 @@ const SETTINGS_GROUPS_BASE = [
|
||||
items: [
|
||||
{ key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` },
|
||||
{ key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true },
|
||||
{ key: 'terms', title: '用户协议', icon: 'file-document-outline', showArrow: true },
|
||||
{ key: 'privacy_policy', title: '隐私政策', icon: 'shield-outline', showArrow: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -240,31 +237,18 @@ export const SettingsScreen: React.FC = () => {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'language': {
|
||||
Alert.alert(
|
||||
'语言设置',
|
||||
'选择应用显示语言',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: '简体中文', onPress: () => {} },
|
||||
{ text: '繁體中文', onPress: () => {} },
|
||||
{ text: 'English', onPress: () => {} },
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'data_storage': {
|
||||
Alert.alert('数据与存储', '缓存清理功能即将上线!');
|
||||
break;
|
||||
}
|
||||
case 'terms': {
|
||||
Alert.alert('用户协议', '用户协议页面即将上线!');
|
||||
case 'about':
|
||||
router.push(hrefs.hrefProfileAbout());
|
||||
break;
|
||||
}
|
||||
case 'privacy_policy': {
|
||||
Alert.alert('隐私政策', '隐私政策页面即将上线!');
|
||||
case 'help':
|
||||
Alert.alert('帮助与反馈', '帮助中心即将上线!');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'edit_profile':
|
||||
router.push(hrefs.hrefProfileEdit());
|
||||
break;
|
||||
@@ -372,7 +356,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
萝卜社区 v{APP_VERSION}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.copyright}>
|
||||
© 2024 Carrot BBS. All rights reserved.
|
||||
© 2026 青春之旅电子信息科技(威海)有限公司
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
|
||||
248
src/screens/profile/TermsOfServiceScreen.tsx
Normal file
248
src/screens/profile/TermsOfServiceScreen.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 用户协议页面 TermsOfServiceScreen
|
||||
* 胡萝卜BBS - 用户服务协议
|
||||
* 扁平化设计风格
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import {
|
||||
useAppColors,
|
||||
spacing,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { Text } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 400;
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
};
|
||||
|
||||
// 协议最后更新日期
|
||||
const LAST_UPDATED = '2026年4月4日';
|
||||
|
||||
// 协议条款内容
|
||||
const TERMS_SECTIONS = [
|
||||
{
|
||||
title: '一、协议的范围',
|
||||
content: `1.1 本协议是您与萝卜社区(以下简称"我们"或"本应用")之间关于您使用本应用服务所订立的协议。
|
||||
|
||||
1.2 本协议内容包括协议正文及所有我们已经发布或将来可能发布的各类规则、公告或通知。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。
|
||||
|
||||
1.3 我们有权根据法律法规的变化及业务调整的需要,不时修改本协议及各类规则。修改后的协议将在应用内公示。如您不同意变更后的内容,应立即停止使用本服务。如您继续使用本服务,即视为您已阅读并同意受变更后的协议约束。`,
|
||||
},
|
||||
{
|
||||
title: '二、账号注册与使用',
|
||||
content: `2.1 您确认,在您开始注册程序使用本应用服务前,您应当具备中华人民共和国法律规定的与您行为相适应的民事行为能力。若您是未满18周岁的未成年人,应在监护人监护、指导下并获得监护人同意后使用本应用服务。
|
||||
|
||||
2.2 您可以使用手机号、邮箱进行注册(具体以应用实际提供的注册方式为准)。您应当提供真实、准确、完整、合法有效的注册信息,并在信息变更时及时更新。
|
||||
|
||||
2.3 您的账号仅限于您本人使用,禁止赠与、借用、租用、转让或售卖。我们有权对账号进行安全验证,如发现异常登录或使用情况,有权采取相应措施。
|
||||
|
||||
2.4 您应妥善保管账号和密码,对账号下的所有行为承担法律责任。因您保管不善导致的账号被盗或密码泄露,我们不承担责任。`,
|
||||
},
|
||||
{
|
||||
title: '三、服务内容',
|
||||
content: `3.1 本应用向您提供校园社交服务,包括但不限于:发布动态、评论互动、私信聊天、群组交流、学习资料分享等功能。
|
||||
|
||||
3.2 我们有权根据实际情况随时调整服务内容,包括但不限于增加、删除或修改功能。服务调整将在应用内进行公告。
|
||||
|
||||
3.3 我们致力于为您提供优质的服务,但不保证服务不会中断,对服务的及时性、安全性、准确性不作担保。`,
|
||||
},
|
||||
{
|
||||
title: '四、用户行为规范',
|
||||
content: `4.1 您在使用本应用服务时,必须遵守中华人民共和国相关法律法规,不得利用本应用从事违法违规活动。
|
||||
|
||||
4.2 您不得发布、传播以下内容:
|
||||
(1)反对宪法所确定的基本原则的;
|
||||
(2)危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的;
|
||||
(3)损害国家荣誉和利益的;
|
||||
(4)煽动民族仇恨、民族歧视,破坏民族团结的;
|
||||
(5)破坏国家宗教政策,宣扬邪教和封建迷信的;
|
||||
(6)散布谣言,扰乱社会秩序,破坏社会稳定的;
|
||||
(7)散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的;
|
||||
(8)侮辱或者诽谤他人,侵害他人合法权益的;
|
||||
(9)含有法律、行政法规禁止的其他内容的。
|
||||
|
||||
4.3 您不得从事以下行为:
|
||||
(1)冒充他人或使用虚假身份;
|
||||
(2)恶意注册账号,包括但不限于批量注册、使用非法手段注册;
|
||||
(3)利用技术手段批量发布内容或进行其他操作;
|
||||
(4)干扰或破坏本应用服务或服务器;
|
||||
(5)侵犯他人知识产权、隐私权等合法权益;
|
||||
(6)发布垃圾信息、广告信息或进行其他骚扰行为。
|
||||
|
||||
4.4 若您是未满18周岁的未成年人,应在监护人监护、指导下并获得监护人同意后使用本应用服务。我们非常重视对未成年人个人信息的保护,若您是未成年人的监护人,请监督您的被监护人使用本服务。
|
||||
|
||||
4.5 若您认为本应用内的内容侵犯了您的合法权益,或您发现任何违法违规内容,请通过 system@qczlit.cn 联系我们进行投诉举报,我们将在收到通知后依法尽快处理。`,
|
||||
},
|
||||
{
|
||||
title: '五、知识产权',
|
||||
content: `5.1 本应用的商标、标识、界面设计、程序代码、数据库等所有知识产权均归我们及我们的关联公司所有或已获得合法授权。
|
||||
|
||||
5.2 您在使用本应用服务过程中产生的内容(包括但不限于文字、图片、视频等),您保留对该内容的知识产权。您授予我们一项全球范围内、免费、非独家、可转授权的许可,允许我们使用、复制、修改、改编、出版、翻译、据以创作衍生作品、传播、表演和展示此等内容,以及用于本应用的运营、推广、宣传和商业用途。
|
||||
|
||||
5.3 未经我们书面许可,您不得复制、修改、传播、出售、出租本应用或其任何部分,不得对本应用进行反向工程、反编译或试图以其他方式发现其源代码。`,
|
||||
},
|
||||
{
|
||||
title: '六、隐私保护',
|
||||
content: `6.1 我们非常重视您的隐私保护,具体隐私政策请参见《隐私政策》。
|
||||
|
||||
6.2 您同意我们可以按照《隐私政策》的规定收集、使用、存储和分享您的个人信息。
|
||||
|
||||
6.3 我们将采取合理的技术和管理措施保护您的个人信息安全,但不对因不可抗力或第三方原因导致的信息泄露承担责任。`,
|
||||
},
|
||||
{
|
||||
title: '七、免责声明',
|
||||
content: `7.1 您理解并同意,本应用服务是按照现有技术和条件所能达到的现状提供的。我们会尽最大努力确保服务的连贯性和安全性,但不能随时预见和防范法律、技术以及其他风险。
|
||||
|
||||
7.2 您理解并同意,我们对以下情形不承担责任:
|
||||
(1)因不可抗力导致的服务中断或数据丢失;
|
||||
(2)因您的过错导致的任何损失;
|
||||
(3)因第三方原因导致的服务中断或信息泄露;
|
||||
(4)其他非因我们原因导致的损失。
|
||||
|
||||
7.3 您通过本应用与其他用户进行交往或交易,由此产生的任何纠纷由您自行解决并承担责任。`,
|
||||
},
|
||||
{
|
||||
title: '八、违约责任',
|
||||
content: `8.1 如果我们发现或收到他人举报您违反本协议约定的,我们有权根据情节轻重对相关内容进行删除、屏蔽,并视行为情节对违规账号处以包括但不限于警告、限制功能、暂停使用、永久封禁等处罚。因法律法规要求或紧急情况下,我们可能在不提前通知的情况下采取上述措施。
|
||||
|
||||
8.2 账号被封禁或注销后,账号内的虚拟资产(包括但不限于积分、虚拟币、会员权益等)将不予退还或清零,您不得因此向我们主张任何赔偿或补偿。
|
||||
|
||||
8.3 因您违反本协议或相关规则,导致或产生任何第三方主张的任何索赔、要求或损失,您应当独立承担责任,并赔偿我们因此遭受的损失。`,
|
||||
},
|
||||
{
|
||||
title: '九、协议的终止',
|
||||
content: `9.1 您有权随时终止使用本应用服务,可以通过注销账号的方式终止本协议。
|
||||
|
||||
9.2 我们有权在以下情况下终止本协议:
|
||||
(1)您违反本协议或相关规则;
|
||||
(2)根据法律法规或政府部门要求;
|
||||
(3)因不可抗力导致无法提供服务。
|
||||
|
||||
9.3 协议终止后,我们有权保留或删除您的账号信息及内容,且不承担任何责任。`,
|
||||
},
|
||||
{
|
||||
title: '十、其他',
|
||||
content: `10.1 本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律。
|
||||
|
||||
10.2 因本协议引起的或与本协议有关的任何争议,由协议各方协商解决,也可由有关部门调解。协商或调解不成的,应向山东省威海市环翠区人民法院提起诉讼。
|
||||
|
||||
10.3 本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,本协议的其余条款仍应有效并且有约束力。`,
|
||||
},
|
||||
];
|
||||
|
||||
function createTermsStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 28,
|
||||
paddingTop: 24,
|
||||
},
|
||||
|
||||
// 内容区域
|
||||
content: {
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
// 更新日期
|
||||
lastUpdated: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
// 协议内容卡片
|
||||
termsCard: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionLast: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: THEME_COLORS.primary,
|
||||
marginBottom: 12,
|
||||
},
|
||||
sectionContent: {
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
|
||||
// 底部版权
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 13,
|
||||
color: colors.text.hint,
|
||||
textAlign: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const TermsOfServiceScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createTermsStyles(colors), [colors]);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isMobile } = useResponsive();
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
|
||||
|
||||
// 底部间距,避免被 TabBar 遮挡
|
||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{/* 更新日期 */}
|
||||
<Text style={styles.lastUpdated}>最后更新:{LAST_UPDATED}</Text>
|
||||
|
||||
{/* 协议内容 */}
|
||||
<View style={styles.termsCard}>
|
||||
{TERMS_SECTIONS.map((section, index) => (
|
||||
<View key={index} style={[styles.section, index === TERMS_SECTIONS.length - 1 && styles.sectionLast]}>
|
||||
<Text style={styles.sectionTitle}>{section.title}</Text>
|
||||
<Text style={styles.sectionContent}>{section.content}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部版权 */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>© 2026 青春之旅电子信息科技(威海)有限公司</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfServiceScreen;
|
||||
@@ -14,6 +14,9 @@ export { default as FollowListScreen } from './FollowListScreen';
|
||||
export { NotificationSettingsScreen } from './NotificationSettingsScreen';
|
||||
export { BlockedUsersScreen } from './BlockedUsersScreen';
|
||||
export { AccountSecurityScreen } from './AccountSecurityScreen';
|
||||
export { AboutScreen } from './AboutScreen';
|
||||
export { TermsOfServiceScreen } from './TermsOfServiceScreen';
|
||||
export { PrivacyPolicyScreen } from './PrivacyPolicyScreen';
|
||||
|
||||
// 导出 Hook 供需要自定义的场景使用
|
||||
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TextInput,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||
import type { PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler';
|
||||
@@ -212,6 +213,13 @@ export const ScheduleScreen: React.FC = () => {
|
||||
const [syncPassword, setSyncPassword] = useState('');
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ((isAddModalVisible || isSettingsModalVisible || isSyncModalVisible) && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
}, [isAddModalVisible, isSettingsModalVisible, isSyncModalVisible]);
|
||||
|
||||
// 组件卸载时设置标记
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user