| | |
| | | <meta charset="UTF-8" /> |
| | | <link rel="icon" href="/favicon.ico" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>看板</title> |
| | | <title>智能工作台</title> |
| | | </head> |
| | | <body> |
| | | <div id="app"></div> |
| | |
| | | msg: string |
| | | } |
| | | |
| | | export interface ListResponse<T = any> { |
| | | code: number |
| | | data: T |
| | | msg: string |
| | | total: number |
| | | } |
| | | |
| | | export interface TaskListParams { |
| | | /** 1未完成2今天未完成3已完成 */ |
| | | type: 1 | 2 | 3 |
| | |
| | | }) |
| | | } |
| | | |
| | | export interface SetCurrentDeviceParams { |
| | | currentDeviceID: string |
| | | } |
| | | |
| | | /** |
| | | * 获取当前面板绑定的设备列表 |
| | | */ |
| | | export function apiSetCurrentDevice(data: SetCurrentDeviceParams) { |
| | | return request<BaseResponse<Devices>>({ |
| | | url: `/v1/device/setCurrentDeviceId`, |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | export interface CraftModelListParams { |
| | | procedureId: number |
| | | page: number |
| | |
| | | * @param params |
| | | */ |
| | | export function apiGetCraftModelList(params: CraftModelListParams) { |
| | | return request<BaseResponse<CraftModel[]>>({ |
| | | return request<ListResponse<CraftModel[]>>({ |
| | | url: '/v1/processModel/list', |
| | | method: 'get', |
| | | params |
| | |
| | | ElSteps: typeof import('element-plus/es')['ElSteps'] |
| | | ElTabPane: typeof import('element-plus/es')['ElTabPane'] |
| | | ElTabs: typeof import('element-plus/es')['ElTabs'] |
| | | ElText: typeof import('element-plus/es')['ElText'] |
| | | RouterLink: typeof import('vue-router')['RouterLink'] |
| | | RouterView: typeof import('vue-router')['RouterView'] |
| | | } |
| | |
| | | /** 是否展示模态框 */ |
| | | modelValue: boolean |
| | | /** 更宽版本 */ |
| | | wider: boolean |
| | | wider?: boolean |
| | | } |
| | | const props = withDefaults(defineProps<BaseModalProps>(), { |
| | | modelValue: false, |
| | |
| | | import { ref, watch } from 'vue' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | import type { CraftModel } from '@/api/craftModel' |
| | | import { apiGetCraftModelList } from '@/api' |
| | | import { useTasksStore } from '@/stores/tasks' |
| | | |
| | | const watcher = ref() |
| | | |
| | | const page = ref(1) |
| | | const total = ref(0) |
| | | export const useCraftModelStore = defineStore('craftModel', () => { |
| | | const craftModelList = ref<CraftModel[]>() |
| | | const craftModelList = ref<CraftModel[]>([]) |
| | | const taskStore = useTasksStore() |
| | | function getCraftModelList() { |
| | | if (taskStore.activeTask?.Procedure.ID) { |
| | | craftModelList.value = [] |
| | | apiGetCraftModelList({ |
| | | procedureId: taskStore.activeTask?.Procedure.ID, |
| | | page: 1, |
| | | pageSize: 999 |
| | | page: page.value, |
| | | pageSize: 6 |
| | | }).then((res) => { |
| | | craftModelList.value = res.data ?? [] |
| | | total.value = res.total |
| | | }) |
| | | } |
| | | } |
| | |
| | | watch( |
| | | () => taskStore.activeTask, |
| | | () => { |
| | | page.value = 1 |
| | | getCraftModelList() |
| | | } |
| | | ) |
| | | } |
| | | |
| | | return { craftModelList, getCraftModelList } |
| | | const loading = ref(false) |
| | | |
| | | function loadMore() { |
| | | if (taskStore.activeTask?.Procedure.ID && !loading.value && hasMore.value) { |
| | | page.value++ |
| | | loading.value = true |
| | | apiGetCraftModelList({ |
| | | procedureId: taskStore.activeTask?.Procedure.ID, |
| | | page: page.value, |
| | | pageSize: 6 |
| | | }) |
| | | .then((res) => { |
| | | craftModelList.value = [...craftModelList.value, ...(res.data ?? [])] |
| | | total.value = res.total |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const hasMore = computed(() => { |
| | | return total.value > craftModelList.value.length |
| | | }) |
| | | |
| | | return { craftModelList, getCraftModelList, hasMore, loading, loadMore } |
| | | }) |
| | |
| | | import { computed, onUnmounted } from 'vue' |
| | | import { computed } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | import { getDeviceList } from '@/api' |
| | | import { useRequest } from 'vue-hooks-plus' |
| | |
| | | |
| | | export const useDevicesStore = defineStore('device', () => { |
| | | const deviceInfo = computed(() => { |
| | | if (deviceInfo?.value) { |
| | | deviceRes.value.data.currentDeviceID = 'wwwwwwww222222' |
| | | } |
| | | return deviceRes?.value?.data as Devices |
| | | }) |
| | | |
| | |
| | | cancelDevicePolling() |
| | | startDevicePolling() |
| | | } |
| | | |
| | | onUnmounted(() => { |
| | | cancelDevicePolling() |
| | | }) |
| | | |
| | | return { deviceInfo, startPollingDevice } |
| | | }) |
| | |
| | | /** |
| | | * 刷新所有数据 |
| | | */ |
| | | function reload(channel: number) { |
| | | function reloadChannel(channel: number) { |
| | | getChannels(currentType.value).then(() => { |
| | | autoSelectTask(channel) |
| | | }) |
| | | } |
| | | |
| | | function reloadAllData() { |
| | | getChannels(currentType.value) |
| | | } |
| | | |
| | | function moreChannelTasksBtn(channelNumber: number) { |
| | |
| | | getChannels, |
| | | moreBtnStatus, |
| | | activeTask, |
| | | reload, |
| | | reloadChannel, |
| | | setActiveTask, |
| | | moreChannelTasksBtn, |
| | | foldChannelTasksBtn |
| | | foldChannelTasksBtn, |
| | | reloadAllData |
| | | } |
| | | }) |
| | |
| | | $color: #30decd; |
| | | |
| | | .current-date-time { |
| | | width: 370px; |
| | | color: $color; |
| | | font-size: 24px; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="dashboard-title"> |
| | | <div class="title-text"> |
| | | 智能工作台 — {{ deviceStore?.deviceInfo?.currentDeviceID ?? '' }} |
| | | 智能工作台 — |
| | | <el-popover |
| | | placement="bottom" |
| | | :width="200" |
| | | trigger="click" |
| | | :content="deviceStore?.deviceInfo?.currentDeviceID ?? ''" |
| | | > |
| | | <template #reference> |
| | | <el-text truncated class="device-name">{{ deviceStore?.deviceInfo?.currentDeviceID ?? '' }}</el-text> |
| | | </template> |
| | | </el-popover> |
| | | <el-icon size="32" color="#0db7f5" style="margin-left: 20px; cursor: pointer" @click="openDevicesModal"> |
| | | <IconSlider></IconSlider> |
| | | </el-icon> |
| | | </div> |
| | | <div class="title-status"> |
| | | <div class="connection-info" @click="openProblemsModal"> |
| | | <el-icon size="30" :color="problemsIconStatus ? '#00ff00' : '#ff0000'"> |
| | | <el-icon size="26" :color="problemsIconStatus ? '#00ff00' : '#ff0000'"> |
| | | <AlertLightIcon></AlertLightIcon> |
| | | </el-icon> |
| | | </div> |
| | | <div class="cloud-connection-status"> |
| | | <el-icon v-if="cloudConnectionIconStatus" size="45" color="#00ff00"> |
| | | <el-icon v-if="cloudConnectionIconStatus" size="38" color="#00ff00"> |
| | | <IconCloudDone></IconCloudDone> |
| | | </el-icon> |
| | | |
| | | <el-icon v-else size="45" color="#ff0000"> |
| | | <el-icon v-else size="38" color="#ff0000"> |
| | | <IconCloudOff></IconCloudOff> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <DeviceCheckList v-model="showDevicesModal" :devices="deviceList"></DeviceCheckList> |
| | | <DeviceCheckList v-model="showDevicesModal" @should-reload="emits('shouldReload')"></DeviceCheckList> |
| | | <TroubleTrackerModal v-model="showProblemsModal" :problems="problemList"></TroubleTrackerModal> |
| | | </template> |
| | | <script setup lang="ts"> |
| | |
| | | import { useRequest } from 'vue-hooks-plus' |
| | | import { apiGetProblemList } from '@/api' |
| | | import { PROBLEMS_POLLING_DURATION } from '@/common/constants' |
| | | |
| | | const emits = defineEmits<{ |
| | | shouldReload: [] |
| | | }>() |
| | | |
| | | // 是否显示问题诊断modal |
| | | const showProblemsModal = ref(false) |
| | |
| | | align-items: center; |
| | | position: absolute; |
| | | top: 16px; |
| | | right: 40px; |
| | | right: 6px; |
| | | } |
| | | .connection-info { |
| | | margin-right: 10px; |
| | | cursor: pointer; |
| | | } |
| | | .device-name { |
| | | max-width: 340px; |
| | | font-size: 40px; |
| | | color: #fff; |
| | | } |
| | | </style> |
| | |
| | | <template #title>设备选择</template> |
| | | <div class="device-box"> |
| | | <el-scrollbar always class="scroller"> |
| | | <template v-if="devices?.length"> |
| | | <template v-if="deviceInfo?.deviceIDList?.length"> |
| | | <div |
| | | v-for="(item, index) in devices" |
| | | v-for="(item, index) in deviceInfo?.deviceIDList" |
| | | :key="index" |
| | | :class="item.checked ? 'device-item check-item' : 'device-item'" |
| | | @click="deviceClick(index)" |
| | | :class="selectedDevice === item ? 'device-item check-item' : 'device-item'" |
| | | @click="deviceClick(item)" |
| | | > |
| | | <div class="item-l"> |
| | | <span>{{ item.number }}</span> |
| | | {{ item.name }} |
| | | <span>{{ item }}</span> |
| | | <!-- {{ item }}--> |
| | | </div> |
| | | <div v-if="item.checked" class="item-r"> |
| | | <div v-if="selectedDevice === item" class="item-r"> |
| | | <el-icon class="item-icon" size="22" color="#00ff00"><CircleCheckFilled /></el-icon> |
| | | </div> |
| | | </div> |
| | |
| | | import { useVModel } from '@vueuse/core' |
| | | import { CircleCheckFilled } from '@element-plus/icons-vue' |
| | | import BigButton from '@/views/dashboard/components/BigButton.vue' |
| | | import { ref } from 'vue' |
| | | import { useDevicesStore } from '@/stores/devices' |
| | | import { storeToRefs } from 'pinia' |
| | | import { ref, watch } from 'vue' |
| | | import { apiSetCurrentDevice } from '@/api' |
| | | import { ElMessage } from 'element-plus' |
| | | export interface DeviceCheckListProps { |
| | | modelValue: boolean |
| | | } |
| | |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [show: boolean] |
| | | shouldReload: [] |
| | | }>() |
| | | const modelData = useVModel(props, 'modelValue', emit) |
| | | const deviceStore = useDevicesStore() |
| | | const { deviceInfo } = storeToRefs(deviceStore) |
| | | |
| | | const devices = ref([ |
| | | { |
| | | number: '111', |
| | | name: '设备A', |
| | | checked: true |
| | | }, |
| | | { |
| | | number: '222', |
| | | name: '设备b' |
| | | // 弹窗打开时设定当前选中的设备 |
| | | const selectedDevice = ref<string>('') |
| | | watch(modelData, () => { |
| | | if (modelData.value) { |
| | | selectedDevice.value = deviceInfo.value?.currentDeviceID ?? '' |
| | | } |
| | | ]) |
| | | }) |
| | | |
| | | function deviceClick(deviceId: string) { |
| | | selectedDevice.value = deviceId |
| | | } |
| | | |
| | | function saveModal() { |
| | | if (!selectedDevice.value) { |
| | | ElMessage({ |
| | | message: '请先选中一个设备', |
| | | type: 'error', |
| | | duration: 3 * 1000 |
| | | }) |
| | | return |
| | | } |
| | | apiSetCurrentDevice({ currentDeviceID: selectedDevice.value }) |
| | | .then(() => { |
| | | ElMessage({ |
| | | message: '设定成功', |
| | | type: 'success', |
| | | duration: 2 * 1000 |
| | | }) |
| | | modelData.value = false |
| | | emit('shouldReload') |
| | | }) |
| | | .catch((err) => { |
| | | console.error(err) |
| | | ElMessage({ |
| | | message: err.msg, |
| | | type: 'error', |
| | | duration: 3 * 1000 |
| | | }) |
| | | }) |
| | | .finally(() => { |
| | | deviceStore.startPollingDevice() |
| | | }) |
| | | } |
| | | function closeModal() { |
| | | modelData.value = false |
| | | } |
| | | function saveModal() {} |
| | | const deviceClick = (index) => { |
| | | devices.value.find((ele) => (ele.checked = false)) |
| | | |
| | | devices.value[index].checked = true |
| | | } |
| | | </script> |
| | | <style scoped lang="scss"> |
| | |
| | | </template> |
| | | </div> |
| | | </div> |
| | | <TaskControlModal |
| | | v-model="showTaskControlModal" |
| | | :task="task" |
| | | @produce-start="emit('shouldReload', task)" |
| | | ></TaskControlModal> |
| | | <TaskControlModal v-model="showTaskControlModal" :task="task" @produce-start="onProduceStart"></TaskControlModal> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import type { Task } from '@/api/task' |
| | |
| | | * 完成任务 |
| | | */ |
| | | function finishTaskProduce() { |
| | | if (task?.value?.Procedure?.ID) { |
| | | if (task?.value && task.value?.Procedure?.ID) { |
| | | finishTask({ id: task!.value.Procedure.ID }).then( |
| | | (res) => { |
| | | ElMessage({ |
| | | message: '操作成功!', |
| | | type: 'success' |
| | | }) |
| | | emit('shouldReload', task.value) |
| | | emit('shouldReload', task.value as Task) |
| | | }, |
| | | (err) => { |
| | | console.error(err) |
| | |
| | | } |
| | | } |
| | | |
| | | function onProduceStart() { |
| | | emit('shouldReload', task!.value as Task) |
| | | } |
| | | |
| | | /** |
| | | * 格式化时间戳 |
| | | * @param timestamp 后端返的10位时间戳 |
New file |
| | |
| | | <template> |
| | | <div class="task-step"> |
| | | <div v-for="(item, index) in props.steps" :key="index" class="task-step-item" :style="{ 'flex-basis': flexBasis }"> |
| | | <el-icon v-if="index + 1 < active" class="icon" size="22" color="#01f304"><CircleCheck /></el-icon> |
| | | |
| | | <el-icon v-if="index + 1 === active" class="icon" size="22" color="#c25915"><Clock /></el-icon> |
| | | <el-icon v-if="index + 1 > active" class="icon" size="22" color="#7d7f83"><Clock /></el-icon> |
| | | <span class="text" :class="{ green: index + 1 < active, red: index + 1 === active, gray: index + 1 > active }"> |
| | | {{ item }} |
| | | </span> |
| | | <span class="line"></span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { CircleCheck, Clock } from '@element-plus/icons-vue' |
| | | import { computed } from 'vue' |
| | | |
| | | export interface TaskStepProps { |
| | | active: number |
| | | steps: string[] |
| | | } |
| | | const props = defineProps<TaskStepProps>() |
| | | |
| | | const flexBasis = computed(() => { |
| | | if (props.steps.length) { |
| | | return `${Math.floor((1 / props.steps.length) * 100)}%` |
| | | } |
| | | return '0' |
| | | }) |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .task-step { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .task-step-item { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 1; |
| | | &:last-child { |
| | | flex-basis: auto !important; |
| | | flex-shrink: 0; |
| | | flex-grow: 0; |
| | | & > .line { |
| | | display: none; |
| | | } |
| | | } |
| | | .icon { |
| | | margin-right: 6px; |
| | | } |
| | | .green { |
| | | color: #01f304; |
| | | } |
| | | .red { |
| | | color: #c25915; |
| | | } |
| | | .gray { |
| | | color: #7d7f83; |
| | | } |
| | | } |
| | | .text { |
| | | flex-shrink: 0; |
| | | } |
| | | .line { |
| | | display: inline-block; |
| | | height: 1px; |
| | | background-color: #fff; |
| | | width: 100%; |
| | | } |
| | | </style> |