欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

atomgit仓库地址:https://gitcode.com/feng8403000/qingyingbanlv_zhinengjianfeizhushou
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目概述与设计理念

1.1 应用背景

随着现代人生活节奏的加快,肥胖问题已经成为困扰全球的健康难题。根据世界卫生组织的统计数据显示,全球有超过19亿成年人处于超重状态,其中约6.5亿人被诊断为肥胖。在中国,这一数字同样令人担忧——超过一半的成年人存在不同程度的体重问题。

面对这一现状,市面上涌现出了大量的减肥类应用。然而,大多数应用要么功能过于简单,只能记录体重变化;要么过于复杂,需要用户投入大量时间学习使用方法。更重要的是,很多应用缺乏科学的数据支撑,无法为用户提供真正有效的减肥指导。

轻盈伴侣的设计初衷,就是要在功能丰富性和使用便捷性之间找到一个平衡点。我们希望打造一款既能满足减肥用户的核心需求,又不会给用户带来额外负担的智能应用。

1.2 技术架构选型

本项目基于鸿蒙系统的Electron框架进行开发,采用Web技术栈构建用户界面。这种技术选型有以下几个方面的考量:

技术方案 优势 适用场景
鸿蒙Electron 原生性能优秀,系统集成度高 需要与鸿蒙系统深度交互的应用
Web前端技术 开发效率高,跨平台能力强 快速迭代,界面复杂的应用
Canvas绑制 图表渲染性能好,自定义能力强 需要动态图表展示的场景
LocalStorage 数据持久化简单,无需后端支持 单机应用,数据量较小的场景

整个应用采用三层架构设计:

┌─────────────────────────────────────────┐
│              用户界面层                   │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │体重追踪 │ │热量管理 │ │运动计划 │   │
│  └─────────┘ └─────────┘ └─────────┘   │
├─────────────────────────────────────────┤
│              业务逻辑层                   │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │数据计算 │ │状态管理 │ │AI建议   │   │
│  └─────────┘ └─────────┘ └─────────┘   │
├─────────────────────────────────────────┤
│              数据存储层                   │
│  ┌─────────────────────────────────┐   │
│  │       LocalStorage / 内存        │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

1.3 功能模块划分

轻盈伴侣将减肥过程中的核心需求归纳为四大功能模块:

体重追踪模块:这是减肥过程中最直观的数据指标。用户可以每日记录体重,系统会自动生成体重变化曲线,计算BMI值,并追踪减肥进度。

热量管理模块:热量摄入与消耗的平衡是减肥成功的关键。本模块提供了丰富的食物数据库,用户可以快速记录每日饮食,系统会实时计算热量摄入情况。

运动计划模块:运动是减肥的重要组成部分。我们提供了运动计时器、消耗计算、周统计等功能,帮助用户科学规划运动计划。

打卡激励模块:减肥是一个长期过程,需要持续的动力支持。打卡功能和连续天数统计,能够有效激励用户坚持下去。


二、核心代码实现详解

2.1 食物数据库与热量计算系统

食物热量数据是减肥应用的核心数据基础。我们构建了一个包含40多种常见食物的热量数据库,涵盖了早餐、午餐、晚餐和零食四大类别。

const foodDatabase = {
    breakfast: {
        '豆浆': { calorie: 150, protein: 10, carb: 15, fat: 5, serving: '1杯(300ml)' },
        '包子': { calorie: 200, protein: 8, carb: 30, fat: 6, serving: '1个' },
        '油条': { calorie: 180, protein: 4, carb: 25, fat: 8, serving: '1根' },
        '鸡蛋': { calorie: 80, protein: 7, carb: 1, fat: 5, serving: '1个' },
        '粥': { calorie: 100, protein: 3, carb: 20, fat: 1, serving: '1碗' }
    },
    lunch: {
        '米饭': { calorie: 200, protein: 4, carb: 45, fat: 1, serving: '1碗' },
        '面条': { calorie: 280, protein: 10, carb: 50, fat: 6, serving: '1碗' },
        '红烧肉': { calorie: 350, protein: 15, carb: 10, fat: 28, serving: '100g' }
    }
};

这段代码的设计有几个值得注意的细节:

首先,我们采用嵌套对象的结构来组织数据。外层以餐别作为分类,内层以食物名称作为键名。这种结构既方便数据的维护和扩展,又能够快速通过名称定位到具体食物。

