单应用项目,可以创建很多独立工具类页面 ,不用登录 初始化的页面
zhangchen
2025-07-26 3871d44d7f578b52e6bcb51d5c9ab1cd59a78559
ID1625-定时任务弹框完成
5个文件已修改
2个文件已添加
573 ■■■■ 已修改文件
src/composables/useAudioPlayer.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/bedsideAuxiliaryScreen.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/type/task.type.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/httpApi.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/bedsideAuxiliaryScreen/components/Header.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/bedsideAuxiliaryScreen/components/ScheduledTask.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/bedsideAuxiliaryScreen/components/TaskAlart.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/useAudioPlayer.ts
New file
@@ -0,0 +1,66 @@
import { ref, watch } from "vue";
let currentAudio: HTMLAudioElement | null = null;
export function useAudioPlayer() {
  const source = ref<string | null>(null);
  const isPlaying = ref(false);
  // 播放音频
  const play = (src: string) => {
    if (!src) return;
    // 如果当前正在播放其他音频,先暂停并释放
    if (currentAudio) {
      currentAudio.pause();
      currentAudio = null;
      isPlaying.value = false;
    }
    currentAudio = new Audio(src);
    currentAudio.loop = true; //  循环播放
    currentAudio.volume = 1.0; //  音量最大
    currentAudio
      .play()
      .then(() => {
        isPlaying.value = true;
      })
      .catch((err) => {
        console.error("音频播放失败:", err);
      });
    // 监听播放完毕
    currentAudio.onended = () => {
      isPlaying.value = false;
    };
    source.value = src;
  };
  // 暂停播放
  const pause = () => {
    if (currentAudio) {
      currentAudio.pause();
      isPlaying.value = false;
    }
  };
  // 停止播放(并清除资源)
  const stop = () => {
    if (currentAudio) {
      currentAudio.pause();
      currentAudio.currentTime = 0;
      currentAudio = null;
      isPlaying.value = false;
      source.value = null;
    }
  };
  return {
    source,
    isPlaying,
    play,
    pause,
    stop,
  };
}
src/store/bedsideAuxiliaryScreen.ts
@@ -134,14 +134,19 @@
          const dataBody = JSON.parse(datax) as SseMsgData;
          console.log("dataBody: ", dataBody);
          // 倒计时提示文本
          if (dataBody.倒计时?.提醒文本) {
            const taskTime = dayjs(dataBody.倒计时?.当前服务器时间).add(dataBody.倒计时?.设定提醒倒计时, 'minute')
          if (dataBody.倒计时?.提醒文本 && Number(dataBody.倒计时?.设定提醒倒计时 > 0)) {
            const serverTimeRaw = dataBody.倒计时?.当前服务器时间;
            const reminderMinutes = Number(dataBody.倒计时?.设定提醒倒计时 ?? 0);
            const serverTimeFormatted = serverTimeRaw.replace(' ', 'T');
            const taskTime = dayjs(serverTimeFormatted).add(reminderMinutes, 'second');
            setSyncTask({
              deviceCode: dataBody.IOT信息.设备唯一编号,
              recordCode: dataBody.透析状态?.透析单编号,
              taskDate: taskTime.format('YYYY-MM-DD HH:mm'),
              taskName: dataBody.倒计时?.提醒文本,
              overdue: false,
              sync: true,
              countdown: dataBody.倒计时?.设定提醒倒计时
            })
          } else {
src/store/type/task.type.ts
@@ -1,3 +1,11 @@
import alertbaojin from "@/assets/alert.wav";
import cxybaojing from "@/assets/cxy.mp3";
import gybaojing from "@/assets/gy.mp3";
import kclbaojing from "@/assets/kcl.mp3";
import tdddbaojing from "@/assets/tzddd.mp3";
import tzxllbaojing from "@/assets/tzxll.mp3";
import cgbaojing from "@/assets/cg.mp3";
export interface Task {
  /** 设备code */
  deviceCode: string;
@@ -9,6 +17,55 @@
  taskName: string;
  /** 是否过期 */
  overdue: boolean;
  /** 倒计时,如果存在该字段则表明是远程传过来的 */
  /** 是否远程传过来的, 只有远程传过来的时间到了再报*/
  sync: boolean;
  /** 倒计时,单位秒 */
  countdown?: number;
}
export interface TaskItem {
  label: string;
  value: string;
  backgroundColor: string;
  promptTone: string;
}
export const taskOptions : TaskItem[] = [
  {
    label: "测血压",
    value: "测血压",
    backgroundColor: "#E6A23C",
    promptTone: cxybaojing,
  },
  {
    label: "开超滤",
    value: "开超滤",
    backgroundColor: "#E6A23C",
    promptTone: kclbaojing,
  },
  {
    label: "给药",
    value: "给药",
    backgroundColor: "#E6A23C",
    promptTone: gybaojing,
  },
  {
    label: "调电导度",
    value: "调电导度",
    backgroundColor: "#E6A23C",
    promptTone: tdddbaojing,
  },
  {
    label: "调血流量",
    value: "调血流量",
    backgroundColor: "#E6A23C",
    promptTone: tzxllbaojing,
  },
  {
    label: "冲管",
    value: "冲管",
    backgroundColor: "#E6A23C",
    promptTone: cgbaojing,
  },
]
src/utils/httpApi.ts
@@ -59,3 +59,21 @@
        throw error;
    }
};
/**
 * 停止定时任务
 * @param deviceCode
 * @returns
 */
