14、企业资源计划 (ERP) - 全球库存协同模拟 - /生产管理组件/erp-global-inventory

76个工业组件库示例汇总

企业资源计划 (ERP) - 全球库存协同模拟组件

概述

这是一个交互式的 Web 组件,用于模拟企业资源计划 (ERP) 系统的核心概念,特别侧重于跨国制造企业的全球库存协同。
它集成了仪表盘、全球库存管理、订单管理、生产概览和财务摘要等关键模块的简化视图。

该组件旨在提供一个可视化的界面,帮助理解 ERP 系统如何整合不同业务领域的数据,并支持全局决策。

注意:这是一个高度简化的概念演示,并非功能齐全的 ERP 系统。数据和流程都经过了大幅简化。

主要功能

  • 模块化导航:
    • 提供清晰的侧边栏导航,可在不同 ERP 模块之间轻松切换。
    • 导航项包括:仪表盘、全球库存、订单管理、生产概览、财务摘要。
  • 仪表盘:
    • 显示关键绩效指标 (KPI) 概览,如总库存价值、待处理订单数、低库存 SKU 数量和模拟的准时交货率。
    • 包含一个模拟的全球库存分布图,概念性地展示不同区域(美洲、欧洲、亚太)的总库存量。
  • 全球库存管理:
    • 以表格形式展示跨越多个虚拟仓库(如上海、法兰克福、洛杉矶、新加坡)的详细库存信息。
    • 包含 SKU、产品名称、地点、现有库存、可用库存(现有 - 已分配)和库存状态(正常、低库存、缺货)。
    • 支持按 SKU、产品名称或地点进行文本搜索。
    • 支持按仓库地点进行筛选。
  • 订单管理 (简化):
    • 展示销售订单和采购订单的列表。
    • 包含订单号、类型、客户/供应商、日期、状态(待处理、处理中、已发货、已完成)和金额。
    • 支持按订单类型和订单状态进行筛选。
  • 生产概览 (简化):
    • 展示进行中和计划中的生产订单列表。
    • 包含生产单号、产品 SKU、计划数量、已完成数量、状态和计划完成日期。
  • 财务摘要 (简化):
    • 显示模拟的关键财务数据,如总收入、总支出、利润率和应收账款。
  • 数据模拟与动态更新:
    • 包含一个"刷新数据"按钮,点击后会模拟库存水平的波动、订单状态的推进、生产进度的更新以及财务数据的变化。
    • 系统时间实时显示。
  • 界面风格:
    • 采用苹果科技风格,注重简洁、清晰和优雅的交互。
    • 采用侧边栏 + 主内容区的响应式布局,适应不同屏幕尺寸。

如何使用

  1. 打开页面: 在浏览器中打开 index.html
  2. 浏览仪表盘: 默认显示仪表盘视图,观察 KPI 和模拟的库存分布图。
  3. 切换模块: 点击左侧导航栏中的链接(如"全球库存"、"订单管理"等)切换到相应的模块视图。
  4. 交互操作:
    • 在"全球库存"视图中,尝试使用顶部的搜索框输入 SKU 或地点进行搜索,或使用下拉菜单按地点筛选库存记录。
    • 在"订单管理"视图中,尝试使用顶部的下拉菜单按订单类型或状态筛选订单。
  5. 模拟数据更新: 点击右上角的刷新按钮 (),观察各个模块中的数据(如 KPI、库存数量、订单状态、生产进度、财务数字)发生模拟的变化。

模拟细节说明

  • 简化集成: 各模块之间的数据联动是高度简化的(例如,库存变化仅随机模拟,未严格根据订单和生产进行扣减/增加)。
  • 随机性: 数据刷新时的变化(库存波动、订单状态推进、生产进度、财务增长)是基于随机数生成的,用于演示动态性。
  • 无持久化: 所有数据仅存在于浏览器会话中,刷新页面将重置模拟。
  • 无业务逻辑: 没有实现真实的库存预留、订单处理、生产调度或财务核算逻辑。
  • 概念性地图: 仪表盘上的地图仅为示意图,未集成真实地图库。

文件结构

生产管理组件/
└── erp-global-inventory/
    ├── index.html       # 组件的 HTML 结构
    ├── styles.css       # CSS 样式文件 (Apple 风格)
    ├── script.js        # JavaScript 文件 (模拟逻辑与交互)
    └── README.md        # 本说明文件

技术栈

  • HTML5
  • CSS3 (Flexbox, Grid, Custom Properties)
  • JavaScript (ES6+)

