制造部工作日报与进度看板

车间工作进度在线管理系统 · 数据自动保存至浏览器 · 就绪
📊 项目汇总
🗂️ 进度看板
📝 工作日报
⚙️ 工序管理
已完成
进行中
已阻塞
未开始
💡 点击单元格直接修改状态
日期姓名项目工序状态工时备注操作
序号分组工序名工序KEY操作
⚠️ 工序KEY 是唯一标识,修改后会影响看板联动,建议新增而非修改KEY
歧义 const jsonStr=JSON.stringify({projects:state.projects,processes:state.processes,reports:state.reports}); let b64=''; try{b64=btoa(unescape(encodeURIComponent(jsonStr)))}catch(e){b64=jsonStr} // 用字符串拼接把 分开,避免源码里出现连续闭合标签 const _end=''; const embeddedData='(function(){try{var d=JSON.parse(atob("'+b64+'"));window.APP_DATA=d;}catch(e){window.APP_DATA={};console.error("Export parse error",e)}})();window.__STANDALONE__=true;'+_end; // 只读覆盖样式 const readonlyCSS=``; const readonlyBanner=`
📋 只读看板 · 由制造部工作日报系统导出 · 如需修改请联系填写人🕐 生成于 ${timeStr}
`; let out=src; // 替换 _data.js 引用 const reScriptSrc=/\s*<\/script>/gi; const hadReplacement=out.match(reScriptSrc); if(hadReplacement){ out=out.replace(reScriptSrc,embeddedData); }else{ // fallback: 直接插到 前 out=out.replace(/<\/body>/,embeddedData+'\n'); } // 确保绝对不会残留的 src="_data.js"(fetch 失败场景) out=out.replace(/<\/script>/gi, embeddedData); if(!out.includes('window.APP_DATA')&&!out.includes('window.APP_DATA=')){ // 最后兜底 out=out.replace(/<\/body>/,embeddedData+'\n'); } // 加只读 CSS 和 banner out=out.replace(/<\/head>/,readonlyCSS+'\n'); out=out.replace(/]*)>/,''); out=out.replace(/
/,readonlyBanner+'\n
'); // 改 title out=out.replace(/[^<]*<\/title>/,`<title>制造部工作日报与进度看板 · 生成于 ${timeStr}`); // 彻底清理所有 _data.js 引用 out=out.replace(/src=["'][^"']*_data\.js[^"']*["']/g,'data:text/javascript,'); const blob=new Blob([out],{type:'text/html;charset=utf-8'}); const url=URL.createObjectURL(blob);const a=document.createElement('a'); a.href=url;a.download=`制造部工作日报与进度看板-${dateStr}.html`;a.click();URL.revokeObjectURL(url); toast('✅ 单文件HTML已导出(可直接发给领导)','success'); } function exportExcel(){ const wb=XLSX.utils.book_new(); // 项目清单 XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet([{序号:'',项目:'',客户:'',项目工程师:'',数量:'',合同发货期:''},...state.projects.map(p=>({序号:p.id,项目:p.name,客户:p.customer,项目工程师:p.engineer,数量:p.qty,合同发货期:p.deadline}))]),'项目清单'); // 工序清单 XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet([{序号:'',分组:'',工序名:'','工序KEY(下拉用)':''},...state.processes.map(p=>({序号:p.id,分组:p.group,工序名:p.name,'工序KEY(下拉用)':p.key}))]),'工序清单'); // 工作日报 XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet([{'/':'',姓名:'',项目:'',工序:'',状态:'','工时(天)':'',备注:''},...state.reports.map(r=>({'/':r.date,姓名:r.name,项目:r.project,工序:r.process,状态:r.status,'工时(天)':r.hours,备注:r.note}))]),'工作日报'); // 项目汇总 XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet([{序号:'',项目:'',客户:'',项目工程师:'',数量:'',合同发货期:'',工序总数:'',已完成:'',进行中:'',已阻塞:'',未开始:'',完成率:'',当前状态:'',最近更新:'','共计工时(天)':''},...state.summary.map(p=>({序号:p.id,项目:p.name,客户:p.customer,项目工程师:p.engineer,数量:p.qty,合同发货期:p.deadline,工序总数:p.total,已完成:p.done,进行中:p.doing,已阻塞:p.blocked,未开始:p.todo,完成率:(p.completion/100).toFixed(2),当前状态:p.status,最近更新:p.lastUpdate,'共计工时(天)':p.hours}))]),'项目汇总'); XLSX.writeFile(wb,`制造部工作日报与进度看板-${new Date().toISOString().split('T')[0]}.xlsx`);toast('Excel 已导出','success'); } document.getElementById('jsonInput').addEventListener('change',e=>{ const f=e.target.files[0];if(!f)return; const r=new FileReader();r.onload=ev=>{ try{const d=JSON.parse(ev.target.result); if(!d.projects||!d.processes||!d.reports)throw new Error('JSON 缺少必要字段'); if(!confirm('确定用导入的 JSON 覆盖当前数据?'))return; state.projects=d.projects;state.processes=d.processes;state.reports=d.reports; recomputeAll();saveState(true);renderAll();toast('导入成功','success'); }catch(err){toast('JSON 解析失败: '+err.message,'error')} };r.readAsText(f);e.target.value=''; }); document.getElementById('excelInput').addEventListener('change',e=>{ const f=e.target.files[0];if(!f)return; const r=new FileReader();r.onload=ev=>{ try{ const wb=XLSX.read(ev.target.result,{type:'array',cellDates:false}); function toRowArray(sheetName){ const ws=wb.Sheets[sheetName];if(!ws)return []; return XLSX.utils.sheet_to_json(ws,{header:1,defval:null}); } const projRows=toRowArray('项目清单');const procRows=toRowArray('工序清单');const reportRows=toRowArray('工作日报'); if(projRows.length===0||procRows.length===0)throw new Error('缺少项目清单或工序清单'); if(!confirm('确定用导入的 Excel 覆盖当前数据?'))return; state.projects=projRows.slice(1).filter(r=>r[1]).map(r=>({id:r[0]||0,name:r[1],customer:r[2]||'',engineer:r[3]||'',qty:r[4]||1,deadline:r[5]||''})); state.processes=procRows.slice(1).filter(r=>r[3]).map(r=>({id:r[0]||0,group:r[1]||'',name:r[2]||'',key:r[3]})); state.reports=reportRows.slice(1).filter(r=>r[2]).map((r,i)=>({id:i+1,date:String(r[0]||'').includes('T')?String(r[0]).split('T')[0]:(r[0]||''),name:r[1]||'',project:r[2],process:r[3],status:r[4]||'未开始',hours:r[5]||0,note:r[6]||''})); recomputeAll();saveState(true);renderAll();toast('Excel 导入成功','success'); }catch(err){toast('Excel 导入失败: '+err.message,'error');console.error(err)} };r.readAsArrayBuffer(f);e.target.value=''; }); function resetData(){ if(!confirm('确定重置为原始 Excel 数据?所有当前编辑将丢失。'))return; localStorage.removeItem(LS_KEY); state={projects:window.APP_DATA.projects.map(p=>({...p})),processes:window.APP_DATA.processes.map(p=>({...p})),reports:window.APP_DATA.reports.map(r=>({...r}))}; recomputeAll();saveState(true);renderAll();toast('已重置','success'); } // ========== 筛选 & Tab ========== document.querySelectorAll('.tab').forEach(t=>{ t.addEventListener('click',()=>{ document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.view').forEach(x=>x.classList.remove('active')); t.classList.add('active');document.getElementById('view-'+t.dataset.tab).classList.add('active'); // 重新渲染避免某些筛选状态残留 if(t.dataset.tab==='board')renderBoard(); if(t.dataset.tab==='reports')renderReports(); }); }); function bind(id,fn){const el=document.getElementById(id);if(el){el.addEventListener('input',fn);el.addEventListener('change',fn)}} bind('summarySearch',renderSummary);bind('summaryStatus',renderSummary);bind('summaryEngineer',renderSummary); bind('boardEngineer',renderBoard);bind('boardGroup',renderBoard);bind('boardStatus',renderBoard); bind('reportSearch',()=>{reportPage=0;renderReports()});bind('reportProject',()=>{reportPage=0;renderReports()}); bind('reportName',()=>{reportPage=0;renderReports()});bind('reportStatus',()=>{reportPage=0;renderReports()}); bind('reportDate',()=>{reportPage=0;renderReports()}); // ========== 启动 ========== loadState();renderAll();