fix(admin): complete container loading workflows
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Boxes, Eye, Pencil, Plus, Sparkles } from '@lucide/vue'
|
||||
import { Boxes, Eye, ListChecks, Pencil, Plus, Sparkles } from '@lucide/vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
@@ -16,7 +16,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { createContainer, getContainer, listContainers, updateContainer, updateContainerTransport } from '../api/platformContainerLoading'
|
||||
import type { ContainerBookingResource, ContainerPlanStatus, ContainerSaveRequest } from '../types'
|
||||
|
||||
type Filters = QueryModel & { keyword: string; warehouseId: string; containerType: string; status: ContainerPlanStatus | ''; sailingRange: string[] }
|
||||
type Filters = QueryModel & { keyword: string; containerType: string; status: ContainerPlanStatus | ''; sailingRange: string[] }
|
||||
type ContainerForm = ContainerSaveRequest & { id: string; version: number }
|
||||
|
||||
const router = useRouter()
|
||||
@@ -30,7 +30,7 @@ const warehouseError = ref<ApiErrorDetails | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const filters = ref<Filters>({ keyword: '', warehouseId: '', containerType: '', status: '', sailingRange: [] })
|
||||
const filters = ref<Filters>({ keyword: '', containerType: '', status: '', sailingRange: [] })
|
||||
const applied = ref<Filters>({ ...filters.value })
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
@@ -65,18 +65,19 @@ const rules: FormRules<ContainerForm> = {
|
||||
const canCreate = computed(() => access.can('platform:firstleg:container:create'))
|
||||
const canEdit = computed(() => access.can('platform:firstleg:container:edit'))
|
||||
const canSmartLoading = computed(() => access.can('platform:firstleg:smart-loading:list'))
|
||||
const canViewPreload = computed(() => access.can('platform:firstleg:preload:list'))
|
||||
const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list'))
|
||||
const editorTitle = computed(() => form.id ? (detail.value?.planStatus === 'LOADING' ? '修改运输信息' : '编辑货柜') : '新增货柜')
|
||||
const transportOnly = computed(() => Boolean(form.id) && detail.value?.planStatus === 'LOADING')
|
||||
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'keyword', type: 'input', label: '关键词', width: 220, placeholder: '柜次号、柜号或提单号', props: { 'aria-label': '关键词', maxlength: 128 } },
|
||||
{ key: 'warehouseId', type: 'select', label: '装柜仓库', width: 220, options: warehouses.value.map((warehouse) => ({ label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`, value: String(warehouse.id) })), props: { 'aria-label': '装柜仓库', filterable: true, loading: warehouseLoading.value } },
|
||||
{ key: 'containerType', type: 'select', label: '柜型', width: 140, options: ['20GP', '40GP', '40HQ', '45HQ'].map((value) => ({ label: value, value })), props: { 'aria-label': '柜型' } },
|
||||
{ key: 'status', type: 'select', label: '排柜状态', width: 150, options: [{ label: '编辑中', value: 'EDITING' }, { label: '装柜中', value: 'LOADING' }, { label: '已完成', value: 'COMPLETED' }], props: { 'aria-label': '排柜状态' } },
|
||||
{ key: 'sailingRange', type: 'daterange', label: '预计开船', width: 260, props: { 'aria-label': '预计开船日期范围' } },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 230, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 310, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'cabinetNo', label: '柜次', minWidth: 180, slotName: 'cabinet' },
|
||||
{ key: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'containerNo' },
|
||||
{ key: 'planStatus', label: '状态', width: 110, slotName: 'status' },
|
||||
@@ -131,7 +132,7 @@ async function loadRows() {
|
||||
try {
|
||||
const result = await listContainers({
|
||||
page: page.value, size: pageSize.value, keyword: applied.value.keyword.trim() || undefined,
|
||||
warehouseId: applied.value.warehouseId || undefined, containerType: applied.value.containerType || undefined,
|
||||
containerType: applied.value.containerType || undefined,
|
||||
status: applied.value.status, estimatedSailingFrom: from ? `${from}T00:00:00` : undefined,
|
||||
estimatedSailingTo: to ? `${to}T23:59:59` : undefined,
|
||||
})
|
||||
@@ -170,11 +171,36 @@ async function save() {
|
||||
ElMessage.success(form.id ? '货柜已更新' : '货柜已新增'); editorOpen.value = false; await loadRows()
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '保存失败')
|
||||
editorError.value = { ...details, message: details.code === 'A0409' ? '数据已被其他人修改,请刷新最新数据后重试' : details.message }
|
||||
ElMessage.error(details.code === 'A0409' ? '数据已被其他人修改,请刷新列表后重试' : details.message)
|
||||
if (details.code === 'A0409' && form.id) {
|
||||
const submitted = { ...form }
|
||||
try {
|
||||
const latest = await getContainer(form.id)
|
||||
detail.value = latest
|
||||
replaceForm(latest)
|
||||
Object.assign(form, submitted, { id: latest.id, version: latest.version })
|
||||
editorError.value = { ...details, message: '数据已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' }
|
||||
ElMessage.warning(editorError.value.message)
|
||||
} catch (refreshError) {
|
||||
editorError.value = getApiErrorDetails(refreshError, '数据已被其他人修改,且最新数据刷新失败,请稍后重试')
|
||||
ElMessage.error(editorError.value.message)
|
||||
}
|
||||
} else {
|
||||
editorError.value = details
|
||||
ElMessage.error(details.message)
|
||||
}
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
function goWorkbench(source: object) { const row = source as ContainerBookingResource; void router.push({ name: 'smart-loading-workbench', params: { containerId: row.id } }) }
|
||||
function goContainerProcess(source: object) {
|
||||
const row = source as ContainerBookingResource
|
||||
if (row.planStatus === 'EDITING') {
|
||||
void router.push({ name: 'smart-loading-workbench', params: { containerId: row.id } })
|
||||
return
|
||||
}
|
||||
void router.push({
|
||||
name: row.planStatus === 'LOADING' ? 'loading-plans' : 'loading-lists',
|
||||
query: { planId: row.planId || undefined, warehouseId: row.warehouseId },
|
||||
})
|
||||
}
|
||||
|
||||
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||||
if (nextPage !== previousPage || nextSize !== previousSize) void loadRows()
|
||||
@@ -189,14 +215,13 @@ onMounted(() => { void Promise.all([loadWarehouses(), loadRows()]) })
|
||||
<template #toolbar>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="货柜筛选" :fields="queryFields" preference-key="container-loading-query" @search="search" />
|
||||
</template>
|
||||
<template v-if="warehouseError || loadError" #state>
|
||||
<LoadErrorState v-if="warehouseError" title="装柜仓库加载失败" :message="warehouseError.message" compact @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-else-if="loadError" title="货柜列表加载失败" :message="loadError.message" :loading="loading" @retry="loadRows"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<template v-if="loadError" #state>
|
||||
<LoadErrorState title="货柜列表加载失败" :message="loadError.message" :loading="loading" @retry="loadRows"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
</template>
|
||||
<section v-if="!warehouseError && !loadError" class="surface-panel table-panel">
|
||||
<section v-if="!loadError" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" preference-key="container-loading-table" empty-text="暂无货柜">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增货柜</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" :aria-label="containerViewLabel(row as ContainerBookingResource)" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="canEdit && row.planStatus !== 'COMPLETED'" link type="primary" @click="openEdit(row)"><Pencil :size="15" />编辑</el-button><el-button v-if="canSmartLoading && row.planStatus !== 'COMPLETED'" link type="primary" @click="goWorkbench(row)"><Sparkles :size="15" />排柜</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" :aria-label="containerViewLabel(row as ContainerBookingResource)" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="canEdit && row.planStatus !== 'COMPLETED'" link type="primary" @click="openEdit(row)"><Pencil :size="15" />编辑</el-button><el-button v-if="row.planStatus === 'EDITING' && canSmartLoading" link type="primary" @click="goContainerProcess(row)"><Sparkles :size="15" />排柜</el-button><el-button v-else-if="row.planStatus === 'LOADING' && canViewPreload" link type="primary" @click="goContainerProcess(row)"><ListChecks :size="15" />查看预装清单</el-button><el-button v-else-if="row.planStatus === 'COMPLETED' && canViewLoadingList" link type="primary" @click="goContainerProcess(row)"><ListChecks :size="15" />查看装柜清单</el-button></div></template>
|
||||
<template #cabinet="{ row }"><strong>{{ row.cabinetNo }}</strong><small>{{ row.bookingNo }}</small></template>
|
||||
<template #containerNo="{ row }">{{ row.containerNo || '待录入' }}</template>
|
||||
<template #status="{ row }"><el-tag :type="row.planStatus === 'COMPLETED' ? 'success' : row.planStatus === 'LOADING' ? 'warning' : 'info'">{{ statusText(row.planStatus) }}</el-tag></template>
|
||||
@@ -213,10 +238,11 @@ onMounted(() => { void Promise.all([loadWarehouses(), loadRows()]) })
|
||||
<el-descriptions v-else-if="detail" :column="2" border class="details"><el-descriptions-item label="柜次号">{{ detail.cabinetNo }}</el-descriptions-item><el-descriptions-item label="集装箱号">{{ detail.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusText(detail.planStatus) }}</el-descriptions-item><el-descriptions-item label="柜型">{{ detail.containerType }}</el-descriptions-item><el-descriptions-item label="装柜仓库">{{ detail.warehouseName || '—' }}</el-descriptions-item><el-descriptions-item label="目的仓">{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}</el-descriptions-item><el-descriptions-item label="船公司">{{ detail.shippingCompanyName || '—' }}</el-descriptions-item><el-descriptions-item label="船名/航次">{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}</el-descriptions-item><el-descriptions-item label="提单号">{{ detail.billOfLadingNo || '—' }}</el-descriptions-item><el-descriptions-item label="拖车公司">{{ detail.terminalTruckingCompany || '—' }}</el-descriptions-item><el-descriptions-item label="预计开船">{{ formatTime(detail.estimatedSailingAt) }}</el-descriptions-item><el-descriptions-item label="预计到港">{{ formatTime(detail.estimatedPortArrivalAt) }}</el-descriptions-item></el-descriptions>
|
||||
</AppDrawer>
|
||||
|
||||
<AppDrawer v-model="editorOpen" :title="editorTitle" size="min(860px, 96vw)" :confirm-loading="saving" confirm-text="保存" @confirm="save">
|
||||
<AppDrawer v-model="editorOpen" :title="editorTitle" size="min(860px, 96vw)" :confirm-loading="saving" :confirm-disabled="Boolean(warehouseError && !transportOnly)" confirm-text="保存" @confirm="save">
|
||||
<LoadErrorState v-if="editorError" title="货柜保存失败" :message="editorError.message" compact :retry-text="form.id ? '刷新最新数据' : '重试保存'" @retry="form.id ? openEdit({ id: form.id }) : save()"><template #details><ApiErrorMeta :details="editorError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="warehouseError && !transportOnly" title="装柜仓库加载失败" :message="warehouseError.message" compact @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<el-alert v-if="transportOnly" title="装柜中仅允许修改集装箱号和航次,其他字段已锁定。" type="warning" :closable="false" />
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="form-grid">
|
||||
<el-form v-if="!warehouseError || transportOnly" ref="formRef" :model="form" :rules="rules" label-position="top" class="form-grid">
|
||||
<el-form-item label="柜次号" prop="cabinetNo"><el-input v-model="form.cabinetNo" maxlength="64" :disabled="transportOnly" /></el-form-item>
|
||||
<el-form-item label="集装箱号"><el-input v-model="form.containerNo" maxlength="64" placeholder="开始装柜前必须填写" /></el-form-item>
|
||||
<el-form-item label="装柜仓库" prop="warehouseId"><el-select v-model="form.warehouseId" filterable :disabled="transportOnly"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`" :value="String(warehouse.id)" /></el-select></el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user