feat(pl): 按运输段产品维护合法使用证明并隔离异步会话。

This commit is contained in:
wuzilong committed 2026-11-01 23:15:17 +08:00
1 parent 722076fb3f
commit 4cb2d88dab
8 files changed
+1103 -404

No files matched your search

+63
View File
@@ -159,7 +159,70 @@ export const quotaCertApi = {
request.get(`${base}/${kind}/${id}/contracts`).then(pickBxData<ContractOption[]>)
}
const legalBase = '/transport/legal-use-documents'
export interface StopLegalDocumentItem {
stopType: 'load' | 'delivery'
stopId: string
stopOrder: number
stopProductId: string
productId: string
productName: string
model?: string
unit?: string
contractId?: string | null
contractNo?: string | null
contractProductId?: string | null
partyName?: string | null
address?: string | null
legalUseCertUrl: string
documentStatus: 'pending' | 'uploaded'
uploadedBy?: string | null
uploadedTime?: string | null
}
export interface StopLegalDocument {
id: string
planNo: string
planDate?: string
licensePlate?: string
transportStatus: string
archiveStatus: string
requiredCount: number
uploadedCount: number
documentStatus: 'pending' | 'uploaded' | 'not_required'
items: StopLegalDocumentItem[]
legacyProducts: {
id: string
productId: string
productName: string
model?: string
legalUseCertUrl?: string | null
}[]
}
export const legalDocumentApi = {
listStops: (params: {
keyword?: string
planNo?: string
status?: 'pending' | 'uploaded'
contractId?: string
page?: number
pageSize?: number
}) =>
request
.get(`${legalBase}/stops`, { params })
.then(
pickBxData<{ list: StopLegalDocument[]; total: number; page: number; pageSize: number }>
),
getStops: (id: string, contractId?: string) =>
request
.get(`${legalBase}/stops/${id}`, { params: { contractId } })
.then(pickBxData<StopLegalDocument>),
saveStops: (
id: string,
items: Pick<StopLegalDocumentItem, 'stopType' | 'stopProductId' | 'legalUseCertUrl'>[],
contractId?: string
) =>
request
.patch(`${legalBase}/stops/${id}`, { items }, { params: { contractId } })
.then(pickBxData<StopLegalDocument>),
list: (params: { keyword?: string; status?: string }) =>
request.get(legalBase, { params }).then(pickBxData<LegalDocument[]>),
get: (id: string) => request.get(`${legalBase}/${id}`).then(pickBxData<LegalDocument>),
@@ -0,0 +1,497 @@
<template>
<section :class="embedded ? 'dialog-list-panel' : 'panel'">
<div v-if="!embedded" class="panel-top"><h2 class="title-bar-heading">合法使用证明</h2></div>
<div class="list-toolbar"
><div class="list-toolbar-row">
<ListQuickSearchInput
v-model="keyword"
placeholder="计划编号"
@search="search"
@clear="search"
/>
<el-select
v-model="status"
clearable
placeholder="上传情况"
aria-label="上传情况"
style="width: 150px"
@change="search"
>
<el-option label="待上传" value="pending" /><el-option label="已上传" value="uploaded" />
</el-select>
<el-button v-if="keyword || status || hasActiveFilters" @click="reset"
><el-icon><RefreshLeft /></el-icon>清除筛选</el-button
>
<ListToolbarEndActions
:loading="loading"
:column-defs="columns"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="load"
@reset-columns="resetToDefault"
/> </div
></div>
<el-alert v-if="error" :title="error" type="error" :closable="false"
><el-button @click="allowed ? load() : init()"
><el-icon><Refresh /></el-icon>重试</el-button
></el-alert
>
<el-empty v-if="!allowed && !loading && !error" description="无合法使用证明查看权限" />
<div v-else class="list-body list-wrap"
><div ref="tableWrapRef" class="table-scroll">
<ListTable
ref="tableRef"
table-key="pl-stop-documents"
:height="tableHeight"
:data="rows"
row-key="id"
stripe
class="list-table"
v-loading="loading"
@expand-change="expand"
>
<el-table-column type="expand"
><template #default="{ row }">
<div
class="view-section documents-expansion"
:style="expansionStyle"
v-loading="details[row.id]?.loading"
>
<el-alert
v-if="details[row.id]?.error"
title="资料加载失败"
type="error"
:closable="false"
><el-button @click="loadDetail(row)"
><el-icon><Refresh /></el-icon>重试</el-button
></el-alert
>
<template v-else-if="details[row.id]?.data">
<el-table
:data="details[row.id].data!.items"
:row-key="itemKey"
stripe
class="list-table"
>
<el-table-column label="运输段" min-width="100"
><template #default="{ row: item }"
><el-tag size="small"
>{{ item.stopType === 'load' ? '装货' : '送货' }}
{{ item.stopOrder }}</el-tag
></template
></el-table-column
>
<el-table-column prop="productName" label="产品" min-width="140" />
<el-table-column prop="model" label="规格" min-width="100" />
<el-table-column prop="unit" label="单位" min-width="80" />
<el-table-column prop="contractNo" label="合同编号" min-width="170" />
<el-table-column prop="partyName" label="本段主体" min-width="180" />
<el-table-column prop="address" label="本段位置" min-width="220" />
<el-table-column label="上传情况" min-width="110"
><template #default="{ row: item }"
><el-tag size="small" :type="statusType(item.documentStatus)">{{
statusLabel(item.documentStatus)
}}</el-tag></template
></el-table-column
>
<el-table-column label="附件" min-width="220"
><template #default="{ row: item }"
><AttachmentUploadField
:model-value="item.legalUseCertUrl || ''"
disabled
compact /></template
></el-table-column>
<el-table-column prop="uploadedTime" label="上传时间" min-width="170" />
<el-table-column v-if="canUpload" label="操作" min-width="260" fixed="right"
><template #default="{ row: item }"
><el-button @click="open(row, item)"
><el-icon><Document /></el-icon>维护本段资料</el-button
></template
></el-table-column
>
</el-table>
<section v-if="details[row.id].data!.legacyProducts.length" class="view-section">
<h3>历史计划级资料</h3
><p class="form-tip">历史附件仅供查看,不计入运输段上传情况。</p>
<div v-for="product in details[row.id].data!.legacyProducts" :key="product.id">
<p>{{ product.productName }} {{ product.model || '' }}</p>
<AttachmentUploadField
:model-value="product.legalUseCertUrl || ''"
disabled
/>
</div>
</section>
</template>
</div> </template
></el-table-column>
<el-table-column
v-for="column in visibleDefs"
:key="column.prop"
:prop="column.prop"
:label="column.label"
min-width="150"
>
<template #header
><div class="col-header-with-filter"
><span class="col-header-with-filter__label">{{ column.label }}</span
><ColumnHeaderFilter
v-if="column.prop === 'planNo'"
prop="planNo"
label="计划编号"
type="text"
:enable-sort="false"
:model-value="filters.planNo || null"
:is-active="isFilterActive('planNo')"
@update:model-value="(value) => setFilterState('planNo', value)" /></div
></template>
<template #default="{ row }">
<el-tag
v-if="column.prop === 'documentStatus'"
size="small"
:type="statusType(row.documentStatus)"
>{{ statusLabel(row.documentStatus) }}</el-tag
>
<span v-else-if="column.prop === 'progress'"
>{{ row.uploadedCount }} / {{ row.requiredCount }}</span
>
<span v-else>{{ row[column.prop] || '—' }}</span>
</template>
</el-table-column>
</ListTable> </div
><div ref="pagerRef" class="pagination list-pager-bar"
><el-pagination
v-model:current-page="page"
:page-size="20"
:total="total"
layout="total, prev, pager, next"
@current-change="load" /></div
></div>
<el-dialog
v-model="visible"
title="本段产品合法使用证明"
width="720px"
align-center
append-to-body
class="dialog-form-layout"
:close-on-click-modal="false"
>
<div v-loading="editorLoading">
<el-alert v-if="editorError" :title="editorError" type="error" :closable="false" />
<template v-if="editing"
><p
>{{ editing.stopType === 'load' ? '装货' : '送货' }} {{ editing.stopOrder }} ·
{{ editing.productName }} {{ editing.model }}</p
><p
>{{ editing.contractNo || '未关联合同' }} · {{ editing.partyName }} ·
{{ editing.address }}</p
>
<el-form label-position="top"
><el-form-item label="合法使用证明"
><AttachmentUploadField
:key="editorEpoch"
ref="upload"
v-model="editing.legalUseCertUrl"
folder="pl/legal-use-document"
:disabled="!canUpload || saving" /></el-form-item
></el-form>
</template>
</div>
<template #footer
><el-button @click="visible = false"
><el-icon><Close /></el-icon>关闭</el-button
><el-button
v-if="canUpload"
type="primary"
:loading="saving"
:disabled="editorLoading || !editing || !!upload?.hasUploading()"
@click="save"
><el-icon><Check /></el-icon>保存本段资料</el-button
></template
>
</el-dialog>
</section>
</template>
<script setup lang="ts">
import { Check, Close, Document, Refresh, RefreshLeft } from '@element-plus/icons-vue'
import { fetchGetMenuCodes } from '@/api/auth'
import { useUserStore } from '@/store/modules/user'
import {
legalDocumentApi,
type StopLegalDocument,
type StopLegalDocumentItem
} from '@/api/pl/quota-cert'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
const props = withDefaults(
defineProps<{ contractId?: string; embedded?: boolean; active?: boolean }>(),
{ active: true }
)
const user = useUserStore()
const allowed = ref(false),
canUpload = ref(false),
loading = ref(false),
error = ref(''),
keyword = ref(''),
status = ref<'' | 'pending' | 'uploaded'>(''),
page = ref(1),
total = ref(0)
const rows = ref<StopLegalDocument[]>([])
const details = reactive<
Record<string, { loading: boolean; error: boolean; data?: StopLegalDocument }>
>({})
const visible = ref(false),
saving = ref(false),
editorLoading = ref(false),
editorError = ref(''),
editorEpoch = ref(0)
const editing = ref<StopLegalDocumentItem>(),
editingPlanId = ref('')
const upload = ref<InstanceType<typeof AttachmentUploadField>>()
let authEpoch = 0,
listEpoch = 0
const columns: ColumnFilterDef[] = [
['planNo', '计划编号'],
['planDate', '计划日期'],
['licensePlate', '车牌号'],
['progress', '已上传 / 应上传'],
['documentStatus', '上传情况']
].map(([prop, label]) => ({ prop, label, type: 'text', queryKey: prop }))
const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns(
'pl-stop-documents-v1',
columns
)
const visibleDefs = computed(() => columns.filter((c) => visibleColumns.value.includes(c.prop)))
const {
filters,
hasActiveFilters,
setFilterState,
clearAllFilters,
isFilterActive,
toQueryParams,
watchVisibleColumns
} = useServerColumnFilters(
columns.filter((c) => c.prop === 'planNo'),
{ onChange: search }
)
watchVisibleColumns(visibleColumns)
const statusLabel = (value: string) =>
value === 'uploaded' ? '已上传' : value === 'not_required' ? '无需上传' : '待上传'
const statusType = (value: string) =>
value === 'uploaded' ? 'success' : value === 'not_required' ? 'info' : 'warning'
const itemKey = (item: StopLegalDocumentItem) => `${item.stopType}:${item.stopProductId}`
function clearDetails() {
Object.keys(details).forEach((key) => delete details[key])
}
function closeEditor() {
editorEpoch.value++
editing.value = undefined
editingPlanId.value = ''
upload.value = undefined
saving.value = false
editorLoading.value = false
editorError.value = ''
}
watch(
visible,
(value) => {
if (!value) closeEditor()
},
{ flush: 'sync' }
)
async function load() {
if (!allowed.value || !props.active) return
const current = ++listEpoch
rows.value = []
clearDetails()
loading.value = true
error.value = ''
try {
const data = await legalDocumentApi.listStops({
page: page.value,
pageSize: 20,
keyword: keyword.value,
...toQueryParams(),
status: status.value || undefined,
contractId: props.contractId
})
if (current !== listEpoch) return
rows.value = data.list
total.value = data.total
} catch {
if (current === listEpoch) error.value = '资料加载失败,请重试'
} finally {
if (current === listEpoch) loading.value = false
}
}
async function loadDetail(row: Pick<StopLegalDocument, 'id'>) {
if (!allowed.value || !props.active) return
const current = listEpoch,
state = reactive({
loading: true,
error: false,
data: undefined as StopLegalDocument | undefined
})
details[row.id] = state
try {
const result = await legalDocumentApi.getStops(row.id, props.contractId)
if (current === listEpoch && details[row.id] === state) state.data = result
} catch {
if (current === listEpoch && details[row.id] === state) state.error = true
} finally {
if (current === listEpoch && details[row.id] === state) state.loading = false
}
}
function expand(row: StopLegalDocument, expanded: StopLegalDocument[]) {
if (expanded.some((r) => r.id === row.id) && !details[row.id]) void loadDetail(row)
}
async function open(
row: Pick<StopLegalDocument, 'id'>,
item: Pick<StopLegalDocumentItem, 'stopType' | 'stopProductId'>
) {
if (!allowed.value || !canUpload.value || !props.active) return
closeEditor()
visible.value = true
editorLoading.value = true
const current = editorEpoch.value
try {
const result = await legalDocumentApi.getStops(row.id, props.contractId)
if (current !== editorEpoch.value) return
const match = result.items.find(
(candidate) => itemKey(candidate) === itemKey(item as StopLegalDocumentItem)
)
if (!match) {
editorError.value = '本段产品已变更,请刷新列表'
return
}
editing.value = { ...match, legalUseCertUrl: match.legalUseCertUrl || '' }
editingPlanId.value = row.id
} catch {
if (current === editorEpoch.value) editorError.value = '资料加载失败,请关闭后重试'
} finally {
if (current === editorEpoch.value) editorLoading.value = false
}
}
async function save() {
if (
!visible.value ||
!editing.value ||
!allowed.value ||
!canUpload.value ||
saving.value ||
editorLoading.value
)
return
if (upload.value?.hasUploading()) {
ElMessage.warning('请等待附件上传完成')
return
}
const current = editorEpoch.value,
item = editing.value
saving.value = true
editorError.value = ''
try {
await legalDocumentApi.saveStops(
editingPlanId.value,
[
{
stopType: item.stopType,
stopProductId: item.stopProductId,
legalUseCertUrl: item.legalUseCertUrl
}
],
props.contractId
)
if (current !== editorEpoch.value) return
ElMessage.success('本段资料已保存')
visible.value = false
void load()
} catch {
if (current === editorEpoch.value) editorError.value = '保存失败,已保留填写内容,请重试'
} finally {
if (current === editorEpoch.value) saving.value = false
}
}
function search() {
page.value = 1
void load()
}
function reset() {
keyword.value = ''
status.value = ''
clearAllFilters()
search()
}
async function init() {
const current = ++authEpoch
listEpoch++
rows.value = []
total.value = 0
page.value = 1
clearDetails()
visible.value = false
closeEditor()
allowed.value = false
canUpload.value = false
loading.value = false
error.value = ''
if (!props.active || !user.accessToken) return
loading.value = true
try {
const permissions = await fetchGetMenuCodes()
if (current !== authEpoch) return
const codes = permissions.menuCodes || [],
all = !!permissions.all || !!permissions.isSuperAdmin
allowed.value = all || codes.includes('pl:legal-use-document:list')
canUpload.value = allowed.value && (all || codes.includes('pl:plan-cert:upload'))
loading.value = false
await load()
} catch {
if (current === authEpoch) {
loading.value = false
error.value = '权限加载失败,请重试'
}
}
}
watch(
() => [props.contractId, props.active, JSON.stringify([user.accessToken, user.getUserInfo])],
() => void init(),
{ immediate: true, flush: 'sync' }
)
onBeforeUnmount(() => {
authEpoch++
listEpoch++
closeEditor()
allowed.value = false
canUpload.value = false
rows.value = []
clearDetails()
})
const tableWrapRef = ref<HTMLElement | null>(null),
pagerRef = ref<HTMLElement | null>(null),
tableRef = ref()
const { width: tableViewportWidth } = useElementSize(tableWrapRef)
const expansionStyle = computed(() => ({
width: tableViewportWidth.value > 0 ? `${tableViewportWidth.value}px` : undefined
}))
const { tableHeight } = useListTableViewportHeight({
tableEl: tableWrapRef,
pagerEl: pagerRef,
tableRef,
enabled: computed(() => !props.embedded)
})
</script>
<style scoped>
.documents-expansion {
box-sizing: border-box;
max-width: 100%;
padding: 16px;
text-align: left;
}
</style>
@@ -1,356 +1,6 @@
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top"><h2 class="title-bar-heading">合法使用证明</h2></div>
<div class="list-toolbar"
><div class="list-toolbar-row"
><ListQuickSearchInput
v-model="keyword"
placeholder="计划编号"
@search="search"
@clear="search" /><el-button v-if="hasActiveFilters || keyword" @click="reset"
><el-icon><RefreshLeft /></el-icon>清除筛选</el-button
><ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="load"
@reset-columns="resetToDefault" /></div
></div>
<div class="list-body list-wrap"
><div ref="tableWrapRef" class="table-scroll"
><ListTable
ref="tableRef"
:height="tableHeight"
table-key="pl-legal-use-document"
v-loading="loading"
:data="pageRows"
row-key="id"
stripe
class="list-table"
@row-dblclick="open"
>
<el-table-column
v-for="column in visibleDefs"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.type === 'text' ? 190 : 140"
><template #header
><div class="col-header-with-filter"
><span class="col-header-with-filter__label">{{ column.label }}</span>
<ColumnHeaderFilter
:prop="column.prop"
:label="column.label"
:type="column.type"
:enable-sort="false"
:options="collectUniqueOptions(column.prop)"
:is-active="isFilterActive(column.prop)"
:model-value="filters[column.prop] || null"
@update:model-value="(v) => setFilterState(column.prop, v)"
/> </div></template
><template #default="{ row }"
><el-link v-if="column.prop === 'planNo'" type="primary" @click="open(row)">{{
row.planNo
}}</el-link
><span v-else-if="column.prop === 'productNames'">{{ productNames(row) }}</span
><el-tag v-else size="small" :type="pendingCount(row) ? 'warning' : 'success'">{{
column.prop === 'documentStatus'
? pendingCount(row)
? '待上传'
: '已上传'
: `${pendingCount(row)} / ${row.products.length}`
}}</el-tag></template
></el-table-column
><el-table-column label="操作" min-width="260" fixed="right"
><template #default="{ row }"
><el-button @click="open(row)"
><el-icon><Document /></el-icon
>{{ canUpload ? '维护资料' : '查看资料' }}</el-button
></template
></el-table-column
>
</ListTable></div
><div ref="pagerRef" class="pagination list-pager-bar"
><el-pagination
v-model:current-page="page"
:page-size="20"
:total="filteredRows.length"
layout="total, prev, pager, next" /></div
></div>
</section>
<el-dialog
v-model="visible"
:title="`合法使用证明 · ${detail?.planNo || ''}`"
width="900px"
align-center
class="dialog-form-layout"
:close-on-click-modal="false"
:close-on-press-escape="!saving"
:show-close="!saving"
>
<section v-for="p in detail?.products || []" :key="p.id" class="view-section">
<h3>{{ p.productName }} {{ p.model || '' }}</h3>
<p v-for="context in p.contexts || []" :key="context.stopProductId"
>{{ context.stopType === 'load' ? '装货' : '送货' }}:{{
context.partyName || '未填写单位'
}}
· {{ context.address || '未填写地址' }} · 合同
{{ context.contractNo || '未关联或无查看权限' }}</p
>
<el-tag size="small" :type="p.legalUseCertUrl ? 'success' : 'warning'">{{
p.legalUseCertUrl ? '已上传' : '待上传'
}}</el-tag>
<AttachmentUploadField
:ref="(el) => setUpload(p.id, el)"
v-model="p.legalUseCertUrl"
folder="pl/legal-use-document"
:disabled="!canUpload || saving"
/>
</section>
<template #footer
><el-button :disabled="saving" @click="visible = false"
><el-icon><Close /></el-icon>关闭</el-button
><el-button v-if="canUpload" type="primary" :loading="saving" @click="save"
><el-icon><Check /></el-icon>保存资料</el-button
></template
>
</el-dialog>
</div>
<div class="page-container"><StopDocuments /></div>
</template>
<script setup lang="ts">
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
useClientColumnFilters,
type ColumnFilterDef,
type ColumnFilterState,
type TextColumnFilterState
} from '@/composables/useClientColumnFilters'
import { Check, Close, Document, RefreshLeft } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { fetchGetMenuCodes } from '@/api/auth'
import { legalDocumentApi, type LegalDocument } from '@/api/pl/quota-cert'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
const page = ref(1)
const keyword = ref('')
const loading = ref(false),
saving = ref(false),
visible = ref(false),
canUpload = ref(false)
const rows = ref<LegalDocument[]>([]),
detail = ref<LegalDocument>()
let listEpoch = 0
let disposed = false
async function load() {
if (disposed) return
const current = ++listEpoch
loading.value = true
try {
const result = await legalDocumentApi.list({})
if (current !== listEpoch || disposed) return
rows.value = result
} finally {
if (current === listEpoch && !disposed) loading.value = false
}
}
const uploads = new Map<string, InstanceType<typeof AttachmentUploadField>>()
function setUpload(id: string, value: any) {
if (value) uploads.set(id, value)
else uploads.delete(id)
}
let session = 0
watch(
visible,
(value) => {
if (!value) session++
},
{ flush: 'sync' }
)
async function open(row: LegalDocument) {
if (saving.value) return
const current = ++session
const result = await legalDocumentApi.get(row.id)
if (current !== session) return
detail.value = result
visible.value = true
}
async function save() {
if (!detail.value || !canUpload.value || saving.value) return
if ([...uploads.values()].some((upload) => upload.hasUploading())) {
ElMessage.warning('请等待附件上传完成')
return
}
const current = session,
targetId = detail.value.id
saving.value = true
try {
const result = await legalDocumentApi.save(
targetId,
detail.value.products.map((p) => ({ id: p.id, legalUseCertUrl: p.legalUseCertUrl }))
)
if (current !== session || detail.value?.id !== targetId) return
detail.value = result
ElMessage.success('合法使用证明已保存')
visible.value = false
await load()
} finally {
saving.value = false
}
}
const user = useUserStore()
let authEpoch = 0
async function init() {
const current = ++authEpoch
const p = await fetchGetMenuCodes()
if (current !== authEpoch || disposed) return
canUpload.value =
!!p.all || !!p.isSuperAdmin || (p.menuCodes || []).includes('pl:plan-cert:upload')
await load()
}
watch(
() => JSON.stringify([user.accessToken, user.getUserInfo]),
() => {
authEpoch++
listEpoch++
rows.value = []
page.value = 1
loading.value = false
canUpload.value = false
session++
visible.value = false
detail.value = undefined
if (user.accessToken) void init()
},
{ flush: 'sync' }
)
onMounted(init)
onBeforeUnmount(() => {
disposed = true
listEpoch++
authEpoch++
})
const productNames = (row: LegalDocument) => row.products.map((p) => p.productName).join('、')
const pendingCount = (row: LegalDocument) =>
row.products.filter((p) => p.documentStatus === 'pending').length
const listColumnDefs: ColumnFilterDef[] = [
{ prop: 'planNo', label: '计划编号', type: 'text' },
{ prop: 'productNames', label: '易制爆产品', type: 'text', getValue: productNames },
{ prop: 'pendingCount', label: '待补明细', type: 'number', getValue: pendingCount },
{
prop: 'documentStatus',
label: '上传情况',
type: 'enum',
enumOptions: [
{ value: 'pending', label: '待上传' },
{ value: 'uploaded', label: '已上传' }
]
}
]
const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns(
'pl-legal-use-document-columns-v1',
listColumnDefs
)
const visibleDefs = computed(() =>
listColumnDefs.filter((c) => visibleColumns.value.includes(c.prop))
)
const appliedKeyword = ref('')
const searchedRows = computed(() =>
rows.value.filter((row) => String(row.planNo).includes(appliedKeyword.value))
)
const {
filters,
filteredRows: columnFilteredRows,
collectUniqueOptions: collectColumnOptions,
hasActiveFilters: hasColumnFilters,
setFilterState: setColumnFilter,
clearAllFilters,
clearFilter,
isFilterActive: isColumnFilterActive
} = useClientColumnFilters(
searchedRows,
listColumnDefs.filter((column) => column.prop !== 'documentStatus')
)
const selectedStatuses = computed(
() => (filters.documentStatus as TextColumnFilterState | undefined)?.selected || []
)
const filteredRows = computed(() =>
columnFilteredRows.value.filter(
(row: LegalDocument) =>
!selectedStatuses.value.length ||
row.products.some((product) => selectedStatuses.value.includes(product.documentStatus))
)
)
const hasActiveFilters = computed(
() => hasColumnFilters.value || selectedStatuses.value.length > 0
)
function setFilterState(prop: string, state: ColumnFilterState | null) {
if (prop !== 'documentStatus') return setColumnFilter(prop, state)
const selected =
(state as TextColumnFilterState | null)?.selected?.filter((value) =>
['pending', 'uploaded'].includes(value)
) || []
if (selected.length) filters.documentStatus = { text: '', selected: [...selected] }
else clearFilter(prop)
}
function isFilterActive(prop: string) {
return prop === 'documentStatus'
? selectedStatuses.value.length > 0
: isColumnFilterActive(prop)
}
function collectUniqueOptions(prop: string) {
if (prop !== 'documentStatus') return collectColumnOptions(prop)
return [
{ value: 'pending', label: '待上传' },
{ value: 'uploaded', label: '已上传' }
].map((option) => ({
...option,
count: searchedRows.value.filter((row) =>
row.products.some((product) => product.documentStatus === option.value)
).length
}))
}
const pageRows = computed(() => filteredRows.value.slice((page.value - 1) * 20, page.value * 20))
function search() {
appliedKeyword.value = keyword.value.trim()
page.value = 1
}
function reset() {
keyword.value = ''
appliedKeyword.value = ''
clearAllFilters()
page.value = 1
}
watch(
filters,
() => {
page.value = 1
},
{ deep: true, flush: 'sync' }
)
watch(visibleColumns, (next, prev) => {
prev.filter((p) => !next.includes(p)).forEach(clearFilter)
})
watch(
() => filteredRows.value.length,
(total) => {
page.value = Math.min(page.value, Math.max(1, Math.ceil(total / 20)))
}
)
const tableWrapRef = ref<HTMLElement | null>(null)
const pagerRef = ref<HTMLElement | null>(null)
const tableRef = ref()
const { tableHeight } = useListTableViewportHeight({
tableEl: tableWrapRef,
pagerEl: pagerRef,
tableRef
})
import StopDocuments from './StopDocuments.vue'
</script>