159 lines
12 KiB
Vue
159 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminDensity.css'
|
|
import '@/styles/adminWorkbench.css'
|
|
import type { InboundDropshipPallet, InboundFileSummary, InboundInternalNote, InboundPalletLocation, InboundStatusHistory } from './types'
|
|
import { inboundHistoryActionLabel, inboundProcessStatusLabels, inboundStatusLabel } from '../status'
|
|
import { downloadDropshipPalletLabel } from './api'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
palletLocations: InboundPalletLocation[]
|
|
dropshipPallets?: InboundDropshipPallet[]
|
|
customerId?: string
|
|
inboundId?: string
|
|
canViewDropship?: boolean
|
|
files: InboundFileSummary[]
|
|
internalNotes: InboundInternalNote[]
|
|
statusHistory: InboundStatusHistory[]
|
|
publicRemark: string | null
|
|
sections?: Array<'dropship' | 'allocations' | 'files' | 'notes' | 'history' | 'remark'>
|
|
}>(), {
|
|
dropshipPallets: () => [],
|
|
canViewDropship: true,
|
|
sections: () => ['dropship', 'allocations', 'files', 'notes', 'history', 'remark'],
|
|
})
|
|
|
|
function shows(section: 'dropship' | 'allocations' | 'files' | 'notes' | 'history' | 'remark') {
|
|
return props.sections.includes(section)
|
|
}
|
|
|
|
function value(value: string | number | null | undefined) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
|
function number(value: number | null) { return value === null ? '—' : value.toFixed(3) }
|
|
function date(value: string | null) {
|
|
if (!value) return '—'
|
|
const parsed = new Date(value)
|
|
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString('zh-CN', { hour12: false })
|
|
}
|
|
function dropshipPalletStatus(value: string) {
|
|
return ({ OPEN: '清点组托中', SEALED: '已封托' } as Record<string, string>)[value] || value || '状态未知'
|
|
}
|
|
async function printPallet(pallet: InboundDropshipPallet) {
|
|
if (!props.customerId || !props.inboundId || !pallet.palletNo) return
|
|
try {
|
|
const blob = await downloadDropshipPalletLabel(props.customerId, props.inboundId, pallet.palletId)
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a'); link.href = url; link.download = `${pallet.palletNo}-托盘唛.pdf`; link.click()
|
|
URL.revokeObjectURL(url)
|
|
} catch { ElMessage.error('托盘唛下载失败,请稍后重试') }
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="history-panel admin-workbench">
|
|
<section v-if="shows('dropship')" class="history-section">
|
|
<div class="section-heading"><h3>一件代发清点与组托结果</h3><span>{{ dropshipPallets.length }} 个托盘</span></div>
|
|
<el-alert v-if="!canViewDropship" title="当前账号缺少一件代发 PDA 查看权限,无法读取清点组托结果。" type="warning" :closable="false" show-icon />
|
|
<div v-else-if="dropshipPallets.length" class="dropship-pallet-list">
|
|
<article v-for="pallet in dropshipPallets" :key="pallet.palletId" class="dropship-pallet-card">
|
|
<header>
|
|
<div><strong>{{ value(pallet.palletNo) }}</strong><el-tag size="small" :type="pallet.status === 'SEALED' ? 'success' : 'warning'" effect="plain">{{ dropshipPalletStatus(pallet.status) }}</el-tag></div>
|
|
<el-button v-if="pallet.palletNo" link type="primary" @click="printPallet(pallet)">打印托盘码</el-button>
|
|
</header>
|
|
<dl class="dropship-pallet-summary">
|
|
<div><dt>箱数</dt><dd>{{ pallet.cartonCount }}</dd></div>
|
|
<div><dt>SKU 数</dt><dd>{{ pallet.skuCount }}</dd></div>
|
|
<div><dt>毛重</dt><dd>{{ number(pallet.weightKg) }} kg</dd></div>
|
|
<div><dt>体积</dt><dd>{{ number(pallet.volumeCbm) }} m³</dd></div>
|
|
<div><dt>差异箱数</dt><dd><el-tag size="small" :type="pallet.discrepancyCartons ? 'warning' : 'success'">{{ pallet.discrepancyCartons }}</el-tag></dd></div>
|
|
<div><dt>封托时间</dt><dd>{{ date(pallet.sealedAt) }}</dd></div>
|
|
</dl>
|
|
<div class="dropship-item-table">
|
|
<div class="dropship-item-row dropship-item-row--header"><span>入仓码</span><span>SKU / 货物</span><span>商品数量</span><span>箱数</span><span>箱码</span></div>
|
|
<div v-for="item in pallet.items" :key="`${pallet.palletId}-${item.inboundDetailId}`" class="dropship-item-row">
|
|
<span>{{ value(item.inboundCode) }}</span>
|
|
<span><strong>{{ value(item.skuCode) }}</strong><small>{{ value(item.cargoName) }}</small></span>
|
|
<span>{{ item.skuQuantity }}</span>
|
|
<span>{{ item.cartonCount }}</span>
|
|
<span>{{ item.cartonMarks.length ? item.cartonMarks.join('、') : '—' }}</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
<p v-else class="muted-empty">暂无一件代发清点与组托结果</p>
|
|
</section>
|
|
<section v-if="shows('allocations')" class="history-section">
|
|
<div class="section-heading"><h3>托盘与库位</h3><span>{{ palletLocations.length }} 条分配</span></div>
|
|
<div v-if="palletLocations.length" class="allocation-list">
|
|
<div v-for="item in palletLocations" :key="`${item.allocationNo}-${item.palletId}-${item.locationId}`" class="allocation-row">
|
|
<div><strong>{{ value(item.palletCode) }}</strong><span>分配 {{ value(item.allocationNo) }}</span></div>
|
|
<div><span>{{ number(item.allocatedVolumeCbm) }} CBM</span><span>{{ value(item.allocatedPackageCount) }} 箱</span></div>
|
|
<div class="allocation-location">{{ value(item.locationCode) }}</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="muted-empty">暂无托盘库位分配记录</p>
|
|
</section>
|
|
<section v-if="shows('files')" class="history-section">
|
|
<div class="section-heading"><h3>文件记录</h3><span>{{ files.length }} 个文件</span></div>
|
|
<div v-if="files.length" class="simple-list"><div v-for="file in files" :key="`${file.fileId}-${file.fileVersion}`"><strong>{{ value(file.fileNameSnapshot) }}</strong><span>{{ value(file.fileType) }} · {{ date(file.uploadedAt) }}</span></div></div>
|
|
<p v-else class="muted-empty">暂无文件记录</p>
|
|
</section>
|
|
<section v-if="shows('notes')" class="history-section">
|
|
<div class="section-heading"><h3>内部备注</h3><span>{{ internalNotes.length }} 条</span></div>
|
|
<div v-if="internalNotes.length" class="simple-list"><div v-for="note in internalNotes" :key="String(note.id)"><strong>{{ value(note.createdByName || note.createdBy) }}</strong><span>{{ value(note.content) }} · {{ date(note.createdAt) }}</span></div></div>
|
|
<p v-else class="muted-empty">暂无内部备注</p>
|
|
</section>
|
|
<section v-if="shows('history')" class="history-section">
|
|
<div class="section-heading"><h3>状态历史</h3><span>{{ statusHistory.length }} 条</span></div>
|
|
<ol v-if="statusHistory.length" class="timeline"><li v-for="(history, index) in statusHistory" :key="`${history.occurredAt}-${index}`"><span class="timeline-dot" /><div><strong>{{ inboundHistoryActionLabel(history.action) }}</strong><span>{{ inboundStatusLabel(history.fromStatus, inboundProcessStatusLabels) }} → {{ inboundStatusLabel(history.toStatus, inboundProcessStatusLabels) }} · {{ value(history.operatorName) }}</span><small>{{ date(history.occurredAt) }}<template v-if="history.reason"> · {{ history.reason }}</template></small></div></li></ol>
|
|
<p v-else class="muted-empty">暂无状态历史</p>
|
|
</section>
|
|
<section v-if="shows('remark') && publicRemark" class="history-section remark-section"><div class="section-heading"><h3>公开备注</h3></div><p>{{ publicRemark }}</p></section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.history-panel { display: grid; gap: 24px; min-width: 0; }
|
|
.history-section { min-width: 0; padding-top: 16px; border-top: 1px solid var(--workbench-border); }
|
|
.history-section:first-child { padding-top: 0; border-top: 0; }
|
|
.section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
|
.section-heading h3 { margin: 0; color: #172033; font-size: 13px; }
|
|
.section-heading span { color: var(--workbench-muted); font-size: 12px; }
|
|
.allocation-list, .simple-list { overflow: hidden; border: 1px solid var(--workbench-border); border-radius: 6px; background: #fff; }
|
|
.allocation-row { display: grid; grid-template-columns: 1fr auto minmax(100px, 0.8fr); gap: 12px; align-items: center; padding: 10px 12px; border-bottom: 1px solid #edf0f4; font-size: 12px; }
|
|
.allocation-row:last-child, .simple-list > div:last-child { border-bottom: 0; }
|
|
.allocation-row > div, .simple-list > div { min-width: 0; }
|
|
.allocation-row span, .simple-list span { display: block; margin-top: 3px; color: var(--workbench-muted); }
|
|
.allocation-location { overflow-wrap: anywhere; color: var(--workbench-primary); text-align: right; }
|
|
.simple-list > div { padding: 10px 12px; border-bottom: 1px solid #edf0f4; font-size: 12px; }
|
|
.simple-list strong, .simple-list span { overflow-wrap: anywhere; }
|
|
.dropship-pallet-list { display: grid; gap: 12px; }
|
|
.dropship-pallet-card { min-width: 0; overflow: hidden; border: 1px solid var(--workbench-border); border-radius: 6px; background: #fff; }
|
|
.dropship-pallet-card > header { display: flex; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 12px; border-bottom: 1px solid #edf0f4; background: #f8fafc; }
|
|
.dropship-pallet-card > header > div { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
|
.dropship-pallet-card > header strong { overflow-wrap: anywhere; color: #172033; font-size: 13px; }
|
|
.dropship-pallet-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); margin: 0; border-bottom: 1px solid #edf0f4; }
|
|
.dropship-pallet-summary > div { min-width: 0; padding: 8px 12px; border-right: 1px solid #edf0f4; }
|
|
.dropship-pallet-summary > div:last-child { border-right: 0; }
|
|
.dropship-pallet-summary dt { color: var(--workbench-muted); font-size: 11px; }
|
|
.dropship-pallet-summary dd { margin: 3px 0 0; overflow-wrap: anywhere; color: var(--workbench-text); font-size: 12px; font-weight: 650; }
|
|
.dropship-item-table { min-width: 680px; }
|
|
.dropship-pallet-card { overflow-x: auto; }
|
|
.dropship-item-row { display: grid; grid-template-columns: minmax(140px, 1fr) minmax(180px, 1.2fr) 100px 70px minmax(220px, 1.5fr); min-width: 800px; border-bottom: 1px solid #edf0f4; }
|
|
.dropship-item-row:last-child { border-bottom: 0; }
|
|
.dropship-item-row > span { min-width: 0; padding: 8px 12px; overflow-wrap: anywhere; color: var(--workbench-text); font-size: 12px; }
|
|
.dropship-item-row > span + span { border-left: 1px solid #edf0f4; }
|
|
.dropship-item-row small { display: block; margin-top: 3px; color: var(--workbench-muted); font-size: 11px; }
|
|
.dropship-item-row--header { background: #f8fafc; }
|
|
.dropship-item-row--header > span { color: var(--workbench-muted); font-size: 11px; font-weight: 650; }
|
|
.muted-empty { margin: 0; padding: 18px 12px; border: 1px dashed var(--workbench-border); border-radius: 6px; background: #f8fafc; color: var(--workbench-muted); font-size: 12px; text-align: center; }
|
|
.timeline { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
|
.timeline li { display: grid; grid-template-columns: 16px 1fr; gap: 10px; min-width: 0; padding-bottom: 16px; }
|
|
.timeline-dot { width: 8px; height: 8px; margin-top: 5px; border: 2px solid var(--workbench-primary); border-radius: 50%; background: #fff; box-shadow: 0 0 0 3px color-mix(in srgb, var(--workbench-primary) 12%, transparent); }
|
|
.timeline li > div { min-width: 0; padding-bottom: 12px; border-bottom: 1px solid #edf0f4; }
|
|
.timeline strong, .timeline span, .timeline small { display: block; overflow-wrap: anywhere; }
|
|
.timeline span, .timeline small { margin-top: 3px; color: var(--workbench-muted); font-size: 12px; line-height: 1.5; }
|
|
.remark-section p { margin: 0; padding: 12px; border-radius: 6px; background: #f8fafc; color: var(--workbench-text); font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
|
@media (max-width: 720px) { .dropship-pallet-summary { grid-template-columns: 1fr; } .dropship-pallet-summary > div { border-right: 0; border-bottom: 1px solid #edf0f4; } .dropship-pallet-summary > div:last-child { border-bottom: 0; } }
|
|
@media (max-width: 480px) { .allocation-row { grid-template-columns: 1fr auto; } .allocation-location { grid-column: 1 / -1; text-align: left; } }
|
|
</style>
|