songshankun
2023-11-01 403ce87b79b7f402b8000b95c5b0b9d77bc393d0
feat: 任务控制弹窗添加状态转换逻辑
10个文件已修改
690 ■■■■ 已修改文件
package-lock.json 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/common/composable/useCountDown.ts 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/tasks.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/TaskControl.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/TaskControlModal.vue 477 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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",
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>
src/api/index.ts
@@ -66,6 +66,11 @@
export interface SendProcessParamsParams {
  procedureId: number
}
/**
 * 下发工艺参数
 * @param params
 */
export function sendProcessParams(params: SendProcessParamsParams) {
  return request<BaseResponse>({
    url: `v1/task/sendProcessParams`,
@@ -73,3 +78,19 @@
    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
  })
}
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
  }
}
src/components.d.ts
@@ -14,6 +14,7 @@
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElProgress: typeof import('element-plus/es')['ElProgress']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
src/stores/tasks.ts
@@ -25,13 +25,43 @@
      offset: 0,
      limit: 3
    }
    getTaskList(params)
    return getTaskList(params)
      .then((res) => {
        channels.value = res.data
      })
      .catch((err) => {
        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)
      })
  }
@@ -96,6 +126,7 @@
    getChannels,
    moreBtnStatus,
    activeTask,
    reload,
    setActiveTask,
    requestParamsMap,
    getParamsByChannel,
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>
        <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>
src/views/dashboard/components/TaskControlModal.vue
@@ -1,33 +1,22 @@
<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">
        <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 box">
              <div class="leaf-shape">
              {{ task?.Order?.amount || 0 }}
            </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 v-else class="content-scroll">
          <div class="content-scroll">
          <div class="scroll-container">
            <el-scrollbar always class="scroller">
              <template v-if="task">
@@ -56,86 +45,104 @@
                    </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>
          </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>
        <template v-if="getCraftParamsTip">
        <div class="btn">
          <BigButton bg-color="#4765c0" @click="closeModal">暂缓生产</BigButton>
            <BigButton bg-color="#4765c0" @click="closeModal"> 关闭 </BigButton>
          </div>
        </template>
        <template v-else>
          <div class="btn">
          <BigButton
            v-if="countdown30s.countdownStatus.value !== 'complete'"
              v-if="!['下发参数成功', '下发参数失败'].includes(state.value as string)"
              bg-color="#4765c0"
              @click="respiteProduce"
            >
              暂缓生产
            </BigButton>
            <BigButton
              v-if="['初始化', '计时中'].includes(state.value as string)"
            color="#0d0d0d"
            :disabled="countdown30s.countdownStatus.value === 'running'"
            @click="startCountdown30s"
              :disabled="state.value === '计时中'"
              @click="prepareProduce"
          >
            生产准备
          </BigButton>
          <BigButton v-if="countdown30s.countdownStatus.value === 'complete'" bg-color="#4efefa" @click="startProduce">
            开始生产
            <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 查看状态转换图
 */
const toggleMachine = createMachine({
  id: 'produce',
  initial: '初始化',
  predictableActionArguments: true,
  states: {
    初始化: {
      on: {
        开始计时: { target: '计时中' },
        结束: { target: '初始化' }
      }
    },
    计时中: {
      on: {
        结束计时: { target: '准备生产' },
        暂缓生产: { target: '初始化' }
      }
    },
    准备生产: {
      on: {
        开始生产: { target: '下发参数中' },
        暂缓生产: { target: '初始化' }
      }
    },
    下发参数中: {
      on: {
        成功: { target: '下发参数成功' },
        失败: { target: '下发参数失败' },
        暂缓生产: { target: '初始化' }
      }
    },
    下发参数成功: {
      on: {
        结束: {
          target: '初始化'
        }
      }
    },
    下发参数失败: {
      on: {
        再次下发: { target: '准备生产' }
      }
    }
  }
})
const { state, send } = useMachine(toggleMachine)
/**
 * 暂缓生产, 直接关闭弹窗
 */
function respiteProduce() {
  modelData.value = false
  send('暂缓生产')
  reset()
}
/**
 * 生产准备
 */
function prepareProduce() {
  send('开始计时')
  countdown30s.startCountdown()
}
// 参数下发成功或失败结果
const deliveryTip = ref('')
// 参数下发成功后延时3秒后关闭弹窗
const countdown3s = useCountDown(3, {
  onEnd: () => {
    closeModal()
  }
})
/**
 * 开始生产 , 下发工艺参数
 */
function startProduce() {
  if (task.value?.Procedure?.ID) {
    message.value = '工艺参数下发中...'
  send('开始生产')
    isLoading.value = true
    sendProcessParams({
      procedureId: task.value.Procedure.ID
    })
      .then(
        (res) => {
          console.log(res)
          messageError.value = '下发成功'
        deliveryTip.value = '下发成功'
        send('成功')
        countdown3s.startCountdown()
        },
        (err) => {
          console.error(err)
          messageError.value = err.msg ? err.msg : '抱歉,工序下发失败!'
        deliveryTip.value = err.msg ? err.msg : '抱歉,工序下发失败!'
        send('失败')
        }
      )
      .finally(() => {
        isLoading.value = false
      })
  }
    .finally(() => {})
}
const { channels } = storeToRefs(useTasksStore())
const safeProduce = computed(() => {
  if (task?.value?.Channel) {
    return channels?.value?.[task.value.Channel]?.Prompt?.safeProduce
/**
 * 再次下发
 */
function deliverParams() {
  send('再次下发')
  }
  return ''
})
const message = ref(safeProduce.value)
const isLoading = ref(false)
/**
 * 关闭弹窗
 */
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>
src/views/dashboard/index.vue
@@ -21,7 +21,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 +47,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'
@@ -73,7 +73,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 +113,10 @@
function changeTab(tab: LabelValue) {
  tasksStore.getChannels(tab.value)
}
function reloadAllData(task: Task) {
  tasksStore.reload(task.Channel)
}
</script>
<style scoped lang="scss">