home.vue 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583
  1. <template>
  2. <view class="notice-board">
  3. <view class="board-header">
  4. <view class="card_logo" @click="linkSet">
  5. <image class="logo_image" src="/static/logo.png" mode=""></image>
  6. <view class="logo_title">观山湖区疾控中心</view>
  7. </view>
  8. <view class="card_yellow">
  9. <view class="card_board">
  10. <text class="board-title">接种留观等待</text>
  11. </view>
  12. </view>
  13. <view class="card_time">
  14. <view class="time_title">{{hhmmss}}</view>
  15. <view class="current-time">{{ currentTime }} {{whatDay}}</view>
  16. </view>
  17. </view>
  18. <!-- 表格容器 -->
  19. <view class="table-container">
  20. <!-- 主要表头 -->
  21. <view class="table-header">
  22. <view class="table-row header-row">
  23. <view class="cell name title_color">姓名</view>
  24. <view class="cell time title_color">留观时间</view>
  25. <view class="cell time title_color">离开时间</view>
  26. <view class="cell status title_color">状态</view>
  27. </view>
  28. </view>
  29. <!-- 特殊状态数据展示区域(固定) -->
  30. <view class="special-status-container" v-if="specialStatusData.length > 0">
  31. <view class="table-row" v-for="(item, index) in specialStatusData" :key="item.id"
  32. :class="`item-${getClass(item.status)}`">
  33. <view class="table-cell name" :class="`title-${getClass(item.status)}`">{{ item.patientName }}
  34. </view>
  35. <view class="table-cell time">{{ formatTime(item.createTime) }}</view>
  36. <view class="table-cell time">{{ formatTime(item.outTime) }}</view>
  37. <view class="table-cell status">
  38. <view class="status-tag" :class="`status-${getClass(item.status)}`">
  39. {{ getStatusText(item) }}
  40. </view>
  41. </view>
  42. </view>
  43. <!-- 分隔线 -->
  44. <view class="divider"></view>
  45. </view>
  46. <!-- 普通数据内容区域 -->
  47. <view class="table-body" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
  48. @touchend="handleTouchEnd">
  49. <view class="table-row" v-for="(item, index) in displayNormalData" :key="item.id"
  50. :class="`item-${getClass(item.status)}`">
  51. <view class="table-cell name" :class="`title-${getClass(item.status)}`">{{ item.patientName }}
  52. </view>
  53. <view class="table-cell time">{{ formatTime(item.createTime) }}</view>
  54. <view class="table-cell time">{{ formatTime(item.outTime) }}</view>
  55. <view class="table-cell status">
  56. <view class="status-tag" :class="`status-${getClass(item.status)}`">
  57. {{ getStatusText(item) }}
  58. </view>
  59. </view>
  60. </view>
  61. <!-- 空数据提示 -->
  62. <view v-if="displayNormalData.length === 0 && specialStatusData.length === 0" class="empty-tip">
  63. 暂无留观人员信息
  64. </view>
  65. </view>
  66. </view>
  67. <view class="card_foot">
  68. <view class="card_tips_box">
  69. <image class="tips_imageil" src="/static/horn.png" mode=""></image>
  70. <view class="title_tips">温馨提示:</view>
  71. </view>
  72. <view class="title_tips_foot">请注意留观30分钟后无不良反应后再离开,谢谢。</view>
  73. </view>
  74. <!-- 连接状态与IP编辑栏 -->
  75. <view class="box_link_set" v-if="linkShow">
  76. <view class="box_popup" :style="{bottom: keyboardHeight + 'px'}">
  77. <view class="head_popup_title">
  78. <view class="title_head_popup">连接设置</view>
  79. <view class="close_title" @click="getClose">×</view>
  80. </view>
  81. <view class="status-bar-bottom" :class="`status-${connectionStatus}`">
  82. <!-- IP 编辑区域 -->
  83. <view class="ip-input-group">
  84. <text class="ip-label">IP:</text>
  85. <input type="text" :value="serverIp" @input="onIpInput" placeholder="192.168.0.41"
  86. class="ip-input" @keyboardheightchange="keyboardheightchange" />
  87. <text class="colon">:</text>
  88. <input type="number" :value="serverPort" @input="onPortInput" placeholder="8811"
  89. class="port-input" @keyboardheightchange="keyboardheightchange" />
  90. <button class="btn-reconnect" size="mini" @click="handleReconnect">
  91. 更新并重连
  92. </button>
  93. </view>
  94. <view class="status-text-box">
  95. <text class="dot" :class="`dot-${connectionStatus}`">●</text>
  96. <text class="status-text">{{ connectionText }}</text>
  97. </view>
  98. </view>
  99. </view>
  100. </view>
  101. </view>
  102. </template>
  103. <script>
  104. export default {
  105. data() {
  106. return {
  107. currentTime: '', // 当前时间
  108. hhmmss: '', // 当前时间
  109. whatDay: '', //星期几
  110. timeTimer: null, // 时间更新定时器
  111. // WebSocket 实例
  112. ws: null,
  113. reconnectTimer: null,
  114. heartbeatTimer: null, // 心跳定时器
  115. heartbeatTimeout: null, // 心跳响应超时计时器
  116. heartbeatInterval: 30000, // 30秒发一次心跳
  117. heartbeatTimeoutTime: 10000, // 10秒内未响应视为超时
  118. isPongReceived: true, // 标记是否收到 pong
  119. // 连接状态
  120. connectionStatus: 'connecting', // connecting, connected, disconnected
  121. statusText: {
  122. observing: '留观中',
  123. completed: '留观完成,可离开',
  124. warning: '提前离开',
  125. hasleft: '已离开',
  126. },
  127. // 可编辑的服务器地址
  128. serverIp: '192.168.0.41',
  129. serverPort: '8811',
  130. // 所有数据留观数据列表
  131. allData: [],
  132. // 当前页码
  133. currentPage: 1,
  134. // 每页显示条数(自适应计算)
  135. pageSize: 10,
  136. // 是否自动滚动
  137. autoScroll: true,
  138. // 自动滚动定时器
  139. autoScrollTimer: null,
  140. // 滚动间隔时间(毫秒)
  141. scrollInterval: 6000,
  142. // 窗口高度
  143. windowHeight: 0,
  144. // 触摸相关
  145. touchStartX: 0,
  146. touchStartTime: 0,
  147. isTouching: false,
  148. touchStartY: 0,
  149. // 垂直滑动相关
  150. lastSwipeTime: 0,
  151. linkShow: false,
  152. ipArray: [],
  153. keyboardHeight: 0,
  154. now: new Date(),
  155. timer: null,
  156. heartbeatRate: 0,
  157. isSend: true,
  158. reconnectionNum: 0,
  159. }
  160. },
  161. computed: {
  162. connectionText() {
  163. const {
  164. connectionStatus,
  165. serverIp,
  166. serverPort
  167. } = this;
  168. return {
  169. connecting: `正在连接 ${serverIp}:${serverPort}...`,
  170. connected: `已连接到 ${serverIp}:${serverPort}`,
  171. disconnected: `连接已断开`
  172. } [connectionStatus];
  173. },
  174. // 特殊状态数据(状态1和2)
  175. specialStatusData() {
  176. return this.allData.filter(item => item.status === 3 || item.status === 4 || item.IsOut == 1)
  177. },
  178. // 普通状态数据(状态0)
  179. normalData() {
  180. return this.allData.filter(item => item.status === 0 || item.status === 1)
  181. },
  182. // 实际显示的条数(减去缓冲行)
  183. actualPageSize() {
  184. return Math.max(1, this.pageSize - 1)
  185. },
  186. // 总页数(基于普通数据计算)
  187. totalPages() {
  188. if (this.actualPageSize <= 0 || this.normalData.length === 0) return 1
  189. return Math.ceil(this.normalData.length / this.actualPageSize)
  190. },
  191. // 显示的普通数据(包含缓冲行)
  192. displayNormalData() {
  193. const start = (this.currentPage - 1) * this.actualPageSize
  194. const end = Math.min(start + this.pageSize, this.normalData.length)
  195. return this.normalData.slice(start, end).map(item => {
  196. const expectedTime = new Date(item.outTime)
  197. const timeDiff = expectedTime - this.now // 毫秒差
  198. if (timeDiff > 0) {
  199. // 未到离开时间
  200. const days = Math.floor(timeDiff / (24 * 3600 * 1000))
  201. const hours = Math.floor((timeDiff % (24 * 3600 * 1000)) / (3600 * 1000))
  202. const minutes = Math.floor((timeDiff % (3600 * 1000)) / (60 * 1000))
  203. const seconds = Math.floor((timeDiff % (60 * 1000)) / 1000)
  204. var fzArr = ''
  205. if (minutes) {
  206. fzArr = `剩余${minutes}分钟`
  207. } else {
  208. fzArr = `剩余${seconds}秒`
  209. }
  210. return {
  211. ...item,
  212. remainingTime: fzArr,
  213. }
  214. } else {
  215. return {
  216. ...item,
  217. remainingTime: '留观完成,可离开',
  218. }
  219. }
  220. })
  221. }
  222. },
  223. mounted() {
  224. this.reconnectionNum = 0
  225. // 每秒更新当前时间
  226. this.timer = setInterval(() => {
  227. this.now = new Date()
  228. }, 1000)
  229. this.getIpAddress()
  230. this.initPage()
  231. const lastIp = uni.getStorageSync('serverIp');
  232. const lastPort = uni.getStorageSync('serverPort');
  233. if (lastIp && lastPort) {
  234. this.serverIp = lastIp
  235. this.serverPort = lastPort
  236. }
  237. // 在H5平台添加鼠标滚轮支持
  238. //#ifdef H5
  239. this.startCheck()
  240. this.addMouseWheelSupport()
  241. //#endif
  242. this.updateCurrentTime(); // 立即更新一次时间
  243. this.timeTimer = setInterval(() => {
  244. this.updateCurrentTime();
  245. }, 1000); // 每秒更新一次时间
  246. },
  247. beforeDestroy() {
  248. // 组件销毁时清理定时器
  249. if (this.timer) {
  250. this.timer = null
  251. clearInterval(this.timer)
  252. }
  253. this.stopAutoScroll()
  254. uni.offWindowResize(this.handleWindowResize)
  255. //#ifdef H5
  256. this.removeMouseWheelSupport()
  257. //#endif
  258. if (this.ws) {
  259. this.ws.close();
  260. }
  261. if (this.reconnectTimer) {
  262. clearTimeout(this.reconnectTimer);
  263. }
  264. this.stopHeartbeat();
  265. // 清理时间更新定时器
  266. if (this.timeTimer) {
  267. clearInterval(this.timeTimer);
  268. }
  269. },
  270. methods: {
  271. keyboardheightchange() {
  272. uni.onKeyboardHeightChange(res => {
  273. this.keyboardHeight = res.height
  274. })
  275. },
  276. findFirstWebSocketIp(ipList, index = 0, onSuccess, onAllFailed) {
  277. // 终止条件:全部失败
  278. if (index >= ipList.length) {
  279. if (typeof onAllFailed === 'function') {
  280. onAllFailed();
  281. }
  282. return;
  283. }
  284. // console.log(ipList, 344)
  285. const ip = ipList[index].trim();
  286. const port = this.serverPort; // 根据你的服务修改端口
  287. const url = `ws://${ip}:${port}/`; // 可改为 /ws, /device 等路径
  288. // console.log(`🔍 正在尝试连接 WebSocket: ${url}`);
  289. // 先确保没有遗留连接
  290. if (this.ws) {
  291. this.ws.close();
  292. this.ws = null;
  293. }
  294. // 创建 SocketTask
  295. this.ws = uni.connectSocket({
  296. url: url,
  297. success: (res) => {
  298. console.log('connectSocket success', res);
  299. },
  300. fail: (err) => {
  301. console.error('connectSocket 失败', err);
  302. this.connectionStatus = 'disconnected';
  303. if (this.serverIp == ip) {
  304. this.heartbeatRate = 0
  305. this.reconnectionNum++
  306. //#ifdef H5
  307. this.isSend = true
  308. this.findFirstWebSocketIp([this.serverIp], index, onSuccess, onAllFailed);
  309. //#endif
  310. }
  311. //#ifdef APP
  312. this.reconnectionNum++
  313. this.isSend = true
  314. this.heartbeatRate = 0
  315. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  316. //#endif
  317. }
  318. });
  319. // --- WebSocket 事件监听 ---
  320. this.ws.onOpen((res) => {
  321. this.serverIp = ip
  322. uni.setStorageSync('serverIp', this.serverIp);
  323. uni.setStorageSync('serverPort', this.serverPort);
  324. // console.log('WebSocket 连接成功', res);
  325. this.connectionStatus = 'connected';
  326. this.ws.send({
  327. data: 'link',
  328. });
  329. onSuccess(ip);
  330. // ✅ 连接成功后启动心跳
  331. this.startHeartbeat();
  332. uni.hideLoading();
  333. });
  334. this.ws.onMessage((res) => {
  335. // ✅ 处理心跳响应 pong
  336. if (res.data === 'PONG' || res.data === '{"type":"PONG"}') {
  337. uni.hideToast();
  338. this.isSend = true
  339. this.reconnectionNum = 0
  340. this.heartbeatRate = 0
  341. this.isPongReceived = true;
  342. // console.log('收到 pong,心跳正常');
  343. // ✅ 清除等待 pong 的超时计时器
  344. if (this.heartbeatTimeout) {
  345. clearTimeout(this.heartbeatTimeout);
  346. this.heartbeatTimeout = null;
  347. }
  348. return;
  349. }
  350. try {
  351. const data = JSON.parse(res.data);
  352. // console.log('收到消息:', data);
  353. this.handleMessage(data);
  354. } catch (e) {
  355. console.warn('非 JSON 消息,已忽略', res.data);
  356. }
  357. });
  358. this.ws.onClose((res) => {
  359. console.log('WebSocket 连接关闭', res);
  360. this.connectionStatus = 'disconnected';
  361. this.stopHeartbeat(); // 停止心跳
  362. var reconnectionTime = setTimeout(() => {
  363. clearTimeout(reconnectionTime)
  364. if (this.serverIp == ip) {
  365. //#ifdef H5
  366. this.isSend = true
  367. this.heartbeatRate = 0
  368. this.reconnectionNum++
  369. this.findFirstWebSocketIp([this.serverIp], index, onSuccess, onAllFailed);
  370. //#endif
  371. }
  372. //#ifdef APP
  373. this.isSend = true
  374. this.heartbeatRate = 0
  375. this.reconnectionNum++
  376. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  377. //#endif
  378. }, 2000)
  379. });
  380. this.ws.onError((err) => {
  381. console.error('WebSocket 错误', err);
  382. this.connectionStatus = 'disconnected';
  383. //#ifdef APP
  384. this.isSend = true
  385. this.heartbeatRate = 0
  386. this.reconnectionNum++
  387. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  388. //#endif
  389. });
  390. },
  391. // 连接设置
  392. linkSet() {
  393. this.linkShow = true
  394. },
  395. // 关闭弹窗
  396. getClose() {
  397. this.linkShow = false
  398. },
  399. // 获取ip地址
  400. getIpAddress() {
  401. //获取插件示例
  402. //#ifdef APP
  403. var deviceFinder = uni.requireNativePlugin("Alikes-NetTools-DeviceFinder")
  404. deviceFinder.scan({}, (res) => {
  405. this.ipArray = res
  406. this.ipArray.unshift(this.serverIp)
  407. this.startCheck()
  408. })
  409. //#endif
  410. },
  411. // 启动检查
  412. startCheck() {
  413. var ipArray = []
  414. //#ifdef H5
  415. ipArray = [this.serverIp]
  416. //#endif
  417. //#ifdef APP
  418. ipArray = this.ipArray
  419. //#endif
  420. this.findFirstWebSocketIp(
  421. ipArray,
  422. 0,
  423. (ip) => {
  424. // ✅ 找到可用设备
  425. this.onDeviceFound(ip);
  426. this.linkShow = false
  427. },
  428. () => {
  429. // ❌ 全部失败
  430. this.onNoDevice();
  431. }
  432. );
  433. },
  434. // 找到设备后的逻辑
  435. onDeviceFound(ip) {
  436. uni.showToast({
  437. title: '连接成功',
  438. icon: 'success'
  439. });
  440. // console.log(`🚀 可用设备: ${ip},开始后续操作...`);
  441. // 例如:跳转控制页面、订阅消息等
  442. },
  443. // 无设备可用
  444. onNoDevice() {
  445. uni.showToast({
  446. title: '无设备响应',
  447. icon: 'none',
  448. duration: 3000
  449. });
  450. this.getIpAddress()
  451. // console.log('🚨 所有设备 WebSocket 均无法连接');
  452. },
  453. getClass(status) {
  454. var title = '';
  455. if (status == 0) {
  456. title = 'observing'
  457. } else if (status == 1) {
  458. title = 'completed'
  459. } else if (status == 3) {
  460. title = 'warning'
  461. } else if (status == 4) {
  462. title = 'hasleft'
  463. }
  464. return title
  465. },
  466. // 获取状态文本
  467. getStatusText(item) {
  468. if (item.status == 0) {
  469. return item.remainingTime
  470. // 每秒更新一次
  471. } else if (item.status == 1) {
  472. return this.statusText.completed
  473. } else if (item.status == 3) {
  474. return this.statusText.warning
  475. } else if (item.status == 4) {
  476. return this.statusText.hasleft
  477. }
  478. },
  479. // 初始化页面
  480. initPage() {
  481. this.calculatePageSize()
  482. this.handleWindowResize = this.debounce(this.calculatePageSize, 300)
  483. uni.onWindowResize(this.handleWindowResize)
  484. },
  485. // 计算自适应的页面大小
  486. calculatePageSize() {
  487. try {
  488. // 获取系统信息
  489. const systemInfo = uni.getSystemInfoSync()
  490. this.windowHeight = systemInfo.windowHeight
  491. // 计算可用高度
  492. const headerHeight = uni.upx2px(130) // 头部高度
  493. const paginationHeight = uni.upx2px(100) // 分页区域高度
  494. const tableHeaderHeight = uni.upx2px(120) // 表格头部高度
  495. const specialDataHeight = this.specialStatusData.length > 0 ?
  496. uni.upx2px(80 * this.specialStatusData.length) : 0 // 特殊数据区域高度 + 分隔线
  497. const padding = uni.upx2px(0) // 上下padding
  498. const availableHeight = this.windowHeight - headerHeight - paginationHeight -
  499. tableHeaderHeight - specialDataHeight - padding
  500. // 计算行高(px)- 固定60rpx
  501. const rowHeightPx = uni.upx2px(140)
  502. // 计算可显示的行数
  503. const calculatedPageSize = Math.floor(availableHeight / rowHeightPx)
  504. // 设置页面大小(最少显示4行,最多显示30行)
  505. this.pageSize = Math.max(4, Math.min(30, calculatedPageSize))
  506. // 验证当前页
  507. this.validateCurrentPage()
  508. } catch (error) {
  509. console.error('计算页面大小失败:', error)
  510. this.pageSize = 10 // 默认值
  511. }
  512. },
  513. // 防抖函数
  514. debounce(func, wait) {
  515. let timeout
  516. return function executedFunction(...args) {
  517. const later = () => {
  518. clearTimeout(timeout)
  519. func(...args)
  520. }
  521. clearTimeout(timeout)
  522. timeout = setTimeout(later, wait)
  523. }
  524. },
  525. // 验证当前页是否有效
  526. validateCurrentPage() {
  527. if (this.currentPage > this.totalPages && this.totalPages > 0) {
  528. this.currentPage = this.totalPages
  529. }
  530. if (this.currentPage < 1) {
  531. this.currentPage = 1
  532. }
  533. },
  534. // 构建 WebSocket URL
  535. getWebSocketUrl() {
  536. return `ws://${this.serverIp}:${this.serverPort}`;
  537. },
  538. onIpInput(value) {
  539. this.serverIp = value.detail.value
  540. },
  541. // 输入
  542. onPortInput(value) {
  543. this.serverPort = value.detail.value
  544. },
  545. // 处理重连
  546. handleReconnect() {
  547. uni.showLoading({
  548. title: '正在重连...'
  549. });
  550. this.linkShow = false
  551. this.connectionStatus = 'connecting';
  552. // 关闭旧连接
  553. if (this.ws) {
  554. this.ws.close({
  555. success: () => {
  556. // console.log('旧连接已关闭');
  557. //#ifdef H5
  558. this.startCheck()
  559. //#endif
  560. //#ifdef APP
  561. this.getIpAddress()
  562. //#endif
  563. },
  564. fail: () => {
  565. //#ifdef H5
  566. this.startCheck()
  567. //#endif
  568. //#ifdef APP
  569. this.getIpAddress()
  570. //#endif
  571. }
  572. });
  573. } else {
  574. //#ifdef H5
  575. this.startCheck()
  576. //#endif
  577. //#ifdef APP
  578. this.getIpAddress()
  579. //#endif
  580. }
  581. setTimeout(() => {
  582. uni.hideLoading();
  583. }, 2000);
  584. },
  585. // 处理收到的消息
  586. handleMessage(data) {
  587. // 1. 全量数据:数组(不是对象,或没有 action 字段)
  588. if (data.action == 'link') {
  589. this.updateListWithArray(data.data);
  590. return;
  591. }
  592. // 2. 增量操作:对象
  593. if (typeof data === 'object' && data !== null) {
  594. const action = data.action;
  595. if (!action) {
  596. console.warn('消息缺少 action 字段', data);
  597. return;
  598. }
  599. switch (action) {
  600. case 'add':
  601. case 'create':
  602. this.batchAdd(data.data);
  603. break;
  604. case 'update':
  605. this.batchUpdate(data.data);
  606. break;
  607. case 'remove':
  608. case 'delete':
  609. this.batchRemove(data.data);
  610. break;
  611. default:
  612. console.warn('未知操作类型:', action);
  613. }
  614. } else {
  615. console.warn('收到未知格式消息:', data);
  616. }
  617. },
  618. // 批量新增
  619. batchAdd(data) {
  620. if (!data) return;
  621. const items = Array.isArray(data) ? data : [data]; // 兼容单条
  622. const validItems = items.filter(item => item && item.id !== undefined);
  623. if (validItems.length === 0) {
  624. console.warn('没有有效数据用于新增', data);
  625. return;
  626. }
  627. // 避免重复添加
  628. const updated = [...this.allData];
  629. validItems.forEach(item => {
  630. const index = updated.findIndex(i => i.id == item.id);
  631. if (index > -1) {
  632. // 已存在 → 更新
  633. updated[index] = {
  634. ...updated[index],
  635. ...item
  636. };
  637. } else {
  638. updated.unshift(item);
  639. }
  640. });
  641. this.allData = updated;
  642. },
  643. // 批量修改
  644. batchUpdate(data) {
  645. if (!data) return;
  646. const items = Array.isArray(data) ? data : [data]; // 支持单条或数组
  647. const updated = [...this.allData];
  648. items.forEach(item => {
  649. if (!item || item.id === undefined) return;
  650. const index = updated.findIndex(i => i.id == item.id);
  651. if (index > -1) {
  652. updated[index] = {
  653. ...updated[index],
  654. ...item
  655. };
  656. }
  657. // else { this.allData.push(item); } // 可选:当作新增
  658. });
  659. this.allData = updated;
  660. },
  661. // 批量删除
  662. batchRemove(data) {
  663. let idsToRemove = [];
  664. if (Array.isArray(data.id)) {
  665. // remove: { id: [1,2,3] }
  666. idsToRemove = data.id;
  667. } else if (data.id !== undefined) {
  668. // remove: { id: 1 }
  669. idsToRemove = [data.id];
  670. } else if (Array.isArray(data.data)) {
  671. // remove: { data: [ {id:1}, {id:2} ] }
  672. idsToRemove = data.data.map(item => item.id).filter(id => id !== undefined);
  673. } else {
  674. idsToRemove = [data.id];
  675. console.warn('无法解析删除指令', data);
  676. return;
  677. }
  678. if (idsToRemove.length === 0) return;
  679. const updated = this.allData.filter(item => !idsToRemove.includes(item.id));
  680. this.allData = updated;
  681. },
  682. updateListWithArray(newList) {
  683. this.allData = []
  684. if (!Array.isArray(newList)) return;
  685. const map = new Map();
  686. newList.forEach(item => {
  687. if (item.id !== undefined) {
  688. map.set(item.id, item);
  689. }
  690. });
  691. const updated = [...this.allData];
  692. // 更新或新增
  693. for (const [id, item] of map.entries()) {
  694. const index = updated.findIndex(i => i.id == id);
  695. if (index > -1) {
  696. updated[index] = item;
  697. } else {
  698. updated.push(item);
  699. }
  700. }
  701. const final = updated.filter(item => map.has(item.id));
  702. this.allData = final;
  703. },
  704. // 断线重连(指数退避)
  705. reconnect() {
  706. if (this.reconnectTimer) return; // 防止重复定时器
  707. this.reconnectTimer = setTimeout(() => {
  708. // console.log('正在尝试重新连接...');
  709. this.connectWebSocket();
  710. this.reconnectTimer = null;
  711. }, 3000);
  712. },
  713. // 启动心跳
  714. startHeartbeat() {
  715. // 清除旧心跳
  716. var that = this
  717. that.stopHeartbeat();
  718. // 发送 ping 的定时器
  719. that.heartbeatTimer = setInterval(() => {
  720. if (that.ws && that.connectionStatus === 'connected') {
  721. // 检查上一次的 pong 是否已收到,未收到则判定为断线
  722. if (!that.isSend && !that.isPongReceived) {
  723. that.heartbeatRate++
  724. console.warn(that.heartbeatRate, '上一次心跳未收到 pong,可能已断线新');
  725. if (that.heartbeatRate >= 3) {
  726. that.ws.close();
  727. return;
  728. }
  729. }
  730. // 发送心跳
  731. that.isPongReceived = false; // 等待 pong
  732. // 设置超时检测:10秒内没收到 pong 就断开
  733. that.heartbeatTimeout = setTimeout(() => {
  734. if (that.isSend && !that.isPongReceived) {
  735. console.warn('⚠️ 心跳超时:未在规定时间内收到 pong,即将重连');
  736. uni.showToast({
  737. title: '连接异常,正在重连...',
  738. icon: 'none',
  739. duration: 2000
  740. });
  741. that.ws.close(); // 触发 onClose → 重连
  742. }
  743. }, that.heartbeatTimeoutTime); // 使用配置的超时时间(如 10000ms)
  744. if (that.isSend) {
  745. that.ws.send({
  746. data: 'PING',
  747. success: () => {
  748. // console.log('ping 已发送');
  749. that.isSend = false
  750. },
  751. fail: (err) => {
  752. console.error('ping 发送失败', err);
  753. that.ws.close();
  754. }
  755. });
  756. }
  757. }
  758. }, that.heartbeatInterval);
  759. // 可选:启动响应超时检测(更严格)
  760. // 通常靠"发 ping 后未收到 pong"即可判断
  761. },
  762. // 停止心跳(断开连接时调用)
  763. stopHeartbeat() {
  764. if (this.heartbeatTimer) {
  765. clearInterval(this.heartbeatTimer);
  766. this.heartbeatTimer = null;
  767. }
  768. if (this.heartbeatTimeout) {
  769. clearTimeout(this.heartbeatTimeout);
  770. this.heartbeatTimeout = null;
  771. }
  772. },
  773. // 格式化时间
  774. formatTime(dateTimeStr) {
  775. // 创建 Date 对象
  776. const date = new Date(dateTimeStr);
  777. // 提取小时和分钟,并格式化为两位数
  778. const hours = String(date.getHours()).padStart(2, '0');
  779. const minutes = String(date.getMinutes()).padStart(2, '0');
  780. const time = `${hours}:${minutes}`;
  781. return time
  782. },
  783. // 上一页
  784. prevPage() {
  785. if (this.currentPage > 1) {
  786. this.currentPage--
  787. this.resetAutoScroll()
  788. }
  789. },
  790. // 下一页
  791. nextPage() {
  792. if (this.currentPage < this.totalPages) {
  793. this.currentPage++
  794. this.resetAutoScroll()
  795. }
  796. },
  797. // 触摸开始
  798. handleTouchStart(e) {
  799. this.touchStartX = e.touches[0].clientX
  800. this.touchStartY = e.touches[0].clientY
  801. this.touchStartTime = Date.now()
  802. this.isTouching = true
  803. // 停止自动滚动
  804. if (this.autoScroll) {
  805. this.stopAutoScroll()
  806. }
  807. },
  808. // 触摸移动
  809. handleTouchMove(e) {
  810. if (!this.isTouching) return
  811. },
  812. // 触摸结束
  813. handleTouchEnd(e) {
  814. if (!this.isTouching) return
  815. const touchEndX = e.changedTouches[0].clientX
  816. const touchEndY = e.changedTouches[0].clientY
  817. const touchEndTime = Date.now()
  818. const deltaX = touchEndX - this.touchStartX
  819. const deltaY = touchEndY - this.touchStartY
  820. const deltaTime = touchEndTime - this.touchStartTime
  821. // 防抖:限制滑动切换频率
  822. const now = Date.now()
  823. if (now - this.lastSwipeTime < 300) {
  824. this.isTouching = false
  825. return
  826. }
  827. // 判断滑动方向
  828. const absDeltaX = Math.abs(deltaX)
  829. const absDeltaY = Math.abs(deltaY)
  830. // 水平滑动优先
  831. if (absDeltaX > 50 && absDeltaX > absDeltaY && deltaTime < 500) {
  832. this.lastSwipeTime = now
  833. if (deltaX > 0) {
  834. // 向右滑动 - 上一页
  835. this.prevPage()
  836. } else {
  837. // 向左滑动 - 下一页
  838. this.nextPage()
  839. }
  840. }
  841. // 垂直滑动
  842. else if (absDeltaY > 50 && absDeltaY > absDeltaX && deltaTime < 500) {
  843. this.lastSwipeTime = now
  844. if (deltaY > 0) {
  845. // 向下滑动 - 上一页
  846. this.prevPage()
  847. } else {
  848. // 向上滑动 - 下一页
  849. this.nextPage()
  850. }
  851. }
  852. this.isTouching = false
  853. // 如果开启了自动滚动,重新启动
  854. if (this.autoScroll) {
  855. this.resetAutoScroll()
  856. }
  857. },
  858. // 添加鼠标滚轮支持(仅H5平台)
  859. //#ifdef H5
  860. addMouseWheelSupport() {
  861. const tableBody = document.querySelector('.table-body')
  862. if (tableBody) {
  863. this.wheelHandler = this.debounce((e) => {
  864. e.preventDefault()
  865. const delta = e.wheelDelta || -e.detail
  866. if (delta < 0) {
  867. // 向下滚动 - 下一页
  868. this.nextPage()
  869. } else {
  870. // 向上滚动 - 上一页
  871. this.prevPage()
  872. }
  873. }, 150)
  874. tableBody.addEventListener('mousewheel', this.wheelHandler, {
  875. passive: false
  876. })
  877. tableBody.addEventListener('DOMMouseScroll', this.wheelHandler, {
  878. passive: false
  879. })
  880. }
  881. },
  882. // 移除鼠标滚轮支持
  883. removeMouseWheelSupport() {
  884. const tableBody = document.querySelector('.table-body')
  885. if (tableBody && this.wheelHandler) {
  886. tableBody.removeEventListener('mousewheel', this.wheelHandler)
  887. tableBody.removeEventListener('DOMMouseScroll', this.wheelHandler)
  888. }
  889. },
  890. //#endif
  891. // 切换自动滚动
  892. toggleAutoScroll(e) {
  893. this.autoScroll = e.detail.value
  894. if (this.autoScroll) {
  895. this.startAutoScroll()
  896. } else {
  897. this.stopAutoScroll()
  898. }
  899. },
  900. // 开始自动滚动
  901. startAutoScroll() {
  902. this.stopAutoScroll()
  903. if (this.autoScroll && this.normalData.length > 0 && this.totalPages > 1) {
  904. this.autoScrollTimer = setInterval(() => {
  905. if (this.currentPage < this.totalPages) {
  906. this.currentPage++
  907. } else {
  908. this.currentPage = 1 // 回到第一页
  909. }
  910. }, this.scrollInterval)
  911. }
  912. },
  913. // 停止自动滚动
  914. stopAutoScroll() {
  915. if (this.autoScrollTimer) {
  916. clearInterval(this.autoScrollTimer)
  917. this.autoScrollTimer = null
  918. }
  919. },
  920. // 重置自动滚动(手动操作后)
  921. resetAutoScroll() {
  922. if (this.autoScroll) {
  923. this.stopAutoScroll()
  924. setTimeout(() => {
  925. this.startAutoScroll()
  926. }, this.scrollInterval)
  927. }
  928. },
  929. // 当前时间
  930. updateCurrentTime() {
  931. const now = new Date();
  932. const year = now.getFullYear();
  933. const month = String(now.getMonth() + 1).padStart(2, '0');
  934. const day = String(now.getDate()).padStart(2, '0');
  935. const hours = String(now.getHours()).padStart(2, '0');
  936. const minutes = String(now.getMinutes()).padStart(2, '0');
  937. const seconds = String(now.getSeconds()).padStart(2, '0');
  938. this.currentTime = `${year}-${month}-${day}`;
  939. this.hhmmss = `${hours}:${minutes}:${seconds}`;
  940. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  941. const weekday = weekdays[now.getDay()];
  942. this.whatDay = weekday
  943. },
  944. },
  945. watch: {
  946. // 监听数据变化
  947. allData: {
  948. handler() {
  949. this.$nextTick(() => {
  950. this.validateCurrentPage()
  951. if (this.autoScroll) {
  952. this.startAutoScroll()
  953. }
  954. })
  955. },
  956. immediate: true
  957. },
  958. // 监听页面大小变化
  959. pageSize() {
  960. this.validateCurrentPage()
  961. },
  962. // 监听特殊状态数据变化
  963. specialStatusData() {
  964. // 特殊状态数据变化时重新计算页面大小
  965. this.$nextTick(() => {
  966. this.calculatePageSize()
  967. })
  968. },
  969. // 重连次数
  970. reconnectionNum: {
  971. handler(newVal, oldVal) {
  972. if (newVal >= 50) {
  973. //#ifdef APP
  974. plus.runtime.restart();
  975. setTimeout(() => {
  976. plus.navigator.closeSplashscreen();
  977. }, 3000);
  978. this.reconnectionNum = 0
  979. //#endif
  980. }
  981. // console.log('对象属性变化', newVal, oldVal);
  982. },
  983. deep: true,
  984. immediate: true
  985. }
  986. }
  987. }
  988. </script>
  989. <style scoped>
  990. .notice-board {
  991. width: 100%;
  992. height: 100vh;
  993. display: flex;
  994. flex-direction: column;
  995. background-color: #0d54ec;
  996. /* padding: 20rpx; */
  997. box-sizing: border-box;
  998. }
  999. .card_logo {
  1000. position: absolute;
  1001. left: 30rpx;
  1002. top: 0;
  1003. bottom: 0;
  1004. display: flex;
  1005. align-items: center;
  1006. cursor: pointer;
  1007. }
  1008. .card_logo:focus {
  1009. border: none !important;
  1010. }
  1011. .board-header {
  1012. /* padding-top: 10rpx; */
  1013. text-align: center;
  1014. /* margin-bottom: 30rpx; */
  1015. position: relative;
  1016. display: flex;
  1017. align-items: center;
  1018. justify-content: center;
  1019. height: 130rpx;
  1020. flex-shrink: 0;
  1021. background-color: #0d54ec;
  1022. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
  1023. }
  1024. .card_yellow {
  1025. display: flex;
  1026. align-items: center;
  1027. justify-content: center;
  1028. width: fit-content;
  1029. width: -webkit-fit-content;
  1030. width: -moz-fit-content;
  1031. padding: 0rpx 40rpx;
  1032. height: 130rpx;
  1033. background-color: #e5cb8d;
  1034. border-bottom-right-radius: 300rpx;
  1035. border-top-left-radius: 300rpx;
  1036. }
  1037. .card_board {
  1038. display: flex;
  1039. align-items: center;
  1040. justify-content: center;
  1041. width: fit-content;
  1042. width: -webkit-fit-content;
  1043. width: -moz-fit-content;
  1044. padding: 0rpx 140rpx;
  1045. height: 130rpx;
  1046. background-color: #d5f3ff;
  1047. border-bottom-right-radius: 120rpx;
  1048. border-top-left-radius: 120rpx;
  1049. }
  1050. .board-title {
  1051. font-size: 60rpx;
  1052. font-weight: bold;
  1053. color: #0e6699;
  1054. }
  1055. .card_time {
  1056. position: absolute;
  1057. right: 30rpx;
  1058. top: 0;
  1059. bottom: 0;
  1060. display: flex;
  1061. flex-direction: column;
  1062. align-items: flex-end;
  1063. justify-content: center;
  1064. }
  1065. .time_title {
  1066. font-size: 60rpx;
  1067. color: #fff;
  1068. font-weight: bold;
  1069. line-height: 60rpx;
  1070. }
  1071. .current-time {
  1072. display: flex;
  1073. align-items: center;
  1074. display: flex;
  1075. font-size: 40rpx;
  1076. color: #fff;
  1077. font-weight: bold;
  1078. line-height: 45rpx;
  1079. }
  1080. .table-container {
  1081. flex: 1;
  1082. margin: 20rpx 20rpx 0rpx 20rpx;
  1083. border-radius: 24rpx;
  1084. overflow: hidden;
  1085. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
  1086. background-color: #ffffff;
  1087. display: flex;
  1088. flex-direction: column;
  1089. }
  1090. .table-header {
  1091. background-color: #71a0ee;
  1092. color: white;
  1093. font-weight: bold;
  1094. }
  1095. .table-row {
  1096. display: flex;
  1097. border-bottom: 2rpx solid #dedede;
  1098. }
  1099. .header-row {
  1100. /* background-color: #71a0ee; */
  1101. color: white;
  1102. font-weight: bold;
  1103. }
  1104. .table-cell {
  1105. padding: 24rpx 16rpx;
  1106. font-size: 50rpx;
  1107. text-align: center;
  1108. color: #333;
  1109. display: flex;
  1110. justify-content: center;
  1111. align-items: center;
  1112. }
  1113. .cell {
  1114. padding: 24rpx 16rpx;
  1115. font-size: 50rpx;
  1116. text-align: center;
  1117. color: #333;
  1118. }
  1119. .name {
  1120. width: 25%;
  1121. white-space: nowrap;
  1122. text-overflow: ellipsis;
  1123. overflow: hidden;
  1124. display: flex;
  1125. align-items: center;
  1126. justify-content: center;
  1127. }
  1128. .time {
  1129. width: 25%;
  1130. display: flex;
  1131. align-items: center;
  1132. justify-content: center;
  1133. }
  1134. .status {
  1135. width: 25%;
  1136. }
  1137. .title_color {
  1138. color: #fff;
  1139. }
  1140. /* 特殊状态数据容器 */
  1141. .special-status-container {
  1142. background-color: #fff8e6;
  1143. }
  1144. .special-row {
  1145. background-color: #fff8e6 !important;
  1146. min-height: 60rpx;
  1147. line-height: 60rpx;
  1148. }
  1149. /* 分隔线 */
  1150. .divider {
  1151. height: 1rpx;
  1152. background-color: #ddd;
  1153. }
  1154. /* 普通数据内容区域 */
  1155. .table-body {
  1156. flex: 1;
  1157. overflow-y: auto;
  1158. /* 添加触摸反馈 */
  1159. -webkit-overflow-scrolling: touch;
  1160. touch-action: pan-y;
  1161. }
  1162. .title-observing {
  1163. color: #007bff;
  1164. }
  1165. .title-completed {
  1166. color: #28a745;
  1167. }
  1168. .title-warning {
  1169. color: #ff0000;
  1170. }
  1171. .title-hasleft {
  1172. color: #fa8c16;
  1173. }
  1174. /* 状态行样式 */
  1175. .status-observing {
  1176. background-color: #9dd6ff;
  1177. color: #007bff;
  1178. border: 2rpx solid #48a0ff;
  1179. }
  1180. .status-completed {
  1181. background-color: #e8f5e8;
  1182. color: #28a745;
  1183. border: 2rpx solid #8bc34a;
  1184. }
  1185. .status-warning {
  1186. background-color: #ffebee;
  1187. color: #ff0000;
  1188. border: 2rpx solid #ef9a9a;
  1189. }
  1190. .status-hasleft {
  1191. background-color: #fff2e8;
  1192. color: #fa8c16;
  1193. border: 2rpx solid #ffbb96;
  1194. }
  1195. .item-observing {
  1196. background-color: #9dd6ff;
  1197. }
  1198. .item-completed {
  1199. background-color: #e8f5e8;
  1200. }
  1201. .item-warning {
  1202. background-color: #ffebee;
  1203. }
  1204. .item-hasleft {
  1205. background-color: #fff2e8;
  1206. }
  1207. /* 状态标签样式 - 控制高度不超过40rpx */
  1208. .status-tag {
  1209. flex: none;
  1210. display: flex;
  1211. align-items: center;
  1212. justify-content: center;
  1213. padding: 8rpx 20rpx;
  1214. border-radius: 32rpx;
  1215. font-size: 48rpx;
  1216. font-weight: 500;
  1217. width: calc(100% - 20rpx);
  1218. }
  1219. .empty-tip {
  1220. text-align: center;
  1221. padding: 80rpx;
  1222. color: #999;
  1223. font-size: 60rpx;
  1224. font-style: italic;
  1225. }
  1226. /* 底部 */
  1227. .card_foot {
  1228. display: flex;
  1229. align-items: center;
  1230. height: 120rpx;
  1231. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
  1232. }
  1233. .card_tips_box {
  1234. display: flex;
  1235. align-items: center;
  1236. padding-left: 15rpx;
  1237. }
  1238. .title_tips {
  1239. color: #fff;
  1240. font-size: 40rpx;
  1241. }
  1242. .title_tips_foot {
  1243. color: #fff;
  1244. flex: 1;
  1245. font-size: 40rpx;
  1246. border-radius: 20rpx;
  1247. margin-right: 20rpx;
  1248. background-color: #71a0ee;
  1249. padding: 20rpx;
  1250. }
  1251. .box_link_set {
  1252. position: fixed;
  1253. display: flex;
  1254. align-items: center;
  1255. justify-content: center;
  1256. width: 100%;
  1257. height: 100%;
  1258. background-color: rgba(0, 0, 0, 0.3);
  1259. }
  1260. .box_popup {
  1261. position: relative;
  1262. background-color: #ffffff;
  1263. border-radius: 8rpx;
  1264. }
  1265. .head_popup_title {
  1266. position: relative;
  1267. display: flex;
  1268. align-items: center;
  1269. justify-content: center;
  1270. }
  1271. .title_head_popup {
  1272. font-size: 40rpx;
  1273. font-weight: 500;
  1274. padding: 30rpx 30rpx 0rpx 30rpx;
  1275. }
  1276. .close_title {
  1277. position: absolute;
  1278. right: 30rpx;
  1279. font-size: 40rpx;
  1280. cursor: pointer;
  1281. }
  1282. /* // 连接状态栏 */
  1283. .status-bar-bottom {
  1284. display: flex;
  1285. flex-direction: column;
  1286. margin: 30rpx;
  1287. padding: 24rpx 30rpx;
  1288. background-color: #ffffff;
  1289. box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
  1290. border-radius: 24rpx;
  1291. display: flex;
  1292. gap: 16rpx;
  1293. }
  1294. .status-text-box {
  1295. display: flex;
  1296. align-items: center;
  1297. font-size: 28rpx;
  1298. }
  1299. .dot {
  1300. color: #d32f2f;
  1301. font-size: 24rpx;
  1302. }
  1303. .dot-connecting {
  1304. color: #f9ae3d;
  1305. }
  1306. .dot-connected {
  1307. color: #28a745;
  1308. }
  1309. .dot-disconnected {
  1310. color: #d32f2f;
  1311. }
  1312. .status-text {
  1313. color: #555;
  1314. }
  1315. .status-text {
  1316. margin-left: 16rpx;
  1317. }
  1318. .title_color {
  1319. color: #fff;
  1320. }
  1321. /* // IP 输入区域 */
  1322. .ip-input-group {
  1323. display: flex;
  1324. align-items: center;
  1325. margin-left: 16rpx;
  1326. flex-wrap: wrap;
  1327. gap: 12rpx;
  1328. }
  1329. .ip-label {
  1330. font-size: 28rpx;
  1331. color: #555;
  1332. }
  1333. .ip-input,
  1334. .port-input {
  1335. border: 2rpx solid #ddd;
  1336. border-radius: 12rpx;
  1337. padding: 12rpx 20rpx;
  1338. font-size: 28rpx;
  1339. width: 200rpx;
  1340. }
  1341. .port-input {
  1342. width: 160rpx;
  1343. }
  1344. .colon {
  1345. font-size: 32rpx;
  1346. color: #666;
  1347. margin: 0 8rpx;
  1348. }
  1349. .btn-reconnect {
  1350. background-color: #007aff;
  1351. color: white;
  1352. font-size: 24rpx;
  1353. padding: 0 16rpx;
  1354. border-radius: 12rpx;
  1355. }
  1356. .logo_image {
  1357. width: 80rpx;
  1358. height: 80rpx;
  1359. }
  1360. .logo_title {
  1361. font-size: 50rpx;
  1362. color: #fff;
  1363. font-weight: bold;
  1364. margin-left: 15rpx;
  1365. }
  1366. .tips_imageil {
  1367. width: 70rpx;
  1368. height: 70rpx;
  1369. }
  1370. /* 响应式优化 */
  1371. @media screen and (max-height: 600px) {
  1372. .card_logo {
  1373. left: 10rpx;
  1374. }
  1375. .logo_title {
  1376. font-size: 18rpx;
  1377. margin-left: 5rpx;
  1378. }
  1379. .logo_image {
  1380. width: 35rpx;
  1381. height: 35rpx;
  1382. }
  1383. .table-row {
  1384. min-height: 35rpx;
  1385. line-height: 35rpx;
  1386. padding: 4rpx 0rpx;
  1387. }
  1388. .table-cell {
  1389. font-size: 22rpx;
  1390. padding: 30rpx 10rpx;
  1391. }
  1392. .status-tag {
  1393. padding: 4rpx 10rpx;
  1394. font-size: 18rpx;
  1395. height: 22rpx;
  1396. min-width: 80rpx;
  1397. }
  1398. .control-btn {
  1399. height: 50rpx;
  1400. line-height: 50rpx;
  1401. font-size: 22rpx;
  1402. }
  1403. .board-header {
  1404. height: 50rpx;
  1405. }
  1406. .card_yellow {
  1407. height: 50rpx;
  1408. padding: 0rpx 20rpx;
  1409. }
  1410. .card_board {
  1411. height: 50rpx;
  1412. padding: 0rpx 30rpx;
  1413. }
  1414. .board-title {
  1415. font-size: 20rpx;
  1416. }
  1417. .card_time {
  1418. right: 10rpx;
  1419. }
  1420. .time_title {
  1421. font-size: 20rpx;
  1422. line-height: 20rpx;
  1423. }
  1424. .current-time {
  1425. font-size: 15rpx;
  1426. line-height: 15rpx;
  1427. }
  1428. .table-container {
  1429. margin: 10rpx 10rpx 0rpx 10rpx;
  1430. border-radius: 8rpx;
  1431. }
  1432. .table-cell {
  1433. padding: 2rpx 5rpx;
  1434. font-size: 18rpx;
  1435. }
  1436. .cell {
  1437. padding: 2rpx 5rpx;
  1438. font-size: 18rpx;
  1439. }
  1440. .card_foot {
  1441. height: 50rpx;
  1442. }
  1443. .title_tips {
  1444. font-size: 15rpx;
  1445. }
  1446. .title_tips_foot {
  1447. font-size: 15rpx;
  1448. padding: 2rpx 2rpx 2rpx 5rpx;
  1449. border-radius: 8rpx;
  1450. margin-right: 10rpx;
  1451. }
  1452. .tips_imageil {
  1453. width: 30rpx;
  1454. height: 30rpx;
  1455. }
  1456. .card_tips_box {
  1457. padding-left: 10rpx;
  1458. }
  1459. .empty-tip {
  1460. padding: 40rpx;
  1461. font-size: 20rpx;
  1462. }
  1463. }
  1464. /* 滚动条样式 */
  1465. .table-body::-webkit-scrollbar {
  1466. width: 4rpx;
  1467. }
  1468. .table-body::-webkit-scrollbar-track {
  1469. background: #f1f1f1;
  1470. border-radius: 6rpx;
  1471. }
  1472. .table-body::-webkit-scrollbar-thumb {
  1473. background: #c1c1c1;
  1474. border-radius: 6rpx;
  1475. }
  1476. .table-body::-webkit-scrollbar-thumb:hover {
  1477. background: #a8a8a8;
  1478. }
  1479. </style>