Files
klp-oa/klp-ui/src/views/wms/post/aps/requirement.vue
王文昊 9b1b9511cd style(wms/aps/requirement): 拆分需求列表项的信息行
将原单行的生产日期、客户和业务员信息拆分为两行展示,优化页面布局可读性
2026-07-09 17:43:23 +08:00

1436 lines
49 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="list-item-body">
<div class="list-item-left">
<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 || '-' }}
</div>
<div class="item-sub">
客户{{ item.customerName || '-' }} 业务员{{ item.businessUser || '-' }}
</div>
<div class="item-actions">
<button class="link-btn" @click.stop="handleDelete(item)">删除</button>
</div>
</div>
<div class="list-item-right">
<el-button size="mini" plain @click.stop="handleJumpToSchedule(item)">跳转排产</el-button>
</div>
</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="900px" append-to-body
:close-on-click-modal="false">
<div style="display: flex; gap: 16px;">
<div style="flex: 1; min-width: 0;">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="80px" 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="paymentDesc">
<el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" 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>
<!-- 右侧历史记录 -->
<div style="width: 200px; flex-shrink: 0;" v-if="!reqForm.scheduleId && scheduleNoHistory.length > 0">
<div style="font-size: 13px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; padding-left: 8px; border-left: 3px solid #ff4d4f;">
历史记录
</div>
<div style="border: 1px solid #e4e7ed; border-radius: 4px; max-height: 450px; overflow-y: auto;">
<div v-for="item in scheduleNoHistory" :key="item.scheduleNo" class="history-item"
@click="useHistoryRecord(item)"
style="padding: 8px 10px; border-bottom: 1px solid #ecf0f1; cursor: pointer; font-size: 12px; transition: background 0.15s;">
<div style="font-weight: 500; color: #2c3e50;">{{ item.scheduleNo }}</div>
<div style="color: #7f8c8d; margin-top: 2px;">{{ item.customerName || '-' }}</div>
<div style="color: #b0b0b0; font-size: 11px;">{{ item.prodDate || '-' }}</div>
</div>
</div>
</div>
</div>
<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 v-if="bindPreviewExistingRels.length > 0" style="margin-top:12px; padding-top:10px; border-top:1px dashed #e4e7ed;">
<div style="font-size:12px; font-weight:600; color:#e74c3c; margin-bottom:6px;">
该订单已绑定以下产需单
</div>
<div style="display:flex; flex-wrap:wrap; gap:6px;">
<el-tag
v-for="rel in bindPreviewExistingRels"
:key="rel.scheduleId"
size="small"
:type="rel.scheduleStatus === 3 ? 'danger' : 'info'"
effect="plain"
:style="rel.scheduleStatus === 2 ? 'color:#555;border-color:#ccc;background:#f5f5f5;' : ''"
>
{{ rel.scheduleNo }}
<span style="margin-left:4px; opacity:0.7;">{{ bindPreviewRelStatusMap[rel.scheduleStatus] || '' }}</span>
</el-tag>
</div>
</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,
// 当前预览订单已绑定的产需单列表
bindPreviewExistingRels: [],
// 产需单状态映射(用于展示绑定状态)
bindPreviewRelStatusMap: { 0: '草稿', 1: '待审核', 2: '已下达', 3: '已退回' },
// 排产明细
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,
scheduleNoHistory: [],
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()
},
// 跳转到排产单页面(路由跳转)
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'
},
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.reqForm.scheduleNo = this.generateScheduleNo()
this.reqForm.prodDate = this.getTodayStr()
const lastForm = this.getLastFormData()
if (lastForm) {
Object.keys(lastForm).forEach(key => {
if (key !== 'scheduleId' && key !== 'scheduleNo' && key !== 'prodDate' && lastForm[key] !== undefined && lastForm[key] !== '' && lastForm[key] !== null) {
this.reqForm[key] = lastForm[key]
}
})
}
this.scheduleNoHistory = this.getScheduleNoHistory()
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 isNew = !this.reqForm.scheduleId
const action = isNew ? addRequirement : updateRequirement
action(this.reqForm).then(() => {
this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功')
if (isNew) {
this.saveHistoryRecord(this.reqForm)
this.saveLastFormData(this.reqForm)
}
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(() => { })
},
getTodayStr() {
return new Date().toISOString().split('T')[0]
},
generateScheduleNo() {
const now = new Date()
const dateStr = now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0')
const prefix = 'PCXD' + dateStr
const existingNums = this.reqList
.filter(item => item.scheduleNo && item.scheduleNo.startsWith(prefix))
.map(item => {
const seq = parseInt(item.scheduleNo.substring(prefix.length), 10)
return isNaN(seq) ? 0 : seq
})
const maxSeq = existingNums.length > 0 ? Math.max(...existingNums) : 0
const newSeq = (maxSeq + 1).toString().padStart(3, '0')
return prefix + newSeq
},
getScheduleNoHistory() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_history') || '[]')
} catch (e) {
return []
}
},
saveHistoryRecord(formData) {
if (!formData.scheduleNo) return
const history = this.getScheduleNoHistory()
const idx = history.findIndex(item => item.scheduleNo === formData.scheduleNo)
if (idx >= 0) history.splice(idx, 1)
history.unshift({ ...formData })
delete history[0].scheduleId
if (history.length > 20) history.pop()
localStorage.setItem('aps_requirement_history', JSON.stringify(history))
},
useHistoryRecord(item) {
Object.keys(item).forEach(key => {
if (key !== 'scheduleId' && item[key] !== undefined) {
this.reqForm[key] = item[key]
}
})
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
},
getLastFormData() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_last_form') || 'null')
} catch (e) {
return null
}
},
saveLastFormData(formData) {
const data = { ...formData }
delete data.scheduleId
localStorage.setItem('aps_requirement_last_form', JSON.stringify(data))
},
openBindDialog() {
this.bindDialogVisible = true
this.selectedBindOrders = []
this.bindPreviewOrder = null
this.bindPreviewProducts = []
this.bindPreviewProductName = ''
this.bindPreviewTotalQty = 0
this.bindPreviewTotalAmount = 0
this.bindPreviewExistingRels = []
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
}
// 查询该订单已关联了哪些产需单
this.bindPreviewExistingRels = []
listRel({ orderId: order.orderId }).then(res => {
const rows = res.rows || []
// 按 scheduleId 去重,并与当前已有的 reqList 匹配获取单号
const seen = new Set()
const reqMap = new Map()
;(this.reqList || []).forEach(r => reqMap.set(r.scheduleId, r))
const unique = []
rows.forEach(r => {
const key = r.scheduleId
if (!key || seen.has(key)) return
seen.add(key)
// 从已加载的 reqList 中找单号,找不到则不展示(已删除或不在当前页)
const match = reqMap.get(key)
if (match) {
unique.push({ ...r, scheduleNo: match.scheduleNo, scheduleStatus: match.scheduleStatus })
}
})
this.bindPreviewExistingRels = unique
}).catch(() => {
this.bindPreviewExistingRels = []
})
},
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-body {
display: flex;
align-items: stretch;
gap: 8px;
}
.list-item-left {
flex: 1;
min-width: 0;
}
.list-item-right {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding-left: 4px;
}
.item-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.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;
}
// ====== 徽标 ======
.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;
}
.history-item:hover {
background: #fdecea;
}
</style>