其次,每个食物条目不仅包含热量值,还记录了蛋白质、碳水化合物、脂肪三大营养素的含量。这些数据为后续的营养分析提供了基础支撑。

第三,serving字段记录了标准份量,这对于用户理解热量数据的含义非常重要。比如"豆浆150kcal"如果不知道是300ml的份量,用户就无法准确估算自己喝了多少热量。

食物添加的核心逻辑如下:

function confirmAddFood() {
    if (!appState.selectedFood) return;
    
    const amount = appState.foodAmount;
    const food = appState.selectedFood;
    const totalCalorie = Math.round(food.calorie * amount);
    
    const entry = {
        name: food.name,
        calorie: totalCalorie,
        protein: Math.round(food.protein * amount * 10) / 10,
        carb: Math.round(food.carb * amount * 10) / 10,
        fat: Math.round(food.fat * amount * 10) / 10,
        time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
    };
    
    appState.calorieLog.push(entry);
    appState.calorieConsumed += totalCalorie;
    
    updateCalorieUI();
    renderCalorieLog();
}

这里我们引入了appState这个全局状态对象来管理应用数据。每当用户添加一种食物,系统会根据用户指定的份量计算实际热量,并更新总摄入量。同时,这条记录会被添加到日志数组中,用于后续的展示和删除操作。

2.2 运动消耗计算与计时器实现

运动消耗的计算需要考虑运动类型、运动时长以及用户体重等多个因素。我们采用了每小时消耗热量作为基准数据:

const exerciseDatabase = {
    '慢跑': { caloriePerHour: 400, icon: '🏃', desc: '户外慢跑' },
    '快走': { caloriePerHour: 280, icon: '🚶', desc: '快步走' },
    '游泳': { caloriePerHour: 500, icon: '🏊', desc: '游泳运动' },
    '骑行': { caloriePerHour: 350, icon: '🚴', desc: '自行车骑行' },
    '跳绳': { caloriePerHour: 450, icon: '🪢', desc: '跳绳运动' },
    '瑜伽': { caloriePerHour: 150, icon: '🧘', desc: '瑜伽练习' }
};

运动计时器是本应用的一个特色功能。用户可以选择运动类型,设置目标时长,然后开始计时。计时结束后,系统会自动计算消耗的热量:

function startTimer() {
    if (appState.timer.isRunning) return;
    
    if (!appState.selectedExercise) {
        alert('请先选择一种运动类型');
        return;
    }
    
    appState.timer.isRunning = true;
    appState.timer.isPaused = false;
    appState.timer.currentExercise = appState.selectedExercise;
    
    appState.timer.interval = setInterval(() => {
        if (!appState.timer.isPaused) {
            appState.timer.seconds--;
            updateTimerDisplay();
            
            if (appState.timer.seconds <= 0) {
                stopTimer();
            }
        }
    }, 1000);
}

function stopTimer() {
    appState.timer.isRunning = false;
    
    if (appState.timer.interval) {
        clearInterval(appState.timer.interval);
    }
    
    const minutes = Math.floor(appState.timer.presetMinutes - appState.timer.seconds / 60);
    
    if (minutes > 0 && appState.timer.currentExercise) {
        addExerciseBurned(appState.timer.currentExercise, minutes);
    }
}

计时器的实现采用了setInterval定时器,每秒更新一次显示。这里有一个细节需要注意:我们使用了isPaused标志来支持暂停功能,而不是直接清除定时器。这样做的好处是暂停后可以无缝继续计时,用户体验更加流畅。

运动消耗的计算公式相对简单:

消耗热量 = 每小时消耗 60 × 运动分钟数 消耗热量 = \frac{每小时消耗}{60} \times 运动分钟数 消耗热量=60每小时消耗×运动分钟数

这个公式假设用户以标准强度进行运动。在实际应用中,如果要更精确地计算,还需要考虑用户的体重因素:

消耗热量 = 每小时消耗 60 × 运动分钟数 × 用户体重 标准体重 消耗热量 = \frac{每小时消耗}{60} \times 运动分钟数 \times \frac{用户体重}{标准体重} 消耗热量=60每小时消耗×运动分钟数×标准体重用户体重

2.3 体重曲线图表绑制

