package-lock.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
package.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/App.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/device.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/common/composable/useCountDown.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/icons/AlertLightIcon.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/stores/devices.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/stores/tasks.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/components/DashboardTitle.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/components/TaskControl.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/components/TaskControlModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
package-lock.json
@@ -10,6 +10,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.1.0", "@vueuse/core": "^10.5.0", "@xstate/vue": "^2.0.0", "axios": "^1.5.1", "dayjs": "^1.11.10", "element-plus": "^2.4.1", @@ -17,7 +18,8 @@ "pinia": "^2.1.7", "vue": "^3.3.4", "vue-hooks-plus": "^1.8.5", "vue-router": "^4.2.5" "vue-router": "^4.2.5", "xstate": "^4.38.3" }, "devDependencies": { "@iconify-json/mdi": "^1.1.55", @@ -1473,6 +1475,24 @@ }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } } }, "node_modules/@xstate/vue": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/@xstate/vue/-/vue-2.0.0.tgz", "integrity": "sha512-JlrJ3d+I6rZCcFBuu3O4GP+mGJfd11O9o69wRedzPMqZ+hxcMRBsih9L5kKnJHcU9CTmdJTT172oxTaYF7thzA==", "peerDependencies": { "@xstate/fsm": "^2.0.0", "vue": "^3.0.0", "xstate": "^4.31.0" }, "peerDependenciesMeta": { "@xstate/fsm": { "optional": true }, "xstate": { "optional": true } } @@ -4923,6 +4943,11 @@ "node": ">=12" } }, "node_modules/xstate": { "version": "4.38.3", "resolved": "https://registry.npmmirror.com/xstate/-/xstate-4.38.3.tgz", "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==" }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", @@ -5951,6 +5976,12 @@ "requires": {} } } }, "@xstate/vue": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/@xstate/vue/-/vue-2.0.0.tgz", "integrity": "sha512-JlrJ3d+I6rZCcFBuu3O4GP+mGJfd11O9o69wRedzPMqZ+hxcMRBsih9L5kKnJHcU9CTmdJTT172oxTaYF7thzA==", "requires": {} }, "acorn": { "version": "8.10.0", @@ -8557,6 +8588,11 @@ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true }, "xstate": { "version": "4.38.3", "resolved": "https://registry.npmmirror.com/xstate/-/xstate-4.38.3.tgz", "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==" }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", package.json
@@ -15,6 +15,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.1.0", "@vueuse/core": "^10.5.0", "@xstate/vue": "^2.0.0", "axios": "^1.5.1", "dayjs": "^1.11.10", "element-plus": "^2.4.1", @@ -22,7 +23,8 @@ "pinia": "^2.1.7", "vue": "^3.3.4", "vue-hooks-plus": "^1.8.5", "vue-router": "^4.2.5" "vue-router": "^4.2.5", "xstate": "^4.38.3" }, "devDependencies": { "@iconify-json/mdi": "^1.1.55", src/App.vue
@@ -4,6 +4,7 @@ <script setup lang="ts"> import { RouterView } from 'vue-router' import 'element-plus/es/components/message/style/css' </script> <style scoped></style> src/api/device.ts
New file @@ -0,0 +1,9 @@ export interface Devices { systemDeviceID: string currentDeviceID: string systemDeviceStatus: number clusterStatus: string clusterNodeQuantity: number systemDeviceRunSince: number deviceIDList: string[] } src/api/index.ts
@@ -1,6 +1,7 @@ import { request } from '@/common/utils' import type { CraftParamsResponse, TasksGroupByChannel } from './task' import type { PLCResponse } from './plc' import type { Devices } from './device' export interface BaseResponse<T = any> { code: number @@ -66,6 +67,11 @@ export interface SendProcessParamsParams { procedureId: number } /** * 下发工艺参数 * @param params */ export function sendProcessParams(params: SendProcessParamsParams) { return request<BaseResponse>({ url: `v1/task/sendProcessParams`, @@ -73,3 +79,29 @@ data: params }) } export interface FinishTaskParams { id: number } /** * 结束任务 * @param params */ export function finishTask(params: FinishTaskParams) { return request<BaseResponse>({ url: `v1/task/finish/${params.id}`, method: 'put', data: params }) } /** * 获取当前面板绑定的设备列表 */ export function getDeviceList() { return request<BaseResponse<Devices>>({ url: `/v1/device/list`, method: 'get' }) } src/common/composable/useCountDown.ts
@@ -19,7 +19,7 @@ } function useCountDown(seconds: number, options?: Options) { const timer = new StepTimer(seconds * 1000, 1000) let timer = new StepTimer(seconds * 1000, 1000) const remainingSeconds = ref<number>(seconds) @@ -65,6 +65,12 @@ timer.abort() } function reset() { timer.destroy() remainingSeconds.value = seconds timer = new StepTimer(seconds * 1000, 1000) } return { startCountdown, remainingSeconds, @@ -72,7 +78,8 @@ continueCountdown, stopCountdown, formattedTime, countdownStatus countdownStatus, reset } } src/components.d.ts
@@ -7,6 +7,7 @@ declare module 'vue' { export interface GlobalComponents { AlertLightIcon: typeof import('./components/icons/AlertLightIcon.vue')['default'] BaseModal: typeof import('./components/BaseModal.vue')['default'] DashboardLayout: typeof import('./components/DashboardLayout.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] @@ -14,6 +15,7 @@ ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElProgress: typeof import('element-plus/es')['ElProgress'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] src/components/icons/AlertLightIcon.vue
New file @@ -0,0 +1,36 @@ <script setup lang="ts"></script> <template> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200px" height="200px" viewBox="0 0 200 200" style=" shape-rendering: geometricPrecision; text-rendering: geometricPrecision; image-rendering: optimizeQuality; fill-rule: evenodd; clip-rule: evenodd; " xmlns:xlink="http://www.w3.org/1999/xlink" > <g> <path style="opacity: 0.988" fill="#d71d05" d="M 87.5,-0.5 C 95.1667,-0.5 102.833,-0.5 110.5,-0.5C 146.888,7.72465 167.388,30.3913 172,67.5C 172.5,101.165 172.667,134.832 172.5,168.5C 123.833,168.5 75.1667,168.5 26.5,168.5C 26.3333,132.498 26.5,96.4985 27,60.5C 33.9986,27.0017 54.1653,6.66837 87.5,-0.5 Z M 101.5,40.5 C 103.678,52.6006 105.011,64.9339 105.5,77.5C 116.858,77.4139 128.191,77.9139 139.5,79C 123.945,100.223 108.778,121.723 94,143.5C 93.167,128.176 92.667,112.842 92.5,97.5C 80.4954,97.6665 68.4954,97.4999 56.5,97C 71.8692,78.4097 86.8692,59.5764 101.5,40.5 Z" /> </g> <g> <path style="opacity: 0.995" fill="#d81d05" d="M 199.5,183.5 C 199.5,186.833 199.5,190.167 199.5,193.5C 197.167,195.167 195.167,197.167 193.5,199.5C 130.833,199.5 68.1667,199.5 5.5,199.5C 3.83333,197.167 1.83333,195.167 -0.5,193.5C -0.5,190.167 -0.5,186.833 -0.5,183.5C 1.67098,181.5 4.00432,179.666 6.5,178C 68.5,177.333 130.5,177.333 192.5,178C 194.996,179.666 197.329,181.5 199.5,183.5 Z" /> </g> </svg> </template> <style scoped lang="scss"></style> src/stores/devices.ts
New file @@ -0,0 +1,23 @@ import { ref, computed } from 'vue' import { defineStore } from 'pinia' import type { Devices } from '@/api/device' import { getDeviceList } from '@/api' export const useDevicesStore = defineStore('counter', () => { const devices = ref<Devices>() const deviceIDList = computed(() => devices?.value?.deviceIDList ?? []) function getDevicesInfo() { getDeviceList().then( (res) => { devices.value = res?.data }, (err) => { console.error(err) devices.value = undefined } ) } return { devices, deviceIDList, getDevicesInfo } }) src/stores/tasks.ts
@@ -25,7 +25,7 @@ offset: 0, limit: 3 } getTaskList(params) return getTaskList(params) .then((res) => { channels.value = res.data }) @@ -33,6 +33,36 @@ console.error(err) channels.value = [] }) } /** * 数据加载完成后自动选中一个任务 */ function autoSelectTask(channel: number) { const currentChannelTaskList = channels.value[channel].Tasks if (currentChannelTaskList?.length) { activeTask.value = currentChannelTaskList[0] } else { const firstNotEmptyChannel = Object.entries(channels.value).find((ele) => { const taskList = (ele[1] as TasksResponse)?.Tasks return !!taskList.length }) if (firstNotEmptyChannel) { const channelNumber = +firstNotEmptyChannel[0] activeTask.value = channels.value[channelNumber].Tasks[0] } } } /** * 刷新所有数据 */ function reload(channel: number) { getChannels(currentType.value).then(() => { autoSelectTask(channel) }) } function moreChannelTasksBtn(channelNumber: number) { @@ -96,6 +126,7 @@ getChannels, moreBtnStatus, activeTask, reload, setActiveTask, requestParamsMap, getParamsByChannel, src/views/dashboard/components/DashboardTitle.vue
New file @@ -0,0 +1,51 @@ <template> <div class="dashboard-title"> <div class="title-text">智能工作台 — {{ props?.deviceInfo?.currentDeviceID ?? '' }}</div> <div class="title-status"> <div class="connection-info" @click="openSelectDeviceModal"> <el-icon size="30" color="red"> <AlertLightIcon></AlertLightIcon> </el-icon> </div> <div class="connection-status"></div> </div> </div> </template> <script setup lang="ts"> import type { Devices } from '@/api/device' import AlertLightIcon from '@/components/icons/AlertLightIcon.vue' import { ref } from 'vue' export interface DashBoardTitleProps { deviceInfo: Devices } const props = defineProps<DashBoardTitleProps>() const showModal = ref(false) function openSelectDeviceModal() { showModal.value = true } </script> <style scoped lang="scss"> .dashboard-title { position: relative; } .title-text { display: flex; align-items: center; justify-content: center; font-size: 40px; font-weight: 700; } .title-status { position: absolute; top: 16px; right: 40px; } .connection-info { cursor: pointer; } </style> src/views/dashboard/components/TaskControl.vue
@@ -23,11 +23,30 @@ <template v-if="task?.Procedure.Status === 2 || task?.Procedure.Status === 3"> <BigButton class="btn" bg-color="#ff9933">打印</BigButton> <BigButton class="btn" bg-color="#00cc33">报工</BigButton> <BigButton class="btn" bg-color="#ff0000">完成</BigButton> <el-popconfirm width="340" confirm-button-text="确定" cancel-button-text="取消" :icon="CircleCloseFilled" icon-color="red" :hide-after="0" :teleported="false" title="请确认是否已完成此生产任务?" placement="top" @confirm="finishTaskProduce" > <template #reference> <BigButton class="btn" bg-color="#ff0000">完成</BigButton> </template> </el-popconfirm> </template> </div> </div> <TaskControlModal v-model="showTaskControlModal" :task="task"></TaskControlModal> <TaskControlModal v-model="showTaskControlModal" :task="task" @produce-start="emit('shouldReload', task)" ></TaskControlModal> </template> <script setup lang="ts"> import type { Task } from '@/api/task' @@ -35,11 +54,18 @@ import BigButton from '@/views/dashboard/components/BigButton.vue' import { useDateFormat } from '@vueuse/core' import TaskControlModal from '@/views/dashboard/components/TaskControlModal.vue' import { CircleCloseFilled } from '@element-plus/icons-vue' import { finishTask } from '@/api' import { ElMessage } from 'element-plus' const props = defineProps<{ task?: Task }>() const { task } = toRefs(props) const emit = defineEmits<{ shouldReload: [task: Task] }>() const showTaskControlModal = ref(false) @@ -48,6 +74,35 @@ */ function startProduce() { showTaskControlModal.value = true } /** * 完成任务 */ function finishTaskProduce() { if (task?.value?.Procedure?.ID) { finishTask({ id: task!.value.Procedure.ID }).then( (res) => { ElMessage({ message: '操作成功!', type: 'success' }) emit('shouldReload', task.value) }, (err) => { console.error(err) ElMessage({ message: '操作失败!', type: 'warning' }) } ) } else { ElMessage({ message: '当前设备没有工序!', type: 'warning' }) } } /** @@ -119,4 +174,41 @@ .finish-btn { background-color: #ff0000; } :deep(.el-popper) { background-color: #133f97; color: #fff; } :deep(.el-popconfirm__main) { font-size: 25px; } :deep(.el-popconfirm__icon) { font-size: 38px; } :deep(.el-popconfirm__main) { margin-bottom: 20px; } :deep(.el-popconfirm__action) { display: flex; align-items: center; justify-content: space-around; } :deep(.el-popconfirm__action .el-button) { font-size: 22px; height: 54px; width: 140px; color: #fff; background-color: #0ae5ec; &:hover { background-color: #0ae5ec; } &.is-text { color: #92a1c0; background-color: #133f97; border: 1px solid #0ae5ec; &:hover { background-color: #133f97; } } } </style> src/views/dashboard/components/TaskControlModal.vue
@@ -1,141 +1,148 @@ <template> <div class="task-control-modal"> <BaseModal v-model="modelData" :wider="false"> <template #title>新任务</template> <template #title> {{ !['下发参数成功', '下发参数失败'].includes(state.value as string) ? '新任务' : '提示' }} </template> <div class="modal-content"> <div v-if="!messageError" class="content-title"> <div class="content-title-item">当前任务:{{ task?.Procedure.procedure.procedureName || '' }}</div> <div class="content-title-item"> 生产数量: <div class="leaf-shape box"> {{ task?.Order?.amount || 0 }} <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> <div v-if="!!messageError" class="content-tips"> <div class="error-t"> <span v-if="messageError === '下发成功!'" class="el-icon-success color_success"></span> <span v-else class="el-icon-error color_error"></span> </div> <div class="error-m"> {{ messageError }} </div> <div class="font_size_20 color_fff" style="text-align: center; width: 100%; margin: 10px 0"> <span v-if="messageError === '下发成功!'" style="font-size: 30px"></span> <span v-else>请重试</span> </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 v-else 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">客户名称:{{ 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 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 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> </div> <div class="title-auto-box"></div> <div v-if="getCraftParamsErrMsg" class="process-err-tip"> <div class="tip-icon"> <span class="el-icon-error color_error"></span> </div> <div class="tip-content">提示: {{ getCraftParamsErrMsg }}</div> </div> <div v-if="countdown30s.countdownStatus.value === 'running'" class="countdown"> {{ countdown30s.formattedTime.value }} </div> <!-- <div v-if="showBtn === 2 || showBtn === 3" class="process-box">--> <!-- <div--> <!-- style="--> <!-- color: red;--> <!-- font-size: 26px;--> <!-- width: 100%;--> <!-- text-align: center;--> <!-- margin-bottom: 15px;--> <!-- line-height: 35px;--> <!-- "--> <!-- :class="showBtn === 3 && isLoading ? 'margin-top-10px' : 'margin-top-40px'"--> <!-- >--> <!-- <div v-if="showBtn === 2 || (showBtn === 3 && !isLoading)" class="gif-box">--> <!-- <template v-if="showBtn === 2">--> <!-- <div class="gif">--> <!-- <img src="../../public/shan.gif" />--> <!-- </div>--> <!-- </template>--> <!-- <template v-if="showBtn === 3 && !isLoading">--> <!-- <div class="gif">--> <!-- <span class="yuandian"></span>--> <!-- </div>--> <!-- </template>--> <!-- <div class="gif-right">--> <!-- <div>----- 剩余时间 -----</div>--> <!-- <div>--> <!-- <span>00:{{ countdown30s.formattedTime }}</span>--> <!-- </div>--> <!-- </div>--> <!-- </div>--> <!-- {{ message }}--> <!-- </div>--> <!-- <template v-if="showBtn === 3 && isLoading">--> <!-- <div class="progress-item">--> <!-- <span>{{ (+num / 30) * 100 }}%</span>--> <!-- <el-progress--> <!-- style="width: calc(100% - 50px); float: right"--> <!-- define-back-color="#CDC6C6"--> <!-- color="#00cc66"--> <!-- text-color="#fff"--> <!-- :text-inside="true"--> <!-- :stroke-width="20"--> <!-- :percentage="(+num / 30) * 100"--> <!-- ></el-progress>--> <!-- </div>--> <!-- </template>--> <!-- </div>--> </template> </el-scrollbar> </template> </el-scrollbar> </div> </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> <div class="btn"> <BigButton bg-color="#4765c0" @click="closeModal">暂缓生产</BigButton> <BigButton v-if="countdown30s.countdownStatus.value !== 'complete'" color="#0d0d0d" :disabled="countdown30s.countdownStatus.value === 'running'" @click="startCountdown30s" > 生产准备 </BigButton> <BigButton v-if="countdown30s.countdownStatus.value === 'complete'" bg-color="#4efefa" @click="startProduce"> 开始生产 </BigButton> </div> <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> @@ -143,13 +150,16 @@ <script setup lang="ts"> import type { CraftParam, Task } from '@/api/task' import { useDateFormat, useVModel } from '@vueuse/core' import { computed, ref, toRefs, watch } from 'vue' 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 @@ -162,15 +172,13 @@ }) const emit = defineEmits<{ 'update:modelValue': [show: boolean] /** 下发成功后触发, 用于外部获得刷新数据的时机 */ produceStart: [] }>() const modelData = useVModel(props, 'modelValue', emit) function closeModal() { modelData.value = false } const { task } = toRefs(props) const messageError = ref('') const { task } = toRefs(props) /** * 格式化时间戳 @@ -186,8 +194,8 @@ // 工艺参数 const craftParams = ref<CraftParam[]>() // 获取工艺参数失败信息 const getCraftParamsErrMsg = ref('') // 获取工艺参数结果信息 const getCraftParamsTip = ref('') /** * 获取当前展示的任务的工艺参数 @@ -195,19 +203,16 @@ function getTaskProduceParams(taskId?: number) { if (taskId) { craftParams.value = [] getCraftParamsErrMsg.value = '' getCraftParamsTip.value = '' getCraftParams({ id: taskId }).then( (res) => { craftParams.value = res.data.Params ?? [] getCraftParamsErrMsg.value = '' // TODO: 处理各个按钮显隐 // this.getInfo() console.log('processParams', craftParams.value) getCraftParamsTip.value = '' }, (err) => { console.error(err) craftParams.value = [] getCraftParamsErrMsg.value = '获取工艺参数失败!' getCraftParamsTip.value = '获取工艺参数失败!' } ) } @@ -217,59 +222,218 @@ // 弹窗显示时获取工艺参数 if (modelData.value) { getTaskProduceParams(task?.value?.Procedure?.ID) } else { reset() } }) const countdown30s = useCountDown(3) const countdown30s = useCountDown(30, { onEnd: () => { send('结束计时') } }) function startCountdown30s() { countdown30s.startCountdown() // 弹窗时获取安全生产提示文本 const { channels } = storeToRefs(useTasksStore()) const safeProduce = ref('') watch(modelData, () => { if (modelData.value) { safeProduce.value = channels?.value?.[task.value.Channel]?.Prompt?.safeProduce ?? '' } }) /** * 重置弹窗缓存状态 */ function reset() { countdown30s.reset() countdown3s.reset() getCraftParamsTip.value = '' deliveryTip.value = '' } /** * 下发工艺参数 * 按钮状态机 * 可以去 https://stately.ai/registry/new?mode=Design 查看状态转换图 */ function startProduce() { if (task.value?.Procedure?.ID) { message.value = '工艺参数下发中...' isLoading.value = true sendProcessParams({ procedureId: task.value.Procedure.ID }) .then( (res) => { console.log(res) messageError.value = '下发成功' }, (err) => { console.error(err) messageError.value = err.msg ? err.msg : '抱歉,工序下发失败!' const toggleMachine = createMachine({ id: 'produce', initial: '初始化', predictableActionArguments: true, states: { 初始化: { on: { 开始计时: { target: '计时中' }, 结束: { target: '初始化' } } }, 计时中: { on: { 结束计时: { target: '准备生产' }, 暂缓生产: { target: '初始化' } } }, 准备生产: { on: { 开始生产: { target: '下发参数中' }, 暂缓生产: { target: '初始化' } } }, 下发参数中: { on: { 成功: { target: '下发参数成功' }, 失败: { target: '下发参数失败' }, 暂缓生产: { target: '初始化' } } }, 下发参数成功: { on: { 结束: { target: '初始化' } ) .finally(() => { isLoading.value = false }) } }, 下发参数失败: { on: { 再次下发: { target: '准备生产' } } } } }) const { state, send } = useMachine(toggleMachine) /** * 暂缓生产, 直接关闭弹窗 */ function respiteProduce() { modelData.value = false send('暂缓生产') reset() } /** * 生产准备 */ function prepareProduce() { send('开始计时') countdown30s.startCountdown() } const { channels } = storeToRefs(useTasksStore()) const safeProduce = computed(() => { if (task?.value?.Channel) { return channels?.value?.[task.value.Channel]?.Prompt?.safeProduce // 参数下发成功或失败结果 const deliveryTip = ref('') // 参数下发成功后延时3秒后关闭弹窗 const countdown3s = useCountDown(3, { onEnd: () => { closeModal() } return '' }) const message = ref(safeProduce.value) const isLoading = ref(false) /** * 开始生产 , 下发工艺参数 */ 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: 400px; 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; @@ -306,7 +470,7 @@ margin: 0 auto; padding: 10px 20px; width: calc(100% - 40px); height: 400px; height: 340px; } .info { display: flex; @@ -325,4 +489,73 @@ .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
@@ -6,7 +6,9 @@ <template #leftBlock2> <ChannelCollapse :channels="channels"></ChannelCollapse> </template> <template #middleBlock1>标题</template> <template #middleBlock1> <DashboardTitle></DashboardTitle> </template> <template #middleBlock2> <el-tabs v-model="activeMainTabName" class="main-info-tabs"> <el-tab-pane label="加工信息" name="加工信息"> @@ -21,7 +23,7 @@ <template #middleBlock3> <SubTitle>任务详情</SubTitle> <div class="task-detail"> <TaskControl :task="activeTask"></TaskControl> <TaskControl :task="activeTask" @should-reload="reloadAllData"></TaskControl> </div> <ColorInfo :order="order" :type="1"></ColorInfo> <ColorInfo :order="order" :type="2"></ColorInfo> @@ -50,7 +52,7 @@ <script setup lang="ts"> import { computed, ref } from 'vue' import ChannelCollapse from '@/views/dashboard/components/ChannelCollapse.vue' import type { Worker, Order } from '@/api/task' import type { Worker, Order, Task } from '@/api/task' import type { PLCResponse } from '@/api/plc' import PersonInfo from '@/views/dashboard/components/PersonInfo.vue' import ProcessInfo from '@/views/dashboard/components/ProcessInfo.vue' @@ -67,6 +69,7 @@ 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' defineOptions({ name: 'DashboardView' @@ -79,7 +82,7 @@ } as unknown as Worker }) const process = computed(() => { return { product: '产品名称', number: '111', procedure: '工艺名称', isUpdate: true } return { product: '产品名称', number: '111', procedure: '工艺名称', isUpdate: true } as any }) const order = computed(() => { @@ -119,6 +122,10 @@ function changeTab(tab: LabelValue) { tasksStore.getChannels(tab.value) } function reloadAllData(task: Task) { tasksStore.reload(task.Channel) } </script> <style scoped lang="scss">