效果展示

在这里插入图片描述

源码

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>企业资源计划 (ERP) - 全球库存协同</title>
    <link rel="stylesheet" href="styles.css">
    <!-- Placeholder for potential mapping library if needed later -->
</head>
<body>
    <div class="erp-container">
        <aside class="erp-sidebar">
            <div class="sidebar-header">
                <h2>ERP 系统</h2>
                <span>(全球库存模拟)</span>
            </div>
            <nav class="erp-nav">
                <ul>
                    <li><a href="#" class="nav-link active" data-target="dashboard"><i class="icon icon-dashboard"></i> 仪表盘</a></li>
                    <li><a href="#" class="nav-link" data-target="inventory"><i class="icon icon-inventory"></i> 全球库存</a></li>
                    <li><a href="#" class="nav-link" data-target="orders"><i class="icon icon-orders"></i> 订单管理</a></li>
                    <li><a href="#" class="nav-link" data-target="production"><i class="icon icon-production"></i> 生产概览</a></li>
                    <li><a href="#" class="nav-link" data-target="finance"><i class="icon icon-finance"></i> 财务摘要</a></li>
                </ul>
            </nav>
            <div class="sidebar-footer">
                 <p>状态: <span id="systemStatus">在线</span></p>
            </div>
        </aside>

        <main class="erp-main-content">
            <header class="main-header">
                <h1 id="mainViewTitle">仪表盘</h1>
                <div class="header-actions">
                    <span id="currentTime">--:--:--</span>
                    <button class="action-button small" id="refreshButton" title="刷新数据"><i class="icon icon-refresh"></i></button>
                </div>
            </header>

            <div class="view-container" id="viewContainer">
                <!-- Views will be loaded here by JS -->

                <!-- Dashboard View -->
                <div id="dashboardView" class="erp-view active">
                    <h3>关键指标概览</h3>
                    <div class="dashboard-widgets">
                        <div class="widget"><h4>总库存价值</h4><p id="kpiTotalInventoryValue">加载中...</p></div>
                        <div class="widget"><h4>待处理订单</h4><p id="kpiPendingOrders">加载中...</p></div>
                        <div class="widget"><h4>低库存 SKU</h4><p id="kpiLowStockItems">加载中...</p></div>
                        <div class="widget"><h4>准时交货率</h4><p id="kpiOnTimeDelivery">加载中...</p></div>
                    </div>
                     <h3>库存分布 (模拟地图)</h3>
                     <div class="map-placeholder" id="inventoryMap">
                        <div class="map-region" data-region="americas">美洲仓: <span class="stock-level">---</span></div>
                        <div class="map-region" data-region="emea">欧洲仓: <span class="stock-level">---</span></div>
                        <div class="map-region" data-region="apac">亚太仓: <span class="stock-level">---</span></div>
                     </div>
                </div>

                <!-- Global Inventory View -->
                <div id="inventoryView" class="erp-view">
                    <div class="toolbar">
                        <input type="text" id="inventorySearch" placeholder="搜索 SKU 或地点...">
                         <select id="inventoryLocationFilter">
                            <option value="all">所有地点</option>
                            <!-- Options populated by JS -->
                        </select>
                    </div>
                    <div class="table-container">
                        <table>
                            <thead>
                                <tr>
                                    <th>SKU</th>
                                    <th>产品名称</th>
                                    <th>地点</th>
                                    <th>现有库存</th>
                                    <th>可用库存</th>
                                    <th>状态</th>
                                </tr>
                            </thead>
                            <tbody id="inventoryTableBody">
                                <!-- Rows populated by JS -->
                                <tr><td colspan="6" class="placeholder">加载中...</td></tr>
                            </tbody>
                        </table>
                    </div>
                </div>

                <!-- Orders View -->
                <div id="ordersView" class="erp-view">
                     <div class="toolbar">
                        <select id="orderTypeFilter">
                            <option value="all">所有订单</option>
                            <option value="sales">销售订单</option>
                            <option value="purchase">采购订单</option>
                        </select>
                        <select id="orderStatusFilter">
                            <option value="all">所有状态</option>
                            <option value="pending">待处理</option>
                            <option value="processing">处理中</option>
                             <option value="shipped">已发货</option>
                            <option value="completed">已完成</option>
                        </select>
                     </div>
                     <div class="table-container">
                         <table>
                             <thead>
                                 <tr>
                                     <th>订单号</th>
                                     <th>类型</th>
                                     <th>客户/供应商</th>
                                     <th>创建日期</th>
                                     <th>状态</th>
                                     <th>总金额</th>
                                 </tr>
                             </thead>
                             <tbody id="ordersTableBody">
                                 <!-- Rows populated by JS -->
                                <tr><td colspan="6" class="placeholder">加载中...</td></tr>
                             </tbody>
                         </table>
                     </div>
                </div>

                <!-- Production View -->
                <div id="productionView" class="erp-view">
                    <h3>进行中的生产订单</h3>
                     <div class="table-container">
                         <table>
                             <thead>
                                 <tr>
                                     <th>生产单号</th>
                                     <th>产品 SKU</th>
                                     <th>计划数量</th>
                                     <th>已完成数量</th>
                                     <th>状态</th>
                                     <th>计划完成日期</th>
                                 </tr>
                             </thead>
                             <tbody id="productionTableBody">
                                 <!-- Rows populated by JS -->
                                <tr><td colspan="6" class="placeholder">加载中...</td></tr>
                             </tbody>
                         </table>
                     </div>
                </div>

                <!-- Finance View -->
                <div id="financeView" class="erp-view">
                     <h3>财务摘要 (模拟)</h3>
                     <div class="dashboard-widgets">
                         <div class="widget"><h4>总收入 (YTD)</h4><p id="financeRevenue">加载中...</p></div>
                         <div class="widget"><h4>总支出 (YTD)</h4><p id="financeExpenses">加载中...</p></div>
                         <div class="widget"><h4>利润率</h4><p id="financeProfitMargin">加载中...</p></div>
                         <div class="widget"><h4>应收账款</h4><p id="financeReceivables">加载中...</p></div>
                    </div>
                     <p class="disclaimer">注:财务数据为高度简化模拟,仅用于演示模块集成概念。</p>
                </div>

            </div>
        </main>
    </div>

    <script src="script.js"></script>
