{{ item.productName || '产品' }}
产品 {{ idx + 1 }}/{{ editableItems.length }}
@@ -102,7 +98,8 @@
实际数量
- 请按现场实装填写
+ 由品牌明细合计
+ 请按现场实装填写
{{ item.unit || '吨' }}
+
syncItemActualQty(item, val)"
+ />
@@ -175,9 +184,11 @@
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
import {
createPublicIoOrderApi,
+ type BrandQtyItem,
type PublicIoOrderItem,
type PublicIoOrderPage
} from '@/api/pl/public-io-order'
+ import BrandQtyChipsField from '@/components/business/brand-qty-chips-field/index.vue'
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
@@ -187,6 +198,11 @@
type IoOrderStatus = 'in_progress' | 'completed' | 'voided'
+ type EditableIoOrderItem = PublicIoOrderItem & {
+ actualQtyText: string
+ brandQtyRows: BrandQtyItem[]
+ }
+
const props = defineProps<{ kind: 'inbound' | 'outbound' }>()
const api = createPublicIoOrderApi(props.kind)
const route = useRoute()
@@ -202,7 +218,7 @@
const feedbackRemark = ref('')
const feedbackImages = ref
([])
const uploaderFiles = ref([])
- const editableItems = ref<(PublicIoOrderItem & { actualQtyText: string })[]>([])
+ const editableItems = ref([])
const selectedStatus = ref('in_progress')
const isReadOnly = computed(() => Boolean(page.value?.voided))
@@ -271,6 +287,54 @@
})) as UploaderFileListItem[]
}
+ function mapBrandQtyRows(it: PublicIoOrderItem): BrandQtyItem[] {
+ const existing = (it.brandQtys || []).map((row) => ({
+ brandId: row.brandId,
+ brandName: row.brandName || '',
+ qty: Number(row.qty ?? 0)
+ }))
+ if (existing.length) return existing
+
+ if (it.brandMode === 'required') {
+ const brandId = it.brandId || it.brandOptions?.[0]?.brandId || ''
+ const brandName =
+ it.brandName || it.brandOptions?.find((o) => o.brandId === brandId)?.brandName || '指定品牌'
+ const seedQty = it.actualQty != null ? Number(it.actualQty) : 0
+ return brandId ? [{ brandId, brandName, qty: seedQty }] : []
+ }
+
+ return []
+ }
+
+ function brandModeForField(item: EditableIoOrderItem): 'any' | 'required' | null {
+ if (item.brandMode === 'any' || item.brandMode === 'required') return item.brandMode
+ return null
+ }
+
+ function syncItemActualQty(item: EditableIoOrderItem, val: number) {
+ item.actualQtyText = String(val)
+ }
+
+ function brandQtySum(rows: BrandQtyItem[]) {
+ return rows.reduce((sum, row) => sum + Number(row.qty || 0), 0)
+ }
+
+ function validateBrandedItemsForCompleted(): string | null {
+ for (const item of editableItems.value) {
+ if (!item.brandMode) continue
+ const positiveRows = (item.brandQtyRows || []).filter((r) => Number(r.qty) > 0)
+ if (!positiveRows.length) {
+ return `「${item.productName || '产品'}」请至少填写一个品牌数量`
+ }
+ const total = Number(brandQtySum(item.brandQtyRows).toFixed(3))
+ const actual = Number(item.actualQtyText || 0)
+ if (Math.abs(total - actual) >= 0.0005) {
+ return `「${item.productName || '产品'}」品牌数量合计须等于实际数量`
+ }
+ }
+ return null
+ }
+
async function loadPage() {
loading.value = true
errorMessage.value = ''
@@ -279,10 +343,22 @@
page.value = data
feedbackRemark.value = data.siteFeedbackRemark || ''
feedbackImages.value = [...(data.siteFeedbackImages || [])]
- editableItems.value = (data.items || []).map(it => ({
- ...it,
- actualQtyText: it.actualQty != null ? String(it.actualQty) : ''
- }))
+ editableItems.value = (data.items || []).map((it) => {
+ const brandQtyRows = mapBrandQtyRows(it)
+ const actualFromBrand =
+ it.brandMode && brandQtyRows.length ? Number(brandQtySum(brandQtyRows).toFixed(3)) : null
+ return {
+ ...it,
+ brandOptions: it.brandOptions || [],
+ brandQtyRows,
+ actualQtyText:
+ actualFromBrand != null
+ ? String(actualFromBrand)
+ : it.actualQty != null
+ ? String(it.actualQty)
+ : ''
+ }
+ })
selectedStatus.value = (
['in_progress', 'completed', 'voided'].includes(data.status || '')
? data.status
@@ -324,7 +400,7 @@
if (isReadOnly.value) return false
const fileUrl = (item as UploaderFileListItem & { fileUrl?: string }).fileUrl
if (fileUrl) {
- feedbackImages.value = feedbackImages.value.filter(u => u !== fileUrl)
+ feedbackImages.value = feedbackImages.value.filter((u) => u !== fileUrl)
}
return true
}
@@ -341,15 +417,32 @@
return
}
}
+ if (selectedStatus.value === 'completed') {
+ const brandErr = validateBrandedItemsForCompleted()
+ if (brandErr) {
+ showFailToast(brandErr)
+ return
+ }
+ }
+
saving.value = true
try {
await api.saveFeedback(token.value, {
status: selectedStatus.value,
remark: feedbackRemark.value,
images: feedbackImages.value,
- items: editableItems.value.map(it => ({
+ items: editableItems.value.map((it) => ({
id: it.id,
- actualQty: Number(it.actualQtyText || 0)
+ actualQty: Number(it.actualQtyText || 0),
+ brandQtys: it.brandMode
+ ? (it.brandQtyRows || [])
+ .filter((row) => Number(row.qty) > 0)
+ .map((row) => ({
+ brandId: row.brandId,
+ brandName: row.brandName,
+ qty: Number(row.qty)
+ }))
+ : undefined
}))
})
showSuccessToast('提交成功')