工艺规程增强

This commit is contained in:
2026-05-12 17:15:29 +08:00
parent b44d9d9daf
commit 38138a828c
27 changed files with 1903 additions and 259 deletions

View File

@@ -1,75 +1,127 @@
<template>
<div class="spec-version-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<span class="page-title">规程版本管理</span>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left:auto"
@click="openSpecDialog()">新建规程</el-button>
</div>
<div>
<el-form>
<el-form-item label="规程类型">
<dict-select @change="loadSpecs" renderType="radio" v-model="queryParams.specType"
dict-type="wms_process_spec_type" :kisv="false" :editable="true"></dict-select>
</el-form-item>
<el-form-item label="产线">
<dict-select @change="loadSpecs" renderType="radio" v-model="queryParams.lineId"
dict-type="wms_process_spec_line" :kisv="false" :editable="true"></dict-select>
</el-form-item>
</el-form>
</div>
<!-- 规程列表 -->
<div class="section-wrapper">
<div class="section-title">规程列表</div>
<el-table :data="specList" size="small" highlight-current-row @row-click="onSpecRowClick"
:row-class-name="tableRowClassName">
<el-table-column label="规程编码" prop="specCode" width="150" />
<el-table-column label="规程名称" prop="specName" />
<el-table-column label="创建时间" prop="createTime" width="180" />
<el-table-column label="操作" align="right" width="180">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openSpecDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeSpec(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="loadSpecs" />
</div>
<!-- 版本列表 -->
<div class="section-wrapper" v-if="currentSpec" v-loading="versionLoading">
<div class="section-title">
版本列表 - {{ currentSpec.specName }}
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</el-button>
<div class="spec-page" v-loading="pageLoading">
<!-- 主体左规程列表 + 右版本面板 -->
<div class="master-detail">
<!-- 规程列表 -->
<div class="master-panel">
<!-- 筛选区 -->
<div class="filter-area">
<div class="filter-row">
<span class="filter-label">规程类型</span>
<dict-select
class="filter-select"
renderType="radio"
v-model="queryParams.specType"
dict-type="wms_process_spec_type"
:kisv="false"
:editable="true"
@change="loadSpecs"
/>
</div>
<div class="filter-row">
<span class="filter-label">产线</span>
<dict-select
class="filter-select"
renderType="radio"
v-model="queryParams.lineId"
dict-type="wms_process_spec_line"
:kisv="false"
:editable="true"
@change="loadSpecs"
/>
</div>
</div>
<!-- 列表头 -->
<div class="panel-hd">
<span class="panel-title">规程列表</span>
<span class="total-badge">{{ total }} </span>
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-left:auto" @click="openSpecDialog()">新建</el-button>
</div>
<el-table
:data="specList"
size="small"
highlight-current-row
@row-click="onSpecRowClick"
:row-class-name="tableRowClassName"
style="width:100%"
>
<el-table-column label="编码" prop="specCode" width="110" show-overflow-tooltip />
<el-table-column label="名称" prop="specName" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ (row.createTime || '').substring(0, 10) }}</template>
</el-table-column>
<el-table-column label="" align="right" width="80">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openSpecDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeSpec(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="loadSpecs" />
</div>
<!-- 版本面板 -->
<div class="detail-panel">
<template v-if="currentSpec">
<div class="panel-hd" v-loading="versionLoading">
<div class="detail-title-row">
<span class="panel-title">{{ currentSpec.specName }}</span>
<span class="spec-code-tag">{{ currentSpec.specCode }}</span>
</div>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</el-button>
</div>
<div v-if="!versionLoading && !versionList.length" class="empty-versions">
<i class="el-icon-document" style="font-size:32px;color:#dcdfe6;margin-bottom:8px;display:block" />
<span>暂无版本点击新建版本开始</span>
</div>
<div class="version-list" v-loading="versionLoading">
<div
v-for="v in versionList"
:key="v.versionId"
class="version-card"
@click="goPlanSpec(v)"
>
<div class="vc-left">
<div class="vc-top">
<span class="vc-code">{{ v.versionCode }}</span>
<el-tag :type="statusType(v.status)" size="mini" effect="plain" class="vc-status">
{{ statusLabel(v.status) }}
</el-tag>
<el-tag v-if="v.isActive === 1" type="success" size="mini" effect="dark" class="vc-active">当前生效</el-tag>
</div>
<div class="vc-meta">
创建于 {{ (v.createTime || '').substring(0, 16) || '—' }}
<span v-if="v.updateTime && v.updateTime !== v.createTime" style="margin-left:8px">
· 更新于 {{ (v.updateTime || '').substring(0, 16) }}
</span>
</div>
</div>
<div class="vc-right" @click.stop>
<el-switch
:value="v.isActive === 1"
active-color="#5F7BA0"
@change="handleActiveChange(v, $event)"
/>
<el-button type="text" size="mini" @click="openVersionDialog(v)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="removeVersion(v)">删除</el-button>
<el-button type="text" size="mini" class="btn-view" @click="goPlanSpec(v)">方案 </el-button>
</div>
</div>
</div>
</template>
<div v-else class="detail-empty">
<i class="el-icon-d-arrow-left" style="font-size:24px;color:#c0c4cc;margin-bottom:12px;display:block" />
<span>请在左侧选择一个规程查看其版本</span>
</div>
</div>
<el-table :data="versionList" size="small" highlight-current-row @row-click="onVersionRowClick">
<el-table-column label="版本号" prop="versionCode" />
<el-table-column label="状态" prop="status" />
<el-table-column label="创建时间" prop="createTime" />
<el-table-column label="生效" align="center">
<template slot-scope="{ row }">
<el-switch :value="row.isActive === 1" active-color="#5F7BA0" @click.native.stop
@change="handleActiveChange(row, $event)" />
</template>
</el-table-column>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openVersionDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeVersion(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!versionList.length && !versionLoading" description="暂无版本,请新建" style="padding:40px 0" />
</div>
<div v-else class="empty-hint">请选择一个规程查看其版本</div>
<!-- 新建/编辑规程 -->
<el-dialog :title="specTitle" :visible.sync="specOpen" width="500px" append-to-body @close="specForm = {}">
<el-dialog :title="specTitle" :visible.sync="specOpen" width="460px" append-to-body @close="specForm = {}">
<el-form ref="specFormRef" :model="specForm" :rules="specRules" label-width="88px" size="small">
<el-form-item label="规程编码" prop="specCode">
<el-input v-model="specForm.specCode" placeholder="请输入规程编码" maxlength="64" />
@@ -78,7 +130,7 @@
<el-input v-model="specForm.specName" placeholder="请输入规程名称" maxlength="200" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="specForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
<el-input v-model="specForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
@@ -88,21 +140,21 @@
</el-dialog>
<!-- 新建/编辑版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="500px" append-to-body @close="versionForm = {}">
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="460px" append-to-body @close="versionForm = {}">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="88px" size="small">
<el-form-item label="版本号" prop="versionCode">
<el-input v-model="versionForm.versionCode" placeholder="如 V1.0" maxlength="64" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="versionForm.status" style="width:100%">
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
<el-option v-for="s in STATUS_OPTIONS" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="保存后生效">
<el-form-item label="设为生效">
<el-switch v-model="versionForm.isActive" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="versionForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
<el-input v-model="versionForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
@@ -114,7 +166,7 @@
</template>
<script>
import { listProcessSpec, getProcessSpec, addProcessSpec, updateProcessSpec, delProcessSpec } from '@/api/wms/processSpec'
import { listProcessSpec, addProcessSpec, updateProcessSpec, delProcessSpec } from '@/api/wms/processSpec'
import {
listProcessSpecVersion,
addProcessSpecVersion,
@@ -123,28 +175,26 @@ import {
activateProcessSpecVersion
} from '@/api/wms/processSpecVersion'
const STATUS_OPTIONS = [
{ value: 'DRAFT', label: '草稿' },
{ value: 'PUBLISHED', label: '已发布' },
{ value: 'OBSOLETE', label: '已作废' }
]
export default {
name: 'SpecVersionManage',
data() {
return {
STATUS_OPTIONS,
pageLoading: false,
specList: [],
currentSpec: null,
currentSpecId: null,
versionList: [],
versionLoading: false,
statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'],
// 分页相关
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
specType: '',
lineId: ''
},
queryParams: { pageNum: 1, pageSize: 10, specType: '', lineId: '' },
// 规程相关
specOpen: false,
specTitle: '',
specSubmitLoading: false,
@@ -154,7 +204,6 @@ export default {
specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }]
},
// 版本相关
versionOpen: false,
versionTitle: '',
versionSubmitLoading: false,
@@ -169,12 +218,17 @@ export default {
this.loadSpecs()
},
methods: {
// 表格行样式
tableRowClassName({ row }) {
return row.specId === this.currentSpecId ? 'current-row' : ''
},
statusType(status) {
return { DRAFT: '', PUBLISHED: 'success', OBSOLETE: 'info' }[status] || ''
},
statusLabel(status) {
const hit = STATUS_OPTIONS.find(s => s.value === status)
return hit ? hit.label : (status || '—')
},
// 加载规程列表
loadSpecs() {
this.pageLoading = true
listProcessSpec(this.queryParams).then(res => {
@@ -182,7 +236,7 @@ export default {
this.specList = res.rows || []
if (this.specList.length > 0 && !this.currentSpec) {
this.selectSpec(this.specList[0])
} else {
} else if (!this.specList.length) {
this.currentSpec = null
this.currentSpecId = null
this.versionList = []
@@ -190,19 +244,13 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.pageLoading = false })
},
// 选择规程
selectSpec(spec) {
this.currentSpec = spec
this.currentSpecId = spec.specId
this.loadVersions()
},
onSpecRowClick(row) { this.selectSpec(row) },
// 点击规程行
onSpecRowClick(row) {
this.selectSpec(row)
},
// 加载版本列表
loadVersions() {
if (!this.currentSpecId) return
this.versionLoading = true
@@ -211,46 +259,29 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.versionLoading = false })
},
// 点击版本行
onVersionRowClick(row) {
this.goPlanSpec(row)
},
// 跳转到方案详情
goPlanSpec(row) {
const basePath = this.$route.path.replace(/\/[^/]*$/, '')
console.log(basePath)
this.$router.push({
path: `/process/processSpec/planSpec`,
query: { specId: this.currentSpecId, versionId: String(row.versionId), versionCode: row.versionCode }
})
},
// 生效切换
handleActiveChange(row, val) {
if (!val) {
this.$message.info('请激活其他版本来替换当前生效版本')
return
}
if (!val) { this.$message.info('请激活其他版本来替换当前生效版本'); return }
this.$modal.confirm('确认将版本"' + row.versionCode + '"设为当前生效版本?').then(() => {
return activateProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('已生效')
this.loadVersions()
}).catch(() => { })
}).catch(() => {})
},
// 规程对话框
openSpecDialog(row) {
this.specForm = row
? { ...row }
: { specCode: undefined, specName: undefined, remark: undefined }
this.specForm = row ? { ...row } : { specCode: undefined, specName: undefined, remark: undefined }
this.specTitle = row ? '编辑规程' : '新建规程'
this.specOpen = true
this.$nextTick(() => this.$refs.specFormRef && this.$refs.specFormRef.clearValidate())
},
// 提交规程
submitSpec() {
this.$refs.specFormRef.validate(ok => {
if (!ok) return
@@ -263,23 +294,18 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.specSubmitLoading = false })
})
},
// 删除规程
removeSpec(row) {
this.$modal.confirm('确认删除规程"' + row.specName + '"').then(() => {
return delProcessSpec(row.specId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.currentSpecId === row.specId) {
this.currentSpec = null
this.currentSpecId = null
this.versionList = []
this.currentSpec = null; this.currentSpecId = null; this.versionList = []
}
this.loadSpecs()
}).catch(() => { })
}).catch(() => {})
},
// 版本对话框
openVersionDialog(row) {
this.versionForm = row
? { ...row }
@@ -288,8 +314,6 @@ export default {
this.versionOpen = true
this.$nextTick(() => this.$refs.versionFormRef && this.$refs.versionFormRef.clearValidate())
},
// 提交版本
submitVersion() {
this.$refs.versionFormRef.validate(ok => {
if (!ok) return
@@ -304,116 +328,173 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.versionSubmitLoading = false })
})
},
// 删除版本
removeVersion(row) {
this.$modal.confirm('确认删除版本"' + row.versionCode + '"').then(() => {
return delProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadVersions()
}).catch(() => { })
}).catch(() => {})
}
}
}
</script>
<style scoped>
.spec-version-page {
padding: 16px 20px;
.spec-page {
padding: 12px 16px;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
/* ── 主体布局 ── */
.master-detail {
display: flex;
gap: 12px;
flex: 1;
min-height: 0;
align-items: flex-start;
}
/* ── 左:规程列表 ── */
.master-panel {
width: 380px;
flex-shrink: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
/* 筛选区 */
.filter-area {
padding: 10px 12px 8px;
background: #fafafa;
border-bottom: 1px solid #f0f2f5;
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
gap: 8px;
}
.filter-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
width: 52px;
flex-shrink: 0;
}
.filter-select { flex: 1; }
.page-title {
font-size: 15px;
.panel-hd {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f2f5;
background: #fff;
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.section-wrapper {
background: #fff;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
border: 1px solid #ebeef5;
.total-badge {
font-size: 12px;
color: #909399;
margin-left: 6px;
}
.section-title {
/* ── 右:版本面板 ── */
.detail-panel {
flex: 1;
min-width: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.detail-title-row {
display: flex;
align-items: center;
gap: 8px;
}
.spec-code-tag {
font-size: 11px;
color: #909399;
background: #f0f2f5;
padding: 1px 8px;
border-radius: 10px;
}
.detail-empty, .empty-versions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #909399;
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
}
/* ── 版本卡片 ── */
.version-list {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.version-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
background: #fff;
}
.empty-hint {
text-align: center;
padding: 60px 0;
color: #909399;
.version-card:hover {
border-color: #5F7BA0;
box-shadow: 0 2px 8px rgba(95,123,160,.12);
}
.vc-left { display: flex; flex-direction: column; gap: 4px; }
.vc-top { display: flex; align-items: center; gap: 6px; }
.vc-code {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.el-table {
border-radius: 0;
.vc-status, .vc-active { border-radius: 10px !important; }
.vc-meta { font-size: 11px; color: #c0c4cc; }
.vc-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.btn-view { color: #5F7BA0 !important; }
::v-deep .el-table .current-row {
background: #f0f7ff !important;
}
/* ── 表格行高亮 ── */
::v-deep .el-table .current-row { background: #f0f7ff !important; }
::v-deep .el-table .current-row td { background: #f0f7ff !important; }
/* ── 按钮主色覆盖 ── */
::v-deep .el-button--primary {
color: #fff !important;
background: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--primary:hover,
::v-deep .el-button--primary:focus {
background: #4d6a8e !important;
border-color: #4d6a8e !important;
}
::v-deep .el-button--primary:active {
background: #4a6585 !important;
border-color: #4a6585 !important;
}
::v-deep .el-button--primary.is-disabled {
opacity: .5;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger) {
color: #606266 !important;
background: #fff !important;
border-color: #dcdfe6 !important;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger):hover {
color: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--text {
background: transparent !important;
border-color: transparent !important;
}
::v-deep .el-button--text.btn-danger {
color: #f56c6c !important;
}
.btn-danger {
color: #f56c6c;
}
::v-deep .el-button--primary.is-disabled { opacity: .5; }
::v-deep .el-button--text { background: transparent !important; border-color: transparent !important; }
::v-deep .el-button--text.btn-danger { color: #f56c6c !important; }
.btn-danger { color: #f56c6c; }
</style>

View File

@@ -2,22 +2,29 @@
<div class="plan-spec-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
<el-button type="text" icon="el-icon-arrow-left" class="back-btn" @click="goBack">返回</el-button>
<div class="header-divider" />
<span class="page-title">方案详情</span>
<span v-if="versionCode" class="version-badge">版本 {{ versionCode }}</span>
<span v-if="versionCode" class="version-badge">
<i class="el-icon-price-tag" style="margin-right:3px" />版本 {{ versionCode }}
</span>
<div style="flex:1" />
<!-- 可配置 / 不可配置 切换 -->
<div class="config-tabs">
<span :class="['config-tab', { active: configMode === 'configurable' }]" @click="configMode = 'configurable'">可配置</span>
<span :class="['config-tab', { active: configMode === 'readonly' }]" @click="configMode = 'readonly'">不可配置</span>
</div>
</div>
<!-- 可配置 / 不可配置 切换 -->
<div class="config-tabs">
<span
:class="['config-tab', { active: configMode === 'configurable' }]"
@click="configMode = 'configurable'"
>可配置</span>
<span
:class="['config-tab', { active: configMode === 'readonly' }]"
@click="configMode = 'readonly'"
>不可配置</span>
</div>
<!-- 全局异常提示条 -->
<el-alert
v-if="allAnomalies.length"
:title="`检测到 ${allAnomalies.length} 项实际生产偏差(来自最近一次实绩分析),请选择对应点位查看详情`"
type="warning"
show-icon
:closable="false"
style="margin-bottom:10px"
/>
<div class="main-layout">
<!-- 左侧段分组 -->
@@ -96,11 +103,7 @@
<template slot-scope="{ row }">{{ segLabel(row.segmentType) }} {{ row.segmentName || '—' }}</template>
</el-table-column>
<el-table-column label="点位名称" prop="pointName" show-overflow-tooltip />
<el-table-column label="实际值ID" prop="actualValueId" show-overflow-tooltip />
<el-table-column label="L1设定值ID" prop="l1SetValueId" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="点位编码" prop="pointCode" show-overflow-tooltip />
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openPlanDialog(row)">编辑</el-button>
@@ -117,12 +120,32 @@
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openParamDialog()">新建参数</el-button>
</div>
<el-table v-loading="paramLoading" :data="paramList" size="small">
<el-table-column label="参数编码" prop="paramCode" show-overflow-tooltip />
<el-table-column label="参数编码" prop="paramCode" width="110" show-overflow-tooltip />
<el-table-column label="参数名称" prop="paramName" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="单位" prop="unit" align="center" />
<el-table-column label="实际值ID" prop="actualSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.actualSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="L1设定值ID" prop="presetSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.presetSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="设定值" prop="targetValue" align="right" width="80" />
<el-table-column label="最小值" prop="lowerLimit" align="right" width="80" />
<el-table-column label="最大值" prop="upperLimit" align="right" width="80" />
<el-table-column label="单位" prop="unit" align="center" width="60" />
<el-table-column label="更新时间" align="center" width="136">
<template slot-scope="{ row }">{{ (row.updateTime || row.createTime || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="实际状态" align="center" width="90">
<template slot-scope="{ row }">
<span v-if="paramAnomalyMap[row.paramCode]" class="anomaly-badge">
<i class="el-icon-warning-outline" /> 异常
</span>
<span v-else-if="row.upperLimit != null || row.lowerLimit != null" class="normal-badge">
<i class="el-icon-circle-check" /> 正常
</span>
<span v-else class="no-data-badge"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="openParamDialog(null, row)">编辑</el-button>
@@ -130,10 +153,166 @@
</template>
</el-table-column>
</el-table>
<!-- 偏差分析区块 -->
<template v-if="planAnomalies.length">
<div class="anomaly-section-header">
<i class="el-icon-warning" style="color:#E6A23C;margin-right:4px" />
实际生产偏差分析
<el-tag type="warning" size="mini" effect="plain" style="margin-left:8px">{{ planAnomalies.length }} 项异常</el-tag>
<el-button type="text" size="mini" style="margin-left:auto" @click="anomalyExpanded = !anomalyExpanded">
{{ anomalyExpanded ? '收起' : '展开' }}
</el-button>
</div>
<div v-show="anomalyExpanded">
<el-table :data="planAnomalies" size="small" border>
<el-table-column label="参数" prop="paramName" width="110" show-overflow-tooltip />
<el-table-column label="规程设定值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedTarget != null ? row.storedTarget : '—' }}</template>
</el-table-column>
<el-table-column label="规程最大值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedUpper != null ? row.storedUpper : '—' }}</template>
</el-table-column>
<el-table-column label="规程最小值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedLower != null ? row.storedLower : '—' }}</template>
</el-table-column>
<el-table-column label="实际最大值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH') ? 'val-over' : ''">
{{ row.actualMax != null ? row.actualMax : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="实际最小值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH') ? 'val-under' : ''">
{{ row.actualMin != null ? row.actualMin : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="最大偏差" align="right" width="88">
<template slot-scope="{ row }">
<span v-if="row.deviationMax != null" class="val-over">+{{ row.deviationMax }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="最小偏差" align="right" width="88">
<template slot-scope="{ row }">
<span v-if="row.deviationMin != null" class="val-under">{{ row.deviationMin }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="来源钢卷" prop="coilId" show-overflow-tooltip width="120" />
<el-table-column label="检测时间" prop="detectedAt" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ (row.detectedAt || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="异常类型" align="center" width="120">
<template slot-scope="{ row }">
<el-tag v-if="row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH'" size="mini" type="danger" style="margin:1px">超上限</el-tag>
<el-tag v-if="row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH'" size="mini" type="warning" style="margin:1px">低于下限</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 对比图 -->
<div ref="anomalyChart" class="anomaly-chart" />
</div>
</template>
</template>
</div>
</div>
<!-- 服役钢卷记录 -->
<div class="coil-record-section" v-loading="coilRecordLoading">
<div class="coil-record-hd">
<i class="el-icon-data-line" style="margin-right:5px;color:#5F7BA0" />
服役钢卷记录
<span class="coil-total-badge"> {{ coilRecordTotal }} </span>
<span v-if="coilRecords.filter(r => r.hasAnomaly).length" class="coil-anomaly-badge">
其中 {{ coilRecords.filter(r => r.hasAnomaly).length }} 根有异常
</span>
</div>
<el-table :data="coilRecords" size="small" border>
<el-table-column label="出口钢卷号" prop="coilId" min-width="140" show-overflow-tooltip />
<el-table-column label="入口钢卷号" prop="enCoilId" min-width="140" show-overflow-tooltip />
<el-table-column label="检测时间" min-width="140">
<template slot-scope="{ row }">{{ (row.processTime || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="参数异常情况" align="center" min-width="160">
<template slot-scope="{ row }">
<template v-if="row.hasAnomaly">
<el-tag type="danger" size="mini" effect="plain" style="margin-right:4px">
<i class="el-icon-warning-outline" /> {{ row.anomalyCnt }} 项参数超限
</el-tag>
<el-button type="text" size="mini" style="color:#F56C6C;padding:0"
@click="jumpToAnomaly(row.coilId)">查看</el-button>
</template>
<span v-else class="normal-badge"><i class="el-icon-circle-check" /> 全部正常</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" min-width="80">
<template slot-scope="{ row }">
<el-tag :type="row.hasAnomaly ? 'danger' : 'success'" size="mini" effect="dark">
{{ row.hasAnomaly ? '异常' : '正常' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<pagination
v-show="coilRecordTotal > 0"
:total="coilRecordTotal"
:page.sync="coilRecordPage"
:limit.sync="coilRecordPageSize"
@pagination="loadCoilRecords"
/>
</div>
<!-- 钢卷异常明细 dialog -->
<el-dialog
:title="`钢卷 ${coilAnomalyCoilId} — 异常明细`"
:visible.sync="coilAnomalyDialog"
width="900px"
append-to-body
>
<el-table :data="coilAnomalyList" size="small" border>
<el-table-column label="参数名称" prop="paramName" min-width="110" show-overflow-tooltip />
<el-table-column label="异常类型" align="center" width="120">
<template slot-scope="{ row }">
<el-tag v-if="row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH'" size="mini" type="danger" style="margin:1px">超上限</el-tag>
<el-tag v-if="row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH'" size="mini" type="warning" style="margin:1px">低于下限</el-tag>
</template>
</el-table-column>
<el-table-column label="规程设定值" prop="storedTarget" align="right" width="100" />
<el-table-column label="规程上限" prop="storedUpper" align="right" width="90" />
<el-table-column label="规程下限" prop="storedLower" align="right" width="90" />
<el-table-column label="实际最大值" align="right" width="100">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH') ? 'val-over' : ''">{{ row.actualMax }}</span>
</template>
</el-table-column>
<el-table-column label="实际最小值" align="right" width="100">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH') ? 'val-under' : ''">{{ row.actualMin }}</span>
</template>
</el-table-column>
<el-table-column label="最大偏差" align="right" width="90">
<template slot-scope="{ row }">
<span v-if="row.deviationMax != null" class="val-over">+{{ row.deviationMax }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="最小偏差" align="right" width="90">
<template slot-scope="{ row }">
<span v-if="row.deviationMin != null" class="val-under">{{ row.deviationMin }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" align="center" width="60" />
</el-table>
<div slot="footer">
<el-button size="small" @click="coilAnomalyDialog = false">关闭</el-button>
</div>
</el-dialog>
<!-- 方案点位 dialog -->
<el-dialog :title="planTitle" :visible.sync="planOpen" width="520px" append-to-body @close="planForm = {}">
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small">
@@ -151,11 +330,11 @@
<el-form-item label="点位编码" prop="pointCode">
<el-input v-model="planForm.pointCode" maxlength="64" />
</el-form-item>
<el-form-item label="实际值ID" prop="actualValueId">
<el-input v-model="planForm.actualValueId" maxlength="64" />
<el-form-item label="实际值ID" prop="actualSrcId">
<el-input v-model="planForm.actualSrcId" maxlength="64" />
</el-form-item>
<el-form-item label="L1设定值ID" prop="l1SetValueId">
<el-input v-model="planForm.l1SetValueId" maxlength="64" />
<el-form-item label="L1设定值ID" prop="presetSrcId">
<el-input v-model="planForm.presetSrcId" maxlength="64" />
</el-form-item>
<el-form-item label="设定值" prop="targetValue">
<el-input v-model="planForm.targetValue" />
@@ -289,9 +468,12 @@
</template>
<script>
import * as XLSX from 'xlsx';
import * as XLSX from 'xlsx'
import * as echarts from 'echarts'
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
import { listAllProcessAnomaly } from '@/api/wms/processAnomaly'
import { listProcessCoilRecord } from '@/api/wms/processCoilRecord'
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
@@ -352,6 +534,19 @@ export default {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
},
// 偏差分析
allAnomalies: [],
anomalyExpanded: true,
anomalyChartInst: null,
// 服役钢卷记录
coilRecords: [],
coilRecordTotal: 0,
coilRecordPage: 1,
coilRecordPageSize: 20,
coilRecordLoading: false,
// 钢卷异常明细弹窗
coilAnomalyDialog: false,
coilAnomalyCoilId: '',
// 导入相关数据
importOpen: false,
file: null,
@@ -437,6 +632,22 @@ export default {
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
return [...SEGMENT_FORM_OPTIONS, ...extra]
},
/** 当前选中点位下的异常条目 */
planAnomalies() {
if (!this.selectedPlan) return []
return this.allAnomalies.filter(a => a.paramCode === this.selectedPlan.pointCode ||
this.paramList.some(p => p.paramCode === a.paramCode))
},
/** paramCode → anomaly 的快速索引(用于状态列) */
paramAnomalyMap() {
const map = {}
this.allAnomalies.forEach(a => { map[a.paramCode] = a })
return map
},
coilAnomalyList() {
if (!this.coilAnomalyCoilId) return []
return this.allAnomalies.filter(a => a.coilId === this.coilAnomalyCoilId)
},
filteredPlans() {
const type = this.activeSegmentType
const sub = this.activeSegmentName
@@ -464,6 +675,18 @@ export default {
},
watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } },
planAnomalies: {
handler(list) {
if (list.length && this.anomalyExpanded) {
this.$nextTick(() => this.renderAnomalyChart())
}
}
},
anomalyExpanded(val) {
if (val && this.planAnomalies.length) {
this.$nextTick(() => this.renderAnomalyChart())
}
},
segmentTypeTabOptions: {
handler() {
const a = this.activeSegmentType
@@ -495,9 +718,121 @@ export default {
this.versionId = q.versionId || undefined
this.versionCode = q.versionCode || ''
this.specId = q.specId || undefined
if (this.versionId) this.loadPlans()
if (this.versionId) {
this.loadPlans()
this.loadAnomalies()
this.loadCoilRecords()
}
},
loadAnomalies() {
if (!this.versionId) return
listAllProcessAnomaly({ versionId: this.versionId }).then(res => {
this.allAnomalies = res.data || []
}).catch(() => { this.allAnomalies = [] })
},
loadCoilRecords() {
if (!this.versionId) return
this.coilRecordLoading = true
listProcessCoilRecord({ versionId: this.versionId, pageNum: this.coilRecordPage, pageSize: this.coilRecordPageSize })
.then(res => {
this.coilRecords = res.rows || []
this.coilRecordTotal = res.total || 0
}).catch(() => {}).finally(() => { this.coilRecordLoading = false })
},
renderAnomalyChart() {
const el = this.$refs.anomalyChart
if (!el || !this.planAnomalies.length) return
if (this.anomalyChartInst && !this.anomalyChartInst.isDisposed()) {
this.anomalyChartInst.dispose()
}
this.anomalyChartInst = echarts.init(el)
const items = this.planAnomalies
const names = items.map(a => a.paramName)
// 范围对比:规程范围 vs 实际范围,使用自定义 bar 叠加实现
const specRangeData = items.map(a => {
const lo = a.storedLower ?? a.storedTarget ?? 0
const hi = a.storedUpper ?? a.storedTarget ?? 0
return [lo, hi]
})
const actualRangeData = items.map(a => {
const lo = a.actualMin ?? 0
const hi = a.actualMax ?? 0
return [lo, hi]
})
// 把真实值编入 data让 ECharts 正确推断 y 轴范围
// data item: [categoryIndex, lo, hi]
const specSeriesData = specRangeData.map(([lo, hi], i) => [i, lo, hi])
const actualSeriesData = actualRangeData.map(([lo, hi], i) => [i, lo, hi])
const allVals = [...specRangeData, ...actualRangeData].flat().filter(v => v != null && isFinite(v))
const yMin = allVals.length ? Math.min(...allVals) : 0
const yMax = allVals.length ? Math.max(...allVals) : 1
const pad = (yMax - yMin) * 0.15 || 1
const option = {
title: { text: '规程范围 vs 实际范围对比', textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 6, left: 8 },
tooltip: {
trigger: 'axis',
formatter(params) {
const idx = params[0].dataIndex
const name = names[idx]
const spec = specRangeData[idx]
const actual = actualRangeData[idx]
return `${name}<br/>规程范围:${spec[0]} ~ ${spec[1]}<br/>实际范围:${actual[0]} ~ ${actual[1]}`
}
},
legend: { data: ['规程范围', '实际范围'], top: 6, right: 8, textStyle: { fontSize: 11 } },
grid: { top: 44, bottom: 36, left: 12, right: 20, containLabel: true },
xAxis: { type: 'category', data: names, axisLabel: { fontSize: 11, rotate: names.length > 5 ? 30 : 0 } },
yAxis: { type: 'value', min: yMin - pad, max: yMax + pad, axisLabel: { fontSize: 11 } },
series: [
{
name: '规程范围',
type: 'custom',
renderItem(params, api) {
const idx = api.value(0)
const lo = api.value(1)
const hi = api.value(2)
const start = api.coord([idx, lo])
const end = api.coord([idx, hi])
const w = api.size([1, 0])[0] * 0.3
return {
type: 'rect',
shape: { x: start[0] - w / 2, y: end[1], width: w, height: Math.max(start[1] - end[1], 2) },
style: { fill: 'rgba(95,123,160,0.35)', stroke: '#5F7BA0', lineWidth: 1 }
}
},
data: specSeriesData,
encode: { x: 0, y: [1, 2] }
},
{
name: '实际范围',
type: 'custom',
renderItem(params, api) {
const idx = api.value(0)
const lo = api.value(1)
const hi = api.value(2)
const start = api.coord([idx, lo])
const end = api.coord([idx, hi])
const w = api.size([1, 0])[0] * 0.18
return {
type: 'rect',
shape: { x: start[0] - w / 2, y: end[1], width: w, height: Math.max(start[1] - end[1], 2) },
style: { fill: 'rgba(245,108,108,0.45)', stroke: '#F56C6C', lineWidth: 1.5 }
}
},
data: actualSeriesData,
encode: { x: 0, y: [1, 2] }
}
]
}
this.anomalyChartInst.setOption(option)
},
goBack() { this.$router.go(-1) },
jumpToAnomaly(coilId) {
this.coilAnomalyCoilId = coilId
this.coilAnomalyDialog = true
},
selectSegmentType(val) {
this.activeSegmentType = val === undefined || val === null ? '' : val
this.activeSegmentName = ''
@@ -549,8 +884,8 @@ export default {
segmentName: undefined,
pointName: undefined,
pointCode: undefined,
actualValueId: undefined,
l1SetValueId: undefined,
actualSrcId: undefined,
presetSrcId: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
@@ -1061,26 +1396,43 @@ export default {
align-items: center;
gap: 10px;
margin-bottom: 12px;
background: #fff;
border-radius: 4px;
padding: 8px 12px;
border: 1px solid #ebeef5;
}
.back-btn {
color: #5F7BA0 !important;
padding: 0 !important;
font-size: 13px !important;
}
.header-divider {
width: 1px;
height: 16px;
background: #dcdfe6;
flex-shrink: 0;
}
.page-title {
font-size: 15px;
font-weight: 600;
font-size: 14px;
font-weight: 700;
color: #303133;
}
.version-badge {
font-size: 12px;
color: #909399;
background: #f0f2f5;
padding: 2px 8px;
color: #5F7BA0;
background: #edf2f8;
padding: 2px 10px;
border-radius: 10px;
border: 1px solid #c8d8ea;
}
.config-tabs {
display: flex;
gap: 0;
margin-bottom: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
@@ -1206,6 +1558,28 @@ export default {
.btn-danger { color: #f56c6c; }
/* ── 偏差分析 ── */
.anomaly-section-header {
display: flex;
align-items: center;
padding: 10px 0 8px;
margin-top: 14px;
border-top: 2px solid #fdf6ec;
font-size: 13px;
font-weight: 600;
color: #E6A23C;
}
.anomaly-chart {
width: 100%;
height: 260px;
margin-top: 10px;
}
.anomaly-badge { font-size: 12px; color: #F56C6C; font-weight: 600; }
.normal-badge { font-size: 12px; color: #67C23A; }
.no-data-badge { font-size: 12px; color: #c0c4cc; }
.val-over { color: #F56C6C; font-weight: 600; }
.val-under { color: #E6A23C; font-weight: 600; }
/* 导入对话框样式 */
.import-container {
padding: 20px;
@@ -1261,4 +1635,35 @@ export default {
.import-error {
margin-bottom: 20px;
}
/* ── 服役钢卷记录 ── */
.coil-record-section {
margin-top: 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.coil-record-hd {
display: flex;
align-items: center;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #f0f2f5;
background: #fafafa;
}
.coil-total-badge {
font-size: 12px;
font-weight: normal;
color: #606266;
margin-left: 8px;
}
.coil-anomaly-badge {
font-size: 12px;
font-weight: normal;
color: #f56c6c;
margin-left: 6px;
}
</style>