From df2afeb5a17065cfcd43be15c6bfd49489df7f20 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 19 Aug 2025 11:03:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=90=A5=E5=85=BB?= =?UTF-8?q?=E9=9B=B7=E8=BE=BE=E5=9B=BE=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=90=A5=E5=85=BB=E5=88=86=E6=9E=90=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在营养分析卡片中引入更新日期和图标,提升信息展示 - 修改营养维度标签,简化文本内容 - 更新雷达图组件,支持自定义尺寸和标签显示 - 增加尺寸配置,优化图表显示效果 - 更新饮食记录服务,添加更新时间字段以支持新功能 --- components/NutritionRadarCard.tsx | 41 ++++++++++++--- components/RadarChart.tsx | 87 ++++++++++++++++++++++++++----- services/dietRecords.ts | 4 ++ 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index be0873d..464d3dc 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -1,4 +1,6 @@ import { NutritionSummary } from '@/services/dietRecords'; +import Feather from '@expo/vector-icons/Feather'; +import dayjs from 'dayjs'; import React, { useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { RadarCategory, RadarChart } from './RadarChart'; @@ -14,8 +16,8 @@ const NUTRITION_DIMENSIONS: RadarCategory[] = [ { key: 'protein', label: '蛋白质' }, { key: 'carbohydrate', label: '碳水' }, { key: 'fat', label: '脂肪' }, - { key: 'fiber', label: '膳食纤维' }, - { key: 'sodium', label: '钠含量' }, + { key: 'fiber', label: '纤维' }, + { key: 'sodium', label: '钠' }, ]; export function NutritionRadarCard({ nutritionSummary, isLoading = false }: NutritionRadarCardProps) { @@ -48,14 +50,21 @@ export function NutritionRadarCard({ nutritionSummary, isLoading = false }: Nutr { label: '蛋白质', value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' }, { label: '碳水', value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' }, { label: '脂肪', value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' }, - { label: '膳食纤维', value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' }, + { label: '纤维', value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' }, { label: '钠', value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' }, ]; }, [nutritionSummary]); return ( - 营养摄入分析 + + 营养摄入分析 + + 更新: {dayjs(nutritionSummary?.updatedAt).format('YYYY-MM-DD HH:mm')} + + + + {isLoading ? ( @@ -67,7 +76,7 @@ export function NutritionRadarCard({ nutritionSummary, isLoading = false }: Nutr @@ -102,11 +111,26 @@ const styles = StyleSheet.create({ shadowRadius: 3.84, elevation: 5, }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16, + }, cardTitle: { fontSize: 18, fontWeight: '800', color: '#192126', - marginBottom: 16, + }, + cardRightContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + cardSubtitle: { + fontSize: 12, + color: '#9AA3AE', + fontWeight: '600', }, contentContainer: { flexDirection: 'row', @@ -114,19 +138,20 @@ const styles = StyleSheet.create({ }, radarContainer: { alignItems: 'center', - marginRight: 20, + marginRight: 6, }, statsContainer: { flex: 1, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', + marginLeft: 4 }, statItem: { flexDirection: 'row', alignItems: 'center', width: '48%', - marginBottom: 8, + marginBottom: 16, }, statDot: { width: 8, diff --git a/components/RadarChart.tsx b/components/RadarChart.tsx index e345688..4eb435f 100644 --- a/components/RadarChart.tsx +++ b/components/RadarChart.tsx @@ -4,25 +4,75 @@ import Svg, * as SvgLib from 'react-native-svg'; export type RadarCategory = { key: string; label: string }; +export type RadarChartSize = 'small' | 'medium' | 'large' | number; + export type RadarChartProps = { categories: RadarCategory[]; values: number[]; // 与 categories 一一对应 maxValue?: number; // 默认 5 - size?: number; // 组件宽高,默认 260 + size?: RadarChartSize; // 组件宽高,默认 medium levels?: number; // 网格层数,默认 5 showGrid?: boolean; + showLabels?: boolean; // 是否显示指标文字 }; -export function RadarChart({ categories, values, maxValue = 5, size = 260, levels = 5, showGrid = true }: RadarChartProps) { - const radius = size * 0.38; - const cx = size / 2; - const cy = size / 2; +// 预设尺寸配置 +const SIZE_CONFIG = { + small: { size: 116, fontSize: 8, labelOffset: -4, radiusRatio: 0.35 }, + medium: { size: 200, fontSize: 11, labelOffset: 15, radiusRatio: 0.38 }, + large: { size: 280, fontSize: 13, labelOffset: 18, radiusRatio: 0.4 }, +}; + +export function RadarChart({ + categories, + values, + maxValue = 5, + size = 'medium', + levels = 5, + showGrid = true, + showLabels = true +}: RadarChartProps) { + // 解析尺寸配置 + const config = useMemo(() => { + if (typeof size === 'number') { + // 自定义尺寸 + const baseSize = Math.max(80, Math.min(400, size)); + let fontSize = 11; + let labelOffset = 15; + let radiusRatio = 0.38; + + // 根据尺寸自适应调整 + if (baseSize < 140) { + fontSize = 9; + labelOffset = 12; + radiusRatio = 0.35; + } else if (baseSize > 240) { + fontSize = 13; + labelOffset = 18; + radiusRatio = 0.4; + } + + return { size: baseSize, fontSize, labelOffset, radiusRatio }; + } + + // 预设尺寸 + return SIZE_CONFIG[size] || SIZE_CONFIG.medium; + }, [size]); + + const { size: actualSize, fontSize, labelOffset, radiusRatio } = config; + const radius = actualSize * radiusRatio; + const cx = actualSize / 2; + const cy = actualSize / 2; const count = categories.length; const points = useMemo(() => { return categories.map((_, i) => { const angle = (Math.PI * 2 * i) / count - Math.PI / 2; // 顶部起点 - return { angle, x: (v: number) => cx + Math.cos(angle) * v, y: (v: number) => cy + Math.sin(angle) * v }; + return { + angle, + x: (v: number) => cx + Math.cos(angle) * v, + y: (v: number) => cy + Math.sin(angle) * v + }; }); }, [categories, count, cx, cy]); @@ -44,9 +94,12 @@ export function RadarChart({ categories, values, maxValue = 5, size = 260, level return polys; }, [levels, radius, points]); + // 检查是否有足够的空间显示文字 + const hasEnoughSpace = actualSize >= 100; + return ( - - + + @@ -79,14 +132,22 @@ export function RadarChart({ categories, values, maxValue = 5, size = 260, level )} - {categories.map((c, i) => { - const r = radius + 18; + {showLabels && hasEnoughSpace && categories.map((c, i) => { + const r = radius + labelOffset; const x = points[i].x(r); const y = points[i].y(r); const anchor = Math.cos(points[i].angle) > 0.2 ? 'start' : Math.cos(points[i].angle) < -0.2 ? 'end' : 'middle'; const dy = Math.sin(points[i].angle) > 0.6 ? 10 : Math.sin(points[i].angle) < -0.6 ? -4 : 4; + return ( - + {c.label} ); @@ -99,6 +160,4 @@ export function RadarChart({ categories, values, maxValue = 5, size = 260, level ); } -const styles = StyleSheet.create({}); - - +const styles = StyleSheet.create({}); \ No newline at end of file diff --git a/services/dietRecords.ts b/services/dietRecords.ts index b0b83d8..a882252 100644 --- a/services/dietRecords.ts +++ b/services/dietRecords.ts @@ -34,6 +34,7 @@ export type NutritionSummary = { totalFiber: number; totalSugar: number; totalSodium: number; + updatedAt: string; }; export async function getDietRecords({ @@ -67,6 +68,7 @@ export function calculateNutritionSummary(records: DietRecord[]): NutritionSumma totalFiber: 0, totalSugar: 0, totalSodium: 0, + updatedAt: '', }; } @@ -79,6 +81,7 @@ export function calculateNutritionSummary(records: DietRecord[]): NutritionSumma totalFiber: summary.totalFiber + (record.fiberGrams || 0), totalSugar: summary.totalSugar + (record.sugarGrams || 0), totalSodium: summary.totalSodium + (record.sodiumMg || 0), + updatedAt: record.updatedAt, }), { totalCalories: 0, @@ -88,6 +91,7 @@ export function calculateNutritionSummary(records: DietRecord[]): NutritionSumma totalFiber: 0, totalSugar: 0, totalSodium: 0, + updatedAt: '', } ); }