引入WMS仓储主数据与关系管理全流程能力
后端实现基于EF Core的库区/库位/容器/物料/关系/历史等模型、服务与RESTful接口,支持多数据库Provider。前端新增类型、API与聚合页面,支持主数据及容器位置/物料关系的增删改查、绑定/解绑、装料/卸料、历史追溯。完善权限、菜单与文档,平台具备完整WMS能力。
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import http from './http'
|
||||
import type {
|
||||
WarehouseArea, Storage, Container, Material,
|
||||
ContainerLocation, ContainerMaterial, ContainerLocationHistory, ContainerMaterialHistory,
|
||||
MasterDataPayload, StoragePayload, MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
|
||||
} from '@/types/wms'
|
||||
|
||||
function q(params?: Record<string, unknown>) {
|
||||
return { params }
|
||||
}
|
||||
|
||||
export async function listAreas(keyword?: string) {
|
||||
const { data } = await http.get<WarehouseArea[]>('/wms/areas', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveArea(payload: MasterDataPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<WarehouseArea>(`/wms/areas/${payload.id}`, payload)
|
||||
: await http.post<WarehouseArea>('/wms/areas', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteArea(id: string, version?: number) {
|
||||
await http.delete(`/wms/areas/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listStorages(keyword?: string) {
|
||||
const { data } = await http.get<Storage[]>('/wms/storages', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveStorage(payload: StoragePayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<Storage>(`/wms/storages/${payload.id}`, payload)
|
||||
: await http.post<Storage>('/wms/storages', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteStorage(id: string, version?: number) {
|
||||
await http.delete(`/wms/storages/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listContainers(keyword?: string) {
|
||||
const { data } = await http.get<Container[]>('/wms/containers', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveContainer(payload: MasterDataPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<Container>(`/wms/containers/${payload.id}`, payload)
|
||||
: await http.post<Container>('/wms/containers', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteContainer(id: string, version?: number) {
|
||||
await http.delete(`/wms/containers/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listMaterials(keyword?: string) {
|
||||
const { data } = await http.get<Material[]>('/wms/materials', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveMaterial(payload: MaterialPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<Material>(`/wms/materials/${payload.id}`, payload)
|
||||
: await http.post<Material>('/wms/materials', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteMaterial(id: string, version?: number) {
|
||||
await http.delete(`/wms/materials/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listContainerLocations(params?: { locationType?: string; q?: string }) {
|
||||
const { data } = await http.get<ContainerLocation[]>('/wms/container-locations', q(params))
|
||||
return data
|
||||
}
|
||||
export async function saveContainerLocation(payload: ContainerLocationPayload) {
|
||||
const { data } = await http.post<ContainerLocation>('/wms/container-locations', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteContainerLocation(containerId: string, reason?: string) {
|
||||
await http.delete(`/wms/container-locations/${containerId}`, q({ reason }))
|
||||
}
|
||||
|
||||
export async function listContainerMaterials(keyword?: string) {
|
||||
const { data } = await http.get<ContainerMaterial[]>('/wms/container-materials', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveContainerMaterial(payload: ContainerMaterialPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<ContainerMaterial>(`/wms/container-materials/${payload.id}`, payload)
|
||||
: await http.post<ContainerMaterial>('/wms/container-materials', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteContainerMaterial(id: string, reason?: string) {
|
||||
await http.delete(`/wms/container-materials/${id}`, q({ reason }))
|
||||
}
|
||||
|
||||
export async function listContainerLocationHistory(containerId?: string) {
|
||||
const { data } = await http.get<ContainerLocationHistory[]>('/wms/container-location-history', q({ containerId }))
|
||||
return data
|
||||
}
|
||||
export async function listContainerMaterialHistory(containerId?: string, materialId?: string) {
|
||||
const { data } = await http.get<ContainerMaterialHistory[]>('/wms/container-material-history', q({ containerId, materialId }))
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
export interface EntityBase {
|
||||
id: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
isDeleted: boolean
|
||||
deletedAt?: string
|
||||
deletedBy: string
|
||||
version: number
|
||||
isLock: boolean
|
||||
createdBy: string
|
||||
updatedBy: string
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface WarehouseArea extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface Storage extends EntityBase {
|
||||
areaId: string
|
||||
code: string
|
||||
name: string
|
||||
storageType: string
|
||||
siteId: string
|
||||
capacity: number
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Container extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
containerType: string
|
||||
status: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Material extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ContainerLocationType = 'Storage' | 'Car'
|
||||
export type ContainerLocationStatus = 'Active' | 'Locked' | 'Exception'
|
||||
export type ContainerMaterialStatus = 'Loaded' | 'Unloaded' | 'Adjusted' | 'Frozen'
|
||||
|
||||
export interface ContainerLocation extends EntityBase {
|
||||
containerId: string
|
||||
locationType: ContainerLocationType
|
||||
locationId: string
|
||||
locationCode: string
|
||||
locationName: string
|
||||
status: ContainerLocationStatus
|
||||
enteredAt: string
|
||||
}
|
||||
|
||||
export interface ContainerMaterial extends EntityBase {
|
||||
containerId: string
|
||||
materialId: string
|
||||
quantity: number
|
||||
batchNo: string
|
||||
serialNo: string
|
||||
status: ContainerMaterialStatus
|
||||
loadedAt: string
|
||||
unloadedAt?: string
|
||||
}
|
||||
|
||||
export interface ContainerLocationHistory {
|
||||
id: string
|
||||
relationId?: string
|
||||
eventType: string
|
||||
beforeJson: string
|
||||
afterJson: string
|
||||
containerId?: string
|
||||
operator: string
|
||||
operatedAt: string
|
||||
source: string
|
||||
reason: string
|
||||
remark: string
|
||||
extend: string
|
||||
fromLocationType: string
|
||||
fromLocationId: string
|
||||
toLocationType: string
|
||||
toLocationId: string
|
||||
}
|
||||
|
||||
export interface ContainerMaterialHistory {
|
||||
id: string
|
||||
relationId?: string
|
||||
eventType: string
|
||||
beforeJson: string
|
||||
afterJson: string
|
||||
containerId?: string
|
||||
operator: string
|
||||
operatedAt: string
|
||||
source: string
|
||||
reason: string
|
||||
remark: string
|
||||
extend: string
|
||||
materialId?: string
|
||||
quantityDelta: number
|
||||
}
|
||||
|
||||
export interface MasterDataPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface StoragePayload {
|
||||
id?: string
|
||||
version?: number
|
||||
areaId: string
|
||||
code: string
|
||||
name: string
|
||||
storageType: string
|
||||
siteId: string
|
||||
capacity: number
|
||||
enabled: boolean
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface MaterialPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
code: string
|
||||
name: string
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
enabled: boolean
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface ContainerLocationPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
containerId: string
|
||||
locationType: ContainerLocationType
|
||||
locationId: string
|
||||
status: ContainerLocationStatus
|
||||
enteredAt?: string
|
||||
source: string
|
||||
reason: string
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface ContainerMaterialPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
containerId: string
|
||||
materialId: string
|
||||
quantity: number
|
||||
batchNo: string
|
||||
serialNo: string
|
||||
status: ContainerMaterialStatus
|
||||
loadedAt?: string
|
||||
unloadedAt?: string
|
||||
source: string
|
||||
reason: string
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="warehouse-page">
|
||||
<el-card shadow="never" class="warehouse-card">
|
||||
<template #header>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>仓储管理</h2>
|
||||
<p>库区、库位、容器、物料主数据,以及容器位置和容器物料关系。</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeGroup">
|
||||
<el-tab-pane label="基础资料" name="master">
|
||||
<el-tabs v-model="activeMaster" lazy>
|
||||
<el-tab-pane label="库区" name="areas">
|
||||
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称" clearable /><el-button type="primary" @click="openArea()">新增库区</el-button></div>
|
||||
<el-table :data="filteredAreas" border size="small">
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="type" label="类型" width="120" />
|
||||
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="isLock" label="锁定" width="90"><template #default="{ row }"><el-tag :type="row.isLock ? 'warning' : 'info'">{{ row.isLock ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openArea(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeArea(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="库位" name="storages">
|
||||
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称/站点" clearable /><el-button type="primary" @click="openStorage()">新增库位</el-button></div>
|
||||
<el-table :data="filteredStorages" border size="small">
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="库区" width="140"><template #default="{ row }">{{ areaName(row.areaId) }}</template></el-table-column>
|
||||
<el-table-column prop="storageType" label="类型" width="120" />
|
||||
<el-table-column prop="siteId" label="站点" width="100" />
|
||||
<el-table-column prop="capacity" label="容量" width="90" />
|
||||
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openStorage(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeStorage(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="容器" name="containers">
|
||||
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称" clearable /><el-button type="primary" @click="openContainer()">新增容器</el-button></div>
|
||||
<el-table :data="filteredContainers" border size="small">
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="containerType" label="类型" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="当前位置"><template #default="{ row }">{{ containerLocationLabel(row.id) }}</template></el-table-column>
|
||||
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openContainer(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeContainer(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="物料" name="materials">
|
||||
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称/规格" clearable /><el-button type="primary" @click="openMaterial()">新增物料</el-button></div>
|
||||
<el-table :data="filteredMaterials" border size="small">
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="spec" label="规格" />
|
||||
<el-table-column prop="unit" label="单位" width="90" />
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openMaterial(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeMaterial(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="关系与历史" name="relations" lazy>
|
||||
<el-tabs v-model="activeRelation" lazy>
|
||||
<el-tab-pane label="容器位置" name="locations">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="locationFilter" clearable placeholder="位置类型"><el-option label="库位" value="Storage" /><el-option label="车辆" value="Car" /></el-select>
|
||||
<el-input v-model="keyword" placeholder="搜索位置编码/名称" clearable />
|
||||
<el-button type="primary" @click="openLocation()">绑定/转移</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredContainerLocations" border size="small">
|
||||
<el-table-column label="容器" width="160"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
|
||||
<el-table-column prop="locationType" label="位置类型" width="100" />
|
||||
<el-table-column prop="locationCode" label="位置编码" width="140" />
|
||||
<el-table-column prop="locationName" label="位置名称" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="enteredAt" label="进入时间" width="180" />
|
||||
<el-table-column label="操作" width="120"><template #default="{ row }"><el-button size="small" type="danger" :disabled="row.isLock" @click="removeLocation(row)">解绑</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="容器物料" name="materials">
|
||||
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索" clearable /><el-button type="primary" @click="openContainerMaterial()">装料/调整</el-button></div>
|
||||
<el-table :data="containerMaterials" border size="small">
|
||||
<el-table-column label="容器" width="160"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
|
||||
<el-table-column label="物料" width="160"><template #default="{ row }">{{ materialName(row.materialId) }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="100" />
|
||||
<el-table-column prop="batchNo" label="批次" width="140" />
|
||||
<el-table-column prop="serialNo" label="序列号" width="140" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column label="操作" width="120"><template #default="{ row }"><el-button size="small" type="danger" :disabled="row.isLock" @click="removeContainerMaterial(row)">卸料</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="历史记录" name="history">
|
||||
<el-table :data="historyRows" border size="small">
|
||||
<el-table-column prop="kind" label="类型" width="110" />
|
||||
<el-table-column prop="eventType" label="事件" width="120" />
|
||||
<el-table-column prop="operator" label="操作人" width="120" />
|
||||
<el-table-column prop="operatedAt" label="时间" width="180" />
|
||||
<el-table-column prop="reason" label="原因" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-form label-width="96px">
|
||||
<template v-if="dialogKind === 'area' || dialogKind === 'container'">
|
||||
<el-form-item label="编码"><el-input v-model="masterForm.code" /></el-form-item>
|
||||
<el-form-item label="名称"><el-input v-model="masterForm.name" /></el-form-item>
|
||||
<el-form-item label="类型"><el-input v-model="masterForm.type" /></el-form-item>
|
||||
<el-form-item v-if="dialogKind === 'container'" label="状态"><el-input v-model="masterForm.status" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
|
||||
<el-form-item label="锁定"><el-switch v-model="masterForm.isLock" /></el-form-item>
|
||||
</template>
|
||||
<template v-if="dialogKind === 'storage'">
|
||||
<el-form-item label="库区"><el-select v-model="storageForm.areaId"><el-option v-for="a in areas" :key="a.id" :label="a.name" :value="a.id" /></el-select></el-form-item>
|
||||
<el-form-item label="编码"><el-input v-model="storageForm.code" /></el-form-item>
|
||||
<el-form-item label="名称"><el-input v-model="storageForm.name" /></el-form-item>
|
||||
<el-form-item label="类型"><el-input v-model="storageForm.storageType" /></el-form-item>
|
||||
<el-form-item label="站点"><el-input v-model="storageForm.siteId" /></el-form-item>
|
||||
<el-form-item label="容量"><el-input-number v-model="storageForm.capacity" :min="0" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="storageForm.enabled" /></el-form-item>
|
||||
<el-form-item label="锁定"><el-switch v-model="storageForm.isLock" /></el-form-item>
|
||||
</template>
|
||||
<template v-if="dialogKind === 'material'">
|
||||
<el-form-item label="编码"><el-input v-model="materialForm.code" /></el-form-item>
|
||||
<el-form-item label="名称"><el-input v-model="materialForm.name" /></el-form-item>
|
||||
<el-form-item label="规格"><el-input v-model="materialForm.spec" /></el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="materialForm.unit" /></el-form-item>
|
||||
<el-form-item label="类别"><el-input v-model="materialForm.category" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="materialForm.enabled" /></el-form-item>
|
||||
<el-form-item label="锁定"><el-switch v-model="materialForm.isLock" /></el-form-item>
|
||||
</template>
|
||||
<template v-if="dialogKind === 'location'">
|
||||
<el-form-item label="容器"><el-select v-model="locationForm.containerId"><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
|
||||
<el-form-item label="位置类型"><el-select v-model="locationForm.locationType"><el-option label="库位" value="Storage" /><el-option label="车辆" value="Car" /></el-select></el-form-item>
|
||||
<el-form-item v-if="locationForm.locationType === 'Storage'" label="库位"><el-select v-model="locationForm.locationId"><el-option v-for="s in storages" :key="s.id" :label="`${s.code} ${s.name}`" :value="s.id" /></el-select></el-form-item>
|
||||
<el-form-item v-else label="车辆 ID"><el-input v-model="locationForm.locationId" /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="locationForm.status"><el-option label="Active" value="Active" /><el-option label="Locked" value="Locked" /><el-option label="Exception" value="Exception" /></el-select></el-form-item>
|
||||
</template>
|
||||
<template v-if="dialogKind === 'containerMaterial'">
|
||||
<el-form-item label="容器"><el-select v-model="containerMaterialForm.containerId"><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
|
||||
<el-form-item label="物料"><el-select v-model="containerMaterialForm.materialId"><el-option v-for="m in materials" :key="m.id" :label="m.code" :value="m.id" /></el-select></el-form-item>
|
||||
<el-form-item label="数量"><el-input-number v-model="containerMaterialForm.quantity" :min="0.0001" :precision="4" /></el-form-item>
|
||||
<el-form-item label="批次"><el-input v-model="containerMaterialForm.batchNo" /></el-form-item>
|
||||
<el-form-item label="序列号"><el-input v-model="containerMaterialForm.serialNo" /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="containerMaterialForm.status"><el-option label="Loaded" value="Loaded" /><el-option label="Adjusted" value="Adjusted" /><el-option label="Frozen" value="Frozen" /></el-select></el-form-item>
|
||||
</template>
|
||||
<el-form-item label="备注"><el-input v-model="currentRemark" type="textarea" /></el-form-item>
|
||||
<el-form-item label="扩展 JSON"><el-input v-model="currentExtend" type="textarea" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitDialog">保存</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import * as wmsApi from '@/api/wms'
|
||||
import type {
|
||||
WarehouseArea, Storage, Container, Material, ContainerLocation, ContainerMaterial,
|
||||
ContainerLocationHistory, ContainerMaterialHistory, MasterDataPayload, StoragePayload,
|
||||
MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
|
||||
} from '@/types/wms'
|
||||
|
||||
type DialogKind = 'area' | 'storage' | 'container' | 'material' | 'location' | 'containerMaterial'
|
||||
|
||||
const activeGroup = ref('master')
|
||||
const activeMaster = ref('areas')
|
||||
const activeRelation = ref('locations')
|
||||
const keyword = ref('')
|
||||
const locationFilter = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogKind = ref<DialogKind>('area')
|
||||
|
||||
const areas = ref<WarehouseArea[]>([])
|
||||
const storages = ref<Storage[]>([])
|
||||
const containers = ref<Container[]>([])
|
||||
const materials = ref<Material[]>([])
|
||||
const containerLocations = ref<ContainerLocation[]>([])
|
||||
const containerMaterials = ref<ContainerMaterial[]>([])
|
||||
const locationHistory = ref<ContainerLocationHistory[]>([])
|
||||
const materialHistory = ref<ContainerMaterialHistory[]>([])
|
||||
|
||||
const masterForm = reactive<MasterDataPayload>(baseMaster())
|
||||
const storageForm = reactive<StoragePayload>(baseStorage())
|
||||
const materialForm = reactive<MaterialPayload>(baseMaterial())
|
||||
const locationForm = reactive<ContainerLocationPayload>(baseLocation())
|
||||
const containerMaterialForm = reactive<ContainerMaterialPayload>(baseContainerMaterial())
|
||||
|
||||
const dialogTitle = computed(() => ({
|
||||
area: '库区',
|
||||
storage: '库位',
|
||||
container: '容器',
|
||||
material: '物料',
|
||||
location: '容器位置',
|
||||
containerMaterial: '容器物料'
|
||||
}[dialogKind.value]))
|
||||
|
||||
const currentRemark = computed({
|
||||
get: () => formOf(dialogKind.value).remark,
|
||||
set: (v: string) => { formOf(dialogKind.value).remark = v }
|
||||
})
|
||||
const currentExtend = computed({
|
||||
get: () => formOf(dialogKind.value).extend,
|
||||
set: (v: string) => { formOf(dialogKind.value).extend = v }
|
||||
})
|
||||
|
||||
const filteredAreas = computed(() => filterRows(areas.value, keyword.value))
|
||||
const filteredStorages = computed(() => filterRows(storages.value, keyword.value))
|
||||
const filteredContainers = computed(() => filterRows(containers.value, keyword.value))
|
||||
const filteredMaterials = computed(() => filterRows(materials.value, keyword.value))
|
||||
const filteredContainerLocations = computed(() => containerLocations.value.filter((r) =>
|
||||
(!locationFilter.value || r.locationType === locationFilter.value) &&
|
||||
(!keyword.value || `${r.locationCode} ${r.locationName}`.toLowerCase().includes(keyword.value.toLowerCase()))))
|
||||
const historyRows = computed(() => [
|
||||
...locationHistory.value.map((x) => ({ ...x, kind: '位置' })),
|
||||
...materialHistory.value.map((x) => ({ ...x, kind: '物料' }))
|
||||
].sort((a, b) => String(b.operatedAt).localeCompare(String(a.operatedAt))))
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [a, s, c, m, cl, cm, lh, mh] = await Promise.all([
|
||||
wmsApi.listAreas(), wmsApi.listStorages(), wmsApi.listContainers(), wmsApi.listMaterials(),
|
||||
wmsApi.listContainerLocations(), wmsApi.listContainerMaterials(), wmsApi.listContainerLocationHistory(), wmsApi.listContainerMaterialHistory()
|
||||
])
|
||||
areas.value = a; storages.value = s; containers.value = c; materials.value = m
|
||||
containerLocations.value = cl; containerMaterials.value = cm; locationHistory.value = lh; materialHistory.value = mh
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openArea(row?: WarehouseArea) { dialogKind.value = 'area'; Object.assign(masterForm, baseMaster(row)); dialogVisible.value = true }
|
||||
function openStorage(row?: Storage) { dialogKind.value = 'storage'; Object.assign(storageForm, baseStorage(row)); dialogVisible.value = true }
|
||||
function openContainer(row?: Container) { dialogKind.value = 'container'; Object.assign(masterForm, baseMaster(row)); dialogVisible.value = true }
|
||||
function openMaterial(row?: Material) { dialogKind.value = 'material'; Object.assign(materialForm, baseMaterial(row)); dialogVisible.value = true }
|
||||
function openLocation(row?: ContainerLocation) { dialogKind.value = 'location'; Object.assign(locationForm, baseLocation(row)); dialogVisible.value = true }
|
||||
function openContainerMaterial(row?: ContainerMaterial) { dialogKind.value = 'containerMaterial'; Object.assign(containerMaterialForm, baseContainerMaterial(row)); dialogVisible.value = true }
|
||||
|
||||
async function submitDialog() {
|
||||
saving.value = true
|
||||
try {
|
||||
JSON.parse(currentExtend.value || '{}')
|
||||
if (dialogKind.value === 'area') await wmsApi.saveArea(masterForm)
|
||||
else if (dialogKind.value === 'container') await wmsApi.saveContainer(masterForm)
|
||||
else if (dialogKind.value === 'storage') await wmsApi.saveStorage(storageForm)
|
||||
else if (dialogKind.value === 'material') await wmsApi.saveMaterial(materialForm)
|
||||
else if (dialogKind.value === 'location') await wmsApi.saveContainerLocation(locationForm)
|
||||
else await wmsApi.saveContainerMaterial(containerMaterialForm)
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await loadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete(message: string) {
|
||||
await ElMessageBox.confirm(message, '确认操作', { type: 'warning' })
|
||||
}
|
||||
async function removeArea(row: WarehouseArea) { await confirmDelete('删除该库区?'); await wmsApi.deleteArea(row.id, row.version); await loadAll() }
|
||||
async function removeStorage(row: Storage) { await confirmDelete('删除该库位?'); await wmsApi.deleteStorage(row.id, row.version); await loadAll() }
|
||||
async function removeContainer(row: Container) { await confirmDelete('删除该容器?'); await wmsApi.deleteContainer(row.id, row.version); await loadAll() }
|
||||
async function removeMaterial(row: Material) { await confirmDelete('删除该物料?'); await wmsApi.deleteMaterial(row.id, row.version); await loadAll() }
|
||||
async function removeLocation(row: ContainerLocation) { await confirmDelete('解绑该容器位置?'); await wmsApi.deleteContainerLocation(row.containerId); await loadAll() }
|
||||
async function removeContainerMaterial(row: ContainerMaterial) { await confirmDelete('卸载该容器物料?'); await wmsApi.deleteContainerMaterial(row.id); await loadAll() }
|
||||
|
||||
function areaName(id: string) { return areas.value.find((a) => a.id === id)?.name ?? id }
|
||||
function containerName(id: string) { const c = containers.value.find((x) => x.id === id); return c ? `${c.code} ${c.name}` : id }
|
||||
function materialName(id: string) { const m = materials.value.find((x) => x.id === id); return m ? `${m.code} ${m.name}` : id }
|
||||
function containerLocationLabel(id: string) {
|
||||
const loc = containerLocations.value.find((x) => x.containerId === id)
|
||||
return loc ? `${loc.locationType}:${loc.locationCode || loc.locationId}` : '未绑定'
|
||||
}
|
||||
function filterRows<T extends Record<string, unknown>>(rows: T[], q: string) {
|
||||
if (!q) return rows
|
||||
const s = q.toLowerCase()
|
||||
return rows.filter((r) => Object.values(r).some((v) => String(v ?? '').toLowerCase().includes(s)))
|
||||
}
|
||||
function formOf(kind: DialogKind): { remark: string; extend: string } {
|
||||
if (kind === 'storage') return storageForm
|
||||
if (kind === 'material') return materialForm
|
||||
if (kind === 'location') return locationForm
|
||||
if (kind === 'containerMaterial') return containerMaterialForm
|
||||
return masterForm
|
||||
}
|
||||
function baseCommon(row?: { id: string; version: number; isLock: boolean; remark: string; extend: string }) {
|
||||
return { id: row?.id, version: row?.version, isLock: row?.isLock ?? false, remark: row?.remark ?? '', extend: row?.extend ?? '{}' }
|
||||
}
|
||||
function baseMaster(row?: WarehouseArea | Container): MasterDataPayload {
|
||||
return { ...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', type: 'type' in (row ?? {}) ? (row as WarehouseArea).type : (row as Container | undefined)?.containerType ?? '', status: (row as Container | undefined)?.status ?? 'Idle', enabled: row?.enabled ?? true, sortOrder: (row as WarehouseArea | undefined)?.sortOrder ?? 0 }
|
||||
}
|
||||
function baseStorage(row?: Storage): StoragePayload {
|
||||
return { ...baseCommon(row), areaId: row?.areaId ?? areas.value[0]?.id ?? '', code: row?.code ?? '', name: row?.name ?? '', storageType: row?.storageType ?? 'Storage', siteId: row?.siteId ?? '', capacity: row?.capacity ?? 0, enabled: row?.enabled ?? true }
|
||||
}
|
||||
function baseMaterial(row?: Material): MaterialPayload {
|
||||
return { ...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', spec: row?.spec ?? '', unit: row?.unit ?? 'pcs', category: row?.category ?? '', enabled: row?.enabled ?? true }
|
||||
}
|
||||
function baseLocation(row?: ContainerLocation): ContainerLocationPayload {
|
||||
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', locationType: row?.locationType ?? 'Storage', locationId: row?.locationId ?? '', status: row?.status ?? 'Active', enteredAt: row?.enteredAt, source: 'Manual', reason: '' }
|
||||
}
|
||||
function baseContainerMaterial(row?: ContainerMaterial): ContainerMaterialPayload {
|
||||
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', materialId: row?.materialId ?? materials.value[0]?.id ?? '', quantity: row?.quantity ?? 1, batchNo: row?.batchNo ?? '', serialNo: row?.serialNo ?? '', status: row?.status ?? 'Loaded', loadedAt: row?.loadedAt, source: 'Manual', reason: '' }
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.warehouse-page { padding: 16px; height: 100%; overflow: auto; }
|
||||
.warehouse-card { min-height: 100%; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
|
||||
.page-header h2 { margin: 0 0 4px; font-size: 20px; color: var(--mg-text-light); }
|
||||
.page-header p { margin: 0; color: var(--mg-text-muted); font-size: 13px; }
|
||||
.toolbar { display: flex; gap: 10px; align-items: center; margin: 0 0 12px; }
|
||||
.toolbar .el-input { max-width: 260px; }
|
||||
.toolbar .el-select { width: 160px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user