From 783b513df88caef1391ecb1df47e8779c90a830e Mon Sep 17 00:00:00 2001 From: songshankun <songshankun@foxmail.com> Date: 星期二, 31 十月 2023 20:19:52 +0800 Subject: [PATCH] feat: 添加任务弹窗组件,计时器,基础弹窗组件 --- public/modal-background-wider.png | 0 src/common/composable/useCountDown.ts | 79 +++++++ src/components/BaseModal.vue | 89 ++++++++ src/common/utils/stepTimer.ts | 142 ++++++++++++ public/modal-background.png | 0 public/leaf-shape.png | 0 src/views/dashboard/components/TaskControlModal.vue | 328 +++++++++++++++++++++++++++++ 7 files changed, 638 insertions(+), 0 deletions(-) diff --git a/public/leaf-shape.png b/public/leaf-shape.png new file mode 100644 index 0000000..dcfa631 --- /dev/null +++ b/public/leaf-shape.png Binary files differ diff --git a/public/modal-background-wider.png b/public/modal-background-wider.png new file mode 100644 index 0000000..48b8e40 --- /dev/null +++ b/public/modal-background-wider.png Binary files differ diff --git a/public/modal-background.png b/public/modal-background.png new file mode 100644 index 0000000..b057156 --- /dev/null +++ b/public/modal-background.png Binary files differ diff --git a/src/common/composable/useCountDown.ts b/src/common/composable/useCountDown.ts new file mode 100644 index 0000000..a5dfa62 --- /dev/null +++ b/src/common/composable/useCountDown.ts @@ -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 } diff --git a/src/common/utils/stepTimer.ts b/src/common/utils/stepTimer.ts new file mode 100644 index 0000000..ddf4fc4 --- /dev/null +++ b/src/common/utils/stepTimer.ts @@ -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 } diff --git a/src/components/BaseModal.vue b/src/components/BaseModal.vue new file mode 100644 index 0000000..34a552a --- /dev/null +++ b/src/components/BaseModal.vue @@ -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> diff --git a/src/views/dashboard/components/TaskControlModal.vue b/src/views/dashboard/components/TaskControlModal.vue new file mode 100644 index 0000000..247a850 --- /dev/null +++ b/src/views/dashboard/components/TaskControlModal.vue @@ -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> -- Gitblit v1.8.0