fix(pl): 统一计划用证三页的检索工具栏与漏斗以保持筛选和分页一致。

This commit is contained in:
wuzilong committed 2026-11-01 19:24:59 +08:00
1 parent c4f173bc3a
commit c16681a16f
8 files changed
+982 -235

No files matched your search

@@ -143,7 +143,7 @@
>
<span class="value-label">{{ opt.label }}</span>
</el-checkbox>
<span class="value-count">{{ opt.count }}</span>
<span v-if="opt.count !== undefined" class="value-count">{{ opt.count }}</span>
</label>
<div v-if="!optionsLoading && !visibleOptions.length" class="empty-tip">暂无可选值</div>
</div>
@@ -267,7 +267,7 @@
type: ColumnFilterType
isActive: boolean
modelValue?: ColumnFilterState | null
options?: Array<{ value: string; label: string; count: number }>
options?: Array<{ value: string; label: string; count?: number }>
/** 枚举选项加载中(动态 facet) */
optionsLoading?: boolean
/** CRM 外壳 CrmColFilterHeader 已自绘双三角时传 false,避免重复 */
@@ -2,71 +2,84 @@
<div class="page-container">
<section class="panel">
<div class="panel-top"><h2 class="title-bar-heading">合法使用证明</h2></div>
<div class="filter-block"
><el-form label-position="top" @submit.prevent="load">
<div class="filter-grid"
><el-form-item label="计划编号"
><el-input v-model="keyword" placeholder="输入计划编号" clearable /></el-form-item
><el-form-item label="上传情况"
><el-select v-model="status" clearable
><el-option value="pending" label="待上传" /><el-option
value="uploaded"
label="已上传" /></el-select></el-form-item
></div>
<div class="filter-actions-row"
><span class="filter-hint">合法使用证明可在运输安排或归档后补传。</span
><div class="filter-actions-end"
><el-button @click="reset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
><el-button type="primary" :loading="loading" @click="load"
><el-icon><Search /></el-icon>搜索</el-button
></div
></div
>
</el-form></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"
><el-table
v-loading="loading"
:data="rows"
row-key="id"
stripe
class="list-table"
@row-dblclick="open"
>
<el-table-column label="计划编号" min-width="180"
><template #default="{ row }"
><el-link type="primary" @click="open(row)">{{ row.planNo }}</el-link></template
></el-table-column
><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 label="易制爆产品" min-width="220"
><template #default="{ row }">{{
row.products.map((p: any) => p.productName).join('、')
}}</template></el-table-column
>
<el-table-column label="待补明细" min-width="120"
><template #default="{ row }"
><el-tag
size="small"
:type="
row.products.some((p: any) => p.documentStatus === 'pending')
? 'warning'
: 'success'
"
>{{ row.products.filter((p: any) => p.documentStatus === 'pending').length }} /
{{ 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
>
</el-table></div
>
<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"
@@ -109,34 +122,45 @@
</template>
<script setup lang="ts">
import { Check, Close, Document, RefreshLeft, Search } from '@element-plus/icons-vue'
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 keyword = ref(''),
status = ref('')
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 {
rows.value = await legalDocumentApi.list({
keyword: keyword.value,
status: status.value || undefined
})
const result = await legalDocumentApi.list({})
if (current !== listEpoch || disposed) return
rows.value = result
} finally {
loading.value = false
if (current === listEpoch && !disposed) loading.value = false
}
}
function reset() {
keyword.value = ''
status.value = ''
load()
}
const uploads = new Map<string, InstanceType<typeof AttachmentUploadField>>()
function setUpload(id: string, value: any) {
if (value) uploads.set(id, value)
@@ -181,10 +205,152 @@
saving.value = false
}
}
onMounted(async () => {
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
})
</script>
+141 -35
View File
@@ -2,7 +2,7 @@
<div :class="mobile ? 'mp-h5-list-content' : 'page-container'">
<section class="panel">
<div v-if="!mobile" class="panel-top"><h2 class="title-bar-heading">计划用证</h2></div>
<div class="filter-block"
<div v-if="mobile" class="filter-block"
><el-form label-position="top" @submit.prevent="load"
><el-form-item label="计划编号"
><el-input v-model="keyword" clearable placeholder="输入计划编号" /></el-form-item
@@ -18,7 +18,26 @@
></el-form
></div
>
<p class="form-tip">按当前来源、合同产品范围和证件余额校验;实际安排时原子预占。</p>
<div v-else 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>
<p v-if="mobile" class="form-tip"
>按当前来源、合同产品范围和证件余额校验;实际安排时原子预占。</p
>
<div v-if="mobile" v-loading="loading"
><section v-for="row in rows" :key="row.id" class="mp-wv-section"
><h3>{{ row.planNo }}</h3
@@ -38,38 +57,69 @@
></div
>
<div v-else class="list-body list-wrap"
><el-table v-loading="loading" :data="rows" stripe class="list-table"
><el-table-column label="计划编号" min-width="190"
><template #default="{ row }"
><el-link type="primary" @click="open(row.id)">{{ row.planNo }}</el-link></template
></el-table-column
><el-table-column prop="planDate" label="计划日期" min-width="140" /><el-table-column
prop="licensePlate"
label="车号"
min-width="160"
/><el-table-column label="用证校验" min-width="160"
><template #default="{ row }"
><el-tag
:type="
row.quotaStatus === 'exception'
? 'danger'
: row.quotaStatus === 'reserved' || row.quotaStatus === 'settled'
? 'success'
: 'info'
"
>{{ stateLabels[row.quotaStatus] }}</el-tag
></template
></el-table-column
><el-table-column label="操作" min-width="260" fixed="right"
><template #default="{ row }"
><el-button @click="open(row.id)"
><el-icon><Document /></el-icon>用证配置与占用</el-button
></template
></el-table-column
></el-table
></div
>
<div class="pagination list-pager-bar"
><div ref="tableWrapRef" class="table-scroll"
><ListTable
ref="tableRef"
:height="tableHeight"
table-key="pl-plan-quota"
v-loading="loading"
:data="rows"
stripe
class="list-table"
><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="column.enumOptions || []"
: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.id)">{{
row.planNo
}}</el-link
><el-tag
v-else-if="column.prop === 'quotaStatus'"
size="small"
:type="
row.quotaStatus === 'exception'
? 'danger'
: ['reserved', 'settled'].includes(row.quotaStatus)
? 'success'
: 'info'
"
>{{ stateLabels[row.quotaStatus] }}</el-tag
><span v-else>{{ row[column.prop] || '—' }}</span></template
></el-table-column
><el-table-column label="操作" min-width="260" fixed="right"
><template #default="{ row }"
><el-button @click="open(row.id)"
><el-icon><Document /></el-icon>用证配置与占用</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="total"
layout="total, prev, pager, next"
@current-change="load" /></div
></div>
<div v-if="mobile" class="pagination list-pager-bar"
><el-pagination
v-model:current-page="page"
:page-size="20"
@@ -82,8 +132,16 @@
</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 { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import { type ColumnFilterDef } from '@/composables/useClientColumnFilters'
import { useUserStore } from '@/store/modules/user'
import { Document, Search } from '@element-plus/icons-vue'
import { Document, Search, RefreshLeft } from '@element-plus/icons-vue'
import { type Plan } from '@/api/pl'
import request from '@/utils/http/bx'
const stateLabels: Record<string, string> = {
@@ -112,6 +170,7 @@
const value = pickBxData<{ list: (Plan & { quotaStatus: string })[]; total: number }>(
await request.get('/transport/quota-certs/plans', {
params: {
...toQueryParams(),
page: page.value,
pageSize: 20,
keyword: keyword.value,
@@ -155,4 +214,51 @@
onBeforeUnmount(() => {
epoch++
})
const listColumnDefs: ColumnFilterDef[] = [
{ prop: 'planNo', label: '计划编号', type: 'text' },
{ prop: 'planDate', label: '计划日期', type: 'datetime' },
{ prop: 'licensePlate', label: '车号', type: 'text' },
{
prop: 'quotaStatus',
label: '用证校验',
type: 'enum',
enumOptions: Object.entries(stateLabels).map(([value, label]) => ({ value, label }))
}
]
const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns(
'pl-plan-quota-columns-v1',
listColumnDefs
)
const visibleDefs = computed(() =>
listColumnDefs.filter((c) => visibleColumns.value.includes(c.prop))
)
const {
filters,
hasActiveFilters,
setFilterState,
clearAllFilters,
isFilterActive,
toQueryParams,
watchVisibleColumns
} = useServerColumnFilters(listColumnDefs, { onChange: search })
watchVisibleColumns(visibleColumns)
function search() {
page.value = 1
void load()
}
function reset() {
keyword.value = ''
status.value = ''
clearAllFilters()
search()
}
const tableWrapRef = ref<HTMLElement | null>(null)
const pagerRef = ref<HTMLElement | null>(null)
const tableRef = ref()
const { tableHeight } = useListTableViewportHeight({
tableEl: tableWrapRef,
pagerEl: pagerRef,
tableRef,
enabled: computed(() => !props.mobile)
})
</script>
+220 -112
View File
@@ -3,105 +3,101 @@
<div class="page-container">
<section class="panel">
<div class="panel-top"><h2 class="title-bar-heading">购买证与运输证</h2></div>
<div class="filter-block">
<el-form label-position="top" @submit.prevent="load">
<div class="filter-grid">
<el-form-item label="证件种类"
><el-select v-model="kind" @change="load"
><el-option label="购买证" value="purchase" /><el-option
label="运输证"
value="transport" /></el-select
></el-form-item>
<el-form-item label="证件编号"
><el-input v-model="keyword" clearable placeholder="输入证件编号"
/></el-form-item>
<el-form-item label="状态"
><el-select v-model="status" clearable
><el-option
v-for="(label, value) in statusLabels"
:key="value"
:label="label"
:value="value" /></el-select
></el-form-item>
</div>
<div class="filter-actions-row"
><span class="filter-hint">先建档核对,再关联合同及产品;同一证件共用一个额度池。</span
><div class="filter-actions-end"
><el-button @click="reset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
><el-button type="primary" :loading="loading" @click="load"
><el-icon><Search /></el-icon>搜索</el-button
></div
></div
>
</el-form>
</div>
<div class="list-toolbar"
><div class="list-toolbar-row">
<span class="list-toolbar-selected">共 {{ rows.length }} 张</span>
<el-button v-if="canEdit" type="primary" @click="edit()"
><div class="list-toolbar-row"
><el-select v-model="kind" aria-label="证件种类" style="width: 130px"
><el-option label="购买证" value="purchase" /><el-option
label="运输证"
value="transport" /></el-select
><ListQuickSearchInput
v-model="keyword"
placeholder="证件编号"
@search="search"
@clear="search" /><el-button v-if="hasActiveFilters || keyword" @click="reset"
><el-icon><RefreshLeft /></el-icon>清除筛选</el-button
><span class="list-toolbar-selected">共 {{ filteredRows.length }} 张</span
><el-button v-if="canEdit" type="primary" @click="edit()"
><el-icon><Plus /></el-icon>新增证件</el-button
>
<el-button @click="scopeDialog?.open()"
><el-button @click="scopeDialog?.open()"
><el-icon><Connection /></el-icon>关联合同产品</el-button
>
</div></div
>
><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">
<el-table
v-loading="loading"
:data="pageRows"
stripe
row-key="id"
class="list-table"
@row-dblclick="view"
<div ref="tableWrapRef" class="table-scroll"
><ListTable
ref="tableRef"
:height="tableHeight"
table-key="pl-quota-cert"
v-loading="loading"
:data="pageRows"
stripe
row-key="id"
class="list-table"
@row-dblclick="view"
>
<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 === 'certNo'" type="primary" @click="view(row)">{{
row.certNo
}}</el-link
><el-tag
v-else-if="column.prop === 'status'"
size="small"
:type="tagType(row.status)"
>{{ statusLabels[row.status as keyof typeof statusLabels] }}</el-tag
><span v-else-if="column.type === 'number'"
>{{ row[column.prop] }} {{ row.quantityUnit }}</span
><span v-else>{{ row[column.prop] || '—' }}</span></template
></el-table-column
><el-table-column label="操作" min-width="320" fixed="right"
><template #default="{ row }">
<el-button v-if="canEdit" size="small" @click="edit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button
v-if="canEdit && row.status !== 'void'"
size="small"
@click="openConfirm(row)"
><el-icon><Check /></el-icon
>{{ row.openingConfirmed ? '复核' : '期初确认' }}</el-button
>
<el-button size="small" @click="showContracts(row)"
><el-icon><Connection /></el-icon>关联合同</el-button
>
</template></el-table-column
>
</ListTable></div
>
<el-table-column label="证件编号" min-width="190"
><template #default="{ row }"
><el-link type="primary" @click="view(row)">{{ row.certNo }}</el-link></template
></el-table-column
>
<el-table-column prop="productNameOnCert" label="证面品名" min-width="150" />
<el-table-column label="状态" min-width="100"
><template #default="{ row }"
><el-tag size="small" :type="tagType(row.status)">{{
statusLabels[row.status as keyof typeof statusLabels]
}}</el-tag></template
></el-table-column
>
<el-table-column
v-for="column in qtyColumns"
:key="column.key"
:label="column.label"
min-width="130"
><template #default="{ row }"
>{{ row[column.key] }} {{ row.quantityUnit }}</template
></el-table-column
>
<el-table-column prop="validTo" label="有效期至" min-width="130" />
<el-table-column label="操作" min-width="320" fixed="right"
><template #default="{ row }">
<el-button v-if="canEdit" size="small" @click="edit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button
v-if="canEdit && row.status !== 'void'"
size="small"
@click="openConfirm(row)"
><el-icon><Check /></el-icon
>{{ row.openingConfirmed ? '复核' : '期初确认' }}</el-button
>
<el-button size="small" @click="showContracts(row)"
><el-icon><Connection /></el-icon>关联合同</el-button
>
</template></el-table-column
>
</el-table>
<div class="pagination list-pager-bar"
<div ref="pagerRef" class="pagination list-pager-bar"
><el-pagination
v-model:current-page="page"
:page-size="20"
:total="rows.length"
:total="filteredRows.length"
layout="total, prev, pager, next"
/></div>
</div>
@@ -359,6 +355,16 @@
</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
} from '@/composables/useClientColumnFilters'
import QuotaHistoryDialog from './QuotaHistoryDialog.vue'
const historyDialog = ref<InstanceType<typeof QuotaHistoryDialog>>()
import {
@@ -369,9 +375,9 @@
Document,
Edit,
Plus,
RefreshLeft,
Search
RefreshLeft
} from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { fetchGetMenuCodes } from '@/api/auth'
import {
quotaCertApi,
@@ -392,7 +398,6 @@
const route = useRoute()
const kind = ref<CertType>('purchase')
const keyword = ref(''),
status = ref(''),
page = ref(1)
const rows = ref<QuotaCert[]>([]),
purchases = ref<QuotaCert[]>([])
@@ -425,7 +430,7 @@
...commonFields,
...(kind.value === 'purchase' ? purchaseFields : transportFields)
])
const pageRows = computed(() => rows.value.slice((page.value - 1) * 20, page.value * 20))
const statusLabels = { draft: '待核对', active: '已启用', suspended: '已停用', void: '已作废' }
const refLabels = {
purchase_permit: '购买许可证',
@@ -454,23 +459,20 @@
].map((k) => [k, [{ required: true, message: '请填写此项', trigger: 'blur' }]])
)
)
let listEpoch = 0
let disposed = false
async function load() {
if (disposed) return
const current = ++listEpoch
loading.value = true
try {
rows.value = await quotaCertApi.list(kind.value, {
keyword: keyword.value,
status: status.value || undefined
})
page.value = 1
const result = await quotaCertApi.list(kind.value)
if (current !== listEpoch || disposed) return
rows.value = result
} finally {
loading.value = false
if (current === listEpoch && !disposed) loading.value = false
}
}
function reset() {
keyword.value = ''
status.value = ''
load()
}
let editSession = 0
watch(
formVisible,
@@ -482,10 +484,19 @@
},
{ flush: 'sync' }
)
watch(kind, () => {
editSession++
formVisible.value = false
})
watch(
kind,
() => {
editSession++
formVisible.value = false
detailVisible.value = false
confirmVisible.value = false
rows.value = []
page.value = 1
void load()
},
{ flush: 'sync' }
)
async function edit(row?: QuotaCert) {
if (saving.value) return
const session = ++editSession
@@ -611,11 +622,108 @@
saving.value = false
}
}
onMounted(async () => {
const user = useUserStore()
let authEpoch = 0
async function init() {
const current = ++authEpoch
const p = await fetchGetMenuCodes()
if (current !== authEpoch || disposed) return
canEdit.value =
!!p.all || !!p.isSuperAdmin || (p.menuCodes || []).includes('pl:quota-cert:edit')
await load()
if (typeof route.query.contractId === 'string') scopeDialog.value?.open(route.query.contractId)
if (current === authEpoch && !disposed && typeof route.query.contractId === 'string')
scopeDialog.value?.open(route.query.contractId)
}
watch(
() => JSON.stringify([user.accessToken, user.getUserInfo]),
() => {
authEpoch++
listEpoch++
rows.value = []
page.value = 1
loading.value = false
canEdit.value = false
editSession++
formVisible.value = false
detailVisible.value = false
confirmVisible.value = false
if (user.accessToken) void init()
},
{ flush: 'sync' }
)
onMounted(init)
onBeforeUnmount(() => {
disposed = true
listEpoch++
authEpoch++
})
const listColumnDefs: ColumnFilterDef[] = [
{ prop: 'certNo', label: '证件编号', type: 'text' },
{ prop: 'productNameOnCert', label: '证面品名', type: 'text' },
{
prop: 'status',
label: '状态',
type: 'enum',
enumOptions: Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
},
...qtyColumns.map((c) => ({ prop: c.key, label: c.label, type: 'number' as const })),
{ prop: 'validTo', label: '有效期至', type: 'datetime' }
]
const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns(
'pl-quota-cert-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.certNo).includes(appliedKeyword.value))
)
const {
filters,
filteredRows,
collectUniqueOptions,
hasActiveFilters,
setFilterState,
clearAllFilters,
clearFilter,
isFilterActive
} = useClientColumnFilters(searchedRows, listColumnDefs)
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
})
</script>