[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes" />
<title>热量计算+体重记录</title>
<style>
/* ===== CSS变量(主题色) ===== */
:root {
--primary-start: #ff7e5f;
--primary-end: #feb47b;
--primary-shadow: rgba(255, 126, 95, 0.25);
--active-bg: #ff7e5f;
--active-color: #fff;
}
/* 主题色方案 12种 */
.theme-1 {
--primary-start: #ff7e5f;
--primary-end: #feb47b;
--primary-shadow: rgba(255, 126, 95, 0.25);
--active-bg: #ff7e5f;
}
.theme-2 {
--primary-start: #11998e;
--primary-end: #38ef7d;
--primary-shadow: rgba(17, 153, 142, 0.25);
--active-bg: #11998e;
}
.theme-3 {
--primary-start: #2193b0;
--primary-end: #6dd5ed;
--primary-shadow: rgba(33, 147, 176, 0.25);
--active-bg: #2193b0;
}
.theme-4 {
--primary-start: #8E2DE2;
--primary-end: #4A00E0;
--primary-shadow: rgba(142, 45, 226, 0.25);
--active-bg: #8E2DE2;
}
.theme-5 {
--primary-start: #f093fb;
--primary-end: #f5576c;
--primary-shadow: rgba(245, 87, 108, 0.25);
--active-bg: #f5576c;
}
.theme-6 {
--primary-start: #a8e063;
--primary-end: #56ab2f;
--primary-shadow: rgba(86, 171, 47, 0.25);
--active-bg: #56ab2f;
}
.theme-7 {
--primary-start: #f2994a;
--primary-end: #f2c94c;
--primary-shadow: rgba(242, 153, 74, 0.25);
--active-bg: #f2994a;
}
.theme-8 {
--primary-start: #00c6fb;
--primary-end: #005bea;
--primary-shadow: rgba(0, 198, 251, 0.25);
--active-bg: #00c6fb;
}
.theme-9 {
--primary-start: #cb2d3e;
--primary-end: #ef473a;
--primary-shadow: rgba(203, 45, 62, 0.25);
--active-bg: #cb2d3e;
}
.theme-10 {
--primary-start: #00b09b;
--primary-end: #96c93d;
--primary-shadow: rgba(0, 176, 155, 0.25);
--active-bg: #00b09b;
}
.theme-11 {
--primary-start: #1a2a6c;
--primary-end: #b21f1f;
--primary-shadow: rgba(26, 42, 108, 0.25);
--active-bg: #1a2a6c;
}
.theme-12 {
--primary-start: #2c3e50;
--primary-end: #3498db;
--primary-shadow: rgba(44, 62, 80, 0.25);
--active-bg: #2c3e50;
}
/* ===== 全局 ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
background: #f2f4f8;
min-height: 100vh;
padding: 8px 10px 100px;
color: #1a1a2e;
overflow-x: hidden;
}
.app {
max-width: 500px;
margin: 0 auto;
position: relative;
}
.card {
background: #fff;
border-radius: 18px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
padding: 14px 14px;
margin-bottom: 12px;
transition: box-shadow 0.2s;
}
/* ===== 顶部栏 ===== */
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, var(--primary-start), var(--primary-end));
border-radius: 18px;
padding: 8px 16px;
color: #fff;
margin-bottom: 10px;
min-height: 48px;
transition: background 0.4s, box-shadow 0.4s;
box-shadow: 0 4px 16px var(--primary-shadow);
}
.top-bar .brand {
display: flex;
align-items: center;
gap: 10px;
}
.top-bar .brand .icon {
font-size: 26px;
line-height: 1;
}
.top-bar .brand .title {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.3px;
}
.top-bar .actions {
display: flex;
gap: 6px;
align-items: center;
}
.top-bar .actions button {
background: rgba(255, 255, 255, 0.20);
border: none;
border-radius: 30px;
padding: 4px 12px;
font-size: 15px;
color: #fff;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
backdrop-filter: blur(2px);
line-height: 1.4;
white-space: nowrap;
font-weight: 500;
}
.top-bar .actions button:active {
background: rgba(255, 255, 255, 0.35);
transform: scale(0.95);
}
/* ===== 日期+餐次+输入 合并卡片 ===== */
.date-meal-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 12px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f2f7;
}
.date-meal-row .date-left {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
flex: 1;
white-space: nowrap;
}
.date-meal-row .date-left .date-icon {
font-size: 18px;
}
.date-meal-row .meal-right-group {
display: flex;
align-items: center;
gap: 6px;
}
.date-meal-row .clear-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: #f0f2f7;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, transform 0.1s;
padding: 0;
}
.date-meal-row .clear-btn:active {
transform: scale(0.9);
background: #e4e7f0;
}
.date-meal-row .meal-select {
padding: 4px 10px;
border-radius: 20px;
border: none;
background: var(--active-bg);
color: var(--active-color);
font-size: 12px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px var(--primary-shadow);
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 24px;
}
.date-meal-row .meal-select:focus {
outline: none;
}
/* 输入区 */
.input-row-wrap {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: nowrap;
}
.input-row-wrap:last-child {
margin-bottom: 0;
}
.input-field {
display: flex;
align-items: center;
gap: 4px;
background: #f7f8fc;
border-radius: 10px;
border: 2px solid transparent;
transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
padding: 0 6px 0 10px;
flex-shrink: 0;
}
.input-field:focus-within {
border-color: var(--active-bg);
background: #fff;
box-shadow: 0 0 0 4px rgba(255, 126, 95, 0.10);
}
.input-field input[type="text"],
.input-field input[type="number"] {
border: none;
outline: none;
background: transparent;
font-size: 15px;
font-weight: 500;
color: #1a1a2e;
padding: 8px 2px 8px 0;
width: 100%;
min-width: 20px;
-moz-appearance: textfield;
}
.input-field input[type="number"]::-webkit-outer-spin-button,
.input-field input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.input-field input::placeholder {
color: #b0b8c4;
font-weight: 400;
font-size: 13px;
}
.input-field .unit-text {
font-size: 13px;
font-weight: 500;
color: #555;
white-space: nowrap;
flex-shrink: 0;
padding-right: 2px;
}
.input-field select {
padding: 6px 2px 6px 0;
font-size: 13px;
font-weight: 500;
color: #2d3436;
background: transparent;
border: none;
outline: none;
cursor: pointer;
flex-shrink: 0;
min-width: 32px;
text-align: right;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0 center;
padding-right: 14px;
}
.input-field select option {
font-weight: 400;
}
/* 各字段宽度 */
.field-name {
flex: 2 1 100px;
min-width: 60px;
}
.field-perunit {
flex: 1 1 80px;
min-width: 70px;
}
.field-energy {
flex: 1 1 60px;
min-width: 50px;
}
.field-serving {
flex: 1 1 80px;
min-width: 70px;
}
.field-serving .input-field {
flex: 1;
}
.field-serving .input-field select {
min-width: 30px;
padding-right: 12px;
font-size: 12px;
}
/* 按钮 */
.btn-save-tag,
.btn-save-record {
flex-shrink: 0;
border: none;
border-radius: 30px;
padding: 6px 14px;
font-size: 14px;
font-weight: 600;
color: #fff;
background: var(--active-bg);
cursor: pointer;
transition: background 0.2s, transform 0.1s;
white-space: nowrap;
box-shadow: 0 2px 8px var(--primary-shadow);
line-height: 1.4;
}
.btn-save-tag:active,
.btn-save-record:active {
transform: scale(0.95);
}
.btn-save-tag {
background: rgba(0, 0, 0, 0.08);
color: #555;
box-shadow: none;
}
.btn-save-tag:active {
background: rgba(0, 0, 0, 0.15);
}
/* ===== 结果区 三列 ===== */
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
align-items: stretch;
}
.result-item {
background: #f7f8fc;
border-radius: 12px;
padding: 10px 6px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 60px;
}
.result-item .rlabel {
font-size: 12px;
font-weight: 500;
color: #888;
}
.result-item .rvalue {
font-size: 24px;
font-weight: 700;
color: #1a1a2e;
line-height: 1.2;
}
.result-item .rvalue .runit {
font-size: 14px;
font-weight: 400;
color: #999;
}
.result-item.highlight-green .rvalue {
color: #00b894;
}
.result-item.highlight-yellow .rvalue {
color: #f9ca24;
}
.result-item.highlight-red .rvalue {
color: #e85d3a;
}
.result-item.target-item .rvalue {
color: #2d7dd2;
}
/* ===== 差值提示(在结果区下方) ===== */
.diff-hint {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f2f7;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #1a1a2e;
min-height: 24px;
}
.diff-hint .diff-value {
font-weight: 700;
}
.diff-hint.success .diff-value {
color: #00b894;
}
.diff-hint.warning .diff-value {
color: #f9ca24;
}
.diff-hint.danger .diff-value {
color: #e85d3a;
}
/* ===== 日历卡片 ===== */
.calendar-wrap {
padding-bottom: 6px;
}
.calendar-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.calendar-nav .cal-month {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
}
.calendar-nav .cal-btn {
background: #f0f2f7;
border: none;
border-radius: 30px;
padding: 2px 14px;
font-size: 14px;
font-weight: 500;
color: #555;
cursor: pointer;
transition: background 0.2s;
}
.calendar-nav .cal-btn:active {
background: #e0e2e7;
}
/* 日历网格 - 紧凑长方形 */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
text-align: center;
}
.calendar-grid .cal-weekday {
font-size: 11px;
font-weight: 500;
color: #999;
padding: 2px 0;
}
.calendar-grid .cal-day {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
border-radius: 8px;
background: #f7f8fc;
font-weight: 500;
color: #1a1a2e;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
padding: 4px 2px 3px;
min-height: 44px;
border: 2px solid transparent;
line-height: 1.2;
position: relative;
}
.calendar-grid .cal-day:active {
transform: scale(0.94);
}
.calendar-grid .cal-day .day-number {
font-size: 15px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 1px;
}
.calendar-grid .cal-day .day-kcal {
font-size: 8px;
font-weight: 700;
color: #e85d3a;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-grid .cal-day .day-weight {
font-size: 8px;
font-weight: 700;
color: #2d7dd2;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-grid .cal-day.status-green {
background: #e6f9ed;
border-color: #4dd4b0;
}
.calendar-grid .cal-day.status-yellow {
background: #fff8e1;
border-color: #f9ca24;
}
.calendar-grid .cal-day.status-red {
background: #fde8e8;
border-color: #e85d3a;
}
.calendar-grid .cal-day.today {
border-color: var(--active-bg);
border-width: 2px;
}
.calendar-grid .cal-day.other-month {
opacity: 0.3;
}
.calendar-grid .cal-day.selected {
box-shadow: 0 0 0 2px var(--active-bg);
}
/* ===== 饮食记录卡片(独立) ===== */
.record-card {
display: none;
margin-top: 0;
}
.record-card.show {
display: block;
}
.record-card .dd-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.record-card .dd-title {
font-size: 14px;
font-weight: 600;
color: #1a1a2e;
}
.record-card .dd-title .dd-total-kcal {
color: #e85d3a;
font-weight: 600;
}
.record-card .dd-weight {
font-size: 13px;
font-weight: 500;
color: #2d7dd2;
white-space: nowrap;
}
.record-card .dd-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
font-size: 14px;
border-bottom: 1px solid #edf0f5;
background: #fafbfc;
border-radius: 6px;
margin-bottom: 4px;
}
.record-card .dd-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.record-card .dd-item .dd-name {
color: #1a1a2e;
font-weight: 500;
}
.record-card .dd-item .dd-kcal {
font-weight: 500;
color: #e85d3a;
margin-left: 8px;
}
.record-card .dd-item .dd-del {
background: none;
border: none;
color: #ccc;
font-size: 16px;
cursor: pointer;
padding: 0 4px;
flex-shrink: 0;
margin-left: 8px;
transition: color 0.2s;
}
.record-card .dd-item .dd-del:active {
color: #e85d3a;
}
.record-card .dd-empty {
color: #b0b8c4;
font-size: 13px;
text-align: center;
padding: 16px 0;
}
.record-card .meal-group-label {
font-weight: 600;
font-size: 13px;
color: #888;
margin: 6px 0 4px 4px;
}
/* ===== 设置弹窗 ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.40);
z-index: 2000;
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(3px);
padding: 20px;
}
.modal-overlay.show {
display: flex;
}
.modal-box {
background: #fff;
border-radius: 24px;
max-width: 420px;
width: 100%;
padding: 20px 18px 18px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.20);
max-height: 90vh;
overflow-y: auto;
}
.modal-box .modal-title {
font-size: 18px;
font-weight: 700;
color: #1a1a2e;
margin-bottom: 14px;
text-align: center;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 12px;
}
.settings-grid .field {
margin-bottom: 0;
}
.settings-grid .field label {
display: block;
font-size: 12px;
font-weight: 500;
color: #2d3436;
margin-bottom: 2px;
}
.settings-grid .field input,
.settings-grid .field select {
width: 100%;
padding: 7px 10px;
border: 1px solid #e0e4ec;
border-radius: 8px;
font-size: 14px;
background: #f7f8fc;
outline: none;
transition: border-color 0.2s;
}
.settings-grid .field input:focus,
.settings-grid .field select:focus {
border-color: var(--active-bg);
background: #fff;
}
.settings-grid .field select {
appearance: auto;
}
.settings-target-full {
grid-column: 1 / -1;
display: flex;
gap: 10px;
margin-top: 2px;
}
.settings-target-full button {
flex: 1;
padding: 7px 0;
border: 2px solid #e0e4ec;
border-radius: 8px;
background: #f7f8fc;
font-size: 14px;
font-weight: 500;
color: #555;
cursor: pointer;
transition: all 0.2s;
}
.settings-target-full button.active {
border-color: var(--active-bg);
background: #fff5f0;
color: var(--active-bg);
}
.settings-target-full button:active {
transform: scale(0.96);
}
.settings-result {
grid-column: 1 / -1;
margin-top: 6px;
padding: 8px 10px;
background: #f0f7ff;
border-radius: 8px;
font-size: 13px;
color: #555;
line-height: 1.6;
}
.settings-result strong {
color: #1a1a2e;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
.modal-actions button {
flex: 1;
padding: 9px 0;
border: none;
border-radius: 30px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.modal-actions .btn-save {
background: var(--active-bg);
color: #fff;
}
.modal-actions .btn-save:active {
opacity: 0.8;
}
.modal-actions .btn-close {
background: #f0f2f7;
color: #555;
}
.modal-actions .btn-close:active {
background: #e0e2e7;
}
.modal-actions .btn-dark-toggle {
flex: 1;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}
.modal-actions .btn-dark-toggle:active {
opacity: 0.85;
}
/* ===== 主题选择弹窗 ===== */
.theme-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 12px 0;
}
.theme-btn {
padding: 12px 0;
border: 3px solid #e0e4ec;
border-radius: 12px;
cursor: pointer;
transition: border-color 0.2s, transform 0.1s;
background: #f7f8fc;
font-size: 12px;
font-weight: 500;
color: #555;
text-align: center;
}
.theme-btn:active {
transform: scale(0.94);
}
.theme-btn.active {
border-color: var(--active-bg);
background: #fff5f0;
}
.theme-btn .color-preview {
width: 100%;
height: 20px;
border-radius: 6px;
margin-bottom: 4px;
}
/* ===== 备份弹窗 ===== */
.backup-section {
margin-bottom: 12px;
}
.backup-section-title {
font-size: 13px;
font-weight: 600;
color: #555;
margin-bottom: 8px;
}
.backup-checkboxes {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
}
.backup-checkbox-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #333;
cursor: pointer;
flex: 1 1 calc(50% - 6px);
min-width: 0;
user-select: none;
}
.backup-checkbox-item.backup-full-row {
flex: 1 1 100%;
}
.backup-checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--active-bg);
flex-shrink: 0;
}
.backup-checkbox-item span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.backup-tip {
font-size: 12px;
color: #999;
text-align: center;
margin-bottom: 4px;
}
/* ===== 体重弹窗 ===== */
.weight-modal .modal-box {
max-width: 460px;
}
.weight-modal .compact-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
margin-bottom: 10px;
}
.weight-modal .compact-row .field-label {
font-size: 14px;
font-weight: 500;
color: #2d3436;
white-space: nowrap;
margin-right: 2px;
}
.weight-modal .compact-row input {
flex: 1;
min-width: 50px;
padding: 8px 8px;
border: 1px solid #e0e4ec;
border-radius: 8px;
font-size: 15px;
background: #f7f8fc;
outline: none;
transition: border-color 0.2s;
width: 100%;
}
.weight-modal .compact-row input:focus {
border-color: var(--active-bg);
background: #fff;
}
.weight-modal .compact-row select {
flex-shrink: 0;
padding: 8px 4px 8px 8px;
border: 1px solid #e0e4ec;
border-radius: 8px;
font-size: 14px;
background: #f7f8fc;
outline: none;
cursor: pointer;
appearance: auto;
}
.weight-modal .compact-row select:focus {
border-color: var(--active-bg);
}
.weight-modal .action-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.weight-modal .action-row .weight-jin {
font-size: 14px;
color: #888;
flex-shrink: 0;
min-width: 60px;
text-align: left;
}
.weight-modal .action-row .modal-actions {
flex: 1;
margin-top: 0;
gap: 8px;
}
.weight-modal .action-row .modal-actions button {
padding: 8px 0;
font-size: 14px;
}
.weight-modal .chart-container {
margin-top: 14px;
border-top: 1px solid #f0f2f7;
padding-top: 12px;
}
.weight-modal .chart-container .chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 4px;
}
.weight-modal .chart-container .chart-header .chart-title {
font-size: 14px;
font-weight: 600;
color: #1a1a2e;
}
.weight-modal .chart-container .chart-header .period-group {
display: flex;
gap: 3px;
flex-wrap: wrap;
}
.weight-modal .chart-container .chart-header .period-group button {
padding: 2px 8px;
border: 1px solid #e0e4ec;
border-radius: 30px;
background: #f7f8fc;
font-size: 11px;
font-weight: 500;
color: #555;
cursor: pointer;
transition: all 0.2s;
}
.weight-modal .chart-container .chart-header .period-group button.active {
background: var(--active-bg);
color: #fff;
border-color: var(--active-bg);
}
.weight-modal .chart-container .chart-header .period-group button:active {
transform: scale(0.94);
}
.weight-modal .chart-container canvas {
width: 100%;
height: 180px;
background: #fafbfc;
border-radius: 10px;
display: block;
}
.weight-modal .chart-container .no-data {
text-align: center;
padding: 30px 0;
color: #b0b8c4;
font-size: 14px;
}
/* ===== 删除确认框 ===== */
.modal-confirm {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 20px;
padding: 20px 24px 18px;
z-index: 1002;
max-width: 300px;
width: 90%;
box-shadow: 0 16px 60px rgba(0, 0, 0, 0.18);
display: none;
text-align: center;
}
.modal-confirm.show {
display: block;
}
.modal-confirm .mc-title {
font-size: 17px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.modal-confirm .mc-desc {
font-size: 13px;
color: #666;
margin-bottom: 14px;
}
.modal-confirm .mc-btns {
display: flex;
gap: 10px;
justify-content: center;
}
.modal-confirm .mc-btns button {
padding: 6px 20px;
border: none;
border-radius: 30px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.modal-confirm .mc-btns .mc-cancel {
background: #f0f2f7;
color: #555;
}
.modal-confirm .mc-btns .mc-confirm {
background: #e85d3a;
color: #fff;
}
.modal-confirm .mc-btns .mc-confirm:active {
background: #d14a2a;
}
.modal-confirm .mc-btns .mc-cancel:active {
background: #e0e2e7;
}
/* ===== Toast 提示 ===== */
.toast-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2000;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.toast-item {
background: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 10px 20px;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
max-width: 80vw;
text-align: center;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toast-item.show {
opacity: 1;
transform: translateY(0);
}
.toast-item.success {
background: rgba(0, 184, 148, 0.9);
}
.toast-item.error {
background: rgba(232, 93, 58, 0.9);
}
.toast-item.warning {
background: rgba(249, 202, 36, 0.9);
color: #333;
}
/* ===== 悬浮标签 ===== */
.tag-fab {
position: fixed;
right: 12px;
bottom: 120px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.tag-fab .fab-btn {
width: 52px;
height: 52px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-start), var(--primary-end));
border: none;
color: #fff;
font-size: 14px;
font-weight: 600;
box-shadow: 0 4px 16px var(--primary-shadow);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.5px;
}
.tag-fab .fab-btn:active {
transform: scale(0.90);
}
.tag-fab .fab-btn .fab-icon {
font-size: 22px;
line-height: 1;
}
/* ===== 标签面板 ===== */
.tag-panel {
position: fixed;
right: -340px;
top: 0;
bottom: 0;
width: 320px;
max-width: 85vw;
background: #fff;
box-shadow: -4px 0 30px rgba(0, 0, 0, 0.12);
z-index: 1001;
transition: right 0.30s cubic-bezier(0.22, 1, 0.36, 1);
display: flex;
flex-direction: column;
border-radius: 20px 0 0 20px;
overflow: hidden;
}
.tag-panel.open {
right: 0;
}
.tag-panel .panel-header {
padding: 14px 16px 10px;
border-bottom: 1px solid #f0f2f7;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.tag-panel .panel-header .ph-title {
font-size: 17px;
font-weight: 700;
color: #1a1a2e;
}
.tag-panel .panel-header .ph-close {
background: none;
border: none;
font-size: 22px;
cursor: pointer;
color: #999;
padding: 0 4px;
}
.tag-panel .panel-body {
flex: 1;
overflow-y: auto;
padding: 10px 12px 16px;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}
.tag-panel .panel-body .tag-item {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
border-radius: 8px;
background: #f7f8fc;
margin-bottom: 4px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
user-select: none;
position: relative;
min-height: 44px;
}
.tag-panel .panel-body .tag-item:active {
transform: scale(0.97);
background: #eef0f7;
}
.tag-panel .panel-body .tag-item.tag-selected {
background: var(--active-bg);
}
.tag-panel .panel-body .tag-item.tag-selected .tag-name {
color: #fff;
}
.tag-panel .panel-body .tag-item.tag-selected span:nth-child(2) {
color: rgba(255,255,255,0.8) !important;
}
.tag-panel .panel-body .tag-item.tag-selected .tag-del {
color: rgba(255,255,255,0.7);
}
.tag-panel .panel-body .tag-item .tag-name {
flex: 1;
font-size: 15px;
font-weight: 500;
color: #1a1a2e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.tag-panel .panel-body .tag-item .tag-del {
background: none;
border: none;
color: #ccc;
font-size: 16px;
cursor: pointer;
padding: 0 4px;
flex-shrink: 0;
pointer-events: auto;
}
.tag-panel .panel-body .tag-item .tag-del:active {
color: #e85d3a;
}
.tag-panel .panel-body .tag-add-area {
margin-top: 10px;
padding: 10px 12px;
background: #f7f8fc;
border-radius: 10px;
border: 2px dashed #dce0e8;
}
.tag-panel .panel-body .tag-add-area .ta-row {
display: flex;
gap: 4px;
margin-bottom: 4px;
flex-wrap: wrap;
}
.tag-panel .panel-body .tag-add-area .ta-row input,
.tag-panel .panel-body .tag-add-area .ta-row select {
flex: 1;
min-width: 50px;
padding: 6px 8px;
border: 1px solid #e0e4ec;
border-radius: 6px;
font-size: 13px;
background: #fff;
outline: none;
transition: border-color 0.2s;
}
.tag-panel .panel-body .tag-add-area .ta-row input:focus,
.tag-panel .panel-body .tag-add-area .ta-row select:focus {
border-color: var(--active-bg);
}
.tag-panel .panel-body .tag-add-area .ta-row input::placeholder {
color: #b0b8c4;
}
.tag-panel .panel-body .tag-add-area .ta-row .ta-btn {
padding: 6px 14px;
border: none;
border-radius: 6px;
background: var(--active-bg);
color: #fff;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.tag-panel .panel-body .tag-add-area .ta-row .ta-btn:active {
opacity: 0.8;
}
.tag-panel .panel-body .tag-add-area .ta-msg {
font-size: 12px;
color: #e85d3a;
min-height: 18px;
padding-left: 4px;
}
.tag-panel .panel-body .tag-add-area .ta-msg.success {
color: #00b894;
}
.tag-panel .panel-body .tag-count {
font-size: 12px;
color: #999;
text-align: right;
padding: 2px 4px 6px;
}
.overlay-tag {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.25);
z-index: 1000;
display: none;
backdrop-filter: blur(2px);
}
.overlay-tag.show {
display: block;
}
/* ===== 暗色模式适配 ===== */
/* 手动切换暗黑模式 */
body.dark-mode {
background: #12121a;
color: #e8e8f0;
}
body.dark-mode .card {
background: #1e1e2a;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
body.dark-mode .date-meal-row {
background: transparent;
border-bottom-color: #2a2a3a;
}
body.dark-mode .date-meal-row .date-left {
color: #e8e8f0;
}
body.dark-mode .date-meal-row .meal-group .meal-btn {
background: #2a2a38;
color: #aaa;
}
body.dark-mode .date-meal-row .meal-group .meal-btn.active {
background: var(--active-bg);
color: #fff;
}
body.dark-mode .input-field {
background: #2a2a38;
}
body.dark-mode .input-field input[type="text"],
body.dark-mode .input-field input[type="number"] {
color: #e8e8f0;
}
body.dark-mode .input-field input::placeholder {
color: #6a6a7a;
}
body.dark-mode .input-field .unit-text {
color: #aaa;
}
body.dark-mode .input-field select {
color: #e8e8f0;
}
body.dark-mode .input-field select option {
background: #2a2a38;
color: #e8e8f0;
}
body.dark-mode .btn-save-tag {
color: #ccc;
background: rgba(255, 255, 255, 0.08);
}
body.dark-mode .result-item {
background: #2a2a38;
}
body.dark-mode .result-item .rlabel {
color: #7a7a8a;
}
body.dark-mode .result-item .rvalue {
color: #e8e8f0;
}
body.dark-mode .result-item.highlight-green .rvalue {
color: #4dd4b0;
}
body.dark-mode .result-item.highlight-yellow .rvalue {
color: #f9ca24;
}
body.dark-mode .result-item.highlight-red .rvalue {
color: #ff6a4a;
}
body.dark-mode .result-item.target-item .rvalue {
color: #5b9bd5;
}
body.dark-mode .diff-hint {
border-top-color: #2a2a3a;
color: #e8e8f0;
}
body.dark-mode .diff-hint.success .diff-value {
color: #4dd4b0;
}
body.dark-mode .diff-hint.warning .diff-value {
color: #f9ca24;
}
body.dark-mode .diff-hint.danger .diff-value {
color: #ff6a4a;
}
body.dark-mode .calendar-grid .cal-day {
background: #2a2a38;
color: #e8e8f0;
}
body.dark-mode .calendar-grid .cal-day .day-kcal {
color: #ff6a4a;
}
body.dark-mode .calendar-grid .cal-day.status-green {
background: #1a2a26;
border-color: #4dd4b0;
}
body.dark-mode .calendar-grid .cal-day.status-yellow {
background: #2a2a1a;
border-color: #f9ca24;
}
body.dark-mode .calendar-grid .cal-day.status-red {
background: #2a1a1a;
border-color: #ff6a4a;
}
body.dark-mode .calendar-grid .cal-day.today {
border-color: var(--active-bg);
}
body.dark-mode .calendar-grid .cal-day.selected {
box-shadow: 0 0 0 2px var(--active-bg);
}
body.dark-mode .calendar-grid .cal-day.other-month {
opacity: 0.2;
}
body.dark-mode .calendar-grid .cal-day .day-weight {
color: #5b9bd5;
}
body.dark-mode .calendar-nav .cal-btn {
background: #2a2a38;
color: #ccc;
}
body.dark-mode .record-card .dd-item {
background: #2a2a38;
border-bottom-color: #3a3a4a;
}
body.dark-mode .record-card .dd-item .dd-name {
color: #e8e8f0;
}
body.dark-mode .record-card .dd-weight {
color: #5b9bd5;
}
body.dark-mode .modal-box {
background: #1e1e2a;
}
body.dark-mode .modal-box .modal-title {
color: #e8e8f0;
}
body.dark-mode .settings-grid .field label {
color: #d0d0e0;
}
body.dark-mode .settings-grid .field input,
body.dark-mode .settings-grid .field select {
background: #2a2a38;
border-color: #3a3a4a;
color: #e8e8f0;
}
body.dark-mode .settings-grid .field input:focus,
body.dark-mode .settings-grid .field select:focus {
border-color: var(--active-bg);
background: #1e1e2a;
}
body.dark-mode .settings-target-full button {
background: #2a2a38;
border-color: #3a3a4a;
color: #aaa;
}
body.dark-mode .settings-target-full button.active {
border-color: var(--active-bg);
background: #2a1e1a;
color: var(--active-bg);
}
body.dark-mode .settings-result {
background: #1a2a3a;
color: #aaa;
}
body.dark-mode .settings-result strong {
color: #e8e8f0;
}
body.dark-mode .modal-actions .btn-save {
background: var(--active-bg);
}
body.dark-mode .modal-actions .btn-close {
background: #2a2a38;
color: #ccc;
}
body.dark-mode .modal-confirm {
background: #1e1e2a;
}
body.dark-mode .modal-confirm .mc-title {
color: #e8e8f0;
}
body.dark-mode .modal-confirm .mc-desc {
color: #aaa;
}
body.dark-mode .modal-confirm .mc-btns .mc-cancel {
background: #2a2a38;
color: #ccc;
}
body.dark-mode .backup-section-title {
color: #aaa;
}
body.dark-mode .backup-checkbox-item {
color: #d0d0e0;
}
body.dark-mode .backup-tip {
color: #7a7a8a;
}
body.dark-mode .tag-panel {
background: #1e1e2a;
}
body.dark-mode .tag-panel .panel-header {
border-bottom-color: #2a2a3a;
}
body.dark-mode .tag-panel .panel-header .ph-title {
color: #e8e8f0;
}
body.dark-mode .tag-panel .panel-body .tag-item {
background: #2a2a38;
}
body.dark-mode .tag-panel .panel-body .tag-item:active {
background: #3a3a4a;
}
body.dark-mode .tag-panel .panel-body .tag-item.tag-selected {
background: var(--active-bg);
}
body.dark-mode .tag-panel .panel-body .tag-item.tag-selected .tag-name {
color: #fff;
}
body.dark-mode .tag-panel .panel-body .tag-item .tag-name {
color: #e8e8f0;
}
body.dark-mode .tag-panel .panel-body .tag-add-area {
background: #2a2a38;
border-color: #3a3a4a;
}
body.dark-mode .tag-panel .panel-body .tag-add-area .ta-row input,
body.dark-mode .tag-panel .panel-body .tag-add-area .ta-row select {
background: #1e1e2a;
border-color: #3a3a4a;
color: #e8e8f0;
}
body.dark-mode .tag-panel .panel-body .tag-add-area .ta-row input::placeholder {
color: #6a6a7a;
}
body.dark-mode .tag-panel .panel-body .tag-count {
color: #6a6a7a;
}
body.dark-mode .overlay-tag {
background: rgba(0, 0, 0, 0.5);
}
body.dark-mode .tag-fab .fab-btn {
background: linear-gradient(135deg, var(--primary-start), var(--primary-end));
}
body.dark-mode .weight-modal .chart-container canvas {
background: #1e1e2a;
}
body.dark-mode .weight-modal .chart-container .chart-header .period-group button {
background: #2a2a38;
border-color: #3a3a4a;
color: #aaa;
}
body.dark-mode .weight-modal .chart-container .chart-header .period-group button.active {
background: var(--active-bg);
border-color: var(--active-bg);
color: #fff;
}
body.dark-mode .weight-modal .compact-row select {
background: #2a2a38;
border-color: #3a3a4a;
color: #e8e8f0;
}
body.dark-mode .weight-modal .compact-row input {
background: #2a2a38;
border-color: #3a3a4a;
color: #e8e8f0;
}
body.dark-mode .weight-modal .compact-row input:focus {
border-color: var(--active-bg);
background: #1e1e2a;
}
/* ===== 响应式 ===== */
@media (max-width: 420px) {
body {
padding: 6px 6px 80px;
}
.card {
padding: 10px 10px;
border-radius: 14px;
}
.top-bar {
padding: 6px 12px;
min-height: 42px;
border-radius: 14px;
}
.top-bar .brand .icon {
font-size: 22px;
}
.top-bar .brand .title {
font-size: 16px;
}
.top-bar .actions button {
font-size: 13px;
padding: 3px 8px;
}
.date-meal-row .date-left {
font-size: 13px;
}
.date-meal-row .meal-group .meal-btn {
font-size: 11px;
padding: 2px 8px;
}
.input-row-wrap {
gap: 6px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.input-field {
padding: 0 4px 0 8px;
border-radius: 8px;
}
.input-field input[type="text"],
.input-field input[type="number"] {
font-size: 14px;
padding: 6px 2px 6px 0;
}
.input-field .unit-text {
font-size: 12px;
}
.input-field select {
font-size: 12px;
padding: 4px 2px 4px 0;
min-width: 24px;
padding-right: 12px;
}
.field-name {
flex: 2 1 70px;
min-width: 50px;
}
.field-perunit {
flex: 1 1 65px;
min-width: 55px;
}
.field-energy {
flex: 1 1 50px;
min-width: 45px;
}
.field-serving {
flex: 1 1 70px;
min-width: 60px;
}
.btn-save-tag,
.btn-save-record {
font-size: 12px;
padding: 4px 10px;
}
.result-grid {
grid-template-columns: 1fr 1fr 1fr;
gap: 4px;
}
.result-item {
padding: 6px 4px;
min-height: 48px;
}
.result-item .rvalue {
font-size: 20px;
}
.diff-hint {
font-size: 13px;
}
.calendar-grid .cal-day {
min-height: 36px;
padding: 3px 2px 2px;
}
.calendar-grid .cal-day .day-number {
font-size: 13px;
}
.calendar-grid .cal-day .day-kcal {
font-size: 7px;
}
.calendar-grid .cal-day .day-weight {
font-size: 7px;
}
.record-card .dd-item {
font-size: 13px;
padding: 4px 6px;
}
.tag-panel {
width: 270px;
max-width: 80vw;
}
.weight-modal .modal-box {
padding: 16px 12px;
}
.weight-modal .compact-row {
flex-wrap: nowrap;
gap: 4px;
}
.weight-modal .compact-row input {
min-width: 40px;
padding: 6px 4px;
font-size: 14px;
}
.weight-modal .compact-row select {
font-size: 12px;
padding: 6px 4px;
}
.weight-modal .chart-container canvas {
height: 130px;
}
.weight-modal .action-row {
flex-wrap: nowrap;
gap: 6px;
}
.weight-modal .action-row .weight-jin {
font-size: 12px;
min-width: 40px;
}
.weight-modal .action-row .modal-actions button {
font-size: 13px;
padding: 6px 0;
}
.settings-grid {
grid-template-columns: 1fr 1fr;
gap: 6px 8px;
}
.settings-grid .field label {
font-size: 11px;
}
.settings-grid .field input,
.settings-grid .field select {
font-size: 13px;
padding: 5px 8px;
}
.settings-target-full button {
font-size: 13px;
padding: 5px 0;
}
.settings-result {
font-size: 12px;
padding: 6px 8px;
}
.modal-box {
padding: 16px 14px;
}
.modal-box .modal-title {
font-size: 16px;
margin-bottom: 10px;
}
.theme-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
}
@media (min-width: 769px) {
body {
padding: 20px 20px 120px;
display: flex;
justify-content: center;
}
.app {
max-width: 540px;
}
.tag-fab {
right: 30px;
bottom: 140px;
}
.tag-panel {
width: 360px;
max-width: 30vw;
border-radius: 24px 0 0 24px;
}
.result-item .rvalue {
font-size: 28px;
}
.calendar-grid .cal-day {
min-height: 48px;
padding: 4px 2px 3px;
}
.calendar-grid .cal-day .day-number {
font-size: 16px;
}
.calendar-grid .cal-day .day-kcal {
font-size: 9px;
}
.calendar-grid .cal-day .day-weight {
font-size: 9px;
}
.weight-modal .chart-container canvas {
height: 200px;
}
.input-field input[type="text"],
.input-field input[type="number"] {
font-size: 16px;
padding: 10px 4px 10px 0;
}
}
</style>
</head>
<body>
<div class="app" id="app">
<!-- ===== 顶部栏 ===== -->
<div class="top-bar" id="topBar">
<div class="brand">
<span class="title">热量计算/体重记录</span>
</div>
<div class="actions">
<button id="weightToggle">体重</button>
<button id="backupToggle">备份</button>
<button id="settingsToggle">设置</button>
<button id="themeToggle">主题</button>
</div>
</div>
<!-- ===== 卡片1:日期+餐次+输入 ===== -->
<div class="card">
<!-- 日期+餐次 -->
<div class="date-meal-row">
<div class="date-left">
<span class="date-icon">📅</span>
<span id="dateDisplay">--</span>
</div>
<div class="meal-right-group">
<button class="clear-btn" id="clearInputsBtn" title="清空输入">🧹</button>
<select id="mealSelect" class="meal-select">
<option value="早餐">早餐</option>
<option value="中餐">中餐</option>
<option value="晚餐">晚餐</option>
<option value="加餐">加餐</option>
</select>
</div>
</div>
<!-- 输入行1:食物名称 + 每份量 + 存为标签 -->
<div class="input-row-wrap">
<div class="field-name">
<div class="input-field">
<input type="text" id="foodName" placeholder="食物名称" />
</div>
</div>
<div class="field-perunit">
<div class="input-field">
<input type="number" id="defaultWeight" value="100" min="0.1" step="1" />
<select id="defaultUnit">
<option value="g">g</option>
<option value="ml">ml</option>
</select>
</div>
</div>
<button class="btn-save-tag" id="saveTagQuick">存为标签</button>
</div>
<!-- 输入行2:能量 + 食用量 + 存储记录 -->
<div class="input-row-wrap">
<div class="field-energy">
<div class="input-field">
<input type="number" id="energy" placeholder="能量" min="0" step="1" />
<span class="unit-text">kJ</span>
</div>
</div>
<div class="field-serving">
<div class="input-field">
<input type="number" id="serving" placeholder="食用量" min="0" step="1" />
<select id="servingUnit">
<option value="g">g</option>
<option value="kg">kg</option>
<option value="ml">ml</option>
</select>
</div>
</div>
<button class="btn-save-record" id="saveRecordBtn">存储记录</button>
</div>
</div>
<!-- ===== 卡片2:结果区 三列 + 差值提示 ===== -->
<div class="card">
<div class="result-grid" id="resultGrid">
<div class="result-item" id="currentMealItem">
<div class="rlabel">本餐热量</div>
<div class="rvalue" id="currentMealKcal"><span class="placeholder">--</span><span class="runit">千卡</span></div>
</div>
<div class="result-item" id="todayTotalItem">
<div class="rlabel">今日总热量</div>
<div class="rvalue" id="todayTotalKcal"><span class="placeholder">0</span><span class="runit">千卡</span></div>
</div>
<div class="result-item target-item" id="targetItem">
<div class="rlabel">目标热量</div>
<div class="rvalue" id="targetKcal"><span class="placeholder">--</span><span class="runit">千卡</span></div>
</div>
</div>
<!-- 差值提示放在结果区下方 -->
<div class="diff-hint" id="diffHint"></div>
</div>
<!-- ===== 卡片3:日历 ===== -->
<div class="card calendar-wrap">
<div class="calendar-nav">
<button class="cal-btn" id="calPrev">‹</button>
<span class="cal-month" id="calMonth">--</span>
<button class="cal-btn" id="calNext">›</button>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
</div>
<!-- ===== 卡片4:饮食记录(点击日期后显示) ===== -->
<div class="card record-card" id="recordCard">
<div class="dd-header">
<div class="dd-title" id="ddTitle">📋 详细记录</div>
<div class="dd-weight" id="ddWeight"></div>
</div>
<div id="ddList"></div>
</div>
</div>
<!-- ===== 设置弹窗 ===== -->
<div class="modal-overlay" id="settingsModal">
<div class="modal-box">
<div class="modal-title">⚙️ 个人基础信息</div>
<div class="settings-grid">
<div class="field">
<label>年龄</label>
<input type="number" id="userAge" placeholder="岁" min="10" max="120" />
</div>
<div class="field">
<label>性别</label>
<select id="userGenderSelect">
<option value="male">男</option>
<option value="female">女</option>
</select>
</div>
<div class="field">
<label>身高 (cm)</label>
<input type="number" id="userHeight" placeholder="cm" min="100" max="250" step="0.1" />
</div>
<div class="field">
<label>运动量</label>
<select id="userActivity">
<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 class="field">
<label>体重 (kg)</label>
<input type="number" id="userWeight" placeholder="kg" min="20" max="300" step="0.1" />
</div>
<div class="field" style="display:flex;align-items:flex-end;">
<div style="width:100%;">
<label>目标</label>
<div class="settings-target-full" id="targetGroup">
<button data-target="maintain">维持</button>
<button data-target="lose" class="active">减肥</button>
</div>
</div>
</div>
<div class="settings-result" id="calcInfo">
<strong>估算</strong> · BMR: <span id="bmrDisplay">--</span> 千卡 | TDEE: <span id="tdeeDisplay">--</span> 千卡<br />
<span id="targetSuggestion">建议每日摄入 <strong>--</strong> 千卡</span>
</div>
</div>
<div class="modal-actions">
<button class="btn-close" id="settingsClose">取消</button>
<button class="btn-save" id="settingsSave">保存</button>
</div>
</div>
</div>
<!-- ===== 主题选择弹窗 ===== -->
<div class="modal-overlay" id="themeModal">
<div class="modal-box">
<div class="modal-title">🎨 选择主题色</div>
<div class="theme-grid" id="themeGrid"></div>
<div class="modal-actions">
<button class="btn-dark-toggle" id="darkModeToggle">🌙 启用暗黑模式</button>
<button class="btn-close" id="themeClose">关闭</button>
</div>
</div>
</div>
<!-- ===== 体重弹窗 ===== -->
<div class="modal-overlay weight-modal" id="weightModal">
<div class="modal-box">
<div class="modal-title">⚖️ 体重记录</div>
<div class="compact-row">
<span class="field-label">体重</span>
<input type="number" id="weightInput" placeholder="kg" min="20" max="300" step="0.1" />
<span style="font-size:14px;color:#888;margin:0 2px;">kg</span>
<span style="font-size:14px;color:#888;margin:0 2px;">·</span>
<select id="weightTime">
<option value="自动">自动</option>
<option value="早晨">早晨</option>
<option value="中午">中午</option>
<option value="晚上">晚上</option>
<option value="其他">其他</option>
</select>
</div>
<div class="action-row">
<span class="weight-jin" id="weightJinDisplay"></span>
<div class="modal-actions">
<button class="btn-close" id="weightClose">取消</button>
<button class="btn-save" id="weightSave">保存</button>
</div>
</div>
<div class="chart-container" id="weightChartContainer">
<div class="chart-header">
<span class="chart-title">📉 体重趋势</span>
<div class="period-group" id="weightPeriodGroup">
<button data-period="7" class="active">7天</button>
<button data-period="15">半月</button>
<button data-period="30">1月</button>
<button data-period="90">3月</button>
<button data-period="180">半年</button>
<button data-period="365">1年</button>
</div>
</div>
<canvas id="weightChartCanvas"></canvas>
<div class="no-data" id="weightNoData" style="display:none;">暂无体重数据</div>
</div>
</div>
</div>
<!-- ===== 备份弹窗 ===== -->
<div class="modal-overlay" id="backupModal">
<div class="modal-box">
<div class="modal-title">💾 数据备份</div>
<div class="backup-section">
<div class="backup-section-title">选择导出内容</div>
<div class="backup-checkboxes">
<label class="backup-checkbox-item">
<input type="checkbox" id="backupPersonal" checked />
<span>个人基本信息</span>
</label>
<label class="backup-checkbox-item">
<input type="checkbox" id="backupTheme" checked />
<span>主题配置</span>
</label>
<label class="backup-checkbox-item">
<input type="checkbox" id="backupTags" checked />
<span>自定义标签</span>
</label>
<label class="backup-checkbox-item backup-full-row">
<input type="checkbox" id="backupRecords" checked />
<span>全部历史记录(饮食+热量)</span>
</label>
<label class="backup-checkbox-item backup-full-row">
<input type="checkbox" id="backupWeight" checked />
<span>体重记录</span>
</label>
</div>
</div>
<div class="backup-tip">导入数据将覆盖当前对应分类的数据</div>
<input type="file" id="backupFileInput" accept=".json,.txt" style="display:none;" />
<div class="modal-actions">
<button class="btn-close" id="backupImportBtn">导入数据</button>
<button class="btn-save" id="backupExportBtn">导出数据</button>
</div>
</div>
</div>
<!-- ===== 遮罩 (标签) ===== -->
<div class="overlay-tag" id="overlayTag"></div>
<!-- ===== 删除确认框 ===== -->
<div class="modal-confirm" id="confirmModal">
<div class="mc-title">⚠️ 确认删除</div>
<div class="mc-desc" id="confirmDesc">确定要删除该记录吗?</div>
<div class="mc-btns">
<button class="mc-cancel" id="confirmCancel">取消</button>
<button class="mc-confirm" id="confirmOk">删除</button>
</div>
</div>
<!-- ===== Toast 提示 ===== -->
<div class="toast-container" id="toastContainer"></div>
<!-- ===== 悬浮标签 ===== -->
<div class="tag-fab" id="tagFab">
<button class="fab-btn" id="fabToggle"><span class="fab-icon">🏷️</span></button>
</div>
<!-- ===== 标签面板 ===== -->
<div class="tag-panel" id="tagPanel">
<div class="panel-header">
<span class="ph-title">🏷️ 常用标签</span>
<button class="ph-close" id="panelClose">✕</button>
</div>
<div class="panel-body" id="panelBody">
<div class="tag-count" id="tagCount">0 / 50</div>
<div id="tagList"></div>
</div>
</div>
<script>
(function() {
'use strict';
// ============================================================
// 数据层
// ============================================================
const STORAGE_KEY = 'food_cal_data_v12';
const SETTINGS_KEY = 'user_settings_v5';
const THEME_KEY = 'user_theme_v2';
const INPUT_CACHE_KEY = 'food_input_cache_v1';
const DEFAULT_TAGS = [
{ name: '鸡胸肉', defaultWeight: 100, unit: 'g', energy: 550 },
{ name: '牛肉(瘦)', defaultWeight: 100, unit: 'g', energy: 480 },
{ name: '猪里脊', defaultWeight: 100, unit: 'g', energy: 520 },
{ name: '三文鱼', defaultWeight: 100, unit: 'g', energy: 600 },
{ name: '鳕鱼', defaultWeight: 100, unit: 'g', energy: 370 },
{ name: '鸡蛋(全蛋)', defaultWeight: 50, unit: 'g', energy: 315 },
{ name: '蛋白', defaultWeight: 50, unit: 'g', energy: 85 },
{ name: '脱脂牛奶', defaultWeight: 100, unit: 'ml', energy: 140 },
{ name: '纯牛奶', defaultWeight: 100, unit: 'ml', energy: 270 },
{ name: '希腊酸奶', defaultWeight: 100, unit: 'g', energy: 320 },
{ name: '燕麦片', defaultWeight: 100, unit: 'g', energy: 1500 },
{ name: '全麦面包', defaultWeight: 100, unit: 'g', energy: 1000 },
{ name: '糙米', defaultWeight: 100, unit: 'g', energy: 480 },
{ name: '藜麦', defaultWeight: 100, unit: 'g', energy: 500 },
{ name: '红薯', defaultWeight: 100, unit: 'g', energy: 360 },
{ name: '玉米', defaultWeight: 100, unit: 'g', energy: 350 },
{ name: '西兰花', defaultWeight: 100, unit: 'g', energy: 140 },
{ name: '菠菜', defaultWeight: 100, unit: 'g', energy: 100 },
{ name: '生菜', defaultWeight: 100, unit: 'g', energy: 60 },
{ name: '番茄', defaultWeight: 100, unit: 'g', energy: 80 },
{ name: '黄瓜', defaultWeight: 100, unit: 'g', energy: 65 },
{ name: '胡萝卜', defaultWeight: 100, unit: 'g', energy: 110 },
{ name: '苹果', defaultWeight: 100, unit: 'g', energy: 218 },
{ name: '香蕉', defaultWeight: 100, unit: 'g', energy: 381 },
{ name: '蓝莓', defaultWeight: 100, unit: 'g', energy: 240 },
{ name: '草莓', defaultWeight: 100, unit: 'g', energy: 130 },
{ name: '牛油果', defaultWeight: 100, unit: 'g', energy: 670 },
{ name: '豆腐', defaultWeight: 100, unit: 'g', energy: 320 },
{ name: '豆浆(无糖)', defaultWeight: 100, unit: 'ml', energy: 120 },
{ name: '奶酪(低脂)', defaultWeight: 100, unit: 'g', energy: 500 },
];
function getDefaultData() {
const now = Date.now();
return {
tags: DEFAULT_TAGS.map((t, i) => ({ ...t, id: 'tag_' + now + '_' + i, lastUsed: now - i })),
records: {},
weightRecords: {}
};
}
let data = loadData();
let userSettings = loadSettings();
let currentTheme = loadTheme();
function loadData() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && parsed.tags && parsed.records) {
if (!parsed.weightRecords) parsed.weightRecords = {};
const now = Date.now();
parsed.tags.forEach((t, i) => {
if (!t.unit) t.unit = 'g';
if (!t.lastUsed) t.lastUsed = now - i;
});
return parsed;
}
}
} catch (_) {}
return getDefaultData();
}
function saveData() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (_) {}
}
function loadSettings() {
try {
const raw = localStorage.getItem(SETTINGS_KEY);
if (raw) {
const s = JSON.parse(raw);
if (s && typeof s === 'object') return s;
}
} catch (_) {}
return {
age: '',
gender: 'male',
height: '',
weight: '',
activity: 1.2,
target: 'lose'
};
}
function saveSettings(settings) {
userSettings = settings;
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch (_) {}
}
function loadTheme() {
try {
const t = localStorage.getItem(THEME_KEY);
if (t) return t;
} catch (_) {}
return 'theme-1';
}
function saveTheme(theme) {
currentTheme = theme;
try {
localStorage.setItem(THEME_KEY, theme);
} catch (_) {}
}
// ============================================================
// 食品输入缓存
// ============================================================
function loadInputCache() {
try {
const raw = localStorage.getItem(INPUT_CACHE_KEY);
if (raw) {
const c = JSON.parse(raw);
if (c && typeof c === 'object') return c;
}
} catch (_) {}
return null;
}
function saveInputCache() {
const cache = {
foodName: document.getElementById('foodName').value,
defaultWeight: document.getElementById('defaultWeight').value,
defaultUnit: document.getElementById('defaultUnit').value,
energy: document.getElementById('energy').value,
serving: document.getElementById('serving').value,
servingUnit: document.getElementById('servingUnit').value
};
try {
localStorage.setItem(INPUT_CACHE_KEY, JSON.stringify(cache));
} catch (_) {}
}
function applyInputCache() {
const cache = loadInputCache();
if (cache) {
if (cache.foodName !== undefined) document.getElementById('foodName').value = cache.foodName;
if (cache.defaultWeight !== undefined) document.getElementById('defaultWeight').value = cache.defaultWeight;
if (cache.defaultUnit !== undefined) document.getElementById('defaultUnit').value = cache.defaultUnit;
if (cache.energy !== undefined) document.getElementById('energy').value = cache.energy;
if (cache.serving !== undefined) document.getElementById('serving').value = cache.serving;
if (cache.servingUnit !== undefined) document.getElementById('servingUnit').value = cache.servingUnit;
}
}
// ============================================================
// 数据导出/导入
// ============================================================
function exportBackupData(options) {
const exportData = {
version: 1,
exportTime: new Date().toISOString(),
app: '热量计算+体重记录'
};
if (options.personal) {
exportData.personal = userSettings;
}
if (options.theme) {
exportData.theme = currentTheme;
}
if (options.tags) {
exportData.tags = data.tags;
}
if (options.records) {
exportData.records = data.records;
}
if (options.weight) {
exportData.weightRecords = data.weightRecords;
}
return exportData;
}
function formatBackupFilename() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return '热量计算加体重记录_' + y + m + day + '_' + h + min + s + '.json';
}
function downloadFile(content, filename) {
const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => { URL.revokeObjectURL(url); }, 100);
}
function doExport() {
const options = {
personal: document.getElementById('backupPersonal').checked,
theme: document.getElementById('backupTheme').checked,
tags: document.getElementById('backupTags').checked,
records: document.getElementById('backupRecords').checked,
weight: document.getElementById('backupWeight').checked
};
const hasAny = options.personal || options.theme || options.tags || options.records || options.weight;
if (!hasAny) {
alert('请至少选择一项导出内容');
return;
}
const exportData = exportBackupData(options);
const content = JSON.stringify(exportData, null, 2);
const filename = formatBackupFilename();
downloadFile(content, filename);
}
function importBackupData(parsedData) {
let importedCount = 0;
if (parsedData.personal && typeof parsedData.personal === 'object') {
saveSettings(parsedData.personal);
importedCount++;
}
if (parsedData.theme && typeof parsedData.theme === 'string') {
applyTheme(parsedData.theme);
importedCount++;
}
if (parsedData.tags && Array.isArray(parsedData.tags)) {
data.tags = parsedData.tags;
saveData();
importedCount++;
}
if (parsedData.records && typeof parsedData.records === 'object') {
data.records = parsedData.records;
saveData();
importedCount++;
}
if (parsedData.weightRecords && typeof parsedData.weightRecords === 'object') {
data.weightRecords = parsedData.weightRecords;
saveData();
importedCount++;
}
return importedCount;
}
function handleImportFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const content = e.target.result;
const parsed = JSON.parse(content);
if (!parsed || typeof parsed !== 'object') {
alert('文件格式不正确');
return;
}
const count = importBackupData(parsed);
if (count === 0) {
alert('未找到可导入的数据');
return;
}
alert('✅ 导入成功,共导入 ' + count + ' 类数据');
closeBackupModal();
renderTags();
renderCalendar();
computePreview();
if (selectedDateStr) {
renderRecordCard(selectedDateStr);
}
} catch (err) {
alert('导入失败:文件格式错误');
}
};
reader.onerror = function() {
alert('读取文件失败');
};
reader.readAsText(file, 'UTF-8');
}
// ============================================================
// 主题
// ============================================================
const THEMES = [
{ id: 'theme-1', name: '橙红', colors: ['#ff7e5f', '#feb47b'] },
{ id: 'theme-2', name: '翠绿', colors: ['#11998e', '#38ef7d'] },
{ id: 'theme-3', name: '天蓝', colors: ['#2193b0', '#6dd5ed'] },
{ id: 'theme-4', name: '紫罗兰', colors: ['#8E2DE2', '#4A00E0'] },
{ id: 'theme-5', name: '粉红', colors: ['#f093fb', '#f5576c'] },
{ id: 'theme-6', name: '青草', colors: ['#a8e063', '#56ab2f'] },
{ id: 'theme-7', name: '日落', colors: ['#f2994a', '#f2c94c'] },
{ id: 'theme-8', name: '冰川', colors: ['#00c6fb', '#005bea'] },
{ id: 'theme-9', name: '玫瑰', colors: ['#cb2d3e', '#ef473a'] },
{ id: 'theme-10', name: '薄荷', colors: ['#00b09b', '#96c93d'] },
{ id: 'theme-11', name: '星空', colors: ['#1a2a6c', '#b21f1f'] },
{ id: 'theme-12', name: '碳灰', colors: ['#2c3e50', '#3498db'] },
];
function applyTheme(themeId) {
const root = document.documentElement;
root.className = themeId;
const theme = THEMES.find(t => t.id === themeId);
if (theme) {
const [start, end] = theme.colors;
root.style.setProperty('--primary-start', start);
root.style.setProperty('--primary-end', end);
root.style.setProperty('--active-bg', start);
root.style.setProperty('--primary-shadow', start + '40');
}
saveTheme(themeId);
}
// ============================================================
// 工具
// ============================================================
function formatDate(d) {
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const wd = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
return m + '月' + day + '日 周' + wd;
}
function formatDateShort(d) {
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return m + '/' + day;
}
function getTodayStr() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
function parseDate(str) {
const parts = str.split('-').map(Number);
return new Date(parts[0], parts[1] - 1, parts[2]);
}
function getAutoMeal() {
const h = new Date().getHours();
if (h >= 5 && h < 9) return '早餐';
if (h >= 11 && h < 14) return '中餐';
if (h >= 17 && h < 21) return '晚餐';
return '加餐';
}
function getNumberValue(el) {
const v = el.value.trim();
if (v === '') return NaN;
const n = parseFloat(v);
return isNaN(n) ? NaN : n;
}
function generateId() {
return 'id_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ============================================================
// 数据操作
// ============================================================
function getRecordsForDate(dateStr) {
return data.records[dateStr] || [];
}
function getTotalKcalForDate(dateStr) {
const recs = getRecordsForDate(dateStr);
let total = 0;
recs.forEach(r => { total += r.totalKcal || 0; });
return total;
}
function getWeightForDate(dateStr) {
return data.weightRecords[dateStr] !== undefined ? data.weightRecords[dateStr] : null;
}
function setWeightForDate(dateStr, weightKg) {
data.weightRecords[dateStr] = weightKg;
saveData();
}
function getWeightRecordsInRange(startDate, endDate) {
const records = data.weightRecords || {};
const result = [];
const start = new Date(startDate);
const end = new Date(endDate);
for (const [dateStr, weight] of Object.entries(records)) {
const d = parseDate(dateStr);
if (d >= start && d <= end) {
result.push({ date: dateStr, weight: weight });
}
}
result.sort((a, b) => a.date.localeCompare(b.date));
return result;
}
function getRecentRange(days) {
const today = new Date();
const end = new Date(today);
const start = new Date(today);
start.setDate(start.getDate() - days + 1);
return { start: start, end: end };
}
// ============================================================
// 设置相关
// ============================================================
function getTargetCalories() {
const age = parseInt(userSettings.age);
const gender = userSettings.gender;
const height = parseFloat(userSettings.height);
const weight = parseFloat(userSettings.weight);
const activity = parseFloat(userSettings.activity);
const target = userSettings.target || 'lose';
if (isNaN(age) || isNaN(height) || isNaN(weight) || age <= 0 || height <= 0 || weight <= 0) {
return { tdee: null, targetKcal: null };
}
let bmr;
if (gender === 'male') {
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
} else {
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
}
const tdee = bmr * activity;
let targetKcal;
if (target === 'maintain') {
targetKcal = tdee;
} else {
targetKcal = tdee - 400;
if (targetKcal < 1000) targetKcal = 1000;
}
return { tdee, targetKcal };
}
function getTargetDisplay() {
const result = getTargetCalories();
if (result.targetKcal === null) return null;
return Math.round(result.targetKcal);
}
// ============================================================
// 渲染:日期+餐次
// ============================================================
function updateDateDisplay() {
const d = new Date();
document.getElementById('dateDisplay').textContent = formatDate(d);
}
// ============================================================
// 渲染:结果区 + 差值提示
// ============================================================
function updateResults(mealKcal, todayTotal, target) {
const mealEl = document.getElementById('currentMealKcal');
const todayEl = document.getElementById('todayTotalKcal');
const targetEl = document.getElementById('targetKcal');
const diffEl = document.getElementById('diffHint');
if (mealKcal !== null && !isNaN(mealKcal) && mealKcal > 0) {
mealEl.innerHTML = mealKcal.toFixed(1) + '<span class="runit"> 千卡</span>';
} else {
mealEl.innerHTML = '<span class="placeholder">--</span><span class="runit"> 千卡</span>';
}
const todayVal = todayTotal || 0;
todayEl.innerHTML = todayVal.toFixed(1) + '<span class="runit"> 千卡</span>';
const todayItem = document.getElementById('todayTotalItem');
todayItem.className = 'result-item';
if (target !== null && target > 0 && todayVal > 0) {
const ratio = todayVal / target;
if (ratio > 1.0) {
todayItem.classList.add('highlight-red');
} else if (ratio >= 0.9) {
todayItem.classList.add('highlight-yellow');
} else {
todayItem.classList.add('highlight-green');
}
} else if (todayVal > 0) {
todayItem.classList.add('highlight-green');
}
if (target !== null) {
targetEl.innerHTML = target + '<span class="runit"> 千卡</span>';
} else {
targetEl.innerHTML = '<span class="placeholder">--</span><span class="runit"> 千卡</span>';
}
// 差值提示
if (target === null || target <= 0) {
diffEl.textContent = '请先在「设置」中填写个人信息以计算目标热量';
diffEl.className = 'diff-hint';
return;
}
const diff = todayVal - target;
const ratio = todayVal / target;
if (diff > 0) {
diffEl.innerHTML = '今日热量超标 <span class="diff-value">' + diff.toFixed(1) + '</span> 千卡';
diffEl.className = 'diff-hint danger';
} else if (ratio >= 0.9) {
diffEl.innerHTML = '今日还可以吃 <span class="diff-value">' + Math.abs(diff).toFixed(1) + '</span> 千卡食物';
diffEl.className = 'diff-hint warning';
} else {
diffEl.innerHTML = '今日还可以吃 <span class="diff-value">' + Math.abs(diff).toFixed(1) + '</span> 千卡食物';
diffEl.className = 'diff-hint success';
}
}
// ============================================================
// 核心计算
// ============================================================
function computePreview() {
const defaultW = getNumberValue(document.getElementById('defaultWeight'));
const defaultUnit = document.getElementById('defaultUnit').value;
const energy = getNumberValue(document.getElementById('energy'));
const servingRaw = getNumberValue(document.getElementById('serving'));
const servingUnit = document.getElementById('servingUnit').value;
const hasDefault = !isNaN(defaultW) && defaultW > 0;
const hasEnergy = !isNaN(energy) && energy >= 0;
const hasServing = !isNaN(servingRaw) && servingRaw >= 0;
let mealKcal = null;
if (hasDefault && hasEnergy && hasServing) {
let servingUnits = servingRaw;
if (servingUnit === 'kg') servingUnits = servingRaw * 1000;
const energyPerUnit = energy / defaultW;
const totalKj = energyPerUnit * servingUnits;
mealKcal = totalKj / 4.184;
}
const todayStr = getTodayStr();
const todayTotal = getTotalKcalForDate(todayStr);
const target = getTargetDisplay();
updateResults(mealKcal, todayTotal, target);
}
// ============================================================
// 保存记录
// ============================================================
function saveRecord() {
const name = document.getElementById('foodName').value.trim();
const defaultW = getNumberValue(document.getElementById('defaultWeight'));
const defaultUnit = document.getElementById('defaultUnit').value;
const energy = getNumberValue(document.getElementById('energy'));
const servingRaw = getNumberValue(document.getElementById('serving'));
const servingUnit = document.getElementById('servingUnit').value;
if (!name) { showToast('请填写食物名称', 'error'); return; }
if (isNaN(defaultW) || defaultW <= 0) { showToast('请填写有效的每份量', 'error'); return; }
if (isNaN(energy) || energy < 0) { showToast('请填写有效的能量 (kJ)', 'error'); return; }
if (isNaN(servingRaw) || servingRaw <= 0) { showToast('请填写有效的食用量', 'error'); return; }
let servingUnits = servingRaw;
if (servingUnit === 'kg') servingUnits = servingRaw * 1000;
const energyPerUnit = energy / defaultW;
const totalKj = energyPerUnit * servingUnits;
const totalKcal = totalKj / 4.184;
const todayStr = getTodayStr();
const meal = document.getElementById('mealSelect').value || '加餐';
const record = {
id: generateId(),
tagName: name,
defaultWeight: defaultW,
unit: defaultUnit,
energy: energy,
serving: servingUnits,
servingUnit: servingUnit,
totalKcal: totalKcal,
meal: meal,
timestamp: new Date().toISOString()
};
if (!data.records[todayStr]) data.records[todayStr] = [];
data.records[todayStr].push(record);
const matchedTag = data.tags.find(t =>
t.name === name && t.defaultWeight === defaultW && t.unit === defaultUnit && t.energy === energy
);
if (matchedTag) {
matchedTag.lastUsed = Date.now();
}
saveData();
showToast('✅ 记录已保存', 'success');
computePreview();
renderCalendar();
// 如果当前选中的是今天,刷新记录卡片
if (selectedDateStr === todayStr) {
renderRecordCard(todayStr);
}
}
// ============================================================
// Toast 提示
// ============================================================
let toastTimer = null;
function showToast(message, type) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = 'toast-item' + (type ? ' ' + type : '');
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
toast.classList.add('show');
});
});
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 1500);
}
// ============================================================
// 通用确认弹窗
// ============================================================
let confirmCallback = null;
function showConfirm(title, desc, confirmText, onConfirm) {
document.querySelector('#confirmModal .mc-title').textContent = title;
document.getElementById('confirmDesc').textContent = desc;
document.querySelector('#confirmModal .mc-confirm').textContent = confirmText;
confirmCallback = onConfirm;
document.getElementById('confirmModal').classList.add('show');
}
function closeConfirm() {
confirmCallback = null;
document.getElementById('confirmModal').classList.remove('show');
}
document.getElementById('confirmOk').addEventListener('click', function() {
const cb = confirmCallback;
closeConfirm();
if (cb) cb();
});
document.getElementById('confirmCancel').addEventListener('click', closeConfirm);
// ============================================================
// 删除记录
// ============================================================
function confirmDeleteRecord(recordId) {
const todayStr = getTodayStr();
showConfirm('⚠️ 确认删除', '确定要删除该记录吗?', '删除', function() {
const records = data.records[todayStr] || [];
const idx = records.findIndex(r => r.id === recordId);
if (idx !== -1) {
records.splice(idx, 1);
if (records.length === 0) {
delete data.records[todayStr];
}
saveData();
renderCalendar();
renderRecordCard(todayStr);
computePreview();
}
});
}
// ============================================================
// 渲染:日历 + 记录卡片
// ============================================================
let calYear = new Date().getFullYear();
let calMonth = new Date().getMonth();
let selectedDateStr = getTodayStr();
function renderCalendar() {
const grid = document.getElementById('calendarGrid');
const monthLabel = document.getElementById('calMonth');
const year = calYear,
month = calMonth;
monthLabel.textContent = year + '年' + (month + 1) + '月';
const firstDayOfMonth = new Date(year, month, 1).getDay();
let startOffset = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrev = new Date(year, month, 0).getDate();
const target = getTargetDisplay();
const todayStr = getTodayStr();
let html = '';
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
weekdays.forEach(w => {
html += `<div class="cal-weekday">${w}</div>`;
});
// 上月
for (let i = startOffset - 1; i >= 0; i--) {
const d = daysInPrev - i;
const dateObj = new Date(year, month - 1, d);
const fullDate = formatDateFull(dateObj);
const shortDate = formatDateShort(dateObj);
const kcal = getTotalKcalForDate(fullDate);
const weight = getWeightForDate(fullDate);
html += `<div class="cal-day other-month" data-date="${fullDate}">
<span class="day-number">${d}</span>
${kcal > 0 ? `<span class="day-kcal">${kcal.toFixed(0)}</span>` : ''}
${weight !== null ? `<span class="day-weight">${weight.toFixed(1)}</span>` : ''}
</div>`;
}
// 本月
for (let d = 1; d <= daysInMonth; d++) {
const dateObj = new Date(year, month, d);
const fullDate = formatDateFull(dateObj);
const shortDate = formatDateShort(dateObj);
const kcal = getTotalKcalForDate(fullDate);
const weight = getWeightForDate(fullDate);
const isToday = fullDate === todayStr;
const isSelected = fullDate === selectedDateStr;
let statusClass = '';
if (kcal > 0 && target !== null && target > 0) {
const ratio = kcal / target;
if (ratio > 1.0) statusClass = 'status-red';
else if (ratio >= 0.9) statusClass = 'status-yellow';
else statusClass = 'status-green';
} else if (kcal > 0) {
statusClass = 'status-green';
}
let cls = 'cal-day';
if (isToday) cls += ' today';
if (isSelected) cls += ' selected';
if (statusClass) cls += ' ' + statusClass;
html += `<div class="${cls}" data-date="${fullDate}">
<span class="day-number">${d}</span>
${kcal > 0 ? `<span class="day-kcal">${kcal.toFixed(0)}</span>` : ''}
${weight !== null ? `<span class="day-weight">${weight.toFixed(1)}</span>` : ''}
</div>`;
}
// 下月填充
const totalCells = startOffset + daysInMonth;
const remaining = (7 - (totalCells % 7)) % 7;
for (let d = 1; d <= remaining; d++) {
const dateObj = new Date(year, month + 1, d);
const fullDate = formatDateFull(dateObj);
const shortDate = formatDateShort(dateObj);
const kcal = getTotalKcalForDate(fullDate);
const weight = getWeightForDate(fullDate);
html += `<div class="cal-day other-month" data-date="${fullDate}">
<span class="day-number">${d}</span>
${kcal > 0 ? `<span class="day-kcal">${kcal.toFixed(0)}</span>` : ''}
${weight !== null ? `<span class="day-weight">${weight.toFixed(1)}</span>` : ''}
</div>`;
}
grid.innerHTML = html;
// 点击日期
grid.querySelectorAll('.cal-day').forEach(el => {
el.addEventListener('click', function() {
const fullDate = this.dataset.date;
if (fullDate) {
selectedDateStr = fullDate;
renderCalendar();
renderRecordCard(fullDate);
}
});
});
// 如果当前选中日期有记录,刷新记录卡片
if (selectedDateStr) {
renderRecordCard(selectedDateStr);
}
}
function formatDateFull(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
function renderRecordCard(dateStr) {
const card = document.getElementById('recordCard');
const list = document.getElementById('ddList');
const title = document.getElementById('ddTitle');
const weightEl = document.getElementById('ddWeight');
const records = getRecordsForDate(dateStr);
const total = getTotalKcalForDate(dateStr);
const weight = getWeightForDate(dateStr);
const todayStr = getTodayStr();
const isToday = (dateStr === todayStr);
if (records.length === 0 && weight === null) {
card.classList.remove('show');
return;
}
card.classList.add('show');
title.innerHTML = '📋 ' + dateStr + ' · 总计 <span class="dd-total-kcal">' + total.toFixed(1) + ' 千卡</span>';
if (weight !== null) {
weightEl.textContent = '⚖️ ' + weight.toFixed(1) + ' kg';
weightEl.style.display = 'block';
} else {
weightEl.style.display = 'none';
}
if (records.length === 0) {
list.innerHTML = '<div class="dd-empty">暂无饮食记录</div>';
return;
}
let html = '';
const mealOrder = ['早餐', '中餐', '晚餐', '加餐'];
const grouped = {};
records.forEach(r => {
const m = r.meal || '加餐';
if (!grouped[m]) grouped[m] = [];
grouped[m].push(r);
});
mealOrder.forEach(m => {
if (grouped[m] && grouped[m].length > 0) {
html += `<div class="meal-group-label">${m}</div>`;
grouped[m].forEach(r => {
const name = r.tagName || '未命名';
const kcal = r.totalKcal || 0;
const gram = r.serving || 0;
const unit = r.unit || 'g';
let delBtn = '';
if (isToday) {
delBtn = `<button class="dd-del" data-id="${r.id}">✕</button>`;
}
html += `<div class="dd-item">
<span class="dd-name">${escapeHtml(name)} (${gram}${unit})</span>
<span style="display:flex;align-items:center;">
<span class="dd-kcal">${kcal.toFixed(1)} 千卡</span>
${delBtn}
</span>
</div>`;
});
}
});
list.innerHTML = html;
if (isToday) {
list.querySelectorAll('.dd-del').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const id = this.dataset.id;
confirmDeleteRecord(id);
});
});
}
}
// ============================================================
// 标签系统(简化版)
// ============================================================
const tagListEl = document.getElementById('tagList');
const tagCountEl = document.getElementById('tagCount');
function renderTags() {
const tags = [...data.tags].sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0));
tagCountEl.textContent = tags.length + ' / 50';
if (tags.length === 0) {
tagListEl.innerHTML =
'<div style="color:#b0b8c4;text-align:center;padding:16px 0;font-size:13px;">暂无标签</div>';
return;
}
let html = '';
tags.forEach((tag) => {
const unitDisplay = tag.unit || 'g';
html += `<div class="tag-item" data-id="${tag.id}">
<span class="tag-name">${escapeHtml(tag.name)}</span>
<span style="font-size:10px;color:#999;flex-shrink:0;">${tag.defaultWeight}${unitDisplay}/${tag.energy}kJ</span>
<button class="tag-del" data-id="${tag.id}">✕</button>
</div>`;
});
tagListEl.innerHTML = html;
tagListEl.querySelectorAll('.tag-del').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const id = this.dataset.id;
const tag = data.tags.find(t => t.id === id);
if (tag) {
const tagId = id;
const tagName = tag.name;
showConfirm('⚠️ 确认删除', '确定要删除标签 "' + tagName + '" 吗?', '删除', function() {
data.tags = data.tags.filter(t => t.id !== tagId);
saveData();
renderTags();
showToast('✅ 标签已删除', 'success');
});
}
});
});
tagListEl.querySelectorAll('.tag-item').forEach(el => {
el.addEventListener('click', function(e) {
if (e.target.closest('.tag-del')) return;
const id = this.dataset.id;
const tag = data.tags.find(t => t.id === id);
if (tag) {
tagListEl.querySelectorAll('.tag-item').forEach(item => {
item.classList.remove('tag-selected');
});
this.classList.add('tag-selected');
setTimeout(() => {
document.getElementById('foodName').value = tag.name;
document.getElementById('defaultWeight').value = tag.defaultWeight;
document.getElementById('defaultUnit').value = tag.unit || 'g';
document.getElementById('energy').value = tag.energy;
const unit = tag.unit || 'g';
document.getElementById('servingUnit').value = (unit === 'ml') ? 'ml' : 'g';
computePreview();
closePanel();
}, 150);
}
});
});
}
// ============================================================
// 标签面板控制
// ============================================================
const fabBtn = document.getElementById('fabToggle');
const panel = document.getElementById('tagPanel');
const overlayTag = document.getElementById('overlayTag');
const panelClose = document.getElementById('panelClose');
function openPanel() {
panel.classList.add('open');
overlayTag.classList.add('show');
renderTags();
}
function closePanel() {
panel.classList.remove('open');
overlayTag.classList.remove('show');
}
fabBtn.addEventListener('click', function() {
if (panel.classList.contains('open')) closePanel();
else openPanel();
});
panelClose.addEventListener('click', closePanel);
overlayTag.addEventListener('click', closePanel);
// ============================================================
// 悬浮按钮拖动
// ============================================================
const tagFab = document.getElementById('tagFab');
const FAB_POS_KEY = 'tagFabPos';
function loadFabPos() {
try {
const saved = localStorage.getItem(FAB_POS_KEY);
if (saved) {
const pos = JSON.parse(saved);
if (pos.right !== undefined) tagFab.style.right = pos.right + 'px';
if (pos.bottom !== undefined) tagFab.style.bottom = pos.bottom + 'px';
if (pos.left !== undefined) {
tagFab.style.left = pos.left + 'px';
tagFab.style.right = 'auto';
}
}
} catch (_) {}
}
function saveFabPos(right, bottom, left) {
try {
const pos = {};
if (left !== undefined) pos.left = left;
else pos.right = right;
pos.bottom = bottom;
localStorage.setItem(FAB_POS_KEY, JSON.stringify(pos));
} catch (_) {}
}
let isDragging = false;
let dragMoved = false;
let startX = 0;
let startY = 0;
let startRight = 0;
let startBottom = 0;
let startLeft = 0;
let isLeftSide = false;
function onDragStart(e) {
isDragging = true;
dragMoved = false;
const rect = tagFab.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
startX = clientX;
startY = clientY;
startRight = window.innerWidth - rect.right;
startBottom = window.innerHeight - rect.bottom;
startLeft = rect.left;
isLeftSide = (rect.left < window.innerWidth / 2);
tagFab.style.transition = 'none';
}
function onDragMove(e) {
if (!isDragging) return;
e.preventDefault();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const dx = clientX - startX;
const dy = clientY - startY;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
dragMoved = true;
}
let newLeft, newRight, newBottom;
if (isLeftSide) {
newLeft = startLeft + dx;
newLeft = Math.max(4, Math.min(window.innerWidth - 56, newLeft));
tagFab.style.left = newLeft + 'px';
tagFab.style.right = 'auto';
} else {
newRight = startRight - dx;
newRight = Math.max(4, Math.min(window.innerWidth - 56, newRight));
tagFab.style.right = newRight + 'px';
tagFab.style.left = 'auto';
}
newBottom = startBottom - dy;
newBottom = Math.max(4, Math.min(window.innerHeight - 56, newBottom));
tagFab.style.bottom = newBottom + 'px';
}
function onDragEnd(e) {
if (!isDragging) return;
isDragging = false;
tagFab.style.transition = '';
if (dragMoved) {
const rect = tagFab.getBoundingClientRect();
const bottom = window.innerHeight - rect.bottom;
const isLeft = rect.left < window.innerWidth / 2;
if (isLeft) {
saveFabPos(undefined, bottom, rect.left);
} else {
const right = window.innerWidth - rect.right;
saveFabPos(right, bottom, undefined);
}
}
}
fabBtn.addEventListener('mousedown', function(e) {
onDragStart(e);
});
document.addEventListener('mousemove', function(e) {
onDragMove(e);
});
document.addEventListener('mouseup', function(e) {
onDragEnd(e);
});
fabBtn.addEventListener('touchstart', function(e) {
onDragStart(e);
}, { passive: false });
document.addEventListener('touchmove', function(e) {
onDragMove(e);
}, { passive: false });
document.addEventListener('touchend', function(e) {
onDragEnd(e);
});
fabBtn.addEventListener('click', function(e) {
if (dragMoved) {
e.stopImmediatePropagation();
e.preventDefault();
}
}, true);
loadFabPos();
// ============================================================
// 设置弹窗
// ============================================================
const settingsModal = document.getElementById('settingsModal');
const settingsToggle = document.getElementById('settingsToggle');
const settingsClose = document.getElementById('settingsClose');
const settingsSave = document.getElementById('settingsSave');
function openSettings() {
document.getElementById('userAge').value = userSettings.age || '';
document.getElementById('userGenderSelect').value = userSettings.gender || 'male';
document.getElementById('userHeight').value = userSettings.height || '';
document.getElementById('userWeight').value = userSettings.weight || '';
document.getElementById('userActivity').value = userSettings.activity || '1.2';
const target = userSettings.target || 'lose';
document.querySelectorAll('#targetGroup button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.target === target);
});
updateCalcInfo();
settingsModal.classList.add('show');
}
function closeSettings() {
settingsModal.classList.remove('show');
}
settingsToggle.addEventListener('click', openSettings);
settingsClose.addEventListener('click', closeSettings);
settingsModal.addEventListener('click', function(e) {
if (e.target === this) closeSettings();
});
document.querySelectorAll('#targetGroup button').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('#targetGroup button').forEach(b => b.classList.remove('active'));
this.classList.add('active');
userSettings.target = this.dataset.target;
updateCalcInfo();
});
});
['userAge', 'userHeight', 'userWeight', 'userActivity', 'userGenderSelect'].forEach(id => {
const el = document.getElementById(id);
el.addEventListener('input', updateCalcInfo);
el.addEventListener('change', updateCalcInfo);
});
function updateCalcInfo() {
const age = parseInt(document.getElementById('userAge').value);
const gender = document.getElementById('userGenderSelect').value;
const height = parseFloat(document.getElementById('userHeight').value);
const weight = parseFloat(document.getElementById('userWeight').value);
const activity = parseFloat(document.getElementById('userActivity').value);
const target = document.querySelector('#targetGroup .active')?.dataset.target || 'lose';
const bmrEl = document.getElementById('bmrDisplay');
const tdeeEl = document.getElementById('tdeeDisplay');
const suggEl = document.getElementById('targetSuggestion');
if (isNaN(age) || isNaN(height) || isNaN(weight) || age <= 0 || height <= 0 || weight <= 0) {
bmrEl.textContent = '--';
tdeeEl.textContent = '--';
suggEl.innerHTML = '请填写完整信息';
return;
}
let bmr;
if (gender === 'male') {
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
} else {
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
}
const tdee = bmr * activity;
let targetKcal = (target === 'maintain') ? tdee : (tdee - 400);
if (targetKcal < 1000) targetKcal = 1000;
bmrEl.textContent = Math.round(bmr);
tdeeEl.textContent = Math.round(tdee);
suggEl.innerHTML = '建议每日摄入 <strong>' + Math.round(targetKcal) + '</strong> 千卡 (' + (target === 'maintain' ?
'维持' : '减肥') + ')';
}
settingsSave.addEventListener('click', function() {
const age = parseInt(document.getElementById('userAge').value);
const gender = document.getElementById('userGenderSelect').value;
const height = parseFloat(document.getElementById('userHeight').value);
const weight = parseFloat(document.getElementById('userWeight').value);
const activity = parseFloat(document.getElementById('userActivity').value);
const target = document.querySelector('#targetGroup .active')?.dataset.target || 'lose';
if (isNaN(age) || isNaN(height) || isNaN(weight) || age <= 0 || height <= 0 || weight <= 0) {
showToast('请完整填写年龄、身高、体重!', 'error');
return;
}
const settings = { age, gender, height, weight, activity, target };
saveSettings(settings);
closeSettings();
computePreview();
renderCalendar();
});
// ============================================================
// 主题弹窗
// ============================================================
const themeModal = document.getElementById('themeModal');
const themeToggle = document.getElementById('themeToggle');
const themeClose = document.getElementById('themeClose');
const themeGrid = document.getElementById('themeGrid');
function renderThemeGrid() {
let html = '';
THEMES.forEach(t => {
const [start, end] = t.colors;
const active = (t.id === currentTheme) ? ' active' : '';
html += `<div class="theme-btn${active}" data-theme="${t.id}">
<div class="color-preview" style="background:linear-gradient(135deg, ${start}, ${end});"></div>
${t.name}
</div>`;
});
themeGrid.innerHTML = html;
themeGrid.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', function() {
const themeId = this.dataset.theme;
applyTheme(themeId);
themeGrid.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
}
function openThemeModal() {
renderThemeGrid();
themeModal.classList.add('show');
}
themeToggle.addEventListener('click', openThemeModal);
themeClose.addEventListener('click', function() {
themeModal.classList.remove('show');
});
themeModal.addEventListener('click', function(e) {
if (e.target === this) themeModal.classList.remove('show');
});
// ============================================================
// 暗黑模式切换
// ============================================================
const DARK_MODE_KEY = 'darkMode';
let isDarkMode = false;
function loadDarkMode() {
const saved = localStorage.getItem(DARK_MODE_KEY);
if (saved !== null) {
return saved === 'true';
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
return false;
}
function applyDarkMode(enabled) {
isDarkMode = enabled;
if (enabled) {
document.body.classList.add('dark-mode');
document.getElementById('darkModeToggle').innerHTML = '☀️ 关闭暗黑模式';
} else {
document.body.classList.remove('dark-mode');
document.getElementById('darkModeToggle').innerHTML = '🌙 启用暗黑模式';
}
localStorage.setItem(DARK_MODE_KEY, enabled);
}
document.getElementById('darkModeToggle').addEventListener('click', function() {
applyDarkMode(!isDarkMode);
});
applyDarkMode(loadDarkMode());
// ============================================================
// 备份弹窗
// ============================================================
const backupModal = document.getElementById('backupModal');
const backupToggle = document.getElementById('backupToggle');
const backupExportBtn = document.getElementById('backupExportBtn');
const backupImportBtn = document.getElementById('backupImportBtn');
const backupFileInput = document.getElementById('backupFileInput');
function openBackupModal() {
backupModal.classList.add('show');
}
function closeBackupModal() {
backupModal.classList.remove('show');
}
backupToggle.addEventListener('click', openBackupModal);
backupModal.addEventListener('click', function(e) {
if (e.target === this) closeBackupModal();
});
backupExportBtn.addEventListener('click', doExport);
backupImportBtn.addEventListener('click', function() {
backupFileInput.click();
});
backupFileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
handleImportFile(file);
}
e.target.value = '';
});
// ============================================================
// 体重弹窗
// ============================================================
const weightModal = document.getElementById('weightModal');
const weightToggle = document.getElementById('weightToggle');
const weightClose = document.getElementById('weightClose');
const weightSave = document.getElementById('weightSave');
const weightInput = document.getElementById('weightInput');
const weightTime = document.getElementById('weightTime');
const weightJinDisplay = document.getElementById('weightJinDisplay');
weightInput.addEventListener('input', function() {
const val = parseFloat(this.value);
if (!isNaN(val) && val > 0) {
weightJinDisplay.textContent = (val * 2).toFixed(1) + ' 斤';
} else {
weightJinDisplay.textContent = '';
}
});
function openWeightModal() {
const today = getTodayStr();
const existing = getWeightForDate(today);
if (existing !== null) {
weightInput.value = existing;
weightJinDisplay.textContent = (existing * 2).toFixed(1) + ' 斤';
} else {
weightInput.value = '';
weightJinDisplay.textContent = '';
}
weightTime.value = '自动';
drawWeightChart();
weightModal.classList.add('show');
}
function closeWeightModal() {
weightModal.classList.remove('show');
}
weightToggle.addEventListener('click', openWeightModal);
weightClose.addEventListener('click', closeWeightModal);
weightModal.addEventListener('click', function(e) {
if (e.target === this) closeWeightModal();
});
weightSave.addEventListener('click', function() {
const val = parseFloat(weightInput.value);
if (isNaN(val) || val <= 0) {
showToast('请输入有效的体重 (kg)', 'error');
return;
}
const today = getTodayStr();
setWeightForDate(today, val);
showToast('✅ 体重已保存: ' + val + ' kg (' + (val * 2).toFixed(1) + ' 斤)', 'success');
drawWeightChart();
renderRecordCard(today);
closeWeightModal();
});
// ============================================================
// 体重图表
// ============================================================
const weightChartCanvas = document.getElementById('weightChartCanvas');
const weightNoData = document.getElementById('weightNoData');
const weightPeriodBtns = document.querySelectorAll('#weightPeriodGroup button');
let weightPeriod = 7;
function drawWeightChart() {
const canvas = weightChartCanvas;
const ctx = canvas.getContext('2d');
const container = document.getElementById('weightChartContainer');
const width = container.clientWidth - 4 || 300;
const height = 180;
canvas.width = width * 0.98;
canvas.height = height;
canvas.style.width = (width * 0.98) + 'px';
canvas.style.height = height + 'px';
const range = getRecentRange(weightPeriod);
const startFull = formatDateFull(range.start);
const endFull = formatDateFull(range.end);
const records = getWeightRecordsInRange(startFull, endFull);
if (records.length === 0) {
weightNoData.style.display = 'block';
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
weightNoData.style.display = 'none';
const padding = { top: 20, bottom: 22, left: 36, right: 16 };
const chartWidth = canvas.width - padding.left - padding.right;
const chartHeight = canvas.height - padding.top - padding.bottom;
let minWeight = Infinity,
maxWeight = -Infinity;
records.forEach(r => {
if (r.weight < minWeight) minWeight = r.weight;
if (r.weight > maxWeight) maxWeight = r.weight;
});
const rangeWeight = maxWeight - minWeight;
const yMin = Math.max(0, minWeight - rangeWeight * 0.15);
const yMax = maxWeight + rangeWeight * 0.15;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#e8ecf1';
ctx.lineWidth = 0.5;
const ySteps = 4;
for (let i = 0; i <= ySteps; i++) {
const y = padding.top + chartHeight - (i / ySteps) * chartHeight;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(canvas.width - padding.right, y);
ctx.stroke();
const value = yMin + (i / ySteps) * (yMax - yMin);
ctx.fillStyle = '#888';
ctx.font = '9px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(value.toFixed(1), padding.left - 4, y);
}
ctx.strokeStyle = '#ff7e5f';
ctx.lineWidth = 2.5;
ctx.beginPath();
records.forEach((r, idx) => {
const x = padding.left + (idx / (records.length - 1 || 1)) * chartWidth;
const y = padding.top + chartHeight - ((r.weight - yMin) / (yMax - yMin)) * chartHeight;
if (idx === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
records.forEach((r, idx) => {
const x = padding.left + (idx / (records.length - 1 || 1)) * chartWidth;
const y = padding.top + chartHeight - ((r.weight - yMin) / (yMax - yMin)) * chartHeight;
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
ctx.fillStyle = '#ff7e5f';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
let labelInterval = 1;
if (weightPeriod > 7 && weightPeriod <= 30) labelInterval = Math.ceil(weightPeriod / 7);
else if (weightPeriod > 30 && weightPeriod <= 90) labelInterval = Math.ceil(weightPeriod / 15);
else if (weightPeriod > 90 && weightPeriod <= 180) labelInterval = Math.ceil(weightPeriod / 30);
else if (weightPeriod > 180) labelInterval = Math.ceil(weightPeriod / 60);
ctx.fillStyle = '#888';
ctx.font = '9px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
records.forEach((r, idx) => {
if (idx % labelInterval === 0 || idx === records.length - 1) {
const x = padding.left + (idx / (records.length - 1 || 1)) * chartWidth;
const parts = r.date.split('-');
const label = parts[1] + '/' + parts[2];
ctx.fillText(label, x, canvas.height - padding.bottom + 2);
}
});
}
weightPeriodBtns.forEach(btn => {
btn.addEventListener('click', function() {
weightPeriodBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
weightPeriod = parseInt(this.dataset.period);
drawWeightChart();
});
});
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (weightModal.classList.contains('show')) {
drawWeightChart();
}
}, 200);
});
// ============================================================
// 单位联动 + 事件绑定
// ============================================================
document.getElementById('defaultUnit').addEventListener('change', function() {
const servingUnit = document.getElementById('servingUnit');
if (this.value === 'ml') {
servingUnit.value = 'ml';
} else {
servingUnit.value = 'g';
}
computePreview();
});
document.getElementById('servingUnit').addEventListener('change', computePreview);
['foodName', 'defaultWeight', 'energy', 'serving'].forEach(id => {
const el = document.getElementById(id);
el.addEventListener('input', function() {
computePreview();
saveInputCache();
});
el.addEventListener('change', function() {
computePreview();
saveInputCache();
});
});
['defaultUnit', 'servingUnit'].forEach(id => {
const el = document.getElementById(id);
el.addEventListener('change', saveInputCache);
});
const autoMeal = getAutoMeal();
document.getElementById('mealSelect').value = autoMeal;
document.getElementById('clearInputsBtn').addEventListener('click', function() {
document.getElementById('foodName').value = '';
document.getElementById('defaultWeight').value = '100';
document.getElementById('energy').value = '';
document.getElementById('serving').value = '';
computePreview();
saveInputCache();
});
document.getElementById('saveRecordBtn').addEventListener('click', saveRecord);
document.getElementById('saveTagQuick').addEventListener('click', function() {
const name = document.getElementById('foodName').value.trim();
const weight = getNumberValue(document.getElementById('defaultWeight'));
const unit = document.getElementById('defaultUnit').value;
const energy = getNumberValue(document.getElementById('energy'));
if (!name) { showToast('请填写食物名称', 'error'); return; }
if (isNaN(weight) || weight <= 0) { showToast('请填写有效的每份量', 'error'); return; }
if (isNaN(energy) || energy < 0) { showToast('请填写有效的能量 (kJ)', 'error'); return; }
if (data.tags.length >= 50) { showToast('标签已达上限 (50)', 'warning'); return; }
const exist = data.tags.find(t => t.name === name && t.unit === unit && t.defaultWeight === weight && t
.energy === energy);
if (exist) {
exist.lastUsed = Date.now();
saveData();
renderTags();
showToast('标签已存在', 'warning');
return;
}
const sameName = data.tags.find(t => t.name === name);
if (sameName) {
showConfirm('⚠️ 确认覆盖', '已存在同名标签,是否覆盖?', '覆盖', function() {
sameName.defaultWeight = weight;
sameName.unit = unit;
sameName.energy = energy;
sameName.lastUsed = Date.now();
saveData();
renderTags();
showToast('✅ 已更新: ' + name, 'success');
});
return;
}
data.tags.unshift({
id: generateId(),
name: name,
defaultWeight: weight,
unit: unit,
energy: energy,
lastUsed: Date.now()
});
saveData();
renderTags();
showToast('✅ 已保存: ' + name, 'success');
});
document.getElementById('calPrev').addEventListener('click', function() {
calMonth--;
if (calMonth < 0) { calMonth = 11;
calYear--; }
renderCalendar();
});
document.getElementById('calNext').addEventListener('click', function() {
calMonth++;
if (calMonth > 11) { calMonth = 0;
calYear++; }
renderCalendar();
});
// ============================================================
// 初始化
// ============================================================
function init() {
applyTheme(currentTheme);
updateDateDisplay();
applyInputCache();
renderCalendar();
computePreview();
setTimeout(() => {
drawWeightChart();
}, 100);
console.log('🍱 热量计算+体重记录 已启动 (紧凑日历+差值提示)');
}
init();
setInterval(() => {
const todayStr = getTodayStr();
const todayTotal = getTotalKcalForDate(todayStr);
const target = getTargetDisplay();
updateResults(null, todayTotal, target);
if (selectedDateStr === todayStr) {
renderRecordCard(todayStr);
}
}, 60000);
})();
</script>
</body>
</html>