新增 WMS 搬运规则与任务管理

支持搬运规则维护、候选预览、任务生成、预占、下发、取消和完成,并完善仓储管理前端交互。
This commit is contained in:
ArtoriasWu
2026-06-25 11:02:22 +08:00
parent 1cb8091bbe
commit 15405ba114
101 changed files with 4167704 additions and 152 deletions
@@ -2,10 +2,11 @@ import http from './http'
import type {
WarehouseArea, Storage, Container, Material,
ContainerLocation, ContainerMaterial, ContainerLocationHistory, ContainerMaterialHistory,
MasterDataPayload, StoragePayload, MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
MasterDataPayload, StoragePayload, MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload,
WmsTransportRule, WmsTransportTask, TransportRulePayload, TransportRequestQuery, TransportCandidatePreview
} from '@/types/wms'
function q(params?: Record<string, unknown>) {
function q<T extends object>(params?: T) {
return { params }
}
@@ -99,3 +100,46 @@ export async function listContainerMaterialHistory(containerId?: string, materia
const { data } = await http.get<ContainerMaterialHistory[]>('/wms/container-material-history', q({ containerId, materialId }))
return data
}
export async function listTransportRules(params?: { triggerType?: string; q?: string }) {
const { data } = await http.get<WmsTransportRule[]>('/wms/transport-rules', q(params))
return data
}
export async function saveTransportRule(payload: TransportRulePayload) {
const { data } = payload.id
? await http.put<WmsTransportRule>(`/wms/transport-rules/${payload.id}`, payload)
: await http.post<WmsTransportRule>('/wms/transport-rules', payload)
return data
}
export async function deleteTransportRule(id: string, version?: number) {
await http.delete(`/wms/transport-rules/${id}`, q({ version }))
}
export async function previewTransportCandidates(params: TransportRequestQuery) {
const { data } = await http.get<TransportCandidatePreview>('/wms/transport-candidates', q(params))
return data
}
export async function generateTransportTask(params: TransportRequestQuery) {
const { data } = await http.post<WmsTransportTask>('/wms/transport-tasks/generate', params)
return data
}
export async function listTransportTasks(params?: { status?: string; triggerType?: string }) {
const { data } = await http.get<WmsTransportTask[]>('/wms/transport-tasks', q(params))
return data
}
export async function reserveTransportTask(id: string, reason?: string) {
const { data } = await http.post<WmsTransportTask>(`/wms/transport-tasks/${id}/reserve`, null, q({ reason }))
return data
}
export async function cancelTransportTask(id: string, reason?: string) {
const { data } = await http.post<WmsTransportTask>(`/wms/transport-tasks/${id}/cancel`, null, q({ reason }))
return data
}
export async function completeTransportTask(id: string, reason?: string) {
const { data } = await http.post<WmsTransportTask>(`/wms/transport-tasks/${id}/complete`, null, q({ reason }))
return data
}
export async function dispatchTransportTask(id: string) {
const { data } = await http.post<WmsTransportTask>(`/wms/transport-tasks/${id}/dispatch`)
return data
}
@@ -36,7 +36,8 @@ export const ADMIN_MENU: NavMenuItem[] = [
children: [
{ path: '/admin/config/strategy', label: '调度策略', icon: SetUp, key: 'admin-config-strategy', group: '平台配置中心' },
{ path: '/admin/config/vehicle-hub', label: '车辆运维', icon: Van, key: 'admin-vehicle-hub', group: '平台配置中心' },
{ path: '/admin/config/facility', label: '设备与库位', icon: OfficeBuilding, key: 'admin-config-facility', group: '平台配置中心' },
{ path: '/admin/config/facility', label: '设备接入', icon: OfficeBuilding, key: 'admin-config-facility', group: '平台配置中心' },
{ path: '/admin/config/warehouse', label: '仓储管理', icon: OfficeBuilding, key: 'admin-config-warehouse', group: '平台配置中心' },
{ path: '/admin/config/business', label: '业务与集成', icon: Link, key: 'admin-config-business', group: '平台配置中心' },
{ path: '/admin/config/ops-center', label: '运维与回放', icon: VideoCamera, key: 'admin-config-ops-center', group: '平台配置中心' },
{ path: '/admin/config/system-center', label: '系统与权限', icon: User, key: 'admin-config-system-center', group: '平台配置中心' }
@@ -43,7 +43,7 @@ const routes: RouteRecordRaw[] = [
{ path: 'config/strategy', name: 'admin-config-strategy', component: () => import('@/views/admin/config/StrategyConfigView.vue'), meta: { title: '调度策略' } },
{ path: 'config/vehicle-hub', name: 'admin-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
{ path: 'config/facility', name: 'admin-config-facility', component: () => import('@/views/admin/config/FacilityConfigView.vue'), meta: { title: '设备接入' } },
{ path: 'config/warehouse', name: 'admin-config-warehouse', component: () => import('@/views/admin/WarehouseManagementView.vue'), meta: { title: '库位管理' } },
{ path: 'config/warehouse', name: 'admin-config-warehouse', component: () => import('@/views/admin/WarehouseManagementView.vue'), meta: { title: '仓储管理' } },
{ path: 'config/business', name: 'admin-config-business', component: () => import('@/views/admin/config/BusinessConfigView.vue'), meta: { title: '业务与集成' } },
{ path: 'config/ops-center', name: 'admin-config-ops-center', component: () => import('@/views/admin/config/OpsCenterView.vue'), meta: { title: '运维与回放' } },
{ path: 'config/system-center', name: 'admin-config-system-center', component: () => import('@/views/admin/config/SystemCenterView.vue'), meta: { title: '系统与权限' } },
@@ -28,6 +28,12 @@ export interface Storage extends EntityBase {
storageType: string
siteId: string
capacity: number
status: string
usage: string
priority: number
zoneCode: string
allowInbound: boolean
allowOutbound: boolean
enabled: boolean
}
@@ -132,6 +138,12 @@ export interface StoragePayload {
storageType: string
siteId: string
capacity: number
status: string
usage: string
priority: number
zoneCode: string
allowInbound: boolean
allowOutbound: boolean
enabled: boolean
isLock: boolean
remark: string
@@ -184,3 +196,95 @@ export interface ContainerMaterialPayload {
remark: string
extend: string
}
export type WmsTransportTriggerType = 'MaterialCall' | 'FinishedGoodsOffline' | 'AutoTransfer'
export type WmsTransportTaskStatus = 'Pending' | 'Reserved' | 'Dispatched' | 'InTransit' | 'Completed' | 'Failed' | 'Cancelled'
export interface WmsTransportRule extends EntityBase {
code: string
name: string
triggerType: WmsTransportTriggerType
enabled: boolean
priority: number
sourceSelectorJson: string
targetSelectorJson: string
taskOptionsJson: string
}
export interface WmsTransportTask extends EntityBase {
businessType: WmsTransportTriggerType
ruleId?: string
sourceStorageId: string
targetStorageId: string
containerId: string
materialId?: string
quantity?: number
status: WmsTransportTaskStatus
dispatchMissionId: string
deliveryId: string
dispatchStatus: string
reason: string
snapshotJson: string
errorMessage: string
taskPriority: number
}
export interface TransportRulePayload {
id?: string
version?: number
code: string
name: string
triggerType: WmsTransportTriggerType
enabled: boolean
priority: number
sourceSelectorJson: string
targetSelectorJson: string
taskOptionsJson: string
isLock: boolean
remark: string
extend: string
}
export interface TransportRequestQuery {
triggerType: WmsTransportTriggerType
ruleId?: string
requestSiteId?: string
materialId?: string
quantity?: number
containerId?: string
sourceStorageId?: string
targetStorageId?: string
batchNo?: string
priority?: number
reason?: string
}
export interface TransportCandidatePair {
ruleId: string
ruleCode: string
sourceStorageId: string
sourceStorageCode: string
targetStorageId: string
targetStorageCode: string
containerId: string
containerCode: string
materialId?: string
materialCode?: string
quantity?: number
score: number
rejectReasons: string[]
}
export interface RulePreviewResult {
ruleId: string
ruleCode: string
ruleName: string
candidateCount: number
rejectReasons: string[]
}
export interface TransportCandidatePreview {
request: TransportRequestQuery
candidates: TransportCandidatePair[]
ruleResults: RulePreviewResult[]
}
@@ -5,7 +5,7 @@
<div class="page-header">
<div>
<h2>仓储管理</h2>
<p>库区库位容器物料主数据以及容器位置和容器物料关系</p>
<p>库区库位容器物料主数据以及容器位置和搬运任务配置</p>
</div>
<el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
</div>
@@ -15,54 +15,82 @@
<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" />
<div class="toolbar">
<el-input v-model="keyword" placeholder="搜索编码/名称" clearable />
<el-button type="primary" @click="openArea()">新增库区</el-button>
<el-button type="danger" :disabled="batchAreas.batchCount === 0" @click="batchDeleteAreas">批量删除 ({{ batchAreas.batchCount }})</el-button>
</div>
<el-table :data="filteredAreas" border size="small" @selection-change="batchAreas.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchAreas.selectable" />
<el-table-column prop="code" label="编码" width="120" />
<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-column prop="type" label="类型" width="100" />
<el-table-column prop="sortOrder" label="排序" width="70" />
<el-table-column prop="enabled" label="启用" width="80"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column prop="isLock" label="锁定" width="80"><template #default="{ row }"><el-tag :type="row.isLock ? 'warning' : 'info'" size="small">{{ row.isLock ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="备注" show-overflow-tooltip><template #default="{ row }">{{ truncate(row.remark) }}</template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openArea(row)">编辑</el-button><el-button size="small" link 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>
<div class="toolbar">
<el-input v-model="keyword" placeholder="搜索编码/名称/站点" clearable />
<el-button type="primary" @click="openStorage()">新增库位</el-button>
<el-button type="danger" :disabled="batchStorages.batchCount === 0" @click="batchDeleteStorages">批量删除 ({{ batchStorages.batchCount }})</el-button>
</div>
<el-table :data="filteredStorages" border size="small" @selection-change="batchStorages.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchStorages.selectable" />
<el-table-column prop="code" label="编码" width="110" />
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column label="库区" width="100"><template #default="{ row }">{{ areaName(row.areaId) }}</template></el-table-column>
<el-table-column label="类型" width="100"><template #default="{ row }">{{ labelOf(STORAGE_TYPES, row.storageType) }}</template></el-table-column>
<el-table-column prop="siteId" label="站点" width="90" />
<el-table-column label="状态" width="90"><template #default="{ row }">{{ labelOf(STORAGE_STATUSES, row.status) }}</template></el-table-column>
<el-table-column prop="priority" label="优先级" width="70" />
<el-table-column prop="zoneCode" label="区域" width="80" />
<el-table-column label="占用" width="70"><template #default="{ row }"><el-tag :type="isStorageOccupied(row.id) ? 'warning' : 'success'" size="small">{{ isStorageOccupied(row.id) ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="入库" width="60"><template #default="{ row }">{{ row.allowInbound ? '是' : '否' }}</template></el-table-column>
<el-table-column label="出库" width="60"><template #default="{ row }">{{ row.allowOutbound ? '是' : '否' }}</template></el-table-column>
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openStorage(row)">编辑</el-button><el-button size="small" link 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" />
<div class="toolbar">
<el-input v-model="keyword" placeholder="搜索编码/名称" clearable />
<el-button type="primary" @click="openContainer()">新增容器</el-button>
<el-button type="danger" :disabled="batchContainers.batchCount === 0" @click="batchDeleteContainers">批量删除 ({{ batchContainers.batchCount }})</el-button>
</div>
<el-table :data="filteredContainers" border size="small" @selection-change="batchContainers.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchContainers.selectable" />
<el-table-column prop="code" label="编码" width="120" />
<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-column label="类型" width="90"><template #default="{ row }">{{ labelOf(CONTAINER_TYPES, row.containerType) }}</template></el-table-column>
<el-table-column label="状态" width="90"><template #default="{ row }">{{ labelOf(CONTAINER_STATUSES, row.status) }}</template></el-table-column>
<el-table-column label="当前位置" min-width="120"><template #default="{ row }">{{ containerLocationLabel(row.id) }}</template></el-table-column>
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column prop="isLock" label="锁定" width="70"><template #default="{ row }"><el-tag :type="row.isLock ? 'warning' : 'info'" size="small">{{ row.isLock ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openContainer(row)">编辑</el-button><el-button size="small" link 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" />
<div class="toolbar">
<el-input v-model="keyword" placeholder="搜索编码/名称/规格" clearable />
<el-button type="primary" @click="openMaterial()">新增物料</el-button>
<el-button type="danger" :disabled="batchMaterials.batchCount === 0" @click="batchDeleteMaterials">批量删除 ({{ batchMaterials.batchCount }})</el-button>
</div>
<el-table :data="filteredMaterials" border size="small" @selection-change="batchMaterials.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchMaterials.selectable" />
<el-table-column prop="code" label="编码" width="120" />
<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-column prop="unit" label="单位" width="70" />
<el-table-column prop="category" label="类别" width="100" />
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openMaterial(row)">编辑</el-button><el-button size="small" link type="danger" :disabled="row.isLock" @click="removeMaterial(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
@@ -75,38 +103,119 @@
<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>
<el-button type="danger" :disabled="batchLocations.batchCount === 0" @click="batchUnbindLocations">批量解绑 ({{ batchLocations.batchCount }})</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 :data="filteredContainerLocations" border size="small" @selection-change="batchLocations.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchLocations.selectable" />
<el-table-column label="容器" width="140"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
<el-table-column prop="locationType" label="位置类型" width="90" />
<el-table-column prop="locationCode" label="位置编码" width="120" />
<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-column label="状态" width="80"><template #default="{ row }">{{ labelOf(CONTAINER_LOCATION_STATUSES, row.status) }}</template></el-table-column>
<el-table-column prop="enteredAt" label="进入时间" width="170" />
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button size="small" link 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>
<div class="toolbar">
<el-input v-model="keyword" placeholder="搜索" clearable />
<el-button type="primary" @click="openContainerMaterial()">装料/调整</el-button>
<el-button type="danger" :disabled="batchContainerMaterials.batchCount === 0" @click="batchUnloadMaterials">批量卸料 ({{ batchContainerMaterials.batchCount }})</el-button>
</div>
<el-table :data="filteredContainerMaterials" border size="small" @selection-change="batchContainerMaterials.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchContainerMaterials.selectable" />
<el-table-column label="容器" width="140"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
<el-table-column label="物料" width="140"><template #default="{ row }">{{ materialName(row.materialId) }}</template></el-table-column>
<el-table-column prop="quantity" label="数量" width="90" />
<el-table-column prop="batchNo" label="批次" width="120" />
<el-table-column prop="serialNo" label="序列号" width="120" />
<el-table-column label="状态" width="90"><template #default="{ row }">{{ labelOf(CONTAINER_MATERIAL_STATUSES, row.status) }}</template></el-table-column>
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button size="small" link 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-column prop="kind" label="类型" width="80" />
<el-table-column prop="eventType" label="事件" width="100" />
<el-table-column label="容器" width="120"><template #default="{ row }">{{ row.containerId ? containerName(row.containerId) : '—' }}</template></el-table-column>
<el-table-column label="库位/位置" min-width="120"><template #default="{ row }">{{ historyLocationLabel(row) }}</template></el-table-column>
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column prop="operatedAt" label="时间" width="170" />
<el-table-column prop="reason" label="原因" show-overflow-tooltip />
</el-table>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane label="搬运配置" name="transport" lazy>
<el-tabs v-model="activeTransport" lazy>
<el-tab-pane label="搬运规则" name="rules">
<div class="toolbar">
<el-select v-model="ruleTriggerFilter" clearable placeholder="触发类型">
<el-option label="呼叫物料" value="MaterialCall" />
<el-option label="成品下线" value="FinishedGoodsOffline" />
<el-option label="自动转运" value="AutoTransfer" />
</el-select>
<el-button type="primary" @click="openRule()">新增规则</el-button>
<el-button type="danger" :disabled="batchRules.batchCount === 0" @click="batchDeleteRules">批量删除 ({{ batchRules.batchCount }})</el-button>
</div>
<el-table :data="filteredRules" border size="small" @selection-change="batchRules.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="batchRules.selectable" />
<el-table-column prop="code" label="编码" width="120" />
<el-table-column prop="name" label="名称" />
<el-table-column label="触发类型" width="120"><template #default="{ row }">{{ TRANSPORT_TRIGGER_LABELS[row.triggerType] ?? row.triggerType }}</template></el-table-column>
<el-table-column prop="priority" label="优先级" width="80" />
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openRule(row)">编辑</el-button><el-button size="small" link type="danger" :disabled="row.isLock" @click="removeRule(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="候选预览" name="candidates">
<el-form inline class="preview-form">
<el-form-item label="触发类型"><el-select v-model="previewForm.triggerType"><el-option label="呼叫物料" value="MaterialCall" /><el-option label="成品下线" value="FinishedGoodsOffline" /><el-option label="自动转运" value="AutoTransfer" /></el-select></el-form-item>
<el-form-item label="呼叫站点"><el-input v-model="previewForm.requestSiteId" placeholder="站点 ID" /></el-form-item>
<el-form-item label="物料"><el-select v-model="previewForm.materialId" clearable><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="previewForm.quantity" :min="0" :precision="4" /></el-form-item>
<el-form-item label="容器"><el-select v-model="previewForm.containerId" clearable><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :loading="previewLoading" @click="runPreview">预览候选</el-button><el-button type="success" :loading="previewLoading" @click="generateFromPreview">生成任务</el-button></el-form-item>
</el-form>
<el-table :data="previewResult?.candidates ?? []" border size="small">
<el-table-column prop="ruleCode" label="规则" width="110" />
<el-table-column prop="sourceStorageCode" label="起点库位" width="110" />
<el-table-column prop="targetStorageCode" label="终点库位" width="110" />
<el-table-column prop="containerCode" label="容器" width="100" />
<el-table-column prop="materialCode" label="物料" width="100" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column prop="score" label="评分" width="70" />
</el-table>
</el-tab-pane>
<el-tab-pane label="搬运任务" name="tasks">
<div class="toolbar">
<el-select v-model="taskStatusFilter" clearable placeholder="状态" @change="loadTransportTasks"><el-option v-for="s in taskStatuses" :key="s" :label="s" :value="s" /></el-select>
<el-button @click="loadTransportTasks">刷新</el-button>
<el-button type="danger" :disabled="batchTasks.batchCount === 0" @click="batchCancelTasks">批量取消 ({{ batchTasks.batchCount }})</el-button>
</div>
<el-table :data="transportTasks" border size="small" @selection-change="batchTasks.onSelectionChange">
<el-table-column type="selection" width="42" :selectable="taskSelectable" />
<el-table-column label="业务类型" width="110"><template #default="{ row }">{{ TRANSPORT_TRIGGER_LABELS[row.businessType] ?? row.businessType }}</template></el-table-column>
<el-table-column label="起点" width="100"><template #default="{ row }">{{ storageCode(row.sourceStorageId) }}</template></el-table-column>
<el-table-column label="终点" width="100"><template #default="{ row }">{{ storageCode(row.targetStorageId) }}</template></el-table-column>
<el-table-column label="容器" width="100"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="TRANSPORT_TASK_STATUS_TYPE[row.status] ?? 'info'" size="small">{{ row.status }}</el-tag></template></el-table-column>
<el-table-column prop="taskPriority" label="优先级" width="70" />
<el-table-column prop="createdAt" label="创建时间" width="170" />
<el-table-column prop="reason" label="原因" show-overflow-tooltip />
<el-table-column prop="errorMessage" label="错误" show-overflow-tooltip />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === 'Pending'" size="small" link @click="reserveTask(row)">预占</el-button>
<el-button v-if="row.status === 'Reserved'" size="small" link @click="completeTask(row)">完成</el-button>
<el-button v-if="['Pending','Reserved','Failed'].includes(row.status)" size="small" link type="danger" @click="cancelTask(row)">取消</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
@@ -114,55 +223,23 @@
</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>
<WmsEntityDialog
v-model:visible="dialogVisible"
:kind="dialogKind"
:saving="saving"
:areas="areas"
:storages="storages"
:containers="containers"
:materials="materials"
:occupied-storage-ids="occupiedStorageIds"
:master-form="masterForm"
:storage-form="storageForm"
:material-form="materialForm"
:location-form="locationForm"
:container-material-form="containerMaterialForm"
:rule-form="ruleForm"
@save="submitDialog"
/>
</div>
</template>
@@ -171,19 +248,30 @@ 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 WmsEntityDialog, { type DialogKind } from '@/views/admin/warehouse/WmsEntityDialog.vue'
import { useWmsBatchActions } from '@/views/admin/warehouse/useWmsBatchActions'
import {
STORAGE_TYPES, STORAGE_STATUSES, CONTAINER_TYPES, CONTAINER_STATUSES,
CONTAINER_LOCATION_STATUSES, CONTAINER_MATERIAL_STATUSES,
TRANSPORT_TRIGGER_LABELS, TRANSPORT_TASK_STATUS_TYPE, labelOf, truncate
} from '@/views/admin/warehouse/wmsOptions'
import type {
WarehouseArea, Storage, Container, Material, ContainerLocation, ContainerMaterial,
ContainerLocationHistory, ContainerMaterialHistory, MasterDataPayload, StoragePayload,
MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload,
WmsTransportRule, WmsTransportTask, TransportRulePayload, TransportRequestQuery, TransportCandidatePreview
} from '@/types/wms'
type DialogKind = 'area' | 'storage' | 'container' | 'material' | 'location' | 'containerMaterial'
const activeGroup = ref('master')
const activeMaster = ref('areas')
const activeRelation = ref('locations')
const activeTransport = ref('rules')
const keyword = ref('')
const locationFilter = ref('')
const ruleTriggerFilter = ref('')
const taskStatusFilter = ref('')
const previewLoading = ref(false)
const taskStatuses = ['Pending', 'Reserved', 'Dispatched', 'InTransit', 'Completed', 'Failed', 'Cancelled']
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
@@ -197,29 +285,33 @@ const containerLocations = ref<ContainerLocation[]>([])
const containerMaterials = ref<ContainerMaterial[]>([])
const locationHistory = ref<ContainerLocationHistory[]>([])
const materialHistory = ref<ContainerMaterialHistory[]>([])
const transportRules = ref<WmsTransportRule[]>([])
const transportTasks = ref<WmsTransportTask[]>([])
const previewResult = ref<TransportCandidatePreview | null>(null)
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 ruleForm = reactive<TransportRulePayload>(baseRule())
const previewForm = reactive<TransportRequestQuery>({ triggerType: 'MaterialCall', priority: 0, reason: '' })
const dialogTitle = computed(() => ({
area: '库区',
storage: '库位',
container: '容器',
material: '物料',
location: '容器位置',
containerMaterial: '容器物料'
}[dialogKind.value]))
const batchAreas = useWmsBatchActions<WarehouseArea>()
const batchStorages = useWmsBatchActions<Storage>()
const batchContainers = useWmsBatchActions<Container>()
const batchMaterials = useWmsBatchActions<Material>()
const batchRules = useWmsBatchActions<WmsTransportRule>()
const batchLocations = useWmsBatchActions<ContainerLocation>()
const batchContainerMaterials = useWmsBatchActions<ContainerMaterial>()
const batchTasks = useWmsBatchActions<WmsTransportTask>()
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 occupiedStorageIds = computed(() => {
const set = new Set<string>()
for (const loc of containerLocations.value) {
if (loc.locationType === 'Storage') set.add(loc.locationId)
}
return set
})
const filteredAreas = computed(() => filterRows(areas.value, keyword.value))
@@ -229,11 +321,18 @@ const filteredMaterials = computed(() => filterRows(materials.value, keyword.val
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 filteredContainerMaterials = computed(() => filterRows(containerMaterials.value, keyword.value))
const filteredRules = computed(() => transportRules.value.filter((r) => !ruleTriggerFilter.value || r.triggerType === ruleTriggerFilter.value))
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))))
function isStorageOccupied(id: string) { return occupiedStorageIds.value.has(id) }
function taskSelectable(row: WmsTransportTask) {
return !row.isLock && ['Pending', 'Reserved', 'Failed'].includes(row.status)
}
async function loadAll() {
loading.value = true
try {
@@ -243,6 +342,8 @@ async function loadAll() {
])
areas.value = a; storages.value = s; containers.value = c; materials.value = m
containerLocations.value = cl; containerMaterials.value = cm; locationHistory.value = lh; materialHistory.value = mh
await loadTransportRules()
await loadTransportTasks()
} finally {
loading.value = false
}
@@ -250,24 +351,60 @@ async function loadAll() {
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 openContainer(row?: Container) {
dialogKind.value = 'container'
Object.assign(masterForm, baseMaster(row))
if (row) masterForm.type = row.containerType
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 }
function openRule(row?: WmsTransportRule) { dialogKind.value = 'rule'; Object.assign(ruleForm, baseRule(row)); dialogVisible.value = true }
async function loadTransportRules() { transportRules.value = await wmsApi.listTransportRules() }
async function loadTransportTasks() { transportTasks.value = await wmsApi.listTransportTasks({ status: taskStatusFilter.value || undefined }) }
async function runPreview() {
previewLoading.value = true
try {
previewResult.value = await wmsApi.previewTransportCandidates({ ...previewForm })
ElMessage.success(`找到 ${previewResult.value.candidates.length} 个候选`)
} finally { previewLoading.value = false }
}
async function generateFromPreview() {
previewLoading.value = true
try {
await wmsApi.generateTransportTask({ ...previewForm })
ElMessage.success('任务已生成')
await loadTransportTasks()
activeGroup.value = 'transport'
activeTransport.value = 'tasks'
} finally { previewLoading.value = false }
}
async function submitDialog() {
saving.value = true
const kind = dialogKind.value
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)
JSON.parse((kind === 'storage' ? storageForm : kind === 'material' ? materialForm : kind === 'rule' ? ruleForm : masterForm).extend || '{}')
if (kind === 'area') await wmsApi.saveArea(masterForm)
else if (kind === 'container') await wmsApi.saveContainer(masterForm)
else if (kind === 'storage') await wmsApi.saveStorage(storageForm)
else if (kind === 'material') await wmsApi.saveMaterial(materialForm)
else if (kind === 'location') await wmsApi.saveContainerLocation(locationForm)
else if (kind === 'rule') {
JSON.parse(ruleForm.sourceSelectorJson || '{}')
JSON.parse(ruleForm.targetSelectorJson || '{}')
JSON.parse(ruleForm.taskOptionsJson || '{}')
await wmsApi.saveTransportRule(ruleForm)
}
else await wmsApi.saveContainerMaterial(containerMaterialForm)
ElMessage.success('已保存')
dialogVisible.value = false
await loadAll()
if (kind === 'rule') await loadTransportRules()
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : String(e))
} finally {
@@ -278,40 +415,107 @@ async function submitDialog() {
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() }
async function removeRule(row: WmsTransportRule) { await confirmDelete('删除该规则?'); await wmsApi.deleteTransportRule(row.id, row.version); await loadTransportRules() }
async function reserveTask(row: WmsTransportTask) { await wmsApi.reserveTransportTask(row.id); ElMessage.success('已预占'); await loadTransportTasks() }
async function completeTask(row: WmsTransportTask) { await wmsApi.completeTransportTask(row.id); ElMessage.success('已完成'); await loadAll() }
async function cancelTask(row: WmsTransportTask) { await wmsApi.cancelTransportTask(row.id); ElMessage.success('已取消'); await loadTransportTasks() }
async function batchDeleteAreas() {
const r = await batchAreas.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个库区?', run: (row) => wmsApi.deleteArea(row.id, row.version) })
if (r.ok > 0) await loadAll()
}
async function batchDeleteStorages() {
const r = await batchStorages.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个库位?', run: (row) => wmsApi.deleteStorage(row.id, row.version) })
if (r.ok > 0) await loadAll()
}
async function batchDeleteContainers() {
const r = await batchContainers.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个容器?', run: (row) => wmsApi.deleteContainer(row.id, row.version) })
if (r.ok > 0) await loadAll()
}
async function batchDeleteMaterials() {
const r = await batchMaterials.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个物料?', run: (row) => wmsApi.deleteMaterial(row.id, row.version) })
if (r.ok > 0) await loadAll()
}
async function batchDeleteRules() {
const r = await batchRules.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 条规则?', run: (row) => wmsApi.deleteTransportRule(row.id, row.version) })
if (r.ok > 0) await loadTransportRules()
}
async function batchUnbindLocations() {
const r = await batchLocations.runBatch({ actionLabel: '批量解绑', confirmMessage: '确认解绑选中的 {n} 条容器位置?', run: (row) => wmsApi.deleteContainerLocation(row.containerId) })
if (r.ok > 0) await loadAll()
}
async function batchUnloadMaterials() {
const r = await batchContainerMaterials.runBatch({ actionLabel: '批量卸料', confirmMessage: '确认卸载选中的 {n} 条容器物料?', run: (row) => wmsApi.deleteContainerMaterial(row.id) })
if (r.ok > 0) await loadAll()
}
async function batchCancelTasks() {
const r = await batchTasks.runBatch({
actionLabel: '批量取消',
confirmMessage: '确认取消选中的 {n} 个搬运任务?',
canRun: (row) => ['Pending', 'Reserved', 'Failed'].includes(row.status),
run: (row) => wmsApi.cancelTransportTask(row.id)
})
if (r.ok > 0) await loadTransportTasks()
}
function storageCode(id: string) { return storages.value.find((s) => s.id === id)?.code ?? id }
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 containerName(id: string) { const c = containers.value.find((x) => x.id === id); return c ? `${c.code}` : id }
function materialName(id: string) { const m = materials.value.find((x) => x.id === id); return m ? `${m.code}` : id }
function containerLocationLabel(id: string) {
const loc = containerLocations.value.find((x) => x.containerId === id)
return loc ? `${loc.locationType}:${loc.locationCode || loc.locationId}` : '未绑定'
}
function historyLocationLabel(row: ContainerLocationHistory | ContainerMaterialHistory) {
if ('toLocationId' in row && row.toLocationId) return `${row.toLocationType}:${row.toLocationId}`
if ('fromLocationId' in row && row.fromLocationId) return `${row.fromLocationType}:${row.fromLocationId}`
return '—'
}
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 baseCommon(row?: { id: string; version: number; isLock: boolean; remark: string; extend: string; updatedAt?: string }) {
return { id: row?.id, version: row?.version, isLock: row?.isLock ?? false, remark: row?.remark ?? '', extend: row?.extend ?? '{}', updatedAt: row?.updatedAt }
}
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 }
return {
...baseCommon(row),
code: row?.code ?? '',
name: row?.name ?? '',
type: 'type' in (row ?? {}) ? (row as WarehouseArea).type : (row as Container | undefined)?.containerType ?? 'Box',
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 }
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,
status: row?.status ?? 'Available', usage: row?.usage ?? '', priority: row?.priority ?? 0, zoneCode: row?.zoneCode ?? '',
allowInbound: row?.allowInbound ?? true, allowOutbound: row?.allowOutbound ?? true, enabled: row?.enabled ?? true
}
}
function baseRule(row?: WmsTransportRule): TransportRulePayload {
const defaultSource = JSON.stringify({ schemaVersion: 1, storage: { requireOccupied: true, allowOutbound: true }, container: { excludeReserved: true }, material: { statusIn: ['Loaded'] }, ranking: [{ field: 'enteredAt', direction: 'asc' }] }, null, 2)
const defaultTarget = JSON.stringify({ schemaVersion: 1, storage: { requireEmpty: true, allowInbound: true, requireSiteId: true }, ranking: [{ field: 'storagePriority', direction: 'desc' }] }, null, 2)
const defaultOptions = JSON.stringify({ autoReserve: true, requireManualConfirm: false, allowSameStorage: false, reservationTtlSeconds: 600, dispatchMode: 'Manual', maxCandidateCount: 20 }, null, 2)
return {
...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', triggerType: row?.triggerType ?? 'MaterialCall',
enabled: row?.enabled ?? true, priority: row?.priority ?? 0,
sourceSelectorJson: row?.sourceSelectorJson ?? defaultSource,
targetSelectorJson: row?.targetSelectorJson ?? defaultTarget,
taskOptionsJson: row?.taskOptionsJson ?? defaultOptions
}
}
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 }
@@ -332,7 +536,8 @@ onMounted(loadAll)
.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 { display: flex; gap: 10px; align-items: center; margin: 0 0 12px; flex-wrap: wrap; }
.toolbar .el-input { max-width: 260px; }
.toolbar .el-select { width: 160px; }
.preview-form { margin-bottom: 12px; flex-wrap: wrap; }
</style>
@@ -0,0 +1,309 @@
<template>
<el-dialog
:model-value="visible"
:title="title"
width="620px"
class="wms-entity-dialog"
append-to-body
@update:model-value="emit('update:visible', $event)"
>
<el-form ref="formRef" :model="formModel" :rules="rules" label-width="108px">
<!-- 库区 / 容器 -->
<template v-if="kind === 'area'">
<el-form-item label="编码" prop="code"><el-input v-model="masterForm.code" :disabled="isEditing" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="masterForm.name" /></el-form-item>
<el-form-item label="类型"><el-select v-model="masterForm.type"><el-option v-for="o in AREA_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="排序"><el-input-number v-model="masterForm.sortOrder" :min="0" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
</template>
<template v-else-if="kind === 'container'">
<el-form-item label="编码" prop="code"><el-input v-model="masterForm.code" :disabled="isEditing" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="masterForm.name" /></el-form-item>
<el-form-item label="容器类型"><el-select v-model="masterForm.type"><el-option v-for="o in CONTAINER_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="状态"><el-select v-model="masterForm.status"><el-option v-for="o in CONTAINER_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
</template>
<template v-else-if="kind === '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="编码" prop="code"><el-input v-model="storageForm.code" :disabled="isEditing" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="storageForm.name" /></el-form-item>
<el-form-item label="库位类型"><el-select v-model="storageForm.storageType"><el-option v-for="o in STORAGE_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="站点">
<el-select
v-model="storageForm.siteId"
filterable
clearable
:loading="sitesLoading"
placeholder="搜索站点 ID 或名称"
:filter-method="onSiteFilter"
class="wms-field-control"
>
<el-option v-for="s in displayedSites" :key="s.id" :label="s.label" :value="s.id" />
</el-select>
<span v-if="sitesLoadError" class="field-hint field-hint-warn">{{ sitesLoadError }}</span>
<span v-else class="field-hint">主数据来自 SimpleLite保存值为站点 id</span>
</el-form-item>
<el-form-item label="状态"><el-select v-model="storageForm.status"><el-option v-for="o in STORAGE_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="优先级"><el-input-number v-model="storageForm.priority" :min="0" /></el-form-item>
<el-form-item label="区域"><el-input v-model="storageForm.zoneCode" /></el-form-item>
<el-form-item label="容量"><el-input-number v-model="storageForm.capacity" :min="0" /><span class="field-hint">展示用途业务占用按 1 库位 1 容器</span></el-form-item>
<el-form-item label="允许入库"><el-switch v-model="storageForm.allowInbound" /></el-form-item>
<el-form-item label="允许出库"><el-switch v-model="storageForm.allowOutbound" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="storageForm.enabled" /></el-form-item>
</template>
<template v-else-if="kind === 'material'">
<el-form-item label="编码" prop="code"><el-input v-model="materialForm.code" :disabled="isEditing" /></el-form-item>
<el-form-item label="名称" prop="name"><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-select v-model="materialForm.unit" filterable allow-create><el-option v-for="o in MATERIAL_UNITS" :key="o.value" :label="o.label" :value="o.value" /></el-select></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>
</template>
<template v-else-if="kind === 'location'">
<el-form-item label="容器"><el-select v-model="locationForm.containerId" :disabled="isEditing"><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" :disabled="isEditing"><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 availableStorages" :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" :disabled="isEditing" /></el-form-item>
<el-form-item label="状态"><el-select v-model="locationForm.status"><el-option v-for="o in CONTAINER_LOCATION_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
<el-form-item label="原因" prop="reason"><el-input v-model="locationForm.reason" placeholder="绑定/转移原因" /></el-form-item>
</template>
<template v-else-if="kind === 'containerMaterial'">
<el-form-item label="容器"><el-select v-model="containerMaterialForm.containerId" :disabled="isEditing"><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" :disabled="isEditing"><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="数量" prop="quantity"><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" :disabled="isEditing" /></el-form-item>
<el-form-item label="序列号"><el-input v-model="containerMaterialForm.serialNo" :disabled="isEditing" /></el-form-item>
<el-form-item label="状态"><el-select v-model="containerMaterialForm.status"><el-option v-for="o in CONTAINER_MATERIAL_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
</template>
<template v-else-if="kind === 'rule'">
<el-form-item label="编码" prop="code"><el-input v-model="ruleForm.code" :disabled="isEditing" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="ruleForm.name" /></el-form-item>
<el-form-item label="触发类型"><el-select v-model="ruleForm.triggerType" :disabled="isEditing"><el-option label="呼叫物料" value="MaterialCall" /><el-option label="成品下线" value="FinishedGoodsOffline" /><el-option label="自动转运" value="AutoTransfer" /></el-select></el-form-item>
<el-form-item label="优先级"><el-input-number v-model="ruleForm.priority" :min="0" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="ruleForm.enabled" /></el-form-item>
<el-form-item label="起点 Selector"><el-input v-model="ruleForm.sourceSelectorJson" type="textarea" :rows="4" /></el-form-item>
<el-form-item label="终点 Selector"><el-input v-model="ruleForm.targetSelectorJson" type="textarea" :rows="4" /></el-form-item>
<el-form-item label="任务选项"><el-input v-model="ruleForm.taskOptionsJson" type="textarea" :rows="3" /></el-form-item>
</template>
<el-form-item label="备注"><el-input v-model="remark" type="textarea" :rows="2" /></el-form-item>
<el-collapse>
<el-collapse-item title="高级配置" name="advanced">
<el-form-item v-if="kind !== 'location' && kind !== 'containerMaterial'" label="锁定"><el-switch v-model="isLock" /></el-form-item>
<el-form-item label="扩展 JSON"><el-input v-model="extend" type="textarea" :rows="3" /></el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
<template #footer>
<el-button @click="emit('update:visible', false)">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import type {
WarehouseArea, Storage, Container, Material,
MasterDataPayload, StoragePayload, MaterialPayload,
ContainerLocationPayload, ContainerMaterialPayload, TransportRulePayload
} from '@/types/wms'
import {
AREA_TYPES, STORAGE_TYPES, STORAGE_STATUSES, CONTAINER_TYPES, CONTAINER_STATUSES,
CONTAINER_LOCATION_STATUSES, CONTAINER_MATERIAL_STATUSES, MATERIAL_UNITS
} from './wmsOptions'
import { useSimpleLiteSites } from './useSimpleLiteSites'
export type DialogKind = 'area' | 'storage' | 'container' | 'material' | 'location' | 'containerMaterial' | 'rule'
const props = defineProps<{
visible: boolean
kind: DialogKind
saving?: boolean
areas: WarehouseArea[]
storages: Storage[]
containers: Container[]
materials: Material[]
occupiedStorageIds: Set<string>
masterForm: MasterDataPayload
storageForm: StoragePayload
materialForm: MaterialPayload
locationForm: ContainerLocationPayload
containerMaterialForm: ContainerMaterialPayload
ruleForm: TransportRulePayload
}>()
const emit = defineEmits<{
'update:visible': [boolean]
save: []
}>()
const formRef = ref<FormInstance>()
const { sites: simpleLiteSites, loading: sitesLoading, loadError: sitesLoadError, load: loadSimpleLiteSites } = useSimpleLiteSites()
const siteSearch = ref('')
const siteOptionsWithCurrent = computed(() => {
const opts = [...simpleLiteSites.value]
const current = props.storageForm.siteId?.trim()
if (current && !opts.some((o) => o.id === current)) {
opts.unshift({ id: current, name: '', label: `#${current}(未在 SimpleLite 列表中)` })
}
return opts
})
const displayedSites = computed(() => {
const q = siteSearch.value.trim().toLowerCase()
if (!q) return siteOptionsWithCurrent.value
return siteOptionsWithCurrent.value.filter(
(s) => s.id.includes(q) || s.name.toLowerCase().includes(q) || s.label.toLowerCase().includes(q)
)
})
function onSiteFilter(query: string) {
siteSearch.value = query
}
const isEditing = computed(() => {
if (props.kind === 'storage') return !!props.storageForm.id
if (props.kind === 'material') return !!props.materialForm.id
if (props.kind === 'location') return !!props.locationForm.id
if (props.kind === 'containerMaterial') return !!props.containerMaterialForm.id
if (props.kind === 'rule') return !!props.ruleForm.id
return !!props.masterForm.id
})
const editCode = computed(() => {
if (props.kind === 'storage') return props.storageForm.code
if (props.kind === 'material') return props.materialForm.code
if (props.kind === 'rule') return props.ruleForm.code
return props.masterForm.code
})
const title = computed(() => {
const names: Record<DialogKind, string> = {
area: '库区', storage: '库位', container: '容器', material: '物料',
location: '容器位置', containerMaterial: '容器物料', rule: '搬运规则'
}
const code = editCode.value?.trim()
return isEditing.value && code ? `编辑${names[props.kind]} · ${code}` : `新增${names[props.kind]}`
})
const availableStorages = computed(() =>
props.storages.filter((s) => !props.occupiedStorageIds.has(s.id) || s.id === props.locationForm.locationId))
const formModel = computed(() => {
switch (props.kind) {
case 'storage': return props.storageForm
case 'material': return props.materialForm
case 'location': return props.locationForm
case 'containerMaterial': return props.containerMaterialForm
case 'rule': return props.ruleForm
default: return props.masterForm
}
})
const remark = computed({
get: () => formModel.value.remark,
set: (v: string) => { formModel.value.remark = v }
})
const extend = computed({
get: () => formModel.value.extend,
set: (v: string) => { formModel.value.extend = v }
})
const isLock = computed({
get: () => formModel.value.isLock,
set: (v: boolean) => { formModel.value.isLock = v }
})
const rules: FormRules = {
code: [{ required: true, message: '请输入编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
reason: [{ required: true, message: '请填写原因', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'change' }]
}
watch(() => props.visible, (v) => {
if (v) {
formRef.value?.clearValidate()
if (props.kind === 'storage') {
siteSearch.value = ''
void loadSimpleLiteSites()
}
}
})
async function submit() {
if (!formRef.value) return
await formRef.value.validate()
emit('save')
}
</script>
<style scoped>
.field-hint { display: block; margin-top: 4px; font-size: 12px; color: rgba(91, 33, 182, 0.65); }
.field-hint-warn { color: #c45656; }
/* el-dialog teleport 到 body,脱离 .mg-content 主题覆盖,需单独强化边框与背景对比 */
.wms-entity-dialog :deep(.el-form-item__label) {
color: rgba(45, 27, 105, 0.88) !important;
}
.wms-entity-dialog :deep(.el-input__wrapper),
.wms-entity-dialog :deep(.el-select .el-input__wrapper),
.wms-entity-dialog :deep(.el-input-number .el-input__wrapper) {
background: #faf8ff !important;
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.34) inset !important;
}
.wms-entity-dialog :deep(.el-input__wrapper:hover),
.wms-entity-dialog :deep(.el-select .el-input__wrapper:hover),
.wms-entity-dialog :deep(.el-input-number .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.52) inset !important;
}
.wms-entity-dialog :deep(.el-input__wrapper.is-focus),
.wms-entity-dialog :deep(.el-select .el-input__wrapper.is-focus),
.wms-entity-dialog :deep(.el-input-number .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #7c3aed inset, 0 0 0 3px rgba(124, 58, 237, 0.14) !important;
}
.wms-entity-dialog :deep(.el-input__inner),
.wms-entity-dialog :deep(.el-textarea__inner) {
color: #2d1b69 !important;
}
.wms-entity-dialog :deep(.el-input__inner::placeholder),
.wms-entity-dialog :deep(.el-textarea__inner::placeholder) {
color: rgba(91, 33, 182, 0.42) !important;
}
.wms-entity-dialog :deep(.el-textarea__inner) {
background: #faf8ff !important;
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.34) inset !important;
}
.wms-entity-dialog :deep(.el-input-number .el-input-number__decrease),
.wms-entity-dialog :deep(.el-input-number .el-input-number__increase) {
background: rgba(124, 58, 237, 0.08) !important;
color: #5b21b6 !important;
border-color: rgba(124, 58, 237, 0.22) !important;
}
.wms-entity-dialog :deep(.el-collapse) {
border-top: 1px solid rgba(124, 58, 237, 0.18);
border-bottom: 1px solid rgba(124, 58, 237, 0.18);
}
.wms-entity-dialog :deep(.el-collapse-item__header) {
color: #5b21b6 !important;
background: transparent !important;
border-bottom: 1px solid rgba(124, 58, 237, 0.12);
}
.wms-entity-dialog :deep(.el-collapse-item__wrap) {
border-bottom: 1px solid rgba(124, 58, 237, 0.12);
}
</style>
@@ -0,0 +1,55 @@
import { ref } from 'vue'
import { reflectionApi } from '@/api/reflection'
import { listSites } from '@/api/projection'
export interface SimpleLiteSiteOption {
id: string
name: string
label: string
}
export async function fetchSimpleLiteSites(): Promise<SimpleLiteSiteOption[]> {
try {
const rows = await reflectionApi.listObjects('site')
if (rows.length > 0) {
return rows
.map((s) => ({
id: String(s.id),
name: s.name || '',
label: `#${s.id} ${s.name || '站点'}`
}))
.sort((a, b) => Number(a.id) - Number(b.id) || a.name.localeCompare(b.name))
}
} catch {
/* 回退 projection API */
}
const rows = await listSites()
return rows
.map((s) => ({
id: String(s.id),
name: s.name || '',
label: `#${s.id} ${s.name || '站点'}`
}))
.sort((a, b) => Number(a.id) - Number(b.id) || a.name.localeCompare(b.name))
}
export function useSimpleLiteSites() {
const sites = ref<SimpleLiteSiteOption[]>([])
const loading = ref(false)
const loadError = ref('')
async function load() {
loading.value = true
loadError.value = ''
try {
sites.value = await fetchSimpleLiteSites()
} catch (e) {
loadError.value = e instanceof Error ? e.message : '加载 SimpleLite 站点失败'
sites.value = []
} finally {
loading.value = false
}
}
return { sites, loading, loadError, load }
}
@@ -0,0 +1,64 @@
import { computed, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
export interface BatchRow {
isLock?: boolean
}
export function useWmsBatchActions<T extends BatchRow>() {
const selected = ref<T[]>([])
const batchCount = computed(() => selected.value.filter((r) => !r.isLock).length)
function onSelectionChange(rows: T[]) {
selected.value = rows
}
function selectable(row: T) {
return !row.isLock
}
function clearSelection() {
selected.value = []
}
async function runBatch(opts: {
actionLabel: string
confirmMessage: string
rows?: T[]
canRun?: (row: T) => boolean
run: (row: T) => Promise<unknown>
}) {
const source = opts.rows ?? selected.value
const pool = source.filter((r) => !r.isLock && (opts.canRun?.(r as T) ?? true)) as T[]
if (pool.length === 0) {
ElMessage.warning('请先选择可操作的记录')
return { ok: 0, fail: 0 }
}
await ElMessageBox.confirm(opts.confirmMessage.replace('{n}', String(pool.length)), '确认操作', { type: 'warning' })
let ok = 0
let fail = 0
for (const row of pool) {
try {
await opts.run(row)
ok += 1
} catch {
fail += 1
}
}
clearSelection()
if (fail === 0) ElMessage.success(`${opts.actionLabel}完成:${ok}`)
else ElMessage.warning(`${opts.actionLabel}完成 ${ok} 条,失败 ${fail}`)
return { ok, fail }
}
return reactive({
selected,
batchCount,
onSelectionChange,
selectable,
clearSelection,
runBatch
})
}
@@ -0,0 +1,76 @@
export const AREA_TYPES = [
{ value: 'Storage', label: '存储区' },
{ value: 'Buffer', label: '缓存区' },
{ value: 'FinishedGoods', label: '成品区' }
]
export const STORAGE_TYPES = [
{ value: 'Storage', label: '普通库位' },
{ value: 'LineSide', label: '线边库' },
{ value: 'OfflinePoint', label: '下线点' },
{ value: 'Buffer', label: '缓存库位' },
{ value: 'FinishedGoods', label: '成品库' }
]
export const STORAGE_STATUSES = [
{ value: 'Available', label: '可用' },
{ value: 'Idle', label: '空闲' },
{ value: 'Occupied', label: '已占用' },
{ value: 'Disabled', label: '停用' }
]
export const CONTAINER_TYPES = [
{ value: 'Box', label: '料箱' },
{ value: 'Pallet', label: '托盘' }
]
export const CONTAINER_STATUSES = [
{ value: 'Idle', label: '空闲' },
{ value: 'Loaded', label: '已装载' },
{ value: 'Disabled', label: '停用' },
{ value: 'Exception', label: '异常' }
]
export const CONTAINER_LOCATION_STATUSES = [
{ value: 'Active', label: '正常' },
{ value: 'Locked', label: '锁定' },
{ value: 'Exception', label: '异常' }
]
export const CONTAINER_MATERIAL_STATUSES = [
{ value: 'Loaded', label: '已装载' },
{ value: 'Adjusted', label: '已调整' },
{ value: 'Frozen', label: '冻结' }
]
export const MATERIAL_UNITS = [
{ value: 'pcs', label: 'pcs' },
{ value: 'kg', label: 'kg' },
{ value: 'box', label: 'box' },
{ value: 'set', label: 'set' }
]
export const TRANSPORT_TRIGGER_LABELS: Record<string, string> = {
MaterialCall: '呼叫物料',
FinishedGoodsOffline: '成品下线',
AutoTransfer: '自动转运'
}
export const TRANSPORT_TASK_STATUS_TYPE: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
Pending: 'info',
Reserved: 'warning',
Dispatched: 'warning',
InTransit: 'warning',
Completed: 'success',
Failed: 'danger',
Cancelled: 'info'
}
export function labelOf(options: { value: string; label: string }[], value: string) {
return options.find((o) => o.value === value)?.label ?? value
}
export function truncate(text: string, max = 24) {
if (!text) return ''
return text.length <= max ? text : `${text.slice(0, max)}`
}