|
@@ -0,0 +1,703 @@
|
|
|
|
|
+<!-- 运输计划合同完善:仅缺合同计划,行内展开分侧保存 -->
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="page-container">
|
|
|
|
|
+ <section class="panel">
|
|
|
|
|
+ <div class="panel-top pl-plan-contract-panel-top">
|
|
|
|
|
+ <h2 class="title-bar-heading">运输计划合同完善</h2>
|
|
|
|
|
+ <span class="pl-plan-contract-stat">待完善 {{ pagination.total }} 条</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="filter-block">
|
|
|
|
|
+ <el-form :model="searchForm" class="filter-form" @submit.prevent>
|
|
|
|
|
+ <div class="filter-grid">
|
|
|
|
|
+ <el-form-item label="计划编号">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="searchForm.planNo"
|
|
|
|
|
+ placeholder="搜索计划编号"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <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-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="供方 / 需方">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="searchForm.customerKeyword"
|
|
|
|
|
+ placeholder="客户名称"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="计划日期">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="dateRange"
|
|
|
|
|
+ type="daterange"
|
|
|
|
|
+ range-separator="-"
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="filter-actions">
|
|
|
|
|
+ <div class="filter-actions-start" />
|
|
|
|
|
+ <div class="filter-actions-end">
|
|
|
|
|
+ <el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
|
|
|
|
+ <el-icon><RefreshLeft /></el-icon>重置
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
|
|
|
|
+ <el-icon><Search /></el-icon>搜索
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <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 class="btn-ghost" :loading="loading" @click="fetchData">
|
|
|
|
|
+ <el-icon><Refresh /></el-icon>刷新
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="list-body list-wrap">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ ref="tableRef"
|
|
|
|
|
+ class="list-table"
|
|
|
|
|
+ :data="tableData"
|
|
|
|
|
+ v-loading="loading"
|
|
|
|
|
+ stripe
|
|
|
|
|
+ row-key="id"
|
|
|
|
|
+ :expand-row-keys="expandedRowKeys"
|
|
|
|
|
+ @expand-change="handleExpandChange"
|
|
|
|
|
+ @row-dblclick="handleView"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column type="expand" min-width="48">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <div class="pl-contract-expand" v-if="expandDraft[row.id]">
|
|
|
|
|
+ <div class="pl-contract-expand__grid">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="pl-contract-side"
|
|
|
|
|
+ :class="sideBoxClass(row, 'supplier')"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="pl-contract-side__head">
|
|
|
|
|
+ <h4>供方合同</h4>
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="sideTagType(row, 'supplier')"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :effect="row.supplierContractExempt ? 'plain' : 'dark'"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ sideTagLabel(row, 'supplier') }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p class="pl-contract-side__customer">
|
|
|
|
|
+ {{ row.supplierName || '—' }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <template v-if="row.supplierContractExempt">
|
|
|
|
|
+ <p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <PlPlanContractMultiSelect
|
|
|
|
|
+ v-model="expandDraft[row.id].supplierContracts"
|
|
|
|
|
+ :customer-id="row.supplierId"
|
|
|
|
|
+ :disabled="!row.supplierId"
|
|
|
|
|
+ :required="false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="pl-contract-side__actions">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ class="btn-main"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :loading="savingKey === `${row.id}:supplier`"
|
|
|
|
|
+ @click="handleSaveSide(row, 'supplier')"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><Check /></el-icon>保存供方合同
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="pl-contract-side"
|
|
|
|
|
+ :class="sideBoxClass(row, 'demander')"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="pl-contract-side__head">
|
|
|
|
|
+ <h4>需方合同</h4>
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="sideTagType(row, 'demander')"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :effect="row.demanderContractExempt ? 'plain' : 'dark'"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ sideTagLabel(row, 'demander') }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p class="pl-contract-side__customer">
|
|
|
|
|
+ {{ row.customerName || '—' }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <template v-if="row.demanderContractExempt">
|
|
|
|
|
+ <p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <PlPlanContractMultiSelect
|
|
|
|
|
+ v-model="expandDraft[row.id].demanderContracts"
|
|
|
|
|
+ :customer-id="row.customerId"
|
|
|
|
|
+ :disabled="!row.customerId"
|
|
|
|
|
+ :required="false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="pl-contract-side__actions">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ class="btn-main"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :loading="savingKey === `${row.id}:demander`"
|
|
|
|
|
+ @click="handleSaveSide(row, 'demander')"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><Check /></el-icon>保存需方合同
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ type="index"
|
|
|
|
|
+ label="序号"
|
|
|
|
|
+ min-width="68"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ :index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column prop="planNo" label="计划编号" min-width="150" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-link type="primary" @click="handleView(row)">{{ row.planNo }}</el-link>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="planDate" label="计划日期" min-width="120" align="center" />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="supplierName"
|
|
|
|
|
+ label="供方"
|
|
|
|
|
+ min-width="200"
|
|
|
|
|
+ align="left"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <div class="party-cell">
|
|
|
|
|
+ <span>{{ row.supplierName || '—' }}</span>
|
|
|
|
|
+ <span class="party-cell__sub">{{ partySubText(row, 'supplier') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="customerName"
|
|
|
|
|
+ label="需方"
|
|
|
|
|
+ min-width="200"
|
|
|
|
|
+ align="left"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <div class="party-cell">
|
|
|
|
|
+ <span>{{ row.customerName || '—' }}</span>
|
|
|
|
|
+ <span class="party-cell__sub">{{ partySubText(row, 'demander') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="合同缺口" min-width="130" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="gapTag(row).type" size="small" effect="dark">
|
|
|
|
|
+ {{ gapTag(row).text }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="followStatus" label="跟进状态" min-width="110" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="getPlPlanFollowStatusTagType(row.followStatus)"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ effect="dark"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ getPlPlanFollowStatusLabel(row.followStatus) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" fixed="right" min-width="200" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ link
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ @click="toggleExpand(row)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><EditPen /></el-icon>
|
|
|
|
|
+ {{ isExpanded(row.id) ? '收起' : '展开补充' }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="primary" @click="handleView(row)">
|
|
|
|
|
+ <el-icon><View /></el-icon>查看计划
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <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, 50, 100]"
|
|
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
+ background
|
|
|
|
|
+ @current-change="handlePageChange"
|
|
|
|
|
+ @size-change="handleSizeChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <PlPlanViewDialog
|
|
|
|
|
+ ref="viewDialogRef"
|
|
|
|
|
+ v-model:visible="viewVisible"
|
|
|
|
|
+ :plan-id="viewPlanId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { Check, EditPen, Refresh, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ planApi,
|
|
|
|
|
+ type PlanContractGapItem,
|
|
|
|
|
+ type PlanContractLink
|
|
|
|
|
+} from '@/api/pl'
|
|
|
|
|
+import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/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 ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
|
|
|
|
+import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
|
|
|
|
+import {
|
|
|
|
|
+ getPlPlanFollowStatusLabel,
|
|
|
|
|
+ getPlPlanFollowStatusTagType
|
|
|
|
|
+} from '@/utils/pl-display'
|
|
|
|
|
+import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
|
|
|
|
+import { useMenuStore } from '@/store/modules/menu'
|
|
|
|
|
+
|
|
|
|
|
+const menuStore = useMenuStore()
|
|
|
|
|
+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 viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
|
|
|
|
+const viewVisible = ref(false)
|
|
|
|
|
+const viewPlanId = ref('')
|
|
|
|
|
+
|
|
|
|
|
+const searchForm = reactive({
|
|
|
|
|
+ planNo: '',
|
|
|
|
|
+ gapType: '' as '' | 'supplier' | 'demander' | 'both',
|
|
|
|
|
+ customerKeyword: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const pagination = reactive({
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 20,
|
|
|
|
|
+ total: 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function cloneLinks(links?: PlanContractLink[]): PlanContractLink[] {
|
|
|
|
|
+ return (links || []).map(item => ({
|
|
|
|
|
+ contractId: item.contractId,
|
|
|
|
|
+ contractNo: item.contractNo
|
|
|
|
|
+ }))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function linksFingerprint(links: PlanContractLink[]) {
|
|
|
|
|
+ return cloneLinks(links)
|
|
|
|
|
+ .map(item => item.contractId)
|
|
|
|
|
+ .sort()
|
|
|
|
|
+ .join(',')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function gapTag(row: PlanContractGapItem) {
|
|
|
|
|
+ const sides = row.gapSides || []
|
|
|
|
|
+ if (sides.includes('supplier') && sides.includes('demander')) {
|
|
|
|
|
+ return { text: '两侧均缺', type: 'danger' as const }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sides.includes('supplier')) {
|
|
|
|
|
+ return { text: '缺供方合同', type: 'warning' as const }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sides.includes('demander')) {
|
|
|
|
|
+ return { text: '缺需方合同', type: 'warning' as const }
|
|
|
|
|
+ }
|
|
|
|
|
+ return { text: '—', type: 'info' as const }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function partySubText(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
|
|
|
|
+ if (side === 'supplier') {
|
|
|
|
|
+ if (row.supplierContractExempt) return '内置 · 合同可免'
|
|
|
|
|
+ const n = row.supplierContracts?.length || 0
|
|
|
|
|
+ return n ? `已关联 ${n} 份` : '未关联'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (row.demanderContractExempt) return '内置 · 合同可免'
|
|
|
|
|
+ const n = row.demanderContracts?.length || 0
|
|
|
|
|
+ return n ? `已关联 ${n} 份` : '未关联'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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 sideTagLabel(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
|
|
|
|
+ const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
|
|
|
|
+ if (exempt) return '可免'
|
|
|
|
|
+ return (row.gapSides || []).includes(side) ? '待补充' : '已齐'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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 isExpanded(id: string) {
|
|
|
|
|
+ return expandedRowKeys.value.includes(id)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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 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 !== id)
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function expandRow(row: PlanContractGapItem) {
|
|
|
|
|
+ const current = expandedRowKeys.value[0]
|
|
|
|
|
+ if (current && current !== row.id) {
|
|
|
|
|
+ const ok = await collapseRow(current)
|
|
|
|
|
+ if (!ok) return
|
|
|
|
|
+ }
|
|
|
|
|
+ ensureDraft(row)
|
|
|
|
|
+ expandedRowKeys.value = [row.id]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleExpand(row: PlanContractGapItem) {
|
|
|
|
|
+ if (isExpanded(row.id)) {
|
|
|
|
|
+ await collapseRow(row.id)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await expandRow(row)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleExpandChange(row: PlanContractGapItem, expandedRows: PlanContractGapItem[]) {
|
|
|
|
|
+ const willExpand = expandedRows.some(item => item.id === row.id)
|
|
|
|
|
+ if (willExpand) {
|
|
|
|
|
+ const current = expandedRowKeys.value[0]
|
|
|
|
|
+ if (current && current !== row.id) {
|
|
|
|
|
+ const ok = await collapseRow(current)
|
|
|
|
|
+ if (!ok) {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ tableRef.value?.toggleRowExpansion?.(row, false)
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ensureDraft(row)
|
|
|
|
|
+ expandedRowKeys.value = [row.id]
|
|
|
|
|
+ } else if (expandedRowKeys.value.includes(row.id)) {
|
|
|
|
|
+ const ok = await collapseRow(row.id)
|
|
|
|
|
+ if (!ok) {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ tableRef.value?.toggleRowExpansion?.(row, true)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchData() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const keyword = quickSearch.value.trim()
|
|
|
|
|
+ const res = await planApi.listContractGaps({
|
|
|
|
|
+ q: keyword || undefined,
|
|
|
|
|
+ planNo: keyword ? undefined : searchForm.planNo || undefined,
|
|
|
|
|
+ gapType: searchForm.gapType || undefined,
|
|
|
|
|
+ customerKeyword: keyword ? undefined : searchForm.customerKeyword || undefined,
|
|
|
|
|
+ planDateFrom: dateRange.value?.[0] || undefined,
|
|
|
|
|
+ planDateTo: dateRange.value?.[1] || undefined,
|
|
|
|
|
+ page: pagination.page,
|
|
|
|
|
+ pageSize: pagination.pageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ 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(id => ids.has(id))
|
|
|
|
|
+ Object.keys(expandDraft).forEach(id => {
|
|
|
|
|
+ if (!ids.has(id)) delete expandDraft[id]
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSearch() {
|
|
|
|
|
+ pagination.page = 1
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleReset() {
|
|
|
|
|
+ searchForm.planNo = ''
|
|
|
|
|
+ searchForm.gapType = ''
|
|
|
|
|
+ searchForm.customerKeyword = ''
|
|
|
|
|
+ quickSearch.value = ''
|
|
|
|
|
+ resetDateShortcut()
|
|
|
|
|
+ pagination.page = 1
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleQuickSearch() {
|
|
|
|
|
+ searchForm.planNo = ''
|
|
|
|
|
+ searchForm.customerKeyword = ''
|
|
|
|
|
+ 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 handleView(row: PlanContractGapItem) {
|
|
|
|
|
+ viewPlanId.value = row.id
|
|
|
|
|
+ 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}`
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await planApi.updateSideContracts(row.id, side, contracts)
|
|
|
|
|
+ const updated = ((res as any).data ?? res) as PlanContractGapItem
|
|
|
|
|
+ ElMessage.success(side === 'supplier' ? '供方合同已保存' : '需方合同已保存')
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 refreshMenuBadges(menuStore.menuList)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ savingKey.value = ''
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.pl-plan-contract-panel-top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 16px 20px 12px;
|
|
|
|
|
+ border-bottom: 1px solid #eef2f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-plan-contract-stat {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ 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);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.party-cell {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.party-cell__sub {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--el-text-color-secondary, #64748b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-contract-expand {
|
|
|
|
|
+ padding: 12px 16px 16px;
|
|
|
|
|
+ background: #f8fffb;
|
|
|
|
|
+ border-top: 1px dashed #a7f3d0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-contract-expand__grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 960px) {
|
|
|
|
|
+ .pl-contract-expand__grid {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-contract-side {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid var(--el-border-color, #d9e8e2);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.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.is-exempt {
|
|
|
|
|
+ border-color: #e2e8f0;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-contract-side__head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ h4 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pl-contract-side__customer {
|
|
|
|
|
+ margin: 0 0 10px;
|
|
|
|
|
+ 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;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|