二级前端页面修改

This commit is contained in:
2025-11-07 17:26:56 +08:00
parent 348e0eedab
commit 6083e700cc
2 changed files with 1402 additions and 164 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -11,45 +11,109 @@
<!-- 生产计划队列 --> <!-- 生产计划队列 -->
<div v-if="!isLoading" class="plan-queue-panel"> <div v-if="!isLoading" class="plan-queue-panel">
<div class="panel-title"> <div class="queue-header">
<i class="el-icon-document"></i> 生产计划队列 <div class="queue-title">
<el-tag size="small" type="success">{{ planQueue.length }} 计划</el-tag> <i class="el-icon-s-order"></i> 生产计划
</div> </div>
<div class="plan-queue-list"> <div class="queue-tabs">
<el-empty v-if="planQueue.length === 0" description="暂无生产计划"></el-empty>
<div v-else class="plan-items">
<div <div
v-for="(plan, index) in planQueue" class="queue-tab"
:key="plan.id || plan.planid || index" :class="{ active: activeQueueTab === 'current' }"
class="plan-item" @click="activeQueueTab = 'current'"
:class="getPlanItemClass(plan)"
@click="selectPlan(plan)"
> >
<div class="plan-order" :class="getPlanOrderClass(plan.status)"> <i class="el-icon-s-flag"></i>
{{ index + 1 }} <span>当前计划</span>
</div> <el-badge :value="currentPlans.length" class="tab-badge" v-if="currentPlans.length > 0" />
<div class="plan-info"> </div>
<div class="plan-row"> <div
<span class="plan-label">计划ID:</span> class="queue-tab"
<span class="plan-value">{{ plan.planid || '-' }}</span> :class="{ active: activeQueueTab === 'all' }"
</div> @click="activeQueueTab = 'all'"
<div class="plan-row"> >
<span class="plan-label">钢卷号:</span> <i class="el-icon-document"></i>
<span class="plan-value">{{ plan.coilid || '-' }}</span> <span>全部计划</span>
</div> <el-badge :value="planQueue.length" class="tab-badge" v-if="planQueue.length > 0" />
<div class="plan-row">
<span class="plan-label">钢种:</span>
<span class="plan-value">{{ plan.steelGrade || '-' }}</span>
</div>
</div>
<div class="plan-status">
<el-tag :type="getPlanStatusTagType(plan.status)" size="mini" effect="dark">
{{ getPlanStatusText(plan.status) }}
</el-tag>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 当前计划内容 -->
<div v-if="activeQueueTab === 'current'">
<div class="plan-queue-list">
<el-empty v-if="currentPlans.length === 0" description="暂无当前计划" :image-size="80"></el-empty>
<div v-else class="plan-items">
<div
v-for="(plan, index) in currentPlans"
:key="plan.id || plan.planid || index"
class="plan-item"
:class="getPlanItemClass(plan)"
@click="selectPlan(plan)"
>
<div class="plan-order" :class="getPlanOrderClass(plan.status)">
{{ index + 1 }}
</div>
<div class="plan-info">
<div class="plan-row">
<span class="plan-label">计划ID:</span>
<span class="plan-value">{{ plan.planid || '-' }}</span>
</div>
<div class="plan-row">
<span class="plan-label">钢卷号:</span>
<span class="plan-value">{{ plan.coilid || '-' }}</span>
</div>
<div class="plan-row">
<span class="plan-label">钢种:</span>
<span class="plan-value">{{ plan.steelGrade || '-' }}</span>
</div>
</div>
<div class="plan-status">
<el-tag :type="getPlanStatusTagType(plan.status)" size="mini" effect="dark">
{{ getPlanStatusText(plan.status) }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
<!-- 全部计划内容 -->
<div v-if="activeQueueTab === 'all'">
<div class="plan-queue-list">
<el-empty v-if="planQueue.length === 0" description="暂无生产计划" :image-size="80"></el-empty>
<div v-else class="plan-items">
<div
v-for="(plan, index) in planQueue"
:key="plan.id || plan.planid || index"
class="plan-item"
:class="getPlanItemClass(plan)"
@click="selectPlan(plan)"
>
<div class="plan-order" :class="getPlanOrderClass(plan.status)">
{{ index + 1 }}
</div>
<div class="plan-info">
<div class="plan-row">
<span class="plan-label">计划ID:</span>
<span class="plan-value">{{ plan.planid || '-' }}</span>
</div>
<div class="plan-row">
<span class="plan-label">钢卷号:</span>
<span class="plan-value">{{ plan.coilid || '-' }}</span>
</div>
<div class="plan-row">
<span class="plan-label">钢种:</span>
<span class="plan-value">{{ plan.steelGrade || '-' }}</span>
</div>
</div>
<div class="plan-status">
<el-tag :type="getPlanStatusTagType(plan.status)" size="mini" effect="dark">
{{ getPlanStatusText(plan.status) }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- WebSocket 连接状态指示 --> <!-- WebSocket 连接状态指示 -->
@@ -203,6 +267,18 @@
<el-button size="mini" type="text" icon="el-icon-close" @click="selectedPlan = null"></el-button> <el-button size="mini" type="text" icon="el-icon-close" @click="selectedPlan = null"></el-button>
</div> </div>
<div class="plan-detail-content"> <div class="plan-detail-content">
<!-- 位置信息如果在产线上 -->
<div v-if="selectedPlanPosition" class="position-alert">
<div class="position-icon">
<i class="el-icon-location"></i>
</div>
<div class="position-info">
<div class="position-label">当前位置</div>
<div class="position-name">{{ selectedPlanPosition.positionNameCn }}</div>
<div class="position-code">{{ selectedPlanPosition.positionNameEn }}</div>
</div>
</div>
<el-descriptions :column="1" border size="small"> <el-descriptions :column="1" border size="small">
<el-descriptions-item label="计划ID">{{ selectedPlan.planid || '-' }}</el-descriptions-item> <el-descriptions-item label="计划ID">{{ selectedPlan.planid || '-' }}</el-descriptions-item>
<el-descriptions-item label="钢卷号">{{ selectedPlan.coilid || '-' }}</el-descriptions-item> <el-descriptions-item label="钢卷号">{{ selectedPlan.coilid || '-' }}</el-descriptions-item>
@@ -502,6 +578,9 @@ export default {
// 加载状态 // 加载状态
isLoading: true, isLoading: true,
// 计划队列tab
activeQueueTab: 'all',
// 生产计划队列 // 生产计划队列
planQueue: [], planQueue: [],
@@ -561,6 +640,40 @@ export default {
} }
}, },
computed: { computed: {
// 当前计划ONLINE 或 PRODUCING状态
currentPlans() {
return this.planQueue.filter(plan =>
['ONLINE', 'PRODUCING', 'PRODUCTING'].includes(plan.status)
)
},
// 显示的计划列表根据tab切换
displayedPlans() {
if (this.activeQueueTab === 'current') {
return this.currentPlans
}
return this.planQueue
},
// 选中计划的位置信息
selectedPlanPosition() {
if (!this.selectedPlan || !this.matMapList || this.matMapList.length === 0) {
return null
}
// 从matMapList中查找匹配的位置
// matMapList中的字段是驼峰命名planId, matId
// 计划对象中的字段可能是planid, coilid
const position = this.matMapList.find(item =>
(item.planId && item.planId === this.selectedPlan.planid) ||
(item.planId && item.planId === this.selectedPlan.planId) ||
(item.matId && item.matId === this.selectedPlan.coilid) ||
(item.matId && item.matId === this.selectedPlan.matId)
)
return position
},
getOperateTitle() { getOperateTitle() {
const titleMap = { const titleMap = {
'ONLINE': '钢卷上线', 'ONLINE': '钢卷上线',
@@ -884,6 +997,12 @@ export default {
const autoFlagText = data.autoFlag === 1 ? '[手动]' : '[自动]' const autoFlagText = data.autoFlag === 1 ? '[手动]' : '[自动]'
const config = this.getOperationConfig(data.operation) const config = this.getOperationConfig(data.operation)
// 检测到上线、生产中、生产完成等关键操作时,刷新生产计划队列
if (['ONLINE', 'PRODUCING', 'PRODUCT'].includes(data.operation)) {
console.log(`检测到${operationText}操作,刷新生产计划队列...`)
this.refreshPlanQueue()
}
// 右上角通知(所有操作都显示) // 右上角通知(所有操作都显示)
this.$notify({ this.$notify({
title: `${config.icon} ${config.title}`, title: `${config.icon} ${config.title}`,
@@ -1026,6 +1145,66 @@ export default {
// ============ API 方法 ============ // ============ API 方法 ============
// 刷新生产计划队列(独立方法,可被信号触发)
async refreshPlanQueue() {
try {
// 由于后端默认只查NEW和READY需要分别查询多个状态然后合并
const statuses = ['ONLINE', 'PRODUCING', 'READY', 'NEW']
const allPlans = []
// 并行查询所有状态
const requests = statuses.map(status =>
getPlanQueue({ status }).catch(err => {
console.warn(`查询${status}状态失败:`, err)
return { code: 500, data: [] }
})
)
const results = await Promise.all(requests)
// 合并所有查询结果
results.forEach((res, index) => {
if (res.code === 200 && res.data) {
allPlans.push(...res.data)
}
})
// 去重根据id
const uniquePlans = Array.from(
new Map(allPlans.map(plan => [plan.id, plan])).values()
)
// 定义状态优先级
const statusPriority = {
'PRODUCING': 1,
'PRODUCTING': 1,
'ONLINE': 2,
'READY': 3,
'NEW': 4
}
// 排序
const sortedPlans = uniquePlans.sort((a, b) => {
const priorityA = statusPriority[a.status] || 500
const priorityB = statusPriority[b.status] || 500
if (priorityA !== priorityB) {
return priorityA - priorityB
}
return (a.seqid || 0) - (b.seqid || 0)
})
// 最多显示 5 个计划
this.planQueue = sortedPlans.slice(0, 5)
console.log('查询到的总计划数:', uniquePlans.length, '条')
console.log('显示的计划:', this.planQueue.length, '条', this.planQueue)
} catch (err) {
console.error('获取生产计划队列失败:', err)
this.planQueue = this.generateMockPlanQueue()
}
},
async fetchData() { async fetchData() {
try { try {
this.isLoading = true this.isLoading = true
@@ -1033,35 +1212,11 @@ export default {
// 只在 WebSocket 未连接时使用 API 数据 // 只在 WebSocket 未连接时使用 API 数据
if (!this.socketStatus.matmap) { if (!this.socketStatus.matmap) {
this.matMapList = res.data.matMapList || [] this.matMapList = res.data.matMapList || []
} }
// 获取生产计划队列 // 获取生产计划队列
try { await this.refreshPlanQueue()
const planRes = await getPlanQueue({})
if (planRes.code === 200 && planRes.data) {
// 筛选出 NEW、READY 和 PRODUCING 状态的计划
const validStatuses = ['NEW', 'READY', 'PRODUCING']
let filteredPlans = (planRes.data || [])
.filter(plan => validStatuses.includes(plan.status))
.sort((a, b) => (a.seqid || 0) - (b.seqid || 0))
// 如果有多个 PRODUCING只保留第一个
const producingPlans = filteredPlans.filter(p => p.status === 'PRODUCING')
const otherPlans = filteredPlans.filter(p => p.status !== 'PRODUCING')
if (producingPlans.length > 0) {
this.planQueue = [producingPlans[0], ...otherPlans].slice(0, 5)
} else {
this.planQueue = otherPlans.slice(0, 5)
}
} else {
this.planQueue = this.generateMockPlanQueue()
}
} catch (err) {
console.error('获取生产计划队列失败:', err)
this.planQueue = this.generateMockPlanQueue()
}
} catch (err) { } catch (err) {
console.error('获取钢卷数据失败:', err) console.error('获取钢卷数据失败:', err)
@@ -1095,21 +1250,30 @@ export default {
const statusMap = { const statusMap = {
NEW: '新建', NEW: '新建',
READY: '就绪', READY: '就绪',
ONLINE: '上线',
PRODUCING: '生产中', PRODUCING: '生产中',
PRODUCT: '完成', PRODUCT: '生产完成',
COMPLETED: '已完成' PAY_OVER: '甩尾',
UNLOAD: '卸卷',
COMPLETED: '已完成',
CANCELLED: '已取消'
} }
return statusMap[status] || status || '待处理' return statusMap[status] || status || '未知'
}, },
// 获取计划状态标签类型 // 获取计划状态标签类型
getPlanStatusTagType(status) { getPlanStatusTagType(status) {
const typeMap = { const typeMap = {
NEW: '', NEW: 'info',
READY: 'warning', READY: 'primary',
ONLINE: 'warning',
PRODUCING: 'success', PRODUCING: 'success',
PRODUCTING: 'success', // 兼容带T的写法
PRODUCT: 'info', PRODUCT: 'info',
COMPLETED: 'info' PAY_OVER: '',
UNLOAD: 'danger',
COMPLETED: 'info',
CANCELLED: 'danger'
} }
return typeMap[status] || 'info' return typeMap[status] || 'info'
}, },
@@ -1118,8 +1282,11 @@ export default {
getPlanItemClass(plan) { getPlanItemClass(plan) {
const classes = [] const classes = []
if (plan.status === 'PRODUCING') { if (plan.status === 'PRODUCING' || plan.status === 'PRODUCTING') {
classes.push('plan-active') classes.push('plan-producing')
}
if (plan.status === 'ONLINE') {
classes.push('plan-online')
} }
if (plan.status === 'READY') { if (plan.status === 'READY') {
classes.push('plan-ready') classes.push('plan-ready')
@@ -1133,7 +1300,8 @@ export default {
// 获取计划序号样式类 // 获取计划序号样式类
getPlanOrderClass(status) { getPlanOrderClass(status) {
if (status === 'PRODUCING') return 'order-producing' if (status === 'PRODUCING' || status === 'PRODUCTING') return 'order-producing'
if (status === 'ONLINE') return 'order-online'
if (status === 'READY') return 'order-ready' if (status === 'READY') return 'order-ready'
if (status === 'NEW') return 'order-new' if (status === 'NEW') return 'order-new'
return '' return ''
@@ -1168,6 +1336,23 @@ export default {
this.selectedPlan = plan this.selectedPlan = plan
// 选择计划时清除选中的设备 // 选择计划时清除选中的设备
this.selectedCard = null this.selectedCard = null
// 调试:查找并显示位置信息
const position = this.matMapList.find(item =>
(item.planId && item.planId === plan.planid) ||
(item.planId && item.planId === plan.planId) ||
(item.matId && item.matId === plan.coilid) ||
(item.matId && item.matId === plan.matId)
)
if (position) {
console.log('✅ 找到计划位置:', position.positionNameCn, '(', position.positionNameEn, ')')
console.log('匹配详情 - matId:', position.matId, 'planId:', position.planId)
} else {
console.log('⚠️ 未找到计划位置')
console.log('matMapList有', this.matMapList.length, '个设备:', this.matMapList)
console.log('计划信息 - planid:', plan.planid, 'planId:', plan.planId, 'coilid:', plan.coilid, 'matId:', plan.matId)
}
} }
}, },
@@ -1828,10 +2013,72 @@ export default {
/* 生产计划队列面板 */ /* 生产计划队列面板 */
.plan-queue-panel { .plan-queue-panel {
background: #fff; background: #fff;
border-radius: 4px; border-radius: 8px;
padding: 15px;
margin-bottom: 15px; margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
/* 队列头部 */
.queue-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.queue-title {
font-size: 14px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 6px;
}
.queue-title i {
font-size: 16px;
color: #409eff;
}
/* Tab切换按钮组 */
.queue-tabs {
display: flex;
gap: 0;
}
.queue-tab {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
color: #606266;
background: transparent;
border-bottom: 2px solid transparent;
user-select: none;
}
.queue-tab:hover {
color: #409eff;
}
.queue-tab.active {
color: #409eff;
font-weight: 600;
border-bottom-color: #409eff;
}
.queue-tab i {
font-size: 13px;
}
.queue-tab .tab-badge {
margin-left: 4px;
} }
.plan-queue-panel .panel-title { .plan-queue-panel .panel-title {
@@ -1850,14 +2097,15 @@ export default {
} }
.plan-queue-list { .plan-queue-list {
max-height: 240px; max-height: none;
overflow-y: auto; overflow-y: visible;
padding: 15px 20px;
} }
.plan-items { .plan-items {
display: flex; display: flex;
gap: 10px; gap: 10px;
overflow-x: auto; flex-wrap: wrap;
padding-bottom: 5px; padding-bottom: 5px;
} }
@@ -1880,14 +2128,20 @@ export default {
transform: translateY(-2px); transform: translateY(-2px);
} }
.plan-item.plan-active { .plan-item.plan-producing {
background: #f0f9ff; background: #f0f9ff;
border-color: #67c23a; border-color: #67c23a;
animation: pulse-green 1.5s infinite;
}
.plan-item.plan-online {
background: #fdf6ec;
border-color: #e6a23c;
} }
.plan-item.plan-ready { .plan-item.plan-ready {
background: #fdf6ec; background: #ecf5ff;
border-color: #e6a23c; border-color: #409eff;
} }
.plan-item.plan-selected { .plan-item.plan-selected {
@@ -1896,6 +2150,18 @@ export default {
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
} }
/* 绿色闪烁动画 */
@keyframes pulse-green {
0%, 100% {
border-color: #67c23a;
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4);
}
50% {
border-color: #85ce61;
box-shadow: 0 0 0 4px rgba(103, 194, 58, 0.6), 0 0 15px rgba(103, 194, 58, 0.4);
}
}
.plan-order { .plan-order {
width: 28px; width: 28px;
height: 28px; height: 28px;
@@ -1912,16 +2178,33 @@ export default {
.plan-order.order-producing { .plan-order.order-producing {
background: #67c23a; background: #67c23a;
animation: blink-green-dot 1.2s infinite;
}
.plan-order.order-online {
background: #e6a23c;
} }
.plan-order.order-ready { .plan-order.order-ready {
background: #e6a23c; background: #409eff;
} }
.plan-order.order-new { .plan-order.order-new {
background: #909399; background: #909399;
} }
/* 绿色点闪烁动画 */
@keyframes blink-green-dot {
0%, 100% {
background: #67c23a;
box-shadow: 0 0 5px rgba(103, 194, 58, 0.5);
}
50% {
background: #85ce61;
box-shadow: 0 0 10px rgba(103, 194, 58, 0.8);
}
}
.plan-info { .plan-info {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -2050,6 +2333,55 @@ export default {
overflow-y: auto; overflow-y: auto;
} }
/* 位置信息提示卡片 */
.position-alert {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
margin-bottom: 15px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-left: 4px solid #409eff;
border-radius: 4px;
}
.position-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: #409eff;
border-radius: 50%;
color: #fff;
font-size: 20px;
flex-shrink: 0;
}
.position-info {
flex: 1;
}
.position-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.position-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 2px;
}
.position-code {
font-size: 12px;
color: #606266;
font-family: 'Courier New', monospace;
}
.plan-time-info { .plan-time-info {
margin-top: 15px; margin-top: 15px;
padding-top: 15px; padding-top: 15px;