From 8c84c7277018b259f5b99f26cbfd14603bc4e4c0 Mon Sep 17 00:00:00 2001
From: haoxuan <haoxuan>
Date: 星期三, 01 十一月 2023 19:39:53 +0800
Subject: [PATCH] Merge branch 'dev' of http://192.168.5.5:10010/r/web/bulletin-board-style1 into dev

---
 src/views/dashboard/components/TaskControl.vue      |   96 +++++
 package-lock.json                                   |   38 ++
 src/components.d.ts                                 |    1 
 src/stores/tasks.ts                                 |   33 ++
 src/api/device.ts                                   |    9 
 src/api/index.ts                                    |   32 ++
 src/views/dashboard/components/TaskControlModal.vue |  573 +++++++++++++++++++++++++----------
 src/components/icons/AlertLightIcon.vue             |   36 ++
 src/common/composable/useCountDown.ts               |   11 
 src/views/dashboard/index.vue                       |   15 
 package.json                                        |    4 
 src/stores/devices.ts                               |   23 +
 src/App.vue                                         |    1 
 src/views/dashboard/components/DashboardTitle.vue   |   51 +++
 14 files changed, 742 insertions(+), 181 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 6e1f76b..42e9c53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
       "dependencies": {
         "@element-plus/icons-vue": "^2.1.0",
         "@vueuse/core": "^10.5.0",
+        "@xstate/vue": "^2.0.0",
         "axios": "^1.5.1",
         "dayjs": "^1.11.10",
         "element-plus": "^2.4.1",
@@ -17,7 +18,8 @@
         "pinia": "^2.1.7",
         "vue": "^3.3.4",
         "vue-hooks-plus": "^1.8.5",
-        "vue-router": "^4.2.5"
+        "vue-router": "^4.2.5",
+        "xstate": "^4.38.3"
       },
       "devDependencies": {
         "@iconify-json/mdi": "^1.1.55",
@@ -1473,6 +1475,24 @@
       },
       "peerDependenciesMeta": {
         "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xstate/vue": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@xstate/vue/-/vue-2.0.0.tgz",
+      "integrity": "sha512-JlrJ3d+I6rZCcFBuu3O4GP+mGJfd11O9o69wRedzPMqZ+hxcMRBsih9L5kKnJHcU9CTmdJTT172oxTaYF7thzA==",
+      "peerDependencies": {
+        "@xstate/fsm": "^2.0.0",
+        "vue": "^3.0.0",
+        "xstate": "^4.31.0"
+      },
+      "peerDependenciesMeta": {
+        "@xstate/fsm": {
+          "optional": true
+        },
+        "xstate": {
           "optional": true
         }
       }
@@ -4923,6 +4943,11 @@
         "node": ">=12"
       }
     },
+    "node_modules/xstate": {
+      "version": "4.38.3",
+      "resolved": "https://registry.npmmirror.com/xstate/-/xstate-4.38.3.tgz",
+      "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw=="
+    },
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
@@ -5951,6 +5976,12 @@
           "requires": {}
         }
       }
+    },
+    "@xstate/vue": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@xstate/vue/-/vue-2.0.0.tgz",
+      "integrity": "sha512-JlrJ3d+I6rZCcFBuu3O4GP+mGJfd11O9o69wRedzPMqZ+hxcMRBsih9L5kKnJHcU9CTmdJTT172oxTaYF7thzA==",
+      "requires": {}
     },
     "acorn": {
       "version": "8.10.0",
@@ -8557,6 +8588,11 @@
       "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
       "dev": true
     },
+    "xstate": {
+      "version": "4.38.3",
+      "resolved": "https://registry.npmmirror.com/xstate/-/xstate-4.38.3.tgz",
+      "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw=="
+    },
     "yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
diff --git a/package.json b/package.json
index e5cd1da..a630e65 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.1.0",
     "@vueuse/core": "^10.5.0",
+    "@xstate/vue": "^2.0.0",
     "axios": "^1.5.1",
     "dayjs": "^1.11.10",
     "element-plus": "^2.4.1",
@@ -22,7 +23,8 @@
     "pinia": "^2.1.7",
     "vue": "^3.3.4",
     "vue-hooks-plus": "^1.8.5",
