fix(admin): render container documents as table
This commit is contained in:
@@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
overview: ContainerOverview
|
||||
canManageDocuments: boolean
|
||||
canManageCustoms: boolean
|
||||
canDownloadCustoms?: boolean
|
||||
canManageException: boolean
|
||||
documentBusy?: boolean
|
||||
customsCompleting?: boolean
|
||||
@@ -30,10 +31,17 @@ const documentTypes = [
|
||||
] as const
|
||||
|
||||
const documentsByType = computed(() => new Map(props.overview.documents.map((item) => [item.documentType, item])))
|
||||
const downloadableTypes = new Set(['DELIVERY_ORDER', 'PICKUP_ORDER'])
|
||||
const containerDownloadableTypes = new Set(['DELIVERY_ORDER', 'PICKUP_ORDER'])
|
||||
const customsTypes = new Set(['DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION'])
|
||||
function canDownloadDocument(documentType: string) {
|
||||
return containerDownloadableTypes.has(documentType) || (documentType === 'CUSTOMS_DECLARATION' && props.canDownloadCustoms)
|
||||
}
|
||||
function canUploadDocument(documentType: string) {
|
||||
return downloadableTypes.has(documentType) ? props.canManageDocuments : customsTypes.has(documentType) && props.canManageCustoms
|
||||
return containerDownloadableTypes.has(documentType) ? props.canManageDocuments : customsTypes.has(documentType) && props.canManageCustoms
|
||||
}
|
||||
function hasDocumentAction(documentType: string) {
|
||||
const document = documentsByType.value.get(documentType)
|
||||
return Boolean((document && canDownloadDocument(documentType)) || (canUploadDocument(documentType) && !document?.completed))
|
||||
}
|
||||
function documentStatusText(documentType: string) {
|
||||
const document = documentsByType.value.get(documentType)
|
||||
@@ -41,6 +49,16 @@ function documentStatusText(documentType: string) {
|
||||
if (!document) return '第一步上传附件,第二步确认报关完成'
|
||||
return document.completed ? '已确认报关完成' : '附件已上传,待确认报关完成'
|
||||
}
|
||||
function documentStatusLabel(documentType: string) {
|
||||
const document = documentsByType.value.get(documentType)
|
||||
if (!document) return '未上传'
|
||||
return document.completed ? '已完成' : '已上传'
|
||||
}
|
||||
function documentStatusType(documentType: string) {
|
||||
const document = documentsByType.value.get(documentType)
|
||||
if (!document) return 'info'
|
||||
return document.completed ? 'success' : 'primary'
|
||||
}
|
||||
const pdaProgress = computed(() => {
|
||||
const pda = props.overview.pda
|
||||
if (!pda || pda.itemCount <= 0) return 0
|
||||
@@ -113,11 +131,18 @@ onBeforeUnmount(() => { photoGeneration += 1; revokePhotoUrls() })
|
||||
|
||||
<section class="overview-section" aria-label="货柜业务资料">
|
||||
<header><h3>业务资料</h3><span>{{ overview.documents.length }} 份</span></header>
|
||||
<div class="document-grid compact-document-list">
|
||||
<article v-for="item in documentTypes" :key="item.type" class="document-item">
|
||||
<div><strong>{{ item.label }}</strong><span>{{ documentsByType.get(item.type)?.fileName || '未上传' }}</span><small>{{ documentStatusText(item.type) }}</small></div>
|
||||
<div class="document-actions"><el-button v-if="documentsByType.get(item.type) && downloadableTypes.has(item.type)" size="small" :loading="documentBusy" @click="emit('downloadDocument', documentsByType.get(item.type)!)"><Download :size="15" />下载</el-button><el-button v-if="canUploadDocument(item.type)" size="small" @click="emit('uploadDocument', item.type)"><FileUp :size="15" />{{ documentsByType.get(item.type) ? '更新附件' : '上传附件' }}</el-button><el-button v-if="item.type === 'CUSTOMS_DECLARATION' && canManageCustoms && documentsByType.get(item.type) && !documentsByType.get(item.type)?.completed" size="small" type="primary" :loading="customsCompleting" @click="emit('completeCustoms', documentsByType.get(item.type)!)"><Check :size="15" />确认报关完成</el-button></div>
|
||||
</article>
|
||||
<div class="document-table-wrap">
|
||||
<table class="document-table">
|
||||
<thead><tr><th class="document-table__actions">操作</th><th>资料类型</th><th>文件名</th><th>上传人</th><th>上传时间</th><th>状态</th></tr></thead>
|
||||
<tbody><tr v-for="item in documentTypes" :key="item.type">
|
||||
<td class="document-table__actions"><div class="document-actions"><el-button v-if="documentsByType.get(item.type) && canDownloadDocument(item.type)" size="small" :loading="documentBusy" @click="emit('downloadDocument', documentsByType.get(item.type)!)"><Download :size="15" />下载</el-button><el-button v-if="canUploadDocument(item.type) && !documentsByType.get(item.type)?.completed" size="small" @click="emit('uploadDocument', item.type)"><FileUp :size="15" />{{ documentsByType.get(item.type) ? '更新附件' : '上传附件' }}</el-button><el-button v-if="item.type === 'CUSTOMS_DECLARATION' && canManageCustoms && documentsByType.get(item.type) && !documentsByType.get(item.type)?.completed" size="small" type="primary" :loading="customsCompleting" @click="emit('completeCustoms', documentsByType.get(item.type)!)"><Check :size="15" />确认报关完成</el-button><span v-if="!hasDocumentAction(item.type)" class="no-action">—</span></div></td>
|
||||
<td><strong>{{ item.label }}</strong><small v-if="item.type === 'CUSTOMS_DECLARATION'">{{ documentStatusText(item.type) }}</small></td>
|
||||
<td><span class="document-file-name" :title="documentsByType.get(item.type)?.fileName || undefined">{{ documentsByType.get(item.type)?.fileName || '—' }}</span></td>
|
||||
<td>{{ documentsByType.get(item.type)?.uploadedByName || '—' }}</td>
|
||||
<td>{{ documentsByType.get(item.type) ? formatTime(documentsByType.get(item.type)?.uploadedAt) : '—' }}</td>
|
||||
<td><el-tag size="small" :type="documentStatusType(item.type)">{{ documentStatusLabel(item.type) }}</el-tag></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -148,18 +173,12 @@ onBeforeUnmount(() => { photoGeneration += 1; revokePhotoUrls() })
|
||||
.overview-section h3 { margin: 0; font-size: 15px; }
|
||||
.overview-section header > span, .overview-section small { color: var(--cc-muted); font-size: 12px; }
|
||||
.overview-section p { margin: 5px 0; color: var(--cc-muted); font-size: 13px; }
|
||||
.document-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
.document-item { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border-top: 1px solid var(--cc-border); }
|
||||
.document-item:nth-child(-n+2){border-top:0}.compact-document-list{column-gap:18px;row-gap:0}
|
||||
.document-item > div:first-child { display: grid; min-width: 0; gap: 4px; }
|
||||
.document-item span { overflow: hidden; color: var(--cc-muted); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.document-actions { display: flex; flex-shrink: 0; gap: 6px; }
|
||||
.document-table-wrap{width:100%;overflow-x:auto;border:1px solid var(--cc-border)}.document-table{width:100%;min-width:940px;border-collapse:collapse;table-layout:fixed;font-size:12px}.document-table th,.document-table td{height:40px;padding:5px 10px;border-bottom:1px solid var(--cc-border);background:#fff;text-align:left;vertical-align:middle}.document-table tbody tr:last-child td{border-bottom:0}.document-table th{background:var(--cc-bg-subtle,#f7f8fa);color:var(--cc-muted);font-weight:650}.document-table th:nth-child(1){width:260px}.document-table th:nth-child(2){width:180px}.document-table th:nth-child(3){width:210px}.document-table th:nth-child(4){width:120px}.document-table th:nth-child(5){width:150px}.document-table th:nth-child(6){width:90px}.document-table__actions{position:sticky;left:0;z-index:1;box-shadow:1px 0 0 var(--cc-border)}.document-table th.document-table__actions{z-index:2}.document-table td strong{font-size:13px}.document-table td small{display:block;overflow:hidden;max-width:170px;margin-top:2px;text-overflow:ellipsis;white-space:nowrap}.document-file-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.document-actions{display:flex;align-items:center;gap:5px;white-space:nowrap}.document-actions :deep(.el-button){margin-left:0}.no-action{color:var(--cc-muted)}
|
||||
.pda-summary{display:grid;grid-template-columns:minmax(220px,1fr) minmax(0,2fr);align-items:center;gap:16px;margin-bottom:10px}.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; margin:0; }
|
||||
.metric-grid > div { display: grid; gap: 2px; padding: 6px 9px; background: var(--cc-bg-subtle, #f7f8fa); }
|
||||
.metric-grid span { color: var(--cc-muted); font-size: 12px; }
|
||||
.metric-grid strong { font-size: 18px; }
|
||||
.photo-gallery{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:10px 0}.photo-thumb{display:grid;min-width:0;overflow:hidden;padding:0;border:1px solid var(--cc-border);background:#fff;color:inherit;text-align:left;cursor:pointer}.photo-thumb:disabled{cursor:default}.photo-thumb img,.photo-placeholder{width:100%;aspect-ratio:4/3;object-fit:cover;background:var(--cc-bg-subtle,#f7f8fa)}.photo-placeholder{display:grid;place-items:center;padding:8px;color:var(--cc-muted);font-size:12px}.photo-placeholder--error{color:var(--el-color-danger)}.photo-caption{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 8px}.photo-caption small{white-space:nowrap}.compact-empty{margin:6px 0;color:var(--cc-muted);font-size:12px}.compact-timeline{display:grid;margin:0;padding:0;list-style:none}.compact-timeline li{display:grid;grid-template-columns:130px 130px 110px minmax(0,1fr);gap:8px;padding:6px 0;border-top:1px solid var(--cc-border);font-size:12px}.compact-timeline li:first-child{border-top:0}.compact-timeline time,.compact-timeline span,.compact-timeline small{overflow:hidden;color:var(--cc-muted);text-overflow:ellipsis;white-space:nowrap}
|
||||
@media (max-width: 1200px){.photo-gallery{grid-template-columns:repeat(4,minmax(0,1fr))}.pda-summary{grid-template-columns:1fr}.compact-timeline li{grid-template-columns:120px 120px minmax(0,1fr)}.compact-timeline small{grid-column:2/-1}}
|
||||
@media (max-width: 720px) { .document-grid { grid-template-columns: 1fr; }.document-item:nth-child(2){border-top:1px solid var(--cc-border)} .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.photo-gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.compact-timeline li{grid-template-columns:1fr 1fr}.compact-timeline span,.compact-timeline small{grid-column:auto;white-space:normal} }
|
||||
@media (max-width: 420px) { .document-item { align-items: stretch; flex-direction: column; } .document-actions :deep(.el-button) { flex: 1; } }
|
||||
@media (max-width: 720px) { .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.photo-gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.compact-timeline li{grid-template-columns:1fr 1fr}.compact-timeline span,.compact-timeline small{grid-column:auto;white-space:normal} }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user