feat(monitoring): 添加设备参数追溯和工艺快照功能

- 在路由配置中新增设备监控参数追溯和工艺快照页面路由
- 在设备参数监控页面添加功能导航下拉菜单,支持跳转到参数追溯和工艺快照
- 新增参数追溯页面,实现设备参数历史数据查询和SPC分析功能
- 新增工艺快照页面,支持创建、查看、对比和删除工艺快照
- 添加相关的API接口用于获取追溯列表和SPC分析数据
- 将预警检查间隔从30秒调整为60秒以优化性能
master
zangch@mesnac.com 7 days ago
parent a12efc2814
commit 81e02c8d5d

@ -0,0 +1,61 @@
import request from '@/utils/request'
// 查询工艺快照列表
export function listProcessSnapshot(query) {
return request({
url: '/base/processSnapshot/list',
method: 'get',
params: query
})
}
// 查询工艺快照详情(含参数)
export function getProcessSnapshotDetail(snapshotId) {
return request({
url: '/base/processSnapshot/detail/' + snapshotId,
method: 'get'
})
}
// 查询工艺快照基本信息
export function getProcessSnapshot(snapshotId) {
return request({
url: '/base/processSnapshot/' + snapshotId,
method: 'get'
})
}
// 新增工艺快照
export function addProcessSnapshot(data) {
return request({
url: '/base/processSnapshot',
method: 'post',
data: data
})
}
// 修改工艺快照
export function updateProcessSnapshot(data) {
return request({
url: '/base/processSnapshot',
method: 'put',
data: data
})
}
// 删除工艺快照
export function delProcessSnapshot(snapshotIds) {
return request({
url: '/base/processSnapshot/' + snapshotIds,
method: 'delete'
})
}
// 对比两个快照
export function compareSnapshots(snapshotId1, snapshotId2) {
return request({
url: '/base/processSnapshot/compare',
method: 'get',
params: { snapshotId1, snapshotId2 }
})
}

@ -85,3 +85,21 @@ export function getDeviceStartTimeList() {
method: 'get'
})
}
// 参数追溯列表(分页)
export function getTraceList(query) {
return request({
url: '/baseDeviceParamVal/val/trace/list',
method: 'get',
params: query
})
}
// 获取SPC分析数据
export function getSPCData(query) {
return request({
url: '/baseDeviceParamVal/val/spcData',
method: 'get',
params: query
})
}

@ -535,6 +535,34 @@ export const dynamicRoutes = [
},
],
},
{
path: "/device/Monitor",
component: Layout,
hidden: true,
permissions: ["baseDeviceParamVal:val:list"],
children: [
{
path: "trace",
component: () => import("@/views/baseDeviceParamVal/trace/index"),
name: "DeviceParamValTrace",
meta: {title: "参数追溯", activeMenu: "/device/Monitor/val"},
},
],
},
{
path: "/base/processSnapshot",
component: Layout,
hidden: true,
permissions: ["base:processSnapshot:list"],
children: [
{
path: "index",
component: () => import("@/views/base/processSnapshot/index"),
name: "ProcessSnapshot",
meta: {title: "工艺快照", activeMenu: "/device/Monitor/val"},
},
],
},
]
// 防止连续点击多次路由报错

