Files
admin-web/src/modules/overseas-warehouse/pages/LocationsView.vue
T
2026-09-30 14:01:58 +08:00

57 lines
13 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 '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Save } from '@lucide/vue'
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 { getApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
import { changeOverseasLocationStatus, createOverseasLocation, deleteOverseasLocation, listAllOverseasLocations, listOverseasLocations, updateOverseasLocation, type OverseasLocationDto, type OverseasLocationStatus, type OverseasLocationType } from '../api/locations'
import { overseasWarehousePermissions as permission } from '../permissions'
const access = useAccessPreviewStore(); const warehouses = ref<ReceivingOverseasWarehouseDto[]>([]); const rows = ref<OverseasLocationDto[]>([])
const loading = ref(false); const contextLoading = ref(false); const loadError = ref<{ message: string } | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
const filters = ref<QueryModel>({ warehouseId: '', locationCode: '', locationType: '', status: '' }); const editorOpen = ref(false); const saving = ref(false); const editing = ref<OverseasLocationDto | null>(null)
const form = reactive({ warehouseId: '', locationCode: '', locationName: '', locationType: 'NORMAL' as const, maxWeightKg: null as number | null, status: 'ACTIVE' as 'ACTIVE' | 'INACTIVE', remark: '' })
const queryFields = computed<QueryField[]>(() => [
{ key: 'warehouseId', type: 'select', label: '海外仓', width: 260, options: warehouses.value.map(item => ({ value: String(item.id), label: `${item.warehouseName}(${item.warehouseCode})` })), props: { filterable: true, clearable: true, disabled: contextLoading.value } },
{ key: 'locationCode', type: 'input', label: '库位编码', width: 180 },
{ key: 'locationType', type: 'select', label: '类型', width: 150, options: [{ label: '普通库位', value: 'NORMAL' }, { label: 'A0共享溢出位', value: 'SHARED_OVERFLOW' }, { label: '虚拟收货位', value: 'VIRTUAL' }] },
{ key: 'status', type: 'select', label: '状态', width: 140, options: [{ label: '可用', value: 'ACTIVE' }, { label: '空闲', value: 'IDLE' }, { label: '已占用', value: 'OCCUPIED' }, { label: '已停用', value: 'DISABLED' }] },
])
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'locationCode', prop: 'locationCode', label: '库位编码', minWidth: 180 }, { key: 'warehouse', label: '海外仓', minWidth: 190, slotName: 'warehouse' }, { key: 'locationType', label: '类型', width: 130, slotName: 'locationType' }, { key: 'status', label: '状态', width: 110, slotName: 'status' }, { key: 'occupiedPalletCount', prop: 'occupiedPalletCount', label: '托盘数', width: 100, align: 'right' }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }]
const typeLabels: Record<OverseasLocationType, string> = { VIRTUAL: '虚拟收货位', NORMAL: '普通库位', SHARED_OVERFLOW: 'A0共享溢出位' }; const statusLabels: Record<OverseasLocationStatus, string> = { ACTIVE: '可用', IDLE: '空闲', OCCUPIED: '已占用', INACTIVE: '已停用', DISABLED: '已停用' }
const selectedWarehouse = computed(() => warehouses.value.find(item => String(item.id) === String(form.warehouseId)))
const warehouseNameById = computed(() => new Map(warehouses.value.map(item => [String(item.id), item.warehouseName])))
function isA0(row: OverseasLocationDto) { return row.sharedOverflow || row.locationType === 'SHARED_OVERFLOW' || row.locationCode.toUpperCase() === 'A0' }
function canEdit(row: OverseasLocationDto) { return access.can(permission.locationEdit) && !isA0(row) && row.locationType === 'NORMAL' && (row.status === 'ACTIVE' || row.status === 'INACTIVE' || row.status === 'DISABLED') }
function canStatus(row: OverseasLocationDto) { return access.can(permission.locationEdit) && !isA0(row) && row.locationType === 'NORMAL' && (row.status === 'ACTIVE' || row.status === 'INACTIVE' || row.status === 'DISABLED') }
function canDelete(row: OverseasLocationDto) { return access.can(permission.locationDelete) && !isA0(row) && row.status === 'DISABLED' && Number(row.occupiedPalletCount || 0) === 0 }
function actionError(error: unknown, fallback: string) { ElMessage.error(getApiErrorDetails(error, fallback).message) }
async function loadWarehouses() { contextLoading.value = true; try { warehouses.value = (await listReceivingOverseasWarehouses()).records; await load() } catch (error) { actionError(error, '海外仓列表加载失败') } finally { contextLoading.value = false } }
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); if (!warehouses.value.length) { rows.value = []; total.value = 0; return }; loading.value = true; loadError.value = null; try { const selectedWarehouseId = String(filters.value.warehouseId || ''); const result = await (selectedWarehouseId ? listOverseasLocations(selectedWarehouseId, { page: 1, size: 200, keyword: String(filters.value.locationCode || '').trim() || undefined, locationType: String(filters.value.locationType || '') as OverseasLocationType | '', status: (String(filters.value.status || '') === 'DISABLED' ? 'INACTIVE' : String(filters.value.status || '')) as OverseasLocationStatus | '' }) : listAllOverseasLocations(warehouses.value.map(item => String(item.id)), { keyword: String(filters.value.locationCode || '').trim() || undefined, locationType: String(filters.value.locationType || '') as OverseasLocationType | '', status: (String(filters.value.status || '') === 'DISABLED' ? 'INACTIVE' : String(filters.value.status || '')) as OverseasLocationStatus | '' })); const records = result.records.map(row => ({ ...row, warehouseName: row.warehouseName || warehouseNameById.value.get(String(row.warehouseId)) || null })); total.value = records.length; const start = (page.value - 1) * pageSize.value; rows.value = records.slice(start, start + pageSize.value) } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '海外库位加载失败') } finally { loading.value = false } }
function search(model: QueryModel) { page.value = 1; void load(model) }
function resetForm() { Object.assign(form, { warehouseId: String(filters.value.warehouseId || ''), locationCode: '', locationName: '', locationType: 'NORMAL', maxWeightKg: null, status: 'ACTIVE', remark: '' }); editing.value = null }
function openCreate() { resetForm(); editorOpen.value = true }
function openEdit(row: OverseasLocationDto) { if (!canEdit(row)) return; Object.assign(form, { warehouseId: row.warehouseId, locationCode: row.locationCode, locationName: row.locationName || '', locationType: 'NORMAL', maxWeightKg: row.maxLoadKg == null ? null : Number(row.maxLoadKg), status: row.status === 'DISABLED' ? 'INACTIVE' : 'ACTIVE', remark: row.remark || '' }); editing.value = row; editorOpen.value = true }
async function save() { if (!form.warehouseId || !form.locationCode.trim() || form.maxWeightKg == null) { ElMessage.warning('请填写海外仓、库位编码和最大承重'); return }; saving.value = true; try { const payload = { locationCode: form.locationCode.trim(), locationType: 'NORMAL' as const, maxLoadKg: form.maxWeightKg, status: form.status, remark: form.remark.trim() || null }; if (editing.value) await updateOverseasLocation(form.warehouseId, editing.value.id, { ...payload, version: editing.value.version }); else await createOverseasLocation(form.warehouseId, payload); editorOpen.value = false; await load() } catch (error) { actionError(error, '保存库位失败') } finally { saving.value = false } }
async function toggleStatus(row: OverseasLocationDto) { if (!canStatus(row)) return; const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; try { await ElMessageBox.confirm(`确认${next === 'ACTIVE' ? '启用' : '停用'}库位“${row.locationCode}”?`, '请确认'); await changeOverseasLocationStatus(row.warehouseId, row.id, { locationCode: row.locationCode, locationType: 'NORMAL', maxLoadKg: Number(row.maxLoadKg || 0), status: next, remark: row.remark || null, version: row.version }); await load() } catch (error) { if (error !== 'cancel' && error !== 'close') actionError(error, '更新库位状态失败') } }
async function remove(row: OverseasLocationDto) { if (!canDelete(row)) return; try { await ElMessageBox.confirm(`确认删除库位“${row.locationCode}”?`, '请确认', { type: 'warning' }); await deleteOverseasLocation(row.warehouseId, row.id, row.version); await load() } catch (error) { if (error !== 'cancel' && error !== 'close') actionError(error, '删除库位失败') } }
onMounted(() => { void loadWarehouses() })
</script>
<template><AdminPage class="admin-density"><ListPageLayout aria-label="海外仓库位管理"><section><DataTable preference-key="overseas-warehouse-locationsview" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" empty-text="暂无库位" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-start><h1>库位管理</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="queryFields" ariaLabel="海外库位筛选" preference-key="overseas-locations-query" @search="search" @reset="page = 1; load()" /></template>
<template #notices><LoadErrorState v-if="loadError" title="海外库位加载失败" :message="loadError.message" :loading="loading" @retry="load" /></template>
<template #toolbar-actions><el-button v-if="access.can(permission.locationCreate)" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template><template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button v-if="canEdit(row as OverseasLocationDto)" size="small" class="density-action" @click="openEdit(row as OverseasLocationDto)">编辑</el-button><el-button v-if="canStatus(row as OverseasLocationDto)" size="small" class="density-action" :data-tone="(row as OverseasLocationDto).status === 'ACTIVE' ? 'danger' : 'success'" @click="toggleStatus(row as OverseasLocationDto)">{{ (row as OverseasLocationDto).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="canDelete(row as OverseasLocationDto)" size="small" class="density-action" data-tone="danger" @click="remove(row as OverseasLocationDto)">删除</el-button></div></template><template #warehouse="{ row }">{{ (row as OverseasLocationDto).warehouseName || warehouseNameById.get(String((row as OverseasLocationDto).warehouseId)) || '—' }}</template><template #locationType="{ row }"><el-tag :type="isA0(row as OverseasLocationDto) ? 'warning' : 'info'">{{ typeLabels[(row as OverseasLocationDto).locationType] || '普通库位' }}</el-tag></template><template #status="{ row }"><el-tag>{{ statusLabels[(row as OverseasLocationDto).status] || '—' }}</el-tag></template><template #pagination-left>共 {{ total }} 个库位</template></DataTable></section></ListPageLayout><AppDialog v-model="editorOpen" :title="editing ? '编辑海外库位' : '新增海外库位'" width="min(560px, calc(100vw - 28px))" :confirm-loading="saving" :show-confirm="false"><el-form label-position="top" :disabled="saving"><div class="editor-grid"><el-form-item label="海外仓" required><el-select v-model="form.warehouseId" :disabled="Boolean(editing)" placeholder="请选择海外仓"><el-option v-for="item in warehouses" :key="item.id" :value="item.id" :label="`${item.warehouseName}(${item.warehouseCode})`" /></el-select></el-form-item><el-form-item label="库位编码" required><el-input v-model="form.locationCode" maxlength="50" placeholder="请输入库位编码" /></el-form-item><el-form-item label="库位名称"><el-input v-model="form.locationName" maxlength="100" placeholder="请输入库位名称(选填)" /></el-form-item><el-form-item label="最大承重(kg)" required><el-input-number v-model="form.maxWeightKg" :min="0" :precision="3" controls-position="right" placeholder="请输入最大承重" /></el-form-item></div><el-form-item label="备注"><el-input v-model="form.remark" maxlength="500" placeholder="请输入备注(选填)" /></el-form-item></el-form><template #footer><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="save"><Save :size="16" />保存库位</el-button></template></AppDialog></AdminPage></template>
<style scoped>.editor-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 14px}.editor-grid :deep(.el-select),.editor-grid :deep(.el-input-number){width:100%}@media(max-width:720px){.editor-grid{grid-template-columns:1fr}}</style>