This commit is contained in:
2026-07-30 10:30:50 +08:00
parent cf6d708566
commit 853c478657
85 changed files with 4711 additions and 1052 deletions
+463
View File
@@ -0,0 +1,463 @@
import { defineStore } from 'pinia'
import { reactive, ref, computed } from 'vue'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
import { formatCurrency, formatNumber, formatDateTime, formatDate } from '@/shared/utils'
import type { Schema } from '@/types/schemas'
declare const AirDatepicker: any
// Dashboard has no Pydantic schema in backend, define it here
interface DashboardData {
finished_product_count: number
total_stock: number
total_value: number
supplier_count: number
customer_count: number
warehouse_count: number
}
export const useInventoryStore = defineStore('inventory', () => {
// ── state ──
const activeTab = ref('dashboard')
const backendDbReady = ref(true)
const backendDbMessage = ref('')
const productCategory = ref('finished')
const dashboard = ref<DashboardData | null>(null)
const financeSummary = ref<Schema<'FinanceSummaryResponse'> | null>(null)
const financePeriod = reactive({ year: new Date().getFullYear(), quarter: '' as string })
const financeTransactions = ref<Schema<'FinanceTransactionResponse'>[]>([])
const receivables = ref<Schema<'ReceivableItemResponse'>[]>([])
const payables = ref<Schema<'PayableItemResponse'>[]>([])
const customerFinanceStatement = ref<Schema<'PartnerStatementItemResponse'>[]>([])
const supplierFinanceStatement = ref<Schema<'PartnerStatementItemResponse'>[]>([])
const customerProductStatement = ref<Schema<'PartnerProductStatementItemResponse'>[]>([])
const supplierProductStatement = ref<Schema<'PartnerProductStatementItemResponse'>[]>([])
const products = ref<Schema<'ProductResponse'>[]>([])
const materials = ref<Schema<'ProductResponse'>[]>([])
const finishedProducts = ref<Schema<'ProductResponse'>[]>([])
const purchaseOrders = ref<Schema<'PurchaseOrderResponse'>[]>([])
const purchaseWarehouseId = ref<number | null>(null)
const purchaseReceiveItems = ref<any[]>([])
const productionOrders = ref<Schema<'SalesOrderResponse'>[]>([])
const productionPlan = ref<any>(null)
const productionWarehouseId = ref<number | null>(null)
const suppliers = ref<Schema<'SupplierResponse'>[]>([])
const customers = ref<Schema<'CustomerResponse'>[]>([])
const warehouses = ref<Schema<'WarehouseResponse'>[]>([])
const inventory = ref<Schema<'InventoryResponse'>[]>([])
const movements = ref<Schema<'StockMovementResponse'>[]>([])
const loading = ref(false)
const showModal = ref(false)
const modalType = ref('')
const editingItem = ref<any>(null)
const productBomItems = ref<any[]>([])
const materialConsumptionItems = ref<any[]>([])
const showMaterialConsumptionModal = ref(false)
const consumedMaterials = ref<any[]>([])
const restockItems = ref<any[]>([])
const showRestockModal = ref(false)
const form = ref<any>({})
// ── date-picker refs ──
let deliveryPicker: any = null
let expectedPicker: any = null
const deliveryDateInput = ref<HTMLElement | null>(null)
const expectedDateInput = ref<HTMLElement | null>(null)
const deliveryDateNativeInput = ref<HTMLElement | null>(null)
const expectedDateNativeInput = ref<HTMLElement | null>(null)
// ── legacy state object (backward-compat for components still using `state.xxx`) ──
const state = reactive({
get activeTab() { return activeTab.value }, set activeTab(v) { activeTab.value = v },
get backendDbReady() { return backendDbReady.value }, set backendDbReady(v) { backendDbReady.value = v },
get backendDbMessage() { return backendDbMessage.value }, set backendDbMessage(v) { backendDbMessage.value = v },
get productCategory() { return productCategory.value }, set productCategory(v) { productCategory.value = v },
get dashboard() { return dashboard.value }, set dashboard(v) { dashboard.value = v },
get financeSummary() { return financeSummary.value }, set financeSummary(v) { financeSummary.value = v },
financePeriod,
get financeTransactions() { return financeTransactions.value }, set financeTransactions(v) { financeTransactions.value = v },
get receivables() { return receivables.value }, set receivables(v) { receivables.value = v },
get payables() { return payables.value }, set payables(v) { payables.value = v },
get customerFinanceStatement() { return customerFinanceStatement.value }, set customerFinanceStatement(v) { customerFinanceStatement.value = v },
get supplierFinanceStatement() { return supplierFinanceStatement.value }, set supplierFinanceStatement(v) { supplierFinanceStatement.value = v },
get customerProductStatement() { return customerProductStatement.value }, set customerProductStatement(v) { customerProductStatement.value = v },
get supplierProductStatement() { return supplierProductStatement.value }, set supplierProductStatement(v) { supplierProductStatement.value = v },
get products() { return products.value }, set products(v) { products.value = v },
get materials() { return materials.value }, set materials(v) { materials.value = v },
get finishedProducts() { return finishedProducts.value }, set finishedProducts(v) { finishedProducts.value = v },
get purchaseOrders() { return purchaseOrders.value }, set purchaseOrders(v) { purchaseOrders.value = v },
get purchaseWarehouseId() { return purchaseWarehouseId.value }, set purchaseWarehouseId(v) { purchaseWarehouseId.value = v },
get purchaseReceiveItems() { return purchaseReceiveItems.value }, set purchaseReceiveItems(v) { purchaseReceiveItems.value = v },
get productionOrders() { return productionOrders.value }, set productionOrders(v) { productionOrders.value = v },
get productionPlan() { return productionPlan.value }, set productionPlan(v) { productionPlan.value = v },
get productionWarehouseId() { return productionWarehouseId.value }, set productionWarehouseId(v) { productionWarehouseId.value = v },
get suppliers() { return suppliers.value }, set suppliers(v) { suppliers.value = v },
get customers() { return customers.value }, set customers(v) { customers.value = v },
get warehouses() { return warehouses.value }, set warehouses(v) { warehouses.value = v },
get inventory() { return inventory.value }, set inventory(v) { inventory.value = v },
get movements() { return movements.value }, set movements(v) { movements.value = v },
get loading() { return loading.value }, set loading(v) { loading.value = v },
get showModal() { return showModal.value }, set showModal(v) { showModal.value = v },
get modalType() { return modalType.value }, set modalType(v) { modalType.value = v },
get editingItem() { return editingItem.value }, set editingItem(v) { editingItem.value = v },
get productBomItems() { return productBomItems.value }, set productBomItems(v) { productBomItems.value = v },
get materialConsumptionItems() { return materialConsumptionItems.value }, set materialConsumptionItems(v) { materialConsumptionItems.value = v },
get showMaterialConsumptionModal() { return showMaterialConsumptionModal.value }, set showMaterialConsumptionModal(v) { showMaterialConsumptionModal.value = v },
get consumedMaterials() { return consumedMaterials.value }, set consumedMaterials(v) { consumedMaterials.value = v },
get restockItems() { return restockItems.value }, set restockItems(v) { restockItems.value = v },
get showRestockModal() { return showRestockModal.value }, set showRestockModal(v) { showRestockModal.value = v },
get form() { return form.value }, set form(v) { form.value = v },
})
// ── helpers ──
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)
}
// ── pickers ──
const destroyPickers = () => {
if (deliveryPicker) { deliveryPicker.destroy(); deliveryPicker = null }
if (expectedPicker) { expectedPicker.destroy(); expectedPicker = null }
}
const initPickers = () => {
destroyPickers()
if (typeof AirDatepicker !== 'function') return
if (modalType.value === 'salesOrder' && deliveryDateInput.value) {
deliveryPicker = new AirDatepicker(deliveryDateInput.value, {
timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd',
onSelect: ({ formattedDate }: any) => {
form.value.delivery_date = formattedDate || ''
form.value.delivery_date_native = toNativeValue(formattedDate || '')
}
})
const initial = parseDateTimeLocal(form.value.delivery_date)
if (initial) deliveryPicker.selectDate(initial, { silent: true })
}
if (modalType.value === 'purchaseOrder' && expectedDateInput.value) {
expectedPicker = new AirDatepicker(expectedDateInput.value, {
timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd',
onSelect: ({ formattedDate }: any) => {
form.value.expected_date = formattedDate || ''
form.value.expected_date_native = toNativeValue(formattedDate || '')
}
})
const initial = parseDateTimeLocal(form.value.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()
}
// ── label helpers ──
const getMovementTypeLabel = (movementType: string): string => {
const map: 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 map[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 map: Record<string, string> = { draft: '已下单', pending: '已下单', partial_received: '部分收货', received: '已收货', paid: '已付款', cancelled: '已作废' }
return map[status] || status
}
const isPurchaseOrderLocked = (status: string): boolean => ['received', 'paid', 'cancelled'].includes(status)
const getSalesOrderStatusLabel = (status: string): string => {
const map: Record<string, string> = { manufacturing: '制造中', delivered: '已交付', paid: '已收款', cancelled: '已作废' }
return map[status] || status
}
const getDeliveryStatusLabel = (ds: string): string => {
const map: Record<string, string> = { manufacturing: '制造中', delivered: '已交付', cancelled: '已作废' }
return map[ds] || ds
}
const getPaymentStatusLabel = (ps: string): string => {
const map: Record<string, string> = { unpaid: '未收款', paid: '已收款' }
return map[ps] || ps
}
const getReceiptStatusLabel = (rs: string): string => {
const map: Record<string, string> = { pending: '已下单', partial_received: '部分收货', received: '已收货', cancelled: '已作废' }
return map[rs] || rs
}
// ── data loading ──
const checkBackendHealth = async () => {
try {
const resp = await fetch('/health', { method: 'GET' })
if (!resp.ok) { backendDbReady.value = false; backendDbMessage.value = '后端服务异常,暂无法加载业务数据'; return }
const health = await resp.json().catch(() => null)
if (health && health.database_connected === false) { backendDbReady.value = false; backendDbMessage.value = '数据库未连接,当前仅可浏览界面,业务数据暂不可用'; return }
backendDbReady.value = true; backendDbMessage.value = ''
} catch { backendDbReady.value = false; backendDbMessage.value = '无法连接后端服务' }
}
const loadDashboard = async () => {
loading.value = true
try { dashboard.value = await apiRequest('/api/dashboard') }
catch (e) { handleApiError(e, '加载仪表盘') }
finally { loading.value = false }
}
const loadFinishedProducts = async () => {
loading.value = true
try { finishedProducts.value = await apiRequest('/api/products?item_type=finished&limit=100') }
catch (e) { handleApiError(e, '加载成品') }
finally { loading.value = false }
}
const loadProducts = async () => { await loadFinishedProducts() }
const loadMaterials = async () => {
loading.value = true
try { materials.value = await apiRequest('/api/products?item_type=material&limit=100') }
catch (e) { handleApiError(e, '加载物料') }
finally { loading.value = false }
}
const loadWarehouses = async () => {
loading.value = true
try { warehouses.value = await apiRequest('/api/warehouses') }
catch (e) { handleApiError(e, '加载仓库') }
finally { loading.value = false }
}
const ensureStockBaseData = async () => {
if (!materials.value.length) await loadMaterials()
if (!warehouses.value.length) await loadWarehouses()
if (!warehouses.value.length) {
try {
await apiRequest('/api/warehouses', { method: 'POST', body: JSON.stringify({ name: '默认仓库' }) })
await loadWarehouses()
addNotification('已自动创建默认仓库', 'success')
} catch (e) { handleApiError(e, '自动创建默认仓库') }
}
}
const loadSuppliers = async () => {
loading.value = true
try { suppliers.value = await apiRequest('/api/suppliers') }
catch (e) { handleApiError(e, '加载供应商') }
finally { loading.value = false }
}
const loadProductionOrders = async () => {
loading.value = true
try {
const [orders, wh] = await Promise.all([apiRequest('/api/sales-orders?limit=100'), apiRequest('/api/warehouses')])
productionOrders.value = orders?.items || []
warehouses.value = wh || []
if (!productionWarehouseId.value) {
productionWarehouseId.value = warehouses.value.find((w: any) => w.is_default)?.id || warehouses.value[0]?.id || null
}
} catch (e) { handleApiError(e, '加载按单生产数据') }
finally { loading.value = false }
}
const loadPurchaseOrders = async () => {
loading.value = true
try {
const [orders, wh] = await Promise.all([apiRequest('/api/purchase-orders?limit=100'), apiRequest('/api/warehouses')])
purchaseOrders.value = orders?.items || []
warehouses.value = wh || []
if (!purchaseWarehouseId.value) {
purchaseWarehouseId.value = warehouses.value.find((w: any) => w.is_default)?.id || warehouses.value[0]?.id || null
}
} catch (e) { handleApiError(e, '加载采购订单') }
finally { loading.value = false }
}
const loadCustomers = async () => {
loading.value = true
try { customers.value = await apiRequest('/api/customers') }
catch (e) { handleApiError(e, '加载客户') }
finally { loading.value = false }
}
const loadInventory = async () => {
loading.value = true
try { inventory.value = (await apiRequest('/api/inventory'))?.items || [] }
catch (e) { handleApiError(e, '加载库存') }
finally { loading.value = false }
}
const loadMovements = async () => {
loading.value = true
try { movements.value = (await apiRequest('/api/stock-movements'))?.items || [] }
catch (e) { handleApiError(e, '加载变动记录') }
finally { loading.value = false }
}
const loadFinance = async () => {
loading.value = true
try {
const selectedYear = Number(financePeriod.year) || new Date().getFullYear()
const selectedQuarter = financePeriod.quarter ? Number(financePeriod.quarter) : null
const periodQuery = selectedQuarter ? `year=${selectedYear}&quarter=${selectedQuarter}` : `year=${selectedYear}`
const [summary, transactions, recv, pay, custStmt, suppStmt, custProdStmt, suppProdStmt] = 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}`)
])
financeSummary.value = summary
financeTransactions.value = transactions?.items || []
receivables.value = recv
payables.value = pay
customerFinanceStatement.value = custStmt.items || []
supplierFinanceStatement.value = suppStmt.items || []
customerProductStatement.value = custProdStmt.items || []
supplierProductStatement.value = suppProdStmt.items || []
} catch (e) { handleApiError(e, '加载财务数据') }
finally { loading.value = false }
}
const refreshFinanceByPeriod = () => { if (activeTab.value === 'finance') loadFinance() }
const switchTab = (tab: string) => { activeTab.value = tab }
const closeModal = () => {
showModal.value = false; modalType.value = ''; editingItem.value = null
productBomItems.value = []; purchaseReceiveItems.value = []; form.value = {}
destroyPickers()
}
const modalTitle = computed(() => {
const prefix = editingItem.value ? '编辑' : '新增'
const typeMap: Record<string, string> = {
product: form.value.item_type === 'finished' ? '成品' : '物料',
inventoryItem: '物料库存', salesOrder: '销售订单', purchaseOrder: '采购订单',
purchaseReceive: '采购到货入库', supplier: '供应商', customer: '客户'
}
return prefix + (typeMap[modalType.value] || '')
})
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 === activeTab.value)
if (item) return { group, item }
}
return null
})
const handleMenuClick = (itemKey: string) => { switchTab(itemKey) }
const switchProductCategory = (category: string) => { productCategory.value = category }
return {
// state refs (new Pinia style)
activeTab, backendDbReady, backendDbMessage, productCategory, dashboard,
financeSummary, financePeriod, financeTransactions, receivables, payables,
customerFinanceStatement, supplierFinanceStatement, customerProductStatement, supplierProductStatement,
products, materials, finishedProducts, purchaseOrders, purchaseWarehouseId, purchaseReceiveItems,
productionOrders, productionPlan, productionWarehouseId, suppliers, customers, warehouses,
inventory, movements, loading, showModal, modalType, editingItem, productBomItems,
materialConsumptionItems, showMaterialConsumptionModal, consumedMaterials,
restockItems, showRestockModal, form,
// legacy state object (backward compat)
state,
// refs
deliveryDateInput, expectedDateInput, deliveryDateNativeInput, expectedDateNativeInput,
// computed
menuGroups, openGroups, activeMenu, modalTitle,
// actions
parseDateTimeLocal, toPickerValue, toApiDateTime, toNativeValue, fromNativeValue,
destroyPickers, initPickers, openDateTimePicker,
getMovementTypeLabel, getMovementBadgeClass, getPurchaseOrderStatusLabel, isPurchaseOrderLocked,
getSalesOrderStatusLabel, getDeliveryStatusLabel, getPaymentStatusLabel, getReceiptStatusLabel,
checkBackendHealth, loadDashboard, loadFinishedProducts, loadProducts, loadMaterials,
loadWarehouses, ensureStockBaseData, loadSuppliers, loadProductionOrders, loadPurchaseOrders,
loadCustomers, loadInventory, loadMovements, loadFinance, refreshFinanceByPeriod,
switchTab, closeModal, toggleGroup, handleMenuClick, switchProductCategory,
formatCurrency, formatNumber, formatDateTime, formatDate
}
})