Files
shanxia2.0/web-admin/src/views/pl/plan-contract/index.vue
T
wuzilongandCursor 8dc1820622 feat(web-admin): add deliveryTimes column on plan contract-gap list
Filter and display deduped delivery dates beside planDate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 21:31:20 +08:00

1123 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- eslint-disable vue/no-v-model-argument -->
<!-- 运输计划合同完善:仅缺合同计划,按段次客户弹窗补充合同 -->
<template>
<div class="page-container">
<section class="panel">
<div class="list-toolbar">
<div class="list-toolbar-row pl-plan-list-toolbar-row">
<ListToolbarDateShortcuts
variant="pills"
:active="activeDateShortcut"
aria-label="计划日期快捷筛选"
@change="handleDateShortcut"
/>
<ListQuickSearchInput
v-model="quickSearch"
placeholder="计划编号/客户名称"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
v-if="hasActiveFilters"
class="btn-ghost"
@click="clearAllFilters"
>
<el-icon><Filter /></el-icon>清空列过滤
</el-button>
<ListExportDropdown
:export-fn="planApi.exportContractGaps"
:get-filter-params="getContractGapExportFilterParams"
:has-selection="false"
:selected-ids="[]"
:visible-columns="contractGapExportColumns"
filename="计划合同完善.xlsx"
/>
<el-button class="btn-ghost" :loading="loading" @click="fetchData">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<span class="pl-plan-contract-stat">待完善 {{ pagination.total }} 条</span>
</div>
</div>
<div class="list-body list-wrap">
<div ref="planContractTableWrapRef" class="table-scroll bx-list-table-wrap">
<el-table
ref="planContractTableRef"
class="list-table pl-plan-merge-list-table"
:height="tableHeight"
:data="displayRows"
v-loading="loading"
row-key="rowKey"
:span-method="planListSpanMethod"
:row-class-name="getPlPlanListRowClassName"
@row-dblclick="handleDisplayRowDblClick"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
:index="getDisplayRowIndex"
/>
<el-table-column prop="planNo" label="计划编号" min-width="150" show-overflow-tooltip>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">计划编号</span>
<ColumnHeaderFilter
prop="planNo"
label="计划编号"
type="text"
:is-active="isFilterActive('planNo')"
:model-value="filters.planNo || null"
@update:model-value="(v) => setFilterState('planNo', v)"
/>
</div>
</template>
<template #default="{ row }">
<el-link type="primary" @click="handleView(asGapItem(row))">{{
row.plan.planNo
}}</el-link>
</template>
</el-table-column>
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center">
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">计划日期</span>
<ColumnHeaderFilter
prop="planDate"
label="计划日期"
type="datetime"
:is-active="isFilterActive('planDate')"
:model-value="filters.planDate || null"
@update:model-value="(v) => setFilterState('planDate', v)"
/>
</div>
</template>
<template #default="{ row }">{{ asGapItem(row).planDate || '—' }}</template>
</el-table-column>
<el-table-column prop="deliveryTimes" label="送货时间" min-width="130" align="center">
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">送货时间</span>
<ColumnHeaderFilter
prop="deliveryTimes"
label="送货时间"
type="datetime"
:is-active="isFilterActive('deliveryTimes')"
:model-value="filters.deliveryTimes || null"
@update:model-value="(v) => setFilterState('deliveryTimes', v)"
/>
</div>
</template>
<template #default="{ row }">
<div
v-if="(asGapItem(row).deliveryTimes || []).length"
class="pl-delivery-times-cell"
>
<div
v-for="(t, i) in asGapItem(row).deliveryTimes"
:key="`${asGapItem(row).id}-dt-${i}`"
>
{{ t }}
</div>
</div>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">单据类型</span>
<ColumnHeaderFilter
prop="planType"
label="单据类型"
type="enum"
:is-active="isFilterActive('planType')"
:model-value="filters.planType || null"
:options="enumOptionsFor('planType')"
@update:model-value="(v) => setFilterState('planType', v)"
/>
</div>
</template>
<template #default="{ row }">
{{ getPlanTypeLabel(asGapItem(row).planType) }}
</template>
</el-table-column>
<el-table-column prop="followStatus" label="跟进状态" min-width="110" align="center">
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">跟进状态</span>
<ColumnHeaderFilter
prop="followStatus"
label="跟进状态"
type="enum"
:is-active="isFilterActive('followStatus')"
:model-value="filters.followStatus || null"
:options="enumOptionsFor('followStatus')"
@update:model-value="(v) => setFilterState('followStatus', v)"
/>
</div>
</template>
<template #default="{ row }">
<el-tag
:type="getPlPlanFollowStatusTagType(asGapItem(row).followStatus)"
size="small"
effect="dark"
>
{{ getPlPlanFollowStatusLabel(asGapItem(row).followStatus) }}
</el-tag>
</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"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label is-left">客户</span>
<ColumnHeaderFilter
prop="stopCustomer"
label="客户"
type="text"
:is-active="isFilterActive('stopCustomer')"
:model-value="filters.stopCustomer || null"
@update:model-value="(v) => setFilterState('stopCustomer', v)"
/>
</div>
</template>
<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"
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
prop="gapType"
label="合同缺口"
min-width="260"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label is-left">合同缺口</span>
<ColumnHeaderFilter
prop="gapType"
label="合同缺口"
type="enum"
:is-active="isFilterActive('gapType')"
:model-value="filters.gapType || null"
:options="enumOptionsFor('gapType')"
@update:model-value="(v) => setFilterState('gapType', v)"
/>
</div>
</template>
<template #default="{ row }">
<div class="pl-gap-contract-cell">
<PlPlanContractCell
:lines="getContractGapStopLines(row)"
@open="openContractDetail"
/>
<el-button
v-if="showGapSupplementAction(row)"
link
type="warning"
class="pl-gap-contract-cell__action"
@click.stop="openSupplementDialog(row)"
>
<el-icon><EditPen /></el-icon>
补充
</el-button>
</div>
</template>
</el-table-column>
<el-table-column
label="产品名称"
min-width="160"
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="asGapItem(row).products"
field="name"
/>
</template>
</el-table-column>
<el-table-column
label="型号/规格"
min-width="120"
align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="asGapItem(row).products"
field="model"
/>
</template>
</el-table-column>
<el-table-column
label="包装规格"
min-width="120"
align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="asGapItem(row).products"
field="packageSpec"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="80" align="center" class-name="pl-plan-stop-col">
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="asGapItem(row).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="asGapItem(row).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="asGapItem(row).products"
field="actualQty"
/>
</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="asGapItem(row).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="asGapItem(row).products"
field="outboundQty"
/>
</template>
</el-table-column>
</el-table>
</div>
<div ref="planContractBelowRef">
<div class="pagination list-pager-bar">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 100, 500]"
layout="total, sizes, prev, pager, next, jumper"
background
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</section>
<PlPlanViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
: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="min(2000px, calc(100vw - 48px))"
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,
Close,
EditPen,
Refresh,
Filter
} from '@element-plus/icons-vue'
import {
planApi,
type PlanContractGapItem,
type PlanContractLink
} from '@/api/pl'
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import { useColumnSort } from '@/composables/useColumnSort'
import { plPlanContractColumnFilterDefs } from './columnFilterDefs'
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 { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import {
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType
} from '@/utils/pl-display'
import { planContractSideLines } from '@/utils/pl-plan-contract-display'
import {
getPlPlanListRowClassName,
getStopAddress,
getStopContractLines,
getStopCustomerName,
getStopKindLabel,
getStopProducts,
getStopTime,
type PlPlanListDisplayRow
} 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 PLAN_MERGE_COLUMN_PROPS = ['planDate', 'deliveryTimes', 'planType', 'followStatus'] as const
const menuStore = useMenuStore()
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
const loading = ref(false)
const tableData = ref<PlanContractGapItem[]>([])
const dateRange = ref<string[]>([])
const quickSearch = 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('')
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
const {
filters,
hasActiveFilters,
setFilterState,
clearAllFilters,
isFilterActive,
toQueryParams,
resetToInitial
} = useServerColumnFilters(plPlanContractColumnFilterDefs, {
initialFilters: {},
onChange: () => handleSearch()
})
const {
sortField,
sortOrder,
setSort,
clearSort,
isSortActive,
toQueryParams: toSortQueryParams
} = useColumnSort({
onChange: () => handleSearch()
})
provide('crmColumnSort', {
setSort,
clearSort,
isSortActive,
sortField,
sortOrder
})
function enumOptionsFor(prop: string) {
const def = plPlanContractColumnFilterDefs.find((d) => d.prop === prop)
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
}
const planContractTableWrapRef = ref<HTMLElement | null>(null)
const planContractBelowRef = ref<HTMLElement | null>(null)
const planContractTableRef = ref<any>(null)
const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
tableEl: planContractTableWrapRef,
pagerEl: planContractBelowRef,
tableRef: planContractTableRef
})
const planListForDisplay = computed(() => tableData.value as unknown as import('@/api/pl').Plan[])
const visiblePlanMergeProps = computed(() => [...PLAN_MERGE_COLUMN_PROPS])
const {
displayRows,
planListSpanMethod,
getDisplayRowIndex,
handleDisplayRowDblClick
} = usePlPlanMergeListTable({
planList: planListForDisplay,
visiblePlanMergeProps,
selectedRows: ref([]),
pagination,
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)
}
const gap = asGapItem(row)
if (row.kind === 'load') {
return planContractSideLines(
'供',
gap.supplierContracts,
gap.supplierName || row.stop.customerName
)
}
return planContractSideLines(
'需',
gap.demanderContracts,
row.stop.customerName || gap.customerName
)
}
function isStopContractExempt(row: PlPlanListDisplayRow): boolean {
return isPlInternalCustomerName(getStopCustomerName(row))
}
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 buildListQueryParams(): Record<string, any> {
const colParams = { ...toQueryParams(), ...toSortQueryParams() }
const keyword = quickSearch.value.trim()
const params: Record<string, any> = { ...colParams }
if (keyword) params.q = keyword
if (colParams.followStatusIn) {
params.followStatus = colParams.followStatusIn
delete params.followStatusIn
}
if (colParams.gapTypeIn) {
params.gapType = colParams.gapTypeIn
delete params.gapTypeIn
}
if (!params.planDateFrom && !params.planDateTo) {
if (dateRange.value?.[0]) params.planDateFrom = dateRange.value[0]
if (dateRange.value?.[1]) params.planDateTo = dateRange.value[1]
}
return params
}
function getContractGapExportFilterParams() {
return { ...buildListQueryParams() }
}
const contractGapExportColumns = [
'planNo',
'planDate',
'deliveryTimes',
'planType',
'followStatus',
'supplierName',
'customerName',
'gapType'
]
function cloneLinks(links?: PlanContractLink[]): PlanContractLink[] {
return (links || []).map((item) => ({
contractId: item.contractId,
contractNo: item.contractNo,
stopId: item.stopId
}))
}
function linksFingerprint(links: PlanContractLink[]) {
return cloneLinks(links)
.map((item) => item.contractId)
.sort()
.join(',')
}
function getPlanTypeLabel(type?: string | null) {
const map: Record<string, string> = {
direct: '直销单',
inbound: '入库单',
outbound: '出库单'
}
return type ? map[type] || type : '—'
}
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 isSupplementDirty() {
return linksFingerprint(supplementDraft.contracts) !== supplementDraft.baseline
}
function resetSupplementDraft() {
supplementRow.value = null
supplementDraft.contracts = []
supplementDraft.baseline = ''
supplementDraft.stopType = 'load'
supplementDraft.stopId = ''
supplementDraft.planId = ''
supplementDraft.rowKey = ''
}
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
}
async function confirmDiscardIfDirty(): Promise<boolean> {
if (!isSupplementDirty()) return true
try {
await ElMessageBox.confirm('当前有未保存的合同变更,确认关闭吗?', '提示', {
type: 'warning',
confirmButtonText: '关闭',
cancelButtonText: '继续编辑'
})
return true
} catch {
return false
}
}
async function requestCloseSupplement() {
const ok = await confirmDiscardIfDirty()
if (!ok) return
supplementVisible.value = false
resetSupplementDraft()
}
async function handleSupplementBeforeClose(done: () => void) {
const ok = await confirmDiscardIfDirty()
if (!ok) return
resetSupplementDraft()
done()
}
async function fetchData() {
loading.value = true
try {
const res = await planApi.listContractGaps({
...buildListQueryParams(),
page: pagination.page,
pageSize: pagination.pageSize
})
const data = res.data ?? res
tableData.value = data.list || []
pagination.total = data.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchData()
}
function handleReset() {
quickSearch.value = ''
clearAllFilters()
resetDateShortcut()
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearch.value = ''
handleQuickSearch()
}
const {
activeDateShortcut,
handleDateShortcut: handleDateShortcutInner,
resetDateShortcut
} = useListDateShortcut(dateRange, handleSearch)
function handleDateShortcut(type: 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week') {
if (filters.planDate) {
delete filters.planDate
}
handleDateShortcutInner(type)
}
function handlePageChange() {
fetchData()
}
function handleSizeChange() {
pagination.page = 1
fetchData()
}
function handleView(row: PlanContractGapItem) {
viewPlanId.value = row.id
viewVisible.value = true
}
async function handleSaveStopContracts() {
if (!supplementDraft.stopId) {
ElMessage.warning('该段次缺少段 ID,无法保存')
return
}
if (!supplementRow.value?.stop.customerId) {
ElMessage.warning('请先维护本段客户后再选择合同')
return
}
if (supplementDraft.contracts.length === 0) {
ElMessage.warning('请先选择合同')
return
}
if (supplementDraft.contracts.length > 1) {
ElMessage.warning('只能保留一份合同,请移除多余合同后再保存')
return
}
supplementSaving.value = true
try {
await planApi.updateStopContracts(supplementDraft.planId, {
stopType: supplementDraft.stopType,
stopId: supplementDraft.stopId,
contracts: supplementDraft.contracts
})
ElMessage.success('本段合同已保存')
// 先对齐 baseline,避免关闭时被 before-close 当成未保存变更
supplementDraft.baseline = linksFingerprint(supplementDraft.contracts)
supplementVisible.value = false
resetSupplementDraft()
await fetchData()
await refreshMenuBadges(menuStore.menuList)
} finally {
supplementSaving.value = false
}
}
onMounted(() => {
fetchData()
nextTick(() => recalcTableHeight())
})
</script>
<style scoped lang="scss">
@use '@styles/business/pl-plan-merge-list-table.scss';
.pl-plan-list-toolbar-row {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
> .list-toolbar-date-shortcuts,
> .quick-search-input,
> .quick-search-group,
> .el-button,
> .pl-plan-contract-stat {
flex-shrink: 0;
}
}
.pl-delivery-times-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
line-height: 1.35;
}
.pl-plan-contract-stat {
display: inline-flex;
align-items: center;
margin-left: auto;
padding: 4px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
color: #d97706;
background: #fffbeb;
border: 1px solid rgba(217, 119, 6, 0.2);
}
.pl-contract-supplement-dialog__title {
font-size: 16px;
font-weight: 600;
color: var(--text, #1e293b);
}
.pl-contract-supplement-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.pl-contract-supplement-summary {
display: grid;
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: 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;
}
}
.pl-contract-side {
background: #fff;
border: 1px solid var(--el-border-color, #d9e8e2);
border-radius: 8px;
padding: 14px;
}
.pl-contract-side.is-need {
border-color: rgba(217, 119, 6, 0.35);
background: #fffdf8;
}
.pl-contract-side.is-ok {
border-color: #a7f3d0;
background: #f6fffb;
}
.pl-contract-side__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
}
.pl-contract-side__customer {
margin: 0 0 12px;
font-size: 13px;
}
.pl-gap-contract-cell {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
width: 100%;
}
.pl-gap-contract-cell__action {
flex-shrink: 0;
margin-top: 0;
}
.pl-plan-contract-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
min-width: 0;
flex: 1;
text-align: left;
}
.pl-plan-contract-cell__row {
display: flex;
align-items: baseline;
gap: 6px;
width: 100%;
min-width: 0;
font-size: 13px;
line-height: 1.5;
color: var(--text, #1e293b);
}
.pl-plan-contract-cell__side {
flex-shrink: 0;
font-weight: 600;
color: var(--mint-text-muted, #64748b);
}
.pl-plan-contract-cell__no {
min-width: 0;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
&.is-pending {
font-weight: 600;
color: #d97706;
}
}
</style>