feat: 更新蓝牙体重秤小程序页面,新增PRD文档,移除启动脚本
@@ -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>';
|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 蓝牙称小程序启动入口,微信一键登录。
|
||||
|
||||
**主要UI元素:**
|
||||
- 品牌Logo区域
|
||||
- 功能亮点展示(精准称重、趋势分析、多成员管理)
|
||||
- 微信一键登录按钮
|
||||
|
||||
**业务规则:**
|
||||
- 首次登录后若无个人信息,跳转至完善个人信息页面
|
||||
- 已完善信息的老用户,登录后直接进入主页
|
||||
|
||||
**交互说明:**
|
||||
- 点击"微信一键登录"按钮,进行微信授权,成功后跳转到完善个人信息页面
|
||||
|
||||
---
|
||||
|
||||
### 2. 完善个人信息(profile-complete.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 新用户首次登录后完善个人基本信息,用于精准计算体脂数据。
|
||||
|
||||
**主要UI元素:**
|
||||
- 提示条:"请完整填写信息,用于精准计算体脂数据"
|
||||
- 表单:姓名(必填)、身份证(必填,输入后自动识别性别和年龄)、性别(根据身份证自动识别)、年龄(根据身份证自动识别)、身高(必填,cm)
|
||||
|
||||
**业务规则:**
|
||||
- 身份证输完后,自动解析性别和出生年份计算年龄
|
||||
- 如身份证存在于长庆系统,则需要将姓名和身份证做校验匹配,必须保持一致才行,如不一致,弹窗提示“当前身份证与姓名信息不一致,请重新填写”
|
||||
|
||||
**交互说明:**
|
||||
- 输入身份证号后自动填充性别和年龄
|
||||
- 点击"完成并进入首页"保存信息并跳转到主页
|
||||
|
||||
---
|
||||
|
||||
## 二、主页
|
||||
|
||||
主页采用底部4个Tab(首页/设备/数据/我的)布局,每个Tab为独立内容面板,通过Tab切换显示。
|
||||
|
||||
---
|
||||
|
||||
### 3. 主页-首页Tab(main.html#tab-home)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示当前用户的设备概览和最近一次测量数据,包括体重、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. 主页-设备Tab(main.html#tab-device)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 管理已绑定的蓝牙体脂秤设备,支持查看设备状态(已连接/未连接)、连接设备、WiFi配网、成员管理、数据补传、取消配对等操作。未绑定设备时展示空状态引导绑定。
|
||||
|
||||
**主要UI元素:**
|
||||
- 绿色头部(同首页Tab,标题连续点击五次,触发运维模式)
|
||||
- 运维模式标记(标题右上角红色徽章"运维模式" + "关闭运维模式"按钮)
|
||||
- 未绑定空状态:设备图标 + "请绑定设备"文案 + "绑定设备"按钮
|
||||
- 已配对设备列表:
|
||||
- 设备卡片(图标、名称、型号、设备编码、连接状态徽章、取消配对按钮)
|
||||
- 操作按钮组:设备WIFI / 成员管理(人数) / 数据补传 / 连接设备
|
||||
- "+ 添加设备"按钮
|
||||
|
||||
**业务规则:**
|
||||
- 连续点击设备Tab标题5次进入运维模式,显示"运维模式"文字 、 "关闭运维模式"按钮、"取消全部配对"按钮
|
||||
- 未连接设备显示"连接设备"、“成员管理”按钮,已连接设备显示“设备WIFI”、“成员管理”、"数据补传"按钮
|
||||
|
||||
**交互说明:**
|
||||
- 未绑定状态:"绑定设备"按钮跳转到搜索设备页面
|
||||
- 已绑定设备:设备WIFI跳转到WiFi连接页面;成员管理跳转到成员管理页面;数据补传打开数据补传弹窗;连接设备打开连接弹窗
|
||||
- 取消配对:点击单个设备的取消配对按钮,弹出二次确认弹窗,确认后从列表中删除该设备;若删除后无剩余设备则恢复空状态
|
||||
- 运维模式-取消全部配对:清除所有配对,弹出二次确认弹窗,确认后从列表中删除所有设备。
|
||||
|
||||
**状态变体 — 未绑定空状态:**
|
||||
|
||||

