New file |
| | |
| | | <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> |