feat: 更新蓝牙体重秤小程序页面,新增PRD文档,移除启动脚本
This commit is contained in:
+94
-192
@@ -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 内容生成 ===== */
|
||||
|
||||
Reference in New Issue
Block a user