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
+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 内容生成 ===== */