Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

# Conflicts:
#	klp-admin/src/main/resources/application-prod.yml
This commit is contained in:
2026-05-23 19:35:22 +08:00
85 changed files with 11908 additions and 843 deletions

View File

@@ -52,7 +52,8 @@
<KLPTable v-loading="loading" :data="receivableList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="应收ID" align="center" prop="receivableId" v-if="false"/>
<!-- <el-table-column label="客户" align="center" prop="customerName" /> -->
<el-table-column label="客户" align="center" prop="customerName" />
<el-table-column label="合同编号" align="center" prop="orderCode" />
<el-table-column label="收款日期" align="center" prop="dueDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.dueDate, '{y}-{m}-{d}') }}</span>
@@ -149,14 +150,18 @@ export default {
order: {
type: Object,
default: undefined
},
customerId: {
type: [Number, String],
default: undefined
}
},
computed: {
orderId() {
return this.order ? this.order.orderId : undefined;
},
customerId() {
return this.order ? this.order.customerId : undefined;
currentCustomerId() {
return this.customerId || (this.order ? this.order.customerId : undefined);
},
receivedAmount() {
return this.receivableList.reduce((total, item) => total + parseFloat(item.amount), 0);
@@ -174,6 +179,15 @@ export default {
}
},
immediate: true,
},
currentCustomerId: {
handler(newVal) {
if (newVal) {
this.queryParams.customerId = newVal;
this.getList();
}
},
immediate: true,
}
},
data() {
@@ -344,7 +358,7 @@ export default {
this.receiveForm = {
receivableId: row.receivableId,
amount: row.balanceAmount,
balanceAmount: row.balanceAmount
balanceAmount: row.balanceAmount
};
this.receiveOpen = true;
},

View File

@@ -0,0 +1,395 @@
<template>
<div class="app-container">
<!-- 筛选区域 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px" v-show="showSearch">
<el-form-item label="客户" prop="customerId">
<CustomerSelect v-model="queryParams.customerId" bindField="customerId" @change="handleCustomerChange"
:style="{ width: '240px' }" />
</el-form-item>
<el-form-item label="合同编号" prop="contractCode">
<el-select v-model="queryParams.contractCode" placeholder="请选择合同编号" filterable clearable style="width: 240px;">
<el-option v-for="item in contractOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 收款明细标题 -->
<el-descriptions title="收款明细"></el-descriptions>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar v-if="searchable" :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 收款明细表格 -->
<KLPTable v-loading="loading" :data="receivableList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="客户" align="center" prop="customerName" />
<el-table-column label="合同编号" align="center" prop="orderCode" />
<el-table-column label="收款日期" align="center" prop="dueDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.dueDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="应收金额" align="center" prop="amount" />
<el-table-column label="已收款" align="center" prop="paidAmount" />
<el-table-column label="未收款" align="center" prop="balanceAmount" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改应收款对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客户" prop="customerId">
<CustomerSelect v-model="form.customerId" bindField="customerId" @change="handleFormCustomerChange"
:style="{ width: '100%' }" />
</el-form-item>
<el-form-item label="合同编号" prop="contractCode">
<el-select v-model="form.contractCode" placeholder="请选择合同编号" filterable clearable style="width: 100%;">
<el-option v-for="item in formContractOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="收款日期" prop="dueDate">
<el-date-picker clearable v-model="form.dueDate" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择收款日期">
</el-date-picker>
</el-form-item>
<el-form-item label="收款金额" prop="amount">
<el-input-number :controls=false v-model="form.amount" :step="1.00"
:precision="2" placeholder="请输入应收金额" :min="0" style="width: 100%;"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listReceivable, getReceivable, delReceivable, addReceivable, updateReceivable } from "@/api/finance/receivable";
import { listOrder } from "@/api/crm/order";
import CustomerSelect from "@/components/KLPService/CustomerSelect/index.vue";
export default {
components: {
CustomerSelect
},
name: "FinanceStatus",
data() {
return {
buttonLoading: false,
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
searchable: true,
total: 0,
receivableList: [],
title: "",
open: false,
queryParams: {
pageNum: 1,
pageSize: 20,
customerId: undefined,
contractCode: undefined,
orderId: undefined,
orderCode: undefined,
dueDate: undefined,
amount: undefined,
paidAmount: undefined,
balanceAmount: undefined,
status: undefined,
},
contractOptions: [],
formContractOptions: [],
form: {},
rules: {}
};
},
computed: {
totalAmount() {
return this.receivableList.reduce((total, item) => total + parseFloat(item.amount || 0), 0);
},
receivedAmount() {
return this.receivableList.reduce((total, item) => total + parseFloat(item.paidAmount || 0), 0);
},
unreceivedAmount() {
return this.totalAmount - this.receivedAmount;
}
},
created() {
this.getList();
this.loadContracts(); // 加载所有合同编号,支持独立查询
},
methods: {
getList() {
this.loading = true;
listReceivable(this.queryParams).then(response => {
this.receivableList = response.rows || [];
this.total = response.total || 0;
}).catch(error => {
console.error('数据加载失败:', error);
this.$message.error('数据加载失败,请稍后重试');
this.receivableList = [];
this.total = 0;
}).finally(() => {
this.loading = false;
});
},
cancel() {
this.open = false;
this.reset();
},
handleCustomerChange(customer) {
// 根据客户ID获取合同列表可选
if (customer && customer.customerId) {
this.loadContracts(customer.customerId);
} else {
// 不传客户ID加载所有合同
this.loadContracts();
}
},
loadContracts(customerId) {
listOrder({ customerId: customerId || undefined, pageNum: 1, pageSize: 100 }).then(res => {
this.contractOptions = (res.rows || []).map(item => ({
value: item.contractCode,
label: item.contractCode
}));
});
},
handleFormCustomerChange(customer) {
// 清空合同选择
this.form.contractCode = undefined;
// 根据客户ID获取合同列表
if (customer && customer.customerId) {
listOrder({ customerId: customer.customerId, pageNum: 1, pageSize: 100 }).then(res => {
this.formContractOptions = (res.rows || []).map(item => ({
value: item.contractCode,
label: item.contractCode
}));
});
} else {
this.formContractOptions = [];
}
},
reset() {
this.form = {
receivableId: undefined,
customerId: undefined,
orderId: undefined,
dueDate: undefined,
amount: undefined,
paidAmount: 0,
balanceAmount: undefined,
status: undefined,
delFlag: undefined,
remark: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined
};
this.resetForm("form");
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.receivableId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
handleAdd() {
this.reset();
this.open = true;
this.title = "添加应收款管理";
},
handleUpdate(row) {
this.loading = true;
this.reset();
const receivableId = row.receivableId || this.ids;
getReceivable(receivableId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改应收款管理";
}).catch(error => {
this.loading = false;
this.$message.error('获取数据失败');
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.receivableId != null) {
const { balanceAmount, ...payload } = this.form;
updateReceivable(payload).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addReceivable(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
handleDelete(row) {
const receivableIds = row.receivableId || this.ids;
this.$modal.confirm('是否确认删除应收款管理编号为"' + receivableIds + '"的数据项?').then(() => {
this.loading = true;
return delReceivable(receivableIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
handleExport() {
this.download('klp/receivable/export', {
...this.queryParams
}, `receivable_${new Date().getTime()}.xlsx`);
},
parseTime(time, pattern) {
if (!time) return '';
const d = new Date(time);
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
return pattern.replace('{y}', year).replace('{m}', month).replace('{d}', day);
}
}
};
</script>
<style scoped>
.stats-card {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.stat-item {
flex: 1;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #303133;
margin-bottom: 8px;
}
.stat-value.success {
color: #67c23a;
}
.stat-value.danger {
color: #f56c6c;
}
.stat-value.warning {
color: #e6a23c;
}
.stat-label {
font-size: 13px;
color: #909399;
}
.quick-filter {
display: flex;
gap: 16px;
margin-bottom: 16px;
padding-left: 8px;
}
.quick-filter .el-button.active {
color: #409eff;
font-weight: 500;
}
.expiring-tag,
.expired-tag {
display: inline-block;
margin-left: 4px;
padding: 2px 6px;
font-size: 10px;
border-radius: 4px;
}
.expiring-tag {
background: #fffbe6;
color: #e6a23c;
}
.expired-tag {
background: #fff2f0;
color: #f56c6c;
}
.text-danger {
color: #f56c6c !important;
}
.text-warning {
color: #e6a23c !important;
}
.detail-content {
padding: 8px;
}
</style>

View File

@@ -70,7 +70,23 @@
</div>
</el-tab-pane>
<el-tab-pane label="财务状态" name="third">
<KLPTable v-loading="loading" :data="financeList">
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleFinanceAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="financeSingle" @click="handleFinanceUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="financeMultiple" @click="handleFinanceDelete">删除</el-button>
</el-col>
</el-row>
<!-- 表格 -->
<KLPTable v-loading="loading" :data="financeList" @selection-change="handleFinanceSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="客户" align="center" prop="customerName" />
<el-table-column label="合同编号" align="center" prop="contractCode" />
<el-table-column label="收款日期" align="center" prop="dueDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.dueDate, '{y}-{m}-{d}') }}</span>
@@ -78,6 +94,12 @@
</el-table-column>
<el-table-column label="收款金额" align="center" prop="amount" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleFinanceUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleFinanceDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
</el-tab-pane>
<el-tab-pane label="订单异议" name="fourth">
@@ -157,6 +179,34 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 财务状态操作对话框 -->
<el-dialog :title="financeTitle" :visible.sync="financeOpen" width="500px" append-to-body>
<el-form ref="financeForm" :model="financeForm" label-width="80px">
<el-form-item label="客户" prop="customerName">
<el-input v-model="financeForm.customerName" disabled />
</el-form-item>
<el-form-item label="合同编号" prop="contractCode">
<el-input v-model="financeForm.contractCode" placeholder="请输入合同编号" />
</el-form-item>
<el-form-item label="收款日期" prop="dueDate">
<el-date-picker clearable v-model="financeForm.dueDate" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择收款日期">
</el-date-picker>
</el-form-item>
<el-form-item label="收款金额" prop="amount">
<el-input-number :controls=false v-model="financeForm.amount" :step="1.00"
:precision="2" placeholder="请输入收款金额" :min="0" style="width: 100%;"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="financeForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="financeSubmitForm"> </el-button>
<el-button @click="financeCancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
@@ -171,6 +221,7 @@ import CoilTable from '../components/CoilTable.vue'
import DeliveryTable from '../components/DeliveryTable.vue'
import { listCustomer, addCustomer, updateCustomer, delCustomer, listCoilByCustomerId, listFinanceByCustomerId } from '@/api/crm/customer'
import { getReceivable, delReceivable, addReceivable, updateReceivable } from '@/api/finance/receivable'
export default {
name: 'CustomerPage',
@@ -209,6 +260,13 @@ export default {
buttonLoading: false,
updateLoading: false,
debounceTimer: null,
// 财务状态相关
financeIds: [],
financeSingle: true,
financeMultiple: true,
financeOpen: false,
financeTitle: "",
financeForm: {},
rules: {
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
companyName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
@@ -380,6 +438,57 @@ export default {
}).catch(() => {
this.$message.info('已取消删除');
});
},
// 财务状态操作
handleFinanceSelectionChange(selection) {
this.financeIds = selection.map(item => item.receivableId);
this.financeSingle = selection.length !== 1;
this.financeMultiple = !selection.length;
},
handleFinanceAdd() {
this.financeForm = {
customerId: this.currentCustomerId,
customerName: this.currentCustomer.companyName
};
this.financeTitle = "添加收款记录";
this.financeOpen = true;
},
handleFinanceUpdate(row) {
const receivableId = row.receivableId || this.financeIds;
getReceivable(receivableId).then(response => {
this.financeForm = response.data;
this.financeTitle = "修改收款记录";
this.financeOpen = true;
});
},
handleFinanceDelete(row) {
const receivableIds = row.receivableId || this.financeIds;
this.$modal.confirm('是否确认删除收款记录?').then(() => {
return delReceivable(receivableIds);
}).then(() => {
this.getFinanceList();
this.$modal.msgSuccess("删除成功");
});
},
financeSubmitForm() {
if (this.financeForm.receivableId != null) {
updateReceivable(this.financeForm).then(response => {
this.$modal.msgSuccess("修改成功");
this.financeOpen = false;
this.getFinanceList();
});
} else {
addReceivable(this.financeForm).then(response => {
this.$modal.msgSuccess("新增成功");
this.financeOpen = false;
this.getFinanceList();
});
}
},
financeCancel() {
this.financeOpen = false;
this.financeForm = {};
}
},
}
@@ -395,4 +504,4 @@ export default {
.dialog-footer {
text-align: center;
}
</style>
</style>

View File

@@ -76,6 +76,7 @@
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">筛选</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<span class="sort-hint">已按合同交货日期倒序 + 明细创建时间倒序排列</span>
</div>
<!-- 订单明细表格 -->
@@ -88,6 +89,7 @@
style="width: 100%"
class="order-item-table"
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 600 }"
:row-class-name="groupRowClassName"
>
<!-- 合同信息列只读灰色背景 -->
<el-table-column label="合同信息" align="center">
@@ -312,7 +314,6 @@
<script>
import { listOrderItem, updateOrderItem } from '@/api/crm/orderItem'
import { listOrder } from '@/api/crm/order'
import { listCategory } from '@/api/wms/category'
export default {
@@ -338,8 +339,6 @@ export default {
deliveryDateStart: undefined,
deliveryDateEnd: undefined
},
// 缓存合同信息
contractMap: {},
// 存储原始数据,用于判断是否有修改
originalData: {},
// 表面处理选项
@@ -370,49 +369,8 @@ export default {
getList() {
this.loading = true
listOrderItem(this.queryParams).then(res => {
const items = res.rows || []
this.orderItemList = res.rows || []
this.total = res.total || 0
// 获取所有相关的合同ID
const orderIds = [...new Set(items.map(item => item.orderId).filter(id => id))]
if (orderIds.length > 0) {
// 批量获取合同信息
this.loadContractInfo(orderIds, items)
} else {
this.orderItemList = items
this.loading = false
}
}).catch(e => {
console.error(e)
this.loading = false
})
},
// 加载合同信息
loadContractInfo(orderIds, items) {
// 使用listOrder获取合同信息通过params传递orderIds
const params = {
pageNum: 1,
pageSize: 9999,
orderIds: orderIds.join(',')
}
listOrder(params).then(res => {
const contracts = res.rows || []
// 构建合同ID到合同信息的映射
contracts.forEach(contract => {
this.contractMap[contract.orderId] = contract
})
// 合并数据
this.orderItemList = items.map(item => {
const contract = this.contractMap[item.orderId] || {}
return {
...item,
contractCode: contract.contractCode || '',
supplier: contract.supplier || '',
customer: contract.customer || '',
signTime: contract.signTime || '',
deliveryDate: contract.deliveryDate || ''
}
})
// 保存原始数据副本
this.originalData = {}
this.orderItemList.forEach(row => {
@@ -420,7 +378,6 @@ export default {
})
}).catch(e => {
console.error(e)
this.orderItemList = items
}).finally(() => {
this.loading = false
})
@@ -687,4 +644,20 @@ export default {
background: #4d6a8e !important;
border-color: #4d6a8e !important;
}
/* 排序状态提示 */
.sort-hint {
margin-left: auto;
font-size: 12px;
color: #909399;
white-space: nowrap;
}
/* 分组交替背景色 */
::v-deep .el-table .group-row-a td {
background-color: #ffffff !important;
}
::v-deep .el-table .group-row-b td {
background-color: #fafcff !important;
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="钢卷号" prop="coilNo">
<el-form-item label="入场钢卷号" prop="coilNo">
<el-input
v-model="queryParams.coilNo"
placeholder="请输入钢卷号"
placeholder="请输入入场钢卷号"
clearable
@keyup.enter.native="handleQuery"
/>
@@ -82,7 +82,7 @@
<el-table v-loading="loading" :data="chemicalItemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="钢卷号" align="center" prop="coilNo" />
<el-table-column label="入场钢卷号" align="center" prop="coilNo" />
<el-table-column label="炉号" align="center" prop="heatNo" />
<el-table-column label="碳(%)" align="center" prop="c" />
<el-table-column label="硅(%)" align="center" prop="si" />
@@ -120,12 +120,15 @@
<!-- 添加或修改质量的化学成分明细对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="钢卷" prop="coilId">
<coil-selector v-model="form.coilId" :use-trigger="true" @select="handleSelect"
:filters="{ selectType: 'product', status: 0, dataType: 1 }" />
</el-form-item>
<el-form-item label="钢卷号" prop="coilNo">
<el-input v-model="form.coilNo" placeholder="自动填写" />
<el-form-item label="入场钢卷" prop="coilNo">
<el-autocomplete
v-model="form.coilNo"
:fetch-suggestions="queryCoilNo"
placeholder="请输入入场钢卷号输入2个字符以上自动搜索"
:loading="coilNoLoading"
:trigger-on-focus="false"
clearable
/>
</el-form-item>
<el-form-item label="炉号" prop="heatNo">
<el-input v-model="form.heatNo" placeholder="请输入炉号" />
@@ -169,23 +172,18 @@
<div class="step-label">下载模板</div>
</div>
<div class="step-connector" :class="{ done: importStatus !== 'idle' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step-badge">2</div>
<div class="step-label">上传文件</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'processing' || importStatus === 'finished' }">
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'processing' || importStatus === 'finished' }">
<div class="step-badge">3</div>
<div class="step-label">校验数据</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'finished' }">
<div class="step-badge step-badge-warn">4</div>
<div class="step-label">匹配钢卷</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'finished' }">
<div class="step-badge step-badge-success">5</div>
<div class="step-badge step-badge-success">4</div>
<div class="step-label">批量导入</div>
</div>
</div>
@@ -202,64 +200,29 @@
</el-upload>
<el-button type="success" plain icon="el-icon-check" @click="importHandleValidate"
:disabled="!importFile || importValidateLoading || importStatus !== 'idle'" :loading="importValidateLoading">校验数据</el-button>
<el-button type="warning" plain icon="el-icon-coordinate" @click="importMatchCoils"
v-if="importStatus === 'validated'" :loading="importMatchingLoading"
:disabled="importMatchingLoading || importErrorList.length > 0">匹配钢卷</el-button>
</div>
<div class="toolbar-right">
<el-button v-if="importStatus === 'matched'" type="primary" icon="el-icon-upload-success" @click="importStartImport" :loading="importLoading">开始导入</el-button>
<el-button v-if="importStatus === 'validated'" type="primary" icon="el-icon-upload-success" @click="importStartImport" :loading="importLoading">开始导入</el-button>
<el-button plain icon="el-icon-download" @click="importDownloadTemplate">下载模板</el-button>
<el-button plain icon="el-icon-refresh" @click="importReset">重置</el-button>
</div>
</div>
<!-- 匹配结果摘要 -->
<div v-if="importStatus === 'matched'" class="match-summary">
<div class="summary-item summary-matched">
<span class="summary-num">{{ matchedCount }}</span>
<span class="summary-label">已匹配</span>
</div>
<div class="summary-item summary-ambiguous">
<span class="summary-num">{{ ambiguousCount }}</span>
<span class="summary-label">待选择</span>
</div>
<div class="summary-item summary-notfound">
<span class="summary-num">{{ notFoundCount }}</span>
<span class="summary-label">未找到</span>
</div>
<div class="summary-item summary-total">
<span class="summary-num">{{ importTableData.length }}</span>
<span class="summary-label">总计</span>
</div>
</div>
<!-- 数据预览表格 -->
<div v-if="importTableData.length > 0" class="data-preview">
<el-table ref="importTable" :data="importTableData" border size="small" max-height="340" stripe
:row-class-name="importTableRowClassName">
<el-table-column label="#" width="48" type="index" align="center" />
<el-table-column prop="currentCoilNo" label="当前钢卷号" width="140" />
<el-table-column prop="coilNo" label="入场钢卷号" width="140" />
<el-table-column prop="c" label="C(%)" width="70" align="center" />
<el-table-column prop="si" label="Si(%)" width="70" align="center" />
<el-table-column prop="mn" label="Mn(%)" width="70" align="center" />
<el-table-column prop="p" label="P(%)" width="70" align="center" />
<el-table-column prop="s" label="S(%)" width="70" align="center" />
<el-table-column prop="als" label="Als(%)" width="70" align="center" />
<el-table-column label="匹配结果" width="130" align="center" fixed="right">
<template slot-scope="scope">
<el-tag v-if="scope.row._status === 'matched'" type="success" size="mini" effect="plain">已匹配</el-tag>
<el-tag v-else-if="scope.row._status === 'ambiguous'" type="warning" size="mini" effect="plain" class="tag-clickable" @click="importOpenCandidateDialog(scope.$index)">
<i class="el-icon-question" /> 需选择
</el-tag>
<el-tag v-else-if="scope.row._status === 'not_found'" type="danger" size="mini" effect="plain">未找到</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">待处理</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right" v-if="importStatus === 'matched'">
<template slot-scope="scope">
<el-button v-if="scope.row._status === 'ambiguous'" type="warning" size="mini" plain @click="importOpenCandidateDialog(scope.$index)">选择钢卷</el-button>
</template>
</el-table-column>
</el-table>
</div>
@@ -289,48 +252,22 @@
<div class="result-desc">{{ importErrorMsg }}</div>
</div>
</div>
<!-- 未找到的提示 -->
<div v-if="importErrorList.length > 0 && importStatus !== 'error'" class="result-panel result-error">
<div class="result-icon"><i class="el-icon-warning-outline" /></div>
<div class="result-body">
<div class="result-title">{{ importErrorList.length }} 条记录未找到对应钢卷</div>
<div class="result-desc">这些记录将在导入时被跳过请检查钢卷号是否正确</div>
</div>
</div>
</div>
</el-dialog>
<!-- 钢卷选择对话框多匹配时 -->
<el-dialog title="选择钢卷" :visible.sync="candidateDialogVisible" width="700px" top="20vh" append-to-body>
<div class="candidate-hint">
<i class="el-icon-info" />
当前钢卷号 <b>{{ candidateCoilNo }}</b> 匹配到多条记录请选择一条
</div>
<el-table ref="candidateTable" :data="candidateList" border height="300" highlight-current-row @row-click="importSelectCandidate">
<el-table-column type="index" label="#" width="48" align="center" />
<el-table-column prop="currentCoilNo" label="当前钢卷号" width="140" />
<el-table-column prop="material" label="材质" width="100" />
<el-table-column prop="specification" label="规格" width="120" />
<el-table-column prop="netWeight" label="重量(t)" width="100" align="center" />
<el-table-column prop="warehouseName" label="库区" />
</el-table>
<div class="candidate-footer">点击行即可选中</div>
</el-dialog>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { listChemicalItem, getChemicalItem, delChemicalItem, addChemicalItem, updateChemicalItem, batchAddChemicalItem } from "@/api/mes/qc/chemicalItem";
import { listMaterialCoil } from "@/api/wms/coil";
import CoilSelector from "@/components/CoilSelector";
import { listMaterialCoil } from "@/api/wms/coil";
const CHEMI_TEMPLATE_HEADERS = ['当前钢卷号', 'C(%)', 'Si(%)', 'Mn(%)', 'P(%)', 'S(%)', 'Als(%)'];
const CHEMI_TEMPLATE_HEADERS = ['入场钢卷号', 'C(%)', 'Si(%)', 'Mn(%)', 'P(%)', 'S(%)', 'Als(%)'];
const CHEMI_HEADER_MAP = {
'当前钢卷号': 'currentCoilNo',
'入场钢卷号': 'coilNo',
'C(%)': 'c',
'Si(%)': 'si',
'Mn(%)': 'mn',
@@ -344,11 +281,7 @@ export default {
components: {
CoilSelector
},
computed: {
matchedCount() { return this.importTableData.filter(r => r._status === 'matched').length; },
ambiguousCount() { return this.importTableData.filter(r => r._status === 'ambiguous').length; },
notFoundCount() { return this.importTableData.filter(r => r._status === 'not_found').length; }
},
computed: {},
data() {
return {
// 按钮loading
@@ -389,7 +322,7 @@ export default {
// 表单校验
rules: {
coilNo: [
{ required: true, message: "钢卷号不能为空", trigger: "blur" }
{ required: true, message: "入场钢卷号不能为空", trigger: "blur" }
],
},
importDialogVisible: false,
@@ -403,12 +336,9 @@ export default {
importStatus: 'idle', // idle | validated | matched | processing | finished | error
importErrorMsg: '',
importValidateLoading: false,
importMatchingLoading: false,
importLoading: false,
candidateDialogVisible: false,
candidateRowIndex: -1,
candidateCoilNo: '',
candidateList: []
coilNoOptions: [],
coilNoLoading: false,
};
},
created() {
@@ -434,7 +364,6 @@ export default {
this.form = {
itemId: undefined,
certificateId: undefined,
coilId: undefined,
coilNo: undefined,
heatNo: undefined,
c: undefined,
@@ -475,15 +404,33 @@ export default {
this.title = "添加质量的化学成分明细";
},
handleSelect(coil) {
this.form.coilId = coil.coilId;
this.form.coilNo = coil.currentCoilNo;
this.form.coilNo = coil.enterCoilNo;
},
queryCoilNo(queryString, cb) {
if (!queryString || queryString.length < 2) {
cb([]);
return;
}
this.coilNoLoading = true;
listMaterialCoil({
enterCoilNo: queryString,
pageNum: 1,
pageSize: 20
}).then(response => {
const options = (response.rows || []).map(item => ({
value: item.enterCoilNo,
label: item.enterCoilNo
}));
cb(options);
}).finally(() => {
this.coilNoLoading = false;
});
},
handleBatchAdd(rows) {
this.loading = true;
this.buttonLoading = true;
const payload = rows.map(coil => ({
coilId: coil.coilId,
coilNo: coil.currentCoilNo,
coilNo: coil.enterCoilNo,
}))
batchAddChemicalItem(payload).then(response => {
this.$modal.msgSuccess("新增成功");
@@ -497,7 +444,7 @@ export default {
this.importDialogVisible = true;
},
importHandleFileChange(file) {
if (this.importValidateLoading || this.importMatchingLoading || this.importLoading) return;
if (this.importValidateLoading || this.importLoading) return;
this.importFile = file.raw;
this.importErrorList = [];
this.importStatus = 'idle';
@@ -541,10 +488,6 @@ export default {
CHEMI_TEMPLATE_HEADERS.forEach((h, j) => { obj[CHEMI_HEADER_MAP[h]] = row[j] != null ? String(row[j]).trim() : ''; });
obj.rowNum = i + 2;
obj._status = 'pending';
obj._coilId = null;
obj._coilNo = null;
obj._candidates = [];
obj._errorMsg = '';
this.importTableData.push(obj);
});
},
@@ -555,76 +498,22 @@ export default {
for (let i = 0; i < this.importRawData.length; i++) {
const rowNum = i + 2;
let coilNo = '';
CHEMI_TEMPLATE_HEADERS.forEach((h, j) => { if (CHEMI_HEADER_MAP[h] === 'currentCoilNo') coilNo = this.importRawData[i][j]; });
CHEMI_TEMPLATE_HEADERS.forEach((h, j) => { if (CHEMI_HEADER_MAP[h] === 'coilNo') coilNo = this.importRawData[i][j]; });
if (!coilNo || !String(coilNo).trim()) this.importErrorList.push({ rowNum, errorMsg: '当前钢卷号不能为空' });
}
if (this.importErrorList.length > 0) {
this.$message.error(`数据校验失败,共发现${this.importErrorList.length}条错误`);
} else {
this.importTableData.forEach(row => { row._status = 'valid'; });
this.$message.success('数据校验通过');
this.importStatus = 'validated';
}
this.importValidateLoading = false;
},
async importMatchCoils() {
this.importMatchingLoading = true;
this.importErrorList = [];
for (let i = 0; i < this.importTableData.length; i++) {
const row = this.importTableData[i];
const coilNo = row.currentCoilNo;
if (!coilNo) { row._status = 'not_found'; row._errorMsg = '钢卷号为空'; continue; }
try {
const res = await listMaterialCoil({ currentCoilNo: coilNo, dataType: 1, pageSize: 999 });
const coils = res.rows || [];
if (coils.length === 0) {
row._status = 'not_found';
row._errorMsg = `未找到钢卷号"${coilNo}"`;
} else if (coils.length === 1) {
row._status = 'matched';
row._coilId = coils[0].coilId;
row._coilNo = coils[0].currentCoilNo;
} else {
row._status = 'ambiguous';
row._candidates = coils;
}
} catch (err) {
row._status = 'not_found';
row._errorMsg = err.message;
}
}
const notFound = this.importTableData.filter(r => r._status === 'not_found');
if (notFound.length > 0) {
this.importErrorList = notFound.map(r => ({ rowNum: r.rowNum, errorMsg: r._errorMsg }));
this.$message.warning(`${notFound.length} 条记录未找到对应钢卷,已被剔除`);
}
this.importMatchingLoading = false;
this.importStatus = 'matched';
},
importOpenCandidateDialog(index) {
const row = this.importTableData[index];
if (!row || row._status !== 'ambiguous') return;
this.candidateRowIndex = index;
this.candidateCoilNo = row.currentCoilNo;
this.candidateList = row._candidates;
this.candidateDialogVisible = true;
},
importSelectCandidate(coil) {
if (this.candidateRowIndex < 0) return;
const row = this.importTableData[this.candidateRowIndex];
row._status = 'matched';
row._coilId = coil.coilId;
row._coilNo = coil.currentCoilNo;
row._candidates = [];
this.candidateDialogVisible = false;
this.candidateRowIndex = -1;
this.candidateList = [];
this.$message.success(`已为第 ${row.rowNum} 行选择钢卷:${coil.currentCoilNo}`);
},
async importStartImport() {
const rows = this.importTableData.filter(r => r._status === 'matched');
const rows = this.importTableData.filter(r => r._status === 'valid');
if (!rows.length) { this.$message.warning('没有可导入的数据'); return; }
const skipped = this.importTableData.length - rows.length;
const ok = await this.$confirm(`确认导入 ${rows.length} 条数据?${skipped ? `${skipped} 条未匹配的记录将被跳过)` : ''}`, '导入确认', { confirmButtonText: '确认导入', cancelButtonText: '取消', type: 'warning' }).catch(() => false);
const ok = await this.$confirm(`确认导入 ${rows.length} 条数据?`, '导入确认', { confirmButtonText: '确认导入', cancelButtonText: '取消', type: 'warning' }).catch(() => false);
if (!ok) return;
this.importLoading = true;
this.importStatus = 'processing';
@@ -633,7 +522,7 @@ export default {
this.importErrorMsg = '';
try {
const payload = rows.map(row => {
const item = { coilId: row._coilId, coilNo: row._coilNo };
const item = { coilId: null, coilNo: row.coilNo };
['c','si','mn','p','s','als'].forEach(f => { if (row[f]) item[f] = row[f]; });
return item;
});
@@ -651,7 +540,7 @@ export default {
}
},
importReset() {
if (this.importValidateLoading || this.importMatchingLoading || this.importLoading || this.importStatus === 'processing') { this.$message.warning('当前有操作正在进行中'); return; }
if (this.importValidateLoading || this.importLoading || this.importStatus === 'processing') { this.$message.warning('当前有操作正在进行中'); return; }
this.importFile = null;
this.importRawData = [];
this.importTableData = [];
@@ -661,10 +550,6 @@ export default {
this.importTotalCount = 0;
this.importStatus = 'idle';
this.importErrorMsg = '';
this.candidateDialogVisible = false;
this.candidateRowIndex = -1;
this.candidateCoilNo = '';
this.candidateList = [];
this.$refs.importUpload?.clearFiles();
},
importDownloadTemplate() {
@@ -680,13 +565,9 @@ export default {
this.importStatus = 'error';
this.importErrorMsg = msg;
this.importValidateLoading = false;
this.importMatchingLoading = false;
this.importLoading = false;
},
importTableRowClassName({ row }) {
if (row._status === 'ambiguous') return 'row-ambiguous';
if (row._status === 'not_found') return 'row-notfound';
if (row._status === 'matched') return 'row-matched';
importTableRowClassName() {
return '';
},

View File

@@ -519,11 +519,11 @@ export default {
};
this.getCoilList();
},
async fetchLatestChemAndPhys(coilId) {
if (!coilId) return {};
async fetchLatestChemAndPhys(coilNo) {
if (!coilNo) return {};
const [chemRes, physRes] = await Promise.all([
listChemicalItem({ coilId, pageNum: 1, pageSize: 1 }),
listPhysicalItem({ coilId, pageNum: 1, pageSize: 1 })
listChemicalItem({ coilNo, pageNum: 1, pageSize: 1 }),
listPhysicalItem({ coilNo, pageNum: 1, pageSize: 1 })
]);
const chem = chemRes.rows && chemRes.rows[0] || {};
const phys = physRes.rows && physRes.rows[0] || {};
@@ -538,7 +538,7 @@ export default {
editRow.size = row.specification;
editRow.weight = row.netWeight;
editRow.pieces = 1;
const data = await this.fetchLatestChemAndPhys(row.coilId);
const data = await this.fetchLatestChemAndPhys(row.enterCoilNo);
Object.assign(editRow, data);
}
this.coilDialogVisible = false;
@@ -583,11 +583,12 @@ export default {
const totalCount = coils.length;
let successCount = 0;
let failCount = 0;
this.loading = true;
for (let index = 0; index < coils.length; index++) {
const coil = coils[index];
try {
const data = await this.fetchLatestChemAndPhys(coil.coilId);
const data = await this.fetchLatestChemAndPhys(coil.enterCoilNo);
const newRow = {
certificateId: this.currentCertificateId,
itemSeqNo: this.certificateItemList.length + index + 1,

View File

@@ -1,10 +1,10 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="钢卷号" prop="coilNo">
<el-form-item label="入场钢卷号" prop="coilNo">
<el-input
v-model="queryParams.coilNo"
placeholder="请输入钢卷号"
placeholder="请输入入场钢卷号"
clearable
@keyup.enter.native="handleQuery"
/>
@@ -131,7 +131,7 @@
<el-table v-loading="loading" :data="physicalItemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物理性能ID" align="center" prop="physicalId" v-if="false"/>
<el-table-column label="钢卷号" align="center" prop="coilNo" />
<el-table-column label="入场钢卷号" align="center" prop="coilNo" />
<el-table-column label="屈服强度(MPa)" align="center" prop="yieldStrength" />
<el-table-column label="抗拉强度(MPa)" align="center" prop="tensileStrength" />
<el-table-column label="伸长率(%)" align="center" prop="elongation" />
@@ -170,12 +170,15 @@
<!-- 添加或修改物理性能明细对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="钢卷" prop="coilId">
<coil-selector v-model="form.coilId" :use-trigger="true" @select="handleSelect"
:filters="{ selectType: 'product', status: 0, dataType: 1 }" />
</el-form-item>
<el-form-item label="钢卷号" prop="coilNo">
<el-input v-model="form.coilNo" placeholder="自动填写" />
<el-form-item label="入场钢卷" prop="coilNo">
<el-autocomplete
v-model="form.coilNo"
:fetch-suggestions="queryCoilNo"
placeholder="请输入入场钢卷号输入2个字符以上自动搜索"
:loading="coilNoLoading"
:trigger-on-focus="false"
clearable
/>
</el-form-item>
<el-form-item label="屈服强度(MPa)" prop="yieldStrength">
<el-input v-model="form.yieldStrength" placeholder="请输入屈服强度(MPa)" />
@@ -219,23 +222,18 @@
<div class="step-label">下载模板</div>
</div>
<div class="step-connector" :class="{ done: importStatus !== 'idle' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step-badge">2</div>
<div class="step-label">上传文件</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'processing' || importStatus === 'finished' }">
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'processing' || importStatus === 'finished' }">
<div class="step-badge">3</div>
<div class="step-label">校验数据</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'matched' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'finished' }">
<div class="step-badge step-badge-warn">4</div>
<div class="step-label">匹配钢卷</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'finished' }">
<div class="step-badge step-badge-success">5</div>
<div class="step-badge step-badge-success">4</div>
<div class="step-label">批量导入</div>
</div>
</div>
@@ -252,43 +250,22 @@
</el-upload>
<el-button type="success" plain icon="el-icon-check" @click="importHandleValidate"
:disabled="!importFile || importValidateLoading || importStatus !== 'idle'" :loading="importValidateLoading">校验数据</el-button>
<el-button type="warning" plain icon="el-icon-coordinate" @click="importMatchCoils"
v-if="importStatus === 'validated'" :loading="importMatchingLoading"
:disabled="importMatchingLoading || importErrorList.length > 0">匹配钢卷</el-button>
</div>
<div class="toolbar-right">
<el-button v-if="importStatus === 'matched'" type="primary" icon="el-icon-upload-success" @click="importStartImport" :loading="importLoading">开始导入</el-button>
<el-button v-if="importStatus === 'validated'" type="primary" icon="el-icon-upload-success" @click="importStartImport" :loading="importLoading">开始导入</el-button>
<el-button plain icon="el-icon-download" @click="importDownloadTemplate">下载模板</el-button>
<el-button plain icon="el-icon-refresh" @click="importReset">重置</el-button>
</div>
</div>
<!-- 匹配结果摘要 -->
<div v-if="importStatus === 'matched'" class="match-summary">
<div class="summary-item summary-matched">
<span class="summary-num">{{ matchedCount }}</span>
<span class="summary-label">已匹配</span>
</div>
<div class="summary-item summary-ambiguous">
<span class="summary-num">{{ ambiguousCount }}</span>
<span class="summary-label">待选择</span>
</div>
<div class="summary-item summary-notfound">
<span class="summary-num">{{ notFoundCount }}</span>
<span class="summary-label">未找到</span>
</div>
<div class="summary-item summary-total">
<span class="summary-num">{{ importTableData.length }}</span>
<span class="summary-label">总计</span>
</div>
</div>
<!-- 数据预览表格 -->
<div v-if="importTableData.length > 0" class="data-preview">
<el-table ref="importTable" :data="importTableData" border size="small" max-height="340" stripe
:row-class-name="importTableRowClassName">
<el-table-column label="#" width="48" type="index" align="center" />
<el-table-column prop="currentCoilNo" label="当前钢卷号" width="140" />
<el-table-column prop="coilNo" label="入场钢卷号" width="140" />
<el-table-column prop="yieldStrength" label="屈服强度(MPa)" width="110" align="center" />
<el-table-column prop="tensileStrength" label="抗拉强度(MPa)" width="110" align="center" />
<el-table-column prop="elongation" label="伸长率(%)" width="80" align="center" />
@@ -297,21 +274,7 @@
<el-table-column prop="surfaceQuality" label="表面质量" width="80" align="center" />
<el-table-column prop="surfaceStructure" label="表面结构" width="80" align="center" />
<el-table-column prop="edgeStatus" label="边缘状态" width="80" align="center" />
<el-table-column label="匹配结果" width="130" align="center" fixed="right">
<template slot-scope="scope">
<el-tag v-if="scope.row._status === 'matched'" type="success" size="mini" effect="plain">已匹配</el-tag>
<el-tag v-else-if="scope.row._status === 'ambiguous'" type="warning" size="mini" effect="plain" class="tag-clickable" @click="importOpenCandidateDialog(scope.$index)">
<i class="el-icon-question" /> 需选择
</el-tag>
<el-tag v-else-if="scope.row._status === 'not_found'" type="danger" size="mini" effect="plain">未找到</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">待处理</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right" v-if="importStatus === 'matched'">
<template slot-scope="scope">
<el-button v-if="scope.row._status === 'ambiguous'" type="warning" size="mini" plain @click="importOpenCandidateDialog(scope.$index)">选择钢卷</el-button>
</template>
</el-table-column>
</el-table>
</div>
@@ -342,42 +305,19 @@
</div>
</div>
<!-- 未找到的提示 -->
<div v-if="importErrorList.length > 0 && importStatus !== 'error'" class="result-panel result-error">
<div class="result-icon"><i class="el-icon-warning-outline" /></div>
<div class="result-body">
<div class="result-title">{{ importErrorList.length }} 条记录未找到对应钢卷</div>
<div class="result-desc">这些记录将在导入时被跳过请检查钢卷号是否正确</div>
</div>
</div>
</div>
</el-dialog>
<!-- 钢卷选择对话框多匹配时 -->
<el-dialog title="选择钢卷" :visible.sync="candidateDialogVisible" width="700px" top="20vh" append-to-body>
<div class="candidate-hint">
<i class="el-icon-info" />
当前钢卷号 <b>{{ candidateCoilNo }}</b> 匹配到多条记录请选择一条
</div>
<el-table ref="candidateTable" :data="candidateList" border height="300" highlight-current-row @row-click="importSelectCandidate">
<el-table-column type="index" label="#" width="48" align="center" />
<el-table-column prop="currentCoilNo" label="当前钢卷号" width="140" />
<el-table-column prop="material" label="材质" width="100" />
<el-table-column prop="specification" label="规格" width="120" />
<el-table-column prop="netWeight" label="重量(t)" width="100" align="center" />
<el-table-column prop="warehouseName" label="库区" />
</el-table>
<div class="candidate-footer">点击行即可选中</div>
</el-dialog>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { listPhysicalItem, getPhysicalItem, delPhysicalItem, addPhysicalItem, updatePhysicalItem, batchAddPhysicalItem } from "@/api/mes/qc/physicalItem";
import { listMaterialCoil } from "@/api/wms/coil";
import CoilSelector from "@/components/CoilSelector";
import { listMaterialCoil } from "@/api/wms/coil";
const PHYS_TEMPLATE_HEADERS = [
'当前钢卷号', '屈服强度(MPa)', '抗拉强度(MPa)', '伸长率(%)', '硬度(HRB)', '弯曲试验',
@@ -385,7 +325,7 @@ const PHYS_TEMPLATE_HEADERS = [
];
const PHYS_HEADER_MAP = {
'当前钢卷号': 'currentCoilNo',
'当前钢卷号': 'coilNo',
'屈服强度(MPa)': 'yieldStrength',
'抗拉强度(MPa)': 'tensileStrength',
'伸长率(%)': 'elongation',
@@ -401,11 +341,7 @@ export default {
components: {
CoilSelector
},
computed: {
matchedCount() { return this.importTableData.filter(r => r._status === 'matched').length; },
ambiguousCount() { return this.importTableData.filter(r => r._status === 'ambiguous').length; },
notFoundCount() { return this.importTableData.filter(r => r._status === 'not_found').length; }
},
computed: {},
data() {
return {
// 按钮loading
@@ -447,7 +383,7 @@ export default {
// 表单校验
rules: {
coilNo: [
{ required: true, message: "钢卷号不能为空", trigger: "blur" }
{ required: true, message: "入场钢卷号不能为空", trigger: "blur" }
],
},
importDialogVisible: false,
@@ -461,12 +397,9 @@ export default {
importStatus: 'idle', // idle | validated | matched | processing | finished | error
importErrorMsg: '',
importValidateLoading: false,
importMatchingLoading: false,
importLoading: false,
candidateDialogVisible: false,
candidateRowIndex: -1,
candidateCoilNo: '',
candidateList: []
coilNoOptions: [],
coilNoLoading: false,
};
},
created() {
@@ -492,7 +425,6 @@ export default {
this.form = {
physicalId: undefined,
certificateId: undefined,
coilId: undefined,
coilNo: undefined,
yieldStrength: undefined,
tensileStrength: undefined,
@@ -534,15 +466,33 @@ export default {
this.title = "添加物理性能明细";
},
handleSelect(coil) {
this.form.coilId = coil.coilId;
this.form.coilNo = coil.currentCoilNo;
this.form.coilNo = coil.enterCoilNo;
},
queryCoilNo(queryString, cb) {
if (!queryString || queryString.length < 2) {
cb([]);
return;
}
this.coilNoLoading = true;
listMaterialCoil({
enterCoilNo: queryString,
pageNum: 1,
pageSize: 20
}).then(response => {
const options = (response.rows || []).map(item => ({
value: item.enterCoilNo,
label: item.enterCoilNo
}));
cb(options);
}).finally(() => {
this.coilNoLoading = false;
});
},
handleBatchAdd(rows) {
this.loading = true;
this.buttonLoading = true;
const payload = rows.map(coil => ({
coilId: coil.coilId,
coilNo: coil.currentCoilNo,
coilNo: coil.coilNo,
}))
batchAddPhysicalItem(payload).then(response => {
this.$modal.msgSuccess("新增成功");
@@ -556,7 +506,7 @@ export default {
this.importDialogVisible = true;
},
importHandleFileChange(file) {
if (this.importValidateLoading || this.importMatchingLoading || this.importLoading) return;
if (this.importValidateLoading || this.importLoading) return;
this.importFile = file.raw;
this.importErrorList = [];
this.importStatus = 'idle';
@@ -600,10 +550,6 @@ export default {
PHYS_TEMPLATE_HEADERS.forEach((h, j) => { obj[PHYS_HEADER_MAP[h]] = row[j] != null ? String(row[j]).trim() : ''; });
obj.rowNum = i + 2;
obj._status = 'pending';
obj._coilId = null;
obj._coilNo = null;
obj._candidates = [];
obj._errorMsg = '';
this.importTableData.push(obj);
});
},
@@ -614,76 +560,22 @@ export default {
for (let i = 0; i < this.importRawData.length; i++) {
const rowNum = i + 2;
let coilNo = '';
PHYS_TEMPLATE_HEADERS.forEach((h, j) => { if (PHYS_HEADER_MAP[h] === 'currentCoilNo') coilNo = this.importRawData[i][j]; });
PHYS_TEMPLATE_HEADERS.forEach((h, j) => { if (PHYS_HEADER_MAP[h] === 'coilNo') coilNo = this.importRawData[i][j]; });
if (!coilNo || !String(coilNo).trim()) this.importErrorList.push({ rowNum, errorMsg: '当前钢卷号不能为空' });
}
if (this.importErrorList.length > 0) {
this.$message.error(`数据校验失败,共发现${this.importErrorList.length}条错误`);
} else {
this.importTableData.forEach(row => { row._status = 'valid'; });
this.$message.success('数据校验通过');
this.importStatus = 'validated';
}
this.importValidateLoading = false;
},
async importMatchCoils() {
this.importMatchingLoading = true;
this.importErrorList = [];
for (let i = 0; i < this.importTableData.length; i++) {
const row = this.importTableData[i];
const coilNo = row.currentCoilNo;
if (!coilNo) { row._status = 'not_found'; row._errorMsg = '钢卷号为空'; continue; }
try {
const res = await listMaterialCoil({ currentCoilNo: coilNo, dataType: 1, pageSize: 999 });
const coils = res.rows || [];
if (coils.length === 0) {
row._status = 'not_found';
row._errorMsg = `未找到钢卷号"${coilNo}"`;
} else if (coils.length === 1) {
row._status = 'matched';
row._coilId = coils[0].coilId;
row._coilNo = coils[0].currentCoilNo;
} else {
row._status = 'ambiguous';
row._candidates = coils;
}
} catch (err) {
row._status = 'not_found';
row._errorMsg = err.message;
}
}
const notFound = this.importTableData.filter(r => r._status === 'not_found');
if (notFound.length > 0) {
this.importErrorList = notFound.map(r => ({ rowNum: r.rowNum, errorMsg: r._errorMsg }));
this.$message.warning(`${notFound.length} 条记录未找到对应钢卷,已被剔除`);
}
this.importMatchingLoading = false;
this.importStatus = 'matched';
},
importOpenCandidateDialog(index) {
const row = this.importTableData[index];
if (!row || row._status !== 'ambiguous') return;
this.candidateRowIndex = index;
this.candidateCoilNo = row.currentCoilNo;
this.candidateList = row._candidates;
this.candidateDialogVisible = true;
},
importSelectCandidate(coil) {
if (this.candidateRowIndex < 0) return;
const row = this.importTableData[this.candidateRowIndex];
row._status = 'matched';
row._coilId = coil.coilId;
row._coilNo = coil.currentCoilNo;
row._candidates = [];
this.candidateDialogVisible = false;
this.candidateRowIndex = -1;
this.candidateList = [];
this.$message.success(`已为第 ${row.rowNum} 行选择钢卷:${coil.currentCoilNo}`);
},
async importStartImport() {
const rows = this.importTableData.filter(r => r._status === 'matched');
const rows = this.importTableData.filter(r => r._status === 'valid');
if (!rows.length) { this.$message.warning('没有可导入的数据'); return; }
const skipped = this.importTableData.length - rows.length;
const ok = await this.$confirm(`确认导入 ${rows.length} 条数据?${skipped ? `${skipped} 条未匹配的记录将被跳过)` : ''}`, '导入确认', { confirmButtonText: '确认导入', cancelButtonText: '取消', type: 'warning' }).catch(() => false);
const ok = await this.$confirm(`确认导入 ${rows.length} 条数据?`, '导入确认', { confirmButtonText: '确认导入', cancelButtonText: '取消', type: 'warning' }).catch(() => false);
if (!ok) return;
this.importLoading = true;
this.importStatus = 'processing';
@@ -692,7 +584,7 @@ export default {
this.importErrorMsg = '';
try {
const payload = rows.map(row => {
const item = { coilId: row._coilId, coilNo: row._coilNo };
const item = { coilId: null, coilNo: row.coilNo };
['yieldStrength','tensileStrength','elongation','hardness','bendingTest','surfaceQuality','surfaceStructure','edgeStatus'].forEach(f => { if (row[f]) item[f] = row[f]; });
return item;
});
@@ -710,7 +602,7 @@ export default {
}
},
importReset() {
if (this.importValidateLoading || this.importMatchingLoading || this.importLoading || this.importStatus === 'processing') { this.$message.warning('当前有操作正在进行中'); return; }
if (this.importValidateLoading || this.importLoading || this.importStatus === 'processing') { this.$message.warning('当前有操作正在进行中'); return; }
this.importFile = null;
this.importRawData = [];
this.importTableData = [];
@@ -720,10 +612,6 @@ export default {
this.importTotalCount = 0;
this.importStatus = 'idle';
this.importErrorMsg = '';
this.candidateDialogVisible = false;
this.candidateRowIndex = -1;
this.candidateCoilNo = '';
this.candidateList = [];
this.$refs.importUpload?.clearFiles();
},
importDownloadTemplate() {
@@ -739,13 +627,9 @@ export default {
this.importStatus = 'error';
this.importErrorMsg = msg;
this.importValidateLoading = false;
this.importMatchingLoading = false;
this.importLoading = false;
},
importTableRowClassName({ row }) {
if (row._status === 'ambiguous') return 'row-ambiguous';
if (row._status === 'not_found') return 'row-notfound';
if (row._status === 'matched') return 'row-matched';
importTableRowClassName() {
return '';
},
/** 修改按钮操作 */

View File

@@ -74,6 +74,7 @@
<el-table-column label="任务ID" align="center" prop="taskId" v-if="false" />
<el-table-column label="任务编号" align="center" prop="taskCode" />
<el-table-column label="任务类型" align="center" prop="taskType" />
<el-table-column label="入场卷号" align="center" prop="enterCoilNos" />
<el-table-column label="所属单位" align="center" prop="belongCompany" />
<el-table-column label="方案名称" align="center" prop="schemeName" />
<el-table-column label="状态" align="center" prop="status" width="90">
@@ -99,7 +100,7 @@
</template>
</el-table-column>
<el-table-column label="最终结果" align="center" prop="result" />
<el-table-column label="关联钢卷" align="center" width="140">
<!-- <el-table-column label="关联钢卷" align="center" width="140">
<template slot-scope="scope">
<div v-if="scope.row.coilList && scope.row.coilList.length > 0"
style="display: flex; flex-wrap: wrap; gap: 4px;">
@@ -109,7 +110,7 @@
</div>
<span v-else>-</span>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template slot-scope="scope">
@@ -244,6 +245,27 @@
<el-form-item label="任务编号" prop="taskCode">
<el-input v-model="taskForm.taskCode" placeholder="请输入任务编号" />
</el-form-item>
<el-form-item label="入场卷号" prop="enterCoilNos">
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;">
<el-tag
v-for="(coilNo, index) in selectedEnterCoilNos"
:key="index"
closable
@close="removeEnterCoilNo(index)"
>
{{ coilNo }}
</el-tag>
</div>
<el-autocomplete
v-model="tempEnterCoilNo"
:fetch-suggestions="queryEnterCoilNo"
placeholder="2个字符以上自动搜索"
:loading="enterCoilNoLoading"
:trigger-on-focus="false"
clearable
@select="handleSelectEnterCoilNo"
/>
</el-form-item>
<el-form-item label="任务类型" prop="taskType">
<el-select v-model="taskForm.taskType" placeholder="请选择任务类型" clearable filterable allow-create>
<el-option label="内控检验" value="内控检验" />
@@ -263,7 +285,8 @@
<el-input v-model="taskForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div style="margin: 0 0 12px 80px;">
<!-- <div style="margin: 0 0 12px 80px;">
<CoilSelector use-trigger multiple @confirm="handleTaskCoilConfirm">
<el-button type="primary" plain icon="el-icon-plus" size="small">选择钢卷</el-button>
</CoilSelector>
@@ -279,7 +302,7 @@
<CurrentCoilNo :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
<el-button type="text" icon="el-icon-close" size="mini" @click="taskCoilList.splice(index, 1)"></el-button>
</div>
</div>
</div> -->
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitTaskForm"> </el-button>
<el-button @click="cancelTask"> </el-button>
@@ -353,7 +376,7 @@ import { listInspectionItemTemplate } from "@/api/mes/qc/inspectionItemTemplate"
import { SchemeSelect } from "@/components/KLPService";
import CoilSelector from "@/components/CoilSelector/index.vue";
import CurrentCoilNo from "@/components/KLPService/Renderer/CurrentCoilNo.vue";
import { listCoilByIds } from "@/api/wms/coil";
import { listCoilByIds, listMaterialCoil } from "@/api/wms/coil";
export default {
name: "InspectionTask",
@@ -396,7 +419,13 @@ export default {
result: undefined,
},
taskForm: {},
taskRules: {},
taskRules: {
taskCode: [{ required: true, message: '请输入任务编号', trigger: 'blur' }],
enterCoilNos: [{ required: true, message: '请输入入场卷号', trigger: 'blur' }],
taskType: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
belongCompany: [{ required: true, message: '请输入所属公司', trigger: 'blur' }],
schemeName: [{ required: true, message: '请输入检验方案名称', trigger: 'blur' }],
},
itemLoading: false,
itemIds: [],
@@ -438,6 +467,10 @@ export default {
schemeOptions: [],
selectedCompany: '',
selectedScheme: '',
enterCoilNoOptions: [],
enterCoilNoLoading: false,
selectedEnterCoilNos: [],
tempEnterCoilNo: '',
};
},
computed: {
@@ -495,6 +528,8 @@ export default {
},
handleTaskAdd() {
this.resetTaskForm();
this.selectedEnterCoilNos = [];
this.tempEnterCoilNo = '';
this.taskOpen = true;
this.taskTitle = "添加检验任务";
},
@@ -511,6 +546,13 @@ export default {
this.taskCoilList = res.rows || [];
});
}
// 初始化已选择的入场卷号
if (response.data.enterCoilNos) {
this.selectedEnterCoilNos = response.data.enterCoilNos.split(',').filter(item => item.trim());
} else {
this.selectedEnterCoilNos = [];
}
this.tempEnterCoilNo = '';
this.taskLoading = false;
this.taskOpen = true;
this.taskTitle = "修改检验任务";
@@ -566,7 +608,7 @@ export default {
handleQuickCreate() {
this.schemeVisible = true;
},
handleSchemeConfirm({ template, taskCode, taskType, belongCompany, coilIds }) {
handleSchemeConfirm({ template, taskCode, taskType, belongCompany, coilIds, enterCoilNos }) {
this.taskLoading = true;
addInspectionTaskWithItems({
taskCode: taskCode,
@@ -574,7 +616,8 @@ export default {
schemeName: template.templateName,
belongCompany: belongCompany,
taskType: taskType,
coilIds: coilIds
coilIds: coilIds || undefined,
enterCoilNos: enterCoilNos || undefined
}).then(() => {
this.$modal.msgSuccess("快捷创建成功");
this.getTaskList();
@@ -655,6 +698,37 @@ export default {
this.taskQueryParams.schemeName = this.selectedScheme || undefined;
this.handleTaskQuery();
},
queryEnterCoilNo(queryString, cb) {
if (!queryString || queryString.length < 2) {
cb([]);
return;
}
this.enterCoilNoLoading = true;
listMaterialCoil({
enterCoilNo: queryString,
pageNum: 1,
pageSize: 20
}).then(response => {
const options = (response.rows || []).map(item => ({
value: item.enterCoilNo,
label: item.enterCoilNo
})).filter(opt => !this.selectedEnterCoilNos.includes(opt.value));
cb(options);
}).finally(() => {
this.enterCoilNoLoading = false;
});
},
handleSelectEnterCoilNo(item) {
if (!this.selectedEnterCoilNos.includes(item.value)) {
this.selectedEnterCoilNos.push(item.value);
}
this.tempEnterCoilNo = '';
this.taskForm.enterCoilNos = this.selectedEnterCoilNos.join(',');
},
removeEnterCoilNo(index) {
this.selectedEnterCoilNos.splice(index, 1);
this.taskForm.enterCoilNos = this.selectedEnterCoilNos.join(',');
},
cancelTask() {
this.taskOpen = false;
this.resetTaskForm();
@@ -683,6 +757,8 @@ export default {
updateBy: undefined
};
this.taskCoilList = [];
this.selectedEnterCoilNos = [];
this.tempEnterCoilNo = '';
this.resetForm("taskForm");
},

View File

@@ -68,9 +68,9 @@
<div class="rh-item"><span class="rh-k">辊型</span><span class="rh-v">{{ selectedRoll.rollType === 'WR' ? '工作辊' : '支撑辊' }}</span></div>
<div class="rh-item"><span class="rh-k">材质</span><span class="rh-v">{{ selectedRoll.material || '—' }}</span></div>
<div class="rh-item"><span class="rh-k">初始辊径</span><span class="rh-v">{{ selectedRoll.initialDia != null ? selectedRoll.initialDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">当前辊径</span><span class="rh-v bold accent">{{ selectedRoll.currentDia != null ? selectedRoll.currentDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">当前辊径</span><span class="rh-v bold accent">{{ effectiveCurrentDia != null ? effectiveCurrentDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">最小辊径</span><span class="rh-v">{{ selectedRoll.minDia != null ? selectedRoll.minDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">磨削次数</span><span class="rh-v">{{ selectedRoll.grindCount != null ? selectedRoll.grindCount + ' 次' : '0 次' }}</span></div>
<div class="rh-item"><span class="rh-k">磨削次数</span><span class="rh-v">{{ tableData.length ? tableData.length + ' 次' : '0 次' }}</span></div>
<div class="rh-item"><span class="rh-k">粗糙度</span><span class="rh-v">{{ selectedRoll.roughness != null ? selectedRoll.roughness + ' μm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">凸度</span><span class="rh-v">{{ selectedRoll.crown != null ? selectedRoll.crown + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">状态</span>
@@ -296,6 +296,22 @@ export default {
})
},
// 有效当前辊径:优先取 currentDia无则取最新磨削记录的磨后径
effectiveCurrentDia() {
if (this.selectedRoll && this.selectedRoll.currentDia != null) {
return parseFloat(this.selectedRoll.currentDia)
}
if (this.grindList.length > 0) {
const latest = [...this.grindList].sort((a, b) => {
const ta = a.grindTime ? new Date(a.grindTime).getTime() : 0
const tb = b.grindTime ? new Date(b.grindTime).getTime() : 0
return tb - ta
})[0]
if (latest && latest.diaAfter != null) return parseFloat(latest.diaAfter)
}
return null
},
// 表格数据:新增时在顶部插入一个编辑行占位
tableData() {
if (this.editRow && this.editRow.__isNew) {
@@ -401,8 +417,7 @@ export default {
rollId: this.selectedRollId,
grindTime,
team: undefined,
diaBefore: this.selectedRoll && this.selectedRoll.currentDia != null
? parseFloat(this.selectedRoll.currentDia) : undefined,
diaBefore: this.effectiveCurrentDia != null ? this.effectiveCurrentDia : undefined,
diaAfter: undefined,
rollShape: '平',
flawResult: '合格',

View File

@@ -11,6 +11,27 @@
</div>
</div>
<div class="section basic-info-section">
<div class="section-header">
<span class="section-icon">💰</span>
<span class="section-title">成本信息</span>
</div>
<div class="section-body">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="囤积成本">
<span class="cost-value">{{ hoardingCost }}</span>
<span class="cost-unit">t·</span>
</el-descriptions-item>
<el-descriptions-item label="囤积天数">
<span>{{ hoardingDays }} </span>
</el-descriptions-item>
<el-descriptions-item label="钢卷净重">
<span>{{ coilInfo.netWeight || 0 }} t</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div class="section trace-section">
<div class="section-header">
<span class="section-icon">🔄</span>
@@ -1218,12 +1239,30 @@ export default {
salesInfo() {
return this.coilInfo.orderList?.[0] || {};
},
// 判断是否为冷硬卷
isColdHardCoil() {
return this.coilInfo.itemName && this.coilInfo.itemName.includes('冷硬卷');
},
hasPerfData() {
return this.perfSeries && this.perfSegCount > 0;
},
hoardingDays() {
const inboundTime = this.getInboundTime();
if (!inboundTime) return 0;
const inboundDate = new Date(inboundTime);
let endDate;
if (this.coilInfo.status == 1) {
endDate = new Date(this.coilInfo.exportTime);
} else {
endDate = new Date();
}
// const today = new Date();
const diffTime = endDate.getTime() - inboundDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return Math.max(0, diffDays);
},
hoardingCost() {
const netWeight = parseFloat(this.coilInfo.netWeight) || 0;
return (this.hoardingDays * netWeight).toFixed(2);
}
},
async created() {
@@ -1343,7 +1382,7 @@ export default {
async getInspectionTasks() {
this.inspectionLoading = true;
try {
const res = await listInspectionTask({ coilIds: this.coilId, pageNum: 1, pageSize: 100 });
const res = await listInspectionTask({ enterCoilNos: this.coilInfo.enterCoilNo, pageNum: 1, pageSize: 100 });
this.inspectionTaskList = res.rows || [];
} catch (e) {
console.error('获取检验任务异常:', e);
@@ -1367,6 +1406,18 @@ export default {
this.$set(this.inspectionItemLoadingMap, taskId, false);
}
},
getInboundTime() {
if (!this.traceResult || !this.traceResult.steps) {
return this.coilInfo.createTime || null;
}
const createStep = this.traceResult.steps.find(step =>
step.action === '新增' || step.action === '创建'
);
if (createStep) {
return createStep.create_time || createStep.update_time || createStep.time;
}
return this.coilInfo.createTime || null;
},
formatTime(timeStamp) {
if (!timeStamp) return '-';
const date = new Date(timeStamp);
@@ -3332,4 +3383,17 @@ export default {
color: #c0c4cc;
font-size: 13px;
}
.cost-value {
font-size: 20px;
font-weight: 700;
color: #d97706;
text-shadow: 0 0 8px rgba(217, 119, 6, 0.4);
}
.cost-unit {
font-size: 12px;
color: #64748b;
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<div class="app-container">
<el-tabs v-model="activeLine" type="card" @tab-click="handleQuery">
<el-tab-pane label="酸连轧" name="acid" />
<el-tab-pane label="双机架" name="dr" />
<el-tab-pane label="镀锌" name="cgl" />
<el-tab-pane label="镀铬" name="ecl" />
<el-tab-pane label="脱脂" name="spl" />
<el-tab-pane label="纵剪" name="sl" />
</el-tabs>
<div class="mb8" style="display:flex;align-items:center;gap:12px;padding:12px 0">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期"
value-format="yyyy-MM-dd HH:mm:ss" :default-time="['00:00:00', '23:59:59']"
style="width:360px" />
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<span style="color:#999;font-size:13px">查询二级实绩中的热卷是否已在三级更新</span>
</div>
<KLPTable v-loading="loading" :data="list" max-height="600">
<el-table-column label="出口卷号" align="center" prop="excoilId" width="150" />
<el-table-column label="热卷号" align="center" prop="hotCoilId" width="150" />
<el-table-column label="下线时间" align="center" width="150">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.endDate) }}</span>
</template>
</el-table-column>
<el-table-column label="工序" align="center" prop="processCode" width="80" />
<el-table-column label="对比状态" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.matchStatus === 0" type="success">已加工</el-tag>
<el-tag v-else-if="scope.row.matchStatus === 1" type="warning">未加工</el-tag>
<el-tag v-else type="danger">未入库</el-tag>
</template>
</el-table-column>
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo" width="150" />
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo" width="150" />
<el-table-column label="供应商钢卷号" align="center" prop="supplierCoilNo" width="150" />
<el-table-column label="物品名称" align="center" prop="itemName" width="120" />
<el-table-column label="物品编码" align="center" prop="itemCode" width="120" />
<el-table-column label="规格" align="center" prop="specification" width="110" />
<el-table-column label="材质" align="center" prop="material" width="100" />
<el-table-column label="生产厂家" align="center" prop="manufacturer" width="100" />
<el-table-column label="库区" align="center" prop="warehouseName" width="130" />
<el-table-column label="钢卷状态" align="center" prop="coilStatus" width="80">
<template slot-scope="scope">
<span v-if="scope.row.coilStatus === 0">在库</span>
<span v-else-if="scope.row.coilStatus === 1">在途</span>
<span v-else-if="scope.row.coilStatus === 2">已出库</span>
<span v-else>-</span>
</template>
</el-table-column>
</KLPTable>
</div>
</template>
<script>
import { getExcoilStatus } from "@/api/wms/coilComparison";
export default {
name: "CoilComparison",
data() {
return {
loading: false,
activeLine: 'acid',
dateRange: [],
list: [],
};
},
created() {
const end = new Date();
const start = new Date();
this.dateRange = [start, end];
this.handleQuery();
},
methods: {
handleQuery() {
this.loading = true;
const params = {};
if (this.dateRange && this.dateRange.length === 2) {
params.startTime = this.dateRange[0];
params.endTime = this.dateRange[1];
} else {
const today = new Date();
params.startTime = this.formatDateTime(today, '00:00:00');
params.endTime = this.formatDateTime(today, '23:59:59');
}
params.lineType = this.activeLine;
getExcoilStatus(params).then(res => {
this.list = res.data || [];
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
formatDateTime(date, time) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day} ${time}`;
},
},
};
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
<template>
<component :is="printComponent" :waybill="wayBill" :waybillDetails="wayBillDetails" />
</template>
<script>
import wayBill from './wayBill.vue';
import wayBill2 from './wayBill2.vue';
import ZincWayBill1 from './ZincWayBill1.vue';
import ZincWayBill2 from './ZincWayBill2.vue';
import DugeWayBill1 from './DugeWayBill1.vue';
import DugeWayBill2 from './DugeWayBill2.vue';
export default {
name: 'WayBillPrinter',
components: {
wayBill,
wayBill2,
ZincWayBill1,
ZincWayBill2,
DugeWayBill1,
DugeWayBill2,
},
props: {
wayBill: {
type: Object,
default: () => {},
},
wayBillDetails: {
type: Array,
default: () => [],
},
// 0 表示含单价的单据1 表示不含单价的单据
printType: {
type: Number,
default: 0,
},
},
computed: {
printComponent() {
if (this.wayBillDetails.length == 0) {
if (this.printType == 0) {
return 'wayBill';
} else {
return 'wayBill2';
}
} else {
const productName = this.wayBillDetails[0].productName;
if (productName.includes('锌')) {
if (this.printType == 0) {
return 'ZincWayBill1';
} else {
return 'ZincWayBill2';
}
} else if (productName.includes('铬')) {
if (this.printType == 0) {
return 'DugeWayBill1';
} else {
return 'DugeWayBill2';
}
} else {
// 其他商品根据printType判断使用哪个组件
if (this.printType == 0) {
return 'wayBill';
} else {
return 'wayBill2';
}
}
}
}
}
}
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -230,8 +230,9 @@
<!-- 打印发货单对话框 -->
<el-dialog title="打印发货单" :visible.sync="printDialogVisible" width="1000px" append-to-body center>
<WayBill v-if="printType === 0" :waybill="currentWaybill" :waybillDetails="currentWaybillDetails" />
<WayBill2 v-else :waybill="currentWaybill" :waybillDetails="currentWaybillDetails" />
<!-- <WayBill v-if="printType === 0" :waybill="currentWaybill" :waybillDetails="currentWaybillDetails" />
<WayBill2 v-else :waybill="currentWaybill" :waybillDetails="currentWaybillDetails" /> -->
<WayBillPrinter :wayBill="currentWaybill" :wayBillDetails="currentWaybillDetails" :printType="printType" />
</el-dialog>
</div>
</template>
@@ -245,20 +246,22 @@ import { listDeliveryWaybillDetail } from "@/api/wms/deliveryWaybillDetail";
import { listOrder } from "@/api/crm/order";
import MemoInput from "@/components/MemoInput";
import DeliveryWaybillDetail from "../components/detailTable.vue";
import WayBill from "../components/wayBill.vue";
// import WayBill from "../components/wayBill.vue";
import PlanList from "../components/planList.vue";
import WayBill2 from "../components/wayBill2.vue";
// import WayBill2 from "../components/wayBill2.vue";
import PlanSelector from "../components/planSelector.vue";
import DragResizePanel from "@/components/DragResizePanel";
import WayBillPrinter from "../components/WayBillPrinter.vue";
export default {
name: "Exp-Waybill",
components: {
MemoInput,
DeliveryWaybillDetail,
WayBill,
WayBillPrinter,
// WayBill,
PlanList,
WayBill2,
// WayBill2,
PlanSelector,
DragResizePanel
},
@@ -693,9 +696,14 @@ export default {
};
this.currentWaybillDetails = this.currentWaybillDetails.map(item => {
const actualWarehouseName = response.rows.find(detail => detail.coilId === item.coilId)?.actualWarehouseName || '';
const zincLayer = response.rows.find(detail => detail.coilId === item.coilId)?.zincLayer || '';
const temperGrade = response.rows.find(detail => detail.coilId === item.coilId)?.temperGrade || '';
console.log(zincLayer, temperGrade, actualWahouseNames);
return {
...item,
actualWarehouseName: actualWarehouseName,
zincLayer: zincLayer || '',
temperGrade: temperGrade || '',
};
});
});

View File

@@ -11,13 +11,14 @@
<div class="operation-bar">
<el-button plain type="primary" icon="el-icon-plus" @click="handleCreate">创建排班</el-button>
<el-button plain type="info" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
<el-button plain type="success" icon="el-icon-setting" @click="showTemplateManager = true">管理模板</el-button>
</div>
<el-alert type="info" title="提示:双击排班单元格可编辑排班"></el-alert>
<!-- 排班表格 -->
<div class="schedule-table-wrapper">
<el-table v-loading="loading" :data="scheduleData" border stripe>
<el-table v-loading="loading" :data="scheduleData" border stripe height="calc(100vh - 200px)">
<!-- 员工列 -->
<el-table-column prop="employeeName" label="员工" width="120" fixed="left" />
<!-- 操作列 -->
@@ -53,45 +54,147 @@
</div>
<!-- 创建排班弹窗 -->
<el-dialog title="创建排班" :visible.sync="dialogVisible" width="800px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="时间段" prop="dateRange">
<el-date-picker v-model="form.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width: 100%;" />
</el-form-item>
<el-form-item label="选择员工" prop="selectedEmployees">
<EmployeeSelector v-model="form.selectedEmployees" :multiple="true" placeholder="请点击选择员工" title="选择排班员工"
@change="handleEmployeeSelect" />
</el-form-item>
<el-form-item label="排班配置" v-if="form.shiftConfig.length > 0">
<div class="shift-config">
<div v-for="(item, index) in form.shiftConfig" :key="index" class="shift-config-item">
<div class="employee-info">
<el-button icon="el-icon-delete" type="default" size="mini" @click="handleDeleteEmployee(index)"></el-button>
<el-tag type="info">{{ item.employeeName || '员工' + (index + 1) }}</el-tag>
</div>
<template>
<el-select clearable v-model="item.shiftId" placeholder="选择班次" style="width: 150px;">
<el-option v-for="shift in shiftList" :key="shift.shiftId" :label="shift.shiftName"
:value="shift.shiftId" />
</el-select>
<div class="shift-time" v-if="item.shiftId">
<span class="time-label">工作时间</span>
<span class="time-value">{{ getShiftTime(item.shiftId) }}</span>
</div>
<el-dialog title="创建排班" :visible.sync="dialogVisible" width="900px">
<!-- 步骤指示器 -->
<div class="steps">
<div :class="['step', { active: currentStep >= 1, done: currentStep > 1 }]">
<span class="step-number">1</span>
<span class="step-text">选择班次</span>
</div>
<div class="step-arrow"></div>
<div :class="['step', { active: currentStep >= 2, done: currentStep > 2 }]">
<span class="step-number">2</span>
<span class="step-text">分配人员</span>
</div>
<div class="step-arrow"></div>
<div :class="['step', { active: currentStep >= 3 }]">
<span class="step-number">3</span>
<span class="step-text">确认生成</span>
</div>
</div>
<el-select clearable v-model="item.shiftRuleId" placeholder="选择倒班规则(不倒班则不填)" style="width: 200px;">
<el-option v-for="rule in shiftRuleList" :key="rule.ruleId" :label="rule.ruleName"
:value="rule.ruleId" />
</el-select>
</template>
<!-- 步骤1选择班次配置 -->
<div v-if="currentStep === 1" class="step-content">
<el-form ref="form" :model="form" label-width="100px">
<el-form-item label="时间段">
<el-date-picker v-model="form.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期"
value-format="yyyy-MM-dd" style="width: 100%;" />
</el-form-item>
<el-form-item label="班次配置">
<div class="shift-config-panel">
<div v-for="(item, index) in form.shiftList" :key="index" class="shift-config-row">
<div class="shift-config-header">
<span class="config-label">班次 {{ index + 1 }}</span>
<el-button icon="el-icon-plus" size="mini" @click="addShiftItem">添加班次</el-button>
<el-button v-if="form.shiftList.length > 1" icon="el-icon-delete" size="mini"
@click="removeShiftItem(index)">删除</el-button>
</div>
<div class="shift-config-fields">
<el-select v-model="item.shiftId" placeholder="选择班次" style="width: 180px;" clearable>
<el-option v-for="shift in shiftList" :key="shift.shiftId"
:label="shift.shiftName" :value="shift.shiftId" />
</el-select>
<span v-if="item.shiftId" class="shift-time-display">
{{ getShiftTime(item.shiftId) }}
</span>
<el-select v-model="item.ruleId" placeholder="倒班规则(可选)" style="width: 180px;" clearable>
<el-option v-for="rule in shiftRuleList" :key="rule.ruleId"
:label="rule.changeDays" :value="rule.ruleId" />
</el-select>
</div>
</div>
</div>
</el-form-item>
</el-form>
</div>
<!-- 步骤2为每个班次分配人员 -->
<div v-if="currentStep === 2" class="step-content">
<div v-for="(item, index) in form.shiftList" :key="index" class="shift-assignment">
<div class="assignment-header">
<el-tag type="primary">{{ getShiftName(item.shiftId) || '未选择班次' }}</el-tag>
<span class="assignment-count">
已分配 {{ item.employeeIds ? item.employeeIds.split(',').filter(id => id.trim()).length : 0 }}
</span>
<div class="import-export-buttons">
<el-button icon="el-icon-download" size="mini" type="success" @click="exportCsv(index)"
title="导出CSV">导出人员</el-button>
<el-button icon="el-icon-upload" size="mini" type="info" @click="importCsv(index)"
title="导入CSV">导入人员</el-button>
<el-button icon="el-icon-save" size="mini" type="warning" @click="saveSingleTemplate(index)"
title="保存为模板">存储为模板</el-button>
<el-button icon="el-icon-folder-open" size="mini" type="primary" @click="openTemplateDialog(index)"
title="使用模板">使用模板</el-button>
</div>
</div>
</el-form-item>
</el-form>
<EmployeeSelector
v-model="item.employeeIds"
:multiple="true"
:disabled-names="getExcludedIds(index)"
placeholder="选择该班次的员工"
title="选择班次员工" />
<input type="file" ref="fileInput" class="file-input" accept=".csv" @change="handleFileChange($event, index)" />
</div>
<!-- <div class="quick-actions">
<el-button type="success" icon="el-icon-random" @click="quickAssignByDepartment">
按部门自动分配
</el-button>
</div> -->
</div>
<!-- 步骤3确认生成 -->
<div v-if="currentStep === 3" class="step-content">
<el-table :data="previewData" border style="width: 100%;">
<el-table-column prop="shiftName" label="班次" />
<el-table-column prop="employeeNames" label="员工列表" />
<el-table-column prop="count" label="人数" align="center" />
</el-table>
<div class="preview-summary">
<span> {{ totalEmployeeCount }} 名员工{{ totalScheduleCount }} 条排班记录</span>
</div>
</div>
<!-- 步骤导航按钮 -->
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="buttonLoading" :disabled="!canSubmit">确定</el-button>
<el-button @click="cancel" v-if="currentStep === 1">取消</el-button>
<template v-else>
<el-button @click="prevStep">上一步</el-button>
</template>
<template v-if="currentStep < 3">
<el-button type="primary" @click="nextStep" :disabled="!canProceed" :loading="buttonLoading">
{{ currentStep === 1 ? '下一步:分配人员' : '下一步:确认' }}
</el-button>
</template>
<template v-else>
<el-button type="primary" @click="submitForm" :loading="buttonLoading">确认生成</el-button>
</template>
</div>
</el-dialog>
<!-- 模板管理弹窗 -->
<el-dialog title="人员列表模板管理" :visible.sync="showTemplateManager" width="1000px" :close-on-click-modal="false">
<AttendanceTemplateManager
:employee-list="employeeListForTransfer"
@update="loadTemplates"
/>
</el-dialog>
<!-- 班次模板选择弹窗 -->
<el-dialog title="选择人员模板" :visible.sync="showTemplateDialog" width="500px">
<el-table :data="templateList" border style="width: 100%;">
<el-table-column prop="name" label="模板名称" />
<el-table-column prop="employeeCount" label="员工数量" align="center" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click.stop="applySingleTemplate(scope.row)">应用</el-button>
<el-button size="mini" type="danger" @click.stop="deleteSingleTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="showTemplateDialog = false">关闭</el-button>
</div>
</el-dialog>
@@ -132,13 +235,15 @@
<script>
import TimeRangePicker from '@/views/wms/report/components/timeRangePicker.vue'
import EmployeeSelector from '@/components/EmployeeSelector/index.vue'
import AttendanceTemplateManager from '@/components/AttendanceTemplateManager/index.vue'
import { listAttendanceSchedule, generateenerateSchedule, updateAttendanceSchedule, addAttendanceSchedule, delAttendanceSchedule } from '@/api/wms/attendanceSchedule'
import { listShift } from '@/api/wms/attendanceShift'
import { listAttendanceShiftRule } from '@/api/wms/attendanceShiftRule'
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
export default {
name: 'AttendanceSchedule',
components: { TimeRangePicker, EmployeeSelector },
components: { TimeRangePicker, EmployeeSelector, AttendanceTemplateManager },
data() {
return {
loading: false,
@@ -152,11 +257,17 @@ export default {
shiftRuleList: [],
dialogVisible: false,
editDialogVisible: false,
scheduleMode: 'single', // single: 普通排班, rotate: 倒班排班
showTemplateDialog: false,
showTemplateManager: false,
templateList: [],
currentShiftIndex: -1,
employeeList: [],
currentStep: 1, // 步骤1-选择班次2-分配人员3-确认
form: {
dateRange: [],
selectedEmployees: '',
shiftConfig: []
shiftList: [
{ shiftId: '', ruleId: '', employeeIds: '' }
]
},
editForm: {
shiftId: ''
@@ -174,33 +285,72 @@ export default {
}
},
computed: {
canSubmit() {
if (!this.form.dateRange || this.form.dateRange.length === 0) {
return false
}
if (!this.form.selectedEmployees) {
return false
}
if (this.form.shiftConfig.length === 0) {
return false
}
if (this.scheduleMode === 'single') {
return this.form.shiftConfig.every(item => item.shiftId)
} else {
return this.form.shiftConfig.every(item => item.shiftRuleId)
// 检查是否可以进入下一步
canProceed() {
if (this.currentStep === 1) {
// 步骤1必须选择时间段和至少一个有效班次
if (!this.form.dateRange || this.form.dateRange.length === 0) return false
return this.form.shiftList.some(item => item.shiftId)
} else if (this.currentStep === 2) {
// 步骤2至少有一个班次分配了人员
return this.form.shiftList.some(item => item.employeeIds && item.employeeIds.trim())
}
return true
},
// 预览数据
previewData() {
return this.form.shiftList
.filter(item => item.shiftId && item.employeeIds)
.map(item => ({
shiftName: this.getShiftName(item.shiftId),
employeeNames: this.getEmployeeNames(item.employeeIds),
count: item.employeeIds.split(',').filter(id => id.trim()).length
}))
},
// 总员工数
totalEmployeeCount() {
const allIds = new Set()
this.form.shiftList.forEach(item => {
if (item.employeeIds) {
item.employeeIds.split(',').forEach(id => {
if (id.trim()) allIds.add(id.trim())
})
}
})
return allIds.size
},
// 总排班记录数
totalScheduleCount() {
let count = 0
this.form.shiftList.forEach(item => {
if (item.employeeIds) {
count += item.employeeIds.split(',').filter(id => id.trim()).length
}
})
return count
},
currentShift() {
if (!this.editForm.shiftId) {
return null
}
return this.shiftList.find(s => s.shiftId === this.editForm.shiftId)
},
employeeListForTransfer() {
return this.employeeList.map(emp => ({
key: emp.id,
label: emp.name
}))
}
},
created() {
this.initDateRange()
this.getShiftList()
this.getShiftRuleList()
this.loadTemplates()
this.getEmployeeList()
},
methods: {
// 刷新排班
@@ -529,74 +679,113 @@ export default {
// 重置表单
reset() {
this.scheduleMode = 'single'
this.currentStep = 1
this.form = {
dateRange: [],
selectedEmployees: '',
shiftConfig: []
shiftList: [
{ shiftId: '', ruleId: '', employeeIds: '' }
]
}
this.resetForm('form')
},
// 员工选择变化时自动生成配置项
handleEmployeeSelect(employees) {
if (!employees || employees.length === 0) {
this.form.shiftConfig = []
return
}
// 为每个选中的员工生成配置项
if (this.scheduleMode === 'single') {
this.form.shiftConfig = employees.map(employee => ({
employeeId: employee.infoId,
employeeName: employee.name,
shiftId: null
}))
} else {
this.form.shiftConfig = employees.map(employee => ({
employeeId: employee.infoId,
employeeName: employee.name,
shiftRuleId: null
}))
if (this.$refs['form']) {
this.$refs['form'].resetFields()
}
},
// 排班模式切换
handleScheduleModeChange() {
// 模式切换时重新生成配置项
if (this.form.selectedEmployees && this.form.selectedEmployees.length > 0) {
this.handleEmployeeSelect(this.form.selectedEmployees)
// 添加班次配置项
addShiftItem() {
this.form.shiftList.push({ shiftId: '', ruleId: '', employeeIds: '' })
},
// 移除班次配置项
removeShiftItem(index) {
this.form.shiftList.splice(index, 1)
},
// 复制班次配置(不复制人员)
copyShiftItem(index) {
const source = this.form.shiftList[index]
this.form.shiftList.push({
shiftId: source.shiftId,
ruleId: source.ruleId,
employeeIds: ''
})
},
// 获取排除的员工ID避免重复分配
getExcludedIds(currentIndex) {
const excluded = []
this.form.shiftList.forEach((item, index) => {
if (index !== currentIndex && item.employeeIds) {
excluded.push(...item.employeeIds.split(','))
}
})
return excluded.join(',').trim()
},
// 获取班次名称
getShiftName(shiftId) {
const shift = this.shiftList.find(s => s.shiftId === shiftId)
return shift ? shift.shiftName : '未知班次'
},
// 获取员工姓名临时实现实际应从API获取
getEmployeeNames(employeeIds) {
if (!employeeIds) return '未分配'
return employeeIds.split(',').filter(id => id.trim()).length + ' 名员工'
},
// 下一步
nextStep() {
if (this.currentStep < 3) {
this.currentStep++
}
},
// 上一步
prevStep() {
if (this.currentStep > 1) {
this.currentStep--
}
},
// 按部门自动分配
quickAssignByDepartment() {
this.$message.info('按部门自动分配功能开发中...')
// 实际实现时可以调用API获取部门员工列表并分配
},
// 提交表单
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
this.loading = true;
this.buttonLoading = true;
// 构建提交数据
const list = []
for (const item of this.form.shiftConfig) {
const payload = {
userId: item.employeeId,
shiftId: item.shiftId,
ruleId: item.shiftRuleId,
startDate: this.dateRangeParams.scheduleDateStart.split(' ')[0],
endDate: this.dateRangeParams.scheduleDateEnd.split(' ')[0]
}
list.push(payload)
}
this.buttonLoading = true
// 调用API
generateenerateSchedule(list).then(response => {
this.$modal.msgSuccess('生成成功')
this.dialogVisible = false
this.loading = false;
this.buttonLoading = false;
this.getScheduleList()
// 构建提交数据
const list = []
const startDate = this.form.dateRange[0]
const endDate = this.form.dateRange[1]
this.form.shiftList.forEach(shiftItem => {
if (!shiftItem.shiftId || !shiftItem.employeeIds) return
const employeeIds = shiftItem.employeeIds.split(',').filter(id => id.trim())
employeeIds.forEach(employeeId => {
list.push({
userId: employeeId,
shiftId: shiftItem.shiftId,
ruleId: shiftItem.ruleId,
startDate: startDate,
endDate: endDate
})
}
})
})
// 调用API
generateenerateSchedule(list).then(response => {
this.$modal.msgSuccess('生成成功')
this.dialogVisible = false
this.buttonLoading = false
this.getScheduleList()
}).catch(() => {
this.buttonLoading = false
})
},
@@ -604,6 +793,189 @@ export default {
cancel() {
this.dialogVisible = false
this.reset()
},
// 导出CSV文件
exportCsv(index) {
const shiftItem = this.form.shiftList[index]
const shiftName = this.getShiftName(shiftItem.shiftId) || '班次'
const ruleName = shiftItem.ruleId ? this.getRuleName(shiftItem.ruleId) : ''
let csvContent = '名字\n'
if (shiftItem.employeeIds) {
const ids = shiftItem.employeeIds.split(',').filter(id => id.trim())
ids.forEach(id => {
csvContent += `${id}\n`
})
}
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
const fileName = `${shiftName}${ruleName ? '_' + ruleName : ''}_员工列表.csv`
link.setAttribute('href', url)
link.setAttribute('download', fileName)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
// 导入CSV文件
importCsv(index) {
const fileInput = document.querySelector('.file-input')
if (fileInput) {
fileInput.click()
}
},
// 处理文件选择
handleFileChange(event, index) {
const file = event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target.result
const lines = content.split('\n').filter(line => line.trim())
if (lines.length === 0) {
this.$message.warning('CSV文件内容为空')
return
}
const header = lines[0].trim()
if (header !== '名字') {
this.$message.warning('CSV文件格式不正确第一行应为"名字"')
return
}
const names = lines.slice(1).map(line => line.trim()).filter(name => name)
if (names.length === 0) {
this.$message.warning('CSV文件中没有有效的员工名字')
return
}
const shiftItem = this.form.shiftList[index]
shiftItem.employeeIds = names.join(',')
this.$message.success(`成功导入 ${names.length} 名员工`)
}
reader.readAsText(file, 'UTF-8')
event.target.value = ''
},
// 获取规则名称
getRuleName(ruleId) {
const rule = this.shiftRuleList.find(r => r.ruleId === ruleId)
return rule ? rule.changeDays : ''
},
// 获取员工列表
getEmployeeList() {
const params = {
pageNum: 1,
pageSize: 9999,
}
listEmployeeInfo(params).then(response => {
// 过滤掉已离职的员工
const filteredList = (response.rows || []).filter(employee => {
return employee.isLeave !== 1 && employee.isLeave !== '1'
})
this.employeeList = filteredList.map(item => ({
id: item.infoId,
name: item.name
}))
}).catch(() => {
this.employeeList = []
})
},
// 加载模板列表
loadTemplates() {
try {
const templates = localStorage.getItem('attendanceTemplates')
this.templateList = templates ? JSON.parse(templates) : []
} catch (e) {
this.templateList = []
}
},
// 保存模板到localStorage
saveTemplates() {
localStorage.setItem('attendanceTemplates', JSON.stringify(this.templateList))
},
// 保存单个班次为模板
saveSingleTemplate(index) {
const shiftItem = this.form.shiftList[index]
if (!shiftItem.employeeIds || !shiftItem.employeeIds.trim()) {
this.$message.warning('请先分配人员')
return
}
this.$prompt('请输入模板名称', '保存模板', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: this.getShiftName(shiftItem.shiftId) + '_' + new Date().toLocaleDateString()
}).then(({ value }) => {
if (!value.trim()) {
this.$message.warning('模板名称不能为空')
return
}
const employeeIds = shiftItem.employeeIds.split(',').filter(id => id.trim())
const template = {
id: Date.now().toString(),
name: value.trim(),
employeeIds: employeeIds,
employeeCount: employeeIds.length,
createTime: new Date().toLocaleString('zh-CN')
}
this.templateList.push(template)
this.saveTemplates()
this.$message.success('模板保存成功')
}).catch(() => {
this.$message.info('已取消保存')
})
},
// 打开模板选择弹窗
openTemplateDialog(index) {
this.currentShiftIndex = index
this.showTemplateDialog = true
},
// 应用单个模板到当前班次
applySingleTemplate(template) {
if (!template || !template.employeeIds || this.currentShiftIndex < 0) return
this.form.shiftList[this.currentShiftIndex].employeeIds = template.employeeIds.join(',')
this.showTemplateDialog = false
this.currentShiftIndex = -1
this.$message.success('模板应用成功')
},
// 删除单个模板
deleteSingleTemplate(template) {
this.$confirm('确定要删除这个模板吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.templateList.findIndex(t => t.id === template.id)
if (index > -1) {
this.templateList.splice(index, 1)
this.saveTemplates()
this.$message.success('删除成功')
}
}).catch(() => {
this.$message.info('已取消删除')
})
}
}
}
@@ -716,4 +1088,161 @@ export default {
font-size: 12px;
color: #606266;
}
/* ===== 优化后的排班弹窗样式 ===== */
/* 步骤指示器 */
.steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
}
.step-number {
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 50%;
background-color: #e4e7ed;
color: #909399;
font-size: 14px;
font-weight: bold;
text-align: center;
transition: all 0.3s;
}
.step.active .step-number {
background-color: #409eff;
color: #fff;
}
.step.done .step-number {
background-color: #67c23a;
color: #fff;
}
.step-text {
margin-top: 8px;
font-size: 13px;
color: #909399;
}
.step.active .step-text,
.step.done .step-text {
color: #606266;
}
.step-arrow {
margin: 0 12px;
color: #c0c4cc;
font-size: 16px;
}
/* 步骤内容 */
.step-content {
min-height: 200px;
}
/* 班次配置面板 */
.shift-config-panel {
margin-top: 12px;
}
.shift-config-row {
margin-bottom: 16px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.shift-config-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.config-label {
font-weight: 600;
color: #606266;
}
.shift-config-fields {
display: flex;
align-items: center;
gap: 16px;
}
.shift-time-display {
font-size: 13px;
color: #67c23a;
padding: 4px 8px;
background-color: #f0f9eb;
border-radius: 4px;
}
/* 班次分配区域 */
.shift-assignment {
margin-bottom: 20px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.assignment-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.assignment-count {
font-size: 13px;
color: #909399;
}
.import-export-buttons {
display: flex;
gap: 8px;
margin-left: auto;
}
.file-input {
display: none;
}
/* 快速操作按钮 */
.quick-actions {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #e4e7ed;
}
/* 预览摘要 */
.preview-summary {
margin-top: 16px;
padding: 12px;
background-color: #f0f9eb;
border-radius: 4px;
text-align: center;
color: #67c23a;
font-size: 14px;
}
/* 对话框按钮 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -2,26 +2,14 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="调拨单号" prop="transferNo">
<el-input
v-model="queryParams.transferNo"
placeholder="请输入调拨单号"
clearable
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.transferNo" placeholder="请输入调拨单号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="调拨单名称" prop="transferName">
<el-input
v-model="queryParams.transferName"
placeholder="请输入调拨单名称,用于帮助记忆与沟通"
clearable
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.transferName" placeholder="请输入调拨单名称,用于帮助记忆与沟通" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="调拨时间" prop="transferTime">
<el-date-picker clearable
v-model="queryParams.transferTime"
type="date"
value-format="yyyy-MM-dd"
<el-date-picker clearable v-model="queryParams.transferTime" type="date" value-format="yyyy-MM-dd"
placeholder="请选择实际调拨时间">
</el-date-picker>
</el-form-item>
@@ -45,42 +33,18 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
@click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@@ -96,7 +60,7 @@
</template>
</el-table-column>
<el-table-column label="调拨类型" align="center" prop="transferType" width="120"></el-table-column>
<el-table-column label="审批状态" align="center" prop="approveStatus" width="120">
<el-table-column label="审批状态" align="center" prop="approveStatus" width="120">
<template slot-scope="scope">
<el-tag :type="getapproveStatusType(scope.row.approveStatus)">
{{ getapproveStatusText(scope.row.approveStatus) }}
@@ -104,7 +68,7 @@
</template>
</el-table-column>
<el-table-column label="审批人" align="center" prop="approver" />
<el-table-column label="审批时间" align="center" prop="approveTime" width="180">
<template slot-scope="scope">
<span>{{ scope.row.approveTime ? parseTime(scope.row.approveTime, '{y}-{m}-{d} {h}:{i}') : '-' }}</span>
@@ -113,77 +77,27 @@
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
>查看</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
:disabled="!canEdit(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
:disabled="!canDelete(scope.row)"
>删除</el-button>
<el-button
size="mini"
plain
type="text"
@click="handleSubmitapprove(scope.row)"
v-if="(scope.row.approveStatus || '0') == '0'"
icon="el-icon-check"
>提交</el-button>
<el-button
size="mini"
plain
type="text"
@click="handleWithdrawapprove(scope.row)"
v-if="scope.row.approveStatus == '1'"
icon="el-icon-refresh"
>撤回</el-button>
<el-button
size="mini"
type="text"
plain
@click="handleReSubmit(scope.row)"
v-if="scope.row.approveStatus == '3'"
icon="el-icon-refresh"
>二次调整</el-button>
<el-button
size="mini"
type="text"
v-hasPermi="['coil:move:approval']"
@click="handleResolve(scope.row)"
icon="el-icon-check"
v-if="scope.row.approveStatus == '1'"
>通过</el-button>
<el-button
size="mini"
type="text"
v-hasPermi="['coil:move:approval']"
@click="handleReject(scope.row)"
v-if="scope.row.approveStatus == '1'"
icon="el-icon-close"
>驳回</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
:disabled="!canEdit(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
:disabled="!canDelete(scope.row)">删除</el-button>
<el-button size="mini" plain type="text" @click="handleSubmitapprove(scope.row)"
v-if="(scope.row.approveStatus || '0') == '0'" icon="el-icon-check">提交</el-button>
<el-button size="mini" plain type="text" @click="handleWithdrawapprove(scope.row)"
v-if="scope.row.approveStatus == '1'" icon="el-icon-refresh">撤回</el-button>
<el-button size="mini" type="text" plain @click="handleReSubmit(scope.row)"
v-if="scope.row.approveStatus == '3'" icon="el-icon-refresh">二次调整</el-button>
<el-button size="mini" type="text" v-hasPermi="['coil:move:approval']" @click="handleResolve(scope.row)"
icon="el-icon-check" v-if="scope.row.approveStatus == '1'">通过</el-button>
<el-button size="mini" type="text" v-hasPermi="['coil:move:approval']" @click="handleReject(scope.row)"
v-if="scope.row.approveStatus == '1'" icon="el-icon-close">驳回</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"
/>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 添加或修改调拨单主对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@@ -195,10 +109,7 @@
<el-input v-model="form.transferName" placeholder="请输入调拨单名称,用于帮助记忆与沟通" />
</el-form-item>
<el-form-item label="调拨时间" prop="transferTime">
<el-date-picker clearable
v-model="form.transferTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
<el-date-picker clearable v-model="form.transferTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择调拨时间">
</el-date-picker>
</el-form-item>
@@ -219,11 +130,15 @@
<el-dialog title="调拨单明细" v-loading="detailLoading" :visible.sync="detailOpen" fullscreen>
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<el-button icon="el-icon-download" type="warning" plain @click="handleExportDetail">导出</el-button>
<el-button style="margin-right: 10px;" icon="el-icon-refresh" type="success" plain @click="handleRefreshDetailList">刷新</el-button>
<coil-selector v-loading="buttonLoading" ref="coilSelector" :filters="{ dataType: 1, status: 0 }" multiple @confirm="handleCoilChange" v-if="canAddCoils"></coil-selector>
<el-button style="margin-right: 10px;" icon="el-icon-refresh" type="success" plain
@click="handleRefreshDetailList">刷新</el-button>
<coil-selector v-loading="buttonLoading" ref="coilSelector" :filters="{ dataType: 1, status: 0 }" multiple
@confirm="handleCoilChange" v-if="canAddCoils"></coil-selector>
<import-coil v-if="canAddCoils" :transferId="currentOrderId" @success="handleImportSuccess" />
<el-checkbox v-model="batchEdit" style="margin-right: 10px;">批量操作</el-checkbox>
</div>
<transfer-item-table ref="transferItemTable" :data="transferOrderItems" :batchEdit="batchEdit" @refreshData="getDetailList" :orderStatus="currentOrderStatus" :canEdit="canAddCoils" />
<transfer-item-table ref="transferItemTable" :data="transferOrderItems" :batchEdit="batchEdit"
@refreshData="getDetailList" :orderStatus="currentOrderStatus" :canEdit="canAddCoils" />
</el-dialog>
</div>
</template>
@@ -233,12 +148,14 @@ import { listTransferOrder, getTransferOrder, delTransferOrder, addTransferOrder
import { listTransferOrderItem, batchAddTransferOrderItem } from "@/api/wms/transferOrderItem";
import CoilSelector from "@/components/CoilSelector";
import TransferItemTable from "@/views/wms/move/components/tranferItemTable.vue";
import ImportCoil from "@/views/wms/move/components/ImportCoil.vue";
export default {
name: "TransferOrder",
components: {
CoilSelector,
TransferItemTable,
ImportCoil,
},
data() {
return {
@@ -267,7 +184,7 @@ export default {
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 50,
transferNo: undefined,
transferName: undefined,
transferStatus: undefined,
@@ -317,20 +234,34 @@ export default {
});
},
handleResolve(row) {
approveTransferOrder(row.orderId, '2').then(response => {
updateTransferOrder({
...row,
approveStatus: '2',
}).then(_ => {
this.$modal.msgSuccess('审批成功');
this.getList();
}).finally(() => {
this.loading = false;
});
})
// approveTransferOrder(row.orderId, '2').then(response => {
// this.$modal.msgSuccess('审批成功');
// this.getList();
// }).finally(() => {
// this.loading = false;
// });
},
handleReject(row) {
approveTransferOrder(row.orderId, '3').then(response => {
this.$modal.msgSuccess('审批成功');
updateTransferOrder({
...row,
approveStatus: '3',
}).then(_ => {
this.$modal.msgSuccess('已驳回调拨单');
this.getList();
}).finally(() => {
this.loading = false;
});
})
// approveTransferOrder(row.orderId, '3').then(response => {
// this.$modal.msgSuccess('已驳回调拨单');
// this.getList();
// }).finally(() => {
// this.loading = false;
// });
},
/** 获取审批状态文本 */
getapproveStatusText(status) {
@@ -468,7 +399,7 @@ export default {
'coil_supplierCoilNo': '厂家卷号',
// 'coil_warehouseName': '逻辑库区',
'coil_netWeight': '净重',
// 'coil_itemName': '物品名称',
// 'coil_materialType': '物料类型',
'coil_qualityStatus': '质量状态',
@@ -484,7 +415,7 @@ export default {
// 准备导出数据
const exportData = this.transferOrderItems.map(item => {
const flatItem = {};
// 处理Before和After字段
exportFields.forEach(field => {
if (field.startsWith('coil_')) {
@@ -494,7 +425,7 @@ export default {
flatItem[field] = item[field] !== null && item[field] !== undefined ? item[field] : '';
}
});
return flatItem;
});
@@ -525,7 +456,7 @@ export default {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
},
/** 钢卷选择器改变 */
handleCoilChange(coils) {
console.log(coils);
@@ -544,31 +475,35 @@ export default {
this.buttonLoading = false;
})
},
/** 导入钢卷成功回调 */
handleImportSuccess() {
this.handleView({ orderId: this.currentOrderId });
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
orderId: undefined,
transferNo: undefined,
transferName: undefined,
transferStatus: undefined,
transferTime: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
updateBy: undefined,
createTime: undefined,
updateTime: undefined,
approveStatus: '0',
approver: undefined,
approveTime: undefined
};
this.resetForm("form");
},
reset() {
this.form = {
orderId: undefined,
transferNo: undefined,
transferName: undefined,
transferStatus: undefined,
transferTime: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
updateBy: undefined,
createTime: undefined,
updateTime: undefined,
approveStatus: '0',
approver: undefined,
approveTime: undefined
};
this.resetForm("form");
},
getDetailList() {
this.handleView({ orderId: this.currentOrderId, approveStatus: this.currentOrderStatus });
},
@@ -601,7 +536,7 @@ export default {
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.orderId)
this.single = selection.length!==1
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
@@ -610,18 +545,18 @@ export default {
// 生成当前时间
const currentDate = new Date();
// 生成调拨单号TR + 年月日时分秒
const transferNo = 'TR' + currentDate.getFullYear() +
String(currentDate.getMonth() + 1).padStart(2, '0') +
String(currentDate.getDate()).padStart(2, '0') +
String(currentDate.getHours()).padStart(2, '0') +
String(currentDate.getMinutes()).padStart(2, '0') +
const transferNo = 'TR' + currentDate.getFullYear() +
String(currentDate.getMonth() + 1).padStart(2, '0') +
String(currentDate.getDate()).padStart(2, '0') +
String(currentDate.getHours()).padStart(2, '0') +
String(currentDate.getMinutes()).padStart(2, '0') +
String(currentDate.getSeconds()).padStart(2, '0');
// 生成调拨时间YYYY-MM-DD HH:mm:ss
const transferTime = currentDate.getFullYear() + '-' +
String(currentDate.getMonth() + 1).padStart(2, '0') + '-' +
String(currentDate.getDate()).padStart(2, '0') + ' ' +
String(currentDate.getHours()).padStart(2, '0') + ':' +
String(currentDate.getMinutes()).padStart(2, '0') + ':' +
const transferTime = currentDate.getFullYear() + '-' +
String(currentDate.getMonth() + 1).padStart(2, '0') + '-' +
String(currentDate.getDate()).padStart(2, '0') + ' ' +
String(currentDate.getHours()).padStart(2, '0') + ':' +
String(currentDate.getMinutes()).padStart(2, '0') + ':' +
String(currentDate.getSeconds()).padStart(2, '0');
// 填充表单
this.form.transferNo = transferNo;

View File

@@ -0,0 +1,819 @@
<template>
<div class="import-coil-container">
<!-- 操作栏 -->
<div class="action-bar">
<el-upload
ref="upload"
class="upload-excel"
action=""
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
accept=".xlsx,.xls"
:disabled="isProcessing"
>
<el-button type="primary" icon="el-icon-upload2" size="small">选择Excel文件</el-button>
</el-upload>
<el-button
type="default"
icon="el-icon-download"
size="small"
@click="downloadTemplate"
>
下载模板
</el-button>
<span v-if="fileName" class="file-name">{{ fileName }}</span>
</div>
<!-- 导入预览弹窗 -->
<el-dialog
:visible.sync="dialogVisible"
:title="dialogTitle"
width="900px"
top="5vh"
:close-on-click-modal="false"
:close-on-press-escape="false"
append-to-body
custom-class="import-coil-dialog"
@close="handleDialogClose"
>
<!-- 匹配摘要统计卡片 -->
<div v-if="step === 'REVIEW' || step === 'IMPORTING' || step === 'FINISHED'" class="summary-cards">
<el-row :gutter="15">
<el-col :span="6">
<div class="stat-card success">
<div class="stat-icon"><i class="el-icon-check"></i></div>
<div class="stat-info">
<div class="stat-value">{{ matchedRows.length }}</div>
<div class="stat-label">已匹配</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card warning">
<div class="stat-icon"><i class="el-icon-warning-outline"></i></div>
<div class="stat-info">
<div class="stat-value">{{ conflictRows.length }}</div>
<div class="stat-label">冲突</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card danger">
<div class="stat-icon"><i class="el-icon-close"></i></div>
<div class="stat-info">
<div class="stat-value">{{ unmatchedRows.length }}</div>
<div class="stat-label">未匹配</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card primary">
<div class="stat-icon"><i class="el-icon-suitcase"></i></div>
<div class="stat-info">
<div class="stat-value">{{ pendingImportCount }}</div>
<div class="stat-label">待导入</div>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 数据预览解析后匹配前 -->
<div v-if="step === 'PARSED'" class="preview-section">
<el-alert
title="数据预览"
type="info"
:description="'共解析出 ' + parsedData.length + ' 条数据,点击「开始匹配」进行钢卷匹配'"
show-icon
:closable="false"
style="margin-bottom: 15px;"
/>
<el-table :data="parsedData" border size="small" max-height="400" stripe>
<el-table-column prop="rowNum" label="行号" width="60" align="center" />
<el-table-column prop="enterCoilNo" label="入场卷号" min-width="150" />
<el-table-column prop="currentCoilNo" label="当前卷号" min-width="150" />
</el-table>
</div>
<!-- 匹配进度 -->
<div v-if="step === 'MATCHING'" class="matching-progress-section">
<div class="matching-progress-header">
<i class="el-icon-loading" style="font-size: 20px; color: #409eff;"></i>
<span class="matching-progress-title">正在匹配钢卷...</span>
<span class="matching-progress-percent">{{ matchingPercentage }}%</span>
</div>
<el-progress
:percentage="matchingPercentage"
:stroke-width="18"
:show-text="false"
status="success"
/>
<div class="matching-progress-detail">
<span>{{ matchingProgress }} / {{ matchingTotal }}</span>
<span v-if="currentMatchingText" class="matching-current">
当前: {{ currentMatchingText }}
<el-tag v-if="lastMatchResult === 'matched'" size="mini" type="success">已匹配</el-tag>
<el-tag v-else-if="lastMatchResult === 'conflict'" size="mini" type="warning">冲突</el-tag>
<el-tag v-else-if="lastMatchResult === 'unmatched'" size="mini" type="danger">未匹配</el-tag>
</span>
</div>
</div>
<!-- 标签页内容匹配结果 -->
<div v-if="step === 'REVIEW' || step === 'IMPORTING' || step === 'FINISHED'" class="tabs-section">
<el-tabs v-model="activeTab" type="border-card">
<!-- 已匹配 -->
<el-tab-pane :label="'已匹配 (' + matchedRows.length + ')'" name="matched">
<div v-if="matchedRows.length === 0" class="empty-tip">
<i class="el-icon-success" style="color: #67c23a; font-size: 48px;"></i>
<p>没有已匹配的钢卷</p>
</div>
<el-table v-else :data="matchedRows" border size="small" max-height="350" stripe>
<el-table-column prop="rowNum" label="行号" width="60" align="center" />
<el-table-column prop="enterCoilNo" label="入场卷号" min-width="120" />
<el-table-column prop="currentCoilNo" label="当前卷号" min-width="120" />
<el-table-column label="匹配钢卷" min-width="120">
<template slot-scope="{ row }">
<el-tag size="small" type="success">{{ row.matchedCoil.currentCoilNo }}</el-tag>
</template>
</el-table-column>
<el-table-column label="净重" width="100" align="right">
<template slot-scope="{ row }">
{{ row.matchedCoil.netWeight }}
</template>
</el-table-column>
<el-table-column label="库区" min-width="120">
<template slot-scope="{ row }">
{{ row.matchedCoil.warehouseName || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="{ $index }">
<el-button type="text" size="mini" @click="removeMatchedRow($index)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 冲突 -->
<el-tab-pane :label="'冲突 (' + conflictRows.length + ')'" name="conflict">
<div v-if="conflictRows.length === 0" class="empty-tip">
<i class="el-icon-warning" style="color: #e6a23c; font-size: 48px;"></i>
<p>没有冲突的钢卷</p>
</div>
<div v-else class="conflict-list">
<el-alert
title="以下数据匹配到多条钢卷,请为每行选择正确的钢卷"
type="warning"
:closable="false"
style="margin-bottom: 10px;"
/>
<el-table :data="conflictRows" border size="small" max-height="350" stripe>
<el-table-column prop="rowNum" label="行号" width="60" align="center" />
<el-table-column prop="enterCoilNo" label="入场卷号" min-width="120" />
<el-table-column prop="currentCoilNo" label="当前卷号" min-width="120" />
<el-table-column label="选择钢卷" min-width="280">
<template slot-scope="{ row }">
<el-select
v-model="row.selectedCoilId"
placeholder="请选择正确的钢卷"
size="small"
style="width: 100%;"
@change="handleConflictSelect(row)"
>
<el-option
v-for="coil in row.candidates"
:key="coil.coilId"
:label="coil.currentCoilNo + ' | 净重:' + coil.netWeight + ' | ' + (coil.warehouseName || '')"
:value="coil.coilId"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row.selectedCoilId" size="small" type="success">已选择</el-tag>
<el-tag v-else size="small" type="danger">待选择</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<!-- 未匹配 -->
<el-tab-pane :label="'未匹配 (' + unmatchedRows.length + ')'" name="unmatched">
<div v-if="unmatchedRows.length === 0" class="empty-tip">
<i class="el-icon-circle-check" style="color: #909399; font-size: 48px;"></i>
<p>没有未匹配的钢卷</p>
</div>
<el-table v-else :data="unmatchedRows" border size="small" max-height="350" stripe>
<el-table-column prop="rowNum" label="行号" width="60" align="center" />
<el-table-column prop="enterCoilNo" label="入场卷号" min-width="150" />
<el-table-column prop="currentCoilNo" label="当前卷号" min-width="150" />
<el-table-column prop="reason" label="原因" min-width="200">
<template slot-scope="{ row }">
<span style="color: #f56c6c;">{{ row.reason }}</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
<!-- 导入进度 -->
<div v-if="step === 'IMPORTING'" class="progress-section">
<el-progress :percentage="importProgress" status="success" :stroke-width="18" />
<p class="progress-text">正在导入 {{ importedCount }} / {{ pendingImportCount }} 条钢卷...</p>
</div>
<!-- 导入完成 -->
<div v-if="step === 'FINISHED'" class="finished-section">
<div class="success-icon">
<i class="el-icon-circle-check"></i>
</div>
<h3>导入完成</h3>
<p>成功导入 {{ importedCount }} 条钢卷到调拨单</p>
</div>
<!-- 底部按钮 -->
<div slot="footer" class="dialog-footer">
<!-- 解析后开始匹配 -->
<template v-if="step === 'PARSED'">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="matchLoading" @click="handleMatch">
开始匹配
</el-button>
</template>
<!-- 匹配后确认导入 -->
<template v-if="step === 'REVIEW'">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="importLoading"
:disabled="pendingImportCount === 0"
@click="handleImport"
>
确认导入 ({{ pendingImportCount }})
</el-button>
</template>
<!-- 匹配中 -->
<template v-if="step === 'MATCHING'">
<el-button disabled>匹配中 {{ matchingProgress }}/{{ matchingTotal }}...</el-button>
</template>
<!-- 导入中禁用关闭 -->
<template v-if="step === 'IMPORTING'">
<el-button disabled>导入中...</el-button>
</template>
<!-- 导入完成关闭 -->
<template v-if="step === 'FINISHED'">
<el-button type="primary" @click="handleFinish">完成</el-button>
</template>
</div>
</el-dialog>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { listMaterialCoil } from '@/api/wms/coil';
import { batchAddTransferOrderItem } from '@/api/wms/transferOrderItem';
// Excel表头定义
const REQUIRED_HEADERS = ['入场卷号', '当前卷号'];
export default {
name: 'ImportCoil',
props: {
transferId: {
type: [String, Number],
required: true
}
},
data() {
return {
step: 'IDLE', // IDLE | PARSED | MATCHING | REVIEW | IMPORTING | FINISHED
dialogVisible: false,
activeTab: 'matched',
fileName: '',
parsedData: [],
matchedRows: [],
conflictRows: [],
unmatchedRows: [],
matchLoading: false,
importLoading: false,
importProgress: 0,
importedCount: 0,
// 匹配进度
matchingTotal: 0,
matchingProgress: 0,
currentMatchingText: '',
lastMatchResult: '',
};
},
computed: {
isProcessing() {
return this.step === 'MATCHING' || this.step === 'IMPORTING';
},
matchingPercentage() {
if (this.matchingTotal === 0) return 0;
return Math.round((this.matchingProgress / this.matchingTotal) * 100);
},
dialogTitle() {
switch (this.step) {
case 'PARSED':
return '导入钢卷 - 数据预览 (' + this.parsedData.length + '条)';
case 'MATCHING':
return '导入钢卷 - 匹配中 (' + this.matchingProgress + '/' + this.matchingTotal + ')';
case 'REVIEW':
case 'IMPORTING':
case 'FINISHED':
return '导入钢卷 - 匹配结果 (' + this.parsedData.length + '条)';
default:
return '导入钢卷';
}
},
// 待导入数量 = 已匹配 + 冲突中已选择的
pendingImportCount() {
let count = this.matchedRows.length;
this.conflictRows.forEach(r => {
if (r.selectedCoilId) count++;
});
return count;
},
// 所有待导入的coilId
pendingCoilIds() {
const ids = this.matchedRows.map(r => r.matchedCoil.coilId);
this.conflictRows.forEach(r => {
if (r.selectedCoilId) {
ids.push(r.selectedCoilId);
}
});
return ids;
}
},
methods: {
/** 下载Excel模板 */
downloadTemplate() {
const wsData = [REQUIRED_HEADERS, ['(示例)G2024001', '(示例)G2024001']];
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = [{ wch: 20 }, { wch: 20 }];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '钢卷导入');
XLSX.writeFile(wb, '调拨单钢卷导入模板.xlsx');
},
/** 选择文件 */
handleFileChange(file) {
if (this.isProcessing) {
this.$message.warning('当前有操作正在进行中');
return;
}
this.fileName = file.name;
this.readExcel(file.raw);
},
/** 读取Excel */
readExcel(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
// 校验表头
const headers = jsonData[0] || [];
const headerErrors = this.validateHeaders(headers);
if (headerErrors.length > 0) {
this.$message.error(headerErrors.join(''));
return;
}
// 解析数据行
const rows = jsonData.slice(1).filter(row => row.some(cell => cell !== undefined && cell !== null && String(cell).trim() !== ''));
if (rows.length === 0) {
this.$message.warning('Excel中没有有效数据');
return;
}
this.parsedData = rows.map((row, index) => ({
rowNum: index + 2,
enterCoilNo: row[0] ? String(row[0]).trim() : '',
currentCoilNo: row[1] ? String(row[1]).trim() : '',
})).filter(item => item.enterCoilNo || item.currentCoilNo);
// 重置匹配结果
this.matchedRows = [];
this.conflictRows = [];
this.unmatchedRows = [];
this.step = 'PARSED';
this.activeTab = 'matched';
this.dialogVisible = true;
this.$message.success('成功解析 ' + this.parsedData.length + ' 条数据');
} catch (error) {
this.$message.error('解析Excel失败' + error.message);
}
};
reader.readAsArrayBuffer(file);
},
/** 校验表头 */
validateHeaders(headers) {
const errors = [];
if (headers.length < REQUIRED_HEADERS.length) {
errors.push('表头列数不匹配,需要' + REQUIRED_HEADERS.length + '列,实际' + headers.length + '列');
return errors;
}
REQUIRED_HEADERS.forEach((h, i) => {
if (headers[i] !== h) {
errors.push('第' + (i + 1) + '列表头应为"' + h + '",实际为"' + headers[i] + '"');
}
});
return errors;
},
/** 匹配钢卷 */
async handleMatch() {
if (this.parsedData.length === 0) return;
this.matchLoading = true;
this.step = 'MATCHING';
this.matchedRows = [];
this.conflictRows = [];
this.unmatchedRows = [];
this.matchingTotal = this.parsedData.length;
this.matchingProgress = 0;
this.currentMatchingText = '';
this.lastMatchResult = '';
try {
// 逐行匹配
for (const row of this.parsedData) {
this.currentMatchingText = row.enterCoilNo || row.currentCoilNo;
this.lastMatchResult = '';
await this.matchOneRow(row);
this.matchingProgress++;
}
this.step = 'REVIEW';
// 自动切换到第一个有数据的tab
if (this.matchedRows.length > 0) {
this.activeTab = 'matched';
} else if (this.conflictRows.length > 0) {
this.activeTab = 'conflict';
} else if (this.unmatchedRows.length > 0) {
this.activeTab = 'unmatched';
}
this.$message.success('匹配完成,请查看结果');
} catch (error) {
this.$message.error('匹配过程出错:' + error.message);
this.step = 'PARSED';
} finally {
this.matchLoading = false;
}
},
/** 匹配单行 */
async matchOneRow(row) {
try {
const params = { dataType: 1, status: 0, pageNum: 1, pageSize: 50 };
if (row.enterCoilNo) params.enterCoilNo = row.enterCoilNo;
if (row.currentCoilNo) params.currentCoilNo = row.currentCoilNo;
const res = await listMaterialCoil(params);
const list = res.rows || [];
if (list.length === 1) {
// 场景1精确匹配1条
this.matchedRows.push({ ...row, matchedCoil: list[0] });
this.lastMatchResult = 'matched';
} else if (list.length > 1) {
// 场景2匹配到多条需用户选择
this.conflictRows.push({ ...row, candidates: list, selectedCoilId: null });
this.lastMatchResult = 'conflict';
} else {
// 场景3未匹配
this.unmatchedRows.push({ ...row, reason: '未找到匹配的钢卷' });
this.lastMatchResult = 'unmatched';
}
} catch (error) {
this.unmatchedRows.push({ ...row, reason: '查询失败:' + error.message });
this.lastMatchResult = 'unmatched';
}
},
/** 冲突行选择钢卷 */
handleConflictSelect(row) {
const selected = row.candidates.find(c => c.coilId === row.selectedCoilId);
if (selected) {
this.$set(row, 'selectedLabel', selected.currentCoilNo);
}
},
/** 移除已匹配行 */
removeMatchedRow(index) {
this.matchedRows.splice(index, 1);
},
/** 确认导入 */
async handleImport() {
const coilIds = this.pendingCoilIds;
if (coilIds.length === 0) {
this.$message.warning('没有可导入的钢卷');
return;
}
// 检查冲突行是否都已选择
const unresolvedConflicts = this.conflictRows.filter(r => !r.selectedCoilId);
if (unresolvedConflicts.length > 0) {
this.$message.warning('还有 ' + unresolvedConflicts.length + ' 条冲突未选择,未选择的将被跳过');
}
this.step = 'IMPORTING';
this.importLoading = true;
this.importProgress = 0;
this.importedCount = 0;
try {
await batchAddTransferOrderItem({
transferId: this.transferId,
coilIds: coilIds
});
this.importedCount = coilIds.length;
this.importProgress = 100;
this.step = 'FINISHED';
this.$message.success('成功导入 ' + coilIds.length + ' 条钢卷');
} catch (error) {
this.$message.error('导入失败:' + error.message);
this.step = 'REVIEW';
} finally {
this.importLoading = false;
}
},
/** 完成导入 */
handleFinish() {
this.dialogVisible = false;
this.$emit('success', this.importedCount);
this.resetData();
},
/** 弹窗关闭 */
handleDialogClose() {
if (this.step === 'IMPORTING') {
this.$message.warning('导入进行中,请勿关闭');
this.dialogVisible = true;
return;
}
this.resetData();
},
/** 重置数据 */
resetData() {
this.step = 'IDLE';
this.fileName = '';
this.parsedData = [];
this.matchedRows = [];
this.conflictRows = [];
this.unmatchedRows = [];
this.importProgress = 0;
this.importedCount = 0;
this.matchingTotal = 0;
this.matchingProgress = 0;
this.currentMatchingText = '';
this.lastMatchResult = '';
this.activeTab = 'matched';
this.$refs.upload && this.$refs.upload.clearFiles();
}
}
};
</script>
<style scoped>
.import-coil-container {
display: inline-block;
}
.action-bar {
display: flex;
align-items: center;
gap: 10px;
}
.file-name {
color: #909399;
font-size: 12px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 统计卡片 */
.summary-cards {
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
padding: 15px;
border-radius: 8px;
background: #f5f7fa;
border-left: 4px solid;
}
.stat-card.success {
border-left-color: #67c23a;
background: #f0f9eb;
}
.stat-card.warning {
border-left-color: #e6a23c;
background: #fdf6ec;
}
.stat-card.danger {
border-left-color: #f56c6c;
background: #fef0f0;
}
.stat-card.primary {
border-left-color: #409eff;
background: #ecf5ff;
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-right: 12px;
background: #fff;
}
.stat-card.success .stat-icon {
color: #67c23a;
}
.stat-card.warning .stat-icon {
color: #e6a23c;
}
.stat-card.danger .stat-icon {
color: #f56c6c;
}
.stat-card.primary .stat-icon {
color: #409eff;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
line-height: 1;
}
.stat-label {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
/* 预览区域 */
.preview-section {
margin-top: 10px;
}
/* 匹配进度区域 */
.matching-progress-section {
padding: 30px 20px;
}
.matching-progress-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.matching-progress-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-left: 8px;
}
.matching-progress-percent {
font-size: 20px;
font-weight: bold;
color: #67c23a;
margin-left: auto;
}
.matching-progress-detail {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
color: #606266;
font-size: 14px;
}
.matching-current {
display: flex;
align-items: center;
gap: 6px;
color: #909399;
}
/* 标签页区域 */
.tabs-section {
margin-top: 10px;
}
/* 空状态 */
.empty-tip {
text-align: center;
padding: 40px 0;
color: #909399;
}
.empty-tip p {
margin-top: 10px;
}
/* 冲突列表 */
.conflict-list {
padding: 10px 0;
}
/* 进度区域 */
.progress-section {
padding: 40px 20px;
text-align: center;
}
.progress-text {
margin-top: 15px;
color: #606266;
font-size: 14px;
}
/* 完成区域 */
.finished-section {
text-align: center;
padding: 40px 20px;
}
.success-icon {
font-size: 80px;
color: #67c23a;
margin-bottom: 20px;
}
.finished-section h3 {
font-size: 20px;
color: #303133;
margin-bottom: 10px;
}
.finished-section p {
color: #606266;
font-size: 14px;
}
/* 底部按钮 */
.dialog-footer {
text-align: right;
}
/* 弹窗自定义样式 */
.import-coil-dialog >>> .el-dialog__body {
max-height: calc(90vh - 180px);
overflow-y: auto;
padding: 20px;
}
.import-coil-dialog >>> .el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #ebeef5;
}
.import-coil-dialog >>> .el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<el-table :data="tableData" style="width: 100%" border v-loading="confirmLoading">
<el-table :data="tableData" style="width: 100%" border v-loading="confirmLoading" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" v-if="canConfirm"></el-table-column>
<el-table-column prop="coilId" label="钢卷号">
<template slot-scope="scope">
<CoilNo :coil-no="scope.row.coil.enterCoilNo" :coil="scope.row.coil" />
@@ -15,7 +16,7 @@
{{ scope.row.materialTypeBefore == '1' ? '原料' : '产品' }}
</template>
</el-table-column>
<el-table-column prop="itemIdBefore" label="调拨前物料" width="260">
<el-table-column prop="itemIdBefore" label="调拨前物料" width="180">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.materialTypeBefore == '2'" :product="scope.row.oBefore" />
<RawMaterialInfo v-else-if="scope.row.materialTypeBefore == '1'" :material="scope.row.oBefore" />
@@ -35,7 +36,7 @@
</div>
</template>
</el-table-column>
<el-table-column prop="itemIdAfter" v-loading="materialLoading" label="调拨后物料" width="260">
<el-table-column prop="itemIdAfter" v-loading="materialLoading" label="调拨后物料" width="180">
<template slot-scope="scope">
<div v-loading="materialLoading" v-if="!scope.row.isConfirmed && canEdit">
<RawMaterialSelect v-model="scope.row.itemIdAfter" v-if="scope.row.materialTypeAfter == '1'" />
@@ -60,7 +61,7 @@
</template>
</el-table-column>
<el-table-column prop="isTransferred" label="生效状态" width="200">
<el-table-column prop="isTransferred" label="生效状态" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.isTransferred == '1' ? 'success' : 'info'">
{{ scope.row.isTransferred == '1' ? '已生效' : '未生效' }}
@@ -84,13 +85,22 @@
</div>
</template>
</el-table-column>
<el-table-column type="action" label="操作" width="180" v-if="canConfirm" fixed="right">
<template slot-scope="scope">
<el-button icon="el-icon-check" size="mini" v-if="!scope.row.isTransferred" @click="handleConfirm(scope.row)"
:loading="buttonLoading">执行</el-button>
<el-button icon="el-icon-close" size="mini" v-if="!scope.row.isTransferred" @click="handleDelete(scope.row)"
:loading="buttonLoading">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import {
matchOrCreateMaterial, confirmTransferOrderItem, cancelTransferOrderItem,
updateTransferOrderItem, matchOrCreateByCoils, batchUpdateTransferOrderItem
matchOrCreateMaterial, confirmTransferOrderItem, cancelTransferOrderItem, delTransferOrderItem,
updateTransferOrderItem, matchOrCreateByCoils, batchUpdateTransferOrderItem, batchConfirmTransferOrderItem
} from '@/api/wms/transferOrderItem'
import ProductSelect from "@/components/KLPService/ProductSelect";
import RawMaterialSelect from "@/components/KLPService/RawMaterialSelect";
@@ -116,7 +126,19 @@ export default {
batchEdit: {
type: Boolean,
default: false
}
},
canConfirm: {
type: Boolean,
default: false
},
refreshLabel: {
type: Boolean,
default: false
},
transferRemark: {
type: String,
default: ''
},
},
components: {
// WarehouseSelect,
@@ -133,7 +155,7 @@ export default {
warehouseList: [],
buttonLoading: false,
confirmLoading: false,
// loading: false,
selectedItems: [],
}
},
mounted() {
@@ -189,7 +211,7 @@ export default {
}
},
immediate: true
}
},
},
methods: {
// 获取库区列表
@@ -221,7 +243,57 @@ export default {
// 确认调拨
handleConfirm(item) {
this.buttonLoading = true
confirmTransferOrderItem(item).then(res => {
let transferRemark = this.transferRemark
if (this.refreshLabel && !transferRemark) {
transferRemark = '调拨'
}
if (!this.refreshLabel) {
transferRemark = ''
}
// console.log(transferRemark, this.refreshLabel, this.transferRemark)
// return;
confirmTransferOrderItem({
...item,
transferRemark
}).then(res => {
if (res.code === 200) {
this.$message({
message: '确认调拨成功',
type: 'success'
})
// 刷新数据
this.$emit('refreshData')
}
}).finally(() => {
this.buttonLoading = false
})
},
handleSelectionChange(val) {
this.selectedItems = val;
this.$emit('selectionChange', val);
},
// handleConfirmAll 批量确认调拨
handleBatchConfirm() {
const unconfirmedItems = this.selectedItems.filter(item => !item.isConfirmed && item.isTransferred != 1);
if (unconfirmedItems.length === 0) {
this.$message({ message: '没有可执行的调拨项', type: 'info' });
return;
}
let transferRemark = this.transferRemark
if (this.refreshLabel && !transferRemark) {
transferRemark = '调拨'
}
if (!this.refreshLabel) {
transferRemark = ''
}
// console.log(transferRemark)
// return;
this.buttonLoading = true;
batchConfirmTransferOrderItem(unconfirmedItems.map(item => ({
...item,
transferRemark
}))).then(res => {
if (res.code === 200) {
this.$message({
message: '确认调拨成功',
@@ -263,6 +335,28 @@ export default {
this.confirmLoading = false;
});
},
// 删除未执行的调拨记录
handleDelete(row) {
this.$confirm('确认要删除该调拨操作吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.buttonLoading = true
delTransferOrderItem(row.orderItemId).then(res => {
if (res.code === 200) {
this.$message({
message: '删除调拨操作成功',
type: 'success'
})
// 刷新数据
this.$emit('refreshData')
}
}).finally(() => {
this.buttonLoading = false
})
})
},
// 取消调拨
handleCancel(item) {
this.$confirm('确认取消调拨吗?', '提示', {

View File

@@ -0,0 +1,209 @@
<template>
<div class="record-container">
<!-- 常规筛选区, 通过调拨前库区和调拨后库区查询 -->
<el-form :model="queryParams" inline>
<!-- <el-form-item label="入场卷号">
<el-input v-model="queryParams.enterCoilNo" placeholder="请输入入场卷号"></el-input>
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="queryParams.currentCoilNo" placeholder="请输入当前卷号"></el-input>
</el-form-item> -->
<el-form-item label="调拨前库区">
<WarehouseSelect v-model="queryParams.warehouseIdBefore" placeholder="请选择调拨前库区">
</WarehouseSelect>
</el-form-item>
<el-form-item label="调拨后库区">
<WarehouseSelect v-model="queryParams.warehouseIdAfter" placeholder="请选择调拨后库区">
</WarehouseSelect>
</el-form-item>
<el-form-item label="调拨单号">
<el-select v-model="queryParams.transferNo" filterable remote clearable
remote-method="remoteSearchTransferOrder" :remote-loading="transferOrderLoading" placeholder="请输入调拨单号搜索"
@change="handleRegularSearch" style="width:220px">
<el-option v-for="item in transferOrderOptions" :key="item.orderId" :label="item.transferNo"
:value="item.transferNo" />
</el-select>
</el-form-item>
<el-form-item label="生效状态">
<el-select v-model="queryParams.isTransferred" @change="handleRegularSearch" clearable>
<el-option value="1" label="已生效"></el-option>
<el-option value="0" label="未生效"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" plain @click="handleRegularSearch">查询</el-button>
<el-button type="success" icon="el-icon-check" plain @click="handleConfirmBatch">批量执行</el-button>
</el-form-item>
</el-form>
<div style="display: flex; align-items: center; gap: 10px;">
<el-checkbox v-model="refreshLabel" @change="handleRegularSearch">执行后重贴标签</el-checkbox>
<el-input v-if="refreshLabel" style="width: 200px" v-model="transferRemark" placeholder="输入重贴备注"></el-input>
<span v-if="refreshLabel">请填写重贴原因不填则默认为调拨</span>
</div>
<!-- 调拨表格, 显示调拨记录, 常规筛选作用下生效 -->
<TransferItemTable :data="transferList" :canEdit="false" :canConfirm="true" v-loading="loading"
ref="transferItemTable" :refreshLabel="refreshLabel" :transferRemark="transferRemark"
@refreshData="handleRegularSearch"></TransferItemTable>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="handleRegularSearch" />
<!-- 弹窗: 展示通过特殊筛选区查询到的钢卷和相关的调拨记录,分组展示 -->
<el-dialog title="钢卷调拨记录追溯" :visible.sync="dialogVisible" width="80%" v-loading="loading">
<div v-for="coil in tracedCoils" :key="coil.id" class="coil-group">
<h3>
<!-- 更全面的展示钢卷的信息, 例如dataType: 0表示历史卷,1表示当前卷. createTime, createBy, quantityStatus, actualLength, netWeight, coilNo, enterCoilNo, status: 0未发货,1已发货. -->
<current-coil-no :currentCoilNo="coil.currentCoilNo"></current-coil-no>
({{ coil.enterCoilNo }})
<span style="margin-left: 20px; font-size: 14px; font-weight: normal;">
{{ coil.dataType === 1 ? '当前卷' : '历史卷' }} |
{{ coil.status === 1 ? '已发货' : '未发货' }}
</span>
</h3>
<el-descriptions :column="4" border style="margin-bottom: 20px;">
<el-descriptions-item label="创建时间">{{ coil.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ coil.createBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="质量状态">{{ coil.qualityStatus || '-' }}</el-descriptions-item>
<el-descriptions-item label="净重">{{ coil.netWeight || '-' }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ coil.itemName || '-' }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ coil.specification || '-' }}</el-descriptions-item>
<el-descriptions-item label="材质">{{ coil.material || '-' }}</el-descriptions-item>
<el-descriptions-item label="厂家">{{ coil.manufacturer || '-' }}</el-descriptions-item>
</el-descriptions>
<TransferItemTable :data="coil.transferRecords" :canEdit="false" :refreshLabel="refreshLabel" :transferRemark="transferRemark"></TransferItemTable>
</div>
</el-dialog>
</div>
</template>
<script>
import TransferItemTable from "@/views/wms/move/components/tranferItemTable.vue";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import { listTransferOrderItem } from "@/api/wms/transferOrderItem";
import { listTransferOrder } from "@/api/wms/transferOrder";
import { listMaterialCoil } from "@/api/wms/coil";
export default {
components: {
TransferItemTable,
WarehouseSelect,
},
data() {
return {
traceParams: {
currentCoilNo: '',
enterCoilNo: '',
},
refreshLabel: false,
transferRemark: '',
queryParams: {
warehouseIdBefore: '',
warehouseIdAfter: '',
transferNo: '',
isTransferred: '0',
pageSize: 20,
pageNum: 1,
},
transferList: [],
tracedCoils: [],
warehouseList: [],
transferOrderOptions: [],
transferOrderLoading: false,
loading: false,
total: 0,
dialogVisible: false,
};
},
methods: {
async remoteSearchTransferOrder(query) {
this.transferOrderLoading = true
try {
const params = { pageSize: 50, pageNum: 1 }
if (query) params.transferNo = query
const res = await listTransferOrder(params)
this.transferOrderOptions = res.rows || []
} catch (_) {
this.transferOrderOptions = []
} finally {
this.transferOrderLoading = false
}
},
async handleTraceSearch() {
if (!this.traceParams.currentCoilNo && !this.traceParams.enterCoilNo) {
this.$message.warning('请输入当前卷号或入库卷号');
return;
}
this.loading = true;
this.dialogVisible = true;
try {
// 查询符合条件的钢卷
const coilRes = await listMaterialCoil(this.traceParams);
if (coilRes.rows && coilRes.rows.length > 0) {
// 遍历钢卷,查询每个钢卷的调拨记录
const tracedCoilsData = await Promise.all(
coilRes.rows.map(async (coil) => {
const transferRes = await listTransferOrderItem({ coilId: coil.coilId });
return {
...coil,
transferRecords: transferRes.rows || [],
};
})
);
this.tracedCoils = tracedCoilsData.filter(coil => coil.transferRecords.length > 0);
} else {
this.$message.info('未找到符合条件的钢卷');
}
} catch (error) {
this.$message.error('查询失败,请重试');
console.error(error);
} finally {
this.loading = false;
}
},
async handleRegularSearch() {
this.loading = true;
try {
const res = await listTransferOrderItem(this.queryParams);
this.transferList = res.rows || [];
this.total = res.total || 0;
} catch (error) {
this.$message.error('查询失败,请重试');
console.error(error);
} finally {
this.loading = false;
}
},
handleConfirmBatch() {
this.$refs.transferItemTable.handleBatchConfirm();
},
},
mounted() {
this.remoteSearchTransferOrder('')
this.handleRegularSearch();
},
};
</script>
<style scoped>
.record-container {
padding: 20px;
}
.special-filter-card,
.regular-filter-card {
margin-bottom: 20px;
}
.coil-group {
margin-bottom: 30px;
}
.coil-group h3 {
margin-bottom: 15px;
color: #333;
font-size: 16px;
font-weight: bold;
}
</style>

View File

@@ -23,8 +23,16 @@
<WarehouseSelect v-model="queryParams.warehouseIdAfter" placeholder="请选择调拨后库区">
</WarehouseSelect>
</el-form-item>
<el-form-item label="调拨单号">
<el-select v-model="queryParams.transferNo" filterable remote clearable
remote-method="remoteSearchTransferOrder" :remote-loading="transferOrderLoading"
placeholder="请输入调拨单号搜索" @change="handleRegularSearch" style="width:220px">
<el-option v-for="item in transferOrderOptions" :key="item.orderId"
:label="item.transferNo" :value="item.transferNo" />
</el-select>
</el-form-item>
<el-form-item label="生效状态">
<el-select v-model="queryParams.isTransferred" @change="handleRegularSearch">
<el-select v-model="queryParams.isTransferred" @change="handleRegularSearch" clearable>
<el-option value="1" label="已生效"></el-option>
<el-option value="0" label="未生效"></el-option>
</el-select>
@@ -71,6 +79,7 @@
import TransferItemTable from "@/views/wms/move/components/tranferItemTable.vue";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import { listTransferOrderItem } from "@/api/wms/transferOrderItem";
import { listTransferOrder } from "@/api/wms/transferOrder";
import { listMaterialCoil } from "@/api/wms/coil";
export default {
@@ -87,6 +96,7 @@ export default {
queryParams: {
warehouseIdBefore: '',
warehouseIdAfter: '',
transferNo: '',
isTransferred: '',
pageSize: 20,
pageNum: 1,
@@ -94,12 +104,27 @@ export default {
transferList: [],
tracedCoils: [],
warehouseList: [],
transferOrderOptions: [],
transferOrderLoading: false,
loading: false,
total: 0,
dialogVisible: false,
};
},
methods: {
async remoteSearchTransferOrder(query) {
this.transferOrderLoading = true
try {
const params = { pageSize: 50, pageNum: 1 }
if (query) params.transferNo = query
const res = await listTransferOrder(params)
this.transferOrderOptions = res.rows || []
} catch (_) {
this.transferOrderOptions = []
} finally {
this.transferOrderLoading = false
}
},
async handleTraceSearch() {
if (!this.traceParams.currentCoilNo && !this.traceParams.enterCoilNo) {
this.$message.warning('请输入当前卷号或入库卷号');
@@ -148,7 +173,7 @@ export default {
},
},
mounted() {
this.remoteSearchTransferOrder('')
this.handleRegularSearch();
},
};

View File

@@ -101,8 +101,20 @@
<el-button type="primary" plain icon="el-icon-plus" size="mini"
@click="openDetailAddDialog">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="detailMultiple"
@click="handleDetailBatchDelete">批量删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini"
@click="handleDetailClear">清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="default" plain icon="el-icon-refresh" size="mini"
@click="getDetailList">刷新</el-button>
</el-col>
</el-row>
<el-table v-loading="detailLoading" :data="detailList" border>
<el-table v-loading="detailLoading" :data="detailList" border @selection-change="handleDetailSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="warehouseArea" label="逻辑库区" width="120" />
<el-table-column prop="lotNo" label="入场卷号" width="120" />
@@ -145,7 +157,7 @@
:limit.sync="detailQueryParams.pageSize" @pagination="getDetailList" />
</el-tab-pane>
<el-tab-pane label="实际入库表格" name="actual">
<el-table v-loading="actualLoading" :data="actualList" border>
<el-table v-loading="actualLoading" :data="actualList" border height="400">
<el-table-column prop="enterCoilNo" label="入场卷号" />
<el-table-column prop="currentCoilNo" label="成品卷号" />
<el-table-column prop="itemName" label="名称" width="120" />
@@ -153,7 +165,7 @@
<el-table-column prop="materialType" label="材质" width="100" />
<el-table-column prop="netWeight" label="重量(kg)" width="100" />
<el-table-column prop="manufacturer" label="生产厂家" width="120" />
<el-table-column prop="zincLayer" label="锌层" width="80" />
<el-table-column prop="zincLayer" label="镀层质量" width="80" />
<el-table-column prop="warehouseName" label="库区" width="100" />
<el-table-column prop="qualityStatus" label="质量状态" width="100" />
<el-table-column prop="createTime" label="入库时间">
@@ -389,7 +401,7 @@
<script>
import * as XLSX from 'xlsx';
import { listDeliveryPlan, getDeliveryPlan, delDeliveryPlan, addDeliveryPlan, updateDeliveryPlan } from "@/api/wms/deliveryPlan";
import { listReceivePlan, getReceivePlan, delReceivePlan, addReceivePlan, updateReceivePlan, checkReceivePlan } from "@/api/wms/receivePlan";
import { listReceivePlan, getReceivePlan, delReceivePlan, addReceivePlan, updateReceivePlan, checkReceivePlan, delReceivePlanBatch } from "@/api/wms/receivePlan";
import { listCoilWithIds } from "@/api/wms/coil";
import { listPendingAction } from "@/api/wms/pendingAction";
@@ -460,6 +472,9 @@ export default {
detailFormOpen: false,
detailFormTitle: '',
detailButtonLoading: false,
detailIds: [],
detailSingle: true,
detailMultiple: true,
detailForm: {},
detailRules: {
lotNo: [{ required: true, message: '入场卷号不能为空', trigger: 'blur' }]
@@ -645,7 +660,7 @@ export default {
calculateDiff() {
this.diffLoading = true;
Promise.all([
listReceivePlan({ pageNum: 1, pageSize: 1000, planId: this.currentPlanId }),
listReceivePlan({ pageNum: 1, pageSize: 99999, planId: this.currentPlanId }),
listPendingAction({
warehouseId: this.currentPlanId,
actionType: 401,
@@ -729,6 +744,9 @@ export default {
},
getDetailList() {
this.detailLoading = true;
this.detailIds = [];
this.detailSingle = true;
this.detailMultiple = true;
listReceivePlan(this.detailQueryParams).then(response => {
this.detailList = response.rows;
this.detailTotal = response.total;
@@ -789,6 +807,41 @@ export default {
this.$modal.msgSuccess("删除成功");
}).catch(() => { });
},
handleDetailSelectionChange(selection) {
this.detailIds = selection.map(item => item.receiveId);
this.detailSingle = this.detailIds.length !== 1;
this.detailMultiple = !this.detailIds.length;
},
handleDetailBatchDelete() {
const receiveIds = this.detailIds.join(',');
this.$modal.confirm('是否确认删除选中的' + this.detailIds.length + '条待收货明细?').then(() => {
this.detailLoading = true;
return delReceivePlan(receiveIds);
}).then(() => {
this.getDetailList();
this.$modal.msgSuccess("删除成功");
}).catch(() => { }).finally(() => {
this.detailLoading = false;
});
},
handleDetailClear() {
this.$modal.confirm('是否确认清空当前收货计划下的所有待收货明细?此操作不可恢复!').then(() => {
this.detailLoading = true;
return listReceivePlan({ pageNum: 1, pageSize: 99999, planId: this.currentPlanId });
}).then(response => {
const allIds = response.rows.map(item => item.receiveId);
if (allIds.length === 0) {
this.$modal.msgInfo('暂无数据可清空');
return;
}
return delReceivePlanBatch(allIds);
}).then(() => {
this.getDetailList();
this.$modal.msgSuccess("清空成功");
}).catch(() => { }).finally(() => {
this.detailLoading = false;
});
},
openImportDialog(row) {
this.currentPlanId = row.planId;
@@ -1019,7 +1072,7 @@ export default {
downloadTemplate() {
const templateData = [
TEMPLATE_HEADERS,
['A库区', 'LOT001', 'SUP001', 'PROD001', '2024-01-01', 1000.50, '钢板', '1.0*1000', 2000, 'Q235', '宝钢', '镀锌', '80', '甲班', '1.5', 'GI', '原料', '示例备注']
['A库区', 'LOT001', 'SUP001', 'PROD001', '2024-01-01', 1000.50, '钢板', '1.0*1000', 2000, 'Q235', '宝钢', '镀锌', '80', '甲班', '1.5', 'GI', '原料', '示例备注']
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(templateData);