feat: connect plan-level preload confirmation
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Eye, Pencil } from '@lucide/vue'
|
||||
import { Eye } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
@@ -16,12 +14,9 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { loadingListStatusLabel } from '../status'
|
||||
import {
|
||||
getPreloadPlan,
|
||||
listConfirmedLoadingLists,
|
||||
listPreloadPlans,
|
||||
updatePreloadTransport,
|
||||
type ConfirmedSummary,
|
||||
type LoadingDetail,
|
||||
type LoadingListMode,
|
||||
type LoadingListQuery,
|
||||
type LoadingListSummary,
|
||||
@@ -41,16 +36,6 @@ const total = ref(0)
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const customersLoading = ref(false)
|
||||
const query = reactive<QueryModel>({ customerId: '', keyword: '', createdRange: [] })
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const detail = ref<LoadingDetail | null>(null)
|
||||
const selectedPlanId = ref('')
|
||||
const selectedWarehouseId = ref('')
|
||||
const editOpen = ref(false)
|
||||
const editSaving = ref(false)
|
||||
const editError = ref<ApiErrorDetails | null>(null)
|
||||
const editFormRef = ref<FormInstance>()
|
||||
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
|
||||
const handledDeepLink = ref('')
|
||||
|
||||
const isPreload = computed(() => props.mode === 'preload')
|
||||
@@ -58,18 +43,17 @@ const title = computed(() => isPreload.value ? '预装清单' : '装柜清单')
|
||||
const listPermission = computed(() => isPreload.value ? 'platform:firstleg:preload:list' : 'platform:firstleg:loading-list:list')
|
||||
const canList = computed(() => access.can(listPermission.value))
|
||||
const canView = computed(() => canList.value)
|
||||
const canEdit = computed(() => isPreload.value && access.can('platform:firstleg:preload:edit'))
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'keyword', type: 'input', label: '关键词', placeholder: isPreload.value ? '预装单号/集装箱号/柜号' : '装柜清单号/集装箱号/柜号', width: 250 },
|
||||
{ key: 'keyword', type: 'input', label: '关键词', placeholder: isPreload.value ? '预装单号/柜次号/集装箱号' : '装柜清单号/柜次号/集装箱号', width: 270 },
|
||||
{ key: 'createdRange', type: 'daterange', label: '创建日期', width: 260 },
|
||||
])
|
||||
|
||||
const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 230, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 110, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'preloadNo', prop: 'preloadNo', label: '预装单号', minWidth: 165 },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145 },
|
||||
{ key: 'internalContainerNo', prop: 'internalContainerNo', label: '柜号', minWidth: 130 },
|
||||
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 150 },
|
||||
{ key: 'customers', label: '客户', minWidth: 190, slotName: 'customers' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 140 },
|
||||
{ key: 'vesselVoyage', label: '船名 / 航次', minWidth: 175, slotName: 'vesselVoyage' },
|
||||
@@ -84,7 +68,7 @@ const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 110, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'loadingListNo', prop: 'loadingListNo', label: '装柜清单号', minWidth: 175 },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145 },
|
||||
{ key: 'internalContainerNo', prop: 'internalContainerNo', label: '柜号', minWidth: 130 },
|
||||
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 150 },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 140 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' },
|
||||
@@ -97,12 +81,6 @@ const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
|
||||
{ key: 'createdAt', label: '创建时间', minWidth: 155, slotName: 'createdAt' },
|
||||
])
|
||||
|
||||
const editRules: FormRules = {
|
||||
containerNo: [{ max: 64, message: '集装箱号不能超过 64 个字符', trigger: 'blur' }],
|
||||
voyageNo: [{ max: 64, message: '航次不能超过 64 个字符', trigger: 'blur' }],
|
||||
operationReason: [{ max: 500, message: '修改原因不能超过 500 个字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
return value ? value.replace('T', ' ').slice(0, 16) : '—'
|
||||
}
|
||||
@@ -165,27 +143,6 @@ function search() {
|
||||
}
|
||||
|
||||
|
||||
async function loadDetail() {
|
||||
if (!canView.value) return
|
||||
if (!selectedPlanId.value || !selectedWarehouseId.value) {
|
||||
detail.value = null
|
||||
detailError.value = {
|
||||
message: '该清单缺少所属仓库信息,无法安全查询详情,请联系管理员核对数据。',
|
||||
code: 'MISSING_WAREHOUSE_CONTEXT',
|
||||
}
|
||||
return
|
||||
}
|
||||
detailLoading.value = true
|
||||
detailError.value = null
|
||||
try {
|
||||
detail.value = await getPreloadPlan(selectedPlanId.value, selectedWarehouseId.value)
|
||||
} catch (cause) {
|
||||
detailError.value = getApiErrorDetails(cause, '清单详情加载失败,请稍后重试')
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(rowValue: object) {
|
||||
const row = rowValue as LoadingListSummary
|
||||
if (isPreload.value) {
|
||||
@@ -195,59 +152,6 @@ function openDetail(rowValue: object) {
|
||||
void router.push({ name: 'loading-list-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
|
||||
}
|
||||
|
||||
async function openEditFromRow(rowValue: object) {
|
||||
const row = rowValue as LoadingListSummary
|
||||
if (!isPreload.value || !canEdit.value || !['EDITING', 'LOADING'].includes(row.status)) return
|
||||
selectedPlanId.value = String(row.planId)
|
||||
selectedWarehouseId.value = String(row.warehouseId || '')
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
await loadDetail()
|
||||
const loadedDetail = detail.value as LoadingDetail | null
|
||||
if (loadedDetail && ['EDITING', 'LOADING'].includes(loadedDetail.summary.status)) openEdit()
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
if (!detail.value || !('preloadNo' in detail.value.summary) || !['EDITING', 'LOADING'].includes(detail.value.summary.status)) return
|
||||
editForm.containerNo = detail.value.summary.containerNo || ''
|
||||
editForm.voyageNo = detail.value.summary.voyageNo || ''
|
||||
editForm.operationReason = ''
|
||||
editError.value = null
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
async function saveTransport() {
|
||||
if (!detail.value || !('preloadNo' in detail.value.summary)) return
|
||||
const valid = await editFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
editSaving.value = true
|
||||
editError.value = null
|
||||
try {
|
||||
detail.value = await updatePreloadTransport(selectedPlanId.value, selectedWarehouseId.value, {
|
||||
containerNo: editForm.containerNo.trim() || null,
|
||||
voyageNo: editForm.voyageNo.trim() || null,
|
||||
operationReason: editForm.operationReason.trim() || null,
|
||||
expectedVersion: detail.value.summary.version,
|
||||
})
|
||||
editOpen.value = false
|
||||
ElMessage.success('运输信息已更新')
|
||||
await load()
|
||||
} catch (cause) {
|
||||
const details = getApiErrorDetails(cause, '运输信息保存失败,请刷新详情后重试')
|
||||
if (details.code === 'A0409' || /版本|并发|已被其他人修改/.test(details.message)) {
|
||||
await loadDetail()
|
||||
editError.value = {
|
||||
...details,
|
||||
message: '运输信息已被其他人修改,系统已刷新最新版本;当前输入已保留,请核对后重新保存。',
|
||||
}
|
||||
} else {
|
||||
editError.value = details
|
||||
}
|
||||
} finally {
|
||||
editSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomers() {
|
||||
customersLoading.value = true
|
||||
try {
|
||||
@@ -305,7 +209,6 @@ onMounted(() => {
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button v-if="canView" size="small" type="primary" plain @click="openDetail(row)"><Eye :size="15" />查看</el-button>
|
||||
<el-button v-if="isPreload && canEdit && ['EDITING', 'LOADING'].includes(row.status)" size="small" @click="openEditFromRow(row)"><Pencil :size="15" />编辑运输信息</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #customers="{ row }">{{ (row as PreloadSummary).customers.map((item) => item.customerName || '客户信息缺失').join('、') || '—' }}</template>
|
||||
@@ -319,26 +222,6 @@ onMounted(() => {
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog
|
||||
v-model="editOpen"
|
||||
class="loading-transport-dialog"
|
||||
title="编辑预装运输信息"
|
||||
width="min(620px, calc(100vw - 28px))"
|
||||
confirm-text="保存"
|
||||
:confirm-loading="editSaving"
|
||||
:confirm-disabled="!canEdit"
|
||||
@confirm="saveTransport"
|
||||
>
|
||||
<el-alert type="info" :closable="false" show-icon title="仅拣货中允许修改;保存时按详情版本执行乐观锁校验。" />
|
||||
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
|
||||
<el-form-item label="集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" show-word-limit placeholder="请输入集装箱号" /></el-form-item>
|
||||
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" show-word-limit placeholder="请输入航次" /></el-form-item>
|
||||
<el-form-item label="修改原因" prop="operationReason"><el-input v-model="editForm.operationReason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="选填,填写后将记录在操作历史中" /></el-form-item>
|
||||
</el-form>
|
||||
<LoadErrorState v-if="editError" compact title="保存失败" :message="editError.message" :show-retry="false">
|
||||
<template #details><ApiErrorMeta :details="editError" /></template>
|
||||
</LoadErrorState>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</AdminPage>
|
||||
</template>
|
||||
@@ -346,10 +229,7 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.loading-lists-page { display: grid; min-width: 0; gap: 16px; }
|
||||
.table-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 16px; }
|
||||
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.row-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||
.row-actions :deep(.el-button) { margin-left: 0; }
|
||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
||||
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } .row-actions { gap: 4px; } }
|
||||
@media (max-width: 720px) { .row-actions { gap: 4px; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user