From c6084fe7020e43dad0236db81e92f1d1d96421e7 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 16 Oct 2025 16:43:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(nutrition):=20=E6=B7=BB=E5=8A=A0=E8=90=A5?= =?UTF-8?q?=E5=85=BB=E5=88=86=E6=9E=90=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=92=8C=E5=9B=BE=E7=89=87=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增删除营养分析记录功能,支持本地状态更新和API调用 - 添加图片全屏预览功能,支持缩放和手势操作 - 实现Liquid Glass风格的删除按钮,包含兼容性处理 - 优化历史记录页面布局和交互体验 - 更新Memory Bank文档,添加Liquid Glass按钮实现指南 --- .kilocode/rules/memory-bank/tasks.md | 86 +++++++++- .kilocode/rules/memory-bank/tech.md | 2 +- app/food/nutrition-analysis-history.tsx | 214 ++++++++++++++++++++++-- app/food/nutrition-label-analysis.tsx | 13 +- services/nutritionLabelAnalysis.ts | 13 ++ 5 files changed, 310 insertions(+), 18 deletions(-) diff --git a/.kilocode/rules/memory-bank/tasks.md b/.kilocode/rules/memory-bank/tasks.md index 3fcdeb7..be36518 100644 --- a/.kilocode/rules/memory-bank/tasks.md +++ b/.kilocode/rules/memory-bank/tasks.md @@ -61,4 +61,88 @@ const styles = StyleSheet.create({ - `app/water/detail.tsx` - `app/profile/goals.tsx` - `app/workout/history.tsx` -- `app/challenges/[id]/leaderboard.tsx` \ No newline at end of file +- `app/challenges/[id]/leaderboard.tsx` + +## Liquid Glass 风格图标按钮实现 + +**最后更新**: 2025-10-16 + +### 问题描述 +在应用中实现符合 Liquid Glass 设计风格的图标按钮,需要考虑毛玻璃效果和兼容性处理。 + +### 解决方案 +使用 `GlassView` 组件实现毛玻璃效果,并提供不支持 Liquid Glass 的设备的降级方案。 + +### 实现模式 + +#### 1. 导入必要的组件和函数 +```typescript +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +``` + +#### 2. 检查设备支持情况 +```typescript +const isGlassAvailable = isLiquidGlassAvailable(); +``` + +#### 3. 实现条件渲染的按钮 +```typescript + + {isGlassAvailable ? ( + + + + ) : ( + + + + )} + +``` + +#### 4. 定义样式 +```typescript +const styles = StyleSheet.create({ + glassButton: { + width: 36, + height: 36, + borderRadius: 18, // 圆形按钮 + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', // 保证玻璃边界圆角效果 + }, + fallbackButton: { + borderWidth: 1, + borderColor: 'rgba(244, 67, 54, 0.3)', + backgroundColor: 'rgba(244, 67, 54, 0.1)', + }, +}); +``` + +### 重要注意事项 +1. **兼容性处理**:必须使用 `isLiquidGlassAvailable()` 检查设备支持情况 +2. **overflow: 'hidden'**:GlassView 组件需要设置此属性以保证圆角效果 +3. **降级样式**:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案 +4. **交互反馈**:设置 `isInteractive={true}` 启用原生的触觉反馈 +5. **色调自定义**:通过 `tintColor` 属性自定义按钮的颜色主题 + +### 常用配置 +- **glassEffectStyle**: "clear"(透明)或 "regular"(常规) +- **tintColor**: 根据按钮功能选择合适的颜色 + - 删除操作:红色系 `rgba(244, 67, 54, 0.2)` + - 确认操作:绿色系 `rgba(76, 175, 80, 0.2)` + - 信息操作:蓝色系 `rgba(33, 150, 243, 0.2)` + +### 参考实现 +- `app/food/nutrition-analysis-history.tsx` - 删除按钮实现 +- `components/glass/button.tsx` - 通用 Glass 按钮组件 +- `app/(tabs)/_layout.tsx` - 标签栏按钮实现 \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/tech.md b/.kilocode/rules/memory-bank/tech.md index 8d229f5..485084e 100644 --- a/.kilocode/rules/memory-bank/tech.md +++ b/.kilocode/rules/memory-bank/tech.md @@ -16,7 +16,7 @@ ### UI 框架和样式 - **React Native Elements**: UI 组件库 - **Expo UI**: 0.2.0-beta.7 - Expo UI 组件 -- **Expo Glass Effect**: 0.1.4 - Liquid Glass 毛玻璃效果 +- **Expo Glass Effect**: 0.1.4 - Liquid Glass 毛玻璃效果, 优先使用 - **React Native Reanimated**: 4.1.0 - 高性能动画库 - **React Native Gesture Handler**: 2.28.0 - 手势处理 - **React Native SVG**: 15.12.1 - SVG 图形支持 diff --git a/app/food/nutrition-analysis-history.tsx b/app/food/nutrition-analysis-history.tsx index 40df38a..02cb5b8 100644 --- a/app/food/nutrition-analysis-history.tsx +++ b/app/food/nutrition-analysis-history.tsx @@ -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(''); const [error, setError] = useState(null); + const [showImagePreview, setShowImagePreview] = useState(false); + const [previewImageUri, setPreviewImageUri] = useState(null); + const [deletingId, setDeletingId] = useState(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() { {/* 头部信息 */} + {isSuccess && ( + + 识别 {item.nutritionCount} 项营养素 + + )} {dayjs(item.createdAt).format('YYYY年M月D日 HH:mm')} @@ -195,22 +267,54 @@ export default function NutritionAnalysisHistoryScreen() { - {isSuccess && ( - - 识别 {item.nutritionCount} 项营养素 - - )} + {/* 删除按钮 */} + handleDeleteRecord(item.id)} + disabled={deletingId === item.id} + activeOpacity={0.7} + > + {isGlassAvailable ? ( + + {deletingId === item.id ? ( + + ) : ( + + )} + + ) : ( + + {deletingId === item.id ? ( + + ) : ( + + )} + + )} + {/* 图片预览 */} {item.imageUrl && ( - + handleImagePreview(item.imageUrl)} + activeOpacity={0.9} + > - + {/* 预览提示图标 */} + + + + )} {/* 分析结果摘要 */} @@ -439,6 +543,33 @@ export default function NutritionAnalysisHistoryScreen() { ListFooterComponent={renderFooter} /> )} + + {/* 图片预览 */} + setShowImagePreview(false)} + swipeToCloseEnabled={true} + doubleTapToZoomEnabled={true} + HeaderComponent={() => ( + + + {dayjs().format('YYYY年M月D日 HH:mm')} + + + )} + FooterComponent={() => ( + + setShowImagePreview(false)} + > + 关闭 + + + )} + /> ); } @@ -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', + }, }); \ No newline at end of file diff --git a/app/food/nutrition-label-analysis.tsx b/app/food/nutrition-label-analysis.tsx index 2574629..adcfa77 100644 --- a/app/food/nutrition-label-analysis.tsx +++ b/app/food/nutrition-label-analysis.tsx @@ -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', diff --git a/services/nutritionLabelAnalysis.ts b/services/nutritionLabelAnalysis.ts index e0852c6..37660a3 100644 --- a/services/nutritionLabelAnalysis.ts +++ b/services/nutritionLabelAnalysis.ts @@ -229,3 +229,16 @@ export async function getNutritionAnalysisRecords(params?: GetNutritionRecordsPa }; } } + +/** + * 删除营养成分分析记录 + */ +export async function deleteNutritionAnalysisRecord(recordId: number): Promise { + try { + const response = await api.delete(`/diet-records/nutrition-analysis-records/${recordId}`); + return response; + } catch (error) { + console.error('[NUTRITION_RECORDS] 删除记录失败:', error); + throw error; + } +}