-    "vue-router": "^4.2.5"
+    "vue-router": "^4.2.5",
+    "xstate": "^4.38.3"
   },
   "devDependencies": {
     "@iconify-json/mdi": "^1.1.55",
diff --git a/src/App.vue b/src/App.vue
index 6cc3382..8217457 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -4,6 +4,7 @@
 
 <script setup lang="ts">
 import { RouterView } from 'vue-router'
+import 'element-plus/es/components/message/style/css'
 </script>
 
 <style scoped></style>
diff --git a/src/api/device.ts b/src/api/device.ts
new file mode 100644
index 0000000..ceaaec2
--- /dev/null
+++ b/src/api/device.ts
@@ -0,0 +1,9 @@
+export interface Devices {
+  systemDeviceID: string
+  currentDeviceID: string
+  systemDeviceStatus: number
+  clusterStatus: string
+  clusterNodeQuantity: number
+  systemDeviceRunSince: number
+  deviceIDList: string[]
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index 708b8b2..c8e30e9 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,6 +1,7 @@
 import { request } from '@/common/utils'
 import type { CraftParamsResponse, TasksGroupByChannel } from './task'
 import type { PLCResponse } from './plc'
+import type { Devices } from './device'
 
 export interface BaseResponse<T = any> {
   code: number
@@ -66,6 +67,11 @@
 export interface SendProcessParamsParams {
   procedureId: number
 }
+
+/**
+ * 涓嬪彂宸ヨ壓鍙傛暟
+ * @param params
+ */
 export function sendProcessParams(params: SendProcessParamsParams) {
   return request<BaseResponse>({
     url: `v1/task/sendProcessParams`,
@@ -73,3 +79,29 @@
     data: params
   })
 }
+
+export interface FinishTaskParams {
+  id: number
+}
+
+/**
+ * 缁撴潫浠诲姟
+ * @param params
+ */
+export function finishTask(params: FinishTaskParams) {
+  return request<BaseResponse>({
+    url: `v1/task/finish/${params.id}`,
+    method: 'put',
+    data: params
+  })
+}
+
+/**
+ * 鑾峰彇褰撳墠闈㈡澘缁戝畾鐨勮澶囧垪琛�
+ */
+export function getDeviceList() {
+  return request<BaseResponse<Devices>>({
+    url: `/v1/device/list`,
+    method: 'get'
+  })
+}
diff --git a/src/common/composable/useCountDown.ts b/src/common/composable/useCountDown.ts
index a5dfa62..4adb7d6 100644
--- a/src/common/composable/useCountDown.ts
+++ b/src/common/composable/useCountDown.ts
@@ -19,7 +19,7 @@
 }
 
 function useCountDown(seconds: number, options?: Options) {
-  const timer = new StepTimer(seconds * 1000, 1000)
+  let timer = new StepTimer(seconds * 1000, 1000)
 
   const remainingSeconds = ref<number>(seconds)
 
@@ -65,6 +65,12 @@
     timer.abort()
   }
 
+  function reset() {
+    timer.destroy()
+    remainingSeconds.value = seconds
+    timer = new StepTimer(seconds * 1000, 1000)
+  }
+
   return {
     startCountdown,
     remainingSeconds,
@@ -72,7 +78,8 @@
     continueCountdown,
     stopCountdown,
     formattedTime,
-    countdownStatus
+    countdownStatus,
+    reset
   }
 }
 
diff --git a/src/components.d.ts b/src/components.d.ts
index cfba2f9..c544730 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -7,6 +7,7 @@
 
 declare module 'vue' {
   export interface GlobalComponents {
+    AlertLightIcon: typeof import('./components/icons/AlertLightIcon.vue')['default']
     BaseModal: typeof import('./components/BaseModal.vue')['default']
     DashboardLayout: typeof import('./components/DashboardLayout.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
diff --git a/src/components/icons/AlertLightIcon.vue b/src/components/icons/AlertLightIcon.vue
new file mode 100644
index 0000000..3a70684
--- /dev/null
+++ b/src/components/icons/AlertLightIcon.vue
@@ -0,0 +1,36 @@
+<script setup lang="ts"></script>
+
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    version="1.1"
+    width="200px"
+    height="200px"
+    viewBox="0 0 200 200"
+    style="
+      shape-rendering: geometricPrecision;
+      text-rendering: geometricPrecision;
+      image-rendering: optimizeQuality;
+      fill-rule: evenodd;
+      clip-rule: evenodd;
+    "
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+  >
+    <g>
+      <path
+        style="opacity: 0.988"
+        fill="#d71d05"
+        d="M 87.5,-0.5 C 95.1667,-0.5 102.833,-0.5 110.5,-0.5C 146.888,7.72465 167.388,30.3913 172,67.5C 172.5,101.165 172.667,134.832 172.5,168.5C 123.833,168.5 75.1667,168.5 26.5,168.5C 26.3333,132.498 26.5,96.4985 27,60.5C 33.9986,27.0017 54.1653,6.66837 87.5,-0.5 Z M 101.5,40.5 C 103.678,52.6006 105.011,64.9339 105.5,77.5C 116.858,77.4139 128.191,77.9139 139.5,79C 123.945,100.223 108.778,121.723 94,143.5C 93.167,128.176 92.667,112.842 92.5,97.5C 80.4954,97.6665 68.4954,97.4999 56.5,97C 71.8692,78.4097 86.8692,59.5764 101.5,40.5 Z"
+      />
+    </g>
+    <g>
+      <path
+        style="opacity: 0.995"
+        fill="#d81d05"
+        d="M 199.5,183.5 C 199.5,186.833 199.5,190.167 199.5,193.5C 197.167,195.167 195.167,197.167 193.5,199.5C 130.833,199.5 68.1667,199.5 5.5,199.5C 3.83333,197.167 1.83333,195.167 -0.5,193.5C -0.5,190.167 -0.5,186.833 -0.5,183.5C 1.67098,181.5 4.00432,179.666 6.5,178C 68.5,177.333 130.5,177.333 192.5,178C 194.996,179.666 197.329,181.5 199.5,183.5 Z"
+      />
+    </g>
+  </svg>
+</template>
+
+<style scoped lang="scss"></style>
diff --git a/src/stores/devices.ts b/src/stores/devices.ts
new file mode 100644
index 0000000..a961994
--- /dev/null
+++ b/src/stores/devices.ts
@@ -0,0 +1,23 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+import type { Devices } from '@/api/device'
+import { getDeviceList } from '@/api'
+
+export const useDevicesStore = defineStore('counter', () => {
+  const devices = ref<Devices>()
+  const deviceIDList = computed(() => devices?.value?.deviceIDList ?? [])
+
+  function getDevicesInfo() {
+    getDeviceList().then(
+      (res) => {
+        devices.value = res?.data
+      },
+      (err) => {
+        console.error(err)
+        devices.value = undefined
+      }
+    )
+  }
+
+  return { devices, deviceIDList, getDevicesInfo }
+})
diff --git a/src/stores/tasks.ts b/src/stores/tasks.ts
index 8f8599d..02f2694 100644
--- a/src/stores/tasks.ts
+++ b/src/stores/tasks.ts
@@ -25,7 +25,7 @@
       offset: 0,
       limit: 3
     }
-    getTaskList(params)
+    return getTaskList(params)
       .then((res) => {
         channels.value = res.data
       })
@@ -33,6 +33,36 @@
         console.error(err)
         channels.value = []
       })
+  }
+
+  /**
+   * 鏁版嵁鍔犺浇瀹屾垚鍚庤嚜鍔ㄩ�変腑涓�涓换鍔�
+   */
+  function autoSelectTask(channel: number) {
+    const currentChannelTaskList = channels.value[channel].Tasks
+    if (currentChannelTaskList?.length) {
+      activeTask.value = currentChannelTaskList[0]
+    } else {
+      const firstNotEmptyChannel = Object.entries(channels.value).find((ele) => {
+        const taskList = (ele[1] as TasksResponse)?.Tasks
+
+        return !!taskList.length
+      })
+
+      if (firstNotEmptyChannel) {
+        const channelNumber = +firstNotEmptyChannel[0]
+        activeTask.value = channels.value[channelNumber].Tasks[0]
+      }
+    }
+  }
+
+  /**
+   * 鍒锋柊鎵�鏈夋暟鎹�
+   */
+  function reload(channel: number) {
+    getChannels(currentType.value).then(() => {
+      autoSelectTask(channel)
+    })
   }
 
   function moreChannelTasksBtn(channelNumber: number) {
@@ -96,6 +126,7 @@
     getChannels,
     moreBtnStatus,
     activeTask,
+    reload,
     setActiveTask,
     requestParamsMap,
     getParamsByChannel,
diff --git a/src/views/dashboard/components/DashboardTitle.vue b/src/views/dashboard/components/DashboardTitle.vue
new file mode 100644
index 0000000..ed14226
--- /dev/null
+++ b/src/views/dashboard/components/DashboardTitle.vue
@@ -0,0 +1,51 @@
+<template>
+  <div class="dashboard-title">
+    <div class="title-text">鏅鸿兘宸ヤ綔鍙� 鈥� {{ props?.deviceInfo?.currentDeviceID ?? '' }}</div>
+    <div class="title-status">
+      <div class="connection-info" @click="openSelectDeviceModal">
+        <el-icon size="30" color="red">
+          <AlertLightIcon></AlertLightIcon>
+        </el-icon>
+      </div>
+      <div class="connection-status"></div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import type { Devices } from '@/api/device'
+import AlertLightIcon from '@/components/icons/AlertLightIcon.vue'
+import { ref } from 'vue'
+
+export interface DashBoardTitleProps {
+  deviceInfo: Devices
+}
+
+const props = defineProps<DashBoardTitleProps>()
+
+const showModal = ref(false)
+
+function openSelectDeviceModal() {
+  showModal.value = true
+}
+</script>
+
+<style scoped lang="scss">
+.dashboard-title {
+  position: relative;
+}
+.title-text {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 40px;
+  font-weight: 700;
+}
+.title-status {
+  position: absolute;
+  top: 16px;
+  right: 40px;
+}
+.connection-info {
+  cursor: pointer;
+}
+</style>
diff --git a/src/views/dashboard/components/TaskControl.vue b/src/views/dashboard/components/TaskControl.vue
index 9a69334..47b9945 100644
--- a/src/views/dashboard/components/TaskControl.vue
+++ b/src/views/dashboard/components/TaskControl.vue
@@ -23,11 +23,30 @@
       <template v-if="task?.Procedure.Status === 2 || task?.Procedure.Status === 3">
         <BigButton class="btn" bg-color="#ff9933">鎵撳嵃</BigButton>
         <BigButton class="btn" bg-color="#00cc33">鎶ュ伐</BigButton>
-        <BigButton class="btn" bg-color="#ff0000">瀹屾垚</BigButton>
+        <el-popconfirm
+          width="340"
+          confirm-button-text="纭畾"
+          cancel-button-text="鍙栨秷"
+          :icon="CircleCloseFilled"
+          icon-color="red"
+          :hide-after="0"
+          :teleported="false"
+          title="璇风‘璁ゆ槸鍚﹀凡瀹屾垚姝ょ敓浜т换鍔�?"
+          placement="top"
+          @confirm="finishTaskProduce"
+        >
+          <template #reference>
+            <BigButton class="btn" bg-color="#ff0000">瀹屾垚</BigButton>
+          </template>
+        </el-popconfirm>
       </template>
     </div>
   </div>
-  <TaskControlModal v-model="showTaskControlModal" :task="task"></TaskControlModal>
+  <TaskControlModal
+    v-model="showTaskControlModal"
+    :task="task"
+    @produce-start="emit('shouldReload', task)"
+  ></TaskControlModal>
 </template>
 <script setup lang="ts">
 import type { Task } from '@/api/task'
@@ -35,11 +54,18 @@
 import BigButton from '@/views/dashboard/components/BigButton.vue'
 import { useDateFormat } from '@vueuse/core'
 import TaskControlModal from '@/views/dashboard/components/TaskControlModal.vue'
+import { CircleCloseFilled } from '@element-plus/icons-vue'
+import { finishTask } from '@/api'
+import { ElMessage } from 'element-plus'
 
 const props = defineProps<{
   task?: Task
 }>()
 const { task } = toRefs(props)
+
+const emit = defineEmits<{
+  shouldReload: [task: Task]
+}>()
 
 const showTaskControlModal = ref(false)
 
@@ -48,6 +74,35 @@
  */
 function startProduce() {
   showTaskControlModal.value = true
+}
+
+/**
+ * 瀹屾垚浠诲姟
+ */
+function finishTaskProduce() {
+  if (task?.value?.Procedure?.ID) {
+    finishTask({ id: task!.value.Procedure.ID }).then(
+      (res) => {
+        ElMessage({
+          message: '鎿嶄綔鎴愬姛锛�',
+          type: 'success'
+        })
+        emit('shouldReload', task.value)
+      },
+      (err) => {
+        console.error(err)
+        ElMessage({
+          message: '鎿嶄綔澶辫触锛�',
+          type: 'warning'
+        })
+      }
+    )
+  } else {
+    ElMessage({
+      message: '褰撳墠璁惧娌℃湁宸ュ簭锛�',
+      type: 'warning'
+    })
+  }
 }
 
 /**
@@ -119,4 +174,41 @@
 .finish-btn {
   background-color: #ff0000;
 }
+
+:deep(.el-popper) {
+  background-color: #133f97;
+  color: #fff;
+}
+:deep(.el-popconfirm__main) {
+  font-size: 25px;
+}
+:deep(.el-popconfirm__icon) {
+  font-size: 38px;
+}
+:deep(.el-popconfirm__main) {
+  margin-bottom: 20px;
+}
+:deep(.el-popconfirm__action) {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+}
+:deep(.el-popconfirm__action .el-button) {
+  font-size: 22px;
+  height: 54px;
+  width: 140px;
+  color: #fff;
+  background-color: #0ae5ec;
+  &:hover {
+    background-color: #0ae5ec;
+  }
+  &.is-text {
+    color: #92a1c0;
+    background-color: #133f97;
+    border: 1px solid #0ae5ec;
+    &:hover {
+      background-color: #133f97;
+    }
+  }
+}
 </style>
diff --git a/src/views/dashboard/components/TaskControlModal.vue b/src/views/dashboard/components/TaskControlModal.vue
index 247a850..e5980ab 100644
--- a/src/views/dashboard/components/TaskControlModal.vue
+++ b/src/views/dashboard/components/TaskControlModal.vue
@@ -1,141 +1,148 @@
 <template>
   <div class="task-control-modal">
     <BaseModal v-model="modelData" :wider="false">
-      <template #title>鏂颁换鍔�</template>
+      <template #title>
+        {{ !['涓嬪彂鍙傛暟鎴愬姛', '涓嬪彂鍙傛暟澶辫触'].includes(state.value as string) ? '鏂颁换鍔�' : '鎻愮ず' }}
+      </template>
       <div class="modal-content">
-        <div v-if="!messageError" class="content-title">
-          <div class="content-title-item">褰撳墠浠诲姟锛歿{ task?.Procedure.procedure.procedureName || '' }}</div>
-          <div class="content-title-item">
-            鐢熶骇鏁伴噺锛�
-            <div class="leaf-shape box">
-              {{ task?.Order?.amount || 0 }}
+        <template v-if="['鍒濆鍖�', '璁℃椂涓�', '鍑嗗鐢熶骇', '涓嬪彂鍙傛暟涓�'].includes(state.value as string)">
+          <div class="content-title">
+            <div class="content-title-item">褰撳墠浠诲姟锛歿{ task?.Procedure.procedure.procedureName || '' }}</div>
+            <div class="content-title-item">
+              鐢熶骇鏁伴噺锛�
+              <div class="leaf-shape">
+                {{ task?.Order?.amount || 0 }}
+              </div>
             </div>
           </div>
-        </div>
 
-        <div v-if="!!messageError" class="content-tips">
-          <div class="error-t">
-            <span v-if="messageError === '涓嬪彂鎴愬姛锛�'" class="el-icon-success color_success"></span>
-            <span v-else class="el-icon-error color_error"></span>
-          </div>
-          <div class="error-m">
-            {{ messageError }}
-          </div>
-          <div class="font_size_20 color_fff" style="text-align: center; width: 100%; margin: 10px 0">
-            <span v-if="messageError === '涓嬪彂鎴愬姛锛�'" style="font-size: 30px"></span>
-            <span v-else>璇烽噸璇�</span>
-          </div>
-        </div>
+          <div class="content-scroll">
+            <div class="scroll-container">
+              <el-scrollbar always class="scroller">
+                <template v-if="task">
+                  <div class="info">
+                    <div class="info-item">璁㈠崟缂栧彿锛歿{ task.Order.orderId || '' }}</div>
+                    <div class="info-item">宸ュ崟缂栧彿锛歿{ task.Order.workOrderId || '' }}</div>
+                    <div class="info-item">浜у搧鍚嶇О锛歿{ task.Order.productName || '' }}</div>
+                    <div class="info-item">鏁伴噺锛歿{ task.Order.amount || 0 }}{{ task.Order.unit }}</div>
+                    <div class="info-item">浜よ揣鏃ユ湡锛歿{ task.Order.deliverDate || '' }}</div>
+                    <div class="info-item">宸ユ椂锛� {{ task.Procedure.procedure.workHours || '' }}</div>
+                    <div class="info-item">
+                      璁″垝鏃堕棿锛� {{ formatDate(task.Procedure.startTime) || '' }}
+                      -
+                      {{ formatDate(task.Procedure.endTime) }}
+                    </div>
 
-        <div v-else class="content-scroll">
-          <div class="scroll-container">
-            <el-scrollbar always class="scroller">
-              <template v-if="task">
-                <div class="info">
-                  <div class="info-item">璁㈠崟缂栧彿锛歿{ task.Order.orderId || '' }}</div>
-                  <div class="info-item">宸ュ崟缂栧彿锛歿{ task.Order.workOrderId || '' }}</div>
-                  <div class="info-item">浜у搧鍚嶇О锛歿{ task.Order.productName || '' }}</div>
-                  <div class="info-item">鏁伴噺锛歿{ task.Order.amount || 0 }}{{ task.Order.unit }}</div>
-                  <div class="info-item">浜よ揣鏃ユ湡锛歿{ task.Order.deliverDate || '' }}</div>
-                  <div class="info-item">宸ユ椂锛� {{ task.Procedure.procedure.workHours || '' }}</div>
-                  <div class="info-item">
-                    璁″垝鏃堕棿锛� {{ formatDate(task.Procedure.startTime) || '' }}
-                    -
-                    {{ formatDate(task.Procedure.endTime) }}
-                  </div>
+                    <div class="info-item">瀹㈡埛鍚嶇О锛歿{ task.Order.customer || '' }}</div>
+                    <div class="info-item info-item-two">閫氶亾锛� {{ CHANNEL_NAME_MAP[task.Channel] || '' }}</div>
 
-                  <div class="info-item">瀹㈡埛鍚嶇О锛歿{ task.Order.customer || '' }}</div>
-                  <div class="info-item info-item-two">閫氶亾锛� {{ CHANNEL_NAME_MAP[task.Channel] || '' }}</div>
+                    <div class="info-item info-item-two">鍙傛暟瑕佹眰锛歿{ task.Order.parameter || '' }}</div>
 
-                  <div class="info-item info-item-two">鍙傛暟瑕佹眰锛歿{ task.Order.parameter || '' }}</div>
-
-                  <div class="info-item-two">
-                    <div style="color: #4efefa; font-size: 18px; margin-bottom: 10px; margin-top: 20px">宸ヨ壓鍙傛暟</div>
-                    <div v-for="(item, index) in craftParams" :key="index" class="info-item info-item-two">
-                      {{ item.Key }}锛歿{ item.Value || '' }}
+                    <div class="info-item-two">
+                      <div style="color: #4efefa; font-size: 18px; margin-bottom: 10px; margin-top: 20px">宸ヨ壓鍙傛暟</div>
+                      <div v-for="(item, index) in craftParams" :key="index" class="info-item info-item-two">
+                        {{ item.Key }}锛歿{ item.Value || '' }}
+                      </div>
                     </div>
                   </div>
-                </div>
-
-                <div class="title-auto-box"></div>
-                <div v-if="getCraftParamsErrMsg" class="process-err-tip">
-                  <div class="tip-icon">
-                    <span class="el-icon-error color_error"></span>
-                  </div>
-                  <div class="tip-content">鎻愮ず: {{ getCraftParamsErrMsg }}</div>
-                </div>
-
-                <div v-if="countdown30s.countdownStatus.value === 'running'" class="countdown">
-                  {{ countdown30s.formattedTime.value }}
-                </div>
-
-                <!--              <div v-if="showBtn === 2 || showBtn === 3" class="process-box">-->
-                <!--                <div-->
-                <!--                  style="-->
-                <!--                    color: red;-->
-                <!--                    font-size: 26px;-->
-                <!--                    width: 100%;-->
-                <!--                    text-align: center;-->
-                <!--                    margin-bottom: 15px;-->
-                <!--                    line-height: 35px;-->
-                <!--                  "-->
-                <!--                  :class="showBtn === 3 && isLoading ? 'margin-top-10px' : 'margin-top-40px'"-->
-                <!--                >-->
-                <!--                  <div v-if="showBtn === 2 || (showBtn === 3 && !isLoading)" class="gif-box">-->
-                <!--                    <template v-if="showBtn === 2">-->
-                <!--                      <div class="gif">-->
-                <!--                        <img src="../../public/shan.gif" />-->
-                <!--                      </div>-->
-                <!--                    </template>-->
-                <!--                    <template v-if="showBtn === 3 && !isLoading">-->
-                <!--                      <div class="gif">-->
-                <!--                        <span class="yuandian"></span>-->
-                <!--                      </div>-->
-                <!--                    </template>-->
-                <!--                    <div class="gif-right">-->
-                <!--                      <div>-&#45;&#45;&#45;&#45; 鍓╀綑鏃堕棿 -&#45;&#45;&#45;&#45;</div>-->
-                <!--                      <div>-->
-                <!--                        <span>00:{{ countdown30s.formattedTime }}</span>-->
-                <!--                      </div>-->
-                <!--                    </div>-->
-                <!--                  </div>-->
-                <!--                  {{ message }}-->
-                <!--                </div>-->
-                <!--                <template v-if="showBtn === 3 && isLoading">-->
-                <!--                  <div class="progress-item">-->
-                <!--                    <span>{{ (+num / 30) * 100 }}%</span>-->
-                <!--                    <el-progress-->
-                <!--                      style="width: calc(100% - 50px); float: right"-->
-                <!--                      define-back-color="#CDC6C6"-->
-                <!--                      color="#00cc66"-->
-                <!--                      text-color="#fff"-->
-                <!--                      :text-inside="true"-->
-                <!--                      :stroke-width="20"-->
-                <!--                      :percentage="(+num / 30) * 100"-->
-                <!--                    ></el-progress>-->
-                <!--                  </div>-->
-                <!--                </template>-->
-                <!--              </div>-->
-              </template>
-            </el-scrollbar>
+                </template>
+              </el-scrollbar>
+            </div>
           </div>
-        </div>
+        </template>
+
+        <!--        鍙湁鑾峰彇鍒板伐鑹哄弬鏁版墠鍙互杩涜鎿嶄綔-->
+        <template v-if="getCraftParamsTip">
+          <div class="content-tips">
+            <div class="craft-params-error">
+              <div class="error-icon">
+                <el-icon size="90" color="red"><CircleCloseFilled /></el-icon>
+              </div>
+              <div class="error-tip">{{ getCraftParamsTip }}</div>
+            </div>
+          </div>
+        </template>
+
+        <template v-else>
+          <div v-if="['璁℃椂涓�', '鍑嗗鐢熶骇'].includes(state.value as string)" class="content-tips">
+            <div class="prepare">
+              <div class="countdown">
+                <div class="alert-light">
+                  <div class="light" :class="{ blink: state.value === '璁℃椂涓�' }"></div>
+                </div>
+                <div class="time">
+                  <div class="time-label">----- 鍓╀綑鏃堕棿 -----</div>
+                  <div class="time-text">00:{{ countdown30s.formattedTime.value }}</div>
+                </div>
+              </div>
+              <div class="safe-tip">
+                {{ safeProduce }}
+              </div>
+            </div>
+          </div>
+
+          <div v-if="['涓嬪彂鍙傛暟涓�'].includes(state.value as string)" class="content-tips">
+            <div class="delivery">
+              <div class="delivery-tip">宸ヨ壓鍙傛暟涓嬪彂涓�...</div>
+              <div class="delivery-progress">
+                <el-progress :text-inside="true" :stroke-width="30" :percentage="50" status="success" />
+              </div>
+            </div>
+          </div>
+
+          <div v-if="['涓嬪彂鍙傛暟鎴愬姛'].includes(state.value as string)" class="delivery-success-tips">
+            <div class="success-icon">
+              <el-icon size="90" color="green"><SuccessFilled /></el-icon>
+            </div>
+            <div class="success-tip">{{ deliveryTip }}</div>
+            <div class="success-sub-tip">{{ countdown3s.remainingSeconds }}s</div>
+          </div>
+
+          <div v-if="['涓嬪彂鍙傛暟澶辫触'].includes(state.value as string)" class="delivery-error-tips">
+            <div class="error-icon">
+              <el-icon size="90" color="red"><CircleCloseFilled /></el-icon>
+            </div>
+            <div class="error-tip">{{ deliveryTip }}</div>
+            <div class="error-sub-tip">璇烽噸璇�</div>
+          </div>
+        </template>
       </div>
       <template #footer>
-        <div class="btn">
-          <BigButton bg-color="#4765c0" @click="closeModal">鏆傜紦鐢熶骇</BigButton>
-          <BigButton
-            v-if="countdown30s.countdownStatus.value !== 'complete'"
-            color="#0d0d0d"
-            :disabled="countdown30s.countdownStatus.value === 'running'"
-            @click="startCountdown30s"
-          >
-            鐢熶骇鍑嗗
-          </BigButton>
-          <BigButton v-if="countdown30s.countdownStatus.value === 'complete'" bg-color="#4efefa" @click="startProduce">
-            寮�濮嬬敓浜�
-          </BigButton>
-        </div>
+        <template v-if="getCraftParamsTip">
+          <div class="btn">
+            <BigButton bg-color="#4765c0" @click="closeModal"> 鍏抽棴 </BigButton>
+          </div>
+        </template>
+
+        <template v-else>
+          <div class="btn">
+            <BigButton
+              v-if="!['涓嬪彂鍙傛暟鎴愬姛', '涓嬪彂鍙傛暟澶辫触'].includes(state.value as string)"
+              bg-color="#4765c0"
+              @click="respiteProduce"
+            >
+              鏆傜紦鐢熶骇
+            </BigButton>
+            <BigButton
+              v-if="['鍒濆鍖�', '璁℃椂涓�'].includes(state.value as string)"
+              color="#0d0d0d"
+              :disabled="state.value === '璁℃椂涓�'"
+              @click="prepareProduce"
+            >
+              鐢熶骇鍑嗗
+            </BigButton>
+            <BigButton v-if="state.value === '鍑嗗鐢熶骇'" bg-color="#4efefa" @click="startProduce"> 寮�濮嬬敓浜� </BigButton>
+            <BigButton v-if="state.value === '涓嬪彂鍙傛暟涓�'" bg-color="#4efefa">
+              <el-icon class="is-loading" color="#000">
+                <Loading />
+              </el-icon>
+            </BigButton>
+            <BigButton v-if="state.value === '涓嬪彂鍙傛暟澶辫触'" bg-color="#4765c0" @click="deliverParams">
+              鍐嶆涓嬪彂
+            </BigButton>
+            <BigButton v-if="state.value === '涓嬪彂鍙傛暟鎴愬姛'" bg-color="#4765c0" @click="closeModal"> 鍏抽棴 </BigButton>
+          </div>
+        </template>
       </template>
     </BaseModal>
   </div>
@@ -143,13 +150,16 @@
 <script setup lang="ts">
 import type { CraftParam, Task } from '@/api/task'
 import { useDateFormat, useVModel } from '@vueuse/core'
-import { computed, ref, toRefs, watch } from 'vue'
+import { ref, toRefs, watch } from 'vue'
 import BigButton from '@/views/dashboard/components/BigButton.vue'
 import { CHANNEL_NAME_MAP } from '@/common/constants'
 import { getCraftParams, sendProcessParams } from '@/api'
 import { useCountDown } from '@/common/composable'
 import { storeToRefs } from 'pinia'
 import { useTasksStore } from '@/stores/tasks'
+import { createMachine } from 'xstate'
+import { useMachine } from '@xstate/vue'
+import { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue'
 
 export interface TaskControlModalProps {
   task?: Task
@@ -162,15 +172,13 @@
 })
 const emit = defineEmits<{
   'update:modelValue': [show: boolean]
+  /** 涓嬪彂鎴愬姛鍚庤Е鍙�, 鐢ㄤ簬澶栭儴鑾峰緱鍒锋柊鏁版嵁鐨勬椂鏈� */
+  produceStart: []
 }>()
 
 const modelData = useVModel(props, 'modelValue', emit)
