当前位置: 首页 > news >正文

36、供应链计划与执行优化 (军方后勤) - /供应链管理组件/military-logistics-scm

76个工业组件库示例汇总

供应链管理组件:军方后勤供应链计划与执行模拟

概述

本组件模拟了一个简化的军方后勤保障场景下的供应链计划与执行流程。它旨在可视化需求预测、库存管理、供应计划(采购与调拨)以及执行监控(在途物资跟踪)等核心环节,并展示关键绩效指标 (KPI)。

组件界面采用苹果科技工业风格,力求信息清晰、布局合理、交互简洁。

功能特性

  • 多维度模拟: 同时模拟需求产生、库存消耗、补货决策、采购执行和调拨运输。
  • 需求预测展示: 显示未来一段时间内(模拟 24 小时)各单位对不同物资的预测需求及紧急程度。
  • 库存状态监控: 按基地/仓库展示各类物资的当前库存、安全库存水平,并通过进度条直观显示库存状态(安全、警戒、危险)。支持按地点筛选。
  • 供应计划可视化: 分别列出待处理的采购订单和计划中的内部调拨任务。
  • 执行过程跟踪: 实时监控进行中的采购和调拨任务状态(运输中、延误、到达等),并显示预计到达时间 (ETA) 和进度。
  • 关键指标 (KPI) 仪表盘: 动态计算并展示核心供应链绩效指标,如:
    • 需求满足率
    • 平均库存水平
    • 运输准时率
    • 采购准时率
    • 紧急订单比例
    • 模拟的总运输成本
  • 模拟控制: 支持启动/停止模拟,并可调节模拟运行速率。
  • 随机事件: 模拟中加入了运输延误、采购延误等随机事件,增加真实感。
  • 视觉风格: 采用苹果科技工业风格,界面清晰、专业。
  • 响应式布局: 适应不同屏幕尺寸。

文件结构

military-logistics-scm/
├── index.html         # 组件的 HTML 结构 (多面板布局)
├── styles.css         # 组件的 CSS 样式 (含表格、进度条、KPI 卡片)
├── script.js          # JavaScript 核心模拟逻辑
└── README.md          # 本说明文档

如何使用

  1. 集成: 确保 供应链管理组件 文件夹存在,并将 military-logistics-scm 文件夹放置其中。
  2. 加载: 在您的应用程序或 Appsmith 等平台中加载 index.html 文件。
  3. 交互:
    • 启动/停止: 使用顶部的"启动"和"停止"按钮控制模拟的运行。
    • 调整速率: 通过速率滑块加快或减慢模拟时间流逝速度。
    • 筛选库存: 在"库存状态"面板顶部的下拉菜单中选择特定基地/仓库以查看其库存详情。
    • 观察数据: 实时观察各面板数据的动态变化,了解模拟的供应链运作情况。
    • 监控 KPI: 在"关键指标"面板查看各项绩效的变化趋势。

技术栈

  • HTML5
  • CSS3 (Grid Layout, Flexbox, CSS Variables)
  • JavaScript (ES6+, Object Oriented Concepts (Implicit), Interval Timing)

模拟假设与限制

  • 简化模型: 这是一个高度简化的供应链模型,未包含所有真实世界的复杂性(如多级库存、产能限制、复杂运输网络、成本优化算法等)。
  • 决策逻辑: 补货决策基于简单的安全库存阈值,未采用高级算法 (如 MRP, DRP)。采购/调拨源选择逻辑也已简化。
  • 数据随机性: 需求生成、延误事件等均基于概率随机发生,可能与实际情况有偏差。
  • 成本模型: 运输成本为示意性计算,未考虑实际运费结构。
  • 可视化: 未包含地图可视化等高级展示方式。

未来可扩展方向

  • 引入更复杂的补货策略和优化算法。
  • 增加供应商选择、产能限制等约束条件。
  • 模拟更详细的运输网络和模式。
  • 添加更丰富的随机事件(如质量问题、需求取消)。
  • 集成可视化图表库 (如 Chart.js) 来展示 KPI 趋势。
  • 允许用户交互式地调整部分参数(如安全库存)。
  • 添加详细的事件日志记录与分析功能。

效果展示

在这里插入图片描述

源码

index.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><link rel="stylesheet" href="styles.css"><!-- Font Awesome for Icons --><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body><div class="app-container"><!-- Header: Title, Controls, Global Status --><header class="app-header"><div class="header-title"><i class="fas fa-truck-loading header-icon"></i><h1>军方后勤供应链</h1></div><div class="controls-status-wrapper"><div class="simulation-controls"><button id="start-btn" class="btn btn-primary"><i class="fas fa-play"></i> 启动</button><button id="stop-btn" class="btn btn-danger" disabled><i class="fas fa-stop"></i> 停止</button><div class="speed-control"><label for="speed-slider">速率:</label><input type="range" id="speed-slider" min="1" max="5" value="1" step="1"><span id="speed-value">x1</span></div></div><div class="global-status"><span>时间: <strong id="simulation-time">T+0 H</strong></span><span>状态: <strong id="simulation-status-text">已停止</strong></span><span id="overall-fulfillment">满足率: <strong >--%</strong></span></div></div></header><!-- Main Content Grid --><main class="app-content"><!-- Demand Forecast Panel --><section class="panel demand-panel"><div class="panel-header"><h2><i class="fas fa-bullseye"></i> 需求预测</h2><span class="panel-info">未来 24 小时</span></div><div id="demand-forecast" class="panel-content scrollable"><!-- Demand items will be loaded here --><p class="placeholder">等待数据加载...</p></div></section><!-- Inventory Status Panel --><section class="panel inventory-panel"><div class="panel-header"><h2><i class="fas fa-boxes"></i> 库存状态</h2><select id="inventory-location-filter" class="header-filter"><option value="all">所有基地</option><!-- Locations loaded by JS --></select></div><div id="inventory-status" class="panel-content scrollable"><!-- Inventory items will be loaded here --><p class="placeholder">等待数据加载...</p></div></section><!-- Supply Plan Panel --><section class="panel supply-plan-panel"><div class="panel-header"><h2><i class="fas fa-clipboard-list"></i> 供应计划</h2><span class="panel-info">采购 & 调拨</span></div><div class="panel-content scrollable"><h4>待处理采购</h4><div id="procurement-plan" class="sub-panel-content"><p class="placeholder">无待处理采购</p></div><h4>计划中调拨</h4><div id="transfer-plan" class="sub-panel-content"><p class="placeholder">无计划中调拨</p></div></div></section><!-- Execution Monitoring Panel --><section class="panel execution-panel"><div class="panel-header"><h2><i class="fas fa-shipping-fast"></i> 执行监控</h2><span class="panel-info">在途物资</span></div><div id="execution-monitor" class="panel-content scrollable"><!-- Ongoing shipments/procurements will be listed here --><p class="placeholder">无在途任务</p></div></section><!-- KPIs Panel --><section class="panel kpi-panel"><div class="panel-header"><h2><i class="fas fa-chart-line"></i> 关键指标 (KPI)</h2></div><div id="kpi-dashboard" class="panel-content kpi-grid"><div class="kpi-item"><span class="kpi-label">需求满足率</span><strong id="kpi-fulfillment" class="kpi-value">--%</strong></div><div class="kpi-item"><span class="kpi-label">平均库存水平</span><strong id="kpi-avg-inventory" class="kpi-value">--</strong></div><div class="kpi-item"><span class="kpi-label">运输准时率</span><strong id="kpi-on-time-delivery" class="kpi-value">--%</strong></div><div class="kpi-item"><span class="kpi-label">采购准时率</span><strong id="kpi-purchase-ontime" class="kpi-value">--%</strong></div><div class="kpi-item"><span class="kpi-label">紧急订单比例</span><strong id="kpi-urgent-orders" class="kpi-value">--%</strong></div><div class="kpi-item"><span class="kpi-label">总运输成本()</span><strong id="kpi-transport-cost" class="kpi-value">--</strong></div></div></section></main></div><script src="script.js"></script>
</body>
</html> 

