feat: connect plan-level preload confirmation
This commit is contained in:
@@ -40,7 +40,7 @@ import {
|
||||
firstLegTransportModeLabel,
|
||||
} from '@/shared/utils/firstLegBusinessLabels'
|
||||
import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows'
|
||||
import SmartLoadingDialog from '@/modules/container-loading/components/SmartLoadingDialog.vue'
|
||||
import { SmartLoadingDialog } from '@/modules/container-loading'
|
||||
import {
|
||||
unifiedOrderHistoryActionLabel,
|
||||
unifiedOrderLoadingStatusLabel,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
真实接口均使用 `/api/platform/v1/inbounds/{inboundId}`:
|
||||
|
||||
- `POST /split/smart`:智能分柜/保存预装清单
|
||||
- `GET /preload`、`POST /preload/confirming`、`POST /preload/confirm`:预装清单查询与确认
|
||||
- `GET /preload`:单票预装状态与明细只读查询;整份计划的确认统一在预装清单真实 `loadingPlan` 详情完成
|
||||
- `GET/POST /loading/picking`、`/loading/picking/start`、`/loading/picking/complete`:拣货
|
||||
- `POST /platform/v1/files/loading-evidence/{tenantId}/{inboundId}`:上传真实装柜证据文件
|
||||
- `GET/POST /loading/evidence`:查询并登记五类装柜证据及锁码
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
adjustSmartLoadingItem,
|
||||
assignSmartLoadingItem,
|
||||
completeLoading,
|
||||
confirmingPreload,
|
||||
deleteSmartLoadingItem,
|
||||
downloadEvidenceFile,
|
||||
getContainerBooking,
|
||||
@@ -62,15 +61,6 @@ describe('unified order workflow API contract', () => {
|
||||
await expect(getPreload('2', '8')).rejects.toThrow('无权限')
|
||||
})
|
||||
|
||||
it('supports the intermediate confirming preload action', async () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'WAIT_SPLIT', preloadPlan: null } })
|
||||
await confirmingPreload('2', '8', { expectedVersion: 1 })
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/platform/v1/inbounds/8/preload/confirming', { expectedVersion: 1 },
|
||||
expect.objectContaining({ params: { tenantId: '2' } }),
|
||||
)
|
||||
})
|
||||
|
||||
it('uploads a real evidence file and voids evidence through audited endpoints', async () => {
|
||||
vi.mocked(http.post)
|
||||
.mockResolvedValueOnce({ data: { fileId: '88', fileName: 'empty.jpg' } })
|
||||
|
||||
@@ -186,10 +186,6 @@ export interface SmartSplitRequest {
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface ConfirmPreloadRequest {
|
||||
expectedVersion: number
|
||||
}
|
||||
|
||||
export interface UpsertEvidenceRequest {
|
||||
expectedVersion: number
|
||||
evidenceType: EvidenceType
|
||||
@@ -446,30 +442,6 @@ export async function smartSplit(
|
||||
return normalizeSplitAction(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function confirmingPreload(
|
||||
tenantId: string | number,
|
||||
inboundId: string | number,
|
||||
request: ConfirmPreloadRequest,
|
||||
) {
|
||||
const response = await http.post<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
|
||||
`${inboundBasePath}/${inboundId}/preload/confirming`, request,
|
||||
writeOptions('preload-confirming', inboundId, tenantId),
|
||||
)
|
||||
return normalizeSplitAction(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function confirmPreload(
|
||||
tenantId: string | number,
|
||||
inboundId: string | number,
|
||||
request: ConfirmPreloadRequest,
|
||||
) {
|
||||
const response = await http.post<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
|
||||
`${inboundBasePath}/${inboundId}/preload/confirm`, request,
|
||||
writeOptions('preload-confirm', inboundId, tenantId),
|
||||
)
|
||||
return normalizeSplitAction(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getPicking(tenantId: string | number, inboundId: string | number) {
|
||||
const response = await http.get<ApiEnvelope<PickingTask>, ApiEnvelope<PickingTask>>(
|
||||
`${inboundBasePath}/${inboundId}/loading/picking`, { params: query(tenantId) },
|
||||
|
||||
@@ -83,6 +83,20 @@ describe('UnifiedOrderWorkflowPanel preload editor', () => {
|
||||
expect(wrapper.find('button[aria-label="编辑第 1 条预装明细"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps single-order preload confirmation read-only and links to the plan list', async () => {
|
||||
const wrapper = mountPanel(['firstleg:inbound:preload-view', 'platform:firstleg:preload:list'])
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('计划级预装确认')
|
||||
expect(wrapper.text()).toContain('从实际预装计划详情确认整份计划')
|
||||
expect(wrapper.text()).toContain('前往预装清单')
|
||||
expect(wrapper.text()).not.toContain('提交确认')
|
||||
expect(wrapper.text()).not.toContain('确认预装清单')
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue'), 'utf8')
|
||||
expect(source).toContain("router.push({ name: 'loading-plans' })")
|
||||
})
|
||||
|
||||
it('does not query or expose editing controls without permission', async () => {
|
||||
const wrapper = mountPanel([])
|
||||
await flushPromises()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { RefreshCw, Save, Check, Play, Camera, Eye, Plus, Pencil, Trash2 } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -15,8 +16,6 @@ import { sanitizePreloadItem, validatePreloadItem, validatePreloadItems } from '
|
||||
import {
|
||||
completeLoading,
|
||||
completePicking,
|
||||
confirmingPreload,
|
||||
confirmPreload,
|
||||
downloadEvidenceFile,
|
||||
getEvidence,
|
||||
getOutboundDispatch,
|
||||
@@ -52,6 +51,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits<{ updated: [] }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const preload = ref<PreloadPlan | null>(null)
|
||||
const preloadItems = ref<PreloadItem[]>([])
|
||||
const picking = ref<PickingTask | null>(null)
|
||||
@@ -87,8 +87,8 @@ const preloadVersion = computed(() => preload.value?.version ?? 0)
|
||||
const pickingExpectedVersion = computed(() => picking.value?.version ?? props.pickingVersion ?? null)
|
||||
const inboundExpectedVersion = computed(() => props.inboundVersion)
|
||||
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 canViewLoadingPlans = computed(() => access.can('platform:firstleg:preload:list'))
|
||||
const canPicking = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:picking:edit'))
|
||||
const canPickingView = computed(() => access.can('platform:firstleg:inbound:picking:view'))
|
||||
// 操作员、理货员由 IAM 授予 loading:edit;拣货员仅能在 PDA 现场采集,不授予该权限。
|
||||
@@ -292,18 +292,8 @@ async function runSmartSplit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startPreloadConfirmation() {
|
||||
const result = await run('preload-confirming', () => confirmingPreload(props.tenantId, props.inboundId, {
|
||||
expectedVersion: preloadVersion.value,
|
||||
}), '预装清单已进入确认中')
|
||||
if (result?.preloadPlan) preload.value = result.preloadPlan
|
||||
}
|
||||
|
||||
async function finishPreloadConfirmation() {
|
||||
const result = await run('preload-confirm', () => confirmPreload(props.tenantId, props.inboundId, {
|
||||
expectedVersion: preloadVersion.value,
|
||||
}), '预装清单已确认,订单进入装柜中')
|
||||
if (result?.preloadPlan) preload.value = result.preloadPlan
|
||||
function openPreloadPlans() {
|
||||
void router.push({ name: 'loading-plans' })
|
||||
}
|
||||
|
||||
function pickingRequest(): { expectedVersion: number; expectedCount: number; pickedCount: number; remark?: string } | null {
|
||||
@@ -465,8 +455,11 @@ onMounted(() => { void refresh() })
|
||||
</template>
|
||||
<div class="workflow-actions">
|
||||
<el-button :disabled="!canSplit || Boolean(busy) || Boolean(preloadLoadError)" :loading="busy === 'smart-split'" @click="runSmartSplit"><Save :size="15" />保存分柜清单</el-button>
|
||||
<el-button :disabled="!canPreload || !preload || preload.status !== 'PENDING' || Boolean(busy)" :loading="busy === 'preload-confirming'" @click="startPreloadConfirmation"><Play :size="15" />提交确认</el-button>
|
||||
<el-button type="primary" :disabled="!canPreload || !preload || preload?.status !== 'CONFIRMING' || Boolean(busy)" :loading="busy === 'preload-confirm'" @click="finishPreloadConfirmation"><Check :size="15" />确认预装清单</el-button>
|
||||
</div>
|
||||
<div class="preload-confirmation-guide">
|
||||
<div><strong>计划级预装确认</strong><span>当前单票状态:{{ preloadStatusLabel(preload?.status) }}。请前往预装清单,从实际预装计划详情确认整份计划。</span></div>
|
||||
<el-button v-if="canViewLoadingPlans" type="primary" plain @click="openPreloadPlans"><Eye :size="15" />前往预装清单</el-button>
|
||||
<span v-else class="workflow-hint">当前账号无预装清单列表权限。</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -573,6 +566,11 @@ onMounted(() => { void refresh() })
|
||||
.preload-dialog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.workflow-actions { margin-top: 14px; }
|
||||
.workflow-remark { margin-top: 10px; }
|
||||
.preload-confirmation-guide { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: #f8fafc; }
|
||||
.preload-confirmation-guide > div { display: grid; min-width: 0; gap: 4px; }
|
||||
.preload-confirmation-guide strong { color: var(--cc-text, #1f2937); font-size: 13px; }
|
||||
.preload-confirmation-guide span { color: var(--cc-text-secondary, #64748b); font-size: 12px; line-height: 1.5; }
|
||||
.preload-confirmation-guide > .el-button { flex: 0 0 auto; }
|
||||
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.evidence-item { display: grid; min-width: 0; gap: 8px; align-content: start; padding: 12px; border: 1px solid var(--cc-border); border-radius: 6px; }
|
||||
.evidence-item strong { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||
@@ -592,6 +590,7 @@ onMounted(() => { void refresh() })
|
||||
@media (max-width: 640px) {
|
||||
.preload-dialog-grid, .evidence-grid { grid-template-columns: 1fr; }
|
||||
.preload-dialog-grid { gap: 0; }
|
||||
.preload-confirmation-guide { align-items: stretch; flex-direction: column; }
|
||||
.evidence-item, .gate-grid { grid-template-columns: 1fr; }
|
||||
.workflow-actions > .el-button { flex: 1 1 150px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user