Browse Source

修改条件不满足时等待,添加仿真页面

一只小菜杨 1 month ago
parent
commit
f4838923a5

BIN
public/canvas-icon/brand.png


BIN
public/canvas-icon/cfg-icon.png


BIN
public/canvas-icon/light-icon.png


BIN
public/canvas-icon/logo-icon.png


BIN
public/canvas-icon/rg45-icon.png


BIN
public/canvas-icon/screen-icon.png


BIN
public/canvas-icon/v-icon.png


+ 7 - 2
src/App.vue

@@ -1,8 +1,8 @@
 <template>
-  <div class="page-main">
+  <div class="page-main" :class="fullScreen.includes(route.path) ? 'full-screen' : ''">
     <router-view></router-view>
   </div>
-  <van-tabbar v-model="tabActive" active-color="#4fb5f9" inactive-color="#333">
+  <van-tabbar v-model="tabActive" active-color="#4fb5f9" inactive-color="#333" v-if="route.path !== '/runtime'">
     <van-tabbar-item name="home" replace to="/home" icon="wap-home-o">首页</van-tabbar-item>
     <van-tabbar-item name="automate" replace to="/automate" icon="star-o">自动化</van-tabbar-item>
   </van-tabbar>
@@ -23,6 +23,8 @@ watch(
   { immediate: true }
 )
 
+const fullScreen = ['/runtime']
+
 const init = async () => {
   if (!window.isInWx) {
     return
@@ -74,4 +76,7 @@ init()
   height: calc(100% - 50px);
   overflow: auto;
 }
+.page-main.full-screen {
+  height: 100%;
+}
 </style>

+ 5 - 0
src/router/index.ts

@@ -27,6 +27,11 @@ const router = createRouter({
       path: '/command-info/:pid/:id',
       name: 'CommandInfo',
       component: () => import('@/views/CommandInfo.vue')
+    },
+    {
+      path: '/runtime',
+      name: 'runtime',
+      component: () => import('@/views/runtime.vue')
     }
   ]
 })

+ 1 - 11
src/stores/global.ts

