Files
klp-oa/docs/rolling_mill_project_management(1).html
2026-06-29 14:33:10 +08:00

4646 lines
288 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
/* ========== CSS Reset & Base ========== */
*{margin:0;padding:0;box-sizing:border-box;}
:root{
--bg:#f0f2f5;--sidebar-bg:#f7f8fa;--sidebar-text:#333;
--accent:#2176ae;--accent2:#27ae60;--accent3:#e74c3c;--accent4:#f39c12;
--card:#fff;--border:#d0d7de;--text:#1a1a2e;--text2:#666;
--success:#27ae60;--warning:#f39c12;--danger:#e74c3c;--info:#2176ae;
--shadow:0 1px 4px rgba(0,0,0,0.06);
--radius:5px;
}
html{font-size:14px;}
body{font-family:'Microsoft YaHei','Segoe UI',sans-serif;background:var(--bg);color:var(--text);display:flex;height:100vh;overflow:hidden;}
/* ========== Sidebar ========== */
.sidebar{width:200px;min-width:200px;background:var(--sidebar-bg);color:var(--sidebar-text);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;transition:width 0.3s;border-right:1px solid var(--border);}
.sidebar-header{padding:12px 14px;font-size:13px;font-weight:700;color:var(--accent);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;}
.sidebar-header .logo{font-size:18px;}
.sidebar-section{padding:8px 0 2px 14px;font-size:10px;color:#999;text-transform:uppercase;letter-spacing:1px;font-weight:600;}
.sidebar-item{padding:7px 14px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;transition:all 0.15s;border-left:3px solid transparent;color:#555;}
.sidebar-item:hover{background:#e8ecf1;color:var(--text);}
.sidebar-item.active{background:#dce8f5;color:var(--accent);border-left-color:var(--accent);font-weight:600;}
.sidebar-item .icon{font-size:14px;width:20px;text-align:center;}
.sidebar-footer{margin-top:auto;padding:10px 14px;border-top:1px solid var(--border);font-size:10px;color:#999;}
/* ========== Main Content ========== */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
.topbar{height:44px;background:var(--card);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 20px;gap:12px;flex-shrink:0;}
.topbar .title{font-size:14px;font-weight:600;}
.topbar .breadcrumb{color:var(--text2);font-size:11px;}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;}
.topbar-btn{background:var(--accent);color:#fff;border:none;padding:5px 12px;border-radius:var(--radius);cursor:pointer;font-size:11px;}
.topbar-btn:hover{opacity:0.85;}
.content{flex:1;overflow-y:auto;overflow-x:hidden;padding:12px 16px;}
/* ========== Dashboard Cards ========== */
.dashboard-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-bottom:12px;}
.stat-card{background:var(--card);border-radius:var(--radius);padding:10px 14px;border:1px solid var(--border);box-shadow:var(--shadow);cursor:pointer;transition:box-shadow 0.15s;}
.stat-card:hover{box-shadow:0 2px 8px rgba(0,0,0,0.1);}
.stat-card .label{font-size:11px;color:var(--text2);margin-bottom:4px;}
.stat-card .value{font-size:20px;font-weight:700;color:var(--accent);}
.stat-card .sub{font-size:10px;color:var(--text2);margin-top:2px;}
.stat-card.green .value{color:var(--success);}
.stat-card.orange .value{color:var(--warning);}
.stat-card.red .value{color:var(--danger);}
/* ========== Progress Bar ========== */
.progress-bar-wrap{background:var(--card);border-radius:var(--radius);padding:10px 14px;margin-bottom:12px;border:1px solid var(--border);box-shadow:var(--shadow);}
.progress-bar-wrap .title{font-size:12px;font-weight:600;margin-bottom:8px;}
.progress-steps{display:flex;gap:0;align-items:center;overflow-x:auto;padding-bottom:4px;}
.progress-step{display:flex;flex-direction:column;align-items:center;min-width:55px;position:relative;}
.progress-step .circle{width:24px;height:24px;border-radius:50%;background:#ddd;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0;z-index:1;}
.progress-step.done .circle{background:var(--success);}
.progress-step.active .circle{background:var(--accent);}
.progress-step .step-label{font-size:9px;color:var(--text2);margin-top:2px;text-align:center;max-width:55px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.progress-line{height:2px;background:#ddd;flex:1;min-width:10px;}
.progress-line.done{background:var(--success);}
/* ========== Module Panel ========== */
.module-panel{background:var(--card);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow);margin-bottom:10px;}
.module-header{padding:8px 14px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.module-body{padding:10px 14px;}
/* ========== Tables ========== */
.data-table{width:100%;border-collapse:collapse;font-size:11px;}
.data-table th{background:#f0f2f5;padding:6px 10px;text-align:left;font-weight:600;color:var(--text2);border-bottom:2px solid var(--border);position:sticky;top:0;white-space:nowrap;}
.data-table td{padding:5px 10px;border-bottom:1px solid #eee;}
.data-table tr:hover td{background:#f8f9fc;}
.status-badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;}
.status-badge.done{background:#e8f8f0;color:#1a7a3c;}
.status-badge.progress{background:#e8f4fd;color:#1a5a9e;}
.status-badge.pending{background:#fff3cd;color:#856404;}
.status-badge.overdue{background:#fde8e8;color:#9e1a1a;}
.status-badge.review{background:#f3e8fd;color:#5a1a9e;}
.status-badge.pass{background:#e8f8f0;color:#1a7a3c;}
.status-badge.fail{background:#fde8e8;color:#9e1a1a;}
.status-badge.reject{background:#fde8e8;color:#9e1a1a;}
.status-badge.signed{background:#e8f8f0;color:#1a7a3c;}
.status-badge.resolved{background:#e8f8f0;color:#1a7a3c;}
.status-badge.processing{background:#e8f4fd;color:#1a5a9e;}
.status-badge.draft{background:#eee;color:#666;}
/* ========== Forms ========== */
.form-row{display:flex;gap:10px;margin-bottom:10px;flex-wrap:wrap;}
.form-group{display:flex;flex-direction:column;gap:3px;flex:1;min-width:140px;}
.form-group label{font-size:11px;font-weight:600;color:var(--text2);}
.form-group input,.form-group select,.form-group textarea{
padding:5px 10px;border:1px solid var(--border);border-radius:var(--radius);font-size:12px;
background:#fff;color:var(--text);font-family:inherit;
}
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(33,118,174,0.12);}
.form-group textarea{resize:vertical;min-height:50px;}
.btn{padding:5px 14px;border:none;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:600;transition:opacity 0.15s;}
.btn-primary{background:var(--accent);color:#fff;}
.btn-success{background:var(--success);color:#fff;}
.btn-warning{background:var(--warning);color:#fff;}
.btn-danger{background:var(--danger);color:#fff;}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
.btn:hover{opacity:0.85;}
.btn-sm{padding:3px 8px;font-size:10px;}
/* ========== Tabs ========== */
.tabs{display:flex;border-bottom:2px solid var(--border);margin-bottom:10px;gap:0;overflow-x:auto;}
.tab{padding:6px 14px;cursor:pointer;font-size:12px;color:var(--text2);border-bottom:2px solid transparent;margin-bottom:-2px;white-space:nowrap;}
.tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600;}
.tab-content{display:none;}
.tab-content.active{display:block;}
/* ========== Modal ========== */
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);z-index:1000;display:none;align-items:center;justify-content:center;}
.modal-overlay.show{display:flex;}
.modal{background:var(--card);border-radius:8px;width:90%;max-width:650px;max-height:85vh;overflow-y:auto;box-shadow:0 6px 24px rgba(0,0,0,0.18);}
.modal-header{padding:10px 16px;font-size:14px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.modal-body{padding:14px 16px;}
/* ========== Checklist ========== */
.checklist-item{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid #f0f0f0;font-size:12px;}
.checklist-item:last-child{border-bottom:none;}
.checklist-item input[type="checkbox"]{width:16px;height:16px;accent-color:var(--accent);}
.checklist-item .item-text{flex:1;}
.checklist-item.checked .item-text{text-decoration:line-through;color:#aaa;}
/* ========== File Upload ========== */
.file-upload-area{border:2px dashed var(--border);border-radius:var(--radius);padding:16px;text-align:center;cursor:pointer;transition:border-color 0.15s;background:#fafbfc;font-size:12px;}
.file-upload-area:hover{border-color:var(--accent);background:#f0f6ff;}
.file-upload-area .icon{font-size:24px;margin-bottom:4px;}
.file-upload-area .text{color:var(--text2);}
.file-list{margin-top:6px;}
.file-item{display:flex;align-items:center;gap:6px;padding:4px 8px;background:#f5f6fa;border-radius:var(--radius);font-size:11px;margin-bottom:3px;}
/* ========== Comparison Table ========== */
.compare-table{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1px;background:var(--border);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin:8px 0;}
.compare-table .cell{padding:6px 10px;background:var(--card);font-size:12px;}
.compare-table .header-cell{background:#f0f2f5;font-weight:600;font-size:11px;color:var(--text2);}
.compare-table .diff{background:#fff3cd;}
/* ========== Timeline ========== */
.timeline{position:relative;padding-left:20px;}
.timeline::before{content:'';position:absolute;left:8px;top:0;bottom:0;width:2px;background:var(--border);}
.timeline-item{position:relative;padding:0 0 14px 14px;font-size:12px;}
.timeline-item::before{content:'';position:absolute;left:-16px;top:3px;width:10px;height:10px;border-radius:50%;background:var(--accent);border:2px solid var(--card);box-shadow:0 0 0 2px var(--accent);}
.timeline-item .t-date{font-size:10px;color:var(--text2);}
.timeline-item .t-title{font-weight:600;margin:1px 0;}
.timeline-item .t-desc{font-size:11px;color:var(--text2);}
/* ========== Manufacturing Detail Panel ========== */
.stage-detail-card{background:#fafbfc;border:1px solid var(--border);border-radius:6px;margin-bottom:8px;overflow:hidden;}
.stage-detail-header{padding:7px 12px;font-size:12px;font-weight:600;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #eee;cursor:pointer;}
.stage-detail-header.overdue{background:#fff5f5;border-left:3px solid var(--danger);}
.stage-detail-header.ontime{background:#f0faf4;border-left:3px solid var(--success);}
.stage-detail-header.upcoming{background:#fef9e7;border-left:3px solid var(--warning);}
.stage-detail-body{padding:8px 12px;display:none;}
.stage-detail-body.open{display:block;}
.stage-detail-body .section-title{font-size:11px;font-weight:600;color:var(--accent);margin:8px 0 4px 0;padding-bottom:3px;border-bottom:1px dashed #eee;}
.stage-detail-body .section-title:first-child{margin-top:0;}
.evidence-grid{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;}
.evidence-item{display:flex;align-items:center;gap:6px;font-size:10px;padding:4px 10px;background:#fff;border:1px solid var(--border);border-radius:4px;}
.evidence-item.missing{background:#fff5f5;border-color:#fcc;color:var(--danger);}
.evidence-item.ok{background:#f0faf4;border-color:#b7ebc5;color:var(--success);}
.evidence-item .ev-icon{font-size:14px;}
.inspection-table{width:100%;font-size:10px;border-collapse:collapse;margin-top:4px;}
.inspection-table td{padding:3px 6px;border-bottom:1px solid #f0f0f0;}
.inspection-table td:first-child{font-weight:600;color:var(--text2);width:100px;white-space:nowrap;}
.inspection-table td.pass{color:var(--success);font-weight:600;}
.inspection-table td.fail{color:var(--danger);font-weight:600;}
.mfg-detail-panel{background:#fff;border:1px solid var(--border);border-radius:6px;margin:8px 0;overflow:hidden;display:none;}
.mfg-detail-panel.show{display:block;}
.mfg-detail-panel .detail-topbar{padding:8px 12px;background:#f8f9fc;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;font-size:11px;}
.tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;}
.tag-danger{background:#fde8e8;color:#c0392b;}
.tag-warning{background:#fef3cd;color:#856404;}
.tag-success{background:#d4edda;color:#155724;}
.tag-info{background:#e8f4fd;color:#1a5a9e;}
/* ========== Color Card ========== */
.color-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;margin-top:12px;}
.color-card{background:var(--card);border:1px solid var(--border);border-radius:8px;overflow:hidden;box-shadow:var(--shadow);transition:box-shadow .2s,transform .15s;}
.color-card:hover{box-shadow:0 4px 12px rgba(0,0,0,0.1);transform:translateY(-2px);}
.color-swatch{height:80px;position:relative;display:flex;align-items:flex-end;padding:8px 12px;}
.color-swatch .cs-code{color:#fff;font-weight:800;font-size:18px;text-shadow:0 1px 3px rgba(0,0,0,0.5);}
.color-info{padding:10px 12px;}
.color-info .ci-name{font-weight:700;font-size:13px;margin-bottom:2px;}
.color-info .ci-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:4px;}
.color-info .ci-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;}
.color-info .ci-tag.standard{background:#e8f4fd;color:#1a5a9e;}
.color-info .ci-tag.usage{background:#fef3cd;color:#856404;}
.color-info .ci-desc{font-size:11px;color:var(--text2);line-height:1.4;margin-top:4px;}
.color-card-actions{display:flex;gap:6px;padding:0 12px 10px;}
.color-card-actions .btn{font-size:10px;padding:3px 10px;}
.color-quick-palette{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;}
.color-quick-chip{cursor:pointer;width:32px;height:32px;border-radius:6px;border:2px solid transparent;transition:all .15s;position:relative;}
.color-quick-chip:hover{border-color:var(--accent);transform:scale(1.15);box-shadow:0 2px 8px rgba(0,0,0,0.15);}
.color-quick-chip.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(33,118,174,0.3);}
.color-picker-row{display:flex;align-items:center;gap:10px;}
.color-picker-row input[type="color"]{width:44px;height:36px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;padding:2px;background:#fff;}
.color-preview-block{width:44px;height:36px;border-radius:var(--radius);border:1px solid var(--border);}
/* Color category sections */
.color-category-section{margin-bottom:20px;}
.color-cat-header{display:flex;align-items:center;gap:8px;padding:8px 12px;background:linear-gradient(135deg,#f8f9fb,#f0f2f5);border-left:4px solid var(--accent);border-radius:0 6px 6px 0;margin-bottom:10px;}
.color-cat-icon{font-size:16px;}
.color-cat-name{font-weight:700;font-size:13px;color:var(--text);}
.color-cat-count{font-size:10px;color:var(--text2);background:#e8e8ec;padding:1px 8px;border-radius:10px;margin-left:auto;}
/* ========== Thinking ========== */
.thinking-badge{cursor:pointer;display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;background:#e8f0fe;color:#1a56db;transition:all .15s;}
.thinking-badge:hover{background:#d0e1f9;transform:scale(1.05);}
.thinking-badge-empty{cursor:pointer;display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;color:#bbb;transition:all .15s;}
.thinking-badge-empty:hover{color:var(--accent);background:#f0f6ff;}
.thinking-display{background:#fafbfc;border:1px solid var(--border);border-radius:6px;padding:14px 16px;font-size:12px;line-height:1.8;white-space:pre-wrap;color:var(--text);max-height:400px;overflow-y:auto;border-left:3px solid var(--accent);}
/* ========== Mind Map ========== */
.mm-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:2000;display:none;}
.mm-overlay.show{display:block;}
.mm-container{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;background:#f8f9fb;}
.mm-toolbar{display:flex;align-items:center;gap:8px;padding:10px 16px;background:#fff;border-bottom:1px solid var(--border);z-index:10;flex-shrink:0;}
.mm-toolbar .mm-title{font-weight:700;font-size:14px;margin-right:12px;}
.mm-toolbar .mm-sep{width:1px;height:20px;background:var(--border);margin:0 4px;}
.mm-toolbar .btn-sm{padding:4px 10px;font-size:11px;}
.mm-canvas-wrap{flex:1;overflow:auto;position:relative;cursor:grab;}
.mm-canvas-wrap:active{cursor:grabbing;}
.mm-canvas-wrap.panning{cursor:grabbing;}
.mm-canvas{position:relative;min-width:3000px;min-height:2000px;}
.mm-canvas svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible;}
.mm-canvas svg path{fill:none;stroke:#b0bec5;stroke-width:2;stroke-linecap:round;}
.mm-canvas svg path.mm-edge-selected{stroke:var(--accent);stroke-width:2.5;}
.mm-node{position:absolute;background:#fff;border:2px solid #cfd8dc;border-radius:8px;padding:8px 14px;min-width:100px;max-width:220px;cursor:pointer;font-size:12px;font-weight:500;color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,0.08);transition:box-shadow .15s,border-color .15s;user-select:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.mm-node:hover{border-color:var(--accent);box-shadow:0 2px 8px rgba(33,118,174,0.15);}
.mm-node.mm-root{background:var(--accent);color:#fff;border-color:var(--accent);font-size:14px;font-weight:700;min-width:140px;padding:12px 20px;border-radius:10px;}
.mm-node.mm-selected{border-color:var(--accent);box-shadow:0 0 0 3px rgba(33,118,174,0.25);}
.mm-node .mm-collapse{position:absolute;right:-7px;top:50%;transform:translateY(-50%);width:14px;height:14px;border-radius:50%;background:#fff;border:1.5px solid #90a4ae;font-size:10px;line-height:12px;text-align:center;cursor:pointer;color:#546e7a;z-index:5;}
.mm-node .mm-collapse:hover{background:var(--accent);border-color:var(--accent);color:#fff;}
.mm-node.mm-leaf .mm-collapse{display:none;}
.mm-ctx-menu{position:fixed;background:#fff;border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.12);z-index:3000;min-width:150px;display:none;padding:4px 0;}
.mm-ctx-menu.show{display:block;}
.mm-ctx-menu .mm-ctx-item{padding:8px 16px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .1s;}
.mm-ctx-menu .mm-ctx-item:hover{background:#f0f6ff;color:var(--accent);}
.mm-ctx-menu .mm-ctx-divider{height:1px;background:var(--border);margin:4px 0;}
.mm-node-edit-input{position:absolute;z-index:100;border:2px solid var(--accent);border-radius:6px;padding:4px 8px;font-size:12px;outline:none;min-width:100px;box-shadow:0 2px 12px rgba(33,118,174,0.2);}
.mm-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;background:#e8f8e0;color:#1a7a3c;margin-left:6px;}
/* ========== Server Config & Warning ========== */
.server-warning{background:#fff3cd;border:1px solid #ffc107;border-left:4px solid var(--danger);border-radius:4px;padding:8px 14px;margin-bottom:10px;font-size:12px;display:flex;align-items:center;gap:8px;}
.server-warning .sw-icon{font-size:18px;}
.server-warning .sw-text{flex:1;color:#856404;}
.server-warning .sw-btn{background:var(--danger);color:#fff;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;white-space:nowrap;}
.server-info{background:#e8f8f0;border:1px solid #a3d9b1;border-radius:4px;padding:6px 14px;margin-bottom:10px;font-size:11px;display:flex;align-items:center;gap:8px;color:#1a7a3c;}
.draw-file-upload{border:2px dashed var(--border);border-radius:6px;padding:12px;text-align:center;background:#fafbfc;margin-bottom:10px;}
.draw-file-upload.has-file{border-color:var(--accent);background:#f0f6ff;}
.draw-file-upload .dfu-hint{font-size:11px;color:var(--text2);margin-top:4px;}
.draw-file-upload .dfu-server-tag{display:inline-block;background:var(--accent);color:#fff;padding:2px 8px;border-radius:3px;font-size:9px;font-weight:600;margin-top:4px;}
.draw-file-name{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--accent);}
.draw-file-name .dfn-icon{font-size:16px;}
.draw-file-link{font-size:10px;color:var(--text2);word-break:break-all;margin-top:2px;}
/* ========== Auto Comparison ========== */
.cmp-panel{background:#f8fafc;border:1px solid var(--border);border-radius:8px;padding:14px;margin-top:12px;}
.cmp-panel h4{margin:0 0 4px 0;font-size:14px;}
.cmp-panel .cmp-subtitle{font-size:11px;color:var(--text2);margin-bottom:10px;}
.cmp-group{margin-bottom:14px;border:1px solid #e2e8f0;border-radius:6px;overflow:hidden;}
.cmp-group-header{background:#f1f5f9;padding:8px 12px;font-size:12px;font-weight:700;display:flex;align-items:center;gap:8px;justify-content:space-between;}
.cmp-group-header .gh-count{font-size:10px;color:var(--text2);font-weight:400;}
.cmp-card{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid #f1f5f9;font-size:12px;transition:background .15s;}
.cmp-card:last-child{border-bottom:none;}
.cmp-card:hover{background:#f8fafc;}
.cmp-card.winner{background:#f0fdf4;border-left:3px solid var(--success);}
.cmp-card .cc-rank{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:12px;flex-shrink:0;}
.cmp-card .cc-rank.r1{background:var(--success);color:#fff;}
.cmp-card .cc-rank.r2{background:#e2e8f0;color:#64748b;}
.cmp-card .cc-rank.r3{background:#f1f5f9;color:#94a3b8;}
.cmp-card .cc-rank.rx{background:#f8fafc;color:#cbd5e1;}
.cmp-card .cc-info{flex:1;min-width:0;}
.cmp-card .cc-supplier{font-weight:700;color:var(--text);}
.cmp-card .cc-item{font-size:10px;color:var(--text2);}
.cmp-card .cc-metrics{display:flex;gap:16px;flex-shrink:0;}
.cmp-card .cc-metric{text-align:center;}
.cmp-card .cc-metric .val{font-weight:700;font-size:14px;}
.cmp-card .cc-metric .lbl{font-size:9px;color:var(--text2);}
.cmp-card .cc-score{width:52px;height:52px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:800;flex-shrink:0;}
.cmp-card .cc-score.high{background:#dcfce7;color:#166534;border:2px solid var(--success);}
.cmp-card .cc-score.med{background:#fef9c3;color:#854d0e;border:2px solid #facc15;}
.cmp-card .cc-score.low{background:#fee2e2;color:#991b1b;border:2px solid #fca5a5;}
.cmp-card .cc-action{flex-shrink:0;}
.cmp-bar-group{margin-bottom:8px;}
.cmp-bar-label{display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px;}
.cmp-bar-track{height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden;}
.cmp-bar-fill{height:100%;border-radius:4px;transition:width .4s;}
.cmp-bar-fill.price-bar{background:var(--accent);}
.cmp-bar-fill.delivery-bar{background:var(--warning);}
.cmp-bar-fill.warranty-bar{background:var(--success);}
.cmp-summary{display:flex;gap:10px;margin-top:12px;}
.cmp-summary-item{flex:1;padding:8px 12px;background:#fff;border:1px solid var(--border);border-radius:6px;font-size:11px;}
.cmp-summary-item .csi-val{font-size:20px;font-weight:800;color:var(--accent);}
.cmp-summary-item .csi-lbl{font-size:10px;color:var(--text2);}
/* ========== Site Modification Media Upload ========== */
.sm-media-section{margin-top:6px;}
.sm-media-section label{font-weight:600;font-size:12px;margin-bottom:4px;display:block;}
.sm-upload-zone{border:2px dashed var(--border);border-radius:6px;padding:10px;text-align:center;background:#fafbfc;cursor:pointer;transition:all .2s;margin-bottom:6px;}
.sm-upload-zone:hover{border-color:var(--accent);background:#f0f6ff;}
.sm-upload-zone .uz-icon{font-size:24px;margin-bottom:2px;}
.sm-upload-zone .uz-text{font-size:11px;color:var(--text2);}
.sm-upload-zone .uz-hint{font-size:9px;color:#aaa;margin-top:2px;}
.sm-media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:6px;margin-bottom:6px;}
.sm-media-thumb{position:relative;border:1px solid var(--border);border-radius:4px;overflow:hidden;background:#f5f5f5;}
.sm-media-thumb img{width:100%;height:60px;object-fit:cover;display:block;}
.sm-media-thumb .sm-thumb-name{font-size:9px;padding:2px 4px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sm-media-thumb .sm-thumb-remove{position:absolute;top:2px;right:2px;width:18px;height:18px;background:rgba(220,38,38,0.85);color:#fff;border:none;border-radius:50%;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;padding:0;}
.sm-video-item{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--border);border-radius:4px;margin-bottom:4px;font-size:11px;background:#fff;}
.sm-video-item .vi-icon{font-size:16px;}
.sm-video-item .vi-info{flex:1;min-width:0;}
.sm-video-item .vi-name{font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sm-video-item .vi-size{font-size:9px;color:var(--text2);}
.sm-video-item .vi-remove{cursor:pointer;color:var(--danger);font-size:14px;line-height:1;}
.sm-media-viewer{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;}
.sm-media-viewer .mv-image{border:1px solid var(--border);border-radius:6px;overflow:hidden;background:#f5f5f5;}
.sm-media-viewer .mv-image img{width:100%;height:140px;object-fit:cover;display:block;}
.sm-media-viewer .mv-image .mv-caption{font-size:10px;padding:4px 8px;color:var(--text2);text-align:center;}
.sm-media-viewer .mv-video{display:flex;align-items:center;justify-content:center;flex-direction:column;border:1px solid var(--border);border-radius:6px;height:140px;background:#1a1a2e;color:#ccc;gap:6px;}
.sm-media-viewer .mv-video .mv-icon{font-size:36px;}
.sm-media-viewer .mv-video .mv-name{font-size:10px;text-align:center;padding:0 6px;word-break:break-all;}
.sm-media-viewer .mv-video .mv-size{font-size:9px;color:#888;}
.tag-media{cursor:pointer;display:inline-flex;align-items:center;gap:3px;font-size:10px;padding:3px 8px;border-radius:10px;font-weight:600;transition:all .15s;}
.tag-media:hover{opacity:.8;transform:scale(1.05);}
.tag-media-img{background:#e8f0fe;color:#1a56db;}
.tag-media-vid{background:#fef3c7;color:#92400e;}
.tag-media-none{background:#f3f4f6;color:#9ca3af;cursor:default;}
.tag-media-none:hover{opacity:1;transform:none;}
/* ========== Responsive ========== */
@media(max-width:768px){
.sidebar{width:50px;min-width:50px;}
.sidebar .sidebar-item span,.sidebar-section,.sidebar-header span,.sidebar-footer{display:none;}
.sidebar-item{justify-content:center;padding:8px 0;}
}
</style>
</head>
<body>
<!-- ========== Sidebar ========== -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<span class="logo">⚙️</span>
<span>设备总包项目管理</span>
</div>
<div class="sidebar-section">项目管理</div>
<div class="sidebar-item active" data-module="dashboard" onclick="switchModule('dashboard')">
<span class="icon">📊</span><span>项目总览</span>
</div>
<div class="sidebar-item" data-module="budget" onclick="switchModule('budget')">
<span class="icon">💰</span><span>项目预算</span>
</div>
<div class="sidebar-section">技术与设计</div>
<div class="sidebar-item" data-module="tech_plan" onclick="switchModule('tech_plan')">
<span class="icon">📋</span><span>技术方案确定</span>
</div>
<div class="sidebar-item" data-module="layout" onclick="switchModule('layout')">
<span class="icon">🗺️</span><span>布局图确定</span>
</div>
<div class="sidebar-item" data-module="tech_review" onclick="switchModule('tech_review')">
<span class="icon">🔍</span><span>技术审查</span>
</div>
<div class="sidebar-item" data-module="drawing_design" onclick="switchModule('drawing_design')">
<span class="icon">📐</span><span>图纸详细设计</span>
</div>
<div class="sidebar-item" data-module="drawing_review" onclick="switchModule('drawing_review')">
<span class="icon">✏️</span><span>图纸审查</span>
</div>
<div class="sidebar-section">采购与合同</div>
<div class="sidebar-item" data-module="procurement" onclick="switchModule('procurement')">
<span class="icon">🛒</span><span>采购管理</span>
</div>
<div class="sidebar-item" data-module="manufacturing" onclick="switchModule('manufacturing')">
<span class="icon">🏭</span><span>设备制造进度</span>
</div>
<div class="sidebar-section">图纸与资料</div>
<div class="sidebar-item" data-module="drawing_compare" onclick="switchModule('drawing_compare')">
<span class="icon">🔄</span><span>图纸优化比较</span>
</div>
<div class="sidebar-item" data-module="doc_lib" onclick="switchModule('doc_lib')">
<span class="icon">📁</span><span>图纸资料库</span>
</div>
<div class="sidebar-item" data-module="site_mod" onclick="switchModule('site_mod')">
<span class="icon">🔧</span><span>现场修改管理</span>
</div>
<div class="sidebar-section">发货与安装</div>
<div class="sidebar-item" data-module="shipping" onclick="switchModule('shipping')">
<span class="icon">📦</span><span>发货前清单</span>
</div>
<div class="sidebar-item" data-module="manuals" onclick="switchModule('manuals')">
<span class="icon">📖</span><span>设备说明书</span>
</div>
<div class="sidebar-item" data-module="install_prep" onclick="switchModule('install_prep')">
<span class="icon">🛠️</span><span>安装前准备</span>
</div>
<div class="sidebar-item" data-module="install_feedback" onclick="switchModule('install_feedback')">
<span class="icon">💬</span><span>安装问题反馈</span>
</div>
<div class="sidebar-item" data-module="acceptance" onclick="switchModule('acceptance')">
<span class="icon"></span><span>安装后验收</span>
</div>
<div class="sidebar-item" data-module="hot_commissioning" onclick="switchModule('hot_commissioning')">
<span class="icon">🔥</span><span>热负荷试车</span>
</div>
<div class="sidebar-footer">连轧机/可逆轧机设备总包管理系统 v1.0</div>
</div>
<!-- ========== Main Content ========== -->
<div class="main">
<div class="topbar">
<div>
<div class="title" id="topbar-title">项目总览</div>
<div class="breadcrumb" id="topbar-breadcrumb">首页 / 项目总览</div>
</div>
<div class="topbar-right">
<button class="topbar-btn" onclick="exportProjectReport()">📄 导出项目报告</button>
<button class="topbar-btn" onclick="saveData()">💾 保存数据</button>
</div>
</div>
<div class="content" id="main-content">
<!-- Dashboard loaded by default -->
</div>
</div>
<!-- ========== Modal ========== -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal-box">
<div class="modal-header">
<span id="modal-title">标题</span>
<span style="cursor:pointer;font-size:18px;" onclick="closeModal()"></span>
</div>
<div class="modal-body" id="modal-body">内容</div>
</div>
</div>
<!-- ========== Mind Map Overlay ========== -->
<div class="mm-overlay" id="mm-overlay">
<div class="mm-container">
<div class="mm-toolbar" id="mm-toolbar">
<span class="mm-title">🧠 思维导图编辑器</span>
<span style="font-size:11px;color:var(--text2);" id="mm-item-name"></span>
<span class="mm-sep"></span>
<button class="btn btn-sm btn-primary" onclick="mmAddChild()" title="添加子节点 (Tab)"> 子节点</button>
<button class="btn btn-sm btn-outline" onclick="mmAddSibling()" title="添加兄弟节点 (Enter)">↪ 兄弟节点</button>
<button class="btn btn-sm btn-outline" onclick="mmEditNode()" title="编辑文字 (F2)">✏️ 编辑</button>
<button class="btn btn-sm btn-outline" onclick="mmDeleteNode()" title="删除节点 (Delete)">🗑 删除</button>
<span class="mm-sep"></span>
<button class="btn btn-sm btn-outline" onclick="mmCollapseNode()" title="折叠/展开">📂 折叠</button>
<button class="btn btn-sm btn-outline" onclick="mmResetView()" title="重置视图">🔄 居中</button>
<button class="btn btn-sm btn-outline" onclick="mmZoom(0.1)">🔍+</button>
<button class="btn btn-sm btn-outline" onclick="mmZoom(-0.1)">🔍−</button>
<span style="flex:1;"></span>
<span style="font-size:10px;color:var(--text2);" id="mm-zoom-label">100%</span>
<span class="mm-sep"></span>
<button class="btn btn-sm btn-success" onclick="mmSave()">💾 保存</button>
<button class="btn btn-sm btn-outline" onclick="mmClose()">✕ 关闭</button>
</div>
<div class="mm-canvas-wrap" id="mm-canvas-wrap">
<div class="mm-canvas" id="mm-canvas">
<svg id="mm-svg"></svg>
</div>
</div>
</div>
</div>
<div class="mm-ctx-menu" id="mm-ctx-menu">
<div class="mm-ctx-item" onclick="mmAddChild()"> 添加子节点</div>
<div class="mm-ctx-item" onclick="mmAddSibling()">↪ 添加兄弟节点</div>
<div class="mm-ctx-item" onclick="mmEditNode()">✏️ 编辑文字</div>
<div class="mm-ctx-divider"></div>
<div class="mm-ctx-item" onclick="mmCollapseNode()">📂 折叠/展开</div>
<div class="mm-ctx-item" onclick="mmDeleteNode()" style="color:var(--danger);">🗑 删除节点</div>
</div>
<script>
// ========== Data Store ==========
const STORAGE_KEY = 'rolling_mill_project_data';
let projectData = loadData();
function loadData() {
const d = localStorage.getItem(STORAGE_KEY);
if (d) {
const data = JSON.parse(d);
// Migrate old string-based colorCard to array
if (data.techReview && typeof data.techReview.colorCard === 'string') {
const oldText = data.techReview.colorCard;
if (oldText && oldText.trim()) {
const lines = oldText.split('\n').filter(l=>l.trim());
data.techReview.colorCard = lines.map(line=>{
const match = line.match(/^(.+?)[:]\s*(.+)$/);
if (match) {
const name = match[1].trim();
const rest = match[2].trim();
const ralMatch = rest.match(/RAL\s*(\d+)/i);
const std = ralMatch ? 'RAL '+ralMatch[1] : '';
return { colorName:name, hex:'#C8CBCE', standard:std, standardLabel:ralMatch?'RAL':'其他', usage:'', desc:rest };
}
return { colorName:line, hex:'#C8CBCE', standard:'', standardLabel:'其他', usage:'', desc:'' };
});
} else {
data.techReview.colorCard = [];
}
}
return data;
}
return getDefaultData();
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(projectData));
showToast('数据已保存');
}
function getDefaultData() {
return {
projectInfo: { name: '1380mm六辊可逆轧机设备总包项目', number: 'DRF-2026-001', client: '昆山德睿福成套设备有限公司', startDate: '2026-06-01', endDate: '2027-02-28', manager: '工程师', contractNos: [] },
budget: [], techPlan: { status: 'pending', items: [] },
layout: { status: 'pending', files: [] },
techReview: { mechanical: [], electrical: [], hydraulic: [], fluid: [], energy: [], colorCard: [
// 流体管路
{ colorName:'冷却水管', hex:'#00773E', standard:'GB 7231', standardLabel:'GB', category:'流体管路', usage:'冷却水', desc:'艳绿色,冷却水管道标识' },
{ colorName:'润滑油管', hex:'#8C6820', standard:'GB 7231', standardLabel:'GB', category:'流体管路', usage:'润滑油', desc:'棕黄色,润滑油管道标识' },
{ colorName:'液压管路', hex:'#005A8C', standard:'RAL 5005', standardLabel:'RAL', category:'流体管路', usage:'液压油', desc:'信号蓝,液压管路及接头标识' },
{ colorName:'乳化液管', hex:'#E5E5E5', standard:'RAL 7035', standardLabel:'RAL', category:'流体管路', usage:'乳化液', desc:'浅灰色,乳化液供回管路' },
// 机械-动力
{ colorName:'主传动轴', hex:'#F0C700', standard:'RAL 1023', standardLabel:'RAL', category:'机械-动力', usage:'主传动', desc:'交通黄,万向接轴及联轴器护罩' },
{ colorName:'齿轮箱', hex:'#D0021B', standard:'RAL 3002', standardLabel:'RAL', category:'机械-动力', usage:'传动齿轮箱', desc:'胭脂红,主减速机及分配齿轮箱' },
// 机械-被动
{ colorName:'轧机牌坊', hex:'#C8CBCE', standard:'RAL 7035', standardLabel:'RAL', category:'机械-被动', usage:'设备主体框架', desc:'浅灰色,轧机牌坊、机架主体涂装' },
{ colorName:'卷取机', hex:'#4A4A4A', standard:'RAL 7021', standardLabel:'RAL', category:'机械-被动', usage:'卷取设备', desc:'黑灰色,卷取机机架及底座' },
{ colorName:'防护罩', hex:'#FF6B00', standard:'RAL 2004', standardLabel:'RAL', category:'机械-被动', usage:'安全防护', desc:'纯橙色,安全防护罩及护栏' },
// 液压缸
{ colorName:'AGC液压缸', hex:'#9013FE', standard:'企业标准', standardLabel:'其他', category:'液压缸', usage:'AGC压下缸', desc:'紫色AGC压下液压缸及伺服阀块' },
{ colorName:'弯辊缸', hex:'#4A90D9', standard:'企业标准', standardLabel:'其他', category:'液压缸', usage:'弯辊液压缸', desc:'蓝色,正负弯辊缸及控制阀组' },
// 电机
{ colorName:'主传动电机', hex:'#C1121C', standard:'RAL 3002', standardLabel:'RAL', category:'电机', usage:'主电机', desc:'胭脂红,主传动直流/交流电机' },
{ colorName:'辅助电机', hex:'#F5A623', standard:'RAL 1028', standardLabel:'RAL', category:'电机', usage:'辅助传动', desc:'橙黄色,开卷/卷取辅助电机' },
// 减速机
{ colorName:'主减速机', hex:'#1A1A1A', standard:'RAL 9005', standardLabel:'RAL', category:'减速机', usage:'主减速', desc:'深黑色,主减速机机体' },
{ colorName:'分配减速机', hex:'#8B572A', standard:'RAL 8004', standardLabel:'RAL', category:'减速机', usage:'分配传动', desc:'铜棕色,齿轮分配箱' }
] },
drawingDesign: [],
drawingReview: [],
serverConfig: { drawingServerUrl: '' },
procurement: { quotes: [], contracts: [], comparisons: [], progress: [] },
manufacturing: [],
drawingCompare: [],
docLib: [],
siteMod: [],
shipping: { checklist: getDefaultChecklist('shipping'), items: [] },
manuals: [],
installPrep: { tools: [
{ name:'桥式起重机', nameEn:'Overhead Crane', spec:'≥50t/20t双梁', qty:'1', unit:'台', unitPrice:'', totalPrice:'', priority:'★★', arrivalDate:'', purpose:'牌坊/主传动大部件吊装', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'液压扁吊带', nameEn:'Flat Web Sling', spec:'SWL 20t, L=8m', qty:'4', unit:'根', unitPrice:'1200', totalPrice:'4800', priority:'★★', arrivalDate:'', purpose:'辊系/AGC缸体吊装防划伤', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'链式手拉葫芦', nameEn:'Chain Block Hoist', spec:'5t/3t各2台', qty:'4', unit:'台', unitPrice:'2800', totalPrice:'11200', priority:'★', arrivalDate:'', purpose:'辅助就位、微调找正', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'卸扣', nameEn:'Shackle', spec:'5t/10t/20t各4套', qty:'12', unit:'个', unitPrice:'350', totalPrice:'4200', priority:'★', arrivalDate:'', purpose:'吊具连接', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'撬杠', nameEn:'Crow Bar', spec:'L=1500mm', qty:'4', unit:'根', unitPrice:'180', totalPrice:'720', priority:'', arrivalDate:'', purpose:'设备就位微调', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'液压千斤顶', nameEn:'Hydraulic Jack', spec:'100t/50t/20t各2台', qty:'6', unit:'台', unitPrice:'4500', totalPrice:'27000', priority:'★', arrivalDate:'', purpose:'设备顶升就位', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'机械千斤顶', nameEn:'Mechanical Jack', spec:'10t, 螺旋式', qty:'8', unit:'台', unitPrice:'680', totalPrice:'5440', priority:'', arrivalDate:'', purpose:'设备支撑微调', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'钢丝绳索具', nameEn:'Wire Rope Sling', spec:'φ32mm, L=6m', qty:'4', unit:'根', unitPrice:'1500', totalPrice:'6000', priority:'★', arrivalDate:'', purpose:'重型部件吊装', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'锚链', nameEn:'Anchor Chain', spec:'φ28mm Grade 80', qty:'20', unit:'m', unitPrice:'65', totalPrice:'1300', priority:'', arrivalDate:'', purpose:'捆绑固定', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'倒链(5T)', nameEn:'Chain Hoist 5T', spec:'电动/手动各1台', qty:'2', unit:'台', unitPrice:'3200', totalPrice:'6400', priority:'★', arrivalDate:'', purpose:'牌坊翻身/辅助就位', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'钢丝绳夹', nameEn:'Wire Rope Clip', spec:'φ32, 配套', qty:'20', unit:'个', unitPrice:'35', totalPrice:'700', priority:'', arrivalDate:'', purpose:'钢丝绳固定连接', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'吊装专用横梁', nameEn:'Lifting Beam', spec:'定制, SWL 30t', qty:'1', unit:'套', unitPrice:'8500', totalPrice:'8500', priority:'★', arrivalDate:'', purpose:'辊架整体吊装', responsible:'赵大勇', status:'待确认', category:'起重吊装', remark:'' },
{ name:'激光跟踪仪', nameEn:'Laser Tracker', spec:'API/Leica, 精度±0.02mm', qty:'1', unit:'套', unitPrice:'85000', totalPrice:'85000', priority:'★★', arrivalDate:'', purpose:'牌坊/轧辊系统三维找正', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'激光对中仪', nameEn:'Laser Shaft Alignment', spec:'Rotalign Ultra, 精度±0.005mm', qty:'1', unit:'套', unitPrice:'42000', totalPrice:'42000', priority:'★★', arrivalDate:'', purpose:'主电机/减速机/主轧辊对中', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'全站仪', nameEn:'Total Station', spec:'Leica TS09, 精度2″', qty:'1', unit:'台', unitPrice:'38000', totalPrice:'38000', priority:'★★', arrivalDate:'', purpose:'基础中心线/轴线精密测量', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'水准仪(精密)', nameEn:'Precision Level', spec:'DS1, 最小读数0.01mm', qty:'2', unit:'台', unitPrice:'8500', totalPrice:'17000', priority:'★★', arrivalDate:'', purpose:'设备基准水平度测量', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'框式水平仪', nameEn:'Frame Level', spec:'精度0.02mm/m, 200mm', qty:'4', unit:'块', unitPrice:'3200', totalPrice:'12800', priority:'★', arrivalDate:'', purpose:'设备就位水平度检测', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'合像水平仪', nameEn:'Coincidence Level', spec:'精度0.01mm/m', qty:'2', unit:'台', unitPrice:'5600', totalPrice:'11200', priority:'★★', arrivalDate:'', purpose:'导轨/轴承座水平精密检测', responsible:'刘明远', status:'待确认', category:'测量仪器', remark:'' },
{ name:'液压扳手(进口)', nameEn:'Hydraulic Torque Wrench', spec:'750-4000N·m套装', qty:'1', unit:'套', unitPrice:'28000', totalPrice:'28000', priority:'★★', arrivalDate:'', purpose:'地脚螺栓/压下螺栓高精扭矩', responsible:'赵大勇', status:'待确认', category:'机械安装', remark:'' },
{ name:'扭矩扳手套装', nameEn:'Torque Wrench Set', spec:'40-1200N·m, 各级', qty:'1', unit:'套', unitPrice:'6500', totalPrice:'6500', priority:'★★', arrivalDate:'', purpose:'关键螺栓扭矩控制', responsible:'赵大勇', status:'待确认', category:'机械安装', remark:'' },
{ name:'液压拉伸器', nameEn:'Hydraulic Bolt Tensioner', spec:'M100, 配套', qty:'2', unit:'套', unitPrice:'35000', totalPrice:'70000', priority:'★★', arrivalDate:'', purpose:'主传动联接螺栓超高精度拉伸', responsible:'赵大勇', status:'待确认', category:'机械安装', remark:'' },
{ name:'轴承加热器', nameEn:'Bearing Heater', spec:'感应式, 最大φ500mm', qty:'2', unit:'台', unitPrice:'12000', totalPrice:'24000', priority:'★★', arrivalDate:'', purpose:'大型轴承热装(加热至100~120℃)', responsible:'钱小刚', status:'待确认', category:'机械安装', remark:'' },
{ name:'液压拆卸工具', nameEn:'Hydraulic Puller', spec:'50t, 配合油泵', qty:'1', unit:'套', unitPrice:'18000', totalPrice:'18000', priority:'★★', arrivalDate:'', purpose:'大型轴承/齿轮拆卸', responsible:'钱小刚', status:'待确认', category:'机械安装', remark:'' },
{ name:'平行垫铁', nameEn:'Parallel Shim Plate', spec:'各规格齐全', qty:'1', unit:'套', unitPrice:'15000', totalPrice:'15000', priority:'★★', arrivalDate:'', purpose:'设备标高调整', responsible:'赵大勇', status:'待确认', category:'机械安装', remark:'' },
{ name:'不锈钢垫片组', nameEn:'SS Shim Set', spec:'0.05~2mm, 各规格', qty:'5', unit:'套', unitPrice:'1200', totalPrice:'6000', priority:'★★', arrivalDate:'', purpose:'精密调整间隙', responsible:'刘明远', status:'待确认', category:'机械安装', remark:'' },
{ name:'液压管路冲洗泵站', nameEn:'Hydraulic Flushing Unit', spec:'流量300L/min, 过滤β10≥200', qty:'1', unit:'套', unitPrice:'65000', totalPrice:'65000', priority:'★★', arrivalDate:'', purpose:'系统投运前管路冲洗至NAS7级', responsible:'孙师傅', status:'待确认', category:'液压专用', remark:'' },
{ name:'液压系统检测仪', nameEn:'Hydraulic Tester', spec:'压力/流量/温度一体', qty:'1', unit:'台', unitPrice:'22000', totalPrice:'22000', priority:'★★', arrivalDate:'', purpose:'系统压力/流量调试检测', responsible:'孙师傅', status:'待确认', category:'液压专用', remark:'' },
{ name:'耐压试验台', nameEn:'Hydraulic Pressure Test Bench', spec:'最高280bar', qty:'1', unit:'台', unitPrice:'38000', totalPrice:'38000', priority:'★★', arrivalDate:'', purpose:'管路焊接后耐压试验', responsible:'孙师傅', status:'待确认', category:'液压专用', remark:'' },
{ name:'油液颗粒计数仪', nameEn:'Particle Counter', spec:'NAS/ISO双标准', qty:'1', unit:'台', unitPrice:'18000', totalPrice:'18000', priority:'★★', arrivalDate:'', purpose:'冲洗清洁度检测', responsible:'孙师傅', status:'待确认', category:'液压专用', remark:'' },
{ name:'绝缘电阻测试仪', nameEn:'Insulation Resistance Tester', spec:'Fluke 1555, 10kV', qty:'2', unit:'台', unitPrice:'12000', totalPrice:'24000', priority:'★★', arrivalDate:'', purpose:'电机/变压器绝缘电阻测试', responsible:'袁胜杰', status:'待确认', category:'电气安装', remark:'' },
{ name:'接地电阻测试仪', nameEn:'Earth Resistance Tester', spec:'Fluke 1623-2, 4线', qty:'1', unit:'台', unitPrice:'8500', totalPrice:'8500', priority:'★★', arrivalDate:'', purpose:'接地系统电阻值测量', responsible:'袁胜杰', status:'待确认', category:'电气安装', remark:'' },
{ name:'钳形电流表', nameEn:'Clamp Meter', spec:'Fluke 376, 1000A', qty:'4', unit:'台', unitPrice:'2800', totalPrice:'11200', priority:'★', arrivalDate:'', purpose:'运行电流在线测量', responsible:'袁胜杰', status:'待确认', category:'电气安装', remark:'' },
{ name:'相序表', nameEn:'Phase Sequence Meter', spec:'三相相序检测', qty:'2', unit:'个', unitPrice:'680', totalPrice:'1360', priority:'★', arrivalDate:'', purpose:'三相电源相序核实', responsible:'袁胜杰', status:'待确认', category:'电气安装', remark:'' }
], personnel: [
{ name:'陈志强', position:'项目经理', positionEn:'Project Manager', planIn:'D+0(开工)', planOut:'D+82', days:'82', dailyRate:'1500', totalWages:'123000', duty:'全局协调/进度管控', qualification:'一级建造师/中级职称', phone:'', remark:'' },
{ name:'刘明远', position:'技术负责人', positionEn:'Technical Lead', planIn:'D+0(开工)', planOut:'D+82', days:'82', dailyRate:'1200', totalWages:'98400', duty:'技术方案/质量基准', qualification:'高级工程师/10年经验', phone:'', remark:'' },
{ name:'马安全', position:'安全质量员', positionEn:'HSE/QA Officer', planIn:'D+0(开工)', planOut:'D+82', days:'82', dailyRate:'800', totalWages:'65600', duty:'安全管理/质量监督', qualification:'安全证+质检证', phone:'', remark:'' },
{ name:'赵大勇', position:'钳工班长', positionEn:'Fitter Foreman', planIn:'D+3', planOut:'D+78', days:'75', dailyRate:'950', totalWages:'71250', duty:'机械安装总负责', qualification:'高级钳工/8年经验', phone:'', remark:'' },
{ name:'钱小刚', position:'精密钳工(1)', positionEn:'Precision Fitter I', planIn:'D+5', planOut:'D+60', days:'55', dailyRate:'850', totalWages:'46750', duty:'辊系/AGC精密安装', qualification:'高级钳工', phone:'', remark:'' },
{ name:'孙德福', position:'精密钳工(2)', positionEn:'Precision Fitter II', planIn:'D+5', planOut:'D+60', days:'55', dailyRate:'850', totalWages:'46750', duty:'主传动/减速机对中', qualification:'高级钳工', phone:'', remark:'' },
{ name:'孙师傅', position:'液压工程师', positionEn:'Hydraulic Engr', planIn:'D+20', planOut:'D+75', days:'55', dailyRate:'1100', totalWages:'60500', duty:'液压系统安装调试', qualification:'液压工程师证', phone:'', remark:'' },
{ name:'袁胜杰', position:'电工班长', positionEn:'Electrician Foreman', planIn:'D+12', planOut:'D+80', days:'68', dailyRate:'900', totalWages:'61200', duty:'电气线路安装总负责', qualification:'电工证/班长', phone:'', remark:'' },
{ name:'王瑞春', position:'自控调试工程师', positionEn:'Automation Engr', planIn:'D+45', planOut:'D+82', days:'37', dailyRate:'1500', totalWages:'55500', duty:'PLC/L1控制调试', qualification:'自控工程师/5年经验', phone:'', remark:'' },
{ name:'禾望', position:'传动调试工程师', positionEn:'Drive Engr', planIn:'D+48', planOut:'D+82', days:'34', dailyRate:'1400', totalWages:'47600', duty:'变频器/主传动调试', qualification:'传动工程师', phone:'', remark:'' },
{ name:'杨喆', position:'轧制工艺工程师', positionEn:'Process Engr', planIn:'D+55', planOut:'D+82', days:'27', dailyRate:'1300', totalWages:'35100', duty:'轧制工艺/首次穿带', qualification:'工艺工程师', phone:'', remark:'' },
{ name:'陈首斌', position:'OEM代表(机)', positionEn:'Mill OEM Mech Rep', planIn:'D+10', planOut:'D+60', days:'50', dailyRate:'2000', totalWages:'100000', duty:'机械安装技术指导', qualification:'OEM认证工程师', phone:'', remark:'' },
{ name:'张伟', position:'OEM代表(电)', positionEn:'Mill OEM Elec Rep', planIn:'D+30', planOut:'D+75', days:'45', dailyRate:'2000', totalWages:'90000', duty:'电气调试技术指导', qualification:'OEM认证工程师', phone:'', remark:'' }
], precision: [
{ system:'轧辊系统', item:'工作辊辊径偏差', nameEn:'WR diameter deviation', target:'±0.005', unit:'mm', importance:'★★★', tool:'外径千分尺', method:'多点等距测量', standard:'SMS规范', requirement:'±0.005mm', actual:'', ok:false, photos:[] },
{ system:'轧辊系统', item:'中间辊/支承辊辊径偏差', nameEn:'IMR/BR diameter deviation', target:'±0.010', unit:'mm', importance:'★★', tool:'外径千分尺', method:'多点等距测量', standard:'SMS规范', requirement:'±0.010mm', actual:'', ok:false, photos:[] },
{ system:'轧辊系统', item:'工作辊轴承座安装间隙', nameEn:'WR chock install clearance', target:'0.10~0.30', unit:'mm', importance:'★★★', tool:'塞尺/内径表', method:'直接测量', standard:'SMS规范', requirement:'0.10~0.30mm', actual:'', ok:false, photos:[] },
{ system:'轧辊系统', item:'工作辊辊缝平行度', nameEn:'WR roll gap parallelism', target:'0.05', unit:'mm', importance:'★★★', tool:'激光跟踪仪', method:'三维坐标法', standard:'SMS规范', requirement:'0.05mm', actual:'', ok:false, photos:[] },
{ system:'AGC系统', item:'AGC缸安装中心偏差', nameEn:'AGC cylinder center offset', target:'±0.10', unit:'mm', importance:'★★★', tool:'激光跟踪仪', method:'激光测量', standard:'SMS规范', requirement:'±0.10mm', actual:'', ok:false, photos:[] },
{ system:'AGC系统', item:'AGC缸垂直度', nameEn:'AGC cylinder verticality', target:'0.05/1000', unit:'mm/m', importance:'★★★', tool:'合像水平仪', method:'水平仪法', standard:'SMS规范', requirement:'0.05/1000', actual:'', ok:false, photos:[] },
{ system:'主机框架', item:'机架标高误差', nameEn:'Stand elevation error', target:'±1.0', unit:'mm', importance:'★★★', tool:'精密水准仪', method:'水准仪法', standard:'GB 50231', requirement:'±1.0mm', actual:'', ok:false, photos:[] },
{ system:'主机框架', item:'轧制中心线偏差', nameEn:'Rolling pass line deviation', target:'±0.5', unit:'mm', importance:'★★★', tool:'激光跟踪仪', method:'激光测量', standard:'SMS规范', requirement:'±0.5mm', actual:'', ok:false, photos:[] },
{ system:'主机框架', item:'机架纵向水平度', nameEn:'Stand longitudinal levelness', target:'0.10/1000', unit:'mm/m', importance:'★★', tool:'精密水准仪', method:'水准仪法', standard:'GB 50231', requirement:'0.10/1000', actual:'', ok:false, photos:[] },
{ system:'液压系统', item:'管路冲洗清洁度', nameEn:'Pipe flushing cleanliness', target:'NAS 7级', unit:'—', importance:'★★★', tool:'颗粒计数仪', method:'取样检测', standard:'NAS1638', requirement:'NAS 7级', actual:'', ok:false, photos:[] },
{ system:'液压系统', item:'系统额定压力试验', nameEn:'System rated pressure test', target:'1.5倍额定压力', unit:'MPa', importance:'★★★', tool:'压力表/记录仪', method:'保压30分钟', standard:'GB/T 3766', requirement:'1.5倍额定压力', actual:'', ok:false, photos:[] },
{ system:'电气系统', item:'电缆绝缘电阻', nameEn:'Cable insulation resistance', target:'≥10MΩ(1kV)', unit:'MΩ', importance:'★★★', tool:'绝缘电阻测试仪', method:'兆欧表法', standard:'GB 50303', requirement:'≥10MΩ', actual:'', ok:false, photos:[] },
{ system:'电气系统', item:'接地电阻值', nameEn:'Grounding resistance', target:'≤4Ω', unit:'Ω', importance:'★★★', tool:'接地电阻测试仪', method:'4线法', standard:'GB 50303', requirement:'≤4Ω', actual:'', ok:false, photos:[] },
{ system:'电气系统', item:'电机绝缘电阻', nameEn:'Motor insulation resistance', target:'≥5MΩ(DC500V)', unit:'MΩ', importance:'★★★', tool:'绝缘电阻测试仪', method:'兆欧表法', standard:'GB 50150', requirement:'≥5MΩ', actual:'', ok:false, photos:[] },
{ system:'辅助设备', item:'卷取机芯轴水平度', nameEn:'Coiler mandrel levelness', target:'0.10/1000', unit:'mm/m', importance:'★★', tool:'水平仪', method:'水平仪法', standard:'SMS规范', requirement:'0.10/1000', actual:'', ok:false, photos:[] },
{ system:'安全装置', item:'安全防护罩安装', nameEn:'Safety guard installation', target:'间隙≥25mm', unit:'mm', importance:'★★★', tool:'直尺', method:'直接测量', standard:'GB 4053', requirement:'间隙≥25mm', actual:'', ok:false, photos:[] }
], progress: [] },
installFeedback: [],
acceptance: { checklist: getDefaultChecklist('acceptance'), items: [] },
hotCommissioning: { checklist: getDefaultChecklist('commissioning'), items: [], clauses: [] },
currentModule: 'dashboard'
};
}
function getDefaultChecklist(type) {
if (type === 'shipping') return [
{ text: '设备清单核对完成', checked: false },
{ text: '包装检验合格', checked: false },
{ text: '防锈处理确认', checked: false },
{ text: '发货单据齐全', checked: false },
{ text: '运输方案确认', checked: false }
];
if (type === 'acceptance') return [
{ text: '设备安装精度验收', checked: false },
{ text: '电气接线验收', checked: false },
{ text: '液压系统验收', checked: false },
{ text: '润滑系统验收', checked: false },
{ text: '安全护罩验收', checked: false },
{ text: '设备运转验收', checked: false }
];
if (type === 'commissioning') return [
{ text: '单体试车完成', checked: false },
{ text: '联动试车完成', checked: false },
{ text: '热负荷试车方案确认', checked: false },
{ text: '安全防护措施确认', checked: false },
{ text: '工艺参数设定确认', checked: false },
{ text: '操作人员培训完成', checked: false }
];
return [];
}
// ========== Toast ==========
function showToast(msg) {
let t = document.getElementById('toast');
if (!t) { t = document.createElement('div'); t.id = 'toast'; t.style.cssText = 'position:fixed;bottom:16px;right:16px;background:#333;color:#fff;padding:8px 16px;border-radius:5px;font-size:12px;z-index:9999;transition:opacity 0.3s;'; document.body.appendChild(t); }
t.textContent = msg; t.style.opacity = '1';
setTimeout(() => { t.style.opacity = '0'; }, 2000);
}
// ========== Module Switching ==========
const moduleTitles = {
dashboard:'项目总览', budget:'项目预算', tech_plan:'技术方案确定', layout:'布局图确定',
tech_review:'技术审查', drawing_design:'图纸详细设计', drawing_review:'图纸审查',
procurement:'采购管理', manufacturing:'设备制造进度', drawing_compare:'图纸优化比较',
doc_lib:'图纸资料库', site_mod:'现场修改管理', shipping:'发货前设备清单',
manuals:'设备说明书和图纸', install_prep:'设备安装前准备', install_feedback:'安装问题反馈',
acceptance:'安装后验收', hot_commissioning:'热负荷试车'
};
const moduleBreadcrumbs = {
dashboard:'首页 / 项目总览', budget:'首页 / 项目预算', tech_plan:'技术与设计 / 技术方案确定',
layout:'技术与设计 / 布局图确定', tech_review:'技术与设计 / 技术审查',
drawing_design:'技术与设计 / 图纸详细设计', drawing_review:'技术与设计 / 图纸审查',
procurement:'采购与合同 / 采购管理', manufacturing:'采购与合同 / 设备制造进度',
drawing_compare:'图纸与资料 / 图纸优化比较', doc_lib:'图纸与资料 / 图纸资料库',
site_mod:'图纸与资料 / 现场修改管理', shipping:'发货与安装 / 发货前清单',
manuals:'发货与安装 / 设备说明书', install_prep:'发货与安装 / 安装前准备',
install_feedback:'发货与安装 / 安装问题反馈', acceptance:'发货与安装 / 安装后验收',
hot_commissioning:'发货与安装 / 热负荷试车'
};
function switchModule(mod) {
document.querySelectorAll('.sidebar-item').forEach(el => el.classList.remove('active'));
const item = document.querySelector(`.sidebar-item[data-module="${mod}"]`);
if (item) item.classList.add('active');
projectData.currentModule = mod;
document.getElementById('topbar-title').textContent = moduleTitles[mod] || mod;
document.getElementById('topbar-breadcrumb').textContent = moduleBreadcrumbs[mod] || '';
renderModule(mod);
}
function renderModule(mod) {
const c = document.getElementById('main-content');
if (mod === 'dashboard') c.innerHTML = renderDashboard();
else if (mod === 'budget') c.innerHTML = renderBudget();
else if (mod === 'tech_plan') c.innerHTML = renderTechPlan();
else if (mod === 'layout') c.innerHTML = renderLayout();
else if (mod === 'tech_review') c.innerHTML = renderTechReview();
else if (mod === 'drawing_design') c.innerHTML = renderDrawingDesign();
else if (mod === 'drawing_review') c.innerHTML = renderDrawingReview();
else if (mod === 'procurement') c.innerHTML = renderProcurement();
else if (mod === 'manufacturing') c.innerHTML = renderManufacturing();
else if (mod === 'drawing_compare') c.innerHTML = renderDrawingCompare();
else if (mod === 'doc_lib') c.innerHTML = renderDocLib();
else if (mod === 'site_mod') c.innerHTML = renderSiteMod();
else if (mod === 'shipping') c.innerHTML = renderShipping();
else if (mod === 'manuals') c.innerHTML = renderManuals();
else if (mod === 'install_prep') c.innerHTML = renderInstallPrep();
else if (mod === 'install_feedback') c.innerHTML = renderInstallFeedback();
else if (mod === 'acceptance') c.innerHTML = renderAcceptance();
else if (mod === 'hot_commissioning') c.innerHTML = renderHotCommissioning();
}
// ========== Dashboard ==========
function renderDashboard() {
const stages = [
{ key: 'budget', label: '项目预算', icon: '💰' },
{ key: 'tech_plan', label: '技术方案', icon: '📋' },
{ key: 'layout', label: '布局图', icon: '🗺️' },
{ key: 'tech_review', label: '技术审查', icon: '🔍' },
{ key: 'drawing_design', label: '图纸设计', icon: '📐' },
{ key: 'drawing_review', label: '图纸审查', icon: '✏️' },
{ key: 'procurement', label: '采购管理', icon: '🛒' },
{ key: 'manufacturing', label: '制造进度', icon: '🏭' },
{ key: 'drawing_compare', label: '图纸比较', icon: '🔄' },
{ key: 'doc_lib', label: '资料库', icon: '📁' },
{ key: 'site_mod', label: '现场修改', icon: '🔧' },
{ key: 'shipping', label: '发货清单', icon: '📦' },
{ key: 'manuals', label: '说明书', icon: '📖' },
{ key: 'install_prep', label: '安装准备', icon: '🛠️' },
{ key: 'install_feedback', label: '问题反馈', icon: '💬' },
{ key: 'acceptance', label: '安装验收', icon: '✅' },
{ key: 'hot_commissioning', label: '热负荷试车', icon: '🔥' }
];
const statusMap = getStageStatuses();
let cards = '';
stages.forEach(s => {
const st = statusMap[s.key] || 'pending';
const badge = st==='done'?'done':st==='progress'?'progress':'pending';
const badgeText = st==='done'?'已完成':st==='progress'?'进行中':'未开始';
cards += `<div class="stat-card" onclick="switchModule('${s.key}')">
<div class="label">${s.icon} ${s.label}</div>
<div class="value" style="font-size:14px;">${badgeText}</div>
<div class="sub">点击进入管理</div>
</div>`;
});
const doneCount = Object.values(statusMap).filter(v=>v==='done').length;
const progCount = Object.values(statusMap).filter(v=>v==='progress').length;
const total = stages.length;
const pct = Math.round((doneCount/total)*100);
return `
<div class="dashboard-grid">
<div class="stat-card"><div class="label">项目名称</div><div class="value" style="font-size:14px;color:var(--text);">${projectData.projectInfo.name}</div><div class="sub">编号: ${projectData.projectInfo.number} <span style="cursor:pointer;color:var(--accent);" onclick="openProjectInfoModal()">✏️</span></div></div>
<div class="stat-card green"><div class="label">已完成阶段</div><div class="value">${doneCount}</div><div class="sub">/ ${total} 个阶段</div></div>
<div class="stat-card orange"><div class="label">进行中</div><div class="value">${progCount}</div><div class="sub">个阶段</div></div>
<div class="stat-card"><div class="label">项目整体进度</div><div class="value" style="color:var(--accent);">${pct}%</div><div class="sub">总包项目全生命周期</div></div>
</div>
<div class="dashboard-grid" style="grid-template-columns:repeat(3,1fr);margin-top:10px;">
<div class="stat-card"><div class="label">关联合同</div><div class="value" style="font-size:13px;">${projectData.projectInfo.contractNos&&projectData.projectInfo.contractNos.length>0?projectData.projectInfo.contractNos.length+'份':'未关联'}</div><div class="sub">${projectData.projectInfo.contractNos&&projectData.projectInfo.contractNos.length>0?projectData.projectInfo.contractNos.join(', '):'点击编辑项目信息关联'}</div></div>
<div class="stat-card"><div class="label">项目日期</div><div class="value" style="font-size:13px;color:var(--text);">${projectData.projectInfo.startDate} ~ ${projectData.projectInfo.endDate}</div><div class="sub">项目经理: ${projectData.projectInfo.manager}</div></div>
<div class="stat-card"><div class="label">客户</div><div class="value" style="font-size:13px;color:var(--text);">${projectData.projectInfo.client}</div><div class="sub">点击右上角编辑项目信息</div></div>
</div>
<div class="progress-bar-wrap">
<div class="title">📊 项目全生命周期进度</div>
<div class="progress-steps">${renderProgressSteps()}</div>
</div>
<div style="font-size:12px;font-weight:600;margin-bottom:8px;">📋 各阶段概览(点击进入)</div>
<div class="dashboard-grid">${cards}</div>
`;
}
function openProjectInfoModal() {
const pi = projectData.projectInfo;
const allContracts = projectData.procurement.contracts || [];
const contractOpts = allContracts.map((c,i) =>
`<option value="${c.number||''}" ${(pi.contractNos||[]).includes(c.number)?'selected':''}>${c.number||''} - ${c.supplier||''} - ${c.item||''}</option>`
).join('');
document.getElementById('modal-title').textContent = '项目信息设置';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>项目名称</label><input id="pi-name" value="${pi.name||''}"></div>
<div class="form-group"><label>项目编号 ⚠️ 可自定义</label><input id="pi-number" value="${pi.number||''}" placeholder="如DRF-2026-A01"></div>
</div>
<div class="form-row">
<div class="form-group"><label>客户名称</label><input id="pi-client" value="${pi.client||''}"></div>
<div class="form-group"><label>项目经理</label><input id="pi-manager" value="${pi.manager||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>开始日期</label><input type="date" id="pi-start" value="${pi.startDate||''}"></div>
<div class="form-group"><label>结束日期</label><input type="date" id="pi-end" value="${pi.endDate||''}"></div>
</div>
<div class="form-group" style="margin-top:10px;">
<label>关联合同编号多选Ctrl+点击选择多项)</label>
<select id="pi-contracts" multiple style="width:100%;min-height:100px;padding:6px;font-size:11px;border:1px solid var(--border);border-radius:4px;">
${contractOpts || '<option disabled>采购模块中暂无合同,请先在采购管理创建合同</option>'}
</select>
<div style="font-size:10px;color:var(--text2);margin-top:3px;">当前已关联: ${(pi.contractNos||[]).length>0?pi.contractNos.join(', '):'无'}</div>
</div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveProjectInfo()">保存项目信息</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveProjectInfo() {
const selected = Array.from(document.getElementById('pi-contracts').selectedOptions||[]).map(o=>o.value).filter(Boolean);
projectData.projectInfo.name = document.getElementById('pi-name').value;
projectData.projectInfo.number = document.getElementById('pi-number').value;
projectData.projectInfo.client = document.getElementById('pi-client').value;
projectData.projectInfo.manager = document.getElementById('pi-manager').value;
projectData.projectInfo.startDate = document.getElementById('pi-start').value;
projectData.projectInfo.endDate = document.getElementById('pi-end').value;
projectData.projectInfo.contractNos = selected;
saveData(); closeModal();
document.getElementById('topbar-title').textContent = projectData.projectInfo.name;
renderModule('dashboard');
showToast('项目信息已更新');
}
// ========== Historical Projects Store ==========
const HISTORY_KEY = 'rolling_mill_project_history';
function loadHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY)||'[]'); } catch(e) { return []; } }
function saveHistory(list) { localStorage.setItem(HISTORY_KEY, JSON.stringify(list)); }
function getStageStatuses() {
const d = projectData;
return {
budget: d.budget && d.budget.length > 0 ? (d.budget.every(i=>i.status==='approved')?'done':'progress') : 'pending',
tech_plan: d.techPlan && d.techPlan.status === 'done' ? 'done' : (d.techPlan && d.techPlan.items && d.techPlan.items.length > 0 ? 'progress' : 'pending'),
layout: d.layout && d.layout.status === 'done' ? 'done' : (d.layout && d.layout.files && d.layout.files.length > 0 ? 'progress' : 'pending'),
tech_review: getTechReviewStatus(),
drawing_design: d.drawingDesign && d.drawingDesign.length > 0 ? (d.drawingDesign.every(i=>i.status==='approved')?'done':'progress') : 'pending',
drawing_review: d.drawingReview && d.drawingReview.length > 0 ? (d.drawingReview.every(i=>i.status==='approved')?'done':'progress') : 'pending',
procurement: d.procurement && d.procurement.contracts && d.procurement.contracts.length > 0 ? 'progress' : 'pending',
manufacturing: d.manufacturing && d.manufacturing.length > 0 ? 'progress' : 'pending',
drawing_compare: d.drawingCompare && d.drawingCompare.length > 0 ? 'progress' : 'pending',
doc_lib: d.docLib && d.docLib.length > 0 ? 'progress' : 'pending',
site_mod: d.siteMod && d.siteMod.length > 0 ? 'progress' : 'pending',
shipping: d.shipping && d.shipping.checklist ? (d.shipping.checklist.every(i=>i.checked)?'done':'progress') : 'pending',
manuals: d.manuals && d.manuals.length > 0 ? 'progress' : 'pending',
install_prep: d.installPrep && d.installPrep.tools && d.installPrep.tools.length > 0 ? 'progress' : 'pending',
install_feedback: d.installFeedback && d.installFeedback.length > 0 ? 'progress' : 'pending',
acceptance: d.acceptance && d.acceptance.checklist ? (d.acceptance.checklist.every(i=>i.checked)?'done':'progress') : 'pending',
hot_commissioning: d.hotCommissioning && d.hotCommissioning.checklist ? (d.hotCommissioning.checklist.every(i=>i.checked)?'done':'progress') : 'pending'
};
}
function getTechReviewStatus() {
const tr = projectData.techReview;
if (!tr) return 'pending';
const allDone = (tr.mechanical || []).every(i=>i.status==='approved') &&
(tr.electrical || []).every(i=>i.status==='approved') &&
(tr.hydraulic || []).every(i=>i.status==='approved') &&
(tr.fluid || []).every(i=>i.status==='approved') &&
(tr.energy || []).every(i=>i.status==='approved') && (tr.colorCard||[]).length > 0;
if (allDone) return 'done';
const hasItems = (tr.mechanical||[]).length > 0 || (tr.electrical||[]).length > 0 || (tr.hydraulic||[]).length > 0;
return hasItems ? 'progress' : 'pending';
}
function renderProgressSteps() {
const stages = [
'budget','tech_plan','layout','tech_review','drawing_design','drawing_review',
'procurement','manufacturing','drawing_compare','doc_lib','site_mod',
'shipping','manuals','install_prep','install_feedback','acceptance','hot_commissioning'
];
const statusMap = getStageStatuses();
let html = '';
stages.forEach((s, i) => {
const st = statusMap[s] || 'pending';
html += `<div class="progress-step ${st==='done'?'done':st==='progress'?'active':''}">
<div class="circle">${st==='done'?'✓':i+1}</div>
<div class="step-label">${moduleTitles[s]||s}</div>
</div>`;
if (i < stages.length - 1) {
html += `<div class="progress-line ${st==='done'?'done':''}"></div>`;
}
});
return html;
}
// ========== Budget Module ==========
function renderBudget() {
const rows = projectData.budget.map((b, i) => `
<tr>
<td>${i+1}</td>
<td>${b.category}</td>
<td>${b.item}</td>
<td style="text-align:right;">¥${Number(b.budgetAmount).toLocaleString()}</td>
<td style="text-align:right;">¥${(b.actualAmount?Number(b.actualAmount).toLocaleString():'-')}</td>
<td><span class="status-badge ${b.status}">${b.status==='draft'?'草稿':b.status==='review'?'审核中':b.status==='approved'?'已批准':'已驳回'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editBudget(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteBudget(${i})">删除</button></td>
</tr>
`).join('');
const totalBudget = projectData.budget.reduce((s,b)=>s+Number(b.budgetAmount||0),0);
const totalActual = projectData.budget.reduce((s,b)=>s+Number(b.actualAmount||0),0);
return `
<div class="module-panel">
<div class="module-header">
<span>💰 项目预算管理</span>
<div style="display:flex;gap:6px;">
<button class="btn btn-sm btn-outline" onclick="importHistoryBudget()" style="font-size:11px;">📂 导入历史预算</button>
<button class="btn btn-sm btn-outline" onclick="referenceHistoryCost()" style="font-size:11px;">📋 参考历史成本</button>
<button class="btn btn-sm btn-outline" onclick="archiveCurrentBudget()" style="font-size:11px;">💾 存档当前预算</button>
<button class="btn btn-primary btn-sm" onclick="openBudgetModal()">+ 新增预算项</button>
</div>
</div>
<div class="module-body">
<div style="display:flex;gap:10px;margin-bottom:10px;flex-wrap:wrap;">
<div class="stat-card" style="flex:1;min-width:140px;"><div class="label">预算总额</div><div class="value">¥${totalBudget.toLocaleString()}</div></div>
<div class="stat-card orange" style="flex:1;min-width:140px;"><div class="label">实际支出</div><div class="value">¥${totalActual.toLocaleString()}</div></div>
<div class="stat-card ${totalActual<=totalBudget?'green':'red'}" style="flex:1;min-width:140px;"><div class="label">预算余量</div><div class="value">¥${(totalBudget-totalActual).toLocaleString()}</div></div>
</div>
<table class="data-table">
<thead><tr><th>#</th><th>类别</th><th>项目</th><th>预算金额(¥)</th><th>实际金额(¥)</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无预算数据,请点击"新增预算项"</td></tr>'}</tbody>
</table>
</div>
</div>
`;
}
function openBudgetModal(idx) {
let b = { category:'', item:'', budgetAmount:'', actualAmount:'', status:'draft', note:'' };
let isEdit = false;
if (idx !== undefined) { b = projectData.budget[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑预算项' : '新增预算项';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>类别</label><select id="budget-cat">
<option value="机械设备" ${b.category==='机械设备'?'selected':''}>机械设备</option>
<option value="电气设备" ${b.category==='电气设备'?'selected':''}>电气设备</option>
<option value="液压设备" ${b.category==='液压设备'?'selected':''}>液压设备</option>
<option value="流体设备" ${b.category==='流体设备'?'selected':''}>流体设备</option>
<option value="能源介质" ${b.category==='能源介质'?'selected':''}>能源介质</option>
<option value="安装费用" ${b.category==='安装费用'?'selected':''}>安装费用</option>
<option value="其他" ${b.category==='其他'?'selected':''}>其他</option>
</select></div>
<div class="form-group"><label>项目名称</label><input id="budget-item" value="${b.item||''}" placeholder="如:主轧机牌坊"></div>
</div>
<div class="form-row">
<div class="form-group"><label>预算金额(¥)</label><input id="budget-amount" type="number" value="${b.budgetAmount||''}" placeholder="预算金额"></div>
<div class="form-group"><label>实际金额(¥)</label><input id="budget-actual" type="number" value="${b.actualAmount||''}" placeholder="实际金额"></div>
</div>
<div class="form-row">
<div class="form-group"><label>状态</label><select id="budget-status">
<option value="draft" ${b.status==='draft'?'selected':''}>草稿</option>
<option value="review" ${b.status==='review'?'selected':''}>审核中</option>
<option value="approved" ${b.status==='approved'?'selected':''}>已批准</option>
<option value="rejected" ${b.status==='rejected'?'selected':''}>已驳回</option>
</select></div>
</div>
<div class="form-row">
<div class="form-group"><label>备注</label><textarea id="budget-note" placeholder="备注信息">${b.note||''}</textarea></div>
</div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveBudget(${isEdit?idx:-1})">保存</button>
</div>
`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveBudget(idx) {
const b = {
category: document.getElementById('budget-cat').value,
item: document.getElementById('budget-item').value,
budgetAmount: document.getElementById('budget-amount').value,
actualAmount: document.getElementById('budget-actual').value,
status: document.getElementById('budget-status').value,
note: document.getElementById('budget-note').value
};
if (!b.item) { showToast('请填写项目名称'); return; }
if (idx >= 0) projectData.budget[idx] = b;
else projectData.budget.push(b);
saveData(); closeModal();
renderModule('budget');
showToast('预算项已保存');
}
function editBudget(i) { openBudgetModal(i); }
function deleteBudget(i) {
if (!confirm('确认删除该预算项?')) return;
projectData.budget.splice(i, 1);
saveData(); renderModule('budget');
}
// ========== Budget History ==========
function archiveCurrentBudget() {
if (projectData.budget.length === 0) { showToast('当前无预算数据可存档'); return; }
const history = loadHistory();
const snapshot = {
id: Date.now(),
date: new Date().toISOString().slice(0,10),
name: projectData.projectInfo.name,
number: projectData.projectInfo.number,
contractNos: projectData.projectInfo.contractNos||[],
totalBudget: projectData.budget.reduce((s,b)=>s+Number(b.budgetAmount||0),0),
totalActual: projectData.budget.reduce((s,b)=>s+Number(b.actualAmount||0),0),
items: JSON.parse(JSON.stringify(projectData.budget)),
note: ''
};
history.unshift(snapshot);
if (history.length > 50) history.length = 50;
saveHistory(history);
showToast(`预算已存档: ${snapshot.name}${snapshot.totalBudget.toLocaleString()})`);
}
function importHistoryBudget() {
const history = loadHistory();
if (history.length === 0) { showToast('暂无历史预算数据,请先"存档当前预算"'); return; }
const options = history.map((h,i) => `
<div style="padding:8px 12px;border:1px solid var(--border);border-radius:6px;margin-bottom:6px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;"
onclick="confirmImportHistory('${i}')">
<div>
<div style="font-weight:600;font-size:12px;">${h.name}</div>
<div style="font-size:10px;color:var(--text2);">编号: ${h.number||''} · ${h.date} · ${h.items.length}项 · 预算总额: ¥${h.totalBudget.toLocaleString()}</div>
</div>
<span style="font-size:10px;color:var(--accent);">选择导入 →</span>
</div>
`).join('');
document.getElementById('modal-title').textContent = '导入历史项目预算';
document.getElementById('modal-body').innerHTML = `
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">共 ${history.length} 个历史存档,点击选择导入</div>
<div style="max-height:350px;overflow-y:auto;">${options}</div>
<div style="margin-top:10px;">
<label style="display:flex;align-items:center;gap:6px;font-size:11px;cursor:pointer;"><input type="checkbox" id="hist-replace"> 替换当前预算(否则追加)</label>
</div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">取消</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
window._importHistoryIdx = null;
}
function confirmImportHistory(i) {
const history = loadHistory();
const h = history[i];
if (!h) return;
const replace = document.getElementById('hist-replace')?.checked || false;
if (replace) projectData.budget = JSON.parse(JSON.stringify(h.items));
else projectData.budget = projectData.budget.concat(JSON.parse(JSON.stringify(h.items)));
saveData(); closeModal(); renderModule('budget');
showToast(`${replace?'替换':'追加'}导入 ${h.items.length} 条预算项: ${h.name}`);
}
function referenceHistoryCost() {
const history = loadHistory();
if (history.length === 0) { showToast('暂无历史项目数据,请先存档预算'); return; }
const cards = history.slice(0,8).map(h => {
const diff = h.totalBudget - h.totalActual;
const pct = h.totalBudget>0 ? Math.round(h.totalActual/h.totalBudget*100) : 0;
return `
<div class="stat-card" style="cursor:pointer;" onclick="selectHistoryRef(${h.id})">
<div class="label">${h.name}</div>
<div class="value" style="font-size:13px;">¥${h.totalActual.toLocaleString()}</div>
<div class="sub">预算: ¥${h.totalBudget.toLocaleString()} · 偏差: ${diff>=0?'+':''}¥${diff.toLocaleString()} (${pct}%)</div>
</div>`;
}).join('');
document.getElementById('modal-title').textContent = '历史项目最终成本参考';
document.getElementById('modal-body').innerHTML = `
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">以下为 ${history.length} 个历史项目的最终成本数据,点击某项目查看明细</div>
<div class="dashboard-grid" style="grid-template-columns:repeat(2,1fr);">${cards}</div>
<div style="margin-top:9px;font-size:10px;color:var(--text2);">💡 提示:选择项目后可将其最终成本逐项对照至当前预算</div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function selectHistoryRef(id) {
const history = loadHistory();
const h = history.find(x=>x.id===id);
if (!h) return;
const rows = h.items.map(it=>`<tr><td>${it.category||''}</td><td>${it.item||''}</td><td style="text-align:right;">¥${Number(it.budgetAmount||0).toLocaleString()}</td><td style="text-align:right;">¥${Number(it.actualAmount||0).toLocaleString()}</td></tr>`).join('');
document.getElementById('modal-body').innerHTML = `
<div style="font-weight:600;font-size:13px;margin-bottom:8px;">📋 ${h.name} - 预算明细</div>
<table class="data-table"><thead><tr><th>类别</th><th>项目</th><th>预算金额</th><th>最终成本</th></tr></thead><tbody>${rows}</tbody></table>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="referenceHistoryCost()">返回列表</button>
<button class="btn btn-outline" onclick="closeModal()">关闭</button>
</div>`;
}
// ========== Tech Plan Module ==========
function renderTechPlan() {
const tp = projectData.techPlan;
const items = (tp.items||[]).map((it,i) => `
<tr>
<td>${i+1}</td>
<td>${it.name}</td>
<td>${it.desc||''}</td>
<td>${it.owner||''}</td>
<td><span class="status-badge ${it.status}">${it.status==='done'?'已完成':it.status==='progress'?'进行中':'未开始'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editTechPlanItem(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteTechPlanItem(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header">
<span>📋 技术方案确定</span>
<button class="btn btn-primary btn-sm" onclick="openTechPlanModal()">+ 新增方案项</button>
</div>
<div class="module-body">
<div class="form-row" style="margin-bottom:16px;align-items:flex-end;">
<div class="form-group" style="flex:0 0 auto;">
<label>方案整体状态</label>
<select id="tech-plan-status" onchange="projectData.techPlan.status=this.value;saveData();">
<option value="pending" ${tp.status==='pending'?'selected':''}>未开始</option>
<option value="progress" ${tp.status==='progress'?'selected':''}>进行中</option>
<option value="done" ${tp.status==='done'?'selected':''}>已完成</option>
</select>
</div>
<div class="form-group" style="flex:1;">
<label>方案概述</label>
<input id="tech-plan-desc" value="${tp.desc||''}" placeholder="技术方案总体描述" onchange="projectData.techPlan.desc=this.value;saveData();">
</div>
</div>
<table class="data-table">
<thead><tr><th>#</th><th>方案名称</th><th>描述</th><th>负责人</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${items || '<tr><td colspan="6" style="text-align:center;color:#aaa;">暂无方案项</td></tr>'}</tbody>
</table>
</div>
</div>
`;
}
function openTechPlanModal(idx) {
let it = { name:'', desc:'', owner:'', status:'pending' };
let isEdit = false;
if (idx !== undefined) { it = projectData.techPlan.items[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑方案项' : '新增方案项';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>方案名称</label><input id="tp-name" value="${it.name||''}" placeholder="如:主轧机技术方案"></div>
<div class="form-group"><label>描述</label><textarea id="tp-desc" placeholder="方案详细描述">${it.desc||''}</textarea></div>
<div class="form-row">
<div class="form-group"><label>负责人</label><input id="tp-owner" value="${it.owner||''}" placeholder="负责人姓名"></div>
<div class="form-group"><label>状态</label><select id="tp-status">
<option value="pending" ${it.status==='pending'?'selected':''}>未开始</option>
<option value="progress" ${it.status==='progress'?'selected':''}>进行中</option>
<option value="done" ${it.status==='done'?'selected':''}>已完成</option>
</select></div>
</div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveTechPlanItem(${isEdit?idx:-1})">保存</button>
</div>
`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveTechPlanItem(idx) {
const it = {
name: document.getElementById('tp-name').value,
desc: document.getElementById('tp-desc').value,
owner: document.getElementById('tp-owner').value,
status: document.getElementById('tp-status').value
};
if (!it.name) { showToast('请填写方案名称'); return; }
if (!projectData.techPlan.items) projectData.techPlan.items = [];
if (idx >= 0) projectData.techPlan.items[idx] = it;
else projectData.techPlan.items.push(it);
saveData(); closeModal();
renderModule('tech_plan');
}
function editTechPlanItem(i) { openTechPlanModal(i); }
function deleteTechPlanItem(i) {
if (!confirm('确认删除?')) return;
projectData.techPlan.items.splice(i, 1);
saveData(); renderModule('tech_plan');
}
// ========== Layout Module ==========
function renderLayout() {
const lo = projectData.layout;
const files = (lo.files||[]).map((f,i) => `
<tr>
<td>${i+1}</td><td>${f.name}</td><td>${f.type||''}</td><td>${f.uploadDate||''}</td>
<td><span class="status-badge ${f.status}">${f.status==='approved'?'已批准':'待审核'}</span></td>
<td><button class="btn btn-sm btn-danger" onclick="deleteLayoutFile(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header">
<span>🗺️ 布局图确定</span>
<div>
<button class="btn btn-primary btn-sm" onclick="uploadLayoutFile()">📤 上传布局图</button>
</div>
</div>
<div class="module-body">
<div class="form-row" style="margin-bottom:16px;">
<div class="form-group" style="flex:0 0 auto;">
<label>状态</label>
<select onchange="projectData.layout.status=this.value;saveData();">
<option value="pending" ${lo.status==='pending'?'selected':''}>未开始</option>
<option value="progress" ${lo.status==='progress'?'selected':''}>进行中</option>
<option value="done" ${lo.status==='done'?'selected':''}>已完成</option>
</select>
</div>
</div>
<div class="file-upload-area" onclick="uploadLayoutFile()" style="margin-bottom:16px;">
<div class="icon">📤</div>
<div class="text">点击上传布局图文件DWG/PDF/DXF</div>
</div>
<table class="data-table">
<thead><tr><th>#</th><th>文件名</th><th>类型</th><th>上传日期</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${files || '<tr><td colspan="6" style="text-align:center;color:#aaa;">暂无布局图文件</td></tr>'}</tbody>
</table>
</div>
</div>
`;
}
function uploadLayoutFile() {
const name = prompt('输入文件名1380mm轧机平面布置图.dwg');
if (!name) return;
if (!projectData.layout.files) projectData.layout.files = [];
projectData.layout.files.push({ name, type:'DWG', uploadDate: new Date().toISOString().slice(0,10), status:'pending' });
saveData(); renderModule('layout');
showToast('布局图已添加');
}
function deleteLayoutFile(i) {
if (!confirm('确认删除?')) return;
projectData.layout.files.splice(i, 1);
saveData(); renderModule('layout');
}
// ========== Tech Review Module ==========
function renderTechReview() {
const tr = projectData.techReview;
const tabs = [
{ key:'mechanical', label:'机械审查', icon:'⚙️' },
{ key:'electrical', label:'电气审查', icon:'⚡' },
{ key:'hydraulic', label:'液压审查', icon:'💧' },
{ key:'fluid', label:'流体审查', icon:'🌊' },
{ key:'energy', label:'能源介质审查', icon:'🔋' },
{ key:'color', label:'色卡确定', icon:'🎨' }
];
return `
<div class="module-panel">
<div class="module-header"><span>🔍 技术审查(机械/电气/液压/流体/能源介质/色卡)</span></div>
<div class="module-body">
<div class="tabs" id="tech-review-tabs">
${tabs.map((t,i)=>`<div class="tab ${i===0?'active':''}" onclick="switchTechReviewTab('${t.key}',this)">${t.icon} ${t.label}</div>`).join('')}
</div>
<div id="tech-review-tab-content">${renderTechReviewTabContent(tabs[0].key)}</div>
</div>
</div>
`;
}
function switchTechReviewTab(key, el) {
document.querySelectorAll('#tech-review-tabs .tab').forEach(t=>t.classList.remove('active'));
el.classList.add('active');
document.getElementById('tech-review-tab-content').innerHTML = renderTechReviewTabContent(key);
}
function renderTechReviewTabContent(key) {
if (key === 'color') {
const cards = projectData.techReview.colorCard || [];
// Category definitions with colors and icons
const catDefs = {
'流体管路': { icon:'🔧', color:'#00773E' },
'机械-动力': { icon:'⚡', color:'#F0C700' },
'机械-被动': { icon:'🔩', color:'#4A4A4A' },
'液压缸': { icon:'💉', color:'#9013FE' },
'电机': { icon:'🔌', color:'#C1121C' },
'减速机': { icon:'⚙️', color:'#1A1A1A' }
};
// Group cards by category
const grouped = {};
cards.forEach((c,i)=>{
const cat = c.category || '未分类';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push({...c, _i:i});
});
const catOrder = ['机械-被动','机械-动力','流体管路','液压缸','电机','减速机'];
let sections = '';
let totalCount = 0;
catOrder.forEach(cat=>{
const items = grouped[cat]||[];
if (items.length===0 && Object.keys(grouped).indexOf(cat)===-1) return; // skip empty ordered cats
totalCount += items.length;
const def = catDefs[cat]||{icon:'📌', color:'#888'};
const cardItems = items.length===0
? `<div style="text-align:center;padding:20px;color:#bbb;font-size:12px;">暂无此类色卡</div>`
: items.map(c=>{
const textColor = getContrastColor(c.hex);
return `
<div class="color-card">
<div class="color-swatch" style="background:${c.hex};">
<span class="cs-code" style="color:${textColor};">${c.hex}</span>
</div>
<div class="color-info">
<div class="ci-name">${c.colorName||''}</div>
<div class="ci-meta">
<span class="ci-tag standard">${c.standard||''}</span>
${c.usage?`<span class="ci-tag usage">${c.usage}</span>`:''}
</div>
${c.desc?`<div class="ci-desc">${c.desc}</div>`:''}
</div>
<div class="color-card-actions">
<button class="btn btn-outline btn-sm" onclick="openColorCardModal(true,${c._i})">✏️ 编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteColorCard(${c._i})">🗑 删除</button>
</div>
</div>`;
}).join('');
sections += `
<div class="color-category-section">
<div class="color-cat-header">
<span class="color-cat-icon">${def.icon}</span>
<span class="color-cat-name">${cat}</span>
<span class="color-cat-count">${items.length}条</span>
</div>
<div class="color-card-grid">${cardItems}</div>
</div>`;
});
// Handle uncategorized cards not in catOrder
const handled = new Set(catOrder);
Object.keys(grouped).forEach(cat=>{
if (!handled.has(cat) && grouped[cat].length>0) {
totalCount += grouped[cat].length;
const items = grouped[cat];
const cardItems = items.map(c=>{
const textColor = getContrastColor(c.hex);
return `
<div class="color-card">
<div class="color-swatch" style="background:${c.hex};">
<span class="cs-code" style="color:${textColor};">${c.hex}</span>
</div>
<div class="color-info">
<div class="ci-name">${c.colorName||''}</div>
<div class="ci-meta">
<span class="ci-tag standard">${c.standard||''}</span>
${c.usage?`<span class="ci-tag usage">${c.usage}</span>`:''}
</div>
${c.desc?`<div class="ci-desc">${c.desc}</div>`:''}
</div>
<div class="color-card-actions">
<button class="btn btn-outline btn-sm" onclick="openColorCardModal(true,${c._i})">✏️ 编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteColorCard(${c._i})">🗑 删除</button>
</div>
</div>`;
}).join('');
sections += `
<div class="color-category-section">
<div class="color-cat-header">
<span class="color-cat-icon">📌</span>
<span class="color-cat-name">${cat}</span>
<span class="color-cat-count">${items.length}条</span>
</div>
<div class="color-card-grid">${cardItems}</div>
</div>`;
}
});
const quickColors = [
'#C8CBCE','#F0C700','#005A8C','#C1121C','#00773E','#8C6820',
'#FFFFFF','#1A1A1A','#E5E5E5','#FF6B00','#4A90D9','#50B848',
'#F5A623','#D0021B','#8B572A','#9013FE','#417505','#BD10E0',
'#4A4A4A','#B8E986','#F8E71C','#7ED321','#D8D8D8','#9B9B9B'
];
return `
<div style="margin-bottom:14px;">
<button class="btn btn-primary btn-sm" onclick="openColorCardModal(false)" style="margin-right:8px;">+ 新增色卡</button>
<span style="font-size:11px;color:var(--text2);">共 ${totalCount} 条色卡 · ${Object.keys(grouped).length} 个分类</span>
</div>
<div class="module-panel" style="background:#fafbfc;">
<div class="module-header"><span>🎨 常用快捷色板(点击可快速选取颜色)</span></div>
<div class="module-body">
<div class="color-quick-palette" id="color-quick-palette">
${quickColors.map(hex=>`<div class="color-quick-chip" style="background:${hex};" data-hex="${hex}" onclick="pickQuickColor('${hex}',this)" title="${hex}"></div>`).join('')}
</div>
</div>
</div>
${sections || `<div style="text-align:center;padding:40px;color:#aaa;font-size:13px;">暂无色卡,点击上方按钮添加</div>`}
`;
}
const labels = { mechanical:'机械审查', electrical:'电气审查', hydraulic:'液压审查', fluid:'流体审查', energy:'能源介质审查' };
const arr = projectData.techReview[key] || [];
const rows = arr.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.name||''}</td><td>${it.reviewer||''}</td><td>${it.date||''}</td>
<td><span class="status-badge ${it.status}">${it.status==='approved'?'已通过':it.status==='rejected'?'已驳回':'待审查'}</span></td>
<td>${it.comment||''}</td>
<td style="text-align:center;">
${it.thinking ? `<span class="thinking-badge" onclick="viewThinkingDetail('${key}',${i})" title="查看思维导入">💡 已录入</span>` : `<span class="thinking-badge-empty" onclick="viewThinkingDetail('${key}',${i})" title="点击添加思维导入">💡 —</span>`}
</td>
<td><button class="btn btn-sm btn-outline" onclick="editTechReviewItem('${key}',${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteTechReviewItem('${key}',${i})">删除</button></td>
</tr>
`).join('');
return `
<div style="margin-bottom:12px;">
<button class="btn btn-primary btn-sm" onclick="openTechReviewModal('${key}')">+ 新增${labels[key]}项</button>
</div>
<table class="data-table">
<thead><tr><th>#</th><th>审查项目</th><th>审查人</th><th>审查日期</th><th>状态</th><th>审查意见</th><th>💡思维导入</th><th>操作</th></tr></thead>
<tbody>${rows || `<tr><td colspan="8" style="text-align:center;color:#aaa;">暂无${labels[key]}数据</td></tr>`}</tbody>
</table>
`;
}
function openTechReviewModal(key, idx) {
let it = { name:'', reviewer:'', date:'', status:'pending', comment:'', thinking:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.techReview[key][idx]; isEdit = true; }
const labels = { mechanical:'机械审查', electrical:'电气审查', hydraulic:'液压审查', fluid:'流体审查', energy:'能源介质审查' };
document.getElementById('modal-title').textContent = (isEdit?'编辑':'新增') + labels[key] + '项';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>审查项目</label><input id="tr-name" value="${it.name||''}" placeholder="审查项目名称"></div>
<div class="form-row">
<div class="form-group"><label>审查人</label><input id="tr-reviewer" value="${it.reviewer||''}" placeholder="审查人姓名"></div>
<div class="form-group"><label>审查日期</label><input id="tr-date" type="date" value="${it.date||''}"></div>
</div>
<div class="form-group"><label>状态</label><select id="tr-status">
<option value="pending" ${it.status==='pending'?'selected':''}>待审查</option>
<option value="approved" ${it.status==='approved'?'selected':''}>已通过</option>
<option value="rejected" ${it.status==='rejected'?'selected':''}>已驳回</option>
</select></div>
<div class="form-group"><label>审查意见</label><textarea id="tr-comment" placeholder="审查意见">${it.comment||''}</textarea></div>
<div class="form-group">
<label>💡 思维导入 <span style="font-weight:400;color:var(--text2);font-size:10px;">— 设计思路、选型依据、技术决策说明</span></label>
<textarea id="tr-thinking" style="min-height:100px;" placeholder="记录设计思维,例如:&#10;1. 选型理由:选择六辊轧机是因板形控制优于四辊...&#10;2. 设计依据参照GB/T标准进行强度校核...&#10;3. 关键决策主电机功率选800kW满载加速力矩满足...&#10;4. 经验参考同类型1380mm轧机实际运行数据...">${it.thinking||''}</textarea>
</div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveTechReviewItem('${key}',${isEdit?idx:-1})">保存</button>
</div>
`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveTechReviewItem(key, idx) {
const it = {
name: document.getElementById('tr-name').value,
reviewer: document.getElementById('tr-reviewer').value,
date: document.getElementById('tr-date').value,
status: document.getElementById('tr-status').value,
comment: document.getElementById('tr-comment').value,
thinking: document.getElementById('tr-thinking').value
};
if (!projectData.techReview[key]) projectData.techReview[key] = [];
if (idx >= 0) projectData.techReview[key][idx] = it;
else projectData.techReview[key].push(it);
saveData(); closeModal();
document.getElementById('tech-review-tab-content').innerHTML = renderTechReviewTabContent(key);
showToast('审查项已保存');
}
function editTechReviewItem(key, i) { openTechReviewModal(key, i); }
function deleteTechReviewItem(key, i) {
if (!confirm('确认删除?')) return;
projectData.techReview[key].splice(i, 1);
saveData();
const activeTab = document.querySelector('#tech-review-tabs .tab.active');
if (activeTab) switchTechReviewTab(key, activeTab);
else renderModule('tech_review');
}
function viewThinkingDetail(key, idx) {
const item = projectData.techReview[key][idx];
const labels = { mechanical:'机械审查', electrical:'电气审查', hydraulic:'液压审查', fluid:'流体审查', energy:'能源介质审查' };
const thinking = item.thinking || '';
document.getElementById('modal-title').textContent = `💡 ${item.name||'审查项'} — 思维导入`;
document.getElementById('modal-body').innerHTML = `
<div style="margin-bottom:10px;display:flex;gap:16px;font-size:11px;color:var(--text2);">
<span>📋 ${labels[key]||key}</span>
<span>👤 ${item.reviewer||'—'}</span>
<span>📅 ${item.date||'—'}</span>
<span><span class="status-badge ${item.status}">${item.status==='approved'?'已通过':item.status==='rejected'?'已驳回':'待审查'}</span></span>
</div>
${thinking ? `
<div class="thinking-display">${thinking.replace(/\n/g,'<br>')}</div>
` : `
<div style="text-align:center;padding:30px;color:#aaa;">暂未录入思维导入内容</div>
<div style="text-align:center;margin-top:8px;">
<button class="btn btn-primary btn-sm" onclick="closeModal();editTechReviewItem('${key}',${idx})">✏️ 去编辑添加</button>
</div>
`}
<div style="text-align:right;margin-top:12px;">
${thinking ? `<button class="btn btn-outline btn-sm" onclick="closeModal();editTechReviewItem('${key}',${idx})">✏️ 编辑</button>` : ''}
<button class="btn btn-outline btn-sm" onclick="closeModal();openMindMap('${key}',${idx})" style="background:#f3e5f5;border-color:#ce93d8;color:#7b1fa2;">🧠 思维导图</button>
<button class="btn btn-outline" onclick="closeModal()">关闭</button>
</div>
`;
document.getElementById('modal-overlay').classList.add('show');
}
function getContrastColor(hex) {
if (!hex) return '#fff';
const h = hex.replace('#','');
const r = parseInt(h.substring(0,2),16);
const g = parseInt(h.substring(2,4),16);
const b = parseInt(h.substring(4,6),16);
return (r*0.299+g*0.587+b*0.114) > 150 ? '#1a1a2e' : '#ffffff';
}
function pickQuickColor(hex, el) {
document.querySelectorAll('.color-quick-chip').forEach(c=>c.classList.remove('selected'));
if (el) el.classList.add('selected');
const colorInput = document.getElementById('cc-hex');
if (colorInput) colorInput.value = hex;
}
function openColorCardModal(isEdit, idx) {
const cards = projectData.techReview.colorCard || [];
let card = { colorName:'', hex:'#C8CBCE', standard:'', standardLabel:'RAL', category:'机械-被动', usage:'', desc:'' };
if (isEdit && idx >= 0) card = cards[idx];
const categories = ['机械-被动','机械-动力','流体管路','液压缸','电机','减速机'];
document.getElementById('modal-title').textContent = isEdit ? '编辑色卡' : '新增色卡';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group" style="flex:1;"><label>部位/名称</label><input id="cc-name" value="${card.colorName||''}" placeholder="如轧机牌坊、AGC液压缸、主电机"></div>
<div class="form-group" style="flex:0 0 100px;"><label>颜色预览</label><div class="color-preview-block" id="cc-preview" style="background:${card.hex||'#C8CBCE'};"></div></div>
</div>
<div class="form-row">
<div class="form-group" style="flex:0 0 160px;">
<label>设备分类</label>
<select id="cc-category">${categories.map(c=>`<option value="${c}" ${card.category===c?'selected':''}>${c}</option>`).join('')}</select>
</div>
<div class="form-group" style="flex:0 0 180px;">
<label>色值 (HEX)</label>
<div class="color-picker-row">
<input type="color" id="cc-hex" value="${card.hex||'#C8CBCE'}" onchange="document.getElementById('cc-preview').style.background=this.value">
<input id="cc-hex-text" value="${card.hex||'#C8CBCE'}" style="flex:1;font-family:monospace;" placeholder="#000000" oninput="updateColorPreview(this.value)">
</div>
</div>
<div class="form-group" style="flex:0 0 120px;">
<label>色卡标准</label>
<select id="cc-standard-label">
<option value="RAL" ${card.standardLabel==='RAL'?'selected':''}>RAL</option>
<option value="Pantone" ${card.standardLabel==='Pantone'?'selected':''}>Pantone</option>
<option value="GB" ${card.standardLabel==='GB'?'selected':''}>GB 国标</option>
<option value="ISO" ${card.standardLabel==='ISO'?'selected':''}>ISO</option>
<option value="JIS" ${card.standardLabel==='JIS'?'selected':''}>JIS</option>
<option value="企业标准" ${card.standardLabel==='企业标准'?'selected':''}>企业标准</option>
<option value="其他" ${card.standardLabel==='其他'?'selected':''}>其他</option>
</select>
</div>
<div class="form-group" style="flex:0 0 130px;">
<label>标准编号</label><input id="cc-standard" value="${card.standard||''}" placeholder="如7035、1023">
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:1;"><label>用途分类</label><input id="cc-usage" value="${card.usage||''}" placeholder="如:设备主体框架、安全防护、液压系统"></div>
</div>
<div class="form-group"><label>描述</label><textarea id="cc-desc" placeholder="详细描述涂装位置和颜色说明">${card.desc||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveColorCardItem(${isEdit},${idx})">保存</button>
</div>
`;
document.getElementById('modal-overlay').classList.add('show');
}
function updateColorPreview(val) {
const preview = document.getElementById('cc-preview');
const colorInput = document.getElementById('cc-hex');
if (preview && /^#[0-9A-Fa-f]{6}$/.test(val)) {
preview.style.background = val;
if (colorInput) colorInput.value = val;
}
}
function saveColorCardItem(isEdit, idx) {
const card = {
colorName: document.getElementById('cc-name').value.trim(),
hex: document.getElementById('cc-hex').value || '#C8CBCE',
standard: document.getElementById('cc-standard').value.trim(),
standardLabel: document.getElementById('cc-standard-label').value,
category: document.getElementById('cc-category').value,
usage: document.getElementById('cc-usage').value.trim(),
desc: document.getElementById('cc-desc').value.trim()
};
if (!card.colorName) { showToast('请输入部位/名称'); return; }
if (!projectData.techReview.colorCard) projectData.techReview.colorCard = [];
if (isEdit && idx >= 0) {
projectData.techReview.colorCard[idx] = card;
} else {
projectData.techReview.colorCard.push(card);
}
saveData(); closeModal();
const activeTab = document.querySelector('#tech-review-tabs .tab.active');
if (activeTab) switchTechReviewTab('color', activeTab);
showToast('色卡已保存');
}
function deleteColorCard(idx) {
if (!confirm('确认删除此色卡?')) return;
projectData.techReview.colorCard.splice(idx, 1);
saveData();
const activeTab = document.querySelector('#tech-review-tabs .tab.active');
if (activeTab) switchTechReviewTab('color', activeTab);
showToast('色卡已删除');
}
// ========== Mind Map Editor (思维导图) ==========
let mmState = { key:'', idx:-1, data:null, selectedId:null, zoom:1, panX:0, panY:0, isPanning:false, panStartX:0, panStartY:0 };
let mmNodeIdCounter = 0;
function mmNewId() { return 'n'+(++mmNodeIdCounter); }
function mmDefaultRoot(text) {
mmNodeIdCounter = 1;
return { id:'n1', text:text||'中心主题', children:[], collapsed:false };
}
function openMindMap(key, idx) {
const item = projectData.techReview[key][idx];
if (!item.mindmap || !item.mindmap.root) {
item.mindmap = { root: mmDefaultRoot(item.name||'审查项') };
}
mmState = { key, idx, data:item.mindmap, selectedId:item.mindmap.root.id, zoom:1, panX:0, panY:0, isPanning:false };
document.getElementById('mm-item-name').textContent = '— '+(item.name||'');
document.getElementById('mm-overlay').classList.add('show');
mmRebuildNodeIds(item.mindmap.root);
mmLayout();
mmRenderAll();
mmResetView();
}
function mmRebuildNodeIds(node) {
if (!node.id) node.id = mmNewId();
if (node.children) node.children.forEach(c=>mmRebuildNodeIds(c));
}
function mmLayout() {
const root = mmState.data.root;
const H_SPACING = 180;
const V_SPACING = 50;
function layoutNode(node, depth, yOffset) {
node._depth = depth;
if (!node.children || node.children.length===0 || node.collapsed) {
node._width = 1;
node.x = depth * H_SPACING;
node.y = yOffset;
return 1;
}
let totalHeight = 0;
const childYs = [];
for (const child of node.children) {
const h = layoutNode(child, depth+1, totalHeight);
totalHeight += h;
childYs.push(child.y);
}
node._width = totalHeight;
const midY = (childYs[0] + childYs[childYs.length-1]) / 2;
node.x = depth * H_SPACING;
node.y = midY;
return totalHeight;
}
layoutNode(root, 0, 0);
// Apply V spacing
function applySpacing(node) {
node.y = node.y * V_SPACING + 40;
if (node.children) node.children.forEach(c=>applySpacing(c));
}
applySpacing(root);
}
function mmRenderAll() {
const canvas = document.getElementById('mm-canvas');
// Remove old nodes
canvas.querySelectorAll('.mm-node,.mm-node-edit-input').forEach(el=>el.remove());
mmRenderNode(mmState.data.root, canvas);
mmRenderEdges();
}
function mmRenderNode(node, parent) {
const div = document.createElement('div');
div.className = 'mm-node'+(node.id===mmState.data.root.id?' mm-root':'');
if (node.children&&node.children.length>0&&!node.collapsed) div.classList.add('mm-has-children');
else if (!node.children||node.children.length===0) div.classList.add('mm-leaf');
if (node.id===mmState.selectedId) div.classList.add('mm-selected');
div.id = 'mm-node-'+node.id;
div.textContent = node.text;
div.style.left = node.x+'px';
div.style.top = node.y+'px';
div.addEventListener('click',(e)=>{ e.stopPropagation(); mmSelectNode(node.id); });
div.addEventListener('dblclick',(e)=>{ e.stopPropagation(); mmEditNode(); });
div.addEventListener('contextmenu',(e)=>{ e.preventDefault(); e.stopPropagation(); mmSelectNode(node.id); mmShowCtxMenu(e.clientX,e.clientY); });
// Collapse toggle
if (node.children && node.children.length>0) {
const collapseBtn = document.createElement('span');
collapseBtn.className = 'mm-collapse';
collapseBtn.textContent = node.collapsed ? '▸' : '▾';
collapseBtn.addEventListener('click',(e)=>{
e.stopPropagation();
node.collapsed = !node.collapsed;
mmLayout(); mmRenderAll();
});
div.appendChild(collapseBtn);
}
parent.appendChild(div);
if (!node.collapsed && node.children) {
node.children.forEach(c=>mmRenderNode(c,parent));
}
}
function mmRenderEdges() {
const svg = document.getElementById('mm-svg');
const canvas = document.getElementById('mm-canvas');
let paths = '';
function walk(node) {
if (!node.collapsed && node.children) {
node.children.forEach(child=>{
// Parent center-right → child center-left
const px = node.x + 130; // approximate node width for root=160, normal=120
const py = node.y + 18; // approximate node half height
const cx = child.x;
const cy = child.y + 18;
// Cubic bezier
const cpx = px + (cx-px)*0.5;
const d = `M${px},${py} C${cpx},${py} ${cpx},${cy} ${cx},${cy}`;
paths += `<path d="${d}" class="${node.id===mmState.selectedId||child.id===mmState.selectedId?'mm-edge-selected':''}"/>`;
walk(child);
});
}
}
walk(mmState.data.root);
svg.innerHTML = paths;
// Match SVG size to canvas content
let maxX=0, maxY=0;
function findMax(node) {
if (node.x+200>maxX) maxX=node.x+200;
if (node.y+100>maxY) maxY=node.y+100;
if (!node.collapsed&&node.children) node.children.forEach(c=>findMax(c));
}
findMax(mmState.data.root);
svg.setAttribute('width',Math.max(maxX+100,3000));
svg.setAttribute('height',Math.max(maxY+100,2000));
canvas.style.minWidth = Math.max(maxX+100,3000)+'px';
canvas.style.minHeight = Math.max(maxY+100,2000)+'px';
}
function mmSelectNode(id) {
mmState.selectedId = id;
document.querySelectorAll('.mm-node').forEach(el=>el.classList.remove('mm-selected'));
const el = document.getElementById('mm-node-'+id);
if (el) el.classList.add('mm-selected');
mmRenderEdges();
}
function mmGetSelectedNode(node) {
if (!node) node = mmState.data.root;
if (node.id === mmState.selectedId) return node;
if (node.children) {
for (const c of node.children) {
const found = mmGetSelectedNode(c);
if (found) return found;
}
}
return null;
}
function mmGetParentNode(targetId, node, parent) {
if (!node) node = mmState.data.root;
if (node.children) {
for (const c of node.children) {
if (c.id === targetId) return node;
const found = mmGetParentNode(targetId, c, node);
if (found) return found;
}
}
return null;
}
function mmAddChild() {
const sel = mmGetSelectedNode();
if (!sel) return showToast('请先选择一个节点');
if (!sel.children) sel.children = [];
sel.collapsed = false;
const newNode = { id:mmNewId(), text:'新节点', children:[], collapsed:false };
sel.children.push(newNode);
mmState.selectedId = newNode.id;
mmLayout(); mmRenderAll();
mmStartEdit(newNode.id);
}
function mmAddSibling() {
const sel = mmGetSelectedNode();
if (!sel) return showToast('请先选择一个节点');
if (sel.id === mmState.data.root.id) return showToast('根节点不能添加兄弟节点,请用子节点');
const parent = mmGetParentNode(sel.id);
if (!parent) return;
const idx = parent.children.findIndex(c=>c.id===sel.id);
const newNode = { id:mmNewId(), text:'新节点', children:[], collapsed:false };
parent.children.splice(idx+1, 0, newNode);
mmState.selectedId = newNode.id;
mmLayout(); mmRenderAll();
mmStartEdit(newNode.id);
}
function mmEditNode() {
const sel = mmGetSelectedNode();
if (!sel) return showToast('请先选择一个节点');
mmStartEdit(sel.id);
}
function mmStartEdit(nodeId) {
const node = document.getElementById('mm-node-'+nodeId);
if (!node) return;
const selData = mmGetSelectedNode();
if (!selData) return;
const input = document.createElement('input');
input.className = 'mm-node-edit-input';
input.value = selData.text;
input.style.left = node.style.left;
input.style.top = node.style.top;
input.style.width = Math.max(node.offsetWidth, 100)+'px';
document.getElementById('mm-canvas').appendChild(input);
input.focus();
input.select();
input.addEventListener('blur',()=>mmFinishEdit(input, nodeId));
input.addEventListener('keydown',(e)=>{
if (e.key==='Enter') { e.preventDefault(); mmFinishEdit(input, nodeId); }
if (e.key==='Escape') { input.remove(); }
});
}
function mmFinishEdit(input, nodeId) {
const text = input.value.trim();
input.remove();
if (text) {
const node = mmGetSelectedNode();
if (node && node.id===nodeId) node.text = text;
mmRenderAll();
}
}
function mmDeleteNode() {
const sel = mmGetSelectedNode();
if (!sel) return showToast('请先选择一个节点');
if (sel.id === mmState.data.root.id) return showToast('不能删除根节点');
if (!confirm('确认删除节点「'+sel.text+'」及其所有子节点?')) return;
const parent = mmGetParentNode(sel.id);
if (parent) {
parent.children = parent.children.filter(c=>c.id!==sel.id);
mmState.selectedId = parent.id;
mmLayout(); mmRenderAll();
}
}
function mmCollapseNode() {
const sel = mmGetSelectedNode();
if (!sel) return showToast('请先选择一个节点');
if (!sel.children || sel.children.length===0) return showToast('该节点无子节点');
sel.collapsed = !sel.collapsed;
mmLayout(); mmRenderAll();
}
function mmShowCtxMenu(x, y) {
const menu = document.getElementById('mm-ctx-menu');
menu.style.left = x+'px';
menu.style.top = y+'px';
menu.classList.add('show');
setTimeout(()=>document.addEventListener('click',mmHideCtxMenu,{once:true}),50);
}
function mmHideCtxMenu() { document.getElementById('mm-ctx-menu').classList.remove('show'); }
function mmResetView() {
mmState.zoom = 1; mmState.panX = 0; mmState.panY = 0;
mmApplyTransform();
document.getElementById('mm-zoom-label').textContent = '100%';
}
function mmZoom(delta) {
mmState.zoom = Math.max(0.3, Math.min(2, mmState.zoom + delta));
mmApplyTransform();
document.getElementById('mm-zoom-label').textContent = Math.round(mmState.zoom*100)+'%';
}
function mmApplyTransform() {
const canvas = document.getElementById('mm-canvas');
canvas.style.transform = `translate(${mmState.panX}px,${mmState.panY}px) scale(${mmState.zoom})`;
canvas.style.transformOrigin = '0 0';
}
function mmSave() {
const item = projectData.techReview[mmState.key][mmState.idx];
// Clean up layout-only fields before saving
function cleanNode(node) {
const clean = { id:node.id, text:node.text, children:[], collapsed:!!node.collapsed };
if (node.children) clean.children = node.children.map(c=>cleanNode(c));
return clean;
}
item.mindmap = { root: cleanNode(mmState.data.root) };
saveData();
showToast('思维导图已保存');
}
function mmClose() {
const item = projectData.techReview[mmState.key][mmState.idx];
const hasContent = item.mindmap && item.mindmap.root &&
(item.mindmap.root.children && item.mindmap.root.children.length>0 || item.mindmap.root.text!=='中心主题');
if (hasContent) {
if (confirm('是否保存思维导图后再关闭?')) mmSave();
}
document.getElementById('mm-overlay').classList.remove('show');
mmHideCtxMenu();
}
// Canvas pan & zoom
document.addEventListener('DOMContentLoaded',()=>{
const wrap = document.getElementById('mm-canvas-wrap');
if (!wrap) return;
wrap.addEventListener('mousedown',(e)=>{
if (e.target.closest('.mm-node')||e.target.closest('.mm-ctx-menu')||e.target.closest('.mm-node-edit-input')) return;
mmState.isPanning = true;
mmState.panStartX = e.clientX - mmState.panX;
mmState.panStartY = e.clientY - mmState.panY;
wrap.classList.add('panning');
});
window.addEventListener('mousemove',(e)=>{
if (!mmState.isPanning) return;
mmState.panX = e.clientX - mmState.panStartX;
mmState.panY = e.clientY - mmState.panStartY;
mmApplyTransform();
});
window.addEventListener('mouseup',()=>{
mmState.isPanning = false;
if (wrap) wrap.classList.remove('panning');
});
wrap.addEventListener('wheel',(e)=>{
e.preventDefault();
const delta = e.deltaY > 0 ? -0.05 : 0.05;
mmZoom(delta);
});
});
// Keyboard shortcuts
document.addEventListener('keydown',(e)=>{
if (!document.getElementById('mm-overlay').classList.contains('show')) return;
if (e.target.classList.contains('mm-node-edit-input')) return;
switch(e.key) {
case 'Tab': e.preventDefault(); mmAddChild(); break;
case 'Enter': e.preventDefault(); mmAddSibling(); break;
case 'F2': e.preventDefault(); mmEditNode(); break;
case 'Delete': e.preventDefault(); mmDeleteNode(); break;
case 'Escape': e.preventDefault(); mmClose(); break;
case '+': case '=': e.preventDefault(); mmZoom(0.1); break;
case '-': e.preventDefault(); mmZoom(-0.1); break;
case '0': e.preventDefault(); mmResetView(); break;
}
});
// ========== Drawing Design Module (图纸详细设计) ==========
function getServerUrl() { return (projectData.serverConfig && projectData.serverConfig.drawingServerUrl) || ''; }
function isServerConfigured() { return !!getServerUrl(); }
function renderDrawingDesign() {
const items = projectData.drawingDesign || [];
const serverOk = isServerConfigured();
const serverUrl = getServerUrl();
const rows = items.map((it,i) => {
const fileHtml = it.drawingFile
? `<span class="draw-file-name"><span class="dfn-icon">📄</span>${it.drawingFile.name||it.drawingFile.fileName||''}</span><div class="draw-file-link">📍 ${it.drawingFile.serverPath||'—'}</div>`
: '<span style="color:#ccc;">未上传</span>';
return `
<tr>
<td>${i+1}</td><td>${it.name||''}</td><td>${it.type||''}</td><td>${it.version||'V1.0'}</td>
<td>${it.drawer||''}</td><td>${it.startDate||it.date||''}</td><td>${it.endDate||''}</td>
<td><span class="status-badge ${it.status}">${it.status==='approved'?'已批准':it.status==='review'?'审核中':'设计中'}</span></td>
<td>${fileHtml}</td>
<td><button class="btn btn-sm btn-outline" onclick="editDrawingDesign(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteDrawingDesign(${i})">删除</button></td>
</tr>`;
}).join('');
const serverBar = serverOk
? `<div class="server-info">🖥️ <b>图纸存储服务器:</b>${serverUrl} <span style="margin-left:auto;cursor:pointer;font-size:11px;color:var(--accent);" onclick="openServerConfig()">⚙️ 修改</span></div>`
: `<div class="server-warning">
<span class="sw-icon">⚠️</span>
<span class="sw-text"><b>未配置公司服务器!</b>图纸文件将无法保存至服务器。笔记本电脑部署后不能保存到本地,必须保存到公司服务器。</span>
<button class="sw-btn" onclick="openServerConfig()">⚙️ 配置服务器</button>
</div>`;
return `
<div class="module-panel">
<div class="module-header"><span>📐 图纸详细设计</span><button class="btn btn-primary btn-sm" onclick="openDrawingDesignModal()">+ 新增图纸</button></div>
<div class="module-body">
${serverBar}
<table class="data-table">
<thead><tr><th>#</th><th>图纸名称</th><th>图纸类型</th><th>版本</th><th>设计人</th><th>设计日期</th><th>结束日期</th><th>状态</th><th>图纸文件</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="10" style="text-align:center;color:#aaa;">暂无图纸数据</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openDrawingDesignModal(idx) {
let it = { name:'', type:'总图', version:'V1.0', drawer:'', startDate:new Date().toISOString().slice(0,10), endDate:'', status:'draft', note:'', drawingFile:null };
let isEdit = false;
if (idx !== undefined) { it = { ...it, ...projectData.drawingDesign[idx] }; isEdit = true; }
const serverOk = isServerConfigured();
const serverUrl = getServerUrl();
const fileInfo = it.drawingFile
? `<div class="draw-file-upload has-file" id="draw-file-area">
<div class="draw-file-name"><span class="dfn-icon">📄</span>${it.drawingFile.name||it.drawingFile.fileName||''}</div>
<div class="draw-file-link">📍 服务器路径:${it.drawingFile.serverPath||'—'}</div>
<div class="dfu-hint" style="cursor:pointer;color:var(--danger);" onclick="clearDrawingFile()">🗑️ 移除文件</div>
</div>`
: `<div class="draw-file-upload" id="draw-file-area" onclick="uploadDrawingFile()">
<div style="font-size:28px;">📤</div>
<div style="font-size:12px;font-weight:600;">点击选择图纸文件</div>
<div class="dfu-hint">支持格式DWG, DXF, PDF, STEP, IGS 等</div>
${serverOk ? '<div class="dfu-server-tag">🖥️ 将保存至公司服务器</div>' : '<div class="dfu-server-tag" style="background:var(--danger);">⚠️ 服务器未配置,无法保存</div>'}
</div>`;
document.getElementById('modal-title').textContent = isEdit ? '编辑图纸' : '新增图纸';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>图纸名称</label><input id="dd-name" value="${it.name||''}" placeholder="如:轧机主机列总图"></div>
<div class="form-row">
<div class="form-group"><label>图纸类型</label><select id="dd-type">
<option value="总图" ${it.type==='总图'?'selected':''}>总图</option>
<option value="装配图" ${it.type==='装配图'?'selected':''}>装配图</option>
<option value="零件图" ${it.type==='零件图'?'selected':''}>零件图</option>
<option value="基础图" ${it.type==='基础图'?'selected':''}>基础图</option>
<option value="配管图" ${it.type==='配管图'?'selected':''}>配管图</option>
<option value="电气原理图" ${it.type==='电气原理图'?'selected':''}>电气原理图</option>
</select></div>
<div class="form-group"><label>版本号</label><input id="dd-version" value="${it.version||'V1.0'}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>设计人</label><input id="dd-drawer" value="${it.drawer||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>设计日期</label><input id="dd-startdate" type="date" value="${it.startDate||''}"></div>
<div class="form-group"><label>结束日期</label><input id="dd-enddate" type="date" value="${it.endDate||''}"></div>
</div>
<div class="form-group"><label>状态</label><select id="dd-status">
<option value="draft" ${it.status==='draft'?'selected':''}>设计中</option>
<option value="review" ${it.status==='review'?'selected':''}>审核中</option>
<option value="approved" ${it.status==='approved'?'selected':''}>已批准</option>
</select></div>
<div class="form-group"><label>图纸文件(必须保存至公司服务器)</label>
${fileInfo}
<input type="file" id="dd-file-input" style="display:none;" accept=".dwg,.dxf,.pdf,.step,.stp,.igs,.iges,.jpg,.png,.zip" onchange="handleDrawingFileSelect(this)">
</div>
<div class="form-group"><label>备注</label><textarea id="dd-note">${it.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveDrawingDesign(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveDrawingDesign(idx) {
const name = document.getElementById('dd-name').value;
if (!name) { showToast('请填写图纸名称'); return; }
// 检查是否有新选择的文件
const fileInput = document.getElementById('dd-file-input');
let drawingFile = window._pendingDrawingFile || null;
// 如果是编辑模式且没有新文件,保留旧文件
if (!drawingFile && idx >= 0 && projectData.drawingDesign[idx]) {
drawingFile = projectData.drawingDesign[idx].drawingFile || null;
}
// 如果有文件但服务器未配置,阻止保存
if (drawingFile && !isServerConfigured()) {
showToast('⚠️ 未配置公司服务器,无法保存图纸文件。请先配置服务器地址。');
return;
}
const it = {
name: name, type: document.getElementById('dd-type').value,
version: document.getElementById('dd-version').value, drawer: document.getElementById('dd-drawer').value,
startDate: document.getElementById('dd-startdate').value, endDate: document.getElementById('dd-enddate').value,
status: document.getElementById('dd-status').value,
note: document.getElementById('dd-note').value,
drawingFile: drawingFile
};
if (!projectData.drawingDesign) projectData.drawingDesign = [];
if (idx >= 0) projectData.drawingDesign[idx] = it; else projectData.drawingDesign.push(it);
window._pendingDrawingFile = null;
saveData(); closeModal(); renderModule('drawing_design');
showToast(drawingFile ? '图纸已保存(含图纸文件)' : '图纸已保存');
}
// 图纸文件选择
function uploadDrawingFile() {
document.getElementById('dd-file-input').click();
}
function handleDrawingFileSelect(input) {
const file = input.files[0];
if (!file) return;
if (!isServerConfigured()) {
showToast('⚠️ 未配置公司服务器,无法上传图纸文件。请先配置服务器。');
input.value = '';
return;
}
const serverUrl = getServerUrl();
// 构建服务器存储路径
const projectNo = (projectData.projectInfo && projectData.projectInfo.number) || 'PRJ';
const serverPath = serverUrl.replace(/\/+$/,'') + '/drawings/' + projectNo + '/' + file.name;
const drawingFile = {
name: file.name,
size: file.size,
type: file.type || '',
serverPath: serverPath,
uploadTime: new Date().toISOString()
};
window._pendingDrawingFile = drawingFile;
// 更新UI显示
const area = document.getElementById('draw-file-area');
if (area) {
area.className = 'draw-file-upload has-file';
area.onclick = null;
const sizeStr = file.size > 1024*1024 ? (file.size/(1024*1024)).toFixed(1)+' MB' : (file.size/1024).toFixed(1)+' KB';
area.innerHTML = `
<div class="draw-file-name"><span class="dfn-icon">📄</span>${file.name} <span style="font-size:10px;color:var(--text2);font-weight:400;">(${sizeStr})</span></div>
<div class="draw-file-link">📍 将保存至:${serverPath}</div>
<div class="dfu-hint" style="display:flex;gap:12px;margin-top:4px;">
<span style="color:var(--success);">✅ 就绪,点击保存后上传至公司服务器</span>
<span style="cursor:pointer;color:var(--danger);" onclick="event.stopPropagation();clearDrawingFile();">🗑️ 移除</span>
</div>`;
}
showToast('图纸文件已选择:' + file.name);
}
function clearDrawingFile() {
window._pendingDrawingFile = null;
const fileInput = document.getElementById('dd-file-input');
if (fileInput) fileInput.value = '';
const area = document.getElementById('draw-file-area');
if (area) {
const serverOk = isServerConfigured();
area.className = 'draw-file-upload';
area.onclick = function(){ uploadDrawingFile(); };
area.innerHTML = `
<div style="font-size:28px;">📤</div>
<div style="font-size:12px;font-weight:600;">点击选择图纸文件</div>
<div class="dfu-hint">支持格式DWG, DXF, PDF, STEP, IGS 等</div>
${serverOk ? '<div class="dfu-server-tag">🖥️ 将保存至公司服务器</div>' : '<div class="dfu-server-tag" style="background:var(--danger);">⚠️ 服务器未配置,无法保存</div>'}
`;
}
}
// 服务器配置
function openServerConfig() {
const url = getServerUrl();
document.getElementById('modal-title').textContent = '⚙️ 图纸服务器配置';
document.getElementById('modal-body').innerHTML = `
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:4px;padding:8px 12px;margin-bottom:12px;font-size:11px;color:#856404;">
⚠️ <b>重要:</b>图纸文件必须保存至公司服务器,不可保存到笔记本电脑本地。请配置公司图纸服务器地址。
</div>
<div class="form-group"><label>公司图纸服务器地址</label>
<input id="sc-server-url" value="${url||''}" placeholder="如http://192.168.1.100:8080/api/drawings 或 \\\\server-fs01\\drawings">
</div>
<div style="font-size:11px;color:var(--text2);margin-bottom:6px;">
💡 支持以下格式:<br>
&nbsp;&nbsp;• HTTP/HTTPS API地址<code>http://192.168.1.100:8080/api/drawings</code><br>
&nbsp;&nbsp;• 网络共享路径:<code>\\\\server-fs01\\drawings</code><br>
&nbsp;&nbsp;• FTP地址<code>ftp://fileserver/drawings</code>
</div>
<div style="background:#e8f8f0;border-radius:4px;padding:8px 12px;font-size:11px;color:#1a7a3c;">
✅ 配置后,所有图纸文件将自动上传至公司服务器指定路径,笔记本电脑不保留本地副本。
</div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveServerConfig()">💾 保存配置</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveServerConfig() {
const url = document.getElementById('sc-server-url').value.trim();
if (!projectData.serverConfig) projectData.serverConfig = {};
projectData.serverConfig.drawingServerUrl = url;
saveData();
closeModal();
renderModule('drawing_design');
showToast(url ? '服务器配置已保存' : '服务器配置已清除');
}
function editDrawingDesign(i) { openDrawingDesignModal(i); }
function deleteDrawingDesign(i) { if (!confirm('确认删除?')) return; projectData.drawingDesign.splice(i,1); saveData(); renderModule('drawing_design'); }
// ========== Drawing Review Module (图纸审查) ==========
function renderDrawingReview() {
const items = projectData.drawingReview || [];
const rows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.drawingName||''}</td><td>${it.reviewer||''}</td><td>${it.date||''}</td>
<td><span class="status-badge ${it.result}">${it.result==='pass'?'通过':it.result==='reject'?'驳回':'待审查'}</span></td>
<td>${it.comment||''}</td>
<td><button class="btn btn-sm btn-outline" onclick="editDrawingReview(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteDrawingReview(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>✏️ 图纸审查</span><button class="btn btn-primary btn-sm" onclick="openDrawingReviewModal()">+ 新增审查记录</button></div>
<div class="module-body">
<table class="data-table">
<thead><tr><th>#</th><th>图纸名称</th><th>审查人</th><th>审查日期</th><th>结果</th><th>审查意见</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无审查记录</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openDrawingReviewModal(idx) {
let it = { drawingName:'', reviewer:'', date:new Date().toISOString().slice(0,10), result:'pending', comment:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.drawingReview[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑审查记录' : '新增审查记录';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>图纸名称</label><input id="dr-drawing" value="${it.drawingName||''}" placeholder="关联图纸名称"></div>
<div class="form-row">
<div class="form-group"><label>审查人</label><input id="dr-reviewer" value="${it.reviewer||''}"></div>
<div class="form-group"><label>审查日期</label><input id="dr-date" type="date" value="${it.date||''}"></div>
</div>
<div class="form-group"><label>审查结果</label><select id="dr-result">
<option value="pending" ${it.result==='pending'?'selected':''}>待审查</option>
<option value="pass" ${it.result==='pass'?'selected':''}>通过</option>
<option value="reject" ${it.result==='reject'?'selected':''}>驳回</option>
</select></div>
<div class="form-group"><label>审查意见</label><textarea id="dr-comment">${it.comment||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveDrawingReview(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveDrawingReview(idx) {
const it = { drawingName:document.getElementById('dr-drawing').value, reviewer:document.getElementById('dr-reviewer').value, date:document.getElementById('dr-date').value, result:document.getElementById('dr-result').value, comment:document.getElementById('dr-comment').value };
if (!it.drawingName) { showToast('请填写图纸名称'); return; }
if (!projectData.drawingReview) projectData.drawingReview = [];
if (idx >= 0) projectData.drawingReview[idx] = it; else projectData.drawingReview.push(it);
saveData(); closeModal(); renderModule('drawing_review'); showToast('审查记录已保存');
}
function editDrawingReview(i) { openDrawingReviewModal(i); }
function deleteDrawingReview(i) { if (!confirm('确认删除?')) return; projectData.drawingReview.splice(i,1); saveData(); renderModule('drawing_review'); }
// ========== Procurement Module (采购管理) ==========
function renderProcurement() {
const quotes = (projectData.procurement.quotes||[]).map((q,i) => `
<tr>
<td>${i+1}</td><td>${q.item||''}</td><td>${q.supplier||''}</td><td>¥${Number(q.price||0).toLocaleString()}</td>
<td>${q.delivery||''}</td><td><span class="status-badge ${q.selected?'done':'pending'}">${q.selected?'已选':'待选'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editQuote(${i})">编辑</button> <button class="btn btn-sm ${q.selected?'btn-success':'btn-primary'}" onclick="selectQuote(${i})">${q.selected?'取消选择':'选择'}</button> <button class="btn btn-sm btn-danger" onclick="deleteQuote(${i})">删除</button></td>
</tr>
`).join('');
const contracts = (projectData.procurement.contracts||[]).map((c,i) => `
<tr>
<td>${i+1}</td><td>${c.number||''}</td><td>${c.supplier||''}</td><td>${c.item||''}</td><td>¥${Number(c.amount||0).toLocaleString()}</td>
<td><span class="status-badge ${c.status}">${c.status==='signed'?'已签订':c.status==='review'?'审核中':'草稿'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editContract(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteContract(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="tabs" id="proc-tabs">
<div class="tab active" onclick="switchProcTab('quotes',this)">📊 报价比较</div>
<div class="tab" onclick="switchProcTab('contracts',this)">📝 合同管理</div>
<div class="tab" onclick="switchProcTab('template',this)">📄 合同模版</div>
<div class="tab" onclick="switchProcTab('progress',this)">📈 采购进度</div>
</div>
<div id="proc-tab-content">${renderQuotesTab(window._cmpResult)}</div>`;
}
function switchProcTab(tab, el) {
document.querySelectorAll('#proc-tabs .tab').forEach(t=>t.classList.remove('active'));
el.classList.add('active');
if (tab==='quotes') document.getElementById('proc-tab-content').innerHTML = renderQuotesTab(window._cmpResult);
else if (tab==='contracts') document.getElementById('proc-tab-content').innerHTML = renderContractsTab();
else if (tab==='template') document.getElementById('proc-tab-content').innerHTML = renderTemplateTab();
else if (tab==='progress') document.getElementById('proc-tab-content').innerHTML = renderProgressTab();
}
function renderQuotesTab(cmpResult) {
const quotes = projectData.procurement.quotes || [];
const rows = quotes.map((q,i) => `
<tr>
<td>${i+1}</td><td>${q.item||''}</td><td>${q.supplier||''}</td><td>¥${Number(q.price||0).toLocaleString()}</td>
<td>${q.delivery||''}天</td><td>${q.warranty||''}个月</td>
<td><span class="status-badge ${q.selected?'done':'pending'}">${q.selected?'✓已选':'待选'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editQuote(${i})">编辑</button> <button class="btn btn-sm ${q.selected?'btn-outline':'btn-primary'}" onclick="selectQuote(${i})">${q.selected?'取消':'选择'}</button> <button class="btn btn-sm btn-danger" onclick="deleteQuote(${i})">删除</button></td>
</tr>
`).join('');
// Build comparison result HTML if available
let cmpHtml = '';
if (cmpResult && cmpResult.groups && cmpResult.groups.length > 0) {
const totalQuotes = cmpResult.groups.reduce((s,g)=>s+g.quotes.length,0);
const totalItems = cmpResult.groups.length;
const groupsHtml = cmpResult.groups.map(g => {
const sorted = [...g.quotes].sort((a,b)=>b.score-a.score);
const cards = sorted.map((q,rank) => {
const rankCls = rank===0?'r1':rank===1?'r2':rank===2?'r3':'rx';
const winCls = rank===0?' winner':'';
const scoreCls = q.score>=80?'high':q.score>=60?'med':'low';
return `
<div class="cmp-card${winCls}">
<div class="cc-rank ${rankCls}">${rank+1}</div>
<div class="cc-info">
<div class="cc-supplier">${q.supplier}${rank===0?' 🏆':''}</div>
<div class="cc-item">${q.item}</div>
</div>
<div class="cc-metrics">
<div class="cc-metric"><div class="val">¥${Number(q.price||0).toLocaleString()}</div><div class="lbl">报价</div></div>
<div class="cc-metric"><div class="val">${q.delivery||0}天</div><div class="lbl">交货</div></div>
<div class="cc-metric"><div class="val">${q.warranty||0}月</div><div class="lbl">质保</div></div>
</div>
<div class="cc-score ${scoreCls}"><span style="font-size:16px;">${Math.round(q.score)}</span><span style="font-size:8px;">分</span></div>
${rank===0?`<div class="cc-action"><button class="btn btn-sm btn-success" onclick="autoSelectBest('${g.itemName.replace(/'/g,"\\'")}')">✓ 选为最优</button></div>`:''}
</div>`;
}).join('');
return `<div class="cmp-group">
<div class="cmp-group-header"><span>📦 ${g.itemName}</span><span class="gh-count">${g.quotes.length}家供应商比价 · 最优:${sorted[0]?.supplier||'—'}</span></div>
${cards}
</div>`;
}).join('');
cmpHtml = `
<div class="cmp-panel">
<h4>📊 自动比价结果</h4>
<div class="cmp-subtitle">评分权重价格50% · 交货期25% · 质保期25%(价格越低越好、交货越短越好、质保越长越好)</div>
<div class="cmp-summary">
<div class="cmp-summary-item"><div class="csi-val">${totalItems}</div><div class="csi-lbl">设备项数</div></div>
<div class="cmp-summary-item"><div class="csi-val">${totalQuotes}</div><div class="csi-lbl">报价总数</div></div>
</div>
${groupsHtml}
<div style="margin-top:10px;display:flex;gap:8px;">
<button class="btn btn-primary btn-sm" onclick="autoSelectAllBest()">🏆 一键选择所有最优报价</button>
<button class="btn btn-outline btn-sm" onclick="clearAllSelections()">✖ 清除所有选择</button>
</div>
</div>`;
}
return `
<div class="module-panel">
<div class="module-header">
<span>📊 报价比较</span>
<div style="display:flex;gap:8px;">
<button class="btn btn-accent btn-sm" onclick="autoCompareQuotes()">🔄 自动比价</button>
<button class="btn btn-primary btn-sm" onclick="openQuoteModal()">+ 新增报价</button>
</div>
</div>
<div class="module-body">
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">💡 同一设备项可添加多家供应商报价,点击「🔄 自动比价」按价格、交货期、质保期综合评分排名。</div>
<table class="data-table">
<thead><tr><th>#</th><th>设备项</th><th>供应商</th><th>报价(¥)</th><th>交货期</th><th>质保期</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="8" style="text-align:center;color:#aaa;">暂无报价数据,请先添加报价</td></tr>'}</tbody>
</table>
${cmpHtml}
</div>
</div>`;
}
function openQuoteModal(idx) {
let q = { item:'', supplier:'', price:'', delivery:'', warranty:'', selected:false, note:'' };
let isEdit = false;
if (idx !== undefined) { q = projectData.procurement.quotes[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑报价' : '新增报价';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>设备项名称</label><input id="q-item" value="${q.item||''}" placeholder="如:工作辊轴承座"></div>
<div class="form-group"><label>供应商名称</label><input id="q-supplier" value="${q.supplier||''}" placeholder="供应商全称"></div>
<div class="form-row">
<div class="form-group"><label>报价金额(¥)</label><input id="q-price" type="number" value="${q.price||''}"></div>
<div class="form-group"><label>交货期(天)</label><input id="q-delivery" type="number" value="${q.delivery||''}"></div>
<div class="form-group"><label>质保期(月)</label><input id="q-warranty" type="number" value="${q.warranty||'12'}"></div>
</div>
<div class="form-group"><label>备注</label><textarea id="q-note">${q.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveQuote(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveQuote(idx) {
const q = { item:document.getElementById('q-item').value, supplier:document.getElementById('q-supplier').value, price:document.getElementById('q-price').value, delivery:document.getElementById('q-delivery').value, warranty:document.getElementById('q-warranty').value, selected:false, note:document.getElementById('q-note').value };
if (!q.item) { showToast('请填写设备项名称'); return; }
if (!projectData.procurement.quotes) projectData.procurement.quotes = [];
if (idx >= 0) { q.selected = projectData.procurement.quotes[idx].selected; projectData.procurement.quotes[idx] = q; }
else projectData.procurement.quotes.push(q);
saveData(); closeModal(); renderModule('procurement'); showToast('报价已保存');
}
function selectQuote(i) {
projectData.procurement.quotes.forEach((q,idx) => { if (idx===i) q.selected = !q.selected; else q.selected = false; });
saveData(); renderModule('procurement'); showToast('已选择该报价');
}
function editQuote(i) { openQuoteModal(i); }
function deleteQuote(i) { if (!confirm('确认删除?')) return; projectData.procurement.quotes.splice(i,1); saveData(); renderModule('procurement'); }
// ========== Auto Price Comparison ==========
function autoCompareQuotes() {
const quotes = projectData.procurement.quotes || [];
if (quotes.length === 0) { showToast('暂无报价数据,请先添加报价'); return; }
// Group quotes by item name
const groups = {};
quotes.forEach(q => {
const item = (q.item||'').trim();
if (!item) return;
if (!groups[item]) groups[item] = [];
groups[item].push(q);
});
const groupNames = Object.keys(groups);
if (groupNames.length === 0) { showToast('请填写设备项名称'); return; }
// For each group, compute scores
const result = { groups: [], totalQuotes: 0, totalItems: groupNames.length };
groupNames.forEach(itemName => {
const groupQuotes = groups[itemName];
if (groupQuotes.length < 2) {
// Single quote — auto 100 score
const q = groupQuotes[0];
result.groups.push({
itemName: itemName,
quotes: [{ ...q, score: 100, priceScore: 50, deliveryScore: 25, warrantyScore: 25 }]
});
result.totalQuotes += 1;
return;
}
const prices = groupQuotes.map(q => Number(q.price) || 0);
const deliveries = groupQuotes.map(q => Number(q.delivery) || 0);
const warranties = groupQuotes.map(q => Number(q.warranty) || 0);
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const minDelivery = Math.min(...deliveries);
const maxDelivery = Math.max(...deliveries);
const minWarranty = Math.min(...warranties);
const maxWarranty = Math.max(...warranties);
const priceRange = maxPrice - minPrice;
const deliveryRange = maxDelivery - minDelivery;
const warrantyRange = maxWarranty - minWarranty;
const scored = groupQuotes.map(q => {
const price = Number(q.price) || 0;
const delivery = Number(q.delivery) || 0;
const warranty = Number(q.warranty) || 0;
// Price: lower is better (max score 50)
const priceScore = priceRange === 0 ? 50 : (1 - (price - minPrice) / priceRange) * 50;
// Delivery: shorter is better (max score 25)
const deliveryScore = deliveryRange === 0 ? 25 : (1 - (delivery - minDelivery) / deliveryRange) * 25;
// Warranty: longer is better (max score 25)
const warrantyScore = warrantyRange === 0 ? 25 : ((warranty - minWarranty) / warrantyRange) * 25;
const score = priceScore + deliveryScore + warrantyScore;
return { ...q, score, priceScore, deliveryScore, warrantyScore };
});
result.groups.push({ itemName, quotes: scored });
result.totalQuotes += scored.length;
});
// Sort groups by item name
result.groups.sort((a, b) => a.itemName.localeCompare(b.itemName, 'zh'));
window._cmpResult = result;
document.getElementById('proc-tab-content').innerHTML = renderQuotesTab(result);
showToast(`比价完成:${result.totalItems}个设备项 · ${result.totalQuotes}条报价`);
}
function autoSelectBest(itemName) {
const result = window._cmpResult;
if (!result) { showToast('请先执行自动比价'); return; }
const group = result.groups.find(g => g.itemName === itemName);
if (!group || group.quotes.length === 0) return;
// Find best quote
const best = group.quotes.reduce((a, b) => b.score > a.score ? b : a, group.quotes[0]);
// Find the matching original quote and select it
const quotes = projectData.procurement.quotes;
quotes.forEach((q, i) => {
if ((q.item||'').trim() === itemName && (q.supplier||'').trim() === (best.supplier||'').trim()) {
quotes[i].selected = true;
} else if ((q.item||'').trim() === itemName) {
quotes[i].selected = false;
}
});
saveData();
document.getElementById('proc-tab-content').innerHTML = renderQuotesTab(result);
showToast(`已选择 ${best.supplier}${itemName})为最优报价`);
}
function autoSelectAllBest() {
const result = window._cmpResult;
if (!result) { showToast('请先执行自动比价'); return; }
if (result.groups.length === 0) return;
const quotes = projectData.procurement.quotes;
// First deselect all
quotes.forEach((q, i) => { quotes[i].selected = false; });
let selectedCount = 0;
result.groups.forEach(group => {
if (group.quotes.length === 0) return;
const best = group.quotes.reduce((a, b) => b.score > a.score ? b : a, group.quotes[0]);
quotes.forEach((q, i) => {
if ((q.item||'').trim() === group.itemName && (q.supplier||'').trim() === (best.supplier||'').trim()) {
quotes[i].selected = true;
selectedCount++;
}
});
});
saveData();
document.getElementById('proc-tab-content').innerHTML = renderQuotesTab(result);
showToast(`已一键选择 ${selectedCount} 个设备项的最优报价`);
}
function clearAllSelections() {
projectData.procurement.quotes.forEach((q, i) => { q.selected = false; });
saveData();
const result = window._cmpResult;
document.getElementById('proc-tab-content').innerHTML = renderQuotesTab(result);
showToast('已清除所有选择');
}
function renderContractsTab() {
const contracts = projectData.procurement.contracts || [];
const rows = contracts.map((c,i) => `
<tr>
<td>${i+1}</td><td>${c.number||''}</td><td>${c.supplier||''}</td><td>${c.item||''}</td>
<td>¥${Number(c.amount||0).toLocaleString()}</td><td>${c.signDate||''}</td>
<td><span class="status-badge ${c.status}">${c.status==='signed'?'已签订':c.status==='review'?'审核中':'草稿'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editContract(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteContract(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>📝 合同管理</span><button class="btn btn-primary btn-sm" onclick="openContractModal()">+ 起草合同</button></div>
<div class="module-body">
<table class="data-table">
<thead><tr><th>#</th><th>合同编号</th><th>供应商</th><th>设备项</th><th>金额(¥)</th><th>签订日期</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="8" style="text-align:center;color:#aaa;">暂无合同数据,点击"起草合同"</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openContractModal(idx) {
let c = { number:'', supplier:'', item:'', amount:'', signDate:new Date().toISOString().slice(0,10), status:'draft', clauses:'', penaltyClause:'', note:'' };
let isEdit = false;
if (idx !== undefined) { c = projectData.procurement.contracts[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑合同' : '起草合同';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>合同编号</label><input id="c-number" value="${c.number||''}" placeholder="如DRF-CT-2026-001"></div>
<div class="form-group"><label>供应商</label><input id="c-supplier" value="${c.supplier||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>设备项</label><input id="c-item" value="${c.item||''}"></div>
<div class="form-group"><label>合同金额(¥)</label><input id="c-amount" type="number" value="${c.amount||''}"></div>
</div>
<div class="form-group"><label>签订日期</label><input id="c-signDate" type="date" value="${c.signDate||''}"></div>
<div class="form-group"><label>合同条款摘要</label><textarea id="c-clauses" placeholder="主要合同条款">${c.clauses||''}</textarea></div>
<div class="form-group"><label>延期/质量违约罚款条款</label><textarea id="c-penalty" placeholder="如每延迟1天罚款合同金额的0.5%">${c.penaltyClause||''}</textarea></div>
<div class="form-group"><label>状态</label><select id="c-status">
<option value="draft" ${c.status==='draft'?'selected':''}>草稿</option>
<option value="review" ${c.status==='review'?'selected':''}>审核中</option>
<option value="signed" ${c.status==='signed'?'selected':''}>已签订</option>
</select></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveContract(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveContract(idx) {
const c = { number:document.getElementById('c-number').value, supplier:document.getElementById('c-supplier').value, item:document.getElementById('c-item').value, amount:document.getElementById('c-amount').value, signDate:document.getElementById('c-signDate').value, clauses:document.getElementById('c-clauses').value, penaltyClause:document.getElementById('c-penalty').value, status:document.getElementById('c-status').value, note:'' };
if (!c.number) { showToast('请填写合同编号'); return; }
if (!projectData.procurement.contracts) projectData.procurement.contracts = [];
if (idx >= 0) projectData.procurement.contracts[idx] = c; else projectData.procurement.contracts.push(c);
saveData(); closeModal(); renderModule('procurement'); showToast('合同已保存');
}
function editContract(i) { openContractModal(i); }
function deleteContract(i) { if (!confirm('确认删除?')) return; projectData.procurement.contracts.splice(i,1); saveData(); renderModule('procurement'); }
function renderTemplateTab() {
return `
<div class="module-panel">
<div class="module-header"><span>📄 采购合同模版</span><button class="btn btn-primary btn-sm" onclick="downloadContractTemplate()">📥 下载模版</button></div>
<div class="module-body" style="font-size:13px;line-height:1.8;">
<div style="background:#f8f9fc;padding:16px;border-radius:6px;border:1px solid var(--border);">
<h3 style="text-align:center;margin-bottom:12px;">设备采购合同模版</h3>
<p><b>合同编号:</b>__________</p>
<p><b>甲方(采购方):</b>昆山德睿福成套设备有限公司</p>
<p><b>乙方(供货方):</b>__________</p>
<p style="margin-top:10px;"><b>一、设备清单</b></p>
<p>设备名称__________ 规格型号__________ 数量__________ 单价__________</p>
<p><b>二、技术标准</b></p>
<p>1. 设备设计制造应符合国家相关标准及甲方提供的技术协议要求。</p>
<p>2. 安装精度、材质要求详见技术附件。</p>
<p><b>三、交货期</b></p>
<p>乙方应于 _____ 年 __ 月 __ 日前将设备交付至甲方指定地点。每延迟1天乙方应按合同总金额的 <b>0.5%</b> 向甲方支付违约金。</p>
<p><b>四、质量检验</b></p>
<p>1. 乙方应在每个制造阶段向甲方提交进度报告及相关检验数据(材质报告、精度检验报告等)。</p>
<p>2. 未按合同要求上传检验报告的,每项处以合同金额 <b>1%</b> 的罚款。</p>
<p>3. 设备到场后,甲方按技术协议进行验收,不合格设备乙方应无偿整改。</p>
<p><b>五、质保期</b></p>
<p>设备质保期为投产之日起 <b>12个月</b> 或货到之日起 <b>18个月</b>,以先到者为准。</p>
<p><b>六、付款方式</b></p>
<p>1. 合同签订后支付 <b>30%</b> 预付款;</p>
<p>2. 设备制造完成预验收合格后支付 <b>30%</b></p>
<p>3. 设备到场安装调试验收合格后支付 <b>35%</b></p>
<p>4. 质保期满后支付剩余 <b>5%</b> 质保金。</p>
<p><b>七、争议解决</b></p>
<p>双方发生争议应协商解决,协商不成的,提交甲方所在地人民法院诉讼解决。</p>
<p style="margin-top:16px;"><b>甲方(盖章):</b>__________ <b>乙方(盖章):</b>__________</p>
<p><b>签订日期:</b>_____ 年 __ 月 __ 日</p>
</div>
</div>
</div>`;
}
function downloadContractTemplate() { showToast('合同模版已生成,请复制上方内容使用'); }
// ========== 采购进度 ==========
const PROC_STAGES = ['询价','报价','比价','合同签订','下单','制造中','已发货','已到货'];
function getDefaultProcProgressStages() { return PROC_STAGES.map(s=>({name:s,done:false,date:''})); }
function renderProgressTab() {
const items = projectData.procurement.progress || [];
const totalItems = items.length;
const totalStages = totalItems * PROC_STAGES.length;
let doneStages = 0;
items.forEach(it => { (it.stages||[]).forEach(s => { if (s.done) doneStages++; }); });
const overallPct = totalStages > 0 ? Math.round(doneStages/totalStages*100) : 0;
// Summary cards
const inquiring = items.filter(i=>!i.stages||!i.stages[0].done).length;
const ordering = items.filter(i=>i.stages&&i.stages[4]&&i.stages[4].done&&i.stages[7]&&!i.stages[7].done).length;
const arrived = items.filter(i=>i.stages&&i.stages[7]&&i.stages[7].done).length;
const rows = items.map((it,idx) => {
const stages = it.stages || [];
const doneCount = stages.filter(s=>s.done).length;
const pct = Math.round(doneCount/PROC_STAGES.length*100);
const statusColor = pct>=100?'var(--success)':pct>=50?'var(--accent)':pct>0?'var(--warning)':'#ddd';
return `
<div class="module-panel" style="margin-bottom:8px;">
<div class="module-header">
<span>🔩 ${it.item||'未命名'} <span style="font-weight:400;font-size:11px;color:var(--text2);">${it.supplier||''} ${it.contractNo?'· '+it.contractNo:''}</span></span>
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-size:11px;font-weight:600;color:${statusColor};">${pct}%</span>
<span style="font-size:11px;color:var(--text2);">¥${Number(it.amount||0).toLocaleString()}</span>
<button class="btn btn-sm btn-outline" onclick="editProcProgress(${idx})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteProcProgress(${idx})">删除</button>
</div>
</div>
<div class="module-body">
<div class="progress-steps" style="margin-bottom:8px;">
${stages.map((s,si) => {
let cls = s.done?'done':'';
if (si===doneCount) cls='active';
return `${si>0?'<div class="progress-line '+(stages[si-1].done?'done':'')+'"></div>':''}
<div class="progress-step ${cls}">
<div class="circle">${s.done?'✓':si+1}</div>
<div class="step-label">${s.name}</div>
${s.date?`<div style="font-size:8px;color:#999;">${s.date.slice(5)}</div>`:''}
</div>`;
}).join('')}
</div>
${it.note?`<div style="font-size:10px;color:var(--text2);margin-top:4px;">📝 ${it.note}</div>`:''}
</div>
</div>`;
}).join('');
return `
<div class="dashboard-grid" style="grid-template-columns:repeat(4,1fr);">
<div class="stat-card" style="text-align:center;"><div class="label">采购总进度</div><div class="value" style="color:var(--accent);">${overallPct}%</div><div class="sub">${doneStages}/${totalStages} 阶段</div></div>
<div class="stat-card orange" style="text-align:center;"><div class="label">询价/报价中</div><div class="value">${inquiring}</div><div class="sub">项待推进</div></div>
<div class="stat-card" style="text-align:center;"><div class="label">制造/运输中</div><div class="value" style="color:var(--accent);">${ordering}</div><div class="sub">项进行中</div></div>
<div class="stat-card green" style="text-align:center;"><div class="label">已到货</div><div class="value">${arrived}</div><div class="sub">项完成</div></div>
</div>
<div style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">
<div style="font-size:12px;color:var(--text2);">共 ${totalItems} 项采购任务 · 8个阶段询价→报价→比价→合同签订→下单→制造中→已发货→已到货</div>
<button class="btn btn-primary btn-sm" onclick="openProcProgressModal()">+ 新增采购进度</button>
</div>
${rows || '<div class="module-panel"><div class="module-body" style="text-align:center;color:#aaa;padding:30px;">暂无采购进度数据,可关联已选报价/合同快速创建</div></div>'}
<div style="margin-top:8px;">
<button class="btn btn-sm btn-outline" onclick="autoCreateProcProgress()" style="font-size:11px;">🔄 从已选报价/合同批量创建</button>
</div>`;
}
function openProcProgressModal(idx) {
let p = { item:'', supplier:'', amount:'', contractNo:'', stages:getDefaultProcProgressStages(), note:'' };
let isEdit = false;
if (idx !== undefined) { p = projectData.procurement.progress[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑采购进度' : '新增采购进度';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>设备项名称 *</label><input id="pp-item" value="${p.item||''}" placeholder="如AGC液压缸"></div>
<div class="form-group"><label>供应商</label><input id="pp-supplier" value="${p.supplier||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>合同金额(¥)</label><input id="pp-amount" type="number" value="${p.amount||''}"></div>
<div class="form-group"><label>合同编号</label><input id="pp-contract" value="${p.contractNo||''}"></div>
</div>
<div class="form-group"><label>进度阶段(勾选=已完成,填写完成日期)</label></div>
<table class="data-table" style="margin-bottom:8px;">
<thead><tr><th style="width:30px;">✓</th><th>阶段</th><th>完成日期</th></tr></thead>
<tbody id="pp-stages">${p.stages.map((s,i)=>`
<tr>
<td><input type="checkbox" id="pps-${i}" ${s.done?'checked':''} onchange="document.getElementById('ppsd-${i}').style.display=this.checked?'':'none'"></td>
<td>${s.name}</td>
<td><input type="date" id="ppsd-${i}" value="${s.date||''}" style="${s.done?'':'display:none;'}width:140px;padding:3px 6px;font-size:11px;"></td>
</tr>`).join('')}</tbody>
</table>
<div class="form-group"><label>备注</label><textarea id="pp-note">${p.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveProcProgress(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveProcProgress(idx) {
const stages = PROC_STAGES.map((name,i) => ({
name,
done: document.getElementById('pps-'+i)?.checked || false,
date: document.getElementById('ppsd-'+i)?.value || ''
}));
const p = {
item: document.getElementById('pp-item').value,
supplier: document.getElementById('pp-supplier').value,
amount: document.getElementById('pp-amount').value,
contractNo: document.getElementById('pp-contract').value,
stages, note: document.getElementById('pp-note').value
};
if (!p.item) { showToast('请填写设备项名称'); return; }
if (!projectData.procurement.progress) projectData.procurement.progress = [];
if (idx >= 0) projectData.procurement.progress[idx] = p; else projectData.procurement.progress.push(p);
saveData(); closeModal(); renderModule('procurement'); switchProcTab('progress', document.querySelector('#proc-tabs .tab:nth-child(4)')); showToast('采购进度已保存');
}
function editProcProgress(i) { openProcProgressModal(i); }
function deleteProcProgress(i) { if (!confirm('确认删除?')) return; projectData.procurement.progress.splice(i,1); saveData(); renderModule('procurement'); switchProcTab('progress', document.querySelector('#proc-tabs .tab:nth-child(4)')); }
function autoCreateProcProgress() {
const quotes = projectData.procurement.quotes || [];
const contracts = projectData.procurement.contracts || [];
const selected = quotes.filter(q=>q.selected);
if (selected.length===0 && contracts.length===0) { showToast('请先在报价中选择供应商或创建合同'); return; }
if (!projectData.procurement.progress) projectData.procurement.progress = [];
let added = 0;
selected.forEach(q => {
const exists = projectData.procurement.progress.some(p=>p.item===q.item&&p.supplier===q.supplier);
if (!exists) {
const cnt = contracts.find(c=>c.item===q.item&&c.supplier===q.supplier);
projectData.procurement.progress.push({
item: q.item, supplier: q.supplier, amount: cnt?cnt.amount:q.price,
contractNo: cnt?cnt.number:'',
stages: getDefaultProcProgressStages().map((s,i) => {
if (i<=1) return {name:s.name,done:true,date:new Date().toISOString().slice(0,10)};
return s;
}), note: '从报价/合同自动创建'
});
added++;
}
});
saveData(); renderModule('procurement');
switchProcTab('progress', document.querySelector('#proc-tabs .tab:nth-child(4)'));
showToast(`已从报价/合同创建 ${added} 条采购进度`);
}
// ========== Manufacturing Progress Module (设备制造进度) ==========
// Stage data model:
// { name, dueDate, done, submittedDate, note,
// manufacturingPhotos: [{fileName, desc, date}], manufacturingVideo: '',
// materialReport: { uploaded, file, standard, result, date },
// precisionReport: { uploaded, instrument, instrumentModel, tolerance, measuredValue, result, reportFile, videoFile, date } }
function renderManufacturing() {
const items = projectData.manufacturing || [];
const rows = items.map((it,i) => {
const stages = it.stages || [];
const totalStages = stages.length;
const doneStages = stages.filter(s=>s.done).length;
const progressPct = totalStages>0 ? Math.round(doneStages/totalStages*100) : 0;
const penalty = calcPenalty(it);
const overdueCount = stages.filter(s=>!s.done&&s.dueDate&&new Date()>new Date(s.dueDate)).length;
const missingMat = stages.filter(s=>s.dueDate&&new Date()>new Date(s.dueDate)&&(!s.materialReport||!s.materialReport.uploaded)).length;
const missingPrec = stages.filter(s=>s.dueDate&&new Date()>new Date(s.dueDate)&&(!s.precisionReport||!s.precisionReport.uploaded)).length;
const lastSubmit = stages.reduce((latest,s)=> s.submittedDate&&s.submittedDate>latest ? s.submittedDate : latest, '');
return `
<tr id="mfg-row-${i}">
<td>${i+1}</td><td><b>${it.equipment||''}</b></td><td>${it.supplier||''}</td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;height:8px;background:#eee;border-radius:4px;overflow:hidden;"><div style="height:100%;background:${penalty>0?'var(--danger)':progressPct>=100?'var(--success)':'var(--accent)'};width:${progressPct}%;"></div></div>
<span style="font-size:12px;white-space:nowrap;font-weight:600;">${progressPct}%</span>
</div>
<div style="font-size:10px;color:var(--text2);margin-top:2px;">${doneStages}/${totalStages} 阶段完成${overdueCount>0?' · <span style=\"color:var(--danger);\">'+overdueCount+'阶段逾期</span>':''}</div>
</td>
<td>${lastSubmit||'-'}</td>
<td>
<span style="color:${penalty>0?'var(--danger)':'var(--success)'};font-weight:600;">${penalty>0?'¥'+penalty.toLocaleString():'无'}</span>
${penalty>0?`<div style="font-size:9px;color:var(--danger);">材缺${missingMat}项 · 精缺${missingPrec}项</div>`:''}
</td>
<td>
<button class="btn btn-sm btn-outline" onclick="toggleMfgDetail(${i})" style="font-size:10px;">📋 详情</button>
<button class="btn btn-sm btn-outline" onclick="editManufacturing(${i})" style="font-size:10px;">✏️ 编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteManufacturing(${i})" style="font-size:10px;">删除</button>
</td>
</tr>
<tr id="mfg-detail-${i}" style="display:none;"><td colspan="7" style="padding:0;">${renderMfgDetailPanel(it,i)}</td></tr>`;
}).join('');
return `
<div class="module-panel">
<div class="module-header"><span>🏭 设备制造进度</span><button class="btn btn-primary btn-sm" onclick="openManufacturingModal()">+ 新增设备</button></div>
<div class="module-body">
<div style="font-size:12px;color:var(--text2);margin-bottom:12px;padding:10px;background:#fff8e1;border-radius:6px;border:1px solid #ffe0b2;">
⚠️ <b>制造监督规则:</b>供应商须按约定日期定期提交制造照片/视频;每阶段须上传<b>材质检验报告</b>和<b>精度检验报告</b>(含检验仪器名称、仪器型号、检验过程视频)。
逾期未提交:材质/精度报告缺1项罚合同金额<b>1%</b>,进度逾期按<b>0.5%/天</b>计算违约金。
</div>
<table class="data-table">
<thead><tr><th>#</th><th>设备名称</th><th>供应商</th><th style="min-width:140px;">制造进度</th><th>最后提交</th><th>自动罚款</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无设备制造数据</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
// ========== Detail Panel ==========
function renderMfgDetailPanel(item, idx) {
const stages = item.stages || [];
const now = new Date();
const cards = stages.map((s,si) => {
const dueDate = s.dueDate ? new Date(s.dueDate+'T00:00:00') : null;
let headerClass = 'upcoming';
let statusTag = '<span class="tag tag-info">进行中</span>';
if (s.done) { headerClass = 'ontime'; statusTag = '<span class="tag tag-success">已完成</span>'; }
else if (dueDate && now > dueDate) { headerClass = 'overdue'; statusTag = '<span class="tag tag-danger">已逾期</span>'; }
// Manufacturing evidence
const photos = s.manufacturingPhotos || [];
const hasVideo = s.manufacturingVideo && s.manufacturingVideo.trim();
const hasPhotos = photos.length > 0;
const photosHtml = photos.map(p => `<div class="evidence-item ok"><span class="ev-icon">📷</span>${p.fileName||'照片'}${p.date?'<span style="color:#999;">('+p.date.slice(5)+')</span>':''}</div>`).join('');
const videoHtml = hasVideo ? `<div class="evidence-item ok"><span class="ev-icon">🎬</span>${s.manufacturingVideo}<span style="color:#999;">(视频)</span></div>` : '';
const noEvidence = !hasPhotos && !hasVideo;
// Material report
const mat = s.materialReport || {};
const matOk = mat.uploaded;
// Precision report
const prec = s.precisionReport || {};
const precOk = prec.uploaded;
return `
<div class="stage-detail-card">
<div class="stage-detail-header ${headerClass}" onclick="this.nextElementSibling.classList.toggle('open');this.querySelector('.arrow').textContent=this.nextElementSibling.classList.contains('open')?'▼':'▶'">
<span><span class="arrow" style="font-size:10px;">▶</span> ${si+1}. ${s.name||'未命名阶段'} ${statusTag}</span>
<span style="font-size:10px;color:var(--text2);">约定日期: ${s.dueDate||'未设定'} ${s.submittedDate?' · 提交: '+s.submittedDate:''}</span>
</div>
<div class="stage-detail-body">
<!-- 制造过程证据(照片/视频) -->
<div class="section-title">📸 制造过程证据(定期提交照片/视频)</div>
<div class="evidence-grid">
${photosHtml}
${videoHtml}
${noEvidence?'<div class="evidence-item missing"><span class="ev-icon">⚠️</span>未提交制造过程证据</div>':''}
</div>
<!-- 材质检验报告 -->
<div class="section-title">🔬 材质检验报告</div>
<table class="inspection-table">
<tr><td>报告状态</td><td class="${matOk?'pass':'fail'}">${matOk?'✓ 已上传':'✗ 未上传'}</td></tr>
<tr><td>报告文件</td><td>${mat.file||'-'}</td></tr>
<tr><td>材质标准</td><td>${mat.standard||'-'}</td></tr>
<tr><td>检验结果</td><td class="${mat.result==='合格'?'pass':mat.result==='不合格'?'fail':''}">${mat.result||'-'}</td></tr>
<tr><td>检验日期</td><td>${mat.date||'-'}</td></tr>
</table>
<!-- 精度检验报告 -->
<div class="section-title">📏 精度检验报告(必需录制检验过程视频)</div>
<table class="inspection-table">
<tr><td>报告状态</td><td class="${precOk?'pass':'fail'}">${precOk?'✓ 已上传':'✗ 未上传'}</td></tr>
<tr><td>检验仪器</td><td>${prec.instrument||'-'} ${prec.instrumentModel?'('+prec.instrumentModel+')':''}</td></tr>
<tr><td>精度要求(公差)</td><td>${prec.tolerance||'-'}</td></tr>
<tr><td>实测值</td><td>${prec.measuredValue||'-'}</td></tr>
<tr><td>判定结果</td><td class="${prec.result==='合格'?'pass':prec.result==='不合格'?'fail':''}">${prec.result||'-'}</td></tr>
<tr><td>检验报告文件</td><td>${prec.reportFile||'-'}</td></tr>
<tr><td>检验过程视频</td><td>${prec.videoFile?`<span style="color:var(--success);">🎬 ${prec.videoFile}</span>`:'<span style="color:var(--danger);">✗ 未上传检验过程视频</span>'}</td></tr>
<tr><td>检验日期</td><td>${prec.date||'-'}</td></tr>
</table>
${s.note?`<div style="font-size:10px;color:var(--text2);margin-top:6px;padding:6px;background:#f9f9f9;border-radius:3px;">📝 ${s.note}</div>`:''}
</div>
</div>`;
}).join('');
const penalty = calcPenalty(item);
const overdueCount = stages.filter(s=>!s.done&&s.dueDate&&now>new Date(s.dueDate+'T00:00:00')).length;
const missingMat = stages.filter(s=>s.dueDate&&now>new Date(s.dueDate+'T00:00:00')&&(!s.materialReport||!s.materialReport.uploaded)).length;
const missingPrec = stages.filter(s=>s.dueDate&&now>new Date(s.dueDate+'T00:00:00')&&(!s.precisionReport||!s.precisionReport.uploaded)).length;
return `
<div class="mfg-detail-panel show" style="border-top:2px solid var(--accent);">
<div class="detail-topbar">
<span><b>${item.equipment||''}</b> · ${item.supplier||'未指定供应商'} · 合同金额: <b>¥${Number(item.contractAmount||0).toLocaleString()}</b></span>
<span>
${overdueCount>0?`<span class="tag tag-danger">${overdueCount}阶段逾期</span> `:''}
${missingMat>0?`<span class="tag tag-danger">材质${missingMat}项缺</span> `:''}
${missingPrec>0?`<span class="tag tag-danger">精度${missingPrec}项缺</span> `:''}
自动罚款: <b style="color:${penalty>0?'var(--danger)':'var(--success)'};">${penalty>0?'¥'+penalty.toLocaleString():'无'}</b>
<button class="btn btn-sm btn-outline" onclick="closeMfgDetail(${idx})" style="margin-left:10px;font-size:10px;">收起</button>
</span>
</div>
<div style="padding:8px 12px;">${cards||'<div style="text-align:center;color:#aaa;padding:20px;">暂无制造阶段数据</div>'}</div>
</div>`;
}
function toggleMfgDetail(idx) {
const detailRow = document.getElementById('mfg-detail-'+idx);
if (!detailRow) return;
const panel = detailRow.querySelector('.mfg-detail-panel');
if (detailRow.style.display === 'none' || detailRow.style.display === '') {
// Close all other open details first
document.querySelectorAll('[id^="mfg-detail-"]').forEach(r => { r.style.display = 'none'; });
detailRow.style.display = '';
} else {
detailRow.style.display = 'none';
}
}
function closeMfgDetail(idx) {
const detailRow = document.getElementById('mfg-detail-'+idx);
if (detailRow) detailRow.style.display = 'none';
}
// ========== Penalty Calculation ==========
function calcPenalty(item) {
if (!item.contractAmount || !item.stages) return 0;
let totalPenalty = 0;
const now = new Date();
item.stages.forEach(s => {
if (!s.done && s.dueDate && now > new Date(s.dueDate+'T00:00:00')) {
// Overdue penalty: 0.5%/day
const dueDate = new Date(s.dueDate+'T00:00:00');
const daysOverdue = Math.ceil((now - dueDate) / (1000*60*60*24));
totalPenalty += item.contractAmount * 0.005 * daysOverdue;
}
// Missing material report when overdue: 1% per item
if (s.dueDate && now > new Date(s.dueDate+'T00:00:00') && (!s.materialReport || !s.materialReport.uploaded)) {
totalPenalty += item.contractAmount * 0.01;
}
// Missing precision report when overdue: 1% per item
if (s.dueDate && now > new Date(s.dueDate+'T00:00:00') && (!s.precisionReport || !s.precisionReport.uploaded)) {
totalPenalty += item.contractAmount * 0.01;
}
});
return Math.round(totalPenalty * 100) / 100;
}
// ========== Modal ==========
function getDefaultStage() {
return {
name: '', done: false, dueDate: '', submittedDate: '', note: '',
manufacturingPhotos: [], manufacturingVideo: '',
materialReport: { uploaded: false, file: '', standard: '', result: '', date: '' },
precisionReport: { uploaded: false, instrument: '', instrumentModel: '', tolerance: '', measuredValue: '', result: '', reportFile: '', videoFile: '', date: '' }
};
}
function openManufacturingModal(idx) {
let it = { equipment:'', supplier:'', contractAmount:'', stages:[getDefaultStage()], note:'' };
let isEdit = false;
if (idx !== undefined) {
it = JSON.parse(JSON.stringify(projectData.manufacturing[idx])); // deep copy
if (!it.stages) it.stages = [getDefaultStage()];
isEdit = true;
}
let stagesHtml = (it.stages||[]).map((s,si) => {
const mp = s.manufacturingPhotos || [];
const mat = s.materialReport || {};
const prec = s.precisionReport || {};
const photosStr = mp.map(p=>p.fileName||'').filter(Boolean).join(';');
return `
<div class="stage-modal-card" style="background:#fafbfc;border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:10px;" id="smc-${si}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<b style="font-size:12px;">阶段 ${si+1}</b>
<div style="display:flex;gap:6px;">
<button class="btn btn-sm btn-outline" onclick="toggleStageModalBody(${si})">展开/折叠</button>
<button class="btn btn-sm btn-danger" onclick="document.getElementById('smc-${si}').remove()">删除</button>
</div>
</div>
<div id="smb-${si}">
<div class="form-row">
<div class="form-group"><label>阶段名称</label><input id="ms-name-${si}" value="${s.name||''}" placeholder="如:粗车完成"></div>
<div class="form-group"><label>约定完成日期</label><input type="date" id="ms-due-${si}" value="${s.dueDate||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>已完成</label><select id="ms-done-${si}"><option value="0" ${!s.done?'selected':''}>否</option><option value="1" ${s.done?'selected':''}>是</option></select></div>
<div class="form-group"><label>提交日期</label><input type="date" id="ms-sub-${si}" value="${s.submittedDate||''}"></div>
</div>
<!-- 制造过程证据 -->
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">📸 制造过程证据(照片/视频)</div>
<div class="form-row">
<div class="form-group"><label>照片文件名(用;分隔多个)</label><input id="ms-photos-${si}" value="${photosStr}" placeholder="粗车完成_01.jpg;粗车完成_02.jpg"></div>
<div class="form-group"><label>制造过程视频文件名</label><input id="ms-video-${si}" value="${s.manufacturingVideo||''}" placeholder="粗车过程.mp4"></div>
</div>
<!-- 材质检验报告 -->
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">🔬 材质检验报告</div>
<div class="form-row">
<div class="form-group"><label>已上传</label><select id="ms-mat-up-${si}"><option value="0" ${!mat.uploaded?'selected':''}>否</option><option value="1" ${mat.uploaded?'selected':''}>是</option></select></div>
<div class="form-group"><label>报告文件名</label><input id="ms-mat-file-${si}" value="${mat.file||''}" placeholder="材质证书_Q345B_20260701.pdf"></div>
</div>
<div class="form-row">
<div class="form-group"><label>材质标准</label><input id="ms-mat-standard-${si}" value="${mat.standard||''}" placeholder="GB/T 1591-2018 Q345B"></div>
<div class="form-group"><label>检验结果</label><select id="ms-mat-result-${si}"><option value="" ${!mat.result?'selected':''}>-</option><option value="合格" ${mat.result==='合格'?'selected':''}>合格</option><option value="不合格" ${mat.result==='不合格'?'selected':''}>不合格</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>检验日期</label><input type="date" id="ms-mat-date-${si}" value="${mat.date||''}"></div>
</div>
<!-- 精度检验报告 -->
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">📏 精度检验报告(必需录制检验过程视频)</div>
<div class="form-row">
<div class="form-group"><label>已上传</label><select id="ms-prec-up-${si}"><option value="0" ${!prec.uploaded?'selected':''}>否</option><option value="1" ${prec.uploaded?'selected':''}>是</option></select></div>
<div class="form-group"><label>检验仪器名称</label><input id="ms-prec-inst-${si}" value="${prec.instrument||''}" placeholder="如:三坐标测量机"></div>
</div>
<div class="form-row">
<div class="form-group"><label>仪器型号</label><input id="ms-prec-model-${si}" value="${prec.instrumentModel||''}" placeholder="如ZEISS CONTURA G2"></div>
<div class="form-group"><label>精度要求/公差</label><input id="ms-prec-tol-${si}" value="${prec.tolerance||''}" placeholder="如±0.01mm"></div>
</div>
<div class="form-row">
<div class="form-group"><label>实测值</label><input id="ms-prec-value-${si}" value="${prec.measuredValue||''}" placeholder="如0.007mm"></div>
<div class="form-group"><label>判定结果</label><select id="ms-prec-result-${si}"><option value="" ${!prec.result?'selected':''}>-</option><option value="合格" ${prec.result==='合格'?'selected':''}>合格</option><option value="不合格" ${prec.result==='不合格'?'selected':''}>不合格</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>检验报告文件名</label><input id="ms-prec-file-${si}" value="${prec.reportFile||''}" placeholder="精度报告_轴承座_20260701.pdf"></div>
<div class="form-group"><label>检验过程视频文件名 ⚠️</label><input id="ms-prec-video-${si}" value="${prec.videoFile||''}" placeholder="精度检验过程_CMM_轴承座.mp4"></div>
</div>
<div class="form-row">
<div class="form-group"><label>检验日期</label><input type="date" id="ms-prec-date-${si}" value="${prec.date||''}"></div>
</div>
<div class="form-group" style="margin-top:6px;"><label>阶段备注</label><input id="ms-note-${si}" value="${s.note||''}" placeholder="备注信息"></div>
</div>
</div>`;
}).join('');
document.getElementById('modal-title').textContent = isEdit ? '编辑设备制造进度' : '新增设备制造进度';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>设备名称 *</label><input id="m-equipment" value="${it.equipment||''}" placeholder="如AGC液压缸"></div>
<div class="form-group"><label>供应商</label><input id="m-supplier" value="${it.supplier||''}"></div>
</div>
<div class="form-group"><label>合同金额(¥) —— 自动罚款计算基数</label><input id="m-amount" type="number" value="${it.contractAmount||''}"></div>
<div style="margin-top:10px;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;font-size:13px;">📋 制造阶段管理</span>
<button class="btn btn-sm btn-outline" onclick="addStageModal()">+ 添加阶段</button>
</div>
<div id="m-stages-container">${stagesHtml}</div>
<div class="form-group" style="margin-top:10px;"><label>备注</label><textarea id="m-note">${it.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveManufacturing(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function toggleStageModalBody(si) {
const body = document.getElementById('smb-'+si);
if (body) body.style.display = body.style.display === 'none' ? '' : 'none';
}
function addStageModal() {
const container = document.getElementById('m-stages-container');
const si = container.children.length;
const div = document.createElement('div');
div.className = 'stage-modal-card';
div.id = 'smc-'+si;
div.style.cssText = 'background:#fafbfc;border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:10px;';
div.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<b style="font-size:12px;">阶段 ${si+1}</b>
<div style="display:flex;gap:6px;">
<button class="btn btn-sm btn-outline" onclick="toggleStageModalBody(${si})">展开/折叠</button>
<button class="btn btn-sm btn-danger" onclick="document.getElementById('smc-${si}').remove()">删除</button>
</div>
</div>
<div id="smb-${si}">
<div class="form-row">
<div class="form-group"><label>阶段名称</label><input id="ms-name-${si}" placeholder="如:粗车完成"></div>
<div class="form-group"><label>约定完成日期</label><input type="date" id="ms-due-${si}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>已完成</label><select id="ms-done-${si}"><option value="0" selected>否</option><option value="1">是</option></select></div>
<div class="form-group"><label>提交日期</label><input type="date" id="ms-sub-${si}"></div>
</div>
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">📸 制造过程证据(照片/视频)</div>
<div class="form-row">
<div class="form-group"><label>照片文件名(用;分隔多个)</label><input id="ms-photos-${si}" placeholder="照片文件名"></div>
<div class="form-group"><label>制造过程视频文件名</label><input id="ms-video-${si}" placeholder="视频文件名"></div>
</div>
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">🔬 材质检验报告</div>
<div class="form-row">
<div class="form-group"><label>已上传</label><select id="ms-mat-up-${si}"><option value="0" selected>否</option><option value="1">是</option></select></div>
<div class="form-group"><label>报告文件名</label><input id="ms-mat-file-${si}" placeholder="材质证书文件"></div>
</div>
<div class="form-row">
<div class="form-group"><label>材质标准</label><input id="ms-mat-standard-${si}" placeholder="如GB/T 1591 Q345B"></div>
<div class="form-group"><label>检验结果</label><select id="ms-mat-result-${si}"><option value="" selected>-</option><option value="合格">合格</option><option value="不合格">不合格</option></select></div>
</div>
<div class="form-row"><div class="form-group"><label>检验日期</label><input type="date" id="ms-mat-date-${si}"></div></div>
<div style="font-weight:600;font-size:11px;color:var(--accent);margin-top:8px;padding:4px 0;border-top:1px dashed #ddd;">📏 精度检验报告(必需录制检验过程视频)</div>
<div class="form-row">
<div class="form-group"><label>已上传</label><select id="ms-prec-up-${si}"><option value="0" selected>否</option><option value="1">是</option></select></div>
<div class="form-group"><label>检验仪器名称</label><input id="ms-prec-inst-${si}" placeholder="如:三坐标测量机"></div>
</div>
<div class="form-row">
<div class="form-group"><label>仪器型号</label><input id="ms-prec-model-${si}" placeholder="如ZEISS CONTURA G2"></div>
<div class="form-group"><label>精度要求/公差</label><input id="ms-prec-tol-${si}" placeholder="如±0.01mm"></div>
</div>
<div class="form-row">
<div class="form-group"><label>实测值</label><input id="ms-prec-value-${si}" placeholder="如0.007mm"></div>
<div class="form-group"><label>判定结果</label><select id="ms-prec-result-${si}"><option value="" selected>-</option><option value="合格">合格</option><option value="不合格">不合格</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>检验报告文件名</label><input id="ms-prec-file-${si}" placeholder="精度报告文件"></div>
<div class="form-group"><label>检验过程视频文件名 ⚠️</label><input id="ms-prec-video-${si}" placeholder="检验过程录像文件"></div>
</div>
<div class="form-row"><div class="form-group"><label>检验日期</label><input type="date" id="ms-prec-date-${si}"></div></div>
<div class="form-group" style="margin-top:6px;"><label>阶段备注</label><input id="ms-note-${si}" placeholder="备注信息"></div>
</div>`;
container.appendChild(div);
}
function saveManufacturing(idx) {
const container = document.getElementById('m-stages-container');
const stages = [];
if (container) {
for (let i = 0; i < container.children.length; i++) {
const el = document.getElementById('ms-name-'+i);
if (!el) continue; // stage card was removed
const photosStr = (document.getElementById('ms-photos-'+i)?.value || '').trim();
const photoFiles = photosStr ? photosStr.split(';').map(fn => ({fileName: fn.trim(), desc: '', date: ''})).filter(p=>p.fileName) : [];
stages.push({
name: document.getElementById('ms-name-'+i)?.value || '',
done: document.getElementById('ms-done-'+i)?.value === '1',
dueDate: document.getElementById('ms-due-'+i)?.value || '',
submittedDate: document.getElementById('ms-sub-'+i)?.value || '',
manufacturingPhotos: photoFiles,
manufacturingVideo: document.getElementById('ms-video-'+i)?.value || '',
materialReport: {
uploaded: document.getElementById('ms-mat-up-'+i)?.value === '1',
file: document.getElementById('ms-mat-file-'+i)?.value || '',
standard: document.getElementById('ms-mat-standard-'+i)?.value || '',
result: document.getElementById('ms-mat-result-'+i)?.value || '',
date: document.getElementById('ms-mat-date-'+i)?.value || ''
},
precisionReport: {
uploaded: document.getElementById('ms-prec-up-'+i)?.value === '1',
instrument: document.getElementById('ms-prec-inst-'+i)?.value || '',
instrumentModel: document.getElementById('ms-prec-model-'+i)?.value || '',
tolerance: document.getElementById('ms-prec-tol-'+i)?.value || '',
measuredValue: document.getElementById('ms-prec-value-'+i)?.value || '',
result: document.getElementById('ms-prec-result-'+i)?.value || '',
reportFile: document.getElementById('ms-prec-file-'+i)?.value || '',
videoFile: document.getElementById('ms-prec-video-'+i)?.value || '',
date: document.getElementById('ms-prec-date-'+i)?.value || ''
},
note: document.getElementById('ms-note-'+i)?.value || ''
});
}
}
const it = {
equipment: document.getElementById('m-equipment').value,
supplier: document.getElementById('m-supplier').value,
contractAmount: Number(document.getElementById('m-amount').value) || 0,
stages,
note: document.getElementById('m-note').value
};
if (!it.equipment) { showToast('请填写设备名称'); return; }
if (!projectData.manufacturing) projectData.manufacturing = [];
if (idx >= 0) projectData.manufacturing[idx] = it; else projectData.manufacturing.push(it);
saveData(); closeModal(); renderModule('manufacturing'); showToast('制造进度已保存');
}
function editManufacturing(i) { openManufacturingModal(i); }
function deleteManufacturing(i) { if (!confirm('确认删除?')) return; projectData.manufacturing.splice(i,1); saveData(); renderModule('manufacturing'); }
// ========== Drawing Compare Module (图纸优化比较) ==========
function renderDrawingCompare() {
const items = projectData.drawingCompare || [];
const rows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.name||''}</td>
<td>${it.beforeVersion||''}</td><td>${it.afterVersion||''}</td>
<td>${it.optimizer||''}</td><td>${it.date||''}</td>
<td><span class="status-badge ${it.status}">${it.status==='approved'?'已确认':it.status==='pending'?'待确认':'已驳回'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="viewCompare(${i})">查看对比</button> <button class="btn btn-sm btn-danger" onclick="deleteCompare(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>🔄 图纸优化前后比较</span><button class="btn btn-primary btn-sm" onclick="openCompareModal()">+ 新增对比</button></div>
<div class="module-body">
<div style="font-size:12px;color:var(--text2);margin-bottom:12px;padding:10px;background:#e8f4fd;border-radius:6px;border:1px solid #b3d8fd;">
💡 上传优化前后的图纸信息,记录优化内容和效果对比,便于追溯设计改进过程。
</div>
<table class="data-table">
<thead><tr><th>#</th><th>图纸名称</th><th>优化前版本</th><th>优化后版本</th><th>优化人</th><th>优化日期</th><th>状态</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="8" style="text-align:center;color:#aaa;">暂无对比数据</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openCompareModal(idx) {
let it = { name:'', beforeVersion:'', afterVersion:'', optimizer:'', date:new Date().toISOString().slice(0,10), status:'pending', beforeDesc:'', afterDesc:'', improvements:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.drawingCompare[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑图纸对比' : '新增图纸优化对比';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>图纸名称</label><input id="dc-name" value="${it.name||''}"></div>
<div class="form-group"><label>优化人</label><input id="dc-optimizer" value="${it.optimizer||''}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>优化前版本</label><input id="dc-before" value="${it.beforeVersion||''}" placeholder="如V1.0"></div>
<div class="form-group"><label>优化后版本</label><input id="dc-after" value="${it.afterVersion||''}" placeholder="如V2.0"></div>
<div class="form-group"><label>优化日期</label><input id="dc-date" type="date" value="${it.date||''}"></div>
</div>
<div class="form-group"><label>优化前问题描述</label><textarea id="dc-before-desc" placeholder="优化前存在的问题">${it.beforeDesc||''}</textarea></div>
<div class="form-group"><label>优化后改进内容</label><textarea id="dc-after-desc" placeholder="优化后改进的内容">${it.afterDesc||''}</textarea></div>
<div class="form-group"><label>优化效果评价</label><textarea id="dc-improvements" placeholder="优化效果刚度提升15%振动降低20%">${it.improvements||''}</textarea></div>
<div class="form-group"><label>状态</label><select id="dc-status">
<option value="pending" ${it.status==='pending'?'selected':''}>待确认</option>
<option value="approved" ${it.status==='approved'?'selected':''}>已确认</option>
<option value="rejected" ${it.status==='rejected'?'selected':''}>已驳回</option>
</select></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveCompare(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveCompare(idx) {
const it = { name:document.getElementById('dc-name').value, beforeVersion:document.getElementById('dc-before').value, afterVersion:document.getElementById('dc-after').value, optimizer:document.getElementById('dc-optimizer').value, date:document.getElementById('dc-date').value, status:document.getElementById('dc-status').value, beforeDesc:document.getElementById('dc-before-desc').value, afterDesc:document.getElementById('dc-after-desc').value, improvements:document.getElementById('dc-improvements').value };
if (!it.name) { showToast('请填写图纸名称'); return; }
if (!projectData.drawingCompare) projectData.drawingCompare = [];
if (idx >= 0) projectData.drawingCompare[idx] = it; else projectData.drawingCompare.push(it);
saveData(); closeModal(); renderModule('drawing_compare'); showToast('对比记录已保存');
}
function viewCompare(i) {
const it = projectData.drawingCompare[i];
if (!it) return;
document.getElementById('modal-title').textContent = '图纸优化对比:' + it.name;
document.getElementById('modal-body').innerHTML = `
<div class="compare-table">
<div class="cell header-cell">对比项</div><div class="cell header-cell">优化前(${it.beforeVersion}</div><div class="cell header-cell">优化后(${it.afterVersion}</div>
<div class="cell">问题描述</div><div class="cell">${it.beforeDesc||'无'}</div><div class="cell">${it.afterDesc||'无'}</div>
<div class="cell">改进效果</div><div class="cell" style="color:var(--success);font-weight:600;">${it.improvements||'无'}</div>
</div>
<div style="margin-top:12px;text-align:right;"><button class="btn btn-primary" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function editCompare(i) { openCompareModal(i); }
function deleteCompare(i) { if (!confirm('确认删除?')) return; projectData.drawingCompare.splice(i,1); saveData(); renderModule('drawing_compare'); }
// ========== Doc Library Module (图纸资料库) ==========
function renderDocLib() {
const items = projectData.docLib || [];
const rows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.name||''}</td><td>${it.category||''}</td><td>${it.version||''}</td>
<td>${it.uploader||''}</td><td>${it.uploadDate||''}</td>
<td><button class="btn btn-sm btn-outline" onclick="viewDoc(${i})">查看</button> <button class="btn btn-sm btn-danger" onclick="deleteDoc(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>📁 图纸资料库</span><button class="btn btn-primary btn-sm" onclick="openDocModal()">+ 上传资料</button></div>
<div class="module-body">
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group" style="flex:1;"><input id="doc-search" placeholder="🔍 搜索资料..." oninput="filterDocs()" style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;"></div>
</div>
<table class="data-table">
<thead><tr><th>#</th><th>文件名称</th><th>分类</th><th>版本</th><th>上传人</th><th>上传日期</th><th>操作</th></tr></thead>
<tbody id="doc-lib-tbody">${rows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无资料,点击"上传资料"</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openDocModal(idx) {
let it = { name:'', category:'图纸', version:'V1.0', uploader:'', uploadDate:new Date().toISOString().slice(0,10), fileUrl:'', description:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.docLib[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑资料' : '上传资料';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>文件名称</label><input id="dl-name" value="${it.name||''}" placeholder="文件名称"></div>
<div class="form-row">
<div class="form-group"><label>分类</label><select id="dl-category">
<option value="图纸" ${it.category==='图纸'?'selected':''}>图纸</option>
<option value="计算书" ${it.category==='计算书'?'selected':''}>计算书</option>
<option value="说明书" ${it.category==='说明书'?'selected':''}>说明书</option>
<option value="检验报靠" ${it.category==='检验报告'?'selected':''}>检验报告</option>
<option value="材质证书" ${it.category==='材质证书'?'selected':''}>材质证书</option>
<option value="其他" ${it.category==='其他'?'selected':''}>其他</option>
</select></div>
<div class="form-group"><label>版本</label><input id="dl-version" value="${it.version||'V1.0'}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>上传人</label><input id="dl-uploader" value="${it.uploader||''}"></div>
<div class="form-group"><label>上传日期</label><input id="dl-date" type="date" value="${it.uploadDate||''}"></div>
</div>
<div class="form-group"><label>文件链接/路径</label><input id="dl-url" value="${it.fileUrl||''}" placeholder="文件存储路径或链接"></div>
<div class="form-group"><label>描述</label><textarea id="dl-desc">${it.description||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveDoc(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveDoc(idx) {
const it = { name:document.getElementById('dl-name').value, category:document.getElementById('dl-category').value, version:document.getElementById('dl-version').value, uploader:document.getElementById('dl-uploader').value, uploadDate:document.getElementById('dl-date').value, fileUrl:document.getElementById('dl-url').value, description:document.getElementById('dl-desc').value };
if (!it.name) { showToast('请填写文件名称'); return; }
if (!projectData.docLib) projectData.docLib = [];
if (idx >= 0) projectData.docLib[idx] = it; else projectData.docLib.push(it);
saveData(); closeModal(); renderModule('doc_lib'); showToast('资料已保存');
}
function viewDoc(i) {
const it = projectData.docLib[i];
document.getElementById('modal-title').textContent = '查看资料:' + it.name;
document.getElementById('modal-body').innerHTML = `
<div style="font-size:13px;line-height:1.8;">
<p><b>文件名称:</b>${it.name}</p>
<p><b>分类:</b>${it.category} <b>版本:</b>${it.version}</p>
<p><b>上传人:</b>${it.uploader} <b>上传日期:</b>${it.uploadDate}</p>
<p><b>文件链接:</b>${it.fileUrl||'无'}</p>
<p><b>描述:</b>${it.description||'无'}</p>
</div>
<div style="margin-top:12px;text-align:right;"><button class="btn btn-primary" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function deleteDoc(i) { if (!confirm('确认删除?')) return; projectData.docLib.splice(i,1); saveData(); renderModule('doc_lib'); }
function filterDocs() { /* frontend filter placeholder */ }
// ========== Site Modification Module (现场修改管理) ==========
function renderSiteMod() {
const items = projectData.siteMod || [];
const rows = items.map((it,i) => {
const imgCount = (it.images||[]).length;
const vidCount = (it.videos||[]).length;
const imgTag = imgCount > 0
? `<span class="tag-media tag-media-img" onclick="viewSiteModMedia(${i},'images')" title="查看图片">📷 ${imgCount}</span>`
: `<span class="tag-media tag-media-none">📷 0</span>`;
const vidTag = vidCount > 0
? `<span class="tag-media tag-media-vid" onclick="viewSiteModMedia(${i},'videos')" title="查看视频">🎬 ${vidCount}</span>`
: `<span class="tag-media tag-media-none">🎬 0</span>`;
return `
<tr>
<td>${i+1}</td><td>${it.equipment||''}</td><td>${it.location||''}</td><td>${it.issue||''}</td>
<td>${it.modifier||''}</td><td>${it.date||''}</td>
<td><span class="status-badge ${it.status}">${it.status==='done'?'已整改':'待整改'}</span></td>
<td style="text-align:center;">${imgTag}</td>
<td style="text-align:center;">${vidTag}</td>
<td><button class="btn btn-sm btn-outline" onclick="editSiteMod(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteSiteMod(${i})">删除</button></td>
</tr>`;
}).join('');
return `
<div class="module-panel">
<div class="module-header"><span>🔧 现场修改管理(防止问题重复发生)</span><button class="btn btn-primary btn-sm" onclick="openSiteModModal()">+ 记录现场修改</button></div>
<div class="module-body">
<div style="font-size:12px;color:var(--text2);margin-bottom:12px;padding:10px;background:#fff3cd;border-radius:6px;border:1px solid #ffe0b2;">
⚠️ <b>重要:</b>现场修改后的设备必须上传修改后的图纸至资料库,防止下次再次出现同样问题。所有现场修改记录将自动同步至图纸资料库。
</div>
<table class="data-table">
<thead><tr><th>#</th><th>设备名称</th><th>修改位置</th><th>问题描述</th><th>修改人</th><th>修改日期</th><th>状态</th><th>📷图片</th><th>🎬视频</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="10" style="text-align:center;color:#aaa;">暂无现场修改记录</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openSiteModModal(idx) {
let it = { equipment:'', location:'', issue:'', solution:'', modifier:'', date:new Date().toISOString().slice(0,10), status:'pending', preventAction:'', drawingUpdated:false, images:[], videos:[] };
let isEdit = false;
if (idx !== undefined) { it = { ...it, ...projectData.siteMod[idx] }; isEdit = true; }
// Initialize pending media from existing data
window._pendingSiteModMedia = { images: [...(it.images||[])], videos: [...(it.videos||[])] };
const existingImages = window._pendingSiteModMedia.images;
const existingVideos = window._pendingSiteModMedia.videos;
const imgGridHtml = existingImages.length > 0
? `<div class="sm-media-grid" id="sm-image-grid">${existingImages.map((img,imgIdx) => `
<div class="sm-media-thumb">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.parentElement.style.display='none'">
<div class="sm-thumb-name" title="${img.name||''}">${img.name||''}</div>
<button class="sm-thumb-remove" onclick="event.stopPropagation();removeSiteModImage(${imgIdx})" title="移除">×</button>
</div>`).join('')}</div>`
: '';
const vidListHtml = existingVideos.length > 0
? `<div id="sm-video-list">${existingVideos.map((v,vidIdx) => `
<div class="sm-video-item">
<span class="vi-icon">🎬</span>
<div class="vi-info">
<div class="vi-name">${v.name||''}</div>
<div class="vi-size">${formatFileSize(v.size||0)}</div>
</div>
<span class="vi-remove" onclick="removeSiteModVideo(${vidIdx})" title="移除">×</span>
</div>`).join('')}</div>`
: '';
document.getElementById('modal-title').textContent = isEdit ? '编辑现场修改' : '记录现场修改';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group"><label>设备名称</label><input id="sm-equipment" value="${it.equipment||''}"></div>
<div class="form-group"><label>修改位置/部位</label><input id="sm-location" value="${it.location||''}" placeholder="如:主传动轴轴承座"></div>
</div>
<div class="form-group"><label>现场问题描述</label><textarea id="sm-issue">${it.issue||''}</textarea></div>
<div class="form-group"><label>修改方案/解决措施</label><textarea id="sm-solution">${it.solution||''}</textarea></div>
<div class="form-row">
<div class="form-group sm-media-section">
<label>📷 现场照片 <span style="font-weight:400;color:var(--text2);">(可多选)</span></label>
<div class="sm-upload-zone" onclick="document.getElementById('sm-image-input').click()">
<div class="uz-icon">📤</div>
<div class="uz-text">点击选择现场照片</div>
<div class="uz-hint">支持 JPG/PNG/WebP单张 ≤ 5MB</div>
</div>
<input type="file" id="sm-image-input" accept="image/*" multiple style="display:none;" onchange="handleSiteModImageSelect(this)">
${imgGridHtml}
</div>
<div class="form-group sm-media-section">
<label>🎬 现场视频 <span style="font-weight:400;color:var(--text2);">(可选)</span></label>
<div class="sm-upload-zone" onclick="document.getElementById('sm-video-input').click()">
<div class="uz-icon">🎥</div>
<div class="uz-text">点击选择现场视频</div>
<div class="uz-hint">支持 MP4/WebM单段 ≤ 100MB</div>
</div>
<input type="file" id="sm-video-input" accept="video/*" style="display:none;" onchange="handleSiteModVideoSelect(this)">
${vidListHtml}
</div>
</div>
<div class="form-row">
<div class="form-group"><label>修改人</label><input id="sm-modifier" value="${it.modifier||''}"></div>
<div class="form-group"><label>修改日期</label><input id="sm-date" type="date" value="${it.date||''}"></div>
<div class="form-group"><label>状态</label><select id="sm-status">
<option value="pending" ${it.status==='pending'?'selected':''}>待整改</option>
<option value="done" ${it.status==='done'?'selected':''}>已整改</option>
</select></div>
</div>
<div class="form-group"><label>防止再发措施(经验反馈)</label><textarea id="sm-prevent" placeholder="记录防止同样问题再次发生的措施">${it.preventAction||''}</textarea></div>
<div style="margin-top:8px;"><label style="display:flex;align-items:center;gap:6px;font-size:13px;"><input type="checkbox" id="sm-drawing-updated" ${it.drawingUpdated?'checked':''}> 已上传修改后的图纸至资料库</label></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveSiteMod(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveSiteMod(idx) {
const pending = window._pendingSiteModMedia || { images: [], videos: [] };
const it = {
equipment:document.getElementById('sm-equipment').value,
location:document.getElementById('sm-location').value,
issue:document.getElementById('sm-issue').value,
solution:document.getElementById('sm-solution').value,
modifier:document.getElementById('sm-modifier').value,
date:document.getElementById('sm-date').value,
status:document.getElementById('sm-status').value,
preventAction:document.getElementById('sm-prevent').value,
drawingUpdated:document.getElementById('sm-drawing-updated')?.checked||false,
images: pending.images,
videos: pending.videos
};
if (!it.equipment) { showToast('请填写设备名称'); return; }
if (!projectData.siteMod) projectData.siteMod = [];
if (idx >= 0) projectData.siteMod[idx] = it; else projectData.siteMod.push(it);
window._pendingSiteModMedia = null;
saveData(); closeModal(); renderModule('site_mod');
const imgN = it.images.length; const vidN = it.videos.length;
showToast(`现场修改已保存${imgN>0||vidN>0?'(📷'+imgN+'张照片 🎬'+vidN+'段视频)':''}`);
}
function editSiteMod(i) { openSiteModModal(i); }
function deleteSiteMod(i) { if (!confirm('确认删除?')) return; projectData.siteMod.splice(i,1); saveData(); renderModule('site_mod'); }
// ========== Site Mod Media Handlers ==========
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/(1024*1024)).toFixed(1) + ' MB';
}
function handleSiteModImageSelect(input) {
const files = Array.from(input.files);
if (files.length === 0) return;
// Check total size
let totalSize = 0;
files.forEach(f => { totalSize += f.size; if (f.size > 5*1024*1024) { showToast(`图片 "${f.name}" 超过5MB限制`); return; }});
if (!window._pendingSiteModMedia) window._pendingSiteModMedia = { images: [], videos: [] };
let loaded = 0;
const total = files.length;
files.forEach(file => {
if (file.size > 5*1024*1024) return; // skip oversized
const reader = new FileReader();
reader.onload = function(e) {
window._pendingSiteModMedia.images.push({
name: file.name,
size: file.size,
type: file.type,
data: e.target.result
});
loaded++;
if (loaded === total || loaded === files.filter(f=>f.size<=5*1024*1024).length) {
refreshSiteModImageGrid();
showToast(`已添加 ${loaded} 张照片`);
}
};
reader.readAsDataURL(file);
});
input.value = '';
}
function removeSiteModImage(imgIdx) {
if (!window._pendingSiteModMedia) return;
window._pendingSiteModMedia.images.splice(imgIdx, 1);
refreshSiteModImageGrid();
}
function refreshSiteModImageGrid() {
const grid = document.getElementById('sm-image-grid');
const images = (window._pendingSiteModMedia && window._pendingSiteModMedia.images) || [];
if (!grid && images.length === 0) return;
if (images.length === 0) {
if (grid) grid.remove();
return;
}
const html = images.map((img, imgIdx) => `
<div class="sm-media-thumb">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.parentElement.style.display='none'">
<div class="sm-thumb-name" title="${img.name||''}">${img.name||''}</div>
<button class="sm-thumb-remove" onclick="event.stopPropagation();removeSiteModImage(${imgIdx})" title="移除">×</button>
</div>`).join('');
if (grid) {
grid.innerHTML = html;
} else {
const uploadZone = document.querySelector('#sm-image-input')?.closest('.sm-media-section')?.querySelector('.sm-upload-zone');
if (uploadZone) {
const newGrid = document.createElement('div');
newGrid.className = 'sm-media-grid';
newGrid.id = 'sm-image-grid';
newGrid.innerHTML = html;
uploadZone.after(newGrid);
}
}
}
function handleSiteModVideoSelect(input) {
const files = Array.from(input.files);
if (files.length === 0) return;
if (!window._pendingSiteModMedia) window._pendingSiteModMedia = { images: [], videos: [] };
let added = 0;
files.forEach(file => {
if (file.size > 100*1024*1024) { showToast(`视频 "${file.name}" 超过100MB限制`); return; }
// Videos are stored as metadata only (no data URL due to size)
window._pendingSiteModMedia.videos.push({
name: file.name,
size: file.size,
type: file.type
});
added++;
});
if (added > 0) {
refreshSiteModVideoList();
showToast(`已添加 ${added} 段视频`);
}
input.value = '';
}
function removeSiteModVideo(vidIdx) {
if (!window._pendingSiteModMedia) return;
window._pendingSiteModMedia.videos.splice(vidIdx, 1);
refreshSiteModVideoList();
}
function refreshSiteModVideoList() {
const list = document.getElementById('sm-video-list');
const videos = (window._pendingSiteModMedia && window._pendingSiteModMedia.videos) || [];
if (!list && videos.length === 0) return;
if (videos.length === 0) {
if (list) list.remove();
return;
}
const html = videos.map((v, vidIdx) => `
<div class="sm-video-item">
<span class="vi-icon">🎬</span>
<div class="vi-info">
<div class="vi-name">${v.name||''}</div>
<div class="vi-size">${formatFileSize(v.size||0)}</div>
</div>
<span class="vi-remove" onclick="removeSiteModVideo(${vidIdx})" title="移除">×</span>
</div>`).join('');
if (list) {
list.innerHTML = html;
} else {
const uploadZone = document.querySelector('#sm-video-input')?.closest('.sm-media-section')?.querySelector('.sm-upload-zone');
if (uploadZone) {
const newList = document.createElement('div');
newList.id = 'sm-video-list';
newList.innerHTML = html;
uploadZone.after(newList);
}
}
}
function viewSiteModMedia(idx, type) {
const item = projectData.siteMod[idx];
if (!item) return;
const images = item.images || [];
const videos = item.videos || [];
if (type === 'images' && images.length === 0) { showToast('该记录暂无照片'); return; }
if (type === 'videos' && videos.length === 0) { showToast('该记录暂无视频'); return; }
let contentHtml = '';
if (type === 'images') {
contentHtml = `<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">📷 共 ${images.length} 张现场照片</div>
<div class="sm-media-viewer">${images.map(img => `
<div class="mv-image">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.style.display='none';this.parentElement.querySelector('.mv-caption').textContent='⚠️ 图片加载失败';">
<div class="mv-caption">${img.name||''}${img.size?' <span style="color:#aaa;">('+formatFileSize(img.size)+')</span>':''}</div>
</div>`).join('')}</div>`;
} else {
contentHtml = `<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">🎬 共 ${videos.length} 段现场视频</div>
<div class="sm-media-viewer">${videos.map(v => `
<div class="mv-video">
<div class="mv-icon">🎬</div>
<div class="mv-name">${v.name||'未命名视频'}</div>
<div class="mv-size">${formatFileSize(v.size||0)}</div>
</div>`).join('')}</div>
<div style="font-size:10px;color:var(--text2);margin-top:8px;">💡 视频存储为文件引用,实际文件请从公司服务器获取。</div>`;
}
document.getElementById('modal-title').textContent = `${type==='images'?'📷':'🎬'} ${item.equipment||'设备'} - ${type==='images'?'现场照片':'现场视频'}`;
document.getElementById('modal-body').innerHTML = contentHtml + `
<div style="text-align:right;margin-top:16px;"><button class="btn btn-outline" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
// ========== Shipping Module (发货前设备清单) ==========
function renderShipping() {
const sc = projectData.shipping.checklist || [];
const items = projectData.shipping.items || [];
const checklistHtml = sc.map((it,i) => `
<div class="checklist-item ${it.checked?'checked':''}">
<input type="checkbox" ${it.checked?'checked':''} onchange="toggleShippingCheck(${i});">
<span class="item-text">${it.text}</span>
</div>
`).join('');
const itemRows = items.map((it,i) => {
const photoCount = (it.photos||[]).length;
return `
<tr>
<td>${i+1}</td><td><b>${it.name||''}</b></td><td>${it.spec||''}</td><td>${it.qty||''}</td>
<td><span class="status-badge ${it.packed?'done':'pending'}">${it.packed?'已打包':'未打包'}</span></td>
<td>
${photoCount>0
? `<span class="tag tag-success" style="cursor:pointer;" title="${(it.photos||[]).join(', ')}">📷 ${photoCount}张</span>`
: '<span class="tag tag-warning">📷 0张</span>'}
</td>
<td>
<button class="btn btn-sm btn-outline" onclick="openShippingItemModal(${i})">编辑</button>
<button class="btn btn-sm btn-outline" onclick="viewShippingPhotos(${i})" style="font-size:10px;">🖼️</button>
<button class="btn btn-sm btn-danger" onclick="deleteShippingItem(${i})">删除</button>
</td>
</tr>`;
}).join('');
const allDone = sc.every(it=>it.checked);
return `
<div class="module-panel">
<div class="module-header"><span>📦 发货前设备清单</span><button class="btn btn-primary btn-sm" onclick="openShippingItemModal()">+ 添加设备项</button></div>
<div class="module-body">
<div style="margin-bottom:16px;"><div style="font-weight:600;margin-bottom:8px;">📋 发货前检查清单 ${allDone?'✅ 全部完成':'⚠️ 尚有未完成项'}</div>${checklistHtml}</div>
<hr style="margin:16px 0;border:none;border-top:1px solid var(--border);">
<div style="font-weight:600;margin-bottom:8px;">📦 设备清单明细</div>
<table class="data-table">
<thead><tr><th>#</th><th>设备名称</th><th>规格</th><th>数量</th><th>打包状态</th><th>照片</th><th>操作</th></tr></thead>
<tbody>${itemRows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无设备清单</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function toggleShippingCheck(i) {
projectData.shipping.checklist[i].checked = !projectData.shipping.checklist[i].checked;
saveData(); renderModule('shipping');
}
function openShippingItemModal(idx) {
let it = { name:'', spec:'', qty:'', packed:false, note:'', photos:[] };
let isEdit = false;
if (idx !== undefined) { it = projectData.shipping.items[idx]; isEdit = true; }
const photosStr = (it.photos||[]).join(';');
document.getElementById('modal-title').textContent = isEdit ? '编辑设备项' : '添加设备项';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>设备名称 *</label><input id="si-name" value="${it.name||''}"></div>
<div class="form-row">
<div class="form-group"><label>规格型号</label><input id="si-spec" value="${it.spec||''}"></div>
<div class="form-group"><label>数量</label><input id="si-qty" type="number" value="${it.qty||''}"></div>
</div>
<div style="margin-top:8px;"><label style="display:flex;align-items:center;gap:6px;font-size:13px;"><input type="checkbox" id="si-packed" ${it.packed?'checked':''}> 已打包</label></div>
<div class="form-group" style="margin-top:10px;">
<label>📷 设备照片(文件名,用 ; 分隔多个文件)</label>
<input id="si-photos" value="${photosStr}" placeholder="主机整体.jpg;铭牌特写.jpg;打包外观.jpg">
${photosStr ? `<div style="font-size:10px;color:var(--text2);margin-top:3px;">已上传 ${(it.photos||[]).length} 张照片</div>` : ''}
</div>
<div class="form-group" style="margin-top:8px;"><label>备注</label><textarea id="si-note">${it.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveShippingItem(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveShippingItem(idx) {
const photosStr = (document.getElementById('si-photos')?.value || '').trim();
const photos = photosStr ? photosStr.split(';').map(f=>f.trim()).filter(Boolean) : [];
const it = {
name: document.getElementById('si-name').value,
spec: document.getElementById('si-spec').value,
qty: document.getElementById('si-qty').value,
packed: document.getElementById('si-packed')?.checked || false,
photos,
note: document.getElementById('si-note').value
};
if (!it.name) { showToast('请填写设备名称'); return; }
if (!projectData.shipping.items) projectData.shipping.items = [];
if (idx >= 0) projectData.shipping.items[idx] = it; else projectData.shipping.items.push(it);
saveData(); closeModal(); renderModule('shipping'); showToast('设备项已保存');
}
function deleteShippingItem(i) { if (!confirm('确认删除?')) return; projectData.shipping.items.splice(i,1); saveData(); renderModule('shipping'); }
function viewShippingPhotos(idx) {
const item = projectData.shipping.items[idx];
if (!item || !item.photos || item.photos.length === 0) { showToast('该设备暂无照片'); return; }
const photoList = item.photos.map((p,i) => `<div class="evidence-item ok" style="margin-bottom:4px;"><span class="ev-icon">📷</span>${p}</div>`).join('');
document.getElementById('modal-title').textContent = `📷 ${item.name} - 设备照片`;
document.getElementById('modal-body').innerHTML = `
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">共 ${item.photos.length} 张照片</div>
<div style="display:flex;flex-direction:column;gap:4px;">${photoList}</div>
<div style="text-align:right;margin-top:16px;"><button class="btn btn-outline" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
// ========== Manuals Module (设备说明书和图纸) ==========
function renderManuals() {
const items = projectData.manuals || [];
const rows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.name||''}</td><td>${it.type||''}</td><td>${it.version||''}</td>
<td>${it.uploadDate||''}</td>
<td><button class="btn btn-sm btn-outline" onclick="editManual(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteManual(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>📖 设备说明书和图纸</span><button class="btn btn-primary btn-sm" onclick="openManualModal()">+ 上传说明书/图纸</button></div>
<div class="module-body">
<table class="data-table">
<thead><tr><th>#</th><th>文件名称</th><th>类型</th><th>版本</th><th>上传日期</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="6" style="text-align:center;color:#aaa;">暂无说明书和图纸</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openManualModal(idx) {
let it = { name:'', type:'说明书', version:'V1.0', uploadDate:new Date().toISOString().slice(0,10), fileUrl:'', description:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.manuals[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑' : '上传说明书/图纸';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>文件名称</label><input id="man-name" value="${it.name||''}" placeholder="如1380mm轧机操作说明书"></div>
<div class="form-row">
<div class="form-group"><label>类型</label><select id="man-type">
<option value="说明书" ${it.type==='说明书'?'selected':''}>说明书</option>
<option value="图纸" ${it.type==='图纸'?'selected':''}>图纸</option>
<option value="维护手册" ${it.type==='维护手册'?'selected':''}>维护手册</option>
<option value="备件清单" ${it.type==='备件清单'?'selected':''}>备件清单</option>
</select></div>
<div class="form-group"><label>版本</label><input id="man-version" value="${it.version||'V1.0'}"></div>
</div>
<div class="form-group"><label>上传日期</label><input id="man-date" type="date" value="${it.uploadDate||''}"></div>
<div class="form-group"><label>文件链接/路径</label><input id="man-url" value="${it.fileUrl||''}"></div>
<div class="form-group"><label>描述</label><textarea id="man-desc">${it.description||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveManual(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveManual(idx) {
const it = { name:document.getElementById('man-name').value, type:document.getElementById('man-type').value, version:document.getElementById('man-version').value, uploadDate:document.getElementById('man-date').value, fileUrl:document.getElementById('man-url').value, description:document.getElementById('man-desc').value };
if (!it.name) { showToast('请填写文件名称'); return; }
if (!projectData.manuals) projectData.manuals = [];
if (idx >= 0) projectData.manuals[idx] = it; else projectData.manuals.push(it);
saveData(); closeModal(); renderModule('manuals'); showToast('已保存');
}
function editManual(i) { openManualModal(i); }
function deleteManual(i) { if (!confirm('确认删除?')) return; projectData.manuals.splice(i,1); saveData(); renderModule('manuals'); }
// ========== Install Prep Module (设备安装前准备) ==========
function renderInstallPrep() {
return `
<div class="tabs" id="install-prep-tabs">
<div class="tab active" onclick="switchInstallPrepTab('tools',this)">🔧 工具准备</div>
<div class="tab" onclick="switchInstallPrepTab('personnel',this)">👷 安装人员</div>
<div class="tab" onclick="switchInstallPrepTab('precision',this)">📏 安装精度</div>
<div class="tab" onclick="switchInstallPrepTab('progress',this)">📊 安装进度</div>
</div>
<div id="install-prep-content">${renderInstallPrepTools()}</div>`;
}
function switchInstallPrepTab(tab, el) {
document.querySelectorAll('#install-prep-tabs .tab').forEach(t=>t.classList.remove('active')); el.classList.add('active');
if (tab==='tools') document.getElementById('install-prep-content').innerHTML = renderInstallPrepTools();
else if (tab==='personnel') document.getElementById('install-prep-content').innerHTML = renderInstallPrepPersonnel();
else if (tab==='precision') document.getElementById('install-prep-content').innerHTML = renderInstallPrepPrecision();
else document.getElementById('install-prep-content').innerHTML = renderInstallPrepProgress();
}
function renderInstallPrepTools() {
const tools = projectData.installPrep.tools || [];
const catOrder = ['起重吊装','测量仪器','机械安装','液压专用','电气安装'];
const catIcons = {'起重吊装':'🏗️','测量仪器':'📐','机械安装':'🔩','液压专用':'💧','电气安装':'⚡'};
const grouped = {};
tools.forEach((t,i)=>{ const cat = t.category||'其他'; if(!grouped[cat])grouped[cat]=[]; grouped[cat].push({...t,_i:i}); });
let sections = '';
let grandTotal = 0;
catOrder.forEach(cat=>{
const items = grouped[cat]||[];
if(items.length===0) return;
const catTotal = items.reduce((s,t)=>s+(parseFloat(t.totalPrice)||0),0);
grandTotal += catTotal;
const rows = items.map(t=>{
const prioLabel = t.priority==='★★'?'<span class="tag tag-danger">关键</span>':t.priority==='★'?'<span class="tag tag-warning">重要</span>':'';
return `<tr>
<td>${t._i+1}</td><td><b title="${t.nameEn||''}">${t.name}</b></td><td style="font-size:10px;">${t.spec||''}</td><td>${t.qty||''}</td><td>${t.unit||''}</td>
<td style="text-align:right;">${t.unitPrice?Number(t.unitPrice).toLocaleString():''}</td>
<td style="text-align:right;font-weight:600;">${t.totalPrice?Number(t.totalPrice).toLocaleString():''}</td>
<td>${prioLabel}</td><td style="font-size:10px;">${t.purpose||''}</td><td>${t.responsible||''}</td>
<td><span class="status-badge ${t.status==='已到位'?'done':t.status==='已确认'?'progress':'pending'}">${t.status||'待确认'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="openInstallToolModal(${t._i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteInstallTool(${t._i})">删除</button></td>
</tr>`;
}).join('');
sections += `<div class="color-category-section">
<div class="color-cat-header"><span class="color-cat-icon">${catIcons[cat]||'📌'}</span><span class="color-cat-name">${cat}</span><span class="color-cat-count">${items.length}项 · ¥${catTotal.toLocaleString()}</span></div>
<table class="data-table"><thead><tr><th>#</th><th>工具名称</th><th>规格型号</th><th>数量</th><th>单位</th><th>单价</th><th>总价</th><th>重要</th><th>用途</th><th>责任人</th><th>状态</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>
</div>`;
});
const totalAll = tools.reduce((s,t)=>s+(parseFloat(t.totalPrice)||0),0);
return `<div class="module-panel"><div class="module-header"><span>🔧 安装工具准备清单</span><button class="btn btn-primary btn-sm" onclick="openInstallToolModal()">+ 添加工具</button></div>
<div class="module-body">
<div class="dashboard-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px;">
<div class="stat-card" style="text-align:center;"><div class="label">工具总项数</div><div class="value">${tools.length}</div><div class="sub">${Object.keys(grouped).length} 个分类</div></div>
<div class="stat-card green" style="text-align:center;"><div class="label">工具总金额</div><div class="value">¥${totalAll.toLocaleString()}</div><div class="sub">预估采购总价</div></div>
<div class="stat-card orange" style="text-align:center;"><div class="label">关键工具(★★)</div><div class="value">${tools.filter(t=>t.priority==='★★').length}</div><div class="sub">必须到位</div></div>
<div class="stat-card" style="text-align:center;"><div class="label">已到位</div><div class="value">${tools.filter(t=>t.status==='已到位').length}</div><div class="sub">/ ${tools.length} 项</div></div>
</div>
${sections||'<div style="text-align:center;color:#aaa;padding:40px;">暂无工具数据,点击上方按钮添加</div>'}
</div></div>`;
}
function openInstallToolModal(idx) {
let t = { name:'', nameEn:'', spec:'', qty:'', unit:'台', unitPrice:'', totalPrice:'', priority:'', arrivalDate:'', purpose:'', responsible:'', status:'待确认', category:'起重吊装', remark:'' };
let isEdit = false;
if (idx !== undefined) { t = projectData.installPrep.tools[idx]; isEdit = true; }
const cats = ['起重吊装','测量仪器','机械安装','液压专用','电气安装','其他'];
document.getElementById('modal-title').textContent = isEdit ? '编辑工具' : '添加工具';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group" style="flex:1;"><label>中文名称</label><input id="ipt-name" value="${t.name||''}" placeholder="如:桥式起重机"></div>
<div class="form-group" style="flex:0 0 160px;"><label>分类</label><select id="ipt-category">${cats.map(c=>`<option value="${c}" ${t.category===c?'selected':''}>${c}</option>`).join('')}</select></div>
</div>
<div class="form-group"><label>英文名称</label><input id="ipt-nameEn" value="${t.nameEn||''}" placeholder="English Name"></div>
<div class="form-row">
<div class="form-group" style="flex:1;"><label>规格型号</label><input id="ipt-spec" value="${t.spec||''}" placeholder="如≥50t/20t双梁"></div>
<div class="form-group" style="flex:0 0 80px;"><label>数量</label><input id="ipt-qty" value="${t.qty||''}" placeholder="数量"></div>
<div class="form-group" style="flex:0 0 80px;"><label>单位</label><input id="ipt-unit" value="${t.unit||'台'}" placeholder="台/套/根"></div>
</div>
<div class="form-row">
<div class="form-group"><label>单价(元)</label><input id="ipt-unitPrice" type="number" value="${t.unitPrice||''}" placeholder="单价" oninput="autoCalcToolTotal()"></div>
<div class="form-group"><label>总价(元)</label><input id="ipt-totalPrice" type="number" value="${t.totalPrice||''}" placeholder="总价"></div>
<div class="form-group"><label>重要程度</label><select id="ipt-priority"><option value="" ${!t.priority?'selected':''}>普通</option><option value="★" ${t.priority==='★'?'selected':''}>★ 重要</option><option value="★★" ${t.priority==='★★'?'selected':''}>★★ 关键</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>到位日期</label><input id="ipt-arrivalDate" type="date" value="${t.arrivalDate||''}"></div>
<div class="form-group"><label>责任人</label><input id="ipt-responsible" value="${t.responsible||''}" placeholder="责任人"></div>
</div>
<div class="form-group"><label>主要用途</label><input id="ipt-purpose" value="${t.purpose||''}" placeholder="如:牌坊吊装"></div>
<div class="form-row">
<div class="form-group"><label>状态</label><select id="ipt-status"><option value="待确认" ${t.status==='待确认'?'selected':''}>待确认</option><option value="已确认" ${t.status==='已确认'?'selected':''}>已确认</option><option value="已到位" ${t.status==='已到位'?'selected':''}>已到位</option><option value="已取消" ${t.status==='已取消'?'selected':''}>已取消</option></select></div>
<div class="form-group"><label>备注</label><input id="ipt-remark" value="${t.remark||''}"></div>
</div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="saveInstallTool(${isEdit?idx:-1})">保存</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function autoCalcToolTotal() {
const qty = parseFloat(document.getElementById('ipt-qty')?.value)||0;
const price = parseFloat(document.getElementById('ipt-unitPrice')?.value)||0;
const totalEl = document.getElementById('ipt-totalPrice');
if(totalEl && qty>0 && price>0) totalEl.value = qty*price;
}
function saveInstallTool(idx) {
const t = {
name:document.getElementById('ipt-name').value,
nameEn:document.getElementById('ipt-nameEn').value,
spec:document.getElementById('ipt-spec').value,
qty:document.getElementById('ipt-qty').value,
unit:document.getElementById('ipt-unit').value,
unitPrice:document.getElementById('ipt-unitPrice').value,
totalPrice:document.getElementById('ipt-totalPrice').value,
priority:document.getElementById('ipt-priority').value,
arrivalDate:document.getElementById('ipt-arrivalDate').value,
purpose:document.getElementById('ipt-purpose').value,
responsible:document.getElementById('ipt-responsible').value,
status:document.getElementById('ipt-status').value,
category:document.getElementById('ipt-category').value,
remark:document.getElementById('ipt-remark').value
};
if (!t.name) { showToast('请填写工具名称'); return; }
if (!projectData.installPrep.tools) projectData.installPrep.tools = [];
if (idx >= 0) projectData.installPrep.tools[idx] = t; else projectData.installPrep.tools.push(t);
saveData(); closeModal(); renderModule('install_prep'); showToast('工具已保存');
}
function deleteInstallTool(i) { if (!confirm('确认删除?')) return; projectData.installPrep.tools.splice(i,1); saveData(); renderModule('install_prep'); }
function renderInstallPrepPersonnel() {
const personnel = projectData.installPrep.personnel || [];
const totalWages = personnel.reduce((s,p)=>s+(parseFloat(p.totalWages)||0),0);
const totalDays = personnel.reduce((s,p)=>s+(parseInt(p.days)||0),0);
const rows = personnel.map((p,i) => {
const wagesNum = parseFloat(p.totalWages)||0;
return `<tr>
<td>${i+1}</td><td><b>${p.name}</b></td><td>${p.position||''}</td><td style="font-size:10px;">${p.positionEn||''}</td>
<td>${p.planIn||''}</td><td>${p.planOut||''}</td><td>${p.days||''}</td>
<td style="text-align:right;">${p.dailyRate?Number(p.dailyRate).toLocaleString():''}</td>
<td style="text-align:right;font-weight:600;">${wagesNum?wagesNum.toLocaleString():''}</td>
<td style="font-size:10px;max-width:200px;">${p.duty||''}</td>
<td style="font-size:10px;">${p.qualification||''}</td>
<td>${p.phone||''}</td>
<td><button class="btn btn-sm btn-outline" onclick="openInstallPersonModal(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteInstallPerson(${i})">删除</button></td>
</tr>`;
}).join('');
return `<div class="module-panel"><div class="module-header"><span>👷 安装人员计划</span><button class="btn btn-primary btn-sm" onclick="openInstallPersonModal()">+ 添加人员</button></div>
<div class="module-body">
<div class="dashboard-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px;">
<div class="stat-card" style="text-align:center;"><div class="label">人员总数</div><div class="value">${personnel.length}</div><div class="sub">人</div></div>
<div class="stat-card green" style="text-align:center;"><div class="label">人工费总预算</div><div class="value">¥${totalWages.toLocaleString()}</div><div class="sub">含全部人员</div></div>
<div class="stat-card orange" style="text-align:center;"><div class="label">总出勤天数</div><div class="value">${totalDays}</div><div class="sub">人·天</div></div>
<div class="stat-card" style="text-align:center;"><div class="label">日均工资</div><div class="value">¥${personnel.length>0?Math.round(totalWages/personnel.length).toLocaleString():0}</div><div class="sub">人均</div></div>
</div>
<table class="data-table"><thead><tr><th>#</th><th>姓名</th><th>岗位</th><th>Position</th><th>计划入场</th><th>计划退场</th><th>在岗天数</th><th>日工资</th><th>总工资</th><th>主要职责</th><th>资质要求</th><th>电话</th><th>操作</th></tr></thead>
<tbody>${rows||'<tr><td colspan="13" style="text-align:center;color:#aaa;">暂无人员数据</td></tr>'}</tbody></table>
</div></div>`;
}
function openInstallPersonModal(idx) {
let p = { name:'', position:'', positionEn:'', planIn:'', planOut:'', days:'', dailyRate:'', totalWages:'', duty:'', qualification:'', phone:'', remark:'' };
let isEdit = false;
if (idx !== undefined) { p = projectData.installPrep.personnel[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑人员' : '添加人员';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group" style="flex:1;"><label>姓名</label><input id="ipp-name" value="${p.name||''}" placeholder="如:陈志强"></div>
<div class="form-group" style="flex:1;"><label>岗位(中文)</label><input id="ipp-position" value="${p.position||''}" placeholder="如:项目经理"></div>
<div class="form-group" style="flex:1;"><label>Position (EN)</label><input id="ipp-positionEn" value="${p.positionEn||''}" placeholder="如Project Manager"></div>
</div>
<div class="form-row">
<div class="form-group"><label>计划入场</label><input id="ipp-planIn" value="${p.planIn||''}" placeholder="如D+0(开工)"></div>
<div class="form-group"><label>计划退场</label><input id="ipp-planOut" value="${p.planOut||''}" placeholder="如D+82"></div>
<div class="form-group"><label>在岗天数</label><input id="ipp-days" value="${p.days||''}" placeholder="如82"></div>
</div>
<div class="form-row">
<div class="form-group"><label>日工资(元)</label><input id="ipp-dailyRate" type="number" value="${p.dailyRate||''}" placeholder="如1500" oninput="autoCalcPersonWages()"></div>
<div class="form-group"><label>总工资(元)</label><input id="ipp-totalWages" type="number" value="${p.totalWages||''}" placeholder="自动计算"></div>
</div>
<div class="form-group"><label>主要职责</label><input id="ipp-duty" value="${p.duty||''}" placeholder="如:全局协调/进度管控"></div>
<div class="form-row">
<div class="form-group"><label>资质要求</label><input id="ipp-qualification" value="${p.qualification||''}" placeholder="如:一级建造师"></div>
<div class="form-group"><label>联系电话</label><input id="ipp-phone" value="${p.phone||''}" placeholder="电话"></div>
</div>
<div class="form-group"><label>备注</label><input id="ipp-remark" value="${p.remark||''}"></div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="saveInstallPerson(${isEdit?idx:-1})">保存</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function autoCalcPersonWages() {
const days = parseInt(document.getElementById('ipp-days')?.value)||0;
const rate = parseFloat(document.getElementById('ipp-dailyRate')?.value)||0;
const totalEl = document.getElementById('ipp-totalWages');
if(totalEl && days>0 && rate>0) totalEl.value = days*rate;
}
function saveInstallPerson(idx) {
const p = {
name:document.getElementById('ipp-name').value,
position:document.getElementById('ipp-position').value,
positionEn:document.getElementById('ipp-positionEn').value,
planIn:document.getElementById('ipp-planIn').value,
planOut:document.getElementById('ipp-planOut').value,
days:document.getElementById('ipp-days').value,
dailyRate:document.getElementById('ipp-dailyRate').value,
totalWages:document.getElementById('ipp-totalWages').value,
duty:document.getElementById('ipp-duty').value,
qualification:document.getElementById('ipp-qualification').value,
phone:document.getElementById('ipp-phone').value,
remark:document.getElementById('ipp-remark').value
};
if (!p.name) { showToast('请填写姓名'); return; }
if (!projectData.installPrep.personnel) projectData.installPrep.personnel = [];
if (idx >= 0) projectData.installPrep.personnel[idx] = p; else projectData.installPrep.personnel.push(p);
saveData(); closeModal(); renderModule('install_prep'); showToast('人员已保存');
}
function deleteInstallPerson(i) { if (!confirm('确认删除?')) return; projectData.installPrep.personnel.splice(i,1); saveData(); renderModule('install_prep'); }
function renderInstallPrepPrecision() {
const prec = projectData.installPrep.precision || [];
const sysOrder = ['轧辊系统','AGC系统','主机框架','液压系统','电气系统','辅助设备','冷却润滑','安全装置'];
const sysIcons = {'轧辊系统':'🔄','AGC系统':'📐','主机框架':'🏛️','液压系统':'💧','电气系统':'⚡','辅助设备':'🔩','冷却润滑':'❄️','安全装置':'🛡️'};
const grouped = {};
prec.forEach((p,i)=>{ const s = p.system||'其他'; if(!grouped[s])grouped[s]=[]; grouped[s].push({...p,_i:i}); });
let sections = '';
sysOrder.forEach(sys=>{
const items = grouped[sys]||[];
if(items.length===0) return;
const rows = items.map(p=>{
const impTag = p.importance==='★★★'?'<span class="tag tag-danger">关键</span>':p.importance==='★★'?'<span class="tag tag-warning">重要</span>':'';
const photoCount = (p.photos||[]).length;
return `<tr>
<td>${p._i+1}</td><td><b>${p.item}</b></td><td style="font-size:10px;">${p.nameEn||''}</td><td>${p.target||p.requirement||''}</td><td>${p.unit||''}</td>
<td>${impTag}</td><td style="font-size:10px;">${p.tool||''}</td><td style="font-size:10px;">${p.method||''}</td><td style="font-size:10px;">${p.standard||''}</td>
<td>${p.actual||''}</td>
<td><span class="status-badge ${p.ok?'done':'pending'}">${p.ok?'合格':'待检'}</span></td>
<td>${photoCount>0?`<span class="tag tag-success" style="cursor:pointer;" onclick="viewPrecisionPhotos(${p._i})" title="${(p.photos||[]).join(', ')}">📷 ${photoCount}张</span>`:'<span class="tag" style="color:#bbb;">📷 0</span>'}</td>
<td><button class="btn btn-sm btn-outline" onclick="openPrecisionModal(${p._i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deletePrecision(${p._i})">删除</button></td>
</tr>`;
}).join('');
sections += `<div class="color-category-section">
<div class="color-cat-header"><span class="color-cat-icon">${sysIcons[sys]||'📌'}</span><span class="color-cat-name">${sys}</span><span class="color-cat-count">${items.length}项 · 合格 ${items.filter(p=>p.ok).length}/${items.length}</span></div>
<table class="data-table"><thead><tr><th>#</th><th>精度项目</th><th>English</th><th>目标值</th><th>单位</th><th>重要性</th><th>检测工具</th><th>检测方法</th><th>依据标准</th><th>实测值</th><th>状态</th><th>📷检测照片</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>
</div>`;
});
const totalItems = prec.length;
const okItems = prec.filter(p=>p.ok).length;
return `<div class="module-panel"><div class="module-header"><span>📏 安装精度标准(带检测照片)</span><button class="btn btn-primary btn-sm" onclick="openPrecisionModal()">+ 添加精度项</button></div>
<div class="module-body">
<div class="dashboard-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px;">
<div class="stat-card" style="text-align:center;"><div class="label">精度项总数</div><div class="value">${totalItems}</div><div class="sub">${Object.keys(grouped).length} 个子系统</div></div>
<div class="stat-card green" style="text-align:center;"><div class="label">合格率</div><div class="value">${totalItems>0?Math.round(okItems/totalItems*100):0}%</div><div class="sub">${okItems}/${totalItems} 项合格</div></div>
<div class="stat-card orange" style="text-align:center;"><div class="label">关键项(★★★)</div><div class="value">${prec.filter(p=>p.importance==='★★★').length}</div><div class="sub">必须全部合格</div></div>
<div class="stat-card" style="text-align:center;"><div class="label">待检项</div><div class="value">${totalItems-okItems}</div><div class="sub">/ ${totalItems} 项</div></div>
</div>
${sections||'<div style="text-align:center;color:#aaa;padding:40px;">暂无精度数据,点击上方按钮添加</div>'}
</div></div>`;
}
function openPrecisionModal(idx) {
let p = { system:'轧辊系统', item:'', nameEn:'', target:'', unit:'mm', importance:'★★★', tool:'', method:'', standard:'SMS规范', requirement:'', actual:'', ok:false, photos:[] };
let isEdit = false;
if (idx !== undefined) { p = projectData.installPrep.precision[idx]; isEdit = true; }
const systems = ['轧辊系统','AGC系统','主机框架','液压系统','电气系统','辅助设备','冷却润滑','安全装置'];
const photosStr = (p.photos||[]).join(';');
document.getElementById('modal-title').textContent = isEdit ? '编辑精度项' : '添加精度项';
document.getElementById('modal-body').innerHTML = `
<div class="form-row">
<div class="form-group" style="flex:0 0 140px;"><label>子系统</label><select id="ipr-system">${systems.map(s=>`<option value="${s}" ${p.system===s?'selected':''}>${s}</option>`).join('')}</select></div>
<div class="form-group" style="flex:1;"><label>精度项目(中文)</label><input id="ipr-item" value="${p.item||''}" placeholder="如:工作辊辊径偏差"></div>
</div>
<div class="form-group"><label>English Name</label><input id="ipr-nameEn" value="${p.nameEn||''}" placeholder="如WR diameter deviation"></div>
<div class="form-row">
<div class="form-group"><label>目标值</label><input id="ipr-target" value="${p.target||p.requirement||''}" placeholder="如±0.005"></div>
<div class="form-group" style="flex:0 0 80px;"><label>单位</label><input id="ipr-unit" value="${p.unit||'mm'}" placeholder="mm"></div>
<div class="form-group" style="flex:0 0 120px;"><label>重要性</label><select id="ipr-importance"><option value="★★★" ${p.importance==='★★★'?'selected':''}>★★★ 关键</option><option value="★★" ${p.importance==='★★'?'selected':''}>★★ 重要</option><option value="★" ${p.importance==='★'?'selected':''}>★ 一般</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>检测工具</label><input id="ipr-tool" value="${p.tool||''}" placeholder="如:外径千分尺"></div>
<div class="form-group"><label>检测方法</label><input id="ipr-method" value="${p.method||''}" placeholder="如:多点等距测量"></div>
</div>
<div class="form-row">
<div class="form-group"><label>依据标准</label><input id="ipr-standard" value="${p.standard||''}" placeholder="如SMS规范、GB 50231"></div>
<div class="form-group"><label>实测值</label><input id="ipr-actual" value="${p.actual||''}" placeholder="如:+0.003mm"></div>
</div>
<div style="margin-top:8px;"><label style="display:flex;align-items:center;gap:6px;font-size:13px;"><input type="checkbox" id="ipr-ok" ${p.ok?'checked':''}> 合格</label></div>
<div class="form-group" style="margin-top:10px;">
<label>📷 检测照片(文件名,用 ; 分隔多个)</label>
<input id="ipr-photos" value="${photosStr}" placeholder="牌坊水平检测_01.jpg;窗口距测量_02.jpg">
${photosStr ? `<div style="font-size:10px;color:var(--text2);margin-top:3px;">已上传 ${(p.photos||[]).length} 张检测照片</div>` : ''}
</div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="savePrecision(${isEdit?idx:-1})">保存</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function savePrecision(idx) {
const photosStr = (document.getElementById('ipr-photos')?.value || '').trim();
const photos = photosStr ? photosStr.split(';').map(f=>f.trim()).filter(Boolean) : [];
const p = {
system: document.getElementById('ipr-system').value,
item: document.getElementById('ipr-item').value,
nameEn: document.getElementById('ipr-nameEn').value,
target: document.getElementById('ipr-target').value,
unit: document.getElementById('ipr-unit').value,
importance: document.getElementById('ipr-importance').value,
tool: document.getElementById('ipr-tool').value,
method: document.getElementById('ipr-method').value,
standard: document.getElementById('ipr-standard').value,
requirement: document.getElementById('ipr-target').value,
actual: document.getElementById('ipr-actual').value,
ok: document.getElementById('ipr-ok')?.checked || false,
photos
};
if (!p.item) { showToast('请填写精度项目'); return; }
if (!projectData.installPrep.precision) projectData.installPrep.precision = [];
if (idx >= 0) projectData.installPrep.precision[idx] = p; else projectData.installPrep.precision.push(p);
saveData(); closeModal(); renderModule('install_prep'); showToast('精度项已保存');
}
function viewPrecisionPhotos(idx) {
const p = projectData.installPrep.precision[idx];
if (!p || !p.photos || p.photos.length===0) { showToast('暂无检测照片'); return; }
const list = p.photos.map(f=>`<div class="evidence-item ok" style="margin-bottom:4px;"><span class="ev-icon">📷</span>${f}</div>`).join('');
document.getElementById('modal-title').textContent = `📷 ${p.item} - 检测照片`;
document.getElementById('modal-body').innerHTML = `
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">共 ${p.photos.length} 张检测照片 · ${p.ok?'状态:合格':'状态:待检'}</div>
<div style="display:flex;flex-direction:column;gap:4px;">${list}</div>
<div style="text-align:right;margin-top:16px;"><button class="btn btn-outline" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function deletePrecision(i) { if (!confirm('确认删除?')) return; projectData.installPrep.precision.splice(i,1); saveData(); renderModule('install_prep'); }
function renderInstallPrepProgress() {
const prog = projectData.installPrep.progress || [];
const totalItems = prog.length;
const doneItems = prog.filter(p=>p.status==='done').length;
const progItems = prog.filter(p=>p.status==='progress').length;
const overallPct = totalItems>0 ? Math.round(doneItems/totalItems*100) : 0;
// Bar chart
const now = new Date();
const barRows = prog.map((p,i) => {
let barPct = 0;
let barColor = '#ddd';
let barLabel = '未开始';
if (p.status === 'done') { barPct = 100; barColor = 'var(--success)'; barLabel = '已完成'; }
else if (p.status === 'progress') {
barPct = p.planStart&&p.planEnd ? calcDateProgress(p.planStart, p.planEnd) : 30;
barColor = 'var(--accent)'; barLabel = '进行中';
}
const isOverdue = p.planEnd && p.status!=='done' && now > new Date(p.planEnd+'T00:00:00');
const barBg = isOverdue ? 'var(--danger)' : barColor;
const barStyle = barPct===100 ? 'var(--success)' : barBg;
const delayHtml = isOverdue && p.delayReason
? `<div style="font-size:10px;color:var(--danger);margin-top:1px;padding-left:2px;">⚠️ 延误原因:${p.delayReason}</div>`
: (isOverdue ? `<div style="font-size:10px;color:var(--danger);margin-top:1px;padding-left:2px;">⚠️ 已逾期,请填写延误原因</div>` : '');
return `
<div style="margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
<span style="font-weight:600;">${p.name||'未命名'} ${isOverdue?'<span class="tag tag-danger">逾期</span>':''}</span>
<span style="color:var(--text2);">${p.planStart||'?'}~${p.planEnd||'?'} · ${barLabel} · ${barPct}%</span>
</div>
<div style="height:16px;background:#eee;border-radius:4px;overflow:hidden;position:relative;">
<div style="height:100%;background:${barStyle};width:${barPct}%;border-radius:4px;transition:width 0.5s;${isOverdue?'background:linear-gradient(90deg,#e74c3c,#c0392b);':''}"></div>
</div>
${p.actualStart||p.actualEnd?`<div style="font-size:9px;color:var(--text2);margin-top:1px;">实际: ${p.actualStart||'-'}~${p.actualEnd||'-'}</div>`:''}
${delayHtml}
</div>`;
}).join('');
// Timeline table
const rows = prog.map((p,i) => {
const isOverdue = p.planEnd && p.status!=='done' && now > new Date(p.planEnd+'T00:00:00');
const imgCount = (p.images||[]).length;
const vidCount = (p.videos||[]).length;
const imgTag = imgCount > 0
? `<span class="tag-media tag-media-img" onclick="viewProgressMedia(${i},'images')" title="查看图片">📷 ${imgCount}</span>`
: `<span class="tag-media tag-media-none">📷 0</span>`;
const vidTag = vidCount > 0
? `<span class="tag-media tag-media-vid" onclick="viewProgressMedia(${i},'videos')" title="查看视频">🎬 ${vidCount}</span>`
: `<span class="tag-media tag-media-none">🎬 0</span>`;
return `<tr${isOverdue?' style="background:#fff5f5;"':''}>
<td>${i+1}</td><td><b>${p.name}</b></td><td>${p.planStart||''}</td><td>${p.planEnd||''}</td><td>${p.actualStart||''}</td><td>${p.actualEnd||''}</td>
<td><span class="status-badge ${p.status}">${p.status==='done'?'已完成':p.status==='progress'?'进行中':'未开始'}</span>${isOverdue?' <span class="tag tag-danger">逾期</span>':''}</td>
<td style="max-width:120px;font-size:10px;">${p.delayReason||''}</td>
<td style="text-align:center;">${imgTag}</td>
<td style="text-align:center;">${vidTag}</td>
<td><button class="btn btn-sm btn-outline" onclick="editProgress(${i})">编辑</button> <button class="btn btn-sm btn-danger" onclick="deleteProgress(${i})">删除</button></td>
</tr>`;
}).join('');
return `<div class="module-panel"><div class="module-header"><span>📊 安装进度计划</span><button class="btn btn-primary btn-sm" onclick="openProgressModal()">+ 添加进度项</button></div><div class="module-body">
<div class="dashboard-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px;">
<div class="stat-card" style="text-align:center;"><div class="label">安装总进度</div><div class="value" style="color:var(--accent);">${overallPct}%</div><div class="sub">${doneItems}/${totalItems} 项完成</div></div>
<div class="stat-card green" style="text-align:center;"><div class="label">已完成</div><div class="value">${doneItems}</div><div class="sub">项</div></div>
<div class="stat-card orange" style="text-align:center;"><div class="label">进行中</div><div class="value">${progItems}</div><div class="sub">项</div></div>
<div class="stat-card" style="text-align:center;"><div class="label">未开始</div><div class="value">${totalItems-doneItems-progItems}</div><div class="sub">项</div></div>
</div>
<div style="font-weight:600;font-size:12px;margin-bottom:8px;">📊 安装进度条形图</div>
${barRows || '<div style="text-align:center;color:#aaa;padding:20px;">暂无安装进度数据</div>'}
<hr style="margin:16px 0;border:none;border-top:1px solid var(--border);">
<div style="font-weight:600;font-size:12px;margin-bottom:8px;">📋 安装进度明细</div>
<table class="data-table"><thead><tr><th>#</th><th>安装项目</th><th>计划开始</th><th>计划结束</th><th>实际开始</th><th>实际结束</th><th>状态</th><th>延误原因</th><th>📷图片</th><th>🎬视频</th><th>操作</th></tr></thead><tbody>${rows||'<tr><td colspan="11" style="text-align:center;color:#aaa;">暂无进度数据</td></tr>'}</tbody></table>
</div></div>`;
}
function calcDateProgress(start, end) {
if (!start || !end) return 0;
const s = new Date(start+'T00:00:00');
const e = new Date(end+'T00:00:00');
const now = new Date();
if (now < s) return 0;
const total = e - s;
const elapsed = now - s;
return Math.min(100, Math.round(elapsed/total*100));
}
function openProgressModal(idx) {
let p = { name:'', planStart:'', planEnd:'', actualStart:'', actualEnd:'', status:'pending', delayReason:'', images:[], videos:[] };
let isEdit = false;
if (idx !== undefined) { p = { ...p, ...projectData.installPrep.progress[idx] }; isEdit = true; }
window._pendingProgMedia = { images: [...(p.images||[])], videos: [...(p.videos||[])] };
const existingImages = window._pendingProgMedia.images;
const existingVideos = window._pendingProgMedia.videos;
const imgGridHtml = existingImages.length > 0
? `<div class="sm-media-grid" id="prog-image-grid">${existingImages.map((img,imgIdx) => `
<div class="sm-media-thumb">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.parentElement.style.display='none'">
<div class="sm-thumb-name" title="${img.name||''}">${img.name||''}</div>
<button class="sm-thumb-remove" onclick="event.stopPropagation();removeProgImage(${imgIdx})" title="移除">×</button>
</div>`).join('')}</div>`
: '';
const vidListHtml = existingVideos.length > 0
? `<div id="prog-video-list">${existingVideos.map((v,vidIdx) => `
<div class="sm-video-item">
<span class="vi-icon">🎬</span>
<div class="vi-info">
<div class="vi-name">${v.name||''}</div>
<div class="vi-size">${formatFileSize(v.size||0)}</div>
</div>
<span class="vi-remove" onclick="removeProgVideo(${vidIdx})" title="移除">×</span>
</div>`).join('')}</div>`
: '';
document.getElementById('modal-title').textContent = isEdit ? '编辑进度项' : '添加进度项';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>安装项目名称</label><input id="prg-name" value="${p.name||''}"></div>
<div class="form-row"><div class="form-group"><label>计划开始</label><input id="prg-ps" type="date" value="${p.planStart||''}"></div><div class="form-group"><label>计划结束</label><input id="prg-pe" type="date" value="${p.planEnd||''}"></div></div>
<div class="form-row"><div class="form-group"><label>实际开始</label><input id="prg-as" type="date" value="${p.actualStart||''}"></div><div class="form-group"><label>实际结束</label><input id="prg-ae" type="date" value="${p.actualEnd||''}"></div></div>
<div class="form-group"><label>状态</label><select id="prg-status"><option value="pending" ${p.status==='pending'?'selected':''}>未开始</option><option value="progress" ${p.status==='progress'?'selected':''}>进行中</option><option value="done" ${p.status==='done'?'selected':''}>已完成</option></select></div>
<div class="form-group"><label>⚠️ 延误原因 <span style="font-weight:400;color:var(--text2);">(逾期时必填)</span></label><textarea id="prg-delay" placeholder="如:土建交接延迟、设备到货滞后、天气原因停工…">${p.delayReason||''}</textarea></div>
<div class="form-row">
<div class="form-group sm-media-section">
<label>📷 现场照片 <span style="font-weight:400;color:var(--text2);">(可多选)</span></label>
<div class="sm-upload-zone" onclick="document.getElementById('prog-image-input').click()">
<div class="uz-icon">📤</div>
<div class="uz-text">点击选择安装现场照片</div>
<div class="uz-hint">支持 JPG/PNG/WebP单张 ≤ 5MB</div>
</div>
<input type="file" id="prog-image-input" accept="image/*" multiple style="display:none;" onchange="handleProgImageSelect(this)">
${imgGridHtml}
</div>
<div class="form-group sm-media-section">
<label>🎬 现场视频 <span style="font-weight:400;color:var(--text2);">(可选)</span></label>
<div class="sm-upload-zone" onclick="document.getElementById('prog-video-input').click()">
<div class="uz-icon">🎥</div>
<div class="uz-text">点击选择安装现场视频</div>
<div class="uz-hint">支持 MP4/WebM单段 ≤ 100MB</div>
</div>
<input type="file" id="prog-video-input" accept="video/*" style="display:none;" onchange="handleProgVideoSelect(this)">
${vidListHtml}
</div>
</div>
<div style="text-align:right;margin-top:12px;"><button class="btn btn-outline" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="saveProgress(${isEdit?idx:-1})">保存</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveProgress(idx) {
const pending = window._pendingProgMedia || { images: [], videos: [] };
const p = {
name:document.getElementById('prg-name').value,
planStart:document.getElementById('prg-ps').value,
planEnd:document.getElementById('prg-pe').value,
actualStart:document.getElementById('prg-as').value,
actualEnd:document.getElementById('prg-ae').value,
status:document.getElementById('prg-status').value,
delayReason:document.getElementById('prg-delay').value,
images: pending.images,
videos: pending.videos
};
if (!p.name) { showToast('请填写项目名称'); return; }
if (!projectData.installPrep.progress) projectData.installPrep.progress = [];
if (idx >= 0) projectData.installPrep.progress[idx] = p; else projectData.installPrep.progress.push(p);
window._pendingProgMedia = null;
saveData(); closeModal(); renderModule('install_prep');
const imgN = p.images.length; const vidN = p.videos.length;
showToast(`进度项已保存${imgN>0||vidN>0?'(📷'+imgN+'张照片 🎬'+vidN+'段视频)':''}`);
}
function editProgress(i) { openProgressModal(i); }
function deleteProgress(i) { if (!confirm('确认删除?')) return; projectData.installPrep.progress.splice(i,1); saveData(); renderModule('install_prep'); }
// ========== Progress Media Handlers ==========
function handleProgImageSelect(input) {
const files = Array.from(input.files);
if (files.length === 0) return;
if (!window._pendingProgMedia) window._pendingProgMedia = { images: [], videos: [] };
let loaded = 0;
const validFiles = files.filter(f => { if (f.size > 5*1024*1024) { showToast(`图片 "${f.name}" 超过5MB限制`); return false; } return true; });
if (validFiles.length === 0) { input.value = ''; return; }
const total = validFiles.length;
validFiles.forEach(file => {
const reader = new FileReader();
reader.onload = function(e) {
window._pendingProgMedia.images.push({ name: file.name, size: file.size, type: file.type, data: e.target.result });
loaded++;
if (loaded === total) { refreshProgImageGrid(); showToast(`已添加 ${loaded} 张照片`); }
};
reader.readAsDataURL(file);
});
input.value = '';
}
function removeProgImage(imgIdx) {
if (!window._pendingProgMedia) return;
window._pendingProgMedia.images.splice(imgIdx, 1);
refreshProgImageGrid();
}
function refreshProgImageGrid() {
const grid = document.getElementById('prog-image-grid');
const images = (window._pendingProgMedia && window._pendingProgMedia.images) || [];
if (images.length === 0) { if (grid) grid.remove(); return; }
const html = images.map((img, imgIdx) => `
<div class="sm-media-thumb">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.parentElement.style.display='none'">
<div class="sm-thumb-name" title="${img.name||''}">${img.name||''}</div>
<button class="sm-thumb-remove" onclick="event.stopPropagation();removeProgImage(${imgIdx})" title="移除">×</button>
</div>`).join('');
if (grid) { grid.innerHTML = html; }
else {
const uploadZone = document.querySelector('#prog-image-input')?.closest('.sm-media-section')?.querySelector('.sm-upload-zone');
if (uploadZone) { const newGrid = document.createElement('div'); newGrid.className = 'sm-media-grid'; newGrid.id = 'prog-image-grid'; newGrid.innerHTML = html; uploadZone.after(newGrid); }
}
}
function handleProgVideoSelect(input) {
const files = Array.from(input.files);
if (files.length === 0) return;
if (!window._pendingProgMedia) window._pendingProgMedia = { images: [], videos: [] };
let added = 0;
files.forEach(file => {
if (file.size > 100*1024*1024) { showToast(`视频 "${file.name}" 超过100MB限制`); return; }
window._pendingProgMedia.videos.push({ name: file.name, size: file.size, type: file.type });
added++;
});
if (added > 0) { refreshProgVideoList(); showToast(`已添加 ${added} 段视频`); }
input.value = '';
}
function removeProgVideo(vidIdx) {
if (!window._pendingProgMedia) return;
window._pendingProgMedia.videos.splice(vidIdx, 1);
refreshProgVideoList();
}
function refreshProgVideoList() {
const list = document.getElementById('prog-video-list');
const videos = (window._pendingProgMedia && window._pendingProgMedia.videos) || [];
if (videos.length === 0) { if (list) list.remove(); return; }
const html = videos.map((v, vidIdx) => `
<div class="sm-video-item">
<span class="vi-icon">🎬</span>
<div class="vi-info">
<div class="vi-name">${v.name||''}</div>
<div class="vi-size">${formatFileSize(v.size||0)}</div>
</div>
<span class="vi-remove" onclick="removeProgVideo(${vidIdx})" title="移除">×</span>
</div>`).join('');
if (list) { list.innerHTML = html; }
else {
const uploadZone = document.querySelector('#prog-video-input')?.closest('.sm-media-section')?.querySelector('.sm-upload-zone');
if (uploadZone) { const newList = document.createElement('div'); newList.id = 'prog-video-list'; newList.innerHTML = html; uploadZone.after(newList); }
}
}
function viewProgressMedia(idx, type) {
const item = projectData.installPrep.progress[idx];
if (!item) return;
const images = item.images || [];
const videos = item.videos || [];
if (type === 'images' && images.length === 0) { showToast('该进度项暂无照片'); return; }
if (type === 'videos' && videos.length === 0) { showToast('该进度项暂无视频'); return; }
let contentHtml = '';
if (type === 'images') {
contentHtml = `<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">📷 共 ${images.length} 张安装现场照片</div>
<div class="sm-media-viewer">${images.map(img => `
<div class="mv-image">
<img src="${img.data||''}" alt="${img.name||''}" onerror="this.style.display='none';this.parentElement.querySelector('.mv-caption').textContent='⚠️ 图片加载失败';">
<div class="mv-caption">${img.name||''}${img.size?' <span style="color:#aaa;">('+formatFileSize(img.size)+')</span>':''}</div>
</div>`).join('')}</div>`;
} else {
contentHtml = `<div style="font-size:12px;color:var(--text2);margin-bottom:10px;">🎬 共 ${videos.length} 段安装现场视频</div>
<div class="sm-media-viewer">${videos.map(v => `
<div class="mv-video">
<div class="mv-icon">🎬</div>
<div class="mv-name">${v.name||'未命名视频'}</div>
<div class="mv-size">${formatFileSize(v.size||0)}</div>
</div>`).join('')}</div>
<div style="font-size:10px;color:var(--text2);margin-top:8px;">💡 视频存储为文件引用,实际文件请从公司服务器获取。</div>`;
}
document.getElementById('modal-title').textContent = `${type==='images'?'📷':'🎬'} ${item.name||'进度项'} - ${type==='images'?'安装照片':'安装视频'}`;
document.getElementById('modal-body').innerHTML = contentHtml + `<div style="text-align:right;margin-top:16px;"><button class="btn btn-outline" onclick="closeModal()">关闭</button></div>`;
document.getElementById('modal-overlay').classList.add('show');
}
// ========== Install Feedback Module (安装问题反馈) ==========
function renderInstallFeedback() {
const items = projectData.installFeedback || [];
const rows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.title||''}</td><td>${it.location||''}</td><td>${it.reporter||''}</td>
<td>${it.date||''}</td><td><span class="status-badge ${it.status}">${it.status==='resolved'?'已解决':it.status==='processing'?'处理中':'待处理'}</span></td>
<td><button class="btn btn-sm btn-outline" onclick="editFeedback(${i})">处理</button> <button class="btn btn-sm btn-danger" onclick="deleteFeedback(${i})">删除</button></td>
</tr>
`).join('');
return `
<div class="module-panel">
<div class="module-header"><span>💬 安装时出现的问题反馈</span><button class="btn btn-primary btn-sm" onclick="openFeedbackModal()">+ 反馈问题</button></div>
<div class="module-body">
<table class="data-table">
<thead><tr><th>#</th><th>问题标题</th><th>发生位置</th><th>反馈人</th><th>反馈日期</th><th>处理状态</th><th>操作</th></tr></thead>
<tbody>${rows || '<tr><td colspan="7" style="text-align:center;color:#aaa;">暂无问题反馈</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function openFeedbackModal(idx) {
let it = { title:'', location:'', reporter:'', date:new Date().toISOString().slice(0,10), status:'pending', description:'', solution:'', preventAction:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.installFeedback[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '处理问题反馈' : '反馈问题';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>问题标题</label><input id="fb-title" value="${it.title||''}"></div>
<div class="form-row">
<div class="form-group"><label>发生位置</label><input id="fb-location" value="${it.location||''}" placeholder="如:主轧机底座安装"></div>
<div class="form-group"><label>反馈人</label><input id="fb-reporter" value="${it.reporter||''}"></div>
</div>
<div class="form-group"><label>问题描述</label><textarea id="fb-desc">${it.description||''}</textarea></div>
<div class="form-group"><label>解决方案</label><textarea id="fb-solution">${it.solution||''}</textarea></div>
<div class="form-group"><label>防止再发措施</label><textarea id="fb-prevent" placeholder="防止同类问题再次发生的措施">${it.preventAction||''}</textarea></div>
<div class="form-group"><label>处理状态</label><select id="fb-status">
<option value="pending" ${it.status==='pending'?'selected':''}>待处理</option>
<option value="processing" ${it.status==='processing'?'selected':''}>处理中</option>
<option value="resolved" ${it.status==='resolved'?'selected':''}>已解决</option>
</select></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveFeedback(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveFeedback(idx) {
const it = { title:document.getElementById('fb-title').value, location:document.getElementById('fb-location').value, reporter:document.getElementById('fb-reporter').value, date:document.getElementById('fb-date')?.value||new Date().toISOString().slice(0,10), status:document.getElementById('fb-status').value, description:document.getElementById('fb-desc').value, solution:document.getElementById('fb-solution').value, preventAction:document.getElementById('fb-prevent').value };
if (!it.title) { showToast('请填写问题标题'); return; }
if (!projectData.installFeedback) projectData.installFeedback = [];
if (idx >= 0) projectData.installFeedback[idx] = it; else projectData.installFeedback.push(it);
saveData(); closeModal(); renderModule('install_feedback'); showToast('问题反馈已保存');
}
function editFeedback(i) { openFeedbackModal(i); }
function deleteFeedback(i) { if (!confirm('确认删除?')) return; projectData.installFeedback.splice(i,1); saveData(); renderModule('install_feedback'); }
// ========== Acceptance Module (安装后验收) ==========
function renderAcceptance() {
const ac = projectData.acceptance.checklist || [];
const items = projectData.acceptance.items || [];
const checklistHtml = ac.map((it,i) => `
<div class="checklist-item ${it.checked?'checked':''}">
<input type="checkbox" ${it.checked?'checked':''} onchange="toggleAcceptanceCheck(${i});">
<span class="item-text">${it.text}</span>
</div>
`).join('');
const itemRows = items.map((it,i) => `
<tr>
<td>${i+1}</td><td>${it.item||''}</td><td>${it.requirement||''}</td><td>${it.actual||''}</td>
<td><span class="status-badge ${it.result==='pass'?'done':it.result==='fail'?'overdue':'pending'}">${it.result==='pass'?'合格':it.result==='fail'?'不合格':'待检'}</span></td>
<td><button class="btn btn-sm btn-danger" onclick="deleteAcceptanceItem(${i})">删除</button></td>
</tr>
`).join('');
const allDone = ac.every(it=>it.checked);
return `
<div class="module-panel">
<div class="module-header"><span>✅ 安装后验收</span><button class="btn btn-primary btn-sm" onclick="openAcceptanceItemModal()">+ 添加验收项</button></div>
<div class="module-body">
<div style="margin-bottom:16px;"><div style="font-weight:600;margin-bottom:8px;">📋 验收检查清单 ${allDone?'✅ 全部合格':'⚠️ 尚有未完成验收项'}</div>${checklistHtml}</div>
<hr style="margin:16px 0;border:none;border-top:1px solid var(--border);">
<div style="font-weight:600;margin-bottom:8px;">📊 详细验收数据</div>
<table class="data-table">
<thead><tr><th>#</th><th>验收项目</th><th>要求值</th><th>实测值</th><th>结果</th><th>操作</th></tr></thead>
<tbody>${itemRows || '<tr><td colspan="6" style="text-align:center;color:#aaa;">暂无验收数据</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function toggleAcceptanceCheck(i) {
projectData.acceptance.checklist[i].checked = !projectData.acceptance.checklist[i].checked;
saveData(); renderModule('acceptance');
}
function openAcceptanceItemModal(idx) {
let it = { item:'', requirement:'', actual:'', result:'pending', note:'' };
let isEdit = false;
if (idx !== undefined) { it = projectData.acceptance.items[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑验收项' : '添加验收项';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>验收项目</label><input id="acc-item" value="${it.item||''}"></div>
<div class="form-row"><div class="form-group"><label>要求值</label><input id="acc-req" value="${it.requirement||''}"></div><div class="form-group"><label>实测值</label><input id="acc-actual" value="${it.actual||''}"></div></div>
<div class="form-group"><label>验收结果</label><select id="acc-result">
<option value="pending" ${it.result==='pending'?'selected':''}>待检</option>
<option value="pass" ${it.result==='pass'?'selected':''}>合格</option>
<option value="fail" ${it.result==='fail'?'selected':''}>不合格</option>
</select></div>
<div class="form-group"><label>备注</label><textarea id="acc-note">${it.note||''}</textarea></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveAcceptanceItem(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveAcceptanceItem(idx) {
const it = { item:document.getElementById('acc-item').value, requirement:document.getElementById('acc-req').value, actual:document.getElementById('acc-actual').value, result:document.getElementById('acc-result').value, note:document.getElementById('acc-note').value };
if (!it.item) { showToast('请填写验收项目'); return; }
if (!projectData.acceptance.items) projectData.acceptance.items = [];
if (idx >= 0) projectData.acceptance.items[idx] = it; else projectData.acceptance.items.push(it);
saveData(); closeModal(); renderModule('acceptance'); showToast('验收项已保存');
}
function deleteAcceptanceItem(i) { if (!confirm('确认删除?')) return; projectData.acceptance.items.splice(i,1); saveData(); renderModule('acceptance'); }
// ========== Hot Commissioning Module (热负荷试车) ==========
function renderHotCommissioning() {
const hc = projectData.hotCommissioning.checklist || [];
const clauses = projectData.hotCommissioning.clauses || [];
const checklistHtml = hc.map((it,i) => `
<div class="checklist-item ${it.checked?'checked':''}">
<input type="checkbox" ${it.checked?'checked':''} onchange="toggleHCCheck(${i});">
<span class="item-text">${it.text}</span>
</div>
`).join('');
const clauseRows = clauses.map((c,i) => `
<tr>
<td>${i+1}</td><td>${c.clause||''}</td><td>${c.standard||''}</td><td>${c.result||''}</td>
<td><span class="status-badge ${c.pass?'done':'pending'}">${c.pass?'合格':'待确认'}</span></td>
<td><button class="btn btn-sm btn-danger" onclick="deleteHCClause(${i})">删除</button></td>
</tr>
`).join('');
const allDone = hc.every(it=>it.checked);
return `
<div class="module-panel">
<div class="module-header"><span>🔥 热负荷试车</span><button class="btn btn-primary btn-sm" onclick="openHCClauseModal()">+ 添加技术协议条款</button></div>
<div class="module-body">
<div style="margin-bottom:16px;padding:10px;background:#fff3cd;border-radius:6px;border:1px solid #ffe0b2;font-size:12px;">
📋 热负荷试车按照<b>技术协议签订条款</b>逐项确定。请添加技术协议中的关键条款,并逐项确认。
</div>
<div style="margin-bottom:16px;"><div style="font-weight:600;margin-bottom:8px;">📋 热负荷试车检查清单 ${allDone?'✅ 全部完成':'⚠️ 尚有未完成项'}</div>${checklistHtml}</div>
<hr style="margin:16px 0;border:none;border-top:1px solid var(--border);">
<div style="font-weight:600;margin-bottom:8px;">📊 技术协议条款确认</div>
<table class="data-table">
<thead><tr><th>#</th><th>协议条款</th><th>标准要求</th><th>试车结果</th><th>确认状态</th><th>操作</th></tr></thead>
<tbody>${clauseRows || '<tr><td colspan="6" style="text-align:center;color:#aaa;">暂无技术协议条款,请点击"添加技术协议条款"</td></tr>'}</tbody>
</table>
</div>
</div>`;
}
function toggleHCCheck(i) {
projectData.hotCommissioning.checklist[i].checked = !projectData.hotCommissioning.checklist[i].checked;
saveData(); renderModule('hot_commissioning');
}
function openHCClauseModal(idx) {
let c = { clause:'', standard:'', result:'', pass:false };
let isEdit = false;
if (idx !== undefined) { c = projectData.hotCommissioning.clauses[idx]; isEdit = true; }
document.getElementById('modal-title').textContent = isEdit ? '编辑条款' : '添加技术协议条款';
document.getElementById('modal-body').innerHTML = `
<div class="form-group"><label>技术协议条款</label><input id="hc-clause" value="${c.clause||''}" placeholder="如轧制力控制精度±2%"></div>
<div class="form-group"><label>标准要求</label><input id="hc-standard" value="${c.standard||''}" placeholder="如GB/T 标准"></div>
<div class="form-group"><label>试车结果</label><textarea id="hc-result" placeholder="试车实际结果">${c.result||''}</textarea></div>
<div style="margin-top:8px;"><label style="display:flex;align-items:center;gap:6px;font-size:13px;"><input type="checkbox" id="hc-pass" ${c.pass?'checked':''}> 合格/通过</label></div>
<div style="text-align:right;margin-top:12px;">
<button class="btn btn-outline" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveHCClause(${isEdit?idx:-1})">保存</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
function saveHCClause(idx) {
const c = { clause:document.getElementById('hc-clause').value, standard:document.getElementById('hc-standard').value, result:document.getElementById('hc-result').value, pass:document.getElementById('hc-pass')?.checked||false };
if (!c.clause) { showToast('请填写协议条款'); return; }
if (!projectData.hotCommissioning.clauses) projectData.hotCommissioning.clauses = [];
if (idx >= 0) projectData.hotCommissioning.clauses[idx] = c; else projectData.hotCommissioning.clauses.push(c);
saveData(); closeModal(); renderModule('hot_commissioning'); showToast('条款已保存');
}
function deleteHCClause(i) { if (!confirm('确认删除?')) return; projectData.hotCommissioning.clauses.splice(i,1); saveData(); renderModule('hot_commissioning'); }
// ========== Export Project Report ==========
function exportProjectReport() {
let html = `<h2>连轧机/可逆轧机设备总包项目报告</h2>`;
html += `<p><b>项目:</b>${projectData.projectInfo.name}</p>`;
html += `<p><b>编号:</b>${projectData.projectInfo.number} <b>客户:</b>${projectData.projectInfo.client}</p>`;
const s = getStageStatuses();
html += `<h3>各阶段状态</h3><ul>`;
Object.entries(s).forEach(([k,v]) => { html += `<li>${moduleTitles[k]||k}${v==='done'?'已完成':v==='progress'?'进行中':'未开始'}</li>`; });
html += `</ul>`;
html += `<p style="color:#888;font-size:12px;">生成时间:${new Date().toLocaleString()}</p>`;
const w = window.open('','_blank');
w.document.write(`<html><head><meta charset="UTF-8"><title>项目报告</title></head><body style="font-family:sans-serif;padding:32px;">${html}</body></html>`);
w.document.close();
w.print();
showToast('项目报告已生成');
}
// ========== Close Modal ==========
function closeModal() { document.getElementById('modal-overlay').classList.remove('show'); }
// ========== Init ==========
renderModule(projectData.currentModule || 'dashboard');
console.log('Rolling Mill Project Management System loaded');
</script>
</body>
</html>