feat: 新增多类业务功能并优化页面展示

1. 新增钢卷周期对比查询API,增加冷轧卷、花纹板物料类型
2. 优化库存积压统计逻辑,支持成品和原料数据合并计算
3. 新增告警统计功能,实现长度/厚度告警的数量和重量统计
4. 替换岗位管理页面为冷轧厂业务流程泳道图页面
This commit is contained in:
2026-06-17 11:01:47 +08:00
parent 7b7f4b902e
commit 791be3e1a5
5 changed files with 573 additions and 46 deletions

View File

@@ -0,0 +1,430 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>冷轧厂业务流程泳道图</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:"Microsoft YaHei","SimHei",sans-serif;background:#f0f4f8;color:#1e293b;overflow-x:hidden}
.header{background:linear-gradient(135deg,#0f2b47 0%,#1a4a7a 50%,#2c6fad 100%);color:#fff;padding:14px 28px;display:flex;align-items:center;gap:16px;box-shadow:0 3px 16px rgba(0,0,0,.2)}
.header h1{font-size:20px;letter-spacing:2px;font-weight:700}
.header .tag{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);border-radius:12px;padding:3px 14px;font-size:11.5px}
.tabs-wrap{background:#dde3ec;padding:8px 16px 0;position:sticky;top:0;z-index:200;box-shadow:0 2px 6px rgba(0,0,0,.05)}
.tabs{display:flex;gap:3px;overflow-x:auto;padding-bottom:0}
.tabs::-webkit-scrollbar{height:0}
.tab{flex-shrink:0;padding:9px 16px;font-size:12px;border:none;background:transparent;cursor:pointer;border-radius:7px 7px 0 0;color:#5a6a7a;font-weight:500;transition:all .2s;white-space:nowrap}
.tab:hover{background:rgba(255,255,255,.5)}
.tab.on{background:#fff;color:#0f2b47;font-weight:700;box-shadow:0 -1px 4px rgba(0,0,0,.05)}
.tab .tn{display:inline-block;background:#3b6ea5;color:#fff;border-radius:50%;width:17px;height:17px;line-height:17px;font-size:9.5px;text-align:center;margin-right:4px}
.tab.on .tn{background:#1a4a7a}
.panel{display:none;padding:18px 20px 36px}
.panel.on{display:block}
/* ========== 公司架构图 ========== */
.org-outer{background:#fff;border-radius:10px;box-shadow:0 3px 16px rgba(0,0,0,.06);overflow-x:auto;padding:24px 16px 32px;position:relative}
.org-row{display:flex;justify-content:center;gap:10px;flex-wrap:wrap;position:relative;margin-bottom:2px}
.org-node{text-align:center;padding:8px 16px;border-radius:9px;font-weight:700;cursor:pointer;transition:all .2s;position:relative;z-index:2;white-space:nowrap}
.org-node:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.15)}
.org-node.lv0{background:linear-gradient(135deg,#0f2b47,#1a4a7a);color:#fff;font-size:15px;padding:13px 28px;min-width:220px}
.org-node.lv1{background:linear-gradient(135deg,#1e3a5f,#2563eb);color:#fff;font-size:13px;padding:9px 20px;min-width:160px}
.org-node.lv2{background:linear-gradient(135deg,#1d4ed8,#3b82f6);color:#fff;font-size:12px;padding:8px 14px;min-width:155px}
.org-node.lv3{background:#dbeafe;border:1.5px solid #60a5fa;color:#1e40af;font-size:11.5px;padding:6px 11px;min-width:120px;font-weight:600}
.org-node .nd-sub{font-size:9.5px;font-weight:400;opacity:.7;display:block;margin-top:2px}
.org-vline{width:2px;height:16px;background:#93c5fd;margin:0 auto}
.org-tip{text-align:center;font-size:11px;color:#94a3b8;margin-top:10px}
/* ========== 岗位职责 ========== */
.resp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(370px,1fr));gap:16px}
.resp-card{background:#fff;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,.05);overflow:hidden;transition:all .2s}
.resp-card:hover{box-shadow:0 4px 20px rgba(0,0,0,.1)}
.resp-hd{padding:14px 18px;cursor:pointer;display:flex;align-items:center;gap:12px;user-select:none;transition:background .2s}
.resp-hd:hover{background:#f0f5ff}
.resp-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.resp-title{font-size:14px;font-weight:700;color:#1e293b}
.resp-sub{font-size:11px;color:#64748b;margin-top:2px}
.resp-arrow{margin-left:auto;font-size:12px;color:#94a3b8;transition:transform .3s}
.resp-card.open .resp-arrow{transform:rotate(90deg)}
.resp-bd{display:none;padding:0 18px 16px;border-top:1px solid #f1f5f9}
.resp-card.open .resp-bd{display:block}
.resp-item{padding:8px 0;border-bottom:1px solid #f8fafc;display:flex;gap:8px;font-size:12.5px;line-height:1.6}
.resp-item:last-child{border-bottom:none}
.resp-bullet{color:#3b82f6;font-weight:700;flex-shrink:0;margin-top:1px}
.resp-tag{display:inline-block;font-size:10px;padding:2px 8px;border-radius:4px;margin-left:8px;font-weight:500}
.resp-tag.core{background:#dbeafe;color:#1e40af}
.resp-tag.support{background:#dcfce7;color:#166534}
.resp-tag.check{background:#fef3c7;color:#92400e}
/* SVG连线动画 */
.sw-svg path{transition:opacity .3s}
.sw-svg path.line-walk{opacity:.85!important;stroke-width:2.8!important}
.sw-svg path.line-walk-done{opacity:.5!important}
.sw-svg path.line-dimmed{opacity:.08!important}
.footer{text-align:center;padding:18px;color:#94a3b8;font-size:10.5px}
</style>
</head>
<body>
<!-- <div class="header">
<h1>⚙️ 冷轧厂业务流程泳道图</h1>
<span class="tag">架构 · 岗位 · 10 条流程 · 步进回放</span>
</div> -->
<div class="tabs-wrap"><div class="tabs" id="tabs"></div></div>
<div id="panels"></div>
<!-- <div class="footer">冷轧厂业务流程泳道图 · 冶金自动化工程</div> -->
<script>
// ═══════════════════════════════════
// 数据定义
// ═══════════════════════════════════
const ORG_TREE = {
name:"股东大会 / 董事会", lv:0, sub:"最高决策层",
children:[
{name:"监事会", lv:1, sub:"监督审计", children:[]},
{name:"总经理", lv:1, sub:"全面经营管理", children:[
{name:"副总经理(生产/设备)", lv:2, sub:"生产运营+设备保障", children:[
{name:"生产部", lv:3, sub:"计划·调度·酸轧·连退·精整"},
{name:"设备部", lv:3, sub:"机械·电气·液压·点检"},
{name:"安全环保部", lv:3, sub:"安监·环保·培训"},
{name:"工厂/分厂厂长", lv:3, sub:"酸轧·连退·镀锌等车间"}
]},
{name:"副总经理(技术/质量)", lv:2, sub:"技术+质量管控", children:[
{name:"技术中心/研发部", lv:3, sub:"工艺标准·新产品开发"},
{name:"质量检验部", lv:3, sub:"来料检·过程检·成品检·实验室"},
{name:"理化检测中心", lv:3, sub:"力学·化学·金相分析"}
]},
{name:"副总经理(营销/供应链)", lv:2, sub:"销售+采购+物流", children:[
{name:"销售部", lv:3, sub:"国内·海外·大客户"},
{name:"采购部", lv:3, sub:"热轧卷·辅料·备件"},
{name:"物流仓储部", lv:3, sub:"原料库·成品库·运输调度"},
{name:"市场部", lv:3, sub:"市场分析·行业研究"}
]},
{name:"总工程师", lv:2, sub:"技术总负责", children:[
{name:"工艺技术科", lv:3, sub:"工艺规程·技术攻关"},
{name:"自动化信息科", lv:3, sub:"L2/MES·信息化"}
]},
{name:"财务总监", lv:2, sub:"资金+成本管控", children:[
{name:"财务部", lv:3, sub:"成本·预算·资金·税务"}
]},
{name:"职能支撑部门", lv:2, sub:"行政+IT+管理", children:[
{name:"综合管理部/办公室", lv:3, sub:"行政·人资·法务"},
{name:"企管部/运营改善部", lv:3, sub:"KPI·流程优化"},
{name:"IT信息化部", lv:3, sub:"ERP·网络·信息安全"}
]}
]}
]
};
const JOBS = [
{name:"生产部",icon:"🏭",color:"#3b82f6",bg:"#dbeafe",lead:"副总经理(生产/设备)",
duties:[
{t:"主生产计划编制与下达",d:"根据销售订单、产能、原料到货编制月/周主生产计划,分解至各产线",tag:"core"},
{t:"车间日/班次排产",d:"编制各车间日计划、班次排产表,协调产线切换与规格衔接",tag:"core"},
{t:"生产调度与异常协调",d:"实时监控产线运行状态,处理停机、改规、换辊等生产异常",tag:"core"},
{t:"订单进度跟踪与反馈",d:"跟踪订单从排产到完工全流程,向销售部反馈进度",tag:"core"},
{t:"产能平衡与瓶颈分析",d:"分析各产线产能利用率,识别瓶颈并提出改善建议",tag:"support"},
{t:"原料需求提报",d:"根据排产计划向采购部提报原料需求计划",tag:"support"}
]},
{name:"设备部",icon:"🔧",color:"#059669",bg:"#d1fae5",lead:"副总经理(生产/设备)",
duties:[
{t:"设备日常/专业点检",d:"执行日常点检、专业点检,记录设备状态数据",tag:"core"},
{t:"维修计划编制与实施",d:"编制定修/年修计划,组织实施维修作业",tag:"core"},
{t:"故障应急响应",d:"处理突发设备故障,组织抢修恢复生产",tag:"core"},
{t:"备件需求提报与管理",d:"提报备件采购需求,管理备件库存与台账",tag:"support"},
{t:"设备技术改造方案",d:"提出设备技改需求,配合总工程师评审方案",tag:"support"},
{t:"维修后验收与归档",d:"组织维修后设备验收,更新设备档案",tag:"check"}
]},
{name:"技术中心/研发部",icon:"🔬",color:"#7c3aed",bg:"#ede9fe",lead:"副总经理(技术/质量)",
duties:[
{t:"工艺标准/工艺卡制定",d:"制定各钢种、各产线工艺标准,编制工艺卡",tag:"core"},
{t:"新产品/新工艺开发",d:"研发新产品、新工艺路线,组织试制与验证",tag:"core"},
{t:"工艺参数下发与交底",d:"向车间下发工艺参数MES/纸质),组织新工艺交底",tag:"core"},
{t:"工艺异常分析与处置",d:"分析工艺异常原因,制定处置方案",tag:"core"},
{t:"工艺变更管理",d:"评审并管理工艺变更,确保变更受控可追溯",tag:"check"},
{t:"封锁品评审",d:"参与封锁品/待判品技术评审,给出处置建议",tag:"check"}
]},
{name:"质量检验部",icon:"✅",color:"#dc2626",bg:"#fee2e2",lead:"副总经理(技术/质量)",
duties:[
{t:"来料取样与检测",d:"对进厂原料/辅料取样检验,出具检验报告",tag:"core"},
{t:"来料合格/不合格判定",d:"依据标准判定来料是否合格,通知采购和仓储",tag:"core"},
{t:"过程检验",d:"按产线执行过程检验(酸洗、轧制、退火、镀锌等)",tag:"core"},
{t:"成品检验与判定",d:"对成品进行力学/表面/尺寸检验,做出合格/不合格判定",tag:"core"},
{t:"合格品放行",d:"确认成品检验合格后签发放行单,通知仓储发货",tag:"core"},
{t:"封锁品/待判品管理",d:"管理封锁品标识与隔离,组织技术中心评审",tag:"check"},
{t:"质量异议现场确认",d:"参与客户质量异议的实物确认与复检",tag:"check"}
]},
{name:"理化检测中心",icon:"🧪",color:"#b45309",bg:"#fef3c7",lead:"副总经理(技术/质量)",
duties:[
{t:"力学性能检测",d:"拉伸、硬度、冲击等力学性能测试",tag:"core"},
{t:"化学成分分析",d:"直读光谱、碳硫分析、化学成分测定",tag:"core"},
{t:"金相组织检验",d:"金相制样、显微组织观察与评级",tag:"core"},
{t:"镀层/涂层检测",d:"镀锌量、涂层厚度、附着力检测",tag:"core"},
{t:"检测报告出具",d:"出具标准检测报告,支持质量判定",tag:"support"}
]},
{name:"销售部",icon:"📊",color:"#2563eb",bg:"#dbeafe",lead:"副总经理(营销/供应链)",
duties:[
{t:"合同评审与签订",d:"组织合同交期/技术/产能评审,签订销售合同",tag:"core"},
{t:"下达销售订单",d:"先交技术中心确认工艺可行性,再下达生产部排产",tag:"core"},
{t:"发货计划编制与跟踪",d:"编制发货计划,跟踪发货确认与签收",tag:"core"},
{t:"质量异议接收与处理",d:"接收客户质量异议,协调技术/质检处理",tag:"core"},
{t:"客户关系维护与回访",d:"维护大客户关系,异议关闭后组织回访",tag:"support"},
{t:"市场信息反馈",d:"收集市场需求、价格走势信息,反馈至生产/技术",tag:"support"}
]},
{name:"采购部",icon:"📦",color:"#ca8a04",bg:"#fef9c3",lead:"副总经理(营销/供应链)",
duties:[
{t:"采购计划编制",d:"根据生产部/设备部需求编制原料/辅料/备件采购计划",tag:"core"},
{t:"采购订单下达与跟踪",d:"下达采购订单,跟踪交期与物流",tag:"core"},
{t:"到货通知与协调",d:"到货前通知物流仓储部准备接收",tag:"core"},
{t:"不合格品退货处理",d:"质量检验部判定不合格后协调退货",tag:"check"},
{t:"质量索赔",d:"对不合格来料向供应商发起质量索赔",tag:"check"},
{t:"供应商管理",d:"供应商资质评审、绩效评价、目录维护",tag:"support"}
]},
{name:"物流仓储部",icon:"🚛",color:"#0d9488",bg:"#ccfbf1",lead:"副总经理(营销/供应链)",
duties:[
{t:"原料入库与投料管理",d:"原料验收入库,按生产计划投料至各车间",tag:"core"},
{t:"成品入库与出库",d:"成品验收入库,按发货计划备货出库",tag:"core"},
{t:"到货接收与暂存",d:"到货暂存待检区,通知质检检验",tag:"core"},
{t:"发货/运输调度",d:"组织发货装车,调度运输",tag:"core"},
{t:"日常出入库记录",d:"记录每日出入库明细,维护库存台账",tag:"support"},
{t:"库存盘点与差异处理",d:"配合财务部进行定期盘点,处理账实差异",tag:"check"},
{t:"呆滞/封锁品报告",d:"定期报告呆滞品与封锁品,通知质检/财务",tag:"check"}
]},
{name:"安全环保部",icon:"🛡️",color:"#dc2626",bg:"#fee2e2",lead:"副总经理(生产/设备)",
duties:[
{t:"安全/环保培训",d:"组织全员安全培训、特种作业培训、环保培训",tag:"core"},
{t:"日常安全检查",d:"执行日常安全巡查,发现隐患并督促整改",tag:"core"},
{t:"环保监测",d:"监测废水、废气排放,确保达标",tag:"core"},
{t:"事故报告与调查",d:"安全事故报告、调查与处理",tag:"core"},
{t:"隐患整改跟踪",d:"跟踪车间隐患整改进度,闭环管理",tag:"check"},
{t:"外部检查对接",d:"对接政府安监/环保检查",tag:"support"}
]},
{name:"总工程师",icon:"🏗️",color:"#7c3aed",bg:"#ede9fe",lead:"总经理",
duties:[
{t:"技改方案评审",d:"评审技术改造方案的技术可行性",tag:"core"},
{t:"定修/年修计划审批",d:"审批定修/年修计划的技术方案",tag:"core"},
{t:"重大工艺变更审批",d:"审批重大工艺变更方案",tag:"core"},
{t:"项目实施协调",d:"协调技改项目实施中的停机窗口",tag:"core"},
{t:"验收与移交",d:"组织技改项目验收与设备移交",tag:"check"},
{t:"资料归档管理",d:"督促技改资料归档至综合管理部",tag:"support"}
]},
{name:"财务部",icon:"💰",color:"#059669",bg:"#d1fae5",lead:"财务总监",
duties:[
{t:"成本核算与分析",d:"各产线/产品成本核算,差异分析",tag:"core"},
{t:"预算编制与管控",d:"年度/月度预算编制,预算执行监控",tag:"core"},
{t:"开票与结算",d:"销售开票、采购付款结算",tag:"core"},
{t:"应收账款跟踪",d:"跟踪应收账款回收,预警逾期",tag:"core"},
{t:"定期盘点组织",d:"组织月/季/年度库存盘点",tag:"check"},
{t:"盘点差异处理",d:"盘点差异审核与账务调整",tag:"check"}
]},
{name:"综合管理部/办公室",icon:"📋",color:"#64748b",bg:"#f1f5f9",lead:"总经理",
duties:[
{t:"行政管理",d:"公司行政事务、会议组织、文件管理",tag:"core"},
{t:"人力资源管理",d:"招聘、培训、薪酬、绩效考核",tag:"core"},
{t:"法务管理",d:"合同审查、法律风险防控",tag:"core"},
{t:"事故调查配合",d:"配合安全环保部事故调查的行政工作",tag:"support"},
{t:"资料归档接收",d:"接收技改/项目归档资料",tag:"support"}
]},
{name:"IT信息化部",icon:"💻",color:"#2563eb",bg:"#dbeafe",lead:"总经理",
duties:[
{t:"MES/L2系统运维",d:"维护MES、L2系统稳定运行故障响应",tag:"core"},
{t:"ERP系统管理",d:"ERP系统配置、数据维护、用户权限管理",tag:"core"},
{t:"网络与信息安全",d:"网络基础设施运维,信息安全防护",tag:"core"},
{t:"系统开发与集成",d:"新系统开发、系统集成、报表定制",tag:"support"},
{t:"数据备份与容灾",d:"数据库备份、容灾方案实施",tag:"support"}
]},
{name:"各车间",icon:"⚙️",color:"#475569",bg:"#f1f5f9",lead:"工厂/分厂厂长",
duties:[
{t:"产线生产执行",d:"按排产计划执行生产作业,确保产量/质量达标",tag:"core"},
{t:"工艺纪律执行",d:"严格执行工艺参数,接受技术中心抽查",tag:"core"},
{t:"设备日常点检",d:"执行设备日常点检,上报隐患与故障",tag:"core"},
{t:"过程质量自检",d:"执行生产过程自检,配合质量检验部过程检",tag:"check"},
{t:"完工入库操作",d:"成品完工后办理入库/转库手续",tag:"support"},
{t:"安全与5S管理",d:"执行安全操作规程保持5S现场",tag:"check"}
]}
];
// ═══════════════════════════════════
// Tab 0: 公司架构图
// ═══════════════════════════════════
(function(){
const tb = document.createElement('button');
tb.className = 'tab on';
tb.innerHTML = '<span class="tn">🏢</span>公司架构';
document.getElementById('tabs').appendChild(tb);
const pn = document.createElement('div');
pn.className = 'panel on';
pn.id = 'org';
let h = '<div class="org-outer" id="org-outer">';
h += '<div class="org-row" id="org-r0" style="margin-bottom:10px">';
h += '<div class="org-node lv0" data-name="股东大会/董事会">股东大会 / 董事会<span class="nd-sub">最高决策层</span></div>';
h += '</div>';
const gmNode = ORG_TREE.children[1];
h += '<div class="org-row" id="org-r1" style="gap:40px;margin-bottom:10px">';
h += '<div class="org-branch"><div class="org-node lv1" data-name="监事会">监事会<span class="nd-sub">监督审计</span></div></div>';
h += '<div class="org-branch"><div class="org-node lv1" id="org-gm" data-name="总经理">总经理<span class="nd-sub">全面经营管理</span></div></div>';
h += '</div>';
h += '<div class="org-row" id="org-r2" style="gap:12px 6px;margin-bottom:10px">';
gmNode.children.forEach((vp,vi) => {
h += '<div class="org-branch" id="org-vp-'+vi+'">';
h += '<div class="org-node lv2" data-name="'+vp.name+'">'+vp.name+'<span class="nd-sub">'+vp.sub+'</span></div>';
if(vp.children && vp.children.length){
h += '<div class="org-vline"></div>';
h += '<div style="display:flex;gap:5px;flex-wrap:wrap;justify-content:center">';
vp.children.forEach(dp => {
h += '<div class="org-node lv3" data-name="'+dp.name+'">'+dp.name+'<span class="nd-sub">'+dp.sub+'</span></div>';
});
h += '</div>';
}
h += '</div>';
});
h += '</div></div>';
h += '<p class="org-tip">💡 点击节点可跳转至岗位职责</p>';
pn.innerHTML = h;
pn.addEventListener('click', function(e){
const node = e.target.closest('.org-node');
if(node && node.dataset.name) jumpToJob(node.dataset.name);
});
document.getElementById('panels').appendChild(pn);
tb.onclick = () => {
document.querySelectorAll('.tab').forEach(x => x.classList.remove('on'));
document.querySelectorAll('.panel').forEach(x => x.classList.remove('on'));
tb.classList.add('on');
pn.classList.add('on');
setTimeout(drawOrgLines, 100);
};
})();
function jumpToJob(name){
const tabs = document.querySelectorAll('.tab');
if(tabs[1]) tabs[1].click();
setTimeout(() => {
const cards = document.querySelectorAll('.resp-card');
cards.forEach(c => {
if(c.dataset.name === name && !c.classList.contains('open')){
c.querySelector('.resp-hd').click();
}
});
const target = [...cards].find(c => c.dataset.name === name);
if(target) target.scrollIntoView({behavior:'smooth',block:'center'});
}, 280);
}
function drawOrgLines(){
const outer = document.getElementById('org-outer');
if(!outer) return;
let svg = document.getElementById('org-svg');
if(svg) svg.remove();
const w = outer.scrollWidth, h = outer.scrollHeight;
svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
svg.id = 'org-svg';
svg.setAttribute('width', w);
svg.setAttribute('height', h);
svg.setAttribute('viewBox','0 0 '+w+' '+h);
svg.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;overflow:visible;z-index:1';
outer.appendChild(svg);
const or = outer.getBoundingClientRect();
function pos(el){
const r = el.getBoundingClientRect();
return {cx:r.left-or.left+r.width/2, cy:r.top-or.top+r.height/2, l:r.left-or.left, t:r.top-or.top, R:r.left-or.left+r.width, b:r.top-or.top+r.height};
}
function mkCurve(x1,y1,x2,y2,color){
const dy = Math.abs(y2-y1)*0.35;
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d','M'+x1+' '+y1+' C'+x1+' '+(y1<y2?y1+dy:y1-dy)+' '+x2+' '+(y2>dy?y2-dy:y2+dy)+' '+x2+' '+y2);
path.setAttribute('fill','none');
path.setAttribute('stroke',color||'#93c5fd');
path.setAttribute('stroke-width','2');
svg.appendChild(path);
}
const r0n = document.querySelector('#org-r0 .org-node');
const r1ns = document.querySelectorAll('#org-r1 .org-node');
if(r0n && r1ns.length>=2){
const p0 = pos(r0n);
r1ns.forEach(n => { const p1=pos(n); mkCurve(p0.cx,p0.b,p1.cx,p1.t,'#93c5fd'); });
}
const gm = document.getElementById('org-gm');
const vpBranches = document.querySelectorAll('#org-r2 > .org-branch');
if(gm && vpBranches.length){
const pGm = pos(gm);
vpBranches.forEach(br => {
const vpn = br.querySelector(':scope > .org-node');
if(!vpn) return;
const pVp = pos(vpn);
mkCurve(pGm.cx,pGm.b,pVp.cx,pVp.t,'#93c5fd');
});
}
vpBranches.forEach(br => {
const vpn = br.querySelector(':scope > .org-node');
if(!vpn) return;
const pVp = pos(vpn);
const depts = br.querySelectorAll(':scope > div:last-child .org-node');
depts.forEach(dn => { const pD=pos(dn); mkCurve(pVp.cx,pVp.b,pD.cx,pD.t,'#93c5fd'); });
});
}
// ═══════════════════════════════════
// Tab 1: 岗位职责
// ═══════════════════════════════════
(function(){
const tb = document.createElement('button');
tb.className = 'tab';
tb.innerHTML = '<span class="tn">👤</span>岗位职责';
document.getElementById('tabs').appendChild(tb);
const pn = document.createElement('div');
pn.className = 'panel';
pn.id = 'jobs';
let h = '<div class="resp-grid">';
JOBS.forEach(j => {
h += '<div class="resp-card" data-name="'+j.name+'">';
h += '<div class="resp-hd" onclick="this.parentElement.classList.toggle(\'open\')">';
h += '<div class="resp-icon" style="background:'+j.bg+';color:'+j.color+'">'+j.icon+'</div>';
h += '<div><div class="resp-title">'+j.name+'</div><div class="resp-sub">上级:'+j.lead+'</div></div>';
h += '<div class="resp-arrow">▶</div>';
h += '</div>';
h += '<div class="resp-bd">';
j.duties.forEach(d => {
const tc = d.tag==='core'?'core':(d.tag==='check'?'check':'support');
const tl = d.tag==='core'?'核心':(d.tag==='check'?'验收':'支撑');
h += '<div class="resp-item"><span class="resp-bullet">•</span><div><b>'+d.t+'</b><span class="resp-tag '+tc+'">'+tl+'</span><br><span style="color:#64748b;font-size:11.5px">'+d.d+'</span></div></div>';
});
h += '</div></div>';
});
h += '</div>';
pn.innerHTML = h;
document.getElementById('panels').appendChild(pn);
tb.onclick = () => {
document.querySelectorAll('.tab').forEach(x => x.classList.remove('on'));
document.querySelectorAll('.panel').forEach(x => x.classList.remove('on'));
tb.classList.add('on');
pn.classList.add('on');
};
})();
// ═══════════════════════════════════
// 初始化
// ═══════════════════════════════════
window.addEventListener('load', () => {
setTimeout(() => drawOrgLines(), 350);
});
window.addEventListener('resize', () => {
const ap = document.querySelector('.panel.on');
if(!ap) return;
if(ap.id === 'org') drawOrgLines();
else if(ap.id !== 'jobs') drawLines(ap.id);
});
</script>
</body>
</html>

View File

@@ -529,4 +529,13 @@ export function getExportColumns() {
url: '/wms/materialCoil/exportColumns', url: '/wms/materialCoil/exportColumns',
method: 'get', method: 'get',
}) })
}
export function listForPeriodComparison(data) {
return request({
url: '/wms/materialCoil/listForPeriodComparison',
method: 'post',
timeout: 600000,
data: data
})
} }

View File

@@ -123,7 +123,7 @@ import * as echarts from 'echarts'
import { getCoilHoardingStats, listCoilHoardingDetail } from '@/api/cost/coil' import { getCoilHoardingStats, listCoilHoardingDetail } from '@/api/cost/coil'
import WarehouseSelect from '@/components/KLPService/WarehouseSelect' import WarehouseSelect from '@/components/KLPService/WarehouseSelect'
const PRODUCT_NAMES = ['镀锌卷', '镀铬卷', '冷硬卷', '热轧卷板'] const PRODUCT_NAMES = ['镀锌卷', '镀铬卷', '冷硬卷', '热轧卷板', '冷轧卷', '花纹板']
function parseFirstCreateTime(row) { function parseFirstCreateTime(row) {
try { try {
@@ -237,17 +237,37 @@ export default {
fetchDimension() { fetchDimension() {
this.dimLoading = true this.dimLoading = true
const base = this.buildQuery() const base = this.buildQuery()
const promises = PRODUCT_NAMES.map(v => { const promises = PRODUCT_NAMES.flatMap(v => {
const body = { ...base, itemName: v } const baseBody = { ...base, itemName: v }
return getCoilHoardingStats(body).then(res => ({ const productReq = getCoilHoardingStats({ ...baseBody, selectType: 'product' }).then(res => ({
label: v, label: v,
avgDays: parseFloat((res.data && res.data.avgHoardingDays) || 0), avgDays: parseFloat((res.data && res.data.avgHoardingDays) || 0),
avgCost: parseFloat((res.data && res.data.avgHoardingCost) || 0), avgCost: parseFloat((res.data && res.data.avgHoardingCost) || 0),
count: parseInt((res.data && res.data.totalCount) || 0) count: parseInt((res.data && res.data.totalCount) || 0)
})) }))
const rawReq = getCoilHoardingStats({ ...baseBody, selectType: 'raw_material' }).then(res => ({
label: v,
avgDays: parseFloat((res.data && res.data.avgHoardingDays) || 0),
avgCost: parseFloat((res.data && res.data.avgHoardingCost) || 0),
count: parseInt((res.data && res.data.totalCount) || 0)
}))
return [productReq, rawReq]
}) })
Promise.all(promises).then(data => { Promise.all(promises).then(data => {
this.dimensionData = data.filter(d => d.count > 0) const map = {}
data.forEach(d => {
if (!map[d.label]) {
map[d.label] = { label: d.label, avgDays: 0, avgCost: 0, count: 0 }
}
const prev = map[d.label]
const totalCount = prev.count + d.count
if (totalCount > 0) {
prev.avgDays = parseFloat(((prev.avgDays * prev.count + d.avgDays * d.count) / totalCount).toFixed(2))
prev.avgCost = parseFloat(((prev.avgCost * prev.count + d.avgCost * d.count) / totalCount).toFixed(2))
}
prev.count = totalCount
})
this.dimensionData = Object.values(map).filter(d => d.count > 0)
this.$nextTick(() => this.updateDimChart()) this.$nextTick(() => this.updateDimChart())
}).finally(() => { this.dimLoading = false }) }).finally(() => { this.dimLoading = false })
}, },

View File

@@ -1,6 +1,8 @@
<template> <template>
<div class="app-container"> <div>
<!-- 顶部搜索与操作栏 --> <iframe style="width: 100%; height: calc(100vh - 200px);" src="/冷轧厂业务流程泳道图(1).html" frameborder="0"></iframe>
</div>
<!-- <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="岗位名称" prop="postName"> <el-form-item label="岗位名称" prop="postName">
<el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable @keyup.enter.native="handleQuery" />
@@ -28,11 +30,9 @@
</div> </div>
</div> </div>
<div ref="treeChart" class="chart-container"></div> <div ref="treeChart" class="chart-container"></div>
<!-- 岗位信息及操作 -->
</div> </div>
<!-- 添加或修改岗位对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级岗位" prop="parentId"> <el-form-item label="上级岗位" prop="parentId">
@@ -42,24 +42,6 @@
<el-form-item label="岗位名称" prop="postName"> <el-form-item label="岗位名称" prop="postName">
<el-input v-model="form.postName" placeholder="请输入岗位名称" /> <el-input v-model="form.postName" placeholder="请输入岗位名称" />
</el-form-item> </el-form-item>
<!-- <el-form-item label="岗位类型" prop="postType">
<el-select v-model="form.postType" placeholder="请选择岗位类型">
<el-option label="生产岗" value="PRODUCTION" />
<el-option label="质检岗" value="QUALITY" />
<el-option label="维修岗" value="MAINTENANCE" />
<el-option label="技术岗" value="TECHNICAL" />
<el-option label="管理岗" value="MANAGEMENT" />
</el-select>
</el-form-item>
<el-form-item label="岗位级别" prop="postLevel">
<el-select v-model="form.postLevel" placeholder="请选择岗位级别">
<el-option label="初级" value="JUNIOR" />
<el-option label="中级" value="MIDDLE" />
<el-option label="高级" value="SENIOR" />
<el-option label="班长" value="LEAD" />
<el-option label="经理" value="MANAGER" />
</el-select>
</el-form-item> -->
<el-form-item label="显示顺序" prop="postSort"> <el-form-item label="显示顺序" prop="postSort">
<el-input-number v-model="form.postSort" :min="0" :max="999" controls-position="right" /> <el-input-number v-model="form.postSort" :min="0" :max="999" controls-position="right" />
</el-form-item> </el-form-item>
@@ -79,7 +61,6 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 添加或修改岗位职责对话框 -->
<el-dialog :title="dutyTitle" :visible.sync="dutyOpen" width="600px" append-to-body> <el-dialog :title="dutyTitle" :visible.sync="dutyOpen" width="600px" append-to-body>
<el-form ref="dutyForm" :model="dutyForm" :rules="dutyRules" label-width="100px"> <el-form ref="dutyForm" :model="dutyForm" :rules="dutyRules" label-width="100px">
<el-form-item label="职责名称" prop="dutyName"> <el-form-item label="职责名称" prop="dutyName">
@@ -88,17 +69,6 @@
<el-form-item label="职责内容" prop="dutyContent"> <el-form-item label="职责内容" prop="dutyContent">
<el-input v-model="dutyForm.dutyContent" type="textarea" :rows="4" placeholder="请输入职责内容" /> <el-input v-model="dutyForm.dutyContent" type="textarea" :rows="4" placeholder="请输入职责内容" />
</el-form-item> </el-form-item>
<!-- <el-form-item label="职责类型" prop="dutyType">
<el-select v-model="dutyForm.dutyType" placeholder="请选择职责类型">
<el-option label="主要职责" value="MAIN" />
<el-option label="次要职责" value="SECONDARY" />
<el-option label="安全职责" value="SAFETY" />
<el-option label="质量职责" value="QUALITY" />
</el-select>
</el-form-item> -->
<!-- <el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="dutyForm.sortOrder" :min="0" :max="999" controls-position="right" />
</el-form-item> -->
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="dutyForm.remark" type="textarea" placeholder="请输入备注" /> <el-input v-model="dutyForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item> </el-form-item>
@@ -109,7 +79,6 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 岗位职责查看对话框双击节点打开 -->
<el-dialog :title="dutyDialogTitle" :visible.sync="dutyDialogVisible" width="700px" append-to-body> <el-dialog :title="dutyDialogTitle" :visible.sync="dutyDialogVisible" width="700px" append-to-body>
<div class="duty-dialog-wrap"> <div class="duty-dialog-wrap">
<div class="duty-dialog-toolbar"> <div class="duty-dialog-toolbar">
@@ -132,7 +101,7 @@
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
</div> </div> -->
</template> </template>
<script> <script>

View File

@@ -126,6 +126,10 @@
<span class="summary-label">消耗合计</span> <span class="summary-label">消耗合计</span>
<span class="summary-value">{{ totalLossCount }} / {{ totalLossWeight }}t</span> <span class="summary-value">{{ totalLossCount }} / {{ totalLossWeight }}t</span>
</div> </div>
<div class="summary-item">
<span class="summary-label">告警合计</span>
<span class="summary-value">{{ totalAlertCount }} / {{ totalAlertWeight }}t</span>
</div>
</div> </div>
<!-- 折线图区域 --> <!-- 折线图区域 -->
@@ -190,6 +194,14 @@
<el-table-column prop="mAbRubbishRate" label="废品库占比" min-width="85" /> <el-table-column prop="mAbRubbishRate" label="废品库占比" min-width="85" />
<el-table-column prop="mAbReturnRate" label="退货库占比" min-width="85" /> <el-table-column prop="mAbReturnRate" label="退货库占比" min-width="85" />
</el-table-column> </el-table-column>
<el-table-column label="告警统计" align="center">
<el-table-column prop="lengthAlertCount" label="长度告警数" min-width="85" />
<el-table-column prop="thicknessAlertCount" label="厚度告警数" min-width="85" />
<el-table-column prop="totalAlertCount" label="总告警数" min-width="75" />
<el-table-column prop="lengthAlertWeight" label="长度告警总重(t)" min-width="105" />
<el-table-column prop="thicknessAlertWeight" label="厚度告警总重(t)" min-width="105" />
<el-table-column prop="totalAlertWeight" label="总告警总重(t)" min-width="95" />
</el-table-column>
</el-table> </el-table>
</div> </div>
</div> </div>
@@ -198,7 +210,7 @@
<script> <script>
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { listLightCoil } from '@/api/wms/coil' import { listForPeriodComparison } from '@/api/wms/coil'
import { listLightPendingAction } from '@/api/wms/pendingAction' import { listLightPendingAction } from '@/api/wms/pendingAction'
import MemoInput from '@/components/MemoInput' import MemoInput from '@/components/MemoInput'
import MutiSelect from '@/components/MutiSelect' import MutiSelect from '@/components/MutiSelect'
@@ -232,7 +244,9 @@ export default {
], ],
allOutList: [], allOutList: [],
allLossList: [], allLossList: [],
periodData: [] periodData: [],
lengthThreshold: 0,
thicknessThreshold: 0
} }
}, },
computed: { computed: {
@@ -252,6 +266,12 @@ export default {
totalLossWeight() { totalLossWeight() {
return this.periodData.reduce((s, i) => s + (parseFloat(i.lossTotalWeight) || 0), 0).toFixed(2) return this.periodData.reduce((s, i) => s + (parseFloat(i.lossTotalWeight) || 0), 0).toFixed(2)
}, },
totalAlertCount() {
return this.periodData.reduce((s, i) => s + (i.totalAlertCount || 0), 0)
},
totalAlertWeight() {
return this.periodData.reduce((s, i) => s + (parseFloat(i.totalAlertWeight) || 0), 0).toFixed(2)
},
chartConfigs() { chartConfigs() {
return [ return [
// ====== Row 1: 数量/总重 ====== // ====== Row 1: 数量/总重 ======
@@ -344,6 +364,27 @@ export default {
], ],
height: '280px' height: '280px'
}, },
// ====== Row 5: 告警统计 ======
{
title: '告警数量趋势',
series: [
{ key: 'lengthAlertCount', label: '长度告警(卷)', color: '#f56c6c', yAxisIndex: 0 },
{ key: 'thicknessAlertCount', label: '厚度告警(卷)', color: '#e6a23c', yAxisIndex: 0 },
{ key: 'totalAlertCount', label: '总告警(卷)', color: '#409eff', yAxisIndex: 0 }
],
yAxis: [{ type: 'value', name: '数量(卷)' }],
height: '280px'
},
{
title: '告警总重趋势',
series: [
{ key: 'lengthAlertWeight', label: '长度告警总重(t)', color: '#f56c6c', yAxisIndex: 0 },
{ key: 'thicknessAlertWeight', label: '厚度告警总重(t)', color: '#e6a23c', yAxisIndex: 0 },
{ key: 'totalAlertWeight', label: '总告警总重(t)', color: '#409eff', yAxisIndex: 0 }
],
yAxis: [{ type: 'value', name: '重量(t)' }],
height: '280px'
},
{ {
title: 'M-异常库位分布(钢卷数与占比)', title: 'M-异常库位分布(钢卷数与占比)',
series: [ series: [
@@ -422,6 +463,58 @@ export default {
this.handleQuery() this.handleQuery()
}, },
// ====== 告警阈值 ======
getAlarmThreshold() {
this.getConfigKey('material.warning.length').then(response => { this.lengthThreshold = parseFloat(response.msg) || 0 })
this.getConfigKey('material.warning.thickness').then(response => { this.thicknessThreshold = parseFloat(response.msg) || 0 })
},
// 计算一个周期内产出钢卷的告警统计(长度告警、厚度告警、总告警的数量和总重)
calcAlertSummary(outList) {
const lt = this.lengthThreshold
const tt = this.thicknessThreshold
let lengthAlertCount = 0
let thicknessAlertCount = 0
let totalAlertCount = 0
let lengthAlertWeight = 0
let thicknessAlertWeight = 0
let totalAlertWeight = 0
outList.forEach(row => {
const actualLength = row.actualLength || 0
const theoreticalLength = row.theoreticalLength || 1
const lengthDiff = actualLength - theoreticalLength
const theoreticalThickness = row.theoreticalThickness || 0
const computedThickness = row.computedThickness || 0
const thicknessDiff = theoreticalThickness - computedThickness
const weight = parseFloat(row.netWeight) || 0
const isLengthAbnormal = Math.abs(lengthDiff) / theoreticalLength > lt
const isThicknessAbnormal = thicknessDiff > tt
if (isLengthAbnormal) {
lengthAlertCount++
lengthAlertWeight += weight
}
if (isThicknessAbnormal) {
thicknessAlertCount++
thicknessAlertWeight += weight
}
if (isLengthAbnormal || isThicknessAbnormal) {
totalAlertCount++
totalAlertWeight += weight
}
})
return {
lengthAlertCount,
thicknessAlertCount,
totalAlertCount,
lengthAlertWeight: lengthAlertWeight.toFixed(2),
thicknessAlertWeight: thicknessAlertWeight.toFixed(2),
totalAlertWeight: totalAlertWeight.toFixed(2)
}
},
// ====== 数据获取 ====== // ====== 数据获取 ======
handleQuery() { handleQuery() {
this.fetchData() this.fetchData()
@@ -439,6 +532,7 @@ export default {
this.disposeCharts() this.disposeCharts()
this.chartInstances = [] this.chartInstances = []
this.periodData = [] this.periodData = []
this.getAlarmThreshold()
try { try {
const baseQuery = { const baseQuery = {
@@ -482,11 +576,11 @@ export default {
}) })
const [outRes, lossRes] = await Promise.all([ const [outRes, lossRes] = await Promise.all([
listLightCoil({ listForPeriodComparison({
...baseQuery, coilIds: outIds, startTime: '', endTime: '', ...baseQuery, coilIds: outIds, startTime: '', endTime: '',
selectType: 'product', pageSize: 99999, pageNum: 1 selectType: 'product', pageSize: 99999, pageNum: 1
}), }),
lossIds ? listLightCoil({ lossIds ? listForPeriodComparison({
...baseQuery, coilIds: lossIds, startTime: '', endTime: '', ...baseQuery, coilIds: lossIds, startTime: '', endTime: '',
selectType: 'raw_material', pageSize: 99999, pageNum: 1 selectType: 'raw_material', pageSize: 99999, pageNum: 1
}) : Promise.resolve({ data: [] }) }) : Promise.resolve({ data: [] })
@@ -535,6 +629,9 @@ export default {
const mAbMap = {} const mAbMap = {}
mAbSummary.forEach(i => { mAbMap[i.label] = i.value }) mAbSummary.forEach(i => { mAbMap[i.label] = i.value })
// 告警统计(长度告警、厚度告警的数量和总重)
const alertSummary = this.calcAlertSummary(outList)
return { return {
periodLabel: p.label, periodLabel: p.label,
// 基础统计 // 基础统计
@@ -567,7 +664,9 @@ export default {
mAbTechRate: mAbMap['技术部占比'] || '0.00%', mAbTechRate: mAbMap['技术部占比'] || '0.00%',
mAbMiniRate: mAbMap['小钢卷库占比'] || '0.00%', mAbMiniRate: mAbMap['小钢卷库占比'] || '0.00%',
mAbRubbishRate: mAbMap['废品库占比'] || '0.00%', mAbRubbishRate: mAbMap['废品库占比'] || '0.00%',
mAbReturnRate: mAbMap['退货库占比'] || '0.00%' mAbReturnRate: mAbMap['退货库占比'] || '0.00%',
// 告警统计
...alertSummary
} }
}) })
}, },