鸿蒙PC Electron——轻盈伴侣 - 智能减肥助手技术实现详解
欢迎加入开源鸿蒙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×45≈283
偏移量 = 圆周长 − 进度百分比 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/
所有评论(0)