This commit is contained in:
2026-05-23 09:18:29 +08:00
9 changed files with 989 additions and 101 deletions

View File

@@ -181,6 +181,59 @@ public class CrmOrderItemVo {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 更新人
*/
private String updateBy;
/**
* 删除标志
*/
private Long delFlag;
/**
* 合同号(联表查询直接映射)
*/
@ExcelProperty(value = "合同号")
private String contractCode;
/**
* 供方(联表查询直接映射)
*/
@ExcelProperty(value = "供方")
private String supplier;
/**
* 需方(联表查询直接映射)
*/
@ExcelProperty(value = "需方")
private String customer;
/**
* 签订时间(联表查询直接映射)
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@ExcelProperty(value = "签订时间")
private Date signTime;
/**
* 交货日期(联表查询直接映射)
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@ExcelProperty(value = "交货日期")
private Date deliveryDate;
/**
* 订单信息
*/

View File

@@ -1,8 +1,10 @@
package com.klp.crm.mapper;
import com.klp.crm.domain.CrmOrderItem;
import com.klp.crm.domain.bo.CrmOrderItemBo;
import com.klp.crm.domain.vo.CrmOrderItemVo;
import com.klp.common.core.mapper.BaseMapperPlus;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -22,4 +24,14 @@ public interface CrmOrderItemMapper extends BaseMapperPlus<CrmOrderItemMapper, C
* @return 订单明细列表
*/
List<CrmOrderItem> selectOrderItemsByOrderIds(@Param("orderIds") List<Long> orderIds);
/**
* 联表查询订单明细(支持排序和分页)
* 排序规则deliveryDate DESC -> orderId ASC -> createTime DESC
*
* @param page 分页对象
* @param bo 查询条件
* @return 分页结果
*/
Page<CrmOrderItemVo> selectVoListWithOrder(Page<CrmOrderItemVo> page, @Param("bo") CrmOrderItemBo bo);
}

View File

