本次提交完成APS(高级计划与排程)模块的全量开发: 1. 新增CRM订单相关API接口,包含列表、详情、明细查询 2. 新增产需单相关CRUD API与页面,支持排产单管理与订单绑定 3. 新增按日期查询排产单、订单下钻详情页面 4. 为排产单实体类添加日期格式化注解,修复参数绑定问题 5. 统一封装APS模块主题样式,提供通用混入与变量 6. 实现产需单与销售订单的绑定解绑、明细自动生成功能
1023 lines
33 KiB
Vue
1023 lines
33 KiB
Vue
<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>
|
||
<button class="header-btn" @click="handleEdit(currentReq)">编辑</button>
|
||
</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>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-card">
|
||
<div class="detail-card-header">
|
||
<span>已绑定的销售订单({{ boundOrderList.length }} 条)</span>
|
||
<button 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 class="link-btn" @click="handleUnbindByOrderId(order.orderId)">解绑</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>
|
||
</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>
|
||
<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>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
listRequirement,
|
||
getRequirement,
|
||
addRequirement,
|
||
updateRequirement,
|
||
delRequirement,
|
||
addRel,
|
||
delRel,
|
||
addRequirementDetail
|
||
} 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' },
|
||
|
||
// 绑定订单
|
||
boundOrderList: [],
|
||
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: [],
|
||
|
||
// 新增/编辑对话框
|
||
dialogVisible: false,
|
||
dialogTitle: '新增产需单',
|
||
btnLoading: false,
|
||
reqForm: this.getEmptyForm(),
|
||
reqRules: {
|
||
scheduleNo: [{ required: true, message: '排产单号不能为空', trigger: 'blur' }],
|
||
prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }]
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
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: ''
|
||
}
|
||
},
|
||
|
||
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 => {
|
||
this.candidateOrderList = res.rows || []
|
||
}).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 allPromises = []
|
||
|
||
this.selectedBindOrders.forEach(order => {
|
||
// 1. 创建订单关联
|
||
allPromises.push(
|
||
addRel({
|
||
orderId: order.orderId,
|
||
scheduleId: scheduleId,
|
||
relWeight: null,
|
||
remark: ''
|
||
})
|
||
)
|
||
|
||
// 2. 解析 productContent,每行产品创建一条排产明细
|
||
if (order.productContent) {
|
||
const parsed = parseProductContent(order.productContent)
|
||
const products = parsed.products || []
|
||
const productType = parsed.productName || ''
|
||
products.forEach(prod => {
|
||
allPromises.push(
|
||
addRequirementDetail({
|
||
scheduleId: scheduleId,
|
||
orderDetailId: order.orderId,
|
||
spec: prod.spec || '',
|
||
material: prod.material || '',
|
||
scheduleWeight: prod.quantity || 0,
|
||
productType: productType,
|
||
remark: prod.remark || ''
|
||
})
|
||
)
|
||
})
|
||
}
|
||
})
|
||
|
||
Promise.all(allPromises).then(() => {
|
||
this.$modal.msgSuccess('绑定成功,排产明细已生成')
|
||
this.bindDialogVisible = false
|
||
this.handleReqClick(this.currentReq)
|
||
}).catch(() => {
|
||
this.$modal.msgError('绑定或创建明细失败')
|
||
}).finally(() => {
|
||
this.bindBtnLoading = false
|
||
})
|
||
},
|
||
|
||
handleUnbindByOrderId(orderId) {
|
||
const rel = this.boundOrderList.find(r => r.orderId === orderId)
|
||
if (!rel) {
|
||
this.$message.warning('未找到关联记录')
|
||
return
|
||
}
|
||
this.$confirm('确认解绑该订单吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
delRel(rel.relId).then(() => {
|
||
this.$modal.msgSuccess('解绑成功')
|
||
this.handleReqClick(this.currentReq)
|
||
})
|
||
}).catch(() => { })
|
||
},
|
||
}
|
||
}
|
||
</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-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>
|