home.vue 39 KB

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