AaronBruin преди 1 месец
ревизия
d0dedc8e53
променени са 100 файла, в които са добавени 6973 реда и са изтрити 0 реда
  1. 12 0
      .hbuilderx/launch.json
  2. 28 0
      App.vue
  3. 20 0
      index.html
  4. 22 0
      main.js
  5. 124 0
      manifest.json
  6. 27 0
      pages.json
  7. 1583 0
      pages/index/home.vue
  8. 749 0
      pages/index/index.vue
  9. 100 0
      pages/index/mine.vue
  10. 804 0
      pages/index/new_file.vue
  11. BIN
      static/horn.png
  12. BIN
      static/logo.png
  13. 0 0
      static/logo.svg
  14. 13 0
      uni.promisify.adaptor.js
  15. 76 0
      uni.scss
  16. BIN
      unpackage/cache/apk/__UNI__E207749_cm.apk
  17. 1 0
      unpackage/cache/apk/apkurl
  18. 0 0
      unpackage/cache/apk/cmManifestCache.json
  19. 4 0
      unpackage/cache/certdata
  20. BIN
      unpackage/cache/wgt/__UNI__E207749/.manifest/google-keystore.keystore
  21. BIN
      unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-hdpi.png
  22. BIN
      unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xhdpi.png
  23. BIN
      unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xxhdpi.png
  24. BIN
      unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xxxhdpi.png
  25. 15 0
      unpackage/cache/wgt/__UNI__E207749/__uniappautomator.js
  26. 31 0
      unpackage/cache/wgt/__UNI__E207749/__uniappchooselocation.js
  27. BIN
      unpackage/cache/wgt/__UNI__E207749/__uniapperror.png
  28. 31 0
      unpackage/cache/wgt/__UNI__E207749/__uniappopenlocation.js
  29. 31 0
      unpackage/cache/wgt/__UNI__E207749/__uniapppicker.js
  30. 6 0
      unpackage/cache/wgt/__UNI__E207749/__uniappquill.js
  31. 0 0
      unpackage/cache/wgt/__UNI__E207749/__uniappquillimageresize.js
  32. 31 0
      unpackage/cache/wgt/__UNI__E207749/__uniappscan.js
  33. BIN
      unpackage/cache/wgt/__UNI__E207749/__uniappsuccess.png
  34. 24 0
      unpackage/cache/wgt/__UNI__E207749/__uniappview.html
  35. 11 0
      unpackage/cache/wgt/__UNI__E207749/app-config-service.js
  36. 1 0
      unpackage/cache/wgt/__UNI__E207749/app-config.js
  37. 0 0
      unpackage/cache/wgt/__UNI__E207749/app-service.js
  38. 0 0
      unpackage/cache/wgt/__UNI__E207749/app.css
  39. 0 0
      unpackage/cache/wgt/__UNI__E207749/manifest.json
  40. 0 0
      unpackage/cache/wgt/__UNI__E207749/pages/index/home.css
  41. 0 0
      unpackage/cache/wgt/__UNI__E207749/pages/index/index.css
  42. 0 0
      unpackage/cache/wgt/__UNI__E207749/pages/index/mine.css
  43. BIN
      unpackage/cache/wgt/__UNI__E207749/static/horn.png
  44. BIN
      unpackage/cache/wgt/__UNI__E207749/static/logo.png
  45. 0 0
      unpackage/cache/wgt/__UNI__E207749/static/logo.svg
  46. 0 0
      unpackage/cache/wgt/__UNI__E207749/uni-app-view.umd.js
  47. BIN
      unpackage/debug/android_debug.apk
  48. 11 0
      unpackage/dist/build/.nvue/app.css.js
  49. 2 0
      unpackage/dist/build/.nvue/app.js
  50. 15 0
      unpackage/dist/build/app-plus/__uniappautomator.js
  51. 31 0
      unpackage/dist/build/app-plus/__uniappchooselocation.js
  52. BIN
      unpackage/dist/build/app-plus/__uniapperror.png
  53. 31 0
      unpackage/dist/build/app-plus/__uniappopenlocation.js
  54. 31 0
      unpackage/dist/build/app-plus/__uniapppicker.js
  55. 6 0
      unpackage/dist/build/app-plus/__uniappquill.js
  56. 0 0
      unpackage/dist/build/app-plus/__uniappquillimageresize.js
  57. 31 0
      unpackage/dist/build/app-plus/__uniappscan.js
  58. BIN
      unpackage/dist/build/app-plus/__uniappsuccess.png
  59. 24 0
      unpackage/dist/build/app-plus/__uniappview.html
  60. 11 0
      unpackage/dist/build/app-plus/app-config-service.js
  61. 1 0
      unpackage/dist/build/app-plus/app-config.js
  62. 0 0
      unpackage/dist/build/app-plus/app-service.js
  63. 0 0
      unpackage/dist/build/app-plus/app.css
  64. 163 0
      unpackage/dist/build/app-plus/manifest.json
  65. 0 0
      unpackage/dist/build/app-plus/pages/index/home.css
  66. 0 0
      unpackage/dist/build/app-plus/pages/index/index.css
  67. 0 0
      unpackage/dist/build/app-plus/pages/index/mine.css
  68. BIN
      unpackage/dist/build/app-plus/static/horn.png
  69. BIN
      unpackage/dist/build/app-plus/static/logo.png
  70. 0 0
      unpackage/dist/build/app-plus/static/logo.svg
  71. 0 0
      unpackage/dist/build/app-plus/uni-app-view.umd.js
  72. 8 0
      unpackage/dist/cache/.vite/deps/_metadata.json
  73. 3 0
      unpackage/dist/cache/.vite/deps/package.json
  74. 11 0
      unpackage/dist/dev/.nvue/app.css.js
  75. 2 0
      unpackage/dist/dev/.nvue/app.js
  76. 15 0
      unpackage/dist/dev/app-plus/__uniappautomator.js
  77. 31 0
      unpackage/dist/dev/app-plus/__uniappchooselocation.js
  78. BIN
      unpackage/dist/dev/app-plus/__uniapperror.png
  79. 31 0
      unpackage/dist/dev/app-plus/__uniappopenlocation.js
  80. 31 0
      unpackage/dist/dev/app-plus/__uniapppicker.js
  81. 6 0
      unpackage/dist/dev/app-plus/__uniappquill.js
  82. 0 0
      unpackage/dist/dev/app-plus/__uniappquillimageresize.js
  83. 31 0
      unpackage/dist/dev/app-plus/__uniappscan.js
  84. BIN
      unpackage/dist/dev/app-plus/__uniappsuccess.png
  85. 24 0
      unpackage/dist/dev/app-plus/__uniappview.html
  86. 11 0
      unpackage/dist/dev/app-plus/app-config-service.js
  87. 1 0
      unpackage/dist/dev/app-plus/app-config.js
  88. 1755 0
      unpackage/dist/dev/app-plus/app-service.js
  89. 0 0
      unpackage/dist/dev/app-plus/app.css
  90. 163 0
      unpackage/dist/dev/app-plus/manifest.json
  91. 501 0
      unpackage/dist/dev/app-plus/pages/index/home.css
  92. 248 0
      unpackage/dist/dev/app-plus/pages/index/index.css
  93. 0 0
      unpackage/dist/dev/app-plus/pages/index/mine.css
  94. BIN
      unpackage/dist/dev/app-plus/static/horn.png
  95. BIN
      unpackage/dist/dev/app-plus/static/logo.png
  96. 0 0
      unpackage/dist/dev/app-plus/static/logo.svg
  97. 0 0
      unpackage/dist/dev/app-plus/uni-app-view.umd.js
  98. 0 0
      unpackage/dist/dev/cache/.app-plus/tsc/app-android/.tsbuildInfo
  99. BIN
      unpackage/release/apk/__UNI__E207749__20250815162836.apk
  100. BIN
      unpackage/release/apk/__UNI__E207749__20250918154748.apk

+ 12 - 0
.hbuilderx/launch.json

@@ -0,0 +1,12 @@
+{
+    "version" : "1.0",
+    "configurations" : [
+        {
+            "customPlaygroundType" : "local",
+            "localRepoPath" : "D:/code/baozhida-observation-system",
+            "packageName" : "uni.app.UNIE207749",
+            "playground" : "custom",
+            "type" : "uni-app:app-android"
+        }
+    ]
+}

+ 28 - 0
App.vue

