feat(nutrition): 添加营养分析历史记录删除和图片预览功能

- 新增删除营养分析记录功能,支持本地状态更新和API调用
- 添加图片全屏预览功能,支持缩放和手势操作
- 实现Liquid Glass风格的删除按钮,包含兼容性处理
- 优化历史记录页面布局和交互体验
- 更新Memory Bank文档,添加Liquid Glass按钮实现指南
This commit is contained in:
richarjiang
2025-10-16 16:43:45 +08:00
parent e4ddd21305
commit c6084fe702
5 changed files with 310 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
deleteNutritionAnalysisRecord,
getNutritionAnalysisRecords,
type GetNutritionRecordsParams,
type NutritionAnalysisRecord,
@@ -10,6 +11,7 @@ import {
import { triggerLightHaptic } from '@/utils/haptics';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
@@ -17,6 +19,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
BackHandler,
FlatList,
RefreshControl,
StyleSheet,
@@ -24,6 +27,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import ImageViewing from 'react-native-image-viewing';
export default function NutritionAnalysisHistoryScreen() {
const safeAreaTop = useSafeAreaTop();
@@ -39,6 +43,23 @@ export default function NutritionAnalysisHistoryScreen() {
const [total, setTotal] = useState(0);
const [statusFilter, setStatusFilter] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [showImagePreview, setShowImagePreview] = useState(false);
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<number | null>(null);
const isGlassAvailable = isLiquidGlassAvailable();
// 处理Android返回键关闭图片预览
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
if (showImagePreview) {
setShowImagePreview(false);
return true; // 阻止默认返回行为
}
return false;
});
return () => backHandler.remove();
}, [showImagePreview]);
// 获取历史记录
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false, currentStatusFilter?: string) => {
@@ -177,6 +198,52 @@ export default function NutritionAnalysisHistoryScreen() {
};
};
// 处理图片预览
const handleImagePreview = useCallback((imageUrl: string) => {
triggerLightHaptic();
setPreviewImageUri(imageUrl);
setShowImagePreview(true);
}, []);
// 处理删除记录
const handleDeleteRecord = useCallback((recordId: number) => {
Alert.alert(
'确认删除',
'确定要删除这条营养分析记录吗?此操作无法撤销。',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: async () => {
try {
setDeletingId(recordId);
await deleteNutritionAnalysisRecord(recordId);
// 从本地状态中移除删除的记录
setRecords(prev => prev.filter(record => record.id !== recordId));
setTotal(prev => Math.max(0, prev - 1));
// 触发轻微震动反馈
triggerLightHaptic();
// 显示成功提示
Alert.alert('成功', '记录已删除');
} catch (error) {
console.error('[HISTORY] 删除记录失败:', error);
Alert.alert('错误', '删除失败,请稍后重试');
} finally {
setDeletingId(null);
}
},
},
]
);
}, []);
// 渲染历史记录项
const renderRecordItem = useCallback(({ item }: { item: NutritionAnalysisRecord }) => {
const isExpanded = expandedItems.has(item.id);
@@ -187,6 +254,11 @@ export default function NutritionAnalysisHistoryScreen() {
{/* 头部信息 */}
<View style={styles.recordHeader}>
<View style={styles.recordInfo}>
{isSuccess && (
<Text style={styles.recordTitle}>
{item.nutritionCount}
</Text>
)}
<Text style={styles.recordDate}>
{dayjs(item.createdAt).format('YYYY年M月D日 HH:mm')}
</Text>
@@ -195,22 +267,54 @@ export default function NutritionAnalysisHistoryScreen() {
</View>
</View>
{isSuccess && (
<Text style={styles.nutritionCount}>
{item.nutritionCount}
</Text>
)}
{/* 删除按钮 */}
<TouchableOpacity
onPress={() => handleDeleteRecord(item.id)}
disabled={deletingId === item.id}
activeOpacity={0.7}
>
{isGlassAvailable ? (
<GlassView
style={styles.glassDeleteButton}
glassEffectStyle="clear"
tintColor="rgba(244, 67, 54, 0.2)"
isInteractive={true}
>
{deletingId === item.id ? (
<ActivityIndicator size="small" color="#F44336" />
) : (
<Ionicons name="trash-outline" size={20} color="#F44336" />
)}
</GlassView>
) : (
<View style={[styles.glassDeleteButton, styles.fallbackDeleteButton]}>
{deletingId === item.id ? (
<ActivityIndicator size="small" color="#F44336" />
) : (
<Ionicons name="trash-outline" size={20} color="#F44336" />
)}
</View>
)}
</TouchableOpacity>
</View>
{/* 图片预览 */}
{item.imageUrl && (
<View style={styles.imageContainer}>
<TouchableOpacity
style={styles.imageContainer}
onPress={() => handleImagePreview(item.imageUrl)}
activeOpacity={0.9}
>
<Image
source={{ uri: item.imageUrl }}
style={styles.thumbnail}
contentFit="cover"
/>
</View>
{/* 预览提示图标 */}
<View style={styles.previewHint}>
<Ionicons name="expand-outline" size={16} color="#FFF" />
</View>
</TouchableOpacity>
)}
{/* 分析结果摘要 */}
@@ -439,6 +543,33 @@ export default function NutritionAnalysisHistoryScreen() {
ListFooterComponent={renderFooter}
/>
)}
{/* 图片预览 */}
<ImageViewing
images={previewImageUri ? [{ uri: previewImageUri }] : []}
imageIndex={0}
visible={showImagePreview}
onRequestClose={() => setShowImagePreview(false)}
swipeToCloseEnabled={true}
doubleTapToZoomEnabled={true}
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{dayjs().format('YYYY年M月D日 HH:mm')}
</Text>
</View>
)}
FooterComponent={() => (
<View style={styles.imageViewerFooter}>
<TouchableOpacity
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
</TouchableOpacity>
</View>
)}
/>
</View>
);
}
@@ -508,12 +639,17 @@ const styles = StyleSheet.create({
recordInfo: {
flex: 1,
},
recordDate: {
recordTitle: {
fontSize: 16,
fontWeight: '600',
color: Colors.light.text,
marginBottom: 4,
},
recordDate: {
fontSize: 14,
color: Colors.light.textSecondary,
marginBottom: 4,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
@@ -525,19 +661,36 @@ const styles = StyleSheet.create({
fontWeight: '500',
color: '#FFF',
},
nutritionCount: {
fontSize: 14,
color: Colors.light.textSecondary,
fontWeight: '500',
glassDeleteButton: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
fallbackDeleteButton: {
borderWidth: 1,
borderColor: 'rgba(244, 67, 54, 0.3)',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
},
imageContainer: {
marginBottom: 12,
position: 'relative',
},
thumbnail: {
width: '100%',
height: 120,
borderRadius: 12,
},
previewHint: {
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 16,
padding: 6,
},
summaryContainer: {
marginBottom: 12,
},
@@ -709,4 +862,41 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '600',
},
// ImageViewing 组件样式
imageViewerHeader: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
zIndex: 1,
},
imageViewerHeaderText: {
color: '#FFF',
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
imageViewerFooter: {
position: 'absolute',
bottom: 60,
left: 20,
right: 20,
alignItems: 'center',
zIndex: 1,
},
imageViewerFooterButton: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 20,
},
imageViewerFooterButtonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '500',
},
});

View File

@@ -451,13 +451,18 @@ const styles = StyleSheet.create({
placeholderContainer: {
width: '100%',
height: '100%',
backgroundColor: '#F8F9FA',
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
borderWidth: 2,
borderColor: '#E9ECEF',
borderStyle: 'dashed',
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 5,
},
placeholderContent: {
alignItems: 'center',