| src/composables/useAudioPlayer.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/store/bedsideAuxiliaryScreen.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/store/type/task.type.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/utils/httpApi.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mobile/bedsideAuxiliaryScreen/components/Header.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mobile/bedsideAuxiliaryScreen/components/ScheduledTask.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mobile/bedsideAuxiliaryScreen/components/TaskAlart.vue | ●●●●● 补丁 | 查看 | 原始文档 | 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>