feat: 净菜类型拆分独立管理(食物成分页操作区食材分类管理后新增净菜管理入口;净菜类型新增规格行级可食部(%)字段,位于规格别名后,逐规格独立维护并同步新增/编辑/详情三页;新增净菜管理二级页:统计卡片、食材名称下拉搜索/食材编码/净菜类型三源关键字/切配形状筛选、异步导出按钮组(导出列表数据+查看导出任务,样式对齐列表页惯例)、组级列rowspan合并+可食部/规格别名/切配规格/刀工按规格逐行拆分(组内虚线分隔、规格列组纵向分隔线)、先选食材回显已有净菜类型再新增/编辑的维护弹窗;首页SITE_MAP与营养模块CHANGELOG同步)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7f184709f3
commit
891678d97e
@@ -0,0 +1,911 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>营养管理 - 净菜管理 | 健康CQ原型</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:#F5F7FA;color:#333;min-width:1280px;}
|
||||
:root{
|
||||
--primary:#1890FF;--primary-light:#E6F7FF;--success:#52C41A;--warning:#FAAD14;--danger:#FF4D4F;
|
||||
--text-primary:#333;--text-secondary:#666;--text-placeholder:#999;--border:#E8E8E8;--background:#F5F7FA;
|
||||
--font-xs:11px;--font-sm:13px;--font-base:15px;--font-lg:17px;--font-xl:20px;--font-xxl:24px;
|
||||
--radius-sm:4px;--radius-md:8px;--radius-lg:12px;
|
||||
--shadow-sm:0 1px 4px rgba(0,0,0,0.08);--shadow-md:0 4px 12px rgba(0,0,0,0.12);
|
||||
}
|
||||
.top-bar{display:flex;align-items:center;height:56px;background:#001529;padding:0 24px;position:fixed;top:0;left:0;right:0;z-index:100;}
|
||||
.top-bar__logo{display:flex;align-items:center;gap:8px;color:#fff;font-size:var(--font-lg);font-weight:600;white-space:nowrap;width:240px;flex-shrink:0;}
|
||||
.top-bar__logo svg{width:28px;height:28px;}
|
||||
.top-bar__nav{display:flex;align-items:center;gap:4px;flex:1;}
|
||||
.top-bar__nav a{color:rgba(255,255,255,0.65);text-decoration:none;padding:6px 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);transition:all .2s;}
|
||||
.top-bar__nav a:hover{color:#fff;background:rgba(255,255,255,0.08);}
|
||||
.top-bar__nav a.active{color:#fff;background:var(--primary);}
|
||||
.top-bar__right{display:flex;align-items:center;gap:16px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);}
|
||||
.top-bar__right .btn-back{color:rgba(255,255,255,0.65);cursor:pointer;display:flex;align-items:center;gap:4px;text-decoration:none;}
|
||||
.top-bar__right .btn-back:hover{color:#fff;}
|
||||
.avatar{width:32px;height:32px;border-radius:50%;background:#1890FF;display:flex;align-items:center;justify-content:center;color:#fff;font-size:var(--font-sm);cursor:pointer;}
|
||||
.sidebar{position:fixed;top:56px;left:0;bottom:0;width:240px;background:#001529;overflow-y:auto;z-index:90;padding-top:8px;}
|
||||
.sidebar-menu__title{color:rgba(255,255,255,0.35);font-size:var(--font-xs);padding:16px 24px 8px;text-transform:uppercase;letter-spacing:.5px;}
|
||||
.sidebar-item{display:flex;align-items:center;gap:8px;padding:10px 24px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);cursor:pointer;transition:all .15s;text-decoration:none;}
|
||||
.sidebar-item:hover{color:#fff;background:rgba(255,255,255,0.06);}
|
||||
.sidebar-item.active{color:#fff;background:var(--primary);}
|
||||
.mi{display:inline-block;width:20px;text-align:center;filter:grayscale(1);opacity:0.65;margin-right:2px;}
|
||||
.sidebar-item.active .mi{opacity:0.9;}
|
||||
.sidebar-subgroup__header{display:flex;align-items:center;gap:6px;padding:10px 24px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);cursor:pointer;transition:all .15s;user-select:none;}
|
||||
.sidebar-subgroup__header:hover{color:rgba(255,255,255,0.85);background:rgba(255,255,255,0.04);}
|
||||
.sidebar-subgroup__header .arrow{width:10px;height:10px;transition:transform .2s;flex-shrink:0;}
|
||||
.sidebar-subgroup__header .arrow path{stroke:rgba(255,255,255,0.45);}
|
||||
.sidebar-subgroup.open>.sidebar-subgroup__header{color:rgba(255,255,255,0.85);}
|
||||
.sidebar-subgroup.open>.sidebar-subgroup__header .arrow{transform:rotate(90deg);}
|
||||
.sidebar-subgroup__body{display:none;padding-left:16px;}
|
||||
.sidebar-subgroup.open>.sidebar-subgroup__body{display:block;}
|
||||
.sidebar-subgroup__body .sidebar-item{padding-left:36px;font-size:12px;}
|
||||
.main{margin-left:240px;margin-top:56px;padding:24px;min-height:calc(100vh - 56px);}
|
||||
/* 面包屑 */
|
||||
.crumb{display:flex;align-items:center;gap:8px;margin-bottom:16px;font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
.crumb a{color:var(--primary);text-decoration:none;}
|
||||
.crumb a:hover{color:#40a9ff;}
|
||||
.crumb .sep{color:var(--text-placeholder);}
|
||||
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
|
||||
.stat-card{background:#fff;border-radius:var(--radius-md);padding:20px 24px;box-shadow:var(--shadow-sm);display:flex;align-items:center;gap:16px;}
|
||||
.stat-card__icon{width:48px;height:48px;border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:24px;}
|
||||
.stat-card__icon.blue{background:var(--primary-light);}
|
||||
.stat-card__icon.green{background:#F6FFED;}
|
||||
.stat-card__icon.orange{background:#FFF7E6;}
|
||||
.stat-card__icon.purple{background:#F9F0FF;}
|
||||
.stat-card__info{display:flex;flex-direction:column;gap:4px;}
|
||||
.stat-card__value{font-size:var(--font-xl);font-weight:700;color:var(--text-primary);}
|
||||
.stat-card__label{font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
.btn{height:36px;padding:0 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);cursor:pointer;border:none;transition:all .2s;display:inline-flex;align-items:center;gap:6px;text-decoration:none;}
|
||||
.btn-primary{background:var(--primary);color:#fff;}.btn-primary:hover{background:#40a9ff;}
|
||||
.btn-default{background:#fff;color:var(--text-primary);border:1px solid var(--border);}.btn-default:hover{color:var(--primary);border-color:var(--primary);}
|
||||
.tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);margin-right:4px;}
|
||||
.tag-blue{background:var(--primary-light);color:var(--primary);}
|
||||
.tag-green{background:#F6FFED;color:var(--success);}
|
||||
.tag-gray{background:#F5F5F5;color:#999;}
|
||||
.link{color:var(--primary);cursor:pointer;font-size:var(--font-sm);text-decoration:none;}.link:hover{color:#40a9ff;}
|
||||
/* 列表卡片 */
|
||||
.list-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);display:flex;flex-direction:column;}
|
||||
.filter-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:16px 24px;border-bottom:1px solid var(--border);}
|
||||
.filter-item{display:flex;align-items:center;gap:8px;}
|
||||
.filter-item>label{font-size:var(--font-sm);color:var(--text-secondary);white-space:nowrap;}
|
||||
.filter-bar input[type="text"],.filter-bar select{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;font-size:var(--font-sm);outline:none;background:#fff;}
|
||||
.filter-bar select{padding-right:28px;appearance: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 10px center;cursor:pointer;}
|
||||
.filter-bar input[type="text"]:focus,.filter-bar select:focus{border-color:var(--primary);}
|
||||
.filter-bar .btn{height:32px;font-size:var(--font-xs);padding:0 14px;}
|
||||
.toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;border-bottom:1px solid var(--border);}
|
||||
.toolbar__right{font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.table-wrap{overflow-x:auto;padding:0 24px;}
|
||||
.veg-list-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);}
|
||||
.veg-list-table th{background:#FAFAFA;padding:10px 12px;text-align:left;color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
|
||||
.veg-list-table td{padding:10px 12px;border-bottom:1px solid #F5F5F5;color:var(--text-primary);vertical-align:top;}
|
||||
.veg-list-table tbody tr:hover{background:#F8FBFF;}
|
||||
.veg-list-table .td-food{white-space:nowrap;}
|
||||
.veg-list-table .td-food .food-name{font-weight:500;}
|
||||
.veg-list-table .td-food .food-code{font-size:var(--font-xs);color:var(--text-placeholder);margin-top:2px;}
|
||||
.veg-list-table .veg-std-name{font-weight:500;color:var(--primary);background:#F8FBFF;}
|
||||
/* 规格行级列:四列与组级列之间加纵向分隔线,组内各规格行之间用虚线分隔 */
|
||||
.veg-list-table td.td-spec-start{border-left:1px solid #EBEBEB;}
|
||||
.veg-list-table td.td-spec--inner{border-bottom:1px dashed #D9D9D9;}
|
||||
.veg-list-table td.td-actions{white-space:nowrap;}
|
||||
.veg-list-table td.td-actions .link{margin:0 4px;}
|
||||
.veg-list-table .empty-row td{text-align:center;color:var(--text-placeholder);padding:48px 0;}
|
||||
.panel-footer{padding:12px 24px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border);}
|
||||
.panel-footer span{font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.page-btn{min-width:26px;height:26px;padding:0 4px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-xs);display:inline-flex;align-items:center;justify-content:center;transition:all .2s;}
|
||||
.page-btn:hover,.page-btn.active{background:var(--primary);color:#fff;border-color:var(--primary);}
|
||||
/* 弹窗 */
|
||||
.dialog-mask{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:200;align-items:center;justify-content:center;}
|
||||
.dialog-mask.open{display:flex;}
|
||||
.dialog{background:#fff;border-radius:var(--radius-lg);box-shadow:var(--shadow-md);max-height:84vh;display:flex;flex-direction:column;}
|
||||
.dialog__header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||||
.dialog__title{font-size:var(--font-lg);font-weight:600;}
|
||||
.dialog__close{width:32px;height:32px;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:var(--radius-sm);color:var(--text-placeholder);font-size:16px;}
|
||||
.dialog__close:hover{background:#F5F5F5;color:var(--text-primary);}
|
||||
.dialog__body{flex:1;overflow-y:auto;padding:16px 24px;}
|
||||
.dialog__footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 24px;border-top:1px solid var(--border);flex-shrink:0;}
|
||||
/* 食材选择列表 */
|
||||
.food-pick__search{margin-bottom:12px;}
|
||||
.food-pick__search input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);outline:none;}
|
||||
.food-pick__search input:focus{border-color:var(--primary);}
|
||||
.food-pick__list{border:1px solid var(--border);border-radius:var(--radius-md);max-height:380px;overflow-y:auto;}
|
||||
.food-pick__item{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;transition:background .15s;border-bottom:1px solid #F5F5F5;}
|
||||
.food-pick__item:last-child{border-bottom:none;}
|
||||
.food-pick__item:hover{background:#F8FBFF;}
|
||||
.food-pick__item.active{background:var(--primary-light);}
|
||||
.food-pick__icon{width:36px;height:36px;border-radius:var(--radius-sm);background:#F5F7FA;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}
|
||||
.food-pick__info{flex:1;min-width:0;}
|
||||
.food-pick__name{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);}
|
||||
.food-pick__meta{font-size:var(--font-xs);color:var(--text-placeholder);margin-top:2px;display:flex;gap:8px;}
|
||||
.food-pick__count{font-size:var(--font-xs);color:var(--text-secondary);flex-shrink:0;}
|
||||
.food-pick__check{display:none;width:20px;height:20px;border-radius:50%;background:var(--primary);color:#fff;font-size:12px;align-items:center;justify-content:center;flex-shrink:0;}
|
||||
.food-pick__item.active .food-pick__check{display:flex;}
|
||||
.food-pick__empty{padding:40px 24px;text-align:center;color:var(--text-placeholder);font-size:var(--font-sm);}
|
||||
/* 净菜维护表(扁平 + rowspan 合并,结构对齐 din-ingredient-add.html) */
|
||||
.veg-table-wrap{overflow-x:auto;}
|
||||
.veg-table{width:100%;border-collapse:collapse;font-size:var(--font-xs);border:1px solid #E8E8E8;border-radius:var(--radius-sm);}
|
||||
.veg-table th{background:#FAFAFA;color:var(--text-secondary);font-weight:500;padding:8px 8px;text-align:left;border-bottom:1px solid #E8E8E8;white-space:nowrap;}
|
||||
.veg-table td{padding:6px 8px;border-bottom:1px solid #F0F0F0;vertical-align:middle;}
|
||||
.veg-table tr.veg-row:last-child td{border-bottom:none;}
|
||||
.veg-table tr.veg-row--spec td{background:#FAFBFC;}
|
||||
.veg-table .veg-cell--merged{background:#FAFBFC;vertical-align:middle;text-align:center;padding:8px;}
|
||||
.veg-table .veg-name{display:block;font-size:var(--font-sm);font-weight:600;color:var(--primary);line-height:1.3;word-break:break-all;}
|
||||
.veg-table .veg-name--placeholder{color:var(--text-placeholder);font-weight:400;font-size:var(--font-xs);}
|
||||
.veg-table .veg-cell--merged .form-input,.veg-table .veg-cell--merged .form-select{height:26px;font-size:11px;}
|
||||
.veg-table .form-input,.veg-table .form-select{height:28px;font-size:var(--font-xs);padding:0 6px;width:100%;}
|
||||
.veg-table .col-name{min-width:130px;}
|
||||
.veg-table .col-alias{min-width:100px;}
|
||||
.veg-table .col-yield{min-width:70px;}
|
||||
.veg-table .col-shape{min-width:90px;}
|
||||
.veg-table .col-spec-alias{min-width:90px;width:90px;}
|
||||
.veg-table .col-dim{min-width:62px;width:62px;}
|
||||
.veg-table .col-tech{min-width:85px;width:85px;}
|
||||
.veg-table .col-action{min-width:60px;white-space:nowrap;text-align:center;}
|
||||
.veg-action-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid var(--border);background:#fff;color:var(--text-secondary);border-radius:var(--radius-sm);cursor:pointer;font-size:13px;line-height:1;transition:all .15s;vertical-align:middle;margin:0 1px;}
|
||||
.veg-action-btn:hover{color:var(--primary);border-color:var(--primary);}
|
||||
.veg-action-btn--add{color:var(--primary);border-color:var(--primary);}
|
||||
.veg-action-btn--add:hover{background:var(--primary-light);}
|
||||
.veg-action-btn--del:hover{color:var(--danger);border-color:var(--danger);background:#FFF1F0;}
|
||||
.veg-table .veg-empty-row td{padding:24px;text-align:center;color:var(--text-placeholder);}
|
||||
.dynamic-row-add{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border:1px dashed var(--primary);color:var(--primary);border-radius:var(--radius-sm);font-size:var(--font-xs);cursor:pointer;background:none;margin-top:8px;transition:all .2s;}
|
||||
.dynamic-row-add:hover{background:var(--primary-light);}
|
||||
.form-input,.form-select{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);outline:none;background:#fff;}
|
||||
.form-input:focus,.form-select:focus{border-color:var(--primary);}
|
||||
/* 维护弹窗内当前食材信息条 */
|
||||
.pick-hint{display:flex;align-items:center;gap:10px;background:#F8FBFF;border:1px solid #D6EAF8;border-radius:var(--radius-sm);padding:10px 14px;margin-bottom:14px;}
|
||||
.pick-hint__icon{font-size:20px;}
|
||||
.pick-hint__text{flex:1;font-size:var(--font-sm);color:var(--text-primary);}
|
||||
.pick-hint__text b{font-weight:600;}
|
||||
.pick-hint__text .code{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:8px;}
|
||||
.pick-hint__switch{font-size:var(--font-xs);color:var(--primary);cursor:pointer;white-space:nowrap;}
|
||||
.pick-hint__switch:hover{text-decoration:underline;}
|
||||
/* 交互注释标注 */
|
||||
.proto-note{display:inline-flex;align-items:center;gap:4px;background:#FFFBE6;border:1px solid #FFE58F;color:#8C6E00;font-size:var(--font-xs);padding:2px 8px;border-radius:10px;font-weight:400;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="top-bar">
|
||||
<div class="top-bar__logo">
|
||||
<svg viewBox="0 0 28 28" fill="none"><rect width="28" height="28" rx="6" fill="#1890FF"/><path d="M7 14h14M14 7v14" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/></svg>
|
||||
营养管理
|
||||
</div>
|
||||
<nav class="top-bar__nav">
|
||||
<a href="trace-query.html">溯源总览</a>
|
||||
<a href="arc-employee.html">档案</a>
|
||||
<a href="farm-land.html">绿色种采</a>
|
||||
<a href="sup-hygiene-check.html">卫生供应</a>
|
||||
<a href="prod-qc-daily.html">健康生产</a>
|
||||
<a href="din-dict-mgmt.html" class="active">营养用餐</a>
|
||||
<a href="ano-dirty.html">异常事件</a>
|
||||
</nav>
|
||||
<div class="top-bar__right">
|
||||
<a href="../../index.html" class="btn-back">← 返回首页</a>
|
||||
<div class="avatar">管</div>
|
||||
<span>系统管理员 ▾</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 侧边栏:营养用餐 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-menu__title">营养用餐</div>
|
||||
<a href="din-canteen.html" class="sidebar-item"><span class="mi">🏢</span>场所管理</a>
|
||||
<a href="din-dict-mgmt.html" class="sidebar-item"><span class="mi">📖</span>字典管理</a>
|
||||
<a href="prod-qc-nutrition.html" class="sidebar-item"><span class="mi">🔬</span>营养检验</a>
|
||||
<a href="din-ingredient.html" class="sidebar-item active"><span class="mi">🧪</span>食物成分</a>
|
||||
<a href="din-dish-all.html" class="sidebar-item"><span class="mi">🍽️</span>餐品总库</a>
|
||||
<a href="din-dish-own.html" class="sidebar-item"><span class="mi">📦</span>自有餐品库</a>
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
|
||||
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
|
||||
📐 算法标准
|
||||
</div>
|
||||
<div class="sidebar-subgroup__body">
|
||||
<a href="din-algo-population.html" class="sidebar-item"><span class="mi">👥</span>人群基础标准</a>
|
||||
<a href="din-algo-vip.html" class="sidebar-item"><span class="mi">👑</span>VIP定制标准</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="din-recipe-mgmt.html" class="sidebar-item"><span class="mi">📋</span>食谱管理</a>
|
||||
<a href="din-order-meal.html" class="sidebar-item"><span class="mi">🍱</span>营养点餐</a>
|
||||
<a href="din-user-mgmt.html" class="sidebar-item"><span class="mi">👤</span>用户管理</a>
|
||||
<a href="din-order-line.html" class="sidebar-item"><span class="mi">🍳</span>餐线管理</a>
|
||||
<a href="din-order-pickup.html" class="sidebar-item"><span class="mi">🎫</span>取餐管理</a>
|
||||
<a href="din-data-consume.html" class="sidebar-item"><span class="mi">💰</span>消费管理</a>
|
||||
<a href="din-data-nutri.html" class="sidebar-item"><span class="mi">📊</span>营养管理</a>
|
||||
<a href="din-data-benefit.html" class="sidebar-item"><span class="mi">📈</span>餐材效益</a>
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
|
||||
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
|
||||
📊 数据分析
|
||||
</div>
|
||||
<div class="sidebar-subgroup__body">
|
||||
<a href="din-biz-analysis.html" class="sidebar-item"><span class="mi">📈</span>经营分析</a>
|
||||
<a href="din-meal-analysis.html" class="sidebar-item"><span class="mi">📊</span>餐品分析</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
|
||||
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
|
||||
📑 报表中心
|
||||
</div>
|
||||
<div class="sidebar-subgroup__body">
|
||||
<a href="din-place-biz-analysis.html" class="sidebar-item"><span class="mi">🏪</span>场所经营分析</a>
|
||||
<a href="din-place-goal.html" class="sidebar-item"><span class="mi">🎯</span>场所目标考核</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
|
||||
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
|
||||
🖥️ 终端管理
|
||||
</div>
|
||||
<div class="sidebar-subgroup__body">
|
||||
<a href="din-terminal.html" class="sidebar-item"><span class="mi">📟</span>设备管理</a>
|
||||
<a href="din-content-center.html" class="sidebar-item"><span class="mi">📦</span>内容中心</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
|
||||
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
|
||||
🤖 模型管理
|
||||
</div>
|
||||
<div class="sidebar-subgroup__body">
|
||||
<a href="din-model-llm.html" class="sidebar-item"><span class="mi">🧠</span>大模型管理</a>
|
||||
<a href="din-model-small.html" class="sidebar-item"><span class="mi">⚙️</span>小模型管理</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="din-innovation.html" class="sidebar-item"><span class="mi">💡</span>味养创新</a>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="main">
|
||||
<!-- 面包屑 -->
|
||||
<div class="crumb">
|
||||
<a href="din-ingredient.html">🧪 食物成分</a><span class="sep">/</span><span>净菜管理</span>
|
||||
<span class="proto-note" style="margin-left:12px;">原型演示:本页净菜类型数据独立维护,与食物成分页共享同一数据模型</span>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card"><div class="stat-card__icon blue">🥬</div><div class="stat-card__info"><div class="stat-card__value" id="statGroupCount">—</div><div class="stat-card__label">净菜类型总数</div></div></div>
|
||||
<div class="stat-card"><div class="stat-card__icon green">🧪</div><div class="stat-card__info"><div class="stat-card__value" id="statFoodCount">—</div><div class="stat-card__label">覆盖食材数</div></div></div>
|
||||
<div class="stat-card"><div class="stat-card__icon orange">📐</div><div class="stat-card__info"><div class="stat-card__value" id="statSpecCount">—</div><div class="stat-card__label">切配规格总数</div></div></div>
|
||||
<div class="stat-card"><div class="stat-card__icon purple">⚖️</div><div class="stat-card__info"><div class="stat-card__value" id="statAvgYield">—</div><div class="stat-card__label">平均出净率</div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="list-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item"><label>食材名称</label>
|
||||
<input type="text" id="fltFoodName" list="foodNameOptions" placeholder="请选择或输入食材名称" style="width:180px;">
|
||||
<datalist id="foodNameOptions"></datalist>
|
||||
</div>
|
||||
<div class="filter-item"><label>食材编码</label><input type="text" id="fltFoodCode" placeholder="请输入食材编码" style="width:140px;"></div>
|
||||
<div class="filter-item"><label>净菜类型</label><input type="text" id="fltVegKeyword" placeholder="净菜俗名 / 净菜标准名 / 规格俗名" style="width:220px;"></div>
|
||||
<div class="filter-item"><label>切配形状</label>
|
||||
<select id="fltShape"><option value="">全部形状</option><option>丝</option><option>片</option><option>块</option><option>沫</option><option>泥</option><option>原材</option></select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="applyFilter()">查 询</button>
|
||||
<button class="btn btn-default" onclick="resetFilter()">重 置</button>
|
||||
</div>
|
||||
<!-- 操作区 -->
|
||||
<div class="toolbar">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="btn btn-primary" onclick="openFoodPicker()">➕ 新增净菜</button>
|
||||
<button class="btn btn-primary" onclick="exportListData()"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 10v2h10v-2M7 2v7M4.5 6.5L7 9l2.5-2.5" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>导出列表数据</button>
|
||||
<button class="btn btn-default" onclick="viewExportTasks()"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M4 5h6M4 7.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>查看导出任务</button>
|
||||
</div>
|
||||
<div class="toolbar__right">共 <b id="totalGroups">0</b> 个净菜类型</div>
|
||||
</div>
|
||||
<!-- 分页列表 -->
|
||||
<div class="table-wrap">
|
||||
<table class="veg-list-table">
|
||||
<thead><tr>
|
||||
<th style="width:48px;">序号</th>
|
||||
<th style="width:150px;">食材</th>
|
||||
<th style="width:150px;">净菜标准名</th>
|
||||
<th style="width:110px;">净菜俗名</th>
|
||||
<th style="width:80px;">切配形状</th>
|
||||
<th style="width:70px;">出净率</th>
|
||||
<th style="width:84px;">可食部(%)</th>
|
||||
<th style="width:100px;">规格别名</th>
|
||||
<th style="width:140px;">切配规格</th>
|
||||
<th style="width:70px;">刀工</th>
|
||||
<th style="width:100px;">操作</th>
|
||||
</tr></thead>
|
||||
<tbody id="vegListBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span id="pageInfo">—</span>
|
||||
<div style="display:flex;gap:4px;" id="pageBtns"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 弹窗一:选择食材(新增净菜第一步) -->
|
||||
<div class="dialog-mask" id="foodPickerDialog">
|
||||
<div class="dialog" style="width:560px;">
|
||||
<div class="dialog__header">
|
||||
<div class="dialog__title">选择食材</div>
|
||||
<button class="dialog__close" onclick="closeFoodPicker()">✕</button>
|
||||
</div>
|
||||
<div class="dialog__body">
|
||||
<div class="food-pick__search"><input type="text" id="foodPickSearch" placeholder="搜索食材名称/编码..." oninput="renderFoodPickList()"></div>
|
||||
<div class="food-pick__list" id="foodPickList"></div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-placeholder);margin-top:10px;">净菜类型需挂在具体食材下维护,请先选定食材;选定后将回显该食材已有的净菜类型,可在其基础上新增或编辑。</div>
|
||||
</div>
|
||||
<div class="dialog__footer">
|
||||
<button class="btn btn-default" onclick="closeFoodPicker()">取消</button>
|
||||
<button class="btn btn-primary" id="btnPickNext" disabled onclick="confirmPickFood()">下一步 · 维护净菜</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗二:净菜类型维护(回显 + 编辑,表格结构对齐新增食物页) -->
|
||||
<div class="dialog-mask" id="vegMaintainDialog">
|
||||
<div class="dialog" style="width:1080px;">
|
||||
<div class="dialog__header">
|
||||
<div class="dialog__title">净菜类型维护</div>
|
||||
<button class="dialog__close" onclick="closeMaintain()">✕</button>
|
||||
</div>
|
||||
<div class="dialog__body">
|
||||
<!-- 当前食材信息条 -->
|
||||
<div class="pick-hint">
|
||||
<span class="pick-hint__icon" id="maintainFoodIcon">🥬</span>
|
||||
<span class="pick-hint__text">当前食材:<b id="maintainFoodName">—</b><span class="code" id="maintainFoodCode"></span></span>
|
||||
<span class="pick-hint__switch" onclick="switchFood()">⇄ 更换食材</span>
|
||||
</div>
|
||||
<div id="dlgVegRows"></div>
|
||||
<button type="button" class="dynamic-row-add" onclick="addDlgGroup()">+ 添加净菜</button>
|
||||
</div>
|
||||
<div class="dialog__footer">
|
||||
<button class="btn btn-default" onclick="closeMaintain()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveMaintain()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗三:删除净菜类型确认 -->
|
||||
<div class="dialog-mask" id="deleteVegDialog">
|
||||
<div class="dialog" style="width:420px;">
|
||||
<div class="dialog__header">
|
||||
<div class="dialog__title">删除净菜类型</div>
|
||||
<button class="dialog__close" onclick="closeDeleteVeg()">✕</button>
|
||||
</div>
|
||||
<div class="dialog__body">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;padding:8px 0;">
|
||||
<div style="font-size:22px;color:var(--warning);line-height:1;">⚠️</div>
|
||||
<div>
|
||||
<div style="font-size:var(--font-sm);color:var(--text-primary);margin-bottom:6px;">确定要删除该净菜类型吗?其下所有切配规格将一并移除。</div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-secondary);">食材:<span id="delVegFood" style="color:var(--text-primary);font-weight:500;"></span></div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-secondary);margin-top:2px;">净菜标准名:<span id="delVegName" style="color:var(--text-primary);font-weight:500;"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog__footer">
|
||||
<button class="btn btn-default" onclick="closeDeleteVeg()">取消</button>
|
||||
<button class="btn btn-primary" style="background:var(--danger);" onclick="confirmDeleteVeg()">确定删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(自包含内联) === */
|
||||
/* 切配规格列:仅展示长宽高尺寸(刀工已拆为独立列) */
|
||||
function formatCutDims(cut){
|
||||
if(!cut) return '—';
|
||||
if(typeof cut === 'string') return cut || '—';
|
||||
var dims = [cut.length, cut.width, cut.height].filter(function(v){
|
||||
return v !== undefined && v !== null && v !== '' && !isNaN(v);
|
||||
});
|
||||
return dims.length ? dims.join('×') + 'mm' : '—';
|
||||
}
|
||||
|
||||
/* === 食材与净菜类型数据(扁平结构,按 groupId 分组,与食物成分页同模型) === */
|
||||
var FOODS = [
|
||||
{name:'粳米(生)',code:'0101001',icon:'🌾',firstClass:'谷薯类',secondClass:'稻米',vegTypes:[
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',edibleRate:'100',cutSpec:{shape:'原材'}},
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'原材'}}
|
||||
]},
|
||||
{name:'猪里脊肉',code:'0201001',icon:'🥩',firstClass:'畜禽肉类',secondClass:'猪肉',vegTypes:[
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'细丝',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'粗丝',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'里脊肉片',specAlias:'薄片',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'片',length:3,width:1,height:5,technique:'平刀'}},
|
||||
{groupId:'g3',alias:'里脊肉块',specAlias:'方块',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'块',length:20,width:20,height:20,technique:'直刀'}}
|
||||
]},
|
||||
{name:'豆腐(北)',code:'0601001',icon:'🧈',firstClass:'豆及豆制品',secondClass:'豆制品',vegTypes:[
|
||||
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',edibleRate:'100',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
|
||||
{groupId:'g1',alias:'块类组',specAlias:'中方块',yieldRate:'96',edibleRate:'100',cutSpec:{shape:'块',length:20,width:20,height:15,technique:'直刀'}}
|
||||
]},
|
||||
{name:'马铃薯',code:'0104001',icon:'🥔',firstClass:'谷薯类',secondClass:'薯类',vegTypes:[
|
||||
{groupId:'g1',alias:'土豆片组',specAlias:'薄片',yieldRate:'86',edibleRate:'94',cutSpec:{shape:'片',length:40,width:40,height:2,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'土豆片组',specAlias:'厚片',yieldRate:'86',edibleRate:'94',cutSpec:{shape:'片',length:40,width:40,height:5,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'土豆片组',specAlias:'薯片',yieldRate:'80',edibleRate:'92',cutSpec:{shape:'片',length:45,width:45,height:1,technique:'斜刀'}},
|
||||
{groupId:'g2',alias:'土豆丝组',specAlias:'粗丝',yieldRate:'84',edibleRate:'94',cutSpec:{shape:'丝',length:50,width:3,height:3,technique:'直刀'}}
|
||||
]},
|
||||
{name:'胡萝卜',code:'0702001',icon:'🥕',firstClass:'蔬菜类',secondClass:'根茎类',vegTypes:[
|
||||
{groupId:'g1',alias:'萝卜丁组',specAlias:'方丁',yieldRate:'90',edibleRate:'96',cutSpec:{shape:'块',length:10,width:10,height:10,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'萝卜丝组',specAlias:'细丝',yieldRate:'88',edibleRate:'96',cutSpec:{shape:'丝',length:60,width:2,height:2,technique:'平刀'}}
|
||||
]},
|
||||
{name:'鸡胸肉',code:'0204001',icon:'🍗',firstClass:'畜禽肉类',secondClass:'鸡肉',vegTypes:[
|
||||
{groupId:'g1',alias:'鸡丁组',specAlias:'方丁',yieldRate:'93',edibleRate:'100',cutSpec:{shape:'块',length:15,width:15,height:15,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'鸡片组',specAlias:'肉片',yieldRate:'91',edibleRate:'100',cutSpec:{shape:'片',length:50,width:30,height:3,technique:'平刀'}},
|
||||
{groupId:'g3',alias:'鸡茸组',specAlias:'鸡茸',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'泥'}}
|
||||
]},
|
||||
{name:'白萝卜',code:'0702002',icon:'🥗',firstClass:'蔬菜类',secondClass:'根茎类',vegTypes:[
|
||||
{groupId:'g1',alias:'滚刀块组',specAlias:'滚刀块',yieldRate:'87',edibleRate:'95',cutSpec:{shape:'块',length:30,width:25,height:25,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'萝卜沫组',specAlias:'细沫',yieldRate:'85',edibleRate:'95',cutSpec:{shape:'沫',length:3,width:3,height:3,technique:'直刀'}}
|
||||
]},
|
||||
{name:'小葱',code:'0701002',icon:'🌿',firstClass:'蔬菜类',secondClass:'叶菜类',vegTypes:[
|
||||
{groupId:'g1',alias:'葱花组',specAlias:'小葱花',yieldRate:'92',edibleRate:'92',cutSpec:{shape:'沫',length:3,width:3,height:3,technique:'直刀'}},
|
||||
{groupId:'g1',alias:'葱花组',specAlias:'葱段',yieldRate:'90',edibleRate:'90',cutSpec:{shape:'原材',length:50,width:'',height:'',technique:'直刀'}}
|
||||
]},
|
||||
{name:'生姜',code:'1102001',icon:'🫚',firstClass:'调味品类',secondClass:'香辛料',vegTypes:[
|
||||
{groupId:'g1',alias:'姜组',specAlias:'姜片',yieldRate:'94',edibleRate:'100',cutSpec:{shape:'片',length:35,width:25,height:2,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'姜组',specAlias:'姜米',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'沫',length:2,width:2,height:2,technique:'直刀'}}
|
||||
]}
|
||||
];
|
||||
|
||||
/* 切配形状/刀工选项(对齐字典 spec_cut_shape,与新增食物页一致) */
|
||||
var VEG_SHAPE_OPTIONS=['','丝','片','块','沫','泥','原材'];
|
||||
var VEG_TECH_OPTIONS=['','平刀','斜刀','直刀','花刀','其他'];
|
||||
|
||||
let currentFood = 0; /* 维护弹窗当前食材下标 */
|
||||
let groupSeq = 100; /* 新增净菜组 ID 自增 */
|
||||
let pickIdx = -1; /* 食材选择弹窗临时选中 */
|
||||
let deleting = null; /* 待删除 {foodIdx, groupId} */
|
||||
|
||||
/* 列表过滤态 */
|
||||
var filterState = {foodName:'',foodCode:'',vegKeyword:'',shape:''};
|
||||
var pageSize = 10;
|
||||
var curPage = 1;
|
||||
|
||||
/* 将扁平 vegTypes 按 groupId 聚合为净菜类型组(保持原顺序) */
|
||||
function collectGroups(food){
|
||||
var keys=[], map={};
|
||||
food.vegTypes.forEach(function(it){
|
||||
if(!map[it.groupId]){keys.push(it.groupId);map[it.groupId]=[];}
|
||||
map[it.groupId].push(it);
|
||||
});
|
||||
return keys.map(function(k){
|
||||
var items=map[k], first=items[0], cs=first.cutSpec||{};
|
||||
return {groupId:k, items:items, alias:first.alias||'', yieldRate:first.yieldRate||'', shape:cs.shape||'', stdName: food.name+'-'+(cs.shape||'')};
|
||||
});
|
||||
}
|
||||
|
||||
/* 全部净菜类型组(带食材引用),用于列表渲染 */
|
||||
function allRows(){
|
||||
var rows=[];
|
||||
FOODS.forEach(function(f,fi){
|
||||
collectGroups(f).forEach(function(g){ rows.push({food:f, foodIdx:fi, group:g}); });
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function filteredRows(){
|
||||
return allRows().filter(function(r){
|
||||
var f=r.food, g=r.group;
|
||||
/* 食材名称:下拉选中精确匹配,手工输入模糊匹配 */
|
||||
if(filterState.foodName){
|
||||
if(f.name!==filterState.foodName && f.name.indexOf(filterState.foodName)<0) return false;
|
||||
}
|
||||
if(filterState.foodCode && f.code.indexOf(filterState.foodCode)<0) return false;
|
||||
/* 净菜类型关键字:命中 净菜俗名/净菜标准名/规格俗名 任一即保留 */
|
||||
if(filterState.vegKeyword){
|
||||
var kw=filterState.vegKeyword, hit=false;
|
||||
if((g.alias||'').indexOf(kw)>=0 || (g.stdName||'').indexOf(kw)>=0) hit=true;
|
||||
if(!hit){ hit=g.items.some(function(s){ return (s.specAlias||'').indexOf(kw)>=0; }); }
|
||||
if(!hit) return false;
|
||||
}
|
||||
if(filterState.shape && g.shape!==filterState.shape) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/* === 统计卡片 === */
|
||||
function renderStats(){
|
||||
var rows=allRows(), specCount=0, foodSet={}, yieldSum=0;
|
||||
rows.forEach(function(r){
|
||||
specCount += r.group.items.length;
|
||||
foodSet[r.food.code]=1;
|
||||
if(r.group.yieldRate!=='') yieldSum += parseInt(r.group.yieldRate,10);
|
||||
});
|
||||
document.getElementById('statGroupCount').textContent = rows.length;
|
||||
document.getElementById('statFoodCount').textContent = Object.keys(foodSet).length;
|
||||
document.getElementById('statSpecCount').textContent = specCount;
|
||||
document.getElementById('statAvgYield').textContent = rows.length ? (Math.round(yieldSum/rows.length*10)/10)+'%' : '—';
|
||||
}
|
||||
|
||||
/* === 分页列表 === */
|
||||
function renderList(){
|
||||
var rows = filteredRows();
|
||||
var totalPages = Math.max(1, Math.ceil(rows.length/pageSize));
|
||||
if(curPage>totalPages) curPage=totalPages;
|
||||
var start=(curPage-1)*pageSize;
|
||||
var pageRows = rows.slice(start,start+pageSize);
|
||||
var html='';
|
||||
if(pageRows.length===0){
|
||||
html='<tr class="empty-row"><td colspan="11">暂无符合条件的净菜类型,点击右上角「新增净菜」创建</td></tr>';
|
||||
} else {
|
||||
pageRows.forEach(function(r,i){
|
||||
var g=r.group, items=g.items, rs=items.length;
|
||||
/* 每个规格占一个表格行:组级列首行 rowspan 合并,规格行级列逐行展示 */
|
||||
items.forEach(function(s,si){
|
||||
var innerCls = si<rs-1 ? ' td-spec--inner' : '';
|
||||
html+='<tr>';
|
||||
if(si===0){
|
||||
html+='<td rowspan="'+rs+'">'+(start+i+1)+'</td>';
|
||||
html+='<td class="td-food" rowspan="'+rs+'"><div class="food-name">'+r.food.name+'</div><div class="food-code">'+r.food.code+'</div></td>';
|
||||
html+='<td class="veg-std-name" rowspan="'+rs+'">'+g.stdName+'</td>';
|
||||
html+='<td rowspan="'+rs+'">'+(g.alias||'—')+'</td>';
|
||||
html+='<td rowspan="'+rs+'"><span class="tag tag-blue">'+(g.shape||'—')+'</span></td>';
|
||||
html+='<td rowspan="'+rs+'">'+(g.yieldRate!==''?g.yieldRate+'%':'—')+'</td>';
|
||||
}
|
||||
/* 可食部/规格别名/切配规格/刀工 四个行级字段,每规格一行 */
|
||||
html+='<td class="td-spec td-spec-start'+innerCls+'">'+(s.edibleRate?s.edibleRate+'%':'—')+'</td>';
|
||||
html+='<td class="td-spec'+innerCls+'">'+(s.specAlias||'—')+'</td>';
|
||||
html+='<td class="td-spec'+innerCls+'">'+formatCutDims(s.cutSpec)+'</td>';
|
||||
html+='<td class="td-spec'+innerCls+'">'+((s.cutSpec&&s.cutSpec.technique)||'—')+'</td>';
|
||||
if(si===0){
|
||||
html+='<td class="td-actions" rowspan="'+rs+'"><span class="link" onclick="openMaintainForFood('+r.foodIdx+')">编辑</span><span class="link" style="color:var(--danger);" onclick="openDeleteVeg('+r.foodIdx+',\''+g.groupId+'\')">删除</span></td>';
|
||||
}
|
||||
html+='</tr>';
|
||||
});
|
||||
});
|
||||
}
|
||||
document.getElementById('vegListBody').innerHTML=html;
|
||||
document.getElementById('totalGroups').textContent=rows.length;
|
||||
document.getElementById('pageInfo').textContent='共 '+rows.length+' 条 · 第 '+curPage+' / '+totalPages+' 页';
|
||||
var btns='<button class="page-btn" onclick="gotoPage('+(curPage-1)+')">‹</button>';
|
||||
for(var p=1;p<=totalPages;p++) btns+='<button class="page-btn'+(p===curPage?' active':'')+'" onclick="gotoPage('+p+')">'+p+'</button>';
|
||||
btns+='<button class="page-btn" onclick="gotoPage('+(curPage+1)+')">›</button>';
|
||||
document.getElementById('pageBtns').innerHTML=btns;
|
||||
}
|
||||
function gotoPage(p){
|
||||
var totalPages=Math.max(1,Math.ceil(filteredRows().length/pageSize));
|
||||
if(p<1||p>totalPages) return;
|
||||
curPage=p; renderList();
|
||||
}
|
||||
|
||||
/* === 筛选 === */
|
||||
/* 食材名称下拉搜索选项(源自 FOODS,去重生成) */
|
||||
function initFoodNameOptions(){
|
||||
var dl=document.getElementById('foodNameOptions');
|
||||
var seen={};
|
||||
FOODS.forEach(function(f){ if(!seen[f.name]){ seen[f.name]=1; var o=document.createElement('option'); o.value=f.name; o.textContent=f.name; dl.appendChild(o); } });
|
||||
}
|
||||
function applyFilter(){
|
||||
filterState.foodName=document.getElementById('fltFoodName').value.trim();
|
||||
filterState.foodCode=document.getElementById('fltFoodCode').value.trim();
|
||||
filterState.vegKeyword=document.getElementById('fltVegKeyword').value.trim();
|
||||
filterState.shape=document.getElementById('fltShape').value;
|
||||
curPage=1; renderList();
|
||||
}
|
||||
function resetFilter(){
|
||||
document.getElementById('fltFoodName').value='';
|
||||
document.getElementById('fltFoodCode').value='';
|
||||
document.getElementById('fltVegKeyword').value='';
|
||||
document.getElementById('fltShape').value='';
|
||||
filterState={foodName:'',foodCode:'',vegKeyword:'',shape:''};
|
||||
curPage=1; renderList();
|
||||
}
|
||||
|
||||
/* === 异步导出(交互对齐列表页惯例:先建任务,再到任务列表查看下载) === */
|
||||
function exportListData(){
|
||||
var n=filteredRows().length;
|
||||
if(n===0){ alert('当前筛选结果无列表数据,未创建导出任务'); return; }
|
||||
alert('导出任务已创建:当前筛选结果 '+n+' 个净菜类型,正在异步生成,点击「查看导出任务」跟进进度');
|
||||
}
|
||||
function viewExportTasks(){
|
||||
alert('原型演示:正式系统在此唤起导出任务列表抽屉(含处理中/已完成/失败状态,完成后提供下载)');
|
||||
}
|
||||
|
||||
/* === 弹窗一:选择食材 === */
|
||||
function openFoodPicker(){
|
||||
pickIdx=-1;
|
||||
document.getElementById('foodPickSearch').value='';
|
||||
document.getElementById('btnPickNext').disabled=true;
|
||||
renderFoodPickList();
|
||||
document.getElementById('foodPickerDialog').classList.add('open');
|
||||
}
|
||||
function closeFoodPicker(){
|
||||
document.getElementById('foodPickerDialog').classList.remove('open');
|
||||
}
|
||||
function renderFoodPickList(){
|
||||
var kw=document.getElementById('foodPickSearch').value.trim();
|
||||
var html=''; var shown=0;
|
||||
FOODS.forEach(function(f,i){
|
||||
if(kw && f.name.indexOf(kw)<0 && f.code.indexOf(kw)<0) return;
|
||||
shown++;
|
||||
var n=collectGroups(f).length;
|
||||
html+='<div class="food-pick__item'+(pickIdx===i?' active':'')+'" onclick="pickFood('+i+')">';
|
||||
html+='<div class="food-pick__icon">'+f.icon+'</div>';
|
||||
html+='<div class="food-pick__info"><div class="food-pick__name">'+f.name+'</div><div class="food-pick__meta"><span>'+f.code+'</span><span>'+f.firstClass+' / '+f.secondClass+'</span></div></div>';
|
||||
html+='<span class="food-pick__count">'+(n>0?'已有 '+n+' 类':'暂无净菜')+'</span>';
|
||||
html+='<span class="food-pick__check">✓</span>';
|
||||
html+='</div>';
|
||||
});
|
||||
if(shown===0) html='<div class="food-pick__empty">未找到匹配食材</div>';
|
||||
document.getElementById('foodPickList').innerHTML=html;
|
||||
}
|
||||
function pickFood(i){
|
||||
pickIdx=i;
|
||||
document.getElementById('btnPickNext').disabled=false;
|
||||
renderFoodPickList();
|
||||
}
|
||||
function confirmPickFood(){
|
||||
if(pickIdx<0) return;
|
||||
currentFood=pickIdx;
|
||||
closeFoodPicker();
|
||||
openMaintain();
|
||||
}
|
||||
|
||||
/* === 弹窗二:净菜类型维护 === */
|
||||
var dlgVegs=[]; /* 弹窗内扁平净菜数据 */
|
||||
|
||||
/* 行编辑入口:直接锁定该食材进入维护(需求:回显已有净菜类型后编辑) */
|
||||
function openMaintainForFood(foodIdx){
|
||||
currentFood=foodIdx;
|
||||
openMaintain();
|
||||
}
|
||||
function switchFood(){
|
||||
closeMaintain();
|
||||
openFoodPicker();
|
||||
}
|
||||
function openMaintain(){
|
||||
var f=FOODS[currentFood];
|
||||
document.getElementById('maintainFoodIcon').textContent=f.icon;
|
||||
document.getElementById('maintainFoodName').textContent=f.name;
|
||||
document.getElementById('maintainFoodCode').textContent=f.code;
|
||||
/* 深拷贝回显该食材已有净菜类型 */
|
||||
dlgVegs = f.vegTypes.map(function(it){
|
||||
return {groupId:it.groupId, alias:it.alias||'', specAlias:it.specAlias||'', yieldRate:it.yieldRate||'', edibleRate:it.edibleRate||'', cutSpec: Object.assign({}, it.cutSpec||{})};
|
||||
});
|
||||
renderDlgVeg();
|
||||
document.getElementById('vegMaintainDialog').classList.add('open');
|
||||
}
|
||||
function closeMaintain(){
|
||||
document.getElementById('vegMaintainDialog').classList.remove('open');
|
||||
}
|
||||
|
||||
/* 由食材名称+切配形状拼接净菜标准名 */
|
||||
function buildVegName(shape){
|
||||
var fn=FOODS[currentFood].name;
|
||||
if(!fn||!shape) return '';
|
||||
return fn+'-'+shape;
|
||||
}
|
||||
function genGroupId(){return 'g'+(groupSeq++);}
|
||||
|
||||
/* 渲染维护表(结构与 din-ingredient-add.html 净菜维护表一致:可食部列位于规格别名之后) */
|
||||
function renderDlgVeg(){
|
||||
var container=document.getElementById('dlgVegRows');
|
||||
var groupKeys=[], groupMap={};
|
||||
dlgVegs.forEach(function(item){
|
||||
if(!groupMap[item.groupId]){groupKeys.push(item.groupId);groupMap[item.groupId]=[];}
|
||||
groupMap[item.groupId].push(item);
|
||||
});
|
||||
var html='<div class="veg-table-wrap"><table class="veg-table"><thead><tr>';
|
||||
html+='<th class="col-name">净菜标准名</th>';
|
||||
html+='<th class="col-alias">净菜俗名</th>';
|
||||
html+='<th class="col-yield">出净率(%)</th>';
|
||||
html+='<th class="col-shape">切配形状</th>';
|
||||
html+='<th class="col-spec-alias">规格别名</th>';
|
||||
html+='<th class="col-yield">可食部(%)</th>';
|
||||
html+='<th class="col-dim">长(mm)</th>';
|
||||
html+='<th class="col-dim">宽(mm)</th>';
|
||||
html+='<th class="col-dim">高(mm)</th>';
|
||||
html+='<th class="col-tech">刀工</th>';
|
||||
html+='<th class="col-action">操作</th>';
|
||||
html+='</tr></thead><tbody>';
|
||||
if(groupKeys.length===0){
|
||||
html+='<tr class="veg-empty-row"><td colspan="11">该食材暂无净菜类型,点击下方"+ 添加净菜"按钮新增</td></tr>';
|
||||
}
|
||||
groupKeys.forEach(function(key){
|
||||
var items=groupMap[key], first=items[0], rs=items.length;
|
||||
var firstCs=first.cutSpec||{};
|
||||
var nameText=buildVegName(firstCs.shape);
|
||||
items.forEach(function(item,idx){
|
||||
var isMaster=idx===0;
|
||||
html+='<tr class="veg-row'+(isMaster?'':' veg-row--spec')+'" data-row-id="'+idx+'" data-group-id="'+key+'">';
|
||||
if(isMaster){
|
||||
var nameClass=nameText?'veg-name':'veg-name veg-name--placeholder';
|
||||
html+='<td class="veg-cell--merged col-name" rowspan="'+rs+'"><span class="'+nameClass+'">'+(nameText||'—')+'</span></td>';
|
||||
html+='<td class="veg-cell--merged col-alias" rowspan="'+rs+'"><input class="form-input veg-group-alias" value="'+(first.alias||'')+'" placeholder="如:细丝组" oninput="dlgGroupField(\''+key+'\',\'alias\',this.value)"></td>';
|
||||
html+='<td class="veg-cell--merged col-yield" rowspan="'+rs+'"><input class="form-input veg-yield" type="number" min="0" max="100" value="'+(first.yieldRate||'')+'" oninput="dlgGroupField(\''+key+'\',\'yieldRate\',this.value)"></td>';
|
||||
html+='<td class="veg-cell--merged col-shape" rowspan="'+rs+'"><select class="form-select veg-shape" onchange="dlgShapeChange(\''+key+'\',this.value)">';
|
||||
VEG_SHAPE_OPTIONS.forEach(function(opt){
|
||||
var sel=(opt && firstCs.shape===opt)?' selected':'';
|
||||
html+='<option value="'+opt+'"'+sel+'>'+(opt||'请选择')+'</option>';
|
||||
});
|
||||
html+='</select></td>';
|
||||
}
|
||||
html+='<td class="col-spec-alias"><input class="form-input veg-spec-alias" value="'+(item.specAlias||'')+'" placeholder="如:细丝" oninput="dlgRowField('+idx+',\''+key+'\',\'specAlias\',this.value)"></td>';
|
||||
/* 可食部为规格行级字段,随各规格独立维护 */
|
||||
html+='<td class="col-yield"><input class="form-input veg-edible" type="number" min="0" max="100" value="'+(item.edibleRate||'')+'" oninput="dlgRowField('+idx+',\''+key+'\',\'edibleRate\',this.value)"></td>';
|
||||
var cs=item.cutSpec||{};
|
||||
html+='<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.length||'')+'" oninput="dlgRowField('+idx+',\''+key+'\',\'length\',this.value)"></td>';
|
||||
html+='<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.width||'')+'" oninput="dlgRowField('+idx+',\''+key+'\',\'width\',this.value)"></td>';
|
||||
html+='<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.height||'')+'" oninput="dlgRowField('+idx+',\''+key+'\',\'height\',this.value)"></td>';
|
||||
html+='<td class="col-tech"><select class="form-select veg-tech" onchange="dlgRowField('+idx+',\''+key+'\',\'technique\',this.value)">';
|
||||
VEG_TECH_OPTIONS.forEach(function(opt){
|
||||
var sel=(opt && cs.technique===opt)?' selected':'';
|
||||
html+='<option value="'+opt+'"'+sel+'>'+(opt||'请选择')+'</option>';
|
||||
});
|
||||
html+='</select></td>';
|
||||
html+='<td class="col-action">';
|
||||
if(isMaster){
|
||||
html+='<button type="button" class="veg-action-btn veg-action-btn--add" onclick="addDlgSpec(\''+key+'\')" title="新增一行规格">+</button>';
|
||||
}
|
||||
html+='<button type="button" class="veg-action-btn veg-action-btn--del" onclick="removeDlgRow('+idx+',\''+key+'\')" title="删除该行">×</button>';
|
||||
html+='</td>';
|
||||
html+='</tr>';
|
||||
});
|
||||
});
|
||||
html+='</tbody></table></div>';
|
||||
container.innerHTML=html;
|
||||
}
|
||||
|
||||
/* 新增净菜(新组) */
|
||||
function addDlgGroup(){
|
||||
saveDlgVeg();
|
||||
dlgVegs.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',edibleRate:'',cutSpec:{}});
|
||||
renderDlgVeg();
|
||||
}
|
||||
/* 在指定净菜组下追加一行规格 */
|
||||
function addDlgSpec(groupId){
|
||||
saveDlgVeg();
|
||||
var master=dlgVegs.find(function(it){return it.groupId===groupId;});
|
||||
var shared={alias:master?master.alias:'',yieldRate:master?master.yieldRate:'',shape:master&&master.cutSpec?master.cutSpec.shape:''};
|
||||
/* 可食部为行级字段,新规格行留空待单独录入 */
|
||||
var newItem={groupId:groupId,alias:shared.alias,specAlias:'',yieldRate:shared.yieldRate,edibleRate:'',cutSpec:{shape:shared.shape}};
|
||||
var lastIdx=-1;
|
||||
for(var i=dlgVegs.length-1;i>=0;i--){ if(dlgVegs[i].groupId===groupId){lastIdx=i;break;} }
|
||||
if(lastIdx>=0) dlgVegs.splice(lastIdx+1,0,newItem);
|
||||
else dlgVegs.push(newItem);
|
||||
renderDlgVeg();
|
||||
}
|
||||
/* 删除单行:同组最后一行则整组消失 */
|
||||
function removeDlgRow(idx,groupId){
|
||||
saveDlgVeg();
|
||||
var sameGroupItems=dlgVegs.filter(function(it){return it.groupId===groupId;});
|
||||
if(idx>=sameGroupItems.length) return;
|
||||
var itemToRemove=sameGroupItems[idx];
|
||||
var pos=dlgVegs.indexOf(itemToRemove);
|
||||
if(pos<0) return;
|
||||
dlgVegs.splice(pos,1);
|
||||
renderDlgVeg();
|
||||
}
|
||||
/* 切配形状变化:同步整组(影响净菜标准名拼接) */
|
||||
function dlgShapeChange(groupId,val){
|
||||
dlgVegs.forEach(function(it){
|
||||
if(it.groupId===groupId){ if(!it.cutSpec) it.cutSpec={}; it.cutSpec.shape=val; }
|
||||
});
|
||||
var masterName=document.querySelector('tr[data-group-id="'+groupId+'"][data-row-id="0"] td.col-name .veg-name');
|
||||
if(masterName){
|
||||
var txt=buildVegName(val);
|
||||
masterName.textContent=txt||'—';
|
||||
masterName.className=txt?'veg-name':'veg-name veg-name--placeholder';
|
||||
}
|
||||
}
|
||||
/* 组级共享字段:净菜俗名/出净率 */
|
||||
function dlgGroupField(groupId,field,val){
|
||||
dlgVegs.forEach(function(it){ if(it.groupId===groupId) it[field]=val; });
|
||||
}
|
||||
/* 行级字段:规格别名/可食部/长宽高/刀工 */
|
||||
function dlgRowField(idx,groupId,field,val){
|
||||
var sameGroupItems=dlgVegs.filter(function(it){return it.groupId===groupId;});
|
||||
if(idx>=sameGroupItems.length) return;
|
||||
var item=sameGroupItems[idx];
|
||||
if(field==='specAlias' || field==='edibleRate'){ item[field]=val; }
|
||||
else if(field==='length'||field==='width'||field==='height'||field==='technique'){
|
||||
if(!item.cutSpec) item.cutSpec={};
|
||||
item.cutSpec[field]=val;
|
||||
}
|
||||
}
|
||||
/* 切换/保存前收集 DOM */
|
||||
function saveDlgVeg(){
|
||||
var newVegs=[];
|
||||
var rows=document.querySelectorAll('#dlgVegRows .veg-table tbody tr.veg-row');
|
||||
var groupShared={};
|
||||
rows.forEach(function(row){
|
||||
var groupId=row.getAttribute('data-group-id');
|
||||
var rowIdx=parseInt(row.getAttribute('data-row-id'),10);
|
||||
var isMaster=rowIdx===0;
|
||||
var item={groupId:groupId};
|
||||
if(isMaster){
|
||||
var aliasInp=row.querySelector('.veg-group-alias');
|
||||
var yieldInp=row.querySelector('.veg-yield');
|
||||
var shapeSel=row.querySelector('.veg-shape');
|
||||
item.alias=aliasInp?aliasInp.value:'';
|
||||
item.yieldRate=yieldInp?yieldInp.value:'';
|
||||
var shape=shapeSel?shapeSel.value:'';
|
||||
item.cutSpec={shape:shape};
|
||||
groupShared[groupId]={alias:item.alias,yieldRate:item.yieldRate,shape:shape};
|
||||
} else {
|
||||
var shared=groupShared[groupId]||{alias:'',yieldRate:'',shape:''};
|
||||
item.alias=shared.alias;
|
||||
item.yieldRate=shared.yieldRate;
|
||||
item.cutSpec={shape:shared.shape};
|
||||
}
|
||||
item.specAlias=row.querySelector('.veg-spec-alias')?row.querySelector('.veg-spec-alias').value:'';
|
||||
/* 可食部为行级字段,逐行收集 */
|
||||
item.edibleRate=row.querySelector('.veg-edible')?row.querySelector('.veg-edible').value:'';
|
||||
var dims=row.querySelectorAll('.veg-dim');
|
||||
var techSel=row.querySelector('.veg-tech');
|
||||
item.cutSpec.length=dims[0]?dims[0].value:'';
|
||||
item.cutSpec.width=dims[1]?dims[1].value:'';
|
||||
item.cutSpec.height=dims[2]?dims[2].value:'';
|
||||
item.cutSpec.technique=techSel?techSel.value:'';
|
||||
newVegs.push(item);
|
||||
});
|
||||
dlgVegs=newVegs;
|
||||
}
|
||||
/* 保存:校验后回写当前食材的 vegTypes,整表替换 */
|
||||
function saveMaintain(){
|
||||
saveDlgVeg();
|
||||
if(dlgVegs.length===0){
|
||||
FOODS[currentFood].vegTypes=[];
|
||||
refresh(); closeMaintain(); alert('净菜类型已保存'); return;
|
||||
}
|
||||
/* 校验:每个净菜组需选定切配形状,且至少一条规格填写规格别名 */
|
||||
var groups={}; var order=[];
|
||||
dlgVegs.forEach(function(it){
|
||||
if(!groups[it.groupId]){groups[it.groupId]=[];order.push(it.groupId);}
|
||||
groups[it.groupId].push(it);
|
||||
});
|
||||
for(var i=0;i<order.length;i++){
|
||||
var items=groups[order[i]];
|
||||
if(!items[0].cutSpec || !items[0].cutSpec.shape){ alert('存在未选择「切配形状」的净菜类型,请补全后再保存'); return; }
|
||||
var hasSpec=items.some(function(it){return (it.specAlias||'').trim()!=='';});
|
||||
if(!hasSpec){ alert('存在未填写任何「规格别名」的净菜类型,请补全后再保存'); return; }
|
||||
}
|
||||
/* 剔除完全空白的组(新增未填),其余回写 */
|
||||
FOODS[currentFood].vegTypes = dlgVegs.filter(function(it){
|
||||
var g=groups[it.groupId][0];
|
||||
return (g.cutSpec&&g.cutSpec.shape) || (it.specAlias||'').trim()!=='';
|
||||
});
|
||||
refresh();
|
||||
closeMaintain();
|
||||
alert('净菜类型已保存');
|
||||
}
|
||||
|
||||
/* === 删除净菜类型 === */
|
||||
function openDeleteVeg(foodIdx,groupId){
|
||||
deleting={foodIdx:foodIdx, groupId:groupId};
|
||||
var f=FOODS[foodIdx];
|
||||
var g=collectGroups(f).find(function(x){return x.groupId===groupId;});
|
||||
document.getElementById('delVegFood').textContent=f.name+'('+f.code+')';
|
||||
document.getElementById('delVegName').textContent=g?g.stdName:groupId;
|
||||
document.getElementById('deleteVegDialog').classList.add('open');
|
||||
}
|
||||
function closeDeleteVeg(){
|
||||
document.getElementById('deleteVegDialog').classList.remove('open');
|
||||
deleting=null;
|
||||
}
|
||||
function confirmDeleteVeg(){
|
||||
if(deleting){
|
||||
var f=FOODS[deleting.foodIdx];
|
||||
f.vegTypes=f.vegTypes.filter(function(it){return it.groupId!==deleting.groupId;});
|
||||
}
|
||||
closeDeleteVeg();
|
||||
refresh();
|
||||
alert('该净菜类型已删除');
|
||||
}
|
||||
|
||||
/* 数据变化后统一刷新 */
|
||||
function refresh(){
|
||||
renderStats();
|
||||
renderList();
|
||||
}
|
||||
|
||||
/* 侧边栏子组展开/收起 */
|
||||
function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
|
||||
|
||||
/* 初始化 */
|
||||
initFoodNameOptions();
|
||||
renderStats();
|
||||
renderList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -577,7 +577,7 @@ function renderVegTypes(){
|
||||
var container = document.getElementById('vegTypeRows');
|
||||
if(!container) return;
|
||||
if(vegTypes.length===0){
|
||||
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
|
||||
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',edibleRate:'',cutSpec:{}});
|
||||
}
|
||||
/* 按 groupId 分组(保持原顺序) */
|
||||
var groupKeys = [];
|
||||
@@ -597,6 +597,7 @@ function renderVegTypes(){
|
||||
html += '<th class="col-yield">出净率(%)</th>';
|
||||
html += '<th class="col-shape">切配形状</th>';
|
||||
html += '<th class="col-spec-alias">规格别名</th>';
|
||||
html += '<th class="col-yield">可食部(%)</th>';
|
||||
html += '<th class="col-dim">长(mm)</th>';
|
||||
html += '<th class="col-dim">宽(mm)</th>';
|
||||
html += '<th class="col-dim">高(mm)</th>';
|
||||
@@ -605,7 +606,7 @@ function renderVegTypes(){
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
if(groupKeys.length===0){
|
||||
html += '<tr class="veg-empty-row"><td colspan="10">暂无净菜类型,点击下方"+ 添加净菜"按钮新增</td></tr>';
|
||||
html += '<tr class="veg-empty-row"><td colspan="11">暂无净菜类型,点击下方"+ 添加净菜"按钮新增</td></tr>';
|
||||
}
|
||||
|
||||
groupKeys.forEach(function(key){
|
||||
@@ -637,8 +638,9 @@ function renderVegTypes(){
|
||||
});
|
||||
html += '</select></td>';
|
||||
}
|
||||
/* 各规格独立列:规格别名/长宽高/刀工 */
|
||||
/* 各规格独立列:规格别名/可食部/长宽高/刀工(可食部随各规格独立维护) */
|
||||
html += '<td class="col-spec-alias"><input class="form-input veg-spec-alias" value="'+(item.specAlias||'')+'" placeholder="如:细丝" oninput="onRowField('+idx+',\''+key+'\',\'specAlias\',this.value)"></td>';
|
||||
html += '<td class="col-yield"><input class="form-input veg-edible" type="number" min="0" max="100" value="'+(item.edibleRate||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'edibleRate\',this.value)"></td>';
|
||||
var cs = item.cutSpec||{};
|
||||
html += '<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.length||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'length\',this.value)"></td>';
|
||||
html += '<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.width||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'width\',this.value)"></td>';
|
||||
@@ -667,7 +669,7 @@ function renderVegTypes(){
|
||||
/* 添加净菜(新组) */
|
||||
function addVegType(){
|
||||
saveVegTypes();
|
||||
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
|
||||
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',edibleRate:'',cutSpec:{}});
|
||||
renderVegTypes();
|
||||
}
|
||||
/* 在指定净菜组下追加一行规格(共用 groupId) */
|
||||
@@ -679,6 +681,7 @@ function addVegSpec(groupId){
|
||||
alias:'',
|
||||
specAlias:'',
|
||||
yieldRate:'',
|
||||
edibleRate:'',
|
||||
cutSpec:{}
|
||||
};
|
||||
/* 紧跟该组最后一项插入 */
|
||||
@@ -701,7 +704,7 @@ function removeVegRow(idx,groupId){
|
||||
if(pos<0) return;
|
||||
vegTypes.splice(pos,1);
|
||||
if(vegTypes.length===0){
|
||||
vegTypes.push({groupId:genGroupId(),shapeCat:'',alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
|
||||
vegTypes.push({groupId:genGroupId(),shapeCat:'',alias:'',specAlias:'',yieldRate:'',edibleRate:'',cutSpec:{}});
|
||||
}
|
||||
renderVegTypes();
|
||||
}
|
||||
@@ -733,7 +736,7 @@ function onRowField(idx,groupId,field,val){
|
||||
var sameGroupItems = vegTypes.filter(function(it){return it.groupId===groupId;});
|
||||
if(idx>=sameGroupItems.length) return;
|
||||
var item = sameGroupItems[idx];
|
||||
if(field==='specAlias'){
|
||||
if(field==='specAlias' || field==='edibleRate'){
|
||||
item[field] = val;
|
||||
} else if(field==='length' || field==='width' || field==='height' || field==='technique'){
|
||||
if(!item.cutSpec) item.cutSpec = {};
|
||||
@@ -769,6 +772,8 @@ function saveVegTypes(){
|
||||
item.cutSpec = {shape:shared.shape};
|
||||
}
|
||||
item.specAlias = row.querySelector('.veg-spec-alias') ? row.querySelector('.veg-spec-alias').value : '';
|
||||
/* 可食部为行级字段,逐行收集 */
|
||||
item.edibleRate = row.querySelector('.veg-edible') ? row.querySelector('.veg-edible').value : '';
|
||||
var dims = row.querySelectorAll('.veg-dim');
|
||||
var techSel = row.querySelector('.veg-tech');
|
||||
item.cutSpec.length = dims[0] ? dims[0].value : '';
|
||||
@@ -777,7 +782,7 @@ function saveVegTypes(){
|
||||
item.cutSpec.technique = techSel ? techSel.value : '';
|
||||
newVegs.push(item);
|
||||
});
|
||||
vegTypes = newVegs.length>0 ? newVegs : [{groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}}];
|
||||
vegTypes = newVegs.length>0 ? newVegs : [{groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',edibleRate:'',cutSpec:{}}];
|
||||
}
|
||||
|
||||
/* 食物名称输入时实时刷新所有净菜标准名 */
|
||||
@@ -938,8 +943,8 @@ if(initTab >= 0 && initTab < totalSteps) currentStep = initTab;
|
||||
/* 编辑模式 mock 数据(与 din-ingredient.html 的 FOODS 结构一致) */
|
||||
var EDIT_FOODS = {
|
||||
'0101001':{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',edible:'97',waterRate:'13.3',dataSource:'0',basePrice:'3.5',vegTypes:[
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',cutSpec:{shape:'原材'}},
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',cutSpec:{shape:'原材'}}
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',edibleRate:'100',cutSpec:{shape:'原材'}},
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'原材'}}
|
||||
],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],
|
||||
nutri:{calorie:346,protein:7.4,fat:0.6,carbohydrate:77.2,dietary_fiber:0.7,
|
||||
@@ -952,9 +957,9 @@ var EDIT_FOODS = {
|
||||
taste:{main:'淡味',sub:'微涩',intensity:'弱',desc:'生米无明显味道,煮熟后清香'},
|
||||
texture:{main:'软糯',chew:'易',digest:'中',desc:'米粒饱满,质地坚硬,煮后软糯',cook:'水米比1:1.2,浸泡30分钟口感更佳'}},
|
||||
'0301001':{name:'猪里脊(生)',code:'0301001',alias:'里脊肉',firstClass:'畜禽肉类',secondClass:'猪肉',edible:'100',waterRate:'73.3',dataSource:'0',basePrice:'18.8',vegTypes:[
|
||||
{groupId:'g1',alias:'丝类组',specAlias:'细丝',yieldRate:'92',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'丝类组',specAlias:'粗丝',yieldRate:'90',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'块类组',specAlias:'方块',yieldRate:'88',cutSpec:{shape:'块',length:15,width:15,height:15,technique:'直刀'}}
|
||||
{groupId:'g1',alias:'丝类组',specAlias:'细丝',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'丝类组',specAlias:'粗丝',yieldRate:'90',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'块类组',specAlias:'方块',yieldRate:'88',edibleRate:'100',cutSpec:{shape:'块',length:15,width:15,height:15,technique:'直刀'}}
|
||||
],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'高血脂人群',level:'适量'}],
|
||||
nutri:{calorie:143,protein:20.2,fat:7.1,carbohydrate:0.7,dietary_fiber:0,
|
||||
@@ -978,8 +983,8 @@ var EDIT_FOODS = {
|
||||
taste:{main:'鲜味',sub:'蛋香',intensity:'中',desc:'蛋白柔和,蛋黄浓郁'},
|
||||
texture:{main:'嫩滑',chew:'易',digest:'易',desc:'水煮蛋弹嫩,炒蛋软嫩,蒸蛋细腻',cook:'蒸蛋口感最软,适合各年龄段'}},
|
||||
'0201001':{name:'菠菜(生)',code:'0201001',alias:'波斯菜',firstClass:'蔬菜类',secondClass:'叶菜类',edible:'89',waterRate:'91.2',dataSource:'0',basePrice:'4.2',vegTypes:[
|
||||
{groupId:'g1',alias:'段类组',specAlias:'净菜',yieldRate:'90',cutSpec:{shape:'原材'}},
|
||||
{groupId:'g1',alias:'段类组',specAlias:'寸段',yieldRate:'88',cutSpec:{shape:'原材',length:50,width:'',height:'',technique:'直刀'}}
|
||||
{groupId:'g1',alias:'段类组',specAlias:'净菜',yieldRate:'90',edibleRate:'89',cutSpec:{shape:'原材'}},
|
||||
{groupId:'g1',alias:'段类组',specAlias:'寸段',yieldRate:'88',edibleRate:'88',cutSpec:{shape:'原材',length:50,width:'',height:'',technique:'直刀'}}
|
||||
],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],
|
||||
nutri:{calorie:28,protein:2.6,fat:0.3,carbohydrate:4.5,dietary_fiber:1.7,
|
||||
@@ -992,8 +997,8 @@ var EDIT_FOODS = {
|
||||
taste:{main:'清甜',sub:'微涩',intensity:'弱',desc:'清爽微甜带涩'},
|
||||
texture:{main:'嫩脆',chew:'易',digest:'易',desc:'叶片嫩滑,茎部脆嫩',cook:'焯水去草酸后口感最佳'}},
|
||||
'0601001':{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',edible:'100',waterRate:'82.8',dataSource:'1',basePrice:'5.0',vegTypes:[
|
||||
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'片类组',specAlias:'薄片',yieldRate:'94',cutSpec:{shape:'片',length:10,width:10,height:3,technique:'平刀'}}
|
||||
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',edibleRate:'100',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'片类组',specAlias:'薄片',yieldRate:'94',edibleRate:'100',cutSpec:{shape:'片',length:10,width:10,height:3,technique:'平刀'}}
|
||||
],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'高血压人群',level:'推荐'},{name:'痛风人群',level:'适量'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],
|
||||
nutri:{calorie:98,protein:12.2,fat:4.8,carbohydrate:1.5,dietary_fiber:0.4,
|
||||
|
||||
@@ -342,6 +342,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
<div class="panel-left__actions">
|
||||
<a href="din-ingredient-add.html" class="btn btn-primary">➕ 新增食物</a>
|
||||
<button class="btn btn-default" onclick="openCategoryDialog()">📂 食材分类管理</button>
|
||||
<a href="din-clean-veg-mgmt.html" class="btn btn-default">🥬 净菜管理</a>
|
||||
</div>
|
||||
<div class="food-list">
|
||||
<div class="food-item" onclick="selectFood(0)">
|
||||
@@ -432,8 +433,8 @@ function formatCutSpecNoShape(cut){
|
||||
// 食物数据(对齐 yx_mater_base_material 表结构)
|
||||
const FOODS = [
|
||||
{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',icon:'🌾',edible:'100',waterRate:'13.3',dataSource:0,basePrice:'3.5',vegTypes:[
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',cutSpec:{shape:'原状'}},
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',cutSpec:{shape:'原状'}}
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',edibleRate:'100',cutSpec:{shape:'原状'}},
|
||||
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'原状'}}
|
||||
],
|
||||
nutri:{calorie:346,protein:7.4,fat:0.8,carbohydrate:77.2,dietary_fiber:0.6,
|
||||
vitamin_a:0,vitamin_b1:0.16,vitamin_b2:0.08,vitamin_b6:0.16,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:0.46,vitamin_k:0,niacin:1.9,folic_acid:3.8,biotin:0,pantothenic_acid:0.6,total_choline:0,
|
||||
@@ -452,10 +453,10 @@ const FOODS = [
|
||||
{type:'编辑',title:'修订色泽描述',time:'2026-07-21 11:05',user:'李建国',role:'营养师',dept:'营养管理部',summary:'色泽描述语义化,补充感官评级。',changes:[{field:'色泽描述',old:'乳白色,半透明',new:'乳白色,半透明,颗粒饱满'},{field:'感官评级',old:'—',new:'优'}]}
|
||||
]},
|
||||
{name:'猪里脊肉',code:'0201001',alias:'里脊',firstClass:'畜禽肉类',secondClass:'猪肉',icon:'🥩',edible:'100',waterRate:'71.4',dataSource:0,basePrice:'18.8',vegTypes:[
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'细丝',yieldRate:'92',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'粗丝',yieldRate:'90',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'里脊肉片',specAlias:'薄片',yieldRate:'95',cutSpec:{shape:'片',length:3,width:1,height:5,technique:'平刀'}},
|
||||
{groupId:'g3',alias:'里脊肉块',specAlias:'方块',yieldRate:'92',cutSpec:{shape:'块',length:20,width:20,height:20,technique:'直刀'}}
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'细丝',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
|
||||
{groupId:'g1',alias:'里脊肉丝',specAlias:'粗丝',yieldRate:'90',edibleRate:'100',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
|
||||
{groupId:'g2',alias:'里脊肉片',specAlias:'薄片',yieldRate:'95',edibleRate:'100',cutSpec:{shape:'片',length:3,width:1,height:5,technique:'平刀'}},
|
||||
{groupId:'g3',alias:'里脊肉块',specAlias:'方块',yieldRate:'92',edibleRate:'100',cutSpec:{shape:'块',length:20,width:20,height:20,technique:'直刀'}}
|
||||
],
|
||||
nutri:{calorie:155,protein:20.2,fat:7.9,carbohydrate:0.7,dietary_fiber:0,
|
||||
vitamin_a:6,vitamin_b1:0.54,vitamin_b2:0.1,vitamin_b6:0.33,vitamin_b12:0.6,vitamin_c:0,vitamin_d:0,vitamin_e:0.34,vitamin_k:0,niacin:5.3,folic_acid:3,biotin:0,pantothenic_acid:0.8,total_choline:0,
|
||||
@@ -507,8 +508,8 @@ const FOODS = [
|
||||
{type:'编辑',title:'补充碘含量',time:'2026-07-04 14:50',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'补充碘元素数据,完善矿物质维度。',changes:[{field:'碘',old:'—',new:'27.2 μg'}]}
|
||||
]},
|
||||
{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',icon:'🧈',edible:'100',waterRate:'82.8',dataSource:1,basePrice:'5.0',vegTypes:[
|
||||
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
|
||||
{groupId:'g1',alias:'块类组',specAlias:'中方块',yieldRate:'98',cutSpec:{shape:'块',length:20,width:20,height:15,technique:'直刀'}}
|
||||
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',edibleRate:'100',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
|
||||
{groupId:'g1',alias:'块类组',specAlias:'中方块',yieldRate:'98',edibleRate:'100',cutSpec:{shape:'块',length:20,width:20,height:15,technique:'直刀'}}
|
||||
],
|
||||
nutri:{calorie:98,protein:12.2,fat:4.8,carbohydrate:1.5,dietary_fiber:0.4,
|
||||
vitamin_a:0,vitamin_b1:0.07,vitamin_b2:0.04,vitamin_b6:0.05,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:2.71,vitamin_k:0,niacin:0.1,folic_acid:37,biotin:0,pantothenic_acid:0.1,total_choline:0,
|
||||
@@ -604,6 +605,8 @@ function renderBasicInfo(f){
|
||||
var cutShape = cs.shape || '—';
|
||||
var cutSpecTxt = formatCutSpecNoShape(item.cutSpec);
|
||||
var specAlias = item.specAlias || '—';
|
||||
/* 可食部为规格行级字段,随各规格独立维护 */
|
||||
var edibleRate = (item.edibleRate!==undefined && item.edibleRate!=='') ? item.edibleRate+'%' : '—';
|
||||
if(si===0){
|
||||
rows += `<tr>`;
|
||||
rows += `<td class="veg-type__name" rowspan="${rs}">${vegName}</td>`;
|
||||
@@ -612,16 +615,18 @@ function renderBasicInfo(f){
|
||||
rows += `<td rowspan="${rs}">${cutShape}</td>`;
|
||||
rows += `<td>${cutSpecTxt}</td>`;
|
||||
rows += `<td class="veg-type__specalias">${specAlias}</td>`;
|
||||
rows += `<td>${edibleRate}</td>`;
|
||||
rows += `</tr>`;
|
||||
} else {
|
||||
rows += `<tr>`;
|
||||
rows += `<td>${cutSpecTxt}</td>`;
|
||||
rows += `<td class="veg-type__specalias">${specAlias}</td>`;
|
||||
rows += `<td>${edibleRate}</td>`;
|
||||
rows += `</tr>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜标准名</th><th>净菜俗名</th><th>出净率</th><th>切配形状</th><th>切配规格</th><th>规格别名</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜标准名</th><th>净菜俗名</th><th>出净率</th><th>切配形状</th><th>切配规格</th><th>规格别名</th><th>可食部(%)</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
} else {
|
||||
vegTypesValue = '—';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# 营养用餐管理端 CHANGELOG
|
||||
|
||||
## [2026-09-07] feat: 净菜类型拆分独立管理(新增净菜管理页 + 规格行级可食部字段)
|
||||
|
||||
- `din-ingredient.html` 食物成分:
|
||||
- 左侧列表操作区「食材分类管理」后新增「净菜管理」按钮,跳转新二级页
|
||||
- 详情净菜类型表「规格别名」后新增「可食部(%)」列:规格行级字段,每个规格独立显示,不随组合并行合并
|
||||
- `din-ingredient-add.html` 新增/编辑食物:
|
||||
- 净菜类型维护表「规格别名」后新增「可食部(%)」列,逐规格独立录入,新增规格行默认空值不继承
|
||||
- 新增 `din-clean-veg-mgmt.html` 净菜管理页:
|
||||
- 统计卡片:净菜类型总数/覆盖食材数/切配规格总数/平均出净率
|
||||
- 筛选区:食材名称(下拉搜索框,选中精确/手输模糊)、食材编码、净菜类型(命中净菜俗名/净菜标准名/规格俗名任一)、切配形状(不设食物分类筛选)
|
||||
- 操作区:新增净菜 + 异步导出按钮组「导出列表数据」「查看导出任务」(样式对齐 farm-purchase 等列表页惯例,空结果不建任务)
|
||||
- 分页列表:组级列(序号/食材/净菜标准名/净菜俗名/切配形状/出净率/操作)rowspan 合并;可食部(%)/规格别名/切配规格/刀工四列按规格逐行拆分(切配规格仅留尺寸、刀工独立成列),组内规格间虚线分隔、规格列组左侧纵向分隔线,组间实线
|
||||
- 新增流程:先弹食材选择器(关键字搜索)→ 选定后回显该食材已有净菜类型 → 在其基础上新增或编辑;维护表结构对齐新增食物页(含行级可食部);支持按组删除(确认弹窗)
|
||||
- `index.html` SITE_MAP「营养用餐」新增「净菜管理」条目
|
||||
|
||||
## [2026-09-01] feat: 餐线营养秤四Tab分页 + 日期默认当天 + 人脸识别数据更名视频数据
|
||||
|
||||
- `din-terminal.html` 设备概览「餐线营养秤」:
|
||||
|
||||
Reference in New Issue
Block a user