845 lines
20 KiB
Vue
845 lines
20 KiB
Vue
<template>
|
||
<div class="call-number-display" ref="screenContainer">
|
||
<!-- 头部区域 -->
|
||
<div class="header">
|
||
<h1>{{ departmentName }}</h1>
|
||
<div class="header-right">
|
||
<button class="fullscreen-btn" @click="toggleFullscreen">
|
||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||
</button>
|
||
<div class="time">{{ currentTime }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前呼叫区 -->
|
||
<div class="current-call">
|
||
<div class="call-box">
|
||
<div class="call-text">
|
||
请 <span class="highlight">{{ currentCall?.number || '-' }}</span> 号
|
||
<span class="highlight">{{ currentCall?.name || '-' }}</span>
|
||
到 <span class="highlight">{{ currentCall?.room || '-' }}</span> 诊室就诊
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 候诊信息区 -->
|
||
<div class="waiting-area">
|
||
<h2 class="section-title">候诊信息</h2>
|
||
<div class="table-container" ref="tableContainer">
|
||
<table class="waiting-table">
|
||
<thead style="position: sticky; top: 0; background: #f0f7ff; z-index: 1;">
|
||
<tr>
|
||
<th>序号</th>
|
||
<th>患者</th>
|
||
<th>诊室</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-for="doctorName in paginatedDoctors">
|
||
<template v-if="groupedPatients[doctorName]">
|
||
<!-- 医生分组标题 -->
|
||
<tr class="doctor-header" :key="`doctor-${doctorName}`">
|
||
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
||
</tr>
|
||
<!-- 患者列表 -->
|
||
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="`${doctorName}-${patient.id}`">
|
||
<td>{{ index + 1 }}</td>
|
||
<td>{{ patient.name }}</td>
|
||
<td>{{ getDoctorRoom(doctorName) }}</td>
|
||
<td :style="{ color: patient.status === 'CALLING' ? '#e74c3c' : '#27ae60' }">
|
||
{{ patient.status === 'CALLING' ? '就诊中' : '等待' }}
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页控制 -->
|
||
<div class="pagination-controls">
|
||
<button
|
||
id="prevPage"
|
||
@click="previousPage"
|
||
:disabled="currentPage === 1 || loading"
|
||
>上一页</button>
|
||
<span id="pageInfo">{{ currentPage }}/{{ totalPages }}</span>
|
||
<button
|
||
id="nextPage"
|
||
@click="nextPage"
|
||
:disabled="currentPage === totalPages || loading"
|
||
>下一页</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 辅助信息区 -->
|
||
<div class="info-bar">
|
||
<div class="info-item">
|
||
<span class="icon">⏱</span>
|
||
<span>当前时间: {{ currentTime }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="icon">🔢</span>
|
||
<span>当前号: {{ currentCall?.number || '-' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="icon">👥</span>
|
||
<span>等待人数: {{ waitingCount }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
||
import { storeToRefs } from 'pinia'
|
||
import { ElMessage } from 'element-plus'
|
||
import dayjs from 'dayjs'
|
||
import request from '@/utils/request'
|
||
import useUserStore from '@/store/modules/user'
|
||
|
||
// ========== 配置参数 ==========
|
||
const userStore = useUserStore()
|
||
const { orgId: userOrgId, tenantId: userTenantId } = storeToRefs(userStore)
|
||
|
||
// 从登录用户获取科室ID(避免硬编码;后端已确保 orgId 以字符串返回)
|
||
const ORGANIZATION_ID = computed(() => (userOrgId.value ? String(userOrgId.value) : ''))
|
||
const TENANT_ID = computed(() => (userTenantId.value ? Number(userTenantId.value) : 1))
|
||
const API_BASE_URL = '/triage/queue'
|
||
// SSE 地址(走后端 API 代理)
|
||
const SSE_URL = computed(() => {
|
||
const baseApi = import.meta.env.VITE_APP_BASE_API || ''
|
||
const orgId = ORGANIZATION_ID.value
|
||
const tenantId = TENANT_ID.value
|
||
return `${baseApi}${API_BASE_URL}/display/stream?organizationId=${encodeURIComponent(orgId)}&tenantId=${tenantId}`
|
||
})
|
||
|
||
// 响应式数据
|
||
const currentTime = ref('')
|
||
const currentCall = ref(null)
|
||
const patients = ref([])
|
||
const loading = ref(false)
|
||
const currentPage = ref(1)
|
||
const patientsPerPage = 5
|
||
const autoScrollInterval = ref(null)
|
||
const scrollInterval = 5000 // 5秒自动翻页
|
||
const sseConnection = ref(null) // SSE 连接
|
||
const timeInterval = ref(null)
|
||
const isFullscreen = ref(false)
|
||
const screenContainer = ref(null)
|
||
let tableContainer = null
|
||
|
||
// 科室名称
|
||
const departmentName = ref('叫号显示屏幕')
|
||
|
||
const applyDefaultDepartmentName = () => {
|
||
if (userStore.orgName) {
|
||
departmentName.value = `${userStore.orgName} 叫号显示屏`
|
||
}
|
||
}
|
||
|
||
// 等待总人数(从后端返回)
|
||
const waitingCount = ref(0)
|
||
|
||
// 计算属性:按医生分组的患者列表
|
||
const groupedPatients = computed(() => {
|
||
const grouped = {}
|
||
patients.value.forEach(doctorGroup => {
|
||
grouped[doctorGroup.doctorName] = doctorGroup.patients || []
|
||
})
|
||
return grouped
|
||
})
|
||
|
||
// 获取排序后的医生列表
|
||
const sortedDoctors = computed(() => {
|
||
return patients.value.map(group => group.doctorName)
|
||
})
|
||
|
||
// 按医生分组的分页逻辑
|
||
const paginatedDoctors = computed(() => {
|
||
const startIndex = (currentPage.value - 1) * 1 // 每页显示1个医生组
|
||
const endIndex = startIndex + 1
|
||
return sortedDoctors.value.slice(startIndex, endIndex)
|
||
})
|
||
|
||
// 获取当前页的患者
|
||
const currentPatients = computed(() => {
|
||
const result = {}
|
||
paginatedDoctors.value.forEach(doctor => {
|
||
result[doctor] = groupedPatients.value[doctor]
|
||
})
|
||
return result
|
||
})
|
||
|
||
const totalPages = computed(() => {
|
||
return Math.ceil(sortedDoctors.value.length) || 1
|
||
})
|
||
|
||
// 方法
|
||
const updateTime = () => {
|
||
const now = dayjs()
|
||
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const updateFullscreenState = () => {
|
||
const isActive = !!document.fullscreenElement
|
||
isFullscreen.value = isActive
|
||
document.body.classList.toggle('call-screen-fullscreen', isActive)
|
||
}
|
||
|
||
const toggleFullscreen = async () => {
|
||
try {
|
||
if (document.fullscreenElement) {
|
||
await document.exitFullscreen()
|
||
} else if (screenContainer.value && screenContainer.value.requestFullscreen) {
|
||
await screenContainer.value.requestFullscreen()
|
||
}
|
||
} catch (error) {
|
||
console.error('切换全屏失败:', error)
|
||
}
|
||
}
|
||
|
||
const formatPatientName = (name) => {
|
||
if (!name || typeof name !== 'string') return '-'
|
||
if (name.length === 0) return '-'
|
||
return name.charAt(0) + '*' + name.slice(-1)
|
||
}
|
||
|
||
const getDoctorRoom = (doctorName) => {
|
||
// 从后端数据中查找医生的诊室号
|
||
const doctorGroup = patients.value.find(group => group.doctorName === doctorName)
|
||
return doctorGroup?.roomNo || '1号'
|
||
}
|
||
|
||
const ensureUserInfo = async () => {
|
||
if (!userStore.orgId) {
|
||
try {
|
||
await userStore.getInfo()
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error)
|
||
}
|
||
}
|
||
applyDefaultDepartmentName()
|
||
}
|
||
|
||
/**
|
||
* 获取显示屏数据(从后端API)
|
||
*/
|
||
const fetchDisplayData = async () => {
|
||
try {
|
||
if (!ORGANIZATION_ID.value) {
|
||
ElMessage.warning('未获取到登录用户科室信息')
|
||
return
|
||
}
|
||
loading.value = true
|
||
|
||
console.log('正在获取显示屏数据...', {
|
||
url: `${API_BASE_URL}/display`,
|
||
organizationId: ORGANIZATION_ID.value,
|
||
tenantId: TENANT_ID.value
|
||
})
|
||
|
||
const response = await request({
|
||
url: `${API_BASE_URL}/display`,
|
||
method: 'get',
|
||
params: {
|
||
organizationId: ORGANIZATION_ID.value,
|
||
tenantId: TENANT_ID.value
|
||
}
|
||
})
|
||
|
||
console.log('后端响应:', response)
|
||
|
||
if (response.code === 200 && response.data) {
|
||
const data = response.data
|
||
|
||
// 更新科室名称
|
||
if (data.departmentName && data.departmentName !== '叫号显示屏') {
|
||
departmentName.value = data.departmentName
|
||
} else {
|
||
applyDefaultDepartmentName()
|
||
}
|
||
|
||
// 更新当前叫号信息
|
||
if (data.currentCall) {
|
||
currentCall.value = data.currentCall
|
||
} else {
|
||
currentCall.value = {
|
||
number: null,
|
||
name: '-',
|
||
room: '-',
|
||
doctor: '-'
|
||
}
|
||
}
|
||
|
||
// 更新等候队列(按医生分组)
|
||
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||
patients.value = data.waitingList
|
||
console.log('等候队列数据:', data.waitingList)
|
||
} else {
|
||
patients.value = []
|
||
console.log('等候队列为空')
|
||
}
|
||
|
||
// 更新等待人数
|
||
waitingCount.value = data.waitingCount || 0
|
||
|
||
console.log('显示屏数据更新成功', data)
|
||
ElMessage.success('数据加载成功')
|
||
} else {
|
||
throw new Error(response.msg || '获取数据失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取显示屏数据失败:', error)
|
||
ElMessage.error('获取显示屏数据失败:' + (error.message || '未知错误'))
|
||
|
||
// 出错时设置默认值
|
||
patients.value = []
|
||
currentCall.value = { number: null, name: '-', room: '-', doctor: '-' }
|
||
waitingCount.value = 0
|
||
applyDefaultDepartmentName()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const previousPage = () => {
|
||
if (currentPage.value > 1) {
|
||
currentPage.value--
|
||
scrollToTop()
|
||
}
|
||
}
|
||
|
||
const nextPage = () => {
|
||
if (currentPage.value < totalPages.value) {
|
||
currentPage.value++
|
||
scrollToTop()
|
||
}
|
||
}
|
||
|
||
const scrollToTop = () => {
|
||
nextTick(() => {
|
||
const container = document.querySelector('.table-container')
|
||
if (container && container.scrollTo) {
|
||
container.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const startAutoScroll = () => {
|
||
stopAutoScroll()
|
||
autoScrollInterval.value = setInterval(() => {
|
||
if (currentPage.value < totalPages.value) {
|
||
currentPage.value++
|
||
} else {
|
||
currentPage.value = 1
|
||
}
|
||
scrollToTop()
|
||
}, scrollInterval)
|
||
}
|
||
|
||
const stopAutoScroll = () => {
|
||
if (autoScrollInterval.value) {
|
||
clearInterval(autoScrollInterval.value)
|
||
autoScrollInterval.value = null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化 SSE 连接
|
||
*/
|
||
const initSse = () => {
|
||
try {
|
||
if (!ORGANIZATION_ID.value) {
|
||
console.warn('未获取到科室ID,跳过 SSE 连接')
|
||
return
|
||
}
|
||
if (sseConnection.value) {
|
||
sseConnection.value.close()
|
||
}
|
||
console.log('正在连接 SSE:', SSE_URL.value)
|
||
sseConnection.value = new EventSource(SSE_URL.value)
|
||
|
||
sseConnection.value.onopen = () => {
|
||
console.log('SSE 连接成功')
|
||
ElMessage.success('实时连接已建立')
|
||
}
|
||
|
||
sseConnection.value.onmessage = (event) => {
|
||
try {
|
||
const message = JSON.parse(event.data)
|
||
console.log('收到 SSE 消息:', message)
|
||
|
||
if (message.type === 'init') {
|
||
handleSseUpdate(message.data)
|
||
} else if (message.type === 'update') {
|
||
handleSseUpdate(message.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('解析 SSE 消息失败:', error)
|
||
}
|
||
}
|
||
|
||
sseConnection.value.onerror = (error) => {
|
||
console.error('SSE 错误:', error)
|
||
ElMessage.error('实时连接出现错误')
|
||
}
|
||
} catch (error) {
|
||
console.error('初始化 SSE 失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 SSE 推送的更新数据
|
||
*/
|
||
const handleSseUpdate = (data) => {
|
||
if (!data) return
|
||
|
||
// 更新科室名称
|
||
if (data.departmentName) {
|
||
departmentName.value = data.departmentName
|
||
}
|
||
|
||
// 更新当前叫号信息
|
||
if (data.currentCall) {
|
||
currentCall.value = data.currentCall
|
||
}
|
||
|
||
// 更新等候队列
|
||
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||
patients.value = data.waitingList
|
||
}
|
||
|
||
// 更新等待人数
|
||
if (data.waitingCount !== undefined) {
|
||
waitingCount.value = data.waitingCount
|
||
}
|
||
|
||
console.log('显示屏数据已更新(来自 SSE)')
|
||
|
||
// 播放语音(如果有新的叫号)
|
||
if (data.currentCall && data.currentCall.number) {
|
||
playVoiceNotification(data.currentCall)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 播放语音通知
|
||
*/
|
||
const playVoiceNotification = (callInfo) => {
|
||
if (!callInfo || !callInfo.number) return
|
||
|
||
try {
|
||
// 使用 Web Speech API 播放语音
|
||
const utterance = new SpeechSynthesisUtterance(
|
||
`请${callInfo.number}号${callInfo.name}到${callInfo.room}诊室就诊`
|
||
)
|
||
utterance.lang = 'zh-CN'
|
||
utterance.rate = 0.9 // 语速
|
||
utterance.pitch = 1.0 // 音调
|
||
utterance.volume = 1.0 // 音量
|
||
|
||
window.speechSynthesis.speak(utterance)
|
||
} catch (error) {
|
||
console.error('语音播放失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭 SSE 连接
|
||
*/
|
||
const closeSse = () => {
|
||
if (sseConnection.value) {
|
||
sseConnection.value.close()
|
||
sseConnection.value = null
|
||
console.log('SSE 连接已关闭')
|
||
}
|
||
}
|
||
|
||
// 生命周期钩子
|
||
onMounted(async () => {
|
||
document.addEventListener('fullscreenchange', updateFullscreenState)
|
||
await ensureUserInfo()
|
||
// 初始化时间
|
||
updateTime()
|
||
// 每分钟更新时间
|
||
timeInterval.value = setInterval(updateTime, 60000)
|
||
|
||
// ✅ 获取初始数据(从后端 API)
|
||
await fetchDisplayData()
|
||
|
||
// ✅ 初始化 SSE 连接(实时推送)
|
||
initSse()
|
||
|
||
// 启动自动滚动
|
||
startAutoScroll()
|
||
|
||
// 鼠标悬停时暂停自动滚动
|
||
tableContainer = document.querySelector('.table-container')
|
||
if (tableContainer) {
|
||
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
||
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 组件卸载时的清理工作
|
||
if (timeInterval.value) {
|
||
clearInterval(timeInterval.value)
|
||
timeInterval.value = null
|
||
}
|
||
stopAutoScroll()
|
||
closeSse() // ✅ 关闭 SSE 连接
|
||
if (tableContainer) {
|
||
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
||
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
||
tableContainer = null
|
||
}
|
||
document.removeEventListener('fullscreenchange', updateFullscreenState)
|
||
document.body.classList.remove('call-screen-fullscreen')
|
||
})
|
||
|
||
// 监听页面变化,重置滚动位置
|
||
watchEffect(() => {
|
||
scrollToTop()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.call-number-display {
|
||
width: 100%;
|
||
max-width: 1200px;
|
||
background-color: #fff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.05);
|
||
overflow: hidden;
|
||
border: 1px solid #eaeaea;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:global(body.call-screen-fullscreen) {
|
||
overflow: hidden;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
:global(body.call-screen-fullscreen .sidebar-wrapper),
|
||
:global(body.call-screen-fullscreen .navbar),
|
||
:global(body.call-screen-fullscreen .tags-view-container),
|
||
:global(body.call-screen-fullscreen #tags-view-container),
|
||
:global(body.call-screen-fullscreen .drawer-bg) {
|
||
display: none !important;
|
||
}
|
||
|
||
:global(body.call-screen-fullscreen .app-wrapper),
|
||
:global(body.call-screen-fullscreen .main-wrapper),
|
||
:global(body.call-screen-fullscreen .content-wrapper),
|
||
:global(body.call-screen-fullscreen .app-main) {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
:global(body.call-screen-fullscreen .app-wrapper) {
|
||
height: 100vh !important;
|
||
}
|
||
|
||
:global(body.call-screen-fullscreen .call-number-display) {
|
||
max-width: none;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
min-height: 100vh;
|
||
margin: 0;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* 头部样式 */
|
||
.header {
|
||
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
||
color: white;
|
||
padding: 20px 30px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-radius: 12px;
|
||
margin-bottom: 20px;
|
||
|
||
h1 {
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
letter-spacing: 1px;
|
||
margin: 0;
|
||
}
|
||
|
||
.time {
|
||
font-size: 1.5rem;
|
||
font-weight: 500;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 5px 15px;
|
||
border-radius: 30px;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.fullscreen-btn {
|
||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||
background: rgba(255, 255, 255, 0.15);
|
||
color: #fff;
|
||
padding: 6px 14px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.fullscreen-btn:hover {
|
||
background: rgba(255, 255, 255, 0.28);
|
||
border-color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
}
|
||
|
||
/* 当前呼叫区 */
|
||
.current-call {
|
||
text-align: center;
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
margin-bottom: 20px;
|
||
|
||
.call-box {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 5px 20px rgba(74, 144, 226, 0.15);
|
||
border: 1px solid #e0e7ff;
|
||
animation: pulse 2s infinite;
|
||
|
||
.call-text {
|
||
font-size: 2.2rem;
|
||
font-weight: 700;
|
||
color: #4a90e2;
|
||
letter-spacing: 2px;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 候诊信息区 */
|
||
.waiting-area {
|
||
flex: 1;
|
||
padding: 0;
|
||
margin-bottom: 20px;
|
||
|
||
.section-title {
|
||
font-size: 1.4rem;
|
||
color: #555;
|
||
margin-bottom: 20px;
|
||
padding-left: 10px;
|
||
border-left: 4px solid #4a90e2;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.table-container {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
scroll-behavior: smooth;
|
||
border-radius: 10px;
|
||
border: 1px solid #eaeaea;
|
||
}
|
||
|
||
.waiting-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: white;
|
||
|
||
th,
|
||
.doctor-header td {
|
||
background-color: #f0f7ff;
|
||
color: #4a90e2;
|
||
font-weight: 600;
|
||
text-align: left;
|
||
padding: 15px 20px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
td {
|
||
padding: 15px 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
tr:nth-child(even) {
|
||
background-color: #fafcff;
|
||
}
|
||
|
||
tr:hover {
|
||
background-color: #f0f7ff;
|
||
}
|
||
|
||
.doctor-header {
|
||
background-color: #f0f7ff !important;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.pagination-controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 15px;
|
||
gap: 10px;
|
||
|
||
button {
|
||
padding: 8px 16px;
|
||
border: 1px solid #eaeaea;
|
||
border-radius: 4px;
|
||
background: #f8f9fa;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: #e9ecef;
|
||
border-color: #4a90e2;
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
span {
|
||
padding: 8px 16px;
|
||
background: #f8f9fa;
|
||
border-radius: 4px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 辅助信息区 */
|
||
.info-bar {
|
||
background: #2c3e50;
|
||
color: white;
|
||
padding: 15px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
border-radius: 12px;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 1.1rem;
|
||
|
||
.icon {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
span {
|
||
margin-left: 10px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 动画效果 */
|
||
@keyframes pulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.4);
|
||
}
|
||
70% {
|
||
box-shadow: 0 0 0 15px rgba(74, 144, 226, 0);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0);
|
||
}
|
||
}
|
||
|
||
.highlight {
|
||
color: #e74c3c;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.call-number-display {
|
||
padding: 10px;
|
||
margin: 0;
|
||
}
|
||
|
||
.header {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
gap: 10px;
|
||
padding: 15px 20px;
|
||
|
||
h1 {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.time {
|
||
font-size: 1.2rem;
|
||
}
|
||
}
|
||
|
||
.current-call {
|
||
padding: 15px;
|
||
|
||
.call-box {
|
||
padding: 15px;
|
||
|
||
.call-text {
|
||
font-size: 1.8rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-bar {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
text-align: center;
|
||
|
||
.info-item {
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.waiting-table {
|
||
th, td {
|
||
padding: 10px 15px;
|
||
font-size: 0.9rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.header h1 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.current-call .call-box .call-text {
|
||
font-size: 1.5rem;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.waiting-table th,
|
||
.waiting-table td {
|
||
padding: 8px 12px;
|
||
font-size: 0.8rem;
|
||
}
|
||
}
|
||
</style> |