This commit is contained in:
zlwu committed 2026-08-30 18:33:13 +08:00
1 parent c8cec132bf
commit 19c5aaa8f5
95 files changed
+309129 -9838

No files matched your search

@@ -0,0 +1,535 @@
<!-- eslint-disable vue/no-v-model-argument -->
<!-- 合同详情 · 关联运输计划列表(列与 /#/pl/plan 一致;壳层对齐 dialog-list-panel / OpenSpec) -->
<template>
<div class="pl-contract-linked-plans-panel panel dialog-list-panel">
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
共关联 <span class="sel-num">{{ pagination.total }}</span> 条运输计划
</div>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll bx-list-table-wrap">
<ListTable
table-key="crm-contract-linked-plans"
class="list-table pl-plan-merge-list-table"
:data="displayRows"
v-loading="loading"
row-key="rowKey"
:span-method="planListSpanMethod"
:row-class-name="getPlPlanListRowClassName"
empty-text="暂无关联运输计划"
@row-dblclick="handleDisplayRowDblClick"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="getDisplayRowIndex"
/>
<el-table-column prop="planNo" label="计划编号" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row.plan)">{{ row.plan.planNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="followStatus" label="跟进状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getPlPlanFollowStatusTagType(row.plan.followStatus)" size="small">
{{ getPlPlanFollowStatusLabel(row.plan.followStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center">
<template #default="{ row }">{{ row.plan.planDate || '—' }}</template>
</el-table-column>
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
<template #default="{ row }">{{ getPlanTypeLabel(row.plan.planType) }}</template>
</el-table-column>
<el-table-column
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ formatPlVehiclePlateDisplay(row.plan.licensePlate, row.plan.trailerNo) }}
</template>
</el-table-column>
<el-table-column
prop="personnelSummary"
label="人员"
min-width="180"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ normalizePlPersonnelSummaryDisplay(row.plan.personnelSummary) }}
</template>
</el-table-column>
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center">
<template #default="{ row }">{{ row.plan.followUserName || '—' }}</template>
</el-table-column>
<el-table-column
prop="arrangeCopy"
label="链接复制"
min-width="88"
align="center"
header-align="center"
>
<template #default="{ row }">
<div v-if="row.plan.canCopyPoundQrcode" class="pl-plan-arrange-copy-cell">
<el-button
class="btn-ghost"
title="复制磅单二维码"
:loading="
arrangeCopyLoadingId === row.plan.id && arrangeCopyLoadingAction === 'qrcode'
"
@click.stop="handleListCopyPoundQrcode(row.plan)"
>
磅单码
</el-button>
</div>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="stopKind"
label="段次"
min-width="88"
align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<span class="pl-plan-stop-kind-text">{{ getStopKindLabel(row) }}</span>
</template>
</el-table-column>
<el-table-column
prop="stopCustomer"
label="客户"
min-width="200"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
show-overflow-tooltip
>
<template #default="{ row }">
<span>{{ getStopCustomerName(row) }}</span>
<el-tag
v-if="isPlInternalCustomerName(getStopCustomerName(row))"
type="success"
size="small"
class="pl-plan-internal-customer-tag"
>
内部
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="stopAddressTime"
label="地址 / 时间"
min-width="220"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<div class="pl-plan-stop-address-time">
<div class="pl-plan-stop-address-time__addr">{{ getStopAddress(row) }}</div>
<div class="pl-plan-stop-address-time__time">{{ getStopTime(row) }}</div>
</div>
</template>
</el-table-column>
<el-table-column
label="产品名称"
min-width="160"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="name"
/>
</template>
</el-table-column>
<el-table-column
label="型号/规格"
min-width="96"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="spec"
/>
</template>
</el-table-column>
<el-table-column
label="包装规格"
min-width="120"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="packageSpec"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="64"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="unit"
/>
</template>
</el-table-column>
<el-table-column
label="计划数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="planQty"
/>
</template>
</el-table-column>
<el-table-column
label="实际数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-kind="row.kind"
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="actualQty"
/>
</template>
</el-table-column>
<el-table-column
label="计划附件"
min-width="200"
align="left"
header-align="left"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListCertCell
:products="resolveStopCertProducts(row.plan, getStopProducts(row))"
compact
hide-product
/>
</template>
</el-table-column>
<el-table-column
label="合同"
min-width="180"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<PlPlanContractCell :lines="getStopContractLines(row)" />
</template>
</el-table-column>
<el-table-column
label="入库数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="inboundQty"
/>
</template>
</el-table-column>
<el-table-column
label="出库数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="outboundQty"
/>
</template>
</el-table-column>
<el-table-column
prop="stopDibang"
label="地磅"
min-width="72"
align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<el-tag
v-if="formatStopDibangLabel(row.stop)"
:type="getStopDibangTagType(row.stop)"
size="small"
>
{{ formatStopDibangLabel(row.stop) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</ListTable>
</div>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
共 <span class="sel-num">{{ pagination.total }}</span> 条
</div>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="sizes, prev, pager, next"
background
@size-change="handlePageSizeChange"
@current-change="fetchData"
/>
</div>
</div>
<PlPlanViewDialog
v-model:visible="viewVisible"
:plan-id="viewPlanId"
show-inbound-outbound-qty
/>
<DbPoundSlipQrcodeDialog
ref="qrcodeDialogRef"
v-model:visible="qrcodeVisible"
:inventory-id="selectedInventoryId"
:quantity-unit="selectedInventoryUnit"
/>
<PlPlanPoundQrcodePickDialog
v-model:visible="poundQrcodePickVisible"
:records="poundQrcodePickRecords"
:plan="arrangeActionPlan"
@copy="handlePoundQrcodePickCopy"
@preview="handlePoundQrcodePickPreview"
/>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { planApi, type Plan } from '@/api/pl'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
import PlPlanPoundQrcodePickDialog from '@/components/business/pl-plan-pound-qrcode-pick-dialog/index.vue'
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
import {
formatStopDibangLabel,
getPlPlanListRowClassName,
getStopAddress,
getStopContractLines,
getStopCustomerName,
getStopDibangTagType,
getStopKindLabel,
getStopProducts,
getStopTime,
resolveStopCertProducts
} from '@/utils/pl-plan-list-display-rows'
import {
formatPlVehiclePlateDisplay,
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType,
normalizePlPersonnelSummaryDisplay
} from '@/utils/pl-display'
import { getPlanTypeLabel } from '@/composables/usePlContractToPlan'
const props = defineProps<{
contractId?: string | null
active?: boolean
}>()
const loading = ref(false)
const planList = ref<Plan[]>([])
const selectedRows = ref<Plan[]>([])
const viewVisible = ref(false)
const viewPlanId = ref('')
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
/** 与 pl/plan 列表 PLAN_MERGE_COLUMN_PROPS 顺序一致(含勾选/序号/计划编号固定 3 列) */
const PLAN_MERGE_COLUMN_PROPS = [
'followStatus',
'planDate',
'planType',
'licensePlate',
'personnelSummary',
'followUserName',
'arrangeCopy'
] as const
const visiblePlanMergeProps = computed(() => [...PLAN_MERGE_COLUMN_PROPS])
const {
displayRows,
planListSpanMethod,
getDisplayRowIndex,
handleDisplayRowDblClick
} = usePlPlanMergeListTable({
planList,
visiblePlanMergeProps,
selectedRows,
pagination,
onView: handleView,
fixedMergeColCount: 2
})
const {
plan: arrangeActionPlan,
qrcodeVisible,
selectedInventoryId,
selectedInventoryUnit,
qrcodeDialogRef,
poundQrcodePickVisible,
poundQrcodePickRecords,
loadArrangeActions,
copyPoundSlipQrcode,
handlePoundQrcodePickCopy,
handlePoundQrcodePickPreview
} = usePlPlanArrangeActions()
const arrangeCopyLoadingId = ref('')
const arrangeCopyLoadingAction = ref<'qrcode' | ''>('')
async function handleListCopyPoundQrcode(row: Plan) {
if (!row.id || arrangeCopyLoadingId.value) return
arrangeCopyLoadingId.value = row.id
arrangeCopyLoadingAction.value = 'qrcode'
try {
await loadArrangeActions(row.id)
await copyPoundSlipQrcode()
} catch {
ElMessage.error('复制失败,请稍后重试')
} finally {
arrangeCopyLoadingId.value = ''
arrangeCopyLoadingAction.value = ''
}
}
async function fetchData() {
const cid = String(props.contractId || '').trim()
if (!cid) {
planList.value = []
pagination.total = 0
return
}
loading.value = true
try {
const res = await planApi.list({
contractId: cid,
page: pagination.page,
pageSize: pagination.pageSize
})
const data = (res as any)?.data ?? res
planList.value = data?.list || []
pagination.total = data?.total || 0
} catch {
planList.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
function handlePageSizeChange() {
pagination.page = 1
fetchData()
}
function handleView(plan: Plan) {
if (!plan?.id) return
viewPlanId.value = plan.id
viewVisible.value = true
}
watch(
() => [props.contractId, props.active] as const,
([cid, active]) => {
if (!active || !cid) return
pagination.page = 1
fetchData()
},
{ immediate: true }
)
defineExpose({ refresh: fetchData })
</script>
<style scoped lang="scss">
@use '@styles/business/pl-plan-merge-list-table.scss';
.pl-contract-linked-plans-panel {
.bx-list-table-wrap {
min-width: 0;
}
.pl-plan-arrange-copy-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.pl-plan-arrange-copy-cell .btn-ghost.el-button {
margin: 0;
}
}
</style>
@@ -1,9 +1,10 @@
<template>
<div class="pl-contract-product-arrange-qty-host">
<el-dialog
:model-value="modelValue"
class="dialog-form-layout pl-contract-product-arrange-qty-dialog"
:title="dialogTitle"
width="640px"
width="min(2000px, calc(100vw - 32px))"
align-center
destroy-on-close
append-to-body
@@ -26,25 +27,274 @@
<div class="v is-avail">{{ availableQtyText }}</div>
</div>
</div>
<div class="arrange-qty-list-title">已安排本产品的运输计划</div>
<el-table
v-if="(detail?.plans || []).length"
:data="detail?.plans || []"
class="list-table"
stripe
size="small"
max-height="280"
>
<el-table-column prop="planNo" label="计划编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="planDate" label="计划日期" min-width="110" />
<el-table-column label="计划数量" min-width="100" align="right">
<template #default="{ row }">{{ formatQty(row.planQty) }}</template>
</el-table-column>
<el-table-column label="状态" min-width="100">
<template #default="{ row }">{{ statusLabel(row) }}</template>
</el-table-column>
</el-table>
<p v-else class="arrange-qty-empty">暂无已安排计划</p>
<div class="panel dialog-list-panel arrange-qty-plan-panel">
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已安排本产品的运输计划
<template v-if="arrangedPlanCount">
· 共 <span class="sel-num">{{ arrangedPlanCount }}</span> 条
</template>
</div>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll bx-list-table-wrap">
<ListTable
v-if="displayRows.length"
table-key="pl-contract-product-arrange-qty-plans"
class="list-table pl-plan-merge-list-table"
:data="displayRows"
row-key="rowKey"
:span-method="planListSpanMethod"
:row-class-name="getPlPlanListRowClassName"
empty-text="暂无已安排计划"
@row-dblclick="handleDisplayRowDblClick"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="getDisplayRowIndex"
/>
<el-table-column prop="planNo" label="计划编号" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row.plan)">{{
row.plan.planNo
}}</el-link>
</template>
</el-table-column>
<el-table-column prop="followStatus" label="跟进状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getPlPlanFollowStatusTagType(row.plan.followStatus)" size="small">
{{ getPlPlanFollowStatusLabel(row.plan.followStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center">
<template #default="{ row }">{{ row.plan.planDate || '—' }}</template>
</el-table-column>
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
<template #default="{ row }">{{ getPlanTypeLabel(row.plan.planType) }}</template>
</el-table-column>
<el-table-column
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ formatPlVehiclePlateDisplay(row.plan.licensePlate, row.plan.trailerNo) }}
</template>
</el-table-column>
<el-table-column
prop="personnelSummary"
label="人员"
min-width="180"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ normalizePlPersonnelSummaryDisplay(row.plan.personnelSummary) }}
</template>
</el-table-column>
<el-table-column
prop="stopKind"
label="段次"
min-width="88"
align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<span class="pl-plan-stop-kind-text">{{ getStopKindLabel(row) }}</span>
</template>
</el-table-column>
<el-table-column
prop="stopCustomer"
label="客户"
min-width="200"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
show-overflow-tooltip
>
<template #default="{ row }">
<span>{{ getStopCustomerName(row) }}</span>
<el-tag
v-if="isPlInternalCustomerName(getStopCustomerName(row))"
type="success"
size="small"
class="pl-plan-internal-customer-tag"
>
内部
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="stopAddressTime"
label="地址 / 时间"
min-width="220"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<div class="pl-plan-stop-address-time">
<div class="pl-plan-stop-address-time__addr">{{ getStopAddress(row) }}</div>
<div class="pl-plan-stop-address-time__time">{{ getStopTime(row) }}</div>
</div>
</template>
</el-table-column>
<el-table-column
label="产品名称"
min-width="160"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="name"
/>
</template>
</el-table-column>
<el-table-column
label="型号/规格"
min-width="96"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="spec"
/>
</template>
</el-table-column>
<el-table-column
label="包装规格"
min-width="120"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="packageSpec"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="64"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="unit"
/>
</template>
</el-table-column>
<el-table-column
label="计划数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="planQty"
/>
</template>
</el-table-column>
<el-table-column
label="实际数量"
min-width="100"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-kind="row.kind"
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="actualQty"
/>
</template>
</el-table-column>
<el-table-column
label="计划附件"
min-width="200"
align="left"
header-align="left"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListCertCell
:products="resolveStopCertProducts(row.plan, getStopProducts(row))"
compact
hide-product
/>
</template>
</el-table-column>
<el-table-column
label="合同"
min-width="180"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<PlPlanContractCell :lines="getStopContractLines(row)" />
</template>
</el-table-column>
<el-table-column
prop="stopDibang"
label="地磅"
min-width="72"
align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<el-tag
v-if="formatStopDibangLabel(row.stop)"
:type="getStopDibangTagType(row.stop)"
size="small"
>
{{ formatStopDibangLabel(row.stop) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</ListTable>
<p v-else class="arrange-qty-empty">暂无已安排计划</p>
</div>
</div>
</div>
</div>
<template #footer>
<el-button type="primary" @click="emit('update:modelValue', false)">
@@ -52,12 +302,53 @@
</el-button>
</template>
</el-dialog>
<PlPlanViewDialog
:visible="viewVisible"
:plan-id="viewPlanId"
show-inbound-outbound-qty
@update:visible="viewVisible = $event"
/>
</div>
</template>
<script setup lang="ts">
import { Check } from '@element-plus/icons-vue'
import { planApi, type ContractProductArrangeQtyDetail } from '@/api/pl'
import {
planApi,
type ContractProductArrangeQtyDetail,
type Plan
} from '@/api/pl'
import ListTable from '@/components/business/list-table/index.vue'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
import { getPlanTypeLabel } from '@/composables/usePlContractToPlan'
import { pickBxData } from '@/utils/http/bx'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
import {
formatPlVehiclePlateDisplay,
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType,
normalizePlPersonnelSummaryDisplay
} from '@/utils/pl-display'
import {
buildPlPlanListSpanMethod,
flattenPlanListRowsForProduct,
formatStopDibangLabel,
getPlanListDisplayIndex,
getPlPlanListRowClassName,
getStopAddress,
getStopContractLines,
getStopCustomerName,
getStopDibangTagType,
getStopKindLabel,
getStopProducts,
getStopTime,
resolveStopCertProducts,
type PlPlanListDisplayRow
} from '@/utils/pl-plan-list-display-rows'
const props = defineProps<{
modelValue: boolean
@@ -71,6 +362,30 @@
const loading = ref(false)
const detail = ref<ContractProductArrangeQtyDetail | null>(null)
const planList = ref<Plan[]>([])
const viewVisible = ref(false)
const viewPlanId = ref('')
/** 与运输计划列表计划区合并列一致(本弹窗无勾选/链接复制) */
const PLAN_MERGE_COLUMN_PROPS = [
'followStatus',
'planDate',
'planType',
'licensePlate',
'personnelSummary'
] as const
const productIdRef = computed(() => String(props.productId || detail.value?.productId || '').trim())
const displayRows = computed(() =>
flattenPlanListRowsForProduct(planList.value, productIdRef.value)
)
const planListSpanMethod = computed(() =>
buildPlPlanListSpanMethod([...PLAN_MERGE_COLUMN_PROPS], 2)
)
const arrangedPlanCount = computed(() => (detail.value?.plans || []).length)
const dialogTitle = computed(
() => `合同用量 · ${props.productName || detail.value?.productName || '产品'}`
@@ -97,13 +412,18 @@
return Number(n).toFixed(3)
}
function statusLabel(row: { archiveStatus?: string; followStatus?: string; transportStatus?: string }) {
const archive = (row.archiveStatus || '').trim()
if (archive === 'archived') return '已归档'
const follow = (row.followStatus || '').trim()
if (follow === 'completed') return '已完成'
if (follow === 'arranged' || row.transportStatus === 'arranged') return '已安排'
return follow || row.transportStatus || '—'
function getDisplayRowIndex(index: number) {
return getPlanListDisplayIndex(displayRows.value, index, 1, displayRows.value.length || 1)
}
function handleView(plan: Plan) {
if (!plan?.id) return
viewPlanId.value = plan.id
viewVisible.value = true
}
function handleDisplayRowDblClick(row: PlPlanListDisplayRow) {
if (row?.plan) handleView(row.plan)
}
async function loadDetail() {
@@ -111,6 +431,7 @@
const productId = String(props.productId || '').trim()
if (!contractId || !productId) {
detail.value = null
planList.value = []
return
}
loading.value = true
@@ -120,9 +441,29 @@
productId,
excludePlanId: props.excludePlanId || undefined
})
detail.value = pickBxData<ContractProductArrangeQtyDetail>(res) || null
const data = pickBxData<ContractProductArrangeQtyDetail>(res) || null
detail.value = data
const orderedIds = (data?.plans || [])
.map((p) => String(p.planId || '').trim())
.filter(Boolean)
if (!orderedIds.length) {
planList.value = []
return
}
const idSet = new Set(orderedIds)
const listRes = await planApi.list({
contractId,
page: 1,
pageSize: 500
})
const listData = (listRes as any)?.data ?? listRes
const all: Plan[] = listData?.list || []
const byId = new Map(all.filter((p) => p?.id && idSet.has(p.id)).map((p) => [p.id, p]))
planList.value = orderedIds.map((id) => byId.get(id)).filter(Boolean) as Plan[]
} catch {
detail.value = null
planList.value = []
} finally {
loading.value = false
}
@@ -137,8 +478,10 @@
</script>
<style scoped lang="scss">
@use '@styles/business/pl-plan-merge-list-table.scss';
.arrange-qty-body {
min-height: 120px;
min-height: 420px;
}
.arrange-qty-sub {
margin: 0 0 12px;
@@ -148,21 +491,21 @@
.arrange-qty-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 14px;
gap: 10px;
margin-bottom: 16px;
}
.arrange-qty-stat {
padding: 10px;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
background: #f8fafc;
.k {
font-size: 11px;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.v {
margin-top: 2px;
font-size: 15px;
margin-top: 4px;
font-size: 18px;
font-weight: 700;
font-variant-numeric: tabular-nums;
&.is-avail {
@@ -170,14 +513,27 @@
}
}
}
.arrange-qty-list-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
.arrange-qty-plan-panel {
margin-top: 0;
}
.arrange-qty-empty {
margin: 0;
font-size: 13px;
margin: 16px 0;
font-size: 14px;
color: var(--mint-text-muted, #64748b);
}
.bx-list-table-wrap {
min-width: 0;
}
</style>
<style lang="scss">
.pl-contract-product-arrange-qty-dialog.dialog-form-layout {
width: min(2000px, calc(100vw - 32px)) !important;
max-width: min(2000px, calc(100vw - 32px));
.el-dialog__body {
max-height: calc(100vh - 140px);
min-height: 480px;
}
}
</style>
@@ -33,6 +33,7 @@
:customer-name="meta.customerName"
:generate-dibang="generateDibang"
:dibang-rows="dibangRows"
:plan-product-rows="products"
:driver-feedback="driverFeedback"
:site-feedback="siteFeedback"
:fallback-remark="fallbackRemark"
@@ -159,7 +159,7 @@
class="pl-stop-feedback-dibang-item"
:class="{ 'pl-stop-feedback-dibang-item--split': dibangRows.length > 1 }"
>
<p v-if="dibangRows.length > 1" class="pl-stop-feedback-ticket__product">
<p class="pl-stop-feedback-ticket__product">
{{ dibangProductLabel(row, idx) }}
</p>
<div class="pl-stop-feedback-ticket__qty pl-stop-feedback-ticket__qty--compact">
@@ -219,6 +219,7 @@
import type { TransportStopFeedback } from '@/api/pl'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { formatKgToTon } from '@/components/business/db/db-related-view'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import type { PlPlanTransportDibangRow } from '@/composables/usePlPlanViewDetail'
import { formatDateTime } from '@/utils/format'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
@@ -229,6 +230,8 @@
customerName?: string
generateDibang?: number
dibangRows?: PlPlanTransportDibangRow[]
/** 用于解析 CRM 标准产品名(库内 link.productName 可能是别名) */
planProductRows?: PlanFormProductRow[]
driverFeedback?: TransportStopFeedback | null
siteFeedback?: TransportStopFeedback | null
/** 段级无备注时回退展示(如运输单级司机回传备注) */
@@ -245,6 +248,7 @@
customerName: '',
generateDibang: 0,
dibangRows: () => [],
planProductRows: () => [],
driverFeedback: null,
siteFeedback: null,
fallbackRemark: '',
@@ -388,8 +392,37 @@
return { qty: String(qty), unit: ` ${unit}` }
}
function resolvePlanProductRow(row: PlPlanTransportDibangRow) {
const id = String(row.link.planProductId || '').trim()
if (!id) return undefined
return (props.planProductRows || []).find((p) => String(p.id || '').trim() === id)
}
function dibangProductLabel(row: PlPlanTransportDibangRow, idx: number): string {
return row.link.productName || row.inventory?.product || `地磅 ${idx + 1}`
const planRow = resolvePlanProductRow(row)
// 优先 CRM 标准名;link.productName / planRow.productName 可能是别名
const product = (
String(planRow?.standardProductName || '').trim() ||
row.inventory?.product ||
row.weighing?.cargoName ||
String(planRow?.productName || '').trim() ||
row.link.productName ||
''
).trim()
const specification = (
String(planRow?.model || '').trim() ||
row.inventory?.specification ||
row.weighing?.specification ||
''
).trim()
if (!product && !specification) return `地磅 ${idx + 1}`
if (!specification) return product
if (!product) return specification
if (product.endsWith(specification)) {
const base = product.slice(0, -specification.length).trimEnd()
return base ? `${base} ${specification}` : specification
}
return `${product} ${specification}`
}
function dibangBatchNo(row: PlPlanTransportDibangRow): string {
@@ -492,6 +525,11 @@
&--dibang {
background: linear-gradient(90deg, #fff7ed 0%, #f8fafc 100%);
}
// 标题栏与正文第一行间距
& + * {
margin-top: 5px;
}
}
.pl-stop-feedback-ticket__who {
@@ -513,7 +551,7 @@
.pl-stop-feedback-ticket__product {
margin: 0;
padding: 8px 10px 0;
padding: 0 10px;
font-size: 12px;
font-weight: 500;
color: #334155;
@@ -600,7 +638,7 @@
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 10px 0;
padding: 0 10px;
}
.pl-stop-feedback-ticket__product-row {
@@ -644,7 +682,7 @@
}
.pl-stop-feedback-ticket__empty {
padding: 12px 10px;
padding: 7px 10px 12px;
flex: 1;
}
@@ -113,7 +113,8 @@
</div>
</div>
<div class="pl-stop-product-dual-item__row2">
<div class="pl-stop-product-dual-item__field">
<!-- 可安排数量仅运输计划新增/编辑提醒用,详情弹窗不展示 -->
<div v-if="showAvailableQty" class="pl-stop-product-dual-item__field">
<span class="pl-stop-product-dual-item__label">可安排数量</span>
<div class="pl-stop-avail-wrap">
<span class="pl-stop-avail-num">{{ formatAvailableDisplay(row) }}</span>
@@ -276,7 +277,12 @@
</template>
</template>
</el-table-column>
<el-table-column label="可安排数量" min-width="120" align="right">
<el-table-column
v-if="showAvailableQty"
label="可安排数量"
min-width="120"
align="right"
>
<template #default="{ row }">
<div class="pl-stop-avail-wrap pl-stop-avail-wrap--table">
<span class="pl-stop-avail-num">{{ formatAvailableDisplay(row) }}</span>
@@ -419,6 +425,7 @@
/>
<PlContractProductArrangeQtyDialog
v-if="showAvailableQty"
v-model="arrangeQtyDialogVisible"
:contract-id="arrangeQtyContractId"
:product-id="arrangeQtyProductId"
@@ -493,6 +500,8 @@
const canRemove = computed(
() => !!props.editable && (props.allowRemove === undefined ? true : props.allowRemove)
)
/** 仅新增/编辑表单展示可安排数量与问号;详情只读不展示 */
const showAvailableQty = computed(() => !!props.editable)
const dictStore = useDictStore()
@@ -574,11 +583,13 @@
watch(
() =>
[
props.editable,
props.contractId,
props.excludePlanId,
(props.products || []).map((p) => p.productId).join(',')
] as const,
() => {
if (!props.editable) return
const cid = String(props.contractId || '').trim()
if (!cid) return
for (const row of props.products || []) {
@@ -324,14 +324,8 @@
:key="row.link.dbInventoryId || idx"
class="pl-feedback-ticket-qty-block"
>
<p
v-if="dibangPlanRows.length > 1"
class="pl-feedback-ticket-product"
>
<p class="pl-feedback-ticket-product">
{{ dibangProductLabel(row, idx) }}
<template v-if="dibangBatchNo(row) !== '—'">
· {{ dibangBatchNo(row) }}
</template>
</p>
<div class="pl-feedback-ticket-qty">
<div>
@@ -716,8 +710,37 @@
]
}
function resolvePlanProductRow(row: PlPlanTransportDibangRow) {
const id = String(row.link.planProductId || '').trim()
if (!id) return undefined
return (props.planProductRows || []).find((p) => String(p.id || '').trim() === id)
}
function dibangProductLabel(row: PlPlanTransportDibangRow, idx: number): string {
return row.link.productName || row.inventory?.product || `地磅 ${idx + 1}`
const planRow = resolvePlanProductRow(row)
// 优先 CRM 标准名;link.productName / planRow.productName 可能是别名
const product = (
String(planRow?.standardProductName || '').trim() ||
row.inventory?.product ||
row.weighing?.cargoName ||
String(planRow?.productName || '').trim() ||
row.link.productName ||
''
).trim()
const specification = (
String(planRow?.model || '').trim() ||
row.inventory?.specification ||
row.weighing?.specification ||
''
).trim()
if (!product && !specification) return `地磅 ${idx + 1}`
if (!specification) return product
if (!product) return specification
if (product.endsWith(specification)) {
const base = product.slice(0, -specification.length).trimEnd()
return base ? `${base} ${specification}` : specification
}
return `${product} ${specification}`
}
function dibangBatchNo(row: PlPlanTransportDibangRow): string {
@@ -130,6 +130,7 @@
:customer-name="loadView.customerName"
:generate-dibang="loadView.generateDibang"
:dibang-rows="loadDibangRows"
:plan-product-rows="productRows"
:driver-feedback="resolveDriverStopFeedback('load', loadStopId)"
:site-feedback="resolveSiteStopFeedback('load', loadStopId)"
:fallback-remark="transport?.feedbackRemark"
@@ -215,6 +216,7 @@
:customer-name="delivery.customerName"
:generate-dibang="delivery.generateDibang"
:dibang-rows="resolveDeliveryDibangRows(delivery)"
:plan-product-rows="productRows"
:driver-feedback="resolveDriverStopFeedback('delivery', delivery.id || '')"
:site-feedback="resolveSiteStopFeedback('delivery', delivery.id || '')"
:fallback-remark="transport?.feedbackRemark"