@@ -17,15 +17,15 @@ import { overseasWarehousePermissions as permission } from '../permissions'
const access = useAccessPreviewStore ( ) ; const warehouses = ref < ReceivingOverseasWarehouseDto [ ] > ( [ ] ) ; const rows = ref < OverseasLocationDto [ ] > ( [ ] )
const loading = ref ( false ) ; const contextLoading = ref ( false ) ; const loadError = ref < { message : string } | null > ( null ) ; const page = ref ( 1 ) ; const pageSize = ref ( 20 ) ; const total = ref ( 0 )
const filters = ref < QueryModel > ( { warehouseId : '' , zoneCode : '' , locationCode : '' , locationType : '' , status : '' } ) ; const editorOpen = ref ( false ) ; const saving = ref ( false ) ; const editing = ref < OverseasLocationDto | null > ( null )
const form = reactive ( { warehouseId : '' , zoneCode : '' , zoneName : '' , locationCode : '' , locationName : '' , locationType : 'NORMAL' as const , maxWeightKg : null as number | null , status : 'ACTIVE' as 'ACTIVE' | 'INACTIVE' , remark : '' } )
const filters = ref < QueryModel > ( { warehouseId : '' , locationCode : '' , locationType : '' , status : '' } ) ; const editorOpen = ref ( false ) ; const saving = ref ( false ) ; const editing = ref < OverseasLocationDto | null > ( null )
const form = reactive ( { warehouseId : '' , locationCode : '' , locationName : '' , locationType : 'NORMAL' as const , maxWeightKg : null as number | null , status : 'ACTIVE' as 'ACTIVE' | 'INACTIVE' , remark : '' } )
const queryFields = computed < QueryField [ ] > ( ( ) => [
{ key : 'warehouseId' , type : 'select' , label : '海外仓' , width : 260 , options : warehouses . value . map ( item => ( { value : String ( item . id ) , label : ` ${ item . warehouseName } ( ${ item . warehouseCode } ) ` } ) ) , props : { filterable : true , clearable : true , disabled : contextLoading . value } } ,
{ key : 'zoneCode' , type : 'input' , label : '库区编码' , width : 180 } , { key : 'locationCode' , type : 'input' , label : '库位编码' , width : 180 } ,
{ key : 'locationCode' , type : 'input' , label : '库位编码' , width : 180 } ,
{ key : 'locationType' , type : 'select' , label : '类型' , width : 150 , options : [ { label : '普通库位' , value : 'NORMAL' } , { label : 'A0共享溢出位' , value : 'SHARED_OVERFLOW' } , { label : '虚拟收货位' , value : 'VIRTUAL' } ] } ,
{ key : 'status' , type : 'select' , label : '状态' , width : 140 , options : [ { label : '可用' , value : 'ACTIVE' } , { label : '空闲' , value : 'IDLE' } , { label : '已占用' , value : 'OCCUPIED' } , { label : '已停用' , value : 'DISABLED' } ] } ,
] )
const columns : DataTableColumn [ ] = [ { key : 'actions' , label : '操作' , width : 220 , fixed : 'left' , slotName : 'actions' } , { key : 'locationCode' , prop : 'locationCode' , label : '库位编码' , minWidth : 180 } , { key : 'warehouse' , label : '海外仓' , minWidth : 190 , slotName : 'warehouse' } , { key : 'zone' , label : '库区' , minWidth : 150 , slotName : 'zone' } , { key : 'locationType' , label : '类型' , width : 130 , slotName : 'locationType' } , { key : 'status' , label : '状态' , width : 110 , slotName : 'status' } , { key : 'occupiedPalletCount' , prop : 'occupiedPalletCount' , label : '托盘数' , width : 100 , align : 'right' } , { key : 'remark' , prop : 'remark' , label : '备注' , minWidth : 180 } ]
const columns : DataTableColumn [ ] = [ { key : 'actions' , label : '操作' , width : 220 , fixed : 'left' , slotName : 'actions' } , { key : 'locationCode' , prop : 'locationCode' , label : '库位编码' , minWidth : 180 } , { key : 'warehouse' , label : '海外仓' , minWidth : 190 , slotName : 'warehouse' } , { key : 'locationType' , label : '类型' , width : 130 , slotName : 'locationType' } , { key : 'status' , label : '状态' , width : 110 , slotName : 'status' } , { key : 'occupiedPalletCount' , prop : 'occupiedPalletCount' , label : '托盘数' , width : 100 , align : 'right' } , { key : 'remark' , prop : 'remark' , label : '备注' , minWidth : 180 } ]
const typeLabels : Record < OverseasLocationType , string > = { VIRTUAL : '虚拟收货位' , NORMAL : '普通库位' , SHARED _OVERFLOW : 'A0共享溢出位' } ; const statusLabels : Record < OverseasLocationStatus , string > = { ACTIVE : '可用' , IDLE : '空闲' , OCCUPIED : '已占用' , INACTIVE : '已停用' , DISABLED : '已停用' }
const selectedWarehouse = computed ( ( ) => warehouses . value . find ( item => String ( item . id ) === String ( form . warehouseId ) ) )
const warehouseNameById = computed ( ( ) => new Map ( warehouses . value . map ( item => [ String ( item . id ) , item . warehouseName ] ) ) )
@@ -35,17 +35,17 @@ function canStatus(row: OverseasLocationDto) { return access.can(permission.loca
function canDelete ( row : OverseasLocationDto ) { return access . can ( permission . locationDelete ) && ! isA0 ( row ) && row . status === 'DISABLED' && Number ( row . occupiedPalletCount || 0 ) === 0 }
function actionError ( error : unknown , fallback : string ) { ElMessage . error ( getApiErrorDetails ( error , fallback ) . message ) }
async function loadWarehouses ( ) { contextLoading . value = true ; try { warehouses . value = ( await listReceivingOverseasWarehouses ( ) ) . records ; await load ( ) } catch ( error ) { actionError ( error , '海外仓列表加载失败' ) } finally { contextLoading . value = false } }
async function load ( model : QueryModel = filters . value ) { Object . assign ( filters . value , model ) ; if ( ! warehouses . value . length ) { rows . value = [ ] ; total . value = 0 ; return } ; loading . value = true ; loadError . value = null ; try { const selectedWarehouseId = String ( filters . value . warehouseId || '' ) ; const result = await ( selectedWarehouseId ? listOverseasLocations ( selectedWarehouseId , { page : 1 , size : 200 , zoneCode : String ( filters . value . zoneCode || '' ) . trim ( ) || undefined , keyword : String ( filters . value . locationCode || '' ) . trim ( ) || undefined , locationType : String ( filters . value . locationType || '' ) as OverseasLocationType | '' , status : ( String ( filters . value . status || '' ) === 'DISABLED' ? 'INACTIVE' : String ( filters . value . status || '' ) ) as OverseasLocationStatus | '' } ) : listAllOverseasLocations ( warehouses . value . map ( item => String ( item . id ) ) , { zoneCode : String ( filters . value . zoneCode || '' ) . trim ( ) || undefined , keyword : String ( filters . value . locationCode || '' ) . trim ( ) || undefined , locationType : String ( filters . value . locationType || '' ) as OverseasLocationType | '' , status : ( String ( filters . value . status || '' ) === 'DISABLED' ? 'INACTIVE' : String ( filters . value . status || '' ) ) as OverseasLocationStatus | '' } ) ) ; const records = result . records . map ( row => ( { ... row , warehouseName : row . warehouseName || warehouseNameById . value . get ( String ( row . warehouseId ) ) || null } ) ) ; total . value = records . length ; const start = ( page . value - 1 ) * pageSize . value ; rows . value = records . slice ( start , start + pageSize . value ) } catch ( error ) { rows . value = [ ] ; total . value = 0 ; loadError . value = getApiErrorDetails ( error , '海外库位加载失败' ) } finally { loading . value = false } }
async function load ( model : QueryModel = filters . value ) { Object . assign ( filters . value , model ) ; if ( ! warehouses . value . length ) { rows . value = [ ] ; total . value = 0 ; return } ; loading . value = true ; loadError . value = null ; try { const selectedWarehouseId = String ( filters . value . warehouseId || '' ) ; const result = await ( selectedWarehouseId ? listOverseasLocations ( selectedWarehouseId , { page : 1 , size : 200 , keyword : String ( filters . value . locationCode || '' ) . trim ( ) || undefined , locationType : String ( filters . value . locationType || '' ) as OverseasLocationType | '' , status : ( String ( filters . value . status || '' ) === 'DISABLED' ? 'INACTIVE' : String ( filters . value . status || '' ) ) as OverseasLocationStatus | '' } ) : listAllOverseasLocations ( warehouses . value . map ( item => String ( item . id ) ) , { keyword : String ( filters . value . locationCode || '' ) . trim ( ) || undefined , locationType : String ( filters . value . locationType || '' ) as OverseasLocationType | '' , status : ( String ( filters . value . status || '' ) === 'DISABLED' ? 'INACTIVE' : String ( filters . value . status || '' ) ) as OverseasLocationStatus | '' } ) ) ; const records = result . records . map ( row => ( { ... row , warehouseName : row . warehouseName || warehouseNameById . value . get ( String ( row . warehouseId ) ) || null } ) ) ; total . value = records . length ; const start = ( page . value - 1 ) * pageSize . value ; rows . value = records . slice ( start , start + pageSize . value ) } catch ( error ) { rows . value = [ ] ; total . value = 0 ; loadError . value = getApiErrorDetails ( error , '海外库位加载失败' ) } finally { loading . value = false } }
function search ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
function resetForm ( ) { Object . assign ( form , { warehouseId : String ( filters . value . warehouseId || '' ) , zoneCode : '' , zoneName : '' , locationCode : '' , locationName : '' , locationType : 'NORMAL' , maxWeightKg : null , status : 'ACTIVE' , remark : '' } ) ; editing . value = null }
function resetForm ( ) { Object . assign ( form , { warehouseId : String ( filters . value . warehouseId || '' ) , locationCode : '' , locationName : '' , locationType : 'NORMAL' , maxWeightKg : null , status : 'ACTIVE' , remark : '' } ) ; editing . value = null }
function openCreate ( ) { resetForm ( ) ; editorOpen . value = true }
function openEdit ( row : OverseasLocationDto ) { if ( ! canEdit ( row ) ) return ; Object . assign ( form , { warehouseId : row . warehouseId , zoneCode : row . zoneCode , zoneName : row . zoneName || '' , locationCode : row . locationCode , locationName : row . locationName || '' , locationType : 'NORMAL' , maxWeightKg : row . maxLoadKg == null ? null : Number ( row . maxLoadKg ) , status : row . status === 'DISABLED' ? 'INACTIVE' : 'ACTIVE' , remark : row . remark || '' } ) ; editing . value = row ; editorOpen . value = true }
async function save ( ) { if ( ! form . warehouseId || ! form . zoneCode . trim ( ) || ! form . locationCode . trim ( ) || form . maxWeightKg == null ) { ElMessage . warning ( '请填写海外仓、库区编码、库 位编码和最大承重' ) ; return } ; saving . value = true ; try { const payload = { zoneCode : form . zoneCode . trim ( ) , locationCode : form . locationCode . trim ( ) , locationType : 'NORMAL' as const , maxLoadKg : form . maxWeightKg , status : form . status , remark : form . remark . trim ( ) || null } ; if ( editing . value ) await updateOverseasLocation ( form . warehouseId , editing . value . id , { ... payload , version : editing . value . version } ) ; else await createOverseasLocation ( form . warehouseId , payload ) ; editorOpen . value = false ; await load ( ) } catch ( error ) { actionError ( error , '保存库位失败' ) } finally { saving . value = false } }
async function toggleStatus ( row : OverseasLocationDto ) { if ( ! canStatus ( row ) ) return ; const next = row . status === 'DISABLED' ? 'ACTIVE' : 'INACTIVE' ; try { await ElMessageBox . confirm ( ` 确认 ${ next === 'ACTIVE' ? '启用' : '停用' } 库位“ ${ row . locationCode } ”? ` , '请确认' ) ; await changeOverseasLocationStatus ( row . warehouseId , row . id , { zoneCode : row . zoneCode , locationCode : row . locationCode , locationType : 'NORMAL' , maxLoadKg : Number ( row . maxLoadKg || 0 ) , status : next , remark : row . remark || null , version : row . version } ) ; await load ( ) } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) actionError ( error , '更新库位状态失败' ) } }
function openEdit ( row : OverseasLocationDto ) { if ( ! canEdit ( row ) ) return ; Object . assign ( form , { warehouseId : row . warehouseId , locationCode : row . locationCode , locationName : row . locationName || '' , locationType : 'NORMAL' , maxWeightKg : row . maxLoadKg == null ? null : Number ( row . maxLoadKg ) , status : row . status === 'DISABLED' ? 'INACTIVE' : 'ACTIVE' , remark : row . remark || '' } ) ; editing . value = row ; editorOpen . value = true }
async function save ( ) { if ( ! form . warehouseId || ! form . locationCode . trim ( ) || form . maxWeightKg == null ) { ElMessage . warning ( '请填写海外仓、库位编码和最大承重' ) ; return } ; saving . value = true ; try { const payload = { locationCode : form . locationCode . trim ( ) , locationType : 'NORMAL' as const , maxLoadKg : form . maxWeightKg , status : form . status , remark : form . remark . trim ( ) || null } ; if ( editing . value ) await updateOverseasLocation ( form . warehouseId , editing . value . id , { ... payload , version : editing . value . version } ) ; else await createOverseasLocation ( form . warehouseId , payload ) ; editorOpen . value = false ; await load ( ) } catch ( error ) { actionError ( error , '保存库位失败' ) } finally { saving . value = false } }
async function toggleStatus ( row : OverseasLocationDto ) { if ( ! canStatus ( row ) ) return ; const next = row . status === 'DISABLED' ? 'ACTIVE' : 'INACTIVE' ; try { await ElMessageBox . confirm ( ` 确认 ${ next === 'ACTIVE' ? '启用' : '停用' } 库位“ ${ row . locationCode } ”? ` , '请确认' ) ; await changeOverseasLocationStatus ( row . warehouseId , row . id , { locationCode : row . locationCode , locationType : 'NORMAL' , maxLoadKg : Number ( row . maxLoadKg || 0 ) , status : next , remark : row . remark || null , version : row . version } ) ; await load ( ) } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) actionError ( error , '更新库位状态失败' ) } }
async function remove ( row : OverseasLocationDto ) { if ( ! canDelete ( row ) ) return ; try { await ElMessageBox . confirm ( ` 确认删除库位“ ${ row . locationCode } ”? ` , '请确认' , { type : 'warning' } ) ; await deleteOverseasLocation ( row . warehouseId , row . id , row . version ) ; await load ( ) } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) actionError ( error , '删除库位失败' ) } }
onMounted ( ( ) => { void loadWarehouses ( ) } )
< / script >
< template > < AdminPage class = "admin-density" > < ListPageLayout aria -label = " 海外仓库位管理 " > < template # toolbar > < QueryToolbar v-model = "filters" :fields="queryFields" ariaLabel="海外库位筛选" preference-key="overseas-locations-query" @search="search" @reset="page = 1; load()" /></template><template #state><LoadErrorState v-if="loadError" title="海外库位加载失败" :message="loadError.message" :loading="loading" @retry="load" /></template><section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" empty-text="暂无库位" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-actions><el-button v-if="access.can(permission.locationCreate)" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template><template #actions="{ row }"><div class="density-actions"><el-button v-if="canEdit(row as OverseasLocationDto)" size="small" class="density-action" @click="openEdit(row as OverseasLocationDto)">编辑</el-button><el-button v-if="canStatus(row as OverseasLocationDto)" size="small" class="density-action" :data-tone="(row as OverseasLocationDto).status === 'DISABLED' || (row as OverseasLocationDto).status === 'INACTIVE' ? 'success' : 'danger'" @click="toggleStatus(row as OverseasLocationDto)">{{ (row as OverseasLocationDto).status === 'DISABLED' || (row as OverseasLocationDto).status === 'INACTIVE' ? '启用' : '停用' }}</el-button><el-button v-if="canDelete(row as OverseasLocationDto)" size="small" class="density-action" data-tone="danger" @click="remove(row as OverseasLocationDto)">删除</el-button></div></template><template #warehouse="{ row }">{{ (row as OverseasLocationDto).warehouseName || warehouseNameById.get(String((row as OverseasLocationDto).warehouseId)) || '—' }}</template><template #zone="{ row }">{{ (row as OverseasLocationDto).zoneName || '—' }}( {{ (row as OverseasLocationDto).zoneCode }}) </template><template #locationType="{ row }"><el-tag :type="isA0(row as OverseasLocationDto) ? 'warning' : 'info'">{{ typeLabels[(row as OverseasLocationDto).locationType] || '普通库位' }}</el-tag></template><template #status="{ row }"><el-tag>{{ statusLabels[(row as OverseasLocationDto).status] || '—' }}</el-tag></template><template #pagination-left>共 {{ total }} 个库位</template></DataTable></section></ListPageLayout><AppDialog v-model="editorOpen" :title="editing ? '编辑海外库位' : '新增海外库位'" width="min(560px, calc(100vw - 28px))" :confirm-loading="saving" :show-confirm="false"><el-form label-position="top" :disabled="saving"><div class="editor-grid"><el-form-item label="海外仓" required><el-select v-model="form.warehouseId" :disabled="Boolean(editing)" placeholder="请选择海外仓"><el-option v-for="item in warehouses" :key="item.id" :value="item.id" :label="`${item.warehouseName}( ${item.warehouseCode}) `" /></el-select></el-form-item><el-form-item label="库位编码" required><el-input v-model="form.locationCode" maxlength="50" placeholder="请输入库位编码" /></el-form-item><el-form-item label="库区编码" required><el-input v-model="form.zoneCode" maxlength="50" placeholder="请输入库区编码" /></el-form-item><el-form-item label="库区名称"><el-input v-model="form.zoneName" maxlength="100" placeholder="请输入库区名称(选填)" /></el-form-item><el-form-item label="库位名称"><el-input v-model="form.locationName" maxlength="100" placeholder="请输入库位名称(选填)" /></el-form-item><el-form-item label="最大承重(kg) " required><el-input-number v-model="form.maxWeightKg" :min="0" :precision="3" controls-position="right" placeholder="请输入最大承重" /></el-form-item></div><el-form-item label="备注"><el-input v-model="form.remark" maxlength="500" placeholder="请输入备注(选填)" /></el-form-item></el-form><template #footer><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="save"><Save :size="16" / > 保存库位 < / el-button > < / template > < / AppDialog > < / AdminPage > < / template >
< template > < AdminPage class = "admin-density" > < ListPageLayout aria -label = " 海外仓库位管理 " > < template # toolbar > < QueryToolbar v-model = "filters" :fields="queryFields" ariaLabel="海外库位筛选" preference-key="overseas-locations-query" @search="search" @reset="page = 1; load()" /></template><template #state><LoadErrorState v-if="loadError" title="海外库位加载失败" :message="loadError.message" :loading="loading" @retry="load" /></template><section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" empty-text="暂无库位" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-actions><el-button v-if="access.can(permission.locationCreate)" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template><template #actions="{ row }"><div class="density-actions"><el-button v-if="canEdit(row as OverseasLocationDto)" size="small" class="density-action" @click="openEdit(row as OverseasLocationDto)">编辑</el-button><el-button v-if="canStatus(row as OverseasLocationDto)" size="small" class="density-action" :data-tone="(row as OverseasLocationDto).status === 'DISABLED' || (row as OverseasLocationDto).status === 'INACTIVE' ? 'success' : 'danger'" @click="toggleStatus(row as OverseasLocationDto)">{{ (row as OverseasLocationDto).status === 'DISABLED' || (row as OverseasLocationDto).status === 'INACTIVE' ? '启用' : '停用' }}</el-button><el-button v-if="canDelete(row as OverseasLocationDto)" size="small" class="density-action" data-tone="danger" @click="remove(row as OverseasLocationDto)">删除</el-button></div></template><template #warehouse="{ row }">{{ (row as OverseasLocationDto).warehouseName || warehouseNameById.get(String((row as OverseasLocationDto).warehouseId)) || '—' }}</template><template #locationType="{ row }"><el-tag :type="isA0(row as OverseasLocationDto) ? 'warning' : 'info'">{{ typeLabels[(row as OverseasLocationDto).locationType] || '普通库位' }}</el-tag></template><template #status="{ row }"><el-tag>{{ statusLabels[(row as OverseasLocationDto).status] || '—' }}</el-tag></template><template #pagination-left>共 {{ total }} 个库位</template></DataTable></section></ListPageLayout><AppDialog v-model="editorOpen" :title="editing ? '编辑海外库位' : '新增海外库位'" width="min(560px, calc(100vw - 28px))" :confirm-loading="saving" :show-confirm="false"><el-form label-position="top" :disabled="saving"><div class="editor-grid"><el-form-item label="海外仓" required><el-select v-model="form.warehouseId" :disabled="Boolean(editing)" placeholder="请选择海外仓"><el-option v-for="item in warehouses" :key="item.id" :value="item.id" :label="`${item.warehouseName}( ${item.warehouseCode}) `" /></el-select></el-form-item><el-form-item label="库位编码" required><el-input v-model="form.locationCode" maxlength="50" placeholder="请输入库位编码" /></el-form-item><el-form-item label="库位名称"><el-input v-model="form.locationName" maxlength="100" placeholder="请输入库位名称(选填)" /></el-form-item><el-form-item label="最大承重(kg) " required><el-input-number v-model="form.maxWeightKg" :min="0" :precision="3" controls-position="right" placeholder="请输入最大承重" /></el-form-item></div><el-form-item label="备注"><el-input v-model="form.remark" maxlength="500" placeholder="请输入备注(选填)" /></el-form-item></el-form><template #footer><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="save"><Save :size="16" / > 保存库位 < / el-button > < / template > < / AppDialog > < / AdminPage > < / template >
< style scoped > . editor - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 0 14 px } . editor - grid : deep ( . el - select ) , . editor - grid : deep ( . el - input - number ) { width : 100 % } @ media ( max - width : 720 px ) { . editor - grid { grid - template - columns : 1 fr } } < / style >