</body>
</html> 

styles.css

:root {
    --background-color: #f8f8f8;
    --sidebar-bg: #2c2c2e; /* Dark grey for sidebar */
    --main-bg: #ffffff;
    --header-bg: #f5f5f7; /* Lighter header */
    --border-color: #d1d1d6;
    --text-primary: #1d1d1f;
    --text-secondary: #6e6e73;
    --text-sidebar: #ffffff;
    --text-sidebar-secondary: #a1a1a6;
    --accent-blue: #007aff;
    --hover-blue: #005bb5;
    --nav-active-bg: rgba(0, 122, 255, 0.15);
    --nav-hover-bg: rgba(255, 255, 255, 0.08);
    --widget-bg: #ffffff;
    --table-header-bg: #f9f9f9;
    --table-row-hover-bg: #f0f0f5;
    --stock-ok: #34c759;
    --stock-low: #ff9500;
    --stock-out: #ff3b30;
    --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --border-radius: 8px;
    --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    --sidebar-width: 240px;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: var(--font-family);
    background-color: var(--background-color);
    color: var(--text-primary);
    line-height: 1.6;
    overflow: hidden; /* Prevent body scroll */
}

.erp-container {
    display: flex;
    height: 100vh; /* Full viewport height */
    max-height: 850px; /* Limit overall height if needed */
}

/* Sidebar */
.erp-sidebar {
    width: var(--sidebar-width);
    background-color: var(--sidebar-bg);
    color: var(--text-sidebar);
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    box-shadow: 2px 0 5px rgba(0,0,0,0.1);
    z-index: 10;
}

.sidebar-header {
    padding: 1.5rem 1rem;
    text-align: center;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.sidebar-header h2 {
    font-size: 1.4rem;
    margin-bottom: 0.2rem;
    font-weight: 600;
}

.sidebar-header span {
    font-size: 0.85rem;
    color: var(--text-sidebar-secondary);
}

.erp-nav {
    flex-grow: 1;
    padding-top: 1rem;
}

.erp-nav ul {
    list-style: none;
}

.erp-nav li {
    margin-bottom: 0.2rem;
}

.erp-nav .nav-link {
    display: flex;
    align-items: center;
    padding: 0.8rem 1.5rem;
    color: var(--text-sidebar);
    text-decoration: none;
    font-size: 0.95rem;
    transition: background-color 0.2s ease, color 0.2s ease;
    border-left: 3px solid transparent;
}

.erp-nav .nav-link:hover {
    background-color: var(--nav-hover-bg);
}

.erp-nav .nav-link.active {
    background-color: var(--nav-active-bg);
    color: var(--accent-blue);
    font-weight: 500;
    border-left-color: var(--accent-blue);
}

.erp-nav .nav-link .icon {
    margin-right: 0.8rem;
    width: 18px; /* Ensure consistent icon spacing */
    text-align: center;
}

.sidebar-footer {
    padding: 1rem 1.5rem;
    font-size: 0.8rem;
    color: var(--text-sidebar-secondary);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}

/* Main Content Area */
.erp-main-content {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    background-color: var(--main-bg);
    overflow: hidden; /* Prevent main area from scrolling, handle inside */
}

.main-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.8rem 1.5rem;
    background-color: var(--header-bg);
    border-bottom: 1px solid var(--border-color);
    flex-shrink: 0;
}

.main-header h1 {
    font-size: 1.5rem;
    font-weight: 600;
}

.header-actions {
    display: flex;
    align-items: center;
}

.header-actions span {
    margin-right: 1rem;
    font-size: 0.9rem;
    color: var(--text-secondary);
}

/* View Container */
.view-container {
    flex-grow: 1;
    padding: 1.5rem;
    overflow-y: auto; /* Enable scrolling for the content of the active view */
}

.erp-view {
    display: none; /* Hide views by default */
}

.erp-view.active {
    display: block; /* Show the active view */
}

.erp-view h3 {
    font-size: 1.2rem;
    font-weight: 500;
    margin-bottom: 1.5rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--border-color);
}

/* Dashboard Widgets */
.dashboard-widgets {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1.5rem;
    margin-bottom: 2rem;
}

.widget {
    background-color: var(--widget-bg);
    padding: 1.2rem;
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    border: 1px solid var(--border-color);
}

.widget h4 {
    font-size: 0.9rem;
    color: var(--text-secondary);
    margin-bottom: 0.5rem;
    font-weight: 500;
}

.widget p {
    font-size: 1.6rem;
    font-weight: 600;
    color: var(--text-primary);
}

/* Map Placeholder */
.map-placeholder {
    background-color: #e9ecef;
    border-radius: var(--border-radius);
    padding: 2rem;
    display: flex;
    justify-content: space-around;
    align-items: center;
    min-height: 150px;
    margin-top: 1rem;
    border: 1px solid var(--border-color);
}

.map-region {
    text-align: center;
    font-weight: 500;
    padding: 1rem;
    background-color: rgba(255,255,255,0.7);
    border-radius: 4px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}

.map-region .stock-level {
    display: block;
    font-size: 1.2rem;
    margin-top: 0.3rem;
    color: var(--accent-blue);
}

/* Table Styling */
.table-container {
    overflow-x: auto; /* Allow horizontal scroll for table if needed */
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    background-color: #fff;
    box-shadow: var(--box-shadow);
}

table {
    width: 100%;
    border-collapse: collapse;
}

thead th {
    background-color: var(--table-header-bg);
    padding: 0.8rem 1rem;
    text-align: left;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--border-color);
}

tbody td {
    padding: 0.8rem 1rem;
    font-size: 0.9rem;
    border-bottom: 1px solid var(--border-color);
    vertical-align: middle;
}

tbody tr:last-child td {
    border-bottom: none;
}

tbody tr:hover {
    background-color: var(--table-row-hover-bg);
}

.placeholder {
    text-align: center;
    color: var(--text-secondary);
    font-style: italic;
    padding: 2rem;
}

.disclaimer {
    font-size: 0.85rem;
    color: var(--text-secondary);
    margin-top: 1.5rem;
    text-align: center;
    font-style: italic;
}

/* Stock Status in Table */
.stock-status {
    font-weight: 500;
    padding: 0.2rem 0.5rem;
    border-radius: 4px;
    font-size: 0.8rem;
    display: inline-block;
    min-width: 50px;
    text-align: center;
}

.stock-status.ok {
    background-color: #e5f5e5; color: var(--stock-ok);
}
.stock-status.low {
    background-color: #fff0e0; color: var(--stock-low);
}
.stock-status.out {
    background-color: #ffe5e5; color: var(--stock-out);
}

/* Toolbar for Filters/Search */
.toolbar {
    margin-bottom: 1.5rem;
    display: flex;
    gap: 1rem;
    align-items: center;
}

.toolbar input[type="text"],
.toolbar select {
    padding: 0.5rem 0.8rem;
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    font-size: 0.9rem;
    background-color: #fff;
}

.toolbar input[type="text"] {
    flex-grow: 1;
    max-width: 300px;
}

