优化
This commit is contained in:
@@ -102,7 +102,7 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
expect(wrapper.text()).toContain('3 个入仓码')
|
||||
expect(wrapper.text()).toContain('3 个成员')
|
||||
expect(wrapper.text()).toContain('13 箱')
|
||||
expect(wrapper.text()).toContain('IN-1')
|
||||
expect(wrapper.text()).toContain('IN-1-B')
|
||||
@@ -116,4 +116,23 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整')
|
||||
expect(ElMessageBox.prompt).toHaveBeenCalledWith(expect.stringContaining('不支持单独移除成员'), '取消同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['undefined', undefined],
|
||||
['null', null],
|
||||
['blank', ' '],
|
||||
])('keeps the member dialog readable when a normalized inbound code is %s', async (_case, inboundCode) => {
|
||||
const incompleteGroup = {
|
||||
...group,
|
||||
members: [{ ...group.members[0], inboundCode: inboundCode as unknown as string }, group.members[1]],
|
||||
}
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, group: incompleteGroup })))
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
expect(wrapper.text()).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
expect(wrapper.text()).not.toContain('ORD-1')
|
||||
expect(wrapper.text()).not.toContain('5001')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -188,12 +188,21 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('v-for="member in coLoadDetailMembers"')
|
||||
expect(source).toContain('<th>入仓码</th><th>箱数</th>')
|
||||
expect(source).toContain('{{ member.inboundCode }}')
|
||||
expect(source).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(source).toContain('coLoadDetailHasMissingInboundCodes')
|
||||
expect(source).not.toContain('visibleInboundCode')
|
||||
expect(source).not.toContain('visibleOrderNo')
|
||||
expect(source).not.toContain('<th>订单编码</th>')
|
||||
expect(source).not.toContain('客户 ID')
|
||||
})
|
||||
|
||||
it('names the cross-domain relationship entry as fulfillment details', () => {
|
||||
expect(source).toContain('>履约详情</el-button>')
|
||||
expect(source).toContain('aria-label="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
expect(source).toContain('title="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
expect(source).not.toContain('>关联数据</el-button>')
|
||||
})
|
||||
|
||||
it('keeps editable query values separate from the applied list request', () => {
|
||||
expect(source).toContain('const appliedFilters = ref<FilterModel>(emptyFilters())')
|
||||
expect(source).toContain('const activeFilters = appliedFilters.value')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
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, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, Unlink, WalletCards } from '@lucide/vue'
|
||||
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,
|
||||
@@ -12,15 +12,11 @@ import {
|
||||
listPlatformUnifiedOrders,
|
||||
unifiedOrderStatusOptions,
|
||||
type UnifiedOrderDetailResource,
|
||||
type UnifiedOrderCoLoadGroupSummary,
|
||||
type UnifiedOrderMainStatus,
|
||||
type UnifiedOrderResource,
|
||||
} from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import {
|
||||
batchQueryPlatformCoLoadGroups,
|
||||
cancelPlatformCoLoadGroup,
|
||||
createPlatformCoLoadGroup,
|
||||
type CoLoadGroup,
|
||||
} from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
|
||||
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -36,7 +32,6 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import LineClampText from '@/shared/components/LineClampText.vue'
|
||||
import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import {
|
||||
@@ -129,9 +124,12 @@ const approvalSaving = ref(false)
|
||||
const batchApprovalSaving = ref(false)
|
||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||
const coLoadGroupsByOrderId = ref(new Map<string, CoLoadGroup>())
|
||||
const coLoadCreateOpen = ref(false)
|
||||
const coLoadDetailGroup = ref<CoLoadGroup | null>(null)
|
||||
const coLoadDetailOpen = ref(false)
|
||||
const coLoadDetailInboundCode = ref('')
|
||||
const coLoadDetailSummary = ref<CoLoadBusinessSummary | null>(null)
|
||||
const coLoadDetailLoading = ref(false)
|
||||
const coLoadDetailError = ref<ApiErrorDetails | null>(null)
|
||||
const coLoadSaving = ref(false)
|
||||
const coLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const approvalOrder = ref<UnifiedOrderResource | null>(null)
|
||||
@@ -141,8 +139,6 @@ const noteContent = ref('')
|
||||
const noteOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const relationOpen = ref(false)
|
||||
const relationOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const inboundRelationshipDialogOpen = ref(false)
|
||||
const inboundRelationshipDialogCode = ref('')
|
||||
const measurementDetail = ref<InboundMeasurementDetail | null>(null)
|
||||
const measurementLoading = ref(false)
|
||||
const measurementError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -709,7 +705,6 @@ async function loadWarehouses() {
|
||||
async function loadOrders() {
|
||||
selectedApprovalOrders.value = []
|
||||
selectedOrders.value = []
|
||||
coLoadGroupsByOrderId.value = new Map()
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
coLoadError.value = null
|
||||
@@ -739,14 +734,6 @@ async function loadOrders() {
|
||||
})
|
||||
records.value = result.records
|
||||
total.value = result.total
|
||||
if (can('platform:firstleg:smart-loading:assign') && result.records.length) {
|
||||
try {
|
||||
const groups = await batchQueryPlatformCoLoadGroups(result.records.map((row) => String(row.orderId)))
|
||||
coLoadGroupsByOrderId.value = new Map(groups.map((item) => [item.orderId, item.group]))
|
||||
} catch (error) {
|
||||
coLoadError.value = getApiErrorDetails(error, '同柜不可拆组信息加载失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
records.value = []
|
||||
total.value = 0
|
||||
@@ -789,40 +776,55 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
selectedApprovalOrders.value = rows.filter(canApprove)
|
||||
}
|
||||
|
||||
function openInboundRelationship(row: UnifiedOrderResource) {
|
||||
const code = row.inboundCode?.trim()
|
||||
if (!code) return
|
||||
inboundRelationshipDialogCode.value = code
|
||||
inboundRelationshipDialogOpen.value = true
|
||||
}
|
||||
|
||||
function coLoadGroup(row: UnifiedOrderResource) {
|
||||
return coLoadGroupsByOrderId.value.get(String(row.orderId)) || null
|
||||
}
|
||||
|
||||
function coLoadGroupOrderCount(group: CoLoadGroup) {
|
||||
return new Set(group.members.map((member) => member.orderId)).size
|
||||
return row.coLoadGroup || null
|
||||
}
|
||||
|
||||
function coLoadGroupMemberLabel(group: CoLoadGroup) {
|
||||
const orderCount = coLoadGroupOrderCount(group)
|
||||
const orderCount = new Set(group.members.map((member) => member.orderId)).size
|
||||
return group.members.length === orderCount
|
||||
? `${orderCount} 票`
|
||||
: `${orderCount} 票 · ${group.members.length} 个入仓片段`
|
||||
}
|
||||
|
||||
function coLoadGroupStatusLabel(group: CoLoadGroup) {
|
||||
function coLoadGroupStatusLabel(group: { status: string }) {
|
||||
return group.status === 'ACTIVE' ? '未锁定' : group.status === 'LOCKED' ? '已锁定' : group.status === 'CONSUMED' ? '已消费' : '已取消'
|
||||
}
|
||||
|
||||
function coLoadGroupSummary(group: CoLoadGroup) {
|
||||
return `同柜组 · ${coLoadGroupOrderCount(group)} 票 · ${coLoadGroupStatusLabel(group)}`
|
||||
function coLoadGroupSummary(group: UnifiedOrderCoLoadGroupSummary | CoLoadBusinessSummary) {
|
||||
return `同柜组 · ${group.memberCount} 票 · ${coLoadGroupStatusLabel(group)}`
|
||||
}
|
||||
|
||||
const coLoadDetailMembers = computed(() => coLoadDetailGroup.value?.members.filter((member) => member.inboundCode.trim()) || [])
|
||||
function hasCoLoadMemberInboundCode(member: CoLoadBusinessSummary['members'][number]) {
|
||||
return typeof member.inboundCode === 'string' && Boolean(member.inboundCode.trim())
|
||||
}
|
||||
|
||||
function openCoLoadDetail(group: CoLoadGroup) {
|
||||
coLoadDetailGroup.value = group
|
||||
const coLoadDetailMembers = computed(() => coLoadDetailSummary.value?.members.filter(hasCoLoadMemberInboundCode) || [])
|
||||
const coLoadDetailHasMissingInboundCodes = computed(() => Boolean(coLoadDetailSummary.value)
|
||||
&& (coLoadDetailMembers.value.length !== coLoadDetailSummary.value!.memberCount
|
||||
|| coLoadDetailSummary.value!.members.some((member) => !hasCoLoadMemberInboundCode(member))))
|
||||
|
||||
async function loadCoLoadDetail() {
|
||||
const inboundCode = coLoadDetailInboundCode.value
|
||||
if (!inboundCode || coLoadDetailLoading.value) return
|
||||
coLoadDetailLoading.value = true
|
||||
coLoadDetailError.value = null
|
||||
coLoadDetailSummary.value = null
|
||||
try {
|
||||
coLoadDetailSummary.value = await getPlatformCoLoadGroupByInboundCode(inboundCode)
|
||||
} catch (error) {
|
||||
coLoadDetailError.value = getApiErrorDetails(error, '同柜组成员加载失败,请刷新后重试')
|
||||
} finally {
|
||||
coLoadDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCoLoadDetail(row: UnifiedOrderResource) {
|
||||
coLoadDetailInboundCode.value = row.inboundCode?.trim() || ''
|
||||
coLoadDetailOpen.value = true
|
||||
coLoadDetailSummary.value = null
|
||||
coLoadDetailError.value = coLoadDetailInboundCode.value ? null : getApiErrorDetails(new Error('同柜组成员入仓码尚未返回'), '同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
if (coLoadDetailInboundCode.value) void loadCoLoadDetail()
|
||||
}
|
||||
|
||||
function openCoLoadCreate() {
|
||||
@@ -847,31 +849,6 @@ async function createCoLoadGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelCoLoadGroup(row: UnifiedOrderResource) {
|
||||
const group = coLoadGroup(row)
|
||||
if (!group || group.status !== 'ACTIVE' || coLoadSaving.value) return
|
||||
let reason = ''
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
`将整组取消(${coLoadGroupMemberLabel(group)})。不支持单独移除成员;如需调整,请取消后重新建组。`,
|
||||
'取消同柜不可拆组',
|
||||
{ confirmButtonText: '确认整组取消', cancelButtonText: '返回', inputPlaceholder: '取消原因(选填)', inputType: 'textarea' },
|
||||
)
|
||||
reason = result.value
|
||||
} catch { return }
|
||||
coLoadSaving.value = true
|
||||
coLoadError.value = null
|
||||
try {
|
||||
await cancelPlatformCoLoadGroup(group.groupId, group.version, reason)
|
||||
ElMessage.success('同柜不可拆组已整组取消')
|
||||
await loadOrders()
|
||||
} catch (error) {
|
||||
coLoadError.value = getApiErrorDetails(error, '取消同柜不可拆组失败')
|
||||
} finally {
|
||||
coLoadSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function batchApproveOrders() {
|
||||
const selected = [...selectedApprovalOrders.value]
|
||||
if (!selected.length || batchApprovalSaving.value) return
|
||||
@@ -925,19 +902,17 @@ function clearMeasurementOrder() {
|
||||
measurementOrder.value = null
|
||||
}
|
||||
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement' | 'cancelCoLoad'
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
|
||||
const unifiedOrderRowActionIcons: Record<UnifiedOrderRowAction, Component> = {
|
||||
note: StickyNote,
|
||||
approve: CircleCheck,
|
||||
measurement: Ruler,
|
||||
cancelCoLoad: Unlink,
|
||||
}
|
||||
function unifiedOrderSecondaryActions(row: UnifiedOrderResource): UnifiedOrderRowAction[] {
|
||||
return [
|
||||
...(can('platform:order:note') ? ['note' as const] : []),
|
||||
...(canApprove(row) ? ['approve' as const] : []),
|
||||
...(row.mainStatus === 'WAIT_MEASURE' && row.inboundId && can('platform:firstleg:inbound:measure:view') ? ['measurement' as const] : []),
|
||||
...(can('platform:firstleg:smart-loading:assign') && coLoadGroup(row)?.status === 'ACTIVE' ? ['cancelCoLoad' as const] : []),
|
||||
]
|
||||
}
|
||||
function directUnifiedOrderActions(row: UnifiedOrderResource) {
|
||||
@@ -952,17 +927,15 @@ function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
|
||||
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
||||
if (action === 'note') return '添加备注'
|
||||
if (action === 'approve') return '审核通过'
|
||||
if (action === 'cancelCoLoad') return '取消同柜组'
|
||||
return '测量作业'
|
||||
}
|
||||
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
|
||||
return action === 'approve' ? 'success' : action === 'cancelCoLoad' ? 'danger' : 'primary'
|
||||
return action === 'approve' ? 'success' : 'primary'
|
||||
}
|
||||
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
|
||||
if (command === 'note') openNoteDialog(row)
|
||||
if (command === 'approve') openApproval(row)
|
||||
if (command === 'measurement') openMeasurement(row)
|
||||
if (command === 'cancelCoLoad') void cancelCoLoadGroup(row)
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
@@ -1361,8 +1334,7 @@ onBeforeUnmount(() => {
|
||||
<template #inboundCode="{ row }">
|
||||
<div class="inbound-relation-cell order-code-cell">
|
||||
<strong class="inbound-code-value" :title="(row as UnifiedOrderResource).inboundCode || '待生成入仓码'">{{ (row as UnifiedOrderResource).inboundCode || '待生成入仓码' }}</strong>
|
||||
<button v-if="coLoadGroup(row as UnifiedOrderResource)" type="button" class="coload-tag-trigger" :aria-label="`查看${coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!)}成员`" @click.stop="openCoLoadDetail(coLoadGroup(row as UnifiedOrderResource)!)"><el-tag size="small" :type="coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!) }}</el-tag></button>
|
||||
<el-button v-if="(row as UnifiedOrderResource).inboundCode" class="order-relation-link" link type="primary" aria-label="查看入仓码关联数据" @click="openInboundRelationship(row as UnifiedOrderResource)"><Link2 :size="14" />关联数据</el-button>
|
||||
<button v-if="coLoadGroup(row as UnifiedOrderResource)" type="button" class="coload-tag-trigger" :aria-label="`查看${coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!)}成员`" @click.stop="openCoLoadDetail(row as UnifiedOrderResource)"><el-tag size="small" :type="coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!) }}</el-tag></button>
|
||||
<el-button
|
||||
v-if="(row as UnifiedOrderResource).relation"
|
||||
class="order-relation-link"
|
||||
@@ -1395,16 +1367,18 @@ onBeforeUnmount(() => {
|
||||
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.inboundCode || '待生成入仓码' }}</span></div>
|
||||
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
||||
</AppDialog>
|
||||
<AppDialog :model-value="Boolean(coLoadDetailGroup)" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" @update:model-value="coLoadDetailGroup = null">
|
||||
<template v-if="coLoadDetailGroup">
|
||||
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailGroup.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailGroup) }}</el-tag><span>{{ coLoadDetailMembers.length }} 个入仓码 · 共 {{ coLoadDetailGroup.fullPackageCount }} 箱</span></div>
|
||||
<div v-if="coLoadDetailMembers.length" class="coload-detail-scroll">
|
||||
<AppDialog v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading">
|
||||
<LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState>
|
||||
<template v-else-if="coLoadDetailSummary">
|
||||
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
|
||||
<el-alert v-if="coLoadDetailHasMissingInboundCodes" type="error" :closable="false" show-icon title="同柜组成员入仓码尚未返回,请刷新后重试" />
|
||||
<div v-else-if="coLoadDetailMembers.length" class="coload-detail-scroll">
|
||||
<table class="coload-detail-table">
|
||||
<thead><tr><th>入仓码</th><th>箱数</th></tr></thead>
|
||||
<tbody><tr v-for="member in coLoadDetailMembers" :key="member.inboundId"><td><strong :title="member.inboundCode">{{ member.inboundCode }}</strong></td><td>{{ member.fullPackageCount }} 箱</td></tr></tbody>
|
||||
<thead><tr><th>入仓码</th></tr></thead>
|
||||
<tbody><tr v-for="member in coLoadDetailMembers" :key="member.inboundCode"><td><strong :title="member.inboundCode">{{ member.inboundCode }}</strong></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无有效入仓码" :image-size="56" />
|
||||
<el-empty v-else description="暂无同柜组成员" :image-size="56" />
|
||||
</template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -1417,7 +1391,6 @@ onBeforeUnmount(() => {
|
||||
:inbound-code="relationOrder.inboundCode"
|
||||
/>
|
||||
</AppDrawer>
|
||||
<InboundCodeRelationshipDialog v-if="inboundRelationshipDialogOpen" v-model="inboundRelationshipDialogOpen" :inbound-code="inboundRelationshipDialogCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
|
||||
|
||||
<AppDialog v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
|
||||
|
||||
Reference in New Issue
Block a user