优化
This commit is contained in:
@@ -83,15 +83,17 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('covers real list states and keeps operation column first and left-fixed', () => {
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
|
||||
expect(source).not.toContain("{ key: 'selection', type: 'selection'")
|
||||
expect(source).toContain("key: 'selection', type: 'selection'")
|
||||
expect(source).not.toContain('批量审核接口待接入')
|
||||
expect(source).not.toContain('@selection-change="handleSelectionChange"')
|
||||
expect(source).toContain('@selection-change="handleApprovalSelectionChange"')
|
||||
expect(source).toContain('batchApprovePlatformUnifiedOrders')
|
||||
expect(source).toContain('批量审核通过')
|
||||
expect(source).toContain('title="订单列表暂时无法加载"')
|
||||
expect(source).toContain('empty-text="暂无符合条件的订单"')
|
||||
expect(source).toContain(':loading="loading"')
|
||||
expect(source).toContain('v-model:page="page"')
|
||||
expect(source).toContain('v-model:page-size="pageSize"')
|
||||
const columnSource = source.slice(source.indexOf('const columns:'), source.indexOf('function emptyFilters'))
|
||||
const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters'))
|
||||
const expectedColumnKeys = [
|
||||
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'totalPackageCount', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
@@ -218,9 +220,29 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain("value.replaceAll('_', ' ')")
|
||||
})
|
||||
|
||||
it('opens the measurement image preview at the clicked image index', () => {
|
||||
expect(source).toContain('v-for="(url, index) in measurementImageUrls"')
|
||||
expect(source).toContain(':initial-index="index"')
|
||||
it('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
|
||||
expect(source).toContain('<el-table :data="measurementDetail?.items || []" row-key="id"')
|
||||
expect(source).toContain('label="尺寸图片"')
|
||||
expect(source).toContain(':file-id="row.intelligentImageFileId"')
|
||||
expect(source).toContain('openMeasurementEvidence(row.intelligentImageFileId')
|
||||
expect(source).toContain('v-for="weightResult in measurementDetail.weightResults"')
|
||||
expect(source).toContain(':file-id="weightResult.fileId"')
|
||||
expect(source).toContain('openMeasurementEvidence(weightResult.fileId')
|
||||
expect(source).toContain('measurementDetail?.actualGrossWeightKg')
|
||||
expect(source).toContain('weightResult.grossWeightKg')
|
||||
expect(source).toContain('weightResult.netWeightKg')
|
||||
expect(source).toContain("weightResult.palletType || '未记录类型'")
|
||||
expect(source).not.toContain('weightResult.palletCode')
|
||||
expect(source).not.toContain('measurementImageUrls')
|
||||
expect(source).not.toContain('const fileIds = [...measurementDetail.value.items')
|
||||
})
|
||||
|
||||
it('keeps authenticated evidence loading, original preview, permission and error states', () => {
|
||||
expect(source).toContain('<MeasurementEvidenceThumbnail')
|
||||
expect(source).toContain("downloadWeightImage(String(summary.tenantId), String(summary.inboundId), fileId)")
|
||||
expect(source).toContain('measurementPreviewError')
|
||||
expect(source).toContain('测量原图暂时无法读取')
|
||||
expect(source).toContain("can('platform:firstleg:inbound:measure:view')")
|
||||
})
|
||||
|
||||
it('shows parent-child markers only for related rows and keeps the aggregate in detail', () => {
|
||||
@@ -289,7 +311,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
it('matches the prototype table grid and distinguishes container identifiers', () => {
|
||||
expect(source).toContain('row-key="orderId" border stripe')
|
||||
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe')
|
||||
expect(source.match(/<el-table :data="measurementDetail\?\.items \|\| \[\]" border stripe/g)).toHaveLength(2)
|
||||
expect(source.match(/<el-table :data="measurementDetail\?\.items \|\| \[\]"[^>]*border stripe/g)).toHaveLength(2)
|
||||
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe empty-text="暂无客户下单数据"')
|
||||
expect(source).toContain('--el-table-border-color: #e5e7eb')
|
||||
expect(source).toContain('--el-table-row-hover-bg-color: #f3f7fb')
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
approvePlatformUnifiedOrder,
|
||||
batchApprovePlatformUnifiedOrders,
|
||||
addPlatformUnifiedOrderNote,
|
||||
listPlatformUnifiedOrders,
|
||||
unifiedOrderStatusOptions,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
downloadWeightImage,
|
||||
getInboundMeasurement,
|
||||
InboundMeasurementDialog,
|
||||
MeasurementEvidenceThumbnail,
|
||||
type InboundMeasurementDetail,
|
||||
} from '@/modules/inbounds'
|
||||
import {
|
||||
@@ -103,6 +105,8 @@ const activeDetailTab = ref('basic')
|
||||
const approvalOpen = ref(false)
|
||||
const approvalLoadingId = ref('')
|
||||
const approvalSaving = ref(false)
|
||||
const batchApprovalSaving = ref(false)
|
||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||
const approvalOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const noteOpen = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
@@ -112,14 +116,21 @@ const relationOpen = ref(false)
|
||||
const relationOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const measurementDetail = ref<InboundMeasurementDetail | null>(null)
|
||||
const measurementLoading = ref(false)
|
||||
const measurementError = ref<ApiErrorDetails | null>(null)
|
||||
const measurementLoadedKey = ref('')
|
||||
const measurementImageUrls = ref<string[]>([])
|
||||
const measurementPreviewOpen = ref(false)
|
||||
const measurementPreviewLoading = ref(false)
|
||||
const measurementPreviewError = ref<ApiErrorDetails | null>(null)
|
||||
const measurementPreviewUrl = ref('')
|
||||
const measurementPreviewTitle = ref('测量图片')
|
||||
const measurementCollapsed = reactive({ customer: false, outer: false, actual: false })
|
||||
const measurementDialogOpen = ref(false)
|
||||
const smartLoadingOpen = ref(false)
|
||||
const measurementOrder = ref<UnifiedOrderResource | null>(null)
|
||||
let detailLoadingKey = ''
|
||||
let detailRequestSerial = 0
|
||||
let measurementRequestSerial = 0
|
||||
let measurementPreviewRequestSerial = 0
|
||||
const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||
@@ -208,8 +219,12 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
? allQueryFields.value
|
||||
: primaryQueryFields.value)
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
...(can('platform:order:approve') ? [{
|
||||
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||
selectable: (row: UnifiedOrderResource) => canApprove(row) && !batchApprovalSaving.value,
|
||||
} satisfies DataTableColumn] : []),
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
|
||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||
@@ -230,7 +245,7 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '内部备注', minWidth: 180, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 150, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, slotName: 'field' },
|
||||
]
|
||||
])
|
||||
|
||||
function emptyFilters(): FilterModel {
|
||||
return {
|
||||
@@ -439,30 +454,53 @@ function measurementTotals(rows: Record<string, unknown>[], customerSource = fal
|
||||
}
|
||||
}
|
||||
|
||||
function revokeMeasurementImages() {
|
||||
measurementImageUrls.value.forEach((url) => URL.revokeObjectURL(url))
|
||||
measurementImageUrls.value = []
|
||||
function releaseMeasurementPreview() {
|
||||
measurementPreviewRequestSerial += 1
|
||||
if (measurementPreviewUrl.value) URL.revokeObjectURL(measurementPreviewUrl.value)
|
||||
measurementPreviewUrl.value = ''
|
||||
measurementPreviewLoading.value = false
|
||||
measurementPreviewError.value = null
|
||||
}
|
||||
|
||||
async function openMeasurementEvidence(fileId: string | null, title: string) {
|
||||
const summary = detail.value?.summary
|
||||
if (!fileId || !summary?.inboundId) return
|
||||
releaseMeasurementPreview()
|
||||
const serial = ++measurementPreviewRequestSerial
|
||||
measurementPreviewTitle.value = title
|
||||
measurementPreviewOpen.value = true
|
||||
measurementPreviewLoading.value = true
|
||||
try {
|
||||
const blob = await downloadWeightImage(String(summary.tenantId), String(summary.inboundId), fileId)
|
||||
if (serial !== measurementPreviewRequestSerial) return
|
||||
measurementPreviewUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) {
|
||||
if (serial !== measurementPreviewRequestSerial) return
|
||||
measurementPreviewError.value = getApiErrorDetails(error, '测量原图读取失败,请稍后重试')
|
||||
} finally {
|
||||
if (serial === measurementPreviewRequestSerial) measurementPreviewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMeasurementDetails() {
|
||||
const summary = detail.value?.summary
|
||||
if (!summary?.inboundId || !can('platform:firstleg:inbound:view')) return
|
||||
if (!summary?.inboundId || !can('platform:firstleg:inbound:measure:view')) return
|
||||
const key = `${summary.tenantId}:${summary.inboundId}`
|
||||
if (measurementLoadedKey.value === key || measurementLoading.value) return
|
||||
const serial = ++measurementRequestSerial
|
||||
measurementLoading.value = true
|
||||
measurementError.value = null
|
||||
try {
|
||||
measurementDetail.value = await getInboundMeasurement(String(summary.tenantId), String(summary.inboundId))
|
||||
const response = await getInboundMeasurement(String(summary.tenantId), String(summary.inboundId))
|
||||
if (serial !== measurementRequestSerial) return
|
||||
measurementDetail.value = response
|
||||
measurementLoadedKey.value = key
|
||||
revokeMeasurementImages()
|
||||
const fileIds = [...measurementDetail.value.items.map((item) => item.intelligentImageFileId), ...measurementDetail.value.weightResults.map((item) => item.fileId)]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
const blobs = await Promise.all(fileIds.map((fileId) => downloadWeightImage(String(summary.tenantId), String(summary.inboundId), fileId).catch(() => null)))
|
||||
measurementImageUrls.value = blobs.filter((blob): blob is Blob => Boolean(blob)).map((blob) => URL.createObjectURL(blob))
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (serial !== measurementRequestSerial) return
|
||||
measurementDetail.value = null
|
||||
revokeMeasurementImages()
|
||||
measurementError.value = getApiErrorDetails(error, '测量数据加载失败,请稍后重试')
|
||||
} finally {
|
||||
measurementLoading.value = false
|
||||
if (serial === measurementRequestSerial) measurementLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,6 +622,7 @@ async function loadWarehouses() {
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
selectedApprovalOrders.value = []
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
@@ -649,6 +688,51 @@ function openMeasurement(row: UnifiedOrderResource) {
|
||||
measurementDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleApprovalSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
selectedApprovalOrders.value = rows.filter(canApprove)
|
||||
}
|
||||
|
||||
async function batchApproveOrders() {
|
||||
const selected = [...selectedApprovalOrders.value]
|
||||
if (!selected.length || batchApprovalSaving.value) return
|
||||
batchApprovalSaving.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量审核通过已勾选的 ${selected.length} 个订单吗?审核后订单将进入待测量。`,
|
||||
'批量审核订单',
|
||||
{ confirmButtonText: '确认批量审核', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
const groups = new Map<string, UnifiedOrderResource[]>()
|
||||
selected.forEach((order) => {
|
||||
const tenantId = String(order.tenantId)
|
||||
groups.set(tenantId, [...(groups.get(tenantId) || []), order])
|
||||
})
|
||||
let successCount = 0
|
||||
let failureCount = 0
|
||||
const failures: string[] = []
|
||||
for (const [tenantId, orders] of groups) {
|
||||
try {
|
||||
const result = await batchApprovePlatformUnifiedOrders(tenantId, orders.map((order) => String(order.orderId)))
|
||||
successCount += result.successCount
|
||||
failureCount += result.failureCount
|
||||
failures.push(...result.failedOrders.map((failure) => failure.message))
|
||||
} catch (error) {
|
||||
failureCount += orders.length
|
||||
failures.push(getApiErrorDetails(error, '该客户订单批量审核失败').message)
|
||||
}
|
||||
}
|
||||
await loadOrders()
|
||||
const reason = [...new Set(failures.filter(Boolean))].slice(0, 3).join(';')
|
||||
if (!failureCount) ElMessage.success(`已批量审核通过 ${successCount} 个订单`)
|
||||
else if (successCount) ElMessage.warning(`批量审核完成:成功 ${successCount} 个,失败 ${failureCount} 个${reason ? `。${reason}` : ''}`)
|
||||
else ElMessage.error(`批量审核失败:${failureCount} 个订单未通过${reason ? `。${reason}` : ''}`)
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '批量审核失败').message)
|
||||
} finally {
|
||||
batchApprovalSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleMeasurementUpdated() {
|
||||
void loadOrders()
|
||||
}
|
||||
@@ -714,9 +798,12 @@ async function loadDetail(force = false) {
|
||||
if (!force && (detailLoadingKey === requestKey || detail.value?.summary.orderId === orderId.value && String(detail.value.summary.tenantId) === tenantId)) return
|
||||
const serial = ++detailRequestSerial
|
||||
detailLoadingKey = requestKey
|
||||
measurementRequestSerial += 1
|
||||
measurementDetail.value = null
|
||||
measurementError.value = null
|
||||
measurementLoadedKey.value = ''
|
||||
revokeMeasurementImages()
|
||||
measurementPreviewOpen.value = false
|
||||
releaseMeasurementPreview()
|
||||
detailLoading.value = true
|
||||
detailError.value = null
|
||||
try {
|
||||
@@ -747,11 +834,18 @@ watch(detailRouteKey, (value) => {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}, { immediate: true })
|
||||
watch(measurementPreviewOpen, (open) => {
|
||||
if (!open) releaseMeasurementPreview()
|
||||
})
|
||||
onMounted(() => {
|
||||
void loadCustomers()
|
||||
void loadWarehouses()
|
||||
if (!orderId.value) void loadOrders()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
measurementRequestSerial += 1
|
||||
releaseMeasurementPreview()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -913,15 +1007,20 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="activeDetailTab === 'measurement'">
|
||||
<div class="panel-title"><div><h2>测量数据</h2><p>{{ detail.summary.measurement?.completedAt ? `完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}</p></div><Ruler :size="19" /></div>
|
||||
<section class="measurement-section">
|
||||
<div class="panel-title"><div><h2>测量数据</h2><p>{{ detail.summary.measurement?.completedAt ? `完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}</p></div><Ruler :size="19" /></div>
|
||||
<el-alert v-if="!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show-icon />
|
||||
<LoadErrorState v-else-if="measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails">
|
||||
<template #details><ApiErrorMeta :details="measurementError" /></template>
|
||||
</LoadErrorState>
|
||||
<template v-else>
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<div v-if="!measurementCollapsed.customer">
|
||||
<div class="detail-table-wrap"><el-table :data="detail.order.cargoes" border stripe empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key, true)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key, true) }}</template></template></el-table-column></el-table></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="measurement-section">
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<div v-if="!measurementCollapsed.outer">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" border stripe empty-text="暂无外箱规数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
|
||||
@@ -931,12 +1030,55 @@ onMounted(() => {
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积重:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span><span>重量体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<div v-if="!measurementCollapsed.actual">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" border stripe empty-text="暂无实际测量数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap">
|
||||
<el-table :data="measurementDetail?.items || []" row-key="id" border stripe empty-text="暂无实际测量数据">
|
||||
<el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120">
|
||||
<template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="尺寸图片" min-width="148">
|
||||
<template #default="{ row }">
|
||||
<MeasurementEvidenceThumbnail
|
||||
:customer-id="String(detail.summary.tenantId)"
|
||||
:inbound-id="String(detail.summary.inboundId)"
|
||||
:file-id="row.intelligentImageFileId"
|
||||
:alt="`测量明细 ${row.itemNo || row.id} 的尺寸图片`"
|
||||
empty-text="未绑定尺寸图片"
|
||||
@open="openMeasurementEvidence(row.intelligentImageFileId, `尺寸图片 · ${row.itemNo || row.id}`)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span></div>
|
||||
<div class="measurement-media"><h4>测量图片</h4><div v-if="measurementImageUrls.length" class="measurement-media-grid"><el-image v-for="(url, index) in measurementImageUrls" :key="url" :src="url" fit="contain" :preview-src-list="measurementImageUrls" :initial-index="index" class="measurement-media-image" /></div><el-empty v-else description="暂无测量图片" :image-size="48" /><h4>测量视频</h4><el-empty description="暂无测量视频" :image-size="48" /></div>
|
||||
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
|
||||
<div class="weight-evidence__heading">
|
||||
<div><h4>称重图片与确认重量</h4><p>称重图片独立展示;重量值直接采用服务端确认结果。</p></div>
|
||||
<dl><dt>实际总毛重</dt><dd>{{ formatNumber(measurementDetail?.actualGrossWeightKg) }} kg</dd></dl>
|
||||
</div>
|
||||
<div v-if="measurementDetail?.weightResults.length" class="weight-evidence-grid">
|
||||
<article v-for="weightResult in measurementDetail.weightResults" :key="weightResult.id" class="weight-evidence-card">
|
||||
<MeasurementEvidenceThumbnail
|
||||
:customer-id="String(detail.summary.tenantId)"
|
||||
:inbound-id="String(detail.summary.inboundId)"
|
||||
:file-id="weightResult.fileId"
|
||||
:alt="`称重结果 ${weightResult.id} 的称重图片`"
|
||||
empty-text="未绑定称重图片"
|
||||
@open="openMeasurementEvidence(weightResult.fileId, `称重图片 · ${weightResult.palletType || '未记录类型'}`)"
|
||||
/>
|
||||
<dl>
|
||||
<div><dt>托盘类型</dt><dd>{{ weightResult.palletType || '未记录类型' }}</dd></div>
|
||||
<div><dt>服务端确认毛重</dt><dd>{{ formatNumber(weightResult.grossWeightKg) }} kg</dd></div>
|
||||
<div><dt>托盘皮重</dt><dd>{{ formatNumber(weightResult.palletTareWeightKg) }} kg</dd></div>
|
||||
<div><dt>货物重量</dt><dd>{{ formatNumber(weightResult.netWeightKg) }} kg</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-else description="暂无称重图片与确认结果" :image-size="48" />
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-title"><div><h2>内部备注</h2><p>仅平台内部可见</p></div><el-button v-if="can('platform:order:note')" type="primary" size="small" @click="openNoteDialog()"><StickyNote :size="15" />添加备注</el-button></div>
|
||||
@@ -987,9 +1129,10 @@ onMounted(() => {
|
||||
|
||||
<template #default>
|
||||
<div v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表">
|
||||
<template v-if="can('platform:firstleg:smart-loading:list')" #toolbar-actions>
|
||||
<el-button type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleApprovalSelectionChange">
|
||||
<template #toolbar-actions>
|
||||
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
|
||||
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
@@ -1054,6 +1197,21 @@ onMounted(() => {
|
||||
<el-alert title="确认订单资料无误后审核通过,订单将进入待测量阶段。" type="info" :closable="false" show-icon />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog
|
||||
v-model="measurementPreviewOpen"
|
||||
:title="measurementPreviewTitle"
|
||||
width="min(920px, calc(100vw - 28px))"
|
||||
:loading="measurementPreviewLoading"
|
||||
:show-footer="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<LoadErrorState v-if="measurementPreviewError" title="测量原图暂时无法读取" :message="measurementPreviewError.message">
|
||||
<template #details><ApiErrorMeta :details="measurementPreviewError" /></template>
|
||||
</LoadErrorState>
|
||||
<img v-else-if="measurementPreviewUrl" class="measurement-original-preview" :src="measurementPreviewUrl" :alt="measurementPreviewTitle" />
|
||||
<el-empty v-else-if="!measurementPreviewLoading" description="该记录未返回可预览的图片" :image-size="56" />
|
||||
</AppDialog>
|
||||
|
||||
<InboundMeasurementDialog
|
||||
v-if="measurementOrder?.inboundId"
|
||||
v-model="measurementDialogOpen"
|
||||
@@ -1148,10 +1306,20 @@ onMounted(() => {
|
||||
.measurement-total { display: flex; min-height: 42px; align-items: center; justify-content: flex-end; gap: 24px; padding: 9px 14px; border: 1px solid var(--cc-border); background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.measurement-total { border-top: 0; border-radius: 0 0 7px 7px; }
|
||||
.measurement-total strong { margin-right: auto; color: var(--cc-text); font-size: 13px; }
|
||||
.measurement-media { margin-top: 20px; }
|
||||
.measurement-media h4 { margin: 18px 0 10px; color: var(--cc-text); font-size: 14px; }
|
||||
.measurement-media-grid { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.measurement-media-image { width: 110px; height: 90px; border: 1px solid var(--cc-border); border-radius: 6px; background: #f8fafc; }
|
||||
.weight-evidence { margin-top: 20px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fbfcfe; }
|
||||
.weight-evidence__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 14px; }
|
||||
.weight-evidence__heading h4 { margin: 0; color: var(--cc-text); font-size: 14px; }
|
||||
.weight-evidence__heading p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-evidence__heading dl { margin: 0; text-align: right; }
|
||||
.weight-evidence__heading dt { color: var(--cc-muted); font-size: 11px; }
|
||||
.weight-evidence__heading dd { margin: 4px 0 0; color: var(--cc-text); font-size: 16px; font-weight: 700; }
|
||||
.weight-evidence-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
||||
.weight-evidence-card { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.weight-evidence-card > dl { display: grid; gap: 7px; margin: 0; padding: 12px; }
|
||||
.weight-evidence-card > dl > div { display: flex; min-width: 0; justify-content: space-between; gap: 12px; }
|
||||
.weight-evidence-card dt { color: var(--cc-muted); font-size: 11px; }
|
||||
.weight-evidence-card dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: 12px; font-weight: 650; text-align: right; overflow-wrap: anywhere; }
|
||||
.measurement-original-preview { display: block; width: 100%; max-height: min(72vh, 760px); object-fit: contain; border-radius: 8px; background: #f2f4f7; }
|
||||
.history-list, .notes-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.history-list li { position: relative; display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 12px; padding-bottom: 20px; }
|
||||
.history-list li:not(:last-child)::after { position: absolute; top: 15px; bottom: 0; left: 7px; width: 1px; background: var(--cc-border); content: ''; }
|
||||
@@ -1167,10 +1335,6 @@ onMounted(() => {
|
||||
.notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: 12px; }
|
||||
@media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.inbound-task-dialog { max-height: 92vh; overflow: hidden; }
|
||||
.inbound-task-dialog .el-dialog__body { max-height: calc(92vh - 62px); overflow: auto; padding: 20px 24px 24px; }
|
||||
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user