styles.css

/* =============================================================================Military Logistics SCM Simulation - Stylesheet=============================================================================Description: Apple-inspired industrial tech style for SCM simulation.Author: Gemini AIVersion: 1.0.0============================================================================= *//* --- Global Variables & Resets --- */
:root {--primary-color: #007aff; /* Apple Blue */--secondary-color: #5ac8fa; /* Lighter Blue */--background-color: #f2f2f7; /* Light Gray Background */--panel-background: #ffffff; /* White Panel Background */--text-color: #1d1d1f; /* Near Black Text */--subtle-text-color: #6e6e73; /* Gray Text */--border-color: #d1d1d6; /* Light Gray Border */--success-color: #34c759; /* Green */--warning-color: #ff9500; /* Orange */--error-color: #ff3b30; /* Red */--info-color: #5ac8fa; /* Light Blue for informational items */--highlight-color: #eef7ff; /* Very light blue for highlights */--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--base-font-size: 14px;--border-radius: 8px;--box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.05);--transition-speed: 0.25s;--panel-gap: 15px;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);font-size: var(--base-font-size);color: var(--text-color);background-color: var(--background-color);line-height: 1.5;overflow-x: hidden;
}/* --- App Container --- */
.app-container {display: flex;flex-direction: column;max-width: 1600px; /* Allow wider view for more panels */margin: 15px auto;background-color: var(--panel-background);border-radius: var(--border-radius);box-shadow: var(--box-shadow);border: 1px solid var(--border-color);overflow: hidden;min-height: 550px; /* Adjust min height */height: auto;
}/* --- Header --- */
.app-header {display: flex;justify-content: space-between;align-items: center;padding: 12px 20px;border-bottom: 1px solid var(--border-color);background-color: #fdfdfd; /* Slightly off-white header */flex-wrap: wrap;gap: 15px;
}.header-title {display: flex;align-items: center;gap: 12px;
}.header-icon {font-size: 1.8em;color: var(--primary-color);
}.header-title h1 {font-size: 1.3em;font-weight: 600;
}.controls-status-wrapper {display: flex;align-items: center;gap: 30px; /* Increased gap */flex-wrap: wrap;
}.simulation-controls {display: flex;align-items: center;gap: 15px;
}.speed-control {display: flex;align-items: center;gap: 8px;font-size: 0.9em;color: var(--subtle-text-color);
}#speed-slider {width: 80px;cursor: pointer;
}#speed-value {font-weight: 600;min-width: 25px; /* Ensure space for x5 */
}.global-status {display: flex;gap: 20px;font-size: 0.9em;color: var(--subtle-text-color);background-color: #f5f5f7;padding: 5px 12px;border-radius: 6px;border: 1px solid var(--border-color);
}.global-status strong {color: var(--text-color);font-weight: 600;
}/* --- Buttons --- */
.btn {padding: 8px 16px;font-size: 0.9em;font-weight: 500;border: none;border-radius: 6px;cursor: pointer;transition: background-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;display: inline-flex; /* Align icon and text */align-items: center;gap: 6px;
}.btn i {font-size: 0.95em;
}.btn:disabled {opacity: 0.6;cursor: not-allowed;
}.btn-primary {background-color: var(--primary-color);color: white;
}
.btn-primary:hover:not(:disabled) {background-color: #005ecb;
}.btn-danger {background-color: var(--error-color);color: white;
}
.btn-danger:hover:not(:disabled) {background-color: #d9352a;
}/* --- Main Content Grid --- */
.app-content {display: grid;/* Adjust grid template based on desired layout */grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));/* grid-template-rows: repeat(2, auto); */gap: var(--panel-gap);padding: var(--panel-gap);flex-grow: 1;overflow: hidden; /* Prevent content overflow from breaking grid */
}/* --- Panels --- */
.panel {background-color: var(--panel-background);border-radius: var(--border-radius);border: 1px solid var(--border-color);box-shadow: var(--box-shadow);display: flex;flex-direction: column;overflow: hidden; /* Important for scrollable content */min-height: 250px; /* Ensure panels have some min height */
}.panel-header {display: flex;justify-content: space-between;align-items: center;padding: 12px 15px;border-bottom: 1px solid var(--border-color);flex-shrink: 0;background-color: #f9f9f9;
}.panel-header h2 {font-size: 1.05em;font-weight: 600;display: flex;align-items: center;gap: 8px;
}.panel-header i {color: var(--primary-color);font-size: 1.1em;
}.panel-info, .header-filter {font-size: 0.85em;color: var(--subtle-text-color);
}.header-filter {padding: 3px 6px;border: 1px solid var(--border-color);border-radius: 4px;background-color: white;max-width: 150px;
}.panel-content {flex-grow: 1;padding: 15px;overflow-y: auto; /* Default overflow behavior */position: relative; /* For placeholder positioning */
}.panel-content.scrollable {overflow-y: auto;
}.placeholder {position: absolute; /* Center placeholder if content is empty */top: 50%;left: 50%;transform: translate(-50%, -50%);text-align: center;color: #b0b0b0;font-style: italic;font-size: 0.9em;
}/* --- Specific Panel Styles --- *//* Demand Panel */
.demand-item {display: flex;justify-content: space-between;align-items: center;padding: 8px 0;border-bottom: 1px dotted var(--border-color);font-size: 0.9em;
}
.demand-item:last-child { border-bottom: none; }
.demand-details { flex-grow: 1; margin-right: 10px; }
.demand-unit { font-weight: 500; }
.demand-material { color: var(--subtle-text-color); font-size: 0.9em; }
.demand-quantity {font-weight: 600;min-width: 50px;text-align: right;
}
.demand-urgency {margin-left: 10px;font-size: 0.8em;padding: 2px 5px;border-radius: 4px;color: white;
}
.demand-urgency.high { background-color: var(--error-color); }
.demand-urgency.medium { background-color: var(--warning-color); }
.demand-urgency.low { background-color: var(--success-color); }/* Inventory Panel */
.inventory-item {display: grid;grid-template-columns: 2fr 1fr 1fr 1.5fr; /* Material, Qty, Safe Stk, Status */gap: 10px;align-items: center;padding: 8px 0;border-bottom: 1px dotted var(--border-color);font-size: 0.9em;
}
.inventory-item:last-child { border-bottom: none; }.inventory-material { font-weight: 500; }
.inventory-qty { text-align: right; }
.inventory-safe-stock { color: var(--subtle-text-color); text-align: right; }
.inventory-status-bar {width: 100%;height: 10px;background-color: #e9ecef;border-radius: 5px;overflow: hidden;position: relative;
}
.inventory-status-fill {height: 100%;background-color: var(--success-color);border-radius: 5px;transition: width var(--transition-speed) ease-out;
}
.inventory-status-fill.warning {background-color: var(--warning-color);
}
.inventory-status-fill.danger {background-color: var(--error-color);
}
.inventory-safe-marker {position: absolute;top: 0;bottom: 0;width: 2px;background-color: rgba(0, 0, 0, 0.3);/* left position set by JS */
}/* Supply Plan Panel */
.supply-plan-panel .panel-content {padding-top: 5px;
}
.supply-plan-panel h4 {font-size: 0.9em;font-weight: 600;color: var(--subtle-text-color);margin-top: 15px;margin-bottom: 5px;padding-bottom: 5px;border-bottom: 1px solid var(--border-color);
}
.supply-plan-panel h4:first-of-type {margin-top: 0;
}
.sub-panel-content {position: relative; /* Needed for placeholder */min-height: 50px; /* Ensure space even when empty */
}.procurement-item, .transfer-item {display: flex;justify-content: space-between;align-items: center;padding: 6px 0;border-bottom: 1px dotted var(--border-color);font-size: 0.85em;
}
.procurement-item:last-child, .transfer-item:last-child { border-bottom: none; }.item-details { flex-grow: 1; margin-right: 10px; }
.item-id { font-weight: 500; font-family: monospace; font-size: 0.9em; }
.item-material { color: var(--subtle-text-color); }
.item-info { font-size: 0.9em; color: var(--subtle-text-color);}
.item-qty { font-weight: 600; min-width: 40px; text-align: right; }
.item-eta, .item-status {font-size: 0.9em;margin-left: 10px;color: var(--subtle-text-color);min-width: 60px; /* Ensure space */text-align: right;
}/* Execution Monitoring Panel */
.execution-item {border: 1px solid var(--border-color);border-radius: 6px;margin-bottom: 10px;padding: 10px;background-color: #fdfdfd;font-size: 0.85em;position: relative;
}
.execution-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 8px;
}
.execution-id {font-weight: 600;font-family: monospace;color: var(--primary-color);
}
.execution-status {padding: 2px 8px;border-radius: 4px;font-size: 0.9em;font-weight: 500;color: white;
}
.execution-status.pending { background-color: var(--subtle-text-color); }
.execution-status.in-transit { background-color: var(--info-color); }
.execution-status.delayed { background-color: var(--warning-color); }
.execution-status.arrived { background-color: var(--success-color); }
.execution-status.issue { background-color: var(--error-color); }.execution-details {display: flex;justify-content: space-between;gap: 15px;margin-bottom: 8px;
}
.execution-details > div { flex: 1; }
.execution-label { font-weight: 500; color: var(--subtle-text-color); }
.execution-value {}.execution-progress-bar {width: 100%;height: 6px;background-color: #e9ecef;border-radius: 3px;overflow: hidden;
}
.execution-progress-fill {height: 100%;background-color: var(--primary-color);border-radius: 3px;transition: width var(--transition-speed) linear;
}
.execution-progress-fill.delayed {background-color: var(--warning-color);
}
.execution-progress-fill.issue {background-color: var(--error-color);
}/* KPI Panel */
.kpi-grid {display: grid;grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));gap: 15px;padding: 10px; /* Reduced padding inside grid */
}.kpi-item {background-color: #f8f8fa;border: 1px solid var(--border-color);border-radius: 6px;padding: 15px;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;
}.kpi-label {font-size: 0.85em;color: var(--subtle-text-color);margin-bottom: 5px;
}.kpi-value {font-size: 1.4em;font-weight: 600;color: var(--primary-color);
}/* --- Responsive Design --- */
@media (max-width: 1200px) {.app-content {grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));}
}@media (max-width: 992px) {.app-header {flex-direction: column;align-items: stretch;}.controls-status-wrapper {flex-direction: column;align-items: stretch;gap: 10px;}.global-status {justify-content: space-around;}.app-content {display: flex; /* Stack panels vertically */flex-direction: column;}.panel {min-height: 200px; /* Reduce min height */}.kpi-grid {grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));}.kpi-value {font-size: 1.2em;}
}@media (max-width: 600px) {.header-title h1 {font-size: 1.1em;}.simulation-controls {flex-direction: column;align-items: stretch;}.speed-control {justify-content: center;}#speed-slider {width: 120px;}.global-status span {font-size: 0.8em;}.kpi-grid {grid-template-columns: repeat(2, 1fr); /* 2 columns on small screens */}.panel-header h2 {font-size: 1em;}.btn {padding: 10px 15px;}
} 

script.js

/* =============================================================================Military Logistics SCM Simulation - Script=============================================================================Description: Simulates military logistics supply chain planning & execution.Author: Gemini AIVersion: 1.0.0============================================================================= */// --- Simulation Configuration ---
const BASE_SIMULATION_INTERVAL_MS = 2000; // Base interval for 1x speed (ms)
const HOURS_PER_TICK = 1; // How many hours pass each base interval tick
const DEMAND_GENERATION_PROBABILITY = 0.6; // Chance to generate new demand each tick
const TRANSPORT_DELAY_PROBABILITY = 0.1;
const PURCHASE_DELAY_PROBABILITY = 0.08;
const REPLENISHMENT_CHECK_INTERVAL_TICKS = 4; // Check inventory levels every N ticks
const FORECAST_HORIZON_HOURS = 24;
const LOG_CAPACITY = 200; // If adding logs later// --- Game Data (Simplified) ---
const MATERIALS = [{ id: 'M01', name: '口粮 (标准)', unit: '箱' },{ id: 'M02', name: '弹药 (5.56mm)', unit: '箱' },{ id: 'M03', name: '医疗包 (战场)', unit: '个' },{ id: 'M04', name: '燃料 (柴油)', unit: '升' },{ id: 'M05', name: '备件 (通讯)', unit: '件' },
];const LOCATIONS = [{ id: 'B01', name: '前线基地 A', type: 'base', x: 100, y: 100 },{ id: 'B02', name: '前线基地 B', type: 'base', x: 400, y: 150 },{ id: 'WH01', name: '后方仓库 Z', type: 'warehouse', x: 250, y: 300 },{ id: 'SUP01', name: '供应商 X', type: 'supplier' },{ id: 'SUP02', name: '供应商 Y', type: 'supplier' },
];const UNITS = [{ id: 'U01', name: '机动步兵连 Alpha', baseId: 'B01' },{ id: 'U02', name: '装甲侦察排 Bravo', baseId: 'B01' },{ id: 'U03', name: '炮兵支援营 Charlie', baseId: 'B02' },
];// Lead times (in simulation hours)
const TRANSPORT_TIMES = {'WH01-B01': 10,'WH01-B02': 12,'B01-B02': 6, // Inter-base transfer
};
const PROCUREMENT_LEAD_TIMES = {'SUP01': 48,'SUP02': 60,
};// --- Global State ---
let state = {isRunning: false,simulationTimer: null,simulationSpeed: 1,currentTimeHours: 0,tickCounter: 0,inventory: {}, // { locationId: { materialId: { qty: number, safetyStock: number, onOrderPO: number, incomingTO: number } } }demandForecast: [], // { id, unitId, materialId, qty, dueTime, urgency('low','medium','high'), status('pending','fulfilled','shortage') }purchaseOrders: [], // { id, supplierId, materialId, qty, destinationId, orderTime, originalEta, currentEta, status('pending', 'ordered', 'in-transit', 'delayed', 'arrived', 'issue'), progress }transferOrders: [], // { id, sourceId, destinationId, materialId, qty, startTime, originalEta, currentEta, status('pending', 'in-transit', 'delayed', 'arrived'), progress }kpis: {totalDemand: 0,fulfilledDemand: 0,totalPOs: 0,onTimePOs: 0,totalTOs: 0,onTimeTOs: 0,urgentOrders: 0,simulatedTransportCost: 0,},locationFilter: 'all',
};// --- DOM Elements ---
const elements = {};// =============================================================================
// Initialization
// =============================================================================document.addEventListener('DOMContentLoaded', initializeApp);function initializeApp() {console.log("Initializing Military Logistics SCM App...");try {// 在Appsmith环境中,使用querySelector找到应用容器const appContainer = document.querySelector('.app-container');if (!appContainer) {console.error("无法找到应用容器 .app-container,应用初始化失败");return;}console.log("找到应用容器,开始初始化...");// 使用容器范围内的查询而不是全局documentif (!queryDOMElements(appContainer)) {console.error("初始化失败:组件内未找到必要的DOM元素");return;}initializeState();setupEventListeners();renderAllPanels(); // 基于初始状态进行首次渲染console.log("Military Logistics SCM App 初始化完成");} catch (error) {console.error("应用初始化过程中发生错误:", error);}
}function queryDOMElements(container) {try {// 基于CSS选择器而不是ID来查找元素elements.startBtn = container.querySelector('#start-btn, button[id="start-btn"]');elements.stopBtn = container.querySelector('#stop-btn, button[id="stop-btn"]');elements.speedSlider = container.querySelector('#speed-slider, input[id="speed-slider"]');elements.speedValue = container.querySelector('#speed-value, span[id="speed-value"]');elements.simulationTime = container.querySelector('#simulation-time, strong[id="simulation-time"]');elements.simulationStatusText = container.querySelector('#simulation-status-text, strong[id="simulation-status-text"]');elements.overallFulfillment = container.querySelector('#overall-fulfillment, span[id="overall-fulfillment"]');elements.demandForecast = container.querySelector('#demand-forecast, div[id="demand-forecast"]');elements.inventoryLocationFilter = container.querySelector('#inventory-location-filter, select[id="inventory-location-filter"]');elements.inventoryStatus = container.querySelector('#inventory-status, div[id="inventory-status"]');elements.procurementPlan = container.querySelector('#procurement-plan, div[id="procurement-plan"]');elements.transferPlan = container.querySelector('#transfer-plan, div[id="transfer-plan"]');elements.executionMonitor = container.querySelector('#execution-monitor, div[id="execution-monitor"]');elements.kpiDashboard = container.querySelector('#kpi-dashboard, div[id="kpi-dashboard"]');elements.kpiFulfillment = container.querySelector('#kpi-fulfillment, strong[id="kpi-fulfillment"]');elements.kpiAvgInventory = container.querySelector('#kpi-avg-inventory, strong[id="kpi-avg-inventory"]');elements.kpiOnTimeDelivery = container.querySelector('#kpi-on-time-delivery, strong[id="kpi-on-time-delivery"]');elements.kpiPurchaseOntime = container.querySelector('#kpi-purchase-ontime, strong[id="kpi-purchase-ontime"]');elements.kpiUrgentOrders = container.querySelector('#kpi-urgent-orders, strong[id="kpi-urgent-orders"]');elements.kpiTransportCost = container.querySelector('#kpi-transport-cost, strong[id="kpi-transport-cost"]');// 检查关键元素,打印调试信息const missingElements = [];for (const [key, element] of Object.entries(elements)) {if (!element) {console.warn(`未找到元素: ${key}`);missingElements.push(key);}}if (missingElements.length > 0) {console.error("缺少以下DOM元素:", missingElements.join(", "));// 不立即返回false,继续尝试初始化}// 即使有些元素找不到,也返回true以允许应用程序继续return true;} catch (error) {console.error("查询DOM元素时发生错误:", error);return false;}
}function initializeState() {state.currentTimeHours = 0;state.tickCounter = 0;state.demandForecast = [];state.purchaseOrders = [];state.transferOrders = [];state.kpis = { totalDemand: 0, fulfilledDemand: 0, totalPOs: 0, onTimePOs: 0, totalTOs: 0, onTimeTOs: 0, urgentOrders: 0, simulatedTransportCost: 0 };state.locationFilter = 'all';if(elements.inventoryLocationFilter) elements.inventoryLocationFilter.value = 'all';// Initialize Inventory & populate filterstate.inventory = {};const locationFilter = elements.inventoryLocationFilter;if(locationFilter) locationFilter.innerHTML = '<option value="all">所有基地</option>'; // Reset filterLOCATIONS.forEach(loc => {if (loc.type === 'base' || loc.type === 'warehouse') {state.inventory[loc.id] = {};if(locationFilter) {const option = document.createElement('option');option.value = loc.id;option.textContent = loc.name;locationFilter.appendChild(option);}MATERIALS.forEach(mat => {// Simulate initial stock & safety stockconst initialQty = Math.floor(Math.random() * 150 + 50); // Random initial stockconst safetyStock = Math.floor(initialQty * (Math.random() * 0.2 + 0.2)); // 20-40% safety stockstate.inventory[loc.id][mat.id] = {qty: initialQty,safetyStock: safetyStock,onOrderPO: 0,incomingTO: 0,};});}});
}function setupEventListeners() {try {// 防御性检查:确保元素存在再添加监听器if (elements.startBtn) {elements.startBtn.addEventListener('click', startSimulation);console.log("已成功添加开始按钮事件监听器");} else {console.error("无法添加事件监听器: start-btn 元素未找到");}if (elements.stopBtn) {elements.stopBtn.addEventListener('click', stopSimulation);console.log("已成功添加停止按钮事件监听器");} else {console.error("无法添加事件监听器: stop-btn 元素未找到");}if (elements.speedSlider) {elements.speedSlider.addEventListener('input', handleSpeedChange);console.log("已成功添加速度滑块事件监听器");} else {console.error("无法添加事件监听器: speed-slider 元素未找到");}if (elements.inventoryLocationFilter) {elements.inventoryLocationFilter.addEventListener('change', handleLocationFilterChange);console.log("已成功添加库存位置过滤器事件监听器");} else {console.error("无法添加事件监听器: inventory-location-filter 元素未找到");}} catch (error) {console.error("设置事件监听器时发生错误:", error);}
}// =============================================================================
// Simulation Control
// =============================================================================function startSimulation() {if (state.isRunning) return;state.isRunning = true;updateControlButtons();updateStatusDisplay();console.log("Simulation started.");const interval = BASE_SIMULATION_INTERVAL_MS / state.simulationSpeed;state.simulationTimer = setInterval(simulationStep, interval);
}function stopSimulation() {if (!state.isRunning) return;state.isRunning = false;clearInterval(state.simulationTimer);state.simulationTimer = null;updateControlButtons();updateStatusDisplay();console.log("Simulation stopped.");
}function handleSpeedChange() {state.simulationSpeed = parseInt(elements.speedSlider.value);elements.speedValue.textContent = `x${state.simulationSpeed}`;if (state.isRunning) {// Restart timer with new speedclearInterval(state.simulationTimer);const interval = BASE_SIMULATION_INTERVAL_MS / state.simulationSpeed;state.simulationTimer = setInterval(simulationStep, interval);}
}function handleLocationFilterChange() {state.locationFilter = elements.inventoryLocationFilter.value;renderInventoryPanel(); // Re-render inventory panel with filter
}// =============================================================================
// Simulation Core Loop
// =============================================================================function simulationStep() {if (!state.isRunning) return;// 1. Advance Timestate.currentTimeHours += HOURS_PER_TICK;state.tickCounter++;// 2. Generate New DemandgenerateDemand();// 3. Fulfill Demand from InventoryfulfillDemand();// 4. Update Ongoing Tasks (Transfers & Procurements)updateTransferOrders();updatePurchaseOrders();// 5. Check Inventory & Trigger Replenishment (less frequently)if (state.tickCounter % REPLENISHMENT_CHECK_INTERVAL_TICKS === 0) {checkAndReplenishInventory();}// 6. Update KPIsupdateKPIs();// 7. Render UIrenderAllPanels();
}// =============================================================================
// Simulation Sub-Logics
// =============================================================================function generateDemand() {if (Math.random() < DEMAND_GENERATION_PROBABILITY) {const randomUnit = UNITS[Math.floor(Math.random() * UNITS.length)];const randomMaterial = MATERIALS[Math.floor(Math.random() * MATERIALS.length)];const randomQty = Math.floor(Math.random() * 20 + 5); // Demand qty 5-25const urgencyRoll = Math.random();let urgency = 'low';if (urgencyRoll < 0.1) urgency = 'high';else if (urgencyRoll < 0.4) urgency = 'medium';const dueTime = state.currentTimeHours + Math.floor(Math.random() * FORECAST_HORIZON_HOURS + 1);const newDemand = {id: `D${String(state.kpis.totalDemand + 1).padStart(4, '0')}`,unitId: randomUnit.id,unitName: randomUnit.name,baseId: randomUnit.baseId,materialId: randomMaterial.id,materialName: randomMaterial.name,qty: randomQty,requestTime: state.currentTimeHours,dueTime: dueTime,urgency: urgency,status: 'pending',};state.demandForecast.push(newDemand);state.kpis.totalDemand++;if (urgency === 'high') state.kpis.urgentOrders++;// console.log(`Generated Demand: ${newDemand.id} for ${newDemand.qty} ${newDemand.materialName} at ${newDemand.unitName}`);}// Clean up old demands (e.g., remove very old fulfilled/shortage demands?)state.demandForecast = state.demandForecast.filter(d => d.status === 'pending' || (state.currentTimeHours - d.requestTime) < 48); // Keep recent history
}function fulfillDemand() {state.demandForecast.forEach(demand => {if (demand.status === 'pending' && demand.dueTime <= state.currentTimeHours) {const inventoryLoc = state.inventory[demand.baseId];if (inventoryLoc && inventoryLoc[demand.materialId] && inventoryLoc[demand.materialId].qty >= demand.qty) {inventoryLoc[demand.materialId].qty -= demand.qty;demand.status = 'fulfilled';state.kpis.fulfilledDemand++;// console.log(`Fulfilled Demand: ${demand.id}`);} else {// Check if partially fulfillable?// For now, mark as shortage if not fully met by due timedemand.status = 'shortage';console.warn(`Shortage for Demand: ${demand.id} at ${demand.baseId}`);}}});
}function updateTransferOrders() {state.transferOrders.forEach(to => {if (to.status === 'in-transit' || to.status === 'delayed') {const transportDuration = getTransportTime(to.sourceId, to.destinationId);if (!transportDuration) {console.error(`Invalid route for TO ${to.id}: ${to.sourceId} -> ${to.destinationId}`);to.status = 'issue';return;}// Calculate expected progress without delaysconst timeElapsed = state.currentTimeHours - to.startTime;let expectedProgress = Math.min(100, Math.max(0, (timeElapsed / transportDuration) * 100));// Simulate potential new delaysif (to.status !== 'delayed' && Math.random() < TRANSPORT_DELAY_PROBABILITY / 10) { // Lower chance for ongoing delayconst delayHours = Math.floor(Math.random() * 5 + 1);to.currentEta += delayHours;to.status = 'delayed';console.warn(`Transport TO ${to.id} delayed by ${delayHours} hours. New ETA: T+${to.currentEta}`);}// Update actual progress based on current time vs start time// Progress stops increasing if delayed beyond original ETA? No, keep it relative to transport duration.to.progress = expectedProgress;// Check for arrivalif (state.currentTimeHours >= to.currentEta) {to.status = 'arrived';to.progress = 100;// Add stock to destinationconst destInv = state.inventory[to.destinationId];if (destInv && destInv[to.materialId]) {destInv[to.materialId].qty += to.qty;destInv[to.materialId].incomingTO -= to.qty;} else {console.error(`Could not add arrived TO ${to.id} stock at ${to.destinationId}`);}state.kpis.totalTOs++;if (to.currentEta <= to.originalEta) state.kpis.onTimeTOs++;state.kpis.simulatedTransportCost += (transportDuration * to.qty * 0.1); // Example cost factorconsole.log(`Transfer Order ${to.id} arrived at ${to.destinationId}.`);}}});// Remove completed orders after a while? For now, keep them.// state.transferOrders = state.transferOrders.filter(to => to.status !== 'arrived');
}function updatePurchaseOrders() {state.purchaseOrders.forEach(po => {if (po.status === 'ordered' || po.status === 'in-transit' || po.status === 'delayed') {const leadTime = PROCUREMENT_LEAD_TIMES[po.supplierId];if (!leadTime) {console.error(`Invalid supplier for PO ${po.id}: ${po.supplierId}`);po.status = 'issue';return;}// Assume 'ordered' becomes 'in-transit' after a short fixed time or % of lead timeif (po.status === 'ordered' && state.currentTimeHours >= po.orderTime + leadTime * 0.1) {po.status = 'in-transit';}if (po.status === 'in-transit' || po.status === 'delayed'){const timeElapsed = state.currentTimeHours - po.orderTime;let expectedProgress = Math.min(100, Math.max(0, (timeElapsed / leadTime) * 100));// Simulate potential new delaysif (po.status !== 'delayed' && Math.random() < PURCHASE_DELAY_PROBABILITY / 10) {const delayHours = Math.floor(Math.random() * 24 + 6); // Longer purchase delayspo.currentEta += delayHours;po.status = 'delayed';console.warn(`Purchase PO ${po.id} delayed by ${delayHours} hours. New ETA: T+${po.currentEta}`);}po.progress = expectedProgress;// Check for arrivalif (state.currentTimeHours >= po.currentEta) {po.status = 'arrived';po.progress = 100;const destInv = state.inventory[po.destinationId];if (destInv && destInv[po.materialId]) {destInv[po.materialId].qty += po.qty;destInv[po.materialId].onOrderPO -= po.qty;} else {console.error(`Could not add arrived PO ${po.id} stock at ${po.destinationId}`);}state.kpis.totalPOs++;if (po.currentEta <= po.originalEta) state.kpis.onTimePOs++;console.log(`Purchase Order ${po.id} arrived at ${po.destinationId}.`);}}}});
}function checkAndReplenishInventory() {console.log(`Tick ${state.tickCounter}: Checking inventory levels...`);for (const locationId in state.inventory) {if (!LOCATIONS.find(l=>l.id === locationId && (l.type === 'base' || l.type === 'warehouse'))) continue;for (const materialId in state.inventory[locationId]) {const item = state.inventory[locationId][materialId];const neededQty = item.safetyStock - item.qty - item.onOrderPO - item.incomingTO;if (neededQty > 0) {console.log(`Replenishment needed for ${materialId} at ${locationId}. Need: ${neededQty}, Current: ${item.qty}, Safety: ${item.safetyStock}, Inbound: ${item.onOrderPO + item.incomingTO}`);// Check if already being replenishedconst isReplenishing = state.purchaseOrders.some(po => po.destinationId === locationId && po.materialId === materialId && po.status !== 'arrived' && po.status !== 'issue') ||state.transferOrders.some(to => to.destinationId === locationId && to.materialId === materialId && to.status !== 'arrived' && to.status !== 'issue');if (!isReplenishing) {console.log(`Triggering replenishment decision for ${materialId} at ${locationId}`);triggerReplenishment(locationId, materialId, neededQty + Math.floor(Math.random()*item.safetyStock)); // Order a bit more than needed} else {console.log(`Replenishment for ${materialId} at ${locationId} already in progress.`);}}}}
}function triggerReplenishment(locationId, materialId, quantity) {// Simple strategy: Prefer Transfer from Warehouse, otherwise Purchaselet transferSource = null;let maxSurplus = 0;// Check Warehouse firstconst warehouse = LOCATIONS.find(l => l.type === 'warehouse');if (warehouse && warehouse.id !== locationId) {const sourceInv = state.inventory[warehouse.id]?.[materialId];if (sourceInv && sourceInv.qty > sourceInv.safetyStock) {const availableSurplus = sourceInv.qty - sourceInv.safetyStock - sourceInv.incomingTO - sourceInv.onOrderPO; // Consider outbound from warehouse?if (availableSurplus >= quantity) {transferSource = warehouse.id;}// Could also check other bases for surplus, more complex logic}}if (transferSource) {// Create Transfer Orderconst transportTime = getTransportTime(transferSource, locationId);if (transportTime) {const eta = state.currentTimeHours + transportTime;const newTO = {id: `TO${String(state.kpis.totalTOs + state.transferOrders.filter(t=>t.status!='arrived').length + 1).padStart(4, '0')}`,sourceId: transferSource,destinationId: locationId,materialId: materialId,qty: quantity,startTime: state.currentTimeHours,originalEta: eta,currentEta: eta,status: 'in-transit', // Assume starts immediately for simplicityprogress: 0,};state.transferOrders.push(newTO);// Deduct from source, add to incomingstate.inventory[transferSource][materialId].qty -= quantity;state.inventory[locationId][materialId].incomingTO += quantity;console.log(`Created Transfer Order: ${newTO.id} (${quantity} ${materialId}) from ${transferSource} to ${locationId}`);} else {console.error(`Could not create TO: No valid transport time from ${transferSource} to ${locationId}`);createPurchaseOrder(locationId, materialId, quantity); // Fallback to PO}} else {// Create Purchase OrdercreatePurchaseOrder(locationId, materialId, quantity);}
}function createPurchaseOrder(locationId, materialId, quantity) {// Select a supplier (randomly for now)const supplier = LOCATIONS.find(l => l.type === 'supplier'); // Just pick the first oneif (!supplier) {console.error("Cannot create PO: No suppliers defined!");return;}const leadTime = PROCUREMENT_LEAD_TIMES[supplier.id];if (!leadTime) {console.error(`Cannot create PO: No lead time for supplier ${supplier.id}!`);return;}const eta = state.currentTimeHours + leadTime;const newPO = {id: `PO${String(state.kpis.totalPOs + state.purchaseOrders.filter(p=>p.status!='arrived').length + 1).padStart(4, '0')}`,supplierId: supplier.id,materialId: materialId,qty: quantity,destinationId: locationId,orderTime: state.currentTimeHours,originalEta: eta,currentEta: eta,status: 'ordered',progress: 0,};state.purchaseOrders.push(newPO);state.inventory[locationId][materialId].onOrderPO += quantity;console.log(`Created Purchase Order: ${newPO.id} (${quantity} ${materialId}) from ${supplier.id} to ${locationId}`);
}function updateKPIs() {// Fulfillment Ratestate.kpis.fulfillmentRate = state.kpis.totalDemand > 0 ? (state.kpis.fulfilledDemand / state.kpis.totalDemand) * 100 : 100;// On-Time Delivery (Transfers)state.kpis.onTimeDelivery = state.kpis.totalTOs > 0 ? (state.kpis.onTimeTOs / state.kpis.totalTOs) * 100 : 100;// On-Time Purchasesstate.kpis.onTimePurchase = state.kpis.totalPOs > 0 ? (state.kpis.onTimePOs / state.kpis.totalPOs) * 100 : 100;// Urgent Order Ratiostate.kpis.urgentOrderRatio = state.kpis.totalDemand > 0 ? (state.kpis.urgentOrders / state.kpis.totalDemand) * 100 : 0;// Average Inventory Level (Simple average quantity across relevant locations/materials)let totalQty = 0;let itemCount = 0;for (const locId in state.inventory) {if (LOCATIONS.find(l => l.id === locId && l.type !== 'supplier')) {for (const matId in state.inventory[locId]) {totalQty += state.inventory[locId][matId].qty;itemCount++;}}}state.kpis.avgInventoryLevel = itemCount > 0 ? totalQty / itemCount : 0;// Transport Cost is accumulated during TO arrival
}// =============================================================================
// UI Rendering
// =============================================================================function renderAllPanels() {try {renderSimulationTime();updateStatusDisplay();renderDemandPanel();renderInventoryPanel();renderSupplyPlanPanel();renderExecutionPanel();renderKPIPanel();} catch (error) {console.error("渲染所有面板时发生错误:", error);}
}function renderSimulationTime() {try {if (elements.simulationTime) {elements.simulationTime.textContent = `T+${state.currentTimeHours} H`;} else {console.warn("无法更新模拟时间显示: simulation-time 元素未找到");}} catch (error) {console.error("渲染模拟时间时出错:", error);}
}function updateStatusDisplay() {try {if (elements.simulationStatusText) {elements.simulationStatusText.textContent = state.isRunning ? '运行中' : '已停止';} else {console.warn("无法更新状态文本: simulation-status-text 元素未找到");}// 更新显示在头部的整体满足率const fulfillmentRate = state.kpis.fulfillmentRate;if (elements.overallFulfillment) {elements.overallFulfillment.innerHTML = `满足率: <strong>${fulfillmentRate.toFixed(1)}%</strong>`;elements.overallFulfillment.style.color = fulfillmentRate < 80 ? 'var(--error-color)' : (fulfillmentRate < 95 ? 'var(--warning-color)' : 'var(--success-color)');} else {console.warn("无法更新满足率显示: overall-fulfillment 元素未找到");}} catch (error) {console.error("更新状态显示时出错:", error);}
}function updateControlButtons() {try {if (elements.startBtn) {elements.startBtn.disabled = state.isRunning;}if (elements.stopBtn) {elements.stopBtn.disabled = !state.isRunning;}} catch (error) {console.error("更新控制按钮状态时出错:", error);}
}function renderDemandPanel() {try {const container = elements.demandForecast;if (!container) {console.warn("无法渲染需求面板: demand-forecast 元素未找到");return;}container.innerHTML = ''; // 清空const forecastHorizon = state.currentTimeHours + FORECAST_HORIZON_HOURS;const demandsToShow = state.demandForecast.filter(d => d.status === 'pending' && d.dueTime <= forecastHorizon).sort((a, b) => a.dueTime - b.dueTime);if (demandsToShow.length === 0) {container.innerHTML = '<p class="placeholder">未来24小时无预测需求</p>';return;}demandsToShow.forEach(d => {const item = document.createElement('div');item.className = 'demand-item';let urgencyClass = 'low';if (d.urgency === 'high') urgencyClass = 'high';else if (d.urgency === 'medium') urgencyClass = 'medium';item.innerHTML = `<div class="demand-details"><div class="demand-unit">${escapeHtml(d.unitName)} (基地: ${escapeHtml(d.baseId)})</div><div class="demand-material">${escapeHtml(d.materialName)} - Due: T+${d.dueTime}H</div></div><div class="demand-quantity">${d.qty}</div><div class="demand-urgency ${urgencyClass}">${d.urgency.toUpperCase()}</div>`;container.appendChild(item);});} catch (error) {console.error("渲染需求面板时出错:", error);}
}function renderInventoryPanel() {try {const container = elements.inventoryStatus;if (!container) {console.warn("无法渲染库存面板: inventory-status 元素未找到");return;}container.innerHTML = ''; // 清空let itemsRendered = 0;for (const locationId in state.inventory) {if (state.locationFilter !== 'all' && state.locationFilter !== locationId) {continue;}const location = LOCATIONS.find(l => l.id === locationId);if (!location) continue;// 可选:如果过滤为"全部",添加位置标题if (state.locationFilter === 'all') {const locHeader = document.createElement('h5');locHeader.textContent = location.name;locHeader.style.marginTop = itemsRendered > 0 ? '15px' : '0';locHeader.style.marginBottom = '5px';container.appendChild(locHeader);}const sortedMaterialIds = Object.keys(state.inventory[locationId]).sort();sortedMaterialIds.forEach(materialId => {const itemData = state.inventory[locationId][materialId];const material = MATERIALS.find(m => m.id === materialId);if (!material) return;const itemDiv = document.createElement('div');itemDiv.className = 'inventory-item';const percentage = itemData.safetyStock > 0 ? (itemData.qty / (itemData.safetyStock * 2)) * 100 : 100; // % of 2*safety stockconst safetyStockPercent = itemData.safetyStock > 0 ? 50 : 0; // Safety stock marker at 50% of the 2x barlet fillColor = 'var(--success-color)';let fillClass = 'success';if (itemData.qty < itemData.safetyStock * 0.5) {fillColor = 'var(--error-color)'; fillClass = 'danger';} else if (itemData.qty < itemData.safetyStock) {fillColor = 'var(--warning-color)'; fillClass = 'warning';}itemDiv.innerHTML = `<span class="inventory-material">${escapeHtml(material.name)}</span><span class="inventory-qty">${itemData.qty} ${escapeHtml(material.unit)}</span><span class="inventory-safe-stock">(${itemData.safetyStock})</span><div class="inventory-status-bar" title="库存: ${itemData.qty} / 安全库存: ${itemData.safetyStock}"><div class="inventory-status-fill ${fillClass}" style="width: ${Math.min(100, percentage)}%; background-color: ${fillColor};"></div><div class="inventory-safe-marker" style="left: ${safetyStockPercent}%;" title="安全库存线"></div></div>`;container.appendChild(itemDiv);itemsRendered++;});};if (itemsRendered === 0 && state.locationFilter !== 'all') {container.innerHTML = `<p class="placeholder">基地 ${escapeHtml(state.locationFilter)} 无库存数据</p>`;} else if (itemsRendered === 0) {container.innerHTML = `<p class="placeholder">无库存数据</p>`;}} catch (error) {console.error("渲染库存面板时出错:", error);}
}function renderSupplyPlanPanel() {try {const procContainer = elements.procurementPlan;const transContainer = elements.transferPlan;if (!procContainer) {console.warn("无法渲染采购计划: procurement-plan 元素未找到");return;}if (!transContainer) {console.warn("无法渲染调拨计划: transfer-plan 元素未找到");return;}procContainer.innerHTML = '';transContainer.innerHTML = '';let procCount = 0;let transCount = 0;// 待处理采购订单 (Status: ordered)state.purchaseOrders.filter(po => po.status === 'ordered').sort((a,b)=>a.originalEta - b.originalEta).forEach(po => {const material = MATERIALS.find(m => m.id === po.materialId);const supplier = LOCATIONS.find(l => l.id === po.supplierId);const itemDiv = document.createElement('div');itemDiv.className = 'procurement-item';itemDiv.innerHTML = `<div class="item-details"><span class="item-id">${escapeHtml(po.id)}</span><span class="item-material">${escapeHtml(material?.name || po.materialId)} from ${escapeHtml(supplier?.name || po.supplierId)}</span></div><span class="item-qty">${po.qty}</span><span class="item-eta">ETA: T+${po.originalEta}H</span>`;procContainer.appendChild(itemDiv);procCount++;});// 待处理调拨订单state.transferOrders.filter(to => to.status === 'pending' || (to.status === 'in-transit' && to.progress < 1)).sort((a,b)=>a.originalEta - b.originalEta).forEach(to => {const material = MATERIALS.find(m => m.id === to.materialId);const source = LOCATIONS.find(l => l.id === to.sourceId);const dest = LOCATIONS.find(l => l.id === to.destinationId);const itemDiv = document.createElement('div');itemDiv.className = 'transfer-item';itemDiv.innerHTML = `<div class="item-details"><span class="item-id">${escapeHtml(to.id)}</span><span class="item-material">${escapeHtml(material?.name || to.materialId)}</span><span class="item-info">从 ${escapeHtml(source?.name)}${escapeHtml(dest?.name)}</span></div><span class="item-qty">${to.qty}</span><span class="item-eta">ETA: T+${to.originalEta}H</span>`;transContainer.appendChild(itemDiv);transCount++;});if (procCount === 0) procContainer.innerHTML = '<p class="placeholder">无待处理采购</p>';if (transCount === 0) transContainer.innerHTML = '<p class="placeholder">无计划中调拨</p>';} catch (error) {console.error("渲染供应计划面板时出错:", error);}
}function renderExecutionPanel() {try {const container = elements.executionMonitor;if (!container) {console.warn("无法渲染执行监控面板: execution-monitor 元素未找到");return;}container.innerHTML = '';let taskCount = 0;const activeTasks = [...state.purchaseOrders.filter(po => ['in-transit', 'delayed'].includes(po.status)),...state.transferOrders.filter(to => ['in-transit', 'delayed'].includes(to.status))].sort((a, b) => a.currentEta - b.currentEta);if (activeTasks.length === 0) {container.innerHTML = '<p class="placeholder">无在途任务</p>';return;}activeTasks.forEach(task => {const itemDiv = document.createElement('div');itemDiv.className = 'execution-item';const material = MATERIALS.find(m => m.id === task.materialId);const isPO = !!task.supplierId;const destination = LOCATIONS.find(l => l.id === task.destinationId);const source = LOCATIONS.find(l => l.id === (isPO ? task.supplierId : task.sourceId));let statusClass = task.status.replace('-', ''); // e.g., in-transit -> intransitlet statusText = task.status;if (task.status === 'in-transit') statusText = '运输中';else if (task.status === 'delayed') statusText = '已延误';itemDiv.innerHTML = `<div class="execution-header"><span class="execution-id">${escapeHtml(task.id)} (${isPO ? '采购' : '调拨'})</span><span class="execution-status ${statusClass}">${statusText}</span></div><div class="execution-details"><div><span class="execution-label">物资:</span> <span class="execution-value">${escapeHtml(material?.name || task.materialId)} (${task.qty})</span></div><div><span class="execution-label">从:</span> <span class="execution-value">${escapeHtml(source?.name || (isPO ? task.supplierId : '?'))}</span></div><div><span class="execution-label">到:</span> <span class="execution-value">${escapeHtml(destination?.name || task.destinationId)}</span></div><div><span class="execution-label">当前ETA:</span> <span class="execution-value">T+${task.currentEta}H ${task.currentEta > task.originalEta ? '(预计延误)': ''}</span></div></div><div class="execution-progress-bar" title="进度: ${task.progress.toFixed(0)}%"><div class="execution-progress-fill ${statusClass}" style="width: ${task.progress.toFixed(0)}%;"></div></div>`;container.appendChild(itemDiv);taskCount++;});if (taskCount === 0) { // 如果activeTasks > 0应该不会发生,但安全检查container.innerHTML = '<p class="placeholder">无在途任务</p>';}} catch (error) {console.error("渲染执行监控面板时出错:", error);}
}function renderKPIPanel() {try {// 为每个KPI元素添加防御性检查if (elements.kpiFulfillment) {elements.kpiFulfillment.textContent = `${state.kpis.fulfillmentRate.toFixed(1)}%`;colorizeKPI(elements.kpiFulfillment, state.kpis.fulfillmentRate, 95, 80);}if (elements.kpiAvgInventory) {elements.kpiAvgInventory.textContent = state.kpis.avgInventoryLevel.toFixed(0);}if (elements.kpiOnTimeDelivery) {elements.kpiOnTimeDelivery.textContent = `${state.kpis.onTimeDelivery.toFixed(1)}%`;colorizeKPI(elements.kpiOnTimeDelivery, state.kpis.onTimeDelivery, 95, 85);}if (elements.kpiPurchaseOntime) {elements.kpiPurchaseOntime.textContent = `${state.kpis.onTimePurchase.toFixed(1)}%`;colorizeKPI(elements.kpiPurchaseOntime, state.kpis.onTimePurchase, 90, 80);}if (elements.kpiUrgentOrders) {elements.kpiUrgentOrders.textContent = `${state.kpis.urgentOrderRatio.toFixed(1)}%`;colorizeKPI(elements.kpiUrgentOrders, 100 - state.kpis.urgentOrderRatio, 95, 85); // 越低越好}if (elements.kpiTransportCost) {elements.kpiTransportCost.textContent = `$${state.kpis.simulatedTransportCost.toFixed(0)}`; // 示例货币}} catch (error) {console.error("渲染KPI面板时出错:", error);}
}function colorizeKPI(element, value, goodThreshold, warnThreshold) {if (!element) return; // 防御性检查try {if (value >= goodThreshold) {element.style.color = 'var(--success-color)';} else if (value >= warnThreshold) {element.style.color = 'var(--warning-color)';} else {element.style.color = 'var(--error-color)';}} catch (error) {console.error("设置KPI颜色时出错:", error);}
}// =============================================================================
// Utility Functions
// =============================================================================function getTransportTime(sourceId, destinationId) {return TRANSPORT_TIMES[`${sourceId}-${destinationId}`] || TRANSPORT_TIMES[`${destinationId}-${sourceId}`] || null;
}function escapeHtml(input) {if (input === null || input === undefined) return '';const str = String(input);const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };return str.replace(/[&<>"']/g, (match) => map[match]);
}// =============================================================================
// Appsmith特定初始化
// =============================================================================// 增加尝试次数和延迟,更好地处理Appsmith的DOM加载
let initializationAttempts = 0;
const MAX_INITIALIZATION_ATTEMPTS = 10;
const INITIALIZATION_RETRY_DELAY = 500; // 毫秒function attemptInitialization() {console.log(`尝试初始化应用 (尝试 ${initializationAttempts + 1}/${MAX_INITIALIZATION_ATTEMPTS})`);// 首先检查DOM是否已经加载if (document.readyState === 'loading') {console.log("文档仍在加载中,等待DOMContentLoaded事件");document.addEventListener('DOMContentLoaded', () => {console.log("DOMContentLoaded事件触发,准备初始化");setTimeout(attemptInitialization, 100);});return;}// 尝试查找应用容器const appContainer = document.querySelector('.app-container');if (!appContainer) {initializationAttempts++;if (initializationAttempts < MAX_INITIALIZATION_ATTEMPTS) {console.log(`未找到应用容器,${INITIALIZATION_RETRY_DELAY}毫秒后重试...`);setTimeout(attemptInitialization, INITIALIZATION_RETRY_DELAY);} else {console.error(`${MAX_INITIALIZATION_ATTEMPTS}次尝试后仍未找到应用容器,放弃初始化`);// 尝试使用替代DOM结构或框架特定的初始化策略tryAlternativeInitialization();}return;}// 找到容器后,开始初始化console.log("找到应用容器,开始初始化应用");initializeApp();
}function tryAlternativeInitialization() {console.log("尝试替代初始化方法...");// 尝试方法1: 查找任何可能包含我们组件的容器const possibleContainers = [document.body,document.querySelector('#root'),document.querySelector('[data-cy="canvas-viewport"]'),document.querySelector('.canvas-viewport'),document.querySelector('.appsmith-component'),document.querySelector('.custom-component-container')].filter(container => container !== null);if (possibleContainers.length > 0) {console.log(`找到${possibleContainers.length}个可能的容器,尝试初始化`);// 使用第一个找到的容器const container = possibleContainers[0];console.log(`使用容器:`, container);// 尝试在容器中查找或创建应用结构try {// 如果容器为空或者不包含我们需要的结构,动态创建if (container.children.length === 0 || !container.querySelector('#start-btn')) {console.log("容器不包含所需结构,尝试动态创建UI元素");createAppStructure(container);} else {console.log("容器已包含UI元素,尝试初始化");initializeApp();}} catch (error) {console.error("替代初始化失败:", error);showErrorMessage();}} else {console.error("找不到任何可用容器,无法初始化应用");showErrorMessage();}
}function createAppStructure(container) {// 这个函数将动态创建应用需要的DOM结构console.log("动态创建应用DOM结构");// 根据index.html的结构创建简化版本的DOMcontainer.innerHTML = `<div class="app-container"><div class="app-message"><h3>军方后勤供应链模拟</h3><p>应用加载中... 如果长时间未加载,请刷新页面或检查控制台错误。</p></div></div>`;// 延迟一会儿再初始化,给DOM留出渲染时间setTimeout(() => {console.log("动态创建的DOM结构已就绪,尝试初始化");initializeApp();}, 200);
}function showErrorMessage() {// 显示错误消息给用户try {const body = document.body;if (body) {const errorDiv = document.createElement('div');errorDiv.style.cssText = `position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);background-color: #f8d7da;color: #721c24;padding: 20px;border-radius: 5px;box-shadow: 0 0 10px rgba(0,0,0,0.2);max-width: 80%;text-align: center;z-index: 9999;`;errorDiv.innerHTML = `<h3>应用初始化失败</h3><p>在Appsmith环境中未能正确初始化军方后勤供应链模拟组件。</p><p>请查看浏览器控制台获取详细错误信息。</p><button onclick="location.reload()" style="margin-top:10px; padding:8px 16px; background:#0d6efd; color:white; border:none; border-radius:4px; cursor:pointer;">刷新页面</button>`;body.appendChild(errorDiv);}} catch (error) {console.error("无法显示错误消息:", error);}
}// 开始初始化过程
window.addEventListener('load', () => {console.log("Window load事件触发,准备初始化应用");setTimeout(attemptInitialization, 100);
});// 如果window.load已经触发过,直接开始尝试初始化
if (document.readyState === 'complete') {console.log("文档已完全加载,立即开始初始化尝试");setTimeout(attemptInitialization, 100);
} 
http://www.xdnf.cn/news/1340101.html

相关文章:

  • 34、扩展仓储管理系统 (跨境汽车零部件模拟) - /物流与仓储组件/extended-warehouse-management
  • 3D 环形旋转图片轮播(纯html,css,js)
  • 力扣hot100:无重复字符的最长子串,找到字符串中所有字母异位词(滑动窗口算法讲解)(3,438)
  • 从零开始理解 K 均值聚类:原理、实现与应用
  • 自学嵌入式第二十四天:数据结构(4)-栈
  • linux-ubuntu里docker的容器portainer容器建立后如何打开?
  • WSL的Ubuntu如何改名字
  • Ubuntu网络图标消失/以太网卡显示“未托管“
  • java项目:如何优化JVM参数?
  • nginx-自制证书实现
  • 读《精益数据分析》:精益画布——创业与产品创新的高效工具
  • 【工具】前端JS/VUE修改图片分辨率
  • 使用Docker部署Coze Studio开源版
  • Advanced Math Math Analysis |02 Limits
  • Oracle CLOB类型转换
  • k8s下的网络通信与认证
  • 【C++】模板(进阶)
  • 从YOLOv5到RKNN:零冲突转换YOLOv5模型至RK3588 NPU全指南
  • 在线课程|基于SprinBoot+vue的在线课程管理系统(源码+数据库+文档)
  • openEuler系统中如何将docker安装在指定目录
  • ES_文档
  • 【数据结构】树与二叉树:结构、性质与存储
  • 牛客:链表的回文结构详解
  • 牛客:链表分割算法详解
  • LeetCode100 -- Day3
  • C++---滑动窗口平滑数据
  • 深度学习之NLP基础
  • KB5063878补丁故障解决方案:从蓝屏幕到系统修复的全面指南
  • 短波红外科研相机:开启科研新视野的利器​
  • 【矩池云】实现Pycharm远程连接,上传数据并解压缩