.gitignore | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
public/leaf-shape.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
public/modal-background-wider.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
public/modal-background.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/task.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/auto-imports.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/common/composable/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/common/composable/useCountDown.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/common/utils/stepTimer.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/BaseModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/components/TaskControl.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/dashboard/components/TaskControlModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
.gitignore
@@ -27,3 +27,6 @@ *.njsproj *.sln *.sw? /src/auto-imports.d.ts /src/components.d.ts public/leaf-shape.png
public/modal-background-wider.png
public/modal-background.png
src/api/index.ts
@@ -1,5 +1,5 @@ import { request } from '@/common/utils' import type { TasksGroupByChannel } from './task' import type { CraftParamsResponse, TasksGroupByChannel } from './task' import type { PLCResponse } from './plc' export interface BaseResponse<T = any> { @@ -36,6 +36,10 @@ procedureId: number } /** * 获取PLC运行数据 * @param params */ export function getProductProgress(params: ProductProgressParams) { return request<BaseResponse<PLCResponse>>({ url: '/v1/plc/productProgress', @@ -43,3 +47,29 @@ data: params }) } export interface CraftParamsParams { id: number } /** * 获取工艺参数 * @param params */ export function getCraftParams(params: CraftParamsParams) { return request<BaseResponse<CraftParamsResponse>>({ url: `v1/task/start/${params.id}`, method: 'get', data: params }) } export interface SendProcessParamsParams { procedureId: number } export function sendProcessParams(params: SendProcessParamsParams) { return request<BaseResponse>({ url: `v1/task/sendProcessParams`, method: 'post', data: params }) } src/api/task.ts
@@ -87,3 +87,16 @@ export interface TasksGroupByChannel { [channel: number]: TasksResponse } /** * 工艺参数 */ export interface CraftParam { Key: string Value: string } export interface CraftParamsResponse { number: string Params: CraftParam[] } src/auto-imports.d.ts
@@ -4,4 +4,6 @@ // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import export {} declare global {} declare global { } src/common/composable/index.ts
@@ -1,3 +1,3 @@ // 本文件放置通用 composable (组合式函数), 注意与 utils 区分 export {} import { useCountDown } from './useCountDown' export { useCountDown } src/common/composable/useCountDown.ts
New file @@ -0,0 +1,79 @@ import { StepTimer } from '@/common/utils/stepTimer' import { computed, onUnmounted, ref } from 'vue' interface Options { onEnd?: Function onAbort?: Function } /** * 格式化计时 * @param time */ function formatTime(time: number) { const minutes = Math.floor(time / 60) const seconds = time % 60 const minutesString = minutes.toString().padStart(2, '0') // 在分钟数不足两位时,在前面补0 const secondsString = seconds.toString().padStart(2, '0') // 在秒数不足两位时,在前面补0 return `${minutesString}:${secondsString}` } function useCountDown(seconds: number, options?: Options) { const timer = new StepTimer(seconds * 1000, 1000) const remainingSeconds = ref<number>(seconds) const formattedTime = computed(() => { return formatTime(remainingSeconds.value) }) const countdownStatus = ref<'stop' | 'running' | 'pause' | 'complete'>('stop') function startCountdown() { countdownStatus.value = 'running' timer .start((round) => { remainingSeconds.value = seconds - round }) .then( (data) => { countdownStatus.value = 'complete' options?.onEnd?.(data) }, (err) => { countdownStatus.value = 'stop' options?.onAbort?.(err) } ) } onUnmounted(() => { countdownStatus.value = 'stop' timer.destroy() }) function pauseCountdown() { countdownStatus.value = 'pause' timer.pause() } function continueCountdown() { countdownStatus.value = 'running' timer.continue() } function stopCountdown() { countdownStatus.value = 'stop' timer.abort() } return { startCountdown, remainingSeconds, pauseCountdown, continueCountdown, stopCountdown, formattedTime, countdownStatus } } export { useCountDown } src/common/utils/stepTimer.ts
New file @@ -0,0 +1,142 @@ type ResolveFn<T = string> = (value: T | PromiseLike<T>) => void type RejectFn = (reason?: any) => void type PerStepFn = (r: number) => void /** * 步进计时器 * * @example * const timer = new Timer(10_000, 1_000) * timer * .start((round: number) => { * console.log(round) * }) * .then( * () => { * console.log('over') * }, * (err) => { * console.log(err) * } * ) */ class StepTimer { private timer?: number /** 计时器总时长 单位毫秒 */ private readonly duration: number = 0 /** 计时步长 单位毫秒 */ private readonly step: number = 1000 /** 计时轮数 */ private round: number = 0 private blockFlag: boolean = false private reject?: RejectFn private perStep?: PerStepFn private resolve?: ResolveFn /** * 创建计时器 * @param duration 计时器总持续时间 * @param step 计时间隔 */ constructor(duration: number, step: number) { this.duration = duration this.step = step this.round = 0 } /** * 启动计时器 * 接受一个函数, 每轮触发一次 * 返回 Promise, 在计时结束后 resolve * 重复调用会清除之前的计时器重新计时 * @param perStep */ start(perStep: PerStepFn): Promise<string> { if (this.timer) { this.reset() } this.perStep = perStep return new Promise((resolve, reject) => { this.resolve = resolve this.reject = reject this.createIntervalTimer(this.duration) }) } private createIntervalTimer(duration: number) { this.timer = setInterval(() => { this.round++ if (this.step * this.round < duration) { if (this.blockFlag) { this.round-- clearInterval(this.timer) return } try { this.perStep!(this.round) } catch (e) { this.reject!(e) } } else if (this.step * this.round === duration) { if (this.blockFlag) { this.round-- clearInterval(this.timer) return } try { this.perStep!(this.round) } catch (e) { this.reject!(e) } clearInterval(this.timer) this.resolve!('time over') } else { clearInterval(this.timer) this.resolve!('time over') } }, this.step) } pause() { if (!this.blockFlag) { this.blockFlag = true } } continue() { if (this.blockFlag) { this.blockFlag = false clearInterval(this.timer) const newDuration = this.duration - this.round * this.step this.createIntervalTimer(newDuration) } } /** * 提前终止计时器 */ abort() { this.reject?.('abort') this.reset() } /** * 销毁定时器 */ destroy() { this.reset() } private reset() { this.blockFlag = false this.round = 0 clearInterval(this.timer) this.timer = undefined this.reject?.('restart') } } export { StepTimer } src/components.d.ts
@@ -7,10 +7,12 @@ declare module 'vue' { export interface GlobalComponents { BaseModal: typeof import('./components/BaseModal.vue')['default'] 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'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElProgress: typeof import('element-plus/es')['ElProgress'] src/components/BaseModal.vue
New file @@ -0,0 +1,89 @@ <template> <div class="base-modal" :class="{ wider: props.wider }"> <el-dialog v-model="modelData" :close-on-click-modal="false" width="753px" :show-close="false"> <template #header> <div class="modal-title"> <div class="modal-title-text"> <slot name="title"></slot> </div> <div v-if="props.wider" class="modal-title-close" @click="closeModal"> <el-icon :size="22"><CloseBold /></el-icon> </div> </div> </template> <template #default> <slot name="default"></slot> </template> <template #footer> <slot name="footer"></slot> </template> </el-dialog> </div> </template> <script setup lang="ts"> import { useVModel } from '@vueuse/core' import { CloseBold } from '@element-plus/icons-vue' export interface BaseModalProps { /** 是否展示模态框 */ modelValue: boolean /** 更宽版本 */ wider: boolean } const props = withDefaults(defineProps<BaseModalProps>(), { modelValue: false, wider: false }) const emit = defineEmits<{ 'update:modelValue': [show: boolean] }>() const modelData = useVModel(props, 'modelValue', emit) function closeModal() { modelData.value = false } </script> <style scoped lang="scss"> :deep(.el-dialog) { background: transparent !important; background-image: url('/modal-background.png') !important; background-repeat: no-repeat; background-size: 100% 100% !important; width: 753px; height: 728px; } .wider { :deep(.el-dialog) { background: transparent !important; background-image: url('/modal-background-wider.png') !important; background-repeat: no-repeat; background-size: 100% 100% !important; width: 938px; height: 733px; } } .modal-title { position: relative; display: flex; align-items: center; &-text { padding-left: 12px; font-size: 26px; font-weight: 600; } &-close { height: 36px; width: 36px; border-radius: 50%; background-color: #00d7e9; position: absolute; display: flex; justify-content: center; align-items: center; top: -16px; right: -30px; } } </style> src/views/dashboard/components/TaskControl.vue
@@ -27,23 +27,27 @@ </template> </div> </div> <TaskControlModal v-model="showTaskControlModal" :task="task"></TaskControlModal> </template> <script setup lang="ts"> import type { Task } from '@/api/task' import { toRefs } from 'vue' import { ref, toRefs } from 'vue' import BigButton from '@/views/dashboard/components/BigButton.vue' import { useDateFormat } from '@vueuse/core' import TaskControlModal from '@/views/dashboard/components/TaskControlModal.vue' const props = defineProps<{ task?: Task }>() const { task } = toRefs(props) const showTaskControlModal = ref(false) /** * 开始生产 */ function startProduce() { // TODO: // console.log(1) showTaskControlModal.value = true } /** src/views/dashboard/components/TaskControlModal.vue
New file @@ -0,0 +1,328 @@ <template> <div class="task-control-modal"> <BaseModal v-model="modelData" :wider="false"> <template #title>新任务</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 }} </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 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 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> <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> </div> </div> </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> </BaseModal> </div> </template> <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 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' export interface TaskControlModalProps { task?: Task /** 是否展示模态框 */ modelValue: boolean } const props = withDefaults(defineProps<TaskControlModalProps>(), { task: undefined, modelValue: false }) const emit = defineEmits<{ 'update:modelValue': [show: boolean] }>() const modelData = useVModel(props, 'modelValue', emit) function closeModal() { modelData.value = false } const { task } = toRefs(props) const messageError = ref('') /** * 格式化时间戳 * @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 getCraftParamsErrMsg = ref('') /** * 获取当前展示的任务的工艺参数 */ function getTaskProduceParams(taskId?: number) { if (taskId) { craftParams.value = [] getCraftParamsErrMsg.value = '' getCraftParams({ id: taskId }).then( (res) => { craftParams.value = res.data.Params ?? [] getCraftParamsErrMsg.value = '' // TODO: 处理各个按钮显隐 // this.getInfo() console.log('processParams', craftParams.value) }, (err) => { console.error(err) craftParams.value = [] getCraftParamsErrMsg.value = '获取工艺参数失败!' } ) } } watch(modelData, () => { // 弹窗显示时获取工艺参数 if (modelData.value) { getTaskProduceParams(task?.value?.Procedure?.ID) } }) const countdown30s = useCountDown(3) function startCountdown30s() { countdown30s.startCountdown() } /** * 下发工艺参数 */ 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 : '抱歉,工序下发失败!' } ) .finally(() => { isLoading.value = false }) } } const { channels } = storeToRefs(useTasksStore()) const safeProduce = computed(() => { if (task?.value?.Channel) { return channels?.value?.[task.value.Channel]?.Prompt?.safeProduce } return '' }) const message = ref(safeProduce.value) const isLoading = ref(false) </script> <style scoped lang="scss"> .modal-content { height: 550px; } .content-scroll { height: 400px; overflow: hidden; } :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: 400px; } .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%; } </style>