体重变化的可视化是激励用户的重要手段。我们使用Canvas来绑制体重曲线图表:

function drawWeightChart() {
    if (!chartCtx) return;
    
    const canvas = document.getElementById('weightChart');
    const ctx = chartCtx;
    const width = canvas.width;
    const height = canvas.height;
    
    ctx.clearRect(0, 0, width, height);
    
    const records = appState.weightRecords;
    if (records.length === 0) {
        ctx.fillStyle = '#999';
        ctx.font = '12px Arial';
        ctx.textAlign = 'center';
        ctx.fillText('暂无体重记录', width / 2, height / 2);
        return;
    }
    
    const weights = records.map(r => r.weight);
    const minWeight = Math.min(...weights) - 2;
    const maxWeight = Math.max(...weights) + 2;
    const range = maxWeight - minWeight || 1;
    
    const padding = 30;
    const chartWidth = width - padding * 2;
    const chartHeight = height - padding * 2;
    
    // 绑制网格线
    ctx.strokeStyle = '#e9ecef';
    ctx.lineWidth = 1;
    for (let i = 0; i <= 4; i++) {
        const y = padding + (chartHeight / 4) * i;
        ctx.beginPath();
        ctx.moveTo(padding, y);
        ctx.lineTo(width - padding, y);
        ctx.stroke();
    }
    
    // 绑制目标线
    if (appState.weightGoal.target) {
        const targetY = padding + chartHeight - 
            ((appState.weightGoal.target - minWeight) / range) * chartHeight;
        ctx.strokeStyle = '#1abc9c';
        ctx.setLineDash([5, 5]);
        ctx.beginPath();
        ctx.moveTo(padding, targetY);
        ctx.lineTo(width - padding, targetY);
        ctx.stroke();
        ctx.setLineDash([]);
    }
    
    // 绑制体重曲线
    ctx.strokeStyle = '#0d7377';
    ctx.lineWidth = 2;
    ctx.beginPath();
    
    records.forEach((record, index) => {
        const x = padding + (index / Math.max(records.length - 1, 1)) * chartWidth;
        const y = padding + chartHeight - 
            ((record.weight - minWeight) / range) * chartHeight;
        
        if (index === 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }
    });
    ctx.stroke();
    
    // 绑制数据点
    records.forEach((record, index) => {
        const x = padding + (index / Math.max(records.length - 1, 1)) * chartWidth;
        const y = padding + chartHeight - 
            ((record.weight - minWeight) / range) * chartHeight;
        
        ctx.beginPath();
        ctx.arc(x, y, 4, 0, Math.PI * 2);
        ctx.fillStyle = '#0d7377';
        ctx.fill();
    });
}

这段代码展示了Canvas绑制图表的完整流程。首先是数据预处理阶段,我们需要计算数据的范围(最小值、最大值),以便将数据点映射到画布坐标系中。

坐标转换的数学原理如下:

x 坐标 = p a d d i n g + 数据索引 数据总数 − 1 × 图表宽度 x坐标 = padding + \frac{数据索引}{数据总数-1} \times 图表宽度 x坐标=padding+数据总数1数据索引×图表宽度

y 坐标 = p a d d i n g + 图表高度 − 数据值 − 最小值 数据范围 × 图表高度 y坐标 = padding + 图表高度 - \frac{数据值-最小值}{数据范围} \times 图表高度 y坐标=padding+图表高度数据范围数据值最小值×图表高度

图表的绑制分为三个层次:底层是网格线,用于辅助用户读取数值;中间是目标线,用虚线样式标识用户的减肥目标;顶层是实际数据曲线和数据点。

2.4 BMI计算与健康状态评估

BMI(Body Mass Index,身体质量指数)是评估体重状况的国际标准指标。其计算公式为:

B M I = 体重 ( k g ) 身高 ( m ) 2 BMI = \frac{体重(kg)}{身高(m)^2} BMI=身高(m)2体重(kg)

我们的实现代码如下:

