@@ -1,5 +1,5 @@
< script setup lang = "ts" >
import { computed , reactive , ref , watch } from 'vue'
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { MapPin , Pencil , Plus , Power , Warehouse as WarehouseIcon } from '@lucide/vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
@@ -15,11 +15,18 @@ import {
type PlatformWarehouseDetailResource ,
type PlatformWarehouseResource ,
type SavePlatformWarehouseRequest ,
type WarehouseType ,
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { PlatformTenantSelector } from '@/modules/iam'
import type { Warehouse } from '@/modules/warehouses/types'
const DEFAULT _TENANT _ID = 10001
const warehouseTypeLabels : Record < WarehouseType , string > = {
DOMESTIC : '国内收货仓' ,
FBA : 'FBA 仓' ,
OVERSEAS : '海外仓' ,
}
const access = useAccessPreviewStore ( )
const editorOpen = ref ( false )
const saving = ref ( false )
@@ -27,16 +34,22 @@ const loading = ref(false)
const editLoading = ref ( false )
const page = ref ( 1 )
const pageSize = ref ( 10 )
const selectedTenantId = ref ( '' )
const loadedTenantId = ref < string | null > ( null )
const loaded = ref ( false )
const loadError = ref < ApiErrorDetails | null > ( null )
const detailWarningCount = ref ( 0 )
const statusLoadingIds = ref ( new Set < string > ( ) )
type WarehouseFilters = QueryModel & { keyword : string ; status : Warehouse [ 'status' ] | '' }
const filters = ref < WarehouseFilters > ( { keyword : '' , status : '' } )
type WarehouseFilters = QueryModel & { keyword : string ; warehouseType : WarehouseType | '' ; status : Warehouse [ 'status' ] | '' }
const filters = ref < WarehouseFilters > ( { keyword : '' , warehouseType : '' , status : '' } )
const appliedFilters = ref < WarehouseFilters > ( { ... filters . value } )
const queryFields : QueryField [ ] = [
{ key : 'keyword' , type : 'input' , placeholder : '仓库名称、编码、城市或地址' } ,
{
key : 'warehouseType' , type : 'select' , placeholder : '全部仓库类型' , options : [
{ label : warehouseTypeLabels . DOMESTIC , value : 'DOMESTIC' } ,
{ label : warehouseTypeLabels . FBA , value : 'FBA' } ,
{ label : warehouseTypeLabels . OVERSEAS , value : 'OVERSEAS' } ,
] ,
} ,
{
key : 'status' , type : 'select' , placeholder : '全部状态' , options : [
{ label : '启用' , value : 'ACTIVE' } ,
@@ -47,6 +60,9 @@ const queryFields: QueryField[] = [
let loadSequence = 0
interface WarehouseRow extends Warehouse {
warehouseType : WarehouseType
countryCode : string
timezone : string
detail : PlatformWarehouseDetailResource | null
}
@@ -54,6 +70,9 @@ const rows = ref<WarehouseRow[]>([])
const warehouseColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'warehouse' , label : '仓库' , minWidth : 200 , slotName : 'warehouse' } ,
{ key : 'warehouseType' , label : '仓库类型' , width : 130 , slotName : 'warehouseType' } ,
{ prop : 'countryCode' , label : '国家/地区码' , width : 120 } ,
{ prop : 'timezone' , label : '时区' , minWidth : 170 } ,
{ key : 'city' , label : '所在城市' , minWidth : 130 , slotName : 'city' } ,
{ key : 'address' , label : '收货地址' , minWidth : 260 , slotName : 'address' } ,
{ key : 'contact' , label : '联系人' , minWidth : 150 , slotName : 'contact' } ,
@@ -61,20 +80,30 @@ const warehouseColumns: DataTableColumn[] = [
{ key : 'status' , label : '状态' , width : 100 , slotName : 'status' } ,
]
const emptyForm = ( ) : Omit < Warehouse , ' updatedAt ' > => ( {
type WarehouseForm = Omit < Warehouse , ' updatedAt ' > & {
warehouseType : WarehouseType
countryCode : string
timezone : string
}
const emptyForm = ( ) : WarehouseForm => ( {
id : '' , code : '' , name : '' , province : '' , city : '' , address : '' , contactName : '' , contactPhone : '' ,
warehouseType : 'DOMESTIC' , countryCode : 'CN' , timezone : 'Asia/Shanghai' ,
businessHours : '周一至周六 08:30-18:00' , status : 'ACTIVE' , remark : '' ,
} )
const form = reactive ( emptyForm ( ) )
type WarehouseRequiredField = 'code' | 'name' | 'city' | 'address'
type WarehouseRequiredField = 'code' | 'name' | 'countryCode' | 'timezone' | 'city' | 'address'
const requiredFieldMessages : Record < WarehouseRequiredField , string > = {
code : '请输入仓库编码' ,
name : '请输入仓库名称' ,
countryCode : '请输入国家或地区二字码' ,
timezone : '请输入 IANA 时区' ,
city : '请输入城市' ,
address : '请输入详细地址' ,
}
const formErrors = reactive < Record < WarehouseRequiredField , string > > ( { code : '' , name : '' , city : '' , address : '' } )
const editorLabel = computed ( ( ) => form . id ? '编辑收货仓' : '新增收货仓 ' )
const formErrors = reactive < Record < WarehouseRequiredField , string > > ( {
code : '' , name : '' , countryCode : '' , timezone : '' , city : '' , address : '' ,
} )
const editorLabel = computed ( ( ) => form . id ? '编辑公共仓库' : '新增公共仓库' )
const editorPermission = computed ( ( ) => form . id ? 'platform:warehouse:edit' : 'platform:warehouse:create' )
const { captureBaseline } = useUnsavedWorkRegistration ( {
key : 'warehouse-editor' ,
@@ -84,18 +113,14 @@ const { captureBaseline } = useUnsavedWorkRegistration({
requiredPermission : editorPermission ,
} )
const warehouseReady = computed ( ( ) => Boolean ( selectedTenantId . value )
&& loadedTenantId . value === selectedTenantId . value
&& ! loading . value )
const tenantSwitchDisabled = computed ( ( ) => editorOpen . value
|| editLoading . value
|| saving . value
|| statusLoadingIds . value . size > 0 )
const warehouseReady = computed ( ( ) => loaded . value && ! loading . value )
const filteredRows = computed ( ( ) => rows . value . filter ( ( warehouse ) => {
const keyword = appliedFilters . value . keyword . trim ( ) . toLowerCase ( )
const matchesKeyword = ! keyword || [ warehouse . code , warehouse . name , warehouse . province , warehouse . city , warehouse . address ]
. some ( ( value ) => value . toLowerCase ( ) . includes ( keyword ) )
return matchesKeyword && ( ! appliedFilters . value . status || warehouse . status === appliedFilters . value . status )
return matchesKeyword
&& ( ! appliedFilters . value . warehouseType || warehouse . warehouseType === appliedFilters . value . warehouseType )
&& ( ! appliedFilters . value . status || warehouse . status === appliedFilters . value . status )
} ) )
const pagedRows = computed ( ( ) => filteredRows . value . slice ( ( page . value - 1 ) * pageSize . value , page . value * pageSize . value ) )
const loadErrorDescription = computed ( ( ) => {
@@ -105,7 +130,7 @@ const loadErrorDescription = computed(() => {
loadError . value . requestId ? ` requestId ${ loadError . value . requestId } ` : '' ,
loadError . value . traceId ? ` traceId ${ loadError . value . traceId } ` : '' ,
] . filter ( Boolean )
const retained = loadedTenantId . value ? ` 当前仍显示租户 ${ loadedTenantId . value } 最近一次成功加载的数据。` : ''
const retained = loaded . value ? ' 当前仍显示最近一次成功加载的数据。' : ''
return [ loadError . value . message , references . join ( ' · ' ) , retained ] . filter ( Boolean ) . join ( ' ' )
} )
@@ -126,6 +151,9 @@ function toRow(warehouse: PlatformWarehouseResource, detail: PlatformWarehouseDe
id : String ( warehouse . id ) ,
code : warehouse . warehouseCode ,
name : warehouse . warehouseName ,
warehouseType : warehouse . warehouseType ,
countryCode : warehouse . countryCode ,
timezone : warehouse . timezone ,
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
address : [ address ? . district , address ? . addressLine1 , address ? . addressLine2 ] . filter ( Boolean ) . join ( ' ' ) ,
@@ -142,6 +170,7 @@ function toRow(warehouse: PlatformWarehouseResource, detail: PlatformWarehouseDe
function queryWarehouses ( model : QueryModel ) {
appliedFilters . value = {
keyword : String ( model . keyword || '' ) ,
warehouseType : ( model . warehouseType || '' ) as WarehouseType | '' ,
status : ( model . status || '' ) as Warehouse [ 'status' ] | '' ,
}
page . value = 1
@@ -167,21 +196,14 @@ function validateWarehouseForm() {
. every ( Boolean )
}
function apiTenantId ( value : string ) {
const numeric = Number ( value )
return Number . isSafeInteger ( numeric ) ? numeric : value
}
async function loadWarehouses ( tenantId = selectedTenantId . value ) {
if ( ! tenantId ) return
async function loadWarehouses ( ) {
const sequence = ++ loadSequence
loading . value = true
loadError . value = null
try {
const requestTenantId = apiTenantId ( tenantId )
const result = await listPlatformWarehouses ( requestTenantId , 1 , 200 )
const result = await listPlatformWarehouses ( DEFAULT _TENANT _ID , 1 , 200 )
const details = await Promise . allSettled (
result . records . map ( ( warehouse ) => getPlatformWarehouse ( requestTenantId , warehouse . id ) ) ,
result . records . map ( ( warehouse ) => getPlatformWarehouse ( DEFAULT _TENANT _ID , warehouse . id ) ) ,
)
if ( sequence !== loadSequence ) return
rows . value = result . records . map ( ( warehouse , index ) => {
@@ -189,7 +211,7 @@ async function loadWarehouses(tenantId = selectedTenantId.value) {
return toRow ( warehouse , detailResult ? . status === 'fulfilled' ? detailResult . value : null )
} )
detailWarningCount . value = details . filter ( ( item ) => item . status === 'rejected' ) . length
loadedTenantId . value = tenantId
loaded . value = true
page . value = 1
} catch ( error ) {
if ( sequence !== loadSequence ) return
@@ -199,16 +221,16 @@ async function loadWarehouses(tenantId = selectedTenantId.value) {
}
}
function requireCurrentTenant ( ) {
function requireWarehouseReady ( ) {
if ( ! warehouseReady . value ) {
ElMessage . warning ( '仓库数据尚未加载完成,请稍后重试' )
return null
return false
}
return apiTenantId ( selectedTenantId . value )
return true
}
function openCreate ( ) {
if ( ! requireCurrentTenant ( ) ) return
if ( ! requireWarehouseReady ( ) ) return
Object . assign ( form , emptyForm ( ) )
clearWarehouseFormErrors ( )
captureBaseline ( )
@@ -217,10 +239,9 @@ function openCreate() {
async function ensureDetail ( warehouse : WarehouseRow ) {
if ( warehouse . detail ) return warehouse . detail
const tenantId = requireCurrentTenant ( )
if ( ! tenantId ) return null
if ( ! requireWarehouseReady ( ) ) return null
try {
const detail = await getPlatformWarehouse ( tenantId , warehouse . id )
const detail = await getPlatformWarehouse ( DEFAULT _TENANT _ID , warehouse . id )
Object . assign ( warehouse , toRow ( detail . warehouse , detail ) )
detailWarningCount . value = rows . value . filter ( ( item ) => ! item . detail ) . length
return detail
@@ -232,16 +253,18 @@ async function ensureDetail(warehouse: WarehouseRow) {
}
async function openEdit ( warehouse : WarehouseRow ) {
const tenantAtStart = selectedTenantId . value
editLoading . value = true
try {
const detail = await ensureDetail ( warehouse )
if ( ! detail || selectedTenantId . value !== tenantAtStart || loadedTenantId . value !== tenantAtStart ) return
if ( ! detail || ! loaded . value ) return
const address = detail . addresses [ 0 ]
Object . assign ( form , warehouse , {
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
address : address ? . addressLine1 || '' ,
warehouseType : detail . warehouse . warehouseType ,
countryCode : detail . warehouse . countryCode ,
timezone : detail . warehouse . timezone ,
} )
clearWarehouseFormErrors ( )
captureBaseline ( )
@@ -279,9 +302,9 @@ function buildSaveRequest(tenantId: number | string): SavePlatformWarehouseReque
tenantId ,
warehouseCode : form . code . trim ( ) . toUpperCase ( ) ,
warehouseName : form . name . trim ( ) ,
warehouseType : warehouse ? . warehouseType || 'DOMESTIC' ,
countryCode : warehouse ? . countryCode || 'CN' ,
timezone : warehouse ? . timezone || 'Asia/Shanghai' ,
warehouseType : form . warehouseType ,
countryCode : form . countryCode . trim ( ) . toUpperCase ( ) ,
timezone : form . timezone . trim ( ) ,
contactName : nullable ( form . contactName ) ,
contactPhone : nullable ( form . contactPhone ) ,
contactEmail : warehouse ? . contactEmail || null ,
@@ -289,7 +312,7 @@ function buildSaveRequest(tenantId: number | string): SavePlatformWarehouseReque
remark : nullable ( form . remark ) ,
address : {
addressType : currentAddress ? . addressType || 'OPERATING' ,
countryCode : currentAddress ? . countryCode || warehouse ? . countryCode || 'CN' ,
countryCode : form . countryCode . trim ( ) . toUpperCase ( ) ,
stateProvince : nullable ( form . province ) ,
city : form . city . trim ( ) ,
district : currentAddress ? . district || null ,
@@ -304,15 +327,14 @@ function buildSaveRequest(tenantId: number | string): SavePlatformWarehouseReque
}
async function saveWarehouse ( ) {
const tenantId = requireCurrentTenant ( )
if ( ! tenantId ) return
if ( ! requireWarehouseReady ( ) ) return
if ( ! validateWarehouseForm ( ) ) return
saving . value = true
try {
const payload = buildSaveRequest ( tenantId )
if ( form . id ) await updatePlatformWarehouse ( tenantId , form . id , payload )
const payload = buildSaveRequest ( DEFAULT _TENANT _ID )
if ( form . id ) await updatePlatformWarehouse ( DEFAULT _TENANT _ID , form . id , payload )
else await createPlatformWarehouse ( payload )
const successMessage = form . id ? '仓库资料已更新' : '收货仓 已创建'
const successMessage = form . id ? '仓库资料已更新' : '公共仓库 已创建'
editorOpen . value = false
ElMessage . success ( successMessage )
await loadWarehouses ( )
@@ -325,20 +347,19 @@ async function saveWarehouse() {
}
async function toggleStatus ( warehouse : WarehouseRow ) {
const tenantId = requireCurrentTenant ( )
if ( ! tenantId ) return
if ( ! requireWarehouseReady ( ) ) return
const target = warehouse . status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
const targetText = target === 'ACTIVE' ? '启用' : '停用'
try {
await ElMessageBox . confirm (
target === 'INACTIVE'
? ` 停用“ ${ warehouse . name } ”后,客户新建头程订单时将无法选择该仓库。 `
? ` 停用“ ${ warehouse . name } ”后,客户新建头程订单时将无法选择该公共 仓库。 `
: ` 确认重新启用“ ${ warehouse . name } ”供客户下单选择吗? ` ,
` ${ targetText } 收货仓 ` ,
` ${ targetText } 公共仓库 ` ,
{ confirmButtonText : ` 确认 ${ targetText } ` , cancelButtonText : '取消' , type : target === 'INACTIVE' ? 'warning' : 'info' } ,
)
statusLoadingIds . value = new Set ( statusLoadingIds . value ) . add ( warehouse . id )
await changePlatformWarehouseStatus ( tenantId , warehouse . id , target )
await changePlatformWarehouseStatus ( DEFAULT _TENANT _ID , warehouse . id , target )
warehouse . status = target
if ( warehouse . detail ) warehouse . detail . warehouse . status = target
ElMessage . success ( ` 仓库已 ${ targetText } ` )
@@ -353,19 +374,11 @@ async function toggleStatus(warehouse: WarehouseRow) {
}
}
watch ( selectedTenantId , ( tenantId ) => {
editorOpen . value = false
rows . value = [ ]
loadedTenantId . value = null
detailWarningCount . value = 0
page . value = 1
void loadWarehouses ( tenantId )
} )
onMounted ( ( ) => void loadWarehouses ( ) )
< / script >
< template >
< div class = "business-page" >
< PlatformTenantSelector v-model = "selectedTenantId" :disabled="tenantSwitchDisabled" / >
< el-alert v-if = "loadError" type="error" :closable="false" show-icon class="state-alert" >
< template # title > 仓库数据加载失败 < / template >
< p > { { loadErrorDescription } } < / p >
@@ -381,8 +394,8 @@ watch(selectedTenantId, (tenantId) => {
< section class = "surface-panel table-panel" aria -live = " polite " >
< div class = "panel-heading" >
< div >
< h2 > 平台收货仓 < / h2 >
< p > 租户 { { selectedTenantId || '—' } } · 共 { { filteredRows . length } } 个收货仓 < / p >
< h2 > 公共仓库 < / h2 >
< p > 国内收货仓 、 FBA 仓和海外仓统一维护 · 共 { { filteredRows . length } } 个 < / p >
< / div >
< el-button
v-if = "access.can('platform:warehouse:create')"
@@ -400,8 +413,8 @@ watch(selectedTenantId, (tenantId) => {
:total = "filteredRows.length"
: page -sizes = " [ 10 , 20 , 50 ] "
row -key = " id "
aria -label = " 平台收货仓 列表"
: empty -text = " loadedTenantId ? ' 暂无符合筛选条件的仓库 ' : ' 仓库数据暂不可用 , 请重新加载 ' "
aria -label = " 公共仓库 列表"
: empty -text = " loaded ? ' 暂无符合筛选条件的仓库 ' : ' 仓库数据暂不可用 , 请重新加载 ' "
>
< template # actions = "{ row }" >
< div v-if = "access.can('platform:warehouse:edit') || access.can('platform:warehouse:status')" class="table-actions" >
@@ -418,17 +431,18 @@ watch(selectedTenantId, (tenantId) => {
< span v-else class = "warehouse-subline" > 仅查看 < / span >
< / 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 >
< template # warehouseType = "{ row }" > < el-tag effect = "plain" > { { warehouseTypeLabels [ ( row as WarehouseRow ) . warehouseType ] } } < / el-tag > < / template >
< template # city = "{ row }" > { { [ ( row as WarehouseRow ) . province , ( row as WarehouseRow ) . city ] . filter ( Boolean ) . join ( ' · ' ) || '-' } } < / template >
< template # address = "{ row }" > < div class = "warehouse-address" > < MapPin :size = "15" / > { { ( row as WarehouseRow ) . address || '详情暂未加载' } } < / div > < / template >
< 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 >
< template # pagination -left > 共 { { filteredRows . length } } 个收货仓 < / template >
< template # pagination -left > 共 { { filteredRows . length } } 个公共仓库 < / template >
< / DataTable >
< / section >
< el-drawer v-model = "editorOpen" :title="editorLabel" size="620px" class="business-drawer editor-drawer" :close-on-click-modal="false" >
< el -form :model = "form" label -position = " top " class = "drawer-form" :disabled = "saving" >
< div class = "form-section" > < h3 > 仓库标识 < / h3 > < div class = "form-grid two-cols" > < el-form-item label = "仓库编码" required : class = "{ 'has-field-error': formErrors.code }" > < el-input v-model = "form.code" maxlength="50" placeholder="例如 CN-NB-01" :aria-invalid="Boolean(formErrors.code)" :aria-describedby="formErrors.code ? 'warehouse-code-error' : undefined" @input="clearFieldError('code')" @blur="validateRequiredField('code')" /><p v-if="formErrors.code" id="warehouse-code-error" class="field-error" role="alert">{{ formErrors.code }}</p></el-form-item><el-form-item label="仓库名称" required :class="{ 'has-field-error': formErrors.name }"><el-input v-model="form.name" maxlength="120" placeholder="请输入客户下单时显示的名称" :aria-invalid="Boolean(formErrors.name)" :aria-describedby="formErrors.name ? 'warehouse-name-error' : undefined" @input="clearFieldError('name')" @blur="validateRequiredField('name')" /><p v-if="formErrors.name" id="warehouse-name-error" class="field-error" role="alert">{{ formErrors.name }}</p></el-form-item><el-form-item label="状态"><el-radio-group v-model="form.status"><el-radio-button value="ACTIVE">启用</el-radio-button><el-radio-button value="INACTIVE" > 停用 < / el -radio -button > < / el-radio-group > < / el-form-item > < / div > < / div >
< div class = "form-section" > < h3 > 仓库标识 < / h3 > < div class = "form-grid two-cols" > < el-form-item label = "仓库编码" required : class = "{ 'has-field-error': formErrors.code }" > < el-input v-model = "form.code" maxlength="50" placeholder="例如 CN-NB-01" :aria-invalid="Boolean(formErrors.code)" :aria-describedby="formErrors.code ? 'warehouse-code-error' : undefined" @input="clearFieldError('code')" @blur="validateRequiredField('code')" /><p v-if="formErrors.code" id="warehouse-code-error" class="field-error" role="alert">{{ formErrors.code }}</p></el-form-item><el-form-item label="仓库名称" required :class="{ 'has-field-error': formErrors.name }"><el-input v-model="form.name" maxlength="120" placeholder="请输入客户下单时显示的名称" :aria-invalid="Boolean(formErrors.name)" :aria-describedby="formErrors.name ? 'warehouse-name-error' : undefined" @input="clearFieldError('name')" @blur="validateRequiredField('name')" /><p v-if="formErrors.name" id="warehouse-name-error" class="field-error" role="alert">{{ formErrors.name }}</p></el-form-item><el-form-item label="仓库类型" required><el-select v-model="form.warehouseType" placeholder="请选择仓库类型"><el-option v-for="(label, value) in warehouseTypeLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item><el-form-item label="状态"><el-radio-group v-model="form.status"><el-radio-button value="ACTIVE">启用</el-radio-button><el-radio-button value="INACTIVE">停用</el-radio-button></el-radio-group></el-form-item><el-form-item label="国家或地区二字码" required :class="{ 'has-field-error': formErrors.countryCode }"><el-input v-model="form.countryCode" maxlength="2" placeholder="例如 CN、US" :aria-invalid="Boolean(formErrors.countryCode)" :aria-describedby="formErrors.countryCode ? 'warehouse-country-error' : undefined" @input="form.countryCode = String($event).toUpperCase(); clearFieldError('countryCode')" @blur="validateRequiredField('countryCode')" /><p v-if="formErrors.countryCode" id="warehouse-country-error" class="field-error" role="alert">{{ formErrors.countryCode }}</p></el-form-item><el-form-item label="IANA 时区" required :class="{ 'has-field-error': formErrors.timezone }"><el-input v-model="form.timezone" maxlength="64" placeholder="例如 Asia/Shanghai" :aria-invalid="Boolean(formErrors.timezone)" :aria-describedby="formErrors.timezone ? 'warehouse-timezone-error' : undefined" @input="clearFieldError('timezone')" @blur="validateRequiredField('timezone')" /><p v-if="formErrors.timezone" id="warehouse-timezone-error" class="field-error" role="alert" > {{ formErrors.timezone }} < / p > < / el -form -item > < / div > < / div >
< div class = "form-section" > < h3 > 地址与收货 < / h3 > < div class = "form-grid two-cols" > < el-form-item label = "省份" > < el-input v-model = "form.province" maxlength="100" placeholder="例如 广东省" /></el-form-item><el-form-item label="城市" required :class="{ 'has-field-error': formErrors.city }"><el-input v-model="form.city" maxlength="100" placeholder="例如 深圳市" :aria-invalid="Boolean(formErrors.city)" :aria-describedby="formErrors.city ? 'warehouse-city-error' : undefined" @input="clearFieldError('city')" @blur="validateRequiredField('city')" /><p v-if="formErrors.city" id="warehouse-city-error" class="field-error" role="alert">{{ formErrors.city }}</p></el-form-item><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="详细地址" required :class="{ 'has-field-error': formErrors.address }"><el-input v-model="form.address" maxlength="255" type="textarea" :rows="3" placeholder="请输入街道、园区、楼栋及库位信息" :aria-invalid="Boolean(formErrors.address)" :aria-describedby="formErrors.address ? 'warehouse-address-error' : undefined" @input="clearFieldError('address')" @blur="validateRequiredField('address')" /><p v-if="formErrors.address" id="warehouse-address-error" class="field-error" role="alert" > {{ formErrors.address }} < / p > < / el -form -item > < / div >
< div class = "form-section" > < h3 > 备注 < / h3 > < el-form-item label = "客户可理解的收货说明" > < el-input v-model = "form.remark" maxlength="500" type="textarea" :rows="3" show-word-limit placeholder="例如 预约要求、进仓指引或其他注意事项" / > < / el-form-item > < / div >
< / el-form >