Files
shanxia2.0/web-admin/src/views/cell-storage/material/index.vue
T
2026-05-20 21:12:16 +08:00

1447 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 市场物料库 -->
<!-- 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>