4646 lines
288 KiB
HTML
4646 lines
288 KiB
HTML
|
|
<!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="记录设计思维,例如: 1. 选型理由:选择六辊轧机是因板形控制优于四辊... 2. 设计依据:参照GB/T标准进行强度校核... 3. 关键决策:主电机功率选800kW,满载加速力矩满足... 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>
|
|||
|
|
• HTTP/HTTPS API地址:<code>http://192.168.1.100:8080/api/drawings</code><br>
|
|||
|
|
• 网络共享路径:<code>\\\\server-fs01\\drawings</code><br>
|
|||
|
|
• 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>
|