@@ -20,16 +20,12 @@ export const useGlobalStore = defineStore(
                 type: 'condition',
                 list: [
                   {
-                    x: 300,
-                    y: 100,
                     id: '111',
                     type: 'input',
                     label: 'DI1',
                     value: '闭合'
                   },
                   {
-                    x: 300,
-                    y: 400,
                     id: '112',
                     type: 'input',
                     label: 'AI1',
@@ -37,8 +33,6 @@ export const useGlobalStore = defineStore(
                     value: '0'
                   },
                   {
-                    x: 300,
-                    y: 700,
                     id: '113',
                     type: 'time',
                     label: '每天',
@@ -50,23 +44,19 @@ export const useGlobalStore = defineStore(
               {
                 id: '12',
                 type: 'delay',
-                value: '1000',
+                value: '5',
                 unit: 's'
               },
               {
                 type: 'exec',
                 list: [
                   {
-                    x: 3700,
-                    y: 20,
                     id: '131',
                     type: 'output',
                     label: 'DO1',
                     value: '打开'
                   },
                   {
-                    x: 3700,
-                    y: 40,
                     id: '132',
                     type: 'output',
                     label: 'AO1',

+ 353 - 0
src/utils/worker.js

@@ -0,0 +1,353 @@
+const yieldCPU = () => {
+  return new Promise((resolve) => {
+    setTimeout(resolve, 0)
+  })
+}
+
+const initWasm = (url) => {
+  importScripts(url)
+  Module.onRuntimeInitialized = async function () {
+    _Init()
+    while (true) {
+      _Loop()
+      for (let i = 0; i < 10; i++) {
+        drawPlc('DO', i, _GetTrustDO(i))
+      }
+      for (let i = 0; i < 2; i++) {
+        drawPlc('AO', i, _GetTrustAO(i))
+      }
+      drawPlc('time', 0, _GetTimeStamp())
+      await yieldCPU() // 让出cpu,保证有间隔进入消息接收回调
+    }
+  }
+}
+
+let canvasEl
+let ctx
+let ratioPixel
+const canvasMsg = {
+  width: 0,
+  height: 0
+}
+
+const drawPlc = async (key, index, val, type) => {
+  if (!ctx) return
+  const distance = 48 // 每个模块间隔
+  const textDis = 95
+  const r = 18
+  const l_r_dis = 50
+  ctx.font = '24px 黑体'
+  ctx.textAlign = 'start'
+  ctx.globalAlpha = 1
+  if (type === 'init') {
+    ctx.beginPath()
+    ctx.rect(l_r_dis - r - 10, 50 - r - 10, (r + 10) * 2, distance * 11 + r / 4)
+    ctx.rect(l_r_dis - r - 10, 50 - r - 10 + distance * 11 + r / 4, (r + 10) * 2, distance * 2)
+    ctx.rect(l_r_dis - r - 10, 50 - r - 10 + distance * 13 + r / 4, (r + 10) * 2, distance * 2)
+
+    ctx.rect(canvasMsg.width - l_r_dis - r - 10, 50 - r - 10, (r + 10) * 2, distance * 11 + r / 4)
+    ctx.rect(canvasMsg.width - l_r_dis - r - 10, 50 + distance * 11 - r - 10 + r / 4, (r + 10) * 2, distance * 5)
+    ctx.rect(canvasMsg.width - l_r_dis - r - 10, 50 + distance * 16 - r - 10 + r / 4, (r + 10) * 2, distance * 2)
+    ctx.rect(canvasMsg.width - l_r_dis - r - 10, 50 + distance * 18 - r - 10 + r / 4, (r + 10) * 2, distance * 2)
+    ctx.rect(canvasMsg.width - l_r_dis - r - 10, 50 + distance * 20 - r - 10 + r / 4, (r + 10) * 2, distance * 2)
+    ctx.stroke()
+    ctx.closePath()
+
+    // GND
+    ctx.beginPath()
+    // ctx.fillStyle = val !== 0 ? 'green' : 'red'
+    ctx.fillStyle = 'red'
+    ctx.arc(canvasMsg.width - l_r_dis, 50 + distance * 11, r, 0, 2 * Math.PI)
+    ctx.fill()
+    const GND_textInfo = ctx.measureText(`GND`)
+    ctx.fillStyle = 'black'
+    ctx.fillText(
+      `GND`,
+      canvasMsg.width - textDis - GND_textInfo.width,
+      50 + distance * 11 + GND_textInfo.actualBoundingBoxAscent / 2
+    )
+
+    // left com
+    ctx.beginPath()
+    ctx.fillStyle = 'red'
+    ctx.arc(l_r_dis, 50, r, 0, 2 * Math.PI)
+    ctx.fill()
+    const textInfo = ctx.measureText('COM')
+    ctx.fillStyle = 'black'
+    ctx.fillText('COM', textDis, 50 + textInfo.actualBoundingBoxAscent / 2)
+
+    // right com
+    ctx.beginPath()
+    ctx.fillStyle = 'red'
+    ctx.arc(canvasMsg.width - l_r_dis, 50, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.fillStyle = 'black'
+    ctx.fillText('COM', canvasMsg.width - textDis - textInfo.width, 50 + textInfo.actualBoundingBoxAscent / 2)
+
+    // RS485
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(l_r_dis, 50 + 11 * distance, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(l_r_dis, 50 + 12 * distance, r, 0, 2 * Math.PI)
+    ctx.fill()
+    const rs485_textInfo = ctx.measureText('RS485_0')
+    ctx.fillStyle = 'black'
+    ctx.fillText('RS485_0', textDis, 50 + 11 * distance + r + rs485_textInfo.actualBoundingBoxAscent / 2)
+
+    let a_textInfo = ctx.measureText('A')
+    ctx.fillText('A', l_r_dis - a_textInfo.width / 2, 50 + 11 * distance + a_textInfo.actualBoundingBoxAscent / 2)
+    let b_textInfo = ctx.measureText('B')
+    ctx.fillText('B', l_r_dis - a_textInfo.width / 2, 50 + 12 * distance + b_textInfo.actualBoundingBoxAscent / 2)
+
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(l_r_dis, 50 + 13 * distance, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(l_r_dis, 50 + 14 * distance, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.fillStyle = 'black'
+    ctx.fillText('RS485_1', textDis, 50 + 13 * distance + r + rs485_textInfo.actualBoundingBoxAscent / 2)
+
+    ctx.fillText('A', l_r_dis - a_textInfo.width / 2, 50 + 13 * distance + a_textInfo.actualBoundingBoxAscent / 2)
+    ctx.fillText('B', l_r_dis - a_textInfo.width / 2, 50 + 14 * distance + b_textInfo.actualBoundingBoxAscent / 2)
+
+    // 配置按钮
+    ctx.beginPath()
+    const response1 = await fetch(new URL('/wxapp/canvas-icon/cfg-icon.png', self.location.href).href)
+    const blob1 = await response1.blob()
+    const imgBitmap1 = await createImageBitmap(blob1)
+    ctx.drawImage(imgBitmap1, l_r_dis - r - 10, 50 + distance * 16 - r, (r + 10) * 2, (r + 10) * 2)
+    const cfg_textInfo = ctx.measureText('配置按钮')
+    ctx.fillText('配置按钮', textDis, 50 + 16 * distance + cfg_textInfo.actualBoundingBoxAscent / 2 + r - 10)
+    // 屏接口
+    ctx.beginPath()
+    const response2 = await fetch(new URL('/wxapp/canvas-icon/screen-icon.png', self.location.href).href)
+    const blob2 = await response2.blob()
+    const imgBitmap2 = await createImageBitmap(blob2)
+    ctx.drawImage(imgBitmap2, l_r_dis - r + 5, 50 + distance * 18 - 20, 25, 80)
+    const screen_textInfo = ctx.measureText('屏接口')
+    ctx.fillText('屏接口', textDis, 50 + 18 * distance + screen_textInfo.actualBoundingBoxAscent / 2 + 40 - 20)
+    // 指示灯
+    ctx.beginPath()
+    const response3 = await fetch(new URL('/wxapp/canvas-icon/light-icon.png', self.location.href).href)
+    const blob3 = await response3.blob()
+    const imgBitmap3 = await createImageBitmap(blob3)
+    ctx.drawImage(imgBitmap3, l_r_dis - r, 50 + distance * 19 + 40, 40, 40)
+    const light1_textInfo = ctx.measureText('指示灯')
+    ctx.fillText('指示灯', textDis, 50 + 19 * distance + light1_textInfo.actualBoundingBoxAscent / 2 + 60)
+    // 网口
+    ctx.beginPath()
+    const response6 = await fetch(new URL('/wxapp/canvas-icon/rg45-icon.png', self.location.href).href)
+    const blob6 = await response6.blob()
+    const imgBitmap6 = await createImageBitmap(blob6)
+    ctx.drawImage(imgBitmap6, l_r_dis - r, 50 + distance * 21 + 20, 40, 40)
+    const rg45_textInfo = ctx.measureText('网口')
+    ctx.fillText('网口', textDis, 50 + 21 * distance + rg45_textInfo.actualBoundingBoxAscent / 2 + 40)
+    // logo
+    ctx.beginPath()
+    const response4 = await fetch(new URL('/wxapp/canvas-icon/logo-icon.png', self.location.href).href)
+    const blob4 = await response4.blob()
+    const imgBitmap4 = await createImageBitmap(blob4)
+    ctx.drawImage(imgBitmap4, canvasMsg.width / 2 - imgBitmap4.width / 2, 100)
+    // brand
+    ctx.beginPath()
+    const response5 = await fetch(new URL('/wxapp/canvas-icon/brand.png', self.location.href).href)
+    const blob5 = await response5.blob()
+    const imgBitmap5 = await createImageBitmap(blob5)
+    ctx.drawImage(imgBitmap5, canvasMsg.width / 2 - 140 / 2, 300, 140, (140 * imgBitmap5.height) / imgBitmap5.width)
+
+    // GND VCC
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(canvasMsg.width - l_r_dis, 50 + distance * 20, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.fillStyle = 'black'
+    ctx.fillText(
+      `GND`,
+      canvasMsg.width - textDis - GND_textInfo.width,
+      50 + distance * 20 + GND_textInfo.actualBoundingBoxAscent / 2
+    )
+    ctx.beginPath()
+    ctx.fillStyle = '#a3a3a3'
+    ctx.arc(canvasMsg.width - l_r_dis, 50 + distance * 21, r, 0, 2 * Math.PI)
+    ctx.fill()
+    ctx.fillStyle = 'black'
+    const VCC_textInfo = ctx.measureText(`VCC`)
+    ctx.fillText(
+      `VCC`,
+      canvasMsg.width - textDis - VCC_textInfo.width,
+      50 + distance * 21 + VCC_textInfo.actualBoundingBoxAscent / 2
+    )
+    ctx.fillText(
+      '+',
+      canvasMsg.width - l_r_dis - a_textInfo.width / 2,
+      50 + 21 * distance + a_textInfo.actualBoundingBoxAscent / 2
+    )
+    ctx.fillText(
+      '-',
+      canvasMsg.width - l_r_dis - a_textInfo.width / 2,
+      50 + 20 * distance + b_textInfo.actualBoundingBoxAscent / 2
+    )
+
+    const tip_textInfo = ctx.measureText('当前plc系统时间')
+    ctx.fillText(
+      `当前plc系统时间`,
+      canvasMsg.width / 2 - tip_textInfo.width / 2,
+      canvasMsg.height - 100 + tip_textInfo.actualBoundingBoxAscent
+    )
+  }
+
+  const y = 50 + distance * (index + 1)
+  switch (key) {
+    case 'DI':
+    case 'DO':
+      ctx.clearRect(key === 'DI' ? l_r_dis - r : canvasMsg.width - l_r_dis - r, y - r, r * 2, r * 2)
+      ctx.beginPath()
+      ctx.fillStyle = val !== 0 ? 'green' : 'red'
+      ctx.arc(key === 'DI' ? l_r_dis : canvasMsg.width - l_r_dis, y, r, 0, 2 * Math.PI)
+      ctx.fill()
+      const DI_DO_textInfo = ctx.measureText(`${key}_${index}`)
+      ctx.fillStyle = 'black'
+      if (type === 'draw') {
+        ctx.fillText(
+          `${key}_${index}`,
+          key === 'DI' ? textDis : canvasMsg.width - textDis - DI_DO_textInfo.width,
+          y + DI_DO_textInfo.actualBoundingBoxAscent / 2
+        )
+      }
+      break
+    case 'AI':
+      ctx.beginPath()
+      // ctx.fillStyle = val !== 0 ? 'green' : 'red'
+      const AI_textInfo = ctx.measureText(`${key}_${index}`)
+      if (type === 'draw') {
+        ctx.fillStyle = 'red'
+        ctx.arc(canvasMsg.width - l_r_dis, y + distance * 11, r, 0, 2 * Math.PI)
+        ctx.fill()
+        ctx.fillStyle = 'black'
+        ctx.fillText(
+          `${key}_${index}`,
+          canvasMsg.width - textDis - AI_textInfo.width,
+          y + distance * 11 + AI_textInfo.actualBoundingBoxAscent / 2
+        )
+      }
+      ctx.clearRect(canvasMsg.width - textDis - AI_textInfo.width - 120, y + distance * 11 - r, 100, r * 2)
+      ctx.beginPath()
+      ctx.rect(canvasMsg.width - textDis - AI_textInfo.width - 120, y + distance * 11 - r, 100, r * 2)
+      ctx.lineCap = 'round'
+      ctx.stroke()
+      ctx.closePath()
+      ctx.textAlign = 'center'
+      const AI_val_textInfo = ctx.measureText(`${val}`)
+
+      ctx.fillText(
+        `${val}`,
+        canvasMsg.width - textDis - AI_textInfo.width - 60 - AI_val_textInfo.width / 2,
+        y + distance * 11 + AI_textInfo.actualBoundingBoxAscent / 2,
+        100
+      )
+      break
+    case 'AO':
+      const y1 = y + distance * (15 + index * 1)
+      // ctx.fillStyle = val !== 0 ? 'green' : 'red'
+      const AO_textInfo = ctx.measureText(`${key}_${index}`)
+      if (type === 'draw') {
+        ctx.beginPath()
+        ctx.fillStyle = 'red'
+        ctx.arc(canvasMsg.width - l_r_dis, y1, r, 0, 2 * Math.PI)
+        ctx.fill()
+        ctx.fillStyle = 'black'
+        ctx.fillText(
+          `${key}_${index}`,
+          canvasMsg.width - textDis - AO_textInfo.width,
+          y1 + r + AO_textInfo.actualBoundingBoxAscent / 2
+        )
+      }
+
+      ctx.clearRect(canvasMsg.width - textDis - AO_textInfo.width - 120, y1, 100, r * 2)
+      ctx.beginPath()
+      ctx.rect(canvasMsg.width - textDis - AO_textInfo.width - 120, y1, 100, r * 2)
+      ctx.lineCap = 'round'
+      ctx.stroke()
+      ctx.closePath()
+      ctx.textAlign = 'center'
+      const AO_val_textInfo = ctx.measureText(`${val}`)
+      ctx.fillText(
+        `${val}`,
+        canvasMsg.width - textDis - AO_textInfo.width - 60 - AO_val_textInfo.width / 2.5,
+        y1 + r + AO_textInfo.actualBoundingBoxAscent / 2,
+        100
+      )
+      if (type === 'draw') {
+        ctx.textAlign = 'start'
+        const add_textInfo = ctx.measureText('+')
+        ctx.fillText(
+          `+`,
+          canvasMsg.width - l_r_dis - add_textInfo.width / 2,
+          y1 + add_textInfo.actualBoundingBoxAscent / 2
+        )
+      }
+
+      const y2 = y + distance * (15 + index * 1 + 1)
+      if (type === 'draw') {
+        ctx.beginPath()
+        // ctx.fillStyle = val !== 0 ? 'green' : 'red'
+        ctx.fillStyle = 'red'
+        ctx.arc(canvasMsg.width - l_r_dis, y2, r, 0, 2 * Math.PI)
+        ctx.fill()
+        ctx.fillStyle = 'black'
+        const sub_textInfo = ctx.measureText('-')
+        ctx.fillText(`-`, canvasMsg.width - l_r_dis - sub_textInfo.width / 2, y2 + sub_textInfo.actualBoundingBoxAscent)
+      }
+      break
+    case 'time':
+      const timeVal = new Date(val * 1000).toLocaleString()
+      const valTextInfo = ctx.measureText(timeVal)
+      ctx.clearRect(
+        canvasMsg.width / 2 - valTextInfo.width / 2,
+        canvasMsg.height - 50 - valTextInfo.actualBoundingBoxAscent,
+        valTextInfo.width,
+        valTextInfo.actualBoundingBoxAscent * 2
+      )
+      ctx.fillText(
+        timeVal,
+        canvasMsg.width / 2 - valTextInfo.width / 2,
+        canvasMsg.height - 50 + valTextInfo.actualBoundingBoxAscent
+      )
+  }
+}
+
+onmessage = (e) => {
+  const { type, url, ratio, canvas } = e.data
+  if (!canvasEl && type === 'init') {
+    canvasEl = canvas
+    ctx = canvas.getContext('2d')
+    initWasm(url)
+    canvasMsg.width = canvas.width
+    canvasMsg.height = canvas.height
+    ratioPixel = ratio
+    drawPlc(null, null, null, 'init')
+    for (let i = 0; i < 10; i++) {
+      drawPlc('DI', i, 0, 'draw')
+      drawPlc('DO', i, 0, 'draw')
+    }
+    for (let i = 0; i < 4; i++) {
+      drawPlc('AI', i, i, 'draw')
+    }
+    for (let i = 0; i < 2; i++) {
+      drawPlc('AO', i, i, 'draw')
+    }
+  }
+  if (type === 1) {
+    if (sendMsg.type === 'AI') {
+      _SetTrustAI(sendMsg.index, +sendMsg.valuel)
+    } else {
+      _SetTrustDI(sendMsg.index, +sendMsg.value)
+    }
+  }
+}

+ 34 - 17
src/views/CommandInfo.vue

@@ -141,7 +141,7 @@
       </div>
       <div class="empty" v-else>当前没有可执行任务</div>
       <div class="footer" v-if="crtCommand!.stepList.length || isExistCnt">
-        <div class="tip">如果没有执行命令,则本条任务不会被编译;如果条件不满足,将会从第一步开始重新执行</div>
+        <div class="tip">如果没有执行命令,则本条任务不会被编译;如果条件不满足,将会等待这一步执行完毕才会继续往下执行。</div>
       </div>
     </div>
 
@@ -248,7 +248,6 @@ watch(
         isExistCnt.value = true
       }
       crtCommand.value = deepClone(command) as CommandType
-      console.log(crtCommand.value)
     }
   },
   {
@@ -273,10 +272,22 @@ const columnsMap: Record<string, any> = {
     { text: 'DI7', value: 'DI7' },
     { text: 'DI8', value: 'DI8' },
     { text: 'DI9', value: 'DI9' },
+    { text: 'DO0', value: 'DO0' },
+    { text: 'DO1', value: 'DO1' },
+    { text: 'DO2', value: 'DO2' },
+    { text: 'DO3', value: 'DO3' },
+    { text: 'DO4', value: 'DO4' },
+    { text: 'DO5', value: 'DO5' },
+    { text: 'DO6', value: 'DO6' },
+    { text: 'DO7', value: 'DO7' },
+    { text: 'DO8', value: 'DO8' },
+    { text: 'DO9', value: 'DO9' },
     { text: 'AI0', value: 'AI0' },
     { text: 'AI1', value: 'AI1' },
     { text: 'AI2', value: 'AI2' },
-    { text: 'AI3', value: 'AI3' }
+    { text: 'AI3', value: 'AI3' },
+    { text: 'AO0', value: 'AO0' },
+    { text: 'AO1', value: 'AO1' }
   ],
   outputLabel: [
     { text: 'DO0', value: 'DO0' },
@@ -352,6 +363,7 @@ const calendarType = ref<'single' | 'range'>('single')
 const onConfirm = () => {
   console.log(pickerValue.value, currentTime.value, pickerType.value)
   if (crtCommand.value) {
+    const crtStep = crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index]
     if (pickerType.value !== 'timeValue') {
       if (pickerValue.value.length && pickerValue.value[0] === '自定义') {
         showCalendar.value = true
@@ -360,18 +372,23 @@ const onConfirm = () => {
         showCalendar.value = true
         calendarType.value = 'single'
       } else {
-        crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] = pickerValue.value[0]
+        crtStep[changeInfo.key] = pickerValue.value[0]
 
         if (pickerValue.value[0].includes('AI') || pickerValue.value[0].includes('AO')) {
-          crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '0'
-          crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].operation = '='
+          if (crtStep.value === '' || !crtStep.value) {
+            crtStep.value = '0'
+          }
+          if (crtStep.operation === '' || !crtStep.operation) {
+            crtStep.operation = '='
+          }
         } else if (pickerValue.value[0].includes('DI') || pickerValue.value[0].includes('DO')) {
-          crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '闭合'
+          if (crtStep.value === '' || !crtStep.value) {
+            crtStep.value = '闭合'
+          }
         }
       }
     } else {
-      crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
-        currentTime.value.join(':')
+      crtStep[changeInfo.key] = currentTime.value.join(':')
     }
     currentTime.value = ['00', '00']
     pickerValue.value = []
@@ -410,7 +427,7 @@ const handleEditCommand = (type: string, index: number, cIndex?: number) => {
         id: '' + Date.now(),
         type: 'input',
         label: 'DI0',
-        value: '闭合',
+        value: '闭合'
         // x,
         // y
       })
@@ -420,7 +437,7 @@ const handleEditCommand = (type: string, index: number, cIndex?: number) => {
         id: '' + Date.now(),
         type: 'output',
         label: 'DO0',
-        value: '打开',
+        value: '打开'
         // x,
         // y
       })
@@ -431,7 +448,7 @@ const handleEditCommand = (type: string, index: number, cIndex?: number) => {
         type: 'time',
         label: '每天',
         operation: '等于',
-        value: '08:00',
+        value: '08:00'
         // x,
         // y
       })
@@ -492,11 +509,11 @@ const handleAddCommand = async (type: string) => {
             id,
             label: 'DI0',
             value: '闭合',
-            type: 'input',
+            type: 'input'
             // x,
             // y: y + 100
           }
-        ],
+        ]
         // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
         // y
       })
@@ -506,7 +523,7 @@ const handleAddCommand = async (type: string) => {
         id,
         type: 'delay',
         value: '5',
-        unit: 's',
+        unit: 's'
         // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
         // y
       })
@@ -520,11 +537,11 @@ const handleAddCommand = async (type: string) => {
             id,
             label: 'DO0',
             value: '打开',
-            type: 'output',
+            type: 'output'
             // x,
             // y: y + 100
           }
-        ],
+        ]
         // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
         // y
       })

+ 32 - 20
src/views/ProjectInfo.vue

@@ -40,7 +40,8 @@
       <template v-if="showMorePopup">
         <van-cell-group :style="{ marginTop: '16px' }">
           <van-cell title="编辑工程名称" clickable @click="handleEditName" :title-style="{ color: '#07c160' }" />
-          <van-cell title="编译文件" @click="get_c_code" clickable :title-style="{ color: '#1989fa' }" />
+          <van-cell title="编译文件" @click="get_c_code('wx')" clickable :title-style="{ color: '#1989fa' }" />
+          <van-cell title="虚拟运行" @click="get_c_code('wxsimu')" clickable :title-style="{ color: '#1989fa' }" />
           <van-cell clickable v-show="crtProject?.isCompiled">
             <template #title>
               <div v-html="wxtag" class="wx-tag"></div>
@@ -144,6 +145,8 @@ watch(
   () => route,
   () => {
     crtProject.value = projectList.value.find((item) => item.id === route.params.id)
+    // console.log(JSON.stringify(crtProject.value))
+
     wxShareTag.value = `<wx-open-launch-weapp appid="wx4c5a777c71f2981c" style="width: 100%;" path="pages/sharefile/index?content=${encodeURIComponent(JSON.stringify({ ...crtProject.value, qrcodeType: 'worldflying_plc_editor' }))}&filename=${crtProject.value?.name}.jmpec"><template><text style="color: #896ef4;">分享</text></template></wx-open-launch-weapp>`
     let imgobjStr = localStorage.getItem('imgobj' + crtProject.value!.id)
     if (imgobjStr) {
@@ -203,6 +206,7 @@ const handleCancel = async () => {
 const handleDelete = (val: string) => {
   if (val) {
     const project = projectList.value.find((item) => (item.id = crtProject.value!.id))
+
     if (project) {
       if (project.commandList.length === 1) return showFailToast('请至少保留一个任务!')
       project.commandList = project.commandList.filter((item) => item.id !== val)
@@ -1520,13 +1524,13 @@ const handleAddCommond = () => {
 //   // link.remove()
 // }
 
-const get_c_code = () => {
+const get_c_code = (codetype: string) => {
   if (!crtProject.value) return
   if (crtProject.value.commandList.length === 0) return showFailToast('请先添加任务!')
   if (crtProject.value.commandList.length === 1 && crtProject.value.commandList[0].stepList.length === 0) {
     return showFailToast('任务内容为空!')
   }
-  if (crtProject.value.isCompiled) {
+  if (crtProject.value.isCompiled && codetype === 'wx') {
     return showFailToast('工程已编译!')
   }
   const isExistExec = crtProject.value.commandList.some((item) =>
@@ -1643,10 +1647,7 @@ static PT_THREAD(PROGRAM${index}_body(struct pt *pt, TIME *pt_delay))
           })
           if (condition_code !== '') {
             body_code += `
-    if (!(${condition_code}))
-    {
-        PT_EXIT(pt);
-    }`
+        PT_WAIT_UNTIL(pt, ${condition_code});`
           }
         }
       })
@@ -1665,7 +1666,8 @@ void IEC_run(void)
   const params = {
     devicetype: 'PLC101',
     xml: c_code,
-    codetype: 'wx'
+    codetype,
+    getlog: true
   }
   fetch('https://plceditor.worldflying.cn/api/build/buildcode', {
     method: 'POST',
@@ -1680,20 +1682,30 @@ void IEC_run(void)
       // 关闭loading
       loading.close()
       if (res.errcode === 3000) {
-        showFailToast('编译失败!')
+        showFailToast('操作失败!')
       } else if (res.errcode === 0) {
-        localStorage.setItem(
-          'imgobj' + crtProject.value!.id,
-          JSON.stringify({
-            url: res.url,
-            deadline: new Date().getTime() + 30 * 60 * 1000
+        if (codetype === 'wx') {
+          localStorage.setItem(
+            'imgobj' + crtProject.value!.id,
+            JSON.stringify({
+              url: res.url,
+              deadline: new Date().getTime() + 30 * 60 * 1000
+            })
+          )
+          crtProject.value!.isCompiled = true
+          crtProject.value!.CompileTime = formatDateTime(new Date())!
+          wxtag.value = `<wx-open-launch-weapp appid="wx4c5a777c71f2981c" style="width: 100%;" path="pages/index/rj45cfgbybt/plc/index?imgurl=${res.url}"><template><text style="color: #7232dd;">烧录设备</text></template></wx-open-launch-weapp>`
+          console.log('本地存储成功')
+          showSuccessToast('编译成功!')
+        } else {
+          router.push({
+            name: 'runtime',
+            query: {
+              id: crtProject.value!.id,
+              url: res.url
+            }
           })
-        )
-        crtProject.value!.isCompiled = true
-        crtProject.value!.CompileTime = formatDateTime(new Date())!
-        wxtag.value = `<wx-open-launch-weapp appid="wx4c5a777c71f2981c" style="width: 100%;" path="pages/index/rj45cfgbybt/plc/index?imgurl=${res.url}"><template><text style="color: #7232dd;">烧录设备</text></template></wx-open-launch-weapp>`
-        console.log('本地存储成功')
-        showSuccessToast('编译成功!')
+        }
       }
     })
 }

+ 94 - 0
src/views/runtime.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="runtime-container">
+    <div class="header">
+      <div class="go-back" @click="router.push(`/project-info/${crtProject!.id}`)">
+        <van-button size="small"><van-icon name="arrow-left" />返回</van-button>
+      </div>
+      <div class="title">{{ crtProject?.name }}</div>
+    </div>
+    <div class="canvas-main" ref="canvasMainRef">
+      <canvas ref="canvasRef"></canvas>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { ProjectType } from '@/types/global'
+import { useGlobalStore } from '@/stores/global'
+import { storeToRefs } from 'pinia'
+
+const { projectList } = storeToRefs(useGlobalStore())
+
+const route = useRoute()
+const router = useRouter()
+const crtProject = ref<ProjectType>()
+watch(
+  () => route,
+  () => {
+    crtProject.value = projectList.value.find((item) => item.id === route.query.id)
+  },
+  {
+    deep: true,
+    immediate: true,
+    once: true
+  }
+)
+const canvasRef = ref<HTMLCanvasElement | null>(null)
+const canvasMainRef = ref<HTMLElement | null>(null)
+const ratio = window.devicePixelRatio
+
+const runtimeWorker = new Worker(new URL('@/utils/worker.js', import.meta.url))
+
+onMounted(() => {
+  if (!canvasRef.value || !canvasMainRef.value) return
+  const w = Math.floor(canvasMainRef.value.clientWidth)
+  const h = Math.floor(canvasMainRef.value.clientHeight)
+  canvasRef.value.width = w * ratio
+  canvasRef.value.height = h * ratio
+  canvasRef.value.style.width = `${w}px`
+  canvasRef.value.style.height = `${h}px`
+  const offscreenCanvas = canvasRef.value.transferControlToOffscreen()
+  runtimeWorker.postMessage(
+    {
+      type: 'init',
+      url: route.query.url,
+      ratio,
+      canvas: offscreenCanvas
+    },
+    [offscreenCanvas]
+  )
+})
+
+onBeforeUnmount(() => {
+  runtimeWorker.terminate()
+})
+</script>
+
+<style scoped>
+.runtime-container {
+  padding: 16px 0 0;
+  height: 100%;
+  /* overflow: hidden; */
+  background: #fff;
+}
+.header {
+  padding: 0 16px 16px;
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+.header .title {
+  flex: 1;
+  padding: 8px 16px;
+  background: #dfebf9;
+  border-radius: 4px;
+  line-height: 1.5;
+  border: 1px solid #ccc;
+}
+.canvas-main {
+  margin: 0 16px;
+  height: calc(100% - 80px);
+  border: 1px solid #666;
+  /* overflow: hidden; */
+}
+</style>