WXL
12 小时以前 d79ce11fcd47db138764557d214f4ea2169fb90a
src/views/ecg/callingscreen/roomscreen.vue
@@ -1,190 +1,771 @@
<script setup lang="ts">
import {RoomBedVO} from "@/api/ecg/doctor";
import {RoomApi} from "@/api/ecg/room";
import {ScreenQueueVO, ScreenApi} from "@/api/ecg/screen";
import {CallApi, CallVO} from "@/api/ecg/call";
import {queueStatusConvert} from "../../../utils/statusFormatter";
import {useCheckTypeStore} from "@/store/modules/checkType";
defineOptions({ name: 'roomscreen' })
const checkTypeStore = useCheckTypeStore()
const list = ref<ScreenQueueVO[]>([]) // 列表的数据
//const listPassed = ref<ScreenQueueVO[]>([]) // 过号列表的数据
let curSpeakPat : CallVO | null = null;
const roomBed = ref<RoomBedVO>({
  roomId : null,
  roomName: null,
  bedNo: null
})
const getList = async () => {
  const data = await ScreenApi.getRoomScreenData()
  list.value = data[1]
  // listPassed.value = data[2]
}
const getRoomByIp = async () => {
  const data = await RoomApi.getRoomByIP()
  roomBed.value = data
}
const startScrolling = () => {
  setInterval(() => {
    getList()
    if ( curSpeakPat === null ) {
      initiateSpeak()
    }
  }, 3000); // 每两秒滚动一次
}
const nameDesensitize = (patName) => {
  if (patName.length == 2) {
    //截取name 字符串截取第一个字符,
    return patName.substring(0, 1) + '*';
  } else if (patName.length == 3) {
    //截取第一个和第三个字符
    return patName.substring(0, 1) + '*' + patName.substring(2, 3);
  } else if (patName.length > 3) {
    //截取第一个和大于第4个字符
    return (
        patName.substring(0, 1) + '*' + '*' + patName.substring(3, patName.length)
    );
  }
}
const tableRowClassName = ({row, rowIndex}: {
  row: ScreenQueueVO
  rowIndex: number
}) => {
  if (row.status === 5) {
    return 'warning-row'
  } else if (row.status === 7) {
    return 'warning-row'  //return 'success-row'
  }
  return ''
}
const onSpeachEndEvent = async (event) => {
  console.log("Speech ended... " + event.currentTarget.text);
  curSpeakPat!.called = 1
  await CallApi.updateCall(curSpeakPat!)
  initiateSpeak()
}
const initiateSpeak = async () => {
  curSpeakPat = await CallApi.getNextInstallCall()
  if (curSpeakPat !== null) {
    speak("请、" + curSpeakPat.patName + "到" + curSpeakPat.roomName + "装机");
  }
}
const speak = (msg) => {
  console.info("speak " + msg);
  var speech = new SpeechSynthesisUtterance()
  speech.text = msg + "。。。" + msg + "。。。" + msg  + "。。。"
  speech.pitch = 1 // 获取并设置话语的音调(0-2 默认1,值越大越尖锐,越低越低沉)
  speech.rate = 0.9 // 获取并设置说话的速度(0.1-10 默认1,值越大语速越快,越小语速越慢)
  speech.volume = 100 // 获取并设置说话的音量
  speech.lang = 'zh-CN' // 设置播放语言
  speech.onend = onSpeachEndEvent
  speechSynthesis.speak(speech)
}
onMounted( () => {
  getRoomByIp()
  startScrolling()
})
</script>
<template>
  <el-container style="height: 100%;">
    <el-header style="font-size: 25px">{{ roomBed.roomName }}</el-header>
    <el-main>
      <el-table
          :data="list"
          :show-header="false"
          style="width: 100%; height: 100%; border: solid var(--el-color-primary-light-7); font-size: 18px;"
          :row-class-name="tableRowClassName"
      >
        <el-table-column
            prop="patName"
            label="预约序号"
            width="60px">
          <template #default="scope">
            <span style="display:inline-block; width:60px;">{{checkTypeStore.getCheckTypeSeqPrefix(scope.row.bookCheckType)}}{{scope.row.bookSeqNum}} </span>
          </template>
        </el-table-column>
        <el-table-column
            prop="patName"
            label="患者姓名"
            width="80px">
          <template #default="scope">
            <span style="display:inline-block; width:80px;">{{ nameDesensitize(scope.row.patName) }} </span>
          </template>
        </el-table-column>
        <el-table-column
            prop="patName"
            label="检查项目"
            width="80px">
          <template #default="scope">
            <span style="display:inline-block; width:80px;">{{scope.row.bookCheckType && checkTypeStore.getCheckTypeName(scope.row.bookCheckType)}} </span>
          </template>
        </el-table-column>
        <el-table-column label="状态" align="center" width="100px">
          <template #default="scope">