@@ -0,0 +1,28 @@
+<script>
+	// 留观大屏
+	export default {
+		mounted() {
+			// 横屏
+			//#ifdef APP
+			plus.screen.lockOrientation('landscape-primary');
+			//#endif
+		},
+		onLaunch: function() {
+			console.log('App Launch')
+			// 横屏
+			//#ifdef APP
+			plus.screen.lockOrientation('landscape-primary');
+			//#endif
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	/*每个页面公共css */
+</style>

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 22 - 0
main.js

@@ -0,0 +1,22 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+  ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  return {
+    app
+  }
+}
+// #endif

+ 124 - 0
manifest.json

@@ -0,0 +1,124 @@
+{
+    "name" : "留观系统",
+    "appid" : "__UNI__E207749",
+    "description" : "",
+    "versionName" : "1.1.0",
+    "versionCode" : 110,
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ],
+                "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
+                "minSdkVersion" : 21
+            },
+            /* ios打包配置 */
+            "ios" : {
+                "dSYMs" : false
+            },
+            /* SDK配置 */
+            "sdkConfigs" : {},
+            "icons" : {
+                "android" : {
+                    "hdpi" : "unpackage/res/icons/72x72.png",
+                    "xhdpi" : "unpackage/res/icons/96x96.png",
+                    "xxhdpi" : "unpackage/res/icons/144x144.png",
+                    "xxxhdpi" : "unpackage/res/icons/192x192.png"
+                },
+                "ios" : {
+                    "appstore" : "unpackage/res/icons/1024x1024.png",
+                    "ipad" : {
+                        "app" : "unpackage/res/icons/76x76.png",
+                        "app@2x" : "unpackage/res/icons/152x152.png",
+                        "notification" : "unpackage/res/icons/20x20.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "proapp@2x" : "unpackage/res/icons/167x167.png",
+                        "settings" : "unpackage/res/icons/29x29.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "spotlight" : "unpackage/res/icons/40x40.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png"
+                    },
+                    "iphone" : {
+                        "app@2x" : "unpackage/res/icons/120x120.png",
+                        "app@3x" : "unpackage/res/icons/180x180.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "notification@3x" : "unpackage/res/icons/60x60.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "settings@3x" : "unpackage/res/icons/87x87.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png",
+                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
+                    }
+                }
+            }
+        },
+        "nativePlugins" : {
+            "Alikes-NetTools" : {
+                "__plugin_info__" : {
+                    "name" : "局域网设备助手",
+                    "description" : "局域网内设备发现,通过ping发现相同网段内的在线设备,从而实现设备列表的查看",
+                    "platforms" : "Android",
+                    "url" : "https://ext.dcloud.net.cn/plugin?id=7437",
+                    "android_package_name" : "uni.app.UNIE207749",
+                    "ios_bundle_id" : "",
+                    "isCloud" : true,
+                    "bought" : 1,
+                    "pid" : "7437",
+                    "parameters" : {}
+                }
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

+ 27 - 0
pages.json

@@ -0,0 +1,27 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/home",
+			"style": {
+				"navigationStyle": "custom"
+			}
+		}, {
+			"path": "pages/index/index",
+			"style": {
+				"navigationStyle": "custom"
+			}
+		}, {
+			"path": "pages/index/mine",
+			"style": {
+				"navigationStyle": "custom"
+			}
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"uniIdRouter": {}
+}

+ 1583 - 0
pages/index/home.vue

@@ -0,0 +1,1583 @@
+<template>
+	<view class="notice-board">
+		<view class="board-header">
+			<view class="card_logo" @click="linkSet">
+				<image class="logo_image" src="/static/logo.png" mode=""></image>
+				<view class="logo_title">观山湖区疾控中心</view>
+			</view>
+			<view class="card_yellow">
+				<view class="card_board">
+					<text class="board-title">接种留观等待</text>
+				</view>
+			</view>
+			<view class="card_time">
+				<view class="time_title">{{hhmmss}}</view>
+				<view class="current-time">{{ currentTime }} {{whatDay}}</view>
+			</view>
+		</view>
+		<!-- 表格容器 -->
+		<view class="table-container">
+			<!-- 主要表头 -->
+			<view class="table-header">
+				<view class="table-row header-row">
+					<view class="cell name title_color">姓名</view>
+					<view class="cell time title_color">留观时间</view>
+					<view class="cell time title_color">离开时间</view>
+					<view class="cell status title_color">状态</view>
+				</view>
+			</view>
+			<!-- 特殊状态数据展示区域(固定) -->
+			<view class="special-status-container" v-if="specialStatusData.length > 0">
+				<view class="table-row" v-for="(item, index) in specialStatusData" :key="item.id"
+					:class="`item-${getClass(item.status)}`">
+					<view class="table-cell name" :class="`title-${getClass(item.status)}`">{{ item.patientName }}
+					</view>
+					<view class="table-cell time">{{ formatTime(item.createTime) }}</view>
+					<view class="table-cell time">{{ formatTime(item.outTime) }}</view>
+					<view class="table-cell status">
+						<view class="status-tag" :class="`status-${getClass(item.status)}`">
+							{{ getStatusText(item) }}
+						</view>
+					</view>
+				</view>
+				<!-- 分隔线 -->
+				<view class="divider"></view>
+			</view>
+			<!-- 普通数据内容区域 -->
+			<view class="table-body" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
+				@touchend="handleTouchEnd">
+				<view class="table-row" v-for="(item, index) in displayNormalData" :key="item.id"
+					:class="`item-${getClass(item.status)}`">
+					<view class="table-cell name" :class="`title-${getClass(item.status)}`">{{ item.patientName }}
+					</view>
+					<view class="table-cell time">{{ formatTime(item.createTime) }}</view>
+					<view class="table-cell time">{{ formatTime(item.outTime) }}</view>
+					<view class="table-cell status">
+						<view class="status-tag" :class="`status-${getClass(item.status)}`">
+							{{ getStatusText(item) }}
+						</view>
+					</view>
+				</view>
+				<!-- 空数据提示 -->
+				<view v-if="displayNormalData.length === 0 && specialStatusData.length === 0" class="empty-tip">
+					暂无留观人员信息
+				</view>
+			</view>
+		</view>
+		<view class="card_foot">
+			<view class="card_tips_box">
+				<image class="tips_imageil" src="/static/horn.png" mode=""></image>
+				<view class="title_tips">温馨提示:</view>
+			</view>
+			<view class="title_tips_foot">请注意留观30分钟后无不良反应后再离开,谢谢。</view>
+		</view>
+		<!-- 连接状态与IP编辑栏 -->
+		<view class="box_link_set" v-if="linkShow">
+			<view class="box_popup" :style="{bottom: keyboardHeight + 'px'}">
+				<view class="head_popup_title">
+					<view class="title_head_popup">连接设置</view>
+					<view class="close_title" @click="getClose">×</view>
+				</view>
+				<view class="status-bar-bottom" :class="`status-${connectionStatus}`">
+					<!-- IP 编辑区域 -->
+					<view class="ip-input-group">
+						<text class="ip-label">IP:</text>
+						<input type="text" :value="serverIp" @input="onIpInput" placeholder="192.168.0.41"
+							class="ip-input" @keyboardheightchange="keyboardheightchange" />
+						<text class="colon">:</text>
+						<input type="number" :value="serverPort" @input="onPortInput" placeholder="8811"
+							class="port-input" @keyboardheightchange="keyboardheightchange" />
+						<button class="btn-reconnect" size="mini" @click="handleReconnect">
+							更新并重连
+						</button>
+					</view>
+					<view class="status-text-box">
+						<text class="dot" :class="`dot-${connectionStatus}`">●</text>
+						<text class="status-text">{{ connectionText }}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				currentTime: '', // 当前时间
+				hhmmss: '', // 当前时间
+				whatDay: '', //星期几
+				timeTimer: null, // 时间更新定时器
+				// WebSocket 实例
+				ws: null,
+				reconnectTimer: null,
+				heartbeatTimer: null, // 心跳定时器
+				heartbeatTimeout: null, // 心跳响应超时计时器
+				heartbeatInterval: 30000, // 30秒发一次心跳
+				heartbeatTimeoutTime: 10000, // 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: 6000,
+				// 窗口高度
+				windowHeight: 0,
+				// 触摸相关
+				touchStartX: 0,
+				touchStartTime: 0,
+				isTouching: false,
+				touchStartY: 0,
+				// 垂直滑动相关
+				lastSwipeTime: 0,
+				linkShow: false,
+				ipArray: [],
+				keyboardHeight: 0,
+				now: 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 days = Math.floor(timeDiff / (24 * 3600 * 1000))
+						const hours = Math.floor((timeDiff % (24 * 3600 * 1000)) / (3600 * 1000))
+						const minutes = Math.floor((timeDiff % (3600 * 1000)) / (60 * 1000))
+						const seconds = Math.floor((timeDiff % (60 * 1000)) / 1000)
+						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 = new Date()
+			}, 1000)
+			this.getIpAddress()
+			this.initPage()
+			const lastIp = uni.getStorageSync('serverIp');
+			const lastPort = uni.getStorageSync('serverPort');
+			if (lastIp && lastPort) {
+				this.serverIp = lastIp
+				this.serverPort = lastPort
+			}
+			// 在H5平台添加鼠标滚轮支持
+			//#ifdef H5
+			this.startCheck()
+			this.addMouseWheelSupport()
+			//#endif
+			this.updateCurrentTime(); // 立即更新一次时间
+			this.timeTimer = setInterval(() => {
+				this.updateCurrentTime();
+			}, 1000); // 每秒更新一次时间
+		},
+		beforeDestroy() {
+			// 组件销毁时清理定时器
+			if (this.timer) {
+				this.timer = null
+				clearInterval(this.timer)
+			}
+			this.stopAutoScroll()
+			uni.offWindowResize(this.handleWindowResize)
+			//#ifdef H5
+			this.removeMouseWheelSupport()
+			//#endif
+			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;
+				}
+				// console.log(ipList, 344)
+				const ip = ipList[index].trim();
+				const port = this.serverPort; // 根据你的服务修改端口
+				const url = `ws://${ip}:${port}/`; // 可改为 /ws, /device 等路径
+				// console.log(`🔍 正在尝试连接 WebSocket: ${url}`);
+				// 先确保没有遗留连接
+				if (this.ws) {
+					this.ws.close();
+					this.ws = null;
+				}
+				// 创建 SocketTask
+				this.ws = uni.connectSocket({
+					url: url,
+					success: (res) => {
+						console.log('connectSocket success', res);
+					},
+					fail: (err) => {
+						console.error('connectSocket 失败', err);
+						this.connectionStatus = 'disconnected';
+						if (this.serverIp == ip) {
+							this.heartbeatRate = 0
+							this.reconnectionNum++
+							//#ifdef H5
+							this.isSend = true
+							this.findFirstWebSocketIp([this.serverIp], index, onSuccess, onAllFailed);
+							//#endif
+						}
+						//#ifdef APP
+						this.reconnectionNum++
+						this.isSend = true
+						this.heartbeatRate = 0
+						this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
+						//#endif
+					}
+				});
+				// --- WebSocket 事件监听 ---
+				this.ws.onOpen((res) => {
+					this.serverIp = ip
+					uni.setStorageSync('serverIp', this.serverIp);
+					uni.setStorageSync('serverPort', this.serverPort);
+					// console.log('WebSocket 连接成功', res);
+					this.connectionStatus = 'connected';
+					this.ws.send({
+						data: 'link',
+					});
+					onSuccess(ip);
+					// ✅ 连接成功后启动心跳
+					this.startHeartbeat();
+					uni.hideLoading();
+				});
+				this.ws.onMessage((res) => {
+					// ✅ 处理心跳响应 pong
+					if (res.data === 'PONG' || res.data === '{"type":"PONG"}') {
+						uni.hideToast();
+						this.isSend = true
+						this.reconnectionNum = 0
+						this.heartbeatRate = 0
+						this.isPongReceived = true;
+						// console.log('收到 pong,心跳正常');
+						// ✅ 清除等待 pong 的超时计时器
+						if (this.heartbeatTimeout) {
+							clearTimeout(this.heartbeatTimeout);
+							this.heartbeatTimeout = null;
+						}
+						return;
+					}
+					try {
+						const data = JSON.parse(res.data);
+						// console.log('收到消息:', data);
+						this.handleMessage(data);
+					} catch (e) {
+						console.warn('非 JSON 消息,已忽略', res.data);
+					}
+				});
+				this.ws.onClose((res) => {
+					console.log('WebSocket 连接关闭', res);
+					this.connectionStatus = 'disconnected';
+					this.stopHeartbeat(); // 停止心跳
+					var reconnectionTime = setTimeout(() => {
+						clearTimeout(reconnectionTime)
+						if (this.serverIp == ip) {
+							//#ifdef H5
+							this.isSend = true
+							this.heartbeatRate = 0
+							this.reconnectionNum++
+							this.findFirstWebSocketIp([this.serverIp], index, onSuccess, onAllFailed);
+							//#endif
+						}
+						//#ifdef APP
+						this.isSend = true
+						this.heartbeatRate = 0
+						this.reconnectionNum++
+						this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
+						//#endif
+					}, 2000)
+				});
+				this.ws.onError((err) => {
+					console.error('WebSocket 错误', err);
+					this.connectionStatus = 'disconnected';
+					//#ifdef APP
+					this.isSend = true
+					this.heartbeatRate = 0
+					this.reconnectionNum++
+					this.findFirstWebSocketIp(ipList, index + 1, onSuccess, onAllFailed);
+					//#endif
+				});
+			},
+			// 连接设置
+			linkSet() {
+				this.linkShow = true
+			},
+			// 关闭弹窗
+			getClose() {
+				this.linkShow = false
+			},
+			// 获取ip地址
+			getIpAddress() {
+				//获取插件示例
+				//#ifdef APP
+				var deviceFinder = uni.requireNativePlugin("Alikes-NetTools-DeviceFinder")
+				deviceFinder.scan({}, (res) => {
+					this.ipArray = res
+					this.ipArray.unshift(this.serverIp)
+					this.startCheck()
+				})
+				//#endif
+			},
+			// 启动检查
+			startCheck() {
+				var ipArray = []
+				//#ifdef H5
+				ipArray = [this.serverIp]
+				//#endif
+				//#ifdef APP
+				ipArray = this.ipArray
+				//#endif
+				this.findFirstWebSocketIp(
+					ipArray,
+					0,
+					(ip) => {
+						// ✅ 找到可用设备
+						this.onDeviceFound(ip);
+						this.linkShow = false
+					},
+					() => {
+						// ❌ 全部失败
+						this.onNoDevice();
+					}
+				);
+			},
+			// 找到设备后的逻辑
+			onDeviceFound(ip) {
+				uni.showToast({
+					title: '连接成功',
+					icon: 'success'
+				});
+				// console.log(`🚀 可用设备: ${ip},开始后续操作...`);
+				// 例如:跳转控制页面、订阅消息等
+			},
+			// 无设备可用
+			onNoDevice() {
+				uni.showToast({
+					title: '无设备响应',
+					icon: 'none',
+					duration: 3000
+				});
+				this.getIpAddress()
+				// console.log('🚨 所有设备 WebSocket 均无法连接');
+			},
+			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) // 上下padding
+
+					const availableHeight = this.windowHeight - headerHeight - paginationHeight -
+						tableHeaderHeight - specialDataHeight - padding
+
+					// 计算行高(px)- 固定60rpx
+					const rowHeightPx = uni.upx2px(140)
+
+					// 计算可显示的行数
+					const calculatedPageSize = Math.floor(availableHeight / rowHeightPx)
+
+					// 设置页面大小(最少显示4行,最多显示30行)
+					this.pageSize = Math.max(4, Math.min(30, calculatedPageSize))
+
+					// 验证当前页
+					this.validateCurrentPage()
+
+				} catch (error) {
+					console.error('计算页面大小失败:', 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: () => {
+							// console.log('旧连接已关闭');
+							//#ifdef H5
+							this.startCheck()
+							//#endif
+							//#ifdef APP
+							this.getIpAddress()
+							//#endif
+						},
+						fail: () => {
+							//#ifdef H5
+							this.startCheck()
+							//#endif
+							//#ifdef APP
+							this.getIpAddress()
+							//#endif
+						}
+					});
+				} else {
+					//#ifdef H5
+					this.startCheck()
+					//#endif
+					//#ifdef APP
+					this.getIpAddress()
+					//#endif
+				}
+				setTimeout(() => {
+					uni.hideLoading();
+				}, 2000);
+			},
+			// 处理收到的消息
+			handleMessage(data) {
+				// 1. 全量数据:数组(不是对象,或没有 action 字段)
+				if (data.action == 'link') {
+					this.updateListWithArray(data.data);
+					return;
+				}
+				// 2. 增量操作:对象
+				if (typeof data === 'object' && data !== null) {
+					const action = data.action;
+
+					if (!action) {
+						console.warn('消息缺少 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:
+							console.warn('未知操作类型:', action);
+					}
+				} else {
+					console.warn('收到未知格式消息:', data);
+				}
+			},
+			// 批量新增
+			batchAdd(data) {
+				if (!data) return;
+				const items = Array.isArray(data) ? data : [data]; // 兼容单条
+				const validItems = items.filter(item => item && item.id !== undefined);
+				if (validItems.length === 0) {
+					console.warn('没有有效数据用于新增', 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 === undefined) return;
+					const index = updated.findIndex(i => i.id == item.id);
+					if (index > -1) {
+						updated[index] = {
+							...updated[index],
+							...item
+						};
+					}
+					// else { this.allData.push(item); } // 可选:当作新增
+				});
+				this.allData = updated;
+			},
+			// 批量删除
+			batchRemove(data) {
+				let idsToRemove = [];
+				if (Array.isArray(data.id)) {
+					// remove: { id: [1,2,3] }
+					idsToRemove = data.id;
+				} else if (data.id !== undefined) {
+					// remove: { id: 1 }
+					idsToRemove = [data.id];
+				} else if (Array.isArray(data.data)) {
+					// remove: { data: [ {id:1}, {id:2} ] }
+					idsToRemove = data.data.map(item => item.id).filter(id => id !== undefined);
+				} else {
+					idsToRemove = [data.id];
+					console.warn('无法解析删除指令', 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 = new Map();
+				newList.forEach(item => {
+					if (item.id !== undefined) {
+						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(() => {
+					// console.log('正在尝试重新连接...');
+					this.connectWebSocket();
+					this.reconnectTimer = null;
+				}, 3000);
+			},
+			// 启动心跳
+			startHeartbeat() {
+				// 清除旧心跳
+				var that = this
+				that.stopHeartbeat();
+				// 发送 ping 的定时器
+				that.heartbeatTimer = setInterval(() => {
+					if (that.ws && that.connectionStatus === 'connected') {
+						// 检查上一次的 pong 是否已收到,未收到则判定为断线
+						if (!that.isSend && !that.isPongReceived) {
+							that.heartbeatRate++
+							console.warn(that.heartbeatRate, '上一次心跳未收到 pong,可能已断线新');
+							if (that.heartbeatRate >= 3) {
+								that.ws.close();
+								return;
+							}
+						}
+						// 发送心跳
+						that.isPongReceived = false; // 等待 pong
+						// 设置超时检测:10秒内没收到 pong 就断开
+						that.heartbeatTimeout = setTimeout(() => {
+							if (that.isSend && !that.isPongReceived) {
+								console.warn('⚠️ 心跳超时:未在规定时间内收到 pong,即将重连');
+								uni.showToast({
+									title: '连接异常,正在重连...',
+									icon: 'none',
+									duration: 2000
+								});
+								that.ws.close(); // 触发 onClose → 重连
+							}
+						}, that.heartbeatTimeoutTime); // 使用配置的超时时间(如 10000ms)
+						if (that.isSend) {
+							that.ws.send({
+								data: 'PING',
+								success: () => {
+									// console.log('ping 已发送');
+									that.isSend = false
+								},
+								fail: (err) => {
+									console.error('ping 发送失败', err);
+									that.ws.close();
+								}
+							});
+						}
+					}
+				}, that.heartbeatInterval);
+				// 可选:启动响应超时检测(更严格)
+				// 通常靠"发 ping 后未收到 pong"即可判断
+			},
+			// 停止心跳(断开连接时调用)
+			stopHeartbeat() {
+				if (this.heartbeatTimer) {
+					clearInterval(this.heartbeatTimer);
+					this.heartbeatTimer = null;
+				}
+				if (this.heartbeatTimeout) {
+					clearTimeout(this.heartbeatTimeout);
+					this.heartbeatTimeout = null;
+				}
+			},
+			// 格式化时间
+			formatTime(dateTimeStr) {
+				// 创建 Date 对象
+				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平台)
+			//#ifdef H5
+			addMouseWheelSupport() {
+				const tableBody = document.querySelector('.table-body')
+				if (tableBody) {
+					this.wheelHandler = this.debounce((e) => {
+						e.preventDefault()
+						const delta = e.wheelDelta || -e.detail
+						if (delta < 0) {
+							// 向下滚动 - 下一页
+							this.nextPage()
+						} else {
+							// 向上滚动 - 上一页
+							this.prevPage()
+						}
+					}, 150)
+
+					tableBody.addEventListener('mousewheel', this.wheelHandler, {
+						passive: false
+					})
+					tableBody.addEventListener('DOMMouseScroll', this.wheelHandler, {
+						passive: false
+					})
+				}
+			},
+			// 移除鼠标滚轮支持
+			removeMouseWheelSupport() {
+				const tableBody = document.querySelector('.table-body')
+				if (tableBody && this.wheelHandler) {
+					tableBody.removeEventListener('mousewheel', this.wheelHandler)
+					tableBody.removeEventListener('DOMMouseScroll', this.wheelHandler)
+				}
+			},
+			//#endif
+			// 切换自动滚动
+			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 = 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 >= 50) {
+						//#ifdef APP
+						plus.runtime.restart();
+						setTimeout(() => {
+							plus.navigator.closeSplashscreen();
+						}, 3000);
+						this.reconnectionNum = 0
+						//#endif
+					}
+					// console.log('对象属性变化', newVal, oldVal);
+				},
+				deep: true,
+				immediate: true
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.notice-board {
+		width: 100%;
+		height: 100vh;
+		display: flex;
+		flex-direction: column;
+		background-color: #0d54ec;
+		/* padding: 20rpx; */
+		box-sizing: border-box;
+	}
+
+	.card_logo {
+		position: absolute;
+		left: 30rpx;
+		top: 0;
+		bottom: 0;
+		display: flex;
+		align-items: center;
+		cursor: pointer;
+	}
+
+	.card_logo:focus {
+		border: none !important;
+	}
+
+	.board-header {
+		/* padding-top: 10rpx; */
+		text-align: center;
+		/* margin-bottom: 30rpx; */
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 130rpx;
+		flex-shrink: 0;
+		background-color: #0d54ec;
+		box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
+	}
+
+	.card_yellow {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: fit-content;
+		width: -webkit-fit-content;
+		width: -moz-fit-content;
+		padding: 0rpx 40rpx;
+		height: 130rpx;
+		background-color: #e5cb8d;
+		border-bottom-right-radius: 300rpx;
+		border-top-left-radius: 300rpx;
+	}
+
+	.card_board {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: fit-content;
+		width: -webkit-fit-content;
+		width: -moz-fit-content;
+		padding: 0rpx 140rpx;
+		height: 130rpx;
+		background-color: #d5f3ff;
+		border-bottom-right-radius: 120rpx;
+		border-top-left-radius: 120rpx;
+	}
+
+	.board-title {
+		font-size: 60rpx;
+		font-weight: bold;
+		color: #0e6699;
+	}
+
+	.card_time {
+		position: absolute;
+		right: 30rpx;
+		top: 0;
+		bottom: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: flex-end;
+		justify-content: center;
+	}
+
+	.time_title {
+		font-size: 60rpx;
+		color: #fff;
+		font-weight: bold;
+		line-height: 60rpx;
+	}
+
+	.current-time {
+		display: flex;
+		align-items: center;
+		display: flex;
+		font-size: 40rpx;
+		color: #fff;
+		font-weight: bold;
+		line-height: 45rpx;
+	}
+
+	.table-container {
+		flex: 1;
+		margin: 20rpx 20rpx 0rpx 20rpx;
+		border-radius: 24rpx;
+		overflow: hidden;
+		box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+		background-color: #ffffff;
+		display: flex;
+		flex-direction: column;
+	}
+
+	.table-header {
+		background-color: #71a0ee;
+		color: white;
+		font-weight: bold;
+	}
+
+	.table-row {
+		display: flex;
+		border-bottom: 2rpx solid #dedede;
+	}
+
+	.header-row {
+		/* background-color: #71a0ee; */
+		color: white;
+		font-weight: bold;
+	}
+
+	.table-cell {
+		padding: 24rpx 16rpx;
+		font-size: 50rpx;
+		text-align: center;
+		color: #333;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+
+	.cell {
+		padding: 24rpx 16rpx;
+		font-size: 50rpx;
+		text-align: center;
+		color: #333;
+	}
+
+	.name {
+		width: 25%;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.time {
+		width: 25%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.status {
+		width: 25%;
+	}
+
+	.title_color {
+		color: #fff;
+	}
+
+	/* 特殊状态数据容器 */
+	.special-status-container {
+		background-color: #fff8e6;
+	}
+
+	.special-row {
+		background-color: #fff8e6 !important;
+		min-height: 60rpx;
+		line-height: 60rpx;
+	}
+
+	/* 分隔线 */
+	.divider {
+		height: 1rpx;
+		background-color: #ddd;
+	}
+
+	/* 普通数据内容区域 */
+	.table-body {
+		flex: 1;
+		overflow-y: auto;
+		/* 添加触摸反馈 */
+		-webkit-overflow-scrolling: touch;
+		touch-action: pan-y;
+	}
+
+	.title-observing {
+		color: #007bff;
+	}
+
+	.title-completed {
+		color: #28a745;
+	}
+
+	.title-warning {
+		color: #ff0000;
+	}
+
+	.title-hasleft {
+		color: #fa8c16;
+	}
+
+	/* 状态行样式 */
+	.status-observing {
+		background-color: #9dd6ff;
+		color: #007bff;
+		border: 2rpx solid #48a0ff;
+	}
+
+	.status-completed {
+		background-color: #e8f5e8;
+		color: #28a745;
+		border: 2rpx solid #8bc34a;
+	}
+
+	.status-warning {
+		background-color: #ffebee;
+		color: #ff0000;
+		border: 2rpx solid #ef9a9a;
+	}
+
+	.status-hasleft {
+		background-color: #fff2e8;
+		color: #fa8c16;
+		border: 2rpx solid #ffbb96;
+	}
+
+	.item-observing {
+		background-color: #9dd6ff;
+	}
+
+	.item-completed {
+		background-color: #e8f5e8;
+	}
+
+	.item-warning {
+		background-color: #ffebee;
+	}
+
+	.item-hasleft {
+		background-color: #fff2e8;
+	}
+
+	/* 状态标签样式 - 控制高度不超过40rpx */
+	.status-tag {
+		flex: none;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 8rpx 20rpx;
+		border-radius: 32rpx;
+		font-size: 48rpx;
+		font-weight: 500;
+		width: calc(100% - 20rpx);
+	}
+
+	.empty-tip {
+		text-align: center;
+		padding: 80rpx;
+		color: #999;
+		font-size: 60rpx;
+		font-style: italic;
+	}
+
+	/* 底部 */
+	.card_foot {
+		display: flex;
+		align-items: center;
+		height: 120rpx;
+		box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+	}
+
+	.card_tips_box {
+		display: flex;
+		align-items: center;
+		padding-left: 15rpx;
+	}
+
+	.title_tips {
+		color: #fff;
+		font-size: 40rpx;
+	}
+
+	.title_tips_foot {
+		color: #fff;
+		flex: 1;
+		font-size: 40rpx;
+		border-radius: 20rpx;
+		margin-right: 20rpx;
+		background-color: #71a0ee;
+		padding: 20rpx;
+	}
+
+	.box_link_set {
+		position: fixed;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		height: 100%;
+		background-color: rgba(0, 0, 0, 0.3);
+	}
+
+	.box_popup {
+		position: relative;
+		background-color: #ffffff;
+		border-radius: 8rpx;
+	}
+
+	.head_popup_title {
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.title_head_popup {
+		font-size: 40rpx;
+		font-weight: 500;
+		padding: 30rpx 30rpx 0rpx 30rpx;
+	}
+
+	.close_title {
+		position: absolute;
+		right: 30rpx;
+		font-size: 40rpx;
+		cursor: pointer;
+	}
+
+	/* // 连接状态栏 */
+	.status-bar-bottom {
+		display: flex;
+		flex-direction: column;
+		margin: 30rpx;
+		padding: 24rpx 30rpx;
+		background-color: #ffffff;
+		box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
+		border-radius: 24rpx;
+		display: flex;
+		gap: 16rpx;
+	}
+
+	.status-text-box {
+		display: flex;
+		align-items: center;
+		font-size: 28rpx;
+	}
+
+	.dot {
+		color: #d32f2f;
+		font-size: 24rpx;
+	}
+
+	.dot-connecting {
+		color: #f9ae3d;
+	}
+
+	.dot-connected {
+		color: #28a745;
+	}
+
+	.dot-disconnected {
+		color: #d32f2f;
+	}
+
+	.status-text {
+		color: #555;
+	}
+
+	.status-text {
+		margin-left: 16rpx;
+	}
+
+	.title_color {
+		color: #fff;
+	}
+
+	/* // IP 输入区域 */
+	.ip-input-group {
+		display: flex;
+		align-items: center;
+		margin-left: 16rpx;
+		flex-wrap: wrap;
+		gap: 12rpx;
+	}
+
+	.ip-label {
+		font-size: 28rpx;
+		color: #555;
+	}
+
+	.ip-input,
+	.port-input {
+		border: 2rpx solid #ddd;
+		border-radius: 12rpx;
+		padding: 12rpx 20rpx;
+		font-size: 28rpx;
+		width: 200rpx;
+	}
+
+	.port-input {
+		width: 160rpx;
+	}
+
+	.colon {
+		font-size: 32rpx;
+		color: #666;
+		margin: 0 8rpx;
+	}
+
+	.btn-reconnect {
+		background-color: #007aff;
+		color: white;
+		font-size: 24rpx;
+		padding: 0 16rpx;
+		border-radius: 12rpx;
+	}
+
+	.logo_image {
+		width: 80rpx;
+		height: 80rpx;
+	}
+
+	.logo_title {
+		font-size: 50rpx;
+		color: #fff;
+		font-weight: bold;
+		margin-left: 15rpx;
+	}
+
+	.tips_imageil {
+		width: 70rpx;
+		height: 70rpx;
+	}
+
+	/* 响应式优化 */
+	@media screen and (max-height: 600px) {
+		.card_logo {
+			left: 10rpx;
+		}
+
+		.logo_title {
+			font-size: 18rpx;
+			margin-left: 5rpx;
+		}
+
+		.logo_image {
+			width: 35rpx;
+			height: 35rpx;
+		}
+
+		.table-row {
+			min-height: 35rpx;
+			line-height: 35rpx;
+			padding: 4rpx 0rpx;
+		}
+
+		.table-cell {
+			font-size: 22rpx;
+			padding: 30rpx 10rpx;
+		}
+
+		.status-tag {
+			padding: 4rpx 10rpx;
+			font-size: 18rpx;
+			height: 22rpx;
+			min-width: 80rpx;
+		}
+
+		.control-btn {
+			height: 50rpx;
+			line-height: 50rpx;
+			font-size: 22rpx;
+		}
+
+		.board-header {
+			height: 50rpx;
+		}
+
+		.card_yellow {
+			height: 50rpx;
+			padding: 0rpx 20rpx;
+		}
+
+		.card_board {
+			height: 50rpx;
+			padding: 0rpx 30rpx;
+		}
+
+		.board-title {
+			font-size: 20rpx;
+		}
+
+		.card_time {
+			right: 10rpx;
+		}
+
+		.time_title {
+			font-size: 20rpx;
+			line-height: 20rpx;
+		}
+
+		.current-time {
+			font-size: 15rpx;
+			line-height: 15rpx;
+		}
+
+		.table-container {
+			margin: 10rpx 10rpx 0rpx 10rpx;
+			border-radius: 8rpx;
+		}
+
+		.table-cell {
+			padding: 2rpx 5rpx;
+			font-size: 18rpx;
+		}
+
+		.cell {
+			padding: 2rpx 5rpx;
+			font-size: 18rpx;
+		}
+
+		.card_foot {
+			height: 50rpx;
+		}
+
+		.title_tips {
+			font-size: 15rpx;
+		}
+
+		.title_tips_foot {
+			font-size: 15rpx;
+			padding: 2rpx 2rpx 2rpx 5rpx;
+			border-radius: 8rpx;
+			margin-right: 10rpx;
+		}
+
+		.tips_imageil {
+			width: 30rpx;
+			height: 30rpx;
+		}
+
+		.card_tips_box {
+			padding-left: 10rpx;
+		}
+
+		.empty-tip {
+			padding: 40rpx;
+			font-size: 20rpx;
+		}
+	}
+
+
+	/* 滚动条样式 */
+	.table-body::-webkit-scrollbar {
+		width: 4rpx;
+	}
+
+	.table-body::-webkit-scrollbar-track {
+		background: #f1f1f1;
+		border-radius: 6rpx;
+	}
+
+	.table-body::-webkit-scrollbar-thumb {
+		background: #c1c1c1;
+		border-radius: 6rpx;
+	}
+
+	.table-body::-webkit-scrollbar-thumb:hover {
+		background: #a8a8a8;
+	}
+</style>

+ 749 - 0
pages/index/index.vue

@@ -0,0 +1,749 @@
+<template>
+	<view class="container">
+		<view class="header">
+			<text class="title">接种留观人员信息表</text>
+			<text class="current-time">{{ currentTime }}</text>
+		</view>
+
+		<!-- 表格 -->
+		<view class="table-container">
+			<!-- 表头 -->
+			<view class="table-header">
+				<view class="table-row header-row">
+					<view class="cell name title_color">姓名</view>
+					<view class="cell time title_color">留观时间</view>
+					<view class="cell time title_color">离开时间</view>
+					<view class="cell status title_color">状态</view>
+				</view>
+			</view>
+
+			<!-- 表格主体 - 支持纵向轮播滚动 -->
+			<view class="table-body-container">
+				<view class="table-body-wrapper" :class="{ 'animate': shouldAnimate }">
+					<view v-for="(item, index) in carouselList" :key="index" class="table-row body-row"
+						:class="`item-${getClass(item.status)}`">
+						<view class="cell name">{{ item.patientName }}</view>
+						<view class="cell time">{{ getTime(item.createTime) }}</view>
+						<view class="cell time">{{ getTime(item.outTime) }}</view>
+						<view class="cell status">
+							<text class="status-tag" :class="`status-${getClass(item.status)}`">
+								{{ getStatusText(item.status) }}
+							</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 连接状态与IP编辑栏 -->
+		<view class="status-bar-bottom" :class="`status-${connectionStatus}`">
+			<view class="status-text-box">
+				<text class="dot" :class="`dot-${connectionStatus}`">●</text>
+				<text class="status-text">{{ connectionText }}</text>
+			</view>
+			<!-- IP 编辑区域 -->
+			<view class="ip-input-group">
+				<text class="ip-label">IP:</text>
+				<input type="text" :value="serverIp" @input="onIpInput" placeholder="192.168.0.41" class="ip-input" />
+				<text class="colon">:</text>
+				<input type="number" :value="serverPort" @input="onPortInput" placeholder="8811" class="port-input" />
+				<button class="btn-reconnect" size="mini" @click="handleReconnect">
+					更新并重连
+				</button>
+				<button class="btn-reconnect" size="mini" @click="goback">
+					测试
+				</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				currentTime: '', // 当前时间
+				timeTimer: null, // 时间更新定时器
+				// WebSocket 实例
+				ws: null,
+				reconnectTimer: null,
+				heartbeatTimer: null, // 心跳定时器
+				heartbeatTimeout: null, // 心跳响应超时计时器
+				heartbeatInterval: 30000, // 30秒发一次心跳
+				heartbeatTimeoutTime: 10000, // 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) {
+				// 创建 Date 对象
+				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: () => {
+							console.log('旧连接已关闭');
+							this.connectWebSocket('link');
+						},
+						fail: () => {
+							this.connectWebSocket('link'); // 即使关闭失败也尝试重连
+						}
+					});
+				} else {
+					this.connectWebSocket('link');
+				}
+				setTimeout(() => {
+					uni.hideLoading();
+				}, 2000);
+			},
+			// 建立 WebSocket 连接
+			connectWebSocket(type) {
+				const url = this.getWebSocketUrl();
+				// 创建 SocketTask
+				this.ws = uni.connectSocket({
+					url: url,
+					success: (res) => {
+						console.log('connectSocket success', res);
+					},
+					fail: (err) => {
+						console.error('connectSocket 失败', err);
+						this.connectionStatus = 'disconnected';
+						this.reconnect();
+					}
+				});
+				// --- WebSocket 事件监听 ---
+				this.ws.onOpen((res) => {
+					// console.log('WebSocket 连接成功', res);
+					this.connectionStatus = 'connected';
+					this.ws.send({
+						data: type,
+					});
+					// ✅ 连接成功后启动心跳
+					this.startHeartbeat();
+					uni.hideLoading();
+				});
+				this.ws.onMessage((res) => {
+					// ✅ 处理心跳响应 pong
+					if (res.data === 'PONG' || res.data === '{"type":"PONG"}') {
+						this.isPongReceived = true;
+						// console.log('收到 pong,心跳正常');
+						// ✅ 清除等待 pong 的超时计时器
+						if (this.heartbeatTimeout) {
+							clearTimeout(this.heartbeatTimeout);
+							this.heartbeatTimeout = null;
+						}
+						return;
+					}
+					try {
+						const data = JSON.parse(res.data);
+						// console.log('收到消息:', data);
+						this.handleMessage(data);
+					} catch (e) {
+						console.warn('非 JSON 消息,已忽略', res.data);
+					}
+				});
+				this.ws.onClose((res) => {
+					console.log('WebSocket 连接关闭', res);
+					this.connectionStatus = 'disconnected';
+					this.stopHeartbeat(); // 停止心跳
+					this.reconnect(); // 断线重连
+				});
+				this.ws.onError((err) => {
+					console.error('WebSocket 错误', err);
+					this.connectionStatus = 'disconnected';
+				});
+			},
+			// 处理收到的消息
+			handleMessage(data) {
+				// 1. 全量数据:数组(不是对象,或没有 action 字段)
+				if (data.action == 'link') {
+					this.updateListWithArray(data.data);
+					return;
+				}
+				// 2. 增量操作:对象
+				if (typeof data === 'object' && data !== null) {
+					const action = data.action;
+
+					if (!action) {
+						console.warn('消息缺少 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:
+							console.warn('未知操作类型:', action);
+					}
+				} else {
+					console.warn('收到未知格式消息:', data);
+				}
+			},
+			// 批量新增
+			batchAdd(data) {
+				if (!data) return;
+				const items = Array.isArray(data) ? data : [data]; // 兼容单条
+				const validItems = items.filter(item => item && item.id !== undefined);
+				if (validItems.length === 0) {
+					console.warn('没有有效数据用于新增', 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 === undefined) return;
+					const index = updated.findIndex(i => i.id == item.id);
+					if (index > -1) {
+						updated[index] = {
+							...updated[index],
+							...item
+						};
+					}
+					// else { this.list.push(item); } // 可选:当作新增
+				});
+				this.list = updated;
+			},
+			// 批量删除
+			batchRemove(data) {
+				let idsToRemove = [];
+				if (Array.isArray(data.id)) {
+					// remove: { id: [1,2,3] }
+					idsToRemove = data.id;
+				} else if (data.id !== undefined) {
+					// remove: { id: 1 }
+					idsToRemove = [data.id];
+				} else if (Array.isArray(data.data)) {
+					// remove: { data: [ {id:1}, {id:2} ] }
+					idsToRemove = data.data.map(item => item.id).filter(id => id !== undefined);
+				} else {
+					idsToRemove = [data.id];
+					console.warn('无法解析删除指令', 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 = new Map();
+				newList.forEach(item => {
+					if (item.id !== undefined) {
+						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(() => {
+					console.log('正在尝试重新连接...');
+					this.connectWebSocket('link');
+					this.reconnectTimer = null;
+				}, 3000);
+			},
+			// 启动心跳
+			startHeartbeat() {
+				console.log(2324)
+				// 清除旧心跳
+				this.stopHeartbeat();
+				// 发送 ping 的定时器
+				this.heartbeatTimer = setInterval(() => {
+					if (this.ws && this.connectionStatus === 'connected') {
+						// 检查上一次的 pong 是否已收到,未收到则判定为断线
+						if (!this.isPongReceived) {
+							console.warn('上一次心跳未收到 pong,可能已断线');
+							this.ws.close();
+							return;
+						}
+						// 发送心跳
+						this.isPongReceived = false; // 等待 pong
+						// 设置超时检测:10秒内没收到 pong 就断开
+						this.heartbeatTimeout = setTimeout(() => {
+							if (!this.isPongReceived) {
+								console.warn('⚠️ 心跳超时:未在规定时间内收到 pong,即将重连');
+								uni.showToast({
+									title: '连接异常,正在重连...',
+									icon: 'none',
+									duration: 2000
+								});
+								this.ws.close(); // 触发 onClose → 重连
+							}
+						}, this.heartbeatTimeoutTime); // 使用配置的超时时间(如 10000ms)
+						try {
+							this.ws.send({
+								data: 'PING',
+								success: () => {
+									// console.log('ping 已发送');
+								},
+								fail: (err) => {
+									console.error('ping 发送失败', err);
+									this.ws.close();
+								}
+							});
+						} catch (e) {
+							console.error('发送 ping 异常', e);
+							this.ws.close();
+						}
+					}
+				}, this.heartbeatInterval);
+				// 可选:启动响应超时检测(更严格)
+				// 通常靠"发 ping 后未收到 pong"即可判断
+			},
+			// 停止心跳(断开连接时调用)
+			stopHeartbeat() {
+				if (this.heartbeatTimer) {
+					clearInterval(this.heartbeatTimer);
+					this.heartbeatTimer = null;
+				}
+				if (this.heartbeatTimeout) {
+					clearTimeout(this.heartbeatTimeout);
+					this.heartbeatTimeout = null;
+				}
+			},
+			updateCurrentTime() {
+				const now = 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();
+			}, 1000); // 每秒更新一次时间
+		},
+		// 页面卸载时关闭连接
+		beforeDestroy() {
+			if (this.ws) {
+				this.ws.close();
+			}
+			if (this.reconnectTimer) {
+				clearTimeout(this.reconnectTimer);
+			}
+			this.stopHeartbeat();
+			// 清理时间更新定时器
+			if (this.timeTimer) {
+				clearInterval(this.timeTimer);
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.container {
+		background-color: #f4f6f8;
+		display: flex;
+		flex-direction: column;
+		height: 100vh;
+		padding: 20rpx 30rpx 30rpx 30rpx;
+		background-color: #f4f6f8;
+		font-family: 'Arial', 'Microsoft YaHei', sans-serif;
+		box-sizing: border-box;
+		overflow: hidden;
+		/* 隐藏页面滚动,让轮播在表格内进行 */
+	}
+
+	.header {
+		padding-top: 10rpx;
+		text-align: center;
+		margin-bottom: 30rpx;
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 100rpx;
+	}
+
+	.title {
+		font-size: 60rpx;
+		font-weight: bold;
+		color: #1e3a8a;
+	}
+
+	.current-time {
+		display: flex;
+		align-items: center;
+		position: absolute;
+		display: flex;
+		height: 100rpx;
+		right: 0rpx;
+		font-size: 55rpx;
+		color: #666;
+		background-color: rgba(30, 58, 138, 0.1);
+		padding: 0rpx 30rpx;
+		border-radius: 12rpx;
+		font-weight: bold;
+	}
+
+	.table-container {
+		flex: 1;
+		margin-bottom: 30rpx;
+		border-radius: 24rpx;
+		overflow: hidden;
+		box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+		background-color: #ffffff;
+		border: 2rpx solid #e0e0e0;
+		display: flex;
+		flex-direction: column;
+	}
+
+	.table-header {
+		background-color: #f0f4f8;
+		color: white;
+		font-weight: bold;
+	}
+
+	.header-row {
+		background-color: #1e3a8a;
+		color: white;
+		font-weight: bold;
+	}
+
+	.table-row {
+		display: flex;
+		border-bottom: 2rpx solid #dedede;
+	}
+
+	.body-row {
+		padding: 10rpx 0rpx;
+	}
+
+	.table-row:last-child {
+		border-bottom: none;
+	}
+
+	.cell {
+		padding: 24rpx 16rpx;
+		font-size: 50rpx;
+		text-align: center;
+		color: #333;
+	}
+
+	.name {
+		width: 25%;
+	}
+
+	.time {
+		width: 25%;
+	}
+
+	.status {
+		width: 25%;
+	}
+
+	.status-tag {
+		padding: 8rpx 20rpx;
+		border-radius: 32rpx;
+		font-size: 48rpx;
+		font-weight: 500;
+	}
+
+	.status-observing {
+		background-color: #e3f2fd;
+		color: #007bff;
+		border: 2rpx solid #9acffa;
+	}
+
+	.status-completed {
+		background-color: #e8f5e8;
+		color: #28a745;
+		border: 2rpx solid #8bc34a;
+	}
+
+	.status-warning {
+		background-color: #ffebee; // 浅红背景
+		color: #c62828; // 深红色文字
+		border: 2rpx solid #ef9a9a;
+	}
+
+	.item-observing {
+		background-color: #e3f2fd;
+	}
+
+	.item-completed {
+		background-color: #e8f5e8;
+	}
+
+	.item-warning {
+		background-color: #ffebee; // 浅红背景
+	}
+
+	.item-hasleft {
+		background-color: #e3f2fd;
+	}
+
+	.table-header {
+		flex: 0 0 auto;
+		overflow: hidden;
+	}
+
+	/* 保持表格形式的轮播样式 */
+	.table-body-container {
+		flex: 1;
+		overflow: hidden;
+		background-color: #fafafa;
+		position: relative;
+	}
+
+	.table-body-wrapper {
+		display: block;
+	}
+
+	.table-body-wrapper.animate {
+		animation: scrollUp 20s linear infinite;
+	}
+
+	@keyframes scrollUp {
+		0% {
+			transform: translateY(0);
+		}
+
+		100% {
+			transform: translateY(calc(-50% - 1px));
+			/* 减去1px确保无缝衔接 */
+		}
+	}
+
+	.empty {
+		text-align: center;
+		padding: 60rpx;
+		color: #999;
+		font-style: italic;
+		font-size: 40rpx;
+	}
+
+	// 连接状态栏
+	.status-bar-bottom {
+		padding: 24rpx 30rpx;
+		background-color: #ffffff;
+		box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
+		border-radius: 24rpx;
+		display: flex;
+		gap: 16rpx;
+	}
+
+	.status-text-box {
+		display: flex;
+		align-items: center;
+		font-size: 28rpx;
+	}
+
+	.dot {
+		color: #d32f2f;
+		font-size: 40rpx;
+	}
+
+	.dot-connecting {
+		color: #f9ae3d;
+	}
+
+	.dot-connected {
+		color: #28a745;
+	}
+
+	.dot-disconnected {
+		color: #d32f2f;
+	}
+
+	.status-text {
+		color: #555;
+	}
+
+	.status-text {
+		margin-left: 16rpx;
+	}
+
+	.title_color {
+		color: #fff;
+	}
+
+	// IP 输入区域
+	.ip-input-group {
+		display: flex;
+		align-items: center;
+		margin-left: 16rpx;
+		flex-wrap: wrap;
+		gap: 12rpx;
+	}
+
+	.ip-label {
+		font-size: 28rpx;
+		color: #555;
+	}
+
+	.ip-input,
+	.port-input {
+		border: 2rpx solid #ddd;
+		border-radius: 12rpx;
+		padding: 12rpx 20rpx;
+		font-size: 28rpx;
+		width: 200rpx;
+	}
+
+	.port-input {
+		width: 160rpx;
+	}
+
+	.colon {
+		font-size: 32rpx;
+		color: #666;
+		margin: 0 8rpx;
+	}
+
+	.btn-reconnect {
+		background-color: #007aff;
+		color: white;
+		font-size: 24rpx;
+		padding: 0 16rpx;
+		border-radius: 12rpx;
+	}
+</style>

+ 100 - 0
pages/index/mine.vue

@@ -0,0 +1,100 @@
+<template>
+  <view class="container">
+    <text>服务端IP:</text>
+    <input v-model="serverIp" placeholder="192.168.1.100" />
+    
+    <button @click="scan">🔍 自动扫描</button>
+    <button @click="connect">🚀 连接</button>
+    
+    <text v-if="scanning">扫描中...</text>
+  </view>
+</template>
+
+<script>
+export default {
+  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() {
+      // 1. 试上次 IP
+      const lastIp = uni.getStorageSync('serverIp');
+      if (lastIp && await this.testIp(lastIp)) {
+        return lastIp;
+      }
+
+      // 2. 获取网段
+      const prefix = this.getNetworkPrefix();
+
+      // 3. 扫描 1-50
+      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: 3000,
+          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';
+      });
+    }
+  }
+};
+</script>

+ 804 - 0
pages/index/new_file.vue

@@ -0,0 +1,804 @@
+<template>
+	<view class="notice-board">
+		<view class="board-header">
+			<text class="board-title">人员留观通知</text>
+		</view>
+		
+		<!-- 表格容器 -->
+		<view class="table-container">
+			<!-- 主要表头 -->
+			<view class="table-header">
+				<view class="table-row header-row">
+					<view class="table-cell cell-id">编号</view>
+					<view class="table-cell cell-name">姓名</view>
+					<view class="table-cell cell-time">留观时间</view>
+					<view class="table-cell cell-status">状态</view>
+				</view>
+			</view>
+			
+			<!-- 特殊状态数据展示区域(固定) -->
+			<view class="special-status-container" v-if="specialStatusData.length > 0">
+				<view 
+					class="table-row special-row"
+					v-for="(item, index) in specialStatusData"
+					:key="item.id"
+					:class="{
+						'status-completed': item.status === 1,
+						'status-left': item.status === 2
+					}">
+					<view class="table-cell cell-id">{{ item.id }}</view>
+					<view class="table-cell cell-name">{{ item.name }}</view>
+					<view class="table-cell cell-time">{{ formatTime(item.watchTime) }}</view>
+					<view class="table-cell cell-status">
+						<view class="status-tag" :class="getStatusClass(item.status)">
+							{{ getStatusText(item.status) }}
+						</view>
+					</view>
+				</view>
+				<!-- 分隔线 -->
+				<view class="divider"></view>
+			</view>
+			
+			<!-- 普通数据内容区域 -->
+			<view 
+				class="table-body"
+				@touchstart="handleTouchStart"
+				@touchmove="handleTouchMove"
+				@touchend="handleTouchEnd">
+				<view 
+					class="table-row"
+					v-for="(item, index) in displayNormalData"
+					:key="item.id"
+					:class="{
+						'row-even': index % 2 === 0, 
+						'row-odd': index % 2 === 1,
+						'buffer-row': index === actualPageSize, // 缓冲行
+						'status-watching': item.status === 0
+					}">
+					<view class="table-cell cell-id">{{ item.id }}</view>
+					<view class="table-cell cell-name">{{ item.name }}</view>
+					<view class="table-cell cell-time">{{ formatTime(item.watchTime) }}</view>
+					<view class="table-cell cell-status">
+						<view class="status-tag" :class="getStatusClass(item.status)">
+							{{ getStatusText(item.status) }}
+						</view>
+					</view>
+				</view>
+				
+				<!-- 空数据提示 -->
+				<view v-if="displayNormalData.length === 0 && !loading" class="empty-tip">
+					暂无留观人员信息
+				</view>
+			</view>
+		</view>
+		
+		<!-- 分页控制 -->
+		<view class="pagination-control">
+			<view class="page-info">
+				第 {{ currentPage }} / {{ totalPages }} 页 (共{{ normalData.length }}人)
+			</view>
+			
+			<view class="control-buttons">
+				<button 
+					class="control-btn"
+					:disabled="currentPage <= 1"
+					@click="prevPage">
+					上一页
+				</button>
+				
+				<label class="auto-scroll-label">
+					<switch 
+						:checked="autoScroll" 
+						@change="toggleAutoScroll" 
+						color="#007AFF"/>
+					<text>自动播放</text>
+				</label>
+				
+				<button 
+					class="control-btn"
+					:disabled="currentPage >= totalPages"
+					@click="nextPage">
+					下一页
+				</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			// 所有数据
+			allData: [],
+			// 当前页码
+			currentPage: 1,
+			// 每页显示条数(自适应计算)
+			pageSize: 10,
+			// 是否自动滚动
+			autoScroll: true,
+			// 自动滚动定时器
+			autoScrollTimer: null,
+			// 滚动间隔时间(毫秒)
+			scrollInterval: 3000,
+			// 加载状态
+			loading: false,
+			// 窗口高度
+			windowHeight: 0,
+			// 触摸相关
+			touchStartX: 0,
+			touchStartTime: 0,
+			isTouching: false,
+			touchStartY: 0,
+			// 垂直滑动相关
+			lastSwipeTime: 0
+		}
+	},
+	
+	computed: {
+		// 特殊状态数据(状态1和2)
+		specialStatusData() {
+			return this.allData.filter(item => item.status === 1 || item.status === 2)
+		},
+		
+		// 普通状态数据(状态0)
+		normalData() {
+			return this.allData.filter(item => item.status === 0)
+		},
+		
+		// 实际显示的条数(减去缓冲行)
+		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)
+		}
+	},
+	
+	mounted() {
+		this.initPage()
+		this.loadData()
+		// 在H5平台添加鼠标滚轮支持
+		//#ifdef H5
+		this.addMouseWheelSupport()
+		//#endif
+	},
+	
+	beforeDestroy() {
+		this.stopAutoScroll()
+		uni.offWindowResize(this.handleWindowResize)
+		//#ifdef H5
+		this.removeMouseWheelSupport()
+		//#endif
+	},
+	
+	methods: {
+		// 初始化页面
+		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(100) // 头部高度
+				const paginationHeight = uni.upx2px(120) // 分页区域高度
+				const tableHeaderHeight = uni.upx2px(80) // 表格头部高度
+				const specialDataHeight = this.specialStatusData.length > 0 ? 
+					uni.upx2px(80 * this.specialStatusData.length) : 0 // 特殊数据区域高度 + 分隔线
+				const padding = uni.upx2px(0) // 上下padding
+				
+				const availableHeight = this.windowHeight - headerHeight - paginationHeight - 
+					tableHeaderHeight - specialDataHeight - padding
+				
+				// 计算行高(px)- 固定60rpx
+				const rowHeightPx = uni.upx2px(60)
+				
+				// 计算可显示的行数
+				const calculatedPageSize = Math.floor(availableHeight / rowHeightPx)
+				
+				// 设置页面大小(最少显示4行,最多显示30行)
+				this.pageSize = Math.max(4, Math.min(30, calculatedPageSize))
+				
+				// 验证当前页
+				this.validateCurrentPage()
+				
+			} catch (error) {
+				console.error('计算页面大小失败:', 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
+			}
+		},
+		
+		// 加载数据 - 状态1和2只保留一条实例
+		loadData() {
+			this.loading = true
+			try {
+				// 生成模拟数据
+				const mockData = []
+				const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十', '郑一', '王二']
+				
+				// 生成100条数据
+				for (let i = 1; i <= 100; i++) {
+					const randomName = names[Math.floor(Math.random() * names.length)] + i
+					
+					let status = 0 // 默认状态为0(留观中)
+					
+					// 特殊处理:只让第10条数据状态为1(完成),第20条数据状态为2(提前离开)
+					if (i === 10) {
+						status = 1 // 完成
+					} else if (i === 20) {
+						status = 2 // 提前离开
+					}
+					// 其他所有数据保持状态0(留观中)
+					
+					mockData.push({
+						id: i,
+						name: randomName,
+						watchTime: Date.now() - Math.random() * 24 * 60 * 60 * 1000,
+						status: status // 0=留观中,1=完成,2=提前离开
+					})
+				}
+				this.allData = mockData
+				
+				console.log('数据加载完成,状态分布:', {
+					'留观中(0)': this.normalData.length,
+					'已完成(1)': this.specialStatusData.filter(item => item.status === 1).length,
+					'提前离开(2)': this.specialStatusData.filter(item => item.status === 2).length
+				})
+				
+				// 验证当前页
+				this.validateCurrentPage()
+				
+			} catch (error) {
+				console.error('加载数据失败:', error)
+				uni.showToast({
+					title: '数据加载失败',
+					icon: 'none'
+				})
+			} finally {
+				this.loading = false
+			}
+		},
+		
+		// 格式化时间
+		formatTime(timestamp) {
+			if (!timestamp) return ''
+			const date = new Date(timestamp)
+			return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+		},
+		
+		// 获取状态文本
+		getStatusText(status) {
+			switch (status) {
+				case 0: return '留观中'
+				case 1: return '已完成'
+				case 2: return '提前离开'
+				default: return '未知'
+			}
+		},
+		
+		// 获取状态样式类
+		getStatusClass(status) {
+			switch (status) {
+				case 0: return 'status-watching-tag'
+				case 1: return 'status-completed-tag'
+				case 2: return 'status-left-tag'
+				default: return ''
+			}
+		},
+		
+		// 上一页
+		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平台)
+		//#ifdef H5
+		addMouseWheelSupport() {
+			const tableBody = document.querySelector('.table-body')
+			if (tableBody) {
+				this.wheelHandler = this.debounce((e) => {
+					e.preventDefault()
+					const delta = e.wheelDelta || -e.detail
+					if (delta < 0) {
+						// 向下滚动 - 下一页
+						this.nextPage()
+					} else {
+						// 向上滚动 - 上一页
+						this.prevPage()
+					}
+				}, 150)
+				
+				tableBody.addEventListener('mousewheel', this.wheelHandler, { passive: false })
+				tableBody.addEventListener('DOMMouseScroll', this.wheelHandler, { passive: false })
+			}
+		},
+		
+		// 移除鼠标滚轮支持
+		removeMouseWheelSupport() {
+			const tableBody = document.querySelector('.table-body')
+			if (tableBody && this.wheelHandler) {
+				tableBody.removeEventListener('mousewheel', this.wheelHandler)
+				tableBody.removeEventListener('DOMMouseScroll', this.wheelHandler)
+			}
+		},
+		//#endif
+		
+		// 切换自动滚动
+		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)
+			}
+		}
+	},
+	
+	watch: {
+		// 监听数据变化
+		allData: {
+			handler() {
+				this.$nextTick(() => {
+					this.validateCurrentPage()
+					if (this.autoScroll) {
+						this.startAutoScroll()
+					}
+				})
+			},
+			immediate: true
+		},
+		
+		// 监听页面大小变化
+		pageSize() {
+			this.validateCurrentPage()
+		},
+		
+		// 监听特殊状态数据变化
+		specialStatusData() {
+			// 特殊状态数据变化时重新计算页面大小
+			this.$nextTick(() => {
+				this.calculatePageSize()
+			})
+		}
+	}
+}
+</script>
+
+<style scoped>
+.notice-board {
+	width: 100%;
+	height: 100vh;
+	display: flex;
+	flex-direction: column;
+	background-color: #f0f2f5;
+	padding: 20rpx;
+	box-sizing: border-box;
+}
+
+.board-header {
+	text-align: center;
+	margin-bottom: 20rpx;
+	flex-shrink: 0;
+}
+
+.board-title {
+	font-size: 36rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.table-container {
+	flex: 1;
+	background-color: #fff;
+	border-radius: 16rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+}
+
+.table-header {
+	background: linear-gradient(135deg, #007AFF, #0066CC);
+	color: #fff;
+	flex-shrink: 0;
+}
+
+.table-row {
+	display: flex;
+	flex-direction: row;
+	min-height: 60rpx; /* 控制行高为60rpx */
+	line-height: 60rpx;
+	border-bottom: 1rpx solid #eee;
+	transition: all 0.2s ease;
+}
+
+.header-row {
+	font-weight: bold;
+}
+
+.table-cell {
+	flex: 1;
+	padding: 0 15rpx;
+	font-size: 24rpx;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	display: flex;
+	align-items: center;
+}
+
+.cell-id {
+	flex: 0.5;
+	text-align: center;
+	justify-content: center;
+}
+
+.cell-name {
+	flex: 1.5;
+}
+
+.cell-time {
+	flex: 2;
+}
+
+.cell-status {
+	flex: 1;
+	text-align: center;
+	justify-content: center;
+}
+
+/* 特殊状态数据容器 */
+.special-status-container {
+	background-color: #fff8e6;
+}
+
+.special-row {
+	background-color: #fff8e6 !important;
+	min-height: 60rpx;
+	line-height: 60rpx;
+}
+
+/* 分隔线 */
+.divider {
+	height: 1rpx;
+	background-color: #ddd;
+}
+
+/* 普通数据内容区域 */
+.table-body {
+	flex: 1;
+	overflow-y: auto;
+	/* 添加触摸反馈 */
+	-webkit-overflow-scrolling: touch;
+	touch-action: pan-y;
+}
+
+.row-even {
+	background-color: #fff;
+}
+
+.row-odd {
+	background-color: #fafafa;
+}
+
+/* 状态行样式 */
+.status-watching {
+	background-color: #e6f7ff !important;
+	border-left: 4rpx solid #1890ff;
+}
+
+.status-completed {
+	background-color: #f6ffed !important;
+	border-left: 4rpx solid #52c41a;
+}
+
+.status-left {
+	background-color: #fff2e8 !important;
+	border-left: 4rpx solid #fa8c16;
+}
+
+/* 缓冲行样式 */
+.buffer-row {
+	background-color: #f0f0f0 !important;
+	opacity: 0.6;
+}
+
+/* 状态标签样式 - 控制高度不超过40rpx */
+.status-tag {
+	padding: 4rpx 12rpx;
+	border-radius: 20rpx;
+	font-size: 20rpx;
+	font-weight: normal;
+	line-height: 32rpx;
+	height: 40rpx;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	min-width: 100rpx;
+}
+
+.status-watching-tag {
+	background-color: #e6f7ff;
+	color: #1890ff;
+	border: 1rpx solid #91d5ff;
+}
+
+.status-completed-tag {
+	background-color: #f6ffed;
+	color: #52c41a;
+	border: 1rpx solid #b7eb8f;
+}
+
+.status-left-tag {
+	background-color: #fff2e8;
+	color: #fa8c16;
+	border: 1rpx solid #ffbb96;
+}
+
+.empty-tip {
+	text-align: center;
+	padding: 80rpx;
+	color: #999;
+	font-size: 28rpx;
+}
+
+/* 分页控制 */
+.pagination-control {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	margin: 20rpx 0;
+	padding: 0 20rpx;
+	flex-shrink: 0;
+	background-color: #fff;
+	border-radius: 12rpx;
+	padding: 20rpx;
+	box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+}
+
+.page-info {
+	font-size: 24rpx;
+	color: #666;
+	margin-bottom: 20rpx;
+}
+
+.control-buttons {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+	width: 100%;
+}
+
+.control-btn {
+	flex: 1;
+	height: 60rpx;
+	line-height: 60rpx;
+	font-size: 24rpx;
+	background: linear-gradient(135deg, #007AFF, #0066CC);
+	color: #fff;
+	border: none;
+	border-radius: 10rpx;
+	margin: 0 10rpx;
+}
+
+.control-btn[disabled] {
+	background: #ccc;
+	opacity: 0.6;
+}
+
+.auto-scroll-label {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: center;
+	font-size: 24rpx;
+	color: #666;
+	flex: 1;
+}
+
+.auto-scroll-label text {
+	margin-left: 10rpx;
+}
+
+/* 响应式优化 */
+@media screen and (max-height: 600px) {
+	.table-row {
+		min-height: 50rpx;
+		line-height: 50rpx;
+	}
+	
+	.table-cell {
+		font-size: 22rpx;
+		padding: 0 10rpx;
+	}
+	
+	.status-tag {
+		padding: 2rpx 10rpx;
+		font-size: 18rpx;
+		line-height: 28rpx;
+		height: 32rpx;
+		min-width: 80rpx;
+	}
+	
+	.control-btn {
+		height: 50rpx;
+		line-height: 50rpx;
+		font-size: 22rpx;
+	}
+}
+
+/* 滚动条样式 */
+.table-body::-webkit-scrollbar {
+	width: 4rpx;
+}
+
+.table-body::-webkit-scrollbar-track {
+	background: #f1f1f1;
+	border-radius: 6rpx;
+}
+
+.table-body::-webkit-scrollbar-thumb {
+	background: #c1c1c1;
+	border-radius: 6rpx;
+}
+
+.table-body::-webkit-scrollbar-thumb:hover {
+	background: #a8a8a8;
+}
+</style>

BIN
static/horn.png


BIN
static/logo.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
static/logo.svg


+ 13 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,13 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => {
+        if (!res) return resolve(res) 
+        return res[0] ? reject(res[0]) : resolve(res[1])
+      });
+    });
+  },
+});

