单应用项目,可以创建很多独立工具类页面 ,不用登录 初始化的页面
zhangchen
2025-09-01 c1129faa1b6aaf08446e9cf0a2eeaaa54d3c7041
src/views/mobile/bedsideAuxiliaryScreen/components/ScheduledTask.vue
@@ -0,0 +1,562 @@
<template>
  <div class="scheduled-task-container">
    <el-dialog
      v-model="isShow"
      center
      title="定时任务"
      width="80%"
      :show-close="false"
      class="scheduled-task-dialog"
      top="0"
      :destroy-on-close="true"
      :close-on-click-modal="false"
    >
      <template #header>
        <div class="scheduled-task-header">
          <span class="header-title">{{ title }}</span>
          <img
            :src="closeImg"
            class="header-close"
            @click="handleCancel"
            alt=""
          />
        </div>
      </template>
      <div class="scheduled-task-content" v-loading="loading">
        <div class="content-left">
          <div class="content-left-date">
            <TimePicker v-model="timeValue" :disabled="type === 1" />
            <!-- 创建的情况 -->
            <div class="date-btn" v-if="type === 0">
              <div
                v-for="(item, index) in dateOptions"
                :key="index"
                class="my-button date"
                @click="onAddMinutesClick(item)"
              >
                {{ item.label }}
              </div>
            </div>
            <!-- 查看的情况 -->
            <div class="countdown" v-else>
              <div class="countdown-btn">
                剩余时间:{{ formattedCountdown }}
              </div>
            </div>
          </div>
          <div class="content-left-stereotyped-writing">
            <div class="stereotyped-writing">
              <input
                v-model.trim="taskName"
                type="text"
                :disabled="isInpDisabled || type === 1"
                class="stereotyped-writing-input"
                placeholder="请输入自定义内容"
              />
            </div>
            <div class="stereotyped-writing-list">
              <div
                v-for="(item, index) in taskOptions"
                :key="index"
                class="my-button list-item"
                @click="onStereotypedWritingClick(item)"
                :class="taskItemCheck === item.value ? 'check' : ''"
              >
                {{ item.label }}
              </div>
            </div>
          </div>
        </div>
        <div class="content-right">
          <div class="stop-btn" v-if="type === 1" @click="stopTask">
            紧急停止
          </div>
        </div>
      </div>
      <template #footer>
        <div class="my-button cancel" @click="handleCancel">取消</div>
        <div
          class="my-button confirm"
          :class="loading ? 'cancel' : ''"
          @click="handleConfirm"
        >
          确认
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script lang="ts" setup>
import { computed, onUnmounted, ref, watch } from "vue";
import dayjs from "dayjs";
import { setTimeoutAlert, stopTimeoutAlert } from "@/utils/httpApi";
import { useBedsideAuxiliaryScreenStore } from "@/store/bedsideAuxiliaryScreen";
import { taskOptions } from "@/store/type/task.type";
import type { TaskItem } from "@/store/type/task.type";
// @ts-ignore
import TimePicker from "./TimePicker.vue";
import closeImg from "@/img/close.png";
import { ElMessage, ElMessageBox } from "element-plus";
interface DateItem {
  label: string;
  value: number;
}
const bedsideAuxiliaryScreenStore = useBedsideAuxiliaryScreenStore();
const isShow = ref(false);
const type = ref<0 | 1>(0); // 0 创建 | 1 查看
const taskName = ref(""); // 任务名称
const isInpDisabled = ref(false); // 输入框是否禁用
const timeValue = ref("");
const detaCheck = ref<number | null>(); // 这个是判断时间按钮的
const loading = ref(false);
const dateOptions = ref<DateItem[]>([
  { label: "15分钟", value: 15 },
  { label: "30分钟", value: 30 },
  { label: "45分钟", value: 45 },
  { label: "60分钟", value: 60 },
]);
let timer: ReturnType<typeof setInterval> | null = null;
const countdown = ref(null); // 定时任务的倒计时
const formattedCountdown = computed(() => {
  if (countdown.value == null || countdown.value <= 0) return "0s";
  const minutes = Math.floor(countdown.value / 60);
  const seconds = countdown.value % 60;
  if (minutes > 0) {
    return `${minutes}m ${seconds}s`;
  } else {
    return `${seconds}s`;
  }
});
const title = computed(() => {
  return type.value ? "查看定时任务" : "创建定时任务";
});
const taskItemCheck = computed(() => {
  return taskOptions.find((e) => e.value === taskName.value)?.value || "";
});
watch(
  () => bedsideAuxiliaryScreenStore.taskData?.[0]?.countdown,
  (val) => {
    if (typeof val === "number") {
      startCountdown(val);
    } else {
      clearTimer();
    }
  },
  { immediate: true }
);
watch(countdown, (newVal) => {
  if (newVal <= 0) {
    loading.value = false;
    handleCancel();
    // ElMessage.warning('当前任务发生变化,请重新设置');
  }
});
// 清除定时器函数
function clearTimer() {
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
}
// 启动新的倒计时
function startCountdown(seconds: number) {
  clearTimer();
  countdown.value = seconds;
  timer = setInterval(() => {
    if (countdown.value > 0) {
      countdown.value -= 1;
    } else {
      clearTimer();
    }
  }, 1000);
}
const openDialog = () => {
  isShow.value = true;
  if (
    !bedsideAuxiliaryScreenStore.taskData ||
    bedsideAuxiliaryScreenStore.taskData.length <= 0
  ) {
    const time = dayjs();
    timeValue.value = time.format("HH:mm");
    detaCheck.value = null;
    isInpDisabled.value = false;
    taskName.value = "";
    type.value = 0;
  } else {
    const taskDataItem = bedsideAuxiliaryScreenStore.taskData[0];
    timeValue.value = dayjs(taskDataItem.taskDate).format("HH:mm");
    // 查是否是定型文的内容
    const writing = taskOptions.find((e) => e.value === taskDataItem.taskName);
    if (writing) {
      taskName.value = writing.value;
      isInpDisabled.value = true;
    } else {
      taskName.value = taskDataItem.taskName;
    }
    type.value = 1;
  }
};
const onStereotypedWritingClick = (item: TaskItem) => {
  if (type.value === 1) return;
  if (taskName.value === item.value) {
    taskName.value = "";
    isInpDisabled.value = false;
  } else {
    taskName.value = item.value;
    isInpDisabled.value = true;
  }
};
const onAddMinutesClick = (item: DateItem) => {
  if (type.value === 1) return;
  detaCheck.value = item.value;
  timeValue.value = addMinutes(timeValue.value, item.value);
};
const addMinutes = (time: string, delta: number): string => {
  const [hours, minutes] = time.split(":").map(Number);
  const totalMinutes = hours * 60 + minutes + delta;
  // 处理负数和超过 1440(一天分钟数)情况
  const normalized = (totalMinutes + 1440) % 1440;
  const newHours = Math.floor(normalized / 60);
  const newMinutes = normalized % 60;
  return `${newHours.toString().padStart(2, "0")}:${newMinutes
    .toString()
    .padStart(2, "0")}`;
};
const handleCancel = () => {
  if (loading.value) return;
  isShow.value = false;
};
const handleConfirm = async () => {
  if (loading.value) return;
  const today = dayjs().format("YYYY-MM-DD");
  const fullDateTime = dayjs(`${today} ${timeValue.value}`)
    .second(0)
    .millisecond(0); // 秒和毫秒都去掉,要不然间隔短了就是0分钟
  const now = dayjs().second(0).millisecond(0); // 秒和毫秒都去掉
  if (!fullDateTime.isAfter(now))
    return ElMessage.warning("任务提醒时间不能早于或等于当前时间");
  if (!taskName.value) return ElMessage.warning("任务内容不能为空");
  loading.value = true;
  try {
    const diffMinutes = fullDateTime.diff(now, "minute");
    const params = {
      deviceCode: bedsideAuxiliaryScreenStore.deviceData.deviceCode,
      minutes: diffMinutes,
      alertText: taskName.value,
    };
    const recordCode = bedsideAuxiliaryScreenStore.deviceData.recordCode;
    const { data, message } = await setTimeoutAlert(params);
    if (data !== "OK") return ElMessage.warning(message);
    ElMessage.success("操作成功");
    bedsideAuxiliaryScreenStore.setSyncTask({
      deviceCode: params.deviceCode,
      recordCode: recordCode,
      taskDate: dayjs(fullDateTime).format("YYYY-MM-DD HH:mm"),
      taskName: params.alertText,
      overdue: false,
      sync: false,
      countdown: diffMinutes * 60,
    });
    // 这里得先把loading关了
    loading.value = false;
    handleCancel();
  } catch (error) {
    console.log(error);
  } finally {
    loading.value = false;
  }
};
/** 停止任务 */
const stopTask = () => {
   ElMessageBox.confirm(
    '是否确认停止当前任务?',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
    }
  )
    .then(async () => {
      loading.value = true;
      try {
        await stopTimeoutAlert(bedsideAuxiliaryScreenStore.deviceData.deviceCode);
        bedsideAuxiliaryScreenStore.clearTask();
        clearTimer();
        loading.value = false;
        handleCancel();
        ElMessage.success('已停止')
      } catch(error) {
        ElMessage.error('操作失败:' + error)
      } finally {
        loading.value = false;
      }
    })
    .catch(() => {
    })
};
onUnmounted(() => {
  clearTimer();
});
defineExpose({
  openDialog,
});
</script>
<style lang="less" scoped>
* {
  box-sizing: border-box;
}
.scheduled-task-container {
  ::v-deep(.el-dialog) {
    padding: 0;
    border-radius: 6px;
    overflow: hidden;
  }
  ::v-deep(.el-dialog__footer) {
    padding: 4px;
  }
  ::v-deep(.el-upload-dragger) {
    height: 65px;
    padding: 0 !important;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  ::v-deep(.el-upload-dragger .el-icon--upload) {
    display: none;
  }
  ::v-deep(.el-dialog__header) {
    padding-bottom: 6px;
  }
  .scheduled-task-header {
    position: relative;
    height: 16px;
    background: #ff7472;
    .header-title {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translateX(-50%) translateY(-50%);
      font-family: AlibabaPuHuiTi, AlibabaPuHuiTi;
      font-weight: 500;
      font-size: 8px;
      color: #ffffff;
      line-height: 11px;
      text-align: center;
    }
    .header-close {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      right: 6px;
      width: 15px;
      height: 15px;
      transition: transform 0.2s;
      &:active {
        opacity: 0.6;
        transform: translateY(-50%) scale(0.95);
      }
    }
  }
  .scheduled-task-content {
    padding: 0 12px 0px 12px;
    margin-bottom: 4px;
    border-bottom: 1px solid #d8d8d8;
    display: flex;
    .content-left {
      flex: 1;
      border-right: 1px solid #d8d8d8;
      padding-bottom: 6px;
      padding-right: 12px;
      .content-left-date {
        border-bottom: 1px solid #d8d8d8;
        display: flex;
        .date-btn {
          flex: 1;
          display: flex;
          flex-wrap: wrap;
          gap: 9px;
          margin-left: 5px;
          justify-content: flex-end;
          .date {
            margin-left: 0;
            padding: 0;
            width: 44px;
            height: 18px;
            line-height: 18px;
            text-align: center;
            font-size: 11px;
            background-color: #769aff;
          }
        }
        .countdown {
          flex: 1;
          display: flex;
          align-items: center;
          justify-content: center;
          margin-left: 5px;
          .countdown-btn {
            padding: 0 8px;
            background: #769aff;
            box-shadow: 1px 1 0px 0px rgba(130, 126, 126, 0.5);
            border-radius: 2px;
            font-family: PingFangSC, PingFang SC;
            font-weight: 600;
            font-size: 11px;
            line-height: 23px;
            color: #ffffff;
            text-shadow: 1px 1px 0px rgba(130, 126, 126, 0.5);
            text-align: left;
            font-style: normal;
          }
        }
      }
      .content-left-stereotyped-writing {
        .stereotyped-writing {
          position: relative;
          height: 16px;
          padding-top: 6px;
          margin-bottom: 6px;
          border-radius: 2px;
          border: 1px solid #979797;
          overflow: hidden;
          .stereotyped-writing-input {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            border: none;
            outline: none;
            padding: 0 4px;
            line-height: 16px;
            font-size: 9px;
            font-family: PingFangSC, PingFang SC;
            vertical-align: middle;
            box-sizing: border-box; // 避免padding撑高
            &::placeholder {
              font-family: inherit;
              font-size: inherit;
              line-height: inherit;
              color: #aaaaaa;
              opacity: 1;
            }
            &:disabled {
              background-color: #f5f5f5; // 灰色背景
              color: #999999; // 灰色文字
              cursor: not-allowed;
            }
          }
        }
        .stereotyped-writing-list {
          display: flex;
          flex-wrap: wrap;
          gap: 7px;
          .list-item {
            width: 40px;
            margin-left: 0;
            padding-left: 0;
            padding-right: 0;
            background-color: #e6a23c;
            &.check {
              background-color: #bbc6dd;
            }
          }
        }
      }
    }
    .content-right {
      width: 59px;
      padding: 0 3px;
      .stop-btn {
        width: 100%;
        background: #ff7472;
        border-radius: 2px;
        font-family: PingFangSC, PingFang SC;
        font-weight: 600;
        font-size: 6px;
        color: #ffffff;
        line-height: 18px;
        text-align: center;
        font-style: normal;
      }
    }
  }
  .my-button {
    display: inline-block;
    border-radius: 2px;
    padding: 0px 10px;
    font-family: PingFangSC, PingFang SC;
    font-weight: 500;
    font-size: 7px;
    color: #ffffff;
    line-height: 16px;
    letter-spacing: 1px;
    text-align: center;
    font-style: normal;
    transition: transform 0.1s ease, opacity 0.1s ease;
    cursor: pointer;
    &:active {
      transform: scale(0.95);
      opacity: 0.8;
    }
    &:not(:first-child) {
      margin-left: 6px;
    }
    &.confirm {
      background: #769aff;
    }
    &.cancel {
      background: #bbc6dd;
    }
    &.refresh {
      background: #e6a23c;
    }
  }
}
</style>
<style>
.scheduled-task-dialog {
  margin: 0 auto;
  top: 50% !important;
  transform: translateY(-50%) !important;
}
</style>