<!--            <dict-tag :type="DICT_TYPE.ECG_QUEUE_STATUS" :value="scope.row.status" />-->
            {{queueStatusConvert(scope.row.status)}}
          </template>
        </el-table-column>
      </el-table>
    </el-main>
    <el-button @click="speak('欢迎使用')" >欢迎使用</el-button>
<!--
    <el-footer height="100px" style="padding: 0 0">
          <el-header height="30px" style="background-color: #98b8e5; line-height: 30px;">过号区</el-header>
          <span v-for="(passedItem, index) in listPassed" :key="index">
                {{nameDesensitize(passedItem.patName) + "&nbsp;&nbsp;&nbsp;&nbsp;"}}
          </span>
    </el-footer>
-->
  </el-container>
  <div class="room-screen-container">
    <div class="search-bar">
      <input
        class="search-input"
        type="text"
        placeholder="请输入房间号查询"
        v-model="searchRoomInput"
        @keyup.enter="searchRoom"
      />
      <button class="search-btn" @click="searchRoom"> 查询 </button>
    </div>
    <div class="header">
      <div class="clinic-title">心电图诊间叫号系统</div>
      <div class="clinic-info">
        <div class="room-name">
          {{ roomProfile.roomName || '诊间加载中...' }}
        </div>
        <div class="screen-type"> 模式:{{ screenTypeText }} </div>
      </div>
    </div>
    <div class="main-content">
      <div class="panel" v-if="showCheckPanel">
        <div class="panel-header">检查队列</div>
        <div class="patient-list">
          <div v-if="checkRelatedPatientList.length === 0" class="empty-state">
            暂无等待检查的患者
          </div>
          <div
            v-for="(patient, index) in checkRelatedPatientList"
            :key="'check-' + index"
            class="patient-item"
            :class="getStatusClass(patient.status)"
          >
            <div class="patient-info">
              <div class="patient-number"> {{ getSeqPrefix(patient) }}{{ patient.bedNo }} </div>
              <div class="patient-name">
                {{ nameDesensitize(patient.patName) }}
              </div>
              <div class="patient-check-type">
                {{ getCheckTypeName(patient.bookCheckType) }}
              </div>
              <div class="patient-status" :class="'status-' + getStatusClass(patient.status)">
                {{ queueStatusConvert(patient.status) }}
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="panel" v-if="showInstallPanel">
        <div class="panel-header">装机队列</div>
        <div class="patient-list">
          <div v-if="installRelatedPatientList.length === 0" class="empty-state">
            暂无等待装机的患者
          </div>
          <div
            v-for="(patient, index) in installRelatedPatientList"
            :key="'install-' + index"
            class="patient-item"
            :class="getStatusClass(patient.status)"
          >
            <div class="patient-info">
              <div class="patient-number">
                {{ patient.bookSeqNum }}
              </div>
              <div class="patient-name">
                {{ nameDesensitize(patient.patName) }}
              </div>
              <div class="patient-check-type">
                {{ getCheckTypeName(patient.bookCheckType) }}
              </div>
              <div class="patient-status" :class="'status-' + getStatusClass(patient.status)">
                {{ queueStatusConvert(patient.status) }}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="footer">
      <div class="announcement">
        {{ announcementText }}
      </div>
      <div class="controls">
        <button class="control-btn pulse" @click="initiateSpeak">
          <i>📢</i>
          <span>叫号</span>
        </button>
        <button class="flex-1 control-btn" @click="speak('欢迎使用诊间叫号系统')">
          <i>🔊</i>
          <span>测试播音</span>
        </button>
        <button class="flex-1 control-btn" @click="changeRoom">
          <i>🔄</i>
          <span>切换诊间</span>
        </button>
      </div>
    </div>
  </div>
