1447 lines
47 KiB
Vue
1447 lines
47 KiB
Vue
<!-- 市场物料库 -->
|
||
<!-- eslint-disable vue/no-v-model-argument -->
|
||
<template>
|
||
<div class="page-container">
|
||
<header class="page-head">
|
||
<h1>物料库</h1>
|
||
<p class="page-desc">管理市场部门的物料信息,支持入库、出库操作</p>
|
||
</header>
|
||
|
||
<section class="panel">
|
||
<div class="panel-top">
|
||
<h2>物料列表</h2>
|
||
</div>
|
||
|
||
<div class="filter-block">
|
||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||
<div class="filter-grid">
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="searchForm.keyword" placeholder="物料编码/名称" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="物料分类">
|
||
<el-select v-model="searchForm.category" placeholder="全部" clearable>
|
||
<el-option
|
||
v-for="opt in categoryOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||
<el-option label="正常" value="1" />
|
||
<el-option label="停用" value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="filter-actions-row">
|
||
<span class="filter-hint">按编码、名称模糊查询,可按分类、状态筛选。</span>
|
||
<div class="filter-actions-end">
|
||
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
||
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>
|
||
新增物料
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="handleBatchStockIn">
|
||
<el-icon><Upload /></el-icon>
|
||
批量入库
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="handleBatchStockOut">
|
||
<el-icon><Download /></el-icon>
|
||
批量出库
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<el-table
|
||
ref="tableRef"
|
||
class="list-table"
|
||
:data="tableData"
|
||
v-loading="loading"
|
||
stripe
|
||
row-key="id"
|
||
@selection-change="handleSelectionChange"
|
||
@row-dblclick="handleView"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="68"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||
/>
|
||
<el-table-column
|
||
prop="materialCode"
|
||
label="物料编码"
|
||
min-width="130"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="materialName"
|
||
label="物料名称"
|
||
min-width="150"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column label="物料分类" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getCategoryTagType(row.category)" size="small">
|
||
{{ getCategoryLabel(row.category) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="specModel"
|
||
label="规格型号"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="unit"
|
||
label="单位"
|
||
min-width="80"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="stockQty"
|
||
label="当前库存"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||
{{ row.status === '1' ? '正常' : '停用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="remark"
|
||
label="备注"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-button class="btn-op-item" @click="handleStockIn(row)"><el-icon><Upload /></el-icon>入库</el-button>
|
||
<el-button class="btn-op-item" @click="handleStockOut(row)"><el-icon><Download /></el-icon>出库</el-button>
|
||
<el-button class="btn-op-item" @click="handleView(row)"
|
||
><el-icon><View /></el-icon>查看</el-button
|
||
>
|
||
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
|
||
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pagination list-pager-bar">
|
||
<el-pagination
|
||
:current-page="pagination.page"
|
||
:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑弹窗 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="560px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-position="top"
|
||
class="material-dialog-form"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="物料编码" prop="materialCode">
|
||
<el-input v-model="formData.materialCode" placeholder="请输入物料编码" />
|
||
</el-form-item>
|
||
<el-form-item label="物料名称" prop="materialName">
|
||
<el-input v-model="formData.materialName" placeholder="请输入物料名称" />
|
||
</el-form-item>
|
||
<el-form-item label="物料分类" prop="category">
|
||
<el-select v-model="formData.category" placeholder="请选择物料分类" clearable>
|
||
<el-option
|
||
v-for="opt in categoryOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="规格型号" prop="specModel">
|
||
<el-input v-model="formData.specModel" placeholder="请输入规格型号" />
|
||
</el-form-item>
|
||
<el-form-item label="单位" prop="unit">
|
||
<el-select
|
||
v-model="formData.unit"
|
||
placeholder="请选择单位"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:loading="unitOptionsLoading"
|
||
:remote-method="searchUnitOptions"
|
||
>
|
||
<el-option
|
||
v-for="item in unitOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态" prop="status">
|
||
<el-radio-group v-model="formData.status">
|
||
<el-radio value="1">正常</el-radio>
|
||
<el-radio value="0">停用</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="form-col-span-2">
|
||
<el-input
|
||
v-model="formData.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 入库弹窗 -->
|
||
<el-dialog
|
||
v-model="stockInVisible"
|
||
title="物料入库"
|
||
width="480px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="stockInFormRef"
|
||
:model="stockInForm"
|
||
:rules="stockInRules"
|
||
label-position="top"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<el-form-item label="入库数量" prop="qty">
|
||
<el-input-number
|
||
v-model="stockInForm.qty"
|
||
:min="0.01"
|
||
:precision="2"
|
||
:step="1"
|
||
placeholder="请输入入库数量"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="入库时间" prop="stockInTime">
|
||
<el-date-picker
|
||
v-model="stockInForm.stockInTime"
|
||
type="datetime"
|
||
placeholder="选择入库时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="stockInForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="stockInVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
@click="handleStockInSubmit"
|
||
:loading="stockInLoading"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确认入库
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 出库弹窗 -->
|
||
<el-dialog
|
||
v-model="stockOutVisible"
|
||
title="物料出库"
|
||
width="480px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="stockOutFormRef"
|
||
:model="stockOutForm"
|
||
:rules="stockOutRules"
|
||
label-position="top"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<el-form-item label="出库数量" prop="qty">
|
||
<el-input-number
|
||
v-model="stockOutForm.qty"
|
||
:min="0.01"
|
||
:precision="2"
|
||
:step="1"
|
||
placeholder="请输入出库数量"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="出库时间" prop="stockOutTime">
|
||
<el-date-picker
|
||
v-model="stockOutForm.stockOutTime"
|
||
type="datetime"
|
||
placeholder="选择出库时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="领用人" prop="receiver">
|
||
<el-select
|
||
v-model="stockOutForm.receiver"
|
||
placeholder="请选择领用人"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:loading="receiverUserLoading"
|
||
:remote-method="searchReceiverUsers"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="u in receiverUserOptions"
|
||
:key="u.id"
|
||
:label="formatReceiverUserLabel(u)"
|
||
:value="(u.nickName || '').trim()"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="用途">
|
||
<el-input
|
||
v-model="stockOutForm.purpose"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="stockOutForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="stockOutVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
@click="handleStockOutSubmit"
|
||
:loading="stockOutLoading"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确认出库
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量入库弹窗 -->
|
||
<el-dialog
|
||
v-model="batchStockInVisible"
|
||
title="批量入库"
|
||
width="920px"
|
||
class="dialog-form-layout material-batch-stock-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="batchStockInFormRef"
|
||
:model="batchStockInForm"
|
||
:rules="batchStockInRules"
|
||
label-position="top"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2 batch-stock-move-head">
|
||
<el-form-item label="入库时间" prop="stockInTime">
|
||
<el-date-picker
|
||
v-model="batchStockInForm.stockInTime"
|
||
type="datetime"
|
||
placeholder="选择入库时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="batchStockInForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="可选,未填写行备注时将作为默认备注"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="batch-stock-move-lines">
|
||
<div class="batch-stock-move-lines-head">
|
||
<div class="batch-stock-move-lines-head-left">
|
||
<span class="batch-stock-move-lines-title">入库明细</span>
|
||
<span class="batch-stock-move-lines-hint">
|
||
共 {{ batchStockInLines.length }} 项,请搜索添加物料并填写入库数量
|
||
</span>
|
||
</div>
|
||
<el-select
|
||
v-model="batchInMaterialPickId"
|
||
class="batch-stock-move-material-select"
|
||
placeholder="搜索物料编码/名称并添加"
|
||
filterable
|
||
remote
|
||
clearable
|
||
reserve-keyword
|
||
:loading="batchInMaterialPickLoading"
|
||
:remote-method="searchBatchInMaterials"
|
||
@change="onBatchInMaterialPick"
|
||
>
|
||
<el-option
|
||
v-for="m in batchInMaterialOptions"
|
||
:key="m.id"
|
||
:label="formatBatchMaterialOptionLabel(m)"
|
||
:value="m.id"
|
||
:disabled="isBatchInMaterialOptionDisabled(m)"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<el-table
|
||
v-if="batchStockInLines.length"
|
||
:data="batchStockInLines"
|
||
stripe
|
||
class="list-table inner-table"
|
||
max-height="320"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="56"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(index) => index + 1"
|
||
/>
|
||
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
|
||
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
|
||
<el-table-column prop="unit" label="单位" width="80" align="center" header-align="center" />
|
||
<el-table-column label="当前库存" width="100" align="center" header-align="center">
|
||
<template #default="{ row }">{{ row.stockQty }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="入库数量" min-width="130" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.qty"
|
||
:min="0.01"
|
||
:precision="2"
|
||
:step="1"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="160" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="batchStockInLines.splice($index, 1)">
|
||
移除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty
|
||
v-else
|
||
description="请通过上方搜索框添加要入库的物料"
|
||
:image-size="56"
|
||
/>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="batchStockInVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="batchStockInLoading"
|
||
@click="handleBatchStockInSubmit"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确认入库
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量出库弹窗 -->
|
||
<el-dialog
|
||
v-model="batchStockOutVisible"
|
||
title="批量出库"
|
||
width="920px"
|
||
class="dialog-form-layout material-batch-stock-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="batchStockOutFormRef"
|
||
:model="batchStockOutForm"
|
||
:rules="batchStockOutRules"
|
||
label-position="top"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2 batch-stock-move-head">
|
||
<el-form-item label="出库时间" prop="stockOutTime">
|
||
<el-date-picker
|
||
v-model="batchStockOutForm.stockOutTime"
|
||
type="datetime"
|
||
placeholder="选择出库时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="领用人" prop="receiver">
|
||
<el-select
|
||
v-model="batchStockOutForm.receiver"
|
||
placeholder="请选择领用人"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:loading="receiverUserLoading"
|
||
:remote-method="searchReceiverUsers"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="u in receiverUserOptions"
|
||
:key="u.id"
|
||
:label="formatReceiverUserLabel(u)"
|
||
:value="(u.nickName || '').trim()"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="用途">
|
||
<el-input
|
||
v-model="batchStockOutForm.purpose"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="batchStockOutForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="batch-stock-move-lines">
|
||
<div class="batch-stock-move-lines-head">
|
||
<div class="batch-stock-move-lines-head-left">
|
||
<span class="batch-stock-move-lines-title">出库明细</span>
|
||
<span class="batch-stock-move-lines-hint">
|
||
共 {{ batchStockOutLines.length }} 项,请搜索添加物料并填写出库数量
|
||
</span>
|
||
</div>
|
||
<el-select
|
||
v-model="batchOutMaterialPickId"
|
||
class="batch-stock-move-material-select"
|
||
placeholder="搜索物料编码/名称并添加"
|
||
filterable
|
||
remote
|
||
clearable
|
||
reserve-keyword
|
||
:loading="batchOutMaterialPickLoading"
|
||
:remote-method="searchBatchOutMaterials"
|
||
@change="onBatchOutMaterialPick"
|
||
>
|
||
<el-option
|
||
v-for="m in batchMaterialOptions"
|
||
:key="m.id"
|
||
:label="formatBatchMaterialOptionLabel(m)"
|
||
:value="m.id"
|
||
:disabled="isBatchOutMaterialOptionDisabled(m)"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<el-table
|
||
v-if="batchStockOutLines.length"
|
||
:data="batchStockOutLines"
|
||
stripe
|
||
class="list-table inner-table"
|
||
max-height="320"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="56"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(index) => index + 1"
|
||
/>
|
||
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
|
||
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
|
||
<el-table-column prop="unit" label="单位" width="80" align="center" header-align="center" />
|
||
<el-table-column label="当前库存" width="100" align="center" header-align="center">
|
||
<template #default="{ row }">{{ row.stockQty }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="出库数量" min-width="130" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.qty"
|
||
:min="0.01"
|
||
:max="row.stockQty"
|
||
:precision="2"
|
||
:step="1"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="160" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="batchStockOutLines.splice($index, 1)">
|
||
移除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty
|
||
v-else
|
||
description="请通过上方搜索框添加要出库的物料"
|
||
:image-size="56"
|
||
/>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="batchStockOutVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="batchStockOutLoading"
|
||
@click="handleBatchStockOutSubmit"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确认出库
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 查看弹窗 -->
|
||
<SystemViewDialog
|
||
v-model:visible="viewVisible"
|
||
:title="viewRow?.materialName || '物料详情'"
|
||
:icon="Box"
|
||
:meta="viewRow?.createTime || ''"
|
||
:sections="viewSections"
|
||
/>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, computed } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Close, Check, View, Box, RefreshLeft, Search, Upload, Download, Edit, Delete } from '@element-plus/icons-vue'
|
||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||
import { fetchDictDataOptions, fetchItemUnitList, fetchUserList } from '@/api/admin'
|
||
import {
|
||
fetchMaterialList,
|
||
fetchCreateMaterial,
|
||
fetchUpdateMaterial,
|
||
fetchDeleteMaterial,
|
||
fetchStockInMaterial,
|
||
fetchStockOutMaterial,
|
||
fetchBatchStockInMaterial,
|
||
fetchBatchStockOutMaterial
|
||
} from '@/api/material'
|
||
import type { Material } from '@/api/material'
|
||
|
||
defineOptions({ name: 'CellStorageMaterial' })
|
||
|
||
const searchForm = reactive({ keyword: '', category: '', status: '' })
|
||
const tableRef = ref()
|
||
const tableData = ref<Material[]>([])
|
||
const selectedRows = ref<Material[]>([])
|
||
const loading = ref(false)
|
||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||
|
||
// 字典选项
|
||
const categoryOptions = ref<{ label: string; value: string }[]>([])
|
||
|
||
// 物品单位(来自物品单位管理)
|
||
const unitOptions = ref<{ label: string; value: string }[]>([])
|
||
const unitOptionsLoading = ref(false)
|
||
|
||
function mapUnitOption(u: { unitName?: string; unitCode?: string }) {
|
||
const name = (u.unitName || '').trim()
|
||
const code = (u.unitCode || '').trim()
|
||
return {
|
||
label: code ? `${name}(${code})` : name,
|
||
value: name
|
||
}
|
||
}
|
||
|
||
function ensureUnitOption(unitName: string) {
|
||
const v = unitName?.trim()
|
||
if (!v) return
|
||
if (!unitOptions.value.some((o) => o.value === v)) {
|
||
unitOptions.value = [{ label: v, value: v }, ...unitOptions.value]
|
||
}
|
||
}
|
||
|
||
async function searchUnitOptions(query: string) {
|
||
unitOptionsLoading.value = true
|
||
try {
|
||
const res = await fetchItemUnitList({
|
||
page: 1,
|
||
pageSize: 100,
|
||
keyword: query?.trim() || undefined
|
||
})
|
||
unitOptions.value = (res.list || []).map(mapUnitOption).filter((o) => o.value)
|
||
} catch {
|
||
unitOptions.value = []
|
||
} finally {
|
||
unitOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 新增/编辑
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('新增物料')
|
||
const isEdit = ref(false)
|
||
const submitLoading = ref(false)
|
||
const formRef = ref()
|
||
const editingId = ref<string | null>(null)
|
||
|
||
const formData = reactive({
|
||
materialCode: '',
|
||
materialName: '',
|
||
category: '',
|
||
specModel: '',
|
||
unit: '',
|
||
status: '1',
|
||
remark: ''
|
||
})
|
||
|
||
const formRules = {
|
||
materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
|
||
materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }]
|
||
}
|
||
|
||
// 入库
|
||
const stockInVisible = ref(false)
|
||
const stockInLoading = ref(false)
|
||
const stockInFormRef = ref()
|
||
const stockInMaterialId = ref<string | null>(null)
|
||
const stockInForm = reactive({ qty: 1, stockInTime: '', remark: '' })
|
||
|
||
const stockInRules = {
|
||
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }],
|
||
stockInTime: [{ required: true, message: '请选择入库时间', trigger: 'change' }]
|
||
}
|
||
|
||
// 出库
|
||
const stockOutVisible = ref(false)
|
||
const stockOutLoading = ref(false)
|
||
const stockOutFormRef = ref()
|
||
const stockOutMaterialId = ref<string | null>(null)
|
||
const stockOutStockQty = ref(0)
|
||
const stockOutForm = reactive({ qty: 1, stockOutTime: '', receiver: '', purpose: '', remark: '' })
|
||
const receiverUserOptions = ref<Api.User.UserItem[]>([])
|
||
const receiverUserLoading = ref(false)
|
||
|
||
function formatReceiverUserLabel(u: Api.User.UserItem) {
|
||
const name = (u.nickName || '').trim() || '-'
|
||
const no = (u.userNo || '').trim()
|
||
const phone = (u.phoneNo || '').trim()
|
||
const parts = [name]
|
||
if (no) parts.push(no)
|
||
if (phone) parts.push(phone)
|
||
return parts.join(' · ')
|
||
}
|
||
|
||
async function searchReceiverUsers(query: string) {
|
||
receiverUserLoading.value = true
|
||
try {
|
||
const res = await fetchUserList({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: query?.trim() || undefined,
|
||
status: '1'
|
||
})
|
||
receiverUserOptions.value = res.list || []
|
||
} catch {
|
||
receiverUserOptions.value = []
|
||
} finally {
|
||
receiverUserLoading.value = false
|
||
}
|
||
}
|
||
|
||
const stockOutRules = {
|
||
qty: [{ required: true, message: '请输入出库数量', trigger: 'blur' }],
|
||
stockOutTime: [{ required: true, message: '请选择出库时间', trigger: 'change' }],
|
||
receiver: [{ required: true, message: '请选择领用人', trigger: 'change' }]
|
||
}
|
||
|
||
// 批量入库
|
||
type BatchStockInLine = {
|
||
materialId: string
|
||
materialCode: string
|
||
materialName: string
|
||
unit: string
|
||
stockQty: number
|
||
qty: number
|
||
remark: string
|
||
}
|
||
|
||
const batchStockInVisible = ref(false)
|
||
const batchStockInLoading = ref(false)
|
||
const batchStockInFormRef = ref()
|
||
const batchStockInLines = ref<BatchStockInLine[]>([])
|
||
const batchInMaterialPickId = ref('')
|
||
const batchInMaterialOptions = ref<Material[]>([])
|
||
const batchInMaterialPickLoading = ref(false)
|
||
const batchStockInForm = reactive({
|
||
stockInTime: '',
|
||
remark: ''
|
||
})
|
||
|
||
const batchStockInRules = {
|
||
stockInTime: [{ required: true, message: '请选择入库时间', trigger: 'change' }]
|
||
}
|
||
|
||
// 批量出库
|
||
type BatchStockOutLine = {
|
||
materialId: string
|
||
materialCode: string
|
||
materialName: string
|
||
unit: string
|
||
stockQty: number
|
||
qty: number
|
||
remark: string
|
||
}
|
||
|
||
const batchStockOutVisible = ref(false)
|
||
const batchStockOutLoading = ref(false)
|
||
const batchStockOutFormRef = ref()
|
||
const batchStockOutLines = ref<BatchStockOutLine[]>([])
|
||
const batchOutMaterialPickId = ref('')
|
||
const batchOutMaterialOptions = ref<Material[]>([])
|
||
const batchOutMaterialPickLoading = ref(false)
|
||
const batchStockOutForm = reactive({
|
||
stockOutTime: '',
|
||
receiver: '',
|
||
purpose: '',
|
||
remark: ''
|
||
})
|
||
|
||
const batchStockOutRules = {
|
||
stockOutTime: [{ required: true, message: '请选择出库时间', trigger: 'change' }],
|
||
receiver: [{ required: true, message: '请选择领用人', trigger: 'change' }]
|
||
}
|
||
|
||
function formatNowDatetime() {
|
||
const now = new Date()
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
||
}
|
||
|
||
// 查看
|
||
const viewVisible = ref(false)
|
||
const viewRow = ref<Material | null>(null)
|
||
|
||
const getCategoryLabel = (code: string) => {
|
||
if (!code) return '-'
|
||
const opt = categoryOptions.value.find((o) => o.value === code)
|
||
return opt ? opt.label : code
|
||
}
|
||
|
||
const getCategoryTagType = (code: string) => {
|
||
if (code === 'xccyw') return 'primary'
|
||
if (code === 'ccyw') return 'success'
|
||
return 'info'
|
||
}
|
||
|
||
const handleSelectionChange = (rows: Material[]) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
const viewSections = computed(() => {
|
||
if (!viewRow.value) return []
|
||
const row = viewRow.value
|
||
return [
|
||
{
|
||
title: '基本信息',
|
||
fields: [
|
||
{ label: '物料编码', value: row.materialCode },
|
||
{ label: '物料名称', value: row.materialName },
|
||
{
|
||
label: '物料分类',
|
||
value: getCategoryLabel(row.category),
|
||
tagType: getCategoryTagType(row.category) as 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
|
||
},
|
||
{ label: '规格型号', value: row.specModel },
|
||
{ label: '单位', value: row.unit },
|
||
{
|
||
label: '状态',
|
||
value: row.status === '1' ? '正常' : '停用',
|
||
tagType: (row.status === '1' ? 'success' : 'info') as 'success' | 'info'
|
||
},
|
||
{ label: '当前库存', value: String(row.stockQty ?? 0) }
|
||
]
|
||
},
|
||
{
|
||
title: '其他信息',
|
||
fields: [
|
||
{ label: '备注', value: row.remark, span: true },
|
||
{ label: '创建时间', value: row.createTime },
|
||
{ label: '更新时间', value: row.updateTime }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
|
||
async function loadDictOptions() {
|
||
try {
|
||
const res = await fetchDictDataOptions('wllb')
|
||
categoryOptions.value = res || []
|
||
} catch {
|
||
categoryOptions.value = []
|
||
}
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchMaterialList({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
keyword: searchForm.keyword || undefined,
|
||
category: searchForm.category || undefined,
|
||
status: searchForm.status || undefined
|
||
})
|
||
tableData.value = res.list || []
|
||
pagination.total = res.total ?? 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function handleReset() {
|
||
searchForm.keyword = ''
|
||
searchForm.category = ''
|
||
searchForm.status = ''
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function resetForm() {
|
||
formData.materialCode = ''
|
||
formData.materialName = ''
|
||
formData.category = ''
|
||
formData.specModel = ''
|
||
formData.unit = ''
|
||
formData.status = '1'
|
||
formData.remark = ''
|
||
editingId.value = null
|
||
}
|
||
|
||
function handleAdd() {
|
||
isEdit.value = false
|
||
dialogTitle.value = '新增物料'
|
||
resetForm()
|
||
dialogVisible.value = true
|
||
void searchUnitOptions('')
|
||
formRef.value?.clearValidate?.()
|
||
}
|
||
|
||
function handleEdit(row: Material) {
|
||
isEdit.value = true
|
||
dialogTitle.value = '编辑物料'
|
||
editingId.value = row.id
|
||
formData.materialCode = row.materialCode || ''
|
||
formData.materialName = row.materialName || ''
|
||
formData.category = row.category || ''
|
||
formData.specModel = row.specModel || ''
|
||
formData.unit = row.unit || ''
|
||
formData.status = row.status || '1'
|
||
formData.remark = row.remark || ''
|
||
ensureUnitOption(row.unit || '')
|
||
dialogVisible.value = true
|
||
void searchUnitOptions('')
|
||
formRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
await formRef.value?.validate?.()
|
||
submitLoading.value = true
|
||
try {
|
||
const payload = {
|
||
materialCode: formData.materialCode.trim(),
|
||
materialName: formData.materialName.trim(),
|
||
category: formData.category || undefined,
|
||
specModel: formData.specModel?.trim() || undefined,
|
||
unit: formData.unit?.trim() || undefined,
|
||
status: formData.status,
|
||
remark: formData.remark?.trim() || undefined
|
||
}
|
||
if (isEdit.value && editingId.value) {
|
||
await fetchUpdateMaterial(editingId.value, payload)
|
||
} else {
|
||
await fetchCreateMaterial(payload)
|
||
}
|
||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||
dialogVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleView(row: Material) {
|
||
viewRow.value = row
|
||
viewVisible.value = true
|
||
}
|
||
|
||
async function handleDelete(row: Material) {
|
||
await ElMessageBox.confirm('确定要删除该物料吗?', '提示', { type: 'warning' })
|
||
await fetchDeleteMaterial(row.id)
|
||
ElMessage.success('删除成功')
|
||
await loadData()
|
||
}
|
||
|
||
function handleStockIn(row: Material) {
|
||
stockInMaterialId.value = row.id
|
||
stockInForm.qty = 1
|
||
stockInForm.stockInTime = formatNowDatetime()
|
||
stockInForm.remark = ''
|
||
stockInVisible.value = true
|
||
stockInFormRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleStockInSubmit() {
|
||
await stockInFormRef.value?.validate?.()
|
||
stockInLoading.value = true
|
||
try {
|
||
await fetchStockInMaterial(stockInMaterialId.value!, {
|
||
qty: stockInForm.qty,
|
||
stockInTime: stockInForm.stockInTime || undefined,
|
||
remark: stockInForm.remark?.trim() || undefined
|
||
})
|
||
ElMessage.success('入库成功')
|
||
stockInVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
stockInLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleStockOut(row: Material) {
|
||
stockOutMaterialId.value = row.id
|
||
stockOutStockQty.value = row.stockQty ?? 0
|
||
stockOutForm.qty = 1
|
||
stockOutForm.stockOutTime = formatNowDatetime()
|
||
stockOutForm.receiver = ''
|
||
stockOutForm.purpose = ''
|
||
stockOutForm.remark = ''
|
||
stockOutVisible.value = true
|
||
void searchReceiverUsers('')
|
||
stockOutFormRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleStockOutSubmit() {
|
||
await stockOutFormRef.value?.validate?.()
|
||
if (stockOutForm.qty > stockOutStockQty.value) {
|
||
ElMessage.warning(`出库数量不能超过当前库存 (${stockOutStockQty.value})`)
|
||
return
|
||
}
|
||
stockOutLoading.value = true
|
||
try {
|
||
await fetchStockOutMaterial(stockOutMaterialId.value!, {
|
||
qty: stockOutForm.qty,
|
||
stockOutTime: stockOutForm.stockOutTime || undefined,
|
||
receiver: stockOutForm.receiver?.trim() || undefined,
|
||
purpose: stockOutForm.purpose?.trim() || undefined,
|
||
remark: stockOutForm.remark?.trim() || undefined
|
||
})
|
||
ElMessage.success('出库成功')
|
||
stockOutVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
stockOutLoading.value = false
|
||
}
|
||
}
|
||
|
||
function formatBatchMaterialOptionLabel(m: Material) {
|
||
const name = m.materialName || '-'
|
||
const code = m.materialCode || ''
|
||
const stock = m.stockQty ?? 0
|
||
return code ? `${name}(${code})· 库存 ${stock}` : `${name} · 库存 ${stock}`
|
||
}
|
||
|
||
function isBatchOutMaterialOptionDisabled(m: Material) {
|
||
if (m.status !== '1') return true
|
||
if ((m.stockQty ?? 0) <= 0) return true
|
||
return batchStockOutLines.value.some((l) => l.materialId === m.id)
|
||
}
|
||
|
||
function isBatchInMaterialOptionDisabled(m: Material) {
|
||
if (m.status !== '1') return true
|
||
return batchStockInLines.value.some((l) => l.materialId === m.id)
|
||
}
|
||
|
||
async function searchBatchOutMaterials(query: string) {
|
||
batchOutMaterialPickLoading.value = true
|
||
try {
|
||
const res = await fetchMaterialList({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: query?.trim() || undefined,
|
||
status: '1'
|
||
})
|
||
batchOutMaterialOptions.value = res.list || []
|
||
} catch {
|
||
batchOutMaterialOptions.value = []
|
||
} finally {
|
||
batchOutMaterialPickLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function searchBatchInMaterials(query: string) {
|
||
batchInMaterialPickLoading.value = true
|
||
try {
|
||
const res = await fetchMaterialList({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: query?.trim() || undefined,
|
||
status: '1'
|
||
})
|
||
batchInMaterialOptions.value = res.list || []
|
||
} catch {
|
||
batchInMaterialOptions.value = []
|
||
} finally {
|
||
batchInMaterialPickLoading.value = false
|
||
}
|
||
}
|
||
|
||
function appendBatchStockOutLine(m: Material) {
|
||
if (batchStockOutLines.value.some((l) => l.materialId === m.id)) {
|
||
ElMessage.warning('该物料已在明细中')
|
||
return false
|
||
}
|
||
if (m.status !== '1') {
|
||
ElMessage.warning('该物料已停用,无法出库')
|
||
return false
|
||
}
|
||
if ((m.stockQty ?? 0) <= 0) {
|
||
ElMessage.warning('该物料库存为 0,无法出库')
|
||
return false
|
||
}
|
||
batchStockOutLines.value.push({
|
||
materialId: m.id,
|
||
materialCode: m.materialCode || '',
|
||
materialName: m.materialName || '',
|
||
unit: m.unit || '',
|
||
stockQty: m.stockQty ?? 0,
|
||
qty: 1,
|
||
remark: ''
|
||
})
|
||
return true
|
||
}
|
||
|
||
function onBatchOutMaterialPick(materialId: string | null | undefined) {
|
||
if (!materialId) return
|
||
const m = batchOutMaterialOptions.value.find((x) => x.id === materialId)
|
||
batchOutMaterialPickId.value = ''
|
||
if (!m) return
|
||
appendBatchStockOutLine(m)
|
||
}
|
||
|
||
function appendBatchStockInLine(m: Material) {
|
||
if (batchStockInLines.value.some((l) => l.materialId === m.id)) {
|
||
ElMessage.warning('该物料已在明细中')
|
||
return false
|
||
}
|
||
if (m.status !== '1') {
|
||
ElMessage.warning('该物料已停用,无法入库')
|
||
return false
|
||
}
|
||
batchStockInLines.value.push({
|
||
materialId: m.id,
|
||
materialCode: m.materialCode || '',
|
||
materialName: m.materialName || '',
|
||
unit: m.unit || '',
|
||
stockQty: m.stockQty ?? 0,
|
||
qty: 1,
|
||
remark: ''
|
||
})
|
||
return true
|
||
}
|
||
|
||
function onBatchInMaterialPick(materialId: string | null | undefined) {
|
||
if (!materialId) return
|
||
const m = batchInMaterialOptions.value.find((x) => x.id === materialId)
|
||
batchInMaterialPickId.value = ''
|
||
if (!m) return
|
||
appendBatchStockInLine(m)
|
||
}
|
||
|
||
function handleBatchStockIn() {
|
||
batchStockInLines.value = []
|
||
batchInMaterialPickId.value = ''
|
||
batchInMaterialOptions.value = []
|
||
batchStockInForm.stockInTime = formatNowDatetime()
|
||
batchStockInForm.remark = ''
|
||
batchStockInVisible.value = true
|
||
void searchBatchInMaterials('')
|
||
batchStockInFormRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleBatchStockInSubmit() {
|
||
await batchStockInFormRef.value?.validate?.()
|
||
if (!batchStockInLines.value.length) {
|
||
ElMessage.warning('请至少添加一条入库物料')
|
||
return
|
||
}
|
||
for (const line of batchStockInLines.value) {
|
||
if (!line.qty || line.qty <= 0) {
|
||
ElMessage.warning(`「${line.materialName}」入库数量必须大于 0`)
|
||
return
|
||
}
|
||
}
|
||
await ElMessageBox.confirm(
|
||
`确定批量入库 ${batchStockInLines.value.length} 项物料吗?`,
|
||
'批量入库确认',
|
||
{ type: 'warning' }
|
||
)
|
||
batchStockInLoading.value = true
|
||
try {
|
||
const res = await fetchBatchStockInMaterial({
|
||
items: batchStockInLines.value.map((l) => ({
|
||
materialId: l.materialId,
|
||
qty: l.qty,
|
||
remark: l.remark?.trim() || undefined
|
||
})),
|
||
stockInTime: batchStockInForm.stockInTime || undefined,
|
||
remark: batchStockInForm.remark?.trim() || undefined
|
||
})
|
||
ElMessage.success(`批量入库成功,共 ${res.count ?? batchStockInLines.value.length} 项`)
|
||
batchStockInVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
batchStockInLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleBatchStockOut() {
|
||
batchStockOutLines.value = []
|
||
batchOutMaterialPickId.value = ''
|
||
batchOutMaterialOptions.value = []
|
||
batchStockOutForm.stockOutTime = formatNowDatetime()
|
||
batchStockOutForm.receiver = ''
|
||
batchStockOutForm.purpose = ''
|
||
batchStockOutForm.remark = ''
|
||
batchStockOutVisible.value = true
|
||
void searchReceiverUsers('')
|
||
void searchBatchOutMaterials('')
|
||
batchStockOutFormRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleBatchStockOutSubmit() {
|
||
await batchStockOutFormRef.value?.validate?.()
|
||
if (!batchStockOutLines.value.length) {
|
||
ElMessage.warning('请至少添加一条出库物料')
|
||
return
|
||
}
|
||
for (const line of batchStockOutLines.value) {
|
||
if (!line.qty || line.qty <= 0) {
|
||
ElMessage.warning(`「${line.materialName}」出库数量必须大于 0`)
|
||
return
|
||
}
|
||
if (line.qty > line.stockQty) {
|
||
ElMessage.warning(`「${line.materialName}」出库数量不能超过当前库存 ${line.stockQty}`)
|
||
return
|
||
}
|
||
}
|
||
await ElMessageBox.confirm(
|
||
`确定为领用人「${batchStockOutForm.receiver}」批量出库 ${batchStockOutLines.value.length} 项物料吗?`,
|
||
'批量出库确认',
|
||
{ type: 'warning' }
|
||
)
|
||
batchStockOutLoading.value = true
|
||
try {
|
||
const res = await fetchBatchStockOutMaterial({
|
||
items: batchStockOutLines.value.map((l) => ({
|
||
materialId: l.materialId,
|
||
qty: l.qty,
|
||
remark: l.remark?.trim() || undefined
|
||
})),
|
||
stockOutTime: batchStockOutForm.stockOutTime || undefined,
|
||
receiver: batchStockOutForm.receiver?.trim() || undefined,
|
||
purpose: batchStockOutForm.purpose?.trim() || undefined,
|
||
remark: batchStockOutForm.remark?.trim() || undefined
|
||
})
|
||
ElMessage.success(`批量出库成功,共 ${res.count ?? batchStockOutLines.value.length} 项`)
|
||
batchStockOutVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
batchStockOutLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadDictOptions()
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.material-dialog-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.material-batch-stock-dialog {
|
||
.batch-stock-move-head {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.batch-stock-move-lines-head {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.batch-stock-move-lines-head-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.batch-stock-move-material-select {
|
||
flex-shrink: 0;
|
||
width: 320px;
|
||
}
|
||
|
||
.batch-stock-move-lines-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--mint-text, #0f172a);
|
||
}
|
||
|
||
.batch-stock-move-lines-hint {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #64748b);
|
||
}
|
||
|
||
.inner-table {
|
||
width: 100%;
|
||
}
|
||
|
||
.inner-cell-input {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|