feat: 餐线营养秤概览筛选栏下沉 + 重量变化折线图

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhangyan
2026-08-31 08:37:33 +08:00
co-authored by Claude Opus 4.6
parent ab27a394f3
commit 7333189799
2 changed files with 215 additions and 15 deletions
+208 -15
View File
@@ -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+='<th>'+actionLabel+'</th>';
return '<thead><tr>'+ths+'</tr></thead>';
}
/* 生成纯文字表头(无筛选图标,用于 status/face/weight 三个 Tab */
function scalePlainThead(tabKey,actionLabel){
var ths=SCALE_COLS[tabKey].map(function(c){return '<th>'+c.label+'</th>';}).join('');
if(actionLabel)ths+='<th>'+actionLabel+'</th>';
return '<thead><tr>'+ths+'</tr></thead>';
}
/* 取某 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;i<cols.length;i++){
var col=cols[i];
@@ -859,6 +902,12 @@ function getFilteredScaleRows(tabKey){
return false;
}
}
/* 日期范围筛选(时间段) */
if(tf&&(dr.from||dr.to)){
var t=normalizeMinute(r[tf]);
if(dr.from&&t<dr.from)return false;
if(dr.to&&t>dr.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='<div class="scale-filter-bar__item"><label>日期时间段</label>'
+'<div class="scale-filter-bar__date">'
+'<input class="scale-filter-bar__input" type="datetime-local" step="60" id="scaleDateFrom_'+tabKey+'" value="'+escapeAttr(dr.from)+'">'
+'<span class="sep">→</span>'
+'<input class="scale-filter-bar__input" type="datetime-local" step="60" id="scaleDateTo_'+tabKey+'" value="'+escapeAttr(dr.to)+'">'
+'</div></div>';
var extraHtml='';
if(tabKey==='status'){
var cur=(SCALE_FILTERS.status&&SCALE_FILTERS.status.item)||'';
var opts='<option value="">全部</option>';
getScaleColValues('status','item').forEach(function(v){
opts+='<option value="'+escapeAttr(v)+'"'+(cur===v?' selected':'')+'>'+escapeHtml(v)+'</option>';
});
extraHtml='<div class="scale-filter-bar__item"><label>监控项</label><select class="scale-filter-bar__select" id="scaleItemFilter">'+opts+'</select></div>';
}
var toggleHtml='';
if(tabKey==='weight'){
toggleHtml='<div class="scale-view-toggle">'
+'<span class="scale-view-toggle__btn'+(SCALE_WEIGHT_VIEW==='table'?' active':'')+'" onclick="switchWeightView(\'table\')">表格</span>'
+'<span class="scale-view-toggle__btn'+(SCALE_WEIGHT_VIEW==='chart'?' active':'')+'" onclick="switchWeightView(\'chart\')">折线图</span>'
+'</div>';
}
return '<div class="scale-filter-bar">'+dateHtml+extraHtml
+'<div class="scale-filter-bar__actions">'
+'<button class="btn btn-default" onclick="applyScaleFilter(\''+tabKey+'\',true)">重置</button>'
+'<button class="btn btn-primary" onclick="applyScaleFilter(\''+tabKey+'\',false)">查询</button>'
+toggleHtml
+'</div></div>';
}
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 '<div class="scale-chart-legend__item"><span class="scale-chart-legend__dot" style="background:'+SCALE_WEIGHT_STATES[s]+'"></span>'+s+'</div>';
}).join('');
if(!arr.length){
return '<div class="scale-chart-legend">'+legend+'</div><div class="ov-placeholder" style="padding:40px 0"><div class="ov-placeholder__text">当前时间范围内无重量变化数据</div></div>';
}
return '<div class="scale-chart-legend">'+legend+'</div>'+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+='<line x1="'+pl+'" y1="'+y+'" x2="'+(w-pr)+'" y2="'+y+'" stroke="#f0f0f0" stroke-width="1"/>'
+'<text x="'+(pl-6)+'" y="'+(y+4)+'" text-anchor="end" fill="#999" font-size="11">'+val.toFixed(1)+'</text>';
}
var xLabels=arr.map(function(r,i){
return '<text x="'+(pl+i*step)+'" y="'+(h-12)+'" text-anchor="middle" fill="#999" font-size="10">'+formatChartTime(r.start)+'</text>';
}).join('');
var line=pts.map(function(p){return p.x+','+p.y;}).join(' ');
var polyline='<polyline points="'+line+'" fill="none" stroke="#1890FF" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>';
var dots=pts.map(function(p){
var color=SCALE_WEIGHT_STATES[p.r.state]||'#999';
return '<circle class="scale-chart-dot" cx="'+p.x+'" cy="'+p.y+'" r="5" fill="'+color+'" stroke="#fff" stroke-width="1.5" onmouseenter="showWeightPointTip(event,\''+p.r.start+'\')" onmouseleave="hideWeightPointTip()"/>';
}).join('');
return '<svg class="scale-chart-svg" viewBox="0 0 '+w+' '+h+'" xmlns="http://www.w3.org/2000/svg">'+grid+xLabels+polyline+dots+'</svg>';
}
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='<div class="scale-chart-tooltip__title">识别点详情</div>'
+'<div class="scale-chart-tooltip__row"><span>方向</span><strong>'+dir+'</strong></div>'
+'<div class="scale-chart-tooltip__row"><span>变化量</span><strong>'+escapeHtml(rec.delta)+'克</strong></div>'
+'<div class="scale-chart-tooltip__row"><span>起始→结束</span><strong>'+escapeHtml(rec.startWeight)+'→'+escapeHtml(rec.total)+'克</strong></div>'
+'<div class="scale-chart-tooltip__row"><span>归属用户</span><strong>'+escapeHtml(rec.userId||rec.target)+'</strong></div>'
+'<div class="scale-chart-tooltip__row"><span>起始时间</span><strong>'+escapeHtml(rec.start)+'</strong></div>'
+'<div class="scale-chart-tooltip__row"><span>结束时间</span><strong>'+escapeHtml(rec.end)+'</strong></div>';
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=>`<tr><td>${s.time}</td><td>${s.item}</td><td>${s.event}</td></tr>`).join('');
return `<div class="ov-section-title">设备状态监控</div>
<div style="overflow-x:auto"><table class="ov-daily-table">${scaleThead('status')}<tbody>${rows}</tbody></table></div>`;
${renderScaleFilterBar('status')}
<div style="overflow-x:auto"><table class="ov-daily-table">${scalePlainThead('status')}<tbody>${rows}</tbody></table></div>`;
}
/* 维度2:人脸识别数据 — 开始/结束时间 / 状态 + 视频回放 */
@@ -1008,7 +1202,8 @@ function renderScaleFace(){
return `<tr><td>${f.start}</td><td>${f.end}</td><td>${statusTag}</td><td><a class="link" onclick="showFaceVideo('${f.start}')">回放视频</a></td></tr>`;
}).join('');
return `<div class="ov-section-title">人脸识别数据</div>
<div style="overflow-x:auto"><table class="ov-daily-table">${scaleThead('face','操作')}<tbody>${rows}</tbody></table></div>`;
${renderScaleFilterBar('face')}
<div style="overflow-x:auto"><table class="ov-daily-table">${scalePlainThead('face','操作')}<tbody>${rows}</tbody></table></div>`;
}
/* 录像回放弹窗:自动播放 + 快进/后退 + 进度条跳转 */
@@ -1042,12 +1237,6 @@ function showFaceVideo(start){
<div class="face-video-bar" id="faceVideoBar" onclick="faceVideoSeekTo(event)"><div class="face-video-bar__progress" id="faceVideoProgress"></div></div>
<div class="face-video-time" id="faceVideoTime">00:00 / 00:30</div>
</div>
<div class="face-video-info">
<div class="face-video-info__item"><span class="label">识别时间</span><strong>${f.start}</strong></div>
<div class="face-video-info__item"><span class="label">状态</span><strong>${f.status}</strong></div>
<div class="face-video-info__item"><span class="label">用户类型</span><strong>${f.userType}</strong></div>
<div class="face-video-info__item"><span class="label">重量变化</span><strong>${f.weight}</strong></div>
</div>
</div>
</div>`;
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 `<div class="ov-section-title">重量变化数据</div>
${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 `<tr><td>${w.start}</td><td>${w.target}</td><td><span style="color:${color}">${w.delta}</span></td><td>${w.total}</td></tr>`;
}).join('');
return `<div class="ov-section-title">重量变化数据</div>
<div style="overflow-x:auto"><table class="ov-daily-table">${scaleThead('weight')}<tbody>${rows}</tbody></table></div>`;
return `<div style="overflow-x:auto"><table class="ov-daily-table">${scalePlainThead('weight')}<tbody>${rows}</tbody></table></div>`;
}
/* 维度4:运行日志 — 日期/时间 / 报错类型 / 报错内容(报错代码,20字符) / 操作 */
+7
View File
@@ -1,5 +1,12 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-31] feat: 餐线营养秤概览筛选栏下沉 + 重量变化折线图
- `din-terminal.html` 设备概览「餐线营养秤」:
- 设备状态监控/重量变化数据/人脸识别数据三个 Tab 的筛选由表头图标改为标题下方筛选栏(日期时间段,设备状态监控额外含监控项下拉)
- 重量变化数据 Tab 新增「表格/折线图」切换:折线图纵轴为重量、横轴为当天时间,识别点按 4 种状态着色(刷脸/离开/陌生人/重量稳定变化),鼠标移入展示当天详情浮层
- 人脸识别视频回放弹窗删除识别时间/状态/用户类型/重量变化信息区
## [2026-08-27] fix: 健康生产菜单「净配」统一改回「净菜组配」
- 「健康生产」模块侧边栏菜单项「净配」→「净菜组配」,10 个页面同步修正: