编辑页面

修改后将从当前时间重新计算过期时间
', okText: '确认驳回', cancelText: '取消', onOk: () => { const ta = document.getElementById('rejectReason'); const v = (ta ? ta.value : '').trim(); if(!v){ toast('请先填写驳回原因'); return false; } applyAudit(card); toast('已驳回,原因:' + v); } }); } /* ---------- 个人信息管理 ---------- */ /* 底部选择面板的勾选图标(个人信息 / 参数设置共用) */ const CK_SVG = '' + ''; /* 下拉数据源:医院名称 / 科室 / 职称 */ const HOSPITALS = ['北京协和医院','北京大学第一医院','复旦大学附属中山医院','上海交通大学医学院附属瑞金医院','四川大学华西医院','中山大学附属第一医院','浙江大学医学院附属第一医院','其他医院']; const DEPTS = ['呼吸内科','耳鼻喉科','神经内科','睡眠医学中心','心血管内科','儿科','老年医学科','其他科室']; const TITLES = ['主任医师','副主任医师','主治医师','住院医师','医师','技师','护师','其他']; /* 底部选择面板配置:kind -> { title, el, list } */ /* 开机延迟治疗时间可选档位:0 / 5 / 10 … 60(min) */ const DELAY_LIST = []; for(let v = 0; v <= 60; v += 5) DELAY_LIST.push(String(v)); const SHEET_CFG = { hospital: { title:'选择医院名称', el:'fHospital', list:HOSPITALS }, dept: { title:'选择科室', el:'fDept', list:DEPTS }, title: { title:'选择职称', el:'fTitle', list:TITLES }, /* 主界面「刺激参数」的开机延迟治疗时间:需先进入编辑态,选中即写入参数 */ delay: { title:'开机延迟治疗时间', el:'pDelay', list:DELAY_LIST, suffix:' min', guard: () => { if(!paramEditing){ toast('请先点击「编辑」解锁参数'); return false; } return true; }, pick: v => { setParam('delay', v); toast('开机延迟治疗时间已设为 ' + v + ' min'); } } }; let profileSnap = null, sheetKind = null; function readProfile(){ return { phone: document.getElementById('fPhone').value.trim(), age: document.getElementById('fAge').value.trim(), hospital: document.getElementById('fHospital').textContent, dept: document.getElementById('fDept').textContent, title: document.getElementById('fTitle').textContent }; } function writeProfile(d){ document.getElementById('fPhone').value = d.phone; document.getElementById('fAge').value = d.age; document.getElementById('fHospital').textContent = d.hospital; document.getElementById('fDept').textContent = d.dept; document.getElementById('fTitle').textContent = d.title; } function openProfile(){ profileSnap = readProfile(); // 进入时记录快照,「取消」可回滚 go('screen-profile'); } function saveProfile(){ const d = readProfile(); if(!/^1[3-9]\d{9}$/.test(d.phone)){ toast('请输入正确的 11 位手机号'); return; } if(!/^\d{1,3}$/.test(d.age) || +d.age < 1 || +d.age > 120){ toast('请输入正确的年龄(1-120)'); return; } if(!d.hospital){ toast('请选择医院名称'); return; } if(!d.dept){ toast('请选择科室'); return; } if(!d.title){ toast('请选择职称'); return; } profileSnap = readProfile(); toast('个人信息已保存'); } function cancelProfile(){ if(profileSnap) writeProfile(profileSnap); toast('已取消修改'); back(); } function leaveProfile(){ // 左上角返回箭头:静默回滚后退出 if(profileSnap) writeProfile(profileSnap); back(); } const PROFILE_DEFAULT = { phone:'13800138000', age:'42', hospital:'北京协和医院', dept:'睡眠医学中心', title:'主任医师' }; function openSheet(kind){ const cfg = SHEET_CFG[kind]; if(!cfg) return; if(typeof cfg.guard === 'function' && cfg.guard() === false) return; // 前置校验(如参数需先进入编辑态) sheetKind = kind; const cur = document.getElementById(cfg.el).textContent; document.getElementById('sheetTitle').textContent = cfg.title; document.getElementById('sheetOpts').innerHTML = cfg.list.map(o => '
' + o + (cfg.suffix || '') + '' + CK_SVG + '
' ).join(''); document.getElementById('sheetMask').classList.add('show'); document.getElementById('sheet').classList.add('show'); } function closeSheet(){ document.getElementById('sheetMask').classList.remove('show'); document.getElementById('sheet').classList.remove('show'); } function applySheet(v){ const cfg = SHEET_CFG[sheetKind]; if(cfg){ if(typeof cfg.pick === 'function'){ cfg.pick(v); // 自定义写入逻辑(如写回参数状态) }else{ document.getElementById(cfg.el).textContent = v; toast(cfg.title.replace('选择', '') + '已选择:' + v); } } closeSheet(); } /* ---------- 数据查看:趋势图分析(范围下拉 + 页内图表) ---------- */ function toggleTrend(e){ if(e && e.stopPropagation) e.stopPropagation(); const p = document.getElementById('trendPanel'); if(!p.hidden){ hideTrend(); return; } // 已展示时再点 = 收起 document.getElementById('trendMenu').classList.toggle('open'); } function pickTrend(el, label){ el.parentElement.querySelectorAll('.trend-opt').forEach(o => o.classList.remove('on')); el.classList.add('on'); document.getElementById('trendMenu').classList.remove('open'); trendRange = label; renderTrendPanel(label); syncView(); toast('已展示「' + label + '」趋势分析'); } function hideTrend(){ trendRange = null; syncView(); } function backToday(){ trendRange = null; applyDay(CAL_TODAY.d); renderCal(); toast('已回到今天'); } /* 点到别处收起下拉 */ document.addEventListener('click', function(e){ const m = document.getElementById('trendMenu'); if(m && m.classList.contains('open') && !m.contains(e.target)) m.classList.remove('open'); }); /* 选中项补勾(复用 CK_SVG) */ document.querySelectorAll('#trendMenu .trend-opt').forEach(function(o){ o.insertAdjacentHTML('beforeend', '' + CK_SVG + ''); }); /* 稳定伪随机(同一图表每次渲染一致) */ function prand(i){ const x = Math.sin(i * 12.9898 + 4.1) * 43758.5453; return x - Math.floor(x); } /* 各时段的说明文案与统计单位 */ const TREND_CFG = { '近 7 天': { meta:'近 7 天 · 按日统计 · 截止今天 · 未佩戴 0 天', unit:'次/日', overUnit:'天' }, '近 30 天': { meta:'近 30 天 · 按日统计 · 截止今天 · 未佩戴 3 天', unit:'次/日', overUnit:'天' }, '近半年': { meta:'近半年 · 按周平均 · 截止今天 · 未佩戴 35 天', unit:'次/周', overUnit:'周' }, '近一年': { meta:'近一年 · 按月平均 · 截止今天 · 未佩戴 62 天', unit:'次/月', overUnit:'个月' }, '全部': { meta:'全部 · 按月平均 · 自 2025/01 · 未佩戴 88 天', unit:'次/月', overUnit:'个月' } }; const TREND = { '近 7 天': { labels:['10/10','10/11','10/12','10/13','10/14','10/15','10/16'], apnea:[4,3,5,2,6,4,3], amp:[45,50,45,40,45,50,50] } }; (function(){ /* 近 30 天:按日 */ const l=[], a=[], m=[]; for(let i=29; i>=0; i--){ const d = new Date(2026, 9, 16 - i); l.push((d.getMonth()+1) + '/' + d.getDate()); a.push(2 + Math.round(prand(i) * 4)); m.push(38 + Math.round(prand(i + 50) * 18)); } TREND['近 30 天'] = { labels:l, apnea:a, amp:m }; /* 近半年:按周平均(26 周,止于 10/16) */ const l2=[], a2=[], m2=[]; for(let i=25; i>=0; i--){ const d = new Date(2026, 9, 16 - i*7); l2.push((d.getMonth()+1) + '/' + d.getDate()); a2.push(+(0.6 + prand(i + 3) * 2.6).toFixed(1)); m2.push(43 + Math.round(prand(i + 9) * 10)); } TREND['近半年'] = { labels:l2, apnea:a2, amp:m2 }; /* 近一年:按月平均 */ const l3=[], a3=[], m3=[]; for(let i=11; i>=0; i--){ const d = new Date(2026, 9 - i, 1); l3.push((d.getMonth()+1) + '月'); a3.push(+(1.6 + prand(i + 13) * 1.8).toFixed(1)); m3.push(42 + Math.round(prand(i + 17) * 12)); } TREND['近一年'] = { labels:l3, apnea:a3, amp:m3 }; /* 全部:按月(2025/1 – 2026/10) */ const l4=[], a4=[], m4=[]; for(let i=0; i<22; i++){ const d = new Date(2025, i, 1); l4.push(String(d.getFullYear()).slice(2) + '/' + (d.getMonth()+1)); a4.push(+(1.4 + prand(i + 7) * 2.2).toFixed(1)); m4.push(40 + Math.round(prand(i + 21) * 12)); } TREND['全部'] = { labels:l4, apnea:a4, amp:m4 }; })(); /* 横轴刻度:点数多时均匀取 6 个 */ function tickIdx(n){ if(n <= 7){ const a=[]; for(let i=0;i 302; return '' + t + ''; } /* 趋势折线图(圆点折线 + 正常范围色带) */ function chartLine2(svg, vals, labels, cfg){ const W=320, H=150, L=30, R=12, T=12, B=26; const X = function(i){ return vals.length<=1 ? L+(W-L-R)/2 : L + (W-L-R)*i/(vals.length-1); }; const Y = function(v){ return T + (H-T-B) * (1-(v-cfg.yLo)/(cfg.yHi-cfg.yLo)); }; let s = ''; /* 正常范围色带 */ if(cfg.band){ s += ''; } /* 刻度与网格 */ for(let v=cfg.yLo; v<=cfg.yHi; v+=cfg.yStep){ const y = Y(v); s += ''; s += ''+v+(cfg.ySuffix||'')+''; } /* 面积 + 折线 */ let dp = ''; vals.forEach(function(v, i){ dp += (i ? 'L' : 'M') + X(i).toFixed(1) + ',' + Y(v).toFixed(1) + ' '; }); if(cfg.areaFill){ s += ''; } s += ''; /* 数据点(可按值变色) */ const r = vals.length > 20 ? 2.4 : 3; vals.forEach(function(v, i){ const c = cfg.dotColor ? cfg.dotColor(v) : cfg.line; s += ''; }); tickIdx(vals.length).forEach(function(i){ s += tickText(X(i), (H-7), labels[i]); }); svg.innerHTML = s; } /* 历史查看横幅:传 text 显示「正在查看 text 的历史数据」,不传则按当前日期决定显隐 */ function updateBanner(text){ const hb = document.getElementById('histBanner'); if(text){ hb.hidden = false; document.getElementById('histBannerTxt').innerHTML = '正在查看 ' + text + ' 的历史数据'; return; } if(viewDay === CAL_TODAY.d){ hb.hidden = true; } else{ hb.hidden = false; document.getElementById('histBannerTxt').innerHTML = '正在查看 10月' + viewDay + '日 的历史数据'; } } /* 唯一的状态同步出口:横幅 / 副标题 / 单日卡片 / 趋势面板 / 按钮态全部由此推导, 保证「正在查看什么」与页面内容永远一致 */ function syncView(){ const inTrend = !!trendRange; const today = viewDay === CAL_TODAY.d; const dd = DAY_DATA[viewDay] || DAY_DATA[CAL_TODAY.d]; document.getElementById('trendPanel').hidden = !inTrend; document.getElementById('dayCards').style.display = inTrend ? 'none' : ''; document.querySelector('.trend-btn').classList.toggle('on', inTrend); const sub = document.getElementById('sdSub'); if(inTrend){ sub.textContent = '趋势分析 · ' + trendRange + ' · 截止今天'; updateBanner(trendRange); }else{ sub.textContent = '10月' + viewDay + '日 ' + (dd.week || '') + ' · ' + (today ? '实时监测中' : '历史数据'); updateBanner(); } } function renderTrendPanel(range){ const d = TREND[range], cfg = TREND_CFG[range]; if(!d || !cfg) return; const sum = function(a){ return a.reduce(function(x,y){ return x+y; }, 0); }; const ap = d.apnea, am = d.amp; const avg = sum(ap)/ap.length, mx = Math.max.apply(null, ap); const over = ap.filter(function(v){ return v > 2; }).length; document.getElementById('tpMeta').textContent = cfg.meta; document.getElementById('stAvg').textContent = avg.toFixed(1); document.getElementById('stUnit').textContent = cfg.unit; document.getElementById('stMax').textContent = mx.toFixed(1); document.getElementById('stOverN').textContent = over; document.getElementById('stOverU').textContent = cfg.overUnit; document.getElementById('amAvg').textContent = Math.round(sum(am)/am.length); document.getElementById('amMax').textContent = Math.max.apply(null, am); document.getElementById('amMin').textContent = Math.min.apply(null, am); chartLine2(document.getElementById('trApneaSvg'), ap, d.labels, { line:'#F79009', band:[0,2], bandFill:'rgba(18,183,106,.14)', dotColor:function(v){ return v > 2 ? '#E5484D' : '#F79009'; }, yLo:0, yHi:Math.max(4, Math.ceil((mx+1)/2)*2), yStep:2 }); const mn = Math.min.apply(null, am), mxx = Math.max.apply(null, am); chartLine2(document.getElementById('trAmpSvg'), am, d.labels, { line:'#7C5CFF', band:[40,50], bandFill:'rgba(124,92,255,.10)', areaFill:'rgba(124,92,255,.10)', yLo:Math.floor((mn-4)/10)*10, yHi:Math.ceil((mxx+4)/10)*10, yStep:10 }); } /* ---------- 数据查看:日历(页内展开)+ 日期数据联动 ---------- */ const CAL_TODAY = { y:2026, m:10, d:16 }; const CAL_DATA = { '2026-10':[5,6,7,8,16] }; // 有监测数据的日期 let calY = CAL_TODAY.y, calM = CAL_TODAY.m; let viewDay = CAL_TODAY.d; // 当前展示的日期 let trendRange = null; // 当前趋势范围(null = 非趋势模式) /* 各日期监测数据(与历史数据列表一致) */ const DAY_DATA = { 5: { hr:18, apnea:6, amp:40, diff: 4, week:'周日', times:[['23:50',10],['00:42',14],['01:30',9]] }, 6: { hr:17, apnea:2, amp:45, diff:-2, week:'周一', times:[['02:10',7],['04:35',13]] }, 7: { hr:15, apnea:4, amp:50, diff: 1, week:'周二', times:[['00:48',15],['01:55',19],['03:20',11]] }, 8: { hr:16, apnea:3, amp:50, diff:-1, week:'周三', times:[['01:12',12],['02:35',8],['04:08',21]] }, 16:{ hr:16, apnea:3, amp:50, diff:-1, week:'周五', times:[['01:12',12],['02:35',8],['04:08',21]] } }; function renderCal(){ document.getElementById('calTitle').textContent = calY + ' 年 ' + calM + ' 月'; const first = new Date(calY, calM - 1, 1).getDay(); // 当月 1 号是周几 const days = new Date(calY, calM, 0).getDate(); // 当月天数 const data = CAL_DATA[calY + '-' + calM] || []; const isCur = calY === CAL_TODAY.y && calM === CAL_TODAY.m; let html = ''; for(let i = 0; i < first; i++) html += ''; for(let d = 1; d <= days; d++){ const has = data.indexOf(d) > -1; const cls = 'cal-d' + (has ? '' : ' off') + (isCur && d === viewDay ? ' sel' : ''); html += '' + d + ''; } document.getElementById('calGrid').innerHTML = html; } /* 点日历图标 → 底部弹出 / 收起 */ function openCal(){ document.getElementById('trendMenu').classList.remove('open'); const p = document.getElementById('calPanel'); if(p.classList.contains('show')){ hideCal(); return; } calY = CAL_TODAY.y; calM = CAL_TODAY.m; renderCal(); document.getElementById('calMask').classList.add('show'); p.classList.add('show'); } function hideCal(){ document.getElementById('calMask').classList.remove('show'); document.getElementById('calPanel').classList.remove('show'); } function calNav(dir){ calM += dir; if(calM < 1){ calM = 12; calY--; } if(calM > 12){ calM = 1; calY++; } renderCal(); } function calToday(){ calY = CAL_TODAY.y; calM = CAL_TODAY.m; trendRange = null; // 选具体日期 → 退出趋势模式 applyDay(CAL_TODAY.d); renderCal(); hideCal(); // 选完日期即收起 } function calPick(d, has){ if(!has){ toast('10月' + d + '日未使用设备,暂无监测数据'); return; } trendRange = null; // 选具体日期 → 退出趋势模式 applyDay(d); renderCal(); hideCal(); // 选完日期即收起 } /* 把某天的数据写回当前页面的卡片 */ function applyDay(d){ const dd = DAY_DATA[d]; if(!dd) return; viewDay = d; const today = (d === CAL_TODAY.d); const hr = dd.hr; document.getElementById('sdHr').textContent = hr; const hb = document.getElementById('sdHrBadge'); hb.textContent = hr < 12 ? '偏慢' : (hr > 20 ? '偏快' : '正常'); hb.className = 'badge ' + (hr < 12 || hr > 20 ? 'a' : 'g'); document.getElementById('sdHrDot').style.left = Math.max(4, Math.min(96, (hr - 8) / 20 * 100)) + '%'; document.getElementById('sdApnea').textContent = dd.apnea; const ab = document.getElementById('sdApneaBadge'); ab.textContent = dd.diff === 0 ? '与前一晚持平' : ('较昨日 ' + (dd.diff > 0 ? '+' : '') + dd.diff + ' 次'); ab.className = 'badge ' + (dd.diff > 0 ? 'a' : 'p'); let dots = ''; for(let i = 0; i < 6; i++) dots += ''; document.getElementById('sdDots').innerHTML = dots; const durs = dd.times.map(function(t){ return t[1]; }); const mx = Math.max.apply(null, durs); document.getElementById('sdHint').textContent = mx >= 25 ? ('存在 ' + mx + ' 秒暂停,已触发干预') : '单次暂停均短于 25 秒,未触发干预'; document.getElementById('sdTimeline').innerHTML = dd.times.map(function(t){ return '
' + t[0] + '持续 ' + t[1] + ' 秒
'; }).join(''); document.getElementById('sdAmpK').textContent = today ? '当前使用刺激幅度' : '当日使用刺激幅度'; document.getElementById('sdAmpHint').textContent = today ? '与「主界面」治疗幅度实时同步' : '该次治疗过程中实际使用的幅度'; /* 今天跟随主界面实时幅度,历史日期用当日记录值 */ document.getElementById('dataAmp').textContent = today ? Number(S.amp) : dd.amp; syncView(); if(!today) toast('已切换到 10月' + d + '日监测数据'); } /* 复位数据查看页的展开面板与日期 */ function resetDataPanels(){ hideCal(); trendRange = null; // 收起趋势面板并恢复单日卡片 document.getElementById('trendMenu').classList.remove('open'); applyDay(CAL_TODAY.d); } /* ---------- 刺激模式(主界面) ---------- */ const MODES = { fixed: { name:'固定时间刺激', desc:'固定时间刺激:按设定间隔定时输出刺激,节奏稳定、可预期' }, breath: { name:'跟随呼吸刺激', desc:'跟随呼吸刺激:与呼吸节律实时同步输出刺激,体感更平顺、干扰更小' }, apnea: { name:'呼吸暂停刺激', desc:'呼吸暂停刺激:检测到呼吸暂停时触发刺激,针对性更强' } }; function pickMode(btn, key){ const m = MODES[key]; if(!m) return; S.mode = key; document.querySelectorAll('#screen-home .mode').forEach(b => b.classList.toggle('on', b === btn)); document.getElementById('modeDesc').textContent = m.desc; toast('刺激模式已切换为「' + m.name + '」'); } function syncModeUI(){ document.querySelectorAll('#screen-home .mode').forEach(b => b.classList.toggle('on', b.dataset.mode === S.mode)); const m = MODES[S.mode]; if(m) document.getElementById('modeDesc').textContent = m.desc; } /* ---------- 参数设置 · 设备刺激参数 ---------- */ const ICO_EDIT = ''; const ICO_CHECK = ''; const HINT_VIEW = '查看状态下参数不可修改,点击上方「编辑」可调整'; const HINT_EDIT = '治疗幅度可左右拖动滑杆调整,其余参数点 ‹ › 步进;完成后点「确认」保存'; let paramEditing = false, paramSnap = ''; const PARAMS = { pw: { title:'脉冲时间', suffix:' μs', el:'pPw', list:['50','100','150','200','250','300','400'] }, hz: { title:'脉冲频率', suffix:' Hz', el:'pHz', list:['5','10','15','20','25','30','40','50'] }, amp: { title:'治疗幅度', suffix:'%', el:'pAmp', list:['10','20','30','40','50','60','70','80','90','100'] }, stim:{ title:'刺激时间', suffix:' s', el:'pStim', list:['1','2','3','5','8','10'] }, gap: { title:'刺激间隔', suffix:' s', el:'pGap', list:['2','3','5','8','10','15'] }, /* 下拉选择项:0–60 min,步进 5(档位表见 DELAY_LIST) */ delay:{ title:'开机延迟治疗时间', suffix:' min', el:'pDelay', list:DELAY_LIST } }; /* 设备侧实际参数:「参数更新」写入、「参数下载」读回 */ let deviceP = { ...PARAM_DEFAULT }; function syncParamUI(){ Object.keys(PARAMS).forEach(k => { const el = document.getElementById(PARAMS[k].el); if(el) el.textContent = S.p[k]; }); syncAmpSlider(); syncStepDim(); } /* 治疗幅度编辑态滑杆的填充与手柄位置 */ function syncAmpSlider(){ const v = Math.max(0, Math.min(100, Number(S.p.amp) || 0)); const fill = document.getElementById('pAmpFill'); const hd = document.getElementById('pAmpHandle'); if(fill) fill.style.width = v + '%'; if(hd) hd.style.left = v + '%'; } /* 到达档位两端时淡化对应箭头 */ function syncStepDim(){ Object.keys(PARAMS).forEach(k => { const cfg = PARAMS[k]; const row = document.querySelector('#paramCard .li[data-kind="' + k + '"]'); if(!row) return; const i = cfg.list.indexOf(String(S.p[k])); const btns = row.querySelectorAll('.st-btn'); if(btns[0]) btns[0].classList.toggle('dim', i <= 0); if(btns[1]) btns[1].classList.toggle('dim', i >= cfg.list.length - 1); }); } /* 按查看 / 编辑状态刷新页面 UI */ function renderParamMode(){ const sec = document.getElementById('paramCard'); if(sec) sec.classList.toggle('editing', paramEditing); const txt = document.getElementById('setEditTxt'); if(txt) txt.textContent = paramEditing ? '确认' : '编辑'; const ico = document.getElementById('setEditIco'); if(ico) ico.innerHTML = paramEditing ? ICO_CHECK : ICO_EDIT; const eb = document.getElementById('setEditBtn'); if(eb) eb.classList.toggle('on', paramEditing); const ub = document.getElementById('btnUp'); if(ub) ub.classList.toggle('off', !paramEditing); const hint = document.getElementById('paramHint'); if(hint) hint.textContent = paramEditing ? HINT_EDIT : HINT_VIEW; syncStepDim(); } /* 右上角 编辑 ⇄ 确认 */ function toggleParamEdit(){ paramEditing = !paramEditing; renderParamMode(); if(paramEditing){ paramSnap = JSON.stringify(S.p); toast('已进入编辑状态,点击箭头调整参数'); }else{ toast(JSON.stringify(S.p) === paramSnap ? '参数未修改' : '参数已确认'); } } /* 查看状态点击参数行:提示先进入编辑 */ function tapParamRow(){ if(!paramEditing) toast('请先点击右上角「编辑」解锁参数'); } /* 写入单个参数值 */ function setParam(kind, v){ const cfg = PARAMS[kind]; if(!cfg) return; S.p[kind] = String(v); if(kind === 'amp'){ setAmp(Number(v)); // 与主界面 / 数据页联动(内部取整并夹在 0–100) S.p.amp = String(S.amp); } const el = document.getElementById(cfg.el); if(el) el.textContent = S.p[kind]; syncAmpSlider(); syncStepDim(); } /* 左右箭头步进调整 */ function stepParam(e, kind, dir){ if(e && e.stopPropagation) e.stopPropagation(); if(!paramEditing){ toast('请先点击右上角「编辑」解锁参数'); return; } const cfg = PARAMS[kind]; if(!cfg) return; const list = cfg.list; let i = list.indexOf(String(S.p[kind])); if(i < 0){ // 当前值不在档位表内,就近对齐 let best = 0, bd = Infinity; list.forEach((v, k) => { const d = Math.abs(Number(v) - Number(S.p[kind])); if(d < bd){ bd = d; best = k; } }); i = best; } const j = Math.max(0, Math.min(list.length - 1, i + dir)); if(j === i){ toast(cfg.title + (dir < 0 ? ' 已是最小值' : ' 已是最大值')); return; } setParam(kind, list[j]); toast(cfg.title + '已设为 ' + list[j] + cfg.suffix + (j === 0 ? '(最小值)' : j === list.length - 1 ? '(最大值)' : '')); } /* 治疗幅度 · 编辑态左右拖动滑杆 */ const pAmpTrack = document.getElementById('pAmpTrack'); let pAmpDragging = false; function pAmpFromEvent(e){ const r = pAmpTrack.getBoundingClientRect(); if(!r.width) return; const x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left; setParam('amp', Math.round(x / r.width * 100)); } if(pAmpTrack){ pAmpTrack.addEventListener('pointerdown', e => { if(!paramEditing){ toast('请先点击右上角「编辑」解锁参数'); return; } e.preventDefault(); e.stopPropagation(); pAmpDragging = true; try{ pAmpTrack.setPointerCapture(e.pointerId); }catch(_){} pAmpFromEvent(e); }); pAmpTrack.addEventListener('pointermove', e => { if(pAmpDragging) pAmpFromEvent(e); }); pAmpTrack.addEventListener('pointerup', () => { if(!pAmpDragging) return; pAmpDragging = false; toast('治疗幅度已调整为 ' + S.p.amp + '%'); }); pAmpTrack.addEventListener('pointercancel', () => { pAmpDragging = false; }); } /* 参数下载:从设备读回当前参数 */ function downloadParams(){ const b = document.getElementById('btnDl'); if(!b || b.dataset.busy) return; b.dataset.busy = '1'; b.textContent = '下载中…'; setTimeout(() => { b.dataset.busy = ''; b.textContent = '参数下载'; S.p = { ...deviceP }; if(paramEditing) paramSnap = JSON.stringify(S.p); syncParamUI(); setAmp(Number(S.p.amp)); toast('已从设备下载最新参数'); }, 700); } /* 参数更新:把当前参数下发到设备 */ function updateParams(){ if(!paramEditing){ toast('查看状态下不可下发,请先点击右上角「编辑」'); return; } const b = document.getElementById('btnUp'); if(!b || b.dataset.busy) return; b.dataset.busy = '1'; b.textContent = '更新中…'; setTimeout(() => { b.dataset.busy = ''; b.textContent = '参数更新'; deviceP = { ...S.p }; toast('参数已更新到设备'); }, 800); } /* 恢复默认参数 */ function resetParams(silent){ S.p = { ...PARAM_DEFAULT }; deviceP = { ...PARAM_DEFAULT }; paramEditing = false; paramSnap = JSON.stringify(S.p); renderParamMode(); syncParamUI(); setAmp(Number(PARAM_DEFAULT.amp)); if(!silent) toast('参数已恢复为默认值'); } /* ---------- 软件更新 ---------- */ function checkUpdate(){ const v = document.getElementById('appVer'); v.textContent = '正在检查…'; v.style.color = 'var(--ink3)'; setTimeout(() => { v.textContent = 'v3.2.0 已是最新'; v.style.color = ''; toast('当前已是最新版本 v3.2.0'); }, 900); } /* ---------- 语言选择 ---------- */ function pickLang(el, name){ document.querySelectorAll('#langList .ck').forEach(c => c.classList.remove('on')); const ck = el.querySelector('.ck'); if(ck) ck.classList.add('on'); document.getElementById('langVal').textContent = name; toast('语言已切换为 ' + name); } function resetAll(){ S.wear = 'warn'; S.fit = 'ok'; S.ble = true; S.alarm = true; S.loggedIn = false; loginNext = null; respRate = 16; setAmp(50, true); setTherapyUI(false); S.mode = 'breath'; syncModeUI(); resetParams(true); resetBind(false); simBindState(false, true); // 默认:尚未添加设备(主界面空态) resetAudit(); writeProfile(PROFILE_DEFAULT); profileSnap = readProfile(); closeSheet(); resetDataPanels(); clearInterval(codeTimer); const cb = document.getElementById('codeBtn'); cb.disabled = false; cb.textContent = '获取验证码'; document.getElementById('inPhone').value = ''; document.getElementById('inCode').value = ''; document.getElementById('cbAgree').classList.remove('on'); syncLoginOpts(); renderLogin(); document.querySelectorAll('.opt').forEach(o => { if(o.dataset.fit) o.classList.toggle('on', o.dataset.fit === 'ok'); if(o.dataset.wear) o.classList.toggle('on', o.dataset.wear === 'warn'); if(o.dataset.ble) o.classList.toggle('on', o.dataset.ble === '1'); if(o.dataset.alarm) o.classList.toggle('on', o.dataset.alarm === '1'); }); renderStatus(); go('screen-home','tab-home'); toast('已复位到初始演示状态'); } /* ---------- 模拟器按钮 ---------- */ document.querySelectorAll('.opt[data-fit]').forEach(o => { o.addEventListener('click', () => { document.querySelectorAll('.opt[data-fit]').forEach(x => x.classList.remove('on')); o.classList.add('on'); S.fit = o.dataset.fit; renderStatus(); toast(FIT[S.fit].toast); }); }); document.querySelectorAll('.opt[data-wear]').forEach(o => { o.addEventListener('click', () => { document.querySelectorAll('.opt[data-wear]').forEach(x => x.classList.remove('on')); o.classList.add('on'); S.wear = o.dataset.wear; renderStatus(); const hint = { normal:'设备电量:充足 86%', warn:'设备电量:偏低 20%,请及时充电', alarm:'设备电量:即将关机 5%,请立即充电' }; toast(hint[S.wear]); }); }); document.querySelectorAll('.opt[data-ble]').forEach(o => { o.addEventListener('click', () => { document.querySelectorAll('.opt[data-ble]').forEach(x => x.classList.remove('on')); o.classList.add('on'); S.ble = o.dataset.ble === '1'; renderStatus(); toast(S.ble ? '蓝牙已连接' : '蓝牙已断开'); }); }); document.querySelectorAll('.opt[data-alarm]').forEach(o => { o.addEventListener('click', () => { S.alarm = o.dataset.alarm === '1'; syncAlarmOpts(); renderStatus(); toast(S.alarm ? '报警提醒已开启' : '报警提醒已关闭'); }); }); document.querySelectorAll('.opt[data-login]').forEach(o => { o.addEventListener('click', () => { S.loggedIn = o.dataset.login === '1'; syncLoginOpts(); renderLogin(); go('screen-mine','tab-mine'); toast(S.loggedIn ? '已切换到「已登录」状态' : '已切换到「未登录」状态'); }); }); document.querySelectorAll('.opt[data-bind]').forEach(o => { o.addEventListener('click', () => { document.querySelectorAll('.opt[data-bind]').forEach(x => x.classList.remove('on')); o.classList.add('on'); simBindState(o.dataset.bind === '1'); }); }); /* 模拟「已绑定 / 未绑定」两态(silent=true 时不弹 toast,用于初始化与复位) */ function simBindState(bound, silent){ const filled = document.getElementById('bindFilled'); const empty = document.getElementById('bindEmpty'); if(bound){ // 演示态:列表为空时恢复默认的两台设备 if(!filled.querySelector('.bind-dev-row')) filled.innerHTML = BIND_FILLED_TPL; filled.style.display = ''; empty.style.display = 'none'; promoteMainDevice(); } else { filled.innerHTML = ''; // 真正「未添加设备」:清空列表 filled.style.display = 'none'; empty.style.display = ''; document.querySelectorAll('#bindResults .bind-btn[data-name]').forEach(b => { b.textContent = '绑定'; b.classList.remove('done'); b.disabled = false; }); } renderBindStatus(); syncBindOpts(); if(!silent) toast(bound ? '设备绑定状态:已绑定设备' : '设备绑定状态:未绑定设备'); } function syncBindOpts(){ const bound = isBound(); document.querySelectorAll('.opt[data-bind]').forEach(x => { x.classList.toggle('on', (x.dataset.bind === '1') === bound); }); } /* ---------- 时钟 ---------- */ function tick(){ const d = new Date(); document.getElementById('clock').textContent = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0'); } tick(); setInterval(tick, 20000); /* ---------- 初始化 ---------- */ setAmp(50, true); syncParamUI(); renderParamMode(); setTherapyUI(false); renderReport(REPORT_DEFAULT); renderStatus(); syncBindOpts(); // 历史数据默认按「近 7 天」筛选 const segFirst = document.querySelector('#screen-history .seg button'); if(segFirst) chipTo(segFirst, 'd7', true); renderLogin(); syncLoginOpts(); resetAudit(); simBindState(false, true); // 初始状态:尚未添加设备 renderHomeDate(); // 顶部日期按当天时间渲染 go('screen-home','tab-home'); // 顶部日期每分钟校准一次(跨零点自动切到新日期) setInterval(renderHomeDate, 60000); // 实时呼吸数据:每 2 秒刷新一次呼吸频率(未连接时不更新) setInterval(respTick, 2000); // 支持 #screen-xxx 深链定位(演示 / 分享时可直接跳到某一页) const __hash = (location.hash || '').replace('#', ''); if(__hash.indexOf('screen-') === 0 && document.getElementById(__hash)) go(__hash);
取消
访问码: n5kdgnsd