haoxuan
2023-10-31 1c22b65361d81ef23bd76d0f76a3dce7ef72fd99
拉代码
1个文件已删除
3个文件已添加
1 文件已重命名
9个文件已修改
646 ■■■■ 已修改文件
src/api/index.ts 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/plc.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/task.ts 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DashboardLayout.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/tasks.ts 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/ChannelCollapse.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/CraftInfo.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/PersonInfo.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/ProcessingInfo.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/TaskInfo.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/components/TaskTabs.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.ts
@@ -1,5 +1,6 @@
import { request } from '@/common/utils'
import type { TasksResponse } from './task'
import type { TasksGroupByChannel } from './task'
import type { PLCResponse } from './plc'
export interface BaseResponse<T = any> {
  code: number
@@ -7,16 +8,38 @@
  msg: string
}
export interface TaskListParams {
  /** 1未完成2今天未完成3已完成 */
  type: 1 | 2 | 3
  /** 通道号 不传查所有通道的 */
  channel?: number
  /** 从第几个开始查,从0开始 */
  offset: number
  /** 查多少条 */
  limit: number
}
/**
 * 获取任务列表
 * @param taskMode 1: 待生产的任务 2: 看板各通道当前展示的任务
 * @param params
 */
export function getTaskList(taskMode: 1 | 2) {
  return request<BaseResponse<TasksResponse>>({
    url: '/v1/task/get',
export function getTaskList(params: TaskListParams) {
  return request<BaseResponse<TasksGroupByChannel>>({
    url: '/v1/task/list',
    method: 'get',
    params: {
      taskMode
    }
    params
  })
}
export interface ProductProgressParams {
  channel: number
  procedureId: number
}
export function getProductProgress(params: ProductProgressParams) {
  return request<BaseResponse<PLCResponse>>({
    url: '/v1/plc/productProgress',
    method: 'post',
    data: params
  })
}
src/api/plc.ts
New file
@@ -0,0 +1,6 @@
export interface PLCResponse {
  finishNumber: number
  totalNumber: number
  /** 1断开 2生产中 3待机*/
  plcStatus: 1 | 2 | 3
}
src/api/task.ts
@@ -48,7 +48,7 @@
  workHours: string
  inputMaterials: string
  outputMaterials: string
  workers: Workers[]
  workers: Worker[]
  allProcedureNames: string[]
  channel: number
}
@@ -62,7 +62,7 @@
  CanStarted: boolean
}
export interface Workers {
export interface Worker {
  workerId: string
  workerName: string
  phoneNum: string
@@ -78,7 +78,11 @@
export interface TasksResponse {
  Tasks: Task[]
  TaskCount: number
  workers: Workers[]
  workers: Worker[]
  Prompt: Prompt
  ChannelAmount: number
}
export interface TasksGroupByChannel {
  [channel: number]: TasksResponse
}
src/components.d.ts
@@ -7,13 +7,8 @@
declare module 'vue' {
  export interface GlobalComponents {
    DashboardLayout: typeof import('./components/DashboardLayout.vue')['default']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    DashboardLayout: (typeof import('./components/DashboardLayout.vue'))['default']
    RouterLink: (typeof import('vue-router'))['RouterLink']
    RouterView: (typeof import('vue-router'))['RouterView']
  }
}
src/components/DashboardLayout.vue
@@ -5,8 +5,8 @@
        <slot name="leftBlock1"></slot>
      </div>
      <div class="double-height-block padding-4">
        <div class="card">
          <el-scrollbar always>
        <div class="card scroll-card">
          <el-scrollbar always class="scroller">
            <slot name="leftBlock2"></slot>
          </el-scrollbar>
        </div>
@@ -16,18 +16,18 @@
      <div class="header-block padding-4">
        <slot name="middleBlock1"></slot>
      </div>
      <div class="base-block padding-4">
      <div class="top-block padding-4">
        <div class="card">
          <slot name="middleBlock2"></slot>
        </div>
      </div>
      <div class="block-container">
        <div class="base-block padding-4">
      <div class="bottom-block">
        <div class="bottom-block-item padding-4">
          <div class="card">
            <slot name="middleBlock3"></slot>
          </div>
        </div>
        <div class="base-block padding-4">
        <div class="bottom-block-item padding-4">
          <div class="card">
            <slot name="middleBlock4"></slot>
          </div>
@@ -67,6 +67,10 @@
$baseBlockHeight: calc((100vh - 2 * $layoutPadding - $headerBlockHeight) / 2);
// 双倍高布局块高度
$doubleBlockHeight: calc($baseBlockHeight * 2);
// 上边固定高
$topBlocHeight: 400px;
// 下边高度
$bottomBlockHeight: calc(100vh - 2 * $layoutPadding - $headerBlockHeight - $topBlocHeight);
.dashboard-layout {
  display: flex;
  align-items: center;
@@ -100,14 +104,24 @@
.padding-4 {
  padding: 6px;
}
.block-container {
.base-block {
  height: $baseBlockHeight;
  flex: 1;
}
.top-block {
  height: $topBlocHeight;
  flex: 0;
}
.bottom-block {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: $bottomBlockHeight;
}
.base-block {
  height: $baseBlockHeight;
  flex-grow: 1;
.bottom-block-item {
  height: $bottomBlockHeight;
  flex: 1;
}
.header-block {
  height: $headerBlockHeight;
@@ -123,4 +137,10 @@
  border-radius: 6px;
  padding: 10px 16px;
}
.scroll-card {
  padding: 0;
}
.scroller {
  padding: 10px 16px;
}
</style>
src/stores/tasks.ts
New file
@@ -0,0 +1,106 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { Task, TasksGroupByChannel, TasksResponse } from '@/api/task'
import type { TaskListParams } from '@/api'
import { getTaskList } from '@/api'
export interface ChannelMoreBtnStatus {
  /** true 任务未加载完  false 所有任务已经加载完成*/
  [channel: number]: boolean
}
export const useTasksStore = defineStore('tasks', () => {
  const channels = ref<TasksGroupByChannel>({})
  const currentType = ref<1 | 2 | 3>(1)
  /**
   * 获取任务数据
   * @param type 1未完成2今天未完成3已完成
   */
  function getChannels(type: 1 | 2 | 3) {
    currentType.value = type
    const params: TaskListParams = {
      type,
      offset: 0,
      limit: 3
    }
    getTaskList(params)
      .then((res) => {
        channels.value = res.data
      })
      .catch((err) => {
        console.error(err)
        channels.value = []
      })
  }
  function moreChannelTasksBtn(channelNumber: number) {
    const taskLength = channels.value[channelNumber].Tasks?.length ?? 0
    const params: TaskListParams = {
      type: currentType.value,
      channel: channelNumber,
      offset: taskLength,
      limit: 10
    }
    getTaskList(params)
      .then((res) => {
        const existTasks = channels.value![channelNumber].Tasks ?? []
        channels.value[channelNumber] = res.data[channelNumber] ?? {}
        channels.value[channelNumber].Tasks = channels.value[channelNumber].Tasks ?? []
        channels.value[channelNumber].Tasks = [...existTasks, ...channels.value[channelNumber].Tasks]
      })
      .catch((err) => {
        console.error(err)
      })
  }
  function foldChannelTasksBtn(channelNumber: number) {
    const tasks = channels.value[channelNumber].Tasks ?? []
    channels.value[channelNumber].Tasks = tasks.slice(0, 3)
  }
  const moreBtnStatus = computed(() => {
    return Object.entries(channels.value).reduce((pre, currentValue) => {
      const channelNumber = +currentValue[0]
      const channelData = currentValue[1] as TasksResponse
      pre[channelNumber] = channelData.TaskCount > (channelData.Tasks?.length ?? 0)
      return pre
    }, {} as ChannelMoreBtnStatus)
  })
  /** 当前高亮的任务 */
  const activeTask = ref<Task>()
  function setActiveTask(task: Task) {
    activeTask.value = task
  }
  const requestParamsMap = ref<{
    [channel: number]: TaskListParams
  }>({})
  function getParamsByChannel(channel: number) {
    return (
      requestParamsMap.value[channel] ?? {
        type: 1,
        offset: 0,
        limit: 3
      }
    )
  }
  function setParamsByChannel(channel: number, params: TaskListParams) {
    requestParamsMap.value[channel] = params
  }
  return {
    channels,
    getChannels,
    moreBtnStatus,
    activeTask,
    setActiveTask,
    requestParamsMap,
    getParamsByChannel,
    setParamsByChannel,
    moreChannelTasksBtn,
    foldChannelTasksBtn
  }
})
src/views/dashboard/components/ChannelCollapse.vue
@@ -2,35 +2,61 @@
  <div class="channel-collapse">
    <el-collapse v-model="activeChannel">
      <el-collapse-item
        v-for="(tasks, channelNumber) in channels"
        v-for="(channel, channelNumber) in channels"
        :key="channelNumber"
        :title="CHANNEL_NAME_MAP[channelNumber] + ' 通道'"
        :name="String(channelNumber)"
      >
        <TaskInfo v-for="task in tasks" :key="task.Procedure.ID" :task="task"></TaskInfo>
        <TaskInfo
          v-for="task in channel.Tasks"
          :key="task.Procedure.ID"
          :active="task.Procedure.ID === tasksStore.activeTask?.Procedure.ID"
          :task="task"
          style="margin-bottom: 16px"
          @click="tasksStore.setActiveTask(task)"
        ></TaskInfo>
        <div
          v-show="channel.Tasks?.length && tasksStore.moreBtnStatus?.[channelNumber]"
          class="btn more"
          @click="tasksStore.moreChannelTasksBtn(channelNumber)"
        >
          查看更多
          <el-icon style="margin-left: 6px"><ArrowDownBold /></el-icon>
        </div>
        <div
          v-show="channel.Tasks?.length && !tasksStore.moreBtnStatus?.[channelNumber]"
          class="btn fold"
          @click="tasksStore.foldChannelTasksBtn(channelNumber)"
        >
          收起
          <el-icon style="margin-left: 6px"><ArrowUpBold /></el-icon>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { Task } from '@/api/task'
import type { TasksGroupByChannel } from '@/api/task'
import TaskInfo from './TaskInfo.vue'
import { CHANNEL_NAME_MAP } from '@/common/constants'
export interface Channel {
  [channelNumber: number]: Task[]
}
import { useTasksStore } from '@/stores/tasks'
import { ArrowDownBold, ArrowUpBold } from '@element-plus/icons-vue'
export interface ChannelCollapseProps {
  channels: Channel
  channels: TasksGroupByChannel
}
const props = defineProps<ChannelCollapseProps>()
const activeChannel = ref<string[]>([])
const tasksStore = useTasksStore()
watchEffect(() => {
  const channelNumbers = Object.keys(props.channels).sort((a, b) => +a - +b)
  // 通道数据变化后
  const channelNumbers = Object.keys(props?.channels ?? {}).sort((a, b) => +a - +b)
  activeChannel.value = [...channelNumbers]
})
</script>
@@ -66,4 +92,18 @@
  font-size: 16px;
  font-weight: 600;
}
.btn {
  width: 70%;
  height: 50px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  background: linear-gradient(to right, rgb(29, 96, 212) 0%, rgb(47, 122, 251), rgb(29, 96, 212) 100%);
}
</style>
src/views/dashboard/components/CraftInfo.vue
File was renamed from src/views/dashboard/components/ProcessInfo.vue
@@ -1,5 +1,5 @@
<template>
  <div class="process-info">
  <div class="craft-info">
    <div class="item-l-bng">
      <img src="~@/assets/images/process-model.png" />
    </div>
@@ -9,7 +9,7 @@
      <div class="item-r-b">{{ process.name }}</div>
    </div>
    <div class="tip-r">
      <img src="~@/assets/images/process-tip.png" />
      <img src="~@/assets/images/process-tip.png" alt="" />
    </div>
    <div class="tip-current">当前使用</div>
    <div class="btn">
@@ -18,7 +18,9 @@
  </div>
</template>
<script setup lang="ts">
import { computed, toRefs } from 'vue'
// 工艺信息
import { toRefs } from 'vue'
export interface ProcessInfoProps {
  process: process
}
@@ -33,7 +35,7 @@
.font_weight {
  font-weight: 600;
}
.process-info {
.craft-info {
  width: calc(50% - 35px);
  height: 110px;
  padding: 23px 10px 10px;
src/views/dashboard/components/PersonInfo.vue
@@ -29,10 +29,12 @@
  </div>
</template>
<script setup lang="ts">
import type { UserFilled } from '@element-plus/icons-vue'
import { computed, toRefs } from 'vue'
import { StarFilled, UserFilled } from '@element-plus/icons-vue'
import { toRefs } from 'vue'
import type { Worker } from '@/api/task'
export interface PersonInfoProps {
  person: person
  person: Worker
}
const props = defineProps<PersonInfoProps>()
src/views/dashboard/components/ProcessingInfo.vue
New file
@@ -0,0 +1,220 @@
<template>
  <div class="processing-info">
    <div class="step">
      <el-steps
        v-if="task?.AllProcedures"
        :active="task.CurrentProcedureIndex ?? 0"
        finish-status="success"
        class="steps"
      >
        <el-step v-for="(item, index) in task.AllProcedures" :key="index" icon="" :title="item"></el-step>
      </el-steps>
    </div>
    <div class="details">
      <div class="row">
        <div class="col">工单编号: {{ task?.Order?.workOrderId || '' }}</div>
        <div class="col">订单编号: {{ task?.Order?.orderId || '' }}</div>
      </div>
      <div class="row">
        <div class="col">产品名称: {{ task?.Order?.productName || '--' }}</div>
        <div class="col">数量: {{ task?.Order?.amount || 0 }}{{ task?.Order?.unit }}</div>
      </div>
      <div class="row">
        <div class="col">交货日期: {{ task?.Order?.deliverDate || '--' }}</div>
        <div class="col">工时: {{ task?.Procedure?.procedure?.workHours || '--' }}</div>
      </div>
      <div class="row">
        <div class="col">
          起止时间: {{ formatDate(task?.Procedure?.startTime) }}
          ~
          {{ formatDate(task?.Procedure?.endTime) }}
        </div>
        <div class="col">通道: {{ isNumber(task?.Channel) ? CHANNEL_NAME_MAP[task?.Channel] : '--' }}</div>
      </div>
      <div class="row">
        <div class="col">客户名称: {{ task?.Order?.customer || '' }}</div>
        <div class="col">参数要求: {{ task?.Order?.parameter || '' }}</div>
      </div>
    </div>
    <div class="process">
      <div>完成进度:</div>
      <div class="process-bar">
        <el-progress
          define-back-color="#132f6e"
          color="#00cc66"
          text-color="#fff"
          :text-inside="true"
          :stroke-width="30"
          :percentage="processingPercent"
        ></el-progress>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
// 加工信息组件
import type { Task } from '@/api/task'
import { computed, onUnmounted, toRefs, watch } from 'vue'
import { useDateFormat } from '@vueuse/core'
import { useRequest } from 'vue-hooks-plus'
import { getProductProgress } from '@/api'
import type { ProductProgressParams } from '@/api'
import { isNumber } from 'lodash-es'
import { CHANNEL_NAME_MAP } from '@/common/constants'
const props = defineProps<{
  task?: Task
}>()
const { task } = toRefs(props)
export interface Statistics {
  totalNumber: number
  finishNumber: number
}
/**
 * 计算生产进度
 * @param statistics
 * @return 进度,0~100
 */
function calculateProgress(statistics: Statistics): number {
  if (!statistics) {
    return 0
  }
  if (statistics.finishNumber === 0) {
    return 0
  }
  if (statistics.finishNumber === statistics.totalNumber) {
    return 100
  }
  const result = Math.floor((statistics.finishNumber / statistics.totalNumber) * 100)
  return result > 100 ? 100 : result
}
/**
 * 计算完成进度, 任务状态未生产固定为 0% 已完成固定为 100% 生产中则从plc获取
 */
const processingPercent = computed(() => {
  if (task?.value?.Procedure?.Status === 1) {
    return 0
  }
  if (task?.value?.Procedure?.Status === 3) {
    return 100
  }
  if (task?.value?.Procedure?.Status === 2) {
    return calculateProgress(plcResponse?.value?.data as Statistics)
  }
  return 0
})
/**
 * 如果任务状态是进行中, 则轮询 plc 取进度
 */
const {
  data: plcResponse,
  run: startPLCPolling,
  cancel: cancelPLCPolling
} = useRequest(
  () =>
    getProductProgress({
      channel: task?.value?.Channel,
      procedureId: task?.value?.Procedure.ID
    } as ProductProgressParams),
  {
    manual: true,
    pollingInterval: 6000,
    pollingWhenHidden: false
  }
)
/**
 * 任务状态是生产中则轮询plc取目标数和完成数计算完成进度
 */
watch(
  () => task?.value,
  () => {
    cancelPLCPolling()
    if (task?.value?.Procedure?.Status === 2) {
      startPLCPolling()
    }
  }
)
onUnmounted(() => {
  cancelPLCPolling()
})
/**
 * 格式化时间戳
 * @param timestamp 后端返的10位时间戳
 */
function formatDate(timestamp?: number) {
  if (!timestamp) {
    return '--'
  }
  const time = useDateFormat(timestamp * 1000, 'YYYY-MM-DD', { locales: 'zh-cn' })
  return time.value
}
</script>
<style scoped lang="scss">
$text-color: #d7d7ca;
.step {
  width: 100%;
  height: 66px;
  overflow-x: auto;
  margin-top: -5px;
  padding: 0 20px;
  .steps {
    height: 100%;
    .el-step__icon {
      width: 16px;
      height: 16px;
    }
    .el-step__title {
      line-height: 25px;
      font-size: 14px;
    }
    .el-step__title.is-process {
      color: #a8abb2;
    }
  }
}
.details {
  font-size: 18px;
  padding: 10px 20px;
  color: $text-color;
  .row {
    width: 100%;
    padding: 2px 0;
    display: flex;
    align-items: center;
  }
  .col {
    width: 50%;
    flex: 1;
  }
}
.process {
  font-size: 18px;
  padding: 10px 20px;
  color: $text-color;
  display: flex;
}
.process-bar {
  flex: 1;
  margin-left: 20px;
}
:deep(.el-progress-bar__outer) {
  border-radius: 8px;
}
:deep(.el-progress-bar__inner) {
  border-radius: 8px;
}
</style>
src/views/dashboard/components/TaskInfo.vue
@@ -1,5 +1,5 @@
<template>
  <div class="task-info" :class="{ selected }">
  <div class="task-info" :class="{ active }">
    <div
      class="task-info-title"
      :class="{
@@ -31,13 +31,13 @@
export interface TaskInfoProps {
  task: Task
  selected?: boolean
  active?: boolean
}
const props = withDefaults(defineProps<TaskInfoProps>(), {
  selected: false
  active: false
})
const { task, selected } = toRefs(props)
const { task, active } = toRefs(props)
const planTimeText = computed(() => {
  const format = (date: number) => {
@@ -56,9 +56,12 @@
$status-ready: #13235a;
$status-done: #13235a;
$text-color: #d7d7d7;
$active-color: #00dfdf;
.task-info {
  background-color: #6b83ff;
  border-radius: 4px;
  overflow: initial;
  cursor: pointer;
}
.task-info-title {
  height: 34px;
@@ -94,4 +97,17 @@
    flex: 1;
  }
}
.active {
  position: relative;
  &:before {
    content: '';
    width: 8px;
    background-color: $active-color;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    border-radius: 6px 0 0 6px;
  }
}
</style>
src/views/dashboard/components/TaskTabs.vue
@@ -1,32 +1,39 @@
<template>
  <div class="task-tabs">
    <div
      v-for="tabName in list"
      :key="tabName"
      v-for="tab in list"
      :key="tab.value"
      class="task-tab-item triangle-tip"
      :class="{ active: props.modelValue === tabName }"
      @click="selectTab(tabName)"
      :class="{ active: props.modelValue === tab.value }"
      @click="selectTab(tab)"
    >
      {{ tabName }}
      {{ tab.label }}
    </div>
  </div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
export interface LabelValue {
  label: string
  value: any
}
const props = defineProps<{
  /** tab 列表*/
  list: string[]
  list: LabelValue[]
  /** 当前选中的 tab*/
  modelValue?: string
  modelValue?: any
}>()
const emit = defineEmits<{
  'update:modelValue': [tabName: string]
  change: [tab: LabelValue]
}>()
const data = useVModel(props, 'modelValue', emit)
function selectTab(tabName: string) {
  data.value = tabName
function selectTab(tab: LabelValue) {
  data.value = tab.value
  emit('change', tab)
}
</script>
<style scoped lang="scss">
src/views/dashboard/index.vue
File was deleted
vite.config.ts
@@ -14,7 +14,7 @@
  server: {
    proxy: {
      '/v1/': {
        target: 'http://192.168.20.4:8003',
        target: 'http://192.168.20.119:8003',
        ws: true,
        changeOrigin: true
      }