@@ -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 : 42 px ; align - items : center ; justify - content : flex - end ; gap : 24 px ; padding : 9 px 14 px ; border : 1 px solid var ( -- cc - border ) ; background : # f8fafc ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. measurement - total { border - top : 0 ; border - radius : 0 0 7 px 7 px ; }
. measurement - total strong { margin - right : auto ; color : var ( -- cc - text ) ; font - size : 13 px ; }
. measurement - media { margin - top : 20 px ; }
. measurement - media h4 { margin : 18 px 0 10 px ; color : var ( -- cc - text ) ; font - size : 14 px ; }
. measurement - media - grid { display : flex ; flex - wrap : wrap ; gap : 12 px ; }
. measurement - media - image { width : 110 px ; height : 90 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : # f8fafc ; }
. weight - evidence { margin - top : 20 px ; padding : 16 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fbfcfe ; }
. weight - evidence _ _heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 20 px ; margin - bottom : 14 px ; }
. weight - evidence _ _heading h4 { margin : 0 ; color : var ( -- cc - text ) ; font - size : 14 px ; }
. weight - evidence _ _heading p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. weight - evidence _ _heading dl { margin : 0 ; text - align : right ; }
. weight - evidence _ _heading dt { color : var ( -- cc - muted ) ; font - size : 11 px ; }
. weight - evidence _ _heading dd { margin : 4 px 0 0 ; color : var ( -- cc - text ) ; font - size : 16 px ; font - weight : 700 ; }
. weight - evidence - grid { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 220 px , 1 fr ) ) ; gap : 14 px ; }
. weight - evidence - card { min - width : 0 ; overflow : hidden ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. weight - evidence - card > dl { display : grid ; gap : 7 px ; margin : 0 ; padding : 12 px ; }
. weight - evidence - card > dl > div { display : flex ; min - width : 0 ; justify - content : space - between ; gap : 12 px ; }
. weight - evidence - card dt { color : var ( -- cc - muted ) ; font - size : 11 px ; }
. weight - evidence - card dd { min - width : 0 ; margin : 0 ; color : var ( -- cc - text ) ; font - size : 12 px ; font - weight : 650 ; text - align : right ; overflow - wrap : anywhere ; }
. measurement - original - preview { display : block ; width : 100 % ; max - height : min ( 72 vh , 760 px ) ; object - fit : contain ; border - radius : 8 px ; 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 : 16 px minmax ( 0 , 1 fr ) ; gap : 12 px ; padding - bottom : 20 px ; }
. history - list li : not ( : last - child ) : : after { position : absolute ; top : 15 px ; bottom : 0 ; left : 7 px ; width : 1 px ; background : var ( -- cc - border ) ; content : '' ; }
@@ -1167,10 +1335,6 @@ onMounted(() => {
. notes - list span { display : block ; margin - top : 5 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
@ media ( max - width : 960 px ) { . detail - overview { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . detail - overview article : nth - child ( 2 ) { border - right : 0 ; } . detail - overview article : nth - child ( - n + 2 ) { border - bottom : 1 px solid var ( -- cc - border ) ; } . order - info - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . measurement - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } }
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep ( . el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep ( . el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { display : grid ; grid - template - columns : auto 36 px minmax ( 0 , 1 fr ) auto ; } . detail - header - actions { display : grid ; grid - template - columns : 1 fr ; } . detail - header - actions : deep ( . el - button ) { min - height : 40 px ; } . detail - overview , . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . detail - overview article { border - right : 0 ; border - bottom : 1 px 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 18 px ; } . detail - panel { padding : 16 px ; } . measurement - section - title { align - items : flex - start ; flex - wrap : wrap ; padding : 12 px ; } . measurement - section - summary { width : 100 % ; flex - basis : calc ( 100 % - 34 px ) ; flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; white - space : normal ; } . measurement - section - title > svg { margin - left : auto ; } . measurement - total { flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; } . measurement - total strong { width : 100 % ; } }
@ media ( max - width : 420 px ) { . order - detail - heading { grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; } . order - mark { display : none ; } }
< / style >
< style >
. inbound - task - dialog { max - height : 92 vh ; overflow : hidden ; }
. inbound - task - dialog . el - dialog _ _body { max - height : calc ( 92 vh - 62 px ) ; overflow : auto ; padding : 20 px 24 px 24 px ; }
@ media ( max - width : 720 px ) { . weight - evidence _ _heading { flex - direction : column ; } . weight - evidence _ _heading dl { text - align : left ; } }
@ media ( max - width : 420 px ) { . order - detail - heading { grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; } . order - mark { display : none ; } . weight - evidence { padding : 10 px ; } . weight - evidence - grid { grid - template - columns : 1 fr ; } }
< / style >