Files
klp-oa/klp-ui/src/views/wms/processSpec/coilSpecBind.vue
2026-05-14 15:36:14 +08:00

1096 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="coil-bind-page">
<!-- 筛选 + 操作一体栏 -->
<div class="filter-bar">
<el-form :inline="true" :model="query" size="small" class="filter-form">
<el-form-item label="出口卷号">
<el-input
v-model="query.coilId"
placeholder="模糊搜索"
clearable
style="width:150px"
@keyup.enter.native="handleSearch"
/>
</el-form-item>
<el-form-item label="入口卷号">
<el-input
v-model="query.enCoilId"
placeholder="模糊搜索"
clearable
style="width:150px"
@keyup.enter.native="handleSearch"
/>
</el-form-item>
<el-form-item label="异常">
<el-select v-model="query.hasAnomaly" placeholder="全部" clearable style="width:88px">
<el-option label="有异常" :value="1" />
<el-option label="正常" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 右侧始终可见的操作区 -->
<div class="toolbar-action">
<transition name="sel-fade">
<span v-if="selected.length" class="sel-badge">
已选 <b>{{ selected.length }}</b>
<el-button type="text" size="mini" style="color:#909399;padding:0 4px" @click="$refs.recordTable.clearSelection()">清除</el-button>
</span>
</transition>
<el-button
type="primary"
size="small"
icon="el-icon-connection"
:disabled="!selected.length"
@click="openBindDialog"
>重新绑定规程方案</el-button>
</div>
</div>
<!-- 表格 -->
<div class="table-wrap" v-loading="loading">
<el-table
ref="recordTable"
:data="rows"
size="small"
border
:height="tableHeight"
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="44" fixed />
<el-table-column type="index" width="44" label="序" align="center" fixed />
<el-table-column prop="coilId" label="出口卷号" width="160" fixed show-overflow-tooltip />
<el-table-column prop="enCoilId" label="入口卷号" width="160" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.enCoilId || '—' }}</template>
</el-table-column>
<el-table-column label="当前规程方案" min-width="220" show-overflow-tooltip>
<template slot-scope="{ row }">
<template v-if="row.specName || row.specCode">
<span class="tag-spec">{{ row.specCode || '—' }}</span>
<span class="tag-spec-name">{{ row.specName }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="版本" width="110">
<template slot-scope="{ row }">
<template v-if="row.versionCode">
<span class="tag-version">{{ row.versionCode }}</span>
<el-tag
v-if="row.isActive === 1"
type="success"
size="mini"
effect="dark"
style="border-radius:8px;margin-left:4px"
>生效</el-tag>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="异常" width="76" align="center">
<template slot-scope="{ row }">
<el-tag
v-if="row.hasAnomaly === 1"
type="danger"
size="mini"
effect="plain"
disable-transitions
>{{ row.anomalyCnt }} </el-tag>
<el-tag v-else type="success" size="mini" effect="plain" disable-transitions>正常</el-tag>
</template>
</el-table-column>
<el-table-column label="检测时间" width="152">
<template slot-scope="{ row }">{{ fmtTime(row.processTime) }}</template>
</el-table-column>
<el-table-column label="记录时间" width="152">
<template slot-scope="{ row }">{{ fmtTime(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="72" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 底部分页栏 -->
<div class="bottom-bar">
<el-pagination
small
layout="total, sizes, prev, pager, next"
:total="total"
:page-size="query.pageSize"
:page-sizes="[20, 50, 100]"
:current-page="query.pageNum"
@size-change="v => { query.pageSize = v; query.pageNum = 1; loadRows() }"
@current-change="v => { query.pageNum = v; loadRows() }"
/>
</div>
<!-- ══════════════════════════════════════
钢卷参数详情 抽屉
══════════════════════════════════════ -->
<el-drawer
:visible.sync="detailDrawerVisible"
:title="detailRow ? ('钢卷参数详情 · ' + detailRow.coilId) : '钢卷参数详情'"
direction="rtl"
size="780px"
append-to-body
@close="onDetailClose"
>
<div class="detail-wrap" v-loading="detailLoading">
<!-- 基本信息卡 -->
<div class="detail-info-card" v-if="detailRow">
<div class="dic-row">
<span class="dic-label">出口卷号</span>
<span class="dic-val">{{ detailRow.coilId || '—' }}</span>
<span class="dic-label">入口卷号</span>
<span class="dic-val">{{ detailRow.enCoilId || '—' }}</span>
</div>
<div class="dic-row">
<span class="dic-label">规程方案</span>
<span class="dic-val">
<span v-if="detailRow.specCode" class="tag-spec">{{ detailRow.specCode }}</span>
<span v-if="detailRow.specName" class="tag-spec-name">{{ detailRow.specName }}</span>
<span v-if="!detailRow.specCode && !detailRow.specName" class="dim">—</span>
</span>
<span class="dic-label">版本</span>
<span class="dic-val">
<span v-if="detailRow.versionCode" class="tag-version">{{ detailRow.versionCode }}</span>
<span v-else class="dim">—</span>
</span>
</div>
<div class="dic-row">
<span class="dic-label">检测时间</span>
<span class="dic-val">{{ fmtTime(detailRow.processTime) }}</span>
<span class="dic-label">异常状态</span>
<span class="dic-val">
<el-tag v-if="detailRow.hasAnomaly === 1" type="danger" size="mini" effect="plain">{{ detailRow.anomalyCnt }} 项异常</el-tag>
<el-tag v-else type="success" size="mini" effect="plain">正常</el-tag>
</span>
</div>
</div>
<!-- 参数分段 Tab -->
<div class="seg-tabs">
<div class="seg-tab-bar">
<button
v-for="seg in detailSegmentTabs"
:key="seg.segmentType"
:class="['seg-tab-btn', { 'is-active': detailActiveTab === seg.segmentType }]"
@click="detailActiveTab = seg.segmentType"
>
{{ seg.segmentLabel }}
<span v-if="seg.anomalyCount > 0" class="seg-badge">{{ seg.anomalyCount }}</span>
</button>
</div>
<template v-for="seg in detailSegmentTabs">
<div v-show="detailActiveTab === seg.segmentType" :key="seg.segmentType" class="seg-tab-pane">
<div v-if="!detailLoading && seg.groups.length === 0" class="detail-empty">暂无参数数据</div>
<div v-for="group in seg.groups" :key="group.planId" class="param-group">
<div class="param-group-hd">
<span class="pg-point">{{ group.pointName }}</span>
<span v-if="group.pointCode" class="pg-code">{{ group.pointCode }}</span>
</div>
<el-table :data="group.params" size="mini" border :row-class-name="paramRowClass">
<el-table-column label="参数名称" prop="paramName" min-width="120" show-overflow-tooltip />
<el-table-column label="编码" prop="paramCode" width="100" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="68" align="center">
<template slot-scope="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="设定目标" width="84" align="right">
<template slot-scope="{ row }">
<span :class="row._hasAnomaly ? 'val-anomaly' : ''">{{ fmtNum(row.targetValue) }}</span>
</template>
</el-table-column>
<el-table-column label="下限" width="72" align="right">
<template slot-scope="{ row }">{{ fmtNum(row.lowerLimit) }}</template>
</el-table-column>
<el-table-column label="上限" width="72" align="right">
<template slot-scope="{ row }">{{ fmtNum(row.upperLimit) }}</template>
</el-table-column>
<el-table-column label="实际最大" width="84" align="right">
<template slot-scope="{ row }">
<span v-if="row._hasAnomaly" :class="row._anomalyType === 'UPPER' ? 'val-danger' : 'val-ok'">{{ fmtNum(row._actualMax) }}</span>
<span v-else class="dim">—</span>
</template>
</el-table-column>
<el-table-column label="实际最小" width="84" align="right">
<template slot-scope="{ row }">
<span v-if="row._hasAnomaly" :class="row._anomalyType === 'LOWER' ? 'val-danger' : 'val-ok'">{{ fmtNum(row._actualMin) }}</span>
<span v-else class="dim">—</span>
</template>
</el-table-column>
<el-table-column label="偏差" width="72" align="right">
<template slot-scope="{ row }">
<span v-if="row._hasAnomaly" class="val-danger">
{{ fmtNum(row._anomalyType === 'UPPER' ? row._deviationMax : row._deviationMin) }}
</span>
<span v-else class="dim">—</span>
</template>
</el-table-column>
<el-table-column label="状态" width="76" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row._hasAnomaly" type="danger" size="mini" effect="plain">
{{ row._anomalyType === 'UPPER' ? '超上限' : row._anomalyType === 'LOWER' ? '低于下限' : '异常' }}
</el-tag>
<el-tag v-else type="success" size="mini" effect="plain">正常</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
</div>
</div>
</el-drawer>
<!-- ══════════════════════════════════════
重新绑定 对话框
══════════════════════════════════════ -->
<el-dialog
title="重新绑定规程方案"
:visible.sync="bindDialogVisible"
width="960px"
append-to-body
:close-on-click-modal="false"
@close="resetBindDialog"
>
<!-- ① 摘要条 — 始终可见 -->
<div :class="['summary-bar', selectedVersion ? 'summary-bar--ready' : 'summary-bar--idle']">
<template v-if="selectedVersion">
<i class="el-icon-success summary-icon" />
<span>
将把
<b class="s-count">{{ selected.length }}</b>
条记录重新绑定至规程
<span class="s-spec">{{ selectedSpec.specName }}</span>
<span class="s-sep"></span>
版本
<span class="s-version">{{ selectedVersion.versionCode }}</span>
</span>
<div class="summary-remove-opt">
<el-checkbox v-model="removeOldRecord" :disabled="!singleSourceVersion">
同时从原版本移除
<span v-if="!singleSourceVersion" class="dim" style="margin-left:4px">(选中记录来自多个版本时不可用)</span>
</el-checkbox>
</div>
</template>
<template v-else>
<i class="el-icon-info summary-icon summary-icon--idle" />
<span class="s-idle">请先在下方选择目标规程,再选择版本后点击「确认绑定」</span>
</template>
</div>
<!-- ② 左右选择区 -->
<div class="bind-body">
<!-- 左:规程列表 -->
<div class="spec-col">
<div class="col-hd">规程列表</div>
<div class="spec-filter">
<el-input
v-model="specKeyword"
placeholder="搜索名称 / 编码"
size="mini"
clearable
prefix-icon="el-icon-search"
@input="filterSpecs"
/>
</div>
<div class="spec-list" v-loading="specLoading">
<div
v-for="spec in filteredSpecList"
:key="spec.specId"
:class="['spec-item', { 'spec-item--active': selectedSpec && selectedSpec.specId === spec.specId }]"
@click="selectSpec(spec)"
>
<div class="spec-item__name">{{ spec.specName }}</div>
<div class="spec-item__code">{{ spec.specCode }}</div>
</div>
<div v-if="!specLoading && !filteredSpecList.length" class="list-empty">暂无规程</div>
</div>
</div>
<div class="col-divider" />
<!-- 右:版本列表 -->
<div class="version-col">
<template v-if="!selectedSpec">
<div class="version-placeholder">
<i class="el-icon-back" style="font-size:18px;color:#c0c4cc;margin-right:6px" />
请在左侧选择规程
</div>
</template>
<template v-else>
<div class="col-hd">
<span class="spec-name-label">{{ selectedSpec.specName }}</span>
<span class="spec-code-chip">{{ selectedSpec.specCode }}</span>
</div>
<div v-if="selectedSpec.remark" class="spec-remark">{{ selectedSpec.remark }}</div>
<div class="version-list" v-loading="versionLoading">
<div v-if="!versionLoading && !versionList.length" class="list-empty">
该规程暂无版本,请先在规程管理中创建
</div>
<div
v-for="ver in versionList"
:key="ver.versionId"
:class="['version-card', {
'version-card--selected': selectedVersion && selectedVersion.versionId === ver.versionId,
'version-card--obsolete': ver.status === 'OBSOLETE'
}]"
@click="ver.status !== 'OBSOLETE' && selectVersion(ver)"
>
<div class="vc-body">
<div class="vc-row1">
<span class="vc-code">{{ ver.versionCode }}</span>
<el-tag :type="versionStatusType(ver.status)" size="mini" effect="plain" style="border-radius:10px;margin-left:6px">
{{ versionStatusLabel(ver.status) }}
</el-tag>
<el-tag v-if="ver.isActive === 1" type="success" size="mini" effect="dark" style="border-radius:10px;margin-left:4px">
当前生效
</el-tag>
<span v-if="ver.status === 'OBSOLETE'" class="obsolete-note">(已作废,不可选)</span>
</div>
<div class="vc-row2">
创建于 {{ (ver.createTime || '').substring(0, 16) || '—' }}
<template v-if="ver.updateTime && ver.updateTime !== ver.createTime">
&nbsp;· 更新于 {{ (ver.updateTime || '').substring(0, 16) }}
</template>
</div>
<div v-if="ver.remark" class="vc-remark">{{ ver.remark }}</div>
</div>
<div class="vc-check">
<i v-if="selectedVersion && selectedVersion.versionId === ver.versionId" class="el-icon-circle-check" />
</div>
</div>
</div>
</template>
</div>
</div>
<div slot="footer" class="bind-footer">
<el-button size="small" @click="bindDialogVisible = false">取消</el-button>
<el-button
size="small"
type="primary"
:disabled="!selectedVersion"
:loading="bindLoading"
@click="confirmBind"
>确认绑定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcessCoilRecord, batchRebindCoilRecord } from '@/api/wms/processCoilRecord'
import { listProcessSpec } from '@/api/wms/processSpec'
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam } from '@/api/wms/processPlanParam'
import { listAllProcessAnomaly } from '@/api/wms/processAnomaly'
import { getPresetSetupByCoilId } from '@/api/l2/timing'
// 三段 L1 预设设定值的结构定义(对应 PLTCM_PRESET_SETUP 表)
const PRESET_PLANS = [
{
segmentType: 'INLET', segmentName: '入口段', pointName: '入口设定值', pointCode: 'PRESET_INLET', sortOrder: 10,
params: [
{ paramCode: 'POR_TEN', paramName: '开卷机单位张力', unit: 'N/mm²' },
{ paramCode: 'CEL_TEN', paramName: '入口活套单位张力', unit: 'N/mm²' },
{ paramCode: 'FLAT_MESH_1', paramName: '矫直机1#辊插入量', unit: 'mm' },
{ paramCode: 'FLAT_MESH_2', paramName: '矫直机2#辊插入量', unit: 'mm' }
]
},
{
segmentType: 'PROCESS', segmentName: '工艺段', pointName: '工艺设定值', pointCode: 'PRESET_PROCESS', sortOrder: 20,
params: [
{ paramCode: 'TLV_TEN', paramName: '拉弯矫直机单位张力', unit: 'N/mm²' },
{ paramCode: 'TLV_ELONG', paramName: '拉弯矫直机延伸率', unit: '%' },
{ paramCode: 'TLV_MESH_1', paramName: '弯曲辊1#弯辊量', unit: 'mm' },
{ paramCode: 'TLV_MESH_2', paramName: '弯曲辊2#弯辊量', unit: 'mm' }
]
},
{
segmentType: 'OUTLET', segmentName: '出口段', pointName: '出口设定值', pointCode: 'PRESET_OUTLET', sortOrder: 30,
params: [
{ paramCode: 'TR_TEN', paramName: '酸洗出口张力辊张力', unit: 'N/mm²' },
{ paramCode: 'TRIM_TEN', paramName: '切边段单位张力', unit: 'N/mm²' },
{ paramCode: 'TEL_TEN', paramName: '联机活套单位张力', unit: 'N/mm²' },
{ paramCode: 'CXL_TEN', paramName: '出口活套单位张力', unit: 'N/mm²' }
]
}
]
const VERSION_STATUS = [
{ value: 'DRAFT', label: '草稿' },
{ value: 'PUBLISHED', label: '已发布' },
{ value: 'OBSOLETE', label: '已作废' }
]
export default {
name: 'CoilSpecBind',
data() {
return {
loading: false,
rows: [],
total: 0,
selected: [],
query: {
pageNum: 1,
pageSize: 20,
coilId: '',
enCoilId: '',
hasAnomaly: null
},
tableHeight: 'calc(100vh - 218px)',
detailDrawerVisible: false,
detailLoading: false,
detailRow: null,
detailParams: [],
detailActiveTab: 'INLET',
bindDialogVisible: false,
removeOldRecord: false,
specLoading: false,
specList: [],
filteredSpecList: [],
specKeyword: '',
selectedSpec: null,
versionLoading: false,
versionList: [],
selectedVersion: null,
bindLoading: false
}
},
computed: {
detailParamGroups() {
const planMap = {}
for (const p of this.detailParams) {
const key = p._planId
if (!planMap[key]) {
planMap[key] = {
planId: p._planId,
segmentType: p._segmentType,
segmentName: p._segmentName,
pointName: p._pointName,
pointCode: p._pointCode,
sortOrder: p._sortOrder,
params: []
}
}
planMap[key].params.push(p)
}
return Object.values(planMap).sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
},
detailSegmentTabs() {
const SEG_ORDER = ['INLET', 'PROCESS', 'OUTLET']
const SEG_LABELS = { INLET: '入口段', PROCESS: '工艺段', OUTLET: '出口段' }
const map = {}
for (const g of this.detailParamGroups) {
const seg = g.segmentType || 'OTHER'
if (!map[seg]) {
map[seg] = { segmentType: seg, segmentLabel: SEG_LABELS[seg] || seg, anomalyCount: 0, groups: [] }
}
map[seg].groups.push(g)
map[seg].anomalyCount += g.params.filter(p => p._hasAnomaly).length
}
for (const seg of SEG_ORDER) {
if (!map[seg]) map[seg] = { segmentType: seg, segmentLabel: SEG_LABELS[seg], anomalyCount: 0, groups: [] }
}
return SEG_ORDER.map(s => map[s])
},
// 选中行是否全来自同一个版本(决定是否可以"移除旧记录"
singleSourceVersion() {
if (!this.selected.length) return false
const vids = [...new Set(this.selected.map(r => r.versionId))]
return vids.length === 1
}
},
created() {
this.loadRows()
},
methods: {
async loadRows() {
this.loading = true
try {
const params = { ...this.query }
if (!params.coilId) delete params.coilId
if (!params.enCoilId) delete params.enCoilId
if (params.hasAnomaly === null) delete params.hasAnomaly
const res = await listProcessCoilRecord(params)
this.rows = res.rows || []
this.total = res.total || 0
} finally {
this.loading = false
}
},
handleSearch() {
this.query.pageNum = 1
this.loadRows()
},
handleReset() {
this.query = { pageNum: 1, pageSize: 20, coilId: '', enCoilId: '', hasAnomaly: null }
this.loadRows()
},
onSelectionChange(val) {
this.selected = val
if (!this.singleSourceVersion) this.removeOldRecord = false
},
// ── 绑定对话框 ──────────────────────────────
openBindDialog() {
this.specKeyword = ''
this.selectedSpec = null
this.selectedVersion = null
this.versionList = []
this.removeOldRecord = false
this.bindDialogVisible = true
this.loadSpecs()
},
resetBindDialog() {
this.selectedSpec = null
this.selectedVersion = null
this.versionList = []
this.specKeyword = ''
this.removeOldRecord = false
},
async loadSpecs() {
this.specLoading = true
try {
const res = await listProcessSpec({ pageNum: 1, pageSize: 200 })
this.specList = res.rows || []
this.filteredSpecList = this.specList
} finally {
this.specLoading = false
}
},
filterSpecs() {
const kw = (this.specKeyword || '').trim().toLowerCase()
this.filteredSpecList = kw
? this.specList.filter(s =>
(s.specName || '').toLowerCase().includes(kw) ||
(s.specCode || '').toLowerCase().includes(kw)
)
: this.specList
},
selectSpec(spec) {
this.selectedSpec = spec
this.selectedVersion = null
this.versionList = []
this.loadVersions(spec.specId)
},
async loadVersions(specId) {
this.versionLoading = true
try {
const res = await listProcessSpecVersion({ specId, pageNum: 1, pageSize: 200 })
this.versionList = res.rows || []
} finally {
this.versionLoading = false
}
},
selectVersion(ver) {
this.selectedVersion = ver
},
async confirmBind() {
if (!this.selectedVersion || !this.selected.length) return
this.bindLoading = true
try {
const coilIds = this.selected.map(r => r.coilId)
const oldVersionId = (this.removeOldRecord && this.singleSourceVersion)
? this.selected[0].versionId
: null
await batchRebindCoilRecord({
coilIds,
newVersionId: this.selectedVersion.versionId,
oldVersionId
})
this.$modal.msgSuccess(
`已成功将 ${this.selected.length} 条记录绑定至【${this.selectedSpec.specName} / ${this.selectedVersion.versionCode}`
)
this.bindDialogVisible = false
this.$refs.recordTable.clearSelection()
this.loadRows()
} catch (e) {
console.error(e)
} finally {
this.bindLoading = false
}
},
// ── 详情抽屉 ──────────────────────────────────
openDetail(row) {
this.detailRow = row
this.detailParams = []
this.detailActiveTab = 'INLET'
this.detailDrawerVisible = true
this.loadDetailData(row)
},
onDetailClose() {
this.detailRow = null
this.detailParams = []
},
// Returns true if any preset plan points were auto-created
async autoInitPresetParams(row, existingPlans) {
try {
const existingCodes = new Set(existingPlans.map(p => p.pointCode))
const missing = PRESET_PLANS.filter(p => !existingCodes.has(p.pointCode))
if (!missing.length) return false
const srcId = row.enCoilId || row.coilId
if (!srcId) return false
const presetRes = await getPresetSetupByCoilId(srcId)
const preset = (presetRes && presetRes.data && presetRes.data.data) || {}
for (const def of missing) {
const planRes = await addProcessPlan({
versionId: row.versionId,
segmentType: def.segmentType,
segmentName: def.segmentName,
pointName: def.pointName,
pointCode: def.pointCode,
sortOrder: def.sortOrder
})
const planId = planRes.data
for (const p of def.params) {
const val = preset[p.paramCode.toLowerCase()] ?? preset[p.paramCode]
await addProcessPlanParam({
planId,
paramCode: p.paramCode,
paramName: p.paramName,
unit: p.unit,
targetValue: (val !== null && val !== undefined && val !== '') ? Number(val) : null,
presetSrcId: srcId
})
}
}
return true
} catch (e) {
console.error('[autoInitPresetParams] failed', e)
return false
}
},
async loadDetailData(row) {
if (!row.versionId) return
this.detailLoading = true
try {
const [plansRes, anomalyRes] = await Promise.all([
listProcessPlan({ versionId: row.versionId, pageNum: 1, pageSize: 200 }),
listAllProcessAnomaly({ coilId: row.coilId, versionId: row.versionId })
])
let plans = plansRes.rows || []
const added = await this.autoInitPresetParams(row, plans)
if (added) {
const refreshed = await listProcessPlan({ versionId: row.versionId, pageNum: 1, pageSize: 200 })
plans = refreshed.rows || []
}
const anomalyList = anomalyRes.data || []
const anomalyMap = {}
for (const a of anomalyList) {
anomalyMap[a.paramCode] = a
}
const allParams = []
const paramPromises = plans.map(plan =>
listProcessPlanParam({ planId: plan.planId, pageNum: 1, pageSize: 500 })
.then(res => {
const params = res.rows || []
for (const p of params) {
const anomaly = anomalyMap[p.paramCode] || null
allParams.push({
...p,
_planId: plan.planId,
_segmentType: plan.segmentType,
_segmentName: plan.segmentName,
_pointName: plan.pointName,
_pointCode: plan.pointCode,
_sortOrder: plan.sortOrder,
_hasAnomaly: !!anomaly,
_anomalyType: anomaly ? anomaly.anomalyType : null,
_actualMax: anomaly ? anomaly.actualMax : null,
_actualMin: anomaly ? anomaly.actualMin : null,
_deviationMax: anomaly ? anomaly.deviationMax : null,
_deviationMin: anomaly ? anomaly.deviationMin : null
})
}
})
)
await Promise.all(paramPromises)
this.detailParams = allParams
} finally {
this.detailLoading = false
}
},
paramRowClass({ row }) {
return row._hasAnomaly ? 'row-anomaly' : ''
},
fmtNum(val) {
if (val === null || val === undefined || val === '') return '—'
const n = Number(val)
return isNaN(n) ? val : n.toFixed(3).replace(/\.?0+$/, '')
},
// ── 工具 ──────────────────────────────────
fmtTime(val) {
if (!val) return '—'
return String(val).substring(0, 16)
},
versionStatusType(status) {
return { DRAFT: '', PUBLISHED: 'success', OBSOLETE: 'info' }[status] || ''
},
versionStatusLabel(status) {
return (VERSION_STATUS.find(s => s.value === status) || {}).label || status || '—'
}
}
}
</script>
<style scoped>
/* ── 页面 ── */
.coil-bind-page {
padding: 12px 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: #fff;
}
/* ── 筛选 + 操作一体栏 ── */
.filter-bar {
flex-shrink: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
.filter-form { flex: 1; min-width: 0; }
.filter-bar .el-form-item { margin-bottom: 6px; }
.toolbar-action {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
padding-top: 2px;
}
.sel-badge {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.sel-badge b { color: #5F7BA0; font-size: 15px; margin: 0 2px; }
.sel-fade-enter-active, .sel-fade-leave-active { transition: opacity .2s; }
.sel-fade-enter, .sel-fade-leave-to { opacity: 0; }
/* ── 表格 ── */
.table-wrap {
flex: 1;
min-height: 0;
border: 1px solid #ebeef5;
border-radius: 3px;
overflow: hidden;
}
.tag-spec {
display: inline-block;
padding: 1px 7px;
border-radius: 3px 0 0 3px;
background: #ecf5ff;
color: #3a6ea8;
font-size: 11px;
border: 1px solid #b3d8ff;
}
.tag-spec-name {
display: inline-block;
padding: 1px 7px;
border-radius: 0 3px 3px 0;
background: #f5f7fa;
color: #606266;
font-size: 11px;
border: 1px solid #dcdfe6;
border-left: none;
}
.tag-version {
display: inline-block;
padding: 1px 8px;
background: #f0f9eb;
color: #4a8a2a;
font-size: 11px;
border: 1px solid #c2e7b0;
border-radius: 3px;
}
.dim { font-size: 12px; color: #c0c4cc; }
/* ── 底部分页 ── */
.bottom-bar {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
padding: 8px 0 0;
}
/* ── 摘要条 ── */
.summary-bar {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 16px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.6;
transition: background .2s, border-color .2s;
flex-wrap: wrap;
}
.summary-bar--idle { background: #f4f4f5; border: 1px solid #e9e9eb; color: #909399; }
.summary-bar--ready { background: #ecf5ff; border: 1px solid #b3d8ff; color: #303133; }
.summary-icon { font-size: 16px; flex-shrink: 0; color: #5F7BA0; margin-top: 2px; }
.summary-icon--idle { color: #c0c4cc; }
.s-count { font-size: 15px; color: #5F7BA0; margin: 0 2px; }
.s-spec { font-weight: 600; color: #303133; margin: 0 3px; }
.s-sep { color: #c0c4cc; margin: 0 3px; }
.s-version { font-weight: 600; color: #5F7BA0; }
.s-idle { color: #909399; }
.summary-remove-opt {
margin-left: auto;
flex-shrink: 0;
font-size: 12px;
color: #606266;
}
/* ── 对话框主体 ── */
.bind-body {
display: flex;
height: 460px;
overflow: hidden;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.col-hd {
padding: 10px 14px 8px;
font-size: 13px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #f0f2f5;
background: #fafafa;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.col-divider { width: 1px; background: #ebeef5; flex-shrink: 0; }
/* 左:规程列 */
.spec-col {
width: 256px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.spec-filter { padding: 8px 10px; border-bottom: 1px solid #f0f2f5; flex-shrink: 0; }
.spec-list { flex: 1; overflow-y: auto; padding: 4px 0; }
.spec-item {
padding: 9px 14px;
cursor: pointer;
border-left: 3px solid transparent;
transition: background .12s;
}
.spec-item:hover { background: #f5f7fa; }
.spec-item--active { background: #ecf5ff !important; border-left-color: #5F7BA0; }
.spec-item__name { font-size: 13px; font-weight: 500; color: #303133; line-height: 1.4; }
.spec-item__code { font-size: 11px; color: #909399; margin-top: 2px; }
/* 右:版本列 */
.version-col {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.spec-name-label { font-size: 13px; font-weight: 600; color: #303133; }
.spec-code-chip {
font-size: 11px; color: #909399;
background: #f0f2f5; padding: 1px 8px; border-radius: 10px;
}
.spec-remark {
font-size: 12px; color: #909399;
padding: 5px 14px; background: #fafbfc;
border-bottom: 1px solid #f0f2f5; flex-shrink: 0;
}
.version-placeholder {
flex: 1; display: flex; align-items: center; justify-content: center;
color: #c0c4cc; font-size: 13px;
}
.version-list {
flex: 1; overflow-y: auto; padding: 10px 12px;
display: flex; flex-direction: column; gap: 8px;
}
/* 版本卡片 */
.version-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px 14px;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s, background .15s;
background: #fff;
}
.version-card:hover:not(.version-card--obsolete) {
border-color: #5F7BA0;
box-shadow: 0 2px 8px rgba(95,123,160,.12);
}
.version-card--selected {
border-color: #5F7BA0 !important;
background: #f0f7ff !important;
box-shadow: 0 0 0 2px rgba(95,123,160,.18) !important;
}
.version-card--obsolete { opacity: .5; cursor: not-allowed; background: #fafafa; }
.vc-body { flex: 1; min-width: 0; }
.vc-row1 { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-bottom: 5px; }
.vc-code { font-size: 14px; font-weight: 600; color: #303133; }
.obsolete-note { font-size: 11px; color: #c0c4cc; margin-left: 4px; }
.vc-row2 { font-size: 11px; color: #c0c4cc; margin-bottom: 3px; }
.vc-remark { font-size: 12px; color: #909399; margin-top: 4px; word-break: break-all; }
.vc-check { font-size: 18px; color: #5F7BA0; flex-shrink: 0; margin-left: 10px; align-self: center; }
/* 底部 */
.bind-footer { display: flex; justify-content: flex-end; gap: 8px; }
.list-empty { padding: 32px 0; text-align: center; font-size: 13px; color: #c0c4cc; }
/* ── 详情抽屉 ── */
.detail-wrap {
padding: 16px 20px 32px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.detail-info-card {
background: #f8f9fb;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 16px;
margin-bottom: 16px;
}
.dic-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.dic-row:last-child { margin-bottom: 0; }
.dic-label {
font-size: 12px;
color: #909399;
flex-shrink: 0;
width: 56px;
text-align: right;
}
.dic-val {
font-size: 13px;
color: #303133;
min-width: 0;
flex: 1;
}
/* ── 分段 Tab ── */
.seg-tabs { margin-top: 0; }
.seg-tab-bar {
display: inline-flex;
background: #f0f2f5;
border-radius: 8px;
padding: 3px;
margin-bottom: 14px;
gap: 2px;
}
.seg-tab-btn {
border: none;
background: transparent;
border-radius: 6px;
padding: 6px 20px;
font-size: 13px;
color: #606266;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background .15s, color .15s, box-shadow .15s;
white-space: nowrap;
outline: none;
}
.seg-tab-btn:hover:not(.is-active) { background: #e4e7ed; color: #303133; }
.seg-tab-btn.is-active {
background: #fff;
color: #303133;
font-weight: 600;
box-shadow: 0 1px 5px rgba(0,0,0,.1);
}
.seg-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: #f56c6c;
color: #fff;
border-radius: 8px;
font-size: 10px;
font-weight: 700;
line-height: 1;
}
.seg-tab-pane {}
.param-group { margin-bottom: 20px; }
.param-group-hd {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
padding: 0 2px;
}
.pg-point { font-size: 13px; font-weight: 600; color: #303133; }
.pg-code { font-size: 11px; color: #909399; }
.val-danger { color: #f56c6c; font-weight: 600; }
.val-ok { color: #67c23a; }
.val-anomaly { color: #e6a23c; font-weight: 600; }
.detail-empty {
padding: 48px 0;
text-align: center;
font-size: 13px;
color: #c0c4cc;
}
.param-group :deep(.row-anomaly td) { background: #fff8f0 !important; }
::v-deep .row-anomaly td { background: #fff8f8 !important; }
::v-deep .el-drawer__header { padding: 16px 20px 12px; font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 0; border-bottom: 1px solid #f0f2f5; }
::v-deep .el-drawer__body { overflow: hidden; }
/* 覆盖 */
::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.is-disabled { opacity: .5; }
::v-deep .el-table .el-table__row:hover td { background: #f5f9ff !important; }
</style>