| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656 |
- <template>
- <view class="notice-board">
- <view class="board-header">
- <view class="card_logo" @click="linkSet">
- <!-- <view class="card_logo"> -->
- <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" style="padding: 0rpx;">
- <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" style="padding: 0rpx;">
- <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,
- TTSSpeech: null,
- speechQueue: [], // 语音队列
- isSpeaking: false, // 是否正在播放语音
- }
- },
- 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: '留观完成,可离开',
- }
- }
- })
- }
- },
- onLoad() {
- //#ifdef APP
- // 引入插件
- this.TTSSpeech = uni.requireNativePlugin('MT-TTS-Speech');
- // 初始化TTS引擎
- this.TTSSpeech.init((status) => { // 👈 同样移除 : number
- if (status === 0) {
- console.log('TTS引擎初始化成功');
- } else {
- console.error('TTS引擎初始化失败,状态码:', status)
- }
- }, 'com.iflytek.speechcloud')
- this.TTSSpeech.setPitch(50)
- this.TTSSpeech.setSpeed(50)
- //#endif
- },
- 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: {
- // 将文本添加到队列
- addToSpeechQueue(textValue) {
- if (textValue && typeof textValue === 'string') {
- this.speechQueue.push(textValue);
- // 如果没有正在播放,则开始播放
- if (!this.isSpeaking) {
- this.processSpeechQueue();
- }
- } else {
- console.warn('Invalid text value for speech:', textValue);
- }
- },
- // 处理语音队列
- async processSpeechQueue() {
- if (this.speechQueue.length === 0) {
- this.isSpeaking = false;
- return;
- }
- this.isSpeaking = true;
- const textToSpeak = this.speechQueue.shift(); // 取出队列第一个元素
- try {
- // 等待当前语音播放完成 (通过 onDone 回调)
- await this.speak(textToSpeak);
- } catch (error) {
- console.error('TTS speak error for text:', textToSpeak, error);
- // 即使当前项播放失败,也继续处理队列中的下一个
- } finally {
- this.processSpeechQueue();
- }
- },
- // 执行实际的语音播放 (返回 Promise)
- speak(textValue) {
- return new Promise((resolve, reject) => {
- if (!this.TTSSpeech) {
- reject(new Error('TTS engine not initialized.'));
- return;
- }
- // 监听 TTS 播放结束事件
- this.TTSSpeech.onDone(() => {
- resolve(); // 播放完成,Promise 成功
- });
- // 尝试启动语音播放
- const speakOptions = {
- text: textValue,
- };
- this.TTSSpeech.speak(speakOptions);
- });
- },
- 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);
- 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;
- case 'msg':
- // 使用队列播放语音
- this.addToSpeechQueue(data.msg)
- 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; */
- font-size: 66rpx;
- font-weight: bold;
- text-align: center;
- color: #333;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .cell {
- padding: 24rpx 16rpx;
- /* font-size: 50rpx; */
- font-size: 60rpx;
- 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: 6rpx solid #007bff;
- }
- .status-completed {
- background-color: #e8f5e8;
- color: #28a745;
- border: 6rpx solid #28a745;
- }
- .status-warning {
- background-color: #ffebee;
- color: #ff0000;
- border: 6rpx solid #ff0000;
- }
- .status-hasleft {
- background-color: #fff2e8;
- color: #fa8c16;
- border: 6rpx solid #fa8c16;
- }
- .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;
- border-radius: 50rpx;
- font-size: 60rpx;
- font-weight: bold;
- width: 80%;
- }
- .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>
|