Files
geMoldInsight/frontend/src/modules/inventory/components/MaterialsTab.vue
T
cjw eab4137c0a fix(frontend)+chore(deploy): 修复明细表格渲染 + static/ 改服务器构建
- SalesOrdersTab/PurchaseOrdersTab/MaterialsTab/ProductsTab: t-table 明细行不渲染(push/splice 改新数组引用)+ Decimal 算术类型错误(Number() 包裹)
- static/ 不再进 git(.gitignore),改为 deploy.sh 在服务器 npm run build
- 新增 deploy.sh:拉取 + 构建前端 + 重启后端
2026-08-03 16:12:34 +08:00

327 lines
11 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.
<script setup lang="ts">
import { ref, computed, onMounted } 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 saving = ref(false)
const form = ref<any>({})
const showRestockModal = ref(false)
const restockItems = ref<any[]>([])
const restockSupplierId = ref<number | null>(null)
const restocking = ref(false)
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 (saving.value) return
if (!form.value.sku) {
addNotification('请输入SKU', 'warning')
return
}
if (!form.value.name) {
addNotification('请输入名称', 'warning')
return
}
saving.value = true
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, '保存物料')
} finally {
saving.value = false
}
}
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 = [...restockItems.value, {
material_id: state.materials[0]?.id || null,
quantity: 1,
unit_price: 0,
remark: ''
}]
}
function removeRestockItem(index: number) {
restockItems.value = restockItems.value.filter((_, i) => i !== index)
}
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 (restocking.value) return
if (!restockSupplierId.value) {
addNotification('请选择供应商', 'warning')
return
}
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
}
}
restocking.value = true
try {
const payload = {
supplier_id: restockSupplierId.value,
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
}))
}
await apiRequest('/api/purchase-orders', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('采购订单创建成功', 'success')
showRestockModal.value = false
restockItems.value = []
restockSupplierId.value = null
loadPurchaseOrders()
} catch (e) {
handleApiError(e, '保存补货订单')
} finally {
restocking.value = false
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
}
function closeRestockModal() {
showRestockModal.value = false
restockItems.value = []
}
onMounted(() => { loadMaterials() })
</script>
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 8px;">
<t-button type="primary" @click="openCreateMaterial">新增物料</t-button>
<t-button type="success" @click="openRestockModal">物料补货</t-button>
</div>
<t-table :data="state.materials" :loading="state.loading" stripe>
<t-table-column prop="sku" label="SKU" />
<t-table-column prop="name" label="名称" />
<t-table-column prop="category" label="分类">
<template #default="{ row }">{{ row.category || '-' }}</template>
</t-table-column>
<t-table-column prop="unit" label="单位" />
<t-table-column label="成本价">
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
</t-table-column>
<t-table-column prop="min_stock" label="最低库存">
<template #default="{ row }">{{ row.min_stock ?? '-' }}</template>
</t-table-column>
<t-table-column label="操作" width="180">
<template #default="{ row }">
<t-button type="primary" size="small" @click="editMaterial(row)">编辑</t-button>
<t-button type="danger" size="small" @click="deleteMaterial(row.id)">删除</t-button>
</template>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.materials.length === 0" description="暂无物料数据" />
<t-dialog
v-model:visible="showModal"
:title="(editingItem ? '编辑' : '新增') + '物料'"
width="560px"
@closed="closeModal"
>
<t-form label-width="100px">
<t-form-item label="类型">
<t-input model-value="物料(纳入库存)" disabled />
</t-form-item>
<t-form-item label="SKU" required>
<t-input v-model="form.sku" placeholder="产品编码" />
</t-form-item>
<t-form-item label="名称" required>
<t-input v-model="form.name" placeholder="产品名称" />
</t-form-item>
<t-form-item label="分类">
<t-input v-model="form.category" placeholder="产品分类" />
</t-form-item>
<t-form-item label="单位">
<t-input v-model="form.unit" placeholder="kg" />
</t-form-item>
<t-form-item label="成本价">
<t-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</t-form-item>
<t-form-item label="最低库存">
<t-input-number v-model="form.min_stock" :min="0" style="width:100%" />
</t-form-item>
<t-form-item label="最高库存">
<t-input-number v-model="form.max_stock" :min="0" style="width:100%" />
</t-form-item>
</t-form>
<template #footer>
<t-button @click="showModal = false" :disabled="saving">取消</t-button>
<t-button type="primary" :loading="saving" :disabled="saving" @click="saveProduct">保存</t-button>
</template>
</t-dialog>
<t-dialog
v-model:visible="showRestockModal"
title="物料补货"
width="900px"
@closed="closeRestockModal"
>
<t-form label-width="100px" style="margin-bottom: 16px;">
<t-form-item label="供应商" required>
<t-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
<t-option
v-for="supplier in state.suppliers"
:key="supplier.id"
:label="supplier.name"
:value="supplier.id"
/>
</t-select>
</t-form-item>
</t-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">补货物料明细</span>
<t-button type="primary" size="small" @click="addRestockItem">+ 添加物料</t-button>
</div>
<t-table :data="restockItems" border size="small">
<t-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<t-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<t-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</t-select>
</template>
</t-table-column>
<t-table-column label="数量" width="120">
<template #default="{ row: line }">
<t-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
</template>
</t-table-column>
<t-table-column label="单价" width="120">
<template #default="{ row: line }">
<t-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
</template>
</t-table-column>
<t-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((line.unit_price || 0) * (line.quantity || 0)) }}
</template>
</t-table-column>
<t-table-column label="备注" width="150">
<template #default="{ row: line }">
<t-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<t-button type="danger" size="small" @click="removeRestockItem($index)">删除</t-button>
</template>
</t-table-column>
</t-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
补货总金额:{{ formatCurrency(restockTotalAmount) }}
</div>
<template #footer>
<t-button @click="showRestockModal = false" :disabled="restocking">取消</t-button>
<t-button type="primary" :loading="restocking" :disabled="restocking" @click="saveRestock">创建采购订单</t-button>
</template>
</t-dialog>
</div>
</template>
<style scoped>
</style>