数据贯通完成,规程重构

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; package com.klp.framework.client;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.klp.framework.config.SqlServerApiProperties; import com.klp.framework.config.SqlServerApiProperties;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -73,7 +74,9 @@ public class SqlServerApiClient {
} }
public static class TableSchemaRequest { public static class TableSchemaRequest {
@JsonProperty("table_type")
private String tableType; private String tableType;
@JsonProperty("table_name")
private String tableName; private String tableName;
public TableSchemaRequest() { public TableSchemaRequest() {
@@ -102,6 +105,7 @@ public class SqlServerApiClient {
} }
public static class ExecuteSqlRequest { public static class ExecuteSqlRequest {
@JsonProperty("table_type")
private String tableType; private String tableType;
private String sql; private String sql;
private Map<String, Object> params; 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( return executeSql(
"oracle", "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() emptyParams()
); );
} }

View File

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

View File

@@ -32,12 +32,24 @@ public class SqlServerApiBusinessService {
} }
/** /**
* 计划列表:查询所有计划,按时间倒序 * 计划列表分页按时间倒序page 从 1 开始
* <p>
* 这是后续按 coilId 关联 SEG、实时数据的入口。
*/ */
public PlanListView getPlanList() { public PlanListView getPlanList(int page, int pageSize) {
return PlanListView.fromExecuteSqlResponse(client.queryPlanList()); 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.common.core.domain.R;
import com.klp.framework.service.SqlServerApiBusinessService; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/** /**
* sql-server-api 业务查询接口。 * sql-server-api 业务查询接口。
* <p> * <p>
@@ -21,11 +25,24 @@ public class SqlServerApiController {
private final SqlServerApiBusinessService businessService; private final SqlServerApiBusinessService businessService;
/** /**
* 计划列表。 * 计划列表(分页)
* page 从 1 开始,默认第 1 页,每页 20 条。
*/ */
@GetMapping("/plans") @GetMapping("/plans")
public R<SqlServerApiBusinessService.PlanListView> planList() { public R<SqlServerApiBusinessService.PlanListView> planList(
return R.ok(businessService.getPlanList()); @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 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多服务地址配置 --- # OEE 聚合klp-da多服务地址配置
da: da:
oee: oee:

View File

@@ -7,6 +7,12 @@ klp:
# 生产环境文件存储目录 # 生产环境文件存储目录
directory-path: /home/ubuntu/oa/folder 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多服务地址配置 --- # OEE 聚合klp-da多服务地址配置
da: da:
oee: oee:

View File

@@ -66,14 +66,6 @@ user:
lockTime: 10 lockTime: 10
# sql-server-api 中间件配置 # 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配置
spring: spring:
application: 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({ export function getTimingPlanList(page = 1, pageSize = 20) {
baseURL: 'http://' + url, return request({
headers: { url: '/sql-server-api/plans',
'Content-Type': 'application/json' method: 'get',
}, params: { page, pageSize }
timeout: 10000 })
}
// 计划总数
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'
}) })
request.interceptors.response.use(response => response.data)
return {
getPlanList: () => 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'
})
}
} }

View File

