|
|
@@ -1,1094 +0,0 @@
|
|
|
-<!-- 紧急电话或广播管理-->
|
|
|
-<template>
|
|
|
- <div class="prototype-container">
|
|
|
- <!-- 头部 -->
|
|
|
- <div class="header">
|
|
|
- <div class="logo">
|
|
|
- <h2>📢 紧急电话与广播路侧分机监控系统</h2>
|
|
|
- <p>设备状态实时监控 · 分区/全线广播 · 应急通信管理</p>
|
|
|
- </div>
|
|
|
- <div class="header-stats">
|
|
|
- <div class="stat-item">
|
|
|
- <span class="stat-value">{{ onlineCount }}</span>
|
|
|
- <span class="stat-label">设备在线</span>
|
|
|
- </div>
|
|
|
- <div class="stat-item warning">
|
|
|
- <span class="stat-value">{{ alarmCount }}</span>
|
|
|
- <span class="stat-label">报警/故障</span>
|
|
|
- </div>
|
|
|
- <div class="stat-item">
|
|
|
- <span class="stat-value">{{ activeCallCount }}</span>
|
|
|
- <span class="stat-label">通话中</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="main-layout">
|
|
|
- <!-- 左侧:设备图元监控面板 -->
|
|
|
- <div class="left-panel">
|
|
|
- <div class="panel-title">
|
|
|
- 📡 路侧分机设备状态
|
|
|
- <span class="live-badge">● 实时监控</span>
|
|
|
- </div>
|
|
|
- <div class="device-grid">
|
|
|
- <div
|
|
|
- v-for="device in devices"
|
|
|
- :key="device.id"
|
|
|
- class="device-card"
|
|
|
- :class="{
|
|
|
- 'status-online': device.status === 'online',
|
|
|
- 'status-calling': device.status === 'calling',
|
|
|
- 'status-broadcast': device.status === 'broadcast',
|
|
|
- 'status-fault': device.status === 'fault',
|
|
|
- 'status-alarm': device.status === 'alarm'
|
|
|
- }"
|
|
|
- @click="showDeviceDetail(device)"
|
|
|
- >
|
|
|
- <div class="device-icon" :class="{ 'pulse-animation': device.status === 'alarm' || device.status === 'calling' }">
|
|
|
- <span v-if="device.type === 'emergency'">📞</span>
|
|
|
- <span v-else>📢</span>
|
|
|
- </div>
|
|
|
- <div class="device-info">
|
|
|
- <div class="device-name">{{ device.name }}</div>
|
|
|
- <div class="device-location">{{ device.location }}</div>
|
|
|
- <div class="device-status">
|
|
|
- <span class="status-dot" :class="getStatusDotClass(device.status)"></span>
|
|
|
- {{ getStatusText(device.status) }}
|
|
|
- </div>
|
|
|
- <div class="device-call-info" v-if="device.status === 'calling' || device.status === 'alarm'">
|
|
|
- <div class="call-time" v-if="device.callTime">呼叫时间: {{ device.callTime }}</div>
|
|
|
- <div class="answer-time" v-if="device.answerTime">接警时间: {{ device.answerTime }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="device-actions">
|
|
|
- <button v-if="device.status === 'calling'" @click.stop="answerCall(device)" class="btn-answer">接听</button>
|
|
|
- <button v-if="device.status === 'calling'" @click.stop="rejectCall(device)" class="btn-reject">拒接</button>
|
|
|
- <button @click.stop="startBroadcast(device)" class="btn-broadcast">📢 广播</button>
|
|
|
- </div>
|
|
|
- <div v-if="device.status === 'alarm' || device.status === 'fault'" class="alarm-wave">
|
|
|
- <div class="wave"></div><div class="wave"></div><div class="wave"></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧:广播控制与呼叫记录 -->
|
|
|
- <div class="right-panel">
|
|
|
- <!-- 广播控制面板 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 🎙️ 广播控制系统
|
|
|
- <span class="broadcast-status" :class="{ active: isBroadcasting }">
|
|
|
- {{ isBroadcasting ? '广播中' : '待机' }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <!-- 分区选择 -->
|
|
|
- <div class="zone-section">
|
|
|
- <div class="section-title">选择广播分区</div>
|
|
|
- <div class="zone-buttons">
|
|
|
- <button
|
|
|
- v-for="zone in zones"
|
|
|
- :key="zone.id"
|
|
|
- class="zone-btn"
|
|
|
- :class="{ active: selectedZones.includes(zone.id) }"
|
|
|
- @click="toggleZone(zone.id)"
|
|
|
- >
|
|
|
- {{ zone.name }}
|
|
|
- </button>
|
|
|
- <button class="zone-btn select-all" @click="selectAllZones">全选</button>
|
|
|
- <button class="zone-btn clear-all" @click="clearAllZones">清空</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 广播模式选择 -->
|
|
|
- <div class="mode-section">
|
|
|
- <div class="section-title">广播模式</div>
|
|
|
- <div class="mode-buttons">
|
|
|
- <button class="mode-btn" :class="{ active: broadcastMode === 'zone' }" @click="broadcastMode = 'zone'">
|
|
|
- 🎯 分区广播
|
|
|
- </button>
|
|
|
- <button class="mode-btn" :class="{ active: broadcastMode === 'all' }" @click="broadcastMode = 'all'">
|
|
|
- 🌐 全线广播
|
|
|
- </button>
|
|
|
- <button class="mode-btn" :class="{ active: broadcastMode === 'background' }" @click="broadcastMode = 'background'">
|
|
|
- 🎵 背景音乐
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 广播内容 -->
|
|
|
- <div class="broadcast-section">
|
|
|
- <div class="section-title">广播内容</div>
|
|
|
- <div class="broadcast-input-area">
|
|
|
- <textarea
|
|
|
- v-model="broadcastText"
|
|
|
- class="broadcast-input"
|
|
|
- placeholder="输入广播内容或选择音频文件..."
|
|
|
- rows="2"
|
|
|
- ></textarea>
|
|
|
- <div class="file-upload-area">
|
|
|
- <label class="upload-btn">
|
|
|
- 📁 上传音频文件
|
|
|
- <input type="file" accept="audio/*" @change="handleFileUpload" hidden>
|
|
|
- </label>
|
|
|
- <span v-if="uploadedFileName" class="file-name">{{ uploadedFileName }}</span>
|
|
|
- <button @click="playPreview" class="preview-btn" :disabled="!uploadedFileName && !broadcastText">▶ 试听</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 广播控制按钮 -->
|
|
|
- <div class="control-buttons">
|
|
|
- <button @click="startBroadcastAll" class="btn-broadcast-start" :disabled="!canBroadcast">
|
|
|
- 📢 开始广播
|
|
|
- </button>
|
|
|
- <button @click="stopBroadcast" class="btn-broadcast-stop" :disabled="!isBroadcasting">
|
|
|
- ⏹️ 停止广播
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 快捷应急广播 -->
|
|
|
- <div class="emergency-section">
|
|
|
- <div class="section-title">应急广播快捷指令</div>
|
|
|
- <div class="emergency-buttons">
|
|
|
- <button @click="emergencyBroadcast('火灾警报')" class="emergency-btn fire">🔥 火灾警报</button>
|
|
|
- <button @click="emergencyBroadcast('泄漏事故')" class="emergency-btn leak">💧 泄漏事故</button>
|
|
|
- <button @click="emergencyBroadcast('紧急疏散')" class="emergency-btn evacuate">🚨 紧急疏散</button>
|
|
|
- <button @click="emergencyBroadcast('天气预警')" class="emergency-btn weather">🌧️ 天气预警</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 呼叫记录与事件日志 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 📋 呼叫记录与接警日志
|
|
|
- <button @click="clearLogs" class="btn-clear">清空</button>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="call-log-list">
|
|
|
- <div v-for="log in callLogs" :key="log.id" class="log-item" :class="log.type">
|
|
|
- <div class="log-time">{{ log.time }}</div>
|
|
|
- <div class="log-device">{{ log.deviceName }}</div>
|
|
|
- <div class="log-location">{{ log.location }}</div>
|
|
|
- <div class="log-action">{{ log.action }}</div>
|
|
|
- <div class="log-duration" v-if="log.duration">{{ log.duration }}</div>
|
|
|
- </div>
|
|
|
- <div v-if="callLogs.length === 0" class="empty-log">暂无呼叫记录</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 设备详情模态框 -->
|
|
|
- <div v-if="detailModalVisible" class="modal-overlay" @click.self="closeDetailModal">
|
|
|
- <div class="modal-content">
|
|
|
- <h3>📟 设备详细信息</h3>
|
|
|
- <div class="detail-row"><span class="detail-label">设备名称:</span>{{ currentDevice?.name }}</div>
|
|
|
- <div class="detail-row"><span class="detail-label">设备ID:</span>{{ currentDevice?.deviceId }}</div>
|
|
|
- <div class="detail-row"><span class="detail-label">安装位置:</span>{{ currentDevice?.location }}</div>
|
|
|
- <div class="detail-row"><span class="detail-label">设备类型:</span>{{ currentDevice?.type === 'emergency' ? '紧急电话' : '广播分机' }}</div>
|
|
|
- <div class="detail-row"><span class="detail-label">工作状态:</span>{{ getStatusText(currentDevice?.status) }}</div>
|
|
|
- <div class="detail-row" v-if="currentDevice?.callTime"><span class="detail-label">呼叫时间:</span>{{ currentDevice?.callTime }}</div>
|
|
|
- <div class="detail-row" v-if="currentDevice?.answerTime"><span class="detail-label">接警时间:</span>{{ currentDevice?.answerTime }}</div>
|
|
|
- <div class="detail-row"><span class="detail-label">最后在线:</span>{{ currentDevice?.lastOnline || '-' }}</div>
|
|
|
- <button @click="closeDetailModal" class="btn-primary" style="margin-top:16px;">关闭</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 隐藏音频播放器 -->
|
|
|
- <audio ref="audioPlayer" style="display: none;"></audio>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
-
|
|
|
-// ==================== 设备数据 ====================
|
|
|
-const devices = ref([
|
|
|
- { id: 1, name: "紧急电话-001", deviceId: "ET-001", type: "emergency", location: "K2+300 东侧", zone: "zone_a", status: "online", lastOnline: "2025-05-08 14:23:12" },
|
|
|
- { id: 2, name: "紧急电话-002", deviceId: "ET-002", type: "emergency", location: "K5+600 南侧", zone: "zone_a", status: "online", lastOnline: "2025-05-08 14:22:45" },
|
|
|
- { id: 3, name: "广播分机-001", deviceId: "SP-001", type: "broadcast", location: "服务区东区", zone: "zone_b", status: "online", lastOnline: "2025-05-08 14:23:00" },
|
|
|
- { id: 4, name: "紧急电话-003", deviceId: "ET-003", type: "emergency", location: "K12+800 隧道口", zone: "zone_b", status: "calling", callTime: "14:22:30", answerTime: null, lastOnline: "2025-05-08 14:22:30" },
|
|
|
- { id: 5, name: "广播分机-002", deviceId: "SP-002", type: "broadcast", location: "收费站广场", zone: "zone_c", status: "broadcast", lastOnline: "2025-05-08 14:20:00" },
|
|
|
- { id: 6, name: "紧急电话-004", deviceId: "ET-004", type: "emergency", location: "K20+500 西侧", zone: "zone_c", status: "alarm", callTime: "14:15:20", answerTime: "14:16:45", lastOnline: "2025-05-08 14:15:20" },
|
|
|
- { id: 7, name: "广播分机-003", deviceId: "SP-003", type: "broadcast", location: "观景台", zone: "zone_d", status: "fault", lastOnline: "2025-05-08 13:50:22" },
|
|
|
- { id: 8, name: "紧急电话-005", deviceId: "ET-005", type: "emergency", location: "K25+100 北侧", zone: "zone_d", status: "online", lastOnline: "2025-05-08 14:21:33" }
|
|
|
-])
|
|
|
-
|
|
|
-// 分区定义
|
|
|
-const zones = ref([
|
|
|
- { id: "zone_a", name: "A区(东段)", devices: [1, 2] },
|
|
|
- { id: "zone_b", name: "B区(隧道段)", devices: [3, 4] },
|
|
|
- { id: "zone_c", name: "C区(收费站)", devices: [5, 6] },
|
|
|
- { id: "zone_d", name: "D区(西段)", devices: [7, 8] }
|
|
|
-])
|
|
|
-
|
|
|
-// ==================== 广播状态 ====================
|
|
|
-const selectedZones = ref([])
|
|
|
-const broadcastMode = ref('zone') // zone, all, background
|
|
|
-const broadcastText = ref('')
|
|
|
-const uploadedFile = ref(null)
|
|
|
-const uploadedFileName = ref('')
|
|
|
-const isBroadcasting = ref(false)
|
|
|
-let broadcastInterval = null
|
|
|
-
|
|
|
-// ==================== 呼叫记录 ====================
|
|
|
-const callLogs = ref([
|
|
|
- { id: 1, time: "14:22:30", deviceName: "紧急电话-003", location: "K12+800 隧道口", action: "呼入等待中", type: "incoming", duration: null },
|
|
|
- { id: 2, time: "14:16:45", deviceName: "紧急电话-004", location: "K20+500 西侧", action: "接警处理", type: "answered", duration: "2分15秒" },
|
|
|
- { id: 3, time: "14:10:22", deviceName: "广播分机-002", location: "收费站广场", action: "应急广播", type: "broadcast", duration: "30秒" },
|
|
|
- { id: 4, time: "13:58:30", deviceName: "紧急电话-001", location: "K2+300 东侧", action: "测试通话", type: "test", duration: "45秒" }
|
|
|
-])
|
|
|
-
|
|
|
-// ==================== 计算属性 ====================
|
|
|
-const onlineCount = computed(() => devices.value.filter(d => d.status === 'online').length)
|
|
|
-const alarmCount = computed(() => devices.value.filter(d => d.status === 'alarm' || d.status === 'fault').length)
|
|
|
-const activeCallCount = computed(() => devices.value.filter(d => d.status === 'calling').length)
|
|
|
-
|
|
|
-const canBroadcast = computed(() => {
|
|
|
- if (broadcastMode.value === 'zone') return selectedZones.value.length > 0 && (broadcastText.value.trim() || uploadedFile.value)
|
|
|
- if (broadcastMode.value === 'all') return broadcastText.value.trim() || uploadedFile.value
|
|
|
- if (broadcastMode.value === 'background') return uploadedFile.value
|
|
|
- return false
|
|
|
-})
|
|
|
-
|
|
|
-// ==================== 辅助函数 ====================
|
|
|
-const getStatusText = (status) => {
|
|
|
- const map = {
|
|
|
- online: '设备在线',
|
|
|
- calling: '对讲中',
|
|
|
- broadcast: '广播中',
|
|
|
- fault: '故障',
|
|
|
- alarm: '报警中'
|
|
|
- }
|
|
|
- return map[status] || status
|
|
|
-}
|
|
|
-
|
|
|
-const getStatusDotClass = (status) => {
|
|
|
- const map = {
|
|
|
- online: 'dot-online',
|
|
|
- calling: 'dot-calling',
|
|
|
- broadcast: 'dot-broadcast',
|
|
|
- fault: 'dot-fault',
|
|
|
- alarm: 'dot-alarm'
|
|
|
- }
|
|
|
- return map[status] || 'dot-online'
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 设备交互 ====================
|
|
|
-const detailModalVisible = ref(false)
|
|
|
-const currentDevice = ref(null)
|
|
|
-
|
|
|
-const showDeviceDetail = (device) => {
|
|
|
- currentDevice.value = device
|
|
|
- detailModalVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-const closeDetailModal = () => {
|
|
|
- detailModalVisible.value = false
|
|
|
- currentDevice.value = null
|
|
|
-}
|
|
|
-
|
|
|
-// 接听呼叫
|
|
|
-const answerCall = (device) => {
|
|
|
- device.status = 'online'
|
|
|
- device.answerTime = new Date().toLocaleTimeString()
|
|
|
- addCallLog(device.name, device.location, '接警处理', 'answered')
|
|
|
- alert(`已接听 ${device.name} 的呼叫,开始通话`)
|
|
|
-}
|
|
|
-
|
|
|
-// 拒接呼叫
|
|
|
-const rejectCall = (device) => {
|
|
|
- device.status = 'online'
|
|
|
- device.callTime = null
|
|
|
- addCallLog(device.name, device.location, '未接来电', 'missed')
|
|
|
- alert(`已拒绝 ${device.name} 的呼叫`)
|
|
|
-}
|
|
|
-
|
|
|
-// 单设备广播
|
|
|
-const startBroadcast = (device) => {
|
|
|
- broadcastText.value = `【广播通知】${device.location} 请注意安全`
|
|
|
- selectedZones.value = []
|
|
|
- broadcastMode.value = 'zone'
|
|
|
- // 找到设备所在分区
|
|
|
- for (const zone of zones.value) {
|
|
|
- if (zone.devices.includes(device.id)) {
|
|
|
- selectedZones.value = [zone.id]
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- startBroadcastAll()
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 广播功能 ====================
|
|
|
-const toggleZone = (zoneId) => {
|
|
|
- if (selectedZones.value.includes(zoneId)) {
|
|
|
- selectedZones.value = selectedZones.value.filter(id => id !== zoneId)
|
|
|
- } else {
|
|
|
- selectedZones.value.push(zoneId)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const selectAllZones = () => {
|
|
|
- selectedZones.value = zones.value.map(z => z.id)
|
|
|
-}
|
|
|
-
|
|
|
-const clearAllZones = () => {
|
|
|
- selectedZones.value = []
|
|
|
-}
|
|
|
-
|
|
|
-// 处理文件上传
|
|
|
-const handleFileUpload = (event) => {
|
|
|
- const file = event.target.files[0]
|
|
|
- if (file) {
|
|
|
- uploadedFile.value = file
|
|
|
- uploadedFileName.value = file.name
|
|
|
- // 创建预览URL
|
|
|
- const url = URL.createObjectURL(file)
|
|
|
- const audio = new Audio()
|
|
|
- audio.src = url
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 播放预览
|
|
|
-const playPreview = () => {
|
|
|
- if (uploadedFile.value) {
|
|
|
- const url = URL.createObjectURL(uploadedFile.value)
|
|
|
- const audio = new Audio(url)
|
|
|
- audio.play().catch(e => console.log('播放失败'))
|
|
|
- } else if (broadcastText.value) {
|
|
|
- // 使用语音合成预览
|
|
|
- const utterance = new SpeechSynthesisUtterance(broadcastText.value)
|
|
|
- speechSynthesis.speak(utterance)
|
|
|
- } else {
|
|
|
- alert('请先输入广播内容或上传音频文件')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 开始广播
|
|
|
-const startBroadcastAll = () => {
|
|
|
- if (!canBroadcast.value) {
|
|
|
- alert('请选择广播分区并输入内容或上传音频文件')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- let targetDevices = []
|
|
|
- if (broadcastMode.value === 'all') {
|
|
|
- targetDevices = devices.value
|
|
|
- } else if (broadcastMode.value === 'zone') {
|
|
|
- const targetZoneIds = selectedZones.value
|
|
|
- const targetDeviceIds = zones.value.filter(z => targetZoneIds.includes(z.id)).flatMap(z => z.devices)
|
|
|
- targetDevices = devices.value.filter(d => targetDeviceIds.includes(d.id))
|
|
|
- } else if (broadcastMode.value === 'background') {
|
|
|
- targetDevices = devices.value.filter(d => d.type === 'broadcast')
|
|
|
- }
|
|
|
-
|
|
|
- isBroadcasting.value = true
|
|
|
-
|
|
|
- // 更新设备广播状态
|
|
|
- targetDevices.forEach(device => {
|
|
|
- if (device.status === 'online') {
|
|
|
- device.status = 'broadcast'
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 模拟广播播放
|
|
|
- if (broadcastText.value) {
|
|
|
- const utterance = new SpeechSynthesisUtterance(broadcastText.value)
|
|
|
- utterance.onend = () => {
|
|
|
- if (!isBroadcasting.value) return
|
|
|
- // 广播结束后恢复状态
|
|
|
- setTimeout(() => {
|
|
|
- stopBroadcast()
|
|
|
- }, 1000)
|
|
|
- }
|
|
|
- speechSynthesis.speak(utterance)
|
|
|
- } else if (uploadedFile.value) {
|
|
|
- const url = URL.createObjectURL(uploadedFile.value)
|
|
|
- const audio = new Audio(url)
|
|
|
- audio.onended = () => {
|
|
|
- setTimeout(() => {
|
|
|
- stopBroadcast()
|
|
|
- }, 1000)
|
|
|
- }
|
|
|
- audio.play().catch(e => console.log('播放失败'))
|
|
|
- }
|
|
|
-
|
|
|
- // 记录广播日志
|
|
|
- const zoneNames = broadcastMode.value === 'zone'
|
|
|
- ? zones.value.filter(z => selectedZones.value.includes(z.id)).map(z => z.name).join(', ')
|
|
|
- : (broadcastMode.value === 'all' ? '全线' : '背景音乐')
|
|
|
-
|
|
|
- addCallLog('系统广播', zoneNames, `广播: ${broadcastText.value || uploadedFileName.value}`, 'broadcast')
|
|
|
- alert(`广播已发送到${broadcastMode.value === 'zone' ? `分区: ${zoneNames}` : (broadcastMode.value === 'all' ? '全线' : '广播分机')}`)
|
|
|
-}
|
|
|
-
|
|
|
-// 停止广播
|
|
|
-const stopBroadcast = () => {
|
|
|
- isBroadcasting.value = false
|
|
|
- // 恢复设备状态
|
|
|
- devices.value.forEach(device => {
|
|
|
- if (device.status === 'broadcast') {
|
|
|
- device.status = 'online'
|
|
|
- }
|
|
|
- })
|
|
|
- if (broadcastInterval) {
|
|
|
- clearInterval(broadcastInterval)
|
|
|
- broadcastInterval = null
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 应急广播
|
|
|
-const emergencyBroadcast = (type) => {
|
|
|
- const messages = {
|
|
|
- '火灾警报': '【紧急广播】火灾警报,请所有人员迅速撤离至安全区域!',
|
|
|
- '泄漏事故': '【紧急广播】泄漏事故警报,请所有人员远离泄漏区域!',
|
|
|
- '紧急疏散': '【紧急广播】紧急疏散指令,请所有人员按照指示有序撤离!',
|
|
|
- '天气预警': '【天气预警】恶劣天气即将到来,请注意安全防范!'
|
|
|
- }
|
|
|
- broadcastText.value = messages[type]
|
|
|
- broadcastMode.value = 'all'
|
|
|
- startBroadcastAll()
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 呼叫记录管理 ====================
|
|
|
-const addCallLog = (deviceName, location, action, type) => {
|
|
|
- callLogs.value.unshift({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleTimeString(),
|
|
|
- deviceName: deviceName,
|
|
|
- location: location,
|
|
|
- action: action,
|
|
|
- type: type,
|
|
|
- duration: null
|
|
|
- })
|
|
|
- if (callLogs.value.length > 50) callLogs.value.pop()
|
|
|
-}
|
|
|
-
|
|
|
-const clearLogs = () => {
|
|
|
- if (confirm('确定清空所有呼叫记录吗?')) {
|
|
|
- callLogs.value = []
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 模拟实时设备状态更新 ====================
|
|
|
-let simulationInterval = null
|
|
|
-
|
|
|
-const simulateDeviceUpdates = () => {
|
|
|
- // 模拟随机呼叫
|
|
|
- if (!isBroadcasting.value && Math.random() < 0.05) {
|
|
|
- const onlineDevices = devices.value.filter(d => d.status === 'online' && d.type === 'emergency')
|
|
|
- if (onlineDevices.length > 0) {
|
|
|
- const randomDevice = onlineDevices[Math.floor(Math.random() * onlineDevices.length)]
|
|
|
- if (randomDevice.status === 'online') {
|
|
|
- randomDevice.status = 'calling'
|
|
|
- randomDevice.callTime = new Date().toLocaleTimeString()
|
|
|
- randomDevice.answerTime = null
|
|
|
- addCallLog(randomDevice.name, randomDevice.location, '呼入等待中', 'incoming')
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 自动接听模拟(20秒后自动接听)
|
|
|
- const callingDevices = devices.value.filter(d => d.status === 'calling')
|
|
|
- callingDevices.forEach(device => {
|
|
|
- if (device.callTime && !device.answerTime) {
|
|
|
- const callTime = new Date(`2000/01/01 ${device.callTime}`)
|
|
|
- const now = new Date()
|
|
|
- const diff = (now - callTime) / 1000
|
|
|
- if (diff > 20) {
|
|
|
- answerCall(device)
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 生命周期 ====================
|
|
|
-onMounted(() => {
|
|
|
- simulationInterval = setInterval(simulateDeviceUpdates, 5000)
|
|
|
-})
|
|
|
-
|
|
|
-onUnmounted(() => {
|
|
|
- if (simulationInterval) clearInterval(simulationInterval)
|
|
|
- if (broadcastInterval) clearInterval(broadcastInterval)
|
|
|
- speechSynthesis.cancel()
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-* {
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- box-sizing: border-box;
|
|
|
-}
|
|
|
-
|
|
|
-.prototype-container {
|
|
|
- max-width: 1600px;
|
|
|
- margin: 0 auto;
|
|
|
- background: #f0f4f8;
|
|
|
- border-radius: 28px;
|
|
|
- box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
|
|
|
- overflow: hidden;
|
|
|
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
|
-}
|
|
|
-
|
|
|
-/* 头部 */
|
|
|
-.header {
|
|
|
- background: #0a2b3c;
|
|
|
- padding: 16px 28px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 16px;
|
|
|
- border-bottom: 3px solid #e74c3c;
|
|
|
-}
|
|
|
-
|
|
|
-.logo h2 {
|
|
|
- color: white;
|
|
|
- font-weight: 600;
|
|
|
- font-size: 1.3rem;
|
|
|
-}
|
|
|
-
|
|
|
-.logo p {
|
|
|
- color: #bdd8e8;
|
|
|
- font-size: 0.7rem;
|
|
|
-}
|
|
|
-
|
|
|
-.header-stats {
|
|
|
- display: flex;
|
|
|
- gap: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-item {
|
|
|
- background: rgba(255, 255, 255, 0.15);
|
|
|
- padding: 6px 16px;
|
|
|
- border-radius: 40px;
|
|
|
- text-align: center;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-item.warning {
|
|
|
- background: #e74c3c;
|
|
|
- animation: pulse 1s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-value {
|
|
|
- font-weight: 700;
|
|
|
- font-size: 1.2rem;
|
|
|
- margin-right: 6px;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-label {
|
|
|
- font-size: 0.7rem;
|
|
|
- opacity: 0.9;
|
|
|
-}
|
|
|
-
|
|
|
-/* 主体布局 */
|
|
|
-.main-layout {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.left-panel {
|
|
|
- flex: 1.5;
|
|
|
- background: #f9fbfd;
|
|
|
- padding: 20px;
|
|
|
- min-width: 500px;
|
|
|
-}
|
|
|
-
|
|
|
-.right-panel {
|
|
|
- flex: 1;
|
|
|
- background: #ffffff;
|
|
|
- padding: 20px;
|
|
|
- min-width: 400px;
|
|
|
- border-left: 1px solid #e2edf2;
|
|
|
-}
|
|
|
-
|
|
|
-.panel-title {
|
|
|
- font-size: 1rem;
|
|
|
- font-weight: 700;
|
|
|
- color: #1d5a78;
|
|
|
- margin-bottom: 20px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-
|
|
|
-.live-badge {
|
|
|
- font-size: 0.7rem;
|
|
|
- background: #e74c3c;
|
|
|
- color: white;
|
|
|
- padding: 4px 12px;
|
|
|
- border-radius: 20px;
|
|
|
- animation: blink 1s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-/* 设备网格 */
|
|
|
-.device-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
- gap: 16px;
|
|
|
- max-height: 600px;
|
|
|
- overflow-y: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.device-card {
|
|
|
- background: white;
|
|
|
- border-radius: 20px;
|
|
|
- padding: 16px;
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- position: relative;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s;
|
|
|
- border: 2px solid transparent;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
-}
|
|
|
-
|
|
|
-.device-card:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
|
|
-}
|
|
|
-
|
|
|
-.device-card.status-online {
|
|
|
- border-left: 4px solid #27ae60;
|
|
|
-}
|
|
|
-
|
|
|
-.device-card.status-calling {
|
|
|
- border-left: 4px solid #f39c12;
|
|
|
- background: #fef9e8;
|
|
|
- animation: callingPulse 1s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-.device-card.status-broadcast {
|
|
|
- border-left: 4px solid #3498db;
|
|
|
- background: #e8f4fd;
|
|
|
-}
|
|
|
-
|
|
|
-.device-card.status-fault {
|
|
|
- border-left: 4px solid #e74c3c;
|
|
|
- background: #fef0ef;
|
|
|
-}
|
|
|
-
|
|
|
-.device-card.status-alarm {
|
|
|
- border-left: 4px solid #e74c3c;
|
|
|
- background: #ffefef;
|
|
|
- animation: alarmPulse 0.8s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-.device-icon {
|
|
|
- font-size: 2rem;
|
|
|
- width: 50px;
|
|
|
- height: 50px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- background: #f0f4f8;
|
|
|
- border-radius: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.device-icon.pulse-animation {
|
|
|
- animation: iconPulse 0.6s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-.device-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.device-name {
|
|
|
- font-weight: 700;
|
|
|
- font-size: 0.85rem;
|
|
|
-}
|
|
|
-
|
|
|
-.device-location {
|
|
|
- font-size: 0.65rem;
|
|
|
- color: #6f8ea8;
|
|
|
- margin: 2px 0;
|
|
|
-}
|
|
|
-
|
|
|
-.device-status {
|
|
|
- font-size: 0.7rem;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- margin-top: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.status-dot {
|
|
|
- width: 8px;
|
|
|
- height: 8px;
|
|
|
- border-radius: 50%;
|
|
|
- display: inline-block;
|
|
|
-}
|
|
|
-
|
|
|
-.dot-online { background: #27ae60; box-shadow: 0 0 4px #27ae60; }
|
|
|
-.dot-calling { background: #f39c12; box-shadow: 0 0 4px #f39c12; animation: blink 0.8s infinite; }
|
|
|
-.dot-broadcast { background: #3498db; box-shadow: 0 0 4px #3498db; }
|
|
|
-.dot-fault { background: #e74c3c; box-shadow: 0 0 4px #e74c3c; }
|
|
|
-.dot-alarm { background: #e74c3c; box-shadow: 0 0 8px #e74c3c; animation: blink 0.5s infinite; }
|
|
|
-
|
|
|
-.device-call-info {
|
|
|
- font-size: 0.65rem;
|
|
|
- color: #e67e22;
|
|
|
- margin-top: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.device-actions {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 6px;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-answer, .btn-reject, .btn-broadcast {
|
|
|
- padding: 4px 10px;
|
|
|
- border-radius: 20px;
|
|
|
- font-size: 0.65rem;
|
|
|
- border: none;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-answer { background: #27ae60; color: white; }
|
|
|
-.btn-reject { background: #e74c3c; color: white; }
|
|
|
-.btn-broadcast { background: #3498db; color: white; }
|
|
|
-
|
|
|
-.alarm-wave {
|
|
|
- position: absolute;
|
|
|
- top: 8px;
|
|
|
- right: 8px;
|
|
|
- display: flex;
|
|
|
- gap: 3px;
|
|
|
-}
|
|
|
-
|
|
|
-.wave {
|
|
|
- width: 4px;
|
|
|
- height: 12px;
|
|
|
- background: #e74c3c;
|
|
|
- border-radius: 2px;
|
|
|
- animation: waveAnim 0.6s infinite ease-in-out;
|
|
|
-}
|
|
|
-
|
|
|
-.wave:nth-child(2) { animation-delay: 0.2s; }
|
|
|
-.wave:nth-child(3) { animation-delay: 0.4s; }
|
|
|
-
|
|
|
-/* 卡片样式 */
|
|
|
-.card {
|
|
|
- background: white;
|
|
|
- border-radius: 20px;
|
|
|
- border: 1px solid #e6edf3;
|
|
|
- margin-bottom: 20px;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.card-header {
|
|
|
- background: #f9fafc;
|
|
|
- padding: 14px 18px;
|
|
|
- border-bottom: 1px solid #eef2f6;
|
|
|
- font-weight: 700;
|
|
|
- font-size: 0.85rem;
|
|
|
- color: #1d5a78;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-
|
|
|
-.card-body {
|
|
|
- padding: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 广播区域 */
|
|
|
-.section-title {
|
|
|
- font-size: 0.7rem;
|
|
|
- font-weight: 600;
|
|
|
- color: #2c5a72;
|
|
|
- margin-bottom: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.zone-buttons {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.zone-btn {
|
|
|
- padding: 6px 14px;
|
|
|
- border-radius: 30px;
|
|
|
- border: 1px solid #cbdde6;
|
|
|
- background: white;
|
|
|
- font-size: 0.7rem;
|
|
|
- cursor: pointer;
|
|
|
- transition: 0.2s;
|
|
|
-}
|
|
|
-
|
|
|
-.zone-btn.active {
|
|
|
- background: #3498db;
|
|
|
- color: white;
|
|
|
- border-color: #3498db;
|
|
|
-}
|
|
|
-
|
|
|
-.zone-btn.select-all {
|
|
|
- background: #27ae60;
|
|
|
- color: white;
|
|
|
- border-color: #27ae60;
|
|
|
-}
|
|
|
-
|
|
|
-.zone-btn.clear-all {
|
|
|
- background: #e74c3c;
|
|
|
- color: white;
|
|
|
- border-color: #e74c3c;
|
|
|
-}
|
|
|
-
|
|
|
-.mode-buttons {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- flex-wrap: wrap;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.mode-btn {
|
|
|
- padding: 6px 16px;
|
|
|
- border-radius: 30px;
|
|
|
- border: 1px solid #cbdde6;
|
|
|
- background: white;
|
|
|
- font-size: 0.7rem;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.mode-btn.active {
|
|
|
- background: #3498db;
|
|
|
- color: white;
|
|
|
- border-color: #3498db;
|
|
|
-}
|
|
|
-
|
|
|
-.broadcast-input-area {
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.broadcast-input {
|
|
|
- width: 100%;
|
|
|
- padding: 10px;
|
|
|
- border-radius: 12px;
|
|
|
- border: 1px solid #cbdde6;
|
|
|
- font-size: 0.75rem;
|
|
|
- resize: vertical;
|
|
|
-}
|
|
|
-
|
|
|
-.file-upload-area {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-top: 10px;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.upload-btn {
|
|
|
- background: #3498db;
|
|
|
- color: white;
|
|
|
- padding: 6px 14px;
|
|
|
- border-radius: 30px;
|
|
|
- font-size: 0.7rem;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.file-name {
|
|
|
- font-size: 0.65rem;
|
|
|
- color: #6f8ea8;
|
|
|
-}
|
|
|
-
|
|
|
-.preview-btn {
|
|
|
- background: #f39c12;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- padding: 6px 14px;
|
|
|
- border-radius: 30px;
|
|
|
- font-size: 0.7rem;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.control-buttons {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-broadcast-start {
|
|
|
- background: #27ae60;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- padding: 10px 20px;
|
|
|
- border-radius: 30px;
|
|
|
- font-weight: 600;
|
|
|
- cursor: pointer;
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-broadcast-start:disabled {
|
|
|
- background: #95a5a6;
|
|
|
- cursor: not-allowed;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-broadcast-stop {
|
|
|
- background: #e74c3c;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- padding: 10px 20px;
|
|
|
- border-radius: 30px;
|
|
|
- font-weight: 600;
|
|
|
- cursor: pointer;
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.emergency-buttons {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.emergency-btn {
|
|
|
- padding: 8px 16px;
|
|
|
- border-radius: 30px;
|
|
|
- border: none;
|
|
|
- font-size: 0.7rem;
|
|
|
- font-weight: 600;
|
|
|
- cursor: pointer;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.emergency-btn.fire { background: #e74c3c; }
|
|
|
-.emergency-btn.leak { background: #f39c12; }
|
|
|
-.emergency-btn.evacuate { background: #e67e22; }
|
|
|
-.emergency-btn.weather { background: #3498db; }
|
|
|
-
|
|
|
-.broadcast-status {
|
|
|
- font-size: 0.65rem;
|
|
|
- padding: 2px 10px;
|
|
|
- border-radius: 20px;
|
|
|
- background: #95a5a6;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.broadcast-status.active {
|
|
|
- background: #27ae60;
|
|
|
- animation: blink 1s infinite;
|
|
|
-}
|
|
|
-
|
|
|
-/* 呼叫记录 */
|
|
|
-.call-log-list {
|
|
|
- max-height: 300px;
|
|
|
- overflow-y: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.log-item {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- padding: 10px 0;
|
|
|
- border-bottom: 1px solid #f0f4f7;
|
|
|
- font-size: 0.7rem;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.log-item.incoming { background: #fef9e8; margin: 0 -8px; padding: 8px 8px; border-radius: 8px; }
|
|
|
-.log-item.answered { background: #e8f8f0; margin: 0 -8px; padding: 8px 8px; border-radius: 8px; }
|
|
|
-.log-item.broadcast { background: #e8f4fd; margin: 0 -8px; padding: 8px 8px; border-radius: 8px; }
|
|
|
-
|
|
|
-.log-time { color: #6f8ea8; width: 70px; font-family: monospace; }
|
|
|
-.log-device { font-weight: 600; min-width: 100px; }
|
|
|
-.log-location { color: #2c5a72; flex: 1; }
|
|
|
-.log-action { color: #3498db; }
|
|
|
-.log-duration { color: #27ae60; }
|
|
|
-
|
|
|
-.empty-log {
|
|
|
- text-align: center;
|
|
|
- padding: 40px;
|
|
|
- color: #8aaec2;
|
|
|
- font-size: 0.8rem;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-clear {
|
|
|
- background: none;
|
|
|
- border: none;
|
|
|
- color: #e74c3c;
|
|
|
- font-size: 0.65rem;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-/* 模态框 */
|
|
|
-.modal-overlay {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- background: rgba(0, 0, 0, 0.5);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- z-index: 1000;
|
|
|
-}
|
|
|
-
|
|
|
-.modal-content {
|
|
|
- background: white;
|
|
|
- border-radius: 24px;
|
|
|
- width: 450px;
|
|
|
- max-width: 90%;
|
|
|
- padding: 24px;
|
|
|
-}
|
|
|
-
|
|
|
-.detail-row {
|
|
|
- margin-bottom: 12px;
|
|
|
- font-size: 0.75rem;
|
|
|
-}
|
|
|
-
|
|
|
-.detail-label {
|
|
|
- font-weight: 600;
|
|
|
- width: 100px;
|
|
|
- display: inline-block;
|
|
|
- color: #2c5a72;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-primary {
|
|
|
- background: #3498db;
|
|
|
- border: none;
|
|
|
- color: white;
|
|
|
- padding: 8px 16px;
|
|
|
- border-radius: 30px;
|
|
|
- cursor: pointer;
|
|
|
- width: 100%;
|
|
|
-}
|
|
|
-
|
|
|
-/* 动画 */
|
|
|
-@keyframes pulse {
|
|
|
- 0%, 100% { opacity: 1; }
|
|
|
- 50% { opacity: 0.7; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes blink {
|
|
|
- 0%, 100% { opacity: 1; }
|
|
|
- 50% { opacity: 0.4; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes callingPulse {
|
|
|
- 0%, 100% { background: #fef9e8; border-left-color: #f39c12; }
|
|
|
- 50% { background: #fef0d8; border-left-color: #e67e22; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes alarmPulse {
|
|
|
- 0%, 100% { background: #ffefef; border-left-color: #e74c3c; }
|
|
|
- 50% { background: #ffe0e0; border-left-color: #c0392b; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes iconPulse {
|
|
|
- 0%, 100% { transform: scale(1); }
|
|
|
- 50% { transform: scale(1.05); background: #ffe0e0; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes waveAnim {
|
|
|
- 0%, 100% { height: 8px; opacity: 0.5; }
|
|
|
- 50% { height: 16px; opacity: 1; }
|
|
|
-}
|
|
|
-</style>
|