feat(zinc): 新增PDO接口和日期筛选组件,重构生产统计页面
refactor(api): 移除冗余API路径前缀 style(websocket): 移除重复的日志打印 feat(label): 新增原料标签组件并替换生产标签 refactor(lines): 移除废弃的流程图组件和配置 feat(utils): 新增日期范围工具函数 feat(statistic): 集成日期筛选和图表分析功能
This commit is contained in:
9
klp-ui/src/api/lines/zinc/pdo.js
Normal file
9
klp-ui/src/api/lines/zinc/pdo.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import zinc1Request from '@/utils/zinc1Request'
|
||||
|
||||
export function listPdo(queryParams) {
|
||||
return zinc1Request({
|
||||
url: '/pdo/list',
|
||||
method: 'post',
|
||||
data: queryParams
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import zinc1Request from '@/utils/zinc1Request'
|
||||
|
||||
export function listDeviceEnumAll() {
|
||||
return zinc1Request({
|
||||
url: '/api/deviceEnum/all',
|
||||
url: '/deviceEnum/all',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import zinc1Request from '@/utils/zinc1Request'
|
||||
|
||||
export function getDeviceFieldMetaAll() {
|
||||
return zinc1Request({
|
||||
url: '/api/deviceFieldMeta/all',
|
||||
url: '/deviceFieldMeta/all',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import zinc1Request from '@/utils/zinc1Request'
|
||||
// 获取最新N条设备快照
|
||||
export function listDeviceSnapshotLatest(params) {
|
||||
return zinc1Request({
|
||||
url: '/api/deviceSnapshot/latest',
|
||||
url: '/deviceSnapshot/latest',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
@@ -12,7 +12,7 @@ export function listDeviceSnapshotLatest(params) {
|
||||
// 按时间范围查询设备快照
|
||||
export function listDeviceSnapshotRange(params) {
|
||||
return zinc1Request({
|
||||
url: '/api/deviceSnapshot/range',
|
||||
url: '/deviceSnapshot/range',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
|
||||
@@ -23,6 +23,8 @@ class WebSocketManager {
|
||||
this.disconnect(type)
|
||||
}
|
||||
|
||||
console.log(`[WebSocket] 正在连接: ${type}`)
|
||||
|
||||
const url = `${this.baseUrl}?type=${type}`
|
||||
console.log(`[WebSocket] 正在连接: ${type}`)
|
||||
|
||||
|
||||
248
klp-ui/src/views/lines/components/DateViewPicker.vue
Normal file
248
klp-ui/src/views/lines/components/DateViewPicker.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="date-filter-view" style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;">
|
||||
<!-- 视图模式选择(Vue2 v-model 绑定value) -->
|
||||
<el-select
|
||||
v-model="innerViewMode"
|
||||
placeholder="选择视图模式"
|
||||
style="width: 120px;"
|
||||
>
|
||||
<el-option label="日视图" value="day"></el-option>
|
||||
<el-option label="月视图" value="month"></el-option>
|
||||
<el-option label="年视图" value="year"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 日视图:日期范围选择 -->
|
||||
<div v-if="innerViewMode === 'day'">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"
|
||||
@change="handleDateRangeChange"
|
||||
style="width: 350px;"
|
||||
></el-date-picker>
|
||||
</div>
|
||||
|
||||
<!-- 月视图:月份范围选择 -->
|
||||
<div v-if="innerViewMode === 'month'">
|
||||
<el-date-picker
|
||||
v-model="monthRange"
|
||||
type="monthrange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始月份"
|
||||
end-placeholder="结束月份"
|
||||
format="yyyy-MM"
|
||||
value-format="yyyy-MM"
|
||||
@change="handleMonthRangeChange"
|
||||
style="width: 350px;"
|
||||
></el-date-picker>
|
||||
</div>
|
||||
|
||||
<!-- 年视图:年份选择 -->
|
||||
<div v-if="innerViewMode === 'year'">
|
||||
<el-date-picker
|
||||
v-model="selectedYear"
|
||||
type="year"
|
||||
placeholder="选择年份"
|
||||
format="yyyy"
|
||||
value-format="yyyy"
|
||||
@change="handleYearChange"
|
||||
style="width: 200px;"
|
||||
></el-date-picker>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 导入日期工具函数(Vue2 导入语法不变)
|
||||
import { getCurrentMonthFirstDayOrToday } from '../utils/monent'; // 请根据实际路径调整
|
||||
|
||||
export default {
|
||||
name: 'DateFilterView',
|
||||
// Vue2 Props 定义(v-model 对应 value,而非 modelValue)
|
||||
props: {
|
||||
// Vue2 v-model 绑定的视图模式(day/month/year)
|
||||
value: {
|
||||
type: String,
|
||||
default: 'day',
|
||||
validator: (val) => ['day', 'month', 'year'].includes(val)
|
||||
},
|
||||
// .sync 双向绑定的开始时间
|
||||
startTime: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// .sync 双向绑定的结束时间
|
||||
endTime: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 内部视图模式(避免直接修改props)
|
||||
innerViewMode: this.value,
|
||||
dateRange: [], // 日视图日期范围
|
||||
monthRange: [], // 月视图月份范围
|
||||
selectedYear: '' // 年视图选中年份
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Vue2 监听外部 v-model 变化(监听 value)
|
||||
value(newVal) {
|
||||
this.innerViewMode = newVal;
|
||||
// 视图变化时初始化对应默认值
|
||||
this.initViewDefault(newVal);
|
||||
},
|
||||
// 监听内部视图模式变化,触发外部事件
|
||||
innerViewMode(newVal) {
|
||||
// Vue2 v-model 更新事件为 input(而非 update:modelValue)
|
||||
this.$emit('input', newVal);
|
||||
// 触发视图切换自定义事件
|
||||
this.$emit('view-change', newVal);
|
||||
// 清空其他视图的选择器
|
||||
this.clearOtherViewData(newVal);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化默认视图的日期值
|
||||
this.initViewDefault(this.innerViewMode);
|
||||
// 如果外部传入了startTime/endTime,同步到内部选择器
|
||||
this.syncExternalDateToInner();
|
||||
},
|
||||
methods: {
|
||||
// 初始化对应视图的默认日期
|
||||
initViewDefault(viewMode) {
|
||||
if (viewMode === 'day') {
|
||||
this.initDayViewDefault();
|
||||
} else if (viewMode === 'month') {
|
||||
this.initMonthViewDefault();
|
||||
} else if (viewMode === 'year') {
|
||||
this.initYearViewDefault();
|
||||
}
|
||||
},
|
||||
// 清空非当前视图的选择器数据
|
||||
clearOtherViewData(currentView) {
|
||||
if (currentView !== 'day') this.dateRange = [];
|
||||
if (currentView !== 'month') this.monthRange = [];
|
||||
if (currentView !== 'year') this.selectedYear = '';
|
||||
},
|
||||
// 同步外部传入的startTime/endTime到内部选择器
|
||||
syncExternalDateToInner() {
|
||||
if (!this.startTime || !this.endTime) return;
|
||||
|
||||
if (this.innerViewMode === 'day') {
|
||||
this.dateRange = [this.startTime, this.endTime];
|
||||
} else if (this.innerViewMode === 'month') {
|
||||
// 从startTime/endTime提取月份(yyyy-MM)
|
||||
const startMonth = this.startTime.slice(0, 7);
|
||||
const endMonth = this.endTime.slice(0, 7);
|
||||
this.monthRange = [startMonth, endMonth];
|
||||
} else if (this.innerViewMode === 'year') {
|
||||
// 从startTime提取年份(yyyy)
|
||||
this.selectedYear = this.startTime.slice(0, 4);
|
||||
}
|
||||
},
|
||||
// 初始化日视图默认值:当前月第一天-今天
|
||||
initDayViewDefault() {
|
||||
if (this.startTime && this.endTime) {
|
||||
this.dateRange = [this.startTime, this.endTime];
|
||||
return;
|
||||
}
|
||||
// 调用工具函数获取默认值
|
||||
const [firstDay, today] = getCurrentMonthFirstDayOrToday();
|
||||
this.dateRange = [firstDay, today];
|
||||
// 更新外部startTime/endTime(Vue2 .sync 触发 update:xxx)
|
||||
this.updateStartAndEndTime(firstDay, today);
|
||||
},
|
||||
// 初始化月视图默认值:当前月-当前月
|
||||
initMonthViewDefault() {
|
||||
if (this.startTime && this.endTime) {
|
||||
const startMonth = this.startTime.slice(0, 7);
|
||||
const endMonth = this.endTime.slice(0, 7);
|
||||
this.monthRange = [startMonth, endMonth];
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
const currentMonth = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||
this.monthRange = [currentMonth, currentMonth];
|
||||
// 转换为该月第一天和最后一天
|
||||
const startDate = `${currentMonth}-01`;
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const endDate = `${lastDay.getFullYear()}-${(lastDay.getMonth() + 1).toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`;
|
||||
this.updateStartAndEndTime(startDate, endDate);
|
||||
},
|
||||
// 初始化年视图默认值:当前年
|
||||
initYearViewDefault() {
|
||||
if (this.startTime && this.endTime) {
|
||||
this.selectedYear = this.startTime.slice(0, 4);
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear().toString();
|
||||
this.selectedYear = currentYear;
|
||||
// 转换为该年1月1日和12月31日
|
||||
const startDate = `${currentYear}-01-01`;
|
||||
const endDate = `${currentYear}-12-31`;
|
||||
this.updateStartAndEndTime(startDate, endDate);
|
||||
},
|
||||
// 更新开始/结束时间(Vue2 .sync 触发 update:startTime/update:endTime)
|
||||
updateStartAndEndTime(start, end) {
|
||||
this.$emit('update:startTime', start);
|
||||
this.$emit('update:endTime', end);
|
||||
// 触发日期变更自定义事件
|
||||
this.$emit('date-change', { startTime: start, endTime: end });
|
||||
},
|
||||
// 日视图日期范围变化处理
|
||||
handleDateRangeChange(val) {
|
||||
if (val && val.length === 2) {
|
||||
this.updateStartAndEndTime(val[0], val[1]);
|
||||
} else {
|
||||
this.updateStartAndEndTime(undefined, undefined);
|
||||
}
|
||||
},
|
||||
// 月视图月份范围变化处理
|
||||
handleMonthRangeChange(val) {
|
||||
if (val && val.length === 2) {
|
||||
// 开始月份转该月第一天
|
||||
const startDate = `${val[0]}-01`;
|
||||
// 结束月份转该月最后一天
|
||||
const [endYear, endMonth] = val[1].split('-');
|
||||
const lastDay = new Date(endYear, endMonth, 0);
|
||||
const endDate = `${lastDay.getFullYear()}-${(lastDay.getMonth() + 1).toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`;
|
||||
this.updateStartAndEndTime(startDate, endDate);
|
||||
}
|
||||
},
|
||||
// 年视图年份变化处理
|
||||
handleYearChange(val) {
|
||||
if (val) {
|
||||
const startDate = `${val}-01-01`;
|
||||
const endDate = `${val}-12-31`;
|
||||
this.updateStartAndEndTime(startDate, endDate);
|
||||
}
|
||||
},
|
||||
// 查询按钮点击事件
|
||||
handleQuery() {
|
||||
this.$emit('query', {
|
||||
viewMode: this.innerViewMode,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.date-filter-view {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<v-stage :config="stageSize">
|
||||
<v-layer>
|
||||
<!-- 设备分组:每个分组包含矩形和对应的文字 -->
|
||||
<v-group
|
||||
v-for="rect in innerRects"
|
||||
:key="rect.id"
|
||||
@click="handleRectClick(rect)"
|
||||
:style="{ cursor: rect.config.cursor || 'default' }"
|
||||
>
|
||||
<!-- 矩形元素:根据选中状态动态设置边框颜色 -->
|
||||
<v-rect
|
||||
:config="{
|
||||
...rect.config,
|
||||
// 选中时边框为橘色,未选中时使用默认边框颜色
|
||||
stroke: rect.id === selectedRectId ? '#4874cb' : rect.config.stroke,
|
||||
// 选中时可以增加边框宽度,增强视觉效果
|
||||
strokeWidth: rect.id === selectedRectId ? 3 : rect.config.strokeWidth
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 如果存在meta.rollid,则显示钢卷号 -->
|
||||
<v-text
|
||||
v-if="rect.meta && rect.meta.matId"
|
||||
:config="{
|
||||
x: rect.config.x + rect.config.width / 4,
|
||||
y: rect.config.y + rect.config.height / 2,
|
||||
text: '[' + rect.meta.matId + ']',
|
||||
fill: 'black',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle'
|
||||
}"
|
||||
/>
|
||||
<!-- 让x轴方向文字居中 -->
|
||||
<v-text
|
||||
:config="{
|
||||
x: rect.config.x + rect.config.width / 2 - rect.textConfig.text.length * 5,
|
||||
y: rect.config.y,
|
||||
text: rect.textConfig.text,
|
||||
fill: rect.textConfig.fill || 'black',
|
||||
fontSize: rect.textConfig.fontSize || 14,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle'
|
||||
}"
|
||||
/>
|
||||
</v-group>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<v-line
|
||||
v-for="line in lines"
|
||||
:key="line.id"
|
||||
:config="line.config"
|
||||
/>
|
||||
</v-layer>
|
||||
</v-stage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PreciseFlowChart',
|
||||
props: {
|
||||
matMapList: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
rects: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
lines: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 舞台配置
|
||||
stageSize: {
|
||||
width: 1000,
|
||||
height: 650,
|
||||
background: 'white'
|
||||
},
|
||||
innerRects: [],
|
||||
// 记录当前选中的矩形ID,初始为null(无选中)
|
||||
selectedRectId: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
matMapList: {
|
||||
handler(newVal) {
|
||||
console.log('matMapList', newVal);
|
||||
// 根据matMapList中每一项的positionNameEn字段和rects中的id字段进行匹配
|
||||
this.innerRects = [...this.rects];
|
||||
newVal.forEach(item => {
|
||||
const rect = this.innerRects.find(rect => rect.id === item.positionNameEn);
|
||||
if (rect) {
|
||||
this.$set(rect, 'meta', item);
|
||||
}
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 矩形分组点击事件处理
|
||||
handleRectClick(rect) {
|
||||
// 切换选中状态:如果点击的是当前选中项,则取消选中;否则选中当前项
|
||||
if (this.selectedRectId === rect.id) {
|
||||
this.selectedRectId = null;
|
||||
} else {
|
||||
this.selectedRectId = rect.id;
|
||||
}
|
||||
this.$emit('rectClick', rect.meta);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,356 +0,0 @@
|
||||
export const rects = [
|
||||
// 左侧:开卷机
|
||||
{
|
||||
id: 'POR1',
|
||||
config: {
|
||||
x: 40,
|
||||
y: 110,
|
||||
width: 200,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '1#开卷机[POR1]' }
|
||||
},
|
||||
{
|
||||
id: 'POR2',
|
||||
config: {
|
||||
x: 40,
|
||||
y: 220,
|
||||
width: 200,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '2#开卷机[POR2]' }
|
||||
},
|
||||
|
||||
// 中上部:焊机、入口活套
|
||||
{
|
||||
id: 'WELDER',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 30,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '焊机[WELDER]' }
|
||||
},
|
||||
{
|
||||
id: 'ENL1',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 110,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '入口活套1[ENL1]' }
|
||||
},
|
||||
{
|
||||
id: 'ENL2',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 160,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '入口活套2[ENL2]' }
|
||||
},
|
||||
|
||||
// 中下部:清洗段
|
||||
{
|
||||
id: 'CLEAN',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 240,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '清洗段[CLEAN]' }
|
||||
},
|
||||
|
||||
// 右侧上:退火炉1-4
|
||||
{
|
||||
id: 'FUR1',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 70,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '退火炉[FUR1]' }
|
||||
},
|
||||
{
|
||||
id: 'FUR2',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 120,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '退火炉[FUR2]' }
|
||||
},
|
||||
{
|
||||
id: 'FUR3',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 170,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '退火炉[FUR3]' }
|
||||
},
|
||||
{
|
||||
id: 'FUR4',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 220,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '退火炉[FUR4]' }
|
||||
},
|
||||
|
||||
// 右侧中:光整机
|
||||
{
|
||||
id: 'TM',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 400,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '光整机[TM]' }
|
||||
},
|
||||
|
||||
// 右侧下:拉矫机
|
||||
{
|
||||
id: 'TL',
|
||||
config: {
|
||||
x: 600,
|
||||
y: 480,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '拉矫机[TL]' }
|
||||
},
|
||||
|
||||
// 中下:后处理
|
||||
{
|
||||
id: 'COAT',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 360,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '后处理[COAT]' }
|
||||
},
|
||||
|
||||
// 中下:出口活套
|
||||
{
|
||||
id: 'CXL1',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 440,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '出口活套[CXL1]' }
|
||||
},
|
||||
{
|
||||
id: 'CXL2',
|
||||
config: {
|
||||
x: 300,
|
||||
y: 490,
|
||||
width: 220,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '出口活套[CXL2]' }
|
||||
},
|
||||
|
||||
// 左下:卷取机、称重位
|
||||
{
|
||||
id: 'TR',
|
||||
config: {
|
||||
x: 40,
|
||||
y: 380,
|
||||
width: 200,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '卷取机[TR]' }
|
||||
},
|
||||
{
|
||||
id: 'WEIT',
|
||||
config: {
|
||||
x: 40,
|
||||
y: 460,
|
||||
width: 200,
|
||||
height: 50,
|
||||
fill: '#d3d3d3',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
textConfig: { text: '称重位[WEIT]' }
|
||||
}
|
||||
]
|
||||
|
||||
export const lines = [
|
||||
// 1#开卷机 → 焊机
|
||||
{
|
||||
id: 'line-por1-welder',
|
||||
config: {
|
||||
points: [
|
||||
40 + 200, 110 + 25,
|
||||
40 + 200 + 30, 110 + 25,
|
||||
40 + 200 + 30, 30 + 25,
|
||||
300, 30 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
},
|
||||
// 2#开卷机 → 焊机
|
||||
{
|
||||
id: 'line-por2-welder',
|
||||
config: {
|
||||
points: [
|
||||
40 + 200, 220 + 25,
|
||||
40 + 200 + 30, 220 + 25,
|
||||
40 + 200 + 30, 30 + 25,
|
||||
300, 30 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
},
|
||||
// 清洗段 → 退火炉1
|
||||
{
|
||||
id: 'line-clean-fur1',
|
||||
config: {
|
||||
points: [
|
||||
300 + 220, 240 + 25,
|
||||
300 + 220 + 40, 240 + 25,
|
||||
300 + 220 + 40, 70 + 25,
|
||||
600, 70 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
},
|
||||
// 退火炉4 → 光整机
|
||||
{
|
||||
id: 'line-fur4-tm',
|
||||
config: {
|
||||
points: [
|
||||
600 + 220, 220 + 25,
|
||||
600 + 220 + 40, 220 + 25,
|
||||
600 + 220 + 40, 400 + 25,
|
||||
600 + 220, 400 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
},
|
||||
// 拉矫机 → 后处理
|
||||
{
|
||||
id: 'line-tl-coat',
|
||||
config: {
|
||||
points: [
|
||||
600, 480 + 25,
|
||||
600 - 40, 480 + 25,
|
||||
600 - 40, 360 + 25,
|
||||
600 - 80, 360 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
},
|
||||
// 出口活套2 → 卷取机
|
||||
{
|
||||
id: 'line-cxl2-tr',
|
||||
config: {
|
||||
points: [
|
||||
300, 490 + 25,
|
||||
300 - 30, 490 + 25,
|
||||
300 - 30, 380 + 25,
|
||||
300 - 60, 380 + 25
|
||||
],
|
||||
stroke: '#686868',
|
||||
strokeWidth: 2,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round'
|
||||
}
|
||||
}
|
||||
]
|
||||
46
klp-ui/src/views/lines/utils/monent.js
Normal file
46
klp-ui/src/views/lines/utils/monent.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 获取当前月的第一天和今天的日期(返回数组:[第一天, 今天])
|
||||
* @param {string} format 日期格式,默认 'yyyy-MM-dd'
|
||||
* @returns {string[]} [当前月第一天, 今天]
|
||||
*/
|
||||
export function getCurrentMonthFirstDayOrToday(format = 'yyyy-MM-dd') {
|
||||
// 重写日期格式化逻辑,移除RegExp.$1的使用
|
||||
const dateFormatter = (date, fmt) => {
|
||||
const o = {
|
||||
'y+': date.getFullYear().toString(), // 年(转为字符串方便补位)
|
||||
'M+': (date.getMonth() + 1).toString(), // 月
|
||||
'd+': date.getDate().toString(), // 日
|
||||
};
|
||||
|
||||
// 处理年份:替换yyyy/yy等格式
|
||||
fmt = fmt.replace(/(y+)/, (match, p1) => {
|
||||
// p1是匹配到的y+(比如yyyy/yy),取年份后p1.length位
|
||||
return o['y+'].slice(-p1.length).padStart(p1.length, '0');
|
||||
});
|
||||
|
||||
// 处理月份:替换MM/M等格式
|
||||
fmt = fmt.replace(/(M+)/, (match, p1) => {
|
||||
return o['M+'].padStart(p1.length, '0');
|
||||
});
|
||||
|
||||
// 处理日期:替换dd/d等格式
|
||||
fmt = fmt.replace(/(d+)/, (match, p1) => {
|
||||
return o['d+'].padStart(p1.length, '0');
|
||||
});
|
||||
|
||||
return fmt;
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
// 1. 获取当前月第一天
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// 2. 获取今天
|
||||
const today = new Date();
|
||||
|
||||
// 3. 按指定格式格式化日期
|
||||
const firstDayStr = dateFormatter(firstDay, format);
|
||||
const todayStr = dateFormatter(today, format);
|
||||
|
||||
// 返回 [第一天, 今天] 数组,适配之前Vue代码中dateRange的格式
|
||||
return [firstDayStr, todayStr];
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
<template>
|
||||
<div class="report-page" style="padding: 20px;">
|
||||
<!-- 集成日期筛选组件 -->
|
||||
<DateFilterView v-model="viewMode" :startTime.sync="queryParams.startDate" :endTime.sync="queryParams.endDate"
|
||||
@view-change="handleViewChange" @date-change="handleDateChange" @query="queryData" />
|
||||
|
||||
<!-- 汇总统计信息 el-descriptions 展示 -->
|
||||
<el-card shadow="hover" class="summary-card" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-size: 16px; font-weight: bold;">生产报表-数据汇总</span>
|
||||
</div>
|
||||
<el-descriptions
|
||||
:data="productStatistic"
|
||||
title=""
|
||||
border
|
||||
column="4"
|
||||
size="middle"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-descriptions :data="productStatistic" title="" border :column="4" size="middle" label-width="140px">
|
||||
<el-descriptions-item label="总出口宽度(mm)">{{ productStatistic.totalExitWidth || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总出口长度(m)">{{ formatNum(productStatistic.totalExitLength) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总理论重量(kg)">{{ formatNum(productStatistic.totalTheoryWeight) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总实际重量(kg)">{{ formatNum(productStatistic.totalActualWeight) }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="总出口厚度(mm)">{{ formatNum(productStatistic.totalExitThickness) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总理论重量(kg)">{{ formatNum(productStatistic.totalTheoryWeight)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="总实际重量(kg)">{{ formatNum(productStatistic.totalActualWeight)
|
||||
}}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="总出口厚度(mm)">{{ formatNum(productStatistic.totalExitThickness)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均出口宽度(mm)">{{ formatNum(productStatistic.avgExitWidth) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均出口长度(m)">{{ formatNum(productStatistic.avgExitLength) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均理论重量(kg)">{{ formatNum(productStatistic.avgTheoryWeight) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均理论重量(kg)">{{ formatNum(productStatistic.avgTheoryWeight)
|
||||
}}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="平均实际重量(kg)">{{ formatNum(productStatistic.avgActualWeight) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均出口厚度(mm)">{{ formatNum(productStatistic.avgExitThickness) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总来料重量(kg)">{{ formatNum(productStatistic.totalEntryWeight) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均实际重量(kg)">{{ formatNum(productStatistic.avgActualWeight)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="平均出口厚度(mm)">{{ formatNum(productStatistic.avgExitThickness)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="总来料重量(kg)">{{ formatNum(productStatistic.totalEntryWeight)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="总卷数">{{ productStatistic.coilCount || 0 }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="成材率" span="4" label-width="140px">
|
||||
@@ -34,21 +38,51 @@
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增ECharts图表区域 -->
|
||||
<el-card shadow="hover" class="chart-card" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-size: 16px; font-weight: bold;">钢卷尺寸分析</span>
|
||||
<!-- 新增汇总指标选择器 -->
|
||||
<el-select v-model="summaryDimension" size="small" style="float: right; width: 180px;" @change="updateCharts">
|
||||
<el-option label="出口宽度范围汇总" value="exitWidth"></el-option>
|
||||
<el-option label="出口长度范围汇总" value="exitLength"></el-option>
|
||||
<el-option label="出口厚度范围汇总" value="exitThickness"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<!-- 图表3:尺寸范围汇总柱状图(替换原折线图) -->
|
||||
<div class="chart-item full-width">
|
||||
<h4 style="text-align: center; margin: 10px 0;">{{ dimensionTitleMap[summaryDimension] }}</h4>
|
||||
<div id="dimensionBarChart" class="chart" style="width: 100%; height: 350px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 图表1:生产状态分布饼图 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-size: 16px; font-weight: bold;">生产状态分析</span>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-item">
|
||||
<h4 style="text-align: center; margin: 10px 0;">生产状态分布</h4>
|
||||
<div id="statusPieChart" class="chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
<!-- 图表2:各钢种实际重量柱状图 -->
|
||||
<div class="chart-item">
|
||||
<h4 style="text-align: center; margin: 10px 0;">各钢种实际重量对比</h4>
|
||||
<div id="steelWeightBarChart" class="chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 明细数据 el-table 展示 -->
|
||||
<el-card shadow="hover" class="detail-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-size: 16px; font-weight: bold;">生产报表-明细数据</span>
|
||||
</div>
|
||||
<el-table
|
||||
:data="reportDetails"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
v-loading="tableLoading"
|
||||
element-loading-text="加载中..."
|
||||
style="width: 100%;"
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table :data="reportDetails" border stripe size="small" v-loading="tableLoading" element-loading-text="加载中..."
|
||||
style="width: 100%;" highlight-current-row>
|
||||
<el-table-column prop="exitMatId" label="出口物料编码" align="center" />
|
||||
<el-table-column prop="entryMatId" label="入口物料编码" align="center" />
|
||||
<el-table-column prop="groupNo" label="班组号" align="center" width="60" />
|
||||
@@ -81,14 +115,61 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getReportSummary, getReportDetails } from '@/api/lines/zinc/report'
|
||||
// 导入接口
|
||||
import { listPdo } from '@/api/lines/zinc/pdo';
|
||||
// 导入封装的日期筛选组件(路径根据实际项目调整)
|
||||
import DateFilterView from '../../components/DateViewPicker.vue';
|
||||
// 导入ECharts
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
// 注册组件
|
||||
components: {
|
||||
DateFilterView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
productStatistic: {}, // 汇总数据
|
||||
reportDetails: [], // 明细数据
|
||||
tableLoading: false, // 表格加载状态
|
||||
// 查询参数:统一格式为yyyy-MM-dd(由组件双向绑定)
|
||||
queryParams: {
|
||||
startDate: '', // 开始日期
|
||||
endDate: '', // 结束日期
|
||||
},
|
||||
viewMode: 'day', // 视图模式:day/month/year(绑定组件v-model)
|
||||
// ECharts实例存储
|
||||
chartInstances: {
|
||||
statusPie: null,
|
||||
steelWeightBar: null,
|
||||
dimensionBar: null // 替换原dimensionLine
|
||||
},
|
||||
// 汇总维度:默认出口宽度
|
||||
summaryDimension: 'exitWidth',
|
||||
// 维度标题映射
|
||||
dimensionTitleMap: {
|
||||
exitWidth: '出口宽度范围汇总(卷数)',
|
||||
exitLength: '出口长度范围汇总(卷数)',
|
||||
exitThickness: '出口厚度范围汇总(卷数)'
|
||||
},
|
||||
// 状态英文转中文映射
|
||||
statusCnMap: {
|
||||
UNWEIGHT: '未称重',
|
||||
COMPLETED: '已完成',
|
||||
FINISHED: '已结束',
|
||||
RUNNING: '运行中',
|
||||
CANCELLED: '已取消',
|
||||
UNKNOWN: '未知'
|
||||
},
|
||||
// 钢铁行业配色方案(工业风)
|
||||
steelIndustryColors: {
|
||||
// 饼图配色(沉稳的工业色系)
|
||||
pie: ['#2E4057', '#4A6FA5', '#C1403D', '#8B7D6B', '#596F62', '#708090'],
|
||||
// 钢种重量柱状图(藏蓝色系,钢铁行业主色)
|
||||
steelWeightBar: '#2E4057',
|
||||
// 尺寸范围柱状图(铁锈红/工业青)
|
||||
dimensionBar: '#C1403D'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -112,34 +193,422 @@ export default {
|
||||
const ss = date.getSeconds().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hh}:${mm}:${ss}`
|
||||
},
|
||||
// 获取汇总数据
|
||||
async getReportSummary() {
|
||||
try {
|
||||
const res = await getReportSummary()
|
||||
this.productStatistic = res || {}
|
||||
} catch (err) {
|
||||
this.$message.error('获取汇总数据失败!')
|
||||
console.error(err)
|
||||
|
||||
// 视图切换事件回调(可选,用于额外逻辑处理)
|
||||
handleViewChange(viewMode) {
|
||||
console.log('视图切换为:', viewMode);
|
||||
// 可添加视图切换后的额外逻辑,比如重置表格、提示等
|
||||
},
|
||||
|
||||
// 日期变更事件回调(可选)
|
||||
handleDateChange({ startTime, endTime }) {
|
||||
console.log('日期范围变更:', startTime, endTime);
|
||||
// 可添加日期变更后的额外逻辑
|
||||
},
|
||||
|
||||
// 前端计算汇总数据
|
||||
calculateSummary(data) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return {
|
||||
totalExitWidth: 0,
|
||||
totalExitLength: 0,
|
||||
totalTheoryWeight: 0,
|
||||
totalActualWeight: 0,
|
||||
totalExitThickness: 0,
|
||||
avgExitWidth: 0,
|
||||
avgExitLength: 0,
|
||||
avgTheoryWeight: 0,
|
||||
avgActualWeight: 0,
|
||||
avgExitThickness: 0,
|
||||
totalEntryWeight: 0,
|
||||
coilCount: 0,
|
||||
yieldRate: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 初始化汇总变量
|
||||
let totalExitWidth = 0;
|
||||
let totalExitLength = 0;
|
||||
let totalTheoryWeight = 0;
|
||||
let totalActualWeight = 0;
|
||||
let totalExitThickness = 0;
|
||||
let totalEntryWeight = 0;
|
||||
const coilCount = data.length;
|
||||
|
||||
// 遍历明细数据累加总值
|
||||
data.forEach(item => {
|
||||
// 空值/非数字处理,避免NaN
|
||||
totalExitWidth += Number(item.exitWidth) || 0;
|
||||
totalExitLength += Number(item.exitLength) || 0;
|
||||
totalTheoryWeight += Number(item.theoryWeight) || 0;
|
||||
totalActualWeight += Number(item.actualWeight) || 0;
|
||||
totalExitThickness += Number(item.exitThickness) || 0;
|
||||
totalEntryWeight += Number(item.entryWeight) || 0;
|
||||
});
|
||||
|
||||
// 计算平均值(避免除以0)
|
||||
const avgExitWidth = coilCount > 0 ? totalExitWidth / coilCount : 0;
|
||||
const avgExitLength = coilCount > 0 ? totalExitLength / coilCount : 0;
|
||||
const avgTheoryWeight = coilCount > 0 ? totalTheoryWeight / coilCount : 0;
|
||||
const avgActualWeight = coilCount > 0 ? totalActualWeight / coilCount : 0;
|
||||
const avgExitThickness = coilCount > 0 ? totalExitThickness / coilCount : 0;
|
||||
|
||||
// 计算成材率(总实际重量/总来料重量,避免除以0)
|
||||
const yieldRate = totalEntryWeight > 0 ? totalActualWeight / totalEntryWeight : 0;
|
||||
|
||||
// 返回汇总结果
|
||||
return {
|
||||
totalExitWidth,
|
||||
totalExitLength,
|
||||
totalTheoryWeight,
|
||||
totalActualWeight,
|
||||
totalExitThickness,
|
||||
avgExitWidth,
|
||||
avgExitLength,
|
||||
avgTheoryWeight,
|
||||
avgActualWeight,
|
||||
avgExitThickness,
|
||||
totalEntryWeight,
|
||||
coilCount,
|
||||
yieldRate
|
||||
};
|
||||
},
|
||||
|
||||
// 生成维度范围(根据不同指标生成对应区间)
|
||||
getDimensionRanges(dimension) {
|
||||
switch (dimension) {
|
||||
case 'exitWidth': // 出口宽度(mm)
|
||||
return [
|
||||
'0-500mm',
|
||||
'500-1000mm',
|
||||
'1000-1500mm',
|
||||
'1500-2000mm',
|
||||
'2000mm以上'
|
||||
];
|
||||
case 'exitLength': // 出口长度(m)
|
||||
return [
|
||||
'0-500m',
|
||||
'500-1000m',
|
||||
'1000-1500m',
|
||||
'1500-2000m',
|
||||
'2000m以上'
|
||||
];
|
||||
case 'exitThickness': // 出口厚度(mm)
|
||||
return [
|
||||
'0-0.5mm',
|
||||
'0.5-1mm',
|
||||
'1-2mm',
|
||||
'2-5mm',
|
||||
'5mm以上'
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 获取明细数据
|
||||
async getReportDetails() {
|
||||
this.tableLoading = true
|
||||
try {
|
||||
const res = await getReportDetails()
|
||||
this.reportDetails = res || []
|
||||
} catch (err) {
|
||||
this.$message.error('获取明细数据失败!')
|
||||
console.error(err)
|
||||
} finally {
|
||||
this.tableLoading = false
|
||||
|
||||
// 统计维度范围的卷数
|
||||
countDimensionRange(data, dimension) {
|
||||
const ranges = this.getDimensionRanges(dimension);
|
||||
const rangeCount = {};
|
||||
// 初始化各范围计数为0
|
||||
ranges.forEach(range => {
|
||||
rangeCount[range] = 0;
|
||||
});
|
||||
|
||||
data.forEach(item => {
|
||||
const value = Number(item[dimension]) || 0;
|
||||
let rangeKey = '';
|
||||
|
||||
switch (dimension) {
|
||||
case 'exitWidth':
|
||||
if (value < 500) rangeKey = '0-500mm';
|
||||
else if (value < 1000) rangeKey = '500-1000mm';
|
||||
else if (value < 1500) rangeKey = '1000-1500mm';
|
||||
else if (value < 2000) rangeKey = '1500-2000mm';
|
||||
else rangeKey = '2000mm以上';
|
||||
break;
|
||||
case 'exitLength':
|
||||
if (value < 500) rangeKey = '0-500m';
|
||||
else if (value < 1000) rangeKey = '500-1000m';
|
||||
else if (value < 1500) rangeKey = '1000-1500m';
|
||||
else if (value < 2000) rangeKey = '1500-2000m';
|
||||
else rangeKey = '2000m以上';
|
||||
break;
|
||||
case 'exitThickness':
|
||||
if (value < 0.5) rangeKey = '0-0.5mm';
|
||||
else if (value < 1) rangeKey = '0.5-1mm';
|
||||
else if (value < 2) rangeKey = '1-2mm';
|
||||
else if (value < 5) rangeKey = '2-5mm';
|
||||
else rangeKey = '5mm以上';
|
||||
break;
|
||||
}
|
||||
|
||||
if (rangeCount.hasOwnProperty(rangeKey)) {
|
||||
rangeCount[rangeKey]++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ranges: ranges,
|
||||
counts: ranges.map(range => rangeCount[range])
|
||||
};
|
||||
},
|
||||
|
||||
// 初始化ECharts实例
|
||||
initCharts() {
|
||||
// 1. 生产状态分布饼图
|
||||
this.chartInstances.statusPie = echarts.init(document.getElementById('statusPieChart'));
|
||||
// 2. 钢种重量柱状图
|
||||
this.chartInstances.steelWeightBar = echarts.init(document.getElementById('steelWeightBarChart'));
|
||||
// 3. 尺寸范围汇总柱状图(替换原折线图)
|
||||
this.chartInstances.dimensionBar = echarts.init(document.getElementById('dimensionBarChart'));
|
||||
|
||||
// 监听窗口大小变化,自适应图表
|
||||
window.addEventListener('resize', () => {
|
||||
Object.values(this.chartInstances).forEach(chart => {
|
||||
if (chart) chart.resize();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 生成无数据配置
|
||||
getEmptyChartOption(titleText = '暂无数据') {
|
||||
return {
|
||||
title: {
|
||||
text: titleText,
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#999', fontSize: 14 }
|
||||
},
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { show: false },
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
series: []
|
||||
};
|
||||
},
|
||||
|
||||
// 更新图表数据
|
||||
updateCharts() {
|
||||
const data = this.reportDetails;
|
||||
const hasData = Array.isArray(data) && data.length > 0;
|
||||
|
||||
// ========== 1. 生产状态分布饼图 ==========
|
||||
if (hasData) {
|
||||
// 统计各状态的卷数(汉化)
|
||||
const statusMap = {};
|
||||
data.forEach(item => {
|
||||
const status = item.status || 'UNKNOWN';
|
||||
const statusCn = this.statusCnMap[status] || '未知';
|
||||
statusMap[statusCn] = (statusMap[statusCn] || 0) + 1;
|
||||
});
|
||||
const statusPieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} 卷 ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 0,
|
||||
textStyle: { fontSize: 12, color: '#333' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '生产状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
data: Object.entries(statusMap).map(([name, value]) => ({ name, value })),
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 11,
|
||||
formatter: '{b}: {c}卷',
|
||||
color: '#333'
|
||||
}
|
||||
}
|
||||
],
|
||||
// 使用钢铁行业饼图配色
|
||||
color: this.steelIndustryColors.pie
|
||||
};
|
||||
// 强制覆盖配置,解决暂无数据文字残留问题
|
||||
this.chartInstances.statusPie.setOption(statusPieOption, { notMerge: true });
|
||||
} else {
|
||||
this.chartInstances.statusPie.setOption(this.getEmptyChartOption('暂无生产状态数据'), { notMerge: true });
|
||||
}
|
||||
|
||||
// ========== 2. 各钢种实际重量柱状图 ==========
|
||||
if (hasData) {
|
||||
// 统计各钢种的总实际重量
|
||||
const steelWeightMap = {};
|
||||
data.forEach(item => {
|
||||
const steelGrade = item.steelGrade || '未知钢种';
|
||||
const weight = Number(item.actualWeight) || 0;
|
||||
steelWeightMap[steelGrade] = (steelWeightMap[steelGrade] || 0) + weight;
|
||||
});
|
||||
const steelWeightBarOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}: {c} kg',
|
||||
textStyle: { color: '#333' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Object.keys(steelWeightMap),
|
||||
axisLabel: {
|
||||
rotate: 30,
|
||||
fontSize: 11,
|
||||
color: '#333'
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#ccc' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '实际重量(kg)',
|
||||
nameTextStyle: { fontSize: 12, color: '#333' },
|
||||
axisLabel: { color: '#333' },
|
||||
axisLine: { lineStyle: { color: '#ccc' } },
|
||||
splitLine: { lineStyle: { color: '#eee' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际重量',
|
||||
type: 'bar',
|
||||
data: Object.values(steelWeightMap),
|
||||
itemStyle: {
|
||||
// 钢铁行业藏蓝色系
|
||||
color: this.steelIndustryColors.steelWeightBar,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 10,
|
||||
formatter: '{c}',
|
||||
color: '#333'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.chartInstances.steelWeightBar.setOption(steelWeightBarOption, { notMerge: true });
|
||||
} else {
|
||||
this.chartInstances.steelWeightBar.setOption(this.getEmptyChartOption('暂无钢种重量数据'), { notMerge: true });
|
||||
}
|
||||
|
||||
// ========== 3. 尺寸范围汇总柱状图(替换原折线图) ==========
|
||||
if (hasData) {
|
||||
const dimensionData = this.countDimensionRange(data, this.summaryDimension);
|
||||
const dimensionBarOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}: {c} 卷',
|
||||
textStyle: { color: '#333' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dimensionData.ranges,
|
||||
axisLabel: {
|
||||
rotate: 15,
|
||||
fontSize: 11,
|
||||
color: '#333'
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#ccc' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '卷数',
|
||||
nameTextStyle: { fontSize: 12, color: '#333' },
|
||||
axisLabel: { color: '#333' },
|
||||
axisLine: { lineStyle: { color: '#ccc' } },
|
||||
splitLine: { lineStyle: { color: '#eee' } },
|
||||
min: 0
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '卷数',
|
||||
type: 'bar',
|
||||
data: dimensionData.counts,
|
||||
itemStyle: {
|
||||
// 钢铁行业铁锈红色系
|
||||
color: this.steelIndustryColors.dimensionBar,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 11,
|
||||
formatter: '{c}',
|
||||
color: '#333'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.chartInstances.dimensionBar.setOption(dimensionBarOption, { notMerge: true });
|
||||
} else {
|
||||
this.chartInstances.dimensionBar.setOption(this.getEmptyChartOption(`暂无${this.dimensionTitleMap[this.summaryDimension].split('(')[0]}数据`), { notMerge: true });
|
||||
}
|
||||
},
|
||||
|
||||
// 自定义获取明细数据并计算汇总的方法
|
||||
fetchData() {
|
||||
this.tableLoading = true; // 增加loading状态
|
||||
listPdo(this.queryParams).then(res => {
|
||||
this.reportDetails = res.rows || [];
|
||||
// 前端计算汇总数据
|
||||
this.productStatistic = this.calculateSummary(this.reportDetails);
|
||||
// 更新图表数据
|
||||
this.updateCharts();
|
||||
}).catch(err => {
|
||||
this.$message.error('获取明细数据失败!');
|
||||
console.error(err);
|
||||
// 异常时重置汇总数据
|
||||
this.productStatistic = this.calculateSummary([]);
|
||||
// 清空图表
|
||||
this.reportDetails = [];
|
||||
this.updateCharts();
|
||||
}).finally(() => {
|
||||
this.tableLoading = false; // 关闭loading
|
||||
});
|
||||
},
|
||||
|
||||
// 统一查询方法(组件查询按钮触发)
|
||||
queryData() {
|
||||
// 只调用fetchData,汇总数据在fetchData里计算
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 页面加载时调用两个接口
|
||||
this.getReportSummary()
|
||||
this.getReportDetails()
|
||||
// 初始化图表
|
||||
this.initCharts();
|
||||
// 页面加载时自动触发查询(组件已初始化默认日期)
|
||||
this.queryData();
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁ECharts实例,释放内存
|
||||
Object.values(this.chartInstances).forEach(chart => {
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
});
|
||||
// 移除窗口resize监听
|
||||
window.removeEventListener('resize', () => {
|
||||
Object.values(this.chartInstances).forEach(chart => {
|
||||
if (chart) chart.resize();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -149,7 +618,49 @@ export default {
|
||||
background: #f5f7fa;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
.summary-card, .detail-card {
|
||||
|
||||
.summary-card,
|
||||
.chart-card,
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* 图表容器样式 */
|
||||
.chart-container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.chart-item.full-width {
|
||||
flex: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.chart {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 调整标题样式,贴合工业风 */
|
||||
.chart-item h4 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: 1px solid #eee;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,728 @@
|
||||
<template>
|
||||
<div>
|
||||
实时监控
|
||||
<div class="realtime-monitoring">
|
||||
<!-- 替换uniapp scroll-view为普通div + 滚动样式 -->
|
||||
<div class="scroll-container" v-if="currentTab === 1" ref="scrollContainer">
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span class="status-label">网络状态</span>
|
||||
<span class="status-value" :class="isConnected ? 'status-通畅' : 'status-异常'">
|
||||
{{ isConnected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-divider"></div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">更新</span>
|
||||
<span class="status-value status-time">{{ lastUpdateTime }}</span>
|
||||
</div>
|
||||
<div class="status-divider"></div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">设备数</span>
|
||||
<span class="status-value">{{ deviceDefs.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 带钢位置和速度模块 -->
|
||||
<div class="section">
|
||||
<div class="section-title">带钢状态</div>
|
||||
<div class="strip-status-grid">
|
||||
<!-- 带钢位置卡片 -->
|
||||
<div class="strip-status-card">
|
||||
<span class="strip-status-label">带钢位置</span>
|
||||
<span class="strip-status-value">{{ getRealtimeValueBySource('ENTRY', 'stripLocation') }}</span>
|
||||
<span class="strip-status-unit">m</span>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: stripLocationProgress + '%' }"></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ stripLocationProgress.toFixed(1) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 带钢速度卡片 -->
|
||||
<div class="strip-status-card">
|
||||
<span class="strip-status-label">带钢速度</span>
|
||||
<span class="strip-status-value">{{ getRealtimeValueBySource('ENTRY', 'stripSpeed') }}</span>
|
||||
<span class="strip-status-unit">m/min</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 带钢速度折线图:替换为ECharts容器 -->
|
||||
<div class="chart-box" ref="stripSpeedChart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 实时数据卡片 + 统计图 -->
|
||||
<div class="section">
|
||||
<div class="section-title">入口段</div>
|
||||
<div class="metrics-grid-3">
|
||||
<div class="metric-box" v-for="it in entryMetrics" :key="'entry_' + it.field">
|
||||
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
||||
<span class="metric-value">{{ getRealtimeValueBySource('ENTRY', it.field) }}</span>
|
||||
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box" ref="entryChart"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">退火炉段</div>
|
||||
<div class="metrics-grid-3">
|
||||
<div class="metric-box" v-for="it in furnaceMetrics" :key="'furnace_' + it.actualField">
|
||||
<span class="metric-name">{{ it.label || getFieldLabel(it.actualField) }}</span>
|
||||
<span class="metric-value">{{ getRealtimeValueBySource('FURNACE', it.actualField) }}</span>
|
||||
<span class="metric-unit">{{ it.unit || getFieldUnit(it.actualField) }}</span>
|
||||
<span class="metric-set-value" v-if="it.setField">
|
||||
设定: {{ getRealtimeValueBySource('FURNACE', it.setField) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">后处理/涂层段</div>
|
||||
<div class="metrics-grid-3">
|
||||
<div class="metric-box" v-for="it in coatMetrics" :key="'coat_' + it.field">
|
||||
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
||||
<span class="metric-value">{{ getRealtimeValueBySource('COAT', it.field) }}</span>
|
||||
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box" ref="coatChart"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">出口段</div>
|
||||
<div class="metrics-grid-3">
|
||||
<div class="metric-box" v-for="it in exitMetrics" :key="'exit_' + it.field">
|
||||
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
||||
<span class="metric-value">{{ getRealtimeValueBySource('EXIT', it.field) }}</span>
|
||||
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box" ref="exitChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入ECharts(Vue2常用图表库,替换uniapp的qiun-data-charts)
|
||||
import * as echarts from 'echarts'
|
||||
import WebSocketManager from '@/utils/websocketManager';
|
||||
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
|
||||
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
|
||||
import { listDeviceSnapshotLatest } from '@/api/pocket/deviceSnapshot'
|
||||
// 模拟API请求(实际项目替换为真实接口)
|
||||
const mockApi = {
|
||||
listDeviceEnumAll: () => Promise.resolve({ data: [] }),
|
||||
getDeviceFieldMetaAll: () => Promise.resolve({ data: {} }),
|
||||
listDeviceSnapshotLatest: ({ limit, deviceCode }) => Promise.resolve({ data: [] })
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'RealTimeMonitoring',
|
||||
// Vue2必须有data函数,返回响应式数据
|
||||
data() {
|
||||
return {
|
||||
currentTab: 1, // 默认显示标签1
|
||||
isConnected: false, // 网络连接状态
|
||||
lastUpdateTime: '--', // 最后更新时间
|
||||
deviceDefs: [], // 设备定义列表
|
||||
fieldMeta: {}, // 字段元数据
|
||||
isDestroyed: false, // 组件销毁标志
|
||||
chartMaxPoints: 30, // 图表最大数据点
|
||||
chartSeries: { // 图表系列数据
|
||||
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
||||
furnace: { time: [], jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], nof5FurnaceTemperatureActual: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], potTemperature: [] },
|
||||
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
||||
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
||||
stripSpeed: { time: [], stripSpeed: [] }
|
||||
},
|
||||
latestMeasure: null, // 最新测量数据
|
||||
socketDataAppended: false, // socket数据是否已追加
|
||||
stripLocationProgress: 0, // 带钢位置进度百分比
|
||||
// 各段指标配置
|
||||
entryMetrics: [
|
||||
{ field: 'stripSpeed', label: '带钢速度', unit: 'm/min' },
|
||||
{ field: 'tensionPorBr1', label: '张力POR-BR1', unit: 'kN' },
|
||||
{ field: 'tensionBr1Br2', label: '张力BR1-BR2', unit: 'kN' },
|
||||
{ field: 'tensionBr2Br3', label: '张力BR2-BR3', unit: 'kN' }
|
||||
],
|
||||
furnaceMetrics: [
|
||||
{ actualField: 'jcf1FurnaceTemperatureActual', setField: 'jcf1FurnaceTemperatureSet', label: 'JCF1温度', unit: '℃' },
|
||||
{ actualField: 'jcf2FurnaceTemperatureActual', setField: 'jcf2FurnaceTemperatureSet', label: 'JCF2温度', unit: '℃' },
|
||||
{ actualField: 'lbzFurnaceTemperatureActual', setField: 'lbzFurnaceTemperatureSet', label: 'LBZ温度', unit: '℃' }
|
||||
],
|
||||
coatMetrics: [
|
||||
{ field: 'avrCoatingWeightTop', label: '上涂层重量', unit: 'g/m²' },
|
||||
{ field: 'avrCoatingWeightBottom', label: '下涂层重量', unit: 'g/m²' },
|
||||
{ field: 'airKnifePressure', label: '气刀压力', unit: 'bar' }
|
||||
],
|
||||
exitMetrics: [
|
||||
{ field: 'tensionBr8Br9', label: '张力BR8-BR9', unit: 'kN' },
|
||||
{ field: 'tensionBr9Tr', label: '张力BR9-TR', unit: 'kN' },
|
||||
{ field: 'speedExitSection', label: '出口速度', unit: 'm/min' }
|
||||
],
|
||||
// ECharts实例缓存
|
||||
chartInstances: {
|
||||
stripSpeed: null,
|
||||
entry: null,
|
||||
coat: null,
|
||||
exit: null
|
||||
}
|
||||
}
|
||||
},
|
||||
// Vue2生命周期钩子
|
||||
created() {
|
||||
// 组件创建时加载初始数据
|
||||
this.loadDeviceDefs()
|
||||
this.loadFieldMeta()
|
||||
this.loadHistoryForCharts()
|
||||
// 初始化WebSocket连接
|
||||
this.initSocket()
|
||||
},
|
||||
mounted() {
|
||||
// DOM挂载后初始化图表
|
||||
this.$nextTick(() => {
|
||||
this.initCharts()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 组件销毁前清理资源
|
||||
this.isDestroyed = true
|
||||
// 关闭WebSocket连接
|
||||
if (this.socket) {
|
||||
this.socket.disconnectAll()
|
||||
}
|
||||
// 销毁ECharts实例
|
||||
Object.values(this.chartInstances).forEach(instance => {
|
||||
if (instance) {
|
||||
instance.dispose()
|
||||
}
|
||||
})
|
||||
// 执行socket清理逻辑
|
||||
if (this._clearSocketFlag) {
|
||||
this._clearSocketFlag()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载设备定义
|
||||
async loadDeviceDefs() {
|
||||
try {
|
||||
const res = await listDeviceEnumAll()
|
||||
if (this.isDestroyed) return
|
||||
this.deviceDefs = res.data || []
|
||||
} catch (e) {
|
||||
console.error('加载设备定义失败:', e)
|
||||
}
|
||||
},
|
||||
// 加载字段元数据
|
||||
async loadFieldMeta() {
|
||||
try {
|
||||
const res = await getDeviceFieldMetaAll()
|
||||
if (this.isDestroyed) return
|
||||
this.fieldMeta = res.data || {}
|
||||
} catch (e) {
|
||||
console.error('加载字段元数据失败:', e)
|
||||
}
|
||||
},
|
||||
// 加载图表历史数据
|
||||
async loadHistoryForCharts() {
|
||||
try {
|
||||
// 重置图表系列(保留带钢速度数据)
|
||||
this.chartSeries = {
|
||||
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
||||
furnace: { time: [], jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], nof5FurnaceTemperatureActual: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], potTemperature: [] },
|
||||
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
||||
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
||||
stripSpeed: this.chartSeries.stripSpeed || { time: [], stripSpeed: [] }
|
||||
}
|
||||
|
||||
const limit = this.chartMaxPoints
|
||||
const tasks = []
|
||||
|
||||
const deviceMap = {
|
||||
entry: { deviceCode: 'POR1', fields: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'] },
|
||||
furnace: {
|
||||
deviceCode: 'FUR2',
|
||||
fields: [
|
||||
'jcf1FurnaceTemperatureActual', 'jcf2FurnaceTemperatureActual',
|
||||
'lbzFurnaceTemperatureActual', 'lthFurnaceTemperatureActual',
|
||||
'nof1FurnaceTemperatureActual', 'nof2FurnaceTemperatureActual',
|
||||
'nof3FurnaceTemperatureActual', 'nof4FurnaceTemperatureActual',
|
||||
'nof5FurnaceTemperatureActual', 'phFurnaceTemperatureActual',
|
||||
'rtf1FurnaceTemperatureActual', 'rtf2FurnaceTemperatureActual',
|
||||
'sfFurnaceTemperatureActual', 'tdsFurnaceTemperatureActual',
|
||||
'potTemperature'
|
||||
]
|
||||
},
|
||||
coat: {
|
||||
deviceCode: 'COAT',
|
||||
fields: ['avrCoatingWeightTop', 'avrCoatingWeightBottom', 'airKnifePressure', 'stripSpeedTmExit']
|
||||
},
|
||||
exit: { deviceCode: 'TR', fields: ['tensionBr8Br9', 'tensionBr9Tr', 'speedExitSection'] }
|
||||
}
|
||||
|
||||
// 批量加载各设备历史快照
|
||||
Object.keys(deviceMap).forEach((group) => {
|
||||
const cfg = deviceMap[group]
|
||||
tasks.push(
|
||||
listDeviceSnapshotLatest({ limit, deviceCode: cfg.deviceCode })
|
||||
.then((res) => {
|
||||
if (this.isDestroyed) return
|
||||
const rows = res.data || []
|
||||
const list = rows.slice().reverse() // 按时间升序
|
||||
list.forEach((row) => {
|
||||
if (this.isDestroyed) return
|
||||
const t = (row.createTime || new Date().toTimeString()).slice(0, 8) // HH:mm:ss
|
||||
let snap = {}
|
||||
try {
|
||||
snap = row.snapshotData ? JSON.parse(row.snapshotData) : {}
|
||||
} catch (e) {
|
||||
snap = {}
|
||||
}
|
||||
const values = {}
|
||||
cfg.fields.forEach((f) => {
|
||||
values[f] = snap[f] || 0
|
||||
})
|
||||
this.pushSeries(group, t, values)
|
||||
})
|
||||
})
|
||||
.catch(() => { })
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(tasks)
|
||||
// 历史数据加载完成后更新图表
|
||||
this.updateCharts()
|
||||
} catch (e) {
|
||||
console.error('加载图表历史数据失败:', e)
|
||||
}
|
||||
},
|
||||
// 初始化WebSocket连接(替换uniapp的WebSocketManager为原生WebSocket)
|
||||
initSocket() {
|
||||
const vm = this
|
||||
let isDestroyed = false
|
||||
const pendingUpdates = []
|
||||
|
||||
// 替换为真实的WebSocket地址
|
||||
const wsUrl = 'ws://140.143.206.120:18081/websocket'
|
||||
const socket = new WebSocketManager(wsUrl)
|
||||
this.socket = socket
|
||||
|
||||
console.log('初始化WebSocket连接')
|
||||
socket.connect('track_measure', {
|
||||
onOpen: () => {
|
||||
if (isDestroyed) return
|
||||
vm.isConnected = true
|
||||
console.log('WebSocket连接成功')
|
||||
},
|
||||
onClose: () => {
|
||||
if (isDestroyed) return
|
||||
vm.isConnected = false
|
||||
// 断线重连逻辑
|
||||
if (!vm.isDestroyed) {
|
||||
setTimeout(() => vm.initSocket(), 3000)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isDestroyed) return
|
||||
vm.isConnected = false
|
||||
console.error('WebSocket错误:', error)
|
||||
},
|
||||
onMessage: (event) => {
|
||||
// if (isDestroyed) return
|
||||
console.log('收到消息:', event, vm)
|
||||
if (!vm) {
|
||||
isDestroyed = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析WebSocket消息
|
||||
let payload = null
|
||||
if (typeof event.data === 'string') {
|
||||
payload = JSON.parse(event)
|
||||
} else {
|
||||
payload = event
|
||||
}
|
||||
|
||||
// if (!payload || isDestroyed || !vm) return
|
||||
|
||||
// 异步更新数据,避免阻塞UI
|
||||
const updateFrame = () => {
|
||||
try {
|
||||
vm.latestMeasure = payload
|
||||
|
||||
vm.appendStripSpeedPoint(payload)
|
||||
vm.updateLastTime()
|
||||
// 实时更新图表
|
||||
vm.updateCharts()
|
||||
// 更新带钢位置进度
|
||||
vm.stripLocationProgress = vm.getRealtimeValueBySource('ENTRY', 'stripLocation') * 10 || 0
|
||||
} catch (e) {
|
||||
isDestroyed = true
|
||||
console.error('更新实时数据失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 用requestAnimationFrame优化渲染
|
||||
let frameId = requestAnimationFrame(updateFrame)
|
||||
pendingUpdates.push(frameId)
|
||||
} catch (e) {
|
||||
if (!isDestroyed) {
|
||||
console.error('解析WebSocket数据失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 保存清理函数
|
||||
this._clearSocketFlag = () => {
|
||||
isDestroyed = true
|
||||
// 取消所有待执行的更新
|
||||
pendingUpdates.forEach(id => cancelAnimationFrame(id))
|
||||
pendingUpdates.length = 0
|
||||
// 关闭socket
|
||||
socket.disconnectAll()
|
||||
}
|
||||
},
|
||||
// 初始化ECharts图表实例
|
||||
initCharts() {
|
||||
// 带钢速度图表
|
||||
this.chartInstances.stripSpeed = echarts.init(this.$refs.stripSpeedChart)
|
||||
this.chartInstances.stripSpeed.setOption({
|
||||
title: { text: '带钢速度趋势' },
|
||||
xAxis: { type: 'category', data: this.chartSeries.stripSpeed.time },
|
||||
yAxis: { type: 'value', name: 'm/min' },
|
||||
series: [{ name: '带钢速度', type: 'line', data: this.chartSeries.stripSpeed.stripSpeed }],
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
|
||||
})
|
||||
|
||||
// 入口段图表
|
||||
this.chartInstances.entry = echarts.init(this.$refs.entryChart)
|
||||
// 涂层段图表
|
||||
this.chartInstances.coat = echarts.init(this.$refs.coatChart)
|
||||
// 出口段图表
|
||||
this.chartInstances.exit = echarts.init(this.$refs.exitChart)
|
||||
|
||||
// 监听窗口大小变化,自适应图表
|
||||
window.addEventListener('resize', () => {
|
||||
Object.values(this.chartInstances).forEach(instance => {
|
||||
if (instance) instance.resize()
|
||||
})
|
||||
})
|
||||
},
|
||||
// 更新图表数据
|
||||
updateCharts() {
|
||||
// 更新带钢速度图表
|
||||
if (this.chartInstances?.stripSpeed) {
|
||||
this.chartInstances.stripSpeed.setOption({
|
||||
xAxis: { data: this.chartSeries.stripSpeed.time },
|
||||
series: [{ data: this.chartSeries.stripSpeed.stripSpeed }]
|
||||
})
|
||||
}
|
||||
|
||||
// 更新入口段图表
|
||||
if (this.chartInstances?.entry) {
|
||||
this.chartInstances.entry.setOption(this.toGroupLineChart('entry'))
|
||||
}
|
||||
|
||||
// 更新涂层段图表
|
||||
if (this.chartInstances?.coat) {
|
||||
this.chartInstances.coat.setOption(this.toGroupLineChart('coat'))
|
||||
}
|
||||
|
||||
// 更新出口段图表
|
||||
if (this.chartInstances?.exit) {
|
||||
this.chartInstances.exit.setOption(this.toGroupLineChart('exit'))
|
||||
}
|
||||
},
|
||||
// 向图表系列追加数据
|
||||
pushSeries(group, time, values) {
|
||||
const series = this.chartSeries[group]
|
||||
if (!series) return
|
||||
|
||||
// 控制数据点数量不超过最大值
|
||||
if (series.time.length >= this.chartMaxPoints) {
|
||||
series.time.shift()
|
||||
Object.keys(values).forEach(key => {
|
||||
if (series[key]) series[key].shift()
|
||||
})
|
||||
}
|
||||
|
||||
// 添加新数据
|
||||
series.time.push(time)
|
||||
Object.keys(values).forEach(key => {
|
||||
if (series[key]) {
|
||||
series[key].push(values[key] || 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 追加图表数据点(从WebSocket)
|
||||
appendChartPoint(payload) {
|
||||
const now = new Date().toTimeString().slice(0, 8)
|
||||
// 按设备分组处理payload数据
|
||||
const groups = {
|
||||
entry: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'],
|
||||
coat: ['avrCoatingWeightTop', 'avrCoatingWeightBottom', 'airKnifePressure', 'stripSpeedTmExit'],
|
||||
exit: ['tensionBr8Br9', 'tensionBr9Tr', 'speedExitSection']
|
||||
}
|
||||
|
||||
Object.keys(groups).forEach(group => {
|
||||
const values = {}
|
||||
groups[group].forEach(field => {
|
||||
values[field] = payload[field] || 0
|
||||
})
|
||||
this.pushSeries(group, now, values)
|
||||
})
|
||||
},
|
||||
// 追加带钢速度数据点
|
||||
appendStripSpeedPoint(payload) {
|
||||
const now = new Date().toTimeString().slice(0, 8)
|
||||
const speed = payload.stripSpeed || 0
|
||||
this.pushSeries('stripSpeed', now, { stripSpeed: speed })
|
||||
},
|
||||
// 更新最后更新时间
|
||||
updateLastTime() {
|
||||
this.lastUpdateTime = new Date().toTimeString().slice(0, 8)
|
||||
},
|
||||
// 根据数据源和字段获取实时值
|
||||
getRealtimeValueBySource(source, field) {
|
||||
if (!this.latestMeasure) return '--'
|
||||
// 按source映射到不同的字段前缀/规则
|
||||
const value = this.latestMeasure[field] || 0
|
||||
return value.toFixed(2) // 格式化显示
|
||||
},
|
||||
// 获取字段标签
|
||||
getFieldLabel(field) {
|
||||
return this.fieldMeta[field]?.label || field
|
||||
},
|
||||
// 获取字段单位
|
||||
getFieldUnit(field) {
|
||||
return this.fieldMeta[field]?.unit || ''
|
||||
},
|
||||
// 生成分组折线图配置
|
||||
toGroupLineChart(group) {
|
||||
const seriesData = this.chartSeries[group]
|
||||
if (!seriesData) return {}
|
||||
|
||||
// 构建ECharts系列配置
|
||||
const series = []
|
||||
// 过滤掉time字段,生成每个指标的折线
|
||||
Object.keys(seriesData).forEach(key => {
|
||||
if (key === 'time') return
|
||||
series.push({
|
||||
name: this.getFieldLabel(key),
|
||||
type: 'line',
|
||||
data: seriesData[key],
|
||||
smooth: true // 平滑曲线
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
title: { text: `${this.getGroupTitle(group)}趋势` },
|
||||
xAxis: { type: 'category', data: seriesData.time },
|
||||
yAxis: { type: 'value' },
|
||||
series,
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 20 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
|
||||
}
|
||||
},
|
||||
// 获取分组标题
|
||||
getGroupTitle(group) {
|
||||
const titles = {
|
||||
entry: '入口段',
|
||||
coat: '后处理/涂层段',
|
||||
exit: '出口段'
|
||||
}
|
||||
return titles[group] || group
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.realtime-monitoring {
|
||||
width: 100%;
|
||||
height: calc(100vh - 124px);
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 状态栏样式 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-通畅 {
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
.status-异常 {
|
||||
color: #f53f3f;
|
||||
}
|
||||
|
||||
.status-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #eee;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* 通用区块样式 */
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* 带钢状态样式 */
|
||||
.strip-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.strip-status-card {
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.strip-status-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.strip-status-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.strip-status-unit {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 指标网格样式 */
|
||||
.metrics-grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
padding: 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.metric-set-value {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 图表容器样式 */
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,466 +0,0 @@
|
||||
<template>
|
||||
<div class="graph-container-box">
|
||||
<el-row>
|
||||
<el-col :span="16">
|
||||
<knova-stage @rectClick="selectCard" :matMapList="matMapList" :rects="rects" :lines="lines"></knova-stage>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div style="border: 1px solid #000; padding: 10px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<!-- 调整工具,选择两个位置,两个下拉选,分别双向绑定 -->
|
||||
<el-form :model="adjustForm" ref="adjustForm" label-width="80px">
|
||||
<el-form-item label="当前位置" prop="current">
|
||||
<el-select v-model="adjustForm.current" placeholder="请选择当前位置">
|
||||
<el-option v-for="item in matMapList" :key="item.positionNameEn" :label="item.positionNameCn" :value="item.positionNameEn"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标位置" prop="target">
|
||||
<el-select v-model="adjustForm.target" placeholder="请选择目标位置">
|
||||
<el-option v-for="item in matMapList" :key="item.positionNameEn" :label="item.positionNameCn" :value="item.positionNameEn"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" :disabled="!adjustForm.current || !adjustForm.target" @click="handleConfirmAdjust">确认调整</el-button>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #000; padding: 10px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<el-row v-if="selectedCard">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">位置名称:</span>
|
||||
<span class="detail-value">{{ selectedCard.positionNameCn || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">位置代号:</span>
|
||||
<span class="detail-value">{{ selectedCard.positionNameEn || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">钢卷号:</span>
|
||||
<span class="detail-value">{{ selectedCard.matId || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">计划ID:</span>
|
||||
<span class="detail-value">{{ selectedCard.planId || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">计划号:</span>
|
||||
<span class="detail-value">{{ selectedCard.planNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">开卷机编号:</span>
|
||||
<span class="detail-value">{{ selectedCard.porIdx || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">卷取机编号:</span>
|
||||
<span class="detail-value">{{ selectedCard.trIdx || '-' }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<!-- 加载状态 -->
|
||||
<div class="empty-tip" v-if="isLoadingReturn">加载回退信息中...</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div class="empty-tip" v-else-if="returnError" style="color: #f56c6c;">
|
||||
{{ returnError }}
|
||||
</div>
|
||||
|
||||
<!-- 回退信息内容 -->
|
||||
<div class="detail-list" v-else-if="Object.keys(returnInfo).length > 0">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">回退钢卷号:</span>
|
||||
<span class="detail-value">{{ returnInfo.entryMatId || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">回退计划ID:</span>
|
||||
<span class="detail-value">{{ returnInfo.planId || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">回退计划号:</span>
|
||||
<span class="detail-value">{{ returnInfo.planNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">回退类型:</span>
|
||||
<span class="detail-value">{{ returnInfo.returnType || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">回退重量:</span>
|
||||
<span class="detail-value">
|
||||
{{ returnInfo.returnWeight || '-' }}
|
||||
{{ returnInfo.returnWeight ? 'kg' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无回退信息 -->
|
||||
<div class="empty-tip" v-else>无回退信息</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-else>
|
||||
<div class="empty-tip">请选择钢卷卡片查看详情</div>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #000; padding: 10px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<el-row v-if="selectedCard">
|
||||
<div class="operation-panel">
|
||||
<div class="panel-content">
|
||||
<!-- 非调整模式:显示操作按钮 -->
|
||||
<div class="operation-buttons">
|
||||
<div class="button-group">
|
||||
<el-button size="mini" type="primary" @click="handleOperate(selectedCard, 'ONLINE')"
|
||||
class="btn-block">
|
||||
钢卷上线
|
||||
</el-button>
|
||||
<el-button size="mini" type="warning" @click="handleOperate(selectedCard, 'UNLOAD')"
|
||||
class="btn-block mt-2">
|
||||
手动卸卷
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleOperate(selectedCard, 'ALL_RETURN')"
|
||||
class="btn-block mt-2">
|
||||
整卷回退
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleOperate(selectedCard, 'HALF_RETURN')"
|
||||
class="btn-block mt-2">
|
||||
半卷回退
|
||||
</el-button>
|
||||
<el-button size="mini" type="info" @click="handleOperate(selectedCard, 'BLOCK')"
|
||||
class="btn-block mt-2">
|
||||
卸卷并封闭
|
||||
</el-button>
|
||||
<!-- <el-button size="mini" type="info" @click="handleOperate(selectedCard, 'THROW_TAIL')" class="btn-block mt-2">
|
||||
甩尾
|
||||
</el-button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<el-row v-else>
|
||||
<div class="empty-tip">请选择钢卷卡片进行操作</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog :visible.sync="operateMatStatus" :title="getOperateTitle" width="50%">
|
||||
<el-form :model="operateMatForm" :rules="operateRules" ref="operateForm" label-width="120px">
|
||||
<el-form-item label="开卷机编号" prop="porIdx">
|
||||
<el-input v-model="operateMatForm.porIdx"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="卷取机编号" prop="trIdx">
|
||||
<el-input v-model="operateMatForm.trIdx"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划id" prop="planId">
|
||||
<el-input v-model="operateMatForm.planId" placeholder="请输入计划ID"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="钢卷号" prop="entryMatId">
|
||||
<el-input v-model="operateMatForm.entryMatId" placeholder="请输入钢卷号"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="计划号" prop="planNo">
|
||||
<el-input v-model="operateMatForm.planNo" placeholder="请输入计划号"></el-input>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="操作类型" prop="operation">
|
||||
<el-select v-model="operateMatForm.operation" disabled>
|
||||
<el-option label="钢卷上线" value="ONLINE"></el-option>
|
||||
<el-option label="手动卸卷" value="UNLOAD"></el-option>
|
||||
<el-option label="整卷回退" value="ALL_RETURN"></el-option>
|
||||
<el-option label="半卷回退" value="HALF_RETURN"></el-option>
|
||||
<el-option label="卸卷并封闭" value="BLOCK"></el-option>
|
||||
<!-- <el-option label="甩尾" value="THROW_TAIL"></el-option> -->
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 回退相关字段 -->
|
||||
<template v-if="['ALL_RETURN', 'HALF_RETURN'].includes(operateMatForm.operation)">
|
||||
<el-form-item label="回退卷号" prop="returnMatId">
|
||||
<el-input v-model="operateMatForm.returnMatId" placeholder="请输入回退卷号"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="回退重量" prop="returnWeight">
|
||||
<el-input v-model="operateMatForm.returnWeight" placeholder="请输入回退重量"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="回退备注" prop="returnRemark">
|
||||
<el-input v-model="operateMatForm.returnRemark" rows="3"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 产出长度字段 -->
|
||||
<template v-if="['PRODUCING', 'PRODUCT'].includes(operateMatForm.operation)">
|
||||
<el-form-item label="产出钢卷长度" prop="coilLength">
|
||||
<el-input v-model="operateMatForm.coilLength" type="number" placeholder="请输入产出钢卷长度"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="operateMatStatus = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitOperateForm">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import createFetch from '@/api/l2/track'
|
||||
import { getConfigKey } from '@/api/system/config'
|
||||
import KnovaStage from './components/knovaStage.vue'
|
||||
import { rects, lines } from './panels/track/rects'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
KnovaStage
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetchApi: undefined,
|
||||
rects,
|
||||
lines,
|
||||
matMapList: [],
|
||||
selectedCard: null, // 非调整模式选中的单个卡片
|
||||
adjustForm: {
|
||||
current: null,
|
||||
target: null
|
||||
}, // 调整模式选中的位置:[当前, 目标],双向绑定
|
||||
adjustMode: false, // 是否为调整模式
|
||||
deviceMap: {},
|
||||
operateMatForm: {
|
||||
porIdx: null,
|
||||
trIdx: null,
|
||||
planId: '',
|
||||
entryMatId: '',
|
||||
// planNo: '',
|
||||
operation: '',
|
||||
returnMatId: '',
|
||||
returnWeight: null,
|
||||
returnRemark: '',
|
||||
coilLength: null
|
||||
},
|
||||
operateRules: {
|
||||
planId: [{ required: true, message: '请输入计划id', trigger: 'blur' }],
|
||||
entryMatId: [{ required: true, message: '请输入钢卷号', trigger: 'blur' }],
|
||||
operation: [{ required: true, message: '请选择操作类型', trigger: 'change' }],
|
||||
returnMatId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入回退卷号',
|
||||
trigger: 'blur',
|
||||
validator: (rule, val, cb) => {
|
||||
if (['ALL_RETURN', 'HALF_RETURN'].includes(this.operateMatForm.operation) && !val) {
|
||||
cb(new Error('请输入回退卷号'))
|
||||
} else cb()
|
||||
}
|
||||
}
|
||||
],
|
||||
returnWeight: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入回退重量',
|
||||
trigger: 'blur',
|
||||
validator: (rule, val, cb) => {
|
||||
if (['ALL_RETURN', 'HALF_RETURN'].includes(this.operateMatForm.operation) && (val === null || val === '')) {
|
||||
cb(new Error('请输入回退重量'))
|
||||
} else cb()
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
operateMatStatus: false, // 操作对话框显示状态
|
||||
returnInfo: {}, // 存储回退接口返回的数据
|
||||
isLoadingReturn: false, // 回退信息加载状态
|
||||
returnError: '' // 回退信息获取失败的提示
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 操作对话框标题
|
||||
getOperateTitle() {
|
||||
const titleMap = {
|
||||
'ONLINE': '钢卷上线',
|
||||
'UNLOAD': '手动卸卷',
|
||||
'ALL_RETURN': '整卷回退',
|
||||
'HALF_RETURN': '半卷回退',
|
||||
'BLOCK': '卸卷并封闭',
|
||||
'THROW_TAIL': '甩尾'
|
||||
}
|
||||
return titleMap[this.operateMatForm.operation] || '钢卷操作'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取钢卷数据
|
||||
fetchData() {
|
||||
this.fetchApi.getTrackMatPosition().then(res => {
|
||||
this.matMapList = res.data.matMapList || []
|
||||
// this.deviceMap = res.data.matMapList || {}
|
||||
}).catch(err => {
|
||||
console.error('获取钢卷数据失败:', err)
|
||||
this.$message.error('获取数据失败,请重试')
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取回退信息
|
||||
* @param {Number} posIdx - 位置索引(接口必填query参数)
|
||||
*/
|
||||
fetchReturnData(posIdx) {
|
||||
// 1. 校验posIdx
|
||||
if (!posIdx && posIdx !== 0) {
|
||||
this.returnInfo = {};
|
||||
this.returnError = '缺少位置索引(posIdx),无法获取回退信息';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 加载状态初始化
|
||||
this.isLoadingReturn = true;
|
||||
this.returnError = '';
|
||||
|
||||
// 3. 调用回退接口(posIdx作为query参数传递)
|
||||
this.fetchApi.getBackData({ posIdx })
|
||||
.then(res => {
|
||||
this.isLoadingReturn = false;
|
||||
// 接口成功且有数据
|
||||
if (res.code === 200 && res.data) {
|
||||
this.returnInfo = res.data;
|
||||
} else {
|
||||
this.returnInfo = {};
|
||||
this.returnError = res.msg || '获取回退信息失败';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.isLoadingReturn = false;
|
||||
this.returnInfo = {};
|
||||
this.returnError = '获取回退信息出错,请重试';
|
||||
console.error('回退信息接口异常:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 选择卡片(区分调整/非调整模式)
|
||||
selectCard(item) {
|
||||
this.selectedCard = this.selectedCard === item ? null : item
|
||||
// 选中卡片时查询回退信息,取消选中时清空
|
||||
if (this.selectedCard) {
|
||||
this.fetchReturnData(this.selectedCard.posIdx);
|
||||
this.adjustForm.current = this.selectedCard.positionNameEn
|
||||
} else {
|
||||
this.returnInfo = {};
|
||||
this.returnError = '';
|
||||
this.adjustForm.current = null
|
||||
}
|
||||
},
|
||||
|
||||
// 确认调整位置
|
||||
handleConfirmAdjust() {
|
||||
const { current, target } = this.adjustForm
|
||||
|
||||
if (!current || !target) {
|
||||
this.$message.warning('请选择当前位置和目标位置')
|
||||
return
|
||||
}
|
||||
|
||||
const params = {
|
||||
currentPos: current,
|
||||
targetPos: target,
|
||||
}
|
||||
|
||||
this.$confirm(`确定将 ${current} 的钢卷调整到 ${target}?`, '确认调整', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.fetchApi.adjustPosition(params).then(() => {
|
||||
this.$message.success('调整成功')
|
||||
this.exitAdjustMode()
|
||||
this.fetchData()
|
||||
}).catch(err => {
|
||||
console.error('调整失败:', err)
|
||||
this.$message.error('调整失败,请重试')
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消调整')
|
||||
})
|
||||
},
|
||||
|
||||
// 打开操作对话框
|
||||
handleOperate(row, operation) {
|
||||
this.$refs.operateForm?.resetFields()
|
||||
this.operateMatForm = {
|
||||
porIdx: row.posIdx || null,
|
||||
trIdx: row.posIdx || null,
|
||||
planId: row.planId || '',
|
||||
entryMatId: row.matId || '',
|
||||
planNo: row.planNo || '',
|
||||
operation: operation,
|
||||
returnMatId: '',
|
||||
returnWeight: null,
|
||||
returnRemark: '',
|
||||
coilLength: null
|
||||
}
|
||||
this.operateMatStatus = true
|
||||
},
|
||||
|
||||
// 提交操作表单
|
||||
submitOperateForm() {
|
||||
this.$refs.operateForm.validate(valid => {
|
||||
if (valid) {
|
||||
this.fetchApi.operateMat(this.operateMatForm).then(() => {
|
||||
this.$message.success('操作成功')
|
||||
this.operateMatStatus = false
|
||||
this.fetchData()
|
||||
}).catch(err => {
|
||||
console.error('操作失败:', err)
|
||||
this.$message.error('操作失败,请重试')
|
||||
})
|
||||
} else {
|
||||
this.$message.warning('请完善表单信息')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
getConfigKey('line.zine.baseURL').then(res => {
|
||||
this.fetchApi = createFetch(res.msg)
|
||||
this.fetchData()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.graph-container-box {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - 86px);
|
||||
padding: 20px;
|
||||
// background-color: #c0c0c0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 1881px;
|
||||
height: 608px;
|
||||
}
|
||||
|
||||
// 图形元素基础样式
|
||||
.graph-list>div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// 文字容器样式
|
||||
.text-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 文字基础样式(可被配置项覆盖)
|
||||
.text-wrapper span {
|
||||
position: absolute;
|
||||
color: #333;
|
||||
/* 默认文字颜色 */
|
||||
font-size: 14px;
|
||||
/* 默认文字大小 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="label-render-container">
|
||||
<!-- 标签预览容器 -->
|
||||
<div class="preview-container" :id="hideActions ? undefined : 'label-preview-container'" ref="labelRef">
|
||||
<ProductionTagPreview
|
||||
<MaterialTag
|
||||
v-if="labelType === '2'"
|
||||
:content="content"
|
||||
:paperWidthMm="100"
|
||||
@@ -31,7 +31,7 @@ import html2canvas from 'html2canvas'; // 高清渲染
|
||||
import { Message } from 'element-ui';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import ProductionTagPreview from './ProductionTagPreview.vue';
|
||||
import MaterialTag from './MaterialTag.vue';
|
||||
import OuterTagPreview from './OuterTagPreview.vue';
|
||||
import SampleTagPreview from './SampleTagPreview.vue';
|
||||
import ForgeTagPreview from './ForgeTagPreview.vue';
|
||||
@@ -40,17 +40,22 @@ import SaltSprayTagPreview from './SaltSprayTagPreview.vue';
|
||||
export default {
|
||||
name: 'LabelRender',
|
||||
components: {
|
||||
ProductionTagPreview,
|
||||
MaterialTag,
|
||||
OuterTagPreview,
|
||||
SampleTagPreview,
|
||||
ForgeTagPreview,
|
||||
SaltSprayTagPreview,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
labelType: '2',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
labelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// labelType: {
|
||||
// type: String,
|
||||
// required: true,
|
||||
// },
|
||||
content: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -60,6 +65,27 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
content: {
|
||||
handler(newVal) {
|
||||
const { itemName, itemType } = newVal;
|
||||
if (itemType == 'raw_material') {
|
||||
this.labelType = '2';
|
||||
} else if (itemType == 'product' && itemName == '冷硬卷') {
|
||||
this.labelType = '3';
|
||||
} else if (itemType == 'product' && itemName == '热轧卷板') {
|
||||
this.labelType = '3';
|
||||
} else if (itemType == 'product' && itemName == '镀锌卷') {
|
||||
this.labelType = '3';
|
||||
} else if (itemType == 'product' && itemName == '冷轧卷') {
|
||||
this.labelType = '3';
|
||||
} else {
|
||||
this.labelType = '3';
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// -------- 图片下载方法(保留,可按需替换为html2canvas) --------
|
||||
async downloadLabelAsImage() {
|
||||
|
||||
Reference in New Issue
Block a user