@@ -69,51 +69,19 @@ public class CrmOrderItemServiceImpl implements ICrmOrderItemService {
/**
* 查询正式订单明细列表
* 实现逻辑:查出全部匹配记录 → Java内存排序交货日期倒序→订单ID升序→创建时间倒序手动分页
* 排序跨页生效,同一合同明细连续排列
* 实现逻辑:SQL联表查询 → 数据库层排序交货日期倒序→订单ID升序→创建时间倒序物理分页
* 排序跨页生效,同一合同明细连续排列,避免内存排序大数据量问题
*/
@Override
public TableDataInfo<CrmOrderItemVo> queryPageList(CrmOrderItemBo bo, PageQuery pageQuery) {
List<Long> orderIdScope = resolveOrderIdScope(bo);
if (orderIdScope != null && orderIdScope.isEmpty()) {
Page<CrmOrderItemVo> emptyPage = new Page<>(ObjectUtil.defaultIfNull(pageQuery.getPageNum(), 1),
ObjectUtil.defaultIfNull(pageQuery.getPageSize(), 10), 0);
emptyPage.setRecords(Collections.emptyList());
return TableDataInfo.build(emptyPage);
}
LambdaQueryWrapper<CrmOrderItem> lqw = buildQueryWrapper(bo);
if (orderIdScope != null) {
lqw.in(CrmOrderItem::getOrderId, orderIdScope);
}
// 1. 查出全部匹配记录
List<CrmOrderItemVo> allItems = baseMapper.selectVoList(lqw);
// 2. 填充订单信息(含 deliveryDate
fillOrderInfoOnItems(allItems);
// 3. 三级排序deliveryDate DESC → orderId ASC → createTime DESC
allItems.sort((a, b) -> {
// 同一合同组内:按创建时间倒序
if (Objects.equals(a.getOrderId(), b.getOrderId())) {
return compareDate(a.getCreateTime(), b.getCreateTime(), true);
}
// 不同合同组:按交货日期倒序
Date dateA = a.getOrderInfo() != null ? a.getOrderInfo().getDeliveryDate() : null;
Date dateB = b.getOrderInfo() != null ? b.getOrderInfo().getDeliveryDate() : null;
int cmp = compareDate(dateA, dateB, true);
if (cmp != 0) return cmp;
// 交货日期相同按订单ID升序保证同一合同组连续
return Long.compare(a.getOrderId(), b.getOrderId());
});
// 4. 手动分页
int total = allItems.size();
int pageNum = ObjectUtil.defaultIfNull(pageQuery.getPageNum(), 1);
int pageSize = ObjectUtil.defaultIfNull(pageQuery.getPageSize(), 10);
int from = (pageNum - 1) * pageSize;
int to = Math.min(from + pageSize, total);
List<CrmOrderItemVo> pageItems = from >= total ? Collections.emptyList() : allItems.subList(from, to);
Page<CrmOrderItemVo> page = new Page<>(pageNum, pageSize, total);
page.setRecords(pageItems);
return TableDataInfo.build(page);
// 使用MyBatis-Plus分页插件SQL层完成联表查询、排序和分页
Page<CrmOrderItemVo> page = new Page<>(
ObjectUtil.defaultIfNull(pageQuery.getPageNum(), 1),
ObjectUtil.defaultIfNull(pageQuery.getPageSize(), 10)
);
// 联表查询在SQL层完成排序避免内存排序
Page<CrmOrderItemVo> resultPage = baseMapper.selectVoListWithOrder(page, bo);
return TableDataInfo.build(resultPage);
}
/**

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.crm.mapper.CrmOrderItemMapper">
<resultMap type="com.klp.crm.domain.CrmOrderItem" id="CrmOrderItemResult">
@@ -36,6 +36,46 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
<!-- 联表查询结果映射 -->
<resultMap type="com.klp.crm.domain.vo.CrmOrderItemVo" id="CrmOrderItemVoResult">
<result property="itemId" column="item_id"/>
<result property="orderId" column="order_id"/>
<result property="productType" column="product_type"/>
<result property="rawMaterialSpec" column="raw_material_spec"/>
<result property="productNum" column="product_num"/>
<result property="specialRequire" column="special_require"/>
<result property="itemAmount" column="item_amount"/>
<result property="remark" column="remark"/>
<result property="finishedProductSpec" column="finished_product_spec"/>
<result property="material" column="material"/>
<result property="grade" column="grade"/>
<result property="weight" column="weight"/>
<result property="widthTolerance" column="width_tolerance"/>
<result property="thicknessTolerance" column="thickness_tolerance"/>
<result property="contractPrice" column="contract_price"/>
<result property="customizer" column="customizer"/>
<result property="shipper" column="shipper"/>
<result property="productionBatch" column="production_batch"/>
<result property="surfaceTreatment" column="surface_treatment"/>
<result property="surfaceQuality" column="surface_quality"/>
<result property="edgeCuttingReq" column="edge_cutting_req"/>
<result property="packagingReq" column="packaging_req"/>
<result property="width" column="width"/>
<result property="thickness" column="thickness"/>
<result property="purpose" column="purpose"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<!-- 合同信息字段 -->
<result property="contractCode" column="contract_code"/>
<result property="supplier" column="supplier"/>
<result property="customer" column="customer"/>
<result property="signTime" column="sign_time"/>
<result property="deliveryDate" column="delivery_date"/>
</resultMap>
<!-- 根据订单ID列表查询订单明细 -->
<select id="selectOrderItemsByOrderIds" resultMap="CrmOrderItemResult">
SELECT
@@ -78,5 +118,132 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY item_id ASC
</select>
<!-- 联表查询订单明细(支持排序和分页) -->
<select id="selectVoListWithOrder" resultMap="CrmOrderItemVoResult">
SELECT
i.item_id,
i.order_id,
i.product_type,
i.raw_material_spec,
i.product_num,
i.special_require,
i.item_amount,
i.remark,
i.finished_product_spec,
i.material,
i.grade,
i.weight,
i.width_tolerance,
i.thickness_tolerance,
i.contract_price,
i.customizer,
i.shipper,
i.production_batch,
i.surface_treatment,
i.surface_quality,
i.edge_cutting_req,
i.packaging_req,
i.width,
i.thickness,
i.purpose,
i.create_by,
i.create_time,
i.update_by,
i.update_time,
i.del_flag,
o.contract_code,
o.supplier,
o.customer,
o.sign_time,
o.delivery_date
FROM crm_order_item i
LEFT JOIN crm_order o ON i.order_id = o.order_id AND o.del_flag = 0
<where>
i.del_flag = 0
<if test="bo.itemId != null">
AND i.item_id = #{bo.itemId}
</if>
<if test="bo.orderId != null">
AND i.order_id = #{bo.orderId}
</if>
<if test="bo.productType != null and bo.productType != ''">
AND i.product_type = #{bo.productType}
</if>
<if test="bo.rawMaterialSpec != null and bo.rawMaterialSpec != ''">
AND i.raw_material_spec = #{bo.rawMaterialSpec}
</if>
<if test="bo.productNum != null">
AND i.product_num = #{bo.productNum}
</if>
<if test="bo.specialRequire != null and bo.specialRequire != ''">
AND i.special_require = #{bo.specialRequire}
</if>
<if test="bo.finishedProductSpec != null and bo.finishedProductSpec != ''">
AND i.finished_product_spec = #{bo.finishedProductSpec}
</if>
<if test="bo.material != null and bo.material != ''">
AND i.material LIKE CONCAT('%', #{bo.material}, '%')
</if>
<if test="bo.grade != null and bo.grade != ''">
AND i.grade = #{bo.grade}
</if>
<if test="bo.weight != null">
AND i.weight = #{bo.weight}
</if>
<if test="bo.contractPrice != null">
AND i.contract_price = #{bo.contractPrice}
</if>
<if test="bo.customizer != null and bo.customizer != ''">
AND i.customizer = #{bo.customizer}
</if>
<if test="bo.shipper != null and bo.shipper != ''">
AND i.shipper = #{bo.shipper}
</if>
<if test="bo.productionBatch != null and bo.productionBatch != ''">
AND i.production_batch = #{bo.productionBatch}
</if>
<if test="bo.surfaceTreatment != null and bo.surfaceTreatment != ''">
AND i.surface_treatment = #{bo.surfaceTreatment}
</if>
<if test="bo.surfaceQuality != null and bo.surfaceQuality != ''">
AND i.surface_quality = #{bo.surfaceQuality}
</if>
<if test="bo.edgeCuttingReq != null and bo.edgeCuttingReq != ''">
AND i.edge_cutting_req = #{bo.edgeCuttingReq}
</if>
<if test="bo.packagingReq != null and bo.packagingReq != ''">
AND i.packaging_req = #{bo.packagingReq}
</if>
<if test="bo.width != null and bo.width != ''">
AND i.width = #{bo.width}
</if>
<if test="bo.thickness != null and bo.thickness != ''">
AND i.thickness = #{bo.thickness}
</if>
<if test="bo.purpose != null and bo.purpose != ''">
AND i.purpose = #{bo.purpose}
</if>
<!-- 合同表筛选条件 -->
<if test="bo.contractCode != null and bo.contractCode != ''">
AND o.contract_code LIKE CONCAT('%', #{bo.contractCode}, '%')
</if>
<if test="bo.customer != null and bo.customer != ''">
AND o.customer LIKE CONCAT('%', #{bo.customer}, '%')
</if>
<if test="bo.signDateStart != null">
AND o.sign_time &gt;= #{bo.signDateStart}
</if>
<if test="bo.signDateEnd != null">
AND o.sign_time &lt; DATE_ADD(#{bo.signDateEnd}, INTERVAL 1 DAY)
</if>
<if test="bo.deliveryDateStart != null">
AND o.delivery_date &gt;= #{bo.deliveryDateStart}
</if>
<if test="bo.deliveryDateEnd != null">
AND o.delivery_date &lt; DATE_ADD(#{bo.deliveryDateEnd}, INTERVAL 1 DAY)
</if>
</where>
ORDER BY o.delivery_date DESC, i.order_id ASC, i.create_time DESC
</select>
</mapper>

View File

@@ -314,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 {
@@ -340,8 +339,6 @@ export default {
deliveryDateStart: undefined,
deliveryDateEnd: undefined
},
// 缓存合同信息
contractMap: {},
// 存储原始数据,用于判断是否有修改
originalData: {},
// 表面处理选项
@@ -372,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
})
// 直接合并数据(后端已按 deliveryDate DESC + createTime DESC 排序)
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 => {
@@ -422,22 +378,11 @@ export default {
})
}).catch(e => {
console.error(e)
this.orderItemList = items
}).finally(() => {
this.loading = false
})
},
// 分组行样式:同一合同组使用交替背景色
groupRowClassName({ row, rowIndex }) {
// 记录上一个orderId切换时翻转颜色
if (rowIndex === 0 || (this._lastOrderId !== undefined && this._lastOrderId !== row.orderId)) {
this._groupColorIndex = (this._groupColorIndex || 0) + 1
}
this._lastOrderId = row.orderId
return this._groupColorIndex % 2 === 1 ? 'group-row-a' : 'group-row-b'
},
// 判断数据是否有变化
hasChanged(row) {
const id = row.itemId || row.detailId

View File

@@ -134,6 +134,7 @@
@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"
@@ -147,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 {
@@ -472,6 +475,10 @@ export default {
this.buttonLoading = false;
})
},
/** 导入钢卷成功回调 */
handleImportSuccess() {
this.handleView({ orderId: this.currentOrderId });
},
// 取消按钮
cancel() {
this.open = false;

View File

@@ -0,0 +1,723 @@
<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 === '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 === '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,
};
},
computed: {
isProcessing() {
return this.step === 'MATCHING' || this.step === 'IMPORTING';
},
dialogTitle() {
switch (this.step) {
case 'PARSED':
return '导入钢卷 - 数据预览 (' + this.parsedData.length + '条)';
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 = [];
try {
// 逐行匹配
for (const row of this.parsedData) {
await this.matchOneRow(row);
}
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] });
} else if (list.length > 1) {
// 场景2匹配到多条需用户选择
this.conflictRows.push({ ...row, candidates: list, selectedCoilId: null });
} else {
// 场景3未匹配
this.unmatchedRows.push({ ...row, reason: '未找到匹配的钢卷' });
}
} catch (error) {
this.unmatchedRows.push({ ...row, reason: '查询失败:' + error.message });
}
},
/** 冲突行选择钢卷 */
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.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;
}
/* 标签页区域 */
.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

@@ -87,4 +87,14 @@ public class WmsReceivableVo {
// 客户编号
private String customerCode;
/**
* 订单编号
*/
private String orderCode;
/**
* 订单名称
*/
private String orderName;
}

View File

@@ -37,9 +37,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
r.update_time,
r.update_by,
c.company_name as customerName,
c.customer_code as customerCode
c.customer_code as customerCode,
o.order_code as orderCode,
o.contract_name as orderName
from wms_receivable r
left join crm_customer c on r.customer_id = c.customer_id and c.del_flag = 0
left join crm_order o on r.order_id = o.order_id and o.del_flag = 0
${ew.customSqlSegment}
</select>