feat: 更新蓝牙体重秤小程序页面,新增PRD文档,移除启动脚本

This commit is contained in:
liuyunqin
2026-07-20 17:14:02 +08:00
parent 5c518cb457
commit 0603113596
32 changed files with 896 additions and 307 deletions
+1 -9
View File
@@ -75,14 +75,6 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica
<img id="lightbox-img" src="" alt="放大预览">
</div>
<script>
/* ===== file:// 协议 → 自动跳转到本地服务器 ===== */
(function() {
if (location.protocol === 'file:') {
var search = location.search; /* 保留 ?file=...&title=... 参数 */
location.href = 'http://localhost:8080/_templates/md-viewer.html' + search;
}
})();
const params = new URLSearchParams(location.search);
const filePath = params.get('file');
const title = params.get('title');
@@ -242,7 +234,7 @@ if (filePath) {
}
})
.catch(err => {
document.getElementById('content').innerHTML = '<div class="loading error">❌ 加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">📁 文件路径:' + filePath + '<br>🔗 请求地址:' + fetchUrl + '<br>💡 请确认服务器已启动(双击 启动服务器.bat),且文件路径正确</small></div>';
document.getElementById('content').innerHTML = '<div class="loading error">❌ 加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">📁 文件路径:' + filePath + '<br>🔗 请求地址:' + fetchUrl + '</small></div>';
});
} else {
document.getElementById('content').innerHTML = '<div class="loading">未指定文件路径</div>';
+94 -192
View File
@@ -150,41 +150,9 @@ body { background: var(--background); font-family: -apple-system, BlinkMacSystem
.prd-item__status--ready { background: var(--green-light); color: #389e0d; border: 1px solid var(--green-border); }
.prd-item__status--empty { background: #fffbe6; color: #d48806; border: 1px solid #ffe58f; }
/* ===== file:// 协议 - 全屏启动遮罩 ===== */
.launch-overlay { display: none; position: fixed; inset: 0; z-index: 99999; background: rgba(15,23,42,0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: center; font-family: -apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue','Segoe UI',sans-serif; }
.launch-overlay--show { display: flex; }
.launch-card { background: #fff; border-radius: 20px; padding: 40px 48px; text-align: center; box-shadow: 0 24px 80px rgba(0,0,0,0.4); max-width: 480px; width: 90%; animation: popIn .35s ease; }
.launch-card__icon { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg, #1677FF, #4facfe); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; font-size: 32px; color: #fff; }
.launch-card__icon--ok { background: linear-gradient(135deg, #52c41a, #95de64); }
.launch-card__title { font-size: 20px; font-weight: 700; color: #1a1a2e; margin-bottom: 8px; }
.launch-card__sub { font-size: 14px; color: #5a607f; margin-bottom: 24px; line-height: 1.6; }
.launch-card__btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 32px; font-size: 15px; font-weight: 600; color: #fff; background: linear-gradient(135deg, #1677FF, #4facfe); border: none; border-radius: 12px; cursor: pointer; text-decoration: none; transition: all .2s; box-shadow: 0 4px 16px rgba(22,119,255,0.35); }
.launch-card__btn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(22,119,255,0.45); }
.launch-card__btn--outline { background: #fff; color: var(--primary); border: 1.5px solid var(--primary); box-shadow: none; }
.launch-card__btn--outline:hover { background: var(--primary-light); box-shadow: 0 4px 12px rgba(22,119,255,0.15); }
.launch-card__status { margin-top: 16px; font-size: 13px; color: var(--text-placeholder); }
.launch-card__spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #e8ecf1; border-top-color: var(--primary); border-radius: 50%; animation: spin .7s linear infinite; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }
.launch-card__actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
</style>
</head>
<body>
<!-- 本地服务器启动遮罩 -->
<div class="launch-overlay" id="launch-overlay">
<div class="launch-card">
<div class="launch-card__icon" id="launch-icon">🚀</div>
<div class="launch-card__title" id="launch-title">需要启动本地服务器</div>
<div class="launch-card__sub" id="launch-sub">PRD文档预览、Markdown查看器等功能需要本地HTTP服务器。<br>请双击项目根目录下的 <code style="background:#f0f2f8;padding:2px 8px;border-radius:4px;">启动服务器.bat</code></div>
<div class="launch-card__actions" id="launch-actions">
<a class="launch-card__btn" href="启动服务器.bat" download>⚡ 下载启动脚本</a>
<a class="launch-card__btn launch-card__btn--outline" href="http://localhost:8080/index.html">📖 直接打开原型</a>
</div>
<div style="margin-top:12px;text-align:center;">
<a href="javascript:void(0)" id="launch-dismiss-btn" style="font-size:13px;color:var(--text-placeholder);text-decoration:none;border-bottom:1px dashed var(--text-placeholder);padding-bottom:1px;">跳过,直接浏览导航页</a>
</div>
<div class="launch-card__status" id="launch-status"><span class="launch-card__spinner"></span>等待服务器就绪(检测中...</div>
</div>
</div>
</style>
</head>
<body>
<div class="header">
<div class="header__inner">
<div class="header__title">健康CQ升级项目 · 高保真交互原型</div>
@@ -195,114 +163,8 @@ body { background: var(--background); font-family: -apple-system, BlinkMacSystem
<div class="main-content">
<div id="tab-contents"></div>
</div>
<script>
/* ===== file:// 协议检测 & 服务器自动启动 ===== */
(function() {
if (location.protocol !== 'file:') return; /* 非 file:// 协议,无需处理 */
var overlay = document.getElementById('launch-overlay');
var launchIcon = document.getElementById('launch-icon');
var launchTitle = document.getElementById('launch-title');
var launchSub = document.getElementById('launch-sub');
var launchStatus = document.getElementById('launch-status');
var launchActions = document.getElementById('launch-actions');
var dismissed = false; /* 用户是否已点击"跳过" */
/* 服务器检测:只用 fetch(比 iframe 更可靠,不会因跨域判断误报) */
function checkServer(callback) {
try {
var controller = new AbortController();
var timeout = setTimeout(function() {
controller.abort();
callback(false);
}, 3000);
fetch('http://localhost:8080/index.html?' + Date.now(), {
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal
}).then(function() {
/* fetch 成功 → 服务器可达(响应了 HTTP,即使是 404 也算可达) */
clearTimeout(timeout);
callback(true);
}).catch(function(err) {
/* fetch 失败 → 服务器不可达(连接拒绝/DNS 失败等) */
clearTimeout(timeout);
callback(false);
});
} catch (e) {
/* AbortController / fetch 不可用(极旧浏览器)→ 保守:认为服务器不在线 */
callback(false);
}
}
/* 服务器已运行 → 无感跳转 */
function redirectToServer() {
location.replace('http://localhost:8080/index.html');
}
/* 显示遮罩 + 轮询 */
function showOverlay() {
overlay.classList.add('launch-overlay--show');
/* 轮询:每2秒检测一次,最多60次(120秒) */
var attempts = 0;
var maxAttempts = 60;
var pollTimer = setInterval(function() {
if (dismissed) { clearInterval(pollTimer); return; }
attempts++;
checkServer(function(ok) {
if (dismissed) return;
if (ok) {
clearInterval(pollTimer);
launchIcon.textContent = '✅';
launchIcon.className = 'launch-card__icon launch-card__icon--ok';
launchTitle.textContent = '服务器已就绪';
launchSub.textContent = '即将自动跳转...';
launchStatus.innerHTML = '<a href="http://localhost:8080/index.html" style="color:var(--primary);font-weight:600;">立即跳转 →</a>';
launchActions.innerHTML = '<a class="launch-card__btn launch-card__btn--outline" href="http://localhost:8080/index.html">📖 打开原型导航</a>';
setTimeout(redirectToServer, 600);
}
if (attempts >= maxAttempts) {
clearInterval(pollTimer);
launchTitle.textContent = '未检测到服务器';
launchSub.textContent = '请确认已双击运行 启动服务器.bat,然后刷新页面或点击下方按钮';
launchStatus.innerHTML = '⏱ 等待超时(120秒)';
launchActions.innerHTML = '<button class="launch-card__btn" onclick="location.reload()">🔄 刷新重试</button> <a class="launch-card__btn launch-card__btn--outline" href="http://localhost:8080/index.html">📖 直接打开原型</a>';
}
});
}, 2000);
}
/* 绑定"跳过"按钮 */
function bindDismissBtn() {
var btn = document.getElementById('launch-dismiss-btn');
if (btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
dismissed = true;
overlay.classList.remove('launch-overlay--show');
});
}
}
/* 初始绑定 */
bindDismissBtn();
/* === 主流程:先静默检测 === */
checkServer(function(running) {
if (dismissed) return;
if (running) {
/* 服务器已在运行 → 直接跳转,用户全程无感 */
redirectToServer();
} else {
/* 服务器未运行 → 显示遮罩引导手动启动 */
showOverlay();
}
});
})();
/* ===== SVG 图标 ===== */
<script>
/* ===== SVG 图标 ===== */
const ICONS = {
web: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><rect x="1" y="2" width="16" height="12" rx="2" stroke="currentColor" stroke-width="1.5"/><line x1="5" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="9" y1="14" x2="9" y2="16" stroke="currentColor" stroke-width="1.5"/></svg>',
app: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><rect x="4" y="1" width="10" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/><line x1="7.5" y1="14" x2="10.5" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
@@ -1026,13 +888,19 @@ const SITE_MAP = [
{label:'净菜包装贴标秤 · 包装历史', file:'clean-veg-packaging-labeler/03-history.html'},
{label:'净菜包装贴标秤 · 标签预览', file:'clean-veg-packaging-labeler/label-preview.html'}
] },
{ name:'蓝牙称小程序', dir:'other-module/bluetooth-scale-miniprogram', entry:'index.html', done:true, pages:[
{ name:'蓝牙称小程序', dir:'other-module/bluetooth-scale-miniprogram', entry:'index.html', prd:'other-module/bluetooth-scale-miniprogram/doc/PRD.md', done:true, pages:[
{label:'登录页', file:'index.html'},
{label:'完善个人信息', file:'profile-complete.html'},
{label:'主页(四Tab', file:'main.html'},
{label:'搜索设备', file:'search-device.html'},
{label:'WiFi连接', file:'wifi-connect.html'},
{label:'测量数据详情', file:'data-detail.html'},
{label:'设备成员管理', file:'device-members.html'}
{label:'成员管理', file:'device-members.html'},
{label:'添加成员', file:'add-member.html'},
{label:'数据Tab', file:'main.html#tab-data'},
{label:'详细报告', file:'data-detail.html'},
{label:'个人信息', file:'profile.html'},
{label:'帮助中心', file:'help-center.html'},
{label:'帮助详情', file:'help-detail.html'}
] }
]}
];
@@ -1070,8 +938,10 @@ SITE_MAP.forEach((t, i) => {
const prdTabIdx = SITE_MAP.length;
tabNav.innerHTML += `<div class="tab-nav__item" data-idx="${prdTabIdx}">${ICONS.prd}产品需求文档</div>`;
/* 生成卡片 — 点击卡片跳转主页,点击徽章弹出页面列表 */
/* 生成卡片 — 点击卡片跳转主页,点击徽章弹出页面列表popover 延迟渲染) */
let openPopover = null;
const pageDataMap = {};
function cardHTML(mod, uid) {
const has = mod.pages.length > 0;
const isPending = !!mod.pending;
@@ -1098,12 +968,9 @@ function cardHTML(mod, uid) {
}
let pageCountHtml = '';
if (has && !isPending) {
let popItems = mod.pages.map(p => {
const dcls = p.dialog ? ' page-popover__item--dialog' : '';
const icon = p.dialog ? ICONS.dialog : ICONS.page;
return `<a class="page-popover__item${dcls}" href="${mod.dir}/${p.file}" target="_blank">${icon}${p.label}</a>`;
}).join('');
pageCountHtml = `<span class="module-card__page-count" data-pop="${uid}" onclick="togglePop('${uid}',event)">${mod.pages.length} 页</span><div class="page-popover" id="pop-${uid}" onclick="event.stopPropagation()"><div class="page-popover__title">页面列表</div>${popItems}</div>`;
/* 存储页面数据供延迟渲染 popover(不生成隐藏 DOM,大幅减少初始化开销) */
pageDataMap[uid] = { dir: mod.dir, pages: mod.pages };
pageCountHtml = `<span class="module-card__page-count" data-pop="${uid}" onclick="togglePop('${uid}',event)">${mod.pages.length} 页</span>`;
}
/* 底部行占位:无内容时保持高度一致 */
@@ -1111,44 +978,66 @@ function cardHTML(mod, uid) {
return `<div class="${cls}" ${onclick}>${watermarkHtml}<div class="module-card__header"><span class="module-card__name">${mod.name}</span>${statusHtml}</div><div class="module-card__bottom">${prdLink}${pageCountHtml}${bottomPlaceholder}</div></div>`;
}
function togglePop(uid, e) {
if (e) { e.stopPropagation(); e.preventDefault(); }
const el = document.getElementById('pop-' + uid);
if (!el) return;
if (openPopover && openPopover !== el) {
openPopover.classList.remove('page-popover--visible');
var prevCard = openPopover.closest('.module-card');
if (prevCard) prevCard.classList.remove('module-card--pop-open');
}
el.classList.toggle('page-popover--visible');
const card = el.closest('.module-card');
if (el.classList.contains('page-popover--visible')) {
if (card) card.classList.add('module-card--pop-open');
openPopover = el;
} else {
if (card) card.classList.remove('module-card--pop-open');
openPopover = null;
}
}
/* 点击页面任意空白处关闭 popover(不依赖 stopPropagation */
document.addEventListener('click', function(e) {
if (!openPopover) return;
/* 点击在 popover 内部 或 页数徽章上 → 不关闭 */
if (e.target.closest('.page-popover')) return;
if (e.target.closest('.module-card__page-count')) return;
/* 其他情况 → 关闭 */
var card = openPopover.closest('.module-card');
if (card) card.classList.remove('module-card--pop-open');
openPopover.classList.remove('page-popover--visible');
openPopover = null;
});
function togglePop(uid, e) {
if (e) { e.stopPropagation(); e.preventDefault(); }
let el = document.getElementById('pop-' + uid);
/* 生成 Tab 内容 */
/* 延迟构建 popover DOM(首次点击时才创建) */
if (!el) {
const data = pageDataMap[uid];
if (!data) return;
const popItems = data.pages.map(p => {
const dcls = p.dialog ? ' page-popover__item--dialog' : '';
const icon = p.dialog ? ICONS.dialog : ICONS.page;
return `<a class="page-popover__item${dcls}" href="${data.dir}/${p.file}" target="_blank">${icon}${p.label}</a>`;
}).join('');
const countEl = document.querySelector('[data-pop="' + uid + '"]');
if (countEl) {
countEl.insertAdjacentHTML('afterend', '<div class="page-popover" id="pop-' + uid + '"><div class="page-popover__title">页面列表</div>' + popItems + '</div>');
el = document.getElementById('pop-' + uid);
}
}
if (!el) return;
if (openPopover && openPopover !== el) {
openPopover.classList.remove('page-popover--visible');
var prevCard = openPopover.closest('.module-card');
if (prevCard) prevCard.classList.remove('module-card--pop-open');
}
el.classList.toggle('page-popover--visible');
const card = el.closest('.module-card');
if (el.classList.contains('page-popover--visible')) {
if (card) card.classList.add('module-card--pop-open');
openPopover = el;
} else {
if (card) card.classList.remove('module-card--pop-open');
openPopover = null;
}
}
/* 点击页面任意空白处关闭 popover(不依赖 stopPropagation */
document.addEventListener('click', function(e) {
if (!openPopover) return;
/* 点击在 popover 内部 或 页数徽章上 → 不关闭 */
if (e.target.closest('.page-popover')) return;
if (e.target.closest('.module-card__page-count')) return;
/* 其他情况 → 关闭 */
var card = openPopover.closest('.module-card');
if (card) card.classList.remove('module-card--pop-open');
openPopover.classList.remove('page-popover--visible');
openPopover = null;
});
/* 生成 Tab 内容(延迟渲染:首次只构建当前 tab,其余按需创建) */
const renderedTabs = {};
let uidCounter = 0;
SITE_MAP.forEach((t, i) => {
function ensureTabRendered(idx) {
if (renderedTabs[idx]) return renderedTabs[idx];
if (idx >= SITE_MAP.length) return null;
const t = SITE_MAP[idx];
const div = document.createElement('div');
div.className = 'tab-content' + (i === 0 ? ' tab-content--active' : '');
div.dataset.idx = i;
div.className = 'tab-content';
div.dataset.idx = idx;
let html = '';
if (t.modules) {
html = '<div class="module-grid">' + t.modules.map(m => cardHTML(m, 'u' + uidCounter++)).join('') + '</div>';
@@ -1167,17 +1056,30 @@ SITE_MAP.forEach((t, i) => {
}
div.innerHTML = html;
tabContents.appendChild(div);
});
renderedTabs[idx] = div;
return div;
}
/* Tab 切换 */
/* 首次加载只渲染第一个 tab */
const firstTab = ensureTabRendered(0);
if (firstTab) firstTab.classList.add('tab-content--active');
/* Tab 切换(首次切换到某 tab 时自动渲染其内容) */
tabNav.addEventListener('click', e => {
const item = e.target.closest('.tab-nav__item');
if (!item) return;
const idx = item.dataset.idx;
const idx = parseInt(item.dataset.idx, 10);
/* 延迟渲染:首次访问该 tab 时才创建内容 DOM */
if (idx < SITE_MAP.length) {
ensureTabRendered(idx);
}
tabNav.querySelectorAll('.tab-nav__item').forEach(el => el.classList.remove('tab-nav__item--active'));
tabContents.querySelectorAll('.tab-content').forEach(el => el.classList.remove('tab-content--active'));
item.classList.add('tab-nav__item--active');
tabContents.querySelector(`[data-idx="${idx}"]`).classList.add('tab-content--active');
const target = tabContents.querySelector('[data-idx="' + idx + '"]');
if (target) target.classList.add('tab-content--active');
});
/* ===== PRD Tab 内容生成 ===== */
@@ -96,7 +96,9 @@
display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 4px; margin-left: 4px;
}
.data-cell__status--normal { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.data-cell__status--warn { background: #fff7e6; color: #fa8c16; border: 1px solid #ffd591; }
.data-cell__status--high { background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }
.data-cell__status--low { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
/* 行布局 */
.data-row {
@@ -171,7 +173,7 @@
<div class="data-cell"><div class="data-cell__label">BMI</div><div class="data-cell__value">23.7<span class="data-cell__status data-cell__status--normal">正常</span></div></div>
<div class="data-cell"><div class="data-cell__label">基础代谢</div><div class="data-cell__value">1,680<span class="data-cell__unit">kcal</span></div></div>
<div class="data-cell"><div class="data-cell__label">体脂肪量</div><div class="data-cell__value">15.2<span class="data-cell__unit">kg</span></div></div>
<div class="data-cell"><div class="data-cell__label">体脂肪率</div><div class="data-cell__value">21.0<span class="data-cell__unit">%</span><span class="data-cell__status data-cell__status--normal">正常</span></div></div>
<div class="data-cell"><div class="data-cell__label">体脂肪率</div><div class="data-cell__value">21.0<span class="data-cell__unit">%</span><span class="data-cell__status data-cell__status--normal" id="detailFatLevelTag">正常</span></div></div>
</div>
<div class="bmi-bar">
<div class="bmi-bar__track"><div class="bmi-bar__pointer" style="left:43.5%"></div></div>
@@ -222,5 +224,36 @@
<div class="home-indicator"></div>
</div>
<script>
/* ===== 体脂率等级判断 ===== */
function getBodyFatLevel(sex, age, bf) {
// 日本肥胖学会分龄分性别标准
var thresholds;
if (sex === 'male' || sex === '男') {
if (age < 40) thresholds = { thin: 11, fit: 20, heavy: 25 };
else if (age < 60) thresholds = { thin: 12, fit: 21, heavy: 26 };
else thresholds = { thin: 14, fit: 23, heavy: 28 };
} else {
if (age < 40) thresholds = { thin: 21, fit: 32, heavy: 37 };
else if (age < 60) thresholds = { thin: 23, fit: 34, heavy: 39 };
else thresholds = { thin: 25, fit: 36, heavy: 41 };
}
if (bf < thresholds.thin) return { label: '偏瘦', cssClass: 'data-cell__status--low' };
if (bf <= thresholds.fit) return { label: '正常', cssClass: 'data-cell__status--normal' };
if (bf <= thresholds.heavy) return { label: '偏胖', cssClass: 'data-cell__status--warn' };
return { label: '肥胖', cssClass: 'data-cell__status--high' };
}
// 初始化体脂肪率等级标签(张建国:男,45岁,体脂率21.0%)
(function() {
var tag = document.getElementById('detailFatLevelTag');
if (tag) {
var level = getBodyFatLevel('male', 45, 21.0);
tag.textContent = level.label;
tag.className = 'data-cell__status ' + level.cssClass;
}
})();
</script>
</body>
</html>
@@ -111,6 +111,7 @@
.member-sync { font-size: 11px; font-weight: 600; margin-left: auto; flex-shrink: 0; }
.member-sync--done { color: #389e0d; }
.member-sync--pending { color: #ff4d4f; }
.member-sync--deleted { color: #ff4d4f; }
.member-detail { font-size: 12px; color: var(--text-placeholder); margin-top: 4px; }
.member-actions { display: flex; gap: 12px; margin-top: 8px; }
.action-btn {
@@ -121,7 +122,10 @@
.action-btn--edit { background: var(--primary-light); color: var(--primary); }
.action-btn--delete { background: #fff2f0; color: #ff4d4f; }
/* 同步提示 */
/* 已删除状态 */
.member-card--deleted { opacity: 0.45; pointer-events: none; }
/* 删除确认弹窗 */
.sync-hint {
margin: 0 0 12px; padding: 10px 14px; border-radius: 10px;
font-size: 12px; background: #fff7e6; color: #d46b08;
@@ -197,6 +201,32 @@
}
.sync-progress__text { font-size: 11px; color: #bbb; }
/* 删除确认弹窗 */
.delete-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000;
display: none; align-items: center; justify-content: center;
}
.delete-overlay--show { display: flex; }
.delete-dialog {
width: 280px; background: #fff; border-radius: 16px; padding: 28px 24px 20px;
text-align: center; box-shadow: 0 12px 40px rgba(0,0,0,0.25);
animation: deleteDialogIn 0.25s ease;
}
@keyframes deleteDialogIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.delete-dialog__icon { font-size: 40px; margin-bottom: 12px; }
.delete-dialog__text { font-size: 15px; color: #333; margin-bottom: 20px; line-height: 1.5; }
.delete-dialog__actions { display: flex; gap: 12px; }
.delete-dialog__btn {
flex: 1; height: 40px; border: none; border-radius: 10px;
font-size: 15px; font-weight: 600; cursor: pointer; transition: opacity 0.15s;
}
.delete-dialog__btn:active { opacity: 0.8; }
.delete-dialog__btn--cancel { background: #f5f5f5; color: #666; }
.delete-dialog__btn--confirm { background: #ff4d4f; color: #fff; }
.home-indicator {
height: 20px; display: flex; align-items: center; justify-content: center;
background: #fff; flex-shrink: 0;
@@ -250,8 +280,7 @@
</div>
<div class="member-detail">男 · 45岁 · 175cm</div>
<div class="member-actions">
<button class="action-btn action-btn--edit" onclick="showToast('编辑功能开发中')">编辑</button>
<button class="action-btn action-btn--delete" onclick="showToast('删除功能开发中')">删除</button>
<button class="action-btn action-btn--delete" onclick="confirmDelete(this)">删除</button>
</div>
</div>
</div>
@@ -260,13 +289,12 @@
<div class="member-info">
<div class="member-top">
<span class="member-name">李秀英</span>
<span class="member-tag--type">家庭成</span>
<span class="member-tag--type"></span>
<span class="member-sync member-sync--done">已同步</span>
</div>
<div class="member-detail">女 · 43岁 · 162cm <span style="font-size:11px;color:#bbb;">(他人添加)</span></div>
<div class="member-actions">
<button class="action-btn action-btn--edit" onclick="showToast('编辑功能开发中')">编辑</button>
<button class="action-btn action-btn--delete" onclick="showToast('删除功能开发中')">删除</button>
<button class="action-btn action-btn--delete" onclick="confirmDelete(this)">删除</button>
</div>
</div>
</div>
@@ -281,7 +309,7 @@
<div class="member-detail">男 · 12岁 · 152cm</div>
<div class="member-actions">
<button class="action-btn action-btn--edit" onclick="showToast('编辑功能开发中')">编辑</button>
<button class="action-btn action-btn--delete" onclick="showToast('删除功能开发中')">删除</button>
<button class="action-btn action-btn--delete" onclick="confirmDelete(this)">删除</button>
</div>
</div>
</div>
@@ -296,7 +324,7 @@
<div class="member-detail">女 · 68岁 · 158cm</div>
<div class="member-actions">
<button class="action-btn action-btn--edit" onclick="showToast('编辑功能开发中')">编辑</button>
<button class="action-btn action-btn--delete" onclick="showToast('删除功能开发中')">删除</button>
<button class="action-btn action-btn--delete" onclick="confirmDelete(this)">删除</button>
</div>
</div>
</div>
@@ -468,6 +496,41 @@
showToast('已添加本人');
}
/* 删除成员确认 */
var _deleteTarget = null;
function confirmDelete(btn) {
_deleteTarget = btn;
var card = btn.closest('.member-card');
var nameEl = card.querySelector('.member-name');
var name = nameEl ? nameEl.textContent : '该成员';
document.getElementById('deleteDialogText').textContent = '是否确认删除' + name + '';
document.getElementById('deleteOverlay').classList.add('delete-overlay--show');
// 绑定确定按钮
document.getElementById('deleteConfirmBtn').onclick = executeDelete;
}
function closeDeleteDialog() {
document.getElementById('deleteOverlay').classList.remove('delete-overlay--show');
_deleteTarget = null;
}
function executeDelete() {
var target = _deleteTarget;
closeDeleteDialog();
if (!target) return;
var card = target.closest('.member-card');
if (card) {
card.classList.add('member-card--deleted');
var syncEl = card.querySelector('.member-sync');
if (syncEl) {
syncEl.textContent = '已删除 · 未同步';
syncEl.className = 'member-sync member-sync--deleted';
}
}
showToast('成员已删除');
}
/* 同步成员至设备 */
function syncMembers() {
var overlay = document.getElementById('syncOverlay');
@@ -514,5 +577,17 @@
</div>
</div>
<!-- 删除确认弹窗 -->
<div class="delete-overlay" id="deleteOverlay">
<div class="delete-dialog">
<div class="delete-dialog__icon">⚠️</div>
<div class="delete-dialog__text" id="deleteDialogText">是否确认删除该成员?</div>
<div class="delete-dialog__actions">
<button class="delete-dialog__btn delete-dialog__btn--cancel" onclick="closeDeleteDialog()">取消</button>
<button class="delete-dialog__btn delete-dialog__btn--confirm" id="deleteConfirmBtn">确定</button>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,569 @@
# 蓝牙称小程序 产品需求文档
## 模块概述
蓝牙称小程序为员工提供智能蓝牙体重秤的配套管理能力,绑定与管理蓝牙体脂秤设备、WiFi配网、多成员管理、体脂测量数据查看与趋势分析、个性化信息维护等功能。员工可通过小程序完成从设备绑定到日常称重的完整闭环,同时支持家庭成员共用一台设备。
### 页面导航结构
```
蓝牙称小程序
├── 登录页(微信一键登录)
├── 完善个人信息(新用户首次)
└── 主页(底部 4 个 Tab
├── 首页 Tab:设备概览 + 最近测量数据展示
│ └── 详细报告
├── 设备 Tab:设备列表与管理
│ ├── 搜索设备 → WiFi连接
│ ├── 成员管理 → 添加成员
│ ├── 数据补传弹窗
│ ├── 连接设备弹窗
│ ├── 取消配对确认弹窗
│ └── 运维模式(连续点击标题5次进入)
├── 数据 Tab:体脂数据记录与统计
│ ├── 切换用户弹窗
│ └── 详细报告
└── 我的 Tab:个人信息与帮助
├── 个人信息查看 → 完善个人信息(编辑模式)
├── 帮助中心 → 帮助详情
└── 退出登录
```
---
## 页面清单
### 登录与信息完善
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 1 | 登录页 | index.html | 页面 |
| 2 | 完善个人信息 | profile-complete.html | 页面 |
### 首页
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 3 | 主页(首页Tab | main.html#tab-home | 页面(Tab内页面) |
### 设备管理
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 4 | 主页(设备Tab | main.html#tab-device | 页面(Tab内页面) |
| - | 主页(设备Tab-未绑定空状态) | main.html#tab-device | 页面(状态变体) |
| 5 | 搜索设备 | search-device.html | 页面 |
| 6 | WiFi连接 | wifi-connect.html | 页面 |
| 7 | 成员管理 | device-members.html | 页面 |
| - | 成员管理(B2空状态-0成员) | device-members.html?device=b2 | 页面(状态变体) |
| - | 同步成员至设备弹窗 | device-members.html(内嵌弹窗) | 弹窗 |
| - | 同步成员至设备弹窗(已完成态) | device-members.html(内嵌弹窗) | 弹窗(状态变体) |
| - | 删除成员确认弹窗 | device-members.html(内嵌弹窗) | 弹窗 |
| 8 | 添加成员 | add-member.html | 页面 |
| - | 连接设备弹窗 | main.html(内嵌弹窗) | 弹窗 |
| - | 取消配对确认弹窗 | main.html(内嵌弹窗) | 弹窗 |
| - | 数据补传弹窗 | main.html(内嵌弹窗) | 弹窗 |
| - | 数据补传弹窗(再次补传-已上传态) | main.html(内嵌弹窗) | 弹窗(状态变体) |
### 数据查看
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 9 | 主页(数据Tab | main.html#tab-data | 页面(Tab内页面) |
| - | 切换用户弹窗 | main.html(内嵌弹窗) | 弹窗 |
| 10 | 详细报告 | data-detail.html | 页面 |
### 我的
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 11 | 主页(我的Tab | main.html#tab-me | 页面(Tab内页面) |
| 12 | 个人信息 | profile.html | 页面 |
| 13 | 帮助中心 | help-center.html | 页面 |
| 14 | 帮助详情 | help-detail.html | 页面 |
---
## 逐页说明
---
## 一、登录与信息完善
### 1. 登录页(index.html
![页面截图](screenshots/index.jpg)
**功能说明:** 蓝牙称小程序启动入口,微信一键登录。
**主要UI元素:**
- 品牌Logo区域
- 功能亮点展示(精准称重、趋势分析、多成员管理)
- 微信一键登录按钮
**业务规则:**
- 首次登录后若无个人信息,跳转至完善个人信息页面
- 已完善信息的老用户,登录后直接进入主页
**交互说明:**
- 点击"微信一键登录"按钮,进行微信授权,成功后跳转到完善个人信息页面
---
### 2. 完善个人信息(profile-complete.html
![页面截图](screenshots/profile-complete.jpg)
**功能说明:** 新用户首次登录后完善个人基本信息,用于精准计算体脂数据。
**主要UI元素:**
- 提示条:"请完整填写信息,用于精准计算体脂数据"
- 表单:姓名(必填)、身份证(必填,输入后自动识别性别和年龄)、性别(根据身份证自动识别)、年龄(根据身份证自动识别)、身高(必填,cm)
**业务规则:**
- 身份证输完后,自动解析性别和出生年份计算年龄
- 如身份证存在于长庆系统,则需要将姓名和身份证做校验匹配,必须保持一致才行,如不一致,弹窗提示“当前身份证与姓名信息不一致,请重新填写”
**交互说明:**
- 输入身份证号后自动填充性别和年龄
- 点击"完成并进入首页"保存信息并跳转到主页
---
## 二、主页
主页采用底部4个Tab(首页/设备/数据/我的)布局,每个Tab为独立内容面板,通过Tab切换显示。
---
### 3. 主页-首页Tabmain.html#tab-home
![页面截图](screenshots/main.jpg)
**功能说明:** 展示当前用户的设备概览和最近一次测量数据,包括体重、BMI、体脂率及核心身体成分指标。
**主要UI元素:**
- 绿色头部(程序信息卡片)
- 个人信息卡片(姓名、身高、年龄、性别)
- 最近测量卡片:
- 标题栏("最近测量" + "详细报告 "链接)
- 测量时间
- 体重数字
- 较上次变化量(绿色↓/红色↑/无变化不显示)
- BMI和体脂率并列展示
- 详细数据网格(体脂肪量、体水分量、蛋白质量)
**业务规则:**
- BMI参考范围:<18.5消瘦、18.5≤BMI<24正常、24≤BMI<28超重、≥28肥胖
- 体脂率等级标准:
- 男性:18-39岁(偏瘦<11%、标准11-20%、偏胖20-25%、肥胖>25%);40-59岁(偏瘦<12%、标准12-21%、偏胖21-26%、肥胖>26%);60岁以上(偏瘦<14%、标准14-23%、偏胖23-28%、肥胖>28%
- 女性:18-39岁(偏瘦<21%、标准21-32%、偏胖32-37%、肥胖>37%);40-59岁(偏瘦<23%、标准23-34%、偏胖34-39%、肥胖>39%);60岁以上(偏瘦<25%、标准25-36%、偏胖36-41%、肥胖>41%
- 首次使用无测量数据时显示空状态
**交互说明:**
- 点击"详细报告"跳转到详细报告页面
---
### 4. 主页-设备Tabmain.html#tab-device
![页面截图](screenshots/main-device.jpg)
**功能说明:** 管理已绑定的蓝牙体脂秤设备,支持查看设备状态(已连接/未连接)、连接设备、WiFi配网、成员管理、数据补传、取消配对等操作。未绑定设备时展示空状态引导绑定。
**主要UI元素:**
- 绿色头部(同首页Tab,标题连续点击五次,触发运维模式)
- 运维模式标记(标题右上角红色徽章"运维模式" + "关闭运维模式"按钮)
- 未绑定空状态:设备图标 + "请绑定设备"文案 + "绑定设备"按钮
- 已配对设备列表:
- 设备卡片(图标、名称、型号、设备编码、连接状态徽章、取消配对按钮)
- 操作按钮组:设备WIFI / 成员管理(人数) / 数据补传 / 连接设备
- " 添加设备"按钮
**业务规则:**
- 连续点击设备Tab标题5次进入运维模式,显示"运维模式"文字 、 "关闭运维模式"按钮、"取消全部配对"按钮
- 未连接设备显示"连接设备"、“成员管理”按钮,已连接设备显示“设备WIFI”、“成员管理”、"数据补传"按钮
**交互说明:**
- 未绑定状态:"绑定设备"按钮跳转到搜索设备页面
- 已绑定设备:设备WIFI跳转到WiFi连接页面;成员管理跳转到成员管理页面;数据补传打开数据补传弹窗;连接设备打开连接弹窗
- 取消配对:点击单个设备的取消配对按钮,弹出二次确认弹窗,确认后从列表中删除该设备;若删除后无剩余设备则恢复空状态
- 运维模式-取消全部配对:清除所有配对,弹出二次确认弹窗,确认后从列表中删除所有设备。
**状态变体 — 未绑定空状态:**
![空状态截图](screenshots/main-device-empty.jpg)
**功能说明:** 设备 Tab 在未绑定任何设备时展示空状态引导页。点击按钮跳转至搜索设备页面。
---
#### 连接设备弹窗(main.html 内嵌)
![弹窗截图](screenshots/main-connect-dialog.jpg)
**功能说明:** 点击未连接设备的"连接设备"按钮后,进行蓝牙设备连接过程。
**主要UI元素:**
- 弹窗loading
**交互说明:**
- 连接成功后,关闭弹窗,失败则提示错误信息弹窗。
---
#### 数据补传弹窗(main.html 内嵌)
![弹窗截图](screenshots/main-retransmission-dialog.jpg)
![弹窗截图](screenshots/main-retransmission-done.jpg)
点击已连接设备的"数据补传"按钮后弹出:
- 1:获取设备本地未上传的数据,弹窗提示“正在上传数据,请稍后...”,弹窗不允许点击空白时关闭
- 2:获取到之后上传数据,弹窗提示“正在上传数据,请稍后...”(弹窗不允许点击空白时关闭),上传成功后关闭弹窗,toast提示“数据已全部上传”
- 3:未获取到提示“设备本地数据已全部上传”
---
#### 取消配对确认弹窗(main.html 内嵌)
**功能说明:** 点击设备卡片右上角"取消配对"按钮后弹出二次确认弹窗,防止误操作导致设备解绑。
**主要UI元素:**
- 确认文字:"是否取消配对{设备名称}设备?"(设备名称根据点击的设备动态显示)
- 取消按钮(灰色)+ 确定按钮(红色)
**业务规则:**
- 弹窗标题文字根据被点击的设备动态拼接,格式为"是否取消配对XXXXX设备?"
- 确认后从设备列表中移除该设备卡片
- 若移除后设备列表为空,自动切换至未绑定空状态
- 取消按钮关闭弹窗,不做任何操作
**交互说明:**
- 点击"确定" → 关闭弹窗,删除对应设备卡片,显示Toast"设备「{设备名称}」已取消配对"
- 点击"取消" → 关闭弹窗,不做任何操作
---
### 5. 搜索设备(search-device.html
![页面截图](screenshots/search-device.jpg)
**功能说明:** 通过蓝牙搜索附近可用的体脂秤设备,搜索过程中显示脉冲动画,搜索完成后列出发现的设备。
**主要UI元素:**
- 搜索动画区域(设备图标 + 三层脉冲波纹动画)
- 搜索中提示:"搜索设备中…"
- 操作提示:"请确保手机蓝牙已开启,设备处于开启状态,手机尽量靠近设备(2米以内)"
- 搜索完成后的设备列表:
- 重新搜索按钮
- 设备卡片(设备图标、名称、信号强度、连接状态)
- 已连接的设备显示"已连接"徽章
- 未连接的设备显示箭头
**交互说明:**
- 页面自动开启搜索,搜索后自动显示结果,脉冲动画停止
- 点击已连接设备 → 直接跳转到设备Tab
- 点击未连接设备 → 完成蓝牙连接操作,连接成功后跳转到WiFi连接页面进行配网
- 点击"重新搜索"再次开启搜索
---
### 6. WiFi连接(wifi-connect.html
![页面截图](screenshots/wifi-connect.jpg)
**功能说明:** 为蓝牙体脂秤配置WiFi网络连接,支持从搜索设备进入(首次配网)和从已配对设备进入(修改WiFi)两种场景。
**主要UI元素:**
- 导航栏(标题"WiFi连接",搜索设备页面连接蓝牙后进入时显示"跳过"按钮)
- 搜索WiFi动画(📡图标 + "正在搜索附近 WiFi…"
- WiFi列表:
- 网络名称、安全类型
- 已连接网络显示 ✓ 标记和绿色边框
- 受密码保护的网络显示🔒标记
**业务规则:**
- 从首页设备页进入:跳过搜索动画直接显示WiFi列表,隐藏"跳过"按钮
- 从搜索设备进入:搜索动画后显示WiFi列表
- "跳过"按钮点击后直接返回设备Tab
**交互说明:**
- 点击WiFi网络 → 密码输入页面,点击连接按钮,进行WiFi连接,成功后跳转到设备Tab
- 点击"刷新" → 重新搜索刷新WiFi列表
- 点击"跳过" → 直接跳转到设备Tab(不配置WiFi)
---
### 7. 成员管理(device-members.html
![页面截图](screenshots/device-members.jpg)
**功能说明:** 管理蓝牙体脂秤设备的测量成员列表,支持成员信息的增删改查、成员信息同步到设备。
**主要UI元素:**
- 设备信息卡片(设备图标、名称、型号、设备编码、连接状态)
- 已录入成员标题(含容量显示如"已录入成员(4/10)"
- 同步按钮("同步成员至设备",设备未连接时禁用并置灰)
- 同步提示条(当前存在未同步的成员信息,"当前成员信息未同步至设备,请在连接设备的状态下,点击同步按钮完成信息同步")
- 成员卡片:
- 头像(按性别分类着色:男=蓝色、女=粉色)
- 姓名 + 身份标签(本人/员工/家庭成员)
- 同步状态(已同步=绿色、未同步=红色)
- 基本信息(性别、年龄、身高)
- 操作按钮(编辑/删除)
- 空状态("暂无成员" + "请添加成员以使用设备测量"文字)
- 按钮:"一键添加本人"按钮 + "添加新成员"按钮
**业务规则:**
- 设备成员容量上限为10人
- 本人成员置顶显示,隐藏编辑按钮
- 他人添加的成员标注"(他人添加)"提示
- 删除成员后卡片变为半透明,"已删除 · 未同步"状态需执行同步才会从设备端移除
- 设备未连接时,同步按钮禁用并置灰
- 员工类型成员时,隐藏编辑按钮
- 未存在身份类型为本人的成员时,显示“一键添加本人”按钮
- 运维模式下:同步成员设备时,不计入关联处理,该成员不与运维人员进行实际性关联,不属于“他人添加”标记
- 所有同步设备操作记录都需留存,操作人、操作时间、成员数据信息、设备信息(设备编码)
**交互说明:**
- 点击"同步成员至设备" → 打开同步进度弹窗
- 点击"添加新成员" → 跳转到添加成员页面
- 点击"一键添加本人" → 自动添加当前个人信息的成员,刷新列表,更新成员计数
- 点击删除按钮 → 打开删除确认弹窗
- 点击编辑按钮 → 提示"编辑功能开发中"
- 同步弹窗进度条动画完成后自动关闭并提示
**成员空状态:**
![空状态截图](screenshots/device-members-empty.jpg)
当设备成员数为 0 时(,页面展示空状态:"暂无成员" + "请添加成员以使用设备测量"文案,底部双按钮"一键添加本人"和"添加新成员"。
---
#### 同步成员至设备弹窗(device-members.html 内嵌)
![弹窗截图](screenshots/device-members-sync-dialog.jpg)
点击"同步成员至设备"后进行设备同步成员操作,并弹窗提示,带进度条,显示"正在同步信息,请勿离开...",弹窗不允许点击空白时关闭
同步成功后,关闭弹窗,toast提示“同步完成”,刷新列表数据
---
#### 删除成员确认弹窗(device-members.html 内嵌)
![弹窗截图](screenshots/device-members-delete-dialog.jpg)
**功能说明:** 删除成员前的二次确认弹窗,防止误操作。
**主要UI元素:**
- ⚠️ 图标
- 确认文字"是否确认删除{成员名}?",如果是他人添加,则确认文字为:‘XXXX’成员为他人添加,请确认是否删除
- 取消按钮 + 确定按钮(红色)
**交互说明:**
- 点击"确定" → 成员卡片变为半透明删除态,同步状态更新为"已删除 · 未同步"
- 点击"取消" → 关闭弹窗
---
### 8. 添加成员(add-member.html
![页面截图](screenshots/add-member.jpg)
**功能说明:** 向设备添加新的测量成员,支持两种用户类型:员工(通过员工编号查询)和家庭成员(手动填写信息或从已有成员中选择)。
**主要UI元素:**
- 用户类型选择(员工 / 家庭成员)
- 员工表单:
- 员工编号输入 + "确认查询"按钮
- 查询结果卡片(姓名、单位-部门、性别、年龄)
- 补充信息表单(身高必填、体重选填,员工有数据则自动填入)
- 家庭成员表单:
- "从已有成员中选择"按钮
- 昵称(必填)、身份证(选填,填写后自动识别性别和出生年月)
- 性别选择(男女按钮)、出生年月(月份选择器,必填)
- 身高(必填,cm)、体重(选填,kg)
- 已有成员选择器弹窗(底部弹出,列出可选成员(本人添加过的当前设备外其他设备已录入的成员),点击选择后自动填充表单)
**业务规则:**
- 员工类型:通过员工编号查询系统返回基本信息,身高和体重查询到数据后自动填入
- 家庭成员类型:身份证填写后可自动解析性别和出生年月
- 家庭成员可从"已有成员"中选择,快速填充当前设备外其他设备已录入的成员
**交互说明:**
- 点击用户类型Tab切换员工/家庭成员表单
- 员工模式:输入编号后点击"确认查询"展示查询结果和补充表单
- 家庭成员模式:点击"从已有成员中选择"打开底部成员选择器弹窗
- 选择已有成员后自动填充昵称、性别、出生年月、身高、体重
- 输入身份证自动解析性别和出生年月
- 点击"保存用户信息" → 保存成功后Toast提示"保存成功!"并返回上一页
---
## 三、数据查看
### 9. 主页-数据Tabmain.html#tab-data
![页面截图](screenshots/main-data.jpg)
**功能说明:** 展示当前用户的体重记录列表和统计摘要,支持切换查看不同成员的数据,以及按时间范围(近7日/近30日/近90日)筛选。
**主要UI元素:**
- 用户信息卡片(头像、姓名、身份类型、身高、年龄、性别 + "切换用户"按钮)
- 时间范围筛选(近7日/近30日/近90日 三个Tab
- 统计卡片行(4格:最高体重/最低体重/平均体重/体重变化)
- 体重记录列表:
- 测量时间、体重数值、BMI + 体脂率
- 体重变化箭头(▲上升红色/▼下降绿色/无变化不显示)
- 右箭头进入详情
**业务规则:**
- 倒序显示
**交互说明:**
- 点击"切换用户" → 打开成员选择弹窗,显示本人及本人添加的成员列表,选择后显示该成员的数据
- 点击时间Tab(近7日/近30日/近90日)切换统计数据和记录列表
- 点击记录条目 → 跳转到详细报告页面
---
#### 切换用户弹窗(main.html 内嵌)
![弹窗截图](screenshots/main-member-modal.jpg)
**功能说明:** 在数据Tab中切换查看不同家庭成员的数据,底部弹出成员列表供选择。
**主要UI元素:**
- 弹窗标题"切换用户" + 关闭按钮(×)
- 成员列表:
- 头像
- 姓名 + 身份标签(本人/员工/家庭成员)
- 基本信息(性别、年龄、身高)
**业务规则:**
- 显示本人及本人添加的成员列表
**交互说明:**
- 点击成员 → 关闭弹窗,Toast提示"已切换到「{姓名}」"
---
### 10. 详细报告(data-detail.html
![页面截图](screenshots/data-detail.jpg)
**功能说明:** 展示单次测量的完整身体成分报告,包含体重的核心指标、身体成分分析(水分、蛋白质、肌肉、骨骼、脂肪、细胞液)。
**主要UI元素:**
- 报告头部(绿色渐变背景):
- 测量时间
- 体重数字大字展示
- 标签(BMI状态如"正常"、较上次变化量)
- 基础指标(网格布局2列):
- 身高/体重/BMI(含状态标签)/基础代谢
- 体脂肪量/体脂肪率(含状态标签)
- BMI彩色标尺
- 水分与蛋白质(行布局):
- 体水分量/体水分率/蛋白质量/蛋白质率
- 肌肉与骨骼:
- 体肌肉量/体肌肉率/体骨骼量
- 脂肪分析:
- 皮下脂肪量/皮下脂肪率/除脂肪量
- 细胞液:
- 细胞外液量/细胞外液率/细胞内液量/细胞内液率
- 其他:
- 矿物质量
**业务规则:**
- 各项指标状态通过标准参考范围判断。体脂肪率等级结合性别和年龄动态计算
- BMI参考范围:<18.5消瘦、18.5≤BMI<24正常、24≤BMI<28超重、≥28肥胖
- 体脂率等级标准:
- 男性:18-39岁(偏瘦<11%、标准11-20%、偏胖20-25%、肥胖>25%);40-59岁(偏瘦<12%、标准12-21%、偏胖21-26%、肥胖>26%);60岁以上(偏瘦<14%、标准14-23%、偏胖23-28%、肥胖>28%
- 女性:18-39岁(偏瘦<21%、标准21-32%、偏胖32-37%、肥胖>37%);40-59岁(偏瘦<23%、标准23-34%、偏胖34-39%、肥胖>39%);60岁以上(偏瘦<25%、标准25-36%、偏胖36-41%、肥胖>41%
**交互说明:**
- 点击返回按钮返回主页
- 页面为纯展示,无可交互元素
---
## 四、我的
### 11. 主页-我的Tabmain.html#tab-me
![页面截图](screenshots/main-me.jpg)
**功能说明:** 个人中心入口,展示用户基本信息,提供个人信息查看、帮助中心和退出登录等功能的入口。
**主要UI元素:**
- 用户信息卡片(头像、姓名、单位-部门)
- 菜单列表:
- 个人信息
- 帮助中心
- 退出登录按钮
**业务规则:**
- 单位部门不存在则隐藏,姓名和头像垂直居中
**交互说明:**
- 点击"个人信息" → 跳转到个人信息查看页面
- 点击"帮助中心" → 跳转到帮助中心页面
- 点击"退出登录" → 弹出二次确认弹窗,确定后跳转到登录页
---
### 12. 个人信息(profile.html
![页面截图](screenshots/profile.jpg)
**功能说明:** 查看当前用户的详细个人信息。
**主要UI元素:**
- 头像展示
- 信息列表:
- 姓名/身份证/性别/年龄/单位/部门/身高/体重
**交互说明:**
- 点击返回按钮返回我的Tab
---
### 13. 帮助中心(help-center.html
![页面截图](screenshots/help-center.jpg)
**功能说明:** 展示常见问题FAQ列表,支持按分类筛选查看。
**主要UI元素:**
- 分类标签(全部/连接配对/使用测量/数据管理)
- FAQ列表:
- 问题标题
- 右箭头进入详情
**交互说明:**
- 点击分类标签筛选列表
- 点击列表 → 跳转到帮助详情页面
---
### 14. 帮助详情(help-detail.html
![页面截图](screenshots/help-detail.jpg)
**功能说明:** 展示单条的详细解答内容
**主要UI元素:**
- 问题标题
- 答案内容(富文本)
**交互说明:**
- 点击返回按钮返回帮助中心页面
---
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -68,11 +68,6 @@
.faq-item:last-child { border-radius: 0 0 14px 14px; border-bottom: none; }
.faq-item:only-child { border-radius: 14px; }
.faq-item:active { background: #fafafa; }
.faq-item__icon {
width: 36px; height: 36px; border-radius: 10px;
background: #f0faf5; display: flex; align-items: center;
justify-content: center; font-size: 16px; flex-shrink: 0;
}
.faq-item__info { flex: 1; min-width: 0; }
.faq-item__title { font-size: 14px; font-weight: 500; color: #1a1a1a; }
.faq-item__arrow { font-size: 16px; color: #ccc; }
@@ -109,42 +104,36 @@
<div class="faq-list" id="faqList">
<a class="faq-item" href="help-detail.html?id=1" data-category="connect">
<div class="faq-item__icon">📡</div>
<div class="faq-item__info">
<div class="faq-item__title">如何连接蓝牙设备?</div>
</div>
<span class="faq-item__arrow"></span>
</a>
<a class="faq-item" href="help-detail.html?id=2" data-category="connect">
<div class="faq-item__icon">📶</div>
<div class="faq-item__info">
<div class="faq-item__title">设备无法连接WiFi怎么办?</div>
</div>
<span class="faq-item__arrow"></span>
</a>
<a class="faq-item" href="help-detail.html?id=3" data-category="measure">
<div class="faq-item__icon">👥</div>
<div class="faq-item__info">
<div class="faq-item__title">如何添加和管理成员?</div>
</div>
<span class="faq-item__arrow"></span>
</a>
<a class="faq-item" href="help-detail.html?id=4" data-category="measure">
<div class="faq-item__icon">📊</div>
<div class="faq-item__info">
<div class="faq-item__title">测量数据不准确如何校准?</div>
</div>
<span class="faq-item__arrow"></span>
</a>
<a class="faq-item" href="help-detail.html?id=5" data-category="data">
<div class="faq-item__icon">🔄</div>
<div class="faq-item__info">
<div class="faq-item__title">数据补传功能如何使用?</div>
</div>
<span class="faq-item__arrow"></span>
</a>
<a class="faq-item" href="help-detail.html?id=6" data-category="measure">
<div class="faq-item__icon">🔋</div>
<div class="faq-item__info">
<div class="faq-item__title">设备电量消耗过快?</div>
</div>
@@ -160,6 +160,18 @@
background: #e8f8ef; color: #07c160; border-radius: 6px;
padding: 2px 8px; font-size: 10px; font-weight: 500; display: inline-block;
}
.tag-blue {
background: #e6f7ff; color: #1890ff; border-radius: 6px;
padding: 2px 8px; font-size: 10px; font-weight: 500; display: inline-block;
}
.tag-orange {
background: #fff7e6; color: #fa8c16; border-radius: 6px;
padding: 2px 8px; font-size: 10px; font-weight: 500; display: inline-block;
}
.tag-red {
background: #fff2f0; color: #ff4d4f; border-radius: 6px;
padding: 2px 8px; font-size: 10px; font-weight: 500; display: inline-block;
}
/* 空状态 */
.empty-state {
@@ -329,7 +341,7 @@
}
.record-change--up { color: var(--danger); }
.record-change--down { color: var(--success); }
.record-change--flat { color: var(--text-placeholder); }
.record-change--flat { display: none; }
.record-arrow { font-size: 16px; color: #ccc; }
/* ===== 我的 Tab 样式 ===== */
@@ -384,6 +396,28 @@
}
.toast--show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* 确认弹窗 */
.confirm-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000;
display: none; align-items: center; justify-content: center;
}
.confirm-overlay--show { display: flex; }
.confirm-dialog {
width: 280px; background: #fff; border-radius: 16px; padding: 28px 24px 20px;
text-align: center; box-shadow: 0 12px 40px rgba(0,0,0,0.25);
animation: connectDialogIn 0.3s ease;
}
.confirm-dialog__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; line-height: 1.5; }
.confirm-dialog__actions { display: flex; gap: 12px; margin-top: 20px; }
.confirm-dialog__btn {
flex: 1; height: 40px; border: none; border-radius: 10px; font-size: 15px;
font-weight: 600; cursor: pointer; transition: all 0.2s;
}
.confirm-dialog__btn--cancel { background: #f5f5f5; color: #666; }
.confirm-dialog__btn--cancel:active { background: #e8e8e8; }
.confirm-dialog__btn--confirm { background: var(--danger); color: #fff; }
.confirm-dialog__btn--confirm:active { background: #d9363e; }
/* 连接弹窗 */
.connect-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000;
@@ -461,13 +495,13 @@
<div class="measure-card__stat">
<div class="measure-card__stat-label">BMI</div>
<div class="measure-card__stat-val">23.8</div>
<div class="measure-card__stat-tag"><span class="tag-green">正常范围</span></div>
<div class="measure-card__stat-tag"><span class="tag-green">正常</span></div>
</div>
<div class="measure-card__stat-divider"></div>
<div class="measure-card__stat">
<div class="measure-card__stat-label">体脂率</div>
<div class="measure-card__stat-val">22.1<span style="font-size:11px;color:#999;">%</span></div>
<div class="measure-card__stat-tag"><span class="tag-green">标准</span></div>
<div class="measure-card__stat-tag"><span class="tag-green" id="homeFatLevelTag">标准</span></div>
</div>
</div>
</div>
@@ -733,7 +767,6 @@
<div class="profile-avatar"></div>
<div class="profile-info">
<div class="profile-name">张建国</div>
<div class="profile-phone">男 · 45岁</div>
<div class="profile-phone">健康科技公司 · 技术部-研发组</div>
</div>
</div>
@@ -992,9 +1025,43 @@
showToast('已取消全部配对');
}
var _unpairTarget = null;
var _deviceNameMap = { 's1': '家庭蓝牙体脂秤 S1', 'b2': '办公蓝牙体重秤 B2' };
function unpairDevice(deviceId) {
// 单个设备取消配对(模拟:提示后不做实际移除,保持列表展示)
showToast('设备「' + (deviceId === 's1' ? '家庭蓝牙体脂秤 S1' : '办公蓝牙体重秤 B2') + '」已取消配对');
_unpairTarget = deviceId;
var name = _deviceNameMap[deviceId] || '未知';
document.getElementById('confirmDialogTitle').textContent = '是否取消配对' + name + '设备?';
document.getElementById('confirmOverlay').classList.add('confirm-overlay--show');
}
function confirmUnpair() {
var deviceId = _unpairTarget;
if (!deviceId) return;
// 从 DOM 中移除对应设备项
var items = document.querySelectorAll('.device-item');
for (var i = 0; i < items.length; i++) {
var btn = items[i].querySelector('.device-item__unpair');
if (btn && btn.getAttribute('onclick').indexOf("'" + deviceId + "'") !== -1) {
items[i].remove();
break;
}
}
// 如果列表中已无设备,恢复空状态
var remaining = document.querySelectorAll('.device-item');
if (remaining.length === 0) {
document.getElementById('deviceEmpty').style.display = '';
document.getElementById('devicePaired').style.display = 'none';
sessionStorage.removeItem('devicePaired');
}
var name = _deviceNameMap[deviceId] || '未知';
showToast('设备「' + name + '」已取消配对');
closeConfirmDialog();
}
function closeConfirmDialog() {
document.getElementById('confirmOverlay').classList.remove('confirm-overlay--show');
_unpairTarget = null;
}
/* ===== 成员选择弹窗 ===== */
@@ -1008,6 +1075,35 @@
closeMemberModal();
showToast('已切换到「' + name + '」');
}
/* ===== 体脂率等级判断 ===== */
function getBodyFatLevel(sex, age, bf) {
// 日本肥胖学会分龄分性别标准
var thresholds;
if (sex === 'male' || sex === '男') {
if (age < 40) thresholds = { thin: 11, fit: 20, heavy: 25 };
else if (age < 60) thresholds = { thin: 12, fit: 21, heavy: 26 };
else thresholds = { thin: 14, fit: 23, heavy: 28 };
} else {
if (age < 40) thresholds = { thin: 21, fit: 32, heavy: 37 };
else if (age < 60) thresholds = { thin: 23, fit: 34, heavy: 39 };
else thresholds = { thin: 25, fit: 36, heavy: 41 };
}
if (bf < thresholds.thin) return { label: '偏瘦', cssClass: 'tag-blue' };
if (bf <= thresholds.fit) return { label: '标准', cssClass: 'tag-green' };
if (bf <= thresholds.heavy) return { label: '偏胖', cssClass: 'tag-orange' };
return { label: '肥胖', cssClass: 'tag-red' };
}
// 初始化首页体脂率等级标签(张建国:男,45岁,体脂率22.1%)
(function() {
var tag = document.getElementById('homeFatLevelTag');
if (tag) {
var level = getBodyFatLevel('male', 45, 22.1);
tag.textContent = level.label;
tag.className = level.cssClass;
}
})();
</script>
<!-- 连接弹窗 -->
@@ -1028,5 +1124,16 @@
</div>
</div>
<!-- 取消配对确认弹窗 -->
<div class="confirm-overlay" id="confirmOverlay">
<div class="confirm-dialog">
<div class="confirm-dialog__title" id="confirmDialogTitle">是否取消配对设备?</div>
<div class="confirm-dialog__actions">
<button class="confirm-dialog__btn confirm-dialog__btn--cancel" onclick="closeConfirmDialog()">取消</button>
<button class="confirm-dialog__btn confirm-dialog__btn--confirm" onclick="confirmUnpair()">确定</button>
</div>
</div>
</div>
</body>
</html>
@@ -69,13 +69,6 @@
.info-label { font-size: 14px; color: var(--text-secondary); width: 72px; flex-shrink: 0; }
.info-value { flex: 1; font-size: 14px; color: var(--text-primary); text-align: right; }
/* 修改按钮 */
.edit-btn {
width: 100%; height: 44px; border: 1px solid var(--primary); border-radius: 12px;
background: #fff; color: var(--primary); font-size: 15px; font-weight: 600;
cursor: pointer; margin-top: 24px;
}
.home-indicator {
height: 20px; display: flex; align-items: center; justify-content: center;
background: #fff; flex-shrink: 0;
@@ -112,9 +105,10 @@
<div class="info-row"><span class="info-label">单位</span><span class="info-value">健康科技公司</span></div>
<div class="info-row"><span class="info-label">部门</span><span class="info-value">技术部-研发组</span></div>
<div class="info-row"><span class="info-label">身高</span><span class="info-value">175cm</span></div>
<div class="info-row"><span class="info-label">体重</span><span class="info-value">72kg</span></div>
</div>
<button class="edit-btn" onclick="location.href='profile-complete.html?edit=1&name=张建国&gender=male&age=45&height=175'">修改信息</button>
</div>
<div class="home-indicator"></div>
-72
View File
@@ -1,72 +0,0 @@
@echo off
chcp 65001 >nul
title 健康CQ原型 - 本地服务器
cd /d "%~dp0"
echo ============================================
echo 健康CQ升级项目 · 高保真交互原型
echo ============================================
echo.
echo [检测] 检查端口 8080 ...
:: ── 检查端口 8080 是否已被占用 ──
netstat -ano 2>nul | findstr ":8080" | findstr "LISTENING" >nul
if %errorlevel% equ 0 (
echo [警告] 端口 8080 已被占用!
echo.
echo 请先关闭占用 8080 端口的程序,或者:
echo 1. 任务管理器关闭相关进程
echo 2. 运行: netstat -ano ^| findstr :8080 查看 PID
echo 3. 运行: taskkill /PID ^<PID^> /F 强制结束
:: 尝试直接打开已有的服务
echo.
echo [尝试] 直接打开浏览器访问 http://localhost:8080/ ...
start http://localhost:8080/
pause
exit /b 0
)
:: ── 优先 Python(最稳定,无需联网)──
where python >nul 2>nul
if %errorlevel% equ 0 (
echo [启动] 使用 Python 启动 HTTP 服务器(端口 8080...
echo.
echo ★ 服务器运行中,请勿关闭本窗口 ★
echo 访问地址:http://localhost:8080/
echo 按 Ctrl+C 停止服务器
echo ============================================
echo.
python -m http.server 8080
goto end
)
:: ── 备选 npx serve(需联网下载)──
where npx >nul 2>nul
if %errorlevel% equ 0 (
echo [启动] 使用 npx serve 启动服务器(端口 8080...
echo 首次运行可能需要下载依赖,请稍候...
echo.
echo ★ 服务器运行中,请勿关闭本窗口 ★
echo 访问地址:http://localhost:8080/
echo 按 Ctrl+C 停止服务器
echo ============================================
echo.
npx --yes serve . -p 8080 --no-clipboard
goto end
)
:: ── 都不可用 ──
echo [错误] 未找到 Python 或 Node.js
echo.
echo 请安装以下任一工具:
echo · Python 3: https://www.python.org/downloads/
echo · Node.js: https://nodejs.org/
echo.
pause
exit /b 1
:end
echo.
echo 服务器已停止。
pause