|
@@ -74,11 +74,7 @@
|
|
|
</van-cell-group>
|
|
</van-cell-group>
|
|
|
|
|
|
|
|
<van-cell-group inset class="section-card" title="产品实际数量">
|
|
<van-cell-group inset class="section-card" title="产品实际数量">
|
|
|
- <div
|
|
|
|
|
- v-for="(item, idx) in editableItems"
|
|
|
|
|
- :key="item.id"
|
|
|
|
|
- class="product-card"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div v-for="(item, idx) in editableItems" :key="item.id" class="product-card">
|
|
|
<div class="product-card__head">
|
|
<div class="product-card__head">
|
|
|
<div class="product-card__name">{{ item.productName || '产品' }}</div>
|
|
<div class="product-card__name">{{ item.productName || '产品' }}</div>
|
|
|
<span class="product-card__badge">产品 {{ idx + 1 }}/{{ editableItems.length }}</span>
|
|
<span class="product-card__badge">产品 {{ idx + 1 }}/{{ editableItems.length }}</span>
|
|
@@ -102,7 +98,8 @@
|
|
|
<div class="actual-box">
|
|
<div class="actual-box">
|
|
|
<div class="actual-box__label">
|
|
<div class="actual-box__label">
|
|
|
<strong>实际数量</strong>
|
|
<strong>实际数量</strong>
|
|
|
- <span>请按现场实装填写</span>
|
|
|
|
|
|
|
+ <span v-if="item.brandMode">由品牌明细合计</span>
|
|
|
|
|
+ <span v-else>请按现场实装填写</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="actual-box__row">
|
|
<div class="actual-box__row">
|
|
|
<van-field
|
|
<van-field
|
|
@@ -111,10 +108,22 @@
|
|
|
class="actual-box__field"
|
|
class="actual-box__field"
|
|
|
placeholder="请输入"
|
|
placeholder="请输入"
|
|
|
:border="false"
|
|
:border="false"
|
|
|
- :readonly="isReadOnly"
|
|
|
|
|
|
|
+ :readonly="isReadOnly || Boolean(item.brandMode)"
|
|
|
/>
|
|
/>
|
|
|
<span class="actual-box__unit">{{ item.unit || '吨' }}</span>
|
|
<span class="actual-box__unit">{{ item.unit || '吨' }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <BrandQtyChipsField
|
|
|
|
|
+ v-if="item.brandMode"
|
|
|
|
|
+ v-model="item.brandQtyRows"
|
|
|
|
|
+ :brand-mode="brandModeForField(item)"
|
|
|
|
|
+ :required-brand-id="item.brandId"
|
|
|
|
|
+ :brand-options="item.brandOptions || []"
|
|
|
|
|
+ :disabled="isReadOnly"
|
|
|
|
|
+ :unit="item.unit || '吨'"
|
|
|
|
|
+ variant="mobile"
|
|
|
|
|
+ :actual-qty="Number(item.actualQtyText || 0)"
|
|
|
|
|
+ @update:actual-qty="(val) => syncItemActualQty(item, val)"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</van-cell-group>
|
|
</van-cell-group>
|
|
@@ -175,9 +184,11 @@
|
|
|
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
|
|
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
|
|
|
import {
|
|
import {
|
|
|
createPublicIoOrderApi,
|
|
createPublicIoOrderApi,
|
|
|
|
|
+ type BrandQtyItem,
|
|
|
type PublicIoOrderItem,
|
|
type PublicIoOrderItem,
|
|
|
type PublicIoOrderPage
|
|
type PublicIoOrderPage
|
|
|
} from '@/api/pl/public-io-order'
|
|
} from '@/api/pl/public-io-order'
|
|
|
|
|
+ import BrandQtyChipsField from '@/components/business/brand-qty-chips-field/index.vue'
|
|
|
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
|
|
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
|
|
|
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
|
|
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
|
|
|
|
|
|
|
@@ -187,6 +198,11 @@
|
|
|
|
|
|
|
|
type IoOrderStatus = 'in_progress' | 'completed' | 'voided'
|
|
type IoOrderStatus = 'in_progress' | 'completed' | 'voided'
|
|
|
|
|
|
|
|
|
|
+ type EditableIoOrderItem = PublicIoOrderItem & {
|
|
|
|
|
+ actualQtyText: string
|
|
|
|
|
+ brandQtyRows: BrandQtyItem[]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const props = defineProps<{ kind: 'inbound' | 'outbound' }>()
|
|
const props = defineProps<{ kind: 'inbound' | 'outbound' }>()
|
|
|
const api = createPublicIoOrderApi(props.kind)
|
|
const api = createPublicIoOrderApi(props.kind)
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
@@ -202,7 +218,7 @@
|
|
|
const feedbackRemark = ref('')
|
|
const feedbackRemark = ref('')
|
|
|
const feedbackImages = ref<string[]>([])
|
|
const feedbackImages = ref<string[]>([])
|
|
|
const uploaderFiles = ref<UploaderFileListItem[]>([])
|
|
const uploaderFiles = ref<UploaderFileListItem[]>([])
|
|
|
- const editableItems = ref<(PublicIoOrderItem & { actualQtyText: string })[]>([])
|
|
|
|
|
|
|
+ const editableItems = ref<EditableIoOrderItem[]>([])
|
|
|
const selectedStatus = ref<IoOrderStatus>('in_progress')
|
|
const selectedStatus = ref<IoOrderStatus>('in_progress')
|
|
|
const isReadOnly = computed(() => Boolean(page.value?.voided))
|
|
const isReadOnly = computed(() => Boolean(page.value?.voided))
|
|
|
|
|
|
|
@@ -271,6 +287,54 @@
|
|
|
})) as UploaderFileListItem[]
|
|
})) 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() {
|
|
async function loadPage() {
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
errorMessage.value = ''
|
|
errorMessage.value = ''
|
|
@@ -279,10 +343,22 @@
|
|
|
page.value = data
|
|
page.value = data
|
|
|
feedbackRemark.value = data.siteFeedbackRemark || ''
|
|
feedbackRemark.value = data.siteFeedbackRemark || ''
|
|
|
feedbackImages.value = [...(data.siteFeedbackImages || [])]
|
|
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 = (
|
|
selectedStatus.value = (
|
|
|
['in_progress', 'completed', 'voided'].includes(data.status || '')
|
|
['in_progress', 'completed', 'voided'].includes(data.status || '')
|
|
|
? data.status
|
|
? data.status
|
|
@@ -324,7 +400,7 @@
|
|
|
if (isReadOnly.value) return false
|
|
if (isReadOnly.value) return false
|
|
|
const fileUrl = (item as UploaderFileListItem & { fileUrl?: string }).fileUrl
|
|
const fileUrl = (item as UploaderFileListItem & { fileUrl?: string }).fileUrl
|
|
|
if (fileUrl) {
|
|
if (fileUrl) {
|
|
|
- feedbackImages.value = feedbackImages.value.filter(u => u !== fileUrl)
|
|
|
|
|
|
|
+ feedbackImages.value = feedbackImages.value.filter((u) => u !== fileUrl)
|
|
|
}
|
|
}
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
@@ -341,15 +417,32 @@
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (selectedStatus.value === 'completed') {
|
|
|
|
|
+ const brandErr = validateBrandedItemsForCompleted()
|
|
|
|
|
+ if (brandErr) {
|
|
|
|
|
+ showFailToast(brandErr)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
saving.value = true
|
|
saving.value = true
|
|
|
try {
|
|
try {
|
|
|
await api.saveFeedback(token.value, {
|
|
await api.saveFeedback(token.value, {
|
|
|
status: selectedStatus.value,
|
|
status: selectedStatus.value,
|
|
|
remark: feedbackRemark.value,
|
|
remark: feedbackRemark.value,
|
|
|
images: feedbackImages.value,
|
|
images: feedbackImages.value,
|
|
|
- items: editableItems.value.map(it => ({
|
|
|
|
|
|
|
+ items: editableItems.value.map((it) => ({
|
|
|
id: it.id,
|
|
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('提交成功')
|
|
showSuccessToast('提交成功')
|