@@ -1,8 +1,10 @@
< template >
< div class = "aps-req-page" >
< el-row :gutter = "20" class = "aps-req-row" >
< drag-resize-panel direction = "horizontal" :initial-size = "360" :min-size = "26 0" class = "aps-req-row" >
< template # panelA >
<!-- 左侧 : 产需单列表 -- >
< el-col :span = "8" class = "aps-req-left" v-loading = "reqLoading" >
< div class = "aps-req-left" v-loading = "reqLoading" >
< div class = "aps-req-scroll" >
<!-- 筛选区 -- >
< div class = "filter-section" style = "padding: 10px; border-bottom: 1px solid #e4e7ed;" >
< div style = "display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;" >
@@ -45,7 +47,7 @@
< / div >
< / div >
< div class = "list-item-right" >
< el-button size = "mini" plain @click.stop="handleJumpToSchedule (item)" > 跳转排 产< / el -button >
< el-button v-if = "item.scheduleStatus === 1 || item.scheduleStatus === 2" size=" mini" plain @click.stop="openConvertDialogForReq (item)" > 产需转化 < / el -button >
< / div >
< / div >
< / div >
@@ -57,10 +59,13 @@
< pagination v-show = "total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync = "queryParams.pageSize" @pagination ="getList"
style = "padding: 10px; margin-bottom: 10px !important;" / >
< / el-col >
< / div >
< / div >
< / template >
< template # panelB >
<!-- 右侧 : 详情区 -- >
< el-col :span = "16" class = "aps-req-right" >
< div class = "aps-req-right" >
< div class = "aps-req-scroll" >
< el-empty v-if = "!currentReq" description="选择产需单查看更多信息" / >
< div v-else class = "detail-panel" >
@@ -74,6 +79,11 @@
< button v-if = "currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn"
style = "background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);"
@click ="handleDispatch" > {{ currentReq.scheduleStatus = = = 3 ? ' 重新下发 ' : ' 下发 ' }} < / button >
< button v-if = "currentReq.scheduleStatus === 1" class="header-btn"
style = "background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);"
@click ="handleRecall" > 撤回 < / button >
< button v-if = "currentReq.scheduleStatus === 1 || currentReq.scheduleStatus === 2" class="header-btn"
@click ="openConvertDialogForReq(currentReq)" > 产需转化 < / button >
< / div >
< / div >
< div class = "detail-card-body" >
@@ -194,13 +204,30 @@
< div class = "detail-card" >
< div class = "detail-card-header" >
< span > 排产明细 ( { { requirementDetailList . length } } 条 , 合计 { { detailTotalWeight } } T ) < / span >
< button v-if = "canEdit" class="header-btn" @click="handleDetailAdd" > + 新增 < / button >
< span style = "display:flex;align-items:center;gap:8px;" >
< span v-if = "detailDispatchProgress.total > 0" style="font-size:12px;font-weight:400;opacity:0.9;" >
已下发 {{ detailDispatchProgress.issued }} / {{ detailDispatchProgress.total }} ( {{ detailDispatchProgress.percent }} % )
< / span >
< button v-if = "canEdit" class="header-btn" @click="handleDetailAdd" > + 新增 < / button >
< / span >
< / div >
< div class = "detail-card-body" style = "padding:0;" >
< el-table :data = "requirementDetailList" border size = "small" v-loading = "detailLoading" class="aps-table" >
< el -table -column label = "规格" prop = "spec" min -width = " 120 " / >
< el-table-column label = "材质" prop = "material" width = "90" align = "center" / >
< el-table-column label = "排产吨数" prop = "scheduleWeight" width = "100" align = "right" / >
< el-table-column label = "工序步骤" min -width = " 130 " >
< template slot -scope = " scope " >
< span style = "font-size:12px;" > { { getStepNamesByProcessId ( scope . row . processId ) || '-' } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "排产日期" prop = "scheduleDate" width = "110" align = "center" / >
< el-table-column label = "下发状态" width = "90" align = "center" >
< template slot -scope = " scope " >
< span v-if = "scope.row.isIssued === 1 || scope.row.isIssued === '1'" style="color:#52c41a;font-size:12px;" > 已下发 < / span >
< span v-else style = "color:#c0c4cc;font-size:12px;" > 未下发 < / span >
< / template >
< / el-table-column >
< el-table-column label = "备注" prop = "remark" min -width = " 120 " / >
< el-table-column label = "操作" width = "130" align = "center" fixed = "right" >
< template slot -scope = " scope " >
@@ -217,8 +244,10 @@
< / div >
< / div >
< / div >
< / el-col >
< / el-row >
< / div >
< / div >
< / template >
< / drag-resize-panel >
<!-- 新增 / 编辑对话框 -- >
< el-dialog :title = "dialogTitle" :visible.sync = "dialogVisible" width = "900px" append -to -body
@@ -350,7 +379,7 @@
<!-- 左侧 : 订单列表 -- >
< div style = "width: 45%; display: flex; flex-direction: column;" >
< div style = "display: flex; gap: 6px; margin-bottom: 10px;" >
< el-input v-model = "bindQuery.keyword" placeholder="搜索订单编号/客户名" size="small" clearable style="flex:1;"
< el-input v-model = "bindQuery.keyword" placeholder="搜索订单编号/客户名/业务员 " size="small" clearable style="flex:1;"
@change ="searchBindOrders" / >
< el-button size = "small" class = "aps-btn-red" icon = "el-icon-search" @click ="searchBindOrders" > 搜索 < / el -button >
< / div >
@@ -449,6 +478,68 @@
< el-button @click ="detailDialogVisible = false" > 取 消 < / el -button >
< / div >
< / el-dialog >
<!-- 产需转化对话框 -- >
< el-dialog title = "产需转化" :visible.sync = "convertDialogVisible" width = "1100px" append -to -body
:close-on-click-modal = "false" >
< div style = "margin-bottom:12px; font-size:13px; color:#2c3e50;" >
产需单号 : < strong > { { convertReqNo } } < / strong >
< span style = "margin-left:16px;" > 共 { { convertDetailList . length } } 条明细 < / span >
< / div >
< el-table :data = "convertDetailList" border size = "small" class = "aps-table"
@ selection -change = " ( val ) = > convertSelection = val ">
<el-table-column type=" selection " width=" 40 " />
<el-table-column label=" 规格 " prop=" spec " min-width=" 100 " />
<el-table-column label=" 材质 " prop=" material " width=" 80 " align=" center " />
<el-table-column label=" 排产吨数 " prop=" scheduleWeight " width=" 90 " align=" right " />
<el-table-column label=" 工序ID " width=" 150 " align=" center ">
<template slot-scope=" scope ">
<el-select v-model=" scope . row . processId " placeholder=" 选择工序 " size=" small " clearable filterable
style=" width : 130 px ; " @change=" ( val ) => onConvertProcessChange ( scope . row , val ) ">
<el-option v-for=" p in processList " :key=" p . processId " :label=" p . processName " :value=" p . processId ">
<span>{{ p.processName }}</span>
<span v-if=" p . _steps " style=" display : block ; font - size : 11 px ; color : # 909399 ; ">{{ p._steps }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label=" 工序步骤 " min-width=" 140 ">
<template slot-scope=" scope ">
<span v-if=" scope . row . _stepNames " style=" font - size : 12 px ; color : # 2 c3e50 ; ">{{ scope.row._stepNames }}</span>
<span v-else style=" font - size : 12 px ; color : # c0c4cc ; ">-</span>
</template>
</el-table-column>
<el-table-column label=" 排产日期 " width=" 150 " align=" center ">
<template slot-scope=" scope ">
<el-date-picker v-model=" scope . row . scheduleDate " type=" date " value-format=" yyyy - MM - dd " placeholder=" 排产日期 "
size=" small " clearable style=" width : 130 px ; " />
</template>
</el-table-column>
<el-table-column label=" 下发状态 " width=" 80 " align=" center ">
<template slot-scope=" scope ">
<span v-if=" scope . row . isIssued === 1 || scope . row . isIssued === '1' " style=" color : # 52 c41a ; font - size : 12 px ; ">已下发</span>
<span v-else style=" color : # c0c4cc ; font - size : 12 px ; ">未下发</span>
</template>
</el-table-column>
<el-table-column label=" 备注 " prop=" remark " min-width=" 100 " />
<el-table-column label=" 操作 " width=" 70 " align=" center " fixed=" right ">
<template slot-scope=" scope ">
<el-button type=" text " size=" small " style=" color : # ff4d4f ; "
:disabled=" ! scope . row . processId || ! scope . row . scheduleDate "
@click=" handleConvertSingle ( scope . row ) ">下发</el-button>
</template>
</el-table-column>
</el-table>
<div v-if=" convertProgressShow " style=" margin - top : 12 px ; ">
<el-progress :percentage=" convertProgress " :status=" convertProgress === 100 ? 'success' : '' " />
</div>
<div slot=" footer " class=" dialog - footer " style=" margin - top : 12 px ; ">
<el-button :loading=" convertBtnLoading " type=" danger " :disabled=" convertSelection . length === 0 "
@click=" confirmConvertSelected ">批量下发({{ convertSelection.length }}) </el-button>
<el-button :loading=" convertBtnLoading " type=" danger " :disabled=" ! convertAllValid " @click=" confirmConvertAll ">全部下发</el-button>
<el-button @click=" convertDialogVisible = false ">关 闭</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -464,13 +555,17 @@ import {
listRel,
addRequirementDetail,
updateRequirementDetail,
delRequirementDetail
delRequirementDetail,
convertRequirementDetail
} from '@/api/aps/requirement'
import { listCrmOrder } from '@/api/aps/order'
import { listProcess } from '@/api/aps/process'
import { parseProductContent } from '@/utils/productContent'
import DragResizePanel from '@/components/DragResizePanel'
export default {
name: 'ApsRequirement',
components: { DragResizePanel },
data() {
return {
// 左侧列表
@@ -520,6 +615,16 @@ export default {
scheduleWeight: [{ required: true, message: '排产吨数不能为空', trigger: 'change' }]
},
// 产需转化对话框
convertDialogVisible: false,
convertDetailList: [],
convertBtnLoading: false,
convertReqNo: '',
convertProgress: 0,
convertProgressShow: false,
convertSelection: [],
processList: [],
// 新增/编辑对话框
dialogVisible: false,
dialogTitle: '新增产需单',
@@ -541,6 +646,16 @@ export default {
const n = Number(item.scheduleWeight)
return isNaN(n) ? sum : sum + n
}, 0)
},
convertAllValid() {
return this.convertDetailList.length > 0 &&
this.convertDetailList.every(d => d.processId && d.scheduleDate)
},
detailDispatchProgress() {
const total = this.requirementDetailList.length
if (total === 0) return { issued: 0, total: 0, percent: 0 }
const issued = this.requirementDetailList.filter(d => d.isIssued === 1 || d.isIssued === '1').length
return { issued, total, percent: Math.round((issued / total) * 100) }
}
},
created() {
@@ -582,6 +697,9 @@ export default {
spec: '',
material: '',
scheduleWeight: 0,
processId: '',
scheduleDate: '',
isIssued: 0,
productType: '',
remark: ''
}
@@ -592,18 +710,6 @@ export default {
this.getList()
},
// 跳转到排产单页面(路由跳转)
handleJumpToSchedule ( item ) {
this . $router . push ( {
path : '/aps/schedule' ,
query : {
prodDate : item . prodDate || '' ,
scheduleNo : item . scheduleNo || '' ,
scheduleStatus : item . scheduleStatus
}
} )
} ,
statusBadgeType(status) {
return this.statusBadgeMap[status] || 'gray'
},
@@ -624,11 +730,22 @@ export default {
handleReqClick(item) {
this.currentReq = item
this.detailLoading = true
getRequirement ( item . scheduleId ) . then ( res => {
Promise.all([
getRequirement(item.scheduleId),
listProcess({ pageNum: 1, pageSize: 1000 })
]).then(([res, processRes]) => {
if (res.data) {
this.currentReq = res.data
this.requirementDetailList = res.data.detailList || []
}
// 构建工序查找表
this.processList = (processRes.rows || []).map(p => ({
...p,
_steps: (p.stepList && p.stepList.length > 0)
? p.stepList.slice().sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
.map(s => s.stepName).join(' → ')
: ''
}))
}).catch(() => {
this.requirementDetailList = []
}).finally(() => {
@@ -963,6 +1080,286 @@ export default {
}).catch(() => { })
},
handleRecall() {
this.$confirm('确认撤回该产需单吗?撤回后将恢复为草稿状态。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
updateRequirement({
scheduleId: this.currentReq.scheduleId,
scheduleStatus: 0
}).then(() => {
this.$modal.msgSuccess('撤回成功')
this.getList()
this.handleReqClick(this.currentReq)
}).catch(() => {
this.$modal.msgError('撤回失败')
})
}).catch(() => { })
},
// ====== 产需转化 ======
openConvertDialogForReq(item) {
// 确保加载了该产需单的详情
this.convertReqNo = item.scheduleNo || ''
this.convertProgressShow = false
this.convertProgress = 0
const loadDetail = item.scheduleId === (this.currentReq && this.currentReq.scheduleId)
? Promise.resolve(this.currentReq)
: getRequirement(item.scheduleId)
// 同时加载工序列表和明细
Promise.all([
loadDetail,
listProcess({ pageNum: 1, pageSize: 1000 })
]).then(([res, processRes]) => {
const data = res.data || res
const detailList = (data.detailList || []).map(d => ({ ...d }))
// 构建工序查找表
const processRows = processRes.rows || []
const processMap = new Map()
processRows.forEach(p => {
processMap.set(p.processId, p)
})
this.processList = processRows.map(p => ({
...p,
_steps: (p.stepList && p.stepList.length > 0)
? p.stepList.slice().sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
.map(s => s.stepName).join(' → ')
: ''
}))
// 为已有 processId 的明细填充 _stepNames, 默认 scheduleDate 为今天
const today = this.getTodayStr()
detailList.forEach(d => {
if (!d.scheduleDate) {
d.scheduleDate = today
}
if (d.processId) {
this.fillProcessStepInfo(d)
}
})
this.convertDetailList = detailList
this.convertDialogVisible = true
}).catch(() => {
this.convertDetailList = []
this.convertDialogVisible = true
})
},
fillProcessStepInfo(row) {
const pid = row.processId ? String(row.processId) : ''
if (!pid) return
const process = this.processList.find(p => String(p.processId) === pid)
if (process) {
const steps = (process.stepList || []).slice().sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
row._stepNames = steps.map(s => s.stepName).join(' → ')
row._stepList = steps
} else {
row._stepNames = ''
row._stepList = []
}
},
onConvertProcessChange(row, val) {
if (!val) {
row._stepNames = ''
row._stepList = []
return
}
this.fillProcessStepInfo(row)
},
getStepNamesByProcessId(processId) {
if (!processId) return ''
const pid = String(processId)
const process = this.processList.find(p => String(p.processId) === pid)
if (!process || !process.stepList || process.stepList.length === 0) return ''
return process.stepList.slice().sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
.map(s => s.stepName).join(' → ')
},
handleConvertSingle(row) {
if (!row.processId) {
this.$message.warning('请先选择工序')
return
}
if (!row.scheduleDate) {
this.$message.warning('请先填写排产日期')
return
}
this.$confirm(`确认下发明细「${row.spec} / ${row.material}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.convertBtnLoading = true
// 先保存 processId、scheduleDate 并标记已下发
updateRequirementDetail({
scheduleDetailId: row.scheduleDetailId,
processId: row.processId,
scheduleDate: row.scheduleDate,
isIssued: 1
}).then(() => {
// 构建下发 BO, 逐条下发
return convertRequirementDetail({
detailList: [{
scheduleDetailId: row.scheduleDetailId,
processId: row.processId || null,
stepList: (row._stepList || []).map(s => ({
stepId: s.stepId,
stepOrder: s.stepOrder,
stepName: s.stepName
}))
}]
})
}).then(() => {
this.$modal.msgSuccess('下发成功')
row.isIssued = 1
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('下发失败')
}).finally(() => {
this.convertBtnLoading = false
})
}).catch(() => { })
},
confirmConvertAll() {
if (this.convertDetailList.length === 0) {
this.$message.warning('没有可下发的明细')
return
}
const invalidItems = this.convertDetailList.filter(d => !d.processId || !d.scheduleDate)
if (invalidItems.length > 0) {
this.$message.warning(`还有 ${invalidItems.length} 条明细未填写工序或排产日期,请完善后再下发`)
return
}
this.$confirm(`确认下发全部 ${this.convertDetailList.length} 条明细吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.convertBtnLoading = true
this.convertProgressShow = true
this.convertProgress = 0
const total = this.convertDetailList.length
let completed = 0
let failed = 0
const processNext = (index) => {
if (index >= total) {
this.convertBtnLoading = false
this.refreshDetailList()
if (failed === 0) {
this.$modal.msgSuccess(`成功下发 ${completed} 条明细`)
} else if (completed === 0) {
this.$modal.msgError('下发失败,请重试')
} else {
this.$modal.msgWarning(`成功下发 ${completed} 条,${failed} 条失败`)
}
return
}
const row = this.convertDetailList[index]
updateRequirementDetail({
scheduleDetailId: row.scheduleDetailId,
processId: row.processId,
scheduleDate: row.scheduleDate,
isIssued: 1
}).then(() => {
return convertRequirementDetail({
detailList: [{
scheduleDetailId: row.scheduleDetailId,
processId: row.processId,
stepList: (row._stepList || []).map(s => ({
stepId: s.stepId,
stepOrder: s.stepOrder,
stepName: s.stepName
}))
}]
})
}).then(() => {
completed++
row.isIssued = 1
}).catch(() => {
failed++
}).finally(() => {
this.convertProgress = Math.round(((completed + failed) / total) * 100)
processNext(index + 1)
})
}
processNext(0)
}).catch(() => { })
},
confirmConvertSelected() {
if (this.convertSelection.length === 0) {
this.$message.warning('请勾选要下发的明细')
return
}
const invalidItems = this.convertSelection.filter(d => !d.processId || !d.scheduleDate)
if (invalidItems.length > 0) {
this.$message.warning(`所选明细中有 ${invalidItems.length} 条未填写工序或排产日期,请完善后再下发`)
return
}
this.$confirm(`确认下发所选 ${this.convertSelection.length} 条明细吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.convertBtnLoading = true
this.convertProgressShow = true
this.convertProgress = 0
const list = [...this.convertSelection]
const total = list.length
let completed = 0
let failed = 0
const processNext = (index) => {
if (index >= total) {
this.convertBtnLoading = false
this.refreshDetailList()
if (failed === 0) {
this.$modal.msgSuccess(`成功下发 ${completed} 条明细`)
} else if (completed === 0) {
this.$modal.msgError('下发失败,请重试')
} else {
this.$modal.msgWarning(`成功下发 ${completed} 条,${failed} 条失败`)
}
return
}
const row = list[index]
updateRequirementDetail({
scheduleDetailId: row.scheduleDetailId,
processId: row.processId,
scheduleDate: row.scheduleDate,
isIssued: 1
}).then(() => {
return convertRequirementDetail({
detailList: [{
scheduleDetailId: row.scheduleDetailId,
processId: row.processId,
stepList: (row._stepList || []).map(s => ({
stepId: s.stepId,
stepOrder: s.stepOrder,
stepName: s.stepName
}))
}]
})
}).then(() => {
completed++
row.isIssued = 1
}).catch(() => {
failed++
}).finally(() => {
this.convertProgress = Math.round(((completed + failed) / total) * 100)
processNext(index + 1)
})
}
processNext(0)
}).catch(() => { })
},
// ====== 排产明细新增/编辑/删除 ======
resetDetailForm() {
this.detailForm = this.getEmptyDetailForm()
@@ -1058,8 +1455,7 @@ export default {
@ import './scss/aps-theme.scss' ;
. aps - req - page {
height : 100 % ;
padding : 8 px ;
height : calc ( 100 vh - 84 px ) ;
box - sizing : border - box ;
background : $aps - silver - 1 ;
}
@@ -1070,17 +1466,19 @@ export default {
// ====== 左侧(参照 HTML 产需单列表) ======
. aps - req - left {
display : flex ;
flex - direction : column ;
height : 100 % ;
background : # fff ;
border : 1 px solid $aps - silver - 3 ;
border - radius : 6 px ;
box - sizing : border - box ;
overflow : hidden ;
min - height : 0 ;
}
. aps - req - scroll {
height : 100 % ;
overflow - y : auto ;
}
. panel - header {
background : $aps - silver - mid ;
padding : 8 px 12 px ;
@@ -1088,15 +1486,12 @@ export default {
font - weight : 600 ;
color : $aps - text ;
border - bottom : 1 px solid $aps - border ;
flex - shrink : 0 ;
display : flex ;
align - items : center ;
justify - content : space - between ;
}
. list - container {
flex : 1 ;
overflow - y : auto ;
min - height : 0 ;
}
@@ -1198,21 +1593,15 @@ export default {
border : 1 px solid $aps - silver - 3 ;
border - radius : 6 px ;
box - sizing : border - box ;
display : flex ;
flex - direction : column ;
overflow : hidden ;
min - height : 0 ;
}
. detail - panel {
flex : 1 ;
overflow - y : auto ;
padding : 16 px ;
display : flex ;
flex - direction : column ;
gap : 16 px ;
background : $aps - bg ;
min - height : 0 ;
}
// ====== 右侧详情卡片 ======