/* Action Buttons */
.action-button {
    display: inline-flex;
    align-items: center;
    background-color: var(--accent-blue);
    color: white;
    border: none;
    padding: 0.6rem 1.2rem;
    border-radius: var(--border-radius);
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 500;
    transition: background-color 0.2s ease;
}

.action-button:hover {
    background-color: var(--hover-blue);
}

.action-button.small {
    padding: 0.4rem 0.8rem;
    font-size: 0.8rem;
}

.action-button .icon {
    margin-right: 0.4rem;
}

/* Responsive Design */
@media (max-width: 992px) {
    .erp-sidebar {
        width: 60px; /* Collapse sidebar */
        overflow: hidden;
    }
    .erp-sidebar:hover {
         width: var(--sidebar-width); /* Expand on hover */
    }
    .sidebar-header h2, .sidebar-header span, .sidebar-footer p {
         /* Hide text when collapsed, consider showing only icons */
         /* visibility: hidden; Doesnt work well with hover expansion */
         /* Use JS or more complex CSS for better collapsed view */
    }
    .erp-nav .nav-link span {
         /* Hide text part of link when collapsed */
    }
    .main-header h1 {
        font-size: 1.3rem;
    }
    .dashboard-widgets {
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    }
}

@media (max-width: 768px) {
    .erp-container {
        flex-direction: column; /* Stack sidebar and content */
        height: auto; /* Allow content height */
        max-height: none;
    }
    .erp-sidebar {
        width: 100%;
        height: auto;
        box-shadow: none;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        flex-direction: row; /* Horizontal layout for sidebar items */
        justify-content: space-between;
        align-items: center;
        padding: 0 0.5rem;
    }
    .sidebar-header {
         padding: 0.5rem;
         border: none;
         text-align: left;
    }
     .sidebar-header h2 {
         font-size: 1rem;
         display: none; /* Hide title on mobile */
     }
      .sidebar-header span {
         display: none;
     }
    .erp-nav {
         padding-top: 0;
         flex-grow: 0;
    }
    .erp-nav ul {
         display: flex;
    }
    .erp-nav li {
         margin-bottom: 0;
    }
    .erp-nav .nav-link {
         padding: 0.6rem 0.8rem; /* Smaller padding */
         border-left: none;
         border-bottom: 3px solid transparent;
         font-size: 0.85rem;
    }
     .erp-nav .nav-link .icon {
         margin-right: 0.3rem;
     }
     .erp-nav .nav-link span { /* Hide text labels */
         display: none;
     }
     .erp-nav .nav-link.active {
         border-bottom-color: var(--accent-blue);
         border-left-color: transparent;
         background-color: transparent;
         color: var(--accent-blue);
     }
     .sidebar-footer {
         display: none; /* Hide footer on mobile */
     }

    .main-header {
         padding: 0.6rem 1rem;
    }
     .main-header h1 {
         font-size: 1.1rem;
     }
    .view-container {
         padding: 1rem;
    }
    .dashboard-widgets {
         grid-template-columns: 1fr; /* Stack widgets */
         gap: 1rem;
    }
     .toolbar {
         flex-direction: column;
         align-items: stretch;
     }
     .toolbar input[type="text"], .toolbar select {
         max-width: none;
     }
}

/* Basic Icon Placeholders */
.icon::before {
    display: inline-block;
    font-weight: bold;
    /* Examples - replace with actual icons */
}
.icon-dashboard::before { content: "📊"; }
.icon-inventory::before { content: "📦"; }
.icon-orders::before { content: "🛒"; }
.icon-production::before { content: "🏭"; }
.icon-finance::before { content: "💰"; }
.icon-refresh::before { content: "🔄"; } 

script.js

// script.js - ERP Global Inventory Component

