151 lines
8.2 KiB
Vue
151 lines
8.2 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminDensity.css'
|
|
import '@/styles/adminWorkbench.css'
|
|
import { computed, reactive, ref } from 'vue'
|
|
import { Container, Plus } from '@lucide/vue'
|
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
|
import { listWarehouseLogisticsChannels, type OceanLogisticsChannel } from '@/modules/ocean-bookings'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE, applyContainerSpecification } from '@/shared/domain/containerSpecifications'
|
|
import { createContainerSimulation } from '../api/platformContainerLoading'
|
|
import type { ContainerSimulationCreateRequest } from '../types'
|
|
import { SMART_LOADING_CHANNEL_GROUPS } from '../smartLoadingChannelGroups'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
loadWarehouseOptions?: () => Promise<PlatformWarehouseSummaryResource[]>
|
|
successMessage?: string
|
|
dense?: boolean
|
|
}>(), { successMessage: '模拟柜已创建,可先排货后关联真实订舱柜' })
|
|
const emit = defineEmits<{ created: [] }>()
|
|
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
|
const warehouseLoading = ref(false)
|
|
let warehousesLoaded = false
|
|
let channelSequence = 0
|
|
const simulationOpen = ref(false)
|
|
const simulationSaving = ref(false)
|
|
const simulationError = ref<ApiErrorDetails | null>(null)
|
|
const simulationChannels = ref<OceanLogisticsChannel[]>([])
|
|
const simulationChannelsLoading = ref(false)
|
|
const simulationFormRef = ref<FormInstance>()
|
|
const simulationForm = reactive<ContainerSimulationCreateRequest>({ channelGroupCode: '', warehouseId: '', containerType: DEFAULT_CONTAINER_TYPE, capacityCbm: 68, maxWeightKg: 19500 })
|
|
const simulationChannelGroups = computed(() => {
|
|
const activeCodes = new Set(simulationChannels.value.map((channel) => channel.channelCode))
|
|
return SMART_LOADING_CHANNEL_GROUPS.filter((group) => group.channelCodes.some((code) => activeCodes.has(code)))
|
|
})
|
|
const simulationRules: FormRules<ContainerSimulationCreateRequest> = {
|
|
warehouseId: [{ required: true, message: '请选择装柜仓库', trigger: 'change' }],
|
|
channelGroupCode: [{ required: true, message: '请选择物流渠道', trigger: 'change' }],
|
|
containerType: [{ required: true, message: '请选择柜型', trigger: 'change' }],
|
|
}
|
|
|
|
async function openSimulation() {
|
|
Object.assign(simulationForm, { channelGroupCode: '', warehouseId: '', containerType: DEFAULT_CONTAINER_TYPE, capacityCbm: 68, maxWeightKg: 19500 })
|
|
simulationChannels.value = []
|
|
simulationError.value = null
|
|
simulationOpen.value = true
|
|
channelSequence++
|
|
simulationChannelsLoading.value = false
|
|
simulationFormRef.value?.clearValidate()
|
|
if (warehousesLoaded) return
|
|
warehouseLoading.value = true
|
|
try {
|
|
warehouses.value = props.loadWarehouseOptions
|
|
? await props.loadWarehouseOptions()
|
|
: (await listAllPlatformWarehouses({ status: 'ACTIVE' })).filter((warehouse) => warehouse.warehouseType === 'DOMESTIC')
|
|
warehousesLoaded = true
|
|
} catch (error) { simulationError.value = getApiErrorDetails(error, '装柜仓库加载失败') }
|
|
finally { warehouseLoading.value = false }
|
|
}
|
|
function selectSimulationContainerType(containerType: string) {
|
|
applyContainerSpecification(simulationForm, containerType)
|
|
}
|
|
async function selectSimulationWarehouse(value: unknown) {
|
|
const sequence = ++channelSequence
|
|
simulationChannelsLoading.value = false
|
|
simulationForm.warehouseId = String(value || '')
|
|
simulationForm.channelGroupCode = ''
|
|
simulationChannels.value = []
|
|
simulationError.value = null
|
|
if (!simulationForm.warehouseId) return
|
|
simulationChannelsLoading.value = true
|
|
try {
|
|
const channels = await listWarehouseLogisticsChannels(simulationForm.warehouseId)
|
|
if (sequence === channelSequence) simulationChannels.value = channels
|
|
}
|
|
catch (error) { if (sequence === channelSequence) simulationError.value = getApiErrorDetails(error, '物流渠道加载失败') }
|
|
finally { if (sequence === channelSequence) simulationChannelsLoading.value = false }
|
|
}
|
|
async function saveSimulation() {
|
|
if (simulationSaving.value || warehouseLoading.value || simulationChannelsLoading.value) return
|
|
if (!(await simulationFormRef.value?.validate().catch(() => false))) return
|
|
simulationSaving.value = true
|
|
simulationError.value = null
|
|
try {
|
|
await createContainerSimulation({ ...simulationForm })
|
|
simulationOpen.value = false
|
|
ElMessage.success(props.successMessage)
|
|
emit('created')
|
|
} catch (error) { simulationError.value = getApiErrorDetails(error, '模拟柜创建失败') }
|
|
finally { simulationSaving.value = false }
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-button type="primary" @click="openSimulation"><Plus :size="16" />新增模拟柜</el-button>
|
|
<AppDialog
|
|
v-model="simulationOpen"
|
|
class="simulation-create-dialog"
|
|
:class="{ 'admin-density-dialog': dense, 'admin-density-dialog--comfortable': dense, 'admin-workbench': dense, 'is-dialog': dense }"
|
|
title="新增模拟柜"
|
|
append-to-body
|
|
width="min(800px, calc(100vw - 32px))"
|
|
confirm-text="创建模拟柜"
|
|
cancel-text="取消"
|
|
:confirm-loading="simulationSaving"
|
|
:confirm-disabled="warehouseLoading || simulationChannelsLoading"
|
|
@confirm="saveSimulation"
|
|
>
|
|
<template #header>
|
|
<div class="simulation-heading"><span class="simulation-heading__icon"><Container :size="20" aria-hidden="true" /></span><span>新增模拟柜</span></div>
|
|
</template>
|
|
<el-form ref="simulationFormRef" :model="simulationForm" :rules="simulationRules" label-position="top" class="simulation-form">
|
|
<div class="simulation-fields">
|
|
<el-form-item label="装柜仓库" prop="warehouseId">
|
|
<el-select :model-value="simulationForm.warehouseId" filterable placeholder="请选择国内装柜仓库" :loading="warehouseLoading" @update:model-value="selectSimulationWarehouse">
|
|
<el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="物流渠道" prop="channelGroupCode">
|
|
<el-select v-model="simulationForm.channelGroupCode" filterable :disabled="!simulationForm.warehouseId || simulationChannelsLoading" :loading="simulationChannelsLoading" :placeholder="simulationForm.warehouseId ? '请选择物流渠道' : '请先选择装柜仓库'" no-data-text="当前仓库暂无有效渠道">
|
|
<el-option v-for="group in simulationChannelGroups" :key="group.code" :label="group.name" :value="group.code" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="柜型" prop="containerType" class="simulation-type">
|
|
<el-select :model-value="simulationForm.containerType" @update:model-value="selectSimulationContainerType(String($event || ''))">
|
|
<el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</div>
|
|
</el-form>
|
|
<LoadErrorState v-if="simulationError" compact title="模拟柜创建失败" :message="simulationError.message" :show-retry="false"><template #details><ApiErrorMeta :details="simulationError" /></template></LoadErrorState>
|
|
</AppDialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.simulation-heading { display: flex; align-items: center; gap: 10px; font-size: var(--admin-font-title, 16px); }
|
|
.simulation-heading__icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px; color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
|
.simulation-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; row-gap: 8px; }
|
|
.simulation-fields > * { min-width: 0; }
|
|
.simulation-form :deep(.el-select) { width: 100%; }
|
|
.simulation-type { grid-column: 1 / -1; }
|
|
.simulation-type :deep(.el-select) { max-width: calc(50% - 12px); }
|
|
@media (max-width: 600px) {
|
|
.simulation-fields { grid-template-columns: minmax(0, 1fr); gap: 4px; }
|
|
.simulation-type :deep(.el-select) { max-width: none; }
|
|
}
|
|
</style>
|