|
||||
|
||||
**功能说明:** 设备 Tab 在未绑定任何设备时展示空状态引导页。点击按钮跳转至搜索设备页面。
|
||||
|
||||
---
|
||||
|
||||
#### 连接设备弹窗(main.html 内嵌)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 点击未连接设备的"连接设备"按钮后,进行蓝牙设备连接过程。
|
||||
|
||||
**主要UI元素:**
|
||||
- 弹窗loading
|
||||
|
||||
**交互说明:**
|
||||
- 连接成功后,关闭弹窗,失败则提示错误信息弹窗。
|
||||
|
||||
---
|
||||
|
||||
#### 数据补传弹窗(main.html 内嵌)
|
||||
|
||||

|
||||

|
||||
|
||||
点击已连接设备的"数据补传"按钮后弹出:
|
||||
- 1:获取设备本地未上传的数据,弹窗提示“正在上传数据,请稍后...”,弹窗不允许点击空白时关闭
|
||||
- 2:获取到之后上传数据,弹窗提示“正在上传数据,请稍后...”(弹窗不允许点击空白时关闭),上传成功后关闭弹窗,toast提示“数据已全部上传”
|
||||
- 3:未获取到提示“设备本地数据已全部上传”
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 取消配对确认弹窗(main.html 内嵌)
|
||||
|
||||
**功能说明:** 点击设备卡片右上角"取消配对"按钮后弹出二次确认弹窗,防止误操作导致设备解绑。
|
||||
|
||||
**主要UI元素:**
|
||||
- 确认文字:"是否取消配对{设备名称}设备?"(设备名称根据点击的设备动态显示)
|
||||
- 取消按钮(灰色)+ 确定按钮(红色)
|
||||
|
||||
**业务规则:**
|
||||
- 弹窗标题文字根据被点击的设备动态拼接,格式为"是否取消配对XXXXX设备?"
|
||||
- 确认后从设备列表中移除该设备卡片
|
||||
- 若移除后设备列表为空,自动切换至未绑定空状态
|
||||
- 取消按钮关闭弹窗,不做任何操作
|
||||
|
||||
**交互说明:**
|
||||
- 点击"确定" → 关闭弹窗,删除对应设备卡片,显示Toast"设备「{设备名称}」已取消配对"
|
||||
- 点击"取消" → 关闭弹窗,不做任何操作
|
||||
|
||||
---
|
||||
|
||||
### 5. 搜索设备(search-device.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 通过蓝牙搜索附近可用的体脂秤设备,搜索过程中显示脉冲动画,搜索完成后列出发现的设备。
|
||||
|
||||
**主要UI元素:**
|
||||
- 搜索动画区域(设备图标 + 三层脉冲波纹动画)
|
||||
- 搜索中提示:"搜索设备中…"
|
||||
- 操作提示:"请确保手机蓝牙已开启,设备处于开启状态,手机尽量靠近设备(2米以内)"
|
||||
- 搜索完成后的设备列表:
|
||||
- 重新搜索按钮
|
||||
- 设备卡片(设备图标、名称、信号强度、连接状态)
|
||||
- 已连接的设备显示"已连接"徽章
|
||||
- 未连接的设备显示箭头
|
||||
|
||||
**交互说明:**
|
||||
- 页面自动开启搜索,搜索后自动显示结果,脉冲动画停止
|
||||
- 点击已连接设备 → 直接跳转到设备Tab
|
||||
- 点击未连接设备 → 完成蓝牙连接操作,连接成功后跳转到WiFi连接页面进行配网
|
||||
- 点击"重新搜索"再次开启搜索
|
||||
|
||||
---
|
||||
|
||||
### 6. WiFi连接(wifi-connect.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 为蓝牙体脂秤配置WiFi网络连接,支持从搜索设备进入(首次配网)和从已配对设备进入(修改WiFi)两种场景。
|
||||
|
||||
**主要UI元素:**
|
||||
- 导航栏(标题"WiFi连接",搜索设备页面连接蓝牙后进入时显示"跳过"按钮)
|
||||
- 搜索WiFi动画(📡图标 + "正在搜索附近 WiFi…")
|
||||
- WiFi列表:
|
||||
- 网络名称、安全类型
|
||||
- 已连接网络显示 ✓ 标记和绿色边框
|
||||
- 受密码保护的网络显示🔒标记
|
||||
|
||||
**业务规则:**
|
||||
- 从首页设备页进入:跳过搜索动画直接显示WiFi列表,隐藏"跳过"按钮
|
||||
- 从搜索设备进入:搜索动画后显示WiFi列表
|
||||
- "跳过"按钮点击后直接返回设备Tab
|
||||
|
||||
**交互说明:**
|
||||
- 点击WiFi网络 → 密码输入页面,点击连接按钮,进行WiFi连接,成功后跳转到设备Tab
|
||||
- 点击"刷新" → 重新搜索刷新WiFi列表
|
||||
- 点击"跳过" → 直接跳转到设备Tab(不配置WiFi)
|
||||
|
||||
---
|
||||
|
||||
### 7. 成员管理(device-members.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 管理蓝牙体脂秤设备的测量成员列表,支持成员信息的增删改查、成员信息同步到设备。
|
||||
|
||||
**主要UI元素:**
|
||||
- 设备信息卡片(设备图标、名称、型号、设备编码、连接状态)
|
||||
- 已录入成员标题(含容量显示如"已录入成员(4/10)")
|
||||
- 同步按钮("同步成员至设备",设备未连接时禁用并置灰)
|
||||
- 同步提示条(当前存在未同步的成员信息,"当前成员信息未同步至设备,请在连接设备的状态下,点击同步按钮完成信息同步")
|
||||
- 成员卡片:
|
||||
- 头像(按性别分类着色:男=蓝色、女=粉色)
|
||||
- 姓名 + 身份标签(本人/员工/家庭成员)
|
||||
- 同步状态(已同步=绿色、未同步=红色)
|
||||
- 基本信息(性别、年龄、身高)
|
||||
- 操作按钮(编辑/删除)
|
||||
- 空状态("暂无成员" + "请添加成员以使用设备测量"文字)
|
||||
- 按钮:"一键添加本人"按钮 + "添加新成员"按钮
|
||||
|
||||
**业务规则:**
|
||||
- 设备成员容量上限为10人
|
||||
- 本人成员置顶显示,隐藏编辑按钮
|
||||
- 他人添加的成员标注"(他人添加)"提示
|
||||
- 删除成员后卡片变为半透明,"已删除 · 未同步"状态需执行同步才会从设备端移除
|
||||
- 设备未连接时,同步按钮禁用并置灰
|
||||
- 员工类型成员时,隐藏编辑按钮
|
||||
- 未存在身份类型为本人的成员时,显示“一键添加本人”按钮
|
||||
- 运维模式下:同步成员设备时,不计入关联处理,该成员不与运维人员进行实际性关联,不属于“他人添加”标记
|
||||
- 所有同步设备操作记录都需留存,操作人、操作时间、成员数据信息、设备信息(设备编码)
|
||||
|
||||
**交互说明:**
|
||||
- 点击"同步成员至设备" → 打开同步进度弹窗
|
||||
- 点击"添加新成员" → 跳转到添加成员页面
|
||||
- 点击"一键添加本人" → 自动添加当前个人信息的成员,刷新列表,更新成员计数
|
||||
- 点击删除按钮 → 打开删除确认弹窗
|
||||
- 点击编辑按钮 → 提示"编辑功能开发中"
|
||||
- 同步弹窗进度条动画完成后自动关闭并提示
|
||||
|
||||
**成员空状态:**
|
||||
|
||||