function calculateBMI() {
    const height = parseFloat(document.getElementById('bmiHeight').value) / 100;
    const weight = parseFloat(document.getElementById('bmiWeight').value);
    
    if (isNaN(height) || isNaN(weight) || height <= 0 || weight <= 0) {
        alert('请输入有效的身高和体重');
        return;
    }
    
    const bmi = weight / (height * height);
    document.getElementById('bmiValue').textContent = bmi.toFixed(1);
    
    let status = '';
    let color = '';
    if (bmi < 18.5) {
        status = '体重过轻';
        color = '#f39c12';
    } else if (bmi < 24) {
        status = '体重正常';
        color = '#1abc9c';
    } else if (bmi < 28) {
        status = '体重过重';
        color = '#f39c12';
    } else {
        status = '肥胖';
        color = '#e74c3c';
    }
    
    document.getElementById('bmiStatus').textContent = status;
    document.getElementById('bmiStatus').style.color = color;
    
    // 根据BMI计算建议目标体重
    const targetBMI = 22;
    const targetWeight = targetBMI * height * height;
    document.getElementById('targetWeight').value = targetWeight.toFixed(1);
}

BMI的判定标准我们采用了中国肥胖问题工作组制定的标准,这与国际标准略有不同:

BMI范围 国际标准 中国标准 状态颜色
< 18.5 体重过轻 体重过轻 橙色
18.5-24.9 正常 18.5-23.9 正常 绿色
25-29.9 超重 24-27.9 超重 橙色
≥ 30 肥胖 ≥ 28 肥胖 红色

在计算BMI的同时,我们还提供了一个智能功能——根据理想BMI值(取22作为标准)反向计算建议的目标体重。这个功能可以帮助用户设定一个科学合理的减肥目标。


三、界面设计与用户体验优化

3.1 三栏布局的信息架构

轻盈伴侣采用了经典的三栏布局设计,这种布局方式在信息密度和视觉平衡之间取得了良好的折中。

左侧面板聚焦于体重追踪功能,这是减肥过程中最核心的数据指标。面板从上到下依次排列:今日体重输入、目标设定、BMI计算器、体重曲线图表、打卡记录。这种排列顺序遵循了用户的使用习惯——先记录当前状态,再设定目标,然后查看进度变化。

中间面板是热量管理中心,这是应用最复杂的区域。顶部是热量摄入的圆形进度指示器,直观展示今日摄入与目标的差距。下方分为左右两部分:左侧是食物快速添加区域,右侧是今日摄入日志。这种布局让用户既能快速添加食物,又能随时查看完整的摄入记录。

右侧面板专注于运动计划,包括运动目标进度、运动类型选择、计时器、AI建议、周统计和饮水追踪。这个面板的设计理念是提供完整的运动支持工具链,从计划到执行再到统计分析。

3.2 色彩系统与视觉层次

