Files
klp-oa/klp-ui/src/views/wms/post/aps/requirement.vue
Joshi ee376f922f feat(aps): 新增排产单明细合并功能及优化界面展示
- 在排产单明细表格中添加多选合并功能
- 实现排产单明细合并对话框及合并逻辑
- 优化排产单明细表格列配置和表单布局
- 添加合并校验和接收产需单API接口
- 重构订单绑定解绑逻辑提升用户体验
- 添加ScheduleDetailCoilBind组件引入
2026-06-29 16:07:29 +08:00

1227 lines
41 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-req-page">
<el-row :gutter="20" class="aps-req-row">
<!-- 左侧产需单列表 -->
<el-col :span="8" class="aps-req-left" v-loading="reqLoading">
<!-- 筛选区 -->
<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;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.scheduleNo" placeholder="排产单号" clearable @keyup.enter.native="handleSearch"
style="width: 130px;" size="small" />
<el-input v-model="queryParams.customerName" placeholder="客户名" clearable
@keyup.enter.native="handleSearch" style="width: 130px;" size="small" />
<el-date-picker v-model="queryParams.prodDate" type="date" placeholder="生产日期" value-format="yyyy-MM-dd"
style="width: 130px;" size="small" clearable @change="handleSearch" />
<el-button class="aps-btn-red" icon="el-icon-search" size="mini" @click="handleSearch">筛选</el-button>
<el-button class="aps-btn-red" icon="el-icon-plus" size="mini" @click="handleAdd"></el-button>
</div>
</div>
</div>
<div class="panel-header">
<span>产需单列表</span>
<span style="font-weight:normal;color:var(--text-muted);font-size:11px;"> {{ total }} </span>
</div>
<div class="list-container">
<div v-for="item in reqList" :key="item.scheduleId" class="list-item"
:class="{ active: currentReq && currentReq.scheduleId === item.scheduleId }" @click="handleReqClick(item)">
<div class="item-title">
{{ item.scheduleNo }}
<span class="badge" :class="'badge-' + statusBadgeType(item.scheduleStatus)">{{
statusMap[item.scheduleStatus] || '未知' }}</span>
</div>
<div class="item-sub">
生产日期{{ item.prodDate || '-' }} 客户{{ item.customerName || '-' }} 业务员{{ item.businessUser || '-' }}
</div>
<div class="item-actions">
<button class="link-btn" @click.stop="handleDelete(item)">删除</button>
</div>
</div>
<div v-if="reqList.length === 0 && !reqLoading" style="padding: 40px; text-align: center; color: #909399;">
暂无产需单数据
</div>
</div>
<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>
<!-- 右侧详情区 -->
<el-col :span="16" class="aps-req-right">
<el-empty v-if="!currentReq" description="选择产需单查看更多信息" />
<div v-else class="detail-panel">
<div class="detail-card">
<div class="detail-card-header">
<span>产需单信息</span>
<div style="display:flex; gap:6px;">
<button class="header-btn" :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3" @click="handleEdit(currentReq)">编辑</button>
<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>
</div>
</div>
<div class="detail-card-body">
<table class="req-info-table">
<tbody>
<tr>
<td class="req-td-label">排产单号</td>
<td class="req-td-value">{{ currentReq.scheduleNo }}</td>
<td class="req-td-label">生产日期</td>
<td class="req-td-value">{{ currentReq.prodDate }}</td>
</tr>
<tr>
<td class="req-td-label">排产状态</td>
<td class="req-td-value"><span class="aps-status-tag"
:class="'status-' + (currentReq.scheduleStatus || 0)">{{ statusMap[currentReq.scheduleStatus] ||
'未知' }}</span></td>
<td class="req-td-label">业务员</td>
<td class="req-td-value">{{ currentReq.businessUser }}</td>
</tr>
<tr>
<td class="req-td-label">联系电话</td>
<td class="req-td-value">{{ currentReq.businessPhone }}</td>
<td class="req-td-label">订货单位</td>
<td class="req-td-value">{{ currentReq.customerName }}</td>
</tr>
<tr>
<td class="req-td-label">交货期()</td>
<td class="req-td-value">{{ currentReq.deliveryCycle }}</td>
<td class="req-td-label">产品用途</td>
<td class="req-td-value">{{ currentReq.usePurpose }}</td>
</tr>
<tr>
<td class="req-td-label">品名</td>
<td class="req-td-value" colspan="3">{{ currentReq.productType }}</td>
</tr>
<tr>
<td class="req-td-label">厚度公差</td>
<td class="req-td-value">{{ currentReq.thicknessTolerance }}</td>
<td class="req-td-label">宽度公差</td>
<td class="req-td-value">{{ currentReq.widthTolerance }}</td>
</tr>
<tr>
<td class="req-td-label">表面质量</td>
<td class="req-td-value">{{ currentReq.surfaceQuality }}</td>
<td class="req-td-label">表面处理</td>
<td class="req-td-value">{{ currentReq.surfaceTreatment }}</td>
</tr>
<tr>
<td class="req-td-label">内径尺寸</td>
<td class="req-td-value">{{ currentReq.innerDiameter }}</td>
<td class="req-td-label">外径要求</td>
<td class="req-td-value">{{ currentReq.outerDiameter }}</td>
</tr>
<tr>
<td class="req-td-label">包装要求</td>
<td class="req-td-value">{{ currentReq.packReq }}</td>
<td class="req-td-label">切边要求</td>
<td class="req-td-value">{{ currentReq.cutEdgeReq }}</td>
</tr>
<tr>
<td class="req-td-label">单件重量</td>
<td class="req-td-value">{{ currentReq.singleCoilWeight }}</td>
<td class="req-td-label">交货重量偏差</td>
<td class="req-td-value">{{ currentReq.weightDeviation }}</td>
</tr>
<tr>
<td class="req-td-label">其他技术要求</td>
<td class="req-td-value" colspan="3">{{ currentReq.otherTechReq }}</td>
</tr>
<tr>
<td class="req-td-label">付款情况说明</td>
<td class="req-td-value" colspan="3">{{ currentReq.paymentDesc }}</td>
</tr>
<tr>
<td class="req-td-label">备注</td>
<td class="req-td-value" colspan="3">{{ currentReq.remark }}</td>
</tr>
<tr v-if="currentReq.returnReason">
<td class="req-td-label" style="color:#e74c3c;">驳回原因</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{ currentReq.returnReason }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="detail-card">
<div class="detail-card-header">
<span>已绑定的销售订单{{ (currentReq.orderList || []).length }} </span>
<button v-if="canEdit" class="header-btn" @click="openBindDialog">+ 添加订单</button>
</div>
<div class="detail-card-body" v-loading="boundLoading">
<div v-if="(currentReq.orderList || []).length === 0"
style="padding: 16px; text-align: center; color: #909399;">暂无绑定订单</div>
<div v-else class="bound-order-cards">
<div v-for="order in currentReq.orderList" :key="order.orderId" class="bound-order-card">
<div class="bound-order-card-header">
<span>{{ order.orderCode || ('订单 #' + order.orderId) }}</span>
<button v-if="canEdit" class="link-btn" @click="handleUnbindByOrderId(order)">解绑</button>
</div>
<div class="bound-order-card-body">
<div style="font-size:12px; color:#7f8c8d; margin-bottom:3px;">
客户<span style="color:#2c3e50;">{{ order.companyName || '-' }}</span>
</div>
<div style="font-size:12px; color:#7f8c8d; margin-bottom:3px;">
销售员<span style="color:#2c3e50;">{{ order.salesman || '-' }}</span>
</div>
<div style="font-size:12px; color:#7f8c8d;">
交货日期<span style="color:#2c3e50;">{{ order.deliveryDate || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<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>
</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="备注" prop="remark" min-width="120" />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;" @click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;" @click="handleDetailDelete(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="requirementDetailList.length === 0 && !detailLoading" description="暂无排产明细" />
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px" append-to-body
:close-on-click-modal="false">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="110px" size="small">
<!-- 第一行必填项 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="排产单号" prop="scheduleNo">
<el-input v-model="reqForm.scheduleNo" placeholder="自动生成或手动填写" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生产日期" prop="prodDate">
<el-date-picker v-model="reqForm.prodDate" type="date" value-format="yyyy-MM-dd" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 基本信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="订货单位" prop="customerName">
<el-input v-model="reqForm.customerName" />
</el-form-item>
<el-form-item label="业务员" prop="businessUser">
<el-input v-model="reqForm.businessUser" />
</el-form-item>
<el-form-item label="联系电话" prop="businessPhone">
<el-input v-model="reqForm.businessPhone" />
</el-form-item>
<el-form-item label="品名" prop="productType">
<el-input v-model="reqForm.productType" />
</el-form-item>
<el-form-item label="交货期(天)" prop="deliveryCycle">
<el-input-number v-model="reqForm.deliveryCycle" :min="0" :max="999" style="width:100%" />
</el-form-item>
<el-form-item label="产品用途" prop="usePurpose">
<el-input v-model="reqForm.usePurpose" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="厚度公差" prop="thicknessTolerance">
<el-input v-model="reqForm.thicknessTolerance" />
</el-form-item>
<el-form-item label="宽度公差" prop="widthTolerance">
<el-input v-model="reqForm.widthTolerance" />
</el-form-item>
<el-form-item label="表面质量" prop="surfaceQuality">
<el-input v-model="reqForm.surfaceQuality" />
</el-form-item>
<el-form-item label="表面处理" prop="surfaceTreatment">
<el-input v-model="reqForm.surfaceTreatment" />
</el-form-item>
<el-form-item label="内径尺寸" prop="innerDiameter">
<el-input v-model="reqForm.innerDiameter" />
</el-form-item>
<el-form-item label="外径要求" prop="outerDiameter">
<el-input v-model="reqForm.outerDiameter" />
</el-form-item>
</el-col>
</el-row>
<!-- 包装及其他 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="包装要求" prop="packReq">
<el-input v-model="reqForm.packReq" />
</el-form-item>
<el-form-item label="切边要求" prop="cutEdgeReq">
<el-input v-model="reqForm.cutEdgeReq" />
</el-form-item>
<el-form-item label="单件重量" prop="singleCoilWeight">
<el-input v-model="reqForm.singleCoilWeight" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货重量偏差" prop="weightDeviation">
<el-input v-model="reqForm.weightDeviation" />
</el-form-item>
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="付款情况说明" prop="paymentDesc">
<el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="reqForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="btnLoading" type="danger" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- 绑定订单对话框 -->
<el-dialog title="添加绑定订单" :visible.sync="bindDialogVisible" width="860px" append-to-body
:close-on-click-modal="false" class="aps-bind-dialog">
<div style="display: flex; gap: 16px; height: 420px;">
<!-- 左侧订单列表 -->
<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;"
@change="searchBindOrders" />
<el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="searchBindOrders">搜索</el-button>
</div>
<div style="flex:1; overflow-y: auto; border: 1px solid #e4e7ed; border-radius: 4px;"
v-loading="bindSearchLoading" element-loading-text="搜索中...">
<div v-for="item in candidateOrderList" :key="item.orderId" class="bind-order-item"
:class="{ selected: isBindSelected(item), previewing: bindPreviewOrder && bindPreviewOrder.orderId === item.orderId }"
@click="handleBindRowClick(item)">
<el-checkbox :value="isBindSelected(item)" @change="toggleBindOrder(item)" @click.stop
style="margin-right: 8px;" />
<div style="flex:1; min-width:0;">
<div style="font-weight:500; font-size:13px; color:#2c3e50;">{{ item.orderCode }}</div>
<div style="font-size:11px; color:#7f8c8d; margin-top:2px;">{{ item.companyName }} · {{ item.salesman }}
</div>
</div>
</div>
<div v-if="candidateOrderList.length === 0" style="padding:30px; text-align:center; color:#909399;">暂无订单
</div>
</div>
</div>
<!-- 右侧产品内容预览 -->
<div style="width: 55%; display: flex; flex-direction: column;">
<div
style="font-size:13px; font-weight:600; color:#2c3e50; margin-bottom:8px; padding-left:8px; border-left:3px solid #ff4d4f;">
{{ bindPreviewOrder ? bindPreviewOrder.orderCode + ' - 产品明细' : '点击订单行预览产品明细' }}
</div>
<div style="flex:1; overflow-y: auto; border: 1px solid #e4e7ed; border-radius: 4px; padding: 10px;"
v-if="bindPreviewOrder">
<el-table :data="bindPreviewProducts" border size="small" style="width:100%;">
<el-table-column label="规格" prop="spec" min-width="100" />
<el-table-column label="材质" prop="material" width="80" />
<el-table-column label="数量(吨)" prop="quantity" width="80" align="right" />
<!-- <el-table-column label="含税单价" prop="taxPrice" width="80" align="right" /> -->
<el-table-column label="备注" prop="remark" min-width="80" />
</el-table>
<div style="margin-top:8px; font-size:12px; color:#7f8c8d; display:flex; gap:16px;">
<span>产品名称{{ bindPreviewProductName }}</span>
<span>总数量{{ bindPreviewTotalQty }} </span>
<span>含税总额{{ bindPreviewTotalAmount }}</span>
</div>
</div>
<div v-else
style="flex:1; display:flex; align-items:center; justify-content:center; color:#bfbfbf; font-size:13px;">
请点击左侧订单行查看产品明细
</div>
</div>
</div>
<div slot="footer" class="dialog-footer" style="margin-top:12px;">
<span style="font-size:12px; color:#7f8c8d; margin-right:auto;">已选 {{ selectedBindOrders.length }} 个订单</span>
<el-button :loading="bindBtnLoading" type="danger" @click="confirmBind"> </el-button>
<el-button @click="bindDialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- 排产明细新增/编辑对话框 -->
<el-dialog :title="detailDialogTitle" :visible.sync="detailDialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<el-form ref="detailForm" :model="detailForm" :rules="detailFormRules" label-width="100px" size="small">
<el-form-item label="规格" prop="spec">
<el-input v-model="detailForm.spec" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="材质" prop="material">
<el-input v-model="detailForm.material" placeholder="请输入材质" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="detailBtnLoading" type="danger" @click="submitDetailForm"> </el-button>
<el-button @click="detailDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listRequirement,
getRequirement,
addRequirement,
updateRequirement,
delRequirement,
addRel,
delRel,
listRel,
addRequirementDetail,
updateRequirementDetail,
delRequirementDetail
} from '@/api/aps/requirement'
import { listCrmOrder } from '@/api/aps/order'
import { parseProductContent } from '@/utils/productContent'
export default {
name: 'ApsRequirement',
data() {
return {
// 左侧列表
reqLoading: false,
reqList: [],
total: 0,
currentReq: null,
queryParams: {
scheduleNo: '',
prodDate: '',
customerName: '',
pageNum: 1,
pageSize: 10
},
statusMap: { 0: '草稿', 1: '待审核', 2: '已下达', 3: '已退回' },
statusBadgeMap: { 0: 'gray', 1: 'blue', 2: 'green', 3: 'red' },
// 绑定订单
bindDialogVisible: false,
bindQuery: { keyword: '', pageNum: 1, pageSize: 50 },
candidateOrderList: [],
selectedBindOrders: [],
bindBtnLoading: false,
bindSearchLoading: false,
bindPreviewOrder: null,
bindPreviewProducts: [],
bindPreviewProductName: '',
bindPreviewTotalQty: 0,
bindPreviewTotalAmount: 0,
// 排产明细
detailLoading: false,
requirementDetailList: [],
// 排产明细新增/编辑对话框
detailDialogVisible: false,
detailDialogTitle: '新增排产明细',
detailBtnLoading: false,
detailForm: this.getEmptyDetailForm(),
detailFormRules: {
spec: [{ required: true, message: '规格不能为空', trigger: 'blur' }],
material: [{ required: true, message: '材质不能为空', trigger: 'blur' }],
scheduleWeight: [{ required: true, message: '排产吨数不能为空', trigger: 'change' }]
},
// 新增/编辑对话框
dialogVisible: false,
dialogTitle: '新增产需单',
btnLoading: false,
reqForm: this.getEmptyForm(),
reqRules: {
scheduleNo: [{ required: true, message: '排产单号不能为空', trigger: 'blur' }],
prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }]
}
}
},
computed: {
canEdit() {
return this.currentReq && (this.currentReq.scheduleStatus === 0 || this.currentReq.scheduleStatus === 3)
},
detailTotalWeight() {
return this.requirementDetailList.reduce((sum, item) => {
const n = Number(item.scheduleWeight)
return isNaN(n) ? sum : sum + n
}, 0)
}
},
created() {
this.getList()
},
methods: {
getEmptyForm() {
return {
scheduleId: undefined,
scheduleNo: '',
prodDate: '',
customerName: '',
businessUser: '',
businessPhone: '',
deliveryCycle: undefined,
usePurpose: '',
productType: '',
thicknessTolerance: '',
widthTolerance: '',
surfaceQuality: '',
surfaceTreatment: '',
innerDiameter: '',
outerDiameter: '',
packReq: '',
cutEdgeReq: '',
singleCoilWeight: '',
weightDeviation: '',
otherTechReq: '',
paymentDesc: '',
remark: ''
}
},
getEmptyDetailForm() {
return {
scheduleDetailId: undefined,
scheduleId: undefined,
orderDetailId: undefined,
spec: '',
material: '',
scheduleWeight: 0,
productType: '',
remark: ''
}
},
handleSearch() {
this.queryParams.pageNum = 1
this.getList()
},
statusBadgeType(status) {
return this.statusBadgeMap[status] || 'gray'
},
getList() {
this.reqLoading = true
listRequirement(this.queryParams).then(res => {
this.reqList = res.rows || []
this.total = res.total || 0
}).catch(() => {
this.reqList = []
this.total = 0
}).finally(() => {
this.reqLoading = false
})
},
handleReqClick(item) {
this.currentReq = item
this.detailLoading = true
getRequirement(item.scheduleId).then(res => {
if (res.data) {
this.currentReq = res.data
this.requirementDetailList = res.data.detailList || []
}
}).catch(() => {
this.requirementDetailList = []
}).finally(() => {
this.detailLoading = false
})
},
// ====== 新增/编辑 ======
resetForm() {
this.reqForm = this.getEmptyForm()
},
handleAdd() {
this.resetForm()
this.dialogTitle = '新增产需单'
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
},
handleEdit(item) {
getRequirement(item.scheduleId).then(res => {
this.reqForm = { ...this.getEmptyForm(), ...res.data }
this.dialogTitle = '编辑产需单'
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
})
},
submitForm() {
this.$refs.reqForm.validate(valid => {
if (!valid) return
this.btnLoading = true
const action = this.reqForm.scheduleId ? updateRequirement : addRequirement
action(this.reqForm).then(() => {
this.$modal.msgSuccess(this.reqForm.scheduleId ? '修改成功' : '新增成功')
this.dialogVisible = false
this.getList()
}).catch(() => {
this.$modal.msgError('操作失败')
}).finally(() => {
this.btnLoading = false
})
})
},
handleDelete(item) {
this.$confirm(`确认删除产需单「${item.scheduleNo}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delRequirement(item.scheduleId).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.currentReq && this.currentReq.scheduleId === item.scheduleId) {
this.currentReq = null
}
this.getList()
})
}).catch(() => { })
},
openBindDialog() {
this.bindDialogVisible = true
this.selectedBindOrders = []
this.bindPreviewOrder = null
this.bindPreviewProducts = []
this.bindPreviewProductName = ''
this.bindPreviewTotalQty = 0
this.bindPreviewTotalAmount = 0
this.searchBindOrders()
},
searchBindOrders() {
this.bindSearchLoading = true
listCrmOrder(this.bindQuery).then(res => {
const boundOrderIds = (this.currentReq?.orderList || []).map(o => o.orderId)
this.candidateOrderList = (res.rows || []).filter(o => !boundOrderIds.includes(o.orderId))
}).catch(() => {
this.candidateOrderList = []
}).finally(() => {
this.bindSearchLoading = false
})
},
isBindSelected(order) {
return this.selectedBindOrders.some(o => o.orderId === order.orderId)
},
toggleBindOrder(order) {
const idx = this.selectedBindOrders.findIndex(o => o.orderId === order.orderId)
if (idx >= 0) {
this.selectedBindOrders.splice(idx, 1)
} else {
this.selectedBindOrders.push(order)
}
},
handleBindRowClick(order) {
this.bindPreviewOrder = order
if (order.productContent) {
const parsed = parseProductContent(order.productContent)
this.bindPreviewProducts = parsed.products || []
this.bindPreviewProductName = parsed.productName || ''
this.bindPreviewTotalQty = parsed.totalQuantity || 0
this.bindPreviewTotalAmount = parsed.totalTaxTotal || 0
} else {
this.bindPreviewProducts = []
this.bindPreviewProductName = ''
this.bindPreviewTotalQty = 0
this.bindPreviewTotalAmount = 0
}
},
confirmBind() {
if (this.selectedBindOrders.length === 0) {
this.$message.warning('请选择要绑定的订单')
return
}
this.bindBtnLoading = true
const scheduleId = this.currentReq.scheduleId
const orderTasks = this.selectedBindOrders.map(order => {
return addRel({
orderId: order.orderId,
scheduleId: scheduleId,
relWeight: null,
remark: ''
}).then(() => {
if (!order.productContent) return
let products = []
let productType = ''
try {
const parsed = parseProductContent(order.productContent)
products = (parsed.products || []).filter(p => p.spec && p.material)
productType = parsed.productName || ''
} catch (e) {
return
}
if (products.length === 0) return
const detailPromises = products.map(prod =>
addRequirementDetail({
scheduleId: scheduleId,
orderDetailId: order.orderId,
spec: prod.spec,
material: prod.material,
scheduleWeight: prod.quantity || 0,
productType: productType,
remark: prod.remark || ''
})
)
return Promise.all(detailPromises)
})
})
Promise.allSettled(orderTasks).then(results => {
const successCount = results.filter(r => r.status === 'fulfilled').length
const failCount = results.filter(r => r.status === 'rejected').length
this.bindDialogVisible = false
this.handleReqClick(this.currentReq)
if (failCount === 0) {
this.$modal.msgSuccess(`成功绑定 ${successCount} 个订单,排产明细已生成`)
} else if (successCount === 0) {
this.$modal.msgError('绑定失败,请重试')
} else {
this.$modal.msgWarning(`成功绑定 ${successCount} 个,${failCount} 个失败`)
}
}).finally(() => {
this.bindBtnLoading = false
})
},
handleUnbindByOrderId(order) {
this.$confirm('确认解绑该订单吗?解绑将同时删除对应的排产明细。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const tasks = []
// 1. 删除该订单关联的排产明细
const detailIds = this.requirementDetailList
.filter(d => d.orderDetailId === order.orderId)
.map(d => d.scheduleDetailId)
detailIds.forEach(id => {
tasks.push(delRequirementDetail(id))
})
// 2. 查询并删除关联表记录
tasks.push(
listRel({ orderId: order.orderId, scheduleId: this.currentReq.scheduleId }).then(res => {
const rows = res.rows || []
if (rows.length === 0) {
throw new Error('未找到关联记录')
}
const relId = rows[0].id || rows[0].relId
return delRel(relId)
})
)
Promise.all(tasks).then(() => {
this.$modal.msgSuccess('解绑成功')
this.handleReqClick(this.currentReq)
}).catch(() => {
this.$modal.msgError('解绑失败')
})
}).catch(() => { })
},
// ====== 下发 ======
handleDispatch() {
this.$confirm('确认下发该产需单吗?下发后将进入审核流程。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
updateRequirement({
scheduleId: this.currentReq.scheduleId,
scheduleStatus: 1
}).then(() => {
this.$modal.msgSuccess('下发成功')
this.getList()
this.handleReqClick(this.currentReq)
}).catch(() => {
this.$modal.msgError('下发失败')
})
}).catch(() => { })
},
// ====== 排产明细新增/编辑/删除 ======
resetDetailForm() {
this.detailForm = this.getEmptyDetailForm()
},
handleDetailAdd() {
if (!this.currentReq) {
this.$message.warning('请先选择一个产需单')
return
}
this.resetDetailForm()
this.detailForm.scheduleId = this.currentReq.scheduleId
this.detailDialogTitle = '新增排产明细'
this.detailDialogVisible = true
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate()
})
},
handleDetailEdit(row) {
this.detailForm = { ...this.getEmptyDetailForm(), ...row }
this.detailDialogTitle = '编辑排产明细'
this.detailDialogVisible = true
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate()
})
},
submitDetailForm() {
this.$refs.detailForm.validate(valid => {
if (!valid) return
this.detailBtnLoading = true
const formData = { ...this.detailForm }
if (this.detailForm.scheduleDetailId) {
// 更新
updateRequirementDetail(formData).then(() => {
this.$modal.msgSuccess('修改成功')
this.detailDialogVisible = false
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('修改失败')
}).finally(() => {
this.detailBtnLoading = false
})
} else {
// 新增
addRequirementDetail(formData).then(() => {
this.$modal.msgSuccess('新增成功')
this.detailDialogVisible = false
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('新增失败')
}).finally(() => {
this.detailBtnLoading = false
})
}
})
},
handleDetailDelete(row) {
this.$confirm(`确认删除排产明细「${row.spec} / ${row.material}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delRequirementDetail(row.scheduleDetailId).then(() => {
this.$modal.msgSuccess('删除成功')
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('删除失败')
})
}).catch(() => { })
},
refreshDetailList() {
if (!this.currentReq) return
this.detailLoading = true
getRequirement(this.currentReq.scheduleId).then(res => {
if (res.data) {
this.requirementDetailList = res.data.detailList || []
}
}).catch(() => {
this.requirementDetailList = []
}).finally(() => {
this.detailLoading = false
})
},
}
}
</script>
<style scoped lang="scss">
@import './scss/aps-theme.scss';
.aps-req-page {
height: 100%;
padding: 8px;
box-sizing: border-box;
background: $aps-silver-1;
}
.aps-req-row {
height: 100%;
}
// ====== 左侧(参照 HTML 产需单列表) ======
.aps-req-left {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
border: 1px solid $aps-silver-3;
border-radius: 6px;
box-sizing: border-box;
overflow: hidden;
min-height: 0;
}
.panel-header {
background: $aps-silver-mid;
padding: 8px 12px;
font-size: 12px;
font-weight: 600;
color: $aps-text;
border-bottom: 1px 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;
}
.list-item {
padding: 10px 12px;
border-bottom: 1px solid $aps-silver-1;
cursor: pointer;
transition: background 0.15s;
}
.list-item:hover {
background: $aps-silver-1;
}
.list-item.active {
background: $aps-red-1;
border-left: 3px solid $aps-red-2;
}
.list-item .item-title {
font-weight: 500;
color: $aps-text;
font-size: 13px;
margin-bottom: 3px;
display: flex;
align-items: center;
gap: 6px;
}
.list-item .item-sub {
font-size: 12px;
color: $aps-text-muted;
margin-bottom: 4px;
}
.list-item .item-actions {
display: flex;
gap: 8px;
}
// ====== 徽标 ======
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
}
.badge-red {
background: #fdecea;
color: $aps-red-2;
border: 1px solid #f5b7b1;
}
.badge-green {
background: #eafaf1;
color: #27ae60;
border: 1px solid #a9dfbf;
}
.badge-blue {
background: #eaf4fb;
color: #2980b9;
border: 1px solid #aed6f1;
}
.badge-gray {
background: $aps-silver-1;
color: $aps-text-muted;
border: 1px solid $aps-border;
}
// ====== 右侧(平铺) ======
.aps-req-right {
height: 100%;
background: #fff;
border: 1px solid $aps-silver-3;
border-radius: 6px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.detail-panel {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
background: $aps-bg;
min-height: 0;
}
// ====== 右侧详情卡片 ======
.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;
}
.detail-card-header .header-btn {
background: rgba(255, 255, 255, 0.25);
color: white;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 3px;
padding: 4px 12px;
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.detail-card-header .header-btn:hover {
background: rgba(255, 255, 255, 0.4);
border-color: rgba(255, 255, 255, 0.7);
}
.detail-card-header .header-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.detail-card-body {
padding: 0;
}
.req-info-table {
width: 100%;
border-collapse: collapse;
border: 1px solid $aps-border;
font-size: 13px;
td {
border: 1px solid $aps-border;
padding: 6px 10px;
vertical-align: middle;
}
.req-td-label {
background: $aps-silver-1;
color: $aps-text-muted;
font-weight: 500;
font-size: 12px;
white-space: nowrap;
width: 100px;
text-align: right;
}
.req-td-value {
color: $aps-text;
background: #fff;
word-break: break-all;
&:empty::after {
content: '-';
color: $aps-silver-3;
}
}
}
.aps-status-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
&.status-0 {
background: $aps-silver-2;
color: $aps-silver-4;
}
&.status-1 {
background: #e6f7ff;
color: #1890ff;
}
&.status-2 {
background: #f6ffed;
color: #52c41a;
}
&.status-3 {
background: $aps-red-1;
color: $aps-red-2;
}
}
.aps-table {
width: 100%;
::v-deep th {
background: $aps-silver-1 !important;
color: $aps-silver-5 !important;
font-weight: 600 !important;
}
}
.link-btn {
color: $aps-red-2;
cursor: pointer;
font-size: 12px;
text-decoration: none;
background: none;
border: none;
padding: 0;
font-family: inherit;
}
.link-btn:hover {
text-decoration: underline;
}
// ====== Element 按钮覆盖 ======
::v-deep .el-button--danger {
background: $aps-red-2;
border-color: $aps-red-2;
}
::v-deep .el-button--danger:hover {
background: $aps-red-3;
border-color: $aps-red-3;
}
// ====== 按钮 ======
.aps-btn-red {
@include aps-btn-red;
}
.aps-btn-silver {
@include aps-btn-silver;
}
// ====== 绑定订单卡片 ======
.bound-order-cards {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.bound-order-card {
border: 1px solid $aps-border;
border-radius: $aps-radius;
background: $aps-white;
flex: 1;
min-width: 200px;
max-width: 300px;
}
.bound-order-card .bound-order-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: $aps-silver-1;
border-bottom: 1px solid $aps-border;
font-size: 13px;
font-weight: 600;
color: $aps-text;
border-radius: $aps-radius $aps-radius 0 0;
}
.bound-order-card .bound-order-card-body {
padding: 10px 12px;
}
// ====== 绑定弹窗 ======
.bind-order-item {
display: flex;
align-items: center;
padding: 8px 10px;
border-bottom: 1px solid #ecf0f1;
cursor: pointer;
transition: background 0.15s;
}
.bind-order-item:hover {
background: #f5f5f5;
}
.bind-order-item.selected {
background: #fdecea;
}
.bind-order-item.previewing {
background: #fdecea;
border-left: 3px solid $aps-red-2;
}
.aps-bind-dialog ::v-deep .el-dialog__body {
padding: 16px 20px 0 20px;
}
</style>