提交
This commit is contained in:
1 parent
4611dbc2b2
commit
ebf006eeff
46 files changed
+36802
-20974
No files matched your search
@@ -159,7 +159,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -220,7 +220,6 @@ const contractList = ref<ContractVO[]>([])
|
||||
const currentRow = ref<ContractVO | null>(null)
|
||||
const selectedId = ref('')
|
||||
const planCountMap = ref<Record<string, number>>({})
|
||||
const contractDetailDialogActive = ref(false)
|
||||
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
|
||||
|
||||
const queryParams = reactive<ContractQuery>({
|
||||
@@ -265,15 +264,32 @@ function formatContractProductLine(product: ContractProductForm) {
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForContractDetailDialog() {
|
||||
if (contractDetailRef.value) return contractDetailRef.value
|
||||
await nextTick()
|
||||
if (contractDetailRef.value) return contractDetailRef.value
|
||||
return new Promise<NonNullable<typeof contractDetailRef.value>>((resolve) => {
|
||||
const stop = watch(
|
||||
contractDetailRef,
|
||||
(val) => {
|
||||
if (val) {
|
||||
stop()
|
||||
resolve(val)
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleViewContract(row: ContractVO) {
|
||||
const contractId = row?.id
|
||||
if (!contractId) {
|
||||
ElMessage.warning('合同信息无效')
|
||||
return
|
||||
}
|
||||
contractDetailDialogActive.value = true
|
||||
await nextTick()
|
||||
contractDetailRef.value?.open(contractId)
|
||||
const dialog = await waitForContractDetailDialog()
|
||||
await dialog.open(contractId)
|
||||
}
|
||||
|
||||
function isContractSelectable(row: ContractVO): boolean {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -147,11 +147,7 @@
|
||||
@confirm-multiple="handleConfirmMultiple"
|
||||
/>
|
||||
|
||||
<ContractDetailDialog
|
||||
v-if="contractDetailDialogActive"
|
||||
ref="contractDetailRef"
|
||||
hide-product-price
|
||||
/>
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -202,7 +198,6 @@ const emit = defineEmits<{
|
||||
const selectorVisible = ref(false)
|
||||
const detailRows = ref<ContractVO[]>([])
|
||||
const loadingDetails = ref(false)
|
||||
const contractDetailDialogActive = ref(false)
|
||||
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
|
||||
const expandedContractIds = ref<Set<string>>(new Set())
|
||||
|
||||
@@ -344,6 +339,24 @@ function removeContract(id: string) {
|
||||
emitLinksFromDetails(next)
|
||||
}
|
||||
|
||||
async function waitForContractDetailDialog() {
|
||||
if (contractDetailRef.value) return contractDetailRef.value
|
||||
await nextTick()
|
||||
if (contractDetailRef.value) return contractDetailRef.value
|
||||
return new Promise<NonNullable<typeof contractDetailRef.value>>((resolve) => {
|
||||
const stop = watch(
|
||||
contractDetailRef,
|
||||
(val) => {
|
||||
if (val) {
|
||||
stop()
|
||||
resolve(val)
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleViewContract(contractId: string) {
|
||||
const id = String(contractId || '').trim()
|
||||
if (!id) {
|
||||
@@ -351,9 +364,8 @@ async function handleViewContract(contractId: string) {
|
||||
return
|
||||
}
|
||||
emit('view', id)
|
||||
contractDetailDialogActive.value = true
|
||||
await nextTick()
|
||||
contractDetailRef.value?.open(id)
|
||||
const dialog = await waitForContractDetailDialog()
|
||||
await dialog.open(id)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
+192
-164
@@ -1,8 +1,20 @@
|
||||
<template>
|
||||
<div class="panel dialog-list-panel pl-plan-product-table-wrap">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<ListTable table-key="business-pl-plan-form-dialog-pl-plan-form-product-table" :data="products" stripe class="list-table pl-plan-product-table" empty-text="暂无产品">
|
||||
<el-table-column label="序号" type="index" width="60" align="center" header-align="center" />
|
||||
<ListTable
|
||||
table-key="business-pl-plan-form-dialog-pl-plan-form-product-table"
|
||||
:data="products"
|
||||
stripe
|
||||
class="list-table pl-plan-product-table"
|
||||
empty-text="暂无产品"
|
||||
>
|
||||
<el-table-column
|
||||
label="序号"
|
||||
type="index"
|
||||
width="60"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="产品名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<PlProductNameHazardCell
|
||||
@@ -10,7 +22,13 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="别名" min-width="140" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="!archiveCompact"
|
||||
label="别名"
|
||||
min-width="140"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="editableAliasBrand" class="line-meta-cell">
|
||||
<el-select
|
||||
@@ -44,7 +62,13 @@
|
||||
<span v-else>{{ row.aliasName || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="品牌" min-width="140" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="!archiveCompact"
|
||||
label="品牌"
|
||||
min-width="140"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="editableAliasBrand" class="line-meta-cell">
|
||||
<el-select
|
||||
@@ -78,7 +102,12 @@
|
||||
<span v-else>{{ formatBrandConstraintLabel(row.brandMode, row.brandName) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="型号/规格" min-width="120" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="!archiveCompact"
|
||||
label="型号/规格"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.model || row.productModel || '—' }}
|
||||
</template>
|
||||
@@ -217,17 +246,18 @@
|
||||
{{ formatPlanProductIoAmount(row.outboundQty, row.outboundPrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showBrandQty"
|
||||
label="品牌数量"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<el-table-column v-if="showBrandQty" label="品牌数量" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ formatBrandQtySummary(row.brandQtyRows) || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="packageSpec" label="包装规格" min-width="150" align="center">
|
||||
<el-table-column
|
||||
v-if="!archiveCompact"
|
||||
prop="packageSpec"
|
||||
label="包装规格"
|
||||
min-width="150"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<DictSelect
|
||||
v-if="editablePackageDelivery"
|
||||
@@ -243,7 +273,7 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="送货方式" min-width="150" align="center">
|
||||
<el-table-column v-if="!archiveCompact" label="送货方式" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<DictSelect
|
||||
v-if="editablePackageDelivery"
|
||||
@@ -263,7 +293,11 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editableRemark" label="备注" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" class="pl-plan-product-cell-control" placeholder="备注" />
|
||||
<el-input
|
||||
v-model="row.remark"
|
||||
class="pl-plan-product-cell-control"
|
||||
placeholder="备注"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
@@ -308,172 +342,166 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
|
||||
import {
|
||||
applyBrandSelectChange,
|
||||
applyManagedProductMetaToLines,
|
||||
formatBrandConstraintLabel,
|
||||
resolveLineProductName
|
||||
} from '@/utils/crm-product-brand'
|
||||
import { formatBrandQtySummary } from '@/utils/pl-display'
|
||||
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
|
||||
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import DictSelect from '@/components/DictSelect/index.vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
|
||||
import {
|
||||
applyBrandSelectChange,
|
||||
applyManagedProductMetaToLines,
|
||||
formatBrandConstraintLabel,
|
||||
resolveLineProductName
|
||||
} from '@/utils/crm-product-brand'
|
||||
import { formatBrandQtySummary } from '@/utils/pl-display'
|
||||
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
|
||||
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import DictSelect from '@/components/DictSelect/index.vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
|
||||
const props = defineProps<{
|
||||
products: PlanFormProductRow[]
|
||||
showDelete?: boolean
|
||||
showSelect?: boolean
|
||||
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
|
||||
selectedProductIds?: string[]
|
||||
editableRemark?: boolean
|
||||
editableQty?: boolean
|
||||
/** 产品信息区:别名 / 品牌可改 */
|
||||
editableAliasBrand?: boolean
|
||||
/** 单位 / 包装规格 / 送货方式可下拉编辑(对齐报价单) */
|
||||
editablePackageDelivery?: boolean
|
||||
/** 新增/编辑弹窗默认不展示实际数量 */
|
||||
showActualQty?: boolean
|
||||
/** 展示归档品牌数量摘要(如:君正 12 / 红四方 13) */
|
||||
showBrandQty?: boolean
|
||||
/** 仅实际数量可编辑,计划数量只读(归档弹窗) */
|
||||
editableActualQtyOnly?: boolean
|
||||
/** 展示入库/出库数量列(不含价格) */
|
||||
showInboundOutboundQty?: boolean
|
||||
/** 展示入库/出库数量、单价与计算金额列 */
|
||||
showArchiveIoFields?: boolean
|
||||
/** 入库/出库数量与价格可编辑(归档弹窗) */
|
||||
editableArchiveIoFields?: boolean
|
||||
/** 归档:仅展示/编辑入库列(装货段) */
|
||||
archiveIoInboundOnly?: boolean
|
||||
/** 归档:仅展示/编辑出库列(送货段) */
|
||||
archiveIoOutboundOnly?: boolean
|
||||
/** 段次归档:入库/出库列统一展示为实际数量、单价、金额 */
|
||||
archiveSegmentLabels?: boolean
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
products: PlanFormProductRow[]
|
||||
showDelete?: boolean
|
||||
showSelect?: boolean
|
||||
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
|
||||
selectedProductIds?: string[]
|
||||
editableRemark?: boolean
|
||||
editableQty?: boolean
|
||||
/** 产品信息区:别名 / 品牌可改 */
|
||||
editableAliasBrand?: boolean
|
||||
/** 单位 / 包装规格 / 送货方式可下拉编辑(对齐报价单) */
|
||||
editablePackageDelivery?: boolean
|
||||
/** 新增/编辑弹窗默认不展示实际数量 */
|
||||
showActualQty?: boolean
|
||||
/** 展示归档品牌数量摘要(如:君正 12 / 红四方 13) */
|
||||
showBrandQty?: boolean
|
||||
/** 仅实际数量可编辑,计划数量只读(归档弹窗) */
|
||||
editableActualQtyOnly?: boolean
|
||||
/** 展示入库/出库数量列(不含价格) */
|
||||
showInboundOutboundQty?: boolean
|
||||
/** 展示入库/出库数量、单价与计算金额列 */
|
||||
showArchiveIoFields?: boolean
|
||||
/** 入库/出库数量与价格可编辑(归档弹窗) */
|
||||
editableArchiveIoFields?: boolean
|
||||
/** 归档:仅展示/编辑入库列(装货段) */
|
||||
archiveIoInboundOnly?: boolean
|
||||
/** 归档:仅展示/编辑出库列(送货段) */
|
||||
archiveIoOutboundOnly?: boolean
|
||||
/** 段次归档:入库/出库列统一展示为实际数量、单价、金额 */
|
||||
archiveSegmentLabels?: boolean
|
||||
/** 归档段表单:仅保留产品名称、单位、计划数量与归档列 */
|
||||
archiveCompact?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [index: number]
|
||||
select: [row: PlanFormProductRow, index: number]
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
remove: [index: number]
|
||||
select: [row: PlanFormProductRow, index: number]
|
||||
}>()
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const selectedProductIdSet = computed(
|
||||
() => new Set((props.selectedProductIds || []).map(id => String(id)))
|
||||
)
|
||||
|
||||
|
||||
function getDeliveryMethodDisplay(method?: string) {
|
||||
const label = dictStore.getLabel('crm_shfs', method || '')
|
||||
if (label && label !== method) return label
|
||||
return getDeliveryMethodLabel(method)
|
||||
}
|
||||
|
||||
function isProductSelected(row: PlanFormProductRow) {
|
||||
return selectedProductIdSet.value.has(String(row.productId))
|
||||
}
|
||||
|
||||
/** 入库/出库金额:数量 × 单价,保留两位小数 */
|
||||
function formatPlanProductIoAmount(
|
||||
qty?: number | string | null,
|
||||
price?: number | string | null
|
||||
) {
|
||||
if (qty == null || price == null || qty === '' || price === '') return '—'
|
||||
const amount = Number(qty) * Number(price)
|
||||
if (!Number.isFinite(amount)) return '—'
|
||||
return amount.toFixed(2)
|
||||
}
|
||||
|
||||
function handleAliasChange(row: PlanFormProductRow) {
|
||||
row.productName = resolveLineProductName(row.standardProductName, row.aliasName)
|
||||
}
|
||||
|
||||
function handleBrandChange(row: PlanFormProductRow, val: string | undefined | null) {
|
||||
applyBrandSelectChange(row, val, row.brandOptions || [])
|
||||
}
|
||||
|
||||
const aliasBrandManageVisible = ref(false)
|
||||
const aliasBrandManageProductId = ref('')
|
||||
const aliasBrandManageProductName = ref('')
|
||||
const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
|
||||
|
||||
function openAliasBrandManage(row: PlanFormProductRow, mode: 'alias' | 'brand') {
|
||||
if (!row.productId) return
|
||||
aliasBrandManageProductId.value = String(row.productId)
|
||||
aliasBrandManageProductName.value = String(row.standardProductName || row.productName || '')
|
||||
aliasBrandManageMode.value = mode
|
||||
aliasBrandManageVisible.value = true
|
||||
}
|
||||
|
||||
function handleAliasBrandManaged(product: Record<string, any>) {
|
||||
applyManagedProductMetaToLines(
|
||||
props.products || [],
|
||||
aliasBrandManageProductId.value,
|
||||
product
|
||||
const selectedProductIdSet = computed(
|
||||
() => new Set((props.selectedProductIds || []).map((id) => String(id)))
|
||||
)
|
||||
const pid = aliasBrandManageProductId.value
|
||||
for (const line of props.products || []) {
|
||||
if (String(line.productId || '') !== pid) continue
|
||||
line.productName = resolveLineProductName(line.standardProductName, line.aliasName)
|
||||
|
||||
function getDeliveryMethodDisplay(method?: string) {
|
||||
const label = dictStore.getLabel('crm_shfs', method || '')
|
||||
if (label && label !== method) return label
|
||||
return getDeliveryMethodLabel(method)
|
||||
}
|
||||
|
||||
function isProductSelected(row: PlanFormProductRow) {
|
||||
return selectedProductIdSet.value.has(String(row.productId))
|
||||
}
|
||||
|
||||
/** 入库/出库金额:数量 × 单价,保留两位小数 */
|
||||
function formatPlanProductIoAmount(qty?: number | string | null, price?: number | string | null) {
|
||||
if (qty == null || price == null || qty === '' || price === '') return '—'
|
||||
const amount = Number(qty) * Number(price)
|
||||
if (!Number.isFinite(amount)) return '—'
|
||||
return amount.toFixed(2)
|
||||
}
|
||||
|
||||
function handleAliasChange(row: PlanFormProductRow) {
|
||||
row.productName = resolveLineProductName(row.standardProductName, row.aliasName)
|
||||
}
|
||||
|
||||
function handleBrandChange(row: PlanFormProductRow, val: string | undefined | null) {
|
||||
applyBrandSelectChange(row, val, row.brandOptions || [])
|
||||
}
|
||||
|
||||
const aliasBrandManageVisible = ref(false)
|
||||
const aliasBrandManageProductId = ref('')
|
||||
const aliasBrandManageProductName = ref('')
|
||||
const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
|
||||
|
||||
function openAliasBrandManage(row: PlanFormProductRow, mode: 'alias' | 'brand') {
|
||||
if (!row.productId) return
|
||||
aliasBrandManageProductId.value = String(row.productId)
|
||||
aliasBrandManageProductName.value = String(row.standardProductName || row.productName || '')
|
||||
aliasBrandManageMode.value = mode
|
||||
aliasBrandManageVisible.value = true
|
||||
}
|
||||
|
||||
function handleAliasBrandManaged(product: Record<string, any>) {
|
||||
applyManagedProductMetaToLines(props.products || [], aliasBrandManageProductId.value, product)
|
||||
const pid = aliasBrandManageProductId.value
|
||||
for (const line of props.products || []) {
|
||||
if (String(line.productId || '') !== pid) continue
|
||||
line.productName = resolveLineProductName(line.standardProductName, line.aliasName)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-plan-product-table-wrap {
|
||||
margin-top: 8px;
|
||||
.pl-plan-product-table-wrap {
|
||||
margin-top: 8px;
|
||||
|
||||
/* 别名 / 品牌下拉与备注输入框同高、同字号 */
|
||||
:deep(.pl-plan-product-cell-control) {
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper {
|
||||
height: var(--el-component-custom-height, 36px);
|
||||
min-height: var(--el-component-custom-height, 36px);
|
||||
font-size: 14px;
|
||||
/* 别名 / 品牌下拉与备注输入框同高、同字号 */
|
||||
:deep(.pl-plan-product-cell-control) {
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper {
|
||||
height: var(--el-component-custom-height, 36px);
|
||||
min-height: var(--el-component-custom-height, 36px);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-input__inner,
|
||||
.el-select__selected-item,
|
||||
.el-select__placeholder,
|
||||
.el-select__input {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner,
|
||||
.el-select__selected-item,
|
||||
.el-select__placeholder,
|
||||
.el-select__input {
|
||||
font-size: 14px;
|
||||
.line-meta-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__control {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__empty {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__btn {
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-meta-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__control {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__empty {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-meta-cell__btn {
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* teleported 下拉挂到 body,需非 scoped;选项字号与输入框一致 */
|
||||
.pl-plan-alias-brand-select-dropdown {
|
||||
.el-select-dropdown__item {
|
||||
font-size: 14px;
|
||||
/* teleported 下拉挂到 body,需非 scoped;选项字号与输入框一致 */
|
||||
.pl-plan-alias-brand-select-dropdown {
|
||||
.el-select-dropdown__item {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,9 @@
|
||||
field === 'planQty' ||
|
||||
field === 'actualQty' ||
|
||||
field === 'inboundQty' ||
|
||||
field === 'outboundQty',
|
||||
field === 'outboundQty' ||
|
||||
field === 'unitPrice' ||
|
||||
field === 'amount',
|
||||
'pl-plan-list-product-cell__line--center':
|
||||
field === 'spec' || field === 'model' || field === 'packageSpec' || field === 'unit'
|
||||
}"
|
||||
@@ -64,7 +66,9 @@
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="field === 'name'">
|
||||
<span class="pl-plan-list-product-cell__text">{{ product.productName || '—' }}</span>
|
||||
<span class="pl-plan-list-product-cell__text">{{
|
||||
resolveListProductName(product)
|
||||
}}</span>
|
||||
<el-tag
|
||||
v-if="getHazardFlags(product).isToxic"
|
||||
type="warning"
|
||||
@@ -109,6 +113,13 @@
|
||||
getPlanProductQuantity,
|
||||
getPlanProductSpec
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
getStopArchiveActualQty,
|
||||
getStopArchiveAmount,
|
||||
getStopArchiveUnitPrice,
|
||||
resolveArchiveListIoColumn,
|
||||
type PlPlanListDisplayRowKind
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export type PlPlanListProductCellField =
|
||||
@@ -121,6 +132,8 @@
|
||||
| 'actualQty'
|
||||
| 'inboundQty'
|
||||
| 'outboundQty'
|
||||
| 'unitPrice'
|
||||
| 'amount'
|
||||
| 'unit'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -134,12 +147,18 @@
|
||||
field?: PlPlanListProductCellField
|
||||
/** 危化品行下方展示证书有无(按产品类型;仅整行模式) */
|
||||
showCertStatus?: boolean
|
||||
/** 运输计划归档列表:段次数量/入出库/单价/金额展示规则 */
|
||||
archiveListMode?: boolean
|
||||
stopKind?: PlPlanListDisplayRowKind
|
||||
stopCustomerName?: string
|
||||
}>(),
|
||||
{
|
||||
products: () => [],
|
||||
stopProducts: () => [],
|
||||
planProducts: () => [],
|
||||
showCertStatus: false
|
||||
showCertStatus: false,
|
||||
archiveListMode: false,
|
||||
stopCustomerName: ''
|
||||
}
|
||||
)
|
||||
|
||||
@@ -168,6 +187,19 @@
|
||||
)
|
||||
}
|
||||
|
||||
/** 列表「产品名称」列:只展示标准产品名,不展示别名 */
|
||||
function resolveListProductName(product: PlanProduct | PlanStopProduct) {
|
||||
if (useStopProducts.value) {
|
||||
const stopProduct = product as PlanStopProduct
|
||||
const linked = resolveLinkedPlanProduct(stopProduct)
|
||||
const linkedName = (linked?.productName || '').trim()
|
||||
if (linkedName) return linkedName
|
||||
return stopProduct.productName || '—'
|
||||
}
|
||||
const planProduct = product as PlanProduct
|
||||
return planProduct.productName || '—'
|
||||
}
|
||||
|
||||
function formatStopProductLine(stopProduct: PlanStopProduct) {
|
||||
const linked = resolveLinkedPlanProduct(stopProduct)
|
||||
if (linked) return formatPlanProductListText(linked, getUnitLabel)
|
||||
@@ -180,14 +212,25 @@
|
||||
return [stopProduct.productName || '—', model, qty].filter(Boolean).join('')
|
||||
}
|
||||
|
||||
function formatStopQty(value: unknown, unitRaw?: string) {
|
||||
function formatStopQty(value: unknown, unitRaw?: string, omitUnit = false) {
|
||||
if (value == null || value === '') return '—'
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return '—'
|
||||
if (omitUnit) return String(num)
|
||||
const unit = getUnitLabel(unitRaw || '') || unitRaw || ''
|
||||
return unit ? `${num} ${unit}` : String(num)
|
||||
}
|
||||
|
||||
function formatArchiveQty(value: number | null) {
|
||||
if (value == null) return '—'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatMoneyValue(value: number | null) {
|
||||
if (value == null) return '—'
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
function formatProductLine(product: PlanProduct) {
|
||||
return formatPlanProductListText(product, getUnitLabel)
|
||||
}
|
||||
@@ -196,6 +239,29 @@
|
||||
if (useStopProducts.value) {
|
||||
const stopProduct = product as PlanStopProduct
|
||||
const linked = resolveLinkedPlanProduct(stopProduct)
|
||||
const stopKind = props.stopKind || 'load'
|
||||
|
||||
if (props.archiveListMode) {
|
||||
const ioColumn = resolveArchiveListIoColumn(stopKind, props.stopCustomerName)
|
||||
const actualQty = getStopArchiveActualQty(stopProduct, stopKind)
|
||||
|
||||
if (props.field === 'actualQty') {
|
||||
return formatArchiveQty(actualQty)
|
||||
}
|
||||
if (props.field === 'inboundQty') {
|
||||
return ioColumn === 'inbound' ? formatArchiveQty(actualQty) : '—'
|
||||
}
|
||||
if (props.field === 'outboundQty') {
|
||||
return ioColumn === 'outbound' ? formatArchiveQty(actualQty) : '—'
|
||||
}
|
||||
if (props.field === 'unitPrice') {
|
||||
return formatMoneyValue(getStopArchiveUnitPrice(stopProduct, stopKind))
|
||||
}
|
||||
if (props.field === 'amount') {
|
||||
return formatMoneyValue(getStopArchiveAmount(stopProduct, stopKind))
|
||||
}
|
||||
}
|
||||
|
||||
if (props.field === 'spec' || props.field === 'model') {
|
||||
return (
|
||||
stopProduct.model || linked?.model || (linked ? getPlanProductSpec(linked) : '—') || '—'
|
||||
@@ -214,6 +280,10 @@
|
||||
return formatStopQty(stopProduct.outboundQty, stopProduct.unit)
|
||||
}
|
||||
if (props.field === 'actualQty') {
|
||||
const segmentKind = props.stopKind || (props.archiveListMode ? 'load' : undefined)
|
||||
if (props.archiveListMode || segmentKind) {
|
||||
return formatArchiveQty(getStopArchiveActualQty(stopProduct, segmentKind || 'load'))
|
||||
}
|
||||
return linked ? getPlanProductActualQuantity(linked) : '—'
|
||||
}
|
||||
if (props.field === 'unit') {
|
||||
@@ -249,6 +319,9 @@
|
||||
if (props.field === 'outboundQty') {
|
||||
return getPlanProductOutboundQuantity(planProduct)
|
||||
}
|
||||
if (props.field === 'unitPrice' || props.field === 'amount') {
|
||||
return '—'
|
||||
}
|
||||
if (props.field === 'unit') {
|
||||
const unitRaw = planProduct.unit || ''
|
||||
if (!unitRaw) return '—'
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
|
||||
<section class="pl-plan-form-section pl-plan-form-section--plain">
|
||||
<h3 class="pl-plan-form-section-title">运输信息</h3>
|
||||
<div
|
||||
v-if="showArrangeLinks"
|
||||
class="pl-plan-arrange-links pl-plan-transport-block"
|
||||
>
|
||||
<div v-if="showArrangeLinks" class="pl-plan-arrange-links pl-plan-transport-block">
|
||||
<div class="pl-arrange-links-body">
|
||||
<div class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">司机链接</span>
|
||||
@@ -123,6 +120,10 @@
|
||||
stop-archive-mode="load"
|
||||
/>
|
||||
<p v-else class="pl-plan-form-empty-hint">暂无装货产品</p>
|
||||
<PlPlanArchiveBrandViewBlock
|
||||
v-if="showArchiveIoFields"
|
||||
:items="loadArchiveBrandItems"
|
||||
/>
|
||||
<PlPlanStopFeedbackPanel
|
||||
v-if="showStopFeedback && !feedbackMirrorTo"
|
||||
stop-label="装货"
|
||||
@@ -136,7 +137,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #delivery>
|
||||
<div v-if="deliveryViews.length > 1" class="pl-plan-delivery-tabs pl-plan-view-delivery-tabs">
|
||||
<div
|
||||
v-if="deliveryViews.length > 1"
|
||||
class="pl-plan-delivery-tabs pl-plan-view-delivery-tabs"
|
||||
>
|
||||
<button
|
||||
v-for="(delivery, index) in deliveryViews"
|
||||
:key="delivery.id || 'delivery-tab-' + index"
|
||||
@@ -199,6 +203,10 @@
|
||||
stop-archive-mode="delivery"
|
||||
/>
|
||||
<p v-else class="pl-plan-form-empty-hint">暂无送货产品</p>
|
||||
<PlPlanArchiveBrandViewBlock
|
||||
v-if="showArchiveIoFields"
|
||||
:items="deliveryArchiveBrandItems(delivery, index)"
|
||||
/>
|
||||
<PlPlanStopFeedbackPanel
|
||||
v-if="showStopFeedback && !feedbackMirrorTo"
|
||||
:stop-label="
|
||||
@@ -360,7 +368,12 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="现场备注">
|
||||
<el-input v-model="ioOrderEditForm.siteFeedbackRemark" type="textarea" :rows="3" maxlength="500" />
|
||||
<el-input
|
||||
v-model="ioOrderEditForm.siteFeedbackRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="现场附件">
|
||||
<AttachmentUploadField v-model="ioOrderEditImagesCsv" />
|
||||
@@ -392,6 +405,7 @@
|
||||
} from '@/components/business/brand-qty-chips-field/index.vue'
|
||||
import PlPlanDriverFeedbackDialog from '@/components/business/pl-plan-driver-feedback-dialog/index.vue'
|
||||
import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue'
|
||||
import PlPlanArchiveBrandViewBlock from '@/components/business/pl-plan-archive-brand-view-block/index.vue'
|
||||
import PlPlanStopFeedbackPanel from '@/components/business/pl-plan-stop-feedback-panel/index.vue'
|
||||
import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue'
|
||||
import PlPlanStopsSplitShell from '@/components/business/pl-plan-stops-split-shell/index.vue'
|
||||
@@ -433,6 +447,7 @@
|
||||
type PlanLoadForm,
|
||||
type PlanStopProductFormRow
|
||||
} from '@/utils/pl-plan-stop-form'
|
||||
import { buildArchiveBrandViewsBySegment } from '@/utils/pl-plan-archive-brand-view'
|
||||
import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
||||
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
@@ -579,8 +594,7 @@
|
||||
{
|
||||
label: '状态',
|
||||
value: statusMap[status] || status,
|
||||
tagType:
|
||||
status === 'completed' ? 'success' : status === 'voided' ? 'info' : 'warning'
|
||||
tagType: status === 'completed' ? 'success' : status === 'voided' ? 'info' : 'warning'
|
||||
},
|
||||
{ label: '计划号', value: ioOrder.value.planNo },
|
||||
{ label: '车牌', value: ioOrder.value.vehicleNo },
|
||||
@@ -608,18 +622,15 @@
|
||||
it.brandId &&
|
||||
!options.some((o) => o.brandId === it.brandId)
|
||||
) {
|
||||
options = [
|
||||
{ brandId: it.brandId, brandName: it.brandName || '指定品牌' },
|
||||
...options
|
||||
]
|
||||
options = [{ brandId: it.brandId, brandName: it.brandName || '指定品牌' }, ...options]
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function mapIoItemBrandQtyRows(it: PlIoOrder['items'][number]): BrandQtyRow[] {
|
||||
const saved = (it.brandQtys || [])
|
||||
.filter(r => Number(r.qty) > 0)
|
||||
.map(r => ({
|
||||
.filter((r) => Number(r.qty) > 0)
|
||||
.map((r) => ({
|
||||
brandId: r.brandId,
|
||||
brandName: r.brandName || '',
|
||||
qty: Number(r.qty)
|
||||
@@ -639,7 +650,7 @@
|
||||
]
|
||||
}
|
||||
if (it.brandMode === 'any') {
|
||||
return (it.brandOptions || []).map(opt => ({
|
||||
return (it.brandOptions || []).map((opt) => ({
|
||||
brandId: opt.brandId,
|
||||
brandName: opt.brandName || '',
|
||||
qty: 0
|
||||
@@ -650,16 +661,14 @@
|
||||
|
||||
function openIoOrderEditDialog() {
|
||||
if (!ioOrder.value) return
|
||||
const planProductById = new Map((products.value || []).map(p => [p.id, p]))
|
||||
const planProductById = new Map((products.value || []).map((p) => [p.id, p]))
|
||||
ioOrderEditForm.value = {
|
||||
...ioOrder.value,
|
||||
items: (ioOrder.value.items || []).map(it => {
|
||||
items: (ioOrder.value.items || []).map((it) => {
|
||||
const brandOptions = resolveIoBrandOptions(it)
|
||||
const brandQtyRows = mapIoItemBrandQtyRows({ ...it, brandOptions })
|
||||
const actualQty = it.brandMode
|
||||
? Number(
|
||||
brandQtyRows.reduce((sum, r) => sum + Number(r.qty || 0), 0).toFixed(3)
|
||||
)
|
||||
? Number(brandQtyRows.reduce((sum, r) => sum + Number(r.qty || 0), 0).toFixed(3))
|
||||
: it.actualQty
|
||||
const planProduct = planProductById.get(it.planProductId)
|
||||
return {
|
||||
@@ -684,7 +693,7 @@
|
||||
for (const it of items) {
|
||||
if (!it.brandMode) continue
|
||||
const rows = it.brandQtyRows || []
|
||||
const positive = rows.filter(r => Number(r.qty) > 0)
|
||||
const positive = rows.filter((r) => Number(r.qty) > 0)
|
||||
if (!positive.length) {
|
||||
ElMessage.warning(`「${it.productName || '产品'}」请至少填写一个品牌数量`)
|
||||
return
|
||||
@@ -699,18 +708,21 @@
|
||||
ioOrderSaving.value = true
|
||||
try {
|
||||
const images = ioOrderEditImagesCsv.value
|
||||
? ioOrderEditImagesCsv.value.split(',').map(s => s.trim()).filter(Boolean)
|
||||
? ioOrderEditImagesCsv.value
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
await api.patch(ioOrderEditForm.value.id, {
|
||||
siteFeedbackRemark: ioOrderEditForm.value.siteFeedbackRemark,
|
||||
siteFeedbackImages: images,
|
||||
items: items.map(it => ({
|
||||
items: items.map((it) => ({
|
||||
id: it.id,
|
||||
actualQty: Number(it.actualQty ?? 0),
|
||||
brandQtys: it.brandMode
|
||||
? (it.brandQtyRows || [])
|
||||
.filter(r => Number(r.qty) > 0)
|
||||
.map(r => ({
|
||||
.filter((r) => Number(r.qty) > 0)
|
||||
.map((r) => ({
|
||||
brandId: r.brandId,
|
||||
brandName: r.brandName,
|
||||
qty: Number(r.qty)
|
||||
@@ -777,9 +789,13 @@
|
||||
productRows.value = enriched
|
||||
}
|
||||
|
||||
watch([products, contractSnapshot], () => {
|
||||
void refreshProductRows()
|
||||
}, { immediate: true, deep: true })
|
||||
watch(
|
||||
[products, contractSnapshot],
|
||||
() => {
|
||||
void refreshProductRows()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const planProductById = computed(() => {
|
||||
const map = new Map<string, (typeof products.value)[number]>()
|
||||
@@ -797,7 +813,9 @@
|
||||
return map
|
||||
})
|
||||
|
||||
function resolveFeedbackPlanProductId(row: TransportDriverProductFeedback | Record<string, unknown>) {
|
||||
function resolveFeedbackPlanProductId(
|
||||
row: TransportDriverProductFeedback | Record<string, unknown>
|
||||
) {
|
||||
const raw =
|
||||
(row as TransportDriverProductFeedback).planProductId ||
|
||||
(row as { plan_product_id?: string }).plan_product_id ||
|
||||
@@ -873,7 +891,9 @@
|
||||
const fb = feedbackById.get(String(pp.id))
|
||||
if (!fb) continue
|
||||
usedIds.add(String(pp.id))
|
||||
rows.push(buildDriverFeedbackDisplayRow(pp, fb, planProductFormById.value.get(String(pp.id))))
|
||||
rows.push(
|
||||
buildDriverFeedbackDisplayRow(pp, fb, planProductFormById.value.get(String(pp.id)))
|
||||
)
|
||||
}
|
||||
|
||||
// 兼容:planProductId 对不上时按序号兜底
|
||||
@@ -891,7 +911,9 @@
|
||||
const id = resolveFeedbackPlanProductId(fb)
|
||||
if (id && usedIds.has(id)) continue
|
||||
const pp = id ? planProductById.value.get(id) : undefined
|
||||
rows.push(buildDriverFeedbackDisplayRow(pp, fb, id ? planProductFormById.value.get(id) : undefined))
|
||||
rows.push(
|
||||
buildDriverFeedbackDisplayRow(pp, fb, id ? planProductFormById.value.get(id) : undefined)
|
||||
)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -963,8 +985,7 @@
|
||||
String(formRow?.productNo || '').trim(),
|
||||
String(pp?.productNo || '').trim()
|
||||
].filter(Boolean)
|
||||
const productNo =
|
||||
candidates.find((no) => no && no !== productId) || candidates[0] || ''
|
||||
const productNo = candidates.find((no) => no && no !== productId) || candidates[0] || ''
|
||||
const brandLabel = resolveDisplayBrandLabel({
|
||||
brandMode: it.brandMode || pp?.brandMode || formRow?.brandMode,
|
||||
brandName: it.brandName || pp?.brandName || formRow?.brandName
|
||||
@@ -986,10 +1007,7 @@
|
||||
remark: (it.remark || pp?.remark || formRow?.remark || '').trim(),
|
||||
unit: unitCode || it.unit,
|
||||
unitLabel:
|
||||
it.unitLabel ||
|
||||
dictStore.getLabel('crm_product_unit', unitCode) ||
|
||||
unitCode ||
|
||||
''
|
||||
it.unitLabel || dictStore.getLabel('crm_product_unit', unitCode) || unitCode || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1006,7 +1024,7 @@
|
||||
)
|
||||
|
||||
const dibangPlanRows = computed(() =>
|
||||
dibangRows.value.filter(row => Boolean(row.link.dbPurchasePlanId?.trim()))
|
||||
dibangRows.value.filter((row) => Boolean(row.link.dbPurchasePlanId?.trim()))
|
||||
)
|
||||
|
||||
const siteInventoryRows = computed(() => siteRows.value)
|
||||
@@ -1169,11 +1187,15 @@
|
||||
ElMessage.warning('暂无现场链接')
|
||||
return
|
||||
}
|
||||
const product = products.value.find(item => item.id === row.link.planProductId)
|
||||
const product = products.value.find((item) => item.id === row.link.planProductId)
|
||||
const productText = product
|
||||
? formatPlanProductListText(product, unit => dictStore.getLabel('crm_product_unit', unit) || unit, {
|
||||
usePlannedQuantity: true
|
||||
})
|
||||
? formatPlanProductListText(
|
||||
product,
|
||||
(unit) => dictStore.getLabel('crm_product_unit', unit) || unit,
|
||||
{
|
||||
usePlannedQuantity: true
|
||||
}
|
||||
)
|
||||
: row.link.productName || '—'
|
||||
const text = buildSiteSummaryFromInventoryLinks({
|
||||
planNo: row.link.planNo || plan.value?.planNo,
|
||||
@@ -1212,12 +1234,14 @@
|
||||
|
||||
const deliveryViews = computed(() => {
|
||||
if (plan.value?.deliveries?.length) {
|
||||
return mapDeliveriesFromApi(plan.value.deliveries, plan.value.load?.customerName).map((row, index) => ({
|
||||
id: plan.value?.deliveries?.[index]?.id,
|
||||
...row,
|
||||
address: resolveDeliveryViewAddress(plan.value, row, index),
|
||||
products: resolveStopProductsForView(row.products, productRows.value)
|
||||
}))
|
||||
return mapDeliveriesFromApi(plan.value.deliveries, plan.value.load?.customerName).map(
|
||||
(row, index) => ({
|
||||
id: plan.value?.deliveries?.[index]?.id,
|
||||
...row,
|
||||
address: resolveDeliveryViewAddress(plan.value, row, index),
|
||||
products: resolveStopProductsForView(row.products, productRows.value)
|
||||
})
|
||||
)
|
||||
}
|
||||
return mapLegacyPlanToStops(plan.value || ({} as any)).deliveries.map((row, index) => ({
|
||||
id: undefined as string | undefined,
|
||||
@@ -1227,6 +1251,23 @@
|
||||
}))
|
||||
})
|
||||
|
||||
const archiveBrandViewsBySegment = computed(() =>
|
||||
buildArchiveBrandViewsBySegment({
|
||||
loadProducts: loadView.value.products || [],
|
||||
deliveries: deliveryViews.value.map((row, index) => ({
|
||||
key: `delivery-${row.id || index}`,
|
||||
products: row.products || []
|
||||
})),
|
||||
planProducts: productRows.value
|
||||
})
|
||||
)
|
||||
|
||||
const loadArchiveBrandItems = computed(() => archiveBrandViewsBySegment.value.get('load') || [])
|
||||
|
||||
function deliveryArchiveBrandItems(delivery: { id?: string }, index: number) {
|
||||
return archiveBrandViewsBySegment.value.get(`delivery-${delivery.id || index}`) || []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => deliveryViews.value.length,
|
||||
(len) => {
|
||||
@@ -1337,7 +1378,6 @@
|
||||
() => plan.value?.transportStatus === 'arranged' && plan.value?.followStatus !== 'cancelled'
|
||||
)
|
||||
|
||||
|
||||
const feedbackBlocksBind = computed(() => ({
|
||||
feedbackEditable: props.feedbackEditable,
|
||||
showArchiveIoFields: props.showArchiveIoFields,
|
||||
@@ -1405,10 +1445,7 @@
|
||||
const qty = (transport.value as Transport & { transportQty?: number })?.transportQty
|
||||
if (qty != null) return String(qty)
|
||||
if (!productRows.value.length) return '—'
|
||||
const sum = productRows.value.reduce(
|
||||
(total, item) => total + Number(item.actualQty ?? 0),
|
||||
0
|
||||
)
|
||||
const sum = productRows.value.reduce((total, item) => total + Number(item.actualQty ?? 0), 0)
|
||||
return sum ? String(sum) : '—'
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user