export const stopTimeoutAlert = async (deviceCode: string) => {
    try {
        const response = await axios.post(`${apiBaseUrl}/patient/hemo/med/record/stopTimeoutAlert`, { deviceCode }, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        });
        return response.data;
    } catch (error) {
        throw error;
    }
}
src/views/mobile/bedsideAuxiliaryScreen/components/Header.vue
@@ -2,7 +2,9 @@
  <div class="bedside-auxiliary-screen-header">
    <div class="header-left">
      <!-- 没有设备编号 -->
      <span v-if="pageType === pageTypeEnum.NOT_INIT" class="info-text">未绑定设备</span>
      <span v-if="pageType === pageTypeEnum.NOT_INIT" class="info-text"
        >未绑定设备</span
      >
      <template v-else>
        <!-- 设备号 -->
        <span class="info-text">{{
@@ -13,7 +15,11 @@
          >页面初始化中,请耐心等待!</span
        >
        <!-- 未排班 -->
        <span v-else-if="pageType === pageTypeEnum.UNPLANNED_SCHEDULE" class="info-text">当前尚未排班</span>
        <span
          v-else-if="pageType === pageTypeEnum.UNPLANNED_SCHEDULE"
          class="info-text"
          >当前尚未排班</span
        >
        <!-- 有排班 -->
        <template v-else>
          <span class="info-text">{{ patientInfo.patientName }}</span>
@@ -22,14 +28,30 @@
          <span v-if="patientInfo.patFormNumber" class="info-text">
            {{ patientInfo.patForm }}:{{ patientInfo.patFormNumber }}</span
          >
          <span v-if="pageType === pageTypeEnum.DURING_DIALYSIS && patientInfo.dialysisAge" class="info-text">
            透析龄: {{ patientInfo.dialysisAge?.years }}年{{ patientInfo.dialysisAge?.months }}月
          <span
            v-if="
              pageType === pageTypeEnum.DURING_DIALYSIS &&
              patientInfo.dialysisAge
            "
            class="info-text"
          >
            透析龄: {{ patientInfo.dialysisAge?.years }}年{{
              patientInfo.dialysisAge?.months
            }}月
          </span>
        </template>
        {{ taskCountdown }}
      </template>
    </div>
    <div class="header-right">
      <span
        v-if="
          bedsideAuxiliaryScreenStore.taskData &&
          bedsideAuxiliaryScreenStore.taskData.length > 0
        "
        class="countdown"
      >
        {{ formattedCountdown }}
      </span>
      <img
        :src="atRegularTimeImg"
        class="btn-img"
@@ -49,6 +71,8 @@
  <SettingDeviceDialog ref="settingDeviceDialogRef" />
  <!-- 定时任务组件 -->
  <ScheduledTaskDialog ref="scheduledTaskDialogRef" />
  <!-- 定时任务提醒组件 -->
  <TaskAlert ref="taskAlertRef" @close="taskAlaetClose" />
</template>
<script lang="ts" setup name="Header">
@@ -68,6 +92,8 @@
const ScheduledTaskDialog = defineAsyncComponent(
  () => import("./ScheduledTask.vue")
);
const TaskAlert = defineAsyncComponent(() => import("./TaskAlart.vue"));
import atRegularTimeImg from "../../../../img/dingshi.png";
import setUpImg from "../../../../img/shezhi.png";
import userImg from "../../../../img/user.png";
@@ -78,12 +104,15 @@
const bedsideAuxiliaryScreenStore = useBedsideAuxiliaryScreenStore();
let timer: number;
let timer: ReturnType<typeof setInterval> | null = null;
const pageTypeEnum = ref(EPageType);
const settingDeviceDialogRef = ref<any>(null);
const scheduledTaskDialogRef = ref<any>(null);
const taskCountdown = ref(""); // 定时任务倒计时文本
const taskAlertRef = ref<any>(null);
const countdown = ref(null); // 定时任务的倒计时
const isTaskAlartIsOpen = ref(false); // 定时任务的提醒弹框是否显示
const pageType = computed(() => {
  return bedsideAuxiliaryScreenStore.deviceData.pageType;
@@ -101,39 +130,100 @@
        ? "门诊号"
        : "住院号",
    patFormNumber: bedsideAuxiliaryScreenStore.deviceData.patFormNumber,
    dialysisAge: bedsideAuxiliaryScreenStore.deviceData.underTreatment.dialysisAge ? convertMonths(bedsideAuxiliaryScreenStore.deviceData.underTreatment.dialysisAge) : null ,
    dialysisAge: bedsideAuxiliaryScreenStore.deviceData.underTreatment
      .dialysisAge
      ? convertMonths(
          bedsideAuxiliaryScreenStore.deviceData.underTreatment.dialysisAge
        )
      : null,
  };
});
// watch(
//   () => bedsideAuxiliaryScreenStore.taskData,
//   (newData: Task[]) => {
//     console.log('定时任务更新了')
//     if (
//       bedsideAuxiliaryScreenStore.deviceData.deviceCode &&
//       newData.length > 0
//     ) {
//       console.log('newData: ', newData)
//       updateCountdown(newData[0].taskDate);
//     } else {
//       taskCountdown.value = "";
//     }
//   },
//   { deep: true }
// );
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`;
  }
});
watch(
  () => bedsideAuxiliaryScreenStore.taskData?.[0]?.countdown,
  (val) => {
    if (typeof val === "number") {
      startCountdown(val);
    } else {
      clearTimer();
    }
  },
  { immediate: true }
);
watch(countdown, (newVal) => {
  if (newVal <= 0 && !isTaskAlartIsOpen.value) {
    isTaskAlartIsOpen.value = true;
    // 弹窗逻辑,替换为你自己的弹窗组件或 UI 框
    showTaskAlart();
  }
});
// 清除定时器函数
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 convertMonths = (months: number): { years: number; months: number } => {
  const years = Math.floor(months / 12);
  const remainingMonths = months % 12;
  return { years, months: remainingMonths };
}
};
const showTaskAlart = () => {
  clearTimer();
  taskAlertRef.value.openDialog(
    bedsideAuxiliaryScreenStore.taskData?.[0]?.taskName
  );
};
const taskAlaetClose = () => {
  clearTimer();
  bedsideAuxiliaryScreenStore.clearTask();
  isTaskAlartIsOpen.value = false;
};
const openSettingDeviceDialog = () => {
  settingDeviceDialogRef.value?.openDialog();
};
const openScheduledTaskDialog = () => {
  if (!bedsideAuxiliaryScreenStore.deviceCode || !bedsideAuxiliaryScreenStore.deviceData.deviceCode) return ElMessage.warning('未初始化或正在进行初始化操作中');
  if (
    !bedsideAuxiliaryScreenStore.deviceCode ||
    !bedsideAuxiliaryScreenStore.deviceData.deviceCode
  )
    return ElMessage.warning("未初始化或正在进行初始化操作中");
  scheduledTaskDialogRef.value?.openDialog();
};
@@ -144,36 +234,8 @@
  });
};
const getCountdown = (taskDate: string) => {
  const now = dayjs();
  const target = dayjs(taskDate).second(0).millisecond(0);
  const diff = target.diff(now, "second");
  if (diff <= 0) return "";
  const minutes = Math.floor(diff / 60);
  const seconds = diff % 60;
  return `${minutes}m${seconds}s`;
};
const updateCountdown = (taskDate: string) => {
  taskCountdown.value = getCountdown(taskDate);
  timer = window.setInterval(updateCountdown, 1000);
};
onMounted(() => {
  if (
    bedsideAuxiliaryScreenStore.deviceData.deviceCode &&
    bedsideAuxiliaryScreenStore.taskData.length > 0
  ) {
    getCountdown(bedsideAuxiliaryScreenStore.taskData[0].taskDate);
  }
});
onUnmounted(() => {
  timer && clearInterval(timer);
  clearTimer();
});
</script>
@@ -218,6 +280,15 @@
      transition: all 0.2s;
    }
    .countdown {
      font-family: PingFangSC, PingFang SC;
      font-weight: 600;
      font-size: 9px;
      color: #bb3e3e;
      line-height: 15rpx;
      text-align: left;
      font-style: normal;
    }
  }
}
</style>
src/views/mobile/bedsideAuxiliaryScreen/components/ScheduledTask.vue
@@ -79,8 +79,10 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import dayjs from "dayjs";
import { setTimeoutAlert } from "@/utils/httpApi";
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";
@@ -95,12 +97,6 @@
import cgbaojing from "@/assets/cg.mp3";
import { ElMessage } from "element-plus";
interface TaskItem {
  label: string;
  value: string;
  backgroundColor: string;
  promptTone: string;
}
interface DateItem {
  label: string;
@@ -120,44 +116,6 @@
const loading = ref(false);
const taskOptions = ref<TaskItem[]>([
  {
    label: "测血压",
    value: "测血压",
    backgroundColor: "#E6A23C",
    promptTone: cxybaojing,
  },
  {
    label: "开超滤",
    value: "开超滤",
    backgroundColor: "#E6A23C",
    promptTone: kclbaojing,
  },
  {
    label: "给药",
    value: "给药",
    backgroundColor: "#E6A23C",
    promptTone: gybaojing,
  },
  {
    label: "调电导度",
    value: "调电导度",
    backgroundColor: "#E6A23C",
    promptTone: tdddbaojing,
  },
  {
    label: "调血流量",
    value: "调血流量",
    backgroundColor: "#E6A23C",
    promptTone: tzxllbaojing,
  },
  {
    label: "冲管",
    value: "冲管",
    backgroundColor: "#E6A23C",
    promptTone: cgbaojing,
  },
]);
const dateOptions = ref<DateItem[]>([
  { label: "15分钟", value: 15 },
@@ -167,7 +125,7 @@
]);
const taskItemCheck = computed(() => {
  return taskOptions.value.find((e) => e.value === taskName.value)?.value || "";
  return taskOptions.find((e) => e.value === taskName.value)?.value || "";
});
const openDialog = () => {
@@ -246,8 +204,14 @@
      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;
  }
src/views/mobile/bedsideAuxiliaryScreen/components/TaskAlart.vue
New file
@@ -0,0 +1,184 @@
<template>
  <div class="task-alert-container">
    <el-dialog
      v-model="show"
      title="任务提醒"
      width="60%"
      :show-close="false"
      class="task-alert-dialog"
      :destroy-on-close="true"
      :close-on-click-modal="false"
      center
    >
      <template #header>
        <div class="task-alert-header">
          <span class="header-title">任务提醒</span>
          <img
            :src="closeImg"
            class="header-close"
            @click="handleCancel"
            alt=""
          />
        </div>
      </template>
      <div class="task-alert-content">
        <span>{{ taskName }}</span>
      </div>
      <template #footer>
        <div class="my-button cancel" @click="handleCancel">关闭</div>
        <div class="my-button confirm" @click="handleCancel">确认</div>
      </template>
    </el-dialog>
  </div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import closeImg from "@/img/close.png";
import { useAudioPlayer } from "@/composables/useAudioPlayer";
import { taskOptions } from "@/store/type/task.type";
import alertbaojin from "@/assets/alert.wav";
const { play, stop } = useAudioPlayer();
const emit = defineEmits<{
  (e: "close"): void;
}>();
const show = ref(false);
const taskName = ref("");
const openDialog = (name: string) => {
  // 这里使用传值是为了防止sse任务结束后推送过来的数据没有
  taskName.value = name;
  show.value = true;
  const item = taskOptions.find((e) => e.value === name);
  if (item) {
    play(item.promptTone);
  } else {
    play(alertbaojin);
  }
};
const handleCancel = () => {
  show.value = false;
  stop();
  emit("close");
};
defineExpose({
  openDialog,
});
</script>
<style lang="less" scoped>
* {
  box-sizing: border-box;
}
.task-alert-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;
  }
  .task-alert-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);
      }
    }
  }
  .task-alert-content {
    height: 50px;
    max-height: 80px;
    font-size: 14px;
    color: #333;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    overflow-y: auto;
  }
  .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>
.task-alert-dialog {
  margin: 0 auto;
  top: 50% !important;
  transform: translateY(-50%) !important;
}
</style>