From 7333189799610e313ab7784109733e9d96d20c84 Mon Sep 17 00:00:00 2001 From: zhangyan Date: Mon, 31 Aug 2026 08:34:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A4=90=E7=BA=BF=E8=90=A5=E5=85=BB?= =?UTF-8?q?=E7=A7=A4=E6=A6=82=E8=A7=88=E7=AD=9B=E9=80=89=E6=A0=8F=E4=B8=8B?= =?UTF-8?q?=E6=B2=89=20+=20=E9=87=8D=E9=87=8F=E5=8F=98=E5=8C=96=E6=8A=98?= =?UTF-8?q?=E7=BA=BF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- web-admin/nutrition/din-terminal.html | 223 ++++++++++++++++++++++++-- web-admin/nutrition/doc/CHANGELOG.md | 7 + 2 files changed, 215 insertions(+), 15 deletions(-) diff --git a/web-admin/nutrition/din-terminal.html b/web-admin/nutrition/din-terminal.html index c13e55d..8b62db8 100644 --- a/web-admin/nutrition/din-terminal.html +++ b/web-admin/nutrition/din-terminal.html @@ -333,6 +333,33 @@ tbody tr:hover{background:#F8FBFF;} .scale-sub-tab{padding:8px 16px;font-size:var(--font-sm);color:var(--text-secondary);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;user-select:none;transition:all .15s;} .scale-sub-tab:hover{color:var(--primary);} .scale-sub-tab.active{color:var(--primary);font-weight:600;border-bottom-color:var(--primary);} +/* ===== 餐线营养秤:标题下方筛选栏 ===== */ +.scale-filter-bar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:0 0 14px;padding:10px 14px;background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-sm);} +.scale-filter-bar__item{display:flex;align-items:center;gap:8px;} +.scale-filter-bar__item label{font-size:var(--font-xs);color:var(--text-secondary);white-space:nowrap;} +.scale-filter-bar__date{display:flex;align-items:center;gap:6px;} +.scale-filter-bar__date .sep{color:var(--text-placeholder);font-size:var(--font-sm);} +.scale-filter-bar__input{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;} +.scale-filter-bar__input:focus{border-color:var(--primary);} +.scale-filter-bar__select{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 26px 0 10px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center;appearance:none;cursor:pointer;min-width:120px;} +.scale-filter-bar__actions{display:flex;align-items:center;gap:8px;margin-left:auto;} +.scale-filter-bar .btn{height:30px;padding:0 14px;font-size:var(--font-xs);} +/* 表格/折线图切换 */ +.scale-view-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;user-select:none;} +.scale-view-toggle__btn{height:30px;line-height:30px;padding:0 14px;font-size:var(--font-sm);color:var(--text-secondary);cursor:pointer;background:#fff;border:none;transition:all .15s;} +.scale-view-toggle__btn+.scale-view-toggle__btn{border-left:1px solid var(--border);} +.scale-view-toggle__btn.active{background:var(--primary);color:#fff;} +/* 重量变化折线图 */ +.scale-chart-legend{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:10px;} +.scale-chart-legend__item{display:flex;align-items:center;gap:6px;font-size:var(--font-xs);color:var(--text-secondary);} +.scale-chart-legend__dot{width:10px;height:10px;border-radius:50%;} +.scale-chart-svg{width:100%;height:auto;display:block;} +.scale-chart-dot{cursor:pointer;} +.scale-chart-tooltip{position:fixed;z-index:1800;display:none;min-width:200px;max-width:280px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 6px 20px rgba(0,0,0,.15);padding:10px 12px;pointer-events:none;} +.scale-chart-tooltip__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;} +.scale-chart-tooltip__row{display:flex;justify-content:space-between;gap:12px;font-size:var(--font-xs);color:var(--text-secondary);line-height:1.9;} +.scale-chart-tooltip__row span{color:var(--text-placeholder);white-space:nowrap;} +.scale-chart-tooltip__row strong{color:var(--text-primary);font-weight:500;text-align:right;word-break:break-all;} /* ===== 人脸识别视频回放弹窗 ===== */ @keyframes ovScan{0%{top:10%}50%{top:88%}100%{top:10%}} .face-video-mask{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1600;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;} @@ -794,6 +821,14 @@ function renderOvTabBody(key){ /* ===== 表头列筛选(餐线营养秤四个 Tab 复用) ===== */ var SCALE_FILTERS={status:{},face:{},weight:{},log:{}}; +/* 日期范围筛选(标题下方筛选栏,status/face/weight 三个 Tab 使用) */ +var SCALE_DATE_RANGE={status:{from:'',to:''},face:{from:'',to:''},weight:{from:'',to:''}}; +/* 各 Tab 用于日期范围筛选的时间字段 */ +var SCALE_TIME_FIELD={status:'time',face:'start',weight:'start'}; +/* 重量变化数据展示样式:table 表格 / chart 折线图 */ +var SCALE_WEIGHT_VIEW='table'; +/* 识别点状态配色(折线图图例与数据点) */ +var SCALE_WEIGHT_STATES={'刷脸':'#1890FF','离开':'#722ED1','陌生人':'#FAAD14','重量稳定变化':'#52C41A'}; /* 每个 Tab 的列定义:key 列标识 / label 表头文字 / field 数据字段 */ var SCALE_COLS={ status:[ @@ -835,6 +870,12 @@ function scaleThead(tabKey,actionLabel){ if(actionLabel)ths+=''+actionLabel+''; return ''+ths+''; } +/* 生成纯文字表头(无筛选图标,用于 status/face/weight 三个 Tab) */ +function scalePlainThead(tabKey,actionLabel){ + var ths=SCALE_COLS[tabKey].map(function(c){return ''+c.label+'';}).join(''); + if(actionLabel)ths+=''+actionLabel+''; + return ''+ths+''; +} /* 取某 Tab 原始数据 */ function getScaleRows(tabKey){ if(tabKey==='status')return SCALE_STATUS_EVENTS||[]; @@ -848,6 +889,8 @@ function getFilteredScaleRows(tabKey){ var rows=getScaleRows(tabKey); var f=SCALE_FILTERS[tabKey]||{}; var cols=SCALE_COLS[tabKey]||[]; + var dr=SCALE_DATE_RANGE[tabKey]||{from:'',to:''}; + var tf=SCALE_TIME_FIELD[tabKey]; return rows.filter(function(r){ for(var i=0;idr.to)return false; + } return true; }); } @@ -972,6 +1021,150 @@ function renderScaleSubBody(key){ return ''; } +/* ===== 标题下方筛选栏(status/face/weight) ===== */ +function renderScaleFilterBar(tabKey){ + var dr=SCALE_DATE_RANGE[tabKey]||{from:'',to:''}; + var dateHtml='
' + +'
' + +'' + +'' + +'' + +'
'; + var extraHtml=''; + if(tabKey==='status'){ + var cur=(SCALE_FILTERS.status&&SCALE_FILTERS.status.item)||''; + var opts=''; + getScaleColValues('status','item').forEach(function(v){ + opts+=''; + }); + extraHtml='
'; + } + var toggleHtml=''; + if(tabKey==='weight'){ + toggleHtml='
' + +'表格' + +'折线图' + +'
'; + } + return '
'+dateHtml+extraHtml + +'
' + +'' + +'' + +toggleHtml + +'
'; +} +function applyScaleFilter(tabKey,reset){ + if(!SCALE_DATE_RANGE[tabKey])SCALE_DATE_RANGE[tabKey]={from:'',to:''}; + if(reset){ + SCALE_DATE_RANGE[tabKey].from=''; + SCALE_DATE_RANGE[tabKey].to=''; + if(tabKey==='status'){ + if(!SCALE_FILTERS.status)SCALE_FILTERS.status={}; + SCALE_FILTERS.status.item=''; + } + }else{ + var f=document.getElementById('scaleDateFrom_'+tabKey); + var t=document.getElementById('scaleDateTo_'+tabKey); + SCALE_DATE_RANGE[tabKey].from=f?f.value:''; + SCALE_DATE_RANGE[tabKey].to=t?t.value:''; + if(tabKey==='status'){ + if(!SCALE_FILTERS.status)SCALE_FILTERS.status={}; + var sel=document.getElementById('scaleItemFilter'); + SCALE_FILTERS.status.item=sel?sel.value:''; + } + } + var body=document.getElementById('scaleSubBody'); + if(body)body.innerHTML=renderScaleSubBody(tabKey); +} +function switchWeightView(view){ + SCALE_WEIGHT_VIEW=view; + var body=document.getElementById('scaleSubBody'); + if(body)body.innerHTML=renderScaleSubBody('weight'); +} + +/* ===== 重量变化折线图 ===== */ +function renderScaleWeightChart(){ + var arr=getFilteredScaleRows('weight').slice().reverse(); + var legend=Object.keys(SCALE_WEIGHT_STATES).map(function(s){ + return '
'+s+'
'; + }).join(''); + if(!arr.length){ + return '
'+legend+'
当前时间范围内无重量变化数据
'; + } + return '
'+legend+'
'+buildWeightChartSvg(arr); +} +function buildWeightChartSvg(arr){ + var w=760,h=260,pl=56,pr=16,pt=20,pb=40; + var cw=w-pl-pr,ch=h-pt-pb; + var weights=arr.map(function(r){return parseFloat(r.total)||0;}); + var min=Math.min.apply(null,weights),max=Math.max.apply(null,weights); + if(max-min<1){var pad=Math.max(1,max*0.1);max+=pad;min-=pad;} + var range=max-min||1; + var step=arr.length>1?cw/(arr.length-1):0; + var pts=arr.map(function(r,i){ + var x=pl+i*step; + var y=pt+ch-((parseFloat(r.total)||0)-min)/range*ch; + return {x:x,y:y,r:r}; + }); + var grid=''; + for(var g=0;g<=4;g++){ + var y=pt+ch-g/4*ch; + var val=min+range*g/4; + grid+='' + +''+val.toFixed(1)+''; + } + var xLabels=arr.map(function(r,i){ + return ''+formatChartTime(r.start)+''; + }).join(''); + var line=pts.map(function(p){return p.x+','+p.y;}).join(' '); + var polyline=''; + var dots=pts.map(function(p){ + var color=SCALE_WEIGHT_STATES[p.r.state]||'#999'; + return ''; + }).join(''); + return ''+grid+xLabels+polyline+dots+''; +} +function formatChartTime(ts){ + var s=String(ts); + var t=s.indexOf(' ')!==-1?s.split(' ')[1]:s; + return t.slice(0,8); +} +function showWeightPointTip(e,start){ + var rec=SCALE_WEIGHT_RECORDS.filter(function(r){return r.start===start;})[0]; + if(!rec)return; + var num=parseFloat(rec.delta); + var dir=num>0?'增':(num<0?'减':'平'); + var tip=document.getElementById('scaleChartTip'); + if(!tip){ + tip=document.createElement('div'); + tip.id='scaleChartTip'; + tip.className='scale-chart-tooltip'; + document.body.appendChild(tip); + } + tip.innerHTML='
识别点详情
' + +'
方向'+dir+'
' + +'
变化量'+escapeHtml(rec.delta)+'克
' + +'
起始→结束'+escapeHtml(rec.startWeight)+'→'+escapeHtml(rec.total)+'克
' + +'
归属用户'+escapeHtml(rec.userId||rec.target)+'
' + +'
起始时间'+escapeHtml(rec.start)+'
' + +'
结束时间'+escapeHtml(rec.end)+'
'; + tip.style.display='block'; + positionWeightPointTip(e,tip); +} +function positionWeightPointTip(e,tip){ + var x=e.clientX,y=e.clientY; + var w=tip.offsetWidth,h=tip.offsetHeight; + var left=x+14,top=y+14; + if(left+w>window.innerWidth)left=x-w-14; + if(top+h>window.innerHeight)top=y-h-14; + tip.style.left=left+'px'; + tip.style.top=top+'px'; +} +function hideWeightPointTip(){ + var tip=document.getElementById('scaleChartTip'); + if(tip)tip.style.display='none'; +} + /* 维度1:设备状态监控 — 状态事件流水 */ SCALE_STATUS_EVENTS=[ {time:'2026-08-25 08:15:32.456',item:'网络连接',event:'重新连接成功'}, @@ -986,7 +1179,8 @@ SCALE_STATUS_EVENTS=[ function renderScaleStatus(){ const rows=getFilteredScaleRows('status').map(s=>`${s.time}${s.item}${s.event}`).join(''); return `
设备状态监控
-
${scaleThead('status')}${rows}
`; + ${renderScaleFilterBar('status')} +
${scalePlainThead('status')}${rows}
`; } /* 维度2:人脸识别数据 — 开始/结束时间 / 状态 + 视频回放 */ @@ -1008,7 +1202,8 @@ function renderScaleFace(){ return `${f.start}${f.end}${statusTag}回放视频`; }).join(''); return `
人脸识别数据
-
${scaleThead('face','操作')}${rows}
`; + ${renderScaleFilterBar('face')} +
${scalePlainThead('face','操作')}${rows}
`; } /* 录像回放弹窗:自动播放 + 快进/后退 + 进度条跳转 */ @@ -1042,12 +1237,6 @@ function showFaceVideo(start){
00:00 / 00:30
-
-
识别时间${f.start}
-
状态${f.status}
-
用户类型${f.userType}
-
重量变化${f.weight}
-
`; mask.addEventListener('click',e=>{if(e.target===mask)closeFaceVideo();}); @@ -1113,21 +1302,25 @@ function formatFaceVideoTime(t){ return (m<10?'0'+m:m)+':'+(s<10?'0'+s:s); } -/* 维度3:重量变化数据 — 日期/时间 / 用户 / 较上次变化 / 当前余量 */ +/* 维度3:重量变化数据 — 日期/时间 / 用户 / 较上次变化 / 当前余量(含折线图所需字段) */ SCALE_WEIGHT_RECORDS=[ - {start:'2026-08-25 08:15:22.701',target:'EMP0234 · 张伟',delta:'+50.0',total:'486.5'}, - {start:'2026-08-25 08:14:50.512',target:'无用户',delta:'+412.0',total:'462.0'}, - {start:'2026-08-25 08:13:46.098',target:'EMP0357 · 王强',delta:'-62.0',total:'501.2'}, - {start:'2026-08-25 08:12:08.876',target:'无用户',delta:'0.0',total:'0.0'} + {start:'2026-08-31 08:15:22.701',end:'2026-08-31 08:15:31.208',target:'EMP0234 · 张伟',userId:'2090646647302909953',delta:'+50.0',total:'486.5',startWeight:'436.5',state:'刷脸'}, + {start:'2026-08-31 08:14:50.512',end:'2026-08-31 08:15:02.118',target:'无用户',userId:'—',delta:'+412.0',total:'462.0',startWeight:'50.0',state:'陌生人'}, + {start:'2026-08-31 08:13:46.098',end:'2026-08-31 08:14:10.540',target:'EMP0357 · 王强',userId:'2090646647302909954',delta:'-62.0',total:'501.2',startWeight:'563.2',state:'离开'}, + {start:'2026-08-31 08:12:08.876',end:'2026-08-31 08:12:30.775',target:'无用户',userId:'—',delta:'0.0',total:'0.0',startWeight:'0.0',state:'重量稳定变化'} ]; function renderScaleWeight(){ + return `
重量变化数据
+ ${renderScaleFilterBar('weight')} + ${SCALE_WEIGHT_VIEW==='chart'?renderScaleWeightChart():renderScaleWeightTable()}`; +} +function renderScaleWeightTable(){ const rows=getFilteredScaleRows('weight').map(w=>{ const num=parseFloat(w.delta); const color=num>0?'var(--success)':(num<0?'var(--danger)':'var(--text-secondary)'); return `${w.start}${w.target}${w.delta}${w.total}`; }).join(''); - return `
重量变化数据
-
${scaleThead('weight')}${rows}
`; + return `
${scalePlainThead('weight')}${rows}
`; } /* 维度4:运行日志 — 日期/时间 / 报错类型 / 报错内容(报错代码,20字符) / 操作 */ diff --git a/web-admin/nutrition/doc/CHANGELOG.md b/web-admin/nutrition/doc/CHANGELOG.md index 14fb8db..22a483e 100644 --- a/web-admin/nutrition/doc/CHANGELOG.md +++ b/web-admin/nutrition/doc/CHANGELOG.md @@ -1,5 +1,12 @@ # 营养用餐管理端 CHANGELOG +## [2026-08-31] feat: 餐线营养秤概览筛选栏下沉 + 重量变化折线图 + +- `din-terminal.html` 设备概览「餐线营养秤」: + - 设备状态监控/重量变化数据/人脸识别数据三个 Tab 的筛选由表头图标改为标题下方筛选栏(日期时间段,设备状态监控额外含监控项下拉) + - 重量变化数据 Tab 新增「表格/折线图」切换:折线图纵轴为重量、横轴为当天时间,识别点按 4 种状态着色(刷脸/离开/陌生人/重量稳定变化),鼠标移入展示当天详情浮层 + - 人脸识别视频回放弹窗删除识别时间/状态/用户类型/重量变化信息区 + ## [2026-08-27] fix: 健康生产菜单「净配」统一改回「净菜组配」 - 「健康生产」模块侧边栏菜单项「净配」→「净菜组配」,10 个页面同步修正: