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

37、需求预测与库存优化 (快消品) - /供应链管理组件/fmcg-inventory-optimization

76个工业组件库示例汇总

需求预测与库存优化 (快消品) - 自定义组件

概述

这是一个用于模拟和可视化快消品 (FMCG) 需求预测与库存优化流程的组件。用户可以选择不同的产品和区域,调整预测参数和库存策略,然后运行模拟以查看历史销售、预测需求以及基于目标安全库存的优化建议。

组件旨在帮助理解季节性对销售的影响、需求预测的基本方法以及如何根据预测和库存策略来指导补货决策。

功能特性

  • 参数化模拟:
    • 选择不同的模拟产品(如饮料、零食、洗护)和销售区域。
    • 调整预测期长度(未来预测多少周)。
    • 调整目标安全库存覆盖天数。
    • 调整用于预测的历史数据长度。
  • 数据生成与模拟:
    • 模拟生成带有明显季节性特征和随机波动的历史周销售数据。
    • 基于历史数据和季节性指数,使用简化的季节性移动平均法生成未来需求预测。
    • 模拟当前的库存水平(基于近期销售表现随机生成)。
  • 库存优化:
    • 根据预测期的平均需求和设定的安全库存天数,计算目标库存水平。
    • 比较当前库存与目标库存,生成明确的"增加库存"、"减少库存"或"库存平衡"的建议及数量。
  • 数据可视化:
    • 使用 Chart.js 动态绘制折线图,清晰展示历史销售曲线和预测需求曲线。
    • 在专门的面板中展示关键信息:当前库存、目标库存和优化建议。
    • 使用条形图直观表示当前库存相对于目标库存的水平。
  • 交互式体验:
    • 通过滑块和下拉菜单轻松调整模拟参数。
    • 参数变化实时反馈到界面。
    • 点击按钮触发预测和优化计算。
    • 清晰的状态指示(未运行、运行中、已完成)。
  • Appsmith 兼容性:
    • 采用健壮的初始化逻辑(多次尝试、延迟)以适应 Appsmith 自定义组件的加载环境。
    • 使用 container.querySelector 进行 DOM 元素查找。
    • 全面的错误处理和用户友好的错误消息提示。

文件结构

/供应链管理组件
└── /fmcg-inventory-optimization├── index.html         # 组件的 HTML 结构├── styles.css         # 组件的 CSS 样式 (苹果科技工业风格)├── script.js          # 组件的核心 JavaScript 逻辑└── README.md          # 本说明文件

使用方法

  1. index.html 的内容复制到 HTML 编辑器中。
  2. styles.css 的内容复制到 CSS 编辑器中。
  3. script.js 的内容复制到 JavaScript 编辑器中。
  4. 调整组件大小以适应内容展示,建议宽度较宽,高度适中(例如 600px)。

交互流程:

  1. 通过配置面板选择产品、区域,并使用滑块调整预测期、安全库存天数和历史数据长度。
  2. 点击"运行预测与优化"按钮。
  3. 组件状态将变为"运行中",然后计算完成变为"已完成"。
  4. 图表区域将显示历史销售和预测需求曲线。
  5. 库存状态面板将显示当前模拟库存、计算出的目标库存以及相应的增加/减少/平衡建议。

技术栈

  • HTML5
  • CSS3 (Flexbox, Grid)
  • JavaScript (ES6+)
  • Chart.js (用于图表绘制)
  • Chart.js Date Adapter (date-fns)
  • Font Awesome (用于图标)

模拟逻辑说明

  • 历史销售: BaseSales * SeasonalityIndex * (1 + RandomNoise)
  • 需求预测: 基于过去 N 周(例如 4 周)的去季节性销量估算基础销量,然后乘以未来周期的季节性指数。
  • 当前库存: 基于最近几周的平均销量乘以一个随机系数(0.5 到 2.5 之间)来模拟。
  • 目标库存: (未来 N 天的平均预测日销量) * 安全库存天数,其中 N 等于安全库存天数。
  • 优化建议: 基于当前库存与目标库存的差异百分比(阈值设为 +/- 10%)决定。

注意事项与限制

  • 这是一个 模拟 组件,所有数据(历史销售、当前库存)都是动态生成的,并非真实数据。
  • 预测模型(季节性移动平均)相对 简化,仅用于演示目的,实际应用中需要更复杂的模型(如 ARIMA, Prophet 等)。
  • 库存优化逻辑基于单一产品的安全库存策略,未考虑多产品关联、供应链延迟、最小订单量等复杂因素。
  • 组件的性能可能受历史数据量和预测期长度影响。

未来可扩展方向

  • 集成更高级的预测模型。
  • 支持多产品、多区域同时分析和优化。
  • 考虑补货提前期、运输成本等因素进行更复杂的库存策略模拟。
  • 允许用户上传或连接外部数据源。
  • 添加更详细的 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"><!-- Chart.js for visualization --><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script><!-- 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 --><header class="app-header"><div class="header-title"><i class="fas fa-chart-pie header-icon"></i><h1>需求预测与库存优化 (快消品)</h1></div><div class="simulation-controls"><button id="run-simulation-btn" class="btn btn-primary"><i class="fas fa-cogs"></i> 运行预测与优化</button><span id="simulation-status" class="status-badge">状态: 未运行</span></div></header><!-- Main Content Area --><main class="app-content"><!-- Configuration Panel --><section class="panel config-panel"><div class="panel-header"><h2><i class="fas fa-sliders-h"></i> 参数配置</h2></div><div class="panel-content"><div class="config-group"><label for="product-select">产品选择:</label><select id="product-select"><option value="P01">畅销饮料 A</option><option value="P02">热门零食 B</option><option value="P03">日用洗护 C</option></select></div><div class="config-group"><label for="region-select">区域选择:</label><select id="region-select"><option value="R01">华东区</option><option value="R02">华南区</option><option value="R03">华北区</option><option value="R04">西南区</option></select></div><div class="config-group"><label for="forecast-horizon-slider">预测期 (): <span id="forecast-horizon-value">12</span></label><input type="range" id="forecast-horizon-slider" min="4" max="24" value="12" step="1"></div><div class="config-group"><label for="safety-stock-days-slider">安全库存天数: <span id="safety-stock-days-value">14</span></label><input type="range" id="safety-stock-days-slider" min="7" max="30" value="14" step="1"></div><div class="config-group"><label for="historical-data-weeks-slider">历史数据 (): <span id="historical-data-weeks-value">52</span></label><input type="range" id="historical-data-weeks-slider" min="26" max="104" value="52" step="1"></div></div></section><!-- History & Forecast Panel --><section class="panel chart-panel"><div class="panel-header"><h2><i class="fas fa-chart-line"></i> 历史销售与需求预测</h2><div class="chart-legend"><span class="legend-item history"><i class="fas fa-circle"></i> 历史销量</span><span class="legend-item forecast"><i class="fas fa-circle"></i> 预测需求</span></div></div><div class="panel-content chart-container"><canvas id="sales-forecast-chart"></canvas><p id="chart-placeholder" class="placeholder">请点击"运行预测与优化"生成图表。</p></div></section><!-- Inventory & Optimization Panel --><section class="panel inventory-panel"><div class="panel-header"><h2><i class="fas fa-boxes"></i> 库存状态与优化建议</h2></div><div class="panel-content scrollable"><div id="inventory-details"><p class="placeholder">请选择产品和区域,并运行模拟。</p><!-- Inventory details and optimization suggestions will be loaded here --><!-- Example Structure:<div class="inventory-item"><h4>华东区 - 畅销饮料 A</h4><p>当前库存: <span class="current-stock">1500</span> | 目标库存: <span class="target-stock">2500</span></p><p class="optimization-suggestion increase">建议增加库存: <span class="suggestion-amount">1000</span></p><div class="inventory-bar"><div class="bar-current" style="width: 60%;"></div><div class="bar-target" style="left: 100%;"></div></div></div>--></div></div></section></main></div><script src="script.js"></script>
</body>
</html> 

styles.css

:root {--primary-color: #007aff; /* Apple Blue */--secondary-color: #5856d6; /* Apple Purple */--background-color: #f2f2f7; /* Apple Light Gray */--panel-background-color: #ffffff;--text-color: #1c1c1e; /* Near Black */--text-color-secondary: #8e8e93; /* Gray */--border-color: #d1d1d6;--success-color: #34c759; /* Apple Green */--warning-color: #ff9500; /* Apple Orange */--error-color: #ff3b30; /* Apple Red */--history-color: #007aff;--forecast-color: #ff9500;--inventory-current-color: #34c759;--inventory-target-color: #ff3b30;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--panel-padding: 15px;--panel-header-height: 45px;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);background-color: var(--background-color);color: var(--text-color);line-height: 1.5;font-size: 14px;overflow-x: hidden; /* Prevent horizontal scroll */
}.app-container {display: flex;flex-direction: column;height: 100vh; /* Fallback for older browsers */height: 100dvh; /* Dynamic viewport height */max-height: 800px; /* Limit height as requested */overflow: hidden; /* Prevent whole container scroll */background-color: var(--panel-background-color);border-radius: var(--border-radius);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);margin: 10px; /* Add some margin around the container */
}/* Header */
.app-header {display: flex;justify-content: space-between;align-items: center;padding: 10px var(--panel-padding);border-bottom: 1px solid var(--border-color);background-color: #f8f8f8;border-top-left-radius: var(--border-radius);border-top-right-radius: var(--border-radius);
}.header-title {display: flex;align-items: center;
}.header-icon {font-size: 1.5em;color: var(--primary-color);margin-right: 10px;
}.app-header h1 {font-size: 1.2em;font-weight: 600;color: var(--text-color);
}.simulation-controls {display: flex;align-items: center;gap: 15px;
}/* Buttons */
.btn {padding: 8px 15px;border: none;border-radius: 6px;cursor: pointer;font-size: 0.9em;font-weight: 500;transition: background-color 0.2s ease, transform 0.1s ease;display: inline-flex;align-items: center;gap: 5px;
}.btn-primary {background-color: var(--primary-color);color: #fff;
}.btn-primary:hover {background-color: #005ec4;
}.btn:active {transform: scale(0.98);
}.status-badge {font-size: 0.85em;padding: 4px 8px;border-radius: 4px;background-color: var(--text-color-secondary);color: #fff;
}/* Main Content Area */
.app-content {display: grid;grid-template-columns: 2fr 3fr; /* Adjust ratio as needed */grid-template-rows: auto 1fr; /* Config row, then charts/inventory */grid-template-areas:"config chart""inventory chart";gap: var(--panel-padding);padding: var(--panel-padding);flex-grow: 1;overflow: hidden; /* Prevent content scroll */
}/* Panels */
.panel {background-color: var(--panel-background-color);border: 1px solid var(--border-color);border-radius: var(--border-radius);display: flex;flex-direction: column;overflow: hidden; /* Important for scrollable content */
}.panel-header {display: flex;justify-content: space-between;align-items: center;padding: 10px var(--panel-padding);border-bottom: 1px solid var(--border-color);background-color: #f8f8f8;height: var(--panel-header-height);flex-shrink: 0;
}.panel-header h2 {font-size: 1em;font-weight: 600;display: flex;align-items: center;gap: 8px;
}.panel-header h2 i {color: var(--text-color-secondary);
}.panel-content {padding: var(--panel-padding);flex-grow: 1;overflow-y: auto; /* Allow vertical scroll within content */
}.scrollable {overflow-y: auto;
}/* Specific Panel Assignments */
.config-panel {grid-area: config;
}.inventory-panel {grid-area: inventory;
}.chart-panel {grid-area: chart;
}/* Configuration Styles */
.config-group {margin-bottom: 15px;
}.config-group label {display: block;font-size: 0.9em;font-weight: 500;margin-bottom: 5px;color: var(--text-color-secondary);
}.config-group select,
.config-group input[type="range"] {width: 100%;padding: 8px;border: 1px solid var(--border-color);border-radius: 6px;font-size: 0.9em;background-color: #fff;
}.config-group input[type="range"] {padding: 0;height: 5px;cursor: pointer;accent-color: var(--primary-color);
}.config-group label span {font-weight: bold;color: var(--text-color);
}/* Chart Styles */
.chart-container {position: relative; /* Needed for placeholder */height: 100%; /* Ensure canvas can take height */min-height: 200px; /* Prevent collapsing */
}#sales-forecast-chart {display: block;width: 100% !important;height: 100% !important;
}.chart-legend {display: flex;gap: 15px;font-size: 0.8em;align-items: center;
}.legend-item i {margin-right: 4px;font-size: 0.8em;
}
.legend-item.history i { color: var(--history-color); }
.legend-item.forecast i { color: var(--forecast-color); }/* Inventory Styles */
#inventory-details .placeholder {text-align: center;color: var(--text-color-secondary);padding-top: 20px;
}.inventory-item {border: 1px solid var(--border-color);border-radius: var(--border-radius);padding: 15px;margin-bottom: 15px;background-color: #f9f9f9;
}.inventory-item h4 {font-size: 1.1em;font-weight: 600;margin-bottom: 10px;
}.inventory-item p {font-size: 0.9em;margin-bottom: 5px;color: var(--text-color-secondary);
}.inventory-item .current-stock,
.inventory-item .target-stock,
.inventory-item .suggestion-amount {font-weight: bold;color: var(--text-color);
}.optimization-suggestion.increase {color: var(--success-color);font-weight: 500;
}.optimization-suggestion.decrease {color: var(--error-color);font-weight: 500;
}.optimization-suggestion.balanced {color: var(--text-color-secondary);font-weight: 500;
}.inventory-bar {height: 8px;background-color: var(--background-color);border-radius: 4px;margin-top: 10px;position: relative;overflow: hidden;
}.inventory-bar .bar-current {position: absolute;left: 0;top: 0;height: 100%;background-color: var(--inventory-current-color);border-radius: 4px;transition: width 0.5s ease;
}.inventory-bar .bar-target {position: absolute;top: -2px; /* Slightly offset to be visible */bottom: -2px;width: 2px;background-color: var(--inventory-target-color);transition: left 0.5s ease;
}/* Placeholder */
.placeholder {text-align: center;color: var(--text-color-secondary);font-style: italic;padding: 20px;
}/* Responsive Adjustments */
@media (max-width: 768px) {.app-content {grid-template-columns: 1fr; /* Stack panels */grid-template-rows: auto auto auto; /* One row per panel type */grid-template-areas:"config""chart""inventory";overflow-y: auto; /* Allow scrolling on mobile */}.app-container {max-height: none; /* Allow full height on mobile */margin: 0;border-radius: 0;}.panel-content {min-height: 150px; /* Give panels some min height */}.chart-container {min-height: 250px; /* Make chart taller on mobile */}.app-header h1 {font-size: 1em;}
} 

script.js

/* =============================================================================FMCG Demand Forecasting & Inventory Optimization - Script============================================================================= */// --- Configuration & Constants ---
const WEEKS_PER_YEAR = 52;
const DAYS_PER_WEEK = 7;// --- Simulation Data (Product & Region Specific Base Sales & Seasonality) ---
const BASE_SALES = {P01: { R01: 1000, R02: 1200, R03: 800, R04: 600 },P02: { R01: 500, R02: 400, R03: 600, R04: 300 },P03: { R01: 700, R02: 800, R03: 500, R04: 450 },
};
// Simplified weekly seasonality index (0-51 weeks), peak around week 20-30
const SEASONALITY_INDEX = Array.from({ length: WEEKS_PER_YEAR }, (_, i) => {// Simple sine wave + noise for seasonalityconst peakWeek = 26;const troughWeek = 0;const amplitude = 0.6; // Peak is 1 + 0.6 = 1.6 times baseconst phaseShift = -Math.PI / 2; // Start near troughconst index = 1 + amplitude * Math.sin((i / WEEKS_PER_YEAR) * 2 * Math.PI + phaseShift);return Math.max(0.4, index + (Math.random() - 0.5) * 0.2); // Add noise, ensure min 0.4
});// --- Global State ---
let state = {selectedProduct: 'P01',selectedRegion: 'R01',forecastHorizonWeeks: 12,safetyStockDays: 14,historicalDataWeeks: 52,isRunning: false,historicalSales: [], // [{ date: Date, sales: number }]forecast: [],        // [{ date: Date, forecast: number }]currentInventory: 0,targetInventory: 0,optimizationSuggestion: { type: 'none', amount: 0 },chartInstance: null,
};// --- DOM Elements Cache ---
const elements = {};// =============================================================================
// Initialization (Appsmith Optimized)
// =============================================================================let initializationAttempts = 0;
const MAX_INITIALIZATION_ATTEMPTS = 10;
const INITIALIZATION_RETRY_DELAY = 500;function attemptInitialization() {console.log(`Attempting to initialize FMCG app (Attempt ${initializationAttempts + 1}/${MAX_INITIALIZATION_ATTEMPTS})`);if (document.readyState === 'loading') {console.log("Document still loading, waiting for DOMContentLoaded.");document.addEventListener('DOMContentLoaded', () => setTimeout(attemptInitialization, 100));return;}const appContainer = document.querySelector('.app-container');if (!appContainer) {initializationAttempts++;if (initializationAttempts < MAX_INITIALIZATION_ATTEMPTS) {console.log(`App container not found, retrying in ${INITIALIZATION_RETRY_DELAY}ms...`);setTimeout(attemptInitialization, INITIALIZATION_RETRY_DELAY);} else {console.error(`Failed to find app container after ${MAX_INITIALIZATION_ATTEMPTS} attempts.`);showErrorMessage("应用容器加载失败,请刷新重试。");}return;}console.log("App container found, proceeding with initialization.");initializeApp(appContainer);
}function initializeApp(container) {console.log("Initializing FMCG App...");try {if (!queryDOMElements(container)) {console.error("Initialization failed: Missing essential DOM elements.");showErrorMessage("界面元素加载不完整,请刷新重试。");return;}setupEventListeners();updateConfigDisplay(); // Update display based on initial staterenderPlaceholder(); // Show initial placeholder messageconsole.log("FMCG App Initialized.");} catch (error) {console.error("Error during app initialization:", error);showErrorMessage(`初始化出错: ${error.message}`);}
}function queryDOMElements(container) {console.log("Querying DOM elements...");const ids = ['run-simulation-btn','simulation-status','product-select','region-select','forecast-horizon-slider','forecast-horizon-value','safety-stock-days-slider','safety-stock-days-value','historical-data-weeks-slider','historical-data-weeks-value','sales-forecast-chart','chart-placeholder','inventory-details'];let allFound = true;elements.container = container; // Store container referenceids.forEach(id => {// Try finding by ID first, then fallback to more specific selectors if neededelements[id] = container.querySelector(`#${id}`);if (!elements[id]) {console.warn(`DOM element not found: #${id}`);// Attempt fallback selectors for specific elements if necessaryif (id === 'sales-forecast-chart') elements[id] = container.querySelector('canvas');// Add more fallbacks if neededif (!elements[id]) {allFound = false;}}});if (!allFound) {console.error("关键 DOM 元素缺失,请检查 HTML 结构。 Missing elements logged above.");return false;}console.log("DOM elements queried successfully.");return true;
}function setupEventListeners() {console.log("Setting up event listeners...");try {if (elements['run-simulation-btn']) {elements['run-simulation-btn'].addEventListener('click', handleRunSimulation);} else console.error("run-simulation-btn not found for event listener");// Config slidersif (elements['forecast-horizon-slider']) {elements['forecast-horizon-slider'].addEventListener('input', handleConfigChange);} else console.error("forecast-horizon-slider not found");if (elements['safety-stock-days-slider']) {elements['safety-stock-days-slider'].addEventListener('input', handleConfigChange);} else console.error("safety-stock-days-slider not found");if (elements['historical-data-weeks-slider']) {elements['historical-data-weeks-slider'].addEventListener('input', handleConfigChange);} else console.error("historical-data-weeks-slider not found");// Config selectsif (elements['product-select']) {elements['product-select'].addEventListener('change', handleConfigChange);} else console.error("product-select not found");if (elements['region-select']) {elements['region-select'].addEventListener('change', handleConfigChange);} else console.error("region-select not found");console.log("Event listeners set up.");} catch (error) {console.error("Error setting up event listeners:", error);}
}// =============================================================================
// Event Handlers
// =============================================================================function handleConfigChange(event) {console.log("Configuration changed:", event.target.id);try {// Update state based on which element triggered the changeswitch (event.target.id) {case 'product-select':state.selectedProduct = event.target.value;break;case 'region-select':state.selectedRegion = event.target.value;break;case 'forecast-horizon-slider':state.forecastHorizonWeeks = parseInt(event.target.value);break;case 'safety-stock-days-slider':state.safetyStockDays = parseInt(event.target.value);break;case 'historical-data-weeks-slider':state.historicalDataWeeks = parseInt(event.target.value);break;}updateConfigDisplay();// Optionally, clear results or re-run simulation automatically?// For now, require manual click on "Run"renderPlaceholder(); // Clear results on config change} catch (error) {console.error("Error handling config change:", error);}
}function handleRunSimulation() {console.log("Run Simulation button clicked.");if (state.isRunning) {console.log("Simulation already running.");return;}state.isRunning = true;updateStatus('运行中...');disableControls(true);// Simulate the process asynchronouslysetTimeout(() => {try {console.log("Starting simulation logic...");// 1. Generate historical datastate.historicalSales = generateHistoricalSales(state.selectedProduct,state.selectedRegion,state.historicalDataWeeks);console.log(`Generated ${state.historicalSales.length} weeks of historical data.`);// 2. Generate forecaststate.forecast = generateDemandForecast(state.historicalSales,state.forecastHorizonWeeks);console.log(`Generated ${state.forecast.length} weeks of forecast data.`);// 3. Simulate current inventory (e.g., based on last few weeks sales)state.currentInventory = simulateCurrentInventory(state.historicalSales);console.log(`Simulated current inventory: ${state.currentInventory}`);// 4. Calculate target inventory & optimizationconst optimizationResult = calculateInventoryOptimization(state.forecast,state.safetyStockDays,state.currentInventory);state.targetInventory = optimizationResult.target;state.optimizationSuggestion = optimizationResult.suggestion;console.log(`Calculated target inventory: ${state.targetInventory}, Suggestion: ${optimizationResult.suggestion.type} ${optimizationResult.suggestion.amount}`);// 5. Render resultsrenderResults();console.log("Simulation and rendering complete.");} catch (error) {console.error("Error during simulation run:", error);showErrorMessage(`模拟运行时出错: ${error.message}`);renderPlaceholder(); // Show placeholder on error} finally {state.isRunning = false;updateStatus('已完成');disableControls(false);}}, 50); // Small delay to allow UI update for status
}// =============================================================================
// Simulation Logic
// =============================================================================function generateHistoricalSales(productId, regionId, weeks) {const salesData = [];const base = BASE_SALES[productId]?.[regionId] || 500; // Default base salesconst today = new Date();today.setHours(0, 0, 0, 0);for (let i = weeks; i > 0; i--) {const date = new Date(today);date.setDate(today.getDate() - i * DAYS_PER_WEEK);const weekOfYear = getWeekOfYear(date);const seasonality = SEASONALITY_INDEX[weekOfYear];// Add random noiseconst noise = 1 + (Math.random() - 0.5) * 0.3; // +/- 15% noiseconst sales = Math.max(0, Math.round(base * seasonality * noise));salesData.push({ date, sales });}return salesData;
}function generateDemandForecast(historicalSales, forecastWeeks) {const forecastData = [];if (!historicalSales || historicalSales.length === 0) return forecastData;const lastHistoricalDate = historicalSales[historicalSales.length - 1].date;const historicalValues = historicalSales.map(d => d.sales);// Simple Seasonal Moving Average (using full cycle for seasonality index)const cycleLength = WEEKS_PER_YEAR;const movingAveragePeriod = 4; // Use 4-week moving averagefor (let i = 1; i <= forecastWeeks; i++) {const forecastDate = new Date(lastHistoricalDate);forecastDate.setDate(lastHistoricalDate.getDate() + i * DAYS_PER_WEEK);const forecastWeekOfYear = getWeekOfYear(forecastDate);const forecastSeasonality = SEASONALITY_INDEX[forecastWeekOfYear];// Calculate base using recent historical data, adjusted for seasonalitylet recentBaseSum = 0;let count = 0;for (let j = 1; j <= movingAveragePeriod; j++) {if (historicalSales.length >= j) {const pastData = historicalSales[historicalSales.length - j];const pastWeekOfYear = getWeekOfYear(pastData.date);const pastSeasonality = SEASONALITY_INDEX[pastWeekOfYear];if (pastSeasonality > 0.1) { // Avoid division by zero/small numbersrecentBaseSum += pastData.sales / pastSeasonality;count++;}}}const estimatedBase = count > 0 ? recentBaseSum / count : (BASE_SALES[state.selectedProduct]?.[state.selectedRegion] || 500);const forecastValue = Math.max(0, Math.round(estimatedBase * forecastSeasonality));forecastData.push({ date: forecastDate, forecast: forecastValue });}return forecastData;
}function simulateCurrentInventory(historicalSales) {// Simple simulation: Assume current inventory is related to recent sales volatility + a base levelif (!historicalSales || historicalSales.length < 4) return 500; // Default if not enough dataconst recentSales = historicalSales.slice(-4).map(d => d.sales);const avgRecentSales = recentSales.reduce((a, b) => a + b, 0) / recentSales.length;// Simulate current inventory between 0.5 and 2.5 weeks of average recent salesconst inventoryWeeks = 0.5 + Math.random() * 2;return Math.max(0, Math.round(avgRecentSales * inventoryWeeks));
}function calculateInventoryOptimization(forecast, safetyStockDays, currentInventory) {if (!forecast || forecast.length === 0) {return { target: currentInventory, suggestion: { type: 'none', amount: 0 } };}// Calculate total forecast demand over the safety stock periodconst safetyStockWeeks = safetyStockDays / DAYS_PER_WEEK;const relevantForecast = forecast.slice(0, Math.ceil(safetyStockWeeks));const avgWeeklyForecast = relevantForecast.reduce((sum, item) => sum + item.forecast, 0) / relevantForecast.length;// Target inventory = Safety Stock Demand// Simplified: Average weekly forecast * safety stock weeksconst target = Math.round(avgWeeklyForecast * safetyStockWeeks);const difference = target - currentInventory;let suggestionType = 'balanced';let suggestionAmount = 0;if (difference > target * 0.1) { // Need > 10% increasesuggestionType = 'increase';suggestionAmount = difference;} else if (difference < -target * 0.1) { // Need > 10% decreasesuggestionType = 'decrease';suggestionAmount = Math.abs(difference);} else {suggestionType = 'balanced';suggestionAmount = Math.abs(difference);}return {target: target,suggestion: {type: suggestionType,amount: Math.round(suggestionAmount)}};
}// =============================================================================
// UI Rendering
// =============================================================================function renderResults() {console.log("Rendering results...");try {if (elements['chart-placeholder']) {elements['chart-placeholder'].style.display = 'none';}if (elements['sales-forecast-chart']) {elements['sales-forecast-chart'].style.display = 'block';}renderChart();renderInventoryDetails();} catch (error) {console.error("Error rendering results:", error);}
}function renderPlaceholder() {console.log("Rendering placeholder...");try {if (elements['chart-placeholder']) {elements['chart-placeholder'].style.display = 'block';elements['chart-placeholder'].textContent = '请点击"运行预测与优化"生成图表和建议。';}if (elements['sales-forecast-chart'] && state.chartInstance) {state.chartInstance.destroy();state.chartInstance = null;elements['sales-forecast-chart'].style.display = 'none';}if (elements['inventory-details']) {elements['inventory-details'].innerHTML = '<p class="placeholder">请选择产品和区域,并运行模拟。</p>';}} catch (error) {console.error("Error rendering placeholder:", error);}}function renderChart() {console.log("Rendering chart...");try {const ctx = elements['sales-forecast-chart']?.getContext('2d');if (!ctx) {console.error("Failed to get chart canvas context.");return;}const historicalData = state.historicalSales.map(d => ({ x: d.date.getTime(), y: d.sales }));const forecastData = state.forecast.map(d => ({ x: d.date.getTime(), y: d.forecast }));// Combine dates for the x-axis labelsconst allDates = [...state.historicalSales.map(d => d.date), ...state.forecast.map(d => d.date)];if (state.chartInstance) {state.chartInstance.destroy();}state.chartInstance = new Chart(ctx, {type: 'line',data: {datasets: [{label: '历史销量',data: historicalData,borderColor: 'var(--history-color)',backgroundColor: 'rgba(0, 122, 255, 0.1)',borderWidth: 2,pointRadius: 1,tension: 0.1},{label: '预测需求',data: forecastData,borderColor: 'var(--forecast-color)',backgroundColor: 'rgba(255, 149, 0, 0.1)',borderWidth: 2,pointRadius: 1,borderDash: [5, 5], // Dashed line for forecasttension: 0.1}]},options: {responsive: true,maintainAspectRatio: false,scales: {x: {type: 'time',time: {unit: 'month',tooltipFormat: 'yyyy-MM-dd',displayFormats: {month: 'yyyy-MM'}},title: {display: true,text: '时间'}},y: {beginAtZero: true,title: {display: true,text: '销量/预测量'}}},plugins: {tooltip: {mode: 'index',intersect: false,},legend: {display: false // Use custom legend in HTML}},animation: {duration: 500 // Add subtle animation}}});console.log("Chart rendered.");} catch (error) {console.error("Error rendering chart:", error);}
}function renderInventoryDetails() {console.log("Rendering inventory details...");try {const container = elements['inventory-details'];if (!container) {console.error("inventory-details element not found for rendering.");return;}const product = state.selectedProduct; // Use actual product name later if neededconst region = state.selectedRegion; // Use actual region name laterconst current = state.currentInventory;const target = state.targetInventory;const suggestion = state.optimizationSuggestion;let suggestionText = '';let suggestionClass = suggestion.type;if (suggestion.type === 'increase') {suggestionText = `建议增加库存: <span class="suggestion-amount">${suggestion.amount}</span>`;} else if (suggestion.type === 'decrease') {suggestionText = `建议减少库存: <span class="suggestion-amount">${suggestion.amount}</span>`;} else {suggestionText = `库存水平良好 (差异: <span class="suggestion-amount">${suggestion.amount}</span>)`;}// Calculate bar percentages (relative to target or max of current/target?)// Let's make the bar width represent target*1.5 for visual spaceconst maxBarValue = Math.max(current, target) * 1.5;const currentPercent = maxBarValue > 0 ? (current / maxBarValue) * 100 : 0;const targetPercent = maxBarValue > 0 ? (target / maxBarValue) * 100 : 0;container.innerHTML = `<div class="inventory-item"><h4>${escapeHtml(region)} - ${escapeHtml(product)}</h4><p>当前模拟库存: <span class="current-stock">${current}</span> | 目标库存 (基于${state.safetyStockDays}天安全库存): <span class="target-stock">${target}</span></p><p class="optimization-suggestion ${suggestionClass}">${suggestionText}</p><div class="inventory-bar" title="当前: ${current} / 目标: ${target}"><div class="bar-current" style="width: ${Math.min(100, currentPercent)}%;"></div><div class="bar-target" style="left: ${Math.min(100, targetPercent)}%;" title="目标库存线"></div></div></div>`;console.log("Inventory details rendered.");} catch (error) {console.error("Error rendering inventory details:", error);}
}function updateConfigDisplay() {console.log("Updating config display values...");try {if (elements['forecast-horizon-value']) {elements['forecast-horizon-value'].textContent = state.forecastHorizonWeeks;} else console.warn("forecast-horizon-value element not found");if (elements['safety-stock-days-value']) {elements['safety-stock-days-value'].textContent = state.safetyStockDays;} else console.warn("safety-stock-days-value element not found");if (elements['historical-data-weeks-value']) {elements['historical-data-weeks-value'].textContent = state.historicalDataWeeks;} else console.warn("historical-data-weeks-value element not found");} catch (error) {console.error("Error updating config display:", error);}
}function updateStatus(message) {try {if (elements['simulation-status']) {elements['simulation-status'].textContent = `状态: ${message}`;if (message === '运行中...') {elements['simulation-status'].style.backgroundColor = 'var(--warning-color)';} else if (message === '已完成') {elements['simulation-status'].style.backgroundColor = 'var(--success-color)';} else {elements['simulation-status'].style.backgroundColor = 'var(--text-color-secondary)';}} else {console.warn("simulation-status element not found to update status");}} catch (error) {console.error("Error updating status display:", error);}}function disableControls(disabled) {try {if (elements['run-simulation-btn']) elements['run-simulation-btn'].disabled = disabled;if (elements['product-select']) elements['product-select'].disabled = disabled;if (elements['region-select']) elements['region-select'].disabled = disabled;if (elements['forecast-horizon-slider']) elements['forecast-horizon-slider'].disabled = disabled;if (elements['safety-stock-days-slider']) elements['safety-stock-days-slider'].disabled = disabled;if (elements['historical-data-weeks-slider']) elements['historical-data-weeks-slider'].disabled = disabled;} catch (error) {console.error("Error disabling/enabling controls:", error);}}// =============================================================================
// Utility Functions
// =============================================================================function getWeekOfYear(date) {const startOfYear = new Date(date.getFullYear(), 0, 1);const diff = date - startOfYear;const oneWeek = 1000 * 60 * 60 * 24 * 7;const weekNumber = Math.floor(diff / oneWeek);return weekNumber % WEEKS_PER_YEAR; // Ensure it wraps around for seasonality index
}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]);
}function showErrorMessage(message) {console.error("Displaying error message to user:", message);try {const container = elements.container || document.body;let errorDiv = container.querySelector('.app-error-message');if (!errorDiv) {errorDiv = document.createElement('div');errorDiv.className = 'app-error-message';errorDiv.style.cssText = `position: absolute; /* Position within container */top: 50%;left: 50%;transform: translate(-50%, -50%);background-color: #f8d7da;color: #721c24;padding: 20px;border-radius: 5px;border: 1px solid #f5c6cb;box-shadow: 0 2px 4px rgba(0,0,0,0.1);max-width: 90%;text-align: center;z-index: 1000;`;container.appendChild(errorDiv);}errorDiv.innerHTML = `<h4 style="margin-bottom:10px; color:#721c24;">发生错误</h4><p style="margin-bottom:15px; font-size:0.9em;">${escapeHtml(message)}</p><button onclick="this.parentElement.style.display='none'" style="padding: 5px 10px; background:#dc3545; color:white; border:none; border-radius:4px; cursor:pointer;">关闭</button>`;errorDiv.style.display = 'block';} catch (error) {console.error("无法显示错误消息:", error);alert("发生严重错误,且无法显示错误提示。请检查控制台。");}
}// --- Start Initialization Process ---
if (document.readyState === 'complete') {console.log("Document already complete, attempting initialization shortly.");setTimeout(attemptInitialization, 50);
} else {window.addEventListener('load', () => {console.log("Window load event fired, attempting initialization shortly.");setTimeout(attemptInitialization, 50);});// Fallback in case load doesn't fire or is misseddocument.addEventListener('DOMContentLoaded', () => {console.log("DOMContentLoaded event fired, attempting initialization shortly if not already done.");setTimeout(attemptInitialization, 100);});
} 
http://www.dtcms.com/a/342913.html

相关文章:

  • 【互动屏幕】大屏拼接在数字展厅展示上有哪些优势?
  • (CVPR-2025)通过频率分解实现身份保持的文本到视频生成
  • 【音视频】闭合GOP和开放GOP
  • 旅游小程序开发指南
  • 第三阶段数据库-5:数据库的主键,索引,约束,表间关系的图形化操作
  • 8.Shell脚本修炼手册---sed工具的基本使用
  • HarmonyOS 实战:6 种实现实时数据更新的方案全解析(含完整 Demo)
  • JavaScript中的深浅拷贝
  • Llama-Factory微调 Qwen2.5-VL-3B 模型
  • 人工智能未来趋势如何?
  • 【秋招笔试】2025.08.19百度秋招机考第一套
  • 算法训练营day57 图论⑦ prim算法精讲、kruskal算法精讲
  • 前端无感刷新 Token 的 Axios 封装方案
  • Github 下载加速--2025-08-21 亲测好用
  • 神经网络(Neural Network, NN)
  • gemini cli 用命令行玩转 AI 多模态开发
  • 网络安全大模型测试指标体系设计思路
  • 我与 ChatGPT 5:一段时间的深度编程体验与全栈评测
  • Zabbix状态图标灯显示
  • K8s部署dashboard平台和基本使用
  • MySQL:MVCC机制及其在Java秋招中的高频考点
  • AutoTrack-IR-DR200平台的构建与实现过程
  • python采集义乌购(yiwugo)商品详情API接口,json数据返回
  • 透射TEM 新手入门:快速掌握核心技能
  • 【每日一道算法题 day5】盛最多水的容器 (Container With Most Water) - LeetCode 题解
  • 深度学习之NLP基础
  • 【React】tab切换功能和排序实现,classnames工具优化类名控制
  • Java基础环境jdk和maven安装及配置+开源项目下载及编译打包教程
  • Flutter如何通过GlobalKey调用组件内的方法
  • 微服务的编程测评系统13-我的竞赛列表-elasticSearch