(function () { 'use strict'; function toArray(list) { return Array.prototype.slice.call(list || []); } function clearNode(node) { while (node && node.firstChild) node.removeChild(node.firstChild); } function addText(parent, tag, text, className) { var element = document.createElement(tag); if (className) element.className = className; element.textContent = text; parent.appendChild(element); return element; } function addList(parent, items) { var list = document.createElement('ul'); items.forEach(function (item) { addText(list, 'li', item); }); parent.appendChild(list); } function addResultBlock(parent, title, items) { var block = document.createElement('section'); block.className = 'dv2-result-block'; addText(block, 'strong', title); addList(block, items); parent.appendChild(block); } function setLoading(result) { clearNode(result); result.dataset.state = 'loading'; result.setAttribute('aria-busy', 'true'); addText(result, 'h3', '正在整理候选路径'); addText(result, 'p', '系统正在把输入转成风险项和待验证清单。'); } function setError(result, message) { clearNode(result); result.dataset.state = 'error'; result.setAttribute('aria-busy', 'false'); addText(result, 'h3', '请补全必要条件'); addText(result, 'p', message); } function beginResult(result, title, summary) { clearNode(result); result.dataset.state = 'result'; result.setAttribute('aria-busy', 'false'); addText(result, 'h3', title); addText(result, 'p', summary); var grid = document.createElement('div'); grid.className = 'dv2-result-grid'; result.appendChild(grid); return grid; } function delayedResult(result, callback) { setLoading(result); window.setTimeout(callback, 180); } function read(form, name) { var field = form.elements.namedItem(name); return field ? String(field.value || '').trim() : ''; } function initLocalFilters() { toArray(document.querySelectorAll('[data-dv2-filter-group]')).forEach(function (group) { var selector = group.getAttribute('data-dv2-filter-target'); var items = selector ? toArray(document.querySelectorAll(selector)) : []; var controls = toArray(group.querySelectorAll('[data-dv2-filter]')); var statusId = group.getAttribute('data-dv2-filter-status'); var status = statusId ? document.getElementById(statusId) : null; if (!items.length || !controls.length) return; controls.forEach(function (control) { control.addEventListener('click', function () { var value = control.getAttribute('data-dv2-filter') || 'all'; var visible = 0; controls.forEach(function (candidate) { candidate.setAttribute('aria-pressed', candidate === control ? 'true' : 'false'); }); items.forEach(function (item) { var values = (item.getAttribute('data-dv2-values') || '').split(/\s+/); var show = value === 'all' || values.indexOf(value) !== -1; item.hidden = !show; if (show) visible += 1; }); if (status) status.textContent = '当前显示 ' + visible + ' 个结构化对象'; }); }); }); } function initMaterialSearch() { var search = document.querySelector('[data-material-search]'); var filters = toArray(document.querySelectorAll('[data-material-filter]')); var cards = toArray(document.querySelectorAll('[data-material-card]')); var status = document.querySelector('[data-material-status]'); if (!cards.length || (!search && !filters.length)) return; var active = 'all'; function apply() { var query = search ? search.value.trim().toLowerCase() : ''; var visible = 0; cards.forEach(function (card) { var category = card.getAttribute('data-material-category') || ''; var haystack = (card.getAttribute('data-material-search-values') || card.textContent).toLowerCase(); var show = (active === 'all' || category === active) && (!query || haystack.indexOf(query) !== -1); card.hidden = !show; if (show) visible += 1; }); if (status) status.textContent = '当前显示 ' + visible + ' 类材料模板。页面未录入具体供应商牌号与性能数值。'; } filters.forEach(function (filter) { filter.addEventListener('click', function () { active = filter.getAttribute('data-material-filter') || 'all'; filters.forEach(function (candidate) { candidate.setAttribute('aria-pressed', candidate === filter ? 'true' : 'false'); }); apply(); }); }); if (search) search.addEventListener('input', apply); } function initTabs() { var tablist = document.querySelector('[data-tool-tabs]'); if (!tablist) return; var tabs = toArray(tablist.querySelectorAll('[role="tab"]')); var panels = tabs.map(function (tab) { return document.getElementById(tab.getAttribute('aria-controls')); }); function syncHash(tab) { var hash = tab.getAttribute('data-tool-hash'); if (hash && window.history && window.history.replaceState) { window.history.replaceState(null, '', '#' + hash); } } function revealTab(tab, immediate) { var listBox = tablist.getBoundingClientRect(); var tabBox = tab.getBoundingClientRect(); var delta = 0; if (tabBox.left < listBox.left) delta = tabBox.left - listBox.left; if (tabBox.right > listBox.right) delta = tabBox.right - listBox.right; if (!delta) return; var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; tablist.scrollTo({ left: tablist.scrollLeft + delta, behavior: immediate || reduceMotion ? 'auto' : 'smooth' }); } function activate(tab, focus, updateHash, reveal, immediateReveal) { tabs.forEach(function (candidate, index) { var selected = candidate === tab; candidate.setAttribute('aria-selected', selected ? 'true' : 'false'); candidate.tabIndex = selected ? 0 : -1; if (panels[index]) panels[index].hidden = !selected; }); if (focus) tab.focus(); if (updateHash) syncHash(tab); if (reveal) revealTab(tab, immediateReveal); } tabs.forEach(function (tab, index) { tab.addEventListener('click', function () { activate(tab, false, true, true); }); tab.addEventListener('keydown', function (event) { if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft' && event.key !== 'Home' && event.key !== 'End') return; event.preventDefault(); var next = index; if (event.key === 'ArrowRight') next = (index + 1) % tabs.length; if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length; if (event.key === 'Home') next = 0; if (event.key === 'End') next = tabs.length - 1; activate(tabs[next], true, true, true); }); }); function activateFromHash() { var requestedHash = window.location.hash.replace(/^#/, ''); if (!requestedHash) return; var requestedTab = tabs.find(function (tab) { return tab.getAttribute('data-tool-hash') === requestedHash; }); if (requestedTab) activate(requestedTab, false, false, true, true); } activateFromHash(); window.addEventListener('hashchange', activateFromHash); } function initDomainTabs() { var controls = toArray(document.querySelectorAll('[data-domain-tab]')); var panels = toArray(document.querySelectorAll('[data-domain-panel]')); if (!controls.length || !panels.length) return; controls.forEach(function (control) { control.addEventListener('click', function () { var target = control.getAttribute('data-domain-tab'); controls.forEach(function (candidate) { candidate.setAttribute('aria-pressed', candidate === control ? 'true' : 'false'); }); panels.forEach(function (panel) { panel.hidden = panel.getAttribute('data-domain-panel') !== target; }); }); }); } function initPaperSelector() { var form = document.querySelector('[data-paper-selector]'); if (!form) return; var result = document.querySelector('[data-paper-result]'); form.addEventListener('submit', function (event) { event.preventDefault(); var formFactor = read(form, 'paper_form'); var contents = read(form, 'paper_contents'); var process = read(form, 'paper_process'); var endPath = read(form, 'paper_end'); if (!formFactor || !contents || !process || !endPath) { setError(result, '包装形态、内容物、关键加工和终端目标均为必要输入。'); return; } delayedResult(result, function () { var candidates = []; var risks = []; var structure = []; var tests = []; if (formFactor === 'rigid') candidates.push('食品餐饮具卡纸、FBB 或 SBS/SBB 类纸板'); if (formFactor === 'flexible') candidates.push('单面光牛皮薄纸、防油纸或柔性包装原纸'); if (formFactor === 'molded') candidates.push('湿压精密纸浆模塑或热成型纤维结构'); if (formFactor === 'medical') candidates.push('与实际包装用途匹配的医用纸或高性能特种纸'); if (formFactor === 'industrial') candidates.push('高强牛皮纸、结构纸板或特种功能纸'); if (contents === 'oil') { risks.push('油脂类型、温度和接触时间会改变耐油需求'); structure.push('优先评估封孔底涂与耐油功能层'); tests.push('Kit 筛查及实际油脂接触测试'); } if (contents === 'liquid') { risks.push('切边吸液、折痕开裂和封口泄漏'); structure.push('评估封孔、耐液体层和边缘管理'); tests.push('Cobb、渗漏、边缘吸液和完整包装测试'); } if (contents === 'aroma') { risks.push('香气透过、吸附和内容物相容性'); structure.push('评估阻氧、保香与内容物接触层'); tests.push('OTR、保香、感官和相容性测试'); } if (contents === 'dry') { risks.push('水汽敏感性与粉体穿漏需要按产品确认'); structure.push('根据货架期评估阻湿层和封口结构'); tests.push('WVTR、封口完整性和储存验证'); } if (contents === 'none') { risks.push('二级或运输包装仍需确认湿热、摩擦和物流风险'); structure.push('根据表面与结构目标选择底涂或保护层'); tests.push('湿热老化、耐磨及运输相关测试'); } if (process === 'fold') { risks.push('压痕和折叠可能破坏连续涂层'); tests.push('折痕后阻隔保持与纤维开裂检查'); } if (process === 'seal') { risks.push('纸面锚固、热封窗口和 Hot Tack 需共同验证'); structure.push('评估独立热封层或兼具封合功能的表面层'); tests.push('起封温度、封口强度、Hot Tack 和老化后封合'); } if (process === 'forming') { risks.push('成型应变、曲率和局部密度会改变涂层完整性'); tests.push('成型后完整包装、凹角、翻边和切边检查'); } if (process === 'print') { risks.push('油墨、光油和涂层之间可能出现附着或迁移问题'); tests.push('油墨附着、耐磨、感官及法规结构复核'); } if (endPath === 'recycle') tests.push('完整印刷涂布制品的目标市场可回收性评价'); if (endPath === 'industrial') tests.push('完整制品工业堆肥路径及认证机构适用性预审'); if (endPath === 'home') tests.push('完整制品家庭堆肥路径预审,不由原料属性外推'); if (endPath === 'undecided') risks.push('终端路径未确定,不能提前使用可回收或可堆肥声明'); var grid = beginResult(result, '候选纸材与验证路径', '这是结构化初筛,不是材料适用性、法规符合性或认证结论。'); addResultBlock(grid, '候选纸材类别', candidates); addResultBlock(grid, '主要风险', risks); addResultBlock(grid, '建议结构方向', structure.length ? structure : ['先完成纸面、孔隙、吸收与加工条件表征']); addResultBlock(grid, '建议验证组合', tests); }); }); } function initBarrierConfigurator() { var form = document.querySelector('[data-barrier-config]'); if (!form) return; var result = document.querySelector('[data-barrier-result]'); form.addEventListener('submit', function (event) { event.preventDefault(); var sensitivity = read(form, 'barrier_sensitivity'); var oil = read(form, 'barrier_oil'); var seal = read(form, 'barrier_seal'); var climate = read(form, 'barrier_climate'); if (!sensitivity || !oil || !seal || !climate) { setError(result, '敏感性、油脂条件、封口方式和温湿度场景均需选择。'); return; } delayedResult(result, function () { var functions = []; var conditions = []; var tests = []; var missing = ['包装尺寸与表面积', '目标货架期及允许损耗', '完整层结构与涂布量']; if (sensitivity === 'moisture' || sensitivity === 'combined') { functions.push('阻湿'); conditions.push('WVTR 必须绑定温度、相对湿度、方向和样品状态'); tests.push('材料 WVTR 与完整包装储存验证'); } if (sensitivity === 'oxygen' || sensitivity === 'combined') { functions.push('阻氧'); conditions.push('OTR 必须绑定温度、湿度、氧浓度与测试方向'); tests.push('材料 OTR 与氧敏感内容物验证'); } if (sensitivity === 'aroma' || sensitivity === 'combined') { functions.push('保香与低吸附'); conditions.push('明确目标香气物质、浓度、时间和内容物相容性'); tests.push('保香、吸附、感官与相容性测试'); } if (oil === 'yes') { functions.push('耐油脂'); conditions.push('明确油脂类型、温度、接触时间与切边状态'); tests.push('Kit 筛查及实际油脂渗透测试'); } if (seal === 'yes') { functions.push('热封'); conditions.push('记录温度、压力、时间、设备和封合面'); tests.push('起封温度、封口强度、Hot Tack 与封合窗口'); } if (climate === 'cold') { conditions.push('纳入低温、冷凝水和冻融影响'); tests.push('低温弯折、冷凝水与封口完整性'); } if (climate === 'humid') { conditions.push('纳入高湿吸湿、挺度和涂层塑化风险'); tests.push('湿热老化、挺度保持和阻隔保持'); } if (climate === 'hot') { conditions.push('纳入热接触、热油或热灌装条件'); tests.push('热接触、迁移、渗漏和结构稳定性'); } var grid = beginResult(result, '初步功能地图', '工具不会自动生成权威 OTR、WVTR 或货架期数值。数值目标需要产品模型与工程师确认。'); addResultBlock(grid, '需要的功能模块', functions.length ? functions : ['基础表面保护与完整性']); addResultBlock(grid, '必须锁定的条件', conditions); addResultBlock(grid, '建议测试层级', tests); addResultBlock(grid, '当前仍缺输入', missing); }); }); } function initCertificationNavigator() { var form = document.querySelector('[data-cert-navigator]'); if (!form) return; var result = document.querySelector('[data-cert-result]'); form.addEventListener('submit', function (event) { event.preventDefault(); var market = read(form, 'cert_market'); var route = read(form, 'cert_route'); var structure = read(form, 'cert_structure'); if (!market || !route || !structure) { setError(result, '目标市场、终端路径和完整结构类型均为必要输入。'); return; } delayedResult(result, function () { var paths = []; var required = ['完整产品结构及各层材料', '最大厚度或涂布量', '油墨、光油和黏合剂信息', '产品版本和目标市场']; var boundaries = ['工具输出是初步路径判断', '最终适用标准与证书范围由认证机构确认', '当地收集和处理系统需单独核实']; if (route === 'recycle') { paths.push(market === 'eu' ? 'CEPI V3 测试方法结合 4evergreen 或目标市场评价协议' : '目标市场纸基包装可回收性方法与收集现实评估'); required.push('完整印刷涂布制品样品及实验室报告'); boundaries.push('技术可回收性不自动保证每个地区收集'); } if (route === 'industrial') { if (market === 'eu') paths.push('EN 13432 路径,可咨询 DIN CERTCO、TÜV AUSTRIA 或 Seedling 授权体系'); if (market === 'us') paths.push('BPI 商业堆肥路径,以 ASTM D6400 为基础,D6868 或 D8410 是否适用由技术审查决定'); if (market === 'au') paths.push('AS 4736 与 ABA 商业堆肥认证路径'); if (market === 'cn') paths.push('先确认目标处置设施、适用标准与认证方案,不套用其他市场标识'); required.push('崩解、生物降解、生态毒性及成分要求资料'); } if (route === 'home') { if (market === 'us') paths.push('BPI 家庭堆肥路径基于 NF T 51-800,并以商业堆肥认证为前提'); if (market === 'eu') paths.push('家庭堆肥方案需按目标认证计划确认,不使用 EN 13432 工业堆肥证书替代'); if (market === 'au') paths.push('AS 5810 与 ABA 家庭堆肥认证路径'); if (market === 'cn') paths.push('先确认目标市场的家庭堆肥标准和标识使用规则'); required.push('完整产品家庭堆肥测试和正式标识授权'); boundaries.push('工业堆肥证书不能证明家庭堆肥'); } if (route === 'biobased') { paths.push('ASTM D6866 或目标计划认可方法用于生物基碳含量'); required.push('明确被测对象及总有机碳边界'); boundaries.push('生物基含量不证明可降解、可堆肥、可回收或低碳'); } if (structure === 'thermoplastic') boundaries.push('含热塑性层时,部分纤维素基堆肥评价路径可能不适用'); var grid = beginResult(result, '可能适用的认证与评价路径', '在正式送检或申请前,应由目标机构确认结构、方法、版本和标识范围。'); addResultBlock(grid, '候选路径', paths); addResultBlock(grid, '需准备的资料', required); addResultBlock(grid, '不能外推的边界', boundaries); addResultBlock(grid, '下一步', ['形成产品结构表', '锁定标准版本', '向认证机构申请技术预审', '建立变更控制记录']); }); }); } var claimRules = [ { pattern: /FDA认证|FDA\s*食品级|食品级/gi, term: '食品级或 FDA 认证', issue: '缺少目标市场、具体用途、食品类型、接触温度、时间和组分授权路径。', rewrite: '改为针对指定用途完成相应食品接触法规路径评估,并显示适用条件。' }, { pattern: /PFAS为零|绝对无氟|全氟为零|无氟/gi, term: '无氟或 PFAS 为零', issue: '配方声明、供应链文件、总氟筛查和目标 PFAS 分析不是同一证据。', rewrite: '如证据充分,可写无有意添加 PFAS,并同步披露对象、版本和检测状态。' }, { pattern: /100%可降解|完全可降解|可降解/gi, term: '可降解', issue: '缺少环境、时间、标准、完整产品结构和测试或认证状态。', rewrite: '说明工业或家庭堆肥等具体路径、标准、证书范围和当地处理条件。' }, { pattern: /家庭可堆肥|家庭堆肥|家庭可降解/gi, term: '家庭堆肥', issue: '家庭堆肥不能由工业堆肥证书、树脂证书或材料属性外推。', rewrite: '仅在最终产品取得对应家庭堆肥证书并有标识授权时使用。' }, { pattern: /可回收|可再浆/gi, term: '可回收或可再浆', issue: '两者不是同义词,并需绑定完整包装、测试方法、评价协议与目标市场。', rewrite: '描述在指定实验室流程下的再制浆表现,或按目标协议完成的技术可回收性评价。' }, { pattern: /生物基/gi, term: '生物基', issue: '应明确被声明对象和生物基碳或质量口径,不能推导可降解、低碳或食品接触。', rewrite: '绑定具体样品和认可方法报告,单独说明生物基含量边界。' }, { pattern: /医疗级|符合ISO\s*11607|ISO\s*11607/gi, term: '医疗级或 ISO 11607', issue: 'ISO 11607 仅适用于终末灭菌医疗器械的特定包装系统与过程验证。', rewrite: '普通药盒或医疗外包装不得笼统使用该表述,特定项目需完成完整验证。' }, { pattern: /抑菌|抗菌|抗病毒/gi, term: '抑菌或抗病毒', issue: '材料名称不能自动证明主动防护功效。', rewrite: '需明确活性成分、对象、方法、接触时间、耐久、迁移毒理和监管属性。' }, { pattern: /低碳|更环保|零污染|绿色环保/gi, term: '低碳或环保', issue: '缺少功能单位、系统边界、基准方案和经审查的生命周期证据。', rewrite: '改为描述具体材料路径、减量目标或验证中的环境属性。' } ]; function initClaimChecker() { var form = document.querySelector('[data-claim-checker]'); if (!form) return; var result = document.querySelector('[data-claim-result]'); form.addEventListener('submit', function (event) { event.preventDefault(); var text = read(form, 'claim_text'); if (!text) { setError(result, '请输入拟公开的产品标题、广告语或技术声明。'); return; } delayedResult(result, function () { var findings = claimRules.filter(function (rule) { rule.pattern.lastIndex = 0; return rule.pattern.test(text); }); clearNode(result); result.setAttribute('aria-busy', 'false'); result.dataset.state = 'result'; addText(result, 'h3', findings.length ? '发现 ' + findings.length + ' 类声明风险' : '未命中首版高风险词典'); addText(result, 'p', findings.length ? '请按对象、结构、条件、方法、市场、证据和边界逐项补全。' : '这不代表声明已经合规或可发布,仍需专业人员核验具体语境和证据。'); if (findings.length) { var container = document.createElement('div'); container.className = 'dv2-claim-findings'; findings.forEach(function (finding) { var item = document.createElement('article'); item.className = 'dv2-finding'; addText(item, 'strong', finding.term); addText(item, 'p', '风险:' + finding.issue); addText(item, 'p', '建议:' + finding.rewrite); container.appendChild(item); }); result.appendChild(container); } }); }); } function initRegistryDemo() { var registry = document.querySelector('[data-registry-demo]'); if (!registry) return; var control = registry.querySelector('[data-registry-check]'); var result = registry.querySelector('[data-registry-result]'); if (!control || !result) return; control.addEventListener('click', function () { result.hidden = false; result.focus(); }); } document.addEventListener('DOMContentLoaded', function () { initLocalFilters(); initMaterialSearch(); initTabs(); initDomainTabs(); initPaperSelector(); initBarrierConfigurator(); initCertificationNavigator(); initClaimChecker(); initRegistryDemo(); }); }());