This commit is contained in:
jhd
2026-05-11 16:45:31 +08:00
21 changed files with 3240 additions and 1202 deletions

View File

@@ -9,6 +9,7 @@
<h3>{{ currentPlanSheetInfo.planSheetName || '排产单详情' }}</h3>
<div>
<el-button type="primary" plain @click="handleAdd">新增明细</el-button>
<el-button type="success" plain @click="handleBatchAdd">批量新增</el-button>
<el-button type="info" plain @click="getList">刷新</el-button>
</div>
</div>
@@ -287,6 +288,75 @@
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</el-dialog>
<!-- 批量新增对话框 -->
<el-dialog title="批量新增" :visible.sync="batchAddDialogVisible" width="80%" append-to-body>
<div class="batch-add-content">
<!-- 合同选择 -->
<div class="contract-section">
<div class="section-title">选择合同</div>
<el-form :model="batchQueryParams" ref="batchQueryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="合同号" prop="contractCode">
<el-input v-model="batchQueryParams.contractCode" placeholder="请输入合同号" style="width: 180px" />
</el-form-item>
<el-form-item label="客户">
<el-input v-model="batchQueryParams.customerName" placeholder="请输入客户名称" style="width: 180px" />
</el-form-item>
<el-form-item label="业务员">
<el-input v-model="batchQueryParams.salesman" placeholder="请输入业务员" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="getBatchOrderList">搜索</el-button>
<el-button icon="el-icon-refresh" @click="resetBatchQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="batchOrderLoading" :data="batchOrderList" style="width: 100%" border @row-click="selectBatchContract"
highlight-current-row>
<el-table-column prop="contractCode" label="合同号" />
<el-table-column prop="signTime" label="签订时间" width="150" />
<el-table-column prop="signLocation" label="签订地点" />
<el-table-column prop="companyName" label="客户公司" />
<el-table-column prop="salesman" label="业务员" width="100" />
<el-table-column prop="deliveryDate" label="交货日期" width="150" />
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click.stop="selectBatchContract(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination background layout="prev, pager, next, jumper" :total="batchOrderTotal"
:page-size="batchQueryParams.pageSize" :current-page.sync="batchQueryParams.pageNum"
@current-change="getBatchOrderList" />
</div>
</div>
<!-- 明细选择 -->
<div class="item-section" v-if="selectedContract">
<div class="section-title">选择明细多选</div>
<el-table v-loading="batchItemLoading" :data="batchItemList" style="width: 100%" border
@selection-change="handleBatchItemSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="产品类型" align="center" prop="productType" />
<el-table-column label="成品宽度" align="center" prop="width" />
<el-table-column label="成品厚度" align="center" prop="thickness" />
<el-table-column label="成品规格" align="center" prop="finishedProductSpec" />
<el-table-column label="材质" align="center" prop="material" />
<el-table-column label="重量" align="center" prop="weight" />
<el-table-column label="卷数" align="center" prop="productNum" />
<el-table-column label="表面处理" align="center" prop="surfaceTreatment" />
<el-table-column label="包装要求" align="center" prop="packagingReq" />
<el-table-column label="切边要求" align="center" prop="edgeCuttingReq" />
<el-table-column label="用途" align="center" prop="purpose" />
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="batchAddDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmBatchAdd" :disabled="selectedBatchItems.length === 0">确认新增</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -351,7 +421,31 @@ export default {
// 订单明细加载状态
orderItemLoading: false,
// 当前编辑的行
currentEditingRow: null
currentEditingRow: null,
// 批量新增对话框
batchAddDialogVisible: false,
// 批量查询参数
batchQueryParams: {
pageNum: 1,
pageSize: 10,
contractCode: undefined,
customerName: undefined,
salesman: undefined
},
// 批量订单列表
batchOrderList: [],
// 批量订单总数
batchOrderTotal: 0,
// 批量订单加载状态
batchOrderLoading: false,
// 批量明细列表
batchItemList: [],
// 批量明细加载状态
batchItemLoading: false,
// 选中的合同
selectedContract: null,
// 选中的明细
selectedBatchItems: []
};
},
created() {
@@ -582,6 +676,98 @@ export default {
// 处理订单行点击
handleOrderSelect(row) {
this.selectOrder(row);
},
// 打开批量新增对话框
handleBatchAdd() {
this.batchAddDialogVisible = true;
this.selectedContract = null;
this.selectedBatchItems = [];
this.getBatchOrderList();
},
// 获取批量订单列表
getBatchOrderList() {
this.batchOrderLoading = true;
listOrder(this.batchQueryParams).then(response => {
this.batchOrderList = response.rows;
this.batchOrderTotal = response.total;
this.batchOrderLoading = false;
});
},
// 重置批量查询参数
resetBatchQuery() {
this.batchQueryParams = {
pageNum: 1,
pageSize: 10,
contractCode: undefined,
customerName: undefined,
salesman: undefined
};
this.getBatchOrderList();
},
// 选择批量合同
selectBatchContract(row) {
this.selectedContract = row;
this.selectedBatchItems = [];
this.batchItemLoading = true;
listOrderItem({ orderId: row.orderId }).then(res => {
this.batchItemList = res.rows;
this.batchItemLoading = false;
});
},
// 处理批量明细选择变更
handleBatchItemSelectionChange(selection) {
this.selectedBatchItems = selection;
},
// 确认批量新增
confirmBatchAdd() {
if (this.selectedBatchItems.length === 0) {
this.$message.warning('请至少选择一条明细');
return;
}
this.$confirm(`确认新增 ${this.selectedBatchItems.length} 条明细?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const currentMaxSeqNo = this.planDetailList.length > 0
? Math.max(...this.planDetailList.map(item => parseInt(item.bizSeqNo) || 0))
: 0;
const addPromises = this.selectedBatchItems.map((item, index) => {
const newRow = {
planSheetId: this.currentPlanSheetId,
bizSeqNo: currentMaxSeqNo + index + 1,
orderCode: this.selectedContract.orderCode,
contractCode: this.selectedContract.contractCode,
customerName: this.selectedContract.companyName,
salesman: this.selectedContract.salesman,
orderId: this.selectedContract.orderId,
productName: item.productType,
productMaterial: item.material,
productWidth: item.width,
rollingThick: item.thickness,
markCoatThick: item.thickness,
tonSteelLengthRange: 0,
planQty: item.productNum,
planWeight: item.weight,
surfaceTreatment: item.surfaceTreatment,
productPackaging: item.packagingReq,
widthReq: item.edgeCuttingReq,
productEdgeReq: item.widthTolerance,
usageReq: item.purpose
};
return addPlanDetail(newRow);
});
Promise.all(addPromises).then(() => {
this.$message.success('批量新增成功');
this.batchAddDialogVisible = false;
this.getList();
}).catch(error => {
this.$message.error('批量新增失败');
});
});
}
}
};
@@ -654,4 +840,25 @@ export default {
/* ::v-deep .el-input__inner {
padding: 0 1px;
} */
.batch-add-content {
padding: 10px 0;
}
.contract-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.item-section {
margin-top: 20px;
}
.section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
color: #303133;
}
</style>

View File

@@ -42,6 +42,16 @@
<el-tag v-else-if="scope.row.objectionStatus === 2" type="info">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="钢卷信息" align="center" prop="coilList" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<div v-if="scope.row.coilList && scope.row.coilList.length > 0">
<div v-for="(coil, index) in scope.row.coilList" :key="coil.coilId || index" style="margin-bottom: 4px;">
<CurrentCoilNo @click.native="handleClickCoil(coil)" :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
</div>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="处理结果" align="center" prop="handleContent" show-overflow-tooltip>
<template slot-scope="scope">
<div class="cell-html" v-html="scope.row.handleContent"></div>
@@ -53,11 +63,6 @@
<span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="结案时间" align="center" prop="closeTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.closeTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
@@ -66,12 +71,6 @@
v-if="scope.row.objectionStatus == 0"></el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)"
v-if="scope.row.objectionStatus == 1"></el-button>
<!-- <el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handleFinish(scope.row)"
>结案</el-button> -->
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
@@ -113,28 +112,28 @@
placeholder="请输入客户诉求"
></el-input>
</el-form-item>
<!-- <el-form-item label="处理内容">
<editor v-model="form.handleContent" :min-height="192"/>
<el-form-item label="关联钢卷">
<div style="width: 100%;">
<CoilSelector
:multiple="true"
:rangeMode="true"
:rangeData="shippedCoils"
:orderBy="true"
@confirm="(coils) => handleCoilConfirm('form', coils)"
placeholder="选择钢卷">
<el-button type="primary" size="small">
<i class="el-icon-search"></i> 选择钢卷
</el-button>
</CoilSelector>
<div v-if="formCoilList.length > 0" style="margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px;">
<div v-for="(coil, index) in formCoilList" :key="coil.coilId || index" style="display: flex; align-items: center; gap: 4px;">
<CurrentCoilNo :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
<el-button type="text" icon="el-icon-close" size="mini" @click="removeCoil('form', index)"></el-button>
</div>
</div>
<span v-else style="color: #909399; font-size: 12px;">暂未选择钢卷</span>
</div>
</el-form-item>
<el-form-item label="处理人" prop="handleUser">
<el-input v-model="form.handleUser" placeholder="请输入处理人" />
</el-form-item>
<el-form-item label="处理时间" prop="handleTime">
<el-date-picker clearable
v-model="form.handleTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择处理时间">
</el-date-picker>
</el-form-item>
<el-form-item label="结案时间" prop="closeTime">
<el-date-picker clearable
v-model="form.closeTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择结案时间">
</el-date-picker>
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
@@ -150,17 +149,6 @@
<el-form-item label="处理内容">
<editor v-model="checkForm.handleContent" :min-height="192" />
</el-form-item>
<!-- <el-form-item label="处理人" prop="handleUser">
<el-input v-model="form.handleUser" placeholder="请输入处理人" />
</el-form-item>
<el-form-item label="处理时间" prop="handleTime">
<el-date-picker clearable
v-model="form.handleTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择处理时间">
</el-date-picker>
</el-form-item> -->
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitCheckForm"> </el-button>
@@ -180,6 +168,14 @@
<el-descriptions-item label="处理内容" :span="2">
<div v-html="viewForm.handleContent"></div>
</el-descriptions-item>
<el-descriptions-item label="关联钢卷" :span="2">
<div v-if="viewForm.coilList && viewForm.coilList.length > 0" style="display: flex; flex-wrap: wrap; gap: 8px;">
<div v-for="(coil, index) in viewForm.coilList" :key="coil.coilId || index">
<CurrentCoilNo @click.native="handleClickCoil(coil)" :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
</div>
</div>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button @click="viewOpen = false"> </el-button>
@@ -190,9 +186,16 @@
<script>
import { listSalesObjection, getSalesObjection, delSalesObjection, addSalesObjection, updateSalesObjection } from "@/api/crm/salesObjection";
import { listOrderPackaging } from "@/api/crm/order";
import CoilSelector from "@/components/CoilSelector/index.vue";
import CurrentCoilNo from "@/components/KLPService/Renderer/CurrentCoilNo.vue";
export default {
name: "SalesObjection",
components: {
CoilSelector,
CurrentCoilNo
},
props: {
order: {
type: Object,
@@ -273,6 +276,11 @@ export default {
checkOpen: false,
viewForm: {},
viewOpen: false,
// 钢卷选择相关
coilSelectorType: 'form', // form 或 check
formCoilList: [], // 表单中的钢卷列表
checkCoilList: [], // 处理中的钢卷列表
shippedCoils: [], // 订单已发货的钢卷(可选范围)
};
},
methods: {
@@ -290,9 +298,11 @@ export default {
this.open = false;
this.reset();
},
handleView(row) {
this.viewForm = row;
this.viewOpen = true;
// 点击钢卷
handleClickCoil(coil) {
this.$router.push({
path: '/wms/coil/' + coil.coilId,
});
},
// 表单重置
reset() {
@@ -335,70 +345,6 @@ export default {
this.single = selection.length !== 1
this.multiple = !selection.length
},
handleDo(row) {
console.log(row, '处理')
this.checkForm = row;
this.checkOpen = true;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加销售异议管理";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const objectionId = row.objectionId || this.ids
getSalesObjection(objectionId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改销售异议管理";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.objectionId != null) {
updateSalesObjection(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addSalesObjection(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 提交处理按钮 */
submitCheckForm() {
this.buttonLoading = true;
updateSalesObjection({
...this.checkForm,
handleUser: this.currentUserName,
handleTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
objectionStatus: 1,
}).then(response => {
this.$modal.msgSuccess("处理成功");
this.checkOpen = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
},
/** 删除按钮操作 */
handleDelete(row) {
const objectionIds = row.objectionId || this.ids;
@@ -419,6 +365,137 @@ export default {
this.download('crm/salesObjection/export', {
...this.queryParams
}, `salesObjection_${new Date().getTime()}.xlsx`)
},
// ==================== 钢卷相关方法 ====================
// 获取订单已发货的钢卷
getShippedCoils() {
if (this.orderId) {
listOrderPackaging(this.orderId).then(response => {
this.shippedCoils = response.data || [];
}).catch(() => {
this.shippedCoils = [];
});
} else {
this.shippedCoils = [];
}
},
// 钢卷选择确认
handleCoilConfirm(type, selectedCoils) {
if (type === 'form') {
this.formCoilList = [...selectedCoils];
} else if (type === 'check') {
this.checkCoilList = [...selectedCoils];
}
},
// 移除选中的钢卷
removeCoil(type, index) {
if (type === 'form') {
this.formCoilList.splice(index, 1);
} else if (type === 'check') {
this.checkCoilList.splice(index, 1);
}
},
// 将钢卷数组转换为CSV字符串
coilListToCsv(coilList) {
if (!coilList || coilList.length === 0) {
return '';
}
return coilList.map(coil => coil.coilId).join(',');
},
// 从coilIds CSV和coilList中恢复钢卷数据
// 重写handleAdd方法
handleAdd() {
this.reset();
this.formCoilList = []; // 清空钢卷列表
this.getShippedCoils(); // 获取可选择的钢卷
this.open = true;
this.title = "添加销售异议管理";
},
// 重写handleUpdate方法
handleUpdate(row) {
this.loading = true;
this.reset();
this.getShippedCoils(); // 获取可选择的钢卷
const objectionId = row.objectionId || this.ids;
getSalesObjection(objectionId).then(response => {
this.loading = false;
this.form = response.data;
// 恢复钢卷列表
this.formCoilList = response.data.coilList || [];
this.open = true;
this.title = "修改销售异议管理";
});
},
// 重写handleDo方法
handleDo(row) {
console.log(row, '处理');
this.checkForm = row;
this.checkCoilList = row.coilList || []; // 恢复钢卷列表
this.getShippedCoils(); // 获取可选择的钢卷
this.checkOpen = true;
},
// 重写handleView方法
handleView(row) {
this.viewForm = row;
this.viewOpen = true;
},
// 重写submitForm方法添加coilIds
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
// 添加coilIds
const formData = {
...this.form,
coilIds: this.coilListToCsv(this.formCoilList)
};
if (this.form.objectionId != null) {
updateSalesObjection(formData).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addSalesObjection(formData).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
// 重写submitCheckForm方法添加coilIds
submitCheckForm() {
this.buttonLoading = true;
updateSalesObjection({
...this.checkForm,
handleUser: this.currentUserName,
handleTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
objectionStatus: 1,
coilIds: this.coilListToCsv(this.checkCoilList)
}).then(response => {
this.$modal.msgSuccess("处理成功");
this.checkOpen = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
};

View File

@@ -42,6 +42,16 @@
<el-tag v-else-if="scope.row.objectionStatus === 2" type="info">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="钢卷信息" align="center" prop="coilList" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<div v-if="scope.row.coilList && scope.row.coilList.length > 0">
<div v-for="(coil, index) in scope.row.coilList" :key="coil.coilId || index" style="margin-bottom: 4px;">
<CurrentCoilNo @click.native="handleClickCoil(coil)" :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
</div>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<!-- <el-table-column label="处理内容" align="center" prop="handleContent" /> -->
<el-table-column label="处理人" align="center" prop="handleUser" />
<el-table-column label="处理时间" align="center" prop="handleTime" width="180">
@@ -110,7 +120,29 @@
></el-input>
</el-form-item>
<el-form-item label="订单编号">
<order-select v-model="form.orderId" placeholder="请选择订单" />
<order-select v-model="form.orderId" placeholder="请选择订单" @change="getShippedCoils" />
</el-form-item>
<el-form-item label="关联钢卷">
<div style="width: 100%;">
<CoilSelector
:multiple="true"
:rangeMode="true"
:rangeData="shippedCoils"
:orderBy="true"
@confirm="(coils) => handleCoilConfirm('form', coils)"
placeholder="选择钢卷">
<el-button type="primary" size="small">
<i class="el-icon-search"></i> 选择钢卷
</el-button>
</CoilSelector>
<div v-if="formCoilList.length > 0" style="margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px;">
<div v-for="(coil, index) in formCoilList" :key="coil.coilId || index" style="display: flex; align-items: center; gap: 4px;">
<CurrentCoilNo :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
<el-button type="text" icon="el-icon-close" size="mini" @click="removeCoil('form', index)"></el-button>
</div>
</div>
<span v-else style="color: #909399; font-size: 12px;">暂未选择钢卷</span>
</div>
</el-form-item>
<!-- <el-form-item label="处理内容">
<editor v-model="form.handleContent" :min-height="192"/>
@@ -163,6 +195,14 @@
<el-descriptions-item label="反馈日期">{{ parseTime(viewForm.returnDate, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="投诉情况" :span="2">{{ viewForm.complaintContent }}</el-descriptions-item>
<el-descriptions-item label="客户诉求" :span="2">{{ viewForm.customerDemand }}</el-descriptions-item>
<el-descriptions-item label="关联钢卷" :span="2">
<div v-if="viewForm.coilList && viewForm.coilList.length > 0" style="display: flex; flex-wrap: wrap; gap: 8px;">
<div v-for="(coil, index) in viewForm.coilList" :key="coil.coilId || index">
<CurrentCoilNo @click.native="handleClickCoil(coil)" :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
</div>
</div>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="处理人">{{ viewForm.handleUser }}</el-descriptions-item>
<el-descriptions-item label="处理时间">{{ parseTime(viewForm.handleTime, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="处理内容" :span="2">
@@ -178,14 +218,19 @@
<script>
import { listSalesObjection, getSalesObjection, delSalesObjection, addSalesObjection, updateSalesObjection } from "@/api/crm/salesObjection";
import { listOrderPackaging } from "@/api/crm/order";
import OrderSelect from "@/components/KLPService/OrderSelect";
import CustomerSelect from "@/components/KLPService/CustomerSelect";
import CoilSelector from "@/components/CoilSelector/index.vue";
import CurrentCoilNo from "@/components/KLPService/Renderer/CurrentCoilNo.vue";
export default {
name: "SalesObjection",
components: {
OrderSelect,
CustomerSelect,
CoilSelector,
CurrentCoilNo
},
computed: {
currentUserName() {
@@ -245,6 +290,11 @@ export default {
checkOpen: false,
viewForm: {},
viewOpen: false,
// 钢卷选择相关
coilSelectorType: 'form', // form 或 check
formCoilList: [], // 表单中的钢卷列表
checkCoilList: [], // 处理中的钢卷列表
shippedCoils: [], // 订单已发货的钢卷(可选范围)
};
},
methods: {
@@ -271,7 +321,7 @@ export default {
this.form = {
objectionId: undefined,
objectionCode: undefined,
orderId: this.orderId,
orderId: undefined,
productCategory: undefined,
returnDate: undefined,
complaintContent: undefined,
@@ -310,11 +360,13 @@ export default {
handleDo(row) {
console.log(row, '处理')
this.checkForm = row;
this.checkCoilList = row.coilList || []; // 恢复钢卷列表
this.checkOpen = true;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.formCoilList = []; // 清空钢卷列表
this.open = true;
this.title = "添加销售异议管理";
},
@@ -326,6 +378,12 @@ export default {
getSalesObjection(objectionId).then(response => {
this.loading = false;
this.form = response.data;
// 恢复钢卷列表
this.formCoilList = response.data.coilList || [];
// 获取可选择的钢卷
if (this.form.orderId) {
this.getShippedCoils();
}
this.open = true;
this.title = "修改销售异议管理";
});
@@ -335,8 +393,14 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
// 添加coilIds
const formData = {
...this.form,
coilIds: this.coilListToCsv(this.formCoilList)
};
if (this.form.objectionId != null) {
updateSalesObjection(this.form).then(response => {
updateSalesObjection(formData).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
@@ -344,7 +408,7 @@ export default {
this.buttonLoading = false;
});
} else {
addSalesObjection(this.form).then(response => {
addSalesObjection(formData).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
@@ -363,6 +427,7 @@ export default {
handleUser: this.currentUserName,
handleTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
objectionStatus: 1,
coilIds: this.coilListToCsv(this.checkCoilList)
}).then(response => {
this.$modal.msgSuccess("处理成功");
this.checkOpen = false;
@@ -391,6 +456,53 @@ export default {
this.download('crm/salesObjection/export', {
...this.queryParams
}, `salesObjection_${new Date().getTime()}.xlsx`)
},
// ==================== 钢卷相关方法 ====================
// 点击钢卷
handleClickCoil(coil) {
this.$router.push({
path: '/wms/coil/' + coil.coilId,
});
},
// 获取订单已发货的钢卷
getShippedCoils() {
if (this.form.orderId) {
listOrderPackaging(this.form.orderId).then(response => {
this.shippedCoils = response.data || [];
}).catch(() => {
this.shippedCoils = [];
});
} else {
this.shippedCoils = [];
}
},
// 钢卷选择确认
handleCoilConfirm(type, selectedCoils) {
if (type === 'form') {
this.formCoilList = [...selectedCoils];
} else if (type === 'check') {
this.checkCoilList = [...selectedCoils];
}
},
// 移除选中的钢卷
removeCoil(type, index) {
if (type === 'form') {
this.formCoilList.splice(index, 1);
} else if (type === 'check') {
this.checkCoilList.splice(index, 1);
}
},
// 将钢卷数组转换为CSV字符串
coilListToCsv(coilList) {
if (!coilList || coilList.length === 0) {
return '';
}
return coilList.map(coil => coil.coilId).join(',');
}
}
};

View File

@@ -0,0 +1,960 @@
<template>
<div class="actual-container">
<!-- 顶部实绩列表 (PLTCM_PDO_EXCOIL) -->
<div class="top-section">
<el-table
ref="excoilTable"
:data="excoilRows"
size="mini"
highlight-current-row
border
:height="topTableHeight"
style="width:100%"
@row-click="handleRowClick"
>
<el-table-column label="子卷号" min-width="110" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.EXCOILID || row.excoilid }}</template>
</el-table-column>
<el-table-column label="热卷号" min-width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.HOT_COILID || row.hot_coilid || '—' }}</template>
</el-table-column>
<el-table-column label="班" width="40" align="center">
<template slot-scope="{ row }">{{ row.SHIFT || row.shift || '—' }}</template>
</el-table-column>
<el-table-column label="组" width="40" align="center">
<template slot-scope="{ row }">{{ row.CREW || row.crew || '—' }}</template>
</el-table-column>
<el-table-column label="钢种" min-width="80" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.ORDER_QUALITY || row.order_quality || row.GRADE || row.grade || '—' }}</template>
</el-table-column>
<el-table-column label="来料厚度" width="68" align="right">
<template slot-scope="{ row }">{{ row.ENTRY_THICK || row.entry_thick || '—' }}</template>
</el-table-column>
<el-table-column label="出口厚度" width="68" align="right">
<template slot-scope="{ row }">{{ row.EXIT_THICK || row.exit_thick || '—' }}</template>
</el-table-column>
<el-table-column label="偏差上限" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_POS_DEV || row.exit_pos_dev || '0' }}</template>
</el-table-column>
<el-table-column label="偏差下限" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_NEG_DEV || row.exit_neg_dev || '0' }}</template>
</el-table-column>
<el-table-column label="来料宽度" width="60" align="right">
<template slot-scope="{ row }">{{ row.ENTRY_WIDTH || row.entry_width || '—' }}</template>
</el-table-column>
<el-table-column label="出口宽度" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_WIDTH || row.exit_width || '—' }}</template>
</el-table-column>
<el-table-column label="来料重量" width="60" align="right">
<template slot-scope="{ row }">{{ row.USED_ENTRY_WEIGHT || row.used_entry_weight || row.ENTRY_WEIGHT || row.entry_weight || '—' }}</template>
</el-table-column>
<el-table-column label="称重重量" width="60" align="right">
<template slot-scope="{ row }">{{ row.MEAS_EXIT_WEIGHT || row.meas_exit_weight || '—' }}</template>
</el-table-column>
<el-table-column label="包装要求" width="60">
<template slot-scope="{ row }">{{ row.PARK_TYPE || row.park_type || '—' }}</template>
</el-table-column>
<el-table-column label="切边要求" width="60">
<template slot-scope="{ row }">{{ row.SIDE_TRIM || row.side_trim || '—' }}</template>
</el-table-column>
<el-table-column label="成品质量" width="58" align="right">
<template slot-scope="{ row }">{{ row.QUALITY || row.quality || '—' }}</template>
</el-table-column>
<el-table-column label="成品长度" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_LENGTH || row.exit_length || '—' }}</template>
</el-table-column>
<el-table-column label="吨钢长度" width="60" align="right">
<template slot-scope="{ row }">{{ calcLengthPerTon(row) }}</template>
</el-table-column>
<el-table-column label="下线时间" width="140">
<template slot-scope="{ row }">{{ formatDate(row.END_DATE || row.end_date) }}</template>
</el-table-column>
<el-table-column label="状态" width="68" align="center">
<template slot-scope="{ row }">
<el-tag type="primary" size="mini" effect="plain">{{ row.STATUS || row.status || '产出' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
small layout="total, prev, pager, next"
:total="pagination.total"
:page-size="pagination.pageSize"
:current-page="pagination.page"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 底部图表区域 -->
<div class="bottom-section">
<div class="chart-section">
<el-tabs v-model="activeTab" size="small" class="chart-tabs" @tab-click="handleTabSwitch">
<!-- 趋势参数左树形目录 + 右单图 -->
<el-tab-pane label="趋势参数" name="trend">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="segLoading" class="no-data-hint">加载中</div>
<div v-else-if="!segData" class="no-data-hint">暂无 SEG 数据</div>
<div v-else class="trend-layout">
<!-- 左侧目录树 -->
<div class="trend-tree">
<div v-for="group in trendGroups" :key="group.label" class="tree-group">
<div class="tree-group-label" @click="toggleGroup(group.label)">
<i :class="expandedGroups[group.label] ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
{{ group.label }}
</div>
<div v-show="expandedGroups[group.label]" class="tree-children">
<div
v-for="item in group.children"
:key="item.col"
class="tree-item"
:class="{ active: selectedTrendParam && selectedTrendParam.col === item.col }"
@click="selectTrendParam(item)"
>{{ item.label }}</div>
</div>
</div>
</div>
<!-- 右侧图表 -->
<div class="trend-chart-area">
<div v-if="!selectedTrendParam" class="no-data-hint"> 点击左侧参数查看曲线</div>
<!-- 保持 DOM 存在仅用 v-show 控制显示避免 ref 失效 -->
<div ref="trendSingleChart" :style="{ display: selectedTrendParam ? 'block' : 'none', height: '100%', width: '100%' }" />
</div>
</div>
</el-tab-pane>
<!-- 厚度曲线 -->
<el-tab-pane label="厚度曲线" name="thickness">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!gaugeRows || !gaugeRows.length" class="no-data-hint">暂无厚度数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartGauge1" class="chart-box" />
<div ref="chartGauge2" class="chart-box" />
<div ref="chartGauge3" class="chart-box" />
<div ref="chartGauge4" class="chart-box" />
</div>
</el-tab-pane>
<!-- 带钢板形 热力图 -->
<el-tab-pane label="带钢板形" name="flatness3d">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll">
<div ref="chartFlatness3d" class="chart-box chart-box-tall" />
</div>
</el-tab-pane>
<!-- 板形曲线一行两图 -->
<el-tab-pane label="板形曲线" name="flatness">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartFlatDev" class="chart-box" />
<div ref="chartTilt" class="chart-box" />
<div ref="chartWrBend" class="chart-box" />
<div ref="chartIrBend" class="chart-box" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 右侧查找面板 -->
<div class="search-panel">
<div class="panel-title">查找</div>
<div class="search-type-group">
<el-radio v-model="searchType" label="coil">按钢卷号</el-radio>
<div v-if="searchType === 'coil'" class="search-field">
<span class="search-label">钢卷号</span>
<el-input v-model="searchCoilId" size="mini" style="width:140px" placeholder="EXCOILID" />
</div>
</div>
<div class="search-type-group">
<el-radio v-model="searchType" label="time">按时间</el-radio>
<template v-if="searchType === 'time'">
<div class="search-field">
<span class="search-label">开始时间</span>
<el-date-picker v-model="searchStartDate" type="datetime" size="mini" style="width:160px"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" />
</div>
<div class="search-field">
<span class="search-label">结束时间</span>
<el-date-picker v-model="searchEndDate" type="datetime" size="mini" style="width:160px"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" />
</div>
</template>
</div>
<div class="search-actions">
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
<el-button size="mini" @click="handleFindReset">重置</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import 'echarts-gl'
import {
getExcoilList,
getExcoilCount,
getTimingSegByEncoilId,
getTimingRealtimeData
} from '@/api/l2/timing'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
const TREND_GROUPS = [
{
label: '张力',
children: [
{ label: '开卷张力', col: 'PORTENS' },
{ label: '入口活套张力', col: 'ENLTENS' },
{ label: '拉矫张力', col: 'TLTENS' },
{ label: '酸洗张力', col: 'PLTENS' },
{ label: '出口活套张力', col: 'CXLTENS' },
{ label: '圆盘剪张力', col: 'TRIMTENS' }
]
},
{
label: '速度',
children: [
{ label: '开卷速度', col: 'PORSPEED' },
{ label: '酸洗速度', col: 'PLSPEED' },
{ label: '圆盘剪速度', col: 'TRIMSPEED' },
{ label: '轧机入口速度', col: 'MILLENTRYSPEED' },
{ label: '轧机出口速度', col: 'MILLEXITSPEED' }
]
},
{
label: '拉矫机',
children: [
{ label: '1#插入量', col: 'TLMESH1' },
{ label: '2#插入量', col: 'TLMESH2' },
{ label: '3#插入量', col: 'TLMESH3' },
{ label: '延伸率', col: 'TLELONG' }
]
},
{
label: '酸洗段',
children: [
{ label: '1#温度', col: 'TK1TEMP' },
{ label: '2#温度', col: 'TK2TEMP' },
{ label: '3#温度', col: 'TK3TEMP' },
{ label: '漂洗温度', col: 'RINSETEMP' }
]
}
]
// V_VBDA_GAUGE 厚度曲线4 个图,列名来自 DDL
const GAUGE_COLS = [
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
]
// V_VBDA_SHAPE 板形曲线4 个图,列名来自 DDL
const SHAPE_SCALAR_COLS = [
{ col: 'ABSDEVIATION', title: '总板形偏差 [IU]' },
{ col: 'TILT', title: '末架倾斜量 [mm]' },
{ col: 'WRBEND', title: '工作辊弯辊力 [kN]' },
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
]
/**
* 从数据数组计算合理的 Y 轴范围(带 15% 上下边距)。
* echarts 默认 scale 在数据变化幅度极小时会把轴拉到很大范围,导致曲线看起来是横线。
*/
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
const min = Math.min(...nums)
const max = Math.max(...nums)
if (min === max) {
const base = Math.abs(min) || 1
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
}
const pad = (max - min) * 0.15
return {
min: parseFloat((min - pad).toFixed(4)),
max: parseFloat((max + pad).toFixed(4))
}
}
function makeLine(title, xData, yData) {
const range = calcYRange(yData)
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
tooltip: { trigger: 'axis' },
grid: { top: 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: {
type: 'category', data: xData,
name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
yAxis: {
type: 'value',
min: range.min,
max: range.max,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 10 }
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseWheel: true }
],
series: [{
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1 }, data: yData
}]
}
}
function getRowVal(row, col) {
const v = row[col] !== undefined ? row[col] : row[col.toLowerCase()]
return v == null ? null : Number(v)
}
function xLocData(rows) {
return rows.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(1)
})
}
export default {
name: 'ActualPerformance',
data() {
return {
excoilLoading: false,
segLoading: false,
realtimeLoading: false,
excoilRows: [],
selectedRow: null,
segData: null,
gaugeRows: null,
shapeRows: null,
activeTab: 'trend',
// 趋势参数树状态
trendGroups: TREND_GROUPS,
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedTrendParam: null,
trendChartInst: null,
// 查找
searchType: 'coil',
searchCoilId: '',
searchStartDate: '',
searchEndDate: '',
pagination: { page: 1, pageSize: 50, total: 0 },
topTableHeight: 'calc(40vh - 80px)',
chartInstances: [],
resizeHandler: null
}
},
created() {
this.loadExcoilCount()
this.loadExcoilList()
},
beforeDestroy() {
this.disposeAllCharts()
},
methods: {
async loadExcoilCount() {
try {
const res = await getExcoilCount()
this.pagination.total = res?.data?.total ?? 0
} catch (_) {}
},
async loadExcoilList() {
this.excoilLoading = true
try {
const res = await getExcoilList(this.pagination.page, this.pagination.pageSize)
this.excoilRows = res?.data?.rows || []
} finally {
this.excoilLoading = false
}
},
handlePageChange(page) {
this.pagination.page = page
this.loadExcoilList()
},
async handleRowClick(row) {
this.selectedRow = row
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.disposeAllCharts()
// PLTCM_PRO_SEG 用 ENCOILID 查(主键前缀)
const encoilId = row.ENCOILID || row.encoilid
// V_VBDA_GAUGE / V_VBDA_SHAPE 用 EXCOILID 作为 MATID
const excoilId = row.EXCOILID || row.excoilid
await Promise.all([
encoilId ? this.loadSeg(encoilId) : Promise.resolve(),
excoilId ? this.loadRealtime(excoilId) : Promise.resolve()
])
await this.$nextTick()
// 加载完成后自动选中第一个趋势参数
if (this.activeTab === 'trend' && this.segData) {
this.selectTrendParam(TREND_GROUPS[0].children[0])
} else {
this.renderCurrentTab()
}
},
async loadSeg(encoilId) {
this.segLoading = true
try {
const res = await getTimingSegByEncoilId(encoilId)
const rows = res?.data?.rows || []
this.segData = rows.length ? (res?.data?.series || null) : null
} catch (_) {
} finally {
this.segLoading = false
}
},
async loadRealtime(excoilId) {
this.realtimeLoading = true
try {
const res = await getTimingRealtimeData(excoilId)
const g = res?.data?.gauge?.result
const s = res?.data?.shape?.result
this.gaugeRows = Array.isArray(g) ? g : null
this.shapeRows = Array.isArray(s) ? s : null
} catch (_) {
} finally {
this.realtimeLoading = false
}
},
// ── 趋势参数树 ──────────────────────────────
toggleGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
},
isGroupExpanded(label) {
return !!this.expandedGroups[label]
},
selectTrendParam(item) {
this.selectedTrendParam = item
this.$nextTick(() => this.renderTrendSingleChart())
},
renderTrendSingleChart() {
if (!this.selectedTrendParam || !this.segData) return
const el = this.$refs.trendSingleChart
if (!el) return
// 复用已有实例,避免重复 init
if (!this.trendChartInst || this.trendChartInst.isDisposed()) {
this.trendChartInst = echarts.init(el)
// 滚轮缩放支持
const resizeFn = () => this.trendChartInst && !this.trendChartInst.isDisposed() && this.trendChartInst.resize()
window.addEventListener('resize', resizeFn)
this._trendResizeFn = resizeFn
}
const x = this.segX()
const yData = this.seg(this.selectedTrendParam.col)
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData), true)
},
// ── Tab 切换 ────────────────────────────────
handleTabSwitch() {
this.$nextTick(() => {
if (this.activeTab === 'trend' && this.selectedTrendParam && this.segData) {
this.renderTrendSingleChart()
} else {
this.renderCurrentTab()
}
})
},
renderCurrentTab() {
this.disposeSideCharts()
if (this.activeTab === 'thickness' && this.gaugeRows?.length) this.renderGaugeCharts()
if (this.activeTab === 'flatness3d' && this.shapeRows?.length) this.renderFlatness3d()
if (this.activeTab === 'flatness' && this.shapeRows?.length) this.renderFlatnessCharts()
},
// ── 销毁 ────────────────────────────────────
disposeSideCharts() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler)
this.resizeHandler = null
}
this.chartInstances.forEach(c => { if (c && !c.isDisposed()) c.dispose() })
this.chartInstances = []
},
disposeAllCharts() {
this.disposeSideCharts()
if (this._trendResizeFn) {
window.removeEventListener('resize', this._trendResizeFn)
this._trendResizeFn = null
}
if (this.trendChartInst && !this.trendChartInst.isDisposed()) {
this.trendChartInst.dispose()
this.trendChartInst = null
}
},
// ── SEG 数据辅助 ─────────────────────────────
seg(col) {
const s = this.segData
// Oracle 返回大写列名,兼容小写
const arr = s[col] !== undefined ? s[col] : (s[col.toLowerCase()] || [])
return arr.map(v => v == null ? null : Number(Number(v).toFixed(3)))
},
segX() {
const s = this.segData
const arr = s['STARTPOS'] !== undefined ? s['STARTPOS'] : (s['startpos'] || [])
return arr.map(v => v == null ? '' : Number(v).toFixed(1))
},
// ── 图表初始化 ───────────────────────────────
makeChart(ref, option) {
const el = this.$refs[ref]
if (!el) return null
const chart = echarts.init(el)
chart.setOption(option)
return chart
},
setupResize() {
this.resizeHandler = () => this.chartInstances.forEach(c => {
if (c && !c.isDisposed()) c.resize()
})
window.addEventListener('resize', this.resizeHandler)
},
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
renderGaugeCharts() {
const rows = this.gaugeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = refs.map((ref, i) => {
const { col, title } = GAUGE_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
// ── 带钢板形 3D 线图 (V_VBDA_SHAPE) ────────────
// 每个通道画一条独立 line3D通道之间不连面形成镂空效果
renderFlatness3d() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const firstRow = rows[0]
const high = parseInt(getRowVal(firstRow, 'HIGHZONEID')) || 26
const low = parseInt(getRowVal(firstRow, 'LOWZONEID')) || 1
const numZones = Math.min(Math.max(high - low + 1, 1), 26)
const zoneCols = Array.from({ length: numZones }, (_, i) =>
`VALUES${String(low + i).padStart(2, '0')}`
)
// X 方向降采样,最多 200 个点
const step = Math.max(1, Math.floor(rows.length / 200))
const sampled = rows.filter((_, i) => i % step === 0)
const numX = sampled.length
// X 轴标签(位置,单位 m
const xLabels = sampled.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(0)
})
// 收集值域用于 visualMap
let minV = Infinity, maxV = -Infinity
sampled.forEach(row => {
zoneCols.forEach(col => {
const v = getRowVal(row, col)
if (v != null) {
if (v < minV) minV = v
if (v > maxV) maxV = v
}
})
})
if (!isFinite(minV)) { minV = -30; maxV = 30 }
const absMax = Math.max(Math.abs(minV), Math.abs(maxV))
// ① 沿 X 方向网格线(每通道一条,按 Z 值着色)
const channelLines = zoneCols.map((col, yi) => ({
type: 'line3D',
coordinateSystem: 'cartesian3D',
data: sampled.map((row, xi) => {
const v = getRowVal(row, col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean),
lineStyle: { width: 2, opacity: 1 }
}))
// ② 沿 Y 方向网格线(每隔若干位置连通各通道,按 Z 值着色)
const xStride = Math.max(1, Math.floor(numX / 60))
const crossLines = []
for (let xi = 0; xi < numX; xi += xStride) {
const pts = zoneCols.map((col, yi) => {
const v = getRowVal(sampled[xi], col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean)
if (pts.length > 1) {
crossLines.push({
type: 'line3D',
coordinateSystem: 'cartesian3D',
data: pts,
lineStyle: { width: 1.5, opacity: 1 }
})
}
}
const series = [...channelLines, ...crossLines]
const option = {
title: { text: '实测平直度 [IU]', textStyle: { fontSize: 13, fontWeight: 'normal' }, top: 6, left: 10 },
tooltip: {},
visualMap: {
show: true,
dimension: 2,
min: -absMax,
max: absMax,
calculable: true,
orient: 'vertical',
right: 10,
top: 'center',
textStyle: { fontSize: 10 },
inRange: {
// 负值红色 → 零值绿色 → 正值蓝紫
color: ['#8B0000','#CC2200','#E84C00','#F46D43',
'#FDAE61','#FEE08B',
'#66BD63','#1A9850','#006837',
'#3288BD','#5E4FA2','#762A83']
}
},
grid3D: {
boxWidth: 200,
boxHeight: 60,
boxDepth: 80,
viewControl: {
projection: 'orthographic',
autoRotate: false,
rotateSensitivity: 1,
zoomSensitivity: 1
},
light: {
main: { intensity: 1.2, shadow: false },
ambient: { intensity: 0.3 }
}
},
xAxis3D: {
type: 'value',
name: '位置',
min: 0,
max: numX - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: {
fontSize: 9,
formatter: v => xLabels[Math.round(v)] || ''
}
},
yAxis3D: {
type: 'value',
name: '通道',
min: 0,
max: numZones - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: {
fontSize: 9,
formatter: v => String(low + Math.round(v))
}
},
zAxis3D: {
type: 'value',
name: 'IU',
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 9 }
},
series
}
const el = this.$refs.chartFlatness3d
if (!el) return
const chart = echarts.init(el)
chart.setOption(option)
this.chartInstances = [chart]
this.setupResize()
},
// ── 板形曲线 (V_VBDA_SHAPE) ──────────────────
renderFlatnessCharts() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartFlatDev', 'chartTilt', 'chartWrBend', 'chartIrBend']
const charts = refs.map((ref, i) => {
const { col, title } = SHAPE_SCALAR_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(3))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
// ── 查找 ─────────────────────────────────────
handleFindSearch() {
if (this.searchType === 'coil' && this.searchCoilId) {
const found = this.excoilRows.find(r =>
(r.EXCOILID || r.excoilid || '').includes(this.searchCoilId)
)
if (found) {
this.$refs.excoilTable && this.$refs.excoilTable.setCurrentRow(found)
this.handleRowClick(found)
} else {
this.$message.info('当前页未找到该卷号,请翻页查找')
}
} else {
this.pagination.page = 1
this.loadExcoilList()
}
},
handleFindReset() {
this.searchCoilId = ''
this.searchStartDate = ''
this.searchEndDate = ''
this.selectedRow = null
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.disposeAllCharts()
this.pagination.page = 1
this.loadExcoilCount()
this.loadExcoilList()
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
if (!len || !wt || wt === 0) return '—'
return (len / wt).toFixed(2)
},
formatDate(v) {
if (!v) return '—'
return String(v).replace('T', ' ').substring(0, 19)
}
}
}
</script>
<style scoped lang="scss">
.actual-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 8px;
background: #f0f2f5;
gap: 6px;
box-sizing: border-box;
}
.top-section {
flex-shrink: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
overflow: hidden;
}
.table-pagination {
display: flex;
justify-content: flex-end;
padding: 4px 8px;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
.bottom-section {
flex: 1;
min-height: 0;
display: flex;
gap: 6px;
}
.chart-section {
flex: 1;
min-height: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
padding: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
.chart-tabs {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
::v-deep .el-tabs__content {
flex: 1;
overflow: hidden;
min-height: 0;
padding: 0;
}
::v-deep .el-tab-pane {
height: 100%;
}
}
}
/* ── 趋势参数:左树 + 右图 ── */
.trend-layout {
display: flex;
height: 100%;
gap: 0;
}
.trend-tree {
width: 140px;
flex-shrink: 0;
overflow-y: auto;
border-right: 1px solid #ebeef5;
padding: 4px 0;
&::-webkit-scrollbar { width: 3px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
.tree-group {
user-select: none;
}
.tree-group-label {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
font-size: 12px;
font-weight: 600;
color: #303133;
cursor: pointer;
&:hover { background: #f5f7fa; }
i { font-size: 10px; color: #909399; }
}
.tree-children { padding-left: 4px; }
.tree-item {
padding: 4px 8px 4px 18px;
font-size: 12px;
color: #606266;
cursor: pointer;
border-radius: 3px;
margin: 1px 4px;
&:hover { background: #ecf5ff; color: #409eff; }
&.active {
background: #ecf5ff;
color: #409eff;
font-weight: 500;
}
}
.trend-chart-area {
flex: 1;
min-width: 0;
height: 100%;
padding: 4px 4px 4px 8px;
display: flex;
align-items: stretch;
}
/* ── 其他图表 ── */
.charts-scroll {
height: 100%;
overflow-y: auto;
padding: 4px;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
/* 一行两图 */
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
align-content: start;
.chart-box { margin-bottom: 0; }
}
.chart-box {
width: 100%;
height: 200px;
margin-bottom: 8px;
}
.chart-box-tall { height: 480px; }
/* ── 查找面板 ── */
.search-panel {
width: 210px;
flex-shrink: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
padding: 10px;
display: flex;
flex-direction: column;
gap: 14px;
overflow-y: auto;
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: #303133;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
}
.search-type-group { display: flex; flex-direction: column; gap: 8px; }
.search-field {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 20px;
}
.search-label { font-size: 11px; color: #909399; }
.search-actions {
display: flex;
gap: 8px;
margin-top: auto;
}
.no-data-hint {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 120px;
color: #c0c4cc;
font-size: 13px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,11 @@
<span slot="title">品质</span>
</el-menu-item>
<el-menu-item index="performance">
<i class="el-icon-coin"></i>
<i class="el-icon-date"></i>
<span slot="title">计划</span>
</el-menu-item>
<el-menu-item index="actualPerformance">
<i class="el-icon-data-analysis"></i>
<span slot="title">实绩</span>
</el-menu-item>
<el-menu-item index="rollConfig">
@@ -42,13 +46,9 @@
<i class="el-icon-warning"></i>
<span slot="title">停机</span>
</el-menu-item>
<!-- <el-menu-item index="realTime">
<i class="el-icon-time"></i>
<span slot="title">实时</span>
</el-menu-item> -->
</el-menu>
</div>
<div style="flex: 1;">
<div style="flex: 1; overflow: hidden;">
<component :is="currentComponent" />
</div>
</div>
@@ -61,7 +61,7 @@ import Report from './components/Report.vue';
import Shipping from './components/Shipping.vue';
import Quality from './components/Quality.vue';
import Performance from './components/Performance.vue';
import RealTime from './components/RealTime.vue';
import ActualPerformance from './components/ActualPerformance.vue';
import RollConfig from '@/views/timing/roll/index.vue';
import RollHistory from '@/views/timing/roll/history.vue';
import Stoppage from '@/views/timing/stoppage/index.vue';
@@ -75,7 +75,7 @@ export default {
Shipping,
Quality,
Performance,
RealTime,
ActualPerformance,
RollConfig,
RollHistory,
Stoppage
@@ -94,7 +94,7 @@ export default {
shipping: 'Shipping',
quality: 'Quality',
performance: 'Performance',
realTime: 'RealTime',
actualPerformance: 'ActualPerformance',
rollConfig: 'RollConfig',
rollHistory: 'RollHistory',
stoppage: 'Stoppage',

View File

@@ -766,7 +766,7 @@ import { listTransferOrderItem } from '@/api/wms/transferOrderItem'
import { listCoilQualityRejudge } from '@/api/wms/coilQualityRejudge'
// 引入 ECharts 和 L2 时序数据 API
import * as echarts from 'echarts'
import { getTimingSegByEncoilId } from '@/api/l2/timing'
import { getTimingSegByEncoilId, getTimingPlanDetailByHotcoilId } from '@/api/l2/timing'
import AbnormalTable from '@/views/wms/coil/components/AbnormalTable.vue';
import FileList from "@/components/FileList";
@@ -1176,11 +1176,16 @@ export default {
},
// 加载生产数据
async loadProductionData() {
const encoilId = this.coilInfo.currentCoilNo;
if (!encoilId) return;
const hotCoilId = this.coilInfo.enterCoilNo;
if (!hotCoilId) return;
this.perfLoading = true;
try {
// 先查询详情
const detail = await getTimingPlanDetailByHotcoilId(hotCoilId);
const encoilId = detail?.data?.firstRow?.coilid || '';
if (!encoilId) return;
const res = await getTimingSegByEncoilId(encoilId);
const series = res?.data?.series || null;
const rows = res?.data?.rows || [];

View File

@@ -607,6 +607,178 @@
</div>
</div>
</el-dialog>
<!-- 钢卷暂存单据管理 -->
<el-card class="box-card" style="margin-top: 20px;" :body-style="{ padding: '20px' }">
<div slot="header" class="clearfix">
<span>钢卷暂存单据管理</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="createTempOrder">创建暂存单据</el-button>
</div>
<!-- 暂存单据列表 -->
<el-table :data="tempOrderList" style="width: 100%" :height="'calc(100vh - 500px)'">
<el-table-column prop="orderName" label="单据名称">
<template slot-scope="scope">
<div style="display: flex; align-items: center; gap: 5px;">
<el-link type="primary" @click="viewSelectedCoils(scope.row)">{{ scope.row.orderName }}</el-link>
<el-button size="mini" type="text" icon="el-icon-edit" @click="editOrderName(scope.row)"></el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="coilCount" label="钢卷数量">
<template slot-scope="scope">
<el-tag size="small">{{ (scope.row.coils && scope.row.coils.length) ? scope.row.coils.length : 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="totalWeight" label="总重量(t)">
<template slot-scope="scope">
{{ calculateTotalWeight(scope.row.coils || []).toFixed(3) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openCoilSelection(scope.row)">选择钢卷</el-button>
<el-button size="mini" type="text" style="color: #f56c6c;" @click="deleteTempOrder(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 当没有数据时显示提示 -->
<div v-if="!tempOrderList || tempOrderList.length === 0" style="text-align: center; padding: 50px; color: #909399;">
<i class="el-icon-document" style="font-size: 48px; margin-bottom: 10px;"></i>
<div>暂无暂存单据</div>
<div style="font-size: 12px; margin-top: 5px;">点击右上角"创建暂存单据"开始使用</div>
</div>
</el-card>
<!-- 钢卷选择对话框 -->
<el-dialog title="选择钢卷" :visible.sync="coilSelectionVisible" width="95%" append-to-body>
<!-- 上方查询条件 -->
<el-card class="query-card" style="margin-bottom: 15px;">
<el-form :model="coilQueryParams" size="small" :inline="true">
<el-form-item label="入场卷号">
<el-input v-model="coilQueryParams.enterCoilNo" placeholder="请输入入场钢卷号" clearable style="width: 200px;" />
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="coilQueryParams.currentCoilNo" placeholder="请输入当前钢卷号" clearable style="width: 200px;" />
</el-form-item>
<el-form-item label="产品名称">
<el-input v-model="coilQueryParams.itemName" placeholder="请选择物料" clearable style="width: 200px;" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="coilQueryParams.itemSpecification" placeholder="请选择规格" clearable style="width: 200px;" />
</el-form-item>
<el-form-item label="材质">
<el-input v-model="coilQueryParams.itemMaterial" placeholder="请选择材质" clearable style="width: 200px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchCoils">查询</el-button>
<el-button @click="resetCoilQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 下方钢卷列表 -->
<div style="height: 500px;">
<el-table v-loading="coilLoading" :data="availableCoils" @selection-change="handleCoilSelection" border height="450">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.enterCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo">
<template slot-scope="scope">
<current-coil-no :current-coil-no="scope.row.currentCoilNo"></current-coil-no>
</template>
</el-table-column>
<el-table-column label="净重" align="center" prop="netWeight" />
<el-table-column label="逻辑库位" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="产品类型" align="center" width="180">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row" />
<RawMaterialInfo v-else-if="scope.row.itemType === 'raw_material'" :material="scope.row" />
</template>
</el-table-column>
<el-table-column label="规格" prop="specification"></el-table-column>
<el-table-column label="物料" prop="itemName"></el-table-column>
<el-table-column label="材质" prop="material"></el-table-column>
<el-table-column label="厂家" prop="manufacturer"></el-table-column>
<el-table-column label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column label="品质" prop="qualityStatus"></el-table-column>
<el-table-column label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0" type="info" size="mini">未发货</el-tag>
<el-tag v-else type="success" size="mini">已发货</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
</el-table>
<div style="margin-top: 10px; text-align: center;">
<el-pagination
@size-change="handleCoilSizeChange"
@current-change="handleCoilCurrentChange"
:current-page="coilQueryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="coilQueryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="coilTotal">
</el-pagination>
</div>
</div>
<div slot="footer" class="dialog-footer">
<div style="margin-bottom: 10px;">
<span>已选择 {{ selectedCoils.length }} 个钢卷总重量{{ calculateSelectedWeight().toFixed(3) }}t</span>
</div>
<el-button @click="coilSelectionVisible = false">取消</el-button>
<el-button type="primary" @click="saveSelectedCoils">确认选择</el-button>
</div>
</el-dialog>
<!-- 已选钢卷列表对话框 -->
<el-dialog title="已选钢卷列表" :visible.sync="selectedCoilsVisible" width="90%" append-to-body>
<el-table :data="currentTempOrder ? (currentTempOrder.coils || []) : []" border>
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.enterCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo">
<template slot-scope="scope">
<current-coil-no :current-coil-no="scope.row.currentCoilNo"></current-coil-no>
</template>
</el-table-column>
<el-table-column label="净重" align="center" prop="netWeight" />
<el-table-column label="逻辑库位" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="产品类型" align="center" width="180">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row" />
<RawMaterialInfo v-else-if="scope.row.itemType === 'raw_material'" :material="scope.row" />
</template>
</el-table-column>
<el-table-column label="规格" prop="specification"></el-table-column>
<el-table-column label="物料" prop="itemName"></el-table-column>
<el-table-column label="材质" prop="material"></el-table-column>
<el-table-column label="厂家" prop="manufacturer"></el-table-column>
<el-table-column label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column label="品质" prop="qualityStatus"></el-table-column>
<el-table-column label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="100">
<template slot-scope="scope">
<el-button size="mini" type="text" style="color: #f56c6c;" @click="removeCoilFromOrder(scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
@@ -994,6 +1166,31 @@ export default {
// 调拨记录弹窗
transferRecordVisible: false,
transferRecordList: [],
// 暂存单据管理相关数据
tempOrderList: [],
currentTempOrder: null,
coilSelectionVisible: false,
selectedCoilsVisible: false,
coilLoading: false,
availableCoils: [],
selectedCoils: [],
coilTotal: 0,
coilQueryParams: {
pageNum: 1,
pageSize: 20,
enterCoilNo: '',
currentCoilNo: '',
itemName: '',
itemSpecification: '',
itemMaterial: '',
status: 0,
orderBy: true,
dataType: 1,
materialType: '成品',
itemType: 'product',
excludeBound: true,
selectType: 'product'
},
};
},
computed: {
@@ -1021,8 +1218,257 @@ export default {
this.warehouseIds = this.warehouseOptions.map(item => item.value).join(',');
}
this.getList();
// 初始化暂存单据列表
this.loadTempOrderList();
},
methods: {
// === 暂存单据管理相关方法 ===
// 加载暂存单据列表
loadTempOrderList() {
const savedOrders = localStorage.getItem('tempCoilOrders');
if (savedOrders) {
this.tempOrderList = JSON.parse(savedOrders);
} else {
this.tempOrderList = [];
}
},
// 保存暂存单据列表到本地存储
saveTempOrderList() {
localStorage.setItem('tempCoilOrders', JSON.stringify(this.tempOrderList));
},
// 创建暂存单据
createTempOrder() {
const orderName = `暂存单据_${new Date().toLocaleString().replace(/[/:]/g, '-')}`;
const newOrder = {
orderId: Date.now().toString(),
orderName: orderName,
createTime: new Date().toLocaleString(),
coils: []
};
this.tempOrderList.unshift(newOrder);
this.saveTempOrderList();
this.$message.success('暂存单据创建成功');
},
// 删除暂存单据
deleteTempOrder(order) {
this.$confirm(`确认删除暂存单据"${order.orderName}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.tempOrderList.findIndex(item => item.orderId === order.orderId);
if (index > -1) {
this.tempOrderList.splice(index, 1);
this.saveTempOrderList();
this.$message.success('删除成功');
}
}).catch(() => {
this.$message.info('已取消删除');
});
},
// 查看已选钢卷列表
viewSelectedCoils(order) {
this.currentTempOrder = order;
this.selectedCoilsVisible = true;
},
// 编辑单据名称
editOrderName(order) {
this.$prompt('请输入新的单据名称', '编辑单据名称', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: order.orderName,
inputValidator: (value) => {
if (!value || value.trim() === '') {
return '单据名称不能为空';
}
return true;
}
}).then(({ value }) => {
const newName = value.trim();
// 检查名称是否重复
const duplicateOrder = this.tempOrderList.find(item =>
item.orderId !== order.orderId && item.orderName === newName
);
if (duplicateOrder) {
this.$message.warning('单据名称已存在,请使用其他名称');
return;
}
// 更新单据名称
order.orderName = newName;
this.saveTempOrderList();
this.$message.success('单据名称修改成功');
}).catch(() => {
this.$message.info('已取消编辑');
});
},
// 打开钢卷选择对话框
openCoilSelection(order) {
this.currentTempOrder = order;
this.coilSelectionVisible = true;
this.searchCoils();
},
// 计算总重量
calculateTotalWeight(coils) {
if (!coils || coils.length === 0) return 0;
return coils.reduce((total, coil) => {
return total + (parseFloat(coil.netWeight) || 0);
}, 0);
},
// 计算选中钢卷的总重量
calculateSelectedWeight() {
return this.calculateTotalWeight(this.selectedCoils);
},
// 查询钢卷
async searchCoils() {
this.coilLoading = true;
try {
// 构建查询参数
const params = { ...this.coilQueryParams };
// 移除空值参数
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key];
}
});
// 调用接口查询钢卷
const response = await listMaterialCoil(params);
this.availableCoils = response.rows || [];
this.coilTotal = response.total || 0;
// 过滤掉所有单据中已选择的钢卷,确保钢卷在所有单据中唯一
const allExistingCoilIds = [];
this.tempOrderList.forEach(order => {
if (order.coils && Array.isArray(order.coils)) {
order.coils.forEach(coil => {
if (coil.coilId) {
allExistingCoilIds.push(coil.coilId);
}
});
}
});
// 如果是当前正在编辑的单据排除当前单据中的钢卷因为它们已经在allExistingCoilIds中了
// 这样可以确保其他单据不能选择当前单据已选的钢卷
this.availableCoils = this.availableCoils.filter(coil => !allExistingCoilIds.includes(coil.coilId));
} catch (error) {
console.error('查询钢卷失败:', error);
this.$message.error('查询钢卷失败');
} finally {
this.coilLoading = false;
}
},
// 重置钢卷查询条件
resetCoilQuery() {
this.coilQueryParams = {
pageNum: 1,
pageSize: 20,
enterCoilNo: '',
currentCoilNo: '',
itemName: '',
itemSpecification: '',
itemMaterial: '',
status: 0,
orderBy: true,
dataType: 1,
materialType: '成品',
itemType: 'product',
excludeBound: true,
selectType: 'product'
};
this.searchCoils();
},
// 处理钢卷选择变化
handleCoilSelection(selection) {
this.selectedCoils = selection;
},
// 保存选中的钢卷
saveSelectedCoils() {
if (!this.currentTempOrder) {
this.$message.error('请先选择暂存单据');
return;
}
if (this.selectedCoils.length === 0) {
this.$message.warning('请选择要添加的钢卷');
return;
}
// 确保coils数组存在
if (!this.currentTempOrder.coils) {
this.currentTempOrder.coils = [];
}
// 由于在查询阶段已经过滤了所有重复的钢卷,这里不需要再次检查
// 但为了安全起见,保留基本的检查逻辑
if (this.currentTempOrder.coils && this.currentTempOrder.coils.length > 0) {
const existingCoilIds = this.currentTempOrder.coils.map(coil => coil.coilId);
const duplicateCoils = this.selectedCoils.filter(coil => existingCoilIds.includes(coil.coilId));
if (duplicateCoils.length > 0) {
this.$message.warning(`钢卷 ${duplicateCoils.map(coil => coil.enterCoilNo).join(', ')} 已存在,不能重复添加`);
return;
}
}
// 添加钢卷到当前单据
this.currentTempOrder.coils.push(...this.selectedCoils);
this.saveTempOrderList();
this.$message.success(`成功添加 ${this.selectedCoils.length} 个钢卷`);
// 关闭对话框并重置选择
this.coilSelectionVisible = false;
this.selectedCoils = [];
},
// 从单据中移除钢卷
removeCoilFromOrder(index) {
if (!this.currentTempOrder || !this.currentTempOrder.coils || !this.currentTempOrder.coils[index]) return;
this.$confirm('确认移除该钢卷吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.currentTempOrder.coils.splice(index, 1);
this.saveTempOrderList();
this.$message.success('移除成功');
}).catch(() => {
this.$message.info('已取消移除');
});
},
// 钢卷分页相关方法
handleCoilSizeChange(val) {
this.coilQueryParams.pageSize = val;
this.coilQueryParams.pageNum = 1;
this.searchCoils();
},
handleCoilCurrentChange(val) {
this.coilQueryParams.pageNum = val;
this.searchCoils();
},
// 进入数字钢卷页面
handleNumberCoilClick(row) {
this.$router.push({

View File

@@ -47,6 +47,15 @@
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="单据状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择完成状态" @change="handleQuery">
<el-option label="已发货" :value="1" />
<el-option label="未发货" :value="0" />
<el-option label="已打印" :value="2" />
<el-option label="未打印" :value="3" />
</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>

View File

@@ -1,601 +1,392 @@
<template>
<div class="spec-page">
<!-- 规程类型 -->
<div class="dict-toolbar-row">
<div class="type-tab-bar">
<span
v-for="t in specTypeTab"
:key="'stype-' + (t.value === '' ? 'all' : t.value)"
:class="['type-tab', { active: activeSpecType === t.value }]"
@click="switchSpecType(t.value)"
>{{ t.label }}</span>
</div>
<dict-select
toolbar-only
:kisv="false"
:editable="true"
:refresh="false"
:dict-type="DICT_SPEC_TYPE"
panel-title="规程工艺类型字典"
@dict-updated="loadSpecTypeDict"
/>
<div class="spec-version-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<span class="page-title">规程版本管理</span>
<el-button
type="primary"
size="small"
icon="el-icon-plus"
style="margin-left:auto"
@click="openSpecDialog()"
>新建规程</el-button>
</div>
<!-- 产线 -->
<div class="dict-toolbar-row line-row">
<div class="line-tab-bar">
<span
:class="['line-tab', { active: activeLineId === '' }]"
@click="switchLine('')"
>全部</span>
<span
v-for="line in lineOptions"
:key="'ln-' + line.lineId"
:class="['line-tab', { active: lineTabActive(line) }]"
@click="switchLine(line.lineId)"
>{{ line.lineName }}</span>
</div>
<dict-select
toolbar-only
:kisv="false"
:editable="true"
:refresh="false"
:dict-type="DICT_LINE"
panel-title="规程产线筛选项字典值为产线 line_id"
@dict-updated="loadLineOptions"
/>
<!-- 规程列表 -->
<div class="section-wrapper">
<div class="section-title">规程列表</div>
<el-table
:data="specList"
size="small"
highlight-current-row
@row-click="onSpecRowClick"
:row-class-name="tableRowClassName"
>
<el-table-column label="规程编码" prop="specCode" width="150" />
<el-table-column label="规程名称" prop="specName" />
<el-table-column label="创建时间" prop="createTime" width="180" />
<el-table-column label="操作" align="right" width="180">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openSpecDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeSpec(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</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 class="section-wrapper" v-if="currentSpec">
<div class="section-title">
版本列表 - {{ currentSpec.specName }}
<el-button
type="primary"
size="mini"
icon="el-icon-plus"
@click="openVersionDialog()"
>新建版本</el-button>
</div>
<el-table
:data="versionList"
size="small"
highlight-current-row
@row-click="onVersionRowClick"
>
<el-table-column label="版本号" prop="versionCode" />
<el-table-column label="状态" prop="status" />
<el-table-column label="创建时间" prop="createTime" />
<el-table-column label="生效" align="center">
<template slot-scope="{ row }">
<el-switch
:value="row.isActive === 1"
active-color="#5F7BA0"
@click.native.stop
@change="handleActiveChange(row, $event)"
/>
</template>
</el-table-column>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openVersionDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeVersion(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!versionList.length && !versionLoading" description="暂无版本,请新建" style="padding:40px 0" />
</div>
<div v-else class="empty-hint">请选择一个规程查看其版本</div>
<!-- 列表 -->
<el-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="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>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 新增/修改 -->
<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-dialog :title="specTitle" :visible.sync="specOpen" width="500px" append-to-body @close="specForm = {}">
<el-form ref="specFormRef" :model="specForm" :rules="specRules" label-width="88px" size="small">
<el-form-item label="规程编码" prop="specCode">
<el-input v-model="specForm.specCode" placeholder="请输入规程编码" maxlength="64" />
</el-form-item>
<el-form-item label="规程名称" prop="specName">
<el-input v-model="form.specName" maxlength="200" show-word-limit />
<el-input v-model="specForm.specName" placeholder="请输入规程名称" maxlength="200" />
</el-form-item>
<el-form-item label="规程类型" prop="specType">
<el-select v-model="form.specType" style="width:100%">
<el-option
v-for="t in specTypeOptionsForForm"
:key="t.dictValue"
:label="t.dictLabel"
:value="t.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item label="产线" prop="lineId">
<el-select v-model="form.lineId" filterable placeholder="请选择" style="width:100%">
<el-option
v-for="line in lineOptionsForForm"
:key="line.lineId"
:label="lineOptLabel(line)"
:value="line.lineId"
/>
</el-select>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<el-input v-model="form.productType" maxlength="100" />
</el-form-item>
<el-form-item label="是否启用" prop="isEnabled">
<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" rows="2" maxlength="500" show-word-limit />
<el-form-item label="备注">
<el-input v-model="specForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="open = false">取消</el-button>
<el-button size="small" type="primary" :loading="btnLoading" @click="submitForm">确定</el-button>
<el-button size="small" @click="specOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="specSubmitLoading" @click="submitSpec">确定</el-button>
</div>
</el-dialog>
<!-- 新建/编辑版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="500px" append-to-body @close="versionForm = {}">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="88px" size="small">
<el-form-item label="版本号" prop="versionCode">
<el-input v-model="versionForm.versionCode" placeholder="如 V1.0" maxlength="64" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="versionForm.status" style="width:100%">
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
</el-select>
</el-form-item>
<el-form-item label="保存后生效">
<el-switch v-model="versionForm.isActive" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="versionForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<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>
</template>
<script>
import { getDicts } from '@/api/system/dict/data'
import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec'
import { listProductionLine } from '@/api/wms/productionLine'
const DICT_SPEC_TYPE = 'wms_process_spec_type'
const DICT_LINE = 'wms_process_spec_line'
const DEFAULT_SPEC_TYPES = [
]
import { listProcessSpec, getProcessSpec, addProcessSpec, updateProcessSpec, delProcessSpec } from '@/api/wms/processSpec'
import {
listProcessSpecVersion,
addProcessSpecVersion,
updateProcessSpecVersion,
delProcessSpecVersion,
activateProcessSpecVersion
} from '@/api/wms/processSpecVersion'
export default {
name: 'ProcessSpec',
name: 'SpecVersionManage',
data() {
return {
DICT_SPEC_TYPE,
DICT_LINE,
loading: false,
btnLoading: false,
total: 0,
dataList: [],
ids: [],
single: true,
multiple: true,
open: false,
dialogTitle: '',
specTypeRows: [],
lineOptions: [],
activeSpecType: '',
activeLineId: '',
queryParams: {
pageNum: 1,
pageSize: 20,
specName: undefined,
specType: undefined,
lineId: undefined
pageLoading: false,
specList: [],
currentSpec: null,
currentSpecId: null,
versionList: [],
versionLoading: false,
statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'],
// 规程相关
specOpen: false,
specTitle: '',
specSubmitLoading: false,
specForm: {},
specRules: {
specCode: [{ required: true, message: '规程编码不能为空', trigger: 'blur' }],
specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }]
},
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' }]
// 版本相关
versionOpen: false,
versionTitle: '',
versionSubmitLoading: false,
versionForm: {},
versionRules: {
versionCode: [{ required: true, message: '版本号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
},
computed: {
specTypeTab() {
const rows = this.mergeSpecTypeRowsWithDefaults()
const sorted = [...rows].sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
return [{ label: '全部', value: '' }, ...sorted.map(r => ({ label: r.dictLabel, value: r.dictValue }))]
},
specTypeOptionsForForm() {
const rows = this.mergeSpecTypeRowsWithDefaults()
return [...rows].sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
},
lineOptionsForForm() {
return this.lineOptions
}
},
created() {
Promise.all([this.loadSpecTypeDict(), this.loadLineOptions()]).finally(() => {
this.getList()
})
this.loadSpecs()
},
methods: {
/**
* 工艺类型:默认 PROCESS/STANDARD 与字典合并。若仅有字典中存在的新项,仍可保留两行基础兜底;同 dict_value 以字典为准(可改名、调 sort
*/
mergeSpecTypeRowsWithDefaults() {
const byVal = new Map()
DEFAULT_SPEC_TYPES.forEach(row => {
byVal.set(row.dictValue, { ...row })
})
for (const row of this.specTypeRows || []) {
if (!row || row.dictValue === undefined || row.dictValue === null || row.dictValue === '') continue
const v = String(row.dictValue)
const prev = byVal.get(v)
const sort = row.dictSort != null && row.dictSort !== ''
? Number(row.dictSort)
: (prev && prev.dictSort != null ? prev.dictSort : 999)
const label = (row.dictLabel != null && String(row.dictLabel).trim() !== '')
? row.dictLabel
: (prev && prev.dictLabel)
byVal.set(v, {
dictLabel: label,
dictValue: v,
dictSort: sort
})
}
return Array.from(byVal.values())
// 表格行样式
tableRowClassName({ row }) {
return row.specId === this.currentSpecId ? 'current-row' : ''
},
/**
* 产线 ID 与字典值:用数字字符串,避免超过 Number.MAX_SAFE_INTEGER 时精度丢失(雪花 id
*/
normalizeLineIdString(value) {
if (value === undefined || value === null || value === '') return ''
const s = String(value).trim()
return /^\d+$/.test(s) ? s : ''
},
/** Tab 高亮:雪花 id 一律按字符串比较 */
lineTabActive(line) {
if (this.activeLineId === '' || this.activeLineId === undefined) return false
return String(this.activeLineId) === String(line.lineId)
},
parseDictRows(res) {
const rows = (res.data || []).filter(d => d.status === '0' || d.status === undefined)
rows.sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
return rows
},
async loadSpecTypeDict() {
try {
const res = await getDicts(DICT_SPEC_TYPE)
this.specTypeRows = this.parseDictRows(res)
} catch (err) {
console.error('规程工艺类型字典加载失败', err)
this.specTypeRows = []
}
},
/**
* 产线 Tab产线主表 字典中合法数字 line_id字典独有也会显示 Tab
* line_id 全程用数字字符串,避免超过 Number.MAX_SAFE_INTEGER 时精度丢失;字典值须为数字 line_id。
*/
async loadLineOptions() {
const previousOptions = Array.isArray(this.lineOptions) && this.lineOptions.length
? this.lineOptions.map(o => ({ ...o }))
: []
let dictRows = []
try {
const res = await getDicts(DICT_LINE)
dictRows = this.parseDictRows(res)
} catch (err) {
console.error('规程产线字典加载失败', err)
}
const dictMeta = new Map()
for (const d of dictRows) {
const idStr = this.normalizeLineIdString(d.dictValue)
if (!idStr) continue
const sort = Number(d.dictSort) || 0
const label = (d.dictLabel != null && String(d.dictLabel).trim() !== '')
? String(d.dictLabel).trim()
: idStr
dictMeta.set(idStr, { label, sort })
}
let tableLines = []
// try {
// const res = await listProductionLine({ pageNum: 1, pageSize: 500 })
// tableLines = (res.rows || []).map(p => {
// const idStr = this.normalizeLineIdString(p.lineId) || (p.lineId != null ? String(p.lineId).trim() : '')
// return {
// lineId: idStr,
// lineName: p.lineName,
// lineCode: p.lineCode
// }
// }).filter(p => p.lineId)
// } catch (e2) {
// console.error('产线列表加载失败', e2)
// }
const tableIdSet = new Set(tableLines.map(l => String(l.lineId)))
const next = []
for (const line of tableLines) {
const idStr = String(line.lineId)
const dm = dictMeta.get(idStr)
next.push({
lineId: idStr,
lineName: dm ? dm.label : line.lineName,
lineCode: line.lineCode
})
}
const dictOnly = []
for (const [idStr, meta] of dictMeta) {
if (!tableIdSet.has(idStr)) {
dictOnly.push({ lineId: idStr, lineName: meta.label, lineCode: undefined, _sort: meta.sort })
// 加载规程列表
loadSpecs() {
this.pageLoading = true
listProcessSpec({ pageNum: 1, pageSize: 500 }).then(res => {
this.specList = res.rows || []
if (this.specList.length > 0 && !this.currentSpec) {
this.selectSpec(this.specList[0])
}
}
dictOnly.sort((a, b) => a._sort - b._sort)
dictOnly.forEach(d => {
const { _sort, ...rest } = d
next.push(rest)
}).catch(e => console.error(e)).finally(() => { this.pageLoading = false })
},
// 选择规程
selectSpec(spec) {
this.currentSpec = spec
this.currentSpecId = spec.specId
this.loadVersions()
},
// 点击规程行
onSpecRowClick(row) {
this.selectSpec(row)
},
// 加载版本列表
loadVersions() {
if (!this.currentSpecId) return
this.versionLoading = true
listProcessSpecVersion({ specId: this.currentSpecId, pageNum: 1, pageSize: 200 }).then(res => {
this.versionList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.versionLoading = false })
},
// 点击版本行
onVersionRowClick(row) {
this.goPlanSpec(row)
},
// 跳转到方案详情
goPlanSpec(row) {
const basePath = this.$route.path.replace(/\/[^/]*$/, '')
console.log(basePath)
this.$router.push({
path: `/process/processSpec/planSpec`,
query: { specId: this.currentSpecId, versionId: String(row.versionId), versionCode: row.versionCode }
})
if (next.length === 0 && previousOptions.length > 0) {
console.warn('[规程产线] 本次未解析出有效筛选项(可能字典值非数字或产线接口异常),保留上一版 Tab')
},
// 生效切换
handleActiveChange(row, val) {
if (!val) {
this.$message.info('请激活其他版本来替换当前生效版本')
return
}
this.lineOptions = next
},
lineOptLabel(line) {
if (line.lineCode) {
return `${line.lineName}${line.lineCode}`
}
return line.lineName
},
getList() {
this.loading = true
listProcessSpec(this.queryParams).then(res => {
this.dataList = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
switchSpecType(val) {
this.activeSpecType = val
this.queryParams.specType = val || undefined
this.queryParams.pageNum = 1
this.getList()
},
switchLine(lineId) {
if (lineId === '' || lineId === undefined || lineId === null) {
this.activeLineId = ''
} else {
const s = this.normalizeLineIdString(lineId)
this.activeLineId = s || String(lineId).trim()
}
this.queryParams.lineId = this.activeLineId === '' ? undefined : this.activeLineId
this.queryParams.pageNum = 1
this.getList()
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams.specName = undefined
this.handleQuery()
},
handleSelectionChange(sel) {
this.ids = sel.map(r => r.specId)
this.single = sel.length !== 1
this.multiple = !sel.length
},
defaultSpecType() {
const first = this.specTypeOptionsForForm[0]
return first ? first.dictValue : 'PROCESS'
},
reset() {
this.form = {
specId: undefined,
specCode: undefined,
specName: undefined,
specType: this.defaultSpecType(),
lineId: undefined,
productType: undefined,
isEnabled: 1,
remark: undefined
}
this.$refs.form && this.$refs.form.clearValidate()
},
handleAdd() {
this.reset()
this.dialogTitle = '新增规程'
this.open = true
},
handleUpdate(row) {
this.reset()
const specId = row ? row.specId : this.ids[0]
getProcessSpec(specId).then(res => {
this.form = res.data || {}
if (this.form.lineId != null && this.form.lineId !== '') {
const sid = this.normalizeLineIdString(this.form.lineId)
this.form.lineId = sid || String(this.form.lineId).trim()
}
this.dialogTitle = '修改规程'
this.open = true
})
},
submitForm() {
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.open = false
this.getList()
}).finally(() => { this.btnLoading = false })
})
},
handleDelete(row) {
const ids = row ? row.specId : this.ids
this.$modal.confirm('确认删除所选规程?').then(() => {
this.loading = true
return delProcessSpec(ids)
this.$modal.confirm('确认将版本"' + row.versionCode + '"设为当前生效版本?').then(() => {
return activateProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
}).catch(() => {}).finally(() => { this.loading = false })
},
toggleEnabled(row, val) {
const updated = { ...row, isEnabled: val ? 1 : 0 }
updateProcessSpec(updated).then(() => {
row.isEnabled = updated.isEnabled
this.$modal.msgSuccess('已生效')
this.loadVersions()
}).catch(() => {})
},
goVersionManage(row) {
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(row.specId) } })
// 规程对话框
openSpecDialog(row) {
this.specForm = row
? { ...row }
: { specCode: undefined, specName: undefined, remark: undefined }
this.specTitle = row ? '编辑规程' : '新建规程'
this.specOpen = true
this.$nextTick(() => this.$refs.specFormRef && this.$refs.specFormRef.clearValidate())
},
// 提交规程
submitSpec() {
this.$refs.specFormRef.validate(ok => {
if (!ok) return
this.specSubmitLoading = true
const req = this.specForm.specId ? updateProcessSpec(this.specForm) : addProcessSpec(this.specForm)
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.specOpen = false
this.loadSpecs()
}).catch(e => console.error(e)).finally(() => { this.specSubmitLoading = false })
})
},
// 删除规程
removeSpec(row) {
this.$modal.confirm('确认删除规程"' + row.specName + '"').then(() => {
return delProcessSpec(row.specId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.currentSpecId === row.specId) {
this.currentSpec = null
this.currentSpecId = null
this.versionList = []
}
this.loadSpecs()
}).catch(() => {})
},
// 版本对话框
openVersionDialog(row) {
this.versionForm = row
? { ...row }
: { specId: this.currentSpecId, 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.currentSpecId })
: addProcessSpecVersion({ ...this.versionForm, specId: this.currentSpecId })
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.versionOpen = false
this.loadVersions()
}).catch(e => console.error(e)).finally(() => { this.versionSubmitLoading = false })
})
},
// 删除版本
removeVersion(row) {
this.$modal.confirm('确认删除版本"' + row.versionCode + '"').then(() => {
return delProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadVersions()
}).catch(() => {})
}
}
}
</script>
<style scoped>
.spec-page {
.spec-version-page {
padding: 16px 20px;
min-height: 100%;
}
/* ── 双色主题:默认=白底灰边,激活/主操作=深藏青 #5F7BA0 ── */
.dict-toolbar-row {
.page-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-bottom: 6px;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.dict-toolbar-row.line-row {
.page-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.section-wrapper {
background: #fff;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
border: 1px solid #ebeef5;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
}
.type-tab-bar {
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
gap: 0;
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;
flex: 0 1 auto;
align-items: center;
gap: 6px;
padding: 10px 0;
min-width: 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;
.empty-hint {
text-align: center;
padding: 60px 0;
color: #909399;
font-size: 14px;
}
.el-table {
border-radius: 4px;
overflow: hidden;
border-radius: 0;
}
::v-deep .el-table .current-row {
background: #f0f7ff !important;
}
/* 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: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) {
::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):not(.el-button--info):hover {
::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: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; }
/* 与 Tab 同一行时,将齿轮框配色贴近规程主题 */
::v-deep .dict-toolbar-row .el-icon-setting {
color: #5F7BA0;
}
</style>

View File

@@ -211,7 +211,7 @@
</el-dialog>
<!-- 模板导入 dialog -->
<el-dialog title="模板导入" :visible.sync="importOpen" width="800px" append-to-body @close="resetImport">
<el-dialog title="模板导入" :visible.sync="importOpen" width="800px" append-to-body>
<div class="import-container">
<!-- 文件上传区域 -->
<div class="file-upload-area" :class="{ disabled: importStatus === 'processing' }">

View File

@@ -466,6 +466,7 @@ export default {
const res = await listPendingAction({ ...this.queryParams, actionType: this.actionType, actionStatus: 2 });
// 获取两层数据
const lossIds = res.rows.filter(item => item.coilId).map(item => item.coilId);
const lossActionIds = res.rows.filter(item => item.actionId).map(item => item.actionId);
// 使用new Set去重
const outIds = [...new Set(res.rows.filter(item => item.processedCoilIds).map(item => item.processedCoilIds))];
@@ -479,7 +480,7 @@ export default {
}
const [lossRes, outRes] = await Promise.all([
listCoilWithIds({ ...this.queryParams, coilIds: lossIds.join(',') || '', startTime: '', endTime: '' }),
listCoilWithIds({ ...this.queryParams, actionIds: lossActionIds.join(',') || '', startTime: '', endTime: '' }),
listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', startTime: '', endTime: '' }),
]);

View File

@@ -240,6 +240,7 @@ export default {
const res = await listPendingAction({ ...this.queryParams, actionType: this.actionType, actionStatus: 2 });
// 获取两层数据
const lossIds = res.rows.map(item => item.coilId);
const lossActionIds = res.rows.map(item => item.actionId);
// 使用new Set去重
const outIds = [...new Set(res.rows.map(item => item.processedCoilIds))];
@@ -253,7 +254,7 @@ export default {
}
const [lossRes, outRes] = await Promise.all([
listCoilWithIds({ ...this.queryParams, coilIds: lossIds.join(',') || '', startTime: '', endTime: '' }),
listCoilWithIds({ ...this.queryParams, actionIds: lossActionIds.join(',') || '', startTime: '', endTime: '' }),
listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', startTime: '', endTime: '' }),
]);
this.lossList = lossRes.rows.map(item => {