+ 76 - 0
uni.scss

@@ -0,0 +1,76 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;

BIN
unpackage/cache/apk/__UNI__E207749_cm.apk


+ 1 - 0
unpackage/cache/apk/apkurl

@@ -0,0 +1 @@
+https://app.liuyingyong.cn/build/download/b82d1cb0-9781-11f0-8696-6185dc159729

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/apk/cmManifestCache.json


+ 4 - 0
unpackage/cache/certdata

@@ -0,0 +1,4 @@
+andrCertfile=E:/baozhida/证书/接种留观/07b479c057fd8ecb245c415082a74aa2.keystore
+andrCertAlias=__uni__e207749
+andrCertPass=2h5Lho0ZddgVgnW14jxAJA==
+storePassword=2h5Lho0ZddgVgnW14jxAJA==

BIN
unpackage/cache/wgt/__UNI__E207749/.manifest/google-keystore.keystore


BIN
unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-hdpi.png


BIN
unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xhdpi.png


BIN
unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xxhdpi.png


BIN
unpackage/cache/wgt/__UNI__E207749/.manifest/icon-android-xxxhdpi.png


Файловите разлики са ограничени, защото са твърде много
+ 15 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappautomator.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappchooselocation.js


BIN
unpackage/cache/wgt/__UNI__E207749/__uniapperror.png


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappopenlocation.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/cache/wgt/__UNI__E207749/__uniapppicker.js


