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>-&#45;&#45;&#45;&#45; 鍓╀綑鏃堕棿 -&#45;&#45;&#45;&#45;</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