-function closeModal() {
-  modelData.value = false
-}
-const { task } = toRefs(props)
 
-const messageError = ref('')
+const { task } = toRefs(props)
 
 /**
  * 鏍煎紡鍖栨椂闂存埑
@@ -186,8 +194,8 @@
 
 // 宸ヨ壓鍙傛暟
 const craftParams = ref<CraftParam[]>()
-// 鑾峰彇宸ヨ壓鍙傛暟澶辫触淇℃伅
-const getCraftParamsErrMsg = ref('')
+// 鑾峰彇宸ヨ壓鍙傛暟缁撴灉淇℃伅
+const getCraftParamsTip = ref('')
 
 /**
  * 鑾峰彇褰撳墠灞曠ず鐨勪换鍔$殑宸ヨ壓鍙傛暟
@@ -195,19 +203,16 @@
 function getTaskProduceParams(taskId?: number) {
   if (taskId) {
     craftParams.value = []
-    getCraftParamsErrMsg.value = ''
+    getCraftParamsTip.value = ''
     getCraftParams({ id: taskId }).then(
       (res) => {
         craftParams.value = res.data.Params ?? []
-        getCraftParamsErrMsg.value = ''
-        // TODO: 澶勭悊鍚勪釜鎸夐挳鏄鹃殣
-        // this.getInfo()
-        console.log('processParams', craftParams.value)
+        getCraftParamsTip.value = ''
       },
       (err) => {
         console.error(err)
         craftParams.value = []
-        getCraftParamsErrMsg.value = '鑾峰彇宸ヨ壓鍙傛暟澶辫触锛�'
+        getCraftParamsTip.value = '鑾峰彇宸ヨ壓鍙傛暟澶辫触锛�'
       }
     )
   }
@@ -217,59 +222,218 @@
   // 寮圭獥鏄剧ず鏃惰幏鍙栧伐鑹哄弬鏁�
   if (modelData.value) {
     getTaskProduceParams(task?.value?.Procedure?.ID)
+  } else {
+    reset()
   }
 })
 
-const countdown30s = useCountDown(3)
+const countdown30s = useCountDown(30, {
+  onEnd: () => {
+    send('缁撴潫璁℃椂')
+  }
+})
 
-function startCountdown30s() {
-  countdown30s.startCountdown()
+// 寮圭獥鏃惰幏鍙栧畨鍏ㄧ敓浜ф彁绀烘枃鏈�
+const { channels } = storeToRefs(useTasksStore())
+const safeProduce = ref('')
+watch(modelData, () => {
+  if (modelData.value) {
+    safeProduce.value = channels?.value?.[task.value.Channel]?.Prompt?.safeProduce ?? ''
+  }
+})
+
+/**
+ * 閲嶇疆寮圭獥缂撳瓨鐘舵��
+ */
+function reset() {
+  countdown30s.reset()
+  countdown3s.reset()
+  getCraftParamsTip.value = ''
+  deliveryTip.value = ''
 }
 
 /**
- * 涓嬪彂宸ヨ壓鍙傛暟
+ * 鎸夐挳鐘舵�佹満
+ * 鍙互鍘� https://stately.ai/registry/new?mode=Design 鏌ョ湅鐘舵�佽浆鎹㈠浘
  */
-function startProduce() {
-  if (task.value?.Procedure?.ID) {
-    message.value = '宸ヨ壓鍙傛暟涓嬪彂涓�...'
-
-    isLoading.value = true
-    sendProcessParams({
-      procedureId: task.value.Procedure.ID
-    })
-      .then(
-        (res) => {
-          console.log(res)
-          messageError.value = '涓嬪彂鎴愬姛'
-        },
-        (err) => {
-          console.error(err)
-          messageError.value = err.msg ? err.msg : '鎶辨瓑锛屽伐搴忎笅鍙戝け璐ワ紒'
+const toggleMachine = createMachine({
+  id: 'produce',
+  initial: '鍒濆鍖�',
+  predictableActionArguments: true,
+  states: {
+    鍒濆鍖�: {
+      on: {
+        寮�濮嬭鏃�: { target: '璁℃椂涓�' },
+        缁撴潫: { target: '鍒濆鍖�' }
+      }
+    },
+    璁℃椂涓�: {
+      on: {
+        缁撴潫璁℃椂: { target: '鍑嗗鐢熶骇' },
+        鏆傜紦鐢熶骇: { target: '鍒濆鍖�' }
+      }
+    },
+    鍑嗗鐢熶骇: {
+      on: {
+        寮�濮嬬敓浜�: { target: '涓嬪彂鍙傛暟涓�' },
+        鏆傜紦鐢熶骇: { target: '鍒濆鍖�' }
+      }
+    },
+    涓嬪彂鍙傛暟涓�: {
+      on: {
+        鎴愬姛: { target: '涓嬪彂鍙傛暟鎴愬姛' },
+        澶辫触: { target: '涓嬪彂鍙傛暟澶辫触' },
+        鏆傜紦鐢熶骇: { target: '鍒濆鍖�' }
+      }
+    },
+    涓嬪彂鍙傛暟鎴愬姛: {
+      on: {
+        缁撴潫: {
+          target: '鍒濆鍖�'
         }
-      )
-      .finally(() => {
-        isLoading.value = false
-      })
+      }
+    },
+    涓嬪彂鍙傛暟澶辫触: {
+      on: {
+        鍐嶆涓嬪彂: { target: '鍑嗗鐢熶骇' }
+      }
+    }
   }
+})
+const { state, send } = useMachine(toggleMachine)
+
+/**
+ * 鏆傜紦鐢熶骇, 鐩存帴鍏抽棴寮圭獥
+ */
+function respiteProduce() {
+  modelData.value = false
+  send('鏆傜紦鐢熶骇')
+  reset()
+}
+/**
+ * 鐢熶骇鍑嗗
+ */
+function prepareProduce() {
+  send('寮�濮嬭鏃�')
+  countdown30s.startCountdown()
 }
 
-const { channels } = storeToRefs(useTasksStore())
-const safeProduce = computed(() => {
-  if (task?.value?.Channel) {
-    return channels?.value?.[task.value.Channel]?.Prompt?.safeProduce
+// 鍙傛暟涓嬪彂鎴愬姛鎴栧け璐ョ粨鏋�
+const deliveryTip = ref('')
+
+// 鍙傛暟涓嬪彂鎴愬姛鍚庡欢鏃�3绉掑悗鍏抽棴寮圭獥
+const countdown3s = useCountDown(3, {
+  onEnd: () => {
+    closeModal()
   }
-  return ''
 })
-const message = ref(safeProduce.value)
-const isLoading = ref(false)
+
+/**
+ * 寮�濮嬬敓浜� , 涓嬪彂宸ヨ壓鍙傛暟
+ */
+function startProduce() {
+  send('寮�濮嬬敓浜�')
+
+  sendProcessParams({
+    procedureId: task.value.Procedure.ID
+  })
+    .then(
+      (res) => {
+        deliveryTip.value = '涓嬪彂鎴愬姛'
+        send('鎴愬姛')
+        countdown3s.startCountdown()
+      },
+      (err) => {
+        console.error(err)
+        deliveryTip.value = err.msg ? err.msg : '鎶辨瓑锛屽伐搴忎笅鍙戝け璐ワ紒'
+        send('澶辫触')
+      }
+    )
+    .finally(() => {})
+}
+
+/**
+ * 鍐嶆涓嬪彂
+ */
+function deliverParams() {
+  send('鍐嶆涓嬪彂')
+}
+
+/**
+ * 鍏抽棴寮圭獥
+ */
+function closeModal() {
+  modelData.value = false
+  send('缁撴潫')
+  reset()
+  emit('produceStart')
+}
 </script>
 <style scoped lang="scss">
 .modal-content {
   height: 550px;
 }
 .content-scroll {
-  height: 400px;
+  height: 350px;
   overflow: hidden;
+}
+.content-tips {
+  height: 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 40px;
+}
+
+@keyframes blink {
+  from {
+    opacity: 1;
+  }
+  50% {
+    opacity: 1;
+  }
+  51% {
+    opacity: 0;
+  }
+  to {
+    opacity: 0;
+  }
+}
+.prepare {
+  width: 100%;
+  .safe-tip {
+    width: 100%;
+    text-align: center;
+    color: red;
+    font-size: 30px;
+    margin-top: 10px;
+    background-color: #142974;
+  }
+}
+.countdown {
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  .alert-light {
+    margin-right: 20px;
+    .light {
+      height: 56px;
+      width: 56px;
+      background-color: red;
+      border-radius: 50%;
+    }
+    .light.blink {
+      animation: blink 800ms infinite;
+    }
+  }
+  .time {
+    .time-text {
+      text-align: center;
+      font-size: 20px;
+      font-weight: 600;
+    }
+  }
 }
 :deep(.el-dialog__body) {
   padding: 0 20px;
@@ -306,7 +470,7 @@
   margin: 0 auto;
   padding: 10px 20px;
   width: calc(100% - 40px);
-  height: 400px;
+  height: 340px;
 }
 .info {
   display: flex;
@@ -325,4 +489,73 @@
 .info-item-two {
   width: 100%;
 }
+
+.delivery-success-tips {
+  padding-top: 140px;
+  color: #fff;
+
+  height: 100%;
+  width: 100%;
+  .success-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .success-tip {
+    margin-top: 50px;
+    text-align: center;
+    font-size: 30px;
+  }
+  .success-sub-tip {
+    margin-top: 10px;
+    font-size: 30px;
+    text-align: center;
+  }
+}
+.delivery-error-tips {
+  padding-top: 140px;
+  color: #fff;
+
+  height: 100%;
+  width: 100%;
+  .error-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .error-tip {
+    margin-top: 50px;
+    text-align: center;
+    font-size: 30px;
+  }
+  .error-sub-tip {
+    margin-top: 10px;
+    font-size: 20px;
+    text-align: center;
+  }
+}
+
+.delivery {
+  height: 100%;
+  width: 100%;
+  padding: 0 90px;
+  .delivery-tip {
+    text-align: center;
+    font-size: 30px;
+    color: red;
+  }
+  .delivery-progress {
+    margin-top: 8px;
+  }
+}
+.craft-params-error {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  .error-tip {
+    font-size: 18px;
+    color: #fff;
+  }
+}
 </style>
diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue
index 7ece016..fb32de0 100644
--- a/src/views/dashboard/index.vue
+++ b/src/views/dashboard/index.vue
@@ -6,7 +6,9 @@
     <template #leftBlock2>
       <ChannelCollapse :channels="channels"></ChannelCollapse>
     </template>
-    <template #middleBlock1>鏍囬</template>
+    <template #middleBlock1>
+      <DashboardTitle></DashboardTitle>
+    </template>
     <template #middleBlock2>
       <el-tabs v-model="activeMainTabName" class="main-info-tabs">
         <el-tab-pane label="鍔犲伐淇℃伅" name="鍔犲伐淇℃伅">
@@ -21,7 +23,7 @@
     <template #middleBlock3>
       <SubTitle>浠诲姟璇︽儏</SubTitle>
       <div class="task-detail">
-        <TaskControl :task="activeTask"></TaskControl>
+        <TaskControl :task="activeTask" @should-reload="reloadAllData"></TaskControl>
       </div>
       <ColorInfo :order="order" :type="1"></ColorInfo>
       <ColorInfo :order="order" :type="2"></ColorInfo>
@@ -47,7 +49,7 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 import ChannelCollapse from '@/views/dashboard/components/ChannelCollapse.vue'
-import type { Worker, Order } from '@/api/task'
+import type { Worker, Order, Task } from '@/api/task'
 import type { PLCResponse } from '@/api/plc'
 import PersonInfo from '@/views/dashboard/components/PersonInfo.vue'
 import ProcessInfo from '@/views/dashboard/components/ProcessInfo.vue'
@@ -61,6 +63,7 @@
 import ProcessingInfo from '@/views/dashboard/components/ProcessingInfo.vue'
 import TaskControl from '@/views/dashboard/components/TaskControl.vue'
 import SubTitle from '@/views/dashboard/components/SubTitle.vue'
+import DashboardTitle from '@/views/dashboard/components/DashboardTitle.vue'
 
 defineOptions({
   name: 'DashboardView'
@@ -73,7 +76,7 @@
   } as unknown as Worker
 })
 const process = computed(() => {
-  return { product: '浜у搧鍚嶇О', number: '111', procedure: '宸ヨ壓鍚嶇О', isUpdate: true }
+  return { product: '浜у搧鍚嶇О', number: '111', procedure: '宸ヨ壓鍚嶇О', isUpdate: true } as any
 })
 
 const order = computed(() => {
@@ -113,6 +116,10 @@
 function changeTab(tab: LabelValue) {
   tasksStore.getChannels(tab.value)
 }
+
+function reloadAllData(task: Task) {
+  tasksStore.reload(task.Channel)
+}
 </script>
 
 <style scoped lang="scss">

--
Gitblit v1.8.0