Merge branch 'master' into feat/AIChat
This commit is contained in:
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,5 +1,32 @@
|
||||
|
||||
|
||||
## [2.6.12](https://github.com/HuLaSpark/HuLa/compare/v2.6.11...v2.6.12) (2025-05-25)
|
||||
|
||||
|
||||
### ✨ Features | 新功能
|
||||
|
||||
* **component:** :sparkles: 增加右键emoji可以另存为 ([0dfdfc1](https://github.com/HuLaSpark/HuLa/commit/0dfdfc1269256baa59316b7936db94468b733b69))
|
||||
* **hook:** :sparkles: 增强文本选择复制 ([791b9cc](https://github.com/HuLaSpark/HuLa/commit/791b9cc69485d9c8245396d72cca834ac9a61aa0)), closes [#279](https://github.com/HuLaSpark/HuLa/issues/279)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes | Bug 修复
|
||||
|
||||
* 修复ws链接断开没有重试问题 ([#276](https://github.com/HuLaSpark/HuLa/issues/276)) ([e068a25](https://github.com/HuLaSpark/HuLa/commit/e068a253e5215aacb606f852087e5e9e67010404))
|
||||
* **agreement:** :bug: 安装界面乱码 ([#277](https://github.com/HuLaSpark/HuLa/issues/277)) ([13c528a](https://github.com/HuLaSpark/HuLa/commit/13c528a35e11ee5fa7325beb1fcef8d28f2550e3)), closes [#275](https://github.com/HuLaSpark/HuLa/issues/275)
|
||||
* **hook:** :bug: 修复除emoji类型和图片类型都可以另存为的bug ([d2b6ab2](https://github.com/HuLaSpark/HuLa/commit/d2b6ab25d6fd7ea5a4e9df1d287fe3d3cc9a1b58))
|
||||
* **hook:** :bug: 修复mac系统右键会选中文本的问题 ([7e762e8](https://github.com/HuLaSpark/HuLa/commit/7e762e8524df0d17f85ca71eedb95d01dea7c8d3))
|
||||
* **hook:** :bug: 暂时移除/唤起ai快捷键识别 ([4a1a05c](https://github.com/HuLaSpark/HuLa/commit/4a1a05cf51b4bab670b2da5faac43bf33f902998))
|
||||
* **input:** :bug: 修复mac下输入框检查拼写和字母大小写问题 ([f8602e5](https://github.com/HuLaSpark/HuLa/commit/f8602e56ebbf4ae90f5f5dc7e7cebee317bf4ab4))
|
||||
* **mac:** :bug: 修复mac下点击关闭按钮无法关闭home窗口问题 ([2a63046](https://github.com/HuLaSpark/HuLa/commit/2a63046bbd2d7c4cd484d456c708ab47bdc8e792))
|
||||
* **view:** :bug: 修复托盘菜单内容不展示问题和托盘图标闪烁后不展示问题 ([c927be4](https://github.com/HuLaSpark/HuLa/commit/c927be4c3fd00cdde9f93c15793ea56ce5b11d14))
|
||||
* **view:** :bug: 修改邮箱输入框长度限制 ([61618db](https://github.com/HuLaSpark/HuLa/commit/61618db93cbe9512eceb66fbc50006a90f7d44f1)), closes [#278](https://github.com/HuLaSpark/HuLa/issues/278)
|
||||
* **worker:** :bug: 修复ws在重连后清空token导致无法对应获取消息问题 ([030fed7](https://github.com/HuLaSpark/HuLa/commit/030fed7d60a6eb03dccb49e6f108b2b5d67161e4))
|
||||
|
||||
|
||||
### ⚡️ Performance Improvements | 性能优化
|
||||
|
||||
* **global:** :zap: 增加ws健康检查兜底刷新最新消息内容、网络断线恢复重连 ([f734dca](https://github.com/HuLaSpark/HuLa/commit/f734dca910b17e3dd8a4d8e5e58cae3e7caaa333))
|
||||
|
||||
## [2.6.11](https://github.com/HuLaSpark/HuLa/compare/v2.6.10...v2.6.11) (2025-05-18)
|
||||
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ Thanks to the following sponsors for their support!
|
||||
| Date | Sponsor | Sum | Platform |
|
||||
|------|--------|------|------|
|
||||
| 2025-04-25 | 上官俊斌 | ¥200 | 微信赞赏码 |
|
||||
| 2025-05-27 | 临安居士 | ¥188 | 微信赞赏码 |
|
||||
| 2025-04-20 | 姜兴(Simon) | ¥188 | 微信赞赏码 |
|
||||
| 2025-02-17 | 禾硕 | ¥168 | 支付宝赞赏 |
|
||||
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
|
||||
@@ -225,6 +226,8 @@ Thanks to the following sponsors for their support!
|
||||
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
|
||||
| 2025-05-15 | 孤鸿影 | ¥56 | 微信红包 |
|
||||
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
|
||||
| 2025-05-27 | 刘启成 | ¥20 | 微信赞赏码 |
|
||||
| 2025-05-20 | 没有留名的赞助者 | ¥20 | 微信赞赏码 |
|
||||
|
||||
> Note: This list is manually updated. If you have sponsored but are not displayed in the list, please contact us by:
|
||||
1. Submit Issue on GitHub
|
||||
|
||||
@@ -216,6 +216,7 @@ sudo xattr -r -d com.apple.quarantine /Applications/应用名称.app
|
||||
| 日期 | 赞助者 | 金额 | 平台 |
|
||||
|------|--------|------|------|
|
||||
| 2025-04-25 | 上官俊斌 | ¥200 | 微信赞赏码 |
|
||||
| 2025-05-27 | 临安居士 | ¥188 | 微信赞赏码 |
|
||||
| 2025-04-20 | 姜兴(Simon) | ¥188 | 微信赞赏码 |
|
||||
| 2025-02-17 | 禾硕 | ¥168 | 支付宝赞赏 |
|
||||
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
|
||||
@@ -225,6 +226,8 @@ sudo xattr -r -d com.apple.quarantine /Applications/应用名称.app
|
||||
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
|
||||
| 2025-05-15 | 孤鸿影 | ¥56 | 微信红包 |
|
||||
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
|
||||
| 2025-05-27 | 刘启成 | ¥20 | 微信赞赏码 |
|
||||
| 2025-05-20 | 没有留名的赞助者 | ¥20 | 微信赞赏码 |
|
||||
|
||||
> 注:该名单为手动更新。如果您已赞助但未显示在列表中,请通过以下方式联系我们:
|
||||
1. 在GitHub上提交Issue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hula",
|
||||
"type": "module",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.x",
|
||||
@@ -114,7 +114,7 @@
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"@vitest/ui": "^3.0.5",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-git": "^1.11.0",
|
||||
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -178,8 +178,8 @@ importers:
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@vueuse/core':
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0(vue@3.5.13(typescript@5.7.2))
|
||||
specifier: ^13.2.0
|
||||
version: 13.2.0(vue@3.5.13(typescript@5.7.2))
|
||||
chalk:
|
||||
specifier: ^5.3.0
|
||||
version: 5.4.1
|
||||
@@ -230,7 +230,7 @@ importers:
|
||||
version: 5.7.2
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2)))
|
||||
version: 19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2)))
|
||||
unplugin-vue-components:
|
||||
specifier: ^28.4.1
|
||||
version: 28.4.1(@babel/parser@7.26.9)(@nuxt/kit@3.15.4(magicast@0.3.5))(vue@3.5.13(typescript@5.7.2))
|
||||
@@ -1850,16 +1850,16 @@ packages:
|
||||
'@vue/test-utils@2.4.6':
|
||||
resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
|
||||
|
||||
'@vueuse/core@13.1.0':
|
||||
resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==}
|
||||
'@vueuse/core@13.2.0':
|
||||
resolution: {integrity: sha512-n5TZoIAxbWAQ3PqdVPDzLgIRQOujFfMlatdI+f7ditSmoEeNpPBvp7h2zamzikCmrhFIePAwdEQB6ENccHr7Rg==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/metadata@13.1.0':
|
||||
resolution: {integrity: sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==}
|
||||
'@vueuse/metadata@13.2.0':
|
||||
resolution: {integrity: sha512-kPpzuQCU0+D8DZCzK0iPpIcXI+6ufWSgwnjJ6//GNpEn+SHViaCtR+XurzORChSgvpHO9YC8gGM97Y1kB+UabA==}
|
||||
|
||||
'@vueuse/shared@13.1.0':
|
||||
resolution: {integrity: sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==}
|
||||
'@vueuse/shared@13.2.0':
|
||||
resolution: {integrity: sha512-vx9ZPDF5HcU9up3Jgt3G62dMUfZEdk6tLyBAHYAG4F4n73vpaA7J5hdncDI/lS9Vm7GA/FPlbOmh9TrDZROTpg==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
@@ -2945,8 +2945,8 @@ packages:
|
||||
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-tsconfig@4.10.0:
|
||||
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-uri@6.0.4:
|
||||
resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==}
|
||||
@@ -6710,16 +6710,16 @@ snapshots:
|
||||
js-beautify: 1.15.1
|
||||
vue-component-type-helpers: 2.2.0
|
||||
|
||||
'@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2))':
|
||||
'@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 13.1.0
|
||||
'@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.7.2))
|
||||
'@vueuse/metadata': 13.2.0
|
||||
'@vueuse/shared': 13.2.0(vue@3.5.13(typescript@5.7.2))
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
|
||||
'@vueuse/metadata@13.1.0': {}
|
||||
'@vueuse/metadata@13.2.0': {}
|
||||
|
||||
'@vueuse/shared@13.1.0(vue@3.5.13(typescript@5.7.2))':
|
||||
'@vueuse/shared@13.2.0(vue@3.5.13(typescript@5.7.2))':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
|
||||
@@ -7997,7 +7997,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.2.7
|
||||
|
||||
get-tsconfig@4.10.0:
|
||||
get-tsconfig@4.10.1:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
optional: true
|
||||
@@ -9825,7 +9825,7 @@ snapshots:
|
||||
tsx@4.19.2:
|
||||
dependencies:
|
||||
esbuild: 0.23.1
|
||||
get-tsconfig: 4.10.0
|
||||
get-tsconfig: 4.10.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
optional: true
|
||||
@@ -9961,7 +9961,7 @@ snapshots:
|
||||
|
||||
unload@2.4.1: {}
|
||||
|
||||
unplugin-auto-import@19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2))):
|
||||
unplugin-auto-import@19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2))):
|
||||
dependencies:
|
||||
local-pkg: 1.0.0
|
||||
magic-string: 0.30.17
|
||||
@@ -9971,7 +9971,7 @@ snapshots:
|
||||
unplugin-utils: 0.2.4
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.15.4(magicast@0.3.5)
|
||||
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.2))
|
||||
'@vueuse/core': 13.2.0(vue@3.5.13(typescript@5.7.2))
|
||||
|
||||
unplugin-utils@0.2.4:
|
||||
dependencies:
|
||||
|
||||
BIN
preview/wx.png
BIN
preview/wx.png
Binary file not shown.
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 212 KiB |
68
src-tauri/Cargo.lock
generated
68
src-tauri/Cargo.lock
generated
@@ -2220,11 +2220,13 @@ dependencies = [
|
||||
"cocoa",
|
||||
"lazy_static",
|
||||
"objc",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.11.27",
|
||||
"rodio",
|
||||
"screenshots",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"surge-ping",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
@@ -2241,6 +2243,7 @@ dependencies = [
|
||||
"tauri-plugin-sql",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-upload",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
@@ -3091,6 +3094,12 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-net"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -3877,6 +3886,48 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_base"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c"
|
||||
dependencies = [
|
||||
"no-std-net",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_macros"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_macros_support"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56"
|
||||
dependencies = [
|
||||
"pnet_base",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_packet"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"pnet_base",
|
||||
"pnet_macros",
|
||||
"pnet_macros_support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
@@ -5309,6 +5360,22 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "surge-ping"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fda78103d8016bb25c331ddc54af634e801806463682cc3e549d335df644d95"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"pnet_packet",
|
||||
"rand 0.9.1",
|
||||
"socket2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -6198,6 +6265,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
|
||||
@@ -37,7 +37,10 @@ tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-http = { version = "2.4.3", features = ["unsafe-headers", "rustls-tls"] }
|
||||
tauri-plugin-http = { version = "2.4.3", features = [
|
||||
"unsafe-headers",
|
||||
"rustls-tls",
|
||||
] }
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
@@ -48,7 +51,8 @@ tauri-plugin-updater = "2.7.1"
|
||||
tauri-plugin-sql = { version = "2.2.0", features = ["sqlite"] }
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-notification = "2.2.2"
|
||||
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
|
||||
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-autostart = "2.3.0"
|
||||
@@ -60,7 +64,10 @@ reqwest = { version = "0.11", features = [
|
||||
"json",
|
||||
"socks",
|
||||
"rustls-tls",
|
||||
"blocking",
|
||||
] }
|
||||
surge-ping = "0.8.0"
|
||||
rand = "0.8.5"
|
||||
[target."cfg(target_os =\"macos\")".dependencies]
|
||||
cocoa = "0.26.0"
|
||||
objc = "0.2.7"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::{env, fs, io};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ensure_frontend_dist()?;
|
||||
tauri_build::build();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_frontend_dist() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn ensure_frontend_dist() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let current_dir = env::current_dir()?;
|
||||
let parent_dir = current_dir
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Cannot find parent directory!"))?;
|
||||
let frontend_dist = parent_dir.join("dist");
|
||||
|
||||
|
||||
// There should not be this directory.
|
||||
let exists = frontend_dist.exists() && frontend_dist.is_dir();
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// 桌面端依赖
|
||||
#[cfg(desktop)]
|
||||
mod desktops;
|
||||
#[cfg(desktop)]
|
||||
use common_cmd::{
|
||||
audio, default_window_icon, screenshot, set_badge_count, set_height,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use common_cmd::{hide_title_bar_buttons};
|
||||
use common_cmd::hide_title_bar_buttons;
|
||||
#[cfg(desktop)]
|
||||
use common_cmd::{audio, default_window_icon, screenshot, set_badge_count, set_height};
|
||||
#[cfg(desktop)]
|
||||
mod proxy;
|
||||
#[cfg(desktop)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula-app.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula.pc",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula-app.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula.pc",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula.pc",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HuLa",
|
||||
"version": "2.6.11",
|
||||
"version": "2.6.12",
|
||||
"identifier": "com.hula.pc",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -125,6 +125,10 @@
|
||||
class="border-(1px solid #90909080)"
|
||||
placeholder="请输入群聊备注"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
@blur="handleRemarkUpdate"
|
||||
@keydown.enter="handleRemarkUpdate" />
|
||||
</div>
|
||||
@@ -147,6 +151,10 @@
|
||||
class="border-(1px solid #90909080)"
|
||||
placeholder="请输入本群昵称"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
@blur="handleNicknameUpdate"
|
||||
@keydown.enter="handleNicknameUpdate" />
|
||||
</div>
|
||||
|
||||
@@ -165,6 +165,10 @@
|
||||
@keydown.enter="saveGroupName"
|
||||
size="tiny"
|
||||
maxlength="12"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
class="border-(solid 1px [--line-color])"
|
||||
placeholder="请输入群名称(最多12字)" />
|
||||
</div>
|
||||
@@ -235,6 +239,10 @@
|
||||
<p class="text-(12px [--chat-text-color]) mt-20px mb-10px">我本群的昵称</p>
|
||||
<n-input
|
||||
class="border-(solid 1px [--line-color]) custom-shadow"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
v-model:value="groupDetail.myNickname"
|
||||
@update:value="updateGroupInfo($event, 'nickname')" />
|
||||
<!-- 群备注 -->
|
||||
@@ -245,6 +253,10 @@
|
||||
<n-input
|
||||
class="border-(solid 1px [--line-color]) custom-shadow"
|
||||
v-model:value="groupDetail.groupRemark"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
@update:value="updateGroupInfo($event, 'remark')" />
|
||||
|
||||
<!-- 群设置选项 -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- 网络断开提示 -->
|
||||
<!-- 网络状态提示 -->
|
||||
<n-flex
|
||||
v-if="!isOnline"
|
||||
v-if="!networkStatus.isOnline.value"
|
||||
align="center"
|
||||
justify="center"
|
||||
class="z-999 absolute w-full h-40px rounded-4px text-(12px [--danger-text]) bg-[--danger-bg]">
|
||||
@@ -448,7 +448,6 @@ import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { useNetwork } from '@vueuse/core'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import VirtualList, { type VirtualListExpose } from '@/components/common/VirtualList.vue'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
@@ -458,6 +457,7 @@ import { useGlobalStore } from '@/stores/global'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import apis from '@/services/apis'
|
||||
import { useNetworkStatus } from '@/hooks/useNetworkStatus'
|
||||
|
||||
const appWindow = WebviewWindow.getCurrent()
|
||||
const { addListener } = useTauriListener()
|
||||
@@ -470,6 +470,7 @@ const userStore = useUserStore()
|
||||
const groupStore = useGroupStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const cachedStore = useCachedStore()
|
||||
const networkStatus = useNetworkStatus()
|
||||
|
||||
// 记录当前滚动位置相关信息
|
||||
const isAutoScrolling = ref(false)
|
||||
@@ -520,8 +521,6 @@ const hoverBubble = ref<{
|
||||
})
|
||||
/** 记录右键菜单时选中的气泡的元素(用于处理mac右键会选中文本的问题) */
|
||||
const recordEL = ref()
|
||||
/** 网络连接是否正常 */
|
||||
const { isOnline } = useNetwork()
|
||||
const isMac = computed(() => type() === 'macos')
|
||||
// 公告展示时需要减去的高度
|
||||
const announcementHeight = computed(() => (isGroup.value && topAnnouncement.value ? 300 : 260))
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
placeholder="搜索"
|
||||
type="text"
|
||||
size="tiny"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
class="h-26px w-95% lh-26px rounded-6px">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px">
|
||||
|
||||
@@ -14,6 +14,24 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
|
||||
// 禁止滚动的默认行为
|
||||
const preventDefault = (e: Event) => e.preventDefault()
|
||||
|
||||
// 禁止选中文本的默认行为
|
||||
const preventTextSelection = (e: Event) => e.preventDefault()
|
||||
|
||||
// 禁用文本选择
|
||||
const disableTextSelection = () => {
|
||||
// 清除当前选择
|
||||
window.getSelection()?.removeAllRanges()
|
||||
// 添加禁止选择事件
|
||||
document.body.classList.add('no-select')
|
||||
window.addEventListener('selectstart', preventTextSelection)
|
||||
}
|
||||
|
||||
// 启用文本选择
|
||||
const enableTextSelection = () => {
|
||||
document.body.classList.remove('no-select')
|
||||
window.removeEventListener('selectstart', preventTextSelection)
|
||||
}
|
||||
|
||||
/**! 解决使用n-virtual-list时,右键菜单出现还可以滚动的问题 */
|
||||
const handleVirtualListScroll = (isBan: boolean) => {
|
||||
const scrollbar_main = document.querySelector('#image-chat-main') as HTMLElement
|
||||
@@ -27,6 +45,10 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (isNull?.value) return
|
||||
|
||||
// 在显示菜单前清除选择
|
||||
disableTextSelection()
|
||||
|
||||
handleVirtualListScroll(true)
|
||||
showMenu.value = true
|
||||
x.value = e.clientX
|
||||
@@ -39,11 +61,36 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
|
||||
if (!event.target.matches('.context-menu, .context-menu *')) {
|
||||
handleVirtualListScroll(false)
|
||||
showMenu.value = false
|
||||
enableTextSelection() // 恢复文本选择功能
|
||||
}
|
||||
window.removeEventListener('wheel', preventDefault) // 移除禁止滚轮滚动
|
||||
}
|
||||
|
||||
// 监听showMenu状态变化
|
||||
watch(
|
||||
() => showMenu.value,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
// 当菜单关闭时,恢复文本选择功能
|
||||
enableTextSelection()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 添加全局样式
|
||||
if (!document.querySelector('#no-select-style')) {
|
||||
const style = document.createElement('style')
|
||||
style.id = 'no-select-style'
|
||||
style.textContent = `.no-select {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-ms-user-select: none !important;
|
||||
}`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
const div = ContextMenuRef.value
|
||||
//这里只监听了div的右键,如果需要监听其他元素的右键,需要在其他元素上监听
|
||||
div.addEventListener('contextmenu', handleContextMenu)
|
||||
@@ -65,8 +112,16 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
|
||||
div?.removeEventListener('contextmenu', handleContextMenu)
|
||||
window.removeEventListener('contextmenu', preventDefault)
|
||||
window.removeEventListener('wheel', preventDefault)
|
||||
window.removeEventListener('selectstart', preventTextSelection)
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
window.removeEventListener('contextmenu', closeMenu, true)
|
||||
|
||||
// 确保恢复选择功能
|
||||
enableTextSelection()
|
||||
|
||||
// 移除样式
|
||||
const style = document.querySelector('#no-select-style')
|
||||
if (style) style.remove()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
173
src/hooks/useNetworkReconnect.ts
Normal file
173
src/hooks/useNetworkReconnect.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useNetwork, useEventListener, useTimeoutFn } from '@vueuse/core'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import webSocket from '@/services/webSocket'
|
||||
|
||||
/**
|
||||
* 网络重连Hook,监测网络恢复并自动刷新数据
|
||||
* 在网络从离线变为在线时触发数据刷新
|
||||
* 处理系统睡眠唤醒和长时间空闲后的连接恢复
|
||||
* 刷新当前聊天室消息、会话列表、群组信息等
|
||||
*/
|
||||
export const useNetworkReconnect = () => {
|
||||
// 获取所需的stores
|
||||
const chatStore = useChatStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupStore = useGroupStore()
|
||||
const cachedStore = useCachedStore()
|
||||
const contactStore = useContactStore()
|
||||
|
||||
// 使用VueUse的useNetwork获取网络状态
|
||||
const { isOnline, offlineAt, onlineAt } = useNetwork()
|
||||
|
||||
// 上次离线的时间戳
|
||||
let lastOfflineTimestamp = 0
|
||||
|
||||
// 上次活动时间戳(用于检测长时间空闲)
|
||||
let lastActivityTimestamp = Date.now()
|
||||
|
||||
// 判断是否是移动设备
|
||||
const isMobile = computed(() => type() === 'android' || type() === 'ios')
|
||||
|
||||
// 最长空闲时间,超过这个时间会检查连接(15分钟)
|
||||
const MAX_IDLE_TIME = 15 * 60 * 1000
|
||||
|
||||
// 定期健康检查定时器
|
||||
const healthCheckTimer = useTimeoutFn(
|
||||
() => {
|
||||
checkConnectionHealth()
|
||||
// 执行完一次后,再次启动定时器,实现循环检查
|
||||
healthCheckTimer.start()
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
{ immediate: true, immediateCallback: false }
|
||||
)
|
||||
|
||||
// 监听网络状态变化
|
||||
watch([isOnline, offlineAt], ([newIsOnline, newOfflineAt], [oldIsOnline]) => {
|
||||
// 网络从离线变为在线时
|
||||
if (!oldIsOnline && newIsOnline) {
|
||||
console.log('[Network] 网络已重新连接,开始刷新数据')
|
||||
|
||||
// 如果知道离线时间,记录下来
|
||||
if (newOfflineAt) {
|
||||
lastOfflineTimestamp = newOfflineAt
|
||||
}
|
||||
|
||||
// 执行数据刷新
|
||||
refreshAllData()
|
||||
}
|
||||
|
||||
// 网络变为离线时记录时间
|
||||
if (oldIsOnline && !newIsOnline && newOfflineAt) {
|
||||
lastOfflineTimestamp = newOfflineAt
|
||||
console.log('[Network] 网络已断开连接')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 在挂起/恢复时可能会改变网络状态
|
||||
* 对所有平台处理可见性变化和潜在的连接问题
|
||||
*/
|
||||
useEventListener(window, 'visibilitychange', () => {
|
||||
const currentTime = Date.now()
|
||||
const idleTime = currentTime - lastActivityTimestamp
|
||||
|
||||
// 页面变为可见时
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log(`[Network] 应用从后台恢复,已离线 ${idleTime / 1000} 秒`)
|
||||
lastActivityTimestamp = currentTime
|
||||
|
||||
// 在移动设备上的恢复逻辑
|
||||
if (isMobile.value && isOnline.value) {
|
||||
// 如果离线超过30秒,则刷新数据
|
||||
if (lastOfflineTimestamp > 0 && currentTime - lastOfflineTimestamp > 30000) {
|
||||
console.log('[Network] 移动端应用从后台恢复,刷新数据')
|
||||
refreshAllData()
|
||||
lastOfflineTimestamp = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 在所有设备上,如果空闲时间过长,执行连接健康检查
|
||||
if (idleTime > 15 * 60 * 1000) {
|
||||
// 如果超过15分钟没活动
|
||||
checkConnectionHealth(true)
|
||||
}
|
||||
} else {
|
||||
// 页面变为不可见时,记录时间戳
|
||||
lastActivityTimestamp = currentTime
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 睡眠/唤醒事件
|
||||
* TODO 实现
|
||||
*/
|
||||
// const setupSystemEventListeners = async () => {
|
||||
// }
|
||||
|
||||
/**
|
||||
* 检查WebSocket连接健康状态
|
||||
* @param forceReconnect 是否在不健康时强制重连
|
||||
*/
|
||||
const checkConnectionHealth = (forceReconnect = false) => {
|
||||
// 调用WebSocket的健康检查
|
||||
const health = webSocket.checkConnectionHealth()
|
||||
|
||||
if (!health) return
|
||||
|
||||
console.log('[Network] 连接健康检查:', health)
|
||||
|
||||
const currentTime = Date.now()
|
||||
const idleTime = currentTime - lastActivityTimestamp
|
||||
|
||||
// 1. 明确要求强制重连
|
||||
// 2. 连接不健康且空闲时间超过MAX_IDLE_TIME
|
||||
if (forceReconnect || (!health.isHealthy && idleTime > MAX_IDLE_TIME)) {
|
||||
console.log('[Network] 检测到不健康或空闲时间过长的连接,正在重新连接...')
|
||||
webSocket.forceReconnect()
|
||||
|
||||
// 刷新数据
|
||||
refreshAllData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有重要数据
|
||||
*/
|
||||
const refreshAllData = async () => {
|
||||
// 刷新会话列表
|
||||
await chatStore.getSessionList(true)
|
||||
// 如果当前有选中的聊天室,重置并刷新消息列表
|
||||
if (globalStore.currentSession?.roomId) {
|
||||
// 获取最新消息
|
||||
await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
}
|
||||
// 如果当前是群聊,刷新群组信息
|
||||
if (globalStore.currentSession?.type === 2) {
|
||||
await groupStore.getGroupUserList(true)
|
||||
await groupStore.getCountStatistic()
|
||||
await cachedStore.getGroupAtUserBaseInfo()
|
||||
}
|
||||
// 刷新联系人列表
|
||||
await contactStore.getContactList(true)
|
||||
// 更新未读消息计数
|
||||
globalStore.updateGlobalUnreadCount()
|
||||
// 刷新在线用户列表
|
||||
await groupStore.refreshGroupMembers()
|
||||
|
||||
console.log('[Network] 数据刷新完成')
|
||||
}
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
offlineAt,
|
||||
onlineAt,
|
||||
refreshAllData,
|
||||
checkConnectionHealth
|
||||
}
|
||||
}
|
||||
42
src/hooks/useNetworkStatus.ts
Normal file
42
src/hooks/useNetworkStatus.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 网络状态监测钩子
|
||||
*/
|
||||
export const useNetworkStatus = () => {
|
||||
// 网络状态 - 基于浏览器navigator.onLine
|
||||
const isOnline = ref(navigator.onLine)
|
||||
|
||||
// 监听浏览器网络状态变化
|
||||
const handleOnline = () => {
|
||||
isOnline.value = true
|
||||
}
|
||||
|
||||
const handleOffline = () => {
|
||||
isOnline.value = false
|
||||
}
|
||||
|
||||
// 初始化网络状态监听
|
||||
const initNetworkListener = () => {
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
}
|
||||
|
||||
// 清理网络状态监听
|
||||
const cleanupNetworkListener = () => {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
}
|
||||
|
||||
// 自动初始化监听器
|
||||
initNetworkListener()
|
||||
|
||||
// 在组件卸载时清理监听器
|
||||
onUnmounted(() => {
|
||||
cleanupNetworkListener()
|
||||
})
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
initNetworkListener,
|
||||
cleanupNetworkListener
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
style="background: var(--search-bg-color)"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
size="small"
|
||||
:placeholder="isSearchMode ? '' : '搜索'">
|
||||
<template #prefix>
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
:passively-activated="true"
|
||||
class="rounded-6px"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
placeholder="请输入你的昵称"
|
||||
show-count
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
minlength="6"
|
||||
v-model:value="info.account"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="accountPH"
|
||||
@focus="accountPH = ''"
|
||||
@blur="accountPH = '输入HuLa账号'"
|
||||
@@ -56,6 +60,10 @@
|
||||
show-password-on="click"
|
||||
v-model:value="info.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="passwordPH"
|
||||
@focus="passwordPH = ''"
|
||||
@blur="passwordPH = '输入HuLa密码'"
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
class="rounded-6px w-full relative text-12px"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="'搜索'">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</n-image-group>
|
||||
</n-scrollbar>
|
||||
|
||||
<n-input />
|
||||
<n-input spellCheck="false" autoComplete="off" autoCorrect="off" autoCapitalize="off" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
placeholder="输入标题"
|
||||
type="text"
|
||||
size="tiny"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
style="width: 200px"
|
||||
class="h-22px lh-22px rounded-6px">
|
||||
</n-input>
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
placeholder="输入标题"
|
||||
type="text"
|
||||
size="tiny"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
style="width: 200px"
|
||||
class="leading-7 text-14px rounded-6px">
|
||||
</n-input>
|
||||
|
||||
@@ -13,8 +13,10 @@ import { useUserStore } from '@/stores/user'
|
||||
import { getEnhancedFingerprint } from '@/services/fingerprint.ts'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useTauriListener } from '@/hooks/useTauriListener'
|
||||
import { listen, emit } from '@tauri-apps/api/event'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
// 使用类型导入避免直接执行代码
|
||||
import type { useNetworkReconnect as UseNetworkReconnectType } from '@/hooks/useNetworkReconnect'
|
||||
|
||||
// 创建 webSocket worker
|
||||
const worker: Worker = new Worker(new URL('../workers/webSocket.worker.ts', import.meta.url), {
|
||||
@@ -29,14 +31,48 @@ const timerWorker: Worker = new Worker(new URL('../workers/timer.worker.ts', imp
|
||||
// 添加一个标识是否是主窗口的变量
|
||||
let isMainWindow = false
|
||||
|
||||
// LRU缓存实现
|
||||
class LRUCache<K, V> {
|
||||
private maxSize: number
|
||||
private cache = new Map<K, V>()
|
||||
|
||||
constructor(maxSize: number = 1000) {
|
||||
this.maxSize = maxSize
|
||||
}
|
||||
|
||||
set(key: K, value: V) {
|
||||
if (this.cache.has(key)) {
|
||||
this.cache.delete(key)
|
||||
} else if (this.cache.size >= this.maxSize) {
|
||||
const firstKey = this.cache.keys().next().value
|
||||
if (firstKey) {
|
||||
this.cache.delete(firstKey)
|
||||
}
|
||||
}
|
||||
this.cache.set(key, value)
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.cache.has(key)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size
|
||||
}
|
||||
}
|
||||
|
||||
class WS {
|
||||
// 添加消息队列大小限制
|
||||
readonly #MAX_QUEUE_SIZE = 100
|
||||
readonly #MAX_QUEUE_SIZE = 50 // 减少队列大小
|
||||
#tasks: WsReqMsgContentType[] = []
|
||||
// 重连🔐
|
||||
#connectReady = false
|
||||
// 使用LRU缓存替代简单的Set
|
||||
#processedMsgCache = new Map<number, number>()
|
||||
#processedMsgCache = new LRUCache<number, number>(1000) // 使用LRU缓存
|
||||
|
||||
#tauriListener: ReturnType<typeof useTauriListener> | null = null
|
||||
|
||||
@@ -50,6 +86,12 @@ class WS {
|
||||
timeSinceLastPong: null as number | null
|
||||
}
|
||||
|
||||
// 网络重连工具,延迟初始化
|
||||
#networkReconnect: ReturnType<typeof UseNetworkReconnectType> | null = null
|
||||
|
||||
// 存储watch清理函数
|
||||
#unwatchFunctions: (() => void)[] = []
|
||||
|
||||
constructor() {
|
||||
this.initWindowType()
|
||||
if (isMainWindow) {
|
||||
@@ -60,9 +102,40 @@ class WS {
|
||||
timerWorker.addEventListener('message', this.onTimerWorkerMsg)
|
||||
// 添加页面可见性监听
|
||||
this.initVisibilityListener()
|
||||
|
||||
this.initNetworkReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化网络重连工具
|
||||
private initNetworkReconnect() {
|
||||
// 动态导入以延迟执行
|
||||
import('@/hooks/useNetworkReconnect')
|
||||
.then(({ useNetworkReconnect }) => {
|
||||
this.#networkReconnect = useNetworkReconnect()
|
||||
console.log('[WebSocket] 网络重连工具初始化完成')
|
||||
|
||||
// 监听网络在线状态变化
|
||||
if (this.#networkReconnect.isOnline) {
|
||||
const unwatch = watch(this.#networkReconnect.isOnline, (newValue, oldValue) => {
|
||||
// 只在网络从离线变为在线时执行重连
|
||||
if (newValue === true && oldValue === false) {
|
||||
console.log('[WebSocket] 网络恢复在线状态,主动重新初始化WebSocket连接')
|
||||
// 重置重连计数并重新初始化连接
|
||||
this.forceReconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// 存储清理函数
|
||||
this.#unwatchFunctions = this.#unwatchFunctions || []
|
||||
this.#unwatchFunctions.push(unwatch)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[WebSocket] 网络重连工具初始化失败:', err)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化页面可见性监听
|
||||
private async initVisibilityListener() {
|
||||
const handleVisibilityChange = (isVisible: boolean) => {
|
||||
@@ -72,6 +145,18 @@ class WS {
|
||||
value: { isHidden: !isVisible }
|
||||
})
|
||||
)
|
||||
|
||||
// 优化的可见性恢复检查
|
||||
if (isVisible && this.#networkReconnect?.isOnline?.value) {
|
||||
// 检查最后一次通信时间,如果太久没有通信,刷新数据
|
||||
const now = Date.now()
|
||||
const lastPongTime = this.#connectionHealth.lastPongTime
|
||||
const heartbeatTimeout = 90000 // 增加到90秒,减少误触发
|
||||
if (lastPongTime && now - lastPongTime > heartbeatTimeout) {
|
||||
console.log('[Network] 应用从后台恢复且长时间无心跳,刷新数据')
|
||||
this.#networkReconnect?.refreshAllData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedVisibilityChange = useDebounceFn((isVisible: boolean) => {
|
||||
@@ -169,31 +254,6 @@ class WS {
|
||||
private async initWindowType() {
|
||||
const currentWindow = WebviewWindow.getCurrent()
|
||||
isMainWindow = currentWindow.label === 'home'
|
||||
|
||||
if (!isMainWindow) {
|
||||
// 非主窗口监听来自主窗口的消息
|
||||
await this.initChildWindowListeners()
|
||||
}
|
||||
}
|
||||
|
||||
// 为子窗口初始化监听器
|
||||
private async initChildWindowListeners() {
|
||||
this.#tauriListener = useTauriListener()
|
||||
|
||||
// 监听主窗口发来的WebSocket消息
|
||||
this.#tauriListener.addListener(
|
||||
listen('ws-message', (event) => {
|
||||
this.onMessage(event.payload as string)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听连接状态变化
|
||||
this.#tauriListener.addListener(
|
||||
listen('ws-state-change', (event) => {
|
||||
const state = event.payload as ConnectionState
|
||||
useMitt.emit('wsConnectionStateChange', state)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
initConnect = async () => {
|
||||
@@ -223,10 +283,6 @@ class WS {
|
||||
switch (params.type) {
|
||||
case WorkerMsgEnum.MESSAGE: {
|
||||
await this.onMessage(params.value as string)
|
||||
// 广播消息给其他窗口
|
||||
if (isMainWindow) {
|
||||
await emit('ws-message', params.value)
|
||||
}
|
||||
break
|
||||
}
|
||||
case WorkerMsgEnum.OPEN: {
|
||||
@@ -243,7 +299,6 @@ class WS {
|
||||
useMitt.emit(WsResponseMessageType.NO_INTERNET, params.value)
|
||||
// 如果是重连失败,可以提示用户刷新页面
|
||||
if ((params.value as { msg: string }).msg.includes('连接失败次数过多')) {
|
||||
useMitt.emit('showMainMessage', { title: '连接断开', content: '连接已断开,请刷新页面或重新登录。' })
|
||||
// 可以触发UI提示,让用户刷新页面
|
||||
useMitt.emit('wsReconnectFailed', params.value)
|
||||
}
|
||||
@@ -299,21 +354,21 @@ class WS {
|
||||
const { state } = params.value as { state: ConnectionState }
|
||||
|
||||
// 检测重连成功: 从RECONNECTING状态变为CONNECTED状态
|
||||
// TODO 重连的时候没有执行这里
|
||||
if (this.#previousConnectionState === ConnectionState.RECONNECTING && state === ConnectionState.CONNECTED) {
|
||||
console.log('🔄 WebSocket 重连成功')
|
||||
// 可以添加UI提示
|
||||
useMitt.emit('showMainMessage', { title: '连接恢复', content: '网络连接已恢复' })
|
||||
// 网络重连成功后刷新数据
|
||||
if (isMainWindow && this.#networkReconnect) {
|
||||
console.log('开始刷新数据...')
|
||||
this.#networkReconnect.refreshAllData()
|
||||
} else if (isMainWindow) {
|
||||
// 如果还没初始化,延迟初始化后再刷新
|
||||
this.initNetworkReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新前一状态
|
||||
this.#previousConnectionState = state
|
||||
|
||||
console.log('连接状态改变', state)
|
||||
useMitt.emit('wsConnectionStateChange', state)
|
||||
// 广播状态变化给其他窗口
|
||||
if (isMainWindow) {
|
||||
await emit('ws-state-change', state)
|
||||
}
|
||||
break
|
||||
}
|
||||
// 处理心跳响应
|
||||
@@ -363,16 +418,22 @@ class WS {
|
||||
if (this.#connectReady) {
|
||||
this.#send(params)
|
||||
} else {
|
||||
// 队列限制
|
||||
// 优化的队列管理
|
||||
if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
|
||||
console.warn('消息队列已满,正在丢弃最旧的消息')
|
||||
this.#tasks.shift()
|
||||
// 优先丢弃非关键消息
|
||||
const nonCriticalIndex = this.#tasks.findIndex(
|
||||
(task) => typeof task === 'object' && task.type !== 1 && task.type !== 2
|
||||
)
|
||||
if (nonCriticalIndex !== -1) {
|
||||
this.#tasks.splice(nonCriticalIndex, 1)
|
||||
console.warn('消息队列已满,丢弃非关键消息')
|
||||
} else {
|
||||
this.#tasks.shift()
|
||||
console.warn('消息队列已满,丢弃最旧消息')
|
||||
}
|
||||
}
|
||||
this.#tasks.push(params)
|
||||
}
|
||||
} else {
|
||||
// 子窗口通过事件发送消息到主窗口
|
||||
emit('ws-send', params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,18 +623,67 @@ class WS {
|
||||
return this.#connectionHealth
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 强制重新连接WebSocket
|
||||
forceReconnect() {
|
||||
console.log('[WebSocket] 强制重新初始化WebSocket连接')
|
||||
// 停止当前的重连计时器
|
||||
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
|
||||
worker.terminate()
|
||||
// 同时终止timer worker相关的心跳
|
||||
timerWorker.postMessage({
|
||||
type: 'stopPeriodicHeartbeat'
|
||||
})
|
||||
this.#tasks = []
|
||||
this.#processedMsgCache.clear()
|
||||
this.#connectReady = false
|
||||
// 清理 Tauri 事件监听器
|
||||
this.#tauriListener?.cleanup()
|
||||
|
||||
// 停止心跳
|
||||
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
|
||||
|
||||
// 重置重连计数并重新初始化
|
||||
worker.postMessage(JSON.stringify({ type: 'resetReconnectCount' }))
|
||||
|
||||
// 重新初始化连接
|
||||
this.initConnect()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
// 优化的资源清理顺序
|
||||
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
|
||||
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
|
||||
|
||||
// 同时终止timer worker相关的心跳
|
||||
timerWorker.postMessage({
|
||||
type: 'stopPeriodicHeartbeat'
|
||||
})
|
||||
|
||||
// 清理内存
|
||||
this.#tasks.length = 0 // 更高效的数组清空
|
||||
this.#processedMsgCache.clear()
|
||||
this.#connectReady = false
|
||||
|
||||
// 重置连接健康状态
|
||||
this.#connectionHealth = {
|
||||
isHealthy: true,
|
||||
lastPongTime: null,
|
||||
timeSinceLastPong: null
|
||||
}
|
||||
|
||||
// 清理 Tauri 事件监听器
|
||||
this.#tauriListener?.cleanup()
|
||||
this.#tauriListener = null
|
||||
|
||||
// 清理所有watch
|
||||
this.#unwatchFunctions.forEach((unwatch) => {
|
||||
try {
|
||||
unwatch()
|
||||
} catch (error) {
|
||||
console.warn('清理watch函数时出错:', error)
|
||||
}
|
||||
})
|
||||
this.#unwatchFunctions.length = 0
|
||||
|
||||
// 最后终止workers
|
||||
setTimeout(() => {
|
||||
worker.terminate()
|
||||
timerWorker.terminate()
|
||||
}, 100) // 给一点时间让消息处理完成
|
||||
} catch (error) {
|
||||
console.error('销毁WebSocket时出错:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -664,7 +664,7 @@ export const useChatStore = defineStore(
|
||||
expirationTimers.clear()
|
||||
}
|
||||
|
||||
// 在 useChatStore 中添加新方法
|
||||
// 更新未读消息计数
|
||||
const updateTotalUnreadCount = () => {
|
||||
// 使用 Array.from 确保遍历的是最新的 sessionList
|
||||
const totalUnread = Array.from(sessionList.value).reduce((total, session) => {
|
||||
@@ -683,9 +683,8 @@ export const useChatStore = defineStore(
|
||||
invoke('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
|
||||
}
|
||||
|
||||
// 在 useChatStore 中添加新方法
|
||||
// 清空所有会话的未读数
|
||||
const clearUnreadCount = () => {
|
||||
// 清空所有会话的未读数
|
||||
sessionList.value.forEach((session) => {
|
||||
session.unreadCount = 0
|
||||
})
|
||||
@@ -693,6 +692,50 @@ export const useChatStore = defineStore(
|
||||
updateTotalUnreadCount()
|
||||
}
|
||||
|
||||
// 重置当前聊天室的消息并刷新最新消息
|
||||
const resetAndRefreshCurrentRoomMessages = async () => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
// 保存当前房间ID,用于后续比较
|
||||
const requestRoomId = currentRoomId.value
|
||||
|
||||
try {
|
||||
// 1. 清空当前消息列表
|
||||
if (currentMessageMap.value) {
|
||||
currentMessageMap.value.clear()
|
||||
}
|
||||
|
||||
// 2. 重置消息加载状态
|
||||
if (currentMessageOptions.value) {
|
||||
currentMessageOptions.value = {
|
||||
isLast: false,
|
||||
isLoading: true,
|
||||
cursor: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清空回复映射
|
||||
if (currentReplyMap.value) {
|
||||
currentReplyMap.value.clear()
|
||||
}
|
||||
|
||||
// 4. 从服务器获取最新的消息(默认20条)
|
||||
await getMsgList(pageSize)
|
||||
|
||||
console.log('[Network] 已重置并刷新当前聊天室的消息列表')
|
||||
} catch (error) {
|
||||
console.error('[Network] 重置并刷新消息列表失败:', error)
|
||||
// 如果获取失败,确保重置加载状态
|
||||
if (currentRoomId.value === requestRoomId && currentMessageOptions.value) {
|
||||
currentMessageOptions.value = {
|
||||
isLast: false,
|
||||
isLoading: false,
|
||||
cursor: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getMsgIndex,
|
||||
chatMessageList,
|
||||
@@ -726,7 +769,8 @@ export const useChatStore = defineStore(
|
||||
recalledMessages,
|
||||
clearAllExpirationTimers,
|
||||
updateTotalUnreadCount,
|
||||
clearUnreadCount
|
||||
clearUnreadCount,
|
||||
resetAndRefreshCurrentRoomMessages
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -101,6 +101,9 @@ svg {
|
||||
// 危险样式
|
||||
--danger-text: #c14053;
|
||||
--danger-bg: #f6dfe3;
|
||||
// 警告样式
|
||||
--warning-text: #f7b668ff;
|
||||
--warning-bg: #fdeeddff;
|
||||
// 头像边框颜色
|
||||
--avatar-border-color: #fff;
|
||||
// hover头像样式
|
||||
@@ -197,7 +200,10 @@ html[data-theme='dark'] {
|
||||
// 危险样式
|
||||
--danger-text: #da8583;
|
||||
--danger-bg: #37292c;
|
||||
// 头像边框颜色
|
||||
// 警告样式
|
||||
--warning-text: #f4c375ff;
|
||||
--warning-bg: #402f1dff;
|
||||
// 头像边框颜色
|
||||
--avatar-border-color: #1b1b1b;
|
||||
// hover头像样式
|
||||
--avatar-hover-bg: rgba(30, 30, 30, 0.6);
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -22,6 +22,7 @@ declare module 'vue' {
|
||||
Emoji: typeof import('./../components/rightBox/renderMessage/Emoji.vue')['default']
|
||||
Emoticon: typeof import('./../components/rightBox/emoticon/index.vue')['default']
|
||||
FloatBlockList: typeof import('./../components/common/FloatBlockList.vue')['default']
|
||||
HInput: typeof import('./../components/common/HInput.vue')['default']
|
||||
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
|
||||
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
|
||||
LoadingSpinner: typeof import('./../components/common/LoadingSpinner.vue')['default']
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
background-color: #404040;
|
||||
color: #fff;
|
||||
"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="锁屏密码"
|
||||
show-password-on="click"
|
||||
type="password"
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
placeholder="填写公告,1~600字"
|
||||
:autosize="{ minRows: 20 }"
|
||||
maxlength="600"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
show-count
|
||||
autofocus />
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
class="border-(1px solid #90909080)"
|
||||
v-model:value="formData.email"
|
||||
placeholder="请输入您的邮箱"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
clearable />
|
||||
</n-form-item>
|
||||
|
||||
@@ -37,6 +41,10 @@
|
||||
class="border-(1px solid #90909080)"
|
||||
v-model:value="formData.imgCode"
|
||||
placeholder="请输入图片验证码"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
maxlength="5" />
|
||||
<n-image
|
||||
width="120"
|
||||
@@ -60,6 +68,10 @@
|
||||
class="border-(1px solid #90909080)"
|
||||
v-model:value="formData.emailCode"
|
||||
placeholder="请输入邮箱验证码"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
maxlength="6" />
|
||||
<n-button
|
||||
color="#13987f"
|
||||
@@ -98,6 +110,10 @@
|
||||
show-password-on="click"
|
||||
placeholder="请输入6-16位新密码"
|
||||
maxlength="16"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
minlength="6" />
|
||||
<n-flex vertical :size="4" class="space-y-4px">
|
||||
<Validation :value="passwordForm.password" message="密码长度为6-16位" :validator="validateMinLength" />
|
||||
@@ -123,6 +139,10 @@
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请再次输入密码"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
maxlength="16"
|
||||
minlength="6" />
|
||||
<n-flex vertical :size="4">
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
:maxlength="60"
|
||||
:count-graphemes="countGraphemes"
|
||||
show-count
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
type="textarea"
|
||||
placeholder="输入几句话,对TA说些什么吧" />
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
:maxlength="60"
|
||||
:count-graphemes="countGraphemes"
|
||||
show-count
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
type="textarea"
|
||||
placeholder="输入验证消息" />
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
:placeholder="searchPlaceholder[searchType]"
|
||||
:maxlength="20"
|
||||
round
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
@clear="handleClear">
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<template v-else-if="searchResults.length > 0">
|
||||
<p class="text-(12px #909090) mb-6px">搜索结果</p>
|
||||
|
||||
<n-scrollbar style="max-height: calc(100vh - 212px)">
|
||||
<n-scrollbar style="max-height: calc(100vh - 142px)">
|
||||
<template v-for="item in searchResults" :key="item.roomId">
|
||||
<n-flex
|
||||
align="center"
|
||||
|
||||
@@ -134,12 +134,6 @@ const sessionList = computed(() => {
|
||||
return (
|
||||
chatStore.sessionList
|
||||
.map((item) => {
|
||||
// 获取该会话的所有消息,避免重复转换
|
||||
const messages = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])
|
||||
// 获取最后一条消息
|
||||
const lastMsg = messages[messages.length - 1]
|
||||
let LastUserMsg = ''
|
||||
|
||||
// 获取最新的头像
|
||||
let latestAvatar = item.avatar
|
||||
if (item.type === RoomTypeEnum.SINGLE && item.id) {
|
||||
@@ -153,27 +147,26 @@ const sessionList = computed(() => {
|
||||
displayName = item.remark
|
||||
}
|
||||
|
||||
if (lastMsg) {
|
||||
// 使用 useAtMention hook 检查是否有@我的消息
|
||||
const { checkRoomAtMe, formatMessageContent, getMessageSenderName } = useReplaceMsg()
|
||||
// 获取该会话的所有消息用于检查@我
|
||||
const messages = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])
|
||||
const { checkRoomAtMe, getAtMeContent, getMessageSenderName, formatMessageContent } = useReplaceMsg()
|
||||
// 检查是否有@我的消息
|
||||
const isAtMe = checkRoomAtMe(item.roomId, item.type, currentSession.value.roomId, messages, item.unreadCount)
|
||||
|
||||
// 获取发送者信息
|
||||
const senderName = getMessageSenderName(lastMsg)
|
||||
// 处理显示消息
|
||||
let displayMsg = ''
|
||||
|
||||
// 检查是否有@我的消息
|
||||
const isAtMe = checkRoomAtMe(item.roomId, item.type, currentSession.value.roomId, messages, item.unreadCount)
|
||||
|
||||
// 使用封装后的方法处理消息内容,包括撤回消息和@提醒
|
||||
LastUserMsg = formatMessageContent(lastMsg, item.type, senderName, isAtMe)
|
||||
|
||||
// 返回带有isAtMe标记的对象和修改后的名称
|
||||
return {
|
||||
...item,
|
||||
avatar: latestAvatar,
|
||||
name: displayName, // 使用可能修改过的显示名称
|
||||
lastMsg: LastUserMsg || item.text || '欢迎使用HuLa',
|
||||
lastMsgTime: formatTimestamp(item?.activeTime),
|
||||
isAtMe: isAtMe
|
||||
// 优先使用session.text作为内容来源
|
||||
if (item.text) {
|
||||
// 如果有@我,对text应用[有人@我]的样式
|
||||
displayMsg = isAtMe ? getAtMeContent(true, item.text) : item.text
|
||||
}
|
||||
// 如果没有text,则尝试从消息列表中获取
|
||||
else if (messages.length > 0) {
|
||||
const lastMsg = messages[messages.length - 1]
|
||||
if (lastMsg) {
|
||||
const senderName = getMessageSenderName(lastMsg)
|
||||
displayMsg = formatMessageContent(lastMsg, item.type, senderName, isAtMe)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +175,9 @@ const sessionList = computed(() => {
|
||||
...item,
|
||||
avatar: latestAvatar,
|
||||
name: displayName, // 使用可能修改过的显示名称
|
||||
lastMsg: item.text || '欢迎使用HuLa',
|
||||
lastMsg: displayMsg || '欢迎使用HuLa',
|
||||
lastMsgTime: formatTimestamp(item?.activeTime),
|
||||
isAtMe: false
|
||||
isAtMe
|
||||
}
|
||||
})
|
||||
// 添加排序逻辑:先按置顶状态排序,再按活跃时间排序
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
:placeholder="accountPH"
|
||||
@focus="accountPH = ''"
|
||||
@blur="accountPH = '邮箱/HuLa账号'"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
clearable>
|
||||
<template #suffix>
|
||||
<n-flex v-if="loginHistories.length > 0" @click="arrowStatus = !arrowStatus">
|
||||
@@ -65,6 +69,10 @@
|
||||
show-password-on="click"
|
||||
v-model:value="info.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="passwordPH"
|
||||
@focus="passwordPH = ''"
|
||||
@blur="passwordPH = '输入HuLa密码'"
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.apiIp"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="127.0.0.1或hulaspark.com" />
|
||||
|
||||
<p class="text-12px pt-6px">端口</p>
|
||||
@@ -27,12 +31,20 @@
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.apiPort"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="443" />
|
||||
<p class="text-12px pt-6px">后缀</p>
|
||||
<n-input
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.apiSuffix"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="api" />
|
||||
</n-flex>
|
||||
</n-collapse-transition>
|
||||
@@ -52,6 +64,10 @@
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.wsIp"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="127.0.0.1或hulaspark.com" />
|
||||
|
||||
<p class="text-12px pt-6px">端口</p>
|
||||
@@ -59,12 +75,20 @@
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.wsPort"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="443" />
|
||||
<p class="text-12px pt-6px">后缀</p>
|
||||
<n-input
|
||||
class="rounded-6px text-12px"
|
||||
v-model:value="savedProxy.wsSuffix"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="websocket" />
|
||||
</n-flex>
|
||||
</n-collapse-transition>
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
size="large"
|
||||
v-model:value="info.name"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="showNamePrefix ? '' : placeholders.name"
|
||||
@focus="handleInputState($event, 'name')"
|
||||
@@ -40,6 +44,10 @@
|
||||
maxlength="16"
|
||||
minlength="6"
|
||||
size="large"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
show-password-on="click"
|
||||
v-model:value="info.password"
|
||||
type="password"
|
||||
@@ -60,6 +68,10 @@
|
||||
maxlength="16"
|
||||
minlength="6"
|
||||
size="large"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
show-password-on="click"
|
||||
v-model:value="confirmPassword"
|
||||
type="password"
|
||||
@@ -123,6 +135,10 @@
|
||||
<n-input
|
||||
style="width: 140px"
|
||||
size="large"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
v-model:value="info.code"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="showCodePrefix ? '' : placeholders.code"
|
||||
|
||||
@@ -24,17 +24,20 @@ const checkAllTimersCompleted = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加调试信息打印函数
|
||||
// 优化的调试信息打印函数
|
||||
const logDebugInfo = (msgId: number, remainingTime: number) => {
|
||||
// 只有开启日志功能时才打印
|
||||
if (enableLogging) {
|
||||
// 只有开启日志功能时才打印,且只在关键时间点打印
|
||||
if (enableLogging && (remainingTime <= 5000 || remainingTime % 10000 < 1000)) {
|
||||
console.log(`[Worker Debug] 消息ID: ${msgId}, 剩余时间: ${(remainingTime / 1000).toFixed(1)}秒`)
|
||||
self.postMessage({
|
||||
type: 'debug',
|
||||
msgId,
|
||||
remainingTime,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
// 减少向主线程发送调试消息的频率
|
||||
if (remainingTime <= 3000) {
|
||||
self.postMessage({
|
||||
type: 'debug',
|
||||
msgId,
|
||||
remainingTime,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +106,8 @@ self.onmessage = (e) => {
|
||||
// 立即打印一次初始状态
|
||||
logDebugInfo(msgId, duration)
|
||||
|
||||
// 创建定时打印的间隔器,每1000ms打印一次
|
||||
// 优化的调试间隔器,减少打印频率
|
||||
const debugInterval = duration > 10000 ? 5000 : 1000 // 长任务每5秒打印,短任务每秒打印
|
||||
const debugId = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const remaining = duration - elapsed
|
||||
@@ -112,7 +116,7 @@ self.onmessage = (e) => {
|
||||
} else {
|
||||
clearInterval(debugId)
|
||||
}
|
||||
}, 1000) // 每秒打印一次
|
||||
}, debugInterval)
|
||||
|
||||
const timerId = setTimeout(() => {
|
||||
clearInterval(debugId)
|
||||
|
||||
@@ -5,9 +5,20 @@ const postMsg = ({ type, value }: { type: string; value?: object }) => {
|
||||
self.postMessage(JSON.stringify({ type, value }))
|
||||
}
|
||||
|
||||
// 连接状态
|
||||
let connectionState = ConnectionState.DISCONNECTED
|
||||
|
||||
// 最后一次收到pong消息的时间
|
||||
let lastPongTime: number | null = null
|
||||
|
||||
// 连续心跳失败计数(未收到pong响应)
|
||||
let consecutiveHeartbeatFailures = 0
|
||||
// 最大允许的连续心跳失败次数,超过这个值会触发重连
|
||||
const MAX_HEARTBEAT_FAILURES = 3
|
||||
|
||||
// 心跳日志记录开关
|
||||
let heartbeatLoggingEnabled = false
|
||||
|
||||
// ws instance
|
||||
let connection: WebSocket
|
||||
|
||||
@@ -22,6 +33,9 @@ let clientId: null | string = null
|
||||
|
||||
let serverUrl: null | string = null
|
||||
|
||||
// 心跳状态
|
||||
let heartbeatActive = false
|
||||
|
||||
// 往 ws 发消息
|
||||
const connectionSend = (value: object) => {
|
||||
connection?.send(JSON.stringify(value))
|
||||
@@ -32,32 +46,131 @@ let heartbeatTimeout: string | null = null
|
||||
const HEARTBEAT_TIMEOUT = 15000 // 15秒超时
|
||||
const HEARTBEAT_INTERVAL = 9900 // 心跳间隔
|
||||
|
||||
// 上次发送心跳的时间
|
||||
let lastPingSent: number | null = null
|
||||
|
||||
// 健康检查间隔
|
||||
const HEALTH_CHECK_INTERVAL = 30000 // 30秒检查一次连接健康状态
|
||||
|
||||
// 健康检查定时器ID
|
||||
let healthCheckTimerId: string | null = null
|
||||
|
||||
// 发送心跳请求,使用timer.worker
|
||||
const sendHeartPack = () => {
|
||||
// 启动健康检查定时器
|
||||
startHealthCheck()
|
||||
|
||||
// 标记心跳活跃
|
||||
heartbeatActive = true
|
||||
|
||||
// 请求主线程启动心跳定时器
|
||||
postMsg({
|
||||
type: 'startHeartbeatTimer',
|
||||
value: { interval: HEARTBEAT_INTERVAL }
|
||||
})
|
||||
|
||||
// 记录日志
|
||||
logHeartbeat('心跳定时器已启动')
|
||||
}
|
||||
|
||||
// 启动定期健康检查
|
||||
const startHealthCheck = () => {
|
||||
// 清除之前的健康检查定时器
|
||||
if (healthCheckTimerId) {
|
||||
postMsg({
|
||||
type: 'clearTimer',
|
||||
value: { msgId: healthCheckTimerId }
|
||||
})
|
||||
healthCheckTimerId = null
|
||||
}
|
||||
|
||||
// 设置新的健康检查定时器
|
||||
const timerId = `health_check_${Date.now()}`
|
||||
healthCheckTimerId = timerId
|
||||
postMsg({
|
||||
type: 'startTimer',
|
||||
value: { msgId: timerId, duration: HEALTH_CHECK_INTERVAL }
|
||||
})
|
||||
|
||||
logHeartbeat('健康检查定时器已启动')
|
||||
}
|
||||
|
||||
// 心跳日志记录
|
||||
const logHeartbeat = (message: string, data?: any) => {
|
||||
if (heartbeatLoggingEnabled) {
|
||||
console.log(`[WebSocket心跳] ${message}`, data || '')
|
||||
postMsg({
|
||||
type: 'heartbeatLog',
|
||||
value: { message, data, timestamp: Date.now() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 发送单次心跳
|
||||
const sendSingleHeartbeat = () => {
|
||||
// 心跳消息类型 2
|
||||
connectionSend({ type: 2 })
|
||||
const pingTime = Date.now()
|
||||
// 检查WebSocket连接状态
|
||||
if (connection?.readyState !== WebSocket.OPEN) {
|
||||
logHeartbeat('尝试发送心跳时发现连接未打开', { readyState: connection?.readyState })
|
||||
tryReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// 检测连接健康状态
|
||||
// 记录本次发送心跳时间
|
||||
lastPingSent = Date.now()
|
||||
|
||||
// 心跳消息类型 2
|
||||
try {
|
||||
connectionSend({ type: 2 })
|
||||
logHeartbeat('心跳已发送', { timestamp: lastPingSent })
|
||||
} catch (err) {
|
||||
logHeartbeat('心跳发送失败', { error: err })
|
||||
// 发送失败,可能连接已经中断但状态未更新
|
||||
tryReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// 优化的连接健康检测机制
|
||||
if (lastPongTime !== null) {
|
||||
const timeSinceLastPong = pingTime - lastPongTime
|
||||
const isConnectionHealthy = timeSinceLastPong < HEARTBEAT_INTERVAL * 2
|
||||
const timeSinceLastPong = lastPingSent - lastPongTime
|
||||
const healthThreshold = HEARTBEAT_INTERVAL * 2.5 // 增加容错时间
|
||||
const isConnectionHealthy = timeSinceLastPong < healthThreshold
|
||||
|
||||
// 如果连接不健康,通知主线程
|
||||
if (!isConnectionHealthy) {
|
||||
postMsg({
|
||||
type: WorkerMsgEnum.ERROR,
|
||||
value: { msg: '连接响应较慢,可能存在网络问题', timeSinceLastPong }
|
||||
})
|
||||
consecutiveHeartbeatFailures++
|
||||
|
||||
// 只在关键阈值时记录日志,减少日志开销
|
||||
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
|
||||
logHeartbeat('连接响应缓慢', {
|
||||
consecutiveFailures: consecutiveHeartbeatFailures,
|
||||
timeSinceLastPong
|
||||
})
|
||||
}
|
||||
|
||||
// 延迟错误通知,避免频繁触发
|
||||
if (consecutiveHeartbeatFailures >= 2) {
|
||||
postMsg({
|
||||
type: WorkerMsgEnum.ERROR,
|
||||
value: {
|
||||
msg: '连接响应较慢,可能存在网络问题',
|
||||
timeSinceLastPong,
|
||||
consecutiveFailures: consecutiveHeartbeatFailures
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 连续失败次数过多,尝试重连
|
||||
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
|
||||
logHeartbeat('连续心跳失败次数过多,触发重连', { consecutiveFailures: consecutiveHeartbeatFailures })
|
||||
tryReconnect()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 重置连续失败计数
|
||||
if (consecutiveHeartbeatFailures > 0) {
|
||||
logHeartbeat('心跳恢复正常', { previousFailures: consecutiveHeartbeatFailures })
|
||||
consecutiveHeartbeatFailures = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +192,16 @@ const sendSingleHeartbeat = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
const updateConnectionState = (newState: ConnectionState) => {
|
||||
connectionState = newState
|
||||
postMsg({ type: 'connectionStateChange', value: { state: connectionState } })
|
||||
}
|
||||
|
||||
// 清除心跳定时器
|
||||
const clearHeartPackTimer = () => {
|
||||
logHeartbeat('清除心跳定时器')
|
||||
heartbeatActive = false
|
||||
postMsg({ type: 'stopHeartbeatTimer' })
|
||||
|
||||
// 清除超时定时器
|
||||
@@ -91,13 +212,55 @@ const clearHeartPackTimer = () => {
|
||||
})
|
||||
heartbeatTimeout = null
|
||||
}
|
||||
|
||||
// 清除健康检查定时器
|
||||
if (healthCheckTimerId) {
|
||||
postMsg({
|
||||
type: 'clearTimer',
|
||||
value: { msgId: healthCheckTimerId }
|
||||
})
|
||||
healthCheckTimerId = null
|
||||
}
|
||||
}
|
||||
|
||||
// 主动尝试重连
|
||||
const tryReconnect = () => {
|
||||
logHeartbeat('触发主动重连')
|
||||
|
||||
// 主动关闭当前连接
|
||||
connection?.close()
|
||||
|
||||
// 重置心跳状态
|
||||
heartbeatActive = false
|
||||
|
||||
// 清除心跳定时器
|
||||
clearHeartPackTimer()
|
||||
|
||||
// 触发重连流程
|
||||
updateConnectionState(ConnectionState.RECONNECTING)
|
||||
if (!lockReconnect) {
|
||||
lockReconnect = true
|
||||
|
||||
// 使用短延迟立即重连
|
||||
postMsg({
|
||||
type: 'startReconnectTimer',
|
||||
value: {
|
||||
delay: 1000, // 快速重连,1秒后
|
||||
reconnectCount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 优化的智能退避算法
|
||||
const getBackoffDelay = (retryCount: number) => {
|
||||
const baseDelay = 1000 // 基础延迟1秒
|
||||
const maxDelay = 30000 // 最大延迟30秒
|
||||
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay)
|
||||
return delay + Math.random() * 1000 // 添加随机抖动
|
||||
const maxDelay = 15000 // 减少最大延迟到15秒
|
||||
const multiplier = Math.min(1.5, 2 - retryCount * 0.1)
|
||||
const delay = Math.min(baseDelay * Math.pow(multiplier, retryCount), maxDelay)
|
||||
|
||||
// 减少随机抖动范围
|
||||
return delay + Math.random() * 500
|
||||
}
|
||||
|
||||
const onCloseHandler = () => {
|
||||
@@ -147,8 +310,18 @@ const onConnectClose = () => {
|
||||
// ws 连接成功
|
||||
const onConnectOpen = () => {
|
||||
console.log('✅ WebSocket 连接成功')
|
||||
// 重置心跳相关状态
|
||||
consecutiveHeartbeatFailures = 0
|
||||
lastPongTime = null
|
||||
lastPingSent = null
|
||||
|
||||
updateConnectionState(ConnectionState.CONNECTED)
|
||||
postMsg({ type: WorkerMsgEnum.OPEN })
|
||||
|
||||
// 连接成功后立即发送一次心跳
|
||||
sendSingleHeartbeat()
|
||||
|
||||
// 然后开始定期心跳
|
||||
sendHeartPack()
|
||||
}
|
||||
// ws 连接 接收到消息
|
||||
@@ -159,11 +332,42 @@ const onConnectMsg = (e: any) => {
|
||||
if (data && (data.type === 'pong' || data.type === 3)) {
|
||||
// 3是pong的消息类型
|
||||
lastPongTime = Date.now()
|
||||
|
||||
// 计算心跳往返时间
|
||||
let roundTripTime = null
|
||||
if (lastPingSent) {
|
||||
roundTripTime = lastPongTime - lastPingSent
|
||||
}
|
||||
|
||||
// 重置连续失败计数
|
||||
if (consecutiveHeartbeatFailures > 0) {
|
||||
logHeartbeat('收到pong响应,重置连续失败计数', {
|
||||
previousFailures: consecutiveHeartbeatFailures,
|
||||
roundTripTime
|
||||
})
|
||||
consecutiveHeartbeatFailures = 0
|
||||
} else {
|
||||
logHeartbeat('收到pong响应', { roundTripTime })
|
||||
}
|
||||
|
||||
// 告知主线程收到了pong
|
||||
postMsg({ type: 'pongReceived', value: { timestamp: lastPongTime } })
|
||||
postMsg({
|
||||
type: 'pongReceived',
|
||||
value: {
|
||||
timestamp: lastPongTime,
|
||||
roundTripTime,
|
||||
consecutiveFailures: consecutiveHeartbeatFailures
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
// 解析失败则当作普通消息处理
|
||||
logHeartbeat('解析消息失败', { error: err })
|
||||
}
|
||||
|
||||
// 如果收到任何消息,说明连接是有效的,更新连接状态
|
||||
if (connectionState !== ConnectionState.CONNECTED) {
|
||||
updateConnectionState(ConnectionState.CONNECTED)
|
||||
}
|
||||
|
||||
// 转发消息给主线程
|
||||
@@ -196,12 +400,18 @@ const initConnection = () => {
|
||||
connection.addEventListener('error', onConnectError)
|
||||
}
|
||||
|
||||
let connectionState = ConnectionState.DISCONNECTED
|
||||
// 停止所有心跳相关活动
|
||||
const stopAllHeartbeat = () => {
|
||||
console.log('停止所有心跳活动')
|
||||
heartbeatActive = false
|
||||
clearHeartPackTimer()
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
const updateConnectionState = (newState: ConnectionState) => {
|
||||
connectionState = newState
|
||||
postMsg({ type: 'connectionStateChange', value: { state: connectionState } })
|
||||
// 重置重连状态
|
||||
const resetReconnection = () => {
|
||||
reconnectCount = 0
|
||||
lockReconnect = false
|
||||
console.log('重置重连计数和状态')
|
||||
}
|
||||
|
||||
self.onmessage = (e: MessageEvent<string>) => {
|
||||
@@ -250,6 +460,22 @@ self.onmessage = (e: MessageEvent<string>) => {
|
||||
postMsg({ type: 'heartbeatTimeout' })
|
||||
break
|
||||
}
|
||||
// 停止心跳
|
||||
case 'stopHeartbeat': {
|
||||
stopAllHeartbeat()
|
||||
break
|
||||
}
|
||||
// 重置重连计数
|
||||
case 'resetReconnectCount': {
|
||||
resetReconnection()
|
||||
break
|
||||
}
|
||||
// 清除重连计时器
|
||||
case 'clearReconnectTimer': {
|
||||
lockReconnect = true // 锁定重连,阻止旧的重连流程
|
||||
console.log('清除重连计时器')
|
||||
break
|
||||
}
|
||||
// 页面可见性变化
|
||||
case 'visibilityChange': {
|
||||
const { isHidden } = value
|
||||
@@ -267,15 +493,78 @@ self.onmessage = (e: MessageEvent<string>) => {
|
||||
case 'checkConnectionHealth': {
|
||||
const now = Date.now()
|
||||
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
|
||||
|
||||
// 连续失败次数也是健康状态的一个指标
|
||||
const healthStatus = {
|
||||
isHealthy,
|
||||
lastPongTime,
|
||||
lastPingSent,
|
||||
connectionState,
|
||||
heartbeatActive,
|
||||
consecutiveFailures: consecutiveHeartbeatFailures,
|
||||
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
|
||||
readyState: connection?.readyState
|
||||
}
|
||||
|
||||
logHeartbeat('健康检查', healthStatus)
|
||||
|
||||
// 如果连接不健康但状态显示已连接,尝试修复
|
||||
if (!isHealthy && connection?.readyState === WebSocket.OPEN && heartbeatActive) {
|
||||
logHeartbeat('健康检查发现异常,尝试恢复心跳', healthStatus)
|
||||
// 立即发送一次心跳
|
||||
sendSingleHeartbeat()
|
||||
}
|
||||
|
||||
// 如果心跳应该活跃但心跳定时器未运行,重启心跳
|
||||
if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
|
||||
logHeartbeat('发现心跳停止但连接正常,重启心跳', healthStatus)
|
||||
sendHeartPack()
|
||||
}
|
||||
|
||||
postMsg({
|
||||
type: 'connectionHealthStatus',
|
||||
value: {
|
||||
isHealthy,
|
||||
lastPongTime,
|
||||
timeSinceLastPong: lastPongTime ? now - lastPongTime : null
|
||||
}
|
||||
value: healthStatus
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// 健康检查定时器触发
|
||||
case 'healthCheckTimeout': {
|
||||
// 定期健康检查触发
|
||||
const { msgId } = value
|
||||
if (msgId === healthCheckTimerId) {
|
||||
// 执行健康检查
|
||||
const now = Date.now()
|
||||
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
|
||||
|
||||
logHeartbeat('定期健康检查', {
|
||||
isHealthy,
|
||||
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
|
||||
heartbeatActive,
|
||||
readyState: connection?.readyState
|
||||
})
|
||||
|
||||
// 如果不健康且连接状态异常,尝试重连
|
||||
if (!isHealthy && consecutiveHeartbeatFailures >= 1) {
|
||||
logHeartbeat('定期健康检查发现连接异常,尝试重连')
|
||||
tryReconnect()
|
||||
}
|
||||
// 如果心跳定时器应该在运行但实际没有运行,重启心跳
|
||||
else if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
|
||||
logHeartbeat('发现心跳停止但连接正常,重启心跳')
|
||||
sendHeartPack()
|
||||
}
|
||||
// 继续启动下一次健康检查
|
||||
startHealthCheck()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 控制心跳日志记录
|
||||
case 'setHeartbeatLogging': {
|
||||
heartbeatLoggingEnabled = !!value.enabled
|
||||
logHeartbeat(`心跳日志${heartbeatLoggingEnabled ? '已开启' : '已关闭'}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user