应用的色彩系统以青绿色(#0d7377)为主色调,这个颜色既传达了健康、活力的意象,又不会过于刺激用户的视觉。我们构建了一个完整的色彩层级:

/* 主色调 */
--primary-color: #0d7377;
--primary-light: #14a085;
--primary-lighter: #1abc9c;

/* 功能色 */
--success-color: #1abc9c;  /* 正常状态 */
--warning-color: #f39c12; /* 提醒状态 */
--danger-color: #e74c3c;  /* 警告状态 */

/* 中性色 */
--background: #f8f9fa;
--border: #e9ecef;
--text-primary: #333;
--text-secondary: #666;
--text-muted: #999;

在热量显示区域,我们运用了色彩来传达不同的状态信息:

  • 当剩余热量充足时,显示为红色(提醒用户还可以摄入)
  • 当剩余热量较少时,显示为橙色(提醒用户注意控制)
  • 当热量已经超标时,显示为深红色(警告用户需要增加运动)

3.3 进度环的SVG实现

热量摄入和运动目标的进度展示,我们采用了SVG圆形进度条。这种实现方式比传统的水平进度条更加美观,也更节省空间:

<svg class="calorie-progress" viewBox="0 0 100 100">
    <circle class="calorie-bg" cx="50" cy="50" r="45"></circle>
    <circle class="calorie-fill" cx="50" cy="50" r="45" id="calorieProgress"></circle>
</svg>

进度条的动态更新通过stroke-dashoffset属性实现:

function updateCalorieUI() {
    const progress = Math.min(100, (appState.calorieConsumed / appState.calorieTarget) * 100);
    const circumference = 283; // 2 * PI * 45
    const offset = circumference - (progress / 100) * circumference;
    document.getElementById('calorieProgress').style.strokeDashoffset = offset;
}

SVG圆环的进度原理是:首先设置stroke-dasharray为圆周长,这样整个圆环会显示完整的描边。然后通过调整stroke-dashoffset来控制描边的起始位置,从而实现进度效果。

圆周长 = 2 π r = 2 × 3.14159 × 45 ≈ 283 圆周长 = 2\pi r = 2 \times 3.14159 \times 45 \approx 283 圆周长=2πr=2×3.14159×45283

偏移量 = 圆周长 − 进度百分比 100 × 圆周长 偏移量 = 圆周长 - \frac{进度百分比}{100} \times 圆周长 偏移量=圆周长100进度百分比×圆周长

3.4 响应式交互设计

在交互设计层面,我们注重提供即时反馈。当用户添加食物时,热量数值会立即更新,进度环会平滑动画过渡,日志列表会新增一条记录。这种即时反馈让用户能够清楚地感知自己的操作效果。

食物添加弹窗的设计也体现了交互细节的考量:

function adjustAmount(delta) {
    const input = document.getElementById('foodAmount');
    let value = parseFloat(input.value) + delta * 0.5;
    value = Math.max(0.5, Math.min(10, value));
    input.value = value;
    appState.foodAmount = value;
    updateTotalCalorie();
}

用户可以通过加减按钮调整食物份量,每次调整的步长是0.5份。同时,我们设置了合理的上下限(0.5到10份),防止用户输入不合理的数值。每次调整后,总热量会实时重新计算,让用户能够直观看到份量变化对热量的影响。


四、数据持久化与状态管理

4.1 应用状态对象设计

整个应用的数据状态由一个全局对象appState统一管理:

const appState = {
    // 体重记录
    weightRecords: [],
    weightGoal: {
        current: 70,
        target: 60,
        days: 90,
        startDate: null
    },

    // 热量追踪
    calorieTarget: 1500,
    calorieConsumed: 0,
    calorieLog: [],

    // 运动追踪
    exerciseGoal: {
        burn: 300,
        minutes: 30
    },
    exerciseBurned: 0,
    exerciseMinutes: 0,
    exerciseLog: [],

    // 饮水追踪
    waterTarget: 8,
    waterCurrent: 0,

    // 打卡记录
    checkins: [],
    streak: 0,

    // 周数据
    weekData: [0, 0, 0, 0, 0, 0, 0]
};

这种集中式状态管理的设计有几个优势:

数据一致性:所有模块共享同一数据源,避免了数据分散导致的同步问题。

易于扩展:当需要添加新功能时,只需在appState中增加相应的字段,然后编写操作函数即可。

便于持久化:整个状态对象可以直接序列化存储到LocalStorage,实现数据的持久化。

4.2 数据计算逻辑

减肥应用涉及大量的数据计算,我们封装了多个计算函数:

体重统计计算

function updateWeightStats() {
    const goal = appState.weightGoal;
    const records = appState.weightRecords;
    
    if (records.length === 0) return;
    
    const latestWeight = records[records.length - 1].weight;
    const firstWeight = records[0].weight;
    
    // 已减体重
    const lost = (firstWeight - latestWeight).toFixed(1);
    document.getElementById('weightLost').textContent = lost;
    
    // 日均减重
    const days = records.length;
    const dailyLoss = days > 0 ? ((firstWeight - latestWeight) / days).toFixed(2) : 0;
    document.getElementById('dailyLoss').textContent = dailyLoss;
    
    // 剩余体重
    const remaining = (latestWeight - goal.target).toFixed(1);
    document.getElementById('remainingWeight').textContent = remaining;
}

这里我们计算了三个关键指标:已减体重、日均减重速度、距离目标的剩余体重。这些指标能够帮助用户全面了解自己的减肥进度。

周运动统计

function updateWeekStats() {
    const bars = document.querySelectorAll('.week-bar');
    const maxValue = Math.max(...appState.weekData, 100);
    
    bars.forEach((bar, index) => {
        const value = appState.weekData[index] || 0;
        const fillHeight = (value / maxValue) * 40;
        
        const fill = bar.querySelector('.bar-fill');
        const valueEl = bar.querySelector('.day-value');
        
        fill.style.height = fillHeight + 'px';
        fill.classList.toggle('active', value > 0);
        valueEl.textContent = value;
    });
    
    const weekBurned = appState.weekData.reduce((a, b) => a + b, 0);
    document.getElementById('weekBurned').textContent = weekBurned;
    document.getElementById('weekMinutes').textContent = appState.exerciseMinutes;
}

周统计采用柱状图的形式展示,每天的消耗热量对应一个柱子。柱子的高度按比例计算,最大值作为参考基准,确保图表的视觉效果。

4.3 打卡连续天数算法

打卡功能的连续天数计算是一个需要仔细考虑的问题。用户可能今天打卡、明天忘记、后天再打卡,这种情况下连续天数应该如何计算?

我们的实现方案是:只有连续打卡才计入连续天数,一旦中断,连续天数归零重新计算:

function updateStreak() {
    const checkins = appState.checkins.sort();
    let streak = 0;
    const today = new Date();
    
    for (let i = checkins.length - 1; i >= 0; i--) {
        const checkDate = new Date(checkins[i]);
        const diffDays = Math.floor((today - checkDate) / (1000 * 60 * 60 * 24));
        
        if (diffDays <= 1) {
            streak++;
        } else {
            break;
        }
    }
    
    appState.streak = streak;
    document.getElementById('streakCount').textContent = streak;
}

这个算法从最近的打卡记录开始向前遍历,检查每条记录与当前日期的差距。如果差距不超过1天(允许昨天打卡但今天未打卡的情况),则计入连续天数;一旦遇到超过1天的差距,立即终止遍历。


五、AI智能建议系统

5.1 本地建议数据库

为了在没有网络连接的情况下也能提供有用的建议,我们构建了一个本地建议数据库:

const weightLossAdvice = {
    lowCalories: [
        '今日摄入热量偏低,建议适当增加蛋白质摄入,保持肌肉量。',
        '热量摄入不足可能导致代谢下降,建议至少摄入1200kcal。',
        '节食减肥不可取,建议搭配适量运动更有效。'
    ],
    highCalories: [
        '今日摄入热量偏高,建议增加有氧运动消耗多余热量。',
        '可以尝试用蔬菜代替部分主食,减少热量摄入。',
        '偶尔放纵没问题,明天记得控制饮食哦!'
    ],
    lowExercise: [
        '今日运动量不足,建议快走30分钟消耗热量。',
        '久坐不利于减肥,每小时起来活动一下吧。',
        '即使是散步也比不动强,现在起身动一动吧!'
    ],
    lowWater: [
        '饮水不足会影响代谢,记得多喝水!',
        '每天8杯水是基本目标,脂肪代谢需要水分参与。',
        '可以在饭前喝一杯水,有助于控制食欲。'
    ]
};

建议数据库按照不同的场景分类存储,每个类别包含多条备选建议。当需要生成建议时,系统会根据用户当前的各项数据指标,从相应的类别中随机选取一条建议。

5.2 综合建议生成算法

综合建议的生成需要考虑多个因素,我们设计了一个多维度评估算法:

function generateComprehensiveAdvice() {
    const calorieBalance = appState.calorieConsumed - appState.exerciseBurned;
    const advices = [];
    
    // 热量建议
    if (calorieBalance > 500) {
        advices.push(getRandomAdvice('highCalories'));
    } else if (calorieBalance < -200) {
        advices.push(getRandomAdvice('balanced'));
    } else {
        advices.push(getRandomAdvice('balanced'));
    }
    
    // 运动建议
    if (appState.exerciseMinutes < 30) {
        advices.push(getRandomAdvice('lowExercise'));
    } else {
        advices.push(getRandomAdvice('goodExercise'));
    }
    
    // 饮水建议
    if (appState.waterCurrent < 6) {
        advices.push(getRandomAdvice('lowWater'));
    } else {
        advices.push(getRandomAdvice('highWater'));
    }
    
    return advices.join(' ');
}

这个算法首先计算热量平衡值(摄入减去消耗),根据这个值的正负和大小选择相应的热量建议。然后检查运动时间是否达标,给出运动方面的建议。最后检查饮水量,提供饮水建议。

三条建议组合在一起,形成了一条完整的综合建议。这种多维度的方式能够全面覆盖用户当天的减肥执行情况。

5.3 在线AI接口集成

除了本地建议,我们还集成了在线AI接口,可以根据用户的详细数据生成更加个性化的建议:

async function getAIAdvice() {
    const adviceContent = document.getElementById('aiAdviceContent');
    adviceContent.innerHTML = '<p>🤖 AI正在分析您的数据...</p>';
    
    const calorieBalance = appState.calorieConsumed - appState.exerciseBurned;
    const weight = appState.weightRecords.length > 0 ? 
        appState.weightRecords[appState.weightRecords.length - 1].weight : 0;
    
    const prompt = `作为减肥顾问,请根据以下数据给出简短的每日减肥建议:
- 今日摄入:${appState.calorieConsumed} kcal
- 今日消耗:${appState.exerciseBurned} kcal  
- 热量差:${calorieBalance} kcal
- 当前体重:${weight} kg
- 运动时间:${appState.exerciseMinutes} 分钟
- 饮水:${appState.waterCurrent} 杯

请用温暖鼓励的语气给出1-2条具体建议。`;
    
    try {
        const response = await fetch(API_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${API_KEY}`
            },
            body: JSON.stringify({
                model: 'qwen/qwen-7b-chat',
                messages: [{ role: 'user', content: prompt }],
                max_tokens: 200
            })
        });
        
        const data = await response.json();
        const advice = data.choices?.[0]?.message?.content || '获取建议失败';
        
        adviceContent.innerHTML = `<p>${advice.replace(/\n/g, '<br>')}</p>`;
    } catch (error) {
        // 降级为本地建议
        adviceContent.innerHTML = `<p>${generateComprehensiveAdvice()}</p>`;
    }
}

在线AI的优势在于能够理解更复杂的上下文,给出更有针对性的建议。比如,如果用户已经连续多天热量超标,AI可能会建议调整饮食结构而不是简单地增加运动。

我们还设计了降级机制:当网络请求失败时,自动切换到本地建议生成。这确保了用户在任何情况下都能获得有用的建议。


六、性能优化与工程实践

6.1 Canvas绑制性能优化

体重曲线图表的绑制频率相对较低(只在添加新记录时更新),但运动计时器的更新频率很高(每秒一次)。为了优化性能,我们采用了以下策略:

避免不必要的重绘:只在数据变化时才触发图表重绘,而不是使用定时器持续刷新。

function recordWeight() {
    const weight = parseFloat(document.getElementById('todayWeight').value);
    // ... 数据处理 ...
    
    updateWeightStats();
    drawWeightChart();  // 只在这里触发一次重绘
}

使用requestAnimationFrame:对于需要动画效果的场景,使用requestAnimationFrame代替setInterval,可以更好地与浏览器的渲染周期同步。

减少DOM操作:在更新UI时,尽量批量更新而不是逐个更新:

function updateCalorieUI() {
    // 批量更新多个元素
    document.getElementById('calorieConsumed').textContent = appState.calorieConsumed;
    document.getElementById('calorieRemaining').textContent = remaining;
    document.getElementById('calorieProgress').style.strokeDashoffset = offset;
}

6.2 事件处理优化

食物列表的点击事件处理采用了事件委托模式:

function renderFoodList(category) {
    const foodList = document.getElementById('foodList');
    const foods = foodDatabase[category];
    
    let html = '';
    for (const [name, info] of Object.entries(foods)) {
        html += `
            <div class="food-item" onclick="openFoodModal('${name}', '${category}')">
                <span class="food-name">${name}</span>
                <span class="food-cal">${info.calorie} kcal</span>
            </div>
        `;
    }
    foodList.innerHTML = html;
}

虽然这里使用了内联onclick,但在实际项目中,更好的做法是在容器元素上绑定事件监听器,通过event.target来判断点击的是哪个子元素。这样可以减少事件监听器的数量,提高性能。

6.3 内存管理

计时器的清理是一个容易被忽视的问题。如果用户频繁开始和停止计时器,而没有正确清理setInterval,会导致多个定时器同时运行,消耗系统资源:

function stopTimer() {
    appState.timer.isRunning = false;
    
    if (appState.timer.interval) {
        clearInterval(appState.timer.interval);
        appState.timer.interval = null;  // 清除引用
    }
    
    // ... 其他处理 ...
}

我们在停止计时器时,不仅调用了clearInterval,还将interval引用设置为null。这样做可以确保定时器被完全清理,避免内存泄漏。


七、扩展功能与未来规划

7.1 数据导出功能

目前应用的数据存储在内存中,页面刷新后会丢失。一个重要的扩展方向是实现数据持久化:

// 保存数据到LocalStorage
function saveData() {
    localStorage.setItem('weightLossData', JSON.stringify(appState));
}

// 从LocalStorage恢复数据
function loadData() {
    const saved = localStorage.getItem('weightLossData');
    if (saved) {
        const data = JSON.parse(saved);
        Object.assign(appState, data);
    }
}

更进一步,可以支持数据导出为CSV或JSON文件,方便用户备份或在其他设备上使用。

7.2 社交分享功能

减肥成功后的分享激励是重要的用户需求。可以添加分享功能,生成包含体重变化曲线、运动成就的图片,方便用户分享到社交平台。

7.3 智能提醒系统

基于用户的历史数据,可以预测用户的减肥进度,并给出智能提醒:

  • 如果连续三天没有记录体重,提醒用户坚持记录
  • 如果热量摄入连续超标,提醒用户注意饮食
  • 如果运动量持续不足,提醒用户增加运动

7.4 食物识别功能

通过集成图像识别AI,用户可以拍摄食物照片,系统自动识别食物类型并估算热量。这将大大简化食物记录的操作流程。


八、总结与心得

8.1 技术收获

通过开发轻盈伴侣这款减肥应用,我在以下几个方面获得了宝贵的技术经验:

Canvas图表绑制:从零开始学习Canvas API,掌握了绑制网格线、曲线、数据点的技巧。特别是坐标转换的数学原理,让我对数据可视化有了更深入的理解。

状态管理模式:采用集中式状态对象管理应用数据,这种模式虽然简单,但对于中小型应用来说非常实用。它避免了复杂的状态管理库引入,同时保持了代码的清晰性。

SVG进度条实现:通过stroke-dashoffset控制圆环进度,这是一个巧妙的技术方案。相比使用JavaScript动态修改SVG路径,这种方式更加简洁高效。

8.2 产品思考

从产品角度,这次开发让我对减肥应用的设计有了更深的认识:

功能聚焦:减肥用户的核心需求其实很明确——记录体重、控制饮食、增加运动。很多应用添加了大量花哨功能,反而分散了用户的注意力。轻盈伴侣坚持只做核心功能,让用户能够专注于减肥本身。

即时反馈:用户每做一个操作,都应该立即看到效果。添加食物后热量数值立即更新,完成运动后消耗数值立即累加。这种即时反馈能够增强用户的参与感。

激励机制:打卡功能和连续天数统计,看似简单,却能有效激励用户坚持。减肥是一个需要长期坚持的过程,任何能够增强用户动力的设计都是值得投入的。

8.3 未来展望

轻盈伴侣目前是一个功能完整但相对简单的应用。未来可以在以下几个方向继续深化:

数据智能分析:基于用户的历史数据,使用机器学习算法预测减肥进度,给出更精准的建议。

社交功能:添加好友系统、排行榜、减肥打卡分享等功能,利用社交动力促进用户坚持。

硬件集成:与智能体重秤、运动手环等硬件设备集成,实现数据的自动采集,减少用户的手动输入负担。


九、附录:完整代码结构

9.1 文件组织

web_engine/src/main/resources/resfile/resources/app/
├── index.html          # 主页面结构
├── style.css           # 样式文件
├── main.js             # Electron主进程
├── preload.js          # 预加载脚本
└── js/
    ├── config.js       # 配置与数据定义
    ├── battle.js       # 核心业务逻辑
    └── ai.js           # AI建议模块

9.2 主要函数清单

函数名称 功能描述 所属文件
initApp 应用初始化 battle.js
showFoodCategory 切换食物分类 battle.js
confirmAddFood 添加食物记录 battle.js
updateCalorieUI 更新热量显示 battle.js
selectExercise 选择运动类型 battle.js
startTimer 启动计时器 battle.js
stopTimer 停止计时器 battle.js
recordWeight 记录体重 battle.js
drawWeightChart 绑制体重图表 battle.js
calculateBMI 计算BMI battle.js
doCheckin 执行打卡 battle.js
updateStreak 更新连续天数 battle.js
adjustWater 调整饮水量 battle.js
getAIAdvice 获取AI建议 battle.js
generateComprehensiveAdvice 生成本地建议 ai.js

欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