if (typeof Promise !== "undefined" && !Promise.prototype.finally) { Promise.prototype.finally = function(callback) { const promise = this.constructor; return this.then( (value) => promise.resolve(callback()).then(() => value), (reason) => promise.resolve(callback()).then(() => { throw reason; }) ); }; } ; if (typeof uni !== "undefined" && uni && uni.requireGlobal) { const global = uni.requireGlobal(); ArrayBuffer = global.ArrayBuffer; Int8Array = global.Int8Array; Uint8Array = global.Uint8Array; Uint8ClampedArray = global.Uint8ClampedArray; Int16Array = global.Int16Array; Uint16Array = global.Uint16Array; Int32Array = global.Int32Array; Uint32Array = global.Uint32Array; Float32Array = global.Float32Array; Float64Array = global.Float64Array; BigInt64Array = global.BigInt64Array; BigUint64Array = global.BigUint64Array; } ; if (uni.restoreGlobal) { uni.restoreGlobal(Vue, weex, plus, setTimeout, clearTimeout, setInterval, clearInterval); } (function(vue) { "use strict"; function requireNativePlugin(name) { return weex.requireModule(name); } function formatAppLog(type, filename, ...args) { if (uni.__log__) { uni.__log__(type, filename, ...args); } else { console[type].apply(console, [...args, filename]); } } const _imports_0 = "/static/logo.png"; const _imports_1 = "/static/horn.png"; const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main$3 = { data() { return { currentTime: "", // 当前时间 hhmmss: "", // 当前时间 whatDay: "", //星期几 timeTimer: null, // 时间更新定时器 // WebSocket 实例 ws: null, reconnectTimer: null, heartbeatTimer: null, // 心跳定时器 heartbeatTimeout: null, // 心跳响应超时计时器 heartbeatInterval: 3e4, // 30秒发一次心跳 heartbeatTimeoutTime: 5e3, // 10秒内未响应视为超时 isPongReceived: true, // 标记是否收到 pong // 连接状态 connectionStatus: "connecting", // connecting, connected, disconnected statusText: { observing: "留观中", completed: "留观完成,可离开", warning: "提前离开", hasleft: "已离开" }, // 可编辑的服务器地址 serverIp: "192.168.0.41", serverPort: "8811", // 所有数据留观数据列表 allData: [], // 当前页码 currentPage: 1, // 每页显示条数(自适应计算) pageSize: 10, // 是否自动滚动 autoScroll: true, // 自动滚动定时器 autoScrollTimer: null, // 滚动间隔时间(毫秒) scrollInterval: 6e3, // 窗口高度 windowHeight: 0, // 触摸相关 touchStartX: 0, touchStartTime: 0, isTouching: false, touchStartY: 0, // 垂直滑动相关 lastSwipeTime: 0, linkShow: false, ipArray: [], keyboardHeight: 0, now: /* @__PURE__ */ new Date(), timer: null, heartbeatRate: 0, isSend: true, reconnectionNum: 0 }; }, computed: { connectionText() { const { connectionStatus, serverIp, serverPort } = this; return { connecting: `正在连接 ${serverIp}:${serverPort}...`, connected: `已连接到 ${serverIp}:${serverPort}`, disconnected: `连接已断开` }[connectionStatus]; }, // 特殊状态数据(状态1和2) specialStatusData() { return this.allData.filter((item) => item.status === 3 || item.status === 4 || item.IsOut == 1); }, // 普通状态数据(状态0) normalData() { return this.allData.filter((item) => item.status === 0 || item.status === 1); }, // 实际显示的条数(减去缓冲行) actualPageSize() { return Math.max(1, this.pageSize - 1); }, // 总页数(基于普通数据计算) totalPages() { if (this.actualPageSize <= 0 || this.normalData.length === 0) return 1; return Math.ceil(this.normalData.length / this.actualPageSize); }, // 显示的普通数据(包含缓冲行) displayNormalData() { const start = (this.currentPage - 1) * this.actualPageSize; const end = Math.min(start + this.pageSize, this.normalData.length); return this.normalData.slice(start, end).map((item) => { const expectedTime = new Date(item.outTime); const timeDiff = expectedTime - this.now; if (timeDiff > 0) { const minutes = Math.floor(timeDiff % (3600 * 1e3) / (60 * 1e3)); const seconds = Math.floor(timeDiff % (60 * 1e3) / 1e3); var fzArr = ""; if (minutes) { fzArr = `剩余${minutes}分钟`; } else { fzArr = `剩余${seconds}秒`; } return { ...item, remainingTime: fzArr }; } else { return { ...item, remainingTime: "留观完成,可离开" }; } }); } }, mounted() { this.reconnectionNum = 0; this.timer = setInterval(() => { this.now = /* @__PURE__ */ new Date(); }, 1e3); this.getIpAddress(); this.initPage(); const lastIp = uni.getStorageSync("serverIp"); const lastPort = uni.getStorageSync("serverPort"); if (lastIp && lastPort) { this.serverIp = lastIp; this.serverPort = lastPort; } this.updateCurrentTime(); this.timeTimer = setInterval(() => { this.updateCurrentTime(); }, 1e3); }, beforeDestroy() { if (this.timer) { this.timer = null; clearInterval(this.timer); } this.stopAutoScroll(); uni.offWindowResize(this.handleWindowResize); if (this.ws) { this.ws.close(); } if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } this.stopHeartbeat(); if (this.timeTimer) { clearInterval(this.timeTimer); } }, methods: { keyboardheightchange() { uni.onKeyboardHeightChange((res) => { this.keyboardHeight = res.height; }); }, findFirstWebSocketIp(ipList, index = 0, onSuccess, onAllFailed) { if (index >= ipList.length) { if (typeof onAllFailed === "function") { onAllFailed(); } return; } const ip = ipList[index].trim(); const port = this.serverPort; const url = `ws://${ip}:${port}/`; if (this.ws) { this.ws.close(); this.ws = null; } this.ws = uni.connectSocket({ url, success: (res) => { formatAppLog("log", "at pages/index/home.vue:299", "connectSocket success", res); }, fail: (err) => { formatAppLog("error", "at pages/index/home.vue:302", "connectSocket 失败", err); this.connectionStatus = "disconnected"; if (this.serverIp == ip) { this.heartbeatRate = 0; this.reconnectionNum++; } this.reconnectionNum++; this.isSend = true; this.heartbeatRate = 0; this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed); } }); this.ws.onOpen((res) => { this.serverIp = ip; uni.setStorageSync("serverIp", this.serverIp); uni.setStorageSync("serverPort", this.serverPort); this.connectionStatus = "connected"; this.ws.send({ data: "link" }); onSuccess(ip); this.startHeartbeat(); uni.hideLoading(); }); this.ws.onMessage((res) => { if (res.data === "PONG" || res.data === '{"type":"PONG"}') { this.isSend = true; this.heartbeatRate = 0; this.isPongReceived = true; if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } return; } try { const data = JSON.parse(res.data); this.handleMessage(data); } catch (e) { formatAppLog("warn", "at pages/index/home.vue:354", "非 JSON 消息,已忽略", res.data); } }); this.ws.onClose((res) => { formatAppLog("log", "at pages/index/home.vue:358", "WebSocket 连接关闭", res); this.connectionStatus = "disconnected"; this.stopHeartbeat(); var reconnectionTime = setTimeout(() => { clearTimeout(reconnectionTime); if (this.serverIp == ip) ; this.isSend = true; this.heartbeatRate = 0; this.reconnectionNum++; this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed); }, 2e3); }); this.ws.onError((err) => { formatAppLog("error", "at pages/index/home.vue:380", "WebSocket 错误", err); this.connectionStatus = "disconnected"; this.isSend = true; this.heartbeatRate = 0; this.reconnectionNum++; this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed); }); }, // 连接设置 linkSet() { this.linkShow = true; }, // 关闭弹窗 getClose() { this.linkShow = false; }, // 获取ip地址 getIpAddress() { var deviceFinder = requireNativePlugin("Alikes-NetTools-DeviceFinder"); deviceFinder.scan({}, (res) => { this.ipArray = res; this.ipArray.unshift(this.serverIp); this.startCheck(); }); }, // 启动检查 startCheck() { var ipArray = []; ipArray = this.ipArray; this.findFirstWebSocketIp( ipArray, 0, (ip) => { this.onDeviceFound(ip); this.linkShow = false; }, () => { this.onNoDevice(); } ); }, // 找到设备后的逻辑 onDeviceFound(ip) { uni.showToast({ title: "连接成功", icon: "success" }); }, // 无设备可用 onNoDevice() { uni.showToast({ title: "无设备响应", icon: "none", duration: 3e3 }); this.getIpAddress(); }, getClass(status) { var title = ""; if (status == 0) { title = "observing"; } else if (status == 1) { title = "completed"; } else if (status == 3) { title = "warning"; } else if (status == 4) { title = "hasleft"; } return title; }, // 获取状态文本 getStatusText(item) { if (item.status == 0) { return item.remainingTime; } else if (item.status == 1) { return this.statusText.completed; } else if (item.status == 3) { return this.statusText.warning; } else if (item.status == 4) { return this.statusText.hasleft; } }, // 初始化页面 initPage() { this.calculatePageSize(); this.handleWindowResize = this.debounce(this.calculatePageSize, 300); uni.onWindowResize(this.handleWindowResize); }, // 计算自适应的页面大小 calculatePageSize() { try { const systemInfo = uni.getSystemInfoSync(); this.windowHeight = systemInfo.windowHeight; const headerHeight = uni.upx2px(130); const paginationHeight = uni.upx2px(100); const tableHeaderHeight = uni.upx2px(120); const specialDataHeight = this.specialStatusData.length > 0 ? uni.upx2px(80 * this.specialStatusData.length) : 0; const padding = uni.upx2px(0); const availableHeight = this.windowHeight - headerHeight - paginationHeight - tableHeaderHeight - specialDataHeight - padding; const rowHeightPx = uni.upx2px(140); const calculatedPageSize = Math.floor(availableHeight / rowHeightPx); this.pageSize = Math.max(4, Math.min(30, calculatedPageSize)); this.validateCurrentPage(); } catch (error) { formatAppLog("error", "at pages/index/home.vue:515", "计算页面大小失败:", error); this.pageSize = 10; } }, // 防抖函数 debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // 验证当前页是否有效 validateCurrentPage() { if (this.currentPage > this.totalPages && this.totalPages > 0) { this.currentPage = this.totalPages; } if (this.currentPage < 1) { this.currentPage = 1; } }, // 构建 WebSocket URL getWebSocketUrl() { return `ws://${this.serverIp}:${this.serverPort}`; }, onIpInput(value) { this.serverIp = value.detail.value; }, // 输入 onPortInput(value) { this.serverPort = value.detail.value; }, // 处理重连 handleReconnect() { uni.showLoading({ title: "正在重连..." }); this.linkShow = false; this.connectionStatus = "connecting"; if (this.ws) { this.ws.close({ success: () => { this.getIpAddress(); }, fail: () => { this.getIpAddress(); } }); } else { this.getIpAddress(); } setTimeout(() => { uni.hideLoading(); }, 2e3); }, // 处理收到的消息 handleMessage(data) { if (data.action == "link") { this.updateListWithArray(data.data); return; } if (typeof data === "object" && data !== null) { const action = data.action; if (!action) { formatAppLog("warn", "at pages/index/home.vue:603", "消息缺少 action 字段", data); return; } switch (action) { case "add": case "create": this.batchAdd(data.data); break; case "update": this.batchUpdate(data.data); break; case "remove": case "delete": this.batchRemove(data.data); break; default: formatAppLog("warn", "at pages/index/home.vue:619", "未知操作类型:", action); } } else { formatAppLog("warn", "at pages/index/home.vue:622", "收到未知格式消息:", data); } }, // 批量新增 batchAdd(data) { if (!data) return; const items = Array.isArray(data) ? data : [data]; const validItems = items.filter((item) => item && item.id !== void 0); if (validItems.length === 0) { formatAppLog("warn", "at pages/index/home.vue:631", "没有有效数据用于新增", data); return; } const updated = [...this.allData]; validItems.forEach((item) => { const index = updated.findIndex((i) => i.id == item.id); if (index > -1) { updated[index] = { ...updated[index], ...item }; } else { updated.unshift(item); } }); this.allData = updated; }, // 批量修改 batchUpdate(data) { if (!data) return; const items = Array.isArray(data) ? data : [data]; const updated = [...this.allData]; items.forEach((item) => { if (!item || item.id === void 0) return; const index = updated.findIndex((i) => i.id == item.id); if (index > -1) { updated[index] = { ...updated[index], ...item }; } }); this.allData = updated; }, // 批量删除 batchRemove(data) { let idsToRemove = []; if (Array.isArray(data.id)) { idsToRemove = data.id; } else if (data.id !== void 0) { idsToRemove = [data.id]; } else if (Array.isArray(data.data)) { idsToRemove = data.data.map((item) => item.id).filter((id) => id !== void 0); } else { idsToRemove = [data.id]; formatAppLog("warn", "at pages/index/home.vue:682", "无法解析删除指令", data); return; } if (idsToRemove.length === 0) return; const updated = this.allData.filter((item) => !idsToRemove.includes(item.id)); this.allData = updated; }, updateListWithArray(newList) { this.allData = []; if (!Array.isArray(newList)) return; const map = /* @__PURE__ */ new Map(); newList.forEach((item) => { if (item.id !== void 0) { map.set(item.id, item); } }); const updated = [...this.allData]; for (const [id, item] of map.entries()) { const index = updated.findIndex((i) => i.id == id); if (index > -1) { updated[index] = item; } else { updated.push(item); } } const final = updated.filter((item) => map.has(item.id)); this.allData = final; }, // 断线重连(指数退避) reconnect() { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { this.connectWebSocket(); this.reconnectTimer = null; }, 3e3); }, // 启动心跳 startHeartbeat() { var that = this; that.stopHeartbeat(); that.heartbeatTimer = setInterval(() => { if (that.ws && that.connectionStatus === "connected") { if (!that.isSend && !that.isPongReceived) { that.heartbeatRate++; formatAppLog("warn", "at pages/index/home.vue:731", that.heartbeatRate, "上一次心跳未收到 pong,可能已断线新"); if (that.heartbeatRate >= 3) { that.ws.close(); return; } } that.isPongReceived = false; that.heartbeatTimeout = setTimeout(() => { if (that.isSend && !that.isPongReceived) { formatAppLog("warn", "at pages/index/home.vue:742", "⚠️ 心跳超时:未在规定时间内收到 pong,即将重连"); uni.showToast({ title: "连接异常,正在重连...", icon: "none", duration: 2e3 }); that.ws.close(); } }, that.heartbeatTimeoutTime); if (that.isSend) { that.ws.send({ data: "PING", success: () => { that.isSend = false; }, fail: (err) => { formatAppLog("error", "at pages/index/home.vue:759", "ping 发送失败", err); that.ws.close(); } }); } } }, that.heartbeatInterval); }, // 停止心跳(断开连接时调用) stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } }, // 格式化时间 formatTime(dateTimeStr) { const date = new Date(dateTimeStr); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const time = `${hours}:${minutes}`; return time; }, // 上一页 prevPage() { if (this.currentPage > 1) { this.currentPage--; this.resetAutoScroll(); } }, // 下一页 nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.resetAutoScroll(); } }, // 触摸开始 handleTouchStart(e) { this.touchStartX = e.touches[0].clientX; this.touchStartY = e.touches[0].clientY; this.touchStartTime = Date.now(); this.isTouching = true; if (this.autoScroll) { this.stopAutoScroll(); } }, // 触摸移动 handleTouchMove(e) { if (!this.isTouching) return; }, // 触摸结束 handleTouchEnd(e) { if (!this.isTouching) return; const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const touchEndTime = Date.now(); const deltaX = touchEndX - this.touchStartX; const deltaY = touchEndY - this.touchStartY; const deltaTime = touchEndTime - this.touchStartTime; const now = Date.now(); if (now - this.lastSwipeTime < 300) { this.isTouching = false; return; } const absDeltaX = Math.abs(deltaX); const absDeltaY = Math.abs(deltaY); if (absDeltaX > 50 && absDeltaX > absDeltaY && deltaTime < 500) { this.lastSwipeTime = now; if (deltaX > 0) { this.prevPage(); } else { this.nextPage(); } } else if (absDeltaY > 50 && absDeltaY > absDeltaX && deltaTime < 500) { this.lastSwipeTime = now; if (deltaY > 0) { this.prevPage(); } else { this.nextPage(); } } this.isTouching = false; if (this.autoScroll) { this.resetAutoScroll(); } }, // 添加鼠标滚轮支持(仅H5平台) // 切换自动滚动 toggleAutoScroll(e) { this.autoScroll = e.detail.value; if (this.autoScroll) { this.startAutoScroll(); } else { this.stopAutoScroll(); } }, // 开始自动滚动 startAutoScroll() { this.stopAutoScroll(); if (this.autoScroll && this.normalData.length > 0 && this.totalPages > 1) { this.autoScrollTimer = setInterval(() => { if (this.currentPage < this.totalPages) { this.currentPage++; } else { this.currentPage = 1; } }, this.scrollInterval); } }, // 停止自动滚动 stopAutoScroll() { if (this.autoScrollTimer) { clearInterval(this.autoScrollTimer); this.autoScrollTimer = null; } }, // 重置自动滚动(手动操作后) resetAutoScroll() { if (this.autoScroll) { this.stopAutoScroll(); setTimeout(() => { this.startAutoScroll(); }, this.scrollInterval); } }, // 当前时间 updateCurrentTime() { const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); this.currentTime = `${year}-${month}-${day}`; this.hhmmss = `${hours}:${minutes}:${seconds}`; const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]; const weekday = weekdays[now.getDay()]; this.whatDay = weekday; } }, watch: { // 监听数据变化 allData: { handler() { this.$nextTick(() => { this.validateCurrentPage(); if (this.autoScroll) { this.startAutoScroll(); } }); }, immediate: true }, // 监听页面大小变化 pageSize() { this.validateCurrentPage(); }, // 监听特殊状态数据变化 specialStatusData() { this.$nextTick(() => { this.calculatePageSize(); }); }, reconnectionNum: { handler(newVal, oldVal) { if (newVal >= 3) { plus.runtime.restart(); setTimeout(() => { plus.navigator.closeSplashscreen(); }, 3e3); this.reconnectionNum = 0; } formatAppLog("log", "at pages/index/home.vue:988", "对象属性变化", newVal, oldVal); }, deep: true, immediate: true } } }; function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) { return vue.openBlock(), vue.createElementBlock("view", { class: "notice-board" }, [ vue.createElementVNode("view", { class: "board-header" }, [ vue.createElementVNode("view", { class: "card_logo", onClick: _cache[0] || (_cache[0] = (...args) => $options.linkSet && $options.linkSet(...args)) }, [ vue.createElementVNode("image", { class: "logo_image", src: _imports_0, mode: "" }), vue.createElementVNode("view", { class: "logo_title" }, "观山湖区疾控中心") ]), vue.createElementVNode("view", { class: "card_yellow" }, [ vue.createElementVNode("view", { class: "card_board" }, [ vue.createElementVNode("text", { class: "board-title" }, "接种留观等待") ]) ]), vue.createElementVNode("view", { class: "card_time" }, [ vue.createElementVNode( "view", { class: "time_title" }, vue.toDisplayString($data.hhmmss), 1 /* TEXT */ ), vue.createElementVNode( "view", { class: "current-time" }, vue.toDisplayString($data.currentTime) + " " + vue.toDisplayString($data.whatDay), 1 /* TEXT */ ) ]) ]), vue.createCommentVNode(" 表格容器 "), vue.createElementVNode("view", { class: "table-container" }, [ vue.createCommentVNode(" 主要表头 "), vue.createElementVNode("view", { class: "table-header" }, [ vue.createElementVNode("view", { class: "table-row header-row" }, [ vue.createElementVNode("view", { class: "cell name title_color" }, "姓名"), vue.createElementVNode("view", { class: "cell time title_color" }, "留观时间"), vue.createElementVNode("view", { class: "cell time title_color" }, "离开时间"), vue.createElementVNode("view", { class: "cell status title_color" }, "状态") ]) ]), vue.createCommentVNode(" 特殊状态数据展示区域(固定) "), $options.specialStatusData.length > 0 ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, class: "special-status-container" }, [ (vue.openBlock(true), vue.createElementBlock( vue.Fragment, null, vue.renderList($options.specialStatusData, (item, index) => { return vue.openBlock(), vue.createElementBlock( "view", { class: vue.normalizeClass(["table-row", `item-${$options.getClass(item.status)}`]), key: item.id }, [ vue.createElementVNode( "view", { class: vue.normalizeClass(["table-cell name", `title-${$options.getClass(item.status)}`]) }, vue.toDisplayString(item.patientName), 3 /* TEXT, CLASS */ ), vue.createElementVNode( "view", { class: "table-cell time" }, vue.toDisplayString($options.formatTime(item.createTime)), 1 /* TEXT */ ), vue.createElementVNode( "view", { class: "table-cell time" }, vue.toDisplayString($options.formatTime(item.outTime)), 1 /* TEXT */ ), vue.createElementVNode("view", { class: "table-cell status" }, [ vue.createElementVNode( "view", { class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`]) }, vue.toDisplayString($options.getStatusText(item)), 3 /* TEXT, CLASS */ ) ]) ], 2 /* CLASS */ ); }), 128 /* KEYED_FRAGMENT */ )), vue.createCommentVNode(" 分隔线 "), vue.createElementVNode("view", { class: "divider" }) ])) : vue.createCommentVNode("v-if", true), vue.createCommentVNode(" 普通数据内容区域 "), vue.createElementVNode( "view", { class: "table-body", onTouchstart: _cache[1] || (_cache[1] = (...args) => $options.handleTouchStart && $options.handleTouchStart(...args)), onTouchmove: _cache[2] || (_cache[2] = (...args) => $options.handleTouchMove && $options.handleTouchMove(...args)), onTouchend: _cache[3] || (_cache[3] = (...args) => $options.handleTouchEnd && $options.handleTouchEnd(...args)) }, [ (vue.openBlock(true), vue.createElementBlock( vue.Fragment, null, vue.renderList($options.displayNormalData, (item, index) => { return vue.openBlock(), vue.createElementBlock( "view", { class: vue.normalizeClass(["table-row", `item-${$options.getClass(item.status)}`]), key: item.id }, [ vue.createElementVNode( "view", { class: vue.normalizeClass(["table-cell name", `title-${$options.getClass(item.status)}`]) }, vue.toDisplayString(item.patientName), 3 /* TEXT, CLASS */ ), vue.createElementVNode( "view", { class: "table-cell time" }, vue.toDisplayString($options.formatTime(item.createTime)), 1 /* TEXT */ ), vue.createElementVNode( "view", { class: "table-cell time" }, vue.toDisplayString($options.formatTime(item.outTime)), 1 /* TEXT */ ), vue.createElementVNode("view", { class: "table-cell status" }, [ vue.createElementVNode( "view", { class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`]) }, vue.toDisplayString($options.getStatusText(item)), 3 /* TEXT, CLASS */ ) ]) ], 2 /* CLASS */ ); }), 128 /* KEYED_FRAGMENT */ )), vue.createCommentVNode(" 空数据提示 "), $options.displayNormalData.length === 0 && $options.specialStatusData.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, class: "empty-tip" }, " 暂无留观人员信息 ")) : vue.createCommentVNode("v-if", true) ], 32 /* NEED_HYDRATION */ ) ]), vue.createElementVNode("view", { class: "card_foot" }, [ vue.createElementVNode("view", { class: "card_tips_box" }, [ vue.createElementVNode("image", { class: "tips_imageil", src: _imports_1, mode: "" }), vue.createElementVNode("view", { class: "title_tips" }, "温馨提示:") ]), vue.createElementVNode("view", { class: "title_tips_foot" }, "请注意留观30分钟后无不良反应后再离开,谢谢。") ]), vue.createCommentVNode(" 连接状态与IP编辑栏 "), $data.linkShow ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, class: "box_link_set" }, [ vue.createElementVNode( "view", { class: "box_popup", style: vue.normalizeStyle({ bottom: $data.keyboardHeight + "px" }) }, [ vue.createElementVNode("view", { class: "head_popup_title" }, [ vue.createElementVNode("view", { class: "title_head_popup" }, "连接设置"), vue.createElementVNode("view", { class: "close_title", onClick: _cache[4] || (_cache[4] = (...args) => $options.getClose && $options.getClose(...args)) }, "×") ]), vue.createElementVNode( "view", { class: vue.normalizeClass(["status-bar-bottom", `status-${$data.connectionStatus}`]) }, [ vue.createCommentVNode(" IP 编辑区域 "), vue.createElementVNode("view", { class: "ip-input-group" }, [ vue.createElementVNode("text", { class: "ip-label" }, "IP:"), vue.createElementVNode("input", { type: "text", value: $data.serverIp, onInput: _cache[5] || (_cache[5] = (...args) => $options.onIpInput && $options.onIpInput(...args)), placeholder: "192.168.0.41", class: "ip-input", onKeyboardheightchange: _cache[6] || (_cache[6] = (...args) => $options.keyboardheightchange && $options.keyboardheightchange(...args)) }, null, 40, ["value"]), vue.createElementVNode("text", { class: "colon" }, ":"), vue.createElementVNode("input", { type: "number", value: $data.serverPort, onInput: _cache[7] || (_cache[7] = (...args) => $options.onPortInput && $options.onPortInput(...args)), placeholder: "8811", class: "port-input", onKeyboardheightchange: _cache[8] || (_cache[8] = (...args) => $options.keyboardheightchange && $options.keyboardheightchange(...args)) }, null, 40, ["value"]), vue.createElementVNode("button", { class: "btn-reconnect", size: "mini", onClick: _cache[9] || (_cache[9] = (...args) => $options.handleReconnect && $options.handleReconnect(...args)) }, " 更新并重连 ") ]), vue.createElementVNode("view", { class: "status-text-box" }, [ vue.createElementVNode( "text", { class: vue.normalizeClass(["dot", `dot-${$data.connectionStatus}`]) }, "●", 2 /* CLASS */ ), vue.createElementVNode( "text", { class: "status-text" }, vue.toDisplayString($options.connectionText), 1 /* TEXT */ ) ]) ], 2 /* CLASS */ ) ], 4 /* STYLE */ ) ])) : vue.createCommentVNode("v-if", true) ]); } 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"]]); const _sfc_main$2 = { data() { return { currentTime: "", // 当前时间 timeTimer: null, // 时间更新定时器 // WebSocket 实例 ws: null, reconnectTimer: null, heartbeatTimer: null, // 心跳定时器 heartbeatTimeout: null, // 心跳响应超时计时器 heartbeatInterval: 3e4, // 30秒发一次心跳 heartbeatTimeoutTime: 1e4, // 10秒内未响应视为超时 isPongReceived: true, // 标记是否收到 pong // 连接状态 connectionStatus: "connecting", // connecting, connected, disconnected // 留观数据列表 list: [], statusText: { observing: "留观中", completed: "留观完成,可离开", warning: "提前离开", hasleft: "已离开" }, // 可编辑的服务器地址 serverIp: "192.168.11.132", serverPort: "8811" }; }, computed: { connectionText() { const { connectionStatus, serverIp, serverPort } = this; return { connecting: `正在连接 ${serverIp}:${serverPort}...`, connected: `已连接到 ${serverIp}:${serverPort}`, disconnected: `连接已断开` }[connectionStatus]; }, // 轮播列表数据 carouselList() { if (this.list.length === 0) return []; if (this.list.length <= 5) return this.list; return [...this.list, ...this.list]; }, // 是否需要启动轮播动画 shouldAnimate() { return this.list.length > 5; } }, methods: { getClass(status) { var title = ""; if (status == 0) { title = "observing"; } else if (status == 1) { title = "completed"; } else if (status == 3) { title = "warning"; } else if (status == 4) { title = "completed"; } return title; }, getStatusText(status) { var title = ""; if (status == 0) { title = this.statusText.observing; } else if (status == 1) { title = this.statusText.completed; } else if (status == 3) { title = this.statusText.warning; } else if (status == 4) { title = this.statusText.hasleft; } return title; }, // 格式时分 getTime(dateTimeStr) { const date = new Date(dateTimeStr); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const time = `${hours}:${minutes}`; return time; }, // 格式化时间范围:09:00 - 09:30 formatTimeRange(start, end) { return start && end ? `${start} - ${end}` : "--"; }, // 输入框事件 onIpInput(e) { this.serverIp = e.detail.value; }, onPortInput(e) { this.serverPort = e.detail.value; }, // 构建 WebSocket URL getWebSocketUrl() { return `ws://${this.serverIp}:${this.serverPort}`; }, // 处理重连 handleReconnect() { uni.showLoading({ title: "正在重连..." }); this.connectionStatus = "connecting"; if (this.ws) { this.ws.close({ success: () => { formatAppLog("log", "at pages/index/index.vue:180", "旧连接已关闭"); this.connectWebSocket("link"); }, fail: () => { this.connectWebSocket("link"); } }); } else { this.connectWebSocket("link"); } setTimeout(() => { uni.hideLoading(); }, 2e3); }, // 建立 WebSocket 连接 connectWebSocket(type) { const url = this.getWebSocketUrl(); this.ws = uni.connectSocket({ url, success: (res) => { formatAppLog("log", "at pages/index/index.vue:201", "connectSocket success", res); }, fail: (err) => { formatAppLog("error", "at pages/index/index.vue:204", "connectSocket 失败", err); this.connectionStatus = "disconnected"; this.reconnect(); } }); this.ws.onOpen((res) => { this.connectionStatus = "connected"; this.ws.send({ data: type }); this.startHeartbeat(); uni.hideLoading(); }); this.ws.onMessage((res) => { if (res.data === "PONG" || res.data === '{"type":"PONG"}') { this.isPongReceived = true; if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } return; } try { const data = JSON.parse(res.data); this.handleMessage(data); } catch (e) { formatAppLog("warn", "at pages/index/index.vue:237", "非 JSON 消息,已忽略", res.data); } }); this.ws.onClose((res) => { formatAppLog("log", "at pages/index/index.vue:241", "WebSocket 连接关闭", res); this.connectionStatus = "disconnected"; this.stopHeartbeat(); this.reconnect(); }); this.ws.onError((err) => { formatAppLog("error", "at pages/index/index.vue:247", "WebSocket 错误", err); this.connectionStatus = "disconnected"; }); }, // 处理收到的消息 handleMessage(data) { if (data.action == "link") { this.updateListWithArray(data.data); return; } if (typeof data === "object" && data !== null) { const action = data.action; if (!action) { formatAppLog("warn", "at pages/index/index.vue:263", "消息缺少 action 字段", data); return; } switch (action) { case "add": case "create": this.batchAdd(data.data); break; case "update": this.batchUpdate(data.data); break; case "remove": case "delete": this.batchRemove(data.data); break; default: formatAppLog("warn", "at pages/index/index.vue:279", "未知操作类型:", action); } } else { formatAppLog("warn", "at pages/index/index.vue:282", "收到未知格式消息:", data); } }, // 批量新增 batchAdd(data) { if (!data) return; const items = Array.isArray(data) ? data : [data]; const validItems = items.filter((item) => item && item.id !== void 0); if (validItems.length === 0) { formatAppLog("warn", "at pages/index/index.vue:291", "没有有效数据用于新增", data); return; } const updated = [...this.list]; validItems.forEach((item) => { const index = updated.findIndex((i) => i.id == item.id); if (index > -1) { updated[index] = { ...updated[index], ...item }; } else { updated.unshift(item); } }); this.list = updated; }, // 批量修改 batchUpdate(data) { if (!data) return; const items = Array.isArray(data) ? data : [data]; const updated = [...this.list]; items.forEach((item) => { if (!item || item.id === void 0) return; const index = updated.findIndex((i) => i.id == item.id); if (index > -1) { updated[index] = { ...updated[index], ...item }; } }); this.list = updated; }, // 批量删除 batchRemove(data) { let idsToRemove = []; if (Array.isArray(data.id)) { idsToRemove = data.id; } else if (data.id !== void 0) { idsToRemove = [data.id]; } else if (Array.isArray(data.data)) { idsToRemove = data.data.map((item) => item.id).filter((id) => id !== void 0); } else { idsToRemove = [data.id]; formatAppLog("warn", "at pages/index/index.vue:342", "无法解析删除指令", data); return; } if (idsToRemove.length === 0) return; const updated = this.list.filter((item) => !idsToRemove.includes(item.id)); this.list = updated; }, updateListWithArray(newList) { if (!Array.isArray(newList)) return; const map = /* @__PURE__ */ new Map(); newList.forEach((item) => { if (item.id !== void 0) { map.set(item.id, item); } }); const updated = [...this.list]; for (const [id, item] of map.entries()) { const index = updated.findIndex((i) => i.id == id); if (index > -1) { updated[index] = item; } else { updated.push(item); } } const final = updated.filter((item) => map.has(item.id)); this.list = final; }, // 断线重连(指数退避) reconnect() { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { formatAppLog("log", "at pages/index/index.vue:377", "正在尝试重新连接..."); this.connectWebSocket("link"); this.reconnectTimer = null; }, 3e3); }, // 启动心跳 startHeartbeat() { formatAppLog("log", "at pages/index/index.vue:384", 2324); this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.ws && this.connectionStatus === "connected") { if (!this.isPongReceived) { formatAppLog("warn", "at pages/index/index.vue:392", "上一次心跳未收到 pong,可能已断线"); this.ws.close(); return; } this.isPongReceived = false; this.heartbeatTimeout = setTimeout(() => { if (!this.isPongReceived) { formatAppLog("warn", "at pages/index/index.vue:401", "⚠️ 心跳超时:未在规定时间内收到 pong,即将重连"); uni.showToast({ title: "连接异常,正在重连...", icon: "none", duration: 2e3 }); this.ws.close(); } }, this.heartbeatTimeoutTime); try { this.ws.send({ data: "PING", success: () => { }, fail: (err) => { formatAppLog("error", "at pages/index/index.vue:417", "ping 发送失败", err); this.ws.close(); } }); } catch (e) { formatAppLog("error", "at pages/index/index.vue:422", "发送 ping 异常", e); this.ws.close(); } } }, this.heartbeatInterval); }, // 停止心跳(断开连接时调用) stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } }, updateCurrentTime() { const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; }, goback() { uni.navigateTo({ url: "/pages/index/home" }); } }, mounted() { this.connectWebSocket("link"); this.updateCurrentTime(); this.timeTimer = setInterval(() => { this.updateCurrentTime(); }, 1e3); }, // 页面卸载时关闭连接 beforeDestroy() { if (this.ws) { this.ws.close(); } if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } this.stopHeartbeat(); if (this.timeTimer) { clearInterval(this.timeTimer); } } }; function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) { return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [ vue.createElementVNode("view", { class: "header" }, [ vue.createElementVNode("text", { class: "title" }, "接种留观人员信息表"), vue.createElementVNode( "text", { class: "current-time" }, vue.toDisplayString($data.currentTime), 1 /* TEXT */ ) ]), vue.createCommentVNode(" 表格 "), vue.createElementVNode("view", { class: "table-container" }, [ vue.createCommentVNode(" 表头 "), vue.createElementVNode("view", { class: "table-header" }, [ vue.createElementVNode("view", { class: "table-row header-row" }, [ vue.createElementVNode("view", { class: "cell name title_color" }, "姓名"), vue.createElementVNode("view", { class: "cell time title_color" }, "留观时间"), vue.createElementVNode("view", { class: "cell time title_color" }, "离开时间"), vue.createElementVNode("view", { class: "cell status title_color" }, "状态") ]) ]), vue.createCommentVNode(" 表格主体 - 支持纵向轮播滚动 "), vue.createElementVNode("view", { class: "table-body-container" }, [ vue.createElementVNode( "view", { class: vue.normalizeClass(["table-body-wrapper", { "animate": $options.shouldAnimate }]) }, [ (vue.openBlock(true), vue.createElementBlock( vue.Fragment, null, vue.renderList($options.carouselList, (item, index) => { return vue.openBlock(), vue.createElementBlock( "view", { key: index, class: vue.normalizeClass(["table-row body-row", `item-${$options.getClass(item.status)}`]) }, [ vue.createElementVNode( "view", { class: "cell name" }, vue.toDisplayString(item.patientName), 1 /* TEXT */ ), vue.createElementVNode( "view", { class: "cell time" }, vue.toDisplayString($options.getTime(item.createTime)), 1 /* TEXT */ ), vue.createElementVNode( "view", { class: "cell time" }, vue.toDisplayString($options.getTime(item.outTime)), 1 /* TEXT */ ), vue.createElementVNode("view", { class: "cell status" }, [ vue.createElementVNode( "text", { class: vue.normalizeClass(["status-tag", `status-${$options.getClass(item.status)}`]) }, vue.toDisplayString($options.getStatusText(item.status)), 3 /* TEXT, CLASS */ ) ]) ], 2 /* CLASS */ ); }), 128 /* KEYED_FRAGMENT */ )) ], 2 /* CLASS */ ) ]) ]), vue.createCommentVNode(" 连接状态与IP编辑栏 "), vue.createElementVNode( "view", { class: vue.normalizeClass(["status-bar-bottom", `status-${$data.connectionStatus}`]) }, [ vue.createElementVNode("view", { class: "status-text-box" }, [ vue.createElementVNode( "text", { class: vue.normalizeClass(["dot", `dot-${$data.connectionStatus}`]) }, "●", 2 /* CLASS */ ), vue.createElementVNode( "text", { class: "status-text" }, vue.toDisplayString($options.connectionText), 1 /* TEXT */ ) ]), vue.createCommentVNode(" IP 编辑区域 "), vue.createElementVNode("view", { class: "ip-input-group" }, [ vue.createElementVNode("text", { class: "ip-label" }, "IP:"), vue.createElementVNode("input", { type: "text", value: $data.serverIp, onInput: _cache[0] || (_cache[0] = (...args) => $options.onIpInput && $options.onIpInput(...args)), placeholder: "192.168.0.41", class: "ip-input" }, null, 40, ["value"]), vue.createElementVNode("text", { class: "colon" }, ":"), vue.createElementVNode("input", { type: "number", value: $data.serverPort, onInput: _cache[1] || (_cache[1] = (...args) => $options.onPortInput && $options.onPortInput(...args)), placeholder: "8811", class: "port-input" }, null, 40, ["value"]), vue.createElementVNode("button", { class: "btn-reconnect", size: "mini", onClick: _cache[2] || (_cache[2] = (...args) => $options.handleReconnect && $options.handleReconnect(...args)) }, " 更新并重连 "), vue.createElementVNode("button", { class: "btn-reconnect", size: "mini", onClick: _cache[3] || (_cache[3] = (...args) => $options.goback && $options.goback(...args)) }, " 测试 ") ]) ], 2 /* CLASS */ ) ]); } 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"]]); const _sfc_main$1 = { data() { return { serverIp: uni.getStorageSync("serverIp") || "", scanning: false }; }, methods: { async scan() { if (this.scanning) return; this.scanning = true; const ip = await this.scanNetwork(); if (ip) { this.serverIp = ip; uni.showToast({ title: "发现: " + ip }); } else { uni.showToast({ icon: "none", title: "未发现" }); } this.scanning = false; }, async scanNetwork() { const lastIp = uni.getStorageSync("serverIp"); if (lastIp && await this.testIp(lastIp)) { return lastIp; } const prefix = this.getNetworkPrefix(); for (let i = 1; i <= 50; i++) { const ip = `${prefix}${i}`; if (await this.testIp(ip)) { return ip; } await this.delay(100); } return null; }, testIp(ip) { return new Promise((resolve) => { uni.request({ url: `http://${ip}:8811/ping`, timeout: 3e3, success: (res) => { resolve(res.statusCode === 200 ? ip : null); }, fail: () => resolve(null) }); }); }, getNetworkPrefix() { let prefix = "192.168.1."; if (typeof plus !== "undefined" && plus.networkinfo) { const ip = plus.networkinfo.getIPAddress(); if (ip) prefix = ip.replace(/\.\d+$/, ".") + "."; } return prefix; }, delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, connect() { if (!this.serverIp) { uni.showToast({ icon: "none", title: "请先设置IP" }); return; } uni.setStorageSync("serverIp", this.serverIp); uni.connectSocket({ url: `ws://${this.serverIp}:8811` }); uni.onSocketOpen(() => { "link"; }); } } }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [ vue.createElementVNode("text", null, "服务端IP:"), vue.withDirectives(vue.createElementVNode( "input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => $data.serverIp = $event), placeholder: "192.168.1.100" }, null, 512 /* NEED_PATCH */ ), [ [vue.vModelText, $data.serverIp] ]), vue.createElementVNode("button", { onClick: _cache[1] || (_cache[1] = (...args) => $options.scan && $options.scan(...args)) }, "🔍 自动扫描"), vue.createElementVNode("button", { onClick: _cache[2] || (_cache[2] = (...args) => $options.connect && $options.connect(...args)) }, "🚀 连接"), $data.scanning ? (vue.openBlock(), vue.createElementBlock("text", { key: 0 }, "扫描中...")) : vue.createCommentVNode("v-if", true) ]); } const PagesIndexMine = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__file", "D:/code/baozhida-observation-system/pages/index/mine.vue"]]); __definePage("pages/index/home", PagesIndexHome); __definePage("pages/index/index", PagesIndexIndex); __definePage("pages/index/mine", PagesIndexMine); const _sfc_main = { mounted() { plus.screen.lockOrientation("landscape-primary"); }, onLaunch: function() { formatAppLog("log", "at App.vue:11", "App Launch"); plus.screen.lockOrientation("landscape-primary"); }, onShow: function() { formatAppLog("log", "at App.vue:18", "App Show"); }, onHide: function() { formatAppLog("log", "at App.vue:21", "App Hide"); } }; const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "D:/code/baozhida-observation-system/App.vue"]]); function createApp() { const app = vue.createVueApp(App); return { app }; } const { app: __app__, Vuex: __Vuex__, Pinia: __Pinia__ } = createApp(); uni.Vuex = __Vuex__; uni.Pinia = __Pinia__; __app__.provide("__globalStyles", __uniConfig.styles); __app__._component.mpType = "app"; __app__._component.render = () => { }; __app__.mount("#app"); })(Vue);