Файловите разлики са ограничени, защото са твърде много
+ 6 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappquill.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappquillimageresize.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappscan.js


BIN
unpackage/cache/wgt/__UNI__E207749/__uniappsuccess.png


+ 24 - 0
unpackage/cache/wgt/__UNI__E207749/__uniappview.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>View</title>
+    <link rel="icon" href="data:,">
+    <link rel="stylesheet" href="app.css" />
+    <script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="uni-app-view.umd.js"></script>
+    
+    
+    
+  </body>
+</html>

+ 11 - 0
unpackage/cache/wgt/__UNI__E207749/app-config-service.js

@@ -0,0 +1,11 @@
+
+  ;(function(){
+  let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
+  const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"留观系统","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.76","entryPagePath":"pages/index/home","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
+  const __uniRoutes = [{"path":"pages/index/home","meta":{"isQuit":true,"isEntry":true,"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/index","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/mine","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
+  __uniConfig.styles=[];//styles
+  __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:16})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:u,window:u,document:u,frames:u,self:u,location:u,navigator:u,localStorage:u,history:u,Caches:u,screen:u,alert:u,confirm:u,prompt:u,fetch:u,XMLHttpRequest:u,WebSocket:u,webkit:u,print:u}}}}); 
+  })();
+  

+ 1 - 0
unpackage/cache/wgt/__UNI__E207749/app-config.js

