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

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

View File

@@ -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;

View File

@@ -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',
},

View File

@@ -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>
);

View 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;

View File

@@ -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>
</>

View 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;

View File

@@ -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';