新增WCS任务模板引擎原型
新增“WCS模板原型”入口及自检脚本,支持无代码拖拽流程设计、选位试算、实例运行、预占管理等。实现了DSL类型定义、控件库、画布节点、节点属性面板、试算台等组件。补充了流程编排、参数组装、表达式求值、选位与打分、预占管理、模板校验等核心逻辑。提供假数据、演示脚本及README说明,便于后续对接真实系统。
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* WCS 任务模板引擎原型:
|
||||
* - 主能力:无代码拖拽流程图设计任务模板(对齐设计文档 §4)
|
||||
* - 辅能力:运行实例、假数据台、演示脚本自检
|
||||
*/
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createWorld } from '@/wcs-proto/runtime/world'
|
||||
import { DEMO_EVENT } from '@/wcs-proto/mock/seed'
|
||||
import WcsTemplateDesigner from '@/wcs-proto/designer/WcsTemplateDesigner.vue'
|
||||
|
||||
const world = createWorld()
|
||||
const { state, reservations } = world
|
||||
|
||||
const mainTab = ref<'design' | 'runtime'>('design')
|
||||
const runtimeTab = ref('instances')
|
||||
|
||||
const eventForm = reactive({
|
||||
eventId: DEMO_EVENT.eventId,
|
||||
materialCode: 'M001',
|
||||
qty: 1,
|
||||
lineId: 'L1'
|
||||
})
|
||||
const selectedInstanceId = ref<string | null>(null)
|
||||
const selectedInstance = computed(() => state.instances.find((i) => i.id === selectedInstanceId.value))
|
||||
const reservationRows = computed(() => reservations.list())
|
||||
|
||||
function onInstanceRowClick(row: { id: string }) {
|
||||
selectedInstanceId.value = row.id
|
||||
}
|
||||
|
||||
function onAffinityInput(row: { materialAffinity: string[] }, v: string) {
|
||||
row.materialAffinity = v.split(/[,,]/).map((s) => s.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function onForceChange(storageId: string, val: string | number | boolean) {
|
||||
if (val) world.forceReserve(storageId)
|
||||
else world.clearForce(storageId)
|
||||
}
|
||||
|
||||
function doTrigger() {
|
||||
const inst = world.trigger({
|
||||
eventId: eventForm.eventId,
|
||||
source: 'mes.materialCall',
|
||||
payload: {
|
||||
materialCode: eventForm.materialCode,
|
||||
qty: eventForm.qty,
|
||||
lineId: eventForm.lineId
|
||||
}
|
||||
})
|
||||
mainTab.value = 'runtime'
|
||||
runtimeTab.value = 'instances'
|
||||
if (inst) {
|
||||
selectedInstanceId.value = inst.id
|
||||
ElMessage.info(`${inst.status} · ${inst.templateName}`)
|
||||
} else {
|
||||
ElMessage.warning('无匹配模板')
|
||||
}
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
world.reset()
|
||||
selectedInstanceId.value = null
|
||||
ElMessage.success('已重置假数据与实例')
|
||||
}
|
||||
|
||||
function runDemos() {
|
||||
const results = world.runDemoScripts()
|
||||
mainTab.value = 'runtime'
|
||||
runtimeTab.value = 'demos'
|
||||
const fail = results.filter((r) => !r.ok)
|
||||
if (fail.length) ElMessage.error(`${fail.length} 条演示失败`)
|
||||
else ElMessage.success('7 条演示脚本全部通过')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wcs-proto-page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h2>WCS 任务模板引擎 · 原型</h2>
|
||||
<p class="sub">无代码拖拽设计模板 → 试算选位 → 模拟触发运行(假数据,不接真实 MES/AGV)</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-radio-group v-model="mainTab" size="small">
|
||||
<el-radio-button value="design">模板设计</el-radio-button>
|
||||
<el-radio-button value="runtime">运行与演示</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button size="small" @click="resetAll">重置世界</el-button>
|
||||
<el-button size="small" type="warning" @click="runDemos">跑 7 条脚本</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-show="mainTab === 'design'" class="design-pane">
|
||||
<WcsTemplateDesigner :world="world" />
|
||||
</div>
|
||||
|
||||
<div v-show="mainTab === 'runtime'" class="runtime-pane">
|
||||
<el-row :gutter="12" class="event-bar">
|
||||
<el-col :span="4">
|
||||
<el-input v-model="eventForm.eventId" placeholder="eventId" size="small" />
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-input v-model="eventForm.materialCode" placeholder="物料" size="small" />
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-input v-model="eventForm.lineId" placeholder="产线" size="small" />
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-input-number v-model="eventForm.qty" :min="1" size="small" controls-position="right" />
|
||||
</el-col>
|
||||
<el-col :span="6" class="event-actions">
|
||||
<el-button type="primary" size="small" @click="doTrigger">模拟触发</el-button>
|
||||
<el-switch v-model="state.lookupBroken" active-text="lookup 故障" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-tabs v-model="runtimeTab">
|
||||
<el-tab-pane label="实例" name="instances">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="14">
|
||||
<el-table
|
||||
:data="state.instances"
|
||||
size="small"
|
||||
highlight-current-row
|
||||
@row-click="onInstanceRowClick"
|
||||
>
|
||||
<el-table-column prop="id" label="实例" width="100" />
|
||||
<el-table-column prop="templateName" label="模板" />
|
||||
<el-table-column prop="status" label="状态" width="130" />
|
||||
<el-table-column label="路径" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.sourceId">{{ row.sourceId }} → {{ row.targetId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'Dispatched'"
|
||||
size="small"
|
||||
@click.stop="world.dispatch(row.id, 'start')"
|
||||
>开始</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'Dispatched' || row.status === 'InTransit'"
|
||||
size="small"
|
||||
type="success"
|
||||
@click.stop="world.dispatch(row.id, 'complete')"
|
||||
>完成</el-button>
|
||||
<el-button
|
||||
v-if="!['Completed','Failed','Cancelled','IgnoredDuplicate'].includes(row.status)"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click.stop="world.cancel(row.id)"
|
||||
>取消</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<template v-if="selectedInstance">
|
||||
<h4>时间线</h4>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="(t, i) in selectedInstance.timeline" :key="i" :timestamp="t.at">
|
||||
{{ t.status }} <span v-if="t.note">· {{ t.note }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<h4>explain</h4>
|
||||
<pre class="explain">{{ JSON.stringify(selectedInstance.explain, null, 2) }}</pre>
|
||||
</template>
|
||||
<el-empty v-else description="选择一条实例" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="假数据台" name="mock">
|
||||
<el-table :data="state.storages" size="small" max-height="360">
|
||||
<el-table-column prop="storageId" label="ID" width="90" />
|
||||
<el-table-column prop="areaId" label="库区" width="90" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="物料亲和" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
size="small"
|
||||
:model-value="row.materialAffinity.join(',')"
|
||||
@update:model-value="(v: string) => onAffinityInput(row, v)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="禁用" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.disabled" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="强制预占" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="!!row.forceReserved"
|
||||
size="small"
|
||||
@change="(v) => onForceChange(row.storageId, v)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<h4>预占表</h4>
|
||||
<el-table :data="reservationRows" size="small">
|
||||
<el-table-column prop="storageId" label="库位" />
|
||||
<el-table-column prop="instanceId" label="实例" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="演示脚本" name="demos">
|
||||
<el-table :data="state.demoResults" size="small">
|
||||
<el-table-column prop="id" label="ID" width="120" />
|
||||
<el-table-column prop="name" label="名称" width="140" />
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ok ? 'success' : 'danger'" size="small">{{ row.ok ? '通过' : '失败' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="说明" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wcs-proto-page {
|
||||
height: calc(100vh - 56px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 12px 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.page-head h2 {
|
||||
margin: 0 0 2px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.sub {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.design-pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.runtime-pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.event-bar {
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.event-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.explain {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
}
|
||||
h4 {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user