This commit is contained in:
zhaowei.huang
2026-06-23 13:50:25 +08:00
45 changed files with 3126 additions and 35 deletions
-10
View File
@@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}
@@ -0,0 +1,101 @@
import http from './http'
import type {
WarehouseArea, Storage, Container, Material,
ContainerLocation, ContainerMaterial, ContainerLocationHistory, ContainerMaterialHistory,
MasterDataPayload, StoragePayload, MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
} from '@/types/wms'
function q(params?: Record<string, unknown>) {
return { params }
}
export async function listAreas(keyword?: string) {
const { data } = await http.get<WarehouseArea[]>('/wms/areas', q({ q: keyword }))
return data
}
export async function saveArea(payload: MasterDataPayload) {
const { data } = payload.id
? await http.put<WarehouseArea>(`/wms/areas/${payload.id}`, payload)
: await http.post<WarehouseArea>('/wms/areas', payload)
return data
}
export async function deleteArea(id: string, version?: number) {
await http.delete(`/wms/areas/${id}`, q({ version }))
}
export async function listStorages(keyword?: string) {
const { data } = await http.get<Storage[]>('/wms/storages', q({ q: keyword }))
return data
}
export async function saveStorage(payload: StoragePayload) {
const { data } = payload.id
? await http.put<Storage>(`/wms/storages/${payload.id}`, payload)
: await http.post<Storage>('/wms/storages', payload)
return data
}
export async function deleteStorage(id: string, version?: number) {
await http.delete(`/wms/storages/${id}`, q({ version }))
}
export async function listContainers(keyword?: string) {
const { data } = await http.get<Container[]>('/wms/containers', q({ q: keyword }))
return data
}
export async function saveContainer(payload: MasterDataPayload) {
const { data } = payload.id
? await http.put<Container>(`/wms/containers/${payload.id}`, payload)
: await http.post<Container>('/wms/containers', payload)
return data
}
export async function deleteContainer(id: string, version?: number) {
await http.delete(`/wms/containers/${id}`, q({ version }))
}
export async function listMaterials(keyword?: string) {
const { data } = await http.get<Material[]>('/wms/materials', q({ q: keyword }))
return data
}
export async function saveMaterial(payload: MaterialPayload) {
const { data } = payload.id
? await http.put<Material>(`/wms/materials/${payload.id}`, payload)
: await http.post<Material>('/wms/materials', payload)
return data
}
export async function deleteMaterial(id: string, version?: number) {
await http.delete(`/wms/materials/${id}`, q({ version }))
}
export async function listContainerLocations(params?: { locationType?: string; q?: string }) {
const { data } = await http.get<ContainerLocation[]>('/wms/container-locations', q(params))
return data
}
export async function saveContainerLocation(payload: ContainerLocationPayload) {
const { data } = await http.post<ContainerLocation>('/wms/container-locations', payload)
return data
}
export async function deleteContainerLocation(containerId: string, reason?: string) {
await http.delete(`/wms/container-locations/${containerId}`, q({ reason }))
}
export async function listContainerMaterials(keyword?: string) {
const { data } = await http.get<ContainerMaterial[]>('/wms/container-materials', q({ q: keyword }))
return data
}
export async function saveContainerMaterial(payload: ContainerMaterialPayload) {
const { data } = payload.id
? await http.put<ContainerMaterial>(`/wms/container-materials/${payload.id}`, payload)
: await http.post<ContainerMaterial>('/wms/container-materials', payload)
return data
}
export async function deleteContainerMaterial(id: string, reason?: string) {
await http.delete(`/wms/container-materials/${id}`, q({ reason }))
}
export async function listContainerLocationHistory(containerId?: string) {
const { data } = await http.get<ContainerLocationHistory[]>('/wms/container-location-history', q({ containerId }))
return data
}
export async function listContainerMaterialHistory(containerId?: string, materialId?: string) {
const { data } = await http.get<ContainerMaterialHistory[]>('/wms/container-material-history', q({ containerId, materialId }))
return data
}
@@ -75,8 +75,9 @@ function onDragStart(e: DragEvent, type: string) {
<style scoped>
.node-palette {
height: 100%;
overflow: auto;
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
@@ -34,7 +34,9 @@
@delete="onDeleteWorkflow"
/>
<div class="wf-panel-title">控件库</div>
<NodePalette class="wf-palette-body" />
<div class="wf-palette-body">
<NodePalette />
</div>
</aside>
<main class="wf-panel wf-panel-center">
@@ -362,7 +364,8 @@ async function onImportJson() {
}
const warnCount = prepared.reduce(
(sum, w) => sum + validateWorkflow(w).filter((i) => i.level === 'warning').length,
(sum, w) =>
sum + validateWorkflow(w).filter((i) => i.level === 'warning').length,
0
)
if (warnCount > 0) {
@@ -657,6 +660,8 @@ async function onClearCanvas() {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.wf-panel-center {
flex: 1;
@@ -0,0 +1,186 @@
export interface EntityBase {
id: string
createdAt: string
updatedAt: string
isDeleted: boolean
deletedAt?: string
deletedBy: string
version: number
isLock: boolean
createdBy: string
updatedBy: string
remark: string
extend: string
}
export interface WarehouseArea extends EntityBase {
code: string
name: string
type: string
enabled: boolean
sortOrder: number
}
export interface Storage extends EntityBase {
areaId: string
code: string
name: string
storageType: string
siteId: string
capacity: number
enabled: boolean
}
export interface Container extends EntityBase {
code: string
name: string
containerType: string
status: string
enabled: boolean
}
export interface Material extends EntityBase {
code: string
name: string
spec: string
unit: string
category: string
enabled: boolean
}
export type ContainerLocationType = 'Storage' | 'Car'
export type ContainerLocationStatus = 'Active' | 'Locked' | 'Exception'
export type ContainerMaterialStatus = 'Loaded' | 'Unloaded' | 'Adjusted' | 'Frozen'
export interface ContainerLocation extends EntityBase {
containerId: string
locationType: ContainerLocationType
locationId: string
locationCode: string
locationName: string
status: ContainerLocationStatus
enteredAt: string
}
export interface ContainerMaterial extends EntityBase {
containerId: string
materialId: string
quantity: number
batchNo: string
serialNo: string
status: ContainerMaterialStatus
loadedAt: string
unloadedAt?: string
}
export interface ContainerLocationHistory {
id: string
relationId?: string
eventType: string
beforeJson: string
afterJson: string
containerId?: string
operator: string
operatedAt: string
source: string
reason: string
remark: string
extend: string
fromLocationType: string
fromLocationId: string
toLocationType: string
toLocationId: string
}
export interface ContainerMaterialHistory {
id: string
relationId?: string
eventType: string
beforeJson: string
afterJson: string
containerId?: string
operator: string
operatedAt: string
source: string
reason: string
remark: string
extend: string
materialId?: string
quantityDelta: number
}
export interface MasterDataPayload {
id?: string
version?: number
code: string
name: string
type: string
status: string
enabled: boolean
sortOrder: number
isLock: boolean
remark: string
extend: string
}
export interface StoragePayload {
id?: string
version?: number
areaId: string
code: string
name: string
storageType: string
siteId: string
capacity: number
enabled: boolean
isLock: boolean
remark: string
extend: string
}
export interface MaterialPayload {
id?: string
version?: number
code: string
name: string
spec: string
unit: string
category: string
enabled: boolean
isLock: boolean
remark: string
extend: string
}
export interface ContainerLocationPayload {
id?: string
version?: number
containerId: string
locationType: ContainerLocationType
locationId: string
status: ContainerLocationStatus
enteredAt?: string
source: string
reason: string
isLock: boolean
remark: string
extend: string
}
export interface ContainerMaterialPayload {
id?: string
version?: number
containerId: string
materialId: string
quantity: number
batchNo: string
serialNo: string
status: ContainerMaterialStatus
loadedAt?: string
unloadedAt?: string
source: string
reason: string
isLock: boolean
remark: string
extend: string
}
@@ -0,0 +1,31 @@
<template>
<div class="mission-page">
<ReflectionManagerPanel
kind="process"
kind-label="任务"
title="任务编排(Mission / 进程,含插件 MissionType 实例化)"
empty-text="当前没有任务点击右上角新建任务从已加载的 MissionType 中选一个实例化"
/>
</div>
</template>
<script setup lang="ts">
/**
* /admin/missions 路由的页面入口。
*
* 由于「任务」与「进程」在后端 ReflectionApiController 共用 kind=process / mission
* UiDiscoveryCache.MissionCreatableTypes 同源),这里直接用 ReflectionManagerPanel
* 接入,按下「新建任务」会列出所有 MissionType 子类(含 taskflow / trigger)。
*
* 旧版本的 DataTablePro + listMissions mock 已经下线,所有数据走 SimpleLite 反射 API。
*/
import ReflectionManagerPanel from '@/components/reflection/ReflectionManagerPanel.vue'
</script>
<style scoped>
.mission-page {
height: calc(100vh - 56px);
padding: 12px 14px;
box-sizing: border-box;
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="orchestration-page">
<el-tabs v-model="tab" class="orch-tabs" type="border-card" @tab-change="onTabChange">
<el-tab-pane label="车辆" name="cars">
<CarPanelView v-if="tab === 'cars'" />
</el-tab-pane>
<el-tab-pane label="进程" name="process">
<MissionEditorView v-if="tab === 'process'" />
</el-tab-pane>
<el-tab-pane label="脚本" name="script">
<ReflectionKindTable
v-if="tab === 'script'"
kind="script"
empty-text="脚本继承 CarProgram 的子类型未发现可暴露条目"
/>
</el-tab-pane>
<el-tab-pane label="任务流" name="taskflow">
<ReflectionKindTable
v-if="tab === 'taskflow'"
kind="mission"
extra-filter="taskflow"
empty-text="未发现任务流条目"
/>
</el-tab-pane>
<el-tab-pane label="触发器" name="trigger">
<ReflectionKindTable
v-if="tab === 'trigger'"
kind="mission"
extra-filter="trigger"
empty-text="未发现触发器条目"
/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import CarPanelView from '@/views/admin/CarPanelView.vue'
import MissionEditorView from '@/views/admin/MissionEditorView.vue'
import ReflectionKindTable from '@/components/orchestration/ReflectionKindTable.vue'
const tab = ref<'cars' | 'process' | 'script' | 'taskflow' | 'trigger'>('cars')
function onTabChange(name: string | number) {
tab.value = name as typeof tab.value
}
onMounted(() => {
const cached = sessionStorage.getItem('orchestration.tab')
if (cached) tab.value = cached as typeof tab.value
})
</script>
<style scoped>
.orchestration-page {
height: calc(100vh - 56px);
padding: 12px;
}
.orch-tabs {
height: 100%;
}
.orch-tabs :deep(.el-tabs__content) {
height: calc(100% - 40px);
overflow: auto;
}
</style>
@@ -0,0 +1,338 @@
<template>
<div class="warehouse-page">
<el-card shadow="never" class="warehouse-card">
<template #header>
<div class="page-header">
<div>
<h2>仓储管理</h2>
<p>库区库位容器物料主数据以及容器位置和容器物料关系</p>
</div>
<el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
</div>
</template>
<el-tabs v-model="activeGroup">
<el-tab-pane label="基础资料" name="master">
<el-tabs v-model="activeMaster" lazy>
<el-tab-pane label="库区" name="areas">
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称" clearable /><el-button type="primary" @click="openArea()">新增库区</el-button></div>
<el-table :data="filteredAreas" border size="small">
<el-table-column prop="code" label="编码" width="140" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column prop="isLock" label="锁定" width="90"><template #default="{ row }"><el-tag :type="row.isLock ? 'warning' : 'info'">{{ row.isLock ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openArea(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeArea(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="库位" name="storages">
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称/站点" clearable /><el-button type="primary" @click="openStorage()">新增库位</el-button></div>
<el-table :data="filteredStorages" border size="small">
<el-table-column prop="code" label="编码" width="140" />
<el-table-column prop="name" label="名称" />
<el-table-column label="库区" width="140"><template #default="{ row }">{{ areaName(row.areaId) }}</template></el-table-column>
<el-table-column prop="storageType" label="类型" width="120" />
<el-table-column prop="siteId" label="站点" width="100" />
<el-table-column prop="capacity" label="容量" width="90" />
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openStorage(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeStorage(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="容器" name="containers">
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称" clearable /><el-button type="primary" @click="openContainer()">新增容器</el-button></div>
<el-table :data="filteredContainers" border size="small">
<el-table-column prop="code" label="编码" width="140" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="containerType" label="类型" width="120" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column label="当前位置"><template #default="{ row }">{{ containerLocationLabel(row.id) }}</template></el-table-column>
<el-table-column prop="enabled" label="启用" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openContainer(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeContainer(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="物料" name="materials">
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索编码/名称/规格" clearable /><el-button type="primary" @click="openMaterial()">新增物料</el-button></div>
<el-table :data="filteredMaterials" border size="small">
<el-table-column prop="code" label="编码" width="140" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="spec" label="规格" />
<el-table-column prop="unit" label="单位" width="90" />
<el-table-column prop="category" label="类别" width="120" />
<el-table-column label="操作" width="170"><template #default="{ row }"><el-button size="small" :disabled="row.isLock" @click="openMaterial(row)">编辑</el-button><el-button size="small" type="danger" :disabled="row.isLock" @click="removeMaterial(row)">删除</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane label="关系与历史" name="relations" lazy>
<el-tabs v-model="activeRelation" lazy>
<el-tab-pane label="容器位置" name="locations">
<div class="toolbar">
<el-select v-model="locationFilter" clearable placeholder="位置类型"><el-option label="库位" value="Storage" /><el-option label="车辆" value="Car" /></el-select>
<el-input v-model="keyword" placeholder="搜索位置编码/名称" clearable />
<el-button type="primary" @click="openLocation()">绑定/转移</el-button>
</div>
<el-table :data="filteredContainerLocations" border size="small">
<el-table-column label="容器" width="160"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
<el-table-column prop="locationType" label="位置类型" width="100" />
<el-table-column prop="locationCode" label="位置编码" width="140" />
<el-table-column prop="locationName" label="位置名称" />
<el-table-column prop="status" label="状态" width="110" />
<el-table-column prop="enteredAt" label="进入时间" width="180" />
<el-table-column label="操作" width="120"><template #default="{ row }"><el-button size="small" type="danger" :disabled="row.isLock" @click="removeLocation(row)">解绑</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="容器物料" name="materials">
<div class="toolbar"><el-input v-model="keyword" placeholder="搜索" clearable /><el-button type="primary" @click="openContainerMaterial()">装料/调整</el-button></div>
<el-table :data="containerMaterials" border size="small">
<el-table-column label="容器" width="160"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
<el-table-column label="物料" width="160"><template #default="{ row }">{{ materialName(row.materialId) }}</template></el-table-column>
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="batchNo" label="批次" width="140" />
<el-table-column prop="serialNo" label="序列号" width="140" />
<el-table-column prop="status" label="状态" width="110" />
<el-table-column label="操作" width="120"><template #default="{ row }"><el-button size="small" type="danger" :disabled="row.isLock" @click="removeContainerMaterial(row)">卸料</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="历史记录" name="history">
<el-table :data="historyRows" border size="small">
<el-table-column prop="kind" label="类型" width="110" />
<el-table-column prop="eventType" label="事件" width="120" />
<el-table-column prop="operator" label="操作人" width="120" />
<el-table-column prop="operatedAt" label="时间" width="180" />
<el-table-column prop="reason" label="原因" />
</el-table>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
<el-form label-width="96px">
<template v-if="dialogKind === 'area' || dialogKind === 'container'">
<el-form-item label="编码"><el-input v-model="masterForm.code" /></el-form-item>
<el-form-item label="名称"><el-input v-model="masterForm.name" /></el-form-item>
<el-form-item label="类型"><el-input v-model="masterForm.type" /></el-form-item>
<el-form-item v-if="dialogKind === 'container'" label="状态"><el-input v-model="masterForm.status" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
<el-form-item label="锁定"><el-switch v-model="masterForm.isLock" /></el-form-item>
</template>
<template v-if="dialogKind === 'storage'">
<el-form-item label="库区"><el-select v-model="storageForm.areaId"><el-option v-for="a in areas" :key="a.id" :label="a.name" :value="a.id" /></el-select></el-form-item>
<el-form-item label="编码"><el-input v-model="storageForm.code" /></el-form-item>
<el-form-item label="名称"><el-input v-model="storageForm.name" /></el-form-item>
<el-form-item label="类型"><el-input v-model="storageForm.storageType" /></el-form-item>
<el-form-item label="站点"><el-input v-model="storageForm.siteId" /></el-form-item>
<el-form-item label="容量"><el-input-number v-model="storageForm.capacity" :min="0" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="storageForm.enabled" /></el-form-item>
<el-form-item label="锁定"><el-switch v-model="storageForm.isLock" /></el-form-item>
</template>
<template v-if="dialogKind === 'material'">
<el-form-item label="编码"><el-input v-model="materialForm.code" /></el-form-item>
<el-form-item label="名称"><el-input v-model="materialForm.name" /></el-form-item>
<el-form-item label="规格"><el-input v-model="materialForm.spec" /></el-form-item>
<el-form-item label="单位"><el-input v-model="materialForm.unit" /></el-form-item>
<el-form-item label="类别"><el-input v-model="materialForm.category" /></el-form-item>
<el-form-item label="启用"><el-switch v-model="materialForm.enabled" /></el-form-item>
<el-form-item label="锁定"><el-switch v-model="materialForm.isLock" /></el-form-item>
</template>
<template v-if="dialogKind === 'location'">
<el-form-item label="容器"><el-select v-model="locationForm.containerId"><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
<el-form-item label="位置类型"><el-select v-model="locationForm.locationType"><el-option label="库位" value="Storage" /><el-option label="车辆" value="Car" /></el-select></el-form-item>
<el-form-item v-if="locationForm.locationType === 'Storage'" label="库位"><el-select v-model="locationForm.locationId"><el-option v-for="s in storages" :key="s.id" :label="`${s.code} ${s.name}`" :value="s.id" /></el-select></el-form-item>
<el-form-item v-else label="车辆 ID"><el-input v-model="locationForm.locationId" /></el-form-item>
<el-form-item label="状态"><el-select v-model="locationForm.status"><el-option label="Active" value="Active" /><el-option label="Locked" value="Locked" /><el-option label="Exception" value="Exception" /></el-select></el-form-item>
</template>
<template v-if="dialogKind === 'containerMaterial'">
<el-form-item label="容器"><el-select v-model="containerMaterialForm.containerId"><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
<el-form-item label="物料"><el-select v-model="containerMaterialForm.materialId"><el-option v-for="m in materials" :key="m.id" :label="m.code" :value="m.id" /></el-select></el-form-item>
<el-form-item label="数量"><el-input-number v-model="containerMaterialForm.quantity" :min="0.0001" :precision="4" /></el-form-item>
<el-form-item label="批次"><el-input v-model="containerMaterialForm.batchNo" /></el-form-item>
<el-form-item label="序列号"><el-input v-model="containerMaterialForm.serialNo" /></el-form-item>
<el-form-item label="状态"><el-select v-model="containerMaterialForm.status"><el-option label="Loaded" value="Loaded" /><el-option label="Adjusted" value="Adjusted" /><el-option label="Frozen" value="Frozen" /></el-select></el-form-item>
</template>
<el-form-item label="备注"><el-input v-model="currentRemark" type="textarea" /></el-form-item>
<el-form-item label="扩展 JSON"><el-input v-model="currentExtend" type="textarea" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitDialog">保存</el-button></template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import * as wmsApi from '@/api/wms'
import type {
WarehouseArea, Storage, Container, Material, ContainerLocation, ContainerMaterial,
ContainerLocationHistory, ContainerMaterialHistory, MasterDataPayload, StoragePayload,
MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload
} from '@/types/wms'
type DialogKind = 'area' | 'storage' | 'container' | 'material' | 'location' | 'containerMaterial'
const activeGroup = ref('master')
const activeMaster = ref('areas')
const activeRelation = ref('locations')
const keyword = ref('')
const locationFilter = ref('')
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const dialogKind = ref<DialogKind>('area')
const areas = ref<WarehouseArea[]>([])
const storages = ref<Storage[]>([])
const containers = ref<Container[]>([])
const materials = ref<Material[]>([])
const containerLocations = ref<ContainerLocation[]>([])
const containerMaterials = ref<ContainerMaterial[]>([])
const locationHistory = ref<ContainerLocationHistory[]>([])
const materialHistory = ref<ContainerMaterialHistory[]>([])
const masterForm = reactive<MasterDataPayload>(baseMaster())
const storageForm = reactive<StoragePayload>(baseStorage())
const materialForm = reactive<MaterialPayload>(baseMaterial())
const locationForm = reactive<ContainerLocationPayload>(baseLocation())
const containerMaterialForm = reactive<ContainerMaterialPayload>(baseContainerMaterial())
const dialogTitle = computed(() => ({
area: '库区',
storage: '库位',
container: '容器',
material: '物料',
location: '容器位置',
containerMaterial: '容器物料'
}[dialogKind.value]))
const currentRemark = computed({
get: () => formOf(dialogKind.value).remark,
set: (v: string) => { formOf(dialogKind.value).remark = v }
})
const currentExtend = computed({
get: () => formOf(dialogKind.value).extend,
set: (v: string) => { formOf(dialogKind.value).extend = v }
})
const filteredAreas = computed(() => filterRows(areas.value, keyword.value))
const filteredStorages = computed(() => filterRows(storages.value, keyword.value))
const filteredContainers = computed(() => filterRows(containers.value, keyword.value))
const filteredMaterials = computed(() => filterRows(materials.value, keyword.value))
const filteredContainerLocations = computed(() => containerLocations.value.filter((r) =>
(!locationFilter.value || r.locationType === locationFilter.value) &&
(!keyword.value || `${r.locationCode} ${r.locationName}`.toLowerCase().includes(keyword.value.toLowerCase()))))
const historyRows = computed(() => [
...locationHistory.value.map((x) => ({ ...x, kind: '位置' })),
...materialHistory.value.map((x) => ({ ...x, kind: '物料' }))
].sort((a, b) => String(b.operatedAt).localeCompare(String(a.operatedAt))))
async function loadAll() {
loading.value = true
try {
const [a, s, c, m, cl, cm, lh, mh] = await Promise.all([
wmsApi.listAreas(), wmsApi.listStorages(), wmsApi.listContainers(), wmsApi.listMaterials(),
wmsApi.listContainerLocations(), wmsApi.listContainerMaterials(), wmsApi.listContainerLocationHistory(), wmsApi.listContainerMaterialHistory()
])
areas.value = a; storages.value = s; containers.value = c; materials.value = m
containerLocations.value = cl; containerMaterials.value = cm; locationHistory.value = lh; materialHistory.value = mh
} finally {
loading.value = false
}
}
function openArea(row?: WarehouseArea) { dialogKind.value = 'area'; Object.assign(masterForm, baseMaster(row)); dialogVisible.value = true }
function openStorage(row?: Storage) { dialogKind.value = 'storage'; Object.assign(storageForm, baseStorage(row)); dialogVisible.value = true }
function openContainer(row?: Container) { dialogKind.value = 'container'; Object.assign(masterForm, baseMaster(row)); dialogVisible.value = true }
function openMaterial(row?: Material) { dialogKind.value = 'material'; Object.assign(materialForm, baseMaterial(row)); dialogVisible.value = true }
function openLocation(row?: ContainerLocation) { dialogKind.value = 'location'; Object.assign(locationForm, baseLocation(row)); dialogVisible.value = true }
function openContainerMaterial(row?: ContainerMaterial) { dialogKind.value = 'containerMaterial'; Object.assign(containerMaterialForm, baseContainerMaterial(row)); dialogVisible.value = true }
async function submitDialog() {
saving.value = true
try {
JSON.parse(currentExtend.value || '{}')
if (dialogKind.value === 'area') await wmsApi.saveArea(masterForm)
else if (dialogKind.value === 'container') await wmsApi.saveContainer(masterForm)
else if (dialogKind.value === 'storage') await wmsApi.saveStorage(storageForm)
else if (dialogKind.value === 'material') await wmsApi.saveMaterial(materialForm)
else if (dialogKind.value === 'location') await wmsApi.saveContainerLocation(locationForm)
else await wmsApi.saveContainerMaterial(containerMaterialForm)
ElMessage.success('已保存')
dialogVisible.value = false
await loadAll()
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : String(e))
} finally {
saving.value = false
}
}
async function confirmDelete(message: string) {
await ElMessageBox.confirm(message, '确认操作', { type: 'warning' })
}
async function removeArea(row: WarehouseArea) { await confirmDelete('删除该库区?'); await wmsApi.deleteArea(row.id, row.version); await loadAll() }
async function removeStorage(row: Storage) { await confirmDelete('删除该库位?'); await wmsApi.deleteStorage(row.id, row.version); await loadAll() }
async function removeContainer(row: Container) { await confirmDelete('删除该容器?'); await wmsApi.deleteContainer(row.id, row.version); await loadAll() }
async function removeMaterial(row: Material) { await confirmDelete('删除该物料?'); await wmsApi.deleteMaterial(row.id, row.version); await loadAll() }
async function removeLocation(row: ContainerLocation) { await confirmDelete('解绑该容器位置?'); await wmsApi.deleteContainerLocation(row.containerId); await loadAll() }
async function removeContainerMaterial(row: ContainerMaterial) { await confirmDelete('卸载该容器物料?'); await wmsApi.deleteContainerMaterial(row.id); await loadAll() }
function areaName(id: string) { return areas.value.find((a) => a.id === id)?.name ?? id }
function containerName(id: string) { const c = containers.value.find((x) => x.id === id); return c ? `${c.code} ${c.name}` : id }
function materialName(id: string) { const m = materials.value.find((x) => x.id === id); return m ? `${m.code} ${m.name}` : id }
function containerLocationLabel(id: string) {
const loc = containerLocations.value.find((x) => x.containerId === id)
return loc ? `${loc.locationType}:${loc.locationCode || loc.locationId}` : '未绑定'
}
function filterRows<T extends Record<string, unknown>>(rows: T[], q: string) {
if (!q) return rows
const s = q.toLowerCase()
return rows.filter((r) => Object.values(r).some((v) => String(v ?? '').toLowerCase().includes(s)))
}
function formOf(kind: DialogKind): { remark: string; extend: string } {
if (kind === 'storage') return storageForm
if (kind === 'material') return materialForm
if (kind === 'location') return locationForm
if (kind === 'containerMaterial') return containerMaterialForm
return masterForm
}
function baseCommon(row?: { id: string; version: number; isLock: boolean; remark: string; extend: string }) {
return { id: row?.id, version: row?.version, isLock: row?.isLock ?? false, remark: row?.remark ?? '', extend: row?.extend ?? '{}' }
}
function baseMaster(row?: WarehouseArea | Container): MasterDataPayload {
return { ...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', type: 'type' in (row ?? {}) ? (row as WarehouseArea).type : (row as Container | undefined)?.containerType ?? '', status: (row as Container | undefined)?.status ?? 'Idle', enabled: row?.enabled ?? true, sortOrder: (row as WarehouseArea | undefined)?.sortOrder ?? 0 }
}
function baseStorage(row?: Storage): StoragePayload {
return { ...baseCommon(row), areaId: row?.areaId ?? areas.value[0]?.id ?? '', code: row?.code ?? '', name: row?.name ?? '', storageType: row?.storageType ?? 'Storage', siteId: row?.siteId ?? '', capacity: row?.capacity ?? 0, enabled: row?.enabled ?? true }
}
function baseMaterial(row?: Material): MaterialPayload {
return { ...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', spec: row?.spec ?? '', unit: row?.unit ?? 'pcs', category: row?.category ?? '', enabled: row?.enabled ?? true }
}
function baseLocation(row?: ContainerLocation): ContainerLocationPayload {
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', locationType: row?.locationType ?? 'Storage', locationId: row?.locationId ?? '', status: row?.status ?? 'Active', enteredAt: row?.enteredAt, source: 'Manual', reason: '' }
}
function baseContainerMaterial(row?: ContainerMaterial): ContainerMaterialPayload {
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', materialId: row?.materialId ?? materials.value[0]?.id ?? '', quantity: row?.quantity ?? 1, batchNo: row?.batchNo ?? '', serialNo: row?.serialNo ?? '', status: row?.status ?? 'Loaded', loadedAt: row?.loadedAt, source: 'Manual', reason: '' }
}
onMounted(loadAll)
</script>
<style scoped>
.warehouse-page { padding: 16px; height: 100%; overflow: auto; }
.warehouse-card { min-height: 100%; }
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.page-header h2 { margin: 0 0 4px; font-size: 20px; color: var(--mg-text-light); }
.page-header p { margin: 0; color: var(--mg-text-muted); font-size: 13px; }
.toolbar { display: flex; gap: 10px; align-items: center; margin: 0 0 12px; }
.toolbar .el-input { max-width: 260px; }
.toolbar .el-select { width: 160px; }
</style>
@@ -1,12 +1,12 @@
import type { ParamFieldDef } from '@/types/workflow'
export const ASSIGN_STRATEGY_OPTIONS = [
{ label: '指定车辆', value: 'fixed' },
{ label: '指定类型', value: 'byType' }
{ label: '指定车辆', value: 'carId' },
{ label: '指定类型', value: 'carType' }
] as const
const FIXED = { key: 'strategy', equals: 'fixed' as const }
const BY_TYPE = { key: 'strategy', equals: 'byType' as const }
const BY_CAR_ID = { key: 'strategy', equals: 'carId' as const }
const BY_CAR_TYPE = { key: 'strategy', equals: 'carType' as const }
/** 分配车辆:指定车辆 / 指定类型 */
export function assignCarParams(): ParamFieldDef[] {
@@ -16,7 +16,7 @@ export function assignCarParams(): ParamFieldDef[] {
label: '分配策略',
type: 'select',
required: true,
defaultValue: 'fixed',
defaultValue: 'carId',
options: [...ASSIGN_STRATEGY_OPTIONS]
},
{
@@ -25,7 +25,7 @@ export function assignCarParams(): ParamFieldDef[] {
type: 'string',
required: true,
placeholder: '如 C01',
when: FIXED
when: BY_CAR_ID
},
{
key: 'carType',
@@ -33,7 +33,7 @@ export function assignCarParams(): ParamFieldDef[] {
type: 'string',
required: true,
placeholder: '如 Forklift / SimpleLite.RCS.Cars.AgvCar',
when: BY_TYPE
when: BY_CAR_TYPE
}
]
}
@@ -132,7 +132,7 @@ export const DEMO_WORKFLOW: WorkflowDefinition = {
viewport: { x: 0, y: 0, zoom: 1 },
nodes: [
{ id: 'n1', type: 'start', label: '开始', position: { x: 60, y: 200 }, params: {} },
{ id: 'n2', type: 'assignCar', label: '分配车辆', position: { x: 240, y: 180 }, params: { strategy: 'fixed', carId: 'C01' } },
{ id: 'n2', type: 'assignCar', label: '分配车辆', position: { x: 240, y: 180 }, params: { strategy: 'carId', carId: 'C01' } },
{ id: 'n3', type: 'gotoSite', label: '前往入库点', position: { x: 460, y: 180 }, params: { siteId: 'S001' } },
{
id: 'n4',
@@ -140,7 +140,7 @@ export const DEMO_WORKFLOW: WorkflowDefinition = {
label: '取货',
position: { x: 680, y: 180 },
params: {
actionType: 'pick',
actionType: 'fetch',
actionParameters: '[{"key":"loadType","value":"pallet"}]'
}
},
@@ -2,9 +2,8 @@ import type { ParamFieldDef } from '@/types/workflow'
/** 动作类型选项 */
export const ACTION_TYPE_OPTIONS = [
{ label: '取货', value: 'pick' },
{ label: '放货', value: 'drop' },
{ label: '充电', value: 'startCharging' }
{ label: '取货', value: 'fetch' },
{ label: '放货', value: 'put' }
] as const
/** 执行动作:对齐 VDA5050 Order.actions[] 单条 Action 结构(不含 actionId,下发时由运行时生成) */
@@ -15,7 +14,7 @@ export function executeActionParams(): ParamFieldDef[] {
label: '动作类型',
type: 'select',
required: true,
defaultValue: 'pick',
defaultValue: 'fetch',
options: [...ACTION_TYPE_OPTIONS]
},
{
@@ -1,7 +1,7 @@
import { toRaw } from 'vue'
import type { ParamFieldDef, WorkflowDefinition, WorkflowEdgeDef, WorkflowNodeDef } from '@/types/workflow'
import type { WorkflowCatalogExport } from '@/workflow/workflowLibrary'
import { NODE_CATALOG_MAP, defaultParamsForType } from '@/workflow/nodeCatalog'
import { defaultParamsForType, NODE_CATALOG_MAP } from '@/workflow/nodeCatalog'
import { WAIT_EVENT_HANDLES, WAIT_EVENT_HANDLE_LABELS } from '@/workflow/waitEventParams'
/** 深拷贝流程定义(workflow 为纯 JSON 结构,避免 structuredClone 对 Proxy/不可克隆对象报错) */
@@ -72,8 +72,10 @@ export const WORKFLOW_CATALOG_VERSION = 3
export const WORKFLOW_EDITOR_STORAGE_KEY = 'workflowEditor.draft'
const DEPRECATED_ASSIGN_STRATEGY: Record<string, string> = {
nearest: 'fixed',
idle: 'byType'
nearest: 'carId',
idle: 'carType',
fixed: 'carId',
byType: 'carType'
}
function isFieldVisible(field: ParamFieldDef, params: Record<string, unknown>): boolean {
@@ -268,11 +270,12 @@ export function attachNextIds(
})
}
/** 导出用:附带 nextId 的流程快照 */
/** 导出用:附带 nextId 的流程快照params 按当前控件 schema 规范化) */
export function workflowForExport(def: WorkflowDefinition): WorkflowDefinition {
const normalized = normalizeWorkflow(def)
return {
...def,
nodes: attachNextIds(def.nodes, def.edges)
...normalized,
nodes: attachNextIds(normalized.nodes, normalized.edges)
}
}