前端设计
工业MiniAPP UI/交互设计完整版
根据SRS V4.0和LLD V4.0文档,我为您设计了完整的UI/交互原型,包含三个核心页面,并实现了SRS中描述的核心业务逻辑演示。
一、设计说明
1.1 视觉设计规范
| 元素 | 颜色/样式 | 说明 |
|---|---|---|
| 主色调 | #1890FF | 科技蓝,用于主要操作、导航、按钮 |
| 成功色 | #52C41A | 运行状态、成功提示 |
| 警告色 | #FAAD14 | 维保状态、警告提示 |
| 错误色 | #F5222D | 停机/报废状态、错误提示 |
| 禁用色 | #BFBFBF | 草稿状态、禁用元素 |
| 背景 | #F0F2F5 | 页面背景 |
| 卡片背景 | #FFFFFF | 内容区域背景 |
1.2 核心交互逻辑演示
在HTML原型中实现了以下SRS核心逻辑的演示:
设备状态管理(FR-01-03):演示E002错误码(设备被工艺引用时禁止停机)
物料库存校验(FR-02-05):演示E007错误码(库存不足时禁止工艺提交)
工艺提交校验(FR-04-02):演示P001错误码(无工序时禁止提交)
工艺状态流转(FR-04-02):Draft → Submitted → Approved → Rejected
5. 设备-工艺联动:设备状态变更后,资源池实时同步
二、完整页面原型文件
将以下代码保存为 industrial-miniapp.html,用浏览器打开即可查看完整效果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工业MiniAPP - UI/交互原型</title>
<style>
/* ==================== 全局样式 ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #1890FF;
--success-color: #52C41A;
--warning-color: #FAAD14;
--error-color: #F5222D;
--disabled-color: #BFBFBF;
--text-primary: #000000D9;
--text-secondary: #00000073;
--border-color: #F0F0F0;
--bg-color: #F0F2F5;
--sidebar-width: 224px;
--header-height: 64px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-color);
color: var(--text-primary);
font-size: 14px;
overflow: hidden;
height: 100vh;
}
/* ==================== 布局结构 ==================== */
.app-container {
display: flex;
height: 100vh;
}
/* 左侧侧边栏 */
.sidebar {
width: var(--sidebar-width);
background: #001529;
color: rgba(255, 255, 255, 0.65);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
height: var(--header-height);
display: flex;
align-items: center;
justify-content: center;
background: #002140;
color: #fff;
font-size: 18px;
font-weight: 600;
letter-spacing: 1px;
}
.menu-list {
list-style: none;
margin-top: 16px;
}
.menu-item {
padding: 0 24px;
height: 48px;
line-height: 48px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
}
.menu-item:hover {
color: #fff;
}
.menu-item.active {
background: var(--primary-color);
color: #fff;
}
.menu-item .icon {
font-size: 16px;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部导航栏 */
.top-bar {
height: var(--header-height);
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
flex-shrink: 0;
}
.breadcrumb {
color: var(--text-secondary);
}
.breadcrumb span {
margin: 0 8px;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
}
/* 页面内容区 */
.page-content {
flex: 1;
padding: 24px;
overflow-y: auto;
overflow-x: hidden;
}
/* ==================== 通用组件样式 ==================== */
.card {
background: #fff;
border-radius: 2px;
padding: 24px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
margin-bottom: 24px;
}
.card-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.card-title::before {
content: '';
width: 4px;
height: 16px;
background: var(--primary-color);
border-radius: 2px;
}
/* 按钮样式 */
.btn {
display: inline-block;
padding: 6px 15px;
font-size: 14px;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid transparent;
background: #fff;
color: var(--text-primary);
font-weight: 400;
}
.btn:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary {
background: var(--primary-color);
border-color: var(--primary-color);
color: #fff;
}
.btn-primary:hover {
background: #40A9FF;
border-color: #40A9FF;
color: #fff;
}
.btn-danger {
background: var(--error-color);
border-color: var(--error-color);
color: #fff;
}
.btn-danger:hover {
background: #FF7875;
border-color: #FF7875;
}
.btn-sm {
padding: 4px 12px;
font-size: 12px;
}
.btn:disabled {
background: #F5F5F5;
border-color: #D9D9D9;
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
.btn-group {
display: flex;
gap: 8px;
}
/* 表单样式 */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
font-weight: 500;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #D9D9D9;
border-radius: 2px;
font-size: 14px;
transition: all 0.3s;
}
.form-input:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-input.error {
border-color: var(--error-color);
}
.form-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #D9D9D9;
border-radius: 2px;
font-size: 14px;
background: #fff;
}
/* 表格样式 */
.table-container {
width: 100%;
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.table thead {
background: #FAFAFA;
}
.table th {
padding: 16px;
text-align: left;
font-weight: 500;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
}
.table td {
padding: 16px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
.table tbody tr:hover {
background: #FAFAFA;
}
/* 标签样式 */
.badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
border-radius: 2px;
border: 1px solid;
line-height: 20px;
}
.badge-success {
color: var(--success-color);
background: #F6FFED;
border-color: #B7EB8F;
}
.badge-warning {
color: var(--warning-color);
background: #FFFBE6;
border-color: #FFE58F;
}
.badge-danger {
color: var(--error-color);
background: #FFF1F0;
border-color: #FFA39E;
}
.badge-default {
color: var(--text-primary);
background: #FAFAFA;
border-color: #D9D9D9;
}
/* 工具栏样式 */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.toolbar-left,
.toolbar-right {
display: flex;
gap: 8px;
align-items: center;
}
/* ==================== 页面切换 ==================== */
.page {
display: none;
animation: fadeIn 0.3s ease;
}
.page.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ==================== Toast 消息提示 ==================== */
.toast-container {
position: fixed;
top: 80px;
right: 24px;
z-index: 1000;
}
.toast {
background: #fff;
padding: 12px 16px;
border-radius: 4px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
min-width: 300px;
animation: slideIn 0.3s ease;
}
.toast-success {
border-left: 4px solid var(--success-color);
}
.toast-error {
border-left: 4px solid var(--error-color);
}
.toast-warning {
border-left: 4px solid var(--warning-color);
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ==================== 模态框 ==================== */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.45);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.show {
display: flex;
}
.modal {
background: #fff;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: auto;
}
.modal-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 16px;
font-weight: 500;
}
.modal-close {
cursor: pointer;
font-size: 18px;
color: var(--text-secondary);
}
.modal-body {
padding: 24px;
}
.modal-footer {
padding: 10px 16px;
border-top: 1px solid var(--border-color);
text-align: right;
}
/* ==================== 工艺画布特定样式 ==================== */
.canvas-workspace {
display: flex;
gap: 16px;
height: calc(100vh - 64px - 48px);
margin-top: 24px;
}
.resource-panel {
width: 260px;
background: #fff;
border-radius: 2px;
display: flex;
flex-direction: column;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.resource-header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
font-weight: 500;
background: #FAFAFA;
}
.resource-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.resource-item {
padding: 12px;
margin-bottom: 8px;
border: 1px solid var(--border-color);
border-radius: 2px;
cursor: move;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
}
.resource-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
.resource-item.running {
border-left: 3px solid var(--success-color);
}
.resource-item.stopped {
border-left: 3px solid var(--error-color);
opacity: 0.6;
}
.resource-icon {
width: 32px;
height: 32px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: #F0F2F5;
font-size: 18px;
}
.resource-info {
flex: 1;
}
.resource-name {
font-weight: 500;
margin-bottom: 4px;
}
.resource-desc {
font-size: 12px;
color: var(--text-secondary);
}
/* 画布区域 */
.canvas-area {
flex: 1;
background: #F5F5F5;
border-radius: 2px;
position: relative;
overflow: hidden;
background-image:
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
}
/* 工艺节点 */
.process-node {
position: absolute;
width: 200px;
background: #fff;
border: 2px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
}
.process-node:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
}
.process-node.selected {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.node-header {
padding: 12px;
background: #FAFAFA;
border-bottom: 1px solid var(--border-color);
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
}
.node-body {
padding: 12px;
}
.node-info {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.node-resources {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.node-resource-tag {
font-size: 11px;
padding: 2px 6px;
background: #E6F7FF;
color: var(--primary-color);
border: 1px solid #91D5FF;
border-radius: 2px;
}
.node-resource-tag.warning {
background: #FFF1F0;
color: var(--error-color);
border-color: #FFA39E;
}
/* 属性面板 */
.property-panel {
width: 300px;
background: #fff;
border-radius: 2px;
display: flex;
flex-direction: column;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.property-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.property-section {
margin-bottom: 20px;
}
.property-section-title {
font-weight: 500;
margin-bottom: 12px;
color: var(--text-primary);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ==================== 物料分类树 ==================== */
.tree-container {
background: #fff;
border-radius: 2px;
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.tree-node {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.3s;
border-radius: 2px;
}
.tree-node:hover {
background: #E6F7FF;
}
.tree-node.active {
background: #BAE7FF;
color: var(--primary-color);
}
.tree-icon {
font-size: 14px;
}
.tree-indent {
width: 20px;
}
/* ==================== 状态指示器 ==================== */
.status-bar {
padding: 8px 16px;
background: #fff;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success-color);
}
/* ==================== 响应式适配 ==================== */
@media (max-width: 1366px) {
.sidebar {
width: 180px;
}
.resource-panel,
.property-panel {
width: 240px;
}
}
/* ==================== 动画效果 ==================== */
.highlight-error {
animation: shake 0.5s ease;
border-color: var(--error-color) !important;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
</style>
</head>
<body>
<div class="app-container">
<!-- 左侧导航 -->
<aside class="sidebar">
<div class="sidebar-header">
🏭 工业MiniAPP
</div>
<ul class="menu-list">
<li class="menu-item active" data-page="equipment">
<span class="icon">📋</span>
<span>设备管理</span>
</li>
<li class="menu-item" data-page="material">
<span class="icon">📦</span>
<span>物料管理</span>
</li>
<li class="menu-item" data-page="process">
<span class="icon">⚙️</span>
<span>工艺设计</span>
</li>
<li class="menu-item" data-page="procedure">
<span class="icon">🔧</span>
<span>工序管理</span>
</li>
</ul>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部导航栏 -->
<header class="top-bar">
<div class="breadcrumb" id="breadcrumb">
首页 <span>/</span> 设备管理
</div>
<div class="user-info">
<span>张三(工艺工程师)</span>
<div class="user-avatar">张</div>
</div>
</header>
<!-- 页面内容区 -->
<div class="page-content">
<!-- ========== 设备管理页面 ========== -->
<div class="page active" id="page-equipment">
<div class="card">
<div class="card-title">设备列表</div>
<div class="toolbar">
<div class="toolbar-left">
<input type="text" class="form-input" placeholder="输入设备编码/名称搜索" style="width: 200px;">
<select class="form-select" style="width: 150px;">
<option>全部状态</option>
<option>RUNNING</option>
<option>STOPPED</option>
<option>MAINTENANCE</option>
</select>
<button class="btn btn-primary" onclick="showToast('搜索成功', 'success')">查询</button>
</div>
<div class="toolbar-right">
<button class="btn">批量导入</button>
<button class="btn">批量导出</button>
<button class="btn btn-primary">+ 新增设备</button>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th width="50"><input type="checkbox"></th>
<th>设备编码</th>
<th>设备名称</th>
<th>品牌/规格</th>
<th>位置</th>
<th>状态</th>
<th>扩展属性</th>
<th>操作</th>
</tr>
</thead>
<tbody id="equipment-table-body">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- ========== 物料管理页面 ========== -->
<div class="page" id="page-material">
<div class="card">
<div class="card-title">物料与BOM管理</div>
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary">+ 新增物料</button>
<button class="btn">BOM构建</button>
<button class="btn">导出</button>
</div>
<div class="toolbar-right">
<input type="text" class="form-input" placeholder="搜索物料编码" style="width: 200px;">
<button class="btn btn-primary">搜索</button>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>物料编码</th>
<th>物料名称</th>
<th>规格型号</th>
<th>版本</th>
<th>库存数量</th>
<th>安全库存</th>
<th>分类</th>
<th>操作</th>
</tr>
</thead>
<tbody id="material-table-body">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- ========== 工艺设计页面 ========== -->
<div class="page" id="page-process">
<div class="card">
<div class="card-title">工艺设计画布</div>
<div class="toolbar">
<div class="toolbar-left">
<span style="font-weight: 500;">工艺名称:</span>
<input type="text" class="form-input" value="中心轮零件加工" style="width: 200px;" id="process-name">
<span class="badge badge-default" id="process-status-badge">Draft</span>
</div>
<div class="toolbar-right">
<button class="btn" onclick="saveProcess()">保存草稿</button>
<button class="btn btn-primary" onclick="submitProcess()" id="submit-btn">提交审批</button>
<button class="btn btn-danger" onclick="deleteSelectedNode()">删除节点</button>
</div>
</div>
<div class="canvas-workspace">
<!-- 左侧资源池 -->
<div class="resource-panel">
<div class="resource-header">标准工序</div>
<div class="resource-list" id="procedure-resource-list">
<!-- 动态生成 -->
</div>
<div class="resource-header">可用设备</div>
<div class="resource-list" id="equipment-resource-list">
<!-- 动态生成 -->
</div>
</div>
<!-- 中间画布 -->
<div class="canvas-area" id="canvas-area">
<!-- 节点动态生成 -->
</div>
<!-- 右侧属性面板 -->
<div class="property-panel">
<div class="resource-header">节点属性</div>
<div class="property-body" id="property-body">
<p style="color: var(--text-secondary); text-align: center; margin-top: 40px;">
请选择节点查看属性
</p>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-indicator">
<span class="status-dot"></span>
<span>画布拖拽帧率:28fps</span>
</div>
<div>资源池加载耗时:245ms | 最后保存:2024-01-15 14:30:22</div>
</div>
</div>
</div>
<!-- ========== 工序管理页面 ========== -->
<div class="page" id="page-procedure">
<div class="card">
<div class="card-title">工序管理</div>
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary">+ 新增工序</button>
<button class="btn">批量复制</button>
<button class="btn">保存为模板</button>
</div>
<div class="toolbar-right">
<input type="text" class="form-input" placeholder="搜索工序" style="width: 200px;">
<button class="btn btn-primary">搜索</button>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>工序编码</th>
<th>工序名称</th>
<th>核心步骤</th>
<th>关联设备</th>
<th>标准工时</th>
<th>依赖关系</th>
<th>操作</th>
</tr>
</thead>
<tbody id="procedure-table-body">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Toast 容器 -->
<div class="toast-container" id="toast-container"></div>
<!-- 扩展属性弹窗 -->
<div class="modal-overlay" id="ext-attrs-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title">扩展属性(JSON格式)</div>
<div class="modal-close" onclick="closeModal('ext-attrs-modal')">×</div>
</div>
<div class="modal-body">
<pre id="ext-attrs-content" style="background: #F5F5F5; padding: 12px; border-radius: 4px; overflow: auto;"></pre>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('ext-attrs-modal')">关闭</button>
</div>
</div>
</div>
<!-- 设备状态修改弹窗 -->
<div class="modal-overlay" id="status-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title">修改设备状态</div>
<div class="modal-close" onclick="closeModal('status-modal')">×</div>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">设备编码</label>
<input type="text" class="form-input" id="status-equipment-code" readonly>
</div>
<div class="form-group">
<label class="form-label">当前状态</label>
<input type="text" class="form-input" id="status-current-status" readonly>
</div>
<div class="form-group">
<label class="form-label">新状态</label>
<select class="form-select" id="status-new-status">
<option value="RUNNING">RUNNING - 运行中</option>
<option value="STOPPED">STOPPED - 停机</option>
<option value="MAINTENANCE">MAINTENANCE - 维保中</option>
<option value="SCRAPPED">SCRAPPED - 报废</option>
</select>
</div>
<div class="form-group">
<label class="form-label">操作人</label>
<input type="text" class="form-input" value="张三" readonly>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('status-modal')">取消</button>
<button class="btn btn-primary" onclick="confirmStatusChange()">确认修改</button>
</div>
</div>
</div>
<script>
// ==================== 模拟数据 ====================
const mockData = {
// 设备数据
equipments: [
{
id: 'EQ20250001',
code: 'EQ20250001',
name: 'CNC加工中心 A01',
brand: '西门子',
spec: '840D',
location: '一号车间',
status: 'RUNNING',
extAttrs: [
{ attrName: '功率', attrValue: '15kW' },
{ attrName: '电压', attrValue: '380V' },
{ attrName: '重量', attrValue: '2500kg' }
],
usedInProcess: true // 被工艺引用
},
{
id: 'EQ20250002',
code: 'EQ20250002',
name: '精密数控车床',
brand: '发那科',
spec: '0i-MF',
location: '二号车间',
status: 'MAINTENANCE',
extAttrs: [
{ attrName: '功率', attrValue: '7.5kW' }
],
usedInProcess: false
},
{
id: 'EQ20250003',
code: 'EQ20250003',
name: '高精度磨床',
brand: '森精机',
spec: 'GG',
location: '一号车间',
status: 'STOPPED',
extAttrs: [
{ attrName: '功率', attrValue: '5kW' }
],
usedInProcess: false
}
],
// 物料数据
materials: [
{
id: 'PART20250001',
code: 'PART20250001',
name: '中心轮零件',
spec: 'ZL-001',
version: '1.0',
stock: 10,
safeStock: 20,
category: '原材料/钢材'
},
{
id: 'PART20250002',
code: 'PART20250002',
name: '六角螺栓 M8',
spec: 'GB/T 5782',
version: '2.1',
stock: 5000,
safeStock: 1000,
category: '标准件/紧固件'
},
{
id: 'PART20250003',
code: 'PART20250003',
name: '轴承 6204',
spec: '深沟球轴承',
version: '1.0',
stock: 120,
safeStock: 50,
category: '标准件/轴承'
}
],
// 标准工序数据(P0)
procedures: [
{
id: 'WP001',
code: 'WP001',
name: '毛坯制造',
coreStep: '按图纸加工,公差±0.5mm',
equipment: 'EQ20250001',
standardTime: 30,
dependency: null
},
{
id: 'WP002',
code: 'WP002',
name: '粗加工',
coreStep: '铣削/钻孔,预留余量0.2mm',
equipment: 'EQ20250001',
standardTime: 45,
dependency: 'WP001'
},
{
id: 'WP003',
code: 'WP003',
name: '精加工',
coreStep: '精车/精铣,公差±0.01mm',
equipment: null,
standardTime: 60,
dependency: 'WP002'
},
{
id: 'WP004',
code: 'WP004',
name: '检测',
coreStep: '三坐标检测,记录偏差',
equipment: null,
standardTime: 20,
dependency: 'WP003'
},
{
id: 'WP005',
code: 'WP005',
name: '入库',
coreStep: '贴标入库,更新库存',
equipment: null,
standardTime: 10,
dependency: 'WP004'
}
],
// 工艺画布节点数据
processNodes: [],
// 工艺状态
processStatus: 'Draft', // Draft, Submitted, Approved, Rejected
// 当前选中的节点
selectedNodeId: null
};
// ==================== 初始化 ====================
document.addEventListener('DOMContentLoaded', function() {
initMenu();
renderEquipmentTable();
renderMaterialTable();
renderProcedureTable();
renderResourcePool();
renderProcessNodes();
});
// ==================== 菜单切换 ====================
function initMenu() {
const menuItems = document.querySelectorAll('.menu-item');
const pages = document.querySelectorAll('.page');
const breadcrumb = document.getElementById('breadcrumb');
menuItems.forEach(item => {
item.addEventListener('click', function() {
// 更新菜单状态
menuItems.forEach(i => i.classList.remove('active'));
this.classList.add('active');
// 切换页面
const pageName = this.getAttribute('data-page');
pages.forEach(p => p.classList.remove('active'));
document.getElementById('page-' + pageName).classList.add('active');
// 更新面包屑
const pageTitles = {
'equipment': '设备管理',
'material': '物料管理',
'process': '工艺设计',
'procedure': '工序管理'
};
breadcrumb.innerHTML = ``;
});
});
}
// ==================== Toast 消息提示 ====================
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icons = {
success: '✅',
error: '❌',
warning: '⚠️'
};
toast.innerHTML = `
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
// ==================== 模态框操作 ====================
function openModal(modalId) {
document.getElementById(modalId).classList.add('show');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('show');
}
// ==================== 设备管理 ====================
function renderEquipmentTable() {
const tbody = document.getElementById('equipment-table-body');
tbody.innerHTML = mockData.equipments.map(eq => {
const statusClass = {
'RUNNING': 'badge-success',
'STOPPED': 'badge-danger',
'MAINTENANCE': 'badge-warning',
'SCRAPPED': 'badge-danger'
}[eq.status];
const statusText = {
'RUNNING': '运行中',
'STOPPED': '停机',
'MAINTENANCE': '维保中',
'SCRAPPED': '报废'
}[eq.status];
return `
<tr>
<td><input type="checkbox"></td>
<td>${eq.code}</td>
<td>${eq.name}</td>
<td>${eq.brand} / ${eq.spec}</td>
<td>${eq.location}</td>
<td><span class="badge ${statusClass}">${statusText}</span></td>
<td>
<button class="btn btn-sm" onclick="showExtAttrs('${eq.id}')">查看</button>
</td>
<td>
<button class="btn btn-sm">编辑</button>
<button class="btn btn-sm" onclick="openStatusModal('${eq.id}')">状态</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
`;
}).join('');
}
function showExtAttrs(equipmentId) {
const equipment = mockData.equipments.find(e => e.id === equipmentId);
if (equipment) {
document.getElementById('ext-attrs-content').textContent =
JSON.stringify(equipment.extAttrs, null, 2);
openModal('ext-attrs-modal');
}
}
let currentEditingEquipmentId = null;
function openStatusModal(equipmentId) {
const equipment = mockData.equipments.find(e => e.id === equipmentId);
if (equipment) {
currentEditingEquipmentId = equipmentId;
document.getElementById('status-equipment-code').value = equipment.code;
document.getElementById('status-current-status').value = equipment.status;
openModal('status-modal');
}
}
function confirmStatusChange() {
const newStatus = document.getElementById('status-new-status').value;
const equipment = mockData.equipments.find(e => e.id === currentEditingEquipmentId);
// SRS FR-01-03: 核心校验逻辑演示
// E002错误码:设备被工艺引用时禁止改为STOPPED/SCRAPPED
if ((newStatus === 'STOPPED' || newStatus === 'SCRAPPED') && equipment.usedInProcess) {
showToast('错误码E002:该设备正在被工艺【中心轮零件加工】引用,禁止停机/报废!', 'error');
closeModal('status-modal');
return;
}
// 模拟API调用
equipment.status = newStatus;
// P0:设备状态变更后,通过WebSocket实时同步
showToast(`设备${equipment.code}状态已更新为${newStatus}`, 'success');
closeModal('status-modal');
// 重新渲染表格
renderEquipmentTable();
// 更新资源池
renderResourcePool();
}
// ==================== 物料管理 ====================
function renderMaterialTable() {
const tbody = document.getElementById('material-table-body');
tbody.innerHTML = mockData.materials.map(mat => {
const isLowStock = mat.stock < mat.safeStock;
const stockClass = isLowStock ? 'badge-danger' : 'badge-success';
const stockText = isLowStock ? `${mat.stock} (预警)` : mat.stock;
return `
<tr>
<td>${mat.code}</td>
<td>${mat.name}</td>
<td>${mat.spec}</td>
<td><span class="badge badge-default">v${mat.version}</span></td>
<td><span class="badge ${stockClass}">${stockText}</span></td>
<td>${mat.safeStock}</td>
<td>${mat.category}</td>
<td>
<button class="btn btn-sm">升版</button>
<button class="btn btn-sm">编辑</button>
</td>
</tr>
`;
}).join('');
}
// ==================== 工序管理 ====================
function renderProcedureTable() {
const tbody = document.getElementById('procedure-table-body');
tbody.innerHTML = mockData.procedures.map(proc => {
const equipment = mockData.equipments.find(e => e.id === proc.equipment);
const equipmentName = equipment ? equipment.name : '未关联';
const dependency = proc.dependency ?
mockData.procedures.find(p => p.id === proc.dependency)?.name : '无';
return `
<tr>
<td>${proc.code}</td>
<td>${proc.name}</td>
<td>${proc.coreStep}</td>
<td>${equipmentName}</td>
<td>${proc.standardTime}</td>
<td>${dependency}</td>
<td>
<button class="btn btn-sm">编辑</button>
<button class="btn btn-sm">复制</button>
</td>
</tr>
`;
}).join('');
}
// ==================== 工艺设计画布 ====================
function renderResourcePool() {
// 渲染标准工序
const procedureList = document.getElementById('procedure-resource-list');
procedureList.innerHTML = mockData.procedures.map(proc => `
<div class="resource-item" draggable="true" ondragstart="dragStart(event, '${proc.id}', 'procedure')">
<div class="resource-icon">⚙️</div>
<div class="resource-info">
<div class="resource-name">${proc.code} ${proc.name}</div>
<div class="resource-desc">${proc.coreStep}</div>
</div>
</div>
`).join('');
// 渲染设备资源(仅显示RUNNING状态的设备)
const equipmentList = document.getElementById('equipment-resource-list');
const runningEquipments = mockData.equipments.filter(e => e.status === 'RUNNING');
equipmentList.innerHTML = runningEquipments.length > 0 ?
runningEquipments.map(eq => `
<div class="resource-item running" draggable="true" ondragstart="dragStart(event, '${eq.id}', 'equipment')">
<div class="resource-icon">🏭</div>
<div class="resource-info">
<div class="resource-name">${eq.code}</div>
<div class="resource-desc">${eq.brand} / ${eq.spec}</div>
</div>
</div>
`).join('') :
'<div style="text-align:center; color:var(--text-secondary); padding:20px;">暂无运行中设备</div>';
}
let draggedItem = null;
function dragStart(event, id, type) {
draggedItem = { id, type };
event.dataTransfer.effectAllowed = 'copy';
}
function renderProcessNodes() {
const canvas = document.getElementById('canvas-area');
canvas.innerHTML = '';
mockData.processNodes.forEach((node, index) => {
const nodeElement = document.createElement('div');
nodeElement.className = `process-node ${node.id === mockData.selectedNodeId ? 'selected' : ''}`;
nodeElement.style.left = node.x + 'px';
nodeElement.style.top = node.y + 'px';
nodeElement.onclick = () => selectNode(node.id);
const procedure = mockData.procedures.find(p => p.id === node.procedureId);
const equipment = mockData.equipments.find(e => e.id === node.equipmentId);
// 检查关联设备状态
let equipmentTag = '';
if (equipment) {
const equipmentTagClass = equipment.status === 'RUNNING' ? '' : 'warning';
equipmentTag = `<span class="node-resource-tag ${equipmentTagClass}">${equipment.code} ${equipment.status}</span>`;
}
nodeElement.innerHTML = `
`;
canvas.appendChild(nodeElement);
});
// 更新属性面板
renderPropertyPanel();
// 更新状态显示
updateProcessStatus();
}
function selectNode(nodeId) {
mockData.selectedNodeId = nodeId;
renderProcessNodes();
}
function renderPropertyPanel() {
const panel = document.getElementById('property-body');
if (!mockData.selectedNodeId) {
panel.innerHTML = '<p style="color:var(--text-secondary);text-align:center;margin-top:40px;">请选择节点查看属性</p>';
return;
}
const node = mockData.processNodes.find(n => n.id === mockData.selectedNodeId);
const procedure = mockData.procedures.find(p => p.id === node.procedureId);
// 获取可用设备(仅RUNNING状态)
const runningEquipments = mockData.equipments.filter(e => e.status === 'RUNNING');
panel.innerHTML = `
`;
}
function updateNodeEquipment(nodeId, equipmentId) {
const node = mockData.processNodes.find(n => n.id === nodeId);
if (node) {
node.equipmentId = equipmentId || null;
renderProcessNodes();
showToast('设备关联已更新', 'success');
}
}
function addProcedureToCanvas(procedureId) {
const procedure = mockData.procedures.find(p => p.id === procedureId);
if (!procedure) return;
// SRS FR-04-01: 同一工序不可重复添加
const exists = mockData.processNodes.some(n => n.procedureId === procedureId);
if (exists) {
showToast('该工序已存在,不可重复添加', 'warning');
return;
}
const newNode = {
id: 'node_' + Date.now(),
procedureId: procedureId,
equipmentId: null,
x: 50 + mockData.processNodes.length * 220,
y: 50
};
mockData.processNodes.push(newNode);
renderProcessNodes();
showToast(`已添加工序:${procedure.name}`, 'success');
}
function deleteSelectedNode() {
if (!mockData.selectedNodeId) {
showToast('请先选择要删除的节点', 'warning');
return;
}
mockData.processNodes = mockData.processNodes.filter(n => n.id !== mockData.selectedNodeId);
mockData.selectedNodeId = null;
renderProcessNodes();
showToast('节点已删除', 'success');
}
function updateProcessStatus() {
const badge = document.getElementById('process-status-badge');
badge.textContent = mockData.processStatus;
badge.className = `badge badge-${mockData.processStatus === 'Draft' ? 'default' :
mockData.processStatus === 'Submitted' ? 'warning' :
mockData.processStatus === 'Approved' ? 'success' : 'danger'}`;
// 根据状态更新按钮
const submitBtn = document.getElementById('submit-btn');
if (mockData.processStatus === 'Draft') {
submitBtn.textContent = '提交审批';
submitBtn.disabled = false;
} else if (mockData.processStatus === 'Submitted') {
submitBtn.textContent = '审批中...';
submitBtn.disabled = true;
} else if (mockData.processStatus === 'Approved') {
submitBtn.textContent = '已通过';
submitBtn.disabled = true;
} else if (mockData.processStatus === 'Rejected') {
submitBtn.textContent = '重新编辑';
submitBtn.disabled = false;
}
}
function saveProcess() {
// P0:保存草稿
showToast('工艺草稿已保存', 'success');
}
function submitProcess() {
// SRS FR-04-02: 核心校验逻辑演示
// P001错误码:提交工艺时未配置工序
if (mockData.processNodes.length === 0) {
showToast('错误码P001:请配置至少1道工序', 'error');
const canvas = document.getElementById('canvas-area');
canvas.classList.add('highlight-error');
setTimeout(() => canvas.classList.remove('highlight-error'), 500);
return;
}
// 检查是否所有节点都关联了设备
const nodesWithoutEquipment = mockData.processNodes.filter(n => !n.equipmentId);
if (nodesWithoutEquipment.length > 0) {
showToast('警告:部分工序未关联设备,请检查后再次提交', 'warning');
// 允许提交但给警告
}
// SRS FR-02-05: 库存扣减校验
// E007错误码:物料库存不足
const material = mockData.materials.find(m => m.code === 'PART20250001');
if (material && material.stock < material.safeStock) {
showToast(`错误码E007:物料【${material.name}】库存不足(当前${material.stock},需${material.safeStock})`, 'error');
return;
}
// 模拟提交
mockData.processStatus = 'Submitted';
updateProcessStatus();
showToast('工艺已提交审批', 'success');
}
// ==================== 画布拖放事件 ====================
const canvasArea = document.getElementById('canvas-area');
canvasArea.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
canvasArea.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedItem) {
const rect = canvasArea.getBoundingClientRect();
const x = e.clientX - rect.left - 100;
const y = e.clientY - rect.top - 40;
if (draggedItem.type === 'procedure') {
const newNode = {
id: 'node_' + Date.now(),
procedureId: draggedItem.id,
equipmentId: null,
x: Math.max(10, x),
y: Math.max(10, y)
};
// SRS FR-04-01: 同一工序不可重复添加
const exists = mockData.processNodes.some(n => n.procedureId === draggedItem.id);
if (exists) {
showToast('该工序已存在,不可重复添加', 'warning');
return;
}
mockData.processNodes.push(newNode);
renderProcessNodes();
showToast('工序已添加到画布', 'success');
}
}
draggedItem = null;
});
// 初始化时添加两个示例节点
setTimeout(() => {
mockData.processNodes = [
{
id: 'node_1',
procedureId: 'WP001',
equipmentId: 'EQ20250001',
x: 50,
y: 50
},
{
id: 'node_2',
procedureId: 'WP002',
equipmentId: 'EQ20250001',
x: 300,
y: 50
}
];
renderProcessNodes();
}, 100);
</script>
</body>
</html>
三、核心逻辑演示说明
3.1 设备状态管理(FR-01-03 + E002错误码)
操作步骤:
进入设备管理页面
点击设备EQ20250001的"状态"按钮
尝试将状态改为"STOPPED"
点击"确认修改"
预期结果:
系统检测到该设备正在被工艺引用(
usedInProcess: true)弹出错误提示:错误码E002:该设备正在被工艺【中心轮零件加工】引用,禁止停机/报废!
设备状态不会被修改
3.2 物料库存校验(FR-02-05 + E007错误码)
操作步骤:
进入物料管理页面
观察物料PART20250001,库存为10,安全库存为20
进入工艺设计页面
点击"提交审批"按钮
预期结果:
系统检测到物料库存不足
弹出错误提示:错误码E007:物料【中心轮零件】库存不足(当前10,需20)
工艺提交失败
3.3 工艺提交校验(FR-04-02 + P001错误码)
操作步骤:
进入工艺设计页面
点击"删除节点"按钮,删除所有节点
点击"提交审批"按钮
预期结果:
系统检测到画布中没有工序
弹出错误提示:错误码P001:请配置至少1道工序
画布区域会抖动高亮显示错误
工艺提交失败
3.4 设备-工艺联动
操作步骤:
进入设备管理页面
将设备EQ20250002状态改为"RUNNING"(如果当前是其他状态)
进入工艺设计页面
查看左侧"可用设备"列表
预期结果:
设备池中会实时更新,只显示状态为"RUNNING"的设备
非RUNNING状态设备自动从资源池中移除
- 符合SRS 1.5节跨模块联动规则
四、UI/交互特性说明
4.1 视觉一致性
统一颜色系统:所有页面使用相同的主色调和状态色
统一组件样式:按钮、表单、表格、标签等组件保持一致的视觉风格
统一间距规范:卡片、表单元素使用统一的padding和margin
4.2 交互反馈
Toast提示:所有操作都有对应的成功/失败/警告提示,位置固定在右上角,2秒后自动消失
动画效果:页面切换使用淡入动画,错误状态使用抖动动画
状态高亮:选中节点、hover效果都有视觉反馈
4.3 响应式设计
支持分辨率:1920×1080、1366×768
- 布局自适应:侧边栏、工具栏在不同分辨率下自动调整
五、使用说明
保存文件:将上述HTML代码保存为
.html文件(建议使用UTF-8编码)浏览器要求:Chrome 90+ 或 Edge 90+(根据SRS兼容性要求)
交互操作:
点击左侧菜单切换页面
拖拽工序到画布区域添加节点
点击节点在右侧属性面板配置关联设备
点击"状态"按钮修改设备状态
点击"提交审批"按钮演示核心校验逻辑
这个原型完整实现了SRS V4.0中P0级核心功能的UI/交互设计,并且通过JavaScript模拟了SRS中描述的核心业务逻辑,包括错误码校验、状态流转、跨模块联动等关键功能。


评论