2713 lines
99 KiB
Vue
2713 lines
99 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { reactive, ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|||
|
|
import { useRoute, useRouter } from 'vue-router'
|
|||
|
|
import { useAppStore } from '@/stores/app'
|
|||
|
|
import { apiRequest } from '@/shared/api'
|
|||
|
|
import { addNotification, handleApiError } from '@/shared/notification'
|
|||
|
|
import { formatFileSize, formatDateTime, formatDate, formatCurrency, formatNumber } from '@/shared/utils'
|
|||
|
|
|
|||
|
|
declare const AirDatepicker: any
|
|||
|
|
|
|||
|
|
const store = useAppStore()
|
|||
|
|
const router = useRouter()
|
|||
|
|
const route = useRoute()
|
|||
|
|
|
|||
|
|
const deliveryDateInput = ref<HTMLElement | null>(null)
|
|||
|
|
const expectedDateInput = ref<HTMLElement | null>(null)
|
|||
|
|
const deliveryDateNativeInput = ref<HTMLElement | null>(null)
|
|||
|
|
const expectedDateNativeInput = ref<HTMLElement | null>(null)
|
|||
|
|
|
|||
|
|
const state = reactive({
|
|||
|
|
activeTab: 'dashboard' as string,
|
|||
|
|
backendDbReady: true,
|
|||
|
|
backendDbMessage: '' as string,
|
|||
|
|
productCategory: 'finished' as string,
|
|||
|
|
dashboard: null as any,
|
|||
|
|
financeSummary: null as any,
|
|||
|
|
financePeriod: {
|
|||
|
|
year: new Date().getFullYear(),
|
|||
|
|
quarter: '' as string
|
|||
|
|
},
|
|||
|
|
financeTransactions: [] as any[],
|
|||
|
|
receivables: [] as any[],
|
|||
|
|
payables: [] as any[],
|
|||
|
|
customerFinanceStatement: [] as any[],
|
|||
|
|
supplierFinanceStatement: [] as any[],
|
|||
|
|
customerProductStatement: [] as any[],
|
|||
|
|
supplierProductStatement: [] as any[],
|
|||
|
|
products: [] as any[],
|
|||
|
|
materials: [] as any[],
|
|||
|
|
finishedProducts: [] as any[],
|
|||
|
|
purchaseOrders: [] as any[],
|
|||
|
|
purchaseWarehouseId: null as number | null,
|
|||
|
|
purchaseReceiveItems: [] as any[],
|
|||
|
|
productionOrders: [] as any[],
|
|||
|
|
productionPlan: null as any,
|
|||
|
|
productionWarehouseId: null as number | null,
|
|||
|
|
suppliers: [] as any[],
|
|||
|
|
customers: [] as any[],
|
|||
|
|
warehouses: [] as any[],
|
|||
|
|
inventory: [] as any[],
|
|||
|
|
movements: [] as any[],
|
|||
|
|
loading: false,
|
|||
|
|
showModal: false,
|
|||
|
|
modalType: '' as string,
|
|||
|
|
editingItem: null as any,
|
|||
|
|
productBomItems: [] as any[],
|
|||
|
|
materialConsumptionItems: [] as any[],
|
|||
|
|
showMaterialConsumptionModal: false,
|
|||
|
|
consumedMaterials: [] as any[],
|
|||
|
|
restockItems: [] as any[],
|
|||
|
|
showRestockModal: false,
|
|||
|
|
form: {} as any
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const parseDateTimeLocal = (text: string | null | undefined): Date | null => {
|
|||
|
|
if (!text) return null
|
|||
|
|
const raw = String(text).trim()
|
|||
|
|
const normalized = raw.replace('T', ' ').slice(0, 16)
|
|||
|
|
const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2})$/)
|
|||
|
|
if (!m) return null
|
|||
|
|
const year = Number(m[1])
|
|||
|
|
const month = Number(m[2])
|
|||
|
|
const day = Number(m[3])
|
|||
|
|
const hour = Number(m[4])
|
|||
|
|
const minute = Number(m[5])
|
|||
|
|
if (!Number.isFinite(year + month + day + hour + minute)) return null
|
|||
|
|
return new Date(year, month - 1, day, hour, minute, 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toPickerValue = (value: any): string => {
|
|||
|
|
if (!value) return ''
|
|||
|
|
const raw = String(value).trim()
|
|||
|
|
if (/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}/.test(raw)) return raw.slice(0, 16)
|
|||
|
|
if (raw.includes('T')) return raw.replace('T', ' ').slice(0, 16)
|
|||
|
|
const dt = new Date(raw)
|
|||
|
|
if (!Number.isFinite(dt.getTime())) return ''
|
|||
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|||
|
|
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toApiDateTime = (value: any): string | null => {
|
|||
|
|
if (!value) return null
|
|||
|
|
const text = String(value).trim()
|
|||
|
|
if (text.includes('T')) return text.split('T')[0]
|
|||
|
|
if (text.includes(' ')) return text.split(' ')[0]
|
|||
|
|
if (text.length === 10) return text
|
|||
|
|
if (value instanceof Date) {
|
|||
|
|
const year = value.getFullYear()
|
|||
|
|
const month = String(value.getMonth() + 1).padStart(2, '0')
|
|||
|
|
const day = String(value.getDate()).padStart(2, '0')
|
|||
|
|
return `${year}-${month}-${day}`
|
|||
|
|
}
|
|||
|
|
return text
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toNativeValue = (value: any): string => {
|
|||
|
|
if (!value) return ''
|
|||
|
|
const text = String(value).trim()
|
|||
|
|
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
|||
|
|
const isoText = text.replace(' ', 'T')
|
|||
|
|
return isoText.length >= 16 ? isoText.slice(0, 16) : isoText
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const fromNativeValue = (value: any): string => {
|
|||
|
|
if (!value) return ''
|
|||
|
|
const text = String(value).trim()
|
|||
|
|
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
|||
|
|
return text.replace('T', ' ').slice(0, 16)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let deliveryPicker: any = null
|
|||
|
|
let expectedPicker: any = null
|
|||
|
|
|
|||
|
|
const destroyPickers = () => {
|
|||
|
|
if (deliveryPicker) {
|
|||
|
|
deliveryPicker.destroy()
|
|||
|
|
deliveryPicker = null
|
|||
|
|
}
|
|||
|
|
if (expectedPicker) {
|
|||
|
|
expectedPicker.destroy()
|
|||
|
|
expectedPicker = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const initPickers = () => {
|
|||
|
|
destroyPickers()
|
|||
|
|
if (typeof AirDatepicker !== 'function') return
|
|||
|
|
|
|||
|
|
if (state.modalType === 'salesOrder' && deliveryDateInput.value) {
|
|||
|
|
deliveryPicker = new AirDatepicker(deliveryDateInput.value, {
|
|||
|
|
timepicker: false,
|
|||
|
|
autoClose: true,
|
|||
|
|
zIndex: 2005,
|
|||
|
|
dateFormat: 'yyyy-MM-dd',
|
|||
|
|
onSelect: ({ formattedDate }: any) => {
|
|||
|
|
state.form.delivery_date = formattedDate || ''
|
|||
|
|
state.form.delivery_date_native = toNativeValue(formattedDate || '')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
const initial = parseDateTimeLocal(state.form.delivery_date)
|
|||
|
|
if (initial) deliveryPicker.selectDate(initial, { silent: true })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (state.modalType === 'purchaseOrder' && expectedDateInput.value) {
|
|||
|
|
expectedPicker = new AirDatepicker(expectedDateInput.value, {
|
|||
|
|
timepicker: false,
|
|||
|
|
autoClose: true,
|
|||
|
|
zIndex: 2005,
|
|||
|
|
dateFormat: 'yyyy-MM-dd',
|
|||
|
|
onSelect: ({ formattedDate }: any) => {
|
|||
|
|
state.form.expected_date = formattedDate || ''
|
|||
|
|
state.form.expected_date_native = toNativeValue(formattedDate || '')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
const initial = parseDateTimeLocal(state.form.expected_date)
|
|||
|
|
if (initial) expectedPicker.selectDate(initial, { silent: true })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openDateTimePicker = (pickerKind: string) => {
|
|||
|
|
if (pickerKind === 'delivery' && deliveryPicker) {
|
|||
|
|
deliveryPicker.show()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (pickerKind === 'expected' && expectedPicker) {
|
|||
|
|
expectedPicker.show()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const nativeInput = pickerKind === 'delivery' ? deliveryDateNativeInput.value : expectedDateNativeInput.value
|
|||
|
|
if (!nativeInput) return
|
|||
|
|
if (typeof (nativeInput as any).showPicker === 'function') {
|
|||
|
|
(nativeInput as any).showPicker()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
nativeInput.focus()
|
|||
|
|
nativeInput.click()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getMovementTypeLabel = (movementType: string): string => {
|
|||
|
|
const movementLabelMap: Record<string, string> = {
|
|||
|
|
in: '其他入库',
|
|||
|
|
out: '其他出库',
|
|||
|
|
adjust: '库存调整',
|
|||
|
|
purchase_in: '采购入库',
|
|||
|
|
return_from_production: '生产退料入库',
|
|||
|
|
outsource_return: '外协回库',
|
|||
|
|
finish_in: '完工入库',
|
|||
|
|
issue_to_production: '生产领料出库',
|
|||
|
|
outsource_send: '外协发料出库',
|
|||
|
|
shipment_out: '销售出库',
|
|||
|
|
scrap_out: '报废出库'
|
|||
|
|
}
|
|||
|
|
return movementLabelMap[movementType] || movementType
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getMovementBadgeClass = (movementType: string): string => {
|
|||
|
|
if (['purchase_in', 'return_from_production', 'outsource_return', 'finish_in', 'in'].includes(movementType)) {
|
|||
|
|
return 'badge-success'
|
|||
|
|
}
|
|||
|
|
if (['issue_to_production', 'outsource_send', 'shipment_out', 'scrap_out', 'out'].includes(movementType)) {
|
|||
|
|
return 'badge-error'
|
|||
|
|
}
|
|||
|
|
return 'badge-warning'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getPurchaseOrderStatusLabel = (status: string): string => {
|
|||
|
|
const statusMap: Record<string, string> = {
|
|||
|
|
draft: '已下单',
|
|||
|
|
pending: '已下单',
|
|||
|
|
received: '已收货',
|
|||
|
|
paid: '已付款'
|
|||
|
|
}
|
|||
|
|
return statusMap[status] || status
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isPurchaseOrderLocked = (status: string): boolean => {
|
|||
|
|
return ['received', 'paid'].includes(status)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadDashboard = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.dashboard = await apiRequest('/api/dashboard')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载仪表盘')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadFinishedProducts = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.finishedProducts = await apiRequest('/api/products?item_type=finished&limit=100')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载成品')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadProducts = async () => {
|
|||
|
|
await loadFinishedProducts()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadMaterials = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.materials = await apiRequest('/api/products?item_type=material&limit=100')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载物料')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadWarehouses = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.warehouses = await apiRequest('/api/warehouses')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载仓库')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const ensureStockBaseData = async () => {
|
|||
|
|
if (!state.materials.length) {
|
|||
|
|
await loadMaterials()
|
|||
|
|
}
|
|||
|
|
if (!state.warehouses.length) {
|
|||
|
|
await loadWarehouses()
|
|||
|
|
}
|
|||
|
|
if (!state.warehouses.length) {
|
|||
|
|
try {
|
|||
|
|
await apiRequest('/api/warehouses', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify({ name: '默认仓库' })
|
|||
|
|
})
|
|||
|
|
await loadWarehouses()
|
|||
|
|
addNotification('已自动创建默认仓库', 'success')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '自动创建默认仓库')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadSuppliers = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.suppliers = await apiRequest('/api/suppliers')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载供应商')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadProductionOrders = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
const [orders, warehouses] = await Promise.all([
|
|||
|
|
apiRequest('/api/sales-orders?limit=100'),
|
|||
|
|
apiRequest('/api/warehouses')
|
|||
|
|
])
|
|||
|
|
state.productionOrders = orders?.items || []
|
|||
|
|
state.warehouses = warehouses || []
|
|||
|
|
if (!state.productionWarehouseId) {
|
|||
|
|
state.productionWarehouseId = state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载按单生产数据')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadPurchaseOrders = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
const [orders, warehouses] = await Promise.all([
|
|||
|
|
apiRequest('/api/purchase-orders?limit=100'),
|
|||
|
|
apiRequest('/api/warehouses')
|
|||
|
|
])
|
|||
|
|
state.purchaseOrders = orders?.items || []
|
|||
|
|
state.warehouses = warehouses || []
|
|||
|
|
if (!state.purchaseWarehouseId) {
|
|||
|
|
state.purchaseWarehouseId = state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载采购订单')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadCustomers = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
state.customers = await apiRequest('/api/customers')
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载客户')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadInventory = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
const data = await apiRequest('/api/inventory')
|
|||
|
|
state.inventory = data?.items || []
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载库存')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadMovements = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
const data = await apiRequest('/api/stock-movements')
|
|||
|
|
state.movements = data?.items || []
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载变动记录')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadFinance = async () => {
|
|||
|
|
state.loading = true
|
|||
|
|
try {
|
|||
|
|
const selectedYear = Number(state.financePeriod.year) || new Date().getFullYear()
|
|||
|
|
const selectedQuarter = state.financePeriod.quarter ? Number(state.financePeriod.quarter) : null
|
|||
|
|
const periodQuery = selectedQuarter
|
|||
|
|
? `year=${selectedYear}&quarter=${selectedQuarter}`
|
|||
|
|
: `year=${selectedYear}`
|
|||
|
|
|
|||
|
|
const [
|
|||
|
|
summary,
|
|||
|
|
transactions,
|
|||
|
|
receivables,
|
|||
|
|
payables,
|
|||
|
|
customerStatement,
|
|||
|
|
supplierStatement,
|
|||
|
|
customerProductStatement,
|
|||
|
|
supplierProductStatement
|
|||
|
|
] = await Promise.all([
|
|||
|
|
apiRequest(`/api/finance/summary?${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/transactions?status=confirmed&limit=20&${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/receivables?limit=20&${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/payables?limit=20&${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/partner-statement/customer?${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/partner-statement/supplier?${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/partner-product-statement/customer?${periodQuery}`),
|
|||
|
|
apiRequest(`/api/finance/partner-product-statement/supplier?${periodQuery}`)
|
|||
|
|
])
|
|||
|
|
state.financeSummary = summary
|
|||
|
|
state.financeTransactions = transactions
|
|||
|
|
state.receivables = receivables
|
|||
|
|
state.payables = payables
|
|||
|
|
state.customerFinanceStatement = customerStatement.items || []
|
|||
|
|
state.supplierFinanceStatement = supplierStatement.items || []
|
|||
|
|
state.customerProductStatement = customerProductStatement.items || []
|
|||
|
|
state.supplierProductStatement = supplierProductStatement.items || []
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '加载财务数据')
|
|||
|
|
} finally {
|
|||
|
|
state.loading = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const refreshFinanceByPeriod = () => {
|
|||
|
|
if (state.activeTab === 'finance') {
|
|||
|
|
loadFinance()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const switchTab = (tab: string) => {
|
|||
|
|
state.activeTab = tab
|
|||
|
|
if (!state.backendDbReady) return
|
|||
|
|
switch (tab) {
|
|||
|
|
case 'dashboard': loadDashboard(); break
|
|||
|
|
case 'sales_orders': loadProductionOrders(); break
|
|||
|
|
case 'products': loadFinishedProducts(); break
|
|||
|
|
case 'materials': loadMaterials(); break
|
|||
|
|
case 'purchases': loadPurchaseOrders(); break
|
|||
|
|
case 'inventory': loadInventory(); break
|
|||
|
|
case 'customers': loadCustomers(); break
|
|||
|
|
case 'suppliers': loadSuppliers(); break
|
|||
|
|
case 'finance': loadFinance(); break
|
|||
|
|
case 'movements': loadMovements(); break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const menuGroups = [
|
|||
|
|
{
|
|||
|
|
key: 'overview',
|
|||
|
|
title: '概览',
|
|||
|
|
items: [{ key: 'dashboard', label: '仪表盘' }]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'sales',
|
|||
|
|
title: '销售',
|
|||
|
|
items: [{ key: 'sales_orders', label: '销售订单管理' }]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'purchase',
|
|||
|
|
title: '采购',
|
|||
|
|
items: [{ key: 'purchases', label: '采购订单管理' }]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'product',
|
|||
|
|
title: '产品',
|
|||
|
|
items: [
|
|||
|
|
{ key: 'products', label: '成品管理' },
|
|||
|
|
{ key: 'materials', label: '物料管理' }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'partner',
|
|||
|
|
title: '往来单位',
|
|||
|
|
items: [
|
|||
|
|
{ key: 'customers', label: '客户管理' },
|
|||
|
|
{ key: 'suppliers', label: '供应商管理' }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'warehouse',
|
|||
|
|
title: '仓库',
|
|||
|
|
items: [
|
|||
|
|
{ key: 'inventory', label: '库存管理' },
|
|||
|
|
{ key: 'movements', label: '库存变动记录' }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'finance',
|
|||
|
|
title: '财务',
|
|||
|
|
items: [{ key: 'finance', label: '财务概览' }]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const openGroups = reactive<Record<string, boolean>>(
|
|||
|
|
Object.fromEntries(menuGroups.map(g => [g.key, true]))
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const toggleGroup = (groupKey: string) => {
|
|||
|
|
openGroups[groupKey] = !openGroups[groupKey]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const activeMenu = computed(() => {
|
|||
|
|
for (const group of menuGroups) {
|
|||
|
|
const item = group.items.find(i => i.key === state.activeTab)
|
|||
|
|
if (item) return { group, item }
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const handleMenuClick = (itemKey: string) => {
|
|||
|
|
switchTab(itemKey)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const switchProductCategory = (category: string) => {
|
|||
|
|
state.productCategory = category
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const checkBackendHealth = async () => {
|
|||
|
|
try {
|
|||
|
|
const resp = await fetch('/health', { method: 'GET' })
|
|||
|
|
if (!resp.ok) {
|
|||
|
|
state.backendDbReady = false
|
|||
|
|
state.backendDbMessage = '后端服务异常,暂无法加载业务数据'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const health = await resp.json().catch(() => null)
|
|||
|
|
if (health && health.database_connected === false) {
|
|||
|
|
state.backendDbReady = false
|
|||
|
|
state.backendDbMessage = '数据库未连接,当前仅可浏览界面,业务数据暂不可用'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
state.backendDbReady = true
|
|||
|
|
state.backendDbMessage = ''
|
|||
|
|
} catch {
|
|||
|
|
state.backendDbReady = false
|
|||
|
|
state.backendDbMessage = '无法连接后端服务'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openCreateProduct = (itemType: string) => {
|
|||
|
|
state.modalType = 'product'
|
|||
|
|
state.editingItem = null
|
|||
|
|
state.form = {
|
|||
|
|
item_type: itemType,
|
|||
|
|
unit: itemType === 'material' ? 'kg' : '件',
|
|||
|
|
min_stock: 0,
|
|||
|
|
max_stock: 1000,
|
|||
|
|
cost_price: 0,
|
|||
|
|
sale_price: 0
|
|||
|
|
}
|
|||
|
|
state.showModal = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openModal = async (type: string, item: any = null) => {
|
|||
|
|
state.modalType = type
|
|||
|
|
state.editingItem = item
|
|||
|
|
if (item) {
|
|||
|
|
if (type === 'salesOrder') {
|
|||
|
|
if (item.status === 'paid') {
|
|||
|
|
addNotification('已收款的销售订单禁止修改', 'warning')
|
|||
|
|
state.modalType = ''
|
|||
|
|
state.editingItem = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await loadCustomers()
|
|||
|
|
await loadFinishedProducts()
|
|||
|
|
const detail = await apiRequest(`/api/sales-orders/${item.id}`)
|
|||
|
|
const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${item.id}&movement_type=consumption`)
|
|||
|
|
state.consumedMaterials = (movements || []).map((movement: any) => ({
|
|||
|
|
material_name: movement.product_name || movement.product_sku || '未知物料',
|
|||
|
|
quantity: Math.abs(movement.quantity),
|
|||
|
|
unit_price: movement.unit_price || 0,
|
|||
|
|
amount: movement.total_amount || 0
|
|||
|
|
}))
|
|||
|
|
state.form = {
|
|||
|
|
customer_id: detail.customer_id,
|
|||
|
|
delivery_date: toPickerValue(detail.delivery_date),
|
|||
|
|
delivery_date_native: toNativeValue(toPickerValue(detail.delivery_date)),
|
|||
|
|
remark: detail.remark || '',
|
|||
|
|
items: (detail.items || []).map((line: any) => ({
|
|||
|
|
mode: line.product_id ? 'existing' : 'new',
|
|||
|
|
product_id: line.product_id,
|
|||
|
|
product_sku: '',
|
|||
|
|
product_name: '',
|
|||
|
|
quantity: line.quantity,
|
|||
|
|
unit_price: line.unit_price,
|
|||
|
|
remark: line.remark || ''
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
} else if (type === 'purchaseOrder') {
|
|||
|
|
await loadSuppliers()
|
|||
|
|
await loadMaterials()
|
|||
|
|
const detail = await apiRequest(`/api/purchase-orders/${item.id}`)
|
|||
|
|
state.form = {
|
|||
|
|
supplier_id: detail.supplier_id,
|
|||
|
|
expected_date: toPickerValue(detail.expected_date),
|
|||
|
|
expected_date_native: toNativeValue(toPickerValue(detail.expected_date)),
|
|||
|
|
remark: detail.remark || '',
|
|||
|
|
items: (detail.items || []).map((line: any) => ({
|
|||
|
|
product_id: line.product_id,
|
|||
|
|
quantity: line.quantity,
|
|||
|
|
remark: line.remark || ''
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
} else if (type === 'purchaseReceive') {
|
|||
|
|
await loadWarehouses()
|
|||
|
|
const detail = await apiRequest(`/api/purchase-orders/${item.id}`)
|
|||
|
|
state.purchaseReceiveItems = (detail.items || [])
|
|||
|
|
.map((line: any) => ({
|
|||
|
|
item_id: line.id,
|
|||
|
|
material_label: `${line.product_sku || line.product_id} - ${line.product_name || ''}`.trim(),
|
|||
|
|
remaining_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0),
|
|||
|
|
receive_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0)
|
|||
|
|
}))
|
|||
|
|
.filter((line: any) => line.remaining_quantity > 0)
|
|||
|
|
state.form = {
|
|||
|
|
warehouse_id: state.purchaseWarehouseId || state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null,
|
|||
|
|
remark: ''
|
|||
|
|
}
|
|||
|
|
} else if (type === 'product') {
|
|||
|
|
state.form = { ...item }
|
|||
|
|
if (item.item_type === 'finished') {
|
|||
|
|
await loadMaterials()
|
|||
|
|
const bom = await apiRequest(`/api/products/${item.id}/materials`)
|
|||
|
|
state.productBomItems = (bom.items || []).map((bomItem: any) => ({
|
|||
|
|
material_id: bomItem.material_id,
|
|||
|
|
quantity: bomItem.quantity
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
state.form = { ...item }
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
state.form = {}
|
|||
|
|
if (type === 'inventoryItem') {
|
|||
|
|
await ensureStockBaseData()
|
|||
|
|
state.form = {
|
|||
|
|
product_id: state.materials[0]?.id || null,
|
|||
|
|
warehouse_id: state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null,
|
|||
|
|
quantity: 0,
|
|||
|
|
locked_quantity: 0,
|
|||
|
|
batch_number: '',
|
|||
|
|
location: ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'salesOrder') {
|
|||
|
|
state.form = {
|
|||
|
|
customer_id: null,
|
|||
|
|
delivery_date: '',
|
|||
|
|
delivery_date_native: '',
|
|||
|
|
remark: '',
|
|||
|
|
items: []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'purchaseOrder') {
|
|||
|
|
state.form = {
|
|||
|
|
supplier_id: null,
|
|||
|
|
expected_date: '',
|
|||
|
|
expected_date_native: '',
|
|||
|
|
remark: '',
|
|||
|
|
items: []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'product') {
|
|||
|
|
state.form = {
|
|||
|
|
item_type: 'finished',
|
|||
|
|
unit: '件',
|
|||
|
|
min_stock: 0,
|
|||
|
|
max_stock: 1000,
|
|||
|
|
cost_price: 0,
|
|||
|
|
sale_price: 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'salesOrder') {
|
|||
|
|
await loadCustomers()
|
|||
|
|
await loadFinishedProducts()
|
|||
|
|
state.form = {
|
|||
|
|
customer_id: state.customers[0]?.id || null,
|
|||
|
|
delivery_date: '',
|
|||
|
|
remark: '',
|
|||
|
|
items: [{
|
|||
|
|
mode: 'new',
|
|||
|
|
product_id: null,
|
|||
|
|
product_sku: '',
|
|||
|
|
product_name: '',
|
|||
|
|
quantity: 1,
|
|||
|
|
unit_price: 0,
|
|||
|
|
remark: ''
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'purchaseOrder') {
|
|||
|
|
await loadSuppliers()
|
|||
|
|
await loadMaterials()
|
|||
|
|
state.form = {
|
|||
|
|
supplier_id: state.suppliers[0]?.id || null,
|
|||
|
|
expected_date: '',
|
|||
|
|
remark: '',
|
|||
|
|
items: [{
|
|||
|
|
product_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1,
|
|||
|
|
remark: ''
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
state.showModal = true
|
|||
|
|
nextTick(() => {
|
|||
|
|
initPickers()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openMaterialConsumptionModal = async () => {
|
|||
|
|
if (state.modalType !== 'salesOrder') {
|
|||
|
|
addNotification('请先打开销售订单编辑页面', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await loadMaterials()
|
|||
|
|
await loadWarehouses()
|
|||
|
|
state.materialConsumptionItems = []
|
|||
|
|
state.showMaterialConsumptionModal = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const addMaterialConsumptionItem = () => {
|
|||
|
|
state.materialConsumptionItems.push({
|
|||
|
|
material_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1,
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeMaterialConsumptionItem = (index: number) => {
|
|||
|
|
state.materialConsumptionItems.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveMaterialConsumption = async () => {
|
|||
|
|
if (state.modalType !== 'salesOrder') {
|
|||
|
|
addNotification('请先打开销售订单编辑页面', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!state.editingItem) {
|
|||
|
|
state.showMaterialConsumptionModal = false
|
|||
|
|
await saveSalesOrder()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const [i, item] of state.materialConsumptionItems.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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/sales-orders/${state.editingItem.id}/consume-materials`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
items: state.materialConsumptionItems.map((item: any) => ({
|
|||
|
|
material_id: item.material_id,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
remark: item.remark
|
|||
|
|
}))
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
addNotification('物料消耗记录保存成功', 'success')
|
|||
|
|
state.showMaterialConsumptionModal = false
|
|||
|
|
const detail = await apiRequest(`/api/sales-orders/${state.editingItem.id}`)
|
|||
|
|
state.form = {
|
|||
|
|
...state.form,
|
|||
|
|
actual_material_cost: detail.actual_material_cost
|
|||
|
|
}
|
|||
|
|
const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${state.editingItem.id}&movement_type=consumption`)
|
|||
|
|
state.consumedMaterials = (movements || []).map((movement: any) => ({
|
|||
|
|
material_name: movement.product_name || movement.product_sku || '未知物料',
|
|||
|
|
quantity: Math.abs(movement.quantity),
|
|||
|
|
unit_price: movement.unit_price || 0,
|
|||
|
|
amount: movement.total_amount || 0
|
|||
|
|
}))
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存物料消耗')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openRestockModal = async () => {
|
|||
|
|
await loadMaterials()
|
|||
|
|
await loadSuppliers()
|
|||
|
|
state.restockItems = []
|
|||
|
|
state.showRestockModal = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const addRestockItem = () => {
|
|||
|
|
state.restockItems.push({
|
|||
|
|
material_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1,
|
|||
|
|
unit_price: 0,
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeRestockItem = (index: number) => {
|
|||
|
|
state.restockItems.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveRestock = async () => {
|
|||
|
|
if (!state.restockItems || state.restockItems.length === 0) {
|
|||
|
|
addNotification('请至少添加一个补货物料', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const [i, item] of state.restockItems.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: state.suppliers[0]?.id || null,
|
|||
|
|
expected_date: new Date().toISOString().split('T')[0],
|
|||
|
|
remark: '物料补货',
|
|||
|
|
items: state.restockItems.map((item: any) => ({
|
|||
|
|
product_id: item.material_id,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
remark: item.remark
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await apiRequest('/api/purchase-orders', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
addNotification('采购订单创建成功', 'success')
|
|||
|
|
state.showRestockModal = false
|
|||
|
|
state.restockItems = []
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存补货订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const closeModal = () => {
|
|||
|
|
state.showModal = false
|
|||
|
|
state.modalType = ''
|
|||
|
|
state.editingItem = null
|
|||
|
|
state.productBomItems = []
|
|||
|
|
state.purchaseReceiveItems = []
|
|||
|
|
state.form = {}
|
|||
|
|
destroyPickers()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveProduct = async () => {
|
|||
|
|
try {
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
await apiRequest(`/api/products/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
if (state.form.item_type === 'finished' && state.productBomItems.length > 0) {
|
|||
|
|
await apiRequest(`/api/products/${state.editingItem.id}/materials`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify({ items: state.productBomItems })
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
addNotification('产品更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/products', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('产品创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
closeModal()
|
|||
|
|
loadProducts()
|
|||
|
|
loadMaterials()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存产品')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteProduct = async (id: number) => {
|
|||
|
|
if (!confirm('确定要删除这个产品吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/products/${id}`, { method: 'DELETE' })
|
|||
|
|
addNotification('产品已删除', 'success')
|
|||
|
|
loadProducts()
|
|||
|
|
loadMaterials()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除产品')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const addBomItem = () => {
|
|||
|
|
state.productBomItems.push({
|
|||
|
|
material_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeBomItem = (idx: number) => {
|
|||
|
|
state.productBomItems.splice(idx, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveSupplier = async () => {
|
|||
|
|
try {
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
await apiRequest(`/api/suppliers/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('供应商更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/suppliers', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('供应商创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
closeModal()
|
|||
|
|
loadSuppliers()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存供应商')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteSupplier = async (id: number) => {
|
|||
|
|
if (!confirm('确定要删除这个供应商吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' })
|
|||
|
|
addNotification('供应商已删除', 'success')
|
|||
|
|
loadSuppliers()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除供应商')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveCustomer = async () => {
|
|||
|
|
try {
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
await apiRequest(`/api/customers/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('客户更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/customers', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('客户创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
closeModal()
|
|||
|
|
loadCustomers()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存客户')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteCustomer = async (id: number) => {
|
|||
|
|
if (!confirm('确定要删除这个客户吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/customers/${id}`, { method: 'DELETE' })
|
|||
|
|
addNotification('客户已删除', 'success')
|
|||
|
|
loadCustomers()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除客户')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveInventoryItem = async () => {
|
|||
|
|
try {
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
await apiRequest(`/api/inventory/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
quantity: state.form.quantity,
|
|||
|
|
locked_quantity: state.form.locked_quantity,
|
|||
|
|
batch_number: state.form.batch_number,
|
|||
|
|
location: state.form.location
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
addNotification('物料库存更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/inventory', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(state.form)
|
|||
|
|
})
|
|||
|
|
addNotification('物料库存创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
closeModal()
|
|||
|
|
loadInventory()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存物料库存')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteInventoryItem = async (id: number) => {
|
|||
|
|
if (!confirm('确定要删除这个物料库存记录吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/inventory/${id}`, { method: 'DELETE' })
|
|||
|
|
addNotification('物料库存已删除', 'success')
|
|||
|
|
loadInventory()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除物料库存')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const addSalesOrderItem = () => {
|
|||
|
|
state.form.items = state.form.items || []
|
|||
|
|
state.form.items.push({
|
|||
|
|
mode: 'new',
|
|||
|
|
product_id: null,
|
|||
|
|
product_sku: '',
|
|||
|
|
product_name: '',
|
|||
|
|
quantity: 1,
|
|||
|
|
unit_price: 0,
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const setSalesOrderLineMode = (line: any, mode: string) => {
|
|||
|
|
line.mode = mode
|
|||
|
|
if (mode === 'new') {
|
|||
|
|
line.product_id = null
|
|||
|
|
line.product_sku = ''
|
|||
|
|
line.product_name = ''
|
|||
|
|
} else {
|
|||
|
|
line.product_sku = ''
|
|||
|
|
line.product_name = ''
|
|||
|
|
if (!line.product_id) {
|
|||
|
|
line.product_id = state.finishedProducts[0]?.id || null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeSalesOrderItem = (index: number) => {
|
|||
|
|
state.form.items.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveSalesOrder = async () => {
|
|||
|
|
try {
|
|||
|
|
if (!state.form.customer_id) {
|
|||
|
|
addNotification('请选择客户', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!state.form.items || !state.form.items.length) {
|
|||
|
|
addNotification('请至少添加一个成品明细', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const [i, item] of state.form.items.entries()) {
|
|||
|
|
const idx = i + 1
|
|||
|
|
if (item.mode === 'existing') {
|
|||
|
|
if (!item.product_id) {
|
|||
|
|
addNotification(`第 ${idx} 行:请选择模具`, 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if (!item.product_sku || !String(item.product_sku).trim()) {
|
|||
|
|
addNotification(`第 ${idx} 行:请输入模具SKU`, 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!item.product_name || !String(item.product_name).trim()) {
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const payload = {
|
|||
|
|
customer_id: state.form.customer_id,
|
|||
|
|
delivery_date: toApiDateTime(state.form.delivery_date),
|
|||
|
|
remark: state.form.remark,
|
|||
|
|
items: state.form.items.map((item: any) => ({
|
|||
|
|
product_id: item.mode === 'existing' ? (item.product_id || null) : null,
|
|||
|
|
product_sku: item.mode === 'existing' ? null : (item.product_sku || null),
|
|||
|
|
product_name: item.mode === 'existing' ? null : (item.product_name || null),
|
|||
|
|
product_category: null,
|
|||
|
|
product_unit: '件',
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
unit_price: item.unit_price,
|
|||
|
|
remark: item.remark || ''
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
let orderId: number
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
orderId = state.editingItem.id
|
|||
|
|
addNotification(result.production_status === 'bom_missing' ? '订单已保存,但成品未配置BOM,未扣减物料' : '销售订单更新成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success')
|
|||
|
|
} else {
|
|||
|
|
const result = await apiRequest('/api/sales-orders', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
orderId = result.id
|
|||
|
|
addNotification(result.production_status === 'bom_missing' ? '订单已创建,但成品未配置BOM,未扣减物料' : '销售订单创建成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (state.materialConsumptionItems && state.materialConsumptionItems.length > 0) {
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/sales-orders/${orderId}/consume-materials`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
items: state.materialConsumptionItems.map((item: any) => ({
|
|||
|
|
material_id: item.material_id,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
remark: item.remark
|
|||
|
|
}))
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
addNotification('物料消耗记录保存成功', 'success')
|
|||
|
|
state.materialConsumptionItems = []
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存物料消耗')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
closeModal()
|
|||
|
|
loadProductionOrders()
|
|||
|
|
loadFinishedProducts()
|
|||
|
|
loadInventory()
|
|||
|
|
loadMovements()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存销售订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateSalesOrderStatus = async (order: any, targetStatus: string) => {
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/sales-orders/${order.id}/status`, {
|
|||
|
|
method: 'PATCH',
|
|||
|
|
body: JSON.stringify({ status: targetStatus })
|
|||
|
|
})
|
|||
|
|
addNotification('订单状态已更新', 'success')
|
|||
|
|
loadProductionOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '更新订单状态')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updatePurchaseOrderStatus = async (order: any, targetStatus: string) => {
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/purchase-orders/${order.id}/status`, {
|
|||
|
|
method: 'PATCH',
|
|||
|
|
body: JSON.stringify({ status: targetStatus })
|
|||
|
|
})
|
|||
|
|
addNotification('采购订单状态已更新', 'success')
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '更新采购订单状态')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteSalesOrder = async (orderId: number) => {
|
|||
|
|
if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/sales-orders/${orderId}`, { method: 'DELETE' })
|
|||
|
|
addNotification('销售订单已删除并回补物料', 'success')
|
|||
|
|
if (state.productionPlan?.sales_order_id === orderId) {
|
|||
|
|
state.productionPlan = null
|
|||
|
|
}
|
|||
|
|
loadProductionOrders()
|
|||
|
|
loadInventory()
|
|||
|
|
loadMovements()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除销售订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const addPurchaseOrderItem = () => {
|
|||
|
|
state.form.items = state.form.items || []
|
|||
|
|
state.form.items.push({
|
|||
|
|
product_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1,
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removePurchaseOrderItem = (index: number) => {
|
|||
|
|
state.form.items.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const savePurchaseOrder = async () => {
|
|||
|
|
try {
|
|||
|
|
if (!state.form.supplier_id) {
|
|||
|
|
addNotification('请选择供应商', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!state.form.items || !state.form.items.length) {
|
|||
|
|
addNotification('请至少添加一个物料明细', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for (const [i, item] of state.form.items.entries()) {
|
|||
|
|
const idx = i + 1
|
|||
|
|
if (!item.product_id) {
|
|||
|
|
addNotification(`第 ${idx} 行:请选择物料`, 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!Number.isFinite(item.quantity) || item.quantity <= 0) {
|
|||
|
|
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const payload = {
|
|||
|
|
supplier_id: state.form.supplier_id,
|
|||
|
|
expected_date: toApiDateTime(state.form.expected_date),
|
|||
|
|
remark: state.form.remark,
|
|||
|
|
items: state.form.items.map((item: any) => ({
|
|||
|
|
product_id: item.product_id,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
remark: item.remark
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
if (state.editingItem) {
|
|||
|
|
await apiRequest(`/api/purchase-orders/${state.editingItem.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
addNotification('采购订单更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/purchase-orders', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
addNotification('采购订单创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
closeModal()
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存采购订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deletePurchaseOrder = async (orderId: number) => {
|
|||
|
|
if (!confirm('确定删除这个采购订单吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/purchase-orders/${orderId}`, { method: 'DELETE' })
|
|||
|
|
addNotification('采购订单已删除', 'success')
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除采购订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const receivePurchaseOrder = async () => {
|
|||
|
|
try {
|
|||
|
|
if (!state.editingItem?.id) return
|
|||
|
|
const items = (state.purchaseReceiveItems || [])
|
|||
|
|
.filter((line: any) => Number(line.receive_quantity) > 0)
|
|||
|
|
.map((line: any) => ({
|
|||
|
|
item_id: line.item_id,
|
|||
|
|
receive_quantity: Number(line.receive_quantity)
|
|||
|
|
}))
|
|||
|
|
if (!items.length) {
|
|||
|
|
addNotification('请填写本次入库数量', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await apiRequest(`/api/purchase-orders/${state.editingItem.id}/receive`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
warehouse_id: state.form.warehouse_id || state.purchaseWarehouseId,
|
|||
|
|
items,
|
|||
|
|
remark: state.form.remark || ''
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
addNotification('采购到货入库成功', 'success')
|
|||
|
|
closeModal()
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
loadInventory()
|
|||
|
|
loadMovements()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '采购到货入库')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
if (!store.user) {
|
|||
|
|
router.push('/login')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
checkBackendHealth().then(() => {
|
|||
|
|
if (!state.backendDbReady) {
|
|||
|
|
addNotification(state.backendDbMessage || '业务服务不可用', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
loadDashboard()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
destroyPickers()
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="page-container">
|
|||
|
|
<div class="page-header">
|
|||
|
|
<h1>进销存管理</h1>
|
|||
|
|
<p>库存、采购、销售管理</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inventory-layout">
|
|||
|
|
<aside class="inventory-sidebar">
|
|||
|
|
<div class="inventory-menu">
|
|||
|
|
<div v-for="group in menuGroups" :key="group.key" class="inventory-menu-group">
|
|||
|
|
<button type="button" class="inventory-menu-title" @click="toggleGroup(group.key)">
|
|||
|
|
<span>{{ group.title }}</span>
|
|||
|
|
<span class="inventory-menu-caret">{{ openGroups[group.key] ? '▾' : '▸' }}</span>
|
|||
|
|
</button>
|
|||
|
|
<div v-show="openGroups[group.key]" class="inventory-menu-items">
|
|||
|
|
<button
|
|||
|
|
v-for="item in group.items"
|
|||
|
|
:key="group.key + '-' + item.key"
|
|||
|
|
type="button"
|
|||
|
|
:class="['inventory-menu-item', { active: state.activeTab === item.key }]"
|
|||
|
|
@click="handleMenuClick(item.key)"
|
|||
|
|
>
|
|||
|
|
{{ item.label }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<section class="inventory-content">
|
|||
|
|
<div class="inventory-content-header">
|
|||
|
|
<div class="inventory-content-title">
|
|||
|
|
<span class="inventory-breadcrumb">{{ activeMenu?.group?.title || '进销存' }}</span>
|
|||
|
|
<span class="inventory-breadcrumb-sep">/</span>
|
|||
|
|
<span class="inventory-breadcrumb-current">{{ activeMenu?.item?.label || '' }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="!state.backendDbReady" class="inline-alert inline-alert-warning">
|
|||
|
|
<div class="inline-alert-title">提示</div>
|
|||
|
|
<div class="inline-alert-message">{{ state.backendDbMessage }}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="state.loading" class="loading-state">
|
|||
|
|
<div class="spinner"></div>
|
|||
|
|
<span>加载中...</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else>
|
|||
|
|
<div v-if="state.activeTab === 'dashboard'" class="dashboard-grid">
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">📦</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ state.dashboard?.product_count || 0 }}</div>
|
|||
|
|
<div class="stat-label">成品数量</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">📊</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ state.dashboard?.total_stock || 0 }}</div>
|
|||
|
|
<div class="stat-label">物料库存总量</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">💰</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ formatCurrency(state.dashboard?.total_value || 0) }}</div>
|
|||
|
|
<div class="stat-label">库存价值</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">🏭</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ state.dashboard?.supplier_count || 0 }}</div>
|
|||
|
|
<div class="stat-label">供应商</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">👥</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ state.dashboard?.customer_count || 0 }}</div>
|
|||
|
|
<div class="stat-label">客户</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">🏪</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ state.dashboard?.warehouse_count || 0 }}</div>
|
|||
|
|
<div class="stat-label">仓库</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'products'">
|
|||
|
|
<div class="table-header">
|
|||
|
|
<button class="btn btn-primary" @click="openCreateProduct('finished')">+ 新增成品</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>SKU</th>
|
|||
|
|
<th>名称</th>
|
|||
|
|
<th>分类</th>
|
|||
|
|
<th>单位</th>
|
|||
|
|
<th>成本价</th>
|
|||
|
|
<th>销售价</th>
|
|||
|
|
<th>基础物料成本</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="product in state.finishedProducts" :key="product.id">
|
|||
|
|
<td>{{ product.sku }}</td>
|
|||
|
|
<td>{{ product.name }}</td>
|
|||
|
|
<td>{{ product.category || '-' }}</td>
|
|||
|
|
<td>{{ product.unit }}</td>
|
|||
|
|
<td>{{ formatCurrency(product.cost_price) }}</td>
|
|||
|
|
<td>{{ formatCurrency(product.sale_price) }}</td>
|
|||
|
|
<td>{{ formatCurrency(product.material_cost || 0) }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('product', product)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteProduct(product.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'materials'">
|
|||
|
|
<div class="table-header">
|
|||
|
|
<button class="btn btn-primary" @click="openCreateProduct('material')">+ 新增物料</button>
|
|||
|
|
<button class="btn btn-secondary" @click="openRestockModal">+ 物料补货</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>SKU</th>
|
|||
|
|
<th>名称</th>
|
|||
|
|
<th>分类</th>
|
|||
|
|
<th>单位</th>
|
|||
|
|
<th>成本价</th>
|
|||
|
|
<th>最低库存</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="product in state.materials" :key="product.id">
|
|||
|
|
<td>{{ product.sku }}</td>
|
|||
|
|
<td>{{ product.name }}</td>
|
|||
|
|
<td>{{ product.category || '-' }}</td>
|
|||
|
|
<td>{{ product.unit }}</td>
|
|||
|
|
<td>{{ formatCurrency(product.cost_price) }}</td>
|
|||
|
|
<td>{{ product.min_stock || '-' }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('product', product)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteProduct(product.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'inventory'">
|
|||
|
|
<div class="table-header">
|
|||
|
|
<button class="btn btn-primary" @click="openModal('inventoryItem')">+ 新增物料库存</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>SKU</th>
|
|||
|
|
<th>物料</th>
|
|||
|
|
<th>仓库</th>
|
|||
|
|
<th>数量</th>
|
|||
|
|
<th>可用</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="item in state.inventory" :key="item.id">
|
|||
|
|
<td>{{ item.product_sku }}</td>
|
|||
|
|
<td>{{ item.product_name }}</td>
|
|||
|
|
<td>{{ item.warehouse_name }}</td>
|
|||
|
|
<td>{{ item.quantity }}</td>
|
|||
|
|
<td>{{ item.available_quantity }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('inventoryItem', item)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteInventoryItem(item.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'purchases'">
|
|||
|
|
<div class="table-header mb-12">
|
|||
|
|
<button class="btn btn-primary" @click="openModal('purchaseOrder')">+ 新增采购订单</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container mb-16">
|
|||
|
|
<div class="flex-row-center">
|
|||
|
|
<label>到货仓库</label>
|
|||
|
|
<select v-model.number="state.purchaseWarehouseId" class="form-input w-260">
|
|||
|
|
<option v-for="warehouse in state.warehouses" :key="'purchase-warehouse-' + warehouse.id" :value="warehouse.id">
|
|||
|
|
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
<button class="btn btn-secondary" @click="loadPurchaseOrders">刷新</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>采购单</th>
|
|||
|
|
<th>供应商</th>
|
|||
|
|
<th>状态</th>
|
|||
|
|
<th>订单创建</th>
|
|||
|
|
<th>预计到货</th>
|
|||
|
|
<th>实际到货</th>
|
|||
|
|
<th>实际付款</th>
|
|||
|
|
<th>总金额</th>
|
|||
|
|
<th>已付款</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="order in state.purchaseOrders" :key="'purchase-order-' + order.id">
|
|||
|
|
<td>{{ order.order_no }}</td>
|
|||
|
|
<td>{{ order.supplier_name }}</td>
|
|||
|
|
<td>{{ getPurchaseOrderStatusLabel(order.status) }}</td>
|
|||
|
|
<td>{{ order.created_at ? formatDateTime(order.created_at) : '-' }}</td>
|
|||
|
|
<td>{{ order.expected_date ? formatDate(order.expected_date) : '-' }}</td>
|
|||
|
|
<td>{{ order.received_date ? formatDateTime(order.received_date) : '-' }}</td>
|
|||
|
|
<td>{{ order.paid_date ? formatDateTime(order.paid_date) : '-' }}</td>
|
|||
|
|
<td>{{ formatCurrency(order.total_amount || 0) }}</td>
|
|||
|
|
<td>{{ formatCurrency(order.paid_amount || 0) }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('purchaseOrder', order)" :disabled="isPurchaseOrderLocked(order.status)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deletePurchaseOrder(order.id)" :disabled="isPurchaseOrderLocked(order.status)">删除</button>
|
|||
|
|
<button v-if="order.status === 'pending'" class="btn btn-sm btn-primary" @click="openModal('purchaseReceive', order)">到货入库</button>
|
|||
|
|
<button v-else-if="order.status === 'received'" class="btn btn-sm btn-success" @click="updatePurchaseOrderStatus(order, 'paid')">标记为已付款</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'suppliers'">
|
|||
|
|
<div class="table-header">
|
|||
|
|
<button class="btn btn-primary" @click="openModal('supplier')">+ 新增供应商</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>编码</th>
|
|||
|
|
<th>名称</th>
|
|||
|
|
<th>联系人</th>
|
|||
|
|
<th>电话</th>
|
|||
|
|
<th>邮箱</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="supplier in state.suppliers" :key="supplier.id">
|
|||
|
|
<td>{{ supplier.code }}</td>
|
|||
|
|
<td>{{ supplier.name }}</td>
|
|||
|
|
<td>{{ supplier.contact_person || '-' }}</td>
|
|||
|
|
<td>{{ supplier.phone || '-' }}</td>
|
|||
|
|
<td>{{ supplier.email || '-' }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('supplier', supplier)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteSupplier(supplier.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'customers'">
|
|||
|
|
<div class="table-header">
|
|||
|
|
<button class="btn btn-primary" @click="openModal('customer')">+ 新增客户</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>编码</th>
|
|||
|
|
<th>名称</th>
|
|||
|
|
<th>联系人</th>
|
|||
|
|
<th>电话</th>
|
|||
|
|
<th>邮箱</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="customer in state.customers" :key="customer.id">
|
|||
|
|
<td>{{ customer.code }}</td>
|
|||
|
|
<td>{{ customer.name }}</td>
|
|||
|
|
<td>{{ customer.contact_person || '-' }}</td>
|
|||
|
|
<td>{{ customer.phone || '-' }}</td>
|
|||
|
|
<td>{{ customer.email || '-' }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('customer', customer)">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteCustomer(customer.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'sales_orders'">
|
|||
|
|
<div class="table-header mb-12">
|
|||
|
|
<button class="btn btn-primary" @click="openModal('salesOrder')">+ 新增销售订单</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container mb-16">
|
|||
|
|
<div class="flex-row-center">
|
|||
|
|
<label>领料仓库</label>
|
|||
|
|
<select v-model.number="state.productionWarehouseId" class="form-input w-260">
|
|||
|
|
<option v-for="warehouse in state.warehouses" :key="'production-warehouse-' + warehouse.id" :value="warehouse.id">
|
|||
|
|
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
<button class="btn btn-secondary" @click="loadProductionOrders">刷新</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>销售单</th>
|
|||
|
|
<th>客户</th>
|
|||
|
|
<th>订单金额</th>
|
|||
|
|
<th>交付日期</th>
|
|||
|
|
<th>订单创建</th>
|
|||
|
|
<th>实际交付</th>
|
|||
|
|
<th>实际收款</th>
|
|||
|
|
<th>订单状态</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="order in state.productionOrders" :key="'production-order-' + order.id">
|
|||
|
|
<td>{{ order.order_no }}</td>
|
|||
|
|
<td>{{ order.customer_name }}</td>
|
|||
|
|
<td>{{ formatCurrency(order.total_amount || 0) }}</td>
|
|||
|
|
<td>{{ order.delivery_date ? formatDate(order.delivery_date) : '-' }}</td>
|
|||
|
|
<td>{{ order.created_at ? formatDateTime(order.created_at) : '-' }}</td>
|
|||
|
|
<td>{{ order.actual_delivery_date ? formatDateTime(order.actual_delivery_date) : '-' }}</td>
|
|||
|
|
<td>{{ order.actual_payment_date ? formatDateTime(order.actual_payment_date) : '-' }}</td>
|
|||
|
|
<td>{{ order.status === 'manufacturing' ? '制造中' : order.status === 'delivered' ? '已交付' : order.status === 'paid' ? '已收款' : order.status }}</td>
|
|||
|
|
<td>
|
|||
|
|
<div class="action-btns">
|
|||
|
|
<div class="dropdown">
|
|||
|
|
<button class="btn btn-sm btn-secondary dropdown-toggle">状态</button>
|
|||
|
|
<div class="dropdown-menu">
|
|||
|
|
<button class="dropdown-item" @click="updateSalesOrderStatus(order, 'delivered')">已交付</button>
|
|||
|
|
<button class="dropdown-item" @click="updateSalesOrderStatus(order, 'paid')">已收款</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-sm btn-secondary" @click="openModal('salesOrder', order)" :disabled="order.status === 'paid'" :class="{'btn-disabled': order.status === 'paid'}">编辑</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" @click="deleteSalesOrder(order.id)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'finance'">
|
|||
|
|
<div class="table-container mb-16">
|
|||
|
|
<div class="flex-row-center">
|
|||
|
|
<div>
|
|||
|
|
<label class="mr-8">年份</label>
|
|||
|
|
<input v-model.number="state.financePeriod.year" type="number" min="2000" max="2100" class="form-input w-inline-120" />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label class="mr-8">季度</label>
|
|||
|
|
<select v-model="state.financePeriod.quarter" class="form-input w-inline-140">
|
|||
|
|
<option value="">全年</option>
|
|||
|
|
<option value="1">Q1</option>
|
|||
|
|
<option value="2">Q2</option>
|
|||
|
|
<option value="3">Q3</option>
|
|||
|
|
<option value="4">Q4</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-primary" @click="refreshFinanceByPeriod">刷新统计</button>
|
|||
|
|
<span class="text-secondary">统计周期:{{ state.financeSummary?.period_label || '-' }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="dashboard-grid">
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">🧾</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.receivable_total || 0) }}</div>
|
|||
|
|
<div class="stat-label">应收总额</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">💸</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.payable_total || 0) }}</div>
|
|||
|
|
<div class="stat-label">应付总额</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">💵</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.period_receipt_total || 0) }}</div>
|
|||
|
|
<div class="stat-label">周期收款</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon">🏦</div>
|
|||
|
|
<div class="stat-content">
|
|||
|
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.period_payment_total || 0) }}</div>
|
|||
|
|
<div class="stat-label">周期付款</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="table-container mt-16">
|
|||
|
|
<h3 class="mb-12">客户账款(周期)</h3>
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>客户</th>
|
|||
|
|
<th>订单数</th>
|
|||
|
|
<th>流水数</th>
|
|||
|
|
<th>订单金额</th>
|
|||
|
|
<th>订单已收</th>
|
|||
|
|
<th>实收流水</th>
|
|||
|
|
<th>应收余额</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="item in state.customerFinanceStatement" :key="'customer-' + item.partner_id">
|
|||
|
|
<td>{{ item.partner_name }}</td>
|
|||
|
|
<td>{{ item.order_count }}</td>
|
|||
|
|
<td>{{ item.transaction_count }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.order_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.settled_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.transaction_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.outstanding_total) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="table-container mt-16">
|
|||
|
|
<h3 class="mb-12">供应商账款(周期)</h3>
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>供应商</th>
|
|||
|
|
<th>订单数</th>
|
|||
|
|
<th>流水数</th>
|
|||
|
|
<th>订单金额</th>
|
|||
|
|
<th>订单已付</th>
|
|||
|
|
<th>实付流水</th>
|
|||
|
|
<th>应付余额</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="item in state.supplierFinanceStatement" :key="'supplier-' + item.partner_id">
|
|||
|
|
<td>{{ item.partner_name }}</td>
|
|||
|
|
<td>{{ item.order_count }}</td>
|
|||
|
|
<td>{{ item.transaction_count }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.order_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.settled_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.transaction_total) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.outstanding_total) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="table-container mt-16">
|
|||
|
|
<h3 class="mb-12">客户-商品追溯(周期)</h3>
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>客户</th>
|
|||
|
|
<th>SKU</th>
|
|||
|
|
<th>商品</th>
|
|||
|
|
<th>订单数</th>
|
|||
|
|
<th>数量</th>
|
|||
|
|
<th>订单金额</th>
|
|||
|
|
<th>已结款</th>
|
|||
|
|
<th>未结款</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="item in state.customerProductStatement" :key="'customer-product-' + item.partner_id + '-' + item.product_id">
|
|||
|
|
<td>{{ item.partner_name }}</td>
|
|||
|
|
<td>{{ item.product_sku || '-' }}</td>
|
|||
|
|
<td>{{ item.product_name }}</td>
|
|||
|
|
<td>{{ item.order_count }}</td>
|
|||
|
|
<td>{{ formatNumber(item.order_quantity) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.order_amount) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.settled_amount) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.outstanding_amount) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="table-container mt-16">
|
|||
|
|
<h3 class="mb-12">供应商-商品追溯(周期)</h3>
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>供应商</th>
|
|||
|
|
<th>SKU</th>
|
|||
|
|
<th>商品</th>
|
|||
|
|
<th>订单数</th>
|
|||
|
|
<th>数量</th>
|
|||
|
|
<th>订单金额</th>
|
|||
|
|
<th>已结款</th>
|
|||
|
|
<th>未结款</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="item in state.supplierProductStatement" :key="'supplier-product-' + item.partner_id + '-' + item.product_id">
|
|||
|
|
<td>{{ item.partner_name }}</td>
|
|||
|
|
<td>{{ item.product_sku || '-' }}</td>
|
|||
|
|
<td>{{ item.product_name }}</td>
|
|||
|
|
<td>{{ item.order_count }}</td>
|
|||
|
|
<td>{{ formatNumber(item.order_quantity) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.order_amount) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.settled_amount) }}</td>
|
|||
|
|
<td>{{ formatCurrency(item.outstanding_amount) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="table-container mt-16">
|
|||
|
|
<h3 class="mb-12">最近财务流水</h3>
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>单号</th>
|
|||
|
|
<th>类型</th>
|
|||
|
|
<th>往来方</th>
|
|||
|
|
<th>金额</th>
|
|||
|
|
<th>状态</th>
|
|||
|
|
<th>日期</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="txn in state.financeTransactions" :key="txn.id">
|
|||
|
|
<td>{{ txn.txn_no }}</td>
|
|||
|
|
<td>{{ txn.txn_type === 'receipt' ? '收款' : '付款' }}</td>
|
|||
|
|
<td>{{ txn.partner_type === 'customer' ? '客户' : '供应商' }}#{{ txn.partner_id }}</td>
|
|||
|
|
<td>{{ formatCurrency(txn.amount) }}</td>
|
|||
|
|
<td>{{ txn.status === 'confirmed' ? '已确认' : '已作废' }}</td>
|
|||
|
|
<td>{{ formatDateTime(txn.txn_date) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="state.activeTab === 'movements'" class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>物料</th>
|
|||
|
|
<th>类型</th>
|
|||
|
|
<th>数量</th>
|
|||
|
|
<th>变动前</th>
|
|||
|
|
<th>变动后</th>
|
|||
|
|
<th>时间</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="movement in state.movements" :key="movement.id">
|
|||
|
|
<td>{{ movement.product_name }}{{ movement.product_sku ? ' (' + movement.product_sku + ')' : '' }}</td>
|
|||
|
|
<td>
|
|||
|
|
<span :class="['badge', getMovementBadgeClass(movement.movement_type)]">
|
|||
|
|
{{ getMovementTypeLabel(movement.movement_type) }}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td>{{ movement.quantity }}</td>
|
|||
|
|
<td>{{ movement.before_quantity }}</td>
|
|||
|
|
<td>{{ movement.after_quantity }}</td>
|
|||
|
|
<td>{{ formatDateTime(movement.created_at) }}</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 模态框 -->
|
|||
|
|
<div v-if="state.showModal" class="modal-overlay" @click.self="closeModal">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h3>{{ (state.editingItem ? '编辑' : '新增') + (state.modalType === 'product' ? (state.form.item_type === 'finished' ? '成品' : '物料') : state.modalType === 'inventoryItem' ? '物料库存' : state.modalType === 'salesOrder' ? '销售订单' : state.modalType === 'purchaseOrder' ? '采购订单' : state.modalType === 'purchaseReceive' ? '采购到货入库' : state.modalType === 'supplier' ? '供应商' : '客户') }}</h3>
|
|||
|
|
<button class="modal-close" @click="closeModal">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<form v-if="state.modalType === 'product'" @submit.prevent="saveProduct">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">类型 *</label>
|
|||
|
|
<input class="form-input" :value="state.form.item_type === 'material' ? '物料(纳入库存)' : '成品(按单生产,不做库存)'" disabled />
|
|||
|
|
<input v-model="state.form.item_type" type="hidden" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">SKU *</label>
|
|||
|
|
<input v-model="state.form.sku" class="form-input" required placeholder="产品编码" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">名称 *</label>
|
|||
|
|
<input v-model="state.form.name" class="form-input" required placeholder="产品名称" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">分类</label>
|
|||
|
|
<input v-model="state.form.category" class="form-input" placeholder="产品分类" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">单位</label>
|
|||
|
|
<input v-model="state.form.unit" class="form-input" placeholder="件/个/箱" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">成本价</label>
|
|||
|
|
<input v-model.number="state.form.cost_price" type="number" step="0.01" class="form-input" placeholder="0.00" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">销售价</label>
|
|||
|
|
<input v-model.number="state.form.sale_price" type="number" step="0.01" class="form-input" placeholder="0.00" />
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.form.item_type === 'material'" class="form-group">
|
|||
|
|
<label class="form-label">最低库存</label>
|
|||
|
|
<input v-model.number="state.form.min_stock" type="number" class="form-input" placeholder="0" />
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.form.item_type === 'finished'" class="form-group">
|
|||
|
|
<label class="form-label">说明</label>
|
|||
|
|
<input disabled value="成品不做库存,成本由下方BOM定义物料构成后自动计算" class="form-input" />
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.form.item_type === 'finished' && state.editingItem" class="bom-section">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">BOM物料配置</label>
|
|||
|
|
<button type="button" class="btn btn-secondary bom-add-btn" @click="addBomItem">+ 添加物料</button>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>物料</th>
|
|||
|
|
<th>数量</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="(item, idx) in state.productBomItems" :key="'edit-bom-item-' + idx">
|
|||
|
|
<td>
|
|||
|
|
<select v-model.number="item.material_id" class="form-input" required>
|
|||
|
|
<option v-for="material in state.materials" :key="'edit-bom-material-' + material.id" :value="material.id">
|
|||
|
|
{{ material.sku }} - {{ material.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</td>
|
|||
|
|
<td><input v-model.number="item.quantity" type="number" min="0.0001" step="0.0001" class="form-input" required /></td>
|
|||
|
|
<td><button type="button" class="btn btn-sm btn-danger" @click="removeBomItem(idx)">删除</button></td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'salesOrder'" @submit.prevent="saveSalesOrder">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">客户 *</label>
|
|||
|
|
<select v-model.number="state.form.customer_id" class="form-input" required>
|
|||
|
|
<option v-for="customer in state.customers" :key="'order-customer-' + customer.id" :value="customer.id">
|
|||
|
|
{{ customer.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">交付日期</label>
|
|||
|
|
<div class="datetime-field" @click="openDateTimePicker('delivery')">
|
|||
|
|
<input ref="deliveryDateInput" v-model="state.form.delivery_date" type="text" class="form-input air-datetime-input" readonly placeholder="选择日期时间" />
|
|||
|
|
<input ref="deliveryDateNativeInput" v-model="state.form.delivery_date_native" @change="state.form.delivery_date = fromNativeValue(state.form.delivery_date_native)" type="datetime-local" class="datetime-native" />
|
|||
|
|
<span class="datetime-field-icon" aria-hidden="true">📅</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="state.form.remark" class="form-input" placeholder="订单备注" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group form-group-spaced">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="addSalesOrderItem">+ 添加模具</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.form.items.length === 0" class="empty-state">
|
|||
|
|
<div class="empty-icon">📦</div>
|
|||
|
|
<div class="empty-title">暂无模具</div>
|
|||
|
|
<div class="empty-desc">请点击"添加模具"按钮添加订单明细</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="sales-order-items">
|
|||
|
|
<div v-for="(line, index) in state.form.items" :key="'sales-order-line-' + index" class="sales-order-item">
|
|||
|
|
<div class="sales-order-item-header">
|
|||
|
|
<button type="button" :class="['btn', line.mode === 'new' ? 'active' : '']" @click="setSalesOrderLineMode(line, 'new')">新模</button>
|
|||
|
|
<button type="button" :class="['btn', line.mode === 'existing' ? 'active' : '']" @click="setSalesOrderLineMode(line, 'existing')">改模</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="sales-order-item-content">
|
|||
|
|
<div v-if="line.mode !== 'existing'">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">模具SKU *</label>
|
|||
|
|
<input v-model="line.product_sku" class="form-input" placeholder="请输入模具SKU" required />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">模具名称 *</label>
|
|||
|
|
<input v-model="line.product_name" class="form-input" placeholder="请输入模具名称" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">选择模具 *</label>
|
|||
|
|
<select v-model.number="line.product_id" class="form-input" required>
|
|||
|
|
<option value="">请选择模具</option>
|
|||
|
|
<option v-for="product in state.finishedProducts" :key="'sales-order-product-' + product.id" :value="product.id">
|
|||
|
|
{{ product.sku }} - {{ product.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sales-order-item-row">
|
|||
|
|
<div class="sales-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">数量 *</label>
|
|||
|
|
<input v-model.number="line.quantity" type="number" min="1" class="form-input" placeholder="请输入数量" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sales-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">单价 *</label>
|
|||
|
|
<input v-model.number="line.unit_price" type="number" min="0" step="0.01" class="form-input" placeholder="请输入单价" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sales-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sales-order-item-actions">
|
|||
|
|
<button type="button" class="btn btn-sm btn-danger" @click="removeSalesOrderItem(Number(index))">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group form-group-spaced">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="openMaterialConsumptionModal">+ 消耗物料</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group form-group-spaced">
|
|||
|
|
<label class="form-label">已消耗物料</label>
|
|||
|
|
<div v-if="state.editingItem && state.editingItem.actual_material_cost > 0" class="material-consumption-list">
|
|||
|
|
<div class="material-consumption-header">
|
|||
|
|
<div class="material-consumption-item">物料名称</div>
|
|||
|
|
<div class="material-consumption-item">数量</div>
|
|||
|
|
<div class="material-consumption-item">单价</div>
|
|||
|
|
<div class="material-consumption-item">金额</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-for="(item, index) in state.consumedMaterials" :key="'consumed-material-' + index" class="material-consumption-row">
|
|||
|
|
<div class="material-consumption-item">{{ item.material_name }}</div>
|
|||
|
|
<div class="material-consumption-item">{{ item.quantity }}</div>
|
|||
|
|
<div class="material-consumption-item">{{ formatCurrency(item.unit_price) }}</div>
|
|||
|
|
<div class="material-consumption-item">{{ formatCurrency(item.amount) }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="material-consumption-total">
|
|||
|
|
<div class="material-consumption-item">合计</div>
|
|||
|
|
<div class="material-consumption-item"></div>
|
|||
|
|
<div class="material-consumption-item"></div>
|
|||
|
|
<div class="material-consumption-item">{{ formatCurrency(state.editingItem.actual_material_cost) }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="empty-state">
|
|||
|
|
<div class="empty-icon">📦</div>
|
|||
|
|
<div class="empty-title">暂无物料消耗记录</div>
|
|||
|
|
<div class="empty-desc">点击"消耗物料"按钮添加物料消耗明细</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">保存订单</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'purchaseOrder'" @submit.prevent="savePurchaseOrder">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">供应商 *</label>
|
|||
|
|
<select v-model.number="state.form.supplier_id" class="form-input" required>
|
|||
|
|
<option v-for="supplier in state.suppliers" :key="'purchase-supplier-' + supplier.id" :value="supplier.id">
|
|||
|
|
{{ supplier.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">预计到货</label>
|
|||
|
|
<div class="datetime-field" @click="openDateTimePicker('expected')">
|
|||
|
|
<input ref="expectedDateInput" v-model="state.form.expected_date" type="text" class="form-input air-datetime-input" readonly placeholder="选择日期时间" />
|
|||
|
|
<input ref="expectedDateNativeInput" v-model="state.form.expected_date_native" @change="state.form.expected_date = fromNativeValue(state.form.expected_date_native)" type="datetime-local" class="datetime-native" />
|
|||
|
|
<span class="datetime-field-icon" aria-hidden="true">📅</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="state.form.remark" class="form-input" placeholder="采购单备注" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group form-group-spaced">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="addPurchaseOrderItem">+ 添加物料</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.form.items.length === 0" class="empty-state">
|
|||
|
|
<div class="empty-icon">📦</div>
|
|||
|
|
<div class="empty-title">暂无物料</div>
|
|||
|
|
<div class="empty-desc">请点击"添加物料"按钮添加采购明细</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="purchase-order-items">
|
|||
|
|
<div v-for="(line, index) in state.form.items" :key="'purchase-order-line-' + index" class="purchase-order-item">
|
|||
|
|
<div class="purchase-order-item-content">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">物料 *</label>
|
|||
|
|
<select v-model.number="line.product_id" class="form-input" required>
|
|||
|
|
<option value="">请选择物料</option>
|
|||
|
|
<option v-for="material in state.materials" :key="'purchase-order-material-' + material.id" :value="material.id">
|
|||
|
|
{{ material.sku }} - {{ material.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-row">
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">数量 *</label>
|
|||
|
|
<input v-model.number="line.quantity" type="number" min="1" class="form-input" placeholder="请输入数量" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">单价</label>
|
|||
|
|
<div class="info-display">
|
|||
|
|
¥{{ (state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0).toFixed(2) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">总价</label>
|
|||
|
|
<div class="info-display">
|
|||
|
|
¥{{ ((state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0) * (line.quantity || 0)).toFixed(2) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-actions">
|
|||
|
|
<button type="button" class="btn btn-sm btn-danger" @click="removePurchaseOrderItem(Number(index))">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">保存采购单</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'purchaseReceive'" @submit.prevent="receivePurchaseOrder">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">入库仓库 *</label>
|
|||
|
|
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
|||
|
|
<option v-for="warehouse in state.warehouses" :key="'purchase-receive-warehouse-' + warehouse.id" :value="warehouse.id">
|
|||
|
|
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="state.form.remark" class="form-input" placeholder="到货说明" />
|
|||
|
|
</div>
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>物料</th>
|
|||
|
|
<th>明细ID</th>
|
|||
|
|
<th>剩余待入库</th>
|
|||
|
|
<th>本次入库</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="line in state.purchaseReceiveItems" :key="'purchase-receive-line-' + line.item_id">
|
|||
|
|
<td>{{ line.material_label }}</td>
|
|||
|
|
<td>{{ line.item_id }}</td>
|
|||
|
|
<td>{{ line.remaining_quantity }}</td>
|
|||
|
|
<td><input v-model.number="line.receive_quantity" type="number" min="0" :max="line.remaining_quantity" class="form-input" required /></td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">确认入库</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'supplier'" @submit.prevent="saveSupplier">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">名称 *</label>
|
|||
|
|
<input v-model="state.form.name" class="form-input" required placeholder="供应商名称" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">联系人</label>
|
|||
|
|
<input v-model="state.form.contact_person" class="form-input" placeholder="联系人姓名" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">电话</label>
|
|||
|
|
<input v-model="state.form.phone" class="form-input" placeholder="联系电话" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">邮箱</label>
|
|||
|
|
<input v-model="state.form.email" type="email" class="form-input" placeholder="email@example.com" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">地址</label>
|
|||
|
|
<input v-model="state.form.address" class="form-input" placeholder="详细地址" />
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'customer'" @submit.prevent="saveCustomer">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">名称 *</label>
|
|||
|
|
<input v-model="state.form.name" class="form-input" required placeholder="客户名称" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">联系人</label>
|
|||
|
|
<input v-model="state.form.contact_person" class="form-input" placeholder="联系人姓名" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">电话</label>
|
|||
|
|
<input v-model="state.form.phone" class="form-input" placeholder="联系电话" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">邮箱</label>
|
|||
|
|
<input v-model="state.form.email" type="email" class="form-input" placeholder="email@example.com" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">地址</label>
|
|||
|
|
<input v-model="state.form.address" class="form-input" placeholder="详细地址" />
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<form v-else-if="state.modalType === 'inventoryItem'" @submit.prevent="saveInventoryItem">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">物料 *</label>
|
|||
|
|
<select v-model.number="state.form.product_id" class="form-input" required>
|
|||
|
|
<option v-if="!state.materials.length" :value="null" disabled>暂无物料,请先新增物料</option>
|
|||
|
|
<option v-for="product in state.materials" :key="'stockin-product-' + product.id" :value="product.id">
|
|||
|
|
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">仓库 *</label>
|
|||
|
|
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
|||
|
|
<option v-if="!state.warehouses.length" :value="null" disabled>暂无仓库,系统将自动创建默认仓库</option>
|
|||
|
|
<option v-for="warehouse in state.warehouses" :key="'stockin-warehouse-' + warehouse.id" :value="warehouse.id">
|
|||
|
|
{{ warehouse.name }}{{ warehouse.code ? ' (' + warehouse.code + ')' : '' }}{{ warehouse.is_default ? ' [默认]' : '' }}(ID: {{ warehouse.id }})
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">数量 *</label>
|
|||
|
|
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="库存数量" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">锁定数量</label>
|
|||
|
|
<input v-model.number="state.form.locked_quantity" type="number" class="form-input" placeholder="锁定库存" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">批次号</label>
|
|||
|
|
<input v-model="state.form.batch_number" class="form-input" placeholder="批次号(可选)" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">库位</label>
|
|||
|
|
<input v-model="state.form.location" class="form-input" placeholder="库位(可选)" />
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
|||
|
|
<button type="submit" class="btn btn-primary" :disabled="!state.form.product_id || !state.form.warehouse_id">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 物料消耗模态框 -->
|
|||
|
|
<div v-if="state.showMaterialConsumptionModal" class="modal-overlay" @click.self="state.showMaterialConsumptionModal = false">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h3>添加物料消耗</h3>
|
|||
|
|
<button class="modal-close" @click="state.showMaterialConsumptionModal = false">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<div class="form-group form-group-spaced">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="addMaterialConsumptionItem">+ 添加物料</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.materialConsumptionItems.length === 0" class="empty-state">
|
|||
|
|
<div class="empty-icon">📦</div>
|
|||
|
|
<div class="empty-title">暂无物料</div>
|
|||
|
|
<div class="empty-desc">请点击"添加物料"按钮添加物料消耗明细</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="purchase-order-items">
|
|||
|
|
<div v-for="(line, index) in state.materialConsumptionItems" :key="'material-consumption-line-' + index" class="purchase-order-item">
|
|||
|
|
<div class="purchase-order-item-content">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">物料 *</label>
|
|||
|
|
<select v-model.number="line.material_id" class="form-input" required>
|
|||
|
|
<option value="">请选择物料</option>
|
|||
|
|
<option v-for="material in state.materials" :key="'consumption-material-' + material.id" :value="material.id">
|
|||
|
|
{{ material.sku }} - {{ material.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-row">
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">数量 *</label>
|
|||
|
|
<input v-model.number="line.quantity" type="number" min="1" class="form-input" placeholder="请输入数量" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">单价</label>
|
|||
|
|
<div class="info-display">
|
|||
|
|
¥{{ (state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0).toFixed(2) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">总价</label>
|
|||
|
|
<div class="info-display">
|
|||
|
|
¥{{ ((state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)).toFixed(2) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-actions">
|
|||
|
|
<button type="button" class="btn btn-sm btn-danger" @click="removeMaterialConsumptionItem(index)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="state.showMaterialConsumptionModal = false">取消</button>
|
|||
|
|
<button type="button" class="btn btn-primary" @click="saveMaterialConsumption">保存物料消耗</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 物料补货模态框 -->
|
|||
|
|
<div v-if="state.showRestockModal" class="modal-overlay" @click.self="state.showRestockModal = false">
|
|||
|
|
<div class="modal-content restock-modal">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h3>物料补货</h3>
|
|||
|
|
<button class="modal-close" @click="state.showRestockModal = false">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<div class="form-section">
|
|||
|
|
<div class="section-header">
|
|||
|
|
<h4>补货物料明细</h4>
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="addRestockItem">+ 添加物料</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="state.restockItems.length === 0" class="empty-state">
|
|||
|
|
<div class="empty-icon">📦</div>
|
|||
|
|
<div class="empty-desc">请点击"添加物料"按钮添加补货物料明细</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="purchase-order-items">
|
|||
|
|
<div v-for="(line, index) in state.restockItems" :key="'restock-line-' + index" class="purchase-order-item">
|
|||
|
|
<div class="purchase-order-item-row">
|
|||
|
|
<div class="purchase-order-item-column restock-col-material">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">物料 <span class="required">*</span></label>
|
|||
|
|
<select v-model.number="line.material_id" class="form-input" required>
|
|||
|
|
<option v-for="material in state.materials" :key="'restock-material-' + material.id" :value="material.id">
|
|||
|
|
{{ material.sku }} - {{ material.name }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">数量 <span class="required">*</span></label>
|
|||
|
|
<input v-model.number="line.quantity" type="number" class="form-input" min="1" step="1" required />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">单价</label>
|
|||
|
|
<input v-model.number="line.unit_price" type="number" class="form-input" min="0" step="0.01" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">总价</label>
|
|||
|
|
<div class="info-display">
|
|||
|
|
¥{{ ((line.unit_price || 0) * (line.quantity || 0)).toFixed(2) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-column">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注</label>
|
|||
|
|
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="purchase-order-item-actions">
|
|||
|
|
<button type="button" class="btn btn-sm btn-danger" @click="removeRestockItem(index)">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" @click="state.showRestockModal = false">取消</button>
|
|||
|
|
<button type="button" class="btn btn-primary" @click="saveRestock">创建采购订单</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.mb-12 { margin-bottom: 12px; }
|
|||
|
|
.mb-16 { margin-bottom: 16px; }
|
|||
|
|
.mt-16 { margin-top: 16px; }
|
|||
|
|
.mr-8 { margin-right: 8px; }
|
|||
|
|
.text-secondary { color: var(--text-secondary); }
|
|||
|
|
.flex-row-center { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|||
|
|
.w-260 { width: 260px; }
|
|||
|
|
.w-inline-120 { width: 120px; display: inline-block; }
|
|||
|
|
.w-inline-140 { width: 140px; display: inline-block; }
|
|||
|
|
.info-display { padding: 8px 12px; background: var(--bg-secondary); }
|
|||
|
|
.form-group-spaced { margin-bottom: var(--space-4); }
|
|||
|
|
|
|||
|
|
.bom-add-btn { margin-bottom: 8px; }
|
|||
|
|
|
|||
|
|
.btn-disabled {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.datetime-field {
|
|||
|
|
position: relative;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.air-datetime-input {
|
|||
|
|
cursor: pointer;
|
|||
|
|
background: var(--bg-primary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.datetime-native {
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 0;
|
|||
|
|
opacity: 0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.datetime-field-icon {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 10px;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
pointer-events: none;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.required {
|
|||
|
|
color: var(--error);
|
|||
|
|
font-weight: var(--font-bold);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-items {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item {
|
|||
|
|
border: 1px solid var(--border-default);
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-header {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0;
|
|||
|
|
background: var(--bg-tertiary);
|
|||
|
|
border-bottom: 1px solid var(--border-default);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-header .btn {
|
|||
|
|
border-radius: 0;
|
|||
|
|
border: none;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-header .btn.active {
|
|||
|
|
background: var(--primary-500);
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-content {
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-row {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-column {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 120px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sales-order-item-actions {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
padding-bottom: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-items {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-item {
|
|||
|
|
border: 1px solid var(--border-default);
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-item-content {
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-item-row {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-item-column {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 120px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.purchase-order-item-actions {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
padding-bottom: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-list {
|
|||
|
|
border: 1px solid var(--border-default);
|
|||
|
|
border-radius: var(--radius-md);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-header {
|
|||
|
|
display: flex;
|
|||
|
|
background: var(--bg-tertiary);
|
|||
|
|
font-weight: var(--font-semibold);
|
|||
|
|
border-bottom: 1px solid var(--border-default);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-row {
|
|||
|
|
display: flex;
|
|||
|
|
border-bottom: 1px solid var(--border-light);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-row:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-total {
|
|||
|
|
display: flex;
|
|||
|
|
background: var(--bg-tertiary);
|
|||
|
|
font-weight: var(--font-bold);
|
|||
|
|
border-top: 1px solid var(--border-default);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-consumption-item {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
min-width: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.bom-section {
|
|||
|
|
margin-top: 12px;
|
|||
|
|
padding-top: 12px;
|
|||
|
|
border-top: 1px solid var(--border-default);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-section {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.restock-modal {
|
|||
|
|
max-width: 900px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.restock-col-material {
|
|||
|
|
flex: 2;
|
|||
|
|
}
|
|||
|
|
</style>
|