新增 WMS 搬运规则与任务管理
支持搬运规则维护、候选预览、任务生成、预占、下发、取消和完成,并完善仓储管理前端交互。
This commit is contained in:
@@ -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)}…`
|
||||
}
|
||||
Reference in New Issue
Block a user