个人健康减脂数据中心,自用健身减肥工具分享
如何使用?
代码一共1454行,在电脑上记事本新建个人健康减脂数据中心.txt
文件, 贴入下文的完整代码,然后修改后缀为.html
,双击打开即可。
【代码在哪里?】文章首发于博客园, 平台限制, 这里展示不全, 文章原文可随便复制,md格式,在博客园有写,这里为副本,主页个人简介有,这里只展示部分内容, 平台不兼容,无法全发。
为什么弄?
在健身房锻炼的时候,看着健身房的减肥知识有感,回来自己阅读了很多本健身相关的书籍,包括但不限于《健身营养全书》、《学会吃饭》、《肥胖代码》等。打开一些软件,相关的功能都是要充会员才能查看,或者使用起来比较麻烦,我只是想用一用小工具而已。
翻出以前大学时期写过的前端三大件相关的笔记,结合AI辅助、文献、以前写的代码进行整合,于是简单验证了这个想法,自用**。
每次制定健身和减肥计划的时候都得在Excel上进行计算(虽然写了宏命令自动化执行),但是还是不够方便,希望能够方便的实现:
1、告诉我每日总消耗热量(TDEE)是多少,我方便用于设置能量缺口
2、能够根据我设置的能量缺口,计算出我多久能够达到减肥目标,体重如何变化,生成图表
3、能够计算出日期之间的差值,距离目标还有多久,多少天以后是什么日子
4、能够计算出BMI
于是查阅文献,根据自己的基本需求,利用一点时间进行编写完成,现在已经成为自己不可替代常用小工具了。
本工具只是个人开发来满足基本需求的,现在开源出来给感兴趣的朋友使用。
小工具是闲暇之余简单写写的,比较简陋,还有很多地方可以改进,比较简单,感兴趣的朋友可以继续完善。
有什么功能?
1、计算每日总消耗热量(TDEE)是多少,用于设置能量缺口,计算原理如下,还能在计算后根据不同软件差异(有些体脂秤会有100大卡左右的BMR差距)进行校准,实现二次计算。计算完毕后还能导出报告,附加励志语。
能量缺口范围: 500~750大卡,低于或高于这个范围都不行。能量缺口超过750大卡,可能会导致一系列的营养缺乏。 不足可能由于计算误差, 导致减肥失败.
Mifflin-St Jeor 公式是一个用于计算基础代谢率(BMR) 的公式。男性和女性的公式不同:男性公式为 (10 × 体重(公斤)) + (6.25 × 身高(公分)) - (5 × 年龄(岁)) + 5,女性公式为 (10 × 体重(公斤)) + (6.25 × 身高(公分)) - (5 × 年龄(岁)) - 161。
TDEE每日总能量消耗=基础热量BMR+非运动性活动产热NEAT+运动能量消耗EAT(例如健身房锻炼或跑步等)+TEF食物热效应
由于吃的食物每天都不一样,TEF不好计算,且占比不高,直接忽略,这样会让总消耗热量减少,会潜在增加能量缺口,更容易减肥。
2、能够根据设置的能量缺口,计算出多久能够达到减肥目标,体重如何变化,生成图表
3、能够计算出日期之间的差值,距离目标还有多久,多少天以后是什么日子
4、能够计算出BMI,并进行判断
let category = ""; if (bmi < 18.5) category = "体重过轻"; else if (bmi < 24) category = "正常范围"; else if (bmi < 28) category = "超重"; else category = "肥胖";
完整代码
点击查看代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>个人健康数据中心 V0.42</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@400;500;700&family=Fira+Code&display=swap"
rel="stylesheet"
/>
<!-- Aplayer 播放器样式 -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css"
/>
<style>
:root {
--bg-color: #111827;
--card-bg: #1f2937;
--primary-color: #0ea5e9;
--text-color: #d1d5db;
--header-font: "Orbitron", sans-serif;
--body-font: "Inter", sans-serif;
--mono-font: "Fira Code", monospace;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--body-font);
overflow: hidden;
}
.bg-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* V0.42 Fix: Allow clicks to pass through the canvas */
pointer-events: none;
}
#matrix-canvas {
z-index: -2;
}
#bg-canvas {
z-index: -1;
}
.main-layout {
display: flex;
height: 100vh;
}
/* 终端样式 */
#terminal-wrapper {
width: 33.333333%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
border-right: 1px solid var(--primary-color);
padding: 1rem;
display: flex;
flex-direction: column;
transition: width 0.3s ease-in-out, padding 0.3s ease-in-out;
overflow: hidden;
}
#terminal-wrapper.hidden {
width: 0;
padding: 0;
}
#terminal-header {
font-family: var(--header-font);
color: var(--primary-color);
margin-bottom: 1rem;
flex-shrink: 0;
}
#terminal-content {
flex-grow: 1;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
font-family: var(--mono-font);
font-size: 0.875rem;
}
#terminal-content .line {
display: block;
}
#terminal-content .line.success {
color: #22c55e;
}
#terminal-content .line.info {
color: #3b82f6;
}
#terminal-content .line.warn {
color: #eab308;
}
#terminal-content .line.data {
color: #a78bfa;
}
#terminal-idle-animation {
white-space: pre;
line-height: 1.2;
margin-top: 1rem;
color: var(--primary-color);
font-size: 0.75rem;
}
/* 主内容区 */
#main-content {
width: 66.666667%;
height: 100vh;
transition: width 0.3s ease-in-out;
position: relative;
}
#main-content.full-width {
width: 100%;
}
.app-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.app-view {
display: none;
}
.app-view.active {
display: block;
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 开机动画 */
#loading-view {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
background-color: var(--bg-color);
}
#loading-view .shape {
position: absolute;
border-radius: 50%;
background: var(--primary-color);
opacity: 0;
animation: float 8s infinite ease-in-out;
}
@keyframes float {
0%,
100% {
transform: translateY(0) scale(1);
opacity: 0;
}
50% {
transform: translateY(-100px) scale(1.2);
opacity: 0.6;
}
}
/* 终端切换按钮 */
#terminal-toggle {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 150;
background-color: var(--card-bg);
color: var(--text-color);
border: 1px solid #4b5563;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
pointer-events: auto; /* Ensure button is clickable */
}
#terminal-toggle:hover {
background-color: var(--primary-color);
color: white;
}
/* 通用样式 */
.card {
background-color: rgba(31, 41, 55, 0.8);
backdrop-filter: blur(10px);
border-radius: 0.75rem;
padding: 1.5rem;
border: 1px solid #374151;
max-height: 90vh;
overflow-y: auto;
}
.menu-button {
background-color: rgba(31, 41, 55, 0.8);
backdrop-filter: blur(10px);
border: 1px solid #374151;
padding: 1.5rem;
border-radius: 0.75rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
font-family: var(--header-font);
}
.menu-button:hover {
transform: translateY(-5px);
background-color: #374151;
border-color: var(--primary-color);
}
.result-box {
background-color: var(--bg-color);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1.5rem;
text-align: center;
border: 1px solid #374151;
}
.btn {
background-color: var(--primary-color);
color: white;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
}
.btn-secondary {
background-color: #4b5563;
}
.btn:hover {
background-color: #0284c7;
}
.btn-secondary:hover {
background-color: #6b7280;
}
label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: #9ca3af;
}
input,
select {
width: 100%;
padding: 0.75rem;
border: 1px solid #4b5563;
border-radius: 0.375rem;
background-color: #374151;
color: var(--text-color);
}
.tab-button {
flex: 1;
padding: 0.5rem;
cursor: pointer;
background-color: #374151;
border: 1px solid #4b5563;
color: #9ca3af;
transition: all 0.2s;
}
.tab-button.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.unit-toggle {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.unit-toggle span {
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.unit-toggle span.active {
background-color: var(--primary-color);
color: white;
}
/* 报告与UI内表格样式 */
#report-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
}
.data-table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
font-size: 0.875rem;
}
.data-table th,
.data-table td {
border: 1px solid #4b5563;
padding: 0.5rem;
text-align: left;
}
.data-table th {
background-color: #374151;
}
</style>
</head>
<body>
<canvas id="matrix-canvas" class="bg-canvas"></canvas>
<canvas id="bg-canvas" class="bg-canvas"></canvas>
<!-- Aplayer 播放器容器 -->
<div
id="my-aplayer"
class="aplayer"
data-id="6895409634"
data-server="netease"
data-type="playlist"
data-fixed="true"
data-autoplay="true"
data-order="random"
data-volume="0.7"
data-theme="#2EA7E0"
data-preload="auto"
data-listFolded="true"
></div>
<!-- 开机动画 -->
<div id="loading-view" class="app-view active">
<h1
class="text-4xl font-bold text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
style="font-family: var(--header-font)"
>
HEALTH DATA CENTER
</h1>
</div>
<div class="main-layout">
<!-- 终端 -->
<div id="terminal-wrapper">
<div id="terminal-header">HDC_TERMINAL v0.42</div>
<div id="terminal-content"></div>
</div>
<!-- 主内容区 -->
<div id="main-content">
<div id="terminal-toggle" onclick="toggleTerminal()">
<svg
id="toggle-icon-hide"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
<svg
id="toggle-icon-show"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="display: none"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</div>
<div class="app-container">
<!-- 主菜单 -->
<div id="menu-view" class="app-view w-full max-w-4xl p-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div class="menu-button" onclick="changeState('tdee-view')">
<h2 class="text-xl font-bold text-white">TDEE 计算</h2>
<p class="text-xs mt-2">TDEE</p>
</div>
<div
class="menu-button"
onclick="changeState('weight-loss-view')"
>
<h2 class="text-xl font-bold text-white">减肥目标</h2>
<p class="text-xs mt-2">Weight Goal</p>
</div>
<div class="menu-button" onclick="changeState('date-calc-view')">
<h2 class="text-xl font-bold text-white">日期计算</h2>
<p class="text-xs mt-2">Date Calc</p>
</div>
<div class="menu-button" onclick="changeState('bmi-view')">
<h2 class="text-xl font-bold text-white">BMI 计算</h2>
<p class="text-xs mt-2">BMI Calc</p>
</div>
</div>
</div>
<!-- 各个计算器模块 -->
<div id="bmi-view" class="app-view w-full max-w-md p-4">
<div class="card">
<h2
class="text-2xl font-bold text-center text-white mb-4"
style="font-family: var(--header-font)"
>
BMI 计算器
</h2>
<div class="space-y-4">
<div>
<label for="bmi_height">身高 (cm)</label
><input type="number" id="bmi_height" />
</div>
<div>
<label for="bmi_weight">体重 (kg)</label
><input type="number" id="bmi_weight" />
</div>
</div>
<button onclick="calculateBMI()" class="btn mt-4">
开始计算
</button>
<div
id="bmiResult"
class="result-box"
style="display: none"
></div>
<button
onclick="changeState('menu-view')"
class="btn btn-secondary mt-4"
>
返回主菜单
</button>
</div>
</div>
<div id="date-calc-view" class="app-view w-full max-w-md p-4">
<div class="card">
<h2
class="text-2xl font-bold text-center text-white mb-4"
style="font-family: var(--header-font)"
>
日期计算器
</h2>
<div class="flex rounded-md overflow-hidden mb-4">
<button
id="tab-date-diff"
class="tab-button active"
onclick="switchDateMode('diff')"
>
日期差距</button
><button
id="tab-date-countdown"
class="tab-button"
onclick="switchDateMode('countdown')"
>
倒计时</button
><button
id="tab-date-op"
class="tab-button"
onclick="switchDateMode('op')"
>
日期运算
</button>
</div>
<div id="mode-date-diff">
<div>
<label for="dd_startDate">开始日期</label
><input type="date" id="dd_startDate" />
</div>
<div class="mt-4">
<label for="dd_endDate">结束日期</label
><input type="date" id="dd_endDate" />
</div>
</div>
<div id="mode-date-countdown" style="display: none">
<div>
<label for="dc_targetDate">目标日期</label
><input type="date" id="dc_targetDate" />
</div>
</div>
<div id="mode-date-op" style="display: none">
<div>
<label for="do_startDate">起始日期</label
><input type="date" id="do_startDate" />
</div>
<div class="flex items-center gap-2 my-4">
<select id="do_operation" class="w-auto">
<option value="add">增加</option>
<option value="subtract">减少</option></select
><input type="number" id="do_days" placeholder="天数" />
</div>
</div>
<button onclick="calculateDate()" class="btn mt-4">
开始计算
</button>
<div
id="dateResult"
class="result-box"
style="display: none"
></div>
<button
onclick="changeState('menu-view')"
class="btn btn-secondary mt-4"
>
返回主菜单
</button>
</div>
</div>
<div id="tdee-view" class="app-view w-full max-w-md p-4">
<div class="card">
<h2
class="text-2xl font-bold text-center text-white mb-4"
style="font-family: var(--header-font)"
>
TDEE 计算器
</h2>
<div class="unit-toggle">
<span
id="tdee_unit_kg"
class="active"
onclick="setTdeeUnit('kg')"
>kg</span
><span id="tdee_unit_jin" onclick="setTdeeUnit('jin')">斤</span>
</div>
<div class="space-y-4">
<div>
<label for="tdee_gender">性别</label
><select id="tdee_gender">
<option value="male">男性</option>
<option value="female">女性</option>
</select>
</div>
<div>
<label for="tdee_age">年龄 (岁)</label
><input type="number" id="tdee_age" />
</div>
<div>
<label for="tdee_height">身高 (cm)</label
><input type="number" id="tdee_height" />
</div>
<div>
<label for="tdee_weight"
>体重 (<span class="unit-label">kg</span>)</label
><input type="number" id="tdee_weight" />
</div>
<div>
<label for="tdee_activity">日常活动等级 (NEAT)</label
><select id="tdee_activity">
<option value="1.2">久坐</option>
<option value="1.375">轻度</option>
<option value="1.55">中度</option>
<option value="1.725">高度</option>
<option value="1.9">极高</option>
</select>
</div>
<div>
<label for="tdee_eat">额外运动消耗 (EAT, 大卡)</label
><input type="number" id="tdee_eat" />
</div>
</div>
<button onclick="calculateTDEE()" class="btn mt-4">
计算 TDEE
</button>
<div id="tdeeResultReport" class="hidden"></div>
<div
id="tdeeResult"
class="result-box"
style="display: none"
></div>
<button
id="tdee_download_btn"
class="btn btn-secondary mt-2"
style="display: none"
onclick="showReportModal('tdee')"
>
导出报告</button
><button
onclick="changeState('menu-view')"
class="btn btn-secondary mt-2"
>
返回主菜单
</button>
</div>
</div>
<div id="weight-loss-view" class="app-view w-full max-w-lg p-4">
<div class="card">
<h2
class="text-2xl font-bold text-center text-white mb-4"
style="font-family: var(--header-font)"
>
减肥目标计算器
</h2>
<div>
<label for="wl_height">身高 (cm)</label
><input type="number" id="wl_height" class="mb-4" />
</div>
<div class="unit-toggle">
<span id="wl_unit_kg" onclick="setWeightLossUnit('kg')">kg</span
><span
id="wl_unit_jin"
class="active"
onclick="setWeightLossUnit('jin')"
>斤</span
>
</div>
<div class="flex rounded-md overflow-hidden mb-4">
<button
id="tab-loss-rate"
class="tab-button active"
onclick="switchWeightLossMode('rate')"
>
按减重速率</button
><button
id="tab-calorie-gap"
class="tab-button"
onclick="switchWeightLossMode('calorie')"
>
按能量缺口
</button>
</div>
<div id="mode-rate" class="space-y-4">
<div>
<label for="wl_currentWeight"
>当前体重 (<span class="unit-label">斤</span>)</label
><input type="number" id="wl_currentWeight" />
</div>
<div>
<label for="wl_targetWeight"
>目标体重 (<span class="unit-label">斤</span>)</label
><input type="number" id="wl_targetWeight" />
</div>
<div>
<label for="wl_weeklyLoss"
>每周计划减重 (<span class="unit-label">斤</span>)</label
><input type="number" id="wl_weeklyLoss" />
</div>
</div>
<div id="mode-calorie" class="space-y-4" style="display: none">
<div>
<label for="wc_currentWeight"
>当前体重 (<span class="unit-label">斤</span>)</label
><input type="number" id="wc_currentWeight" />
</div>
<div>
<label for="wc_targetWeight"
>目标体重 (<span class="unit-label">斤</span>)</label
><input type="number" id="wc_targetWeight" />
</div>
<div>
<label for="wc_calorieDeficit">每日能量缺口 (大卡/kcal)</label
><input type="number" id="wc_calorieDeficit" />
</div>
</div>
<button onclick="calculateWeightLossDate()" class="btn mt-4">
开始计算
</button>
<div
id="weightLossResult"
class="result-box"
style="display: none"
></div>
<div class="mt-4"><canvas id="weightLossChart"></canvas></div>
<!-- V0.42: Container for the UI table -->
<div
id="weightLossTableContainer"
class="mt-4"
style="display: none"
></div>
<div id="wlResultReport" class="hidden"></div>
<button
id="wl_download_btn"
class="btn btn-secondary mt-2"
style="display: none"
onclick="showReportModal('wl')"
>
导出报告
</button>
<button
onclick="changeState('menu-view')"
class="btn btn-secondary mt-4"
>
返回主菜单
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 报告模态框 -->
<div id="report-modal">
<div class="card w-full max-w-md">
<h3 class="text-xl font-bold text-center mb-4">生成报告</h3>
<label for="report-quote">输入你的励志语 (可选)</label
><textarea
id="report-quote"
rows="3"
class="w-full p-2 bg-gray-700 rounded-md border border-gray-600"
placeholder="例如:坚持就是胜利!"
></textarea>
<div class="flex gap-4 mt-4">
<button onclick="generateReport()" class="btn">生成并下载</button
><button onclick="hideReportModal()" class="btn btn-secondary">
取消
</button>
</div>
</div>
</div>
<!-- Aplayer 和 MetingJS 脚本 -->
<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/meting@1.2.0/dist/Meting.min.js"></script>
<!-- 点击特效脚本 -->
<script src="https://cdn.jsdelivr.net/gh/wallleap/cdn/js/shehuizhuyi.js"></script>
<script src="https://cdn.jsdelivr.net/gh/wallleap/cdn/js/love.js"></script>
<script>
// --- 全局变量 & 初始化 ---
let currentState = "loading-view";
let lastCalculationData = {};
window.onload = () => {
initLoadingAnimation();
initParticleAnimation();
initMatrixRain();
};
function changeState(newState) {
document.getElementById(currentState)?.classList.remove("active");
document.getElementById(newState)?.classList.add("active");
currentState = newState;
}
// --- 头部互动 ---
(function () {
var OriginTitile = document.title,
titleTime;
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
document.title = "歪,你去哪里了?";
clearTimeout(titleTime);
} else {
document.title = "(つェ⊂)咦,又回来了!";
titleTime = setTimeout(function () {
document.title = OriginTitile;
}, 2000);
}
});
})();
// --- 开机动画 ---
function initLoadingAnimation() {
const container = document.getElementById("loading-view");
for (let i = 0; i < 15; i++) {
const shape = document.createElement("div");
shape.className = "shape";
shape.style.width = `${Math.random() * 20 + 5}px`;
shape.style.height = shape.style.width;
shape.style.left = `${Math.random() * 100}%`;
shape.style.top = `${Math.random() * 100}%`;
shape.style.animationDelay = `${Math.random() * 5}s`;
container.appendChild(shape);
}
setTimeout(() => {
changeState("menu-view");
printToTerminal(
[
{
text: "欢迎来到个人健康数据中心 v0.42 (*^▽^*)",
className: "info",
},
{ text: "系统初始化...[OK]" },
{ text: "所有模块已加载,等待用户指令..." },
],
true
);
}, 2500);
}
// --- 终端 ---
const terminalWrapper = document.getElementById("terminal-wrapper");
const mainContent = document.getElementById("main-content");
const terminalContent = document.getElementById("terminal-content");
let typeInterval;
const alpacaArt = `
* * ┏┓ ┏┓+ +
* ┏┛┻━━━┛┻┓ + +
* ┃ ┃
* ┃ ━ ┃ ++ + + +
* ████━████ ┃+
* ┃ ┃ +
* ┃ ┻ ┃
* ┃ ┃ + +
* ┗━┓ ┏━┛
* ┃ ┃
* ┃ ┃ + + + +
* ┃ ┃
* ┃ ┃ + 神兽保佑
* ┃ ┃ 代码无bug
* ┃ ┃ +
* ┃ ┗━━━┓ + +
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛ + + + +
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛+ + + +
`;
function startIdleAnimation() {
const existingIdle = document.getElementById("terminal-idle-animation");
if (existingIdle) return;
const animationEl = document.createElement("pre");
animationEl.id = "terminal-idle-animation";
animationEl.textContent = alpacaArt;
terminalContent.appendChild(animationEl);
}
function printToTerminal(lines, startIdleOnComplete = false) {
clearInterval(typeInterval);
terminalContent.innerHTML = "";
let lineIndex = 0;
let charIndex = 0;
function type() {
if (lineIndex >= lines.length) {
clearInterval(typeInterval);
if (startIdleOnComplete) startIdleAnimation();
return;
}
const currentLine = lines[lineIndex];
if (charIndex === 0) {
const lineEl = document.createElement("span");
lineEl.className = `line ${currentLine.className || ""}`;
terminalContent.appendChild(lineEl);
}
const allLineEls = terminalContent.querySelectorAll(".line");
const targetLineEl = allLineEls[allLineEls.length - 1];
targetLineEl.textContent = currentLine.text.substring(
0,
charIndex + 1
);
charIndex++;
if (charIndex >= currentLine.text.length) {
charIndex = 0;
lineIndex++;
}
terminalContent.scrollTop = terminalContent.scrollHeight;
}
typeInterval = setInterval(type, 30);
}
// --- 背景特效 ---
function initMatrixRain() {
const canvas = document.getElementById("matrix-canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const characters = "01";
const fontSize = 16;
const columns = Math.floor(canvas.width / fontSize);
const drops = Array(columns).fill(1);
function draw() {
ctx.fillStyle = "rgba(17, 24, 39, 0.05)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#0ea5e9";
ctx.font = `${fontSize}px Fira Code`;
for (let i = 0; i < drops.length; i++) {
const text = characters.charAt(
Math.floor(Math.random() * characters.length)
);
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
}
setInterval(draw, 40);
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
drops.length = Math.floor(canvas.width / fontSize);
drops.fill(1);
});
}
function initParticleAnimation() {
const canvas = document.getElementById("bg-canvas");
const ctx = canvas.getContext("2d");
let particles = [];
const particleCount = 80;
const connectDistance = 120;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 1;
this.speedX = Math.random() * 0.5 - 0.25;
this.speedY = Math.random() * 0.5 - 0.25;
}
update() {
if (this.x > canvas.width || this.x < 0) this.speedX *= -1;
if (this.y > canvas.height || this.y < 0) this.speedY *= -1;
this.x += this.speedX;
this.y += this.speedY;
}
draw() {
ctx.fillStyle = "rgba(14, 165, 233, 0.8)";
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
function initParticles() {
for (let i = 0; i < particleCount; i++)
particles.push(new Particle());
}
function connectParticles() {
for (let a = 0; a < particles.length; a++) {
for (let b = a; b < particles.length; b++) {
const distance = Math.sqrt(
Math.pow(particles[a].x - particles[b].x, 2) +
Math.pow(particles[a].y - particles[b].y, 2)
);
if (distance < connectDistance) {
ctx.strokeStyle = `rgba(14, 165, 233, ${
1 - distance / connectDistance
})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(particles[a].x, particles[a].y);
ctx.lineTo(particles[b].x, particles[b].y);
ctx.stroke();
}
}
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p) => {
p.update();
p.draw();
});
connectParticles();
requestAnimationFrame(animate);
}
initParticles();
animate();
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
particles = [];
initParticles();
});
}
// --- 报告系统 ---
const reportModal = document.getElementById("report-modal");
let currentReportType = "";
function showReportModal(type) {
currentReportType = type;
reportModal.style.display = "flex";
}
function hideReportModal() {
reportModal.style.display = "none";
}
function generateReport() {
const quote = document.getElementById("report-quote").value;
const timestamp = new Date().toLocaleString("zh-CN");
const data = lastCalculationData[currentReportType];
if (!data) return;
let inputsHTML = Object.entries(data.inputs)
.map(
([key, value]) =>
`<tr><td class="font-semibold">${key}</td><td>${value}</td></tr>`
)
.join("");
let progressHTML = "";
if (data.progress) {
progressHTML = `<h3 class="text-lg font-bold text-sky-400 mt-4">进度预测</h3><table class="data-table"><thead><tr><th>天数</th><th>日期</th><th>体重</th><th>BMI</th></tr></thead><tbody>${data.progress
.map(
(p) =>
`<tr><td>${p.day}</td><td>${p.date}</td><td>${p.weightKg} kg (${p.weightJin} 斤)</td><td>${p.bmi}</td></tr>`
)
.join("")}</tbody></table>`;
}
let sourceEl, resultHTML;
if (currentReportType === "tdee") {
sourceEl = document.getElementById("tdeeResultReport");
resultHTML = document.getElementById("tdeeResult").innerHTML;
} else if (currentReportType === "wl") {
sourceEl = document.getElementById("wlResultReport");
const chartImage = document
.getElementById("weightLossChart")
.toDataURL("image/png");
resultHTML =
document.getElementById("weightLossResult").innerHTML +
`<img src="${chartImage}" class="mt-4 rounded-lg" />`;
}
sourceEl.innerHTML = `<div class="p-6 bg-gray-800 text-white rounded-lg border border-sky-500"><h2 class="text-2xl font-bold text-sky-400 mb-4" style="font-family: var(--header-font);">健康数据报告</h2><h3 class="text-lg font-bold text-sky-400">输入参数</h3><table class="data-table">${inputsHTML}</table><h3 class="text-lg font-bold text-sky-400 mt-4">计算结果</h3><div class="result-box !mt-0">${resultHTML}</div>${progressHTML}${
quote
? `<div class="mt-4 p-4 border-l-4 border-sky-500 bg-gray-700 italic">"${quote}"</div>`
: ""
}<p class="text-right text-xs text-gray-400 mt-4">${timestamp}</p></div>`;
sourceEl.classList.remove("hidden");
html2canvas(sourceEl, { backgroundColor: "#1f2937", scale: 2 }).then(
(canvas) => {
const link = document.createElement("a");
link.download = `report-${currentReportType}-${Date.now()}.jpg`;
link.href = canvas.toDataURL("image/jpeg", 0.9);
link.click();
sourceEl.classList.add("hidden");
sourceEl.innerHTML = "";
}
);
hideReportModal();
}
// --- 减肥计算器 ---
let weightLossChartInstance;
function calculateWeightLossDate() {
const resultEl = document.getElementById("weightLossResult");
const tableContainer = document.getElementById(
"weightLossTableContainer"
);
const toKg = (val) => (wl_unit === "jin" ? val / 2 : val);
let daysRequired, startWeightKg, targetKg, inputs;
const height = parseFloat(document.getElementById("wl_height").value);
if (isNaN(height) || height <= 0) {
printToTerminal(
[
{
text: "[ERROR] 请在减肥目标计算器中输入有效的身高(cm)。",
className: "warn",
},
],
true
);
return;
}
const heightM = height / 100;
if (weightLossMode === "rate") {
const current = parseFloat(
document.getElementById("wl_currentWeight").value
);
const target = parseFloat(
document.getElementById("wl_targetWeight").value
);
const weeklyLoss = parseFloat(
document.getElementById("wl_weeklyLoss").value
);
startWeightKg = toKg(current);
targetKg = toKg(target);
let weeklyLossKg = toKg(weeklyLoss);
if (
isNaN(startWeightKg) ||
isNaN(targetKg) ||
isNaN(weeklyLossKg) ||
weeklyLossKg <= 0 ||
startWeightKg <= targetKg
) {
return;
}
daysRequired = Math.ceil(
((startWeightKg - targetKg) / weeklyLossKg) * 7
);
inputs = {
身高: `${height} cm`,
当前体重: `${current} ${wl_unit}`,
目标体重: `${target} ${wl_unit}`,
每周减重: `${weeklyLoss} ${wl_unit}`,
};
} else {
const current = parseFloat(
document.getElementById("wc_currentWeight").value
);
const target = parseFloat(
document.getElementById("wc_targetWeight").value
);
const deficit = parseFloat(
document.getElementById("wc_calorieDeficit").value
);
startWeightKg = toKg(current);
targetKg = toKg(target);
if (
isNaN(startWeightKg) ||
isNaN(targetKg) ||
isNaN(deficit) ||
deficit <= 0 ||
startWeightKg <= targetKg
) {
return;
}
daysRequired = Math.ceil(
((startWeightKg - targetKg) * 7700) / deficit
);
inputs = {
身高: `${height} cm`,
当前体重: `${current} ${wl_unit}`,
目标体重: `${target} ${wl_unit}`,
每日能量缺口: `${deficit} kcal`,
};
}
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() + daysRequired);
const targetDateFormatted = targetDate.toISOString().split("T")[0];
resultEl.innerHTML = `预计需要 <strong class="text-sky-400">${daysRequired}</strong> 天, <br>将在 <strong class="text-sky-400">${targetDateFormatted}</strong> 左右达成目标。`;
resultEl.style.display = "block";
document.getElementById("wl_download_btn").style.display = "block";
const weightLossPerDay = (startWeightKg - targetKg) / daysRequired;
const labels = [],
data = [],
progressLog = [];
const terminalLines = [
{
text: ">> system.run(module.Weight_Projection)",
className: "info",
},
];
for (let i = 0; i <= 10; i++) {
const progressDay = Math.round(daysRequired * (i / 10));
const checkpointDate = new Date();
checkpointDate.setDate(checkpointDate.getDate() + progressDay);
const formattedDate = checkpointDate.toISOString().split("T")[0];
const currentWeightKg =
startWeightKg - weightLossPerDay * progressDay;
const currentWeightJin = currentWeightKg * 2;
const bmi = (currentWeightKg / (heightM * heightM)).toFixed(1);
progressLog.push({
day: progressDay,
date: formattedDate,
weightKg: currentWeightKg.toFixed(1),
weightJin: currentWeightJin.toFixed(1),
bmi: bmi,
});
terminalLines.push({
text: `> [DAY ${progressDay} | ${formattedDate}] 预测体重: ${currentWeightKg.toFixed(
1
)} kg (${currentWeightJin.toFixed(1)} 斤), BMI: ${bmi}`,
});
}
lastCalculationData.wl = { inputs, progress: progressLog };
labels.push(...progressLog.map((p) => `D${p.day}`));
data.push(...progressLog.map((p) => p.weightKg));
terminalLines.push({
text: `> [SUCCESS] 预测完成,图表已生成!`,
className: "success",
});
terminalLines.push({ text: ">> 模块运行结束。" });
printToTerminal(terminalLines, true);
// 展示表格UI界面
let tableHTML = `<h3 class="text-lg font-bold text-sky-400 mb-2">进度预测</h3><table class="data-table"><thead><tr><th>天数</th><th>日期</th><th>体重</th><th>BMI</th></tr></thead><tbody>`;
progressLog.forEach((p) => {
tableHTML += `<tr><td>${p.day}</td><td>${p.date}</td><td>${p.weightKg} kg (${p.weightJin} 斤)</td><td>${p.bmi}</td></tr>`;
});
tableHTML += "</tbody></table>";
tableContainer.innerHTML = tableHTML;
tableContainer.style.display = "block";
const ctx = document.getElementById("weightLossChart").getContext("2d");
if (weightLossChartInstance) weightLossChartInstance.destroy();
weightLossChartInstance = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "体重 (kg)",
data,
borderColor: "rgb(14, 165, 233)",
backgroundColor: "rgba(14, 165, 233, 0.2)",
fill: true,
tension: 0.3,
},
],
},
options: {
scales: {
y: { beginAtZero: false, ticks: { color: "#9ca3af" } },
x: { ticks: { color: "#9ca3af" } },
},
plugins: { legend: { labels: { color: "#9ca3af" } } },
},
});
}
// --- 其他模块的JS代码 ---
function toggleTerminal() {
terminalWrapper.classList.toggle("hidden");
mainContent.classList.toggle("full-width");
document.getElementById("toggle-icon-hide").style.display =
terminalWrapper.classList.contains("hidden") ? "none" : "block";
document.getElementById("toggle-icon-show").style.display =
terminalWrapper.classList.contains("hidden") ? "block" : "none";
}
function calculateBMI() {
const height = parseFloat(document.getElementById("bmi_height").value);
const weight = parseFloat(document.getElementById("bmi_weight").value);
const resultEl = document.getElementById("bmiResult");
const script = [
{ text: ">> system.run(module.BMI_Calculator)", className: "info" },
{ text: `> 用户数据已捕获...` },
{ text: `> 身高参数: ${height} cm`, className: "data" },
{ text: `> 体重参数: ${weight} kg`, className: "data" },
];
if (isNaN(height) || isNaN(weight) || height <= 0 || weight <= 0) {
script.push({
text: "[ERROR] 输入数据无效,计算中止。",
className: "warn",
});
printToTerminal(script, true);
resultEl.innerHTML = "请输入有效的身高和体重。";
resultEl.style.display = "block";
return;
}
script.push(
{ text: "> 正在载入核心计算公式: [weight / (height/100)^2]..." },
{ text: "> 计算中..." }
);
const bmi = (weight / Math.pow(height / 100, 2)).toFixed(2);
let category = "";
if (bmi < 18.5) category = "体重过轻";
else if (bmi < 24) category = "正常范围";
else if (bmi < 28) category = "超重";
else category = "肥胖";
script.push({
text: `> 计算完毕... BMI值: ${bmi}`,
className: "success",
});
script.push({ text: `> 分析结果: ${category}`, className: "success" });
script.push({ text: ">> 模块运行结束。" });
printToTerminal(script, true);
resultEl.innerHTML = `您的BMI是 <strong class="text-sky-400 text-xl">${bmi}</strong>, 属于 <strong class="text-sky-400">${category}</strong> 范围。`;
resultEl.style.display = "block";
}
let dateMode = "diff";
function switchDateMode(mode) {
dateMode = mode;
["diff", "countdown", "op"].forEach((m) => {
document.getElementById(`mode-date-${m}`).style.display =
m === mode ? "block" : "none";
document
.getElementById(`tab-date-${m}`)
.classList.toggle("active", m === mode);
});
document.getElementById("dateResult").style.display = "none";
}
function calculateDate() {
const resultEl = document.getElementById("dateResult");
let resultText = "";
if (dateMode === "diff") {
const start = document.getElementById("dd_startDate").value;
const end = document.getElementById("dd_endDate").value;
if (!start || !end) {
resultEl.innerHTML = "请选择两个日期";
resultEl.style.display = "block";
return;
}
const diff = Math.round(
Math.abs(new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24)
);
resultText = `两个日期相差 <strong class="text-sky-400">${diff}</strong> 天`;
} else if (dateMode === "countdown") {
const target = document.getElementById("dc_targetDate").value;
if (!target) {
resultEl.innerHTML = "请选择目标日期";
resultEl.style.display = "block";
return;
}
const today = new Date();
today.setHours(0, 0, 0, 0);
const diff = Math.round(
(new Date(target) - today) / (1000 * 60 * 60 * 24)
);
resultText =
diff >= 0
? `距离目标还有 <strong class="text-sky-400">${diff}</strong> 天`
: `目标已过去 <strong class="text-yellow-400">${-diff}</strong> 天`;
} else {
const start = document.getElementById("do_startDate").value;
const days = parseInt(document.getElementById("do_days").value);
const op = document.getElementById("do_operation").value;
if (!start || isNaN(days)) {
resultEl.innerHTML = "请输入有效日期和天数";
resultEl.style.display = "block";
return;
}
const newDate = new Date(start);
newDate.setDate(newDate.getDate() + (op === "add" ? days : -days));
resultText = `计算结果: <strong class="text-sky-400">${
newDate.toISOString().split("T")[0]
}</strong>`;
}
resultEl.innerHTML = resultText;
resultEl.style.display = "block";
printToTerminal(
[{ text: "[SUCCESS] 日期计算完成。", className: "success" }],
true
);
}
function calculateTDEE(isRecalculation = false) {
const resultEl = document.getElementById("tdeeResult");
let bmr, neat, eat, tdee, inputs;
const script = [
{ text: ">> system.run(module.TDEE_Calculator)", className: "info" },
];
if (isRecalculation) {
bmr = parseFloat(document.getElementById("editable_bmr").value);
neat = parseFloat(document.getElementById("editable_neat").value);
eat = parseFloat(document.getElementById("editable_eat").value);
if (isNaN(bmr) || isNaN(neat) || isNaN(eat)) {
return;
}
script.push({ text: "> 接收到手动调整参数...", className: "data" });
tdee = bmr + neat + eat;
script.push({
text: `> 重新计算... BMR+NEAT+EAT = ${tdee.toFixed(0)}`,
className: "success",
});
inputs = lastCalculationData.tdee.inputs;
} else {
const gender = document.getElementById("tdee_gender").value;
const age = parseInt(document.getElementById("tdee_age").value);
const height = parseFloat(
document.getElementById("tdee_height").value
);
let weight = parseFloat(document.getElementById("tdee_weight").value);
const activity = parseFloat(
document.getElementById("tdee_activity").value
);
eat = parseFloat(document.getElementById("tdee_eat").value) || 0;
if (isNaN(age) || isNaN(height) || isNaN(weight)) {
return;
}
let displayWeight = weight;
if (tdee_unit === "jin") weight /= 2;
if (gender === "male") {
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
} else {
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
}
neat = bmr * (activity - 1);
tdee = bmr + neat + eat;
inputs = {
性别: gender,
年龄: age,
"身高(cm)": height,
体重: `${displayWeight} ${tdee_unit}`,
活动等级: activity,
EAT: eat,
};
script.push({
text: `> BMR计算完成: ${bmr.toFixed(0)} kcal`,
className: "success",
});
script.push({
text: `> NEAT计算完成: ${neat.toFixed(0)} kcal`,
className: "success",
});
script.push({
text: `> 总消耗TDEE计算完成: ${tdee.toFixed(0)} kcal`,
className: "success",
});
}
lastCalculationData.tdee = {
inputs,
results: { bmr, neat, eat, tdee },
};
script.push({ text: ">> 模块运行结束。" });
printToTerminal(script, true);
resultEl.innerHTML = `<div class="p-2"><p class="text-sm text-gray-400">每日总能量消耗 (TDEE)</p><p class="text-3xl font-bold text-sky-400 my-2">${Math.round(
tdee
)} kcal</p><div class="text-left mt-4 text-sm space-y-2"><div class="flex items-center gap-2"><label class="w-28" for="editable_bmr">基础代谢 (BMR):</label><input type="number" id="editable_bmr" value="${Math.round(
bmr
)}"></div><div class="flex items-center gap-2"><label class="w-28" for="editable_neat">日常消耗 (NEAT):</label><input type="number" id="editable_neat" value="${Math.round(
neat
)}"></div><div class="flex items-center gap-2"><label class="w-28" for="editable_eat">运动消耗 (EAT):</label><input type="number" id="editable_eat" value="${Math.round(
eat
)}"></div></div><button onclick="calculateTDEE(true)" class="btn btn-secondary mt-4 text-xs">重新计算 TDEE</button></div>`;
resultEl.style.display = "block";
document.getElementById("tdee_download_btn").style.display = "block";
}
let wl_unit = "jin";
function setWeightLossUnit(unit) {
wl_unit = unit;
document
.querySelectorAll("#weight-loss-view .unit-label")
.forEach((el) => (el.textContent = unit));
document
.getElementById("wl_unit_kg")
.classList.toggle("active", unit === "kg");
document
.getElementById("wl_unit_jin")
.classList.toggle("active", unit === "jin");
}
let weightLossMode = "rate";
function switchWeightLossMode(mode) {
weightLossMode = mode;
document.getElementById("mode-rate").style.display =
mode === "rate" ? "block" : "none";
document.getElementById("mode-calorie").style.display =
mode === "calorie" ? "block" : "none";
document
.getElementById("tab-loss-rate")
.classList.toggle("active", mode === "rate");
document
.getElementById("tab-calorie-gap")
.classList.toggle("active", mode === "calorie");
document.getElementById("weightLossResult").style.display = "none";
}
let tdee_unit = "kg";
function setTdeeUnit(unit) {
tdee_unit = unit;
document
.querySelectorAll("#tdee-view .unit-label")
.forEach((el) => (el.textContent = unit));
document
.getElementById("tdee_unit_kg")
.classList.toggle("active", unit === "kg");
document
.getElementById("tdee_unit_jin")
.classList.toggle("active", unit === "jin");
}
</script>
</body>
</html>
本文来自博客园,作者:舟清颺,转载请注明原文链接:https://www.cnblogs.com/zqingyang/p/19116634