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;
+ }
+}