</template>
<style scoped lang="scss">
.el-header, .el-footer {
  background-color: var(--el-color-primary-light-7);
<script>
import { ref, computed, onBeforeUnmount } from 'vue'
import { ScreenApi } from '@/api/ecg/screen'
import { RoomApi } from '@/api/ecg/room'
import axios from 'axios'
export default {
  name: 'RoomScreen',
  setup() {
    // 响应式数据
    const roomProfile = ref({
      roomName: '心电图诊室 01',
      callingScreenType: 40
    })
    const checkRelatedPatientList = ref([])
    const installRelatedPatientList = ref([])
    const curSpeakPat = ref(null)
    const announcementText = ref('系统运行中...')
    const roomId = ref(1)
    const timer = ref(null)
    const speechSynthesis = ref(window.speechSynthesis || null)
    const searchRoomInput = ref('')
    // 计算属性
    const screenTypeText = computed(() => {
      const types = {
        10: '仅检查队列',
        20: '仅检查队列',
        30: '仅装机队列',
        40: '双队列模式',
        50: '双队列模式'
      }
      return types[roomProfile.value.callingScreenType] || '未知模式'
    })
    const showCheckPanel = computed(() => {
      return [10, 20, 40, 50].includes(roomProfile.value.callingScreenType)
    })
    const showInstallPanel = computed(() => {
      return [30, 40, 50].includes(roomProfile.value.callingScreenType)
    })
    // 方法
    const searchRoom = () => {
      if (!searchRoomInput.value.trim()) {
        announcementText.value = '请输入有效的房间号'
        return
      }
      RoomApi.getRoomByIP(roomId.value).then((response) => {
        roomProfile.value = response
        console.log(roomProfile.value.callingScreenType,'类型');
      })
      announcementText.value = `正在查询 ${searchRoomInput.value} 房间信息...`
      setTimeout(() => {
        const roomNum = parseInt(searchRoomInput.value) || 0
        // roomId.value = roomNum % 3
        getRoomByIp()
        announcementText.value = `已加载 ${roomProfile.value.roomName} 信息`
      }, 500)
    }
    const getRoomByIp = () => {
      setTimeout(() => {
        ScreenApi.getRoomScreenData(searchRoomInput.value)
          .then((response) => {
            console.log(response, '991')
            checkRelatedPatientList.value = response[1]
          })
          .catch((error) => {
            console.error('获取患者失败:', error)
          })
        // axios
        //   .get(`http://localhost:48080/admin-api/ecg/screen/room-screen-data`, {
        //     params: {
        //       roomId: searchRoomInput.value
        //     }
        //   })
        //   .then((response) => {
        //     const patient = response.data
        //     console.log(response.data, '55')
        //     if (patient && patient.called === 0) {
        //       curSpeakPat.value = patient
        //       speak(`请${patient.patName}到${roomProfile.value.roomName}装机`)
        //     }
        //   })
        //   .catch((error) => {
        //     console.error('获取下一位患者失败:', error)
        //   })
        const rooms = [
          { roomName: '心电图诊室 01', callingScreenType: 40 },
          { roomName: '动态心电图室', callingScreenType: 10 },
          { roomName: '运动试验室', callingScreenType: 30 }
        ]
        roomProfile.value = rooms[roomId.value % 3]
        announcementText.value = `已加载 ${roomProfile.value.roomName} 信息`
      }, 300)
    }
    const getList = () => {
      setTimeout(() => {
        ScreenApi.getRoomScreenData(searchRoomInput.value)
          .then((response) => {
            console.log(response, '199')
            checkRelatedPatientList.value = response[1]
          })
          .catch((error) => {
            console.error('获取患者失败:', error)
          })
        // axios
        //   .get(`http://localhost:48080/admin-api/ecg/screen/room-screen-data`, {
        //     params: {
        //       roomId: searchRoomInput.value
        //     }
        //   })
        //   .then((response) => {
        //     checkRelatedPatientList.value = response.data.data[1]
        //     console.log(response.data.data[1], '66')
        //   })
        //   .catch((error) => {
        //     console.error('获取下一位患者失败:', error)
        //   })
        if (!curSpeakPat.value && Math.random() > 0.7) {
          initiateSpeak()
        }
      }, 500)
    }
    const startScrolling = () => {
      getList()
      timer.value = setInterval(() => {
        getList()
      }, 5000)
    }
    const nameDesensitize = (patName) => {
      if (!patName) return ''
      if (patName.length === 2) {
        return patName.substring(0, 1) + '*'
      } else if (patName.length === 3) {
        return patName.substring(0, 1) + '*' + patName.substring(2, 3)
      } else if (patName.length > 3) {
        return patName.substring(0, 1) + '*' + '*' + patName.substring(3, patName.length)
      }
      return patName
    }
    const getStatusClass = (status) => {
      if (status === 10 || status === 40 || status === 33 || status === 20 || status === 10)
        return 'waiting'
      if (status === 30) return 'in-progress'
      if (status === 7 || status === 3 || status === 5) return 'completed'
      return ''
    }
    const queueStatusConvert = (status) => {
      const statusMap = {
        3: '已过号-排队',
        5: '已过号',
        7: '已过号-安装',
        10: '排队中',
        12: '亲和',
        13: '亲和-安装',
        15: '已召回',
        20: '候诊中',
        30: '就诊中',
        33: '已领用',
        34: '已召回-安装',
        36: '安装中',
        40: '已就诊'
      }
      return statusMap[status] || '未知状态'
    }
    const getCheckTypeName = (type) => {
      const types = {
        1: '常规心电图',
        2: '动态心电图',
        3: '运动试验',
        4: '心电监护'
      }
      return types[type] || '未知检查'
    }
    const getSeqPrefix = (patient) => {
      const types = {
        1: 'A001',
        2: 'A002',
        3: 'A003',
        4: 'A004'
      }
      return types[patient.bookCheckType] || ''
    }
    const initiateSpeak = () => {
      const waitingPatients = installRelatedPatientList.value.filter((p) => p.status === 5)
      if (waitingPatients.length === 0) {
        announcementText.value = '当前没有等待装机的患者'
        return
      }
      const patient = waitingPatients[0]
      curSpeakPat.value = {
        patName: patient.patName,
        roomName: roomProfile.value.roomName
      }
      speak('请' + patient.patName + '到' + roomProfile.value.roomName + '装机')
    }
    const speak = (msg) => {
      announcementText.value = '正在呼叫: ' + msg
      if (!speechSynthesis.value) {
        console.warn('当前浏览器不支持语音合成')
        return
      }
      speechSynthesis.value.cancel()
      const speech = new SpeechSynthesisUtterance()
      speech.text = msg + '。。。' + msg + '。。。' + msg
      speech.pitch = 1
      speech.rate = 0.9
      speech.volume = 1
      speech.lang = 'zh-CN'
      speechSynthesis.value.speak(speech)
    }
    const onSpeachEndEvent = (event) => {
      curSpeakPat.value = null
      announcementText.value = '系统运行中...'
    }
    const changeRoom = () => {
      roomId.value = (roomId.value + 1) % 3
      getRoomByIp()
      announcementText.value = '正在切换诊间...'
    }
    // 生命周期钩子
    onBeforeUnmount(() => {
      if (timer.value) {
        clearInterval(timer.value)
      }
    })
    // 初始化
    getRoomByIp()
    startScrolling()
    // 初始化语音合成
    if (speechSynthesis.value) {
      speechSynthesis.value.onend = onSpeachEndEvent
    }
    return {
      roomProfile,
      checkRelatedPatientList,
      installRelatedPatientList,
      curSpeakPat,
      announcementText,
      roomId,
      timer,
      speechSynthesis,
      searchRoomInput,
      screenTypeText,
      showCheckPanel,
      showInstallPanel,
      searchRoom,
      getRoomByIp,
      getList,
      startScrolling,
      nameDesensitize,
      getStatusClass,
      queueStatusConvert,
      getCheckTypeName,
      getSeqPrefix,
      initiateSpeak,
      speak,
      onSpeachEndEvent,
      changeRoom
    }
  }
}
</script>
<style scoped>
.room-screen-container {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: linear-gradient(135deg, #e6f0f8, #d9e4f0);
  color: #333;
  line-height: 1.5;
  overflow: hidden;
  padding: 10px;
}
.search-bar {
  background: rgba(255, 255, 255, 0.8);
  border-radius: 16px;
  padding: 12px 15px;
  margin-bottom: 10px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
}
.search-input {
  flex: 1;
  border: none;
  background: rgba(240, 244, 249, 0.7);
  border-radius: 12px;
  padding: 8px 12px;
  font-size: 0.9rem;
  outline: none;
  color: #4a5568;
}
.search-btn {
  margin-left: 8px;
  background: #5b8cff;
  color: white;
  border: none;
  border-radius: 12px;
  padding: 8px 15px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.3s;
}
.search-btn:hover {
  background: #3a7bff;
}
.header {
  background: rgba(255, 255, 255, 0.8);
  border-radius: 16px;
  padding: 12px 15px;
  margin-bottom: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  align-items: center;
}
.clinic-title {
  font-size: 1.4rem;
  font-weight: bold;
  text-align: center;
  line-height: 60px;
  margin-bottom: 5px;
  color: #4a7dff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.el-aside {
  background-color: var(--el-color-primary-light-7);
  color: #333;
.clinic-info {
  display: flex;
  justify-content: space-between;
  width: 100%;
  font-size: 0.9rem;
  color: #555;
}
.room-name {
  background: rgba(91, 140, 255, 0.1);
  padding: 4px 10px;
  border-radius: 20px;
  min-width: 120px;
  text-align: center;
  line-height: 200px;
  color: #4a7dff;
}
.el-main {
  background-color: var(--el-color-primary-light-7);
  color: #333;
  padding: 0 0;
.screen-type {
  background: rgba(91, 140, 255, 0.1);
  padding: 4px 10px;
  border-radius: 20px;
  color: #4a7dff;
}
.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow: hidden;
}
.panel {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 14px;
  padding: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  flex: 1;
  border: 1px solid rgba(0, 0, 0, 0.05);
}
.panel-header {
  background: linear-gradient(90deg, #a8c4ff, #c0d3ff);
  color: #2c3e50;
  padding: 8px 12px;
  border-radius: 8px;
  margin-bottom: 10px;
  font-size: 1rem;
  font-weight: bold;
  text-align: center;
  line-height: 160px;
}
.el-table ::v-deep .warning-row {
  --el-table-tr-bg-color: var(--el-color-warning-light-9);
}
.el-table ::v-deep .success-row {
  --el-table-tr-bg-color: var(--el-color-success-light-9);
.patient-list {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.patient-item {
  display: flex;
  align-items: center;
  padding: 10px 8px;
  border-bottom: 1px solid #eee;
  transition: all 0.3s;
}
.patient-item:last-child {
  border-bottom: none;
}
.patient-item.warning {
  background-color: #fdf6ec;
}
.patient-item.in-progress {
  background-color: #f0f9eb;
}
.patient-item.completed {
  background-color: #f4f4f5;
}
.patient-info {
  display: flex;
  flex: 1;
  min-width: 0;
}
.patient-number {
  width: 80px;
  font-weight: bold;
  color: #5b8cff;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.patient-name {
  width: 100px;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.patient-check-type {
  flex: 1;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.patient-status {
  width: 70px;
  font-size: 0.8rem;
  font-weight: bold;
  text-align: center;
  padding: 3px 8px;
  border-radius: 10px;
}
.status-waiting {
  background-color: #fdf6ec;
  color: #e6a23c;
}
.status-in-progress {
  background-color: #f0f9eb;
  color: #67c23a;
}
.status-completed {
  background-color: #f4f4f5;
  color: #909399;
}
.patient-bed {
  width: 60px;
  font-size: 0.85rem;
  text-align: right;
  color: #666;
}
.footer {
  background: rgba(255, 255, 255, 0.8);
  border-radius: 16px;
  padding: 12px 15px;
  margin-top: 10px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(0, 0, 0, 0.05);
}
.announcement {
  background: rgba(91, 140, 255, 0.1);
  padding: 8px 15px;
  border-radius: 20px;
  font-size: 0.9rem;
  text-align: center;
  margin-bottom: 12px;
  min-height: 20px;
  color: #4a7dff;
}
.controls {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.control-btn {
  padding: 8px 5px;
  background: rgba(91, 140, 255, 0.1);
  border: none;
  border-radius: 12px;
  color: #4a7dff;
  font-size: 0.85rem;
  cursor: pointer;
  transition: all 0.3s;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.control-btn i {
  font-size: 1.2rem;
  margin-bottom: 3px;
}
.control-btn:hover {
  background: rgba(91, 140, 255, 0.2);
}
.control-btn:active {
  transform: scale(0.95);
}
.empty-state {
  text-align: center;
  color: #999;
  padding: 20px;
  font-size: 0.9rem;
}
/* 滚动条样式 */
.patient-list::-webkit-scrollbar {
  width: 5px;
}
.patient-list::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}
.patient-list::-webkit-scrollbar-thumb {
  background: #c0c4cc;
  border-radius: 4px;
}
.patient-list::-webkit-scrollbar-thumb:hover {
  background: #909399;
}
/* 动画效果 */
@keyframes pulse {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
  100% {
    transform: scale(1);
  }
}
.pulse {
  animation: pulse 2s infinite;
}
/* 响应式调整 */
@media (max-width: 480px) {
  .header {
    padding: 10px 12px;
  }
  .clinic-title {
    font-size: 1.2rem;
  }
  .clinic-info {
    font-size: 0.8rem;
  }
  .panel {
    padding: 10px;
  }
  .panel-header {
    font-size: 0.9rem;
    padding: 6px 10px;
  }
  .patient-item {
    padding: 8px 6px;
  }
  .patient-number,
  .patient-name {
    width: 60px;
    font-size: 0.9rem;
  }
  .patient-status {
    width: 60px;
    font-size: 0.7rem;
  }
  .patient-bed {
    width: 50px;
  }
}
@media (max-height: 600px) {
  .header {
    padding: 8px 10px;
  }
  .panel {
    padding: 8px;
  }
  .patient-item {
    padding: 6px 4px;
  }
}
</style>