fix(pl): 统一计划用证三页的检索工具栏与漏斗以保持筛选和分页一致。
This commit is contained in:
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user