Files
klp-oa/klp-ui/src/views/wms/post/aps/schedule.vue
砂糖 b94b7823e5 refactor(盘库流程): 重构盘库流程页面与组件,完善排产明细功能
1.  重构盘库流程的步骤与状态映射,调整流程节点顺序与名称
2.  拆分通用盘库详情组件PlanDetailPanel,复用各流程页面
3.  新增计划审批、盘库执行页面,完善差异审批页面
4.  为排产单明细添加增删改查API与前端操作功能
5.  为排产日期添加格式化注解,完善参数接收格式
2026-06-27 11:15:13 +08:00

641 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="aps-sch-page">
<!-- 顶部工具栏 -->
<div class="aps-sch-toolbar">
<span class="aps-sch-label">生产日期</span>
<el-date-picker
v-model="queryDate"
type="date"
placeholder="选择生产日期"
value-format="yyyy-MM-dd"
size="small"
style="width:160px"
@change="handleDateChange"
/>
<el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-tabs v-model="activeTab" size="small" style="margin:0 0 0 16px;" @tab-click="handleQuery">
<el-tab-pane label="待审核" name="pending" />
<el-tab-pane label="已排产" name="scheduled" />
</el-tabs>
<div class="aps-sch-summary" v-if="summaryText">
<span>{{ summaryText }}</span>
</div>
</div>
<!-- 待审核 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'pending'">
<div class="detail-card-header">
<span>待审核产需单明细</span>
<span v-if="pendingScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ pendingScheduleList.length }} 个产需单
</span>
</div>
<div class="detail-card-body" style="padding:0;" v-loading="loading">
<el-table
v-if="pendingScheduleList.length > 0"
:data="pendingScheduleList"
border
size="small"
class="aps-table"
@row-click="handleScheduleClick"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
row-key="scheduleId"
>
<el-table-column type="expand" width="40">
<template slot-scope="{ row }">
<el-table :data="expandDetailMap[row.scheduleId] || []" border size="mini" style="margin:6px 0;" @row-click.stop="handleRowClick">
<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="品名" prop="productType" min-width="100" />
<el-table-column label="备注" prop="remark" min-width="100" />
</el-table>
</template>
</el-table-column>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" />
<el-table-column label="订货单位" prop="customerName" min-width="140" />
<el-table-column label="业务员" prop="businessUser" width="80" align="center" />
<el-table-column label="明细数量" width="80" align="center">
<template slot-scope="{ row }">
{{ (row.detailList || []).length }}
</template>
</el-table-column>
<el-table-column label="排产总吨数" width="100" align="right">
<template slot-scope="{ row }">
{{ scheduleTotalWeight(row) }}
</template>
</el-table-column>
<el-table-column label="交货期(天)" prop="deliveryCycle" width="90" align="center" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button type="text" size="small" style="color:#52c41a;" @click.stop="handleAccept(scope.row)">接收</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;" @click.stop="handleReject(scope.row)">驳回</el-button>
</span>
</template>
</el-table-column>
</el-table>
<div v-else-if="!loading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无待审核产需单' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 已排产 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'scheduled'">
<div class="detail-card-header">
<span>已排产明细</span>
<span v-if="scheduledItemList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ scheduledItemList.length }}
</span>
</div>
<div class="detail-card-body" style="padding:0;" v-loading="schLoading">
<el-table
v-if="scheduledItemList.length > 0"
:data="scheduledItemList"
border
size="small"
class="aps-table"
>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" />
<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="品名" prop="productType" min-width="100" />
<el-table-column label="订货单位" prop="customerName" min-width="140" />
<el-table-column label="备注" prop="remark" min-width="100" />
<el-table-column label="操作" width="110" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button type="text" size="small" style="color:#409EFF;" @click="handleEditScheduled(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;" @click="handleDeleteScheduled(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
</el-table>
<div v-else-if="!schLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无已排产数据' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 下钻弹窗来源订单信息 -->
<el-dialog title="来源订单信息" :visible.sync="drillDialogVisible" width="600px" append-to-body>
<div v-if="drillOrder" class="detail-card" style="border:none;box-shadow:none;">
<div class="detail-card-body">
<div class="form-grid-2">
<div class="form-field"><label>订单编号</label><div class="field-value">{{ drillOrder.orderCode }}</div></div>
<div class="form-field"><label>销售员</label><div class="field-value">{{ drillOrder.salesman }}</div></div>
<div class="form-field"><label>客户公司</label><div class="field-value">{{ drillOrder.companyName }}</div></div>
<div class="form-field"><label>联系人</label><div class="field-value">{{ drillOrder.contactPerson }}</div></div>
<div class="form-field"><label>联系电话</label><div class="field-value">{{ drillOrder.contactWay }}</div></div>
<div class="form-field"><label>交货日期</label><div class="field-value">{{ drillOrder.deliveryDate }}</div></div>
<div class="form-field"><label>合同号</label><div class="field-value">{{ drillOrder.contractCode }}</div></div>
<div class="form-field" style="grid-column:1/3;"><label>备注</label><div class="field-value">{{ drillOrder.remark }}</div></div>
</div>
</div>
</div>
<div v-else>
<el-empty description="未找到订单信息" />
</div>
</el-dialog>
<!-- 驳回理由对话框 -->
<el-dialog title="驳回产需单" :visible.sync="rejectDialogVisible" width="450px" append-to-body :close-on-click-modal="false">
<el-form ref="rejectForm" :model="rejectForm" label-width="90px" size="small">
<el-form-item label="排产单号">
<span style="color:#2c3e50;font-weight:500;">{{ rejectForm.scheduleNo }}</span>
</el-form-item>
<el-form-item label="驳回理由" prop="returnReason">
<el-input v-model="rejectForm.returnReason" type="textarea" :rows="4" placeholder="请填写驳回理由" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="rejectBtnLoading" type="danger" @click="confirmReject"> </el-button>
<el-button @click="rejectDialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- 已排产明细编辑对话框 -->
<el-dialog :title="editDialogTitle" :visible.sync="editDialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<el-form ref="editForm" :model="editForm" label-width="100px" size="small">
<el-form-item label="排产单号">
<span style="color:#2c3e50;">{{ editForm.scheduleNo }}</span>
</el-form-item>
<el-form-item label="规格" prop="spec">
<el-input v-model="editForm.spec" />
</el-form-item>
<el-form-item label="材质" prop="material">
<el-input v-model="editForm.material" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="editForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="品名" prop="productType">
<el-input v-model="editForm.productType" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="editBtnLoading" type="danger" @click="submitEditForm"> </el-button>
<el-button @click="editDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listRequirement,
updateRequirement
} from '@/api/aps/requirement'
import {
getCrmOrderInfo,
listScheduleItem,
addScheduleItem,
updateScheduleItem,
delScheduleItem
} from '@/api/aps/schedule'
export default {
name: 'ApsSchedule',
data() {
const today = new Date()
const y = today.getFullYear()
const m = String(today.getMonth() + 1).padStart(2, '0')
const d = String(today.getDate()).padStart(2, '0')
return {
queryDate: `${y}-${m}-${d}`,
activeTab: 'pending',
loading: false,
schLoading: false,
hasQueried: false,
// 待审核
pendingScheduleList: [],
expandedRowKeys: [],
expandDetailMap: {},
summaryText: '',
// 已排产
scheduledItemList: [],
// 驳回
rejectDialogVisible: false,
rejectBtnLoading: false,
rejectForm: {
scheduleId: null,
scheduleNo: '',
returnReason: ''
},
// 已排产编辑
editDialogVisible: false,
editDialogTitle: '编辑排产明细',
editBtnLoading: false,
editForm: this.getEmptyEditForm(),
// 下钻
drillDialogVisible: false,
drillOrder: null
}
},
created() {
this.handleQuery()
},
methods: {
getEmptyEditForm() {
return {
itemId: undefined,
scheduleId: undefined,
scheduleNo: '',
spec: '',
material: '',
scheduleWeight: 0,
productType: '',
customerName: '',
remark: ''
}
},
handleDateChange() {
this.handleQuery()
},
handleQuery() {
if (!this.queryDate) {
this.$message.warning('请选择生产日期')
return
}
this.hasQueried = true
if (this.activeTab === 'pending') {
this.queryPending()
} else {
this.queryScheduled()
}
},
// ====== 待审核 ======
queryPending() {
this.loading = true
this.pendingScheduleList = []
listRequirement({
prodDate: this.queryDate,
scheduleStatus: 1,
pageNum: 1,
pageSize: 999
}).then(res => {
const list = res.rows || []
this.pendingScheduleList = list
// 构建展开映射
const map = {}
list.forEach(sch => {
map[sch.scheduleId] = (sch.detailList || []).map(d => ({
...d,
scheduleId: sch.scheduleId,
productType: sch.productType || d.productType || ''
}))
})
this.expandDetailMap = map
const totalWeight = list.reduce((sum, sch) => {
const details = sch.detailList || []
return sum + details.reduce((s, d) => s + (parseFloat(d.scheduleWeight) || 0), 0)
}, 0)
const totalCount = list.reduce((sum, sch) => sum + (sch.detailList || []).length, 0)
this.summaryText = `${list.length} 个产需单,${totalCount} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
}).catch(() => {
this.pendingScheduleList = []
this.summaryText = ''
}).finally(() => {
this.loading = false
})
},
handleAccept(sch) {
this.$confirm(
`确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => {
const details = sch.detailList || []
if (details.length === 0) {
this.$message.warning('该产需单无可排产的明细')
return
}
const promises = details.map(detail =>
addScheduleItem({
scheduleId: sch.scheduleId,
scheduleNo: sch.scheduleNo,
spec: detail.spec || '',
material: detail.material || '',
scheduleWeight: detail.scheduleWeight || 0,
productType: sch.productType || detail.productType || '',
customerName: sch.customerName || '',
remark: detail.remark || ''
})
)
// 更新产需单状态为 2已接收
promises.push(
updateRequirement({
scheduleId: sch.scheduleId,
scheduleStatus: 2
})
)
this.$message.info('正在处理接收...')
Promise.all(promises).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入')
this.queryPending()
}).catch(() => {
this.$modal.msgError('接收失败')
})
}).catch(() => {})
},
handleReject(sch) {
this.rejectForm.scheduleId = sch.scheduleId
this.rejectForm.scheduleNo = sch.scheduleNo
this.rejectForm.returnReason = ''
this.rejectDialogVisible = true
this.$nextTick(() => {
this.$refs.rejectForm && this.$refs.rejectForm.clearValidate()
})
},
confirmReject() {
if (!this.rejectForm.returnReason || !this.rejectForm.returnReason.trim()) {
this.$message.warning('请填写驳回理由')
return
}
this.rejectBtnLoading = true
updateRequirement({
scheduleId: this.rejectForm.scheduleId,
scheduleStatus: 3,
returnReason: this.rejectForm.returnReason.trim()
}).then(() => {
this.$modal.msgSuccess('已驳回')
this.rejectDialogVisible = false
this.queryPending()
}).catch(() => {
this.$modal.msgError('驳回失败')
}).finally(() => {
this.rejectBtnLoading = false
})
},
// ====== 已排产 ======
queryScheduled() {
this.schLoading = true
this.scheduledItemList = []
listScheduleItem({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => {
this.scheduledItemList = res.rows || []
const totalWeight = this.scheduledItemList.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0)
this.summaryText = `${this.scheduledItemList.length} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
}).catch(() => {
this.scheduledItemList = []
this.summaryText = ''
}).finally(() => {
this.schLoading = false
})
},
handleEditScheduled(row) {
this.editForm = { ...this.getEmptyEditForm(), ...row }
this.editDialogTitle = '编辑排产明细'
this.editDialogVisible = true
this.$nextTick(() => {
this.$refs.editForm && this.$refs.editForm.clearValidate()
})
},
submitEditForm() {
this.editBtnLoading = true
updateScheduleItem(this.editForm).then(() => {
this.$modal.msgSuccess('修改成功')
this.editDialogVisible = false
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('修改失败')
}).finally(() => {
this.editBtnLoading = false
})
},
handleDeleteScheduled(row) {
this.$confirm(`确认删除排产明细「${row.spec} / ${row.material}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delScheduleItem(row.itemId || row.scheduleItemId).then(() => {
this.$modal.msgSuccess('删除成功')
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('删除失败')
})
}).catch(() => {})
},
// ====== 下钻 ======
// ====== 待审核辅助方法 ======
scheduleTotalWeight(sch) {
const details = sch.detailList || []
const total = details.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0)
return total.toFixed(3)
},
handleScheduleClick(sch) {
// 父行单击展开/折叠
const idx = this.expandedRowKeys.indexOf(sch.scheduleId)
if (idx >= 0) {
this.expandedRowKeys.splice(idx, 1)
} else {
this.expandedRowKeys.push(sch.scheduleId)
}
},
handleExpandChange() {
// noop
},
handleRowClick(row) {
// 子行(明细行)点击查看来源订单
const sch = this.pendingScheduleList.find(s => s.scheduleId === row.scheduleId)
if (!sch || !sch.orderList || sch.orderList.length === 0) {
this.$message.warning('未找到关联订单')
return
}
const order = sch.orderList[0]
getCrmOrderInfo(order.orderId).then(res => {
this.drillOrder = res.data
this.drillDialogVisible = true
}).catch(() => {
this.$message.warning('未找到来源订单')
})
}
}
}
</script>
<style scoped lang="scss">
@import './scss/aps-theme.scss';
.aps-sch-page {
height: 100%;
padding: 8px;
box-sizing: border-box;
background: $aps-bg;
display: flex;
flex-direction: column;
gap: 12px;
}
// 工具栏
.aps-sch-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
flex-shrink: 0;
flex-wrap: wrap;
}
.aps-sch-label {
font-size: 13px;
font-weight: 600;
color: $aps-text;
white-space: nowrap;
}
.aps-sch-summary {
margin-left: auto;
font-size: 12px;
color: $aps-text-muted;
background: $aps-silver-1;
padding: 4px 12px;
border-radius: $aps-radius;
border: 1px solid $aps-border;
}
// 排产卡片
.aps-sch-card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
.detail-card-body {
flex: 1;
overflow: auto;
min-height: 0;
}
}
// 表格
.aps-table {
width: 100%;
::v-deep th {
background: $aps-silver-1 !important;
color: $aps-text !important;
font-weight: 600 !important;
}
::v-deep .el-table__body tr:hover > td {
background-color: $aps-red-1 !important;
cursor: pointer;
}
::v-deep td {
padding: 6px 8px;
}
}
// 复用卡片/网格变量
.aps-btn-red {
@include aps-btn-red;
}
.detail-card {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
overflow: hidden;
}
.detail-card-header {
background: linear-gradient(to right, $aps-red-2, $aps-red-3);
color: white;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.detail-card-body {
padding: 14px;
}
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.form-field label {
font-size: 11px;
color: $aps-text-muted;
font-weight: 500;
}
.form-field .field-value {
font-size: 13px;
color: $aps-text;
padding: 4px 0;
border-bottom: 1px solid $aps-silver-mid;
}
// Tabs 覆盖
::v-deep .el-tabs__item {
font-size: 13px;
padding: 0 14px;
}
::v-deep .el-tabs__header {
margin: 0;
}
::v-deep .el-tabs__active-bar {
background-color: $aps-red-2 !important;
}
::v-deep .el-tabs__item.is-active {
color: $aps-red-2 !important;
}
</style>