2026-07-30 17:59:43 +08:00
< script setup lang = "ts" >
2026-08-04 11:14:34 +08:00
import { computed , onMounted , reactive , ref , watch } from 'vue'
2026-08-06 13:56:04 +08:00
import { ElMessage , ElMessageBox , type UploadFile } from 'element-plus'
2026-08-10 16:24:54 +08:00
import { ChevronDown , Download , FileSpreadsheet , FileUp , MapPin , Pencil , Plus , Power , Save , Warehouse as WarehouseIcon } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
2026-08-03 18:49:18 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 16:24:54 +08:00
import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-03 18:49:18 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-07-31 13:43:16 +08:00
import {
changePlatformWarehouseStatus ,
2026-08-06 13:56:04 +08:00
createFbaWarehouse ,
2026-07-31 13:43:16 +08:00
createPlatformWarehouse ,
2026-08-06 13:56:04 +08:00
downloadWarehouseImportTemplate ,
2026-07-31 13:43:16 +08:00
getPlatformWarehouse ,
2026-08-06 13:56:04 +08:00
importWarehouses ,
2026-07-31 13:43:16 +08:00
listPlatformWarehouses ,
updatePlatformWarehouse ,
2026-08-06 13:56:04 +08:00
type WarehouseImportMode ,
type WarehouseImportResult ,
2026-07-31 13:43:16 +08:00
type PlatformWarehouseDetailResource ,
2026-08-06 13:56:04 +08:00
type PlatformWarehouseSummaryResource ,
2026-07-31 13:43:16 +08:00
type SavePlatformWarehouseRequest ,
2026-08-04 11:14:34 +08:00
type WarehouseType ,
2026-08-03 18:49:18 +08:00
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import type { Warehouse } from '@/modules/warehouses/types'
2026-07-30 17:59:43 +08:00
2026-08-04 11:14:34 +08:00
const warehouseTypeLabels : Record < WarehouseType , string > = {
DOMESTIC : '国内收货仓' ,
2026-08-06 13:56:04 +08:00
FBA : 'FBA 地址' ,
2026-08-04 11:14:34 +08:00
OVERSEAS : '海外仓' ,
}
2026-08-06 13:56:04 +08:00
const timezoneOptions = [
'UTC-12' , 'UTC-11' , 'UTC-10' , 'UTC-9' , 'UTC-8' , 'UTC-7' , 'UTC-6' , 'UTC-5' , 'UTC-4' , 'UTC-3' ,
'UTC-2' , 'UTC-1' , 'UTC+0' , 'UTC+1' , 'UTC+2' , 'UTC+3' , 'UTC+4' , 'UTC+5' , 'UTC+6' , 'UTC+7' ,
'UTC+8' , 'UTC+9' , 'UTC+10' , 'UTC+11' , 'UTC+12' , 'UTC+13' , 'UTC+14' ,
]
const countryOptions = [
{ label : '美国 US' , value : 'US' },
{ label : '加拿大 CA' , value : 'CA' },
{ label : '英国 GB' , value : 'GB' },
{ label : '德国 DE' , value : 'DE' },
{ label : '法国 FR' , value : 'FR' },
{ label : '意大利 IT' , value : 'IT' },
{ label : '西班牙 ES' , value : 'ES' },
{ label : '日本 JP' , value : 'JP' },
{ label : '澳大利亚 AU' , value : 'AU' },
]
const countryDisplayLabels : Record < string , string > = {
CN : '中国' ,
US : '美国' ,
CA : '加拿大' ,
GB : '英国' ,
DE : '德国' ,
FR : '法国' ,
IT : '意大利' ,
ES : '西班牙' ,
JP : '日本' ,
AU : '澳大利亚' ,
}
const usStateOptions = [
{ label : 'AL Alabama' , value : 'AL' , timezone : 'UTC-6' },
{ label : 'AK Alaska' , value : 'AK' , timezone : 'UTC-9' },
{ label : 'AZ Arizona' , value : 'AZ' , timezone : 'UTC-7' },
{ label : 'AR Arkansas' , value : 'AR' , timezone : 'UTC-6' },
{ label : 'CA California' , value : 'CA' , timezone : 'UTC-8' },
{ label : 'CO Colorado' , value : 'CO' , timezone : 'UTC-7' },
{ label : 'CT Connecticut' , value : 'CT' , timezone : 'UTC-5' },
{ label : 'DE Delaware' , value : 'DE' , timezone : 'UTC-5' },
{ label : 'FL Florida' , value : 'FL' , timezone : null },
{ label : 'GA Georgia' , value : 'GA' , timezone : 'UTC-5' },
{ label : 'HI Hawaii' , value : 'HI' , timezone : 'UTC-10' },
{ label : 'IA Iowa' , value : 'IA' , timezone : 'UTC-6' },
{ label : 'ID Idaho' , value : 'ID' , timezone : null },
{ label : 'IL Illinois' , value : 'IL' , timezone : 'UTC-6' },
{ label : 'IN Indiana' , value : 'IN' , timezone : null },
{ label : 'KS Kansas' , value : 'KS' , timezone : null },
{ label : 'KY Kentucky' , value : 'KY' , timezone : null },
{ label : 'LA Louisiana' , value : 'LA' , timezone : 'UTC-6' },
{ label : 'MA Massachusetts' , value : 'MA' , timezone : 'UTC-5' },
{ label : 'MD Maryland' , value : 'MD' , timezone : 'UTC-5' },
{ label : 'ME Maine' , value : 'ME' , timezone : 'UTC-5' },
{ label : 'MI Michigan' , value : 'MI' , timezone : null },
{ label : 'MN Minnesota' , value : 'MN' , timezone : 'UTC-6' },
{ label : 'MO Missouri' , value : 'MO' , timezone : 'UTC-6' },
{ label : 'MS Mississippi' , value : 'MS' , timezone : 'UTC-6' },
{ label : 'MT Montana' , value : 'MT' , timezone : 'UTC-7' },
{ label : 'NC North Carolina' , value : 'NC' , timezone : 'UTC-5' },
{ label : 'ND North Dakota' , value : 'ND' , timezone : null },
{ label : 'NE Nebraska' , value : 'NE' , timezone : null },
{ label : 'NH New Hampshire' , value : 'NH' , timezone : 'UTC-5' },
{ label : 'NJ New Jersey' , value : 'NJ' , timezone : 'UTC-5' },
{ label : 'NM New Mexico' , value : 'NM' , timezone : 'UTC-7' },
{ label : 'NV Nevada' , value : 'NV' , timezone : 'UTC-8' },
{ label : 'NY New York' , value : 'NY' , timezone : 'UTC-5' },
{ label : 'OH Ohio' , value : 'OH' , timezone : 'UTC-5' },
{ label : 'OK Oklahoma' , value : 'OK' , timezone : 'UTC-6' },
{ label : 'OR Oregon' , value : 'OR' , timezone : null },
{ label : 'PA Pennsylvania' , value : 'PA' , timezone : 'UTC-5' },
{ label : 'RI Rhode Island' , value : 'RI' , timezone : 'UTC-5' },
{ label : 'SC South Carolina' , value : 'SC' , timezone : 'UTC-5' },
{ label : 'SD South Dakota' , value : 'SD' , timezone : null },
{ label : 'TN Tennessee' , value : 'TN' , timezone : null },
{ label : 'TX Texas' , value : 'TX' , timezone : null },
{ label : 'VA Virginia' , value : 'VA' , timezone : 'UTC-5' },
{ label : 'VT Vermont' , value : 'VT' , timezone : 'UTC-5' },
{ label : 'WA Washington' , value : 'WA' , timezone : 'UTC-8' },
{ label : 'WI Wisconsin' , value : 'WI' , timezone : 'UTC-6' },
{ label : 'WV West Virginia' , value : 'WV' , timezone : 'UTC-5' },
{ label : 'WY Wyoming' , value : 'WY' , timezone : 'UTC-7' },
]
2026-08-04 11:14:34 +08:00
2026-07-30 17:59:43 +08:00
const access = useAccessPreviewStore ()
const editorOpen = ref ( false )
2026-08-06 13:56:04 +08:00
const editorMode = ref < 'CREATE' | 'EDIT' > ( 'CREATE' )
const createType = ref < WarehouseType >( 'FBA' )
2026-07-31 13:43:16 +08:00
const saving = ref ( false )
const loading = ref ( false )
2026-08-04 09:05:19 +08:00
const editLoading = ref ( false )
2026-07-30 17:59:43 +08:00
const page = ref ( 1 )
2026-08-06 13:56:04 +08:00
const pageSize = ref ( 20 )
const total = ref ( 0 )
2026-08-04 11:14:34 +08:00
const loaded = ref ( false )
2026-07-31 13:43:16 +08:00
const loadError = ref < ApiErrorDetails | null >( null )
const statusLoadingIds = ref ( new Set < string >())
2026-08-06 13:56:04 +08:00
const importOpen = ref ( false )
const importFile = ref < File | null >( null )
const importMode = ref < WarehouseImportMode >( 'CREATE_ONLY' )
const importResult = ref < WarehouseImportResult | null >( null )
const importing = ref ( false )
const downloadingTemplate = ref ( false )
2026-08-04 11:14:34 +08:00
type WarehouseFilters = QueryModel & { keyword : string ; warehouseType : WarehouseType | '' ; status : Warehouse [ 'status' ] | '' }
const filters = ref < WarehouseFilters >({ keyword : '' , warehouseType : '' , status : '' })
2026-08-01 12:17:33 +08:00
const appliedFilters = ref < WarehouseFilters >({ ... filters . value })
const queryFields : QueryField [] = [
2026-08-06 19:03:13 +08:00
{ key : 'keyword' , type : 'input' , label : '仓库关键词' , placeholder : '仓库名称、编码、城市或地址' },
2026-08-04 11:14:34 +08:00
{
2026-08-06 19:03:13 +08:00
key : 'warehouseType' , type : 'select' , label : '仓库类型' , placeholder : '全部仓库类型' , options : [
2026-08-04 11:14:34 +08:00
{ label : warehouseTypeLabels . DOMESTIC , value : 'DOMESTIC' },
{ label : warehouseTypeLabels . FBA , value : 'FBA' },
{ label : warehouseTypeLabels . OVERSEAS , value : 'OVERSEAS' },
],
},
2026-08-01 12:17:33 +08:00
{
2026-08-06 19:03:13 +08:00
key : 'status' , type : 'select' , label : '状态' , placeholder : '全部状态' , options : [
2026-08-01 12:17:33 +08:00
{ label : '启用' , value : 'ACTIVE' },
{ label : '停用' , value : 'INACTIVE' },
],
},
]
2026-07-31 13:43:16 +08:00
let loadSequence = 0
interface WarehouseRow extends Warehouse {
2026-08-04 11:14:34 +08:00
warehouseType : WarehouseType
countryCode : string
timezone : string
2026-08-06 13:56:04 +08:00
postalCode : string
2026-07-31 13:43:16 +08:00
detail : PlatformWarehouseDetailResource | null
}
const rows = ref < WarehouseRow [] >([])
2026-08-01 12:17:33 +08:00
const warehouseColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' },
2026-08-06 13:56:04 +08:00
{ key : 'warehouse' , label : '仓库' , minWidth : 190 , slotName : 'warehouse' },
{ key : 'warehouseType' , label : '仓库类型' , width : 120 , slotName : 'warehouseType' },
{ key : 'countryCode' , label : '国家/地区' , width : 140 , slotName : 'countryCode' },
2026-08-12 12:19:02 +08:00
{ key : 'timezone' , prop : 'timezone' , label : '时区' , width : 110 , defaultVisible : false },
2026-08-06 13:56:04 +08:00
{ key : 'city' , label : '所在城市' , minWidth : 140 , slotName : 'city' },
2026-08-12 12:19:02 +08:00
{ key : 'address' , label : '地址' , minWidth : 280 , slotName : 'address' , defaultVisible : false },
{ key : 'contact' , label : '联系人' , minWidth : 150 , slotName : 'contact' , defaultVisible : false },
2026-08-01 12:17:33 +08:00
{ key : 'status' , label : '状态' , width : 100 , slotName : 'status' },
]
2026-07-30 17:59:43 +08:00
2026-08-04 11:14:34 +08:00
type WarehouseForm = Omit < Warehouse , ' updatedAt ' > & {
warehouseType : WarehouseType
countryCode : string
timezone : string
2026-08-06 13:56:04 +08:00
postalCode : string
2026-08-04 11:14:34 +08:00
}
2026-08-06 13:56:04 +08:00
function generatedCode ( type : WarehouseType ) {
const prefix = type === 'DOMESTIC' ? 'CN' : type === 'FBA' ? 'FBA' : 'US'
return ` ${ prefix } -WH- ${ Date . now (). toString ( 36 ). slice ( - 7 ). toUpperCase () } `
}
const emptyForm = ( type : WarehouseType = 'FBA' ) : WarehouseForm => ({
id : '' ,
code : type === 'FBA' ? '' : generatedCode ( type ),
name : '' ,
province : '' ,
city : '' ,
address : '' ,
postalCode : '' ,
contactName : '' ,
contactPhone : '' ,
warehouseType : type ,
countryCode : type === 'DOMESTIC' ? 'CN' : 'US' ,
timezone : type === 'DOMESTIC' ? 'UTC+8' : '' ,
businessHours : type === 'FBA' ? '' : '周一至周六 08:30-18:00' ,
status : 'ACTIVE' ,
remark : '' ,
2026-07-30 17:59:43 +08:00
})
2026-08-06 13:56:04 +08:00
2026-07-30 17:59:43 +08:00
const form = reactive ( emptyForm ())
2026-08-06 13:56:04 +08:00
type WarehouseRequiredField = 'code' | 'name' | 'province' | 'countryCode' | 'timezone' | 'city' | 'postalCode' | 'address'
2026-08-01 12:17:33 +08:00
const requiredFieldMessages : Record < WarehouseRequiredField , string > = {
code : '请输入仓库编码' ,
name : '请输入仓库名称' ,
2026-08-06 13:56:04 +08:00
province : '请选择或输入州/省' ,
countryCode : '请选择国家或地区' ,
timezone : '请输入时区' ,
2026-08-01 12:17:33 +08:00
city : '请输入城市' ,
2026-08-06 13:56:04 +08:00
postalCode : '请输入邮编' ,
2026-08-01 12:17:33 +08:00
address : '请输入详细地址' ,
}
2026-08-04 11:14:34 +08:00
const formErrors = reactive < Record < WarehouseRequiredField , string > > ({
2026-08-06 13:56:04 +08:00
code : '' , name : '' , province : '' , countryCode : '' , timezone : '' , city : '' , postalCode : '' , address : '' ,
2026-08-04 11:14:34 +08:00
})
2026-08-06 13:56:04 +08:00
const editorLabel = computed (() => editorMode . value === 'EDIT'
? `编辑 ${ warehouseTypeLabels [ form . warehouseType ] } `
: `新增 ${ warehouseTypeLabels [ createType . value ] } ` )
const editorPermission = computed (() => editorMode . value === 'EDIT' ? 'platform:warehouse:edit' : 'platform:warehouse:create' )
const isFbaCreate = computed (() => editorMode . value === 'CREATE' && createType . value === 'FBA' )
const isDomesticCreate = computed (() => editorMode . value === 'CREATE' && createType . value === 'DOMESTIC' )
const activeWarehouseType = computed (() => editorMode . value === 'EDIT' ? form . warehouseType : createType . value )
const isDomesticForm = computed (() => activeWarehouseType . value === 'DOMESTIC' )
const usesUsStateSelect = computed (() => ! isDomesticForm . value && form . countryCode === 'US' )
const addressRegionLabel = computed (() => usesUsStateSelect . value || isFbaCreate . value ? '州' : '省 / 州' )
const regionPlaceholder = computed (() => isDomesticForm . value ? '例如 浙江省' : usesUsStateSelect . value ? '请选择州' : '例如 Ontario' )
const cityPlaceholder = computed (() => isDomesticForm . value ? '例如 义乌市' : '例如 Long Beach' )
const addressPlaceholder = computed (() => isFbaCreate . value ? '请输入街道和门牌信息' : '请输入街道、园区、楼栋及门牌信息' )
const canCommitImport = computed (() => Boolean (
importFile . value
&& importResult . value
&& ! importResult . value . committed
&& importResult . value . errorCount === 0
&& ( importMode . value === 'CREATE_ONLY' || access . can ( 'platform:warehouse:edit' )),
))
2026-07-30 17:59:43 +08:00
const { captureBaseline } = useUnsavedWorkRegistration ({
key : 'warehouse-editor' ,
label : editorLabel ,
isOpen : editorOpen ,
model : form ,
requiredPermission : editorPermission ,
})
2026-08-04 11:14:34 +08:00
const warehouseReady = computed (() => loaded . value && ! loading . value )
2026-07-31 13:43:16 +08:00
const loadErrorDescription = computed (() => {
if ( ! loadError . value ) return ''
const references = [
loadError . value . code ? `错误码 ${ loadError . value . code } ` : '' ,
loadError . value . requestId ? `requestId ${ loadError . value . requestId } ` : '' ,
loadError . value . traceId ? `traceId ${ loadError . value . traceId } ` : '' ,
]. filter ( Boolean )
2026-08-10 16:24:54 +08:00
return [ loadError . value . message , references . join ( ' · ' )]. filter ( Boolean ). join ( ' ' )
2026-07-31 13:43:16 +08:00
})
2026-07-30 17:59:43 +08:00
watch (
2026-07-31 15:34:35 +08:00
() => access . can ( editorPermission . value ),
2026-07-30 17:59:43 +08:00
( canWrite ) => {
if ( canWrite || ! editorOpen . value ) return
editorOpen . value = false
Object . assign ( form , emptyForm ())
},
{ flush : 'sync' },
)
2026-08-06 13:56:04 +08:00
watch ([ page , pageSize ], ([ nextPage , nextSize ], [ previousPage , previousSize ]) => {
if ( nextPage === previousPage && nextSize === previousSize ) return
void loadWarehouses ()
})
watch ( importMode , () => { importResult . value = null })
watch ( activeWarehouseType , ( type ) => {
if ( type === 'DOMESTIC' ) {
form . countryCode = 'CN'
form . timezone = 'UTC+8'
} else if ( form . countryCode === 'CN' ) {
form . countryCode = 'US'
form . province = ''
form . timezone = ''
}
clearFieldError ( 'countryCode' )
clearFieldError ( 'timezone' )
})
2026-07-30 17:59:43 +08:00
2026-08-06 13:56:04 +08:00
function toRow ( warehouse : PlatformWarehouseSummaryResource ) : WarehouseRow {
const address = warehouse . primaryAddress
const addressText = warehouse . addressSummary
|| [ address ? . district , address ? . addressLine1 , address ? . addressLine2 ]. filter ( Boolean ). join ( ' ' )
2026-07-31 13:43:16 +08:00
return {
id : String ( warehouse . id ),
code : warehouse . warehouseCode ,
name : warehouse . warehouseName ,
2026-08-04 11:14:34 +08:00
warehouseType : warehouse . warehouseType ,
countryCode : warehouse . countryCode ,
timezone : warehouse . timezone ,
2026-07-31 13:43:16 +08:00
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
2026-08-06 13:56:04 +08:00
address : addressText ,
postalCode : address ? . postalCode || '' ,
2026-07-31 13:43:16 +08:00
contactName : warehouse . contactName || '' ,
contactPhone : warehouse . contactPhone || '' ,
2026-08-06 13:56:04 +08:00
businessHours : '' ,
2026-07-31 13:43:16 +08:00
status : warehouse . status ,
remark : warehouse . remark || '' ,
updatedAt : warehouse . updatedAt ? . replace ( 'T' , ' ' ). slice ( 0 , 16 ) || '' ,
2026-08-06 13:56:04 +08:00
detail : null ,
2026-07-31 13:43:16 +08:00
}
}
2026-08-01 12:17:33 +08:00
function queryWarehouses ( model : QueryModel ) {
appliedFilters . value = {
keyword : String ( model . keyword || '' ),
2026-08-04 11:14:34 +08:00
warehouseType : ( model . warehouseType || '' ) as WarehouseType | '' ,
2026-08-01 12:17:33 +08:00
status : ( model . status || '' ) as Warehouse [ 'status' ] | '' ,
}
2026-08-06 13:56:04 +08:00
if ( page . value === 1 ) void loadWarehouses ()
else page . value = 1
2026-07-30 17:59:43 +08:00
}
2026-08-01 12:17:33 +08:00
function clearWarehouseFormErrors () {
for ( const field of Object . keys ( formErrors ) as WarehouseRequiredField []) formErrors [ field ] = ''
}
function clearFieldError ( field : WarehouseRequiredField ) {
if ( formErrors [ field ]) formErrors [ field ] = ''
}
2026-08-06 13:56:04 +08:00
function formatCountryDisplay ( countryCode : string ) {
const code = countryCode . trim (). toUpperCase ()
if ( ! code ) return '-'
const label = countryDisplayLabels [ code ]
return label ? ` ${ label } ${ code } ` : code
}
function validateFields ( fields : WarehouseRequiredField []) {
return fields . map (( field ) => {
formErrors [ field ] = form [ field ]. trim () ? '' : requiredFieldMessages [ field ]
return ! formErrors [ field ]
}). every ( Boolean )
2026-08-01 12:17:33 +08:00
}
function validateWarehouseForm () {
2026-08-06 13:56:04 +08:00
if ( isFbaCreate . value ) return validateFields ([ 'code' , 'province' , 'city' , 'postalCode' , 'timezone' , 'address' ])
const fields : WarehouseRequiredField [] = [ 'code' , 'name' , 'countryCode' , 'timezone' , 'city' , 'address' ]
if ( usesUsStateSelect . value ) fields . splice ( 4 , 0 , 'province' )
return validateFields ( fields )
}
function onCountryChange ( value : string ) {
form . countryCode = value . toUpperCase ()
clearFieldError ( 'countryCode' )
if ( ! usesUsStateSelect . value ) {
form . province = ''
form . timezone = ''
clearFieldError ( 'province' )
return
}
const matchedState = usStateOptions . find (( state ) => state . value === form . province . toUpperCase ())
if ( ! matchedState ) {
form . province = ''
form . timezone = ''
return
}
onRegionChange ( matchedState . value )
}
function onRegionChange ( value : string ) {
form . province = value . toUpperCase ()
clearFieldError ( 'province' )
if ( ! usesUsStateSelect . value ) return
const matchedState = usStateOptions . find (( state ) => state . value === form . province )
if ( matchedState ? . timezone ) {
form . timezone = matchedState . timezone
clearFieldError ( 'timezone' )
} else {
form . timezone = ''
}
2026-08-01 12:17:33 +08:00
}
2026-08-04 11:14:34 +08:00
async function loadWarehouses () {
2026-07-31 13:43:16 +08:00
const sequence = ++ loadSequence
loading . value = true
loadError . value = null
try {
2026-08-06 13:56:04 +08:00
const result = await listPlatformWarehouses ({
page : page . value ,
size : pageSize . value ,
keyword : appliedFilters . value . keyword . trim () || undefined ,
warehouseType : appliedFilters . value . warehouseType || undefined ,
status : appliedFilters . value . status || undefined ,
2026-07-31 13:43:16 +08:00
})
2026-08-06 13:56:04 +08:00
if ( sequence !== loadSequence ) return
rows . value = result . records . map ( toRow )
total . value = result . total
2026-08-04 11:14:34 +08:00
loaded . value = true
2026-07-31 13:43:16 +08:00
} catch ( error ) {
if ( sequence !== loadSequence ) return
loadError . value = getApiErrorDetails ( error , '仓库数据加载失败,请稍后重试' )
} finally {
if ( sequence === loadSequence ) loading . value = false
}
}
2026-08-04 11:14:34 +08:00
function requireWarehouseReady () {
2026-08-01 12:17:33 +08:00
if ( ! warehouseReady . value ) {
ElMessage . warning ( '仓库数据尚未加载完成,请稍后重试' )
2026-08-04 11:14:34 +08:00
return false
2026-07-31 13:43:16 +08:00
}
2026-08-04 11:14:34 +08:00
return true
2026-07-31 13:43:16 +08:00
}
2026-08-06 13:56:04 +08:00
function openCreate ( type : WarehouseType ) {
2026-08-04 11:14:34 +08:00
if ( ! requireWarehouseReady ()) return
2026-08-06 13:56:04 +08:00
editorMode . value = 'CREATE'
createType . value = type
Object . assign ( form , emptyForm ( type ))
2026-08-01 12:17:33 +08:00
clearWarehouseFormErrors ()
2026-07-30 17:59:43 +08:00
captureBaseline ()
editorOpen . value = true
}
2026-07-31 13:43:16 +08:00
async function ensureDetail ( warehouse : WarehouseRow ) {
if ( warehouse . detail ) return warehouse . detail
2026-08-04 11:14:34 +08:00
if ( ! requireWarehouseReady ()) return null
2026-07-31 13:43:16 +08:00
try {
2026-08-06 13:56:04 +08:00
const detail = await getPlatformWarehouse ( warehouse . id )
warehouse . detail = detail
2026-07-31 13:43:16 +08:00
return detail
} catch ( error ) {
const details = getApiErrorDetails ( error , '仓库详情加载失败,请重试' )
ElMessage . error ( details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message )
return null
}
}
async function openEdit ( warehouse : WarehouseRow ) {
2026-08-04 09:05:19 +08:00
editLoading . value = true
try {
const detail = await ensureDetail ( warehouse )
2026-08-04 11:14:34 +08:00
if ( ! detail || ! loaded . value ) return
2026-08-04 09:05:19 +08:00
const address = detail . addresses [ 0 ]
2026-08-06 13:56:04 +08:00
const receiving = detail . capabilities . find (( item ) => item . capabilityCode === 'RECEIVING' )
editorMode . value = 'EDIT'
Object . assign ( form , {
id : String ( detail . warehouse . id ),
code : detail . warehouse . warehouseCode ,
name : detail . warehouse . warehouseName ,
2026-08-04 11:14:34 +08:00
warehouseType : detail . warehouse . warehouseType ,
countryCode : detail . warehouse . countryCode ,
timezone : detail . warehouse . timezone ,
2026-08-06 13:56:04 +08:00
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
address : address ? . addressLine1 || '' ,
postalCode : address ? . postalCode || '' ,
contactName : detail . warehouse . contactName || '' ,
contactPhone : detail . warehouse . contactPhone || '' ,
businessHours : receiving ? . description || '' ,
status : detail . warehouse . status ,
remark : detail . warehouse . remark || '' ,
2026-08-04 09:05:19 +08:00
})
clearWarehouseFormErrors ()
captureBaseline ()
editorOpen . value = true
} finally {
editLoading . value = false
}
2026-07-30 17:59:43 +08:00
}
2026-07-31 13:43:16 +08:00
function nullable ( value : string ) {
return value . trim () || null
}
2026-08-06 13:56:04 +08:00
function buildSaveRequest () : SavePlatformWarehouseRequest {
2026-07-31 13:43:16 +08:00
const existing = form . id ? rows . value . find (( item ) => item . id === form . id ) ? . detail : null
const warehouse = existing ? . warehouse
const currentAddress = existing ? . addresses [ 0 ]
2026-08-06 13:56:04 +08:00
const countryCode = isDomesticForm . value ? 'CN' : form . countryCode . trim (). toUpperCase ()
const timezone = isDomesticForm . value ? 'UTC+8' : form . timezone . trim ()
2026-07-31 13:43:16 +08:00
const capabilities = ( existing ? . capabilities || []). map (( capability ) => ({
capabilityCode : capability . capabilityCode ,
capabilityName : capability . capabilityName ,
enabled : capability . enabled ?? true ,
cutoffTime : capability . cutoffTime ,
description : capability . capabilityCode === 'RECEIVING' ? nullable ( form . businessHours ) : capability . description ,
}))
2026-08-06 13:56:04 +08:00
if ( form . warehouseType !== 'FBA' && ! capabilities . some (( item ) => item . capabilityCode === 'RECEIVING' )) {
2026-07-31 13:43:16 +08:00
capabilities . push ({
2026-08-06 13:56:04 +08:00
capabilityCode : 'RECEIVING' , capabilityName : '收货服务' , enabled : true , cutoffTime : null ,
2026-07-31 13:43:16 +08:00
description : nullable ( form . businessHours ),
})
}
return {
warehouseCode : form . code . trim (). toUpperCase (),
warehouseName : form . name . trim (),
2026-08-04 11:14:34 +08:00
warehouseType : form . warehouseType ,
2026-08-06 13:56:04 +08:00
countryCode ,
timezone ,
2026-07-31 13:43:16 +08:00
contactName : nullable ( form . contactName ),
contactPhone : nullable ( form . contactPhone ),
contactEmail : warehouse ? . contactEmail || null ,
status : form . status ,
remark : nullable ( form . remark ),
address : {
addressType : currentAddress ? . addressType || 'OPERATING' ,
2026-08-06 13:56:04 +08:00
countryCode ,
2026-07-31 13:43:16 +08:00
stateProvince : nullable ( form . province ),
city : form . city . trim (),
district : currentAddress ? . district || null ,
2026-08-06 13:56:04 +08:00
postalCode : nullable ( form . postalCode ),
2026-07-31 13:43:16 +08:00
addressLine1 : form . address . trim (),
addressLine2 : currentAddress ? . addressLine2 || null ,
latitude : currentAddress ? . latitude || null ,
longitude : currentAddress ? . longitude || null ,
},
capabilities ,
}
}
async function saveWarehouse () {
2026-08-06 13:56:04 +08:00
if ( ! requireWarehouseReady () || ! validateWarehouseForm ()) return
2026-07-31 13:43:16 +08:00
saving . value = true
2026-07-30 17:59:43 +08:00
try {
2026-08-06 13:56:04 +08:00
if ( isFbaCreate . value ) {
await createFbaWarehouse ({
warehouseCode : form . code . trim (). toUpperCase (),
stateProvince : form . province . trim (). toUpperCase (),
city : form . city . trim (),
postalCode : form . postalCode . trim (),
addressLine1 : form . address . trim (),
timezone : form . timezone . trim () || null ,
})
} else {
const payload = buildSaveRequest ()
if ( editorMode . value === 'EDIT' ) await updatePlatformWarehouse ( form . id , payload )
else await createPlatformWarehouse ( payload )
}
2026-07-30 17:59:43 +08:00
editorOpen . value = false
2026-08-06 13:56:04 +08:00
ElMessage . success ( editorMode . value === 'EDIT' ? '仓库资料已更新' : ` ${ warehouseTypeLabels [ createType . value ] } 已创建` )
if ( page . value === 1 ) await loadWarehouses ()
else page . value = 1
2026-07-30 17:59:43 +08:00
} catch ( error ) {
2026-07-31 13:43:16 +08:00
const details = getApiErrorDetails ( error , '仓库保存失败,请检查后重试' )
ElMessage . error ( details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message )
} finally {
saving . value = false
2026-07-30 17:59:43 +08:00
}
}
2026-07-31 13:43:16 +08:00
async function toggleStatus ( warehouse : WarehouseRow ) {
2026-08-04 11:14:34 +08:00
if ( ! requireWarehouseReady ()) return
2026-07-30 17:59:43 +08:00
const target = warehouse . status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
const targetText = target === 'ACTIVE' ? '启用' : '停用'
try {
await ElMessageBox . confirm (
target === 'INACTIVE'
2026-08-04 11:14:34 +08:00
? `停用“ ${ warehouse . name } ”后,客户新建头程订单时将无法选择该公共仓库。`
2026-07-30 17:59:43 +08:00
: `确认重新启用“ ${ warehouse . name } ”供客户下单选择吗?` ,
2026-08-04 11:14:34 +08:00
` ${ targetText } 公共仓库` ,
2026-07-30 17:59:43 +08:00
{ confirmButtonText : `确认 ${ targetText } ` , cancelButtonText : '取消' , type : target === 'INACTIVE' ? 'warning' : 'info' },
)
2026-07-31 13:43:16 +08:00
statusLoadingIds . value = new Set ( statusLoadingIds . value ). add ( warehouse . id )
2026-08-06 13:56:04 +08:00
await changePlatformWarehouseStatus ( warehouse . id , target )
2026-07-31 13:43:16 +08:00
warehouse . status = target
if ( warehouse . detail ) warehouse . detail . warehouse . status = target
2026-07-30 17:59:43 +08:00
ElMessage . success ( `仓库已 ${ targetText } ` )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
2026-07-31 13:43:16 +08:00
const details = getApiErrorDetails ( error , `仓库 ${ targetText } 失败,请重试` )
ElMessage . error ( details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message )
} finally {
const next = new Set ( statusLoadingIds . value )
next . delete ( warehouse . id )
statusLoadingIds . value = next
2026-07-30 17:59:43 +08:00
}
}
2026-07-31 13:43:16 +08:00
2026-08-06 13:56:04 +08:00
function openImport () {
importFile . value = null
importMode . value = 'CREATE_ONLY'
importResult . value = null
importOpen . value = true
}
function selectImportFile ( uploadFile : UploadFile ) {
const file = uploadFile . raw ?? null
if ( ! file ) return
if ( ! file . name . toLowerCase (). endsWith ( '.xlsx' )) {
importFile . value = null
ElMessage . error ( '请选择标准 XLSX 文件' )
return
}
if ( file . size > 5 * 1024 * 1024 ) {
importFile . value = null
ElMessage . error ( '文件不得超过 5MB' )
return
}
importFile . value = file
importResult . value = null
}
function removeImportFile () {
importFile . value = null
importResult . value = null
}
function saveBlob ( blob : Blob , fileName : string ) {
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = fileName
anchor . click ()
URL . revokeObjectURL ( url )
}
async function downloadTemplate () {
downloadingTemplate . value = true
try {
const blob = await downloadWarehouseImportTemplate ()
saveBlob ( blob , '公共仓库导入模板.xlsx' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '模板下载失败,请稍后重试' ). message )
} finally {
downloadingTemplate . value = false
}
}
async function runImport ( dryRun : boolean ) {
if ( ! importFile . value ) {
ElMessage . warning ( '请先选择 XLSX 文件' )
return
}
importing . value = true
try {
importResult . value = await importWarehouses ( importFile . value , dryRun , importMode . value )
if ( dryRun ) {
ElMessage . success ( importResult . value . errorCount ? '预检完成,请处理错误后重新上传' : '预检通过,可以提交导入' )
} else if ( importResult . value . committed ) {
ElMessage . success ( '公共仓库已导入' )
if ( page . value === 1 ) await loadWarehouses ()
else page . value = 1
}
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , dryRun ? '文件预检失败,请检查后重试' : '公共仓库导入失败,请重试' ). message )
} finally {
importing . value = false
}
}
function downloadImportErrors () {
if ( ! importResult . value ? . errors . length ) return
const escape = ( value : string | number ) => `" ${ String ( value ). replaceAll ( '"' , '""' ) } "`
const lines = [ '行号,字段,错误码,原因' , ... importResult . value . errors . map (( error ) => (
[ error . row , error . field , error . code || '-' , error . message ]. map ( escape ). join ( ',' )
))]
saveBlob ( new Blob ([ ` \ uFEFF ${ lines . join ( '\r\n' ) } ` ], { type : 'text/csv;charset=utf-8' }), '公共仓库导入错误清单.csv' )
}
function formatImportErrorCode ( row : WarehouseImportResult [ 'errors' ][ number ]) {
return row . code || '-'
}
2026-08-04 11:14:34 +08:00
onMounted (() => void loadWarehouses ())
2026-07-30 17:59:43 +08:00
</ script >
< template >
2026-08-10 16:24:54 +08:00
< AdminPage as = "div" >
< ListPageLayout aria -label =" 仓库列表页面 " >
< template # toolbar >
< QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="仓库筛选" :fields="queryFields" preference-key="warehouses-query" @search="queryWarehouses" />
</ template >
< template v-if = "loadError" # state >
< LoadErrorState
title = "仓库数据加载失败"
:message = "loadErrorDescription"
:loading = "loading"
retry -text =" 重新查询 "
@retry ="loadWarehouses"
/>
</ template >
2026-07-31 13:43:16 +08:00
2026-08-10 16:24:54 +08:00
< section v-if = "!loadError" class="surface-panel table-panel" aria-live="polite" >
2026-08-01 12:17:33 +08:00
< DataTable
v -model:page = "page"
v -model:page-size = "pageSize"
2026-08-06 13:56:04 +08:00
:data = "rows"
2026-08-01 12:17:33 +08:00
:columns = "warehouseColumns"
2026-08-06 19:03:13 +08:00
preference -key =" warehouses -table "
2026-08-01 12:17:33 +08:00
:loading = "loading"
2026-08-06 13:56:04 +08:00
:total = "total"
: page -sizes ="[ 20 , 50 , 100 ]"
2026-08-01 12:17:33 +08:00
row -key =" id "
2026-08-04 11:14:34 +08:00
aria -label =" 公共仓库列表 "
: empty -text =" loaded ? ' 暂无符合筛选条件的仓库 ' : ' 仓库数据暂不可用 , 请重新加载 '"
2026-08-01 12:17:33 +08:00
>
2026-08-06 20:14:56 +08:00
< template # toolbar -actions >
< div v-if = "access.can('platform:warehouse:create')" class="warehouse-primary-actions" >
2026-08-10 16:24:54 +08:00
< el-button :disabled = "!warehouseReady" @click ="openImport" >< FileUp :size = "16" /> 导入仓库 </ el-button >
2026-08-06 20:14:56 +08:00
< el-dropdown trigger = "click" :disabled = "!warehouseReady" @command ="openCreate($event as WarehouseType)" >
< el-button type = "primary" :disabled = "!warehouseReady" >< Plus :size = "16" /> 新增仓库 < ChevronDown :size = "15" /></ el-button >
< template # dropdown >< el-dropdown-menu >< el-dropdown-item command = "FBA" >< FileSpreadsheet :size = "15" /> 新增 FBA 地址 </ el-dropdown-item >< el-dropdown-item command = "DOMESTIC" >< WarehouseIcon :size = "15" /> 新增国内仓 </ el-dropdown-item >< el-dropdown-item command = "OVERSEAS" >< MapPin :size = "15" /> 新增海外仓 </ el-dropdown-item ></ el-dropdown-menu ></ template >
</ el-dropdown >
</ div >
</ template >
2026-08-01 12:17:33 +08:00
< template # actions = "{ row }" >
2026-08-06 13:56:04 +08:00
< div v-if = "access.can('platform:warehouse:edit') || access.can('platform:warehouse:status')" class="table-actions" >
< el-button v-if = "access.can('platform:warehouse:edit')" link type="primary" :disabled="!warehouseReady || editLoading" @click="openEdit(row as WarehouseRow)"><Pencil :size="15" /> 编辑 </ el-button >
< el-button
v-if = "access.can('platform:warehouse:status')"
link
: type = "(row as WarehouseRow).status === 'ACTIVE' ? 'danger' : 'success'"
:disabled = "!warehouseReady"
: loading = "statusLoadingIds.has((row as WarehouseRow).id)"
@click ="toggleStatus(row as WarehouseRow)"
>< Power :size = "15" />{{ ( row as WarehouseRow ). status === 'ACTIVE' ? '停用' : '启用' }}</ el-button >
</ div >
< span v-else class = "warehouse-subline" > 仅查看 </ span >
2026-08-01 12:17:33 +08:00
</ template >
< template # warehouse = "{ row }" >< div class = "warehouse-cell" >< span >< WarehouseIcon :size = "18" /></ span >< div >< strong >{{ ( row as WarehouseRow ). name }}</ strong >< small >{{ ( row as WarehouseRow ). code }}</ small ></ div ></ div ></ template >
2026-08-04 11:14:34 +08:00
< template # warehouseType = "{ row }" >< el-tag effect = "plain" >{{ warehouseTypeLabels [( row as WarehouseRow ). warehouseType ] }}</ el-tag ></ template >
2026-08-06 13:56:04 +08:00
< template # countryCode = "{ row }" >{{ formatCountryDisplay (( row as WarehouseRow ). countryCode ) }}</ template >
2026-08-01 12:17:33 +08:00
< template # city = "{ row }" >{{ [( row as WarehouseRow ). province , ( row as WarehouseRow ). city ]. filter ( Boolean ). join ( ' · ' ) || '-' }}</ template >
2026-08-06 13:56:04 +08:00
< template # address = "{ row }" >< div class = "warehouse-address" >< MapPin :size = "15" />< span >{{ ( row as WarehouseRow ). address || '-' }}< small v-if = "(row as WarehouseRow).postalCode" >{{ ( row as WarehouseRow ) .postalCode }}</ small ></ span ></ div ></ template >
2026-08-01 12:17:33 +08:00
< template # contact = "{ row }" >< strong >{{ ( row as WarehouseRow ). contactName || '-' }}</ strong >< span class = "warehouse-subline" >{{ ( row as WarehouseRow ). contactPhone || '-' }}</ span ></ template >
< template # status = "{ row }" >< el-tag : type = "(row as WarehouseRow).status === 'ACTIVE' ? 'success' : 'info'" effect = "light" >{{ ( row as WarehouseRow ). status === 'ACTIVE' ? '启用' : '停用' }}</ el-tag ></ template >
2026-08-06 13:56:04 +08:00
< template # pagination -left =" { total : tableTotal } " > 共 {{ tableTotal }} 个公共仓库 </ template >
2026-08-01 12:17:33 +08:00
</ DataTable >
2026-08-10 16:24:54 +08:00
</ section >
</ ListPageLayout >
2026-07-30 17:59:43 +08:00
2026-08-10 16:24:54 +08:00
< AppDrawer v-model = "editorOpen" :title="editorLabel" size="min(620px, 94vw)" class="editor-drawer" :close-on-click-modal="false" :confirm-loading="saving" :show-confirm="false" >
2026-08-01 12:17:33 +08:00
< el-form :model = "form" label -position =" top " class = "drawer-form" :disabled = "saving" >
2026-08-10 16:24:54 +08:00
< FormSection title = "基础信息" >
2026-08-06 13:56:04 +08:00
< div class = "form-grid two-cols" >
< el-form-item v-if = "isFbaCreate" label="FBA 编码" required :class="{ 'has-field-error': formErrors.code }"><el-input v-model="form.code" maxlength="50" placeholder="例如 LAX9" @input="form.code = String($event).toUpperCase(); clearFieldError('code')" /><p v-if="formErrors.code" class="field-error" role="alert" >{{ formErrors.code }}</ p > </ el -form-item >
< el-form-item v-if = "isFbaCreate" label="国家或地区" required >
< el-select v-model = "form.countryCode" disabled placeholder="美国 US" >
< el-option label = "美国 US" value = "US" />
</ el-select >
</ el-form-item >
< template v-else >
< el-form-item label = "仓库名称" required : class = "{ 'has-field-error': formErrors.name }" >< el-input v-model = "form.name" maxlength="120" placeholder="请输入仓库名称" @input="clearFieldError('name')" /><p v-if="formErrors.name" class="field-error" role="alert" >{{ formErrors.name }}</ p > </ el -form-item >
< el-form-item v-if = "editorMode === 'EDIT'" label="仓库类型" required><el-select v-model="form.warehouseType"><el-option v-for="(label, value) in warehouseTypeLabels" :key="value" :label="label" :value="value" /></ el-select ></ el-form-item >
< el-form-item v-if = "!isDomesticCreate" label="国家或地区" required :class="{ 'has-field-error': formErrors.countryCode }" >
< el-select v-if = "isDomesticForm" v-model="form.countryCode" disabled placeholder="中国 CN"><el-option label="中国 CN" value="CN" /></ el-select >
< el-select v-else v-model = "form.countryCode" filterable placeholder="请选择国家或地区" @change="onCountryChange"><el-option v-for="country in countryOptions" :key="country.value" :label="country.label" :value="country.value" /></ el-select >
< p v-if = "formErrors.countryCode" class="field-error" role="alert" >{{ formErrors.countryCode }}</ p >
</ el -form-item >
< el-form-item v-if = "!isDomesticCreate" label="时区" required :class="{ 'has-field-error': formErrors.timezone }" >
< el-select v-if = "isDomesticForm" v-model="form.timezone" disabled placeholder="UTC+8"><el-option label="UTC+8" value="UTC+8" /></ el-select >
< el-select v-else v-model = "form.timezone" filterable placeholder="请选择时区" @change="clearFieldError('timezone')"><el-option v-for="timezone in timezoneOptions" :key="timezone" :label="timezone" :value="timezone" /></ el-select >
< p v-if = "formErrors.timezone" class="field-error" role="alert" >{{ formErrors.timezone }}</ p >
</ el -form-item >
</ template >
</ div >
2026-08-10 16:24:54 +08:00
</ FormSection >
2026-08-06 13:56:04 +08:00
2026-08-10 16:24:54 +08:00
< FormSection title = "地址信息" >
2026-08-06 13:56:04 +08:00
< div class = "form-grid two-cols" >
< el-form-item :label = "addressRegionLabel" : required = "isFbaCreate || usesUsStateSelect" : class = "{ 'has-field-error': formErrors.province }" >
< el-select v-if = "usesUsStateSelect" v-model="form.province" filterable :placeholder="regionPlaceholder" @change="onRegionChange"><el-option v-for="state in usStateOptions" :key="state.value" :label="state.label" :value="state.value" /></ el-select >
< el-input v-else v-model = "form.province" maxlength="100" :placeholder="regionPlaceholder" @input="isFbaCreate && (form.province = String($event).toUpperCase()); clearFieldError('province')" />
< p v-if = "formErrors.province" class="field-error" role="alert" >{{ formErrors.province }}</ p >
</ el -form-item >
< el-form-item label = "城市" required : class = "{ 'has-field-error': formErrors.city }" >< el-input v-model = "form.city" maxlength="100" :placeholder="cityPlaceholder" @input="clearFieldError('city')" /><p v-if="formErrors.city" class="field-error" role="alert" >{{ formErrors.city }}</ p > </ el -form-item >
< el-form-item label = "邮编" :required = "isFbaCreate" : class = "{ 'has-field-error': formErrors.postalCode }" >< el-input v-model = "form.postalCode" maxlength="20" :placeholder="isDomesticForm ? '例如 322000' : '例如 90802'" @input="clearFieldError('postalCode')" /><p v-if="formErrors.postalCode" class="field-error" role="alert" >{{ formErrors.postalCode }}</ p > </ el -form-item >
</ div >
< el-form-item label = "详细地址" required : class = "{ 'has-field-error': formErrors.address }" >< el-input v-model = "form.address" maxlength="255" type="textarea" :rows="3" :placeholder="addressPlaceholder" @input="clearFieldError('address')" /><p v-if="formErrors.address" class="field-error" role="alert" >{{ formErrors.address }}</ p > </ el -form-item >
2026-08-10 16:24:54 +08:00
</ FormSection >
2026-08-06 13:56:04 +08:00
2026-08-10 16:24:54 +08:00
< FormSection v-if = "!isFbaCreate" title="编码与状态" >
2026-08-06 13:56:04 +08:00
< div class = "form-grid two-cols compact-grid" >
< el-form-item label = "仓库编码" : class = "{ 'has-field-error': formErrors.code }" >< el-input v-model = "form.code" maxlength="50" placeholder="系统自动生成" @input="form.code = String($event).toUpperCase(); clearFieldError('code')" /><p v-if="formErrors.code" class="field-error" role="alert" >{{ formErrors.code }}</ p > </ el -form-item >
< el-form-item label = "状态" >< el-radio-group v-model = "form.status" class="warehouse-status-toggle"><el-radio-button value="ACTIVE">启用</el-radio-button><el-radio-button value="INACTIVE" > 停用 </ el -radio-button ></ el-radio-group ></ el-form-item >
</ div >
2026-08-10 16:24:54 +08:00
</ FormSection >
2026-08-06 13:56:04 +08:00
2026-08-10 16:24:54 +08:00
< FormSection v-if = "!isFbaCreate" title="联系信息" >
2026-08-06 13:56:04 +08:00
< div class = "form-grid two-cols compact-grid" >
< el-form-item label = "联系人" >< el-input v-model = "form.contactName" maxlength="64" placeholder="选填" /></ el-form-item >
< el-form-item label = "联系电话" >< el-input v-model = "form.contactPhone" maxlength="32" placeholder="选填" /></ el-form-item >
< el-form-item label = "收货时间" >< el-input v-model = "form.businessHours" maxlength="500" placeholder="例如 周一至周六 08:30-18:00" /></ el-form-item >
</ div >
< el-form-item label = "备注" >< el-input v-model = "form.remark" maxlength="500" type="textarea" :rows="3" show-word-limit placeholder="选填" /></ el-form-item >
2026-08-10 16:24:54 +08:00
</ FormSection >
2026-07-30 17:59:43 +08:00
</ el-form >
2026-08-10 16:24:54 +08:00
< template # footer >< div class = "drawer-footer" >< el-button :disabled = "saving" @click ="editorOpen = false" > 取消 </ el -button >< el-button type = "primary" :loading = "saving" @click ="saveWarehouse" >< Save :size = "16" />{{ editorMode === 'EDIT' ? '保存修改' : '确认新增' }}</ el-button ></ div ></ template >
</ AppDrawer >
2026-08-06 13:56:04 +08:00
< el-dialog v-model = "importOpen" title="导入公共仓库" width="min(760px, calc(100vw - 32px))" :close-on-click-modal="!importing" :close-on-press-escape="!importing" >
< div class = "import-content" >
< div class = "import-toolbar" >
< el-button :loading = "downloadingTemplate" @click ="downloadTemplate" >< Download :size = "16" /> 下载 XLSX 模板 </ el-button >
< el-radio-group v-model = "importMode" :disabled="importing" >
< el-radio-button value = "CREATE_ONLY" > 仅新增 </ el-radio-button >
< el-radio-button value = "UPSERT" :disabled = "!access.can('platform:warehouse:edit')" > 新增并更新 </ el-radio-button >
</ el-radio-group >
</ div >
< el-upload accept = ".xlsx" :auto-upload = "false" :limit = "1" :on-change = "selectImportFile" :on-remove = "removeImportFile" >
2026-08-10 16:24:54 +08:00
< el-button >< FileUp :size = "16" /> 选择 XLSX 文件 </ el-button >
2026-08-06 13:56:04 +08:00
< template # tip >< div class = "el-upload__tip" > 最多 5 MB 、 1000 行 ; 请使用标准模板 , FBA 名称可不填并默认使用编码 。 </ div ></ template >
</ el-upload >
< div v-if = "importResult" class="import-result" role="status" >
< dl class = "import-summary" >
< div >< dt > 总行数 </ dt >< dd >{{ importResult . totalRows }}</ dd ></ div >
< div >< dt > 有效 </ dt >< dd >{{ importResult . validRows }}</ dd ></ div >
< div >< dt > 新增 </ dt >< dd >{{ importResult . createCount }}</ dd ></ div >
< div >< dt > 更新 </ dt >< dd >{{ importResult . updateCount }}</ dd ></ div >
< div >< dt > 无变化 </ dt >< dd >{{ importResult . unchangedCount }}</ dd ></ div >
< div >< dt > 错误 </ dt >< dd : class = "{ 'is-error': importResult.errorCount }" >{{ importResult . errorCount }}</ dd ></ div >
</ dl >
< div class = "import-result-heading" >< strong >{{ importResult . committed ? '导入已提交' : ( importResult . errorCount ? '预检未通过,未写入数据' : '预检通过,尚未写入数据' ) }}</ strong >< el-button v-if = "importResult.errors.length" size="small" @click="downloadImportErrors"><Download :size="15" /> 下载错误清单 </ el-button ></ div >
< el-table v-if = "importResult.errors.length" :data="importResult.errors" max-height="260" size="small" >
< el-table-column prop = "row" label = "行号" width = "70" />< el-table-column prop = "field" label = "字段" width = "110" />< el-table-column prop = "code" label = "错误码" width = "130" :formatter = "formatImportErrorCode" />< el-table-column prop = "message" label = "原因" min -width =" 220 " />
</ el-table >
</ div >
</ div >
2026-08-10 16:24:54 +08:00
< template # footer >< el-button :disabled = "importing" @click ="importOpen = false" > 关闭 </ el -button >< el-button :loading = "importing" :disabled = "!importFile" @click ="runImport(true)" > 预检文件 </ el -button >< el-button type = "primary" :loading = "importing" :disabled = "!canCommitImport" @click ="runImport(false)" >< FileUp :size = "16" /> 确认导入 </ el-button ></ template >
2026-08-06 13:56:04 +08:00
</ el-dialog >
2026-08-10 16:24:54 +08:00
</ AdminPage >
2026-07-30 17:59:43 +08:00
</ template >
< style scoped >
2026-07-31 13:43:16 +08:00
. state - alert p { margin : 6 px 0 10 px ; line - height : 1.55 ; }
2026-08-06 13:56:04 +08:00
. warehouse - heading { gap : 16 px ; }
. warehouse - primary - actions , . import - toolbar , . import - result - heading { display : flex ; flex - wrap : wrap ; align - items : center ; gap : 10 px ; }
2026-07-30 17:59:43 +08:00
. warehouse - cell { display : flex ; align - items : center ; gap : 10 px ; }
2026-08-06 13:56:04 +08:00
. warehouse - cell > span { display : grid ; width : 34 px ; height : 34 px ; flex : 0 0 34 px ; place - items : center ; border - radius : 8 px ; background : # eef3fa ; color : var ( -- cc - primary ); }
2026-07-30 17:59:43 +08:00
. warehouse - cell > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
2026-07-31 13:43:16 +08:00
. warehouse - cell strong { overflow - wrap : anywhere ; }
2026-08-06 13:56:04 +08:00
. warehouse - cell small , . warehouse - subline , . warehouse - address small { display : block ; color : var ( -- cc - muted ); font - size : 11 px ; }
2026-07-31 13:43:16 +08:00
. warehouse - address { display : flex ; align - items : flex - start ; gap : 7 px ; line - height : 1.55 ; overflow - wrap : anywhere ; }
2026-07-30 17:59:43 +08:00
. warehouse - address svg { flex : 0 0 15 px ; margin - top : 2 px ; color : var ( -- cc - muted ); }
2026-08-06 13:56:04 +08:00
. warehouse - address span { min - width : 0 ; }
2026-08-01 12:17:33 +08:00
. field - error { width : 100 % ; margin : 6 px 0 0 ; color : var ( -- el - color - danger ); font - size : 12 px ; line - height : 1.35 ; overflow - wrap : anywhere ; }
2026-08-06 13:56:04 +08:00
. compact - grid { row - gap : 10 px ; }
. warehouse - status - toggle : deep (. el - radio - button__inner ) { min - width : 74 px ; }
. import - content { display : grid ; gap : 20 px ; }
. import - toolbar { justify - content : space - between ; }
. import - result { display : grid ; gap : 14 px ; padding - top : 16 px ; border - top : 1 px solid var ( -- cc - border ); }
. import - summary { display : grid ; grid - template - columns : repeat ( 6 , minmax ( 0 , 1 fr )); margin : 0 ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; overflow : hidden ; }
. import - summary div { min - width : 0 ; padding : 10 px 12 px ; border - right : 1 px solid var ( -- cc - border ); background : var ( -- cc - surface - soft , # f8fafc ); }
. import - summary div : last - child { border - right : 0 ; }
. import - summary dt { color : var ( -- cc - muted ); font - size : 11 px ; }
. import - summary dd { margin : 3 px 0 0 ; color : var ( -- cc - text ); font - size : 16 px ; font - weight : 700 ; }
. import - summary dd . is - error { color : var ( -- el - color - danger ); }
. import - result - heading { justify - content : space - between ; }
2026-08-01 12:17:33 +08:00
: deep (. has - field - error . el - input__wrapper ) { box - shadow : 0 0 0 1 px var ( -- el - color - danger ) inset ; }
: deep (. has - field - error . el - textarea__inner ) { box - shadow : 0 0 0 1 px var ( -- el - color - danger ) inset ; }
2026-08-06 13:56:04 +08:00
: global (. el - dropdown - menu__item ) { gap : 8 px ; }
2026-07-31 13:43:16 +08:00
2026-08-03 11:45:45 +08:00
@ media ( max - width : 720 px ) {
2026-08-06 13:56:04 +08:00
. warehouse - heading { align - items : stretch ; }
. warehouse - primary - actions { width : 100 % ; }
. warehouse - primary - actions > . el - button , . warehouse - primary - actions > . el - dropdown { flex : 1 1 150 px ; }
. warehouse - primary - actions : deep (. el - dropdown . el - button ) { width : 100 % ; min - height : 44 px ; }
. warehouse - primary - actions > . el - button { min - height : 44 px ; }
. import - toolbar { align - items : stretch ; flex - direction : column ; }
. import - summary { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); }
. import - summary div : nth - child ( 3 ) { border - right : 0 ; }
. import - summary div : nth - child ( - n + 3 ) { border - bottom : 1 px solid var ( -- cc - border ); }
2026-08-03 11:45:45 +08:00
}
2026-08-06 13:56:04 +08:00
@ media ( max - width : 420 px ) {
. warehouse - primary - actions { flex - direction : column ; }
. warehouse - primary - actions > . el - button , . warehouse - primary - actions > . el - dropdown { width : 100 % ; flex - basis : auto ; }
. import - summary { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. import - summary div : nth - child ( 3 ) { border - right : 1 px solid var ( -- cc - border ); }
. import - summary div : nth - child ( 2 n ) { border - right : 0 ; }
. import - summary div : nth - child ( - n + 4 ) { border - bottom : 1 px solid var ( -- cc - border ); }
}
2026-07-30 17:59:43 +08:00
</ style >