物料-采购件入库页面功能实现
This commit is contained in:
415
inventory-web/src/views/stock/inbound/buy.vue
Normal file
415
inventory-web/src/views/stock/inbound/buy.vue
Normal file
@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="buy-module">
|
||||
<div class="header-tools">
|
||||
<div class="left-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">全量采购入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData">刷新数据</el-button>
|
||||
</div>
|
||||
|
||||
<el-popover placement="bottom" title="显示列配置" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button :icon="Setting">自定义表格表头</el-button>
|
||||
</template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-divider content-position="left">基础层字段</el-divider>
|
||||
<el-checkbox v-for="c in baseColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-divider content-position="left">库存/财务层字段</el-divider>
|
||||
<el-checkbox v-for="c in stockColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
size="small"
|
||||
highlight-current-row
|
||||
>
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column
|
||||
v-if="visibleColumnProps.includes(col.prop)"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || '120'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope" v-if="col.prop === 'serial_batch'">
|
||||
<span v-if="scope.row.serial_number" style="color: #409EFF; font-weight: bold;">
|
||||
SN: {{ scope.row.serial_number }}
|
||||
</span>
|
||||
<span v-else-if="scope.row.batch_number" style="color: #67C23A; font-weight: bold;">
|
||||
BN: {{ scope.row.batch_number }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag size="small" :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
|
||||
{{ formatMoney(scope.row[col.prop]) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="small">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination-container"
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增采购件入库' : '编辑入库信息'"
|
||||
width="950px"
|
||||
top="3vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules" size="default">
|
||||
|
||||
<el-divider content-position="left"><b>1. 基础核心层 (Material Base)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="名称" prop="material_name">
|
||||
<el-input v-model="form.material_name" :disabled="dialogStatus === 'update'" placeholder="必填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="规格型号" prop="spec_model">
|
||||
<el-input v-model="form.spec_model" :disabled="dialogStatus === 'update'" placeholder="必填: 内部货号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="计量单位" prop="unit"><el-input v-model="form.unit" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="类别" prop="category"><el-input v-model="form.category" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" prop="material_type"><el-input v-model="form.material_type" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="可见等级" prop="visibility_level"><el-input-number v-model="form.visibility_level" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left"><b>2. 实体库存层 (Stock Buy)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库日期" prop="in_date">
|
||||
<el-input v-model="form.in_date" disabled placeholder="系统自动生成">
|
||||
<template #suffix><el-icon><Calendar /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #fffbf0; border-radius: 4px; padding-top:10px;">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="设备SN码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="批号" prop="batch_number">
|
||||
<el-input v-model="form.batch_number" placeholder="生产批次号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div style="font-size: 12px; color: #e6a23c; margin-left: 120px; margin-bottom: 10px; line-height: 1;">
|
||||
* 规则:序列号与批号互斥且必填其一 (填写一个会自动清空另一个)
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 10px;">
|
||||
<el-col :span="12"><el-form-item label="到检状态" prop="inspection_status"><el-input v-model="form.inspection_status" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="照片/到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #f8fcfd; padding-top: 18px; border-radius: 4px; margin-top: 10px;">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库量" prop="in_quantity">
|
||||
<el-input-number
|
||||
v-model="form.in_quantity"
|
||||
:min="1"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
style="width:100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="库存数量" prop="stock_quantity">
|
||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="可用数量" prop="available_quantity">
|
||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left"><b>3. 财务与商务信息</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" style="width:100%" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="总价" prop="total_price"><el-input-number v-model="form.total_price" :precision="4" style="width:100%" disabled :controls="false" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="供应商" prop="supplier_name"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">
|
||||
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Calendar } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getBuyList, createBuyInbound, updateBuyInbound, deleteBuyInbound } from '@/api/inbound/buy'
|
||||
|
||||
// 状态控制
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 15 })
|
||||
|
||||
// --- 1. 列定义 ---
|
||||
const baseColumns = [
|
||||
{ prop: 'material_name', label: '物料名称', minWidth: '150' },
|
||||
{ prop: 'category', label: '类别', minWidth: '100' },
|
||||
{ prop: 'spec_model', label: '规格型号', minWidth: '150' },
|
||||
{ prop: 'unit', label: '单位', minWidth: '70' }
|
||||
]
|
||||
|
||||
const stockColumns = [
|
||||
{ prop: 'sku', label: '编码/SKU', minWidth: '140' },
|
||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
|
||||
// 组合展示列
|
||||
{ prop: 'serial_batch', label: '序列号/批号', minWidth: '160' },
|
||||
{ prop: 'stock_quantity', label: '库存数', minWidth: '90' },
|
||||
{ prop: 'available_quantity', label: '可用数', minWidth: '90' },
|
||||
{ prop: 'in_quantity', label: '入库量', minWidth: '90' },
|
||||
{ prop: 'unit_price', label: '单价', minWidth: '110' },
|
||||
{ prop: 'total_price', label: '总价', minWidth: '110' },
|
||||
{ prop: 'status', label: '状态', minWidth: '90' },
|
||||
{ prop: 'warehouse_location', label: '库位', minWidth: '100' },
|
||||
{ prop: 'supplier_name', label: '供应商', minWidth: '150' }
|
||||
]
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
// --- 2. 默认展示列 ---
|
||||
const visibleColumnProps = ref([
|
||||
'material_name', 'spec_model', 'inbound_date',
|
||||
'serial_batch', 'stock_quantity', 'available_quantity', 'status'
|
||||
])
|
||||
|
||||
// --- 3. 表单对象 ---
|
||||
const form = reactive({
|
||||
id: undefined, // 编辑时使用
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
serial_number: '', batch_number: '',
|
||||
status: '在库', inspection_status: '未检',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
})
|
||||
|
||||
// --- 4. 校验逻辑 (互斥且必填其一) ---
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
if (!form.serial_number && !form.batch_number) {
|
||||
callback(new Error('序列号和批号至少填写一项'))
|
||||
} else {
|
||||
// 清除交叉报错
|
||||
if (formRef.value) {
|
||||
if (rule.field === 'serial_number' && form.batch_number) formRef.value.clearValidate('batch_number')
|
||||
if (rule.field === 'batch_number' && form.serial_number) formRef.value.clearValidate('serial_number')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
material_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
spec_model: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }],
|
||||
batch_number: [{ validator: validateIdentity, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// --- 5. 监听逻辑 ---
|
||||
watch(() => form.serial_number, (val) => {
|
||||
if (val && form.batch_number) form.batch_number = ''
|
||||
})
|
||||
watch(() => form.batch_number, (val) => {
|
||||
if (val && form.serial_number) form.serial_number = ''
|
||||
})
|
||||
|
||||
watch(() => form.in_quantity, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
if (dialogStatus.value === 'create') {
|
||||
form.stock_quantity = newVal
|
||||
form.available_quantity = newVal
|
||||
}
|
||||
form.total_price = Number((newVal * form.unit_price).toFixed(4))
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => form.unit_price, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
form.total_price = Number((newVal * form.in_quantity).toFixed(4))
|
||||
}
|
||||
})
|
||||
|
||||
// --- 6. 核心操作 ---
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getBuyList(queryParams)
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
resetForm()
|
||||
form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 核心修改:手动映射后端数据到前端表单
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
|
||||
// 先重置表单防止残留
|
||||
resetForm()
|
||||
|
||||
// 1. 基础字段拷贝
|
||||
Object.assign(form, row)
|
||||
|
||||
// 2. 修正字段映射 (后端Key -> 前端Form Key)
|
||||
form.id = row.id
|
||||
form.in_quantity = Number(row.qty_inbound) || 1
|
||||
form.stock_quantity = Number(row.qty_inbound) || 1 // 这里假设库存没变,或者应由后端传回stock_quantity
|
||||
form.available_quantity = Number(row.qty_available) || 1
|
||||
form.unit_price = Number(row.price_unit) || 0
|
||||
form.warehouse_location = row.warehouse_loc || ''
|
||||
form.serial_number = row.serial_number || ''
|
||||
form.batch_number = row.batch_number || ''
|
||||
form.status = row.status || '在库'
|
||||
|
||||
// 3. 计算总价
|
||||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2))
|
||||
|
||||
// 4. 补充日期
|
||||
if (!form.in_date) form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await deleteBuyInbound(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
await createBuyInbound(form)
|
||||
ElMessage.success('入库成功')
|
||||
} else {
|
||||
await updateBuyInbound(form.id!, form)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
visible.value = false
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '提交失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
serial_number: '', batch_number: '',
|
||||
status: '在库', inspection_status: '未检',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
return status === '在库' ? 'success' : 'info'
|
||||
}
|
||||
|
||||
const formatMoney = (val: number) => {
|
||||
return val ? `¥ ${Number(val).toFixed(2)}` : '-'
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.buy-module { background: #fff; border-radius: 8px; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.column-selector { display: flex; flex-direction: column; padding: 10px; max-height: 400px; overflow-y: auto; }
|
||||
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
|
||||
:deep(.el-divider--horizontal) { margin: 15px 0 15px 0; }
|
||||
</style>
|
||||
27
inventory-web/src/views/stock/inbound/index.vue
Normal file
27
inventory-web/src/views/stock/inbound/index.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="inbound-container" style="padding: 20px;">
|
||||
<el-tabs v-model="activeModule" type="border-card">
|
||||
<el-tab-pane label="物料采购入库" name="buy">
|
||||
<BuyInbound v-if="activeModule === 'buy'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="半成品入库" name="semi">
|
||||
<SemiInbound v-if="activeModule === 'semi'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="成品入库" name="product">
|
||||
<ProductInbound v-if="activeModule === 'product'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
// 因为在同一个文件夹下,直接用 ./ 即可
|
||||
import BuyInbound from './buy.vue'
|
||||
import SemiInbound from './semi.vue'
|
||||
import ProductInbound from './product.vue'
|
||||
|
||||
const activeModule = ref('buy')
|
||||
</script>
|
||||
11
inventory-web/src/views/stock/inbound/product.vue
Normal file
11
inventory-web/src/views/stock/inbound/product.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
11
inventory-web/src/views/stock/inbound/semi.vue
Normal file
11
inventory-web/src/views/stock/inbound/semi.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
1
inventory-web/src/views/stock/inbound/service.vue
Normal file
1
inventory-web/src/views/stock/inbound/service.vue
Normal file
@ -0,0 +1 @@
|
||||
<template><div style="padding:20px;"><h2>服务权益管理</h2></div></template>
|
||||
Reference in New Issue
Block a user