Files
shanxia2.0/web-admin/src/views/pl/plan-cert/index.vue
T
2026-08-27 19:38:01 +08:00

1002 lines
33 KiB
Vue

<!-- 危化品证上传 -->
<template>
<div class="page-container">
<section class="panel">
<PlPlanTypeTabs
v-model="activePlanTypeTab"
:tabs="PL_PLAN_CERT_TABS"
@change="handlePlanTypeTabChange"
/>
<!-- 筛选区 -->
<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.followStatus" placeholder="全部" clearable>
<el-option
v-for="opt in PL_PLAN_FOLLOW_STATUS_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户名称">
<el-input v-model="searchForm.customerName" placeholder="搜索客户名称" clearable />
</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>
<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" />
<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="canToolbarMarkCertReady ? 'btn-main' : 'btn-ghost'"
:type="canToolbarMarkCertReady ? 'primary' : undefined"
:disabled="!canToolbarMarkCertReady"
:loading="certReadyLoading"
@click="handleToolbarMarkCertReady"
>
<el-icon><CircleCheck /></el-icon>证书齐备
</el-button>
<el-button
:class="
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
? 'btn-ghost'
: 'btn-main'
"
:type="
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
? undefined
: 'primary'
"
:disabled="!selectedPlan || !canUploadCert(selectedPlan)"
@click="handleToolbarUploadCert"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
class="btn-ghost"
:disabled="!canToolbarUnmarkCertReady"
:loading="certReadyLoading"
@click="handleToolbarUnmarkCertReady"
>
<el-icon><CircleClose /></el-icon>取消齐备
</el-button>
<ListExportDropdown
:export-fn="planApi.export"
:get-filter-params="getPlanCertExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => r.id)"
:visible-columns="planCertExportColumns"
filename="计划证件.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<!-- 数据表格 -->
<div class="list-body list-wrap">
<ListTable
table-key="pl-plan-cert"
class="list-table pl-plan-merge-list-table"
:data="displayRows"
v-loading="loading"
row-key="rowKey"
:span-method="planListSpanMethod"
:row-class-name="getPlPlanListRowClassName"
@selection-change="handleDisplaySelectionChange"
@row-dblclick="handleDisplayRowDblClick"
>
<el-table-column
type="selection"
width="55"
align="center"
header-align="center"
:selectable="isPlanFirstRowSelectable"
/>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="getDisplayRowIndex"
/>
<el-table-column prop="planNo" label="计划编号" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row.plan)">{{ row.plan.planNo }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planDate')"
prop="planDate"
label="计划日期"
min-width="120"
align="center"
>
<template #default="{ row }">{{ row.plan.planDate || '—' }}</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
label="单据类型"
min-width="100"
align="center"
>
<template #default="{ row }">
{{ getPlanTypeLabel(row.plan.planType) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('followStatus')"
prop="followStatus"
label="跟进状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getPlPlanFollowStatusTagType(row.plan.followStatus)" size="small">
{{ getPlPlanFollowStatusLabel(row.plan.followStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('hazardCertReady')"
label="证书齐备"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getHazardCertReadyTagType(row.plan)" size="small">
{{ getHazardCertReadyLabel(row.plan) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planAttachments')"
label="计划附件"
min-width="280"
>
<template #default="{ row }">
<PlPlanListCertCell
:products="row.plan.products"
hide-product
clickable
:clickable-disabled="!canUploadCert(row.plan)"
:media-active="mediaActive"
@click="handleUploadCert(row.plan)"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ formatPlVehiclePlateDisplay(row.plan.licensePlate, row.plan.trailerNo) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('personnelSummary')"
prop="personnelSummary"
label="人员"
min-width="180"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ normalizePlPersonnelSummaryDisplay(row.plan.personnelSummary) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('followUserName')"
prop="followUserName"
label="跟进人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.plan.followUserName || '—' }}</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('stopKind')"
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
v-if="isColumnVisible('stopCustomer')"
prop="stopCustomer"
label="客户"
min-width="200"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
show-overflow-tooltip
>
<template #default="{ row }">
<span>{{ getStopCustomerName(row) }}</span>
<el-tag
v-if="isPlInternalCustomerName(getStopCustomerName(row))"
type="success"
size="small"
class="pl-plan-internal-customer-tag"
>
内部
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('stopAddressTime')"
prop="stopAddressTime"
label="地址 / 时间"
min-width="220"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<div class="pl-plan-stop-address-time">
<div class="pl-plan-stop-address-time__addr">{{ getStopAddress(row) }}</div>
<div class="pl-plan-stop-address-time__time">{{ getStopTime(row) }}</div>
</div>
</template>
</el-table-column>
<el-table-column
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="产品名称"
min-width="160"
align="left"
header-align="left"
class-name="col-name-wrap pl-plan-stop-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="name"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productSpec')"
label="型号/规格"
min-width="96"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="spec"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productUnit')"
label="单位"
min-width="64"
align="center"
header-align="center"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="unit"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productPlanQty')"
label="计划数量"
min-width="88"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="planQty"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productActualQty')"
label="实际数量"
min-width="88"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-kind="row.kind"
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="actualQty"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productInboundQty')"
label="入库数量"
min-width="88"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="inboundQty"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productOutboundQty')"
label="出库数量"
min-width="88"
align="right"
header-align="right"
class-name="pl-plan-stop-col"
>
<template #default="{ row }">
<PlPlanListProductCell
:stop-products="getStopProducts(row)"
:plan-products="row.plan.products"
field="outboundQty"
/>
</template>
</el-table-column>
</ListTable>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<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"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</section>
<PlPlanViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
:plan-id="viewPlanId"
show-inbound-outbound-qty
>
<template v-if="viewingPlan" #hero-actions>
<el-button
v-if="canManageCertReady(viewingPlan) && !isHazardCertReady(viewingPlan)"
class="btn-main"
type="primary"
:loading="certReadyLoading"
@click="handleMarkCertReady(viewingPlan)"
>
<el-icon><CircleCheck /></el-icon>证书齐备
</el-button>
<el-button
v-if="canUploadCert(viewingPlan)"
class="btn-ghost"
@click="handleUploadCert(viewingPlan)"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
v-if="canManageCertReady(viewingPlan) && isHazardCertReady(viewingPlan)"
class="btn-ghost"
:loading="certReadyLoading"
@click="handleUnmarkCertReady(viewingPlan)"
>
<el-icon><CircleClose /></el-icon>取消齐备
</el-button>
</template>
</PlPlanViewDialog>
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
<PlPlanCertUploadDialog
v-model:visible="certDialogVisible"
:plan-id="certPlanId"
@success="handleCertUploadSuccess"
/>
</div>
</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 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 { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
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 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-v4', listColumnDefs)
const selectedRows = ref<Plan[]>([])
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
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,
hazardCertReady:
searchForm.hazardCertReady === '' ? undefined : Number(searchForm.hazardCertReady)
}
}
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
})
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
})
function getPlanTypeLabel(type: string) {
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
return map[type] || type
}
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 handleSearch() {
pagination.page = 1
fetchData()
}
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()
}
function handleReset() {
activePlanTypeTab.value = 'all'
searchForm.planNo = ''
searchForm.planType = ''
searchForm.followStatus = ''
searchForm.customerName = ''
searchForm.certUploadStatus = ''
searchForm.hazardCertReady = ''
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)
}
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';
.pl-plan-list-toolbar-row {
flex-wrap: wrap;
}
</style>