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
@@ -1,7 +1,15 @@
<script setup lang="ts">
import { useInventory } from '@/modules/inventory/composables/useInventory'
import { useRouter } from 'vue-router'
import { useInventoryStore } from '@/stores/inventory'
const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInventory()
const router = useRouter()
const store = useInventoryStore()
const { menuGroups, openGroups, toggleGroup } = store
const navigateTo = (itemKey: string) => {
store.activeTab = itemKey
router.push(`/inventory/${itemKey}`)
}
</script>
<template>
@@ -15,8 +23,8 @@ const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInven
<div
v-for="item in group.items"
:key="item.key"
:class="['sidebar-item', { active: state.activeTab === item.key }]"
@click="handleMenuClick(item.key)"
:class="['sidebar-item', { active: store.activeTab === item.key }]"
@click="navigateTo(item.key)"
>
{{ item.label }}
</div>
@@ -3,7 +3,9 @@ import { ref, computed, onMounted } from 'vue'
import { DialogPlugin } from 'tdesign-vue-next'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { inventoryApi } from '@/shared/api-client'
import { addNotification, handleApiError } from '@/shared/notification'
import type { Schema } from '@/types/schemas'
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
return new Promise((resolve) => {
@@ -47,6 +49,46 @@ const receiveWarehouseId = ref<number | null>(null)
const receiveRemark = ref('')
const receiving = ref(false)
// ── 采购需求推导 ──
const showDemandModal = ref(false)
const demandLoading = ref(false)
const salesOrdersForDemand = ref<Schema<'SalesOrderResponse'>[]>([])
const selectedSalesOrderIds = ref<number[]>([])
const demandResult = ref<Schema<'PurchaseDemandResponse'> | null>(null)
async function openDemandDialog() {
showDemandModal.value = true
demandResult.value = null
selectedSalesOrderIds.value = []
try {
const data = await inventoryApi.listSalesOrders({ limit: 100 })
salesOrdersForDemand.value = (data as any)?.items || data || []
} catch (e) {
handleApiError(e, '加载销售订单')
}
}
async function calculateDemands() {
if (!selectedSalesOrderIds.value.length) {
addNotification('请选择至少一个销售订单', 'warning')
return
}
demandLoading.value = true
try {
demandResult.value = await inventoryApi.calculatePurchaseDemands(selectedSalesOrderIds.value)
} catch (e) {
handleApiError(e, '计算采购需求')
} finally {
demandLoading.value = false
}
}
function closeDemandModal() {
showDemandModal.value = false
demandResult.value = null
selectedSalesOrderIds.value = []
}
function openCreateOrder() {
editingItem.value = null
form.value = {
@@ -299,6 +341,7 @@ onMounted(() => {
</t-select>
<t-button @click="loadPurchaseOrders">刷新</t-button>
<t-button type="primary" @click="openCreateOrder">新增采购订单</t-button>
<t-button theme="success" variant="outline" @click="openDemandDialog">采购建议</t-button>
</div>
<t-table :data="state.purchaseOrders" :loading="state.loading" stripe>
@@ -515,6 +558,87 @@ onMounted(() => {
<t-button type="primary" :loading="receiving" :disabled="receiving" @click="receivePurchaseOrder">确认入库</t-button>
</template>
</t-dialog>
<!-- 采购需求推导对话框 -->
<t-dialog
v-model:visible="showDemandModal"
title="采购需求推导"
width="1000px"
@closed="closeDemandModal"
>
<div style="margin-bottom: 16px;">
<p style="margin-bottom: 8px; color: var(--td-text-color-secondary);">
选择销售订单,系统将自动按 BOM 展开物料需求、对比库存、推荐供应商。
</p>
<div style="display: flex; gap: 12px; align-items: center;">
<t-select
v-model="selectedSalesOrderIds"
multiple
placeholder="请选择销售订单"
style="flex: 1;"
:loading="salesOrdersForDemand.length === 0"
>
<t-option
v-for="order in salesOrdersForDemand"
:key="order.id"
:label="`${order.order_no} - ${order.customer_name || ''}`"
:value="order.id"
/>
</t-select>
<t-button
type="primary"
:loading="demandLoading"
:disabled="!selectedSalesOrderIds.length"
@click="calculateDemands"
>
计算
</t-button>
</div>
</div>
<template v-if="demandResult">
<div style="display: flex; gap: 24px; margin-bottom: 12px; font-weight: 600;">
<span>来源订单:{{ demandResult.source_order_nos?.join(', ') || '-' }}</span>
<span>缺货物料:<t-tag :type="demandResult.shortage_count > 0 ? 'danger' : 'success'" size="small">{{ demandResult.shortage_count }}</t-tag></span>
<span>预计采购总额:{{ formatCurrency(Number(demandResult.total_estimated_cost || 0)) }}</span>
</div>
<t-table :data="demandResult.items || []" stripe border size="small" max-height="400">
<t-table-column prop="material_sku" label="物料SKU" width="120" />
<t-table-column prop="material_name" label="物料名称" min-width="140" />
<t-table-column label="需求量" width="90">
<template #default="{ row }">{{ row.required_quantity }}</template>
</t-table-column>
<t-table-column label="库存量" width="90">
<template #default="{ row }">{{ row.available_quantity }}</template>
</t-table-column>
<t-table-column label="缺口" width="90">
<template #default="{ row }">
<t-tag :type="row.shortage_quantity > 0 ? 'danger' : 'success'" size="small">
{{ row.shortage_quantity }}
</t-tag>
</template>
</t-table-column>
<t-table-column label="单价" width="90">
<template #default="{ row }">{{ formatCurrency(Number(row.unit_cost || 0)) }}</template>
</t-table-column>
<t-table-column label="预计金额" width="110">
<template #default="{ row }">{{ formatCurrency(Number(row.estimated_cost || 0)) }}</template>
</t-table-column>
<t-table-column prop="suggested_supplier_name" label="建议供应商" min-width="120">
<template #default="{ row }">{{ row.suggested_supplier_name || '-' }}</template>
</t-table-column>
<t-table-column label="交期(天)" width="80">
<template #default="{ row }">{{ row.supplier_lead_time ?? '-' }}</template>
</t-table-column>
</t-table>
<t-empty v-if="(demandResult.items || []).length === 0" description="无物料需求(BOM 为空或订单无明细)" />
</template>
<template #footer>
<t-button @click="showDemandModal = false">关闭</t-button>
</template>
</t-dialog>
</div>
</template>