feat(pl): 按运输段产品维护合法使用证明并隔离异步会话。
This commit is contained in:
1 parent
722076fb3f
commit
4cb2d88dab
8 files changed
+1103
-404
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user