优化
This commit is contained in:
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { unifiedOrderStatusOptions } from '../api/unifiedOrders'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/pages/UnifiedOrdersView.vue'), 'utf8')
|
||||
const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8')
|
||||
|
||||
describe('UnifiedOrdersView contract', () => {
|
||||
it('only exposes the seven product main statuses', () => {
|
||||
@@ -57,12 +58,11 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('v-model:page-size="pageSize"')
|
||||
})
|
||||
|
||||
it('keeps business actions as explicit C01/C03/C04/C06 boundaries', () => {
|
||||
it('keeps only confirmed business actions as explicit boundaries', () => {
|
||||
expect(source).toContain('nextActionBoundary')
|
||||
expect(source).toContain('待接入:${label}')
|
||||
expect(source).toContain('CONFIRM_PRELOAD')
|
||||
expect(source).toContain("can('platform:firstleg:inbound:view')")
|
||||
expect(source).toContain('openMeasurement')
|
||||
expect(source).not.toContain('待接入:智能分柜')
|
||||
expect(source).not.toContain('updateMainStatus')
|
||||
expect(source).not.toContain('人工进入分柜')
|
||||
expect(source).not.toContain('performPlatformUnifiedOrderAction')
|
||||
@@ -85,6 +85,9 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('aria-label="状态历史"')
|
||||
expect(source).toContain('aria-label="内部备注"')
|
||||
expect(source).toContain('订单详情暂时无法加载')
|
||||
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
|
||||
expect(source).toContain('aria-label="更多订单操作"')
|
||||
expect(source).toContain("can('platform:order:note')")
|
||||
})
|
||||
|
||||
it('renders order detail as a routed full page instead of a drawer', () => {
|
||||
@@ -97,9 +100,10 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain('detailOpen')
|
||||
})
|
||||
|
||||
it('exposes the C06 workflow entry only for inbound-backed downstream states', () => {
|
||||
expect(source).toContain("name: 'unified-order-workflow'")
|
||||
expect(source).toContain("['WAIT_SPLIT', 'LOADING', 'LOADED'].includes(row.mainStatus)")
|
||||
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')
|
||||
expect(routeSource).not.toContain('unified-order-workflow')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, ClipboardList, Eye, History, ListChecks, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -80,6 +80,7 @@ const carrierForm = reactive({ carrierId: '' })
|
||||
const noteOpen = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
const noteContent = ref('')
|
||||
const noteOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const carrierRules: FormRules = {
|
||||
@@ -305,19 +306,8 @@ function measurementStatusLabel(value: string | null | undefined) {
|
||||
return ({ UNRECORDED: '未录入', PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' } as Record<string, string>)[value || ''] || value || '未录入'
|
||||
}
|
||||
|
||||
const nextActionLabels: Record<string, string> = {
|
||||
APPROVE_ORDER: '审核通过',
|
||||
CONFIRM_MEASUREMENT: '完成测量',
|
||||
SMART_SPLIT: '智能分柜',
|
||||
CONFIRM_PRELOAD: '确认预装清单',
|
||||
COMPLETE_LOADING: '确认装柜完成',
|
||||
COMPLETE_DELIVERY: '确认派送完成',
|
||||
}
|
||||
|
||||
function nextActionBoundary(row: UnifiedOrderResource) {
|
||||
if (row.nextAction === 'APPROVE_ORDER') return ''
|
||||
const label = row.nextAction ? nextActionLabels[row.nextAction] || row.nextAction : ''
|
||||
return label ? `待接入:${label}` : ''
|
||||
return row.nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''
|
||||
}
|
||||
|
||||
function canApprove(row: UnifiedOrderResource) {
|
||||
@@ -407,23 +397,29 @@ async function assignCarrierAndApprove() {
|
||||
}
|
||||
}
|
||||
|
||||
function openNoteDialog() {
|
||||
if (!detail.value || !can('platform:order:note')) return
|
||||
function openNoteDialog(order?: UnifiedOrderResource) {
|
||||
if (!can('platform:order:note')) return
|
||||
const target = order || detail.value?.summary || null
|
||||
if (!target) return
|
||||
noteOrder.value = target
|
||||
noteContent.value = ''
|
||||
noteOpen.value = true
|
||||
}
|
||||
|
||||
async function saveNote() {
|
||||
const content = noteContent.value.trim()
|
||||
if (!detail.value || !content) {
|
||||
if (!noteOrder.value || !content) {
|
||||
ElMessage.warning('请填写内部备注')
|
||||
return
|
||||
}
|
||||
noteSaving.value = true
|
||||
try {
|
||||
await addPlatformUnifiedOrderNote(String(detail.value.summary.tenantId), String(detail.value.summary.orderId), content)
|
||||
const target = noteOrder.value
|
||||
await addPlatformUnifiedOrderNote(String(target.tenantId), String(target.orderId), content)
|
||||
noteOpen.value = false
|
||||
await loadDetail()
|
||||
if (detail.value?.summary.orderId === target.orderId) await loadDetail()
|
||||
else await loadOrders()
|
||||
noteOrder.value = null
|
||||
ElMessage.success('内部备注已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '内部备注保存失败,请稍后重试').message)
|
||||
@@ -533,17 +529,20 @@ function openMeasurement(row: UnifiedOrderResource) {
|
||||
void router.push({ name: 'inbound-order-detail', params: { inboundId: String(row.inboundId) }, query: { tenantId: String(row.tenantId) } })
|
||||
}
|
||||
|
||||
function workflowEligible(row: UnifiedOrderResource) {
|
||||
return Boolean(row.inboundId && ['WAIT_SPLIT', 'LOADING', 'LOADED'].includes(row.mainStatus))
|
||||
function openInboundFromDetail() {
|
||||
const summary = detail.value?.summary
|
||||
if (!summary?.inboundId || !can('platform:firstleg:inbound:view')) return
|
||||
void router.push({
|
||||
name: 'inbound-order-detail',
|
||||
params: { inboundId: String(summary.inboundId) },
|
||||
query: { tenantId: String(summary.tenantId) },
|
||||
})
|
||||
}
|
||||
|
||||
function openWorkflow(row: UnifiedOrderResource) {
|
||||
if (!workflowEligible(row) || !can('platform:firstleg:inbound:view')) return
|
||||
void router.push({
|
||||
name: 'unified-order-workflow',
|
||||
params: { orderId: String(row.orderId) },
|
||||
query: { tenantId: String(row.tenantId) },
|
||||
})
|
||||
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
|
||||
if (command === 'note') openNoteDialog(row)
|
||||
if (command === 'approve') openApproval(row)
|
||||
if (command === 'measurement') openMeasurement(row)
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
@@ -590,18 +589,19 @@ onMounted(() => {
|
||||
<template v-if="orderId">
|
||||
<header class="order-detail-header">
|
||||
<div class="order-detail-heading">
|
||||
<el-button class="back-button" @click="closeDetail"><ArrowLeft :size="17" />返回订单列表</el-button>
|
||||
<span class="order-mark" aria-hidden="true"><Box :size="23" /></span>
|
||||
<el-button link class="back-button" @click="closeDetail"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<span class="order-mark" aria-hidden="true"><Box :size="19" /></span>
|
||||
<div class="order-title">
|
||||
<span>入仓码</span>
|
||||
<h1>{{ detail?.summary.inboundCode || '暂未生成' }}</h1>
|
||||
<h1>
|
||||
<el-button v-if="detail?.summary.inboundId && can('platform:firstleg:inbound:view')" link type="primary" class="inbound-code-link" @click="openInboundFromDetail">{{ detail.summary.inboundCode || `入库单 ${detail.summary.inboundId}` }}</el-button>
|
||||
<span v-else>{{ detail?.summary.inboundCode || '暂未生成' }}</span>
|
||||
</h1>
|
||||
<p>{{ detail?.summary.orderNo || `订单 ${orderId}` }}</p>
|
||||
</div>
|
||||
<StatusBadge v-if="detail" :status="statusLabel(detail.summary.mainStatus)" />
|
||||
</div>
|
||||
<div v-if="detail" class="detail-header-actions">
|
||||
<div v-if="detail && canApprove(detail.summary)" class="detail-header-actions">
|
||||
<el-button v-if="canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" />审核通过</el-button>
|
||||
<el-button v-if="workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" @click="openWorkflow(detail.summary)"><ClipboardList :size="15" />后半程作业</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -669,7 +669,7 @@ onMounted(() => {
|
||||
</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>
|
||||
<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>
|
||||
<el-empty v-if="detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="64" />
|
||||
<ul v-else class="notes-list" aria-label="内部备注"><li v-for="item in detail.order.internalNotes" :key="String(item.id)"><p>{{ item.content }}</p><span>操作人 {{ item.createdBy || '系统' }} · {{ formatDate(item.createdAt) }}</span></li></ul>
|
||||
</template>
|
||||
@@ -710,15 +710,24 @@ onMounted(() => {
|
||||
|
||||
<div v-else id="unified-orders-results" class="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" empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleSelectionChange">
|
||||
<template #toolbar-start>
|
||||
<template #toolbar-start><div><h2>统一订单列表</h2><p>共 {{ total }} 条订单</p></div></template>
|
||||
<template #toolbar-actions>
|
||||
<el-button type="primary" plain :disabled="selectedOrders.length === 0" title="批量审核接口待接入" @click="openBulkReviewPlaceholder"><ListChecks :size="16" />批量审核<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as UnifiedOrderResource)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="canApprove(row as UnifiedOrderResource)" link type="success" :loading="approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="openApproval(row as UnifiedOrderResource)"><CircleCheck :size="15" />审核通过</el-button>
|
||||
<el-button v-if="(row as UnifiedOrderResource).mainStatus === 'WAIT_MEASURE' && (row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view')" link type="primary" @click="openMeasurement(row as UnifiedOrderResource)">测量作业</el-button>
|
||||
<el-button v-if="workflowEligible(row as UnifiedOrderResource) && can('platform:firstleg:inbound:view')" link type="primary" @click="openWorkflow(row as UnifiedOrderResource)"><ClipboardList :size="15" />后半程作业</el-button>
|
||||
<el-dropdown trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)">
|
||||
<el-button link type="primary" aria-label="更多订单操作"><MoreHorizontal :size="17" />更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="can('platform:order:note')" command="note"><StickyNote :size="15" />添加备注</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canApprove(row as UnifiedOrderResource)" command="approve"><CircleCheck :size="15" />审核通过</el-dropdown-item>
|
||||
<el-dropdown-item v-if="(row as UnifiedOrderResource).mainStatus === 'WAIT_MEASURE' && (row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view')" command="measurement">测量作业</el-dropdown-item>
|
||||
<el-dropdown-item v-if="!can('platform:order:note') && !canApprove(row as UnifiedOrderResource) && !((row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view'))" disabled>暂无可用操作</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<span v-if="nextActionBoundary(row as UnifiedOrderResource)" class="action-boundary">{{ nextActionBoundary(row as UnifiedOrderResource) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -784,14 +793,15 @@ onMounted(() => {
|
||||
.primary-cell { display: grid; min-width: 0; gap: 4px; }
|
||||
.primary-cell strong { overflow-wrap: anywhere; }
|
||||
.primary-cell span { color: var(--cc-muted); font-size: 12px; overflow-wrap: anywhere; }
|
||||
.order-detail-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 18px; }
|
||||
.order-detail-heading { display: flex; min-width: 0; align-items: center; gap: 12px; }
|
||||
.back-button { flex: 0 0 auto; }
|
||||
.order-mark { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; border-radius: 8px; background: #1d2939; color: #fff; }
|
||||
.order-detail-header { display: flex; min-width: 0; min-height: 42px; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; }
|
||||
.order-detail-heading { display: flex; min-width: 0; align-items: center; gap: 10px; }
|
||||
.back-button { flex: 0 0 auto; padding: 0; }
|
||||
.order-mark { display: grid; width: 36px; height: 36px; flex: 0 0 36px; place-items: center; border-radius: 7px; background: #1d2939; color: #fff; }
|
||||
.order-title { min-width: 0; }
|
||||
.order-title > span { display: block; margin-bottom: 2px; color: var(--cc-muted); font-size: 11px; }
|
||||
.order-title h1 { margin: 0; overflow: hidden; color: #172033; font-size: 22px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-title p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.order-title h1 { margin: 0; overflow: hidden; color: #172033; font-size: 18px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.inbound-code-link { width: 100%; max-width: 100%; overflow: hidden; justify-content: flex-start; padding: 0; color: var(--cc-primary); font: inherit; font-weight: 700; vertical-align: baseline; }
|
||||
.inbound-code-link :deep(span) { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-title p { margin: 2px 0 0; color: var(--cc-muted); font-size: 11px; }
|
||||
.detail-header-actions { display: flex; flex: 0 0 auto; gap: 8px; }
|
||||
.detail-header-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.detail-load-error { min-height: 280px; }
|
||||
@@ -835,6 +845,6 @@ onMounted(() => {
|
||||
.notes-list li > p { margin: 0; color: var(--cc-text); font-size: 13px; }
|
||||
.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 40px minmax(0, 1fr) auto; } .order-mark { width: 40px; height: 40px; } .detail-header-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-header-actions :deep(.el-button) { min-height: 44px; } .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; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: 1fr auto; } .back-button { grid-column: 1 / -1; justify-self: start; } .order-mark { display: none; } .detail-header-actions { grid-template-columns: 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; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } }
|
||||
</style>
|
||||
|
||||
@@ -12,17 +12,6 @@ export const unifiedOrderRoutes = [
|
||||
permissionAnyOf: ['platform:firstleg:inbound:list'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'orders/:orderId/workflow',
|
||||
name: 'unified-order-workflow',
|
||||
component: () => import('./workflows/pages/UnifiedOrderWorkflowView.vue'),
|
||||
meta: {
|
||||
title: '订单后半程作业',
|
||||
description: '处理分柜、预装、装柜和派送作业',
|
||||
permission: 'platform:firstleg:inbound:view',
|
||||
permissionAnyOf: ['platform:firstleg:inbound:list', 'platform:order:list'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'orders/:orderId',
|
||||
name: 'unified-order-detail',
|
||||
|
||||
Reference in New Issue
Block a user