This commit is contained in:
砂糖
2026-01-05 16:17:06 +08:00
11 changed files with 1550 additions and 389 deletions

View File

@@ -68,4 +68,17 @@ export function deletePdo(excoilid, planId) {
})
}
/**
* PDO - 下载质保单(单卷)
* @param {string} excoilid 成品卷号
* @returns {Promise<Blob>}
*/
export function downloadWarranty(excoilid) {
return l2Request({
method: 'get',
url: `/api/pdo/warranty/download/${excoilid}`,
responseType: 'blob'
})
}

View File

@@ -79,7 +79,7 @@
<div>
<pdo-summary :table-data="tableData" />
</div>
<!-- 统计汇总和图表区域 -->
<div class="statistics-container">
@@ -535,4 +535,4 @@ export default {
height: 100%;
padding-left: 15px;
}
</style>
</style>

View File

@@ -17,6 +17,50 @@
</el-button>
</div>
<!-- 计划队列 -->
<div v-loading="planQueueLoading" class="plan-queue-section">
<div class="section-header">
<i class="el-icon-s-order"></i>
<span>生产队列</span>
<el-badge :value="sortedPlanQueue.length" class="tab-badge" style="margin-left: 10px;" />
</div>
<div class="plan-list">
<div
v-for="plan in sortedPlanQueue"
:key="plan.id"
class="plan-item"
:class="{
'plan-item-producing': plan.status === 'PRODUCING',
'plan-item-ready': plan.status === 'READY',
'plan-item-new': plan.status === 'NEW'
}"
>
<div class="plan-status">
<span class="status-dot" :class="{
'status-producing': plan.status === 'PRODUCING',
'status-ready': plan.status === 'READY',
'status-new': plan.status === 'NEW'
}"></span>
<el-tag
:type="plan.status === 'PRODUCING' ? 'success' :
plan.status === 'READY' ? 'primary' : 'info'"
size="mini"
>
{{ plan.status === 'PRODUCING' ? '生产中' :
plan.status === 'READY' ? '就绪' : '新建' }}
</el-tag>
</div>
<div class="plan-content">
<span class="plan-no">{{ plan.planid }}</span>
<span class="coil-no">{{ plan.coilid }}</span>
</div>
</div>
<div v-if="sortedPlanQueue.length === 0 && !planQueueLoading" class="empty-text">
暂无生产计划
</div>
</div>
</div>
<!-- 卡片列表 -->
<div v-loading="loading" class="card-grid-container">
<el-row :gutter="20">
@@ -60,10 +104,19 @@
type="primary"
size="mini"
icon="el-icon-s-promotion"
@click="handleSend(setup)"
@click="handleSendCurrent(setup)"
:loading="setup.sending"
>
下发
当前计划下发
</el-button>
<el-button
type="success"
size="mini"
icon="el-icon-right"
@click="handleSendNext(setup)"
:loading="setup.sendingNext"
>
下一计划下发
</el-button>
</div>
</div>
@@ -103,6 +156,7 @@
import { listSetup } from '@/api/business/setup'
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
import { getLastSuccess } from '@/api/l2/sendTemplate'
import { listPlan } from '@/api/l2/plan'
// 传动字段定义(中文界面,贴合工业场景)
const DRIVE_FIELDS = [
@@ -165,6 +219,8 @@ export default {
loading: false,
lastSuccess: null,
setups: [],
planQueueLoading: false,
planQueue: [],
driveFields: DRIVE_FIELDS,
driveAddress: { ...DRIVE_ADDRESS }
}
@@ -172,8 +228,53 @@ export default {
created() {
this.reload()
},
computed: {
// 生产队列(不含已完成),按优先级排序:生产中 > 就绪 > 新建
sortedPlanQueue() {
const statusPriority = { PRODUCING: 1, READY: 2, NEW: 3 }
return (this.planQueue || []).slice().sort((a, b) => {
const pa = statusPriority[a.status] || 999
const pb = statusPriority[b.status] || 999
return pa - pb
})
},
// 下一计划队列中第一个非生产中计划READY/NEW若没有则取队列第一个
nextPlan() {
const list = this.sortedPlanQueue
if (!list.length) return null
const next = list.find(p => p.status !== 'PRODUCING')
return next || list[0]
}
},
methods: {
// 获取生产计划队列
async getPlanQueue() {
this.planQueueLoading = true
try {
// 查询状态为 PRODUCING, READY, NEW 的计划
const res = await listPlan({
status: 'PRODUCING,READY,NEW',
pageSize: 100, // 获取足够多的计划
pageNum: 1
})
this.planQueue = res.data || []
} catch (e) {
console.error('获取计划队列失败:', e)
this.$message.error('获取计划队列失败')
} finally {
this.planQueueLoading = false
}
},
async reload() {
// 同时加载计划队列和设置
await Promise.all([
this.getPlanQueue(),
this.loadSetups()
])
},
async loadSetups() {
this.loading = true
try {
// 1. 获取传动模块上次成功下发数据
@@ -204,7 +305,8 @@ export default {
return {
...s,
params,
sending: false
sending: false,
sendingNext: false
}
})
} catch (e) {
@@ -242,7 +344,7 @@ export default {
return new Date(t).toLocaleString()
},
handleSend(setup) {
handleSendCurrent(setup) {
this.$confirm(
`确认要下发【${setup.coilid || '-'}】钢卷的传动参数吗?`,
'提示',
@@ -254,29 +356,53 @@ export default {
).then(() => this.doSend(setup)).catch(() => {})
},
async doSend(setup) {
setup.sending = true
handleSendNext(setup) {
if (!this.nextPlan) {
this.$message.warning('暂无下一计划')
return
}
const plan = this.nextPlan
this.$confirm(
`确认要按下一计划【${plan.coilid || '-'}】下发传动参数吗?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => this.doSendNextPlan(setup, plan)).catch(() => {})
},
async doSend(setup, plan) {
const isNextPlan = !!plan
if (isNextPlan) {
setup.sendingNext = true
} else {
setup.sending = true
}
try {
const items = this.driveFields.map(f => ({
paramCode: f.key,
address: this.driveAddress[f.key],
valueRaw: String(setup.params[f.key] || ''),
setTime: new Date()
})).filter(it => !!it.address) // 过滤无OPC地址的项
})).filter(it => !!it.address)
if (!items.length) {
this.$message.warning('OPC地址未配置无可下发内容')
return
}
const bizKey = isNextPlan ? plan.coilid : setup.coilid
const dto = {
deviceName: 'CGL_LINE_1',
bizKey: setup.coilid,
bizKey: bizKey,
groups: [
{
groupNo: 1,
groupType: 'DRIVE',
groupName: `传动参数_${setup.coilid || ''}`,
groupName: `传动参数_${bizKey || ''}`,
items
}
]
@@ -294,8 +420,16 @@ export default {
console.error(e)
this.$message.error(e.message || '下发失败')
} finally {
setup.sending = false
if (isNextPlan) {
setup.sendingNext = false
} else {
setup.sending = false
}
}
},
doSendNextPlan(setup, plan) {
this.doSend(setup, plan)
}
}
}
@@ -303,6 +437,60 @@ export default {
<style scoped>
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
.plan-queue-section {
margin-bottom: 20px;
background: #ffffff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 15px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
font-weight: 600;
color: #303133;
}
.plan-list {
padding: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.plan-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #ffffff;
border: 1px solid #e4e7ed;
border-left: 3px solid #409eff;
border-radius: 4px;
min-width: 220px;
}
.plan-item-producing { border-left-color: #67c23a; }
.plan-item-ready { border-left-color: #409eff; }
.plan-item-new { border-left-color: #909399; }
.plan-status { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.plan-content { display: flex; flex-direction: column; gap: 2px; }
.plan-no { font-weight: 600; color: #409eff; }
.coil-no { font-size: 12px; color: #606266; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; }
.status-producing { background: #67c23a; box-shadow: 0 0 6px rgba(103, 194, 58, 0.6); }
.status-ready { background: #409eff; box-shadow: 0 0 6px rgba(64, 158, 255, 0.5); }
.status-new { background: #909399; box-shadow: 0 0 6px rgba(144, 147, 153, 0.4); }
.empty-text { padding: 10px; color: #909399; }
.card-grid-container { min-height: 300px; }
.card-col { margin-bottom: 20px; }
.parameter-card .card-header { display:flex; justify-content:space-between; align-items:center; }
@@ -310,7 +498,7 @@ export default {
.card-title-row { margin-bottom: 4px; }
.card-title { font-weight: 600; font-size: 16px; }
.card-subtitle { font-size: 12px; color: #909399; display: flex; gap: 12px; }
.header-right { flex-shrink: 0; margin-left: 16px; }
.header-right { flex-shrink: 0; margin-left: 16px; display: flex; gap: 8px; align-items: center; }
.last-send-time { font-size: 12px; color:#909399; margin-right:16px; }
.empty-data { margin-top: 20px; }
</style>