<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="format-detection" content="telephone=no">
|
<title>产品溯源</title>
|
<style>
|
:root {
|
--primary: #2e7d32;
|
--primary-light: #43a047;
|
--primary-bg: #e8f5e9;
|
--text: #1f2933;
|
--text-sub: #6b7280;
|
--border: #e5e7eb;
|
--bg: #f5f7f4;
|
--card-shadow: 0 1px 4px rgba(0,0,0,.06);
|
--green: #2e7d32;
|
--red: #d32f2f;
|
--blue: #1565c0;
|
--gray: #6b7280;
|
}
|
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
html, body { height: 100%; }
|
body {
|
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif;
|
background: var(--bg);
|
color: var(--text);
|
line-height: 1.5;
|
-webkit-font-smoothing: antialiased;
|
}
|
.wrap { max-width: 480px; margin: 0 auto; padding: 0 12px 32px; }
|
|
/* 顶部品牌区 */
|
.brand {
|
max-width: 480px; margin: 0 auto;
|
padding: 20px 16px 14px;
|
text-align: center;
|
}
|
.brand .logo {
|
display: inline-flex; align-items: center; gap: 8px;
|
font-size: 20px; font-weight: 700; color: var(--primary);
|
}
|
.brand .logo .mark {
|
width: 26px; height: 26px; border-radius: 50%;
|
background: linear-gradient(135deg, var(--primary-light), var(--primary));
|
position: relative;
|
}
|
.brand .logo .mark::after {
|
content: ""; position: absolute; left: 50%; top: 50%;
|
width: 10px; height: 10px; border: 2px solid #fff; border-radius: 50%;
|
transform: translate(-50%, -50%);
|
}
|
.brand .sub { margin-top: 4px; font-size: 12px; color: var(--text-sub); }
|
|
/* 查询栏 */
|
.query-bar { display: flex; gap: 8px; margin-bottom: 14px; }
|
.query-bar input {
|
flex: 1; height: 42px; padding: 0 12px;
|
border: 1px solid var(--border); border-radius: 8px;
|
font-size: 14px; background: #fff; outline: none;
|
}
|
.query-bar input:focus { border-color: var(--primary); }
|
.query-bar button {
|
height: 42px; padding: 0 18px;
|
border: none; border-radius: 8px;
|
background: var(--primary); color: #fff; font-size: 14px;
|
cursor: pointer;
|
}
|
.query-bar button:active { background: var(--primary-light); }
|
|
/* 状态 */
|
.state { padding-top: 8px; }
|
.loading { text-align: center; padding: 48px 0; color: var(--text-sub); }
|
.spinner {
|
width: 32px; height: 32px; margin: 0 auto 12px;
|
border: 3px solid var(--primary-bg); border-top-color: var(--primary);
|
border-radius: 50%; animation: spin .8s linear infinite;
|
}
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.empty-tip {
|
padding: 12px 16px; border-radius: 10px; background: #fff;
|
box-shadow: var(--card-shadow); font-size: 14px; color: var(--text-sub);
|
text-align: center;
|
}
|
.empty-tip.error { color: var(--red); background: #fdecec; }
|
.empty-tip.primary { color: var(--primary); background: var(--primary-bg); }
|
|
/* 卡片 */
|
.card {
|
background: #fff; border-radius: 12px; box-shadow: var(--card-shadow);
|
margin-bottom: 12px; overflow: hidden;
|
}
|
.card .card-hd {
|
display: flex; align-items: center; gap: 8px;
|
padding: 12px 16px; border-bottom: 1px solid var(--border);
|
font-size: 15px; font-weight: 600;
|
}
|
.card .card-hd .bar { width: 4px; height: 16px; border-radius: 2px; background: var(--primary); }
|
.card .card-hd .count { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--text-sub); }
|
.card .card-bd { padding: 12px 16px; }
|
|
/* 信息行 */
|
.kv { display: flex; padding: 6px 0; font-size: 14px; }
|
.kv .k { flex: 0 0 88px; color: var(--text-sub); }
|
.kv .v { flex: 1; word-break: break-all; }
|
.kv .v.dim { color: var(--text-sub); }
|
|
/* 徽章 */
|
.badge {
|
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
font-size: 12px; line-height: 18px;
|
}
|
.badge.green { color: var(--green); background: #e8f5e9; }
|
.badge.red { color: var(--red); background: #fdecec; }
|
.badge.blue { color: var(--blue); background: #e3f2fd; }
|
.badge.gray { color: var(--gray); background: #f1f2f4; }
|
|
.badge-big { display: inline-block; padding: 4px 12px; border-radius: 16px; font-size: 14px; }
|
.badge-big.green { color: #fff; background: var(--green); }
|
.badge-big.red { color: #fff; background: var(--red); }
|
.badge-big.gray { color: #fff; background: var(--gray); }
|
|
/* 列表 */
|
.mini-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
.mini-table th, .mini-table td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--border); }
|
.mini-table th { color: var(--text-sub); font-weight: 400; background: #fafbf9; white-space: nowrap; }
|
.mini-table tr:last-child td { border-bottom: none; }
|
.mini-table td { word-break: break-all; }
|
|
.pill-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
.pill {
|
display: inline-flex; align-items: center; gap: 6px;
|
padding: 6px 12px; border-radius: 16px; font-size: 13px;
|
background: #f3f4f6;
|
}
|
.pill .dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
.scroll-list { max-height: 300px; overflow-y: auto; }
|
.scroll-list::-webkit-scrollbar { width: 4px; }
|
.scroll-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; }
|
|
footer {
|
max-width: 480px; margin: 0 auto;
|
padding: 20px 16px 28px; text-align: center;
|
font-size: 12px; color: #9ca3af;
|
}
|
</style>
|
</head>
|
<body>
|
|
<div class="brand">
|
<div class="logo"><span class="mark"></span>种子产品溯源</div>
|
<div class="sub">来源可查 · 去向可追 · 责任可究</div>
|
</div>
|
|
<div class="wrap">
|
<!-- 手动查询栏:无 code 参数时提示输入;有参数时自动查询 -->
|
<div class="query-bar">
|
<input id="codeInput" type="text" placeholder="请输入追溯码(袋码 / 托盘码 / 批次码)" autocomplete="off">
|
<button id="searchBtn" type="button">查询</button>
|
</div>
|
|
<section id="state" class="state"></section>
|
</div>
|
|
<footer>本系统数据仅用于产品溯源,请通过正规渠道购买。</footer>
|
|
<script>
|
(function () {
|
'use strict';
|
|
var STATE = document.getElementById('state');
|
var CODE_INPUT = document.getElementById('codeInput');
|
var SEARCH_BTN = document.getElementById('searchBtn');
|
|
var BATCH_STATUS = { 10: ['草稿', 'gray'], 20: ['生产中', 'blue'], 30: ['已完成', 'green'], 40: ['已作废', 'red'] };
|
var BAG_STATUS = { 10: ['已生成', 'gray'], 20: ['已导出印刷', 'blue'], 30: ['已作废', 'red'] };
|
var QUALITY_RESULT = { 0: ['处理中', 'gray'], 1: ['合格', 'green'], 2: ['不合格', 'red'] };
|
var WORKER_TYPE = { 10: '组长', 20: '操作工', 30: '包装工', 40: '质检员', 50: '其他' };
|
|
// ========== 工具函数 ==========
|
function esc(v) {
|
if (v === null || v === undefined) return '';
|
return String(v).replace(/[&<>"']/g, function (c) {
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
});
|
}
|
|
function text(v) {
|
return (v === null || v === undefined || v === '') ? '-' : esc(v);
|
}
|
|
function fmtDate(s) {
|
if (!s) return '-';
|
return esc(String(s).replace('T', ' ').replace(/\.\d+$/, '').slice(0, 16));
|
}
|
|
function fmtNum(v) {
|
return (v === null || v === undefined) ? '-' : esc(v);
|
}
|
|
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 card(title, countHtml, bodyHtml) {
|
return '<div class="card">' +
|
'<div class="card-hd"><span class="bar"></span>' + esc(title) +
|
(countHtml ? '<span class="count">' + countHtml + '</span>' : '') +
|
'</div>' + '<div class="card-bd">' + bodyHtml + '</div>' + '</div>';
|
}
|
|
function kv(k, v, dim) {
|
return '<div class="kv"><span class="k">' + esc(k) + '</span><span class="v' + (dim ? ' dim' : '') + '">' + v + '</span></div>';
|
}
|
|
function isEmptyList(list) {
|
return !list || list.length === 0;
|
}
|
|
// ========== 查询 ==========
|
function query(code) {
|
var value = (code || '').trim();
|
if (!value) {
|
renderState('<div class="empty-tip primary">请输入追溯码后再查询</div>');
|
return;
|
}
|
renderState('<div class="loading"><div class="spinner"></div>正在查询追溯信息...</div>');
|
fetch('/public/mes/trace/query?code=' + encodeURIComponent(value))
|
.then(function (resp) { return resp.json(); })
|
.then(function (res) {
|
if (res.code === 0) {
|
renderResult(res.data);
|
} else {
|
var msg = res.msg || '查询失败,请稍后重试';
|
renderState('<div class="empty-tip error">' + esc(msg) + '</div>');
|
}
|
})
|
.catch(function () {
|
renderState('<div class="empty-tip error">网络异常,请检查网络后重试</div>');
|
});
|
}
|
|
function renderState(html) {
|
STATE.innerHTML = html;
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
}
|
|
// ========== 结果渲染(信息区块模块化,后续新数据源在此追加区块) ==========
|
function renderResult(d) {
|
if (!d) {
|
renderState('<div class="empty-tip error">未查询到该追溯码,请核对二维码或联系官方</div>');
|
return;
|
}
|
var html = '';
|
html += buildItemCard(d);
|
html += buildBatchCard(d);
|
html += buildQualityCard(d);
|
html += buildWorkerCard(d);
|
html += buildInboundCard(d);
|
html += buildPalletCard(d);
|
html += buildBagCard(d);
|
html += buildOptionalCard(d);
|
STATE.innerHTML = html || '<div class="empty-tip">暂无追溯数据</div>';
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
}
|
|
// 区块1:品种信息
|
function buildItemCard(d) {
|
if (!d.itemCode && !d.itemName && !d.itemSpecification) return '';
|
return card('品种信息', null,
|
kv('品种名称', text(d.itemName)) +
|
kv('规格', text(d.itemSpecification)) +
|
kv('品种编码', text(d.itemCode))
|
);
|
}
|
|
// 区块2:生产批次
|
function buildBatchCard(d) {
|
if (!d.batchCode && !d.tempCode) return '';
|
var status = statusBadge(BATCH_STATUS, d.batchStatus);
|
return card('生产批次', status,
|
kv('批次号', text(d.batchCode)) +
|
kv('状态', status) +
|
kv('生产日期', fmtDate(d.produceDate)) +
|
kv('产线', text(d.lineName ? (d.lineName + (d.lineCode ? ' / ' + d.lineCode : '')) : d.lineCode)) +
|
kv('责任人', text(d.ownerUserName)) +
|
kv('计划数量', fmtNum(d.planQuantity) === '-' ? '-' : fmtNum(d.planQuantity)) +
|
kv('已生成袋码', fmtNum(d.bagQuantity)) +
|
kv('已生成托盘', fmtNum(d.palletQuantity)) +
|
kv('入库时间', fmtDate(d.inboundTime)) +
|
(d.remark ? kv('备注', text(d.remark)) : '')
|
);
|
}
|
|
// 区块3:种子质检
|
function buildQualityCard(d) {
|
var q = d.quality;
|
if (!q) return '';
|
var result = QUALITY_RESULT[q.result];
|
var badge = result
|
? '<span class="badge-big ' + result[1] + '">' + esc(result[0]) + '</span>'
|
: '<span class="badge-big gray">未知</span>';
|
|
var rows = '';
|
if (q.indicators && q.indicators.length) {
|
rows = '<table class="mini-table"><thead><tr><th>指标</th><th>检测值</th><th>标准范围</th><th>结果</th></tr></thead><tbody>';
|
q.indicators.forEach(function (i) {
|
var range = (i.minValue !== null && i.minValue !== undefined && i.maxValue !== null && i.maxValue !== undefined)
|
? esc(i.minValue) + ' ~ ' + esc(i.maxValue) : '-';
|
var pass = (i.pass === true)
|
? '<span class="badge green">合格</span>'
|
: (i.pass === false ? '<span class="badge red">不合格</span>' : '<span class="badge gray">--</span>');
|
rows += '<tr><td>' + text(i.indicatorName) + '</td><td>' + text(i.value) + '</td><td>' + range + '</td><td>' + pass + '</td></tr>';
|
});
|
rows += '</tbody></table>';
|
}
|
|
return card('种子质检', badge,
|
kv('检验结论', badge) +
|
kv('检验员', text(q.inspectorName)) +
|
kv('检验时间', fmtDate(q.inspectedAt)) +
|
(q.reason ? kv('不合格原因', text(q.reason)) : '') +
|
(rows ? '<div style="margin-top:8px">' + rows + '</div>' : '')
|
);
|
}
|
|
// 区块4:生产人员
|
function buildWorkerCard(d) {
|
if (isEmptyList(d.workers)) return '';
|
var html = '<div class="pill-list">';
|
d.workers.forEach(function (w) {
|
var name = w.userName || '-';
|
var type = w.workerTypeName || (WORKER_TYPE[w.workerType] || '');
|
var color = { 10: 'var(--blue)', 20: 'var(--green)', 30: 'var(--primary-light)', 40: 'var(--red)', 50: 'var(--gray)' }[w.workerType] || 'var(--gray)';
|
html += '<span class="pill"><span class="dot" style="background:' + color + '"></span>' +
|
esc(type) + ':' + esc(name) + '</span>';
|
});
|
html += '</div>';
|
return card('生产人员', null, html);
|
}
|
|
// 区块5:扫码入库事件
|
function buildInboundCard(d) {
|
var e = d.inbound;
|
if (!e) return '';
|
return card('扫码入库', null,
|
kv('事件号', text(e.eventId)) +
|
kv('入库数量', fmtNum(e.quantity)) +
|
kv('设备编码', text(e.deviceCode)) +
|
kv('产线', text(e.lineCode)) +
|
kv('入库时间', fmtDate(e.eventTime))
|
);
|
}
|
|
// 区块6:托盘
|
function buildPalletCard(d) {
|
if (isEmptyList(d.pallets)) return '';
|
var countHtml = '共 ' + esc(d.palletCount || d.pallets.length) + ' 个';
|
var rows = '<table class="mini-table"><thead><tr><th>托盘码</th><th>序号</th><th>已装/可装</th></tr></thead><tbody>';
|
d.pallets.forEach(function (p) {
|
rows += '<tr><td>' + text(p.code) + '</td><td>' + fmtNum(p.palletNo) + '</td><td>' +
|
fmtNum(p.bagCount) + ' / ' + fmtNum(p.capacity) + '</td></tr>';
|
});
|
rows += '</tbody></table>';
|
return card('托盘信息', countHtml, rows);
|
}
|
|
// 区块7:袋码(一袋一码)
|
function buildBagCard(d) {
|
if (d.bagTotal === null || d.bagTotal === undefined || d.bagTotal === 0) return '';
|
var countHtml = '共 ' + esc(d.bagTotal) + ' 个' + (d.bags && d.bags.length < d.bagTotal ? '(展示前 ' + esc(d.bags.length) + ' 个)' : '');
|
var rows = '<table class="mini-table"><thead><tr><th>袋码</th><th>袋号</th><th>托盘</th><th>状态</th></tr></thead><tbody>';
|
(d.bags || []).forEach(function (b) {
|
rows += '<tr><td>' + text(b.code) + '</td><td>' + fmtNum(b.bagNo) + '</td><td>' + text(b.palletCode) + '</td><td>' +
|
statusBadge(BAG_STATUS, b.status) + '</td></tr>';
|
});
|
rows += '</tbody></table>';
|
return card('袋码信息', countHtml,
|
'<div class="scroll-list">' + rows + '</div>'
|
);
|
}
|
|
// 区块8:可选扩展(采购来源 / 销售去向 / 加工流转),有数据才展示
|
function buildOptionalCard(d) {
|
var html = '';
|
if (!isEmptyList(d.purchaseSources)) {
|
var rows = '<table class="mini-table"><thead><tr><th>采购入库单</th><th>供应商</th><th>数量</th><th>日期</th></tr></thead><tbody>';
|
d.purchaseSources.forEach(function (s) {
|
rows += '<tr><td>' + text(s.receiptCode) + '</td><td>' + text(s.vendorName) + '</td><td>' + fmtNum(s.receivedQuantity) + '</td><td>' + fmtDate(s.receiptDate) + '</td></tr>';
|
});
|
rows += '</tbody></table>';
|
html += card('采购来源', null, rows);
|
}
|
if (!isEmptyList(d.salesTargets)) {
|
var rows2 = '<table class="mini-table"><thead><tr><th>销售出库单</th><th>客户</th><th>数量</th><th>日期</th></tr></thead><tbody>';
|
d.salesTargets.forEach(function (s) {
|
rows2 += '<tr><td>' + text(s.outCode || s.salesOrderCode || '') + '</td><td>' + text(s.customerName) + '</td><td>' + fmtNum(s.quantity) + '</td><td>' + fmtDate(s.outTime) + '</td></tr>';
|
});
|
rows2 += '</tbody></table>';
|
html += card('销售去向', null, rows2);
|
}
|
if (!isEmptyList(d.processTraces)) {
|
var rows3 = '<table class="mini-table"><thead><tr><th>节点</th><th>说明</th><th>时间</th></tr></thead><tbody>';
|
d.processTraces.forEach(function (t) {
|
rows3 += '<tr><td>' + text(t.nodeName) + '</td><td>' + text(t.description) + '</td><td>' + fmtDate(t.processTime) + '</td></tr>';
|
});
|
rows3 += '</tbody></table>';
|
html += card('加工流转', null, rows3);
|
}
|
return html;
|
}
|
|
// ========== 初始化 ==========
|
SEARCH_BTN.addEventListener('click', function () { query(CODE_INPUT.value); });
|
CODE_INPUT.addEventListener('keydown', function (e) { if (e.key === 'Enter') query(CODE_INPUT.value); });
|
|
var initialCode = new URLSearchParams(location.search).get('code') || '';
|
CODE_INPUT.value = initialCode;
|
if (initialCode) {
|
query(initialCode);
|
} else {
|
renderState('<div class="empty-tip">扫码或输入追溯码,即可查询产品全程溯源信息</div>');
|
CODE_INPUT.focus();
|
}
|
})();
|
</script>
|
</body>
|
</html>
|