document.addEventListener('DOMContentLoaded', () => {
    // --- DOM Elements ---
    const navLinks = document.querySelectorAll('.nav-link');
    const views = document.querySelectorAll('.erp-view');
    const mainViewTitle = document.getElementById('mainViewTitle');
    const currentTimeEl = document.getElementById('currentTime');
    const refreshButton = document.getElementById('refreshButton');

    // Dashboard elements
    const kpiTotalInventoryValueEl = document.getElementById('kpiTotalInventoryValue');
    const kpiPendingOrdersEl = document.getElementById('kpiPendingOrders');
    const kpiLowStockItemsEl = document.getElementById('kpiLowStockItems');
    const kpiOnTimeDeliveryEl = document.getElementById('kpiOnTimeDelivery');
    const inventoryMapRegions = document.querySelectorAll('#inventoryMap .map-region');

    // Inventory elements
    const inventorySearchInput = document.getElementById('inventorySearch');
    const inventoryLocationFilter = document.getElementById('inventoryLocationFilter');
    const inventoryTableBody = document.getElementById('inventoryTableBody');

    // Orders elements
    const orderTypeFilter = document.getElementById('orderTypeFilter');
    const orderStatusFilter = document.getElementById('orderStatusFilter');
    const ordersTableBody = document.getElementById('ordersTableBody');

    // Production elements
    const productionTableBody = document.getElementById('productionTableBody');

    // Finance elements
    const financeRevenueEl = document.getElementById('financeRevenue');
    const financeExpensesEl = document.getElementById('financeExpenses');
    const financeProfitMarginEl = document.getElementById('financeProfitMargin');
    const financeReceivablesEl = document.getElementById('financeReceivables');

    // --- Simulation Data ---
    let inventoryData = [];
    let orderData = [];
    let productionData = [];
    let financeData = {};
    let locations = ['上海仓库', '法兰克福仓库', '洛杉矶仓库', '新加坡仓库'];
    let products = [
        { sku: 'CPU-001', name: '高性能处理器 X1', cost: 150 },
        { sku: 'MEM-002', name: '16GB DDR5 内存条', cost: 50 },
        { sku: 'GPU-003', name: '图形加速卡 GFX Pro', cost: 400 },
        { sku: 'SSD-004', name: '1TB NVMe 固态硬盘', cost: 80 },
        { sku: 'PSU-005', name: '750W 电源供应器', cost: 60 }
    ];
    const lowStockThreshold = 50;
    const onTimeDeliveryTarget = 0.95; // 95%

    // --- Initialization ---
    function initializeERP() {
        generateInitialData();
        setupEventListeners();
        updateTime();
        setInterval(updateTime, 1000);
        renderDashboard();
        renderInventory();
        renderOrders();
        renderProduction();
        renderFinance();
        populateLocationFilter();
        // Set initial view
        switchView('dashboard');
        console.log("ERP Simulation Initialized");
    }

    function generateInitialData() {
        inventoryData = [];
        locations.forEach(loc => {
            products.forEach(prod => {
                const onHand = Math.floor(Math.random() * 500) + 20; // 20 to 519
                const allocated = Math.floor(Math.random() * onHand * 0.3); // Allocate up to 30%
                inventoryData.push({
                    ...prod,
                    location: loc,
                    onHand: onHand,
                    allocated: allocated,
                    available: onHand - allocated,
                    status: getStockStatus(onHand - allocated)
                });
            });
        });

        orderData = [
            { id: 'SO-1001', type: 'sales', customer: '客户A', date: '2024-07-15', status: 'pending', amount: 15000 },
            { id: 'PO-2001', type: 'purchase', customer: '供应商X', date: '2024-07-14', status: 'processing', amount: 8000 },
            { id: 'SO-1002', type: 'sales', customer: '客户B', date: '2024-07-16', status: 'processing', amount: 22000 },
            { id: 'SO-1003', type: 'sales', customer: '客户C', date: '2024-07-17', status: 'shipped', amount: 5000 },
            { id: 'PO-2002', type: 'purchase', customer: '供应商Y', date: '2024-07-18', status: 'completed', amount: 12000 },
             { id: 'SO-1004', type: 'sales', customer: '客户A', date: '2024-07-19', status: 'pending', amount: 9500 },
        ];

        productionData = [
            { id: 'MO-3001', sku: 'CPU-001', plannedQty: 500, completedQty: 250, status: '进行中', dueDate: '2024-07-25' },
            { id: 'MO-3002', sku: 'GPU-003', plannedQty: 200, completedQty: 0, status: '计划中', dueDate: '2024-07-28' },
             { id: 'MO-3003', sku: 'MEM-002', plannedQty: 1000, completedQty: 1000, status: '已完成', dueDate: '2024-07-20' },
        ];

        financeData = {
            revenue: 550000,
            expenses: 420000,
            receivables: 85000
        };
    }

    function simulateDataChanges() {
        // Simulate inventory changes
        inventoryData.forEach(item => {
            const change = Math.floor(Math.random() * 21) - 10; // -10 to +10
            item.onHand = Math.max(0, item.onHand + change);
            const allocatedChange = Math.floor(Math.random() * Math.abs(change) * 0.5) * (change > 0 ? 1 : -1);
            item.allocated = Math.max(0, Math.min(item.onHand, item.allocated + allocatedChange));
            item.available = item.onHand - item.allocated;
            item.status = getStockStatus(item.available);
        });

        // Simulate order status changes
        orderData.forEach(order => {
            if (order.status === 'pending' && Math.random() < 0.1) {
                order.status = 'processing';
            } else if (order.status === 'processing' && Math.random() < 0.08) {
                order.status = 'shipped';
            } else if (order.status === 'shipped' && Math.random() < 0.05) {
                order.status = 'completed';
            }
        });

        // Simulate production progress
        productionData.forEach(prodOrder => {
             if (prodOrder.status === '进行中') {
                 const progress = Math.floor(Math.random() * (prodOrder.plannedQty * 0.1)); // Progress up to 10%
                 prodOrder.completedQty = Math.min(prodOrder.plannedQty, prodOrder.completedQty + progress);
                 if (prodOrder.completedQty === prodOrder.plannedQty) {
                     prodOrder.status = '已完成';
                 }
             } else if (prodOrder.status === '计划中' && Math.random() < 0.05) {
                 prodOrder.status = '进行中';
             }
        });

        // Simulate finance changes
        financeData.revenue += Math.floor(Math.random() * 5000);
        financeData.expenses += Math.floor(Math.random() * 3000);
        financeData.receivables += Math.floor(Math.random() * 1000) - 500; // Can go up or down

        console.log("Simulated data changes applied.");
        // Re-render all views to reflect changes
        renderDashboard();
        renderInventory();
        renderOrders();
        renderProduction();
        renderFinance();
    }

    // --- Rendering Functions ---
    function renderDashboard() {
        // KPIs
        const totalValue = inventoryData.reduce((sum, item) => sum + item.onHand * item.cost, 0);
        kpiTotalInventoryValueEl.textContent = formatCurrency(totalValue);

        const pendingOrders = orderData.filter(o => o.status === 'pending' || o.status === 'processing').length;
        kpiPendingOrdersEl.textContent = pendingOrders;

        const lowStockItems = inventoryData.filter(item => item.status === 'low').length;
        kpiLowStockItemsEl.textContent = lowStockItems;

        // Simulate OTD
        const simulatedDeliveries = 100;
        const onTime = Math.floor(simulatedDeliveries * (onTimeDeliveryTarget + (Math.random() * 0.1 - 0.05))); // +/- 5% variation
        kpiOnTimeDeliveryEl.textContent = `${((onTime / simulatedDeliveries) * 100).toFixed(1)}%`;

        // Map
        const regionStock = {
            americas: inventoryData.filter(i => i.location === '洛杉矶仓库').reduce((sum, i) => sum + i.onHand, 0),
            emea: inventoryData.filter(i => i.location === '法兰克福仓库').reduce((sum, i) => sum + i.onHand, 0),
            apac: inventoryData.filter(i => i.location === '上海仓库' || i.location === '新加坡仓库').reduce((sum, i) => sum + i.onHand, 0),
        };

        inventoryMapRegions.forEach(regionEl => {
            const region = regionEl.dataset.region;
            const stockLevelEl = regionEl.querySelector('.stock-level');
            if (stockLevelEl && regionStock[region] !== undefined) {
                stockLevelEl.textContent = regionStock[region].toLocaleString();
            }
        });
    }

    function renderInventory() {
        const searchTerm = inventorySearchInput.value.toLowerCase();
        const locationFilter = inventoryLocationFilter.value;

        const filteredData = inventoryData.filter(item => {
            const matchesSearch = item.sku.toLowerCase().includes(searchTerm) ||
                                  item.name.toLowerCase().includes(searchTerm) ||
                                  item.location.toLowerCase().includes(searchTerm);
            const matchesLocation = locationFilter === 'all' || item.location === locationFilter;
            return matchesSearch && matchesLocation;
        });

        inventoryTableBody.innerHTML = ''; // Clear existing
        if (filteredData.length === 0) {
            inventoryTableBody.innerHTML = '<tr><td colspan="6" class="placeholder">没有找到匹配的库存记录</td></tr>';
            return;
        }

        filteredData.forEach(item => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${item.sku}</td>
                <td>${item.name}</td>
                <td>${item.location}</td>
                <td>${item.onHand.toLocaleString()}</td>
                <td>${item.available.toLocaleString()}</td>
                <td><span class="stock-status ${item.status}">${getStockStatusReadable(item.status)}</span></td>
            `;
            inventoryTableBody.appendChild(row);
        });
    }

    function renderOrders() {
        const typeFilter = orderTypeFilter.value;
        const statusFilter = orderStatusFilter.value;

         const filteredData = orderData.filter(order => {
            const matchesType = typeFilter === 'all' || order.type === typeFilter;
            const matchesStatus = statusFilter === 'all' || order.status === statusFilter;
            return matchesType && matchesStatus;
         });

        ordersTableBody.innerHTML = ''; // Clear existing
         if (filteredData.length === 0) {
            ordersTableBody.innerHTML = '<tr><td colspan="6" class="placeholder">没有找到匹配的订单记录</td></tr>';
            return;
        }

        filteredData.forEach(order => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${order.id}</td>
                <td>${order.type === 'sales' ? '销售' : '采购'}</td>
                <td>${order.customer}</td>
                <td>${order.date}</td>
                <td>${getReadableOrderStatus(order.status)}</td>
                <td>${formatCurrency(order.amount)}</td>
            `;
            ordersTableBody.appendChild(row);
        });
    }

    function renderProduction() {
         productionTableBody.innerHTML = ''; // Clear existing
         if (productionData.length === 0) {
            productionTableBody.innerHTML = '<tr><td colspan="6" class="placeholder">没有生产订单</td></tr>';
            return;
        }
        productionData.forEach(order => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${order.id}</td>
                <td>${order.sku}</td>
                <td>${order.plannedQty.toLocaleString()}</td>
                <td>${order.completedQty.toLocaleString()}</td>
                <td>${order.status}</td>
                <td>${order.dueDate}</td>
            `;
            productionTableBody.appendChild(row);
        });
    }

    function renderFinance() {
        financeRevenueEl.textContent = formatCurrency(financeData.revenue);
        financeExpensesEl.textContent = formatCurrency(financeData.expenses);
        const profit = financeData.revenue - financeData.expenses;
        const margin = financeData.revenue > 0 ? ((profit / financeData.revenue) * 100).toFixed(1) : 0;
        financeProfitMarginEl.textContent = `${margin}%`;
        financeReceivablesEl.textContent = formatCurrency(financeData.receivables);
    }

    // --- Event Listeners & Navigation ---
    function setupEventListeners() {
        navLinks.forEach(link => {
            link.addEventListener('click', (e) => {
                e.preventDefault();
                const targetView = link.dataset.target;
                switchView(targetView);
            });
        });

        refreshButton.addEventListener('click', simulateDataChanges);

        // Filter/Search listeners
        inventorySearchInput.addEventListener('input', renderInventory);
        inventoryLocationFilter.addEventListener('change', renderInventory);
        orderTypeFilter.addEventListener('change', renderOrders);
        orderStatusFilter.addEventListener('change', renderOrders);
    }

    function switchView(targetViewId) {
        // Update Navigation Links
        navLinks.forEach(link => {
            if (link.dataset.target === targetViewId) {
                link.classList.add('active');
                mainViewTitle.textContent = link.textContent.trim(); // Set header title
            } else {
                link.classList.remove('active');
            }
        });

        // Update Views
        views.forEach(view => {
            if (view.id === `${targetViewId}View`) {
                view.classList.add('active');
            } else {
                view.classList.remove('active');
            }
        });
        // Scroll view content to top
        document.getElementById('viewContainer').scrollTop = 0;
    }

    // --- Utility Functions ---
    function updateTime() {
        const now = new Date();
        currentTimeEl.textContent = now.toLocaleTimeString('zh-CN');
    }

    function formatCurrency(value) {
        return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
    }

    function getStockStatus(availableQty) {
        if (availableQty <= 0) return 'out';
        if (availableQty < lowStockThreshold) return 'low';
        return 'ok';
    }

    function getStockStatusReadable(status) {
        switch (status) {
            case 'ok': return '正常';
            case 'low': return '低库存';
            case 'out': return '缺货';
            default: return status;
        }
    }

     function getReadableOrderStatus(status) {
        switch (status) {
            case 'pending': return '待处理';
            case 'processing': return '处理中';
            case 'shipped': return '已发货';
            case 'completed': return '已完成';
            default: return status;
        }
    }

    function populateLocationFilter() {
         inventoryLocationFilter.innerHTML = '<option value="all">所有地点</option>'; // Reset
         locations.forEach(loc => {
             const option = document.createElement('option');
             option.value = loc;
             option.textContent = loc;
             inventoryLocationFilter.appendChild(option);
         });
    }

    // --- Initial Call ---
    initializeERP();
}); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

地上一の鹅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值