企业资源计划 (ERP) - 全球库存协同模拟组件
概述
这是一个交互式的 Web 组件,用于模拟企业资源计划 (ERP) 系统的核心概念,特别侧重于跨国制造企业的全球库存协同。
它集成了仪表盘、全球库存管理、订单管理、生产概览和财务摘要等关键模块的简化视图。
该组件旨在提供一个可视化的界面,帮助理解 ERP 系统如何整合不同业务领域的数据,并支持全局决策。
注意:这是一个高度简化的概念演示,并非功能齐全的 ERP 系统。数据和流程都经过了大幅简化。
主要功能
- 模块化导航:
- 提供清晰的侧边栏导航,可在不同 ERP 模块之间轻松切换。
- 导航项包括:仪表盘、全球库存、订单管理、生产概览、财务摘要。
- 仪表盘:
- 显示关键绩效指标 (KPI) 概览,如总库存价值、待处理订单数、低库存 SKU 数量和模拟的准时交货率。
- 包含一个模拟的全球库存分布图,概念性地展示不同区域(美洲、欧洲、亚太)的总库存量。
- 全球库存管理:
- 以表格形式展示跨越多个虚拟仓库(如上海、法兰克福、洛杉矶、新加坡)的详细库存信息。
- 包含 SKU、产品名称、地点、现有库存、可用库存(现有 - 已分配)和库存状态(正常、低库存、缺货)。
- 支持按 SKU、产品名称或地点进行文本搜索。
- 支持按仓库地点进行筛选。
- 订单管理 (简化):
- 展示销售订单和采购订单的列表。
- 包含订单号、类型、客户/供应商、日期、状态(待处理、处理中、已发货、已完成)和金额。
- 支持按订单类型和订单状态进行筛选。
- 生产概览 (简化):
- 展示进行中和计划中的生产订单列表。
- 包含生产单号、产品 SKU、计划数量、已完成数量、状态和计划完成日期。
- 财务摘要 (简化):
- 显示模拟的关键财务数据,如总收入、总支出、利润率和应收账款。
- 数据模拟与动态更新:
- 包含一个"刷新数据"按钮,点击后会模拟库存水平的波动、订单状态的推进、生产进度的更新以及财务数据的变化。
- 系统时间实时显示。
- 界面风格:
- 采用苹果科技风格,注重简洁、清晰和优雅的交互。
- 采用侧边栏 + 主内容区的响应式布局,适应不同屏幕尺寸。
如何使用
- 打开页面: 在浏览器中打开
index.html
。 - 浏览仪表盘: 默认显示仪表盘视图,观察 KPI 和模拟的库存分布图。
- 切换模块: 点击左侧导航栏中的链接(如"全球库存"、"订单管理"等)切换到相应的模块视图。
- 交互操作:
- 在"全球库存"视图中,尝试使用顶部的搜索框输入 SKU 或地点进行搜索,或使用下拉菜单按地点筛选库存记录。
- 在"订单管理"视图中,尝试使用顶部的下拉菜单按订单类型或状态筛选订单。
- 模拟数据更新: 点击右上角的刷新按钮 (),观察各个模块中的数据(如 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();
});