feat(pdo): 新增PDO管理功能及相关组件

新增PDO管理页面及相关功能组件,包括:
1. 新增数据修正组件DataCorrection.vue
2. 新增标签打印组件LabelPrint.vue
3. 新增统计汇总组件PdoSummary.vue
4. 新增图表展示组件line.vue
5. 实现主页面index.vue布局及功能
6. 新增API接口文件用于业务数据交互
7. 修改lines/index.vue配置,移除baseURL动态获取逻辑
This commit is contained in:
砂糖
2025-12-25 15:50:19 +08:00
parent 65c04f3ed2
commit 30d5d914ef
15 changed files with 3486 additions and 5 deletions

View File

@@ -41,7 +41,7 @@ import RollerAnalysis from './panels/analysis/roller.vue'
data() {
return {
activeName: 'Track',
baseURL: '',
baseURL: '140.143.206.120:18081',
ready: false,
tabs: []
}
@@ -49,10 +49,6 @@ import RollerAnalysis from './panels/analysis/roller.vue'
created() {
// 获取路由地址的最后一项
const route = this.$route.path.split('/').pop()
getConfigKey(`line.${route}.baseURL`).then(res => {
this.baseURL = res.msg
this.ready = true
})
getConfigKey(`line.${route}.config`).then(res => {
const tabs = res.msg.split(',')
// 是否包含Track, 如果不包含则添加

View File

@@ -0,0 +1,310 @@
<template>
<el-form
:model="formData"
ref="correctionForm"
label-width="120px"
:rules="formRules"
>
<el-row :gutter="20">
<!-- 成品卷 -->
<el-col :span="8">
<el-form-item label="成品卷" prop="exitMatId">
<el-input v-model="formData.exitMatId"></el-input>
</el-form-item>
</el-col>
<!-- 来料卷 -->
<el-col :span="8">
<el-form-item label="来料卷" prop="entryMatId">
<el-input v-model="formData.entryMatId"></el-input>
</el-form-item>
</el-col>
<!-- 分切数 -->
<el-col :span="8">
<el-form-item label="分切数" prop="subId">
<el-input-number v-model="formData.subId" :min="0" :step="1" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 开始位置 -->
<el-col :span="8">
<el-form-item label="开始位置" prop="startPosition">
<el-input-number v-model="formData.startPosition" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 结束位置 -->
<el-col :span="8">
<el-form-item label="结束位置" prop="endPosition">
<el-input-number v-model="formData.endPosition" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 计划ID -->
<el-col :span="8">
<el-form-item label="计划ID" prop="planId">
<el-input-number v-model="formData.planId" :step="1" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 计划号 -->
<el-col :span="8">
<el-form-item label="计划号" prop="planNo">
<el-input v-model="formData.planNo"></el-input>
</el-form-item>
</el-col>
<!-- 产品类型 -->
<el-col :span="8">
<el-form-item label="产品类型" prop="prodCode">
<el-input v-model="formData.prodCode"></el-input>
</el-form-item>
</el-col>
<!-- 班号 -->
<el-col :span="8">
<el-form-item label="班号" prop="groupNo">
<el-input v-model="formData.groupNo"></el-input>
</el-form-item>
</el-col>
<!-- 组号 -->
<el-col :span="8">
<el-form-item label="组号" prop="shiftNo">
<el-input v-model="formData.shiftNo"></el-input>
</el-form-item>
</el-col>
<!-- 状态 -->
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-input v-model="formData.status"></el-input>
</el-form-item>
</el-col>
<!-- 钢种 -->
<el-col :span="8">
<el-form-item label="钢种" prop="steelGrade">
<el-input v-model="formData.steelGrade"></el-input>
</el-form-item>
</el-col>
<!-- 来料厚度 -->
<el-col :span="8">
<el-form-item label="来料厚度" prop="entryThick">
<el-input-number v-model="formData.entryThick" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 来料宽度 -->
<el-col :span="8">
<el-form-item label="来料宽度" prop="entryWidth">
<el-input-number v-model="formData.entryWidth" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 来料长度 -->
<el-col :span="8">
<el-form-item label="来料长度" prop="entryLength">
<el-input-number v-model="formData.entryLength" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 来料重量 -->
<el-col :span="8">
<el-form-item label="来料重量" prop="entryWeight">
<el-input-number v-model="formData.entryWeight" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 上表面镀锌 -->
<el-col :span="8">
<el-form-item label="上表面镀锌" prop="weightTop">
<el-input-number v-model="formData.weightTop" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 下表面镀锌 -->
<el-col :span="8">
<el-form-item label="下表面镀锌" prop="weightBottom">
<el-input-number v-model="formData.weightBottom" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 成品长度 -->
<el-col :span="8">
<el-form-item label="成品长度" prop="exitLength">
<el-input-number v-model="formData.exitLength" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 成品带涂料重量 -->
<el-col :span="8">
<el-form-item label="成品带涂料重量" prop="exitNetWeight">
<el-input-number v-model="formData.exitNetWeight" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 理论重量 -->
<el-col :span="8">
<el-form-item label="理论重量" prop="theoryWeight">
<el-input-number v-model="formData.theoryWeight" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 实际重量 -->
<el-col :span="8">
<el-form-item label="实际重量" prop="actualWeight">
<el-input-number v-model="formData.actualWeight" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 成品外径 -->
<el-col :span="8">
<el-form-item label="成品外径" prop="exitOuterDiameter">
<el-input-number v-model="formData.exitOuterDiameter" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 成品厚度 -->
<el-col :span="8">
<el-form-item label="成品厚度" prop="exitThickness">
<el-input-number v-model="formData.exitThickness" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 成品宽度 -->
<el-col :span="8">
<el-form-item label="成品宽度" prop="exitWidth">
<el-input-number v-model="formData.exitWidth" :step="0.01" style="width: 100%;"></el-input-number>
</el-form-item>
</el-col>
<!-- 客户 -->
<el-col :span="8">
<el-form-item label="客户" prop="customer">
<el-input v-model="formData.customer"></el-input>
</el-form-item>
</el-col>
<!-- 上线时间 -->
<el-col :span="8">
<el-form-item label="上线时间" prop="onlineTime">
<el-date-picker v-model="formData.onlineTime" type="datetime" value-format="yyyy-MM-ddTHH:mm:ss" />
</el-form-item>
</el-col>
<!-- 开始时间 -->
<el-col :span="8">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="formData.startTime" type="datetime" value-format="yyyy-MM-ddTHH:mm:ss" />
</el-form-item>
</el-col>
<!-- 结束时间 -->
<el-col :span="8">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="formData.endTime" type="datetime" value-format="yyyy-MM-ddTHH:mm:ss" />
</el-form-item>
</el-col>
<!-- 机组号 -->
<el-col :span="8">
<el-form-item label="机组号" prop="unitCode">
<el-input v-model="formData.unitCode"></el-input>
</el-form-item>
</el-col>
<!-- 工序号 -->
<el-col :span="8">
<el-form-item label="工序号" prop="processCode">
<el-input v-model="formData.processCode"></el-input>
</el-form-item>
</el-col>
<!-- 是否尾卷 -->
<el-col :span="8">
<el-form-item label="是否尾卷" prop="lastFlag">
<el-switch v-model="formData.lastFlag"></el-switch>
</el-form-item>
</el-col>
<!-- 是否分卷 -->
<el-col :span="8">
<el-form-item label="是否分卷" prop="separateFlag">
<el-switch v-model="formData.separateFlag"></el-switch>
</el-form-item>
</el-col>
<!-- 计划来源 -->
<el-col :span="8">
<el-form-item label="计划来源" prop="planOrigin">
<el-select v-model="formData.planOrigin" placeholder="请选择计划来源">
<el-option label="L3计划" value="L3"></el-option>
<el-option label="人工" value="MANUAL"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 保存按钮 -->
<div class="dialog-footer">
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
</div>
</el-form>
</template>
<script>
export default {
name: 'PdoDataCorrection',
props: {
// 接收父组件传递的详情数据
detail: {
type: Object,
required: true,
default: () => ({})
},
// 保存回调函数(父组件处理实际保存逻辑)
saveCallback: {
type: Function,
required: true
},
// 保存按钮loading状态由父组件控制
saveLoading: {
type: Boolean,
required: true,
default: false
}
},
data() {
return {
// 内部表单数据拷贝父组件传递的detail避免直接修改父组件数据
formData: {},
// 表单验证规则
formRules: {
exitMatId: [{ required: true, message: '请输入成品卷号', trigger: 'blur' }],
entryMatId: [{ required: true, message: '请输入来料卷号', trigger: 'blur' }],
planNo: [{ required: true, message: '请输入计划号', trigger: 'blur' }],
steelGrade: [{ required: true, message: '请输入钢种', trigger: 'blur' }]
// 可根据实际需求补充其他字段验证规则
}
}
},
watch: {
// 监听detail变化深拷贝更新内部formData
detail: {
handler(newVal) {
// 深拷贝避免引用类型数据相互影响
this.formData = JSON.parse(JSON.stringify(newVal))
// 初始化数值类型字段防止null/undefined导致输入框异常
this.initNumberFields()
},
immediate: true, // 初始渲染时立即执行
deep: true // 深度监听对象内部变化
}
},
methods: {
// 初始化数值类型字段确保默认值为0而非null/undefined
initNumberFields() {
const numberFields = [
'subId', 'startPosition', 'endPosition', 'planId', 'entryThick',
'entryWidth', 'entryLength', 'entryWeight', 'weightTop', 'weightBottom',
'exitLength', 'exitNetWeight', 'theoryWeight', 'actualWeight',
'exitOuterDiameter', 'exitThickness', 'exitWidth'
]
numberFields.forEach(field => {
if (this.formData[field] === undefined || this.formData[field] === null) {
this.formData[field] = 0
}
})
// 初始化布尔类型字段
this.formData.lastFlag = this.formData.lastFlag ?? false
this.formData.separateFlag = this.formData.separateFlag ?? false
},
// 处理保存逻辑(先验证表单,通过后触发父组件回调)
handleSave() {
this.$refs.correctionForm.validate(valid => {
if (valid) {
// 触发父组件保存回调,传递内部修改后的表单数据
this.saveCallback(this.formData)
}
})
}
}
}
</script>
<style scoped>
.dialog-footer {
text-align: right;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,271 @@
<template>
<div class="label-print-container">
<!-- 预览区域与打印内容一致且可编辑 -->
<div class="label-preview" v-if="Object.keys(editableData).length">
<!-- 给打印区域添加一个唯一ID -->
<div id="printContent" class="label-content">
<!-- 公司名称 -->
<div class="company-name">
{{ editableData.companyName }}
</div>
<!-- 内容区域使用Flexbox布局 -->
<div class="content-grid">
<div class="grid-item label">钢卷号</div>
<div class="grid-item value">
<input v-model="editableData.exitMatId" :border="false" class="editable-input"
placeholder="钢卷号"></input>
</div>
<div class="grid-item label">热卷号</div>
<div class="grid-item value">
<input v-model="editableData.entryMatId" :border="false" class="editable-input"
placeholder="热卷号"></input>
</div>
<div class="grid-item label">规格</div>
<div class="grid-item value">
<input v-model="editableData.spec" :border="false" class="editable-input" placeholder="规格"></input>
</div>
<div class="grid-item label">材质</div>
<div class="grid-item value">
<input v-model="editableData.steelGrade" :border="false" class="editable-input"
placeholder="材质"></input>
</div>
<div class="grid-item label">净重</div>
<div class="grid-item value">
<input v-model="editableData.actualWeight" :border="false" class="editable-input"
placeholder="净重"></input>
</div>
<div class="grid-item label">生产班组</div>
<div class="grid-item value">
<input v-model="editableData.groupNo" :border="false" class="editable-input"
placeholder="生产班组"></input>
</div>
<div class="grid-item label">产品名称</div>
<div class="grid-item value">
<input v-model="editableData.prodCode" :border="false" class="editable-input"
placeholder="产品名称"></input>
</div>
<div class="grid-item label">生产日期</div>
<div class="grid-item value">
<input v-model="editableData.productionDate" :border="false" class="editable-input"
placeholder="生产日期"></input>
</div>
</div>
</div>
</div>
<!-- 打印按钮 -->
<div class="print-btn-container">
<el-button type="primary" @click="handlePrint" icon="el-icon-printer"
:disabled="!Object.keys(editableData).length">
打印标签
</el-button>
</div>
</div>
</template>
<script>
import printJS from 'print-js';
import dayjs from 'dayjs';
export default {
name: 'PdoLabelPrint',
props: {
detail: {
type: Object,
required: true,
default: () => ({})
},
},
data() {
return {
// 可编辑的数据对象
editableData: {}
};
},
watch: {
// 监听detail变化初始化可编辑数据
detail: {
handler(newVal) {
if (newVal) {
this.initEditableData(newVal);
}
},
immediate: true,
deep: true
}
},
methods: {
// 初始化可编辑数据
initEditableData(detail) {
const {
exitMatId, entryMatId, exitThickness, exitWidth,
actualWeight, steelGrade, groupNo, prodCode
} = detail;
// 格式化规格
const spec = exitThickness && exitWidth
? `${exitThickness}*${exitWidth}`
: '';
// 格式化生产日期
const productionDate = dayjs().format('M月D日');
// 初始化可编辑数据
this.editableData = {
companyName: '嘉祥科伦普重工有限公司',
exitMatId: exitMatId || '',
entryMatId: entryMatId || '',
spec: spec || '',
steelGrade: steelGrade || '',
actualWeight: actualWeight || '',
groupNo: groupNo || '',
prodCode: prodCode || '',
productionDate: productionDate
};
},
// 处理打印逻辑
handlePrint() {
// 定义打印样式,确保打印效果与预览一致
const printStyle = `
.label-content {
width: 200mm;
height: 160mm;
padding: 0;
box-sizing: border-box;
font-family: SimSun;
}
.company-name {
text-align: center;
padding: 8mm 0;
font-size: 16pt;
font-weight: bold;
border: 1px solid #000;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 0;
}
.grid-item {
min-height: 15mm;
display: flex;
border: 1px solid #000;
align-items: center;
box-sizing: border-box;
}
.label {
font-weight: bold;
font-size: 12pt;
padding-left: 2mm;
}
.value {
font-size: 12pt;
}
input {
border: none !important;
width: 100%;
height: 100%;
font-size: 12pt;
padding: 0 2mm;
box-sizing: border-box;
}
`;
// 调用printJS进行打印
printJS({
printable: 'printContent', // 要打印的元素ID
type: 'html',
header: null, // 不显示默认页眉
footer: null, // 不显示默认页脚
// style: printStyle, // 应用打印样式
scanStyles: true, // 不扫描页面现有样式
targetStyles: ['*'], // 允许所有目标样式
documentTitle: '标签打印', // 打印文档标题
onPrintDialogClose: () => {
console.log('打印对话框已关闭');
}
});
}
}
}
</script>
<style scoped>
.label-print-container {
padding: 10px;
}
/* 预览区域样式 */
.label-preview {
margin-bottom: 20px;
}
/* 标签内容样式(与打印尺寸等比例) */
.label-content {
width: 500px; /* 200mm × 2.5 = 500px */
height: 400px; /* 160mm × 2.5 = 400px */
border: 1px solid #ccc;
padding: 25px; /* 10mm × 2.5 = 25px */
box-sizing: border-box;
font-family: SimSun;
}
.company-name {
text-align: center;
padding: 20px;
border: 1px solid white;
font-size: 24px;
font-weight: bold;
}
/* 内容网格布局 */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 0;
border: 1px solid white;
}
.grid-item {
border: 1px solid white;
min-height: 40px;
align-items: center;
box-sizing: border-box;
}
.label {
font-weight: bold;
line-height: 60px;
font-size: 16px;
}
/* 无边框输入框样式 */
.editable-input {
width: 100%;
padding: 4px;
text-align: inherit;
background: transparent;
font-size: inherit;
border: none;
outline: none;
height: 60px;
box-sizing: border-box;
line-height: 60px;
}
.editable-input:hover,
.editable-input:focus {
background: rgba(255, 255, 255, 0.8);
outline: none;
}
.print-btn-container {
text-align: left;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,113 @@
<!-- src/views/PdoManagement/components/PdoSummary.vue -->
<template>
<el-descriptions
class="summary-content"
:column="1"
:style="{ width: '100%' }"
size="mini"
>
<!-- 循环渲染汇总项从计算属性获取数据 -->
<el-descriptions-item
v-for="(item, index) in summaryResult"
:key="index"
:label="item.label"
label-class="summary-label"
>
<!-- 根据类型格式化显示重量/百分比/数量 -->
<span :class="item.type === 'rate' ? 'summary-rate' : ''">
{{ item.value }}
{{ item.unit }}
</span>
</el-descriptions-item>
</el-descriptions>
</template>
<script>
export default {
name: 'PdoSummary',
props: {
// 接收表格原始数据(父组件传递)
tableData: {
type: Array,
required: true,
default: () => [] // 默认空数组,避免无数据时报错
}
},
computed: {
// 核心根据tableData计算汇总结果
summaryResult() {
// 1. 基础数值汇总处理字符串转数字避免NaN
const totalEntryWeight = this.tableData.reduce((sum, item) => {
return sum + (Number(item.entryWeight) || 0)
}, 0) // 原料总重(来料重量求和)
const totalExitWeight = this.tableData.reduce((sum, item) => {
return sum + (Number(item.exitNetWeight) || 0)
}, 0) // 产品总重(成品带涂料重量求和)
const totalZincRemoved = totalEntryWeight - totalExitWeight // 去锌总理重(原料总重 - 产品总重)
// 2. 成材率计算避免除数为0
const yieldRate = totalEntryWeight > 0
? ((totalExitWeight / totalEntryWeight) * 100).toFixed(2)
: '0.00'
// 3. 卷数统计(原料/成品卷数默认按表格行数可根据实际业务调整如去重entryMatId
const totalEntryCoils = this.tableData.length // 原料总卷数
const totalExitCoils = this.tableData.length // 成品总卷数
// 4. 返回格式化后的汇总项
return [
{ label: '原料总重', value: totalEntryWeight.toFixed(1), unit: 't', type: 'weight' },
{ label: '去锌总理重', value: totalZincRemoved.toFixed(1), unit: 't', type: 'weight' },
{ label: '产品总重', value: totalExitWeight.toFixed(1), unit: 't', type: 'weight' },
{ label: '成材率', value: yieldRate, unit: '%', type: 'rate' },
{ label: '原料总卷数', value: totalEntryCoils, unit: '卷', type: 'count' },
{ label: '成品总卷数', value: totalExitCoils, unit: '卷', type: 'count' }
]
}
}
}
</script>
<style lang="scss" scoped>
.summary-content {
padding: 10px;
flex: 1;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
}
::v-deep .el-descriptions__label {
font-weight: 500;
color: #666;
font-size: 12px;
}
::v-deep .el-descriptions__content {
color: #333;
font-size: 13px;
font-weight: 500;
}
}
.summary-label {
font-weight: 500;
color: #666;
}
.summary-rate {
color: #333;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,551 @@
<template>
<div class="monitoring-container">
<div class="chart-wrapper" v-loading="loading">
<!-- 参数选择下拉框 -->
<el-select v-model="paramField" class="param-select" @change="handleParamChange" size="mini">
<el-option v-for="item in paramFields" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<!-- 图表容器 -->
<div ref="chart" class="chart-content"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import createFetch from '@/api/l2/pdo'
export default {
name: 'DeviceMonitoring',
props: {
enCoilID: {
type: String,
required: true,
},
url: {
type: String,
required: true,
}
},
data() {
return {
pdoApi: undefined,
// 参数列表来源于 DeviceEnum 定义的监测字段
paramFields: [
{ label: '带钢速度', value: 'stripSpeed' },
{ label: '开卷张力1#', value: 'tensionPorBr1' },
{ label: '开卷张力2#', value: 'tensionPorBr2' },
{ label: '清洗电压', value: 'cleaningVoltage' },
{ label: '清洗电流', value: 'cleaningCurrent' },
{ label: '碱液浓度', value: 'alkaliConcentration' },
{ label: '碱液温度', value: 'alkaliTemperature' },
{ label: 'PH炉出口温度', value: 'phfExitStripTemp' },
{ label: '加热段出口温度', value: 'rtfExitStripTemp' },
{ label: '冷却段出口温度', value: 'jcsExitStripTemp' },
{ label: '均衡段出口温度', value: 'scsExitStripTemp' },
{ label: '锌锅温度', value: 'potTemperature' },
{ label: '锌锅功率', value: 'zincPotPower' },
{ label: '燃气消耗', value: 'gasConsumption' },
{ label: '冷却塔温度', value: 'coolingTowerStripTemp' },
{ label: '光整机张力', value: 'tensionBr5Tm' },
{ label: 'TM出口速度', value: 'stripSpeedTmExit' },
{ label: '拉矫延伸率', value: 'tlElongation' },
{ label: '拉矫张力', value: 'tensionTlBr7' }
],
paramField: 'stripSpeed',
treeProps: {
children: 'children',
label: 'label'
},
currentParam: null, // 当前选中的参数
chart: null, // 图表实例
chartData: [], // 图表数据
timeStamps: [], // 时间戳
originalTimeStamps: [], // 原始时间戳(用于采样)
originalChartData: [], // 原始图表数据(用于采样)
timeRange: '1', // 时间范围(分钟)
socket: null, // 模拟socket
latestValue: 0,
maxValue: 0,
minValue: 0,
avgValue: 0,
loading: false,
maxXAxisLabels: 40 // X轴最多显示的标签数量
}
},
watch: {
enCoilID: {
handler(newVal) {
console.log('enCoilID 变化:', newVal);
if (newVal && this.pdoApi) {
this.fetchChartData();
} else {
this.drawEmptyChart();
}
},
immediate: true
},
url: {
handler(newVal) {
console.log('url 变化:', newVal);
if (newVal) {
this.pdoApi = createFetch(newVal)
} else {
this.pdoApi = null
}
},
immediate: true
},
},
mounted() {
// 初始化图表
this.initChart();
},
beforeDestroy() {
// 销毁图表实例和事件监听
if (this.chart) {
this.chart.dispose();
}
window.removeEventListener('resize', this.handleResize);
},
methods: {
// 初始化图表
initChart() {
this.$nextTick(() => {
if (this.$refs.chart) {
this.chart = echarts.init(this.$refs.chart);
// 监听窗口大小变化,调整图表尺寸
window.addEventListener('resize', this.handleResize);
// 初始显示
if (this.enCoilID) {
this.fetchChartData();
} else {
this.drawEmptyChart();
}
}
});
},
// 处理窗口大小变化
handleResize() {
if (this.chart) {
this.chart.resize();
}
},
// 获取图表数据
fetchChartData() {
if (!this.enCoilID || !this.paramField || !this.pdoApi) {
this.drawNoDataChart();
}
this.loading = true;
this.pdoApi.getSegmentList({
enCoilID: this.enCoilID,
paramField: this.paramField
}).then(res => {
// 假设res.data格式为[{ segNo: '时间戳', value: '数值' }]
if (res.data && res.data.length) {
this.processChartData(res.data);
this.drawLineChart();
console.log('有数据,绘制折线图')
} else {
this.drawNoDataChart();
console.log('无数据,清空折线图')
}
}).catch(err => {
console.error('获取数据失败:', err);
this.drawErrorChart();
}).finally(_ => {
this.loading = false;
});
},
// 处理图表数据
processChartData(rawData) {
// 排序数据按segNo假设是时间戳或序号
rawData.sort((a, b) => a.segNo - b.segNo);
// 保存原始数据
this.originalTimeStamps = rawData.map(item => item.segNo);
this.originalChartData = rawData.map(item => parseFloat(item.value));
// 根据数据量决定是否采样
// if (rawData.length > this.maxXAxisLabels) {
// // 数据采样 - 均匀采样
// this.sampleData(rawData);
// } else {
// 数据量适中,直接使用全部数据
this.timeStamps = this.originalTimeStamps;
this.chartData = this.originalChartData;
// }
// 计算统计值(基于原始数据)
if (this.originalChartData.length) {
this.latestValue = this.originalChartData[this.originalChartData.length - 1];
this.maxValue = Math.max(...this.originalChartData);
this.minValue = Math.min(...this.originalChartData);
this.avgValue = this.originalChartData.reduce((sum, val) => sum + val, 0) / this.originalChartData.length;
}
},
// 数据采样方法
sampleData(rawData) {
const total = rawData.length;
const sampleInterval = Math.ceil(total / this.maxXAxisLabels);
// 初始化采样数组
this.timeStamps = [];
this.chartData = [];
// 均匀采样
for (let i = 0; i < total; i += sampleInterval) {
this.timeStamps.push(rawData[i].segNo);
this.chartData.push(parseFloat(rawData[i].value));
}
// 确保最后一个数据点被包含
if (this.timeStamps[this.timeStamps.length - 1] !== rawData[total - 1].segNo) {
this.timeStamps.push(rawData[total - 1].segNo);
this.chartData.push(parseFloat(rawData[total - 1].value));
}
},
// 绘制折线图
drawLineChart() {
if (!this.chart) return;
// 清理旧图层,避免“暂无数据”残留
this.chart.clear();
const option = {
backgroundColor: 'transparent',
graphic: [], // 清除占位/无数据图层
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line'
},
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#d4d4d4',
borderWidth: 1,
textStyle: {
color: '#333'
},
formatter: (params) => {
const param = params[0];
return `
序号: ${param.name}<br>
值: ${param.value.toFixed(2)}<br>
单位: ${this.getParamUnit()}
`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '10%', // 增加底部空间,防止标签被截断
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
// data: this.timeStamps,
// axisLine: {
// lineStyle: {
// color: '#d4d4d4'
// }
// },
// axisLabel: {
// color: '#666',
// rotate: 45,
// // 动态计算标签显示间隔
// interval: (index, value) => {
// // 如果数据量大于maxXAxisLabels按采样后的间隔显示
// return index % Math.max(1, Math.ceil(this.timeStamps.length / this.maxXAxisLabels)) === 0;
// }
// },
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
name: this.getParamUnit(),
nameLocation: 'middle',
nameGap: 30,
nameTextStyle: {
color: '#666'
},
axisLine: {
lineStyle: {
color: '#d4d4d4'
}
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#e8e8e8',
type: 'dashed'
}
}
},
series: [
{
name: this.getParamLabel(),
type: 'line',
data: this.chartData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: {
color: '#666',
width: 2
},
itemStyle: {
color: '#999'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(153, 153, 153, 0.3)' },
{ offset: 1, color: 'rgba(153, 153, 153, 0.05)' }
]
}
},
markPoint: {
data: [
{ type: 'max', name: '最大值' },
{ type: 'min', name: '最小值' }
],
itemStyle: {
color: '#999'
},
label: {
color: '#333'
}
},
markLine: {
data: [
{ type: 'average', name: '平均值' }
],
lineStyle: {
color: '#999',
type: 'dashed'
},
label: {
color: '#333'
}
}
}
]
};
this.chart.setOption(option, true);
},
// 绘制空图表(未选择实绩)
drawEmptyChart() {
if (!this.chart) return;
const option = {
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '请选择实绩',
fontSize: 16,
fontWeight: 'bold',
fill: '#999'
}
}
]
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: []
};
this.chart.setOption(option);
},
// 绘制无数据图表
drawNoDataChart() {
if (!this.chart) return;
// 先清除原有图表内容
this.chart.clear();
const option = {
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '暂无数据',
fontSize: 16,
fontWeight: 'bold',
fill: '#999'
}
}
]
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: []
};
this.chart.setOption(option);
},
// 绘制错误图表
drawErrorChart() {
if (!this.chart) return;
const option = {
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '数据加载失败',
fontSize: 16,
fontWeight: 'bold',
fill: '#f56c6c'
}
}
]
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: []
};
this.chart.setOption(option);
},
// 处理参数变更
handleParamChange() {
// 清空现有数据
this.chartData = [];
this.timeStamps = [];
this.originalTimeStamps = [];
this.originalChartData = [];
// 重新获取数据
if (this.enCoilID) {
this.fetchChartData();
}
},
// 获取参数标签
getParamLabel() {
const param = this.paramFields.find(item => item.value === this.paramField);
return param ? param.label : this.paramField;
},
// 获取参数单位
getParamUnit() {
// 根据不同参数返回不同单位
switch (this.paramField) {
case 'stripSpeed':
return 'm/s';
case 'tensionPorBr1':
case 'tensionPorBr2':
return 'N';
case 'cleaningVoltage':
return 'V';
case 'cleaningCurrent':
return 'A';
case 'alkaliConcentration':
return '%';
case 'alkaliTemperature':
case 'phfExitStripTemp':
case 'rtfExitStripTemp':
case 'jcsExitStripTemp':
case 'scsExitStripTemp':
case 'potTemperature':
case 'coolingTowerStripTemp':
return '°C';
case 'zincPotPower':
return 'kW';
case 'gasConsumption':
return 'm³/h';
case 'tensionBr5Tm':
case 'tensionTlBr7':
return 'N';
case 'stripSpeedTmExit':
return 'm/s';
case 'tlElongation':
return '%';
default:
return '';
}
}
}
}
</script>
<style lang="scss" scoped>
.monitoring-container {
height: 100%;
box-sizing: border-box;
}
.chart-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.param-select {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
width: 150px;
::v-deep .el-input__inner {
border-color: #d4d4d4;
}
}
.chart-content {
width: 100%;
flex: 1;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,579 @@
<template>
<div class="pdo-container">
<!-- 查询表单区域 -->
<div class="pdo-header">
<el-form :inline="true" :model="queryForm" ref="queryForm" label-width="100px" size="small">
<el-form-item label="钢卷号" prop="coilid">
<el-input v-model="queryForm.coilid" placeholder="请输入钢卷号"></el-input>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="queryForm.startDate" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd"
clearable></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="queryForm.endDate" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd"
clearable></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="btnLoading" icon="el-icon-search">查询</el-button>
<el-button @click="handleReset" icon="el-icon-refresh">重置</el-button>
<el-button type="success" @click="handleAdd" icon="el-icon-plus">补录</el-button>
</el-form-item>
</el-form>
</div>
<!-- 卡片网格布局 -->
<div v-loading="tableLoading" class="card-grid-container">
<el-row :gutter="10">
<el-col v-for="(item, index) in tableData" :key="index" :xs="24" :sm="12" :md="8" :lg="6" :xl="5"
class="card-col">
<el-card class="parameter-card" shadow="never" :body-style="{ padding: '8px' }"
:class="{ 'card-selected': currentRow.exitMatId === item.exitMatId }" @click.native="handleRowClick(item)">
<div slot="header" class="card-header">
<div class="card-title">成品卷: {{ item.exitMatId || '-' }}</div>
<div class="card-subtitle">{{ item.entryMatId || '-' }} | {{ item.planNo || '-' }}</div>
</div>
<div class="card-body">
<div class="param-groups-row">
<!-- 基本信息 -->
<div class="param-group">
<div class="group-title">基本信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">状态:</span>
<span class="param-value">{{ item.status || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">钢种:</span>
<span class="param-value">{{ item.steelGrade || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">产品类型:</span>
<span class="param-value">{{ item.prodCode || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">客户:</span>
<span class="param-value">{{ item.customer || '-' }}</span>
</div>
</div>
</div>
<!-- 来料信息 -->
<div class="param-group">
<div class="group-title">来料信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">厚度:</span>
<span class="param-value">{{ item.entryThick || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">宽度:</span>
<span class="param-value">{{ item.entryWidth || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">长度:</span>
<span class="param-value">{{ item.entryLength || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">重量:</span>
<span class="param-value">{{ item.entryWeight || '-' }}</span>
</div>
</div>
</div>
<!-- 成品信息 -->
<div class="param-group">
<div class="group-title">成品信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">厚度:</span>
<span class="param-value">{{ item.exitThickness || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">宽度:</span>
<span class="param-value">{{ item.exitWidth || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">长度:</span>
<span class="param-value">{{ item.exitLength || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">重量:</span>
<span class="param-value">{{ item.exitNetWeight || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<el-button size="mini" type="text" icon="el-icon-view" @click.stop="handlePrint(item)">打印</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEdit(item)">操作</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" :loading="item.deleteLoading"
@click.stop="handleDelete(item)">删除</el-button>
</div>
</el-card>
</el-col>
</el-row>
<div v-if="tableData.length === 0 && !tableLoading" class="empty-data">
<el-empty description="暂无数据"></el-empty>
</div>
</div>
<!-- 统计汇总和图表区域 -->
<div class="statistics-container">
<div class="statistics-header">
<div class="selected-info" v-if="currentRow && currentRow.entryMatId">
<i class="el-icon-check"></i>
<span class="selected-label">已选中:</span>
<span class="selected-value">成品卷 {{ currentRow.exitMatId }}</span>
<span class="selected-detail" v-if="currentRow.entryMatId">(来料卷: {{ currentRow.entryMatId }})</span>
</div>
<div class="selected-info empty" v-else>
<i class="el-icon-info"></i>
<span>请选择上方卡片查看详情</span>
</div>
</div>
<el-row :gutter="15" class="statistics-content">
<el-col :span="4" class="summary-col">
<div class="summary-wrapper">
<div class="summary-header">统计汇总</div>
<pdo-summary :table-data="tableData" />
</div>
</el-col>
<el-col :span="20" class="chart-col">
<div class="chart-wrapper">
<line-chart :enCoilID="currentRow.entryMatId" :url="activeUrl" />
</div>
</el-col>
</el-row>
</div>
<!-- 编辑/新增弹窗根据isAdd条件渲染内容 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="80%" :close-on-click-modal="false">
<!-- 补录新增仅显示数据修正组件无tab -->
<div v-if="isAdd">
<pdo-data-correction :detail="formData" :save-callback="handleSave"
:save-loading="saveLoading"></pdo-data-correction>
</div>
<pdo-data-correction :detail="formData" :save-callback="handleSave"
:save-loading="saveLoading"></pdo-data-correction>
</el-dialog>
<el-dialog title="标签打印" :visible.sync="printOpen" width="600px" :close-on-click-modal="false">
<div style="display: flex; justify-content: center; align-items: center;">
<pdo-label-print :detail="formData"></pdo-label-print>
</div>
</el-dialog>
</div>
</template>
<script>
import createFetch from '@/api/l2/pdo'
import LineChart from './components/line.vue'
// 引入封装的两个组件
import PdoDataCorrection from './components/DataCorrection.vue'
import PdoLabelPrint from './components/LabelPrint.vue'
import PdoSummary from './components/PdoSummary.vue'
export default {
name: 'PdoManagement',
dicts: ['pdo_plan_origin'],
components: {
LineChart,
PdoDataCorrection, // 注册数据修正组件
PdoLabelPrint, // 注册标签打印组件
PdoSummary
},
data() {
return {
activeUrl: '140.143.206.120:18081',
queryForm: { coilid: '', startDate: '', endDate: '' },
printOpen: false,
tableData: [],
tableLoading: false,
btnLoading: false,
dialogVisible: false,
dialogTitle: '新增实绩',
// 新增:区分「补录(新增)」和「编辑」的状态标识
isAdd: false,
// 传递给子组件的详情数据(父组件仅做数据中转,不直接修改)
formData: {},
saveLoading: false,
currentRow: {},
socketType: [
{ value: 'alarm', label: '报警' },
{ value: 'track_position', label: '跟踪位置' },
{ value: 'track_measure', label: '跟踪测量' },
{ value: 'track_signal', label: '跟踪信号' },
{ value: 'track_matmap', label: '跟踪料号' },
{ value: 'calc_setup_result', label: '计算结果' }
]
}
},
created() {
this.pdoApi = createFetch(this.activeUrl)
this.getPdoList()
},
methods: {
// 获取列表数据(保持不变)
getPdoList() {
this.tableLoading = true
this.pdoApi.getPdoList(this.queryForm).then(res => {
this.tableData = res.data.map(item => ({ ...item, deleteLoading: false }))
}).catch(err => {
console.error(err)
this.$message.error('获取数据失败')
}).finally(() => {
this.tableLoading = false
this.btnLoading = false
})
},
// 查询(保持不变)
handleQuery() {
this.btnLoading = true;
this.getPdoList()
},
// 重置(保持不变)
handleReset() {
this.$refs.queryForm.resetFields();
this.getPdoList()
},
// 行点击(保持不变)
handleRowClick(row) {
this.currentRow = row;
},
// 格式化时间
formatTime(time) {
if (!time) return '-';
return this.parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}');
},
// 补录新增设置isAdd=true初始化空表单
handleAdd() {
this.isAdd = true; // 标记为「补录」
this.dialogTitle = '新增实绩';
// 初始化空表单数据(传递给子组件)
this.formData = {
subId: 0,
startPosition: 0,
endPosition: 0,
planId: 0,
entryThick: 0,
entryWidth: 0,
entryLength: 0,
entryWeight: 0,
weightTop: 0,
weightBottom: 0,
exitLength: 0,
exitNetWeight: 0,
theoryWeight: 0,
actualWeight: 0,
exitOuterDiameter: 0,
exitThickness: 0,
exitWidth: 0,
lastFlag: false,
separateFlag: false
};
this.dialogVisible = true
},
// 编辑设置isAdd=false赋值行数据
handleEdit(row) {
this.isAdd = false; // 标记为「编辑」
this.dialogTitle = '编辑实绩';
// 深拷贝行数据(避免直接修改表格数据)
this.formData = JSON.parse(JSON.stringify(row));
this.dialogVisible = true
},
handlePrint(row) {
// 深拷贝行数据(避免直接修改表格数据)
this.formData = JSON.parse(JSON.stringify(row));
this.printOpen = true
},
// 删除(保持不变)
handleDelete(row) {
this.$confirm(`确定删除成品卷 ${row.exitMatId}?`, '确认删除', { type: 'danger' }).then(() => {
row.deleteLoading = true
this.pdoApi.deletePdo(row.exitMatId, row.planId).then(res => {
if (res.code === 200) {
this.$message.success('删除成功');
this.getPdoList()
} else this.$message.error(res.msg || '删除失败')
}).finally(() => row.deleteLoading = false)
})
},
// 保存回调子组件触发处理实际API调用
handleSave(formData) {
this.saveLoading = true
// 根据是否有id判断是新增还是编辑
const request = formData.id ? this.pdoApi.updatePdo(formData) : this.pdoApi.addPdo(formData)
request.then(res => {
if (res.code === 200) {
this.$message.success('保存成功');
this.dialogVisible = false;
this.getPdoList() // 刷新列表
} else this.$message.error(res.msg || '保存失败')
}).finally(() => {
this.saveLoading = false
})
},
}
}
</script>
<style lang="scss" scoped>
.pdo-container {
height: calc(100vh - 84px);
display: flex;
flex-direction: column;
padding: 15px;
background: #f5f5f5;
overflow: hidden;
}
.pdo-header {
background: #ffffff;
border: 1px solid #d4d4d4;
border-radius: 2px;
padding: 15px;
margin-bottom: 15px;
flex-shrink: 0;
}
.card-grid-container {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 5px;
margin-bottom: 15px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
}
}
.card-col {
margin-bottom: 10px;
}
.parameter-card {
border: 1px solid #d4d4d4;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s;
&.card-selected {
border-color: #999;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
background: #fafafa;
}
::v-deep .el-card__header {
padding: 6px 8px;
background: #f8f8f8;
border-bottom: 1px solid #e0e0e0;
}
&:hover {
border-color: #999;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
}
.card-header {
.card-title {
font-weight: 600;
font-size: 13px;
color: #333;
margin-bottom: 2px;
line-height: 1.4;
}
.card-subtitle {
font-size: 11px;
color: #888;
line-height: 1.3;
}
}
.card-body {
.param-groups-row {
display: flex;
gap: 8px;
.param-group {
flex: 1;
min-width: 0;
.group-title {
font-size: 11px;
font-weight: 600;
color: #555;
padding: 3px 0;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 4px;
background: #f5f5f5;
padding-left: 4px;
padding-right: 4px;
margin-left: -4px;
margin-right: -4px;
text-align: center;
}
.param-list {
.param-line {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
border-bottom: 1px dotted #e8e8e8;
&:last-child {
border-bottom: none;
}
.param-label {
color: #777;
font-size: 11px;
flex-shrink: 0;
margin-right: 8px;
}
.param-value {
color: #333;
font-weight: 500;
font-size: 11px;
text-align: right;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
}
.card-footer {
display: flex;
justify-content: flex-end;
gap: 6px;
padding-top: 6px;
border-top: 1px solid #e0e0e0;
margin-top: 6px;
.el-button {
font-size: 11px;
padding: 2px 6px;
}
}
.empty-data {
text-align: center;
padding: 40px 0;
}
.statistics-container {
flex: 0 0 auto;
background: #ffffff;
border: 1px solid #d4d4d4;
border-radius: 2px;
padding: 0;
display: flex;
flex-direction: column;
height: 280px;
}
.statistics-header {
padding: 8px 15px;
background: #f8f8f8;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
.selected-info {
display: flex;
align-items: center;
font-size: 13px;
&.empty {
color: #999;
i {
color: #bbb;
margin-right: 6px;
}
}
i {
color: #67c23a;
margin-right: 6px;
font-size: 14px;
}
.selected-label {
color: #666;
margin-right: 6px;
font-weight: 500;
}
.selected-value {
color: #333;
font-weight: 600;
margin-right: 8px;
}
.selected-detail {
color: #888;
font-size: 12px;
}
}
}
.statistics-content {
flex: 1;
min-height: 0;
padding: 12px 15px;
}
.summary-col {
height: 100%;
}
.summary-wrapper {
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid #e8e8e8;
padding-right: 15px;
overflow: hidden;
}
.summary-header {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
}
.chart-col {
height: 100%;
}
.chart-wrapper {
height: 100%;
padding-left: 15px;
}
</style>

View File

@@ -0,0 +1,687 @@
<template>
<div class="roller-page" v-loading="loading">
<el-row :gutter="12" style="height: 90vh;">
<!-- 历史记录表格区域 -->
<el-col :span="18" class="panel-col" style="height: 100%;">
<div class="panel-card">
<div class="history-container">
<div class="history-table" ref="historyWrapper" @mouseleave="handleTableLeave">
<el-table
:data="tableData"
border
stripe
class="compact-table"
height="70vh"
:show-overflow-tooltip="true"
@cell-mouse-enter="handleCellEnter"
@row-mouseleave="handleRowLeave"
>
<el-table-column prop="changeid" label="换辊号" align="center" show-overflow-tooltip />
<el-table-column prop="rollid" label="轧辊号" align="center" show-overflow-tooltip />
<el-table-column prop="standid" label="机架号" align="center" show-overflow-tooltip />
<el-table-column label="位置" align="center" prop="position">
<template slot-scope="scope">
<dict-tag :options="dict.type.main_roll_position" :value="scope.row.position" />
</template>
</el-table-column>
<el-table-column label="类型" align="center" prop="type">
<template slot-scope="scope">
<dict-tag :options="dict.type.main_roll_type" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column prop="changeType" label="换辊类型" align="center" show-overflow-tooltip />
<el-table-column prop="changeTime" label="换辊时间" align="center" show-overflow-tooltip />
<el-table-column prop="instalTime" label="安装时间" align="center" show-overflow-tooltip />
<el-table-column prop="deinstalTime" label="拆卸时间" align="center" show-overflow-tooltip />
</el-table>
<transition name="el-fade-in-linear">
<div
v-if="tooltipVisible && hoveredRow"
class="row-tooltip"
:style="tooltipStyle"
ref="rowTooltip"
>
<div class="tooltip-title">详细信息</div>
<div class="tooltip-list">
<div
class="tooltip-item"
v-for="field in detailFields"
:key="field.prop"
>
<span class="label">{{ field.label }}</span>
<span class="value">{{ formatTooltipValue(hoveredRow, field) }}</span>
</div>
</div>
</div>
</transition>
</div>
<el-pagination
v-show="pagination.total>0"
:total="pagination.total"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@current-change="getList"
@size-change="handleSizeChange"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</div>
</el-col>
<!-- 筛选条件区域 -->
<el-col :span="6" class="panel-col" style="height: 100%;">
<div class="panel-card">
<div class="filter-container">
<div class="filter-panel">
<div class="panel-header">
<h4>历史检索</h4>
</div>
<div class="filter-item">
<div class="filter-label"> 按时间</div>
<div class="filter-content time-range">
<div class="time-label">开始时间</div>
<el-date-picker
v-model="startDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 140px"
></el-date-picker>
<el-time-picker
v-model="startTime"
format="HH:mm"
placeholder="选择时间"
value-format="HH:mm"
style="width: 140px; margin-left: 5px"
></el-time-picker>
<div class="time-label" style="margin-top: 10px">结束时间</div>
<el-date-picker
v-model="endDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 140px"
></el-date-picker>
<el-time-picker
v-model="endTime"
format="HH:mm"
placeholder="选择时间"
value-format="HH:mm"
style="width: 140px; margin-left: 5px"
></el-time-picker>
</div>
</div>
<div class="filter-item">
<div class="filter-label"> 按换辊号</div>
<div class="filter-content">
<div class="input-label">换辊号</div>
<el-select filterable v-model="queryParams.changeId" placeholder="请选择" clearable style="width: 240px">
<el-option
v-for="item in changeIdOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</div>
</div>
<div class="filter-item">
<div class="filter-label"> 按轧辊号</div>
<div class="filter-content">
<div class="input-label">轧辊号</div>
<el-select filterable v-model="queryParams.rollId" placeholder="请选择" clearable style="width: 240px">
<el-option
v-for="item in rollIdOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</div>
</div>
<div class="filter-buttons">
<el-button type="primary" @click="handleSearch"> </el-button>
<el-button @click="resetQuery"> </el-button>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import createFetch from '@/api/l2/roller'
export default {
name: 'Roller',
dicts: ['main_roll_type','main_roll_position'],
data() {
return {
// 原有父组件数据
onlineData: [],
loading: false,
historyParams: {},
// FilterVue 组件数据
activeUrl: '140.143.206.120:18081',
queryParams: {
changeId: '',
rollId: ''
},
startDate: '',
startTime: '',
endDate: '',
endTime: '',
changeIdOptions: [],
rollIdOptions: [],
rollerApi: undefined,
// History 组件数据
tableData: [],
pagination: {
currentPage: 1,
pageSize: 15,
total: 0
},
tooltipVisible: false,
tooltipStyle: {
top: '0px',
left: '0px'
},
hoveredRow: null,
searching: false,
searchForm: {
changeId: '',
rollId: '',
changeTimeRange: [],
instalTimeRange: [],
deinstalTimeRange: []
},
detailFields: [
{ label: '换辊号', prop: 'changeid' },
{ label: '轧辊号', prop: 'rollid' },
{ label: '机架号', prop: 'standid' },
{ label: '位置', prop: 'position', dict: 'main_roll_position' },
{ label: '类型', prop: 'type', dict: 'main_roll_type' },
{ label: '换辊类型', prop: 'changeType' },
{ label: '换辊时间', prop: 'changeTime' },
{ label: '安装时间', prop: 'instalTime' },
{ label: '拆卸时间', prop: 'deinstalTime' },
{ label: '直径', prop: 'diameter' },
{ label: '粗糙度', prop: 'rough' },
{ label: '凸度', prop: 'crown' },
{ label: '成分', prop: 'composition' },
{ label: '磨削次数', prop: 'grindCount' },
{ label: '轧制重量', prop: 'rolledWeight' },
{ label: '轧制数量', prop: 'rolledCount' },
{ label: '轧制长度', prop: 'rolledLength' },
{ label: '总轧制重量', prop: 'totalRolledWeight' },
{ label: '总轧制长度', prop: 'totalRolledLength' },
{ label: '总轧制数量', prop: 'totalRolledCount' }
]
}
},
created() {
// 初始化筛选组件数据
this.rollerApi = createFetch(this.activeUrl);
this.getChangeIdOptions()
this.getRollIdOptions()
this.setDefaultTimeRange()
this.fetchHistoryData()
},
methods: {
setHistoryParams(params) {
console.log(params, 'params,setHistoryParams');
this.historyParams = params
// 触发数据查询
this.pagination.currentPage = 1
this.fetchHistoryData()
},
// ========== FilterVue 组件方法 ==========
getChangeIdOptions() {
this.rollerApi.getChangeIdList().then(res => {
console.log('换辊号列表响应:', res)
if (res.code === 200 && res.data) {
this.changeIdOptions = res.data
} else {
this.$message.error(res.msg || '获取换辊号列表失败')
}
}).catch(error => {
console.error('获取换辊号列表失败', error)
this.$message.error('获取换辊号列表失败:' + (error.message || '未知错误'))
})
},
getRollIdOptions() {
this.rollerApi.getRollIdList().then(res => {
console.log('轧辊号列表响应:', res)
if (res.code === 200 && res.data) {
this.rollIdOptions = res.data
} else {
this.$message.error(res.msg || '获取轧辊号列表失败')
}
}).catch(error => {
console.error('获取轧辊号列表失败', error)
this.$message.error('获取轧辊号列表失败:' + (error.message || '未知错误'))
})
},
formatDateStr(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
formatTimeStr(date) {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
},
setDefaultTimeRange() {
this.startDate = '2020-01-01'
this.startTime = '10:00'
this.setEndToNow()
},
setEndToNow() {
const now = new Date()
this.endDate = this.formatDateStr(now)
this.endTime = this.formatTimeStr(now)
},
handleSearch() {
this.setEndToNow()
// 构建搜索参数
const params = {
startTime: `${this.startDate} ${this.startTime}:00`,
endTime: `${this.endDate} ${this.endTime}:00`,
changeId: this.queryParams.changeId || '',
rollid: this.queryParams.rollId || ''
}
console.log('搜索参数:', params)
this.historyParams = params
this.pagination.currentPage = 1
this.fetchHistoryData()
},
resetQuery() {
this.queryParams = {
changeId: '',
rollId: ''
}
this.setDefaultTimeRange()
this.historyParams = {}
this.pagination.currentPage = 1
this.fetchHistoryData()
},
// ========== History 组件方法 ==========
fetchHistoryData() {
const requestBody = {
...this.historyParams,
...this.buildSearchParams(),
pageNum: this.pagination.currentPage,
pageSize: this.pagination.pageSize
}
const loading = this.$loading({
lock: true,
text: '加载中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
return this.rollerApi.getRollHistorytList(requestBody)
.then(res => {
if (res.code === 200 && res.data) {
this.tableData = res.data.list || []
this.pagination.total = res.data.total || 0
this.pagination.currentPage = res.data.pageNum || 1
this.tableData.forEach(item => {
if (item.changeTime) item.changeTime = this.formatDate(item.changeTime)
if (item.instalTime) item.instalTime = this.formatDate(item.instalTime)
if (item.deinstalTime) item.deinstalTime = this.formatDate(item.deinstalTime)
})
} else {
this.$message.error(res.msg || '获取数据失败')
this.tableData = []
this.pagination.total = 0
}
})
.catch(error => {
console.error('获取轧辊历史数据失败', error)
this.$message.error('获取数据失败:' + (error.message || '未知错误'))
this.tableData = []
this.pagination.total = 0
})
.finally(() => {
loading.close()
})
},
formatDate(dateStr) {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} catch (error) {
return dateStr
}
},
getList() {
this.fetchHistoryData()
},
handleSizeChange(pageSize) {
this.pagination.pageSize = pageSize
this.fetchHistoryData()
},
buildSearchParams() {
const params = {}
if (this.searchForm.changeId) {
params.changeId = this.searchForm.changeId
}
if (this.searchForm.rollId) {
params.rollid = this.searchForm.rollId
}
const [changeStart, changeEnd] = this.searchForm.changeTimeRange || []
if (changeStart && changeEnd) {
params.changeTimeBegin = changeStart
params.changeTimeEnd = changeEnd
}
const [instalStart, instalEnd] = this.searchForm.instalTimeRange || []
if (instalStart && instalEnd) {
params.instalTimeBegin = instalStart
params.instalTimeEnd = instalEnd
}
const [deinstalStart, deinstalEnd] = this.searchForm.deinstalTimeRange || []
if (deinstalStart && deinstalEnd) {
params.deinstalTimeBegin = deinstalStart
params.deinstalTimeEnd = deinstalEnd
}
return params
},
handleCellEnter(row, column, cell, event) {
if (!row || !event) return
this.hoveredRow = row
this.tooltipVisible = true
this.updateTooltipPosition(event)
},
handleRowLeave() {
this.tooltipVisible = false
this.hoveredRow = null
},
handleTableLeave() {
this.tooltipVisible = false
this.hoveredRow = null
},
updateTooltipPosition(event) {
this.$nextTick(() => {
const wrapper = this.$refs.historyWrapper
const tooltipEl = this.$refs.rowTooltip
if (!wrapper || !tooltipEl) return
const wrapperRect = wrapper.getBoundingClientRect()
const tooltipRect = tooltipEl.getBoundingClientRect()
let left = event.clientX - wrapperRect.left + 16
let top = event.clientY - wrapperRect.top + 12
if (left + tooltipRect.width > wrapperRect.width) {
left = wrapperRect.width - tooltipRect.width - 8
}
if (left < 8) left = 8
if (top + tooltipRect.height > wrapperRect.height) {
top = wrapperRect.height - tooltipRect.height - 8
}
if (top < 8) top = 8
this.tooltipStyle = {
top: `${top}px`,
left: `${left}px`
}
})
},
formatTooltipValue(row, field) {
const value = row[field.prop]
if (value === null || value === undefined || value === '') {
return '-'
}
if (field.dict && this.dict && this.dict.type && this.dict.type[field.dict]) {
const match = this.dict.type[field.dict].find(item => item.value === value)
return match ? match.label : value
}
return value
}
}
}
</script>
<style lang="scss" scoped>
.roller-page {
padding: 15px 20px 20px;
background: #f5f5f5;
min-height: calc(100vh - 84px);
box-sizing: border-box;
}
.panel-row {
margin-bottom: 12px;
align-items: stretch;
&:last-child {
margin-bottom: 0;
}
}
.panel-col {
display: flex;
}
.panel-card {
background: #ffffff;
border: 1px solid #dcdcdc;
border-radius: 4px;
padding: 12px 0;
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.tall-panel {
height: 100%;
}
.bottom-row .panel-card {
min-height: 360px;
}
// Filter 组件样式
.filter-container {
height: 100%;
padding: 0;
display: flex;
flex-direction: column;
.filter-panel {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
border-radius: 4px;
box-sizing: border-box;
}
.panel-header {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.filter-item {
margin-bottom: 20px;
.filter-label {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.filter-content {
padding-left: 15px;
.input-label {
margin-bottom: 5px;
}
}
.time-range {
.time-label {
margin-bottom: 5px;
}
}
}
.filter-buttons {
text-align: center;
.el-button {
width: 100px;
margin: 0 10px;
}
}
}
// History 组件样式
.history-container {
border-radius: 4px;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
.history-search-panel {
margin-bottom: 8px;
background: #ffffff;
.history-form {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 8px 12px;
}
}
.history-table {
flex: 1;
overflow: hidden;
position: relative;
margin-bottom: 10px;
background: #ffffff;
.compact-table {
width: 100%;
}
}
.pagination-container {
margin: 10px 0;
text-align: right;
}
.button-container {
display: flex;
justify-content: center;
padding: 10px 0;
.el-button + .el-button {
margin-left: 20px;
}
}
}
::v-deep .el-table th,
::v-deep .el-table td {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.row-tooltip {
position: absolute;
background: #ffffff;
border: 1px solid #dcdcdc;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px 14px;
pointer-events: none;
z-index: 5;
max-height: 70%;
overflow: auto;
}
.tooltip-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.tooltip-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px 12px;
}
.tooltip-item {
font-size: 12px;
display: flex;
line-height: 1.5;
.label {
color: #666;
margin-right: 4px;
white-space: nowrap;
}
.value {
flex: 1;
color: #333;
font-weight: 500;
word-break: break-all;
}
}
</style>

View File

@@ -0,0 +1,609 @@
<template>
<div class="stoppage-management">
<!-- 查询表单区域 -->
<div class="stop-header">
<el-form :inline="true" :model="queryForm" ref="queryForm" label-width="80px" size="small">
<el-form-item label="开始时间" prop="startDate">
<el-date-picker v-model="queryForm.startDate" type="date" placeholder="选择开始时间" value-format="yyyy-MM-dd"
:clearable="true"></el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endDate">
<el-date-picker v-model="queryForm.endDate" type="date" placeholder="选择结束时间" value-format="yyyy-MM-dd"
:clearable="true"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="btnLoading" icon="el-icon-search">查询</el-button>
<el-button @click="handleReset" icon="el-icon-refresh">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 卡片网格布局 -->
<div v-loading="tableLoading" class="card-grid-container">
<el-row :gutter="10">
<el-col
v-for="(item, index) in tableData"
:key="index"
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="5"
class="card-col"
>
<el-card
class="parameter-card"
shadow="never"
:body-style="{ padding: '8px' }"
:class="{ 'card-selected': currentRow.stopid === item.stopid }"
@click.native="handleRowClick(item)"
>
<div slot="header" class="card-header">
<div class="card-title">停机ID: {{ item.stopid || '-' }}</div>
<div class="card-subtitle">钢卷号: {{ item.coilid || '-' }}</div>
</div>
<div class="card-body">
<div class="param-groups-row">
<!-- 基本信息 -->
<div class="param-group">
<div class="group-title">基本信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">停机类型:</span>
<span class="param-value">{{ item.stopType || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">:</span>
<span class="param-value">{{ item.shift || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">:</span>
<span class="param-value">{{ item.crew || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">区域:</span>
<span class="param-value">{{ item.area || '-' }}</span>
</div>
</div>
</div>
<!-- 设备信息 -->
<div class="param-group">
<div class="group-title">设备信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">设备:</span>
<span class="param-value">{{ item.seton || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">机组:</span>
<span class="param-value">{{ item.unit || '-' }}</span>
</div>
<div class="param-line">
<span class="param-label">停机时长:</span>
<span class="param-value">{{ item.duration || '-' }} 分钟</span>
</div>
<div class="param-line">
<span class="param-label">更新时间:</span>
<span class="param-value">{{ formatTime(item.insdate) }}</span>
</div>
</div>
</div>
<!-- 时间信息 -->
<div class="param-group">
<div class="group-title">时间信息</div>
<div class="param-list">
<div class="param-line">
<span class="param-label">开始时间:</span>
<span class="param-value">{{ formatTime(item.startDate) }}</span>
</div>
<div class="param-line">
<span class="param-label">结束时间:</span>
<span class="param-value">{{ formatTime(item.endDate) }}</span>
</div>
<div class="param-line full-width" v-if="item.remark">
<span class="param-label">停机原因:</span>
<span class="param-value">{{ item.remark }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<el-button
size="mini"
type="primary"
icon="el-icon-edit"
@click.stop="handleEdit(item)"
>编辑</el-button>
<el-button
size="mini"
type="danger"
icon="el-icon-delete"
:loading="item.deleteLoading"
@click.stop="handleDelete(item)"
>删除</el-button>
</div>
</el-card>
</el-col>
</el-row>
<div v-if="tableData.length === 0 && !tableLoading" class="empty-data">
<el-empty description="暂无数据"></el-empty>
</div>
</div>
<!-- 编辑/新增弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="60%" :close-on-click-modal="false">
<el-form :model="formData" ref="formData" label-width="120px" :rules="formRules">
<el-form-item label="停机ID" prop="stopid" v-if="formData.stopid">
<el-input v-model="formData.stopid" disabled></el-input>
</el-form-item>
<el-form-item label="钢卷号" prop="coilid">
<el-input v-model="formData.coilid" placeholder="请输入钢卷号"></el-input>
</el-form-item>
<el-form-item label="班" prop="shift">
<el-select v-model="formData.shift" placeholder="请选择班次" clearable>
<el-option label="早班" value="早班"></el-option>
<el-option label="中班" value="中班"></el-option>
<el-option label="晚班" value="晚班"></el-option>
</el-select>
</el-form-item>
<el-form-item label="组" prop="crew">
<el-input v-model="formData.crew" placeholder="请输入组号"></el-input>
</el-form-item>
<el-form-item label="区域" prop="area">
<el-input v-model="formData.area" placeholder="请输入区域"></el-input>
</el-form-item>
<el-form-item label="机组" prop="unit">
<el-input v-model="formData.unit" placeholder="请输入机组"></el-input>
</el-form-item>
<el-form-item label="设备" prop="seton">
<el-input v-model="formData.seton" placeholder="请输入设备"></el-input>
</el-form-item>
<el-form-item label="开始时间" prop="startDate">
<el-date-picker v-model="formData.startDate" type="datetime" placeholder="选择开始时间"
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endDate">
<el-date-picker v-model="formData.endDate" type="datetime" placeholder="选择结束时间"
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
</el-form-item>
<el-form-item label="停机类型" prop="stopType">
<el-select v-model="formData.stopType" placeholder="请选择停机类型" clearable>
<el-option label="计划停机" value="计划停机"></el-option>
<el-option label="故障停机" value="故障停机"></el-option>
<el-option label="维护停机" value="维护停机"></el-option>
<el-option label="其他" value="其他"></el-option>
</el-select>
</el-form-item>
<el-form-item label="停机原因" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入停机原因" type="textarea" rows="4"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import createFetch from '@/api/l2/stop'; // 导入接口
export default {
name: 'StoppageManagement',
data() {
return {
// 查询表单数据
queryForm: {
// 只保留年月日YYYY-mm-dd
startDate: '2023-08-13',
endDate: '2025-08-20'
},
activeUrl: '140.143.206.120:18081',
// 表格数据
tableData: [],
// 加载状态
tableLoading: false,
btnLoading: false,
// 弹窗状态
dialogVisible: false,
dialogTitle: '新增停机记录',
// 表单数据
formData: {
stopid: '',
coilid: '',
shift: '',
crew: '',
area: '',
unit: '',
seton: '',
startDate: '',
endDate: '',
duration: 0,
insdate: '',
stopType: '',
remark: ''
},
// 表单验证规则
formRules: {
startDate: [
{ required: true, message: '请选择开始时间', trigger: 'blur' }
],
endDate: [
{ required: true, message: '请选择结束时间', trigger: 'blur' }
],
seton: [
{ required: true, message: '请输入设备', trigger: 'blur' }
]
},
// 保存按钮加载状态
saveLoading: false,
// 当前操作的行数据
currentRow: {},
stopApi: undefined,
};
},
created() {
// 页面加载时默认查询一次
this.stopApi = createFetch(this.activeUrl);
this.getStoppageList();
},
methods: {
// 获取停机记录列表
getStoppageList() {
this.tableLoading = true;
this.stopApi.listStoppage(this.queryForm)
.then(response => {
this.tableLoading = false;
this.btnLoading = false;
this.tableData = response.data.map(item => ({
...item,
deleteLoading: false
}));
})
.catch(error => {
this.tableLoading = false;
this.btnLoading = false;
console.error('获取数据失败:', error);
this.$message.error('获取数据失败,请稍后重试');
});
},
// 处理查询
handleQuery() {
// 验证开始时间不能晚于结束时间
if (this.queryForm.startDate && this.queryForm.endDate &&
new Date(this.queryForm.startDate) > new Date(this.queryForm.endDate)) {
this.$message.warning('开始时间不能晚于结束时间');
return;
}
this.btnLoading = true;
this.getStoppageList();
},
// 处理重置
handleReset() {
this.$refs.queryForm.resetFields();
this.getStoppageList();
},
// 行点击事件
handleRowClick(row) {
this.currentRow = row;
},
// 格式化时间
formatTime(time) {
if (!time) return '-';
// 如果是日期时间格式,直接返回
if (time.includes(' ')) {
return time;
}
// 如果是日期格式,返回日期
return time;
},
// 新增
handleAdd() {
this.dialogTitle = '新增停机记录';
this.formData = {
stopid: '',
coilid: '',
shift: '',
crew: '',
area: '',
unit: '',
seton: '',
startDate: '',
endDate: '',
duration: 0,
insdate: '',
stopType: '',
remark: ''
};
this.dialogVisible = true;
},
// 编辑
handleEdit(row) {
this.dialogTitle = '编辑停机记录';
this.formData = { ...row };
this.dialogVisible = true;
},
// 删除
handleDelete(row) {
this.$confirm(`确定要删除ID为 ${row.stopid} 的停机记录吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(() => {
// 设置当前行的删除按钮为加载状态
row.deleteLoading = true;
this.stopApi.deleteStoppage(row.stopid)
.then(response => {
row.deleteLoading = false;
if (response.code === 200) {
this.$message.success('删除成功');
// 重新查询数据
this.getStoppageList();
} else {
this.$message.error(response.msg || '删除失败');
}
})
.catch(error => {
row.deleteLoading = false;
console.error('删除失败:', error);
this.$message.error('删除失败,请稍后重试');
});
}).catch(() => {
// 用户取消删除
this.$message.info('已取消删除');
});
},
// 保存
handleSave() {
this.$refs.formData.validate(valid => {
if (valid) {
// 验证开始时间不能晚于结束时间
if (new Date(this.formData.startDate) > new Date(this.formData.endDate)) {
this.$message.warning('开始时间不能晚于结束时间');
return;
}
// 计算停机时长(分钟)
const start = new Date(this.formData.startDate).getTime();
const end = new Date(this.formData.endDate).getTime();
this.formData.duration = Math.round((end - start) / (1000 * 60) * 10) / 10;
// 设置当前时间为更新时间
const now = new Date();
this.formData.insdate = now.toISOString().slice(0, 19).replace('T', ' ');
this.saveLoading = true;
// 调用更新接口
this.stopApi.updateStoppage(this.formData)
.then(response => {
this.saveLoading = false;
if (response.code === 200) {
this.$message.success('保存成功');
this.dialogVisible = false;
// 重新查询数据
this.getStoppageList();
} else {
this.$message.error(response.msg || '保存失败');
}
})
.catch(error => {
this.saveLoading = false;
console.error('保存失败:', error);
this.$message.error('保存失败,请稍后重试');
});
}
});
}
}
};
</script>
<style lang="scss" scoped>
.stoppage-management {
height: calc(100vh - 84px);
display: flex;
flex-direction: column;
padding: 15px;
background: #f5f5f5;
overflow: hidden;
}
.stop-header {
background: #ffffff;
border: 1px solid #d4d4d4;
border-radius: 2px;
padding: 15px;
margin-bottom: 15px;
flex-shrink: 0;
}
.card-grid-container {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 5px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
}
}
.card-col {
margin-bottom: 10px;
}
.parameter-card {
border: 1px solid #d4d4d4;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s;
&.card-selected {
border-color: #999;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
background: #fafafa;
}
::v-deep .el-card__header {
padding: 6px 8px;
background: #f8f8f8;
border-bottom: 1px solid #e0e0e0;
}
&:hover {
border-color: #999;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
}
.card-header {
.card-title {
font-weight: 600;
font-size: 13px;
color: #333;
margin-bottom: 2px;
line-height: 1.4;
}
.card-subtitle {
font-size: 11px;
color: #888;
line-height: 1.3;
}
}
.card-body {
.param-groups-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
.param-group {
flex: 1;
min-width: 0;
.group-title {
font-size: 11px;
font-weight: 600;
color: #555;
padding: 3px 0;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 4px;
background: #f5f5f5;
padding-left: 4px;
padding-right: 4px;
margin-left: -4px;
margin-right: -4px;
text-align: center;
}
.param-list {
.param-line {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
border-bottom: 1px dotted #e8e8e8;
&.full-width {
flex-direction: column;
align-items: flex-start;
.param-label {
margin-bottom: 2px;
}
.param-value {
text-align: left;
word-break: break-word;
white-space: normal;
}
}
&:last-child {
border-bottom: none;
}
.param-label {
color: #777;
font-size: 11px;
flex-shrink: 0;
margin-right: 8px;
}
.param-value {
color: #333;
font-weight: 500;
font-size: 11px;
text-align: right;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
}
.card-footer {
display: flex;
justify-content: flex-end;
gap: 6px;
padding-top: 6px;
border-top: 1px solid #e0e0e0;
margin-top: 6px;
.el-button {
font-size: 11px;
padding: 2px 6px;
}
}
.empty-data {
text-align: center;
padding: 40px 0;
}
::v-deep .el-form-item {
margin-bottom: 15px;
}
</style>