feat(profile): add data storage settings screen with cache management
Add DataStorageScreen for managing media cache and storage: - Display cache statistics (image/video/audio counts and sizes) - Show AsyncStorage usage information - Add clear all cache functionality - Add cleanup expired cache option (7 days threshold) - Add storage usage breakdown by category - Add "Last cleaned" timestamp display Also fix MediaCacheManager to skip native cache operations on web platform: - Return original URI directly on web without caching - Skip directory creation on web - Skip file existence checks on web - Skip startup and periodic cleanup on web
This commit is contained in:
@@ -41,6 +41,7 @@ export default function ProfileStackLayout() {
|
|||||||
<Stack.Screen name="terms" options={{ title: '用户协议' }} />
|
<Stack.Screen name="terms" options={{ title: '用户协议' }} />
|
||||||
<Stack.Screen name="privacy" options={{ title: '隐私政策' }} />
|
<Stack.Screen name="privacy" options={{ title: '隐私政策' }} />
|
||||||
<Stack.Screen name="verification" options={{ title: '身份认证' }} />
|
<Stack.Screen name="verification" options={{ title: '身份认证' }} />
|
||||||
|
<Stack.Screen name="data-storage" options={{ title: '数据与存储' }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/(app)/(tabs)/profile/data-storage.tsx
Normal file
5
app/(app)/(tabs)/profile/data-storage.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { DataStorageScreen } from '../../../../src/screens/profile';
|
||||||
|
|
||||||
|
export default function DataStorageRoute() {
|
||||||
|
return <DataStorageScreen />;
|
||||||
|
}
|
||||||
19
src/infrastructure/cache/MediaCacheManager.ts
vendored
19
src/infrastructure/cache/MediaCacheManager.ts
vendored
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { File, Paths } from 'expo-file-system';
|
import { File, Paths } from 'expo-file-system';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import {
|
import {
|
||||||
MediaType,
|
MediaType,
|
||||||
CleanupTrigger,
|
CleanupTrigger,
|
||||||
@@ -102,13 +103,15 @@ export class MediaCacheManager {
|
|||||||
// 加载缓存记录
|
// 加载缓存记录
|
||||||
await this.loadCacheRecords();
|
await this.loadCacheRecords();
|
||||||
|
|
||||||
// 启动时清理
|
// 启动时清理 (非web平台)
|
||||||
if (this.policy.cleanupOnStart) {
|
if (this.policy.cleanupOnStart && Platform.OS !== 'web') {
|
||||||
await this.cleanup(CleanupTrigger.STARTUP);
|
await this.cleanup(CleanupTrigger.STARTUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动定期清理
|
// 启动定期清理 (非web平台)
|
||||||
this.startPeriodicCleanup();
|
if (Platform.OS !== 'web') {
|
||||||
|
this.startPeriodicCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -121,6 +124,8 @@ export class MediaCacheManager {
|
|||||||
* 确保缓存目录存在
|
* 确保缓存目录存在
|
||||||
*/
|
*/
|
||||||
private async ensureDirectories(): Promise<void> {
|
private async ensureDirectories(): Promise<void> {
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dirs = [getImageDir(), getVideoDir(), getAudioDir()];
|
const dirs = [getImageDir(), getVideoDir(), getAudioDir()];
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
@@ -168,6 +173,8 @@ export class MediaCacheManager {
|
|||||||
* 检查文件是否存在
|
* 检查文件是否存在
|
||||||
*/
|
*/
|
||||||
private async checkFileExists(path: string): Promise<boolean> {
|
private async checkFileExists(path: string): Promise<boolean> {
|
||||||
|
if (Platform.OS === 'web') return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = new File(path);
|
const file = new File(path);
|
||||||
return await file.exists;
|
return await file.exists;
|
||||||
@@ -199,6 +206,10 @@ export class MediaCacheManager {
|
|||||||
size?: number;
|
size?: number;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
const { conversationId, messageId, size = 0 } = options;
|
const { conversationId, messageId, size = 0 } = options;
|
||||||
|
|
||||||
// 生成唯一键
|
// 生成唯一键
|
||||||
|
|||||||
@@ -206,3 +206,7 @@ export function hrefVerificationForm(identity?: string): string {
|
|||||||
}
|
}
|
||||||
return '/verification-form';
|
return '/verification-form';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hrefProfileDataStorage(): string {
|
||||||
|
return '/profile/data-storage';
|
||||||
|
}
|
||||||
|
|||||||
466
src/screens/profile/DataStorageScreen.tsx
Normal file
466
src/screens/profile/DataStorageScreen.tsx
Normal file
@@ -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<CacheStats | null>(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 (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
|
<Text variant="body" color={colors.text.secondary} style={styles.loadingText}>
|
||||||
|
正在计算存储空间...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 存储总览 */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text variant="caption" style={styles.sectionTitle}>
|
||||||
|
存储使用
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.totalCard}>
|
||||||
|
<View style={styles.totalCardTop}>
|
||||||
|
<MaterialCommunityIcons name="database" size={28} color={colors.primary.main} />
|
||||||
|
<View style={styles.totalCardInfo}>
|
||||||
|
<Text variant="h3" color={colors.text.primary} style={styles.totalSize}>
|
||||||
|
{formatBytes(totalSize)}
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.secondary}>
|
||||||
|
总缓存大小
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.progressBarBg}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.progressBarFill,
|
||||||
|
{
|
||||||
|
width: `${Math.min((totalSize / (500 * 1024 * 1024)) * 100, 100)}%`,
|
||||||
|
backgroundColor:
|
||||||
|
totalSize > 400 * 1024 * 1024
|
||||||
|
? colors.error.main
|
||||||
|
: totalSize > 250 * 1024 * 1024
|
||||||
|
? colors.warning.main
|
||||||
|
: colors.primary.main,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text variant="caption" color={colors.text.hint}>
|
||||||
|
上限 500 MB
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 缓存明细 */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text variant="caption" style={styles.sectionTitle}>
|
||||||
|
缓存明细
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{storageItems.map((item) => (
|
||||||
|
<View key={item.icon} style={styles.statItem}>
|
||||||
|
<View style={styles.statItemLeft}>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={item.icon as any}
|
||||||
|
size={22}
|
||||||
|
color={colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text variant="body" color={colors.text.primary}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text variant="body" color={colors.text.secondary}>
|
||||||
|
{item.value}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 缓存管理 */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text variant="caption" style={styles.sectionTitle}>
|
||||||
|
缓存管理
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.actionItem}
|
||||||
|
onPress={handleCleanupExpired}
|
||||||
|
disabled={clearing}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.actionItemLeft}>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="broom"
|
||||||
|
size={22}
|
||||||
|
color={colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text variant="body" color={colors.text.primary}>
|
||||||
|
清理过期缓存
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
|
||||||
|
清除超过 7 天未访问的缓存
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionItem, styles.actionItemLast]}
|
||||||
|
onPress={handleClearCache}
|
||||||
|
disabled={clearing}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.actionItemLeft}>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="delete-sweep-outline"
|
||||||
|
size={22}
|
||||||
|
color={colors.error.main}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text variant="body" color={colors.error.main}>
|
||||||
|
清除所有缓存
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
|
||||||
|
删除所有已缓存的媒体文件
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{clearing ? (
|
||||||
|
<ActivityIndicator size="small" color={colors.error.main} />
|
||||||
|
) : (
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 缓存策略说明 */}
|
||||||
|
<View style={styles.tipContainer}>
|
||||||
|
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
|
||||||
|
<Text variant="caption" color={colors.text.hint} style={styles.tipText}>
|
||||||
|
应用会自动清理过期缓存(7 天未访问)并在缓存超过 500 MB 时启用 LRU 策略自动回收空间。清除缓存不会影响账号数据与聊天记录。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.scrollContent,
|
||||||
|
{ paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -239,10 +239,9 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'data_storage': {
|
case 'data_storage':
|
||||||
Alert.alert('数据与存储', '缓存清理功能即将上线!');
|
router.push(hrefs.hrefProfileDataStorage());
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case 'about':
|
case 'about':
|
||||||
router.push(hrefs.hrefProfileAbout());
|
router.push(hrefs.hrefProfileAbout());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export { AboutScreen } from './AboutScreen';
|
|||||||
export { TermsOfServiceScreen } from './TermsOfServiceScreen';
|
export { TermsOfServiceScreen } from './TermsOfServiceScreen';
|
||||||
export { PrivacyPolicyScreen } from './PrivacyPolicyScreen';
|
export { PrivacyPolicyScreen } from './PrivacyPolicyScreen';
|
||||||
export { VerificationSettingsScreen } from './VerificationSettingsScreen';
|
export { VerificationSettingsScreen } from './VerificationSettingsScreen';
|
||||||
|
export { DataStorageScreen } from './DataStorageScreen';
|
||||||
|
|
||||||
// 导出 Hook 供需要自定义的场景使用
|
// 导出 Hook 供需要自定义的场景使用
|
||||||
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
|
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
|
||||||
|
|||||||
Reference in New Issue
Block a user