const STORAGE_KEY = "traceability-mvp-data-v3"; const FIELD_TYPES = ["string", "integer", "char", "datetime", "json", "image", "video_url", "link", "select", "multi_select"]; const seedState = { templates: [ { id: "tpl-silk-v1", name: "蚕丝被标准模板 V1", description: "适用于蚕丝被产品的一批一码溯源模板。", nodes: [ { id: "tn-1", source: "library", nodeId: "biz-order" }, { id: "tn-2", source: "library", nodeId: "biz-farmer" }, { id: "tn-3", source: "library", nodeId: "biz-grow" }, { id: "tn-4", source: "library", nodeId: "biz-buy" }, { id: "tn-5", source: "library", nodeId: "biz-silk" }, { id: "tn-6", source: "library", nodeId: "biz-manufacture" }, { id: "tn-7", source: "library", nodeId: "biz-inspection" }, { id: "tn-8", source: "library", nodeId: "biz-package" }, { id: "tn-9", source: "library", nodeId: "pub-company" }, { id: "tn-10", source: "library", nodeId: "pub-county" }, { id: "tn-11", source: "library", nodeId: "pub-organic" } ] } ], nodeLibrary: [ { id: "biz-order", category: "business", name: "订种信息", description: "记录订种单、所属乡镇、蚕品种等基础信息。", consumerVisible: true, fields: [ { key: "town", label: "所属乡镇", type: "string", required: true, visible: true, defaultValue: "河西乡" }, { key: "silkwormBreed", label: "蚕品种", type: "string", required: true, visible: true, defaultValue: "桂蚕 8 号" }, { key: "orderNo", label: "订种单号", type: "string", required: true, visible: true, defaultValue: "DZ-2026-001" }, { key: "orderDate", label: "订种日期", type: "datetime", required: true, visible: true, defaultValue: "2026-03-05T09:00" } ] }, { id: "biz-farmer", category: "business", name: "农户信息", description: "记录蚕农、合作社、所在村镇等信息。", consumerVisible: true, fields: [ { key: "farmerName", label: "蚕农姓名", type: "string", required: true, visible: true, defaultValue: "周志远" }, { key: "coopName", label: "合作社名称", type: "string", required: true, visible: true, defaultValue: "锦绣桑蚕合作社" }, { key: "village", label: "所在村镇", type: "string", required: false, visible: true, defaultValue: "新桥村" }, { key: "contact", label: "联系电话", type: "char", required: false, visible: false, defaultValue: "13800001234" } ] }, { id: "biz-grow", category: "business", name: "共育情况", description: "记录共育基地、规模、环境与照片。", consumerVisible: true, fields: [ { key: "baseName", label: "基地名称", type: "string", required: true, visible: true, defaultValue: "春晖共育基地" }, { key: "scale", label: "规模", type: "integer", required: true, visible: true, defaultValue: "320" }, { key: "environment", label: "环境说明", type: "string", required: false, visible: true, defaultValue: "恒温恒湿,独立消杀管理" }, { key: "baseImage", label: "基地照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1501004318641-b39e6451bec6?auto=format&fit=crop&w=900&q=80" } ] }, { id: "biz-buy", category: "business", name: "收购信息", description: "记录收购单、质量等级、采收时间和现场图片。", consumerVisible: true, fields: [ { key: "purchaseNo", label: "收购单号", type: "string", required: true, visible: true, defaultValue: "SG-2026-0331" }, { key: "grade", label: "质量等级", type: "select", required: true, visible: true, defaultValue: "A", options: ["A", "B", "C"] }, { key: "harvestTime", label: "采收时间", type: "datetime", required: true, visible: true, defaultValue: "2026-03-31T11:20" }, { key: "purchaseImage", label: "收购照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=900&q=80" } ] }, { id: "biz-silk", category: "business", name: "制丝/家纺", description: "记录原料领用、操作人员、批次和庄口。", consumerVisible: true, fields: [ { key: "materialTime", label: "原料领用时间", type: "datetime", required: true, visible: true, defaultValue: "2026-04-02T08:40" }, { key: "operator", label: "操作人员", type: "string", required: true, visible: true, defaultValue: "韩海燕" }, { key: "lotNo", label: "批次", type: "string", required: true, visible: true, defaultValue: "ZS-2026-010" }, { key: "station", label: "庄口", type: "string", required: false, visible: true, defaultValue: "一庄口" } ] }, { id: "biz-manufacture", category: "business", name: "制造信息", description: "记录缝制、填充量、规格和工艺。", consumerVisible: true, fields: [ { key: "sewInfo", label: "缝制信息", type: "string", required: true, visible: true, defaultValue: "双针锁边,分区定格" }, { key: "fillWeight", label: "蚕丝被填充量(g)", type: "integer", required: true, visible: true, defaultValue: "3000" }, { key: "craft", label: "工艺", type: "string", required: false, visible: true, defaultValue: "手工拉网 + 定位绗缝" }, { key: "size", label: "规格", type: "string", required: false, visible: true, defaultValue: "220cm × 240cm" } ] }, { id: "biz-inspection", category: "business", name: "质检信息", description: "记录检测项目、报告、合格证明和质检时间。", consumerVisible: true, fields: [ { key: "testItems", label: "检测项目", type: "multi_select", required: true, visible: true, defaultValue: ["纤维含量", "含水率"], options: ["纤维含量", "含水率", "外观", "清洁度"] }, { key: "report", label: "检测报告", type: "link", required: false, visible: true, defaultValue: "https://example.com/report/2026-silk-01" }, { key: "certificateImage", label: "合格证明", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&w=900&q=80" }, { key: "testDate", label: "质检日期", type: "datetime", required: true, visible: true, defaultValue: "2026-04-05T14:10" } ] }, { id: "biz-package", category: "business", name: "包装信息", description: "记录包装时间、包装方式和成品图片。", consumerVisible: true, fields: [ { key: "packageTime", label: "包装时间", type: "datetime", required: true, visible: true, defaultValue: "2026-04-06T16:00" }, { key: "packageStyle", label: "包装方式", type: "string", required: false, visible: true, defaultValue: "礼盒包装 + 防潮袋" }, { key: "productImage", label: "成品照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1616627547584-bf28cee262db?auto=format&fit=crop&w=900&q=80" } ] }, { id: "pub-company", category: "public", name: "企业信息", description: "固定展示品牌主体、工厂信息和联系方式。", consumerVisible: true, fields: [ { key: "factoryName", label: "加工厂名称", type: "string", required: true, visible: true, defaultValue: "广西锦绣桑蚕实业有限公司" }, { key: "address", label: "地址", type: "string", required: true, visible: true, defaultValue: "广西象州县工业园桑蚕家纺产业区 8 号" }, { key: "qualification", label: "资质", type: "string", required: false, visible: true, defaultValue: "ISO9001 / 农产品加工示范企业" }, { key: "contact", label: "联系方式", type: "string", required: false, visible: true, defaultValue: "0772-8888666" } ] }, { id: "pub-county", category: "public", name: "县域情况", description: "展示县域产业特色和区域优势。", consumerVisible: true, fields: [ { key: "countyName", label: "县域名称", type: "string", required: true, visible: true, defaultValue: "象州县" }, { key: "countyIntro", label: "县域介绍", type: "string", required: true, visible: true, defaultValue: "桑蚕养殖基础扎实,形成从蚕种、养殖、制丝到家纺的完整链条。" }, { key: "countyTag", label: "特色标签", type: "string", required: false, visible: true, defaultValue: "国家现代农业示范区" } ] }, { id: "pub-organic", category: "public", name: "有机证书", description: "展示证书编号、发证机构和有效期。", consumerVisible: true, fields: [ { key: "certName", label: "证书名称", type: "string", required: true, visible: true, defaultValue: "有机产品认证证书" }, { key: "certNo", label: "证书编号", type: "string", required: true, visible: true, defaultValue: "ORG-2026-99881" }, { key: "issuer", label: "发证机构", type: "string", required: false, visible: true, defaultValue: "中国质量认证中心" }, { key: "validUntil", label: "有效期至", type: "datetime", required: false, visible: true, defaultValue: "2027-12-31T00:00" } ] } ], traces: [ { id: "trace-2026-silk-001", templateId: "tpl-silk-v1", name: "2026 春季蚕丝被 001 批", code: "TR-2026-001", status: "已发布", currentIndex: 10, scans: 286, nodeData: {} } ] }; let state = loadState(); seedState.traces[0].nodeData = createNodeData(seedState.templates[0], seedState.nodeLibrary); state = normalizeState(state); let ui = { page: "templates", libraryTab: "business", consumerTab: "trace", templateId: state.templates[0]?.id || "", traceId: state.traces[0]?.id || "", templateNodeId: state.templates[0]?.nodes[0]?.id || "", libraryNodeId: state.nodeLibrary.find((item) => item.category === "business")?.id || "", consumerQuery: state.traces[0]?.code || "", dragTemplateNodeId: "" }; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); const parsed = raw ? JSON.parse(raw) : null; if (parsed?.templates && parsed?.nodeLibrary && parsed?.traces) return parsed; } catch (error) { console.warn("load failed", error); } return structuredClone(seedState); } function normalizeState(input) { const cloned = structuredClone(input); cloned.templates.forEach((template) => { template.nodes.forEach((item) => { if (!item.id) item.id = `tn-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }); }); cloned.traces.forEach((trace) => { const template = cloned.templates.find((item) => item.id === trace.templateId); if (!trace.nodeData || Object.keys(trace.nodeData).length === 0) { trace.nodeData = createNodeData(template, cloned.nodeLibrary); } }); return cloned; } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function resetDemo() { localStorage.removeItem(STORAGE_KEY); state = normalizeState(structuredClone(seedState)); ui = { page: "templates", libraryTab: "business", consumerTab: "trace", templateId: state.templates[0]?.id || "", traceId: state.traces[0]?.id || "", templateNodeId: state.templates[0]?.nodes[0]?.id || "", libraryNodeId: state.nodeLibrary.find((item) => item.category === "business")?.id || "", consumerQuery: state.traces[0]?.code || "", dragTemplateNodeId: "" }; saveState(); render(); } const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector)); function getTemplate(id) { return state.templates.find((item) => item.id === id); } function getTrace(id) { return state.traces.find((item) => item.id === id); } function getLibraryNode(id) { return state.nodeLibrary.find((item) => item.id === id); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function formatValue(value, field) { if (value === undefined || value === null || value === "") return "未填写"; if (Array.isArray(value)) return value.join("、"); if (field?.type === "datetime") return String(value).replace("T", " "); return String(value); } function totalScans() { return state.traces.reduce((sum, item) => sum + (item.scans || 0), 0); } function nodeRefToModel(nodeRef) { if (!nodeRef) return null; return nodeRef.source === "library" ? getLibraryNode(nodeRef.nodeId) : nodeRef.node; } function isPublicNode(nodeRef) { return nodeRefToModel(nodeRef)?.category === "public"; } function createNodeData(template, library = state.nodeLibrary) { const data = {}; if (!template) return data; template.nodes.forEach((nodeRef) => { const node = nodeRef.source === "library" ? library.find((item) => item.id === nodeRef.nodeId) : nodeRef.node; if (!node) return; data[nodeRef.id] = {}; node.fields.forEach((field) => { data[nodeRef.id][field.key] = Array.isArray(field.defaultValue) ? [...field.defaultValue] : (field.defaultValue ?? ""); }); }); return data; } function createEmptyNode(category = "business") { return { category, name: category === "public" ? "新公共资料块" : "新业务节点", description: "请完善节点说明与字段。", consumerVisible: true, fields: [ { key: "field1", label: "字段一", type: "string", required: true, visible: true, defaultValue: "" } ] }; } function cloneNode(node) { return structuredClone(node); } function templateNodeLabel(nodeRef) { const node = nodeRefToModel(nodeRef); return node?.name || "未命名节点"; } function templateNodeTypeLabel(nodeRef) { if (!nodeRef) return "未选择节点"; if (nodeRef.source === "custom") return "临时节点"; return isPublicNode(nodeRef) ? "公共资料块" : "业务节点"; } function nextId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; } function createTemplate() { const template = { id: nextId("tpl"), name: "新模板", description: "请完善模板说明并编排节点。", nodes: [] }; state.templates.unshift(template); ui.templateId = template.id; ui.templateNodeId = ""; saveState(); render(); } function createTrace() { const template = getTemplate(ui.templateId); if (!template) return; const code = `TR-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`; const trace = { id: nextId("trace"), templateId: template.id, name: `${template.name} 新批次`, code, status: "进行中", currentIndex: 0, scans: 0, nodeData: createNodeData(template) }; state.traces.unshift(trace); ui.traceId = trace.id; ui.consumerQuery = trace.code; saveState(); render(); } function addLibraryNodeToTemplate(category) { const template = getTemplate(ui.templateId); const selectedId = $(`#template-add-${category}`)?.value; if (!template || !selectedId) return; template.nodes.push({ id: nextId("tn"), source: "library", nodeId: selectedId }); ui.templateNodeId = template.nodes[template.nodes.length - 1].id; state.traces.filter((trace) => trace.templateId === template.id).forEach((trace) => { trace.nodeData[ui.templateNodeId] = {}; const node = getLibraryNode(selectedId); node.fields.forEach((field) => { trace.nodeData[ui.templateNodeId][field.key] = Array.isArray(field.defaultValue) ? [...field.defaultValue] : (field.defaultValue ?? ""); }); }); saveState(); render(); } function addCustomTemplateNode() { const template = getTemplate(ui.templateId); if (!template) return; const nodeRef = { id: nextId("tn"), source: "custom", node: createEmptyNode("business") }; template.nodes.push(nodeRef); ui.templateNodeId = nodeRef.id; state.traces.filter((trace) => trace.templateId === template.id).forEach((trace) => { trace.nodeData[nodeRef.id] = { field1: "" }; }); saveState(); render(); } function addLibraryNode(category) { const node = { id: nextId(category === "public" ? "pub" : "biz"), ...createEmptyNode(category) }; state.nodeLibrary.unshift(node); ui.libraryNodeId = node.id; ui.libraryTab = category; saveState(); render(); } function addFieldToNode(target) { const node = target(); if (!node) return; const index = node.fields.length + 1; node.fields.push({ key: `field${index}`, label: `字段 ${index}`, type: "string", required: false, visible: true, defaultValue: "" }); saveState(); render(); } function removeTemplateNode() { const template = getTemplate(ui.templateId); if (!template || !ui.templateNodeId) return; template.nodes = template.nodes.filter((item) => item.id !== ui.templateNodeId); ui.templateNodeId = template.nodes[0]?.id || ""; saveState(); render(); } function saveTemplateMeta() { const template = getTemplate(ui.templateId); if (!template) return; template.name = ($("#template-name")?.value || "").trim() || template.name; template.description = ($("#template-description")?.value || "").trim(); saveState(); render(); } function currentTemplateNodeRef() { const template = getTemplate(ui.templateId); return template?.nodes.find((item) => item.id === ui.templateNodeId) || null; } function currentLibraryNode() { return getLibraryNode(ui.libraryNodeId); } function saveNodeMeta(target) { const node = target(); if (!node) return; node.name = ($("#node-name")?.value || "").trim() || node.name; node.description = ($("#node-description")?.value || "").trim(); node.consumerVisible = Boolean($("#node-visible")?.checked); saveState(); render(); } function saveTraceBase() { const trace = getTrace(ui.traceId); if (!trace) return; trace.name = ($("#trace-name")?.value || "").trim() || trace.name; trace.code = ($("#trace-code")?.value || "").trim() || trace.code; ui.consumerQuery = trace.code; saveState(); render(); } function saveCurrentTraceNode() { const trace = getTrace(ui.traceId); const template = trace ? getTemplate(trace.templateId) : null; const nodeRef = template?.nodes[trace.currentIndex]; const node = nodeRefToModel(nodeRef); if (!trace || !nodeRef || !node) return; const values = trace.nodeData[nodeRef.id] || {}; let valid = true; node.fields.forEach((field) => { const nextValue = field.type === "multi_select" ? $$(`[data-field-key="${field.key}"]:checked`).map((item) => item.value) : ($(`[data-field-key="${field.key}"]`)?.value ?? ""); if (field.required && (!nextValue || (Array.isArray(nextValue) && nextValue.length === 0))) valid = false; values[field.key] = nextValue; }); if (!valid) return alert("请先补全当前节点的必填字段。"); trace.nodeData[nodeRef.id] = values; if (trace.currentIndex < template.nodes.length - 1) trace.currentIndex += 1; trace.status = trace.currentIndex >= template.nodes.length - 1 ? "待发布" : "进行中"; saveState(); render(); } function publishTrace() { const trace = getTrace(ui.traceId); if (!trace) return; trace.status = "已发布"; trace.scans += 1; saveState(); render(); } function moveTemplateNode(fromId, toId) { const template = getTemplate(ui.templateId); if (!template || fromId === toId) return; const fromIndex = template.nodes.findIndex((item) => item.id === fromId); const toIndex = template.nodes.findIndex((item) => item.id === toId); if (fromIndex < 0 || toIndex < 0) return; const [moved] = template.nodes.splice(fromIndex, 1); template.nodes.splice(toIndex, 0, moved); saveState(); render(); } function updateField(target, index, patch) { const node = target(); if (!node?.fields[index]) return; node.fields[index] = { ...node.fields[index], ...patch }; if (["select", "multi_select"].includes(node.fields[index].type) && !node.fields[index].options) { node.fields[index].options = ["选项一", "选项二"]; } saveState(); } function renderFieldEditor(node, editable) { return node.fields.map((field, index) => `
${escapeHtml(currentNode?.name || "")} · ${escapeHtml(templateNodeTypeLabel(currentNodeRef))}