app-service.js 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755
  1. if (typeof Promise !== "undefined" && !Promise.prototype.finally) {
  2. Promise.prototype.finally = function(callback) {
  3. const promise = this.constructor;
  4. return this.then(
  5. (value) => promise.resolve(callback()).then(() => value),
  6. (reason) => promise.resolve(callback()).then(() => {
  7. throw reason;
  8. })
  9. );
  10. };
  11. }
  12. ;
  13. if (typeof uni !== "undefined" && uni && uni.requireGlobal) {
  14. const global = uni.requireGlobal();
  15. ArrayBuffer = global.ArrayBuffer;
  16. Int8Array = global.Int8Array;
  17. Uint8Array = global.Uint8Array;
  18. Uint8ClampedArray = global.Uint8ClampedArray;
  19. Int16Array = global.Int16Array;
  20. Uint16Array = global.Uint16Array;
  21. Int32Array = global.Int32Array;
  22. Uint32Array = global.Uint32Array;
  23. Float32Array = global.Float32Array;
  24. Float64Array = global.Float64Array;
  25. BigInt64Array = global.BigInt64Array;
  26. BigUint64Array = global.BigUint64Array;
  27. }
  28. ;
  29. if (uni.restoreGlobal) {
  30. uni.restoreGlobal(Vue, weex, plus, setTimeout, clearTimeout, setInterval, clearInterval);
  31. }
  32. (function(vue) {
  33. "use strict";
  34. function requireNativePlugin(name) {
  35. return weex.requireModule(name);
  36. }
  37. function formatAppLog(type, filename, ...args) {
  38. if (uni.__log__) {
  39. uni.__log__(type, filename, ...args);
  40. } else {
  41. console[type].apply(console, [...args, filename]);
  42. }
  43. }
  44. const _imports_0 = "/static/logo.png";
  45. const _imports_1 = "/static/horn.png";
  46. const _export_sfc = (sfc, props) => {
  47. const target = sfc.__vccOpts || sfc;
  48. for (const [key, val] of props) {
  49. target[key] = val;
  50. }
  51. return target;
  52. };
  53. const _sfc_main$3 = {
  54. data() {
  55. return {
  56. currentTime: "",
  57. // 当前时间
  58. hhmmss: "",
  59. // 当前时间
  60. whatDay: "",
  61. //星期几
  62. timeTimer: null,
  63. // 时间更新定时器
  64. // WebSocket 实例
  65. ws: null,
  66. reconnectTimer: null,
  67. heartbeatTimer: null,
  68. // 心跳定时器
  69. heartbeatTimeout: null,
  70. // 心跳响应超时计时器
  71. heartbeatInterval: 3e4,
  72. // 30秒发一次心跳
  73. heartbeatTimeoutTime: 5e3,
  74. // 10秒内未响应视为超时
  75. isPongReceived: true,
  76. // 标记是否收到 pong
  77. // 连接状态
  78. connectionStatus: "connecting",
  79. // connecting, connected, disconnected
  80. statusText: {
  81. observing: "留观中",
  82. completed: "留观完成,可离开",
  83. warning: "提前离开",
  84. hasleft: "已离开"
  85. },
  86. // 可编辑的服务器地址
  87. serverIp: "192.168.0.41",
  88. serverPort: "8811",
  89. // 所有数据留观数据列表
  90. allData: [],
  91. // 当前页码
  92. currentPage: 1,
  93. // 每页显示条数(自适应计算)
  94. pageSize: 10,
  95. // 是否自动滚动
  96. autoScroll: true,
  97. // 自动滚动定时器
  98. autoScrollTimer: null,
  99. // 滚动间隔时间(毫秒)
  100. scrollInterval: 6e3,
  101. // 窗口高度
  102. windowHeight: 0,
  103. // 触摸相关
  104. touchStartX: 0,
  105. touchStartTime: 0,
  106. isTouching: false,
  107. touchStartY: 0,
  108. // 垂直滑动相关
  109. lastSwipeTime: 0,
  110. linkShow: false,
  111. ipArray: [],
  112. keyboardHeight: 0,
  113. now: /* @__PURE__ */ new Date(),
  114. timer: null,
  115. heartbeatRate: 0,
  116. isSend: true,
  117. reconnectionNum: 0
  118. };
  119. },
  120. computed: {
  121. connectionText() {
  122. const {
  123. connectionStatus,
  124. serverIp,
  125. serverPort
  126. } = this;
  127. return {
  128. connecting: `正在连接 ${serverIp}:${serverPort}...`,
  129. connected: `已连接到 ${serverIp}:${serverPort}`,
  130. disconnected: `连接已断开`
  131. }[connectionStatus];
  132. },
  133. // 特殊状态数据(状态1和2)
  134. specialStatusData() {
  135. return this.allData.filter((item) => item.status === 3 || item.status === 4 || item.IsOut == 1);
  136. },
  137. // 普通状态数据(状态0)
  138. normalData() {
  139. return this.allData.filter((item) => item.status === 0 || item.status === 1);
  140. },
  141. // 实际显示的条数(减去缓冲行)
  142. actualPageSize() {
  143. return Math.max(1, this.pageSize - 1);
  144. },
  145. // 总页数(基于普通数据计算)
  146. totalPages() {
  147. if (this.actualPageSize <= 0 || this.normalData.length === 0)
  148. return 1;
  149. return Math.ceil(this.normalData.length / this.actualPageSize);
  150. },
  151. // 显示的普通数据(包含缓冲行)
  152. displayNormalData() {
  153. const start = (this.currentPage - 1) * this.actualPageSize;
  154. const end = Math.min(start + this.pageSize, this.normalData.length);
  155. return this.normalData.slice(start, end).map((item) => {
  156. const expectedTime = new Date(item.outTime);
  157. const timeDiff = expectedTime - this.now;
  158. if (timeDiff > 0) {
  159. const minutes = Math.floor(timeDiff % (3600 * 1e3) / (60 * 1e3));
  160. const seconds = Math.floor(timeDiff % (60 * 1e3) / 1e3);
  161. var fzArr = "";
  162. if (minutes) {
  163. fzArr = `剩余${minutes}分钟`;
  164. } else {
  165. fzArr = `剩余${seconds}秒`;
  166. }
  167. return {
  168. ...item,
  169. remainingTime: fzArr
  170. };
  171. } else {
  172. return {
  173. ...item,
  174. remainingTime: "留观完成,可离开"
  175. };
  176. }
  177. });
  178. }
  179. },
  180. mounted() {
  181. this.reconnectionNum = 0;
  182. this.timer = setInterval(() => {
  183. this.now = /* @__PURE__ */ new Date();
  184. }, 1e3);
  185. this.getIpAddress();
  186. this.initPage();
  187. const lastIp = uni.getStorageSync("serverIp");
  188. const lastPort = uni.getStorageSync("serverPort");
  189. if (lastIp && lastPort) {
  190. this.serverIp = lastIp;
  191. this.serverPort = lastPort;
  192. }
  193. this.updateCurrentTime();
  194. this.timeTimer = setInterval(() => {
  195. this.updateCurrentTime();
  196. }, 1e3);
  197. },
  198. beforeDestroy() {
  199. if (this.timer) {
  200. this.timer = null;
  201. clearInterval(this.timer);
  202. }
  203. this.stopAutoScroll();
  204. uni.offWindowResize(this.handleWindowResize);
  205. if (this.ws) {
  206. this.ws.close();
  207. }
  208. if (this.reconnectTimer) {
  209. clearTimeout(this.reconnectTimer);
  210. }
  211. this.stopHeartbeat();
  212. if (this.timeTimer) {
  213. clearInterval(this.timeTimer);
  214. }
  215. },
  216. methods: {
  217. keyboardheightchange() {
  218. uni.onKeyboardHeightChange((res) => {
  219. this.keyboardHeight = res.height;
  220. });
  221. },
  222. findFirstWebSocketIp(ipList, index = 0, onSuccess, onAllFailed) {
  223. if (index >= ipList.length) {
  224. if (typeof onAllFailed === "function") {
  225. onAllFailed();
  226. }
  227. return;
  228. }
  229. const ip = ipList[index].trim();
  230. const port = this.serverPort;
  231. const url = `ws://${ip}:${port}/`;
  232. if (this.ws) {
  233. this.ws.close();
  234. this.ws = null;
  235. }
  236. this.ws = uni.connectSocket({
  237. url,
  238. success: (res) => {
  239. formatAppLog("log", "at pages/index/home.vue:299", "connectSocket success", res);
  240. },
  241. fail: (err) => {
  242. formatAppLog("error", "at pages/index/home.vue:302", "connectSocket 失败", err);
  243. this.connectionStatus = "disconnected";
  244. if (this.serverIp == ip) {
  245. this.heartbeatRate = 0;
  246. this.reconnectionNum++;
  247. }
  248. this.reconnectionNum++;
  249. this.isSend = true;
  250. this.heartbeatRate = 0;
  251. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  252. }
  253. });
  254. this.ws.onOpen((res) => {
  255. this.serverIp = ip;
  256. uni.setStorageSync("serverIp", this.serverIp);
  257. uni.setStorageSync("serverPort", this.serverPort);
  258. this.connectionStatus = "connected";
  259. this.ws.send({
  260. data: "link"
  261. });
  262. onSuccess(ip);
  263. this.startHeartbeat();
  264. uni.hideLoading();
  265. });
  266. this.ws.onMessage((res) => {
  267. if (res.data === "PONG" || res.data === '{"type":"PONG"}') {
  268. this.isSend = true;
  269. this.heartbeatRate = 0;
  270. this.isPongReceived = true;
  271. if (this.heartbeatTimeout) {
  272. clearTimeout(this.heartbeatTimeout);
  273. this.heartbeatTimeout = null;
  274. }
  275. return;
  276. }
  277. try {
  278. const data = JSON.parse(res.data);
  279. this.handleMessage(data);
  280. } catch (e) {
  281. formatAppLog("warn", "at pages/index/home.vue:354", "非 JSON 消息,已忽略", res.data);
  282. }
  283. });
  284. this.ws.onClose((res) => {
  285. formatAppLog("log", "at pages/index/home.vue:358", "WebSocket 连接关闭", res);
  286. this.connectionStatus = "disconnected";
  287. this.stopHeartbeat();
  288. var reconnectionTime = setTimeout(() => {
  289. clearTimeout(reconnectionTime);
  290. if (this.serverIp == ip)
  291. ;
  292. this.isSend = true;
  293. this.heartbeatRate = 0;
  294. this.reconnectionNum++;
  295. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  296. }, 2e3);
  297. });
  298. this.ws.onError((err) => {
  299. formatAppLog("error", "at pages/index/home.vue:380", "WebSocket 错误", err);
  300. this.connectionStatus = "disconnected";
  301. this.isSend = true;
  302. this.heartbeatRate = 0;
  303. this.reconnectionNum++;
  304. this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
  305. });
  306. },
  307. // 连接设置
  308. linkSet() {
  309. this.linkShow = true;
  310. },
  311. // 关闭弹窗
  312. getClose() {
  313. this.linkShow = false;
  314. },
  315. // 获取ip地址
  316. getIpAddress() {
  317. var deviceFinder = requireNativePlugin("Alikes-NetTools-DeviceFinder");
  318. deviceFinder.scan({}, (res) => {
  319. this.ipArray = res;
  320. this.ipArray.unshift(this.serverIp);
  321. this.startCheck();
  322. });
  323. },
  324. // 启动检查
  325. startCheck() {
  326. var ipArray = [];
  327. ipArray = this.ipArray;
  328. this.findFirstWebSocketIp(
  329. ipArray,
  330. 0,
  331. (ip) => {
  332. this.onDeviceFound(ip);
  333. this.linkShow = false;
  334. },
  335. () => {
  336. this.onNoDevice();
  337. }
  338. );
  339. },
  340. // 找到设备后的逻辑
  341. onDeviceFound(ip) {
  342. uni.showToast({
  343. title: "连接成功",
  344. icon: "success"
  345. });
  346. },
  347. // 无设备可用
  348. onNoDevice() {
  349. uni.showToast({
  350. title: "无设备响应",
  351. icon: "none",
  352. duration: 3e3
  353. });
  354. this.getIpAddress();
  355. },
  356. getClass(status) {
  357. var title = "";
  358. if (status == 0) {
  359. title = "observing";
  360. } else if (status == 1) {
  361. title = "completed";
  362. } else if (status == 3) {
  363. title = "warning";
  364. } else if (status == 4) {
  365. title = "hasleft";
  366. }
  367. return title;
  368. },
  369. // 获取状态文本
  370. getStatusText(item) {
  371. if (item.status == 0) {
  372. return item.remainingTime;
  373. } else if (item.status == 1) {
  374. return this.statusText.completed;
  375. } else if (item.status == 3) {
  376. return this.statusText.warning;
  377. } else if (item.status == 4) {
  378. return this.statusText.hasleft;
  379. }
  380. },
  381. // 初始化页面
  382. initPage() {
  383. this.calculatePageSize();
  384. this.handleWindowResize = this.debounce(this.calculatePageSize, 300);
  385. uni.onWindowResize(this.handleWindowResize);
  386. },
  387. // 计算自适应的页面大小
  388. calculatePageSize() {
  389. try {
  390. const systemInfo = uni.getSystemInfoSync();
  391. this.windowHeight = systemInfo.windowHeight;
  392. const headerHeight = uni.upx2px(130);
  393. const paginationHeight = uni.upx2px(100);
  394. const tableHeaderHeight = uni.upx2px(120);
  395. const specialDataHeight = this.specialStatusData.length > 0 ? uni.upx2px(80 * this.specialStatusData.length) : 0;
  396. const padding = uni.upx2px(0);
  397. const availableHeight = this.windowHeight - headerHeight - paginationHeight - tableHeaderHeight - specialDataHeight - padding;
  398. const rowHeightPx = uni.upx2px(140);
  399. const calculatedPageSize = Math.floor(availableHeight / rowHeightPx);
  400. this.pageSize = Math.max(4, Math.min(30, calculatedPageSize));
  401. this.validateCurrentPage();
  402. } catch (error) {
  403. formatAppLog("error", "at pages/index/home.vue:515", "计算页面大小失败:", error);
  404. this.pageSize = 10;
  405. }
  406. },
  407. // 防抖函数
  408. debounce(func, wait) {
  409. let timeout;
  410. return function executedFunction(...args) {
  411. const later = () => {
  412. clearTimeout(timeout);
  413. func(...args);
  414. };
  415. clearTimeout(timeout);
  416. timeout = setTimeout(later, wait);
  417. };
  418. },
  419. // 验证当前页是否有效
  420. validateCurrentPage() {
  421. if (this.currentPage > this.totalPages && this.totalPages > 0) {
  422. this.currentPage = this.totalPages;
  423. }
  424. if (this.currentPage < 1) {
  425. this.currentPage = 1;
  426. }
  427. },
  428. // 构建 WebSocket URL
  429. getWebSocketUrl() {
  430. return `ws://${this.serverIp}:${this.serverPort}`;
  431. },
  432. onIpInput(value) {
  433. this.serverIp = value.detail.value;
  434. },
  435. // 输入
  436. onPortInput(value) {
  437. this.serverPort = value.detail.value;
  438. },
  439. // 处理重连
  440. handleReconnect() {
  441. uni.showLoading({
  442. title: "正在重连..."
  443. });
  444. this.linkShow = false;
  445. this.connectionStatus = "connecting";
  446. if (this.ws) {
  447. this.ws.close({
  448. success: () => {
  449. this.getIpAddress();
  450. },
  451. fail: () => {
  452. this.getIpAddress();
  453. }
  454. });
  455. } else {
  456. this.getIpAddress();
  457. }
  458. setTimeout(() => {
  459. uni.hideLoading();
  460. }, 2e3);
  461. },
  462. // 处理收到的消息
  463. handleMessage(data) {
  464. if (data.action == "link") {
  465. this.updateListWithArray(data.data);
  466. return;
  467. }
  468. if (typeof data === "object" && data !== null) {
  469. const action = data.action;
  470. if (!action) {
  471. formatAppLog("warn", "at pages/index/home.vue:603", "消息缺少 action 字段", data);
  472. return;
  473. }
  474. switch (action) {
  475. case "add":
  476. case "create":
  477. this.batchAdd(data.data);
  478. break;
  479. case "update":
  480. this.batchUpdate(data.data);
  481. break;
  482. case "remove":
  483. case "delete":
  484. this.batchRemove(data.data);
  485. break;
  486. default:
  487. formatAppLog("warn", "at pages/index/home.vue:619", "未知操作类型:", action);
  488. }
  489. } else {
  490. formatAppLog("warn", "at pages/index/home.vue:622", "收到未知格式消息:", data);
  491. }
  492. },
  493. // 批量新增
  494. batchAdd(data) {
  495. if (!data)
  496. return;
  497. const items = Array.isArray(data) ? data : [data];
  498. const validItems = items.filter((item) => item && item.id !== void 0);
  499. if (validItems.length === 0) {
  500. formatAppLog("warn", "at pages/index/home.vue:631", "没有有效数据用于新增", data);
  501. return;
  502. }
  503. const updated = [...this.allData];
  504. validItems.forEach((item) => {
  505. const index = updated.findIndex((i) => i.id == item.id);
  506. if (index > -1) {
  507. updated[index] = {
  508. ...updated[index],
  509. ...item
  510. };
  511. } else {
  512. updated.unshift(item);
  513. }
  514. });
  515. this.allData = updated;
  516. },
  517. // 批量修改
  518. batchUpdate(data) {
  519. if (!data)
  520. return;
  521. const items = Array.isArray(data) ? data : [data];
  522. const updated = [...this.allData];
  523. items.forEach((item) => {
  524. if (!item || item.id === void 0)
  525. return;
  526. const index = updated.findIndex((i) => i.id == item.id);
  527. if (index > -1) {
  528. updated[index] = {
  529. ...updated[index],
  530. ...item
  531. };
  532. }
  533. });
  534. this.allData = updated;
  535. },
  536. // 批量删除
  537. batchRemove(data) {
  538. let idsToRemove = [];
  539. if (Array.isArray(data.id)) {
  540. idsToRemove = data.id;
  541. } else if (data.id !== void 0) {
  542. idsToRemove = [data.id];
  543. } else if (Array.isArray(data.data)) {
  544. idsToRemove = data.data.map((item) => item.id).filter((id) => id !== void 0);
  545. } else {
  546. idsToRemove = [data.id];
  547. formatAppLog("warn", "at pages/index/home.vue:682", "无法解析删除指令", data);
  548. return;
  549. }
  550. if (idsToRemove.length === 0)
  551. return;
  552. const updated = this.allData.filter((item) => !idsToRemove.includes(item.id));
  553. this.allData = updated;
  554. },
  555. updateListWithArray(newList) {
  556. this.allData = [];
  557. if (!Array.isArray(newList))
  558. return;
  559. const map = /* @__PURE__ */ new Map();
  560. newList.forEach((item) => {
  561. if (item.id !== void 0) {
  562. map.set(item.id, item);
  563. }
  564. });
  565. const updated = [...this.allData];
  566. for (const [id, item] of map.entries()) {
  567. const index = updated.findIndex((i) => i.id == id);
  568. if (index > -1) {
  569. updated[index] = item;
  570. } else {
  571. updated.push(item);
  572. }
  573. }
  574. const final = updated.filter((item) => map.has(item.id));
  575. this.allData = final;
  576. },
  577. // 断线重连(指数退避)
  578. reconnect() {
  579. if (this.reconnectTimer)
  580. return;
  581. this.reconnectTimer = setTimeout(() => {
  582. this.connectWebSocket();
  583. this.reconnectTimer = null;
  584. }, 3e3);
  585. },
  586. // 启动心跳
  587. startHeartbeat() {
  588. var that = this;
  589. that.stopHeartbeat();
  590. that.heartbeatTimer = setInterval(() => {
  591. if (that.ws && that.connectionStatus === "connected") {
  592. if (!that.isSend && !that.isPongReceived) {
  593. that.heartbeatRate++;
  594. formatAppLog("warn", "at pages/index/home.vue:731", that.heartbeatRate, "上一次心跳未收到 pong,可能已断线新");
  595. if (that.heartbeatRate >= 3) {
  596. that.ws.close();
  597. return;
  598. }
  599. }
  600. that.isPongReceived = false;
  601. that.heartbeatTimeout = setTimeout(() => {
  602. if (that.isSend && !that.isPongReceived) {
  603. formatAppLog("warn", "at pages/index/home.vue:742", "⚠️ 心跳超时:未在规定时间内收到 pong,即将重连");
  604. uni.showToast({
  605. title: "连接异常,正在重连...",
  606. icon: "none",
  607. duration: 2e3
  608. });
  609. that.ws.close();
  610. }
  611. }, that.heartbeatTimeoutTime);
  612. if (that.isSend) {
  613. that.ws.send({
  614. data: "PING",
  615. success: () => {
  616. that.isSend = false;
  617. },
  618. fail: (err) => {
  619. formatAppLog("error", "at pages/index/home.vue:759", "ping 发送失败", err);
  620. that.ws.close();
  621. }
  622. });
  623. }
  624. }
  625. }, that.heartbeatInterval);
  626. },
  627. // 停止心跳(断开连接时调用)
  628. stopHeartbeat() {
  629. if (this.heartbeatTimer) {
  630. clearInterval(this.heartbeatTimer);
  631. this.heartbeatTimer = null;
  632. }
  633. if (this.heartbeatTimeout) {
  634. clearTimeout(this.heartbeatTimeout);
  635. this.heartbeatTimeout = null;
  636. }
  637. },
  638. // 格式化时间
  639. formatTime(dateTimeStr) {
  640. const date = new Date(dateTimeStr);
  641. const hours = String(date.getHours()).padStart(2, "0");
  642. const minutes = String(date.getMinutes()).padStart(2, "0");
  643. const time = `${hours}:${minutes}`;
  644. return time;
  645. },
  646. // 上一页
  647. prevPage() {
  648. if (this.currentPage > 1) {
  649. this.currentPage--;
  650. this.resetAutoScroll();
  651. }
  652. },
  653. // 下一页
  654. nextPage() {
  655. if (this.currentPage < this.totalPages) {
  656. this.currentPage++;
  657. this.resetAutoScroll();
  658. }
  659. },
  660. // 触摸开始
  661. handleTouchStart(e) {
  662. this.touchStartX = e.touches[0].clientX;
  663. this.touchStartY = e.touches[0].clientY;
  664. this.touchStartTime = Date.now();
  665. this.isTouching = true;
  666. if (this.autoScroll) {
  667. this.stopAutoScroll();
  668. }
  669. },
  670. // 触摸移动
  671. handleTouchMove(e) {
  672. if (!this.isTouching)
  673. return;
  674. },
  675. // 触摸结束
  676. handleTouchEnd(e) {
  677. if (!this.isTouching)
  678. return;
  679. const touchEndX = e.changedTouches[0].clientX;
  680. const touchEndY = e.changedTouches[0].clientY;
  681. const touchEndTime = Date.now();
  682. const deltaX = touchEndX - this.touchStartX;
  683. const deltaY = touchEndY - this.touchStartY;
  684. const deltaTime = touchEndTime - this.touchStartTime;
  685. const now = Date.now();
  686. if (now - this.lastSwipeTime < 300) {
  687. this.isTouching = false;
  688. return;
  689. }
  690. const absDeltaX = Math.abs(deltaX);
  691. const absDeltaY = Math.abs(deltaY);
  692. if (absDeltaX > 50 && absDeltaX > absDeltaY && deltaTime < 500) {
  693. this.lastSwipeTime = now;
  694. if (deltaX > 0) {
  695. this.prevPage();
  696. } else {
  697. this.nextPage();
  698. }
  699. } else if (absDeltaY > 50 && absDeltaY > absDeltaX && deltaTime < 500) {
  700. this.lastSwipeTime = now;
  701. if (deltaY > 0) {
  702. this.prevPage();
  703. } else {
  704. this.nextPage();
  705. }
  706. }
  707. this.isTouching = false;
  708. if (this.autoScroll) {
  709. this.resetAutoScroll();
  710. }
  711. },
  712. // 添加鼠标滚轮支持(仅H5平台)
  713. // 切换自动滚动
  714. toggleAutoScroll(e) {
  715. this.autoScroll = e.detail.value;
  716. if (this.autoScroll) {
  717. this.startAutoScroll();
  718. } else {
  719. this.stopAutoScroll();
  720. }
  721. },
  722. // 开始自动滚动
  723. startAutoScroll() {
  724. this.stopAutoScroll();
  725. if (this.autoScroll && this.normalData.length > 0 && this.totalPages > 1) {
  726. this.autoScrollTimer = setInterval(() => {
  727. if (this.currentPage < this.totalPages) {
  728. this.currentPage++;
  729. } else {
  730. this.currentPage = 1;
  731. }
  732. }, this.scrollInterval);
  733. }
  734. },
  735. // 停止自动滚动
  736. stopAutoScroll() {
  737. if (this.autoScrollTimer) {
  738. clearInterval(this.autoScrollTimer);
  739. this.autoScrollTimer = null;
  740. }
  741. },
  742. // 重置自动滚动(手动操作后)
  743. resetAutoScroll() {
  744. if (this.autoScroll) {
  745. this.stopAutoScroll();
  746. setTimeout(() => {
  747. this.startAutoScroll();
  748. }, this.scrollInterval);
  749. }
  750. },
  751. // 当前时间
  752. updateCurrentTime() {
  753. const now = /* @__PURE__ */ new Date();
  754. const year = now.getFullYear();
  755. const month = String(now.getMonth() + 1).padStart(2, "0");
  756. const day = String(now.getDate()).padStart(2, "0");
  757. const hours = String(now.getHours()).padStart(2, "0");
  758. const minutes = String(now.getMinutes()).padStart(2, "0");
  759. const seconds = String(now.getSeconds()).padStart(2, "0");
  760. this.currentTime = `${year}-${month}-${day}`;
  761. this.hhmmss = `${hours}:${minutes}:${seconds}`;
  762. const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
  763. const weekday = weekdays[now.getDay()];
  764. this.whatDay = weekday;
  765. }
  766. },
  767. watch: {
  768. // 监听数据变化
  769. allData: {
  770. handler() {
  771. this.$nextTick(() => {
  772. this.validateCurrentPage();
  773. if (this.autoScroll) {
  774. this.startAutoScroll();
  775. }
  776. });
  777. },
  778. immediate: true
  779. },
  780. // 监听页面大小变化
  781. pageSize() {
  782. this.validateCurrentPage();
  783. },
  784. // 监听特殊状态数据变化
  785. specialStatusData() {
  786. this.$nextTick(() => {
  787. this.calculatePageSize();
  788. });
  789. },
  790. reconnectionNum: {
  791. handler(newVal, oldVal) {
  792. if (newVal >= 3) {
  793. plus.runtime.restart();
  794. setTimeout(() => {
  795. plus.navigator.closeSplashscreen();
  796. }, 3e3);
  797. this.reconnectionNum = 0;
  798. }
  799. formatAppLog("log", "at pages/index/home.vue:988", "对象属性变化", newVal, oldVal);
  800. },
  801. deep: true,
  802. immediate: true
  803. }
  804. }
  805. };
  806. function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
  807. return vue.openBlock(), vue.createElementBlock("view", { class: "notice-board" }, [
  808. vue.createElementVNode("view", { class: "board-header" }, [
  809. vue.createElementVNode("view", {
  810. class: "card_logo",
  811. onClick: _cache[0] || (_cache[0] = (...args) => $options.linkSet && $options.linkSet(...args))
  812. }, [
  813. vue.createElementVNode("image", {
  814. class: "logo_image",
  815. src: _imports_0,
  816. mode: ""
  817. }),
  818. vue.createElementVNode("view", { class: "logo_title" }, "观山湖区疾控中心")
  819. ]),
  820. vue.createElementVNode("view", { class: "card_yellow" }, [
  821. vue.createElementVNode("view", { class: "card_board" }, [
  822. vue.createElementVNode("text", { class: "board-title" }, "接种留观等待")
  823. ])
  824. ]),
  825. vue.createElementVNode("view", { class: "card_time" }, [
  826. vue.createElementVNode(
  827. "view",
  828. { class: "time_title" },
  829. vue.toDisplayString($data.hhmmss),
  830. 1
  831. /* TEXT */
  832. ),
  833. vue.createElementVNode(
  834. "view",
  835. { class: "current-time" },
  836. vue.toDisplayString($data.currentTime) + " " + vue.toDisplayString($data.whatDay),
  837. 1
  838. /* TEXT */
  839. )
  840. ])
  841. ]),
  842. vue.createCommentVNode(" 表格容器 "),
  843. vue.createElementVNode("view", { class: "table-container" }, [
  844. vue.createCommentVNode(" 主要表头 "),
  845. vue.createElementVNode("view", { class: "table-header" }, [
  846. vue.createElementVNode("view", { class: "table-row header-row" }, [
  847. vue.createElementVNode("view", { class: "cell name title_color" }, "姓名"),
  848. vue.createElementVNode("view", { class: "cell time title_color" }, "留观时间"),
  849. vue.createElementVNode("view", { class: "cell time title_color" }, "离开时间"),
  850. vue.createElementVNode("view", { class: "cell status title_color" }, "状态")
  851. ])
  852. ]),
  853. vue.createCommentVNode(" 特殊状态数据展示区域(固定) "),
  854. $options.specialStatusData.length > 0 ? (vue.openBlock(), vue.createElementBlock("view", {
  855. key: 0,
  856. class: "special-status-container"
  857. }, [
  858. (vue.openBlock(true), vue.createElementBlock(
  859. vue.Fragment,
  860. null,
  861. vue.renderList($options.specialStatusData, (item, index) => {
  862. return vue.openBlock(), vue.createElementBlock(
  863. "view",
  864. {
  865. class: vue.normalizeClass(["table-row", `item-${$options.getClass(item.status)}`]),
  866. key: item.id
  867. },
  868. [
  869. vue.createElementVNode(
  870. "view",
  871. {
  872. class: vue.normalizeClass(["table-cell name", `title-${$options.getClass(item.status)}`])
  873. },
  874. vue.toDisplayString(item.patientName),
  875. 3
  876. /* TEXT, CLASS */
  877. ),
  878. vue.createElementVNode(
  879. "view",
  880. { class: "table-cell time" },
  881. vue.toDisplayString($options.formatTime(item.createTime)),
  882. 1
  883. /* TEXT */
  884. ),
  885. vue.createElementVNode(
  886. "view",
  887. { class: "table-cell time" },
  888. vue.toDisplayString($options.formatTime(item.outTime)),
  889. 1
  890. /* TEXT */
  891. ),
  892. vue.createElementVNode("view", { class: "table-cell status" }, [
  893. vue.createElementVNode(
  894. "view",
  895. {
  896. class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`])
  897. },
  898. vue.toDisplayString($options.getStatusText(item)),
  899. 3
  900. /* TEXT, CLASS */
  901. )
  902. ])
  903. ],
  904. 2
  905. /* CLASS */
  906. );
  907. }),
  908. 128
  909. /* KEYED_FRAGMENT */
  910. )),
  911. vue.createCommentVNode(" 分隔线 "),
  912. vue.createElementVNode("view", { class: "divider" })
  913. ])) : vue.createCommentVNode("v-if", true),
  914. vue.createCommentVNode(" 普通数据内容区域 "),
  915. vue.createElementVNode(
  916. "view",
  917. {
  918. class: "table-body",
  919. onTouchstart: _cache[1] || (_cache[1] = (...args) => $options.handleTouchStart && $options.handleTouchStart(...args)),
  920. onTouchmove: _cache[2] || (_cache[2] = (...args) => $options.handleTouchMove && $options.handleTouchMove(...args)),
  921. onTouchend: _cache[3] || (_cache[3] = (...args) => $options.handleTouchEnd && $options.handleTouchEnd(...args))
  922. },
  923. [
  924. (vue.openBlock(true), vue.createElementBlock(
  925. vue.Fragment,
  926. null,
  927. vue.renderList($options.displayNormalData, (item, index) => {
  928. return vue.openBlock(), vue.createElementBlock(
  929. "view",
  930. {
  931. class: vue.normalizeClass(["table-row", `item-${$options.getClass(item.status)}`]),
  932. key: item.id
  933. },
  934. [
  935. vue.createElementVNode(
  936. "view",
  937. {
  938. class: vue.normalizeClass(["table-cell name", `title-${$options.getClass(item.status)}`])
  939. },
  940. vue.toDisplayString(item.patientName),
  941. 3
  942. /* TEXT, CLASS */
  943. ),
  944. vue.createElementVNode(
  945. "view",
  946. { class: "table-cell time" },
  947. vue.toDisplayString($options.formatTime(item.createTime)),
  948. 1
  949. /* TEXT */
  950. ),
  951. vue.createElementVNode(
  952. "view",
  953. { class: "table-cell time" },
  954. vue.toDisplayString($options.formatTime(item.outTime)),
  955. 1
  956. /* TEXT */
  957. ),
  958. vue.createElementVNode("view", { class: "table-cell status" }, [
  959. vue.createElementVNode(
  960. "view",
  961. {
  962. class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`])
  963. },
  964. vue.toDisplayString($options.getStatusText(item)),
  965. 3
  966. /* TEXT, CLASS */
  967. )
  968. ])
  969. ],
  970. 2
  971. /* CLASS */
  972. );
  973. }),
  974. 128
  975. /* KEYED_FRAGMENT */
  976. )),
  977. vue.createCommentVNode(" 空数据提示 "),
  978. $options.displayNormalData.length === 0 && $options.specialStatusData.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", {
  979. key: 0,
  980. class: "empty-tip"
  981. }, " 暂无留观人员信息 ")) : vue.createCommentVNode("v-if", true)
  982. ],
  983. 32
  984. /* NEED_HYDRATION */
  985. )
  986. ]),
  987. vue.createElementVNode("view", { class: "card_foot" }, [
  988. vue.createElementVNode("view", { class: "card_tips_box" }, [
  989. vue.createElementVNode("image", {
  990. class: "tips_imageil",
  991. src: _imports_1,
  992. mode: ""
  993. }),
  994. vue.createElementVNode("view", { class: "title_tips" }, "温馨提示:")
  995. ]),
  996. vue.createElementVNode("view", { class: "title_tips_foot" }, "请注意留观30分钟后无不良反应后再离开,谢谢。")
  997. ]),
  998. vue.createCommentVNode(" 连接状态与IP编辑栏 "),
  999. $data.linkShow ? (vue.openBlock(), vue.createElementBlock("view", {
  1000. key: 0,
  1001. class: "box_link_set"
  1002. }, [
  1003. vue.createElementVNode(
  1004. "view",
  1005. {
  1006. class: "box_popup",
  1007. style: vue.normalizeStyle({ bottom: $data.keyboardHeight + "px" })
  1008. },
  1009. [
  1010. vue.createElementVNode("view", { class: "head_popup_title" }, [
  1011. vue.createElementVNode("view", { class: "title_head_popup" }, "连接设置"),
  1012. vue.createElementVNode("view", {
  1013. class: "close_title",
  1014. onClick: _cache[4] || (_cache[4] = (...args) => $options.getClose && $options.getClose(...args))
  1015. }, "×")
  1016. ]),
  1017. vue.createElementVNode(
  1018. "view",
  1019. {
  1020. class: vue.normalizeClass(["status-bar-bottom", `status-${$data.connectionStatus}`])
  1021. },
  1022. [
  1023. vue.createCommentVNode(" IP 编辑区域 "),
  1024. vue.createElementVNode("view", { class: "ip-input-group" }, [
  1025. vue.createElementVNode("text", { class: "ip-label" }, "IP:"),
  1026. vue.createElementVNode("input", {
  1027. type: "text",
  1028. value: $data.serverIp,
  1029. onInput: _cache[5] || (_cache[5] = (...args) => $options.onIpInput && $options.onIpInput(...args)),
  1030. placeholder: "192.168.0.41",
  1031. class: "ip-input",
  1032. onKeyboardheightchange: _cache[6] || (_cache[6] = (...args) => $options.keyboardheightchange && $options.keyboardheightchange(...args))
  1033. }, null, 40, ["value"]),
  1034. vue.createElementVNode("text", { class: "colon" }, ":"),
  1035. vue.createElementVNode("input", {
  1036. type: "number",
  1037. value: $data.serverPort,
  1038. onInput: _cache[7] || (_cache[7] = (...args) => $options.onPortInput && $options.onPortInput(...args)),
  1039. placeholder: "8811",
  1040. class: "port-input",
  1041. onKeyboardheightchange: _cache[8] || (_cache[8] = (...args) => $options.keyboardheightchange && $options.keyboardheightchange(...args))
  1042. }, null, 40, ["value"]),
  1043. vue.createElementVNode("button", {
  1044. class: "btn-reconnect",
  1045. size: "mini",
  1046. onClick: _cache[9] || (_cache[9] = (...args) => $options.handleReconnect && $options.handleReconnect(...args))
  1047. }, " 更新并重连 ")
  1048. ]),
  1049. vue.createElementVNode("view", { class: "status-text-box" }, [
  1050. vue.createElementVNode(
  1051. "text",
  1052. {
  1053. class: vue.normalizeClass(["dot", `dot-${$data.connectionStatus}`])
  1054. },
  1055. "●",
  1056. 2
  1057. /* CLASS */
  1058. ),
  1059. vue.createElementVNode(
  1060. "text",
  1061. { class: "status-text" },
  1062. vue.toDisplayString($options.connectionText),
  1063. 1
  1064. /* TEXT */
  1065. )
  1066. ])
  1067. ],
  1068. 2
  1069. /* CLASS */
  1070. )
  1071. ],
  1072. 4
  1073. /* STYLE */
  1074. )
  1075. ])) : vue.createCommentVNode("v-if", true)
  1076. ]);
  1077. }
  1078. const PagesIndexHome = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$2], ["__scopeId", "data-v-760d994e"], ["__file", "D:/code/baozhida-observation-system/pages/index/home.vue"]]);
  1079. const _sfc_main$2 = {
  1080. data() {
  1081. return {
  1082. currentTime: "",
  1083. // 当前时间
  1084. timeTimer: null,
  1085. // 时间更新定时器
  1086. // WebSocket 实例
  1087. ws: null,
  1088. reconnectTimer: null,
  1089. heartbeatTimer: null,
  1090. // 心跳定时器
  1091. heartbeatTimeout: null,
  1092. // 心跳响应超时计时器
  1093. heartbeatInterval: 3e4,
  1094. // 30秒发一次心跳
  1095. heartbeatTimeoutTime: 1e4,
  1096. // 10秒内未响应视为超时
  1097. isPongReceived: true,
  1098. // 标记是否收到 pong
  1099. // 连接状态
  1100. connectionStatus: "connecting",
  1101. // connecting, connected, disconnected
  1102. // 留观数据列表
  1103. list: [],
  1104. statusText: {
  1105. observing: "留观中",
  1106. completed: "留观完成,可离开",
  1107. warning: "提前离开",
  1108. hasleft: "已离开"
  1109. },
  1110. // 可编辑的服务器地址
  1111. serverIp: "192.168.11.132",
  1112. serverPort: "8811"
  1113. };
  1114. },
  1115. computed: {
  1116. connectionText() {
  1117. const {
  1118. connectionStatus,
  1119. serverIp,
  1120. serverPort
  1121. } = this;
  1122. return {
  1123. connecting: `正在连接 ${serverIp}:${serverPort}...`,
  1124. connected: `已连接到 ${serverIp}:${serverPort}`,
  1125. disconnected: `连接已断开`
  1126. }[connectionStatus];
  1127. },
  1128. // 轮播列表数据
  1129. carouselList() {
  1130. if (this.list.length === 0)
  1131. return [];
  1132. if (this.list.length <= 5)
  1133. return this.list;
  1134. return [...this.list, ...this.list];
  1135. },
  1136. // 是否需要启动轮播动画
  1137. shouldAnimate() {
  1138. return this.list.length > 5;
  1139. }
  1140. },
  1141. methods: {
  1142. getClass(status) {
  1143. var title = "";
  1144. if (status == 0) {
  1145. title = "observing";
  1146. } else if (status == 1) {
  1147. title = "completed";
  1148. } else if (status == 3) {
  1149. title = "warning";
  1150. } else if (status == 4) {
  1151. title = "completed";
  1152. }
  1153. return title;
  1154. },
  1155. getStatusText(status) {
  1156. var title = "";
  1157. if (status == 0) {
  1158. title = this.statusText.observing;
  1159. } else if (status == 1) {
  1160. title = this.statusText.completed;
  1161. } else if (status == 3) {
  1162. title = this.statusText.warning;
  1163. } else if (status == 4) {
  1164. title = this.statusText.hasleft;
  1165. }
  1166. return title;
  1167. },
  1168. // 格式时分
  1169. getTime(dateTimeStr) {
  1170. const date = new Date(dateTimeStr);
  1171. const hours = String(date.getHours()).padStart(2, "0");
  1172. const minutes = String(date.getMinutes()).padStart(2, "0");
  1173. const time = `${hours}:${minutes}`;
  1174. return time;
  1175. },
  1176. // 格式化时间范围:09:00 - 09:30
  1177. formatTimeRange(start, end) {
  1178. return start && end ? `${start} - ${end}` : "--";
  1179. },
  1180. // 输入框事件
  1181. onIpInput(e) {
  1182. this.serverIp = e.detail.value;
  1183. },
  1184. onPortInput(e) {
  1185. this.serverPort = e.detail.value;
  1186. },
  1187. // 构建 WebSocket URL
  1188. getWebSocketUrl() {
  1189. return `ws://${this.serverIp}:${this.serverPort}`;
  1190. },
  1191. // 处理重连
  1192. handleReconnect() {
  1193. uni.showLoading({
  1194. title: "正在重连..."
  1195. });
  1196. this.connectionStatus = "connecting";
  1197. if (this.ws) {
  1198. this.ws.close({
  1199. success: () => {
  1200. formatAppLog("log", "at pages/index/index.vue:180", "旧连接已关闭");
  1201. this.connectWebSocket("link");
  1202. },
  1203. fail: () => {
  1204. this.connectWebSocket("link");
  1205. }
  1206. });
  1207. } else {
  1208. this.connectWebSocket("link");
  1209. }
  1210. setTimeout(() => {
  1211. uni.hideLoading();
  1212. }, 2e3);
  1213. },
  1214. // 建立 WebSocket 连接
  1215. connectWebSocket(type) {
  1216. const url = this.getWebSocketUrl();
  1217. this.ws = uni.connectSocket({
  1218. url,
  1219. success: (res) => {
  1220. formatAppLog("log", "at pages/index/index.vue:201", "connectSocket success", res);
  1221. },
  1222. fail: (err) => {
  1223. formatAppLog("error", "at pages/index/index.vue:204", "connectSocket 失败", err);
  1224. this.connectionStatus = "disconnected";
  1225. this.reconnect();
  1226. }
  1227. });
  1228. this.ws.onOpen((res) => {
  1229. this.connectionStatus = "connected";
  1230. this.ws.send({
  1231. data: type
  1232. });
  1233. this.startHeartbeat();
  1234. uni.hideLoading();
  1235. });
  1236. this.ws.onMessage((res) => {
  1237. if (res.data === "PONG" || res.data === '{"type":"PONG"}') {
  1238. this.isPongReceived = true;
  1239. if (this.heartbeatTimeout) {
  1240. clearTimeout(this.heartbeatTimeout);
  1241. this.heartbeatTimeout = null;
  1242. }
  1243. return;
  1244. }
  1245. try {
  1246. const data = JSON.parse(res.data);
  1247. this.handleMessage(data);
  1248. } catch (e) {
  1249. formatAppLog("warn", "at pages/index/index.vue:237", "非 JSON 消息,已忽略", res.data);
  1250. }
  1251. });
  1252. this.ws.onClose((res) => {
  1253. formatAppLog("log", "at pages/index/index.vue:241", "WebSocket 连接关闭", res);
  1254. this.connectionStatus = "disconnected";
  1255. this.stopHeartbeat();
  1256. this.reconnect();
  1257. });
  1258. this.ws.onError((err) => {
  1259. formatAppLog("error", "at pages/index/index.vue:247", "WebSocket 错误", err);
  1260. this.connectionStatus = "disconnected";
  1261. });
  1262. },
  1263. // 处理收到的消息
  1264. handleMessage(data) {
  1265. if (data.action == "link") {
  1266. this.updateListWithArray(data.data);
  1267. return;
  1268. }
  1269. if (typeof data === "object" && data !== null) {
  1270. const action = data.action;
  1271. if (!action) {
  1272. formatAppLog("warn", "at pages/index/index.vue:263", "消息缺少 action 字段", data);
  1273. return;
  1274. }
  1275. switch (action) {
  1276. case "add":
  1277. case "create":
  1278. this.batchAdd(data.data);
  1279. break;
  1280. case "update":
  1281. this.batchUpdate(data.data);
  1282. break;
  1283. case "remove":
  1284. case "delete":
  1285. this.batchRemove(data.data);
  1286. break;
  1287. default:
  1288. formatAppLog("warn", "at pages/index/index.vue:279", "未知操作类型:", action);
  1289. }
  1290. } else {
  1291. formatAppLog("warn", "at pages/index/index.vue:282", "收到未知格式消息:", data);
  1292. }
  1293. },
  1294. // 批量新增
  1295. batchAdd(data) {
  1296. if (!data)
  1297. return;
  1298. const items = Array.isArray(data) ? data : [data];
  1299. const validItems = items.filter((item) => item && item.id !== void 0);
  1300. if (validItems.length === 0) {
  1301. formatAppLog("warn", "at pages/index/index.vue:291", "没有有效数据用于新增", data);
  1302. return;
  1303. }
  1304. const updated = [...this.list];
  1305. validItems.forEach((item) => {
  1306. const index = updated.findIndex((i) => i.id == item.id);
  1307. if (index > -1) {
  1308. updated[index] = {
  1309. ...updated[index],
  1310. ...item
  1311. };
  1312. } else {
  1313. updated.unshift(item);
  1314. }
  1315. });
  1316. this.list = updated;
  1317. },
  1318. // 批量修改
  1319. batchUpdate(data) {
  1320. if (!data)
  1321. return;
  1322. const items = Array.isArray(data) ? data : [data];
  1323. const updated = [...this.list];
  1324. items.forEach((item) => {
  1325. if (!item || item.id === void 0)
  1326. return;
  1327. const index = updated.findIndex((i) => i.id == item.id);
  1328. if (index > -1) {
  1329. updated[index] = {
  1330. ...updated[index],
  1331. ...item
  1332. };
  1333. }
  1334. });
  1335. this.list = updated;
  1336. },
  1337. // 批量删除
  1338. batchRemove(data) {
  1339. let idsToRemove = [];
  1340. if (Array.isArray(data.id)) {
  1341. idsToRemove = data.id;
  1342. } else if (data.id !== void 0) {
  1343. idsToRemove = [data.id];
  1344. } else if (Array.isArray(data.data)) {
  1345. idsToRemove = data.data.map((item) => item.id).filter((id) => id !== void 0);
  1346. } else {
  1347. idsToRemove = [data.id];
  1348. formatAppLog("warn", "at pages/index/index.vue:342", "无法解析删除指令", data);
  1349. return;
  1350. }
  1351. if (idsToRemove.length === 0)
  1352. return;
  1353. const updated = this.list.filter((item) => !idsToRemove.includes(item.id));
  1354. this.list = updated;
  1355. },
  1356. updateListWithArray(newList) {
  1357. if (!Array.isArray(newList))
  1358. return;
  1359. const map = /* @__PURE__ */ new Map();
  1360. newList.forEach((item) => {
  1361. if (item.id !== void 0) {
  1362. map.set(item.id, item);
  1363. }
  1364. });
  1365. const updated = [...this.list];
  1366. for (const [id, item] of map.entries()) {
  1367. const index = updated.findIndex((i) => i.id == id);
  1368. if (index > -1) {
  1369. updated[index] = item;
  1370. } else {
  1371. updated.push(item);
  1372. }
  1373. }
  1374. const final = updated.filter((item) => map.has(item.id));
  1375. this.list = final;
  1376. },
  1377. // 断线重连(指数退避)
  1378. reconnect() {
  1379. if (this.reconnectTimer)
  1380. return;
  1381. this.reconnectTimer = setTimeout(() => {
  1382. formatAppLog("log", "at pages/index/index.vue:377", "正在尝试重新连接...");
  1383. this.connectWebSocket("link");
  1384. this.reconnectTimer = null;
  1385. }, 3e3);
  1386. },
  1387. // 启动心跳
  1388. startHeartbeat() {
  1389. formatAppLog("log", "at pages/index/index.vue:384", 2324);
  1390. this.stopHeartbeat();
  1391. this.heartbeatTimer = setInterval(() => {
  1392. if (this.ws && this.connectionStatus === "connected") {
  1393. if (!this.isPongReceived) {
  1394. formatAppLog("warn", "at pages/index/index.vue:392", "上一次心跳未收到 pong,可能已断线");
  1395. this.ws.close();
  1396. return;
  1397. }
  1398. this.isPongReceived = false;
  1399. this.heartbeatTimeout = setTimeout(() => {
  1400. if (!this.isPongReceived) {
  1401. formatAppLog("warn", "at pages/index/index.vue:401", "⚠️ 心跳超时:未在规定时间内收到 pong,即将重连");
  1402. uni.showToast({
  1403. title: "连接异常,正在重连...",
  1404. icon: "none",
  1405. duration: 2e3
  1406. });
  1407. this.ws.close();
  1408. }
  1409. }, this.heartbeatTimeoutTime);
  1410. try {
  1411. this.ws.send({
  1412. data: "PING",
  1413. success: () => {
  1414. },
  1415. fail: (err) => {
  1416. formatAppLog("error", "at pages/index/index.vue:417", "ping 发送失败", err);
  1417. this.ws.close();
  1418. }
  1419. });
  1420. } catch (e) {
  1421. formatAppLog("error", "at pages/index/index.vue:422", "发送 ping 异常", e);
  1422. this.ws.close();
  1423. }
  1424. }
  1425. }, this.heartbeatInterval);
  1426. },
  1427. // 停止心跳(断开连接时调用)
  1428. stopHeartbeat() {
  1429. if (this.heartbeatTimer) {
  1430. clearInterval(this.heartbeatTimer);
  1431. this.heartbeatTimer = null;
  1432. }
  1433. if (this.heartbeatTimeout) {
  1434. clearTimeout(this.heartbeatTimeout);
  1435. this.heartbeatTimeout = null;
  1436. }
  1437. },
  1438. updateCurrentTime() {
  1439. const now = /* @__PURE__ */ new Date();
  1440. const year = now.getFullYear();
  1441. const month = String(now.getMonth() + 1).padStart(2, "0");
  1442. const day = String(now.getDate()).padStart(2, "0");
  1443. const hours = String(now.getHours()).padStart(2, "0");
  1444. const minutes = String(now.getMinutes()).padStart(2, "0");
  1445. const seconds = String(now.getSeconds()).padStart(2, "0");
  1446. this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  1447. },
  1448. goback() {
  1449. uni.navigateTo({
  1450. url: "/pages/index/home"
  1451. });
  1452. }
  1453. },
  1454. mounted() {
  1455. this.connectWebSocket("link");
  1456. this.updateCurrentTime();
  1457. this.timeTimer = setInterval(() => {
  1458. this.updateCurrentTime();
  1459. }, 1e3);
  1460. },
  1461. // 页面卸载时关闭连接
  1462. beforeDestroy() {
  1463. if (this.ws) {
  1464. this.ws.close();
  1465. }
  1466. if (this.reconnectTimer) {
  1467. clearTimeout(this.reconnectTimer);
  1468. }
  1469. this.stopHeartbeat();
  1470. if (this.timeTimer) {
  1471. clearInterval(this.timeTimer);
  1472. }
  1473. }
  1474. };
  1475. function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
  1476. return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
  1477. vue.createElementVNode("view", { class: "header" }, [
  1478. vue.createElementVNode("text", { class: "title" }, "接种留观人员信息表"),
  1479. vue.createElementVNode(
  1480. "text",
  1481. { class: "current-time" },
  1482. vue.toDisplayString($data.currentTime),
  1483. 1
  1484. /* TEXT */
  1485. )
  1486. ]),
  1487. vue.createCommentVNode(" 表格 "),
  1488. vue.createElementVNode("view", { class: "table-container" }, [
  1489. vue.createCommentVNode(" 表头 "),
  1490. vue.createElementVNode("view", { class: "table-header" }, [
  1491. vue.createElementVNode("view", { class: "table-row header-row" }, [
  1492. vue.createElementVNode("view", { class: "cell name title_color" }, "姓名"),
  1493. vue.createElementVNode("view", { class: "cell time title_color" }, "留观时间"),
  1494. vue.createElementVNode("view", { class: "cell time title_color" }, "离开时间"),
  1495. vue.createElementVNode("view", { class: "cell status title_color" }, "状态")
  1496. ])
  1497. ]),
  1498. vue.createCommentVNode(" 表格主体 - 支持纵向轮播滚动 "),
  1499. vue.createElementVNode("view", { class: "table-body-container" }, [
  1500. vue.createElementVNode(
  1501. "view",
  1502. {
  1503. class: vue.normalizeClass(["table-body-wrapper", { "animate": $options.shouldAnimate }])
  1504. },
  1505. [
  1506. (vue.openBlock(true), vue.createElementBlock(
  1507. vue.Fragment,
  1508. null,
  1509. vue.renderList($options.carouselList, (item, index) => {
  1510. return vue.openBlock(), vue.createElementBlock(
  1511. "view",
  1512. {
  1513. key: index,
  1514. class: vue.normalizeClass(["table-row body-row", `item-${$options.getClass(item.status)}`])
  1515. },
  1516. [
  1517. vue.createElementVNode(
  1518. "view",
  1519. { class: "cell name" },
  1520. vue.toDisplayString(item.patientName),
  1521. 1
  1522. /* TEXT */
  1523. ),
  1524. vue.createElementVNode(
  1525. "view",
  1526. { class: "cell time" },
  1527. vue.toDisplayString($options.getTime(item.createTime)),
  1528. 1
  1529. /* TEXT */
  1530. ),
  1531. vue.createElementVNode(
  1532. "view",
  1533. { class: "cell time" },
  1534. vue.toDisplayString($options.getTime(item.outTime)),
  1535. 1
  1536. /* TEXT */
  1537. ),
  1538. vue.createElementVNode("view", { class: "cell status" }, [
  1539. vue.createElementVNode(
  1540. "text",
  1541. {
  1542. class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`])
  1543. },
  1544. vue.toDisplayString($options.getStatusText(item.status)),
  1545. 3
  1546. /* TEXT, CLASS */
  1547. )
  1548. ])
  1549. ],
  1550. 2
  1551. /* CLASS */
  1552. );
  1553. }),
  1554. 128
  1555. /* KEYED_FRAGMENT */
  1556. ))
  1557. ],
  1558. 2
  1559. /* CLASS */
  1560. )
  1561. ])
  1562. ]),
  1563. vue.createCommentVNode(" 连接状态与IP编辑栏 "),
  1564. vue.createElementVNode(
  1565. "view",
  1566. {
  1567. class: vue.normalizeClass(["status-bar-bottom", `status-${$data.connectionStatus}`])
  1568. },
  1569. [
  1570. vue.createElementVNode("view", { class: "status-text-box" }, [
  1571. vue.createElementVNode(
  1572. "text",
  1573. {
  1574. class: vue.normalizeClass(["dot", `dot-${$data.connectionStatus}`])
  1575. },
  1576. "●",
  1577. 2
  1578. /* CLASS */
  1579. ),
  1580. vue.createElementVNode(
  1581. "text",
  1582. { class: "status-text" },
  1583. vue.toDisplayString($options.connectionText),
  1584. 1
  1585. /* TEXT */
  1586. )
  1587. ]),
  1588. vue.createCommentVNode(" IP 编辑区域 "),
  1589. vue.createElementVNode("view", { class: "ip-input-group" }, [
  1590. vue.createElementVNode("text", { class: "ip-label" }, "IP:"),
  1591. vue.createElementVNode("input", {
  1592. type: "text",
  1593. value: $data.serverIp,
  1594. onInput: _cache[0] || (_cache[0] = (...args) => $options.onIpInput && $options.onIpInput(...args)),
  1595. placeholder: "192.168.0.41",
  1596. class: "ip-input"
  1597. }, null, 40, ["value"]),
  1598. vue.createElementVNode("text", { class: "colon" }, ":"),
  1599. vue.createElementVNode("input", {
  1600. type: "number",
  1601. value: $data.serverPort,
  1602. onInput: _cache[1] || (_cache[1] = (...args) => $options.onPortInput && $options.onPortInput(...args)),
  1603. placeholder: "8811",
  1604. class: "port-input"
  1605. }, null, 40, ["value"]),
  1606. vue.createElementVNode("button", {
  1607. class: "btn-reconnect",
  1608. size: "mini",
  1609. onClick: _cache[2] || (_cache[2] = (...args) => $options.handleReconnect && $options.handleReconnect(...args))
  1610. }, " 更新并重连 "),
  1611. vue.createElementVNode("button", {
  1612. class: "btn-reconnect",
  1613. size: "mini",
  1614. onClick: _cache[3] || (_cache[3] = (...args) => $options.goback && $options.goback(...args))
  1615. }, " 测试 ")
  1616. ])
  1617. ],
  1618. 2
  1619. /* CLASS */
  1620. )
  1621. ]);
  1622. }
  1623. const PagesIndexIndex = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$1], ["__scopeId", "data-v-1cf27b2a"], ["__file", "D:/code/baozhida-observation-system/pages/index/index.vue"]]);
  1624. const _sfc_main$1 = {
  1625. data() {
  1626. return {
  1627. serverIp: uni.getStorageSync("serverIp") || "",
  1628. scanning: false
  1629. };
  1630. },
  1631. methods: {
  1632. async scan() {
  1633. if (this.scanning)
  1634. return;
  1635. this.scanning = true;
  1636. const ip = await this.scanNetwork();
  1637. if (ip) {
  1638. this.serverIp = ip;
  1639. uni.showToast({ title: "发现: " + ip });
  1640. } else {
  1641. uni.showToast({ icon: "none", title: "未发现" });
  1642. }
  1643. this.scanning = false;
  1644. },
  1645. async scanNetwork() {
  1646. const lastIp = uni.getStorageSync("serverIp");
  1647. if (lastIp && await this.testIp(lastIp)) {
  1648. return lastIp;
  1649. }
  1650. const prefix = this.getNetworkPrefix();
  1651. for (let i = 1; i <= 50; i++) {
  1652. const ip = `${prefix}${i}`;
  1653. if (await this.testIp(ip)) {
  1654. return ip;
  1655. }
  1656. await this.delay(100);
  1657. }
  1658. return null;
  1659. },
  1660. testIp(ip) {
  1661. return new Promise((resolve) => {
  1662. uni.request({
  1663. url: `http://${ip}:8811/ping`,
  1664. timeout: 3e3,
  1665. success: (res) => {
  1666. resolve(res.statusCode === 200 ? ip : null);
  1667. },
  1668. fail: () => resolve(null)
  1669. });
  1670. });
  1671. },
  1672. getNetworkPrefix() {
  1673. let prefix = "192.168.1.";
  1674. if (typeof plus !== "undefined" && plus.networkinfo) {
  1675. const ip = plus.networkinfo.getIPAddress();
  1676. if (ip)
  1677. prefix = ip.replace(/\.\d+$/, ".") + ".";
  1678. }
  1679. return prefix;
  1680. },
  1681. delay(ms) {
  1682. return new Promise((resolve) => setTimeout(resolve, ms));
  1683. },
  1684. connect() {
  1685. if (!this.serverIp) {
  1686. uni.showToast({ icon: "none", title: "请先设置IP" });
  1687. return;
  1688. }
  1689. uni.setStorageSync("serverIp", this.serverIp);
  1690. uni.connectSocket({ url: `ws://${this.serverIp}:8811` });
  1691. uni.onSocketOpen(() => {
  1692. "link";
  1693. });
  1694. }
  1695. }
  1696. };
  1697. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  1698. return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
  1699. vue.createElementVNode("text", null, "服务端IP:"),
  1700. vue.withDirectives(vue.createElementVNode(
  1701. "input",
  1702. {
  1703. "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => $data.serverIp = $event),
  1704. placeholder: "192.168.1.100"
  1705. },
  1706. null,
  1707. 512
  1708. /* NEED_PATCH */
  1709. ), [
  1710. [vue.vModelText, $data.serverIp]
  1711. ]),
  1712. vue.createElementVNode("button", {
  1713. onClick: _cache[1] || (_cache[1] = (...args) => $options.scan && $options.scan(...args))
  1714. }, "🔍 自动扫描"),
  1715. vue.createElementVNode("button", {
  1716. onClick: _cache[2] || (_cache[2] = (...args) => $options.connect && $options.connect(...args))
  1717. }, "🚀 连接"),
  1718. $data.scanning ? (vue.openBlock(), vue.createElementBlock("text", { key: 0 }, "扫描中...")) : vue.createCommentVNode("v-if", true)
  1719. ]);
  1720. }
  1721. const PagesIndexMine = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__file", "D:/code/baozhida-observation-system/pages/index/mine.vue"]]);
  1722. __definePage("pages/index/home", PagesIndexHome);
  1723. __definePage("pages/index/index", PagesIndexIndex);
  1724. __definePage("pages/index/mine", PagesIndexMine);
  1725. const _sfc_main = {
  1726. mounted() {
  1727. plus.screen.lockOrientation("landscape-primary");
  1728. },
  1729. onLaunch: function() {
  1730. formatAppLog("log", "at App.vue:11", "App Launch");
  1731. plus.screen.lockOrientation("landscape-primary");
  1732. },
  1733. onShow: function() {
  1734. formatAppLog("log", "at App.vue:18", "App Show");
  1735. },
  1736. onHide: function() {
  1737. formatAppLog("log", "at App.vue:21", "App Hide");
  1738. }
  1739. };
  1740. const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "D:/code/baozhida-observation-system/App.vue"]]);
  1741. function createApp() {
  1742. const app = vue.createVueApp(App);
  1743. return {
  1744. app
  1745. };
  1746. }
  1747. const { app: __app__, Vuex: __Vuex__, Pinia: __Pinia__ } = createApp();
  1748. uni.Vuex = __Vuex__;
  1749. uni.Pinia = __Pinia__;
  1750. __app__.provide("__globalStyles", __uniConfig.styles);
  1751. __app__._component.mpType = "app";
  1752. __app__._component.render = () => {
  1753. };
  1754. __app__.mount("#app");
  1755. })(Vue);