<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-if="getCraftParamsTip && currentDeviceAllowNoParams" class="info-item info-item-two">
|
未获取到工艺参数, 请手动设置或在云端工艺模型中上传
|
</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 && !currentDeviceAllowNoParams">
|
<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 && !currentDeviceAllowNoParams">
|
<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'
|
import { useDevicesStore } from '@/stores/devices'
|
|
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('')
|
// 当前设备若没有工艺参数是否允许下发
|
const { currentDeviceAllowNoParams } = storeToRefs(useDevicesStore())
|
/**
|
* 获取当前展示的任务的工艺参数
|
*/
|
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: center;
|
& > button {
|
margin-right: 20px;
|
&:last-child {
|
margin-right: 0;
|
}
|
}
|
}
|
.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>
|