fix(scrap): 报废申请页搜索/选择解耦,审批人常驻必填
一、搜索丢失已选(核心 bug) 主表格 :data="stockList" 同时充当搜索结果与选择容器。Element Plus 的 setData 在 reserveSelection 为假时走 clearSelection(),进而 emit selection-change([]),故每次重新搜索都会清空选中态、「已选 N 条」归零。 (源码路径:table/src/store/index.mjs:34-52 → store/watcher.mjs:145-152) 改为「购物车 + 选择弹窗」结构,对齐出库选单页: · 主表格数据源改为 cart(已选清单),删除复选列,改「移除」按钮; · 新增「选择库存物品」弹窗,搜索框移入其中; · 表格 row-key + :reserve-selection="true",跨搜索/翻页保留勾选; · 点行勾选、改数量自动勾选、数字框 @click.stop 防冒泡反转勾选; · 服务端搜索(350ms 防抖)+ 分页 20/50/100/200; · uniqueKey 取 source_table_id(三表主键各自独立,必须带表名前缀)。 二、审批人字段不显示 原为 v-if="approvalVisible",仅库管代建或命中需审批物料时才渲染。 因全库仅 1 个物料标记 is_approval_required,该条件几乎从不成立。 现改为常驻,并用 el-form rules 声明 approver_id / remark 双必填; 打开弹窗即调用 checkScrapApproval 预检并展示提示文案。 三、顺带修复 库位列此前恒为空:三张库存表 to_dict 只输出 warehouse_loc,而模板绑定 warehouse_location。已在 loadStockList 中归一化。 SFC 编译与 vue-tsc 通过,无新增类型错误。
This commit is contained in:
@ -3,44 +3,180 @@
|
|||||||
<el-card shadow="always">
|
<el-card shadow="always">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
<span class="title">报废申请</span>
|
<span class="title">报废申请</span>
|
||||||
|
<span class="subtitle">(请添加需要报废的物品)</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-input
|
<el-button type="danger" :disabled="cart.length === 0" @click="clearCart">清空列表</el-button>
|
||||||
v-model="keyword"
|
<el-divider direction="vertical" />
|
||||||
placeholder="按名称/规格/SKU 搜索库存"
|
<el-button type="primary" :icon="Plus" @click="openManualSelect">手动添加库存</el-button>
|
||||||
style="width: 260px; margin-right: 10px;"
|
<el-button type="success" :icon="Select" :disabled="cart.length === 0" @click="openSubmitDialog">
|
||||||
clearable
|
提交报废申请
|
||||||
@keyup.enter="loadStock"
|
</el-button>
|
||||||
@clear="loadStock"
|
|
||||||
/>
|
|
||||||
<el-button type="primary" @click="loadStock">查询</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-table :data="stockList" border height="480" v-loading="loading" @selection-change="onSelect">
|
<el-alert
|
||||||
<el-table-column type="selection" width="50" />
|
v-if="cart.length === 0"
|
||||||
|
title="清单为空,请点击右上角【手动添加库存】选择要报废的物品"
|
||||||
|
type="info"
|
||||||
|
center
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- ★ 主表格 = 已选清单(购物车),不再承载搜索结果,避免搜索重置选中态 -->
|
||||||
|
<el-table
|
||||||
|
v-else
|
||||||
|
:data="cart"
|
||||||
|
border
|
||||||
|
row-key="uniqueKey"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="typeLabel" label="类型" width="90" align="center" />
|
<el-table-column prop="typeLabel" label="类型" width="90" align="center" />
|
||||||
<el-table-column prop="name" label="物料名称" min-width="170" show-overflow-tooltip />
|
<el-table-column prop="name" label="物料名称" min-width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="规格型号" min-width="150" show-overflow-tooltip>
|
<el-table-column label="规格型号" min-width="150" show-overflow-tooltip>
|
||||||
<template #default="{ row }">{{ row.standard || row.spec_model || '-' }}</template>
|
<template #default="{ row }">{{ row.spec_model || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="warehouse_location" label="库位" width="120" show-overflow-tooltip />
|
<el-table-column label="库位" width="120" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="color:#409EFF;">{{ row.location || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="批次/序列号" width="140" show-overflow-tooltip>
|
<el-table-column label="批次/序列号" width="140" show-overflow-tooltip>
|
||||||
<template #default="{ row }">{{ row.batch_number || row.serial_number || '-' }}</template>
|
<template #default="{ row }">{{ row.batch_number || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="可用库存" width="110" align="right">
|
<el-table-column label="可用库存" width="110" align="right">
|
||||||
<template #default="{ row }">{{ Number(row.available_quantity || 0) }}</template>
|
<template #default="{ row }">{{ row.available_qty }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="本次报废" width="150" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-number
|
||||||
|
v-model="row.scrap_qty"
|
||||||
|
:min="1"
|
||||||
|
:max="row.available_qty"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" link @click="removeCartRow(row)">移除</el-button>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top:12px; text-align:right;">
|
<div v-if="cart.length > 0" class="cart-summary">
|
||||||
<span style="margin-right:12px;color:#909399;">已选 {{ selected.length }} 条</span>
|
共 <span class="num">{{ cart.length }}</span> 种物品,
|
||||||
<el-button type="primary" :disabled="selected.length===0" @click="openSubmitDialog">提交报废申请</el-button>
|
合计报废 <span class="num">{{ totalScrapCount }}</span> 件
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
★ 选择库存物品(独立弹窗:搜索与已选解耦)
|
||||||
|
reserve-selection + row-key 保证跨搜索/翻页保留勾选
|
||||||
|
============================================================ -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="manualDialogVisible"
|
||||||
|
title="选择库存物品"
|
||||||
|
width="85%"
|
||||||
|
top="5vh"
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="请输入物料名称 / 规格型号 / SKU 进行搜索"
|
||||||
|
style="width: 320px"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
@input="filterStock"
|
||||||
|
/>
|
||||||
|
<span class="filter-tip">
|
||||||
|
提示:点击表格行可勾选;<span style="color:#F56C6C;font-weight:bold;">修改数量会自动勾选</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
ref="manualTableRef"
|
||||||
|
:data="stockList"
|
||||||
|
v-loading="stockLoading"
|
||||||
|
height="500"
|
||||||
|
border
|
||||||
|
row-key="uniqueKey"
|
||||||
|
@selection-change="handleStockSelection"
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
style="cursor: pointer"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
|
||||||
|
<el-table-column label="类型" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" type="info">{{ row.typeLabel }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="规格" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.standard || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sku" label="SKU" width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="库位" width="120" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.warehouse_location || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="批次/序列号" width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.batch_number || row.serial_number || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="available_quantity" label="可用库存" width="110" align="right" />
|
||||||
|
<el-table-column label="本次报废" width="150" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- ★ @click.stop:防止点击数字框冒泡触发 @row-click 反转勾选 -->
|
||||||
|
<el-input-number
|
||||||
|
v-model="row.scrap_qty"
|
||||||
|
:min="1"
|
||||||
|
:max="Number(row.available_quantity || 0)"
|
||||||
|
:precision="0"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="0"
|
||||||
|
@click.stop
|
||||||
|
@change="(val: any) => handleManualQuantityChange(val, row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
style="margin-top: 12px; justify-content: flex-end; display: flex;"
|
||||||
|
v-model:current-page="stockPage"
|
||||||
|
v-model:page-size="stockPageSize"
|
||||||
|
:total="stockTotal"
|
||||||
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
background
|
||||||
|
@size-change="handleStockSizeChange"
|
||||||
|
@current-change="handleStockPageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span style="float:left; line-height:32px; color:#909399;">
|
||||||
|
已勾选 <b style="color:#409EFF;">{{ tempSelection.length }}</b> 项
|
||||||
|
<span v-if="alreadyInCartCount > 0" style="margin-left:6px;">
|
||||||
|
(其中 {{ alreadyInCartCount }} 项已在清单中)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<el-button @click="manualDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmManualAdd">确认添加</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 提交报废申请 -->
|
<!-- 提交报废申请 -->
|
||||||
<el-dialog v-model="dialogVisible" title="提交报废申请" width="760px" destroy-on-close :close-on-click-modal="false">
|
<el-dialog v-model="dialogVisible" title="提交报废申请" width="760px" destroy-on-close :close-on-click-modal="false">
|
||||||
<el-table :data="cart" border size="small" max-height="300">
|
<el-table :data="cart" border size="small" max-height="300">
|
||||||
@ -62,15 +198,16 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-form label-width="100px" style="margin-top:14px;">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="margin-top:14px;">
|
||||||
<el-form-item v-if="approvalVisible" label="指定审批人">
|
<!-- ★ 业务规则:所有报废申请一律需审批,审批人恒为必填项 -->
|
||||||
|
<el-form-item label="指定审批人" prop="approver_id">
|
||||||
<div v-if="approvalText" style="color:#E6A23C; font-size:12px; line-height:1.4; margin-bottom:4px;">{{ approvalText }}</div>
|
<div v-if="approvalText" style="color:#E6A23C; font-size:12px; line-height:1.4; margin-bottom:4px;">{{ approvalText }}</div>
|
||||||
<el-select v-model="approverId" placeholder="请选择审批人" style="width:100%" filterable>
|
<el-select v-model="form.approver_id" placeholder="请选择审批人" style="width:100%" filterable>
|
||||||
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="报废原因" required>
|
<el-form-item label="报废原因" prop="remark">
|
||||||
<el-input v-model="remark" type="textarea" :rows="2" placeholder="请填写报废原因(必填)" maxlength="200" show-word-limit />
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请填写报废原因(必填)" maxlength="200" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@ -83,46 +220,188 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, reactive, computed, onBeforeUnmount } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTable, ElForm } from 'element-plus'
|
||||||
import { getStockList } from '@/api/inbound/stock'
|
import { Plus, Search, Select } from '@element-plus/icons-vue'
|
||||||
|
import { getScrapStockList } from '@/api/scrap'
|
||||||
import { getApproversList } from '@/api/auth'
|
import { getApproversList } from '@/api/auth'
|
||||||
import { submitScrapRequest, checkScrapApproval } from '@/api/scrap'
|
import { submitScrapRequest, checkScrapApproval } from '@/api/scrap'
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
// --- 已选清单(唯一数据源,主表格 / 选择弹窗 / 提交弹窗共享同一份对象) ---
|
||||||
const loading = ref(false)
|
const cart = ref<any[]>([])
|
||||||
const keyword = ref('')
|
|
||||||
const stockList = ref<any[]>([])
|
|
||||||
const selected = ref<any[]>([])
|
|
||||||
|
|
||||||
|
// --- 选择库存物品弹窗 ---
|
||||||
|
const manualDialogVisible = ref(false)
|
||||||
|
const manualTableRef = ref<InstanceType<typeof ElTable>>()
|
||||||
|
const stockList = ref<any[]>([]) // 弹窗内搜索结果(服务端分页)
|
||||||
|
const stockTotal = ref(0)
|
||||||
|
const stockPage = ref(1)
|
||||||
|
const stockPageSize = ref(20)
|
||||||
|
const stockLoading = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const tempSelection = ref<any[]>([]) // 弹窗内暂存勾选
|
||||||
|
let stockSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// --- 提交对话框 ---
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const cart = ref<any[]>([])
|
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||||
const remark = ref('')
|
|
||||||
const approverId = ref<number | null>(null)
|
|
||||||
const approvers = ref<any[]>([])
|
const approvers = ref<any[]>([])
|
||||||
const approvalVisible = ref(false)
|
|
||||||
const approvalText = ref('')
|
const approvalText = ref('')
|
||||||
|
// ★ 预检结果:提交前由 checkScrapApproval 写入,供文案提示使用
|
||||||
|
const isApprovalRequired = ref(false)
|
||||||
|
|
||||||
const isKeeper = () => (userStore.role || '').toUpperCase() === 'WAREHOUSE_MGR'
|
const form = reactive({
|
||||||
|
approver_id: null as number | null,
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
|
|
||||||
const loadStock = async () => {
|
// ★ 业务规则:所有报废申请一律需审批 → 审批人与报废原因恒为必填
|
||||||
loading.value = true
|
const rules = {
|
||||||
|
approver_id: [{ required: true, message: '请选择审批人', trigger: 'change' }],
|
||||||
|
remark: [{ required: true, message: '请填写报废原因', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 库存行的唯一键:三张库存表主键各自独立,必须带表名前缀防跨表碰撞。
|
||||||
|
// source_table 由后端稳定注入,取值恰好就是提交报废申请所需的 source_table。
|
||||||
|
// ============================================================
|
||||||
|
const stockKey = (row: any): string => `${row.source_table}_${row.id}`
|
||||||
|
|
||||||
|
// 库存行 → 购物车项
|
||||||
|
const toCartItem = (row: any) => ({
|
||||||
|
uniqueKey: stockKey(row),
|
||||||
|
source_table: row.source_table,
|
||||||
|
stock_id: row.id,
|
||||||
|
base_id: row.base_id,
|
||||||
|
name: row.name || row.material_name || '',
|
||||||
|
spec_model: row.standard || row.spec_model || '',
|
||||||
|
sku: row.sku || '',
|
||||||
|
location: row.warehouse_location || row.warehouse_loc || '',
|
||||||
|
batch_number: row.batch_number || row.serial_number || '',
|
||||||
|
available_qty: Number(row.available_quantity || 0),
|
||||||
|
scrap_qty: Number(row.scrap_qty) || 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 计算属性 ---
|
||||||
|
const totalScrapCount = computed(() =>
|
||||||
|
cart.value.reduce((sum, it) => sum + (Number(it.scrap_qty) || 0), 0)
|
||||||
|
)
|
||||||
|
const alreadyInCartCount = computed(() => {
|
||||||
|
const keys = new Set(cart.value.map(it => it.uniqueKey))
|
||||||
|
return tempSelection.value.filter(it => keys.has(stockKey(it))).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 加载库存(服务端搜索 + 分页)---
|
||||||
|
// 注意:不传 is_aggregated —— 报废要求 stock_id 精确到单条库存行,
|
||||||
|
// 聚合后 id 会退化为组内代表项,导致「申请扣 A 行、实际报废 B 行」。
|
||||||
|
const loadStockList = async () => {
|
||||||
|
stockLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockList({ page: 1, pageSize: 200, keyword: keyword.value.trim() })
|
const res: any = await getScrapStockList({
|
||||||
const items = res?.data?.items || res?.data?.list || res?.data || []
|
page: stockPage.value,
|
||||||
stockList.value = items
|
pageSize: stockPageSize.value,
|
||||||
|
keyword: searchKeyword.value.trim(),
|
||||||
|
})
|
||||||
|
// ★ uniqueKey 必须物化到行对象上:reserve-selection 依赖 row-key 真实存在
|
||||||
|
stockList.value = (res.data?.list || []).map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
uniqueKey: stockKey(item),
|
||||||
|
// 后端 to_dict 只输出 warehouse_loc,统一归一化,否则库位列恒为空
|
||||||
|
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || '',
|
||||||
|
}))
|
||||||
|
stockTotal.value = res.data?.total || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('加载库存失败')
|
ElMessage.error('加载库存失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
stockLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadStock()
|
|
||||||
|
|
||||||
const onSelect = (rows: any[]) => { selected.value = rows }
|
const openManualSelect = async () => {
|
||||||
|
manualDialogVisible.value = true
|
||||||
|
stockPage.value = 1
|
||||||
|
searchKeyword.value = ''
|
||||||
|
tempSelection.value = []
|
||||||
|
await loadStockList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索防抖 → 服务端过滤
|
||||||
|
const filterStock = () => {
|
||||||
|
if (stockSearchTimer) clearTimeout(stockSearchTimer)
|
||||||
|
stockSearchTimer = setTimeout(() => {
|
||||||
|
stockPage.value = 1
|
||||||
|
loadStockList()
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStockPageChange = (page: number) => {
|
||||||
|
stockPage.value = page
|
||||||
|
loadStockList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStockSizeChange = (size: number) => {
|
||||||
|
stockPageSize.value = size
|
||||||
|
stockPage.value = 1
|
||||||
|
loadStockList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStockSelection = (val: any[]) => { tempSelection.value = val }
|
||||||
|
|
||||||
|
// 点击行任意位置切换勾选(undefined = 取反)
|
||||||
|
const handleRowClick = (row: any) => {
|
||||||
|
manualTableRef.value?.toggleRowSelection(row, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗内数量变化联动勾选
|
||||||
|
const handleManualQuantityChange = (val: number | undefined, row: any) => {
|
||||||
|
if (val && val > 0) manualTableRef.value?.toggleRowSelection(row, true)
|
||||||
|
else manualTableRef.value?.toggleRowSelection(row, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认添加:按 uniqueKey 回查当前页行取权威 scrap_qty(tempSelection 可能是旧对象快照)
|
||||||
|
const confirmManualAdd = () => {
|
||||||
|
if (tempSelection.value.length === 0) return ElMessage.warning('请先勾选需要添加的物品')
|
||||||
|
|
||||||
|
const existing = new Set(cart.value.map(it => it.uniqueKey))
|
||||||
|
const toAdd: any[] = []
|
||||||
|
for (const it of tempSelection.value) {
|
||||||
|
const key = stockKey(it)
|
||||||
|
if (existing.has(key)) continue
|
||||||
|
// 优先用当前 stockList 里的同名行,保证拿到的是最新数量
|
||||||
|
const fresh = stockList.value.find(s => stockKey(s) === key) || it
|
||||||
|
toAdd.push(toCartItem(fresh))
|
||||||
|
existing.add(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.length === 0) {
|
||||||
|
manualDialogVisible.value = false
|
||||||
|
return ElMessage.warning('选中的物品已全部在清单中')
|
||||||
|
}
|
||||||
|
|
||||||
|
cart.value.push(...toAdd)
|
||||||
|
manualDialogVisible.value = false
|
||||||
|
tempSelection.value = []
|
||||||
|
ElMessage.success(`成功添加 ${toAdd.length} 项物品`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 清单维护 ---
|
||||||
|
const removeCartRow = (row: any) => {
|
||||||
|
const idx = cart.value.findIndex(it => it.uniqueKey === row.uniqueKey)
|
||||||
|
if (idx > -1) cart.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCart = () => {
|
||||||
|
ElMessageBox.confirm('确定要清空已选清单吗?', '提示', { type: 'warning' })
|
||||||
|
.then(() => { cart.value = [] })
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (stockSearchTimer) clearTimeout(stockSearchTimer)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 提交 ---
|
||||||
const loadApprovers = async () => {
|
const loadApprovers = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await getApproversList()
|
const res: any = await getApproversList()
|
||||||
@ -130,36 +409,7 @@ const loadApprovers = async () => {
|
|||||||
} catch (e) { approvers.value = [] }
|
} catch (e) { approvers.value = [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSubmitDialog = async () => {
|
const buildItems = () => cart.value.map(it => ({
|
||||||
// 构造购物车(保留 source_table + stock_id 精准定位 + 快照字段)
|
|
||||||
cart.value = selected.value.map((r: any) => ({
|
|
||||||
source_table: r.source_table,
|
|
||||||
stock_id: r.id,
|
|
||||||
base_id: r.base_id,
|
|
||||||
name: r.name || r.material_name || '',
|
|
||||||
spec_model: r.standard || r.spec_model || '',
|
|
||||||
sku: r.sku || '',
|
|
||||||
location: r.warehouse_location || '',
|
|
||||||
batch_number: r.batch_number || r.serial_number || '',
|
|
||||||
available_qty: Number(r.available_quantity || 0),
|
|
||||||
scrap_qty: 1,
|
|
||||||
}))
|
|
||||||
|
|
||||||
remark.value = ''
|
|
||||||
approverId.value = null
|
|
||||||
approvalText.value = ''
|
|
||||||
// 库管代建默认显示审批人
|
|
||||||
approvalVisible.value = isKeeper()
|
|
||||||
if (approvalVisible.value) approvalText.value = '库管代建报废申请需审批,请选择审批人'
|
|
||||||
loadApprovers()
|
|
||||||
dialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!remark.value.trim()) {
|
|
||||||
return ElMessage.warning('请填写报废原因')
|
|
||||||
}
|
|
||||||
const items = cart.value.map(it => ({
|
|
||||||
source_table: it.source_table,
|
source_table: it.source_table,
|
||||||
stock_id: it.stock_id,
|
stock_id: it.stock_id,
|
||||||
scrap_qty: Number(it.scrap_qty || 0),
|
scrap_qty: Number(it.scrap_qty || 0),
|
||||||
@ -170,45 +420,75 @@ const submit = async () => {
|
|||||||
batch_number: it.batch_number,
|
batch_number: it.batch_number,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
submitting.value = true
|
// ★ 预检:调用 checkScrapApproval,把结果写入 isApprovalRequired 并生成提示文案。
|
||||||
try {
|
// 所有报废申请一律需审批,此处主要用于向用户展示「哪些物料触发了审批」。
|
||||||
// ★ 预检:库管代建一律需审批;否则命中需审批物料才需审批
|
const runApprovalPrecheck = async (items: any[]) => {
|
||||||
const isK = isKeeper()
|
|
||||||
let needApproval = isK
|
|
||||||
let flagged: any[] = []
|
|
||||||
try {
|
try {
|
||||||
const chk: any = await checkScrapApproval({ items })
|
const chk: any = await checkScrapApproval({ items })
|
||||||
needApproval = needApproval || !!chk?.data?.need_approval
|
isApprovalRequired.value = true // 业务上恒需审批
|
||||||
flagged = chk?.data?.materials || []
|
const flagged: any[] = chk?.data?.materials || []
|
||||||
} catch (e) { flagged = [] }
|
approvalText.value = flagged.length > 0
|
||||||
approvalVisible.value = needApproval
|
? `以下物料需审批报废:${flagged.map((m: any) => `${m.name}(${m.spec_model || '-'})`).join(';')}`
|
||||||
approvalText.value = isK && flagged.length === 0
|
: '所有报废申请均需审批,请选择审批人'
|
||||||
? '库管代建报废申请需审批,请选择审批人'
|
} catch (e) {
|
||||||
: flagged.map((m: any) => `${m.name}(${m.spec_model || '-'})`).join(';')
|
isApprovalRequired.value = true
|
||||||
|
approvalText.value = '所有报废申请均需审批,请选择审批人'
|
||||||
if (needApproval && !approverId.value) {
|
|
||||||
return ElMessage.warning(`${approvalText.value || '该申请需审批'}。请先选择审批人`)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSubmitDialog = async () => {
|
||||||
|
if (cart.value.length === 0) return ElMessage.warning('请先添加要报废的物品')
|
||||||
|
|
||||||
|
form.approver_id = null
|
||||||
|
form.remark = ''
|
||||||
|
isApprovalRequired.value = true
|
||||||
|
approvalText.value = '所有报废申请均需审批,请选择审批人'
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
|
||||||
|
await loadApprovers()
|
||||||
|
// 打开弹窗即预检,让用户在填写前就看到需审批提示
|
||||||
|
await runApprovalPrecheck(buildItems())
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
// ★ 表单校验:审批人与报废原因均为必填
|
||||||
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const items = buildItems()
|
||||||
|
await runApprovalPrecheck(items)
|
||||||
|
|
||||||
await submitScrapRequest({
|
await submitScrapRequest({
|
||||||
items,
|
items,
|
||||||
remark: remark.value.trim(),
|
remark: form.remark.trim(),
|
||||||
approver_id: needApproval ? approverId.value : null,
|
approver_id: form.approver_id,
|
||||||
})
|
})
|
||||||
ElMessage.success('报废申请已提交(含需审批物料时将进入审批,否则直接待库管执行)')
|
ElMessage.success('报废申请已提交,待审批人审批')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
selected.value = []
|
cart.value = []
|
||||||
loadStock()
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 拦截器已按后端 data.msg 弹业务错误,HTTP 错误不重复
|
// 拦截器已按后端 data.msg 弹业务错误,HTTP 错误不重复
|
||||||
if (!err?.response) ElMessage.error('网络异常,请重试')
|
if (!err?.response) ElMessage.error('网络异常,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.header-left { display: flex; align-items: baseline; gap: 8px; }
|
||||||
.title { font-size: 18px; font-weight: bold; }
|
.title { font-size: 18px; font-weight: bold; }
|
||||||
|
.subtitle { font-size: 12px; color: #909399; }
|
||||||
|
|
||||||
|
.filter-container { display: flex; align-items: center; gap: 15px; margin-bottom: 12px; }
|
||||||
|
.filter-tip { color: #909399; font-size: 12px; }
|
||||||
|
|
||||||
|
.cart-summary { margin-top: 15px; text-align: right; color: #606266; font-size: 14px; }
|
||||||
|
.cart-summary .num { color: #F56C6C; font-weight: bold; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user