| | |
| | | <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"> |
| | |
| | | </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>----- 剩余时间 -----</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> |
| | |
| | | <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 |
| | |
| | | }) |
| | | 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) |
| | | |
| | | /** |
| | | * 格式化时间戳 |
| | |
| | | |
| | | // 工艺参数 |
| | | const craftParams = ref<CraftParam[]>() |
| | | // 获取工艺参数失败信息 |
| | | const getCraftParamsErrMsg = ref('') |
| | | // 获取工艺参数结果信息 |
| | | const getCraftParamsTip = ref('') |
| | | |
| | | /** |
| | | * 获取当前展示的任务的工艺参数 |
| | |
| | | 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 = '获取工艺参数失败!' |
| | | } |
| | | ) |
| | | } |
| | |
| | | // 弹窗显示时获取工艺参数 |
| | | 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; |
| | |
| | | margin: 0 auto; |
| | | padding: 10px 20px; |
| | | width: calc(100% - 40px); |
| | | height: 400px; |
| | | height: 340px; |
| | | } |
| | | .info { |
| | | display: flex; |
| | |
| | | .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> |