| | |
| | | <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="220"> |
| | | <template #default="scope"> |
| | | {{scope.row.bookSeqNum}} {{ nameDesensitize(scope.row.patName) }} |
| | | {{scope.row.bookCheckType && checkTypeStore.getCheckTypeName(scope.row.bookCheckType)}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center" width="100"> |
| | | <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) + " "}} |
| | | </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> |