haoxuan
2023-11-02 33bdc00cdee41fa0e8d315f4bb2c3fc13ed0df10
Merge branch 'dev' of http://192.168.5.5:10010/r/web/bulletin-board-style1 into wn
2个文件已添加
770 ■■■■■ 已修改文件
src/views/dashboard/components/TaskControlModal.vue 561 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/index.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
src/views/dashboard/index.vue
New file
@@ -0,0 +1,209 @@
<template>
  <DashboardLayout>
    <template #leftBlock1>
      <TaskTabs v-model="activeTaskTab" style="margin-top: 20px" :list="taskTabsList" @change="changeTab"></TaskTabs>
    </template>
    <template #leftBlock2>
      <ChannelCollapse :channels="channels"></ChannelCollapse>
    </template>
    <template #middleBlock1>
      <DashboardTitle></DashboardTitle>
    </template>
    <template #middleBlock2>
      <el-tabs v-model="activeMainTabName" class="main-info-tabs">
        <el-tab-pane label="加工信息" name="加工信息">
          <ProcessingInfo style="margin-top: 6px" :task="activeTask"></ProcessingInfo>
        </el-tab-pane>
        <el-tab-pane label="工艺信息" name="工艺信息">
          <ProcessInfo
            v-for="item in craftModelStore.craftModelList"
            :key="item.ID"
            :craft-model="item"
            @detail="openCraftModelDetailModal"
          ></ProcessInfo>
        </el-tab-pane>
        <el-tab-pane label="物料清单" name="物料清单">
          <InputMaterialsList></InputMaterialsList>
          <OutputMaterialsList></OutputMaterialsList>
        </el-tab-pane>
      </el-tabs>
    </template>
    <template #middleBlock3>
      <SubTitle>任务详情</SubTitle>
      <div class="task-detail">
        <TaskControl :task="activeTask" @should-reload="reloadAllData"></TaskControl>
      </div>
      <ColorInfo :order="order" :type="1"></ColorInfo>
      <ColorInfo :order="order" :type="2"></ColorInfo>
    </template>
    <template #middleBlock4>
      <SubTitle>人员信息</SubTitle>
      <PersonInfo v-for="worker in workers" :key="worker.workerId" :person="worker"></PersonInfo>
    </template>
    <template #rightBlock1>
      <div class="date-time">
        <CurrentDateTime></CurrentDateTime>
      </div>
    </template>
    <template #rightBlock2>
      <DeviceStatusInfo :plc="plcStore.plcInfo" :type="1"></DeviceStatusInfo>
      <DeviceStatusInfo :device="deviceStore.deviceInfo" :type="2"></DeviceStatusInfo>
      <DeviceNumberInfo></DeviceNumberInfo>
    </template>
    <template #rightBlock3>
      <SubTitle>知识库</SubTitle>
      <KnowledgeInfo></KnowledgeInfo>
      <BigButton class="btn" bg-color="red">红灯呼叫</BigButton>
    </template>
  </DashboardLayout>
  <CraftDetailModal v-model="showCraftModelDetail" @close="showCraftModelDetail = false"></CraftDetailModal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import ChannelCollapse from '@/views/dashboard/components/ChannelCollapse.vue'
import type { Worker, Order, Task } from '@/api/task'
import PersonInfo from '@/views/dashboard/components/PersonInfo.vue'
import ProcessInfo from '@/views/dashboard/components/ProcessInfo.vue'
import ColorInfo from '@/views/dashboard/components/ColorInfo.vue'
import DeviceStatusInfo from '@/views/dashboard/components/DeviceStatusInfo.vue'
import DeviceNumberInfo from '@/views/dashboard/components/DeviceNumberInfo.vue'
import KnowledgeInfo from '@/views/dashboard/components/KnowledgeInfo.vue'
import InputMaterialsList from '@/views/dashboard/components/InputMaterialsList.vue'
import OutputMaterialsList from '@/views/dashboard/components/OutputMaterialsList.vue'
import BigButton from '@/views/dashboard/components/BigButton.vue'
import type { LabelValue } from '@/views/dashboard/components/TaskTabs.vue'
import TaskTabs from '@/views/dashboard/components/TaskTabs.vue'
import CurrentDateTime from '@/views/dashboard/components/CurrentDateTime.vue'
import { useTasksStore } from '@/stores/tasks'
import { storeToRefs } from 'pinia'
import ProcessingInfo from '@/views/dashboard/components/ProcessingInfo.vue'
import TaskControl from '@/views/dashboard/components/TaskControl.vue'
import SubTitle from '@/views/dashboard/components/SubTitle.vue'
import DashboardTitle from '@/views/dashboard/components/DashboardTitle.vue'
import { usePLCStore } from '@/stores/plc'
import { useDevicesStore } from '@/stores/devices'
import { useCraftModelStore } from '@/stores/craftModel'
import CraftDetailModal from '@/views/dashboard/components/CraftDetailModal.vue'
defineOptions({
  name: 'DashboardView'
})
// 获取当前高亮任务的值班人信息
const taskStore = useTasksStore()
const workers = computed(() => {
  return taskStore.activeTask?.Procedure?.procedure?.workers ?? []
})
const process = computed(() => {
  return { product: '产品名称', number: '111', procedure: '工艺名称', isUpdate: true } as any
})
const order = computed(() => {
  return {
    finishNumber: 0,
    unit: '个',
    amount: '10'
  } as unknown as Order
})
const taskTabsList = [
  {
    label: '未完成',
    value: 1
  },
  {
    label: '今日任务',
    value: 2
  },
  {
    label: '已完成',
    value: 3
  }
]
const activeTaskTab = ref(1)
const activeMainTabName = ref<string>('加工信息')
const tasksStore = useTasksStore()
const { activeTask, channels } = storeToRefs(tasksStore)
tasksStore.getChannels(1)
function changeTab(tab: LabelValue) {
  tasksStore.getChannels(tab.value)
}
/**
 * 完成任务或者下发参数成功后要刷新通道数据
 * @param task
 */
function reloadAllData(task: Task) {
  tasksStore.reload(task.Channel)
}
// 启动plc 轮询
const plcStore = usePLCStore()
plcStore.startPollingPLC()
// 启动 设备 轮询
const deviceStore = useDevicesStore()
deviceStore.startPollingDevice()
// 切换任务时获取对应任务的工艺模型信息
const craftModelStore = useCraftModelStore()
watch(activeTask, () => {
  craftModelStore.getCraftModelList()
})
const showCraftModelDetail = ref(false)
function openCraftModelDetailModal() {
  console.log(1)
  showCraftModelDetail.value = true
}
</script>
<style scoped lang="scss">
$active-tab-color: #00dfdf;
.date-time {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 12px;
}
:deep(.el-tabs) {
  height: 100%;
}
:deep(.el-tab-pane) {
  height: 100%;
}
:deep(.el-tabs__content) {
  height: calc(100% - 56px);
}
:deep(.el-tabs__item) {
  color: #fff;
  font-size: 20px;
}
:deep(.el-tabs__nav-scroll) {
  height: 46px;
}
:deep(.el-tabs__nav) {
  height: 46px;
}
:deep(.el-step__title.is-process) {
  color: #fff;
}
:deep(.el-tabs__item.is-active) {
  color: $active-tab-color;
  font-weight: 600;
}
:deep(.el-tabs__active-bar) {
  background-color: $active-tab-color;
  height: 4px;
}
:deep(.el-tabs__nav-wrap::after) {
  height: 1px;
}
.btn {
  width: 100%;
  font-size: 20px;
}
</style>