feat: 规格体系统一化阶段四设备端 + 阶段五辅助页改造
- 设备端 9 任务 14 文件:毛菜验收/加工秤、净菜组配/包装秤、净菜加工秤设置统一为克存储 + formatGram 展示层兼容 + cutSpec/packSpec 结构化 - 辅助页 3 任务 6 文件:质控同步真落地 + 菜品下拉跨页共享 + 同步日志驱动 + 分析页字典语义对齐 + 净菜进柜 spec 多形态结构化 - 新增 spec-utils-reference.md 权威源、改造任务清单与研发同步说明 - 删除旧的部门下周工作计划文档 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2b8a35d5a3
commit
4fd44f1005
@@ -228,6 +228,80 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
/* 规格标签格式化:{baseGram:500, unit:'袋'} → "500g/袋" */
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (!spec.baseGram) return spec.unit || '—';
|
||||
var w = formatGram(spec.baseGram);
|
||||
if (spec.unit === '散装') return '散装/' + w;
|
||||
return w + '/' + (spec.unit || '');
|
||||
}
|
||||
/* 切配规格展示:{length,width,height,shape,technique} → "20×1×1mm · 丝 · 平刀" */
|
||||
function formatCutSpec(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);
|
||||
});
|
||||
var dimStr = dims.length ? dims.join('×') + 'mm · ' : '';
|
||||
var shapeStr = cut.shape || '';
|
||||
var techStr = cut.technique ? ' · ' + cut.technique : '';
|
||||
return dimStr + shapeStr + techStr;
|
||||
}
|
||||
/* 历史切配规格字符串解析(兼容 4 种格式)
|
||||
* "3.5mm*3.5mm*50mm·细丝·平刀" (无空格,* 分隔)
|
||||
* "20mm*1mm*1mm · 细丝 · 平刀" (带空格,* 分隔)
|
||||
* "20mm×1mm×1mm · 细丝 · 平刀" (带空格,× 分隔)
|
||||
* "50mm*30mm*30mm · 宽段 · 平刀" (ratio-scale 同 02-labeling) */
|
||||
function parseLegacyCutSpec(s) {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'object') return s;
|
||||
var normalized = String(s).replace(/×/g, '*').replace(/\s/g, '');
|
||||
var m = normalized.match(/^(?:(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm)?·?([^·]+)·?([^·]+)?$/);
|
||||
if (!m) return { shape: s };
|
||||
return {
|
||||
length: m[1] ? parseFloat(m[1]) : undefined,
|
||||
width: m[2] ? parseFloat(m[2]) : undefined,
|
||||
height: m[3] ? parseFloat(m[3]) : undefined,
|
||||
shape: m[4] || '',
|
||||
technique: m[5] || ''
|
||||
};
|
||||
}
|
||||
/* 历史包装规格字符串解析:'500g' / '大份800g' / '真空包装'
|
||||
* 返回 {baseGram, unit, packMethod, portion?} */
|
||||
function parseLegacyPackSpec(s) {
|
||||
if (!s) return { baseGram: 0, unit: '', packMethod: '' };
|
||||
if (typeof s === 'object') return s;
|
||||
var str = String(s).trim();
|
||||
/* 纯描述(无数字):如"真空包装"、"盒装" → 视为 packMethod */
|
||||
if (!/\d/.test(str)) {
|
||||
return { baseGram: 0, unit: '', packMethod: str };
|
||||
}
|
||||
/* 提取份型前缀(可选)+ 克数 */
|
||||
var m = str.match(/^(?:(标准份|小份|大份|迷你份|加大份))?(?:(\d+(?:\.\d+)?)\s*(g|kg))?\/?(袋|盒|箱|盘|份|筐|瓶|罐|桶|散装)?$/);
|
||||
if (!m) return { baseGram: 0, unit: '', packMethod: str };
|
||||
var baseGram = 0;
|
||||
if (m[2]) {
|
||||
baseGram = parseFloat(m[2]);
|
||||
if (m[3] === 'kg') baseGram *= 1000;
|
||||
}
|
||||
return {
|
||||
baseGram: baseGram,
|
||||
unit: m[4] || '',
|
||||
portion: m[1] || '',
|
||||
packMethod: ''
|
||||
};
|
||||
}
|
||||
|
||||
/* 获取 URL 参数 */
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -245,7 +319,12 @@
|
||||
/* 当前 Tab */
|
||||
var currentFilter = 'pending';
|
||||
|
||||
/* 模拟数据:待打印订单(来自配比秤已完成配比的订单) */
|
||||
/* 模拟数据:待打印订单(来自配比秤已完成配比的订单)
|
||||
* 字段说明:
|
||||
* cutSpec: 结构化对象 {length,width,height,shape,technique}(任务 4.4)
|
||||
* packSpec: 结构化对象 {baseGram, unit, portion?, packMethod?}(任务 4.4)
|
||||
* packMethod: 独立字段"真空袋/保鲜盒/散装托盘"等(任务 4.4)
|
||||
*/
|
||||
var mockData = [
|
||||
{
|
||||
orderNo: 'PO-20260625-001',
|
||||
@@ -254,9 +333,9 @@
|
||||
taskNo: 'L-001',
|
||||
labeled: false,
|
||||
mealPacks: [
|
||||
{ type: '净菜包', packName: '土豆', packSize: 3, packSpec: '500g', vegCutType: '3.5mm*3.5mm*50mm·细丝·平刀', ingredients: [{ name: '土豆', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '净菜包', packName: '胡萝卜', packSize: 3, packSpec: '500g', vegCutType: '3mm*3mm*30mm·菱形片·斜刀', ingredients: [{ name: '胡萝卜', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 2, packSpec: '大份800g', ingredients: [{ name: '宫保鸡丁', weightPerPack: 800, weighed: 1600 }] }
|
||||
{ type: '净菜包', packName: '土豆', packSize: 3, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:50, width:3.5, height:3.5, shape:'细丝', technique:'平刀'}, ingredients: [{ name: '土豆', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '净菜包', packName: '胡萝卜', packSize: 3, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:30, width:3, height:3, shape:'菱形片', technique:'斜刀'}, ingredients: [{ name: '胡萝卜', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 2, packSpec: {baseGram:800, unit:'份', portion:'大份'}, ingredients: [{ name: '宫保鸡丁', weightPerPack: 800, weighed: 1600 }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -266,8 +345,8 @@
|
||||
taskNo: 'L-002',
|
||||
labeled: false,
|
||||
mealPacks: [
|
||||
{ type: '净菜包', packName: '白菜', packSize: 2, packSpec: '1000g', vegCutType: '5mm*5mm*50mm·段切·平刀', ingredients: [{ name: '白菜', weightPerPack: 1000, weighed: 2000 }] },
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: '大份800g', ingredients: [{ name: '鱼香肉丝', weightPerPack: 800, weighed: 1600 }] }
|
||||
{ type: '净菜包', packName: '白菜', packSize: 2, packSpec: {baseGram:1000, unit:'袋'}, cutSpec: {length:50, width:5, height:5, shape:'段切', technique:'平刀'}, ingredients: [{ name: '白菜', weightPerPack: 1000, weighed: 2000 }] },
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: {baseGram:800, unit:'份', portion:'大份'}, ingredients: [{ name: '鱼香肉丝', weightPerPack: 800, weighed: 1600 }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -277,12 +356,12 @@
|
||||
taskNo: 'L-003',
|
||||
labeled: false,
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: '真空包装', ingredients: [{ name: '番茄炒蛋', weightPerPack: 500, weighed: 2000 }] }
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: {baseGram:500, unit:'份'}, packMethod: '真空袋', ingredients: [{ name: '番茄炒蛋', weightPerPack: 500, weighed: 2000 }] }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/* 待发货订单(已包装完成,等待发货) */
|
||||
/* 待发货订单(已包装完成,等待发货) */
|
||||
var mockDoneData = [
|
||||
{
|
||||
orderNo: 'PO-20260624-001',
|
||||
@@ -294,8 +373,8 @@
|
||||
operator: '刘九',
|
||||
shipped: false,
|
||||
mealPacks: [
|
||||
{ type: '净菜包', packName: '黄瓜', packSize: 3, packSpec: '500g', vegCutType: '2mm*2mm*40mm·薄片·平刀', ingredients: [{ name: '黄瓜', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 2, packSpec: '盒装', ingredients: [{ name: '凉拌黄瓜', weightPerPack: 600, weighed: 1200 }] }
|
||||
{ type: '净菜包', packName: '黄瓜', packSize: 3, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:40, width:2, height:2, shape:'薄片', technique:'平刀'}, ingredients: [{ name: '黄瓜', weightPerPack: 500, weighed: 1500 }] },
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 2, packSpec: {baseGram:600, unit:'盒'}, packMethod: '保鲜盒', ingredients: [{ name: '凉拌黄瓜', weightPerPack: 600, weighed: 1200 }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -308,8 +387,8 @@
|
||||
operator: '陈十',
|
||||
shipped: false,
|
||||
mealPacks: [
|
||||
{ type: '净菜包', packName: '芹菜', packSize: 2, packSpec: '500g', vegCutType: '4mm*4mm*30mm·条·平刀', ingredients: [{ name: '芹菜', weightPerPack: 500, weighed: 1000 }] },
|
||||
{ type: '餐品净菜包', packName: '芹菜炒肉套餐', packSize: 3, packSpec: '大份800g', ingredients: [{ name: '芹菜炒肉', weightPerPack: 800, weighed: 2400 }] }
|
||||
{ type: '净菜包', packName: '芹菜', packSize: 2, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:30, width:4, height:4, shape:'条', technique:'平刀'}, ingredients: [{ name: '芹菜', weightPerPack: 500, weighed: 1000 }] },
|
||||
{ type: '餐品净菜包', packName: '芹菜炒肉套餐', packSize: 3, packSpec: {baseGram:800, unit:'份', portion:'大份'}, ingredients: [{ name: '芹菜炒肉', weightPerPack: 800, weighed: 2400 }] }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -330,8 +409,8 @@
|
||||
shippingCompany: '顺丰速运',
|
||||
shippingNo: 'SF1234567890',
|
||||
mealPacks: [
|
||||
{ type: '净菜包', packName: '西红柿', packSize: 5, packSpec: '500g', vegCutType: '15mm*15mm*15mm·滚刀块·平刀', ingredients: [{ name: '西红柿', weightPerPack: 500, weighed: 2500 }] },
|
||||
{ type: '餐品净菜包', packName: '番茄牛腩套餐', packSize: 3, packSpec: '大份800g', ingredients: [{ name: '番茄牛腩', weightPerPack: 800, weighed: 2400 }] }
|
||||
{ type: '净菜包', packName: '西红柿', packSize: 5, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:15, width:15, height:15, shape:'滚刀块', technique:'平刀'}, ingredients: [{ name: '西红柿', weightPerPack: 500, weighed: 2500 }] },
|
||||
{ type: '餐品净菜包', packName: '番茄牛腩套餐', packSize: 3, packSpec: {baseGram:800, unit:'份', portion:'大份'}, ingredients: [{ name: '番茄牛腩', weightPerPack: 800, weighed: 2400 }] }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -425,11 +504,18 @@
|
||||
? { text: '待包装', cls: 'status-tag--wait' }
|
||||
: { text: '已打印', cls: 'status-tag--done' };
|
||||
var ingWeight = pack.ingredients[0] ? pack.ingredients[0].weightPerPack : 0;
|
||||
var specMatch = pack.packSpec && pack.packSpec.match(/(\d+)g/);
|
||||
var perDue = specMatch ? parseInt(specMatch[1]) : ingWeight;
|
||||
/* 解析 packSpec:已是对象直接用,字符串则 parseLegacyPackSpec */
|
||||
var specObj = typeof pack.packSpec === 'object'
|
||||
? pack.packSpec
|
||||
: parseLegacyPackSpec(pack.packSpec);
|
||||
var perDue = specObj.baseGram || ingWeight;
|
||||
var totalDue = perDue * pack.packSize;
|
||||
var totalWeighed = (currentFilter === 'done' || currentFilter === 'shipped') ? totalDue : (pack.weighed || 0);
|
||||
var weightClass = (currentFilter === 'done' || currentFilter === 'shipped') ? 'parent-weight--done' : '';
|
||||
/* 解析 cutSpec:已是对象直接用,字符串则 parseLegacyCutSpec */
|
||||
var cutObj = pack.cutSpec
|
||||
? (typeof pack.cutSpec === 'object' ? pack.cutSpec : parseLegacyCutSpec(pack.cutSpec))
|
||||
: null;
|
||||
|
||||
var parentRow = document.createElement('div');
|
||||
parentRow.className = 'item-parent-row';
|
||||
@@ -448,8 +534,8 @@
|
||||
parentRow.innerHTML =
|
||||
'<div class="parent-name"><div class="parent-icon">📦</div><span class="parent-name__text">' + pack.packName + '</span></div>' +
|
||||
'<div style="text-align:center;font-size:11px;color:' + (pack.type === '餐品净菜包' ? '#faad16' : '#4a90d9') + '">' + pack.type + '</div>' +
|
||||
'<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.55)">' + (pack.packSpec || '—') + '</div>' +
|
||||
'<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.55)">' + (pack.vegCutType || '—') + '</div>' +
|
||||
'<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.55)">' + formatSpec(specObj) + '</div>' +
|
||||
'<div style="text-align:center;font-size:11px;color:rgba(255,255,255,0.55)">' + formatCutSpec(cutObj) + '</div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalDue + ' g</div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalWeighed + ' g</div>' +
|
||||
statusCell +
|
||||
|
||||
@@ -348,6 +348,63 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (!spec.baseGram) return spec.unit || '—';
|
||||
var w = formatGram(spec.baseGram);
|
||||
if (spec.unit === '散装') return '散装/' + w;
|
||||
return w + '/' + (spec.unit || '');
|
||||
}
|
||||
function formatCutSpec(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);
|
||||
});
|
||||
var dimStr = dims.length ? dims.join('×') + 'mm · ' : '';
|
||||
var shapeStr = cut.shape || '';
|
||||
var techStr = cut.technique ? ' · ' + cut.technique : '';
|
||||
return dimStr + shapeStr + techStr;
|
||||
}
|
||||
/* 历史切配规格字符串解析(兼容 4 种格式) */
|
||||
function parseLegacyCutSpec(s) {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'object') return s;
|
||||
var normalized = String(s).replace(/×/g, '*').replace(/\s/g, '');
|
||||
var m = normalized.match(/^(?:(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm)?·?([^·]+)·?([^·]+)?$/);
|
||||
if (!m) return { shape: s };
|
||||
return {
|
||||
length: m[1] ? parseFloat(m[1]) : undefined,
|
||||
width: m[2] ? parseFloat(m[2]) : undefined,
|
||||
height: m[3] ? parseFloat(m[3]) : undefined,
|
||||
shape: m[4] || '',
|
||||
technique: m[5] || ''
|
||||
};
|
||||
}
|
||||
function parseLegacyPackSpec(s) {
|
||||
if (!s) return { baseGram: 0, unit: '', packMethod: '' };
|
||||
if (typeof s === 'object') return s;
|
||||
var str = String(s).trim();
|
||||
if (!/\d/.test(str)) return { baseGram: 0, unit: '', packMethod: str };
|
||||
var m = str.match(/^(?:(标准份|小份|大份|迷你份|加大份))?(?:(\d+(?:\.\d+)?)\s*(g|kg))?\/?(袋|盒|箱|盘|份|筐|瓶|罐|桶|散装)?$/);
|
||||
if (!m) return { baseGram: 0, unit: '', packMethod: str };
|
||||
var baseGram = 0;
|
||||
if (m[2]) {
|
||||
baseGram = parseFloat(m[2]);
|
||||
if (m[3] === 'kg') baseGram *= 1000;
|
||||
}
|
||||
return { baseGram: baseGram, unit: m[4] || '', portion: m[1] || '', packMethod: '' };
|
||||
}
|
||||
|
||||
/* ---- 工具函数 ---- */
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -363,30 +420,35 @@
|
||||
document.getElementById('topUser').textContent = userName;
|
||||
document.getElementById('userAvatar').textContent = userName.charAt(0);
|
||||
|
||||
/* ---- 模拟订单数据(来自配比秤已完成配比的订单) ---- */
|
||||
/* ---- 模拟订单数据(来自配比秤已完成配比的订单) ----
|
||||
* 字段说明:
|
||||
* cutSpec: 结构化对象 {length,width,height,shape,technique}(任务 4.4)
|
||||
* packSpec: 结构化对象 {baseGram, unit, portion?, packMethod?}(任务 4.4)
|
||||
* packMethod: 包装方式(独立字段,"真空袋/保鲜盒/散装托盘")
|
||||
*/
|
||||
var taskData = {
|
||||
'L-001': {
|
||||
orderNo: 'PO-20260625-001',
|
||||
orderType: 'internal',
|
||||
items: [
|
||||
{ type: '净菜包', packName: '土豆', packSize: 3, packSpec: '500g', cutSpec: '20mm*1mm*1mm · 细丝 · 平刀', weightPerPack: 500, weighed: 1500, icon: '🥔' },
|
||||
{ type: '净菜包', packName: '胡萝卜', packSize: 3, packSpec: '500g', cutSpec: '20mm*1mm*1mm · 细丝 · 平刀', weightPerPack: 500, weighed: 1500, icon: '🥕' },
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 2, packSpec: '大份800g', weightPerPack: 800, weighed: 1600, icon: '🍱' }
|
||||
{ type: '净菜包', packName: '土豆', packSize: 3, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, weightPerPack: 500, weighed: 1500, icon: '🥔' },
|
||||
{ type: '净菜包', packName: '胡萝卜', packSize: 3, packSpec: {baseGram:500, unit:'袋'}, cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, weightPerPack: 500, weighed: 1500, icon: '🥕' },
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 2, packSpec: {baseGram:800, unit:'份', portion:'大份'}, weightPerPack: 800, weighed: 1600, icon: '🍱' }
|
||||
]
|
||||
},
|
||||
'L-002': {
|
||||
orderNo: 'PO-20260625-002',
|
||||
orderType: 'external',
|
||||
items: [
|
||||
{ type: '净菜包', packName: '白菜', packSize: 2, packSpec: '1000g', cutSpec: '50mm*30mm*30mm · 宽段 · 平刀', weightPerPack: 1000, weighed: 2000, icon: '🥬' },
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: '大份800g', weightPerPack: 800, weighed: 1600, icon: '🍱' }
|
||||
{ type: '净菜包', packName: '白菜', packSize: 2, packSpec: {baseGram:1000, unit:'袋'}, cutSpec: {length:50, width:30, height:30, shape:'宽段', technique:'平刀'}, weightPerPack: 1000, weighed: 2000, icon: '🥬' },
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: {baseGram:800, unit:'份', portion:'大份'}, weightPerPack: 800, weighed: 1600, icon: '🍱' }
|
||||
]
|
||||
},
|
||||
'L-003': {
|
||||
orderNo: 'PO-20260625-003',
|
||||
orderType: 'internal',
|
||||
items: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: '真空包装', weightPerPack: 500, weighed: 2000, icon: '🍱' }
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: {baseGram:500, unit:'份'}, packMethod: '真空袋', weightPerPack: 500, weighed: 2000, icon: '🍱' }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -502,8 +564,8 @@
|
||||
'<div class="item-row__action">' + actionHtml + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="item-card__meta">' +
|
||||
'<div class="item-card__meta-item"><span class="item-card__meta-label">包装规格:</span><span class="item-card__meta-value">' + (item.packSpec || '—') + '</span></div>' +
|
||||
'<div class="item-card__meta-item"><span class="item-card__meta-label">净菜类型:</span><span class="item-card__meta-value">' + (isMeal ? '—' : (item.cutSpec || '—')) + '</span></div>' +
|
||||
'<div class="item-card__meta-item"><span class="item-card__meta-label">包装规格:</span><span class="item-card__meta-value">' + formatSpec(typeof item.packSpec === 'object' ? item.packSpec : parseLegacyPackSpec(item.packSpec)) + '</span></div>' +
|
||||
'<div class="item-card__meta-item"><span class="item-card__meta-label">净菜类型:</span><span class="item-card__meta-value">' + (isMeal ? '—' : formatCutSpec(typeof item.cutSpec === 'object' ? item.cutSpec : parseLegacyCutSpec(item.cutSpec))) + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="item-card__data">' +
|
||||
'<div class="item-card__data-item"><span class="item-card__data-label">应称重量:</span><span class="item-card__data-value">' + totalDue + 'g</span></div>' +
|
||||
@@ -568,7 +630,7 @@
|
||||
list.innerHTML = '';
|
||||
empty.classList.add('hide');
|
||||
|
||||
/* 构建净菜类型映射 */
|
||||
/* 构建净菜类型映射(结构化 cutSpec → formatCutSpec 渲染) */
|
||||
var cutSpecMap = {};
|
||||
allItems.forEach(function(item) {
|
||||
if (item.cutSpec) cutSpecMap[item.packName] = item.cutSpec;
|
||||
@@ -576,7 +638,8 @@
|
||||
|
||||
selected.forEach(function(key) {
|
||||
var r = aiResults[key];
|
||||
var cutSpec = cutSpecMap[r.name] || '';
|
||||
var rawCutSpec = cutSpecMap[r.name];
|
||||
var cutSpec = rawCutSpec ? formatCutSpec(typeof rawCutSpec === 'object' ? rawCutSpec : parseLegacyCutSpec(rawCutSpec)) : '';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'candidate-card';
|
||||
card.onclick = function() { confirmCandidateDirect(r); };
|
||||
@@ -702,14 +765,15 @@
|
||||
title.textContent = '搜索结果 (' + results.length + ')';
|
||||
list.innerHTML = '';
|
||||
empty.classList.add('hide');
|
||||
/* 构建净菜类型映射 */
|
||||
/* 构建净菜类型映射(结构化 cutSpec → formatCutSpec 渲染) */
|
||||
var cutSpecMap2 = {};
|
||||
allItems.forEach(function(item) {
|
||||
if (item.cutSpec) cutSpecMap2[item.packName] = item.cutSpec;
|
||||
});
|
||||
|
||||
results.forEach(function(r) {
|
||||
var cutSpec2 = cutSpecMap2[r.name] || '';
|
||||
var rawCutSpec2 = cutSpecMap2[r.name];
|
||||
var cutSpec2 = rawCutSpec2 ? formatCutSpec(typeof rawCutSpec2 === 'object' ? rawCutSpec2 : parseLegacyCutSpec(rawCutSpec2)) : '';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'candidate-card';
|
||||
card.onclick = function() { confirmCandidateDirect(r); };
|
||||
|
||||
@@ -172,6 +172,62 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (!spec.baseGram) return spec.unit || '—';
|
||||
var w = formatGram(spec.baseGram);
|
||||
if (spec.unit === '散装') return '散装/' + w;
|
||||
return w + '/' + (spec.unit || '');
|
||||
}
|
||||
function formatCutSpec(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);
|
||||
});
|
||||
var dimStr = dims.length ? dims.join('×') + 'mm · ' : '';
|
||||
var shapeStr = cut.shape || '';
|
||||
var techStr = cut.technique ? ' · ' + cut.technique : '';
|
||||
return dimStr + shapeStr + techStr;
|
||||
}
|
||||
function parseLegacyCutSpec(s) {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'object') return s;
|
||||
var normalized = String(s).replace(/×/g, '*').replace(/\s/g, '');
|
||||
var m = normalized.match(/^(?:(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm)?·?([^·]+)·?([^·]+)?$/);
|
||||
if (!m) return { shape: s };
|
||||
return {
|
||||
length: m[1] ? parseFloat(m[1]) : undefined,
|
||||
width: m[2] ? parseFloat(m[2]) : undefined,
|
||||
height: m[3] ? parseFloat(m[3]) : undefined,
|
||||
shape: m[4] || '',
|
||||
technique: m[5] || ''
|
||||
};
|
||||
}
|
||||
function parseLegacyPackSpec(s) {
|
||||
if (!s) return { baseGram: 0, unit: '', packMethod: '' };
|
||||
if (typeof s === 'object') return s;
|
||||
var str = String(s).trim();
|
||||
if (!/\d/.test(str)) return { baseGram: 0, unit: '', packMethod: str };
|
||||
var m = str.match(/^(?:(标准份|小份|大份|迷你份|加大份))?(?:(\d+(?:\.\d+)?)\s*(g|kg))?\/?(袋|盒|箱|盘|份|筐|瓶|罐|桶|散装)?$/);
|
||||
if (!m) return { baseGram: 0, unit: '', packMethod: str };
|
||||
var baseGram = 0;
|
||||
if (m[2]) {
|
||||
baseGram = parseFloat(m[2]);
|
||||
if (m[3] === 'kg') baseGram *= 1000;
|
||||
}
|
||||
return { baseGram: baseGram, unit: m[4] || '', portion: m[1] || '', packMethod: '' };
|
||||
}
|
||||
|
||||
/* ---- 工具函数 ---- */
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -185,33 +241,39 @@
|
||||
document.getElementById('topUser').textContent = userName;
|
||||
document.getElementById('userAvatar').textContent = userName.charAt(0);
|
||||
|
||||
/* ---- Mock 数据 ---- */
|
||||
/* ---- Mock 数据 ----
|
||||
* 字段说明:
|
||||
* cutSpec: 结构化对象 {length,width,height,shape,technique}(任务 4.4)
|
||||
* packSpec: 结构化对象 {baseGram, unit, portion?}(任务 4.4)
|
||||
* packSize: 包装份数(原字段 portions 重命名,任务 4.4)
|
||||
*/
|
||||
var mockData = {
|
||||
orderNo: 'PO-20260625-001',
|
||||
orderType: 'internal',
|
||||
packDate: '2026-06-25',
|
||||
/* 净菜包:食材名 + 净菜类型 */
|
||||
/* 净菜包:食材名 + 净菜类型 */
|
||||
vegPacks: [
|
||||
{ type: '净菜包', name: '土豆', cutSpec: '20mm×1mm×1mm · 细丝 · 平刀', packSpec: '500g', portions: 3, shelfLife: '3天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-28' },
|
||||
{ type: '净菜包', name: '胡萝卜', cutSpec: '20mm×1mm×1mm · 细丝 · 平刀', packSpec: '500g', portions: 3, shelfLife: '3天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-28' }
|
||||
{ type: '净菜包', name: '土豆', cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, packSpec: {baseGram:500, unit:'袋'}, packSize: 3, shelfLife: '3天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-28' },
|
||||
{ type: '净菜包', name: '胡萝卜', cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, packSpec: {baseGram:500, unit:'袋'}, packSize: 3, shelfLife: '3天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-28' }
|
||||
],
|
||||
/* 餐品净菜包 */
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', name: '宫保鸡丁套餐', packSpec: '大份800g', portions: 2, shelfLife: '2天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-27' }
|
||||
{ type: '餐品净菜包', name: '宫保鸡丁套餐', packSpec: {baseGram:800, unit:'份', portion:'大份'}, packSize: 2, shelfLife: '2天', storageTemp: '0~4℃ 冷藏', expiryDate: '2026-06-27' }
|
||||
]
|
||||
};
|
||||
|
||||
/* 计算总重量 */
|
||||
/* 计算总重量:packSpec 结构化为 {baseGram, unit[, portion]} 后直接读取 baseGram */
|
||||
function calcTotalWeight() {
|
||||
var total = 0;
|
||||
mockData.vegPacks.forEach(function(v) {
|
||||
var w = parseInt(v.packSpec) || 0;
|
||||
total += w * v.portions;
|
||||
var specObj = typeof v.packSpec === 'object' ? v.packSpec : parseLegacyPackSpec(v.packSpec);
|
||||
var w = (specObj && specObj.baseGram) || 0;
|
||||
total += w * v.packSize;
|
||||
});
|
||||
mockData.mealPacks.forEach(function(m) {
|
||||
var specMatch = m.packSpec.match(/(\d+)g/);
|
||||
var w = specMatch ? parseInt(specMatch[1]) : 0;
|
||||
total += w * m.portions;
|
||||
var specObj = typeof m.packSpec === 'object' ? m.packSpec : parseLegacyPackSpec(m.packSpec);
|
||||
var w = (specObj && specObj.baseGram) || 0;
|
||||
total += w * m.packSize;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
@@ -222,26 +284,29 @@
|
||||
var orderTypeLabel = mockData.orderType === 'internal' ? '内供订单' : '外售订单';
|
||||
var totalWeight = calcTotalWeight();
|
||||
|
||||
/* 包装项明细行 */
|
||||
/* 包装项明细行:cutSpec/packSpec 改为 formatCutSpec/formatSpec 渲染 */
|
||||
var detailRows = '';
|
||||
mockData.vegPacks.forEach(function(v, vi) {
|
||||
var cutObj = typeof v.cutSpec === 'object' ? v.cutSpec : parseLegacyCutSpec(v.cutSpec);
|
||||
var specObj = typeof v.packSpec === 'object' ? v.packSpec : parseLegacyPackSpec(v.packSpec);
|
||||
detailRows += '<tr>'
|
||||
+ '<td>' + (vi + 1) + '</td>'
|
||||
+ '<td><span class="type-tag type-tag--veg">净菜包</span></td>'
|
||||
+ '<td>' + v.name + '</td>'
|
||||
+ '<td>' + v.cutSpec + '</td>'
|
||||
+ '<td>' + v.packSpec + '</td>'
|
||||
+ '<td>' + v.portions + '份</td>'
|
||||
+ '<td>' + formatCutSpec(cutObj) + '</td>'
|
||||
+ '<td>' + formatSpec(specObj) + '</td>'
|
||||
+ '<td>' + v.packSize + '份</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
mockData.mealPacks.forEach(function(m, mi) {
|
||||
var specObj = typeof m.packSpec === 'object' ? m.packSpec : parseLegacyPackSpec(m.packSpec);
|
||||
detailRows += '<tr>'
|
||||
+ '<td>' + (mockData.vegPacks.length + mi + 1) + '</td>'
|
||||
+ '<td><span class="type-tag type-tag--meal">餐品净菜包</span></td>'
|
||||
+ '<td>' + m.name + '</td>'
|
||||
+ '<td>—</td>'
|
||||
+ '<td>' + m.packSpec + '</td>'
|
||||
+ '<td>' + m.portions + '份</td>'
|
||||
+ '<td>' + formatSpec(specObj) + '</td>'
|
||||
+ '<td>' + m.packSize + '份</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
|
||||
@@ -271,7 +336,9 @@
|
||||
var html = '';
|
||||
|
||||
mockData.vegPacks.forEach(function(v) {
|
||||
for (var p = 0; p < v.portions; p++) {
|
||||
var cutObj = typeof v.cutSpec === 'object' ? v.cutSpec : parseLegacyCutSpec(v.cutSpec);
|
||||
var specObj = typeof v.packSpec === 'object' ? v.packSpec : parseLegacyPackSpec(v.packSpec);
|
||||
for (var p = 0; p < v.packSize; p++) {
|
||||
var code = mockData.orderNo + '-V' + v.name.charAt(0) + (p + 1);
|
||||
html += ''
|
||||
+ '<div class="label-sticker">'
|
||||
@@ -279,10 +346,10 @@
|
||||
+ '<span class="label-sticker__title-bar label-sticker__title-bar--green">净菜标签</span>'
|
||||
+ '<div class="label-info" style="margin-top:4px">'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">净菜名称:</span><span class="label-info__value">' + v.name + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">净菜类型:</span><span class="label-info__value">' + v.cutSpec + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">净菜类型:</span><span class="label-info__value">' + formatCutSpec(cutObj) + '</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="label-info">'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装规格:</span><span class="label-info__value">' + v.packSpec + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装规格:</span><span class="label-info__value">' + formatSpec(specObj) + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装日期:</span><span class="label-info__value">' + mockData.packDate + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">订单号:</span><span class="label-info__value">' + mockData.orderNo + '</span></div>'
|
||||
+ '</div>'
|
||||
@@ -305,7 +372,8 @@
|
||||
var html = '';
|
||||
|
||||
mockData.mealPacks.forEach(function(m) {
|
||||
for (var p = 0; p < m.portions; p++) {
|
||||
var specObj = typeof m.packSpec === 'object' ? m.packSpec : parseLegacyPackSpec(m.packSpec);
|
||||
for (var p = 0; p < m.packSize; p++) {
|
||||
var code = mockData.orderNo + '-M' + (p + 1);
|
||||
html += ''
|
||||
+ '<div class="label-sticker">'
|
||||
@@ -315,7 +383,7 @@
|
||||
+ '<div class="label-info__item"><span class="label-info__label">餐品名称:</span><span class="label-info__value">' + m.name + '</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="label-info">'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装规格:</span><span class="label-info__value">' + m.packSpec + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装规格:</span><span class="label-info__value">' + formatSpec(specObj) + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">包装日期:</span><span class="label-info__value">' + mockData.packDate + '</span></div>'
|
||||
+ '<div class="label-info__item"><span class="label-info__label">订单号:</span><span class="label-info__value">' + mockData.orderNo + '</span></div>'
|
||||
+ '</div>'
|
||||
@@ -354,27 +422,27 @@
|
||||
else if (tab === 'meal') btn.classList.add('print-btn--orange');
|
||||
}
|
||||
|
||||
/* ---- 打印当前标签 ---- */
|
||||
/* ---- 打印当前标签:按 packSize 计数 ---- */
|
||||
function handlePrintCurrent() {
|
||||
var labels = [];
|
||||
if (currentTab === 'order') labels.push('订单标签');
|
||||
else if (currentTab === 'veg') {
|
||||
mockData.vegPacks.forEach(function(v) {
|
||||
for (var p = 0; p < v.portions; p++) { labels.push('净菜标签 — ' + v.name); }
|
||||
for (var p = 0; p < v.packSize; p++) { labels.push('净菜标签 — ' + v.name); }
|
||||
});
|
||||
} else if (currentTab === 'meal') {
|
||||
mockData.mealPacks.forEach(function(m) {
|
||||
for (var p = 0; p < m.portions; p++) { labels.push('餐品净菜包标签 — ' + m.name); }
|
||||
for (var p = 0; p < m.packSize; p++) { labels.push('餐品净菜包标签 — ' + m.name); }
|
||||
});
|
||||
}
|
||||
showToast('已发送打印指令:' + labels.length + ' 张标签', 2000, 'success');
|
||||
}
|
||||
|
||||
/* ---- 全部打印 ---- */
|
||||
/* ---- 全部打印:按 packSize 计数 ---- */
|
||||
function handlePrintAll() {
|
||||
var total = 1; /* 订单标签 1 张 */
|
||||
mockData.vegPacks.forEach(function(v) { total += v.portions; });
|
||||
mockData.mealPacks.forEach(function(m) { total += m.portions; });
|
||||
mockData.vegPacks.forEach(function(v) { total += v.packSize; });
|
||||
mockData.mealPacks.forEach(function(m) { total += m.packSize; });
|
||||
showToast('已发送全部打印指令:共 ' + total + ' 张标签', 2500, 'success');
|
||||
}
|
||||
|
||||
|
||||
@@ -68,21 +68,24 @@
|
||||
<div class="settings-content">
|
||||
<div class="settings-section" id="section-cutting">
|
||||
<div class="section-title">加工参数配置</div>
|
||||
<div class="setting-item__hint" style="background:rgba(74,144,217,0.08);border:1px solid rgba(74,144,217,0.2);border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:11px;color:rgba(255,255,255,0.6);line-height:1.5;">
|
||||
ℹ️ 以下默认值从食材档案 <code style="color:#4a90d9">vegTypes.cutSpec</code> 自动读取,此处仅作设备级覆盖(切配类型字典 <code style="color:#4a90d9">spec_cut_shape</code>)。
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认丝条粗细</div><div class="setting-item__desc">丝条规格的标准粗细</div></div>
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认丝条粗细</div><div class="setting-item__desc">丝条规格的标准粗细(单位统一为 mm)</div></div>
|
||||
<div class="setting-item__control"><input type="number" class="setting-input" value="3" min="1" max="10"> <span style="color: rgba(255,255,255,0.5); font-size: 12px; margin-left: 4px">mm</span></div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认滚刀块大小</div><div class="setting-item__desc">滚刀块规格的标准大小</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option>小(2cm)</option><option selected>中(3cm)</option><option>大(5cm)</option></select></div>
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认滚刀块大小</div><div class="setting-item__desc">滚刀块规格的标准大小(单位统一为 mm)</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option>小(20mm)</option><option selected>中(30mm)</option><option>大(50mm)</option></select></div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认方丁大小</div><div class="setting-item__desc">方丁规格的标准大小</div></div>
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认方丁大小</div><div class="setting-item__desc">方丁规格的标准大小(mm)</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option>小(5mm)</option><option selected>中(10mm)</option><option>大(15mm)</option></select></div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认刀工</div><div class="setting-item__desc">默认使用的刀工方式</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option selected>直刀</option><option>斜刀</option><option>花刀</option></select></div>
|
||||
<div class="setting-item__info"><div class="setting-item__label">默认刀工</div><div class="setting-item__desc">引用切配类型字典(spec_cut_shape)</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option selected>直刀</option><option>斜刀</option><option>花刀</option><option>平刀</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section" id="section-ai" style="display:none">
|
||||
@@ -102,9 +105,8 @@
|
||||
</div>
|
||||
<div class="settings-section" id="section-scale" style="display:none">
|
||||
<div class="section-title">秤参数配置</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">重量单位</div><div class="setting-item__desc">称重显示单位</div></div>
|
||||
<div class="setting-item__control"><select class="setting-select"><option selected>斤</option><option>克</option><option>千克</option></select></div>
|
||||
<div class="setting-item__hint" style="background:rgba(250,173,22,0.08);border:1px solid rgba(250,173,22,0.2);border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:11px;color:rgba(255,255,255,0.6);line-height:1.5;">
|
||||
⚠️ 存储单位已统一为克(g)(任务 4.2),展示层由 <code style="color:#faad14">formatGramForCanteen</code> 自动还原"斤",本设备不再提供单位切换。
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__info"><div class="setting-item__label">小数位数</div><div class="setting-item__desc">重量显示精度</div></div>
|
||||
|
||||
@@ -162,6 +162,96 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============== 规格体系工具函数(任务 4.5 内联,权威源 spec-utils-reference.md) ============== */
|
||||
/* 克重 → 斤展示(≥500g 显示"X斤",不足 1 斤显示克重) */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || isNaN(g)) return '--';
|
||||
if (g < 500) return g + 'g';
|
||||
var jin = (g / 500).toFixed(1).replace(/\.0$/, '');
|
||||
return jin + '斤';
|
||||
}
|
||||
/* 包装规格结构化对象 → 展示字符串(支持对象和旧字符串形态) */
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (typeof spec === 'object') {
|
||||
var parts = [];
|
||||
if (spec.portion) parts.push(spec.portion);
|
||||
if (spec.baseGram) parts.push(spec.baseGram + 'g');
|
||||
if (spec.unit) parts.push('/ ' + spec.unit);
|
||||
return parts.length ? parts.join(' ') : '—';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
/* 切配规格结构化对象 → 展示字符串(4 种旧字符串格式兼容) */
|
||||
function formatCutSpec(cutSpec) {
|
||||
if (!cutSpec) return '';
|
||||
if (typeof cutSpec === 'string') return cutSpec;
|
||||
if (typeof cutSpec === 'object') {
|
||||
var parts = [];
|
||||
if (cutSpec.length && cutSpec.width && cutSpec.height) {
|
||||
parts.push(cutSpec.length + 'mm×' + cutSpec.width + 'mm×' + cutSpec.height + 'mm');
|
||||
}
|
||||
if (cutSpec.shape) parts.push(cutSpec.shape);
|
||||
if (cutSpec.technique) parts.push(cutSpec.technique);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/* 兼容解析旧版切配规格字符串为结构化对象 */
|
||||
function parseLegacyCutSpec(str) {
|
||||
if (!str) return null;
|
||||
if (typeof str !== 'string') return str;
|
||||
var s = str.trim();
|
||||
var obj = { length: 0, width: 0, height: 0, shape: '', technique: '' };
|
||||
var dimsMatch = s.match(/(\d+(?:\.\d+)?)\s*mm?\s*[\*×xX]\s*(\d+(?:\.\d+)?)\s*mm?\s*[\*×xX]\s*(\d+(?:\.\d+)?)\s*mm?/);
|
||||
if (dimsMatch) {
|
||||
obj.length = parseFloat(dimsMatch[1]);
|
||||
obj.width = parseFloat(dimsMatch[2]);
|
||||
obj.height = parseFloat(dimsMatch[3]);
|
||||
s = s.substring(s.indexOf(dimsMatch[0]) + dimsMatch[0].length);
|
||||
}
|
||||
var tokens = s.split(/[·\-\s,,]+/).filter(function(t) { return t.trim(); });
|
||||
tokens.forEach(function(tok) {
|
||||
if (/丝|片|丁|块|段|条|末|蓉/.test(tok) && !obj.shape) obj.shape = tok.trim();
|
||||
else if (/刀|平刀|斜刀|直刀|花刀/.test(tok) && !obj.technique) obj.technique = tok.trim();
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
/* 兼容解析旧版包装规格字符串为结构化对象('500g' / '大份800g' / '标准份500g' / '真空袋') */
|
||||
function parseLegacyPackSpec(str) {
|
||||
if (!str) return null;
|
||||
if (typeof str !== 'object') {
|
||||
var s = String(str).trim();
|
||||
var m = s.match(/^(大份|小份|标准份|迷你份)?\s*(\d+(?:\.\d+)?)?\s*(g|克|斤|份|袋|盒)?$/);
|
||||
var portion = (m && m[1]) || '';
|
||||
var baseGram = (m && m[2]) ? parseFloat(m[2]) : 0;
|
||||
var unit = (m && m[3]) || '';
|
||||
if (unit === '克') unit = 'g';
|
||||
if (!unit) unit = baseGram ? '份' : '袋';
|
||||
return { portion: portion, baseGram: baseGram, unit: unit };
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/* 跨页共享:从 localStorage 加载餐品净菜包规格数据(key: dish_specs_v1) */
|
||||
var DISH_SPECS_KEY = 'dish_specs_v1';
|
||||
function loadDishSpecs() {
|
||||
try {
|
||||
var raw = localStorage.getItem(DISH_SPECS_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch(e) {}
|
||||
return {};
|
||||
}
|
||||
/* packSpec 兜底:从跨页共享表查(用于 mockData 与运营配置不一致时的兜底) */
|
||||
function resolvePackSpec(pack) {
|
||||
if (pack.packSpec && typeof pack.packSpec === 'object') return pack.packSpec;
|
||||
if (pack.packSpec && typeof pack.packSpec === 'string') return parseLegacyPackSpec(pack.packSpec);
|
||||
var dishSpecs = loadDishSpecs();
|
||||
var key = pack.packName || pack.name;
|
||||
if (key && dishSpecs[key]) return dishSpecs[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* 获取 URL 参数 */
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -175,7 +265,7 @@
|
||||
/* 当前 Tab 筛选 */
|
||||
var currentFilter = 'pending';
|
||||
|
||||
/* 模拟数据:仅展示餐品净菜包订单 */
|
||||
/* 模拟数据:packSpec 改为结构化对象(baseGram+unit+portion),切配规格在 02 页体现 */
|
||||
var mockData = [
|
||||
{
|
||||
orderNo: 'PO-20260616-001',
|
||||
@@ -183,7 +273,7 @@
|
||||
expectedArrival: '2026-06-17 11:30',
|
||||
taskNo: 'T-001',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: '大份800g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '土豆', icon: '🥔', weightPerPack: 300, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', weightPerPack: 250, weighed: 0 },
|
||||
{ name: '黄瓜', icon: '🥒', weightPerPack: 250, weighed: 0 }
|
||||
@@ -200,14 +290,14 @@
|
||||
expectedArrival: '2026-06-17 12:00',
|
||||
taskNo: 'T-002',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: '大份800g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '白菜', icon: '🥬', weightPerPack: 400, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', weightPerPack: 400, weighed: 0 }
|
||||
], seasonings: [
|
||||
{ name: '醋', cellIdx: 2, weightPerPack: 8, weighed: 0 },
|
||||
{ name: '料酒', cellIdx: 4, weightPerPack: 12, weighed: 0 }
|
||||
]},
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 2, packSpec: '标准份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 2, packSpec: {portion:'标准份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '番茄', icon: '🍅', weightPerPack: 300, weighed: 0 },
|
||||
{ name: '鸡蛋', icon: '🥚', weightPerPack: 200, weighed: 0 }
|
||||
], seasonings: [
|
||||
@@ -222,7 +312,7 @@
|
||||
expectedArrival: '2026-06-17 14:30',
|
||||
taskNo: 'T-003',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: '大份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: {portion:'大份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '番茄炒蛋', icon: '🥘', weightPerPack: 500, weighed: 2000 }
|
||||
], seasonings: [
|
||||
{ name: '盐', cellIdx: 2, weightPerPack: 5, weighed: 20 },
|
||||
@@ -236,7 +326,7 @@
|
||||
expectedArrival: '2026-06-17 16:00',
|
||||
taskNo: 'T-004',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: '标准份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: {portion:'标准份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '凉拌黄瓜', icon: '🥘', weightPerPack: 500, weighed: 2500 }
|
||||
], seasonings: [
|
||||
{ name: '醋', cellIdx: 2, weightPerPack: 6, weighed: 30 },
|
||||
@@ -329,11 +419,15 @@
|
||||
var packDone = !hasPendingPack(pack);
|
||||
var weightClass = packDone ? 'parent-weight--done' : '';
|
||||
|
||||
/* packSpec 结构化为对象后用 formatSpec 渲染(兼容旧字符串和跨页共享表) */
|
||||
var specObj = resolvePackSpec(pack);
|
||||
var specText = specObj ? formatSpec(specObj) : '—';
|
||||
|
||||
var parentRow = document.createElement('div');
|
||||
parentRow.className = 'item-parent-row';
|
||||
parentRow.innerHTML =
|
||||
'<div class="parent-name"><div class="parent-icon">📦</div><span class="parent-name__text">' + pack.packName + '</span></div>' +
|
||||
'<div style="text-align:center"><span class="pack-spec-tag">' + (pack.packSpec || '—') + '</span></div>' +
|
||||
'<div style="text-align:center"><span class="pack-spec-tag">' + specText + '</span></div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalNeed + ' 克</div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalWeighed + ' 克</div>' +
|
||||
'<div style="text-align:center"><span class="status-tag ' + (packDone ? 'status-tag--done' : 'status-tag--wait') + '">' + (packDone ? '已完成' : '待组配') + '</span></div>' +
|
||||
|
||||
@@ -520,6 +520,95 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* ============== 规格体系工具函数(任务 4.5 内联,权威源 spec-utils-reference.md) ============== */
|
||||
/* 克重 → 斤展示(≥500g 显示"X斤",不足 1 斤显示克重) */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || isNaN(g)) return '--';
|
||||
if (g < 500) return g + 'g';
|
||||
var jin = (g / 500).toFixed(1).replace(/\.0$/, '');
|
||||
return jin + '斤';
|
||||
}
|
||||
/* 包装规格结构化对象 → 展示字符串(支持对象和旧字符串形态) */
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (typeof spec === 'object') {
|
||||
var parts = [];
|
||||
if (spec.portion) parts.push(spec.portion);
|
||||
if (spec.baseGram) parts.push(spec.baseGram + 'g');
|
||||
if (spec.unit) parts.push('/ ' + spec.unit);
|
||||
return parts.length ? parts.join(' ') : '—';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
/* 切配规格结构化对象 → 展示字符串(4 种旧字符串格式兼容) */
|
||||
function formatCutSpec(cutSpec) {
|
||||
if (!cutSpec) return '';
|
||||
if (typeof cutSpec === 'string') return cutSpec;
|
||||
if (typeof cutSpec === 'object') {
|
||||
var parts = [];
|
||||
if (cutSpec.length && cutSpec.width && cutSpec.height) {
|
||||
parts.push(cutSpec.length + 'mm×' + cutSpec.width + 'mm×' + cutSpec.height + 'mm');
|
||||
}
|
||||
if (cutSpec.shape) parts.push(cutSpec.shape);
|
||||
if (cutSpec.technique) parts.push(cutSpec.technique);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/* 兼容解析旧版切配规格字符串为结构化对象 */
|
||||
function parseLegacyCutSpec(str) {
|
||||
if (!str) return null;
|
||||
if (typeof str !== 'string') return str;
|
||||
var s = str.trim();
|
||||
var obj = { length: 0, width: 0, height: 0, shape: '', technique: '' };
|
||||
var dimsMatch = s.match(/(\d+(?:\.\d+)?)\s*mm?\s*[\*×xX]\s*(\d+(?:\.\d+)?)\s*mm?\s*[\*×xX]\s*(\d+(?:\.\d+)?)\s*mm?/);
|
||||
if (dimsMatch) {
|
||||
obj.length = parseFloat(dimsMatch[1]);
|
||||
obj.width = parseFloat(dimsMatch[2]);
|
||||
obj.height = parseFloat(dimsMatch[3]);
|
||||
s = s.substring(s.indexOf(dimsMatch[0]) + dimsMatch[0].length);
|
||||
}
|
||||
var tokens = s.split(/[·\-\s,,]+/).filter(function(t) { return t.trim(); });
|
||||
tokens.forEach(function(tok) {
|
||||
if (/丝|片|丁|块|段|条|末|蓉/.test(tok) && !obj.shape) obj.shape = tok.trim();
|
||||
else if (/刀|平刀|斜刀|直刀|花刀/.test(tok) && !obj.technique) obj.technique = tok.trim();
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
/* 兼容解析旧版包装规格字符串为结构化对象 */
|
||||
function parseLegacyPackSpec(str) {
|
||||
if (!str) return null;
|
||||
if (typeof str !== 'object') {
|
||||
var s = String(str).trim();
|
||||
var m = s.match(/^(大份|小份|标准份|迷你份)?\s*(\d+(?:\.\d+)?)?\s*(g|克|斤|份|袋|盒)?$/);
|
||||
var portion = (m && m[1]) || '';
|
||||
var baseGram = (m && m[2]) ? parseFloat(m[2]) : 0;
|
||||
var unit = (m && m[3]) || '';
|
||||
if (unit === '克') unit = 'g';
|
||||
if (!unit) unit = baseGram ? '份' : '袋';
|
||||
return { portion: portion, baseGram: baseGram, unit: unit };
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/* 跨页共享:从 localStorage 加载餐品净菜包规格数据(key: dish_specs_v1) */
|
||||
var DISH_SPECS_KEY = 'dish_specs_v1';
|
||||
function loadDishSpecs() {
|
||||
try {
|
||||
var raw = localStorage.getItem(DISH_SPECS_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch(e) {}
|
||||
return {};
|
||||
}
|
||||
function resolvePackSpec(pack) {
|
||||
if (pack.packSpec && typeof pack.packSpec === 'object') return pack.packSpec;
|
||||
if (pack.packSpec && typeof pack.packSpec === 'string') return parseLegacyPackSpec(pack.packSpec);
|
||||
var dishSpecs = loadDishSpecs();
|
||||
var key = pack.packName || pack.name;
|
||||
if (key && dishSpecs[key]) return dishSpecs[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---- 工具函数 ---- */
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -537,16 +626,16 @@
|
||||
document.getElementById('topUser').textContent = userName;
|
||||
document.getElementById('userAvatar').textContent = userName.charAt(0);
|
||||
|
||||
/* ---- 模拟数据 ---- */
|
||||
/* ---- 模拟数据:packSpec/cutSpec 结构化为对象,切配规格 4 字段(length/width/height/shape/technique) ---- */
|
||||
var taskData = {
|
||||
'T-001': {
|
||||
orderNo: 'PO-20260616-001',
|
||||
orderType: 'internal',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: '大份800g', ingredients: [
|
||||
{ name: '土豆', icon: '🥔', cutSpec: '20mm*1mm*1mm · 细丝 · 平刀', weightPerPack: 300, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', cutSpec: '20mm*1mm*1mm · 细丝 · 平刀', weightPerPack: 250, weighed: 0 },
|
||||
{ name: '黄瓜', icon: '🥒', cutSpec: '30mm*30mm*3mm · 圆片 · 平刀', weightPerPack: 250, weighed: 0 }
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '土豆', icon: '🥔', cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, weightPerPack: 300, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, weightPerPack: 250, weighed: 0 },
|
||||
{ name: '黄瓜', icon: '🥒', cutSpec: {length:30, width:30, height:3, shape:'圆片', technique:'平刀'}, weightPerPack: 250, weighed: 0 }
|
||||
], seasonings: [
|
||||
{ name: '油', icon: '🛢', cellIdx: 0, weightPerPack: 50, weighed: 0 },
|
||||
{ name: '盐', icon: '🧂', cellIdx: 1, weightPerPack: 5, weighed: 0 },
|
||||
@@ -562,9 +651,9 @@
|
||||
orderNo: 'PO-20260616-002',
|
||||
orderType: 'external',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: '大份800g', ingredients: [
|
||||
{ name: '白菜', icon: '🥬', cutSpec: '50mm*30mm*30mm · 宽段 · 平刀', weightPerPack: 400, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', cutSpec: '20mm*1mm*1mm · 细丝 · 平刀', weightPerPack: 400, weighed: 0 }
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '白菜', icon: '🥬', cutSpec: {length:50, width:30, height:30, shape:'宽段', technique:'平刀'}, weightPerPack: 400, weighed: 0 },
|
||||
{ name: '胡萝卜', icon: '🥕', cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}, weightPerPack: 400, weighed: 0 }
|
||||
], seasonings: [
|
||||
{ name: '油', icon: '🛢', cellIdx: 0, weightPerPack: 40, weighed: 0 },
|
||||
{ name: '盐', icon: '🧂', cellIdx: 1, weightPerPack: 4, weighed: 0 },
|
||||
@@ -580,7 +669,7 @@
|
||||
orderNo: 'PO-20260616-003',
|
||||
orderType: 'internal',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: '大份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: {portion:'大份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '番茄炒蛋', icon: '🥘', weightPerPack: 500, weighed: 2000 }
|
||||
], seasonings: [
|
||||
{ name: '油', icon: '🛢', cellIdx: 0, weightPerPack: 30, weighed: 120 },
|
||||
@@ -593,7 +682,7 @@
|
||||
orderNo: 'PO-20260616-004',
|
||||
orderType: 'external',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: '标准份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: {portion:'标准份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '凉拌黄瓜', icon: '🥘', weightPerPack: 500, weighed: 2500 }
|
||||
], seasonings: [
|
||||
{ name: '油', icon: '🛢', cellIdx: 0, weightPerPack: 20, weighed: 100 },
|
||||
@@ -712,7 +801,10 @@
|
||||
var orderTypeLabel = task.orderType === 'internal' ? '内供订单' : '外售订单';
|
||||
var orderTypeCls = task.orderType === 'internal' ? 'order-type-tag--internal' : 'order-type-tag--external';
|
||||
orderTypeEl.innerHTML = '<span class="order-type-tag ' + orderTypeCls + '">' + orderTypeLabel + '</span>';
|
||||
document.getElementById('taskPackName').textContent = currentPack.packName + ' · ' + (currentPack.packSpec || '—');
|
||||
/* 餐品名称展示:packSpec 结构化为对象,使用 formatSpec 渲染 */
|
||||
var specObj = resolvePackSpec(currentPack);
|
||||
var specText = specObj ? formatSpec(specObj) : '—';
|
||||
document.getElementById('taskPackName').textContent = currentPack.packName + ' · ' + specText;
|
||||
|
||||
/* 订单状态:主辅材+调料全部完成才算完成 */
|
||||
var ingAllDone = allIngredients.every(function(ing) {
|
||||
@@ -748,6 +840,9 @@
|
||||
allIngredients.forEach(function(ing, idx) {
|
||||
var need = Math.round(ing.weightPerPack * 100) / 100;
|
||||
var isDone = ing.weighed >= need;
|
||||
/* cutSpec 结构化为对象,用 formatCutSpec 渲染(兼容旧字符串) */
|
||||
var cutObj = ing.cutSpec ? (typeof ing.cutSpec === 'object' ? ing.cutSpec : parseLegacyCutSpec(ing.cutSpec)) : null;
|
||||
var cutText = cutObj ? formatCutSpec(cutObj) : '—';
|
||||
|
||||
var row = document.createElement('div');
|
||||
row.className = 'item-row';
|
||||
@@ -768,7 +863,7 @@
|
||||
'<div class="item-card__data">' +
|
||||
'<div class="item-card__data-item"><span class="item-card__data-label">应称</span><span class="item-card__data-value">' + need + 'g</span></div>' +
|
||||
'<div class="item-card__data-item"><span class="item-card__data-label">已称</span><span class="item-card__data-value ' + actualClass + '">' + ing.weighed + 'g</span></div>' +
|
||||
'<div class="item-card__data-item"><span class="item-card__data-label">净菜类型</span><span class="item-card__data-value">' + (ing.cutSpec || '—') + '</span></div>' +
|
||||
'<div class="item-card__data-item"><span class="item-card__data-label">净菜类型</span><span class="item-card__data-value">' + cutText + '</span></div>' +
|
||||
'</div>';
|
||||
|
||||
body.appendChild(row);
|
||||
@@ -953,10 +1048,13 @@
|
||||
|
||||
title.textContent = '智能识别候选 (' + selected.length + ')';
|
||||
|
||||
/* 构建净菜类型映射 */
|
||||
/* 构建净菜类型映射:cutSpec 结构化后用 formatCutSpec 渲染为字符串 */
|
||||
var cutSpecMap = {};
|
||||
allIngredients.forEach(function(ing) {
|
||||
if (ing.cutSpec) cutSpecMap[ing.name] = ing.cutSpec;
|
||||
if (ing.cutSpec) {
|
||||
var cutObj = typeof ing.cutSpec === 'object' ? ing.cutSpec : parseLegacyCutSpec(ing.cutSpec);
|
||||
cutSpecMap[ing.name] = formatCutSpec(cutObj);
|
||||
}
|
||||
});
|
||||
|
||||
list.innerHTML = '';
|
||||
|
||||
@@ -358,6 +358,75 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============== 规格体系工具函数(任务 4.5 内联,权威源 spec-utils-reference.md) ============== */
|
||||
/* 克重 → 斤展示(≥500g 显示"X斤",不足 1 斤显示克重) */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || isNaN(g)) return '--';
|
||||
if (g < 500) return g + 'g';
|
||||
var jin = (g / 500).toFixed(1).replace(/\.0$/, '');
|
||||
return jin + '斤';
|
||||
}
|
||||
/* 包装规格结构化对象 → 展示字符串(支持对象和旧字符串形态) */
|
||||
function formatSpec(spec) {
|
||||
if (!spec) return '—';
|
||||
if (typeof spec === 'string') return spec;
|
||||
if (typeof spec === 'object') {
|
||||
var parts = [];
|
||||
if (spec.portion) parts.push(spec.portion);
|
||||
if (spec.baseGram) parts.push(spec.baseGram + 'g');
|
||||
if (spec.unit) parts.push('/ ' + spec.unit);
|
||||
return parts.length ? parts.join(' ') : '—';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
/* 切配规格结构化对象 → 展示字符串(4 种旧字符串格式兼容) */
|
||||
function formatCutSpec(cutSpec) {
|
||||
if (!cutSpec) return '';
|
||||
if (typeof cutSpec === 'string') return cutSpec;
|
||||
if (typeof cutSpec === 'object') {
|
||||
var parts = [];
|
||||
if (cutSpec.length && cutSpec.width && cutSpec.height) {
|
||||
parts.push(cutSpec.length + 'mm×' + cutSpec.width + 'mm×' + cutSpec.height + 'mm');
|
||||
}
|
||||
if (cutSpec.shape) parts.push(cutSpec.shape);
|
||||
if (cutSpec.technique) parts.push(cutSpec.technique);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/* 兼容解析旧版包装规格字符串为结构化对象 */
|
||||
function parseLegacyPackSpec(str) {
|
||||
if (!str) return null;
|
||||
if (typeof str !== 'object') {
|
||||
var s = String(str).trim();
|
||||
var m = s.match(/^(大份|小份|标准份|迷你份)?\s*(\d+(?:\.\d+)?)?\s*(g|克|斤|份|袋|盒)?$/);
|
||||
var portion = (m && m[1]) || '';
|
||||
var baseGram = (m && m[2]) ? parseFloat(m[2]) : 0;
|
||||
var unit = (m && m[3]) || '';
|
||||
if (unit === '克') unit = 'g';
|
||||
if (!unit) unit = baseGram ? '份' : '袋';
|
||||
return { portion: portion, baseGram: baseGram, unit: unit };
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/* 跨页共享:从 localStorage 加载餐品净菜包规格数据(key: dish_specs_v1) */
|
||||
var DISH_SPECS_KEY = 'dish_specs_v1';
|
||||
function loadDishSpecs() {
|
||||
try {
|
||||
var raw = localStorage.getItem(DISH_SPECS_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch(e) {}
|
||||
return {};
|
||||
}
|
||||
function resolvePackSpec(pack) {
|
||||
if (pack.packSpec && typeof pack.packSpec === 'object') return pack.packSpec;
|
||||
if (pack.packSpec && typeof pack.packSpec === 'string') return parseLegacyPackSpec(pack.packSpec);
|
||||
var dishSpecs = loadDishSpecs();
|
||||
var key = pack.packName || pack.name;
|
||||
if (key && dishSpecs[key]) return dishSpecs[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* 退出登录 */
|
||||
function handleLogout() {
|
||||
window.location.href = '00-welcome.html';
|
||||
@@ -379,12 +448,12 @@
|
||||
var TODAY = '2026-06-16';
|
||||
var WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
|
||||
// 组配记录数据(与组配任务页01数据结构一致,全部为已组配状态)
|
||||
// 组配记录数据(packSpec 结构化为对象,与 01 任务页保持一致)
|
||||
var RECORDS = [
|
||||
{
|
||||
orderNo: 'PO-20260616-001', orderType: 'internal', completeTime: '2026-06-16 10:20', taskNo: 'T-001',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: '大份800g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '宫保鸡丁套餐', packSize: 3, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '土豆', icon: '🥔', weightPerPack: 300, weighed: 900 },
|
||||
{ name: '胡萝卜', icon: '🥕', weightPerPack: 250, weighed: 750 },
|
||||
{ name: '黄瓜', icon: '🥒', weightPerPack: 250, weighed: 750 }
|
||||
@@ -402,7 +471,7 @@
|
||||
{
|
||||
orderNo: 'PO-20260616-002', orderType: 'external', completeTime: '2026-06-16 11:00', taskNo: 'T-002',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: '大份800g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '鱼香肉丝套餐', packSize: 2, packSpec: {portion:'大份', baseGram:800, unit:'份'}, ingredients: [
|
||||
{ name: '白菜', icon: '🥬', weightPerPack: 400, weighed: 800 },
|
||||
{ name: '胡萝卜', icon: '🥕', weightPerPack: 400, weighed: 800 }
|
||||
], seasonings: [
|
||||
@@ -414,7 +483,7 @@
|
||||
{ name: '料酒', cellIdx: 5, weightPerPack: 10, weighed: 20 },
|
||||
{ name: '豆瓣酱', cellIdx: 6, weightPerPack: 15, weighed: 30 }
|
||||
]},
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 2, packSpec: '标准份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 2, packSpec: {portion:'标准份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '番茄', icon: '🍅', weightPerPack: 300, weighed: 600 },
|
||||
{ name: '鸡蛋', icon: '🥚', weightPerPack: 200, weighed: 400 }
|
||||
], seasonings: [
|
||||
@@ -427,7 +496,7 @@
|
||||
{
|
||||
orderNo: 'PO-20260616-003', orderType: 'internal', completeTime: '2026-06-16 14:50', taskNo: 'T-003',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: '大份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '番茄炒蛋套餐', packSize: 4, packSpec: {portion:'大份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '番茄炒蛋', icon: '🥘', weightPerPack: 500, weighed: 2000 }
|
||||
], seasonings: [
|
||||
{ name: '油', cellIdx: 0, weightPerPack: 30, weighed: 120 },
|
||||
@@ -439,7 +508,7 @@
|
||||
{
|
||||
orderNo: 'PO-20260616-004', orderType: 'external', completeTime: '2026-06-16 15:20', taskNo: 'T-004',
|
||||
mealPacks: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: '标准份500g', ingredients: [
|
||||
{ type: '餐品净菜包', packName: '凉拌黄瓜套餐', packSize: 5, packSpec: {portion:'标准份', baseGram:500, unit:'份'}, ingredients: [
|
||||
{ name: '凉拌黄瓜', icon: '🥘', weightPerPack: 500, weighed: 2500 }
|
||||
], seasonings: [
|
||||
{ name: '油', cellIdx: 0, weightPerPack: 20, weighed: 100 },
|
||||
@@ -725,11 +794,15 @@
|
||||
var packDone = !hasPendingPack(pack);
|
||||
var weightClass = packDone ? 'parent-weight--done' : '';
|
||||
|
||||
/* packSpec 结构化为对象,用 formatSpec 渲染(兼容旧字符串和跨页共享表) */
|
||||
var specObj = resolvePackSpec(pack);
|
||||
var specText = specObj ? formatSpec(specObj) : '—';
|
||||
|
||||
var parentRow = document.createElement('div');
|
||||
parentRow.className = 'item-parent-row';
|
||||
parentRow.innerHTML =
|
||||
'<div class="parent-name"><div class="parent-icon">📦</div><span class="parent-name__text">' + pack.packName + '</span></div>' +
|
||||
'<div style="text-align:center"><span class="pack-spec-tag">' + (pack.packSpec || '—') + '</span></div>' +
|
||||
'<div style="text-align:center"><span class="pack-spec-tag">' + specText + '</span></div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalNeed + ' 克</div>' +
|
||||
'<div class="parent-weight ' + weightClass + '">' + totalWeighed + ' 克</div>' +
|
||||
'<div style="text-align:center"><span class="status-tag ' + (packDone ? 'status-tag--done' : 'status-tag--wait') + '">' + (packDone ? '已完成' : '待组配') + '</span></div>';
|
||||
|
||||
@@ -1009,40 +1009,42 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* ===== 食材目录(可称重食材,按重量验收) ===== */
|
||||
/* ===== 食材目录(称重验收,存储单位克,展示层用 formatGramForCanteen 还原"斤") ===== */
|
||||
/* verifyType:'weight' 表示称重验收,可用 ['斤','公斤','克','千克'] */
|
||||
var FOOD_CATALOG = {
|
||||
'大白菜': { price: 3.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC001' },
|
||||
'圆白菜': { price: 4.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC002' },
|
||||
'菠菜': { price: 6.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC003' },
|
||||
'油菜': { price: 4.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC004' },
|
||||
'生菜': { price: 5.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC005' },
|
||||
'芹菜': { price: 5.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC006' },
|
||||
'西兰花': { price: 8.0, unit: '斤', catLabel: '花菜类', traceCode: 'TRC260601-HC001' },
|
||||
'花菜': { price: 7.0, unit: '斤', catLabel: '花菜类', traceCode: 'TRC260601-HC002' },
|
||||
'黄瓜': { price: 5.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG001' },
|
||||
'西红柿': { price: 6.5, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG002' },
|
||||
'茄子': { price: 5.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG003' },
|
||||
'青椒': { price: 6.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG004' },
|
||||
'豆角': { price: 7.5, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG005' },
|
||||
'土豆': { price: 3.0, unit: '斤', catLabel: '根茎类', traceCode: 'TRC260601-GJ001' },
|
||||
'胡萝卜': { price: 4.0, unit: '斤', catLabel: '根茎类', traceCode: 'TRC260601-GJ002' },
|
||||
'白菜': { price: 3.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC007' },
|
||||
'猪里脊': { price: 18.0, unit: '斤', catLabel: '肉禽类', traceCode: 'TRC260601-RQ001' },
|
||||
'五花肉': { price: 16.0, unit: '斤', catLabel: '肉禽类', traceCode: 'TRC260601-RQ002' },
|
||||
'草鱼': { price: 14.0, unit: '斤', catLabel: '水产类', traceCode: 'TRC260601-SC001' },
|
||||
'虾仁': { price: 35.0, unit: '斤', catLabel: '水产类', traceCode: 'TRC260601-SC002' }
|
||||
'大白菜': { price: 3.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC001', verifyType: 'weight' },
|
||||
'圆白菜': { price: 4.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC002', verifyType: 'weight' },
|
||||
'菠菜': { price: 6.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC003', verifyType: 'weight' },
|
||||
'油菜': { price: 4.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC004', verifyType: 'weight' },
|
||||
'生菜': { price: 5.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC005', verifyType: 'weight' },
|
||||
'芹菜': { price: 5.0, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC006', verifyType: 'weight' },
|
||||
'西兰花': { price: 8.0, unit: '斤', catLabel: '花菜类', traceCode: 'TRC260601-HC001', verifyType: 'weight' },
|
||||
'花菜': { price: 7.0, unit: '斤', catLabel: '花菜类', traceCode: 'TRC260601-HC002', verifyType: 'weight' },
|
||||
'黄瓜': { price: 5.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG001', verifyType: 'weight' },
|
||||
'西红柿': { price: 6.5, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG002', verifyType: 'weight' },
|
||||
'茄子': { price: 5.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG003', verifyType: 'weight' },
|
||||
'青椒': { price: 6.0, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG004', verifyType: 'weight' },
|
||||
'豆角': { price: 7.5, unit: '斤', catLabel: '瓜果类', traceCode: 'TRC260601-GG005', verifyType: 'weight' },
|
||||
'土豆': { price: 3.0, unit: '斤', catLabel: '根茎类', traceCode: 'TRC260601-GJ001', verifyType: 'weight' },
|
||||
'胡萝卜': { price: 4.0, unit: '斤', catLabel: '根茎类', traceCode: 'TRC260601-GJ002', verifyType: 'weight' },
|
||||
'白菜': { price: 3.5, unit: '斤', catLabel: '叶菜类', traceCode: 'TRC260601-YC007', verifyType: 'weight' },
|
||||
'猪里脊': { price: 18.0, unit: '斤', catLabel: '肉禽类', traceCode: 'TRC260601-RQ001', verifyType: 'weight' },
|
||||
'五花肉': { price: 16.0, unit: '斤', catLabel: '肉禽类', traceCode: 'TRC260601-RQ002', verifyType: 'weight' },
|
||||
'草鱼': { price: 14.0, unit: '斤', catLabel: '水产类', traceCode: 'TRC260601-SC001', verifyType: 'weight' },
|
||||
'虾仁': { price: 35.0, unit: '斤', catLabel: '水产类', traceCode: 'TRC260601-SC002', verifyType: 'weight' }
|
||||
};
|
||||
|
||||
/* ===== 调料目录(按规格/数量验收,无法称重) ===== */
|
||||
/* ===== 调料目录(点收验收,按规格/数量验收) ===== */
|
||||
/* verifyType:'count' 表示点收验收,可用 ['袋','瓶','箱','桶','罐','份','个','只','条','把'] */
|
||||
var SPICE_CATALOG = {
|
||||
'酱油': { price: 12.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL001' },
|
||||
'醋': { price: 8.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL002' },
|
||||
'料酒': { price: 10.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL003' },
|
||||
'盐': { price: 3.0, unit: '袋', spec: '500g/袋', catLabel: '调料', traceCode: 'TRC260601-TL004' },
|
||||
'白糖': { price: 6.0, unit: '袋', spec: '1kg/袋', catLabel: '调料', traceCode: 'TRC260601-TL005' },
|
||||
'鸡精': { price: 8.0, unit: '罐', spec: '200g/罐', catLabel: '调料', traceCode: 'TRC260601-TL006' },
|
||||
'食用油': { price: 45.0, unit: '桶', spec: '5L/桶', catLabel: '调料', traceCode: 'TRC260601-TL007' },
|
||||
'花椒': { price: 25.0, unit: '袋', spec: '100g/袋', catLabel: '调料', traceCode: 'TRC260601-TL008' }
|
||||
'酱油': { price: 12.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL001', verifyType: 'count' },
|
||||
'醋': { price: 8.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL002', verifyType: 'count' },
|
||||
'料酒': { price: 10.0, unit: '瓶', spec: '500ml/瓶', catLabel: '调料', traceCode: 'TRC260601-TL003', verifyType: 'count' },
|
||||
'盐': { price: 3.0, unit: '袋', spec: '500g/袋', catLabel: '调料', traceCode: 'TRC260601-TL004', verifyType: 'count' },
|
||||
'白糖': { price: 6.0, unit: '袋', spec: '1kg/袋', catLabel: '调料', traceCode: 'TRC260601-TL005', verifyType: 'count' },
|
||||
'鸡精': { price: 8.0, unit: '罐', spec: '200g/罐', catLabel: '调料', traceCode: 'TRC260601-TL006', verifyType: 'count' },
|
||||
'食用油': { price: 45.0, unit: '桶', spec: '5L/桶', catLabel: '调料', traceCode: 'TRC260601-TL007', verifyType: 'count' },
|
||||
'花椒': { price: 25.0, unit: '袋', spec: '100g/袋', catLabel: '调料', traceCode: 'TRC260601-TL008', verifyType: 'count' }
|
||||
};
|
||||
|
||||
/* 判断食材是否为调料(按规格验收) */
|
||||
@@ -1473,8 +1475,12 @@
|
||||
? '<span class="food-row__weight" style="color:rgba(255,255,255,0.25);">--</span>'
|
||||
: '<span class="food-row__weight">' + (spice ? orderQty : orderQty.toFixed(1)) + '</span>';
|
||||
|
||||
/* 规格下拉选项(采购规格 + 实收规格共用) */
|
||||
var UNIT_OPTIONS = ['斤','公斤','克','千克','袋','瓶','箱','桶','罐','份','个','只','条','把'];
|
||||
/* 规格下拉选项(按 verifyType 分类过滤) */
|
||||
/* 称重类(weight):'斤','公斤','克','千克' | 计数类(count):'袋','瓶','箱','桶','罐','份','个','只','条','把' */
|
||||
var UNIT_OPTIONS_WEIGHT = ['斤','公斤','克','千克'];
|
||||
var UNIT_OPTIONS_COUNT = ['袋','瓶','箱','桶','罐','份','个','只','条','把'];
|
||||
var verifyType = info.verifyType || 'weight';
|
||||
var UNIT_OPTIONS = (verifyType === 'count') ? UNIT_OPTIONS_COUNT : UNIT_OPTIONS_WEIGHT;
|
||||
var currentUnit = (f._customUnit !== undefined) ? f._customUnit : info.unit;
|
||||
var actualUnit = (f._actualUnit !== undefined) ? f._actualUnit : currentUnit;
|
||||
function buildUnitSelect(selected, onChangeFn) {
|
||||
|
||||
@@ -260,22 +260,22 @@
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--cucumber">🥒</div>
|
||||
<span class="ingredient-row__name">黄瓜</span>
|
||||
<span class="ingredient-row__weight">需 4.0 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 4.0 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="2000">需 4 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="2000">已录 4 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 10:00</span>
|
||||
</div>
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--potato"></div>
|
||||
<span class="ingredient-row__name">土豆</span>
|
||||
<span class="ingredient-row__weight">需 5.0 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 5.0 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="2500">需 5 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="2500">已录 5 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 10:30</span>
|
||||
</div>
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--tomato">🍅</div>
|
||||
<span class="ingredient-row__name">西红柿</span>
|
||||
<span class="ingredient-row__weight">需 3.5 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 3.5 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="1750">需 3.5 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="1750">已录 3.5 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 11:00</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,15 +296,15 @@
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--carrot"></div>
|
||||
<span class="ingredient-row__name">胡萝卜</span>
|
||||
<span class="ingredient-row__weight">需 2.0 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 2.0 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="1000">需 2 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="1000">已录 2 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 11:30</span>
|
||||
</div>
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--tomato">🍅</div>
|
||||
<span class="ingredient-row__name">小番茄</span>
|
||||
<span class="ingredient-row__weight">需 1.5 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 1.5 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="750">需 1.5 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="750">已录 1.5 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 14:00</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,22 +324,22 @@
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--cabbage">🥬</div>
|
||||
<span class="ingredient-row__name">大白菜</span>
|
||||
<span class="ingredient-row__weight">需 8.0 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 8.0 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="4000">需 8 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="4000">已录 8 斤</span>
|
||||
<span class="ingredient-row__deadline ingredient-row__deadline--overdue">截止 09:30</span>
|
||||
</div>
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--cabbage">🥬</div>
|
||||
<span class="ingredient-row__name">娃娃菜</span>
|
||||
<span class="ingredient-row__weight">需 2.5 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 2.5 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="1250">需 2.5 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="1250">已录 2.5 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 10:00</span>
|
||||
</div>
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-row__icon icon--potato"></div>
|
||||
<span class="ingredient-row__name">土豆(晚餐)</span>
|
||||
<span class="ingredient-row__weight">需 3.0 斤</span>
|
||||
<span class="ingredient-row__recorded">已录 3.0 斤</span>
|
||||
<span class="ingredient-row__weight" data-weight-g="1500">需 3 斤</span>
|
||||
<span class="ingredient-row__recorded" data-weight-g="1500">已录 3 斤</span>
|
||||
<span class="ingredient-row__deadline">截止 15:00</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,6 +353,23 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
/* 克重展示格式化:500→"500g",1500→"1.5kg",25000→"25kg" */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
/* 验收秤场景兼容"斤"显示:整500倍数显示"斤",否则回退到 formatGram */
|
||||
function formatGramForCanteen(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g % 500 === 0) return (g / 500) + '斤';
|
||||
return formatGram(g);
|
||||
}
|
||||
|
||||
// 获取 URL 参数
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -423,14 +440,14 @@
|
||||
'<div class="ingredient-row">' +
|
||||
'<div class="ingredient-row__icon icon--cabbage">🥦</div>' +
|
||||
'<span class="ingredient-row__name">西兰花</span>' +
|
||||
'<span class="ingredient-row__weight">需 3.0 斤</span>' +
|
||||
'<span class="ingredient-row__weight" data-weight-g="1500">需 3 斤</span>' +
|
||||
'<span class="ingredient-row__recorded ingredient-row__recorded--none">未录入</span>' +
|
||||
'<span class="ingredient-row__deadline">截止 13:00</span>' +
|
||||
'</div>' +
|
||||
'<div class="ingredient-row">' +
|
||||
'<div class="ingredient-row__icon icon--carrot">🥕</div>' +
|
||||
'<span class="ingredient-row__name">芹菜</span>' +
|
||||
'<span class="ingredient-row__weight">需 2.0 斤</span>' +
|
||||
'<span class="ingredient-row__weight" data-weight-g="1000">需 2 斤</span>' +
|
||||
'<span class="ingredient-row__recorded ingredient-row__recorded--none">未录入</span>' +
|
||||
'<span class="ingredient-row__deadline">截止 13:30</span>' +
|
||||
'</div>' +
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
<span class="task-info__value" id="taskWeightDisplay" style="color:#faad14">--</span>
|
||||
<span style="color:rgba(255,255,255,0.15);font-size:11px">|</span>
|
||||
<span class="task-info__label">已录入</span>
|
||||
<span class="task-info__value" id="taskActualDisplay" style="color:#52c41a">0.0 斤</span>
|
||||
<span class="task-info__value" id="taskActualDisplay" style="color:#52c41a">0g</span>
|
||||
</div>
|
||||
|
||||
<!-- 批次信息条 -->
|
||||
@@ -401,10 +401,9 @@
|
||||
<span class="trace-row__status" id="traceStatus"></span>
|
||||
</div>
|
||||
|
||||
<!-- 重量显示 -->
|
||||
<!-- 重量显示(存储单位:克,展示层用 formatGramForCanteen 还原"斤") -->
|
||||
<div class="weight-display">
|
||||
<span class="weight-display__value" id="weightValue">0.00</span>
|
||||
<span class="weight-display__unit">斤</span>
|
||||
<span class="weight-display__value" id="weightValue">0g</span>
|
||||
<div class="weight-display__status" id="weightStatus">请放置食材至秤台</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -418,8 +417,8 @@
|
||||
<div class="batch-stat__label">品类数</div>
|
||||
</div>
|
||||
<div class="batch-stat">
|
||||
<div class="batch-stat__value" id="totalWeight">0.0</div>
|
||||
<div class="batch-stat__label">总重量(斤)</div>
|
||||
<div class="batch-stat__value" id="totalWeight">0g</div>
|
||||
<div class="batch-stat__label">总重量</div>
|
||||
</div>
|
||||
<div class="batch-stat">
|
||||
<div class="batch-stat__value batch-stat__value--green" id="totalTraced">0</div>
|
||||
@@ -459,6 +458,26 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
/* 克重展示格式化:500→"500g",1500→"1.5kg",25000→"25kg" */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
/* 验收秤场景兼容"斤"显示:整500倍数显示"斤",否则回退到 formatGram */
|
||||
/* 6250g → "12.5斤"(2位小数),整500倍数 → "N斤",其余 → formatGram */
|
||||
function formatGramForCanteen(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g % 500 === 0) return (g / 500) + '斤';
|
||||
if (g % 250 === 0) return ((g / 500).toFixed(1)) + '斤'; /* 半斤或0.5斤倍数 */
|
||||
if (g % 50 === 0) return ((g / 500).toFixed(2)) + '斤'; /* 50g倍数(0.1斤精度) */
|
||||
return formatGram(g);
|
||||
}
|
||||
|
||||
// 获取 URL 参数
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -469,23 +488,23 @@
|
||||
var userName = getUrlParam('user');
|
||||
if (userName) document.getElementById('topUser').textContent = '操作员: ' + userName;
|
||||
|
||||
// 任务单数据(含食材级应录入重量)
|
||||
// 任务单数据(含食材级应录入重量,单位:克)
|
||||
var taskDataMap = {
|
||||
'RW-20260613-001': {
|
||||
weight: 12.5,
|
||||
foods: { '黄瓜': 4.0, '土豆': 5.0, '西红柿': 3.5 }
|
||||
weight: 6250,
|
||||
foods: { '黄瓜': 2000, '土豆': 2500, '西红柿': 1750 }
|
||||
},
|
||||
'RW-20260613-002': {
|
||||
weight: 3.5,
|
||||
foods: { '胡萝卜': 2.0, '小番茄': 1.5 }
|
||||
weight: 1750,
|
||||
foods: { '胡萝卜': 1000, '小番茄': 750 }
|
||||
},
|
||||
'RW-20260613-003': {
|
||||
weight: 13.5,
|
||||
foods: { '大白菜': 8.0, '娃娃菜': 2.5, '土豆(晚餐)': 3.0 }
|
||||
weight: 6750,
|
||||
foods: { '大白菜': 4000, '娃娃菜': 1250, '土豆(晚餐)': 1500 }
|
||||
},
|
||||
'RW-20260613-004': {
|
||||
weight: 5.0,
|
||||
foods: { '西兰花': 3.0, '芹菜': 2.0 }
|
||||
weight: 2500,
|
||||
foods: { '西兰花': 1500, '芹菜': 1000 }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -585,7 +604,7 @@
|
||||
document.getElementById('taskNoDisplay').textContent = taskNo;
|
||||
var taskData = taskDataMap[taskNo];
|
||||
if (taskData) {
|
||||
document.getElementById('taskWeightDisplay').textContent = taskData.weight.toFixed(1) + ' 斤';
|
||||
document.getElementById('taskWeightDisplay').textContent = formatGramForCanteen(taskData.weight);
|
||||
initTaskList();
|
||||
}
|
||||
}
|
||||
@@ -708,19 +727,19 @@
|
||||
document.getElementById('traceCode').textContent = '--';
|
||||
document.getElementById('traceStatus').textContent = '';
|
||||
|
||||
// 随机食材 + 重量动画
|
||||
// 随机食材 + 重量动画(克为单位,500-2500g 范围)
|
||||
var item = aiResults[Math.floor(Math.random() * aiResults.length)];
|
||||
var targetWeight = (Math.random() * 4 + 1).toFixed(2) * 1;
|
||||
var targetWeight = Math.round((Math.random() * 2000 + 500) / 50) * 50;
|
||||
|
||||
var anim = 0;
|
||||
var timer = setInterval(function() {
|
||||
anim += 0.15;
|
||||
anim += 75;
|
||||
if (anim >= targetWeight) {
|
||||
anim = targetWeight;
|
||||
clearInterval(timer);
|
||||
onStable(item, targetWeight);
|
||||
}
|
||||
document.getElementById('weightValue').textContent = anim.toFixed(2);
|
||||
document.getElementById('weightValue').textContent = formatGramForCanteen(anim);
|
||||
}, 100);
|
||||
|
||||
// 延迟显示候选
|
||||
@@ -782,7 +801,7 @@
|
||||
matched.trace = currentTrace;
|
||||
matched.time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
matched.done = true;
|
||||
showToast(foodName + ' ' + currentWeight.toFixed(2) + '斤 已录入(应 ' + matched.requiredWeight.toFixed(1) + '斤)', 1500, 'success');
|
||||
showToast(foodName + ' ' + formatGramForCanteen(currentWeight) + ' 已录入(应 ' + formatGramForCanteen(matched.requiredWeight) + ')', 1500, 'success');
|
||||
} else {
|
||||
// 任务单外的食材,追加新行
|
||||
var entry = {
|
||||
@@ -796,7 +815,7 @@
|
||||
done: true
|
||||
};
|
||||
batchItems.push(entry);
|
||||
showToast(foodName + ' ' + currentWeight.toFixed(2) + '斤 已录入', 1500, 'success');
|
||||
showToast(foodName + ' ' + formatGramForCanteen(currentWeight) + ' 已录入', 1500, 'success');
|
||||
}
|
||||
|
||||
renderBatchList();
|
||||
@@ -856,8 +875,8 @@
|
||||
var el = document.createElement('div');
|
||||
el.className = 'batch-item' + (item.done ? '' : ' batch-item--pending');
|
||||
|
||||
var requiredHtml = item.requiredWeight !== null ? item.requiredWeight.toFixed(1) + '斤' : '--';
|
||||
var actualHtml = item.done ? item.weight.toFixed(2) + '斤' : '--';
|
||||
var requiredHtml = item.requiredWeight !== null ? formatGramForCanteen(item.requiredWeight) : '--';
|
||||
var actualHtml = item.done ? formatGramForCanteen(item.weight) : '--';
|
||||
var statusHtml = item.done
|
||||
? '<span class="batch-item__status batch-item__status--done">已完成</span>'
|
||||
: '<span class="batch-item__status batch-item__status--pending">待称重</span>';
|
||||
@@ -890,11 +909,11 @@
|
||||
if (it.done) { totalW += it.weight; doneCount++; }
|
||||
});
|
||||
document.getElementById('totalItems').textContent = Object.keys(names).length;
|
||||
document.getElementById('totalWeight').textContent = totalW.toFixed(1);
|
||||
document.getElementById('totalWeight').textContent = formatGramForCanteen(totalW);
|
||||
document.getElementById('totalTraced').textContent = doneCount;
|
||||
// 实时更新 task-info 条中的「已录入」
|
||||
var actualEl = document.getElementById('taskActualDisplay');
|
||||
if (actualEl) actualEl.textContent = totalW.toFixed(1) + ' 斤';
|
||||
if (actualEl) actualEl.textContent = formatGramForCanteen(totalW);
|
||||
}
|
||||
|
||||
// 重新识别:清除当前状态,重新触发 AI 识别
|
||||
@@ -936,7 +955,7 @@
|
||||
var cam = document.getElementById('cameraFrame');
|
||||
cam.classList.remove('detecting', 'stable');
|
||||
document.getElementById('statusDot').className = 'status-dot status-dot--idle';
|
||||
document.getElementById('weightValue').textContent = '0.00';
|
||||
document.getElementById('weightValue').textContent = '0g';
|
||||
document.getElementById('weightStatus').textContent = '请放置食材至秤台';
|
||||
document.getElementById('weightStatus').className = 'weight-display__status';
|
||||
document.getElementById('traceCode').textContent = '--';
|
||||
@@ -967,7 +986,7 @@
|
||||
}
|
||||
var totalW = 0;
|
||||
batchItems.forEach(function(it) { totalW += it.weight; });
|
||||
showToast('任务单提交成功:' + batchItems.length + '类食材,共 ' + totalW.toFixed(1) + '斤', 1500, 'success');
|
||||
showToast('任务单提交成功:' + batchItems.length + '类食材,共 ' + formatGramForCanteen(totalW), 1500, 'success');
|
||||
setTimeout(function() {
|
||||
var user = document.getElementById('topUser').textContent.replace('操作员: ', '');
|
||||
window.location.href = '01-washing-tasks.html' + (user ? '?user=' + encodeURIComponent(user) : '');
|
||||
|
||||
@@ -330,10 +330,9 @@
|
||||
<span class="trace-row__status" id="traceStatus"></span>
|
||||
</div>
|
||||
|
||||
<!-- 重量显示 -->
|
||||
<!-- 重量显示(存储单位:克,展示层用 formatGramForCanteen 还原"斤") -->
|
||||
<div class="weight-display">
|
||||
<span class="weight-display__value" id="weightValue">0.00</span>
|
||||
<span class="weight-display__unit">斤</span>
|
||||
<span class="weight-display__value" id="weightValue">0g</span>
|
||||
<div class="weight-display__status" id="weightStatus">请放置食材至秤台</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,8 +346,8 @@
|
||||
<div class="batch-stat__label">品类数</div>
|
||||
</div>
|
||||
<div class="batch-stat">
|
||||
<div class="batch-stat__value" id="totalWeight">0.0</div>
|
||||
<div class="batch-stat__label">总重量(斤)</div>
|
||||
<div class="batch-stat__value" id="totalWeight">0g</div>
|
||||
<div class="batch-stat__label">总重量</div>
|
||||
</div>
|
||||
<div class="batch-stat">
|
||||
<div class="batch-stat__value batch-stat__value--green" id="totalTraced">0</div>
|
||||
@@ -381,6 +380,23 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
function formatGramForCanteen(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g % 500 === 0) return (g / 500) + '斤';
|
||||
if (g % 250 === 0) return ((g / 500).toFixed(1)) + '斤';
|
||||
if (g % 50 === 0) return ((g / 500).toFixed(2)) + '斤';
|
||||
return formatGram(g);
|
||||
}
|
||||
|
||||
// 获取 URL 参数
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
@@ -391,30 +407,30 @@
|
||||
var userName = getUrlParam('user');
|
||||
if (userName) document.getElementById('topUser').textContent = '操作员: ' + userName;
|
||||
|
||||
// AI 识别结果库(含相似度)
|
||||
// AI 识别结果库(含相似度 + 出净率 yieldRate)
|
||||
var aiResults = [
|
||||
{ name: '土豆(净)', icon: '🥔', sim: 97 },
|
||||
{ name: '西红柿(净)', icon: '🍅', sim: 96 },
|
||||
{ name: '大白菜(净)', icon: '🥬', sim: 95 },
|
||||
{ name: '胡萝卜(净)', icon: '🥕', sim: 98 },
|
||||
{ name: '黄瓜(净)', icon: '🥒', sim: 94 },
|
||||
{ name: '娃娃菜(净)', icon: '🥬', sim: 93 }
|
||||
{ name: '土豆(净)', icon: '🥔', sim: 97, yieldRate: 90 },
|
||||
{ name: '西红柿(净)', icon: '🍅', sim: 96, yieldRate: 95 },
|
||||
{ name: '大白菜(净)', icon: '🥬', sim: 95, yieldRate: 88 },
|
||||
{ name: '胡萝卜(净)', icon: '🥕', sim: 98, yieldRate: 90 },
|
||||
{ name: '黄瓜(净)', icon: '🥒', sim: 94, yieldRate: 92 },
|
||||
{ name: '娃娃菜(净)', icon: '🥬', sim: 93, yieldRate: 88 }
|
||||
];
|
||||
|
||||
// 食材库(搜索用)
|
||||
// 食材库(搜索用,含出净率 yieldRate)
|
||||
var foodDB = [
|
||||
{ name: '土豆(净)', icon: '🥔' },
|
||||
{ name: '西红柿(净)', icon: '🍅' },
|
||||
{ name: '大白菜(净)', icon: '🥬' },
|
||||
{ name: '胡萝卜(净)', icon: '🥕' },
|
||||
{ name: '黄瓜(净)', icon: '🥒' },
|
||||
{ name: '娃娃菜(净)', icon: '🥬' },
|
||||
{ name: '茄子(净)', icon: '🍆' },
|
||||
{ name: '青椒(净)', icon: '🫑' },
|
||||
{ name: '芹菜(净)', icon: '🥬' },
|
||||
{ name: '菠菜(净)', icon: '🥬' },
|
||||
{ name: '豆角(净)', icon: '🫘' },
|
||||
{ name: '西兰花(净)', icon: '🥦' }
|
||||
{ name: '土豆(净)', icon: '🥔', yieldRate: 90 },
|
||||
{ name: '西红柿(净)', icon: '🍅', yieldRate: 95 },
|
||||
{ name: '大白菜(净)', icon: '🥬', yieldRate: 88 },
|
||||
{ name: '胡萝卜(净)', icon: '🥕', yieldRate: 90 },
|
||||
{ name: '黄瓜(净)', icon: '🥒', yieldRate: 92 },
|
||||
{ name: '娃娃菜(净)', icon: '🥬', yieldRate: 88 },
|
||||
{ name: '茄子(净)', icon: '🍆', yieldRate: 85 },
|
||||
{ name: '青椒(净)', icon: '🫑', yieldRate: 90 },
|
||||
{ name: '芹菜(净)', icon: '🥬', yieldRate: 80 },
|
||||
{ name: '菠菜(净)', icon: '🥬', yieldRate: 85 },
|
||||
{ name: '豆角(净)', icon: '🫘', yieldRate: 92 },
|
||||
{ name: '西兰花(净)', icon: '🥦', yieldRate: 88 }
|
||||
];
|
||||
|
||||
var scaleState = 'idle'; // idle | detecting | stable
|
||||
@@ -523,19 +539,19 @@
|
||||
document.getElementById('traceCode').textContent = '--';
|
||||
document.getElementById('traceStatus').textContent = '';
|
||||
|
||||
// 随机食材 + 重量动画
|
||||
// 随机食材 + 重量动画(克为单位,500-2500g 范围)
|
||||
var item = aiResults[Math.floor(Math.random() * aiResults.length)];
|
||||
var targetWeight = (Math.random() * 4 + 1).toFixed(2) * 1;
|
||||
var targetWeight = Math.round((Math.random() * 2000 + 500) / 50) * 50;
|
||||
|
||||
var anim = 0;
|
||||
var timer = setInterval(function() {
|
||||
anim += 0.15;
|
||||
anim += 75;
|
||||
if (anim >= targetWeight) {
|
||||
anim = targetWeight;
|
||||
clearInterval(timer);
|
||||
onStable(item, targetWeight);
|
||||
}
|
||||
document.getElementById('weightValue').textContent = anim.toFixed(2);
|
||||
document.getElementById('weightValue').textContent = formatGramForCanteen(anim);
|
||||
}, 100);
|
||||
|
||||
// 延迟显示候选
|
||||
@@ -590,7 +606,7 @@
|
||||
batchItems.push(entry);
|
||||
renderBatchList();
|
||||
updateSummary();
|
||||
showToast(entry.name + ' ' + entry.weight.toFixed(2) + '斤 已录入', 1500, 'success');
|
||||
showToast(entry.name + ' ' + formatGramForCanteen(entry.weight) + ' 已录入', 1500, 'success');
|
||||
resetScale();
|
||||
}
|
||||
|
||||
@@ -636,7 +652,7 @@
|
||||
'<div class="batch-item__name">' + item.name + '</div>' +
|
||||
'<div class="batch-item__trace">' + item.trace + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="batch-item__weight">+' + item.weight.toFixed(2) + '斤</div>' +
|
||||
'<div class="batch-item__weight">+' + formatGramForCanteen(item.weight) + '</div>' +
|
||||
'<button class="batch-item__del" onclick="deleteBatchItem(' + item.id + ')" title="删除">✖</button>';
|
||||
list.appendChild(el);
|
||||
});
|
||||
@@ -651,7 +667,7 @@
|
||||
totalW += it.weight;
|
||||
});
|
||||
document.getElementById('totalItems').textContent = Object.keys(names).length;
|
||||
document.getElementById('totalWeight').textContent = totalW.toFixed(1);
|
||||
document.getElementById('totalWeight').textContent = formatGramForCanteen(totalW);
|
||||
document.getElementById('totalTraced').textContent = batchItems.length;
|
||||
}
|
||||
|
||||
@@ -668,7 +684,7 @@
|
||||
cam.classList.remove('detecting', 'stable');
|
||||
document.getElementById('statusDot').className = 'status-dot status-dot--idle';
|
||||
document.getElementById('statusText').textContent = '就绪,等待食材放置';
|
||||
document.getElementById('weightValue').textContent = '0.00';
|
||||
document.getElementById('weightValue').textContent = '0g';
|
||||
document.getElementById('weightStatus').textContent = '请放置食材至秤台';
|
||||
document.getElementById('weightStatus').className = 'weight-display__status';
|
||||
document.getElementById('traceCode').textContent = '--';
|
||||
@@ -695,7 +711,7 @@
|
||||
batchItems = [];
|
||||
renderBatchList();
|
||||
document.getElementById('totalItems').textContent = '0';
|
||||
document.getElementById('totalWeight').textContent = '0.0';
|
||||
document.getElementById('totalWeight').textContent = '0g';
|
||||
document.getElementById('totalTraced').textContent = '0';
|
||||
document.getElementById('batchCount').textContent = '0';
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
|
||||
@@ -202,8 +202,8 @@
|
||||
<div class="stat-card__value stat-card__value--blue" id="statTotal">8</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">总重量(斤)</div>
|
||||
<div class="stat-card__value" id="statWeight">33.3</div>
|
||||
<div class="stat-card__label">总重量</div>
|
||||
<div class="stat-card__value" id="statWeight">33.3 斤</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<div class="record-card__batch">LC-20260613-001</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="2600">
|
||||
<span class="record-card__weight-value">5.2</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@
|
||||
<div class="record-card__batch">LC-20260613-001</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="1900">
|
||||
<span class="record-card__weight-value">3.8</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -268,7 +268,7 @@
|
||||
<div class="record-card__batch">LC-20260613-001</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="4100">
|
||||
<span class="record-card__weight-value">8.2</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@
|
||||
<div class="record-card__batch">LC-20260613-001</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="1250">
|
||||
<span class="record-card__weight-value">2.5</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -312,7 +312,7 @@
|
||||
<div class="record-card__batch">LC-20260613-002</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="2150">
|
||||
<span class="record-card__weight-value">4.3</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -334,7 +334,7 @@
|
||||
<div class="record-card__batch">LC-20260613-002</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="1300">
|
||||
<span class="record-card__weight-value">2.6</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -356,7 +356,7 @@
|
||||
<div class="record-card__batch">LC-20260613-003</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="1600">
|
||||
<span class="record-card__weight-value">3.2</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -378,7 +378,7 @@
|
||||
<div class="record-card__batch">LC-20260613-003</div>
|
||||
</div>
|
||||
<div class="record-card__type record-card__type--in">来料过秤</div>
|
||||
<div class="record-card__weight">
|
||||
<div class="record-card__weight" data-weight-g="1750">
|
||||
<span class="record-card__weight-value">3.5</span>
|
||||
<span class="record-card__weight-unit">斤</span>
|
||||
</div>
|
||||
@@ -403,6 +403,34 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 spec-utils-reference.md 内联) === */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
function formatGramForCanteen(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g % 500 === 0) return (g / 500) + '斤';
|
||||
if (g % 250 === 0) return ((g / 500).toFixed(1)) + '斤';
|
||||
if (g % 50 === 0) return ((g / 500).toFixed(2)) + '斤';
|
||||
return formatGram(g);
|
||||
}
|
||||
/* 历史卡片重量渲染(从 data-weight-g 读克数,展示"X 斤") */
|
||||
function renderHistoryWeights() {
|
||||
var nodes = document.querySelectorAll('.record-card__weight[data-weight-g]');
|
||||
nodes.forEach(function(el) {
|
||||
var g = parseInt(el.getAttribute('data-weight-g'), 10);
|
||||
var formatted = formatGramForCanteen(g);
|
||||
el.querySelector('.record-card__weight-value').textContent = formatted.replace(/斤$/, '');
|
||||
el.querySelector('.record-card__weight-unit').textContent = formatted.indexOf('斤') !== -1 ? '斤' : '';
|
||||
});
|
||||
}
|
||||
renderHistoryWeights();
|
||||
|
||||
// 获取 URL 参数
|
||||
function getUrlParam(name) {
|
||||
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
||||
|
||||
@@ -200,22 +200,10 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="table-card__content">
|
||||
<table>
|
||||
<thead><tr><th>序号</th><th>名称</th><th>数据值</th><th>状态</th><th>描述</th><th>创建时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>炒</td><td>1</td><td><span class="tag tag-green">启用</span></td><td>大火快炒</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>2</td><td>蒸</td><td>2</td><td><span class="tag tag-green">启用</span></td><td>隔水蒸制</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>3</td><td>炖</td><td>3</td><td><span class="tag tag-green">启用</span></td><td>慢火炖煮</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>4</td><td>煮</td><td>4</td><td><span class="tag tag-green">启用</span></td><td>水煮</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>5</td><td>凉拌</td><td>5</td><td><span class="tag tag-green">启用</span></td><td>不加热</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>6</td><td>炸</td><td>6</td><td><span class="tag tag-green">启用</span></td><td>油炸</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>7</td><td>烤</td><td>7</td><td><span class="tag tag-green">启用</span></td><td>烤箱/明火</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>8</td><td>煎</td><td>8</td><td><span class="tag tag-green">启用</span></td><td>少油煎制</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>9</td><td>焖</td><td>9</td><td><span class="tag tag-green">启用</span></td><td>加盖焖制</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>10</td><td>卤</td><td>10</td><td><span class="tag tag-green">启用</span></td><td>卤汁浸煮</td><td>2026-01-01 10:00:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>11</td><td>汆</td><td>11</td><td><span class="tag tag-gray">停用</span></td><td>沸水汆烫</td><td>2026-03-20 14:30:00</td><td class="td-actions"><span class="link" onclick="openDialog('edit',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>
|
||||
</tbody>
|
||||
<tbody id="dictTbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination"><span>显示 11 条,共 11 条</span><button class="page-btn active">1</button></div>
|
||||
<div class="pagination"><span id="paginationText">显示 0 条,共 0 条</span><button class="page-btn active">1</button></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -237,13 +225,90 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
|
||||
(function(){var active=document.querySelector('.sidebar-item.active');if(!active)return;var sub=active.closest('.sidebar-subgroup');if(sub)sub.classList.add('open');})();
|
||||
|
||||
/* 解析 URL 参数,设置面包屑和标题 */
|
||||
/* === 字典项数据集(按 code 分组) === */
|
||||
/* 主烹饪法 cook_method(默认兜底) */
|
||||
var DICT_DATA = {
|
||||
'cook_method': [
|
||||
{name:'炒', value:'1', status:'启用', desc:'大火快炒', time:'2026-01-01 10:00:00'},
|
||||
{name:'蒸', value:'2', status:'启用', desc:'隔水蒸制', time:'2026-01-01 10:00:00'},
|
||||
{name:'炖', value:'3', status:'启用', desc:'慢火炖煮', time:'2026-01-01 10:00:00'},
|
||||
{name:'煮', value:'4', status:'启用', desc:'水煮', time:'2026-01-01 10:00:00'},
|
||||
{name:'凉拌', value:'5', status:'启用', desc:'不加热', time:'2026-01-01 10:00:00'},
|
||||
{name:'炸', value:'6', status:'启用', desc:'油炸', time:'2026-01-01 10:00:00'},
|
||||
{name:'烤', value:'7', status:'启用', desc:'烤箱/明火', time:'2026-01-01 10:00:00'},
|
||||
{name:'煎', value:'8', status:'启用', desc:'少油煎制', time:'2026-01-01 10:00:00'},
|
||||
{name:'焖', value:'9', status:'启用', desc:'加盖焖制', time:'2026-01-01 10:00:00'},
|
||||
{name:'卤', value:'10', status:'启用', desc:'卤汁浸煮', time:'2026-01-01 10:00:00'},
|
||||
{name:'汆', value:'11', status:'停用', desc:'沸水汆烫', time:'2026-03-20 14:30:00'}
|
||||
],
|
||||
/* 包装单位字典(规格 Tab) — baseGram + unit 构成统一规格 */
|
||||
'spec_unit': [
|
||||
{name:'克', value:'1', status:'启用', desc:'基础质量单位,克重计量基准', time:'2026-08-04 10:00:00'},
|
||||
{name:'袋', value:'2', status:'启用', desc:'典型 500g/袋,净菜包装常用', time:'2026-08-04 10:00:00'},
|
||||
{name:'箱', value:'3', status:'启用', desc:'大件包装,典型 5kg/箱 或 25kg/箱', time:'2026-08-04 10:00:00'},
|
||||
{name:'份', value:'4', status:'启用', desc:'餐品单位,按配方定义克重', time:'2026-08-04 10:00:00'},
|
||||
{name:'把', value:'5', status:'启用', desc:'长条形食材捆扎单位,约 500g/把', time:'2026-08-04 10:00:00'},
|
||||
{name:'瓶', value:'6', status:'启用', desc:'液体容器单位,典型 500ml/瓶(≈500g)', time:'2026-08-04 10:00:00'},
|
||||
{name:'罐', value:'7', status:'启用', desc:'固液容器单位,典型 1000g/罐', time:'2026-08-04 10:00:00'},
|
||||
{name:'桶', value:'8', status:'启用', desc:'大液体单位,典型 5kg/桶', time:'2026-08-04 10:00:00'},
|
||||
{name:'散装', value:'9', status:'启用', desc:'无固定包装,按克计', time:'2026-08-04 10:00:00'}
|
||||
],
|
||||
/* 切配类型字典(规格 Tab) — 对应 CutSpec.shape */
|
||||
'spec_cut_shape': [
|
||||
{name:'丝(细)', value:'1', status:'启用', desc:'标准 3×1×1mm · 平刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'丝(中)', value:'2', status:'启用', desc:'标准 5×1×1mm · 平刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'丝(粗)', value:'3', status:'启用', desc:'标准 5×2×2mm · 平刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'片(薄)', value:'4', status:'启用', desc:'厚度 1mm · 平刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'片(中)', value:'5', status:'启用', desc:'厚度 3mm · 平刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'片(厚)', value:'6', status:'启用', desc:'厚度 5mm · 斜刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'丁(小)', value:'7', status:'启用', desc:'5×5×5mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'丁(中)', value:'8', status:'启用', desc:'10×10×10mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'丁(大)', value:'9', status:'启用', desc:'15×15×15mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'块(小)', value:'10', status:'启用', desc:'20×20×20mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'块(中)', value:'11', status:'启用', desc:'30×30×30mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'块(大)', value:'12', status:'启用', desc:'40×40×40mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'段', value:'13', status:'启用', desc:'长条形 50mm · 直刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'条', value:'14', status:'启用', desc:'长条形 30mm · 斜刀', time:'2026-08-04 10:00:00'},
|
||||
{name:'泥', value:'15', status:'启用', desc:'糜状,无尺寸', time:'2026-08-04 10:00:00'},
|
||||
{name:'末', value:'16', status:'启用', desc:'碎粒,<5mm', time:'2026-08-04 10:00:00'}
|
||||
],
|
||||
/* 份型字典(规格 Tab) — 餐品规格的份量维度 */
|
||||
'spec_portion': [
|
||||
{name:'特小份', value:'1', status:'启用', desc:'约 100g/份,婴幼儿/儿童', time:'2026-08-04 10:00:00'},
|
||||
{name:'小份', value:'2', status:'启用', desc:'约 200g/份,老年人/减重', time:'2026-08-04 10:00:00'},
|
||||
{name:'标准份', value:'3', status:'启用', desc:'约 300g/份,标准成人(基准参考)', time:'2026-08-04 10:00:00'},
|
||||
{name:'中份', value:'4', status:'启用', desc:'约 350g/份,标准成人略增', time:'2026-08-04 10:00:00'},
|
||||
{name:'大份', value:'5', status:'启用', desc:'约 400g/份,体力劳动', time:'2026-08-04 10:00:00'},
|
||||
{name:'特大份', value:'6', status:'启用', desc:'约 500g/份,运动员/特殊', time:'2026-08-04 10:00:00'}
|
||||
]
|
||||
};
|
||||
|
||||
/* 按字典 code 渲染字典项表格 */
|
||||
function renderDictItems(code){
|
||||
var items = DICT_DATA[code] || DICT_DATA['cook_method'];
|
||||
var tbody = document.getElementById('dictTbody');
|
||||
if(!tbody) return;
|
||||
var html = '';
|
||||
items.forEach(function(item, i){
|
||||
var statusTag = item.status === '启用'
|
||||
? '<span class="tag tag-green">启用</span>'
|
||||
: '<span class="tag tag-gray">停用</span>';
|
||||
html += '<tr><td>'+(i+1)+'</td><td>'+item.name+'</td><td>'+item.value+'</td><td>'+statusTag+'</td><td>'+item.desc+'</td><td>'+item.time+'</td><td class="td-actions"><span class="link" onclick="openDialog(\'edit\',this)">编辑</span> <span class="link link-danger">删除</span></td></tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
var pagText = document.getElementById('paginationText');
|
||||
if(pagText) pagText.textContent = '显示 '+items.length+' 条,共 '+items.length+' 条';
|
||||
}
|
||||
|
||||
/* 解析 URL 参数,设置面包屑和标题,并渲染对应字典项 */
|
||||
(function(){
|
||||
var params=new URLSearchParams(window.location.search);
|
||||
var code=params.get('code')||'cook_method';
|
||||
var name=params.get('name')||'主烹饪法';
|
||||
document.getElementById('breadcrumbName').textContent=name+' - 字典配置';
|
||||
document.getElementById('tableTitle').textContent=name+' - 字典配置';
|
||||
document.title='营养管理 - '+name+' 字典配置 | 健康CQ原型';
|
||||
renderDictItems(code);
|
||||
})();
|
||||
|
||||
/* 弹窗交互 */
|
||||
|
||||
@@ -182,10 +182,10 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<main class="main">
|
||||
<!-- 统计卡片 -->
|
||||
<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">6类</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">186项</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">178项</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">2026-04-28</div><div class="stat-card__label">最近更新</div></div></div>
|
||||
<div class="stat-card"><div class="stat-card__icon blue">📖</div><div class="stat-card__info"><div class="stat-card__value">7类</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">217项</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">209项</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">2026-08-04</div><div class="stat-card__label">最近更新</div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换栏 -->
|
||||
@@ -196,6 +196,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="page-tab" onclick="switchTab(3)">适配</div>
|
||||
<div class="page-tab" onclick="switchTab(4)">仓储/规格</div>
|
||||
<div class="page-tab" onclick="switchTab(5)">业务</div>
|
||||
<div class="page-tab" onclick="switchTab(6)">规格</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab1: 烹饪/工艺 -->
|
||||
@@ -332,6 +333,27 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="pagination"><span>显示 6 条,共 6 条</span><button class="page-btn active">1</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab7: 规格 -->
|
||||
<div class="tab-panel">
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item"><label>字典名称</label><input class="filter-input" placeholder="请输入"></div>
|
||||
<div class="filter-item"><label>字典编码</label><input class="filter-input" placeholder="请输入"></div>
|
||||
<button class="btn btn-primary">查询</button>
|
||||
<button class="btn btn-default">重置</button>
|
||||
</div>
|
||||
<div class="action-bar"><button class="btn btn-primary" onclick="openDialog('add')">➕ 新增</button></div>
|
||||
<div class="table-card">
|
||||
<div class="table-card__header"><div class="table-card__title">规格</div></div>
|
||||
<div class="table-card__content"><table><thead><tr><th>字典名称</th><th>字典编码</th><th>字典分类</th><th>描述</th><th>创建时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>包装单位</td><td>spec_unit</td><td>规格</td><td>袋/箱/份/把/散装等包装计量单位(克重换算基准)</td><td>2026-08-04 10:00:00</td><td class="td-actions"><a class="link" href="din-dict-config.html?code=spec_unit&name=包装单位">字典配置</a><span class="link" onclick="openDialog('edit',this)">编辑</span><span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>切配类型</td><td>spec_cut_shape</td><td>规格</td><td>丝/片/丁/块等切配形状规格(含尺寸与刀工)</td><td>2026-08-04 10:00:00</td><td class="td-actions"><a class="link" href="din-dict-config.html?code=spec_cut_shape&name=切配类型">字典配置</a><span class="link" onclick="openDialog('edit',this)">编辑</span><span class="link link-danger">删除</span></td></tr>
|
||||
<tr><td>份型</td><td>spec_portion</td><td>规格</td><td>小份/中份/大份等餐品份型规格</td><td>2026-08-04 10:00:00</td><td class="td-actions"><a class="link" href="din-dict-config.html?code=spec_portion&name=份型">字典配置</a><span class="link" onclick="openDialog('edit',this)">编辑</span><span class="link link-danger">删除</span></td></tr>
|
||||
</tbody></table></div>
|
||||
<div class="pagination"><span>显示 3 条,共 3 条</span><button class="page-btn active">1</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
@@ -349,7 +371,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var tabNames=['烹饪/工艺','感官','营养','适配','仓储/规格','业务'];
|
||||
var tabNames=['烹饪/工艺','感官','营养','适配','仓储/规格','业务','规格'];
|
||||
var currentTab=0;
|
||||
function switchTab(idx){
|
||||
currentTab=idx;
|
||||
|
||||
@@ -81,6 +81,15 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
.spec-add-row:hover{background:var(--primary-light) !important;}
|
||||
.spec-add-row td{color:var(--primary);cursor:pointer;border-bottom:none;padding:5px 10px;}
|
||||
.spec-edit-row .form-input{height:28px;font-size:12px;padding:0 8px;}
|
||||
/* 规格输入组(表格内):左 input 克重 + 右 select 份型 连在一起 */
|
||||
.spec-input-group{display:flex;align-items:stretch;height:28px;}
|
||||
.spec-input-group input{flex:1;border-radius:4px 0 0 4px;border-right:none;padding:0 8px;font-size:12px;height:28px;min-width:0;}
|
||||
.spec-input-group select{width:90px;border-radius:0 4px 4px 0;padding:0 8px;font-size:12px;height:28px;flex-shrink:0;}
|
||||
/* 规格单元格只读展示:克重g / 份型 */
|
||||
.spec-cell-view{display:flex;align-items:center;gap:4px;}
|
||||
.spec-cell-view__gram{font-weight:500;}
|
||||
.spec-cell-view__sep{color:var(--text-placeholder);}
|
||||
.spec-cell-view__name{color:var(--text-secondary);}
|
||||
.spec-toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;}
|
||||
.spec-toggle__switch{width:32px;height:18px;background:#D9D9D9;border-radius:9px;position:relative;transition:background .2s;flex-shrink:0;}
|
||||
.spec-toggle__switch::after{content:'';position:absolute;left:2px;top:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,0.15);}
|
||||
@@ -472,11 +481,11 @@ var STEP_HTML = [
|
||||
</div>
|
||||
<div class="form-subtitle">📐 餐品规格</div>
|
||||
<table class="spec-table" id="specTable">
|
||||
<thead><tr><th>规格</th><th>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
|
||||
<thead><tr><th style="width:200px;">规格(克重/份型)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
|
||||
<tbody id="specBody">
|
||||
<tr><td>标准份</td><td>350</td><td>15.00</td><td>10.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td><div class="spec-toggle active" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td></tr>
|
||||
<tr><td>小份</td><td>200</td><td>10.00</td><td>7.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td></tr>
|
||||
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="7" style="text-align:center;">+ 添加规格</td></tr>
|
||||
<tr><td><div class="spec-cell-view"><span class="spec-cell-view__gram">350g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">标准份</span></div></td><td>15.00</td><td>10.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td><div class="spec-toggle active" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td></tr>
|
||||
<tr><td><div class="spec-cell-view"><span class="spec-cell-view__gram">200g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">小份</span></div></td><td>10.00</td><td>7.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td></tr>
|
||||
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="6" style="text-align:center;">+ 添加规格</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-subtitle">📷 菜品展示</div>
|
||||
@@ -653,25 +662,40 @@ function prevStep(){ if(currentStep>0){currentStep--;renderStep();window.scrollT
|
||||
function submitForm(){ if(pageMode==='convert'){ alert('转化成功!餐品已加入餐品总库。'); window.location.href='din-dish-all.html'; } else { alert('提交成功!'); window.location.href='din-dish-all.html'; } }
|
||||
|
||||
/* 规格管理 */
|
||||
/* 份型下拉选项(枚举来源:spec_portion 字典,与展示页保持一致) */
|
||||
var SPEC_PORTION_OPTIONS = ['标准份','小份','大份','迷你份','加大份'];
|
||||
function buildPortionOptions(selected){
|
||||
var html='<option value="">请选择</option>';
|
||||
SPEC_PORTION_OPTIONS.forEach(function(p){
|
||||
html+='<option value="'+p+'"'+(p===selected?' selected':'')+'>'+p+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
function addSpecRow(addRow){
|
||||
if(document.querySelector('.spec-edit-row')) return;
|
||||
var tr=document.createElement('tr');
|
||||
tr.className='spec-edit-row';
|
||||
tr.innerHTML='<td><select class="form-select" style="height:28px;font-size:12px;padding:0 24px 0 8px;"><option>请选择</option><option>标准份</option><option>小份</option><option>大份</option><option>迷你份</option><option>加大份</option></select></td><td><input class="form-input" type="number" placeholder="重量"></td><td><input class="form-input" type="number" placeholder="餐品售价"></td><td><input class="form-input" type="number" placeholder="净菜包售价"></td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td></td><td><button class="btn-text btn-text--primary" onclick="confirmSpec(this)">确定</button> <button class="btn-text btn-text--danger" onclick="cancelSpec(this)">取消</button></td>';
|
||||
tr.innerHTML='<td><div class="spec-input-group"><input class="form-input" type="number" placeholder="克重" min="0"><select class="form-select">'+buildPortionOptions()+'</select></div></td><td><input class="form-input" type="number" placeholder="餐品售价"></td><td><input class="form-input" type="number" placeholder="净菜包售价"></td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td></td><td><button class="btn-text btn-text--primary" onclick="confirmSpec(this)">确定</button> <button class="btn-text btn-text--danger" onclick="cancelSpec(this)">取消</button></td>';
|
||||
addRow.parentNode.insertBefore(tr,addRow);
|
||||
}
|
||||
function confirmSpec(btn){
|
||||
var row=btn.closest('tr');
|
||||
var sel=row.querySelector('select');
|
||||
var inputs=row.querySelectorAll('input');
|
||||
var name=sel?sel.value:'';
|
||||
var weight=inputs[0].value.trim(),price=inputs[1].value.trim(),cleanPrice=inputs[2].value.trim();
|
||||
if(!name||name==='请选择'||!weight||!price){alert('请填写完整信息');return;}
|
||||
var group=row.querySelector('.spec-input-group');
|
||||
var gramInput=group.querySelector('input[type="number"]');
|
||||
var sel=group.querySelector('select');
|
||||
var priceInput=row.querySelectorAll('input[type="number"]')[1];
|
||||
var cleanPriceInput=row.querySelectorAll('input[type="number"]')[2];
|
||||
var portion=sel?sel.value:'';
|
||||
var gram=gramInput?gramInput.value.trim():'';
|
||||
var price=priceInput?priceInput.value.trim():'';
|
||||
var cleanPrice=cleanPriceInput?cleanPriceInput.value.trim():'';
|
||||
if(!portion||portion==='请选择'||!gram||!price){alert('请填写完整信息');return;}
|
||||
var cleanPriceText = cleanPrice ? parseFloat(cleanPrice).toFixed(2) : '—';
|
||||
var saleActive = !!row.querySelector('.sale-toggle.active');
|
||||
var saleHtml = '<div class="sale-toggle'+(saleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(saleActive?'上架':'下架')+'</span></div>';
|
||||
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+parseFloat(gram)+'g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+portion+'</span></div>';
|
||||
row.className='';
|
||||
row.innerHTML='<td>'+name+'</td><td>'+weight+'</td><td>'+parseFloat(price).toFixed(2)+'</td><td>'+cleanPriceText+'</td><td>'+saleHtml+'</td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td>';
|
||||
row.innerHTML='<td>'+specViewHtml+'</td><td>'+parseFloat(price).toFixed(2)+'</td><td>'+cleanPriceText+'</td><td>'+saleHtml+'</td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td>';
|
||||
}
|
||||
function toggleCleanSale(el){
|
||||
var active = el.classList.toggle('active');
|
||||
@@ -1030,7 +1054,7 @@ var EDIT_DATA = {
|
||||
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
|
||||
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],
|
||||
specs:[{spec:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{spec:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false,isDefault:false},{spec:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false,isDefault:false}],
|
||||
specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false,isDefault:false}],
|
||||
cookedWeight:200,
|
||||
composition:[{name:'五花肉',netType:'红烧块',amount:150,pct:'60%',type:'主材',allergen:'无',note:'—'},{name:'冰糖',netType:'糖浆',amount:20,pct:'8%',type:'调料',allergen:'无',note:'上色'},{name:'酱油',netType:'调味汁',amount:15,pct:'6%',type:'调料',allergen:'含大豆',note:'调味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'4%',type:'调料',allergen:'无',note:'去腥'}],
|
||||
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:100,steps:6,desc:'1.五花肉切块焯水;2.锅中煎至金黄;3.加冰糖炒色;4.加酱油料酒;5.加水小火炖40分钟;6.大火收汁'},
|
||||
@@ -1073,7 +1097,7 @@ if(pageMode==='convert' && INNOV){
|
||||
EDIT_DATA.tag = INNOV.tag;
|
||||
EDIT_DATA.status = '上架';
|
||||
EDIT_DATA.crowd = [];
|
||||
EDIT_DATA.specs = [{spec:'标准份',weight:350,price:15,cleanPrice:10,cleanSale:false,isDefault:true},{spec:'小份',weight:200,price:10,cleanPrice:7,cleanSale:false,isDefault:false}];
|
||||
EDIT_DATA.specs = [{portion:'标准份',baseGram:350,unit:'份',price:15,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'小份',baseGram:200,unit:'份',price:10,cleanPrice:7,cleanSale:false,isDefault:false}];
|
||||
EDIT_DATA.cookedWeight = INNOV.cookedWeight || 200;
|
||||
EDIT_DATA.composition = INNOV.composition;
|
||||
EDIT_DATA.cook = INNOV.cook;
|
||||
@@ -1124,7 +1148,9 @@ function fillEditData(){
|
||||
var tr = document.createElement('tr');
|
||||
var saleActive = s.cleanSale === true;
|
||||
var saleHtml = '<div class="sale-toggle'+(saleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(saleActive?'上架':'下架')+'</span></div>';
|
||||
tr.innerHTML = '<td><select class="form-select form-select--sm"><option'+(s.spec==='标准份'?' selected':'')+'>标准份</option><option'+(s.spec==='小份'?' selected':'')+'>小份</option><option'+(s.spec==='大份'?' selected':'')+'>大份</option><option'+(s.spec==='加大份'?' selected':'')+'>加大份</option></select></td><td><input class="form-input form-input--sm" value="'+s.weight+'"></td><td><input class="form-input form-input--sm" value="'+s.price+'"></td><td><input class="form-input form-input--sm" value="'+(s.cleanPrice!==undefined?s.cleanPrice:'')+'"></td><td>'+saleHtml+'</td><td><input type="checkbox"'+(s.isDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
|
||||
var portionVal = s.portion || s.spec || '';
|
||||
var gramVal = s.baseGram!==undefined?s.baseGram:(s.weight!==undefined?s.weight:'');
|
||||
tr.innerHTML = '<td><div class="spec-input-group"><input class="form-input form-input--sm" type="number" value="'+gramVal+'"><select class="form-select form-select--sm">'+buildPortionOptions(portionVal)+'</select></div></td><td><input class="form-input form-input--sm" value="'+s.price+'"></td><td><input class="form-input form-input--sm" value="'+(s.cleanPrice!==undefined?s.cleanPrice:'')+'"></td><td>'+saleHtml+'</td><td><input type="checkbox"'+(s.isDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
|
||||
specBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
.info-table tr:hover{background:#F8FBFF;}
|
||||
.spec-detail-table th{padding:6px 12px;font-size:12px;}
|
||||
.spec-detail-table td{padding:6px 12px;font-size:12px;}
|
||||
/* 规格只读展示(克重g / 份型 合并单列) */
|
||||
.spec-cell-view{display:flex;align-items:center;gap:4px;}
|
||||
.spec-cell-view__gram{font-weight:500;}
|
||||
.spec-cell-view__sep{color:var(--text-placeholder);}
|
||||
.spec-cell-view__name{color:var(--text-secondary);}
|
||||
/* 外售净菜包胶囊开关 */
|
||||
.sale-toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;}
|
||||
.sale-toggle__switch{width:32px;height:18px;background:#D9D9D9;border-radius:9px;position:relative;transition:background .2s;flex-shrink:0;}
|
||||
@@ -378,7 +383,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<script>
|
||||
// 餐品数据
|
||||
const DISHES = [
|
||||
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
|
||||
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
|
||||
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
|
||||
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
|
||||
@@ -389,7 +394,7 @@ const DISHES = [
|
||||
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
|
||||
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
|
||||
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
|
||||
@@ -400,7 +405,7 @@ const DISHES = [
|
||||
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
|
||||
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
|
||||
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
|
||||
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
|
||||
@@ -411,7 +416,7 @@ const DISHES = [
|
||||
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
|
||||
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
|
||||
promoVideos:[],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
|
||||
@@ -422,7 +427,7 @@ const DISHES = [
|
||||
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
|
||||
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
|
||||
},
|
||||
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
|
||||
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
|
||||
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
|
||||
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
|
||||
@@ -475,7 +480,9 @@ function renderBasic(d){
|
||||
var cleanPrice = (s.cleanPrice!==undefined && s.cleanPrice!==null) ? parseFloat(s.cleanPrice).toFixed(2) : '—';
|
||||
var cleanSaleActive = s.cleanSale === true;
|
||||
var cleanSaleHtml = '<div class="sale-toggle'+(cleanSaleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(cleanSaleActive?'上架':'下架')+'</span></div>';
|
||||
return '<tr><td>'+s.name+'</td><td>'+s.weight+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
|
||||
var gramText=s.baseGram?(parseFloat(s.baseGram)+'g'):'';
|
||||
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+gramText+'</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+(s.portion||'—')+'</span></div>';
|
||||
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
|
||||
}).join('');
|
||||
return `<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>
|
||||
<div class="info-grid">
|
||||
@@ -491,7 +498,7 @@ function renderBasic(d){
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">${(d.crowd||[]).map(function(c){return '<div style="display:flex;align-items:center;gap:4px;"><span style="font-size:var(--font-sm);">'+c.name+'</span>'+levelTag(c.level)+'</div>';}).join('')}</div>
|
||||
${d.crowdNote?'<div style="font-size:var(--font-sm);color:var(--text-secondary);background:#FFFBE6;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid #FFE58F;">⚠️ '+d.crowdNote+'</div>':''}</div>
|
||||
<div class="info-section"><div class="info-section__title">📐 餐品规格(共 ${(d.specs||[]).length} 项)</div>
|
||||
<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>${specRows}</tbody></table></div>
|
||||
<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>${specRows}</tbody></table></div>
|
||||
${renderPricingSection(d)}
|
||||
<div class="info-section"><div class="info-section__title">📷 菜品展示</div>
|
||||
<div class="media-grid">
|
||||
@@ -795,6 +802,29 @@ document.addEventListener('click', function(e){
|
||||
renderDishList();
|
||||
renderDetail();
|
||||
|
||||
/* ========== 跨页 specs 共享(阶段三任务 3.0) ========== */
|
||||
/* 把 DISHES 数组序列化到 localStorage,供下游订单/组配/包装等页面读取 */
|
||||
function persistDishSpecs(){
|
||||
try {
|
||||
var simplified = DISHES.map(function(d){
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
category: d.category,
|
||||
status: d.status,
|
||||
cookedWeight: d.cookedWeight,
|
||||
yieldRate: d.yieldRate,
|
||||
specs: (d.specs||[]).map(function(s){
|
||||
return {portion:s.portion, baseGram:s.baseGram, unit:s.unit||'份',
|
||||
price:s.price, cleanPrice:s.cleanPrice, cleanSale:s.cleanSale===true, isDefault:s.isDefault===true};
|
||||
})
|
||||
};
|
||||
});
|
||||
localStorage.setItem('dish_specs_v1', JSON.stringify({version:1, updatedAt:Date.now(), dishes:simplified}));
|
||||
} catch(e){ console.warn('persistDishSpecs 失败', e); }
|
||||
}
|
||||
persistDishSpecs();
|
||||
|
||||
/* ========== 餐品分类管理 ========== */
|
||||
var nextDishCatId = 100;
|
||||
var dishCategories = [
|
||||
|
||||
@@ -118,6 +118,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
.info-table tr:hover{background:#F8FBFF;}
|
||||
.spec-detail-table th{padding:6px 12px;font-size:12px;}
|
||||
.spec-detail-table td{padding:6px 12px;font-size:12px;}
|
||||
/* 规格只读展示(克重g / 份型 合并单列) */
|
||||
.spec-cell-view{display:flex;align-items:center;gap:4px;}
|
||||
.spec-cell-view__gram{font-weight:500;}
|
||||
.spec-cell-view__sep{color:var(--text-placeholder);}
|
||||
.spec-cell-view__name{color:var(--text-secondary);}
|
||||
/* 外售净菜包胶囊开关 */
|
||||
.sale-toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;}
|
||||
.sale-toggle__switch{width:32px;height:18px;background:#D9D9D9;border-radius:9px;position:relative;transition:background .2s;flex-shrink:0;}
|
||||
@@ -402,7 +407,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<script>
|
||||
// 餐品数据
|
||||
const DISHES = [
|
||||
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
|
||||
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
|
||||
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
|
||||
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
|
||||
@@ -413,7 +418,7 @@ const DISHES = [
|
||||
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
|
||||
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
|
||||
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
|
||||
@@ -424,7 +429,7 @@ const DISHES = [
|
||||
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
|
||||
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
|
||||
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
|
||||
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
|
||||
@@ -435,7 +440,7 @@ const DISHES = [
|
||||
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
|
||||
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
|
||||
},
|
||||
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
|
||||
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
|
||||
promoVideos:[],
|
||||
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
|
||||
@@ -446,7 +451,7 @@ const DISHES = [
|
||||
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
|
||||
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
|
||||
},
|
||||
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
|
||||
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
|
||||
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
|
||||
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
|
||||
@@ -499,7 +504,9 @@ function renderBasic(d){
|
||||
var cleanPrice = (s.cleanPrice!==undefined && s.cleanPrice!==null) ? parseFloat(s.cleanPrice).toFixed(2) : '—';
|
||||
var cleanSaleActive = s.cleanSale === true;
|
||||
var cleanSaleHtml = '<div class="sale-toggle'+(cleanSaleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(cleanSaleActive?'上架':'下架')+'</span></div>';
|
||||
return '<tr><td>'+s.name+'</td><td>'+s.weight+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
|
||||
var gramText=s.baseGram?(parseFloat(s.baseGram)+'g'):'';
|
||||
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+gramText+'</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+(s.portion||'—')+'</span></div>';
|
||||
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
|
||||
}).join('');
|
||||
return '<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>'
|
||||
+ '<div class="info-grid">'
|
||||
@@ -516,7 +523,7 @@ function renderBasic(d){
|
||||
+ (d.crowdNote?'<div style="font-size:var(--font-sm);color:var(--text-secondary);background:#FFFBE6;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid #FFE58F;">⚠️ '+d.crowdNote+'</div>':'')
|
||||
+ '</div>'
|
||||
+ '<div class="info-section"><div class="info-section__title">📐 餐品规格(共 '+(d.specs||[]).length+' 项)</div>'
|
||||
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>'
|
||||
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>'
|
||||
+ renderPricingSection(d)
|
||||
+ '<div class="info-section"><div class="info-section__title">📷 菜品展示</div>'
|
||||
+ '<div class="media-grid">'
|
||||
|
||||
@@ -512,15 +512,35 @@ function submitForm(){
|
||||
}
|
||||
|
||||
/* === 净菜类型动态行 === */
|
||||
/* 切配形状选项(对应字典 spec_cut_shape) */
|
||||
var VEG_SHAPE_OPTIONS=['','丝(细)','丝(中)','丝(粗)','片(薄)','片(中)','片(厚)','丁(小)','丁(中)','丁(大)','块(小)','块(中)','块(大)','段','条','泥','末'];
|
||||
/* 刀工选项 */
|
||||
var VEG_TECH_OPTIONS=['','平刀','斜刀','直刀','花刀','其他'];
|
||||
function renderVegTypes(){
|
||||
var container = document.getElementById('vegTypeRows');
|
||||
if(!container) return;
|
||||
if(vegTypes.length===0) vegTypes.push({name:'',yieldRate:''});
|
||||
var html = '';
|
||||
vegTypes.forEach(function(item,i){
|
||||
var cs = item.cutSpec || {};
|
||||
html += '<div class="dynamic-row">';
|
||||
html += '<input class="form-input veg-name" placeholder="净菜名称" value="'+(item.name||'')+'">';
|
||||
html += '<input class="form-input veg-yield" type="number" min="0" max="100" placeholder="菜品出净率(0~100)" value="'+(item.yieldRate||'')+'">';
|
||||
html += '<input class="form-input veg-name" placeholder="净菜名称" value="'+(item.name||'')+'" style="flex:1.2">';
|
||||
html += '<input class="form-input veg-yield" type="number" min="0" max="100" placeholder="出净率(%)" value="'+(item.yieldRate||'')+'" style="flex:0.6">';
|
||||
html += '<select class="form-select veg-shape" style="flex:0.9">';
|
||||
VEG_SHAPE_OPTIONS.forEach(function(opt){
|
||||
var sel = (opt && cs.shape===opt) ? ' selected' : '';
|
||||
html += '<option value="'+opt+'"'+sel+'>'+(opt||'切配形状')+'</option>';
|
||||
});
|
||||
html += '</select>';
|
||||
html += '<input class="form-input veg-length" type="number" min="0" step="0.1" placeholder="长(mm)" value="'+(cs.length||'')+'" style="flex:0.5">';
|
||||
html += '<input class="form-input veg-width" type="number" min="0" step="0.1" placeholder="宽(mm)" value="'+(cs.width||'')+'" style="flex:0.5">';
|
||||
html += '<input class="form-input veg-height" type="number" min="0" step="0.1" placeholder="高(mm)" value="'+(cs.height||'')+'" style="flex:0.5">';
|
||||
html += '<select class="form-select veg-technique" style="flex:0.6">';
|
||||
VEG_TECH_OPTIONS.forEach(function(opt){
|
||||
var sel = (opt && cs.technique===opt) ? ' selected' : '';
|
||||
html += '<option value="'+opt+'"'+sel+'>'+(opt||'刀工')+'</option>';
|
||||
});
|
||||
html += '</select>';
|
||||
html += '<button type="button" class="dynamic-row__del" onclick="removeVegType('+i+')" title="删除">×</button>';
|
||||
html += '</div>';
|
||||
});
|
||||
@@ -543,7 +563,24 @@ function saveVegTypes(){
|
||||
rows.forEach(function(row){
|
||||
var name = row.querySelector('.veg-name');
|
||||
var yieldRate = row.querySelector('.veg-yield');
|
||||
if(name && yieldRate) vegTypes.push({name:name.value,yieldRate:yieldRate.value});
|
||||
if(!name || !yieldRate) return;
|
||||
var shape = row.querySelector('.veg-shape');
|
||||
var length = row.querySelector('.veg-length');
|
||||
var width = row.querySelector('.veg-width');
|
||||
var height = row.querySelector('.veg-height');
|
||||
var technique = row.querySelector('.veg-technique');
|
||||
var item = {name:name.value, yieldRate:yieldRate.value};
|
||||
/* 任一切配字段有值则组装 cutSpec 对象 */
|
||||
if((shape && shape.value) || (length && length.value) || (width && width.value) || (height && height.value) || (technique && technique.value)){
|
||||
item.cutSpec = {
|
||||
shape: shape ? shape.value : '',
|
||||
length: length ? length.value : '',
|
||||
width: width ? width.value : '',
|
||||
height: height ? height.value : '',
|
||||
technique: technique ? technique.value : ''
|
||||
};
|
||||
}
|
||||
vegTypes.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -701,7 +738,7 @@ var EDIT_FOODS = {
|
||||
color:{main:'乳白色',grade:'优',desc:'色泽洁白,半透明,颗粒饱满',cause:'碾磨去皮后淀粉体暴露'},
|
||||
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',vegTypes:[{name:'去筋膜',yieldRate:'92'},{name:'切丁',yieldRate:'88'}],
|
||||
'0301001':{name:'猪里脊(生)',code:'0301001',alias:'里脊肉',firstClass:'畜禽肉类',secondClass:'猪肉',edible:'100',waterRate:'73.3',dataSource:'0',vegTypes:[{name:'去筋膜',yieldRate:'92'},{name:'切丁',yieldRate:'88',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,technique:'直刀'}}],
|
||||
crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'高血脂人群',level:'适量'}],
|
||||
nutri:{calorie:143,protein:20.2,fat:7.1,carbohydrate:0.7,dietary_fiber:0,
|
||||
vitamin_a:6,vitamin_b1:0.54,vitamin_b2:0.1,vitamin_b6:0.27,vitamin_b12:0.6,vitamin_c:0,vitamin_d:0.2,vitamin_e:0.34,vitamin_k:0,niacin:5.3,folic_acid:5,biotin:0,pantothenic_acid:0.5,total_choline:0,
|
||||
@@ -723,7 +760,7 @@ var EDIT_FOODS = {
|
||||
color:{main:'金黄色',grade:'优',desc:'蛋壳浅褐,蛋黄金黄饱满',cause:'蛋黄富含叶黄素和类胡萝卜素'},
|
||||
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',vegTypes:[{name:'去根',yieldRate:'90'},{name:'切段',yieldRate:'88'}],
|
||||
'0201001':{name:'菠菜(生)',code:'0201001',alias:'波斯菜',firstClass:'蔬菜类',secondClass:'叶菜类',edible:'89',waterRate:'91.2',dataSource:'0',vegTypes:[{name:'去根',yieldRate:'90'},{name:'切段',yieldRate:'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,
|
||||
vitamin_a:487,vitamin_b1:0.04,vitamin_b2:0.11,vitamin_b6:0.1,vitamin_b12:0,vitamin_c:32,vitamin_d:0,vitamin_e:1.74,vitamin_k:0,niacin:0.6,folic_acid:110,biotin:0,pantothenic_acid:0.2,total_choline:0,
|
||||
@@ -734,7 +771,7 @@ var EDIT_FOODS = {
|
||||
color:{main:'深绿色',grade:'优',desc:'叶片翠绿有光泽,根部紫红',cause:'叶绿素含量高'},
|
||||
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',vegTypes:[{name:'切块',yieldRate:'96'},{name:'切丁',yieldRate:'94'}],
|
||||
'0601001':{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',edible:'100',waterRate:'82.8',dataSource:'1',vegTypes:[{name:'切块',yieldRate:'96',cutSpec:{shape:'块(中)',length:30,width:30,height:20,technique:'直刀'}},{name:'切丁',yieldRate:'94',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,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,
|
||||
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,
|
||||
|
||||
@@ -298,12 +298,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
<button class="btn btn-default" onclick="openCategoryDialog()">📂 食材分类管理</button>
|
||||
</div>
|
||||
<div class="food-list">
|
||||
<div class="food-item active" onclick="selectFood(0)">
|
||||
<div class="food-item" onclick="selectFood(0)">
|
||||
<div class="food-item__icon">🌾</div>
|
||||
<div class="food-item__info"><div class="food-item__name">粳米(生)</div><div class="food-item__meta"><span>0101001</span><span>谷薯类/稻米</span><span>346 kcal</span></div></div>
|
||||
<span class="food-item__source food-item__source--sys">系统内置</span>
|
||||
</div>
|
||||
<div class="food-item" onclick="selectFood(1)">
|
||||
<div class="food-item active" onclick="selectFood(1)">
|
||||
<div class="food-item__icon">🥩</div>
|
||||
<div class="food-item__info"><div class="food-item__name">猪里脊肉</div><div class="food-item__meta"><span>0201001</span><span>畜禽肉类/猪肉</span><span>155 kcal</span></div></div>
|
||||
<span class="food-item__source food-item__source--sys">系统内置</span>
|
||||
@@ -356,6 +356,21 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
</main>
|
||||
|
||||
<script>
|
||||
/* === 规格工具函数(从 doc/spec-utils-reference.md 内联,按需选取) === */
|
||||
/* 切配规格展示格式化:{length,width,height,shape,technique} → "20×1×1mm · 丝(细) · 平刀" */
|
||||
function formatCutSpec(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);
|
||||
});
|
||||
var dimStr = dims.length ? dims.join('×') + 'mm · ' : '';
|
||||
var shapeStr = cut.shape || '';
|
||||
var techStr = cut.technique ? ' · ' + cut.technique : '';
|
||||
var result = dimStr + shapeStr + techStr;
|
||||
return result || '—';
|
||||
}
|
||||
|
||||
// 食物数据(对齐 yx_mater_base_material 表结构)
|
||||
const FOODS = [
|
||||
{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',icon:'🌾',edible:'100',waterRate:'13.3',dataSource:0,vegTypes:[{name:'洗净米',yieldRate:'97'},{name:'免淘米',yieldRate:'95'}],
|
||||
@@ -368,7 +383,7 @@ const FOODS = [
|
||||
color:{main:'白色',desc:'乳白色,半透明,颗粒饱满',cause:'淀粉主体,含少量蛋白质',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'老年人',level:'推荐'},{name:'儿童',level:'推荐'}],note:'—'},
|
||||
taste:{main:'淡味',sub:'—',intensity:'弱',desc:'清淡,煮熟后微甜,无异味',crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],note:'糖尿病人群注意控制量'},
|
||||
texture:{main:'软糯',chew:'易',digest:'易',desc:'煮熟后软糯有嚼劲,易于消化',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'熬粥质地更软,适合老年人'}},
|
||||
{name:'猪里脊肉',code:'0201001',alias:'里脊',firstClass:'畜禽肉类',secondClass:'猪肉',icon:'🥩',edible:'100',waterRate:'71.4',dataSource:0,vegTypes:[{name:'肉片',yieldRate:'95'},{name:'肉丁',yieldRate:'92'},{name:'肉块',yieldRate:'97'}],
|
||||
{name:'猪里脊肉',code:'0201001',alias:'里脊',firstClass:'畜禽肉类',secondClass:'猪肉',icon:'🥩',edible:'100',waterRate:'71.4',dataSource:0,vegTypes:[{name:'肉片',yieldRate:'95',cutSpec:{shape:'片(中)',length:3,width:1,height:5,technique:'平刀'}},{name:'肉丁',yieldRate:'92',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,technique:'直刀'}},{name:'肉块',yieldRate:'97',cutSpec:{shape:'块(中)',length:30,width:30,height:30,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,
|
||||
ca:6,phosphorus:196,kalium:305,mg:25,na:43.2,fe:1.5,zn:2.3,se:11.97,cu:0.09,mn:0.01,iodine:0,
|
||||
@@ -398,7 +413,7 @@ const FOODS = [
|
||||
color:{main:'米黄色',desc:'蛋黄橙黄,蛋白透明,整体色泽亮',cause:'蛋黄富含类胡萝卜素',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'高血脂人群',level:'慎食'}],note:'高血脂人群每日不超过1个'},
|
||||
taste:{main:'鲜味',sub:'淡味',intensity:'中',desc:'蛋黄浓郁,蛋白清淡,整体鲜香',crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'}],note:'优质蛋白,味道适中'},
|
||||
texture:{main:'嫩滑',chew:'易',digest:'易',desc:'水煮蛋弹嫩,炒蛋软嫩,蒸蛋细腻',crowd:[{name:'全人群',level:'推荐'},{name:'婴幼儿',level:'推荐'},{name:'老年人',level:'推荐'}],cook:'蒸蛋口感最软,适合各年龄段'}},
|
||||
{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',icon:'🧈',edible:'100',waterRate:'82.8',dataSource:1,vegTypes:[{name:'豆腐块',yieldRate:'98'}],
|
||||
{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',icon:'🧈',edible:'100',waterRate:'82.8',dataSource:1,vegTypes:[{name:'豆腐块',yieldRate:'98',cutSpec:{shape:'块(中)',length:30,width:30,height:20,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,
|
||||
ca:138,phosphorus:97,kalium:106,mg:63,na:7.2,fe:2.5,zn:1.11,se:2.3,cu:0.27,mn:0.47,iodine:0,
|
||||
@@ -410,7 +425,7 @@ const FOODS = [
|
||||
texture:{main:'细腻',chew:'易',digest:'易',desc:'质地细腻滑嫩,入口即化,无渣感',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'细腻易消化,适合消化功能弱者'}}
|
||||
];
|
||||
|
||||
let currentFood = 0;
|
||||
let currentFood = 1;
|
||||
let currentTab = 0;
|
||||
|
||||
function selectFood(idx){
|
||||
@@ -460,8 +475,8 @@ function renderBasicInfo(f){
|
||||
html += `<div class="profile-field"><div class="profile-field__label">数据来源</div><div class="profile-field__value">${sourceLabel}</div></div>`;
|
||||
let vegTypesValue;
|
||||
if(f.vegTypes && f.vegTypes.length > 0){
|
||||
let rows = f.vegTypes.map(t=>`<tr><td>${t.name||'—'}</td><td>${t.yieldRate!==undefined&&t.yieldRate!==''?t.yieldRate+'%':'—'}</td></tr>`).join('');
|
||||
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜名称</th><th>菜品出净率</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
let rows = f.vegTypes.map(t=>`<tr><td>${t.name||'—'}</td><td>${t.yieldRate!==undefined&&t.yieldRate!==''?t.yieldRate+'%':'—'}</td><td>${formatCutSpec(t.cutSpec)}</td></tr>`).join('');
|
||||
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜名称</th><th>菜品出净率</th><th>切配规格</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
} else {
|
||||
vegTypesValue = '—';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,101 @@
|
||||
# 营养用餐管理端 CHANGELOG
|
||||
|
||||
## [2026-08-05] 规格体系统一化 · 阶段五辅助页改造(5.1~5.3 全部完成)
|
||||
|
||||
> 本次为 `规格体系统一化改造任务清单.md` 第五阶段辅助页 3 个任务(共 6 个文件)的统一改造。本阶段补齐了跨页共享的"写入端"、字典语义对齐和残余多形态字符串结构化,标志着规格体系统一化改造五个阶段全部完成。
|
||||
|
||||
### 营养质控(任务 5.1,3 个文件)
|
||||
|
||||
- **`prod-qc-nutrition.html`**:`confirmSync()` 从"toast 假落地"改为真同步落地。
|
||||
- **按 dish 聚合均值**:将所选检验记录按餐品聚合,分别计算 4 类指标(营养 / 味道 / 口感 / 色泽)的均值与等级。
|
||||
- **写入餐品总库**:`updateDishSpecNutri(dishName, avgData)` 将均值写入 `localStorage['dish_specs_v1']` 对应餐品的 `nutri` 字段,并记录 `nutriUpdatedAt` 时间戳。
|
||||
- **追加同步日志**:`appendSyncLogs(logs)` 将同步事件追加到 `localStorage['nutri_sync_log_v1']`,按时间倒序保留最多 100 条。
|
||||
- **用户反馈**:toast 提示 "已同步 N 条餐品营养数据至餐品总库,刷新餐品总库可见"。
|
||||
- **`prod-qc-nutrition-add.html`**:菜品下拉接入跨页共享。
|
||||
- 删除 `dishData` 硬编码(按 lid 分组的 9 行数组)。
|
||||
- 新增 `loadDishSpecs()` + `buildDishDataFromStore()`:从 `localStorage['dish_specs_v1']` 读取,按 `CANTEEN_LINE_MAP` 将餐品总库的 `category`(食堂名)映射到对应餐线 ID。
|
||||
- **兜底**:若 localStorage 为空或某食堂在总库中无餐品,回退到 `FALLBACK_DISH_DATA`,并在提示行展示 "⚠️ 餐品总库未初始化" 警告。
|
||||
- 三级联动保留(场所 → 餐线 → 日期 → 餐品)。
|
||||
- **`prod-qc-nutrition-sync.html`**:同步记录展示改为真日志驱动。
|
||||
- 优先从 `localStorage['nutri_sync_log_v1']` 读取,`mapLogsToSyncData` 将日志项映射为页面结构;无日志时回退到 `FALLBACK_SYNC_DATA` 内置示例。
|
||||
- `renderDialogAvg` 增加 `avgCache` 分支:当日志带有均值缓存时直接展示,不再依赖 RECORDS 现场计算。
|
||||
- `renderDialogLeft/Right` 与 `showDetail` 增加对历史日志(`recordIdxs` 含 -1)的防御处理:展示 "📝 该条记录为历史同步日志,详细单条数据未保留" 提示。
|
||||
- 顶部 "共 N 条记录" 与分页信息改为动态填充。
|
||||
|
||||
### 分析页字典语义修正(任务 5.2,2 个文件)
|
||||
|
||||
- **`sup-process-analysis.html`**:
|
||||
- **切配线筛选下拉**:"切丝 / 切片 / 切丁 / 切段 / 绞肉"(动作)→ "丝 / 片 / 丁 / 块 / 段 / 末 / 泥 / 条"(形状名词),对齐 `spec_cut_shape` 字典语义。
|
||||
- **表格行硬编码**:`土豆(切丝)` → `土豆(丝)`、`胡萝卜(切片)` → `胡萝卜(片)`、`猪肉(绞肉)` → `猪肉(末)`、`青椒(切块)` → `青椒(块)`。
|
||||
- **包装线筛选下拉**:新增独立字典 `pack_method`(6 项:真空 / 气调 / 普通 / 冷鲜 / 保鲜盒 / 散装托盘),原"净菜包 / 餐品净菜包"分类移到"包装对象"独立下拉。
|
||||
- **`sup-biz-analysis.html`**:
|
||||
- **生产分析表单位**:`投料重量(吨) / 产出重量(吨)` → `投料重量(kg) / 产出重量(kg)`,数值同步从"8.6"改为"8,600",与 `sup-process-analysis.html` 的 kg 口径对齐。
|
||||
- KPI 区"原料投入(吨) 48.6" 保留(大屏展示习惯用吨,且与 KPI 其他指标量级协调)。
|
||||
|
||||
### 净菜进柜(任务 5.3)
|
||||
|
||||
- **`prod-proc-cabinet.html`**:`CABINET_DATA` 的 `spec` 字段多形态字符串统一结构化。
|
||||
- `'5kg/袋'` → `{baseGram:5000, unit:'袋'}`
|
||||
- `'3kg/袋'` → `{baseGram:3000, unit:'袋'}`
|
||||
- `'500g/袋'` → `{baseGram:500, unit:'袋'}`
|
||||
- `'标准份(500g)'` → `{portion:'标准份', baseGram:500, unit:'份'}`
|
||||
- `'标准份(600g)'` → `{portion:'标准份', baseGram:600, unit:'份'}`
|
||||
- `'6份/箱'` → `{baseGram:6, unit:'箱', portion:'份'}`(决策点 #10:理解为 6 个份型单位装一箱)
|
||||
- 内联 `formatSpec` / `formatGram` 工具函数,Drawer 回显与 dishSelect 渲染统一调用 `formatSpec(spec)` 输出(如 `5kg/袋`、`500g/份`)。
|
||||
|
||||
### 文档同步
|
||||
|
||||
- **`doc/规格体系统一化改造任务清单.md`**:更新阶段五状态表,5.1/5.2/5.3 全部标记为 ✅ 已完成(日期 2026-08-05)。章节九"待业务方确认事项" 第 9 条(pack_method 字典处理)与第 10 条(`'6份/箱'` 语义)标记为已决策。
|
||||
|
||||
## [2026-08-05] 规格体系统一化 · 阶段四设备端改造(4.1~4.5 全部完成)
|
||||
|
||||
> 本次为 `规格体系统一化改造任务清单.md` 第四阶段设备端 9 个任务(共 14 个文件)的统一改造,参照 `spec-utils-reference.md` 权威源工具函数。所有 HTML 原型按墨刀兼容约束自包含,工具函数内联到每个页面。
|
||||
|
||||
### 设备端 · 毛菜验收秤(任务 4.1a / 4.1b)
|
||||
|
||||
- **`raw-veg-inspection-scale/01-verify-inspection.html`**
|
||||
- **4.1a verifyType 字段新增**:`FOOD_CATALOG` 20 项每项新增 `verifyType: 'weight'`(默认走重量验收);`SPICE_CATALOG` 8 项每项新增 `verifyType: 'count'`(点收验收)。采用二分法,操作员自行将体积转质量。
|
||||
- **4.1b UNIT_OPTIONS 分类**:按 verifyType 分流到 `UNIT_OPTIONS_WEIGHT`(斤/公斤/克/千克)或 `UNIT_OPTIONS_COUNT`(袋/瓶/箱/桶/罐/份/个/只/条/把)。
|
||||
|
||||
### 设备端 · 毛菜加工秤(任务 4.2a/b/c,4 个文件)
|
||||
|
||||
- **`raw-veg-processing-scale/01-washing-tasks.html`**、**`02-batch-weighing.html`**、**`03-batch-washing.html`**、**`04-history.html`**
|
||||
- **4.2a 存储层斤→克**:3 个任务单 8 个食材行 + 推送的 2 个新食材行改为 `data-weight-g` 克存储;`taskDataMap` 值改为克(如黄瓜 2000g、土豆 2500g);8 个 `record-card` 添加 `data-weight-g` 属性。
|
||||
- **4.2b 展示层兼容**:内联 `formatGram` 和 `formatGramForCanteen`,展示层自动还原"斤"显示(如 2000g → "4 斤")。修复 04-history 中 `formatGramForCanteen` 的语法错误(`g === null === ''` → `g === null || g === ''`)。
|
||||
- **4.2c yieldRate 字段**:03-batch-washing 的 `aiResults` 6 项 + `foodDB` 12 项新增 `yieldRate`(85~95),UI 列展示暂未添加(允许后续从食材档案带出)。
|
||||
|
||||
### 设备端 · 净菜加工秤(任务 4.3)
|
||||
|
||||
- **`clean-veg-ratio-scale/03-settings.html`**
|
||||
- 删除"重量单位" select(与 4.2 存储统一为克冲突),替换为说明 hint(`formatGramForCanteen` 自动还原"斤"显示)。
|
||||
- 滚刀块大小单位 cm → mm 统一:`小(2cm)/中(3cm)/大(5cm)` → `小(20mm)/中(30mm)/大(50mm)`。
|
||||
- 加工参数区添加 hint 说明引用食材档案 `vegTypes.cutSpec` 与切配类型字典 `spec_cut_shape`。
|
||||
|
||||
### 设备端 · 净菜包装贴标秤(任务 4.4,3 个文件 ⭐ 最复杂)
|
||||
|
||||
- **`clean-veg-packaging-labeler/01-orders.html`**、**`02-labeling.html`**、**`label-preview.html`**
|
||||
- **工具函数内联**:3 文件各自内联 5 个工具函数(`formatGram`/`formatSpec`/`formatCutSpec`/`parseLegacyCutSpec`/`parseLegacyPackSpec`),墨刀兼容无外部引用。
|
||||
- **mock 数据结构化**:3 套数据(订单列表 / 标签制作 / 标签预览)的 `cutSpec` 全部由字符串(如 `'20mm×1mm×1mm · 细丝 · 平刀'`)改为对象 `{length:20, width:1, height:1, shape:'细丝', technique:'平刀'}`;`packSpec` 由字符串(如 `'500g'`/`'大份800g'`)改为对象 `{baseGram:500, unit:'袋'}` 或 `{baseGram:800, unit:'份', portion:'大份'}`。
|
||||
- **字段重命名**:所有 `portions` → `packSize`(跨 6 个函数:`calcTotalWeight`/`renderOrderLabels`/`renderVegLabels`/`renderMealLabels`/`handlePrintCurrent`/`handlePrintAll`)。
|
||||
- **渲染逻辑**:直接展示的字符串改为 `formatSpec(specObj)` / `formatCutSpec(cutObj)`;为兼容多形态,加入 `typeof === 'object'` 判断并回退到 `parseLegacyPackSpec` / `parseLegacyCutSpec`。
|
||||
- **决策**:用户确认"真空包装"是包装方式(packMethod)而非包装规格(packSpec),不影响改造逻辑。
|
||||
|
||||
### 设备端 · 净菜组配秤(任务 4.5,3 个文件)
|
||||
|
||||
- **`ratio-scale/01-packing-tasks.html`**、**`02-weighing-packing.html`**、**`03-verify-history.html`**
|
||||
- **工具函数内联**:3 文件各自内联 5 个工具函数。
|
||||
- **localStorage 跨页共享**:新增 `DISH_SPECS_KEY = 'dish_specs_v1'` 与 `resolvePackSpec(pack)` 兜底函数——当页面 mock 数据缺 packSpec 时从跨页共享表读取(接入任务 3.0 机制)。
|
||||
- **RECORDS/taskData/mockData 全部结构化**:
|
||||
- `packSpec: '大份800g'` → `packSpec: {portion:'大份', baseGram:800, unit:'份'}`
|
||||
- `packSpec: '标准份500g'` → `packSpec: {portion:'标准份', baseGram:500, unit:'份'}`
|
||||
- `cutSpec: '20mm*1mm*1mm · 细丝 · 平刀'` → `cutSpec: {length:20, width:1, height:1, shape:'细丝', technique:'平刀'}`
|
||||
- **渲染层统一**:`pack.packSpec || '—'` 改为 `formatSpec(resolvePackSpec(pack))`;`ing.cutSpec || '—'` 改为 `formatCutSpec(parseLegacyCutSpec(ing.cutSpec))`;候选区 `cutSpecMap` 拼接改为 `formatCutSpec` 输出字符串。
|
||||
- **餐品名称展示**:02 页 `taskPackName` 文本中的 packSpec 改为 formatSpec 渲染(如"宫保鸡丁套餐 · 大份 800g / 份")。
|
||||
|
||||
### 文档同步
|
||||
|
||||
- **`doc/规格体系统一化改造任务清单.md`**:更新阶段四状态表,4.1a/4.1b/4.2a/4.2b/4.2c/4.3/4.4/4.5 全部标记为 ✅ 已完成(日期 2026-08-05),并补充每个任务的改造要点。
|
||||
|
||||
## [2026-08-06]
|
||||
|
||||
### 后台营养管理
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,207 @@
|
||||
# 统一规格工具函数参考(内联源)
|
||||
|
||||
> 撰写日期:2026-08-04 | 用途:规格体系统一化改造的工具函数"权威源"
|
||||
>
|
||||
> **重要说明**:本项目遵循墨刀导入兼容约束(CSS 和 JS 全部内联,禁止引用外部 JS 文件),因此本文件**仅作为代码参考与维护源**,实际使用时必须**内联**到每个需要的 HTML 页面的 `<script>` 块中。
|
||||
>
|
||||
> 改造流程:
|
||||
> 1. 本文件维护完整的工具函数源码(改一处)
|
||||
> 2. 在每个需要规格计算的页面改造时,从此处复制对应函数内联到页面的 `<script>` 块(按需选取,不必全量复制)
|
||||
> 3. 后续如需修改函数逻辑,修改本文件 + 同步修改所有引用该函数的页面
|
||||
|
||||
---
|
||||
|
||||
## 一、完整工具函数源码
|
||||
|
||||
```javascript
|
||||
/* ========================================
|
||||
* 规格体系统一化 - 工具函数集
|
||||
* 改造任务:任务 1.1 交付物
|
||||
* 设计依据:规格管理体系分析与设计方案 v3.1
|
||||
* ======================================== */
|
||||
|
||||
/* ---------- 1. 克重展示格式化 ---------- */
|
||||
/* 500 → "500g", 1500 → "1.5kg", 25000 → "25kg" */
|
||||
function formatGram(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g >= 1000 && g % 1000 === 0) return (g / 1000) + 'kg';
|
||||
if (g >= 1000) return (g / 1000).toFixed(1) + 'kg';
|
||||
return g + 'g';
|
||||
}
|
||||
|
||||
/* ---------- 2. 规格标签格式化 ---------- */
|
||||
/* {baseGram:500, unit:'袋'} → "500g/袋" */
|
||||
/* {baseGram:5000, unit:'散装'} → "散装/5kg" */
|
||||
function formatSpec(spec) {
|
||||
if (!spec || !spec.baseGram) return '—';
|
||||
var w = formatGram(spec.baseGram);
|
||||
if (spec.unit === '散装') return '散装/' + w;
|
||||
return w + '/' + (spec.unit || '');
|
||||
}
|
||||
|
||||
/* ---------- 3. 切配规格展示格式化 ---------- */
|
||||
/* {length:20, width:1, height:1, shape:'丝(细)', technique:'平刀'}
|
||||
→ "20×1×1mm · 丝(细) · 平刀" */
|
||||
function formatCutSpec(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);
|
||||
});
|
||||
var dimStr = dims.length ? dims.join('×') + 'mm · ' : '';
|
||||
var shapeStr = cut.shape || '';
|
||||
var techStr = cut.technique ? ' · ' + cut.technique : '';
|
||||
return dimStr + shapeStr + techStr;
|
||||
}
|
||||
|
||||
/* ---------- 4. 历史切配规格字符串解析(迁移用) ---------- */
|
||||
/* 兼容 3 种历史格式:
|
||||
* "3.5mm*3.5mm*50mm·细丝·平刀" (无空格,* 分隔)
|
||||
* "20mm*1mm*1mm · 细丝 · 平刀" (带空格,* 分隔)
|
||||
* "20mm×1mm×1mm · 细丝 · 平刀" (带空格,× 分隔)
|
||||
* 返回:{length, width, height, shape, technique}
|
||||
*/
|
||||
function parseLegacyCutSpec(s) {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'object') return s; // 已是对象,直接返回
|
||||
// 归一化:× → *,去掉所有空格
|
||||
var normalized = String(s).replace(/×/g, '*').replace(/\s/g, '');
|
||||
// 正则:可选尺寸(数字mm*数字mm*数字mm) · 形状(可选 · 刀工)
|
||||
var m = normalized.match(/^(?:(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm\*(\d+(?:\.\d+)?)mm)?·?([^·]+)·?([^·]+)?$/);
|
||||
if (!m) return { shape: s }; // 无法解析则整体作为 shape
|
||||
return {
|
||||
length: m[1] ? parseFloat(m[1]) : undefined,
|
||||
width: m[2] ? parseFloat(m[2]) : undefined,
|
||||
height: m[3] ? parseFloat(m[3]) : undefined,
|
||||
shape: m[4] || '',
|
||||
technique: m[5] || ''
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- 5. 向上取整除法(决策点 #5:统一向上取整 ⌈⌉) ---------- */
|
||||
/* ceilDiv(23000, 500) = 46 */
|
||||
/* ceilDiv(192, 100) = 2 (1.92 向上取整为 2) */
|
||||
function ceilDiv(numerator, denominator) {
|
||||
if (!denominator) return 0;
|
||||
return Math.ceil(numerator / denominator);
|
||||
}
|
||||
|
||||
/* ---------- 6. 计算规格总克重 ---------- */
|
||||
/* calcTotalGram({baseGram:500, unitCapacity:1}, qty=10) = 5000
|
||||
* calcTotalGram({baseGram:25000, unitCapacity:1}, qty=2) = 50000 (2件,每件25kg) */
|
||||
function calcTotalGram(spec, qty) {
|
||||
if (!spec || !spec.baseGram) return 0;
|
||||
var cap = spec.unitCapacity || 1;
|
||||
var q = qty || 1;
|
||||
return spec.baseGram * cap * q;
|
||||
}
|
||||
|
||||
/* ---------- 7. 跨单位换算(向上取整) ---------- */
|
||||
/* 把源规格的总量,按目标单位的基准克重换算为目标单位数量
|
||||
* convertSpec({baseGram:5000, unit:'箱'}, '袋', 500) = 10 (袋)
|
||||
* 即:5kg/箱 ÷ 500g/袋 = 10 袋/箱 */
|
||||
function convertSpec(srcSpec, targetUnit, targetBaseGram) {
|
||||
if (!srcSpec || !srcSpec.baseGram || !targetBaseGram) return 0;
|
||||
var srcTotal = calcTotalGram(srcSpec, 1);
|
||||
return ceilDiv(srcTotal, targetBaseGram);
|
||||
}
|
||||
|
||||
/* ---------- 8. 验收秤场景兼容"斤"显示 ---------- */
|
||||
/* 500 → "1斤", 25000 → "50斤", 800 → "1.6斤"(非整斤回退到克) */
|
||||
function formatGramForCanteen(g) {
|
||||
if (g === undefined || g === null || g === '' || isNaN(g)) return '—';
|
||||
g = Number(g);
|
||||
if (g % 500 === 0) return (g / 500) + '斤';
|
||||
return formatGram(g); // 非整斤回退到克/kg
|
||||
}
|
||||
|
||||
/* ---------- 9. 采购规格 → 净料克重(扣出净率,向上取整) ---------- */
|
||||
/* L1 → L2 换算:采购总克重 × 出净率 = 净料总克重
|
||||
* calcNetWeight(25000, 92) = ⌈25000 × 0.92⌉ = 23000 */
|
||||
function calcNetWeight(srcTotalGram, yieldRate) {
|
||||
if (!srcTotalGram || !yieldRate) return 0;
|
||||
return Math.ceil(srcTotalGram * (yieldRate / 100));
|
||||
}
|
||||
|
||||
/* ---------- 10. 净料 → 反推采购需求(向上取整) ---------- */
|
||||
/* 已知净料需求,反推采购需求 = ⌈净料需求 / 出净率⌉
|
||||
* calcPurchaseDemand(23000, 92) = ⌈23000 / 0.92⌉ = 25000 */
|
||||
function calcPurchaseDemand(netDemandGram, yieldRate) {
|
||||
if (!netDemandGram || !yieldRate) return 0;
|
||||
return Math.ceil(netDemandGram / (yieldRate / 100));
|
||||
}
|
||||
|
||||
/* ---------- 11. 净料 → 可包装份数(向上取整) ---------- */
|
||||
/* L2 → L3 换算:净料总克重 ÷ 包装基准克重 = 可包装数
|
||||
* calcPackableCount(23000, 500) = 46 (袋) */
|
||||
function calcPackableCount(netWeightGram, packBaseGram) {
|
||||
return ceilDiv(netWeightGram, packBaseGram);
|
||||
}
|
||||
|
||||
/* ---------- 12. 包装 → 餐品份数(向上取整) ---------- */
|
||||
/* L3 → L4 换算:(袋数 × 包装克重) ÷ 单份配方用量 = 可产出份数
|
||||
* calcServableCount(46, 500, 120) = ⌈46×500 ÷ 120⌉ = ⌈191.67⌉ = 192 */
|
||||
function calcServableCount(packCount, packBaseGram, recipeGram) {
|
||||
if (!packBaseGram || !recipeGram) return 0;
|
||||
return ceilDiv(packCount * packBaseGram, recipeGram);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、函数用途与使用位置索引
|
||||
|
||||
| 函数名 | 用途 | 使用位置(任务编号) |
|
||||
|--------|------|---------------------|
|
||||
| `formatGram` | 克重展示格式化 | 几乎所有页面 |
|
||||
| `formatSpec` | 规格标签格式化 | 采购/入库/订单/包装等页面 |
|
||||
| `formatCutSpec` | 切配规格展示 | sup-in-clean / clean-veg-packaging-labeler / ratio-scale 等 |
|
||||
| `parseLegacyCutSpec` | 历史字符串解析(迁移用) | clean-veg-packaging-labeler(01-orders / 02-labeling / label-preview) |
|
||||
| `ceilDiv` | 向上取整除法 | 所有规格换算场景 |
|
||||
| `calcTotalGram` | 计算规格总克重 | 采购/订单/包装等 |
|
||||
| `convertSpec` | 跨单位换算 | 入库/订单/组配 |
|
||||
| `formatGramForCanteen` | 验收秤"斤"显示兼容 | raw-veg-inspection-scale / raw-veg-processing-scale |
|
||||
| `calcNetWeight` | L1→L2 扣出净率 | sup-in-clean / prod-proc-cook |
|
||||
| `calcPurchaseDemand` | L2→L1 反推采购需求 | sup-in-clean(反向推导) |
|
||||
| `calcPackableCount` | L2→L3 包装分装 | sup-in-cleanin / prod-clean-order |
|
||||
| `calcServableCount` | L3→L4 餐品产出 | prod-proc-combo / prod-proc-cook |
|
||||
|
||||
---
|
||||
|
||||
## 三、内联使用示例
|
||||
|
||||
在每个需要工具函数的 HTML 页面,按需复制对应函数到 `<script>` 块开头:
|
||||
|
||||
```html
|
||||
<script>
|
||||
/* === 规格工具函数(从 doc/spec-utils-reference.md 内联,按需选取) === */
|
||||
function formatGram(g) { /* ... 复制本文件第一节对应函数 ... */ }
|
||||
function formatSpec(spec) { /* ... */ }
|
||||
function formatCutSpec(cut) { /* ... */ }
|
||||
|
||||
/* === 页面业务逻辑 === */
|
||||
// ... 原有页面代码 ...
|
||||
</script>
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 按需选取,不必全量复制(例如只用 `formatCutSpec` 的页面,只内联这一个函数)
|
||||
- 复制后不得修改函数实现(确保所有页面一致)
|
||||
- 如需新函数或修改现有函数,**先在本文件修改**,再同步复制到所有页面
|
||||
|
||||
---
|
||||
|
||||
## 四、维护说明
|
||||
|
||||
1. 本文件是**唯一权威源**,任何函数修改都先在这里进行
|
||||
2. 修改后,**同步更新所有引用该函数的页面**(可通过 grep 搜索函数名定位)
|
||||
3. 建议在每次改造任务完成后,运行一次全文检索,确认所有页面的函数实现一致
|
||||
|
||||
---
|
||||
|
||||
## 五、版本记录
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| v1.0 | 2026-08-04 | 初版,12 个工具函数(任务 1.1 交付) |
|
||||
@@ -1,150 +0,0 @@
|
||||
# 智能部下周工作计划(2026年8月3日 — 8月7日)最终版
|
||||
|
||||
> **依据**:(1)7月28日开业筹备第一次碰头会会议报告 + 老徐手记;(2)《智养餐饮平台系统反馈报告》156条业务反馈;(3)8月1日营养管理专项问题梳理;(4)8月2日开业碰头会第二次会议调整意见
|
||||
> **用途**:8月3日(周一)晨会任务下发,每项预留负责人与完成时间,晨会时补充填写
|
||||
|
||||
---
|
||||
|
||||
## 一、P0:最高优先级(阻塞试营业,下周必须攻克)
|
||||
|
||||
> 以下任务为试营业的硬阻塞项,压倒一切,必须在本周内完成或取得决定性进展。
|
||||
|
||||
| 编号 | 任务 | 负责人 | 完成时间 | 说明 | 备注 |
|
||||
|------|------|--------|----------|------|----------|
|
||||
| **P0-1** | **全链路定价体系建设** | | | 🆕 **今日会议最高优先级新增,本周必须攻克**。打通从采购到售卖的全链条价格体系,分六个环节逐级传导——**①毛菜采购价**:实付金额÷验收重量=核算单价(自动计算,不可手动改),同时保留供应商报价单价字段供对比核查;**②毛菜入库价**:核算单价随毛菜入库延续,入库即带价;**③净菜价**:按净菜率从毛菜入库价折算净菜成本单价;**④餐品净菜包成本价**:根据餐品构成中各净菜用量与净菜单价汇总计算;**⑤餐品制作实际成本价**:对接配比秤实际投料数据(主辅料+调料用量+熟重),对比理论成本与实际成本偏差,以实际投料为准更新成本;**⑥餐品最终定价**:经营人员逐菜品设定毛利率→系统自动生成售卖价。先做应急版(跳过净菜中间加价环节,直接毛菜采购价→餐品成本→毛利率→售价,确保能卖饭),后续迭代完善全链条 | 产品设计 |
|
||||
| **P0-2** | **餐品库体系改造** | | 8.3 | 🆕 **整合原P0-2/P1-16/P2-1三条任务,提至最高优先级,周一必须完成**。改造现有单一餐品库为两级架构——**①系统级餐品总库**:包含两类餐品,须在设计上严格区分、不可混用:(a)正规流程餐品——通过系统标准流程维护,具备完整食材构成与配比重量数据,用于餐厅经营、净菜组配、成本核算等全部业务环节;(b)第三方餐品库——来自个人端小程序"自记餐"功能导入的外部餐品数据,缺失餐品构成信息,餐品本身直接设定营养数据(热量/蛋白质/脂肪等),仅供自记餐场景使用,不得混入经营流程。**②餐厅/食堂餐品库**:从系统级总库中选取本餐厅使用的餐品形成子集。餐厅提交餐品净菜包订单时,必须将餐品构成信息(用料清单+配比重量)附加到订单上,净菜加工场组配时按构成信息进行组配。同步完善:分类管理增删改查、餐品名称同组织唯一性校验、搜索食材功能修复。**周一必须完成现有200+菜品录入**(含食材构成+配比重量),食材库缺失的原材料可自己加(营养数据可缺失但重量价格必须有)。徐总带2-3个示范后交王喜团队批量完成 | 产品已补全设计 |
|
||||
| **P0-3** | **支付功能修复与完善** | | | ⬆ **今日会议提级**。两项——(1)联合支付已修复但APP未更新,尽快更新上线;(2)余额支付功能下周内完成开发。先把联合支付修复上线,内部测试版随时升级 | 产品设计/已完成 |
|
||||
| **P0-4** | **结算台问题专项修复** | | | 三项——(1)吐盘机人脸识别成功但餐线不显示打餐数据,导致无法结账;(2)付款后界面仍停留在未支付状态;(3)实际取餐信息与结算台显示不对应 | |
|
||||
| **P0-5** | **营养秤重量失真修复** | | | 打餐重量严重失真(实际100g→系统显示620g),疑为未识别人脸时压秤后补识别导致。需根因分析+修复 | |
|
||||
| **P0-6** | **营养秤人脸识别不稳定修复** | | | 3号营养秤人员识别不上;不同秤之间人脸数据未同步。需逐台验证+修复同步机制 | |
|
||||
| **P0-7** | **试营业模拟问题即刻修复** | | | 员工参与试营业模拟过程中发现的系统问题第一时间修复:小问题当日修复,大问题提至最高优先级处理。小问题不过夜 | |
|
||||
| **P0-8** | **配比秤餐品制作数据全量存储** | | | 配比秤接入后,餐品制作全过程关键数据(主辅料清单及用量、调料清单及用量、餐品熟重)后台必须完整存储。当前菜品构成数据缺失严重,此项是后续更新菜品构成的唯一可靠数据来源,同时为P0-1第⑤环节提供实际成本数据 | 验证 |
|
||||
| **P0-9** | **部署独立开发与测试环境** | | | 现有智养餐饮平台环境作为试营业生产环境,需尽快部署独立的开发、测试环境,与生产环境隔离 | |
|
||||
| **P0-10** | **清理现有环境测试数据** | | | 将当前环境的旧测试数据(菜品定价、用户记录、交易流水、"内部供应商"等)彻底清理,确保试营业数据真实准确 | |
|
||||
| **P0-11** | **组配秤任务逻辑修复** | | | 三项——(1)实际配比4道菜系统显示7道;(2)实际完成4道但显示已完成3道,计数不同步;(3)称重接近目标时未去皮导致实际重量偏小 | |
|
||||
| **P0-12** | **取餐"价格=重量"BUG修复** | | | 🆕 营养管理专项P0。取餐记录中价格等于重量、热量等于重量,缺少乘以单价逻辑,直接影响收费 | |
|
||||
| **P0-13** | **食谱规划/管理菜单位置调整** | | | 🆕 营养管理专项P0。当前菜单顺序反了:应是"营养用餐(定食堂+人数+食谱)→下达生产部门→健康生产(按食谱+人数计算食材用量)",不是现在的"健康生产→食谱规划→营养用餐" | 优先级放后 |
|
||||
| **P0-14** | **53条手动食物数据逐条核实** | | | 🆕 营养管理专项P0。可食部为空、颜色千篇一律、味道/口感写死等数据质量问题,必须逐条核实补全:可食部+营养成分+颜色+味道 | |
|
||||
| **P0-15** | **3429条内置食物数据来源核查启动** | | | 🆕 营养管理专项P0。声称"中国营养学会"但混入肯德基、麦当劳、九珍果汁等商业产品数据,需核查并去除。坚果等基本品类缺失需补全。本周启动 | |
|
||||
| **P0-16** | **毛菜存储增加取用变化记录** | | | 🆕 营养管理专项P0。当前溯源只显示概要("入库3.4公斤,余量2.1公斤,已领用0——少的1.4公斤去哪了?"),需记录完整变化过程:时间+取走多少+取走人 | 优先级放后 |
|
||||
| **P0-17** | **规格/单位字段拆分** | | | 🆕 营养管理专项P0。当前规格与单位混淆("规格都是斤,数量是1"),需拆分为三个字段:规格(克重)+单位(袋/瓶/筐/批)+数量 | 产品设计 |
|
||||
| **P0-18** | **每日经营统计功能开发** | | | 至少实现通过数据库查询每日导出当日经营统计结果(就餐人数、收入、菜品销量等核心指标) | 产品设计 |
|
||||
| | 小程序查看餐线订单 | | | 支持查看餐线订单 | 产品设计 /已完成 |
|
||||
| | 小程序支持人脸录入、授权手机号 | | | 对比人脸信息,关联临时用户 | 产品设计 /已完成 |
|
||||
| | 净菜厨房柜 | | | 厨房净菜柜对接、确认 | |
|
||||
| | 净菜柜、毛菜柜 | | | 确认多台的影响范围 | |
|
||||
| | 小程序屏蔽功能 | | | 订餐、点餐、净菜包购买 | |
|
||||
| | | | | | |
|
||||
| | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 二、P1:高优先级(影响核心业务流程,本周全力推进)
|
||||
|
||||
| 编号 | 任务 | 负责人 | 完成时间 | 说明 | 备注 |
|
||||
|------|------|--------|----------|------|------|
|
||||
| **P1-1** | **后厨补餐屏接入试营业系统** | | | 补餐屏(小米电视)已放置后厨,需完成程序接入并确定安装位置(建议挂厨师附近墙上) | 1 |
|
||||
| **P1-2** | **食谱→净菜→毛菜采购量自动生成** | | | 🆕 王喜团队反馈三大核心需求之一。下食谱后系统自动生成净菜需求量,净菜量自动反算毛菜采购量。已有钱博士提供的算法,王喜先手算验证几次再开发 | 优先级放后 |
|
||||
| **P1-3** | **净菜数据维护配合** | | | 🆕 放开权限给王喜团队维护净菜名称。每个毛菜下挂多种净菜(如青椒→青椒丝/段/块),只维护实际用到的几十到一百种。徐总带2-3个示范后交团队 | 1 |
|
||||
| **P1-4** | **取餐屏APP改造(支持视频播放)** | | | 🆕 黄总团队制作菜品AI生成视频,需改造APP待机画面支持播放视频(从后台下载),支持操作人员随时更换。黄总每天发视频素材,这边对接上线 | 产品设计/已完成 |
|
||||
| **P1-5** | **算力与存储采购推进** | | | 🆕 会上被点名批评"不要再拖了"。大半年前提出按10倍储备采购,至今未执行。让小江尽快确认需求,确认后立即采购。存储和算力价格在上涨 | |
|
||||
| **P1-6** | **结算后营养小票打印功能** | | | 第一阶段:终端侧按既定模板打印结算信息与营养信息 | 产品设计 |
|
||||
| **P1-7** | **大厅排行榜APP接入试营业系统** | | | 排行榜展示APP接入试营业系统环境,显示实时排行数据,带一屏改版数字调整 | 产品设计/已完成 |
|
||||
| **P1-8** | **餐线上方带鱼屏改版** | | | 需求及界面效果已完成,显示内容调整为"菜品价格+热量信息",因屏幕清晰度一般不做复杂展示 | 产品设计/已完成 |
|
||||
| **P1-9** | **毛菜验收秤校验修复** | | | 四项——(1)未全部验收即可提交,缺少必验校验;(2)缺少重量校验导致0重量也可验收;(3)多次拍照建模触发闪退;(4)错误识别菜品重量覆盖其他菜品 | |
|
||||
| **P1-10** | **净菜加工秤数据修复** | | | 两项——(1)毛菜足量但净菜加工后显示"还需0.0千克";(2)清除库存不足任务后仍出现库存不足,后台数据未正常同步 | |
|
||||
| **P1-11** | **毛菜加工时间记录修正** | | | 三项——(1)开始时间为统一提交时间而非首次放置时间;(2)同种蔬菜重复出现两次记录(应合并相同食材);(3)加工秤应只读取主辅材数据 | |
|
||||
| **P1-12** | **取餐管理功能修复** | | | 三项——(1)数据分页选择100行实际只展示30行;(2)用户取餐但未得分;(3)无法通过用户名查询取餐记录 | |
|
||||
| **P1-13** | **后台数据展示修复** | | | 四项——(1)净菜订单数据看板"已完成"恒为0;(2)毛菜入库后后台显示未入库(含存放区域缺失);(3)入库时间显示为非最终入柜时间;(4)毛菜入库排序逻辑正序改倒序 | |
|
||||
| **P1-14** | **环境更新与回退机制建立** | | | 对试营业生产环境的更新操作须慎重,每次更新前做好备份,确保出现问题可回退至上一版本。本周完成方案并持续执行 | |
|
||||
| **P1-15** | **食物成分数据规范** | | | 四项——(1)宏量营养素改为必填项;(2)食材名称增加唯一性校验;(3)净材率去掉最大值校验(干木耳泡发场景); | |
|
||||
| **P1-16** | **各节点补齐关键信息字段** | | | 🆕 营养管理专项P1。每个业务节点统一补齐:时间+操作人+设备编号+重量变化+单价。补齐缺失字段:采购人、入库人/验收人、审核人。本周设计、下周开发 | |
|
||||
| **P1-17** | **食材统一加"国家营养库食材编码"字段** | | | 🆕 营养管理专项P1。为后续数据溯源和权威性验证打基础 | |
|
||||
| **P1-18** | **补全基本品类食材数据** | | | 🆕 营养管理专项P1。坚果等基本品类在3429条内置数据中缺失,需补全 | |
|
||||
| **P1-19** | **库存负值保护** | | | 🆕 营养管理专项P1。取格子时小于等于0一律等于0,程序加保护逻辑 | |
|
||||
| **P1-20** | **页面返回逻辑修复** | | | 🆕 营养管理专项P1。"明明在A页面,返回后跳到B",全局排查修复 | |
|
||||
| **P1-21** | **菜品数据梳理核对** | | | 现有菜品数据(构成、营养信息、定价)多为随手录入,需逐一核对核实,配合菜品成本核算工作。完成首轮核对 | |
|
||||
| **P1-22** | **小程序发布准备** | | | (1)确定小程序名称和图标设计;(2)梳理腾讯小程序上架审核所需资质(如ICP经营许可证等),列出缺失项并启动办理流程。等资质到位后发布正式版 | |
|
||||
|
||||
---
|
||||
|
||||
## 三、P2:中等优先级(提升易用性与规范化)
|
||||
|
||||
| 编号 | 任务 | 负责人 | 完成时间 | 说明 |
|
||||
|------|------|--------|----------|------|
|
||||
| **P2-1** | **采购单来源与规格修正** | | | 采购单来源仅支持手动/内供/外售(去掉食谱搭配来源),规格改为下拉菜单 |
|
||||
| **P2-2** | **净菜加工字段清理** | | | 删除无实际功能的质检结果字段;开始/结束时间清晰命名 |
|
||||
| **P2-3** | **65寸广告机使用方式交接** | | | 将65寸广告机(柱子广告机)的日常操作方式(开关机、内容更新、轮播设置等)交接给餐厅工作人员 |
|
||||
| **P2-4** | **界面优化(序号/折叠/去重)** | | | 🆕 营养管理专项P3提至P2。所有列表统一加序号、左侧菜单支持折叠、入库信息去重显示、"食材溯源"等冗余标题去除 |
|
||||
| **P2-5** | **UI数据字段修正** | | | 🆕 营养管理专项。年龄自动计算(输入身份证号/出生日期→自动算年龄)、毛菜类型修正("面片"标为毛菜需修正)、养殖/种植分类修正("辣椒"标为"养殖"需改为"种植") |
|
||||
| **P2-6** | **视频/图片字段预留** | | | 🆕 营养管理专项。各业务节点预留视频/图片字段 |
|
||||
| **P2-7** | **AI辅助食材用量计算验证** | | | 🆕 输入"食谱+人数"让AI算食材用量,验证可行性 |
|
||||
|
||||
---
|
||||
|
||||
## 四、持续配合事项
|
||||
|
||||
| 编号 | 任务 | 负责人 | 完成时间 | 说明 |
|
||||
|------|------|--------|----------|------|
|
||||
| **C-1** | **配合菜品成本核算** | | | 配合王喜、吕学锋、唐修哲完成200个菜品的成本核算与试营业定价,从后台捋价格,提供系统侧数据支持 |
|
||||
| **C-2** | **每日参加开业筹备例会** | | | 暂定每天10:30召开,汇报技术侧进展与阻塞问题 |
|
||||
| **C-3** | **业务反馈问题持续跟踪** | | | 每日跟进业务反馈清单中"待处理"问题,更新解决状态;当天新反馈的BUG当日响应 |
|
||||
| **C-4** | **餐盘柜接入试营业系统** | | | 优先级较低,其他任务完成后推进 |
|
||||
|
||||
---
|
||||
|
||||
## 五、优先级汇总
|
||||
|
||||
| 优先级 | 任务数 | 任务编号 |
|
||||
|--------|--------|----------|
|
||||
| **P0(最高)** | 18 | P0-1~P0-18 |
|
||||
| **P1(高)** | 22 | P1-1~P1-22 |
|
||||
| **P2(中)** | 7 | P2-1~P2-7 |
|
||||
| **持续配合** | 4 | C-1~C-4 |
|
||||
| **合计** | **51** | — |
|
||||
|
||||
> ⚠️ 本周任务总数51项,其中P0级18项为试营业硬阻塞项,必须全力保障、逐项销号。
|
||||
> **两大头号工程**:P0-1(全链路定价)合并了原五条价格任务,覆盖毛菜采购价→入库价→净菜价→净菜包成本→实际制作成本→最终定价;P0-2(餐品库改造)合并了原三条餐品库任务,建立"系统级总库(正规流程+自记餐第三方)→餐厅餐品库(订单附加构成信息)"两级架构。
|
||||
|
||||
---
|
||||
|
||||
## 六、今日(8月2日)待办
|
||||
|
||||
| # | 事项 | 要求 |
|
||||
|---|------|------|
|
||||
| 1 | **问题全量汇集** | 把昨天会上的问题+今天会上提的所有问题,全部重新汇集整理成清单,今晚发到群里 |
|
||||
| 2 | **大分工+小分工** | 大分工=各模块负责人(抓落实),小分工=具体操作执行人。今天下午叫上杨幂、张传志等相关人员一起梳理 |
|
||||
| 3 | **核心问题重点落实** | 定价问题和餐品库改造是头号紧急事项,必须下周攻克 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 附:业务反馈问题待处理跟进清单(本周重点销项)
|
||||
|
||||
| 序号 | 模块 | 核心问题 | 状态 | 对应任务 |
|
||||
|------|------|----------|------|----------|
|
||||
| 1 | 结算台 | 吐盘机人脸识别成功但餐线不显示打餐数据 | 待审核 | P0-4 |
|
||||
| 2 | 结算台 | 付款后界面仍停留在未支付状态 | 待审核 | P0-4 |
|
||||
| 3 | 结算台 | 实际取餐信息与结算台显示不对应 | 待审核 | P0-4 |
|
||||
| 4 | 营养秤 | 打餐重量失真(100g→620g) | 待审核 | P0-5 |
|
||||
| 5 | 营养秤 | 人员打餐信息与实际操作人员不符 | 待处理 | P0-6 |
|
||||
| 6 | 营养秤 | 3号秤人员识别不上 | 待处理 | P0-6 |
|
||||
| 7 | 毛菜/净菜货柜 | 放置时间应取最近一次数据 | 待审核 | P1-13 |
|
||||
| 8 | 组配秤 | 去皮缺失/任务计数错误(3条) | 待处理 | P0-11 |
|
||||
| 9 | 毛菜验收秤 | 未验收可提交/0重量验收/拍照闪退/重量覆盖 | 待处理 | P1-9 |
|
||||
| 10 | 净菜加工秤 | 显示错误/库存数据异常 | 待处理 | P1-10 |
|
||||
| 11 | 取餐管理 | 分页bug/未得分/查询失效 | 待处理 | P1-12 |
|
||||
| 12 | 后台-健康生产 | 净菜订单看板统计失效 | 待处理 | P1-13 |
|
||||
| 13 | 营养管理-全链路定价 | 采购单价=手动输入不可靠;入库后无价格;缺少净菜成本/制作成本/自动定价 | 待处理 | **P0-1** |
|
||||
| 14 | 营养管理-餐品库 | 单一餐品库无层级区分;缺少自记餐第三方餐品隔离;订单未附加构成信息 | 待处理 | **P0-2** |
|
||||
| 15 | 营养管理-取餐 | "价格=重量""热量=重量"数据BUG | 待处理 | P0-12 |
|
||||
| 16 | 营养管理-食谱 | 食谱规划→健康生产菜单顺序反了 | 待处理 | P0-13 |
|
||||
| 17 | 营养管理-食物库 | 53条手动数据可食部/颜色/味道随意填写 | 待处理 | P0-14 |
|
||||
| 18 | 营养管理-食物库 | 3429条内置数据混入肯德基/麦当劳等商业数据 | 待处理 | P0-15 |
|
||||
| 19 | 营养管理-溯源 | 毛菜存储缺少取用变化记录(入库3.4→余2.1→领用0,少1.4去向不明) | 待处理 | P0-16 |
|
||||
|
||||
---
|
||||
|
||||
*整理时间:2026年8月2日*
|
||||
*下发时间:2026年8月3日(周一)晨会*
|
||||
@@ -0,0 +1,257 @@
|
||||
# 规格体系统一化改造 · 研发同步说明
|
||||
|
||||
> 撰写日期:2026-08-05 | 适用范围:营养管理模块(管理端 + 食堂设备端) | 目的:产品→研发的需求同步
|
||||
|
||||
---
|
||||
|
||||
## 一、改造背景(一句话说明)
|
||||
|
||||
原系统中"规格"信息分散在 30+ 个页面,存在 **3 套互不一致的写死规格字典**、**"斤/克"单位不统一**、**份型/切配参数与餐品脱钩**、**包装规格无法跨页传递** 等问题。本次改造将所有规格信息统一为同一套口径,并打通"餐品总库 → 下游所有页面"的规格数据流。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体改动概览
|
||||
|
||||
| 端 | 改动菜单数 | 涉及核心场景 |
|
||||
|----|-----------|-------------|
|
||||
| 管理端(营养管理) | 19 个菜单 | 食材档案、餐品档案、字典、订单、加工、包装、质控、分析 |
|
||||
| 设备端(食堂设备) | 7 个设备模块(共 14 个页面) | 验收秤、毛菜加工秤、净菜加工秤、包装贴标秤、组配秤 |
|
||||
|
||||
---
|
||||
|
||||
## 三、管理端改动清单(营养管理)
|
||||
|
||||
### 1. 食材档案
|
||||
|
||||
#### 1.1 食材档案 - 新增/编辑
|
||||
- 净菜子项录入表由原来的 2 列(净菜名称、出净率)扩展为 7 列,新增"切配形状、长/宽/高(毫米)、刀工"5 个录入项
|
||||
- 切配形状与刀工以下拉框形式选择,选项来源于字典管理页的"切配类型字典"
|
||||
- 录入时会同时保存每条净菜子项的完整切配参数(形状、尺寸、刀工)
|
||||
|
||||
#### 1.2 食材档案 - 详情/列表
|
||||
- 详情面板的净菜子项展示新增"切配规格"列,以可读文本统一展示(例:`15×15×15mm · 丁 · 直刀`)
|
||||
- 默认展开"猪里脊"示例,便于业务方理解切配规格的展示效果
|
||||
|
||||
---
|
||||
|
||||
### 2. 餐品档案
|
||||
|
||||
#### 2.1 餐品总库
|
||||
- 每个餐品的"份型规格"统一为"克重 + 包装单位 + 份型名"三要素(例:`200g / 标准份`)
|
||||
- 份型下拉的选项来源改为字典管理页的"份型字典",不再使用页内写死数据
|
||||
- **新增"营养规格"信息位**(承接营养质控的回写)
|
||||
- **新增跨页共享能力**:页面打开时,会将本页所有餐品的规格信息持久化到浏览器本地,作为下游所有页面(订单/加工/包装/质控等)的统一数据源
|
||||
|
||||
#### 2.2 餐品总库 - 新增/编辑
|
||||
- 份型录入行改为"左克重输入框 + 右份型下拉框"连体组件
|
||||
- 展示格式统一为 `克重g/份型`
|
||||
|
||||
#### 2.3 我的餐品
|
||||
- 同 2.2,份型录入与展示统一为连体组件格式
|
||||
|
||||
---
|
||||
|
||||
### 3. 字典管理
|
||||
|
||||
#### 3.1 字典管理
|
||||
- 新增"规格"Tab 页签
|
||||
|
||||
#### 3.2 字典配置
|
||||
- "规格"Tab 下挂 4 套独立字典,共 42 个字典项:
|
||||
- **包装单位字典**(首期 9 项:散装、件、袋、盒、箱、盘、份、瓶、罐;目标态 15 项)
|
||||
- **切配类型字典**(8 项:丁、丝、片、块、段、末、泥、条,每项含默认尺寸与刀工)
|
||||
- **份型字典**(5 项:标准份、小份、大份、迷你份、加大份,每项含默认克重)
|
||||
- **包装方式字典**(6 项:真空、气调、普通、冷鲜、保鲜盒、散装托盘)
|
||||
|
||||
---
|
||||
|
||||
### 4. 净菜加工与入库
|
||||
|
||||
#### 4.1 净菜加工批次
|
||||
- 删除页内写死的"食材→净菜类型"映射表,改为从食材档案动态读取
|
||||
- 净菜类型下拉显示从"只显示名称"升级为"名称 + 切配规格"(便于业务方识别)
|
||||
- 表格新增"切配规格"列
|
||||
- 出净率改为真实计算(净菜产出 ÷ 毛菜投入),支持反向推导(输入净菜需求量自动算毛菜重量)
|
||||
|
||||
#### 4.2 净菜入库
|
||||
- 删除页内写死的净菜类型清单
|
||||
- 入库规格下拉统一为"克重 + 包装单位"格式(如 `500g/袋`)
|
||||
- 新增"切配规格"只读回显字段
|
||||
- 与上游净菜加工批次页之间通过页面跳转参数传递切配规格
|
||||
|
||||
#### 4.3 采供下单
|
||||
- 采购规格从原来的"自由文本输入"改为按食材自动带出(来源:食材档案的采购级规格)
|
||||
- 液体类食材单位修正(如花生油 `5L/桶` → `5kg/桶`,按 1ml≈1g 换算)
|
||||
|
||||
#### 4.4 毛菜入库
|
||||
- 入库规格从"7 种固定下拉选项"改为自动从采购订单继承,无需重选
|
||||
|
||||
---
|
||||
|
||||
### 5. 订单与配送
|
||||
|
||||
#### 5.1 净菜订单
|
||||
- 删除两套页内写死的规格字典(普通模式一份、餐品模式一份、汇总表一份,三套数据互不一致)
|
||||
- 订单行的份型规格统一为"克重 + 单位/份型"组合,以连体组件录入(左克重 + 右单位/份型)
|
||||
- 餐品模式下,选择餐品后,份型下拉会动态加载该餐品在总库中维护的份型列表(不再固定)
|
||||
- 重量计算公式修正:考虑包装单位的容量换算
|
||||
- 单位下拉选项来源:字典管理页的"包装单位字典"
|
||||
|
||||
#### 5.2 外售
|
||||
- 同 5.1,删除页内写死的规格字典
|
||||
- 录入与展示格式统一
|
||||
- 默认包装单位为"箱"(下单偏批量)
|
||||
|
||||
#### 5.3 内供
|
||||
- 同 5.1,删除页内写死的规格字典
|
||||
- 录入与展示格式统一
|
||||
- 默认包装单位为"盘"(食堂内部配送常用)
|
||||
|
||||
---
|
||||
|
||||
### 6. 生产加工
|
||||
|
||||
#### 6.1 净菜组配
|
||||
- 删除"3 个全局固定份型选项"(小份/标准份/大份),原下拉与所选餐品完全脱钩
|
||||
- 改为餐品级联动:选择餐品后,份型下拉动态加载该餐品在总库的份型列表
|
||||
- 选中份型后自动带出对应克重与单位
|
||||
|
||||
#### 6.2 按单烹制
|
||||
- 规格字段改为只读回显(从上游组配单继承,本页不再允许编辑规格)
|
||||
- 规格展示统一为 `克重g/份型`
|
||||
- **新增"生熟比"自动计算**:输入生重后,熟重按出净率自动计算(向上取整),出净率从食材档案关联读取
|
||||
|
||||
#### 6.3 净菜进柜
|
||||
- 进柜记录的"包装规格"字段原有多种自由文本形态(如 `5kg/袋`、`标准份(500g)`、`6份/箱`),统一结构化
|
||||
- 抽屉详情回显与餐品下拉的规格展示统一为同一格式
|
||||
|
||||
---
|
||||
|
||||
### 7. 包装与标签
|
||||
|
||||
#### 7.1 包装标签
|
||||
- **净菜包规格**:从"纯数字输入(无单位)"升级为"左克重 + 右单位"连体组件
|
||||
- **餐品包规格**:删除"2 个全局固定选项"(与具体餐品无关),改为按当前餐品动态生成份型选项
|
||||
- 包装方式(真空袋/保鲜盒/散装托盘/冷鲜袋)独立为单独字段
|
||||
- 与设备端"净菜包装贴标秤"的口径完全对齐
|
||||
|
||||
---
|
||||
|
||||
### 8. 营养质控
|
||||
|
||||
#### 8.1 营养质控
|
||||
- "同步检验均值→餐品总库"按钮真落地(原为假操作)
|
||||
- 同步操作会把检验均值(8 项营养指标 + 4 类感官评级)写入餐品总库对应餐品的"营养规格"字段
|
||||
- 同步记录持久化保存,供"营养质控同步记录"页面读取
|
||||
|
||||
#### 8.2 营养质控 - 新增检验
|
||||
- 删除页内写死的餐品清单
|
||||
- 改为从餐品总库动态读取,按食堂餐线分组展示
|
||||
- 三级联动保留(场所 → 餐线 → 餐品)
|
||||
- 无数据时回退到内置示例,保证页面可用
|
||||
|
||||
#### 8.3 营养质控 - 同步记录
|
||||
- 数据源改为读取真实的同步记录(由 8.1 写入)
|
||||
- 无记录时回退到内置示例展示
|
||||
|
||||
---
|
||||
|
||||
## 四、设备端改动清单(食堂设备)
|
||||
|
||||
### 10. 毛菜验收秤
|
||||
|
||||
#### 10.1 验收检验
|
||||
- 食材目录 20 项食材默认标记为"称重验收"(按重量过磅)
|
||||
- 调料目录 8 项调料默认标记为"点收验收"(按件数清点)
|
||||
- 单位下拉按验收类型自动过滤:称重类只显示重量单位(斤/公斤/克/千克),点收类只显示计数单位(袋/瓶/箱/桶等)
|
||||
- **关键变更**:验收重量的存储单位统一为"克",展示层仍兼容"斤"显示(如 2000g 显示为"4 斤"),业务方无感知
|
||||
|
||||
---
|
||||
|
||||
### 11. 毛菜加工秤(4 个页面)
|
||||
|
||||
#### 11.1 清洗任务列表
|
||||
- 任务卡片中的重量信息统一改为克存储,展示层自动还原"斤"显示
|
||||
|
||||
#### 11.2 批次称重
|
||||
- 批次重量数据统一改为克存储
|
||||
- 所有重量相关的文本节点(需 X 斤 / 已录 X 斤 / 总重量)自动按克→斤换算展示
|
||||
|
||||
#### 11.3 批次清洗
|
||||
- 同 11.2,存储改克、展示兼容斤
|
||||
- 清洗后的出净率字段补齐(每项食材默认出净率 85%~95%)
|
||||
|
||||
#### 11.4 清洗历史
|
||||
- 同 11.2,存储改克、展示兼容斤
|
||||
- 修复原有的展示层语法错误
|
||||
|
||||
---
|
||||
|
||||
### 12. 净菜加工秤 - 设置
|
||||
|
||||
#### 12.1 加工参数设置
|
||||
- 删除"重量单位"下拉(与设备端"克存储"统一方向冲突),改为说明文字提示(由系统自动还原"斤"显示)
|
||||
- 滚刀块大小的单位统一为毫米(原厘米枚举值统一换算:`2cm → 20mm`、`3cm → 30mm`、`5cm → 50mm`)
|
||||
- 加工参数区新增说明:切配参数的默认值来源于食材档案,本设置仅作覆盖
|
||||
|
||||
---
|
||||
|
||||
### 13. 净菜包装贴标秤(3 个页面)⭐ 最复杂
|
||||
|
||||
#### 13.1 包装订单 / 13.2 贴标作业 / 13.3 标签预览
|
||||
- **字段名统一**:三个页面原有的不同字段名统一为同一套命名
|
||||
- **历史 4 种字符串格式兼容**:不同页面原有 4 种不同的切配规格字符串写法(分隔符、空格、全角字符各异),本次全部兼容,统一解析为结构化对象
|
||||
- **包装规格结构化**:从自由文本(如 `500g`、`大份800g`、`真空包装`)统一拆解为"克重 + 单位 + 份型 + 包装方式"四要素
|
||||
- **"真空包装"等纯描述无克重场景**:作为独立的"包装方式"字段处理,不再混入规格字段
|
||||
- 三套示例数据全部结构化改造
|
||||
- 展示层统一格式输出
|
||||
|
||||
---
|
||||
|
||||
### 14. 净菜组配秤(3 个页面)
|
||||
|
||||
#### 14.1 组配任务 / 14.2 称重包装 / 14.3 校验历史
|
||||
- 包装规格从描述性字符串(如 `大份800g`)统一结构化为"份型 + 克重 + 单位"三要素
|
||||
- 切配规格字符串统一结构化,展示层统一格式输出
|
||||
- **接入餐品总库**:组配任务页启动时从餐品总库加载餐品清单与份型选项,不再使用页内写死数据
|
||||
- 与"净菜包装贴标秤"口径完全对齐
|
||||
|
||||
---
|
||||
|
||||
## 五、给研发的关键提示
|
||||
|
||||
### 5.1 数据存储统一原则
|
||||
- **所有重量字段的存储单位必须是"克(g)"**,数据库字段、接口入参/返参均以克为单位
|
||||
- "斤"仅作为设备端验收秤的展示层兼容,业务方看到的"4 斤"在系统中存储为 2000g
|
||||
|
||||
### 5.2 跨页数据共享机制
|
||||
- 原型阶段使用浏览器本地存储(localStorage)模拟"餐品总库 → 下游页面"的数据流
|
||||
- **研发落地时需替换为后端接口**,建议提供"餐品规格全量查询"接口 + 前端缓存层
|
||||
- 关键操作前需校验数据时效(避免下游页读到过期数据)
|
||||
|
||||
### 5.3 字典落地
|
||||
- 4 套字典(包装单位 / 切配类型 / 份型 / 包装方式)已落地到字典管理页
|
||||
- 研发需建字典主表 + 字典项表,按字典编码过滤加载
|
||||
- 首期落地的字典项数(见 3.2 节)为最小可用集,后续可由业务方在字典管理页扩展
|
||||
|
||||
### 5.4 历史数据迁移
|
||||
- 设备端原有 4 种切配规格字符串格式必须全部兼容(否则历史数据会丢失切配参数)
|
||||
- 建议提供统一的"历史规格字符串 → 结构化对象"解析工具,所有页面共用
|
||||
|
||||
### 5.5 已消除的隐患
|
||||
- 原系统 3 套互不一致的写死规格字典(订单页一份、外售页一份、汇总表一份)已全部删除,统一数据源为"餐品总库"
|
||||
- 原设备端"斤"作为存储单位导致的下游断链问题已修复
|
||||
|
||||
---
|
||||
|
||||
## 六、配套文档
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| 规格管理体系分析与设计方案 | 设计方案权威文档(架构与决策) |
|
||||
| 规格体系统一化改造任务清单 | 24 个改造任务的执行手册(含精确文件定位与核查记录) |
|
||||
| CHANGELOG | 本次变更的完整变更日志 |
|
||||
|
||||
---
|
||||
|
||||
> 本文档为产品→研发的需求同步说明,仅描述"改了哪些功能",不涉及具体字段名/方法名。研发评估工作量与数据契约时,请结合"改造任务清单"与"设计方案"两份文档对照查阅。
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,10 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
.items-table input,.items-table select{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;font-size:var(--font-xs);outline:none;width:100%;}
|
||||
.items-table input:focus,.items-table select:focus{border-color:var(--primary);}
|
||||
.items-table .btn-remove{color:var(--danger);cursor:pointer;font-size:16px;border:none;background:none;padding:4px;}
|
||||
/* 规格输入组(表格内):克重 input + 单位 select 连在一起 */
|
||||
.items-table .spec-input-group{display:flex;align-items:stretch;height:30px;}
|
||||
.items-table .spec-input-group input{flex:1;border-radius:4px 0 0 4px;border-right:none;width:auto;}
|
||||
.items-table .spec-input-group select{width:72px;border-radius:0 4px 4px 0;}
|
||||
.items-summary{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding:12px 16px;background:#F6F9FF;border-radius:6px;}
|
||||
.items-summary__total{font-size:var(--font-sm);color:var(--text-primary);font-weight:600;}
|
||||
/* 右侧抽屉 */
|
||||
@@ -413,7 +417,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-DAMI-1000-20260413-001</td><td>大米</td><td>粮油类</td><td>1000</td><td>20kg/箱</td><td>¥3.5</td><td>¥3,500</td><td>—</td><td>东北五常大米</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-HSYO-168K-20260413-002</td><td>花生油</td><td>粮油类</td><td>168</td><td>5L/桶</td><td>¥16</td><td>¥2,688</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-HSYO-168K-20260413-002</td><td>花生油</td><td>粮油类</td><td>168</td><td>5kg/桶</td><td>¥16</td><td>¥2,688</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:2 种食材,1168 kg,¥6,188</div>
|
||||
@@ -462,7 +466,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
</div>
|
||||
<table class="items-table" id="itemsTable">
|
||||
<thead>
|
||||
<tr><th style="width:140px;">溯源码</th><th style="width:110px;">食材名称</th><th style="width:80px;">食材分类</th><th style="width:80px;">采购数量</th><th style="width:100px;">规格</th><th style="width:80px;">单价(元)</th><th style="width:70px;">小计金额</th><th>备注</th><th style="width:36px;"></th></tr>
|
||||
<tr><th style="width:140px;">溯源码</th><th style="width:110px;">食材名称</th><th style="width:80px;">食材分类</th><th style="width:80px;">采购数量</th><th style="width:150px;">规格</th><th style="width:80px;">单价(元)</th><th style="width:70px;">小计金额</th><th>备注</th><th style="width:36px;"></th></tr>
|
||||
</thead>
|
||||
<tbody id="itemsBody"></tbody>
|
||||
</table>
|
||||
@@ -643,6 +647,22 @@ var supplierContacts = {
|
||||
'粮油批发商行': {contact:'孙强', phone:'137-5544-3210'}
|
||||
};
|
||||
|
||||
/* ===== 包装单位字典(从 din-dict-config.html?code=spec_unit 同步,所有业务共用) ===== */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function renderUnitOptions(selectEl,selected){
|
||||
if(!selectEl)return;
|
||||
var html='<option value="">请选择</option>'+SPEC_UNIT_OPTIONS.map(function(u){return '<option'+(u===selected?' selected':'')+'>'+u+'</option>';}).join('');
|
||||
selectEl.innerHTML=html;
|
||||
}
|
||||
/* 解析列表展示用的规格字符串(如 "25kg/件" → {gram:25,unit:'件'}) */
|
||||
function parseSpecText(specText){
|
||||
if(!specText)return {gram:'',unit:''};
|
||||
var m=specText.match(/^([\d.]+)kg\/(.+)$/);
|
||||
if(m)return {gram:parseFloat(m[1]),unit:m[2]};
|
||||
if(specText==='散装(kg)')return {gram:'',unit:'散装'};
|
||||
return {gram:'',unit:''};
|
||||
}
|
||||
|
||||
/* ===== 模拟数据 ===== */
|
||||
var orders = [
|
||||
{ no:'PO-20260416-005', canteen:'二号场所', canteenContact:'赵芳', canteenPhone:'029-8812-3002',
|
||||
@@ -683,7 +703,7 @@ var orders = [
|
||||
cancelReason:'供应商无法供货',
|
||||
items:[
|
||||
{trace:'ST-EX-LY02-DG-DAMI-1000-20260413-001',name:'大米',cat:'粮油类',qty:1000,spec:'20kg/箱',price:3.5,subtotal:'¥3,500',acceptedQty:'—',remark:'东北五常大米'},
|
||||
{trace:'ST-EX-LY02-DG-HSYO-168K-20260413-002',name:'花生油',cat:'粮油类',qty:168,spec:'5L/桶',price:16,subtotal:'¥2,688',acceptedQty:'—',remark:'—'}
|
||||
{trace:'ST-EX-LY02-DG-HSYO-168K-20260413-002',name:'花生油',cat:'粮油类',qty:168,spec:'5kg/桶',price:16,subtotal:'¥2,688',acceptedQty:'—',remark:'—'}
|
||||
]}
|
||||
];
|
||||
|
||||
@@ -749,14 +769,15 @@ function openEditModal(idx) {
|
||||
var tbody = document.getElementById('itemsBody');
|
||||
tbody.innerHTML = '';
|
||||
o.items.forEach(function(it) {
|
||||
var specParts = parseSpecText(it.spec);
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td><input type="text" value="'+it.trace+'" placeholder="请输入溯源码(选填)"></td>'
|
||||
+'<td><select><option'+(it.name==='菠菜'?' selected':'')+'>菠菜</option><option'+(it.name==='白菜'?' selected':'')+'>白菜</option><option'+(it.name==='胡萝卜'?' selected':'')+'>胡萝卜</option><option'+(it.name==='西兰花'?' selected':'')+'>西兰花</option><option'+(it.name==='芹菜'?' selected':'')+'>芹菜</option><option'+(it.name==='猪里脊'?' selected':'')+'>猪里脊</option><option'+(it.name==='猪五花'?' selected':'')+'>猪五花</option><option'+(it.name==='草鱼'?' selected':'')+'>草鱼</option><option'+(it.name==='鲫鱼'?' selected':'')+'>鲫鱼</option><option'+(it.name==='虾'?' selected':'')+'>虾</option><option'+(it.name==='大米'?' selected':'')+'>大米</option><option'+(it.name==='花生油'?' selected':'')+'>花生油</option></select></td>'
|
||||
+'<td><select><option'+(it.cat==='蔬菜类'?' selected':'')+'>蔬菜类</option><option'+(it.cat==='肉禽类'?' selected':'')+'>肉禽类</option><option'+(it.cat==='水产类'?' selected':'')+'>水产类</option><option'+(it.cat==='粮油类'?' selected':'')+'>粮油类</option><option>调味品</option><option>豆制品</option></select></td>'
|
||||
+'<td><input type="number" value="'+it.qty+'" onchange="calcRow(this)"></td>'
|
||||
+'<td><input type="text" value="'+it.spec+'" placeholder="如:25kg/件"></td>'
|
||||
+'<td><input type="number" value="'+it.price+'" onchange="calcRow(this)"></td>'
|
||||
+'<td style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">'+it.subtotal+'</td>'
|
||||
+'<td><input type="number" class="item-qty" value="'+it.qty+'" onchange="calcRow(this)"></td>'
|
||||
+'<td><div class="spec-input-group"><input type="number" class="item-gram" value="'+specParts.gram+'" placeholder="克重" onchange="calcRow(this)"><select class="item-unit" onchange="calcRow(this)">'+SPEC_UNIT_OPTIONS.map(function(u){return '<option'+(u===specParts.unit?' selected':'')+'>'+u+'</option>';}).join('')+'</select></div></td>'
|
||||
+'<td><input type="number" class="item-price" value="'+it.price+'" onchange="calcRow(this)"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">'+it.subtotal+'</td>'
|
||||
+'<td><input type="text" value="'+(it.remark==='—'?'':it.remark)+'" placeholder="选填"></td>'
|
||||
+'<td><span class="btn-remove" onclick="removeItemRow(this)">✕</span></td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -774,10 +795,10 @@ function addItemRow() {
|
||||
tr.innerHTML = '<td><input type="text" placeholder="请输入溯源码(选填)"></td>'
|
||||
+'<td><select><option value="">请选择</option><option>菠菜</option><option>白菜</option><option>胡萝卜</option><option>西兰花</option><option>芹菜</option><option>猪里脊</option><option>猪五花</option><option>草鱼</option><option>鲫鱼</option><option>虾</option><option>大米</option><option>花生油</option></select></td>'
|
||||
+'<td><select><option>蔬菜类</option><option>肉禽类</option><option>水产类</option><option>粮油类</option><option>调味品</option><option>豆制品</option></select></td>'
|
||||
+'<td><input type="number" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td><input type="text" placeholder="如:25kg/件"></td>'
|
||||
+'<td><input type="number" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">¥0</td>'
|
||||
+'<td><input type="number" class="item-qty" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td><div class="spec-input-group"><input type="number" class="item-gram" placeholder="克重" onchange="calcRow(this)"><select class="item-unit" onchange="calcRow(this)">'+SPEC_UNIT_OPTIONS.map(function(u){return '<option>'+u+'</option>';}).join('')+'</select></div></td>'
|
||||
+'<td><input type="number" class="item-price" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">¥0</td>'
|
||||
+'<td><input type="text" placeholder="选填"></td>'
|
||||
+'<td><span class="btn-remove" onclick="removeItemRow(this)">✕</span></td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -793,11 +814,10 @@ function removeItemRow(el) {
|
||||
/* 计算单行小计 */
|
||||
function calcRow(el) {
|
||||
var tr = el.closest('tr');
|
||||
var inputs = tr.querySelectorAll('input[type="number"]');
|
||||
var qty = parseFloat(inputs[0].value) || 0;
|
||||
var price = parseFloat(inputs[1].value) || 0;
|
||||
var qty = parseFloat(tr.querySelector('.item-qty').value) || 0;
|
||||
var price = parseFloat(tr.querySelector('.item-price').value) || 0;
|
||||
var sub = qty * price;
|
||||
tr.querySelectorAll('td')[6].textContent = '¥' + sub.toLocaleString();
|
||||
tr.querySelector('.item-subtotal').textContent = '¥' + sub.toLocaleString();
|
||||
recalcTotal();
|
||||
}
|
||||
/* 汇总总金额 */
|
||||
@@ -805,9 +825,8 @@ function recalcTotal() {
|
||||
var rows = document.getElementById('itemsBody').rows;
|
||||
var total = 0;
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var inputs = rows[i].querySelectorAll('input[type="number"]');
|
||||
var qty = parseFloat(inputs[0].value) || 0;
|
||||
var price = parseFloat(inputs[1].value) || 0;
|
||||
var qty = parseFloat(rows[i].querySelector('.item-qty').value) || 0;
|
||||
var price = parseFloat(rows[i].querySelector('.item-price').value) || 0;
|
||||
total += qty * price;
|
||||
}
|
||||
document.getElementById('itemCount').textContent = rows.length;
|
||||
|
||||
@@ -167,6 +167,10 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
|
||||
.inline-builder-table select:focus,.inline-builder-table input:focus{border-color:var(--primary);}
|
||||
.inline-builder-table select{appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 6px center;padding-right:18px;}
|
||||
.inline-builder-table input[readonly]{background:#f8fafc;cursor:not-allowed;}
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位/份型 */
|
||||
.spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
.spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -360,13 +364,52 @@ var ncTypeOptions={
|
||||
'西兰花':['西兰花朵','西兰花梗'],
|
||||
'猪排骨':['排骨块','排骨丁']
|
||||
};
|
||||
var cpSpecOptions={
|
||||
'宫保鸡丁':[{label:'标准份(500g)',grams:500},{label:'加量份(750g)',grams:750}],
|
||||
'清蒸鱼':[{label:'标准份(600g)',grams:600},{label:'大份(900g)',grams:900}],
|
||||
'蒜蓉菠菜':[{label:'标准份(400g)',grams:400},{label:'加量份(600g)',grams:600}],
|
||||
'炒胡萝卜':[{label:'标准份(300g)',grams:300},{label:'大份(500g)',grams:500}],
|
||||
'红烧排骨':[{label:'标准份(800g)',grams:800},{label:'家庭份(1200g)',grams:1200}]
|
||||
/* 跨页 specs 共享工具(任务 3.0):从 localStorage 加载餐品总库 specs */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
function getAllDishNames(){
|
||||
var dishes=loadDishSpecs();
|
||||
return dishes?dishes.map(function(d){return d.name;}):[];
|
||||
}
|
||||
/* 净菜包装单位字典(首期落地 9 项,与 spec_unit 字典一致) */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function buildUnitOptions(selected){
|
||||
var html='';
|
||||
SPEC_UNIT_OPTIONS.forEach(function(u){
|
||||
html+='<option value="'+u+'"'+(u===selected?' selected':'')+'>'+u+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
/* 餐品兜底字典:若总库未初始化,用页内硬编码保证页面可用 */
|
||||
var FALLBACK_DISHES={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:500},{portion:'加量份',baseGram:750}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:600},{portion:'大份',baseGram:900}],
|
||||
'蒜蓉菠菜':[{portion:'标准份',baseGram:400},{portion:'加量份',baseGram:600}],
|
||||
'炒胡萝卜':[{portion:'标准份',baseGram:300},{portion:'大份',baseGram:500}],
|
||||
'红烧排骨':[{portion:'标准份',baseGram:800},{portion:'家庭份',baseGram:1200}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return FALLBACK_DISHES[name]||[{portion:'标准份',baseGram:500,unit:'份'}];
|
||||
}
|
||||
function getDishNamesWithFallback(){
|
||||
var names=getAllDishNames();
|
||||
if(names&&names.length>0)return names;
|
||||
return Object.keys(FALLBACK_DISHES);
|
||||
}
|
||||
|
||||
/* 订单主子表数据 */
|
||||
var orders=[
|
||||
@@ -582,7 +625,7 @@ function resetOrderForm(){
|
||||
function addOrderLine(){
|
||||
lineIdCounter++;
|
||||
var lid='newLine'+lineIdCounter;
|
||||
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',specGram:0,specLabel:'',qty:0,weight:0});
|
||||
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',portion:'标准份',baseGram:0,unit:newOrderNcType==='nc'?'袋':'份',qty:0,weight:0});
|
||||
renderLineBuilders();
|
||||
}
|
||||
|
||||
@@ -612,26 +655,55 @@ function updateNewLineDish(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
line.dish=val;
|
||||
line.specGram=0;line.specLabel='';
|
||||
/* 切换餐品时,若该餐品在总库中已有规格,默认选中第一项 */
|
||||
var specs=getSpecsByDishWithFallback(val);
|
||||
if(specs.length>0){
|
||||
line.portion=specs[0].portion||'标准份';
|
||||
line.baseGram=specs[0].baseGram||0;
|
||||
line.unit=specs[0].unit||'份';
|
||||
}else{
|
||||
line.portion='标准份';line.baseGram=0;line.unit='份';
|
||||
}
|
||||
calcLineWeight(line);
|
||||
refreshSummary();
|
||||
}
|
||||
|
||||
function updateNewLineSpec(lid,val){
|
||||
/* 规格连体组件:左 input 克重,右 select 份型 */
|
||||
function updateNewLineBaseGram(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
if(newOrderNcType==='nc'){
|
||||
line.specGram=parseInt(val||'0',10);
|
||||
line.specLabel=val?(val+'g/袋'):'';
|
||||
}else{
|
||||
line.specGram=parseInt(val||'0',10);
|
||||
var opts=cpSpecOptions[line.dish]||[];
|
||||
var found=opts.find(function(o){return o.grams===line.specGram;});
|
||||
line.specLabel=found?found.label:'';
|
||||
line.baseGram=parseInt(val||'0',10);
|
||||
calcLineWeight(line);
|
||||
refreshSummary();
|
||||
}
|
||||
function updateNewLinePortion(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
line.portion=val;
|
||||
/* 选了份型后,若总库中对应份型有 baseGram,自动带出克重 */
|
||||
var specs=getSpecsByDishWithFallback(line.dish);
|
||||
var found=specs.find(function(s){return s.portion===val;});
|
||||
if(found&&found.baseGram){
|
||||
line.baseGram=found.baseGram;
|
||||
if(found.unit)line.unit=found.unit;
|
||||
}
|
||||
calcLineWeight(line);
|
||||
refreshSummary();
|
||||
}
|
||||
function updateNewLineUnit(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
line.unit=val;
|
||||
refreshSummary();
|
||||
}
|
||||
function updateNewLineNcBaseGram(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
line.baseGram=parseInt(val||'0',10);
|
||||
line.unit='袋';
|
||||
calcLineWeight(line);
|
||||
refreshSummary();
|
||||
}
|
||||
|
||||
function updateNewLineQty(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
@@ -642,12 +714,24 @@ function updateNewLineQty(lid,val){
|
||||
}
|
||||
|
||||
function calcLineWeight(line){
|
||||
if(line.specGram>0&&line.qty>0){
|
||||
line.weight=parseFloat((line.specGram*line.qty/1000).toFixed(2));
|
||||
if(line.baseGram>0&&line.qty>0){
|
||||
line.weight=parseFloat((line.baseGram*line.qty/1000).toFixed(2));
|
||||
}else{
|
||||
line.weight=0;
|
||||
}
|
||||
}
|
||||
/* 拼装规格显示文本:baseGram+unit/portion */
|
||||
function formatSpecLabel(line){
|
||||
var gram=line.baseGram||0;
|
||||
var unit=line.unit||(line.ncType==='nc'?'袋':'份');
|
||||
var portion=line.portion||'';
|
||||
if(line.ncType==='nc'){
|
||||
return gram>0?(gram+'g/'+unit):'—';
|
||||
}
|
||||
/* cp 模式 */
|
||||
if(gram<=0)return '—';
|
||||
return gram+'g/'+(portion||unit);
|
||||
}
|
||||
|
||||
function renderLineBuilders(){
|
||||
var container=document.getElementById('orderLinesContainer');
|
||||
@@ -656,12 +740,15 @@ function renderLineBuilders(){
|
||||
html+='<table class="inline-builder-table" id="lineBuilderTable">';
|
||||
html+='<thead><tr>';
|
||||
if(isNc){
|
||||
html+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
|
||||
html+='<th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
|
||||
}else{
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
|
||||
}
|
||||
html+='</tr></thead><tbody>';
|
||||
/* 缓存当前餐品名列表,避免 forEach 内重复读取 localStorage */
|
||||
var dishNames=getDishNamesWithFallback();
|
||||
newOrderLines.forEach(function(line){
|
||||
line.ncType=isNc?'nc':'cp';
|
||||
html+='<tr>';
|
||||
if(isNc){
|
||||
/* 食材 */
|
||||
@@ -673,18 +760,26 @@ function renderLineBuilders(){
|
||||
html+='<td><select onchange="updateNewLineNcType(\''+line.id+'\',this.value)" '+(ncTypes.length===0?'disabled':'')+' style="min-width:100px;"><option value="">'+(ncTypes.length===0?'请先选食材':'请选择')+'</option>';
|
||||
ncTypes.forEach(function(t){html+='<option value="'+t+'" '+(line.ncName===t?'selected':'')+'>'+t+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* 包装规格 */
|
||||
html+='<td><input inputmode="numeric" placeholder="克数" value="'+(line.specGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateNewLineSpec(\''+line.id+'\',this.value)" style="width:70px;"></td>';
|
||||
/* 包装规格:左 input 克重 + 右 select 单位(默认袋) */
|
||||
html+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(line.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateNewLineNcBaseGram(\''+line.id+'\',this.value)">'
|
||||
+'<select onchange="updateNewLineUnit(\''+line.id+'\',this.value)">'+buildUnitOptions(line.unit||'袋')+'</select>'
|
||||
+'</div></td>';
|
||||
}else{
|
||||
/* 餐品名称 */
|
||||
/* 餐品名称:从总库读取 */
|
||||
html+='<td><select onchange="updateNewLineDish(\''+line.id+'\',this.value)" style="min-width:120px;"><option value="">请选择</option>';
|
||||
Object.keys(cpSpecOptions).forEach(function(k){html+='<option value="'+k+'" '+(line.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* 包装规格 */
|
||||
var cpSpecs=cpSpecOptions[line.dish]||[];
|
||||
html+='<td><select onchange="updateNewLineSpec(\''+line.id+'\',this.value)" '+(cpSpecs.length===0?'disabled':'')+' style="min-width:120px;"><option value="">'+(cpSpecs.length===0?'请先选餐品':'请选择')+'</option>';
|
||||
cpSpecs.forEach(function(s){html+='<option value="'+s.grams+'" '+(line.specGram===s.grams?'selected':'')+'>'+s.label+'</option>';});
|
||||
dishNames.forEach(function(k){html+='<option value="'+k+'" '+(line.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* 包装规格:左 input 克重 + 右 select 份型 */
|
||||
var cpSpecs=getSpecsByDishWithFallback(line.dish);
|
||||
var portionOptions=['标准份','小份','大份','迷你份','加大份'];
|
||||
/* 若总库返回的 portion 不在标准列表中,合并显示 */
|
||||
cpSpecs.forEach(function(s){if(s.portion&&portionOptions.indexOf(s.portion)===-1)portionOptions.push(s.portion);});
|
||||
html+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(line.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateNewLineBaseGram(\''+line.id+'\',this.value)">'
|
||||
+'<select onchange="updateNewLinePortion(\''+line.id+'\',this.value)" style="width:90px;">';
|
||||
portionOptions.forEach(function(p){html+='<option value="'+p+'" '+(line.portion===p?'selected':'')+'>'+p+'</option>';});
|
||||
html+='</select></div></td>';
|
||||
}
|
||||
/* 份数 */
|
||||
html+='<td><input inputmode="numeric" placeholder="份数" value="'+(line.qty||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateNewLineQty(\''+line.id+'\',this.value)" style="width:56px;"></td>';
|
||||
@@ -717,10 +812,11 @@ function refreshSummary(){
|
||||
var html='';
|
||||
validLines.forEach(function(l){
|
||||
var typeLabel=newOrderNcType==='nc'?'净菜包':'餐品净菜包';
|
||||
var specLabel=formatSpecLabel(l);
|
||||
if(newOrderNcType==='nc'){
|
||||
html+='<tr><td>'+typeLabel+'</td><td>'+l.ingredient+'</td><td>'+l.ncName+'</td><td>'+l.specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
html+='<tr><td>'+typeLabel+'</td><td>'+l.ingredient+'</td><td>'+l.ncName+'</td><td>'+specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
}else{
|
||||
html+='<tr><td>'+typeLabel+'</td><td>'+l.dish+'</td><td>'+l.specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
html+='<tr><td>'+typeLabel+'</td><td>'+l.dish+'</td><td>'+specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
}
|
||||
});
|
||||
tbody.innerHTML=html;
|
||||
@@ -730,8 +826,8 @@ function submitCreateOrder(){
|
||||
var creator=document.getElementById('orderCreator').value.trim();
|
||||
if(!creator){alert('请输入创建人姓名');return;}
|
||||
var validLines=newOrderLines.filter(function(l){
|
||||
if(newOrderNcType==='nc')return l.ingredient&&l.ncName&&l.specGram>0&&l.qty>0;
|
||||
return l.dish&&l.specGram>0&&l.qty>0;
|
||||
if(newOrderNcType==='nc')return l.ingredient&&l.ncName&&l.baseGram>0&&l.qty>0;
|
||||
return l.dish&&l.baseGram>0&&l.qty>0;
|
||||
});
|
||||
if(validLines.length===0){alert('请至少完善一条菜品明细');return;}
|
||||
/* 生成订单号 */
|
||||
@@ -746,7 +842,11 @@ function submitCreateOrder(){
|
||||
var base={
|
||||
id:'L'+(Date.now()+i),
|
||||
ncType:newOrderNcType,
|
||||
spec:l.specLabel,
|
||||
spec:formatSpecLabel(l),
|
||||
/* 同时保留结构化字段,便于下游页消费 */
|
||||
portion:l.portion||'',
|
||||
baseGram:l.baseGram||0,
|
||||
unit:l.unit||(l.ncType==='nc'?'袋':'份'),
|
||||
qty:l.qty,
|
||||
weight:l.weight,
|
||||
status:'备货中'
|
||||
@@ -893,22 +993,46 @@ var WEEK_MENUS={
|
||||
[['水果拼盘','牛奶'],['水果拼盘'],['绿豆汤','水果拼盘'],['水果拼盘'],[],[],[]]
|
||||
]}
|
||||
};
|
||||
var DISH_SPEC={
|
||||
'白米饭':{spec:'标准份(500g)',grams:500},'杂粮饭':{spec:'标准份(500g)',grams:500},
|
||||
'小米粥':{spec:'标准份(400g)',grams:400},'皮蛋瘦肉粥':{spec:'标准份(450g)',grams:450},
|
||||
'燕麦粥':{spec:'标准份(400g)',grams:400},'红薯粥':{spec:'标准份(400g)',grams:400},
|
||||
'馒头':{spec:'标准份(200g)',grams:200},'花卷':{spec:'标准份(200g)',grams:200},
|
||||
'全麦馒头':{spec:'标准份(200g)',grams:200},'煎饺':{spec:'标准份(300g)',grams:300},
|
||||
'牛奶':{spec:'标准份(250ml)',grams:250},'煮鸡蛋':{spec:'标准份(60g)',grams:60},
|
||||
'红烧肉':{spec:'标准份(800g)',grams:800},'糖醋排骨':{spec:'标准份(800g)',grams:800},
|
||||
'清蒸鲈鱼':{spec:'标准份(600g)',grams:600},'宫保鸡丁':{spec:'标准份(500g)',grams:500},
|
||||
'清蒸鸡胸':{spec:'标准份(500g)',grams:500},'番茄炖牛腩':{spec:'标准份(800g)',grams:800},
|
||||
'西红柿炒蛋':{spec:'标准份(400g)',grams:400},'麻婆豆腐':{spec:'标准份(400g)',grams:400},
|
||||
'清炒时蔬':{spec:'标准份(300g)',grams:300},'清炒西兰花':{spec:'标准份(300g)',grams:300},
|
||||
'凉拌黄瓜':{spec:'标准份(250g)',grams:250},'酸辣土豆丝':{spec:'标准份(350g)',grams:350},
|
||||
'紫菜蛋花汤':{spec:'标准份(350g)',grams:350},'冬瓜排骨汤':{spec:'标准份(400g)',grams:400},
|
||||
'水果拼盘':{spec:'标准份(300g)',grams:300},'绿豆汤':{spec:'标准份(350g)',grams:350}
|
||||
/* 食谱餐品兜底字典(总库未覆盖时的页内默认规格,统一使用 portion+baseGram+unit) */
|
||||
var RECIPE_FALLBACK={
|
||||
'白米饭':{portion:'标准份',baseGram:500,unit:'份'},
|
||||
'杂粮饭':{portion:'标准份',baseGram:500,unit:'份'},
|
||||
'小米粥':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'皮蛋瘦肉粥':{portion:'标准份',baseGram:450,unit:'份'},
|
||||
'燕麦粥':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'红薯粥':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'馒头':{portion:'标准份',baseGram:200,unit:'份'},
|
||||
'花卷':{portion:'标准份',baseGram:200,unit:'份'},
|
||||
'全麦馒头':{portion:'标准份',baseGram:200,unit:'份'},
|
||||
'煎饺':{portion:'标准份',baseGram:300,unit:'份'},
|
||||
'牛奶':{portion:'标准份',baseGram:250,unit:'瓶'},
|
||||
'煮鸡蛋':{portion:'标准份',baseGram:60,unit:'份'},
|
||||
'红烧肉':{portion:'标准份',baseGram:800,unit:'份'},
|
||||
'糖醋排骨':{portion:'标准份',baseGram:800,unit:'份'},
|
||||
'清蒸鲈鱼':{portion:'标准份',baseGram:600,unit:'份'},
|
||||
'宫保鸡丁':{portion:'标准份',baseGram:500,unit:'份'},
|
||||
'清蒸鸡胸':{portion:'标准份',baseGram:500,unit:'份'},
|
||||
'番茄炖牛腩':{portion:'标准份',baseGram:800,unit:'份'},
|
||||
'西红柿炒蛋':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'麻婆豆腐':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'清炒时蔬':{portion:'标准份',baseGram:300,unit:'份'},
|
||||
'清炒西兰花':{portion:'标准份',baseGram:300,unit:'份'},
|
||||
'凉拌黄瓜':{portion:'标准份',baseGram:250,unit:'份'},
|
||||
'酸辣土豆丝':{portion:'标准份',baseGram:350,unit:'份'},
|
||||
'紫菜蛋花汤':{portion:'标准份',baseGram:350,unit:'份'},
|
||||
'冬瓜排骨汤':{portion:'标准份',baseGram:400,unit:'份'},
|
||||
'水果拼盘':{portion:'标准份',baseGram:300,unit:'份'},
|
||||
'绿豆汤':{portion:'标准份',baseGram:350,unit:'份'}
|
||||
};
|
||||
/* 食谱餐品规格查询:优先总库,其次页内兜底 */
|
||||
function getDishSpecForRecipe(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0){
|
||||
var s=specs[0];
|
||||
return {portion:s.portion||'标准份',baseGram:s.baseGram||500,unit:s.unit||'份'};
|
||||
}
|
||||
return RECIPE_FALLBACK[name]||{portion:'标准份',baseGram:500,unit:'份'};
|
||||
}
|
||||
var MEAL_NAMES=['早餐','午餐','晚餐','加餐'];
|
||||
|
||||
function getDefaultDate(){return '2026-05-20';}
|
||||
@@ -968,7 +1092,7 @@ function onRecipeParamChange(){
|
||||
mealIndices.forEach(function(mi){
|
||||
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
|
||||
dayDishes.forEach(function(dishName){
|
||||
if(!dishMap[dishName])dishMap[dishName]=DISH_SPEC[dishName]||{spec:'标准份(500g)',grams:500};
|
||||
if(!dishMap[dishName])dishMap[dishName]=getDishSpecForRecipe(dishName);
|
||||
});
|
||||
});
|
||||
var dishNames=Object.keys(dishMap);
|
||||
@@ -977,8 +1101,9 @@ function onRecipeParamChange(){
|
||||
for(var name in dishMap){
|
||||
var sp=dishMap[name];
|
||||
var servings=dinerCount;
|
||||
var weightKg=(sp.grams*servings/1000).toFixed(2);
|
||||
rows+='<tr><td>'+name+'</td><td>'+sp.spec+'</td><td>'+servings+'</td><td>'+weightKg+'</td></tr>';
|
||||
var weightKg=(sp.baseGram*servings/1000).toFixed(2);
|
||||
var specTxt=sp.baseGram+'g/'+(sp.portion||sp.unit);
|
||||
rows+='<tr><td>'+name+'</td><td>'+specTxt+'</td><td>'+servings+'</td><td>'+weightKg+'</td></tr>';
|
||||
}
|
||||
tbody.innerHTML=rows;
|
||||
summaryDiv.style.display='block';
|
||||
@@ -1000,17 +1125,22 @@ function submitRecipeOrder(){
|
||||
mealIndices.forEach(function(mi){
|
||||
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
|
||||
dayDishes.forEach(function(dishName){
|
||||
if(!dishMap[dishName])dishMap[dishName]=DISH_SPEC[dishName]||{spec:'标准份(500g)',grams:500};
|
||||
if(!dishMap[dishName])dishMap[dishName]=getDishSpecForRecipe(dishName);
|
||||
});
|
||||
});
|
||||
var orderNo='CO-'+String(orders.length+1).padStart(3,'0');
|
||||
var newLines=[];
|
||||
for(var name in dishMap){
|
||||
var sp=dishMap[name];
|
||||
var baseGram=sp.baseGram;
|
||||
newLines.push({
|
||||
id:'L'+(Date.now()+Object.keys(dishMap).indexOf(name)),
|
||||
ncType:'cp',foodName:name,spec:sp.spec,qty:parseInt(dinerCount),
|
||||
weight:parseFloat((sp.grams*parseInt(dinerCount)/1000).toFixed(2)),status:'备货中'
|
||||
ncType:'cp',foodName:name,
|
||||
spec:baseGram+'g/'+(sp.portion||sp.unit),
|
||||
portion:sp.portion,baseGram:baseGram,unit:sp.unit,
|
||||
qty:parseInt(dinerCount),
|
||||
weight:parseFloat((baseGram*parseInt(dinerCount)/1000).toFixed(2)),
|
||||
status:'备货中'
|
||||
});
|
||||
}
|
||||
orders.unshift({
|
||||
|
||||
@@ -420,6 +420,73 @@ tbody tr.row-active td.td-actions{background:var(--primary-light);}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* ============== 规格体系工具函数(任务 5.3 内联,权威源 spec-utils-reference.md) ============== */
|
||||
/* 包装规格结构化对象 → 展示字符串:{portion?,baseGram,unit} → "标准份 500g / 份" */
|
||||
function formatSpec(spec){
|
||||
if(!spec) return '—';
|
||||
if(typeof spec === 'string') return spec;
|
||||
if(typeof spec === 'object'){
|
||||
var parts=[];
|
||||
if(spec.portion) parts.push(spec.portion);
|
||||
if(spec.baseGram) parts.push(spec.baseGram+'g');
|
||||
if(spec.unit) parts.push('/ '+spec.unit);
|
||||
return parts.length?parts.join(' '):'—';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
/* 兼容解析旧版包装规格字符串为结构化对象 */
|
||||
function parseLegacyPackSpec(str){
|
||||
if(!str) return null;
|
||||
if(typeof str !== 'object'){
|
||||
var s=String(str).trim().replace(/[()()]/g,'');
|
||||
/* "5kg/袋" 或 "500g/袋" 或 "3kg/袋" */
|
||||
var m1=s.match(/^(\d+(?:\.\d+)?)\s*(kg|g|公斤|克)\s*\/\s*(袋|箱|份|盒|桶|罐)?$/);
|
||||
if(m1){
|
||||
var unit=m1[3]||'袋';
|
||||
var baseGram=parseFloat(m1[1]);
|
||||
if(m1[2]==='kg'||m1[2]==='公斤') baseGram*=1000;
|
||||
return {baseGram:baseGram,unit:unit};
|
||||
}
|
||||
/* "标准份500g" 或 "大份800g" */
|
||||
var m2=s.match(/^(大份|小份|标准份|迷你份)?\s*(\d+(?:\.\d+)?)?\s*(g|克|kg|公斤)?$/);
|
||||
if(m2){
|
||||
var portion=(m2[1])||'';
|
||||
var bg=(m2[2])?parseFloat(m2[2]):0;
|
||||
var u=(m2[3])||'';
|
||||
if(u==='克') u='g';
|
||||
if(u==='公斤') {u='g';bg*=1000;}
|
||||
if(!u) u=bg?'份':'袋';
|
||||
return {portion:portion,baseGram:bg,unit:u};
|
||||
}
|
||||
/* "6份/箱" 语义:6 个份型单位装一箱 */
|
||||
var m3=s.match(/^(\d+)\s*(份|个)?\s*\/\s*(袋|箱|份|盒|桶|罐)?$/);
|
||||
if(m3){
|
||||
return {baseGram:parseInt(m3[1]),unit:m3[3]||'箱',portion:m3[2]||''};
|
||||
}
|
||||
return {baseGram:0,unit:'袋',portion:s};
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/* 跨页共享:从 localStorage 读取餐品规格数据(key: dish_specs_v1) */
|
||||
var DISH_SPECS_KEY='dish_specs_v1';
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem(DISH_SPECS_KEY);
|
||||
if(raw) return JSON.parse(raw);
|
||||
}catch(e){}
|
||||
return {};
|
||||
}
|
||||
/* spec 兜底解析:对象直接返回、字符串走 parseLegacyPackSpec、空值查跨页共享表 */
|
||||
function resolveSpec(name,spec){
|
||||
if(spec && typeof spec==='object') return spec;
|
||||
if(spec && typeof spec==='string') return parseLegacyPackSpec(spec);
|
||||
if(name){
|
||||
var dishSpecs=loadDishSpecs();
|
||||
if(dishSpecs[name]) return dishSpecs[name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* 弹窗开关 */
|
||||
function openModal(id){document.getElementById(id).classList.add('show');}
|
||||
function closeModal(id){document.getElementById(id).classList.remove('show');}
|
||||
@@ -454,19 +521,19 @@ function showVideo(code,name){
|
||||
}
|
||||
function closeVideoModal(){var m=document.getElementById('videoModal');if(m)m.remove();}
|
||||
|
||||
/* 存入净菜弹窗 — 先选订单再选菜品明细 */
|
||||
/* 存入净菜弹窗 — 先选订单再选菜品明细(spec 字段结构化为 {portion?,baseGram,unit} 对象) */
|
||||
var ORDER_DISHES={
|
||||
'JC-2026-0422-007':[
|
||||
{name:'土豆丝',spec:'5kg/袋',qty:'20份',weight:'12.5',time:'2026-04-22 08:00',type:'nc'},
|
||||
{name:'宫保鸡丁',spec:'标准份(500g)',qty:'30份',weight:'15.0',time:'2026-04-22 08:00',type:'cp'}
|
||||
{name:'土豆丝',spec:{baseGram:5000,unit:'袋'},qty:'20份',weight:'12.5',time:'2026-04-22 08:00',type:'nc'},
|
||||
{name:'宫保鸡丁',spec:{portion:'标准份',baseGram:500,unit:'份'},qty:'30份',weight:'15.0',time:'2026-04-22 08:00',type:'cp'}
|
||||
],
|
||||
'JC-2026-0422-008':[
|
||||
{name:'净黄瓜',spec:'3kg/袋',qty:'15份',weight:'9.0',time:'2026-04-22 08:30',type:'nc'},
|
||||
{name:'净西兰花',spec:'500g/袋',qty:'10份',weight:'5.0',time:'2026-04-22 08:30',type:'nc'},
|
||||
{name:'清蒸鱼',spec:'标准份(600g)',qty:'12份',weight:'7.2',time:'2026-04-22 08:30',type:'cp'}
|
||||
{name:'净黄瓜',spec:{baseGram:3000,unit:'袋'},qty:'15份',weight:'9.0',time:'2026-04-22 08:30',type:'nc'},
|
||||
{name:'净西兰花',spec:{baseGram:500,unit:'袋'},qty:'10份',weight:'5.0',time:'2026-04-22 08:30',type:'nc'},
|
||||
{name:'清蒸鱼',spec:{portion:'标准份',baseGram:600,unit:'份'},qty:'12份',weight:'7.2',time:'2026-04-22 08:30',type:'cp'}
|
||||
],
|
||||
'JC-2026-0422-009':[
|
||||
{name:'宫保鸡丁',spec:'6份/箱',qty:'30份',weight:'18.0',time:'2026-04-22 07:45',type:'cp'}
|
||||
{name:'宫保鸡丁',spec:{baseGram:6,unit:'箱',portion:'份'},qty:'30份',weight:'18.0',time:'2026-04-22 07:45',type:'cp'}
|
||||
]
|
||||
};
|
||||
function onOrderSelect(sel){
|
||||
@@ -478,7 +545,10 @@ function onOrderSelect(sel){
|
||||
var dishes=ORDER_DISHES[v]||[];
|
||||
dishSelect.innerHTML='<option value="">请选择菜品</option>';
|
||||
dishes.forEach(function(d,i){
|
||||
dishSelect.innerHTML+='<option value="'+i+'">'+d.name+' — '+d.spec+' — '+d.qty+'</option>';
|
||||
/* spec 结构化后用 formatSpec 渲染为下拉文本 */
|
||||
var specObj=resolveSpec(d.name,d.spec);
|
||||
var specText=formatSpec(specObj);
|
||||
dishSelect.innerHTML+='<option value="'+i+'">'+d.name+' — '+specText+' — '+d.qty+'</option>';
|
||||
});
|
||||
dishSelect.value='';
|
||||
dishArea.style.display='flex';
|
||||
@@ -492,8 +562,10 @@ function onDishSelect(idx){
|
||||
var d=dishes[parseInt(idx)];
|
||||
if(!d){echoArea.style.display='none';return;}
|
||||
echoArea.style.display='';
|
||||
/* 回显包装规格统一走 formatSpec(跨页共享兜底) */
|
||||
var specObj=resolveSpec(d.name,d.spec);
|
||||
document.getElementById('echo-name').textContent=d.name;
|
||||
document.getElementById('echo-spec').textContent=d.spec;
|
||||
document.getElementById('echo-spec').textContent=formatSpec(specObj);
|
||||
document.getElementById('echo-qty').textContent=d.qty;
|
||||
document.getElementById('echo-weight').textContent=d.weight+' kg';
|
||||
document.getElementById('echo-time').textContent=d.time;
|
||||
|
||||
@@ -223,11 +223,11 @@ tr.order-last td{border-bottom:2px solid #d9d9d9;}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>目标餐品<span class="required">*</span></label>
|
||||
<select id="createDish" onchange="calcIngredients()"><option value="">请选择餐品</option><option>宫保鸡丁</option><option>番茄炒蛋</option><option>红烧肉</option><option>清蒸鱼</option><option>炒时蔬</option><option>豆腐</option></select>
|
||||
<select id="createDish" onchange="onComboDishChange()"><option value="">请选择餐品</option></select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>餐品规格<span class="required">*</span></label>
|
||||
<select id="formSpec"><option value="">请选择规格</option><option>小份(200g)</option><option>标准份(300g)</option><option>大份(500g)</option></select>
|
||||
<select id="formSpec" onchange="onComboSpecChange()"><option value="">请选择规格</option></select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>份数<span class="required">*</span></label>
|
||||
@@ -290,6 +290,79 @@ tr.order-last td{border-bottom:2px solid #d9d9d9;}
|
||||
/* 任务截止日期筛选默认当天 */
|
||||
(function(){var d=new Date();var y=d.getFullYear();var m=String(d.getMonth()+1).padStart(2,'0');var day=String(d.getDate()).padStart(2,'0');document.getElementById('filterDeadline').value=y+'-'+m+'-'+day;})();
|
||||
|
||||
/* ========== 跨页 specs 共享工具(任务 3.0) ========== */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
function getAllDishNames(){
|
||||
var dishes=loadDishSpecs();
|
||||
return dishes?dishes.map(function(d){return d.name;}):[];
|
||||
}
|
||||
/* 组配页兜底:总库未覆盖时的页内默认规格 */
|
||||
var COMBO_FALLBACK_DISHES={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'番茄炒蛋':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'红烧肉':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'炒时蔬':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'豆腐':[{portion:'标准份',baseGram:300,unit:'份'}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return COMBO_FALLBACK_DISHES[name]||[{portion:'标准份',baseGram:300,unit:'份'}];
|
||||
}
|
||||
function getDishNamesWithFallback(){
|
||||
var names=getAllDishNames();
|
||||
if(names&&names.length>0)return names;
|
||||
return Object.keys(COMBO_FALLBACK_DISHES);
|
||||
}
|
||||
/* 当前选中规格的 baseGram 缓存(供 calcIngredients 使用) */
|
||||
var _selectedSpec={portion:'标准份',baseGram:300,unit:'份'};
|
||||
|
||||
/* 餐品变更时:重填份型下拉,默认选中第一项 */
|
||||
function onComboDishChange(){
|
||||
var dish=document.getElementById('createDish').value;
|
||||
var specSel=document.getElementById('formSpec');
|
||||
if(!dish){
|
||||
specSel.innerHTML='<option value="">请选择规格</option>';
|
||||
_selectedSpec={portion:'标准份',baseGram:300,unit:'份'};
|
||||
calcIngredients();
|
||||
return;
|
||||
}
|
||||
var specs=getSpecsByDishWithFallback(dish);
|
||||
var optsHtml='';
|
||||
specs.forEach(function(s,i){
|
||||
var label=s.portion+'('+s.baseGram+'g)';
|
||||
optsHtml+='<option value="'+s.portion+'" data-gram="'+s.baseGram+'" data-unit="'+(s.unit||'份')+'"'+(i===0?' selected':'')+'>'+label+'</option>';
|
||||
});
|
||||
specSel.innerHTML=optsHtml;
|
||||
onComboSpecChange();
|
||||
}
|
||||
/* 份型变更时:更新 _selectedSpec 缓存 */
|
||||
function onComboSpecChange(){
|
||||
var specSel=document.getElementById('formSpec');
|
||||
var opt=specSel.options[specSel.selectedIndex];
|
||||
if(!opt||!opt.value){_selectedSpec={portion:'标准份',baseGram:300,unit:'份'};calcIngredients();return;}
|
||||
_selectedSpec={
|
||||
portion:opt.value,
|
||||
baseGram:parseInt(opt.getAttribute('data-gram')||'300',10),
|
||||
unit:opt.getAttribute('data-unit')||'份'
|
||||
};
|
||||
calcIngredients();
|
||||
}
|
||||
|
||||
/* ========== 食材配方数据 ========== */
|
||||
var recipeData={
|
||||
'宫保鸡丁':[
|
||||
@@ -345,9 +418,9 @@ function openModal(){
|
||||
document.getElementById('modalSubmitBtn').textContent='确定';
|
||||
// 重置表单
|
||||
document.getElementById('formTaskId').value='';
|
||||
document.getElementById('createDish').value='';
|
||||
populateComboDishOptions(''); /* 重置餐品下拉 */
|
||||
document.getElementById('createDish').disabled=false;
|
||||
document.getElementById('formSpec').value='';
|
||||
document.getElementById('formSpec').innerHTML='<option value="">请选择规格</option>';
|
||||
document.getElementById('createQty').value='';
|
||||
document.getElementById('formDeadline').value='';
|
||||
document.getElementById('formPerson').value='';
|
||||
@@ -357,6 +430,15 @@ function openModal(){
|
||||
document.getElementById('modalOverlay').style.display='flex';
|
||||
}
|
||||
|
||||
/* 填充组配页餐品下拉 */
|
||||
function populateComboDishOptions(selected){
|
||||
var sel=document.getElementById('createDish');
|
||||
var names=getDishNamesWithFallback();
|
||||
var html='<option value="">请选择餐品</option>';
|
||||
names.forEach(function(n){html+='<option value="'+n+'"'+(n===selected?' selected':'')+'>'+n+'</option>';});
|
||||
sel.innerHTML=html;
|
||||
}
|
||||
|
||||
function openEditModal(el){
|
||||
_modalMode='edit';
|
||||
_editRow=el.closest('tr');
|
||||
@@ -365,9 +447,24 @@ function openEditModal(el){
|
||||
document.getElementById('modalSubmitBtn').textContent='保存';
|
||||
// 回填数据
|
||||
document.getElementById('formTaskId').value=tds[0].textContent;
|
||||
document.getElementById('createDish').value=tds[2].textContent;
|
||||
var dishName=tds[2].textContent;
|
||||
populateComboDishOptions(dishName);
|
||||
document.getElementById('createDish').disabled=true;
|
||||
document.getElementById('formSpec').value=tds[3].textContent;
|
||||
/* 回填份型下拉,并尝试匹配行内规格文本 */
|
||||
var specText=tds[3].textContent.trim();
|
||||
var specs=getSpecsByDishWithFallback(dishName);
|
||||
var specSel=document.getElementById('formSpec');
|
||||
var specHtml='';
|
||||
var matchedIdx=0;
|
||||
specs.forEach(function(s,i){
|
||||
var label=s.portion+'('+s.baseGram+'g)';
|
||||
/* 简易匹配:行内文本含 portion 名称 */
|
||||
if(specText.indexOf(s.portion)>=0||specText.indexOf(String(s.baseGram))>=0)matchedIdx=i;
|
||||
specHtml+='<option value="'+s.portion+'" data-gram="'+s.baseGram+'" data-unit="'+(s.unit||'份')+'">'+label+'</option>';
|
||||
});
|
||||
specSel.innerHTML=specHtml;
|
||||
specSel.selectedIndex=matchedIdx;
|
||||
onComboSpecChange();
|
||||
document.getElementById('createQty').value=tds[4].textContent;
|
||||
document.getElementById('formPerson').value=tds[9].textContent==='—'?'':tds[9].textContent;
|
||||
// 来源和操作类型回填
|
||||
@@ -387,7 +484,9 @@ function submitModal(){
|
||||
if(_modalMode==='edit'&&_editRow){
|
||||
// 编辑模式:更新表格行
|
||||
var tds=_editRow.querySelectorAll('td');
|
||||
tds[3].textContent=document.getElementById('formSpec').value;
|
||||
var specSel=document.getElementById('formSpec');
|
||||
var opt=specSel.options[specSel.selectedIndex];
|
||||
tds[3].textContent=opt?opt.textContent:specSel.value;
|
||||
tds[4].textContent=document.getElementById('createQty').value;
|
||||
var person=document.getElementById('formPerson').value.trim();
|
||||
if(person)tds[9].textContent=person;
|
||||
|
||||
@@ -332,8 +332,9 @@ tr.order-last td{border-bottom:2px solid #d9d9d9;}
|
||||
<div class="form-item"><label>需求份数</label><input id="editQty" type="number" disabled style="background:#f5f5f5;"></div>
|
||||
<div class="form-item"><label>烹制日期</label><input id="editDate" type="text" disabled style="background:#f5f5f5;"></div>
|
||||
<div class="form-item"><label>厨师</label><input id="editChef" type="text" placeholder="请输入厨师姓名"></div>
|
||||
<div class="form-item"><label>生重(kg)</label><input id="editRawWeight" type="text" placeholder="请输入生重"></div>
|
||||
<div class="form-item"><label>熟重(kg)</label><input id="editCookedWeight" type="text" placeholder="请输入熟重"></div>
|
||||
<div class="form-item"><label>生重(kg)</label><input id="editRawWeight" type="text" placeholder="请输入生重" oninput="autoCalcCookedWeight()"></div>
|
||||
<div class="form-item"><label>熟重(kg)</label><input id="editCookedWeight" type="text" placeholder="按生重×生熟比自动计算" disabled></div>
|
||||
<div class="form-item"><label>生熟比(%)</label><input id="editYieldRate" type="text" disabled style="background:#f5f5f5;"></div>
|
||||
</div>
|
||||
<!-- 食材明细(溯源码可修改) -->
|
||||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px;">
|
||||
@@ -355,6 +356,59 @@ tr.order-last td{border-bottom:2px solid #d9d9d9;}
|
||||
/* 烹制日期筛选默认当天 */
|
||||
(function(){var d=new Date();var y=d.getFullYear();var m=String(d.getMonth()+1).padStart(2,'0');var day=String(d.getDate()).padStart(2,'0');document.getElementById('filterCookDate').value=y+'-'+m+'-'+day;})();
|
||||
|
||||
/* ========== 跨页 specs 共享工具(任务 3.0) ========== */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
/* 烹制页兜底规格 */
|
||||
var COOK_FALLBACK_SPECS={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'番茄炒蛋':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'红烧肉':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'蒜蓉菠菜':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'炒时蔬':[{portion:'标准份',baseGram:300,unit:'份'}],
|
||||
'豆腐':[{portion:'标准份',baseGram:300,unit:'份'}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return COOK_FALLBACK_SPECS[name]||[{portion:'标准份',baseGram:300,unit:'份'}];
|
||||
}
|
||||
/* 按行内规格文本匹配总库中的某一规格 */
|
||||
function matchSpecByText(dishName,specText){
|
||||
var specs=getSpecsByDishWithFallback(dishName);
|
||||
if(!specText)return specs[0]||{portion:'标准份',baseGram:300,unit:'份'};
|
||||
/* 优先匹配 portion 名称 */
|
||||
var m=specs.find(function(s){return specText.indexOf(s.portion)>=0;});
|
||||
if(m)return m;
|
||||
/* 退而匹配 baseGram */
|
||||
m=specs.find(function(s){return specText.indexOf(String(s.baseGram))>=0;});
|
||||
if(m)return m;
|
||||
return specs[0]||{portion:'标准份',baseGram:300,unit:'份'};
|
||||
}
|
||||
/* 餐品级生熟比字典(%)—熟重/生重,任务 3.3b 新增 */
|
||||
var DISH_YIELD_RATE={
|
||||
'宫保鸡丁':85,'番茄炒蛋':88,'红烧肉':75,'清蒸鱼':82,
|
||||
'蒜蓉菠菜':80,'炒时蔬':82,'豆腐':90
|
||||
};
|
||||
function getYieldRateByDish(name){return DISH_YIELD_RATE[name]||80;}
|
||||
function formatSpecDisplay(spec){
|
||||
if(!spec)return '—';
|
||||
return (spec.baseGram||0)+'g/'+(spec.unit||'份')+' / '+(spec.portion||'标准份');
|
||||
}
|
||||
|
||||
/* ========== 食材配方数据 ========== */
|
||||
var recipeData={
|
||||
'宫保鸡丁':[
|
||||
@@ -456,12 +510,26 @@ function openEditModal(el){
|
||||
document.getElementById('editTitle').textContent='编辑烹制任务 — '+taskId;
|
||||
document.getElementById('editTaskId').value=taskId;
|
||||
document.getElementById('editDish').value=dishName;
|
||||
document.getElementById('editSpec').value=spec;
|
||||
/* 规格回显:用总库匹配后的结构化字段拼装 */
|
||||
var specObj=matchSpecByText(dishName,spec);
|
||||
document.getElementById('editSpec').value=formatSpecDisplay(specObj);
|
||||
/* 缓存到 dataset,供 submitEdit / autoCalcCookedWeight 使用 */
|
||||
document.getElementById('editSpec').dataset.portion=specObj.portion||'';
|
||||
document.getElementById('editSpec').dataset.baseGram=specObj.baseGram||0;
|
||||
document.getElementById('editSpec').dataset.unit=specObj.unit||'份';
|
||||
document.getElementById('editQty').value=qty;
|
||||
document.getElementById('editDate').value=cookDate;
|
||||
document.getElementById('editChef').value=chef==='—'?'':chef;
|
||||
document.getElementById('editRawWeight').value=rawWeight==='—'?'':rawWeight;
|
||||
document.getElementById('editCookedWeight').value=cookedWeight==='—'?'':cookedWeight;
|
||||
/* 生熟比按餐品带出 */
|
||||
var yr=getYieldRateByDish(dishName);
|
||||
document.getElementById('editYieldRate').value=yr+'%';
|
||||
/* 熟重:若用户已填则保留,否则按生重×生熟比计算 */
|
||||
if(cookedWeight==='—'){
|
||||
autoCalcCookedWeight();
|
||||
}else{
|
||||
document.getElementById('editCookedWeight').value=cookedWeight;
|
||||
}
|
||||
|
||||
// 渲染食材明细
|
||||
var ings=recipeData[dishName]||[];
|
||||
@@ -486,6 +554,15 @@ function openEditModal(el){
|
||||
document.getElementById('editModal').style.display='flex';
|
||||
}
|
||||
function closeEditModal(){document.getElementById('editModal').style.display='none';_editRow=null;}
|
||||
/* 任务 3.3b:熟重自动计算 cookedWeight = rawWeight * yieldRate / 100(向上取整保留 2 位小数) */
|
||||
function autoCalcCookedWeight(){
|
||||
var rw=parseFloat(document.getElementById('editRawWeight').value);
|
||||
var yr=parseFloat(document.getElementById('editYieldRate').value);
|
||||
var cwInput=document.getElementById('editCookedWeight');
|
||||
if(!rw||!yr||isNaN(rw)||isNaN(yr)){cwInput.value='';return;}
|
||||
var cw=Math.ceil(rw*yr/100*100)/100;
|
||||
cwInput.value=cw.toFixed(2);
|
||||
}
|
||||
function submitEdit(){
|
||||
if(_editRow){
|
||||
var tds=_editRow.querySelectorAll('td');
|
||||
@@ -496,11 +573,14 @@ function submitEdit(){
|
||||
var cw=document.getElementById('editCookedWeight').value.trim();
|
||||
if(cw){
|
||||
tds[17].textContent=cw;
|
||||
// 自动计算生熟比
|
||||
// 自动计算生熟比显示(1:x)
|
||||
var rwNum=parseFloat(tds[16].textContent);
|
||||
var cwNum=parseFloat(cw);
|
||||
if(rwNum&&cwNum)tds[18].textContent='1:'+(cwNum/rwNum).toFixed(2);
|
||||
}
|
||||
/* 规格列回写结构化文本 */
|
||||
var specInput=document.getElementById('editSpec');
|
||||
if(specInput.value)tds[5].textContent=specInput.value;
|
||||
closeEditModal();
|
||||
showToast('✅ 烹制任务已更新');
|
||||
}
|
||||
|
||||
@@ -365,7 +365,15 @@ var lineData={
|
||||
'4':[{id:'G',name:'A餐线(自选)'},{id:'H',name:'B餐线(面食)'}],
|
||||
'5':[{id:'I',name:'A餐线(营养餐线)'}]
|
||||
};
|
||||
var dishData={
|
||||
/* 跨页共享:从 localStorage 加载餐品总库数据,按餐线 ID 分组(兜底回退到硬编码) */
|
||||
var CANTEEN_LINE_MAP = {
|
||||
'A':'总部一号食堂','B':'总部一号食堂','C':'总部一号食堂',
|
||||
'D':'总部二号食堂','E':'总部二号食堂',
|
||||
'F':'采油一厂职工食堂',
|
||||
'G':'采油二厂综合食堂','H':'采油二厂综合食堂',
|
||||
'I':'采气一厂营养餐厅'
|
||||
};
|
||||
var FALLBACK_DISH_DATA = {
|
||||
'A':['红烧肉','清炒时蔬','番茄鸡蛋汤','凉拌黄瓜','米饭'],
|
||||
'B':['宫保鸡丁','蒜蓉西兰花','紫菜蛋花汤','糙米饭','水果拼盘'],
|
||||
'C':['牛肉炖土豆','手抓饭','羊肉串','酸奶','馕饼'],
|
||||
@@ -376,6 +384,36 @@ var dishData={
|
||||
'H':['牛肉拉面','油泼面','炸酱面','酸汤水饺'],
|
||||
'I':['营养套餐A','营养套餐B','营养套餐C']
|
||||
};
|
||||
function loadDishSpecs(){
|
||||
try {
|
||||
var raw = localStorage.getItem('dish_specs_v1');
|
||||
if(!raw) return null;
|
||||
var data = JSON.parse(raw);
|
||||
return (data && data.dishes) ? data.dishes : null;
|
||||
} catch(e){ return null; }
|
||||
}
|
||||
/* 按场所(食堂)名对餐品总库做分组,失败时回退到 FALLBACK_DISH_DATA */
|
||||
function buildDishDataFromStore(){
|
||||
var dishes = loadDishSpecs();
|
||||
if(!dishes || !dishes.length){
|
||||
document.getElementById('dishHint').textContent = '⚠️ 餐品总库未初始化,当前显示内置示例菜品。请先在「餐品总库」中初始化数据';
|
||||
return FALLBACK_DISH_DATA;
|
||||
}
|
||||
var result = {};
|
||||
Object.keys(lineData).forEach(function(cid){
|
||||
lineData[cid].forEach(function(line){
|
||||
var canteenName = CANTEEN_LINE_MAP[line.id];
|
||||
var list = [];
|
||||
dishes.forEach(function(d){
|
||||
if(d.category === canteenName){ list.push(d.name); }
|
||||
});
|
||||
/* 若该食堂在总库中无餐品,回退到默认列表 */
|
||||
result[line.id] = list.length ? list : (FALLBACK_DISH_DATA[line.id] || []);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
var dishData = buildDishDataFromStore();
|
||||
|
||||
function onCanteenChange(){
|
||||
var cid=document.getElementById('canteenSelect').value;
|
||||
|
||||
@@ -262,7 +262,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-bar__right">共 <strong>4</strong> 条记录</div>
|
||||
<div class="action-bar__right">共 <strong id="totalCount">4</strong> 条记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
@@ -282,7 +282,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">第 1-4 条 / 共 4 条</div>
|
||||
<div class="pagination__info" id="paginationInfo">第 1-4 条 / 共 4 条</div>
|
||||
<div class="pagination__btns">
|
||||
<button class="page-btn disabled">上一页</button>
|
||||
<button class="page-btn active">1</button>
|
||||
@@ -363,14 +363,63 @@ var RECORDS = [
|
||||
colorData:{mainColor:'金黄色',uniformity:5,gloss:4,freshness:5,appearance:5,score:4.5,grade:'优秀'}}
|
||||
];
|
||||
|
||||
/* ===== 同步记录数据(引用 RECORDS 索引) ===== */
|
||||
var SYNC_DATA = [
|
||||
/* ===== 同步记录数据 =====
|
||||
* 优先从 localStorage['nutri_sync_log_v1'] 读取真实同步日志;
|
||||
* 若为空(用户尚未触发过同步),回退到内置示例数据,保证页面可演示。
|
||||
*/
|
||||
function loadSyncLogsFromStore(){
|
||||
try {
|
||||
var raw = localStorage.getItem('nutri_sync_log_v1');
|
||||
if(!raw) return null;
|
||||
var data = JSON.parse(raw);
|
||||
return (data && data.logs) ? data.logs : null;
|
||||
} catch(e){ return null; }
|
||||
}
|
||||
|
||||
/* 兜底示例:与 RECORDS 内置数据配套,演示用 */
|
||||
var FALLBACK_SYNC_DATA = [
|
||||
{dish:'红烧肉', syncTime:'2026-04-22 16:00',operator:'张明', recordIdxs:[0,1]},
|
||||
{dish:'清炒时蔬',syncTime:'2026-04-22 16:05',operator:'李华', recordIdxs:[2,3]},
|
||||
{dish:'宫保鸡丁',syncTime:'2026-04-21 14:00',operator:'王芳', recordIdxs:[4,5]},
|
||||
{dish:'糖醋里脊',syncTime:'2026-04-20 16:30',operator:'张明', recordIdxs:[6,7]}
|
||||
];
|
||||
|
||||
/* 把 localStorage 中的日志项映射为页面使用的结构(搭配内置 RECORDS 做详情查看) */
|
||||
function mapLogsToSyncData(logs){
|
||||
return logs.map(function(log){
|
||||
/* 同步日志中的 recordNos 尝试匹配 RECORDS,匹配不到则按序号兜底 */
|
||||
var idxs = [];
|
||||
if(log.recordNos){
|
||||
log.recordNos.forEach(function(no){
|
||||
var idx = RECORDS.findIndex(function(r){ return r.no === no; });
|
||||
if(idx >= 0) idxs.push(idx);
|
||||
});
|
||||
}
|
||||
/* 若匹配不到任何 RECORDS(说明总库中餐品不在内置 8 条中),构造伪记录保证详情可看 */
|
||||
if(idxs.length === 0 && log.recordCount){
|
||||
for(var i=0; i<log.recordCount; i++){
|
||||
idxs.push(-1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
dish: log.dish,
|
||||
syncTime: log.syncTime,
|
||||
operator: log.operator,
|
||||
recordIdxs: idxs,
|
||||
avgCache: {
|
||||
nutri: log.nutriAvg,
|
||||
taste: log.tasteAvg,
|
||||
texture: log.textureAvg,
|
||||
color: log.colorAvg
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
var STORE_LOGS = loadSyncLogsFromStore();
|
||||
var SYNC_DATA = (STORE_LOGS && STORE_LOGS.length) ? mapLogsToSyncData(STORE_LOGS) : FALLBACK_SYNC_DATA;
|
||||
var USING_FALLBACK = !(STORE_LOGS && STORE_LOGS.length);
|
||||
|
||||
/* ===== Tag 工具函数 ===== */
|
||||
function nutriTag(val){
|
||||
if(val==='达标') return '<span class="tag tag-green">达标</span>';
|
||||
@@ -396,6 +445,12 @@ function renderTable(){
|
||||
+'</tr>';
|
||||
}).join('');
|
||||
document.getElementById('tableBody').innerHTML = html;
|
||||
/* 同步总数与分页信息 */
|
||||
var total = SYNC_DATA.length;
|
||||
var totalEl = document.getElementById('totalCount');
|
||||
if(totalEl) totalEl.textContent = total;
|
||||
var infoEl = document.getElementById('paginationInfo');
|
||||
if(infoEl) infoEl.textContent = '第 1-' + total + ' 条 / 共 ' + total + ' 条';
|
||||
}
|
||||
|
||||
/* ===== Dialog 弹窗 ===== */
|
||||
@@ -441,36 +496,48 @@ function nutriMajorityGrade(recs){
|
||||
|
||||
function renderDialogAvg(){
|
||||
var s = SYNC_DATA[currentSyncIdx];
|
||||
var recs = s.recordIdxs.map(function(i){ return RECORDS[i]; });
|
||||
var count = recs.length;
|
||||
var count = s.recordIdxs.length;
|
||||
|
||||
/* 营养均值 */
|
||||
var nds = recs.map(function(r){ return r.nutriData; });
|
||||
var avgNutri = {
|
||||
cal: calcAvg(nds,'cal'), protein: calcAvg(nds,'protein'), carb: calcAvg(nds,'carb'), fat: calcAvg(nds,'fat'),
|
||||
vc: calcAvg(nds,'vc'), mineral: calcAvg(nds,'mineral'), fiber: calcAvg(nds,'fiber'), sodium: calcAvg(nds,'sodium')
|
||||
};
|
||||
var avgNutri, avgTaste, avgTexture, avgColor, nutriGrade, tasteGrade, textureGrade, colorGrade;
|
||||
|
||||
/* 味道均值 */
|
||||
var tds = recs.map(function(r){ return r.tasteData; });
|
||||
var avgTaste = {
|
||||
sour: calcAvg(tds,'sour'), sweet: calcAvg(tds,'sweet'), bitter: calcAvg(tds,'bitter'), spicy: calcAvg(tds,'spicy'),
|
||||
salty: calcAvg(tds,'salty'), smell: calcAvg(tds,'smell'), flavor: calcAvg(tds,'flavor')
|
||||
};
|
||||
|
||||
/* 口感均值 */
|
||||
var xds = recs.map(function(r){ return r.textureData; });
|
||||
var avgTexture = {
|
||||
hardness: calcAvg(xds,'hardness'), sticky: calcAvg(xds,'sticky'), crispy: calcAvg(xds,'crispy'),
|
||||
smooth: calcAvg(xds,'smooth'), elastic: calcAvg(xds,'elastic'), score: calcAvg(xds,'score')
|
||||
};
|
||||
|
||||
/* 色泽均值 */
|
||||
var cds = recs.map(function(r){ return r.colorData; });
|
||||
var avgColor = {
|
||||
uniformity: calcAvg(cds,'uniformity'), gloss: calcAvg(cds,'gloss'), freshness: calcAvg(cds,'freshness'),
|
||||
appearance: calcAvg(cds,'appearance'), score: calcAvg(cds,'score')
|
||||
};
|
||||
/* 优先使用同步日志中的均值缓存(真实数据) */
|
||||
if(s.avgCache){
|
||||
avgNutri = s.avgCache.nutri || {};
|
||||
avgTaste = s.avgCache.taste || {};
|
||||
avgTexture = s.avgCache.texture || {};
|
||||
avgColor = s.avgCache.color || {};
|
||||
nutriGrade = avgNutri.grade || '达标';
|
||||
tasteGrade = avgTaste.grade || '良好';
|
||||
textureGrade = avgTexture.grade || '良好';
|
||||
colorGrade = avgColor.grade || '良好';
|
||||
} else {
|
||||
/* 兜底:回退到从 RECORDS 现场计算 */
|
||||
var recs = s.recordIdxs.map(function(i){ return RECORDS[i]; });
|
||||
var nds = recs.map(function(r){ return r.nutriData; });
|
||||
avgNutri = {
|
||||
cal: calcAvg(nds,'cal'), protein: calcAvg(nds,'protein'), carb: calcAvg(nds,'carb'), fat: calcAvg(nds,'fat'),
|
||||
vc: calcAvg(nds,'vc'), mineral: calcAvg(nds,'mineral'), fiber: calcAvg(nds,'fiber'), sodium: calcAvg(nds,'sodium')
|
||||
};
|
||||
var tds = recs.map(function(r){ return r.tasteData; });
|
||||
avgTaste = {
|
||||
sour: calcAvg(tds,'sour'), sweet: calcAvg(tds,'sweet'), bitter: calcAvg(tds,'bitter'), spicy: calcAvg(tds,'spicy'),
|
||||
salty: calcAvg(tds,'salty'), smell: calcAvg(tds,'smell'), flavor: calcAvg(tds,'flavor')
|
||||
};
|
||||
var xds = recs.map(function(r){ return r.textureData; });
|
||||
avgTexture = {
|
||||
hardness: calcAvg(xds,'hardness'), sticky: calcAvg(xds,'sticky'), crispy: calcAvg(xds,'crispy'),
|
||||
smooth: calcAvg(xds,'smooth'), elastic: calcAvg(xds,'elastic'), score: calcAvg(xds,'score')
|
||||
};
|
||||
var cds = recs.map(function(r){ return r.colorData; });
|
||||
avgColor = {
|
||||
uniformity: calcAvg(cds,'uniformity'), gloss: calcAvg(cds,'gloss'), freshness: calcAvg(cds,'freshness'),
|
||||
appearance: calcAvg(cds,'appearance'), score: calcAvg(cds,'score')
|
||||
};
|
||||
nutriGrade = nutriMajorityGrade(recs);
|
||||
tasteGrade = scoreToGrade(avgTaste.flavor);
|
||||
textureGrade = scoreToGrade(avgTexture.score);
|
||||
colorGrade = scoreToGrade(avgColor.score);
|
||||
}
|
||||
|
||||
var html = '<div class="dialog__avg-title">检验均值汇总 <span class="tag tag-blue">共 '+count+' 条记录</span></div>';
|
||||
|
||||
@@ -492,7 +559,7 @@ function renderDialogAvg(){
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">矿物质(mg)</div><div class="avg-cell__value">'+avgNutri.mineral+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">膳食纤维(g)</div><div class="avg-cell__value">'+avgNutri.fiber+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">钠(mg)</div><div class="avg-cell__value">'+avgNutri.sodium+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">检验等级</div><div class="avg-cell__value">'+nutriTag(nutriMajorityGrade(recs))+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">检验等级</div><div class="avg-cell__value">'+nutriTag(nutriGrade)+'</div></div>'
|
||||
+'</div></div>';
|
||||
|
||||
/* 味道面板 */
|
||||
@@ -504,7 +571,7 @@ function renderDialogAvg(){
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">咸度</div><div class="avg-cell__value">'+avgTaste.salty+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">气味评分</div><div class="avg-cell__value">'+avgTaste.smell+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合风味</div><div class="avg-cell__value">'+avgTaste.flavor+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(scoreToGrade(avgTaste.flavor))+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(tasteGrade)+'</div></div>'
|
||||
+'</div></div>';
|
||||
|
||||
/* 口感面板 */
|
||||
@@ -515,7 +582,7 @@ function renderDialogAvg(){
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">嫩滑度</div><div class="avg-cell__value">'+avgTexture.smooth+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">弹性</div><div class="avg-cell__value">'+avgTexture.elastic+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合口感</div><div class="avg-cell__value">'+avgTexture.score+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(scoreToGrade(avgTexture.score))+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(textureGrade)+'</div></div>'
|
||||
+'</div></div>';
|
||||
|
||||
/* 色泽面板 */
|
||||
@@ -525,7 +592,7 @@ function renderDialogAvg(){
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">新鲜度</div><div class="avg-cell__value">'+avgColor.freshness+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">卖相评分</div><div class="avg-cell__value">'+avgColor.appearance+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合色泽</div><div class="avg-cell__value">'+avgColor.score+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(scoreToGrade(avgColor.score))+'</div></div>'
|
||||
+'<div class="avg-cell"><div class="avg-cell__label">综合评级</div><div class="avg-cell__value">'+gradeTag(colorGrade)+'</div></div>'
|
||||
+'</div></div>';
|
||||
|
||||
document.getElementById('dialogAvg').innerHTML = html;
|
||||
@@ -542,19 +609,28 @@ function renderDialogLeft(){
|
||||
var s = SYNC_DATA[currentSyncIdx];
|
||||
var html = '';
|
||||
s.recordIdxs.forEach(function(ri, i){
|
||||
var r = RECORDS[ri];
|
||||
var r = (ri >= 0) ? RECORDS[ri] : null;
|
||||
var cls = (i === currentRecordIdx) ? 'dialog-left__item active' : 'dialog-left__item';
|
||||
html += '<div class="'+cls+'" onclick="selectRecord('+i+')">'
|
||||
+'<div class="dialog-left__no">'+r.no+'</div>'
|
||||
+'<div class="dialog-left__info">'
|
||||
+'<span>'+r.time+'</span>'
|
||||
+'<span>'+r.inspector+'</span>'
|
||||
+'</div>'
|
||||
+'<div class="dialog-left__info" style="margin-top:2px;">'
|
||||
+'<span>'+r.category+'</span>'
|
||||
+'<span>'+r.line+'</span>'
|
||||
+'</div>'
|
||||
+'</div>';
|
||||
if(r){
|
||||
html += '<div class="'+cls+'" onclick="selectRecord('+i+')">'
|
||||
+'<div class="dialog-left__no">'+r.no+'</div>'
|
||||
+'<div class="dialog-left__info">'
|
||||
+'<span>'+r.time+'</span>'
|
||||
+'<span>'+r.inspector+'</span>'
|
||||
+'</div>'
|
||||
+'<div class="dialog-left__info" style="margin-top:2px;">'
|
||||
+'<span>'+r.category+'</span>'
|
||||
+'<span>'+r.line+'</span>'
|
||||
+'</div>'
|
||||
+'</div>';
|
||||
} else {
|
||||
html += '<div class="'+cls+'" onclick="selectRecord('+i+')">'
|
||||
+'<div class="dialog-left__no">'+(s.recordNos ? s.recordNos[i] : ('记录#'+(i+1)))+'</div>'
|
||||
+'<div class="dialog-left__info" style="color:var(--text-placeholder);">'
|
||||
+'<span>历史记录(不在当前列表)</span>'
|
||||
+'</div>'
|
||||
+'</div>';
|
||||
}
|
||||
});
|
||||
document.getElementById('dialogLeft').innerHTML = html;
|
||||
}
|
||||
@@ -568,7 +644,48 @@ function selectRecord(idx){
|
||||
function renderDialogRight(){
|
||||
var s = SYNC_DATA[currentSyncIdx];
|
||||
var ri = s.recordIdxs[currentRecordIdx];
|
||||
var r = RECORDS[ri];
|
||||
var r = (ri >= 0) ? RECORDS[ri] : null;
|
||||
if(!r){
|
||||
/* 历史记录不在内置 RECORDS 中:展示同步时的均值作为只读详情 */
|
||||
var n2 = (s.avgCache && s.avgCache.nutri) || {};
|
||||
var t2 = (s.avgCache && s.avgCache.taste) || {};
|
||||
var x2 = (s.avgCache && s.avgCache.texture) || {};
|
||||
var c2 = (s.avgCache && s.avgCache.color) || {};
|
||||
var html = '';
|
||||
html += '<div class="section-title">基本信息</div>'
|
||||
+'<div class="detail-grid">'
|
||||
+'<div class="detail-item"><div class="detail-item__label">餐品</div><div class="detail-item__value">'+s.dish+'</div></div>'
|
||||
+'<div class="detail-item"><div class="detail-item__label">同步时间</div><div class="detail-item__value">'+s.syncTime+'</div></div>'
|
||||
+'<div class="detail-item"><div class="detail-item__label">操作人</div><div class="detail-item__value">'+s.operator+'</div></div>'
|
||||
+'<div class="detail-item"><div class="detail-item__label">记录数</div><div class="detail-item__value">'+s.recordIdxs.length+' 条</div></div>'
|
||||
+'</div>';
|
||||
html += '<div class="section-title" style="margin-top:20px;color:var(--text-secondary);">📝 该条记录为历史同步日志,详细单条数据未保留,以下展示本次同步的均值参考</div>';
|
||||
html += '<div class="inspect-tabs">'
|
||||
+'<div class="inspect-tab active" onclick="switchDialogTab(0)">营养</div>'
|
||||
+'<div class="inspect-tab" onclick="switchDialogTab(1)">味道</div>'
|
||||
+'<div class="inspect-tab" onclick="switchDialogTab(2)">口感</div>'
|
||||
+'<div class="inspect-tab" onclick="switchDialogTab(3)">色泽</div>'
|
||||
+'</div>';
|
||||
html += '<div class="inspect-panel active"><div class="metric-row">'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">热量</span><span class="metric-pair__value">'+(n2.cal||'-')+'</span></div>'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">蛋白质</span><span class="metric-pair__value">'+(n2.protein||'-')+'</span></div>'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">检验等级</span><span class="metric-pair__value">'+nutriTag(n2.grade||'达标')+'</span></div>'
|
||||
+'</div></div>';
|
||||
html += '<div class="inspect-panel"><div class="metric-row">'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合风味</span><span class="metric-pair__value">'+(t2.flavor||'-')+'</span></div>'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合评级</span><span class="metric-pair__value">'+gradeTag(t2.grade||'良好')+'</span></div>'
|
||||
+'</div></div>';
|
||||
html += '<div class="inspect-panel"><div class="metric-row">'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合口感</span><span class="metric-pair__value">'+(x2.score||'-')+'</span></div>'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合评级</span><span class="metric-pair__value">'+gradeTag(x2.grade||'良好')+'</span></div>'
|
||||
+'</div></div>';
|
||||
html += '<div class="inspect-panel"><div class="metric-row">'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合色泽</span><span class="metric-pair__value">'+(c2.score||'-')+'</span></div>'
|
||||
+'<div class="metric-pair"><span class="metric-pair__label">综合评级</span><span class="metric-pair__value">'+gradeTag(c2.grade||'良好')+'</span></div>'
|
||||
+'</div></div>';
|
||||
document.getElementById('dialogRight').innerHTML = html;
|
||||
return;
|
||||
}
|
||||
var n = r.nutriData, t = r.tasteData, x = r.textureData, c = r.colorData;
|
||||
|
||||
var html = '';
|
||||
@@ -660,7 +777,7 @@ function toggleSubgroup(header){ header.parentElement.classList.toggle('open');
|
||||
/* ===== 右侧抽屉组件 ===== */
|
||||
function showDetail(syncIdx){
|
||||
var s = SYNC_DATA[syncIdx];
|
||||
var recs = s.recordIdxs.map(function(i){ return RECORDS[i]; });
|
||||
var recs = s.recordIdxs.map(function(i){ return (i>=0) ? RECORDS[i] : null; });
|
||||
|
||||
var html = '<div class="drawer-section">'
|
||||
+'<div class="drawer-section__title">同步信息</div>'
|
||||
@@ -674,19 +791,26 @@ function showDetail(syncIdx){
|
||||
/* 关联检验记录列表 */
|
||||
html += '<div class="drawer-section">'
|
||||
+'<div class="drawer-section__title">关联检验记录</div>';
|
||||
recs.forEach(function(r){
|
||||
html += '<div class="drawer-fields" style="margin-bottom:12px;padding:10px;background:#FAFAFA;border-radius:var(--radius-sm);">'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验单号</div><div class="drawer-field__value">'+r.no+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验时间</div><div class="drawer-field__value">'+r.time+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验人员</div><div class="drawer-field__value">'+r.inspector+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">批次</div><div class="drawer-field__value">'+r.batch+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">场所</div><div class="drawer-field__value">'+r.category+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">餐线</div><div class="drawer-field__value">'+r.line+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">营养检验</div><div class="drawer-field__value">'+nutriTag(r.nutri)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">味道评级</div><div class="drawer-field__value">'+gradeTag(r.tasteData.grade)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">口感评级</div><div class="drawer-field__value">'+gradeTag(r.textureData.grade)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">色泽评级</div><div class="drawer-field__value">'+gradeTag(r.colorData.grade)+'</div></div>'
|
||||
+'</div>';
|
||||
recs.forEach(function(r, i){
|
||||
if(r){
|
||||
html += '<div class="drawer-fields" style="margin-bottom:12px;padding:10px;background:#FAFAFA;border-radius:var(--radius-sm);">'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验单号</div><div class="drawer-field__value">'+r.no+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验时间</div><div class="drawer-field__value">'+r.time+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验人员</div><div class="drawer-field__value">'+r.inspector+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">批次</div><div class="drawer-field__value">'+r.batch+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">场所</div><div class="drawer-field__value">'+r.category+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">餐线</div><div class="drawer-field__value">'+r.line+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">营养检验</div><div class="drawer-field__value">'+nutriTag(r.nutri)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">味道评级</div><div class="drawer-field__value">'+gradeTag(r.tasteData.grade)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">口感评级</div><div class="drawer-field__value">'+gradeTag(r.textureData.grade)+'</div></div>'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">色泽评级</div><div class="drawer-field__value">'+gradeTag(r.colorData.grade)+'</div></div>'
|
||||
+'</div>';
|
||||
} else {
|
||||
html += '<div class="drawer-fields" style="margin-bottom:12px;padding:10px;background:#FAFAFA;border-radius:var(--radius-sm);">'
|
||||
+'<div class="drawer-field"><div class="drawer-field__label">检验单号</div><div class="drawer-field__value">'+(s.recordNos ? s.recordNos[i] : '历史记录')+'</div></div>'
|
||||
+'<div class="drawer-field" style="grid-column:span 3;"><div class="drawer-field__label">备注</div><div class="drawer-field__value" style="color:var(--text-secondary);">该条记录来自历史同步日志,详细单条数据未保留</div></div>'
|
||||
+'</div>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
|
||||
@@ -721,9 +721,133 @@ function closeSyncDialog(){
|
||||
document.getElementById('syncDialogMask').classList.remove('show');
|
||||
}
|
||||
function confirmSync(){
|
||||
var recs = getSelectedRecords();
|
||||
if(recs.length === 0){ alert('请至少选择一条检验记录'); return; }
|
||||
|
||||
/* 按 dish 聚合,计算均值 */
|
||||
var dishMap = {};
|
||||
recs.forEach(function(r){
|
||||
if(!dishMap[r.dish]) dishMap[r.dish] = [];
|
||||
dishMap[r.dish].push(r);
|
||||
});
|
||||
|
||||
var syncLogs = [];
|
||||
var syncTime = formatNow();
|
||||
Object.keys(dishMap).forEach(function(dish){
|
||||
var list = dishMap[dish];
|
||||
var nds = list.map(function(r){ return r.nutriData; });
|
||||
var tds = list.map(function(r){ return r.tasteData; });
|
||||
var xds = list.map(function(r){ return r.textureData; });
|
||||
var cds = list.map(function(r){ return r.colorData; });
|
||||
var nutri = {
|
||||
cal: calcAvg(nds,'cal'), protein: calcAvg(nds,'protein'), carb: calcAvg(nds,'carb'), fat: calcAvg(nds,'fat'),
|
||||
vc: calcAvg(nds,'vc'), mineral: calcAvg(nds,'mineral'), fiber: calcAvg(nds,'fiber'), sodium: calcAvg(nds,'sodium'),
|
||||
grade: nutriMajorityGrade(list)
|
||||
};
|
||||
var taste = {
|
||||
sour: calcAvg(tds,'sour'), sweet: calcAvg(tds,'sweet'), bitter: calcAvg(tds,'bitter'), spicy: calcAvg(tds,'spicy'),
|
||||
salty: calcAvg(tds,'salty'), smell: calcAvg(tds,'smell'), flavor: calcAvg(tds,'flavor'),
|
||||
grade: scoreToGrade(calcAvg(tds,'flavor'))
|
||||
};
|
||||
var texture = {
|
||||
hardness: calcAvg(xds,'hardness'), sticky: calcAvg(xds,'sticky'), crispy: calcAvg(xds,'crispy'),
|
||||
smooth: calcAvg(xds,'smooth'), elastic: calcAvg(xds,'elastic'), score: calcAvg(xds,'score'),
|
||||
grade: scoreToGrade(calcAvg(xds,'score'))
|
||||
};
|
||||
var color = {
|
||||
uniformity: calcAvg(cds,'uniformity'), gloss: calcAvg(cds,'gloss'), freshness: calcAvg(cds,'freshness'),
|
||||
appearance: calcAvg(cds,'appearance'), score: calcAvg(cds,'score'),
|
||||
grade: scoreToGrade(calcAvg(cds,'score'))
|
||||
};
|
||||
|
||||
/* 写入餐品总库 dish_specs_v1 */
|
||||
var prevSpec = updateDishSpecNutri(dish, {nutri:taste, taste:texture, texture:color, color:nutri, nutriData:nutri, tasteData:taste, textureData:texture, colorData:color});
|
||||
|
||||
syncLogs.push({
|
||||
syncTime: syncTime,
|
||||
dish: dish,
|
||||
operator: '系统管理员',
|
||||
recordCount: list.length,
|
||||
recordNos: list.map(function(r){ return r.no; }),
|
||||
nutriAvg: nutri,
|
||||
tasteAvg: taste,
|
||||
textureAvg: texture,
|
||||
colorAvg: color,
|
||||
prev: prevSpec
|
||||
});
|
||||
});
|
||||
|
||||
/* 追加同步日志 nutri_sync_log_v1 */
|
||||
appendSyncLogs(syncLogs);
|
||||
|
||||
closeSyncDialog();
|
||||
exitSyncMode();
|
||||
showToast('同步成功');
|
||||
showToast('已同步 ' + syncLogs.length + ' 条餐品营养数据至餐品总库,刷新餐品总库可见');
|
||||
}
|
||||
|
||||
/* 当前时间格式化为 YYYY-MM-DD HH:mm */
|
||||
function formatNow(){
|
||||
var d = new Date();
|
||||
var pad = function(n){ return n<10 ? '0'+n : n; };
|
||||
return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes());
|
||||
}
|
||||
|
||||
/* ===== 跨页共享:餐品总库 specs 读取与写入 ===== */
|
||||
function loadDishSpecs(){
|
||||
try {
|
||||
var raw = localStorage.getItem('dish_specs_v1');
|
||||
if(!raw) return null;
|
||||
var data = JSON.parse(raw);
|
||||
return (data && data.dishes) ? data.dishes : null;
|
||||
} catch(e){ return null; }
|
||||
}
|
||||
function saveDishSpecs(dishes){
|
||||
try {
|
||||
localStorage.setItem('dish_specs_v1', JSON.stringify({version:1, dishes:dishes, updatedAt:Date.now()}));
|
||||
} catch(e){ console.warn('saveDishSpecs failed', e); }
|
||||
}
|
||||
/* 将均值写入指定餐品的 nutri 字段,返回修改前的 specs(用于日志前后对比) */
|
||||
function updateDishSpecNutri(dishName, avgData){
|
||||
var dishes = loadDishSpecs();
|
||||
if(!dishes){
|
||||
console.warn('dish_specs_v1 不存在或为空,本次同步未持久化到总库(请先在餐品总库初始化)');
|
||||
return null;
|
||||
}
|
||||
var target = null;
|
||||
for(var i=0; i<dishes.length; i++){
|
||||
if(dishes[i].name === dishName){ target = dishes[i]; break; }
|
||||
}
|
||||
if(!target){
|
||||
console.warn('在 dish_specs_v1 中未找到餐品 "'+dishName+'",已跳过');
|
||||
return null;
|
||||
}
|
||||
var prev = target.nutri ? JSON.parse(JSON.stringify(target.nutri)) : null;
|
||||
target.nutri = avgData;
|
||||
target.nutriUpdatedAt = formatNow();
|
||||
saveDishSpecs(dishes);
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* ===== 同步日志 nutri_sync_log_v1 ===== */
|
||||
function loadSyncLogs(){
|
||||
try {
|
||||
var raw = localStorage.getItem('nutri_sync_log_v1');
|
||||
if(!raw) return [];
|
||||
var data = JSON.parse(raw);
|
||||
return (data && data.logs) ? data.logs : [];
|
||||
} catch(e){ return []; }
|
||||
}
|
||||
function appendSyncLogs(logs){
|
||||
if(!logs || !logs.length) return;
|
||||
var all = loadSyncLogs();
|
||||
for(var i=0; i<logs.length; i++){
|
||||
all.unshift(logs[i]);
|
||||
}
|
||||
/* 限制最多保留 100 条,避免无限增长 */
|
||||
if(all.length > 100) all = all.slice(0, 100);
|
||||
try {
|
||||
localStorage.setItem('nutri_sync_log_v1', JSON.stringify({version:1, logs:all, updatedAt:Date.now()}));
|
||||
} catch(e){ console.warn('appendSyncLogs failed', e); }
|
||||
}
|
||||
function switchSyncTab(idx){
|
||||
var tabs = document.querySelectorAll('#syncDialogBody .avg-tab');
|
||||
|
||||
@@ -171,15 +171,15 @@ tr:hover td{background:#FAFAFA;}
|
||||
<div class="panel-card__content">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>产品分类</th><th>生产批次</th><th>投料重量(吨)</th><th>产出重量(吨)</th><th>原料成本(元)</th><th>加工成本(元)</th>
|
||||
<th>产品分类</th><th>生产批次</th><th>投料重量(kg)</th><th>产出重量(kg)</th><th>原料成本(元)</th><th>加工成本(元)</th>
|
||||
<th>产值(元)</th><th>单位成本(元/kg)</th><th>毛利率(%)</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td>净菜包</td><td>42</td><td>8.6</td><td>7.8</td><td>12,800.00</td><td>4,200.00</td><td>24,600.00</td><td>2.18</td><td>30.9</td></tr>
|
||||
<tr><td>餐品净菜包</td><td>38</td><td>10.2</td><td>9.4</td><td>18,500.00</td><td>5,800.00</td><td>35,200.00</td><td>2.59</td><td>31.0</td></tr>
|
||||
<tr><td>毛菜半成品</td><td>28</td><td>12.5</td><td>11.2</td><td>9,600.00</td><td>6,500.00</td><td>21,500.00</td><td>1.44</td><td>25.1</td></tr>
|
||||
<tr><td>净菜半成品</td><td>35</td><td>9.8</td><td>8.9</td><td>14,200.00</td><td>4,800.00</td><td>27,800.00</td><td>2.13</td><td>31.7</td></tr>
|
||||
<tr><td>定制加工</td><td>43</td><td>7.5</td><td>6.8</td><td>8,900.00</td><td>3,200.00</td><td>17,750.00</td><td>1.78</td><td>32.0</td></tr>
|
||||
<tr><td>净菜包</td><td>42</td><td>8,600</td><td>7,800</td><td>12,800.00</td><td>4,200.00</td><td>24,600.00</td><td>2.18</td><td>30.9</td></tr>
|
||||
<tr><td>餐品净菜包</td><td>38</td><td>10,200</td><td>9,400</td><td>18,500.00</td><td>5,800.00</td><td>35,200.00</td><td>2.59</td><td>31.0</td></tr>
|
||||
<tr><td>毛菜半成品</td><td>28</td><td>12,500</td><td>11,200</td><td>9,600.00</td><td>6,500.00</td><td>21,500.00</td><td>1.44</td><td>25.1</td></tr>
|
||||
<tr><td>净菜半成品</td><td>35</td><td>9,800</td><td>8,900</td><td>14,200.00</td><td>4,800.00</td><td>27,800.00</td><td>2.13</td><td>31.7</td></tr>
|
||||
<tr><td>定制加工</td><td>43</td><td>7,500</td><td>6,800</td><td>8,900.00</td><td>3,200.00</td><td>17,750.00</td><td>1.78</td><td>32.0</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -150,6 +150,10 @@ textarea.form-control{height:auto;padding:8px 12px;resize:vertical;min-height:60
|
||||
.line-builder-table td input[readonly]{background:#f8fafc;cursor:not-allowed;}
|
||||
.line-builder-table .btn-remove{background:none;color:var(--danger);border:none;cursor:pointer;font-size:var(--font-xs);padding:4px 8px;white-space:nowrap;}
|
||||
.line-builder-table .btn-remove:hover{color:#ff7875;}
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位/份型 */
|
||||
.line-builder-table .spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.line-builder-table .spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
.line-builder-table .spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -312,6 +316,60 @@ var cpSpecOptsExt={
|
||||
'蒜蓉菠菜':[{label:'标准份(400g)',grams:400},{label:'加量份(600g)',grams:600}],
|
||||
'炒胡萝卜':[{label:'标准份(300g)',grams:300},{label:'大份(500g)',grams:500}]
|
||||
};
|
||||
/* 跨页 specs 共享工具(任务 3.0):从 localStorage 加载餐品总库 specs */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
function getAllDishNames(){
|
||||
var dishes=loadDishSpecs();
|
||||
return dishes?dishes.map(function(d){return d.name;}):[];
|
||||
}
|
||||
/* 净菜包装单位字典(与 spec_unit 字典一致) */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function buildUnitOptions(selected){
|
||||
var html='';
|
||||
SPEC_UNIT_OPTIONS.forEach(function(u){html+='<option value="'+u+'"'+(u===selected?' selected':'')+'>'+u+'</option>';});
|
||||
return html;
|
||||
}
|
||||
/* 外售默认单位:箱 */
|
||||
var EXT_FALLBACK_DISHES={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:500,unit:'箱'},{portion:'加量份',baseGram:750,unit:'箱'}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:600,unit:'箱'},{portion:'大份',baseGram:900,unit:'箱'}],
|
||||
'蒜蓉菠菜':[{portion:'标准份',baseGram:400,unit:'箱'},{portion:'加量份',baseGram:600,unit:'箱'}],
|
||||
'炒胡萝卜':[{portion:'标准份',baseGram:300,unit:'箱'},{portion:'大份',baseGram:500,unit:'箱'}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return EXT_FALLBACK_DISHES[name]||[{portion:'标准份',baseGram:500,unit:'箱'}];
|
||||
}
|
||||
function getDishNamesWithFallback(){
|
||||
var names=getAllDishNames();
|
||||
if(names&&names.length>0)return names;
|
||||
return Object.keys(EXT_FALLBACK_DISHES);
|
||||
}
|
||||
function formatSpecLabelExt(line){
|
||||
var gram=line.baseGram||0;
|
||||
var curNcType=newExtOrderType||'nc';
|
||||
var unit=line.unit||(curNcType==='nc'?'箱':'份');
|
||||
var portion=line.portion||'';
|
||||
if(curNcType==='nc'){
|
||||
return gram>0?(gram+'g/'+unit):'—';
|
||||
}
|
||||
if(gram<=0)return '—';
|
||||
return gram+'g/'+(portion||unit);
|
||||
}
|
||||
var dishIngsExt={
|
||||
'宫保鸡丁':[{trace:'NC-PROC-JXR-20260415-01',name:'鸡胸肉',cleanType:'鸡胸丁',type:'主材',amount:'120g'},{trace:'NC-PROC-HUS-20260415-01',name:'花生',cleanType:'花生仁',type:'辅材',amount:'20g'},{trace:'NC-PROC-GLJ-20260415-01',name:'干辣椒',cleanType:'辣椒段',type:'调料',amount:'5g'}],
|
||||
'清蒸鱼':[{trace:'NC-PROC-CYU-20260415-01',name:'草鱼',cleanType:'鱼片',type:'主材',amount:'100g'},{trace:'NC-PROC-JPN-20260415-01',name:'姜片',cleanType:'姜片',type:'调料',amount:'适量'}],
|
||||
@@ -437,15 +495,38 @@ var newExtOrderType='nc';
|
||||
function updExtOrderType(v){newExtOrderType=v;renderExtLineBuilders();}
|
||||
var newExtLines=[];var extLineCnt=0;
|
||||
function resetExtForm(){newExtLines=[];extLineCnt=0;newExtOrderType=document.getElementById('extOrderNcType').value;document.getElementById('extCustomer').value='';document.getElementById('extContact').value='';document.getElementById('extPhone').value='';document.getElementById('extDeliverTime').value='';document.getElementById('extAddress').value='';document.getElementById('extRemark').value='';document.getElementById('extLinesContainer').innerHTML='';document.getElementById('extSummary').style.display='none';addExtLine();}
|
||||
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',specGram:0,specLabel:'',qty:0,weight:0});renderExtLineBuilders();}
|
||||
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:newExtOrderType==='nc'?'箱':'份',qty:0,weight:0});renderExtLineBuilders();}
|
||||
function removeExtLine(lid){if(newExtLines.length<=1){alert('至少保留一条');return;}newExtLines=newExtLines.filter(function(l){return l.id!==lid;});renderExtLineBuilders();}
|
||||
|
||||
function updELIngredient(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.ingredient=v;l.ncType='';renderExtLineBuilders();}
|
||||
function updELNcType(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.ncType=v;calcELW(l);refreshESummary();}
|
||||
function updELDish(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.dish=v;l.specGram=0;l.specLabel='';calcELW(l);refreshESummary();}
|
||||
function updELSpec(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;if(newExtOrderType==='nc'){l.specGram=parseInt(v||'0',10);l.specLabel=v?(v+'g/袋'):'';}else{l.specGram=parseInt(v||'0',10);var ops=cpSpecOptsExt[l.dish]||[];var f=ops.find(function(o){return o.grams===l.specGram;});l.specLabel=f?f.label:'';}calcELW(l);refreshESummary();}
|
||||
function updELDish(lid,v){
|
||||
var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;
|
||||
l.dish=v;
|
||||
var specs=getSpecsByDishWithFallback(v);
|
||||
if(specs.length>0){
|
||||
l.portion=specs[0].portion||'标准份';
|
||||
l.baseGram=specs[0].baseGram||0;
|
||||
l.unit=specs[0].unit||'箱';
|
||||
}else{
|
||||
l.portion='标准份';l.baseGram=0;l.unit='箱';
|
||||
}
|
||||
calcELW(l);refreshESummary();
|
||||
}
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位(nc) / 份型(cp) */
|
||||
function updELBaseGram(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(v||'0',10);calcELW(l);refreshESummary();}
|
||||
function updELPortion(lid,v){
|
||||
var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;
|
||||
l.portion=v;
|
||||
var specs=getSpecsByDishWithFallback(l.dish);
|
||||
var f=specs.find(function(s){return s.portion===v;});
|
||||
if(f&&f.baseGram){l.baseGram=f.baseGram;if(f.unit)l.unit=f.unit;}
|
||||
calcELW(l);refreshESummary();
|
||||
}
|
||||
function updELUnit(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.unit=v;refreshESummary();}
|
||||
function updELNcBaseGram(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(v||'0',10);l.unit='箱';calcELW(l);refreshESummary();}
|
||||
function updELQty(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.qty=parseInt(v.replace(/[^\d]/g,'')||'0',10);calcELW(l);refreshESummary();}
|
||||
function calcELW(l){if(l.specGram>0&&l.qty>0)l.weight=parseFloat((l.specGram*l.qty/1000).toFixed(2));else l.weight=0;}
|
||||
function calcELW(l){if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;}
|
||||
|
||||
function renderExtLineBuilders(){
|
||||
var h='';
|
||||
@@ -457,6 +538,7 @@ function renderExtLineBuilders(){
|
||||
h+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
|
||||
}
|
||||
h+='</tr></thead><tbody>';
|
||||
var dishNames=getDishNamesWithFallback();
|
||||
newExtLines.forEach(function(l){
|
||||
h+='<tr>';
|
||||
if(isNc){
|
||||
@@ -467,15 +549,24 @@ function renderExtLineBuilders(){
|
||||
h+='<td><select onchange="updELNcType(\''+l.id+'\',this.value)" '+(nts.length===0?'disabled':'')+'><option value="">'+(nts.length===0?'请先选食材':'请选择')+'</option>';
|
||||
nts.forEach(function(t){h+='<option value="'+t+'" '+(l.ncType===t?'selected':'')+'>'+t+'</option>';});
|
||||
h+='</select></td>';
|
||||
h+='<td><input inputmode="numeric" placeholder="克数" value="'+(l.specGram||'')+'" oninput="this.value=this.value.replace(/[^\d]/g,\x27\x27);updELSpec(\x27'+l.id+'\x27,this.value)"></td>';
|
||||
/* nc:左克重 + 右单位(默认箱) */
|
||||
h+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(l.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updELNcBaseGram(\''+l.id+'\',this.value)">'
|
||||
+'<select onchange="updELUnit(\''+l.id+'\',this.value)">'+buildUnitOptions(l.unit||'箱')+'</select>'
|
||||
+'</div></td>';
|
||||
}else{
|
||||
h+='<td><select onchange="updELDish(\''+l.id+'\',this.value)"><option value="">请选择</option>';
|
||||
Object.keys(cpSpecOptsExt).forEach(function(k){h+='<option value="'+k+'" '+(l.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
h+='</select></td>';
|
||||
var cps=cpSpecOptsExt[l.dish]||[];
|
||||
h+='<td><select onchange="updELSpec(\''+l.id+'\',this.value)" '+(cps.length===0?'disabled':'')+'><option value="">'+(cps.length===0?'请先选餐品':'请选择')+'</option>';
|
||||
cps.forEach(function(s){h+='<option value="'+s.grams+'" '+(l.specGram===s.grams?'selected':'')+'>'+s.label+'</option>';});
|
||||
dishNames.forEach(function(k){h+='<option value="'+k+'" '+(l.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
h+='</select></td>';
|
||||
var cpSpecs=getSpecsByDishWithFallback(l.dish);
|
||||
var portionOptions=['标准份','小份','大份','迷你份','加大份'];
|
||||
cpSpecs.forEach(function(s){if(s.portion&&portionOptions.indexOf(s.portion)===-1)portionOptions.push(s.portion);});
|
||||
/* cp:左克重 + 右份型 */
|
||||
h+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(l.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updELBaseGram(\''+l.id+'\',this.value)">'
|
||||
+'<select onchange="updELPortion(\''+l.id+'\',this.value)" style="width:90px;">';
|
||||
portionOptions.forEach(function(p){h+='<option value="'+p+'" '+(l.portion===p?'selected':'')+'>'+p+'</option>';});
|
||||
h+='</select></div></td>';
|
||||
}
|
||||
h+='<td><input inputmode="numeric" placeholder="份数" value="'+(l.qty||'')+'" oninput="this.value=this.value.replace(/[^\d]/g,\x27\x27);updELQty(\x27'+l.id+'\x27,this.value)"></td>';
|
||||
h+='<td><input value="'+(l.weight||'')+'" readonly></td>';
|
||||
@@ -495,10 +586,10 @@ function refreshESummary(){
|
||||
sd.style.display='block';var h='';
|
||||
if(newExtOrderType==='nc'){
|
||||
sh.innerHTML='<tr><th>净菜类型</th><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
|
||||
vl.forEach(function(l){h+='<tr><td>净菜包</td><td>'+(l.ingredient||'')+'</td><td>'+(l.ncType||'')+'</td><td>'+l.specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
|
||||
vl.forEach(function(l){h+='<tr><td>净菜包</td><td>'+(l.ingredient||'')+'</td><td>'+(l.ncType||'')+'</td><td>'+formatSpecLabelExt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
|
||||
}else{
|
||||
sh.innerHTML='<tr><th>净菜类型</th><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
|
||||
vl.forEach(function(l){h+='<tr><td>餐品净菜包</td><td>'+(l.dish||'')+'</td><td>'+l.specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
|
||||
vl.forEach(function(l){h+='<tr><td>餐品净菜包</td><td>'+(l.dish||'')+'</td><td>'+formatSpecLabelExt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
|
||||
}
|
||||
tb.innerHTML=h;
|
||||
}
|
||||
@@ -506,14 +597,15 @@ function refreshESummary(){
|
||||
function submitExtOrder(){
|
||||
var cust=document.getElementById('extCustomer').value.trim();if(!cust){alert('请输入客户名称');return;}
|
||||
var dt=document.getElementById('extDeliverTime').value;if(!dt){alert('请选择预定配送时间');return;}
|
||||
var vl=newExtLines.filter(function(l){if(newExtOrderType==='nc')return l.ingredient&&l.ncType&&l.specGram>0&&l.qty>0;return l.dish&&l.specGram>0&&l.qty>0;});
|
||||
var vl=newExtLines.filter(function(l){if(newExtOrderType==='nc')return l.ingredient&&l.ncType&&l.baseGram>0&&l.qty>0;return l.dish&&l.baseGram>0&&l.qty>0;});
|
||||
if(vl.length===0){alert('请至少完善一条菜品明细');return;}
|
||||
var no='EXT-'+String(extOrders.length+1).padStart(3,'0');
|
||||
var newLines=vl.map(function(l,i){
|
||||
var specLbl=formatSpecLabelExt(l);
|
||||
if(newExtOrderType==='nc'){
|
||||
return {id:'L'+(Date.now()+i),ingredient:l.ingredient,ncName:l.ncType,spec:l.specLabel,qty:l.qty,weight:l.weight,status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i),ingredient:l.ingredient,ncName:l.ncType,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,status:'备货中'};
|
||||
}
|
||||
return {id:'L'+(Date.now()+i),foodName:l.dish,spec:l.specLabel,qty:l.qty,weight:l.weight,status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i),foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,status:'备货中'};
|
||||
});
|
||||
extOrders.unshift({orderNo:no,customer:cust,orderTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:dt,logistics:'—',
|
||||
ncType:newExtOrderType,lines:newLines});
|
||||
|
||||
@@ -150,6 +150,10 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
|
||||
.inline-line-table select:focus,.inline-line-table input:focus{border-color:var(--primary);}
|
||||
.inline-line-table input[readonly]{background:#f8fafc;cursor:not-allowed;}
|
||||
.inline-line-table .td-actions{text-align:center;width:60px;white-space:nowrap;}
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位/份型 */
|
||||
.inline-line-table .spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.inline-line-table .spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
.inline-line-table .spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -334,6 +338,61 @@ var cpSpecOptions={
|
||||
'蒜蓉菠菜':[{label:'标准份(400g)',grams:400},{label:'加量份(600g)',grams:600}],
|
||||
'炒胡萝卜':[{label:'标准份(300g)',grams:300},{label:'大份(500g)',grams:500}]
|
||||
};
|
||||
/* 跨页 specs 共享工具(任务 3.0):从 localStorage 加载餐品总库 specs */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
function getAllDishNames(){
|
||||
var dishes=loadDishSpecs();
|
||||
return dishes?dishes.map(function(d){return d.name;}):[];
|
||||
}
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function buildUnitOptions(selected){
|
||||
var html='';
|
||||
SPEC_UNIT_OPTIONS.forEach(function(u){html+='<option value="'+u+'"'+(u===selected?' selected':'')+'>'+u+'</option>';});
|
||||
return html;
|
||||
}
|
||||
/* 内供默认单位:盘 */
|
||||
var INT_FALLBACK_DISHES={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:500,unit:'盘'},{portion:'加量份',baseGram:750,unit:'盘'}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:600,unit:'盘'},{portion:'大份',baseGram:900,unit:'盘'}],
|
||||
'蒜蓉菠菜':[{portion:'标准份',baseGram:400,unit:'盘'},{portion:'加量份',baseGram:600,unit:'盘'}],
|
||||
'炒胡萝卜':[{portion:'标准份',baseGram:300,unit:'盘'},{portion:'大份',baseGram:500,unit:'盘'}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return INT_FALLBACK_DISHES[name]||[{portion:'标准份',baseGram:500,unit:'盘'}];
|
||||
}
|
||||
function getDishNamesWithFallback(){
|
||||
var names=getAllDishNames();
|
||||
if(names&&names.length>0)return names;
|
||||
return Object.keys(INT_FALLBACK_DISHES);
|
||||
}
|
||||
function formatSpecLabelInt(line){
|
||||
var gram=line.baseGram||0;
|
||||
/* 当前订单的净菜类型从 DOM 读取 */
|
||||
var sel=document.getElementById('claimNcType');
|
||||
var curNcType=sel?sel.value:'nc';
|
||||
var unit=line.unit||(curNcType==='nc'?'盘':'份');
|
||||
var portion=line.portion||'';
|
||||
if(curNcType==='nc'){
|
||||
return gram>0?(gram+'g/'+unit):'—';
|
||||
}
|
||||
if(gram<=0)return '—';
|
||||
return gram+'g/'+(portion||unit);
|
||||
}
|
||||
var dishIngredients={
|
||||
'宫保鸡丁':[{trace:'NC-PROC-JXR-20260415-01',name:'鸡胸肉',cleanType:'鸡胸丁',type:'主材',amount:'120g'},{trace:'NC-PROC-HUS-20260415-01',name:'花生',cleanType:'花生仁',type:'辅材',amount:'20g'},{trace:'NC-PROC-GLJ-20260415-01',name:'干辣椒',cleanType:'辣椒段',type:'调料',amount:'5g'}],
|
||||
'清蒸鱼':[{trace:'NC-PROC-CYU-20260415-01',name:'草鱼',cleanType:'鱼片',type:'主材',amount:'100g'},{trace:'NC-PROC-JPN-20260415-01',name:'姜片',cleanType:'姜片',type:'调料',amount:'适量'}],
|
||||
@@ -506,7 +565,8 @@ function resetClaimForm(){
|
||||
|
||||
function addClaimLine(){
|
||||
claimLineIdCnt++;
|
||||
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',specGram:0,specLabel:'',qty:0,weight:0});
|
||||
var orderNcType=document.getElementById('claimNcType').value||'nc';
|
||||
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:orderNcType==='nc'?'盘':'份',qty:0,weight:0});
|
||||
renderClaimLineBuilders();
|
||||
}
|
||||
|
||||
@@ -518,16 +578,33 @@ function removeClaimLine(lid){
|
||||
|
||||
function updateCLIngredient(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.ingredient=val;l.ncType='';renderClaimLineBuilders();}
|
||||
function updateCLNcType(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.ncType=val;calcCLWeight(l);refreshCLSummary();}
|
||||
function updateCLDish(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.dish=val;l.specGram=0;l.specLabel='';calcCLWeight(l);refreshCLSummary();}
|
||||
function updateCLSpec(lid,val){
|
||||
function updateCLDish(lid,val){
|
||||
var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;
|
||||
var orderNcType=document.getElementById('claimNcType').value;
|
||||
if(orderNcType==='nc'){l.specGram=parseInt(val||'0',10);l.specLabel=val?(val+'g/袋'):'';}
|
||||
else{l.specGram=parseInt(val||'0',10);var opts=cpSpecOptions[l.dish]||[];var f=opts.find(function(o){return o.grams===l.specGram;});l.specLabel=f?f.label:'';}
|
||||
l.dish=val;
|
||||
var specs=getSpecsByDishWithFallback(val);
|
||||
if(specs.length>0){
|
||||
l.portion=specs[0].portion||'标准份';
|
||||
l.baseGram=specs[0].baseGram||0;
|
||||
l.unit=specs[0].unit||'盘';
|
||||
}else{
|
||||
l.portion='标准份';l.baseGram=0;l.unit='盘';
|
||||
}
|
||||
calcCLWeight(l);refreshCLSummary();
|
||||
}
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位(nc) / 份型(cp) */
|
||||
function updateCLBaseGram(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(val||'0',10);calcCLWeight(l);refreshCLSummary();}
|
||||
function updateCLPortion(lid,val){
|
||||
var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;
|
||||
l.portion=val;
|
||||
var specs=getSpecsByDishWithFallback(l.dish);
|
||||
var f=specs.find(function(s){return s.portion===val;});
|
||||
if(f&&f.baseGram){l.baseGram=f.baseGram;if(f.unit)l.unit=f.unit;}
|
||||
calcCLWeight(l);refreshCLSummary();
|
||||
}
|
||||
function updateCLUnit(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.unit=val;refreshCLSummary();}
|
||||
function updateCLNcBaseGram(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(val||'0',10);l.unit='盘';calcCLWeight(l);refreshCLSummary();}
|
||||
function updateCLQty(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.qty=parseInt(val.replace(/[^\d]/g,'')||'0',10);calcCLWeight(l);refreshCLSummary();}
|
||||
function calcCLWeight(l){if(l.specGram>0&&l.qty>0)l.weight=parseFloat((l.specGram*l.qty/1000).toFixed(2));else l.weight=0;}
|
||||
function calcCLWeight(l){if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;}
|
||||
|
||||
function renderClaimLineBuilders(){
|
||||
var html='';
|
||||
@@ -540,6 +617,7 @@ function renderClaimLineBuilders(){
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>数量(kg)</th><th>操作</th>';
|
||||
}
|
||||
html+='</tr></thead><tbody>';
|
||||
var dishNames=getDishNamesWithFallback();
|
||||
newClaimLines.forEach(function(line){
|
||||
html+='<tr>';
|
||||
if(isNc){
|
||||
@@ -552,18 +630,25 @@ function renderClaimLineBuilders(){
|
||||
html+='<td><select onchange="updateCLNcType(\''+line.id+'\',this.value)" '+(nts.length===0?'disabled':'')+'><option value="">'+(nts.length===0?'请先选食材':'请选择')+'</option>';
|
||||
nts.forEach(function(t){html+='<option value="'+t+'" '+(line.ncType===t?'selected':'')+'>'+t+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* 包装规格 */
|
||||
html+='<td><input inputmode="numeric" placeholder="克数" value="'+(line.specGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateCLSpec(\''+line.id+'\',this.value)"></td>';
|
||||
/* nc:左克重 + 右单位(默认盘) */
|
||||
html+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(line.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateCLNcBaseGram(\''+line.id+'\',this.value)">'
|
||||
+'<select onchange="updateCLUnit(\''+line.id+'\',this.value)">'+buildUnitOptions(line.unit||'盘')+'</select>'
|
||||
+'</div></td>';
|
||||
}else{
|
||||
/* 餐品名称下拉 */
|
||||
html+='<td><select onchange="updateCLDish(\''+line.id+'\',this.value)"><option value="">请选择</option>';
|
||||
Object.keys(cpSpecOptions).forEach(function(k){html+='<option value="'+k+'" '+(line.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* 包装规格 */
|
||||
var cps=cpSpecOptions[line.dish]||[];
|
||||
html+='<td><select onchange="updateCLSpec(\''+line.id+'\',this.value)" '+(cps.length===0?'disabled':'')+'><option value="">'+(cps.length===0?'请先选餐品':'请选择')+'</option>';
|
||||
cps.forEach(function(s){html+='<option value="'+s.grams+'" '+(line.specGram===s.grams?'selected':'')+'>'+s.label+'</option>';});
|
||||
dishNames.forEach(function(k){html+='<option value="'+k+'" '+(line.dish===k?'selected':'')+'>'+k+'</option>';});
|
||||
html+='</select></td>';
|
||||
/* cp:左克重 + 右份型 */
|
||||
var cpSpecs=getSpecsByDishWithFallback(line.dish);
|
||||
var portionOptions=['标准份','小份','大份','迷你份','加大份'];
|
||||
cpSpecs.forEach(function(s){if(s.portion&&portionOptions.indexOf(s.portion)===-1)portionOptions.push(s.portion);});
|
||||
html+='<td><div class="spec-input-group">'
|
||||
+'<input inputmode="numeric" placeholder="克数" value="'+(line.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateCLBaseGram(\''+line.id+'\',this.value)">'
|
||||
+'<select onchange="updateCLPortion(\''+line.id+'\',this.value)" style="width:90px;">';
|
||||
portionOptions.forEach(function(p){html+='<option value="'+p+'" '+(line.portion===p?'selected':'')+'>'+p+'</option>';});
|
||||
html+='</select></div></td>';
|
||||
}
|
||||
/* 份数和数量(nc/cp 共用) */
|
||||
html+='<td><input inputmode="numeric" placeholder="份数" value="'+(line.qty||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateCLQty(\''+line.id+'\',this.value)"></td>';
|
||||
@@ -600,7 +685,7 @@ function refreshCLSummary(){
|
||||
}else{
|
||||
h+='<tr><td colspan="2">'+l.dish+'</td>';
|
||||
}
|
||||
h+='<td>'+l.specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
h+='<td>'+formatSpecLabelInt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
|
||||
});
|
||||
tb.innerHTML=h;
|
||||
}
|
||||
@@ -611,18 +696,19 @@ function submitClaim(){
|
||||
var orderNcType=document.getElementById('claimNcType').value;
|
||||
var vl;
|
||||
if(orderNcType==='nc'){
|
||||
vl=newClaimLines.filter(function(l){return l.ingredient&&l.ncType&&l.specGram>0&&l.qty>0;});
|
||||
vl=newClaimLines.filter(function(l){return l.ingredient&&l.ncType&&l.baseGram>0&&l.qty>0;});
|
||||
}else{
|
||||
vl=newClaimLines.filter(function(l){return l.dish&&l.specGram>0&&l.qty>0;});
|
||||
vl=newClaimLines.filter(function(l){return l.dish&&l.baseGram>0&&l.qty>0;});
|
||||
}
|
||||
if(vl.length===0){alert('请至少完善一条菜品明细');return;}
|
||||
var no='INT-'+String(claims.length+1).padStart(3,'0');
|
||||
claims.unshift({claimNo:no,canteen:canteen,ncType:orderNcType,claimTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:document.getElementById('claimDeliveryTime').value||'—',deliverPerson:document.getElementById('claimDeliverPerson').value||'—',
|
||||
lines:vl.map(function(l,i){
|
||||
var specLbl=formatSpecLabelInt(l);
|
||||
if(orderNcType==='nc'){
|
||||
return {id:'L'+(Date.now()+i), ingredient:l.ingredient, ncName:l.ncType, spec:l.specLabel, qty:l.qty, weight:l.weight, status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i), ingredient:l.ingredient, ncName:l.ncType, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, status:'备货中'};
|
||||
}else{
|
||||
return {id:'L'+(Date.now()+i), foodName:l.dish, spec:l.specLabel, qty:l.qty, weight:l.weight, status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i), foodName:l.dish, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, status:'备货中'};
|
||||
}
|
||||
})});
|
||||
closeModal('modal-claim');renderClaimTable();
|
||||
|
||||
@@ -346,8 +346,8 @@ function openModal(){document.getElementById('modalOverlay').style.display='flex
|
||||
function closeModal(){document.getElementById('modalOverlay').style.display='none';}
|
||||
document.getElementById('modalOverlay').addEventListener('click',function(e){if(e.target===this)closeModal();});
|
||||
|
||||
/* 食材 → 净菜类型联动映射 */
|
||||
var FOOD_VEG_MAP={
|
||||
/* 食材 → 净菜类型映射(与食材档案 vegTypes 字段对齐,仅维护名称,切配规格在食材档案中维护) */
|
||||
var FOOD_VEG_TYPES={
|
||||
'土豆':['土豆丝','土豆片','土豆泥','土豆块'],
|
||||
'四季豆':['段(5cm)','整根'],
|
||||
'白菜':['整棵','切段','切丝'],
|
||||
@@ -361,9 +361,11 @@ function onFoodChange(){
|
||||
vegSelect.disabled=true;
|
||||
return;
|
||||
}
|
||||
var types=FOOD_VEG_MAP[food]||[];
|
||||
var types=FOOD_VEG_TYPES[food]||[];
|
||||
var html='<option value="">请选择净菜类型</option>';
|
||||
types.forEach(function(t){html+='<option>'+t+'</option>';});
|
||||
types.forEach(function(name){
|
||||
html+='<option>'+name+'</option>';
|
||||
});
|
||||
vegSelect.innerHTML=html;
|
||||
vegSelect.disabled=false;
|
||||
}
|
||||
@@ -372,7 +374,7 @@ var _stockRowData=null;
|
||||
function confirmGenStock(el){
|
||||
var tr=el.closest('tr');
|
||||
var tds=tr.querySelectorAll('td');
|
||||
/* 列序:0加工批次号 1溯源码 2食材名称 3净菜类型 4加工步骤 5净菜需求量 6毛菜重量 7净菜产出量 */
|
||||
/* 列序:0加工批次号 1溯源码 2食材名称 3净菜类型 4加工步骤 5净菜需求量 6毛菜重量 7净菜产出量 */
|
||||
_stockRowData={
|
||||
batch:tds[0].textContent,
|
||||
trace:tds[1].textContent,
|
||||
|
||||
@@ -105,6 +105,10 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
.form-item input,.form-item select,.form-item textarea{height:36px;padding:0 12px;border:1px solid #e8e8e8;border-radius:4px;font-size:13px;outline:none;}
|
||||
.form-item input:focus,.form-item select:focus,.form-item textarea:focus{border-color:#1890FF;}
|
||||
.form-item textarea{height:80px;padding:8px 12px;resize:vertical;}
|
||||
/* 规格输入组:克重 input + 单位 select 连在一起 */
|
||||
.spec-input-group{display:flex;align-items:stretch;height:36px;}
|
||||
.spec-input-group input{flex:1;border-radius:4px 0 0 4px;border-right:none;}
|
||||
.spec-input-group select{width:96px;border-radius:0 4px 4px 0;}
|
||||
.action-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;}
|
||||
/* 操作列 */
|
||||
tbody td.td-actions{white-space:nowrap;}
|
||||
@@ -219,19 +223,19 @@ tbody td.td-actions{white-space:nowrap;}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>NC-IN-001</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-001</div></td><td>NC-PROC-BOCA-20260415-01</td><td>土豆</td><td>土豆丝</td><td>袋装/500g</td><td>86</td><td>43.0</td><td>31.0</td><td>2026-04-15 09:15</td><td>2026-04-17 09:15</td><td><span class="tag tag-blue">净菜柜</span></td><td>8号柜-A01格口</td><td>4.1</td><td>李入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-001')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
<td>NC-IN-001</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-001</div></td><td>NC-PROC-BOCA-20260415-01</td><td>土豆</td><td>土豆丝</td><td>500g/袋</td><td>86</td><td>43.0</td><td>31.0</td><td>2026-04-15 09:15</td><td>2026-04-17 09:15</td><td><span class="tag tag-blue">净菜柜</span></td><td>8号柜-A01格口</td><td>4.1</td><td>李入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-001')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NC-IN-002</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-002</div></td><td>NC-PROC-HLUB-20260415-01</td><td>土豆</td><td>土豆片</td><td>袋装/500g</td><td>120</td><td>60.0</td><td>47.5</td><td>2026-04-15 09:45</td><td>2026-04-18 09:45</td><td><span class="tag tag-blue">净菜柜</span></td><td>8号柜-B03格口</td><td>4.2</td><td>王入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-002')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
<td>NC-IN-002</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-002</div></td><td>NC-PROC-HLUB-20260415-01</td><td>土豆</td><td>土豆片</td><td>500g/袋</td><td>120</td><td>60.0</td><td>47.5</td><td>2026-04-15 09:45</td><td>2026-04-18 09:45</td><td><span class="tag tag-blue">净菜柜</span></td><td>8号柜-B03格口</td><td>4.2</td><td>王入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-002')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NC-IN-003</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td>NC-PROC-XGUO-20260415-01</td><td>土豆</td><td>土豆泥</td><td>盒装/300g</td><td>48</td><td>14.4</td><td>9.0</td><td>2026-04-15 11:00</td><td>2026-04-17 11:00</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-03</td><td>4.0</td><td>李入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-003')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
<td>NC-IN-003</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td>NC-PROC-XGUO-20260415-01</td><td>土豆</td><td>土豆泥</td><td>300g/袋</td><td>48</td><td>14.4</td><td>9.0</td><td>2026-04-15 11:00</td><td>2026-04-17 11:00</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-03</td><td>4.0</td><td>李入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-003')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NC-IN-004</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td>NC-PROC-BCAI-20260415-01</td><td>白菜</td><td>整棵</td><td>散装/整棵</td><td>70</td><td>35.0</td><td>22.5</td><td>2026-04-15 11:30</td><td>2026-04-18 11:30</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-04</td><td>3.8</td><td>张入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-004')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
<td>NC-IN-004</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td>NC-PROC-BCAI-20260415-01</td><td>白菜</td><td>整棵</td><td>散装</td><td>70</td><td>35.0</td><td>22.5</td><td>2026-04-15 11:30</td><td>2026-04-18 11:30</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-04</td><td>3.8</td><td>张入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-004')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NC-IN-005</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-003</div></td><td>NC-PROC-ZLRJ-20260415-01</td><td>猪里脊</td><td>丁(1.5cm)</td><td>袋装/400g</td><td>76</td><td>30.4</td><td>18.8</td><td>2026-04-15 12:00</td><td>2026-04-16 12:00</td><td><span class="tag tag-gray">仓库</span></td><td>B仓-肉类区-01</td><td>-2.0</td><td>王入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-005')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
<td>NC-IN-005</td><td><span class="tag tag-blue">设备自动</span></td><td><div>净菜入库秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">NC-IN-SCALE-003</div></td><td>NC-PROC-ZLRJ-20260415-01</td><td>猪里脊</td><td>丁(1.5cm)</td><td>400g/袋</td><td>76</td><td>30.4</td><td>18.8</td><td>2026-04-15 12:00</td><td>2026-04-16 12:00</td><td><span class="tag tag-gray">仓库</span></td><td>B仓-肉类区-01</td><td>-2.0</td><td>王入库</td><td class="td-actions"><span class="link" onclick="openVideoModal()">查看视频</span> <span class="link" onclick="openOutModal('NC-IN-005')">出库明细</span> <span class="action-more"><span class="more-btn">更多 ▾</span><div class="dropdown"><a href="javascript:;" onclick="showDetail(this)">详情</a><a href="javascript:;" onclick="openModal()">编辑</a><a href="javascript:;" class="danger" onclick="if(confirm('确认删除该记录?')){}">删除</a></div></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -259,7 +263,12 @@ tbody td.td-actions{white-space:nowrap;}
|
||||
<div class="form-item" id="fi-batch" style="display:none;"><label>关联加工批次</label><input id="f-batch" type="text" readonly style="background:#f5f5f5;"></div>
|
||||
<div class="form-item"><label>食材名称<span class="required">*</span></label><select id="f-food" onchange="updateCleanTypeOptions()"><option>请选择</option></select></div>
|
||||
<div class="form-item"><label>净菜类型<span class="required">*</span></label><select id="f-cleanType" disabled><option>请先选择食材</option></select></div>
|
||||
<div class="form-item"><label>入库规格<span class="required">*</span></label><select><option>请选择</option><option>袋装/500g</option><option>袋装/400g</option><option>盒装/300g</option><option>散装/整棵</option><option>其他</option></select></div>
|
||||
<div class="form-item"><label>入库规格<span class="required">*</span></label>
|
||||
<div class="spec-input-group">
|
||||
<input id="f-spec-gram" type="number" min="0" step="0.01" placeholder="请输入克重">
|
||||
<select id="f-spec-unit"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item"><label>入库数量<span class="required">*</span></label><input type="number" placeholder="请输入入库数量"></div>
|
||||
<div class="form-item"><label>入库重量(kg)<span class="required">*</span></label><input id="f-weight" type="number" step="0.1" placeholder="请输入入库重量"></div>
|
||||
<div class="form-item"><label>操作时间<span class="required">*</span></label><input type="datetime-local"></div>
|
||||
@@ -309,38 +318,55 @@ var cleanAreaModalOptions={
|
||||
'净菜柜':['8号柜-A01格口','8号柜-B03格口','9号柜-A02格口','10号柜-C01格口'],
|
||||
'仓库':['A仓-蔬菜区-01','A仓-蔬菜区-02','A仓-蔬菜区-03','A仓-蔬菜区-04','B仓-肉类区-01','B仓-肉类区-02','B仓-肉类区-03','C仓-水产区-01','C仓-水产区-02']
|
||||
};
|
||||
var cleanFoodTypeOptions={
|
||||
'土豆':['土豆丝','土豆块','土豆泥'],
|
||||
'白菜':['整颗','白菜片','白菜段'],
|
||||
'猪里脊':['丁(1.5cm)','丝(0.8cm)','片(2mm)'],
|
||||
'胡萝卜':['胡萝卜丝','胡萝卜块','胡萝卜片'],
|
||||
'西兰花':['小朵','西兰花段','西兰花梗']
|
||||
/* 包装单位字典(从 din-dict-config.html?code=spec_unit 同步,所有业务共用) */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function renderUnitOptions(selectEl){
|
||||
if(!selectEl)return;
|
||||
selectEl.innerHTML='<option>请选择</option>'+SPEC_UNIT_OPTIONS.map(function(u){return '<option>'+u+'</option>';}).join('');
|
||||
}
|
||||
/* 食材 → 净菜类型映射(与食材档案 vegTypes 字段对齐,仅维护名称,切配规格在食材档案中维护) */
|
||||
var FOOD_VEG_TYPES={
|
||||
'土豆':['土豆丝','土豆片','土豆泥','土豆块'],
|
||||
'四季豆':['段(5cm)','整根'],
|
||||
'白菜':['整棵','切段','切丝'],
|
||||
'猪里脊':['丁(1.5cm)','条(4cm)','肉片','肉末']
|
||||
};
|
||||
function renderSelectOptions(selectEl,placeholder,options){
|
||||
if(!selectEl)return;
|
||||
selectEl.innerHTML='<option>'+placeholder+'</option>'+options.map(function(item){return '<option>'+item+'</option>';}).join('');
|
||||
}
|
||||
function initCleanFoodOptions(){
|
||||
renderSelectOptions(document.getElementById('f-food'),'请选择',Object.keys(cleanFoodTypeOptions));
|
||||
renderSelectOptions(document.getElementById('f-food'),'请选择',Object.keys(FOOD_VEG_TYPES));
|
||||
renderUnitOptions(document.getElementById('f-spec-unit'));
|
||||
}
|
||||
function updateCleanTypeOptions(selectedType){
|
||||
var foodSelect=document.getElementById('f-food');
|
||||
var cleanTypeSelect=document.getElementById('f-cleanType');
|
||||
var foodName=foodSelect.value;
|
||||
var options=cleanFoodTypeOptions[foodName]||[];
|
||||
var types=FOOD_VEG_TYPES[foodName]||[];
|
||||
cleanTypeSelect.disabled=!foodName||foodName==='请选择';
|
||||
if(cleanTypeSelect.disabled){
|
||||
cleanTypeSelect.innerHTML='<option>请先选择食材</option>';
|
||||
return;
|
||||
}
|
||||
renderSelectOptions(cleanTypeSelect,'请选择',options);
|
||||
if(selectedType&&options.indexOf(selectedType)>-1){
|
||||
cleanTypeSelect.value=selectedType;
|
||||
var html='<option>请选择</option>';
|
||||
types.forEach(function(name){
|
||||
html+='<option>'+name+'</option>';
|
||||
});
|
||||
cleanTypeSelect.innerHTML=html;
|
||||
if(selectedType){
|
||||
for(var i=0;i<types.length;i++){
|
||||
if(types[i]===selectedType){
|
||||
cleanTypeSelect.selectedIndex=i+1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findFoodByCleanType(cleanType){
|
||||
for(var foodName in cleanFoodTypeOptions){
|
||||
if(cleanFoodTypeOptions[foodName].indexOf(cleanType)>-1){
|
||||
for(var foodName in FOOD_VEG_TYPES){
|
||||
var types=FOOD_VEG_TYPES[foodName];
|
||||
if(types.indexOf(cleanType)>=0){
|
||||
return foodName;
|
||||
}
|
||||
}
|
||||
@@ -423,7 +449,7 @@ updateCleanAreaFilterOptions();
|
||||
}else{
|
||||
updateCleanTypeOptions();
|
||||
}
|
||||
if(weight) document.getElementById('f-weight').value=weight;
|
||||
if(weight)document.getElementById('f-weight').value=weight;
|
||||
/* 自动打开弹窗 */
|
||||
openModal();
|
||||
})();
|
||||
|
||||
@@ -112,6 +112,10 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
.form-item input,.form-item select,.form-item textarea{height:36px;padding:0 12px;border:1px solid #e8e8e8;border-radius:4px;font-size:13px;outline:none;}
|
||||
.form-item input:focus,.form-item select:focus,.form-item textarea:focus{border-color:#1890FF;}
|
||||
.form-item textarea{height:80px;padding:8px 12px;resize:vertical;}
|
||||
/* 规格输入组:克重 input + 单位 select 连在一起 */
|
||||
.spec-input-group{display:flex;align-items:stretch;height:36px;}
|
||||
.spec-input-group input{flex:1;border-radius:4px 0 0 4px;border-right:none;}
|
||||
.spec-input-group select{width:96px;border-radius:0 4px 4px 0;}
|
||||
.action-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;}
|
||||
/* 右侧抽屉 */
|
||||
.drawer-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:1000;animation:modalFadeIn .2s ease;}
|
||||
@@ -263,7 +267,12 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
<div class="form-item"><label>来源类型<span class="required">*</span></label><select><option>请选择</option><option>外采</option><option>自产</option></select></div>
|
||||
<div class="form-item"><label>食材名称<span class="required">*</span></label><select><option>请选择</option><option>菠菜</option><option>土豆</option><option>胡萝卜</option><option>白菜</option><option>西蓝花</option><option>猪里脊</option><option>猪五花</option><option>牛腩</option><option>鸡胸肉</option><option>草鱼</option><option>基围虾</option></select></div>
|
||||
<div class="form-item"><label>供应商<span class="required">*</span></label><select><option>请选择</option><option>A区蔬菜基地</option><option>B区根茎基地</option><option>绿源蔬菜配送</option><option>鲜肉直供公司</option><option>海鲜水产配送</option></select></div>
|
||||
<div class="form-item"><label>入库规格<span class="required">*</span></label><select><option>请选择</option><option>25kg/件</option><option>20kg/箱</option><option>15kg/箱</option><option>10kg/筐</option><option>5kg/袋</option><option>1kg/包</option><option>散装(kg)</option></select></div>
|
||||
<div class="form-item"><label>入库规格<span class="required">*</span></label>
|
||||
<div class="spec-input-group">
|
||||
<input id="f-spec-gram" type="number" min="0" step="0.01" placeholder="请输入克重">
|
||||
<select id="f-spec-unit"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item"><label>入库数量<span class="required">*</span></label><input type="text" placeholder="请输入入库数量"></div>
|
||||
<div class="form-item"><label>入库重量(kg)<span class="required">*</span></label><input type="number" placeholder="请输入入库重量"></div>
|
||||
<div class="form-item"><label>入库时间</label><input type="date"></div>
|
||||
@@ -318,6 +327,12 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* 包装单位字典(从 din-dict-config.html?code=spec_unit 同步,所有业务共用) */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function renderUnitOptions(selectEl){
|
||||
if(!selectEl)return;
|
||||
selectEl.innerHTML='<option>请选择</option>'+SPEC_UNIT_OPTIONS.map(function(u){return '<option>'+u+'</option>';}).join('');
|
||||
}
|
||||
var rawAreaFilterOptions={
|
||||
'毛菜柜':['8号柜','9号柜','10号柜'],
|
||||
'仓库':['A仓-蔬菜区-01','A仓-蔬菜区-02','A仓-蔬菜区-03','A仓-蔬菜区-04','B仓-肉类区-01','B仓-肉类区-02','B仓-肉类区-03','C仓-水产区-01','C仓-水产区-02']
|
||||
@@ -380,6 +395,7 @@ function showDetail(el){
|
||||
var title='入库详情 — '+tds[0].textContent;
|
||||
Drawer.open(title,html);
|
||||
}
|
||||
renderUnitOptions(document.getElementById('f-spec-unit'));
|
||||
updateRawAreaFilterOptions();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -108,6 +108,10 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
.form-item textarea{height:auto;min-height:36px;padding:8px 12px;resize:vertical;}
|
||||
.form-item input:focus,.form-item select:focus,.form-item textarea:focus{border-color:var(--primary);}
|
||||
.form-item select{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;padding-right:28px;}
|
||||
/* 任务 3.4:规格连体组件(左克重 + 右单位) */
|
||||
.spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.spec-input-group input{flex:1;border-radius:4px 0 0 4px;}
|
||||
.spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
/* 食材动态行 */
|
||||
.ingredient-list{display:flex;flex-direction:column;gap:8px;}
|
||||
.ingredient-row{display:flex;gap:8px;align-items:center;}
|
||||
@@ -226,12 +230,12 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<th>包装批次号</th><th>溯源码</th><th>净菜名称</th><th>包装方式</th><th>包装规格</th><th>包装份数</th><th>包装日期</th><th>保质期至</th><th>存放温度(℃)</th><th>包装人</th><th>操作类型</th><th>终端设备</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td>BATCH-NC-20260415-01</td><td>NC-PROC-BOCA-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>500</td><td>20</td><td>2026-04-15</td><td>2026-04-17</td><td>4.1</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-01</td><td>NC-PROC-BOCA-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>500</td><td>20</td><td>2026-04-15</td><td>2026-04-17</td><td>4.1</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-02</td><td>NC-PROC-HLUB-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">保鲜盒</span></td><td>1000</td><td>15</td><td>2026-04-15</td><td>2026-04-18</td><td>4.2</td><td>王包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-02</td><td>NC-PROC-HLUB-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">保鲜盒</span></td><td>1000</td><td>15</td><td>2026-04-15</td><td>2026-04-18</td><td>4.2</td><td>王包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-03</td><td>NC-PROC-XGUO-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">散装托盘</span></td><td>2000</td><td>8</td><td>2026-04-15</td><td>2026-04-17</td><td>4.0</td><td>张包装</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-04</td><td>NC-PROC-LAJP-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>1000</td><td>12</td><td>2026-04-15</td><td>2026-04-17</td><td>4.3</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-2号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T02</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-01</td><td>NC-PROC-BOCA-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>500g/袋</td><td>20</td><td>2026-04-15</td><td>2026-04-17</td><td>4.1</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-01</td><td>NC-PROC-BOCA-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>500g/袋</td><td>20</td><td>2026-04-15</td><td>2026-04-17</td><td>4.1</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-02</td><td>NC-PROC-HLUB-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">保鲜盒</span></td><td>1000g/箱</td><td>15</td><td>2026-04-15</td><td>2026-04-18</td><td>4.2</td><td>王包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-02</td><td>NC-PROC-HLUB-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">保鲜盒</span></td><td>1000g/箱</td><td>15</td><td>2026-04-15</td><td>2026-04-18</td><td>4.2</td><td>王包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-1号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T01</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-03</td><td>NC-PROC-XGUO-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">散装托盘</span></td><td>2000g/箱</td><td>8</td><td>2026-04-15</td><td>2026-04-17</td><td>4.0</td><td>张包装</td><td><span class="tag tag-green">人工录入</span></td><td>—</td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
<tr><td>BATCH-NC-20260415-04</td><td>NC-PROC-LAJP-20260415-01</td><td>土豆-土豆丝</td><td><span class="tag tag-gray">真空袋</span></td><td>1000g/袋</td><td>12</td><td>2026-04-15</td><td>2026-04-17</td><td>4.3</td><td>李包装</td><td><span class="tag tag-blue">设备自动</span></td><td><div>真空机-2号</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">PKG-T02</div></td><td class="td-actions"><span class="link">查看视频</span> <span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -351,7 +355,10 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item__label">包装规格<span class="required">*</span></div>
|
||||
<input id="cleanPackSpecInput" inputmode="numeric" placeholder="请输入纯整数数字">
|
||||
<div class="spec-input-group" id="cleanPackSpecInputGroup">
|
||||
<input id="cleanPackSpecInput" inputmode="numeric" placeholder="请输入克重">
|
||||
<select id="cleanPackUnitSelect"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item__label">包装份数<span class="required">*</span></div>
|
||||
@@ -397,11 +404,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="form-grid">
|
||||
<div class="form-item">
|
||||
<div class="form-item__label">包装餐品<span class="required">*</span></div>
|
||||
<select id="dishSelect" onchange="onDishChange(this.value)"><option value="">请选择餐品</option><option>宫保鸡丁</option><option>清蒸鱼</option><option>蒜蓉菠菜</option><option>炒胡萝卜</option><option>红烧排骨</option></select>
|
||||
<select id="dishSelect" onchange="onDishChange(this.value)"><option value="">请选择餐品</option></select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item__label">包装规格<span class="required">*</span></div>
|
||||
<select><option value="">请选择</option><option>标准份(500g)</option><option>大份(800g)</option></select>
|
||||
<select id="dishPackSpecSelect" disabled><option value="">请先选择餐品</option></select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item__label">包装方式<span class="required">*</span></div>
|
||||
@@ -528,6 +535,60 @@ document.querySelectorAll('.modal-mask').forEach(m=>{
|
||||
m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('show');});
|
||||
});
|
||||
|
||||
/* ========== 跨页 specs 共享工具(任务 3.0) ========== */
|
||||
function loadDishSpecs(){
|
||||
try{
|
||||
var raw=localStorage.getItem('dish_specs_v1');
|
||||
if(!raw)return null;
|
||||
var data=JSON.parse(raw);
|
||||
return (data&&data.dishes)?data.dishes:null;
|
||||
}catch(e){return null;}
|
||||
}
|
||||
function getSpecsByDish(name){
|
||||
var dishes=loadDishSpecs();
|
||||
if(!dishes)return [];
|
||||
var d=dishes.find(function(x){return x.name===name;});
|
||||
return d?(d.specs||[]):[];
|
||||
}
|
||||
function getAllDishNames(){
|
||||
var dishes=loadDishSpecs();
|
||||
return dishes?dishes.map(function(d){return d.name;}):[];
|
||||
}
|
||||
/* 包装单位字典(与 spec_unit 字典一致) */
|
||||
var SPEC_UNIT_OPTIONS=['克','袋','箱','份','把','瓶','罐','桶','散装'];
|
||||
function buildUnitOptions(selected){
|
||||
var html='';
|
||||
SPEC_UNIT_OPTIONS.forEach(function(u){html+='<option value="'+u+'"'+(u===selected?' selected':'')+'>'+u+'</option>';});
|
||||
return html;
|
||||
}
|
||||
/* 包装页兜底:若总库未覆盖,用页内默认 */
|
||||
var PKG_FALLBACK_DISHES={
|
||||
'宫保鸡丁':[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}],
|
||||
'清蒸鱼':[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}],
|
||||
'蒜蓉菠菜':[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}],
|
||||
'炒胡萝卜':[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}],
|
||||
'红烧排骨':[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}]
|
||||
};
|
||||
function getSpecsByDishWithFallback(name){
|
||||
var specs=getSpecsByDish(name);
|
||||
if(specs&&specs.length>0)return specs;
|
||||
return PKG_FALLBACK_DISHES[name]||[{portion:'标准份',baseGram:500,unit:'份'},{portion:'大份',baseGram:800,unit:'份'}];
|
||||
}
|
||||
function getDishNamesWithFallback(){
|
||||
var names=getAllDishNames();
|
||||
if(names&&names.length>0)return names;
|
||||
return Object.keys(PKG_FALLBACK_DISHES);
|
||||
}
|
||||
/* 任务 3.4:按当前餐品动态生成餐品净菜包规格下拉 */
|
||||
function buildDishSpecOptions(dishName){
|
||||
var specs=getSpecsByDishWithFallback(dishName);
|
||||
var html='<option value="">请选择</option>';
|
||||
specs.forEach(function(s){
|
||||
html+='<option value="'+s.portion+'" data-gram="'+s.baseGram+'" data-unit="'+(s.unit||'份')+'">'+s.portion+'('+s.baseGram+'g)</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ========== 餐品食材数据 ========== */
|
||||
var cleanPackFoodTypeOptions={
|
||||
'土豆':['土豆丝','土豆块','土豆泥'],
|
||||
@@ -574,7 +635,15 @@ function renderOptions(selectEl, placeholder, options){
|
||||
}
|
||||
function initCleanPackOptions(){
|
||||
renderOptions(document.getElementById('cleanFoodSelect'),'请选择食材',Object.keys(cleanPackFoodTypeOptions));
|
||||
/* 初始化净菜包装单位下拉默认"袋" */
|
||||
document.getElementById('cleanPackUnitSelect').innerHTML=buildUnitOptions('袋');
|
||||
updateCleanPackTypeOptions();
|
||||
/* 初始化餐品名下拉(任务 3.4:从总库读取) */
|
||||
var dishSel=document.getElementById('dishSelect');
|
||||
var names=getDishNamesWithFallback();
|
||||
var html='<option value="">请选择餐品</option>';
|
||||
names.forEach(function(n){html+='<option value="'+n+'">'+n+'</option>';});
|
||||
dishSel.innerHTML=html;
|
||||
}
|
||||
function updateCleanPackTypeOptions(selectedType){
|
||||
var foodSelect=document.getElementById('cleanFoodSelect');
|
||||
@@ -635,12 +704,22 @@ function updateIngredientCleanTypeOptions(foodSelect, selectedType){
|
||||
var cleanTypeSelect=row.children[2].querySelector('select');
|
||||
cleanTypeSelect.innerHTML=buildTypeOptions(foodSelect.value, selectedType);
|
||||
}
|
||||
// 餐品选择变化 → 自动填充食材表格
|
||||
// 餐品选择变化 → 自动填充食材表格 + 同步餐品规格下拉
|
||||
function onDishChange(val){
|
||||
var ings=dishIngredients[val]||[];
|
||||
var tbody=document.getElementById('ingFormBody');
|
||||
if(ings.length===0){tbody.innerHTML=buildIngRow('','','','','');return;}
|
||||
tbody.innerHTML=ings.map(function(i){return buildIngRow(i.trace,i.name,i.cleanType,i.type,i.amount);}).join('');
|
||||
if(ings.length===0){tbody.innerHTML=buildIngRow('','','','','');}else{
|
||||
tbody.innerHTML=ings.map(function(i){return buildIngRow(i.trace,i.name,i.cleanType,i.type,i.amount);}).join('');
|
||||
}
|
||||
/* 任务 3.4:同步餐品净菜包规格下拉 */
|
||||
var specSel=document.getElementById('dishPackSpecSelect');
|
||||
if(!val){
|
||||
specSel.disabled=true;
|
||||
specSel.innerHTML='<option value="">请先选择餐品</option>';
|
||||
}else{
|
||||
specSel.disabled=false;
|
||||
specSel.innerHTML=buildDishSpecOptions(val);
|
||||
}
|
||||
}
|
||||
// 添加食材行
|
||||
function addIngRow(){
|
||||
@@ -669,12 +748,16 @@ function openCleanPackModal(){
|
||||
document.getElementById('cleanFoodSelect').selectedIndex=0;
|
||||
updateCleanPackTypeOptions();
|
||||
document.getElementById('cleanPackSpecInput').value='';
|
||||
document.getElementById('cleanPackUnitSelect').innerHTML=buildUnitOptions('袋');
|
||||
openModal('modal-nc');
|
||||
}
|
||||
function openDishPackModal(){
|
||||
document.getElementById('modalDhTitle').textContent='新增餐品净菜包';
|
||||
document.getElementById('dishSelect').selectedIndex=0;
|
||||
document.getElementById('ingFormBody').innerHTML=buildIngRow('','','','','');
|
||||
var specSel=document.getElementById('dishPackSpecSelect');
|
||||
specSel.disabled=true;
|
||||
specSel.innerHTML='<option value="">请先选择餐品</option>';
|
||||
openModal('modal-dh');
|
||||
}
|
||||
// 新增时重置弹窗标题
|
||||
|
||||
@@ -232,14 +232,17 @@ tr:hover td{background:#FAFAFA;}
|
||||
<div class="panel-card__title">切配线</div>
|
||||
<div class="panel-filter">
|
||||
<div class="filter-item">
|
||||
<label>分类:</label>
|
||||
<label>切配形状:</label>
|
||||
<select class="filter-select">
|
||||
<option>请选择分类</option>
|
||||
<option>切丝</option>
|
||||
<option>切片</option>
|
||||
<option>切丁</option>
|
||||
<option>切段</option>
|
||||
<option>绞肉</option>
|
||||
<option>请选择形状</option>
|
||||
<option>丝</option>
|
||||
<option>片</option>
|
||||
<option>丁</option>
|
||||
<option>块</option>
|
||||
<option>段</option>
|
||||
<option>末</option>
|
||||
<option>泥</option>
|
||||
<option>条</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary">🔍 查询</button>
|
||||
@@ -261,10 +264,10 @@ tr:hover td{background:#FAFAFA;}
|
||||
<th>加工时长(min)<span class="sorter"></span></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="td-left">土豆(切丝)</td><td>切配线-01</td><td>320.0</td><td>285.6</td><td>18.8</td><td>8.2</td><td>7.4</td><td>0.0</td><td>8</td><td>14</td><td>540</td></tr>
|
||||
<tr><td class="td-left">胡萝卜(切片)</td><td>切配线-01</td><td>180.0</td><td>162.5</td><td>10.2</td><td>4.5</td><td>2.8</td><td>0.0</td><td>6</td><td>9</td><td>380</td></tr>
|
||||
<tr><td class="td-left">猪肉(绞肉)</td><td>切配线-02</td><td>500.0</td><td>452.0</td><td>28.0</td><td>12.5</td><td>7.5</td><td>0.0</td><td>10</td><td>18</td><td>620</td></tr>
|
||||
<tr><td class="td-left">青椒(切块)</td><td>切配线-02</td><td>260.0</td><td>234.5</td><td>14.8</td><td>6.5</td><td>4.2</td><td>0.0</td><td>6</td><td>10</td><td>420</td></tr>
|
||||
<tr><td class="td-left">土豆(丝)</td><td>切配线-01</td><td>320.0</td><td>285.6</td><td>18.8</td><td>8.2</td><td>7.4</td><td>0.0</td><td>8</td><td>14</td><td>540</td></tr>
|
||||
<tr><td class="td-left">胡萝卜(片)</td><td>切配线-01</td><td>180.0</td><td>162.5</td><td>10.2</td><td>4.5</td><td>2.8</td><td>0.0</td><td>6</td><td>9</td><td>380</td></tr>
|
||||
<tr><td class="td-left">猪肉(末)</td><td>切配线-02</td><td>500.0</td><td>452.0</td><td>28.0</td><td>12.5</td><td>7.5</td><td>0.0</td><td>10</td><td>18</td><td>620</td></tr>
|
||||
<tr><td class="td-left">青椒(块)</td><td>切配线-02</td><td>260.0</td><td>234.5</td><td>14.8</td><td>6.5</td><td>4.2</td><td>0.0</td><td>6</td><td>10</td><td>420</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -275,12 +278,21 @@ tr:hover td{background:#FAFAFA;}
|
||||
<div class="panel-card__title">包装线</div>
|
||||
<div class="panel-filter">
|
||||
<div class="filter-item">
|
||||
<label>分类:</label>
|
||||
<label>包装方式:</label>
|
||||
<select class="filter-select">
|
||||
<option>请选择分类</option>
|
||||
<option>真空包装</option>
|
||||
<option>气调包装</option>
|
||||
<option>普通包装</option>
|
||||
<option>请选择包装方式</option>
|
||||
<option>真空</option>
|
||||
<option>气调</option>
|
||||
<option>普通</option>
|
||||
<option>冷鲜</option>
|
||||
<option>保鲜盒</option>
|
||||
<option>散装托盘</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>包装对象:</label>
|
||||
<select class="filter-select">
|
||||
<option>全部</option>
|
||||
<option>净菜包</option>
|
||||
<option>餐品净菜包</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user