Files
shanxia2.0/web-admin/src/views/cell-storage/material/index.vue
T
2026-05-20 14:32:07 +08:00

670 lines
21 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>
</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>