2026-07-30 17:59:43 +08:00
< script setup lang = "ts" >
2026-08-04 09:05:19 +08:00
import { computed , reactive , ref , watch } from 'vue'
2026-07-30 17:59:43 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-01 12:17:33 +08:00
import { MapPin , Pencil , Plus , Power , Warehouse as WarehouseIcon } from '@lucide/vue'
2026-08-03 18:49:18 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
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 ,
createPlatformWarehouse ,
getPlatformWarehouse ,
listPlatformWarehouses ,
updatePlatformWarehouse ,
type PlatformWarehouseDetailResource ,
type PlatformWarehouseResource ,
type SavePlatformWarehouseRequest ,
2026-08-03 18:49:18 +08:00
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-04 09:05:19 +08:00
import { PlatformTenantSelector } from '@/modules/iam'
2026-08-03 18:49:18 +08:00
import type { Warehouse } from '@/modules/warehouses/types'
2026-07-30 17:59:43 +08:00
const access = useAccessPreviewStore ()
const editorOpen = ref ( false )
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-01 12:17:33 +08:00
const pageSize = ref ( 10 )
2026-08-04 09:05:19 +08:00
const selectedTenantId = ref ( '' )
const loadedTenantId = ref < string | null >( null )
2026-07-31 13:43:16 +08:00
const loadError = ref < ApiErrorDetails | null >( null )
const detailWarningCount = ref ( 0 )
const statusLoadingIds = ref ( new Set < string >())
2026-08-01 12:17:33 +08:00
type WarehouseFilters = QueryModel & { keyword : string ; status : Warehouse [ 'status' ] | '' }
const filters = ref < WarehouseFilters >({ keyword : '' , status : '' })
const appliedFilters = ref < WarehouseFilters >({ ... filters . value })
const queryFields : QueryField [] = [
{ key : 'keyword' , type : 'input' , placeholder : '仓库名称、编码、城市或地址' },
{
key : 'status' , type : 'select' , placeholder : '全部状态' , options : [
{ label : '启用' , value : 'ACTIVE' },
{ label : '停用' , value : 'INACTIVE' },
],
},
]
2026-07-31 13:43:16 +08:00
let loadSequence = 0
interface WarehouseRow extends Warehouse {
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' },
{ key : 'warehouse' , label : '仓库' , minWidth : 200 , slotName : 'warehouse' },
{ key : 'city' , label : '所在城市' , minWidth : 130 , slotName : 'city' },
{ key : 'address' , label : '收货地址' , minWidth : 260 , slotName : 'address' },
{ key : 'contact' , label : '联系人' , minWidth : 150 , slotName : 'contact' },
{ prop : 'businessHours' , label : '收货时间' , minWidth : 190 , formatter : ( row : WarehouseRow ) => row . businessHours || '-' },
{ key : 'status' , label : '状态' , width : 100 , slotName : 'status' },
]
2026-07-30 17:59:43 +08:00
const emptyForm = () : Omit < Warehouse , ' updatedAt ' > => ({
id : '' , code : '' , name : '' , province : '' , city : '' , address : '' , contactName : '' , contactPhone : '' ,
businessHours : '周一至周六 08:30-18:00' , status : 'ACTIVE' , remark : '' ,
})
const form = reactive ( emptyForm ())
2026-08-01 12:17:33 +08:00
type WarehouseRequiredField = 'code' | 'name' | 'city' | 'address'
const requiredFieldMessages : Record < WarehouseRequiredField , string > = {
code : '请输入仓库编码' ,
name : '请输入仓库名称' ,
city : '请输入城市' ,
address : '请输入详细地址' ,
}
const formErrors = reactive < Record < WarehouseRequiredField , string > > ({ code : '' , name : '' , city : '' , address : '' })
2026-07-30 17:59:43 +08:00
const editorLabel = computed (() => form . id ? '编辑收货仓' : '新增收货仓' )
2026-07-31 15:34:35 +08:00
const editorPermission = computed (() => form . id ? 'platform:warehouse:edit' : 'platform:warehouse:create' )
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 09:05:19 +08:00
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 )
2026-07-31 13:43:16 +08:00
const filteredRows = computed (() => rows . value . filter (( warehouse ) => {
2026-08-01 12:17:33 +08:00
const keyword = appliedFilters . value . keyword . trim (). toLowerCase ()
2026-07-30 17:59:43 +08:00
const matchesKeyword = ! keyword || [ warehouse . code , warehouse . name , warehouse . province , warehouse . city , warehouse . address ]
. some (( value ) => value . toLowerCase (). includes ( keyword ))
2026-08-01 12:17:33 +08:00
return matchesKeyword && ( ! appliedFilters . value . status || warehouse . status === appliedFilters . value . status )
2026-07-30 17:59:43 +08:00
}))
2026-08-01 12:17:33 +08:00
const pagedRows = computed (() => filteredRows . value . slice (( page . value - 1 ) * pageSize . value , page . value * pageSize . 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 )
const retained = loadedTenantId . value ? `当前仍显示租户 ${ loadedTenantId . value } 最近一次成功加载的数据。` : ''
return [ loadError . value . message , references . join ( ' · ' ), retained ]. filter ( Boolean ). join ( ' ' )
})
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-07-31 13:43:16 +08:00
function toRow ( warehouse : PlatformWarehouseResource , detail : PlatformWarehouseDetailResource | null ) : WarehouseRow {
const address = detail ? . addresses [ 0 ]
const receiving = detail ? . capabilities . find (( item ) => item . capabilityCode === 'RECEIVING' )
return {
id : String ( warehouse . id ),
code : warehouse . warehouseCode ,
name : warehouse . warehouseName ,
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
address : [ address ? . district , address ? . addressLine1 , address ? . addressLine2 ]. filter ( Boolean ). join ( ' ' ),
contactName : warehouse . contactName || '' ,
contactPhone : warehouse . contactPhone || '' ,
businessHours : receiving ? . description || ( receiving ? . cutoffTime ? `每日截单 ${ receiving . cutoffTime } ` : '' ),
status : warehouse . status ,
remark : warehouse . remark || '' ,
updatedAt : warehouse . updatedAt ? . replace ( 'T' , ' ' ). slice ( 0 , 16 ) || '' ,
detail ,
}
}
2026-08-01 12:17:33 +08:00
function queryWarehouses ( model : QueryModel ) {
appliedFilters . value = {
keyword : String ( model . keyword || '' ),
status : ( model . status || '' ) as Warehouse [ 'status' ] | '' ,
}
page . value = 1
void loadWarehouses ()
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 ] = ''
}
function validateRequiredField ( field : WarehouseRequiredField ) {
formErrors [ field ] = form [ field ]. trim () ? '' : requiredFieldMessages [ field ]
return ! formErrors [ field ]
}
function validateWarehouseForm () {
return ( Object . keys ( requiredFieldMessages ) as WarehouseRequiredField [])
. map ( validateRequiredField )
. every ( Boolean )
}
2026-08-04 09:05:19 +08:00
function apiTenantId ( value : string ) {
const numeric = Number ( value )
return Number . isSafeInteger ( numeric ) ? numeric : value
}
async function loadWarehouses ( tenantId = selectedTenantId . value ) {
if ( ! tenantId ) return
2026-07-31 13:43:16 +08:00
const sequence = ++ loadSequence
loading . value = true
loadError . value = null
try {
2026-08-04 09:05:19 +08:00
const requestTenantId = apiTenantId ( tenantId )
const result = await listPlatformWarehouses ( requestTenantId , 1 , 200 )
2026-07-31 13:43:16 +08:00
const details = await Promise . allSettled (
2026-08-04 09:05:19 +08:00
result . records . map (( warehouse ) => getPlatformWarehouse ( requestTenantId , warehouse . id )),
2026-07-31 13:43:16 +08:00
)
if ( sequence !== loadSequence ) return
rows . value = result . records . map (( warehouse , index ) => {
const detailResult = details [ index ]
return toRow ( warehouse , detailResult ? . status === 'fulfilled' ? detailResult . value : null )
})
detailWarningCount . value = details . filter (( item ) => item . status === 'rejected' ). length
2026-08-04 09:05:19 +08:00
loadedTenantId . value = tenantId
2026-07-31 13:43:16 +08:00
page . value = 1
} catch ( error ) {
if ( sequence !== loadSequence ) return
loadError . value = getApiErrorDetails ( error , '仓库数据加载失败,请稍后重试' )
} finally {
if ( sequence === loadSequence ) loading . value = false
}
}
function requireCurrentTenant () {
2026-08-01 12:17:33 +08:00
if ( ! warehouseReady . value ) {
ElMessage . warning ( '仓库数据尚未加载完成,请稍后重试' )
2026-07-31 13:43:16 +08:00
return null
}
2026-08-04 09:05:19 +08:00
return apiTenantId ( selectedTenantId . value )
2026-07-31 13:43:16 +08:00
}
2026-07-30 17:59:43 +08:00
function openCreate () {
2026-07-31 13:43:16 +08:00
if ( ! requireCurrentTenant ()) return
2026-07-30 17:59:43 +08:00
Object . assign ( form , emptyForm ())
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
const tenantId = requireCurrentTenant ()
if ( ! tenantId ) return null
try {
const detail = await getPlatformWarehouse ( tenantId , warehouse . id )
Object . assign ( warehouse , toRow ( detail . warehouse , detail ))
detailWarningCount . value = rows . value . filter (( item ) => ! item . detail ). length
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
const tenantAtStart = selectedTenantId . value
editLoading . value = true
try {
const detail = await ensureDetail ( warehouse )
if ( ! detail || selectedTenantId . value !== tenantAtStart || loadedTenantId . value !== tenantAtStart ) return
const address = detail . addresses [ 0 ]
Object . assign ( form , warehouse , {
province : address ? . stateProvince || '' ,
city : address ? . city || '' ,
address : address ? . addressLine1 || '' ,
})
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-04 09:05:19 +08:00
function buildSaveRequest ( tenantId : number | string ) : 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 ]
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 ,
}))
if ( ! capabilities . some (( item ) => item . capabilityCode === 'RECEIVING' )) {
capabilities . push ({
capabilityCode : 'RECEIVING' ,
capabilityName : '收货服务' ,
enabled : true ,
cutoffTime : null ,
description : nullable ( form . businessHours ),
})
}
return {
tenantId ,
warehouseCode : form . code . trim (). toUpperCase (),
warehouseName : form . name . trim (),
warehouseType : warehouse ? . warehouseType || 'DOMESTIC' ,
countryCode : warehouse ? . countryCode || 'CN' ,
timezone : warehouse ? . timezone || 'Asia/Shanghai' ,
contactName : nullable ( form . contactName ),
contactPhone : nullable ( form . contactPhone ),
contactEmail : warehouse ? . contactEmail || null ,
status : form . status ,
remark : nullable ( form . remark ),
address : {
addressType : currentAddress ? . addressType || 'OPERATING' ,
countryCode : currentAddress ? . countryCode || warehouse ? . countryCode || 'CN' ,
stateProvince : nullable ( form . province ),
city : form . city . trim (),
district : currentAddress ? . district || null ,
postalCode : currentAddress ? . postalCode || null ,
addressLine1 : form . address . trim (),
addressLine2 : currentAddress ? . addressLine2 || null ,
latitude : currentAddress ? . latitude || null ,
longitude : currentAddress ? . longitude || null ,
},
capabilities ,
}
}
async function saveWarehouse () {
const tenantId = requireCurrentTenant ()
if ( ! tenantId ) return
2026-08-01 12:17:33 +08:00
if ( ! validateWarehouseForm ()) return
2026-07-31 13:43:16 +08:00
saving . value = true
2026-07-30 17:59:43 +08:00
try {
2026-07-31 13:43:16 +08:00
const payload = buildSaveRequest ( tenantId )
if ( form . id ) await updatePlatformWarehouse ( tenantId , form . id , payload )
else await createPlatformWarehouse ( payload )
const successMessage = form . id ? '仓库资料已更新' : '收货仓已创建'
2026-07-30 17:59:43 +08:00
editorOpen . value = false
2026-07-31 13:43:16 +08:00
ElMessage . success ( successMessage )
2026-08-01 12:17:33 +08:00
await loadWarehouses ()
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 ) {
const tenantId = requireCurrentTenant ()
if ( ! tenantId ) 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'
? `停用“ ${ warehouse . name } ”后,客户新建头程订单时将无法选择该仓库。`
: `确认重新启用“ ${ warehouse . name } ”供客户下单选择吗?` ,
` ${ targetText } 收货仓` ,
{ 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 )
await changePlatformWarehouseStatus ( tenantId , warehouse . id , target )
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-04 09:05:19 +08:00
watch ( selectedTenantId , ( tenantId ) => {
editorOpen . value = false
rows . value = []
loadedTenantId . value = null
detailWarningCount . value = 0
page . value = 1
void loadWarehouses ( tenantId )
2026-07-31 13:43:16 +08:00
})
2026-07-30 17:59:43 +08:00
</ script >
< template >
< div class = "business-page" >
2026-08-04 09:05:19 +08:00
< PlatformTenantSelector v-model = "selectedTenantId" :disabled="tenantSwitchDisabled" />
2026-07-31 13:43:16 +08:00
< el-alert v-if = "loadError" type="error" :closable="false" show-icon class="state-alert" >
< template # title > 仓库数据加载失败 </ template >
< p >{{ loadErrorDescription }}</ p >
< el-button size = "small" @click ="loadWarehouses()" > 重新查询 </ el -button >
</ el-alert >
< el-alert v-else-if = "detailWarningCount" type="warning" :closable="false" show-icon class="state-alert" >
< template # title > 部分仓库详情暂未加载 </ template >
< p >{{ detailWarningCount }} 个仓库缺少地址或服务能力详情 ; 编辑时会再次尝试加载 , 不会用模拟数据补齐 。 </ p >
</ el-alert >
2026-08-03 11:45:45 +08:00
< QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="仓库筛选" :fields="queryFields" @search="queryWarehouses" />
< section class = "surface-panel table-panel" aria -live =" polite " >
< div class = "panel-heading" >
< div >
< h2 > 平台收货仓 </ h2 >
2026-08-04 09:05:19 +08:00
< p > 租户 {{ selectedTenantId || '—' }} · 共 {{ filteredRows . length }} 个收货仓 </ p >
2026-08-03 11:45:45 +08:00
</ div >
2026-07-31 13:43:16 +08:00
< el-button
2026-07-31 15:34:35 +08:00
v-if = "access.can('platform:warehouse:create')"
2026-08-03 11:45:45 +08:00
type = "primary"
2026-08-01 12:17:33 +08:00
:disabled = "!warehouseReady"
2026-07-31 13:43:16 +08:00
@click ="openCreate"
>< Plus :size = "16" /> 新增仓库 </ el-button >
2026-08-03 11:45:45 +08:00
</ div >
2026-08-01 12:17:33 +08:00
< DataTable
v -model:page = "page"
v -model:page-size = "pageSize"
:data = "pagedRows"
:columns = "warehouseColumns"
:loading = "loading"
:total = "filteredRows.length"
: page -sizes ="[ 10 , 20 , 50 ]"
row -key =" id "
aria -label =" 平台收货仓列表 "
: empty -text =" loadedTenantId ? ' 暂无符合筛选条件的仓库 ' : ' 仓库数据暂不可用 , 请重新加载 '"
>
< template # actions = "{ row }" >
2026-07-31 15:34:35 +08:00
< div v-if = "access.can('platform:warehouse:edit') || access.can('platform:warehouse:status')" class="table-actions" >
2026-08-01 12:17:33 +08:00
< el-button v-if = "access.can('platform:warehouse:edit')" link type="primary" :disabled="!warehouseReady" @click="openEdit(row as WarehouseRow)"><Pencil :size="15" /> 编辑 </ el-button >
2026-07-31 13:43:16 +08:00
< el-button
2026-07-31 15:34:35 +08:00
v-if = "access.can('platform:warehouse:status')"
2026-07-31 13:43:16 +08:00
link
: type = "(row as WarehouseRow).status === 'ACTIVE' ? 'danger' : 'success'"
2026-08-01 12:17:33 +08:00
:disabled = "!warehouseReady"
2026-07-31 13:43:16 +08:00
: 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 >
< 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 >
</ DataTable >
2026-07-30 17:59:43 +08:00
</ section >
< el-drawer v-model = "editorOpen" :title="editorLabel" size="620px" class="business-drawer editor-drawer" :close-on-click-modal="false" >
2026-08-01 12:17:33 +08:00
< 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 = "省份" >< 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 >
2026-07-30 17:59:43 +08:00
</ el-form >
2026-07-31 13:43:16 +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" > 保存仓库 </ el -button ></ div ></ template >
2026-07-30 17:59:43 +08:00
</ el-drawer >
</ div >
</ template >
< style scoped >
2026-07-31 13:43:16 +08:00
. state - alert p { margin : 6 px 0 10 px ; line - height : 1.55 ; }
. warehouse - table - state { min - height : 180 px ; }
2026-07-30 17:59:43 +08:00
. warehouse - cell { display : flex ; align - items : center ; gap : 10 px ; }
. warehouse - cell > span { display : grid ; width : 34 px ; height : 34 px ; flex : 0 0 34 px ; place - items : center ; border - radius : 7 px ; background : # eef3fa ; color : var ( -- cc - primary ); }
. 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-07-30 17:59:43 +08:00
. warehouse - cell small , . warehouse - subline { 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-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 ; }
: 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-07-31 13:43:16 +08:00
2026-08-03 11:45:45 +08:00
@ media ( max - width : 720 px ) {
. panel - heading . el - button { width : 100 % ; }
}
2026-07-30 17:59:43 +08:00
</ style >