优化
This commit is contained in:
@@ -145,10 +145,12 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('暂无费用数据')
|
||||
})
|
||||
|
||||
it('does not expose the unconfirmed downstream workflow', () => {
|
||||
expect(source).not.toContain('后半程作业')
|
||||
expect(source).not.toContain('openWorkflow')
|
||||
expect(source).not.toContain('workflowEligible')
|
||||
it('mounts downstream progress inside the existing order detail without opening unsafe writes', () => {
|
||||
expect(source).toContain('UnifiedOrderWorkflowPanel')
|
||||
expect(source).toContain("{ key: 'workflow', label: '后半程作业'")
|
||||
expect(source).toContain(':read-only="true"')
|
||||
expect(source).toContain(':show-delivery="false"')
|
||||
expect(source).toContain("detail.value?.summary.inboundId")
|
||||
expect(routeSource).not.toContain('unified-order-workflow')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import LineClampText from '@/shared/components/LineClampText.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows'
|
||||
import {
|
||||
downloadWeightImage,
|
||||
getInboundMeasurement,
|
||||
@@ -105,6 +106,10 @@ const detailTabs = [
|
||||
{ key: 'other', label: '其他', icon: StickyNote },
|
||||
]
|
||||
|
||||
const detailTabOptions = computed(() => detail.value?.summary.inboundId
|
||||
? [...detailTabs, { key: 'workflow', label: '后半程作业', icon: ListChecks }]
|
||||
: detailTabs)
|
||||
|
||||
const customerMeasurementColumns = [
|
||||
{ key: 'itemNo', label: '#' }, { key: 'lengthCm', label: '箱规长(cm)' }, { key: 'widthCm', label: '箱规宽(cm)' },
|
||||
{ key: 'heightCm', label: '箱规高(cm)' }, { key: 'packageCount', label: '箱数' }, { key: 'unitWeightKg', label: '每箱毛重' },
|
||||
@@ -789,7 +794,7 @@ onMounted(() => {
|
||||
</section>
|
||||
|
||||
<div class="detail-tabs" role="tablist" aria-label="订单详情模块">
|
||||
<button v-for="tab in detailTabs" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="selectDetailTab(tab.key)"><component :is="tab.icon" :size="17" /><span>{{ tab.label }}</span></button>
|
||||
<button v-for="tab in detailTabOptions" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="selectDetailTab(tab.key)"><component :is="tab.icon" :size="17" /><span>{{ tab.label }}</span></button>
|
||||
</div>
|
||||
|
||||
<section :id="`order-detail-panel-${activeDetailTab}`" class="detail-panel" role="tabpanel" :aria-labelledby="`order-detail-tab-${activeDetailTab}`" tabindex="0">
|
||||
@@ -922,7 +927,20 @@ onMounted(() => {
|
||||
<ol v-else class="history-list" aria-label="状态历史"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><span class="history-marker" aria-hidden="true" /><div><div class="history-line"><strong>{{ historyActionLabel(item.action || item.eventType) }}</strong><StatusBadge :status="statusLabel(item.mainStatus)" /></div><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" class="history-remark">{{ item.remark }}</p></div></li></ol>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'measurement'">
|
||||
<template v-else-if="activeDetailTab === 'workflow'">
|
||||
<UnifiedOrderWorkflowPanel
|
||||
:tenant-id="String(detail.summary.tenantId)"
|
||||
:inbound-id="String(detail.summary.inboundId)"
|
||||
:main-status="detail.summary.mainStatus"
|
||||
:inbound-version="detail.summary.inboundVersion"
|
||||
:picking-version="detail.summary.loading?.pickingVersion"
|
||||
:delivery-tracking-no="detail.summary.delivery?.trackingNo"
|
||||
:read-only="true"
|
||||
:show-delivery="false"
|
||||
@updated="loadDetail(true)"
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -30,10 +30,14 @@ const props = withDefaults(defineProps<{
|
||||
inboundVersion?: number | null
|
||||
pickingVersion?: number | null
|
||||
deliveryTrackingNo?: string | null
|
||||
readOnly?: boolean
|
||||
showDelivery?: boolean
|
||||
}>(), {
|
||||
inboundVersion: null,
|
||||
pickingVersion: null,
|
||||
deliveryTrackingNo: null,
|
||||
readOnly: false,
|
||||
showDelivery: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ updated: [] }>()
|
||||
@@ -64,15 +68,15 @@ const isLoaded = computed(() => props.mainStatus === 'LOADED')
|
||||
const preloadVersion = computed(() => preload.value?.version ?? 0)
|
||||
const pickingExpectedVersion = computed(() => picking.value?.version ?? props.pickingVersion ?? null)
|
||||
const inboundExpectedVersion = computed(() => props.inboundVersion)
|
||||
const canSplit = computed(() => access.can('firstleg:inbound:split'))
|
||||
const canPreload = computed(() => access.can('firstleg:inbound:preload'))
|
||||
const canSplit = computed(() => !props.readOnly && access.can('firstleg:inbound:split'))
|
||||
const canPreload = computed(() => !props.readOnly && access.can('firstleg:inbound:preload'))
|
||||
const canPreloadView = computed(() => access.can('firstleg:inbound:preload-view'))
|
||||
const canPicking = computed(() => access.can('platform:firstleg:inbound:picking:edit'))
|
||||
const canPicking = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:picking:edit'))
|
||||
const canPickingView = computed(() => access.can('platform:firstleg:inbound:picking:view'))
|
||||
const canEvidence = computed(() => access.can('platform:firstleg:inbound:loading:edit'))
|
||||
const canEvidence = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:loading:edit'))
|
||||
const canEvidenceView = computed(() => access.can('platform:firstleg:inbound:loading:view'))
|
||||
const canLoadingComplete = computed(() => access.can('platform:firstleg:inbound:loading:complete'))
|
||||
const canDeliveryComplete = computed(() => access.can('platform:firstleg:inbound:delivery:complete'))
|
||||
const canLoadingComplete = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:loading:complete'))
|
||||
const canDeliveryComplete = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:delivery:complete'))
|
||||
|
||||
function itemTemplate(): PreloadItem {
|
||||
return { containerCode: '', cargoReference: '', packageCount: null, volumeCbm: 0, remark: '' }
|
||||
@@ -279,7 +283,7 @@ onMounted(() => { void refresh() })
|
||||
<div class="workflow-heading">
|
||||
<div>
|
||||
<h2>后半程作业</h2>
|
||||
<p>订单按待分柜、装柜中、装柜完成和派送完成推进;资料可修改,客户确认不阻断。</p>
|
||||
<p>查看订单的分柜、预装、拣货和装柜进度。</p>
|
||||
</div>
|
||||
<el-button text :loading="loading" aria-label="刷新作业数据" @click="refresh"><RefreshCw :size="15" />刷新</el-button>
|
||||
</div>
|
||||
@@ -290,6 +294,7 @@ onMounted(() => { void refresh() })
|
||||
</el-alert>
|
||||
<el-alert v-if="notice" class="workflow-alert" type="success" :closable="false" show-icon>{{ notice }}</el-alert>
|
||||
<el-alert v-if="busy" class="workflow-alert" type="info" :closable="false">{{ idempotentMessage(busy) }}</el-alert>
|
||||
<el-alert v-if="props.readOnly" class="workflow-alert" type="info" :closable="false" show-icon>当前仅开放进度查询;五图取证和装柜完成门禁接通后再开放操作。</el-alert>
|
||||
|
||||
<template v-if="isSplit">
|
||||
<el-card shadow="never" class="workflow-card">
|
||||
@@ -297,14 +302,14 @@ onMounted(() => { void refresh() })
|
||||
<el-empty v-if="!preload && !loading" description="暂无预装清单,可直接生成分柜方案" />
|
||||
<div class="preload-items" role="group" aria-label="预装清单明细">
|
||||
<div v-for="(item, index) in preloadItems" :key="index" class="preload-row">
|
||||
<el-input v-model="item.containerCode" aria-label="柜号" placeholder="柜号" />
|
||||
<el-input v-model="item.cargoReference" aria-label="货物引用" placeholder="货物引用" />
|
||||
<el-input-number v-model="item.packageCount" aria-label="件数" :min="1" controls-position="right" />
|
||||
<el-input-number v-model="item.volumeCbm" aria-label="体积" :min="0" :precision="3" controls-position="right" />
|
||||
<el-button text type="danger" aria-label="删除预装项" @click="removeItem(index)">删除</el-button>
|
||||
<el-input v-model="item.containerCode" :disabled="props.readOnly" aria-label="柜号" placeholder="柜号" />
|
||||
<el-input v-model="item.cargoReference" :disabled="props.readOnly" aria-label="货物引用" placeholder="货物引用" />
|
||||
<el-input-number v-model="item.packageCount" :disabled="props.readOnly" aria-label="件数" :min="1" controls-position="right" />
|
||||
<el-input-number v-model="item.volumeCbm" :disabled="props.readOnly" aria-label="体积" :min="0" :precision="3" controls-position="right" />
|
||||
<el-button v-if="!props.readOnly" text type="danger" aria-label="删除预装项" @click="removeItem(index)">删除</el-button>
|
||||
</div>
|
||||
<el-button plain @click="addItem">新增预装项</el-button>
|
||||
<el-input v-model="splitRemark" class="workflow-remark" type="textarea" :rows="2" aria-label="分柜备注" placeholder="分柜备注(可选)" />
|
||||
<el-button v-if="!props.readOnly" plain @click="addItem">新增预装项</el-button>
|
||||
<el-input v-model="splitRemark" :disabled="props.readOnly" class="workflow-remark" type="textarea" :rows="2" aria-label="分柜备注" placeholder="分柜备注(可选)" />
|
||||
</div>
|
||||
<div class="workflow-actions">
|
||||
<el-button :disabled="!canSplit || Boolean(busy)" :loading="busy === 'smart-split'" @click="runSmartSplit"><Save :size="15" />智能分柜/保存调整</el-button>
|
||||
@@ -328,7 +333,7 @@ onMounted(() => { void refresh() })
|
||||
<el-button :disabled="!canPicking || !picking || Boolean(busy)" :loading="busy === 'picking-start'" @click="startPickingTask"><Play :size="15" />开始拣货</el-button>
|
||||
<el-button :disabled="!canPicking || !picking || Boolean(busy)" :loading="busy === 'picking-complete'" @click="completePickingTask"><Check :size="15" />完成拣货</el-button>
|
||||
</div>
|
||||
<el-input v-model="loadingRemark" class="workflow-remark" type="textarea" :rows="2" aria-label="装柜备注" placeholder="装柜备注(可选)" />
|
||||
<el-input v-model="loadingRemark" :disabled="props.readOnly" class="workflow-remark" type="textarea" :rows="2" aria-label="装柜备注" placeholder="装柜备注(可选)" />
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="workflow-card">
|
||||
@@ -336,8 +341,8 @@ onMounted(() => { void refresh() })
|
||||
<div class="evidence-grid">
|
||||
<div v-for="entry in evidenceTypes" :key="entry.type" class="evidence-item">
|
||||
<strong><Camera :size="15" />{{ entry.label }}</strong>
|
||||
<el-input v-if="entry.type === 'SEAL'" :model-value="evidence(entry.type)?.sealNo || ''" aria-label="封签号" placeholder="封签号" @update:model-value="updateEvidenceField(entry.type, 'sealNo', $event)" />
|
||||
<el-input v-else :model-value="evidence(entry.type)?.fileName || ''" aria-label="照片文件名" placeholder="文件上传完成后填写文件名" @update:model-value="updateEvidenceField(entry.type, 'fileName', $event)" />
|
||||
<el-input v-if="entry.type === 'SEAL'" :disabled="props.readOnly" :model-value="evidence(entry.type)?.sealNo || ''" aria-label="封签号" placeholder="封签号" @update:model-value="updateEvidenceField(entry.type, 'sealNo', $event)" />
|
||||
<el-input v-else :disabled="props.readOnly" :model-value="evidence(entry.type)?.fileName || ''" aria-label="照片文件名" placeholder="文件上传完成后填写文件名" @update:model-value="updateEvidenceField(entry.type, 'fileName', $event)" />
|
||||
<el-button plain :disabled="!canEvidence || Boolean(busy)" :loading="busy === `evidence-${entry.type}`" @click="saveEvidence(entry.type)">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +354,7 @@ onMounted(() => { void refresh() })
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<el-card v-if="isLoaded" shadow="never" class="workflow-card">
|
||||
<el-card v-if="isLoaded && props.showDelivery" shadow="never" class="workflow-card">
|
||||
<template #header><div class="card-title"><span>派送</span><el-tag type="success">装柜完成</el-tag></div></template>
|
||||
<el-descriptions v-if="props.deliveryTrackingNo" :column="1" border>
|
||||
<el-descriptions-item label="派送单号">{{ props.deliveryTrackingNo }}</el-descriptions-item>
|
||||
|
||||
Reference in New Issue
Block a user