feat(zinc): 新增PDO接口和日期筛选组件,重构生产统计页面

refactor(api): 移除冗余API路径前缀
style(websocket): 移除重复的日志打印
feat(label): 新增原料标签组件并替换生产标签
refactor(lines): 移除废弃的流程图组件和配置
feat(utils): 新增日期范围工具函数
feat(statistic): 集成日期筛选和图表分析功能
This commit is contained in:
砂糖
2026-01-23 14:01:52 +08:00
parent 8adad71acd
commit d8f68c9ec2
14 changed files with 1629 additions and 1005 deletions

View File

@@ -0,0 +1,9 @@
import zinc1Request from '@/utils/zinc1Request'
export function listPdo(queryParams) {
return zinc1Request({
url: '/pdo/list',
method: 'post',
data: queryParams
})
}

View File

@@ -2,7 +2,7 @@ import zinc1Request from '@/utils/zinc1Request'
export function listDeviceEnumAll() {
return zinc1Request({
url: '/api/deviceEnum/all',
url: '/deviceEnum/all',
method: 'get'
})
}

View File

@@ -2,7 +2,7 @@ import zinc1Request from '@/utils/zinc1Request'
export function getDeviceFieldMetaAll() {
return zinc1Request({
url: '/api/deviceFieldMeta/all',
url: '/deviceFieldMeta/all',
method: 'get'
})
}

View File

@@ -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
})

View File

@@ -23,6 +23,8 @@ class WebSocketManager {
this.disconnect(type)
}
console.log(`[WebSocket] 正在连接: ${type}`)
const url = `${this.baseUrl}?type=${type}`
console.log(`[WebSocket] 正在连接: ${type}`)

View 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/endTimeVue2 .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>

View File

@@ -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>

View File

@@ -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'
}
}
]

View 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];
}

View File

@@ -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>

View File

@@ -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>
// 引入EChartsVue2常用图表库替换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>

View File

@@ -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>

View File

@@ -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() {