新增WCS任务模板引擎原型

新增“WCS模板原型”入口及自检脚本,支持无代码拖拽流程设计、选位试算、实例运行、预占管理等。实现了DSL类型定义、控件库、画布节点、节点属性面板、试算台等组件。补充了流程编排、参数组装、表达式求值、选位与打分、预占管理、模板校验等核心逻辑。提供假数据、演示脚本及README说明,便于后续对接真实系统。
This commit is contained in:
2026-08-25 11:21:11 +08:00
parent 4ee6379c26
commit 95205cbcde
24 changed files with 3264 additions and 1 deletions
@@ -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>