This commit is contained in:
2026-06-02 09:55:28 +08:00
parent 8cca6f9d32
commit cafa3ef883
@@ -0,0 +1,466 @@
<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,
formatDateTime,
formatDate,
getPurchaseOrderStatusLabel,
isPurchaseOrderLocked,
loadPurchaseOrders,
loadSuppliers,
loadMaterials,
loadInventory,
loadMovements
} = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = ref<any>({})
const orderItems = ref<any[]>([])
const purchaseWarehouseId = ref<number | null>(null)
const showReceiveModal = ref(false)
const receivingOrder = ref<any>(null)
const receiveItems = ref<any[]>([])
const receiveWarehouseId = ref<number | null>(null)
const receiveRemark = ref('')
function openCreateOrder() {
editingItem.value = null
form.value = {
supplier_id: null,
expected_date: '',
remark: ''
}
orderItems.value = []
showModal.value = true
}
function addOrderItem() {
orderItems.value.push({
product_id: state.materials[0]?.id || null,
quantity: 1,
remark: ''
})
}
function removeOrderItem(index: number) {
orderItems.value.splice(index, 1)
}
const orderTotalAmount = computed(() => {
return orderItems.value.reduce((sum: number, item: any) => {
const material = state.materials.find((m: any) => m.id === item.product_id)
const unitPrice = material?.cost_price || 0
return sum + unitPrice * (item.quantity || 0)
}, 0)
})
async function editOrder(order: any) {
await loadMaterials()
await loadSuppliers()
editingItem.value = order
form.value = {
supplier_id: order.supplier_id || null,
expected_date: order.expected_date || '',
remark: order.remark || ''
}
try {
const detail = await apiRequest(`/api/purchase-orders/${order.id}`)
orderItems.value = (detail.items || []).map((item: any) => ({
product_id: item.product_id,
quantity: item.quantity,
remark: item.remark || ''
}))
} catch (e) {
handleApiError(e, '加载采购订单详情')
}
showModal.value = true
}
async function savePurchaseOrder() {
if (!form.value.supplier_id) {
addNotification('请选择供应商', 'warning')
return
}
if (!orderItems.value || orderItems.value.length === 0) {
addNotification('请至少添加一个物料', 'warning')
return
}
for (const [i, item] of orderItems.value.entries()) {
if (!item.product_id) {
addNotification(`第 ${i + 1} 行:请选择物料`, 'warning')
return
}
if (!item.quantity || item.quantity <= 0) {
addNotification(`第 ${i + 1} 行:数量必须大于 0`, 'warning')
return
}
}
try {
const payload = {
supplier_id: form.value.supplier_id,
expected_date: form.value.expected_date || null,
remark: form.value.remark || '',
items: orderItems.value.map((item: any) => ({
product_id: item.product_id,
quantity: item.quantity,
remark: item.remark
}))
}
if (editingItem.value) {
await apiRequest(`/api/purchase-orders/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('采购订单更新成功', 'success')
} else {
await apiRequest('/api/purchase-orders', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('采购订单创建成功', 'success')
}
showModal.value = false
editingItem.value = null
form.value = {}
orderItems.value = []
loadPurchaseOrders()
} catch (e) {
handleApiError(e, '保存采购订单')
}
}
async function deleteOrder(id: number) {
if (!confirm('确定要删除这个采购订单吗?')) return
try {
await apiRequest(`/api/purchase-orders/${id}`, { method: 'DELETE' })
addNotification('采购订单已删除', 'success')
loadPurchaseOrders()
} catch (e) {
handleApiError(e, '删除采购订单')
}
}
async function openReceiveDialog(order: any) {
try {
const detail = await apiRequest(`/api/purchase-orders/${order.id}`)
receivingOrder.value = order
receiveItems.value = (detail.items || []).map((item: any) => ({
id: item.id,
material_label: item.product_name || item.product_sku || '',
quantity: item.quantity || 0,
received_quantity: item.received_quantity || 0,
remaining: (item.quantity || 0) - (item.received_quantity || 0),
receive_quantity: 0
}))
receiveWarehouseId.value = purchaseWarehouseId.value || state.warehouses[0]?.id || null
receiveRemark.value = ''
showReceiveModal.value = true
} catch (e) {
handleApiError(e, '加载采购订单详情')
}
}
async function receivePurchaseOrder() {
if (!receiveWarehouseId.value) {
addNotification('请选择入库仓库', 'warning')
return
}
const items = receiveItems.value
.filter((item: any) => item.receive_quantity > 0)
.map((item: any) => ({
item_id: item.id,
quantity: item.receive_quantity
}))
if (items.length === 0) {
addNotification('请输入本次入库数量', 'warning')
return
}
try {
await apiRequest(`/api/purchase-orders/${receivingOrder.value.id}/receive`, {
method: 'POST',
body: JSON.stringify({
warehouse_id: receiveWarehouseId.value,
remark: receiveRemark.value,
items
})
})
addNotification('入库完成', 'success')
showReceiveModal.value = false
receivingOrder.value = null
receiveItems.value = []
loadPurchaseOrders()
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '采购入库')
}
}
async function markAsPaid(orderId: number) {
if (!confirm('确认标记为已付款?')) return
try {
await apiRequest(`/api/purchase-orders/${orderId}/status`, {
method: 'PATCH',
body: JSON.stringify({ status: 'paid' })
})
addNotification('已标记为已付款', 'success')
loadPurchaseOrders()
loadMovements()
} catch (e) {
handleApiError(e, '更新状态')
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
orderItems.value = []
}
function closeReceiveModal() {
showReceiveModal.value = false
receivingOrder.value = null
receiveItems.value = []
}
async function initData() {
await loadPurchaseOrders()
await loadMaterials()
await loadSuppliers()
}
initData()
</script>
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-select
v-model="purchaseWarehouseId"
placeholder="选择仓库"
style="width: 200px;"
clearable
>
<el-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
<el-button @click="loadPurchaseOrders">刷新</el-button>
<el-button type="primary" @click="openCreateOrder">新增采购订单</el-button>
</div>
<el-table :data="state.purchaseOrders" v-loading="state.loading" stripe>
<el-table-column prop="order_no" label="采购单" />
<el-table-column prop="supplier_name" label="供应商" />
<el-table-column label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'received' ? 'success' : row.status === 'paid' ? 'info' : 'warning'">
{{ getPurchaseOrderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="订单创建">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="预计到货">
<template #default="{ row }">{{ formatDate(row.expected_date) }}</template>
</el-table-column>
<el-table-column label="实际到货">
<template #default="{ row }">{{ row.received_at ? formatDateTime(row.received_at) : '-' }}</template>
</el-table-column>
<el-table-column label="实际付款">
<template #default="{ row }">{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}</template>
</el-table-column>
<el-table-column label="总金额">
<template #default="{ row }">{{ formatCurrency(row.total_amount) }}</template>
</el-table-column>
<el-table-column label="已付款">
<template #default="{ row }">{{ formatCurrency(row.paid_amount || 0) }}</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button
type="primary"
size="small"
:disabled="isPurchaseOrderLocked(row.status)"
@click="editOrder(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
:disabled="isPurchaseOrderLocked(row.status)"
@click="deleteOrder(row.id)"
>
删除
</el-button>
<el-button
v-if="row.status === 'pending'"
type="success"
size="small"
@click="openReceiveDialog(row)"
>
到货入库
</el-button>
<el-button
v-if="row.status === 'received'"
type="warning"
size="small"
@click="markAsPaid(row.id)"
>
标记为已付款
</el-button>
</template>
</el-table-column>
<el-empty v-if="!state.loading && state.purchaseOrders.length === 0" description="暂无采购订单" />
</el-table>
<el-dialog
v-model="showModal"
:title="(editingItem ? '编辑' : '新增') + '采购订单'"
width="800px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="供应商" required>
<el-select v-model="form.supplier_id" 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-item label="预计到货">
<el-date-picker
v-model="form.expected_date"
type="date"
placeholder="选择日期"
style="width:100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="备注信息" />
</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="addOrderItem">+ 添加物料</el-button>
</div>
<el-table :data="orderItems" border size="small">
<el-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.product_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="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number
:model-value="(state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0)"
disabled
size="small"
style="width:100%"
/>
</template>
</el-table-column>
<el-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((state.materials.find((m: any) => m.id === line.product_id)?.cost_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="removeOrderItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="savePurchaseOrder">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="showReceiveModal"
title="采购到货入库"
width="700px"
@closed="closeReceiveModal"
>
<el-form label-width="100px">
<el-form-item label="入库仓库" required>
<el-select v-model="receiveWarehouseId" placeholder="请选择仓库" style="width:100%">
<el-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="receiveRemark" placeholder="入库备注" />
</el-form-item>
</el-form>
<el-table :data="receiveItems" border size="small">
<el-table-column prop="material_label" label="物料" />
<el-table-column prop="id" label="明细ID" width="80" />
<el-table-column label="剩余待入库" width="100">
<template #default="{ row: item }">{{ item.remaining }}</template>
</el-table-column>
<el-table-column label="本次入库" width="140">
<template #default="{ row: item }">
<el-input-number
v-model="item.receive_quantity"
:min="0"
:max="item.remaining"
size="small"
style="width:100%"
/>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showReceiveModal = false">取消</el-button>
<el-button type="primary" @click="receivePurchaseOrder">确认入库</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>