style(admin): unify dialog layouts and dense readonly presentation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onActivated, onMounted, ref, onBeforeUnmount } from 'vue'
|
||||
import { computed, onActivated, onMounted, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { CheckCircle2, Eye, FileUp, Undo2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -14,6 +14,7 @@ import { downloadRelabelCompletionPhoto, downloadRelabelPdf, getRelabelOrder, up
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LineClampText from '@/shared/components/LineClampText.vue'
|
||||
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
|
||||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||||
|
||||
@@ -31,28 +32,28 @@ async function voidOrder() {
|
||||
if (voiding.value || confirming.value || returning.value || !canVoid.value || !access.can(permission.relabelOrderStatus)) return
|
||||
voiding.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm('确认作废当前换标单?作废后停止换标作业,保留原单和操作记录。', '作废换标单', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' })
|
||||
await ElMessageBox.confirm(`换标单 ${detail.value?.application.relabelNo || orderId.value}:确认作废?作废后停止换标作业,保留原单和操作记录。`, '作废换标单', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' })
|
||||
await changeRelabelOrderStatus(orderId.value, 'CANCELLED', '平台作废换标单')
|
||||
ElMessage.success('换标单已作废'); await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标单作废失败,请重试').message) }
|
||||
finally { voiding.value = false }
|
||||
}
|
||||
async function confirmCompletion() {
|
||||
if (confirming.value || voiding.value) return
|
||||
if (confirming.value || returning.value || voiding.value) return
|
||||
confirming.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续后续流程吗?', '换标完成确认', { customClass: 'admin-density-confirm', confirmButtonText: '确认并继续', cancelButtonText: '取消' })
|
||||
await ElMessageBox.confirm(`换标单 ${detail.value?.application.relabelNo || orderId.value}:确认仓库上传的换标结果无误并继续后续流程吗?`, '换标完成确认', { customClass: 'admin-density-confirm', confirmButtonText: '确认并继续', cancelButtonText: '取消' })
|
||||
await confirmRelabelCompletion(orderId.value)
|
||||
ElMessage.success('已确认换标完成,订单已进入后续流程'); await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标完成确认失败,请刷新后重试').message) }
|
||||
finally { confirming.value = false }
|
||||
}
|
||||
async function returnForRework() {
|
||||
if (returning.value || voiding.value) return
|
||||
if (returning.value || confirming.value || voiding.value) return
|
||||
returning.value = true
|
||||
try {
|
||||
const task = await getRelabelReviewTask(orderId.value)
|
||||
const { value } = await ElMessageBox.prompt('填写退回原因,PDA 将展示此备注并重新逐箱作业、上传照片。', '退回重新贴标', { customClass: 'admin-density-confirm', inputType: 'textarea', inputValidator: (value) => Boolean(value?.trim()) && value.trim().length <= 1000 || '请填写 1 至 1000 字退回原因', confirmButtonText: '确认退回' })
|
||||
const { value } = await ElMessageBox.prompt(`换标单 ${detail.value?.application.relabelNo || orderId.value}:填写退回原因,PDA 将展示此备注并重新逐箱作业、上传照片。`, '退回重新贴标', { customClass: 'admin-density-confirm', inputType: 'textarea', inputValidator: (value) => Boolean(value?.trim()) && value.trim().length <= 1000 || '请填写 1 至 1000 字退回原因', confirmButtonText: '确认退回' })
|
||||
await returnRelabelForRework(orderId.value, task.version, value.trim(), crypto.randomUUID())
|
||||
ElMessage.success('已退回 PDA 重新贴标'); await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '退回失败,请刷新后重试').message) }
|
||||
@@ -63,6 +64,16 @@ const uploading = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('')
|
||||
const previewOpen = ref(false)
|
||||
const previewLoading = ref(false), previewError = ref<ApiErrorDetails | null>(null), uploadError = ref<ApiErrorDetails | null>(null)
|
||||
const previewRequest = ref<{ no: string; fileId: string; name: string; scope: 'TOTAL' | 'ITEM'; itemId?: string } | null>(null)
|
||||
let previewGeneration = 0
|
||||
function resetPreview() {
|
||||
previewGeneration++; previewLoading.value = false; previewError.value = null; previewRequest.value = null
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''
|
||||
}
|
||||
watch(previewOpen, visible => { if (!visible) resetPreview() })
|
||||
watch(orderId, () => { previewOpen.value = false; resetPreview() })
|
||||
const groups = [
|
||||
{ title: '单据信息', fields: ['换标单号', '关联订单', '状态', '资金状态', '创建时间', '更新时间'] },
|
||||
{ title: '商品信息', fields: ['商品SKU', '中文品名', '箱数', '预算总价'] },
|
||||
@@ -115,21 +126,30 @@ async function load() {
|
||||
function back() { void router.push({ name: 'overseas-relabel-orders' }) }
|
||||
onMounted(load)
|
||||
onActivated(() => { if (!loading.value) void load() })
|
||||
onBeforeUnmount(() => { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value) })
|
||||
onBeforeUnmount(resetPreview)
|
||||
async function uploadPdf(event: Event, scope: 'TOTAL' | 'ITEM', itemId?: string) {
|
||||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||||
if (!file || !detail.value) return
|
||||
if (!file || !detail.value || uploading.value || !access.can(permission.relabelOrderCreate)) return
|
||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { ElMessage.warning('只支持PDF文件'); return }
|
||||
if (file.size > 50 * 1024 * 1024) { ElMessage.warning('PDF文件不能超过50MB'); return }
|
||||
uploading.value = true
|
||||
try { detail.value = await uploadRelabelPdf(orderId.value, file, scope, itemId); ElMessage.success(scope === 'TOTAL' ? '总PDF上传成功' : '明细PDF上传成功') }
|
||||
catch (error) { ElMessage.error(getApiErrorDetails(error, 'PDF上传失败').message) }
|
||||
uploading.value = true; uploadError.value = null
|
||||
const no = orderId.value
|
||||
try { const result = await uploadRelabelPdf(no, file, scope, itemId); if (no === orderId.value) { detail.value = result; ElMessage.success(scope === 'TOTAL' ? '总PDF上传成功' : '明细PDF上传成功') } }
|
||||
catch (error) { if (no === orderId.value) uploadError.value = getApiErrorDetails(error, 'PDF上传失败') }
|
||||
finally { uploading.value = false }
|
||||
}
|
||||
async function previewPdf(fileId: string | null | undefined, name: string, scope: 'TOTAL' | 'ITEM', itemId?: string) {
|
||||
if (!fileId) return
|
||||
try { const blob = await downloadRelabelPdf(orderId.value, fileId, scope, itemId); if (previewUrl.value) URL.revokeObjectURL(previewUrl.value); previewUrl.value = URL.createObjectURL(blob); previewTitle.value = name; previewOpen.value = true }
|
||||
catch (error) { ElMessage.error(getApiErrorDetails(error, 'PDF预览加载失败').message) }
|
||||
const sequence = ++previewGeneration, no = orderId.value
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''; previewTitle.value = name; previewRequest.value = { no, fileId, name, scope, itemId }
|
||||
previewOpen.value = true; previewLoading.value = true; previewError.value = null
|
||||
try {
|
||||
const blob = await downloadRelabelPdf(no, fileId, scope, itemId)
|
||||
if (sequence !== previewGeneration || no !== orderId.value || !previewOpen.value) return
|
||||
previewUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) { if (sequence === previewGeneration && no === orderId.value && previewOpen.value) previewError.value = getApiErrorDetails(error, 'PDF预览加载失败') }
|
||||
finally { if (sequence === previewGeneration) previewLoading.value = false }
|
||||
}
|
||||
async function previewCompletionPhoto(fileId: string) {
|
||||
try { const blob = await downloadRelabelCompletionPhoto(orderId.value, fileId); const url = URL.createObjectURL(blob); window.open(url, '_blank', 'noopener,noreferrer'); window.setTimeout(() => URL.revokeObjectURL(url), 60_000) }
|
||||
@@ -142,16 +162,17 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
<template #state><LoadErrorState v-if="loadError" title="换标单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<template #content-after>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && canVoid" class="relabel-document-panel relabel-review-actions"><el-button type="danger" :loading="voiding" :disabled="confirming || returning" @click="voidOrder">作废</el-button></section>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(detail?.application.status || '') && detail?.completionEvidence?.photoFileIds?.length" class="relabel-document-panel relabel-review-actions"><el-button v-if="detail?.application.status === 'CUSTOMER_CONFIRM_PENDING'" type="primary" :loading="confirming" @click="confirmCompletion"><CheckCircle2 :size="15" />确认换标完成</el-button><el-button type="warning" :loading="returning" @click="returnForRework"><Undo2 :size="15" />退回重新贴标</el-button></section>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(detail?.application.status || '') && detail?.completionEvidence?.photoFileIds?.length" class="relabel-document-panel relabel-review-actions"><el-button v-if="detail?.application.status === 'CUSTOMER_CONFIRM_PENDING'" type="primary" :loading="confirming" :disabled="returning || voiding" @click="confirmCompletion"><CheckCircle2 :size="15" />确认换标完成</el-button><el-button type="warning" :loading="returning" :disabled="confirming || voiding" @click="returnForRework"><Undo2 :size="15" />退回重新贴标</el-button></section>
|
||||
<LoadErrorState v-if="uploadError" title="换标PDF操作失败" :message="uploadError.message" :show-retry="false"><template #details><ApiErrorMeta :details="uploadError" /></template></LoadErrorState>
|
||||
<RelabelDocumentsPanel v-if="detail" :relabel-no="detail.application.relabelNo" :detail="detail" /><section class="relabel-document-panel" aria-label="PDA换标完工证据"><div class="relabel-mark-list__head"><h2>PDA 换标完工证据</h2><span>{{ detail?.completionEvidence?.reviewerName ? `复核人:${detail.completionEvidence.reviewerName}` : '等待仓库作业' }}</span></div><div v-if="detail?.completionEvidence?.photoFileIds?.length" class="document-row"><strong>共 {{ detail.completionEvidence.photoFileIds.length }} 张完工照片</strong><span class="document-actions density-actions"><el-button class="density-action" data-tone="neutral" v-for="(fileId, index) in detail.completionEvidence.photoFileIds" :key="fileId" size="small" @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></span></div><div v-else class="attachment-empty">暂未上传完工照片</div></section>
|
||||
<section class="relabel-document-panel" aria-label="总换标文件"><div class="relabel-mark-list__head"><h2>总换标 PDF</h2><label class="file-picker"><FileUp :size="15" />{{ uploading ? '上传中' : (detail?.application.totalFileName ? '更换PDF' : '上传PDF') }}<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'TOTAL')" /></label></div><div v-if="detail?.application.totalFileId" class="document-row"><strong>{{ detail.application.totalFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(detail.application.totalFileId, detail.application.totalFileName || '总换标PDF', 'TOTAL')"><Eye :size="14" />直接预览</el-button></div><div v-else class="attachment-empty">暂未上传总换标PDF</div></section>
|
||||
<section class="relabel-document-panel" aria-label="总换标文件"><div class="relabel-mark-list__head"><h2>总换标 PDF</h2><label v-if="access.can(permission.relabelOrderCreate)" class="file-picker"><FileUp :size="15" />{{ uploading ? '上传中' : (detail?.application.totalFileName ? '更换PDF' : '上传PDF') }}<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'TOTAL')" /></label></div><div v-if="detail?.application.totalFileId" class="document-row"><strong>{{ detail.application.totalFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(detail.application.totalFileId, detail.application.totalFileName || '总换标PDF', 'TOTAL')"><Eye :size="14" />直接预览</el-button></div><div v-else class="attachment-empty">暂未上传总换标PDF</div></section>
|
||||
<section class="relabel-mark-list" aria-label="箱唛换标信息">
|
||||
<div class="relabel-mark-list__head"><h2>箱唛换标信息</h2><span>共 {{ detail?.items.length || 0 }} 条</span></div>
|
||||
<div class="relabel-mark-list__scroll">
|
||||
<table class="density-table">
|
||||
<thead><tr><th>我方箱唛</th><th>FBA 箱唛</th><th>商品 SKU</th><th>中文品名</th><th>FBA 货件编号</th><th>换标新 SKU</th><th>目的仓</th><th>明细PDF</th></tr></thead>
|
||||
<thead><tr><th class="relabel-pdf-actions">操作(明细PDF)</th><th>我方箱唛</th><th>FBA 箱唛</th><th>商品 SKU</th><th>中文品名</th><th>FBA 货件编号</th><th>换标新 SKU</th><th>目的仓</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in detail?.items || []" :key="item.id"><td class="mark-code">{{ text(item.cartonMark) }}</td><td class="mark-code">{{ text(item.fbaCartonMark) }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ text(item.productNameZh) }}</td><td>{{ text(item.shipmentNo) }}</td><td><template v-if="item.skuItems?.length"><div v-for="(sku, index) in item.skuItems" :key="index">{{ sku.skuCode }} → {{ sku.newSkuCode }} × {{ sku.quantity }} 件</div></template><span v-else>{{ text(item.newSkuCode) }}</span></td><td>{{ text(item.destinationFbaWarehouseCode || item.destinationWarehouseId) }}</td><td><span v-if="item.relabelFileName" class="document-actions density-actions"><strong>{{ item.relabelFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(item.relabelFileId, item.relabelFileName, 'ITEM', item.id)">预览</el-button></span><label v-else class="inline-file-picker"><FileUp :size="14" />上传<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'ITEM', item.id)" /></label></td></tr>
|
||||
<tr v-for="item in detail?.items || []" :key="item.id"><td><span v-if="item.relabelFileName" class="document-actions density-actions"><LineClampText :content="item.relabelFileName" /><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(item.relabelFileId, item.relabelFileName, 'ITEM', item.id)">预览</el-button></span><label v-else-if="access.can(permission.relabelOrderCreate)" class="inline-file-picker"><FileUp :size="14" />上传<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'ITEM', item.id)" /></label><span v-else>—</span></td><td class="mark-code">{{ text(item.cartonMark) }}</td><td class="mark-code">{{ text(item.fbaCartonMark) }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ text(item.productNameZh) }}</td><td>{{ text(item.shipmentNo) }}</td><td><template v-if="item.skuItems?.length"><div v-for="(sku, index) in item.skuItems" :key="index">{{ sku.skuCode }} → {{ sku.newSkuCode }} × {{ sku.quantity }} 件</div></template><span v-else>{{ text(item.newSkuCode) }}</span></td><td>{{ text(item.destinationFbaWarehouseCode) }}</td></tr>
|
||||
<tr v-if="!detail?.items.length"><td class="empty-cell" colspan="8">暂无箱唛换标信息</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -159,7 +180,7 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
</section>
|
||||
</template>
|
||||
</OverseasReadOnlyDetail>
|
||||
<AppDialog class="admin-density-dialog" v-if="previewUrl" v-model="previewOpen" :title="previewTitle" width="min(900px, calc(100vw - 28px))" :show-footer="false"><div class="pdf-preview"><iframe :src="previewUrl" :title="`${previewTitle}预览`" /></div></AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="previewOpen" :title="(previewRequest?.no || orderId) + ' · ' + previewTitle" width="min(900px, calc(100vw - 28px))" fullscreenable :loading="previewLoading" :show-footer="false"><div class="density-dialog-sections"><p v-if="previewLoading" class="density-dialog-note" role="status">正在读取换标PDF…</p><LoadErrorState v-if="previewError" title="换标PDF预览失败" :message="previewError.message" :loading="previewLoading" @retry="previewRequest && previewPdf(previewRequest.fileId, previewRequest.name, previewRequest.scope, previewRequest.itemId)"><template #details><ApiErrorMeta :details="previewError" /></template></LoadErrorState><div v-if="previewUrl" class="pdf-preview"><iframe :src="previewUrl" :title="`${previewTitle}预览`" /></div></div></AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -175,8 +196,10 @@ table { min-width: 1120px; }
|
||||
.document-row { display:flex; align-items:center; justify-content:space-between; gap:var(--admin-space-5); padding:var(--admin-space-5); border:1px solid var(--cc-border); border-radius:var(--admin-radius); }
|
||||
.file-picker, .inline-file-picker { display:inline-flex; align-items:center; gap:var(--admin-space-2); color:var(--el-color-primary); cursor:pointer; }
|
||||
.file-picker input, .inline-file-picker input { display:none; }
|
||||
.pdf-preview { overflow:hidden; border:1px solid var(--cc-border); border-radius:var(--admin-radius); background:#f5f7fa; }
|
||||
.pdf-preview iframe { display:block; width:100%; height:min(72vh,760px); border:0; background:#fff; }
|
||||
.pdf-preview { overflow: hidden; }
|
||||
.pdf-preview iframe { display:block; width:100%; height:min(72vh,760px); border:0; }
|
||||
.relabel-pdf-actions { width: 220px; }
|
||||
.document-actions > .line-clamp-text { max-width: 160px; }
|
||||
|
||||
.empty-cell { height: var(--admin-empty-height); color: var(--cc-muted); text-align: center; }
|
||||
.document-row strong { min-width: 0; overflow-wrap: anywhere; }
|
||||
|
||||
Reference in New Issue
Block a user