数据贯通完成,规程重构

This commit is contained in:
2026-04-27 20:37:59 +08:00
parent 5b38ef734a
commit b7161e9541
13 changed files with 1492 additions and 861 deletions

View File

@@ -1,5 +1,6 @@
package com.klp.framework.client;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.klp.framework.config.SqlServerApiProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
@@ -73,7 +74,9 @@ public class SqlServerApiClient {
}
public static class TableSchemaRequest {
@JsonProperty("table_type")
private String tableType;
@JsonProperty("table_name")
private String tableName;
public TableSchemaRequest() {
@@ -102,6 +105,7 @@ public class SqlServerApiClient {
}
public static class ExecuteSqlRequest {
@JsonProperty("table_type")
private String tableType;
private String sql;
private Map<String, Object> params;
@@ -311,10 +315,23 @@ public class SqlServerApiClient {
);
}
public ExecuteSqlResponse queryPlanList() {
public ExecuteSqlResponse queryPlanList(int page, int pageSize) {
int endRow = page * pageSize;
int startRow = endRow - pageSize;
Map<String, Object> params = new java.util.HashMap<>();
params.put("startRow", startRow);
params.put("endRow", endRow);
return executeSql(
"oracle",
"select * from JXPLTCM.PLTCM_PDI_PLAN order by INSDATE desc",
"select * from (select t.*, ROWNUM rn from (select * from JXPLTCM.PLTCM_PDI_PLAN order by INSDATE desc) t where ROWNUM <= :endRow) where rn > :startRow",
params
);
}
public ExecuteSqlResponse queryPlanCount() {
return executeSql(
"oracle",
"select count(*) as total from JXPLTCM.PLTCM_PDI_PLAN",
emptyParams()
);
}

View File

@@ -17,7 +17,7 @@ public class SqlServerApiClientConfig {
public RestTemplate sqlServerApiRestTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(30))
.setReadTimeout(Duration.ofSeconds(60))
.build();
}
}

View File

@@ -32,12 +32,24 @@ public class SqlServerApiBusinessService {
}
/**
* 计划列表:查询所有计划,按时间倒序
* <p>
* 这是后续按 coilId 关联 SEG、实时数据的入口。
* 计划列表分页按时间倒序page 从 1 开始
*/
public PlanListView getPlanList() {
return PlanListView.fromExecuteSqlResponse(client.queryPlanList());
public PlanListView getPlanList(int page, int pageSize) {
return PlanListView.fromExecuteSqlResponse(client.queryPlanList(page, pageSize));
}
/**
* 计划总数。
*/
public long getPlanCount() {
SqlServerApiClient.ExecuteSqlResponse resp = client.queryPlanCount();
List<Map<String, Object>> rows = asRowList(resp);
if (rows.isEmpty()) {
return 0L;
}
Object total = rows.get(0).get("total");
Number n = asNumber(total);
return n == null ? 0L : n.longValue();
}
/**

View File

@@ -1,4 +1,4 @@
package com.klp.web.controller.sqlserver;
package com.klp.framework.sqlserver;
import com.klp.common.core.domain.R;
import com.klp.framework.service.SqlServerApiBusinessService;
@@ -6,8 +6,12 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* sql-server-api 业务查询接口。
* <p>
@@ -21,11 +25,24 @@ public class SqlServerApiController {
private final SqlServerApiBusinessService businessService;
/**
* 计划列表。
* 计划列表(分页)
* page 从 1 开始,默认第 1 页,每页 20 条。
*/
@GetMapping("/plans")
public R<SqlServerApiBusinessService.PlanListView> planList() {
return R.ok(businessService.getPlanList());
public R<SqlServerApiBusinessService.PlanListView> planList(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize) {
return R.ok(businessService.getPlanList(page, pageSize));
}
/**
* 计划总数,用于前端分页器。
*/
@GetMapping("/plans/count")
public R<Map<String, Long>> planCount() {
Map<String, Long> result = new HashMap<>();
result.put("total", businessService.getPlanCount());
return R.ok(result);
}
/**

View File

@@ -7,6 +7,12 @@ klp:
# 开发环境文件存储目录
directory-path: testDirectory
--- # sql-server-api 中间件配置(开发/测试环境)
sql-server-api:
host: 140.143.206.120
port: 15000
base-url: http://${sql-server-api.host}:${sql-server-api.port}
--- # OEE 聚合klp-da多服务地址配置
da:
oee:

View File

@@ -7,6 +7,12 @@ klp:
# 生产环境文件存储目录
directory-path: /home/ubuntu/oa/folder
--- # sql-server-api 中间件配置(生产环境)
sql-server-api:
host: 192.168.0.219
port: 15000
base-url: http://${sql-server-api.host}:${sql-server-api.port}
--- # OEE 聚合klp-da多服务地址配置
da:
oee:

View File

@@ -66,14 +66,6 @@ user:
lockTime: 10
# sql-server-api 中间件配置
sql-server-api:
# 请求地址,请替换为实际 IP
host: 127.0.0.1
# 请求端口,请替换为实际端口
port: 8080
# 基础访问地址(可直接注入使用)
base-url: http://${sql-server-api.host}:${sql-server-api.port}
# Spring配置
spring:
application:

View File

@@ -1,36 +1,50 @@
import axios from 'axios'
import request from '@/utils/request'
export default function createTimingFetch(url) {
const request = axios.create({
baseURL: 'http://' + url,
headers: {
'Content-Type': 'application/json'
},
timeout: 10000
})
request.interceptors.response.use(response => response.data)
return {
getPlanList: () => request({
// 计划列表(分页)
export function getTimingPlanList(page = 1, pageSize = 20) {
return request({
url: '/sql-server-api/plans',
method: 'get'
}),
getPlanDetail: (coilId) => request({
url: `/sql-server-api/plans/${coilId}`,
method: 'get'
}),
getSegByEncoilId: (encoilId) => request({
url: `/sql-server-api/seg/${encoilId}`,
method: 'get'
}),
getSegByExcoilId: (excoilId) => request({
url: `/sql-server-api/seg-by-excoil/${excoilId}`,
method: 'get'
}),
getRealtimeData: (matId) => request({
url: `/sql-server-api/realtime/${matId}`,
method: 'get',
params: { page, pageSize }
})
}
// 计划总数
export function getTimingPlanCount() {
return request({
url: '/sql-server-api/plans/count',
method: 'get'
})
}
// 计划详情
export function getTimingPlanDetail(coilId) {
return request({
url: '/sql-server-api/plans/' + coilId,
method: 'get'
})
}
// 钢卷实际 SEG按入口卷号查询
export function getTimingSegByEncoilId(encoilId) {
return request({
url: '/sql-server-api/seg/' + encoilId,
method: 'get'
})
}
// 钢卷实际 SEG按出口卷号查询
export function getTimingSegByExcoilId(excoilId) {
return request({
url: '/sql-server-api/seg-by-excoil/' + excoilId,
method: 'get'
})
}
// 实时数据Gauge + Shape
export function getTimingRealtimeData(matId) {
return request({
url: '/sql-server-api/realtime/' + matId,
method: 'get'
})
}
}

View File

@@ -1,151 +1,204 @@
<template>
<div class="timing-page acid-page">
<el-card class="page-card" shadow="never">
<div slot="header" class="card-header">
<span>酸轧实绩页</span>
<el-tag type="success" size="mini">Plan + Seg + Quality</el-tag>
<div class="acid-view">
<div class="filter-bar">
<el-input
v-model="queryForm.coilId"
placeholder="热卷号 / 成品卷号"
clearable
size="small"
style="width: 220px"
@keyup.enter.native="handleSearch"
/>
<el-button size="small" type="primary" :loading="loading" @click="handleSearch">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</div>
<el-form :inline="true" :model="queryForm" size="mini" class="query-form">
<el-form-item label="计划号">
<el-input v-model="queryForm.coilId" placeholder="输入 coilId" clearable style="width: 220px;" />
</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>
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never" class="sub-card">
<div slot="header" class="sub-header">计划列表</div>
<el-row :gutter="12" class="main-row">
<!-- 左侧计划列表 -->
<el-col :span="9">
<div class="left-card">
<el-table
ref="planTable"
:data="planRows"
height="560"
size="mini"
highlight-current-row
:height="tableHeight"
@row-click="handlePlanRowClick"
>
<el-table-column prop="COILID" label="COILID" min-width="140" />
<el-table-column prop="HOT_COILID" label="卷号" min-width="140" />
<el-table-column prop="STATUS" label="状态" width="90" />
</el-table>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never" class="sub-card">
<div slot="header" class="sub-header">计划详情</div>
<el-empty v-if="!selectedPlan" description="请选择一条计划" />
<el-descriptions v-else :column="3" border size="mini">
<el-descriptions-item label="COILID">{{ selectedPlan.COILID }}</el-descriptions-item>
<el-descriptions-item label="HOT_COILID">{{ selectedPlan.HOT_COILID }}</el-descriptions-item>
<el-descriptions-item label="STATUS">{{ selectedPlan.STATUS }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_THICK">{{ selectedPlan.ENTRY_THICK }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_WIDTH">{{ selectedPlan.ENTRY_WIDTH }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_WEIGHT">{{ selectedPlan.ENTRY_WEIGHT }}</el-descriptions-item>
<el-descriptions-item label="EXIT_THICK">{{ selectedPlan.EXIT_THICK }}</el-descriptions-item>
<el-descriptions-item label="EXIT_WIDTH">{{ selectedPlan.EXIT_WIDTH }}</el-descriptions-item>
<el-descriptions-item label="EXIT_LENGTH">{{ selectedPlan.EXIT_LENGTH }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="sub-card" style="margin-top: 16px;">
<div slot="header" class="sub-header">SEG 实绩</div>
<el-alert
v-if="!segView"
title="请选择计划后自动加载对应 SEG"
type="info"
:closable="false"
show-icon
/>
<template v-else>
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never">
<div slot="header">SEGNO</div>
<div class="seg-list">
<el-tag
v-for="item in segView.segNo"
:key="item"
size="mini"
class="seg-tag"
>
{{ item }}
</el-tag>
</div>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never">
<div slot="header">属性数组</div>
<el-table :data="seriesTable" size="mini" height="240" border>
<el-table-column prop="key" label="属性" width="180" />
<el-table-column prop="values" label="数组值" min-width="300">
<template slot-scope="scope">
<el-tag v-for="(item, index) in scope.row.values" :key="index" size="mini" style="margin-right: 4px; margin-bottom: 4px;">
{{ item }}
</el-tag>
<el-table-column prop="hot_coilid" label="热卷号" show-overflow-tooltip />
<el-table-column prop="coilid" label="原料卷号" show-overflow-tooltip />
<el-table-column label="状态">
<template slot-scope="{ row }">
<span :class="['status-dot', statusClass(row.status)]" width="70px"/>
<span class="status-text">{{ row.status || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="厚×宽">
<template slot-scope="{ row }">{{ row.entry_thick }}×{{ row.entry_width }}</template>
</el-table-column>
<el-table-column prop="exit_thick" label="出口厚" />
<el-table-column prop="entry_weight" label="重量(t)" />
</el-table>
</el-card>
<div class="pagination-bar">
<el-pagination
small
layout="total, prev, pager, next"
:total="pagination.total"
:page-size="pagination.pageSize"
:current-page="pagination.page"
@current-change="handlePageChange"
/>
</div>
</div>
</el-col>
</el-row>
<!-- 右侧详情 + 图表 -->
<el-col :span="15">
<div v-if="!selectedPlan" class="empty-hint">
<el-empty description="选择左侧计划" :image-size="72" />
</div>
<template v-else>
<div class="detail-grid">
<div v-for="f in planFields" :key="f.key" class="detail-cell">
<span class="cell-label">{{ f.label }}</span>
<span class="cell-value">{{ selectedPlan[f.key] != null ? selectedPlan[f.key] : '—' }}</span>
</div>
</div>
<div class="perf-header">
实轧实绩
<span v-if="hasPerfData" class="perf-count">({{ perfSegCount }} )</span>
<el-tag v-if="perfLoading" size="mini" type="info" style="margin-left:8px">加载中</el-tag>
</div>
<div v-if="hasPerfData" class="charts-wrap">
<div ref="chartSpeed" class="chart-box" />
<div ref="chartMillSpeed" class="chart-box" />
<div ref="chartTension" class="chart-box" />
</div>
<el-empty v-else-if="!perfLoading" description="暂无实绩数据" :image-size="56" style="margin-top: 24px" />
</template>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
import createTimingFetch from '@/api/l2/timing'
import * as echarts from 'echarts'
import {
getTimingPlanList,
getTimingPlanCount,
getTimingPlanDetail,
getTimingSegByEncoilId
} from '@/api/l2/timing'
const PLAN_FIELDS = [
{ key: 'status', label: '状态' },
{ key: 'process_code', label: '工艺编码' },
{ key: 'entry_thick', label: '入口厚度(mm)' },
{ key: 'entry_width', label: '入口宽度(mm)' },
{ key: 'entry_weight', label: '入口重量(t)' },
{ key: 'exit_thick', label: '出口厚度(mm)' },
{ key: 'exit_width', label: '出口宽度(mm)' },
{ key: 'exit_length', label: '出口长度(m)' }
]
const STATUS_CLASS = {
READY: 'status-ready',
ONLINE: 'status-online',
PRODUCING: 'status-producing',
PRODUCT: 'status-product'
}
function makeLine(name, data) {
return { name, type: 'line', smooth: true, symbol: 'none', data }
}
function baseOption(title, xData, series, yName) {
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
tooltip: { trigger: 'axis' },
legend: { top: 4, right: 8, textStyle: { fontSize: 11 } },
grid: { top: 36, bottom: 28, left: 8, right: 8, containLabel: true },
xAxis: {
type: 'category',
data: xData,
name: 'pos(m)',
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 10 }
},
yAxis: { type: 'value', name: yName, nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 } },
series
}
}
export default {
name: 'TimingAcidPage',
props: {
baseURL: { type: String, required: true }
},
data() {
return {
fetchApi: null,
loading: false,
perfLoading: false,
queryForm: { coilId: '' },
planRows: [],
selectedPlan: null,
segView: null,
seriesTable: []
perfSeries: null,
perfSegCount: 0,
planFields: PLAN_FIELDS,
pagination: { page: 1, pageSize: 50, total: 0 },
tableHeight: 'calc(100vh - 210px)'
}
},
computed: {
hasPerfData() {
return this.perfSeries && this.perfSegCount > 0
}
},
created() {
this.fetchApi = createTimingFetch(this.baseURL)
// plain instance property — Vue 2 does NOT proxy underscore-prefixed names
this.chartInstances = []
this.resizeHandler = null
this.loadPlanCount()
this.loadPlanList()
},
beforeDestroy() {
this.disposeCharts()
},
methods: {
statusClass(status) {
return STATUS_CLASS[status] || 'status-default'
},
async loadPlanCount() {
try {
const res = await getTimingPlanCount()
this.pagination.total = res?.data?.total ?? 0
} catch (_) {}
},
async loadPlanList() {
this.loading = true
try {
const res = await this.fetchApi.getPlanList()
const rows = res?.data?.rows || res?.rows || []
this.planRows = rows
const { page, pageSize } = this.pagination
const res = await getTimingPlanList(page, pageSize)
this.planRows = res?.data?.rows || []
} finally {
this.loading = false
}
},
handlePageChange(page) {
this.pagination.page = page
this.loadPlanList()
},
async handleSearch() {
if (!this.queryForm.coilId) {
return this.loadPlanList()
}
if (!this.queryForm.coilId) return this.loadPlanList()
this.loading = true
try {
const res = await this.fetchApi.getPlanDetail(this.queryForm.coilId)
const row = res?.data?.firstRow || res?.firstRow || null
const res = await getTimingPlanDetail(this.queryForm.coilId)
const row = res?.data?.firstRow || null
if (row) {
this.selectedPlan = row
if (row && row.ENCOILID) {
await this.loadSeg(row.ENCOILID)
await this.loadPerf(row)
}
} finally {
this.loading = false
@@ -153,25 +206,86 @@ export default {
},
async handlePlanRowClick(row) {
this.selectedPlan = row
if (row?.COILID) {
const detail = await this.fetchApi.getPlanDetail(row.COILID)
const plan = detail?.data?.firstRow || detail?.firstRow || row
this.selectedPlan = plan
const encoilId = plan.ENCOILID || plan.ENCOILID || plan.COILID
if (encoilId) await this.loadSeg(encoilId)
this.perfSeries = null
this.perfSegCount = 0
this.disposeCharts()
await this.loadPerf(row)
},
async loadPerf(plan) {
const encoilId = plan.encoilid || plan.coilid
if (!encoilId) return
this.perfLoading = true
try {
const res = await getTimingSegByEncoilId(encoilId)
const series = res?.data?.series || null
const rows = res?.data?.rows || []
this.perfSegCount = rows.length
this.perfSeries = series
if (series && rows.length) {
await this.$nextTick()
this.renderCharts(series)
}
} catch (_) {
this.perfSeries = null
this.perfSegCount = 0
} finally {
this.perfLoading = false
}
},
async loadSeg(encoilId) {
const res = await this.fetchApi.getSegByEncoilId(encoilId)
const view = res?.data || res
this.segView = view
this.seriesTable = Object.entries(view?.series || {}).map(([key, values]) => ({ key, values }))
disposeCharts() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler)
this.resizeHandler = null
}
if (this.chartInstances && this.chartInstances.length) {
this.chartInstances.forEach(c => { if (c) c.dispose() })
this.chartInstances = []
}
},
renderCharts(series) {
this.disposeCharts()
const pick = key => (series[key] || []).map(v => v == null ? null : Number(v).toFixed(2))
const xData = (series.startpos || []).map(v => v == null ? '' : Number(v).toFixed(1))
const c1 = echarts.init(this.$refs.chartSpeed)
c1.setOption(baseOption(
'速度趋势 (m/min)', xData,
[
makeLine('轧制速度 plspeed', pick('plspeed')),
makeLine('剪切速度 trimspeed', pick('trimspeed'))
],
'm/min'
))
const c2 = echarts.init(this.$refs.chartMillSpeed)
c2.setOption(baseOption(
'轧机出口速度 (m/min)', xData,
[makeLine('millexitspeed', pick('millexitspeed'))],
'm/min'
))
const c3 = echarts.init(this.$refs.chartTension)
c3.setOption(baseOption(
'张力趋势 (N)', xData,
[
makeLine('出口张力 pltens', pick('pltens')),
makeLine('入口张力 enltens', pick('enltens')),
makeLine('cxltens', pick('cxltens'))
],
'N'
))
this.chartInstances = [c1, c2, c3]
this.resizeHandler = () => this.chartInstances.forEach(c => { if (c) c.resize() })
window.addEventListener('resize', this.resizeHandler)
},
handleReset() {
this.queryForm.coilId = ''
this.selectedPlan = null
this.segView = null
this.seriesTable = []
this.perfSeries = null
this.perfSegCount = 0
this.disposeCharts()
this.pagination.page = 1
this.loadPlanList()
}
}
@@ -179,11 +293,115 @@ export default {
</script>
<style scoped>
.timing-page { padding: 16px; }
.page-card { border-radius: 12px; }
.card-header, .sub-header { display: flex; align-items: center; justify-content: space-between; font-weight: 600; }
.sub-card { border-radius: 10px; }
.query-form { margin-bottom: 12px; }
.seg-list { display: flex; flex-wrap: wrap; gap: 6px; }
.seg-tag { margin-right: 0; }
.acid-view {
padding: 12px 16px;
}
.filter-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.main-row {
margin: 0 !important;
}
.left-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.empty-hint {
display: flex;
justify-content: center;
padding-top: 80px;
}
.pagination-bar {
display: flex;
justify-content: flex-end;
padding: 6px 8px;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-top: 1px solid #ebeef5;
border-left: 1px solid #ebeef5;
margin-bottom: 16px;
}
.detail-cell {
display: flex;
flex-direction: column;
padding: 8px 10px;
border-right: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
}
.cell-label {
font-size: 11px;
color: #909399;
margin-bottom: 3px;
}
.cell-value {
font-size: 13px;
color: #303133;
font-weight: 500;
}
.perf-header {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.perf-count {
font-weight: normal;
color: #909399;
font-size: 12px;
}
.charts-wrap {
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
max-height: calc(100vh - 310px);
}
.chart-box {
width: 100%;
height: 200px;
}
/* 状态指示点 */
.status-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
vertical-align: middle;
}
.status-text {
font-size: 8px;
vertical-align: middle;
}
.status-ready { background: #909399; }
.status-online { background: #67c23a; }
.status-producing { background: #e6a23c; }
.status-product { background: #409eff; }
.status-default { background: #c0c4cc; }
</style>

View File

@@ -55,16 +55,12 @@
</template>
<script>
import createTimingFetch from '@/api/l2/timing'
import { getTimingRealtimeData } from '@/api/l2/timing'
export default {
name: 'TimingRealtimePage',
props: {
baseURL: { type: String, required: true }
},
data() {
return {
fetchApi: null,
loading: false,
queryForm: { matId: '' },
realtimeData: null,
@@ -72,9 +68,6 @@ export default {
shapeRows: []
}
},
created() {
this.fetchApi = createTimingFetch(this.baseURL)
},
methods: {
async handleQuery() {
if (!this.queryForm.matId) {
@@ -83,10 +76,10 @@ export default {
}
this.loading = true
try {
const res = await this.fetchApi.getRealtimeData(this.queryForm.matId)
this.realtimeData = res?.data || res
this.gaugeRows = this.realtimeData?.gauge?.result || []
this.shapeRows = this.realtimeData?.shape?.result || []
const res = await getTimingRealtimeData(this.queryForm.matId)
this.realtimeData = res && res.data ? res.data : res
this.gaugeRows = this.realtimeData && this.realtimeData.gauge ? this.realtimeData.gauge.result || [] : []
this.shapeRows = this.realtimeData && this.realtimeData.shape ? this.realtimeData.shape.result || [] : []
} finally {
this.loading = false
}

View File

@@ -1,87 +1,80 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="规程编号" prop="specCode">
<el-input v-model="queryParams.specCode" placeholder="规程编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="规程名称" prop="specName">
<el-input v-model="queryParams.specName" placeholder="规程名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="规程类型" prop="specType">
<el-select v-model="queryParams.specType" placeholder="全部" clearable>
<el-option v-for="item in specTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产线" prop="lineId">
<el-select v-model="queryParams.lineId" placeholder="全部" clearable filterable>
<el-option
<div class="spec-page">
<!-- 规程类型 tabs -->
<div class="type-tab-bar">
<span
v-for="t in specTypeTab"
:key="t.value"
:class="['type-tab', { active: activeSpecType === t.value }]"
@click="switchSpecType(t.value)"
>{{ t.label }}</span>
</div>
<!-- 产线 tabs -->
<div class="line-tab-bar">
<span
:class="['line-tab', { active: activeLineId === '' }]"
@click="switchLine('')"
>全部</span>
<span
v-for="line in lineOptions"
:key="line.lineId"
:label="formatLineOption(line)"
:value="line.lineId"
:class="['line-tab', { active: activeLineId === line.lineId }]"
@click="switchLine(line.lineId)"
>{{ line.lineName }}</span>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAdd">新增</el-button>
<el-button size="mini" icon="el-icon-edit" :disabled="single" @click="handleUpdate()">修改</el-button>
<el-button size="mini" icon="el-icon-delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
</div>
<div class="toolbar-right">
<el-input
v-model="queryParams.specName"
size="small"
placeholder="规程名称"
clearable
style="width:180px; margin-right:8px"
@keyup.enter.native="handleQuery"
/>
</el-select>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<el-input v-model="queryParams.productType" placeholder="产品类型" clearable />
</el-form-item>
<el-form-item label="是否启用" prop="isEnabled">
<el-select v-model="queryParams.isEnabled" placeholder="全部" clearable>
<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" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-button size="mini" type="primary" @click="handleQuery">查询</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</div>
</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<KLPTable v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="规程编号" align="center" prop="specCode" min-width="120" show-overflow-tooltip />
<el-table-column label="规程名称" align="center" prop="specName" min-width="140" show-overflow-tooltip />
<el-table-column label="规程类型" align="center" prop="specType" width="100">
<template slot-scope="scope">
<span>{{ formatSpecType(scope.row.specType) }}</span>
<!-- 列表 -->
<el-table
v-loading="loading"
:data="dataList"
size="small"
highlight-current-row
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" />
<el-table-column label="规程编号" prop="specCode" show-overflow-tooltip />
<el-table-column label="规程名称" prop="specName" show-overflow-tooltip />
<el-table-column label="产品类型" prop="productType" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" />
<el-table-column label="启用" align="center">
<template slot-scope="{ row }">
<el-switch
:value="row.isEnabled === 1"
active-color="#5F7BA0"
@change="toggleEnabled(row, $event)"
/>
</template>
</el-table-column>
<el-table-column label="产线" align="center" min-width="160" show-overflow-tooltip>
<template slot-scope="scope">
{{ getLineName(scope.row.lineId) }}
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="goVersionManage(row)">版本与方案</el-button>
<el-button type="text" size="mini" @click="handleUpdate(row)">修改</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
<el-table-column label="产品类型" align="center" prop="productType" min-width="100" show-overflow-tooltip />
<el-table-column label="是否启用" align="center" prop="isEnabled" width="90">
<template slot-scope="scope">
<dict-tag :options="dict.type.common_swicth" :value="scope.row.isEnabled" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-document" @click="goVersionManage(scope.row)">版本与方案</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
</el-table>
<pagination
v-show="total > 0"
@@ -91,45 +84,43 @@
@pagination="getList"
/>
<el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<!-- 新增/修改 -->
<el-dialog :title="dialogTitle" :visible.sync="open" width="520px" append-to-body @close="reset">
<el-form ref="form" :model="form" :rules="rules" label-width="88px" size="small">
<el-form-item label="规程编号" prop="specCode">
<el-input v-model="form.specCode" placeholder="唯一编号" maxlength="64" show-word-limit />
</el-form-item>
<el-form-item label="规程名称" prop="specName">
<el-input v-model="form.specName" placeholder="规程名称" maxlength="200" show-word-limit />
<el-input v-model="form.specName" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="规程类型" prop="specType">
<el-select v-model="form.specType" placeholder="请选择">
<el-option v-for="item in specTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-select v-model="form.specType" style="width:100%">
<el-option v-for="t in specTypeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="产线" prop="lineId">
<el-select v-model="form.lineId" placeholder="请选择产线" filterable style="width: 100%">
<el-select v-model="form.lineId" filterable placeholder="请选择" style="width:100%">
<el-option
v-for="line in lineOptions"
:key="line.lineId"
:label="formatLineOption(line)"
:label="line.lineCode ? line.lineName + '' + line.lineCode + '' : line.lineName"
:value="line.lineId"
/>
</el-select>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<el-input v-model="form.productType" placeholder="可选" maxlength="100" />
<el-input v-model="form.productType" maxlength="100" />
</el-form-item>
<el-form-item label="是否启用" prop="isEnabled">
<el-radio-group v-model="form.isEnabled">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="备注" maxlength="500" show-word-limit rows="2" />
<el-input v-model="form.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
<div slot="footer">
<el-button size="small" @click="open = false">取消</el-button>
<el-button size="small" type="primary" :loading="btnLoading" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
@@ -139,184 +130,255 @@
import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec'
import { listProductionLine } from '@/api/wms/productionLine'
const SPEC_TYPES = [
{ label: '工艺规程', value: 'PROCESS' },
{ label: '标准', value: 'STANDARD' }
]
export default {
name: 'ProcessSpec',
dicts: ['common_swicth'],
data() {
return {
buttonLoading: false,
loading: true,
loading: false,
btnLoading: false,
total: 0,
dataList: [],
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
dataList: [],
title: '',
open: false,
dialogTitle: '',
lineOptions: [],
specTypeOptions: [
{ label: '工艺规程', value: 'PROCESS' },
{ label: '标准', value: 'STANDARD' }
],
specTypeTab: [{ label: '全部', value: '' }, ...SPEC_TYPES],
specTypeOptions: SPEC_TYPES,
activeSpecType: '',
activeLineId: '',
queryParams: {
pageNum: 1,
pageSize: 20,
specCode: undefined,
specName: undefined,
specType: undefined,
lineId: undefined,
productType: undefined,
isEnabled: undefined
lineId: undefined
},
form: {},
rules: {
specCode: [{ required: true, message: '规程编号不能为空', trigger: 'blur' }],
specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }],
specType: [{ required: true, message: '规程类型不能为空', trigger: 'change' }],
lineId: [{ required: true, message: '产线不能为空', trigger: 'change' }]
specType: [{ required: true, message: '请选择规程类型', trigger: 'change' }],
lineId: [{ required: true, message: '请选择产线', trigger: 'change' }]
}
}
},
created() {
this.loadLineOptions()
listProductionLine({ pageNum: 1, pageSize: 500 }).then(res => {
this.lineOptions = res.rows || []
})
this.getList()
},
methods: {
formatSpecType(value) {
const hit = this.specTypeOptions.find((x) => x.value === value)
return hit ? hit.label : value
},
formatLineOption(line) {
if (!line) {
return ''
}
return line.lineCode ? `${line.lineName}${line.lineCode}` : line.lineName
},
getLineName(lineId) {
if (lineId == null) {
return ''
}
const hit = this.lineOptions.find((o) => o.lineId === lineId)
return hit ? this.formatLineOption(hit) : lineId
},
loadLineOptions() {
listProductionLine({ pageNum: 1, pageSize: 500 }).then((res) => {
this.lineOptions = res.rows || []
}).catch((err) => {
console.error('加载产线列表失败', err)
})
},
getList() {
this.loading = true
listProcessSpec(this.queryParams).then((response) => {
this.dataList = response.rows
this.total = response.total
this.loading = false
}).catch((err) => {
console.error('加载规程列表失败', err)
this.loading = false
})
listProcessSpec(this.queryParams).then(res => {
this.dataList = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
cancel() {
this.open = false
this.reset()
switchSpecType(val) {
this.activeSpecType = val
this.queryParams.specType = val || undefined
this.queryParams.pageNum = 1
this.getList()
},
reset() {
this.form = {
specId: undefined,
specCode: undefined,
specName: undefined,
specType: 'PROCESS',
lineId: undefined,
productType: undefined,
isEnabled: 1,
remark: undefined
}
this.resetForm('form')
switchLine(lineId) {
this.activeLineId = lineId
this.queryParams.lineId = lineId || undefined
this.queryParams.pageNum = 1
this.getList()
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.specName = undefined
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.specId)
this.single = selection.length !== 1
this.multiple = !selection.length
handleSelectionChange(sel) {
this.ids = sel.map(r => r.specId)
this.single = sel.length !== 1
this.multiple = !sel.length
},
reset() {
this.form = { specId: undefined, specCode: undefined, specName: undefined, specType: 'PROCESS', lineId: undefined, productType: undefined, isEnabled: 1, remark: undefined }
this.$refs.form && this.$refs.form.clearValidate()
},
handleAdd() {
this.reset()
this.dialogTitle = '新增规程'
this.open = true
this.title = '添加规程'
},
handleUpdate(row) {
this.loading = true
this.reset()
const specId = row.specId || this.ids
getProcessSpec(specId).then((response) => {
this.loading = false
this.form = response.data
const specId = row ? row.specId : this.ids[0]
getProcessSpec(specId).then(res => {
this.form = res.data || {}
this.dialogTitle = '修改规程'
this.open = true
this.title = '修改规程'
}).catch((err) => {
console.error('获取规程详情失败', err)
this.loading = false
})
},
submitForm() {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
this.buttonLoading = true
const req = this.form.specId != null ? updateProcessSpec(this.form) : addProcessSpec(this.form)
this.$refs.form.validate(valid => {
if (!valid) return
this.btnLoading = true
const req = this.form.specId ? updateProcessSpec(this.form) : addProcessSpec(this.form)
req.then(() => {
this.$modal.msgSuccess(this.form.specId != null ? '修改成功' : '新增成功')
this.$modal.msgSuccess('保存成功')
this.open = false
this.getList()
}).catch((err) => {
console.error('保存规程失败', err)
}).finally(() => {
this.buttonLoading = false
})
}).finally(() => { this.btnLoading = false })
})
},
handleDelete(row) {
const specIds = row.specId || this.ids
this.$modal.confirm('是否确认删除选中的规程数据').then(() => {
const ids = row ? row.specId : this.ids
this.$modal.confirm('确认删除所选规程').then(() => {
this.loading = true
return delProcessSpec(specIds)
return delProcessSpec(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {}).finally(() => {
this.loading = false
})
this.getList()
}).catch(() => {}).finally(() => { this.loading = false })
},
handleExport() {
this.download('wms/processSpec/export', {
...this.queryParams
}, `processSpec_${new Date().getTime()}.xlsx`)
toggleEnabled(row, val) {
const updated = { ...row, isEnabled: val ? 1 : 0 }
updateProcessSpec(updated).then(() => {
row.isEnabled = updated.isEnabled
}).catch(() => {})
},
goVersionManage(row) {
const specId = row.specId
if (specId == null || specId === '') {
this.$modal.msgWarning('无法获取规程ID请刷新列表后重试')
return
}
// 固定落在「…/processSpec/version」避免列表为 …/processSpec/list 时拼成 …/list/version 导致路由不匹配、query 丢失
const pathCurrent = this.$route.path.replace(/\/$/, '')
const m = pathCurrent.match(/^(.*\/processSpec)(?:\/.*)?$/)
const base = m ? m[1] : pathCurrent
this.$router.push({
path: `${base}/version`,
query: { specId: String(specId) }
})
this.$router.push({ path: `${base}/version`, query: { specId: String(row.specId) } })
}
}
}
</script>
<style scoped>
.spec-page {
padding: 16px 20px;
min-height: 100%;
}
/* ── 双色主题:默认=白底灰边,激活/主操作=深藏青 #5F7BA0 ── */
.type-tab-bar {
display: flex;
gap: 0;
margin-bottom: 10px;
width: fit-content;
border-radius: 4px;
overflow: hidden;
border: 1px solid #dcdfe6;
}
.type-tab {
padding: 5px 14px;
font-size: 12px;
cursor: pointer;
color: #606266;
background: #fff;
border: none;
border-right: 1px solid #dcdfe6;
transition: color .15s, background .15s;
user-select: none;
line-height: 1;
}
.type-tab:last-child { border-right: none; }
.type-tab:hover { color: #5F7BA0; }
.type-tab.active {
color: #fff;
background: #5F7BA0;
}
.line-tab-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
padding: 10px 0;
}
.line-tab {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
color: #606266;
background: #fff;
border: 1px solid #dcdfe6;
transition: color .15s, background .15s, border-color .15s;
user-select: none;
line-height: 1;
}
.line-tab:hover { color: #5F7BA0; border-color: #5F7BA0; }
.line-tab.active {
color: #fff;
background: #5F7BA0;
border-color: #5F7BA0;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 6px;
}
.el-table {
border-radius: 4px;
overflow: hidden;
}
/* el-button主操作类 → 深藏青;默认类 → 白底灰边 */
::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):not(.el-button--info) {
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):not(.el-button--info):hover {
color: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger):not(.el-button--info).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

@@ -0,0 +1,533 @@
<template>
<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>
<span class="page-title">方案详情</span>
<span v-if="versionCode" class="version-badge">版本 {{ versionCode }}</span>
</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>
<div class="main-layout">
<!-- 左侧段分组 -->
<div class="left-tree">
<div
:class="['tree-item', { active: activeSegment === '' }]"
@click="activeSegment = ''"
>全部</div>
<div
v-for="seg in segmentOptions"
:key="seg.value"
:class="['tree-item', { active: activeSegment === seg.value }]"
@click="activeSegment = seg.value"
>
<span class="tree-arrow"></span> {{ seg.label }}
</div>
</div>
<!-- 右侧内容 -->
<div class="right-content">
<!-- 搜索栏 -->
<div class="search-bar">
<span class="search-label">点位名称</span>
<el-input
v-model="filterName"
placeholder="请输入"
size="small"
style="width:200px"
clearable
@keyup.enter.native="applyFilter"
/>
<el-button size="mini" type="primary" @click="applyFilter">查询</el-button>
<el-button size="mini" @click="resetFilter">重置</el-button>
<el-button
size="mini"
type="primary"
icon="el-icon-plus"
style="margin-left:auto"
@click="openPlanDialog()"
>新建方案点位</el-button>
</div>
<!-- 方案点位表 -->
<el-table
v-loading="planLoading"
:data="filteredPlans"
size="small"
highlight-current-row
@current-change="onPlanSelect"
>
<el-table-column label="序号" type="index" align="center" />
<el-table-column label="父级名称" show-overflow-tooltip>
<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="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openPlanDialog(row)">编辑</el-button>
<el-button type="text" size="mini" @click.stop="openParamDialog(row)">参数</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removePlan(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 方案参数面板 -->
<template v-if="selectedPlan">
<div class="param-header">
<span>{{ selectedPlan.pointName || selectedPlan.pointCode }} 参数</span>
<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="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="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="openParamDialog(null, row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="removeParam(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
</div>
</div>
<!-- 方案点位 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">
<el-form-item label="段类型" prop="segmentType">
<el-select v-model="planForm.segmentType" style="width:100%">
<el-option v-for="s in segmentOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="段名称" prop="segmentName">
<el-input v-model="planForm.segmentName" maxlength="100" />
</el-form-item>
<el-form-item label="点位名称" prop="pointName">
<el-input v-model="planForm.pointName" maxlength="200" />
</el-form-item>
<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>
<el-form-item label="L1设定值ID" prop="l1SetValueId">
<el-input v-model="planForm.l1SetValueId" maxlength="64" />
</el-form-item>
<el-form-item label="设定值" prop="targetValue">
<el-input v-model="planForm.targetValue" />
</el-form-item>
<el-form-item label="下限" prop="lowerLimit">
<el-input v-model="planForm.lowerLimit" />
</el-form-item>
<el-form-item label="上限" prop="upperLimit">
<el-input v-model="planForm.upperLimit" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="planForm.sortOrder" :min="0" controls-position="right" style="width:100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="planForm.remark" type="textarea" rows="2" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="planOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="planSubmitLoading" @click="submitPlan">确定</el-button>
</div>
</el-dialog>
<!-- 方案参数 dialog -->
<el-dialog :title="paramTitle" :visible.sync="paramOpen" width="480px" append-to-body @close="paramForm = {}">
<el-form ref="paramFormRef" :model="paramForm" :rules="paramRules" label-width="90px" size="small">
<el-form-item label="参数编码" prop="paramCode">
<el-input v-model="paramForm.paramCode" maxlength="64" />
</el-form-item>
<el-form-item label="参数名称" prop="paramName">
<el-input v-model="paramForm.paramName" maxlength="200" />
</el-form-item>
<el-form-item label="设定值">
<el-input v-model="paramForm.targetValue" />
</el-form-item>
<el-form-item label="下限">
<el-input v-model="paramForm.lowerLimit" />
</el-form-item>
<el-form-item label="上限">
<el-input v-model="paramForm.upperLimit" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="paramForm.unit" maxlength="32" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="paramForm.remark" type="textarea" rows="2" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="paramOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="paramSubmitLoading" @click="submitParam">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
const SEGMENTS = [
{ label: '入口段', value: 'INLET' },
{ label: '工艺段', value: 'PROCESS' },
{ label: '出口段', value: 'OUTLET' }
]
export default {
name: 'ProcessSpecPlanSpec',
data() {
return {
pageLoading: false,
versionId: undefined,
versionCode: '',
specId: undefined,
configMode: 'configurable',
activeSegment: '',
filterName: '',
appliedFilterName: '',
segmentOptions: SEGMENTS,
planList: [],
planLoading: false,
selectedPlan: null,
paramList: [],
paramLoading: false,
planOpen: false,
planTitle: '',
planSubmitLoading: false,
planForm: {},
planRules: {
segmentType: [{ required: true, message: '请选择段类型', trigger: 'change' }],
pointName: [{ required: true, message: '点位名称不能为空', trigger: 'blur' }],
pointCode: [{ required: true, message: '点位编码不能为空', trigger: 'blur' }]
},
paramOpen: false,
paramTitle: '',
paramSubmitLoading: false,
paramForm: {},
paramRules: {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
}
}
},
computed: {
filteredPlans() {
return this.planList.filter(p => {
const segOk = !this.activeSegment || p.segmentType === this.activeSegment
const nameOk = !this.appliedFilterName || (p.pointName || '').includes(this.appliedFilterName)
return segOk && nameOk
})
}
},
watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } }
},
methods: {
syncFromRoute() {
const q = this.$route.query
this.versionId = q.versionId || undefined
this.versionCode = q.versionCode || ''
this.specId = q.specId || undefined
if (this.versionId) this.loadPlans()
},
goBack() { this.$router.go(-1) },
segLabel(val) {
const hit = SEGMENTS.find(s => s.value === val)
return hit ? hit.label : val || ''
},
loadPlans() {
this.planLoading = true
this.selectedPlan = null
this.paramList = []
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
this.planList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.planLoading = false })
},
loadParams(planId) {
this.paramLoading = true
listProcessPlanParam({ planId, pageNum: 1, pageSize: 500 }).then(res => {
this.paramList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.paramLoading = false })
},
onPlanSelect(row) {
if (!row) return
this.selectedPlan = row
this.loadParams(row.planId)
},
applyFilter() { this.appliedFilterName = this.filterName },
resetFilter() { this.filterName = ''; this.appliedFilterName = '' },
openPlanDialog(row) {
this.planForm = row
? { ...row }
: {
versionId: this.versionId,
segmentType: 'PROCESS',
segmentName: undefined,
pointName: undefined,
pointCode: undefined,
actualValueId: undefined,
l1SetValueId: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
sortOrder: 0,
remark: undefined
}
this.planTitle = row ? '编辑方案点位' : '新建方案点位'
this.planOpen = true
this.$nextTick(() => this.$refs.planFormRef && this.$refs.planFormRef.clearValidate())
},
submitPlan() {
this.$refs.planFormRef.validate(ok => {
if (!ok) return
this.planSubmitLoading = true
const req = this.planForm.planId ? updateProcessPlan(this.planForm) : addProcessPlan(this.planForm)
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.planOpen = false
this.loadPlans()
}).catch(e => console.error(e)).finally(() => { this.planSubmitLoading = false })
})
},
removePlan(row) {
this.$modal.confirm('确认删除该方案点位?').then(() => {
return delProcessPlan(row.planId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.selectedPlan && this.selectedPlan.planId === row.planId) {
this.selectedPlan = null
this.paramList = []
}
this.loadPlans()
}).catch(() => {})
},
openParamDialog(planRow, paramRow) {
const targetPlan = planRow || this.selectedPlan
if (!targetPlan) { this.$message.warning('请先选择一个方案点位'); return }
if (!this.selectedPlan || this.selectedPlan.planId !== targetPlan.planId) {
this.selectedPlan = targetPlan
this.loadParams(targetPlan.planId)
}
this.paramForm = paramRow
? { ...paramRow }
: {
planId: targetPlan.planId,
paramCode: undefined,
paramName: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
unit: undefined,
remark: undefined
}
this.paramTitle = paramRow ? '编辑方案参数' : '新建方案参数'
this.paramOpen = true
this.$nextTick(() => this.$refs.paramFormRef && this.$refs.paramFormRef.clearValidate())
},
submitParam() {
this.$refs.paramFormRef.validate(ok => {
if (!ok) return
this.paramSubmitLoading = true
const req = this.paramForm.paramId ? updateProcessPlanParam(this.paramForm) : addProcessPlanParam(this.paramForm)
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.paramOpen = false
this.loadParams(this.selectedPlan.planId)
}).catch(e => console.error(e)).finally(() => { this.paramSubmitLoading = false })
})
},
removeParam(row) {
this.$modal.confirm('确认删除该参数?').then(() => {
return delProcessPlanParam(row.paramId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadParams(this.selectedPlan.planId)
}).catch(() => {})
}
}
}
</script>
<style scoped>
.plan-spec-page {
padding: 16px 20px;
min-height: 100%;
}
.page-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.page-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.version-badge {
font-size: 12px;
color: #909399;
background: #f0f2f5;
padding: 2px 8px;
border-radius: 10px;
}
.config-tabs {
display: flex;
gap: 0;
margin-bottom: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
width: fit-content;
}
.config-tab {
padding: 5px 20px;
font-size: 12px;
cursor: pointer;
color: #606266;
background: #fff;
border: none;
border-right: 1px solid #dcdfe6;
transition: color .15s, background .15s;
user-select: none;
line-height: 1;
}
.config-tab:last-child { border-right: none; }
.config-tab:hover { color: #5F7BA0; }
.config-tab.active {
color: #fff;
background: #5F7BA0;
}
.main-layout {
display: flex;
gap: 0;
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
overflow: hidden;
}
.left-tree {
width: 120px;
flex-shrink: 0;
border-right: 1px solid #ebeef5;
padding: 8px 0;
}
.tree-item {
padding: 9px 16px;
font-size: 13px;
cursor: pointer;
color: #606266;
transition: all .15s;
user-select: none;
}
.tree-item:hover { background: #f5f7fa; }
.tree-item.active {
background: #5F7BA0;
color: #fff;
font-weight: 500;
}
.tree-item.active .tree-arrow { color: rgba(255,255,255,.6); }
.tree-arrow {
margin-right: 4px;
color: #c0c4cc;
}
.right-content {
flex: 1;
min-width: 0;
padding: 12px;
}
.search-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.search-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.param-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 8px;
margin-top: 12px;
border-top: 1px solid #ebeef5;
font-size: 13px;
font-weight: 600;
color: #606266;
}
.el-table { border-radius: 0; }
::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; }
</style>

View File

@@ -1,192 +1,97 @@
<template>
<div class="app-container" v-loading="pageLoading">
<el-page-header v-if="specInfo.specId" @back="goBack" :content="'规程版本与方案 — ' + (specInfo.specName || '') + '' + (specInfo.specCode || '') + ''" />
<el-card v-else shadow="never" class="mb8">
<div slot="header">请选择规程</div>
<p class="text-muted mb8" style="color: #909399; font-size: 13px">规程管理列表进入时会自动带上规程从菜单直接进入时请先选择规程</p>
<el-form inline size="small" @submit.native.prevent>
<el-form-item label="规程">
<div class="ver-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
<span class="page-title" v-if="specInfo.specName">
{{ specInfo.specName }}
<span class="spec-code">{{ specInfo.specCode }}</span>
</span>
<el-button
v-if="specInfo.specId"
type="primary"
size="mini"
icon="el-icon-plus"
style="margin-left:auto"
@click="openVersionDialog()"
>新建版本</el-button>
</div>
<!-- specId 时选择规程 -->
<div v-if="!specInfo.specId" class="pick-card">
<p class="pick-hint">规程管理列表进入会自动带上规程也可在下方手动选择</p>
<el-select
v-model="specPickerId"
filterable
clearable
placeholder="请选择规程"
style="min-width: 280px"
filterable clearable
placeholder="选择规程"
style="width:300px; margin-right:8px"
:loading="specPickerLoading"
>
<el-option v-for="s in specPickerOptions" :key="s.specId" :label="(s.specCode || '') + ' — ' + (s.specName || '')" :value="String(s.specId)" />
<el-option
v-for="s in specPickerOptions"
:key="s.specId"
:label="(s.specCode || '') + ' — ' + (s.specName || '')"
:value="String(s.specId)"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!specPickerId" @click="applySpecPicker">进入维护</el-button>
</el-form-item>
</el-form>
</el-card>
<el-button type="primary" size="small" :disabled="!specPickerId" @click="applySpecPicker">进入维护</el-button>
</div>
<template v-if="specInfo.specId">
<el-card shadow="never" class="mb8">
<div slot="header" class="clearfix">
<span>规程版本</span>
<el-button style="float: right; padding: 3px 10px" type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</el-button>
</div>
<KLPTable
<!-- 版本列表 -->
<div class="section-title">规程版本</div>
<el-table
:data="versionList"
size="small"
highlight-current-row
@row-click="onVersionRowClick"
>
<el-table-column label="版本号" prop="versionCode" min-width="120" align="center" />
<el-table-column label="是否生效" prop="isActive" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isActive === 1" type="success" size="mini">生效</el-tag>
<span v-else></span>
<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="状态" prop="status" width="120" align="center" />
<el-table-column label="创建时间" prop="createTime" width="170" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click.stop="activateVersion(scope.row)">设为生效</el-button>
<el-button type="text" size="mini" @click.stop="openVersionDialog(scope.row)">编辑</el-button>
<el-button type="text" size="mini" @click.stop="removeVersion(scope.row)">删除</el-button>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="goPlanSpec(row)">方案点位</el-button>
<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>
</KLPTable>
</el-card>
</el-table>
<el-card shadow="never" v-if="selectedVersion">
<div slot="header" class="clearfix">
<span>方案点位版本 {{ selectedVersion.versionCode }}</span>
<el-button style="float: right; padding: 3px 10px" type="primary" size="mini" icon="el-icon-plus" @click="openPlanDialog()">新建方案点位</el-button>
</div>
<KLPTable
v-loading="planLoading"
:data="planList"
highlight-current-row
@row-click="onPlanRowClick"
>
<el-table-column label="段类型" prop="segmentType" width="110" align="center" />
<el-table-column label="段名称" prop="segmentName" min-width="100" show-overflow-tooltip align="center" />
<el-table-column label="点位名称" prop="pointName" min-width="120" show-overflow-tooltip align="center" />
<el-table-column label="点位编码" prop="pointCode" min-width="120" align="center" />
<el-table-column label="排序" prop="sortOrder" width="80" align="center" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click.stop="openPlanDialog(scope.row)">编辑</el-button>
<el-button type="text" size="mini" @click.stop="removePlan(scope.row)">删除</el-button>
<el-button type="text" size="mini" @click.stop="onPlanRowClick(scope.row)">维护参数</el-button>
</template>
</el-table-column>
</KLPTable>
</el-card>
<el-card shadow="never" v-if="selectedVersion && selectedPlan" class="mb8">
<div slot="header" class="clearfix">
<span>方案参数{{ selectedPlan.pointName || selectedPlan.pointCode }}</span>
<el-button style="float: right; padding: 3px 10px" type="primary" size="mini" icon="el-icon-plus" @click="openParamDialog()">新建参数</el-button>
</div>
<KLPTable v-loading="paramLoading" :data="paramList">
<el-table-column label="参数编码" prop="paramCode" min-width="100" align="center" />
<el-table-column label="参数名称" prop="paramName" min-width="120" show-overflow-tooltip align="center" />
<el-table-column label="设定值" prop="targetValue" width="100" align="center" />
<el-table-column label="下限" prop="lowerLimit" width="90" align="center" />
<el-table-column label="上限" prop="upperLimit" width="90" align="center" />
<el-table-column label="单位" prop="unit" width="80" align="center" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openParamDialog(scope.row)">编辑</el-button>
<el-button type="text" size="mini" @click="removeParam(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
</el-card>
<el-card v-if="specInfo.specId && !selectedVersion" shadow="never" class="mb8">
<el-empty description="请在上方选择一个规程版本以维护方案点位" />
</el-card>
<el-empty v-if="!versionList.length && !pageLoading" description="暂无版本,请新建" style="padding:40px 0" />
</template>
<!-- 版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="520px" append-to-body @close="versionForm = {}">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="100px">
<!-- 新建/编辑版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="480px" 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" maxlength="64" placeholder="如 V1.0" />
<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" placeholder="请选择" style="width: 100%">
<el-select v-model="versionForm.status" style="width:100%">
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
</el-select>
</el-form-item>
<el-form-item label="保存后生效" prop="isActive">
<el-form-item label="保存后生效">
<el-switch v-model="versionForm.isActive" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-form-item label="备注">
<el-input v-model="versionForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="versionSubmitLoading" type="primary" @click="submitVersion"> </el-button>
<el-button @click="versionOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 方案点位 -->
<el-dialog :title="planTitle" :visible.sync="planOpen" width="560px" append-to-body @close="planForm = {}">
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="100px">
<el-form-item label="段类型" prop="segmentType">
<el-select v-model="planForm.segmentType" placeholder="请选择" style="width: 100%">
<el-option v-for="s in segmentOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="段名称" prop="segmentName">
<el-input v-model="planForm.segmentName" maxlength="100" />
</el-form-item>
<el-form-item label="点位名称" prop="pointName">
<el-input v-model="planForm.pointName" maxlength="200" />
</el-form-item>
<el-form-item label="点位编码" prop="pointCode">
<el-input v-model="planForm.pointCode" maxlength="64" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="planForm.sortOrder" :min="0" :max="999999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="planForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="planSubmitLoading" type="primary" @click="submitPlan"> </el-button>
<el-button @click="planOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 方案参数 -->
<el-dialog :title="paramTitle" :visible.sync="paramOpen" width="560px" append-to-body @close="paramForm = {}">
<el-form ref="paramFormRef" :model="paramForm" :rules="paramRules" label-width="100px">
<el-form-item label="参数编码" prop="paramCode">
<el-input v-model="paramForm.paramCode" maxlength="64" />
</el-form-item>
<el-form-item label="参数名称" prop="paramName">
<el-input v-model="paramForm.paramName" maxlength="200" />
</el-form-item>
<el-form-item label="设定值" prop="targetValue">
<el-input v-model="paramForm.targetValue" placeholder="数值" />
</el-form-item>
<el-form-item label="下限" prop="lowerLimit">
<el-input v-model="paramForm.lowerLimit" placeholder="数值" />
</el-form-item>
<el-form-item label="上限" prop="upperLimit">
<el-input v-model="paramForm.upperLimit" placeholder="数值" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="paramForm.unit" maxlength="32" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="paramForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="paramSubmitLoading" type="primary" @click="submitParam"> </el-button>
<el-button @click="paramOpen = false"> </el-button>
<div slot="footer">
<el-button size="small" @click="versionOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="versionSubmitLoading" @click="submitVersion">确定</el-button>
</div>
</el-dialog>
</div>
@@ -201,8 +106,6 @@ import {
delProcessSpecVersion,
activateProcessSpecVersion
} from '@/api/wms/processSpecVersion'
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
export default {
name: 'ProcessSpecVersionManage',
@@ -212,18 +115,7 @@ export default {
specId: undefined,
specInfo: {},
versionList: [],
selectedVersion: null,
planList: [],
planLoading: false,
selectedPlan: null,
paramList: [],
paramLoading: false,
statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'],
segmentOptions: [
{ label: '入口', value: 'INLET' },
{ label: '过程', value: 'PROCESS' },
{ label: '出口', value: 'OUTLET' }
],
versionOpen: false,
versionTitle: '',
versionSubmitLoading: false,
@@ -232,180 +124,62 @@ export default {
versionCode: [{ required: true, message: '版本号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
},
planOpen: false,
planTitle: '',
planSubmitLoading: false,
planForm: {},
planRules: {
segmentType: [{ required: true, message: '段类型不能为空', trigger: 'change' }],
pointName: [{ required: true, message: '点位名称不能为空', trigger: 'blur' }],
pointCode: [{ required: true, message: '点位编码不能为空', trigger: 'blur' }],
sortOrder: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
},
specPickerId: '',
specPickerOptions: [],
specPickerLoading: false,
paramOpen: false,
paramTitle: '',
paramSubmitLoading: false,
paramForm: {},
paramRules: {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
}
specPickerLoading: false
}
},
watch: {
'$route': {
immediate: true,
handler() {
this.syncSpecIdFromRoute()
}
}
$route: { immediate: true, handler() { this.syncFromRoute() } }
},
methods: {
syncSpecIdFromRoute() {
syncFromRoute() {
const raw = this.$route.query.specId
if (raw != null && raw !== '') {
this.specId = String(raw)
} else {
this.specId = undefined
}
this.specId = raw != null && raw !== '' ? String(raw) : undefined
this.initPage()
},
loadSpecPickerOptions() {
this.specPickerLoading = true
listProcessSpec({ pageNum: 1, pageSize: 500 })
.then((res) => {
this.specPickerOptions = res.rows || []
})
.catch((e) => console.error('加载规程列表失败', e))
.finally(() => {
this.specPickerLoading = false
})
},
applySpecPicker() {
if (!this.specPickerId) {
return
}
this.$router.replace({
path: this.$route.path,
query: { ...this.$route.query, specId: this.specPickerId }
})
},
goBack() {
this.$router.go(-1)
},
goBack() { this.$router.go(-1) },
initPage() {
if (!this.specId) {
this.specInfo = {}
if (this.specPickerOptions.length === 0) {
this.loadSpecPickerOptions()
}
if (!this.specPickerOptions.length) this.loadSpecPicker()
return
}
this.pageLoading = true
getProcessSpec(this.specId)
.then((res) => {
getProcessSpec(this.specId).then(res => {
this.specInfo = res.data || {}
return listProcessSpecVersion({ specId: this.specId, pageNum: 1, pageSize: 200 })
})
.then((res) => {
}).then(res => {
this.versionList = res.rows || []
this.selectedVersion = null
this.planList = []
this.selectedPlan = null
this.paramList = []
})
.catch((e) => {
console.error('加载规程版本失败', e)
})
.finally(() => {
this.pageLoading = false
})
}).catch(e => console.error(e)).finally(() => { this.pageLoading = false })
},
loadSpecPicker() {
this.specPickerLoading = true
listProcessSpec({ pageNum: 1, pageSize: 500 }).then(res => {
this.specPickerOptions = res.rows || []
}).finally(() => { this.specPickerLoading = false })
},
applySpecPicker() {
if (!this.specPickerId) return
this.$router.replace({ path: this.$route.path, query: { ...this.$route.query, specId: this.specPickerId } })
},
onVersionRowClick(row) {
this.selectedVersion = row
this.selectedPlan = null
this.paramList = []
this.loadPlans(row.versionId)
this.goPlanSpec(row)
},
onPlanRowClick(row) {
this.selectedPlan = row
this.loadParams(row.planId)
goPlanSpec(row) {
const pathCurrent = this.$route.path.replace(/\/$/, '')
const m = pathCurrent.match(/^(.*\/processSpec)(?:\/.*)?$/)
const base = m ? m[1] : pathCurrent
this.$router.push({
path: `${base}/planSpec`,
query: { specId: this.specId, versionId: String(row.versionId), versionCode: row.versionCode }
})
},
loadPlans(versionId) {
if (!versionId) {
this.planList = []
this.selectedPlan = null
this.paramList = []
handleActiveChange(row, val) {
if (!val) {
this.$message.info('请激活其他版本来替换当前生效版本')
return
}
this.selectedPlan = null
this.paramList = []
this.planLoading = true
listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
.then((res) => {
this.planList = res.rows || []
})
.catch((e) => {
console.error('加载方案点位失败', e)
})
.finally(() => {
this.planLoading = false
})
},
loadParams(planId) {
if (!planId) {
this.paramList = []
return
}
this.paramLoading = true
listProcessPlanParam({ planId, pageNum: 1, pageSize: 500 })
.then((res) => {
this.paramList = res.rows || []
})
.catch((e) => {
console.error('加载方案参数失败', e)
})
.finally(() => {
this.paramLoading = false
})
},
openVersionDialog(row) {
this.versionForm = row
? { ...row }
: {
specId: this.specId,
versionCode: undefined,
status: 'DRAFT',
isActive: 0,
remark: undefined
}
this.versionTitle = row ? '编辑版本' : '新建版本'
this.versionOpen = true
this.$nextTick(() => this.$refs.versionFormRef && this.$refs.versionFormRef.clearValidate())
},
submitVersion() {
this.$refs.versionFormRef.validate((ok) => {
if (!ok) return
this.versionSubmitLoading = true
const req = this.versionForm.versionId
? updateProcessSpecVersion({ ...this.versionForm, specId: this.specId })
: addProcessSpecVersion({ ...this.versionForm, specId: this.specId })
req
.then(() => {
this.$modal.msgSuccess('保存成功')
this.versionOpen = false
this.initPage()
})
.catch((e) => console.error('保存版本失败', e))
.finally(() => {
this.versionSubmitLoading = false
})
})
},
activateVersion(row) {
this.$modal.confirm('确认将版本「' + row.versionCode + '」设为当前生效版本?').then(() => {
return activateProcessSpecVersion(row.versionId)
}).then(() => {
@@ -413,129 +187,116 @@ export default {
this.initPage()
}).catch(() => {})
},
openVersionDialog(row) {
this.versionForm = row
? { ...row }
: { specId: this.specId, versionCode: undefined, status: 'DRAFT', isActive: 0, remark: undefined }
this.versionTitle = row ? '编辑版本' : '新建版本'
this.versionOpen = true
this.$nextTick(() => this.$refs.versionFormRef && this.$refs.versionFormRef.clearValidate())
},
submitVersion() {
this.$refs.versionFormRef.validate(ok => {
if (!ok) return
this.versionSubmitLoading = true
const req = this.versionForm.versionId
? updateProcessSpecVersion({ ...this.versionForm, specId: this.specId })
: addProcessSpecVersion({ ...this.versionForm, specId: this.specId })
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.versionOpen = false
this.initPage()
}).catch(e => console.error(e)).finally(() => { this.versionSubmitLoading = false })
})
},
removeVersion(row) {
this.$modal.confirm('确认删除版本「' + row.versionCode + '」及其下方案点位').then(() => {
this.$modal.confirm('确认删除版本「' + row.versionCode + '」?').then(() => {
return delProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.selectedVersion = null
this.planList = []
this.selectedPlan = null
this.paramList = []
this.initPage()
}).catch(() => {})
},
openPlanDialog(row) {
if (!this.selectedVersion) {
this.$modal.msgWarning('请先选择一个规程版本')
return
}
this.planForm = row
? { ...row }
: {
versionId: this.selectedVersion.versionId,
segmentType: 'PROCESS',
segmentName: undefined,
pointName: undefined,
pointCode: undefined,
sortOrder: 0,
remark: undefined
}
this.planTitle = row ? '编辑方案点位' : '新建方案点位'
this.planOpen = true
this.$nextTick(() => this.$refs.planFormRef && this.$refs.planFormRef.clearValidate())
},
submitPlan() {
this.$refs.planFormRef.validate((ok) => {
if (!ok) return
this.planSubmitLoading = true
const req = this.planForm.planId ? updateProcessPlan(this.planForm) : addProcessPlan(this.planForm)
req
.then(() => {
this.$modal.msgSuccess('保存成功')
this.planOpen = false
this.loadPlans(this.selectedVersion.versionId)
})
.catch((e) => console.error('保存方案点位失败', e))
.finally(() => {
this.planSubmitLoading = false
})
})
},
removePlan(row) {
this.$modal.confirm('确认删除该方案点位?').then(() => {
return delProcessPlan(row.planId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.selectedPlan && this.selectedPlan.planId === row.planId) {
this.selectedPlan = null
this.paramList = []
}
this.loadPlans(this.selectedVersion.versionId)
}).catch(() => {})
},
openParamDialog(row) {
if (!this.selectedPlan) {
this.$modal.msgWarning('请先在上方方案点位表格中选中一行')
return
}
this.paramForm = row
? { ...row }
: {
planId: this.selectedPlan.planId,
paramCode: undefined,
paramName: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
unit: undefined,
remark: undefined
}
this.paramTitle = row ? '编辑方案参数' : '新建方案参数'
this.paramOpen = true
this.$nextTick(() => this.$refs.paramFormRef && this.$refs.paramFormRef.clearValidate())
},
parseDecimal(val) {
if (val === undefined || val === null || val === '') {
return undefined
}
const n = Number(val)
return Number.isFinite(n) ? n : undefined
},
buildParamPayload() {
return {
...this.paramForm,
targetValue: this.parseDecimal(this.paramForm.targetValue),
lowerLimit: this.parseDecimal(this.paramForm.lowerLimit),
upperLimit: this.parseDecimal(this.paramForm.upperLimit)
}
},
submitParam() {
this.$refs.paramFormRef.validate((ok) => {
if (!ok) return
this.paramSubmitLoading = true
const payload = this.buildParamPayload()
const req = payload.paramId ? updateProcessPlanParam(payload) : addProcessPlanParam(payload)
req
.then(() => {
this.$modal.msgSuccess('保存成功')
this.paramOpen = false
this.loadParams(this.selectedPlan.planId)
})
.catch((e) => console.error('保存方案参数失败', e))
.finally(() => {
this.paramSubmitLoading = false
})
})
},
removeParam(row) {
this.$modal.confirm('确认删除该方案参数?').then(() => {
return delProcessPlanParam(row.paramId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadParams(this.selectedPlan.planId)
}).catch(() => {})
}
}
}
</script>
<style scoped>
.ver-page {
padding: 16px 20px;
min-height: 100%;
}
.page-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.page-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.spec-code {
font-size: 12px;
font-weight: normal;
color: #909399;
margin-left: 6px;
}
.pick-card {
background: #fff;
border-radius: 4px;
padding: 24px;
border: 1px solid #ebeef5;
margin-bottom: 16px;
}
.pick-hint {
font-size: 13px;
color: #909399;
margin-bottom: 12px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
.el-table {
border-radius: 4px;
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: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; }
</style>