@ -0,0 +1,607 @@
<template>
<div class="app-container">
<!-- 返回导航栏 -->
<div class="breadcrumb-nav">
<el-page-header @back="goBack" content="工艺快照">
<template slot="title">
<span style="cursor: pointer;" @click="goBack"></span>
</template>
</el-page-header>
</div>
<!-- 搜索表单 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" class="search-form">
<el-form-item label="设备" prop="deviceCode">
<el-select v-model="queryParams.deviceCode" placeholder="请选择设备" clearable filterable style="width: 200px;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item label="快照类型" prop="snapshotType">
<el-select v-model="queryParams.snapshotType" placeholder="请选择类型" clearable style="width: 140px;">
<el-option label="开始生产" value="START" />
<el-option label="结束生产" value="END" />
<el-option label="手动创建" value="MANUAL" />
<el-option label="参数变化" value="CHANGE" />
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="yyyy-MM-dd HH:mm:ss"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" @click="resetQuery"></el-button>
<el-button type="success" icon="el-icon-plus" @click="handleAdd"></el-button>
</el-form-item>
</el-form>
<!-- 主体内容左侧快照列表 + 右侧快照详情 -->
<div class="main-content">
<!-- 左侧快照列表 -->
<div class="snapshot-list-panel">
<div class="panel-header">
<span class="panel-title">快照列表 ({{ total }})</span>
<el-button v-if="compareMode" type="text" size="mini" @click="cancelCompare"></el-button>
</div>
<div class="snapshot-list" v-loading="loading">
<div
v-for="snapshot in snapshotList"
:key="snapshot.snapshotId"
:class="['snapshot-item', {
'active': selectedSnapshot && selectedSnapshot.snapshotId === snapshot.snapshotId,
'compare-selected': compareSnapshots.includes(snapshot.snapshotId)
}]"
@click="selectSnapshot(snapshot)"
>
<div class="snapshot-header">
<el-tag :type="getSnapshotTypeTag(snapshot.snapshotType)" size="mini">
{{ getSnapshotTypeText(snapshot.snapshotType) }}
</el-tag>
<el-checkbox
v-if="compareMode"
:value="compareSnapshots.includes(snapshot.snapshotId)"
@change="toggleCompareSnapshot(snapshot.snapshotId)"
@click.native.stop
/>
</div>
<div class="snapshot-device">{{ snapshot.deviceName || snapshot.deviceCode }}</div>
<div class="snapshot-time">{{ parseTime(snapshot.snapshotTime) }}</div>
<div class="snapshot-actions">
<el-button type="text" size="mini" @click.stop="handleCompareWith(snapshot)">对比</el-button>
<el-button type="text" size="mini" style="color: #f56c6c;" @click.stop="handleDelete(snapshot)">删除</el-button>
</div>
</div>
<div v-if="snapshotList.length === 0" class="no-data">
<i class="el-icon-camera"></i>
<span>暂无快照数据</span>
</div>
</div>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
layout="total, prev, pager, next"
:pager-count="5"
@pagination="getList"
style="padding: 8px;"
/>
</div>
<!-- 右侧快照详情 -->
<div class="snapshot-detail-panel">
<template v-if="selectedSnapshot">
<div class="detail-header">
<div class="detail-title">
<span>快照详情</span>
<el-tag :type="getSnapshotTypeTag(selectedSnapshot.snapshotType)" size="small">
{{ getSnapshotTypeText(selectedSnapshot.snapshotType) }}
</el-tag>
</div>
<div class="detail-meta">
<span><b>设备:</b> {{ selectedSnapshot.deviceName || selectedSnapshot.deviceCode }}</span>
<span><b>时间:</b> {{ parseTime(selectedSnapshot.snapshotTime) }}</span>
<span v-if="selectedSnapshot.orderCode"><b>工单:</b> {{ selectedSnapshot.orderCode }}</span>
</div>
</div>
<el-divider content-position="left">参数列表 ({{ snapshotParams.length }})</el-divider>
<div class="param-list" v-loading="detailLoading">
<el-table :data="snapshotParams" border stripe size="small" max-height="400">
<el-table-column label="参数编号" prop="paramCode" width="100" />
<el-table-column label="参数名称" prop="paramName" min-width="180" show-overflow-tooltip />
<el-table-column label="参数值" prop="paramValue" width="120" align="center">
<template slot-scope="scope">
<span class="param-value">{{ scope.row.paramValue }}</span>
</template>
</el-table-column>
<el-table-column label="采集时间" prop="collectTime" width="160">
<template slot-scope="scope">
{{ parseTime(scope.row.collectTime, '{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
</el-table>
</div>
</template>
<template v-else>
<div class="no-selection">
<i class="el-icon-document"></i>
<span>请从左侧选择一个快照查看详情</span>
</div>
</template>
</div>
</div>
<!-- 创建快照对话框 -->
<el-dialog title="创建快照" :visible.sync="addDialogVisible" width="500px" append-to-body>
<el-form ref="addForm" :model="addForm" :rules="addRules" label-width="100px">
<el-form-item label="设备" prop="deviceCode">
<el-select v-model="addForm.deviceCode" placeholder="请选择设备" filterable style="width: 100%;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item label="快照类型" prop="snapshotType">
<el-select v-model="addForm.snapshotType" placeholder="请选择类型" style="width: 100%;">
<el-option label="开始生产" value="START" />
<el-option label="结束生产" value="END" />
<el-option label="手动创建" value="MANUAL" />
<el-option label="参数变化" value="CHANGE" />
</el-select>
</el-form-item>
<el-form-item label="关联工单" prop="orderCode">
<el-input v-model="addForm.orderCode" placeholder="请输入关联工单号(可选)" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="addForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAdd" :loading="submitLoading">确定</el-button>
</div>
</el-dialog>
<!-- 快照对比对话框 -->
<el-dialog title="快照对比" :visible.sync="compareDialogVisible" width="900px" append-to-body>
<div v-loading="compareLoading" class="compare-content">
<div v-if="compareResult && compareResult.length > 0">
<div class="compare-header">
<span><b>快照1:</b> {{ compareSnapshot1 ? parseTime(compareSnapshot1.snapshotTime) : '-' }}</span>
<span style="margin: 0 20px;"></span>
<span><b>快照2:</b> {{ compareSnapshot2 ? parseTime(compareSnapshot2.snapshotTime) : '-' }}</span>
</div>
<el-table :data="compareResult" border stripe size="small" max-height="400">
<el-table-column label="参数编号" prop="paramCode" width="100" />
<el-table-column label="参数名称" prop="paramName" min-width="180" show-overflow-tooltip />
<el-table-column label="快照1值" prop="beforeValue" width="120" align="center" />
<el-table-column label="快照2值" prop="afterValue" width="120" align="center" />
<el-table-column label="变化" prop="isChanged" width="80" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isChanged === 'Y'" type="danger" size="mini"></el-tag>
<el-tag v-else type="info" size="mini">未变化</el-tag>
</template>
</el-table-column>
</el-table>
<div class="compare-summary">
<span>变化参数: <b style="color: #f56c6c;">{{ compareResult.filter(r => r.isChanged === 'Y').length }}</b></span>
<span>未变化参数: <b>{{ compareResult.filter(r => r.isChanged !== 'Y').length }}</b></span>
</div>
</div>
<div v-else class="no-data">
<i class="el-icon-warning-outline"></i>
<span>暂无对比数据</span>
</div>
</div>
<div slot="footer">
<el-button @click="compareDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcessSnapshot, getProcessSnapshotDetail, addProcessSnapshot, delProcessSnapshot, compareSnapshots } from "@/api/base/processSnapshot";
import { getDeviceLedgerList } from "@/api/base/deviceLedger";
export default {
name: "ProcessSnapshot",
data() {
return {
loading: false,
detailLoading: false,
submitLoading: false,
compareLoading: false,
total: 0,
snapshotList: [],
deviceList: [],
selectedSnapshot: null,
snapshotParams: [],
dateRange: [],
//
addDialogVisible: false,
addForm: {
deviceCode: '',
snapshotType: 'MANUAL',
orderCode: '',
remark: ''
},
addRules: {
deviceCode: [{ required: true, message: '请选择设备', trigger: 'change' }],
snapshotType: [{ required: true, message: '请选择快照类型', trigger: 'change' }]
},
//
compareMode: false,
compareSnapshots: [],
compareDialogVisible: false,
compareSnapshot1: null,
compareSnapshot2: null,
compareResult: [],
queryParams: {
pageNum: 1,
pageSize: 20,
deviceCode: '',
snapshotType: '',
params: {}
}
};
},
created() {
this.loadDeviceList();
//
if (this.$route.query.deviceCode) {
this.queryParams.deviceCode = this.$route.query.deviceCode;
}
this.getList();
},
methods: {
goBack() {
this.$router.push('/device/Monitor/val');
},
loadDeviceList() {
getDeviceLedgerList().then(response => {
this.deviceList = response.data || response.rows || [];
});
},
getList() {
this.loading = true;
const params = { ...this.queryParams };
if (this.dateRange && this.dateRange.length === 2) {
params.params = {
beginTime: this.dateRange[0],
endTime: this.dateRange[1]
};
}
listProcessSnapshot(params).then(response => {
this.snapshotList = response.rows || [];
this.total = response.total || 0;
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.dateRange = [];
this.queryParams = {
pageNum: 1,
pageSize: 20,
deviceCode: '',
snapshotType: '',
params: {}
};
this.selectedSnapshot = null;
this.snapshotParams = [];
this.getList();
},
selectSnapshot(snapshot) {
this.selectedSnapshot = snapshot;
this.loadSnapshotDetail(snapshot.snapshotId);
},
loadSnapshotDetail(snapshotId) {
this.detailLoading = true;
getProcessSnapshotDetail(snapshotId).then(response => {
const data = response.data;
if (data && data.paramList) {
this.snapshotParams = data.paramList;
} else {
this.snapshotParams = [];
}
this.detailLoading = false;
}).catch(() => {
this.detailLoading = false;
this.snapshotParams = [];
});
},
handleAdd() {
this.addDialogVisible = true;
this.addForm = {
deviceCode: this.queryParams.deviceCode || '',
snapshotType: 'MANUAL',
orderCode: '',
remark: ''
};
},
submitAdd() {
this.$refs.addForm.validate(valid => {
if (!valid) return;
this.submitLoading = true;
addProcessSnapshot(this.addForm).then(() => {
this.$message.success('快照创建成功');
this.addDialogVisible = false;
this.submitLoading = false;
this.getList();
}).catch(() => {
this.submitLoading = false;
});
});
},
handleDelete(snapshot) {
this.$confirm('确认删除该快照吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delProcessSnapshot(snapshot.snapshotId).then(() => {
this.$message.success('删除成功');
if (this.selectedSnapshot && this.selectedSnapshot.snapshotId === snapshot.snapshotId) {
this.selectedSnapshot = null;
this.snapshotParams = [];
}
this.getList();
});
}).catch(() => {});
},
handleCompareWith(snapshot) {
if (!this.selectedSnapshot) {
this.$message.warning('请先选择一个快照作为对比基准');
return;
}
if (this.selectedSnapshot.snapshotId === snapshot.snapshotId) {
this.$message.warning('不能与自身对比');
return;
}
if (this.selectedSnapshot.deviceCode !== snapshot.deviceCode) {
this.$message.warning('只能对比同一设备的快照');
return;
}
this.compareSnapshot1 = this.selectedSnapshot;
this.compareSnapshot2 = snapshot;
this.compareDialogVisible = true;
this.loadCompareResult();
},
loadCompareResult() {
this.compareLoading = true;
compareSnapshots(this.compareSnapshot1.snapshotId, this.compareSnapshot2.snapshotId).then(response => {
this.compareResult = response.data || [];
this.compareLoading = false;
}).catch(() => {
this.compareLoading = false;
this.compareResult = [];
});
},
toggleCompareSnapshot(snapshotId) {
const index = this.compareSnapshots.indexOf(snapshotId);
if (index > -1) {
this.compareSnapshots.splice(index, 1);
} else {
if (this.compareSnapshots.length >= 2) {
this.compareSnapshots.shift();
}
this.compareSnapshots.push(snapshotId);
}
},
cancelCompare() {
this.compareMode = false;
this.compareSnapshots = [];
},
getSnapshotTypeText(type) {
const map = { 'START': '开始生产', 'END': '结束生产', 'MANUAL': '手动创建', 'CHANGE': '参数变化' };
return map[type] || type;
},
getSnapshotTypeTag(type) {
const map = { 'START': 'success', 'END': 'info', 'MANUAL': 'warning', 'CHANGE': 'danger' };
return map[type] || '';
}
}
};
</script>
<style scoped>
.breadcrumb-nav {
margin-bottom: 16px;
padding: 12px 16px;
background: #fff;
border-radius: 4px;
}
.search-form {
padding: 16px;
background: #fff;
border-radius: 4px;
margin-bottom: 16px;
}
.main-content {
display: flex;
gap: 16px;
height: calc(100vh - 280px);
min-height: 400px;
}
.snapshot-list-panel {
width: 320px;
flex-shrink: 0;
background: #fff;
border-radius: 4px;
border: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.snapshot-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.snapshot-item {
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.snapshot-item:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
.snapshot-item.active {
border-color: #1890ff;
background: #e6f7ff;
}
.snapshot-item.compare-selected {
border-color: #722ed1;
background: #f9f0ff;
}
.snapshot-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.snapshot-device {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.snapshot-time {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.snapshot-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.snapshot-detail-panel {
flex: 1;
background: #fff;
border-radius: 4px;
border: 1px solid #e8e8e8;
padding: 16px;
overflow-y: auto;
}
.detail-header {
margin-bottom: 16px;
}
.detail-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.detail-meta {
display: flex;
gap: 24px;
font-size: 13px;
color: #666;
}
.detail-meta b {
color: #999;
margin-right: 4px;
}
.param-value {
font-weight: 600;
color: #1890ff;
}
.no-selection, .no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
color: #999;
}
.no-selection i, .no-data i {
font-size: 48px;
margin-bottom: 12px;
}
.compare-content {
min-height: 200px;
}
.compare-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
font-size: 14px;
}
.compare-summary {
display: flex;
justify-content: center;
gap: 32px;
margin-top: 16px;
font-size: 14px;
}
</style>

@ -0,0 +1,455 @@
<template>
<div class="app-container">
<!-- 返回导航栏 -->
<div class="breadcrumb-nav">
<el-page-header @back="goBack" content="参数追溯">
<template slot="title">
<span style="cursor: pointer;" @click="goBack"></span>
</template>
</el-page-header>
</div>
<!-- 搜索表单 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" class="search-form">
<el-form-item label="设备" prop="deviceCode">
<el-select v-model="queryParams.deviceCode" placeholder="请选择设备" clearable filterable style="width: 200px;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
>
<span style="float: left">{{ item.deviceName || item.deviceCode }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.deviceCode }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="参数" prop="paramCode">
<el-select v-model="queryParams.paramCode" placeholder="请选择参数(可选)" clearable filterable style="width: 200px;">
<el-option
v-for="item in paramList"
:key="item.paramCode"
:label="item.paramName || item.paramCode"
:value="item.paramCode"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="yyyy-MM-dd HH:mm:ss"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" @click="resetQuery"></el-button>
<el-button type="warning" icon="el-icon-download" @click="handleExport" :loading="exportLoading">导出</el-button>
<el-button type="success" icon="el-icon-s-data" @click="handleSPC" :disabled="!queryParams.paramCode">SPC分析</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="traceList" border stripe>
<el-table-column label="设备编号" prop="deviceCode" width="120" />
<el-table-column label="参数编号" prop="paramCode" width="100" />
<el-table-column label="参数名称" prop="paramName" min-width="180" show-overflow-tooltip />
<el-table-column label="参数值" prop="paramValue" width="120" align="center">
<template slot-scope="scope">
<span class="param-value">{{ scope.row.paramValue }}</span>
</template>
</el-table-column>
<el-table-column label="采集时间" prop="collectTime" width="180">
<template slot-scope="scope">
{{ parseTime(scope.row.collectTime) }}
</template>
</el-table-column>
<el-table-column label="记录时间" prop="recordTime" width="180">
<template slot-scope="scope">
{{ parseTime(scope.row.recordTime) }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- SPC分析对话框 -->
<el-dialog title="SPC分析" :visible.sync="spcDialogVisible" width="800px" append-to-body>
<div v-loading="spcLoading" class="spc-content">
<div v-if="spcData" class="spc-result">
<div class="spc-header">
<div class="spc-info">
<span><b>设备:</b> {{ spcData.deviceCode }}</span>
<span><b>参数:</b> {{ spcData.paramName }}</span>
<span><b>样本数:</b> {{ spcData.sampleSize }}</span>
</div>
</div>
<el-divider />
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover" class="spc-card">
<div slot="header">统计指标</div>
<div class="spc-stat">
<div class="stat-item"><span class="label">均值 (X̄)</span><span class="value">{{ spcData.mean }}</span></div>
<div class="stat-item"><span class="label">标准差 (σ)</span><span class="value">{{ spcData.stdDev }}</span></div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="spc-card">
<div slot="header">控制限</div>
<div class="spc-stat">
<div class="stat-item"><span class="label">UCL (上控制限)</span><span class="value ucl">{{ spcData.ucl }}</span></div>
<div class="stat-item"><span class="label">CL (中心线)</span><span class="value">{{ spcData.cl }}</span></div>
<div class="stat-item"><span class="label">LCL (下控制限)</span><span class="value lcl">{{ spcData.lcl }}</span></div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="spc-card">
<div slot="header">过程能力</div>
<div class="spc-stat">
<div class="stat-item"><span class="label">CPK</span><span :class="['value', 'cpk', getCPKClass(spcData.cpk)]">{{ spcData.cpk }}</span></div>
<div class="stat-item"><span class="label">USL (上规格限)</span><span class="value">{{ spcData.usl }}</span></div>
<div class="stat-item"><span class="label">LSL (下规格限)</span><span class="value">{{ spcData.lsl }}</span></div>
</div>
</el-card>
</el-col>
</el-row>
<el-divider />
<div class="cpk-legend">
<span class="legend-item cpk-good">CPK 1.33: 优秀</span>
<span class="legend-item cpk-ok">1.0 CPK < 1.33: 合格</span>
<span class="legend-item cpk-bad">CPK < 1.0: 需改进</span>
</div>
</div>
<div v-else class="no-data">
<i class="el-icon-warning-outline"></i>
<span>暂无SPC数据请确保查询条件正确</span>
</div>
</div>
<div slot="footer">
<el-button @click="spcDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getTraceList, getSPCData, getLatestVal } from "@/api/baseDeviceParamVal/val";
import { getDeviceLedgerList } from "@/api/base/deviceLedger";
export default {
name: "DeviceParamValTrace",
data() {
return {
loading: false,
exportLoading: false,
spcLoading: false,
spcDialogVisible: false,
total: 0,
traceList: [],
deviceList: [],
paramList: [],
dateRange: [],
spcData: null,
queryParams: {
pageNum: 1,
pageSize: 20,
deviceCode: '',
paramCode: '',
startTime: '',
endTime: ''
}
};
},
created() {
this.loadDeviceList();
//
if (this.$route.query.deviceCode) {
this.queryParams.deviceCode = this.$route.query.deviceCode;
this.loadParamList(this.queryParams.deviceCode);
}
// 1
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
this.dateRange = [this.formatDate(oneHourAgo), this.formatDate(now)];
},
watch: {
'queryParams.deviceCode'(val) {
if (val) {
this.loadParamList(val);
} else {
this.paramList = [];
this.queryParams.paramCode = '';
}
}
},
methods: {
formatDate(date) {
const y = date.getFullYear();
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const d = date.getDate().toString().padStart(2, '0');
const h = date.getHours().toString().padStart(2, '0');
const i = date.getMinutes().toString().padStart(2, '0');
const s = date.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${h}:${i}:${s}`;
},
goBack() {
this.$router.push('/device/Monitor/val');
},
loadDeviceList() {
getDeviceLedgerList().then(response => {
this.deviceList = response.data || response.rows || [];
});
},
loadParamList(deviceCode) {
getLatestVal({ deviceCode }).then(response => {
const rows = response.data || response.rows || [];
//
const paramMap = {};
rows.forEach(item => {
if (!paramMap[item.paramCode]) {
paramMap[item.paramCode] = { paramCode: item.paramCode, paramName: item.paramName };
}
});
this.paramList = Object.values(paramMap);
});
},
getList() {
if (!this.queryParams.deviceCode) {
this.$message.warning('请选择设备');
return;
}
if (!this.dateRange || this.dateRange.length !== 2) {
this.$message.warning('请选择时间范围');
return;
}
this.queryParams.startTime = this.dateRange[0];
this.queryParams.endTime = this.dateRange[1];
this.loading = true;
getTraceList(this.queryParams).then(response => {
this.traceList = response.rows || [];
this.total = response.total || 0;
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.dateRange = [];
this.queryParams = {
pageNum: 1,
pageSize: 20,
deviceCode: '',
paramCode: '',
startTime: '',
endTime: ''
};
this.traceList = [];
this.total = 0;
},
handleExport() {
if (!this.queryParams.deviceCode || !this.dateRange || this.dateRange.length !== 2) {
this.$message.warning('请先设置查询条件');
return;
}
this.exportLoading = true;
const params = {
deviceCode: this.queryParams.deviceCode,
paramCode: this.queryParams.paramCode,
startTime: this.dateRange[0],
endTime: this.dateRange[1]
};
this.download('/baseDeviceParamVal/val/trace/export', params, `参数追溯_${new Date().getTime()}.xlsx`);
setTimeout(() => { this.exportLoading = false; }, 1000);
},
handleSPC() {
if (!this.queryParams.deviceCode || !this.queryParams.paramCode) {
this.$message.warning('请选择设备和参数');
return;
}
if (!this.dateRange || this.dateRange.length !== 2) {
this.$message.warning('请选择时间范围');
return;
}
this.spcDialogVisible = true;
this.spcLoading = true;
this.spcData = null;
getSPCData({
deviceCode: this.queryParams.deviceCode,
paramCode: this.queryParams.paramCode,
startTime: this.dateRange[0],
endTime: this.dateRange[1]
}).then(response => {
this.spcData = response.data;
this.spcLoading = false;
}).catch(() => {
this.spcLoading = false;
});
},
getCPKClass(cpk) {
if (cpk >= 1.33) return 'cpk-good';
if (cpk >= 1.0) return 'cpk-ok';
return 'cpk-bad';
}
}
};
</script>
<style scoped>
.breadcrumb-nav {
margin-bottom: 16px;
padding: 12px 16px;
background: #fff;
border-radius: 4px;
}
.search-form {
padding: 16px;
background: #fff;
border-radius: 4px;
margin-bottom: 16px;
}
.param-value {
font-weight: 600;
color: #1890ff;
}
.spc-content {
min-height: 200px;
}
.spc-header {
margin-bottom: 16px;
}
.spc-info {
display: flex;
gap: 24px;
font-size: 14px;
}
.spc-info b {
color: #666;
margin-right: 4px;
}
.spc-card {
height: 180px;
}
.spc-card .el-card__header {
padding: 10px 16px;
font-weight: 600;
background: #fafafa;
}
.spc-stat {
padding: 8px 0;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.stat-item:last-child {
border-bottom: none;
}
.stat-item .label {
color: #666;
font-size: 13px;
}
.stat-item .value {
font-weight: 600;
font-size: 16px;
color: #333;
}
.stat-item .value.ucl {
color: #f56c6c;
}
.stat-item .value.lcl {
color: #67c23a;
}
.stat-item .value.cpk-good {
color: #67c23a;
}
.stat-item .value.cpk-ok {
color: #e6a23c;
}
.stat-item .value.cpk-bad {
color: #f56c6c;
}
.cpk-legend {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 16px;
}
.legend-item {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
}
.legend-item.cpk-good {
background: #f0f9eb;
color: #67c23a;
}
.legend-item.cpk-ok {
background: #fdf6ec;
color: #e6a23c;
}
.legend-item.cpk-bad {
background: #fef0f0;
color: #f56c6c;
}
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.no-data i {
font-size: 48px;
margin-bottom: 12px;
}
</style>

@ -54,6 +54,20 @@
@change="onAutoRefreshChange"
/>
<el-button type="primary" size="mini" icon="el-icon-refresh" @click="refreshData"></el-button>
<!-- 功能导航入口 -->
<el-dropdown @command="handleMenuCommand" trigger="click">
<el-button type="success" size="mini">
功能导航 <i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="trace">
<i class="el-icon-search"></i> 参数追溯
</el-dropdown-item>
<el-dropdown-item command="snapshot">
<i class="el-icon-camera"></i> 工艺快照
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
@ -211,7 +225,7 @@ export default {
clockTimer: null,
//
autoRefresh: true,
refreshIntervalMs: 10000,
refreshIntervalMs: 10000, // 10000 = 10
refreshTimer: null,
//
deviceList: [],
@ -229,7 +243,7 @@ export default {
alertLoading: false,
showAlertPanel: false,
alertTimer: null,
alertCheckIntervalMs: 30000, // 30
alertCheckIntervalMs: 60000, // 60
lastAlertIds: [], // ID
};
},
@ -398,8 +412,8 @@ export default {
/** 加载预警列表 */
loadAlerts() {
this.alertLoading = true;
listProcessAlert({
pageNum: 1,
listProcessAlert({
pageNum: 1,
pageSize: 20,
alertStatus: '0' //
}).then(response => {
@ -497,6 +511,27 @@ export default {
this.loadAlerts();
});
}).catch(() => {});
},
/** 功能导航菜单 */
handleMenuCommand(command) {
if (command === 'trace') {
//
this.$router.push({
path: '/device/Monitor/trace',
query: this.selectedDevice ? {
deviceCode: this.selectedDevice.deviceCode,
deviceName: this.selectedDevice.deviceName
} : {}
});
} else if (command === 'snapshot') {
//
this.$router.push({
path: '/base/processSnapshot/index',
query: this.selectedDevice ? {
deviceCode: this.selectedDevice.deviceCode
} : {}
});
}
}
}
};

Loading…
Cancel
Save