@@ -0,0 +1 @@
+(function(){})();

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/app-service.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/app.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/manifest.json


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/pages/index/home.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/pages/index/index.css


+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/pages/index/mine.css


BIN
unpackage/cache/wgt/__UNI__E207749/static/horn.png


BIN
unpackage/cache/wgt/__UNI__E207749/static/logo.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/static/logo.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/cache/wgt/__UNI__E207749/uni-app-view.umd.js


BIN
unpackage/debug/android_debug.apk


+ 11 - 0
unpackage/dist/build/.nvue/app.css.js

@@ -0,0 +1,11 @@
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __commonJS = (cb, mod) => function __require() {
+  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var require_app_css = __commonJS({
+  "app.css.js"(exports) {
+    const _style_0 = {};
+    exports.styles = [_style_0];
+  }
+});
+export default require_app_css();

+ 2 - 0
unpackage/dist/build/.nvue/app.js

@@ -0,0 +1,2 @@
+Promise.resolve("./app.css.js").then(() => {
+});

Файловите разлики са ограничени, защото са твърде много
+ 15 - 0
unpackage/dist/build/app-plus/__uniappautomator.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/build/app-plus/__uniappchooselocation.js


BIN
unpackage/dist/build/app-plus/__uniapperror.png


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/build/app-plus/__uniappopenlocation.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/build/app-plus/__uniapppicker.js


Файловите разлики са ограничени, защото са твърде много
+ 6 - 0
unpackage/dist/build/app-plus/__uniappquill.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/__uniappquillimageresize.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/build/app-plus/__uniappscan.js


BIN
unpackage/dist/build/app-plus/__uniappsuccess.png


+ 24 - 0
unpackage/dist/build/app-plus/__uniappview.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>View</title>
+    <link rel="icon" href="data:,">
+    <link rel="stylesheet" href="app.css" />
+    <script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="uni-app-view.umd.js"></script>
+    
+    
+    
+  </body>
+</html>

+ 11 - 0
unpackage/dist/build/app-plus/app-config-service.js

@@ -0,0 +1,11 @@
+
+  ;(function(){
+  let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
+  const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"留观系统","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.76","entryPagePath":"pages/index/home","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
+  const __uniRoutes = [{"path":"pages/index/home","meta":{"isQuit":true,"isEntry":true,"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/index","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/mine","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
+  __uniConfig.styles=[];//styles
+  __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:16})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:u,window:u,document:u,frames:u,self:u,location:u,navigator:u,localStorage:u,history:u,Caches:u,screen:u,alert:u,confirm:u,prompt:u,fetch:u,XMLHttpRequest:u,WebSocket:u,webkit:u,print:u}}}}); 
+  })();
+  

