This commit is contained in:
2026-06-02 09:55:06 +08:00
parent ecc6b8ab8a
commit 8cca6f9d32
28 changed files with 1951 additions and 2658 deletions
@@ -0,0 +1,145 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useInventory } from '@/modules/inventory/composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const { state, loadCustomers } = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = reactive({
name: '',
contact_person: '',
phone: '',
email: '',
address: ''
})
function openCreateCustomer() {
editingItem.value = null
form.name = ''
form.contact_person = ''
form.phone = ''
form.email = ''
form.address = ''
showModal.value = true
}
function editCustomer(item: any) {
editingItem.value = item
form.name = item.name ?? ''
form.contact_person = item.contact_person ?? ''
form.phone = item.phone ?? ''
form.email = item.email ?? ''
form.address = item.address ?? ''
showModal.value = true
}
async function save() {
try {
const payload = {
name: form.name,
contact_person: form.contact_person || null,
phone: form.phone || null,
email: form.email || null,
address: form.address || null
}
if (editingItem.value) {
await apiRequest(`/api/customers/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('客户更新成功', 'success')
} else {
await apiRequest('/api/customers', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('客户已创建', 'success')
}
showModal.value = false
loadCustomers()
} catch (e) {
handleApiError(e, '保存客户')
}
}
async function deleteCustomer(id: number) {
if (!confirm('确定要删除这个客户吗?')) return
try {
await apiRequest(`/api/customers/${id}`, { method: 'DELETE' })
addNotification('客户已删除', 'success')
loadCustomers()
} catch (e) {
handleApiError(e, '删除客户')
}
}
</script>
<template>
<div class="customers-tab">
<div class="table-header">
<el-button type="primary" @click="openCreateCustomer">+ 新增客户</el-button>
</div>
<el-table :data="state.customers" style="width: 100%" v-loading="state.loading" empty-text="暂无客户数据">
<el-table-column prop="code" label="编码" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="contact_person" label="联系人">
<template #default="{ row }">
{{ row.contact_person || '-' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="电话">
<template #default="{ row }">
{{ row.phone || '-' }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱">
<template #default="{ row }">
{{ row.email || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="editCustomer(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCustomer(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑客户' : '新增客户'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contact_person" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.customers-tab {
padding: 0;
}
.table-header {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { useInventory } from '@/modules/inventory/composables/useInventory'
const { state, formatCurrency } = useInventory()
</script>
<template>
<el-row :gutter="16">
<el-col :span="8" v-for="card in [
{ icon: '📦', value: state.dashboard?.product_count || 0, label: '成品数量' },
{ icon: '📊', value: state.dashboard?.total_stock || 0, label: '物料库存总量' },
{ icon: '💰', value: formatCurrency(state.dashboard?.total_value || 0), label: '库存价值' },
{ icon: '🏭', value: state.dashboard?.supplier_count || 0, label: '供应商' },
{ icon: '👥', value: state.dashboard?.customer_count || 0, label: '客户' },
{ icon: '🏪', value: state.dashboard?.warehouse_count || 0, label: '仓库' }
]" :key="card.label" style="margin-bottom: 16px">
<el-card shadow="hover">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 32px;">{{ card.icon }}</span>
<div>
<div style="font-size: 24px; font-weight: 700;">{{ card.value }}</div>
<div style="color: var(--text-tertiary); font-size: 13px;">{{ card.label }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</template>
<style scoped>
</style>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { useInventory } from '@/modules/inventory/composables/useInventory'
const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInventory()
</script>
<template>
<el-menu
:default-active="state.activeTab"
class="inventory-sidebar"
@select="(key: string) => handleMenuClick(key)"
>
<div v-for="group in menuGroups" :key="group.key" class="sidebar-group">
<div class="sidebar-group-title" @click="toggleGroup(group.key)">
<span>{{ group.title }}</span>
<el-icon><component :is="openGroups[group.key] ? 'ArrowDown' : 'ArrowRight'" /></el-icon>
</div>
<template v-if="openGroups[group.key]">
<el-menu-item
v-for="item in group.items"
:key="group.key + '-' + item.key"
:index="item.key"
>
{{ item.label }}
</el-menu-item>
</template>
</div>
</el-menu>
</template>
@@ -0,0 +1,156 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useInventory } from '@/modules/inventory/composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const { state, loadInventory, ensureStockBaseData } = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = reactive({
product_id: null as number | null,
warehouse_id: null as number | null,
quantity: 0,
locked_quantity: 0,
batch_no: '',
location: ''
})
function openCreate() {
ensureStockBaseData()
editingItem.value = null
form.product_id = null
form.warehouse_id = null
form.quantity = 0
form.locked_quantity = 0
form.batch_no = ''
form.location = ''
showModal.value = true
}
function openEdit(item: any) {
ensureStockBaseData()
editingItem.value = item
form.product_id = item.product_id ?? null
form.warehouse_id = item.warehouse_id ?? null
form.quantity = item.quantity ?? 0
form.locked_quantity = item.locked_quantity ?? 0
form.batch_no = item.batch_no ?? ''
form.location = item.location ?? ''
showModal.value = true
}
async function save() {
try {
const payload = {
product_id: form.product_id,
warehouse_id: form.warehouse_id,
quantity: form.quantity,
locked_quantity: form.locked_quantity,
batch_no: form.batch_no || null,
location: form.location || null
}
if (editingItem.value) {
await apiRequest(`/api/inventory/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('物料库存更新成功', 'success')
} else {
await apiRequest('/api/inventory', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('物料库存已创建', 'success')
}
showModal.value = false
loadInventory()
} catch (e) {
handleApiError(e, '保存物料库存')
}
}
async function deleteItem(id: number) {
if (!confirm('确定要删除这个物料库存记录吗?')) return
try {
await apiRequest(`/api/inventory/${id}`, { method: 'DELETE' })
addNotification('物料库存已删除', 'success')
loadInventory()
} catch (e) {
handleApiError(e, '删除物料库存')
}
}
</script>
<template>
<div class="inventory-tab">
<div class="table-header">
<el-button type="primary" @click="openCreate">+ 新增物料库存</el-button>
</div>
<el-table :data="state.inventory" style="width: 100%" v-loading="state.loading" empty-text="暂无库存数据">
<el-table-column prop="product_sku" label="SKU" />
<el-table-column prop="product_name" label="物料" />
<el-table-column prop="warehouse_name" label="仓库" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="available_quantity" label="可用" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteItem(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑物料库存' : '新增物料库存'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="物料" required>
<el-select v-model="form.product_id" placeholder="请选择物料" style="width: 100%">
<el-option
v-for="m in state.materials"
:key="m.id"
:label="m.name + (m.sku ? ' (' + m.sku + ')' : '')"
:value="m.id"
/>
</el-select>
</el-form-item>
<el-form-item label="仓库" required>
<el-select v-model="form.warehouse_id" placeholder="请选择仓库" style="width: 100%">
<el-option
v-for="w in state.warehouses"
:key="w.id"
:label="w.name"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item label="数量" required>
<el-input-number v-model="form.quantity" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="锁定数量">
<el-input-number v-model="form.locked_quantity" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="批次号">
<el-input v-model="form.batch_no" placeholder="请输入批次号" />
</el-form-item>
<el-form-item label="库位">
<el-input v-model="form.location" placeholder="请输入库位" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.inventory-tab {
padding: 0;
}
.table-header {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,314 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const {
state,
formatCurrency,
loadMaterials,
loadSuppliers,
loadPurchaseOrders
} = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = ref<any>({})
const showRestockModal = ref(false)
const restockItems = ref<any[]>([])
const restockSupplierId = ref<number | null>(null)
function openCreateMaterial() {
editingItem.value = null
form.value = {
item_type: 'material',
unit: 'kg',
min_stock: 0,
max_stock: 1000,
cost_price: 0,
sale_price: 0,
sku: '',
name: '',
category: ''
}
showModal.value = true
}
async function editMaterial(product: any) {
editingItem.value = product
form.value = { ...product }
showModal.value = true
}
async function saveProduct() {
if (!form.value.sku) {
addNotification('请输入SKU', 'warning')
return
}
if (!form.value.name) {
addNotification('请输入名称', 'warning')
return
}
try {
if (editingItem.value) {
await apiRequest(`/api/products/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(form.value)
})
addNotification('物料更新成功', 'success')
} else {
await apiRequest('/api/products', {
method: 'POST',
body: JSON.stringify(form.value)
})
addNotification('物料创建成功', 'success')
}
showModal.value = false
editingItem.value = null
form.value = {}
loadMaterials()
} catch (e) {
handleApiError(e, '保存物料')
}
}
async function deleteMaterial(id: number) {
if (!confirm('确定要删除这个物料吗?')) return
try {
await apiRequest(`/api/products/${id}`, { method: 'DELETE' })
addNotification('物料已删除', 'success')
loadMaterials()
} catch (e) {
handleApiError(e, '删除物料')
}
}
async function openRestockModal() {
await loadMaterials()
await loadSuppliers()
restockItems.value = []
showRestockModal.value = true
}
function addRestockItem() {
restockItems.value.push({
material_id: state.materials[0]?.id || null,
quantity: 1,
unit_price: 0,
remark: ''
})
}
function removeRestockItem(index: number) {
restockItems.value.splice(index, 1)
}
const restockTotalAmount = computed(() => {
return restockItems.value.reduce((sum: number, item: any) => {
return sum + (item.unit_price || 0) * (item.quantity || 0)
}, 0)
})
async function saveRestock() {
if (!restockItems.value || restockItems.value.length === 0) {
addNotification('请至少添加一个补货物料', 'warning')
return
}
for (const [i, item] of restockItems.value.entries()) {
const idx = i + 1
if (!item.material_id) {
addNotification(`第 ${idx} 行:请选择物料`, 'warning')
return
}
if (!Number.isFinite(item.quantity) || item.quantity <= 0) {
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning')
return
}
if (!Number.isFinite(item.unit_price) || item.unit_price < 0) {
addNotification(`第 ${idx} 行:单价必须大于等于 0`, 'warning')
return
}
}
try {
const payload = {
supplier_id: restockSupplierId.value || state.suppliers[0]?.id || null,
expected_date: new Date().toISOString().split('T')[0],
remark: '物料补货',
items: restockItems.value.map((item: any) => ({
product_id: item.material_id,
quantity: item.quantity,
unit_price: item.unit_price,
remark: item.remark
}))
}
if (!payload.supplier_id) {
addNotification('请先添加供应商', 'warning')
return
}
await apiRequest('/api/purchase-orders', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('采购订单创建成功', 'success')
showRestockModal.value = false
restockItems.value = []
loadPurchaseOrders()
} catch (e) {
handleApiError(e, '保存补货订单')
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
}
function closeRestockModal() {
showRestockModal.value = false
restockItems.value = []
}
</script>
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 8px;">
<el-button type="primary" @click="openCreateMaterial">新增物料</el-button>
<el-button type="success" @click="openRestockModal">物料补货</el-button>
</div>
<el-table :data="state.materials" v-loading="state.loading" stripe>
<el-table-column prop="sku" label="SKU" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="category" label="分类">
<template #default="{ row }">{{ row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" />
<el-table-column label="成本价">
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
</el-table-column>
<el-table-column prop="min_stock" label="最低库存">
<template #default="{ row }">{{ row.min_stock ?? '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editMaterial(row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteMaterial(row.id)">删除</el-button>
</template>
</el-table-column>
<el-empty v-if="!state.loading && state.materials.length === 0" description="暂无物料数据" />
</el-table>
<el-dialog
v-model="showModal"
:title="(editingItem ? '编辑' : '新增') + '物料'"
width="560px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="类型">
<el-input model-value="物料(纳入库存)" disabled />
</el-form-item>
<el-form-item label="SKU" required>
<el-input v-model="form.sku" placeholder="产品编码" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="产品名称" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="form.category" placeholder="产品分类" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" placeholder="kg" />
</el-form-item>
<el-form-item label="成本价">
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="最低库存">
<el-input-number v-model="form.min_stock" :min="0" style="width:100%" />
</el-form-item>
<el-form-item label="最高库存">
<el-input-number v-model="form.max_stock" :min="0" style="width:100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveProduct">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="showRestockModal"
title="物料补货"
width="900px"
@closed="closeRestockModal"
>
<el-form label-width="100px" style="margin-bottom: 16px;">
<el-form-item label="供应商">
<el-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
<el-option
v-for="supplier in state.suppliers"
:key="supplier.id"
:label="supplier.name"
:value="supplier.id"
/>
</el-select>
</el-form-item>
</el-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">补货物料明细</span>
<el-button type="primary" size="small" @click="addRestockItem">+ 添加物料</el-button>
</div>
<el-table :data="restockItems" border size="small">
<el-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<el-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((line.unit_price || 0) * (line.quantity || 0)) }}
</template>
</el-table-column>
<el-table-column label="备注" width="150">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeRestockItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
补货总金额:{{ formatCurrency(restockTotalAmount) }}
</div>
<template #footer>
<el-button @click="showRestockModal = false">取消</el-button>
<el-button type="primary" @click="saveRestock">创建采购订单</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { useInventory } from '@/modules/inventory/composables/useInventory'
const { state, getMovementTypeLabel, getMovementBadgeClass, formatDateTime } = useInventory()
const getMovementTagType = (movementType: string): 'success' | 'danger' | 'warning' | 'info' | '' => {
const badgeClass = getMovementBadgeClass(movementType)
if (badgeClass === 'badge-success') return 'success'
if (badgeClass === 'badge-error') return 'danger'
if (badgeClass === 'badge-warning') return 'warning'
return 'info'
}
</script>
<template>
<div class="movements-tab">
<el-table :data="state.movements" style="width: 100%" v-loading="state.loading" empty-text="暂无库存变动记录">
<el-table-column label="物料">
<template #default="{ row }">
{{ row.product_name }}{{ row.product_sku ? ' (' + row.product_sku + ')' : '' }}
</template>
</el-table-column>
<el-table-column label="类型" width="140">
<template #default="{ row }">
<el-tag :type="getMovementTagType(row.movement_type)" size="small">
{{ getMovementTypeLabel(row.movement_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column prop="before_quantity" label="变动前" width="80" />
<el-table-column prop="after_quantity" label="变动后" width="80" />
<el-table-column label="时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
</el-table>
</div>
</template>
<style scoped>
.movements-tab {
padding: 0;
}
</style>
@@ -0,0 +1,226 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const {
state,
formatCurrency,
loadFinishedProducts,
loadMaterials
} = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = ref<any>({})
const bomItems = ref<any[]>([])
function openCreateProduct() {
editingItem.value = null
form.value = {
item_type: 'finished',
unit: '件',
min_stock: 0,
max_stock: 1000,
cost_price: 0,
sale_price: 0,
sku: '',
name: '',
category: ''
}
bomItems.value = []
showModal.value = true
}
async function editProduct(product: any) {
editingItem.value = product
form.value = { ...product }
await loadMaterials()
try {
const bom = await apiRequest(`/api/products/${product.id}/materials`)
bomItems.value = (bom.items || []).map((bomItem: any) => ({
material_id: bomItem.material_id,
quantity: bomItem.quantity
}))
} catch (e) {
handleApiError(e, '加载BOM')
}
showModal.value = true
}
function addBomItem() {
bomItems.value.push({
material_id: state.materials[0]?.id || null,
quantity: 1
})
}
function removeBomItem(index: number) {
bomItems.value.splice(index, 1)
}
async function saveProduct() {
if (!form.value.sku) {
addNotification('请输入SKU', 'warning')
return
}
if (!form.value.name) {
addNotification('请输入名称', 'warning')
return
}
try {
if (editingItem.value) {
await apiRequest(`/api/products/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(form.value)
})
if (form.value.item_type === 'finished' && bomItems.value.length > 0) {
await apiRequest(`/api/products/${editingItem.value.id}/materials`, {
method: 'PUT',
body: JSON.stringify({ items: bomItems.value })
})
}
addNotification('成品更新成功', 'success')
} else {
await apiRequest('/api/products', {
method: 'POST',
body: JSON.stringify(form.value)
})
addNotification('成品创建成功', 'success')
}
showModal.value = false
editingItem.value = null
form.value = {}
bomItems.value = []
loadFinishedProducts()
loadMaterials()
} catch (e) {
handleApiError(e, '保存成品')
}
}
async function deleteProduct(id: number) {
if (!confirm('确定要删除这个成品吗?')) return
try {
await apiRequest(`/api/products/${id}`, { method: 'DELETE' })
addNotification('成品已删除', 'success')
loadFinishedProducts()
loadMaterials()
} catch (e) {
handleApiError(e, '删除成品')
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
bomItems.value = []
}
</script>
<template>
<div>
<div style="margin-bottom: 12px;">
<el-button type="primary" @click="openCreateProduct">新增成品</el-button>
</div>
<el-table :data="state.finishedProducts" v-loading="state.loading" stripe>
<el-table-column prop="sku" label="SKU" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="category" label="分类">
<template #default="{ row }">{{ row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" />
<el-table-column label="成本价">
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
</el-table-column>
<el-table-column label="销售价">
<template #default="{ row }">{{ formatCurrency(row.sale_price) }}</template>
</el-table-column>
<el-table-column label="基础物料成本">
<template #default="{ row }">{{ formatCurrency(row.material_cost || 0) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editProduct(row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteProduct(row.id)">删除</el-button>
</template>
</el-table-column>
<el-empty v-if="!state.loading && state.finishedProducts.length === 0" description="暂无成品数据" />
</el-table>
<el-dialog
v-model="showModal"
:title="(editingItem ? '编辑' : '新增') + '成品'"
width="560px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="类型">
<el-input model-value="成品(按单生产,不做库存)" disabled />
</el-form-item>
<el-form-item label="SKU" required>
<el-input v-model="form.sku" placeholder="产品编码" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="产品名称" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="form.category" placeholder="产品分类" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" placeholder="件" />
</el-form-item>
<el-form-item label="成本价">
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="销售价">
<el-input-number v-model="form.sale_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="说明">
<el-input model-value="成品不做库存,成本由下方BOM定义物料构成后自动计算" disabled />
</el-form-item>
<template v-if="editingItem">
<el-divider content-position="left">BOM物料配置</el-divider>
<div style="padding: 0 12px 12px 12px;">
<el-button type="primary" size="small" @click="addBomItem" style="margin-bottom:8px">+ 添加物料</el-button>
<el-table :data="bomItems" size="small" border>
<el-table-column label="物料">
<template #default="{ row: bomItem }">
<el-select v-model="bomItem.material_id" placeholder="选择物料" style="width:100%">
<el-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
<template #default="{ row: bomItem }">
<el-input-number v-model="bomItem.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeBomItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveProduct">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,145 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useInventory } from '@/modules/inventory/composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const { state, loadSuppliers } = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = reactive({
name: '',
contact_person: '',
phone: '',
email: '',
address: ''
})
function openCreateSupplier() {
editingItem.value = null
form.name = ''
form.contact_person = ''
form.phone = ''
form.email = ''
form.address = ''
showModal.value = true
}
function editSupplier(item: any) {
editingItem.value = item
form.name = item.name ?? ''
form.contact_person = item.contact_person ?? ''
form.phone = item.phone ?? ''
form.email = item.email ?? ''
form.address = item.address ?? ''
showModal.value = true
}
async function save() {
try {
const payload = {
name: form.name,
contact_person: form.contact_person || null,
phone: form.phone || null,
email: form.email || null,
address: form.address || null
}
if (editingItem.value) {
await apiRequest(`/api/suppliers/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('供应商更新成功', 'success')
} else {
await apiRequest('/api/suppliers', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('供应商已创建', 'success')
}
showModal.value = false
loadSuppliers()
} catch (e) {
handleApiError(e, '保存供应商')
}
}
async function deleteSupplier(id: number) {
if (!confirm('确定要删除这个供应商吗?')) return
try {
await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' })
addNotification('供应商已删除', 'success')
loadSuppliers()
} catch (e) {
handleApiError(e, '删除供应商')
}
}
</script>
<template>
<div class="suppliers-tab">
<div class="table-header">
<el-button type="primary" @click="openCreateSupplier">+ 新增供应商</el-button>
</div>
<el-table :data="state.suppliers" style="width: 100%" v-loading="state.loading" empty-text="暂无供应商数据">
<el-table-column prop="code" label="编码" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="contact_person" label="联系人">
<template #default="{ row }">
{{ row.contact_person || '-' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="电话">
<template #default="{ row }">
{{ row.phone || '-' }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱">
<template #default="{ row }">
{{ row.email || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="editSupplier(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSupplier(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑供应商' : '新增供应商'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="请输入供应商名称" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contact_person" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.suppliers-tab {
padding: 0;
}
.table-header {
margin-bottom: 16px;
}
</style>