@@ -1,151 +1,204 @@
<template> <template>
<div class="timing-page acid-page"> <div class="acid-view">
<el-card class="page-card" shadow="never"> <div class="filter-bar">
<div slot="header" class="card-header"> <el-input
<span>酸轧实绩页</span> v-model="queryForm.coilId"
<el-tag type="success" size="mini">Plan + Seg + Quality</el-tag> placeholder="热卷号 / 成品卷号"
</div> 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-row :gutter="12" class="main-row">
<el-form-item label="计划号"> <!-- 左侧计划列表 -->
<el-input v-model="queryForm.coilId" placeholder="输入 coilId" clearable style="width: 220px;" /> <el-col :span="9">
</el-form-item> <div class="left-card">
<el-form-item> <el-table
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">查询</el-button> ref="planTable"
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button> :data="planRows"
</el-form-item> size="mini"
</el-form> highlight-current-row
:height="tableHeight"
@row-click="handlePlanRowClick"
>
<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-row :gutter="16"> <div class="pagination-bar">
<el-col :span="8"> <el-pagination
<el-card shadow="never" class="sub-card"> small
<div slot="header" class="sub-header">计划列表</div> layout="total, prev, pager, next"
<el-table :total="pagination.total"
:data="planRows" :page-size="pagination.pageSize"
height="560" :current-page="pagination.page"
size="mini" @current-change="handlePageChange"
highlight-current-row
@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> </div>
<el-row :gutter="16"> </div>
<el-col :span="8"> </el-col>
<el-card shadow="never">
<div slot="header">SEGNO</div> <!-- 右侧详情 + 图表 -->
<div class="seg-list"> <el-col :span="15">
<el-tag <div v-if="!selectedPlan" class="empty-hint">
v-for="item in segView.segNo" <el-empty description="选择左侧计划" :image-size="72" />
:key="item" </div>
size="mini"
class="seg-tag" <template v-else>
> <div class="detail-grid">
{{ item }} <div v-for="f in planFields" :key="f.key" class="detail-cell">
</el-tag> <span class="cell-label">{{ f.label }}</span>
</div> <span class="cell-value">{{ selectedPlan[f.key] != null ? selectedPlan[f.key] : '—' }}</span>
</el-card> </div>
</el-col> </div>
<el-col :span="16">
<el-card shadow="never"> <div class="perf-header">
<div slot="header">属性数组</div> 实轧实绩
<el-table :data="seriesTable" size="mini" height="240" border> <span v-if="hasPerfData" class="perf-count">({{ perfSegCount }} )</span>
<el-table-column prop="key" label="属性" width="180" /> <el-tag v-if="perfLoading" size="mini" type="info" style="margin-left:8px">加载中</el-tag>
<el-table-column prop="values" label="数组值" min-width="300"> </div>
<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;"> <div v-if="hasPerfData" class="charts-wrap">
{{ item }} <div ref="chartSpeed" class="chart-box" />
</el-tag> <div ref="chartMillSpeed" class="chart-box" />
</template> <div ref="chartTension" class="chart-box" />
</el-table-column> </div>
</el-table>
</el-card> <el-empty v-else-if="!perfLoading" description="暂无实绩数据" :image-size="56" style="margin-top: 24px" />
</el-col> </template>
</el-row> </el-col>
</template> </el-row>
</el-card>
</el-col>
</el-row>
</el-card>
</div> </div>
</template> </template>
<script> <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 { export default {
name: 'TimingAcidPage', name: 'TimingAcidPage',
props: {
baseURL: { type: String, required: true }
},
data() { data() {
return { return {
fetchApi: null,
loading: false, loading: false,
perfLoading: false,
queryForm: { coilId: '' }, queryForm: { coilId: '' },
planRows: [], planRows: [],
selectedPlan: null, selectedPlan: null,
segView: null, perfSeries: null,
seriesTable: [] 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() { 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() this.loadPlanList()
}, },
beforeDestroy() {
this.disposeCharts()
},
methods: { 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() { async loadPlanList() {
this.loading = true this.loading = true
try { try {
const res = await this.fetchApi.getPlanList() const { page, pageSize } = this.pagination
const rows = res?.data?.rows || res?.rows || [] const res = await getTimingPlanList(page, pageSize)
this.planRows = rows this.planRows = res?.data?.rows || []
} finally { } finally {
this.loading = false this.loading = false
} }
}, },
handlePageChange(page) {
this.pagination.page = page
this.loadPlanList()
},
async handleSearch() { async handleSearch() {
if (!this.queryForm.coilId) { if (!this.queryForm.coilId) return this.loadPlanList()
return this.loadPlanList()
}
this.loading = true this.loading = true
try { try {
const res = await this.fetchApi.getPlanDetail(this.queryForm.coilId) const res = await getTimingPlanDetail(this.queryForm.coilId)
const row = res?.data?.firstRow || res?.firstRow || null const row = res?.data?.firstRow || null
this.selectedPlan = row if (row) {
if (row && row.ENCOILID) { this.selectedPlan = row
await this.loadSeg(row.ENCOILID) await this.loadPerf(row)
} }
} finally { } finally {
this.loading = false this.loading = false
@@ -153,25 +206,86 @@ export default {
}, },
async handlePlanRowClick(row) { async handlePlanRowClick(row) {
this.selectedPlan = row this.selectedPlan = row
if (row?.COILID) { this.perfSeries = null
const detail = await this.fetchApi.getPlanDetail(row.COILID) this.perfSegCount = 0
const plan = detail?.data?.firstRow || detail?.firstRow || row this.disposeCharts()
this.selectedPlan = plan await this.loadPerf(row)
const encoilId = plan.ENCOILID || plan.ENCOILID || plan.COILID },
if (encoilId) await this.loadSeg(encoilId) 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) { disposeCharts() {
const res = await this.fetchApi.getSegByEncoilId(encoilId) if (this.resizeHandler) {
const view = res?.data || res window.removeEventListener('resize', this.resizeHandler)
this.segView = view this.resizeHandler = null
this.seriesTable = Object.entries(view?.series || {}).map(([key, values]) => ({ key, values })) }
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() { handleReset() {
this.queryForm.coilId = '' this.queryForm.coilId = ''
this.selectedPlan = null this.selectedPlan = null
this.segView = null this.perfSeries = null
this.seriesTable = [] this.perfSegCount = 0
this.disposeCharts()
this.pagination.page = 1
this.loadPlanList() this.loadPlanList()
} }
} }
@@ -179,11 +293,115 @@ export default {
</script> </script>
<style scoped> <style scoped>
.timing-page { padding: 16px; } .acid-view {
.page-card { border-radius: 12px; } padding: 12px 16px;
.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; } .filter-bar {
.seg-list { display: flex; flex-wrap: wrap; gap: 6px; } display: flex;
.seg-tag { margin-right: 0; } 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> </style>

View File

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

View File

@@ -1,87 +1,80 @@
<template> <template>
<div class="app-container"> <div class="spec-page">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px"> <!-- 规程类型 tabs -->
<el-form-item label="规程编号" prop="specCode"> <div class="type-tab-bar">
<el-input v-model="queryParams.specCode" placeholder="规程编号" clearable @keyup.enter.native="handleQuery" /> <span
</el-form-item> v-for="t in specTypeTab"
<el-form-item label="规程名称" prop="specName"> :key="t.value"
<el-input v-model="queryParams.specName" placeholder="规程名称" clearable @keyup.enter.native="handleQuery" /> :class="['type-tab', { active: activeSpecType === t.value }]"
</el-form-item> @click="switchSpecType(t.value)"
<el-form-item label="规程类型" prop="specType"> >{{ t.label }}</span>
<el-select v-model="queryParams.specType" placeholder="全部" clearable> </div>
<el-option v-for="item in specTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> <!-- 产线 tabs -->
</el-form-item> <div class="line-tab-bar">
<el-form-item label="产线" prop="lineId"> <span
<el-select v-model="queryParams.lineId" placeholder="全部" clearable filterable> :class="['line-tab', { active: activeLineId === '' }]"
<el-option @click="switchLine('')"
v-for="line in lineOptions" >全部</span>
:key="line.lineId" <span
:label="formatLineOption(line)" v-for="line in lineOptions"
:value="line.lineId" :key="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-button size="mini" type="primary" @click="handleQuery">查询</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</div>
</div>
<!-- 列表 -->
<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)"
/> />
</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-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>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="产线" align="center" min-width="160" show-overflow-tooltip> <el-table-column label="操作" align="right">
<template slot-scope="scope"> <template slot-scope="{ row }">
{{ getLineName(scope.row.lineId) }} <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="产品类型" align="center" prop="productType" min-width="100" show-overflow-tooltip /> </el-table>
<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>
<pagination <pagination
v-show="total > 0" v-show="total > 0"
@@ -91,45 +84,43 @@
@pagination="getList" @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-form-item label="规程编号" prop="specCode">
<el-input v-model="form.specCode" placeholder="唯一编号" maxlength="64" show-word-limit /> <el-input v-model="form.specCode" placeholder="唯一编号" maxlength="64" show-word-limit />
</el-form-item> </el-form-item>
<el-form-item label="规程名称" prop="specName"> <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>
<el-form-item label="规程类型" prop="specType"> <el-form-item label="规程类型" prop="specType">
<el-select v-model="form.specType" placeholder="请选择"> <el-select v-model="form.specType" style="width:100%">
<el-option v-for="item in specTypeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="t in specTypeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="产线" prop="lineId"> <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 <el-option
v-for="line in lineOptions" v-for="line in lineOptions"
:key="line.lineId" :key="line.lineId"
:label="formatLineOption(line)" :label="line.lineCode ? line.lineName + '' + line.lineCode + '' : line.lineName"
:value="line.lineId" :value="line.lineId"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="产品类型" prop="productType"> <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>
<el-form-item label="是否启用" prop="isEnabled"> <el-form-item label="是否启用" prop="isEnabled">
<el-radio-group v-model="form.isEnabled"> <el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" />
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <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-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button> <el-button size="small" @click="open = false">取消</el-button>
<el-button @click="cancel"> </el-button> <el-button size="small" type="primary" :loading="btnLoading" @click="submitForm">确定</el-button>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
@@ -139,184 +130,255 @@
import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec' import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec'
import { listProductionLine } from '@/api/wms/productionLine' import { listProductionLine } from '@/api/wms/productionLine'
const SPEC_TYPES = [
{ label: '工艺规程', value: 'PROCESS' },
{ label: '标准', value: 'STANDARD' }
]
export default { export default {
name: 'ProcessSpec', name: 'ProcessSpec',
dicts: ['common_swicth'],
data() { data() {
return { return {
buttonLoading: false, loading: false,
loading: true, btnLoading: false,
total: 0,
dataList: [],
ids: [], ids: [],
single: true, single: true,
multiple: true, multiple: true,
showSearch: true,
total: 0,
dataList: [],
title: '',
open: false, open: false,
dialogTitle: '',
lineOptions: [], lineOptions: [],
specTypeOptions: [ specTypeTab: [{ label: '全部', value: '' }, ...SPEC_TYPES],
{ label: '工艺规程', value: 'PROCESS' }, specTypeOptions: SPEC_TYPES,
{ label: '标准', value: 'STANDARD' } activeSpecType: '',
], activeLineId: '',
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 20, pageSize: 20,
specCode: undefined,
specName: undefined, specName: undefined,
specType: undefined, specType: undefined,
lineId: undefined, lineId: undefined
productType: undefined,
isEnabled: undefined
}, },
form: {}, form: {},
rules: { rules: {
specCode: [{ required: true, message: '规程编号不能为空', trigger: 'blur' }], specCode: [{ required: true, message: '规程编号不能为空', trigger: 'blur' }],
specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }], specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }],
specType: [{ required: true, message: '规程类型不能为空', trigger: 'change' }], specType: [{ required: true, message: '请选择规程类型', trigger: 'change' }],
lineId: [{ required: true, message: '产线不能为空', trigger: 'change' }] lineId: [{ required: true, message: '请选择产线', trigger: 'change' }]
} }
} }
}, },
created() { created() {
this.loadLineOptions() listProductionLine({ pageNum: 1, pageSize: 500 }).then(res => {
this.lineOptions = res.rows || []
})
this.getList() this.getList()
}, },
methods: { 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() { getList() {
this.loading = true this.loading = true
listProcessSpec(this.queryParams).then((response) => { listProcessSpec(this.queryParams).then(res => {
this.dataList = response.rows this.dataList = res.rows || []
this.total = response.total this.total = res.total || 0
this.loading = false }).finally(() => { this.loading = false })
}).catch((err) => {
console.error('加载规程列表失败', err)
this.loading = false
})
}, },
cancel() { switchSpecType(val) {
this.open = false this.activeSpecType = val
this.reset() this.queryParams.specType = val || undefined
this.queryParams.pageNum = 1
this.getList()
}, },
reset() { switchLine(lineId) {
this.form = { this.activeLineId = lineId
specId: undefined, this.queryParams.lineId = lineId || undefined
specCode: undefined, this.queryParams.pageNum = 1
specName: undefined, this.getList()
specType: 'PROCESS',
lineId: undefined,
productType: undefined,
isEnabled: 1,
remark: undefined
}
this.resetForm('form')
}, },
handleQuery() { handleQuery() {
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1
this.getList() this.getList()
}, },
resetQuery() { resetQuery() {
this.resetForm('queryForm') this.queryParams.specName = undefined
this.handleQuery() this.handleQuery()
}, },
handleSelectionChange(selection) { handleSelectionChange(sel) {
this.ids = selection.map((item) => item.specId) this.ids = sel.map(r => r.specId)
this.single = selection.length !== 1 this.single = sel.length !== 1
this.multiple = !selection.length 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() { handleAdd() {
this.reset() this.reset()
this.dialogTitle = '新增规程'
this.open = true this.open = true
this.title = '添加规程'
}, },
handleUpdate(row) { handleUpdate(row) {
this.loading = true
this.reset() this.reset()
const specId = row.specId || this.ids const specId = row ? row.specId : this.ids[0]
getProcessSpec(specId).then((response) => { getProcessSpec(specId).then(res => {
this.loading = false this.form = res.data || {}
this.form = response.data this.dialogTitle = '修改规程'
this.open = true this.open = true
this.title = '修改规程'
}).catch((err) => {
console.error('获取规程详情失败', err)
this.loading = false
}) })
}, },
submitForm() { submitForm() {
this.$refs.form.validate((valid) => { this.$refs.form.validate(valid => {
if (!valid) { if (!valid) return
return this.btnLoading = true
} const req = this.form.specId ? updateProcessSpec(this.form) : addProcessSpec(this.form)
this.buttonLoading = true
const req = this.form.specId != null ? updateProcessSpec(this.form) : addProcessSpec(this.form)
req.then(() => { req.then(() => {
this.$modal.msgSuccess(this.form.specId != null ? '修改成功' : '新增成功') this.$modal.msgSuccess('保存成功')
this.open = false this.open = false
this.getList() this.getList()
}).catch((err) => { }).finally(() => { this.btnLoading = false })
console.error('保存规程失败', err)
}).finally(() => {
this.buttonLoading = false
})
}) })
}, },
handleDelete(row) { handleDelete(row) {
const specIds = row.specId || this.ids const ids = row ? row.specId : this.ids
this.$modal.confirm('是否确认删除选中的规程数据').then(() => { this.$modal.confirm('确认删除所选规程').then(() => {
this.loading = true this.loading = true
return delProcessSpec(specIds) return delProcessSpec(ids)
}).then(() => { }).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功') this.$modal.msgSuccess('删除成功')
}).catch(() => {}).finally(() => { this.getList()
this.loading = false }).catch(() => {}).finally(() => { this.loading = false })
})
}, },
handleExport() { toggleEnabled(row, val) {
this.download('wms/processSpec/export', { const updated = { ...row, isEnabled: val ? 1 : 0 }
...this.queryParams updateProcessSpec(updated).then(() => {
}, `processSpec_${new Date().getTime()}.xlsx`) row.isEnabled = updated.isEnabled
}).catch(() => {})
}, },
goVersionManage(row) { 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 pathCurrent = this.$route.path.replace(/\/$/, '')
const m = pathCurrent.match(/^(.*\/processSpec)(?:\/.*)?$/) const m = pathCurrent.match(/^(.*\/processSpec)(?:\/.*)?$/)
const base = m ? m[1] : pathCurrent const base = m ? m[1] : pathCurrent
this.$router.push({ this.$router.push({ path: `${base}/version`, query: { specId: String(row.specId) } })
path: `${base}/version`,
query: { specId: String(specId) }
})
} }
} }
} }
</script> </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> <template>
<div class="app-container" v-loading="pageLoading"> <div class="ver-page" 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 class="page-header">
<div slot="header">请选择规程</div> <el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
<p class="text-muted mb8" style="color: #909399; font-size: 13px">规程管理列表进入时会自动带上规程从菜单直接进入时请先选择规程</p> <span class="page-title" v-if="specInfo.specName">
<el-form inline size="small" @submit.native.prevent> {{ specInfo.specName }}
<el-form-item label="规程"> <span class="spec-code">{{ specInfo.specCode }}</span>
<el-select </span>
v-model="specPickerId" <el-button
filterable v-if="specInfo.specId"
clearable type="primary"
placeholder="请选择规程" size="mini"
style="min-width: 280px" icon="el-icon-plus"
:loading="specPickerLoading" style="margin-left:auto"
> @click="openVersionDialog()"
<el-option v-for="s in specPickerOptions" :key="s.specId" :label="(s.specCode || '') + ' — ' + (s.specName || '')" :value="String(s.specId)" /> >新建版本</el-button>
</el-select> </div>
</el-form-item>
<el-form-item> <!-- specId 时选择规程 -->
<el-button type="primary" :disabled="!specPickerId" @click="applySpecPicker">进入维护</el-button> <div v-if="!specInfo.specId" class="pick-card">
</el-form-item> <p class="pick-hint">规程管理列表进入会自动带上规程也可在下方手动选择</p>
</el-form> <el-select
</el-card> v-model="specPickerId"
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-select>
<el-button type="primary" size="small" :disabled="!specPickerId" @click="applySpecPicker">进入维护</el-button>
</div>
<template v-if="specInfo.specId"> <template v-if="specInfo.specId">
<el-card shadow="never" class="mb8"> <!-- 版本列表 -->
<div slot="header" class="clearfix"> <div class="section-title">规程版本</div>
<span>规程版本</span> <el-table
<el-button style="float: right; padding: 3px 10px" type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</el-button> :data="versionList"
</div> size="small"
<KLPTable highlight-current-row
:data="versionList" @row-click="onVersionRowClick"
highlight-current-row >
@row-click="onVersionRowClick" <el-table-column label="版本号" prop="versionCode" />
> <el-table-column label="状态" prop="status" />
<el-table-column label="版本号" prop="versionCode" min-width="120" align="center" /> <el-table-column label="创建时间" prop="createTime" />
<el-table-column label="是否生效" prop="isActive" width="100" align="center"> <el-table-column label="生效" align="center">
<template slot-scope="scope"> <template slot-scope="{ row }">
<el-tag v-if="scope.row.isActive === 1" type="success" size="mini">生效</el-tag> <el-switch
<span v-else></span> :value="row.isActive === 1"
</template> active-color="#5F7BA0"
</el-table-column> @click.native.stop
<el-table-column label="状态" prop="status" width="120" align="center" /> @change="handleActiveChange(row, $event)"
<el-table-column label="创建时间" prop="createTime" width="170" align="center" /> />
<el-table-column label="操作" width="220" align="center" fixed="right"> </template>
<template slot-scope="scope"> </el-table-column>
<el-button type="text" size="mini" @click.stop="activateVersion(scope.row)">设为生效</el-button> <el-table-column label="操作" align="right">
<el-button type="text" size="mini" @click.stop="openVersionDialog(scope.row)">编辑</el-button> <template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="removeVersion(scope.row)">删除</el-button> <el-button type="text" size="mini" @click.stop="goPlanSpec(row)">方案点位</el-button>
</template> <el-button type="text" size="mini" @click.stop="openVersionDialog(row)">编辑</el-button>
</el-table-column> <el-button type="text" size="mini" class="btn-danger" @click.stop="removeVersion(row)">删除</el-button>
</KLPTable> </template>
</el-card> </el-table-column>
</el-table>
<el-card shadow="never" v-if="selectedVersion"> <el-empty v-if="!versionList.length && !pageLoading" description="暂无版本,请新建" style="padding:40px 0" />
<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>
</template> </template>
<!-- 版本 --> <!-- 新建/编辑版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="520px" append-to-body @close="versionForm = {}"> <el-dialog :title="versionTitle" :visible.sync="versionOpen" width="480px" append-to-body @close="versionForm = {}">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="100px"> <el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="88px" size="small">
<el-form-item label="版本号" prop="versionCode"> <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>
<el-form-item label="状态" prop="status"> <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-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
</el-select> </el-select>
</el-form-item> </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-switch v-model="versionForm.isActive" :active-value="1" :inactive-value="0" />
</el-form-item> </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-input v-model="versionForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer">
<el-button :loading="versionSubmitLoading" type="primary" @click="submitVersion"> </el-button> <el-button size="small" @click="versionOpen = false">取消</el-button>
<el-button @click="versionOpen = false"> </el-button> <el-button size="small" type="primary" :loading="versionSubmitLoading" @click="submitVersion">确定</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> </div>
</el-dialog> </el-dialog>
</div> </div>
@@ -201,8 +106,6 @@ import {
delProcessSpecVersion, delProcessSpecVersion,
activateProcessSpecVersion activateProcessSpecVersion
} from '@/api/wms/processSpecVersion' } from '@/api/wms/processSpecVersion'
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
export default { export default {
name: 'ProcessSpecVersionManage', name: 'ProcessSpecVersionManage',
@@ -212,18 +115,7 @@ export default {
specId: undefined, specId: undefined,
specInfo: {}, specInfo: {},
versionList: [], versionList: [],
selectedVersion: null,
planList: [],
planLoading: false,
selectedPlan: null,
paramList: [],
paramLoading: false,
statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'], statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'],
segmentOptions: [
{ label: '入口', value: 'INLET' },
{ label: '过程', value: 'PROCESS' },
{ label: '出口', value: 'OUTLET' }
],
versionOpen: false, versionOpen: false,
versionTitle: '', versionTitle: '',
versionSubmitLoading: false, versionSubmitLoading: false,
@@ -232,180 +124,62 @@ export default {
versionCode: [{ required: true, message: '版本号不能为空', trigger: 'blur' }], versionCode: [{ required: true, message: '版本号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }] 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: '', specPickerId: '',
specPickerOptions: [], specPickerOptions: [],
specPickerLoading: false, specPickerLoading: false
paramOpen: false,
paramTitle: '',
paramSubmitLoading: false,
paramForm: {},
paramRules: {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
}
} }
}, },
watch: { watch: {
'$route': { $route: { immediate: true, handler() { this.syncFromRoute() } }
immediate: true,
handler() {
this.syncSpecIdFromRoute()
}
}
}, },
methods: { methods: {
syncSpecIdFromRoute() { syncFromRoute() {
const raw = this.$route.query.specId const raw = this.$route.query.specId
if (raw != null && raw !== '') { this.specId = raw != null && raw !== '' ? String(raw) : undefined
this.specId = String(raw)
} else {
this.specId = undefined
}
this.initPage() this.initPage()
}, },
loadSpecPickerOptions() { goBack() { this.$router.go(-1) },
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)
},
initPage() { initPage() {
if (!this.specId) { if (!this.specId) {
this.specInfo = {} this.specInfo = {}
if (this.specPickerOptions.length === 0) { if (!this.specPickerOptions.length) this.loadSpecPicker()
this.loadSpecPickerOptions()
}
return return
} }
this.pageLoading = true this.pageLoading = true
getProcessSpec(this.specId) getProcessSpec(this.specId).then(res => {
.then((res) => { this.specInfo = res.data || {}
this.specInfo = res.data || {} return listProcessSpecVersion({ specId: this.specId, pageNum: 1, pageSize: 200 })
return listProcessSpecVersion({ specId: this.specId, pageNum: 1, pageSize: 200 }) }).then(res => {
}) this.versionList = res.rows || []
.then((res) => { }).catch(e => console.error(e)).finally(() => { this.pageLoading = false })
this.versionList = res.rows || [] },
this.selectedVersion = null loadSpecPicker() {
this.planList = [] this.specPickerLoading = true
this.selectedPlan = null listProcessSpec({ pageNum: 1, pageSize: 500 }).then(res => {
this.paramList = [] this.specPickerOptions = res.rows || []
}) }).finally(() => { this.specPickerLoading = false })
.catch((e) => { },
console.error('加载规程版本失败', e) applySpecPicker() {
}) if (!this.specPickerId) return
.finally(() => { this.$router.replace({ path: this.$route.path, query: { ...this.$route.query, specId: this.specPickerId } })
this.pageLoading = false
})
}, },
onVersionRowClick(row) { onVersionRowClick(row) {
this.selectedVersion = row this.goPlanSpec(row)
this.selectedPlan = null
this.paramList = []
this.loadPlans(row.versionId)
}, },
onPlanRowClick(row) { goPlanSpec(row) {
this.selectedPlan = row const pathCurrent = this.$route.path.replace(/\/$/, '')
this.loadParams(row.planId) const m = pathCurrent.match(/^(.*\/processSpec)(?:\/.*)?$/)
}, const base = m ? m[1] : pathCurrent
loadPlans(versionId) { this.$router.push({
if (!versionId) { path: `${base}/planSpec`,
this.planList = [] query: { specId: this.specId, versionId: String(row.versionId), versionCode: row.versionCode }
this.selectedPlan = null
this.paramList = []
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) { handleActiveChange(row, val) {
if (!val) {
this.$message.info('请激活其他版本来替换当前生效版本')
return
}
this.$modal.confirm('确认将版本「' + row.versionCode + '」设为当前生效版本?').then(() => { this.$modal.confirm('确认将版本「' + row.versionCode + '」设为当前生效版本?').then(() => {
return activateProcessSpecVersion(row.versionId) return activateProcessSpecVersion(row.versionId)
}).then(() => { }).then(() => {
@@ -413,129 +187,116 @@ export default {
this.initPage() this.initPage()
}).catch(() => {}) }).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) { removeVersion(row) {
this.$modal.confirm('确认删除版本「' + row.versionCode + '」及其下方案点位').then(() => { this.$modal.confirm('确认删除版本「' + row.versionCode + '」?').then(() => {
return delProcessSpecVersion(row.versionId) return delProcessSpecVersion(row.versionId)
}).then(() => { }).then(() => {
this.$modal.msgSuccess('删除成功') this.$modal.msgSuccess('删除成功')
this.selectedVersion = null
this.planList = []
this.selectedPlan = null
this.paramList = []
this.initPage() this.initPage()
}).catch(() => {}) }).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> </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>