feat(aps): 新增排产单明细合并功能及优化界面展示

- 在排产单明细表格中添加多选合并功能
- 实现排产单明细合并对话框及合并逻辑
- 优化排产单明细表格列配置和表单布局
- 添加合并校验和接收产需单API接口
- 重构订单绑定解绑逻辑提升用户体验
- 添加ScheduleDetailCoilBind组件引入
This commit is contained in:
2026-06-29 16:07:29 +08:00
parent da01bfaa48
commit ee376f922f
19 changed files with 1547 additions and 154 deletions

View File

@@ -0,0 +1,334 @@
<template>
<span>
<el-button icon="el-icon-link" size="mini" @click="open">绑定排产明细</el-button>
<el-dialog title="排产明细 - 钢卷绑定" :visible.sync="visible" width="1200px" append-to-body :close-on-click-modal="false"
v-loading="loading">
<!-- 上半部分今日排产明细 -->
<div class="bind-section">
<div class="bind-section-header">今日排产明细{{ scheduleItems.length }} </div>
<el-table :data="scheduleItems" highlight-current-row @row-click="handleDetailClick" max-height="300"
size="small">
<el-table-column prop="scheduleNo" label="排产单号" min-width="130" show-overflow-tooltip />
<el-table-column prop="prodDate" label="生产日期" width="90" />
<el-table-column prop="spec" label="规格" width="90" show-overflow-tooltip />
<el-table-column prop="material" label="材质" width="70" />
<el-table-column prop="scheduleWeight" label="排产吨数" width="80" />
<el-table-column prop="productType" label="品名" min-width="100" show-overflow-tooltip />
<el-table-column prop="customerName" label="订货单位" min-width="100" show-overflow-tooltip />
<el-table-column label="已绑钢卷" width="80">
<template slot-scope="scope">
<el-tag size="mini" type="info">{{ scope.row._boundCount || 0 }} </el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 下半部分已绑定钢卷 -->
<div class="bind-section" v-if="currentDetail" style="margin-top:16px;">
<div class="bind-section-header">
已绑定钢卷 {{ currentDetail.spec }} / {{ currentDetail.material }}
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openCoilSelect">绑定钢卷</el-button>
</div>
<el-table :data="boundCoils" size="small" v-loading="boundCoilsLoading">
<el-table-column prop="currentCoilNo" label="钢卷号" width="140" show-overflow-tooltip />
<el-table-column prop="specification" label="规格" width="100" show-overflow-tooltip />
<el-table-column prop="material" label="材质" width="70" />
<el-table-column prop="netWeight" label="净重" width="80" />
<el-table-column prop="_remark" label="备注" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="70" align="center">
<template slot-scope="scope">
<el-button type="text" style="color:#ff4d4f;" size="mini" @click="handleUnbind(scope.row)">解绑</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="boundCoils.length === 0 && !boundCoilsLoading" description="暂未绑定钢卷" />
</div>
<el-empty v-if="scheduleItems.length === 0 && !loading" description="今日无排产明细" />
</el-dialog>
<!-- 选卷子弹窗 -->
<el-dialog title="选择钢卷" :visible.sync="coilDialogVisible" width="900px" append-to-body
:close-on-click-modal="false">
<div style="display:flex;gap:8px;margin-bottom:10px;">
<el-input v-model="coilQuery.keyword" placeholder="钢卷号 / 入场卷号" size="small" style="width:220px"
clearable @keyup.enter.native="searchCoils" />
<el-button size="small" type="primary" icon="el-icon-search" @click="searchCoils">搜索</el-button>
<span style="margin-left:auto;font-size:12px;color:#909399;line-height:32px;">
已选 {{ selectedCoils.length }}
</span>
</div>
<el-table :data="coilList" @selection-change="handleCoilSelection" ref="coilSelectTable" size="small"
v-loading="coilListLoading" max-height="400">
<el-table-column type="selection" width="50" :selectable="checkCoilSelectable" />
<el-table-column prop="currentCoilNo" label="钢卷号" width="140" show-overflow-tooltip />
<el-table-column prop="enterCoilNo" label="入场卷号" width="140" show-overflow-tooltip />
<el-table-column prop="specification" label="规格" width="100" show-overflow-tooltip />
<el-table-column prop="material" label="材质"/>
<el-table-column prop="netWeight" label="净重" width="80" />
<el-table-column prop="warehouseName" label="仓库" width="80" show-overflow-tooltip />
</el-table>
<pagination v-show="coilTotal > 0" :total="coilTotal" :page.sync="coilQuery.pageNum"
:limit.sync="coilQuery.pageSize" @pagination="searchCoils" style="padding:10px 0 0 0;" />
<span slot="footer">
<el-button type="primary" @click="confirmBindCoils" :loading="bindLoading"
:disabled="selectedCoils.length === 0">确定绑定{{ selectedCoils.length }} </el-button>
<el-button @click="coilDialogVisible = false">取消</el-button>
</span>
</el-dialog>
</span>
</template>
<script>
import { listScheduleItem } from '@/api/aps/schedule'
import { listMaterialCoil } from '@/api/wms/coil'
import { listDetailCoilRel, batchAddDetailCoilRel, delDetailCoilRel } from '@/api/aps/detailCoilRel'
export default {
name: 'ScheduleDetailCoilBind',
data() {
return {
visible: false,
loading: false,
scheduleItems: [],
currentDetail: null,
boundCoils: [],
boundCoilsLoading: false,
allBoundCoilIds: new Set(),
coilDialogVisible: false,
coilListLoading: false,
coilList: [],
coilTotal: 0,
selectedCoils: [],
bindLoading: false,
coilQuery: {
keyword: '',
pageNum: 1,
pageSize: 20
}
}
},
computed: {
prodDate() {
return new Date().toISOString().slice(0, 10)
}
},
methods: {
open() {
this.visible = true
this.currentDetail = null
this.boundCoils = []
this.loadScheduleItems()
},
async loadScheduleItems() {
this.loading = true
try {
const res = await listScheduleItem({ prodDate: this.prodDate })
this.scheduleItems = (res.rows || []).map(item => {
item._boundCount = 0
return item
})
// 加载所有已绑定的钢卷ID
await this.loadAllBoundCoilIds()
} catch (e) {
this.scheduleItems = []
} finally {
this.loading = false
}
},
async loadAllBoundCoilIds() {
const ids = this.scheduleItems.map(i => i.id || i.scheduleDetailId).filter(Boolean)
if (ids.length === 0) return
try {
// 尝试批量查询
const res = await listDetailCoilRel({ scheduleDetailIds: ids.join(',') })
const rows = res.rows || []
this.allBoundCoilIds = new Set(rows.map(r => r.coilId))
this.applyBoundCounts(rows)
} catch (e) {
// 批量查询失败则逐个查询
try {
const allRows = []
const tasks = ids.map(id =>
listDetailCoilRel({ scheduleDetailId: id }).then(r => {
(r.rows || []).forEach(row => allRows.push(row))
}).catch(() => {})
)
await Promise.all(tasks)
this.allBoundCoilIds = new Set(allRows.map(r => r.coilId))
this.applyBoundCounts(allRows)
} catch (e2) {
// ignore
}
}
},
applyBoundCounts(rows) {
const countMap = {}
rows.forEach(r => {
const key = r.scheduleDetailId
countMap[key] = (countMap[key] || 0) + 1
})
this.scheduleItems.forEach(item => {
const key = item.id || item.scheduleDetailId
item._boundCount = countMap[key] || 0
})
},
async handleDetailClick(row) {
this.currentDetail = row
this.boundCoilsLoading = true
try {
const detailId = row.id || row.scheduleDetailId
const res = await listDetailCoilRel({ scheduleDetailId: detailId })
const rels = res.rows || []
if (rels.length > 0) {
const coilIds = rels.map(r => r.coilId).filter(Boolean)
const coilRes = await listMaterialCoil({
pageNum: 1,
pageSize: 200,
coilIds: coilIds.join(',')
})
const coilMap = {}
if (coilRes.rows) {
coilRes.rows.forEach(c => { coilMap[c.coilId || c.id] = c })
}
this.boundCoils = rels.map(r => ({
...r,
...(coilMap[r.coilId] || {}),
_remark: r.remark || ''
}))
} else {
this.boundCoils = []
}
} catch (e) {
this.boundCoils = []
} finally {
this.boundCoilsLoading = false
}
},
async openCoilSelect() {
this.coilDialogVisible = true
this.selectedCoils = []
this.coilQuery.keyword = ''
this.coilQuery.pageNum = 1
await this.searchCoils()
},
async searchCoils() {
this.coilListLoading = true
try {
const params = {
pageNum: this.coilQuery.pageNum,
pageSize: this.coilQuery.pageSize,
status: 0,
dataType: 1,
exclusiveStatus: 0
}
if (this.coilQuery.keyword) {
params.currentCoilNo = this.coilQuery.keyword
}
const res = await listMaterialCoil(params)
// 过滤掉已绑定的钢卷
this.coilList = (res.rows || []).filter(c => !this.allBoundCoilIds.has(c.coilId || c.id))
this.coilTotal = res.total || 0
} catch (e) {
this.coilList = []
} finally {
this.coilListLoading = false
}
},
checkCoilSelectable(row) {
return !this.allBoundCoilIds.has(row.coilId || row.id)
},
handleCoilSelection(selection) {
this.selectedCoils = selection
},
async confirmBindCoils() {
if (this.selectedCoils.length === 0) {
this.$message.warning('请至少选择一个钢卷')
return
}
this.bindLoading = true
try {
const detailId = this.currentDetail.id || this.currentDetail.scheduleDetailId
const data = this.selectedCoils.map(c => ({
scheduleDetailId: detailId,
coilId: c.coilId || c.id,
remark: ''
}))
await batchAddDetailCoilRel(data)
this.$modal.msgSuccess(`成功绑定 ${this.selectedCoils.length}`)
this.coilDialogVisible = false
// 记录新绑定的钢卷ID
this.selectedCoils.forEach(c => {
this.allBoundCoilIds.add(c.coilId || c.id)
})
// 更新当前明细的已绑卷数
if (this.currentDetail) {
this.currentDetail._boundCount = (this.currentDetail._boundCount || 0) + this.selectedCoils.length
}
// 刷新已绑定钢卷列表
await this.handleDetailClick(this.currentDetail)
} catch (e) {
this.$modal.msgError('绑定失败')
} finally {
this.bindLoading = false
}
},
async handleUnbind(row) {
try {
await this.$confirm('确认解绑该钢卷吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const relId = row.relId || row.id
await delDetailCoilRel(relId)
this.$modal.msgSuccess('解绑成功')
// 从已绑定集合中移除
if (row.coilId) {
this.allBoundCoilIds.delete(row.coilId)
}
if (this.currentDetail) {
this.currentDetail._boundCount = Math.max(0, (this.currentDetail._boundCount || 0) - 1)
}
await this.handleDetailClick(this.currentDetail)
} catch (e) {
if (e !== 'cancel') {
this.$modal.msgError('解绑失败')
}
}
}
}
}
</script>
<style scoped lang="scss">
.bind-section {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.bind-section-header {
background: #f5f7fa;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e4e7ed;
}
</style>

View File

@@ -26,6 +26,7 @@
<div class="flow-left">
<div class="flow-section-title">
<span>源卷列表</span>
<schedule-detail-coil-bind />
<el-button v-if="!readonly" type="primary" size="small" @click="addSourceCoil">添加源卷</el-button>
</div>
@@ -374,6 +375,7 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm';
import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect";
@@ -389,6 +391,7 @@ export default {
AbnormalForm,
ContractSelect,
PlanSheetViewer,
ScheduleDetailCoilBind,
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {

View File

@@ -11,6 +11,7 @@
:name="tab.value"></el-tab-pane>
</el-tabs>
<h3 class="section-title" v-else>待领物料列表</h3>
<schedule-detail-coil-bind />
<el-button size="mini" icon="el-icon-refresh" @click="getMaterialCoil">刷新</el-button>
</div>
@@ -309,6 +310,7 @@ import CoilCard from '@/components/KLPService/Renderer/CoilCard.vue'
import LabelRender from './LabelRender/index.vue'
import StepSplit from './stepSplit.vue'
import ExceptionManager from '../components/ExceptionManager'
import ScheduleDetailCoilBind from '../components/ScheduleDetailCoilBind'
import { getCoilTagPrintType } from '@/views/wms/coil/js/coilPrint'
export default {
@@ -334,7 +336,8 @@ export default {
CoilCard,
LabelRender,
StepSplit,
ExceptionManager
ExceptionManager,
ScheduleDetailCoilBind
},
data() {
return {

View File

@@ -25,6 +25,7 @@
<div class="flow-left">
<div class="flow-section-title">
<span>母卷信息</span>
<schedule-detail-coil-bind />
</div>
<div class="coil-card mother-coil">
<div class="coil-header">
@@ -377,6 +378,7 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm';
import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect";
@@ -391,6 +393,7 @@ export default {
AbnormalForm,
ContractSelect,
PlanSheetViewer,
ScheduleDetailCoilBind,
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {

View File

@@ -30,9 +30,10 @@
<!-- 右侧更新表单 -->
<div>
<el-card class="form-card">
<div slot="header" class="card-header">
<div slot="header" class="card-header">
<span><i class="el-icon-edit-outline"></i> {{ '更新信息' }}</span>
<div>
<schedule-detail-coil-bind />
<el-button size="small" @click="saveTemp" :loading="loading">暂存内容</el-button>
<el-button type="primary" size="small" @click="handleSave" :loading="loading">保存更新</el-button>
</div>
@@ -405,6 +406,7 @@ import ContractSelect from "@/components/KLPService/ContractSelect";
import L2MatchPanel from './panels/L2MatchPanel.vue'
import DrMatchPanel from './panels/DrMatchPanel.vue';
import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
// actionType -> 产线名称映射
@@ -431,6 +433,7 @@ export default {
L2MatchPanel,
DrMatchPanel,
PlanSheetViewer,
ScheduleDetailCoilBind,
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {