feat: 新增膳食问卷页面,更新膳食建议和体重管理页面
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,592 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>膳食偏好设置 - 员工端 | 健康CQ原型</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
|
||||
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; position: relative; display: flex; flex-direction: column; }
|
||||
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
|
||||
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
|
||||
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
|
||||
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #1a1a1a; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; }
|
||||
.nav-placeholder { width: 56px; flex-shrink: 0; }
|
||||
|
||||
/* ===== 问卷主体(替代原 guide-overlay 的 absolute 定位) ===== */
|
||||
.questionnaire { flex: 1; overflow-y: auto; background: #f5f7fa; display: flex; flex-direction: column; }
|
||||
.questionnaire::-webkit-scrollbar { display: none; }
|
||||
|
||||
.questionnaire__hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px 20px 24px; color: #fff; text-align: center; flex-shrink: 0; }
|
||||
.questionnaire__hero-icon { font-size: 40px; margin-bottom: 8px; }
|
||||
.questionnaire__hero-title { font-size: 20px; font-weight: 800; margin-bottom: 4px; }
|
||||
.questionnaire__hero-sub { font-size: 13px; opacity: 0.85; }
|
||||
|
||||
.questionnaire__body { flex: 1; padding: 16px 16px 0; display: flex; flex-direction: column; gap: 14px; }
|
||||
.questionnaire__q { background: #fff; border-radius: 14px; padding: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); }
|
||||
.questionnaire__q-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.questionnaire__q-num { width: 22px; height: 22px; border-radius: 50%; background: #667eea; color: #fff; font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.questionnaire__q-title { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.questionnaire__q-required { font-size: 10px; color: #ff4d4f; margin-left: auto; }
|
||||
|
||||
/* 地区触发器 */
|
||||
.questionnaire__trigger { display: flex; align-items: center; justify-content: space-between; height: 42px; padding: 0 14px; background: #f7f8fa; border-radius: 10px; font-size: 13px; color: #bbb; cursor: pointer; }
|
||||
.questionnaire__trigger--filled { color: #333; font-weight: 600; }
|
||||
.questionnaire__trigger-arrow { color: #bbb; font-size: 12px; }
|
||||
|
||||
/* Chip 标签选择 */
|
||||
.questionnaire__chips { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.questionnaire__chip { font-size: 13px; padding: 8px 16px; border-radius: 20px; font-weight: 600; cursor: pointer; border: 1.5px solid #e0e0e0; color: #666; background: #fff; transition: all .2s; }
|
||||
.questionnaire__chip:hover { border-color: #91b8ff; background: #f5f9ff; }
|
||||
.questionnaire__chip--active { border-color: #667eea; background: #f0f2ff; color: #667eea; }
|
||||
.questionnaire__chip--active::before { content: '✓ '; font-size: 11px; }
|
||||
|
||||
/* 提交按钮 */
|
||||
.questionnaire__footer { padding: 16px 16px 24px; flex-shrink: 0; }
|
||||
.questionnaire__submit { width: 100%; height: 48px; border-radius: 24px; border: none; font-size: 16px; font-weight: 700; color: #fff; cursor: pointer; background: #d0d5e0; transition: all .25s; }
|
||||
.questionnaire__submit--ready { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 6px 20px rgba(102,126,234,0.3); }
|
||||
.questionnaire__submit--ready:active { transform: scale(0.97); }
|
||||
|
||||
/* ===== 地区选择底部 Sheet(省-市级联) ===== */
|
||||
.region-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 200; display: none; }
|
||||
.region-mask.show { display: block; }
|
||||
.region-sheet { position: absolute; left: 0; right: 0; bottom: 0; background: #fff; border-radius: 18px 18px 0 0; z-index: 201; display: none; padding-bottom: 16px; }
|
||||
.region-sheet.show { display: block; animation: qSlideUp .25s ease; }
|
||||
.region-sheet__head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #f0f0f0; }
|
||||
.region-sheet__cancel { font-size: 15px; color: #999; cursor: pointer; }
|
||||
.region-sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
||||
.region-sheet__submit { font-size: 15px; color: #667eea; font-weight: 600; cursor: pointer; }
|
||||
.region-sheet__list { padding: 6px 0; max-height: 360px; overflow-y: auto; }
|
||||
.region-sheet__list::-webkit-scrollbar { display: none; }
|
||||
.region-sheet__item { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; font-size: 15px; color: #333; cursor: pointer; }
|
||||
.region-sheet__item.active { color: #667eea; font-weight: 600; background: #f0f2ff; }
|
||||
.region-sheet__check { width: 18px; height: 18px; border-radius: 50%; border: 1px solid #d0d0d0; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.region-sheet__item.active .region-sheet__check { background: #667eea; border-color: #667eea; }
|
||||
.region-sheet__check svg { width: 12px; height: 12px; stroke: #fff; stroke-width: 3; fill: none; display: none; }
|
||||
.region-sheet__item.active .region-sheet__check svg { display: block; }
|
||||
|
||||
/* 省-市级联视图 */
|
||||
.region-sheet__view { display: none; }
|
||||
.region-sheet__view.active { display: block; }
|
||||
.region-sheet__back { display: flex; align-items: center; gap: 6px; padding: 10px 18px; font-size: 13px; cursor: pointer; border-bottom: 1px solid #f0f0f0; }
|
||||
.region-sheet__back-arrow { font-size: 18px; line-height: 1; color: #667eea; }
|
||||
.region-sheet__back-label { color: #999; }
|
||||
.region-sheet__back-province { color: #1a1a1a; font-weight: 700; margin-left: auto; }
|
||||
.region-sheet__item--parent .region-sheet__sub { font-size: 13px; color: #bbb; font-weight: 600; display: flex; align-items: center; }
|
||||
.region-sheet__item--parent.active .region-sheet__sub { color: #667eea; }
|
||||
|
||||
/* ===== 自定义规避食材底部 Sheet ===== */
|
||||
.custom-avoid-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 300; display: none; }
|
||||
.custom-avoid-mask.show { display: block; }
|
||||
.custom-avoid-sheet { position: absolute; left: 0; right: 0; bottom: 0; background: #fff; border-radius: 18px 18px 0 0; z-index: 301; display: none; padding-bottom: 16px; }
|
||||
.custom-avoid-sheet.show { display: block; animation: qSlideUp .25s ease; }
|
||||
.custom-avoid-sheet__head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #f0f0f0; }
|
||||
.custom-avoid-sheet__cancel { font-size: 15px; color: #999; cursor: pointer; }
|
||||
.custom-avoid-sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
||||
.custom-avoid-sheet__submit { font-size: 15px; color: #667eea; font-weight: 600; cursor: pointer; }
|
||||
.custom-avoid-sheet__body { padding: 16px 18px; }
|
||||
.custom-avoid-sheet__input { width: 100%; height: 44px; border: 1.5px solid #e0e0e0; border-radius: 10px; padding: 0 14px; font-size: 15px; outline: none; transition: border-color .2s; font-family: inherit; }
|
||||
.custom-avoid-sheet__input:focus { border-color: #667eea; }
|
||||
.custom-avoid-sheet__hint { font-size: 11px; color: #bbb; margin-top: 8px; }
|
||||
.custom-avoid-sheet__quick-label { font-size: 12px; color: #999; font-weight: 600; margin: 16px 0 8px; }
|
||||
.custom-avoid-sheet__quick { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.custom-avoid-sheet__quick-tag { font-size: 12px; padding: 5px 12px; border-radius: 16px; border: 1px solid #e0e0e0; color: #666; background: #fafafa; cursor: pointer; transition: all .2s; }
|
||||
.custom-avoid-sheet__quick-tag:hover { border-color: #91b8ff; background: #f5f9ff; }
|
||||
.custom-avoid-sheet__quick-tag--added { border-color: #ffa39e; background: #fff1f0; color: #cf1322; }
|
||||
.custom-avoid-sheet__quick-tag--added::after { content: ' ✓'; }
|
||||
|
||||
/* ===== 自定义食材标签 ===== */
|
||||
.questionnaire__custom-avoid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.questionnaire__custom-tag { font-size: 13px; padding: 8px 14px; border-radius: 20px; font-weight: 600; cursor: pointer; border: 1.5px solid #ffa39e; color: #cf1322; background: #fff1f0; transition: all .2s; display: flex; align-items: center; gap: 6px; }
|
||||
.questionnaire__custom-tag:hover { background: #ffe9e7; }
|
||||
.questionnaire__custom-tag::after { content: '✕'; font-size: 10px; opacity: 0.7; }
|
||||
.questionnaire__chip--add { border-style: dashed !important; color: #bbb !important; border-color: #d0d0d0 !important; background: #fafafa !important; }
|
||||
.questionnaire__chip--add:hover { border-color: #667eea !important; color: #667eea !important; background: #f5f9ff !important; }
|
||||
.questionnaire__chip--add::before { content: '+ ' !important; }
|
||||
|
||||
/* 底部 Sheet 滑入动画 */
|
||||
@keyframes qSlideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone-shell">
|
||||
<div class="status-bar">
|
||||
<span class="time">09:40</span>
|
||||
<div class="icons">
|
||||
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
|
||||
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
|
||||
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-bar">
|
||||
<div class="nav-back" id="navBack" onclick="goBack()">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></polyline></svg>
|
||||
</div>
|
||||
<div class="nav-title">膳食偏好设置</div>
|
||||
<div class="nav-placeholder"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 问卷主体 ===== -->
|
||||
<div class="questionnaire">
|
||||
<div class="questionnaire__hero">
|
||||
<div class="questionnaire__hero-icon">🥗</div>
|
||||
<div class="questionnaire__hero-title">定制你的专属膳食方案</div>
|
||||
<div class="questionnaire__hero-sub">根据你的偏好与健康数据生成个性化食谱</div>
|
||||
</div>
|
||||
<div class="questionnaire__body">
|
||||
|
||||
<!-- 问题1:所在地区 -->
|
||||
<div class="questionnaire__q">
|
||||
<div class="questionnaire__q-header">
|
||||
<span class="questionnaire__q-num">1</span>
|
||||
<span class="questionnaire__q-title">所在地区</span>
|
||||
<span class="questionnaire__q-required">必选</span>
|
||||
</div>
|
||||
<div class="questionnaire__trigger" id="regionTrigger" onclick="openRegionSheet()">
|
||||
请选择常驻地区
|
||||
<span class="questionnaire__trigger-arrow">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 问题2:口味偏好 -->
|
||||
<div class="questionnaire__q">
|
||||
<div class="questionnaire__q-header">
|
||||
<span class="questionnaire__q-num">2</span>
|
||||
<span class="questionnaire__q-title">口味偏好</span>
|
||||
<span class="questionnaire__q-required">必选(可多选)</span>
|
||||
</div>
|
||||
<div class="questionnaire__chips" id="tasteChips">
|
||||
<span class="questionnaire__chip" data-val="light" onclick="selectChip(this, 'tasteChips', 'multi')">清淡/原味</span>
|
||||
<span class="questionnaire__chip" data-val="umami" onclick="selectChip(this, 'tasteChips', 'multi')">鲜香</span>
|
||||
<span class="questionnaire__chip" data-val="salty_savory" onclick="selectChip(this, 'tasteChips', 'multi')">咸鲜</span>
|
||||
<span class="questionnaire__chip" data-val="mild_spicy" onclick="selectChip(this, 'tasteChips', 'multi')">微辣</span>
|
||||
<span class="questionnaire__chip" data-val="numbing_spicy" onclick="selectChip(this, 'tasteChips', 'multi')">麻辣</span>
|
||||
<span class="questionnaire__chip" data-val="sour_spicy" onclick="selectChip(this, 'tasteChips', 'multi')">酸辣</span>
|
||||
<span class="questionnaire__chip" data-val="sweet_sour" onclick="selectChip(this, 'tasteChips', 'multi')">酸甜</span>
|
||||
<span class="questionnaire__chip" data-val="sauce_rich" onclick="selectChip(this, 'tasteChips', 'multi')">酱香/红烧</span>
|
||||
<span class="questionnaire__chip" data-val="garlic" onclick="selectChip(this, 'tasteChips', 'multi')">蒜香/葱香</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 问题3:规避食材 -->
|
||||
<div class="questionnaire__q">
|
||||
<div class="questionnaire__q-header">
|
||||
<span class="questionnaire__q-num">3</span>
|
||||
<span class="questionnaire__q-title">规避食材(过敏 / 不喜欢)</span>
|
||||
<span class="questionnaire__q-required">选填</span>
|
||||
</div>
|
||||
<div class="questionnaire__chips" id="avoidChips"></div>
|
||||
<!-- 自定义食材标签 + 添加按钮 -->
|
||||
<div class="questionnaire__custom-avoid" id="customAvoidTags"></div>
|
||||
<div style="margin-top:8px;">
|
||||
<span class="questionnaire__chip questionnaire__chip--add" onclick="openCustomAvoidInput()">添加</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questionnaire__footer">
|
||||
<button class="questionnaire__submit" id="submitBtn" onclick="submitQuestionnaire()">请先填写必选项</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 地区选择底部Sheet(省-市级联) ===== -->
|
||||
<div class="region-mask" id="regionMask" onclick="closeRegionSheet()"></div>
|
||||
<div class="region-sheet" id="regionSheet">
|
||||
<div class="region-sheet__head">
|
||||
<span class="region-sheet__cancel" onclick="closeRegionSheet()">取消</span>
|
||||
<span class="region-sheet__title">选择所在地区</span>
|
||||
<span class="region-sheet__submit" onclick="submitRegion()">确定</span>
|
||||
</div>
|
||||
<!-- 省份视图 -->
|
||||
<div class="region-sheet__view active" id="regionProvinceView">
|
||||
<div class="region-sheet__list" id="regionProvinceList"></div>
|
||||
</div>
|
||||
<!-- 城市视图 -->
|
||||
<div class="region-sheet__view" id="regionCityView">
|
||||
<div class="region-sheet__back" onclick="showProvinceView()">
|
||||
<span class="region-sheet__back-arrow">‹</span>
|
||||
<span class="region-sheet__back-label">返回选择省份</span>
|
||||
<span class="region-sheet__back-province" id="regionBackProvince"></span>
|
||||
</div>
|
||||
<div class="region-sheet__list" id="regionCityList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 自定义规避食材底部Sheet ===== -->
|
||||
<div class="custom-avoid-mask" id="customAvoidMask" onclick="closeCustomAvoidInput()"></div>
|
||||
<div class="custom-avoid-sheet" id="customAvoidSheet">
|
||||
<div class="custom-avoid-sheet__head">
|
||||
<span class="custom-avoid-sheet__cancel" onclick="closeCustomAvoidInput()">取消</span>
|
||||
<span class="custom-avoid-sheet__title">添加规避食材</span>
|
||||
<span class="custom-avoid-sheet__submit" onclick="confirmCustomAvoid()">确定</span>
|
||||
</div>
|
||||
<div class="custom-avoid-sheet__body">
|
||||
<input type="text" id="customAvoidInput" class="custom-avoid-sheet__input" placeholder="输入食材名称,如 苦瓜、芹菜" maxlength="10" onkeydown="if(event.keyCode===13) confirmCustomAvoid()" />
|
||||
<div class="custom-avoid-sheet__quick-label">常用食材快捷添加</div>
|
||||
<div class="custom-avoid-sheet__quick" id="customAvoidQuickList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
用户偏好状态
|
||||
================================================================ */
|
||||
var userPreferences = {
|
||||
region: '',
|
||||
regionLabel: '',
|
||||
taste: [], // 多选口味
|
||||
tasteLabel: '',
|
||||
avoidFoods: []
|
||||
};
|
||||
|
||||
/* 长庆油田工作区域:省 → 地级市 */
|
||||
var REGION_DATA = [
|
||||
{ name: '陕西省', cities: ['西安市','延安市','榆林市','咸阳市'] },
|
||||
{ name: '甘肃省', cities: ['庆阳市','平凉市'] },
|
||||
{ name: '宁夏回族自治区', cities: ['银川市','吴忠市'] },
|
||||
{ name: '内蒙古自治区', cities: ['鄂尔多斯市'] },
|
||||
{ name: '山西省', cities: ['吕梁市','临汾市'] }
|
||||
];
|
||||
var REGION_OTHER = '其他地区';
|
||||
|
||||
var TASTE_MAP = { light:'清淡/原味', umami:'鲜香', salty_savory:'咸鲜', mild_spicy:'微辣', numbing_spicy:'麻辣', sour_spicy:'酸辣', sweet_sour:'酸甜', sauce_rich:'酱香/红烧', garlic:'蒜香/葱香' };
|
||||
var AVOID_MAP = { lamb:'羊肉', seafood:'海鲜', organ_meat:'动物内脏', peanut:'花生', egg:'鸡蛋', milk:'牛奶', coriander:'香菜', chili:'辣椒', garlic:'大蒜', mushroom:'菌菇' };
|
||||
|
||||
/* ================================================================
|
||||
Chip 选择(single / multi)
|
||||
================================================================ */
|
||||
function selectChip(el, containerId, mode) {
|
||||
var container = document.getElementById(containerId);
|
||||
if (mode === 'single') {
|
||||
container.querySelectorAll('.questionnaire__chip').forEach(function(c) { c.classList.remove('questionnaire__chip--active'); });
|
||||
el.classList.add('questionnaire__chip--active');
|
||||
} else {
|
||||
el.classList.toggle('questionnaire__chip--active');
|
||||
}
|
||||
updateSubmitButton();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
提交按钮状态
|
||||
================================================================ */
|
||||
function checkReady() {
|
||||
var hasRegion = userPreferences.region !== '';
|
||||
var hasTaste = document.querySelector('#tasteChips .questionnaire__chip--active') !== null;
|
||||
return hasRegion && hasTaste;
|
||||
}
|
||||
|
||||
function updateSubmitButton() {
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (checkReady()) {
|
||||
btn.textContent = '开始推荐';
|
||||
btn.classList.add('questionnaire__submit--ready');
|
||||
} else {
|
||||
btn.textContent = '请先填写必选项';
|
||||
btn.classList.remove('questionnaire__submit--ready');
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
地区 Sheet(省-市级联)
|
||||
================================================================ */
|
||||
var selectedProvince = null;
|
||||
var selectedCity = null;
|
||||
var regionMask = document.getElementById('regionMask');
|
||||
var regionSheet = document.getElementById('regionSheet');
|
||||
var regionSheetTimer = null;
|
||||
|
||||
function openRegionSheet() {
|
||||
// 立即关闭自定义食材弹窗(遮罩不延迟)
|
||||
clearTimeout(customAvoidSheetTimer);
|
||||
customAvoidSheetTimer = null;
|
||||
document.getElementById('customAvoidSheet').classList.remove('show');
|
||||
document.getElementById('customAvoidMask').classList.remove('show');
|
||||
|
||||
// 回填已选地区
|
||||
if (userPreferences.region) {
|
||||
if (userPreferences.region === REGION_OTHER) {
|
||||
selectedProvince = REGION_OTHER;
|
||||
selectedCity = '';
|
||||
showProvinceView();
|
||||
} else {
|
||||
var dash = userPreferences.region.indexOf('-');
|
||||
selectedProvince = userPreferences.region.substring(0, dash);
|
||||
selectedCity = userPreferences.region.substring(dash + 1);
|
||||
showCityView();
|
||||
}
|
||||
} else {
|
||||
selectedProvince = null;
|
||||
selectedCity = null;
|
||||
showProvinceView();
|
||||
}
|
||||
clearTimeout(regionSheetTimer);
|
||||
regionMask.classList.add('show');
|
||||
regionSheetTimer = setTimeout(function(){
|
||||
regionSheet.classList.add('show');
|
||||
regionSheetTimer = null;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeRegionSheet() {
|
||||
clearTimeout(regionSheetTimer);
|
||||
regionSheetTimer = null;
|
||||
regionSheet.classList.remove('show');
|
||||
regionMask.classList.remove('show');
|
||||
}
|
||||
|
||||
/* 渲染省份列表 */
|
||||
function renderProvinceList() {
|
||||
var html = '';
|
||||
for (var i = 0; i < REGION_DATA.length; i++) {
|
||||
var p = REGION_DATA[i].name;
|
||||
var active = selectedProvince === p ? ' active' : '';
|
||||
var sub = (active && selectedCity)
|
||||
? '<span class="region-sheet__sub">' + selectedCity + '</span>'
|
||||
: '<span class="region-sheet__sub">›</span>';
|
||||
html += '<div class="region-sheet__item region-sheet__item--parent' + active + '" onclick="selectProvince(\'' + p + '\')"><span>' + p + '</span>' + sub + '</div>';
|
||||
}
|
||||
var activeOther = selectedProvince === REGION_OTHER ? ' active' : '';
|
||||
html += '<div class="region-sheet__item' + activeOther + '" onclick="selectProvince(\'' + REGION_OTHER + '\')"><span>' + REGION_OTHER + '</span><span class="region-sheet__check"><svg viewBox="0 0 12 12"><polyline points="2,6.5 5,9.5 10,3"/></svg></span></div>';
|
||||
document.getElementById('regionProvinceList').innerHTML = html;
|
||||
}
|
||||
|
||||
/* 渲染城市列表 */
|
||||
function renderCityList() {
|
||||
var cities = [];
|
||||
for (var i = 0; i < REGION_DATA.length; i++) {
|
||||
if (REGION_DATA[i].name === selectedProvince) { cities = REGION_DATA[i].cities; break; }
|
||||
}
|
||||
var html = '';
|
||||
for (var j = 0; j < cities.length; j++) {
|
||||
var c = cities[j];
|
||||
var active = selectedCity === c ? ' active' : '';
|
||||
html += '<div class="region-sheet__item' + active + '" onclick="selectCity(\'' + c + '\')"><span>' + c + '</span><span class="region-sheet__check"><svg viewBox="0 0 12 12"><polyline points="2,6.5 5,9.5 10,3"/></svg></span></div>';
|
||||
}
|
||||
document.getElementById('regionCityList').innerHTML = html;
|
||||
}
|
||||
|
||||
function showProvinceView() {
|
||||
document.getElementById('regionProvinceView').classList.add('active');
|
||||
document.getElementById('regionCityView').classList.remove('active');
|
||||
renderProvinceList();
|
||||
}
|
||||
|
||||
function showCityView() {
|
||||
document.getElementById('regionProvinceView').classList.remove('active');
|
||||
document.getElementById('regionCityView').classList.add('active');
|
||||
document.getElementById('regionBackProvince').textContent = selectedProvince || '';
|
||||
renderCityList();
|
||||
}
|
||||
|
||||
function selectProvince(name) {
|
||||
if (name === REGION_OTHER) {
|
||||
selectedProvince = REGION_OTHER;
|
||||
selectedCity = '';
|
||||
renderProvinceList();
|
||||
} else {
|
||||
selectedProvince = name;
|
||||
selectedCity = null;
|
||||
showCityView();
|
||||
}
|
||||
}
|
||||
|
||||
function selectCity(name) {
|
||||
selectedCity = name;
|
||||
renderCityList();
|
||||
}
|
||||
|
||||
function submitRegion() {
|
||||
if (!selectedProvince) { alert('请选择所在地区'); return; }
|
||||
if (selectedProvince === REGION_OTHER) {
|
||||
userPreferences.region = REGION_OTHER;
|
||||
userPreferences.regionLabel = REGION_OTHER;
|
||||
} else {
|
||||
if (!selectedCity) { alert('请选择城市'); return; }
|
||||
userPreferences.region = selectedProvince + '-' + selectedCity;
|
||||
userPreferences.regionLabel = selectedProvince + ' ' + selectedCity;
|
||||
}
|
||||
var trigger = document.getElementById('regionTrigger');
|
||||
trigger.textContent = userPreferences.regionLabel;
|
||||
trigger.classList.add('questionnaire__trigger--filled');
|
||||
closeRegionSheet();
|
||||
updateSubmitButton();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
提交问卷
|
||||
================================================================ */
|
||||
function submitQuestionnaire() {
|
||||
if (!checkReady()) return;
|
||||
|
||||
// 收集口味(多选)
|
||||
var tasteActives = document.querySelectorAll('#tasteChips .questionnaire__chip--active');
|
||||
userPreferences.taste = [];
|
||||
var tasteLabels = [];
|
||||
tasteActives.forEach(function(c) {
|
||||
var val = c.getAttribute('data-val');
|
||||
userPreferences.taste.push(val);
|
||||
tasteLabels.push(TASTE_MAP[val] || val);
|
||||
});
|
||||
userPreferences.tasteLabel = tasteLabels.join('、');
|
||||
|
||||
// 规避食材已由自定义标签系统管理,直接使用 avoidFoods
|
||||
|
||||
// 存入 localStorage
|
||||
localStorage.setItem('dietPreferences', JSON.stringify(userPreferences));
|
||||
|
||||
// 跳转到膳食建议页
|
||||
location.href = 'diet-advice.html';
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
自定义规避食材 — 底部输入 Sheet
|
||||
================================================================ */
|
||||
var CUSTOM_AVOID_QUICK = ['苦瓜','芹菜','茄子','胡萝卜','青椒','香菇','洋葱','韭菜','虾皮','豆腐','西红柿','土豆','山药','冬瓜','南瓜','菠菜','白菜','豆芽','海带','竹笋'];
|
||||
var customAvoidSheetTimer = null;
|
||||
|
||||
function openCustomAvoidInput() {
|
||||
// 立即关闭地区弹窗(遮罩不延迟,避免两个弹窗同时出现)
|
||||
clearTimeout(regionSheetTimer);
|
||||
regionSheetTimer = null;
|
||||
regionSheet.classList.remove('show');
|
||||
regionMask.classList.remove('show');
|
||||
|
||||
renderCustomAvoidQuick();
|
||||
clearTimeout(customAvoidSheetTimer);
|
||||
document.getElementById('customAvoidMask').classList.add('show');
|
||||
customAvoidSheetTimer = setTimeout(function(){
|
||||
document.getElementById('customAvoidSheet').classList.add('show');
|
||||
document.getElementById('customAvoidInput').focus();
|
||||
customAvoidSheetTimer = null;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeCustomAvoidInput() {
|
||||
clearTimeout(customAvoidSheetTimer);
|
||||
customAvoidSheetTimer = null;
|
||||
document.getElementById('customAvoidSheet').classList.remove('show');
|
||||
document.getElementById('customAvoidMask').classList.remove('show');
|
||||
document.getElementById('customAvoidInput').value = '';
|
||||
}
|
||||
|
||||
function renderCustomAvoidQuick() {
|
||||
var avoid = userPreferences.avoidFoods;
|
||||
var html = '';
|
||||
for (var i = 0; i < CUSTOM_AVOID_QUICK.length; i++) {
|
||||
var name = CUSTOM_AVOID_QUICK[i];
|
||||
var added = avoid.indexOf(name) !== -1;
|
||||
html += '<span class="custom-avoid-sheet__quick-tag' + (added ? ' custom-avoid-sheet__quick-tag--added' : '') + '" data-name="' + name + '" onclick="toggleCustomAvoidQuick(this)">' + name + '</span>';
|
||||
}
|
||||
document.getElementById('customAvoidQuickList').innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleCustomAvoidQuick(el) {
|
||||
var name = el.getAttribute('data-name');
|
||||
var idx = userPreferences.avoidFoods.indexOf(name);
|
||||
if (idx === -1) {
|
||||
userPreferences.avoidFoods.push(name);
|
||||
} else {
|
||||
userPreferences.avoidFoods.splice(idx, 1);
|
||||
}
|
||||
renderCustomAvoidQuick();
|
||||
renderCustomAvoidTags();
|
||||
}
|
||||
|
||||
function confirmCustomAvoid() {
|
||||
var input = document.getElementById('customAvoidInput');
|
||||
var name = input.value.trim();
|
||||
if (!name) { closeCustomAvoidInput(); return; }
|
||||
|
||||
// 去重
|
||||
if (userPreferences.avoidFoods.indexOf(name) !== -1) {
|
||||
alert('「' + name + '」已在规避列表中');
|
||||
return;
|
||||
}
|
||||
|
||||
userPreferences.avoidFoods.push(name);
|
||||
closeCustomAvoidInput();
|
||||
renderCustomAvoidTags();
|
||||
}
|
||||
|
||||
function removeCustomAvoidTag(el) {
|
||||
var name = el.getAttribute('data-name');
|
||||
var idx = userPreferences.avoidFoods.indexOf(name);
|
||||
if (idx !== -1) userPreferences.avoidFoods.splice(idx, 1);
|
||||
renderCustomAvoidTags();
|
||||
}
|
||||
|
||||
function renderCustomAvoidTags() {
|
||||
var container = document.getElementById('customAvoidTags');
|
||||
var avoid = userPreferences.avoidFoods;
|
||||
var presetKeys = Object.keys(AVOID_MAP);
|
||||
var html = '';
|
||||
for (var i = 0; i < avoid.length; i++) {
|
||||
var item = avoid[i];
|
||||
// 只渲染自定义食材(不在预置 key 中的)
|
||||
if (presetKeys.indexOf(item) === -1) {
|
||||
html += '<span class="questionnaire__custom-tag" data-name="' + item + '" onclick="removeCustomAvoidTag(this)">' + item + '</span>';
|
||||
}
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
返回按钮
|
||||
================================================================ */
|
||||
function goBack() {
|
||||
history.back();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
初始化 — 回填已有偏好
|
||||
================================================================ */
|
||||
function init() {
|
||||
// 尝试从 localStorage 读取已有偏好("修改偏好"场景)
|
||||
var saved = localStorage.getItem('dietPreferences');
|
||||
if (saved) {
|
||||
try {
|
||||
var prefs = JSON.parse(saved);
|
||||
userPreferences = prefs;
|
||||
|
||||
// 回填地区
|
||||
if (prefs.region) {
|
||||
var trigger = document.getElementById('regionTrigger');
|
||||
trigger.textContent = prefs.regionLabel || prefs.region;
|
||||
trigger.classList.add('questionnaire__trigger--filled');
|
||||
}
|
||||
|
||||
// 回填口味(多选)
|
||||
if (prefs.taste && prefs.taste.length) {
|
||||
var tasteArr = Array.isArray(prefs.taste) ? prefs.taste : [prefs.taste]; // 兼容旧版单字符串
|
||||
document.querySelectorAll('#tasteChips .questionnaire__chip').forEach(function(c) {
|
||||
if (tasteArr.indexOf(c.getAttribute('data-val')) !== -1) c.classList.add('questionnaire__chip--active');
|
||||
});
|
||||
}
|
||||
|
||||
// 回填规避食材(自定义标签)
|
||||
renderCustomAvoidTags();
|
||||
|
||||
updateSubmitButton();
|
||||
} catch(e) {
|
||||
// 解析失败则忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 无历史偏好时,默认选中常用口味
|
||||
if (!userPreferences.taste || !userPreferences.taste.length) {
|
||||
var defaultTastes = ['light', 'salty_savory'];
|
||||
document.querySelectorAll('#tasteChips .questionnaire__chip').forEach(function(c) {
|
||||
if (defaultTastes.indexOf(c.getAttribute('data-val')) !== -1) c.classList.add('questionnaire__chip--active');
|
||||
});
|
||||
updateSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,442 @@
|
||||
# 体重管理 · 膳食建议模块 需求文档
|
||||
|
||||
> 整合自《高血压、高尿酸干预行动项目方案(修订版)》项目诉求 + 员工端 APP 体重管理模块现有业务
|
||||
> 面向:产品 / 健康平台开发方 / AI 算法团队 / 医学顾问
|
||||
> 版本:v1.0 · 2026-07-03
|
||||
|
||||
---
|
||||
|
||||
## 一、文档目的与范围
|
||||
|
||||
本文档围绕员工端 APP **体重管理模块 → 膳食建议子模块**(`diet-advice.html`),完成三件事:
|
||||
|
||||
1. **诉求分析**:从项目方案文档中提取对"膳食建议"的直接诉求;
|
||||
2. **数据结合逻辑**:说明如何把员工健康数据、慢病数据、饮食打卡数据、个人偏好数据结合,输出**针对性个性化膳食建议**;
|
||||
3. **移动端落地**:明确页面交互与展示方式,并列出全部需要准备的资料清单。
|
||||
|
||||
范围限定在"膳食建议"页面本身及其数据上下游;运动建议、体重打卡等兄弟模块仅作为数据来源提及。
|
||||
|
||||
---
|
||||
|
||||
## 二、项目背景与诉求分析
|
||||
|
||||
### 2.1 项目背景
|
||||
|
||||
机械制造总厂员工因工作性质(部分岗位体力劳动强度大、倒班作息),**高血压、高尿酸**患病率高于普通人群。现有健康管理在"饮食干预"上存在三大痛点,这三大痛点直接构成膳食建议模块要解决的核心诉求。
|
||||
|
||||
### 2.2 三大痛点 → 模块诉求
|
||||
|
||||
| # | 痛点(项目方案原文) | 对膳食建议模块的诉求 |
|
||||
|---|---|---|
|
||||
| 1 | 通用膳食建议与员工口味偏好(如西北员工喜辛辣、面食)脱节,执行度低 | 建议**必须贴合口味偏好**,改良而非替代(如辛辣口味→"低盐油泼面"而非强制清淡) |
|
||||
| 2 | 慢病饮食规则(低盐、低嘌呤)与当地饮食习惯(偏好羊肉、浓肉汤)冲突,员工难平衡 | 建议**必须同时满足慢病管控规则 + 地区饮食适配**,本地食材占比≥70% |
|
||||
| 3 | 健康平台仅提供基础体重管理,未针对"双高"人群输出个性化饮食方案,与油田干预体系缺乏数据联动 | 建议**必须为"高血压+高尿酸"等双高/合并慢病人群**输出个性化方案,并与平台数据联动(同步延迟≤1h) |
|
||||
|
||||
### 2.3 项目核心目标(量化,作为本模块验收基线)
|
||||
|
||||
| 维度 | 目标 |
|
||||
|---|---|
|
||||
| 膳食建议符合《高血压/高尿酸膳食指导》率 | ≥ 95% |
|
||||
| 口味/地区适配符合率(西北员工食谱本地食材占比≥70%) | ≥ 92% |
|
||||
| 核心营养指标(钠、嘌呤)计算偏差率 | ≤ 8% |
|
||||
| 与健康长庆平台体重管理模块数据同步延迟 | ≤ 1 小时 |
|
||||
| 与油田公司干预体系数据对接成功率 | 100% |
|
||||
| 接口响应(1000 并发)平均/峰值 | ≤500ms / ≤1s,稳定性≥99.9% |
|
||||
|
||||
### 2.4 对膳食建议模块的直接结论
|
||||
|
||||
膳食建议不是"给一份通用食谱",而是要做一个**数据驱动的个性化推荐引擎 + 移动端落地页**,做到:
|
||||
|
||||
- **合规**:钠、嘌呤、热量严格按慢病等级限值;
|
||||
- **个性**:口味/地区/班次三维度适配,改良而非替代;
|
||||
- **联动**:读取平台健康数据,回写打卡/反馈,形成闭环。
|
||||
|
||||
---
|
||||
|
||||
## 三、模块业务定位与上下游关系
|
||||
|
||||
### 3.1 在体重管理模块中的位置
|
||||
|
||||
膳食建议是体重管理主页(`weight-main.html`)右侧"建议入口"之一,与"运动建议""报告"并列。主页的"吃打卡"卡片(今日摄入 kcal)与膳食建议共享同一套热量/营养数据底座。
|
||||
|
||||
```
|
||||
体重管理主页 weight-main.html
|
||||
├── 吃/动/体重 今日打卡
|
||||
│ └── 吃打卡 eat-checkin.html ──► 消费记录 / 今日已摄入 ←──┐
|
||||
├── 减重进度 / 吃动平衡 │
|
||||
├── 体重记录 / BMI │
|
||||
└── 建议入口
|
||||
├── 膳食建议 diet-advice.html ◄── 本文档核心 ──────────┘
|
||||
├── 运动建议 exercise-advice.html
|
||||
└── 报告 report-detail.html
|
||||
```
|
||||
|
||||
### 3.2 上下游数据来源(已落地页面 → 字段)
|
||||
|
||||
| 来源页面 | 提供的字段 | 用于哪一层 |
|
||||
|---|---|---|
|
||||
| `basic-info.html`(基础信息) | 身高(cm)、体力活动等级(5 档) | 第一层 BMR / 活动系数 |
|
||||
| `weight-checkin.html` / `indicators-diseases.html`(体重指标) | 最新体重(kg)、BMI | 第一层 BMR |
|
||||
| `indicators-diseases.html`(指标慢病) | 收缩压/舒张压、血尿酸、血糖、血脂;慢病确诊列表(高血压、肾病、糖尿病…) | 第二层慢病分级 / 合并调整 |
|
||||
| `eat-checkin.html`(吃打卡) | 今日各餐已摄入 kcal、碳水/脂肪/蛋白质;菜品克数记录 | "已摄入/还可摄入"展示 + 钠/嘌呤累计 |
|
||||
| `diet-advice.html`(本页引导) | 所在地区、口味偏好、上班时间、规避食材 | 第三层个性化适配 |
|
||||
| 智能餐线系统(外部) | 近 6 个月消费记录(脱敏:菜品类型+频次) | 第三层口味偏好提取/校准 |
|
||||
|
||||
> 年龄、性别由员工档案/身份证自动计算,不要求员工在膳食页手填。
|
||||
|
||||
### 3.3 数据流转闭环
|
||||
|
||||
```
|
||||
档案/体检/打卡/偏好 ─► 算法接口(三层) ─► 个性化食谱 ─► 膳食建议页展示
|
||||
▲ │
|
||||
│ 员工标记"不喜欢"/替换菜品/反馈 │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
员工在膳食建议页的"不喜欢食材"标记、对菜品的替换/反馈,会回写偏好库,下次调用接口自动剔除——这是项目方案明确要求的"个性化调整"能力。
|
||||
|
||||
---
|
||||
|
||||
## 四、个性化膳食建议生成逻辑(数据如何结合)
|
||||
|
||||
### 4.1 总体架构:"三层规则 + AI 推理"
|
||||
|
||||
项目方案指定采用混合架构,本模块按此落地:
|
||||
|
||||
```
|
||||
输入数据 → 第一层 基础营养层(固定公式,确定性)
|
||||
→ 第二层 慢病管控层(AI 动态调整阈值优先级)
|
||||
→ 第三层 个性化适配层(AI 推理 + 规则库,生成食谱)
|
||||
→ 输出:当日三餐+加餐 / 一周食谱 / 慢病提醒
|
||||
```
|
||||
|
||||
- 第一层无需模型,确定性计算,保证合规底线;
|
||||
- 第二层用模型按"慢病组合"调整营养阈值与优先级;
|
||||
- 第三层用模型 + 规则库(地区食材库、口味标签、菜品营养标注)生成可执行的个性化食谱。
|
||||
|
||||
### 4.2 数据输入清单
|
||||
|
||||
#### 4.2.1 系统自动获取(员工无感)
|
||||
|
||||
| 字段 | 来源 | 用途 |
|
||||
|---|---|---|
|
||||
| 用户ID(哈希化) | 登录态脱敏 | 全链路标识 |
|
||||
| 身高 | `basic-info.html` | BMR |
|
||||
| 体重 | 最近一次 `weight-checkin.html` | BMR |
|
||||
| 年龄/性别 | 员工档案 | BMR 公式选择 |
|
||||
| 体力活动等级 | `basic-info.html`(5 档) | 活动系数 |
|
||||
| 收缩压/舒张压 | `indicators-diseases.html` | 高血压分级 |
|
||||
| 血尿酸 | `indicators-diseases.html` | 高尿酸分期 |
|
||||
| 血糖/血脂 | `indicators-diseases.html` | 合并慢病判断 |
|
||||
| 慢病确诊(高血压/肾病/糖尿病…) | `indicators-diseases.html` 慢性病 tab | 第二层合并调整 |
|
||||
| 智能餐线消费记录 | 餐线系统脱敏(菜品类型+频次) | 口味偏好提取/校准 |
|
||||
| 今日已摄入 kcal/营养 | `eat-checkin.html` | 热量缺口与"还可摄入"展示 |
|
||||
|
||||
#### 4.2.2 问卷/页面采集(员工主动填写)
|
||||
|
||||
在膳食建议页**首次进入引导**中采集(详见 5.2):
|
||||
|
||||
| 字段 | 取值 | 用途 |
|
||||
|---|---|---|
|
||||
| 所在地区 | 西安/榆林/延安/汉中/安康/其他 | 地区食材库匹配 |
|
||||
| 口味偏好 | 清淡/辛辣/咸鲜/酸甜 | 口味适配 |
|
||||
| 上班时间 | 正常白班/早班/晚班/倒班 | 场景适配(餐次热量分配) |
|
||||
| 规避食材 | 羊肉/海鲜/动物内脏/香菜/沙棘… +自定义 | 排除特定食材 |
|
||||
|
||||
#### 4.2.3 外部数据(AI/医学团队预整理,非实时调用)
|
||||
|
||||
| 资料 | 来源 | 用途 |
|
||||
|---|---|---|
|
||||
| 西北/陕北/陕南地区常见食材库 | 地方疾控《居民膳食报告》+ 本地餐饮协会 | 地区匹配,目标占比≥70% |
|
||||
| 餐品嘌呤等级(高/中/低) | 团队预标注 | 第二层嘌呤分级 |
|
||||
| 餐品钠含量(mg/100g) | 团队预标注 | 第二层钠计算 |
|
||||
| 菜品-口味标签映射 | 团队预标注 | 第三层从消费记录提取口味 |
|
||||
| 菜品碳水/脂肪/蛋白质/ GI | 团队预标注 | 宏量分配、糖尿病低GI |
|
||||
|
||||
#### 4.2.4 数据校验规则(接口前置)
|
||||
|
||||
| 字段 | 校验 |
|
||||
|---|---|
|
||||
| 血尿酸 | 150–1000 μmol/L,超限返回"数据异常" |
|
||||
| 口味偏好 | 仅枚举;"微辣"等非标值自动映射为"辛辣" |
|
||||
| 身高 | 100–250 cm |
|
||||
| 体重 | 30–300 kg |
|
||||
| 年龄 | 18–70 岁 |
|
||||
| 血压 | 收缩压 60–250 / 舒张压 30–150 mmHg |
|
||||
|
||||
### 4.3 第一层:基础营养计算(确定性公式)
|
||||
|
||||
**BMR(Mifflin-St Jeor)**
|
||||
```
|
||||
男:BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 + 5
|
||||
女:BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 - 161
|
||||
```
|
||||
> 注:项目方案给出的是企业版公式(男性+166、女性不变),落地时与平台 `basic-info.html` 现有口径对齐,二选一并在文档固化。
|
||||
|
||||
**活动系数**(与 `basic-info.html` 体力活动 5 档一一对应)
|
||||
|
||||
| 员工选项 | 系数 |
|
||||
|---|---|
|
||||
| 久坐不运动 | 1.2 |
|
||||
| 久坐运动或久站 | 1.375 |
|
||||
| 体力劳动者 | 1.55 |
|
||||
| 久站+运动 | 1.725 |
|
||||
| 健身+体力劳动 | 1.9 |
|
||||
|
||||
**总热量**
|
||||
```
|
||||
总热量 = BMR × 活动系数 - 300(固定热量缺口,对应减重目标)
|
||||
```
|
||||
|
||||
**三大宏量供能比(固定)**:蛋白质 15% / 碳水 55% / 脂肪 30%(÷4、÷4、÷9 得克数)。
|
||||
|
||||
### 4.4 第二层:慢病管控(AI 动态调整)
|
||||
|
||||
#### 4.4.1 高血压 → 钠控制
|
||||
|
||||
| 等级 | 判定 | 钠上限/日 | ≈盐 |
|
||||
|---|---|---|---|
|
||||
| 正常 | SBP<140 且 DBP<90 | 无特殊限制 | — |
|
||||
| 1 级 | SBP 140–159 或 DBP 90–99 | ≤2000mg | ≈5g |
|
||||
| 2 级 | SBP≥160 或 DBP≥100 | ≤1500mg | ≈3.75g |
|
||||
| 合并肾病 | 任意级+肾病 | ≤1200mg | ≈3g |
|
||||
|
||||
约束:**每餐钠不超过日上限 40%**,防止单次超标。
|
||||
|
||||
#### 4.4.2 高尿酸 → 嘌呤控制
|
||||
|
||||
| 分期 | 判定 | 嘌呤上限/日 | 处理 |
|
||||
|---|---|---|---|
|
||||
| 无症状期 | 血尿酸 420–600 μmol/L | ≤300mg | 直接排除动物内脏、沙丁鱼 |
|
||||
| 急性发作期 | >600 或近期发作 | ≤150mg | 海鲜类一并排除 |
|
||||
| 痛风石 | 已确诊痛风石 | ≤100mg | 仅保留极低嘌呤食材 |
|
||||
|
||||
#### 4.4.3 合并慢病 → AI 调整优先级
|
||||
|
||||
| 组合 | 调整 |
|
||||
|---|---|
|
||||
| 高血压 + 高尿酸 | 优先控钠和嘌呤;钾 2500→2800mg/日 |
|
||||
| 高尿酸 + 肾病 | 优先控嘌呤和蛋白质;蛋白质 1.2→0.8g/kg |
|
||||
| 高血压 + 糖尿病 | 碳水中低 GI 占比≥60%;钠按等级控制 |
|
||||
|
||||
> 第二层的输出是"营养阈值集合 + 优先级",交给第三层作为生成约束。
|
||||
|
||||
### 4.5 第三层:个性化适配(AI 推理 + 规则库)
|
||||
|
||||
#### 4.5.1 口味适配(改良而非替代)
|
||||
- 从消费记录提取高频菜品 → 匹配口味标签;
|
||||
- 例:高频"油泼面"→辛辣 → 推荐"低盐油泼面"(钠减少 30%),**而非强制清淡菜**;
|
||||
- 原则:保留风味,仅降低钠/嘌呤/热量。
|
||||
|
||||
#### 4.5.2 地区适配
|
||||
- 食谱生成时优先从对应地区食材库选材,**本地食材占比≥70%**;
|
||||
- 西安/陕北→杂粮、羊肉(改良做法)、土豆、菠菜;陕南→米食、水产(改良) 等;
|
||||
- 早餐优先"杂粮粥+全麦馒头"而非"白粥+包子"。
|
||||
|
||||
#### 4.5.3 场景适配(按上班时间分配餐次热量)
|
||||
|
||||
| 上班时间 | 策略 |
|
||||
|---|---|
|
||||
| 正常白班 | 早/午/晚 = 30%/40%/30% |
|
||||
| 早班 | 早餐上调至 35%,凌晨餐推荐低 GI 主食 |
|
||||
| 晚班 | 晚餐为主餐 40%,夜间加餐推荐低 GI(燕麦/荞麦)+高蛋白(卤牛肉),避免血糖波动 |
|
||||
| 倒班 | 按当日实际班次动态调整,优先保证蛋白质稳定 |
|
||||
|
||||
### 4.6 端到端数据结合示例(走查)
|
||||
|
||||
> **画像**:45 岁男,身高 175cm,体重 72.5kg,体力劳动者,收缩压 145、血尿酸 526,辛辣口味,西安地区,正常白班,不吃羊肉。
|
||||
|
||||
| 步骤 | 数据结合 | 结果 |
|
||||
|---|---|---|
|
||||
| ①第一层 | BMR=1900 × 1.55 - 300 | 总热量 2645 kcal |
|
||||
| ②第二层 | SBP 145→高血压 1 级;尿酸 526→无症状期 | 钠≤2000mg/日,嘌呤≤300mg/日;剔除动物内脏/沙丁鱼 |
|
||||
| ③第三层·口味 | 辛辣→保留西北风味但降盐 | "低盐油泼面"(钠-30%) |
|
||||
| ③第三层·地区 | 西安→西北食材库≥70% | 杂粮粥、全麦馒头、土豆烧牛肉(牛肉焯水去嘌呤) |
|
||||
| ③第三层·场景 | 正常白班 | 早/午/晚=30/40/30 |
|
||||
| ③第三层·规避 | 不吃羊肉 | 自动剔除含羊肉菜品 |
|
||||
| ④展示 | 叠加 eat-checkin 今日已摄入 1486 | 还可摄入 1159,热量缺口 300 |
|
||||
|
||||
输出即 `diet-advice.html` 上所见:慢病提醒横幅 + 热量目标卡 + 钠/嘌呤进度 + 三餐推荐 + 一周食谱。
|
||||
|
||||
---
|
||||
|
||||
## 五、移动端页面交互与展示设计
|
||||
|
||||
> 已在 `diet-advice.html` 落地原型,以下为交互/展示规范,供开发方 1:1 实现。
|
||||
|
||||
### 5.1 页面结构总览(自上而下)
|
||||
|
||||
| 区块 | 作用 | 数据来源 |
|
||||
|---|---|---|
|
||||
| ① 偏好标签行 | 一眼看到当前个性化维度 | 引导采集的 region/taste/schedule |
|
||||
| ② 慢病提醒横幅 | 红色/黄色突出慢病约束与自动剔除动作 | 第二层 + `warning` |
|
||||
| ③ 每日热量目标卡 | 大数字+进度条,BMR×系数、已摄入/还可摄入、缺口 | 第一层 + eat-checkin |
|
||||
| ④ 营养素分配卡 | 蛋白质/碳水/脂肪环形进度 + 钠/嘌呤慢病管控条 | 第一层宏量 + 第二层阈值 |
|
||||
| ⑤ 今日三餐推荐 | 早/午/晚/加餐 tab,每道菜图标+名称+标签+份量+烹饪建议+kcal | 第三层 `meals` |
|
||||
| ⑥ 一周食谱概览 | 7 天切换,每天四餐合计 | `weekly_plan` |
|
||||
| ⑦ 不喜欢食材 | 标签开关 + 自定义,即时刷新推荐 | 偏好回写 |
|
||||
| ⑧ 膳食注意事项 | 分层:慢病管控/通用健康/地区适配 | 第二层 rule + 静态内容 |
|
||||
|
||||
### 5.2 首次引导:偏好设置(覆盖层)
|
||||
|
||||
**触发**:首次进入或点击右上"修改偏好"。引导覆盖层占满,主内容隐藏。
|
||||
|
||||
| 序号 | 问题 | 控件 | 必填 |
|
||||
|---|---|---|---|
|
||||
| 1 | 所在地区 | 底部 sheet 单选(西安/榆林/延安/汉中/安康/其他) | 必选 |
|
||||
| 2 | 口味偏好 | chip 单选(清淡/辛辣/咸鲜/酸甜) | 必选 |
|
||||
| 3 | 上班时间 | chip 单选(正常白班/早班/晚班/倒班) | 必选 |
|
||||
| 4 | 规避食材 | chip 多选(羊肉/海鲜/动物内脏/香菜/沙棘/其他)+ 自定义 | 选填 |
|
||||
|
||||
- 底部提交按钮:未填齐必选时置灰"请先填写必选项",填齐后高亮"开始推荐";
|
||||
- "修改偏好"回填上次选择,支持局部调整后重新生成。
|
||||
|
||||
### 5.3 各区块交互细节
|
||||
|
||||
**① 偏好标签行**:三个 pill(地区·口味·班次),点击无动作仅展示;地区变更后⑧地区适配文案随之更新。
|
||||
|
||||
**② 慢病提醒横幅**:
|
||||
- 有合并慢病/超标→红色(warn);仅提示→黄色;
|
||||
- 文案突出"已自动剔除"的具体食材(如"已剔除动物内脏、沙丁鱼"),让员工感知到个性化。
|
||||
|
||||
**③ 热量目标卡**:
|
||||
- 大数字=每日推荐摄入;副标题=BMR × 活动系数(标注档位名);
|
||||
- 进度条=已摄入/目标;下方"还可摄入 X 千卡(热量缺口 300kcal)";
|
||||
- 已摄入数据来自 `eat-checkin.html` 当日累计,实时联动。
|
||||
|
||||
**④ 营养素 + 慢病管控**:
|
||||
- 三个环形:蛋白质/碳水/脂肪(目标克数 + 供能比);
|
||||
- 下方两条慢病进度:钠(橙红渐变)、嘌呤(黄橙渐变),显示"当前/上限 mg"+ 状态(✓安全 / △注意 / ✗超标);
|
||||
- 状态色:安全绿、注意黄、超标红,超标时点击可跳转注意事项。
|
||||
|
||||
**⑤ 今日三餐推荐**:
|
||||
- 卡片式,每道菜:图标(主食/蔬菜/肉/汤 色块) + 名称 + 徽章(低钠/低嘌呤/本地食材)+ 份量描述 + 💡烹饪建议 + 右侧 kcal;
|
||||
- tab 切换早/午/晚/加餐,带"AI 推荐理由"副标题(拼出"辛辣口味·西安·正常白班");
|
||||
- **交互**:点击菜品→展开详细烹饪步骤与食材称重参考(项目方案要求的"100g 鸡胸肉≈手掌大小"图示);
|
||||
- **替换**:菜品右滑/长按→"换一道",基于同营养档位重选,剔除规避食材。
|
||||
|
||||
**⑥ 一周食谱**:7 天日期切换,选中天显示四餐合计 kcal 与菜品清单;支持"导入今日打卡"一键把某餐写入 `eat-checkin`。
|
||||
|
||||
**⑦ 不喜欢食材**:标签即时开关,新增/取消后立即重渲染当前餐次;自定义食材用输入框;提示"已标记 N 项,今日食谱已自动排除"。
|
||||
|
||||
**⑧ 注意事项**:三层分组——🏥慢病管控(限钠控盐、低嘌呤)、🥗通用健康(少食多餐、饮水、食材多样)、🌍地区适配(本地食材优先文案)。
|
||||
|
||||
### 5.4 状态与异常处理
|
||||
|
||||
| 场景 | 处理 |
|
||||
|---|---|
|
||||
| 必要数据缺失(如未填体力活动) | 引导跳转 `basic-info.html` 补全,膳食页显示"完善基础信息后生成专属方案"占位 |
|
||||
| 体检数据过期(>1 年) | 横幅提示"体检数据为 X 年,建议更新",仍按历史值降级生成 |
|
||||
| 接口超时/无网络 | 展示上次缓存食谱 + 离线提示(见 5.5) |
|
||||
| 钠/嘌呤当日累计超标 | 横幅变红 + 推荐里自动剔除超标食材 + 注意事项高亮 |
|
||||
|
||||
### 5.5 离线缓存
|
||||
|
||||
项目方案明确要求适配部分车间无 WiFi 场景:
|
||||
- 接口返回后本地缓存当日食谱与一周食谱;
|
||||
- 无网络时展示缓存版,顶部标注"离线数据,可能非最新";
|
||||
- 偏好/不喜欢食材标记本地暂存,联网后回写。
|
||||
|
||||
---
|
||||
|
||||
## 六、所需准备的资料清单
|
||||
|
||||
> 按"谁准备 / 何时用"分类,全部到位后方可启动开发与模型微调。
|
||||
|
||||
### 6.1 数据类(算法/平台方提供)
|
||||
|
||||
| # | 资料 | 来源/责任方 | 用途 | 隐私处理 |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 员工基础档案(身高/年龄/性别) | 健康平台 | 第一层 BMR | 仅取计算字段 |
|
||||
| D2 | 体重打卡记录 | `weight-checkin.html` | BMR、趋势 | — |
|
||||
| D3 | 年度体检报告(血压/血尿酸/血糖/血脂) | 体检系统→`indicators-diseases.html` | 第二层慢病分级 | API 取核心字段,不存完整体检 |
|
||||
| D4 | 慢病确诊清单(高血压/肾病/糖尿病…) | `indicators-diseases.html` 慢病 tab | 第二层合并调整 | — |
|
||||
| D5 | 智能餐线近 6 个月消费记录 | 餐线系统/营养监控平台 | 第三层口味提取 | 脱敏:仅菜品类型+频次,剔除姓名/工号 |
|
||||
| D6 | 吃打卡菜品库与当日记录 | `eat-checkin.html` | 已摄入/钠/嘌呤累计 | — |
|
||||
| D7 | 企业 500 条员工健康数据(含慢病类型) | 平台脱敏导出 | 模型微调1(慢病适配) | 用户ID哈希化 |
|
||||
| D8 | 员工口味/地区饮食数据样本 | 问卷/消费记录 | 模型微调2(个性化适配) | 同上 |
|
||||
|
||||
### 6.2 内容/知识库类(AI/医学团队预整理)
|
||||
|
||||
| # | 资料 | 内容要求 | 用途 |
|
||||
|---|---|---|---|
|
||||
| K1 | 西北/陕北/陕南地区食材库 | 莜面、土豆、菠菜、羊肉、小米、全麦粉、沙棘果…,含季节标注 | 地区匹配≥70% |
|
||||
| K2 | 餐品嘌呤等级表 | 高/中/低三级,覆盖常见菜品 | 嘌呤分级排除 |
|
||||
| K3 | 餐品钠含量表 | mg/100g,覆盖常见菜品与调料 | 钠计算 |
|
||||
| K4 | 菜品-口味标签映射 | 油泼面→辛辣 等 | 口味适配 |
|
||||
| K5 | 菜品营养库 | 每 100g 的碳水/脂肪/蛋白质/kcal/GI | 宏量分配、糖尿病低GI |
|
||||
| K6 | 烹饪改良话术库 | "牛肉焯水去嘌呤""香料替盐"等 | 烹饪建议展示 |
|
||||
| K7 | 食材称重参考图示 | 100g 鸡胸≈手掌、100g 米饭≈拳头… | 份量参考展示 |
|
||||
| K8 | 《高血压/高尿酸膳食指导》权威条款 | 作为合规校验基线 | 验收符合率≥95% |
|
||||
|
||||
### 6.3 设计/素材类(产品/设计提供)
|
||||
|
||||
| # | 资料 | 说明 |
|
||||
|---|---|---|
|
||||
| S1 | 膳食建议页高保真稿 | 基于 `diet-advice.html` 细化,含菜品图、称重图 |
|
||||
| S2 | 菜品图标/缩略图素材 | 主食/蔬菜/肉/汤四类色块 + 实物图 |
|
||||
| S3 | 引导覆盖层插画 | hero 区"定制专属膳食方案"配图 |
|
||||
| S4 | 状态/空态/异常态图 | 数据缺失、离线、超标等 |
|
||||
|
||||
### 6.4 技术对接类(算法团队交付)
|
||||
|
||||
| # | 资料 | 形式 | 说明 |
|
||||
|---|---|---|---|
|
||||
| T1 | 算法 API 接口文档 | RESTful/HTTPS/POST/JSON | 请求/响应字段、错误码、Postman 示例 |
|
||||
| T2 | 规则配置文件 | XML | 非技术人员可改:地区食材库/营养阈值/口味维度 |
|
||||
| T3 | 《模型使用手册》 | 文档 | 接口调用 + 规则配置教程 + 维护周期(季度更新食材库、半年更新阈值) |
|
||||
| T4 | 《数据对接规范》 | 文档 | 输入输出格式 + 校验规则(见 4.2.4) |
|
||||
| T5 | 《测试报告》 | 文档 | ≥10 类场景 + 性能测试(响应/并发/稳定性) |
|
||||
| T6 | 《维护手册》 | 文档 | 常见配置错误案例与解决方案 |
|
||||
|
||||
### 6.5 资料准备优先级(建议排期)
|
||||
|
||||
1. **先阻塞模型**:D7、D8(微调数据)+ K1–K5(知识库)→ 第 6–15 工作日;
|
||||
2. **并行页面**:S1–S4 + 复用现有 `diet-advice.html` 原型 → 第 1–5 工作日;
|
||||
3. **后阻塞对接**:T1 接口文档先出 → 第 15 工作日联调;T2–T6 随交付;
|
||||
4. **持续**:D5 餐线记录、K8 合规基线贯穿验收。
|
||||
|
||||
---
|
||||
|
||||
## 七、验收标准(映射项目方案)
|
||||
|
||||
### 7.1 功能验收
|
||||
|
||||
| 验收项 | 方法 | 合格标准 |
|
||||
|---|---|---|
|
||||
| 膳食建议合规性 | 随机抽 20 条食谱对照《膳食指导》 | 符合率≥95% |
|
||||
| 口味/地区适配 | 30 名员工(覆盖口味/地区)评分 1–5 分 | 平均≥4.2 分(≈92%) |
|
||||
| 钠/嘌呤计算 | 复算抽检 | 偏差率≤8% |
|
||||
| 接口响应 | Jmeter 1000 并发 | 平均≤500ms,无≥3s 超时 |
|
||||
| 体系对接 | 平台改 10 条数据看油田体系同步 | 成功率100%,延迟≤1h |
|
||||
| 个性化剔除 | 标记规避食材后刷新 | 含该食材菜品 100% 不出现 |
|
||||
|
||||
### 7.2 交付物验收
|
||||
|
||||
| 交付物 | 方法 | 标准 |
|
||||
|---|---|---|
|
||||
| AI 模型 + 接口 | 调 10 条数据 | 完整度100%,偏差≤8% |
|
||||
| 4 类技术资料(T3–T6) | 抽 3 个步骤由开发方执行 | 成功率100%,无歧义 |
|
||||
| 测试报告(T5) | 复现 5 场景核对输入-输出-偏差 | 100% 一致 |
|
||||
| 规则配置文件(T2) | 改阈值/食材库后验证生效 | 100% 生效 |
|
||||
|
||||
---
|
||||
|
||||
## 八、附:与现有原型的差异/待补点
|
||||
|
||||
> 基于 `diet-advice.html` 现状,列出落地时需补齐的差距。
|
||||
|
||||
| 现状 | 待补 |
|
||||
|---|---|
|
||||
| BMR/热量为 mock 常量 | 接入 `basic-info.html` 真实身高/活动等级,按 4.3 公式计算 |
|
||||
| 已摄入 1486 为写死 | 接 `eat-checkin.html` 当日累计,且 eat-checkin 菜品库需补钠/嘌呤字段以支撑慢病进度条 |
|
||||
| 三餐数据按口味预置 4 套静态库 | 改为调算法接口动态生成,支持规避/替换 |
|
||||
| 慢病分级(高血压1级/无症状期)写死 | 接 `indicators-diseases.html` 血压/尿酸/慢病自动分级 |
|
||||
| 无菜品详情/称重参考图 | 补 K7 素材与点击展开交互 |
|
||||
| 无离线缓存 | 增加本地缓存与离线态提示 |
|
||||
| 无"换一道"替换 | 增加同营养档位重选交互 |
|
||||
| 一周食谱静态 | 接口 `weekly_plan` 动态下发 |
|
||||
|
||||
---
|
||||
|
||||
*本需求文档与 `AI-REQUIREMENTS.md`(面向 AI 工程师的算法/API 细则)互补:本文档侧重"诉求—数据结合—移动端交互—资料清单"的产品整体视角,算法实现细节以 `AI-REQUIREMENTS.md` 为准。*
|
||||
@@ -1134,7 +1134,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 右侧:建议 & 报告入口 -->
|
||||
<div class="dash-grid__right">
|
||||
<div class="sug-card sug-card--diet" onclick="location.href='diet-advice.html'" style="cursor:pointer;">
|
||||
<div class="sug-card sug-card--diet" onclick="location.href='diet-questionnaire.html'" style="cursor:pointer;">
|
||||
<div class="sug-card__label">膳食建议</div>
|
||||
<div class="sug-card__val">1950<span class="sug-card__unit">千卡</span></div>
|
||||
<div class="sug-card__sub">今日推荐摄入</div>
|
||||
|
||||
Reference in New Issue
Block a user