提交
This commit is contained in:
1 parent
4611dbc2b2
commit
ebf006eeff
46 files changed
+36802
-20974
No files matched your search
@@ -360,6 +360,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('stopContract')"
|
||||
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)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productName')"
|
||||
label="产品名称"
|
||||
@@ -436,6 +452,9 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -452,6 +471,9 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="inboundQty"
|
||||
@@ -468,12 +490,53 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="outboundQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productUnitPrice')"
|
||||
label="单价(元)"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="unitPrice"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productAmount')"
|
||||
label="金额(元)"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="amount"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="archiveStatsRowRef"
|
||||
@@ -521,6 +584,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<PlPlanArchiveDialog
|
||||
v-model:visible="archiveDialogVisible"
|
||||
:plan-id="archivePlanId"
|
||||
@@ -535,6 +600,7 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import {
|
||||
@@ -550,6 +616,7 @@ import {
|
||||
enrichPlanProductsHazardFromCrm
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanArchiveDialog from '@/components/business/pl-plan-archive-dialog/index.vue'
|
||||
import PlPlanDocumentAttachmentCell from '@/components/business/pl-plan-document-attachment-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
@@ -574,11 +641,14 @@ import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
sumStopActualQty,
|
||||
sumStopArchiveActualQty,
|
||||
sumStopArchiveAmount,
|
||||
sumStopArchiveIoQty,
|
||||
sumStopProductsField,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
@@ -587,6 +657,7 @@ import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
/** 计划区可合并列(顺序须与表格左侧列一致,不含勾选/序号/计划编号) */
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
@@ -627,13 +698,16 @@ const listColumnDefs = [
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'stopContract', label: '合同' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
{ prop: 'productOutboundQty', label: '出库数量' },
|
||||
{ prop: 'productUnitPrice', label: '单价(元)' },
|
||||
{ prop: 'productAmount', label: '金额(元)' }
|
||||
]
|
||||
|
||||
const {
|
||||
@@ -642,7 +716,7 @@ const {
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-archive-list-columns-v3', listColumnDefs)
|
||||
} = usePersistedListColumns('pl-plan-archive-list-columns-v5', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
|
||||
@@ -701,10 +775,21 @@ function getArchiveStatCellValue(row: PlPlanListDisplayRow, prop: string): unkno
|
||||
if (prop === 'stopAddressTime') {
|
||||
return `${getStopAddress(row)} ${getStopTime(row)}`.trim()
|
||||
}
|
||||
if (prop === 'stopContract') {
|
||||
return getStopContractLines(row)
|
||||
.map((l) => `${l.side}${l.no}`)
|
||||
.join(' ')
|
||||
}
|
||||
if (prop === 'productPlanQty') return sumStopProductsField(stopProducts, 'planQty')
|
||||
if (prop === 'productActualQty') return sumStopActualQty(plan, stopProducts)
|
||||
if (prop === 'productInboundQty') return sumStopProductsField(stopProducts, 'inboundQty')
|
||||
if (prop === 'productOutboundQty') return sumStopProductsField(stopProducts, 'outboundQty')
|
||||
if (prop === 'productActualQty') return sumStopArchiveActualQty(stopProducts, row.kind)
|
||||
if (prop === 'productInboundQty') {
|
||||
return sumStopArchiveIoQty(stopProducts, row.kind, row.stop.customerName, 'inbound')
|
||||
}
|
||||
if (prop === 'productOutboundQty') {
|
||||
return sumStopArchiveIoQty(stopProducts, row.kind, row.stop.customerName, 'outbound')
|
||||
}
|
||||
if (prop === 'productUnitPrice') return ''
|
||||
if (prop === 'productAmount') return sumStopArchiveAmount(stopProducts, row.kind)
|
||||
if (prop === 'productName') {
|
||||
return stopProducts
|
||||
.map((p) => p.productName || '')
|
||||
|
||||
@@ -31,11 +31,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="searchForm.customerName"
|
||||
placeholder="搜索客户名称"
|
||||
clearable
|
||||
/>
|
||||
<el-input v-model="searchForm.customerName" placeholder="搜索客户名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期">
|
||||
<el-date-picker
|
||||
@@ -48,6 +44,16 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="证书齐备">
|
||||
<el-select v-model="searchForm.hazardCertReady" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="opt in PL_PLAN_HAZARD_CERT_READY_FILTER_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-start" />
|
||||
@@ -78,25 +84,9 @@
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
<el-button
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'btn-main'
|
||||
: 'btn-ghost'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'primary'
|
||||
: undefined
|
||||
"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:class="canToolbarMarkCertReady ? 'btn-main' : 'btn-ghost'"
|
||||
:type="canToolbarMarkCertReady ? 'primary' : undefined"
|
||||
:disabled="!canToolbarMarkCertReady"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarMarkCertReady"
|
||||
>
|
||||
@@ -104,16 +94,12 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
|
||||
? 'btn-ghost'
|
||||
: 'btn-main'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
|
||||
? undefined
|
||||
: 'primary'
|
||||
"
|
||||
@@ -124,11 +110,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
!isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:disabled="!canToolbarUnmarkCertReady"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarUnmarkCertReady"
|
||||
>
|
||||
@@ -335,6 +317,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('stopContract')"
|
||||
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)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productName')"
|
||||
label="产品名称"
|
||||
@@ -411,6 +409,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -503,6 +502,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<PlPlanCertUploadDialog
|
||||
v-model:visible="certDialogVisible"
|
||||
:plan-id="certPlanId"
|
||||
@@ -512,180 +513,134 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Upload
|
||||
} from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import {
|
||||
buildHazardCertReadyConfirmMessage,
|
||||
isHazardCertReady,
|
||||
planHasHazardProducts
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_CERT_TABS,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
plPlanCertTabToFilters,
|
||||
type PlPlanCertTab
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { CircleCheck, CircleClose, RefreshLeft, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import {
|
||||
buildBatchHazardCertReadyConfirmMessage,
|
||||
buildBatchHazardCertUnmarkConfirmMessage,
|
||||
buildHazardCertReadyConfirmMessage,
|
||||
isHazardCertReady,
|
||||
planHasHazardProducts
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_CERT_TABS,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
PL_PLAN_HAZARD_CERT_READY_FILTER_OPTIONS,
|
||||
plPlanCertTabToFilters,
|
||||
type PlPlanCertTab
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'planDate',
|
||||
'planType',
|
||||
'followStatus',
|
||||
'hazardCertReady',
|
||||
'planAttachments',
|
||||
'licensePlate',
|
||||
'personnelSummary',
|
||||
'followUserName'
|
||||
] as const
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'planDate',
|
||||
'planType',
|
||||
'followStatus',
|
||||
'hazardCertReady',
|
||||
'planAttachments',
|
||||
'licensePlate',
|
||||
'personnelSummary',
|
||||
'followUserName'
|
||||
] as const
|
||||
|
||||
const loading = ref(false)
|
||||
const { mediaActive } = useListMediaActive()
|
||||
const certReadyLoading = ref(false)
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
const certDialogVisible = ref(false)
|
||||
const certPlanId = ref('')
|
||||
const planList = ref<Plan[]>([])
|
||||
const loading = ref(false)
|
||||
const { mediaActive } = useListMediaActive()
|
||||
const certReadyLoading = ref(false)
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
const certDialogVisible = ref(false)
|
||||
const certPlanId = ref('')
|
||||
const planList = ref<Plan[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
{ prop: 'followUserName', label: '跟进人', defaultVisible: false },
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
]
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
{ prop: 'followUserName', label: '跟进人', defaultVisible: false },
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'stopContract', label: '合同' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-cert-list-columns-v3', listColumnDefs)
|
||||
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
|
||||
usePersistedListColumns('pl-plan-cert-list-columns-v4', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const visiblePlanMergeProps = computed(() =>
|
||||
PLAN_MERGE_COLUMN_PROPS.filter((prop) => isColumnVisible(prop))
|
||||
)
|
||||
const visiblePlanMergeProps = computed(() =>
|
||||
PLAN_MERGE_COLUMN_PROPS.filter((prop) => isColumnVisible(prop))
|
||||
)
|
||||
|
||||
const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
isPlanFirstRowSelectable,
|
||||
handleDisplaySelectionChange,
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList,
|
||||
visiblePlanMergeProps,
|
||||
selectedRows,
|
||||
pagination,
|
||||
onView: (plan) => handleView(plan)
|
||||
})
|
||||
function getPlanCertExportFilterParams() {
|
||||
const keyword = quickSearch.value.trim()
|
||||
return {
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const planCertExportColumns = computed(() => {
|
||||
const skip = new Set(['stopKind', 'stopCustomer', 'stopAddressTime'])
|
||||
const cols = visibleColumns.value.filter((col) => !skip.has(col))
|
||||
if (!cols.includes('planNo')) cols.unshift('planNo')
|
||||
return cols
|
||||
})
|
||||
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanCertTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: '',
|
||||
certUploadStatus: '' as string
|
||||
})
|
||||
|
||||
function getPlanTypeLabel(type: string) {
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
isPlanFirstRowSelectable,
|
||||
handleDisplaySelectionChange,
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList,
|
||||
visiblePlanMergeProps,
|
||||
selectedRows,
|
||||
pagination,
|
||||
onView: (plan) => handleView(plan)
|
||||
})
|
||||
function getPlanCertExportFilterParams() {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const res = await planApi.list({
|
||||
return {
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
@@ -695,194 +650,352 @@ async function fetchData() {
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
planList.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
hazardCertReady:
|
||||
searchForm.hazardCertReady === '' ? undefined : Number(searchForm.hazardCertReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePlanTypeTabChange(tab: PlPlanCertTab | string) {
|
||||
const filters = plPlanCertTabToFilters(tab as PlPlanCertTab)
|
||||
searchForm.planType = filters.planType
|
||||
searchForm.certUploadStatus = filters.certUploadStatus || ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
activePlanTypeTab.value = 'all'
|
||||
searchForm.planNo = ''
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.certUploadStatus = ''
|
||||
quickSearch.value = ''
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearch.value = ''
|
||||
handleQuickSearch()
|
||||
}
|
||||
|
||||
const { activeDateShortcut, handleDateShortcut, resetDateShortcut } = useListDateShortcut(
|
||||
dateRange,
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canUploadCert(row: Plan) {
|
||||
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
|
||||
}
|
||||
|
||||
function canManageCertReady(row: Plan) {
|
||||
return canUploadCert(row) && planHasHazardProducts(row.products)
|
||||
}
|
||||
|
||||
function getHazardCertReadyLabel(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return '无需'
|
||||
return isHazardCertReady(row) ? '已齐备' : '待确认'
|
||||
}
|
||||
|
||||
function getHazardCertReadyTagType(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return 'info'
|
||||
return isHazardCertReady(row) ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
async function refreshSidebarBadges() {
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
}
|
||||
|
||||
async function handleMarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm(buildHazardCertReadyConfirmMessage(), '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
const planCertExportColumns = computed(() => {
|
||||
const skip = new Set(['stopKind', 'stopCustomer', 'stopAddressTime', 'stopContract'])
|
||||
const cols = visibleColumns.value.filter((col) => !skip.has(col))
|
||||
if (!cols.includes('planNo')) cols.unshift('planNo')
|
||||
return cols
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
ElMessage.success('已标记证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnmarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm('确认取消该计划的证书齐备状态?取消后将无法安排运输。', '取消齐备', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
const selectedPlan = computed(() =>
|
||||
selectedRows.value.length === 1 ? selectedRows.value[0] : null
|
||||
)
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanCertTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: '',
|
||||
certUploadStatus: '' as string,
|
||||
hazardCertReady: '' as string
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
ElMessage.success('已取消证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
|
||||
function getPlanTypeLabel(type: string) {
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
return map[type] || type
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarMarkCertReady() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleMarkCertReady(selectedRows.value[0])
|
||||
}
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const res = await planApi.list({
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined,
|
||||
hazardCertReady:
|
||||
searchForm.hazardCertReady === '' ? undefined : Number(searchForm.hazardCertReady),
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
planList.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarUnmarkCertReady() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUnmarkCertReady(selectedRows.value[0])
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function syncViewingPlanFromList() {
|
||||
syncViewRowFromFlatList(viewingPlan, planList.value)
|
||||
}
|
||||
function handlePlanTypeTabChange(tab: PlPlanCertTab | string) {
|
||||
const filters = plPlanCertTabToFilters(tab as PlPlanCertTab)
|
||||
searchForm.planType = filters.planType
|
||||
searchForm.certUploadStatus = filters.certUploadStatus || ''
|
||||
searchForm.hazardCertReady = filters.hazardCertReady ?? ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
async function refreshViewIfOpen() {
|
||||
if (!viewVisible.value || !viewPlanId.value) return
|
||||
syncViewingPlanFromList()
|
||||
await viewDialogRef.value?.reload()
|
||||
}
|
||||
|
||||
async function handleCertUploadSuccess() {
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
}
|
||||
|
||||
function handleView(row: Plan) {
|
||||
viewingPlan.value = row
|
||||
viewPlanId.value = row.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleUploadCert(row: Plan) {
|
||||
certPlanId.value = row.id
|
||||
certDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleToolbarUploadCert() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUploadCert(selectedRows.value[0])
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
planNo: searchForm.planNo,
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
certUploadStatus: searchForm.certUploadStatus,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
function handleReset() {
|
||||
activePlanTypeTab.value = 'all'
|
||||
searchForm.planNo = ''
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.certUploadStatus = ''
|
||||
searchForm.hazardCertReady = ''
|
||||
quickSearch.value = ''
|
||||
handleSearch()
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearch.value = ''
|
||||
handleQuickSearch()
|
||||
}
|
||||
|
||||
const { activeDateShortcut, handleDateShortcut, resetDateShortcut } = useListDateShortcut(
|
||||
dateRange,
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canUploadCert(row: Plan) {
|
||||
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
|
||||
}
|
||||
|
||||
function canManageCertReady(row: Plan) {
|
||||
return canUploadCert(row) && planHasHazardProducts(row.products)
|
||||
}
|
||||
|
||||
const certReadyEligibleRows = computed(() =>
|
||||
selectedRows.value.filter((row) => canManageCertReady(row) && !isHazardCertReady(row))
|
||||
)
|
||||
const canToolbarMarkCertReady = computed(() => certReadyEligibleRows.value.length > 0)
|
||||
const certUnmarkEligibleRows = computed(() =>
|
||||
selectedRows.value.filter((row) => canManageCertReady(row) && isHazardCertReady(row))
|
||||
)
|
||||
const canToolbarUnmarkCertReady = computed(() => certUnmarkEligibleRows.value.length > 0)
|
||||
|
||||
function getHazardCertReadyLabel(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return '无需'
|
||||
return isHazardCertReady(row) ? '已齐备' : '待确认'
|
||||
}
|
||||
|
||||
function getHazardCertReadyTagType(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return 'info'
|
||||
return isHazardCertReady(row) ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
async function refreshSidebarBadges() {
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
}
|
||||
|
||||
async function handleMarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm(buildHazardCertReadyConfirmMessage(), '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
ElMessage.success('已标记证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnmarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm('确认取消该计划的证书齐备状态?取消后将无法安排运输。', '取消齐备', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
ElMessage.success('已取消证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarMarkCertReady() {
|
||||
void handleBatchMarkCertReady()
|
||||
}
|
||||
|
||||
async function handleBatchMarkCertReady() {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先勾选运输计划')
|
||||
return
|
||||
}
|
||||
const targets = certReadyEligibleRows.value
|
||||
const skipped = selectedRows.value.length - targets.length
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('所选计划均不可标记证书齐备(已齐备、已归档/作废或无危化品)')
|
||||
return
|
||||
}
|
||||
|
||||
const confirmMessage =
|
||||
targets.length === 1
|
||||
? buildHazardCertReadyConfirmMessage()
|
||||
: buildBatchHazardCertReadyConfirmMessage(targets.length)
|
||||
|
||||
await ElMessageBox.confirm(confirmMessage, '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
|
||||
certReadyLoading.value = true
|
||||
let success = 0
|
||||
let failed = 0
|
||||
try {
|
||||
for (const row of targets) {
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
success += 1
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed === 0) {
|
||||
const skippedHint = skipped > 0 ? `,已跳过 ${skipped} 条不可操作项` : ''
|
||||
ElMessage.success(`已标记 ${success} 条计划证书齐备${skippedHint}`)
|
||||
} else {
|
||||
ElMessage.warning(`成功 ${success} 条,失败 ${failed} 条`)
|
||||
}
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarUnmarkCertReady() {
|
||||
void handleBatchUnmarkCertReady()
|
||||
}
|
||||
|
||||
async function handleBatchUnmarkCertReady() {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先勾选运输计划')
|
||||
return
|
||||
}
|
||||
const targets = certUnmarkEligibleRows.value
|
||||
const skipped = selectedRows.value.length - targets.length
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('所选计划均不可取消证书齐备(未齐备、已归档/作废或无危化品)')
|
||||
return
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
buildBatchHazardCertUnmarkConfirmMessage(targets.length),
|
||||
'取消齐备',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
}
|
||||
)
|
||||
|
||||
certReadyLoading.value = true
|
||||
let success = 0
|
||||
let failed = 0
|
||||
try {
|
||||
for (const row of targets) {
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
success += 1
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed === 0) {
|
||||
const skippedHint = skipped > 0 ? `,已跳过 ${skipped} 条不可操作项` : ''
|
||||
ElMessage.success(`已取消 ${success} 条计划证书齐备${skippedHint}`)
|
||||
} else {
|
||||
ElMessage.warning(`成功 ${success} 条,失败 ${failed} 条`)
|
||||
}
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function syncViewingPlanFromList() {
|
||||
syncViewRowFromFlatList(viewingPlan, planList.value)
|
||||
}
|
||||
|
||||
async function refreshViewIfOpen() {
|
||||
if (!viewVisible.value || !viewPlanId.value) return
|
||||
syncViewingPlanFromList()
|
||||
await viewDialogRef.value?.reload()
|
||||
}
|
||||
|
||||
async function handleCertUploadSuccess() {
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
}
|
||||
|
||||
function handleView(row: Plan) {
|
||||
viewingPlan.value = row
|
||||
viewPlanId.value = row.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleUploadCert(row: Plan) {
|
||||
certPlanId.value = row.id
|
||||
certDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleToolbarUploadCert() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUploadCert(selectedRows.value[0])
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
planNo: searchForm.planNo,
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
certUploadStatus: searchForm.certUploadStatus,
|
||||
hazardCertReady: searchForm.hazardCertReady,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
quickSearch.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<!-- 运输计划合同完善:仅缺合同计划,行内展开分侧保存 -->
|
||||
<!-- 运输计划合同完善:仅缺合同计划,按段次客户弹窗补充合同 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
@@ -17,15 +17,15 @@
|
||||
<el-form-item label="缺口类型">
|
||||
<el-select v-model="searchForm.gapType" placeholder="全部缺口" clearable>
|
||||
<el-option label="全部缺口" value="" />
|
||||
<el-option label="仅缺供方合同" value="supplier" />
|
||||
<el-option label="仅缺需方合同" value="demander" />
|
||||
<el-option label="两侧均缺" value="both" />
|
||||
<el-option label="装货段缺合同" value="supplier" />
|
||||
<el-option label="送货段缺合同" value="demander" />
|
||||
<el-option label="装货与送货均缺" value="both" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="供方 / 需方">
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="searchForm.customerKeyword"
|
||||
placeholder="客户名称"
|
||||
placeholder="段次客户名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -66,7 +66,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/供方/需方"
|
||||
placeholder="计划编号/客户名称"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -87,107 +87,14 @@
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="list-table pl-plan-merge-list-table"
|
||||
:data="displayRows"
|
||||
v-loading="loading"
|
||||
row-key="rowKey"
|
||||
:span-method="planListSpanMethod"
|
||||
:row-class-name="getPlPlanListRowClassName"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
@expand-change="handleExpandChange"
|
||||
@row-dblclick="handleDisplayRowDblClick"
|
||||
>
|
||||
<el-table-column type="expand" class-name="pl-contract-expand-col" width="1">
|
||||
<template #default="{ row }">
|
||||
<div class="pl-contract-expand" v-if="expandDraft[row.plan.id]">
|
||||
<div class="pl-contract-expand__grid">
|
||||
<div
|
||||
class="pl-contract-side"
|
||||
:class="sideBoxClass(asGapItem(row), 'supplier')"
|
||||
>
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>供方合同</h4>
|
||||
<el-tag
|
||||
:type="sideTagType(asGapItem(row), 'supplier')"
|
||||
size="small"
|
||||
:effect="asGapItem(row).supplierContractExempt ? 'plain' : 'dark'"
|
||||
>
|
||||
{{ sideTagLabel(asGapItem(row), 'supplier') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">
|
||||
{{ asGapItem(row).supplierName || '—' }}
|
||||
</p>
|
||||
<template v-if="asGapItem(row).supplierContractExempt">
|
||||
<p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="expandDraft[row.plan.id].supplierContracts"
|
||||
:customer-id="asGapItem(row).supplierId"
|
||||
:disabled="!asGapItem(row).supplierId"
|
||||
:required="false"
|
||||
/>
|
||||
<div class="pl-contract-side__actions">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="savingKey === `${row.plan.id}:supplier`"
|
||||
@click="handleSaveSide(asGapItem(row), 'supplier')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存供方合同
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pl-contract-side"
|
||||
:class="sideBoxClass(asGapItem(row), 'demander')"
|
||||
>
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>需方合同</h4>
|
||||
<el-tag
|
||||
:type="sideTagType(asGapItem(row), 'demander')"
|
||||
size="small"
|
||||
:effect="asGapItem(row).demanderContractExempt ? 'plain' : 'dark'"
|
||||
>
|
||||
{{ sideTagLabel(asGapItem(row), 'demander') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">
|
||||
{{ asGapItem(row).customerName || '—' }}
|
||||
</p>
|
||||
<template v-if="asGapItem(row).demanderContractExempt">
|
||||
<p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="expandDraft[row.plan.id].demanderContracts"
|
||||
:customer-id="asGapItem(row).customerId"
|
||||
:disabled="!asGapItem(row).customerId"
|
||||
:required="false"
|
||||
/>
|
||||
<div class="pl-contract-side__actions">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="savingKey === `${row.plan.id}:demander`"
|
||||
@click="handleSaveSide(asGapItem(row), 'demander')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存需方合同
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -278,28 +185,19 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-gap-contract-cell">
|
||||
<div class="pl-plan-contract-cell">
|
||||
<div
|
||||
v-for="(line, index) in getContractGapStopLines(row)"
|
||||
:key="`${line.side}-${line.no}-${index}`"
|
||||
class="pl-plan-contract-cell__row"
|
||||
>
|
||||
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
|
||||
<span
|
||||
class="pl-plan-contract-cell__no"
|
||||
:class="{ 'is-pending': line.pending }"
|
||||
>{{ line.no }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<PlPlanContractCell
|
||||
:lines="getContractGapStopLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showGapSupplementAction(row)"
|
||||
link
|
||||
type="warning"
|
||||
class="pl-gap-contract-cell__action"
|
||||
@click.stop="toggleExpand(asGapItem(row))"
|
||||
@click.stop="openSupplementDialog(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
{{ isExpanded(asGapItem(row).id) ? '收起' : '补充' }}
|
||||
补充
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -382,6 +280,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="asGapItem(row).products"
|
||||
field="actualQty"
|
||||
@@ -441,22 +340,108 @@
|
||||
:plan-id="viewPlanId"
|
||||
show-inbound-outbound-qty
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="supplementVisible"
|
||||
class="dialog-form-layout pl-contract-supplement-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
width="960px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:before-close="handleSupplementBeforeClose"
|
||||
>
|
||||
<template #header>
|
||||
<span class="pl-contract-supplement-dialog__title">
|
||||
补充合同 · {{ supplementStopKindLabel }} · {{ supplementPlanNo }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-if="supplementRow" class="pl-contract-supplement-body">
|
||||
<div class="pl-contract-supplement-summary">
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">计划日期</div>
|
||||
<div class="v">{{ asGapItem(supplementRow).planDate || '—' }}</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">跟进状态</div>
|
||||
<div class="v">
|
||||
<el-tag
|
||||
:type="getPlPlanFollowStatusTagType(asGapItem(supplementRow).followStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ getPlPlanFollowStatusLabel(asGapItem(supplementRow).followStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">本段客户</div>
|
||||
<div class="v">{{ getStopCustomerName(supplementRow) }}</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">地址 / 时间</div>
|
||||
<div class="v">
|
||||
{{ getStopAddress(supplementRow) }}
|
||||
<template v-if="getStopTime(supplementRow)">
|
||||
· {{ getStopTime(supplementRow) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-contract-side" :class="supplementSideBoxClass">
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>{{ supplementStopKindLabel }}合同</h4>
|
||||
<el-tag :type="supplementSideTagType" size="small" effect="dark">
|
||||
{{ supplementSideTagLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">{{ getStopCustomerName(supplementRow) }}</p>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="supplementDraft.contracts"
|
||||
:customer-id="supplementRow.stop.customerId"
|
||||
:disabled="!supplementRow.stop.customerId"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="requestCloseSupplement">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!supplementDraft.stopId || !supplementRow?.stop.customerId"
|
||||
:loading="supplementSaving"
|
||||
@click="handleSaveStopContracts"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存本段合同
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, EditPen, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { Check, Close, EditPen, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
planApi,
|
||||
type PlanContractGapItem,
|
||||
type PlanContractLink
|
||||
} from '@/api/pl'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import {
|
||||
@@ -481,24 +466,29 @@ import { useMenuStore } from '@/store/modules/menu'
|
||||
const PLAN_MERGE_COLUMN_PROPS = ['planDate', 'planType', 'followStatus'] as const
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<PlanContractGapItem[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const expandedRowKeys = ref<string[]>([])
|
||||
const expandDraft = reactive<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
supplierContracts: PlanContractLink[]
|
||||
demanderContracts: PlanContractLink[]
|
||||
baselineSupplier: string
|
||||
baselineDemander: string
|
||||
}
|
||||
>
|
||||
>({})
|
||||
const savingKey = ref('')
|
||||
const supplementVisible = ref(false)
|
||||
const supplementSaving = ref(false)
|
||||
const supplementRow = ref<PlPlanListDisplayRow | null>(null)
|
||||
const supplementDraft = reactive<{
|
||||
contracts: PlanContractLink[]
|
||||
baseline: string
|
||||
stopType: 'load' | 'delivery'
|
||||
stopId: string
|
||||
planId: string
|
||||
rowKey: string
|
||||
}>({
|
||||
contracts: [],
|
||||
baseline: '',
|
||||
stopType: 'load',
|
||||
stopId: '',
|
||||
planId: '',
|
||||
rowKey: ''
|
||||
})
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
@@ -523,8 +513,7 @@ const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
handleDisplayRowDblClick,
|
||||
findPlanFirstDisplayRow
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList: planListForDisplay,
|
||||
visiblePlanMergeProps,
|
||||
@@ -533,11 +522,28 @@ const {
|
||||
onView: (plan) => handleView(plan as unknown as PlanContractGapItem)
|
||||
})
|
||||
|
||||
const supplementStopKindLabel = computed(() =>
|
||||
supplementRow.value ? getStopKindLabel(supplementRow.value) : ''
|
||||
)
|
||||
const supplementPlanNo = computed(() =>
|
||||
supplementRow.value ? asGapItem(supplementRow.value).planNo || '—' : '—'
|
||||
)
|
||||
const supplementSideBoxClass = computed(() => {
|
||||
if (!supplementDraft.contracts.length) return 'is-need'
|
||||
return 'is-ok'
|
||||
})
|
||||
const supplementSideTagLabel = computed(() =>
|
||||
supplementDraft.contracts.length ? '已选' : '待补充'
|
||||
)
|
||||
const supplementSideTagType = computed(() =>
|
||||
supplementDraft.contracts.length ? 'success' : 'warning'
|
||||
)
|
||||
|
||||
function asGapItem(row: PlPlanListDisplayRow): PlanContractGapItem {
|
||||
return row.plan as unknown as PlanContractGapItem
|
||||
}
|
||||
|
||||
/** 段内合同缺口:装货行展示供方,送货行展示需方 */
|
||||
/** 段内合同:优先段上 contracts;无则回退旧计划级供/需合同 */
|
||||
function getContractGapStopLines(row: PlPlanListDisplayRow) {
|
||||
if ((row.stop.contracts?.length ?? 0) > 0) {
|
||||
return getStopContractLines(row)
|
||||
@@ -557,16 +563,19 @@ function getContractGapStopLines(row: PlPlanListDisplayRow) {
|
||||
)
|
||||
}
|
||||
|
||||
function showGapSupplementAction(row: PlPlanListDisplayRow): boolean {
|
||||
const gap = asGapItem(row)
|
||||
if (row.kind === 'load') {
|
||||
return !gap.supplierContractExempt && (gap.gapSides || []).includes('supplier')
|
||||
}
|
||||
return !gap.demanderContractExempt && (gap.gapSides || []).includes('demander')
|
||||
function isStopContractExempt(row: PlPlanListDisplayRow): boolean {
|
||||
return isPlInternalCustomerName(getStopCustomerName(row))
|
||||
}
|
||||
|
||||
function gapExpandRowKey(planId: string) {
|
||||
return `${planId}-load`
|
||||
function stopHasContracts(row: PlPlanListDisplayRow): boolean {
|
||||
return getContractGapStopLines(row).some((line) => !!line.contractId && !line.pending)
|
||||
}
|
||||
|
||||
/** 仅非内置客户、且本段缺合同时显示「补充」 */
|
||||
function showGapSupplementAction(row: PlPlanListDisplayRow): boolean {
|
||||
if (isStopContractExempt(row)) return false
|
||||
if (!row.stop.id) return false
|
||||
return !stopHasContracts(row)
|
||||
}
|
||||
|
||||
function getContractGapExportFilterParams() {
|
||||
@@ -591,17 +600,17 @@ const contractGapExportColumns = [
|
||||
'gapType'
|
||||
]
|
||||
|
||||
|
||||
function cloneLinks(links?: PlanContractLink[]): PlanContractLink[] {
|
||||
return (links || []).map(item => ({
|
||||
return (links || []).map((item) => ({
|
||||
contractId: item.contractId,
|
||||
contractNo: item.contractNo
|
||||
contractNo: item.contractNo,
|
||||
stopId: item.stopId
|
||||
}))
|
||||
}
|
||||
|
||||
function linksFingerprint(links: PlanContractLink[]) {
|
||||
return cloneLinks(links)
|
||||
.map(item => item.contractId)
|
||||
.map((item) => item.contractId)
|
||||
.sort()
|
||||
.join(',')
|
||||
}
|
||||
@@ -615,116 +624,75 @@ function getPlanTypeLabel(type?: string | null) {
|
||||
return type ? map[type] || type : '—'
|
||||
}
|
||||
|
||||
function sideBoxClass(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return 'is-exempt'
|
||||
const missing = (row.gapSides || []).includes(side)
|
||||
return missing ? 'is-need' : 'is-ok'
|
||||
function resolveStopDraftContracts(row: PlPlanListDisplayRow): PlanContractLink[] {
|
||||
if ((row.stop.contracts?.length ?? 0) > 0) {
|
||||
return cloneLinks(row.stop.contracts)
|
||||
}
|
||||
const gap = asGapItem(row)
|
||||
if (row.kind === 'load') return cloneLinks(gap.supplierContracts)
|
||||
return cloneLinks(gap.demanderContracts)
|
||||
}
|
||||
|
||||
function sideTagLabel(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return '可免'
|
||||
return (row.gapSides || []).includes(side) ? '待补充' : '已齐'
|
||||
function isSupplementDirty() {
|
||||
return linksFingerprint(supplementDraft.contracts) !== supplementDraft.baseline
|
||||
}
|
||||
|
||||
function sideTagType(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return 'info'
|
||||
return (row.gapSides || []).includes(side) ? 'warning' : 'success'
|
||||
function resetSupplementDraft() {
|
||||
supplementRow.value = null
|
||||
supplementDraft.contracts = []
|
||||
supplementDraft.baseline = ''
|
||||
supplementDraft.stopType = 'load'
|
||||
supplementDraft.stopId = ''
|
||||
supplementDraft.planId = ''
|
||||
supplementDraft.rowKey = ''
|
||||
}
|
||||
|
||||
function isExpanded(id: string) {
|
||||
return expandedRowKeys.value.includes(gapExpandRowKey(id))
|
||||
function openSupplementDialog(row: PlPlanListDisplayRow) {
|
||||
if (!row.stop.id) {
|
||||
ElMessage.warning('该段次缺少段 ID,无法补充合同')
|
||||
return
|
||||
}
|
||||
if (isStopContractExempt(row)) {
|
||||
ElMessage.info('内置客户无需补充合同')
|
||||
return
|
||||
}
|
||||
const contracts = resolveStopDraftContracts(row)
|
||||
supplementRow.value = row
|
||||
supplementDraft.contracts = contracts
|
||||
supplementDraft.baseline = linksFingerprint(contracts)
|
||||
supplementDraft.stopType = row.kind
|
||||
supplementDraft.stopId = String(row.stop.id || '').trim()
|
||||
supplementDraft.planId = row.plan.id
|
||||
supplementDraft.rowKey = row.rowKey
|
||||
supplementVisible.value = true
|
||||
}
|
||||
|
||||
function ensureDraft(row: PlanContractGapItem) {
|
||||
if (expandDraft[row.id]) return
|
||||
const supplierContracts = cloneLinks(row.supplierContracts)
|
||||
const demanderContracts = cloneLinks(row.demanderContracts)
|
||||
expandDraft[row.id] = {
|
||||
supplierContracts,
|
||||
demanderContracts,
|
||||
baselineSupplier: linksFingerprint(supplierContracts),
|
||||
baselineDemander: linksFingerprint(demanderContracts)
|
||||
async function confirmDiscardIfDirty(): Promise<boolean> {
|
||||
if (!isSupplementDirty()) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前有未保存的合同变更,确认关闭吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '关闭',
|
||||
cancelButtonText: '继续编辑'
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isDraftDirty(id: string) {
|
||||
const draft = expandDraft[id]
|
||||
if (!draft) return false
|
||||
return (
|
||||
linksFingerprint(draft.supplierContracts) !== draft.baselineSupplier ||
|
||||
linksFingerprint(draft.demanderContracts) !== draft.baselineDemander
|
||||
)
|
||||
async function requestCloseSupplement() {
|
||||
const ok = await confirmDiscardIfDirty()
|
||||
if (!ok) return
|
||||
supplementVisible.value = false
|
||||
resetSupplementDraft()
|
||||
}
|
||||
|
||||
async function collapseRow(id: string) {
|
||||
if (isDraftDirty(id)) {
|
||||
try {
|
||||
await ElMessageBox.confirm('当前有未保存的合同变更,确认收起吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '收起',
|
||||
cancelButtonText: '继续编辑'
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
delete expandDraft[id]
|
||||
expandedRowKeys.value = expandedRowKeys.value.filter((key) => key !== gapExpandRowKey(id))
|
||||
return true
|
||||
}
|
||||
|
||||
async function expandRow(row: PlanContractGapItem) {
|
||||
const currentKey = expandedRowKeys.value[0]
|
||||
const currentId = currentKey?.replace(/-load$/, '')
|
||||
if (currentId && currentId !== row.id) {
|
||||
const ok = await collapseRow(currentId)
|
||||
if (!ok) return
|
||||
}
|
||||
ensureDraft(row)
|
||||
expandedRowKeys.value = [gapExpandRowKey(row.id)]
|
||||
}
|
||||
|
||||
async function toggleExpand(row: PlanContractGapItem) {
|
||||
if (isExpanded(row.id)) {
|
||||
await collapseRow(row.id)
|
||||
} else {
|
||||
await expandRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExpandChange(
|
||||
displayRow: PlPlanListDisplayRow,
|
||||
expandedRows: PlPlanListDisplayRow[]
|
||||
) {
|
||||
const gap = asGapItem(displayRow)
|
||||
const willExpand = expandedRows.some((item) => item.rowKey === displayRow.rowKey)
|
||||
if (willExpand) {
|
||||
const currentKey = expandedRowKeys.value[0]
|
||||
const currentId = currentKey?.replace(/-load$/, '')
|
||||
if (currentId && currentId !== gap.id) {
|
||||
const ok = await collapseRow(currentId)
|
||||
if (!ok) {
|
||||
nextTick(() => {
|
||||
const firstRow = findPlanFirstDisplayRow(gap.id)
|
||||
if (firstRow) tableRef.value?.toggleRowExpansion?.(firstRow, false)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
ensureDraft(gap)
|
||||
expandedRowKeys.value = [gapExpandRowKey(gap.id)]
|
||||
} else if (expandedRowKeys.value.includes(gapExpandRowKey(gap.id))) {
|
||||
const ok = await collapseRow(gap.id)
|
||||
if (!ok) {
|
||||
nextTick(() => {
|
||||
const firstRow = findPlanFirstDisplayRow(gap.id)
|
||||
if (firstRow) tableRef.value?.toggleRowExpansion?.(firstRow, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
async function handleSupplementBeforeClose(done: () => void) {
|
||||
const ok = await confirmDiscardIfDirty()
|
||||
if (!ok) return
|
||||
resetSupplementDraft()
|
||||
done()
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -744,15 +712,6 @@ async function fetchData() {
|
||||
const data = (res as any).data ?? res
|
||||
tableData.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
// 收起已不在列表中的展开行
|
||||
const ids = new Set(tableData.value.map((item) => item.id))
|
||||
expandedRowKeys.value = expandedRowKeys.value.filter((key) => {
|
||||
const planId = key.replace(/-load$/, '')
|
||||
return ids.has(planId)
|
||||
})
|
||||
Object.keys(expandDraft).forEach(id => {
|
||||
if (!ids.has(id)) delete expandDraft[id]
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -804,37 +763,37 @@ function handleView(row: PlanContractGapItem) {
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSaveSide(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const draft = expandDraft[row.id]
|
||||
if (!draft) return
|
||||
const contracts =
|
||||
side === 'supplier' ? draft.supplierContracts : draft.demanderContracts
|
||||
savingKey.value = `${row.id}:${side}`
|
||||
async function handleSaveStopContracts() {
|
||||
if (!supplementDraft.stopId) {
|
||||
ElMessage.warning('该段次缺少段 ID,无法保存')
|
||||
return
|
||||
}
|
||||
if (!supplementRow.value?.stop.customerId) {
|
||||
ElMessage.warning('请先维护本段客户后再选择合同')
|
||||
return
|
||||
}
|
||||
if (!supplementDraft.contracts.length) {
|
||||
ElMessage.warning('请先选择至少 1 份合同')
|
||||
return
|
||||
}
|
||||
supplementSaving.value = true
|
||||
try {
|
||||
const res = await planApi.updateSideContracts(row.id, side, contracts)
|
||||
const updated = ((res as any).data ?? res) as PlanContractGapItem
|
||||
ElMessage.success(side === 'supplier' ? '供方合同已保存' : '需方合同已保存')
|
||||
await planApi.updateStopContracts(supplementDraft.planId, {
|
||||
stopType: supplementDraft.stopType,
|
||||
stopId: supplementDraft.stopId,
|
||||
contracts: supplementDraft.contracts
|
||||
})
|
||||
ElMessage.success('本段合同已保存')
|
||||
|
||||
if (!(updated.gapSides || []).length) {
|
||||
tableData.value = tableData.value.filter(item => item.id !== row.id)
|
||||
pagination.total = Math.max(0, pagination.total - 1)
|
||||
delete expandDraft[row.id]
|
||||
expandedRowKeys.value = []
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
return
|
||||
}
|
||||
// 先对齐 baseline,避免关闭时被 before-close 当成未保存变更
|
||||
supplementDraft.baseline = linksFingerprint(supplementDraft.contracts)
|
||||
supplementVisible.value = false
|
||||
resetSupplementDraft()
|
||||
|
||||
const idx = tableData.value.findIndex(item => item.id === row.id)
|
||||
if (idx >= 0) {
|
||||
tableData.value[idx] = updated
|
||||
}
|
||||
draft.supplierContracts = cloneLinks(updated.supplierContracts)
|
||||
draft.demanderContracts = cloneLinks(updated.demanderContracts)
|
||||
draft.baselineSupplier = linksFingerprint(draft.supplierContracts)
|
||||
draft.baselineDemander = linksFingerprint(draft.demanderContracts)
|
||||
await fetchData()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
} finally {
|
||||
savingKey.value = ''
|
||||
supplementSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,21 +832,48 @@ onMounted(() => {
|
||||
border: 1px solid rgba(217, 119, 6, 0.2);
|
||||
}
|
||||
|
||||
.pl-contract-expand {
|
||||
padding: 12px 16px 16px;
|
||||
background: #f8fffb;
|
||||
border-top: 1px dashed #a7f3d0;
|
||||
.pl-contract-supplement-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.pl-contract-expand__grid {
|
||||
.pl-contract-supplement-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pl-contract-supplement-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.pl-contract-expand__grid {
|
||||
grid-template-columns: 1fr;
|
||||
@media (max-width: 700px) {
|
||||
.pl-contract-supplement-summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-contract-supplement-kv {
|
||||
min-width: 0;
|
||||
|
||||
.k {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.v {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #1e293b);
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,7 +881,7 @@ onMounted(() => {
|
||||
background: #fff;
|
||||
border: 1px solid var(--el-border-color, #d9e8e2);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.pl-contract-side.is-need {
|
||||
@@ -908,11 +894,6 @@ onMounted(() => {
|
||||
background: #f6fffb;
|
||||
}
|
||||
|
||||
.pl-contract-side.is-exempt {
|
||||
border-color: #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-contract-side__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -922,43 +903,16 @@ onMounted(() => {
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-contract-side__customer {
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pl-contract-side__exempt {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary, #64748b);
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-contract-side__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.pl-contract-expand-col) {
|
||||
.cell {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.el-table__expand-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-gap-contract-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -483,6 +483,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -515,20 +516,10 @@
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-plan-contract-cell">
|
||||
<div
|
||||
v-for="(line, index) in getStopContractLines(row)"
|
||||
:key="`${line.side}-${line.no}-${index}`"
|
||||
class="pl-plan-contract-cell__row"
|
||||
>
|
||||
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
|
||||
<span
|
||||
class="pl-plan-contract-cell__no"
|
||||
:class="{ 'is-pending': line.pending }"
|
||||
>{{ line.no }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<PlPlanContractCell
|
||||
:lines="getStopContractLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -691,6 +682,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<DbPoundSlipQrcodeDialog
|
||||
v-model:visible="qrcodeVisible"
|
||||
:inventory-id="selectedInventoryId"
|
||||
@@ -797,6 +790,7 @@
|
||||
import PlDeliverySlipPrintDialog from '@/components/business/pl-delivery-slip-print-dialog/index.vue'
|
||||
import PlSubpackSlipPrintDialog from '@/components/business/pl-subpack-slip-print-dialog/index.vue'
|
||||
import PlPlanDeliveryCustomerSelectDialog from '@/components/business/pl-plan-delivery-customer-select-dialog/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -804,6 +798,7 @@
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
@@ -831,7 +826,7 @@
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
resolveStopCertProducts,
|
||||
sumStopActualQty,
|
||||
sumStopArchiveActualQty,
|
||||
sumStopProductsField,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
@@ -860,6 +855,7 @@
|
||||
] as const
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
const loading = ref(false)
|
||||
const voidLoading = ref(false)
|
||||
@@ -1023,7 +1019,7 @@
|
||||
if (prop === 'productPlanQty') return sumStopProductsField(stopProducts, 'planQty')
|
||||
if (prop === 'productInboundQty') return sumStopProductsField(stopProducts, 'inboundQty')
|
||||
if (prop === 'productOutboundQty') return sumStopProductsField(stopProducts, 'outboundQty')
|
||||
if (prop === 'productActualQty') return sumStopActualQty(plan, stopProducts)
|
||||
if (prop === 'productActualQty') return sumStopArchiveActualQty(stopProducts, row.kind)
|
||||
if (prop === 'productName') {
|
||||
return stopProducts
|
||||
.map((p) => p.productName || '')
|
||||
|
||||
Reference in new issue
Block a user