| | |
| | | // 因此统一由后端下发(/public/mes/trace/config),禁止在此硬编码。 |
| | | var COMPANY_NAME = ''; |
| | | var TRACE_URL = ''; |
| | | var TRACE_LOCATION_URL = ''; |
| | | var IS_IMPORTED = false; |
| | | |
| | | // 拉取溯源页配置;失败时不阻断查询(查询结果本身也带生产经营者与追溯网址) |
| | |
| | | if (res && res.code === 0 && res.data) { |
| | | COMPANY_NAME = res.data.operatorName || ''; |
| | | TRACE_URL = res.data.traceUrl || ''; |
| | | TRACE_LOCATION_URL = res.data.traceLocationUrl || ''; |
| | | IS_IMPORTED = !!res.data.imported; |
| | | } |
| | | }) |
| | |
| | | var CODE_INPUT = document.getElementById('codeInput'); |
| | | var SEARCH_BTN = document.getElementById('searchBtn'); |
| | | |
| | | var FLOW_DIRECTION = { 1: ['入库', 'green'], 2: ['出库', 'blue'] }; |
| | | var FLOW_BIZ_TYPE = { |
| | | 110: '采购入库', 111: '采购入库取消', 112: '调拨出库', 113: '调拨入库', |
| | | 114: '其他出库', 115: '其他入库', 116: '生产领料', 117: '生产退料', |
| | | 118: '生产入库', 119: '销售出库', 120: '销售退货入库', 121: '外协发料', |
| | | 122: '外协入库', 123: '生产消耗', 124: '产品产出', 125: '供应商退货出库', |
| | | 126: '库存调整入库', 127: '库存调整出库' |
| | | }; |
| | | var SOURCE_TYPE = { BAG: '袋码', PALLET: '托盘码', BATCH: '批次码' }; |
| | | |
| | | // ========== 工具函数 ========== |
| | |
| | | // 表格窄列(数量 / 日期)不换行,避免数字被拆行 |
| | | function nw(html) { |
| | | return '<span style="white-space:nowrap">' + html + '</span>'; |
| | | } |
| | | |
| | | function statusBadge(map, key) { |
| | | var item = map[key]; |
| | | if (!item) return '<span class="badge gray">' + (key === null || key === undefined ? '' : esc(key)) + '</span>'; |
| | | return '<span class="badge ' + item[1] + '">' + esc(item[0]) + '</span>'; |
| | | } |
| | | |
| | | function kv(k, v, dim) { |
| | |
| | | return; |
| | | } |
| | | var html = ''; |
| | | if (new URLSearchParams(location.search).get('view') === 'location') { |
| | | html += locationSections(d); |
| | | html += sec('产品追溯标识', null, tracePanel(d), null); |
| | | STATE.innerHTML = html; |
| | | window.scrollTo({ top: 0, behavior: 'smooth' }); |
| | | return; |
| | | } |
| | | html += sec('防伪查询', null, fakePanel(d), null); |
| | | html += sec('追溯查询', null, tracePanel(d), null); |
| | | // 生产明细折叠区:对外展示种子流向、加工流转,生产批次信息不展示。 |
| | | // 生产明细折叠区:对外展示加工流转,生产批次信息不展示。 |
| | | // 质检/人员/托盘/袋码/采购来源等内部明细不对外展示, |
| | | // 如需一并展示,改为 detailSections(d, true) 即可。 |
| | | html += detailSections(d, false); |
| | | STATE.innerHTML = html || '<div class="empty-tip">暂无追溯数据</div>'; |
| | | window.scrollTo({ top: 0, behavior: 'smooth' }); |
| | | } |
| | | |
| | | // 定位授权入口沿用本页的品牌背景与区块样式,按生产、仓储、物流组织已有真实数据。 |
| | | function groupOutboundOrders(rows, codeOf, dateField) { |
| | | var grouped = []; |
| | | var byCode = new Map(); |
| | | rows.forEach(function (item) { |
| | | var code = String(codeOf(item) || '').trim(); |
| | | var quantity = Number(item.quantity); |
| | | var validQuantity = item.quantity !== null && item.quantity !== undefined && Number.isFinite(quantity); |
| | | var existing = code ? byCode.get(code) : null; |
| | | if (!existing) { |
| | | var order = Object.assign({}, item, { quantity: validQuantity ? Math.abs(quantity) : null }); |
| | | grouped.push(order); |
| | | if (code) byCode.set(code, order); |
| | | } else { |
| | | if (validQuantity) existing.quantity = (existing.quantity || 0) + Math.abs(quantity); |
| | | if (!existing.contactName && item.contactName) existing.contactName = item.contactName; |
| | | if (item[dateField] && (!existing[dateField] || item[dateField] > existing[dateField])) { |
| | | existing[dateField] = item[dateField]; |
| | | } |
| | | } |
| | | }); |
| | | return grouped; |
| | | } |
| | | |
| | | function locationSections(d) { |
| | | var production = kv('产品名称', text(d.itemName)) + |
| | | kv('规格', text(d.itemSpecification || d.itemUnitMeasureName)) + |
| | | kv('加工批次', text(d.batchCode || d.tempCode)); |
| | | if (d.produceDate) production = kv('生产日期', fmtDay(d.produceDate)) + production; |
| | | var html = sec('生产信息', null, production, null); |
| | | |
| | | var salesFlows = groupOutboundOrders((d.seedFlows || []).filter(function (item) { return item.bizType === 119; }), |
| | | function (item) { return item.bizCode; }, 'transactionTime'); |
| | | var salesTargets = groupOutboundOrders(d.salesTargets || [], |
| | | function (item) { return item.salesCode || item.salesOrderCode; }, 'salesDate'); |
| | | var flow = (d.seedFlows || []).find(function (item) { return item.warehouseName; }); |
| | | var storage = ''; |
| | | if (d.inboundTime) storage += kv('入库日期', fmtDay(d.inboundTime)); |
| | | var outboundDates = salesFlows.map(function (item) { return item.transactionTime; }) |
| | | .concat(salesTargets.map(function (item) { return item.salesDate; })).filter(Boolean).sort(); |
| | | if (outboundDates.length) storage += kv('最近出库日期', fmtDay(outboundDates[outboundDates.length - 1])); |
| | | if (flow) storage += kv('仓库', text(flow.warehouseName)); |
| | | html += sec('仓储信息', null, storage || '<div class="empty-tip">暂无仓储信息</div>', null); |
| | | |
| | | var logistics = salesFlows.map(function (item) { |
| | | var route = [item.warehouseName, item.clientName].filter(Boolean).join(' → '); |
| | | var description = '该批次' + (item.transactionTime ? '于 ' + String(item.transactionTime).slice(0, 10) + ' ' : '') + |
| | | (item.warehouseName ? '从' + item.warehouseName + '出库' : '完成销售出库') + |
| | | (item.clientName ? ',发往' + item.clientName : '') + '。'; |
| | | return li('<span class="li-ttl">' + text(route || '销售出库') + '</span>', fmtDay(item.transactionTime), |
| | | kv('出库单号', text(item.bizCode)) + kv('数量', fmtNum(item.quantity)) + |
| | | kv('流转说明', text(description)) + |
| | | (item.contactName ? kv('联系人', text(item.contactName)) : '')); |
| | | }).join(''); |
| | | if (!logistics && !isEmptyList(salesTargets)) { |
| | | logistics = salesTargets.map(function (item) { |
| | | var description = '该批次' + (item.salesDate ? '于 ' + String(item.salesDate).slice(0, 10) + ' ' : '') + |
| | | '完成销售出库' + (item.clientName ? ',去向' + item.clientName : '') + '。'; |
| | | return li('<span class="li-ttl">' + text(item.clientName || '销售去向') + '</span>', fmtDay(item.salesDate), |
| | | kv('出库单号', text(item.salesCode || item.salesOrderCode)) + kv('数量', fmtNum(item.quantity)) + |
| | | kv('流转说明', text(description)) + |
| | | (item.contactName ? kv('联系人', text(item.contactName)) : '')); |
| | | }).join(''); |
| | | } |
| | | html += sec('物流信息', null, logistics || '<div class="empty-tip">暂无物流信息</div>', null); |
| | | return html; |
| | | } |
| | | |
| | | function stripUrl(v) { |
| | |
| | | // 生产经营者与追溯网址优先取查询结果(与标签印刷内容同源),退回页面配置 |
| | | var operatorName = d.operatorName || COMPANY_NAME; |
| | | var traceUrl = d.traceUrl || TRACE_URL; |
| | | var locationUrl = new URL(TRACE_LOCATION_URL || location.pathname.replace(/trace\.html$/, 'trace-location.html'), location.href); |
| | | locationUrl.searchParams.set('code', code || ''); |
| | | var operatorLabel = IS_IMPORTED ? '进口商名称' : '生产经营者'; |
| | | // 只有袋码才是「最小销售单元」,其码值才称「单元识别代码」; |
| | | // 托盘码/批次码不属于最小销售单元标识,按实际类型展示,避免误导消费者 |
| | |
| | | kv(operatorLabel, text(operatorName)) + |
| | | kv(codeLabel, text(code || '-')) + |
| | | kv('追溯网址', traceUrl |
| | | ? '<a class="link" href="' + esc(traceUrl) + '">' + esc(traceUrl) + '</a>' |
| | | ? '<a class="link" target="_top" href="' + esc(locationUrl.href) + '">' + esc(traceUrl) + '</a>' |
| | | : '-') + |
| | | kv('计量单位', text(d.itemUnitMeasureName)) + |
| | | kv('追溯类型', esc(SOURCE_TYPE[d.sourceType] || '追溯码')) + |
| | |
| | | html += sec('采购来源', null, table(['采购入库单', '供应商', '数量', '日期'], psRows), { collapsible: true, open: false }); |
| | | } |
| | | |
| | | // 种子流向信息(仅销售出库 bizType=119) |
| | | var salesFlows = (d.seedFlows || []).filter(function (f) { return f.bizType === 119; }); |
| | | if (!isEmptyList(salesFlows)) { |
| | | var sfBody = salesFlows.map(function (f) { |
| | | var qty = (f.quantity === null || f.quantity === undefined) ? '-' : fmtNum(Math.abs(Number(f.quantity))); |
| | | var wh = [f.warehouseName, f.locationName].filter(Boolean).join(' / ') || '-'; |
| | | return li(statusBadge(FLOW_DIRECTION, f.type) + '<span class="li-ttl">' + esc(FLOW_BIZ_TYPE[f.bizType] || '') + '</span>', |
| | | fmtDate(f.transactionTime), |
| | | kv('来源单号', text(f.bizCode)) + |
| | | kv('客户', text(f.clientName)) + |
| | | kv('数量', qty) + |
| | | kv('仓库/库区', text(wh))); |
| | | }).join(''); |
| | | html += sec('种子流向信息', null, '<div class="scroll-list">' + sfBody + '</div>', { collapsible: true, open: false }); |
| | | } else if (!isEmptyList(d.salesTargets)) { |
| | | var stRows = d.salesTargets.map(function (s) { |
| | | return [text(s.salesCode || s.salesOrderCode || ''), text(s.clientName), nw(fmtNum(s.quantity)), nw(fmtDay(s.salesDate))]; |
| | | }); |
| | | html += sec('种子流向信息', null, table(['销售出库单', '客户', '数量', '日期'], stRows), { collapsible: true, open: false }); |
| | | } |
| | | |
| | | // 加工流转 |
| | | if (!isEmptyList(d.processTraces)) { |
| | | var ptBody = d.processTraces.map(function (t) { |
| | |
| | | })(); |
| | | |
| | | // 先拉取页面配置(生产经营者名称、追溯网址),再执行查询;查询结果中同样带有这两项,互为兜底 |
| | | loadTraceConfig(); |
| | | |
| | | var initialCode = new URLSearchParams(location.search).get('code') || ''; |
| | | CODE_INPUT.value = initialCode; |
| | | if (initialCode) { |
| | | query(initialCode); |
| | | } else { |
| | | renderState('<div class="empty-tip">扫码或输入追溯码,即可查询产品全程溯源信息</div>'); |
| | | } |
| | | loadTraceConfig().then(function () { |
| | | if (initialCode) { |
| | | query(initialCode); |
| | | } else { |
| | | renderState('<div class="empty-tip">扫码或输入追溯码,即可查询产品全程溯源信息</div>'); |
| | | } |
| | | }); |
| | | })(); |
| | | </script> |
| | | </body> |