feat: 新增膳食问卷页面,更新膳食建议和体重管理页面

This commit is contained in:
liuyunqin
2026-07-03 16:39:32 +08:00
parent 5592230aa3
commit 1c1469402a
4 changed files with 1543 additions and 355 deletions
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 数据校验规则(接口前置)
| 字段 | 校验 |
|---|---|
| 血尿酸 | 1501000 μmol/L,超限返回"数据异常" |
| 口味偏好 | 仅枚举;"微辣"等非标值自动映射为"辛辣" |
| 身高 | 100250 cm |
| 体重 | 30300 kg |
| 年龄 | 1870 岁 |
| 血压 | 收缩压 60250 / 舒张压 30150 mmHg |
### 4.3 第一层:基础营养计算(确定性公式)
**BMRMifflin-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 140159 或 DBP 9099 | ≤2000mg | ≈5g |
| 2 级 | SBP≥160 或 DBP≥100 | ≤1500mg | ≈3.75g |
| 合并肾病 | 任意级+肾病 | ≤1200mg | ≈3g |
约束:**每餐钠不超过日上限 40%**,防止单次超标。
#### 4.4.2 高尿酸 → 嘌呤控制
| 分期 | 判定 | 嘌呤上限/日 | 处理 |
|---|---|---|---|
| 无症状期 | 血尿酸 420600 μ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. **并行页面**S1S4 + 复用现有 `diet-advice.html` 原型 → 第 15 工作日;
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>