670 lines
21 KiB
Vue
670 lines
21 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>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<el-table
|
||
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 label="操作" 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-input v-model="formData.unit" placeholder="请输入单位" />
|
||
</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="备注">
|
||
<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="receiver">
|
||
<el-input v-model="stockOutForm.receiver" placeholder="请输入领用人" />
|
||
</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>
|
||
|
||
<!-- 查看弹窗 -->
|
||
<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 } from '@/api/admin'
|
||
import {
|
||
fetchMaterialList,
|
||
fetchCreateMaterial,
|
||
fetchUpdateMaterial,
|
||
fetchDeleteMaterial,
|
||
fetchStockInMaterial,
|
||
fetchStockOutMaterial
|
||
} from '@/api/material'
|
||
import type { Material } from '@/api/material'
|
||
|
||
defineOptions({ name: 'CellStorageMaterial' })
|
||
|
||
const searchForm = reactive({ keyword: '', category: '', status: '' })
|
||
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 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, remark: '' })
|
||
|
||
const stockInRules = {
|
||
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }]
|
||
}
|
||
|
||
// 出库
|
||
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, receiver: '', purpose: '', remark: '' })
|
||
|
||
const stockOutRules = {
|
||
qty: [{ required: true, message: '请输入出库数量', trigger: 'blur' }],
|
||
receiver: [{ required: true, message: '请输入领用人', trigger: 'blur' }]
|
||
}
|
||
|
||
// 查看
|
||
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
|
||
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 || ''
|
||
dialogVisible.value = true
|
||
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.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,
|
||
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.receiver = ''
|
||
stockOutForm.purpose = ''
|
||
stockOutForm.remark = ''
|
||
stockOutVisible.value = true
|
||
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,
|
||
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
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadDictOptions()
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.material-dialog-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
</style>
|