2026-08-04 21:01:39 +08:00
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox , type FormInstance , type FormRules } from 'element-plus'
import { Boxes , Eye , Pencil , Plus , Trash2 , Warehouse as WarehouseIcon } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
2026-08-06 14:55:17 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseResource } from '@/modules/warehouses'
2026-08-04 21:01:39 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import {
createPlatformPallet ,
deletePlatformPallet ,
getPlatformPallet ,
listPlatformPallets ,
updatePlatformPallet ,
type PlatformPalletResource ,
type SavePlatformPalletRequest ,
} from '../api/platformPallets'
const access = useAccessPreviewStore ()
const warehouses = ref < PlatformWarehouseResource [] >([])
const warehouseLoading = ref ( false )
const warehouseError = ref < ApiErrorDetails | null >( null )
const rows = ref < PlatformPalletResource [] >([])
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
const editorOpen = ref ( false )
const detailOpen = ref ( false )
const detail = ref < PlatformPalletResource | null >( null )
const detailLoading = ref ( false )
const saving = ref ( false )
const deletingIds = ref ( new Set < string >())
const formRef = ref < FormInstance >()
let warehouseSequence = 0
let palletSequence = 0
2026-08-05 15:54:59 +08:00
type PalletFilters = QueryModel & { warehouseId : string ; palletCode : string ; palletType : string }
type AppliedPalletFilters = Pick < PalletFilters , ' warehouseId ' | ' palletCode ' | ' palletType ' >
const filters = ref < PalletFilters >({ warehouseId : '' , palletCode : '' , palletType : '' })
const appliedFilters = ref < AppliedPalletFilters >({ warehouseId : '' , palletCode : '' , palletType : '' })
2026-08-04 21:01:39 +08:00
const palletTypeOptions = [ '木托盘' , '塑料托盘' , '金属托盘' , '纸托盘' , '免熏蒸托盘' ]
const queryFields = computed < QueryField [] >(() => [
{
key : 'warehouseId' ,
type : 'select' ,
2026-08-06 19:03:13 +08:00
label : '所属仓库' ,
2026-08-05 15:54:59 +08:00
width : 300 ,
2026-08-04 21:01:39 +08:00
placeholder : warehouseLoading . value ? '正在加载所属仓库' : warehouseError . value ? '所属仓库加载失败' : '全部仓库' ,
2026-08-06 15:32:02 +08:00
options : domesticWarehouses . value . map (( warehouse ) => ({
2026-08-04 21:01:39 +08:00
label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) `,
value : String ( warehouse . id ),
})),
props : {
filterable : true ,
2026-08-05 15:54:59 +08:00
disabled : contextLocked . value || warehouseLoading . value || Boolean ( warehouseError . value ),
2026-08-04 21:01:39 +08:00
'aria-label' : '所属仓库' ,
},
},
2026-08-06 19:03:13 +08:00
{ key : 'palletCode' , type : 'input' , width : 240 , label : '托盘编码' , placeholder : '托盘编码(模糊查询)' , props : { maxlength : 50 , 'aria-label' : '托盘编码' } },
2026-08-04 21:01:39 +08:00
{
key : 'palletType' ,
type : 'select' ,
2026-08-06 19:03:13 +08:00
label : '托盘类型' ,
2026-08-04 21:01:39 +08:00
width : 180 ,
placeholder : '全部托盘类型' ,
options : palletTypeOptions . map (( value ) => ({ label : value , value })),
props : { filterable : true , allowCreate : true , defaultFirstOption : true , 'aria-label' : '托盘类型' },
},
])
2026-08-06 15:32:02 +08:00
const emptyForm = () => ({ id : '' , warehouseId : '' , palletCode : '' , palletType : '' , capacityCbm : null as number | null , weightKg : null as number | null , color : '' , remark : '' })
2026-08-04 21:01:39 +08:00
const form = reactive ( emptyForm ())
const rules : FormRules < typeof form > = {
2026-08-06 14:55:17 +08:00
warehouseId : [{ required : true , message : '请选择国内仓' , trigger : 'change' }],
2026-08-04 21:01:39 +08:00
palletType : [
{ required : true , message : '请输入托盘类型' , trigger : 'blur' },
{ max : 32 , message : '托盘类型不能超过 32 个字符' , trigger : 'blur' },
],
capacityCbm : [{
validator : ( _rule , value , callback ) => {
if ( typeof value !== 'number' || ! Number . isFinite ( value ) || value <= 0 ) callback ( new Error ( '请输入大于 0 的托盘容积' ))
else callback ()
},
trigger : 'change' ,
}],
2026-08-06 14:55:17 +08:00
weightKg : [{
validator : ( _rule , value , callback ) => {
if ( typeof value !== 'number' || ! Number . isFinite ( value ) || value <= 0 ) callback ( new Error ( '请输入大于 0 的托盘重量' ))
else callback ()
},
trigger : 'change' ,
}],
color : [
{ required : true , message : '请输入托盘颜色' , trigger : 'blur' },
{ max : 32 , message : '托盘颜色不能超过 32 个字符' , trigger : 'blur' },
],
2026-08-04 21:01:39 +08:00
remark : [{ max : 500 , message : '备注不能超过 500 个字符' , trigger : 'blur' }],
}
const editorWarehouse = computed (() => warehouses . value . find (( item ) => String ( item . id ) === form . warehouseId ) || null )
2026-08-06 14:55:17 +08:00
const domesticWarehouses = computed (() => warehouses . value . filter (( warehouse ) => (
warehouse . warehouseType === 'DOMESTIC' && warehouse . status === 'ACTIVE'
)))
2026-08-04 21:01:39 +08:00
const editorTitle = computed (() => form . id ? '编辑托盘' : '新增托盘' )
const editorPermission = computed (() => form . id ? 'platform:pallet:edit' : 'platform:pallet:create' )
const contextLocked = computed (() => editorOpen . value || detailOpen . value || saving . value || deletingIds . value . size > 0 )
const filtered = computed (() => Object . values ( appliedFilters . value ). some (( value ) => String ( value || '' ). trim ()))
const { captureBaseline } = useUnsavedWorkRegistration ({
key : 'pallet-editor' ,
label : editorTitle ,
isOpen : editorOpen ,
model : form ,
requiredPermission : editorPermission ,
})
const columns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , minWidth : 224 , fixed : 'left' , slotName : 'actions' },
{ key : 'pallet' , label : '托盘编码' , minWidth : 190 , slotName : 'pallet' },
2026-08-06 19:03:13 +08:00
{ key : 'palletType' , prop : 'palletType' , label : '托盘类型' , minWidth : 140 },
2026-08-04 21:01:39 +08:00
{ key : 'capacity' , label : '托盘容积(m³)' , minWidth : 160 , slotName : 'capacity' },
2026-08-06 14:55:17 +08:00
{ key : 'weight' , label : '托盘重量(kg) ' , minWidth : 160 , slotName : 'weight' },
2026-08-06 19:03:13 +08:00
{ key : 'color' , prop : 'color' , label : '颜色' , minWidth : 120 },
2026-08-04 21:01:39 +08:00
{ key : 'remark' , label : '备注' , minWidth : 220 , slotName : 'remark' },
]
function apiId ( value : string ) {
const numeric = Number ( value )
return Number . isSafeInteger ( numeric ) ? numeric : value
}
function formatDateTime ( value ?: string | null ) {
return value ? . replace ( 'T' , ' ' ). slice ( 0 , 16 ) || '—'
}
function formatCapacity ( value : number | string ) {
const numeric = Number ( value )
if ( ! Number . isFinite ( numeric )) return String ( value )
return ` ${ numeric . toLocaleString ( 'zh-CN' , { maximumFractionDigits : 6 } )} m³`
}
2026-08-06 14:55:17 +08:00
function formatWeight ( value : number | string | null ) {
if ( value === null ) return '—'
const numeric = Number ( value )
if ( ! Number . isFinite ( numeric )) return String ( value )
return ` ${ numeric . toLocaleString ( 'zh-CN' , { maximumFractionDigits : 3 } )} kg`
}
2026-08-04 21:01:39 +08:00
function warehouseFor ( warehouseId : number | string ) {
return warehouses . value . find (( item ) => String ( item . id ) === String ( warehouseId )) || null
}
2026-08-06 14:55:17 +08:00
function isDomesticWarehouse ( warehouseId : number | string ) {
return warehouseFor ( warehouseId ) ? . warehouseType === 'DOMESTIC'
}
function isActiveDomesticWarehouse ( warehouseId : number | string ) {
return domesticWarehouses . value . some (( warehouse ) => String ( warehouse . id ) === String ( warehouseId ))
}
2026-08-04 21:01:39 +08:00
function errorMessage ( details : ApiErrorDetails ) {
return details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message
}
2026-08-05 15:54:59 +08:00
async function loadWarehouses () {
2026-08-04 21:01:39 +08:00
const sequence = ++ warehouseSequence
warehouses . value = []
warehouseError . value = null
warehouseLoading . value = true
try {
2026-08-06 14:55:17 +08:00
const records = await listAllPlatformWarehouses ()
2026-08-04 21:01:39 +08:00
if ( sequence !== warehouseSequence ) return
2026-08-06 14:55:17 +08:00
warehouses . value = records
2026-08-04 21:01:39 +08:00
} catch ( error ) {
if ( sequence !== warehouseSequence ) return
warehouseError . value = getApiErrorDetails ( error , '仓库列表加载失败,请稍后重试' )
} finally {
if ( sequence === warehouseSequence ) warehouseLoading . value = false
}
}
async function loadPallets () {
const sequence = ++ palletSequence
rows . value = []
total . value = 0
loadError . value = null
loading . value = true
try {
const result = await listPlatformPallets ({
page : page . value ,
size : pageSize . value ,
2026-08-05 15:54:59 +08:00
...( appliedFilters . value . warehouseId ? { warehouseId : apiId ( appliedFilters . value . warehouseId ) } : {}),
2026-08-04 21:01:39 +08:00
...( appliedFilters . value . palletCode ? { palletCode : appliedFilters . value . palletCode } : {}),
...( appliedFilters . value . palletType ? { palletType : appliedFilters . value . palletType } : {}),
})
if ( sequence !== palletSequence ) return
rows . value = result . records
total . value = result . total
} catch ( error ) {
if ( sequence !== palletSequence ) return
loadError . value = getApiErrorDetails ( error , '托盘列表加载失败,请稍后重试' )
} finally {
if ( sequence === palletSequence ) loading . value = false
}
}
function applyFilters ( model : QueryModel ) {
2026-08-05 15:54:59 +08:00
appliedFilters . value = {
warehouseId : String ( model . warehouseId || '' ),
palletCode : String ( model . palletCode || '' ),
palletType : String ( model . palletType || '' ),
}
2026-08-04 21:01:39 +08:00
if ( page . value === 1 ) void loadPallets ()
else page . value = 1
}
function resetFilters () {
2026-08-05 15:54:59 +08:00
appliedFilters . value = { warehouseId : '' , palletCode : '' , palletType : '' }
2026-08-04 21:01:39 +08:00
if ( page . value === 1 ) void loadPallets ()
else page . value = 1
}
function openCreate () {
Object . assign ( form , emptyForm ())
2026-08-06 14:55:17 +08:00
form . warehouseId = isActiveDomesticWarehouse ( filters . value . warehouseId ) ? filters . value . warehouseId : ''
2026-08-04 21:01:39 +08:00
editorOpen . value = true
captureBaseline ()
void formRef . value ? . clearValidate ()
}
function assignForm ( resource : PlatformPalletResource ) {
Object . assign ( form , {
id : String ( resource . id ),
warehouseId : String ( resource . warehouseId ),
palletCode : resource . palletCode ,
palletType : resource . palletType ,
capacityCbm : Number ( resource . capacityCbm ),
2026-08-06 14:55:17 +08:00
weightKg : resource . weightKg === null ? null : Number ( resource . weightKg ),
color : resource . color || '' ,
2026-08-04 21:01:39 +08:00
remark : resource . remark || '' ,
})
editorOpen . value = true
captureBaseline ()
void formRef . value ? . clearValidate ()
}
async function fetchDetail ( row : PlatformPalletResource ) {
2026-08-06 15:32:02 +08:00
return getPlatformPallet ( apiId ( String ( row . warehouseId )), row . id )
2026-08-04 21:01:39 +08:00
}
async function openDetail ( row : PlatformPalletResource ) {
detailLoading . value = true
detailOpen . value = true
detail . value = null
try {
detail . value = await fetchDetail ( row )
} catch ( error ) {
detailOpen . value = false
ElMessage . error ( errorMessage ( getApiErrorDetails ( error , '托盘详情加载失败,请稍后重试' )))
} finally {
detailLoading . value = false
}
}
async function openEdit ( row : PlatformPalletResource ) {
2026-08-06 14:55:17 +08:00
if ( ! isDomesticWarehouse ( row . warehouseId )) {
ElMessage . warning ( '仅支持编辑国内仓托盘' )
return
}
2026-08-04 21:01:39 +08:00
try {
assignForm ( await fetchDetail ( row ))
} catch ( error ) {
ElMessage . error ( errorMessage ( getApiErrorDetails ( error , '托盘详情加载失败,请稍后重试' )))
}
}
async function savePallet () {
2026-08-06 14:55:17 +08:00
if ( ! await formRef . value ? . validate (). catch (() => false ) || form . capacityCbm === null || form . weightKg === null ) return
2026-08-05 15:54:59 +08:00
const warehouse = editorWarehouse . value
2026-08-06 14:55:17 +08:00
const warehouseAllowed = form . id
? warehouse ? . warehouseType === 'DOMESTIC'
: warehouse ? . warehouseType === 'DOMESTIC' && warehouse . status === 'ACTIVE'
if ( ! warehouse || ! warehouseAllowed ) {
ElMessage . error ( form . id ? '托盘仅支持归属国内仓' : '新增托盘仅支持选择启用的国内仓' )
2026-08-05 15:54:59 +08:00
return
}
2026-08-04 21:01:39 +08:00
saving . value = true
const payload : SavePlatformPalletRequest = {
warehouseId : apiId ( form . warehouseId ),
palletType : form . palletType . trim (),
capacityCbm : form . capacityCbm ,
2026-08-06 14:55:17 +08:00
weightKg : form . weightKg ,
2026-08-06 15:32:02 +08:00
color : form . color . trim (),
2026-08-04 21:01:39 +08:00
remark : form . remark . trim () || null ,
}
try {
2026-08-06 15:32:02 +08:00
if ( form . id ) await updatePlatformPallet ( apiId ( form . warehouseId ), form . id , payload )
else await createPlatformPallet ( payload )
2026-08-04 21:01:39 +08:00
ElMessage . success ( form . id ? '托盘已更新' : '托盘已新增' )
editorOpen . value = false
await loadPallets ()
} catch ( error ) {
ElMessage . error ( errorMessage ( getApiErrorDetails ( error , '托盘保存失败,请稍后重试' )))
} finally {
saving . value = false
}
}
async function removePallet ( row : PlatformPalletResource ) {
try {
await ElMessageBox . confirm (
`确定删除托盘“ ${ row . palletCode } ”吗?已预占或已占用的托盘无法删除。` ,
'删除托盘' ,
{ confirmButtonText : '删除' , cancelButtonText : '取消' , type : 'warning' , confirmButtonClass : 'el-button--danger' },
)
} catch {
return
}
const id = String ( row . id )
deletingIds . value = new Set ([... deletingIds . value , id ])
try {
2026-08-06 15:32:02 +08:00
await deletePlatformPallet ( apiId ( String ( row . warehouseId )), row . id )
2026-08-04 21:01:39 +08:00
ElMessage . success ( '托盘已删除' )
if ( rows . value . length === 1 && page . value > 1 ) page . value -= 1
else await loadPallets ()
} catch ( error ) {
ElMessage . error ( errorMessage ( getApiErrorDetails ( error , '托盘删除失败,请稍后重试' )))
} finally {
const next = new Set ( deletingIds . value )
next . delete ( id )
deletingIds . value = next
}
}
watch ([ page , pageSize ], ([ nextPage , nextSize ], [ previousPage , previousSize ]) => {
if ( nextPage === previousPage && nextSize === previousSize ) return
void loadPallets ()
})
watch (() => access . can ( editorPermission . value ), ( canWrite ) => {
if ( ! canWrite && editorOpen . value ) editorOpen . value = false
}, { flush : 'sync' })
onMounted (() => {
2026-08-05 15:54:59 +08:00
void loadWarehouses ()
2026-08-04 21:01:39 +08:00
void loadPallets ()
})
</ script >
< template >
< section class = "pallet-page" >
< QueryToolbar
v-model = "filters"
class = "surface-panel pallet-filter-toolbar"
ariaLabel = "托盘筛选"
:fields = "queryFields"
2026-08-06 19:03:13 +08:00
preference -key =" pallets -query "
2026-08-04 21:01:39 +08:00
:search-on-reset = "false"
@search ="applyFilters"
@reset ="resetFilters"
/>
< LoadErrorState
v-if = "warehouseError"
title = "仓库列表暂时无法加载"
:message = "warehouseError.message"
:loading = "warehouseLoading"
2026-08-05 15:54:59 +08:00
@retry ="loadWarehouses"
2026-08-04 21:01:39 +08:00
/>
< LoadErrorState
v-if = "loadError"
title = "托盘列表暂时无法加载"
:message = "loadError.message"
:loading = "loading"
@retry ="loadPallets"
/>
< section class = "table-panel surface-panel" aria -label =" 托盘列表区域 " >
< div class = "panel-heading" >
2026-08-05 15:54:59 +08:00
< div >< h2 > 托盘列表 </ h2 >< p > 共 {{ total }} 个托盘 </ p ></ div >
< el-button v-if = "access.can('platform:pallet:create')" type="primary" @click="openCreate"><Plus :size="16" /> 新增托盘 </ el-button >
2026-08-04 21:01:39 +08:00
</ div >
< DataTable
v -model:page = "page"
v -model:page-size = "pageSize"
:data = "rows"
:columns = "columns"
2026-08-06 19:03:13 +08:00
preference -key =" pallets -table "
2026-08-04 21:01:39 +08:00
: loading = "loading || warehouseLoading"
:total = "total"
row -key =" id "
2026-08-05 15:54:59 +08:00
: empty -text =" filtered ? ' 暂无符合筛选条件的托盘 ' : ' 暂无托盘数据 '"
2026-08-04 21:01:39 +08:00
aria -label =" 托盘列表 "
>
< template # actions = "{ row }" >
< div class = "table-actions pallet-row-actions" >
< el-button link type = "primary" title = "查看托盘" @click ="openDetail(row as PlatformPalletResource)" >< Eye :size = "15" /> 查看 </ el-button >
2026-08-06 14:55:17 +08:00
< el-button v-if = "access.can('platform:pallet:edit') && isDomesticWarehouse((row as PlatformPalletResource).warehouseId)" link type="primary" title="编辑托盘" @click="openEdit(row as PlatformPalletResource)"><Pencil :size="15" /> 编辑 </ el-button >
2026-08-04 21:01:39 +08:00
< el-button
v-if = "access.can('platform:pallet:delete')"
link
type = "danger"
title = "删除托盘"
: loading = "deletingIds.has(String((row as PlatformPalletResource).id))"
@click ="removePallet(row as PlatformPalletResource)"
>< Trash2 :size = "15" /> 删除 </ el-button >
</ div >
</ template >
< template # pallet = "{ row }" >
< div class = "pallet-cell" >< span >< Boxes :size = "18" /></ span >< strong >{{ ( row as PlatformPalletResource ). palletCode }}</ strong ></ div >
</ template >
< template # capacity = "{ row }" >< strong >{{ formatCapacity (( row as PlatformPalletResource ). capacityCbm ) }}</ strong ></ template >
2026-08-06 14:55:17 +08:00
< template # weight = "{ row }" >< strong >{{ formatWeight (( row as PlatformPalletResource ). weightKg ) }}</ strong ></ template >
2026-08-04 21:01:39 +08:00
< template # remark = "{ row }" >< span class = "remark-text" >{{ ( row as PlatformPalletResource ). remark || '—' }}</ span ></ template >
</ DataTable >
</ section >
< el-drawer v-model = "detailOpen" title="托盘详情" size="520px" class="business-drawer" >
< div v-loading = "detailLoading" class="detail-body" >
< template v-if = "detail" >
< div class = "detail-hero" >< span >< Boxes :size = "22" /></ span >< div >< h2 >{{ detail . palletCode }}</ h2 >< p >{{ detail . palletType }}</ p ></ div ></ div >
< h3 class = "detail-section-title" > 归属信息 </ h3 >
< el-descriptions :column = "1" border >
2026-08-06 13:56:04 +08:00
< el-descriptions-item label = "资源作用域" > 平台公共资源 </ el-descriptions-item >
2026-08-04 21:01:39 +08:00
< el-descriptions-item label = "所属仓库" >{{ warehouseFor ( detail . warehouseId ) ? . warehouseName || '—' }} ( {{ warehouseFor ( detail . warehouseId ) ? . warehouseCode || '—' }} ) </ el-descriptions-item >
</ el-descriptions >
< h3 class = "detail-section-title" > 托盘规格 </ h3 >
< el-descriptions :column = "1" border >
< el-descriptions-item label = "托盘编码" >{{ detail . palletCode }}</ el-descriptions-item >
< el-descriptions-item label = "托盘类型" >{{ detail . palletType }}</ el-descriptions-item >
< el-descriptions-item label = "托盘容积" >{{ formatCapacity ( detail . capacityCbm ) }}</ el-descriptions-item >
2026-08-06 14:55:17 +08:00
< el-descriptions-item label = "托盘重量" >{{ formatWeight ( detail . weightKg ) }}</ el-descriptions-item >
< el-descriptions-item label = "颜色" >{{ detail . color || '—' }}</ el-descriptions-item >
2026-08-04 21:01:39 +08:00
< el-descriptions-item label = "备注" >{{ detail . remark || '—' }}</ el-descriptions-item >
</ el-descriptions >
< h3 class = "detail-section-title" > 记录信息 </ h3 >
< el-descriptions :column = "1" border >
< el-descriptions-item label = "创建时间" >{{ formatDateTime ( detail . createdAt ) }}</ el-descriptions-item >
< el-descriptions-item label = "更新时间" >{{ formatDateTime ( detail . updatedAt ) }}</ el-descriptions-item >
</ el-descriptions >
</ template >
</ div >
< template # footer >< el-button @click ="detailOpen = false" > 关闭 </ el -button ></ template >
</ el-drawer >
2026-08-05 17:10:10 +08:00
< el-drawer v-model = "editorOpen" size="560px" class="business-drawer pallet-editor-drawer" :close-on-click-modal="false" >
< template # header = "{ titleId, titleClass }" >
< div class = "pallet-editor-header" >
< span aria -hidden =" true " >< Boxes :size = "20" /></ span >
< div >
< h2 :id = "titleId" :class = "titleClass" >{{ editorTitle }}</ h2 >
< p >{{ form . id ? '调整托盘规格与现场识别信息' : '录入归属仓库与基础规格,创建后即可用于仓储作业' }}</ p >
</ div >
</ div >
</ template >
2026-08-04 21:01:39 +08:00
< el-form ref = "formRef" :model = "form" :rules = "rules" label -position =" top " class = "editor-form" >
2026-08-05 17:10:10 +08:00
< section class = "editor-section" aria -labelledby =" pallet -ownership-heading " >
< div class = "editor-section-heading" >
< WarehouseIcon :size = "18" aria -hidden =" true " />
< div >< h3 id = "pallet-ownership-heading" > 归属仓库 </ h3 >< p > 托盘创建后固定归属于所选仓库 , 不支持跨仓移动 。 </ p ></ div >
</ div >
< div v-if = "form.id" class="editor-context" >
2026-08-06 13:56:04 +08:00
< div >< strong >{{ editorWarehouse ? . warehouseName || '当前仓库' }}</ strong >< span >{{ editorWarehouse ? . warehouseCode || form . warehouseId }} · 平台公共资源 </ span ></ div >
2026-08-05 17:10:10 +08:00
</ div >
< el-form-item v-else label = "所属仓库" prop = "warehouseId" required >
2026-08-06 14:55:17 +08:00
< el-select v-model = "form.warehouseId" filterable placeholder="请选择国内仓" :loading="warehouseLoading" >
2026-08-05 17:10:10 +08:00
< el-option
2026-08-06 14:55:17 +08:00
v-for = "warehouse in domesticWarehouses"
2026-08-05 17:10:10 +08:00
:key = "String(warehouse.id)"
:label = "`${warehouse.warehouseName}( ${warehouse.warehouseCode}) `"
:value = "String(warehouse.id)"
/>
</ el-select >
</ el-form-item >
</ section >
< section class = "editor-section" aria -labelledby =" pallet -spec-heading " >
< div class = "editor-section-heading" >
< Boxes :size = "18" aria -hidden =" true " />
2026-08-06 14:55:17 +08:00
< div >< h3 id = "pallet-spec-heading" > 托盘规格 </ h3 >< p > 编码由系统自动生成 , 请维护类型 、 容积 、 重量和颜色 。 </ p ></ div >
2026-08-05 17:10:10 +08:00
</ div >
< div class = "editor-field-grid" >
2026-08-06 14:55:17 +08:00
< el-form-item label = "托盘编码" >
< el-input : model -value =" form.palletCode || ' 保存后自动生成 '" disabled aria -label =" 托盘编码 ( 系统生成 ) " />
2026-08-05 17:10:10 +08:00
</ el-form-item >
< el-form-item label = "托盘类型" prop = "palletType" required >
< el-select
v-model = "form.palletType"
filterable
allow -create
default -first-option
placeholder = "请选择或输入托盘类型"
aria -label =" 编辑托盘类型 "
>
< el-option v-for = "type in palletTypeOptions" :key="type" :label="type" :value="type" />
</ el-select >
</ el-form-item >
</ div >
2026-08-06 14:55:17 +08:00
< div class = "editor-field-grid" >
< el-form-item label = "托盘容积(m³)" prop = "capacityCbm" required >
< el-input-number v-model = "form.capacityCbm" :min="0.000001" :max="999999999999.999999" :precision="6" :step="0.1" controls-position="right" aria-label="托盘容积" />
</ el-form-item >
< el-form-item label = "托盘重量(kg) " prop = "weightKg" required >
< el-input-number v-model = "form.weightKg" :min="0.001" :max="999999999999999.999" :precision="3" :step="0.5" controls-position="right" aria-label="托盘重量" />
</ el-form-item >
</ div >
< el-form-item label = "颜色" prop = "color" required >
< el-input v-model = "form.color" maxlength="32" show-word-limit placeholder="例如 黄色" />
2026-08-05 17:10:10 +08:00
</ el-form-item >
< el-form-item label = "备注" prop = "remark" class = "editor-remark-field" >
2026-08-06 14:55:17 +08:00
< el-input v-model = "form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="可填写其他说明" />
2026-08-05 17:10:10 +08:00
</ el-form-item >
</ section >
2026-08-04 21:01:39 +08:00
</ el-form >
< template # footer >
2026-08-05 17:10:10 +08:00
< div class = "drawer-footer pallet-editor-footer" >
< span >< i aria -hidden =" true " > * </ i > 为必填项 </ span >
< div >< el-button :disabled = "saving" @click ="editorOpen = false" > 取消 </ el -button >< el-button type = "primary" :loading = "saving" @click ="savePallet" > 保存托盘 </ el -button ></ div >
</ div >
2026-08-04 21:01:39 +08:00
</ template >
</ el-drawer >
</ section >
</ template >
< style scoped >
. pallet - page { display : grid ; min - width : 0 ; gap : 16 px ; }
. surface - panel { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }
. table - panel { min - width : 0 ; }
. panel - heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 20 px ; padding : 14 px 18 px ; border - bottom : 1 px solid var ( -- cc - border ); }
. panel - heading h2 { margin : 0 ; font - size : 16 px ; }
. panel - heading p { margin : 4 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; }
. context - empty { padding : 56 px 20 px ; color : var ( -- cc - muted ); text - align : center ; }
. pallet - row - actions { gap : 8 px ; white - space : nowrap ; }
. pallet - row - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
. pallet - cell { display : flex ; align - items : center ; gap : 10 px ; }
. pallet - cell > span , . detail - hero > span { display : inline - flex ; align - items : center ; justify - content : center ; flex : 0 0 auto ; border - radius : 8 px ; background : var ( -- el - color - primary - light - 9 ); color : var ( -- cc - primary ); }
. pallet - cell > span { width : 34 px ; height : 34 px ; }
. remark - text { display : - webkit - box ; overflow : hidden ; - webkit - box - orient : vertical ; - webkit - line - clamp : 2 ; line - height : 1.55 ; }
2026-08-06 19:03:13 +08:00
. detail - body { min - height : 180 px ; padding : 24 px ; box - sizing : border - box ; overflow - x : hidden ; }
2026-08-04 21:01:39 +08:00
. detail - hero { display : flex ; align - items : center ; gap : 12 px ; margin - bottom : 20 px ; }
. detail - hero > span { width : 46 px ; height : 46 px ; }
. detail - hero h2 , . detail - hero p { margin : 0 ; }
. detail - hero h2 { font - size : 18 px ; }
. detail - hero p { margin - top : 4 px ; color : var ( -- cc - muted ); font - size : 13 px ; }
. detail - section - title { margin : 20 px 0 8 px ; color : var ( -- cc - text ); font - size : 14 px ; }
. detail - section - title : first - of - type { margin - top : 0 ; }
2026-08-05 17:10:10 +08:00
. pallet - editor - header { display : flex ; min - width : 0 ; align - items : center ; gap : 12 px ; }
. pallet - editor - header > span { display : grid ; width : 38 px ; height : 38 px ; flex : 0 0 auto ; place - items : center ; border - radius : 8 px ; background : var ( -- el - color - primary - light - 9 ); color : var ( -- cc - primary ); }
. pallet - editor - header h2 { margin : 0 ; color : # 172033 ; font - size : 17 px ; line - height : 1.4 ; }
. pallet - editor - header p { margin : 3 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.5 ; }
. editor - form { padding : 4 px 24 px 28 px ; }
. editor - section { padding : 20 px 0 4 px ; border - bottom : 1 px solid var ( -- cc - border ); }
. editor - section : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
. editor - section - heading { display : flex ; align - items : flex - start ; gap : 9 px ; margin - bottom : 18 px ; }
. editor - section - heading > svg { margin - top : 1 px ; flex : 0 0 auto ; color : var ( -- cc - primary ); }
. editor - section - heading h3 { margin : 0 ; color : # 172033 ; font - size : 14 px ; line - height : 1.4 ; }
. editor - section - heading p { margin : 3 px 0 0 ; color : var ( -- cc - muted ); font - size : 11 px ; line - height : 1.5 ; }
. editor - field - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 14 px ; }
. editor - context { margin - bottom : 18 px ; padding : 12 px 14 px ; border - radius : 6 px ; background : # f8fafc ; }
. editor - context div { display : grid ; gap : 3 px ; }
. editor - context strong { color : # 172033 ; font - size : 13 px ; }
. editor - context span { color : var ( -- cc - muted ); font - size : 11 px ; }
2026-08-04 21:01:39 +08:00
. editor - form : deep (. el - input - number ), . editor - form : deep (. el - select ) { width : 100 % ; }
2026-08-05 17:10:10 +08:00
. editor - form : deep (. el - form - item ) { margin - bottom : 18 px ; }
. editor - form : deep (. el - form - item__label ) { padding - bottom : 7 px ; color : # 344054 ; font - size : 12 px ; font - weight : 650 ; line - height : 1.4 ; }
. editor - remark - field { margin - bottom : 0 ! important ; }
2026-08-04 21:01:39 +08:00
. field - help { margin : 6 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.5 ; }
. drawer - footer { display : flex ; justify - content : flex - end ; gap : 8 px ; }
. drawer - footer : deep (. el - button + . el - button ) { margin - left : 0 ; }
2026-08-05 17:10:10 +08:00
. pallet - editor - footer { align - items : center ; justify - content : space - between ; }
. pallet - editor - footer > span { color : var ( -- cc - muted ); font - size : 11 px ; }
. pallet - editor - footer > span i { color : var ( -- el - color - danger ); font - style : normal ; }
. pallet - editor - footer > div { display : flex ; gap : 8 px ; }
. pallet - editor - footer > div : deep (. el - button + . el - button ) { margin - left : 0 ; }
2026-08-04 21:01:39 +08:00
@ media ( max - width : 720 px ) {
2026-08-06 19:03:13 +08:00
. detail - body { padding : 16 px ; }
2026-08-04 21:01:39 +08:00
. panel - heading { align - items : stretch ; flex - direction : column ; }
. panel - heading > . el - button { width : 100 % ; min - height : 44 px ; }
. pallet - row - actions : deep (. el - button ) { min - height : 36 px ; padding - inline : 5 px ; }
2026-08-05 17:10:10 +08:00
. editor - form { padding - right : 16 px ; padding - left : 16 px ; }
. editor - field - grid { grid - template - columns : 1 fr ; }
. pallet - editor - footer { display : block ; }
. pallet - editor - footer > span { display : none ; }
. pallet - editor - footer > div { display : grid ; grid - template - columns : 1 fr 1 fr ; }
. pallet - editor - footer : deep (. el - button ) { min - height : 44 px ; margin - left : 0 ; }
2026-08-04 21:01:39 +08:00
}
@ media ( max - width : 768 px ) {
. pallet - filter - toolbar : deep (. query - toolbar__filters ) { width : 100 % ; flex - direction : column ; }
. pallet - filter - toolbar : deep (. query - toolbar__filters . el - input ),
. pallet - filter - toolbar : deep (. query - toolbar__filters . el - select ),
. pallet - filter - toolbar : deep (. query - toolbar__filters . el - date - editor ) { width : 100 % ! important ; }
}
</ style >