feat: 暂存,修订体重管理PRD文档

This commit is contained in:
fengpu
2026-07-17 11:38:21 +08:00
parent d5b3e52ed4
commit 6836a0794b
5 changed files with 82 additions and 318 deletions
+14 -7
View File
@@ -161,8 +161,13 @@ stateDiagram-v2
- 如果有指标慢病异常时,“指标慢病”步骤入口显示黄色警示图标和不同样式
- 审核状态:待审核(黄色)、审核通过(绿色)、已驳回(红色,含驳回原因)
- 驳回后可修改重新提交
- 审核通过后签订承诺书
- 特别注意:员工如参加过活动(签署过承诺书),后续活动则自动参加(无需提交审核+签署承诺书),承诺书使用之前的数据
- 关于承诺书:首次参加体重管理活动审核通过后必须签订承诺书,一旦签署过一次承诺书后续的活动只要体重目标审核通过则认为是报名成功,无需二次签署承诺书;
- 关于报名流程:若用户参加过一次体重管理活动,后续再开展的新活动用户可选择忽略设置体重目标,若未设置减重目标该活动开始后系统会给用户自动报名,报名的初始体重值取当前最新值,目标值使用自动推荐值(范围的中位数)。系统给自动报名的用户在活动期间内可更改一次目标设定(仅限一次),更改目标需后台审核,审核通过后替换自动报名时系统设置的推荐值;
**数据展示规则:**
- 体重管理活动说明书的数据在以下两种情况下会回填显示:1.用户正在进行自我体重目标管理;2.用户正在报名或正在参加单位活动;其中后者的优先级高于前者(同时存在则取活动数据显示)。
- 特别说明:用户只要报名了活动,即便体重目标正在审核中、审核驳回、或者是审核通过后活动暂未开始这里都是显示活动相关的体重数据;
- 活动结束后恢复成个人体重管理目标数据,未设定个人目标则不显示;
---
@@ -221,8 +226,9 @@ stateDiagram-v2
- 单位活动模式:活动天数不可修改
- 个人目标模式:自我管理天数默认一个月,可更改
- 审核状态为驳回时,显示驳回原因入口
- 审核通过后可修改目标并重新提交,状态栏提示"管理员已审核通过,请尽快签订承诺书参加活动"
- 审核通过后可修改目标并重新提交,若首次参加状态栏提示"管理员已审核通过,请尽快签订承诺书参加活动",非首次参加审核通过后就是已签署承诺书状态(报名成功);
- 推荐设定的推荐规则见附属文档;
- 个人目标自动起名规则:姓名+年月+体重管理(例:张三丰2026年7月体重管理),个人目标名称不做唯一限制;
---
### 3. 基本信息(basic-info.html
@@ -252,7 +258,7 @@ stateDiagram-v2
- 点击返回按钮返回上一页
**业务规则:**
- 注:此处填写身高、体重、体脂率、腰围信息均不计入当日打卡;
---
@@ -284,8 +290,8 @@ stateDiagram-v2
**业务规则:**
- 异常指标红色高亮显示
- 重点指标从体检数据中获取,慢性疾病从五类人群数据中获取,在该页面进行手动修改数据时,会新建一张表
单独存放这些数据,后续进行查询时,从两个来源地方取最新
- 重点指标从体检数据中获取,慢性疾病从五类人群数据中获取,在该页面进行手动修改数据时不能影响体检或者是五类人群的原始数据,会新建一张表
单独存放这些自记数据,后续进行查询时,从两个来源取最新值显示
---
@@ -330,6 +336,7 @@ stateDiagram-v2
**业务规则:**
- 页面为横屏布局
- 签名不可为空
- 签名文件在保证清晰度的同时注意体积压缩,文件不宜过大;
---
### 7. 我的承诺(my-promise.html
@@ -1,308 +0,0 @@
/**
* 体重管理模块页面截图脚本
* 使用 Puppeteer 批量截取 HTML 原型页面,输出 JPEG(质量 85%)到 doc/screenshots/
*
* 用法: node doc/screenshot.js
*/
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
const MODULE_DIR = __dirname.replace(/\\doc$/, ''); // weight-management/
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
/**
* 页面截图配置
* 每项可以是 string(简单页面名)或 object
* { file: 'xxx.html', type: 'state', outName?: 'override-name' }
* - type: URL 查询参数 ?type= 的值(页面通过右上角菜单切换状态)
* - outName: 输出文件名(不含扩展名),默认取自 file + type
*/
const PAGES = [
// ===== 目标承诺(多步骤流程) =====
// 目标承诺 - 7 种状态(默认:unset-hasact
{ file: 'goal-commitment.html', type: 'unset-hasact', outName: 'goal-commitment' },
{ file: 'goal-commitment.html', type: 'unset-noact', outName: 'goal-commitment-unset-noact' },
{ file: 'goal-commitment.html', type: 'personal', outName: 'goal-commitment-personal' },
{ file: 'goal-commitment.html', type: 'wait', outName: 'goal-commitment-wait' },
{ file: 'goal-commitment.html', type: 'pass', outName: 'goal-commitment-pass' },
{ file: 'goal-commitment.html', type: 'reject', outName: 'goal-commitment-reject' },
{ file: 'goal-commitment.html', type: 'signed', outName: 'goal-commitment-signed' },
// 目标管理 - 7 种状态(默认:unset-hasact
{ file: 'goal-management.html', type: 'unset-hasact', outName: 'goal-management' },
{ file: 'goal-management.html', type: 'unset-noact', outName: 'goal-management-unset-noact' },
{ file: 'goal-management.html', type: 'personal', outName: 'goal-management-personal' },
{ file: 'goal-management.html', type: 'wait', outName: 'goal-management-wait' },
{ file: 'goal-management.html', type: 'pass', outName: 'goal-management-pass' },
{ file: 'goal-management.html', type: 'reject', outName: 'goal-management-reject' },
{ file: 'goal-management.html', type: 'signed', outName: 'goal-management-signed' },
// 其余目标承诺流程页面(单状态)
'basic-info.html',
// 指标慢病 - 2 种 Tab(默认:重点指标;慢性疾病需 eval 切换)
'indicators-diseases.html',
{ file: 'indicators-diseases.html', outName: 'indicators-diseases-diseases', eval: 'switchTab("diseases")' },
'contraindications.html',
'promise-signing.html',
'my-promise.html',
'activity-detail.html',
// ===== 体重管理主页 - 3 种模式(默认:hasact =====
{ file: 'weight-main.html', type: 'hasact', outName: 'weight-main' },
{ file: 'weight-main.html', type: 'personal', outName: 'weight-main-personal' },
{ file: 'weight-main.html', type: 'none', outName: 'weight-main-none' },
// ===== 每日打卡(单状态) =====
'eat-checkin.html',
'exercise-checkin.html',
'weight-checkin.html',
// ===== 历史记录(单状态) =====
// 详细报告 - 3 种设备(默认:体脂仪 fat)
{ file: 'report-detail.html', type: 'fat', outName: 'report-detail' },
{ file: 'report-detail.html', type: 'scale', outName: 'report-detail-scale' },
{ file: 'report-detail.html', type: 'bluetooth', outName: 'report-detail-bluetooth' },
'weight-history.html',
'eat-history.html',
'exercise-history.html',
'balance-history.html',
// ===== 建议与报告 =====
// 体重报告 - 3 种模式(默认:hasact)
{ file: 'weight-report.html', type: 'hasact', outName: 'weight-report' },
{ file: 'weight-report.html', type: 'personal', outName: 'weight-report-personal' },
{ file: 'weight-report.html', type: 'none', outName: 'weight-report-none' },
// 排行榜 - 3 种模式(默认:hasact)
{ file: 'weight-ranking.html', type: 'hasact', outName: 'weight-ranking' },
{ file: 'weight-ranking.html', type: 'personal', outName: 'weight-ranking-personal' },
{ file: 'weight-ranking.html', type: 'none', outName: 'weight-ranking-none' },
// 其余建议与报告页面(单状态)
'diet-advice.html',
'diet-questionnaire.html',
'exercise-advice.html',
// ===== 弹窗截图 =====
// 目标承诺 - 驳回原因弹窗(需要先切换到 reject 状态)
{ file: 'goal-commitment.html', type: 'reject', outName: 'goal-commitment-reason', eval: 'openReasonModal()' },
// 目标管理 - 4 个弹窗
{ file: 'goal-management.html', outName: 'goal-management-act-sheet', eval: 'openActSheet()' },
{ file: 'goal-management.html', outName: 'goal-management-target-sheet', eval: 'openTargetSheet("targetWeight")' },
{ file: 'goal-management.html', outName: 'goal-management-confirm', eval: 'openConfirm()' },
{ file: 'goal-management.html', type: 'reject', outName: 'goal-management-reason', eval: 'openRejectModal()' },
// 基本信息 - 2 个弹窗
{ file: 'basic-info.html', outName: 'basic-info-height', eval: 'openHeightSheet()' },
{ file: 'basic-info.html', outName: 'basic-info-activity', eval: 'openSheet()' },
// 指标慢病 - 2 个弹窗
{ file: 'indicators-diseases.html', outName: 'indicators-diseases-measure', eval: 'openSheet("weight")' },
{ file: 'indicators-diseases.html', outName: 'indicators-diseases-disease-edit', eval: 'openDiseaseModal("原发性高血压")' },
// 吃打卡 - 4 个弹窗
{ file: 'eat-checkin.html', outName: 'eat-checkin-portion', eval: 'selectedMeal="lunch";openGramPanel(1)' },
{ file: 'eat-checkin.html', outName: 'eat-checkin-summary', eval: 'selectedMeal="lunch";cart[1]=[150];cart[6]=[100];cart[7]=[120];openSummary()' },
{ file: 'eat-checkin.html', outName: 'eat-checkin-confirm', eval: 'selectedMeal="lunch";cart[1]=[150];cart[6]=[100];openConfirm()' },
{ file: 'eat-checkin.html', outName: 'eat-checkin-tip', eval: 'if(typeof showAutoMealTip==="function")showAutoMealTip();else document.getElementById("tipMask").classList.add("show")' },
// 动打卡 - 4 个弹窗
{ file: 'exercise-checkin.html', outName: 'exercise-checkin-duration', eval: 'selectedDay="wed";openDurationPanel(1)' },
{ file: 'exercise-checkin.html', outName: 'exercise-checkin-summary', eval: 'cart[1]=[30];cart[2]=[45];cart[9]=[20];openSummary()' },
{ file: 'exercise-checkin.html', outName: 'exercise-checkin-confirm', eval: 'cart[1]=[30];cart[2]=[45];openConfirm()' },
{ file: 'exercise-checkin.html', outName: 'exercise-checkin-tip', eval: 'if(typeof showWearableTip==="function")showWearableTip();else document.getElementById("tipMask").classList.add("show")' },
// 体重打卡 - 4 个弹窗
{ file: 'weight-checkin.html', outName: 'weight-checkin-value', eval: 'selectedDay="wed";openValuePanel("weight")' },
{ file: 'weight-checkin.html', outName: 'weight-checkin-summary', eval: 'cart["weight"]=65.5;cart["fat"]=22.3;cart["waist"]=78;openSummary()' },
{ file: 'weight-checkin.html', outName: 'weight-checkin-confirm', eval: 'cart["weight"]=65.5;cart["fat"]=22.3;openConfirm()' },
{ file: 'weight-checkin.html', outName: 'weight-checkin-tip', eval: 'if(typeof showHeightScaleTip==="function")showHeightScaleTip();else document.getElementById("tipMask").classList.add("show")' },
// 膳食建议 - 3 个弹窗
{ file: 'diet-advice.html', outName: 'diet-advice-detail', eval: 'if(window._currentMealItems&&window._currentMealItems.length>0)openMealDetail(0)' },
{ file: 'diet-advice.html', outName: 'diet-advice-dislike', eval: 'if(window._currentMealItems&&window._currentMealItems.length>0){openMealDetail(0);setTimeout(function(){var b=document.querySelector("[class*=dislike]");if(b)b.click()},400)}' },
{ file: 'diet-advice.html', outName: 'diet-advice-region', eval: 'var g=document.getElementById("guideOverlay");if(g)g.classList.add("hidden");if(typeof openRegionSheet==="function")openRegionSheet()' },
// 膳食偏好 - 2 个弹窗
{ file: 'diet-questionnaire.html', outName: 'diet-questionnaire-region', eval: 'if(typeof openRegionSheet==="function")openRegionSheet()' },
{ file: 'diet-questionnaire.html', outName: 'diet-questionnaire-avoid', eval: 'if(typeof openCustomAvoidInput==="function")openCustomAvoidInput()' },
// 签订承诺 - 1 个弹窗
{ file: 'promise-signing.html', outName: 'promise-signing-giveup', eval: 'if(typeof openGiveup==="function")openGiveup();else document.getElementById("giveupMask").classList.add("show")' },
];
// 特殊页面配置(按输出文件名匹配)
const PAGE_CONFIGS = {
'promise-signing': { viewportWidth: 844, viewportHeight: 390 },
// 膳食建议页需要注入 localStorage 跳过引导遮罩(guide 截图除外)
'diet-advice': { injectPrefs: true },
'diet-advice-detail': { injectPrefs: true },
'diet-advice-dislike': { injectPrefs: true },
'diet-advice-region': { injectPrefs: true },
};
/**
* 解析页面配置项,返回 { file, url, outName }
*/
function resolvePageConfig(item) {
if (typeof item === 'string') {
return { file: item, url: item, outName: item.replace('.html', '') };
}
const file = item.file;
const type = item.type;
const outName = item.outName || file.replace('.html', '') + (type ? '-' + type : '');
const url = type ? file + '?type=' + encodeURIComponent(type) : file;
const evalAfterLoad = item.eval || null;
return { file, url, outName, evalAfterLoad };
}
async function screenshotPage(browser, pageConfig) {
const page = await browser.newPage();
const { file, url, outName, evalAfterLoad } = pageConfig;
const filePath = path.join(MODULE_DIR, file);
const fileUrl = `file:///${filePath.replace(/\\/g, '/')}`;
// 为带参数的 URL 追加查询字符串
const fullUrl = url.includes('?') ? fileUrl + '?' + url.split('?')[1] : fileUrl;
const config = PAGE_CONFIGS[outName] || PAGE_CONFIGS[file.replace('.html', '')] || {};
const viewportWidth = config.viewportWidth || 390;
const viewportHeight = config.viewportHeight || 844;
await page.setViewport({ width: viewportWidth, height: viewportHeight, deviceScaleFactor: 1 });
try {
// 特殊处理:在页面加载前注入 localStorage
if (config.injectPrefs) {
await page.evaluateOnNewDocument(() => {
localStorage.setItem('dietPreferences', JSON.stringify({
region: '陕西省-西安市',
taste: 'spicy',
workSchedule: 'normal',
avoidFoods: ['羊肉', '海鲜']
}));
});
}
await page.goto(fullUrl, { waitUntil: 'networkidle0', timeout: 15000 });
// 等待页面渲染完成
await new Promise(r => setTimeout(r, 500));
// 注入 CSS:移除 phone-shell 限制,展开全部内容
await page.evaluate(() => {
const style = document.createElement('style');
style.id = 'screenshot-unwrap';
style.textContent = `
/* 移除手机壳固定高度和装饰 */
.phone-shell {
height: auto !important;
min-height: auto !important;
max-height: none !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: visible !important;
}
/* 滚动区域全部展开 */
.scroll-content,
[class*="scroll"] {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
/* 页面外层背景改为白色 */
body {
background: #fff !important;
padding: 0 !important;
margin: 0 !important;
}
/* 隐藏 type-menu、type-mask 等弹窗遮挡元素 */
.type-menu, .type-mask, .nav-menu,
.reason-mask, .value-mask, .summary-mask,
.act-mask, .act-sheet, .target-mask, .target-sheet,
.confirm-mask, .reject-mask, .confirm-card, .reject-card,
.mask, .sheet, .height-mask, .height-sheet,
.overlay, .picker-mask, .picker-panel {
display: none !important;
}
/* 底部安全区缩到最小 */
.bottom-safe {
height: 8px !important;
}
`;
document.head.appendChild(style);
});
// 等待样式生效
await new Promise(r => setTimeout(r, 300));
// 执行页面特定的 JS(如切换 Tab)
if (pageConfig.evalAfterLoad) {
await page.evaluate((code) => { eval(code); }, pageConfig.evalAfterLoad);
await new Promise(r => setTimeout(r, 200));
}
// 获取实际内容高度
const bodyHandle = await page.$('.phone-shell');
const contentHeight = await page.evaluate((el) => {
if (el) return el.scrollHeight;
return document.body.scrollHeight;
}, bodyHandle);
const finalHeight = Math.max(contentHeight || viewportHeight, 400);
await page.setViewport({ width: viewportWidth, height: Math.min(finalHeight + 40, 12000), deviceScaleFactor: 1 });
const outFile = outName + '.jpg';
const outPath = path.join(SCREENSHOTS_DIR, outFile);
await page.screenshot({
path: outPath,
type: 'jpeg',
quality: 85,
fullPage: true,
});
console.log(`${url}${outFile} (${viewportWidth}×${finalHeight})`);
} catch (err) {
console.error(`${url}: ${err.message}`);
} finally {
await page.close();
}
}
(async () => {
console.log('体重管理模块 - 页面截图');
console.log(`模块目录: ${MODULE_DIR}`);
console.log(`输出目录: ${SCREENSHOTS_DIR}`);
console.log(`${PAGES.length} 个页面状态\n`);
// 确保输出目录存在
if (!fs.existsSync(SCREENSHOTS_DIR)) {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
const browser = await puppeteer.launch({
headless: true,
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
for (let i = 0; i < PAGES.length; i++) {
const item = PAGES[i];
const cfg = resolvePageConfig(item);
const label = typeof item === 'string' ? item : `${item.file}?type=${item.type}`;
console.log(`[${i + 1}/${PAGES.length}] ${label}`);
await screenshotPage(browser, cfg);
}
console.log(`\n✅ 全部完成!共 ${PAGES.length} 张截图,保存在 ${SCREENSHOTS_DIR}`);
} catch (err) {
console.error('执行出错:', err);
process.exit(1);
} finally {
await browser.close();
}
})();