+ 1 - 0
unpackage/dist/build/app-plus/app-config.js

@@ -0,0 +1 @@
+(function(){})();

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/app-service.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/app.css


+ 163 - 0
unpackage/dist/build/app-plus/manifest.json

@@ -0,0 +1,163 @@
+{
+  "@platforms": [
+    "android",
+    "iPhone",
+    "iPad"
+  ],
+  "id": "__UNI__E207749",
+  "name": "留观系统",
+  "version": {
+    "name": "1.1.0",
+    "code": 110
+  },
+  "description": "",
+  "developer": {
+    "name": "",
+    "email": "",
+    "url": ""
+  },
+  "permissions": {
+    "UniNView": {
+      "description": "UniNView原生渲染"
+    }
+  },
+  "plus": {
+    "useragent": {
+      "value": "uni-app",
+      "concatenate": true
+    },
+    "splashscreen": {
+      "target": "id:1",
+      "autoclose": true,
+      "waiting": true,
+      "delay": 0
+    },
+    "popGesture": "close",
+    "launchwebview": {
+      "render": "always",
+      "id": "1",
+      "kernel": "WKWebview"
+    },
+    "usingComponents": true,
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "distribute": {
+      "icons": {
+        "android": {
+          "hdpi": "unpackage/res/icons/72x72.png",
+          "xhdpi": "unpackage/res/icons/96x96.png",
+          "xxhdpi": "unpackage/res/icons/144x144.png",
+          "xxxhdpi": "unpackage/res/icons/192x192.png"
+        },
+        "ios": {
+          "appstore": "unpackage/res/icons/1024x1024.png",
+          "ipad": {
+            "app": "unpackage/res/icons/76x76.png",
+            "app@2x": "unpackage/res/icons/152x152.png",
+            "notification": "unpackage/res/icons/20x20.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "proapp@2x": "unpackage/res/icons/167x167.png",
+            "settings": "unpackage/res/icons/29x29.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "spotlight": "unpackage/res/icons/40x40.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png"
+          },
+          "iphone": {
+            "app@2x": "unpackage/res/icons/120x120.png",
+            "app@3x": "unpackage/res/icons/180x180.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "notification@3x": "unpackage/res/icons/60x60.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "settings@3x": "unpackage/res/icons/87x87.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png",
+            "spotlight@3x": "unpackage/res/icons/120x120.png"
+          }
+        }
+      },
+      "google": {
+        "permissions": [
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-feature android:name=\"android.hardware.camera\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+        ],
+        "abiFilters": [
+          "armeabi-v7a",
+          "arm64-v8a",
+          "x86"
+        ],
+        "minSdkVersion": 21
+      },
+      "apple": {
+        "dSYMs": false
+      },
+      "plugins": {
+        "audio": {
+          "mp3": {
+            "description": "Android平台录音支持MP3格式文件"
+          }
+        }
+      }
+    },
+    "nativePlugins": {
+      "Alikes-NetTools": {
+        "__plugin_info__": {
+          "name": "局域网设备助手",
+          "description": "局域网内设备发现,通过ping发现相同网段内的在线设备,从而实现设备列表的查看",
+          "platforms": "Android",
+          "url": "https://ext.dcloud.net.cn/plugin?id=7437",
+          "android_package_name": "uni.app.UNIE207749",
+          "ios_bundle_id": "",
+          "isCloud": true,
+          "bought": 1,
+          "pid": "7437",
+          "parameters": {}
+        }
+      }
+    },
+    "statusbar": {
+      "immersed": "supportedDevice",
+      "style": "dark",
+      "background": "#F8F8F8"
+    },
+    "uniStatistics": {
+      "enable": false
+    },
+    "allowsInlineMediaPlayback": true,
+    "uni-app": {
+      "control": "uni-v3",
+      "vueVersion": "3",
+      "compilerVersion": "4.76",
+      "nvueCompiler": "uni-app",
+      "renderer": "auto",
+      "nvue": {
+        "flex-direction": "column"
+      },
+      "nvueLaunchMode": "normal",
+      "webView": {
+        "minUserAgentVersion": "49.0"
+      }
+    }
+  },
+  "app-harmony": {
+    "useragent": {
+      "value": "uni-app",
+      "concatenate": true
+    },
+    "uniStatistics": {
+      "enable": false
+    }
+  },
+  "launch_path": "__uniappview.html"
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/home.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/pages/index/index.css


+ 0 - 0
unpackage/dist/build/app-plus/pages/index/mine.css


BIN
unpackage/dist/build/app-plus/static/horn.png


BIN
unpackage/dist/build/app-plus/static/logo.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/static/logo.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/build/app-plus/uni-app-view.umd.js


+ 8 - 0
unpackage/dist/cache/.vite/deps/_metadata.json

@@ -0,0 +1,8 @@
+{
+  "hash": "4ee7720d",
+  "configHash": "e4c575f2",
+  "lockfileHash": "e3b0c442",
+  "browserHash": "065c81da",
+  "optimized": {},
+  "chunks": {}
+}

+ 3 - 0
unpackage/dist/cache/.vite/deps/package.json

@@ -0,0 +1,3 @@
+{
+  "type": "module"
+}

+ 11 - 0
unpackage/dist/dev/.nvue/app.css.js

@@ -0,0 +1,11 @@
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __commonJS = (cb, mod) => function __require() {
+  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var require_app_css = __commonJS({
+  "app.css.js"(exports) {
+    const _style_0 = {};
+    exports.styles = [_style_0];
+  }
+});
+export default require_app_css();

+ 2 - 0
unpackage/dist/dev/.nvue/app.js

@@ -0,0 +1,2 @@
+Promise.resolve("./app.css.js").then(() => {
+});

Файловите разлики са ограничени, защото са твърде много
+ 15 - 0
unpackage/dist/dev/app-plus/__uniappautomator.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/dev/app-plus/__uniappchooselocation.js


BIN
unpackage/dist/dev/app-plus/__uniapperror.png


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/dev/app-plus/__uniappopenlocation.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/dev/app-plus/__uniapppicker.js


Файловите разлики са ограничени, защото са твърде много
+ 6 - 0
unpackage/dist/dev/app-plus/__uniappquill.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/app-plus/__uniappquillimageresize.js


Файловите разлики са ограничени, защото са твърде много
+ 31 - 0
unpackage/dist/dev/app-plus/__uniappscan.js


BIN
unpackage/dist/dev/app-plus/__uniappsuccess.png


+ 24 - 0
unpackage/dist/dev/app-plus/__uniappview.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>View</title>
+    <link rel="icon" href="data:,">
+    <link rel="stylesheet" href="app.css" />
+    <script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="uni-app-view.umd.js"></script>
+    
+    
+    
+  </body>
+</html>

+ 11 - 0
unpackage/dist/dev/app-plus/app-config-service.js

@@ -0,0 +1,11 @@
+
+  ;(function(){
+  let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
+  const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"留观系统","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.76","entryPagePath":"pages/index/home","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
+  const __uniRoutes = [{"path":"pages/index/home","meta":{"isQuit":true,"isEntry":true,"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/index","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}},{"path":"pages/index/mine","meta":{"navigationBar":{"style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
+  __uniConfig.styles=[];//styles
+  __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
+  service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:16})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:u,window:u,document:u,frames:u,self:u,location:u,navigator:u,localStorage:u,history:u,Caches:u,screen:u,alert:u,confirm:u,prompt:u,fetch:u,XMLHttpRequest:u,WebSocket:u,webkit:u,print:u}}}}); 
+  })();
+  

