diff --git a/app/(app)/(tabs)/profile/_layout.tsx b/app/(app)/(tabs)/profile/_layout.tsx
index 3d62485..f5ac872 100644
--- a/app/(app)/(tabs)/profile/_layout.tsx
+++ b/app/(app)/(tabs)/profile/_layout.tsx
@@ -41,6 +41,7 @@ export default function ProfileStackLayout() {
+
);
}
diff --git a/app/(app)/(tabs)/profile/data-storage.tsx b/app/(app)/(tabs)/profile/data-storage.tsx
new file mode 100644
index 0000000..b8475ae
--- /dev/null
+++ b/app/(app)/(tabs)/profile/data-storage.tsx
@@ -0,0 +1,5 @@
+import { DataStorageScreen } from '../../../../src/screens/profile';
+
+export default function DataStorageRoute() {
+ return ;
+}
diff --git a/src/infrastructure/cache/MediaCacheManager.ts b/src/infrastructure/cache/MediaCacheManager.ts
index 856da55..96c3a9e 100644
--- a/src/infrastructure/cache/MediaCacheManager.ts
+++ b/src/infrastructure/cache/MediaCacheManager.ts
@@ -6,6 +6,7 @@
import { File, Paths } from 'expo-file-system';
import AsyncStorage from '@react-native-async-storage/async-storage';
+import { Platform } from 'react-native';
import {
MediaType,
CleanupTrigger,
@@ -102,13 +103,15 @@ export class MediaCacheManager {
// 加载缓存记录
await this.loadCacheRecords();
- // 启动时清理
- if (this.policy.cleanupOnStart) {
+ // 启动时清理 (非web平台)
+ if (this.policy.cleanupOnStart && Platform.OS !== 'web') {
await this.cleanup(CleanupTrigger.STARTUP);
}
- // 启动定期清理
- this.startPeriodicCleanup();
+ // 启动定期清理 (非web平台)
+ if (Platform.OS !== 'web') {
+ this.startPeriodicCleanup();
+ }
this.isInitialized = true;
} catch (error) {
@@ -121,6 +124,8 @@ export class MediaCacheManager {
* 确保缓存目录存在
*/
private async ensureDirectories(): Promise {
+ if (Platform.OS === 'web') return;
+
try {
const dirs = [getImageDir(), getVideoDir(), getAudioDir()];
for (const dir of dirs) {
@@ -168,6 +173,8 @@ export class MediaCacheManager {
* 检查文件是否存在
*/
private async checkFileExists(path: string): Promise {
+ if (Platform.OS === 'web') return false;
+
try {
const file = new File(path);
return await file.exists;
@@ -199,6 +206,10 @@ export class MediaCacheManager {
size?: number;
} = {}
): Promise {
+ if (Platform.OS === 'web') {
+ return uri;
+ }
+
const { conversationId, messageId, size = 0 } = options;
// 生成唯一键
diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts
index c12009e..1f62a04 100644
--- a/src/navigation/hrefs.ts
+++ b/src/navigation/hrefs.ts
@@ -206,3 +206,7 @@ export function hrefVerificationForm(identity?: string): string {
}
return '/verification-form';
}
+
+export function hrefProfileDataStorage(): string {
+ return '/profile/data-storage';
+}
diff --git a/src/screens/profile/DataStorageScreen.tsx b/src/screens/profile/DataStorageScreen.tsx
new file mode 100644
index 0000000..d8f95a8
--- /dev/null
+++ b/src/screens/profile/DataStorageScreen.tsx
@@ -0,0 +1,466 @@
+/**
+ * 数据与存储设置页 DataStorageScreen(响应式适配)
+ * 胡萝卜BBS - 缓存管理、存储统计
+ */
+
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ ActivityIndicator,
+} from 'react-native';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
+import { Text } from '../../components/common';
+import { useResponsive, useResponsiveSpacing } from '../../hooks';
+import { mediaCacheManager } from '../../infrastructure/cache';
+import type { CacheStats } from '../../infrastructure/cache';
+
+const CONTENT_MAX_WIDTH = 720;
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+}
+
+interface StorageStatItem {
+ icon: string;
+ label: string;
+ value: string;
+ count?: number;
+}
+
+export const DataStorageScreen: React.FC = () => {
+ const colors = useAppColors();
+ const styles = useMemo(() => createDataStorageStyles(colors), [colors]);
+ const { isMobile } = useResponsive();
+ const insets = useSafeAreaInsets();
+ const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
+
+ const [cacheStats, setCacheStats] = useState(null);
+ const [asyncStorageSize, setAsyncStorageSize] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [clearing, setClearing] = useState(false);
+
+ const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
+
+ const loadStats = useCallback(async () => {
+ try {
+ setLoading(true);
+ await mediaCacheManager.initialize();
+ const stats = mediaCacheManager.getCacheStats();
+ setCacheStats(stats);
+
+ const keys = await AsyncStorage.getAllKeys();
+ if (keys.length > 0) {
+ const items = await AsyncStorage.multiGet(keys);
+ let totalSize = 0;
+ for (const [, value] of items) {
+ if (value) {
+ totalSize += value.length * 2;
+ }
+ }
+ setAsyncStorageSize(totalSize);
+ } else {
+ setAsyncStorageSize(0);
+ }
+ } catch (error) {
+ console.error('加载存储统计失败:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadStats();
+ }, [loadStats]);
+
+ const handleClearCache = () => {
+ Alert.alert(
+ '清除缓存',
+ '确定要清除所有媒体缓存吗?此操作不可撤销。',
+ [
+ { text: '取消', style: 'cancel' },
+ {
+ text: '清除',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ setClearing(true);
+ await mediaCacheManager.clearAll();
+ await loadStats();
+ Alert.alert('完成', '缓存已清除');
+ } catch (error) {
+ console.error('清除缓存失败:', error);
+ Alert.alert('错误', '清除缓存失败,请稍后重试');
+ } finally {
+ setClearing(false);
+ }
+ },
+ },
+ ]
+ );
+ };
+
+ const handleCleanupExpired = () => {
+ Alert.alert(
+ '清理过期缓存',
+ '将清理超过 7 天未访问的缓存文件。',
+ [
+ { text: '取消', style: 'cancel' },
+ {
+ text: '清理',
+ onPress: async () => {
+ try {
+ setClearing(true);
+ const result = await mediaCacheManager.cleanupExpired();
+ await loadStats();
+ Alert.alert(
+ '清理完成',
+ `已清理 ${result.deletedCount} 个文件,释放 ${formatBytes(result.freedSize)} 空间`
+ );
+ } catch (error) {
+ console.error('清理过期缓存失败:', error);
+ Alert.alert('错误', '清理失败,请稍后重试');
+ } finally {
+ setClearing(false);
+ }
+ },
+ },
+ ]
+ );
+ };
+
+ const totalSize = (cacheStats?.totalSize ?? 0) + asyncStorageSize;
+
+ const storageItems: StorageStatItem[] = useMemo(() => {
+ if (!cacheStats) return [];
+ return [
+ {
+ icon: 'image-outline',
+ label: '图片缓存',
+ value: cacheStats.imageCount > 0 ? `${cacheStats.imageCount} 个文件` : '暂无',
+ count: cacheStats.imageCount,
+ },
+ {
+ icon: 'video-outline',
+ label: '视频缓存',
+ value: cacheStats.videoCount > 0 ? `${cacheStats.videoCount} 个文件` : '暂无',
+ count: cacheStats.videoCount,
+ },
+ {
+ icon: 'music-note-outline',
+ label: '音频缓存',
+ value: cacheStats.audioCount > 0 ? `${cacheStats.audioCount} 个文件` : '暂无',
+ count: cacheStats.audioCount,
+ },
+ {
+ icon: 'file-document-outline',
+ label: '本地数据',
+ value: asyncStorageSize > 0 ? formatBytes(asyncStorageSize) : '暂无',
+ },
+ ];
+ }, [cacheStats, asyncStorageSize]);
+
+ const renderContent = () => {
+ if (loading) {
+ return (
+
+
+
+ 正在计算存储空间...
+
+
+ );
+ }
+
+ return (
+ <>
+ {/* 存储总览 */}
+
+
+
+ 存储使用
+
+
+
+
+
+
+
+ {formatBytes(totalSize)}
+
+
+ 总缓存大小
+
+
+
+
+ 400 * 1024 * 1024
+ ? colors.error.main
+ : totalSize > 250 * 1024 * 1024
+ ? colors.warning.main
+ : colors.primary.main,
+ },
+ ]}
+ />
+
+
+ 上限 500 MB
+
+
+
+
+ {/* 缓存明细 */}
+
+
+
+ 缓存明细
+
+
+ {storageItems.map((item) => (
+
+
+
+
+
+
+ {item.label}
+
+
+
+ {item.value}
+
+
+ ))}
+
+
+ {/* 缓存管理 */}
+
+
+
+ 缓存管理
+
+
+
+
+
+
+
+
+
+ 清理过期缓存
+
+
+ 清除超过 7 天未访问的缓存
+
+
+
+
+
+
+
+
+
+
+
+
+ 清除所有缓存
+
+
+ 删除所有已缓存的媒体文件
+
+
+
+ {clearing ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* 缓存策略说明 */}
+
+
+
+ 应用会自动清理过期缓存(7 天未访问)并在缓存超过 500 MB 时启用 LRU 策略自动回收空间。清除缓存不会影响账号数据与聊天记录。
+
+
+ >
+ );
+ };
+
+ return (
+
+
+ {renderContent()}
+
+
+ );
+};
+
+function createDataStorageStyles(colors: AppColors) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background.paper,
+ },
+ scrollContent: {
+ paddingVertical: spacing.lg,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: spacing['3xl'],
+ },
+ loadingText: {
+ marginTop: spacing.md,
+ },
+ section: {
+ marginBottom: spacing.xl,
+ maxWidth: CONTENT_MAX_WIDTH,
+ alignSelf: 'center',
+ width: '100%',
+ },
+ sectionHeader: {
+ paddingHorizontal: spacing['2xl'],
+ marginBottom: spacing.sm,
+ marginTop: spacing.sm,
+ },
+ sectionTitle: {
+ fontWeight: '600',
+ fontSize: fontSizes.sm,
+ color: colors.text.secondary,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ totalCard: {
+ marginHorizontal: spacing.lg,
+ padding: spacing.xl,
+ borderRadius: borderRadius.lg,
+ backgroundColor: colors.background.default,
+ },
+ totalCardTop: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ },
+ totalCardInfo: {
+ marginLeft: spacing.lg,
+ },
+ totalSize: {
+ marginBottom: 2,
+ },
+ progressBarBg: {
+ height: 6,
+ borderRadius: 3,
+ backgroundColor: colors.divider,
+ marginBottom: spacing.sm,
+ overflow: 'hidden',
+ },
+ progressBarFill: {
+ height: '100%',
+ borderRadius: 3,
+ },
+ statItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 14,
+ paddingHorizontal: spacing['2xl'],
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.divider,
+ },
+ statItemLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ iconContainer: {
+ width: 24,
+ height: 24,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: spacing.lg,
+ },
+ actionItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 16,
+ paddingHorizontal: spacing['2xl'],
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.divider,
+ },
+ actionItemLast: {
+ borderBottomWidth: 0,
+ },
+ actionItemLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ subtitle: {
+ marginTop: 2,
+ },
+ tipContainer: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginHorizontal: spacing['2xl'],
+ padding: spacing.md,
+ backgroundColor: colors.background.default,
+ borderRadius: 14,
+ gap: spacing.sm,
+ maxWidth: CONTENT_MAX_WIDTH,
+ alignSelf: 'center',
+ width: '100%',
+ },
+ tipText: {
+ flex: 1,
+ lineHeight: 20,
+ },
+ });
+}
+
+export default DataStorageScreen;
diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx
index ba65fb9..789940e 100644
--- a/src/screens/profile/SettingsScreen.tsx
+++ b/src/screens/profile/SettingsScreen.tsx
@@ -239,10 +239,9 @@ export const SettingsScreen: React.FC = () => {
break;
}
- case 'data_storage': {
- Alert.alert('数据与存储', '缓存清理功能即将上线!');
+ case 'data_storage':
+ router.push(hrefs.hrefProfileDataStorage());
break;
- }
case 'about':
router.push(hrefs.hrefProfileAbout());
break;
diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts
index 7784813..2f622a3 100644
--- a/src/screens/profile/index.ts
+++ b/src/screens/profile/index.ts
@@ -18,6 +18,7 @@ export { AboutScreen } from './AboutScreen';
export { TermsOfServiceScreen } from './TermsOfServiceScreen';
export { PrivacyPolicyScreen } from './PrivacyPolicyScreen';
export { VerificationSettingsScreen } from './VerificationSettingsScreen';
+export { DataStorageScreen } from './DataStorageScreen';
// 导出 Hook 供需要自定义的场景使用
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';