优化前端,使用组件Element

This commit is contained in:
2026-06-02 15:56:04 +08:00
parent cafa3ef883
commit 65033666ef
21 changed files with 820 additions and 16 deletions
@@ -0,0 +1,538 @@
<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,
getSalesOrderStatusLabel,
loadProductionOrders,
loadFinishedProducts,
loadCustomers,
loadMaterials,
loadInventory,
loadMovements
} = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = ref<any>({})
const moldItems = ref<any[]>([])
const productionWarehouseId = ref<number | null>(null)
const showConsumptionModal = ref(false)
const consumptionItems = ref<any[]>([])
const consumedMaterials = ref<any[]>([])
function openCreateOrder() {
editingItem.value = null
form.value = {
customer_id: null,
delivery_date: '',
remark: ''
}
moldItems.value = []
consumedMaterials.value = []
showModal.value = true
}
function addMoldItem() {
moldItems.value.push({
mold_mode: 'new',
mold_sku: '',
mold_name: '',
mold_id: null,
quantity: 1,
unit_price: 0,
remark: ''
})
}
function removeMoldItem(index: number) {
moldItems.value.splice(index, 1)
}
const orderTotalAmount = computed(() => {
return moldItems.value.reduce((sum: number, item: any) => {
return sum + (item.unit_price || 0) * (item.quantity || 0)
}, 0)
})
async function editOrder(order: any) {
await loadCustomers()
await loadFinishedProducts()
await loadMaterials()
try {
const detail = await apiRequest(`/api/sales-orders/${order.id}`)
editingItem.value = order
form.value = {
customer_id: detail.customer_id || order.customer_id || null,
delivery_date: detail.delivery_date || order.delivery_date || '',
remark: detail.remark || order.remark || ''
}
moldItems.value = (detail.items || []).map((item: any) => ({
mold_mode: item.mold_id ? 'existing' : 'new',
mold_sku: item.sku || '',
mold_name: item.name || '',
mold_id: item.mold_id || null,
quantity: item.quantity || 1,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
try {
const consumptionData = await apiRequest(`/api/sales-orders/${order.id}/consume-materials`)
consumedMaterials.value = (consumptionData.items || []).map((item: any) => ({
material_id: item.material_id,
material_name: item.material_name || item.material_sku || '',
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
} catch {
consumedMaterials.value = []
}
} catch (e) {
handleApiError(e, '加载销售订单详情')
}
showModal.value = true
}
async function saveSalesOrder() {
if (!form.value.customer_id) {
addNotification('请选择客户', 'warning')
return
}
if (!moldItems.value || moldItems.value.length === 0) {
addNotification('请至少添加一个模具', 'warning')
return
}
try {
const payload = {
customer_id: form.value.customer_id,
delivery_date: form.value.delivery_date || null,
remark: form.value.remark || '',
items: moldItems.value.map((item: any) => ({
mold_mode: item.mold_mode,
mold_sku: item.mold_sku || '',
mold_name: item.mold_name || '',
mold_id: item.mold_id || null,
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
}
if (editingItem.value) {
await apiRequest(`/api/sales-orders/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('销售订单更新成功', 'success')
} else {
await apiRequest('/api/sales-orders', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('销售订单创建成功', 'success')
}
showModal.value = false
editingItem.value = null
form.value = {}
moldItems.value = []
consumedMaterials.value = []
loadProductionOrders()
loadFinishedProducts()
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '保存销售订单')
}
}
async function deleteOrder(id: number) {
if (!confirm('确定要删除这个销售订单吗?')) return
try {
await apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' })
addNotification('销售订单已删除', 'success')
loadProductionOrders()
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '删除销售订单')
}
}
async function updateStatus(orderId: number, status: string) {
const label = status === 'delivered' ? '已交付' : '已收款'
if (!confirm(`确认标记为${label}?`)) return
try {
await apiRequest(`/api/sales-orders/${orderId}/status`, {
method: 'PATCH',
body: JSON.stringify({ status })
})
addNotification(`已标记为${label}`, 'success')
loadProductionOrders()
loadMovements()
} catch (e) {
handleApiError(e, '更新状态')
}
}
function openConsumptionModal() {
consumptionItems.value = []
showConsumptionModal.value = true
}
function addConsumptionItem() {
consumptionItems.value.push({
material_id: state.materials[0]?.id || null,
quantity: 1,
remark: ''
})
}
function removeConsumptionItem(index: number) {
consumptionItems.value.splice(index, 1)
}
const consumptionTotalAmount = computed(() => {
return consumptionItems.value.reduce((sum: number, item: any) => {
const material = state.materials.find((m: any) => m.id === item.material_id)
const unitPrice = material?.cost_price || 0
return sum + unitPrice * (item.quantity || 0)
}, 0)
})
async function saveConsumption() {
if (!editingItem.value) return
if (!consumptionItems.value || consumptionItems.value.length === 0) {
addNotification('请至少添加一个消耗物料', 'warning')
return
}
try {
const payload = {
items: consumptionItems.value.map((item: any) => ({
material_id: item.material_id,
quantity: item.quantity,
unit_price: state.materials.find((m: any) => m.id === item.material_id)?.cost_price || 0,
remark: item.remark || ''
}))
}
await apiRequest(`/api/sales-orders/${editingItem.value.id}/consume-materials`, {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('物料消耗记录保存成功', 'success')
showConsumptionModal.value = false
try {
const consumptionData = await apiRequest(`/api/sales-orders/${editingItem.value.id}/consume-materials`)
consumedMaterials.value = (consumptionData.items || []).map((item: any) => ({
material_id: item.material_id,
material_name: item.material_name || item.material_sku || '',
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
} catch {
consumedMaterials.value = []
}
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '保存物料消耗')
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
moldItems.value = []
consumedMaterials.value = []
}
function closeConsumptionModal() {
showConsumptionModal.value = false
consumptionItems.value = []
}
async function initData() {
await loadProductionOrders()
await loadCustomers()
await loadMaterials()
await loadFinishedProducts()
}
initData()
</script>
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-select
v-model="productionWarehouseId"
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="loadProductionOrders">刷新</el-button>
<el-button type="primary" @click="openCreateOrder">新增销售订单</el-button>
</div>
<el-table :data="state.productionOrders" v-loading="state.loading" stripe>
<el-table-column prop="order_no" label="销售单" />
<el-table-column prop="customer_name" label="客户" />
<el-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.total_amount) }}</template>
</el-table-column>
<el-table-column label="交付日期">
<template #default="{ row }">{{ formatDate(row.delivery_date) }}</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 }">{{ row.delivered_at ? formatDateTime(row.delivered_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 }">
<el-tag :type="row.status === 'paid' ? 'success' : row.status === 'delivered' ? 'warning' : 'info'">
{{ getSalesOrderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240">
<template #default="{ row }">
<el-dropdown style="margin-right: 8px;">
<el-button size="small">
状态 ▾
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="updateStatus(row.id, 'delivered')">已交付</el-dropdown-item>
<el-dropdown-item @click="updateStatus(row.id, 'paid')">已收款</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
type="primary"
size="small"
:disabled="row.status === 'paid'"
@click="editOrder(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="deleteOrder(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
<el-empty v-if="!state.loading && state.productionOrders.length === 0" description="暂无销售订单" />
</el-table>
<el-dialog
v-model="showModal"
:title="(editingItem ? '编辑' : '新增') + '销售订单'"
width="900px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="客户" required>
<el-select v-model="form.customer_id" placeholder="请选择客户" style="width:100%">
<el-option
v-for="customer in state.customers"
:key="customer.id"
:label="customer.name"
:value="customer.id"
/>
</el-select>
</el-form-item>
<el-form-item label="交付日期">
<el-date-picker
v-model="form.delivery_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>
<div style="display: flex; gap: 8px;">
<el-button v-if="editingItem" type="warning" size="small" @click="openConsumptionModal">+ 消耗物料</el-button>
<el-button type="primary" size="small" @click="addMoldItem">+ 添加模具</el-button>
</div>
</div>
<el-table :data="moldItems" border size="small">
<el-table-column label="模式" width="80">
<template #default="{ row: line }">
<el-select v-model="line.mold_mode" size="small" style="width:100%">
<el-option label="新模" value="new" />
<el-option label="改模" value="existing" />
</el-select>
</template>
</el-table-column>
<el-table-column label="模具SKU" min-width="120">
<template #default="{ row: line }">
<el-input v-if="line.mold_mode === 'new'" v-model="line.mold_sku" size="small" placeholder="模具SKU" />
<el-select v-else v-model="line.mold_id" placeholder="选择模具" size="small" style="width:100%">
<el-option
v-for="product in state.finishedProducts"
:key="product.id"
:label="`${product.sku} - ${product.name}`"
:value="product.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="模具名称" min-width="120">
<template #default="{ row: line }">
<el-input
v-if="line.mold_mode === 'new'"
v-model="line.mold_name"
size="small"
placeholder="模具名称"
/>
<span v-else style="padding-left: 8px;">
{{ state.finishedProducts.find((p: any) => p.id === line.mold_id)?.name || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="数量" width="100">
<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 }">
<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="removeMoldItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div>
<template v-if="editingItem && consumedMaterials.length > 0">
<el-divider content-position="left">已消耗物料</el-divider>
<el-table :data="consumedMaterials" border size="small">
<el-table-column label="物料">
<template #default="{ row: item }">{{ item.material_name }}</template>
</el-table-column>
<el-table-column label="数量">
<template #default="{ row: item }">{{ item.quantity }}</template>
</el-table-column>
<el-table-column label="单价">
<template #default="{ row: item }">{{ formatCurrency(item.unit_price) }}</template>
</el-table-column>
<el-table-column label="备注">
<template #default="{ row: item }">{{ item.remark || '-' }}</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveSalesOrder">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="showConsumptionModal"
title="物料消耗"
width="800px"
@closed="closeConsumptionModal"
>
<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="addConsumptionItem">+ 添加物料</el-button>
</div>
<el-table :data="consumptionItems" 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="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.material_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.material_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="removeConsumptionItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
消耗总金额:{{ formatCurrency(consumptionTotalAmount) }}
</div>
<template #footer>
<el-button @click="showConsumptionModal = false">取消</el-button>
<el-button type="primary" @click="saveConsumption">保存消耗</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>