+ 1 - 0
unpackage/dist/dev/app-plus/app-config.js

@@ -0,0 +1 @@
+(function(){})();

+ 1755 - 0
unpackage/dist/dev/app-plus/app-service.js

@@ -0,0 +1,1755 @@
+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);

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/app-plus/app.css


+ 163 - 0
unpackage/dist/dev/app-plus/manifest.json

@@ -0,0 +1,163 @@
+{
+  "@platforms": [
+    "android",
+    "iPhone",
+    "iPad"
+  ],
+  "id": "__UNI__E207749",
+  "name": "留观系统",
+  "version": {
+    "name": "1.0.8",
+    "code": 108
+  },
+  "description": "",
+  "developer": {
+    "name": "",
+    "email": "",
+    "url": ""
+  },
+  "permissions": {
+    "UniNView": {
+      "description": "UniNView原生渲染"
+    }
+  },
+  "plus": {
+    "useragent": {
+      "value": "uni-app",
+      "concatenate": true
+    },
+    "splashscreen": {
+      "target": "id:1",
+      "autoclose": true,
+      "waiting": true,
+      "delay": 0
+    },
+    "popGesture": "close",
+    "launchwebview": {
+      "render": "always",
+      "id": "1",
+      "kernel": "WKWebview"
+    },
+    "usingComponents": true,
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "distribute": {
+      "icons": {
+        "android": {
+          "hdpi": "unpackage/res/icons/72x72.png",
+          "xhdpi": "unpackage/res/icons/96x96.png",
+          "xxhdpi": "unpackage/res/icons/144x144.png",
+          "xxxhdpi": "unpackage/res/icons/192x192.png"
+        },
+        "ios": {
+          "appstore": "unpackage/res/icons/1024x1024.png",
+          "ipad": {
+            "app": "unpackage/res/icons/76x76.png",
+            "app@2x": "unpackage/res/icons/152x152.png",
+            "notification": "unpackage/res/icons/20x20.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "proapp@2x": "unpackage/res/icons/167x167.png",
+            "settings": "unpackage/res/icons/29x29.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "spotlight": "unpackage/res/icons/40x40.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png"
+          },
+          "iphone": {
+            "app@2x": "unpackage/res/icons/120x120.png",
+            "app@3x": "unpackage/res/icons/180x180.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "notification@3x": "unpackage/res/icons/60x60.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "settings@3x": "unpackage/res/icons/87x87.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png",
+            "spotlight@3x": "unpackage/res/icons/120x120.png"
+          }
+        }
+      },
+      "google": {
+        "permissions": [
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-feature android:name=\"android.hardware.camera\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+        ],
+        "abiFilters": [
+          "armeabi-v7a",
+          "arm64-v8a",
+          "x86"
+        ],
+        "minSdkVersion": 21
+      },
+      "apple": {
+        "dSYMs": false
+      },
+      "plugins": {
+        "audio": {
+          "mp3": {
+            "description": "Android平台录音支持MP3格式文件"
+          }
+        }
+      }
+    },
+    "nativePlugins": {
+      "Alikes-NetTools": {
+        "__plugin_info__": {
+          "name": "局域网设备助手",
+          "description": "局域网内设备发现,通过ping发现相同网段内的在线设备,从而实现设备列表的查看",
+          "platforms": "Android",
+          "url": "https://ext.dcloud.net.cn/plugin?id=7437",
+          "android_package_name": "uni.app.UNIE207749",
+          "ios_bundle_id": "",
+          "isCloud": true,
+          "bought": 1,
+          "pid": "7437",
+          "parameters": {}
+        }
+      }
+    },
+    "statusbar": {
+      "immersed": "supportedDevice",
+      "style": "dark",
+      "background": "#F8F8F8"
+    },
+    "uniStatistics": {
+      "enable": false
+    },
+    "allowsInlineMediaPlayback": true,
+    "uni-app": {
+      "control": "uni-v3",
+      "vueVersion": "3",
+      "compilerVersion": "4.76",
+      "nvueCompiler": "uni-app",
+      "renderer": "auto",
+      "nvue": {
+        "flex-direction": "column"
+      },
+      "nvueLaunchMode": "normal",
+      "webView": {
+        "minUserAgentVersion": "49.0"
+      }
+    }
+  },
+  "app-harmony": {
+    "useragent": {
+      "value": "uni-app",
+      "concatenate": true
+    },
+    "uniStatistics": {
+      "enable": false
+    }
+  },
+  "launch_path": "__uniappview.html"
+}

+ 501 - 0
unpackage/dist/dev/app-plus/pages/index/home.css

@@ -0,0 +1,501 @@
+
+.notice-board[data-v-760d994e] {
+		width: 100%;
+		height: 100vh;
+		display: flex;
+		flex-direction: column;
+		background-color: #0d54ec;
+		/* padding: 20rpx; */
+		box-sizing: border-box;
+}
+.card_logo[data-v-760d994e] {
+		position: absolute;
+		left: 0.9375rem;
+		top: 0;
+		bottom: 0;
+		display: flex;
+		align-items: center;
+		cursor: pointer;
+}
+.card_logo[data-v-760d994e]:focus {
+		border: none !important;
+}
+.board-header[data-v-760d994e] {
+		/* padding-top: 10rpx; */
+		text-align: center;
+		/* margin-bottom: 30rpx; */
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 4.0625rem;
+		flex-shrink: 0;
+		background-color: #0d54ec;
+		box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2);
+}
+.card_yellow[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: fit-content;
+		width: -webkit-fit-content;
+		width: -moz-fit-content;
+		padding: 0 1.25rem;
+		height: 4.0625rem;
+		background-color: #e5cb8d;
+		border-bottom-right-radius: 9.375rem;
+		border-top-left-radius: 9.375rem;
+}
+.card_board[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: fit-content;
+		width: -webkit-fit-content;
+		width: -moz-fit-content;
+		padding: 0 4.375rem;
+		height: 4.0625rem;
+		background-color: #d5f3ff;
+		border-bottom-right-radius: 3.75rem;
+		border-top-left-radius: 3.75rem;
+}
+.board-title[data-v-760d994e] {
+		font-size: 1.875rem;
+		font-weight: bold;
+		color: #0e6699;
+}
+.card_time[data-v-760d994e] {
+		position: absolute;
+		right: 0.9375rem;
+		top: 0;
+		bottom: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: flex-end;
+		justify-content: center;
+}
+.time_title[data-v-760d994e] {
+		font-size: 1.875rem;
+		color: #fff;
+		font-weight: bold;
+		line-height: 1.875rem;
+}
+.current-time[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		display: flex;
+		font-size: 1.25rem;
+		color: #fff;
+		font-weight: bold;
+		line-height: 1.40625rem;
+}
+.table-container[data-v-760d994e] {
+		flex: 1;
+		margin: 0.625rem 0.625rem 0 0.625rem;
+		border-radius: 0.75rem;
+		overflow: hidden;
+		box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
+		background-color: #ffffff;
+		display: flex;
+		flex-direction: column;
+}
+.table-header[data-v-760d994e] {
+		background-color: #71a0ee;
+		color: white;
+		font-weight: bold;
+}
+.table-row[data-v-760d994e] {
+		display: flex;
+		border-bottom: 0.0625rem solid #dedede;
+}
+.header-row[data-v-760d994e] {
+		/* background-color: #71a0ee; */
+		color: white;
+		font-weight: bold;
+}
+.table-cell[data-v-760d994e] {
+		padding: 0.75rem 0.5rem;
+		font-size: 1.5625rem;
+		text-align: center;
+		color: #333;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+}
+.cell[data-v-760d994e] {
+		padding: 0.75rem 0.5rem;
+		font-size: 1.5625rem;
+		text-align: center;
+		color: #333;
+}
+.name[data-v-760d994e] {
+		width: 25%;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+}
+.time[data-v-760d994e] {
+		width: 25%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+}
+.status[data-v-760d994e] {
+		width: 25%;
+}
+.title_color[data-v-760d994e] {
+		color: #fff;
+}
+
+	/* 特殊状态数据容器 */
+.special-status-container[data-v-760d994e] {
+		background-color: #fff8e6;
+}
+.special-row[data-v-760d994e] {
+		background-color: #fff8e6 !important;
+		min-height: 1.875rem;
+		line-height: 1.875rem;
+}
+
+	/* 分隔线 */
+.divider[data-v-760d994e] {
+		height: 0.03125rem;
+		background-color: #ddd;
+}
+
+	/* 普通数据内容区域 */
+.table-body[data-v-760d994e] {
+		flex: 1;
+		overflow-y: auto;
+		/* 添加触摸反馈 */
+		-webkit-overflow-scrolling: touch;
+		touch-action: pan-y;
+}
+.title-observing[data-v-760d994e] {
+		color: #007bff;
+}
+.title-completed[data-v-760d994e] {
+		color: #28a745;
+}
+.title-warning[data-v-760d994e] {
+		color: #ff0000;
+}
+.title-hasleft[data-v-760d994e] {
+		color: #fa8c16;
+}
+
+	/* 状态行样式 */
+.status-observing[data-v-760d994e] {
+		background-color: #9dd6ff;
+		color: #007bff;
+		border: 0.0625rem solid #48a0ff;
+}
+.status-completed[data-v-760d994e] {
+		background-color: #e8f5e8;
+		color: #28a745;
+		border: 0.0625rem solid #8bc34a;
+}
+.status-warning[data-v-760d994e] {
+		background-color: #ffebee;
+		color: #ff0000;
+		border: 0.0625rem solid #ef9a9a;
+}
+.status-hasleft[data-v-760d994e] {
+		background-color: #fff2e8;
+		color: #fa8c16;
+		border: 0.0625rem solid #ffbb96;
+}
+.item-observing[data-v-760d994e] {
+		background-color: #9dd6ff;
+}
+.item-completed[data-v-760d994e] {
+		background-color: #e8f5e8;
+}
+.item-warning[data-v-760d994e] {
+		background-color: #ffebee;
+}
+.item-hasleft[data-v-760d994e] {
+		background-color: #fff2e8;
+}
+
+	/* 状态标签样式 - 控制高度不超过40rpx */
+.status-tag[data-v-760d994e] {
+		flex: none;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 0.25rem 0.625rem;
+		border-radius: 1rem;
+		font-size: 1.5rem;
+		font-weight: 500;
+		width: calc(100% - 0.625rem);
+}
+.empty-tip[data-v-760d994e] {
+		text-align: center;
+		padding: 2.5rem;
+		color: #999;
+		font-size: 1.875rem;
+		font-style: italic;
+}
+
+	/* 底部 */
+.card_foot[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		height: 3.75rem;
+		box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
+}
+.card_tips_box[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		padding-left: 0.46875rem;
+}
+.title_tips[data-v-760d994e] {
+		color: #fff;
+		font-size: 1.25rem;
+}
+.title_tips_foot[data-v-760d994e] {
+		color: #fff;
+		flex: 1;
+		font-size: 1.25rem;
+		border-radius: 0.625rem;
+		margin-right: 0.625rem;
+		background-color: #71a0ee;
+		padding: 0.625rem;
+}
+.box_link_set[data-v-760d994e] {
+		position: fixed;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		height: 100%;
+		background-color: rgba(0, 0, 0, 0.3);
+}
+.box_popup[data-v-760d994e] {
+		position: relative;
+		background-color: #ffffff;
+		border-radius: 0.25rem;
+}
+.head_popup_title[data-v-760d994e] {
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+}
+.title_head_popup[data-v-760d994e] {
+		font-size: 1.25rem;
+		font-weight: 500;
+		padding: 0.9375rem 0.9375rem 0 0.9375rem;
+}
+.close_title[data-v-760d994e] {
+		position: absolute;
+		right: 0.9375rem;
+		font-size: 1.25rem;
+		cursor: pointer;
+}
+
+	/* // 连接状态栏 */
+.status-bar-bottom[data-v-760d994e] {
+		display: flex;
+		flex-direction: column;
+		margin: 0.9375rem;
+		padding: 0.75rem 0.9375rem;
+		background-color: #ffffff;
+		box-shadow: 0 -0.125rem 0.625rem rgba(0, 0, 0, 0.05);
+		border-radius: 0.75rem;
+		display: flex;
+		gap: 0.5rem;
+}
+.status-text-box[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		font-size: 0.875rem;
+}
+.dot[data-v-760d994e] {
+		color: #d32f2f;
+		font-size: 0.75rem;
+}
+.dot-connecting[data-v-760d994e] {
+		color: #f9ae3d;
+}
+.dot-connected[data-v-760d994e] {
+		color: #28a745;
+}
+.dot-disconnected[data-v-760d994e] {
+		color: #d32f2f;
+}
+.status-text[data-v-760d994e] {
+		color: #555;
+}
+.status-text[data-v-760d994e] {
+		margin-left: 0.5rem;
+}
+.title_color[data-v-760d994e] {
+		color: #fff;
+}
+
+	/* // IP 输入区域 */
+.ip-input-group[data-v-760d994e] {
+		display: flex;
+		align-items: center;
+		margin-left: 0.5rem;
+		flex-wrap: wrap;
+		gap: 0.375rem;
+}
+.ip-label[data-v-760d994e] {
+		font-size: 0.875rem;
+		color: #555;
+}
+.ip-input[data-v-760d994e],
+	.port-input[data-v-760d994e] {
+		border: 0.0625rem solid #ddd;
+		border-radius: 0.375rem;
+		padding: 0.375rem 0.625rem;
+		font-size: 0.875rem;
+		width: 6.25rem;
+}
+.port-input[data-v-760d994e] {
+		width: 5rem;
+}
+.colon[data-v-760d994e] {
+		font-size: 1rem;
+		color: #666;
+		margin: 0 0.25rem;
+}
+.btn-reconnect[data-v-760d994e] {
+		background-color: #007aff;
+		color: white;
+		font-size: 0.75rem;
+		padding: 0 0.5rem;
+		border-radius: 0.375rem;
+}
+.logo_image[data-v-760d994e] {
+		width: 2.5rem;
+		height: 2.5rem;
+}
+.logo_title[data-v-760d994e] {
+		font-size: 1.5625rem;
+		color: #fff;
+		font-weight: bold;
+		margin-left: 0.46875rem;
+}
+.tips_imageil[data-v-760d994e] {
+		width: 2.1875rem;
+		height: 2.1875rem;
+}
+
+	/* 响应式优化 */
+@media screen and (max-height: 600px) {
+.card_logo[data-v-760d994e] {
+			left: 0.3125rem;
+}
+.logo_title[data-v-760d994e] {
+			font-size: 0.5625rem;
+			margin-left: 0.15625rem;
+}
+.logo_image[data-v-760d994e] {
+			width: 1.09375rem;
+			height: 1.09375rem;
+}
+.table-row[data-v-760d994e] {
+			min-height: 1.09375rem;
+			line-height: 1.09375rem;
+			padding: 0.125rem 0;
+}
+.table-cell[data-v-760d994e] {
+			font-size: 0.6875rem;
+			padding: 0.9375rem 0.3125rem;
+}
+.status-tag[data-v-760d994e] {
+			padding: 0.125rem 0.3125rem;
+			font-size: 0.5625rem;
+			height: 0.6875rem;
+			min-width: 2.5rem;
+}
+.control-btn[data-v-760d994e] {
+			height: 1.5625rem;
+			line-height: 1.5625rem;
+			font-size: 0.6875rem;
+}
+.board-header[data-v-760d994e] {
+			height: 1.5625rem;
+}
+.card_yellow[data-v-760d994e] {
+			height: 1.5625rem;
+			padding: 0 0.625rem;
+}
+.card_board[data-v-760d994e] {
+			height: 1.5625rem;
+			padding: 0 0.9375rem;
+}
+.board-title[data-v-760d994e] {
+			font-size: 0.625rem;
+}
+.card_time[data-v-760d994e] {
+			right: 0.3125rem;
+}
+.time_title[data-v-760d994e] {
+			font-size: 0.625rem;
+			line-height: 0.625rem;
+}
+.current-time[data-v-760d994e] {
+			font-size: 0.46875rem;
+			line-height: 0.46875rem;
+}
+.table-container[data-v-760d994e] {
+			margin: 0.3125rem 0.3125rem 0 0.3125rem;
+			border-radius: 0.25rem;
+}
+.table-cell[data-v-760d994e] {
+			padding: 0.0625rem 0.15625rem;
+			font-size: 0.5625rem;
+}
+.cell[data-v-760d994e] {
+			padding: 0.0625rem 0.15625rem;
+			font-size: 0.5625rem;
+}
+.card_foot[data-v-760d994e] {
+			height: 1.5625rem;
+}
+.title_tips[data-v-760d994e] {
+			font-size: 0.46875rem;
+}
+.title_tips_foot[data-v-760d994e] {
+			font-size: 0.46875rem;
+			padding: 0.0625rem 0.0625rem 0.0625rem 0.15625rem;
+			border-radius: 0.25rem;
+			margin-right: 0.3125rem;
+}
+.tips_imageil[data-v-760d994e] {
+			width: 0.9375rem;
+			height: 0.9375rem;
+}
+.card_tips_box[data-v-760d994e] {
+			padding-left: 0.3125rem;
+}
+.empty-tip[data-v-760d994e] {
+			padding: 1.25rem;
+			font-size: 0.625rem;
+}
+}
+
+
+	/* 滚动条样式 */
+.table-body[data-v-760d994e]::-webkit-scrollbar {
+		width: 0.125rem;
+}
+.table-body[data-v-760d994e]::-webkit-scrollbar-track {
+		background: #f1f1f1;
+		border-radius: 0.1875rem;
+}
+.table-body[data-v-760d994e]::-webkit-scrollbar-thumb {
+		background: #c1c1c1;
+		border-radius: 0.1875rem;
+}
+.table-body[data-v-760d994e]::-webkit-scrollbar-thumb:hover {
+		background: #a8a8a8;
+}

+ 248 - 0
unpackage/dist/dev/app-plus/pages/index/index.css

@@ -0,0 +1,248 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+/* 颜色变量 */
+/* 行为相关颜色 */
+/* 文字基本颜色 */
+/* 背景颜色 */
+/* 边框颜色 */
+/* 尺寸变量 */
+/* 文字尺寸 */
+/* 图片尺寸 */
+/* Border Radius */
+/* 水平间距 */
+/* 垂直间距 */
+/* 透明度 */
+/* 文章场景相关 */
+.container[data-v-1cf27b2a] {
+  background-color: #f4f6f8;
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  padding: 0.625rem 0.9375rem 0.9375rem 0.9375rem;
+  background-color: #f4f6f8;
+  font-family: "Arial", "Microsoft YaHei", sans-serif;
+  box-sizing: border-box;
+  overflow: hidden;
+  /* 隐藏页面滚动,让轮播在表格内进行 */
+}
+.header[data-v-1cf27b2a] {
+  padding-top: 0.3125rem;
+  text-align: center;
+  margin-bottom: 0.9375rem;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 3.125rem;
+}
+.title[data-v-1cf27b2a] {
+  font-size: 1.875rem;
+  font-weight: bold;
+  color: #1e3a8a;
+}
+.current-time[data-v-1cf27b2a] {
+  display: flex;
+  align-items: center;
+  position: absolute;
+  display: flex;
+  height: 3.125rem;
+  right: 0;
+  font-size: 1.71875rem;
+  color: #666;
+  background-color: rgba(30, 58, 138, 0.1);
+  padding: 0 0.9375rem;
+  border-radius: 0.375rem;
+  font-weight: bold;
+}
+.table-container[data-v-1cf27b2a] {
+  flex: 1;
+  margin-bottom: 0.9375rem;
+  border-radius: 0.75rem;
+  overflow: hidden;
+  box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
+  background-color: #ffffff;
+  border: 0.0625rem solid #e0e0e0;
+  display: flex;
+  flex-direction: column;
+}
+.table-header[data-v-1cf27b2a] {
+  background-color: #f0f4f8;
+  color: white;
+  font-weight: bold;
+}
+.header-row[data-v-1cf27b2a] {
+  background-color: #1e3a8a;
+  color: white;
+  font-weight: bold;
+}
+.table-row[data-v-1cf27b2a] {
+  display: flex;
+  border-bottom: 0.0625rem solid #dedede;
+}
+.body-row[data-v-1cf27b2a] {
+  padding: 0.3125rem 0;
+}
+.table-row[data-v-1cf27b2a]:last-child {
+  border-bottom: none;
+}
+.cell[data-v-1cf27b2a] {
+  padding: 0.75rem 0.5rem;
+  font-size: 1.5625rem;
+  text-align: center;
+  color: #333;
+}
+.name[data-v-1cf27b2a] {
+  width: 25%;
+}
+.time[data-v-1cf27b2a] {
+  width: 25%;
+}
+.status[data-v-1cf27b2a] {
+  width: 25%;
+}
+.status-tag[data-v-1cf27b2a] {
+  padding: 0.25rem 0.625rem;
+  border-radius: 1rem;
+  font-size: 1.5rem;
+  font-weight: 500;
+}
+.status-observing[data-v-1cf27b2a] {
+  background-color: #e3f2fd;
+  color: #007bff;
+  border: 0.0625rem solid #9acffa;
+}
+.status-completed[data-v-1cf27b2a] {
+  background-color: #e8f5e8;
+  color: #28a745;
+  border: 0.0625rem solid #8bc34a;
+}
+.status-warning[data-v-1cf27b2a] {
+  background-color: #ffebee;
+  color: #c62828;
+  border: 0.0625rem solid #ef9a9a;
+}
+.item-observing[data-v-1cf27b2a] {
+  background-color: #e3f2fd;
+}
+.item-completed[data-v-1cf27b2a] {
+  background-color: #e8f5e8;
+}
+.item-warning[data-v-1cf27b2a] {
+  background-color: #ffebee;
+}
+.item-hasleft[data-v-1cf27b2a] {
+  background-color: #e3f2fd;
+}
+.table-header[data-v-1cf27b2a] {
+  flex: 0 0 auto;
+  overflow: hidden;
+}
+
+/* 保持表格形式的轮播样式 */
+.table-body-container[data-v-1cf27b2a] {
+  flex: 1;
+  overflow: hidden;
+  background-color: #fafafa;
+  position: relative;
+}
+.table-body-wrapper[data-v-1cf27b2a] {
+  display: block;
+}
+.table-body-wrapper.animate[data-v-1cf27b2a] {
+  animation: scrollUp-1cf27b2a 20s linear infinite;
+}
+@keyframes scrollUp-1cf27b2a {
+0% {
+    transform: translateY(0);
+}
+100% {
+    transform: translateY(calc(-50% - 1px));
+    /* 减去1px确保无缝衔接 */
+}
+}
+.empty[data-v-1cf27b2a] {
+  text-align: center;
+  padding: 1.875rem;
+  color: #999;
+  font-style: italic;
+  font-size: 1.25rem;
+}
+.status-bar-bottom[data-v-1cf27b2a] {
+  padding: 0.75rem 0.9375rem;
+  background-color: #ffffff;
+  box-shadow: 0 -0.125rem 0.625rem rgba(0, 0, 0, 0.05);
+  border-radius: 0.75rem;
+  display: flex;
+  gap: 0.5rem;
+}
+.status-text-box[data-v-1cf27b2a] {
+  display: flex;
+  align-items: center;
+  font-size: 0.875rem;
+}
+.dot[data-v-1cf27b2a] {
+  color: #d32f2f;
+  font-size: 1.25rem;
+}
+.dot-connecting[data-v-1cf27b2a] {
+  color: #f9ae3d;
+}
+.dot-connected[data-v-1cf27b2a] {
+  color: #28a745;
+}
+.dot-disconnected[data-v-1cf27b2a] {
+  color: #d32f2f;
+}
+.status-text[data-v-1cf27b2a] {
+  color: #555;
+}
+.status-text[data-v-1cf27b2a] {
+  margin-left: 0.5rem;
+}
+.title_color[data-v-1cf27b2a] {
+  color: #fff;
+}
+.ip-input-group[data-v-1cf27b2a] {
+  display: flex;
+  align-items: center;
+  margin-left: 0.5rem;
+  flex-wrap: wrap;
+  gap: 0.375rem;
+}
+.ip-label[data-v-1cf27b2a] {
+  font-size: 0.875rem;
+  color: #555;
+}
+.ip-input[data-v-1cf27b2a],
+.port-input[data-v-1cf27b2a] {
+  border: 0.0625rem solid #ddd;
+  border-radius: 0.375rem;
+  padding: 0.375rem 0.625rem;
+  font-size: 0.875rem;
+  width: 6.25rem;
+}
+.port-input[data-v-1cf27b2a] {
+  width: 5rem;
+}
+.colon[data-v-1cf27b2a] {
+  font-size: 1rem;
+  color: #666;
+  margin: 0 0.25rem;
+}
+.btn-reconnect[data-v-1cf27b2a] {
+  background-color: #007aff;
+  color: white;
+  font-size: 0.75rem;
+  padding: 0 0.5rem;
+  border-radius: 0.375rem;
+}

+ 0 - 0
unpackage/dist/dev/app-plus/pages/index/mine.css


BIN
unpackage/dist/dev/app-plus/static/horn.png


BIN
unpackage/dist/dev/app-plus/static/logo.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/app-plus/static/logo.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/app-plus/uni-app-view.umd.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/cache/.app-plus/tsc/app-android/.tsbuildInfo


BIN
unpackage/release/apk/__UNI__E207749__20250815162836.apk


BIN
unpackage/release/apk/__UNI__E207749__20250918154748.apk


Някои файлове не бяха показани, защото твърде много файлове са промени