2026-08-04 21:01:39 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-09-14 15:33:35 +08:00
|
|
|
|
import '@/styles/adminDensity.css'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
2026-09-14 17:54:21 +08:00
|
|
|
|
import { MapPin, Plus, Save } from '@lucide/vue'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
2026-08-10 16:24:54 +08:00
|
|
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
2026-08-12 16:58:52 +08:00
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
2026-08-10 16:24:54 +08:00
|
|
|
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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'
|
2026-08-06 14:55:17 +08:00
|
|
|
|
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
2026-08-13 20:00:38 +08:00
|
|
|
|
type EditorMode = 'create' | 'edit'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
type LocationFilters = QueryModel & {
|
|
|
|
|
|
warehouseId: string
|
|
|
|
|
|
zoneCode: string
|
|
|
|
|
|
locationCode: string
|
|
|
|
|
|
locationType: PlatformLocationType | ''
|
|
|
|
|
|
status: PlatformLocationStatus | ''
|
|
|
|
|
|
}
|
|
|
|
|
|
type AppliedLocationFilters = {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
warehouseId: string
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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)
|
2026-08-13 20:00:38 +08:00
|
|
|
|
const editorOpen = ref(false)
|
|
|
|
|
|
const editorMode = ref<EditorMode>('create')
|
|
|
|
|
|
const editorLoading = ref(false)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const actionLoadingIds = ref(new Set<string>())
|
|
|
|
|
|
const formRef = ref<FormInstance>()
|
2026-08-05 15:54:59 +08:00
|
|
|
|
let warehouseLoadSequence = 0
|
2026-08-04 21:01:39 +08:00
|
|
|
|
let locationLoadSequence = 0
|
2026-08-13 20:00:38 +08:00
|
|
|
|
let editorLoadSequence = 0
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
const filters = ref<LocationFilters>({ warehouseId: '', zoneCode: '', locationCode: '', locationType: '', status: '' })
|
|
|
|
|
|
const appliedFilters = ref<AppliedLocationFilters>({ warehouseId: '', zoneCode: '', locationCode: '', locationType: '', status: '' })
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const selectedWarehouseId = computed(() => String(filters.value.warehouseId || ''))
|
|
|
|
|
|
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,
|
|
|
|
|
|
placeholder: warehouseLoading.value ? '仓库加载中' : '全部仓库',
|
2026-08-04 21:01:39 +08:00
|
|
|
|
props: {
|
|
|
|
|
|
'aria-label': '所属仓库',
|
|
|
|
|
|
filterable: true,
|
2026-08-05 15:54:59 +08:00
|
|
|
|
disabled: selectionDisabled.value || warehouseLoading.value || Boolean(warehouseError.value),
|
2026-08-04 21:01:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
options: warehouses.value.map((warehouse) => ({
|
|
|
|
|
|
value: String(warehouse.id),
|
2026-08-06 13:56:04 +08:00
|
|
|
|
label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
})),
|
|
|
|
|
|
},
|
2026-08-06 19:03:13 +08:00
|
|
|
|
{ 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': '库位编码' } },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
{
|
2026-08-06 19:03:13 +08:00
|
|
|
|
key: 'locationType', type: 'select', width: 170, label: '库位类型', placeholder: '全部类型', props: { 'aria-label': '库位类型' },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
options: [
|
|
|
|
|
|
{ label: '货架位', value: 'SHELF' },
|
|
|
|
|
|
{ label: '托盘位', value: 'PALLET' },
|
|
|
|
|
|
{ label: '地堆位', value: 'FLOOR' },
|
|
|
|
|
|
{ label: '流利架', value: 'FLOW_RACK' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-08-06 19:03:13 +08:00
|
|
|
|
key: 'status', type: 'select', width: 160, label: '库位状态', placeholder: '全部状态', props: { 'aria-label': '库位状态' },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
options: [
|
|
|
|
|
|
{ label: '可用', value: 'AVAILABLE' },
|
|
|
|
|
|
{ label: '已预占', value: 'RESERVED' },
|
|
|
|
|
|
{ label: '已占用', value: 'OCCUPIED' },
|
|
|
|
|
|
{ label: '已停用', value: 'DISABLED' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const emptyForm = () => ({
|
|
|
|
|
|
id: '',
|
2026-08-05 15:54:59 +08:00
|
|
|
|
warehouseId: '',
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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> = {
|
2026-08-06 14:55:17 +08:00
|
|
|
|
warehouseId: [{ required: true, message: '请选择国内仓', trigger: 'change' }],
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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' }],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
const selectedWarehouse = 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-13 20:00:38 +08:00
|
|
|
|
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)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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[] = [
|
2026-08-13 15:51:32 +08:00
|
|
|
|
{ key: 'actions', label: '操作', width: 240, fixed: 'left', slotName: 'actions' },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
{ 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' },
|
2026-08-12 12:19:02 +08:00
|
|
|
|
{ key: 'maxWeightKg', label: '最大承重(kg)', minWidth: 140, slotName: 'maxWeightKg', defaultVisible: false },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
2026-08-12 12:19:02 +08:00
|
|
|
|
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark', defaultVisible: false },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
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')
|
2026-08-06 14:55:17 +08:00
|
|
|
|
&& isDomesticWarehouse(row.warehouseId)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
&& !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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 14:55:17 +08:00
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
function requireCreateScope() {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
if (!selectedWarehouse.value || !form.warehouseId) {
|
2026-08-06 14:55:17 +08:00
|
|
|
|
ElMessage.warning('请选择国内仓')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-08-06 15:32:02 +08:00
|
|
|
|
return { warehouseId: apiId(form.warehouseId) }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recordScope(row: PlatformLocationResource) {
|
2026-08-06 15:32:02 +08:00
|
|
|
|
return { warehouseId: apiId(String(row.warehouseId)) }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
async function loadWarehouses() {
|
|
|
|
|
|
const sequence = ++warehouseLoadSequence
|
2026-08-04 21:01:39 +08:00
|
|
|
|
warehouseLoading.value = true
|
|
|
|
|
|
warehouseError.value = null
|
|
|
|
|
|
warehouses.value = []
|
|
|
|
|
|
try {
|
2026-08-06 15:32:02 +08:00
|
|
|
|
// 库位只属于国内仓,查询候选仓库时直接限制仓库类型,避免把 FBA/海外仓带入页面。
|
|
|
|
|
|
const records = await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC' })
|
2026-08-05 15:54:59 +08:00
|
|
|
|
if (sequence !== warehouseLoadSequence) return
|
2026-08-06 15:32:02 +08:00
|
|
|
|
// 保留前端兜底过滤,避免旧接口或异常响应返回其他仓库类型。
|
|
|
|
|
|
warehouses.value = records.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} catch (error) {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
if (sequence !== warehouseLoadSequence) return
|
2026-08-04 21:01:39 +08:00
|
|
|
|
warehouseError.value = getApiErrorDetails(error, '仓库列表加载失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
if (sequence === warehouseLoadSequence) warehouseLoading.value = false
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
2026-08-05 15:54:59 +08:00
|
|
|
|
if (appliedFilters.value.warehouseId) query.warehouseId = apiId(appliedFilters.value.warehouseId)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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 = {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
warehouseId: String(model.warehouseId || ''),
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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() {
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorLoadSequence += 1
|
2026-08-05 15:54:59 +08:00
|
|
|
|
Object.assign(form, emptyForm())
|
2026-08-06 14:55:17 +08:00
|
|
|
|
form.warehouseId = isActiveDomesticWarehouse(selectedWarehouseId.value) ? selectedWarehouseId.value : ''
|
2026-08-05 15:54:59 +08:00
|
|
|
|
form.status = selectedWarehouse.value?.status === 'INACTIVE' ? 'DISABLED' : 'AVAILABLE'
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorMode.value = 'create'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
captureBaseline()
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorOpen.value = true
|
2026-08-04 21:01:39 +08:00
|
|
|
|
void formRef.value?.clearValidate()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 20:00:38 +08:00
|
|
|
|
async function openEdit(row: PlatformLocationResource) {
|
|
|
|
|
|
if (!isDomesticWarehouse(row.warehouseId)) {
|
2026-08-06 14:55:17 +08:00
|
|
|
|
ElMessage.warning('仅支持编辑国内仓库位')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-13 20:00:38 +08:00
|
|
|
|
const sequence = ++editorLoadSequence
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const scope = recordScope(row)
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorLoading.value = true
|
2026-08-04 21:01:39 +08:00
|
|
|
|
try {
|
2026-08-06 15:32:02 +08:00
|
|
|
|
const result = await getPlatformLocation(scope.warehouseId, row.id)
|
2026-08-13 20:00:38 +08:00
|
|
|
|
if (sequence !== editorLoadSequence) return
|
|
|
|
|
|
editorMode.value = 'edit'
|
|
|
|
|
|
assignForm(result.location)
|
|
|
|
|
|
captureBaseline()
|
|
|
|
|
|
editorOpen.value = true
|
|
|
|
|
|
void formRef.value?.clearValidate()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} catch (error) {
|
2026-08-13 20:00:38 +08:00
|
|
|
|
if (sequence !== editorLoadSequence) return
|
|
|
|
|
|
actionError(error, '库位资料加载失败,请稍后重试')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} finally {
|
2026-08-13 20:00:38 +08:00
|
|
|
|
if (sequence === editorLoadSequence) editorLoading.value = false
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
|
|
|
|
if (!valid) return
|
2026-08-06 14:55:17 +08:00
|
|
|
|
const warehouseAllowed = form.id
|
|
|
|
|
|
? isDomesticWarehouse(form.warehouseId)
|
|
|
|
|
|
: isActiveDomesticWarehouse(form.warehouseId)
|
|
|
|
|
|
if (!warehouseAllowed) {
|
|
|
|
|
|
ElMessage.error(form.id ? '库位仅支持归属国内仓' : '新增库位仅支持选择启用的国内仓')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-06 15:32:02 +08:00
|
|
|
|
const scope = form.id ? { warehouseId: apiId(form.warehouseId) } : requireCreateScope()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
if (!scope) return
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = buildPayload()
|
2026-08-06 15:32:02 +08:00
|
|
|
|
if (form.id) await updatePlatformLocation(scope.warehouseId, form.id, payload)
|
|
|
|
|
|
else await createPlatformLocation(payload)
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorOpen.value = false
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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}库位`,
|
2026-09-15 09:58:15 +08:00
|
|
|
|
{ customClass: 'admin-density-confirm', type: nextStatus === 'AVAILABLE' ? 'success' : 'warning', confirmButtonText: `确认${action}`, cancelButtonText: '取消' },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setActionLoading(row.id, true)
|
|
|
|
|
|
try {
|
2026-08-06 15:32:02 +08:00
|
|
|
|
await changePlatformLocationStatus(scope.warehouseId, row.id, nextStatus, row.version)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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}”吗?系统会保留审计记录,此操作不能直接恢复。`,
|
|
|
|
|
|
'删除库位',
|
2026-09-14 15:33:35 +08:00
|
|
|
|
{ customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' },
|
2026-08-04 21:01:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setActionLoading(row.id, true)
|
|
|
|
|
|
try {
|
2026-08-06 15:32:02 +08:00
|
|
|
|
await deletePlatformLocation(scope.warehouseId, row.id, row.version)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorOpen.value = false
|
2026-08-04 21:01:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
{ flush: 'sync' },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch([page, pageSize], () => { void loadLocations() })
|
2026-08-13 20:00:38 +08:00
|
|
|
|
watch(editorOpen, (open) => {
|
2026-08-07 20:07:37 +08:00
|
|
|
|
if (open) return
|
2026-08-13 20:00:38 +08:00
|
|
|
|
editorLoadSequence += 1
|
|
|
|
|
|
editorLoading.value = false
|
2026-08-07 20:07:37 +08:00
|
|
|
|
})
|
2026-08-05 15:54:59 +08:00
|
|
|
|
watch(() => form.warehouseId, (warehouseId) => {
|
2026-08-13 20:00:38 +08:00
|
|
|
|
if (editorMode.value !== 'create') return
|
2026-08-05 15:54:59 +08:00
|
|
|
|
const warehouse = warehouses.value.find((item) => String(item.id) === warehouseId)
|
|
|
|
|
|
form.status = warehouse?.status === 'INACTIVE' ? 'DISABLED' : 'AVAILABLE'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
void loadWarehouses()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
void loadLocations()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-09-14 15:33:35 +08:00
|
|
|
|
<AdminPage as="div" class="location-page admin-density">
|
2026-08-10 16:24:54 +08:00
|
|
|
|
<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>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
2026-08-10 16:24:54 +08:00
|
|
|
|
<section v-if="!loadError" class="surface-panel table-panel" aria-label="库位列表区域">
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<DataTable
|
|
|
|
|
|
v-model:page="page"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:data="rows"
|
|
|
|
|
|
:columns="columns"
|
2026-08-12 18:24:05 +08:00
|
|
|
|
border
|
|
|
|
|
|
stripe
|
2026-08-06 19:03:13 +08:00
|
|
|
|
preference-key="locations-table"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
:loading="loading"
|
|
|
|
|
|
:total="total"
|
|
|
|
|
|
:page-sizes="[20, 50, 100, 200]"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
aria-label="库位列表"
|
|
|
|
|
|
:empty-text="emptyDescription"
|
|
|
|
|
|
>
|
2026-08-06 20:14:56 +08:00
|
|
|
|
<template #toolbar-actions><el-button v-if="access.can('platform:location:create')" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<template #actions="{ row }">
|
2026-09-14 17:54:21 +08:00
|
|
|
|
<div class="location-actions density-actions">
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="canEdit(row as PlatformLocationResource)"
|
2026-08-12 18:24:05 +08:00
|
|
|
|
size="small"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
class="density-action"
|
2026-08-13 20:00:38 +08:00
|
|
|
|
:loading="editorLoading"
|
|
|
|
|
|
:disabled="loading || editorLoading"
|
|
|
|
|
|
@click="openEdit(row as PlatformLocationResource)"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
>编辑</el-button>
|
2026-08-10 17:42:17 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="canChangeStatus(row as PlatformLocationResource)"
|
2026-08-12 18:24:05 +08:00
|
|
|
|
size="small"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
class="density-action"
|
|
|
|
|
|
:data-tone="(row as PlatformLocationResource).status === 'AVAILABLE' ? 'danger' : 'success'"
|
2026-08-10 17:42:17 +08:00
|
|
|
|
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
|
|
|
|
|
|
@click="toggleStatus(row as PlatformLocationResource)"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
>{{ (row as PlatformLocationResource).status === 'AVAILABLE' ? '停用' : '启用' }}</el-button>
|
2026-08-10 17:42:17 +08:00
|
|
|
|
<el-button
|
2026-08-13 20:00:38 +08:00
|
|
|
|
v-if="canDelete(row as PlatformLocationResource)"
|
2026-08-12 18:24:05 +08:00
|
|
|
|
size="small"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
class="density-action" data-tone="danger"
|
2026-08-10 17:42:17 +08:00
|
|
|
|
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
|
|
|
|
|
|
@click="removeLocation(row as PlatformLocationResource)"
|
2026-09-14 17:54:21 +08:00
|
|
|
|
>删除</el-button>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</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">
|
2026-08-05 15:54:59 +08:00
|
|
|
|
<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>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</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
|
2026-08-05 15:54:59 +08:00
|
|
|
|
v-if="!filtered && access.can('platform:location:create')"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="openCreate"
|
|
|
|
|
|
><Plus :size="16" />新增第一个库位</el-button>
|
|
|
|
|
|
</el-empty>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 个库位</span></template>
|
|
|
|
|
|
</DataTable>
|
2026-08-10 16:24:54 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</ListPageLayout>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
2026-08-13 20:00:38 +08:00
|
|
|
|
<AppDialog
|
|
|
|
|
|
v-model="editorOpen"
|
|
|
|
|
|
:title="editorTitle"
|
|
|
|
|
|
width="min(560px, calc(100vw - 28px))"
|
2026-09-14 15:33:35 +08:00
|
|
|
|
class="location-editor-dialog admin-density-dialog"
|
2026-08-10 16:24:54 +08:00
|
|
|
|
:confirm-loading="saving"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
|
:close-on-press-escape="!saving"
|
|
|
|
|
|
:show-close="!saving"
|
2026-08-13 20:00:38 +08:00
|
|
|
|
:show-confirm="false"
|
|
|
|
|
|
destroy-on-close
|
2026-08-04 21:01:39 +08:00
|
|
|
|
>
|
2026-08-13 20:00:38 +08:00
|
|
|
|
<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>
|
2026-08-14 09:14:55 +08:00
|
|
|
|
<el-select v-model="form.warehouseId" filterable placeholder="请选择国内仓" :loading="warehouseLoading" :disabled="editorMode === 'edit'">
|
2026-08-05 15:54:59 +08:00
|
|
|
|
<el-option
|
2026-08-06 14:55:17 +08:00
|
|
|
|
v-for="warehouse in domesticWarehouses"
|
2026-08-05 15:54:59 +08:00
|
|
|
|
:key="String(warehouse.id)"
|
2026-08-06 13:56:04 +08:00
|
|
|
|
:label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`"
|
2026-08-05 15:54:59 +08:00
|
|
|
|
:value="String(warehouse.id)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
2026-08-13 20:00:38 +08:00
|
|
|
|
<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>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2026-08-13 20:00:38 +08:00
|
|
|
|
<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>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-08-13 20:00:38 +08:00
|
|
|
|
</AppDialog>
|
2026-08-10 16:24:54 +08:00
|
|
|
|
</AdminPage>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.state-alert p { margin: var(--admin-space-2, 6px) 0 0; line-height: 1.55; }
|
|
|
|
|
|
.state-alert .el-button { margin-top: var(--admin-space-4, 10px); }
|
|
|
|
|
|
.location-actions { white-space: nowrap; }
|
|
|
|
|
|
.location-cell { display: flex; min-width: 0; align-items: center; gap: var(--admin-space-4, 10px); }
|
2026-09-14 15:33:35 +08:00
|
|
|
|
.location-cell > span { display: grid; width: 20px; height: 20px; flex: 0 0 20px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.location-cell > div, .stacked-value { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
2026-09-14 15:33:35 +08:00
|
|
|
|
.location-cell strong, .stacked-value strong { overflow-wrap: anywhere; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.location-cell small, .stacked-value small { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.remark-cell { display: -webkit-box; overflow: hidden; line-height: 1.5; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
2026-09-14 15:33:35 +08:00
|
|
|
|
.editor-form { padding: 0; }
|
2026-08-13 20:00:38 +08:00
|
|
|
|
.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%; }
|
2026-09-14 15:33:35 +08:00
|
|
|
|
.editor-form :deep(.el-form-item) { margin-bottom: 14px; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.editor-form :deep(.el-form-item__label) { padding-bottom: var(--admin-space-1, 4px); color: #344054; font-size: var(--admin-font-caption, 12px); font-weight: 650; line-height: 1.4; }
|
2026-08-13 20:00:38 +08:00
|
|
|
|
.editor-remark-field { margin-bottom: 0 !important; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.editor-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-3, 8px); }
|
|
|
|
|
|
.editor-footer > span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
2026-08-13 20:00:38 +08:00
|
|
|
|
.editor-footer > span i { color: var(--el-color-danger); font-style: normal; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.editor-footer > div { display: flex; gap: var(--admin-space-3, 8px); }
|
2026-08-13 20:00:38 +08:00
|
|
|
|
.editor-footer :deep(.el-button + .el-button) { margin-left: 0; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
2026-08-13 20:00:38 +08:00
|
|
|
|
.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; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|