|
||||
|
||||
当设备成员数为 0 时(,页面展示空状态:"暂无成员" + "请添加成员以使用设备测量"文案,底部双按钮"一键添加本人"和"添加新成员"。
|
||||
|
||||
---
|
||||
|
||||
#### 同步成员至设备弹窗(device-members.html 内嵌)
|
||||
|
||||
|
||||

|
||||
|
||||
点击"同步成员至设备"后进行设备同步成员操作,并弹窗提示,带进度条,显示"正在同步信息,请勿离开...",弹窗不允许点击空白时关闭
|
||||
同步成功后,关闭弹窗,toast提示“同步完成”,刷新列表数据
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 删除成员确认弹窗(device-members.html 内嵌)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 删除成员前的二次确认弹窗,防止误操作。
|
||||
|
||||
**主要UI元素:**
|
||||
- ⚠️ 图标
|
||||
- 确认文字"是否确认删除{成员名}?",如果是他人添加,则确认文字为:‘XXXX’成员为他人添加,请确认是否删除
|
||||
- 取消按钮 + 确定按钮(红色)
|
||||
|
||||
**交互说明:**
|
||||
- 点击"确定" → 成员卡片变为半透明删除态,同步状态更新为"已删除 · 未同步"
|
||||
- 点击"取消" → 关闭弹窗
|
||||
|
||||
---
|
||||
|
||||
### 8. 添加成员(add-member.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 向设备添加新的测量成员,支持两种用户类型:员工(通过员工编号查询)和家庭成员(手动填写信息或从已有成员中选择)。
|
||||
|
||||
**主要UI元素:**
|
||||
- 用户类型选择(员工 / 家庭成员)
|
||||
- 员工表单:
|
||||
- 员工编号输入 + "确认查询"按钮
|
||||
- 查询结果卡片(姓名、单位-部门、性别、年龄)
|
||||
- 补充信息表单(身高必填、体重选填,员工有数据则自动填入)
|
||||
- 家庭成员表单:
|
||||
- "从已有成员中选择"按钮
|
||||
- 昵称(必填)、身份证(选填,填写后自动识别性别和出生年月)
|
||||
- 性别选择(男女按钮)、出生年月(月份选择器,必填)
|
||||
- 身高(必填,cm)、体重(选填,kg)
|
||||
- 已有成员选择器弹窗(底部弹出,列出可选成员(本人添加过的当前设备外其他设备已录入的成员),点击选择后自动填充表单)
|
||||
|
||||
**业务规则:**
|
||||
- 员工类型:通过员工编号查询系统返回基本信息,身高和体重查询到数据后自动填入
|
||||
- 家庭成员类型:身份证填写后可自动解析性别和出生年月
|
||||
- 家庭成员可从"已有成员"中选择,快速填充当前设备外其他设备已录入的成员
|
||||
|
||||
**交互说明:**
|
||||
- 点击用户类型Tab切换员工/家庭成员表单
|
||||
- 员工模式:输入编号后点击"确认查询"展示查询结果和补充表单
|
||||
- 家庭成员模式:点击"从已有成员中选择"打开底部成员选择器弹窗
|
||||
- 选择已有成员后自动填充昵称、性别、出生年月、身高、体重
|
||||
- 输入身份证自动解析性别和出生年月
|
||||
- 点击"保存用户信息" → 保存成功后Toast提示"保存成功!"并返回上一页
|
||||
|
||||
---
|
||||
|
||||
## 三、数据查看
|
||||
|
||||
### 9. 主页-数据Tab(main.html#tab-data)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示当前用户的体重记录列表和统计摘要,支持切换查看不同成员的数据,以及按时间范围(近7日/近30日/近90日)筛选。
|
||||
|
||||
**主要UI元素:**
|
||||
- 用户信息卡片(头像、姓名、身份类型、身高、年龄、性别 + "切换用户"按钮)
|
||||
- 时间范围筛选(近7日/近30日/近90日 三个Tab)
|
||||
- 统计卡片行(4格:最高体重/最低体重/平均体重/体重变化)
|
||||
- 体重记录列表:
|
||||
- 测量时间、体重数值、BMI + 体脂率
|
||||
- 体重变化箭头(▲上升红色/▼下降绿色/无变化不显示)
|
||||
- 右箭头进入详情
|
||||
|
||||
**业务规则:**
|
||||
- 倒序显示
|
||||
|
||||
**交互说明:**
|
||||
- 点击"切换用户" → 打开成员选择弹窗,显示本人及本人添加的成员列表,选择后显示该成员的数据
|
||||
- 点击时间Tab(近7日/近30日/近90日)切换统计数据和记录列表
|
||||
- 点击记录条目 → 跳转到详细报告页面
|
||||
|
||||
---
|
||||
|
||||
#### 切换用户弹窗(main.html 内嵌)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 在数据Tab中切换查看不同家庭成员的数据,底部弹出成员列表供选择。
|
||||
|
||||
**主要UI元素:**
|
||||
- 弹窗标题"切换用户" + 关闭按钮(×)
|
||||
- 成员列表:
|
||||
- 头像
|
||||
- 姓名 + 身份标签(本人/员工/家庭成员)
|
||||
- 基本信息(性别、年龄、身高)
|
||||
|
||||
**业务规则:**
|
||||
- 显示本人及本人添加的成员列表
|
||||
|
||||
**交互说明:**
|
||||
- 点击成员 → 关闭弹窗,Toast提示"已切换到「{姓名}」"
|
||||
|
||||
---
|
||||
|
||||
### 10. 详细报告(data-detail.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示单次测量的完整身体成分报告,包含体重的核心指标、身体成分分析(水分、蛋白质、肌肉、骨骼、脂肪、细胞液)。
|
||||
|
||||
**主要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. 主页-我的Tab(main.html#tab-me)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 个人中心入口,展示用户基本信息,提供个人信息查看、帮助中心和退出登录等功能的入口。
|
||||
|
||||
**主要UI元素:**
|
||||
- 用户信息卡片(头像、姓名、单位-部门)
|
||||
- 菜单列表:
|
||||
- 个人信息
|
||||
- 帮助中心
|
||||
- 退出登录按钮
|
||||
|
||||
**业务规则:**
|
||||
- 单位部门不存在则隐藏,姓名和头像垂直居中
|
||||
|
||||
**交互说明:**
|
||||
- 点击"个人信息" → 跳转到个人信息查看页面
|
||||
- 点击"帮助中心" → 跳转到帮助中心页面
|
||||
- 点击"退出登录" → 弹出二次确认弹窗,确定后跳转到登录页
|
||||
|
||||
---
|
||||
|
||||
### 12. 个人信息(profile.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看当前用户的详细个人信息。
|
||||
|
||||
**主要UI元素:**
|
||||
- 头像展示
|
||||
- 信息列表:
|
||||
- 姓名/身份证/性别/年龄/单位/部门/身高/体重
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回按钮返回我的Tab
|
||||
|
||||
---
|
||||
|
||||
### 13. 帮助中心(help-center.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示常见问题FAQ列表,支持按分类筛选查看。
|
||||
|
||||
**主要UI元素:**
|
||||
- 分类标签(全部/连接配对/使用测量/数据管理)
|
||||
- FAQ列表:
|
||||
- 问题标题
|
||||
- 右箭头进入详情
|
||||
|
||||
**交互说明:**
|
||||
- 点击分类标签筛选列表
|
||||
- 点击列表 → 跳转到帮助详情页面
|
||||
|
||||
---
|
||||
|
||||
### 14. 帮助详情(help-detail.html)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示单条的详细解答内容
|
||||
|
||||
**主要UI元素:**
|
||||
- 问题标题
|
||||
- 答案内容(富文本)
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回按钮返回帮助中心页面
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
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>
|
||||
|
||||
@@ -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
|
||||