feat(合同管理): 新增钢卷与合同关联功能

- 添加钢卷与合同关联的API接口
- 在合卷、分条、打字等操作中增加合同选择组件
- 创建合同选择组件ContractSelect
- 在合同详情页新增生产成果展示页签
- 实现合同列表的本地存储功能
This commit is contained in:
2026-04-18 16:18:22 +08:00
parent 143764f7f8
commit af002b84d3
9 changed files with 414 additions and 109 deletions

View File

@@ -52,3 +52,14 @@ export function listOrderPackaging(orderId) {
method: 'get',
})
}
/**
* 查询今日订单
*/
export function listTodayOrder(query) {
return request({
url: '/crm/order/daily',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询钢卷与合同关联关系列表
export function listCoilContractRel(query) {
return request({
url: '/wms/coilContractRel/list',
method: 'get',
params: query
})
}
// 查询钢卷与合同关联关系详细
export function getCoilContractRel(relId) {
return request({
url: '/wms/coilContractRel/' + relId,
method: 'get'
})
}
// 新增钢卷与合同关联关系
export function addCoilContractRel(data) {
return request({
url: '/wms/coilContractRel',
method: 'post',
data: data
})
}
// 修改钢卷与合同关联关系
export function updateCoilContractRel(data) {
return request({
url: '/wms/coilContractRel',
method: 'put',
data: data
})
}
// 删除钢卷与合同关联关系
export function delCoilContractRel(relId) {
return request({
url: '/wms/coilContractRel/' + relId,
method: 'delete'
})
}

View File

@@ -0,0 +1,269 @@
<template>
<div>
<div style="display: flex; align-items: center; margin-bottom: 12px;">
<el-select v-model="selectedValue" placeholder="请选择合同" style="width: 100%">
<el-option v-for="item in contractList" :key="item.orderId" :value="item.orderId"
:label="item.contractCode" />
</el-select>
<!-- 编辑按钮点击打开弹窗 -->
<el-button v-if="mode == 'today'" @click="openSelectDialog" type="primary" size="small" style="margin-left: 8px; padding: 0 12px;">
<i class="el-icon-setting"></i>
</el-button>
<!-- 刷新时不会移除手动添加的合同 -->
<el-button @click="handleRefresh" type="info" size="small" style="margin-left: 8px; padding: 0 12px;">
<i class="el-icon-refresh"></i>
</el-button>
</div>
<!-- 查询所有的合同和当前localstorage中的合同可以对localstorage中的合同进行新增和删除手动新增的合同带有手动添加的标记 -->
<el-dialog title="可选合同配置" :visible.sync="dialogVisible" width="80%">
<el-tabs v-model="activeTab">
<!-- 已配置合同tab -->
<el-tab-pane label="可选合同" name="configured">
<div v-if="contractList.length === 0" style="text-align: center; padding: 20px;">
暂无已配置合同
</div>
<el-table v-else :data="contractList" style="width: 100%" size="mini" height="600">
<el-table-column prop="contractCode" label="合同编号" width="160" />
<el-table-column prop="contractName" label="合同名称" width="180" />
<el-table-column prop="customer" label="客户" width="140" />
<el-table-column prop="salesman" label="销售人员" width="100" />
<el-table-column prop="deliveryDate" label="交付日期" width="110">
<template slot-scope="scope">
{{ scope.row.deliveryDate || '-' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
<el-table-column label="添加方式" width="90">
<template slot-scope="scope">
<el-tag v-if="scope.row.isManual" type="success" size="small">手动</el-tag>
<el-tag v-else type="info" size="small">接口</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="70">
<template slot-scope="scope">
<el-button type="danger" size="mini" @click="removeContract(scope.row.orderId)" plain>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 所有合同tab -->
<el-tab-pane label="所有合同" name="all">
<div style="margin-bottom: 16px;">
<el-input v-model="searchKeyword" placeholder="搜索合同" @input="handleSearch" clearable size="small">
<el-button slot="append" icon="el-icon-search" size="small"></el-button>
</el-input>
</div>
<el-table :data="allContracts" style="width: 100%" size="mini" height="600">
<el-table-column prop="contractCode" label="合同编号" width="160" />
<el-table-column prop="contractName" label="合同名称" width="180" />
<el-table-column prop="customer" label="客户" width="140" />
<el-table-column prop="salesman" label="销售人员" width="100" />
<el-table-column prop="deliveryDate" label="交付日期" width="110">
<template slot-scope="scope">
{{ scope.row.deliveryDate || '-' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
<el-table-column label="状态" width="80">
<template slot-scope="scope">
<el-tag v-if="isContractInList(scope.row.orderId)" type="success" size="small">已添加</el-tag>
<el-tag v-else type="info" size="small">未添加</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button
v-if="!isContractInList(scope.row.orderId)"
type="primary"
size="mini"
@click="addContract(scope.row)"
plain
>
添加
</el-button>
<el-button
v-else
type="danger"
size="mini"
@click="removeContract(scope.row.orderId)"
plain
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="allContracts.length === 0" style="text-align: center; padding: 20px;">
暂无合同数据
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script>
import { listOrder, listTodayOrder } from '@/api/crm/order';
export default {
name: "ContractSelect",
props: {
value: {
type: String,
default: ""
},
mode: {
type: String,
default: "today" // today 或 all
}
},
data() {
return {
contractList: [],
dialogVisible: false,
searchKeyword: '',
allContracts: [],
activeTab: 'configured', // 默认显示已配置合同tab
}
},
computed: {
selectedValue: {
get() {
return this.value;
},
set(val) {
this.$emit('input', val);
}
}
},
mounted() {
if (this.mode == "today") {
// 从localstorage中获取合同列表
this.loadFromLocalStorage();
} else {
this.loadContractList();
}
},
methods: {
// 从localStorage中加载合同列表
loadFromLocalStorage() {
try {
const storedContracts = localStorage.getItem('todayContracts');
if (storedContracts) {
this.contractList = JSON.parse(storedContracts);
} else {
// 如果localStorage中没有从接口获取
this.loadContractList();
}
} catch (error) {
console.error('Failed to load contracts from localStorage:', error);
this.loadContractList();
}
},
// 保存合同列表到localStorage
saveToLocalStorage() {
try {
localStorage.setItem('todayContracts', JSON.stringify(this.contractList));
} catch (error) {
console.error('Failed to save contracts to localStorage:', error);
}
},
// 加载合同列表
async loadContractList(keyword) {
if (this.mode == "all") {
const res = await listOrder({
pageNum: 1,
pageSize: 1000,
keyword: keyword || undefined,
});
this.contractList = res.rows || [];
}
else if (this.mode == "today") {
const res = await listTodayOrder();
// 获取现有手动添加的合同
const existingManualContracts = this.contractList.filter(item => item.isManual);
// 将接口返回的合同标记为非手动添加
const apiContracts = (res.data || []).map(item => ({
...item,
isManual: false
}));
// 合并合同列表,保留手动添加的合同
this.contractList = [...apiContracts, ...existingManualContracts];
// 去重,避免重复合同
this.contractList = this.contractList.filter((item, index, self) =>
index === self.findIndex(t => t.orderId === item.orderId)
);
// 保存到localStorage
this.saveToLocalStorage();
}
},
// 打开选择弹窗
async openSelectDialog() {
this.dialogVisible = true;
// 加载所有合同供选择
await this.loadAllContracts();
},
// 加载所有合同
async loadAllContracts() {
try {
const res = await listOrder({
pageNum: 1,
pageSize: 1000,
keyword: this.searchKeyword || undefined,
});
// 合并现有合同(包括手动添加的)
const existingContracts = this.contractList;
this.allContracts = [...res.rows || [], ...existingContracts].filter((item, index, self) =>
index === self.findIndex(t => t.orderId === item.orderId)
);
} catch (error) {
console.error('Failed to load all contracts:', error);
this.allContracts = [];
}
},
// 搜索合同
handleSearch() {
this.loadAllContracts();
},
// 检查合同是否在列表中
isContractInList(orderId) {
return this.contractList.some(item => item.orderId === orderId);
},
// 添加合同
addContract(contract) {
if (!this.isContractInList(contract.orderId)) {
this.contractList.push({
...contract,
isManual: true // 标记为手动添加
});
this.saveToLocalStorage();
}
},
// 移除合同
removeContract(orderId) {
this.contractList = this.contractList.filter(item => item.orderId !== orderId);
this.saveToLocalStorage();
},
// 刷新合同列表
handleRefresh() {
this.loadContractList();
},
}
}
</script>

View File

@@ -20,6 +20,12 @@
<!-- 订单异议内容 -->
<OrderObjection :order="currentOrder" />
</div>
</el-tab-pane>
<el-tab-pane label="生产成果" name="product">
<div class="order-record" v-if="activeTab === 'product'">
<!-- 生产成果内容 -->
<CoilTable :data="productList || []" />
</div>
</el-tab-pane>
<el-tab-pane label="发货配卷" name="coil">
<div class="order-record" v-if="activeTab === 'coil'">
@@ -99,6 +105,10 @@ export default {
type: Array,
default: () => []
},
productList: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false

View File

@@ -24,7 +24,7 @@
<!-- 右侧下方Tab标签页 -->
<div class="tab-panel" ref="tabPanel" style="flex: 1; overflow-y: auto;">
<ContractTabs :orderId="form.orderId" :deliveryWaybillList="wmsDeliveryWaybills" :coilList="coilList" :contract-attachment="form.businessAnnex" :technical-agreement="form.techAnnex"
:other-attachment="form.productionSchedule" :currentOrder="form" />
:other-attachment="form.productionSchedule" :currentOrder="form" :productList="form.coilList" />
</div>
</div>
<div v-else style="flex: 1; display: flex; flex-direction: column;">

View File

@@ -252,6 +252,12 @@
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="关联合同" prop="contractId">
<ContractSelect v-model="targetCoil.contractId" placeholder="请选择合同" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="异常信息" class="form-item-full">
<div class="abnormal-container">
@@ -301,6 +307,8 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm';
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect";
import { addCoilContractRel } from "@/api/wms/coilContractRel";
export default {
name: 'MergeCoil',
@@ -311,7 +319,8 @@ export default {
ProductSelector,
WarehouseSelect,
TimeInput,
AbnormalForm
AbnormalForm,
ContractSelect
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {
@@ -752,11 +761,17 @@ export default {
text: '正在合卷,请稍后...',
background: 'rgba(0, 0, 0, 0.7)'
});
await mergeMaterialCoil(mergeData);
const response = await mergeMaterialCoil(mergeData);
const coilId = response.data;
addCoilContractRel({
coilId: coilId,
contractId: this.targetCoil.contractId,
})
this.$message.success('合卷保存成功');
// 延迟返回,让用户看到成功提示
setTimeout(() => {
this.$router.back();

View File

@@ -187,7 +187,8 @@
<MemoInput storageKey="coatingType" v-model="splitForm.coatingType" placeholder="请输入镀层种类" />
</el-form-item>
<el-form-item label="钢卷表面处理" prop="coilSurfaceTreatment">
<MemoInput storageKey="surfaceTreatmentDesc" v-model="splitForm.coilSurfaceTreatment" placeholder="请输入钢卷表面处理" />
<MemoInput storageKey="surfaceTreatmentDesc" v-model="splitForm.coilSurfaceTreatment"
placeholder="请输入钢卷表面处理" />
</el-form-item>
<el-form-item label="生产开始时间" prop="productionStartTime">
<TimeInput v-model="splitForm.productionStartTime" @input="calculateProductionDuration" />
@@ -203,6 +204,10 @@
<el-input v-model="splitForm.remark" placeholder="请输入备注" type="textarea" />
</el-form-item>
<el-form-item label="关联合同" prop="contractId">
<ContractSelect v-model="splitForm.contractId" placeholder="请选择合同" />
</el-form-item>
<el-form-item label="异常信息">
<div class="abnormal-container">
<div v-for="(abnormal, index) in abnormals" :key="index" class="abnormal-item"
@@ -271,7 +276,8 @@
<el-descriptions-item label="调制度">{{ selectedSplitItem.temperGrade || '-' }}</el-descriptions-item>
<el-descriptions-item label="镀层种类">{{ selectedSplitItem.coatingType || '-' }}</el-descriptions-item>
<el-descriptions-item label="钢卷表面处理">{{ selectedSplitItem.coilSurfaceTreatment || '-' }}</el-descriptions-item>
<el-descriptions-item label="钢卷表面处理">{{ selectedSplitItem.coilSurfaceTreatment || '-'
}}</el-descriptions-item>
<el-descriptions-item label="生产开始时间">{{ selectedSplitItem.productionStartTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="生产结束时间">{{ selectedSplitItem.productionEndTime || '-'
@@ -316,6 +322,8 @@ import AbnormalForm from '../components/AbnormalForm';
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import ContractSelect from "@/components/KLPService/ContractSelect";
import { addCoilContractRel } from "@/api/wms/coilContractRel";
export default {
name: 'StepSplit',
@@ -346,6 +354,7 @@ export default {
AbnormalForm,
ProductInfo,
RawMaterialInfo,
ContractSelect,
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {
@@ -406,21 +415,6 @@ export default {
}
}, trigger: 'blur'
},
// 仅在新增的时候校验
// {
// validator: (rule, value, callback) => {
// // 没有coilId则为新增 触发校验
// checkCoilNo({ currentCoilNo: value, coilId: this.splitForm.coilId }).then(res => {
// const { duplicateType } = res.data;
// if (duplicateType === 'current' || duplicateType === 'both') {
// // alert('当前钢卷号重复,请重新输入');
// callback(new Error('当前钢卷号重复,请重新输入'));
// } else {
// callback();
// }
// })
// }, trigger: 'blur'
// }
],
materialType: [{ required: true, message: '请选择材料类型', trigger: 'change' }],
itemId: [{ required: true, message: '请选择成品/原料', trigger: 'change' }],
@@ -547,7 +541,6 @@ export default {
}
const action = await getPendingAction(this.actionId)
this.currentAction = action.data || {}
// this.$set(this.splitForm, 'productionStartTime', action.data.createTime)
const coilIds = action.data.remark;
console.log('coilIds', coilIds)
if (!coilIds) {
@@ -617,7 +610,7 @@ export default {
// 材料类型变更处理
handleMaterialTypeChange(val) {
// 清空物品选择
// 清空物品选择
this.splitForm.itemId = null;
// 根据材料类型设置物品类型
@@ -681,6 +674,11 @@ export default {
} else {
// 新增分条:调用创建接口
res = await createSpecialChild(this.coilId, this.actionId, splitData)
// 新增分条后,需要添加分条的合同关系
addCoilContractRel({
coilId: res.data.coilId,
contractId: this.splitForm.contractId,
})
}
this.$message.success(this.splitForm.coilId ? '编辑分条成功' : '新增分条成功')

View File

@@ -249,6 +249,11 @@
<el-form-item label="备注">
<el-input v-model="item.remark" placeholder="请输入备注" :disabled="readonly" />
</el-form-item>
<el-form-item label="关联合同" prop="contractId">
<ContractSelect v-model="item.contractId" placeholder="请选择合同" />
</el-form-item>
<el-form-item label="异常信息">
<div class="abnormal-container">
<div
@@ -316,6 +321,8 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm';
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect";
import { addCoilContractRel } from "@/api/wms/coilContractRel";
export default {
name: 'SplitCoil',
@@ -325,7 +332,8 @@ export default {
ProductSelect,
WarehouseSelect,
TimeInput,
AbnormalForm
AbnormalForm,
ContractSelect
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {
@@ -674,6 +682,16 @@ export default {
if (response.code === 200) {
this.$message.success('分条保存成功');
// 拿到多个子卷的coilId
const newCoilIds = response.msg.split(',');
// 为每个子卷添加合同关联
Promise.all(newCoilIds.map(async (coilId, index) => {
addCoilContractRel({
coilId,
contractId: this.splitList[index].contractId
});
}));
// 如果是从待操作列表进来的,标记操作为完成
if (this.actionId) {
await completeAction(this.actionId, response.msg);

View File

@@ -94,7 +94,7 @@
</div>
<div class="info-row">
<span class="info-label">逻辑库区</span>
<span class="info-value">{{ currentInfo.nextWarehouseName || '—' }}</span>
<span class="info-value">{{ currentInfo.warehouseName || '—' }}</span>
</div>
<div class="info-row" v-if="currentInfo.remark">
<span class="info-label">备注</span>
@@ -195,7 +195,7 @@
<el-form-item label="实测厚度(m)" prop="actualThickness" class="form-item-half">
<el-input-number :controls="false" v-model="updateForm.actualThickness" placeholder="请输入实测厚度"
type="number" :step="0.01" :disabled="readonly">
type="number" :step="0.01">
<template slot="append"></template>
</el-input-number>
</el-form-item>
@@ -251,6 +251,10 @@
show-word-limit />
</el-form-item>
<el-form-item label="关联合同" prop="contractId">
<ContractSelect v-model="updateForm.contractId" placeholder="请选择合同" />
</el-form-item>
<el-form-item label="异常信息">
<div class="abnormal-container">
<div
@@ -335,6 +339,8 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm';
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import { addCoilContractRel } from "@/api/wms/coilContractRel";
import ContractSelect from "@/components/KLPService/ContractSelect";
export default {
name: 'TypingCoil',
@@ -344,7 +350,8 @@ export default {
ProductSelect,
WarehouseSelect,
TimeInput,
AbnormalForm
AbnormalForm,
ContractSelect
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {
@@ -365,7 +372,6 @@ export default {
netWeight: undefined,
warehouseId: null,
warehouseId: null,
nextWarehouseName: '',
status: 0,
remark: '',
length: undefined,
@@ -449,8 +455,6 @@ export default {
{ required: true, message: '请选择逻辑库区', trigger: 'change' }
],
},
warehouseList: [],
historySteps: [],
actionId: null,
acidPrefill: {
visible: false,
@@ -458,10 +462,6 @@ export default {
title: ''
},
isAcidRolling: false,
// 原材料和产品列表(实时搜索,不再保存完整备份)
rawMaterialList: [],
productList: [],
itemSearchLoading: false,
// 酸连轧最近记录
acidRecentRecords: [],
// 异常信息
@@ -480,7 +480,8 @@ export default {
defectCode: null,
degree: null,
remark: null
}
},
contractList: [],
};
},
computed: {
@@ -502,26 +503,8 @@ export default {
}
return '请先选择材料类型';
},
// 当前物品列表(根据物品类型动态切换)
currentItemList() {
if (this.updateForm.itemType === 'raw_material') {
return this.rawMaterialList.map(item => ({
id: item.rawMaterialId,
name: this.formatItemName(item)
}));
} else if (this.updateForm.itemType === 'product') {
return this.productList.map(item => ({
id: item.productId,
name: this.formatItemName(item)
}));
}
return [];
}
},
async created() {
// 先加载库区列表
await this.loadWarehouses();
// 从路由参数获取coilId和actionId
const coilId = this.$route.query.coilId;
const actionId = this.$route.query.actionId;
@@ -645,12 +628,9 @@ export default {
// 根据材料类型设置物品类型
if (value === '成品') {
this.$set(this.updateForm, 'itemType', 'product');
// 清空列表,等待用户搜索
this.productList = [];
} else if (value === '原料') {
this.$set(this.updateForm, 'itemType', 'raw_material');
// 清空列表,等待用户搜索
this.rawMaterialList = [];
}
},
@@ -675,8 +655,6 @@ export default {
// 填充当前信息(左侧)
this.currentInfo = {
...data,
itemName: this.getItemName(data),
nextWarehouseName: this.getWarehouseName(data.warehouseId),
};
// 填充时间相关字段
@@ -698,14 +676,6 @@ export default {
}
},
// 获取物料名称
getItemName(data) {
if (data.itemName) {
return data.itemName;
}
return '';
},
// 获取物品类型文本
getItemTypeText(itemType) {
if (itemType === 'raw_material') return '原材料';
@@ -713,13 +683,6 @@ export default {
return '—';
},
// 获取库区名称
getWarehouseName(warehouseId) {
if (!warehouseId) return '';
const warehouse = this.warehouseList.find(w => w.warehouseId === warehouseId);
return warehouse ? warehouse.warehouseName : '';
},
// 格式化物品名称(添加规格和参数信息)
formatItemName(item) {
if (!item) return '';
@@ -754,41 +717,7 @@ export default {
return displayName;
},
// 加载库区列表
async loadWarehouses() {
try {
const response = await listWarehouse({ pageNum: 1, pageSize: 1000 });
if (response.code === 200) {
this.warehouseList = response.rows || response.data || [];
}
} catch (error) {
console.error('加载库区列表失败', error);
}
},
// 加载变更历史
async loadHistory() {
if (!this.currentInfo.enterCoilNo) {
return;
}
try {
this.historyLoading = true;
const response = await getMaterialCoilTrace({
enterCoilNo: this.currentInfo.enterCoilNo,
currentCoilNo: this.currentInfo.currentCoilNo || undefined
});
if (response.code === 200 && response.data) {
this.historySteps = response.data.steps || [];
}
} catch (error) {
console.error('加载变更历史失败', error);
} finally {
this.historyLoading = false;
}
},
// 复制当前信息到更新表单
copyFromCurrent() {
@@ -872,6 +801,15 @@ export default {
const response = await updateMaterialCoil(updateData);
// 更新完成后如果选定了合同,需要增加与合同的绑定关系
const coilId = response.msg;
if (this.updateForm.contractId) {
await addCoilContractRel({
coilId: coilId,
contractId: this.updateForm.contractId,
});
}
if (response.code === 200) {
this.$message.success('钢卷信息更新成功');
@@ -880,6 +818,8 @@ export default {
await completeAction(this.actionId, response.msg);
}
// 延迟返回
setTimeout(() => {
this.$router.back();