haoxuan
2023-11-02 33bdc00cdee41fa0e8d315f4bb2c3fc13ed0df10
src/views/dashboard/components/TaskControlModal.vue
New file
@@ -0,0 +1,561 @@
<template>
  <div class="task-control-modal">
    <BaseModal v-model="modelData" :wider="false">
      <template #title>
        {{ !['下发参数成功', '下发参数失败'].includes(state.value as string) ? '新任务' : '提示' }}
      </template>
      <div class="modal-content">
        <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 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 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>
                    </div>
                  </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>
          </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>
</template>
<script setup lang="ts">
import type { CraftParam, Task } from '@/api/task'
import { useDateFormat, useVModel } from '@vueuse/core'
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
  /** 是否展示模态框 */
  modelValue: boolean
}
const props = withDefaults(defineProps<TaskControlModalProps>(), {
  task: undefined,
  modelValue: false
})
const emit = defineEmits<{
  'update:modelValue': [show: boolean]
  /** 下发成功后触发, 用于外部获得刷新数据的时机 */
  produceStart: []
}>()
const modelData = useVModel(props, 'modelValue', emit)
const { task } = toRefs(props)
/**
 * 格式化时间戳
 * @param timestamp 后端返的10位时间戳
 */
function formatDate(timestamp?: number) {
  if (!timestamp) {
    return '--'
  }
  const time = useDateFormat(timestamp * 1000, 'YYYY-MM-DD', { locales: 'zh-cn' })
  return time.value
}
// 工艺参数
const craftParams = ref<CraftParam[]>()
// 获取工艺参数结果信息
const getCraftParamsTip = ref('')
/**
 * 获取当前展示的任务的工艺参数
 */
function getTaskProduceParams(taskId?: number) {
  if (taskId) {
    craftParams.value = []
    getCraftParamsTip.value = ''
    getCraftParams({ id: taskId }).then(
      (res) => {
        craftParams.value = res.data.Params ?? []
        getCraftParamsTip.value = ''
      },
      (err) => {
        console.error(err)
        craftParams.value = []
        getCraftParamsTip.value = '获取工艺参数失败!'
      }
    )
  }
}
watch(modelData, () => {
  // 弹窗显示时获取工艺参数
  if (modelData.value) {
    getTaskProduceParams(task?.value?.Procedure?.ID)
  } else {
    reset()
  }
})
const countdown30s = useCountDown(30, {
  onEnd: () => {
    send('结束计时')
  }
})
// 弹窗时获取安全生产提示文本
const { channels } = storeToRefs(useTasksStore())
const safeProduce = ref('')
watch(modelData, () => {
  if (modelData.value) {
    safeProduce.value = channels?.value?.[task?.value?.Channel ?? 0]?.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() {
  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: 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;
}
.btn {
  display: flex;
  align-items: center;
  justify-content: space-around;
}
.content-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 40px;
}
.content-title-item {
  width: 50%;
  font-size: 20px;
  color: #4efefa;
}
.leaf-shape {
  position: relative;
  display: inline-block;
  width: 140px;
  height: 46px;
  border-radius: 23px 0 23px 0;
  text-align: center;
  line-height: 46px;
  color: #fff;
  background: url('/leaf-shape.png') no-repeat center center / cover;
}
.scroll-container {
  margin: 0 auto;
  padding: 10px 20px;
  width: calc(100% - 40px);
  height: 340px;
}
.info {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  background-color: #0e246a;
  padding: 10px 20px;
}
.info-item {
  width: 50%;
  height: 35px;
  line-height: 35px;
  font-size: 16px;
  color: #fff;
}
.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>