feat(web-admin): enter archive inbound/outbound qty by brand
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
cc2edfc61f
commit
38b1613fee
3 files changed
+179
-1
No files matched your search
@@ -23,6 +23,43 @@
|
||||
show-actual-qty
|
||||
editable-actual-qty-only
|
||||
/>
|
||||
<div
|
||||
v-for="row in brandedProductRows"
|
||||
:key="row.id || row.productId"
|
||||
class="pl-archive-brand-qty-block"
|
||||
>
|
||||
<div class="pl-archive-brand-qty-head">
|
||||
<span class="pl-archive-brand-qty-title">{{ row.productName }} · 按品牌填量</span>
|
||||
<el-tag v-if="row.brandMode === 'required'" size="small" type="warning">指定品牌</el-tag>
|
||||
<el-tag v-else size="small" type="info">不限品牌</el-tag>
|
||||
<span
|
||||
v-if="!isBrandQtySumMatch(row)"
|
||||
class="pl-archive-brand-qty-warn"
|
||||
>
|
||||
各品牌合计须等于实际数量({{ Number(row.actualQty ?? 0) }})
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="row.brandQtyRows || []" stripe size="small" class="list-table">
|
||||
<el-table-column label="品牌" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row: brandRow }">
|
||||
{{ brandRow.brandName || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" min-width="140" align="right">
|
||||
<template #default="{ row: brandRow }">
|
||||
<el-input-number
|
||||
v-model="brandRow.qty"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="row.brandMode === 'required'"
|
||||
style="width: 100%"
|
||||
@change="() => onBrandQtyChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section pl-plan-freight-section">
|
||||
@@ -114,6 +151,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { fetchProductDetail, type ProductDetail } from '@/api/crm/product'
|
||||
import {
|
||||
inboundOrderApi,
|
||||
outboundOrderApi,
|
||||
@@ -193,12 +231,50 @@ const dialogTitle = computed(() => {
|
||||
|
||||
const submitLabel = computed(() => (isAdjustMode.value ? '确认保存' : '确认归档'))
|
||||
|
||||
const brandedProductRows = computed(() =>
|
||||
productRows.value.filter(row => row.brandMode === 'required' || row.brandMode === 'any')
|
||||
)
|
||||
|
||||
function brandQtySum(row: PlanFormProductRow): number {
|
||||
return (row.brandQtyRows || []).reduce((sum, item) => sum + Number(item.qty || 0), 0)
|
||||
}
|
||||
|
||||
function isBrandQtySumMatch(row: PlanFormProductRow): boolean {
|
||||
if (!row.brandMode) return true
|
||||
return Math.abs(brandQtySum(row) - Number(row.actualQty ?? 0)) < 0.0005
|
||||
}
|
||||
|
||||
function onBrandQtyChange(row: PlanFormProductRow) {
|
||||
if (row.brandMode === 'any') {
|
||||
row.actualQty = Number(brandQtySum(row).toFixed(3))
|
||||
}
|
||||
}
|
||||
|
||||
function syncRequiredBrandQty(row: PlanFormProductRow) {
|
||||
if (row.brandMode !== 'required' || !row.brandQtyRows?.length) return
|
||||
row.brandQtyRows[0].qty = Number(row.actualQty ?? 0)
|
||||
}
|
||||
|
||||
watch(
|
||||
productRows,
|
||||
rows => {
|
||||
rows.forEach(row => {
|
||||
if (row.brandMode === 'required') syncRequiredBrandQty(row)
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
function mapPlanProducts(products: PlanProduct[]): PlanFormProductRow[] {
|
||||
return products.map((item, index) => ({
|
||||
id: item.id,
|
||||
productId: String(item.productId || index),
|
||||
productNo: item.productNo || String(item.productId || index),
|
||||
productName: item.productName || '',
|
||||
brandMode: item.brandMode,
|
||||
brandId: item.brandId,
|
||||
brandName: item.brandName,
|
||||
brandQtyRows: [],
|
||||
model: item.model,
|
||||
categoryName: item.categoryName,
|
||||
unit: item.unit || '吨',
|
||||
@@ -215,6 +291,55 @@ function mapPlanProducts(products: PlanProduct[]): PlanFormProductRow[] {
|
||||
}))
|
||||
}
|
||||
|
||||
async function initBrandQtyRows(rows: PlanFormProductRow[]) {
|
||||
const needDetailIds = [
|
||||
...new Set(
|
||||
rows
|
||||
.filter(row => row.brandMode === 'any' && row.productId)
|
||||
.map(row => row.productId)
|
||||
)
|
||||
]
|
||||
const detailMap = new Map<string, ProductDetail>()
|
||||
await Promise.all(
|
||||
needDetailIds.map(async productId => {
|
||||
try {
|
||||
const res = await fetchProductDetail({ id: productId })
|
||||
const detail = pickBxData<ProductDetail>(res)
|
||||
if (detail?.id) detailMap.set(productId, detail)
|
||||
} catch {
|
||||
// 品牌列表加载失败时保留空行,提交时由后端校验
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
rows.forEach(row => {
|
||||
const actual = Number(row.actualQty ?? 0)
|
||||
if (row.brandMode === 'required') {
|
||||
row.brandQtyRows = [
|
||||
{
|
||||
brandId: row.brandId || '',
|
||||
brandName: row.brandName || '指定品牌',
|
||||
qty: actual
|
||||
}
|
||||
]
|
||||
return
|
||||
}
|
||||
if (row.brandMode === 'any') {
|
||||
const brands = (detailMap.get(row.productId)?.brands || []).filter(
|
||||
b => (b.status || 'on') === 'on'
|
||||
)
|
||||
row.brandQtyRows = brands.map(b => ({
|
||||
brandId: b.id || '',
|
||||
brandName: b.brandName,
|
||||
qty: 0
|
||||
}))
|
||||
if (row.brandQtyRows.length === 1) {
|
||||
row.brandQtyRows[0].qty = actual
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
plan.value = {}
|
||||
productRows.value = []
|
||||
@@ -295,6 +420,7 @@ async function handleOpened() {
|
||||
// 无入库/出库单时保持计划原实际数量
|
||||
}
|
||||
}
|
||||
await initBrandQtyRows(productRows.value)
|
||||
await applyFreightPrefill(data)
|
||||
} catch (error) {
|
||||
innerVisible.value = false
|
||||
@@ -322,6 +448,11 @@ async function handleSubmit() {
|
||||
ElMessage.warning('请填写有效的实际数量')
|
||||
return
|
||||
}
|
||||
const brandMismatch = brandedProductRows.value.find(row => !isBrandQtySumMatch(row))
|
||||
if (brandMismatch) {
|
||||
ElMessage.warning(`「${brandMismatch.productName}」各品牌数量合计须等于实际数量`)
|
||||
return
|
||||
}
|
||||
if (
|
||||
formData.hasFreight === 1 &&
|
||||
formData.freightType === 'by_weight' &&
|
||||
@@ -350,7 +481,16 @@ async function handleSubmit() {
|
||||
const body: PlanArchiveBody = {
|
||||
products: productRows.value.map(row => ({
|
||||
id: row.id!,
|
||||
actualQty: Number(row.actualQty ?? 0)
|
||||
actualQty: Number(row.actualQty ?? 0),
|
||||
brandQtys: row.brandMode
|
||||
? (row.brandQtyRows || [])
|
||||
.filter(r => Number(r.qty) > 0)
|
||||
.map(r => ({
|
||||
brandId: r.brandId,
|
||||
brandName: r.brandName,
|
||||
qty: Number(r.qty)
|
||||
}))
|
||||
: undefined
|
||||
})),
|
||||
freight: {
|
||||
hasFreight: formData.hasFreight,
|
||||
@@ -432,6 +572,33 @@ async function handleSubmit() {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.pl-archive-brand-qty-block {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
background: var(--mint-bg-soft, #f8fafc);
|
||||
}
|
||||
|
||||
.pl-archive-brand-qty-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pl-archive-brand-qty-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.pl-archive-brand-qty-warn {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger, #ef4444);
|
||||
}
|
||||
|
||||
.pl-plan-freight-section {
|
||||
.pl-plan-subtitle {
|
||||
margin-top: 4px;
|
||||
|
||||
Reference in new issue
Block a user