Files
admin-web/src/modules/warehouse-resources/locations/pages/LocationsView.vue
T
2026-08-13 20:00:38 +08:00

714 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { MapPin, Pencil, Plus, Power, Save, Trash2 } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import {
changePlatformLocationStatus,
createPlatformLocation,
deletePlatformLocation,
getPlatformLocation,
listPlatformLocations,
updatePlatformLocation,
type EditablePlatformLocationStatus,
type PlatformLocationQuery,
type PlatformLocationResource,
type PlatformLocationStatus,
type PlatformLocationType,
type SavePlatformLocationRequest,
} from '../api/platformLocations'
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
type EditorMode = 'create' | 'edit'
type LocationFilters = QueryModel & {
warehouseId: string
zoneCode: string
locationCode: string
locationType: PlatformLocationType | ''
status: PlatformLocationStatus | ''
}
type AppliedLocationFilters = {
warehouseId: string
zoneCode: string
locationCode: string
locationType: PlatformLocationType | ''
status: PlatformLocationStatus | ''
}
const access = useAccessPreviewStore()
const warehouses = ref<PlatformWarehouseResource[]>([])
const warehouseLoading = ref(false)
const warehouseError = ref<ApiErrorDetails | null>(null)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const rows = ref<PlatformLocationResource[]>([])
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const editorOpen = ref(false)
const editorMode = ref<EditorMode>('create')
const editorLoading = ref(false)
const saving = ref(false)
const actionLoadingIds = ref(new Set<string>())
const formRef = ref<FormInstance>()
let warehouseLoadSequence = 0
let locationLoadSequence = 0
let editorLoadSequence = 0
const filters = ref<LocationFilters>({ warehouseId: '', zoneCode: '', locationCode: '', locationType: '', status: '' })
const appliedFilters = ref<AppliedLocationFilters>({ warehouseId: '', zoneCode: '', locationCode: '', locationType: '', status: '' })
const selectedWarehouseId = computed(() => String(filters.value.warehouseId || ''))
const queryFields = computed<QueryField[]>(() => [
{
key: 'warehouseId',
type: 'select',
label: '所属仓库',
width: 300,
placeholder: warehouseLoading.value ? '仓库加载中' : '全部仓库',
props: {
'aria-label': '所属仓库',
filterable: true,
disabled: selectionDisabled.value || warehouseLoading.value || Boolean(warehouseError.value),
},
options: warehouses.value.map((warehouse) => ({
value: String(warehouse.id),
label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`,
})),
},
{ key: 'zoneCode', type: 'input', width: 210, label: '库区编码', placeholder: '库区编码', props: { maxlength: 50, 'aria-label': '库区编码' } },
{ key: 'locationCode', type: 'input', width: 220, label: '库位编码', placeholder: '库位编码', props: { maxlength: 50, 'aria-label': '库位编码' } },
{
key: 'locationType', type: 'select', width: 170, label: '库位类型', placeholder: '全部类型', props: { 'aria-label': '库位类型' },
options: [
{ label: '货架位', value: 'SHELF' },
{ label: '托盘位', value: 'PALLET' },
{ label: '地堆位', value: 'FLOOR' },
{ label: '流利架', value: 'FLOW_RACK' },
],
},
{
key: 'status', type: 'select', width: 160, label: '库位状态', placeholder: '全部状态', props: { 'aria-label': '库位状态' },
options: [
{ label: '可用', value: 'AVAILABLE' },
{ label: '已预占', value: 'RESERVED' },
{ label: '已占用', value: 'OCCUPIED' },
{ label: '已停用', value: 'DISABLED' },
],
},
])
const emptyForm = () => ({
id: '',
warehouseId: '',
zoneCode: '',
zoneName: '',
locationCode: '',
locationName: '',
locationType: 'SHELF' as PlatformLocationType,
maxWeightKg: null as number | null,
status: 'AVAILABLE' as EditablePlatformLocationStatus,
remark: '',
version: 0,
})
const form = reactive(emptyForm())
const rules: FormRules<typeof form> = {
warehouseId: [{ required: true, message: '请选择国内仓', trigger: 'change' }],
zoneCode: [{ required: true, message: '请输入库区编码', trigger: 'blur' }],
zoneName: [{ required: true, message: '请输入库区名称', trigger: 'blur' }],
locationCode: [
{ required: true, message: '请输入库位编码', trigger: 'blur' },
{
validator: (_rule, value: string, callback) => {
if (value.trim().toUpperCase() === 'A0') callback(new Error('A0 是系统共享溢出位保留编码'))
else callback()
},
trigger: 'blur',
},
],
locationType: [{ required: true, message: '请选择库位类型', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}
const selectedWarehouse = computed(() => warehouses.value.find((item) => String(item.id) === form.warehouseId) ?? null)
const domesticWarehouses = computed(() => warehouses.value.filter((warehouse) => (
warehouse.warehouseType === 'DOMESTIC' && warehouse.status === 'ACTIVE'
)))
const editorTitle = computed(() => editorMode.value === 'create' ? '新增库位' : '编辑库位')
const editorPermission = computed(() => editorMode.value === 'create' ? 'platform:location:create' : 'platform:location:edit')
const selectionDisabled = computed(() => editorOpen.value || editorLoading.value || saving.value || actionLoadingIds.value.size > 0)
const filtered = computed(() => Object.values(appliedFilters.value).some((value) => String(value || '').trim()))
const emptyDescription = computed(() => filtered.value ? '暂无符合筛选条件的库位' : '暂无库位数据')
const { captureBaseline } = useUnsavedWorkRegistration({
key: 'location-editor',
label: editorTitle,
isOpen: editorOpen,
model: form,
requiredPermission: editorPermission,
})
const statusMeta: Record<PlatformLocationStatus, { label: string; type: 'success' | 'warning' | 'danger' | 'info' }> = {
AVAILABLE: { label: '可用', type: 'success' },
RESERVED: { label: '已预占', type: 'warning' },
OCCUPIED: { label: '已占用', type: 'danger' },
DISABLED: { label: '已停用', type: 'info' },
}
const typeLabels: Record<PlatformLocationType, string> = {
SHELF: '货架位',
PALLET: '托盘位',
FLOOR: '地堆位',
FLOW_RACK: '流利架',
}
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 240, fixed: 'left', slotName: 'actions' },
{ key: 'location', label: '库位编码', minWidth: 190, slotName: 'location' },
{ key: 'warehouse', label: '所属仓库', minWidth: 210, slotName: 'warehouse' },
{ key: 'zone', label: '库区', minWidth: 150, slotName: 'zone' },
{ key: 'locationType', label: '库位类型', width: 120, slotName: 'locationType' },
{ key: 'maxWeightKg', label: '最大承重(kg)', minWidth: 140, slotName: 'maxWeightKg', defaultVisible: false },
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark', defaultVisible: false },
]
function apiId(value: string) {
return value
}
function optionalText(value: string) {
return value.trim() || null
}
function formatWeight(value: number | string | null) {
if (value === null || value === '') return '—'
const numeric = Number(value)
return Number.isFinite(numeric) ? `${numeric.toLocaleString('zh-CN', { maximumFractionDigits: 3 })} kg` : `${value} kg`
}
function canEdit(row: PlatformLocationResource) {
return access.can('platform:location:edit')
&& isDomesticWarehouse(row.warehouseId)
&& !row.sharedOverflow
&& (row.status === 'AVAILABLE' || row.status === 'DISABLED')
}
function canChangeStatus(row: PlatformLocationResource) {
return access.can('platform:location:status')
&& !row.sharedOverflow
&& (row.status === 'AVAILABLE' || row.status === 'DISABLED')
}
function canDelete(row: PlatformLocationResource) {
return access.can('platform:location:delete') && !row.sharedOverflow && row.status === 'DISABLED'
}
function actionError(error: unknown, fallback: string) {
const details = getApiErrorDetails(error, fallback)
ElMessage.error(details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message)
}
function isDomesticWarehouse(warehouseId: number | string) {
return warehouses.value.some((warehouse) => (
String(warehouse.id) === String(warehouseId)
&& warehouse.warehouseType === 'DOMESTIC'
))
}
function isActiveDomesticWarehouse(warehouseId: number | string) {
return domesticWarehouses.value.some((warehouse) => String(warehouse.id) === String(warehouseId))
}
function requireCreateScope() {
if (!selectedWarehouse.value || !form.warehouseId) {
ElMessage.warning('请选择国内仓')
return null
}
return { warehouseId: apiId(form.warehouseId) }
}
function recordScope(row: PlatformLocationResource) {
return { warehouseId: apiId(String(row.warehouseId)) }
}
async function loadWarehouses() {
const sequence = ++warehouseLoadSequence
warehouseLoading.value = true
warehouseError.value = null
warehouses.value = []
try {
// 库位只属于国内仓,查询候选仓库时直接限制仓库类型,避免把 FBA/海外仓带入页面。
const records = await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC' })
if (sequence !== warehouseLoadSequence) return
// 保留前端兜底过滤,避免旧接口或异常响应返回其他仓库类型。
warehouses.value = records.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC')
} catch (error) {
if (sequence !== warehouseLoadSequence) return
warehouseError.value = getApiErrorDetails(error, '仓库列表加载失败,请稍后重试')
} finally {
if (sequence === warehouseLoadSequence) warehouseLoading.value = false
}
}
async function loadLocations() {
const sequence = ++locationLoadSequence
loading.value = true
loadError.value = null
try {
const query: PlatformLocationQuery = {
page: page.value,
size: pageSize.value,
zoneCode: appliedFilters.value.zoneCode,
locationCode: appliedFilters.value.locationCode,
locationType: appliedFilters.value.locationType,
status: appliedFilters.value.status,
}
if (appliedFilters.value.warehouseId) query.warehouseId = apiId(appliedFilters.value.warehouseId)
const result = await listPlatformLocations(query)
if (sequence !== locationLoadSequence) return
rows.value = result.records
total.value = result.total
} catch (error) {
if (sequence !== locationLoadSequence) return
rows.value = []
total.value = 0
loadError.value = getApiErrorDetails(error, '库位列表加载失败,请稍后重试')
} finally {
if (sequence === locationLoadSequence) loading.value = false
}
}
function applyFilters(model: QueryModel) {
appliedFilters.value = {
warehouseId: String(model.warehouseId || ''),
zoneCode: String(model.zoneCode || ''),
locationCode: String(model.locationCode || ''),
locationType: (model.locationType || '') as PlatformLocationType | '',
status: (model.status || '') as PlatformLocationStatus | '',
}
if (page.value === 1) void loadLocations()
else page.value = 1
}
function assignForm(resource: PlatformLocationResource) {
Object.assign(form, {
id: String(resource.id),
warehouseId: String(resource.warehouseId),
zoneCode: resource.zoneCode,
zoneName: resource.zoneName,
locationCode: resource.locationCode,
locationName: resource.locationName ?? '',
locationType: resource.locationType,
maxWeightKg: resource.maxWeightKg === null ? null : Number(resource.maxWeightKg),
status: resource.status as EditablePlatformLocationStatus,
remark: resource.remark ?? '',
version: resource.version,
})
}
function openCreate() {
editorLoadSequence += 1
Object.assign(form, emptyForm())
form.warehouseId = isActiveDomesticWarehouse(selectedWarehouseId.value) ? selectedWarehouseId.value : ''
form.status = selectedWarehouse.value?.status === 'INACTIVE' ? 'DISABLED' : 'AVAILABLE'
editorMode.value = 'create'
captureBaseline()
editorOpen.value = true
void formRef.value?.clearValidate()
}
async function openEdit(row: PlatformLocationResource) {
if (!isDomesticWarehouse(row.warehouseId)) {
ElMessage.warning('仅支持编辑国内仓库位')
return
}
const sequence = ++editorLoadSequence
const scope = recordScope(row)
editorLoading.value = true
try {
const result = await getPlatformLocation(scope.warehouseId, row.id)
if (sequence !== editorLoadSequence) return
editorMode.value = 'edit'
assignForm(result.location)
captureBaseline()
editorOpen.value = true
void formRef.value?.clearValidate()
} catch (error) {
if (sequence !== editorLoadSequence) return
actionError(error, '库位资料加载失败,请稍后重试')
} finally {
if (sequence === editorLoadSequence) editorLoading.value = false
}
}
function buildPayload(): SavePlatformLocationRequest {
return {
warehouseId: apiId(form.warehouseId),
zoneCode: form.zoneCode.trim(),
zoneName: form.zoneName.trim(),
locationCode: form.locationCode.trim(),
locationName: optionalText(form.locationName),
locationType: form.locationType,
maxWeightKg: form.maxWeightKg,
status: form.status,
remark: optionalText(form.remark),
...(form.id ? { version: form.version } : {}),
}
}
async function saveLocation() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
const warehouseAllowed = form.id
? isDomesticWarehouse(form.warehouseId)
: isActiveDomesticWarehouse(form.warehouseId)
if (!warehouseAllowed) {
ElMessage.error(form.id ? '库位仅支持归属国内仓' : '新增库位仅支持选择启用的国内仓')
return
}
const scope = form.id ? { warehouseId: apiId(form.warehouseId) } : requireCreateScope()
if (!scope) return
saving.value = true
try {
const payload = buildPayload()
if (form.id) await updatePlatformLocation(scope.warehouseId, form.id, payload)
else await createPlatformLocation(payload)
editorOpen.value = false
ElMessage.success(form.id ? '库位资料已更新' : '库位已创建')
await loadLocations()
} catch (error) {
actionError(error, '库位保存失败,请检查后重试')
if (form.id) await loadLocations()
} finally {
saving.value = false
}
}
function setActionLoading(id: number | string, active: boolean) {
const next = new Set(actionLoadingIds.value)
if (active) next.add(String(id))
else next.delete(String(id))
actionLoadingIds.value = next
}
async function toggleStatus(row: PlatformLocationResource) {
const scope = recordScope(row)
if (!canChangeStatus(row)) return
const nextStatus: EditablePlatformLocationStatus = row.status === 'AVAILABLE' ? 'DISABLED' : 'AVAILABLE'
const action = nextStatus === 'AVAILABLE' ? '启用' : '停用'
try {
await ElMessageBox.confirm(
nextStatus === 'DISABLED'
? `确定停用“${row.locationCode}”吗?停用前后端会再次检查是否存在活动预占或占用。`
: `确定重新启用“${row.locationCode}”吗?仅启用仓库中的库位可以恢复可用。`,
`${action}库位`,
{ type: nextStatus === 'AVAILABLE' ? 'success' : 'warning', confirmButtonText: `确认${action}`, cancelButtonText: '取消' },
)
} catch {
return
}
setActionLoading(row.id, true)
try {
await changePlatformLocationStatus(scope.warehouseId, row.id, nextStatus, row.version)
ElMessage.success(`库位 ${row.locationCode} 已${action}`)
await loadLocations()
} catch (error) {
actionError(error, `${action}库位失败`)
await loadLocations()
} finally {
setActionLoading(row.id, false)
}
}
async function removeLocation(row: PlatformLocationResource) {
const scope = recordScope(row)
if (!canDelete(row)) return
try {
await ElMessageBox.confirm(
`确定删除已停用库位“${row.locationCode}”吗?系统会保留审计记录,此操作不能直接恢复。`,
'删除库位',
{ type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' },
)
} catch {
return
}
setActionLoading(row.id, true)
try {
await deletePlatformLocation(scope.warehouseId, row.id, row.version)
ElMessage.success(`库位 ${row.locationCode} 已删除`)
await loadLocations()
} catch (error) {
actionError(error, '删除库位失败')
await loadLocations()
} finally {
setActionLoading(row.id, false)
}
}
watch(
() => access.can(editorPermission.value),
(allowed) => {
if (allowed || !editorOpen.value) return
editorOpen.value = false
},
{ flush: 'sync' },
)
watch([page, pageSize], () => { void loadLocations() })
watch(editorOpen, (open) => {
if (open) return
editorLoadSequence += 1
editorLoading.value = false
})
watch(() => form.warehouseId, (warehouseId) => {
if (editorMode.value !== 'create') return
const warehouse = warehouses.value.find((item) => String(item.id) === warehouseId)
form.status = warehouse?.status === 'INACTIVE' ? 'DISABLED' : 'AVAILABLE'
})
onMounted(() => {
void loadWarehouses()
void loadLocations()
})
</script>
<template>
<AdminPage as="div" class="location-page">
<ListPageLayout aria-label="库位列表页面">
<template #toolbar>
<QueryToolbar
v-model="filters"
ariaLabel="库位筛选"
:fields="queryFields"
preference-key="locations-query"
@search="applyFilters"
/>
</template>
<template #state>
<LoadErrorState
v-if="warehouseError"
title="仓库列表加载失败"
:message="warehouseError.message"
:loading="warehouseLoading"
@retry="loadWarehouses"
/>
<el-alert v-else-if="!warehouseLoading && domesticWarehouses.length === 0" type="info" :closable="false" show-icon title="暂无可选国内仓">
请先在仓库管理中建立国内仓,再维护库位。
</el-alert>
<LoadErrorState
v-else-if="loadError"
title="库位列表加载失败"
:message="loadError.message"
:loading="loading"
@retry="loadLocations"
/>
</template>
<section v-if="!loadError" class="surface-panel table-panel" aria-label="库位列表区域">
<DataTable
v-model:page="page"
v-model:page-size="pageSize"
:data="rows"
:columns="columns"
border
stripe
preference-key="locations-table"
:loading="loading"
:total="total"
:page-sizes="[20, 50, 100, 200]"
row-key="id"
aria-label="库位列表"
:empty-text="emptyDescription"
>
<template #toolbar-actions><el-button v-if="access.can('platform:location:create')" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template>
<template #actions="{ row }">
<div class="table-actions location-actions">
<el-button
v-if="canEdit(row as PlatformLocationResource)"
size="small"
plain
type="primary"
:loading="editorLoading"
:disabled="loading || editorLoading"
@click="openEdit(row as PlatformLocationResource)"
><Pencil :size="15" />编辑</el-button>
<el-button
v-if="canChangeStatus(row as PlatformLocationResource)"
size="small"
plain
:type="(row as PlatformLocationResource).status === 'AVAILABLE' ? 'danger' : 'success'"
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
@click="toggleStatus(row as PlatformLocationResource)"
><Power :size="15" />{{ (row as PlatformLocationResource).status === 'AVAILABLE' ? '停用' : '启用' }}</el-button>
<el-button
v-if="canDelete(row as PlatformLocationResource)"
size="small"
plain
type="danger"
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
@click="removeLocation(row as PlatformLocationResource)"
><Trash2 :size="15" />删除</el-button>
</div>
</template>
<template #location="{ row }">
<div class="location-cell">
<span><MapPin :size="17" /></span>
<div>
<strong>{{ (row as PlatformLocationResource).locationCode }}</strong>
<small>{{ (row as PlatformLocationResource).locationName || '未命名库位' }}</small>
</div>
<el-tag v-if="(row as PlatformLocationResource).sharedOverflow" size="small" type="warning">系统位</el-tag>
</div>
</template>
<template #zone="{ row }">
<div class="stacked-value"><strong>{{ (row as PlatformLocationResource).zoneName }}</strong><small>{{ (row as PlatformLocationResource).zoneCode }}</small></div>
</template>
<template #warehouse="{ row }">
<div class="stacked-value">
<strong>{{ (row as PlatformLocationResource).warehouseName || warehouses.find((item) => String(item.id) === String((row as PlatformLocationResource).warehouseId))?.warehouseName || '—' }}</strong>
<small>{{ (row as PlatformLocationResource).warehouseCode || warehouses.find((item) => String(item.id) === String((row as PlatformLocationResource).warehouseId))?.warehouseCode || '—' }}</small>
</div>
</template>
<template #locationType="{ row }">{{ typeLabels[(row as PlatformLocationResource).locationType] }}</template>
<template #maxWeightKg="{ row }">{{ formatWeight((row as PlatformLocationResource).maxWeightKg) }}</template>
<template #status="{ row }">
<el-tag :type="statusMeta[(row as PlatformLocationResource).status].type" effect="light">
{{ statusMeta[(row as PlatformLocationResource).status].label }}
</el-tag>
</template>
<template #remark="{ row }"><span class="remark-cell">{{ (row as PlatformLocationResource).remark || '—' }}</span></template>
<template #empty>
<el-empty v-if="!loading" :description="emptyDescription">
<el-button
v-if="!filtered && access.can('platform:location:create')"
type="primary"
@click="openCreate"
><Plus :size="16" />新增第一个库位</el-button>
</el-empty>
</template>
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 个库位</span></template>
</DataTable>
</section>
</ListPageLayout>
<AppDialog
v-model="editorOpen"
:title="editorTitle"
width="min(560px, calc(100vw - 28px))"
class="location-editor-dialog"
:confirm-loading="saving"
:close-on-click-modal="false"
:close-on-press-escape="!saving"
:show-close="!saving"
:show-confirm="false"
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="editor-form" :disabled="saving">
<div class="editor-field-grid">
<el-form-item label="所属仓库" prop="warehouseId" required>
<el-select v-model="form.warehouseId" filterable placeholder="请选择国内仓" :loading="warehouseLoading">
<el-option
v-for="warehouse in domesticWarehouses"
:key="String(warehouse.id)"
:label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`"
:value="String(warehouse.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="库位编码" prop="locationCode" required>
<el-input v-model.trim="form.locationCode" maxlength="50" placeholder="例如 A1-01" />
</el-form-item>
</div>
<div class="editor-field-grid">
<el-form-item label="库位名称">
<el-input v-model.trim="form.locationName" maxlength="100" placeholder="例如 A1 区 01 位" />
</el-form-item>
<el-form-item label="库区编码" prop="zoneCode" required>
<el-input v-model.trim="form.zoneCode" maxlength="50" placeholder="例如 A" />
</el-form-item>
</div>
<div class="editor-field-grid">
<el-form-item label="库区名称" prop="zoneName" required>
<el-input v-model.trim="form.zoneName" maxlength="100" placeholder="例如 常规区" />
</el-form-item>
<el-form-item label="库位类型" prop="locationType" required>
<el-select v-model="form.locationType" placeholder="请选择库位类型">
<el-option label="货架位" value="SHELF" />
<el-option label="托盘位" value="PALLET" />
<el-option label="地堆位" value="FLOOR" />
<el-option label="流利架" value="FLOW_RACK" />
</el-select>
</el-form-item>
</div>
<div class="editor-field-grid">
<el-form-item label="最大承重(kg)">
<el-input-number v-model="form.maxWeightKg" :min="0.001" :precision="3" :step="10" controls-position="right" placeholder="选填" />
</el-form-item>
<el-form-item label="状态" prop="status" required>
<el-select v-model="form.status" :disabled="editorMode === 'edit'" placeholder="请选择状态">
<el-option label="可用" value="AVAILABLE" :disabled="selectedWarehouse?.status !== 'ACTIVE'" />
<el-option label="停用" value="DISABLED" />
</el-select>
</el-form-item>
</div>
<el-form-item label="备注" class="editor-remark-field">
<el-input v-model.trim="form.remark" maxlength="500" show-word-limit placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<div class="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="saveLocation"><Save :size="16" />保存库位</el-button>
</div>
</div>
</template>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.state-alert p { margin: 6px 0 0; line-height: 1.55; }
.state-alert .el-button { margin-top: 10px; }
.location-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.location-actions :deep(.el-button) { margin-left: 0; }
.location-cell { display: flex; min-width: 0; align-items: center; gap: 10px; }
.location-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); }
.location-cell > div, .stacked-value { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.location-cell strong, .stacked-value strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.location-cell small, .stacked-value small { color: var(--cc-muted); font-size: 11px; }
.remark-cell { display: -webkit-box; overflow: hidden; line-height: 1.5; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
:global(.location-editor-dialog) { max-height: min(760px, calc(100vh - 32px)); display: flex; flex-direction: column; }
:global(.location-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }
:global(.location-editor-dialog .el-dialog__footer) { flex: 0 0 auto; }
.editor-form { padding: 8px 24px 12px; }
.editor-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
.editor-form :deep(.el-input-number), .editor-form :deep(.el-select) { width: 100%; }
.editor-form :deep(.el-form-item) { margin-bottom: 18px; }
.editor-form :deep(.el-form-item__label) { padding-bottom: 7px; color: #344054; font-size: 12px; font-weight: 650; line-height: 1.4; }
.editor-remark-field { margin-bottom: 0 !important; }
.editor-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.editor-footer > span { color: var(--cc-muted); font-size: 11px; }
.editor-footer > span i { color: var(--el-color-danger); font-style: normal; }
.editor-footer > div { display: flex; gap: 8px; }
.editor-footer :deep(.el-button + .el-button) { margin-left: 0; }
@media (max-width: 720px) {
.editor-form { padding-right: 16px; padding-left: 16px; }
.editor-field-grid { grid-template-columns: minmax(0, 1fr); }
.editor-footer { display: block; }
.editor-footer > span { display: none; }
.editor-footer > div { display: grid; grid-template-columns: 1fr 1fr; }
.editor-footer :deep(.el-button) { min-height: 44px; margin-left: 0; }
}
</style>