From 6dde2cec07ff9226594473c2441acf92867c33e8 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 31 Jul 2026 05:23:37 +0800 Subject: [PATCH] feat(web-admin): site public page brand qty chips entry Co-authored-by: Cursor --- web-admin/src/api/pl/public-io-order.ts | 13 +- .../business/brand-qty-chips-field/index.vue | 345 ++++++++++++++++++ .../public/pl/_shared/PublicIoOrderPage.vue | 123 ++++++- 3 files changed, 465 insertions(+), 16 deletions(-) create mode 100644 web-admin/src/components/business/brand-qty-chips-field/index.vue diff --git a/web-admin/src/api/pl/public-io-order.ts b/web-admin/src/api/pl/public-io-order.ts index 0c9b53158..db479039a 100644 --- a/web-admin/src/api/pl/public-io-order.ts +++ b/web-admin/src/api/pl/public-io-order.ts @@ -1,5 +1,11 @@ import { publicPlGet, publicPlPost, publicPlUpload } from '@/utils/http/public-pl' +export interface BrandQtyItem { + brandId: string + brandName?: string + qty: number +} + export interface PublicIoOrderItem { id: string productNo?: string @@ -10,6 +16,11 @@ export interface PublicIoOrderItem { actualQty?: number packageSpec?: string remark?: string + brandMode?: string | null + brandId?: string | null + brandName?: string | null + brandOptions?: { brandId: string; brandName: string }[] + brandQtys?: BrandQtyItem[] } export interface PublicIoOrderPage { @@ -50,7 +61,7 @@ export function createPublicIoOrderApi(kind: 'inbound' | 'outbound') { status: 'in_progress' | 'completed' | 'voided' remark?: string images?: string[] - items: { id: string; actualQty: number }[] + items: { id: string; actualQty: number; brandQtys?: BrandQtyItem[] }[] } ) => publicPlPost<{ success: boolean }>(`${base}/${token}/feedback`, data), uploadAttachment: (token: string, file: File) => diff --git a/web-admin/src/components/business/brand-qty-chips-field/index.vue b/web-admin/src/components/business/brand-qty-chips-field/index.vue new file mode 100644 index 000000000..e2b34714d --- /dev/null +++ b/web-admin/src/components/business/brand-qty-chips-field/index.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue b/web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue index 5d7d2d950..15cb9b87d 100644 --- a/web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue +++ b/web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue @@ -74,11 +74,7 @@ -
+
{{ item.productName || '产品' }}
产品 {{ idx + 1 }}/{{ editableItems.length }} @@ -102,7 +98,8 @@
实际数量 - 请按现场实装填写 + 由品牌明细合计 + 请按现场实装填写
{{ item.unit || '吨' }}
+
@@ -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('提交成功')